feat: migrate frontend app to nextjs app router

Signed-off-by: Tilman Vatteroth <git@tilmanvatteroth.de>
This commit is contained in:
Tilman Vatteroth 2023-05-29 17:32:44 +02:00
parent 5b5dabc84e
commit 8602645bea
108 changed files with 893 additions and 1188 deletions

View file

@ -1,16 +0,0 @@
/*
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { CommonErrorPage } from '../components/error-pages/common-error-page'
import type { NextPage } from 'next'
/**
* Renders a hedgedoc themed 404 page.
*/
const Custom404: NextPage = () => {
return <CommonErrorPage titleI18nKey={'errors.notFound.title'} descriptionI18nKey={'errors.notFound.description'} />
}
export default Custom404

View file

@ -1,40 +0,0 @@
/*
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { getNote } from '../api/notes'
import { Redirect } from '../components/common/redirect'
import { useSingleStringUrlParameter } from '../hooks/common/use-single-string-url-parameter'
import Custom404 from './404'
import type { NextPage } from 'next'
import React from 'react'
import { useAsync } from 'react-use'
/**
* Redirects the user to the editor if the link is a root level direct link to a version 1 note.
*/
export const DirectLinkFallback: NextPage = () => {
const id = useSingleStringUrlParameter('id', undefined)
const { error, value } = useAsync(async () => {
if (id === undefined) {
throw new Error('No note id found in path')
}
const noteData = await getNote(id)
if (noteData.metadata.version !== 1) {
throw new Error('Note is not a version 1 note')
}
return id
})
if (error !== undefined) {
return <Custom404 />
} else if (value !== undefined) {
return <Redirect to={`/n/${value}`} />
} else {
return <span>Loading</span>
}
}
export default DirectLinkFallback

View file

@ -1,76 +0,0 @@
/*
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import '../../global-styles/index.scss'
import type { FrontendConfig } from '../api/config/types'
import { ApplicationLoader } from '../components/application-loader/application-loader'
import type { BaseUrls } from '../components/common/base-url/base-url-context-provider'
import { BaseUrlContextProvider } from '../components/common/base-url/base-url-context-provider'
import { FrontendConfigContextProvider } from '../components/common/frontend-config-context/frontend-config-context-provider'
import { ErrorBoundary } from '../components/error-boundary/error-boundary'
import { BaseHead } from '../components/layout/base-head'
import { UiNotificationBoundary } from '../components/notifications/ui-notification-boundary'
import { StoreProvider } from '../redux/store-provider'
import { BaseUrlFromEnvExtractor } from '../utils/base-url-from-env-extractor'
import { configureLuxon } from '../utils/configure-luxon'
import { determineCurrentOrigin } from '../utils/determine-current-origin'
import { ExpectedOriginBoundary } from '../utils/expected-origin-boundary'
import { FrontendConfigFetcher } from '../utils/frontend-config-fetcher'
import { isTestMode } from '../utils/test-modes'
import type { AppContext, AppInitialProps, AppProps } from 'next/app'
import React from 'react'
configureLuxon()
interface AppPageProps {
baseUrls: BaseUrls | undefined
frontendConfig: FrontendConfig | undefined
currentOrigin: string | undefined
}
/**
* The actual hedgedoc next js app.
* Provides necessary wrapper components to every page.
*/
function HedgeDocApp({ Component, pageProps }: AppProps<AppPageProps>) {
return (
<BaseUrlContextProvider baseUrls={pageProps.baseUrls}>
<FrontendConfigContextProvider config={pageProps.frontendConfig}>
<ExpectedOriginBoundary currentOrigin={pageProps.currentOrigin}>
<StoreProvider>
<BaseHead />
<ApplicationLoader>
<ErrorBoundary>
<UiNotificationBoundary>
<Component {...pageProps} />
</UiNotificationBoundary>
</ErrorBoundary>
</ApplicationLoader>
</StoreProvider>
</ExpectedOriginBoundary>
</FrontendConfigContextProvider>
</BaseUrlContextProvider>
)
}
const baseUrlFromEnvExtractor = new BaseUrlFromEnvExtractor()
const frontendConfigFetcher = new FrontendConfigFetcher()
HedgeDocApp.getInitialProps = async ({ ctx }: AppContext): Promise<AppInitialProps<AppPageProps>> => {
const baseUrls = baseUrlFromEnvExtractor.extractBaseUrls().orElse(undefined)
const frontendConfig = isTestMode ? undefined : await frontendConfigFetcher.fetch(baseUrls) //some tests mock the frontend config. Therefore it needs to be fetched in the browser.
const currentOrigin = determineCurrentOrigin(ctx)
return {
pageProps: {
baseUrls,
frontendConfig,
currentOrigin
}
}
}
// noinspection JSUnusedGlobalSymbols
export default HedgeDocApp

