mirror of
https://github.com/hedgedoc/hedgedoc.git
synced 2025-05-22 03:05:19 -04:00
feat: migrate frontend app to nextjs app router
Signed-off-by: Tilman Vatteroth <git@tilmanvatteroth.de>
This commit is contained in:
parent
5b5dabc84e
commit
8602645bea
108 changed files with 893 additions and 1188 deletions
37
frontend/src/app/(editor)/[id]/page.tsx
Normal file
37
frontend/src/app/(editor)/[id]/page.tsx
Normal file
|
@ -0,0 +1,37 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
import { getNote } from '../../../api/notes'
|
||||
import { redirect } from 'next/navigation'
|
||||
import { baseUrlFromEnvExtractor } from '../../../utils/base-url-from-env-extractor'
|
||||
import { notFound } from 'next/navigation'
|
||||
|
||||
interface PageProps {
|
||||
params: { id: string | undefined }
|
||||
}
|
||||
|
||||
/**
|
||||
* Redirects the user to the editor if the link is a root level direct link to a version 1 note.
|
||||
*/
|
||||
const DirectLinkFallback = async ({ params }: PageProps) => {
|
||||
const baseUrl = baseUrlFromEnvExtractor.extractBaseUrls().editor
|
||||
|
||||
if (params.id === undefined) {
|
||||
notFound()
|
||||
}
|
||||
|
||||
try {
|
||||
const noteData = await getNote(params.id, baseUrl)
|
||||
if (noteData.metadata.version !== 1) {
|
||||
notFound()
|
||||
}
|
||||
} catch (error) {
|
||||
notFound()
|
||||
}
|
||||
|
||||
redirect(`/n/${params.id}`)
|
||||
}
|
||||
|
||||
export default DirectLinkFallback
|
20
frontend/src/app/(editor)/cheatsheet/page.tsx
Normal file
20
frontend/src/app/(editor)/cheatsheet/page.tsx
Normal file
|
@ -0,0 +1,20 @@
|
|||
'use client'
|
||||
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
import { CheatsheetContent } from '../../../components/cheatsheet/cheatsheet-content'
|
||||
import type { NextPage } from 'next'
|
||||
import { Container } from 'react-bootstrap'
|
||||
|
||||
const CheatsheetPage: NextPage = () => {
|
||||
return (
|
||||
<Container>
|
||||
<CheatsheetContent />
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
export default CheatsheetPage
|
42
frontend/src/app/(editor)/global-error.tsx
Normal file
42
frontend/src/app/(editor)/global-error.tsx
Normal file
|
@ -0,0 +1,42 @@
|
|||
'use client'
|
||||
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
import { UiIcon } from '../../components/common/icons/ui-icon'
|
||||
import { ExternalLink } from '../../components/common/links/external-link'
|
||||
import links from '../../links.json'
|
||||
import React, { useEffect } from 'react'
|
||||
import { Button, Container } from 'react-bootstrap'
|
||||
import { ArrowRepeat as IconArrowRepeat } from 'react-bootstrap-icons'
|
||||
|
||||
export default function Error({ error, reset }: { error: Error; reset: () => void }) {
|
||||
useEffect(() => {
|
||||
console.error(error)
|
||||
}, [error])
|
||||
|
||||
return (
|
||||
<html>
|
||||
<body>
|
||||
<Container className='d-flex flex-column mvh-100'>
|
||||
<div className='d-flex flex-column align-items-center justify-content-center my-5'>
|
||||
<h1>An unknown error occurred</h1>
|
||||
<p>
|
||||
Don't worry, this happens sometimes. If this is the first time you see this page then try reloading
|
||||
the app.
|
||||
</p>
|
||||
If you can reproduce this error, then we would be glad if you{' '}
|
||||
<ExternalLink text={'open an issue on github'} href={links.issues} className={'text-primary'} /> or{' '}
|
||||
<ExternalLink text={'contact us on matrix.'} href={links.chat} className={'text-primary'} />
|
||||
<Button onClick={reset} title={'Reload App'} className={'mt-4'}>
|
||||
<UiIcon icon={IconArrowRepeat} />
|
||||
Reload App
|
||||
</Button>
|
||||
</div>
|
||||
</Container>
|
||||
</body>
|
||||
</html>
|
||||
)
|
||||
}
|
44
frontend/src/app/(editor)/history/page.tsx
Normal file
44
frontend/src/app/(editor)/history/page.tsx
Normal file
|
@ -0,0 +1,44 @@
|
|||
'use client'
|
||||
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2023 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
|
44
frontend/src/app/(editor)/intro/page.tsx
Normal file
44
frontend/src/app/(editor)/intro/page.tsx
Normal file
|
@ -0,0 +1,44 @@
|
|||
'use client'
|
||||
|
||||
/*
|
||||
* 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
|
61
frontend/src/app/(editor)/layout.tsx
Normal file
61
frontend/src/app/(editor)/layout.tsx
Normal file
|
@ -0,0 +1,61 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
import '../../../global-styles/index.scss'
|
||||
import { ApplicationLoader } from '../../components/application-loader/application-loader'
|
||||
import { BaseUrlContextProvider } from '../../components/common/base-url/base-url-context-provider'
|
||||
import { FrontendConfigContextProvider } from '../../components/common/frontend-config-context/frontend-config-context-provider'
|
||||
import { MotdModal } from '../../components/global-dialogs/motd-modal/motd-modal'
|
||||
import { DarkMode } from '../../components/layout/dark-mode/dark-mode'
|
||||
import { ExpectedOriginBoundary } from '../../components/layout/expected-origin-boundary'
|
||||
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 type { Metadata } from 'next'
|
||||
import React from 'react'
|
||||
import { getConfig } from '../../api/config'
|
||||
|
||||
configureLuxon()
|
||||
|
||||
export default async function RootLayout({ children }: { children: React.ReactNode }) {
|
||||
const baseUrls = baseUrlFromEnvExtractor.extractBaseUrls()
|
||||
const frontendConfig = await getConfig(baseUrls.editor)
|
||||
|
||||
return (
|
||||
<html lang='en'>
|
||||
<head>
|
||||
<link color='#b51f08' href='/icons/safari-pinned-tab.svg' rel='mask-icon' />
|
||||
</head>
|
||||
<body>
|
||||
<ExpectedOriginBoundary expectedOrigin={baseUrls.editor}>
|
||||
<BaseUrlContextProvider baseUrls={baseUrls}>
|
||||
<FrontendConfigContextProvider config={frontendConfig}>
|
||||
<StoreProvider>
|
||||
<ApplicationLoader>
|
||||
<DarkMode />
|
||||
<MotdModal />
|
||||
<UiNotificationBoundary>{children}</UiNotificationBoundary>
|
||||
</ApplicationLoader>
|
||||
</StoreProvider>
|
||||
</FrontendConfigContextProvider>
|
||||
</BaseUrlContextProvider>
|
||||
</ExpectedOriginBoundary>
|
||||
</body>
|
||||
</html>
|
||||
)
|
||||
}
|
||||
|
||||
export const metadata: Metadata = {
|
||||
themeColor: '#b51f08',
|
||||
applicationName: 'HedgeDoc',
|
||||
appleWebApp: {
|
||||
title: 'HedgeDoc'
|
||||
},
|
||||
description: 'HedgeDoc - Ideas grow better together',
|
||||
viewport: 'width=device-width, initial-scale=1',
|
||||
title: 'HedgeDoc',
|
||||
manifest: '/icons/site.webmanifest'
|
||||
}
|
94
frontend/src/app/(editor)/login/page.tsx
Normal file
94
frontend/src/app/(editor)/login/page.tsx
Normal file
|
@ -0,0 +1,94 @@
|
|||
'use client'
|
||||
|
||||
/*
|
||||
* 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 type { NextPage } from 'next'
|
||||
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.
|
||||
*/
|
||||
const LoginPage: NextPage = () => {
|
||||
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
|
54
frontend/src/app/(editor)/n/[noteId]/page.tsx
Normal file
54
frontend/src/app/(editor)/n/[noteId]/page.tsx
Normal file
|
@ -0,0 +1,54 @@
|
|||
'use client'
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
import type { NoteIdProps } from '../../../../components/common/note-loading-boundary/note-loading-boundary'
|
||||
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 type { NextPage } from 'next'
|
||||
import React from 'react'
|
||||
|
||||
interface PageParams {
|
||||
params: NoteIdProps
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders a page that is used by the user to edit markdown notes. It contains the editor and a renderer.
|
||||
*/
|
||||
const EditorPage: NextPage<PageParams> = ({ params }) => {
|
||||
return (
|
||||
<NoteLoadingBoundary noteId={params.noteId}>
|
||||
<EditorToRendererCommunicatorContextProvider>
|
||||
<EditorPageContent />
|
||||
</EditorToRendererCommunicatorContextProvider>
|
||||
</NoteLoadingBoundary>
|
||||
)
|
||||
}
|
||||
|
||||
/*
|
||||
TODO: implement these in generateMetadata. We need these only in SSR.
|
||||
|
||||
See https://github.com/hedgedoc/hedgedoc/issues/4766
|
||||
|
||||
But its problematic because we dont get the opengraph meta data via API.
|
||||
|
||||
<NoteAndAppTitleHead />
|
||||
<OpengraphHead />
|
||||
<LicenseLinkHead />
|
||||
|
||||
export async function generateMetadata({ params }: PageParams): Promise<Metadata> {
|
||||
if (!params.noteId) {
|
||||
return {}
|
||||
}
|
||||
const note = await getNote(params.noteId, getBaseUrls().editor)
|
||||
return {
|
||||
title: `HedgeDoc - ${ note.metadata.title }`
|
||||
description: note.metadata.description
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
export default EditorPage
|
48
frontend/src/app/(editor)/new/page.tsx
Normal file
48
frontend/src/app/(editor)/new/page.tsx
Normal file
|
@ -0,0 +1,48 @@
|
|||
'use client'
|
||||
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2023 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.
|
||||
*/
|
||||
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
|
16
frontend/src/app/(editor)/not-found.tsx
Normal file
16
frontend/src/app/(editor)/not-found.tsx
Normal file
|
@ -0,0 +1,16 @@
|
|||
/*
|
||||
* 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 NotFound: NextPage = () => {
|
||||
return <CommonErrorPage titleI18nKey={'errors.notFound.title'} descriptionI18nKey={'errors.notFound.description'} />
|
||||
}
|
||||
|
||||
export default NotFound
|
35
frontend/src/app/(editor)/p/[noteId]/page.tsx
Normal file
35
frontend/src/app/(editor)/p/[noteId]/page.tsx
Normal file
|
@ -0,0 +1,35 @@
|
|||
'use client'
|
||||
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
import type { NoteIdProps } from '../../../../components/common/note-loading-boundary/note-loading-boundary'
|
||||
import { NoteLoadingBoundary } from '../../../../components/common/note-loading-boundary/note-loading-boundary'
|
||||
import { useNoteAndAppTitle } from '../../../../components/editor-page/head-meta-properties/use-note-and-app-title'
|
||||
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 type { NextPage } from 'next'
|
||||
import React from 'react'
|
||||
|
||||
interface PageParams {
|
||||
params: NoteIdProps
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders a page that is used by the user to hold a presentation. It contains the renderer for the presentation.
|
||||
*/
|
||||
const SlideShowPage: NextPage<PageParams> = ({ params }) => {
|
||||
useNoteAndAppTitle()
|
||||
|
||||
return (
|
||||
<NoteLoadingBoundary noteId={params.noteId}>
|
||||
<EditorToRendererCommunicatorContextProvider>
|
||||
<SlideShowPageContent />
|
||||
</EditorToRendererCommunicatorContextProvider>
|
||||
</NoteLoadingBoundary>
|
||||
)
|
||||
}
|
||||
|
||||
export default SlideShowPage
|
50
frontend/src/app/(editor)/profile/page.tsx
Normal file
50
frontend/src/app/(editor)/profile/page.tsx
Normal file
|
@ -0,0 +1,50 @@
|
|||
'use client'
|
||||
|
||||
/*
|
||||
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 type { NextPage } from 'next'
|
||||
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.
|
||||
*/
|
||||
const ProfilePage: NextPage = () => {
|
||||
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
|
120
frontend/src/app/(editor)/register/page.tsx
Normal file
120
frontend/src/app/(editor)/register/page.tsx
Normal file
|
@ -0,0 +1,120 @@
|
|||
'use client'
|
||||
/*
|
||||
* 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/navigation'
|
||||
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.
|
||||
*/
|
||||
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
|
38
frontend/src/app/(editor)/s/[noteId]/page.tsx
Normal file
38
frontend/src/app/(editor)/s/[noteId]/page.tsx
Normal file
|
@ -0,0 +1,38 @@
|
|||
'use client'
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
import type { NoteIdProps } from '../../../../components/common/note-loading-boundary/note-loading-boundary'
|
||||
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 { useNoteAndAppTitle } from '../../../../components/editor-page/head-meta-properties/use-note-and-app-title'
|
||||
import { EditorToRendererCommunicatorContextProvider } from '../../../../components/editor-page/render-context/editor-to-renderer-communicator-context-provider'
|
||||
import { BaseAppBar } from '../../../../components/layout/app-bar/base-app-bar'
|
||||
import type { NextPage } from 'next'
|
||||
import React from 'react'
|
||||
|
||||
interface PageParams {
|
||||
params: NoteIdProps
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders a page that contains only the rendered document without an editor or realtime updates.
|
||||
*/
|
||||
const DocumentReadOnlyPage: NextPage<PageParams> = ({ params }) => {
|
||||
useNoteAndAppTitle()
|
||||
|
||||
return (
|
||||
<EditorToRendererCommunicatorContextProvider>
|
||||
<NoteLoadingBoundary noteId={params.noteId}>
|
||||
<div className={'d-flex flex-column mvh-100'}>
|
||||
<BaseAppBar />
|
||||
<DocumentReadOnlyPageContent />
|
||||
</div>
|
||||
</NoteLoadingBoundary>
|
||||
</EditorToRendererCommunicatorContextProvider>
|
||||
)
|
||||
}
|
||||
|
||||
export default DocumentReadOnlyPage
|
42
frontend/src/app/(render)/global-error.tsx
Normal file
42
frontend/src/app/(render)/global-error.tsx
Normal file
|
@ -0,0 +1,42 @@
|
|||
'use client'
|
||||
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
import { UiIcon } from '../../components/common/icons/ui-icon'
|
||||
import { ExternalLink } from '../../components/common/links/external-link'
|
||||
import links from '../../links.json'
|
||||
import React, { useEffect } from 'react'
|
||||
import { Button, Container } from 'react-bootstrap'
|
||||
import { ArrowRepeat as IconArrowRepeat } from 'react-bootstrap-icons'
|
||||
|
||||
export default function Error({ error, reset }: { error: Error; reset: () => void }) {
|
||||
useEffect(() => {
|
||||
console.error(error)
|
||||
}, [error])
|
||||
|
||||
return (
|
||||
<html>
|
||||
<body>
|
||||
<Container className='d-flex flex-column mvh-100'>
|
||||
<div className='d-flex flex-column align-items-center justify-content-center my-5'>
|
||||
<h1>An unknown error occurred</h1>
|
||||
<p>
|
||||
Don't worry, this happens sometimes. If this is the first time you see this page then try reloading
|
||||
the app.
|
||||
</p>
|
||||
If you can reproduce this error, then we would be glad if you 
|
||||
<ExternalLink text={'open an issue on github'} href={links.issues} className={'text-primary'} />
|
||||
  or <ExternalLink text={'contact us on matrix.'} href={links.chat} className={'text-primary'} />
|
||||
<Button onClick={reset} title={'Reload App'} className={'mt-4'}>
|
||||
<UiIcon icon={IconArrowRepeat} />
|
||||
Reload App
|
||||
</Button>
|
||||
</div>
|
||||
</Container>
|
||||
</body>
|
||||
</html>
|
||||
)
|
||||
}
|
35
frontend/src/app/(render)/layout.tsx
Normal file
35
frontend/src/app/(render)/layout.tsx
Normal file
|
@ -0,0 +1,35 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
import '../../../global-styles/index.scss'
|
||||
import { ApplicationLoader } from '../../components/application-loader/application-loader'
|
||||
import { BaseUrlContextProvider } from '../../components/common/base-url/base-url-context-provider'
|
||||
import { FrontendConfigContextProvider } from '../../components/common/frontend-config-context/frontend-config-context-provider'
|
||||
import { ExpectedOriginBoundary } from '../../components/layout/expected-origin-boundary'
|
||||
import { StoreProvider } from '../../redux/store-provider'
|
||||
import { baseUrlFromEnvExtractor } from '../../utils/base-url-from-env-extractor'
|
||||
import React from 'react'
|
||||
import { getConfig } from '../../api/config'
|
||||
|
||||
export default async function RootLayout({ children }: { children: React.ReactNode }) {
|
||||
const baseUrls = baseUrlFromEnvExtractor.extractBaseUrls()
|
||||
const frontendConfig = await getConfig(baseUrls.renderer)
|
||||
|
||||
return (
|
||||
<html lang='en'>
|
||||
<body>
|
||||
<ExpectedOriginBoundary expectedOrigin={baseUrls.renderer}>
|
||||
<BaseUrlContextProvider baseUrls={baseUrls}>
|
||||
<FrontendConfigContextProvider config={frontendConfig}>
|
||||
<StoreProvider>
|
||||
<ApplicationLoader>{children}</ApplicationLoader>
|
||||
</StoreProvider>
|
||||
</FrontendConfigContextProvider>
|
||||
</BaseUrlContextProvider>
|
||||
</ExpectedOriginBoundary>
|
||||
</body>
|
||||
</html>
|
||||
)
|
||||
}
|
23
frontend/src/app/(render)/render/page.tsx
Normal file
23
frontend/src/app/(render)/render/page.tsx
Normal file
|
@ -0,0 +1,23 @@
|
|||
'use client'
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
const RenderPage: NextPage = () => {
|
||||
return (
|
||||
<RendererToEditorCommunicatorContextProvider>
|
||||
<RenderPageContent />
|
||||
</RendererToEditorCommunicatorContextProvider>
|
||||
)
|
||||
}
|
||||
|
||||
export default RenderPage
|
BIN
frontend/src/app/apple-icon.png
Normal file
BIN
frontend/src/app/apple-icon.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 2.1 KiB |
3
frontend/src/app/apple-icon.png.license
Normal file
3
frontend/src/app/apple-icon.png.license
Normal file
|
@ -0,0 +1,3 @@
|
|||
SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||
|
||||
SPDX-License-Identifier: LicenseRef-HedgeDoc-Icon-Usage-Guidelines
|
BIN
frontend/src/app/favicon.ico
Normal file
BIN
frontend/src/app/favicon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 15 KiB |
3
frontend/src/app/favicon.ico.license
Normal file
3
frontend/src/app/favicon.ico.license
Normal file
|
@ -0,0 +1,3 @@
|
|||
SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||
|
||||
SPDX-License-Identifier: LicenseRef-HedgeDoc-Icon-Usage-Guidelines
|
BIN
frontend/src/app/icon.png
Normal file
BIN
frontend/src/app/icon.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 9.2 KiB |
3
frontend/src/app/icon.png.license
Normal file
3
frontend/src/app/icon.png.license
Normal file
|
@ -0,0 +1,3 @@
|
|||
SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||
|
||||
SPDX-License-Identifier: LicenseRef-HedgeDoc-Icon-Usage-Guidelines
|
Loading…
Add table
Add a link
Reference in a new issue