Merge pull request #20140 from overleaf/rd-footer-react

[web] Migrate the footer to React

GitOrigin-RevId: 9571d7b3ef9a267fb8250956f821b1ee34ce07c7
This commit is contained in:
Rebeka Dekany
2024-08-29 12:48:16 +02:00
committed by Copybot
parent eb0f3e5bf0
commit f6213de83b
14 changed files with 406 additions and 7 deletions

View File

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

View File

@@ -0,0 +1 @@
#fat-footer-container

View File

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

View File

@@ -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(<DefaultNavbar {...navbarProps} />, element)
ReactDOM.render(<DefaultNavbar {...navbarProps} />, navbarElement)
}
const footerElement = document.getElementById('fat-footer-container')
if (footerElement) {
const footerProps = getMeta('ol-footer')
ReactDOM.render(<FatFooter {...footerProps} />, footerElement)
}

View File

@@ -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 (
<footer className="fat-footer-base">
<div className="fat-footer-base-section fat-footer-base-meta">
<div className="fat-footer-base-item">
<div className="fat-footer-base-copyright">
© {currentYear} Overleaf
</div>
<FooterBaseLink href="/legal">
{t('privacy_and_terms')}
</FooterBaseLink>
<FooterBaseLink href="https://www.digital-science.com/security-certifications/">
{t('compliance')}
</FooterBaseLink>
</div>
<div className="fat-footer-base-item fat-footer-base-language">
<LanguagePicker />
</div>
</div>
<div className="fat-footer-base-section fat-footer-base-social">
<div className="fat-footer-base-item">
<SocialMediaLink
href="https://twitter.com/overleaf"
icon="twitter-square"
accessibilityLabel={t('app_on_x', { social: 'Twitter' })}
/>
<SocialMediaLink
href="https://www.facebook.com/overleaf.editor"
icon="facebook-square"
accessibilityLabel={t('app_on_x', { social: 'Facebook' })}
/>
<SocialMediaLink
href="https://www.linkedin.com/company/writelatex-limited"
icon="linkedin-square"
accessibilityLabel={t('app_on_x', { social: 'LinkedIn' })}
/>
</div>
</div>
</footer>
)
}
function FooterBaseLink({ href, children }: FooterLinkProps) {
return (
<a className="fat-footer-link" href={href}>
{children}
</a>
)
}
function SocialMediaLink({
href,
icon,
accessibilityLabel,
}: SocialMediaLinkProps) {
return (
<a className="fat-footer-social" href={href}>
<Icon
type={icon}
className="fa"
accessibilityLabel={accessibilityLabel}
/>
</a>
)
}
export default FatFooterBase

View File

@@ -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 (
<footer className="fat-footer hidden-print">
<div
role="navigation"
aria-label={t('footer_navigation')}
className="fat-footer-container"
>
<div className={`fat-footer-sections ${hideFatFooter ? 'hidden' : ''}`}>
<div className="footer-section" id="footer-brand">
<a href="/" aria-label={t('overleaf')} className="footer-brand">
<span className="visually-hidden">{t('overleaf')}</span>
</a>
</div>
{sections.map(section => (
<div className="footer-section" key={section.title}>
<FooterSection title={section.title} links={section.links} />
</div>
))}
</div>
<FatFooterBase />
</div>
</footer>
)
}
function FooterSection({ title, links }: FooterSectionProps) {
const { t } = useTranslation()
return (
<>
<h2 className="footer-section-heading">{t(title)}</h2>
<ul className="list-unstyled">
{links.map(link => (
<li key={link.href}>
<a href={link.href}>{t(link.label)}</a>
</li>
))}
</ul>
</>
)
}
export default FatFooter

View File

@@ -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 (
<Dropdown>
<DropdownToggle
id="language-picker-toggle"
aria-label={t('select_a_language')}
data-bs-toggle="dropdown"
className="btn-inline-link"
variant="link"
>
<Icon
type="language"
className="fa fa-fw"
accessibilityLabel={t('select_a_language')}
/>
{translatedLanguages?.[currentLangCode]}
</DropdownToggle>
<DropdownMenu className="sm" aria-labelledby="language-picker-toggle">
{subdomainLang &&
Object.entries(subdomainLang).map(([subdomain, subdomainDetails]) => {
if (!subdomainDetails || !subdomainDetails.lngCode) return null
const isActive = subdomainDetails.lngCode === currentLangCode
return (
<li role="none" key={subdomain}>
<DropdownItem
href={`${subdomainDetails.url}${currentUrlWithQueryParams}`}
active={isActive}
aria-current={isActive ? 'true' : false}
trailingIcon={isActive ? 'check' : null}
>
{translatedLanguages?.[subdomainDetails.lngCode]}
</DropdownItem>
</li>
)
})}
</DropdownMenu>
</Dropdown>
)
}
export default LanguagePicker

View File

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

View File

@@ -0,0 +1,8 @@
export interface SubdomainDetails {
lngCode: string
url: string
}
export interface SubdomainLang {
[subdomain: string]: SubdomainDetails
}

View File

@@ -15,7 +15,7 @@ type SplitButtonItemProps = Pick<
export type SplitButtonVariants = Extract<
ButtonProps['variant'],
'primary' | 'secondary' | 'danger'
'primary' | 'secondary' | 'danger' | 'link'
>
export type SplitButtonProps = PropsWithChildren<{

View File

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

View File

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

View File

@@ -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 users projects",
"select_a_payment_method": "Select a payment method",
"select_a_project": "Select a Project",

View File

@@ -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(<LanguagePicker />)
cy.get('#language-picker-toggle').should('contain', 'English')
})
it('opens the dropdown and lists available languages', function () {
cy.mount(<LanguagePicker />)
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(<LanguagePicker />)
cy.get('#language-picker-toggle').should('exist').click()
cy.contains('Français').click()
cy.url().should('include', 'fr.overleaf.com')
})
})