diff --git a/content/_data/constants.yml b/content/_data/constants.yml index c661ec3b47d7..647bc8a7deed 100644 --- a/content/_data/constants.yml +++ b/content/_data/constants.yml @@ -28,6 +28,15 @@ header_nav: - title: Search page: pages/search.md +cookie_consent: + title: We Use Cookies + chapters: + - text: |- + We value your privacy. The FiQCI webpage uses functional cookies and anonymous analytics cookies. + By clicking "Accept", you consent to the use of these cookies. + - text: |- + The data collected is non-identifiable and cannot be traced back to you. + #Following are constants per page. Can be accessed with page.url "/": hero: @@ -233,3 +242,93 @@ header_nav: basis: "PRX, CZ" topology: "Square grid" device_id: "Q50" + +"/cookies/": + title: Cookie Policy + desc: |- + This is the cookie policy for FiQCI, accessible from https://fiqci.fi/cookies/ + + general: + - When you visit our website, cookies will save information about your stay. Cookies are small text files that are placed on your device. + There are two main types of cookies; session cookies and persistent cookies. Session cookies will be removed from your device as soon + as you close your browser. Persistent cookies will remain stored until they are deleted or expire. + + - The data collected by the cookies is used to track the trends in our site usage. No personal data is collected. + This way, we can improve the usability as well as identify the most interesting areas of our website. + + - Cookies help us get an overview of your visit to our website so we can get an idea of how many visitors we have, which pages are visited, + and how long visitors stay on our website. We use this information to improve our website and make it more user-friendly. + All data collected is anonymous and cannot be used to identify you as an individual. + + - The FiQCI webpages uses anonymous cookies to collect data about user behavior through Matomo. + This information helps us improve our services and provide a better user experience by telling us how users use the site. + The data collected is non-identifiable and cannot be traced back to you. + + - Additionally we use mandatory functional cookies to ensure the website functions properly. + These cookies do not collect any information about you and only help the website function. + They are only valid for the current session and are deleted when you close your browser. + + delete: + title: How to Delete Cookies + desc: |- + The cookies can be deleted by clearing your browser's cache and cookies. You can also manage your cookie preferences through + your browser settings. You can also simply refuse cookie consent through the cookie consent banner that appears when you first visit our website. + + change: + title: How to Change Consent + desc: |- + If you have already accepted/declined cookies, you can change your preferences at any time by clicking below. + + lifetime: + title: Cookie Lifetime + desc: |- + The length of time a cookie is stored on your devices and browsers varies.The lifetime is calculated according to your last visit + to the website. When a cookie expires, it is automatically deleted. All our cookies' lifetimes are specified in our cookie policy. + + cookies: + title: Cookies Used + types: + - name: "Functional Cookies" + desc: |- + These cookies are necessary for the website to function properly. + lifetime: Session + - name: "Analytics Cookies" + desc: |- + These cookies help us understand how visitors interact with our website by collecting and reporting information anonymously. + lifetime: 1 year + +"/accessibility/": + title: Accessibility Statement + desc: |- + This is the accessibility statement for FiQCI, accessible from https://fiqci.fi/accessibility/. + Updated on 19.6.2025 + + general: + - The FiQCI website is designed to be accessible to all users. + We strive to comply with the Web Content Accessibility Guidelines (WCAG) 2.1 Level AA standards. + If you encounter any accessibility issues while using our website, please contact us at + + status: + title: Compliance Status + desc: |- + Meets all critical accessibility requirements. + + complaint: + title: Reporting Accessibility Issues + desc: |- + If you notice accessibility problems on the website, start by giving feedback to us, that is, + the website administrator. Receiving a response may take 14 days. If you are not satisfied with + the response from us or if you do not receive any response within two weeks, you may file a report + with the + link: + title: Regional State Administrative Agency for Southern Finland + href: https://www.saavutettavuusvaatimukset.fi/en/user-rights/submit-complaint-web-accessibility-or-request-clarification + + contact: + title: Supervisory Authority Contact Information + info: + name: Liikenne- ja viestintävirasto Traficom + division: Digitaalisen esteettömyyden ja saavutettavuuden valvontayksikkö + web: www.saavutettavuusvaatimukset.fi + email: saavutettavuus@traficom.fi + phone: 029 534 5000 diff --git a/content/_layouts/home.html b/content/_layouts/home.html index 3557a58d1660..0997aabcab94 100644 --- a/content/_layouts/home.html +++ b/content/_layouts/home.html @@ -72,7 +72,7 @@

