From f6213de83bbf228528efe8ce046267df9c969ce6 Mon Sep 17 00:00:00 2001
From: Rebeka Dekany <50901361+rebekadekany@users.noreply.github.com>
Date: Thu, 29 Aug 2024 12:48:16 +0200
Subject: [PATCH] Merge pull request #20140 from overleaf/rd-footer-react
[web] Migrate the footer to React
GitOrigin-RevId: 9571d7b3ef9a267fb8250956f821b1ee34ce07c7
---
services/web/app/views/layout-react.pug | 9 +-
.../layout/fat-footer-react-bootstrap-5.pug | 1 +
.../web/frontend/extracted-translations.json | 32 +++++
.../js/features/header-footer-react/index.tsx | 15 +-
.../bootstrap-5/footer/fat-footer-base.tsx | 86 +++++++++++
.../bootstrap-5/footer/fat-footer.tsx | 133 ++++++++++++++++++
.../bootstrap-5/language-picker.tsx | 60 ++++++++
.../components/types/fat-footer-metadata.ts | 7 +
.../ui/components/types/fat-footer.ts | 8 ++
.../ui/components/types/split-button-props.ts | 2 +-
services/web/frontend/js/utils/meta.ts | 3 +-
.../bootstrap-5/components/footer.scss | 6 +
services/web/locales/en.json | 1 +
.../shared/language-picker.spec.tsx | 50 +++++++
14 files changed, 406 insertions(+), 7 deletions(-)
create mode 100644 services/web/app/views/layout/fat-footer-react-bootstrap-5.pug
create mode 100644 services/web/frontend/js/features/ui/components/bootstrap-5/footer/fat-footer-base.tsx
create mode 100644 services/web/frontend/js/features/ui/components/bootstrap-5/footer/fat-footer.tsx
create mode 100644 services/web/frontend/js/features/ui/components/bootstrap-5/language-picker.tsx
create mode 100644 services/web/frontend/js/features/ui/components/types/fat-footer-metadata.ts
create mode 100644 services/web/frontend/js/features/ui/components/types/fat-footer.ts
create mode 100644 services/web/test/frontend/components/shared/language-picker.spec.tsx
diff --git a/services/web/app/views/layout-react.pug b/services/web/app/views/layout-react.pug
index b0cc7eabd8..5fa3fec4b3 100644
--- a/services/web/app/views/layout-react.pug
+++ b/services/web/app/views/layout-react.pug
@@ -33,6 +33,10 @@ block append meta
adminUrl: settings.adminUrl,
items: cloneAndTranslateText(nav.header_extras)
})
+ meta(name="ol-footer" data-type="json" content={
+ subdomainLang: settings.i18n.subdomainLang,
+ translatedLanguages: settings.translatedLanguages
+ })
block body
if (typeof suppressNavbar === "undefined")
@@ -47,7 +51,10 @@ block body
if showThinFooter
include layout/footer-marketing
else
- include layout/fat-footer
+ if bootstrapVersion === 5
+ include layout/fat-footer-react-bootstrap-5
+ else
+ include layout/fat-footer
if (typeof suppressCookieBanner === "undefined")
include _cookie_banner
diff --git a/services/web/app/views/layout/fat-footer-react-bootstrap-5.pug b/services/web/app/views/layout/fat-footer-react-bootstrap-5.pug
new file mode 100644
index 0000000000..297afdf746
--- /dev/null
+++ b/services/web/app/views/layout/fat-footer-react-bootstrap-5.pug
@@ -0,0 +1 @@
+#fat-footer-container
diff --git a/services/web/frontend/extracted-translations.json b/services/web/frontend/extracted-translations.json
index 17bf04a058..78926f3ee2 100644
--- a/services/web/frontend/extracted-translations.json
+++ b/services/web/frontend/extracted-translations.json
@@ -2,8 +2,14 @@
"1_2_width": "",
"1_4_width": "",
"3_4_width": "",
+ "About": "",
"Account": "",
"Account Settings": "",
+ "Documentation": "",
+ "Get Involved": "",
+ "Help": "",
+ "Learn": "",
+ "Plans and Pricing": "",
"a_custom_size_has_been_used_in_the_latex_code": "",
"a_fatal_compile_error_that_completely_blocks_compilation": "",
"a_file_with_that_name_already_exists_and_will_be_overriden": "",
@@ -96,6 +102,7 @@
"anonymous": "",
"anyone_with_link_can_edit": "",
"anyone_with_link_can_view": "",
+ "app_on_x": "",
"apply_suggestion": "",
"archive": "",
"archive_projects": "",
@@ -124,6 +131,7 @@
"back_to_editor": "",
"back_to_subscription": "",
"back_to_your_projects": "",
+ "become_an_advisor": "",
"before_you_use_the_ai_error_assistant": "",
"beta_program_already_participating": "",
"beta_program_benefits": "",
@@ -132,6 +140,7 @@
"binary_history_error": "",
"blank_project": "",
"blocked_filename": "",
+ "blog": "",
"browser": "",
"bulk_accept_confirm": "",
"bulk_reject_confirm": "",
@@ -157,6 +166,7 @@
"card_details_are_not_valid": "",
"card_must_be_authenticated_by_3dsecure": "",
"card_payment": "",
+ "careers": "",
"category_arrows": "",
"category_greek": "",
"category_misc": "",
@@ -223,6 +233,7 @@
"compile_terminated_by_user": "",
"compiler": "",
"compiling": "",
+ "compliance": "",
"compromised_password": "",
"configure_sso": "",
"confirm": "",
@@ -465,7 +476,14 @@
"following_paths_conflict": "",
"font_family": "",
"font_size": "",
+ "footer_about_us": "",
+ "footer_contact_us": "",
+ "footer_navigation": "",
+ "for_enterprise": "",
+ "for_individuals_and_groups": "",
"for_more_information_see_managed_accounts_section": "",
+ "for_students": "",
+ "for_universities": "",
"format": "",
"found_matching_deleted_users": "",
"free_7_day_trial_billed_annually": "",
@@ -623,6 +641,8 @@
"hotkey_undo": "",
"hotkeys": "",
"how_it_works": "",
+ "how_to_create_tables": "",
+ "how_to_insert_images": "",
"how_we_use_your_data": "",
"how_we_use_your_data_explanation": "",
"i_want_to_stay": "",
@@ -694,6 +714,7 @@
"ip_address": "",
"is_email_affiliated": "",
"issued_on": "",
+ "join_beta_program": "",
"join_now": "",
"join_overleaf_labs": "",
"join_project": "",
@@ -723,6 +744,7 @@
"last_used": "",
"latam_discount_modal_info": "",
"latam_discount_modal_title": "",
+ "latex_in_thirty_minutes": "",
"latex_places_figures_according_to_a_special_algorithm": "",
"latex_places_tables_according_to_a_special_algorithm": "",
"layout": "",
@@ -742,6 +764,7 @@
"length_unit": "",
"let_us_know": "",
"let_us_know_how_we_can_help": "",
+ "let_us_know_what_you_think": "",
"lets_fix_your_errors": "",
"library": "",
"license_for_educational_purposes": "",
@@ -949,6 +972,7 @@
"other": "",
"other_logs_and_files": "",
"other_output_files": "",
+ "our_values": "",
"out_of_sync": "",
"out_of_sync_detail": "",
"output_file": "",
@@ -1016,8 +1040,10 @@
"plus_more": "",
"postal_code": "",
"premium_feature": "",
+ "premium_features": "",
"premium_plan_label": "",
"presentation_mode": "",
+ "press_and_awards": "",
"previous_page": "",
"price": "",
"primarily_work_study_question": "",
@@ -1028,6 +1054,7 @@
"primarily_work_study_question_university_school": "",
"primary_certificate": "",
"priority_support": "",
+ "privacy_and_terms": "",
"private": "",
"problem_talking_to_publishing_service": "",
"problem_with_subscription_contact_us": "",
@@ -1227,6 +1254,7 @@
"select_a_file": "",
"select_a_file_figure_modal": "",
"select_a_group_optional": "",
+ "select_a_language": "",
"select_a_new_owner_for_projects": "",
"select_a_payment_method": "",
"select_a_project": "",
@@ -1567,6 +1595,7 @@
"turn_off_link_sharing": "",
"turn_on": "",
"turn_on_link_sharing": "",
+ "tutorials": "",
"unarchive": "",
"uncategorized": "",
"uncategorized_projects": "",
@@ -1675,6 +1704,8 @@
"we_do_not_share_personal_information": "",
"we_logged_you_in": "",
"we_sent_new_code": "",
+ "webinars": "",
+ "website_status": "",
"wed_love_you_to_stay": "",
"welcome_to_sl": "",
"were_making_some_changes_to_project_sharing_this_means_you_will_be_visible": "",
@@ -1688,6 +1719,7 @@
"what_happens_when_sso_is_enabled": "",
"what_should_we_call_you": "",
"when_you_tick_the_include_caption_box": "",
+ "why_latex": "",
"wide": "",
"will_lose_edit_access_on_date": "",
"with_premium_subscription_you_also_get": "",
diff --git a/services/web/frontend/js/features/header-footer-react/index.tsx b/services/web/frontend/js/features/header-footer-react/index.tsx
index fa0e9a5cff..f3395fe316 100644
--- a/services/web/frontend/js/features/header-footer-react/index.tsx
+++ b/services/web/frontend/js/features/header-footer-react/index.tsx
@@ -1,9 +1,16 @@
import ReactDOM from 'react-dom'
-import DefaultNavbar from '@/features/ui/components/bootstrap-5/navbar/default-navbar'
import getMeta from '@/utils/meta'
+import DefaultNavbar from '@/features/ui/components/bootstrap-5/navbar/default-navbar'
+import FatFooter from '@/features/ui/components/bootstrap-5/footer/fat-footer'
-const element = document.getElementById('navbar-container')
-if (element) {
+const navbarElement = document.getElementById('navbar-container')
+if (navbarElement) {
const navbarProps = getMeta('ol-navbar')
- ReactDOM.render(, element)
+ ReactDOM.render(, navbarElement)
+}
+
+const footerElement = document.getElementById('fat-footer-container')
+if (footerElement) {
+ const footerProps = getMeta('ol-footer')
+ ReactDOM.render(, footerElement)
}
diff --git a/services/web/frontend/js/features/ui/components/bootstrap-5/footer/fat-footer-base.tsx b/services/web/frontend/js/features/ui/components/bootstrap-5/footer/fat-footer-base.tsx
new file mode 100644
index 0000000000..2e213cec96
--- /dev/null
+++ b/services/web/frontend/js/features/ui/components/bootstrap-5/footer/fat-footer-base.tsx
@@ -0,0 +1,86 @@
+import React from 'react'
+import { useTranslation } from 'react-i18next'
+import LanguagePicker from '../language-picker'
+import Icon from '@/shared/components/icon'
+
+type FooterLinkProps = {
+ href: string
+ children: React.ReactNode
+}
+
+type SocialMediaLinkProps = {
+ href: string
+ icon: string
+ accessibilityLabel: string
+}
+
+function FatFooterBase() {
+ const { t } = useTranslation()
+ const currentYear = new Date().getFullYear()
+
+ return (
+
+ )
+}
+
+function FooterBaseLink({ href, children }: FooterLinkProps) {
+ return (
+
+ {children}
+
+ )
+}
+
+function SocialMediaLink({
+ href,
+ icon,
+ accessibilityLabel,
+}: SocialMediaLinkProps) {
+ return (
+
+
+
+ )
+}
+
+export default FatFooterBase
diff --git a/services/web/frontend/js/features/ui/components/bootstrap-5/footer/fat-footer.tsx b/services/web/frontend/js/features/ui/components/bootstrap-5/footer/fat-footer.tsx
new file mode 100644
index 0000000000..bd7e03e116
--- /dev/null
+++ b/services/web/frontend/js/features/ui/components/bootstrap-5/footer/fat-footer.tsx
@@ -0,0 +1,133 @@
+import React from 'react'
+import { useTranslation } from 'react-i18next'
+import FatFooterBase from './fat-footer-base'
+import { FatFooterMetadata } from '../../types/fat-footer-metadata'
+
+type FooterLinkProps = {
+ href: string
+ label: string
+}
+
+type FooterSectionProps = {
+ title: string
+ links: FooterLinkProps[]
+}
+
+function FatFooter(props: FatFooterMetadata) {
+ const { t } = useTranslation()
+ const hideFatFooter = false
+
+ const sections = [
+ {
+ title: t('About'),
+ links: [
+ { href: '/about', label: t('footer_about_us') },
+ { href: '/about/values', label: t('our_values') },
+ { href: '/about/careers', label: t('careers') },
+ { href: '/for/press', label: t('press_and_awards') },
+ { href: '/blog', label: t('blog') },
+ ],
+ },
+ {
+ title: t('Learn'),
+ links: [
+ {
+ href: '/learn/latex/Learn_LaTeX_in_30_minutes',
+ label: t('latex_in_thirty_minutes'),
+ },
+ { href: '/latex/templates', label: t('templates') },
+ { href: '/events/webinars', label: t('webinars') },
+ { href: '/learn/latex/Tutorials', label: t('tutorials') },
+ {
+ href: '/learn/latex/Inserting_Images',
+ label: t('how_to_insert_images'),
+ },
+ { href: '/learn/latex/Tables', label: t('how_to_create_tables') },
+ ],
+ },
+ {
+ title: t('Plans and Pricing'),
+ links: [
+ {
+ href: '/learn/how-to/Overleaf_premium_features',
+ label: t('premium_features'),
+ },
+ {
+ href: '/user/subscription/plans?itm_referrer=footer-for-indv-groups',
+ label: t('for_individuals_and_groups'),
+ },
+ { href: '/for/enterprises', label: t('for_enterprise') },
+ { href: '/for/universities', label: t('for_universities') },
+ {
+ href: '/user/subscription/plans?itm_referrer=footer-for-students#student-annual',
+ label: t('for_students'),
+ },
+ ],
+ },
+ {
+ title: t('Get Involved'),
+ links: [
+ { href: '/for/community/advisors', label: t('become_an_advisor') },
+ {
+ href: 'https://forms.gle/67PSpN1bLnjGCmPQ9',
+ label: t('let_us_know_what_you_think'),
+ },
+ { href: '/beta/participate', label: t('join_beta_program') },
+ ],
+ },
+ {
+ title: t('Help'),
+ links: [
+ { href: '/about/why-latex', label: t('why_latex') },
+ { href: '/learn', label: t('Documentation') },
+ { href: '/contact', label: t('footer_contact_us') },
+ { href: 'https://status.overleaf.com/', label: t('website_status') },
+ ],
+ },
+ ]
+
+ return (
+
+ )
+}
+
+function FooterSection({ title, links }: FooterSectionProps) {
+ const { t } = useTranslation()
+
+ return (
+ <>
+
{t(title)}
+
+ >
+ )
+}
+
+export default FatFooter
diff --git a/services/web/frontend/js/features/ui/components/bootstrap-5/language-picker.tsx b/services/web/frontend/js/features/ui/components/bootstrap-5/language-picker.tsx
new file mode 100644
index 0000000000..61e776cd1e
--- /dev/null
+++ b/services/web/frontend/js/features/ui/components/bootstrap-5/language-picker.tsx
@@ -0,0 +1,60 @@
+import React from 'react'
+import {
+ Dropdown,
+ DropdownItem,
+ DropdownMenu,
+ DropdownToggle,
+} from './dropdown-menu'
+import { useTranslation } from 'react-i18next'
+import getMeta from '@/utils/meta'
+import Icon from '@/shared/components/icon'
+
+function LanguagePicker() {
+ const { t } = useTranslation()
+
+ const currentLangCode = getMeta('ol-i18n').currentLangCode
+ const translatedLanguages = getMeta('ol-footer').translatedLanguages
+ const subdomainLang = getMeta('ol-footer').subdomainLang
+ const currentUrlWithQueryParams = window.location.pathname
+
+ return (
+
+
+
+ {translatedLanguages?.[currentLangCode]}
+
+
+
+ {subdomainLang &&
+ Object.entries(subdomainLang).map(([subdomain, subdomainDetails]) => {
+ if (!subdomainDetails || !subdomainDetails.lngCode) return null
+ const isActive = subdomainDetails.lngCode === currentLangCode
+ return (
+
+
+ {translatedLanguages?.[subdomainDetails.lngCode]}
+
+
+ )
+ })}
+
+
+ )
+}
+
+export default LanguagePicker
diff --git a/services/web/frontend/js/features/ui/components/types/fat-footer-metadata.ts b/services/web/frontend/js/features/ui/components/types/fat-footer-metadata.ts
new file mode 100644
index 0000000000..1d695f91d2
--- /dev/null
+++ b/services/web/frontend/js/features/ui/components/types/fat-footer-metadata.ts
@@ -0,0 +1,7 @@
+import type { SubdomainLang } from '@/features/ui/components/types/fat-footer'
+
+export type FatFooterMetadata = {
+ subdomainLang?: SubdomainLang
+ translatedLanguages: { [key: string]: string }
+ currentLangCode: string
+}
diff --git a/services/web/frontend/js/features/ui/components/types/fat-footer.ts b/services/web/frontend/js/features/ui/components/types/fat-footer.ts
new file mode 100644
index 0000000000..fe460ca828
--- /dev/null
+++ b/services/web/frontend/js/features/ui/components/types/fat-footer.ts
@@ -0,0 +1,8 @@
+export interface SubdomainDetails {
+ lngCode: string
+ url: string
+}
+
+export interface SubdomainLang {
+ [subdomain: string]: SubdomainDetails
+}
diff --git a/services/web/frontend/js/features/ui/components/types/split-button-props.ts b/services/web/frontend/js/features/ui/components/types/split-button-props.ts
index 28c10c75ec..9059216543 100644
--- a/services/web/frontend/js/features/ui/components/types/split-button-props.ts
+++ b/services/web/frontend/js/features/ui/components/types/split-button-props.ts
@@ -15,7 +15,7 @@ type SplitButtonItemProps = Pick<
export type SplitButtonVariants = Extract<
ButtonProps['variant'],
- 'primary' | 'secondary' | 'danger'
+ 'primary' | 'secondary' | 'danger' | 'link'
>
export type SplitButtonProps = PropsWithChildren<{
diff --git a/services/web/frontend/js/utils/meta.ts b/services/web/frontend/js/utils/meta.ts
index 7c3e6a0d25..4ae6e3fb68 100644
--- a/services/web/frontend/js/utils/meta.ts
+++ b/services/web/frontend/js/utils/meta.ts
@@ -47,7 +47,7 @@ import { Subscription as ProjectDashboardSubscription } from '../../../types/pro
import { ThirdPartyIds } from '../../../types/third-party-ids'
import { Publisher } from '../../../types/subscription/dashboard/publisher'
import { DefaultNavbarMetadata } from '@/features/ui/components/types/default-navbar-metadata'
-
+import { FatFooterMetadata } from '@/features/ui/components/types/fat-footer-metadata'
export interface Meta {
'ol-ExposedSettings': ExposedSettings
'ol-allInReconfirmNotificationPeriods': UserEmailData[]
@@ -83,6 +83,7 @@ export interface Meta {
'ol-error': { name: string } | undefined
'ol-expired': boolean
'ol-features': Features
+ 'ol-footer': FatFooterMetadata
'ol-fromPlansPage': boolean
'ol-gitBridgeEnabled': boolean
'ol-gitBridgePublicBaseUrl': string
diff --git a/services/web/frontend/stylesheets/bootstrap-5/components/footer.scss b/services/web/frontend/stylesheets/bootstrap-5/components/footer.scss
index 6104b513b8..5bbe3039c1 100644
--- a/services/web/frontend/stylesheets/bootstrap-5/components/footer.scss
+++ b/services/web/frontend/stylesheets/bootstrap-5/components/footer.scss
@@ -139,6 +139,12 @@ footer.site-footer {
#language-picker-toggle {
color: var(--content-secondary-dark);
+ cursor: pointer;
+ text-decoration: none;
+
+ &::after {
+ display: none;
+ }
}
.fat-footer-base-meta a:not(.dropdown-toggle) {
diff --git a/services/web/locales/en.json b/services/web/locales/en.json
index 6d5216de95..008cc7f29d 100644
--- a/services/web/locales/en.json
+++ b/services/web/locales/en.json
@@ -1770,6 +1770,7 @@
"select_a_file": "Select a File",
"select_a_file_figure_modal": "Select a file",
"select_a_group_optional": "Select a Group (optional)",
+ "select_a_language": "Select a language",
"select_a_new_owner_for_projects": "Select a new owner for this user’s projects",
"select_a_payment_method": "Select a payment method",
"select_a_project": "Select a Project",
diff --git a/services/web/test/frontend/components/shared/language-picker.spec.tsx b/services/web/test/frontend/components/shared/language-picker.spec.tsx
new file mode 100644
index 0000000000..867fc8e612
--- /dev/null
+++ b/services/web/test/frontend/components/shared/language-picker.spec.tsx
@@ -0,0 +1,50 @@
+import React from 'react'
+import '../../helpers/bootstrap-5'
+import LanguagePicker from '../../../../frontend/js/features/ui/components/bootstrap-5/language-picker'
+import getMeta from '@/utils/meta'
+import exposedSettings from '../../../../modules/admin-panel/test/frontend/js/features/user/data/exposedSettings'
+
+describe('LanguagePicker', function () {
+ beforeEach(function () {
+ window.metaAttributesCache.set('ol-i18n', {
+ currentLangCode: 'en',
+ })
+ window.metaAttributesCache.set('ol-footer', {
+ translatedLanguages: {
+ en: 'English',
+ fr: 'Français',
+ es: 'Español',
+ },
+ subdomainLang: {
+ en: { lngCode: 'en', url: 'overleaf.com' },
+ fr: { lngCode: 'fr', url: 'fr.overleaf.com' },
+ es: { lngCode: 'es', url: 'es.overleaf.com' },
+ },
+ })
+
+ Object.assign(getMeta('ol-ExposedSettings'), exposedSettings)
+ })
+
+ it('renders the language picker with the current language', function () {
+ cy.mount()
+ cy.get('#language-picker-toggle').should('contain', 'English')
+ })
+
+ it('opens the dropdown and lists available languages', function () {
+ cy.mount()
+ cy.get('#language-picker-toggle').click()
+
+ cy.get('.dropdown-menu').within(() => {
+ cy.contains('English').should('exist')
+ cy.contains('Français').should('exist')
+ cy.contains('Español').should('exist')
+ })
+ })
+
+ it('changes the language and updates the URL when a language is selected', function () {
+ cy.mount()
+ cy.get('#language-picker-toggle').should('exist').click()
+ cy.contains('Français').click()
+ cy.url().should('include', 'fr.overleaf.com')
+ })
+})