Prechádzať zdrojové kódy

Merge pull request #27254 from overleaf/td-project-dashboard-cookie-banner

Implement React cookie banner on project dashboard

GitOrigin-RevId: 95d2778d7ce7cb3054a06b06486b815a3453a623
Tim Down 1 rok pred
rodič
commit
0778bab910

+ 4 - 4
services/web/app/views/_cookie_banner.pug

@@ -1,13 +1,13 @@
-section.cookie-banner.hidden-print.hidden(aria-label='Cookie banner')
-	.cookie-banner-content We only use cookies for essential purposes and to improve your experience on our site. You can find out more in our <a href="/legal#Cookies">cookie policy</a>.
+section.cookie-banner.hidden-print.hidden(aria-label=translate('cookie_banner'))
+	.cookie-banner-content !{translate('cookie_banner_info', {}, [{ name: 'a', attrs: { href: '/legal#Cookies' }}])}
 	.cookie-banner-actions
 		button(
 			type='button'
 			class='btn btn-link btn-sm'
 			data-ol-cookie-banner-set-consent='essential'
-		) Essential cookies only
+		) #{translate('essential_cookies_only')}
 		button(
 			type='button'
 			class='btn btn-primary btn-sm'
 			data-ol-cookie-banner-set-consent='all'
-		) Accept all cookies
+		) #{translate('accept_all_cookies')}

+ 1 - 1
services/web/app/views/general/post-gateway.pug

@@ -4,7 +4,7 @@ block vars
 	- var suppressNavbar = true
 	- var suppressFooter = true
 	- var suppressSkipToContent = true
-	- var suppressCookieBanner = true
+	- var suppressPugCookieBanner = true
 
 block content
 	.content.content-alt

+ 1 - 1
services/web/app/views/layout-marketing.pug

@@ -24,7 +24,7 @@ block body
 		else
 			include layout/fat-footer
 
-	if typeof suppressCookieBanner == 'undefined'
+	if typeof suppressPugCookieBanner == 'undefined'
 		include _cookie_banner
 
 	if bootstrapVersion === 5

+ 1 - 1
services/web/app/views/layout-react.pug

@@ -69,5 +69,5 @@ block body
 		else
 			include layout/fat-footer-react-bootstrap-5
 
-	if typeof suppressCookieBanner === 'undefined'
+	if typeof suppressPugCookieBanner === 'undefined'
 		include _cookie_banner

+ 1 - 1
services/web/app/views/layout-website-redesign.pug

@@ -27,7 +27,7 @@ block body
 		else
 			include layout/fat-footer-website-redesign
 
-	if typeof suppressCookieBanner == 'undefined'
+	if typeof suppressPugCookieBanner == 'undefined'
 		include _cookie_banner
 
 	block contactModal

+ 1 - 1
services/web/app/views/project/editor/new_from_template.pug

@@ -2,7 +2,7 @@ extends ../../layout-marketing
 
 block vars
 	- var suppressFooter = true
-	- var suppressCookieBanner = true
+	- var suppressPugCookieBanner = true
 	- var suppressSkipToContent = true
 
 block content

+ 1 - 1
services/web/app/views/project/ide-react-detached.pug

@@ -7,7 +7,7 @@ block vars
 	- var suppressNavbar = true
 	- var suppressFooter = true
 	- var suppressSkipToContent = true
-	- var suppressCookieBanner = true
+	- var suppressPugCookieBanner = true
 	- metadata.robotsNoindexNofollow = true
 
 block content

+ 1 - 0
services/web/app/views/project/list-react.pug

@@ -7,6 +7,7 @@ block vars
 	- const suppressNavContentLinks = true
 	- const suppressNavbar = true
 	- const suppressFooter = true
