From a8e1f28d3fb2a2ecb875745cc369dbf7107ece5e Mon Sep 17 00:00:00 2001
From: Tim Down <158919+timdown@users.noreply.github.com>
Date: Mon, 21 Jul 2025 11:50:29 +0100
Subject: [PATCH] Merge pull request #27254 from
overleaf/td-project-dashboard-cookie-banner
Implement React cookie banner on project dashboard
GitOrigin-RevId: 95d2778d7ce7cb3054a06b06486b815a3453a623
---
services/web/app/views/_cookie_banner.pug | 8 +--
.../web/app/views/general/post-gateway.pug | 2 +-
services/web/app/views/layout-marketing.pug | 2 +-
services/web/app/views/layout-react.pug | 2 +-
.../web/app/views/layout-website-redesign.pug | 2 +-
.../project/editor/new_from_template.pug | 2 +-
.../app/views/project/ide-react-detached.pug | 2 +-
services/web/app/views/project/list-react.pug | 1 +
.../app/views/project/token/access-react.pug | 2 +-
.../views/project/token/sharing-updates.pug | 2 +-
.../web/frontend/extracted-translations.json | 4 ++
.../js/features/cookie-banner/index.js | 53 -----------------
.../js/features/cookie-banner/index.ts | 32 ++++++++++
.../js/features/cookie-banner/utils.ts | 43 ++++++++++++++
.../components/project-list-ds-nav.tsx | 2 +
.../components/project-list-root.tsx | 10 +++-
.../js/shared/components/cookie-banner.tsx | 58 +++++++++++++++++++
.../pages/project-list-ds-nav.scss | 18 +++++-
services/web/locales/en.json | 4 ++
services/web/types/window.ts | 1 +
20 files changed, 181 insertions(+), 69 deletions(-)
delete mode 100644 services/web/frontend/js/features/cookie-banner/index.js
create mode 100644 services/web/frontend/js/features/cookie-banner/index.ts
create mode 100644 services/web/frontend/js/features/cookie-banner/utils.ts
create mode 100644 services/web/frontend/js/shared/components/cookie-banner.tsx
diff --git a/services/web/app/views/_cookie_banner.pug b/services/web/app/views/_cookie_banner.pug
index 56974326cd..7cbc569bc1 100644
--- a/services/web/app/views/_cookie_banner.pug
+++ b/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 cookie policy.
+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')}
diff --git a/services/web/app/views/general/post-gateway.pug b/services/web/app/views/general/post-gateway.pug
index c6bbc92d01..86f379ac1b 100644
--- a/services/web/app/views/general/post-gateway.pug
+++ b/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
diff --git a/services/web/app/views/layout-marketing.pug b/services/web/app/views/layout-marketing.pug
index b54c30f033..26e4eb539d 100644
--- a/services/web/app/views/layout-marketing.pug
+++ b/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
diff --git a/services/web/app/views/layout-react.pug b/services/web/app/views/layout-react.pug
index 94ff3ba247..e9c4c932c4 100644
--- a/services/web/app/views/layout-react.pug
+++ b/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
diff --git a/services/web/app/views/layout-website-redesign.pug b/services/web/app/views/layout-website-redesign.pug
index 61ed83043b..aa7fea9f07 100644
--- a/services/web/app/views/layout-website-redesign.pug
+++ b/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
diff --git a/services/web/app/views/project/editor/new_from_template.pug b/services/web/app/views/project/editor/new_from_template.pug
index c84288a21a..a5dc3ff33c 100644
--- a/services/web/app/views/project/editor/new_from_template.pug
+++ b/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
diff --git a/services/web/app/views/project/ide-react-detached.pug b/services/web/app/views/project/ide-react-detached.pug
index ca1a178bbf..fa695b1af5 100644
--- a/services/web/app/views/project/ide-react-detached.pug
+++ b/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
diff --git a/services/web/app/views/project/list-react.pug b/services/web/app/views/project/list-react.pug
index 78103e75a6..47bff344b6 100644
--- a/services/web/app/views/project/list-react.pug
+++ b/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(
diff --git a/services/web/app/views/project/token/access-react.pug b/services/web/app/views/project/token/access-react.pug
index 80b91f1a99..6c01ad15b1 100644
--- a/services/web/app/views/project/token/access-react.pug
+++ b/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
diff --git a/services/web/app/views/project/token/sharing-updates.pug b/services/web/app/views/project/token/sharing-updates.pug
index d1818be0af..2f67e5a3c1 100644
--- a/services/web/app/views/project/token/sharing-updates.pug
+++ b/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
diff --git a/services/web/frontend/extracted-translations.json b/services/web/frontend/extracted-translations.json
index ef2a9c6a2c..2775c04601 100644
--- a/services/web/frontend/extracted-translations.json
+++ b/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": "",
diff --git a/services/web/frontend/js/features/cookie-banner/index.js b/services/web/frontend/js/features/cookie-banner/index.js
deleted file mode 100644
index 3d9b2b8d6c..0000000000
--- a/services/web/frontend/js/features/cookie-banner/index.js
+++ /dev/null
@@ -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')
- }
- }
-}
diff --git a/services/web/frontend/js/features/cookie-banner/index.ts b/services/web/frontend/js/features/cookie-banner/index.ts
new file mode 100644
index 0000000000..2ea97e875a
--- /dev/null
+++ b/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)
+ }
+}
diff --git a/services/web/frontend/js/features/cookie-banner/utils.ts b/services/web/frontend/js/features/cookie-banner/utils.ts
new file mode 100644
index 0000000000..5c045d4e71
--- /dev/null
+++ b/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='))
+}
diff --git a/services/web/frontend/js/features/project-list/components/project-list-ds-nav.tsx b/services/web/frontend/js/features/project-list/components/project-list-ds-nav.tsx
index 3d24f9845c..07319ffaf1 100644
--- a/services/web/frontend/js/features/project-list/components/project-list-ds-nav.tsx
+++ b/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() {
+
diff --git a/services/web/frontend/js/features/project-list/components/project-list-root.tsx b/services/web/frontend/js/features/project-list/components/project-list-root.tsx
index 679b645a6f..d5b4257f03 100644
--- a/services/web/frontend/js/features/project-list/components/project-list-root.tsx
+++ b/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 (
-
-
-
+ <>
+
+
+
+
+ >
)
}
return (
diff --git a/services/web/frontend/js/shared/components/cookie-banner.tsx b/services/web/frontend/js/shared/components/cookie-banner.tsx
new file mode 100644
index 0000000000..d057c7230f
--- /dev/null
+++ b/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 (
+
+
+ ]}
+ />
+
+
+
makeCookieChoice('essential')}
+ >
+ {t('essential_cookies_only')}
+
+
makeCookieChoice('all')}
+ >
+ {t('accept_all_cookies')}
+
+
+
+ )
+}
+
+export default CookieBanner
diff --git a/services/web/frontend/stylesheets/bootstrap-5/pages/project-list-ds-nav.scss b/services/web/frontend/stylesheets/bootstrap-5/pages/project-list-ds-nav.scss
index d71de37470..93ab22866f 100644
--- a/services/web/frontend/stylesheets/bootstrap-5/pages/project-list-ds-nav.scss
+++ b/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;
+ }
}
}
diff --git a/services/web/locales/en.json b/services/web/locales/en.json
index adfb33d8f8..ddaeb8e79f 100644
--- a/services/web/locales/en.json
+++ b/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 policy0>.",
"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…",
diff --git a/services/web/types/window.ts b/services/web/types/window.ts
index 5688faa9a4..6869e90957 100644
--- a/services/web/types/window.ts
+++ b/services/web/types/window.ts
@@ -27,5 +27,6 @@ declare global {
gtag?: (...args: any) => void
propensity?: (propensityId?: string) => void
+ olLoadGA?: () => void
}
}