diff --git a/apps/events-helsinki/src/constants.ts b/apps/events-helsinki/src/constants.ts index 76c3a095b..e8b1ba046 100644 --- a/apps/events-helsinki/src/constants.ts +++ b/apps/events-helsinki/src/constants.ts @@ -1,4 +1,5 @@ export const ROUTES = { + FRONT_PAGE: '/', SEARCH: '/search', EVENTS: '/events/[eventId]', ARTICLE_ARCHIVE: '/article-archive', @@ -6,6 +7,7 @@ export const ROUTES = { PAGES: '/pages/[...slug]', LINK: '', VENUES: '/venues/[venueId]', + COOKIE_CONSENT: '/cookie-consent', }; export const AUTOSUGGEST_KEYWORD_BLACK_LIST = [ diff --git a/apps/events-helsinki/src/domain/app/AppConfig.ts b/apps/events-helsinki/src/domain/app/AppConfig.ts index d86c12eb0..9442f00cf 100644 --- a/apps/events-helsinki/src/domain/app/AppConfig.ts +++ b/apps/events-helsinki/src/domain/app/AppConfig.ts @@ -92,6 +92,20 @@ class AppConfig { }; } + static askemFeedbackConfiguration(locale: 'en' | 'fi' | 'sv') { + const askemEnabled = process.env.NEXT_PUBLIC_ASKEM_ENABLED; + let askemApiKey = process.env.NEXT_PUBLIC_ASKEM_API_KEY_FI; + if (locale === 'en') { + askemApiKey = process.env.NEXT_PUBLIC_ASKEM_API_KEY_EN; + } else if (locale === 'sv') { + askemApiKey = process.env.NEXT_PUBLIC_ASKEM_API_KEY_SV; + } + return { + disabled: !parseEnvValue(askemEnabled), + apiKey: askemApiKey as string, + }; + } + static get defaultRevalidate() { const envValue = process.env.NEXT_PUBLIC_DEFAULT_ISR_REVALIDATE_SECONDS; const value = envValue ? parseEnvValue(envValue) : 60; diff --git a/apps/events-helsinki/src/domain/cookieConsent/ConsentPageContent.tsx b/apps/events-helsinki/src/domain/cookieConsent/ConsentPageContent.tsx new file mode 100644 index 000000000..121371117 --- /dev/null +++ b/apps/events-helsinki/src/domain/cookieConsent/ConsentPageContent.tsx @@ -0,0 +1,13 @@ +import React from 'react'; + +import styles from './consentPageContent.module.scss'; + +interface Props { + children: React.ReactNode; +} + +const ConsentPageContent: React.FC = ({ children }) => { + return
{children}
; +}; + +export default ConsentPageContent; diff --git a/apps/events-helsinki/src/domain/cookieConsent/consentPageContent.module.scss b/apps/events-helsinki/src/domain/cookieConsent/consentPageContent.module.scss new file mode 100644 index 000000000..a36fa60be --- /dev/null +++ b/apps/events-helsinki/src/domain/cookieConsent/consentPageContent.module.scss @@ -0,0 +1,3 @@ +.container { + margin-bottom: var(--spacing-3-xl); +} diff --git a/apps/events-helsinki/src/pages/_app.tsx b/apps/events-helsinki/src/pages/_app.tsx index f0c2c4b50..6104d8f7c 100644 --- a/apps/events-helsinki/src/pages/_app.tsx +++ b/apps/events-helsinki/src/pages/_app.tsx @@ -1,13 +1,19 @@ import 'nprogress/nprogress.css'; import type { NavigationProviderProps } from '@events-helsinki/components'; -import { BaseApp, useCommonTranslation } from '@events-helsinki/components'; +import { + useLocale, + BaseApp, + useCommonTranslation, +} from '@events-helsinki/components'; import type { AppProps as NextAppProps } from 'next/app'; +import { useRouter } from 'next/router'; import type { SSRConfig } from 'next-i18next'; import { appWithTranslation } from 'next-i18next'; import React from 'react'; import '../styles/globals.scss'; import nextI18nextConfig from '../../next-i18next.config'; +import { ROUTES } from '../constants'; import AppConfig from '../domain/app/AppConfig'; import EventsApolloProvider from '../domain/app/EventsApolloProvider'; import cmsHelper from '../domain/app/headlessCmsHelper'; @@ -22,6 +28,8 @@ export type CustomPageProps = NavigationProviderProps & SSRConfig; function MyApp({ Component, pageProps }: AppProps) { const { t } = useCommonTranslation(); + const locale = useLocale(); + const { asPath, pathname } = useRouter(); return ( @@ -30,6 +38,11 @@ function MyApp({ Component, pageProps }: AppProps) { cmsHelper={cmsHelper} routerHelper={routerHelper} matomoConfiguration={AppConfig.matomoConfiguration} + askemFeedbackConfiguration={AppConfig.askemFeedbackConfiguration( + locale + )} + withConsent={pathname !== ROUTES.COOKIE_CONSENT} + asPath={asPath} {...pageProps} > diff --git a/apps/events-helsinki/src/pages/articles/index.tsx b/apps/events-helsinki/src/pages/articles/index.tsx index e17cd40ae..782ed2bed 100644 --- a/apps/events-helsinki/src/pages/articles/index.tsx +++ b/apps/events-helsinki/src/pages/articles/index.tsx @@ -197,6 +197,7 @@ export default function ArticleArchive({ } /> diff --git a/apps/events-helsinki/src/pages/cookie-consent/index.tsx b/apps/events-helsinki/src/pages/cookie-consent/index.tsx new file mode 100644 index 000000000..3f5724892 --- /dev/null +++ b/apps/events-helsinki/src/pages/cookie-consent/index.tsx @@ -0,0 +1,77 @@ +import { + NavigationContext, + Navigation, + MatomoWrapper, + useCommonTranslation, + FooterSection, + getLanguageOrDefault, + usePageScrollRestoration, + EventsCookieConsent, +} from '@events-helsinki/components'; +import type { GetStaticPropsContext } from 'next'; +import { useRouter } from 'next/router'; +import React, { useCallback, useContext } from 'react'; +import { Page as HCRCApolloPage } from 'react-helsinki-headless-cms/apollo'; +import { ROUTES } from '../../constants'; +import getEventsStaticProps from '../../domain/app/getEventsStaticProps'; +import ConsentPageContent from '../../domain/cookieConsent/ConsentPageContent'; +import serverSideTranslationsWithCommon from '../../domain/i18n/serverSideTranslationsWithCommon'; + +export default function CookieConsent() { + const { footerMenu } = useContext(NavigationContext); + const { t } = useCommonTranslation(); + const router = useRouter(); + /* + // bug or feature: query is empty in handleRedirect + const router = useRouter(); + const params: { returnPath?: string } = router.query; */ + + const handleRedirect = useCallback(() => { + if (window) { + const urlSearchParams = new URLSearchParams(window.location.search); + const returnPath: string | null = urlSearchParams.get('returnPath'); + if (returnPath) { + router.push(returnPath); + } + } + }, [router]); + + usePageScrollRestoration(); + + return ( + + } + content={ + + + + } + footer={ + + } + /> + + ); +} + +export async function getStaticProps(context: GetStaticPropsContext) { + return getEventsStaticProps(context, async () => { + const language = getLanguageOrDefault(context.locale); + return { + props: { + ...(await serverSideTranslationsWithCommon(language)), + }, + }; + }); +} diff --git a/apps/events-helsinki/src/pages/index.tsx b/apps/events-helsinki/src/pages/index.tsx index e1c8dc370..8581022f4 100644 --- a/apps/events-helsinki/src/pages/index.tsx +++ b/apps/events-helsinki/src/pages/index.tsx @@ -61,7 +61,11 @@ const HomePage: NextPage<{ /> } footer={ - + } /> diff --git a/apps/events-helsinki/src/pages/search/index.tsx b/apps/events-helsinki/src/pages/search/index.tsx index d902dbec8..648f1c199 100644 --- a/apps/events-helsinki/src/pages/search/index.tsx +++ b/apps/events-helsinki/src/pages/search/index.tsx @@ -58,6 +58,7 @@ export default function Search() { } /> diff --git a/apps/hobbies-helsinki/src/constants.ts b/apps/hobbies-helsinki/src/constants.ts index a71d073a6..6a0d429f5 100644 --- a/apps/hobbies-helsinki/src/constants.ts +++ b/apps/hobbies-helsinki/src/constants.ts @@ -1,4 +1,5 @@ export const ROUTES = { + FRONT_PAGE: '/', SEARCH: '/search', COURSES: '/courses/[eventId]', ARTICLE_ARCHIVE: '/article-archive', @@ -6,6 +7,7 @@ export const ROUTES = { PAGES: '/pages/[...slug]', VENUES: '/venues/[venueId]', LINK: '', + COOKIE_CONSENT: '/cookie-consent', }; export const AUTOSUGGEST_KEYWORD_BLACK_LIST = [ diff --git a/apps/hobbies-helsinki/src/domain/app/AppConfig.ts b/apps/hobbies-helsinki/src/domain/app/AppConfig.ts index 4bd4776b7..51909d239 100644 --- a/apps/hobbies-helsinki/src/domain/app/AppConfig.ts +++ b/apps/hobbies-helsinki/src/domain/app/AppConfig.ts @@ -92,6 +92,20 @@ class AppConfig { }; } + static askemFeedbackConfiguration(locale: 'en' | 'fi' | 'sv') { + const askemEnabled = process.env.NEXT_PUBLIC_ASKEM_ENABLED; + let askemApiKey = process.env.NEXT_PUBLIC_ASKEM_API_KEY_FI; + if (locale === 'en') { + askemApiKey = process.env.NEXT_PUBLIC_ASKEM_API_KEY_EN; + } else if (locale === 'sv') { + askemApiKey = process.env.NEXT_PUBLIC_ASKEM_API_KEY_SV; + } + return { + disabled: !parseEnvValue(askemEnabled), + apiKey: askemApiKey as string, + }; + } + static get defaultRevalidate() { const envValue = process.env.NEXT_PUBLIC_DEFAULT_ISR_REVALIDATE_SECONDS; const value = envValue ? parseEnvValue(envValue) : 60; diff --git a/apps/hobbies-helsinki/src/domain/cookieConsent/ConsentPageContent.tsx b/apps/hobbies-helsinki/src/domain/cookieConsent/ConsentPageContent.tsx new file mode 100644 index 000000000..121371117 --- /dev/null +++ b/apps/hobbies-helsinki/src/domain/cookieConsent/ConsentPageContent.tsx @@ -0,0 +1,13 @@ +import React from 'react'; + +import styles from './consentPageContent.module.scss'; + +interface Props { + children: React.ReactNode; +} + +const ConsentPageContent: React.FC = ({ children }) => { + return
{children}
; +}; + +export default ConsentPageContent; diff --git a/apps/hobbies-helsinki/src/domain/cookieConsent/consentPageContent.module.scss b/apps/hobbies-helsinki/src/domain/cookieConsent/consentPageContent.module.scss new file mode 100644 index 000000000..a36fa60be --- /dev/null +++ b/apps/hobbies-helsinki/src/domain/cookieConsent/consentPageContent.module.scss @@ -0,0 +1,3 @@ +.container { + margin-bottom: var(--spacing-3-xl); +} diff --git a/apps/hobbies-helsinki/src/pages/_app.tsx b/apps/hobbies-helsinki/src/pages/_app.tsx index e491e0964..8427f60ef 100644 --- a/apps/hobbies-helsinki/src/pages/_app.tsx +++ b/apps/hobbies-helsinki/src/pages/_app.tsx @@ -1,12 +1,18 @@ import 'nprogress/nprogress.css'; import type { NavigationProviderProps } from '@events-helsinki/components'; -import { BaseApp, useCommonTranslation } from '@events-helsinki/components'; +import { + useLocale, + BaseApp, + useCommonTranslation, +} from '@events-helsinki/components'; +import { useRouter } from 'next/router'; import type { SSRConfig } from 'next-i18next'; import { appWithTranslation } from 'next-i18next'; import React from 'react'; import '../styles/globals.scss'; import nextI18nextConfig from '../../next-i18next.config'; +import { ROUTES } from '../constants'; import AppConfig from '../domain/app/AppConfig'; import cmsHelper from '../domain/app/headlessCmsHelper'; import HobbiesApolloProvider from '../domain/app/HobbiesApolloProvider'; @@ -22,6 +28,8 @@ export type CustomPageProps = NavigationProviderProps & SSRConfig; function MyApp({ Component, pageProps }: AppProps) { const { t } = useCommonTranslation(); + const locale = useLocale(); + const { asPath, pathname } = useRouter(); return ( @@ -30,6 +38,11 @@ function MyApp({ Component, pageProps }: AppProps) { cmsHelper={cmsHelper} routerHelper={routerHelper} matomoConfiguration={AppConfig.matomoConfiguration} + askemFeedbackConfiguration={AppConfig.askemFeedbackConfiguration( + locale + )} + withConsent={pathname !== ROUTES.COOKIE_CONSENT} + asPath={asPath} {...pageProps} > diff --git a/apps/hobbies-helsinki/src/pages/articles/index.tsx b/apps/hobbies-helsinki/src/pages/articles/index.tsx index f7a64abe6..9f83091f6 100644 --- a/apps/hobbies-helsinki/src/pages/articles/index.tsx +++ b/apps/hobbies-helsinki/src/pages/articles/index.tsx @@ -197,6 +197,7 @@ export default function ArticleArchive({ } /> diff --git a/apps/hobbies-helsinki/src/pages/cookie-consent/index.tsx b/apps/hobbies-helsinki/src/pages/cookie-consent/index.tsx new file mode 100644 index 000000000..d72e9537b --- /dev/null +++ b/apps/hobbies-helsinki/src/pages/cookie-consent/index.tsx @@ -0,0 +1,77 @@ +import { + NavigationContext, + Navigation, + MatomoWrapper, + useCommonTranslation, + FooterSection, + getLanguageOrDefault, + usePageScrollRestoration, + EventsCookieConsent, +} from '@events-helsinki/components'; +import type { GetStaticPropsContext } from 'next'; +import { useRouter } from 'next/router'; +import React, { useCallback, useContext } from 'react'; +import { Page as HCRCApolloPage } from 'react-helsinki-headless-cms/apollo'; +import { ROUTES } from '../../constants'; +import getHobbiesStaticProps from '../../domain/app/getHobbiesStaticProps'; +import ConsentPageContent from '../../domain/cookieConsent/ConsentPageContent'; +import serverSideTranslationsWithCommon from '../../domain/i18n/serverSideTranslationsWithCommon'; + +export default function CookieConsent() { + const { footerMenu } = useContext(NavigationContext); + const { t } = useCommonTranslation(); + const router = useRouter(); + /* + // bug or feature: query is empty in handleRedirect + const router = useRouter(); + const params: { returnPath?: string } = router.query; */ + + const handleRedirect = useCallback(() => { + if (window) { + const urlSearchParams = new URLSearchParams(window.location.search); + const returnPath: string | null = urlSearchParams.get('returnPath'); + if (returnPath) { + router.push(returnPath); + } + } + }, [router]); + + usePageScrollRestoration(); + + return ( + + } + content={ + + + + } + footer={ + + } + /> + + ); +} + +export async function getStaticProps(context: GetStaticPropsContext) { + return getHobbiesStaticProps(context, async () => { + const language = getLanguageOrDefault(context.locale); + return { + props: { + ...(await serverSideTranslationsWithCommon(language)), + }, + }; + }); +} diff --git a/apps/hobbies-helsinki/src/pages/index.tsx b/apps/hobbies-helsinki/src/pages/index.tsx index e9923f377..2dee04af3 100644 --- a/apps/hobbies-helsinki/src/pages/index.tsx +++ b/apps/hobbies-helsinki/src/pages/index.tsx @@ -60,7 +60,11 @@ const HomePage: NextPage<{ /> } footer={ - + } /> diff --git a/apps/hobbies-helsinki/src/pages/search/index.tsx b/apps/hobbies-helsinki/src/pages/search/index.tsx index 80526bd50..1630d07cb 100644 --- a/apps/hobbies-helsinki/src/pages/search/index.tsx +++ b/apps/hobbies-helsinki/src/pages/search/index.tsx @@ -57,6 +57,7 @@ export default function Search() { } /> diff --git a/apps/sports-helsinki/src/constants.ts b/apps/sports-helsinki/src/constants.ts index fb596aedf..4987beb46 100644 --- a/apps/sports-helsinki/src/constants.ts +++ b/apps/sports-helsinki/src/constants.ts @@ -6,11 +6,13 @@ export const SEARCH_ROUTES = { // TODO: Set a typing that tells that it is extending SEARCH_ROUTES export const ROUTES = { ...SEARCH_ROUTES, + FRONT_PAGE: '/', COURSES: '/courses/[eventId]', VENUES: '/venues/[venueId]', ARTICLE_ARCHIVE: '/article-archive', ARTICLES: '/articles/[...slug]', PAGES: '/pages/[...slug]', + COOKIE_CONSENT: '/cookie-consent', LINK: '', }; diff --git a/apps/sports-helsinki/src/domain/app/AppConfig.ts b/apps/sports-helsinki/src/domain/app/AppConfig.ts index f00847d45..34b92a790 100644 --- a/apps/sports-helsinki/src/domain/app/AppConfig.ts +++ b/apps/sports-helsinki/src/domain/app/AppConfig.ts @@ -92,6 +92,20 @@ class AppConfig { }; } + static askemFeedbackConfiguration(locale: 'en' | 'fi' | 'sv') { + const askemEnabled = process.env.NEXT_PUBLIC_ASKEM_ENABLED; + let askemApiKey = process.env.NEXT_PUBLIC_ASKEM_API_KEY_FI; + if (locale === 'en') { + askemApiKey = process.env.NEXT_PUBLIC_ASKEM_API_KEY_EN; + } else if (locale === 'sv') { + askemApiKey = process.env.NEXT_PUBLIC_ASKEM_API_KEY_SV; + } + return { + disabled: !parseEnvValue(askemEnabled), + apiKey: askemApiKey as string, + }; + } + static get defaultRevalidate() { const envValue = process.env.NEXT_PUBLIC_DEFAULT_ISR_REVALIDATE_SECONDS; const value = envValue ? parseEnvValue(envValue) : 60; diff --git a/apps/sports-helsinki/src/domain/cookieConsent/ConsentPageContent.tsx b/apps/sports-helsinki/src/domain/cookieConsent/ConsentPageContent.tsx new file mode 100644 index 000000000..121371117 --- /dev/null +++ b/apps/sports-helsinki/src/domain/cookieConsent/ConsentPageContent.tsx @@ -0,0 +1,13 @@ +import React from 'react'; + +import styles from './consentPageContent.module.scss'; + +interface Props { + children: React.ReactNode; +} + +const ConsentPageContent: React.FC = ({ children }) => { + return
{children}
; +}; + +export default ConsentPageContent; diff --git a/apps/sports-helsinki/src/domain/cookieConsent/consentPageContent.module.scss b/apps/sports-helsinki/src/domain/cookieConsent/consentPageContent.module.scss new file mode 100644 index 000000000..a36fa60be --- /dev/null +++ b/apps/sports-helsinki/src/domain/cookieConsent/consentPageContent.module.scss @@ -0,0 +1,3 @@ +.container { + margin-bottom: var(--spacing-3-xl); +} diff --git a/apps/sports-helsinki/src/pages/_app.tsx b/apps/sports-helsinki/src/pages/_app.tsx index d96462d5c..f0010a718 100644 --- a/apps/sports-helsinki/src/pages/_app.tsx +++ b/apps/sports-helsinki/src/pages/_app.tsx @@ -1,13 +1,19 @@ import 'nprogress/nprogress.css'; import type { NavigationProviderProps } from '@events-helsinki/components'; -import { BaseApp, useCommonTranslation } from '@events-helsinki/components'; +import { + useLocale, + BaseApp, + useCommonTranslation, +} from '@events-helsinki/components'; import type { AppProps as NextAppProps } from 'next/app'; +import { useRouter } from 'next/router'; import type { SSRConfig } from 'next-i18next'; import { appWithTranslation } from 'next-i18next'; import React from 'react'; import '../styles/globals.scss'; import nextI18nextConfig from '../../next-i18next.config'; +import { ROUTES } from '../constants'; import AppConfig from '../domain/app/AppConfig'; import cmsHelper from '../domain/app/headlessCmsHelper'; import routerHelper from '../domain/app/routerHelper'; @@ -22,6 +28,8 @@ export type CustomPageProps = NavigationProviderProps & SSRConfig; function MyApp({ Component, pageProps }: AppProps) { const { t } = useCommonTranslation(); + const locale = useLocale(); + const { asPath, pathname } = useRouter(); return ( @@ -30,6 +38,11 @@ function MyApp({ Component, pageProps }: AppProps) { cmsHelper={cmsHelper} routerHelper={routerHelper} matomoConfiguration={AppConfig.matomoConfiguration} + askemFeedbackConfiguration={AppConfig.askemFeedbackConfiguration( + locale + )} + withConsent={pathname !== ROUTES.COOKIE_CONSENT} + asPath={asPath} {...pageProps} > diff --git a/apps/sports-helsinki/src/pages/articles/index.tsx b/apps/sports-helsinki/src/pages/articles/index.tsx index b1bee65cb..e8e9f2e39 100644 --- a/apps/sports-helsinki/src/pages/articles/index.tsx +++ b/apps/sports-helsinki/src/pages/articles/index.tsx @@ -198,6 +198,7 @@ export default function ArticleArchive({ } /> diff --git a/apps/sports-helsinki/src/pages/cookie-consent/index.tsx b/apps/sports-helsinki/src/pages/cookie-consent/index.tsx new file mode 100644 index 000000000..4c471a93e --- /dev/null +++ b/apps/sports-helsinki/src/pages/cookie-consent/index.tsx @@ -0,0 +1,77 @@ +import { + NavigationContext, + Navigation, + MatomoWrapper, + useCommonTranslation, + FooterSection, + getLanguageOrDefault, + usePageScrollRestoration, + EventsCookieConsent, +} from '@events-helsinki/components'; +import type { GetStaticPropsContext } from 'next'; +import { useRouter } from 'next/router'; +import React, { useCallback, useContext } from 'react'; +import { Page as HCRCApolloPage } from 'react-helsinki-headless-cms/apollo'; +import { ROUTES } from '../../constants'; +import getSportsStaticProps from '../../domain/app/getSportsStaticProps'; +import ConsentPageContent from '../../domain/cookieConsent/ConsentPageContent'; +import serverSideTranslationsWithCommon from '../../domain/i18n/serverSideTranslationsWithCommon'; + +export default function CookieConsent() { + const { footerMenu } = useContext(NavigationContext); + const { t } = useCommonTranslation(); + const router = useRouter(); + /* + // bug or feature: query is empty in handleRedirect + const router = useRouter(); + const params: { returnPath?: string } = router.query; */ + + const handleRedirect = useCallback(() => { + if (window) { + const urlSearchParams = new URLSearchParams(window.location.search); + const returnPath: string | null = urlSearchParams.get('returnPath'); + if (returnPath) { + router.push(returnPath); + } + } + }, [router]); + + usePageScrollRestoration(); + + return ( + + } + content={ + + + + } + footer={ + + } + /> + + ); +} + +export async function getStaticProps(context: GetStaticPropsContext) { + return getSportsStaticProps(context, async () => { + const language = getLanguageOrDefault(context.locale); + return { + props: { + ...(await serverSideTranslationsWithCommon(language)), + }, + }; + }); +} diff --git a/apps/sports-helsinki/src/pages/index.tsx b/apps/sports-helsinki/src/pages/index.tsx index 5321a4e25..1e11e4e00 100644 --- a/apps/sports-helsinki/src/pages/index.tsx +++ b/apps/sports-helsinki/src/pages/index.tsx @@ -62,7 +62,11 @@ const HomePage: NextPage<{ /> } footer={ - + } /> diff --git a/apps/sports-helsinki/src/pages/search/index.tsx b/apps/sports-helsinki/src/pages/search/index.tsx index d0252954e..ee9b5a918 100644 --- a/apps/sports-helsinki/src/pages/search/index.tsx +++ b/apps/sports-helsinki/src/pages/search/index.tsx @@ -27,7 +27,11 @@ export default function Search() { navigation={} content={} footer={ - + } /> diff --git a/packages/common-i18n/src/locales/en/consent.json b/packages/common-i18n/src/locales/en/consent.json index 9e1257598..bcf2cb453 100644 --- a/packages/common-i18n/src/locales/en/consent.json +++ b/packages/common-i18n/src/locales/en/consent.json @@ -11,15 +11,22 @@ "text": "Matomo Analytics software is being used to follow and improve the website to better attend to user's needs.", "expandAriaLabel": "Show cookie information related to the Matomo.", "checkboxAriaDescription": "Matomo uses cookies." + }, + "optionalAskem": { + "title": "Askem", + "text": "Askem enables quick feedback submission on the website.", + "expandAriaLabel": "Show cookie information related to the Askem.", + "checkboxAriaDescription": "Askem uses cookies." } }, "cookies": { - "matomo": "A cookie of the Matomo statistics system", - "linkedevents": "A cookie to authorise the retrieval of events", - "i18next": "A cookie to remember the website language selection", + "matomo": "A cookie of the Matomo statistics system.", + "askem": "A record related to the operation of the Askem react buttons.", + "linkedevents": "A cookie to authorise the retrieval of events.", + "i18next": "A cookie to remember the website language selection.", "servicemap_analytics": "A cookie of the Matomo statistics system for the Service Map.", - "servicemap_session": "Service Map session", - "servicemap_ga": "A Google analytics cookie for the Service Map", + "servicemap_session": "Service Map session.", + "servicemap_ga": "A Google analytics cookie for the Service Map.", "wordpress": "A cookie for operation of the WordPress-content management system." }, "expiration": { @@ -30,5 +37,10 @@ "years": "{{ days }} years", "hours": "{{ hours }} hours", "days": "{{ days }} days" + }, + "askem": { + "cookiesRequiredTitle": "Feedback buttons cannot be displayed", + "cookiesRequiredDescription": "To use this feature, you must accept cookies from the site. You can provide feedback on this page by enabling statistics-related cookies.", + "cookieSettingsButtonText": "Change cookie settings" } } diff --git a/packages/common-i18n/src/locales/fi/consent.json b/packages/common-i18n/src/locales/fi/consent.json index 6d58a1f8d..1484f8250 100644 --- a/packages/common-i18n/src/locales/fi/consent.json +++ b/packages/common-i18n/src/locales/fi/consent.json @@ -8,18 +8,25 @@ }, "optionalMatomo": { "title": "Matomo", - "text": "Matomo Analytics-programvaran används för att spåra och utveckla webbplatsen för att möta användarnas behov.", - "expandAriaLabel": "Visa information om kakor som är relaterade till Matomo.", - "checkboxAriaDescription": "Matomo använder kakor." + "text": "Matomo-analytiikkaohjelmistolla seurataan ja kehitetään verkkosivustoa käyttäjien tarpeita vastaavaksi.", + "expandAriaLabel": "Näytä Matomoon liittyvät evästetiedot.", + "checkboxAriaDescription": "Matomo käyttää evästeitä." + }, + "optionalAskem": { + "title": "Askem", + "text": "Askem mahdollistaa nopean palautteen jättämisen sivustolla.", + "expandAriaLabel": "Näytä Askemin liittyvät evästetiedot.", + "checkboxAriaDescription": "Askem käyttää evästeitä." } }, "cookies": { - "matomo": "Matomo-tilastointijärjestelmän eväste", - "linkedevents": "Eväste tapahtumien hakemisen valtuuttamiseen", - "i18next": "Eväste sivuston kielivalinnan muistamiseen", + "matomo": "Matomo-tilastointijärjestelmän eväste.", + "askem": "Askem-reaktionappien toimintaan liittyvä tietue.", + "linkedevents": "Eväste tapahtumien hakemisen valtuuttamiseen.", + "i18next": "Eväste sivuston kielivalinnan muistamiseen.", "servicemap_analytics": "Matomo-tilastointijärjestelmän eväste palvelukartalle.", - "servicemap_session": "Palvelukartan istunto", - "servicemap_ga": "Google-analytiikan eväste palvelukartalle", + "servicemap_session": "Palvelukartan istunto.", + "servicemap_ga": "Google-analytiikan eväste palvelukartalle.", "wordpress": "Wordpress-sisällönhallintajärjestelmän toimintaan liittyvä eväste." }, "expiration": { @@ -30,5 +37,10 @@ "years": "{{ days }} vuotta", "hours": "{{ hours }} tuntia", "days": "{{ days }} päivää" + }, + "askem": { + "cookiesRequiredTitle": "Sivun palautenappeja ei voida näyttää", + "cookiesRequiredDescription": "Toiminnon käyttämiseksi vaaditaan, että hyväksyt sivuston evästeet. Pääset antamaan palautetta tästä sivusta sallimalla evästeasetuksista tilastointiin liittyvät evästeet.", + "cookieSettingsButtonText": "Muokkaa evästeasetuksia" } } diff --git a/packages/common-i18n/src/locales/sv/consent.json b/packages/common-i18n/src/locales/sv/consent.json index e0601ee39..f53923ea0 100644 --- a/packages/common-i18n/src/locales/sv/consent.json +++ b/packages/common-i18n/src/locales/sv/consent.json @@ -7,19 +7,26 @@ "checkboxAriaDescription": "Kakor används för att visa huvudstadstadsregionens servicekarta." }, "optionalMatomo": { - "title": "Servicekarta", - "text": "Huvudstadsregionens servicekarta hjälper användaren att hitta evenemang på kartan.", - "expandAriaLabel": "Visa information om servicekartans kakor", - "checkboxAriaDescription": "Kakor används för att visa huvudstadstadsregionens servicekarta." + "title": "Matomo", + "text": "Matomo Analytics-programvaran används för att spåra och utveckla webbplatsen för att möta användarnas behov.", + "expandAriaLabel": "Visa information om kakor som är relaterade till Matomo.", + "checkboxAriaDescription": "Matomo använder kakor." + }, + "optionalAskem": { + "title": "Askem", + "text": "Askem möjliggör snabb feedback på webbplatsen.", + "expandAriaLabel": "Visa information om kakor som är relaterade till Askem.", + "checkboxAriaDescription": "Askem uses cookies." } }, "cookies": { - "matomo": "Kaka för statistiksystemet Matomo", - "linkedevents": "Kaka för att söka evenemang", - "i18next": "Kaka som sparar användarens språkval på webbplatsen", - "servicemap_analytics": "Kaka för statistiksystemet Matomo på servicekartan", - "servicemap_session": "Session på servicekartan", - "servicemap_ga": "Kaka för analysverktyget Google Analytics på servicekartan", + "matomo": "Kaka för statistiksystemet Matomo.", + "askem": "En post relaterad till driften av reaktionsknappen Askem.", + "linkedevents": "Kaka för att söka evenemang.", + "i18next": "Kaka som sparar användarens språkval på webbplatsen.", + "servicemap_analytics": "Kaka för statistiksystemet Matomo på servicekartan.", + "servicemap_session": "Session på servicekartan.", + "servicemap_ga": "Kaka för analysverktyget Google Analytics på servicekartan.", "wordpress": "Kaka för användning av WordPress-innehållshanteringssystemet." }, "expiration": { @@ -30,5 +37,10 @@ "years": "{{ days }} år", "hours": "{{ hours }} timmar", "days": "{{ days }} dagar" + }, + "askem": { + "cookiesRequiredTitle": "Feedbackknappar kan inte visas", + "cookiesRequiredDescription": "För att använda denna funktion måste du acceptera cookies från webbplatsen. Du kan ge feedback på den här sidan genom att aktivera statistikrelaterade cookies.", + "cookieSettingsButtonText": "Ändra inställningarna för cookies" } } diff --git a/packages/components/src/app/BaseApp.tsx b/packages/components/src/app/BaseApp.tsx index e0ea20c99..0e044fa07 100644 --- a/packages/components/src/app/BaseApp.tsx +++ b/packages/components/src/app/BaseApp.tsx @@ -4,13 +4,17 @@ import { } from '@jonkoops/matomo-tracker-react'; import 'nprogress/nprogress.css'; +import { useCookies } from 'hds-react'; import type { SSRConfig } from 'next-i18next'; -import React from 'react'; +import React, { useCallback, useEffect, useState } from 'react'; import { ErrorBoundary } from 'react-error-boundary'; import { ToastContainer } from 'react-toastify'; import '../styles/globals.scss'; +import '../styles/askem.scss'; import { CmsHelperProvider } from '../cmsHelperProvider'; +import { createAskemInstance } from '../components/askem'; +import AskemProvider from '../components/askem/AskemProvider'; import ErrorFallback from '../components/errorPages/ErrorFallback'; import EventsCookieConsent from '../components/eventsCookieConsent/EventsCookieConsent'; import ResetFocus from '../components/resetFocus/ResetFocus'; @@ -25,6 +29,10 @@ export type Props = { routerHelper: CmsRoutedAppHelper; appName: string; matomoConfiguration: Parameters[0]; + askemFeedbackConfiguration: Parameters[0]; + onConsentGiven?: (askemConsentGiven: boolean) => void; + asPath: string; + withConsent: boolean; } & NavigationProviderProps & SSRConfig; @@ -37,13 +45,19 @@ function BaseApp({ cmsHelper, routerHelper, matomoConfiguration, + askemFeedbackConfiguration, + asPath, + withConsent, }: Props) { + const { getAllConsents } = useCookies(); + // Unset hidden visibility that was applied to hide the first server render // that does not include styles from HDS. HDS applies styling by injecting // style tags into the head. This requires the existence of a document object. // The document object does not exist during server side renders. // TODO: Remove this hackfix to ensure that pre-rendered pages' // SEO performance is not impacted. + React.useEffect(() => { setTimeout(() => { const body = document?.body; @@ -54,10 +68,36 @@ function BaseApp({ }, 10); }, []); + const [askemConsentGiven, setAskemConsentGiven] = useState(false); + + // todo: matomo is not updated. + const handleConsentGiven = useCallback(() => { + const consents = getAllConsents(); + setAskemConsentGiven( + consents['askemBid'] && + consents['askemBidTs'] && + consents['askemReaction'] + ); + }, [getAllConsents]); + + useEffect(() => { + if (asPath) { + handleConsentGiven(); + } + }, [handleConsentGiven, asPath]); + + const askemFeedbackInstance = React.useMemo( + () => + createAskemInstance({ + ...askemFeedbackConfiguration, + consentGiven: askemConsentGiven, + }), + [askemFeedbackConfiguration, askemConsentGiven] + ); + const matomoInstance = React.useMemo( () => createMatomoInstance(matomoConfiguration), - // eslint-disable-next-line react-hooks/exhaustive-deps - [createMatomoInstance] + [matomoConfiguration] ); const FallbackComponent = ({ error }: { error: Error }) => ( @@ -68,21 +108,26 @@ function BaseApp({ - - - - {children} - - - - + + + + + {children} + {withConsent && ( + + )} + + + + diff --git a/packages/components/src/components/askem/Askem.ts b/packages/components/src/components/askem/Askem.ts new file mode 100644 index 000000000..85af31f0e --- /dev/null +++ b/packages/components/src/components/askem/Askem.ts @@ -0,0 +1,62 @@ +import type { AskemConfigs } from './types'; + +class Askem { + disabled = false; + consentGiven = true; + + constructor(config: AskemConfigs) { + this.initialize(config); + } + + private initialize({ + apiKey, + scriptUrl = 'https://cdn.reactandshare.com/plugin/rns.js', + disabled = false, + consentGiven, + }: AskemConfigs) { + if (disabled || !apiKey || typeof window === 'undefined') { + return; + } + + this.disabled = disabled; + this.consentGiven = Boolean(consentGiven); + + window.rnsData = window.rnsData || {}; + window.rnsData.apiKey; + if (!window.rnsData.apiKey) { + window.rnsData.apiKey = apiKey; + } + + const doc = document; + const scriptElement = doc.createElement('script'); + const scripts = doc.getElementsByTagName('script')[0]; + +Object.assign(scriptElement, { + type: 'text/javascript', + async: true, + defer: true, + src: scriptUrl +}) + + if (scripts && scripts.parentNode) { + scripts.parentNode.insertBefore(scriptElement, scripts); + } + } + + setRnsConfigValue( + propName: string, + value: string | string[] | number + ): Askem { + if (typeof window !== 'undefined') { + window.rnsData = { + ...window.rnsData, + [propName]: value, + }; + window.resetRns(); + } + + return this; + } +} + +export default Askem; diff --git a/packages/components/src/components/askem/AskemContext.tsx b/packages/components/src/components/askem/AskemContext.tsx new file mode 100644 index 000000000..9f9431c61 --- /dev/null +++ b/packages/components/src/components/askem/AskemContext.tsx @@ -0,0 +1,6 @@ +import { createContext } from 'react'; +import type { AskemInstance } from './types'; + +const MatomoContext = createContext(null); + +export default MatomoContext; diff --git a/packages/components/src/components/askem/AskemFeedbackContainer.tsx b/packages/components/src/components/askem/AskemFeedbackContainer.tsx new file mode 100644 index 000000000..e34a3b074 --- /dev/null +++ b/packages/components/src/components/askem/AskemFeedbackContainer.tsx @@ -0,0 +1,63 @@ +import classNames from 'classnames'; +import { Button } from 'hds-react'; +import { useRouter } from 'next/router'; +import React from 'react'; +import { useTranslation } from 'react-i18next'; +import styles from './askem.module.scss'; +import useAskem from './useAskem'; + +interface AskemFeedbackContainerProps { + consentUrl?: string; + withPadding?: boolean; +} + +const AskemFeedbackContainer: React.FC = ({ + consentUrl, + withPadding = false, +}) => { + const { consentGiven, disabled } = useAskem(); + const { t } = useTranslation(); + const router = useRouter(); + + const handleConsentPageRedirect = () => { + if (consentUrl) { + router.push(`${consentUrl}?returnPath=${router.asPath}`); + } + }; + + if (disabled) { + return null; + } + + return ( +
+
+ {!consentGiven && ( +
+

{t('consent:askem.cookiesRequiredTitle')}

+

{t('consent:askem.cookiesRequiredDescription')}

+ {consentUrl && ( + + )} +
+ )} + {!disabled && consentGiven &&
} +
+
+ ); +}; + +AskemFeedbackContainer.displayName = 'AskemFeedbackContainer'; + +export default AskemFeedbackContainer; diff --git a/packages/components/src/components/askem/AskemProvider.tsx b/packages/components/src/components/askem/AskemProvider.tsx new file mode 100644 index 000000000..4e0936f95 --- /dev/null +++ b/packages/components/src/components/askem/AskemProvider.tsx @@ -0,0 +1,19 @@ +import React from 'react'; +import AskemContext from './AskemContext'; +import type { AskemInstance } from './types'; + +export interface AskemProviderProps { + children?: React.ReactNode; + value: AskemInstance | null; +} + +const AskemProvider: React.FC = function ({ + children, + value, +}) { + const Context = AskemContext; + + return {children}; +}; + +export default AskemProvider; diff --git a/packages/components/src/components/askem/askem.module.scss b/packages/components/src/components/askem/askem.module.scss new file mode 100644 index 000000000..2fb8377a9 --- /dev/null +++ b/packages/components/src/components/askem/askem.module.scss @@ -0,0 +1,34 @@ +@import 'src/styles/breakpoints'; + +.rnsContainer { + background-color: var(--color-black-5); + .rnsContainerInner { + margin: 0 auto; + max-width: var(--container-width-xl); + + @include respond_below(xl) { + padding: 0 var(--spacing-m); + } + + &.withPadding { + padding-top: 48px; + } + + .rnsCookiesRequired { + display: flex; + flex-direction: column; + align-items: flex-start; + padding-bottom: 80px; + + & > h2 { + font-size: 1.5rem; + font-weight: 400; + line-height: 1.5rem; + margin: 0; + } + & > p { + margin: 2rem 0; + } + } + } +} diff --git a/packages/components/src/components/askem/index.ts b/packages/components/src/components/askem/index.ts new file mode 100644 index 000000000..4bb99f22a --- /dev/null +++ b/packages/components/src/components/askem/index.ts @@ -0,0 +1,4 @@ +export { default as MatomoContext } from './AskemContext'; +export { default as MatomoProvider } from './AskemProvider'; +export { default as createAskemInstance } from './instance'; +export { default as AskemFeedbackContainer } from './AskemFeedbackContainer'; diff --git a/packages/components/src/components/askem/instance.ts b/packages/components/src/components/askem/instance.ts new file mode 100644 index 000000000..ec4ab4741 --- /dev/null +++ b/packages/components/src/components/askem/instance.ts @@ -0,0 +1,8 @@ +import Askem from './Askem'; +import type { AskemConfigs } from './types'; + +function createInstance(params: AskemConfigs): Askem { + return new Askem(params); +} + +export default createInstance; diff --git a/packages/components/src/components/askem/types.ts b/packages/components/src/components/askem/types.ts new file mode 100644 index 000000000..9bb1dfd38 --- /dev/null +++ b/packages/components/src/components/askem/types.ts @@ -0,0 +1,14 @@ +import type { RnsData } from '../../types'; +import type Askem from './Askem'; + +export type AskemConfigs = { + disabled?: boolean; + scriptUrl?: string; + consentGiven?: boolean; +} & RnsData; + +export interface AskemInstance { + disabled: boolean; + setRnsConfigValue: Askem['setRnsConfigValue']; + consentGiven: boolean; +} diff --git a/packages/components/src/components/askem/useAskem.ts b/packages/components/src/components/askem/useAskem.ts new file mode 100644 index 000000000..c4923a7a0 --- /dev/null +++ b/packages/components/src/components/askem/useAskem.ts @@ -0,0 +1,20 @@ +import { useCallback, useContext } from 'react'; +import AskemContext from './AskemContext'; + +const useAskem = () => { + const instance = useContext(AskemContext); + + const setRnsConfigValue = useCallback( + (propName: string, value: string | number | string[]) => + instance?.setRnsConfigValue(propName, value), + [instance] + ); + + return { + setRnsConfigValue, + disabled: instance?.disabled, + consentGiven: instance?.consentGiven, + }; +}; + +export default useAskem; diff --git a/packages/components/src/components/eventsCookieConsent/EventsCookieConsent.tsx b/packages/components/src/components/eventsCookieConsent/EventsCookieConsent.tsx index c2f80fd95..fe1545942 100644 --- a/packages/components/src/components/eventsCookieConsent/EventsCookieConsent.tsx +++ b/packages/components/src/components/eventsCookieConsent/EventsCookieConsent.tsx @@ -1,24 +1,30 @@ +import { useMatomo } from '@jonkoops/matomo-tracker-react'; import type { ContentSource } from 'hds-react'; -import { useCookies, CookieModal } from 'hds-react'; -import React from 'react'; +import { CookiePage, useCookies, CookieModal } from 'hds-react'; +import React, { useCallback } from 'react'; import { MAIN_CONTENT_ID } from '../../constants'; import { useConsentTranslation } from '../../hooks'; import useLocale from '../../hooks/useLocale'; type Props = { appName: string; + onConsentGiven?: () => void; allowLanguageSwitch?: boolean; + isModal?: boolean; }; const EventsCookieConsent: React.FC = ({ appName, + onConsentGiven, allowLanguageSwitch = true, + isModal = true, }) => { const locale = useLocale(); const { t, i18n } = useConsentTranslation(); const [language, setLanguage] = React.useState(locale); const { getAllConsents } = useCookies(); + const { pushInstruction } = useMatomo(); const [showCookieConsentModal, setShowCookieConsentModal] = React.useState( !Object.keys(getAllConsents()).length ); @@ -32,11 +38,28 @@ const EventsCookieConsent: React.FC = ({ }, [i18n, setLanguage, allowLanguageSwitch] ); + + const handleMatomoUpdate = useCallback(() => { + const getConsentStatus = (cookieId: string) => { + const consents = getAllConsents(); + return consents[cookieId]; + }; + if (getConsentStatus('matomo')) { + pushInstruction('requireCookieConsent'); + } else { + pushInstruction('setCookieConsentGiven'); + } + }, [getAllConsents, pushInstruction]); + const contentSource: ContentSource = React.useMemo( () => ({ siteName: appName, onAllConsentsGiven: () => { setShowCookieConsentModal(false); + if (onConsentGiven) { + handleMatomoUpdate(); + onConsentGiven(); + } }, currentLanguage: language as string as ContentSource['currentLanguage'], requiredCookies: { @@ -121,6 +144,37 @@ const EventsCookieConsent: React.FC = ({ }, ], }, + { + title: t('consent:groups.optionalAskem.title'), + text: t('consent:groups.optionalAskem.text'), + expandAriaLabel: t('consent:groups.optionalAskem.expandAriaLabel'), + checkboxAriaDescription: t( + 'consent:groups.optionalAskem.checkboxAriaDescription' + ), + cookies: [ + { + id: 'askemBid', + name: 'rnsbid', + hostName: 'reactandshare.com', + description: t('consent:cookies.askem'), + expiration: '-', + }, + { + id: 'askemBidTs', + name: 'rnsbid_ts', + hostName: 'reactandshare.com', + description: t('consent:cookies.askem'), + expiration: '-', + }, + { + id: 'askemReaction', + name: 'rns_reaction_*', + hostName: 'reactandshare.com', + description: t('consent:cookies.askem'), + expiration: '-', + }, + ], + }, ], }, language: { @@ -129,12 +183,17 @@ const EventsCookieConsent: React.FC = ({ }, focusTargetSelector: MAIN_CONTENT_ID, }), - [t, language, appName, onLanguageChange] + [appName, language, t, onLanguageChange, onConsentGiven, handleMatomoUpdate] ); - if (!showCookieConsentModal) return null; - - return ; + return ( + <> + {isModal && showCookieConsentModal && ( + + )} + {!isModal && } + + ); }; export default EventsCookieConsent; diff --git a/packages/components/src/components/footer/Footer.tsx b/packages/components/src/components/footer/Footer.tsx index a89091c30..5e4aee8bf 100644 --- a/packages/components/src/components/footer/Footer.tsx +++ b/packages/components/src/components/footer/Footer.tsx @@ -1,6 +1,6 @@ import { Footer, Link } from 'hds-react'; +import dynamic from 'next/dynamic'; import type { FunctionComponent } from 'react'; -import React from 'react'; import type { Menu } from 'react-helsinki-headless-cms'; import { useMenuQuery } from 'react-helsinki-headless-cms/apollo'; import { DEFAULT_FOOTER_MENU_NAME } from '../../constants'; @@ -13,13 +13,27 @@ import styles from './footer.module.scss'; type FooterSectionProps = { appName: string; menu?: Menu; + hasFeedBack?: boolean; + feedbackWithPadding?: boolean; + consentUrl?: string; }; +const AskemFeedbackContainer = dynamic( + () => import('../../components/askem/AskemFeedbackContainer'), + { + ssr: false, + } +); + const FooterSection: FunctionComponent = ({ appName, menu, + hasFeedBack = true, + feedbackWithPadding = false, + consentUrl = '/cookie-consent', }: FooterSectionProps) => { const { t } = useFooterTranslation(); + const locale = useLocale(); const { data: footerMenuData } = useMenuQuery({ @@ -38,29 +52,37 @@ const FooterSection: FunctionComponent = ({ }; return ( -
- - - {footerMenu?.menuItems?.nodes?.map( - // NOTE: HCRC-build sometimes fails - this type should not be needed. - (navigationItem: Menu['menuItems']['nodes'][number]) => ( - - ) - )} - -
+ <> + {hasFeedBack && ( + + )} +
+ + + {footerMenu?.menuItems?.nodes?.map( + // NOTE: HCRC-build sometimes fails - this type should not be needed. + (navigationItem: Menu['menuItems']['nodes'][number]) => ( + + ) + )} + +
+ ); }; diff --git a/packages/components/src/components/matomoWrapper/MatomoWrapper.tsx b/packages/components/src/components/matomoWrapper/MatomoWrapper.tsx index 5d6ed8512..f67fd132c 100644 --- a/packages/components/src/components/matomoWrapper/MatomoWrapper.tsx +++ b/packages/components/src/components/matomoWrapper/MatomoWrapper.tsx @@ -21,7 +21,6 @@ const MatomoWrapper: React.FC = ({ children }) => { }; // if enabled, should be callled before each trackPage instruction - if (getConsentStatus('matomo')) { pushInstruction('requireCookieConsent'); } else { diff --git a/packages/components/src/styles/askem.scss b/packages/components/src/styles/askem.scss new file mode 100644 index 000000000..c79506cc1 --- /dev/null +++ b/packages/components/src/styles/askem.scss @@ -0,0 +1,615 @@ +.rns { + --hel-icon--face-smile: url(/shared-assets/images/feedback/face-smile.svg); + --hel-icon--face-sad: url(/shared-assets/images/feedback/face-sad.svg); + --hel-icon--face-neutral: url(/shared-assets/images/feedback/face-neutral.svg); + --hel-icon--arrow-right: url(/shared-assets/images/feedback/arrow-right.svg); + --hel-icon--email: url(/shared-assets/images/feedback/envelope.svg); + + --hel-icon--facebook: url(/shared-assets/images/share/facebook.svg); + --hel-icon--twitter: url(/shared-assets/images/share/twitter.svg); + --hel-icon--whatsapp: url(/shared-assets/images/share/whatsapp.svg); + --hel-icon--share: url(/shared-assets/images/share/share.svg); + + max-width: 1296px; + margin-left: auto; + margin-right: auto; + background-color: var(--color-black-5); +} + +@media (min-width: 768px) { + .rns { + max-width: 1328px; + } +} + +.rns .rns-plugin { + font-family: HelsinkiGrotesk, Arial, sans-serif; + margin: 0; + padding-bottom: 6rem; +} + +.rns .rns-plugin .rns-reactions .rns-header { + font-size: 1.5rem; + font-weight: 400; + line-height: 1.5rem; +} + +.rns .rns-plugin .rns-reactions .reactions { + margin-top: 24px; +} + +@media only screen and (min-width: 681px) { + .rns .rns-plugin .rns-reactions .reactions { + margin-left: calc(-24px / 2); + margin-right: calc(-24px / 2); + } +} + +.rns .rns-plugin .rns-reactions .reactions .rns-reaction { + flex-grow: 0; + margin: 0 calc(24px / 2); + max-width: 190px; + padding: 0; +} + +@media only screen and (max-width: 680px) { + .rns .rns-plugin .rns-reactions .reactions .rns-reaction { + max-width: 100%; + } +} + +@media only screen and (max-width: 680px) { + .rns .rns-plugin .rns-reactions .reactions .rns-reaction { + margin: 16px 0 0; + padding: 0; + width: 100%; + } +} + +@media only screen and (max-width: 680px) { + .rns .rns-plugin .rns-reactions .reactions .rns-reaction:first-child { + margin-top: 0; + } +} + +.rns .rns-plugin .rns-reactions .reactions .rns-reaction .rns-reaction-button { + border-color: #000; + padding: 14px 32px; + position: relative; +} + +.rns + .rns-plugin + .rns-reactions + .reactions + .rns-reaction + .rns-reaction-button::before { + --size: 100%; + border: 2px solid rgba(0, 0, 0, 0); + content: ''; + height: var(--size); + left: 0; + position: absolute; + width: var(--size); +} + +.rns + .rns-plugin + .rns-reactions + .reactions + .rns-reaction + .rns-reaction-button + .rns-label { + --line-height: 1.5; + font-size: 1rem; + font-weight: 500; + line-height: var(--line-height); + align-items: center; + color: #000; + display: flex; + justify-content: center; + min-width: -moz-max-content; + min-width: max-content; + width: 100%; +} + +.rns + .rns-plugin + .rns-reactions + .reactions + .rns-reaction + .rns-reaction-button + .rns-label::after { + background-color: #000; + background-size: contain; + content: ''; + display: flex; + height: 24px; + -webkit-mask-image: var(--hel-icon--face-neutral); + mask-image: var(--hel-icon--face-neutral); + -webkit-mask-position: center; + mask-position: center; + -webkit-mask-repeat: no-repeat; + mask-repeat: no-repeat; + -webkit-mask-size: 24px 24px; + mask-size: 24px 24px; + vertical-align: bottom; + width: 24px; + margin-left: 16px; +} + +.rns + .rns-plugin + .rns-reactions + .reactions + .rns-reaction + .rns-reaction-button + .rns-reaction-count { + display: none; +} + +.rns + .rns-plugin + .rns-reactions + .reactions + .rns-reaction + .rns-reaction-button:focus { + background-color: #fff; + box-shadow: none; + color: #000; +} + +.rns + .rns-plugin + .rns-reactions + .reactions + .rns-reaction + .rns-reaction-button:focus + .rns-label { + color: #000; +} + +.rns + .rns-plugin + .rns-reactions + .reactions + .rns-reaction + .rns-reaction-button:focus::before { + --size: calc(100% + calc(24px / 2)); + border-color: #000; + left: calc(-24px / 4); +} + +.rns + .rns-plugin + .rns-reactions + .reactions + .rns-reaction + .rns-reaction-button:hover { + background-color: #000; +} + +.rns + .rns-plugin + .rns-reactions + .reactions + .rns-reaction + .rns-reaction-button:hover + .rns-label { + color: #fff; +} + +.rns + .rns-plugin + .rns-reactions + .reactions + .rns-reaction + .rns-reaction-button:hover + .rns-label::after { + background-color: #fff; +} + +.rns + .rns-plugin + .rns-reactions + .reactions + .rns-reaction + .rns-reaction-button:focus:hover + .rns-label { + color: #fff; +} + +.rns + .rns-plugin + .rns-reactions + .reactions + .rns-reaction.selected + .rns-reaction-button { + background-color: #000 !important; +} + +.rns + .rns-plugin + .rns-reactions + .reactions + .rns-reaction.selected + .rns-reaction-button + .rns-label { + color: #fff; +} + +.rns + .rns-plugin + .rns-reactions + .reactions + .rns-reaction.selected + .rns-reaction-button + .rns-label::after { + background-color: #fff; +} + +.rns + .rns-plugin + .rns-reactions + .reactions + .rns-reaction:first-child + .rns-label::after { + -webkit-mask-image: var(--hel-icon--face-smile); + mask-image: var(--hel-icon--face-smile); +} + +.rns + .rns-plugin + .rns-reactions + .reactions + .rns-reaction:last-child + .rns-label::after { + -webkit-mask-image: var(--hel-icon--face-sad); + mask-image: var(--hel-icon--face-sad); +} + +.rns .rns-plugin .rns-shares { + margin-top: 64px; + padding: 0 calc(24px / 4); +} + +.rns .rns-plugin .rns-shares .rns-header { + --line-height: 1.2222222222; + font-size: 1.125rem; + font-weight: 500; + line-height: var(--line-height); + margin-bottom: 0; +} + +@media (min-width: 992px) { + .rns .rns-plugin .rns-shares .rns-header { + --line-height: 1.5; + font-size: 1.25rem; + font-weight: 500; + } +} + +.rns .rns-plugin .rns-shares .rns-shares-list { + margin-top: 32px; +} + +@media only screen and (min-width: 681px) { + .rns .rns-plugin .rns-shares .rns-shares-list { + margin-bottom: 0; + margin-left: -8px; + margin-right: -8px; + margin-top: 32px; + } +} + +.rns .rns-plugin .rns-shares .rns-shares-list .rns-share { + padding: 0 !important; +} + +.rns .rns-plugin .rns-shares .rns-shares-list .rns-share { + margin: 0 8px; + width: auto; +} + +@media only screen and (max-width: 680px) { + .rns .rns-plugin .rns-shares .rns-shares-list .rns-share { + padding: 0 !important; + } + + .rns .rns-plugin .rns-shares .rns-shares-list .rns-share { + margin: 16px 0 0; + width: calc(50% - 16px); + } +} + +@media only screen and (max-width: 680px) { + .rns .rns-plugin .rns-shares .rns-shares-list .rns-share:first-child, + .rns .rns-plugin .rns-shares .rns-shares-list .rns-share:nth-child(2) { + margin: 0; + } +} + +@media only screen and (max-width: 680px) { + .rns .rns-plugin .rns-shares .rns-shares-list .rns-share:nth-child(odd) { + margin-right: 4px; + } +} + +@media only screen and (max-width: 680px) { + .rns .rns-plugin .rns-shares .rns-shares-list .rns-share:nth-child(even) { + margin-left: 4px; + } +} + +.rns .rns-plugin .rns-shares .rns-shares-list .rns-share .rns-share-button { + align-items: center; + background-color: rgba(0, 0, 0, 0); + border: 0; + color: #000; + display: flex; + justify-content: left; + padding: 0; +} + +.rns + .rns-plugin + .rns-shares + .rns-shares-list + .rns-share + .rns-share-button::after { + --size: 100%; + border: 2px solid rgba(0, 0, 0, 0); + content: ''; + height: var(--size); + left: 0; + position: absolute; + width: var(--size); +} + +.rns + .rns-plugin + .rns-shares + .rns-shares-list + .rns-share + .rns-share-button::before { + background-color: #000; + background-size: contain; + content: ''; + display: flex; + height: 32px; + -webkit-mask-image: var(--hel-icon--share); + mask-image: var(--hel-icon--share); + -webkit-mask-position: center; + mask-position: center; + -webkit-mask-repeat: no-repeat; + mask-repeat: no-repeat; + -webkit-mask-size: 32px 32px; + mask-size: 32px 32px; + vertical-align: bottom; + width: 32px; + margin-right: 8px; +} + +.rns + .rns-plugin + .rns-shares + .rns-shares-list + .rns-share + .rns-share-button:focus { + box-shadow: none; + outline: none; +} + +.rns + .rns-plugin + .rns-shares + .rns-shares-list + .rns-share + .rns-share-button:focus::after { + --size: calc(100% + 8px); + border-color: #000; + left: calc(-8px / 2); + top: calc(-8px / 2); +} + +.rns + .rns-plugin + .rns-shares + .rns-shares-list + .rns-share + .rns-share-button:hover { + opacity: 1; +} + +.rns + .rns-plugin + .rns-shares + .rns-shares-list + .rns-share + .rns-share-button:hover::before { + background-color: #000; +} + +.rns + .rns-plugin + .rns-shares + .rns-shares-list + .rns-share + .rns-share-button:hover + .rns-label { + color: #000; + text-decoration: underline; +} + +.rns + .rns-plugin + .rns-shares + .rns-shares-list + .rns-share + .rns-share-button + .rns-icon { + display: none; +} + +.rns + .rns-plugin + .rns-shares + .rns-shares-list + .rns-share + .rns-share-button + .rns-label { + --line-height: 1.5; + font-size: 1rem; + font-weight: 500; + line-height: var(--line-height); + padding-right: calc(8px / 2); +} + +.rns + .rns-plugin + .rns-shares + .rns-shares-list + .rns-share.rns-share-facebook + .rns-share-button::before { + -webkit-mask-image: var(--hel-icon--facebook); + mask-image: var(--hel-icon--facebook); +} + +.rns + .rns-plugin + .rns-shares + .rns-shares-list + .rns-share.rns-share-whatsapp + .rns-share-button::before { + -webkit-mask-image: var(--hel-icon--whatsapp); + mask-image: var(--hel-icon--whatsapp); +} + +.rns + .rns-plugin + .rns-shares + .rns-shares-list + .rns-share.rns-share-twitter + .rns-share-button::before { + -webkit-mask-image: var(--hel-icon--twitter); + mask-image: var(--hel-icon--twitter); +} + +.rns + .rns-plugin + .rns-shares + .rns-shares-list + .rns-share.rns-share-email + .rns-share-button::before { + -webkit-mask-image: var(--hel-icon--email); + mask-image: var(--hel-icon--email); +} + +.rns .rns-plugin .rns-inputs { + margin-top: 48px; + max-width: 688px; + padding: calc(24px / 4); +} + +.rns .rns-plugin .rns-inputs .rns-header { + --line-height: 1.2222222222; + font-size: 1.125rem; + font-weight: 500; + line-height: var(--line-height); + margin-bottom: 0; +} + +@media (min-width: 992px) { + .rns .rns-plugin .rns-inputs .rns-header { + --line-height: 1.5; + font-size: 1.25rem; + font-weight: 500; + } +} + +.rns .rns-plugin .rns-inputs .rns-input-description { + --line-height: 1.5; + font-size: 1rem; + font-weight: 400; + line-height: var(--line-height); + color: #666; + margin: 24px 0 0; + opacity: 1; +} + +.rns .rns-plugin .rns-inputs .rns-input-field { + --line-height: 1.5555555556; + font-size: 1.125rem; + font-weight: 400; + line-height: var(--line-height); + border: 2px solid gray; + font-family: HelsinkiGrotesk, Arial, sans-serif; + height: 188px; + margin-top: 24px; + padding: 16px; +} + +.rns .rns-plugin .rns-inputs .rns-form-submit { + --line-height: 1.5; + font-size: 1rem; + font-weight: 500; + line-height: var(--line-height); + background-color: #000; + border: 2px solid #000; + color: #fff; + display: inline-flex; + margin-top: 24px; + padding: 16px 24px; + position: relative; +} + +.rns .rns-plugin .rns-inputs .rns-form-submit::after { + background-color: #fff; + background-size: contain; + content: ''; + display: flex; + height: 24px; + -webkit-mask-image: var(--hel-icon--arrow-right); + mask-image: var(--hel-icon--arrow-right); + -webkit-mask-position: center; + mask-position: center; + -webkit-mask-repeat: no-repeat; + mask-repeat: no-repeat; + -webkit-mask-size: 24px 24px; + mask-size: 24px 24px; + vertical-align: bottom; + width: 24px; + margin-left: 16px; +} + +.rns .rns-plugin .rns-inputs .rns-form-submit::before { + --size: 100%; + border: 2px solid rgba(0, 0, 0, 0); + content: ''; + height: var(--size); + left: 0; + position: absolute; + top: 0; + width: var(--size); +} + +.rns .rns-plugin .rns-inputs .rns-form-submit:focus { + box-shadow: none; + outline: none; +} + +.rns .rns-plugin .rns-inputs .rns-form-submit:focus::before { + --size: calc(100% + calc(24px / 2)); + border-color: #000; + left: calc(-24px / 4); + top: calc(-24px / 4); +} + +.rns .rns-plugin .rns-inputs .rns-form-submit:hover { + background-color: #fff; + color: #000; +} + +.rns .rns-plugin .rns-inputs .rns-form-submit:hover::after { + background-color: #000; +} diff --git a/packages/components/src/types/types.ts b/packages/components/src/types/types.ts index 351d012d0..d209baaff 100644 --- a/packages/components/src/types/types.ts +++ b/packages/components/src/types/types.ts @@ -107,3 +107,23 @@ export enum SPORTS_CATEGORIES { export const isSportsCategory = (value: unknown): value is SPORTS_CATEGORIES => Object.values(SPORTS_CATEGORIES).includes(value as SPORTS_CATEGORIES); + +export type RnsData = { + apiKey?: string; + title?: string; + canonicalUrl?: string; + author?: string; + date?: string; + categories?: string[]; + commentNumber?: number; + postId?: string; +}; + +export {}; + +declare global { + interface Window { + rnsData: RnsData; + resetRns: () => void; + } +} diff --git a/static/assets/images/feedback/arrow-right.svg b/static/assets/images/feedback/arrow-right.svg new file mode 100644 index 000000000..49a58c01e --- /dev/null +++ b/static/assets/images/feedback/arrow-right.svg @@ -0,0 +1,10 @@ + + + SoMe/facebook Copy 5 + + + + + + + \ No newline at end of file diff --git a/static/assets/images/feedback/envelope.svg b/static/assets/images/feedback/envelope.svg new file mode 100644 index 000000000..bc3517706 --- /dev/null +++ b/static/assets/images/feedback/envelope.svg @@ -0,0 +1,10 @@ + + + SoMe/facebook Copy 4 + + + + + + + \ No newline at end of file diff --git a/static/assets/images/feedback/face-neutral.svg b/static/assets/images/feedback/face-neutral.svg new file mode 100644 index 000000000..876447cd4 --- /dev/null +++ b/static/assets/images/feedback/face-neutral.svg @@ -0,0 +1,12 @@ + + + UI/Notifications and expressions/face-neutral + + + + + + + + + \ No newline at end of file diff --git a/static/assets/images/feedback/face-sad.svg b/static/assets/images/feedback/face-sad.svg new file mode 100644 index 000000000..2353b21d9 --- /dev/null +++ b/static/assets/images/feedback/face-sad.svg @@ -0,0 +1,12 @@ + + + UI/Notifications and expressions/face-neutral Copy + + + + + + + + + \ No newline at end of file diff --git a/static/assets/images/feedback/face-smile.svg b/static/assets/images/feedback/face-smile.svg new file mode 100644 index 000000000..69a0a3a8a --- /dev/null +++ b/static/assets/images/feedback/face-smile.svg @@ -0,0 +1,12 @@ + + + UI/Notifications and expressions/face-neutral Copy 2 + + + + + + + + + \ No newline at end of file diff --git a/static/assets/images/share/share.svg b/static/assets/images/share/share.svg new file mode 100644 index 000000000..2c4b0f258 --- /dev/null +++ b/static/assets/images/share/share.svg @@ -0,0 +1,10 @@ + + + SoMe/facebook Copy + + + + + + + \ No newline at end of file diff --git a/static/assets/images/share/whatsapp.svg b/static/assets/images/share/whatsapp.svg new file mode 100644 index 000000000..5e1011418 --- /dev/null +++ b/static/assets/images/share/whatsapp.svg @@ -0,0 +1,10 @@ + + + SoMe/facebook Copy 2 + + + + + + + \ No newline at end of file