FiQCI Mission

-

FiQCI Mission

+

FiQCI Mission

diff --git a/content/_layouts/post.html b/content/_layouts/post.html index 8ca962d171c2..59010c693d24 100644 --- a/content/_layouts/post.html +++ b/content/_layouts/post.html @@ -45,7 +45,7 @@

Give feedback!

{%- include react/root.html id='references-accordion' -%}
-
+
{%- include react/root.html id='read-next' -%}
diff --git a/content/pages/about.md b/content/pages/about.md index e55c5bd14818..567d28713c92 100644 --- a/content/pages/about.md +++ b/content/pages/about.md @@ -7,7 +7,7 @@ react: true {% assign about_data = site.data.constants.["/about/"] %} -
+

About FiQCI

{{ about_data.desc }}

diff --git a/content/pages/accessibility.md b/content/pages/accessibility.md new file mode 100644 index 000000000000..80f2b7067def --- /dev/null +++ b/content/pages/accessibility.md @@ -0,0 +1,42 @@ +--- +layout: page +title: Accessibility Statement +subtitle: Accessibility Statement +react: true +--- + +{% assign accessibility_data = site.data.constants.["/accessibility/"] %} + +
+
+
+

{{ accessibility_data.title }}

+

{{ accessibility_data.general }} {{ site.data.constants.feedback_email }}

+ +

{{ accessibility_data.status.title }}

+

{{ accessibility_data.status.desc }}

+ +

{{ accessibility_data.complaint.title }}

+

{{ accessibility_data.complaint.desc }} {{ accessibility_data.complaint.link.title }}

+ +

{{ accessibility_data.complaint.contact.title }}

+ {% assign contact = accessibility_data.complaint.contact.info %} + {% assign contact_labels = "name:Name,division:Division,web:Website,email:Email,phone:Phone" | split: "," %} + {% for pair in contact_labels %} + {% assign parts = pair | split: ":" %} + {% assign key = parts[0] %} + {% assign label = parts[1] %} + {% if contact[key] %} +

{{ label }}: + {% if key == "web" %} + {{ contact[key] }} + {% elsif key == "email" %} + {{ contact[key] }} + {% else %} + {{ contact[key] }} + {% endif %} +

+ {% endif %} + {% endfor %} +
+
\ No newline at end of file diff --git a/content/pages/cookies.md b/content/pages/cookies.md new file mode 100644 index 000000000000..e1f4feb7cc41 --- /dev/null +++ b/content/pages/cookies.md @@ -0,0 +1,46 @@ +--- +layout: page +title: Cookie Policy +subtitle: Cookie policy +react: true +--- + +{% assign cookie_data = site.data.constants.["/cookies/"] %} + +
+
+
+

{{ cookie_data.title }}

+ {% for i in cookie_data.general %} +

{{ i }}

+ {% endfor %} + +

{{ cookie_data.delete.title }}

+

{{ cookie_data.delete.desc }}

+ +

{{ cookie_data.change.title }}

+

{{ cookie_data.change.desc }}

+
+ {%- include react/root.html id='open-cookie-modal' -%} +
+ +

{{ cookie_data.lifetime.title }}

+

{{ cookie_data.lifetime.desc }}

+ +

{{ cookie_data.cookies.title }}

+ + {% for i in cookie_data.cookies.types %} +

{{ i.name }}

+
+
+

Description:

+

{{ i.desc }}

+
+
+

Lifetime:

+

{{ i.lifetime }}