View file

@ -5,72 +5,89 @@
*/
import type { FrontendConfig } from '../../../api/config/types'
import { AuthProviderType } from '../../../api/config/types'
import { HttpMethod, respondToMatchingRequest } from '../../../handler-utils/respond-to-matching-request'
import {
HttpMethod,
respondToMatchingRequest,
respondToTestRequest
} from '../../../handler-utils/respond-to-matching-request'
import { isTestMode } from '../../../utils/test-modes'
import type { NextApiRequest, NextApiResponse } from 'next'
const initialConfig: FrontendConfig = {
allowAnonymous: true,
allowRegister: true,
branding: {
name: 'DEMO Corp',
logo: '/public/img/demo.png'
},
useImageProxy: false,
specialUrls: {
privacy: 'https://example.com/privacy',
termsOfUse: 'https://example.com/termsOfUse',
imprint: 'https://example.com/imprint'
},
version: {
major: isTestMode ? 0 : 2,
minor: 0,
patch: 0,
preRelease: isTestMode ? undefined : '',
commit: 'mock'
},
plantumlServer: isTestMode ? 'http://mock-plantuml.local' : 'https://www.plantuml.com/plantuml',
maxDocumentLength: isTestMode ? 200 : 1000000,
authProviders: [
{
type: AuthProviderType.LOCAL
},
{
type: AuthProviderType.FACEBOOK
},
{
type: AuthProviderType.GITHUB
},
{
type: AuthProviderType.TWITTER
},
{
type: AuthProviderType.DROPBOX
},
{
type: AuthProviderType.GOOGLE
},
{
type: AuthProviderType.LDAP,
identifier: 'test-ldap',
providerName: 'Test LDAP'
},
{
type: AuthProviderType.GITLAB,
identifier: 'test-gitlab',
providerName: 'Test GitLab'
},
{
type: AuthProviderType.OAUTH2,
identifier: 'test-oauth2',
providerName: 'Test OAuth2'
},
{
type: AuthProviderType.SAML,
identifier: 'test-saml',
providerName: 'Test SAML'
}
]
}
let currentConfig: FrontendConfig = initialConfig
const handler = (req: NextApiRequest, res: NextApiResponse) => {
respondToMatchingRequest<FrontendConfig>(HttpMethod.GET, req, res, {
allowAnonymous: true,
allowRegister: true,
authProviders: [
{
type: AuthProviderType.LOCAL
},
{
type: AuthProviderType.LDAP,
identifier: 'test-ldap',
providerName: 'Test LDAP'
},
{
type: AuthProviderType.DROPBOX
},
{
type: AuthProviderType.FACEBOOK
},
{
type: AuthProviderType.GITHUB
},
{
type: AuthProviderType.GITLAB,
identifier: 'test-gitlab',
providerName: 'Test GitLab'
},
{
type: AuthProviderType.GOOGLE
},
{
type: AuthProviderType.OAUTH2,
identifier: 'test-oauth2',
providerName: 'Test OAuth2'
},
{
type: AuthProviderType.SAML,
identifier: 'test-saml',
providerName: 'Test SAML'
},
{
type: AuthProviderType.TWITTER
respondToMatchingRequest<FrontendConfig>(HttpMethod.GET, req, res, currentConfig, 200, false) ||
respondToTestRequest<FrontendConfig>(req, res, () => {
currentConfig = {
...initialConfig,
...(req.body as FrontendConfig)
}
],
branding: {
name: 'DEMO Corp',
logo: '/public/img/demo.png'
},
useImageProxy: false,
specialUrls: {
privacy: 'https://example.com/privacy',
termsOfUse: 'https://example.com/termsOfUse',
imprint: 'https://example.com/imprint'
},
version: {
major: 2,
minor: 0,
patch: 0,
commit: 'mock'
},
plantumlServer: 'https://www.plantuml.com/plantuml',
maxDocumentLength: 1000000
})
return currentConfig
})
}
export default handler

View file

@ -1,21 +0,0 @@
/*
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { CheatsheetContent } from '../components/cheatsheet/cheatsheet-content'
import { useApplyDarkModeStyle } from '../hooks/dark-mode/use-apply-dark-mode-style'
import type { NextPage } from 'next'
import { Container } from 'react-bootstrap'
const CheatsheetPage: NextPage = () => {
useApplyDarkModeStyle()
return (
<Container>
<CheatsheetContent></CheatsheetContent>
</Container>
)
}
export default CheatsheetPage

View file

@ -1,42 +0,0 @@
/*
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { HistoryContent } from '../components/history-page/history-content/history-content'
import { HistoryToolbar } from '../components/history-page/history-toolbar/history-toolbar'
import { useSafeRefreshHistoryStateCallback } from '../components/history-page/history-toolbar/hooks/use-safe-refresh-history-state'
import { HistoryToolbarStateContextProvider } from '../components/history-page/history-toolbar/toolbar-context/history-toolbar-state-context-provider'
import { LandingLayout } from '../components/landing-layout/landing-layout'
import type { NextPage } from 'next'
import React, { useEffect } from 'react'
import { Row } from 'react-bootstrap'
import { Trans, useTranslation } from 'react-i18next'
/**
* The page that shows the local and remote note history.
*/
const HistoryPage: NextPage = () => {
useTranslation()
const safeRefreshHistoryStateCallback = useSafeRefreshHistoryStateCallback()
useEffect(() => {
safeRefreshHistoryStateCallback()
}, [safeRefreshHistoryStateCallback])
return (
<LandingLayout>
<HistoryToolbarStateContextProvider>
<h1 className='mb-4'>
<Trans i18nKey={'landing.navigation.history'} />
</h1>
<Row className={'justify-content-center mt-5 mb-3'}>
<HistoryToolbar />
</Row>
<HistoryContent />
</HistoryToolbarStateContextProvider>
</LandingLayout>
)
}
export default HistoryPage

View file

@ -1,42 +0,0 @@
/*
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { CustomBranding } from '../components/common/custom-branding/custom-branding'
import { HedgeDocLogoVertical } from '../components/common/hedge-doc-logo/hedge-doc-logo-vertical'
import { LogoSize } from '../components/common/hedge-doc-logo/logo-size'
import { EditorToRendererCommunicatorContextProvider } from '../components/editor-page/render-context/editor-to-renderer-communicator-context-provider'
import { CoverButtons } from '../components/intro-page/cover-buttons/cover-buttons'
import { IntroCustomContent } from '../components/intro-page/intro-custom-content'
import { LandingLayout } from '../components/landing-layout/landing-layout'
import type { NextPage } from 'next'
import React from 'react'
import { Trans } from 'react-i18next'
/**
* Renders the intro page with the logo and the customizable intro text.
*/
const IntroPage: NextPage = () => {
return (
<LandingLayout>
<EditorToRendererCommunicatorContextProvider>
<div className={'flex-fill mt-3'}>
<h1 dir='auto' className={'align-items-center d-flex justify-content-center flex-column'}>
<HedgeDocLogoVertical size={LogoSize.BIG} autoTextColor={true} />
</h1>
<p className='lead'>
<Trans i18nKey='app.slogan' />
</p>
<div className={'mb-5'}>
<CustomBranding />
</div>
<CoverButtons />
<IntroCustomContent />
</div>
</EditorToRendererCommunicatorContextProvider>
</LandingLayout>
)
}
export default IntroPage

View file

@ -1,91 +0,0 @@
/*
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import type { AuthProviderWithCustomName } from '../api/config/types'
import { AuthProviderType } from '../api/config/types'
import { useFrontendConfig } from '../components/common/frontend-config-context/use-frontend-config'
import { RedirectBack } from '../components/common/redirect-back'
import { ShowIf } from '../components/common/show-if/show-if'
import { LandingLayout } from '../components/landing-layout/landing-layout'
import { filterOneClickProviders } from '../components/login-page/auth/utils'
import { ViaLdap } from '../components/login-page/auth/via-ldap'
import { ViaLocal } from '../components/login-page/auth/via-local'
import { ViaOneClick } from '../components/login-page/auth/via-one-click'
import { useApplicationState } from '../hooks/common/use-application-state'
import React, { useMemo } from 'react'
import { Card, Col, Row } from 'react-bootstrap'
import { Trans, useTranslation } from 'react-i18next'
/**
* Renders the login page with buttons and fields for the enabled auth providers.
* Redirects the user to the history page if they are already logged in.
*/
export const LoginPage: React.FC = () => {
useTranslation()
const authProviders = useFrontendConfig().authProviders
const userLoggedIn = useApplicationState((state) => !!state.user)
const ldapProviders = useMemo(() => {
return authProviders
.filter((provider) => provider.type === AuthProviderType.LDAP)
.map((provider) => {
const ldapProvider = provider as AuthProviderWithCustomName
return (
<ViaLdap
providerName={ldapProvider.providerName}
identifier={ldapProvider.identifier}
key={ldapProvider.identifier}
/>
)
})
}, [authProviders])
const localLoginEnabled = useMemo(() => {
return authProviders.some((provider) => provider.type === AuthProviderType.LOCAL)
}, [authProviders])
const oneClickProviders = useMemo(() => {
return authProviders.filter(filterOneClickProviders).map((provider, index) => (
<div className={'p-2 d-flex flex-column social-button-container'} key={index}>
<ViaOneClick provider={provider} />
</div>
))
}, [authProviders])
if (userLoggedIn) {
return <RedirectBack />
}
return (
<LandingLayout>
<div className='my-3'>
<Row className='h-100 d-flex justify-content-center'>
<ShowIf condition={ldapProviders.length > 0 || localLoginEnabled}>
<Col xs={12} sm={10} lg={4}>
<ShowIf condition={localLoginEnabled}>
<ViaLocal />
</ShowIf>
{ldapProviders}
</Col>
</ShowIf>
<ShowIf condition={oneClickProviders.length > 0}>
<Col xs={12} sm={10} lg={4}>
<Card className='mb-4'>
<Card.Body>
<Card.Title>
<Trans i18nKey='login.signInVia' values={{ service: '' }} />
</Card.Title>
{oneClickProviders}
</Card.Body>
</Card>
</Col>
</ShowIf>
</Row>
</div>
</LandingLayout>
)
}
export default LoginPage

View file

@ -1,31 +0,0 @@
/*
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { NoteLoadingBoundary } from '../../components/common/note-loading-boundary/note-loading-boundary'
import { EditorPageContent } from '../../components/editor-page/editor-page-content'
import { EditorToRendererCommunicatorContextProvider } from '../../components/editor-page/render-context/editor-to-renderer-communicator-context-provider'
import { ResetRealtimeStateBoundary } from '../../components/editor-page/reset-realtime-state-boundary'
import { useApplyDarkModeStyle } from '../../hooks/dark-mode/use-apply-dark-mode-style'
import type { NextPage } from 'next'
import React from 'react'
/**
* Renders a page that is used by the user to edit markdown notes. It contains the editor and a renderer.
*/
export const EditorPage: NextPage = () => {
useApplyDarkModeStyle()
return (
<ResetRealtimeStateBoundary>
<NoteLoadingBoundary>
<EditorToRendererCommunicatorContextProvider>
<EditorPageContent />
</EditorToRendererCommunicatorContextProvider>
</NoteLoadingBoundary>
</ResetRealtimeStateBoundary>
)
}
export default EditorPage

View file

@ -1,46 +0,0 @@
/*
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { createNote } from '../api/notes'
import type { Note } from '../api/notes/types'
import { LoadingScreen } from '../components/application-loader/loading-screen/loading-screen'
import { CustomAsyncLoadingBoundary } from '../components/common/async-loading-boundary/custom-async-loading-boundary'
import { Redirect } from '../components/common/redirect'
import { ShowIf } from '../components/common/show-if/show-if'
import { CommonErrorPage } from '../components/error-pages/common-error-page'
import { useSingleStringUrlParameter } from '../hooks/common/use-single-string-url-parameter'
import type { NextPage } from 'next'
import React from 'react'
import { useAsync } from 'react-use'
/**
* Creates a new note, optionally including the passed content and redirects to that note.
*/
export const NewNotePage: NextPage = () => {
const newContent = useSingleStringUrlParameter('content', '')
const { loading, error, value } = useAsync(() => {
return createNote(newContent)
}, [newContent])
return (
<CustomAsyncLoadingBoundary
loading={loading || !value}
error={error}
loadingComponent={<LoadingScreen />}
errorComponent={
<CommonErrorPage
titleI18nKey={'errors.noteCreationFailed.title'}
descriptionI18nKey={'errors.noteCreationFailed.description'}
/>
}>
<ShowIf condition={!!value}>
<Redirect to={`/n/${(value as Note).metadata.primaryAddress}`} replace={true} />
</ShowIf>
</CustomAsyncLoadingBoundary>
)
}
export default NewNotePage

View file

@ -1,26 +0,0 @@
/*
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { NoteLoadingBoundary } from '../../components/common/note-loading-boundary/note-loading-boundary'
import { HeadMetaProperties } from '../../components/editor-page/head-meta-properties/head-meta-properties'
import { EditorToRendererCommunicatorContextProvider } from '../../components/editor-page/render-context/editor-to-renderer-communicator-context-provider'
import { SlideShowPageContent } from '../../components/slide-show-page/slide-show-page-content'
import React from 'react'
/**
* Renders a page that is used by the user to hold a presentation. It contains the renderer for the presentation.
*/
export const SlideShowPage: React.FC = () => {
return (
<NoteLoadingBoundary>
<HeadMetaProperties />
<EditorToRendererCommunicatorContextProvider>
<SlideShowPageContent />
</EditorToRendererCommunicatorContextProvider>
</NoteLoadingBoundary>
)
}
export default SlideShowPage

View file

@ -1,47 +0,0 @@
/*
SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
SPDX-License-Identifier: AGPL-3.0-only
*/
import { AuthProviderType } from '../api/config/types'
import { Redirect } from '../components/common/redirect'
import { ShowIf } from '../components/common/show-if/show-if'
import { LandingLayout } from '../components/landing-layout/landing-layout'
import { ProfileAccessTokens } from '../components/profile-page/access-tokens/profile-access-tokens'
import { ProfileAccountManagement } from '../components/profile-page/account-management/profile-account-management'
import { ProfileChangePassword } from '../components/profile-page/settings/profile-change-password'
import { ProfileDisplayName } from '../components/profile-page/settings/profile-display-name'
import { useApplicationState } from '../hooks/common/use-application-state'
import React from 'react'
import { Col, Row } from 'react-bootstrap'
/**
* Profile page that includes forms for changing display name, password (if internal login is used),
* managing access tokens and deleting the account.
*/
export const ProfilePage: React.FC = () => {
const userProvider = useApplicationState((state) => state.user?.authProvider)
if (!userProvider) {
return <Redirect to={'/login'} />
}
return (
<LandingLayout>
<div className='my-3'>
<Row className='h-100 flex justify-content-center'>
<Col lg={6}>
<ProfileDisplayName />
<ShowIf condition={userProvider === (AuthProviderType.LOCAL as string)}>
<ProfileChangePassword />
</ShowIf>
<ProfileAccessTokens />
<ProfileAccountManagement />
</Col>
</Row>
</div>
</LandingLayout>
)
}
export default ProfilePage

View file

@ -1,119 +0,0 @@
/*
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { doLocalRegister } from '../api/auth/local'
import type { ApiError } from '../api/common/api-error'
import { DisplayNameField } from '../components/common/fields/display-name-field'
import { NewPasswordField } from '../components/common/fields/new-password-field'
import { PasswordAgainField } from '../components/common/fields/password-again-field'
import { UsernameLabelField } from '../components/common/fields/username-label-field'
import { useFrontendConfig } from '../components/common/frontend-config-context/use-frontend-config'
import { Redirect } from '../components/common/redirect'
import { LandingLayout } from '../components/landing-layout/landing-layout'
import { fetchAndSetUser } from '../components/login-page/auth/utils'
import { useUiNotifications } from '../components/notifications/ui-notification-boundary'
import { RegisterError } from '../components/register-page/register-error'
import { RegisterInfos } from '../components/register-page/register-infos'
import { useApplicationState } from '../hooks/common/use-application-state'
import { useLowercaseOnInputChange } from '../hooks/common/use-lowercase-on-input-change'
import { useOnInputChange } from '../hooks/common/use-on-input-change'
import type { NextPage } from 'next'
import { useRouter } from 'next/router'
import type { FormEvent } from 'react'
import React, { useCallback, useMemo, useState } from 'react'
import { Button, Card, Col, Form, Row } from 'react-bootstrap'
import { Trans, useTranslation } from 'react-i18next'
/**
* Renders the registration page with fields for username, display name, password, password retype and information about terms and conditions.
*/
export const RegisterPage: NextPage = () => {
useTranslation()
const router = useRouter()
const allowRegister = useFrontendConfig().allowRegister
const userExists = useApplicationState((state) => !!state.user)
const [username, setUsername] = useState('')
const [displayName, setDisplayName] = useState('')
const [password, setPassword] = useState('')
const [passwordAgain, setPasswordAgain] = useState('')
const [error, setError] = useState<ApiError>()
const { dispatchUiNotification } = useUiNotifications()
const doRegisterSubmit = useCallback(
(event: FormEvent) => {
doLocalRegister(username, displayName, password)
.then(() => fetchAndSetUser())
.then(() => dispatchUiNotification('login.register.success.title', 'login.register.success.message', {}))
.then(() => router.push('/'))
.catch((error: ApiError) => setError(error))
event.preventDefault()
},
[username, displayName, password, dispatchUiNotification, router]
)
const ready = useMemo(() => {
return username.trim() !== '' && displayName.trim() !== '' && password.trim() !== '' && password === passwordAgain
}, [username, password, displayName, passwordAgain])
const isWeakPassword = useMemo(() => {
return error?.backendErrorName === 'PasswordTooWeakError'
}, [error])
const isValidUsername = useMemo(() => Boolean(username.trim()), [username])
const onUsernameChange = useLowercaseOnInputChange(setUsername)
const onDisplayNameChange = useOnInputChange(setDisplayName)
const onPasswordChange = useOnInputChange(setPassword)
const onPasswordAgainChange = useOnInputChange(setPasswordAgain)
if (userExists) {
return <Redirect to={'/intro'} />
}
if (!allowRegister) {
return <Redirect to={'/login'} />
}
return (
<LandingLayout>
<div className='my-3'>
<h1 className='mb-4'>
<Trans i18nKey='login.register.title' />
</h1>
<Row className='h-100 d-flex justify-content-center'>
<Col lg={6}>
<Card className='mb-4 text-start'>
<Card.Body>
<Form onSubmit={doRegisterSubmit} className={'d-flex flex-column gap-3'}>
<UsernameLabelField onChange={onUsernameChange} value={username} isValid={isValidUsername} />
<DisplayNameField onChange={onDisplayNameChange} value={displayName} />
<NewPasswordField onChange={onPasswordChange} value={password} hasError={isWeakPassword} />
<PasswordAgainField
password={password}
onChange={onPasswordAgainChange}
value={passwordAgain}
hasError={isWeakPassword}
/>
<RegisterInfos />
<Button variant='primary' type='submit' disabled={!ready}>
<Trans i18nKey='login.register.title' />
</Button>
<RegisterError error={error} />
</Form>
</Card.Body>
</Card>
</Col>
</Row>
</div>
</LandingLayout>
)
}
export default RegisterPage

View file

@ -1,22 +0,0 @@
/*
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { RendererToEditorCommunicatorContextProvider } from '../components/editor-page/render-context/renderer-to-editor-communicator-context-provider'
import { RenderPageContent } from '../components/render-page/render-page-content'
import type { NextPage } from 'next'
import React from 'react'
/**
* Renders the actual markdown renderer that receives the content and metadata via iframe communication.
*/
export const RenderPage: NextPage = () => {
return (
<RendererToEditorCommunicatorContextProvider>
<RenderPageContent />
</RendererToEditorCommunicatorContextProvider>
)
}
export default RenderPage

View file

@ -1,37 +0,0 @@
/*
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { NoteLoadingBoundary } from '../../components/common/note-loading-boundary/note-loading-boundary'
import { DocumentReadOnlyPageContent } from '../../components/document-read-only-page/document-read-only-page-content'
import { HeadMetaProperties } from '../../components/editor-page/head-meta-properties/head-meta-properties'
import { EditorToRendererCommunicatorContextProvider } from '../../components/editor-page/render-context/editor-to-renderer-communicator-context-provider'
import { MotdModal } from '../../components/global-dialogs/motd-modal/motd-modal'
import { BaseAppBar } from '../../components/layout/app-bar/base-app-bar'
import { useApplyDarkModeStyle } from '../../hooks/dark-mode/use-apply-dark-mode-style'
import { useSaveDarkModePreferenceToLocalStorage } from '../../hooks/dark-mode/use-save-dark-mode-preference-to-local-storage'
import React from 'react'
/**
* Renders a page that contains only the rendered document without an editor or realtime updates.
*/
export const DocumentReadOnlyPage: React.FC = () => {
useApplyDarkModeStyle()
useSaveDarkModePreferenceToLocalStorage()
return (
<EditorToRendererCommunicatorContextProvider>
<NoteLoadingBoundary>
<HeadMetaProperties />
<MotdModal />
<div className={'d-flex flex-column mvh-100'}>
<BaseAppBar />
<DocumentReadOnlyPageContent />
</div>
</NoteLoadingBoundary>
</EditorToRendererCommunicatorContextProvider>
)
}
export default DocumentReadOnlyPage