diff --git a/package.json b/package.json index 2b715854e..b5e88486f 100644 --- a/package.json +++ b/package.json @@ -30,6 +30,7 @@ "dependencies": { "@contentful/rich-text-html-renderer": "^16.0.5", "@contentful/rich-text-plain-text-renderer": "^16.0.7", + "@orestbida/iframemanager": "^1.3.0", "@reach/accordion": "^0.18.0", "@reach/listbox": "^0.18.0", "@reach/tabs": "^0.18.0", @@ -95,6 +96,7 @@ "stylis": "^4.3.1", "unified": "^10.1.1", "util": "^0.12.5", + "vanilla-cookieconsent": "^3.0.1", "webpack": "^5.84.1", "what-input": "^5.2.12" }, diff --git a/src/assets/locales/de/cookie-consent-translations.json b/src/assets/locales/de/cookie-consent-translations.json new file mode 100644 index 000000000..bd60e143a --- /dev/null +++ b/src/assets/locales/de/cookie-consent-translations.json @@ -0,0 +1,53 @@ +{ + "consentModal": { + "title": "Ihre Datenschutzoptionen", + "description": "Wir verwenden Cookies, um Ihre Erfahrung zu verbessern, den Datenverkehr zu analysieren und personalisierte Inhalte bereitzustellen. Sie können entweder alle Cookies akzeptieren oder Ihre Präferenzen für verschiedene Kategorien verwalten.", + "acceptAllBtn": "Alle Cookies akzeptieren", + "acceptNecessaryBtn": "Nur notwendige Cookies akzeptieren", + "showPreferencesBtn": "Cookie-Präferenzen verwalten" + }, + "preferencesModal": { + "title": "Verwalten Sie Ihre Cookie-Präferenzen", + "acceptAllBtn": "Alle Cookies akzeptieren", + "acceptNecessaryBtn": "Nur notwendige Cookies akzeptieren", + "savePreferencesBtn": "Präferenzen speichern", + "closeIconLabel": "Schließen", + "sections": [ + { + "title": "Verwendung von Cookies", + "description": "Unsere Website verwendet Cookies, um wesentliche Funktionen sicherzustellen, Ihr Surferlebnis zu verbessern und personalisierte Inhalte bereitzustellen. Einige Cookies sind erforderlich, wie z. B. der, der zur Speicherung Ihrer Zustimmungspräferenzen verwendet wird, während andere uns helfen, unsere Dienste zu verbessern." + }, + { + "title": "Leistungs- und Analyse-Cookies", + "description": "Leadinfo Cookies helfen uns zu analysieren, wie Besucher mit unserer Website interagieren, und ermöglichen es uns, die Leistung zu messen und zu verbessern. Sie sammeln Informationen in anonymer Form. YouTube kann Cookies setzen, um Videoaufrufe und Benutzerinteraktionen zu verfolgen.", + "linkedCategory": "analytics", + "cookieTable": { + "headers": { + "name": "Name", + "domain": "Dienst", + "description": "Beschreibung", + "expiration": "Ablauf" + }, + "body": [ + { + "name": "_li_id", + "domain": "Leadinfo.com", + "description": "Leadinfo platziert zwei Cookies, die Satellytes nur Einblicke in das Verhalten auf der Website geben. Diese Cookies werden nicht mit anderen Parteien geteilt. Datenschutzerklärung von Leadinfo", + "expiration": "_li_id wird für zwei Jahre gespeichert" + }, + { + "name": "_li_ses", + "domain": "Leadinfo.com", + "description": "Leadinfo platziert zwei Cookies, die Satellytes nur Einblicke in das Verhalten auf der Website geben. Diese Cookies werden nicht mit anderen Parteien geteilt. Datenschutzerklärung von Leadinfo", + "expiration": "_li_ses wird nur für die aktuelle Sitzung verwendet" + } + ] + } + }, + { + "title": "Weitere Informationen", + "description": "Bei Fragen zu unserer Cookie-Richtlinie oder Ihren Auswahlmöglichkeiten wenden Sie sich bitte an uns." + } + ] + } +} diff --git a/src/assets/locales/de/translations.json b/src/assets/locales/de/translations.json index eabab787a..9fd345fae 100644 --- a/src/assets/locales/de/translations.json +++ b/src/assets/locales/de/translations.json @@ -87,5 +87,8 @@ "blog.copied": "Kopiert", "blog.follow": "Blog abonnieren", "blog.pagination": "Seite", - "image.attribution": "Foto von " + "image.attribution": "Foto von ", + "iframemanager.youtube.notice": "Dieser Inhalt wird von einer externen Quelle gehostet. Durch das Laden des Inhalts akzeptieren Sie die Nutzungsbedingungen von youtube.com und stimmen einer möglichen Übertragung Ihrer Daten in Drittländer zu.", + "iframemanager.youtube.load-button": "Inhalt laden", + "iframemanager.youtube.load-all-button": "Inhalt immer laden" } diff --git a/src/assets/locales/en/cookie-consent-translations.json b/src/assets/locales/en/cookie-consent-translations.json new file mode 100644 index 000000000..b7e593de8 --- /dev/null +++ b/src/assets/locales/en/cookie-consent-translations.json @@ -0,0 +1,53 @@ +{ + "consentModal": { + "title": "Your Privacy Choices", + "description": "We use cookies to enhance your experience, analyze traffic, and deliver personalized content. You can choose to accept all cookies or manage your preferences for different categories.", + "acceptAllBtn": "Accept All Cookies", + "acceptNecessaryBtn": "Reject all", + "showPreferencesBtn": "Manage Cookie Preferences" + }, + "preferencesModal": { + "title": "Manage Your Cookie Preferences", + "acceptAllBtn": "Accept All Cookies", + "acceptNecessaryBtn": "Reject all", + "savePreferencesBtn": "Save Preferences", + "closeIconLabel": "Close", + "sections": [ + { + "title": "Cookie Usage", + "description": "Our website uses cookies to ensure essential functionality, enhance your browsing experience, and provide personalized content. Some cookies are required, such as the one used to store your consent preferences, while others help us improve our services." + }, + { + "title": "Performance and Analytics Cookies", + "description": "Leadinfo cookies help us analyze how visitors interact with our website, allowing us to measure and improve performance. They collect information in an anonymous form. YouTube may set cookies to track video views and user interactions.", + "linkedCategory": "analytics", + "cookieTable": { + "headers": { + "name": "Name", + "domain": "Service", + "description": "Description", + "expiration": "Expiration" + }, + "body": [ + { + "name": "_li_id", + "domain": "Leadinfo.com", + "description": "Leadinfo places two cookies that only provides Satellytes insights into the behaviour on the website. These cookies will not be shared with other parties. Privacy statement of Leadinfo", + "expiration": "_li_id will be saved for two years" + }, + { + "name": "_li_ses", + "domain": "Leadinfo.com", + "description": "Leadinfo places two cookies that only provides Satellytes insights into the behaviour on the website. These cookies will not be shared with other parties. Privacy statement of Leadinfo", + "expiration": "_li_ses is only used for the current session" + } + ] + } + }, + { + "title": "More information", + "description": "For any queries in relation to our policy on cookies and your choices, please contact us." + } + ] + } +} diff --git a/src/assets/locales/en/translations.json b/src/assets/locales/en/translations.json index 572f167e7..6f5f6c75d 100644 --- a/src/assets/locales/en/translations.json +++ b/src/assets/locales/en/translations.json @@ -87,5 +87,8 @@ "blog.copied": "Copied", "blog.follow": "Subscribe to blog", "blog.pagination": "Page", - "image.attribution": "Photo by " + "image.attribution": "Photo by ", + "iframemanager.youtube.notice": "This content is hosted by a third party. By showing the external content you accept the terms and conditions of youtube.com and agree to a possible transfer of your data to third countries.", + "iframemanager.youtube.load-button": "Load video", + "iframemanager.youtube.load-all-button": "Load & don't ask again" } diff --git a/src/components/cookie-consent.tsx b/src/components/cookie-consent.tsx new file mode 100644 index 000000000..2c0e7431b --- /dev/null +++ b/src/components/cookie-consent.tsx @@ -0,0 +1,89 @@ +import React, { useEffect } from 'react'; +import '@orestbida/iframemanager/dist/iframemanager.css'; +import '@orestbida/iframemanager/dist/iframemanager.js'; +import 'vanilla-cookieconsent/dist/cookieconsent.css'; +import * as CookieConsentLib from 'vanilla-cookieconsent'; +import enCookieConsentTranslations from '../assets/locales/en/cookie-consent-translations.json'; +import deCookieConsentTranslations from '../assets/locales/de/cookie-consent-translations.json'; +import { useTranslation } from 'react-i18next'; +import LeadinfoScript from './layout/leadinfo-script'; + +export const CookieConsent = () => { + const { t, i18n } = useTranslation(); + // @ts-expect-error - iframemanager is defined at runtime + const im = typeof iframemanager !== 'undefined' ? iframemanager() : null; + + // Code partially from https://cookieconsent.orestbida.com/advanced/iframemanager-setup.html + const iframeManagerOnChange = ({ changedServices, eventSource }) => { + if (eventSource.type === 'click') { + const servicesToAccept = [ + ...CookieConsentLib.getUserPreferences().acceptedServices['analytics'], + ...changedServices, + ]; + + CookieConsentLib.acceptService(servicesToAccept, 'analytics'); + } + }; + + const setupIframeManager = () => { + im.reset(); + im.run({ + currLang: i18n.language, + onChange: iframeManagerOnChange, + services: { + youtube: { + embedUrl: 'https://www.youtube-nocookie.com/embed/{data-id}', + thumbnailUrl: 'https://i3.ytimg.com/vi/{data-id}/hqdefault.jpg', + iframe: { + allow: + 'accelerometer; encrypted-media; gyroscope; picture-in-picture; fullscreen;', + }, + languages: { + en: { + notice: t('iframemanager.youtube.notice'), + loadBtn: t('iframemanager.youtube.load-button'), + loadAllBtn: t('iframemanager.youtube.load-all-button'), + }, + de: { + notice: t('iframemanager.youtube.notice'), + loadBtn: t('iframemanager.youtube.load-button'), + loadAllBtn: t('iframemanager.youtube.load-all-button'), + }, + }, + }, + }, + }); + }; + + const setupCookieConsent = () => { + CookieConsentLib.reset(); + CookieConsentLib.run({ + categories: { + analytics: { + services: { + youtube: { + label: 'YouTube Embeds', + onAccept: () => im.acceptService('youtube'), + onReject: () => im.rejectService('youtube'), + }, + }, + }, + }, + language: { + default: i18n.language, + translations: { + en: enCookieConsentTranslations, + de: deCookieConsentTranslations, + }, + }, + }).then(); + }; + + useEffect(() => { + if (!im) return; + setupIframeManager(); + setupCookieConsent(); + }, [i18n.language]); + + return ; +}; diff --git a/src/components/layout/layout.tsx b/src/components/layout/layout.tsx index c0c7cc979..d1907110d 100644 --- a/src/components/layout/layout.tsx +++ b/src/components/layout/layout.tsx @@ -13,10 +13,11 @@ import { up } from '../support/breakpoint'; import { Breadcrumb, BreadcrumbEntry } from './breadcrumb/breadcrumb'; import { setPolarityBodyClass } from './set-polarity'; import { useAnchorTagScrolling } from './use-anchor-tag-scrolling'; +import { CookieConsent } from '../cookie-consent'; /** * this container is used to push the footer to the bottom - * if the page content is to short + * if the page content is too short */ const FullHeightContainer = styled.div` display: flex; @@ -123,6 +124,7 @@ export const Layout = ({ return ( +
{ useEffect(() => { const script = document.createElement('script'); + script.setAttribute('type', 'text/plain'); + script.setAttribute('data-category', 'analytics'); + script.setAttribute('data-service', 'Leadinfo'); + script.innerHTML = ` (function(l,e,a,d,i,n,f,o){if(!l[i]){l.GlobalLeadinfoNamespace=l.GlobalLeadinfoNamespace||[]; l.GlobalLeadinfoNamespace.push(i);l[i]=function(){(l[i].q=l[i].q||[]).push(arguments)};l[i].t=l[i].t||n; @@ -12,7 +16,12 @@ const LeadinfoScript = () => { document.head.appendChild(script); return () => { - document.head.removeChild(script); + const existingScript = document.querySelector( + 'script[data-service="Leadinfo"]', + ); + if (existingScript) { + document.head.removeChild(existingScript); + } }; }, []); diff --git a/src/components/legacy/markdown/custom-components.tsx b/src/components/legacy/markdown/custom-components.tsx index e4f6a2d5e..94eed7097 100644 --- a/src/components/legacy/markdown/custom-components.tsx +++ b/src/components/legacy/markdown/custom-components.tsx @@ -214,6 +214,12 @@ const customSatellytesComponents = { ); }, p(props) { + const hasVideo = React.Children.toArray(props.children).some( + (child) => React.isValidElement(child) && child.props?.videoId, + ); + if (hasVideo) { + return <>{props.children}; + } return {props.children}; }, em(props) { diff --git a/src/components/legacy/markdown/youtube-embed.tsx b/src/components/legacy/markdown/youtube-embed.tsx index c69bdfcef..a12e9d382 100644 --- a/src/components/legacy/markdown/youtube-embed.tsx +++ b/src/components/legacy/markdown/youtube-embed.tsx @@ -1,96 +1,72 @@ -import React, { useContext } from 'react'; +import React from 'react'; import styled from 'styled-components'; -import { theme } from '../../layout/theme'; -import { Button } from '../../ui/buttons/button'; import { TextStyles } from '../../typography'; -import YouTubeConsentContext from '../../../context/youtube-consent-context'; -import { Link } from '../links/links'; +import { resetButton } from '../../support/css-helpers'; interface YoutubeEmbedProps { videoId: string; } -// how to make iframe responsive: https://stackoverflow.com/questions/17838607/making-an-iframe-responsive -const StyledIframe = styled.iframe` - position: absolute; - top: 0; - left: 0; - width: 100%; - height: 100%; -`; - const YoutubeEmbedWrapper = styled.div` position: relative; padding-bottom: 56.25%; /* 16:9 */ padding-top: 25px; + margin-bottom: 24px; height: 0; -`; -const YouTubePrivacyBannerWrapper = styled.div` - position: absolute; - top: 0; - left: 0; - width: 100%; - height: 100%; - padding: 16px; - gap: 16px; - background-color: ${theme.palette.background.card}; - display: flex; - flex-direction: column; - justify-content: center; - align-items: center; -`; + /* button container */ + div[data-service] .cll .c-n-a { + display: flex; + justify-content: center; + align-items: center; + flex-wrap: wrap; + gap: 8px; + margin-top: 18px; + } -const LegalText = styled.p` - ${TextStyles.label} - text-align: center; -`; + /* legal text */ + .cc-text { + font-family: 'CocoGothic', sans-serif; + ${TextStyles.textXS}; + } -const StyledButton = styled(Button)` - width: fit-content; -`; + /* buttons */ + div[data-service] .cll .c-l-b, + div[data-service] .cll .c-la-b { + ${resetButton} + ${TextStyles.toplineS} + font-family: 'CocoGothic', sans-serif; + display: inline-flex; + align-items: center; + padding: 8px 16px; + border-radius: 30px; + background: linear-gradient(275.41deg, #543fd7 0%, #2756fd 100%); + text-wrap: nowrap; + color: #ffffff; + + transition: unset; -const SyledLink = styled(Link)` - color: ${theme.palette.text.link.default}; + &:hover { + background: #3e61ee; + } - &:hover { - border-bottom: 1px solid ${theme.palette.text.link.default}; + &::before { + content: unset; + } } `; export const YoutubeEmbed = ({ videoId }: YoutubeEmbedProps) => { - const { consentGiven, giveConsent } = useContext(YouTubeConsentContext); - return ( - {consentGiven ? ( - - ) : ( - - )} +
); }; - -const YouTubePrivacyBanner = ({ giveConsent }) => { - return ( - - - By clicking on "Show YouTube Content" you accept{' '} - - YouTube's privacy policy - - . - - Show YouTube Content - - ); -}; diff --git a/src/components/pages/blog-post/blog-post.tsx b/src/components/pages/blog-post/blog-post.tsx index 41a44a654..8e913020e 100644 --- a/src/components/pages/blog-post/blog-post.tsx +++ b/src/components/pages/blog-post/blog-post.tsx @@ -12,7 +12,6 @@ import FollowPanel from './follow-panel'; import SharePanel from './share-panel'; import { ContentfulRichText } from '../../content/rich-text/rich-text'; import { TextStyles } from '../../typography'; -import { YouTubeConsentProvider } from '../../../context/youtube-consent-context'; interface BlogPostPageProps { blogPost: BlogArticleQueryData; @@ -64,43 +63,41 @@ export const BlogPostPage = ({ const heroByLine = `${formattedDate} • ${readingTime} • ${byLine}`; return ( - - - } - leadbox={leadbox} - showLanguageSwitch={false} - breadcrumb={breadcrumb} - > - - {blogPost.introRichText && ( - - - - )} - + + } + leadbox={leadbox} + showLanguageSwitch={false} + breadcrumb={breadcrumb} + > + + {blogPost.introRichText && ( + + + + )} + - + - - - - - - + + + + + ); }; diff --git a/src/context/youtube-consent-context.tsx b/src/context/youtube-consent-context.tsx deleted file mode 100644 index 10ecb4d37..000000000 --- a/src/context/youtube-consent-context.tsx +++ /dev/null @@ -1,41 +0,0 @@ -import React, { createContext, useState, useEffect, ReactNode } from 'react'; - -interface YouTubeConsentContextType { - consentGiven: boolean; - giveConsent: () => void; -} - -const YouTubeConsentContext = createContext({ - consentGiven: false, - giveConsent: () => {}, -}); - -interface YouTubeConsentProviderProps { - children: ReactNode; -} - -export const YouTubeConsentProvider = ({ - children, -}: YouTubeConsentProviderProps) => { - const [consentGiven, setConsentGiven] = useState(false); - - useEffect(() => { - const consent = localStorage.getItem('youtube-consent'); - if (consent === 'true') { - setConsentGiven(true); - } - }, []); - - const giveConsent = () => { - setConsentGiven(true); - localStorage.setItem('youtube-consent', 'true'); - }; - - return ( - - {children} - - ); -}; - -export default YouTubeConsentContext; diff --git a/src/pages/index.tsx b/src/pages/index.tsx index 2f97547c6..b6860297a 100644 --- a/src/pages/index.tsx +++ b/src/pages/index.tsx @@ -11,7 +11,6 @@ import { } from '../types'; import { IGatsbyImageData } from 'gatsby-plugin-image'; import { StructuredOrganizationData } from '../components/pages/landingpage/structured-organization-data'; -import LeadinfoScript from '../components/layout/leadinfo-script'; // Import the new component export interface OfficeImage { relativePath: string; @@ -73,7 +72,6 @@ export const Head = ({ data, location }: PageProps) => { locales={data.locales} /> - ); }; diff --git a/tsconfig.json b/tsconfig.json index a0d77f406..49cb13e29 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -11,7 +11,8 @@ "noUnusedLocals": false, "skipLibCheck": true, "allowJs": true, - "noImplicitAny": false + "noImplicitAny": false, + "resolveJsonModule": true }, "exclude": ["node_modules", "public", ".cache"] } diff --git a/yarn.lock b/yarn.lock index 750721cb6..a32ad3d34 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2085,6 +2085,11 @@ "@nodelib/fs.scandir" "2.1.5" fastq "^1.6.0" +"@orestbida/iframemanager@^1.3.0": + version "1.3.0" + resolved "https://registry.yarnpkg.com/@orestbida/iframemanager/-/iframemanager-1.3.0.tgz#d6f33acbf523c234f4f7145cea0b9c3e59b2ee7c" + integrity sha512-hhLS9FJrN3sW/B3U2AbtCKsdiD/XzSVld/hnKkP8hcdBo3BW+98+LFbJL68Cghuay0/fxJpScJSrGb7MFrgjQQ== + "@parcel/bundler-default@2.8.3": version "2.8.3" resolved "https://registry.yarnpkg.com/@parcel/bundler-default/-/bundler-default-2.8.3.tgz#d64739dbc2dbd59d6629861bf77a8083aced5229" @@ -15148,6 +15153,11 @@ value-or-promise@^1.0.12: resolved "https://registry.yarnpkg.com/value-or-promise/-/value-or-promise-1.0.12.tgz#0e5abfeec70148c78460a849f6b003ea7986f15c" integrity sha512-Z6Uz+TYwEqE7ZN50gwn+1LCVo9ZVrpxRPOhOLnncYkY1ZzOYtrX8Fwf/rFktZ8R5mJms6EZf5TqNOMeZmnPq9Q== +vanilla-cookieconsent@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/vanilla-cookieconsent/-/vanilla-cookieconsent-3.0.1.tgz#059d1b2c712476ae4172d4ec7fa9d553a16be12d" + integrity sha512-gqc4x7O9t1I4xWr7x6/jtQWPr4PZK26SmeA0iyTv1WyoECfAGnu5JEOExmMEP+5Fz66AT9OiCBO3GII4wDQHLw== + vary@^1, vary@~1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc"