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 ( +
+
+
+
+ © {currentYear} Overleaf +
+ + {t('privacy_and_terms')} + + + {t('compliance')} + +
+
+ +
+
+
+
+ + + +
+
+
+ ) +} + +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') + }) +})