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,55 @@
import { FC } from 'react'
import { useTranslation } from 'react-i18next'
export const AccessAttemptScreen: FC<{
loadingScreenBrandHeight: string
inflight: boolean
accessError: string | boolean
}> = ({ loadingScreenBrandHeight, inflight, accessError }) => {
const { t } = useTranslation()
return (
<div className="loading-screen">
<div className="loading-screen-brand-container">
<div
className="loading-screen-brand"
style={{ height: loadingScreenBrandHeight }}
/>
</div>
<h3 className="loading-screen-label text-center">
{t('join_project')}
{inflight && <LoadingScreenEllipses />}
</h3>
{accessError && (
<div className="global-alerts text-center">
<div>
<br />
{accessError === 'not_found' ? (
<div>
<h4 aria-live="assertive">Project not found</h4>
</div>
) : (
<div>
<div className="alert alert-danger" aria-live="assertive">
{t('token_access_failure')}
</div>
<p>
<a href="/">{t('home')}</a>
</p>
</div>
)}
</div>
</div>
)}
</div>
)
}
const LoadingScreenEllipses = () => (
<span aria-hidden>
<span className="loading-screen-ellip">.</span>
<span className="loading-screen-ellip">.</span>
<span className="loading-screen-ellip">.</span>
</span>
)

View File

@@ -0,0 +1,53 @@
import OLButton from '@/features/ui/components/ol/ol-button'
import OLModal, {
OLModalBody,
OLModalFooter,
OLModalHeader,
OLModalTitle,
} from '@/features/ui/components/ol/ol-modal'
import Notification from '@/shared/components/notification'
import { useTranslation } from 'react-i18next'
type LeaveProjectModalProps = {
showModal: boolean
handleCloseModal: () => void
handleLeaveAction: () => void
}
function LeaveProjectModal({
showModal,
handleCloseModal,
handleLeaveAction,
}: LeaveProjectModalProps) {
const { t } = useTranslation()
return (
<OLModal
animation
show={showModal}
onHide={handleCloseModal}
id="action-project-modal"
backdrop="static"
>
<OLModalHeader closeButton>
<OLModalTitle>{t('leave_project')}</OLModalTitle>
</OLModalHeader>
<OLModalBody>
<p>{t('about_to_leave_project')}</p>
<Notification
content={t('this_action_cannot_be_undone')}
type="warning"
/>
</OLModalBody>
<OLModalFooter>
<OLButton variant="secondary" onClick={handleCloseModal}>
{t('cancel')}
</OLButton>
<OLButton variant="danger" onClick={() => handleLeaveAction()}>
{t('confirm')}
</OLButton>
</OLModalFooter>
</OLModal>
)
}
export default LeaveProjectModal

View File

@@ -0,0 +1,69 @@
import { FC } from 'react'
import { useTranslation } from 'react-i18next'
import getMeta from '@/utils/meta'
export type RequireAcceptData = {
projectName?: string
}
export const RequireAcceptScreen: FC<{
requireAcceptData: RequireAcceptData
sendPostRequest: (confirmedByUser: boolean) => void
}> = ({ requireAcceptData, sendPostRequest }) => {
const { t } = useTranslation()
const user = getMeta('ol-user')
return (
<div className="container">
<div className="row">
<div className="col-md-12">
<div className="card">
<div className="card-body">
<div className="text-center link-sharing-invite">
<div className="link-sharing-invite-header">
<p>
{t('youre_joining')}
<br />
<em>
<strong>
{requireAcceptData.projectName || 'This project'}
</strong>
</em>
{user && (
<>
<br />
{t('as_email', { email: user.email })}
</>
)}
</p>
</div>
</div>
<div className="row row-spaced text-center">
<div className="col-md-12">
<p>
{t(
'your_name_and_email_address_will_be_visible_to_the_project_owner_and_other_editors'
)}
</p>
</div>
</div>
<div className="row row-spaced text-center">
<div className="col-md-12">
<button
className="btn btn-lg btn-primary"
type="submit"
onClick={() => sendPostRequest(true)}
>
{t('ok_join_project')}
</button>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
)
}

View File

