first commit

This commit is contained in:
2025-04-24 13:11:28 +08:00
commit ff9c54d5e4
5960 changed files with 834111 additions and 0 deletions

View File

@@ -0,0 +1,132 @@
import { ReactNode } from 'react'
import { useTranslation } from 'react-i18next'
import { sendMB } from '@/infrastructure/event-tracking'
import OLBadge from '@/features/ui/components/ol/ol-badge'
import OLButton from '@/features/ui/components/ol/ol-button'
function trackUpgradeClick() {
sendMB('settings-upgrade-click')
}
type EnableWidgetProps = {
logo: ReactNode
title: string
description: string
helpPath: string
helpTextOverride?: string
hasFeature?: boolean
isPremiumFeature?: boolean
statusIndicator?: ReactNode
children?: ReactNode
linked?: boolean
handleLinkClick: () => void
handleUnlinkClick: () => void
disabled?: boolean
}
export function EnableWidget({
logo,
title,
description,
helpPath,
helpTextOverride,
hasFeature,
isPremiumFeature,
statusIndicator,
linked,
handleLinkClick,
handleUnlinkClick,
children,
disabled,
}: EnableWidgetProps) {
const { t } = useTranslation()
const helpText = helpTextOverride || t('learn_more')
return (
<div className="settings-widget-container">
<div>{logo}</div>
<div className="description-container">
<div className="title-row">
<h4>{title}</h4>
{!hasFeature && isPremiumFeature && (
<OLBadge bg="info">{t('premium_feature')}</OLBadge>
)}
</div>
<p className="small">
{description}{' '}
<a href={helpPath} target="_blank" rel="noreferrer">
{helpText}
</a>
</p>
{children}
{hasFeature && statusIndicator}
</div>
<div>
<ActionButton
hasFeature={hasFeature}
linked={linked}
handleUnlinkClick={handleUnlinkClick}
handleLinkClick={handleLinkClick}
disabled={disabled}
/>
</div>
</div>
)
}
type ActionButtonProps = {
hasFeature?: boolean
linked?: boolean
handleUnlinkClick: () => void
handleLinkClick: () => void
disabled?: boolean
linkText?: string
unlinkText?: string
}
export function ActionButton({
linked,
handleUnlinkClick,
handleLinkClick,
hasFeature,
disabled,
linkText,
unlinkText,
}: ActionButtonProps) {
const { t } = useTranslation()
const linkingText = linkText || t('turn_on')
const unlinkingText = unlinkText || t('turn_off')
if (!hasFeature) {
return (
<OLButton
variant="primary"
href="/user/subscription/plans"
onClick={trackUpgradeClick}
>
<span className="text-capitalize">{t('upgrade')}</span>
</OLButton>
)
} else if (linked) {
return (
<OLButton
variant="danger-ghost"
onClick={handleUnlinkClick}
disabled={disabled}
>
{unlinkingText}
</OLButton>
)
} else {
return (
<OLButton
variant="secondary"
disabled={disabled}
onClick={handleLinkClick}
>
{linkingText}
</OLButton>
)
}
}
export default EnableWidget

View File