+	- const suppressPugCookieBanner = true
 
 block append meta
 	meta(

+ 1 - 1
services/web/app/views/project/token/access-react.pug

@@ -5,7 +5,7 @@ block entrypointVar
 
 block vars
 	- var suppressFooter = true
-	- var suppressCookieBanner = true
+	- var suppressPugCookieBanner = true
 	- var suppressSkipToContent = true
 
 block append meta

+ 1 - 1
services/web/app/views/project/token/sharing-updates.pug

@@ -5,7 +5,7 @@ block entrypointVar
 
 block vars
 	- var suppressFooter = true
-	- var suppressCookieBanner = true
+	- var suppressPugCookieBanner = true
 	- var suppressSkipToContent = true
 
 block append meta

+ 4 - 0
services/web/frontend/extracted-translations.json

@@ -35,6 +35,7 @@
   "about_to_remove_user_preamble": "",
   "about_to_trash_projects": "",
   "abstract": "",
+  "accept_all_cookies": "",
   "accept_and_continue": "",
   "accept_change": "",
   "accept_change_error_description": "",
@@ -332,6 +333,8 @@
   "continue_to": "",
   "continue_using_free_features": "",
   "continue_with_free_plan": "",
+  "cookie_banner": "",
+  "cookie_banner_info": "",
   "copied": "",
   "copy": "",
   "copy_code": "",
@@ -544,6 +547,7 @@
   "error_opening_document_detail": "",
   "error_performing_request": "",
   "error_processing_file": "",
+  "essential_cookies_only": "",
   "example_project": "",
   "existing_plan_active_until_term_end": "",
   "expand": "",

+ 0 - 53
services/web/frontend/js/features/cookie-banner/index.js

@@ -1,53 +0,0 @@
-import getMeta from '@/utils/meta'
-
-function loadGA() {
-  if (window.olLoadGA) {
-    window.olLoadGA()
-  }
-}
-
-function setConsent(value) {
-  document.querySelector('.cookie-banner').classList.add('hidden')
-  const cookieDomain = getMeta('ol-ExposedSettings').cookieDomain
-  const oneYearInSeconds = 60 * 60 * 24 * 365
-  const cookieAttributes =
-    '; path=/' +
-    '; domain=' +
-    cookieDomain +
-    '; max-age=' +
-    oneYearInSeconds +
-    '; SameSite=Lax; Secure'
-  if (value === 'all') {
-    document.cookie = 'oa=1' + cookieAttributes
-    loadGA()
-    window.dispatchEvent(new CustomEvent('cookie-consent', { detail: true }))
-  } else {
-    document.cookie = 'oa=0' + cookieAttributes
-    window.dispatchEvent(new CustomEvent('cookie-consent', { detail: false }))
-  }
-}
-
-if (
-  getMeta('ol-ExposedSettings').gaToken ||
-  getMeta('ol-ExposedSettings').gaTokenV4 ||
-  getMeta('ol-ExposedSettings').propensityId ||
-  getMeta('ol-ExposedSettings').hotjarId
-) {
-  document
-    .querySelectorAll('[data-ol-cookie-banner-set-consent]')
-    .forEach(el => {
-      el.addEventListener('click', function (e) {
-        e.preventDefault()
-        const consentType = el.getAttribute('data-ol-cookie-banner-set-consent')
-        setConsent(consentType)
-      })
-    })
-
-  const oaCookie = document.cookie.split('; ').find(c => c.startsWith('oa='))
-  if (!oaCookie) {
-    const cookieBannerEl = document.querySelector('.cookie-banner')
-    if (cookieBannerEl) {
-      cookieBannerEl.classList.remove('hidden')
-    }
-  }
-}

+ 32 - 0
services/web/frontend/js/features/cookie-banner/index.ts

@@ -0,0 +1,32 @@
+import {
+  CookieConsentValue,
+  cookieBannerRequired,
+  hasMadeCookieChoice,
+  setConsent,
+} from '@/features/cookie-banner/utils'
+
+function toggleCookieBanner(hidden: boolean) {
+  const cookieBannerEl = document.querySelector('.cookie-banner')
+  if (cookieBannerEl) {
+    cookieBannerEl.classList.toggle('hidden', hidden)
+  }
+}
+
+if (cookieBannerRequired()) {
+  document
+    .querySelectorAll('[data-ol-cookie-banner-set-consent]')
+    .forEach(el => {
+      el.addEventListener('click', function (e) {
+        e.preventDefault()
+        toggleCookieBanner(true)
+        const consentType = el.getAttribute(
+          'data-ol-cookie-banner-set-consent'
+        ) as CookieConsentValue | null
+        setConsent(consentType)
+      })
+    })
+
+  if (!hasMadeCookieChoice()) {
+    toggleCookieBanner(false)
+  }
+}

+ 43 - 0
services/web/frontend/js/features/cookie-banner/utils.ts

@@ -0,0 +1,43 @@
+import getMeta from '@/utils/meta'
+
+export type CookieConsentValue = 'all' | 'essential'
+
+function loadGA() {
+  if (window.olLoadGA) {
+    window.olLoadGA()
+  }
+}
+
+export function setConsent(value: CookieConsentValue | null) {
+  const cookieDomain = getMeta('ol-ExposedSettings').cookieDomain
+  const oneYearInSeconds = 60 * 60 * 24 * 365
+  const cookieAttributes =
+    '; path=/' +
+    '; domain=' +
+    cookieDomain +
+    '; max-age=' +
+    oneYearInSeconds +
+    '; SameSite=Lax; Secure'
+  if (value === 'all') {
+    document.cookie = 'oa=1' + cookieAttributes
+    loadGA()
+    window.dispatchEvent(new CustomEvent('cookie-consent', { detail: true }))
+  } else {
+    document.cookie = 'oa=0' + cookieAttributes
+    window.dispatchEvent(new CustomEvent('cookie-consent', { detail: false }))
+  }
+}
+
+export function cookieBannerRequired() {
+  const exposedSettings = getMeta('ol-ExposedSettings')
+  return Boolean(
+    exposedSettings.gaToken ||
+      exposedSettings.gaTokenV4 ||
+      exposedSettings.propensityId ||
+      exposedSettings.hotjarId
+  )
+}
+
+export function hasMadeCookieChoice() {
+  return document.cookie.split('; ').some(c => c.startsWith('oa='))
+}

+ 2 - 0
services/web/frontend/js/features/project-list/components/project-list-ds-nav.tsx

@@ -20,6 +20,7 @@ import Footer from '@/features/ui/components/bootstrap-5/footer/footer'
 import SidebarDsNav from '@/features/project-list/components/sidebar/sidebar-ds-nav'
 import SystemMessages from '@/shared/components/system-messages'
 import overleafLogo from '@/shared/svgs/overleaf-a-ds-solution-mallard.svg'
+import CookieBanner from '@/shared/components/cookie-banner'
 
 export function ProjectListDsNav() {
   const navbarProps = getMeta('ol-navbar')
@@ -125,6 +126,7 @@ export function ProjectListDsNav() {
             </div>
             <Footer {...footerProps} />
           </div>
+          <CookieBanner />
         </div>
       </main>
     </div>

+ 7 - 3
services/web/frontend/js/features/project-list/components/project-list-root.tsx

@@ -18,6 +18,7 @@ import Footer from '@/features/ui/components/bootstrap-5/footer/footer'
 import WelcomePageContent from '@/features/project-list/components/welcome-page-content'
 import { ProjectListDsNav } from '@/features/project-list/components/project-list-ds-nav'
 import { DsNavStyleProvider } from '@/features/project-list/components/use-is-ds-nav'
+import CookieBanner from '@/shared/components/cookie-banner'
 
 function ProjectListRoot() {
   const { isReady } = useWaitForI18n()
@@ -88,9 +89,12 @@ function ProjectListPageContent() {
 
   if (totalProjectsCount === 0) {
     return (
-      <DefaultPageContentWrapper>
-        <WelcomePageContent />
-      </DefaultPageContentWrapper>
+      <>
+        <DefaultPageContentWrapper>
+          <WelcomePageContent />
+        </DefaultPageContentWrapper>
+        <CookieBanner />
+      </>
     )
   }
   return (

+ 58 - 0
services/web/frontend/js/shared/components/cookie-banner.tsx

@@ -0,0 +1,58 @@
+import OLButton from '@/features/ui/components/ol/ol-button'
+import { Trans, useTranslation } from 'react-i18next'
+import React, { useState } from 'react'
+import {
+  CookieConsentValue,
+  cookieBannerRequired,
+  hasMadeCookieChoice,
+  setConsent,
+} from '@/features/cookie-banner/utils'
+
+function CookieBanner() {
+  const { t } = useTranslation()
+  const [hidden, setHidden] = useState(
+    () => !cookieBannerRequired() || hasMadeCookieChoice()
+  )
+
+  function makeCookieChoice(value: CookieConsentValue) {
+    setConsent(value)
+    setHidden(true)
+  }
+
+  if (hidden) {
+    return null
+  }
+
+  return (
+    <section
+      className="cookie-banner hidden-print"
+      aria-label={t('cookie_banner')}
+    >
+      <div className="cookie-banner-content">
+        <Trans
+          i18nKey="cookie_banner_info"
+          // eslint-disable-next-line react/jsx-key, jsx-a11y/anchor-has-content
+          components={[<a href="/legal#Cookies" />]}
+        />
+      </div>
+      <div className="cookie-banner-actions">
+        <OLButton
+          variant="link"
+          size="sm"
+          onClick={() => makeCookieChoice('essential')}
+        >
+          {t('essential_cookies_only')}
+        </OLButton>
+        <OLButton
+          variant="primary"
+          size="sm"
+          onClick={() => makeCookieChoice('all')}
+        >
+          {t('accept_all_cookies')}
+        </OLButton>
+      </div>
+    </section>
+  )
+}
+
+export default CookieBanner

+ 17 - 1
services/web/frontend/stylesheets/bootstrap-5/pages/project-list-ds-nav.scss

@@ -255,6 +255,12 @@
       display: flex;
       flex-direction: column;
 
+      > * {
+        @include media-breakpoint-up(md) {
+          border-left: 1px solid var(--border-divider);
+        }
+      }
+
       .project-ds-nav-content {
         flex-grow: 1;
         overflow-y: auto;
@@ -263,10 +269,20 @@
 
         @include media-breakpoint-up(md) {
           border-top-left-radius: var(--border-radius-large);
-          border-left: 1px solid var(--border-divider);
           border-top: 1px solid var(--border-divider);
         }
       }
+
+      .cookie-banner {
+        position: static;
+        background-color: var(--bg-light-primary);
+
+        // Remove the parts of the shadow that stick out of the sides
+        clip-path: inset(-13px 0 0 0);
+
+        // Prevent the cookie banner being overlaid on top of the navigation
+        z-index: auto;
+      }
     }
   }
 

+ 4 - 0
services/web/locales/en.json

@@ -38,6 +38,7 @@
   "about_to_trash_projects": "You are about to trash the following projects:",
   "abstract": "Abstract",
   "accept": "Accept",
+  "accept_all_cookies": "Accept all cookies",
   "accept_and_continue": "Accept and continue",
   "accept_change": "Accept change",
   "accept_change_error_description": "There was an error accepting a track change. Please try again in a few moments.",
@@ -433,6 +434,8 @@
   "continue_using_free_features": "Continue using our free features",
   "continue_with_free_plan": "Continue with free plan",
   "continue_with_service": "Continue with __service__",
+  "cookie_banner": "Cookie banner",
+  "cookie_banner_info": "We only use cookies for essential purposes and to improve your experience on our site. You can find out more in our <0>cookie policy</0>.",
   "copied": "Copied",
   "copy": "Copy",
   "copy_code": "Copy code",
@@ -700,6 +703,7 @@
   "error_performing_request": "An error has occurred while performing your request.",
   "error_processing_file": "Sorry, something went wrong processing this file. Please try again.",
   "es": "Spanish",
+  "essential_cookies_only": "Essential cookies only",
   "estimated_number_of_overleaf_users": "Estimated number of __appName__ users",
   "every": "per",
   "everything_in_free_plus": "Everything in Free, plus…",

+ 1 - 0
services/web/types/window.ts

@@ -27,5 +27,6 @@ declare global {
     gtag?: (...args: any) => void
 
     propensity?: (propensityId?: string) => void
+    olLoadGA?: () => void
   }
 }