@@ -0,0 +1,165 @@
import useWaitForI18n from '@/shared/hooks/use-wait-for-i18n'
import { Trans, useTranslation } from 'react-i18next'
import withErrorBoundary from '@/infrastructure/error-boundary'
import { GenericErrorBoundaryFallback } from '@/shared/components/generic-error-boundary-fallback'
import { useCallback, useState } from 'react'
import getMeta from '@/utils/meta'
import { postJSON } from '@/infrastructure/fetch-json'
import { debugConsole } from '@/utils/debugging'
import useAsync from '@/shared/hooks/use-async'
import Notification from '@/shared/components/notification'
import { sendMB } from '@/infrastructure/event-tracking'
import LeaveProjectModal from './leave-project-modal'
import OLButton from '@/features/ui/components/ol/ol-button'
function SharingUpdatesRoot() {
const [showModal, setShowModal] = useState(false)
const { isReady } = useWaitForI18n()
const { t } = useTranslation()
const { isLoading, isSuccess, isError, runAsync } = useAsync()
const projectId = getMeta('ol-project_id')
const joinProject = useCallback(() => {
sendMB('notification-click', {
name: 'link-sharing-collaborator',
button: 'ok',
})
runAsync(postJSON(`/project/${projectId}/sharing-updates/join`))
.then(() => {
location.assign(`/project/${projectId}`)
})
.catch(debugConsole.error)
}, [runAsync, projectId])
const viewProject = useCallback(() => {
sendMB('notification-click', {
name: 'link-sharing-collaborator',
button: 'anonymous',
})
runAsync(postJSON(`/project/${projectId}/sharing-updates/view`))
.then(() => {
location.assign(`/project/${projectId}`)
})
.catch(debugConsole.error)
}, [runAsync, projectId])
const leaveProject = useCallback(() => {
sendMB('notification-click', {
name: 'link-sharing-collaborator',
button: 'leave',
})
runAsync(postJSON(`/project/${projectId}/leave`))
.then(() => {
location.assign('/project')
})
.catch(debugConsole.error)
}, [runAsync, projectId])
if (!isReady) {
return null
}
return (
<div className="container">
<LeaveProjectModal
handleLeaveAction={leaveProject}
showModal={showModal}
handleCloseModal={() => setShowModal(false)}
/>
<div className="row">
<div className="col-md-6 col-md-offset-3 offset-md-3">
<div className="card sharing-updates">
<div className="card-body">
<div className="row">
<div className="col-md-12">
<h1 className="sharing-updates-h1">
{t('updates_to_project_sharing')}
</h1>
</div>
</div>
<div className="row row-spaced">
<div className="col-md-12">
<p>
<Trans
i18nKey="were_making_some_changes_to_project_sharing_this_means_you_will_be_visible"
components={[
// eslint-disable-next-line jsx-a11y/anchor-has-content, react/jsx-key
<a
href="/blog/changes-to-project-sharing"
rel="noopener noreferrer"
target="_blank"
onClick={() => {
sendMB('notification-click', {
name: 'link-sharing-collaborator',
button: 'learn',
})
}}
/>,
]}
/>
</p>
</div>
</div>
<div className="row row-spaced">
<div className="col-md-12">
<OLButton
variant="primary"
onClick={joinProject}
disabled={isLoading}
>
{t('ok_continue_to_project')}
</OLButton>
</div>
</div>
{isError && (
<div className="row row-spaced">
<div className="col-md-12">
<Notification
type="error"
content={t('generic_something_went_wrong')}
/>
</div>
</div>
)}
<div className="row row-spaced">
<div className="col-md-12">
<p>
<small>
<Trans
i18nKey="you_can_also_choose_to_view_anonymously_or_leave_the_project"
components={[
// eslint-disable-next-line react/jsx-key
<button
className="btn btn-inline-link"
onClick={() => viewProject()}
disabled={isLoading || isSuccess}
/>,
// eslint-disable-next-line react/jsx-key
<button
className="btn btn-inline-link"
onClick={() => setShowModal(true)}
disabled={isLoading || isSuccess}
/>,
]}
/>
</small>
</p>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
)
}
export default withErrorBoundary(
SharingUpdatesRoot,
GenericErrorBoundaryFallback
)

