From d5643d53b38bbdb9839b2b9051ccb7bb1a390727 Mon Sep 17 00:00:00 2001 From: Tim Down <158919+timdown@users.noreply.github.com> Date: Wed, 21 Aug 2024 11:33:17 +0100 Subject: [PATCH] Merge pull request #18996 from overleaf/td-bs5-nav-react Main navigation React component GitOrigin-RevId: c99a4b4a2f6fd02618689f829681118b2b64aa8d --- .../app/src/infrastructure/ExpressLocals.js | 20 +++ services/web/app/views/_mixins/navbar.pug | 2 +- services/web/app/views/layout-react.pug | 61 +++++++++ .../navbar-marketing-react-bootstrap-5.pug | 1 + services/web/app/views/project/list-react.pug | 2 +- services/web/app/views/user/settings.pug | 2 +- .../web/frontend/extracted-translations.json | 5 + .../js/features/header-footer-react/index.tsx | 9 ++ .../components/bootstrap-5/dropdown-menu.tsx | 4 +- .../bootstrap-5/navbar/admin-menu.tsx | 49 ++++++++ .../bootstrap-5/navbar/contact-us-item.tsx | 25 ++++ .../bootstrap-5/navbar/default-navbar.tsx | 119 ++++++++++++++++++ .../navbar/header-logo-or-title.tsx | 32 +++++ .../bootstrap-5/navbar/logged-in-items.tsx | 60 +++++++++ .../navbar/nav-dropdown-divider.tsx | 5 + .../navbar/nav-dropdown-from-data.tsx | 44 +++++++ .../bootstrap-5/navbar/nav-dropdown-item.tsx | 5 + .../navbar/nav-dropdown-link-item.tsx | 22 ++++ .../bootstrap-5/navbar/nav-dropdown-menu.tsx | 23 ++++ .../bootstrap-5/navbar/nav-item-from-data.tsx | 29 +++++ .../bootstrap-5/navbar/nav-item.tsx | 10 ++ .../bootstrap-5/navbar/nav-link-item.tsx | 23 ++++ .../ui/components/bootstrap-5/navbar/util.ts | 23 ++++ .../types/default-navbar-metadata.ts | 20 +++ .../components/types/dropdown-menu-props.ts | 1 + .../js/features/ui/components/types/navbar.ts | 52 ++++++++ .../frontend/js/features/utils/bootstrap-5.ts | 4 +- services/web/frontend/js/marketing.js | 1 + .../web/frontend/js/pages/project-list.jsx | 2 +- services/web/frontend/js/utils/meta.ts | 2 + .../abstracts/variable-overrides.scss | 3 + .../bootstrap-5/components/navbar.scss | 15 ++- services/web/locales/en.json | 9 +- 33 files changed, 669 insertions(+), 15 deletions(-) create mode 100644 services/web/app/views/layout-react.pug create mode 100644 services/web/app/views/layout/navbar-marketing-react-bootstrap-5.pug create mode 100644 services/web/frontend/js/features/header-footer-react/index.tsx create mode 100644 services/web/frontend/js/features/ui/components/bootstrap-5/navbar/admin-menu.tsx create mode 100644 services/web/frontend/js/features/ui/components/bootstrap-5/navbar/contact-us-item.tsx create mode 100644 services/web/frontend/js/features/ui/components/bootstrap-5/navbar/default-navbar.tsx create mode 100644 services/web/frontend/js/features/ui/components/bootstrap-5/navbar/header-logo-or-title.tsx create mode 100644 services/web/frontend/js/features/ui/components/bootstrap-5/navbar/logged-in-items.tsx create mode 100644 services/web/frontend/js/features/ui/components/bootstrap-5/navbar/nav-dropdown-divider.tsx create mode 100644 services/web/frontend/js/features/ui/components/bootstrap-5/navbar/nav-dropdown-from-data.tsx create mode 100644 services/web/frontend/js/features/ui/components/bootstrap-5/navbar/nav-dropdown-item.tsx create mode 100644 services/web/frontend/js/features/ui/components/bootstrap-5/navbar/nav-dropdown-link-item.tsx create mode 100644 services/web/frontend/js/features/ui/components/bootstrap-5/navbar/nav-dropdown-menu.tsx create mode 100644 services/web/frontend/js/features/ui/components/bootstrap-5/navbar/nav-item-from-data.tsx create mode 100644 services/web/frontend/js/features/ui/components/bootstrap-5/navbar/nav-item.tsx create mode 100644 services/web/frontend/js/features/ui/components/bootstrap-5/navbar/nav-link-item.tsx create mode 100644 services/web/frontend/js/features/ui/components/bootstrap-5/navbar/util.ts create mode 100644 services/web/frontend/js/features/ui/components/types/default-navbar-metadata.ts create mode 100644 services/web/frontend/js/features/ui/components/types/navbar.ts diff --git a/services/web/app/src/infrastructure/ExpressLocals.js b/services/web/app/src/infrastructure/ExpressLocals.js index 0f4b117a6b..17fdbab086 100644 --- a/services/web/app/src/infrastructure/ExpressLocals.js +++ b/services/web/app/src/infrastructure/ExpressLocals.js @@ -227,6 +227,26 @@ module.exports = function (webRouter, privateApiRouter, publicApiRouter) { webRouter.use(function (req, res, next) { res.locals.translate = req.i18n.translate + const addTranslatedTextDeep = obj => { + if (_.isObject(obj)) { + if (_.has(obj, 'text')) { + obj.translatedText = req.i18n.translate(obj.text) + } + _.forOwn(obj, value => { + addTranslatedTextDeep(value) + }) + } + } + + // This function is used to add translations from the server for main + // navigation items because it's tricky to get them in the front end + // otherwise. + res.locals.cloneAndTranslateText = obj => { + const clone = _.cloneDeep(obj) + addTranslatedTextDeep(clone) + return clone + } + // Don't include the query string parameters, otherwise Google // treats ?nocdn=true as the canonical version try { diff --git a/services/web/app/views/_mixins/navbar.pug b/services/web/app/views/_mixins/navbar.pug index 274332f99c..f3482d3b54 100644 --- a/services/web/app/views/_mixins/navbar.pug +++ b/services/web/app/views/_mixins/navbar.pug @@ -3,7 +3,7 @@ mixin nav-item block mixin nav-link - a(role="menuitem")&attributes(attributes) + a(role="menuitem").nav-link&attributes(attributes) block mixin dropdown-menu diff --git a/services/web/app/views/layout-react.pug b/services/web/app/views/layout-react.pug new file mode 100644 index 0000000000..b0cc7eabd8 --- /dev/null +++ b/services/web/app/views/layout-react.pug @@ -0,0 +1,61 @@ +//- This is used for pages that are migrated to Bootstrap 5 but don't use Bootstrap's own JS, instead using +//- react-bootstrap for all Bootstrap components +extends ./layout-base + +include ./_mixins/formMessages +include ./_mixins/bootstrap_js + +block entrypointVar + - entrypoint = 'marketing' + +block append meta + if bootstrapVersion === 5 + - const canDisplayAdminMenu = hasAdminAccess() + - const canDisplayAdminRedirect = canRedirectToAdminDomain() + - const sessionUser = getSessionUser() + - const staffAccess = sessionUser?.staffAccess + - const canDisplaySplitTestMenu = hasFeature('saas') && (canDisplayAdminMenu || staffAccess?.splitTestMetrics || staffAccess?.splitTestManagement) + - const canDisplaySurveyMenu = hasFeature('saas') && canDisplayAdminMenu + - const enableUpgradeButton = projectDashboardReact && usersBestSubscription && usersBestSubscription.type === 'free' + + meta(name="ol-navbar" data-type="json" content={ + customLogo: settings.nav.custom_logo, + title: nav.title, + canDisplayAdminMenu, + canDisplayAdminRedirect, + canDisplaySplitTestMenu, + canDisplaySurveyMenu, + enableUpgradeButton, + suppressNavbarRight: !!suppressNavbarRight, + suppressNavContentLinks: !!suppressNavContentLinks, + showSubscriptionLink: nav.showSubscriptionLink, + sessionUser: sessionUser ? { email: sessionUser.email} : undefined, + adminUrl: settings.adminUrl, + items: cloneAndTranslateText(nav.header_extras) + }) + +block body + if (typeof suppressNavbar === "undefined") + if bootstrapVersion === 5 + include layout/navbar-marketing-react-bootstrap-5 + else + include layout/navbar-marketing + + block content + + if (typeof suppressFooter === "undefined") + if showThinFooter + include layout/footer-marketing + else + include layout/fat-footer + + if (typeof suppressCookieBanner === "undefined") + include _cookie_banner + + if bootstrapVersion === 3 + != moduleIncludes("contactModal-marketing", locals) + +block prepend foot-scripts + //- Only include Bootstrap JS if using Bootstrap 3 + if bootstrapVersion === 3 + +bootstrap-js(3) diff --git a/services/web/app/views/layout/navbar-marketing-react-bootstrap-5.pug b/services/web/app/views/layout/navbar-marketing-react-bootstrap-5.pug new file mode 100644 index 0000000000..ee0fcd2ff4 --- /dev/null +++ b/services/web/app/views/layout/navbar-marketing-react-bootstrap-5.pug @@ -0,0 +1 @@ +#navbar-container diff --git a/services/web/app/views/project/list-react.pug b/services/web/app/views/project/list-react.pug index 7e40ab4e21..ae8e350db7 100644 --- a/services/web/app/views/project/list-react.pug +++ b/services/web/app/views/project/list-react.pug @@ -1,4 +1,4 @@ -extends ../layout-marketing +extends ../layout-react block entrypointVar - entrypoint = 'pages/project-list' diff --git a/services/web/app/views/user/settings.pug b/services/web/app/views/user/settings.pug index 0f444765e6..e0584ac3cf 100644 --- a/services/web/app/views/user/settings.pug +++ b/services/web/app/views/user/settings.pug @@ -1,4 +1,4 @@ -extends ../layout-marketing +extends ../layout-react block entrypointVar - entrypoint = 'pages/user/settings' diff --git a/services/web/frontend/extracted-translations.json b/services/web/frontend/extracted-translations.json index 9b26a618c2..a1e1381a62 100644 --- a/services/web/frontend/extracted-translations.json +++ b/services/web/frontend/extracted-translations.json @@ -2,6 +2,8 @@ "1_2_width": "", "1_4_width": "", "3_4_width": "", + "Account": "", + "Account Settings": "", "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": "", @@ -770,6 +772,7 @@ "log_entry_maximum_entries_title": "", "log_hint_extra_info": "", "log_in_with_primary_email_address": "", + "log_out": "", "log_out_lowercase_dot": "", "log_viewer_error": "", "logging_in_or_managing_your_account": "", @@ -782,6 +785,7 @@ "lost_connection": "", "main_document": "", "main_file_not_found": "", + "main_navigation": "", "make_a_copy": "", "make_email_primary_description": "", "make_owner": "", @@ -1358,6 +1362,7 @@ "submit_title": "", "subscribe": "", "subscribe_to_find_the_symbols_you_need_faster": "", + "subscription": "", "subscription_admins_cannot_be_deleted": "", "subscription_canceled": "", "subscription_canceled_and_terminate_on_x": "", diff --git a/services/web/frontend/js/features/header-footer-react/index.tsx b/services/web/frontend/js/features/header-footer-react/index.tsx new file mode 100644 index 0000000000..fa0e9a5cff --- /dev/null +++ b/services/web/frontend/js/features/header-footer-react/index.tsx @@ -0,0 +1,9 @@ +import ReactDOM from 'react-dom' +import DefaultNavbar from '@/features/ui/components/bootstrap-5/navbar/default-navbar' +import getMeta from '@/utils/meta' + +const element = document.getElementById('navbar-container') +if (element) { + const navbarProps = getMeta('ol-navbar') + ReactDOM.render(, element) +} diff --git a/services/web/frontend/js/features/ui/components/bootstrap-5/dropdown-menu.tsx b/services/web/frontend/js/features/ui/components/bootstrap-5/dropdown-menu.tsx index 18396d0998..ff116eac27 100644 --- a/services/web/frontend/js/features/ui/components/bootstrap-5/dropdown-menu.tsx +++ b/services/web/frontend/js/features/ui/components/bootstrap-5/dropdown-menu.tsx @@ -99,8 +99,8 @@ export const DropdownMenu = forwardRef< }) DropdownMenu.displayName = 'DropdownMenu' -export function DropdownDivider({ as = 'li' }: DropdownDividerProps) { - return +export function DropdownDivider({ as = 'li', ...props }: DropdownDividerProps) { + return } export function DropdownHeader({ as = 'li', ...props }: DropdownHeaderProps) { diff --git a/services/web/frontend/js/features/ui/components/bootstrap-5/navbar/admin-menu.tsx b/services/web/frontend/js/features/ui/components/bootstrap-5/navbar/admin-menu.tsx new file mode 100644 index 0000000000..2c65c19cb8 --- /dev/null +++ b/services/web/frontend/js/features/ui/components/bootstrap-5/navbar/admin-menu.tsx @@ -0,0 +1,49 @@ +import type { DefaultNavbarMetadata } from '@/features/ui/components/types/default-navbar-metadata' +import NavDropdownMenu from '@/features/ui/components/bootstrap-5/navbar/nav-dropdown-menu' +import NavDropdownLinkItem from '@/features/ui/components/bootstrap-5/navbar/nav-dropdown-link-item' + +export default function AdminMenu({ + canDisplayAdminMenu, + canDisplayAdminRedirect, + canDisplaySplitTestMenu, + canDisplaySurveyMenu, + adminUrl, +}: Pick< + DefaultNavbarMetadata, + | 'canDisplayAdminMenu' + | 'canDisplayAdminRedirect' + | 'canDisplaySplitTestMenu' + | 'canDisplaySurveyMenu' + | 'adminUrl' +>) { + return ( + + {canDisplayAdminMenu ? ( + <> + Manage Site + + Manage Users + + + Project URL lookup + + + ) : null} + {canDisplayAdminRedirect && adminUrl ? ( + + Switch to Admin + + ) : null} + {canDisplaySplitTestMenu ? ( + + Manage Feature Flags + + ) : null} + {canDisplaySurveyMenu ? ( + + Manage Surveys + + ) : null} + + ) +} diff --git a/services/web/frontend/js/features/ui/components/bootstrap-5/navbar/contact-us-item.tsx b/services/web/frontend/js/features/ui/components/bootstrap-5/navbar/contact-us-item.tsx new file mode 100644 index 0000000000..27af356580 --- /dev/null +++ b/services/web/frontend/js/features/ui/components/bootstrap-5/navbar/contact-us-item.tsx @@ -0,0 +1,25 @@ +import NavDropdownLinkItem from '@/features/ui/components/bootstrap-5/navbar/nav-dropdown-link-item' +import { sendMB } from '@/infrastructure/event-tracking' +import { useTranslation } from 'react-i18next' +import { useContactUsModal } from '@/shared/hooks/use-contact-us-modal' +import { UserProvider } from '@/shared/context/user-context' + +export default function ContactUsItem() { + const { t } = useTranslation() + const { modal, showModal } = useContactUsModal({ autofillProjectUrl: false }) + + return ( + <> + { + sendMB('menu-clicked-contact') + showModal() + }} + > + {t('contact_us')} + + {modal} + + ) +} diff --git a/services/web/frontend/js/features/ui/components/bootstrap-5/navbar/default-navbar.tsx b/services/web/frontend/js/features/ui/components/bootstrap-5/navbar/default-navbar.tsx new file mode 100644 index 0000000000..5421da70f4 --- /dev/null +++ b/services/web/frontend/js/features/ui/components/bootstrap-5/navbar/default-navbar.tsx @@ -0,0 +1,119 @@ +import { sendMB } from '@/infrastructure/event-tracking' +import { useTranslation } from 'react-i18next' +import { Button, Container, Nav, Navbar } from 'react-bootstrap-5' +import useWaitForI18n from '@/shared/hooks/use-wait-for-i18n' +import AdminMenu from '@/features/ui/components/bootstrap-5/navbar/admin-menu' +import type { DefaultNavbarMetadata } from '@/features/ui/components/types/default-navbar-metadata' +import NavItemFromData from '@/features/ui/components/bootstrap-5/navbar/nav-item-from-data' +import LoggedInItems from '@/features/ui/components/bootstrap-5/navbar/logged-in-items' +import HeaderLogoOrTitle from '@/features/ui/components/bootstrap-5/navbar/header-logo-or-title' +import MaterialIcon from '@/shared/components/material-icon' + +function LoggedOutItems() { + return Logged out +} + +function DefaultNavbar(props: DefaultNavbarMetadata) { + const { + customLogo, + title, + canDisplayAdminMenu, + canDisplayAdminRedirect, + canDisplaySplitTestMenu, + canDisplaySurveyMenu, + enableUpgradeButton, + suppressNavbarRight, + suppressNavContentLinks, + showSubscriptionLink, + sessionUser, + adminUrl, + items, + } = props + const { t } = useTranslation() + const { isReady } = useWaitForI18n() + + if (!isReady) { + return null + } + + return ( + <> + + +
+ + {enableUpgradeButton ? ( + + ) : null} +
+ {suppressNavbarRight ? null : ( + <> + + + + + + )} +
+
+ + ) +} + +export default DefaultNavbar diff --git a/services/web/frontend/js/features/ui/components/bootstrap-5/navbar/header-logo-or-title.tsx b/services/web/frontend/js/features/ui/components/bootstrap-5/navbar/header-logo-or-title.tsx new file mode 100644 index 0000000000..44500f1b82 --- /dev/null +++ b/services/web/frontend/js/features/ui/components/bootstrap-5/navbar/header-logo-or-title.tsx @@ -0,0 +1,32 @@ +import type { DefaultNavbarMetadata } from '@/features/ui/components/types/default-navbar-metadata' +import getMeta from '@/utils/meta' + +export default function HeaderLogoOrTitle({ + customLogo, + title, +}: Pick) { + const { appName } = getMeta('ol-ExposedSettings') + + if (customLogo) { + return ( + // eslint-disable-next-line jsx-a11y/anchor-has-content + + ) + } else if (title) { + return ( + + {title} + + ) + } else { + return ( + // eslint-disable-next-line jsx-a11y/anchor-has-content + + ) + } +} diff --git a/services/web/frontend/js/features/ui/components/bootstrap-5/navbar/logged-in-items.tsx b/services/web/frontend/js/features/ui/components/bootstrap-5/navbar/logged-in-items.tsx new file mode 100644 index 0000000000..b108fccce3 --- /dev/null +++ b/services/web/frontend/js/features/ui/components/bootstrap-5/navbar/logged-in-items.tsx @@ -0,0 +1,60 @@ +import { useTranslation } from 'react-i18next' +import { Dropdown } from 'react-bootstrap-5' +import NavDropdownDivider from '@/features/ui/components/bootstrap-5/navbar/nav-dropdown-divider' +import getMeta from '@/utils/meta' +import NavDropdownMenu from '@/features/ui/components/bootstrap-5/navbar/nav-dropdown-menu' +import NavDropdownLinkItem from '@/features/ui/components/bootstrap-5/navbar/nav-dropdown-link-item' +import NavDropdownItem from '@/features/ui/components/bootstrap-5/navbar/nav-dropdown-item' +import type { NavbarSessionUser } from '@/features/ui/components/types/navbar' +import NavLinkItem from '@/features/ui/components/bootstrap-5/navbar/nav-link-item' + +export default function LoggedInItems({ + sessionUser, + showSubscriptionLink, +}: { + sessionUser: NavbarSessionUser + showSubscriptionLink: boolean +}) { + const { t } = useTranslation() + const logOutFormId = 'logOutForm' + + return ( + <> + {t('projects')} + + + {sessionUser.email} + + + + {t('Account Settings')} + + {showSubscriptionLink ? ( + + {t('subscription')} + + ) : null} + + + { + // The button is outside the form but still belongs to it via the + // form attribute. The reason to do this is that if the button is + // inside the form, screen readers will not count it in the total + // number of menu items + } + + {t('log_out')} + +
+ +
+
+
+ + ) +} diff --git a/services/web/frontend/js/features/ui/components/bootstrap-5/navbar/nav-dropdown-divider.tsx b/services/web/frontend/js/features/ui/components/bootstrap-5/navbar/nav-dropdown-divider.tsx new file mode 100644 index 0000000000..a9bf2ed41d --- /dev/null +++ b/services/web/frontend/js/features/ui/components/bootstrap-5/navbar/nav-dropdown-divider.tsx @@ -0,0 +1,5 @@ +import { DropdownDivider } from '@/features/ui/components/bootstrap-5/dropdown-menu' + +export default function NavDropdownDivider() { + return +} diff --git a/services/web/frontend/js/features/ui/components/bootstrap-5/navbar/nav-dropdown-from-data.tsx b/services/web/frontend/js/features/ui/components/bootstrap-5/navbar/nav-dropdown-from-data.tsx new file mode 100644 index 0000000000..98d42ec35c --- /dev/null +++ b/services/web/frontend/js/features/ui/components/bootstrap-5/navbar/nav-dropdown-from-data.tsx @@ -0,0 +1,44 @@ +import type { NavbarDropdownItemData } from '@/features/ui/components/types/navbar' +import NavDropdownDivider from '@/features/ui/components/bootstrap-5/navbar/nav-dropdown-divider' +import { sendMB } from '@/infrastructure/event-tracking' +import { isDropdownLinkItem } from '@/features/ui/components/bootstrap-5/navbar/util' +import NavDropdownLinkItem from '@/features/ui/components/bootstrap-5/navbar/nav-dropdown-link-item' +import NavDropdownItem from '@/features/ui/components/bootstrap-5/navbar/nav-dropdown-item' +import NavDropdownMenu from '@/features/ui/components/bootstrap-5/navbar/nav-dropdown-menu' +import ContactUsItem from '@/features/ui/components/bootstrap-5/navbar/contact-us-item' + +export default function NavDropdownFromData({ + item, +}: { + item: NavbarDropdownItemData +}) { + return ( + + {item.dropdown.map((child, index) => { + if ('divider' in child) { + return + } else if ('isContactUs' in child) { + return + } else if (isDropdownLinkItem(child)) { + return ( + { + sendMB(child.event) + }} + > + {child.translatedText} + + ) + } else { + return ( + + {child.translatedText} + + ) + } + })} + + ) +} diff --git a/services/web/frontend/js/features/ui/components/bootstrap-5/navbar/nav-dropdown-item.tsx b/services/web/frontend/js/features/ui/components/bootstrap-5/navbar/nav-dropdown-item.tsx new file mode 100644 index 0000000000..b23c29cbd7 --- /dev/null +++ b/services/web/frontend/js/features/ui/components/bootstrap-5/navbar/nav-dropdown-item.tsx @@ -0,0 +1,5 @@ +import { ReactNode } from 'react' + +export default function NavDropdownItem({ children }: { children: ReactNode }) { + return
  • {children}
  • +} diff --git a/services/web/frontend/js/features/ui/components/bootstrap-5/navbar/nav-dropdown-link-item.tsx b/services/web/frontend/js/features/ui/components/bootstrap-5/navbar/nav-dropdown-link-item.tsx new file mode 100644 index 0000000000..235a2227e7 --- /dev/null +++ b/services/web/frontend/js/features/ui/components/bootstrap-5/navbar/nav-dropdown-link-item.tsx @@ -0,0 +1,22 @@ +import { ReactNode } from 'react' +import NavDropdownItem from '@/features/ui/components/bootstrap-5/navbar/nav-dropdown-item' +import { DropdownItem } from 'react-bootstrap-5' +import { DropdownItemProps } from 'react-bootstrap-5/DropdownItem' + +export default function NavDropdownLinkItem({ + href, + onClick, + children, +}: { + href: string + onClick?: DropdownItemProps['onClick'] + children: ReactNode +}) { + return ( + + + {children} + + + ) +} diff --git a/services/web/frontend/js/features/ui/components/bootstrap-5/navbar/nav-dropdown-menu.tsx b/services/web/frontend/js/features/ui/components/bootstrap-5/navbar/nav-dropdown-menu.tsx new file mode 100644 index 0000000000..d98613aa9d --- /dev/null +++ b/services/web/frontend/js/features/ui/components/bootstrap-5/navbar/nav-dropdown-menu.tsx @@ -0,0 +1,23 @@ +import { ReactNode } from 'react' +import { Dropdown } from 'react-bootstrap-5' + +export default function NavDropdownMenu({ + title, + className, + children, +}: { + title: string + className?: string + children: ReactNode +}) { + // Can't use a NavDropdown here because it's impossible to render the menu as + // a
      element using NavDropdown + return ( + + {title} + + {children} + + + ) +} diff --git a/services/web/frontend/js/features/ui/components/bootstrap-5/navbar/nav-item-from-data.tsx b/services/web/frontend/js/features/ui/components/bootstrap-5/navbar/nav-item-from-data.tsx new file mode 100644 index 0000000000..b81ff6ecb5 --- /dev/null +++ b/services/web/frontend/js/features/ui/components/bootstrap-5/navbar/nav-item-from-data.tsx @@ -0,0 +1,29 @@ +import type { NavbarItemData } from '@/features/ui/components/types/navbar' +import { + isDropdownItem, + isLinkItem, +} from '@/features/ui/components/bootstrap-5/navbar/util' +import NavDropdownFromData from '@/features/ui/components/bootstrap-5/navbar/nav-dropdown-from-data' +import NavItem from '@/features/ui/components/bootstrap-5/navbar/nav-item' +import { sendMB } from '@/infrastructure/event-tracking' +import NavLinkItem from '@/features/ui/components/bootstrap-5/navbar/nav-link-item' + +export default function NavItemFromData({ item }: { item: NavbarItemData }) { + if (isDropdownItem(item)) { + return + } else if (isLinkItem(item)) { + return ( + { + sendMB(item.event) + }} + > + {item.translatedText} + + ) + } else { + return {item.translatedText} + } +} diff --git a/services/web/frontend/js/features/ui/components/bootstrap-5/navbar/nav-item.tsx b/services/web/frontend/js/features/ui/components/bootstrap-5/navbar/nav-item.tsx new file mode 100644 index 0000000000..58295a47eb --- /dev/null +++ b/services/web/frontend/js/features/ui/components/bootstrap-5/navbar/nav-item.tsx @@ -0,0 +1,10 @@ +import { Nav, NavItemProps } from 'react-bootstrap-5' + +export default function NavItem(props: Omit) { + const { children, ...rest } = props + return ( + + {children} + + ) +} diff --git a/services/web/frontend/js/features/ui/components/bootstrap-5/navbar/nav-link-item.tsx b/services/web/frontend/js/features/ui/components/bootstrap-5/navbar/nav-link-item.tsx new file mode 100644 index 0000000000..4e3875f5bb --- /dev/null +++ b/services/web/frontend/js/features/ui/components/bootstrap-5/navbar/nav-link-item.tsx @@ -0,0 +1,23 @@ +import { ReactNode } from 'react' +import { Nav } from 'react-bootstrap-5' +import NavItem from '@/features/ui/components/bootstrap-5/navbar/nav-item' + +export default function NavLinkItem({ + href, + className, + onClick, + children, +}: { + href: string + className?: string + onClick?: React.ComponentProps['onClick'] + children: ReactNode +}) { + return ( + + + {children} + + + ) +} diff --git a/services/web/frontend/js/features/ui/components/bootstrap-5/navbar/util.ts b/services/web/frontend/js/features/ui/components/bootstrap-5/navbar/util.ts new file mode 100644 index 0000000000..d769b289fa --- /dev/null +++ b/services/web/frontend/js/features/ui/components/bootstrap-5/navbar/util.ts @@ -0,0 +1,23 @@ +import type { + NavbarDropdownItem, + NavbarDropdownItemData, + NavbarDropdownLinkItem, + NavbarItemData, + NavbarLinkItemData, +} from '@/features/ui/components/types/navbar' + +export function isDropdownLinkItem( + item: NavbarDropdownItem +): item is NavbarDropdownLinkItem { + return 'url' in item +} + +export function isDropdownItem( + item: NavbarItemData +): item is NavbarDropdownItemData { + return 'dropdown' in item +} + +export function isLinkItem(item: NavbarItemData): item is NavbarLinkItemData { + return 'url' in item +} diff --git a/services/web/frontend/js/features/ui/components/types/default-navbar-metadata.ts b/services/web/frontend/js/features/ui/components/types/default-navbar-metadata.ts new file mode 100644 index 0000000000..5cb1b08231 --- /dev/null +++ b/services/web/frontend/js/features/ui/components/types/default-navbar-metadata.ts @@ -0,0 +1,20 @@ +import type { + NavbarItemData, + NavbarSessionUser, +} from '@/features/ui/components/types/navbar' + +export type DefaultNavbarMetadata = { + customLogo?: string + title?: string + canDisplayAdminMenu: boolean + canDisplayAdminRedirect: boolean + canDisplaySplitTestMenu: boolean + canDisplaySurveyMenu: boolean + enableUpgradeButton: boolean + suppressNavbarRight: boolean + suppressNavContentLinks: boolean + showSubscriptionLink: boolean + sessionUser?: NavbarSessionUser + adminUrl?: string + items: NavbarItemData[] +} diff --git a/services/web/frontend/js/features/ui/components/types/dropdown-menu-props.ts b/services/web/frontend/js/features/ui/components/types/dropdown-menu-props.ts index c952b0dd9f..6118c5c8ce 100644 --- a/services/web/frontend/js/features/ui/components/types/dropdown-menu-props.ts +++ b/services/web/frontend/js/features/ui/components/types/dropdown-menu-props.ts @@ -55,6 +55,7 @@ export type DropdownMenuProps = PropsWithChildren<{ export type DropdownDividerProps = PropsWithChildren<{ as?: ElementType + className?: string }> export type DropdownHeaderProps = PropsWithChildren<{ diff --git a/services/web/frontend/js/features/ui/components/types/navbar.ts b/services/web/frontend/js/features/ui/components/types/navbar.ts new file mode 100644 index 0000000000..2ab49e18a9 --- /dev/null +++ b/services/web/frontend/js/features/ui/components/types/navbar.ts @@ -0,0 +1,52 @@ +export interface NavbarDropdownDivider { + divider: true +} + +export interface NavbarDropdownContactUsItem { + isContactUs: true +} + +export interface NavbarDropdownTextItem { + text: string + translatedText: string + class?: string +} + +export interface NavbarDropdownLinkItem extends NavbarDropdownTextItem { + url: string + event: string + eventSegmentation?: Record +} + +export type NavbarDropdownItem = + | NavbarDropdownDivider + | NavbarDropdownContactUsItem + | NavbarDropdownTextItem + | NavbarDropdownLinkItem + +export type NavbarItemDropdownData = NavbarDropdownItem[] + +export interface NavbarTextItemData { + text: string + translatedText: string + only_when_logged_in?: boolean + only_when_logged_out?: boolean + only_content_pages?: boolean + class?: string +} + +export interface NavbarDropdownItemData extends NavbarTextItemData { + dropdown: NavbarItemDropdownData +} + +export interface NavbarLinkItemData extends NavbarTextItemData { + url: string + event: string +} + +export type NavbarItemData = + | NavbarDropdownItemData + | NavbarLinkItemData + | NavbarTextItemData + +export type NavbarSessionUser = { email: string } diff --git a/services/web/frontend/js/features/utils/bootstrap-5.ts b/services/web/frontend/js/features/utils/bootstrap-5.ts index 371cc7fee4..b201c6a1fa 100644 --- a/services/web/frontend/js/features/utils/bootstrap-5.ts +++ b/services/web/frontend/js/features/utils/bootstrap-5.ts @@ -1,7 +1,7 @@ import getMeta from '@/utils/meta' -// The reason this is a function to ensure that meta tag is read before any -// isBootstrap5 check is performed +// The reason this is a function is to ensure that the meta tag is read before +// any isBootstrap5 check is performed export const isBootstrap5 = () => getMeta('ol-bootstrapVersion') === 5 export const bsVersion = ({ bs5, bs3 }: { bs5?: string; bs3?: string }) => { diff --git a/services/web/frontend/js/marketing.js b/services/web/frontend/js/marketing.js index 5e360b2dd7..ad32a0d29b 100644 --- a/services/web/frontend/js/marketing.js +++ b/services/web/frontend/js/marketing.js @@ -9,3 +9,4 @@ import './features/multi-submit' import './features/cookie-banner' import './features/autoplay-video' import './features/mathjax' +import './features/header-footer-react' diff --git a/services/web/frontend/js/pages/project-list.jsx b/services/web/frontend/js/pages/project-list.jsx index 744f5f1bf7..68f4564c46 100644 --- a/services/web/frontend/js/pages/project-list.jsx +++ b/services/web/frontend/js/pages/project-list.jsx @@ -2,10 +2,10 @@ import './../utils/meta' import './../utils/webpack-public-path' import './../infrastructure/error-reporter' import './../i18n' -import '../features/contact-form' import '../features/event-tracking' import '../features/cookie-banner' import '../features/link-helpers/slow-link' +import '../features/header-footer-react' import ReactDOM from 'react-dom' import ProjectListRoot from '../features/project-list/components/project-list-root' diff --git a/services/web/frontend/js/utils/meta.ts b/services/web/frontend/js/utils/meta.ts index f009afa050..57d76075ca 100644 --- a/services/web/frontend/js/utils/meta.ts +++ b/services/web/frontend/js/utils/meta.ts @@ -46,6 +46,7 @@ import { PasswordStrengthOptions } from '../../../types/password-strength-option import { Subscription as ProjectDashboardSubscription } from '../../../types/project/dashboard/subscription' import { ThirdPartyIds } from '../../../types/third-party-ids' import { Publisher } from '../../../types/subscription/dashboard/publisher' +import { DefaultNavbarMetadata } from '@/features/ui/components/types/default-navbar-metadata' export interface Meta { 'ol-ExposedSettings': ExposedSettings @@ -135,6 +136,7 @@ export interface Meta { 'ol-memberGroupSubscriptions': MemberGroupSubscription[] 'ol-memberOfSSOEnabledGroups': GroupSSOLinkingStatus[] 'ol-members': MinimalUser[] + 'ol-navbar': DefaultNavbarMetadata 'ol-no-single-dollar': boolean 'ol-notifications': NotificationType[] 'ol-notificationsInstitution': InstitutionType[] diff --git a/services/web/frontend/stylesheets/bootstrap-5/abstracts/variable-overrides.scss b/services/web/frontend/stylesheets/bootstrap-5/abstracts/variable-overrides.scss index 1a1ba3c8aa..39277ca24b 100644 --- a/services/web/frontend/stylesheets/bootstrap-5/abstracts/variable-overrides.scss +++ b/services/web/frontend/stylesheets/bootstrap-5/abstracts/variable-overrides.scss @@ -204,6 +204,9 @@ $navbar-toggler-padding-x: var(--spacing-05); $navbar-toggler-padding-y: var(--spacing-02); $navbar-toggler-border-radius: var(--border-radius-base); +// Nav links +$navbar-nav-link-padding-x: var(--navbar-btn-padding-h); + // Spacing scale used by Bootstrap utilities $spacer: $spacing-06; $spacers: ( diff --git a/services/web/frontend/stylesheets/bootstrap-5/components/navbar.scss b/services/web/frontend/stylesheets/bootstrap-5/components/navbar.scss index 2e6cc0452a..d4ec9e59bd 100644 --- a/services/web/frontend/stylesheets/bootstrap-5/components/navbar.scss +++ b/services/web/frontend/stylesheets/bootstrap-5/components/navbar.scss @@ -45,7 +45,7 @@ > li { display: inline-flex; - > a, + > .nav-link, > .dropdown-toggle { display: block; color: var(--navbar-link-color); @@ -69,7 +69,7 @@ } } - &.subdued > a, + &.subdued > .nav-link, &.subdued > .dropdown-toggle { border: 0; color: var(--navbar-subdued-color); @@ -87,6 +87,8 @@ } .navbar-toggler { + --bs-navbar-toggler-padding-x: var(--spacing-04); + color: var(--navbar-link-color); border-radius: var(--border-radius-base); border-width: 0; @@ -97,6 +99,12 @@ background-color: var(--navbar-toggler-expanded-bg); transition: 0.2s ease-in; } + + & .material-symbols { + font-size: inherit; + font-weight: bold; + vertical-align: middle; + } } .navbar-collapse, @@ -129,7 +137,8 @@ > a, > .dropdown-toggle { margin: 0; - padding: var(--spacing-05) var(--navbar-padding-h); + padding-top: var(--spacing-05); + padding-bottom: var(--spacing-05); width: 100%; text-align: left; border-radius: 0; diff --git a/services/web/locales/en.json b/services/web/locales/en.json index 0da35425b5..6ff74fc92e 100644 --- a/services/web/locales/en.json +++ b/services/web/locales/en.json @@ -674,11 +674,11 @@ "footer_about_us": "About us", "footer_contact_us": "Contact us", "footer_navigation": "Footer navigation", - "footer_plans_and_pricing": "Plans & pricing", + "footer_plans_and_pricing": "Plans & pricing", "for_business": "For business", "for_enterprise": "For enterprise", "for_groups_or_site_wide": "For groups or site-wide", - "for_individuals_and_groups": "For individuals & groups", + "for_individuals_and_groups": "For individuals & groups", "for_large_institutions_and_organizations_need_sitewide_on_premise": "For large institutions and organizations that need site-wide access or an on-premises solution.", "for_more_information_see_managed_accounts_section": "For more information, see the \"Managed Accounts\" section in <0>our terms of use, which you agree to by clicking Accept invitation.", "for_publishers": "For publishers", @@ -1151,6 +1151,7 @@ "lost_connection": "Lost Connection", "main_document": "Main document", "main_file_not_found": "Unknown main document", + "main_navigation": "Main navigation", "maintenance": "Maintenance", "make_a_copy": "Make a copy", "make_email_primary_description": "Make this the primary email, used to log in", @@ -1443,7 +1444,7 @@ "plan": "Plan", "plan_tooltip": "You’re on the __plan__ plan. Click to find out how to make the most of your Overleaf premium features.", "planned_maintenance": "Planned Maintenance", - "plans_amper_pricing": "Plans & Pricing", + "plans_amper_pricing": "Plans & Pricing", "plans_and_pricing": "Plans and Pricing", "plans_and_pricing_lowercase": "plans and pricing", "please_ask_the_project_owner_to_upgrade_to_track_changes": "Please ask the project owner to upgrade to use track changes", @@ -1486,7 +1487,7 @@ "premium_plan_label": "You’re using Overleaf Premium", "presentation": "Presentation", "presentation_mode": "Presentation mode", - "press_and_awards": "Press & awards", + "press_and_awards": "Press & awards", "previous_page": "Previous page", "price": "Price", "primarily_work_study_question": "Where do you primarily work or study?",