@@ -0,0 +1,218 @@
import { useCallback, useState, ReactNode } from 'react'
import { useTranslation } from 'react-i18next'
import OLBadge from '@/features/ui/components/ol/ol-badge'
import getMeta from '../../../../utils/meta'
import { sendMB } from '../../../../infrastructure/event-tracking'
import OLButton from '@/features/ui/components/ol/ol-button'
import OLModal, {
OLModalBody,
OLModalFooter,
OLModalHeader,
OLModalTitle,
} from '@/features/ui/components/ol/ol-modal'
function trackUpgradeClick(integration: string) {
sendMB('settings-upgrade-click', { integration })
}
function trackLinkingClick(integration: string) {
sendMB('link-integration-click', { integration, location: 'Settings' })
}
type IntegrationLinkingWidgetProps = {
logo: ReactNode
title: string
description: string
helpPath: string
hasFeature?: boolean
statusIndicator?: ReactNode
linked?: boolean
linkPath: string
unlinkPath: string
unlinkConfirmationTitle: string
unlinkConfirmationText: string
disabled?: boolean
}
export function IntegrationLinkingWidget({
logo,
title,
description,
helpPath,
hasFeature,
statusIndicator,
linked,
linkPath,
unlinkPath,
unlinkConfirmationTitle,
unlinkConfirmationText,
disabled,
}: IntegrationLinkingWidgetProps) {
const { t } = useTranslation()
const [showModal, setShowModal] = useState(false)
const handleUnlinkClick = useCallback(() => {
setShowModal(true)
}, [])
const handleModalHide = useCallback(() => {
setShowModal(false)
}, [])
return (
<div className="settings-widget-container">
<div>{logo}</div>
<div className="description-container">
<div className="title-row">
<h4>{title}</h4>
{!hasFeature && <OLBadge bg="info">{t('premium_feature')}</OLBadge>}
</div>
<p className="small">
{description}{' '}
<a href={helpPath} target="_blank" rel="noreferrer">
{t('learn_more')}
</a>
</p>
{hasFeature && statusIndicator}
</div>
<div>
<ActionButton
integration={title}
hasFeature={hasFeature}
linked={linked}
handleUnlinkClick={handleUnlinkClick}
linkPath={linkPath}
disabled={disabled}
/>
</div>
<UnlinkConfirmationModal
integration={title}
show={showModal}
title={unlinkConfirmationTitle}
content={unlinkConfirmationText}
unlinkPath={unlinkPath}
handleHide={handleModalHide}
/>
</div>
)
}
type ActionButtonProps = {
integration: string
hasFeature?: boolean
linked?: boolean
handleUnlinkClick: () => void
linkPath: string
disabled?: boolean
}
function ActionButton({
hasFeature,
linked,
handleUnlinkClick,
linkPath,
disabled,
integration,
}: ActionButtonProps) {
const { t } = useTranslation()
if (!hasFeature) {
return (
<OLButton
variant="primary"
href="/user/subscription/plans"
onClick={() => trackUpgradeClick(integration)}
>
<span className="text-capitalize">{t('upgrade')}</span>
</OLButton>
)
} else if (linked) {
return (
<OLButton
variant="danger-ghost"
onClick={handleUnlinkClick}
disabled={disabled}
>
{t('unlink')}
</OLButton>
)
} else {
return (
<>
{disabled ? (
<OLButton disabled variant="secondary" className="text-capitalize">
{t('link')}
</OLButton>
) : (
<OLButton
variant="secondary"
href={linkPath}
className="text-capitalize"
onClick={() => trackLinkingClick(integration)}
>
{t('link')}
</OLButton>
)}
</>
)
}
}
type UnlinkConfirmModalProps = {
show: boolean
title: string
integration: string
content: string
unlinkPath: string
handleHide: () => void
}
function UnlinkConfirmationModal({
show,
title,
integration,
content,
unlinkPath,
handleHide,
}: UnlinkConfirmModalProps) {
const { t } = useTranslation()
const handleCancel = (event: React.MouseEvent<HTMLButtonElement>) => {
event.preventDefault()
handleHide()
}
const handleConfirm = () => {
sendMB('unlink-integration-click', {
integration,
})
}
return (
<OLModal show={show} onHide={handleHide}>
<OLModalHeader closeButton>
<OLModalTitle>{title}</OLModalTitle>
</OLModalHeader>
<OLModalBody>
<p>{content}</p>
</OLModalBody>
<OLModalFooter>
<form action={unlinkPath} method="POST" className="form-inline">
<input type="hidden" name="_csrf" value={getMeta('ol-csrfToken')} />
<OLButton variant="secondary" onClick={handleCancel}>
{t('cancel')}
</OLButton>
<OLButton
type="submit"
variant="danger-ghost"
onClick={handleConfirm}
>
{t('unlink')}
</OLButton>
</form>
</OLModalFooter>
</OLModal>
)
}

View File

