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

Implement React cookie banner on project dashboard

GitOrigin-RevId: 95d2778d7ce7cb3054a06b06486b815a3453a623
This commit is contained in:
Tim Down
2025-07-21 11:50:29 +01:00
committed by Copybot
parent 9d899f0254
commit a8e1f28d3f
20 changed files with 181 additions and 69 deletions

View File

@@ -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')}

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

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

View File

@@ -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

View File

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

View File

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

View File

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

View File

@@ -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": "",

View File

@@ -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')
}
}
}

View File

@@ -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)
}
}

View File

@@ -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='))
}

View File

@@ -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>

View File

@@ -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 (

View File

@@ -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

View File

@@ -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;
}
}
}

View File

@@ -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…",

View File

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