first commit
This commit is contained in:
@@ -0,0 +1,93 @@
|
||||
import { OverlayTrigger, Tooltip } from 'react-bootstrap-5'
|
||||
import type { MergeAndOverride } from '../../../../../../types/utils'
|
||||
import BadgeLink, { type BadgeLinkProps } from './badge-link'
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import classNames from 'classnames'
|
||||
|
||||
type BadgeLinkWithTooltipProps = MergeAndOverride<
|
||||
BadgeLinkProps,
|
||||
{
|
||||
placement?: 'top' | 'right' | 'bottom' | 'left'
|
||||
tooltipTitle: string
|
||||
}
|
||||
>
|
||||
|
||||
function getElementWidth(el: Element) {
|
||||
const elComputedStyle = window.getComputedStyle(el)
|
||||
const elPaddingX =
|
||||
parseFloat(elComputedStyle.paddingLeft) +
|
||||
parseFloat(elComputedStyle.paddingRight)
|
||||
const elBorderX =
|
||||
parseFloat(elComputedStyle.borderLeftWidth) +
|
||||
parseFloat(elComputedStyle.borderRightWidth)
|
||||
return el.scrollWidth - elPaddingX - elBorderX
|
||||
}
|
||||
|
||||
function BadgeLinkWithTooltip({
|
||||
children,
|
||||
tooltipTitle,
|
||||
placement,
|
||||
...rest
|
||||
}: BadgeLinkWithTooltipProps) {
|
||||
const badgeContentRef = useRef<HTMLElement>(null)
|
||||
const [showTooltip, setShowTooltip] = useState(true)
|
||||
const [noMaxWidth, setNoMaxWidth] = useState(false)
|
||||
|
||||
const badgeLinkClasses = classNames({ 'badge-link-no-max-width': noMaxWidth })
|
||||
|
||||
const renderTooltip = (props: any) => {
|
||||
if (showTooltip) {
|
||||
return (
|
||||
<Tooltip
|
||||
id={`badge-tooltip-${rest.href.replace(/\//g, '-')}`}
|
||||
{...props}
|
||||
>
|
||||
{tooltipTitle}
|
||||
</Tooltip>
|
||||
)
|
||||
} else {
|
||||
return <></>
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (badgeContentRef.current) {
|
||||
// Check if tooltip needed.
|
||||
// If .badge-content does not extend beyond max-width limit on
|
||||
// .badge then tooltip is not needed. max-width is always
|
||||
// removed when withTooltip exists and tooltip is not needed
|
||||
// to avoid any differences in width calculation after font
|
||||
// loaded (for example, Noto sans). Othwerise, badge might get
|
||||
// clipped due to font loaded causing .badge-content to be
|
||||
// greater than .badge max-width and no tooltip was determined
|
||||
// to be needed with default font (for example, sans-serif)
|
||||
const badgeContentWidth = badgeContentRef.current.scrollWidth
|
||||
if (badgeContentRef.current?.parentElement) {
|
||||
const badgeWidth = getElementWidth(
|
||||
badgeContentRef.current?.parentElement
|
||||
)
|
||||
if (badgeContentWidth <= badgeWidth) {
|
||||
// no tooltip and remove max-width
|
||||
setNoMaxWidth(true)
|
||||
setShowTooltip(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<OverlayTrigger placement={placement || 'bottom'} overlay={renderTooltip}>
|
||||
<span>
|
||||
<BadgeLink
|
||||
{...rest}
|
||||
badgeContentRef={badgeContentRef}
|
||||
badgeLinkClasses={badgeLinkClasses}
|
||||
>
|
||||
{children}
|
||||
</BadgeLink>
|
||||
</span>
|
||||
</OverlayTrigger>
|
||||
)
|
||||
}
|
||||
|
||||
export default BadgeLinkWithTooltip
|
@@ -0,0 +1,31 @@
|
||||
import classNames from 'classnames'
|
||||
import type { MergeAndOverride } from '../../../../../../types/utils'
|
||||
import Badge, { type BadgeProps } from './badge'
|
||||
|
||||
export type BadgeLinkProps = MergeAndOverride<
|
||||
BadgeProps,
|
||||
{
|
||||
href: string
|
||||
badgeContentRef?: React.RefObject<HTMLElement>
|
||||
badgeLinkClasses?: string
|
||||
}
|
||||
>
|
||||
|
||||
function BadgeLink({
|
||||
href,
|
||||
badgeLinkClasses,
|
||||
children,
|
||||
...badgeProps
|
||||
}: BadgeLinkProps) {
|
||||
const containerClass = classNames(badgeLinkClasses, 'badge-link', {
|
||||
[`badge-link-${badgeProps.bg}`]: badgeProps.bg,
|
||||
})
|
||||
|
||||
return (
|
||||
<a className={containerClass} href={href}>
|
||||
<Badge {...badgeProps}>{children}</Badge>
|
||||
</a>
|
||||
)
|
||||
}
|
||||
|
||||
export default BadgeLink
|
@@ -0,0 +1,23 @@
|
||||
import { Badge as BSBadge, BadgeProps as BSBadgeProps } from 'react-bootstrap-5'
|
||||
import { MergeAndOverride } from '../../../../../../types/utils'
|
||||
|
||||
export type BadgeProps = MergeAndOverride<
|
||||
BSBadgeProps,
|
||||
{
|
||||
prepend?: React.ReactNode
|
||||
badgeContentRef?: React.RefObject<HTMLElement>
|
||||
}
|
||||
>
|
||||
|
||||
function Badge({ prepend, children, badgeContentRef, ...rest }: BadgeProps) {
|
||||
return (
|
||||
<BSBadge {...rest}>
|
||||
{prepend && <span className="badge-prepend">{prepend}</span>}
|
||||
<span className="badge-content" ref={badgeContentRef}>
|
||||
{children}
|
||||
</span>
|
||||
</BSBadge>
|
||||
)
|
||||
}
|
||||
|
||||
export default Badge
|
@@ -0,0 +1,81 @@
|
||||
import { forwardRef } from 'react'
|
||||
import { Button as BS5Button, Spinner } from 'react-bootstrap-5'
|
||||
import type { ButtonProps } from '@/features/ui/components/types/button-props'
|
||||
import classNames from 'classnames'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import MaterialIcon from '@/shared/components/material-icon'
|
||||
|
||||
const Button = forwardRef<HTMLButtonElement, ButtonProps>(
|
||||
(
|
||||
{
|
||||
children,
|
||||
className,
|
||||
leadingIcon,
|
||||
isLoading = false,
|
||||
loadingLabel,
|
||||
trailingIcon,
|
||||
variant = 'primary',
|
||||
...props
|
||||
},
|
||||
ref
|
||||
) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const buttonClassName = classNames('d-inline-grid', className, {
|
||||
'button-loading': isLoading,
|
||||
})
|
||||
|
||||
const loadingSpinnerClassName =
|
||||
props.size === 'lg' ? 'loading-spinner-large' : 'loading-spinner-small'
|
||||
const materialIconClassName =
|
||||
props.size === 'lg' ? 'icon-large' : 'icon-small'
|
||||
|
||||
const leadingIconComponent =
|
||||
leadingIcon && typeof leadingIcon === 'string' ? (
|
||||
<MaterialIcon type={leadingIcon} className={materialIconClassName} />
|
||||
) : (
|
||||
leadingIcon
|
||||
)
|
||||
|
||||
const trailingIconComponent =
|
||||
trailingIcon && typeof trailingIcon === 'string' ? (
|
||||
<MaterialIcon type={trailingIcon} className={materialIconClassName} />
|
||||
) : (
|
||||
trailingIcon
|
||||
)
|
||||
|
||||
return (
|
||||
<BS5Button
|
||||
className={buttonClassName}
|
||||
variant={variant}
|
||||
{...props}
|
||||
ref={ref}
|
||||
disabled={isLoading || props.disabled}
|
||||
data-ol-loading={isLoading}
|
||||
>
|
||||
{isLoading && (
|
||||
<span className="spinner-container">
|
||||
<Spinner
|
||||
animation="border"
|
||||
aria-hidden="true"
|
||||
as="span"
|
||||
className={loadingSpinnerClassName}
|
||||
role="status"
|
||||
/>
|
||||
<span className="visually-hidden">
|
||||
{loadingLabel ?? t('loading')}
|
||||
</span>
|
||||
</span>
|
||||
)}
|
||||
<span className="button-content" aria-hidden={isLoading}>
|
||||
{leadingIconComponent}
|
||||
{children}
|
||||
{trailingIconComponent}
|
||||
</span>
|
||||
</BS5Button>
|
||||
)
|
||||
}
|
||||
)
|
||||
Button.displayName = 'Button'
|
||||
|
||||
export default Button
|
@@ -0,0 +1,9 @@
|
||||
import { ReactNode } from 'react'
|
||||
|
||||
export default function DropdownListItem({
|
||||
children,
|
||||
}: {
|
||||
children: ReactNode
|
||||
}) {
|
||||
return <li role="none">{children}</li>
|
||||
}
|
@@ -0,0 +1,146 @@
|
||||
import React, { forwardRef } from 'react'
|
||||
import {
|
||||
Dropdown as BS5Dropdown,
|
||||
DropdownToggle as BS5DropdownToggle,
|
||||
DropdownMenu as BS5DropdownMenu,
|
||||
DropdownItem as BS5DropdownItem,
|
||||
DropdownDivider as BS5DropdownDivider,
|
||||
DropdownHeader as BS5DropdownHeader,
|
||||
Button as BS5Button,
|
||||
type ButtonProps,
|
||||
} from 'react-bootstrap-5'
|
||||
import type {
|
||||
DropdownProps,
|
||||
DropdownItemProps,
|
||||
DropdownToggleProps,
|
||||
DropdownMenuProps,
|
||||
DropdownDividerProps,
|
||||
DropdownHeaderProps,
|
||||
} from '@/features/ui/components/types/dropdown-menu-props'
|
||||
import MaterialIcon from '@/shared/components/material-icon'
|
||||
import { fixedForwardRef } from '@/utils/react'
|
||||
import classnames from 'classnames'
|
||||
|
||||
export function Dropdown({ ...props }: DropdownProps) {
|
||||
return <BS5Dropdown {...props} />
|
||||
}
|
||||
|
||||
function DropdownItem(
|
||||
{
|
||||
active,
|
||||
children,
|
||||
className,
|
||||
description,
|
||||
leadingIcon,
|
||||
trailingIcon,
|
||||
...props
|
||||
}: DropdownItemProps,
|
||||
ref: React.ForwardedRef<typeof BS5DropdownItem>
|
||||
) {
|
||||
let leadingIconComponent = null
|
||||
if (leadingIcon) {
|
||||
if (typeof leadingIcon === 'string') {
|
||||
leadingIconComponent = (
|
||||
<MaterialIcon
|
||||
className="dropdown-item-leading-icon"
|
||||
type={leadingIcon}
|
||||
/>
|
||||
)
|
||||
} else {
|
||||
leadingIconComponent = (
|
||||
<span className="dropdown-item-leading-icon" aria-hidden="true">
|
||||
{leadingIcon}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
let trailingIconComponent = null
|
||||
if (trailingIcon) {
|
||||
if (typeof trailingIcon === 'string') {
|
||||
const trailingIconType = active ? 'check' : trailingIcon
|
||||
|
||||
trailingIconComponent = (
|
||||
<MaterialIcon
|
||||
className="dropdown-item-trailing-icon"
|
||||
type={trailingIconType}
|
||||
/>
|
||||
)
|
||||
} else {
|
||||
trailingIconComponent = (
|
||||
<span className="dropdown-item-trailing-icon" aria-hidden="true">
|
||||
{trailingIcon}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<BS5DropdownItem
|
||||
active={active}
|
||||
className={className}
|
||||
role="menuitem"
|
||||
{...props}
|
||||
ref={ref}
|
||||
>
|
||||
{leadingIconComponent}
|
||||
<div
|
||||
className={classnames({
|
||||
'dropdown-item-description-container': description,
|
||||
})}
|
||||
>
|
||||
{children}
|
||||
{trailingIconComponent}
|
||||
{description && (
|
||||
<span className="dropdown-item-description">{description}</span>
|
||||
)}
|
||||
</div>
|
||||
</BS5DropdownItem>
|
||||
)
|
||||
}
|
||||
|
||||
function EmptyLeadingIcon() {
|
||||
return <span className="dropdown-item-leading-icon-empty" />
|
||||
}
|
||||
|
||||
const ForwardReferredDropdownItem = fixedForwardRef(DropdownItem, {
|
||||
EmptyLeadingIcon,
|
||||
})
|
||||
|
||||
export { ForwardReferredDropdownItem as DropdownItem }
|
||||
|
||||
export const DropdownToggleCustom = forwardRef<HTMLButtonElement, ButtonProps>(
|
||||
({ children, className, ...props }, ref) => (
|
||||
<BS5Button
|
||||
ref={ref}
|
||||
className={classnames('custom-toggle', className)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<MaterialIcon type="expand_more" />
|
||||
</BS5Button>
|
||||
)
|
||||
)
|
||||
DropdownToggleCustom.displayName = 'CustomCaret'
|
||||
|
||||
export const DropdownToggle = forwardRef<
|
||||
typeof BS5DropdownToggle,
|
||||
DropdownToggleProps
|
||||
>((props, ref) => <BS5DropdownToggle {...props} ref={ref} />)
|
||||
DropdownToggle.displayName = 'DropdownToggle'
|
||||
|
||||
export const DropdownMenu = forwardRef<
|
||||
typeof BS5DropdownMenu,
|
||||
DropdownMenuProps
|
||||
>(({ as = 'ul', ...props }, ref) => {
|
||||
return <BS5DropdownMenu as={as} role="menu" {...props} ref={ref} />
|
||||
})
|
||||
DropdownMenu.displayName = 'DropdownMenu'
|
||||
|
||||
export function DropdownDivider({ as = 'li', ...props }: DropdownDividerProps) {
|
||||
return <BS5DropdownDivider as={as} {...props} />
|
||||
}
|
||||
|
||||
export function DropdownHeader({ as = 'li', ...props }: DropdownHeaderProps) {
|
||||
return <BS5DropdownHeader as={as} {...props} />
|
||||
}
|
@@ -0,0 +1,52 @@
|
||||
import { ReactNode, forwardRef } from 'react'
|
||||
import { BsPrefixRefForwardingComponent } from 'react-bootstrap-5/helpers'
|
||||
import type { DropdownToggleProps } from '@/features/ui/components/types/dropdown-menu-props'
|
||||
import {
|
||||
DropdownToggle as BS5DropdownToggle,
|
||||
OverlayTrigger,
|
||||
OverlayTriggerProps,
|
||||
Tooltip,
|
||||
} from 'react-bootstrap-5'
|
||||
import type { MergeAndOverride } from '../../../../../../types/utils'
|
||||
|
||||
type DropdownToggleWithTooltipProps = MergeAndOverride<
|
||||
DropdownToggleProps,
|
||||
{
|
||||
children: ReactNode
|
||||
overlayTriggerProps?: Omit<OverlayTriggerProps, 'overlay' | 'children'>
|
||||
toolTipDescription: string
|
||||
tooltipProps?: Omit<React.ComponentProps<typeof Tooltip>, 'children'>
|
||||
'aria-label'?: string
|
||||
}
|
||||
>
|
||||
const DropdownToggleWithTooltip = forwardRef<
|
||||
BsPrefixRefForwardingComponent<'button', DropdownToggleProps>,
|
||||
DropdownToggleWithTooltipProps
|
||||
>(
|
||||
(
|
||||
{
|
||||
children,
|
||||
toolTipDescription,
|
||||
overlayTriggerProps,
|
||||
tooltipProps,
|
||||
id,
|
||||
...toggleProps
|
||||
},
|
||||
ref
|
||||
) => {
|
||||
return (
|
||||
<OverlayTrigger
|
||||
overlay={<Tooltip {...tooltipProps}>{toolTipDescription}</Tooltip>}
|
||||
{...overlayTriggerProps}
|
||||
>
|
||||
<BS5DropdownToggle {...toggleProps} ref={ref}>
|
||||
{children}
|
||||
</BS5DropdownToggle>
|
||||
</OverlayTrigger>
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
DropdownToggleWithTooltip.displayName = 'DropdownToggleWithTooltip'
|
||||
|
||||
export default DropdownToggleWithTooltip
|
@@ -0,0 +1,91 @@
|
||||
import React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import LanguagePicker from '../language-picker'
|
||||
import FacebookLogo from '@/shared/svgs/facebook-logo'
|
||||
import LinkedInLogo from '@/shared/svgs/linkedin-logo'
|
||||
import XLogo from '@/shared/svgs/x-logo'
|
||||
import classNames from 'classnames'
|
||||
|
||||
type FooterLinkProps = {
|
||||
href: string
|
||||
children: React.ReactNode
|
||||
}
|
||||
|
||||
type SocialMediaLinkProps = {
|
||||
href: string
|
||||
icon: React.ReactNode
|
||||
className: 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 showHeader={false} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="fat-footer-base-section fat-footer-base-social">
|
||||
<div className="fat-footer-base-item">
|
||||
<SocialMediaLink
|
||||
href="https://x.com/overleaf"
|
||||
icon={<XLogo />}
|
||||
className="x-logo"
|
||||
accessibilityLabel={t('app_on_x', { social: 'X' })}
|
||||
/>
|
||||
<SocialMediaLink
|
||||
href="https://www.facebook.com/overleaf.editor"
|
||||
icon={<FacebookLogo />}
|
||||
className="facebook-logo"
|
||||
accessibilityLabel={t('app_on_x', { social: 'Facebook' })}
|
||||
/>
|
||||
<SocialMediaLink
|
||||
href="https://www.linkedin.com/company/writelatex-limited"
|
||||
icon={<LinkedInLogo />}
|
||||
className="linkedin-logo"
|
||||
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,
|
||||
className,
|
||||
accessibilityLabel,
|
||||
}: SocialMediaLinkProps) {
|
||||
return (
|
||||
<a className={classNames('fat-footer-social', className)} href={href}>
|
||||
{icon}
|
||||
<span className="visually-hidden">{accessibilityLabel}</span>
|
||||
</a>
|
||||
)
|
||||
}
|
||||
|
||||
export default FatFooterBase
|
@@ -0,0 +1,133 @@
|
||||
import React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import FatFooterBase from './fat-footer-base'
|
||||
|
||||
type FooterLinkProps = {
|
||||
href: string
|
||||
label: string
|
||||
}
|
||||
|
||||
type FooterSectionProps = {
|
||||
title: string
|
||||
links: FooterLinkProps[]
|
||||
}
|
||||
|
||||
function FatFooter() {
|
||||
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'),
|
||||
},
|
||||
{ href: '/for/government', label: t('for_government') },
|
||||
],
|
||||
},
|
||||
{
|
||||
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
|
@@ -0,0 +1,9 @@
|
||||
import { FooterMetadata } from '@/features/ui/components/types/footer-metadata'
|
||||
import ThinFooter from '@/features/ui/components/bootstrap-5/footer/thin-footer'
|
||||
import FatFooter from '@/features/ui/components/bootstrap-5/footer/fat-footer'
|
||||
|
||||
function Footer(props: FooterMetadata) {
|
||||
return props.showThinFooter ? <ThinFooter {...props} /> : <FatFooter />
|
||||
}
|
||||
|
||||
export default Footer
|
@@ -0,0 +1,92 @@
|
||||
import type {
|
||||
FooterItem,
|
||||
FooterMetadata,
|
||||
} from '@/features/ui/components/types/footer-metadata'
|
||||
import OLRow from '@/features/ui/components/ol/ol-row'
|
||||
import LanguagePicker from '@/features/ui/components/bootstrap-5/language-picker'
|
||||
import React from 'react'
|
||||
|
||||
function FooterItemLi({
|
||||
text,
|
||||
translatedText,
|
||||
url: href,
|
||||
class: className,
|
||||
label,
|
||||
}: FooterItem) {
|
||||
const textToDisplay = translatedText || text
|
||||
|
||||
if (!href) {
|
||||
return <li>{textToDisplay}</li>
|
||||
}
|
||||
|
||||
const linkProps = {
|
||||
href,
|
||||
className,
|
||||
'aria-label': label,
|
||||
}
|
||||
|
||||
return (
|
||||
<li>
|
||||
<a {...linkProps} dangerouslySetInnerHTML={{ __html: textToDisplay }} />
|
||||
</li>
|
||||
)
|
||||
}
|
||||
|
||||
function Separator() {
|
||||
return (
|
||||
<li role="separator" className="text-muted">
|
||||
<strong>|</strong>
|
||||
</li>
|
||||
)
|
||||
}
|
||||
|
||||
function ThinFooter({
|
||||
showPoweredBy,
|
||||
subdomainLang,
|
||||
leftItems,
|
||||
rightItems,
|
||||
}: FooterMetadata) {
|
||||
const showLanguagePicker = Boolean(
|
||||
subdomainLang && Object.keys(subdomainLang).length > 1
|
||||
)
|
||||
|
||||
const hasCustomLeftNav = Boolean(leftItems && leftItems.length > 0)
|
||||
|
||||
return (
|
||||
<footer className="site-footer">
|
||||
<div className="site-footer-content d-print-none">
|
||||
<OLRow>
|
||||
<ul className="site-footer-items col-lg-9">
|
||||
{showPoweredBy ? (
|
||||
<>
|
||||
<li>
|
||||
{/* year of Server Pro release, static */}© 2025{' '}
|
||||
<a href="https://www.overleaf.com/for/enterprises">
|
||||
Powered by Overleaf
|
||||
</a>
|
||||
</li>
|
||||
{showLanguagePicker || hasCustomLeftNav ? <Separator /> : null}
|
||||
</>
|
||||
) : null}
|
||||
{showLanguagePicker ? (
|
||||
<>
|
||||
<li>
|
||||
<LanguagePicker showHeader />
|
||||
</li>
|
||||
{hasCustomLeftNav ? <Separator /> : null}
|
||||
</>
|
||||
) : null}
|
||||
{leftItems?.map(item => <FooterItemLi key={item.text} {...item} />)}
|
||||
</ul>
|
||||
<ul className="site-footer-items col-lg-3 text-end">
|
||||
{rightItems?.map(item => (
|
||||
<FooterItemLi key={item.text} {...item} />
|
||||
))}
|
||||
</ul>
|
||||
</OLRow>
|
||||
</div>
|
||||
</footer>
|
||||
)
|
||||
}
|
||||
|
||||
export default ThinFooter
|
@@ -0,0 +1,51 @@
|
||||
import React, { forwardRef } from 'react'
|
||||
import {
|
||||
Form,
|
||||
FormControlProps as BS5FormControlProps,
|
||||
} from 'react-bootstrap-5'
|
||||
import classnames from 'classnames'
|
||||
import type { BsPrefixRefForwardingComponent } from 'react-bootstrap-5/helpers'
|
||||
|
||||
export type OLBS5FormControlProps = BS5FormControlProps & {
|
||||
prepend?: React.ReactNode
|
||||
append?: React.ReactNode
|
||||
}
|
||||
|
||||
const FormControl: BsPrefixRefForwardingComponent<
|
||||
'input',
|
||||
OLBS5FormControlProps
|
||||
> = forwardRef<HTMLInputElement, OLBS5FormControlProps>(
|
||||
({ prepend, append, className, ...props }, ref) => {
|
||||
if (prepend || append) {
|
||||
const wrapperClassNames = classnames('form-control-wrapper', {
|
||||
'form-control-wrapper-sm': props.size === 'sm',
|
||||
'form-control-wrapper-lg': props.size === 'lg',
|
||||
'form-control-wrapper-disabled': props.disabled,
|
||||
})
|
||||
|
||||
const formControlClassNames = classnames(className, {
|
||||
'form-control-offset-start': prepend,
|
||||
'form-control-offset-end': append,
|
||||
})
|
||||
|
||||
return (
|
||||
<div className={wrapperClassNames}>
|
||||
{prepend && (
|
||||
<span className="form-control-start-icon">{prepend}</span>
|
||||
)}
|
||||
<Form.Control
|
||||
{...props}
|
||||
className={formControlClassNames}
|
||||
ref={ref}
|
||||
/>
|
||||
{append && <span className="form-control-end-icon">{append}</span>}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return <Form.Control ref={ref} className={className} {...props} />
|
||||
}
|
||||
)
|
||||
FormControl.displayName = 'FormControl'
|
||||
|
||||
export default FormControl
|
@@ -0,0 +1,20 @@
|
||||
import { Form } from 'react-bootstrap-5'
|
||||
import FormText from '@/features/ui/components/bootstrap-5/form/form-text'
|
||||
import { ComponentProps } from 'react'
|
||||
|
||||
export type FormFeedbackProps = Pick<
|
||||
ComponentProps<typeof Form.Control.Feedback>,
|
||||
'type' | 'className' | 'children'
|
||||
>
|
||||
|
||||
function FormFeedback(props: FormFeedbackProps) {
|
||||
return (
|
||||
<Form.Control.Feedback {...props}>
|
||||
<FormText type={props.type === 'invalid' ? 'error' : 'success'}>
|
||||
{props.children}
|
||||
</FormText>
|
||||
</Form.Control.Feedback>
|
||||
)
|
||||
}
|
||||
|
||||
export default FormFeedback
|
@@ -0,0 +1,14 @@
|
||||
import { forwardRef } from 'react'
|
||||
import { FormGroup as BS5FormGroup, FormGroupProps } from 'react-bootstrap-5'
|
||||
import classnames from 'classnames'
|
||||
|
||||
const FormGroup = forwardRef<typeof BS5FormGroup, FormGroupProps>(
|
||||
({ className, ...props }, ref) => {
|
||||
const classNames = classnames('form-group', className)
|
||||
|
||||
return <BS5FormGroup className={classNames} {...props} ref={ref} />
|
||||
}
|
||||
)
|
||||
FormGroup.displayName = 'FormGroup'
|
||||
|
||||
export default FormGroup
|
@@ -0,0 +1,58 @@
|
||||
import { Form, FormTextProps as BS5FormTextProps } from 'react-bootstrap-5'
|
||||
import MaterialIcon from '@/shared/components/material-icon'
|
||||
import classnames from 'classnames'
|
||||
import { MergeAndOverride } from '../../../../../../../types/utils'
|
||||
|
||||
type TextType = 'default' | 'info' | 'success' | 'warning' | 'error'
|
||||
|
||||
export type FormTextProps = MergeAndOverride<
|
||||
BS5FormTextProps,
|
||||
{
|
||||
type?: TextType
|
||||
}
|
||||
>
|
||||
|
||||
const typeClassMap: Partial<Record<TextType, string>> = {
|
||||
error: 'text-danger',
|
||||
success: 'text-success',
|
||||
warning: 'text-warning',
|
||||
}
|
||||
|
||||
export const getFormTextClass = (type?: TextType) =>
|
||||
typeClassMap[type || 'default']
|
||||
|
||||
function FormTextIcon({ type }: { type?: TextType }) {
|
||||
switch (type) {
|
||||
case 'info':
|
||||
return <MaterialIcon type="info" className="text-info" />
|
||||
case 'success':
|
||||
return <MaterialIcon type="check_circle" />
|
||||
case 'warning':
|
||||
return <MaterialIcon type="warning" />
|
||||
case 'error':
|
||||
return <MaterialIcon type="error" />
|
||||
default:
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
function FormText({
|
||||
type = 'default',
|
||||
children,
|
||||
className,
|
||||
...rest
|
||||
}: FormTextProps) {
|
||||
return (
|
||||
<Form.Text
|
||||
className={classnames(className, getFormTextClass(type))}
|
||||
{...rest}
|
||||
>
|
||||
<span className="form-text-inner">
|
||||
<FormTextIcon type={type} />
|
||||
{children}
|
||||
</span>
|
||||
</Form.Text>
|
||||
)
|
||||
}
|
||||
|
||||
export default FormText
|
@@ -0,0 +1,37 @@
|
||||
import { forwardRef } from 'react'
|
||||
import classNames from 'classnames'
|
||||
import MaterialIcon from '@/shared/components/material-icon'
|
||||
import Button from './button'
|
||||
import type { IconButtonProps } from '@/features/ui/components/types/icon-button-props'
|
||||
|
||||
const IconButton = forwardRef<HTMLButtonElement, IconButtonProps>(
|
||||
(
|
||||
{ accessibilityLabel, icon, isLoading = false, size, className, ...props },
|
||||
ref
|
||||
) => {
|
||||
const iconButtonClassName = classNames(className, {
|
||||
'icon-button': !size,
|
||||
'icon-button-small': size === 'sm',
|
||||
'icon-button-large': size === 'lg',
|
||||
})
|
||||
const iconSizeClassName = size === 'lg' ? 'icon-large' : 'icon-small'
|
||||
const materialIconClassName = classNames(iconSizeClassName, {
|
||||
'button-content-hidden': isLoading,
|
||||
})
|
||||
|
||||
return (
|
||||
<Button
|
||||
className={iconButtonClassName}
|
||||
isLoading={isLoading}
|
||||
aria-label={accessibilityLabel}
|
||||
{...props}
|
||||
ref={ref}
|
||||
>
|
||||
<MaterialIcon className={materialIconClassName} type={icon} />
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
)
|
||||
IconButton.displayName = 'IconButton'
|
||||
|
||||
export default IconButton
|
@@ -0,0 +1,69 @@
|
||||
import React from 'react'
|
||||
import {
|
||||
Dropdown,
|
||||
DropdownItem,
|
||||
DropdownMenu,
|
||||
DropdownToggle,
|
||||
DropdownHeader,
|
||||
} from './dropdown-menu'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import getMeta from '@/utils/meta'
|
||||
import MaterialIcon from '@/shared/components/material-icon'
|
||||
|
||||
function LanguagePicker({ showHeader } = { showHeader: false }) {
|
||||
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 drop="up">
|
||||
<DropdownToggle
|
||||
id="language-picker-toggle"
|
||||
aria-label={t('select_a_language')}
|
||||
data-bs-toggle="dropdown"
|
||||
className="btn-inline-link"
|
||||
variant="link"
|
||||
>
|
||||
<MaterialIcon type="translate" />
|
||||
|
||||
<span className="language-picker-text">
|
||||
{translatedLanguages?.[currentLangCode]}
|
||||
</span>
|
||||
</DropdownToggle>
|
||||
|
||||
<DropdownMenu
|
||||
className="dropdown-menu-sm-width"
|
||||
aria-labelledby="language-picker-toggle"
|
||||
>
|
||||
{showHeader ? <DropdownHeader>{t('language')}</DropdownHeader> : null}
|
||||
{subdomainLang &&
|
||||
Object.entries(subdomainLang).map(([subdomain, subdomainDetails]) => {
|
||||
if (
|
||||
!subdomainDetails ||
|
||||
!subdomainDetails.lngCode ||
|
||||
subdomainDetails.hide
|
||||
)
|
||||
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
|
@@ -0,0 +1,59 @@
|
||||
import { Dropdown } from 'react-bootstrap-5'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import getMeta from '@/utils/meta'
|
||||
import type { NavbarSessionUser } from '@/features/ui/components/types/navbar'
|
||||
import DropdownListItem from '@/features/ui/components/bootstrap-5/dropdown-list-item'
|
||||
import NavDropdownDivider from './nav-dropdown-divider'
|
||||
import NavDropdownLinkItem from './nav-dropdown-link-item'
|
||||
import { useDsNavStyle } from '@/features/project-list/components/use-is-ds-nav'
|
||||
import { SignOut } from '@phosphor-icons/react'
|
||||
|
||||
export function AccountMenuItems({
|
||||
sessionUser,
|
||||
showSubscriptionLink,
|
||||
}: {
|
||||
sessionUser: NavbarSessionUser
|
||||
showSubscriptionLink: boolean
|
||||
}) {
|
||||
const { t } = useTranslation()
|
||||
const logOutFormId = 'logOutForm'
|
||||
const dsNavStyle = useDsNavStyle()
|
||||
return (
|
||||
<>
|
||||
<Dropdown.Item as="li" disabled role="menuitem">
|
||||
{sessionUser.email}
|
||||
</Dropdown.Item>
|
||||
<NavDropdownDivider />
|
||||
<NavDropdownLinkItem href="/user/settings">
|
||||
{t('Account Settings')}
|
||||
</NavDropdownLinkItem>
|
||||
{showSubscriptionLink ? (
|
||||
<NavDropdownLinkItem href="/user/subscription">
|
||||
{t('subscription')}
|
||||
</NavDropdownLinkItem>
|
||||
) : null}
|
||||
<NavDropdownDivider />
|
||||
<DropdownListItem>
|
||||
{
|
||||
// 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
|
||||
}
|
||||
<Dropdown.Item
|
||||
as="button"
|
||||
type="submit"
|
||||
form={logOutFormId}
|
||||
role="menuitem"
|
||||
className="d-flex align-items-center justify-content-between"
|
||||
>
|
||||
<span>{t('log_out')}</span>
|
||||
{dsNavStyle && <SignOut size={16} />}
|
||||
</Dropdown.Item>
|
||||
<form id={logOutFormId} method="POST" action="/logout">
|
||||
<input type="hidden" name="_csrf" value={getMeta('ol-csrfToken')} />
|
||||
</form>
|
||||
</DropdownListItem>
|
||||
</>
|
||||
)
|
||||
}
|
@@ -0,0 +1,69 @@
|
||||
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'
|
||||
import { useSendProjectListMB } from '@/features/project-list/components/project-list-events'
|
||||
|
||||
export default function AdminMenu({
|
||||
canDisplayAdminMenu,
|
||||
canDisplayAdminRedirect,
|
||||
canDisplaySplitTestMenu,
|
||||
canDisplaySurveyMenu,
|
||||
canDisplayScriptLogMenu,
|
||||
adminUrl,
|
||||
}: Pick<
|
||||
DefaultNavbarMetadata,
|
||||
| 'canDisplayAdminMenu'
|
||||
| 'canDisplayAdminRedirect'
|
||||
| 'canDisplaySplitTestMenu'
|
||||
| 'canDisplaySurveyMenu'
|
||||
| 'canDisplayScriptLogMenu'
|
||||
| 'adminUrl'
|
||||
>) {
|
||||
const sendProjectListMB = useSendProjectListMB()
|
||||
return (
|
||||
<NavDropdownMenu
|
||||
title="Admin"
|
||||
className="subdued"
|
||||
onToggle={nextShow => {
|
||||
if (nextShow) {
|
||||
sendProjectListMB('menu-expand', {
|
||||
item: 'admin',
|
||||
location: 'top-menu',
|
||||
})
|
||||
}
|
||||
}}
|
||||
>
|
||||
{canDisplayAdminMenu ? (
|
||||
<>
|
||||
<NavDropdownLinkItem href="/admin">Manage Site</NavDropdownLinkItem>
|
||||
<NavDropdownLinkItem href="/admin/user">
|
||||
Manage Users
|
||||
</NavDropdownLinkItem>
|
||||
<NavDropdownLinkItem href="/admin/project">
|
||||
Project URL lookup
|
||||
</NavDropdownLinkItem>
|
||||
</>
|
||||
) : null}
|
||||
{canDisplayAdminRedirect && adminUrl ? (
|
||||
<NavDropdownLinkItem href={adminUrl}>
|
||||
Switch to Admin
|
||||
</NavDropdownLinkItem>
|
||||
) : null}
|
||||
{canDisplaySplitTestMenu ? (
|
||||
<NavDropdownLinkItem href="/admin/split-test">
|
||||
Manage Feature Flags
|
||||
</NavDropdownLinkItem>
|
||||
) : null}
|
||||
{canDisplaySurveyMenu ? (
|
||||
<NavDropdownLinkItem href="/admin/survey">
|
||||
Manage Surveys
|
||||
</NavDropdownLinkItem>
|
||||
) : null}
|
||||
{canDisplayScriptLogMenu ? (
|
||||
<NavDropdownLinkItem href="/admin/script-logs">
|
||||
View Script Logs
|
||||
</NavDropdownLinkItem>
|
||||
) : null}
|
||||
</NavDropdownMenu>
|
||||
)
|
||||
}
|
@@ -0,0 +1,33 @@
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { DropdownItem } from 'react-bootstrap-5'
|
||||
import DropdownListItem from '@/features/ui/components/bootstrap-5/dropdown-list-item'
|
||||
import {
|
||||
type ExtraSegmentations,
|
||||
useSendProjectListMB,
|
||||
} from '@/features/project-list/components/project-list-events'
|
||||
|
||||
export default function ContactUsItem({
|
||||
showModal,
|
||||
location,
|
||||
}: {
|
||||
showModal: (event?: Event) => void
|
||||
location: ExtraSegmentations['menu-click']['location']
|
||||
}) {
|
||||
const { t } = useTranslation()
|
||||
const sendMB = useSendProjectListMB()
|
||||
|
||||
return (
|
||||
<DropdownListItem>
|
||||
<DropdownItem
|
||||
as="button"
|
||||
role="menuitem"
|
||||
onClick={() => {
|
||||
sendMB('menu-click', { item: 'contact', location })
|
||||
showModal()
|
||||
}}
|
||||
>
|
||||
{t('contact_us')}
|
||||
</DropdownItem>
|
||||
</DropdownListItem>
|
||||
)
|
||||
}
|
@@ -0,0 +1,145 @@
|
||||
import { useState } from 'react'
|
||||
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 LoggedOutItems from '@/features/ui/components/bootstrap-5/navbar/logged-out-items'
|
||||
import HeaderLogoOrTitle from '@/features/ui/components/bootstrap-5/navbar/header-logo-or-title'
|
||||
import MaterialIcon from '@/shared/components/material-icon'
|
||||
import { useContactUsModal } from '@/shared/hooks/use-contact-us-modal'
|
||||
import { UserProvider } from '@/shared/context/user-context'
|
||||
import { X } from '@phosphor-icons/react'
|
||||
|
||||
function DefaultNavbar(props: DefaultNavbarMetadata) {
|
||||
const {
|
||||
customLogo,
|
||||
title,
|
||||
canDisplayAdminMenu,
|
||||
canDisplayAdminRedirect,
|
||||
canDisplaySplitTestMenu,
|
||||
canDisplaySurveyMenu,
|
||||
canDisplayScriptLogMenu,
|
||||
enableUpgradeButton,
|
||||
suppressNavbarRight,
|
||||
suppressNavContentLinks,
|
||||
showCloseIcon = false,
|
||||
showSubscriptionLink,
|
||||
showSignUpLink,
|
||||
sessionUser,
|
||||
adminUrl,
|
||||
items,
|
||||
} = props
|
||||
const { t } = useTranslation()
|
||||
const { isReady } = useWaitForI18n()
|
||||
const [expanded, setExpanded] = useState(false)
|
||||
|
||||
// The Contact Us modal is rendered at this level rather than inside the nav
|
||||
// bar because otherwise the help wiki search results dropdown doesn't show up
|
||||
const { modal: contactUsModal, showModal: showContactUsModal } =
|
||||
useContactUsModal({
|
||||
autofillProjectUrl: false,
|
||||
})
|
||||
|
||||
if (!isReady) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Navbar
|
||||
className="navbar-default navbar-main"
|
||||
expand="lg"
|
||||
onToggle={expanded => setExpanded(expanded)}
|
||||
>
|
||||
<Container className="navbar-container" fluid>
|
||||
<div className="navbar-header">
|
||||
<HeaderLogoOrTitle title={title} customLogo={customLogo} />
|
||||
{enableUpgradeButton ? (
|
||||
<Button
|
||||
as="a"
|
||||
href="/user/subscription/plans"
|
||||
className="me-2 d-md-none"
|
||||
onClick={() => {
|
||||
sendMB('upgrade-button-click', {
|
||||
source: 'dashboard-top',
|
||||
'project-dashboard-react': 'enabled',
|
||||
'is-dashboard-sidebar-hidden': 'true',
|
||||
'is-screen-width-less-than-768px': 'true',
|
||||
})
|
||||
}}
|
||||
>
|
||||
{t('upgrade')}
|
||||
</Button>
|
||||
) : null}
|
||||
</div>
|
||||
{suppressNavbarRight ? null : (
|
||||
<>
|
||||
<Navbar.Toggle
|
||||
aria-controls="navbar-main-collapse"
|
||||
aria-expanded="false"
|
||||
aria-label={t('main_navigation')}
|
||||
>
|
||||
{showCloseIcon && expanded ? (
|
||||
<X />
|
||||
) : (
|
||||
<MaterialIcon type="menu" />
|
||||
)}
|
||||
</Navbar.Toggle>
|
||||
<Navbar.Collapse
|
||||
id="navbar-main-collapse"
|
||||
className="justify-content-end"
|
||||
>
|
||||
<Nav as="ul" className="ms-auto" role="menubar">
|
||||
{canDisplayAdminMenu ||
|
||||
canDisplayAdminRedirect ||
|
||||
canDisplaySplitTestMenu ? (
|
||||
<AdminMenu
|
||||
canDisplayAdminMenu={canDisplayAdminMenu}
|
||||
canDisplayAdminRedirect={canDisplayAdminRedirect}
|
||||
canDisplaySplitTestMenu={canDisplaySplitTestMenu}
|
||||
canDisplaySurveyMenu={canDisplaySurveyMenu}
|
||||
canDisplayScriptLogMenu={canDisplayScriptLogMenu}
|
||||
adminUrl={adminUrl}
|
||||
/>
|
||||
) : null}
|
||||
{items.map((item, index) => {
|
||||
const showNavItem =
|
||||
(item.only_when_logged_in && sessionUser) ||
|
||||
(item.only_when_logged_out && sessionUser) ||
|
||||
(!item.only_when_logged_out &&
|
||||
!item.only_when_logged_in &&
|
||||
!item.only_content_pages) ||
|
||||
(item.only_content_pages && !suppressNavContentLinks)
|
||||
|
||||
return showNavItem ? (
|
||||
<NavItemFromData
|
||||
item={item}
|
||||
key={index}
|
||||
showContactUsModal={showContactUsModal}
|
||||
/>
|
||||
) : null
|
||||
})}
|
||||
{sessionUser ? (
|
||||
<LoggedInItems
|
||||
sessionUser={sessionUser}
|
||||
showSubscriptionLink={showSubscriptionLink}
|
||||
/>
|
||||
) : (
|
||||
<LoggedOutItems showSignUpLink={showSignUpLink} />
|
||||
)}
|
||||
</Nav>
|
||||
</Navbar.Collapse>
|
||||
</>
|
||||
)}
|
||||
</Container>
|
||||
</Navbar>
|
||||
<UserProvider>{contactUsModal}</UserProvider>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default DefaultNavbar
|
@@ -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<DefaultNavbarMetadata, 'customLogo' | 'title'>) {
|
||||
const { appName } = getMeta('ol-ExposedSettings')
|
||||
|
||||
if (customLogo) {
|
||||
return (
|
||||
// eslint-disable-next-line jsx-a11y/anchor-has-content
|
||||
<a
|
||||
href="/"
|
||||
aria-label={appName}
|
||||
className="navbar-brand"
|
||||
style={{ backgroundImage: `url("${customLogo}")` }}
|
||||
/>
|
||||
)
|
||||
} else if (title) {
|
||||
return (
|
||||
<a href="/" aria-label={appName} className="navbar-title">
|
||||
{title}
|
||||
</a>
|
||||
)
|
||||
} else {
|
||||
return (
|
||||
// eslint-disable-next-line jsx-a11y/anchor-has-content
|
||||
<a href="/" aria-label={appName} className="navbar-brand" />
|
||||
)
|
||||
}
|
||||
}
|
@@ -0,0 +1,41 @@
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import NavDropdownMenu from '@/features/ui/components/bootstrap-5/navbar/nav-dropdown-menu'
|
||||
import type { NavbarSessionUser } from '@/features/ui/components/types/navbar'
|
||||
import NavLinkItem from '@/features/ui/components/bootstrap-5/navbar/nav-link-item'
|
||||
import { AccountMenuItems } from './account-menu-items'
|
||||
import { useSendProjectListMB } from '@/features/project-list/components/project-list-events'
|
||||
|
||||
export default function LoggedInItems({
|
||||
sessionUser,
|
||||
showSubscriptionLink,
|
||||
}: {
|
||||
sessionUser: NavbarSessionUser
|
||||
showSubscriptionLink: boolean
|
||||
}) {
|
||||
const { t } = useTranslation()
|
||||
const sendProjectListMB = useSendProjectListMB()
|
||||
return (
|
||||
<>
|
||||
<NavLinkItem href="/project" className="nav-item-projects">
|
||||
{t('projects')}
|
||||
</NavLinkItem>
|
||||
<NavDropdownMenu
|
||||
title={t('Account')}
|
||||
className="nav-item-account"
|
||||
onToggle={nextShow => {
|
||||
if (nextShow) {
|
||||
sendProjectListMB('menu-expand', {
|
||||
item: 'account',
|
||||
location: 'top-menu',
|
||||
})
|
||||
}
|
||||
}}
|
||||
>
|
||||
<AccountMenuItems
|
||||
sessionUser={sessionUser}
|
||||
showSubscriptionLink={showSubscriptionLink}
|
||||
/>
|
||||
</NavDropdownMenu>
|
||||
</>
|
||||
)
|
||||
}
|
@@ -0,0 +1,37 @@
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import NavLinkItem from '@/features/ui/components/bootstrap-5/navbar/nav-link-item'
|
||||
import { useSendProjectListMB } from '@/features/project-list/components/project-list-events'
|
||||
|
||||
export default function LoggedOutItems({
|
||||
showSignUpLink,
|
||||
}: {
|
||||
showSignUpLink: boolean
|
||||
}) {
|
||||
const { t } = useTranslation()
|
||||
const sendMB = useSendProjectListMB()
|
||||
|
||||
return (
|
||||
<>
|
||||
{showSignUpLink ? (
|
||||
<NavLinkItem
|
||||
href="/register"
|
||||
className="primary nav-account-item"
|
||||
onClick={() => {
|
||||
sendMB('menu-click', { item: 'register', location: 'top-menu' })
|
||||
}}
|
||||
>
|
||||
{t('sign_up')}
|
||||
</NavLinkItem>
|
||||
) : null}
|
||||
<NavLinkItem
|
||||
href="/login"
|
||||
className="nav-account-item"
|
||||
onClick={() => {
|
||||
sendMB('menu-click', { item: 'login', location: 'top-menu' })
|
||||
}}
|
||||
>
|
||||
{t('log_in')}
|
||||
</NavLinkItem>
|
||||
</>
|
||||
)
|
||||
}
|
@@ -0,0 +1,5 @@
|
||||
import { DropdownDivider } from '@/features/ui/components/bootstrap-5/dropdown-menu'
|
||||
|
||||
export default function NavDropdownDivider() {
|
||||
return <DropdownDivider className="d-none d-lg-block" />
|
||||
}
|
@@ -0,0 +1,95 @@
|
||||
import type {
|
||||
NavbarDropdownItemData,
|
||||
NavbarItemDropdownData,
|
||||
} from '@/features/ui/components/types/navbar'
|
||||
import NavDropdownDivider from '@/features/ui/components/bootstrap-5/navbar/nav-dropdown-divider'
|
||||
import { isDropdownLinkItem } from '@/features/ui/components/bootstrap-5/navbar/util'
|
||||
import NavDropdownLinkItem from '@/features/ui/components/bootstrap-5/navbar/nav-dropdown-link-item'
|
||||
import DropdownListItem from '@/features/ui/components/bootstrap-5/dropdown-list-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'
|
||||
import {
|
||||
type ExtraSegmentations,
|
||||
useSendProjectListMB,
|
||||
} from '@/features/project-list/components/project-list-events'
|
||||
|
||||
export default function NavDropdownFromData({
|
||||
item,
|
||||
showContactUsModal,
|
||||
}: {
|
||||
item: NavbarDropdownItemData
|
||||
showContactUsModal: (event?: Event) => void
|
||||
}) {
|
||||
const sendProjectListMB = useSendProjectListMB()
|
||||
return (
|
||||
<NavDropdownMenu
|
||||
title={item.translatedText}
|
||||
className={item.class}
|
||||
onToggle={nextShow => {
|
||||
if (nextShow) {
|
||||
sendProjectListMB('menu-expand', {
|
||||
item: item.trackingKey,
|
||||
location: 'top-menu',
|
||||
})
|
||||
}
|
||||
}}
|
||||
>
|
||||
<NavDropdownMenuItems
|
||||
dropdown={item.dropdown}
|
||||
showContactUsModal={showContactUsModal}
|
||||
location="top-menu"
|
||||
/>
|
||||
</NavDropdownMenu>
|
||||
)
|
||||
}
|
||||
|
||||
export function NavDropdownMenuItems({
|
||||
dropdown,
|
||||
showContactUsModal,
|
||||
location,
|
||||
}: {
|
||||
dropdown: NavbarItemDropdownData
|
||||
showContactUsModal: (event?: Event) => void
|
||||
location: ExtraSegmentations['menu-expand']['location']
|
||||
}) {
|
||||
const sendProjectListMB = useSendProjectListMB()
|
||||
return (
|
||||
<>
|
||||
{dropdown.map((child, index) => {
|
||||
if ('divider' in child) {
|
||||
return <NavDropdownDivider key={index} />
|
||||
} else if ('isContactUs' in child) {
|
||||
return (
|
||||
<ContactUsItem
|
||||
key={index}
|
||||
showModal={showContactUsModal}
|
||||
location={location}
|
||||
/>
|
||||
)
|
||||
} else if (isDropdownLinkItem(child)) {
|
||||
return (
|
||||
<NavDropdownLinkItem
|
||||
key={index}
|
||||
href={child.url}
|
||||
onClick={() => {
|
||||
sendProjectListMB('menu-click', {
|
||||
item: child.trackingKey as ExtraSegmentations['menu-click']['item'],
|
||||
location,
|
||||
destinationURL: child.url,
|
||||
})
|
||||
}}
|
||||
>
|
||||
{child.translatedText}
|
||||
</NavDropdownLinkItem>
|
||||
)
|
||||
} else {
|
||||
return (
|
||||
<DropdownListItem key={index}>
|
||||
{child.translatedText}
|
||||
</DropdownListItem>
|
||||
)
|
||||
}
|
||||
})}
|
||||
</>
|
||||
)
|
||||
}
|
@@ -0,0 +1,22 @@
|
||||
import { ReactNode } from 'react'
|
||||
import DropdownListItem from '@/features/ui/components/bootstrap-5/dropdown-list-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 (
|
||||
<DropdownListItem>
|
||||
<DropdownItem href={href} role="menuitem" onClick={onClick}>
|
||||
{children}
|
||||
</DropdownItem>
|
||||
</DropdownListItem>
|
||||
)
|
||||
}
|
@@ -0,0 +1,41 @@
|
||||
import { type ReactNode, useState } from 'react'
|
||||
import { Dropdown } from 'react-bootstrap-5'
|
||||
import { CaretUp, CaretDown } from '@phosphor-icons/react'
|
||||
import { useDsNavStyle } from '@/features/project-list/components/use-is-ds-nav'
|
||||
|
||||
export default function NavDropdownMenu({
|
||||
title,
|
||||
className,
|
||||
children,
|
||||
onToggle,
|
||||
}: {
|
||||
title: string
|
||||
className?: string
|
||||
children: ReactNode
|
||||
onToggle?: (nextShow: boolean) => void
|
||||
}) {
|
||||
const [show, setShow] = useState(false)
|
||||
const dsNavStyle = useDsNavStyle()
|
||||
// Can't use a NavDropdown here because it's impossible to render the menu as
|
||||
// a <ul> element using NavDropdown
|
||||
const Caret = show ? CaretUp : CaretDown
|
||||
return (
|
||||
<Dropdown
|
||||
as="li"
|
||||
role="none"
|
||||
className={className}
|
||||
onToggle={nextShow => {
|
||||
setShow(nextShow)
|
||||
onToggle?.(nextShow)
|
||||
}}
|
||||
>
|
||||
<Dropdown.Toggle role="menuitem">
|
||||
{title}
|
||||
{dsNavStyle && <Caret weight="bold" className="ms-2" />}
|
||||
</Dropdown.Toggle>
|
||||
<Dropdown.Menu as="ul" role="menu" align="end">
|
||||
{children}
|
||||
</Dropdown.Menu>
|
||||
</Dropdown>
|
||||
)
|
||||
}
|
@@ -0,0 +1,45 @@
|
||||
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 NavLinkItem from '@/features/ui/components/bootstrap-5/navbar/nav-link-item'
|
||||
import { useSendProjectListMB } from '@/features/project-list/components/project-list-events'
|
||||
|
||||
export default function NavItemFromData({
|
||||
item,
|
||||
showContactUsModal,
|
||||
}: {
|
||||
item: NavbarItemData
|
||||
showContactUsModal: (event?: Event) => void
|
||||
}) {
|
||||
const sendProjectListMB = useSendProjectListMB()
|
||||
if (isDropdownItem(item)) {
|
||||
return (
|
||||
<NavDropdownFromData
|
||||
item={item}
|
||||
showContactUsModal={showContactUsModal}
|
||||
/>
|
||||
)
|
||||
} else if (isLinkItem(item)) {
|
||||
return (
|
||||
<NavLinkItem
|
||||
className={item.class}
|
||||
href={item.url}
|
||||
onClick={() => {
|
||||
sendProjectListMB('menu-click', {
|
||||
item: item.trackingKey as any,
|
||||
location: 'top-menu',
|
||||
destinationURL: item.url,
|
||||
})
|
||||
}}
|
||||
>
|
||||
{item.translatedText}
|
||||
</NavLinkItem>
|
||||
)
|
||||
} else {
|
||||
return <NavItem className={item.class}>{item.translatedText}</NavItem>
|
||||
}
|
||||
}
|
@@ -0,0 +1,10 @@
|
||||
import { Nav, NavItemProps } from 'react-bootstrap-5'
|
||||
|
||||
export default function NavItem(props: Omit<NavItemProps, 'as'>) {
|
||||
const { children, ...rest } = props
|
||||
return (
|
||||
<Nav.Item as="li" role="none" {...rest}>
|
||||
{children}
|
||||
</Nav.Item>
|
||||
)
|
||||
}
|
@@ -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<typeof Nav.Link>['onClick']
|
||||
children: ReactNode
|
||||
}) {
|
||||
return (
|
||||
<NavItem className={className}>
|
||||
<Nav.Link role="menuitem" href={href} onClick={onClick}>
|
||||
{children}
|
||||
</Nav.Link>
|
||||
</NavItem>
|
||||
)
|
||||
}
|
@@ -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
|
||||
}
|
@@ -0,0 +1,43 @@
|
||||
import { Table as BS5Table } from 'react-bootstrap-5'
|
||||
import classnames from 'classnames'
|
||||
|
||||
export function TableContainer({
|
||||
responsive,
|
||||
bordered,
|
||||
striped,
|
||||
children,
|
||||
}: React.ComponentProps<typeof BS5Table>) {
|
||||
return (
|
||||
<div
|
||||
className={classnames('table-container', {
|
||||
'table-container-bordered': bordered,
|
||||
'table-responsive': responsive,
|
||||
'table-striped': striped,
|
||||
})}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
type TableProps = React.ComponentProps<typeof BS5Table> & {
|
||||
container?: boolean
|
||||
}
|
||||
|
||||
function Table({
|
||||
container = true,
|
||||
responsive,
|
||||
bordered,
|
||||
striped,
|
||||
...rest
|
||||
}: TableProps) {
|
||||
return container ? (
|
||||
<TableContainer responsive={responsive} bordered={bordered}>
|
||||
<BS5Table striped={striped} {...rest} />
|
||||
</TableContainer>
|
||||
) : (
|
||||
<BS5Table {...rest} />
|
||||
)
|
||||
}
|
||||
|
||||
export default Table
|
@@ -0,0 +1,74 @@
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Badge, BadgeProps } from 'react-bootstrap-5'
|
||||
import MaterialIcon from '@/shared/components/material-icon'
|
||||
import { MergeAndOverride } from '../../../../../../types/utils'
|
||||
import classnames from 'classnames'
|
||||
import { forwardRef } from 'react'
|
||||
|
||||
type TagProps = MergeAndOverride<
|
||||
BadgeProps,
|
||||
{
|
||||
prepend?: React.ReactNode
|
||||
contentProps?: React.ComponentProps<'button'>
|
||||
closeBtnProps?: React.ComponentProps<'button'>
|
||||
}
|
||||
>
|
||||
|
||||
const Tag = forwardRef<HTMLElement, TagProps>(
|
||||
(
|
||||
{ prepend, children, contentProps, closeBtnProps, className, ...rest },
|
||||
ref
|
||||
) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const content = (
|
||||
<>
|
||||
{prepend && <span className="badge-prepend">{prepend}</span>}
|
||||
<span className="badge-content">{children}</span>
|
||||
</>
|
||||
)
|
||||
|
||||
return (
|
||||
<Badge
|
||||
ref={ref}
|
||||
bg="light"
|
||||
className={classnames('badge-tag', className)}
|
||||
{...rest}
|
||||
>
|
||||
{contentProps?.onClick ? (
|
||||
<button
|
||||
type="button"
|
||||
{...contentProps}
|
||||
className={classnames(
|
||||
'badge-tag-content badge-tag-content-btn',
|
||||
contentProps.className
|
||||
)}
|
||||
>
|
||||
{content}
|
||||
</button>
|
||||
) : (
|
||||
<span
|
||||
{...contentProps}
|
||||
className={classnames('badge-tag-content', contentProps?.className)}
|
||||
>
|
||||
{content}
|
||||
</span>
|
||||
)}
|
||||
{closeBtnProps && (
|
||||
<button
|
||||
type="button"
|
||||
className="badge-close"
|
||||
aria-label={t('remove_tag', { tagName: children })}
|
||||
{...closeBtnProps}
|
||||
>
|
||||
<MaterialIcon className="badge-close-icon" type="close" />
|
||||
</button>
|
||||
)}
|
||||
</Badge>
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
Tag.displayName = 'Tag'
|
||||
|
||||
export default Tag
|
@@ -0,0 +1,88 @@
|
||||
import { cloneElement, useEffect, forwardRef } from 'react'
|
||||
import {
|
||||
OverlayTrigger,
|
||||
OverlayTriggerProps,
|
||||
Tooltip as BSTooltip,
|
||||
TooltipProps as BSTooltipProps,
|
||||
} from 'react-bootstrap-5'
|
||||
import { callFnsInSequence } from '@/utils/functions'
|
||||
|
||||
type OverlayProps = Omit<OverlayTriggerProps, 'overlay' | 'children'>
|
||||
|
||||
type UpdatingTooltipProps = {
|
||||
popper: {
|
||||
scheduleUpdate: () => void
|
||||
}
|
||||
show: boolean
|
||||
[x: string]: unknown
|
||||
}
|
||||
|
||||
const UpdatingTooltip = forwardRef<HTMLDivElement, UpdatingTooltipProps>(
|
||||
({ popper, children, show: _, ...props }, ref) => {
|
||||
useEffect(() => {
|
||||
popper.scheduleUpdate()
|
||||
}, [children, popper])
|
||||
|
||||
return (
|
||||
<BSTooltip ref={ref} {...props}>
|
||||
{children}
|
||||
</BSTooltip>
|
||||
)
|
||||
}
|
||||
)
|
||||
UpdatingTooltip.displayName = 'UpdatingTooltip'
|
||||
|
||||
export type TooltipProps = {
|
||||
description: React.ReactNode
|
||||
id: string
|
||||
overlayProps?: OverlayProps
|
||||
tooltipProps?: BSTooltipProps
|
||||
hidden?: boolean
|
||||
children: React.ReactElement
|
||||
}
|
||||
|
||||
function Tooltip({
|
||||
id,
|
||||
description,
|
||||
children,
|
||||
tooltipProps,
|
||||
overlayProps,
|
||||
hidden,
|
||||
}: TooltipProps) {
|
||||
const delay = overlayProps?.delay
|
||||
let delayShow = 300
|
||||
let delayHide = 300
|
||||
if (delay) {
|
||||
delayShow = typeof delay === 'number' ? delay : delay.show
|
||||
delayHide = typeof delay === 'number' ? delay : delay.hide
|
||||
}
|
||||
|
||||
const hideTooltip = (e: React.MouseEvent) => {
|
||||
if (e.currentTarget instanceof HTMLElement) {
|
||||
e.currentTarget.blur()
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<OverlayTrigger
|
||||
overlay={
|
||||
<UpdatingTooltip
|
||||
id={`${id}-tooltip`}
|
||||
{...tooltipProps}
|
||||
style={{ display: hidden ? 'none' : 'block' }}
|
||||
>
|
||||
{description}
|
||||
</UpdatingTooltip>
|
||||
}
|
||||
{...overlayProps}
|
||||
delay={{ show: delayShow, hide: delayHide }}
|
||||
placement={overlayProps?.placement || 'top'}
|
||||
>
|
||||
{cloneElement(children, {
|
||||
onClick: callFnsInSequence(children.props.onClick, hideTooltip),
|
||||
})}
|
||||
</OverlayTrigger>
|
||||
)
|
||||
}
|
||||
|
||||
export default Tooltip
|
@@ -0,0 +1,5 @@
|
||||
import MaterialIcon from '@/shared/components/material-icon'
|
||||
|
||||
export default function OLTagIcon() {
|
||||
return <MaterialIcon type="sell" />
|
||||
}
|
@@ -0,0 +1,17 @@
|
||||
import Badge from '@/features/ui/components/bootstrap-5/badge'
|
||||
|
||||
function OLBadge(props: React.ComponentProps<typeof Badge>) {
|
||||
let { bg, text, ...rest } = props
|
||||
|
||||
// For warning badges, use a light background by default. We still want the
|
||||
// Bootstrap warning colour to be dark for text though, so make an
|
||||
// adjustment here
|
||||
if (bg === 'warning') {
|
||||
bg = 'warning-light-bg'
|
||||
text = 'warning'
|
||||
}
|
||||
|
||||
return <Badge bg={bg} text={text} {...rest} />
|
||||
}
|
||||
|
||||
export default OLBadge
|
@@ -0,0 +1,7 @@
|
||||
import { ButtonGroup, ButtonGroupProps } from 'react-bootstrap-5'
|
||||
|
||||
function OLButtonGroup({ as, ...rest }: ButtonGroupProps) {
|
||||
return <ButtonGroup {...rest} as={as} />
|
||||
}
|
||||
|
||||
export default OLButtonGroup
|
@@ -0,0 +1,7 @@
|
||||
import { ButtonToolbar, ButtonToolbarProps } from 'react-bootstrap-5'
|
||||
|
||||
function OLButtonToolbar(props: ButtonToolbarProps) {
|
||||
return <ButtonToolbar {...props} />
|
||||
}
|
||||
|
||||
export default OLButtonToolbar
|
@@ -0,0 +1,13 @@
|
||||
import { forwardRef } from 'react'
|
||||
import type { ButtonProps } from '@/features/ui/components/types/button-props'
|
||||
import Button from '../bootstrap-5/button'
|
||||
|
||||
export type OLButtonProps = ButtonProps
|
||||
|
||||
const OLButton = forwardRef<HTMLButtonElement, OLButtonProps>((props, ref) => {
|
||||
return <Button {...props} ref={ref} />
|
||||
})
|
||||
|
||||
OLButton.displayName = 'OLButton'
|
||||
|
||||
export default OLButton
|
@@ -0,0 +1,12 @@
|
||||
import { Card } from 'react-bootstrap-5'
|
||||
import { FC } from 'react'
|
||||
|
||||
const OLCard: FC<{ className?: string }> = ({ children, className }) => {
|
||||
return (
|
||||
<Card className={className}>
|
||||
<Card.Body>{children}</Card.Body>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
export default OLCard
|
@@ -0,0 +1,15 @@
|
||||
import { CloseButton, CloseButtonProps } from 'react-bootstrap-5'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { forwardRef } from 'react'
|
||||
|
||||
const OLCloseButton = forwardRef<HTMLButtonElement, CloseButtonProps>(
|
||||
(props, ref) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return <CloseButton ref={ref} aria-label={t('close')} {...props} />
|
||||
}
|
||||
)
|
||||
|
||||
OLCloseButton.displayName = 'OLCloseButton'
|
||||
|
||||
export default OLCloseButton
|
@@ -0,0 +1,7 @@
|
||||
import { Col } from 'react-bootstrap-5'
|
||||
|
||||
function OLCol(props: React.ComponentProps<typeof Col>) {
|
||||
return <Col {...props} />
|
||||
}
|
||||
|
||||
export default OLCol
|
@@ -0,0 +1,14 @@
|
||||
import { DropdownItem } from '@/features/ui/components/bootstrap-5/dropdown-menu'
|
||||
import { DropdownItemProps } from '@/features/ui/components/types/dropdown-menu-props'
|
||||
import DropdownListItem from '@/features/ui/components/bootstrap-5/dropdown-list-item'
|
||||
|
||||
// This represents a menu item. It wraps the item within an <li> element.
|
||||
function OLDropdownMenuItem(props: DropdownItemProps) {
|
||||
return (
|
||||
<DropdownListItem>
|
||||
<DropdownItem {...props} />
|
||||
</DropdownListItem>
|
||||
)
|
||||
}
|
||||
|
||||
export default OLDropdownMenuItem
|
@@ -0,0 +1,42 @@
|
||||
import { Form, FormCheckProps } from 'react-bootstrap-5'
|
||||
import { MergeAndOverride } from '../../../../../../types/utils'
|
||||
import FormText from '../bootstrap-5/form/form-text'
|
||||
|
||||
type OLFormCheckboxProps = MergeAndOverride<
|
||||
FormCheckProps,
|
||||
{
|
||||
inputRef?: React.MutableRefObject<HTMLInputElement | null>
|
||||
} & (
|
||||
| { description: string; id: string }
|
||||
| { description?: undefined; id?: string }
|
||||
)
|
||||
>
|
||||
|
||||
function OLFormCheckbox(props: OLFormCheckboxProps) {
|
||||
const { inputRef, ...rest } = props
|
||||
|
||||
return rest.type === 'radio' ? (
|
||||
<Form.Check
|
||||
ref={inputRef}
|
||||
aria-describedby={rest.description ? `${rest.id}-description` : undefined}
|
||||
{...rest}
|
||||
label={
|
||||
<>
|
||||
{rest.label}
|
||||
{rest.description && (
|
||||
<FormText
|
||||
id={`${rest.id}-description`}
|
||||
className="form-check-label-description"
|
||||
>
|
||||
{rest.description}
|
||||
</FormText>
|
||||
)}
|
||||
</>
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
<Form.Check ref={inputRef} {...rest} />
|
||||
)
|
||||
}
|
||||
|
||||
export default OLFormCheckbox
|
@@ -0,0 +1,30 @@
|
||||
import { forwardRef } from 'react'
|
||||
import FormControl, {
|
||||
type OLBS5FormControlProps,
|
||||
} from '@/features/ui/components/bootstrap-5/form/form-control'
|
||||
import OLSpinner from '@/features/ui/components/ol/ol-spinner'
|
||||
import type { BsPrefixRefForwardingComponent } from 'react-bootstrap-5/helpers'
|
||||
|
||||
type OLFormControlProps = OLBS5FormControlProps & {
|
||||
'data-ol-dirty'?: unknown
|
||||
'main-field'?: any // For the CM6's benefit in the editor search panel
|
||||
loading?: boolean
|
||||
}
|
||||
|
||||
const OLFormControl: BsPrefixRefForwardingComponent<
|
||||
'input',
|
||||
OLFormControlProps
|
||||
> = forwardRef<HTMLInputElement, OLFormControlProps>((props, ref) => {
|
||||
const { append, ...rest } = props
|
||||
|
||||
return (
|
||||
<FormControl
|
||||
ref={ref}
|
||||
{...rest}
|
||||
append={rest.loading ? <OLSpinner size="sm" /> : append}
|
||||
/>
|
||||
)
|
||||
})
|
||||
OLFormControl.displayName = 'OLFormControl'
|
||||
|
||||
export default OLFormControl
|
@@ -0,0 +1,14 @@
|
||||
import { Form } from 'react-bootstrap-5'
|
||||
import { ComponentProps } from 'react'
|
||||
import FormFeedback from '@/features/ui/components/bootstrap-5/form/form-feedback'
|
||||
|
||||
type OLFormFeedbackProps = Pick<
|
||||
ComponentProps<typeof Form.Control.Feedback>,
|
||||
'type' | 'className' | 'children'
|
||||
>
|
||||
|
||||
function OLFormFeedback(props: OLFormFeedbackProps) {
|
||||
return <FormFeedback {...props} />
|
||||
}
|
||||
|
||||
export default OLFormFeedback
|
@@ -0,0 +1,8 @@
|
||||
import { FormGroupProps } from 'react-bootstrap-5'
|
||||
import FormGroup from '@/features/ui/components/bootstrap-5/form/form-group'
|
||||
|
||||
function OLFormGroup(props: FormGroupProps) {
|
||||
return <FormGroup {...props} />
|
||||
}
|
||||
|
||||
export default OLFormGroup
|
@@ -0,0 +1,7 @@
|
||||
import { Form } from 'react-bootstrap-5'
|
||||
|
||||
function OLFormLabel(props: React.ComponentProps<(typeof Form)['Label']>) {
|
||||
return <Form.Label {...props} />
|
||||
}
|
||||
|
||||
export default OLFormLabel
|
@@ -0,0 +1,11 @@
|
||||
import { forwardRef } from 'react'
|
||||
import { Form, FormSelectProps } from 'react-bootstrap-5'
|
||||
|
||||
const OLFormSelect = forwardRef<HTMLSelectElement, FormSelectProps>(
|
||||
(props, ref) => {
|
||||
return <Form.Select ref={ref} {...props} />
|
||||
}
|
||||
)
|
||||
OLFormSelect.displayName = 'OLFormSelect'
|
||||
|
||||
export default OLFormSelect
|
@@ -0,0 +1,20 @@
|
||||
import { FormCheck, FormCheckProps, FormLabel } from 'react-bootstrap-5'
|
||||
|
||||
type OLFormSwitchProps = FormCheckProps & {
|
||||
inputRef?: React.MutableRefObject<HTMLInputElement | null>
|
||||
}
|
||||
|
||||
function OLFormSwitch(props: OLFormSwitchProps) {
|
||||
const { inputRef, label, id, ...rest } = props
|
||||
|
||||
return (
|
||||
<>
|
||||
<FormCheck type="switch" ref={inputRef} id={id} {...rest} />
|
||||
<FormLabel htmlFor={id} visuallyHidden>
|
||||
{label}
|
||||
</FormLabel>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default OLFormSwitch
|
@@ -0,0 +1,9 @@
|
||||
import FormText, {
|
||||
FormTextProps,
|
||||
} from '@/features/ui/components/bootstrap-5/form/form-text'
|
||||
|
||||
function OLFormText({ as = 'div', ...rest }: FormTextProps) {
|
||||
return <FormText {...rest} as={as} />
|
||||
}
|
||||
|
||||
export default OLFormText
|
@@ -0,0 +1,8 @@
|
||||
import { Form } from 'react-bootstrap-5'
|
||||
import { ComponentProps } from 'react'
|
||||
|
||||
function OLForm(props: ComponentProps<typeof Form>) {
|
||||
return <Form {...props} />
|
||||
}
|
||||
|
||||
export default OLForm
|
@@ -0,0 +1,15 @@
|
||||
import { forwardRef } from 'react'
|
||||
import type { IconButtonProps } from '@/features/ui/components/types/icon-button-props'
|
||||
import IconButton from '../bootstrap-5/icon-button'
|
||||
|
||||
export type OLIconButtonProps = IconButtonProps
|
||||
|
||||
const OLIconButton = forwardRef<HTMLButtonElement, OLIconButtonProps>(
|
||||
(props, ref) => {
|
||||
// BS5 tooltip relies on the ref
|
||||
return <IconButton {...props} ref={ref} />
|
||||
}
|
||||
)
|
||||
OLIconButton.displayName = 'OLIconButton'
|
||||
|
||||
export default OLIconButton
|
@@ -0,0 +1,15 @@
|
||||
import { ListGroupItem, ListGroupItemProps } from 'react-bootstrap-5'
|
||||
|
||||
function OLListGroupItem(props: ListGroupItemProps) {
|
||||
const as = props.as ?? 'button'
|
||||
|
||||
return (
|
||||
<ListGroupItem
|
||||
{...props}
|
||||
as={as}
|
||||
type={as === 'button' ? 'button' : undefined}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export default OLListGroupItem
|
@@ -0,0 +1,9 @@
|
||||
import { ListGroup, ListGroupProps } from 'react-bootstrap-5'
|
||||
|
||||
function OLListGroup(props: ListGroupProps) {
|
||||
const as = props.as ?? 'div'
|
||||
|
||||
return <ListGroup {...props} as={as} />
|
||||
}
|
||||
|
||||
export default OLListGroup
|
@@ -0,0 +1,37 @@
|
||||
import {
|
||||
Modal,
|
||||
ModalProps,
|
||||
ModalHeaderProps,
|
||||
ModalTitleProps,
|
||||
ModalFooterProps,
|
||||
} from 'react-bootstrap-5'
|
||||
import { ModalBodyProps } from 'react-bootstrap-5/ModalBody'
|
||||
|
||||
type OLModalProps = ModalProps & {
|
||||
size?: 'sm' | 'lg'
|
||||
onHide: () => void
|
||||
}
|
||||
|
||||
export default function OLModal({ children, ...props }: OLModalProps) {
|
||||
return <Modal {...props}>{children}</Modal>
|
||||
}
|
||||
|
||||
export function OLModalHeader({ children, ...props }: ModalHeaderProps) {
|
||||
return <Modal.Header {...props}>{children}</Modal.Header>
|
||||
}
|
||||
|
||||
export function OLModalTitle({ children, ...props }: ModalTitleProps) {
|
||||
return (
|
||||
<Modal.Title as="h2" {...props}>
|
||||
{children}
|
||||
</Modal.Title>
|
||||
)
|
||||
}
|
||||
|
||||
export function OLModalBody({ children, ...props }: ModalBodyProps) {
|
||||
return <Modal.Body {...props}>{children}</Modal.Body>
|
||||
}
|
||||
|
||||
export function OLModalFooter({ children, ...props }: ModalFooterProps) {
|
||||
return <Modal.Footer {...props}>{children}</Modal.Footer>
|
||||
}
|
@@ -0,0 +1,11 @@
|
||||
import Notification from '@/shared/components/notification'
|
||||
|
||||
function OLNotification(props: React.ComponentProps<typeof Notification>) {
|
||||
return (
|
||||
<div className="notification-list">
|
||||
<Notification {...props} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default OLNotification
|
@@ -0,0 +1,7 @@
|
||||
import { Overlay, OverlayProps } from 'react-bootstrap-5'
|
||||
|
||||
function OLOverlay(props: OverlayProps) {
|
||||
return <Overlay {...props} />
|
||||
}
|
||||
|
||||
export default OLOverlay
|
@@ -0,0 +1,19 @@
|
||||
import { Card, CardBody } from 'react-bootstrap-5'
|
||||
import { FC } from 'react'
|
||||
import classNames from 'classnames'
|
||||
|
||||
// This wraps the Bootstrap 5 Card component but is restricted to the very
|
||||
// basic way we're using it, which is as a container for page content. The
|
||||
// Bootstrap 3 equivalent previously in our codebase is a div with class "card"
|
||||
const OLPageContentCard: FC<{ className?: string }> = ({
|
||||
children,
|
||||
className,
|
||||
}) => {
|
||||
return (
|
||||
<Card className={classNames('page-content-card', className)}>
|
||||
<CardBody>{children}</CardBody>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
export default OLPageContentCard
|
@@ -0,0 +1,20 @@
|
||||
import { forwardRef } from 'react'
|
||||
import { Popover, PopoverProps } from 'react-bootstrap-5'
|
||||
|
||||
type OLPopoverProps = Omit<PopoverProps, 'title'> & {
|
||||
title?: React.ReactNode
|
||||
}
|
||||
|
||||
const OLPopover = forwardRef<HTMLDivElement, OLPopoverProps>((props, ref) => {
|
||||
const { title, children, ...bs5Props } = props
|
||||
|
||||
return (
|
||||
<Popover {...bs5Props} ref={ref}>
|
||||
{title && <Popover.Header>{title}</Popover.Header>}
|
||||
<Popover.Body>{children}</Popover.Body>
|
||||
</Popover>
|
||||
)
|
||||
})
|
||||
OLPopover.displayName = 'OLPopover'
|
||||
|
||||
export default OLPopover
|
@@ -0,0 +1,7 @@
|
||||
import { Row } from 'react-bootstrap-5'
|
||||
|
||||
function OLRow(props: React.ComponentProps<typeof Row>) {
|
||||
return <Row {...props} />
|
||||
}
|
||||
|
||||
export default OLRow
|
@@ -0,0 +1,16 @@
|
||||
import { Spinner } from 'react-bootstrap-5'
|
||||
|
||||
export type OLSpinnerSize = 'sm' | 'lg'
|
||||
|
||||
function OLSpinner({ size = 'sm' }: { size: OLSpinnerSize }) {
|
||||
return (
|
||||
<Spinner
|
||||
size={size === 'sm' ? 'sm' : undefined}
|
||||
animation="border"
|
||||
aria-hidden="true"
|
||||
role="status"
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export default OLSpinner
|
@@ -0,0 +1,7 @@
|
||||
import Table from '@/features/ui/components/bootstrap-5/table'
|
||||
|
||||
function OLTable(props: React.ComponentProps<typeof Table>) {
|
||||
return <Table {...props} />
|
||||
}
|
||||
|
||||
export default OLTable
|
@@ -0,0 +1,12 @@
|
||||
import Tag from '@/features/ui/components/bootstrap-5/tag'
|
||||
import { forwardRef } from 'react'
|
||||
|
||||
type OLTagProps = React.ComponentProps<typeof Tag>
|
||||
|
||||
const OLTag = forwardRef<HTMLElement, OLTagProps>((props: OLTagProps, ref) => {
|
||||
return <Tag ref={ref} {...props} />
|
||||
})
|
||||
|
||||
OLTag.displayName = 'OLTag'
|
||||
|
||||
export default OLTag
|
@@ -0,0 +1,19 @@
|
||||
import { CSSProperties, FC } from 'react'
|
||||
import { ToastContainer as BS5ToastContainer } from 'react-bootstrap-5'
|
||||
|
||||
type OLToastContainerProps = {
|
||||
style?: CSSProperties
|
||||
className?: string
|
||||
}
|
||||
|
||||
export const OLToastContainer: FC<OLToastContainerProps> = ({
|
||||
children,
|
||||
className,
|
||||
style,
|
||||
}) => {
|
||||
return (
|
||||
<BS5ToastContainer className={className} style={style}>
|
||||
{children}
|
||||
</BS5ToastContainer>
|
||||
)
|
||||
}
|
@@ -0,0 +1,89 @@
|
||||
import classNames from 'classnames'
|
||||
import { Toast as BS5Toast } from 'react-bootstrap-5'
|
||||
import {
|
||||
NotificationIcon,
|
||||
NotificationType,
|
||||
} from '../../../../shared/components/notification'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import MaterialIcon from '../../../../shared/components/material-icon'
|
||||
import { ReactNode, useCallback, useState } from 'react'
|
||||
|
||||
export type OLToastProps = {
|
||||
type: NotificationType
|
||||
className?: string
|
||||
title?: string
|
||||
content: string | ReactNode
|
||||
isDismissible?: boolean
|
||||
onDismiss?: () => void
|
||||
autoHide?: boolean
|
||||
delay?: number
|
||||
}
|
||||
|
||||
export const OLToast = ({
|
||||
type = 'info',
|
||||
className = '',
|
||||
content,
|
||||
title,
|
||||
isDismissible,
|
||||
onDismiss,
|
||||
autoHide,
|
||||
delay,
|
||||
}: OLToastProps) => {
|
||||
const { t } = useTranslation()
|
||||
const [show, setShow] = useState(true)
|
||||
|
||||
const toastClassName = classNames(
|
||||
'notification',
|
||||
`notification-type-${type}`,
|
||||
className,
|
||||
'toast-content'
|
||||
)
|
||||
|
||||
const handleClose = useCallback(() => {
|
||||
setShow(false)
|
||||
}, [])
|
||||
|
||||
const handleOnHidden = useCallback(() => {
|
||||
if (onDismiss) onDismiss()
|
||||
}, [onDismiss])
|
||||
|
||||
const toastElement = (
|
||||
<div className={toastClassName}>
|
||||
<NotificationIcon notificationType={type} />
|
||||
|
||||
<div className="notification-content-and-cta">
|
||||
<div className="notification-content">
|
||||
{title && (
|
||||
<p>
|
||||
<b>{title}</b>
|
||||
</p>
|
||||
)}
|
||||
{content}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{isDismissible && (
|
||||
<div className="notification-close-btn">
|
||||
<button
|
||||
aria-label={t('close')}
|
||||
data-bs-dismiss="toast"
|
||||
onClick={handleClose}
|
||||
>
|
||||
<MaterialIcon type="close" />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
return (
|
||||
<BS5Toast
|
||||
onClose={handleClose}
|
||||
autohide={autoHide}
|
||||
onExited={handleOnHidden}
|
||||
delay={delay}
|
||||
show={show}
|
||||
>
|
||||
{toastElement}
|
||||
</BS5Toast>
|
||||
)
|
||||
}
|
@@ -0,0 +1,7 @@
|
||||
import { ToggleButtonGroup, ToggleButtonGroupProps } from 'react-bootstrap-5'
|
||||
|
||||
function OLToggleButtonGroup<T>(props: ToggleButtonGroupProps<T>) {
|
||||
return <ToggleButtonGroup {...props} />
|
||||
}
|
||||
|
||||
export default OLToggleButtonGroup
|
@@ -0,0 +1,7 @@
|
||||
import { ToggleButton, ToggleButtonProps } from 'react-bootstrap-5'
|
||||
|
||||
function OLToggleButton(props: ToggleButtonProps) {
|
||||
return <ToggleButton {...props} />
|
||||
}
|
||||
|
||||
export default OLToggleButton
|
@@ -0,0 +1,7 @@
|
||||
import Tooltip from '@/features/ui/components/bootstrap-5/tooltip'
|
||||
|
||||
function OLTooltip(props: React.ComponentProps<typeof Tooltip>) {
|
||||
return <Tooltip {...props} />
|
||||
}
|
||||
|
||||
export default OLTooltip
|
@@ -0,0 +1,38 @@
|
||||
import type { ReactNode } from 'react'
|
||||
|
||||
export type ButtonProps = {
|
||||
children?: ReactNode
|
||||
className?: string
|
||||
disabled?: boolean
|
||||
download?: boolean | string
|
||||
draggable?: boolean
|
||||
form?: string
|
||||
leadingIcon?: string | React.ReactNode
|
||||
href?: string
|
||||
id?: string
|
||||
target?: string
|
||||
rel?: string
|
||||
isLoading?: boolean
|
||||
loadingLabel?: string
|
||||
onClick?: React.MouseEventHandler<HTMLButtonElement>
|
||||
onMouseDown?: React.MouseEventHandler<HTMLButtonElement>
|
||||
onMouseOver?: React.MouseEventHandler<HTMLButtonElement>
|
||||
onMouseOut?: React.MouseEventHandler<HTMLButtonElement>
|
||||
onFocus?: React.FocusEventHandler<HTMLButtonElement>
|
||||
onBlur?: React.FocusEventHandler<HTMLButtonElement>
|
||||
size?: 'sm' | 'lg' | undefined
|
||||
style?: Record<PropertyKey, string>
|
||||
active?: boolean
|
||||
trailingIcon?: string | React.ReactNode
|
||||
type?: 'button' | 'reset' | 'submit'
|
||||
variant?:
|
||||
| 'primary'
|
||||
| 'secondary'
|
||||
| 'ghost'
|
||||
| 'danger'
|
||||
| 'danger-ghost'
|
||||
| 'premium'
|
||||
| 'premium-secondary'
|
||||
| 'link'
|
||||
| 'info'
|
||||
}
|
@@ -0,0 +1,24 @@
|
||||
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
|
||||
canDisplayScriptLogMenu: boolean
|
||||
enableUpgradeButton: boolean
|
||||
suppressNavbarRight: boolean
|
||||
suppressNavContentLinks: boolean
|
||||
showCloseIcon?: boolean
|
||||
showSubscriptionLink: boolean
|
||||
showSignUpLink: boolean
|
||||
currentUrl: string
|
||||
sessionUser?: NavbarSessionUser
|
||||
adminUrl?: string
|
||||
items: NavbarItemData[]
|
||||
}
|
@@ -0,0 +1,82 @@
|
||||
import type { ElementType, ReactNode, PropsWithChildren } from 'react'
|
||||
import type { ButtonProps } from '@/features/ui/components/types/button-props'
|
||||
|
||||
type SplitButtonVariants = Extract<
|
||||
ButtonProps['variant'],
|
||||
'primary' | 'secondary' | 'danger' | 'link'
|
||||
>
|
||||
|
||||
export type DropdownProps = {
|
||||
align?:
|
||||
| 'start'
|
||||
| 'end'
|
||||
| { sm: 'start' | 'end' }
|
||||
| { md: 'start' | 'end' }
|
||||
| { lg: 'start' | 'end' }
|
||||
| { xl: 'start' | 'end' }
|
||||
| { xxl: 'start' | 'end' }
|
||||
as?: ElementType
|
||||
children: ReactNode
|
||||
className?: string
|
||||
onSelect?: (eventKey: any, event: object) => any
|
||||
onToggle?: (show: boolean) => void
|
||||
show?: boolean
|
||||
autoClose?: boolean | 'inside' | 'outside'
|
||||
drop?: 'up' | 'up-centered' | 'start' | 'end' | 'down' | 'down-centered'
|
||||
focusFirstItemOnShow?: false | true | 'keyboard'
|
||||
onKeyDown?: (event: React.KeyboardEvent) => void
|
||||
}
|
||||
|
||||
export type DropdownItemProps = PropsWithChildren<{
|
||||
active?: boolean
|
||||
as?: ElementType
|
||||
description?: ReactNode
|
||||
disabled?: boolean
|
||||
eventKey?: string | number
|
||||
href?: string
|
||||
leadingIcon?: string | React.ReactNode
|
||||
onClick?: React.MouseEventHandler
|
||||
onMouseEnter?: React.MouseEventHandler
|
||||
trailingIcon?: string | React.ReactNode
|
||||
variant?: 'default' | 'danger'
|
||||
className?: string
|
||||
role?: string
|
||||
tabIndex?: number
|
||||
target?: string
|
||||
download?: boolean | string
|
||||
rel?: string
|
||||
}>
|
||||
|
||||
export type DropdownToggleProps = PropsWithChildren<{
|
||||
bsPrefix?: string
|
||||
className?: string
|
||||
disabled?: boolean
|
||||
split?: boolean
|
||||
id?: string // necessary for assistive technologies
|
||||
variant?: SplitButtonVariants
|
||||
as?: ElementType
|
||||
size?: 'sm' | 'lg' | undefined
|
||||
tabIndex?: number
|
||||
'aria-label'?: string
|
||||
onMouseEnter?: React.MouseEventHandler
|
||||
}>
|
||||
|
||||
export type DropdownMenuProps = PropsWithChildren<{
|
||||
as?: ElementType
|
||||
disabled?: boolean
|
||||
show?: boolean
|
||||
className?: string
|
||||
flip?: boolean
|
||||
id?: string
|
||||
renderOnMount?: boolean
|
||||
}>
|
||||
|
||||
export type DropdownDividerProps = PropsWithChildren<{
|
||||
as?: ElementType
|
||||
className?: string
|
||||
}>
|
||||
|
||||
export type DropdownHeaderProps = PropsWithChildren<{
|
||||
as?: ElementType
|
||||
className?: string
|
||||
}>
|
@@ -0,0 +1,9 @@
|
||||
export interface SubdomainDetails {
|
||||
hide?: boolean
|
||||
lngCode: string
|
||||
url: string
|
||||
}
|
||||
|
||||
export interface SubdomainLang {
|
||||
[subdomain: string]: SubdomainDetails
|
||||
}
|
@@ -0,0 +1,18 @@
|
||||
import type { SubdomainLang } from '@/features/ui/components/types/fat-footer'
|
||||
|
||||
export type FooterItem = {
|
||||
text: string
|
||||
translatedText?: string
|
||||
url?: string
|
||||
class?: string
|
||||
label?: string
|
||||
}
|
||||
|
||||
export type FooterMetadata = {
|
||||
showThinFooter: boolean
|
||||
translatedLanguages: { [key: string]: string }
|
||||
showPoweredBy?: boolean
|
||||
subdomainLang?: SubdomainLang
|
||||
leftItems?: FooterItem[]
|
||||
rightItems?: FooterItem[]
|
||||
}
|
@@ -0,0 +1,7 @@
|
||||
import { ButtonProps } from './button-props'
|
||||
|
||||
export type IconButtonProps = ButtonProps & {
|
||||
accessibilityLabel?: string
|
||||
icon: string
|
||||
type?: 'button' | 'submit'
|
||||
}
|
@@ -0,0 +1,53 @@
|
||||
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
|
||||
trackingKey: string
|
||||
eventSegmentation?: Record<string, any>
|
||||
}
|
||||
|
||||
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
|
||||
trackingKey: 'help' | 'account' | 'features' | 'admin'
|
||||
}
|
||||
|
||||
export interface NavbarLinkItemData extends NavbarTextItemData {
|
||||
url: string
|
||||
trackingKey: string
|
||||
}
|
||||
|
||||
export type NavbarItemData =
|
||||
| NavbarDropdownItemData
|
||||
| NavbarLinkItemData
|
||||
| NavbarTextItemData
|
||||
|
||||
export type NavbarSessionUser = { email: string }
|
Reference in New Issue
Block a user