@@ -0,0 +1,176 @@
import { useCallback, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { FetchError } from '../../../../infrastructure/fetch-json'
import IEEELogo from '../../../../shared/svgs/ieee-logo'
import GoogleLogo from '../../../../shared/svgs/google-logo'
import OrcidLogo from '../../../../shared/svgs/orcid-logo'
import LinkingStatus from './status'
import OLButton from '@/features/ui/components/ol/ol-button'
import OLModal, {
OLModalBody,
OLModalFooter,
OLModalHeader,
OLModalTitle,
} from '@/features/ui/components/ol/ol-modal'
const providerLogos: { readonly [p: string]: JSX.Element } = {
collabratec: <IEEELogo />,
google: <GoogleLogo />,
orcid: <OrcidLogo />,
}
type SSOLinkingWidgetProps = {
providerId: string
title: string
description: string
helpPath?: string
linked?: boolean
linkPath: string
onUnlink: () => Promise<void>
}
export function SSOLinkingWidget({
providerId,
title,
description,
helpPath,
linked,
linkPath,
onUnlink,
}: SSOLinkingWidgetProps) {
const { t } = useTranslation()
const [showModal, setShowModal] = useState(false)
const [unlinkRequestInflight, setUnlinkRequestInflight] = useState(false)
const [errorMessage, setErrorMessage] = useState('')
const handleUnlinkClick = useCallback(() => {
setShowModal(true)
setErrorMessage('')
}, [])
const handleUnlinkConfirmationClick = useCallback(() => {
setShowModal(false)
setUnlinkRequestInflight(true)
onUnlink()
.catch((error: FetchError) => {
setErrorMessage(error.getUserFacingMessage())
})
.finally(() => {
setUnlinkRequestInflight(false)
})
}, [onUnlink])
const handleModalHide = useCallback(() => {
setShowModal(false)
}, [])
return (
<div className="settings-widget-container">
<div>{providerLogos[providerId]}</div>
<div className="description-container">
<div className="title-row">
<h4>{title}</h4>
</div>
<p className="small">
{description?.replace(/<[^>]+>/g, '')}{' '}
{helpPath ? (
<a href={helpPath} target="_blank" rel="noreferrer">
{t('learn_more')}
</a>
) : null}
</p>
{errorMessage ? (
<LinkingStatus status="error" description={errorMessage} />
) : null}
</div>
<div>
<ActionButton
unlinkRequestInflight={unlinkRequestInflight}
accountIsLinked={linked}
linkPath={`${linkPath}?intent=link`}
onUnlinkClick={handleUnlinkClick}
/>
</div>
<UnlinkConfirmModal
title={title}
show={showModal}
handleConfirmation={handleUnlinkConfirmationClick}
handleHide={handleModalHide}
/>
</div>
)
}
type ActionButtonProps = {
unlinkRequestInflight: boolean
accountIsLinked?: boolean
linkPath: string
onUnlinkClick: () => void
}
function ActionButton({
unlinkRequestInflight,
accountIsLinked,
linkPath,
onUnlinkClick,
}: ActionButtonProps) {
const { t } = useTranslation()
if (unlinkRequestInflight) {
return (
<OLButton variant="danger-ghost" disabled>
{t('unlinking')}
</OLButton>
)
} else if (accountIsLinked) {
return (
<OLButton variant="danger-ghost" onClick={onUnlinkClick}>
{t('unlink')}
</OLButton>
)
} else {
return (
<OLButton variant="secondary" href={linkPath} className="text-capitalize">
{t('link')}
</OLButton>
)
}
}
type UnlinkConfirmModalProps = {
title: string
show: boolean
handleConfirmation: () => void
handleHide: () => void
}
function UnlinkConfirmModal({
title,
show,
handleConfirmation,
handleHide,
}: UnlinkConfirmModalProps) {
const { t } = useTranslation()
return (
<OLModal show={show} onHide={handleHide}>
<OLModalHeader closeButton>
<OLModalTitle>
{t('unlink_provider_account_title', { provider: title })}
</OLModalTitle>
</OLModalHeader>
<OLModalBody>
<p>{t('unlink_provider_account_warning', { provider: title })}</p>
</OLModalBody>
<OLModalFooter>
<OLButton variant="secondary" onClick={handleHide}>
{t('cancel')}
</OLButton>
<OLButton variant="danger-ghost" onClick={handleConfirmation}>
{t('unlink')}
</OLButton>
</OLModalFooter>
</OLModal>
)
}

View File

@@ -0,0 +1,57 @@
import { ReactNode } from 'react'
import Icon from '../../../../shared/components/icon'
type Status = 'pending' | 'success' | 'error'
type LinkingStatusProps = {
status: Status
description: string | ReactNode
}
export default function LinkingStatus({
status,
description,
}: LinkingStatusProps) {
return (
<span>
<StatusIcon status={status} />
<span className="small"> {description}</span>
</span>
)
}
type StatusIconProps = {
status: Status
}
function StatusIcon({ status }: StatusIconProps) {
switch (status) {
case 'success':
return (
<Icon
type="check-circle"
fw
className="settings-widget-status-icon status-success"
/>
)
case 'error':
return (
<Icon
type="times-circle"
fw
className="settings-widget-status-icon status-error"
/>
)
case 'pending':
return (
<Icon
type="circle"
fw
className="settings-widget-status-icon status-pending"
spin
/>
)
default:
return null
}
}