+
+
+ {% endfor %} +
+
diff --git a/src/components/CookieConsent.jsx b/src/components/CookieConsent.jsx new file mode 100644 index 000000000000..39169545f3c8 --- /dev/null +++ b/src/components/CookieConsent.jsx @@ -0,0 +1,171 @@ +import React, { useEffect, useState } from 'react' + +import { CModal, CCard, CCardTitle, CCardContent, CButton } from '@cscfi/csc-ui-react' + +export const CookieModal = props => { + const [isModalOpen, setIsModalOpen] = useState(false); + + const chapters = props.chapters || []; + + useEffect(() => { + const cookieConsent = document.cookie + .split('; ') + .find(row => row.startsWith('cookie_consent=')); + + if (!cookieConsent) { + setIsModalOpen(true); + return; + } + + const consentValue = cookieConsent.split('=')[1]; + if (consentValue === 'true' || consentValue === 'false') { + setIsModalOpen(false); + } else { + setIsModalOpen(true); + } + }, []); + + // Unified close handler + const closeModal = () => setIsModalOpen(false); + + const clickOutside = () => { + if (isModalOpen) { + closeModal(); + const expiryDate = new Date(); + expiryDate.setFullYear(expiryDate.getFullYear() + 1); + document.cookie = `cookie_consent=false; path=/; expires=${expiryDate.toUTCString()}`; + } + } + + const handleAcceptCookies = () => { + closeModal(); + const expiryDate = new Date(); + expiryDate.setFullYear(expiryDate.getFullYear() + 1); + document.cookie = `cookie_consent=true; path=/; expires=${expiryDate.toUTCString()}`; + } + const handleDeclineCookies = () => { + closeModal(); + const cookies = document.cookie.split(';'); + for (const cookie of cookies) { + const eqPos = cookie.indexOf('='); + const name = eqPos > -1 ? cookie.substr(0, eqPos).trim() : cookie.trim(); + // Only delete cookies for the rahtiapp domain + if (name.endsWith('.rahtiapp.fi')) { + document.cookie = `${name}=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/; domain=.rahtiapp.fi`; + } + } + const expiryDate = new Date(); + expiryDate.setFullYear(expiryDate.getFullYear() + 1); + document.cookie = `cookie_consent=false; path=/; expires=${expiryDate.toUTCString()}`; + } + + return ( + clickOutside()} + > + + Cookie consent + +

{props.title}

+ {chapters.map((chapter, index) => ( +
+

{chapter.text}

+
+ ))} + + Cookie policy +
+ Decline + + Accept + +
+
+
+
+ ); +}; + +export const CookieModalManual = props => { + const [isModalOpen, setIsModalOpen] = useState(false); + const closeModal = () => setIsModalOpen(false); + + const chapters = props.chapters || []; + + const handleAcceptCookies = () => { + closeModal(); + const expiryDate = new Date(); + expiryDate.setFullYear(expiryDate.getFullYear() + 1); + document.cookie = `cookie_consent=true; path=/; expires=${expiryDate.toUTCString()}`; + } + const handleDeclineCookies = () => { + closeModal(); + const cookies = document.cookie.split(';'); + for (const cookie of cookies) { + const eqPos = cookie.indexOf('='); + const name = eqPos > -1 ? cookie.substr(0, eqPos).trim() : cookie.trim(); + // Only delete cookies for the rahtiapp domain + if (name.endsWith('.rahtiapp.fi')) { + document.cookie = `${name}=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/; domain=.rahtiapp.fi`; + } + } + const expiryDate = new Date(); + expiryDate.setFullYear(expiryDate.getFullYear() + 1); + document.cookie = `cookie_consent=false; path=/; expires=${expiryDate.toUTCString()}`; + } + + return ( +
+ setIsModalOpen(true)}>Open Cookie Consent popup + closeModal()} + > + + Cookie consent + +

{props.title}

+ {chapters.map((chapter, index) => ( +
+

{chapter.text}

+
+ ))} + Cookie policy +
+ Decline + + Accept + +
+
+
+
+
+ + ); +}; diff --git a/src/components/Footer.jsx b/src/components/Footer.jsx index 7dc0bec4cbcb..02fb775e4473 100644 --- a/src/components/Footer.jsx +++ b/src/components/Footer.jsx @@ -19,7 +19,7 @@ export const Footer = (props) => { : <> return ( - +
); }; diff --git a/src/components/ReadNext.jsx b/src/components/ReadNext.jsx index 6549c3561220..4f0648924109 100644 --- a/src/components/ReadNext.jsx +++ b/src/components/ReadNext.jsx @@ -15,7 +15,7 @@ export const ReadNext = ({ title, blogs }) => {
-
+

Read next:

{SITE.publications.filter((blog) => blog.title !== title).slice(-5).map((blog, index) => (
diff --git a/src/components/SiteSearch.jsx b/src/components/SiteSearch.jsx index 191ceead8900..d73bb7dd0d99 100644 --- a/src/components/SiteSearch.jsx +++ b/src/components/SiteSearch.jsx @@ -345,9 +345,7 @@ export const SiteSearch = () => { // Try to load from localStorage on mount useEffect(() => { let params = new URLSearchParams(document.location.search); - console.log("URL params:", params); let initSearch = params.get("search"); - console.log("Initial search query:", initSearch); if (initSearch && initSearch.trim() !== "") { setQuery(initSearch); const searchResults = searchContent(initSearch, STORE); diff --git a/src/layouts/base.html.jsx b/src/layouts/base.html.jsx index c2cb6de9afe0..315607e0be13 100644 --- a/src/layouts/base.html.jsx +++ b/src/layouts/base.html.jsx @@ -1,4 +1,5 @@ -import React from 'react' +import React, { useEffect } from 'react' +import { useState } from 'react' import { createPortal } from 'react-dom' import { NavigationHeader } from '../components/NavigationHeader' @@ -6,6 +7,7 @@ import { Footer } from '../components/Footer' import { useMatomo } from '../hooks/useMatomo' +import { CookieModal } from '../components/CookieConsent' const Analytics = () => { const url = process.env.MATOMO_URL @@ -16,7 +18,10 @@ const Analytics = () => { return <> } + export const BaseLayout = props => { + + const [cookieConsentState, setCookieConsentState] = useState(null); const headerProps = { logo: props.logo, nav: props.header_nav @@ -28,8 +33,39 @@ export const BaseLayout = props => { copyright: props.copyright } + useEffect(() => { + + const cookieConsent = document.cookie + .split('; ') + .find(row => row.startsWith('cookie_consent=')); + + if (!cookieConsent) { + setCookieConsentState(null); + return; + } + + const consentValue = cookieConsent.split('=')[1]; + if (consentValue === 'true' || consentValue === 'false') { + setCookieConsentState(consentValue === 'true'); + if (consentValue === 'false') { + const cookies = document.cookie.split(';'); + for (const cookie of cookies) { + const eqPos = cookie.indexOf('='); + const name = eqPos > -1 ? cookie.substr(0, eqPos).trim() : cookie.trim(); + // Only delete cookies for the rahtiapp domain + if (name.endsWith('.rahtiapp.fi')) { + document.cookie = `${name}=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/; domain=.rahtiapp.fi`; + } + } + } + } else { + setCookieConsentState(null); + } + }, []); + return <> - + {window.location.pathname !== '/cookies/' && } + {cookieConsentState === true && } {createPortal( , document.getElementById('navigation-header') diff --git a/src/pages/accessibility.md.jsx b/src/pages/accessibility.md.jsx new file mode 100644 index 000000000000..ccedf32af77c --- /dev/null +++ b/src/pages/accessibility.md.jsx @@ -0,0 +1,22 @@ +import React from 'react' +import { createRoot } from 'react-dom/client' +import { createPortal } from 'react-dom' + +import { PageLayout } from '../layouts/page.html' + +import { useJsonApi } from '../hooks/useJsonApi' + + +const AccessibilityPage = () => { + const themeConstants = useJsonApi('api/theme/constants.json') + + return <> + + +} + +document.addEventListener('DOMContentLoaded', () => { + const root = createRoot(document.getElementById('react-root')) + + root.render() +}) diff --git a/src/pages/cookies.md.jsx b/src/pages/cookies.md.jsx new file mode 100644 index 000000000000..3b5289c7c15b --- /dev/null +++ b/src/pages/cookies.md.jsx @@ -0,0 +1,29 @@ +import React from 'react' +import { createRoot } from 'react-dom/client' +import { createPortal } from 'react-dom' + +import { PageLayout } from '../layouts/page.html' + +import { useJsonApi } from '../hooks/useJsonApi' + +import { CookieModal, CookieModalManual } from '../components/CookieConsent' + + +const CookiesPage = () => { + const themeConstants = useJsonApi('api/theme/constants.json') + const cookieConsentProps = themeConstants.cookie_consent + return <> + + + {createPortal( + , + document.getElementById('open-cookie-modal') + )} + +} + +document.addEventListener('DOMContentLoaded', () => { + const root = createRoot(document.getElementById('react-root')) + + root.render() +})