View File

@@ -0,0 +1,140 @@
import useWaitForI18n from '@/shared/hooks/use-wait-for-i18n'
import withErrorBoundary from '@/infrastructure/error-boundary'
import { GenericErrorBoundaryFallback } from '@/shared/components/generic-error-boundary-fallback'
import { ElementType, useCallback, useEffect, useRef, useState } from 'react'
import getMeta from '@/utils/meta'
import { postJSON } from '@/infrastructure/fetch-json'
import { debugConsole } from '@/utils/debugging'
import { useLocation } from '@/shared/hooks/use-location'
import { AccessAttemptScreen } from '@/features/token-access/components/access-attempt-screen'
import {
RequireAcceptData,
RequireAcceptScreen,
} from '@/features/token-access/components/require-accept-screen'
import Icon from '@/shared/components/icon'
import importOverleafModules from '../../../../macros/import-overleaf-module.macro'
type Mode = 'access-attempt' | 'v1Import' | 'requireAccept'
const [v1ImportDataScreenModule] = importOverleafModules(
'v1ImportDataScreen'
) as {
import: { default: ElementType }
}[]
const V1ImportDataScreen = v1ImportDataScreenModule?.import.default
export type V1ImportData = {
name?: string
status: string
projectId: string
}
function TokenAccessRoot() {
const [mode, setMode] = useState<Mode>('access-attempt')
const [inflight, setInflight] = useState(false)
const [accessError, setAccessError] = useState<string | boolean>(false)
const [v1ImportData, setV1ImportData] = useState<V1ImportData>()
const [requireAcceptData, setRequireAcceptData] =
useState<RequireAcceptData>()
const [loadingScreenBrandHeight, setLoadingScreenBrandHeight] =
useState('0px')
const location = useLocation()
const sendPostRequest = useCallback(
(confirmedByUser = false) => {
setInflight(true)
postJSON(getMeta('ol-postUrl'), {
body: {
confirmedByUser,
tokenHashPrefix: document.location.hash,
},
})
.then(async data => {
setAccessError(false)
if (data.redirect) {
location.replace(data.redirect)
} else if (data.v1Import) {
setMode('v1Import')
setV1ImportData(data.v1Import)
} else if (data.requireAccept) {
setMode('requireAccept')
setRequireAcceptData(data.requireAccept)
} else {
debugConsole.warn(
'invalid data from server in success response',
data
)
setAccessError(true)
}
})
.catch(error => {
debugConsole.warn('error response from server', error)
setAccessError(error.response?.status === 404 ? 'not_found' : 'error')
})
.finally(() => {
setInflight(false)
})
},
[location]
)
const postedRef = useRef(false)
useEffect(() => {
if (!postedRef.current) {
postedRef.current = true
sendPostRequest()
setTimeout(() => {
setLoadingScreenBrandHeight('20%')
}, 500)
}
}, [sendPostRequest])
const { isReady } = useWaitForI18n()
if (!isReady) {
return null
}
// We don't want the full-size div and back link(?) on
// the new page, but we do this so the original page
// doesn't change.
// TODO: clean up the DOM in the main return
if (mode === 'requireAccept' && requireAcceptData) {
return (
<RequireAcceptScreen
requireAcceptData={requireAcceptData}
sendPostRequest={sendPostRequest}
/>
)
}
return (
<div className="full-size">
<div>
<a
href="/project"
// TODO: class name
style={{ fontSize: '2rem', marginLeft: '1rem', color: '#ddd' }}
>
<Icon type="arrow-left" />
</a>
</div>
{mode === 'access-attempt' && (
<AccessAttemptScreen
accessError={accessError}
inflight={inflight}
loadingScreenBrandHeight={loadingScreenBrandHeight}
/>
)}
{V1ImportDataScreen && mode === 'v1Import' && v1ImportData && (
<V1ImportDataScreen v1ImportData={v1ImportData} />
)}
</div>
)
}
export default withErrorBoundary(TokenAccessRoot, GenericErrorBoundaryFallback)