From 8602645bea17eaaf4ce42e6e3d6bdd914229cb1f Mon Sep 17 00:00:00 2001 From: Tilman Vatteroth Date: Mon, 29 May 2023 17:32:44 +0200 Subject: [PATCH] feat: migrate frontend app to nextjs app router Signed-off-by: Tilman Vatteroth --- frontend/build.sh | 2 +- frontend/cypress/e2e/note-meta-head.spec.ts | 47 ------ frontend/cypress/support/config.ts | 9 +- frontend/locales/en.json | 1 - frontend/next-env.d.ts | 1 + frontend/next.config.js | 22 +-- frontend/package.json | 4 +- frontend/public/icons/browserconfig.xml | 9 -- frontend/public/icons/favicon-16x16.png | Bin 428 -> 0 bytes frontend/public/icons/favicon-32x32.png | Bin 605 -> 0 bytes frontend/public/icons/mstile-144x144.png | Bin 2094 -> 0 bytes frontend/public/icons/mstile-150x150.png | Bin 1824 -> 0 bytes frontend/public/icons/mstile-310x150.png | Bin 1898 -> 0 bytes frontend/public/icons/mstile-310x310.png | Bin 3737 -> 0 bytes frontend/public/icons/mstile-70x70.png | Bin 1426 -> 0 bytes frontend/public/icons/safari-pinned-tab.svg | 6 + frontend/src/api/config/index.ts | 6 +- frontend/src/api/notes/index.ts | 4 +- frontend/src/app/(editor)/[id]/page.tsx | 37 +++++ .../(editor)/cheatsheet/page.tsx} | 9 +- frontend/src/app/(editor)/global-error.tsx | 42 +++++ .../(editor)/history/page.tsx} | 14 +- .../intro.tsx => app/(editor)/intro/page.tsx} | 16 +- frontend/src/app/(editor)/layout.tsx | 61 +++++++ .../login.tsx => app/(editor)/login/page.tsx} | 27 ++-- frontend/src/app/(editor)/n/[noteId]/page.tsx | 54 +++++++ .../new.tsx => app/(editor)/new/page.tsx} | 22 +-- .../404.tsx => app/(editor)/not-found.tsx} | 6 +- frontend/src/app/(editor)/p/[noteId]/page.tsx | 35 ++++ .../(editor)/profile/page.tsx} | 23 +-- .../(editor)/register/page.tsx} | 37 ++--- frontend/src/app/(editor)/s/[noteId]/page.tsx | 38 +++++ frontend/src/app/(render)/global-error.tsx | 42 +++++ frontend/src/app/(render)/layout.tsx | 35 ++++ .../(render)/render/page.tsx} | 7 +- .../app/apple-icon.png} | Bin frontend/src/app/apple-icon.png.license | 3 + .../{public/icons => src/app}/favicon.ico | Bin frontend/src/app/favicon.ico.license | 3 + frontend/src/app/icon.png | Bin 0 -> 9405 bytes frontend/src/app/icon.png.license | 3 + .../application-loader/application-loader.tsx | 1 + .../initializers/load-dark-mode.ts | 4 - .../initializers/setupI18n.ts | 5 +- .../base-url/base-url-context-provider.tsx | 8 +- .../copyable-field/copyable-field.tsx | 3 +- .../copyable/hooks/use-copy-overlay.tsx | 6 - .../frontend-config-context-provider.tsx | 25 +-- ...reate-non-existing-note-hint.spec.tsx.snap | 2 + .../create-non-existing-note-hint.spec.tsx | 43 +++-- .../create-non-existing-note-hint.tsx | 17 +- .../hooks/use-load-note-from-server.ts | 11 +- .../note-loading-boundary.spec.tsx | 14 +- .../note-loading-boundary.tsx | 16 +- .../src/components/common/redirect-back.tsx | 2 +- frontend/src/components/common/redirect.tsx | 9 +- .../editor-page/editor-page-content.tsx | 10 +- .../hooks/yjs/use-on-note-deleted.ts | 9 +- .../hooks/yjs/use-receive-realtime-users.ts | 11 ++ .../head-meta-properties.tsx | 22 --- .../license-link-head.tsx | 24 --- .../head-meta-properties/opengraph-head.tsx | 28 ---- ...le-head.tsx => use-note-and-app-title.tsx} | 13 +- ...renderer-communicator-context-provider.tsx | 2 + ...o-editor-communicator-context-provider.tsx | 2 + .../reset-realtime-state-boundary.tsx | 27 ---- .../delete-note-sidebar-entry.tsx | 9 +- .../error-boundary/error-boundary.tsx | 69 -------- .../error-pages/common-error-page.tsx | 1 + .../global-dialogs/motd-modal/motd-modal.tsx | 1 + .../settings-dialog/settings-button.tsx | 2 + ...history-toolbar-state-context-provider.tsx | 8 +- .../use-sync-toolbar-state-to-url-effect.ts | 54 ++++--- .../landing-layout/landing-layout.tsx | 7 - .../navigation/header-bar/header-nav-link.tsx | 8 +- .../navigation/sign-out-dropdown-button.tsx | 2 +- .../help-dropdown/submenues/legal-submenu.tsx | 13 +- frontend/src/components/layout/base-head.tsx | 23 --- .../components/layout/dark-mode/dark-mode.tsx | 14 ++ .../dark-mode/use-apply-dark-mode-style.ts | 2 +- .../layout/expected-origin-boundary.tsx | 37 +++++ frontend/src/components/layout/fav-icon.tsx | 28 ---- .../utils/get-one-click-provider-metadata.ts | 2 +- .../ui-notification-boundary.tsx | 1 + .../document/document-markdown-renderer.tsx | 2 +- .../simple/simple-markdown-renderer.tsx | 2 +- .../respond-to-matching-request.ts | 40 ++++- .../common/use-array-string-url-parameter.ts | 9 +- frontend/src/hooks/common/use-base-url.tsx | 8 +- .../common/use-single-string-url-parameter.ts | 9 +- frontend/src/pages/[id].tsx | 40 ----- frontend/src/pages/_app.tsx | 76 --------- frontend/src/pages/api/private/config.ts | 141 +++++++++------- frontend/src/pages/n/[noteId].tsx | 31 ---- frontend/src/pages/p/[noteId].tsx | 26 --- frontend/src/pages/s/[noteId].tsx | 37 ----- frontend/src/redux/store-provider.tsx | 1 + .../utils/base-url-from-env-extractor.spec.ts | 43 +++-- .../src/utils/base-url-from-env-extractor.ts | 60 ++++--- .../utils/determine-current-origin.spec.ts | 151 ------------------ .../src/utils/determine-current-origin.ts | 40 ----- .../src/utils/expected-origin-boundary.tsx | 38 ----- frontend/src/utils/frontend-config-fetcher.ts | 35 ---- frontend/src/utils/is-apple-device.ts | 2 +- .../src/utils/is-client-side-rendering.ts | 12 -- frontend/src/utils/test-modes.js | 10 +- frontend/tsconfig.json | 44 ++--- yarn.lock | 4 +- 108 files changed, 893 insertions(+), 1188 deletions(-) delete mode 100644 frontend/cypress/e2e/note-meta-head.spec.ts delete mode 100644 frontend/public/icons/browserconfig.xml delete mode 100644 frontend/public/icons/favicon-16x16.png delete mode 100644 frontend/public/icons/favicon-32x32.png delete mode 100644 frontend/public/icons/mstile-144x144.png delete mode 100644 frontend/public/icons/mstile-150x150.png delete mode 100644 frontend/public/icons/mstile-310x150.png delete mode 100644 frontend/public/icons/mstile-310x310.png delete mode 100644 frontend/public/icons/mstile-70x70.png create mode 100644 frontend/src/app/(editor)/[id]/page.tsx rename frontend/src/{pages/cheatsheet.tsx => app/(editor)/cheatsheet/page.tsx} (58%) create mode 100644 frontend/src/app/(editor)/global-error.tsx rename frontend/src/{pages/history.tsx => app/(editor)/history/page.tsx} (59%) rename frontend/src/{pages/intro.tsx => app/(editor)/intro/page.tsx} (62%) create mode 100644 frontend/src/app/(editor)/layout.tsx rename frontend/src/{pages/login.tsx => app/(editor)/login/page.tsx} (72%) create mode 100644 frontend/src/app/(editor)/n/[noteId]/page.tsx rename frontend/src/{pages/new.tsx => app/(editor)/new/page.tsx} (56%) rename frontend/src/{pages/404.tsx => app/(editor)/not-found.tsx} (70%) create mode 100644 frontend/src/app/(editor)/p/[noteId]/page.tsx rename frontend/src/{pages/profile.tsx => app/(editor)/profile/page.tsx} (53%) rename frontend/src/{pages/register.tsx => app/(editor)/register/page.tsx} (72%) create mode 100644 frontend/src/app/(editor)/s/[noteId]/page.tsx create mode 100644 frontend/src/app/(render)/global-error.tsx create mode 100644 frontend/src/app/(render)/layout.tsx rename frontend/src/{pages/render.tsx => app/(render)/render/page.tsx} (69%) rename frontend/{public/icons/apple-touch-icon.png => src/app/apple-icon.png} (100%) create mode 100644 frontend/src/app/apple-icon.png.license rename frontend/{public/icons => src/app}/favicon.ico (100%) create mode 100644 frontend/src/app/favicon.ico.license create mode 100644 frontend/src/app/icon.png create mode 100644 frontend/src/app/icon.png.license delete mode 100644 frontend/src/components/editor-page/head-meta-properties/head-meta-properties.tsx delete mode 100644 frontend/src/components/editor-page/head-meta-properties/license-link-head.tsx delete mode 100644 frontend/src/components/editor-page/head-meta-properties/opengraph-head.tsx rename frontend/src/components/editor-page/head-meta-properties/{note-and-app-title-head.tsx => use-note-and-app-title.tsx} (78%) delete mode 100644 frontend/src/components/editor-page/reset-realtime-state-boundary.tsx delete mode 100644 frontend/src/components/error-boundary/error-boundary.tsx delete mode 100644 frontend/src/components/layout/base-head.tsx create mode 100644 frontend/src/components/layout/dark-mode/dark-mode.tsx rename frontend/src/{hooks => components/layout}/dark-mode/use-apply-dark-mode-style.ts (88%) create mode 100644 frontend/src/components/layout/expected-origin-boundary.tsx delete mode 100644 frontend/src/components/layout/fav-icon.tsx delete mode 100644 frontend/src/pages/[id].tsx delete mode 100644 frontend/src/pages/_app.tsx delete mode 100644 frontend/src/pages/n/[noteId].tsx delete mode 100644 frontend/src/pages/p/[noteId].tsx delete mode 100644 frontend/src/pages/s/[noteId].tsx delete mode 100644 frontend/src/utils/determine-current-origin.spec.ts delete mode 100644 frontend/src/utils/determine-current-origin.ts delete mode 100644 frontend/src/utils/expected-origin-boundary.tsx delete mode 100644 frontend/src/utils/frontend-config-fetcher.ts delete mode 100644 frontend/src/utils/is-client-side-rendering.ts diff --git a/frontend/build.sh b/frontend/build.sh index 715ee30b2..fe086eaf4 100755 --- a/frontend/build.sh +++ b/frontend/build.sh @@ -19,7 +19,7 @@ else fi echo "🦔 > Building" -next build +BUILD_TIME=true next build echo "🦔 > Bundling" mv .next/standalone dist diff --git a/frontend/cypress/e2e/note-meta-head.spec.ts b/frontend/cypress/e2e/note-meta-head.spec.ts deleted file mode 100644 index e79e1a365..000000000 --- a/frontend/cypress/e2e/note-meta-head.spec.ts +++ /dev/null @@ -1,47 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file) - * - * SPDX-License-Identifier: AGPL-3.0-only - */ - -describe('Opengraph metadata', () => { - beforeEach(() => { - cy.visitTestNote() - }) - - it('includes the note title if not overridden', () => { - cy.setCodemirrorContent('---\ntitle: Test title\n---') - cy.get('meta[property="og:title"]').should('have.attr', 'content', 'Test title') - }) - - it('includes the note title if overridden', () => { - cy.setCodemirrorContent('---\ntitle: Test title\nopengraph:\n title: Overridden title\n---') - cy.get('meta[property="og:title"]').should('have.attr', 'content', 'Overridden title') - }) - - it('includes custom opengraph tags', () => { - cy.setCodemirrorContent('---\nopengraph:\n image: https://dummyimage.com/48\n---') - cy.get('meta[property="og:image"]').should('have.attr', 'content', 'https://dummyimage.com/48') - }) -}) - -describe('License frontmatter', () => { - beforeEach(() => { - cy.visitTestNote() - }) - - it('sets the link tag if defined and not blank', () => { - cy.setCodemirrorContent('---\nlicense: https://example.com\n---') - cy.get('link[rel="license"]').should('have.attr', 'href', 'https://example.com') - }) - - it('does not set the link tag if not defined', () => { - cy.setCodemirrorContent('---\ntitle: No license for this note\n---') - cy.get('link[rel="license"]').should('not.exist') - }) - - it('does not set the link tag if defined but blank', () => { - cy.setCodemirrorContent('---\nlicense: \n---') - cy.get('link[rel="license"]').should('not.exist') - }) -}) diff --git a/frontend/cypress/support/config.ts b/frontend/cypress/support/config.ts index b9647c834..32435ae81 100644 --- a/frontend/cypress/support/config.ts +++ b/frontend/cypress/support/config.ts @@ -4,6 +4,7 @@ * SPDX-License-Identifier: AGPL-3.0-only */ import { AuthProviderType } from '../../src/api/config/types' +import { HttpMethod } from '../../src/handler-utils/respond-to-matching-request' declare namespace Cypress { interface Chainable { @@ -80,13 +81,7 @@ export const config = { } Cypress.Commands.add('loadConfig', (additionalConfig?: Partial) => { - return cy.intercept('/api/private/config', { - statusCode: 200, - body: { - ...config, - ...additionalConfig - } - }) + return cy.request(HttpMethod.POST, '/api/private/config', { ...config, ...additionalConfig }) }) beforeEach(() => { diff --git a/frontend/locales/en.json b/frontend/locales/en.json index b9b83597a..ca8c23bcd 100644 --- a/frontend/locales/en.json +++ b/frontend/locales/en.json @@ -1,7 +1,6 @@ { "app": { "slogan": "Ideas grow better together", - "title": "Collaborative markdown notes", "icon": "HedgeDoc logo with text" }, "notificationTest": { diff --git a/frontend/next-env.d.ts b/frontend/next-env.d.ts index 4f11a03dc..fd36f9494 100644 --- a/frontend/next-env.d.ts +++ b/frontend/next-env.d.ts @@ -1,5 +1,6 @@ /// /// +/// // NOTE: This file should not be edited // see https://nextjs.org/docs/basic-features/typescript for more information. diff --git a/frontend/next.config.js b/frontend/next.config.js index 7a34ca0ad..129bda01c 100644 --- a/frontend/next.config.js +++ b/frontend/next.config.js @@ -3,7 +3,7 @@ * * SPDX-License-Identifier: AGPL-3.0-only */ -const { isMockMode, isTestMode, isProfilingMode } = require('./src/utils/test-modes') +const { isMockMode, isTestMode, isProfilingMode, isBuildTime } = require('./src/utils/test-modes') const path = require('path') const CopyWebpackPlugin = require('copy-webpack-plugin') const withBundleAnalyzer = require('@next/bundle-analyzer')({ @@ -12,15 +12,13 @@ const withBundleAnalyzer = require('@next/bundle-analyzer')({ console.log('Node environment is', process.env.NODE_ENV) -if (isMockMode) { - console.log('Use mock API') -} - if (isTestMode) { console.warn(`This build runs in test mode. This means: - - no sandboxed iframe + - No sandboxed iframe - Additional data-attributes for e2e tests added to DOM - - Editor and renderer are running on the same origin`) + - Editor and renderer are running on the same origin + - No frontend config caching +`) } if (isMockMode) { @@ -28,7 +26,14 @@ if (isMockMode) { - No real data. All API responses are mocked - No persistent data - No realtime editing - `) +`) +} + +if (isBuildTime) { + console.warn(`This process runs in build mode. During build time this means: + - Editor and Renderer base urls are https://example.org + - No frontend config will be fetched +`) } if (isProfilingMode) { @@ -54,7 +59,6 @@ const svgrConfig = { /** @type {import('next').NextConfig} */ const rawNextConfig = { webpack: (config) => { - config.module.rules.push({ test: /\.svg$/i, issuer: /\.[jt]sx?$/, diff --git a/frontend/package.json b/frontend/package.json index 174c2f0f0..0978eacdf 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -5,8 +5,8 @@ "license": "AGPL-3.0", "scripts": { "build": "cross-env NODE_ENV=production ./build.sh", - "build:mock": "cross-env NEXT_PUBLIC_USE_MOCK_API=true ./build.sh --keep-mock-api", - "build:test": "cross-env NODE_ENV=test NEXT_PUBLIC_TEST_MODE=true ./build.sh --keep-mock-api", + "build:mock": "cross-env BUILD_TIME=true NEXT_PUBLIC_USE_MOCK_API=true ./build.sh --keep-mock-api", + "build:test": "cross-env BUILD_TIME=true NODE_ENV=test NEXT_PUBLIC_TEST_MODE=true ./build.sh --keep-mock-api", "analyze": "cross-env ANALYZE=true yarn build --profile", "format": "prettier -c \"src/**/*.{ts,tsx,js}\" \"cypress/**/*.{ts,tsx}\"", "format:fix": "prettier -w \"src/**/*.{ts,tsx,js}\" \"cypress/**/*.{ts,tsx}\"", diff --git a/frontend/public/icons/browserconfig.xml b/frontend/public/icons/browserconfig.xml deleted file mode 100644 index be3e03846..000000000 --- a/frontend/public/icons/browserconfig.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - #b51f08 - - - diff --git a/frontend/public/icons/favicon-16x16.png b/frontend/public/icons/favicon-16x16.png deleted file mode 100644 index 695b8aaa05745c859496e780d7466f7349747fe4..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 428 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!63?wyl`GbKJOS+@4BLl<6e(pbstUx|vage(c z!@6@aFM%BU0G|-oZIVpeB^bAhGj0=Q1hPR$f@zmJ!!}6}_wUu&m(uKyyQ@5#k$Jty zv#lWD`_VpbcIIoFn|CR*-L4KidfW9j&?v5wAiv=Mn%cUbrtW0yzj`N?1>_vg0*}aI z1_q`DAk26&weAT}u-4PXF+}2WXn!}~Q3Vd?-LWAoCGS4|zh71O-c<3%WPxav>nCPv ze7V91<#dSO)n!)X=THNM6(w7@>8CzX(x1vxwBgL;6DIn6 z94Yw;Ej@2{-ne5FpdW85?RI|g_V4+^@h;y#q?g#%)SOyr_jKRa|4h5tt4kZ2)&Lz6 zUKJ8i5|mi3P*9YgmYI{PP*Pcts*qVwlFYzRG3W6o9*)8=4UJR&r_Xpk4Pszc=GIH* z7FHJao-D#Ftl-jMayW%qd2@)u=^Iy09657D<_P=g29E_^dJM0`1xr3TnN9^-!QkoY K=d#Wzp$P!Wr=2(e diff --git a/frontend/public/icons/favicon-32x32.png b/frontend/public/icons/favicon-32x32.png deleted file mode 100644 index 80afec65c18fd83df7f0acc4e604d2f616f7e215..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 605 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnE3?yBabR7dyEa{HEjtmSN`?>!lvI6;x#X;^) z4C~IxyaaMW0(?STw@ET>lK>({5GlzB^vNvTE)L{^MI@O1UY&h4&-Hw)IZ)HR zmWYfn+mG9u-X7|GF(+q_2G?;9HERO}CPu~$le0EW&Av1trOq+?4p1piNswRg|9g{T zj?LF#5O~uR@q=^c9H2O7fk$L90|ToA2s1iv-`5Cqn7F5lV~EA++Q~P?niY6l+__C| zb%{;a`~QC-@21YybHS5#HmR%ZaJ%T*)u;NTHAqx3K#1M5lxf9LPhDBYs}J0pgcewd zTv@>|rB>|(0pF}Q^@l34 z+3$I`v!3(4b8Dle;KBEIeY%VVYaX_n@h#=cpTYk&QMcsT>Ma&3LJPUO55M}GmofLB zetg`o{d)@k?yUUztDcePjBLODZNp!{a0{;ri6{w5ELSKf%1_J8NmVGREJ#(zEGS84 zV5pe$_!AFDVVH)-DgV=FJf8+JFe`KGC36ca3wuu%VHQ?!X)rmQ!mPYGMB(&}D<_Ve kIU;j}{d9xJ0xvy=SK@*tpPWpm01boFyt=akR{07{qc6951J diff --git a/frontend/public/icons/mstile-144x144.png b/frontend/public/icons/mstile-144x144.png deleted file mode 100644 index fa2152abd047c56d6e12939b29d2019f6c94b432..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2094 zcmV+}2+{Y6P)Px#Bv4FLMNDaN|NsC0|NsC0|NsC0|NsC0|NsC0|NsC0 z|NsC0|NsC0|7|JN%K!iX3v^OWQvi<<=W@;|J-bz=8&BiQ000LrNkl{V)@53`!XQxYg$VIKvIE9}b? zTV&h#CH<&kJfNSK@QEd%4)z-i5Q9{5@z{>e93e1nQ9u-u(;**+R)m%q1qO&fV)WK> zk~nS$ydlfQc-qI<#~c`0WK+pksWnKM>- zi~Qp-?3?Z+{=Vy2t!}75>B3-Zk}7z%hFR*7`Bzmuz4qEoJw4;3e{inDRX$<$+0R^T zXDG>I25Xvn`iZHF=%zYeH&qmhfc=!WIEy`!%IRNR`1#uJ#FXBe5?Ua}_`|txw_*$o zv~0zc(3xOEJ3(Eani85#GJ@cF&y-LQ5U@sI)HV)`L*U#iw!_z`&~#%fU--Lt)Yq70 zA6q0ZI`yum+D@Wv4vdIrF5NM@Hc3lIW1H!=V{V&HHOY2Rs%;Jwh1_&=%4o96|*W-mf8x#hKEMq)21B-P&=1-k9VU zX}JrG)Gb>&4$Qk|B7Wsw1(*qza`d--;ZPKvV!B zF09xTNb|ABYmldv7$C=u?HK3)5rNi_av%xN5{|E)4iL!51px>s=+ML<^)-(tNQNW< zDr4R1=>P%U<0PsWc<)&)p+R5!xB_Aafkd=%jFM%RvfhE5lxRQ+@~xAH&;XvJfgq$} z9y(Dd>~NI?kV?4-iBLEzf>|?U_iR2w6$uc>(UYn|!*xpr#2^i%R#q;tRW)v;n~%_m z>PAAx*33s}qBu4uY|mH`q7k+wxb;5SWK-5KG>4~h6tE;D!jT*WERH4!%tx-pj*vJK zNFr3@fAcGFAk!DeTJeWTgcs#*%UF$Rkq`IVu~4-VWsV~`(K-`1%5C`bWu4a!6D`(i z=8;p5f@9iDF8~WYZp*HcSs2(pN8x`Z#s!jeXK)CDM0&JyAl@MW3Q;11z2};e8 zULVGbny?BiN=zbpS`4!#+Vj63V^P@MR;xf+EQ=4%M*mse-NDMw#H!Dz3M`mg+M%ZU z1XW-sZ+MidPr$IcDWRvj>XSH>MGxO`|EmqjSO@|Z#(l&i1;&n4X+Du|GYR7ib-uL?24l*A+lwOn?69 z)ATWYxg}p5fRJcc1zsF*vYVhJzo6GcXr=On1c)Ydt1a_U1Grp4A^8WdT@GkI=jDoJ za5G>DkI50r9oBf-GhnlB1Uq7dMuSU=mMdP)VXkx~W+GJ8;z|tl-*@OGos;LG+U6Cgx@G{a;ABePT>%h=S&#LUDT#0SfONT5nC Y0O}VJbn-$ql>h($07*qoM6N<$f{IbwPXGV_ diff --git a/frontend/public/icons/mstile-150x150.png b/frontend/public/icons/mstile-150x150.png deleted file mode 100644 index 5b277a2cb44acd20d983ca008a6b1a1c3a49cccf..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1824 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 diff --git a/frontend/public/icons/mstile-310x310.png b/frontend/public/icons/mstile-310x310.png deleted file mode 100644 index a43ae7ccad1bdd45e00cf3552c9515c3530faada..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3737 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 diff --git a/frontend/public/icons/safari-pinned-tab.svg b/frontend/public/icons/safari-pinned-tab.svg index aedf0d299..d67904f24 100644 --- a/frontend/public/icons/safari-pinned-tab.svg +++ b/frontend/public/icons/safari-pinned-tab.svg @@ -1 +1,7 @@ + + diff --git a/frontend/src/api/config/index.ts b/frontend/src/api/config/index.ts index 32df13eef..6fc0a3e47 100644 --- a/frontend/src/api/config/index.ts +++ b/frontend/src/api/config/index.ts @@ -5,6 +5,7 @@ */ import { GetApiRequestBuilder } from '../common/api-request-builder/get-api-request-builder' import type { FrontendConfig } from './types' +import { isBuildTime } from '../../utils/test-modes' /** * Fetches the frontend config from the backend. @@ -12,7 +13,10 @@ import type { FrontendConfig } from './types' * @return The frontend config. * @throws {Error} when the api request wasn't successful. */ -export const getConfig = async (baseUrl?: string): Promise => { +export const getConfig = async (baseUrl?: string): Promise => { + if (isBuildTime) { + return undefined + } const response = await new GetApiRequestBuilder('config', baseUrl).sendRequest() return response.asParsedJsonObject() } diff --git a/frontend/src/api/notes/index.ts b/frontend/src/api/notes/index.ts index 6684005b1..92a44e97f 100644 --- a/frontend/src/api/notes/index.ts +++ b/frontend/src/api/notes/index.ts @@ -16,8 +16,8 @@ import type { Note, NoteDeletionOptions, NoteMetadata } from './types' * @return Content and metadata of the specified note. * @throws {Error} when the api request wasn't successful. */ -export const getNote = async (noteIdOrAlias: string): Promise => { - const response = await new GetApiRequestBuilder('notes/' + noteIdOrAlias).sendRequest() +export const getNote = async (noteIdOrAlias: string, baseUrl?: string): Promise => { + const response = await new GetApiRequestBuilder('notes/' + noteIdOrAlias, baseUrl).sendRequest() return response.asParsedJsonObject() } diff --git a/frontend/src/app/(editor)/[id]/page.tsx b/frontend/src/app/(editor)/[id]/page.tsx new file mode 100644 index 000000000..29422d35e --- /dev/null +++ b/frontend/src/app/(editor)/[id]/page.tsx @@ -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 diff --git a/frontend/src/pages/cheatsheet.tsx b/frontend/src/app/(editor)/cheatsheet/page.tsx similarity index 58% rename from frontend/src/pages/cheatsheet.tsx rename to frontend/src/app/(editor)/cheatsheet/page.tsx index 96ee0a091..3d3713389 100644 --- a/frontend/src/pages/cheatsheet.tsx +++ b/frontend/src/app/(editor)/cheatsheet/page.tsx @@ -1,19 +1,18 @@ +'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 { useApplyDarkModeStyle } from '../hooks/dark-mode/use-apply-dark-mode-style' +import { CheatsheetContent } from '../../../components/cheatsheet/cheatsheet-content' import type { NextPage } from 'next' import { Container } from 'react-bootstrap' const CheatsheetPage: NextPage = () => { - useApplyDarkModeStyle() - return ( - + ) } diff --git a/frontend/src/app/(editor)/global-error.tsx b/frontend/src/app/(editor)/global-error.tsx new file mode 100644 index 000000000..dd8ee431f --- /dev/null +++ b/frontend/src/app/(editor)/global-error.tsx @@ -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 ( + + + +
+

An unknown error occurred

+

+ Don't worry, this happens sometimes. If this is the first time you see this page then try reloading + the app. +

+ If you can reproduce this error, then we would be glad if you{' '} + or{' '} + + +
+
+ + + ) +} diff --git a/frontend/src/pages/history.tsx b/frontend/src/app/(editor)/history/page.tsx similarity index 59% rename from frontend/src/pages/history.tsx rename to frontend/src/app/(editor)/history/page.tsx index 502ba6903..7305fc41c 100644 --- a/frontend/src/pages/history.tsx +++ b/frontend/src/app/(editor)/history/page.tsx @@ -1,13 +1,15 @@ +'use client' + /* - * SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file) + * 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 { 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' diff --git a/frontend/src/pages/intro.tsx b/frontend/src/app/(editor)/intro/page.tsx similarity index 62% rename from frontend/src/pages/intro.tsx rename to frontend/src/app/(editor)/intro/page.tsx index a8c0957e5..d4834e825 100644 --- a/frontend/src/pages/intro.tsx +++ b/frontend/src/app/(editor)/intro/page.tsx @@ -1,15 +1,17 @@ +'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 { 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' diff --git a/frontend/src/app/(editor)/layout.tsx b/frontend/src/app/(editor)/layout.tsx new file mode 100644 index 000000000..e9822556c --- /dev/null +++ b/frontend/src/app/(editor)/layout.tsx @@ -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 ( + + + + + + + + + + + + + {children} + + + + + + + + ) +} + +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' +} diff --git a/frontend/src/pages/login.tsx b/frontend/src/app/(editor)/login/page.tsx similarity index 72% rename from frontend/src/pages/login.tsx rename to frontend/src/app/(editor)/login/page.tsx index 3fa24b062..670f1b4d9 100644 --- a/frontend/src/pages/login.tsx +++ b/frontend/src/app/(editor)/login/page.tsx @@ -1,19 +1,22 @@ +'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 { 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' @@ -22,7 +25,7 @@ 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 = () => { +const LoginPage: NextPage = () => { useTranslation() const authProviders = useFrontendConfig().authProviders const userLoggedIn = useApplicationState((state) => !!state.user) diff --git a/frontend/src/app/(editor)/n/[noteId]/page.tsx b/frontend/src/app/(editor)/n/[noteId]/page.tsx new file mode 100644 index 000000000..ef6ee99c1 --- /dev/null +++ b/frontend/src/app/(editor)/n/[noteId]/page.tsx @@ -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 = ({ params }) => { + return ( + + + + + + ) +} + +/* + 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. + + + + + + export async function generateMetadata({ params }: PageParams): Promise { + 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 diff --git a/frontend/src/pages/new.tsx b/frontend/src/app/(editor)/new/page.tsx similarity index 56% rename from frontend/src/pages/new.tsx rename to frontend/src/app/(editor)/new/page.tsx index ece3f6554..474e4db1d 100644 --- a/frontend/src/pages/new.tsx +++ b/frontend/src/app/(editor)/new/page.tsx @@ -1,16 +1,18 @@ +'use client' + /* - * SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file) + * 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 { 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' @@ -18,7 +20,7 @@ import { useAsync } from 'react-use' /** * Creates a new note, optionally including the passed content and redirects to that note. */ -export const NewNotePage: NextPage = () => { +const NewNotePage: NextPage = () => { const newContent = useSingleStringUrlParameter('content', '') const { loading, error, value } = useAsync(() => { diff --git a/frontend/src/pages/404.tsx b/frontend/src/app/(editor)/not-found.tsx similarity index 70% rename from frontend/src/pages/404.tsx rename to frontend/src/app/(editor)/not-found.tsx index a2a0745a5..db02b35e8 100644 --- a/frontend/src/pages/404.tsx +++ b/frontend/src/app/(editor)/not-found.tsx @@ -3,14 +3,14 @@ * * SPDX-License-Identifier: AGPL-3.0-only */ -import { CommonErrorPage } from '../components/error-pages/common-error-page' +import { CommonErrorPage } from '../../components/error-pages/common-error-page' import type { NextPage } from 'next' /** * Renders a hedgedoc themed 404 page. */ -const Custom404: NextPage = () => { +const NotFound: NextPage = () => { return } -export default Custom404 +export default NotFound diff --git a/frontend/src/app/(editor)/p/[noteId]/page.tsx b/frontend/src/app/(editor)/p/[noteId]/page.tsx new file mode 100644 index 000000000..a65729dd3 --- /dev/null +++ b/frontend/src/app/(editor)/p/[noteId]/page.tsx @@ -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 = ({ params }) => { + useNoteAndAppTitle() + + return ( + + + + + + ) +} + +export default SlideShowPage diff --git a/frontend/src/pages/profile.tsx b/frontend/src/app/(editor)/profile/page.tsx similarity index 53% rename from frontend/src/pages/profile.tsx rename to frontend/src/app/(editor)/profile/page.tsx index 6d32836fb..428ebb6eb 100644 --- a/frontend/src/pages/profile.tsx +++ b/frontend/src/app/(editor)/profile/page.tsx @@ -1,17 +1,20 @@ +'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 { 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' @@ -19,7 +22,7 @@ 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 ProfilePage: NextPage = () => { const userProvider = useApplicationState((state) => state.user?.authProvider) if (!userProvider) { diff --git a/frontend/src/pages/register.tsx b/frontend/src/app/(editor)/register/page.tsx similarity index 72% rename from frontend/src/pages/register.tsx rename to frontend/src/app/(editor)/register/page.tsx index ce6f988e2..41cd3f92e 100644 --- a/frontend/src/pages/register.tsx +++ b/frontend/src/app/(editor)/register/page.tsx @@ -1,26 +1,27 @@ +'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 { 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 { 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' @@ -29,7 +30,7 @@ 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 = () => { +const RegisterPage: NextPage = () => { useTranslation() const router = useRouter() const allowRegister = useFrontendConfig().allowRegister diff --git a/frontend/src/app/(editor)/s/[noteId]/page.tsx b/frontend/src/app/(editor)/s/[noteId]/page.tsx new file mode 100644 index 000000000..bcd6702d7 --- /dev/null +++ b/frontend/src/app/(editor)/s/[noteId]/page.tsx @@ -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 = ({ params }) => { + useNoteAndAppTitle() + + return ( + + +
+ + +
+
+
+ ) +} + +export default DocumentReadOnlyPage diff --git a/frontend/src/app/(render)/global-error.tsx b/frontend/src/app/(render)/global-error.tsx new file mode 100644 index 000000000..4a1dfec11 --- /dev/null +++ b/frontend/src/app/(render)/global-error.tsx @@ -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 ( + + + +
+

An unknown error occurred

+

+ Don't worry, this happens sometimes. If this is the first time you see this page then try reloading + the app. +

+ If you can reproduce this error, then we would be glad if you + + or + +
+
+ + + ) +} diff --git a/frontend/src/app/(render)/layout.tsx b/frontend/src/app/(render)/layout.tsx new file mode 100644 index 000000000..8afb48915 --- /dev/null +++ b/frontend/src/app/(render)/layout.tsx @@ -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 ( + + + + + + + {children} + + + + + + + ) +} diff --git a/frontend/src/pages/render.tsx b/frontend/src/app/(render)/render/page.tsx similarity index 69% rename from frontend/src/pages/render.tsx rename to frontend/src/app/(render)/render/page.tsx index 5fc8d313a..2b777b579 100644 --- a/frontend/src/pages/render.tsx +++ b/frontend/src/app/(render)/render/page.tsx @@ -1,17 +1,18 @@ +'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 { 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 = () => { +const RenderPage: NextPage = () => { return ( diff --git a/frontend/public/icons/apple-touch-icon.png b/frontend/src/app/apple-icon.png similarity index 100% rename from frontend/public/icons/apple-touch-icon.png rename to frontend/src/app/apple-icon.png diff --git a/frontend/src/app/apple-icon.png.license b/frontend/src/app/apple-icon.png.license new file mode 100644 index 000000000..d685f690e --- /dev/null +++ b/frontend/src/app/apple-icon.png.license @@ -0,0 +1,3 @@ +SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file) + +SPDX-License-Identifier: LicenseRef-HedgeDoc-Icon-Usage-Guidelines diff --git a/frontend/public/icons/favicon.ico b/frontend/src/app/favicon.ico similarity index 100% rename from frontend/public/icons/favicon.ico rename to frontend/src/app/favicon.ico diff --git a/frontend/src/app/favicon.ico.license b/frontend/src/app/favicon.ico.license new file mode 100644 index 000000000..d685f690e --- /dev/null +++ b/frontend/src/app/favicon.ico.license @@ -0,0 +1,3 @@ +SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file) + +SPDX-License-Identifier: LicenseRef-HedgeDoc-Icon-Usage-Guidelines diff --git a/frontend/src/app/icon.png b/frontend/src/app/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..1eedb77affb7f1ef0d3268b220886e12dd780693 GIT binary patch literal 9405 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 LTx}m&Gh_Y-uw_kB literal 0 HcmV?d00001 diff --git a/frontend/src/app/icon.png.license b/frontend/src/app/icon.png.license new file mode 100644 index 000000000..d685f690e --- /dev/null +++ b/frontend/src/app/icon.png.license @@ -0,0 +1,3 @@ +SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file) + +SPDX-License-Identifier: LicenseRef-HedgeDoc-Icon-Usage-Guidelines diff --git a/frontend/src/components/application-loader/application-loader.tsx b/frontend/src/components/application-loader/application-loader.tsx index cdc0a993f..3c0062e0c 100644 --- a/frontend/src/components/application-loader/application-loader.tsx +++ b/frontend/src/components/application-loader/application-loader.tsx @@ -1,3 +1,4 @@ +'use client' /* * SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file) * diff --git a/frontend/src/components/application-loader/initializers/load-dark-mode.ts b/frontend/src/components/application-loader/initializers/load-dark-mode.ts index 20f2d9404..e055031b4 100644 --- a/frontend/src/components/application-loader/initializers/load-dark-mode.ts +++ b/frontend/src/components/application-loader/initializers/load-dark-mode.ts @@ -6,7 +6,6 @@ import { DARK_MODE_LOCAL_STORAGE_KEY } from '../../../hooks/dark-mode/use-save-dark-mode-preference-to-local-storage' import { setDarkModePreference } from '../../../redux/dark-mode/methods' import { DarkModePreference } from '../../../redux/dark-mode/types' -import { isClientSideRendering } from '../../../utils/is-client-side-rendering' import { Logger } from '../../../utils/logger' const logger = new Logger('Dark mode initializer') @@ -29,9 +28,6 @@ export const loadDarkMode = (): Promise => { * {@link false} if the user doesn't prefer dark mode or if the value couldn't be read from local storage. */ const fetchDarkModeFromLocalStorage = (): DarkModePreference => { - if (!isClientSideRendering()) { - return DarkModePreference.AUTO - } try { const colorScheme = window.localStorage.getItem(DARK_MODE_LOCAL_STORAGE_KEY) if (colorScheme === 'dark') { diff --git a/frontend/src/components/application-loader/initializers/setupI18n.ts b/frontend/src/components/application-loader/initializers/setupI18n.ts index 0d8892315..beb2e78e2 100644 --- a/frontend/src/components/application-loader/initializers/setupI18n.ts +++ b/frontend/src/components/application-loader/initializers/setupI18n.ts @@ -36,6 +36,9 @@ export const setUpI18n = async (): Promise => { } }) - i18n.on('languageChanged', (language) => (Settings.defaultLocale = language)) + i18n.on('languageChanged', (language) => { + Settings.defaultLocale = language + document.documentElement.lang = i18n.language + }) Settings.defaultLocale = i18n.language } diff --git a/frontend/src/components/common/base-url/base-url-context-provider.tsx b/frontend/src/components/common/base-url/base-url-context-provider.tsx index 609e78eb7..aa4d2c926 100644 --- a/frontend/src/components/common/base-url/base-url-context-provider.tsx +++ b/frontend/src/components/common/base-url/base-url-context-provider.tsx @@ -1,10 +1,11 @@ +'use client' /* * SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file) * * SPDX-License-Identifier: AGPL-3.0-only */ import type { PropsWithChildren } from 'react' -import React, { createContext, useState } from 'react' +import React, { createContext } from 'react' export interface BaseUrls { renderer: string @@ -27,10 +28,9 @@ export const BaseUrlContextProvider: React.FC { - const [baseUrlState] = useState(() => baseUrls) - return baseUrlState === undefined ? ( + return baseUrls === undefined ? ( HedgeDoc is not configured correctly! Please check the server log. ) : ( - {children} + {children} ) } diff --git a/frontend/src/components/common/copyable/copyable-field/copyable-field.tsx b/frontend/src/components/common/copyable/copyable-field/copyable-field.tsx index b6852dd7b..f458b29e0 100644 --- a/frontend/src/components/common/copyable/copyable-field/copyable-field.tsx +++ b/frontend/src/components/common/copyable/copyable-field/copyable-field.tsx @@ -3,7 +3,6 @@ * * SPDX-License-Identifier: AGPL-3.0-only */ -import { isClientSideRendering } from '../../../../utils/is-client-side-rendering' import { Logger } from '../../../../utils/logger' import { UiIcon } from '../../icons/ui-icon' import { ShowIf } from '../../show-if/show-if' @@ -30,7 +29,7 @@ export const CopyableField: React.FC = ({ content, shareOrig useTranslation() const sharingSupported = useMemo( - () => shareOriginUrl !== undefined && isClientSideRendering() && typeof navigator.share === 'function', + () => shareOriginUrl !== undefined && typeof navigator.share === 'function', [shareOriginUrl] ) diff --git a/frontend/src/components/common/copyable/hooks/use-copy-overlay.tsx b/frontend/src/components/common/copyable/hooks/use-copy-overlay.tsx index c75b85ee7..9a22fd812 100644 --- a/frontend/src/components/common/copyable/hooks/use-copy-overlay.tsx +++ b/frontend/src/components/common/copyable/hooks/use-copy-overlay.tsx @@ -3,7 +3,6 @@ * * SPDX-License-Identifier: AGPL-3.0-only */ -import { isClientSideRendering } from '../../../../utils/is-client-side-rendering' import { Logger } from '../../../../utils/logger' import { ShowIf } from '../../show-if/show-if' import type { ReactElement, RefObject } from 'react' @@ -45,11 +44,6 @@ export const useCopyOverlay = ( }, [reset, showState]) const copyToClipboard = useCallback(() => { - if (!isClientSideRendering()) { - setShowState(SHOW_STATE.ERROR) - log.error('Clipboard not available in server side rendering') - return - } if (typeof navigator.clipboard === 'undefined') { setShowState(SHOW_STATE.ERROR) return diff --git a/frontend/src/components/common/frontend-config-context/frontend-config-context-provider.tsx b/frontend/src/components/common/frontend-config-context/frontend-config-context-provider.tsx index 09b7de918..6452a3715 100644 --- a/frontend/src/components/common/frontend-config-context/frontend-config-context-provider.tsx +++ b/frontend/src/components/common/frontend-config-context/frontend-config-context-provider.tsx @@ -1,17 +1,13 @@ +'use client' /* * SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file) * * SPDX-License-Identifier: AGPL-3.0-only */ -import { getConfig } from '../../../api/config' import type { FrontendConfig } from '../../../api/config/types' -import { useBaseUrl } from '../../../hooks/common/use-base-url' -import { Logger } from '../../../utils/logger' import { frontendConfigContext } from './context' import type { PropsWithChildren } from 'react' -import React, { useEffect, useState } from 'react' - -const logger = new Logger('FrontendConfigContextProvider') +import React from 'react' interface FrontendConfigContextProviderProps extends PropsWithChildren { config?: FrontendConfig @@ -24,22 +20,9 @@ interface FrontendConfigContextProviderProps extends PropsWithChildren { * @param children the react elements to show if the config is valid */ export const FrontendConfigContextProvider: React.FC = ({ config, children }) => { - const [configState, setConfigState] = useState(() => config) - - const baseUrl = useBaseUrl() - - useEffect(() => { - if (config === undefined && configState === undefined) { - logger.debug('Fetching Config client side') - getConfig(baseUrl) - .then((config) => setConfigState(config)) - .catch((error) => logger.error(error)) - } - }, [baseUrl, config, configState]) - - return configState === undefined ? ( + return config === undefined ? ( No frontend config received! Please check the server log. ) : ( - {children} + {children} ) } diff --git a/frontend/src/components/common/note-loading-boundary/__snapshots__/create-non-existing-note-hint.spec.tsx.snap b/frontend/src/components/common/note-loading-boundary/__snapshots__/create-non-existing-note-hint.spec.tsx.snap index 6fd3da83b..8ff28ee55 100644 --- a/frontend/src/components/common/note-loading-boundary/__snapshots__/create-non-existing-note-hint.spec.tsx.snap +++ b/frontend/src/components/common/note-loading-boundary/__snapshots__/create-non-existing-note-hint.spec.tsx.snap @@ -38,6 +38,8 @@ exports[`create non existing note hint renders an button as initial state 1`] = `; +exports[`create non existing note hint renders nothing if no note id has been provided 1`] = `
`; + exports[`create non existing note hint shows an error message if note couldn't be created 1`] = `
{ const mockedNoteId = 'mockedNoteId' - const mockGetNoteIdQueryParameter = () => { - const expectedQueryParameter = 'noteId' - jest.spyOn(useSingleStringUrlParameterModule, 'useSingleStringUrlParameter').mockImplementation((parameter) => { - expect(parameter).toBe(expectedQueryParameter) - return mockedNoteId - }) - } - const mockCreateNoteWithPrimaryAlias = () => { jest .spyOn(createNoteWithPrimaryAliasModule, 'createNoteWithPrimaryAlias') @@ -59,14 +50,24 @@ describe('create non existing note hint', () => { jest.resetModules() }) - beforeEach(() => { - mockGetNoteIdQueryParameter() + it('renders nothing if no note id has been provided', async () => { + const onNoteCreatedCallback = jest.fn() + const view = render( + + ) + await waitForOtherPromisesToFinish() + expect(onNoteCreatedCallback).not.toBeCalled() + expect(view.container).toMatchSnapshot() }) it('renders an button as initial state', async () => { mockCreateNoteWithPrimaryAlias() const onNoteCreatedCallback = jest.fn() - const view = render() + const view = render( + + ) await screen.findByTestId('createNoteMessage') await waitForOtherPromisesToFinish() expect(onNoteCreatedCallback).not.toBeCalled() @@ -76,7 +77,11 @@ describe('create non existing note hint', () => { it('renders a waiting message when button is clicked', async () => { mockCreateNoteWithPrimaryAlias() const onNoteCreatedCallback = jest.fn() - const view = render() + const view = render( + + ) const button = await screen.findByTestId('createNoteButton') await act(() => { button.click() @@ -92,7 +97,11 @@ describe('create non existing note hint', () => { it('shows success message when the note has been created', async () => { mockCreateNoteWithPrimaryAlias() const onNoteCreatedCallback = jest.fn() - const view = render() + const view = render( + + ) const button = await screen.findByTestId('createNoteButton') await act(() => { button.click() @@ -108,7 +117,11 @@ describe('create non existing note hint', () => { it("shows an error message if note couldn't be created", async () => { mockFailingCreateNoteWithPrimaryAlias() const onNoteCreatedCallback = jest.fn() - const view = render() + const view = render( + + ) const button = await screen.findByTestId('createNoteButton') await act(() => { button.click() diff --git a/frontend/src/components/common/note-loading-boundary/create-non-existing-note-hint.tsx b/frontend/src/components/common/note-loading-boundary/create-non-existing-note-hint.tsx index 939a45b9a..991efc2e5 100644 --- a/frontend/src/components/common/note-loading-boundary/create-non-existing-note-hint.tsx +++ b/frontend/src/components/common/note-loading-boundary/create-non-existing-note-hint.tsx @@ -4,7 +4,6 @@ * SPDX-License-Identifier: AGPL-3.0-only */ import { createNoteWithPrimaryAlias } from '../../../api/notes' -import { useSingleStringUrlParameter } from '../../../hooks/common/use-single-string-url-parameter' import { testId } from '../../../utils/test-id' import { UiIcon } from '../icons/ui-icon' import { ShowIf } from '../show-if/show-if' @@ -20,6 +19,7 @@ import { useAsyncFn } from 'react-use' export interface CreateNonExistingNoteHintProps { onNoteCreated: () => void + noteId: string | undefined } /** @@ -27,17 +27,16 @@ export interface CreateNonExistingNoteHintProps { * When the button was clicked it also shows the progress. * * @param onNoteCreated A function that will be called after the note was created. + * @param noteId The wanted id for the note to create */ -export const CreateNonExistingNoteHint: React.FC = ({ onNoteCreated }) => { +export const CreateNonExistingNoteHint: React.FC = ({ onNoteCreated, noteId }) => { useTranslation() - const noteIdFromUrl = useSingleStringUrlParameter('noteId', undefined) const [returnState, createNote] = useAsyncFn(async () => { - if (noteIdFromUrl === undefined) { - throw new Error('Note id not set') + if (noteId !== undefined) { + return await createNoteWithPrimaryAlias('', noteId) } - return await createNoteWithPrimaryAlias('', noteIdFromUrl) - }, [noteIdFromUrl]) + }, [noteId]) const onClickHandler = useCallback(() => { void createNote() @@ -49,7 +48,7 @@ export const CreateNonExistingNoteHint: React.FC } }, [onNoteCreated, returnState.value]) - if (noteIdFromUrl === undefined) { + if (noteId === undefined) { return null } else if (returnState.value) { return ( @@ -76,7 +75,7 @@ export const CreateNonExistingNoteHint: React.FC return ( - +
-
- - ) - } else { - return this.props.children - } - } -} diff --git a/frontend/src/components/error-pages/common-error-page.tsx b/frontend/src/components/error-pages/common-error-page.tsx index d345d5a49..d39e5bc70 100644 --- a/frontend/src/components/error-pages/common-error-page.tsx +++ b/frontend/src/components/error-pages/common-error-page.tsx @@ -1,3 +1,4 @@ +'use client' /* * SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file) * diff --git a/frontend/src/components/global-dialogs/motd-modal/motd-modal.tsx b/frontend/src/components/global-dialogs/motd-modal/motd-modal.tsx index 622faabf0..872b9bfb5 100644 --- a/frontend/src/components/global-dialogs/motd-modal/motd-modal.tsx +++ b/frontend/src/components/global-dialogs/motd-modal/motd-modal.tsx @@ -1,3 +1,4 @@ +'use client' /* * SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file) * diff --git a/frontend/src/components/global-dialogs/settings-dialog/settings-button.tsx b/frontend/src/components/global-dialogs/settings-dialog/settings-button.tsx index 53f8540f7..13bd5d09f 100644 --- a/frontend/src/components/global-dialogs/settings-dialog/settings-button.tsx +++ b/frontend/src/components/global-dialogs/settings-dialog/settings-button.tsx @@ -5,6 +5,7 @@ */ import { useBooleanState } from '../../../hooks/common/use-boolean-state' import { useOutlineButtonVariant } from '../../../hooks/dark-mode/use-outline-button-variant' +import { useSaveDarkModePreferenceToLocalStorage } from '../../../hooks/dark-mode/use-save-dark-mode-preference-to-local-storage' import { cypressId } from '../../../utils/cypress-attribute' import { IconButton } from '../../common/icon-button/icon-button' import { SettingsModal } from './settings-modal' @@ -19,6 +20,7 @@ export type SettingsButtonProps = Omit export const SettingsButton: React.FC = (props) => { const [show, showModal, hideModal] = useBooleanState(false) const buttonVariant = useOutlineButtonVariant() + useSaveDarkModePreferenceToLocalStorage() return ( diff --git a/frontend/src/components/history-page/history-toolbar/toolbar-context/history-toolbar-state-context-provider.tsx b/frontend/src/components/history-page/history-toolbar/toolbar-context/history-toolbar-state-context-provider.tsx index ad3d54073..df30f0cbb 100644 --- a/frontend/src/components/history-page/history-toolbar/toolbar-context/history-toolbar-state-context-provider.tsx +++ b/frontend/src/components/history-page/history-toolbar/toolbar-context/history-toolbar-state-context-provider.tsx @@ -20,13 +20,13 @@ export const historyToolbarStateContext = createContext> = ({ children }) => { - const urlParameterSearch = useSingleStringUrlParameter('search', '') - const urlParameterSelectedTags = useArrayStringUrlParameter('selectedTags') + const search = useSingleStringUrlParameter('search', '') + const selectedTags = useArrayStringUrlParameter('selectedTags') const stateWithDispatcher = useState(() => ({ viewState: ViewStateEnum.CARD, - search: urlParameterSearch, - selectedTags: urlParameterSelectedTags, + search: search, + selectedTags: selectedTags, titleSortDirection: SortModeEnum.no, lastVisitedSortDirection: SortModeEnum.down })) diff --git a/frontend/src/components/history-page/history-toolbar/toolbar-context/use-sync-toolbar-state-to-url-effect.ts b/frontend/src/components/history-page/history-toolbar/toolbar-context/use-sync-toolbar-state-to-url-effect.ts index c65f66bc5..ba20f6410 100644 --- a/frontend/src/components/history-page/history-toolbar/toolbar-context/use-sync-toolbar-state-to-url-effect.ts +++ b/frontend/src/components/history-page/history-toolbar/toolbar-context/use-sync-toolbar-state-to-url-effect.ts @@ -3,42 +3,46 @@ * * SPDX-License-Identifier: AGPL-3.0-only */ -import { useArrayStringUrlParameter } from '../../../../hooks/common/use-array-string-url-parameter' -import { useSingleStringUrlParameter } from '../../../../hooks/common/use-single-string-url-parameter' -import { Logger } from '../../../../utils/logger' import { useHistoryToolbarState } from './use-history-toolbar-state' import equal from 'fast-deep-equal' -import { useRouter } from 'next/router' +import { usePathname, useRouter, useSearchParams } from 'next/navigation' import { useEffect } from 'react' -const logger = new Logger('useSyncToolbarStateToUrl') - /** * Pushes the current search and tag selection into the navigation history stack of the browser. */ export const useSyncToolbarStateToUrlEffect = (): void => { const router = useRouter() - const urlParameterSearch = useSingleStringUrlParameter('search', '') - const urlParameterSelectedTags = useArrayStringUrlParameter('selectedTags') + const searchParams = useSearchParams() const [state] = useHistoryToolbarState() + const pathname = usePathname() useEffect(() => { - if (!equal(state.search, urlParameterSearch) || !equal(state.selectedTags, urlParameterSelectedTags)) { - router - .push( - { - pathname: router.pathname, - query: { - search: state.search === '' ? [] : state.search, - selectedTags: state.selectedTags - } - }, - undefined, - { - shallow: true - } - ) - .catch(() => logger.error("Can't update route")) + if (!searchParams || !pathname) { + return } - }, [state, router, urlParameterSearch, urlParameterSelectedTags]) + + const urlParameterSearch = searchParams.get('search') ?? '' + const urlParameterSelectedTags = searchParams.getAll('selectedTags') + const params = new URLSearchParams(searchParams.toString()) + let shouldUpdate = false + + if (!equal(state.search, urlParameterSearch)) { + if (!state.search) { + params.delete('search') + } else { + params.set('search', state.search) + } + shouldUpdate = true + } + if (!equal(state.selectedTags, urlParameterSelectedTags)) { + params.delete('selectedTags') + state.selectedTags.forEach((tag) => params.append('selectedTags', tag)) + shouldUpdate = true + } + + if (shouldUpdate) { + router.push(`${pathname}?${params.toString()}`) + } + }, [state, router, searchParams, pathname]) } diff --git a/frontend/src/components/landing-layout/landing-layout.tsx b/frontend/src/components/landing-layout/landing-layout.tsx index 921f8fb55..d500f9472 100644 --- a/frontend/src/components/landing-layout/landing-layout.tsx +++ b/frontend/src/components/landing-layout/landing-layout.tsx @@ -3,9 +3,6 @@ * * SPDX-License-Identifier: AGPL-3.0-only */ -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 { MotdModal } from '../global-dialogs/motd-modal/motd-modal' import { BaseAppBar } from '../layout/app-bar/base-app-bar' import { HeaderBar } from './navigation/header-bar/header-bar' import type { PropsWithChildren } from 'react' @@ -18,13 +15,9 @@ import { Container } from 'react-bootstrap' * @param children The children that should be rendered on the page. */ export const LandingLayout: React.FC = ({ children }) => { - useApplyDarkModeStyle() - useSaveDarkModePreferenceToLocalStorage() - return (
-
diff --git a/frontend/src/components/landing-layout/navigation/header-bar/header-nav-link.tsx b/frontend/src/components/landing-layout/navigation/header-bar/header-nav-link.tsx index 3c405864e..7281be3a8 100644 --- a/frontend/src/components/landing-layout/navigation/header-bar/header-nav-link.tsx +++ b/frontend/src/components/landing-layout/navigation/header-bar/header-nav-link.tsx @@ -8,7 +8,7 @@ import type { PropsWithDataCypressId } from '../../../../utils/cypress-attribute import { cypressId } from '../../../../utils/cypress-attribute' import styles from './header-nav-link.module.scss' import Link from 'next/link' -import { useRouter } from 'next/router' +import { usePathname } from 'next/navigation' import type { PropsWithChildren } from 'react' import React, { useMemo } from 'react' import { Nav } from 'react-bootstrap' @@ -25,17 +25,17 @@ export interface HeaderNavLinkProps extends PropsWithDataCypressId { * @param props Other navigation item props */ export const HeaderNavLink: React.FC> = ({ to, children, ...props }) => { - const { route } = useRouter() + const pathname = usePathname() const className = useMemo(() => { return concatCssClasses( { - [styles.active]: route === to + [styles.active]: pathname === to }, 'nav-link', styles.link ) - }, [route, to]) + }, [pathname, to]) return ( diff --git a/frontend/src/components/landing-layout/navigation/sign-out-dropdown-button.tsx b/frontend/src/components/landing-layout/navigation/sign-out-dropdown-button.tsx index e24933709..a85c13523 100644 --- a/frontend/src/components/landing-layout/navigation/sign-out-dropdown-button.tsx +++ b/frontend/src/components/landing-layout/navigation/sign-out-dropdown-button.tsx @@ -8,7 +8,7 @@ import { clearUser } from '../../../redux/user/methods' import { cypressId } from '../../../utils/cypress-attribute' import { UiIcon } from '../../common/icons/ui-icon' import { useUiNotifications } from '../../notifications/ui-notification-boundary' -import { useRouter } from 'next/router' +import { useRouter } from 'next/navigation' import React, { useCallback } from 'react' import { Dropdown } from 'react-bootstrap' import { BoxArrowRight as IconBoxArrowRight } from 'react-bootstrap-icons' diff --git a/frontend/src/components/layout/app-bar/app-bar-elements/help-dropdown/submenues/legal-submenu.tsx b/frontend/src/components/layout/app-bar/app-bar-elements/help-dropdown/submenues/legal-submenu.tsx index 3d5b2de41..b9ee33b8b 100644 --- a/frontend/src/components/layout/app-bar/app-bar-elements/help-dropdown/submenues/legal-submenu.tsx +++ b/frontend/src/components/layout/app-bar/app-bar-elements/help-dropdown/submenues/legal-submenu.tsx @@ -3,24 +3,23 @@ * * SPDX-License-Identifier: AGPL-3.0-only */ -import { useFrontendConfig } from '../../../../../common/frontend-config-context/use-frontend-config' import { ShowIf } from '../../../../../common/show-if/show-if' import { DropdownHeader } from '../dropdown-header' import { TranslatedDropdownItem } from '../translated-dropdown-item' -import React, { Fragment, useMemo } from 'react' +import type { ReactElement } from 'react' +import React, { Fragment } from 'react' import { Dropdown } from 'react-bootstrap' import { useTranslation } from 'react-i18next' +import { useFrontendConfig } from '../../../../../common/frontend-config-context/use-frontend-config' /** * Renders the legal submenu for the help dropdown. */ -export const LegalSubmenu: React.FC = () => { +export const LegalSubmenu: React.FC = (): null | ReactElement => { useTranslation() const specialUrls = useFrontendConfig().specialUrls - const linksConfigured = useMemo( - () => specialUrls.privacy || specialUrls.termsOfUse || specialUrls.imprint, - [specialUrls] - ) + + const linksConfigured = specialUrls?.privacy || specialUrls?.termsOfUse || specialUrls?.imprint if (!linksConfigured) { return null diff --git a/frontend/src/components/layout/base-head.tsx b/frontend/src/components/layout/base-head.tsx deleted file mode 100644 index 78fe90946..000000000 --- a/frontend/src/components/layout/base-head.tsx +++ /dev/null @@ -1,23 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file) - * - * SPDX-License-Identifier: AGPL-3.0-only - */ -import { useAppTitle } from '../../hooks/common/use-app-title' -import { FavIcon } from './fav-icon' -import Head from 'next/head' -import React from 'react' - -/** - * Sets basic browser meta tags. - */ -export const BaseHead: React.FC = () => { - const appTitle = useAppTitle() - return ( - - {appTitle} - - - - ) -} diff --git a/frontend/src/components/layout/dark-mode/dark-mode.tsx b/frontend/src/components/layout/dark-mode/dark-mode.tsx new file mode 100644 index 000000000..b612d81ba --- /dev/null +++ b/frontend/src/components/layout/dark-mode/dark-mode.tsx @@ -0,0 +1,14 @@ +'use client' +/* + * SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file) + * + * SPDX-License-Identifier: AGPL-3.0-only + */ +import { useApplyDarkModeStyle } from './use-apply-dark-mode-style' +import type React from 'react' + +export const DarkMode: React.FC = () => { + useApplyDarkModeStyle() + + return null +} diff --git a/frontend/src/hooks/dark-mode/use-apply-dark-mode-style.ts b/frontend/src/components/layout/dark-mode/use-apply-dark-mode-style.ts similarity index 88% rename from frontend/src/hooks/dark-mode/use-apply-dark-mode-style.ts rename to frontend/src/components/layout/dark-mode/use-apply-dark-mode-style.ts index ec5861e24..6baba3356 100644 --- a/frontend/src/hooks/dark-mode/use-apply-dark-mode-style.ts +++ b/frontend/src/components/layout/dark-mode/use-apply-dark-mode-style.ts @@ -3,7 +3,7 @@ * * SPDX-License-Identifier: AGPL-3.0-only */ -import { useDarkModeState } from './use-dark-mode-state' +import { useDarkModeState } from '../../../hooks/dark-mode/use-dark-mode-state' import { useEffect } from 'react' /** diff --git a/frontend/src/components/layout/expected-origin-boundary.tsx b/frontend/src/components/layout/expected-origin-boundary.tsx new file mode 100644 index 000000000..e440ab47e --- /dev/null +++ b/frontend/src/components/layout/expected-origin-boundary.tsx @@ -0,0 +1,37 @@ +/* + * SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file) + * + * SPDX-License-Identifier: AGPL-3.0-only + */ +import { headers } from 'next/headers' +import type { PropsWithChildren } from 'react' +import React from 'react' + +export interface ExpectedOriginBoundaryProps extends PropsWithChildren { + expectedOrigin: string +} + +export const buildOriginFromHeaders = (): string | undefined => { + const headers1 = headers() + const host = headers1.get('x-forwarded-host') ?? headers1.get('host') + if (host === null) { + return undefined + } + + const protocol = headers1.get('x-forwarded-proto')?.split(',')[0] ?? 'http' + return `${protocol}://${host}` +} + +export const ExpectedOriginBoundary: React.FC = ({ children, expectedOrigin }) => { + const currentOrigin = buildOriginFromHeaders() + + if (new URL(expectedOrigin).origin !== currentOrigin) { + return ( + {`You can't open this page using this URL. For this endpoint "${expectedOrigin}" is expected.`} + ) + } + return children +} diff --git a/frontend/src/components/layout/fav-icon.tsx b/frontend/src/components/layout/fav-icon.tsx deleted file mode 100644 index e8e7fc2e4..000000000 --- a/frontend/src/components/layout/fav-icon.tsx +++ /dev/null @@ -1,28 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file) - * - * SPDX-License-Identifier: AGPL-3.0-only - */ -import React, { Fragment } from 'react' - -/** - * Sets meta tags for the favicon. - */ -export const FavIcon: React.FC = () => { - return ( - - - - - - - - - - - - - - - ) -} diff --git a/frontend/src/components/login-page/auth/utils/get-one-click-provider-metadata.ts b/frontend/src/components/login-page/auth/utils/get-one-click-provider-metadata.ts index 14bc8d3a3..08335f3c8 100644 --- a/frontend/src/components/login-page/auth/utils/get-one-click-provider-metadata.ts +++ b/frontend/src/components/login-page/auth/utils/get-one-click-provider-metadata.ts @@ -28,7 +28,7 @@ export interface OneClickMetadata { } const getBackendAuthUrl = (providerIdentifer: string): string => { - return `auth/${providerIdentifer}` + return `/auth/${providerIdentifer}` } const logger = new Logger('GetOneClickProviderMetadata') diff --git a/frontend/src/components/notifications/ui-notification-boundary.tsx b/frontend/src/components/notifications/ui-notification-boundary.tsx index 7b63bd786..90896abff 100644 --- a/frontend/src/components/notifications/ui-notification-boundary.tsx +++ b/frontend/src/components/notifications/ui-notification-boundary.tsx @@ -1,3 +1,4 @@ +'use client' /* * SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file) * diff --git a/frontend/src/components/render-page/renderers/document/document-markdown-renderer.tsx b/frontend/src/components/render-page/renderers/document/document-markdown-renderer.tsx index efacb57fa..e785eb40d 100644 --- a/frontend/src/components/render-page/renderers/document/document-markdown-renderer.tsx +++ b/frontend/src/components/render-page/renderers/document/document-markdown-renderer.tsx @@ -3,9 +3,9 @@ * * SPDX-License-Identifier: AGPL-3.0-only */ -import { useApplyDarkModeStyle } from '../../../../hooks/dark-mode/use-apply-dark-mode-style' import { cypressId } from '../../../../utils/cypress-attribute' import type { ScrollProps } from '../../../editor-page/synced-scroll/scroll-props' +import { useApplyDarkModeStyle } from '../../../layout/dark-mode/use-apply-dark-mode-style' import type { LineMarkers } from '../../../markdown-renderer/extensions/linemarker/add-line-marker-markdown-it-plugin' import { LinemarkerMarkdownExtension } from '../../../markdown-renderer/extensions/linemarker/linemarker-markdown-extension' import { useCalculateLineMarkerPosition } from '../../../markdown-renderer/hooks/use-calculate-line-marker-positions' diff --git a/frontend/src/components/render-page/renderers/simple/simple-markdown-renderer.tsx b/frontend/src/components/render-page/renderers/simple/simple-markdown-renderer.tsx index cc16fd3dc..fa39c3493 100644 --- a/frontend/src/components/render-page/renderers/simple/simple-markdown-renderer.tsx +++ b/frontend/src/components/render-page/renderers/simple/simple-markdown-renderer.tsx @@ -3,8 +3,8 @@ * * SPDX-License-Identifier: AGPL-3.0-only */ -import { useApplyDarkModeStyle } from '../../../../hooks/dark-mode/use-apply-dark-mode-style' import { cypressId } from '../../../../utils/cypress-attribute' +import { useApplyDarkModeStyle } from '../../../layout/dark-mode/use-apply-dark-mode-style' import { useMarkdownExtensions } from '../../../markdown-renderer/hooks/use-markdown-extensions' import { MarkdownToReact } from '../../../markdown-renderer/markdown-to-react/markdown-to-react' import { useOnHeightChange } from '../../hooks/use-on-height-change' diff --git a/frontend/src/handler-utils/respond-to-matching-request.ts b/frontend/src/handler-utils/respond-to-matching-request.ts index 916fea563..22f89ceae 100644 --- a/frontend/src/handler-utils/respond-to-matching-request.ts +++ b/frontend/src/handler-utils/respond-to-matching-request.ts @@ -3,7 +3,7 @@ * * SPDX-License-Identifier: AGPL-3.0-only */ -import { isMockMode } from '../utils/test-modes' +import { isMockMode, isTestMode } from '../utils/test-modes' import type { NextApiRequest, NextApiResponse } from 'next' export enum HttpMethod { @@ -22,6 +22,7 @@ export enum HttpMethod { * @param res The response object. * @param response The response data that will be returned when the HTTP method was the expected one. * @param statusCode The status code with which the response will be sent. + * @param respondMethodNotAllowedOnMismatch If set and the method can't process the request then a 405 will be returned. Used for chaining multiple calls together. * @return {@link true} if the HTTP method of the request is the expected one, {@link false} otherwise. */ export const respondToMatchingRequest = ( @@ -29,17 +30,42 @@ export const respondToMatchingRequest = ( req: NextApiRequest, res: NextApiResponse, response: T, - statusCode = 200 + statusCode = 200, + respondMethodNotAllowedOnMismatch = true ): boolean => { if (!isMockMode) { res.status(404).send('Mock API is disabled') return false - } - if (method !== req.method) { - res.status(405).send('Method not allowed') - return false - } else { + } else if (method === req.method) { res.status(statusCode).json(response) return true + } else if (respondMethodNotAllowedOnMismatch) { + res.status(405).send('Method not allowed') + return true + } else { + return false } } + +/** + * Intercepts a mock HTTP request that is only allowed in test mode. + * Such requests can only be issued from localhost and only if mock API is activated. + * + * @param req The request object. + * @param res The response object. + * @param response The response data that will be returned when the HTTP method was the expected one. + */ +export const respondToTestRequest = (req: NextApiRequest, res: NextApiResponse, response: () => T): boolean => { + if (!isMockMode) { + res.status(404).send('Mock API is disabled') + } else if (req.method !== HttpMethod.POST) { + res.status(405).send('Method not allowed') + } else if (!isTestMode) { + res.status(404).send('Route only available in test mode') + } else if (req.socket.remoteAddress !== '127.0.0.1' && req.socket.remoteAddress !== '::1') { + res.status(403).send('Request must come from localhost') + } else { + res.status(200).json(response()) + } + return true +} diff --git a/frontend/src/hooks/common/use-array-string-url-parameter.ts b/frontend/src/hooks/common/use-array-string-url-parameter.ts index 508da50b6..2fe48b66a 100644 --- a/frontend/src/hooks/common/use-array-string-url-parameter.ts +++ b/frontend/src/hooks/common/use-array-string-url-parameter.ts @@ -3,7 +3,7 @@ * * SPDX-License-Identifier: AGPL-3.0-only */ -import { useRouter } from 'next/router' +import { useSearchParams } from 'next/navigation' import { useMemo } from 'react' /** @@ -13,10 +13,9 @@ import { useMemo } from 'react' * @return An array of values extracted from the router. */ export const useArrayStringUrlParameter = (parameter: string): string[] => { - const router = useRouter() + const router = useSearchParams() return useMemo(() => { - const value = router.query[parameter] - return (typeof value === 'string' ? [value] : value) ?? [] - }, [parameter, router.query]) + return router?.getAll(parameter) ?? [] + }, [parameter, router]) } diff --git a/frontend/src/hooks/common/use-base-url.tsx b/frontend/src/hooks/common/use-base-url.tsx index ea8359a36..641772427 100644 --- a/frontend/src/hooks/common/use-base-url.tsx +++ b/frontend/src/hooks/common/use-base-url.tsx @@ -4,7 +4,7 @@ * SPDX-License-Identifier: AGPL-3.0-only */ import { baseUrlContext } from '../../components/common/base-url/base-url-context-provider' -import { useRouter } from 'next/router' +import { usePathname } from 'next/navigation' import { useContext, useMemo } from 'react' export enum ORIGIN { @@ -22,11 +22,11 @@ export const useBaseUrl = (origin = ORIGIN.CURRENT_PAGE): string => { throw new Error('No base url context received. Did you forget to use the provider component?') } - const router = useRouter() + const route = usePathname() return useMemo(() => { - return (router.route === '/render' && origin === ORIGIN.CURRENT_PAGE) || origin === ORIGIN.RENDERER + return (route === '/render' && origin === ORIGIN.CURRENT_PAGE) || origin === ORIGIN.RENDERER ? baseUrls.renderer : baseUrls.editor - }, [origin, baseUrls.renderer, baseUrls.editor, router.route]) + }, [origin, baseUrls.renderer, baseUrls.editor, route]) } diff --git a/frontend/src/hooks/common/use-single-string-url-parameter.ts b/frontend/src/hooks/common/use-single-string-url-parameter.ts index e8035b9ed..c0aeb6960 100644 --- a/frontend/src/hooks/common/use-single-string-url-parameter.ts +++ b/frontend/src/hooks/common/use-single-string-url-parameter.ts @@ -3,7 +3,7 @@ * * SPDX-License-Identifier: AGPL-3.0-only */ -import { useRouter } from 'next/router' +import { useSearchParams } from 'next/navigation' import { useMemo } from 'react' /** @@ -14,10 +14,9 @@ import { useMemo } from 'react' * @return A value extracted from the router. */ export const useSingleStringUrlParameter = (parameter: string, fallback: T): string | T => { - const router = useRouter() + const router = useSearchParams() return useMemo(() => { - const value = router.query[parameter] - return (typeof value === 'string' ? value : value?.[0]) ?? fallback - }, [fallback, parameter, router.query]) + return router?.get(parameter) ?? fallback + }, [fallback, parameter, router]) } diff --git a/frontend/src/pages/[id].tsx b/frontend/src/pages/[id].tsx deleted file mode 100644 index a9027776f..000000000 --- a/frontend/src/pages/[id].tsx +++ /dev/null @@ -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 - } else if (value !== undefined) { - return - } else { - return Loading - } -} - -export default DirectLinkFallback diff --git a/frontend/src/pages/_app.tsx b/frontend/src/pages/_app.tsx deleted file mode 100644 index 97b5b32f7..000000000 --- a/frontend/src/pages/_app.tsx +++ /dev/null @@ -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) { - return ( - - - - - - - - - - - - - - - - - ) -} - -const baseUrlFromEnvExtractor = new BaseUrlFromEnvExtractor() -const frontendConfigFetcher = new FrontendConfigFetcher() - -HedgeDocApp.getInitialProps = async ({ ctx }: AppContext): Promise> => { - 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 diff --git a/frontend/src/pages/api/private/config.ts b/frontend/src/pages/api/private/config.ts index 3692d1204..9f186ba91 100644 --- a/frontend/src/pages/api/private/config.ts +++ b/frontend/src/pages/api/private/config.ts @@ -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(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(HttpMethod.GET, req, res, currentConfig, 200, false) || + respondToTestRequest(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 diff --git a/frontend/src/pages/n/[noteId].tsx b/frontend/src/pages/n/[noteId].tsx deleted file mode 100644 index 680cb1e38..000000000 --- a/frontend/src/pages/n/[noteId].tsx +++ /dev/null @@ -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 ( - - - - - - - - ) -} - -export default EditorPage diff --git a/frontend/src/pages/p/[noteId].tsx b/frontend/src/pages/p/[noteId].tsx deleted file mode 100644 index e57fafdfc..000000000 --- a/frontend/src/pages/p/[noteId].tsx +++ /dev/null @@ -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 ( - - - - - - - ) -} - -export default SlideShowPage diff --git a/frontend/src/pages/s/[noteId].tsx b/frontend/src/pages/s/[noteId].tsx deleted file mode 100644 index 819505e8c..000000000 --- a/frontend/src/pages/s/[noteId].tsx +++ /dev/null @@ -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 ( - - - - -
- - -
-
-
- ) -} - -export default DocumentReadOnlyPage diff --git a/frontend/src/redux/store-provider.tsx b/frontend/src/redux/store-provider.tsx index 06163ec0a..f2e4a7a4d 100644 --- a/frontend/src/redux/store-provider.tsx +++ b/frontend/src/redux/store-provider.tsx @@ -1,3 +1,4 @@ +'use client' /* * SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file) * diff --git a/frontend/src/utils/base-url-from-env-extractor.spec.ts b/frontend/src/utils/base-url-from-env-extractor.spec.ts index cba2e0884..cbf66b3af 100644 --- a/frontend/src/utils/base-url-from-env-extractor.spec.ts +++ b/frontend/src/utils/base-url-from-env-extractor.spec.ts @@ -9,10 +9,9 @@ describe('BaseUrlFromEnvExtractor', () => { it('should return the base urls if both are valid urls', () => { process.env.HD_BASE_URL = 'https://editor.example.org/' process.env.HD_RENDERER_BASE_URL = 'https://renderer.example.org/' - const baseUrlFromEnvExtractor = new BaseUrlFromEnvExtractor() - const result = baseUrlFromEnvExtractor.extractBaseUrls() - expect(result.isPresent()).toBeTruthy() - expect(result.get()).toStrictEqual({ + const sut = new BaseUrlFromEnvExtractor() + + expect(sut.extractBaseUrls()).toStrictEqual({ renderer: 'https://renderer.example.org/', editor: 'https://editor.example.org/' }) @@ -21,31 +20,33 @@ describe('BaseUrlFromEnvExtractor', () => { it('should return an empty optional if no var is set', () => { process.env.HD_BASE_URL = undefined process.env.HD_RENDERER_BASE_URL = undefined - const baseUrlFromEnvExtractor = new BaseUrlFromEnvExtractor() - expect(baseUrlFromEnvExtractor.extractBaseUrls().isEmpty()).toBeTruthy() + const sut = new BaseUrlFromEnvExtractor() + + expect(() => sut.extractBaseUrls()).toThrow() }) it("should return an empty optional if editor base url isn't an URL", () => { process.env.HD_BASE_URL = 'bibedibabedibu' process.env.HD_RENDERER_BASE_URL = 'https://renderer.example.org/' - const baseUrlFromEnvExtractor = new BaseUrlFromEnvExtractor() - expect(baseUrlFromEnvExtractor.extractBaseUrls().isEmpty()).toBeTruthy() + const sut = new BaseUrlFromEnvExtractor() + + expect(() => sut.extractBaseUrls()).toThrow() }) it("should return an empty optional if renderer base url isn't an URL", () => { process.env.HD_BASE_URL = 'https://editor.example.org/' process.env.HD_RENDERER_BASE_URL = 'bibedibabedibu' - const baseUrlFromEnvExtractor = new BaseUrlFromEnvExtractor() - expect(baseUrlFromEnvExtractor.extractBaseUrls().isEmpty()).toBeTruthy() + const sut = new BaseUrlFromEnvExtractor() + + expect(() => sut.extractBaseUrls()).toThrow() }) it("should return an optional if editor base url isn't ending with a slash", () => { process.env.HD_BASE_URL = 'https://editor.example.org' process.env.HD_RENDERER_BASE_URL = 'https://renderer.example.org/' - const baseUrlFromEnvExtractor = new BaseUrlFromEnvExtractor() - const result = baseUrlFromEnvExtractor.extractBaseUrls() - expect(result.isPresent()).toBeTruthy() - expect(result.get()).toStrictEqual({ + const sut = new BaseUrlFromEnvExtractor() + + expect(sut.extractBaseUrls()).toStrictEqual({ renderer: 'https://renderer.example.org/', editor: 'https://editor.example.org/' }) @@ -54,10 +55,9 @@ describe('BaseUrlFromEnvExtractor', () => { it("should return an optional if renderer base url isn't ending with a slash", () => { process.env.HD_BASE_URL = 'https://editor.example.org/' process.env.HD_RENDERER_BASE_URL = 'https://renderer.example.org' - const baseUrlFromEnvExtractor = new BaseUrlFromEnvExtractor() - const result = baseUrlFromEnvExtractor.extractBaseUrls() - expect(result.isPresent()).toBeTruthy() - expect(result.get()).toStrictEqual({ + const sut = new BaseUrlFromEnvExtractor() + + expect(sut.extractBaseUrls()).toStrictEqual({ renderer: 'https://renderer.example.org/', editor: 'https://editor.example.org/' }) @@ -66,10 +66,9 @@ describe('BaseUrlFromEnvExtractor', () => { it('should copy editor base url to renderer base url if renderer base url is omitted', () => { process.env.HD_BASE_URL = 'https://editor.example.org/' delete process.env.HD_RENDERER_BASE_URL - const baseUrlFromEnvExtractor = new BaseUrlFromEnvExtractor() - const result = baseUrlFromEnvExtractor.extractBaseUrls() - expect(result.isPresent()).toBeTruthy() - expect(result.get()).toStrictEqual({ + const sut = new BaseUrlFromEnvExtractor() + + expect(sut.extractBaseUrls()).toStrictEqual({ renderer: 'https://editor.example.org/', editor: 'https://editor.example.org/' }) diff --git a/frontend/src/utils/base-url-from-env-extractor.ts b/frontend/src/utils/base-url-from-env-extractor.ts index 90c511202..72a74b7f4 100644 --- a/frontend/src/utils/base-url-from-env-extractor.ts +++ b/frontend/src/utils/base-url-from-env-extractor.ts @@ -5,15 +5,15 @@ */ import type { BaseUrls } from '../components/common/base-url/base-url-context-provider' import { Logger } from './logger' -import { isTestMode } from './test-modes' +import { isTestMode, isBuildTime } from './test-modes' import { NoSubdirectoryAllowedError, parseUrl } from '@hedgedoc/commons' import { Optional } from '@mrdrogdrog/optional' /** - * Extracts the editor and renderer base urls from the environment variables. + * Extracts and caches the editor and renderer base urls from the environment variables. */ export class BaseUrlFromEnvExtractor { - private baseUrls: Optional | undefined + private baseUrls: BaseUrls | undefined private readonly logger = new Logger('Base URL Configuration') private extractUrlFromEnvVar(envVarName: string, envVarValue: string | undefined): Optional { @@ -51,23 +51,17 @@ export class BaseUrlFromEnvExtractor { return this.extractUrlFromEnvVar('HD_RENDERER_BASE_URL', process.env.HD_RENDERER_BASE_URL) } - private renewBaseUrls(): void { - this.baseUrls = this.extractEditorBaseUrlFromEnv().flatMap((editorBaseUrl) => - this.extractRendererBaseUrlFromEnv(editorBaseUrl).map((rendererBaseUrl) => { - return { - editor: editorBaseUrl.toString(), - renderer: rendererBaseUrl.toString() - } - }) - ) - this.baseUrls.ifPresent((urls) => { - this.logger.info('Editor base URL', urls.editor.toString()) - this.logger.info('Renderer base URL', urls.renderer.toString()) - }) - } - - private isEnvironmentExtractDone(): boolean { - return this.baseUrls !== undefined + private renewBaseUrls(): BaseUrls { + return this.extractEditorBaseUrlFromEnv() + .flatMap((editorBaseUrl) => + this.extractRendererBaseUrlFromEnv(editorBaseUrl).map((rendererBaseUrl) => { + return { + editor: editorBaseUrl.toString(), + renderer: rendererBaseUrl.toString() + } + }) + ) + .orElseThrow(() => new Error('couldnt parse env vars')) } /** @@ -75,10 +69,28 @@ export class BaseUrlFromEnvExtractor { * * @return An {@link Optional} with the base urls. */ - public extractBaseUrls(): Optional { - if (!this.isEnvironmentExtractDone()) { - this.renewBaseUrls() + public extractBaseUrls(): BaseUrls { + if (isBuildTime) { + return { + editor: 'https://example.org/', + renderer: 'https://example.org/' + } } - return Optional.ofNullable(this.baseUrls).flatMap((value) => value) + + if (this.baseUrls === undefined) { + this.baseUrls = this.renewBaseUrls() + this.logBaseUrls() + } + return this.baseUrls + } + + private logBaseUrls() { + if (this.baseUrls === undefined) { + return + } + this.logger.info('Editor base URL', this.baseUrls.editor.toString()) + this.logger.info('Renderer base URL', this.baseUrls.renderer.toString()) } } + +export const baseUrlFromEnvExtractor = new BaseUrlFromEnvExtractor() diff --git a/frontend/src/utils/determine-current-origin.spec.ts b/frontend/src/utils/determine-current-origin.spec.ts deleted file mode 100644 index 6fdbb4d2f..000000000 --- a/frontend/src/utils/determine-current-origin.spec.ts +++ /dev/null @@ -1,151 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file) - * - * SPDX-License-Identifier: AGPL-3.0-only - */ -import { determineCurrentOrigin } from './determine-current-origin' -import * as IsClientSideRenderingModule from './is-client-side-rendering' -import type { NextPageContext } from 'next' -import { Mock } from 'ts-mockery' - -jest.mock('./is-client-side-rendering') -describe('determineCurrentOrigin', () => { - describe('client side', () => { - it('parses a client side origin correctly', () => { - jest.spyOn(IsClientSideRenderingModule, 'isClientSideRendering').mockImplementation(() => true) - const expectedOrigin = 'expectedOrigin' - Object.defineProperty(window, 'location', { value: { origin: expectedOrigin } }) - expect(determineCurrentOrigin(Mock.of({}))).toBe(expectedOrigin) - }) - }) - - describe('server side', () => { - beforeEach(() => { - jest.spyOn(IsClientSideRenderingModule, 'isClientSideRendering').mockImplementation(() => false) - }) - - it("won't return an origin if no request is present", () => { - expect(determineCurrentOrigin(Mock.of({}))).toBeUndefined() - }) - - it("won't return an origin if no headers are present", () => { - expect(determineCurrentOrigin(Mock.of({ req: { headers: undefined } }))).toBeUndefined() - }) - - it("won't return an origin if no host is present", () => { - expect( - determineCurrentOrigin( - Mock.of({ - req: { - headers: {} - } - }) - ) - ).toBeUndefined() - }) - - it('will return an origin for a forwarded host', () => { - expect( - determineCurrentOrigin( - Mock.of({ - req: { - headers: { - 'x-forwarded-host': 'forwardedMockHost', - 'x-forwarded-proto': 'mockProtocol' - } - } - }) - ) - ).toBe('mockProtocol://forwardedMockHost') - }) - - it("will fallback to host header if x-forwarded-host isn't present", () => { - expect( - determineCurrentOrigin( - Mock.of({ - req: { - headers: { - host: 'mockHost', - 'x-forwarded-proto': 'mockProtocol' - } - } - }) - ) - ).toBe('mockProtocol://mockHost') - }) - - it('will prefer x-forwarded-host over host', () => { - expect( - determineCurrentOrigin( - Mock.of({ - req: { - headers: { - 'x-forwarded-host': 'forwardedMockHost', - host: 'mockHost', - 'x-forwarded-proto': 'mockProtocol' - } - } - }) - ) - ).toBe('mockProtocol://forwardedMockHost') - }) - - it('will fallback to http if x-forwarded-proto is missing', () => { - expect( - determineCurrentOrigin( - Mock.of({ - req: { - headers: { - 'x-forwarded-host': 'forwardedMockHost' - } - } - }) - ) - ).toBe('http://forwardedMockHost') - }) - - it('will use the first header if x-forwarded-proto is defined multiple times', () => { - expect( - determineCurrentOrigin( - Mock.of({ - req: { - headers: { - 'x-forwarded-proto': ['mockProtocol1', 'mockProtocol2'], - 'x-forwarded-host': 'forwardedMockHost' - } - } - }) - ) - ).toBe('mockProtocol1://forwardedMockHost') - }) - - it('will use the first header if x-forwarded-host is defined multiple times', () => { - expect( - determineCurrentOrigin( - Mock.of({ - req: { - headers: { - 'x-forwarded-host': ['forwardedMockHost1', 'forwardedMockHost2'] - } - } - }) - ) - ).toBe('http://forwardedMockHost1') - }) - - it('will use the first value if x-forwarded-proto is a comma separated list', () => { - expect( - determineCurrentOrigin( - Mock.of({ - req: { - headers: { - 'x-forwarded-proto': 'mockProtocol1,mockProtocol2', - 'x-forwarded-host': 'forwardedMockHost' - } - } - }) - ) - ).toBe('mockProtocol1://forwardedMockHost') - }) - }) -}) diff --git a/frontend/src/utils/determine-current-origin.ts b/frontend/src/utils/determine-current-origin.ts deleted file mode 100644 index fcadf16b5..000000000 --- a/frontend/src/utils/determine-current-origin.ts +++ /dev/null @@ -1,40 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file) - * - * SPDX-License-Identifier: AGPL-3.0-only - */ -import { isClientSideRendering } from './is-client-side-rendering' -import { Optional } from '@mrdrogdrog/optional' -import type { IncomingHttpHeaders } from 'http' -import type { NextPageContext } from 'next' - -/** - * Determines the location origin of the current request. - * Client side rendering will use the browsers window location. - * Server side rendering will use the http request. - * - * @param context The next page context that contains the http headers - * @return the determined request origin. Will be undefined if no origin could be determined. - */ -export const determineCurrentOrigin = (context: NextPageContext): string | undefined => { - if (isClientSideRendering()) { - return window.location.origin - } - return Optional.ofNullable(context.req?.headers) - .flatMap((headers) => buildOriginFromHeaders(headers)) - .orElse(undefined) -} - -const buildOriginFromHeaders = (headers: IncomingHttpHeaders) => { - const rawHost = headers['x-forwarded-host'] ?? headers['host'] - return extractFirstValue(rawHost).map((host) => { - const protocol = extractFirstValue(headers['x-forwarded-proto']).orElse('http') - return `${protocol}://${host}` - }) -} - -const extractFirstValue = (rawValue: string | string[] | undefined): Optional => { - return Optional.ofNullable(rawValue) - .map((value) => (typeof value === 'string' ? value : value[0])) - .map((value) => value.split(',')[0]) -} diff --git a/frontend/src/utils/expected-origin-boundary.tsx b/frontend/src/utils/expected-origin-boundary.tsx deleted file mode 100644 index f3893ef01..000000000 --- a/frontend/src/utils/expected-origin-boundary.tsx +++ /dev/null @@ -1,38 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file) - * - * SPDX-License-Identifier: AGPL-3.0-only - */ -import { useBaseUrl } from '../hooks/common/use-base-url' -import type { PropsWithChildren } from 'react' -import React, { Fragment, useMemo } from 'react' - -export interface ExpectedOriginBoundaryProps { - currentOrigin?: string -} - -/** - * Checks if the url of the current browser window matches the expected origin. - * This is necessary to ensure that the render endpoint is only opened from the rendering origin. - * - * @param children The children react element that should be rendered if the origin is correct - * @param currentOrigin the current origin from client or server side rendering context - */ -export const ExpectedOriginBoundary: React.FC> = ({ - children, - currentOrigin -}) => { - const baseUrl = useBaseUrl() - const expectedOrigin = useMemo(() => new URL(baseUrl).origin, [baseUrl]) - - if (currentOrigin !== expectedOrigin) { - return ( - {`You can't open this page using this URL. For this endpoint "${expectedOrigin}" is expected.`} - ) - } else { - return {children} - } -} diff --git a/frontend/src/utils/frontend-config-fetcher.ts b/frontend/src/utils/frontend-config-fetcher.ts deleted file mode 100644 index 2dccf6497..000000000 --- a/frontend/src/utils/frontend-config-fetcher.ts +++ /dev/null @@ -1,35 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file) - * - * SPDX-License-Identifier: AGPL-3.0-only - */ -import { getConfig } from '../api/config' -import type { FrontendConfig } from '../api/config/types' -import type { BaseUrls } from '../components/common/base-url/base-url-context-provider' -import { Logger } from './logger' - -/** - * Fetches and caches the {@link FrontendConfig frontend config} from the backend. - */ -export class FrontendConfigFetcher { - private readonly logger = new Logger('Frontend config fetcher') - - private frontendConfig: FrontendConfig | undefined = undefined - - public async fetch(baseUrls: BaseUrls | undefined): Promise { - if (!this.frontendConfig) { - if (baseUrls === undefined) { - return undefined - } - const baseUrl = baseUrls.editor.toString() - try { - this.frontendConfig = await getConfig(baseUrl) - } catch (error) { - this.logger.error(`Couldn't fetch frontend configuration from ${baseUrl}`, error) - return undefined - } - this.logger.info(`Fetched frontend config from ${baseUrl}`) - } - return this.frontendConfig - } -} diff --git a/frontend/src/utils/is-apple-device.ts b/frontend/src/utils/is-apple-device.ts index 8dcb212ea..48bd53639 100644 --- a/frontend/src/utils/is-apple-device.ts +++ b/frontend/src/utils/is-apple-device.ts @@ -8,7 +8,7 @@ * Determines if the client is running on an Apple device like a Mac or an iPhone. * This is necessary to e.g. determine different keyboard shortcuts. */ -export const isAppleDevice: () => boolean = () => { +export const isAppleDevice = (): boolean => { const platform = navigator?.userAgentData?.platform || navigator?.platform || 'unknown' return platform.startsWith('Mac') || platform === 'iPhone' } diff --git a/frontend/src/utils/is-client-side-rendering.ts b/frontend/src/utils/is-client-side-rendering.ts deleted file mode 100644 index fbab8d7c8..000000000 --- a/frontend/src/utils/is-client-side-rendering.ts +++ /dev/null @@ -1,12 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file) - * - * SPDX-License-Identifier: AGPL-3.0-only - */ - -/** - * Detects if the application is running on client side. - */ -export const isClientSideRendering = (): boolean => { - return typeof window !== 'undefined' && typeof window.navigator !== 'undefined' -} diff --git a/frontend/src/utils/test-modes.js b/frontend/src/utils/test-modes.js index 13b52e7b9..56ad561a6 100644 --- a/frontend/src/utils/test-modes.js +++ b/frontend/src/utils/test-modes.js @@ -43,9 +43,17 @@ const isDevMode = process.env.NODE_ENV === 'development' */ const isProfilingMode = !!process.env.ANALYZE && isPositiveAnswer(process.env.ANALYZE) +/** + * Defines if the currently running process is building or executing. + * + * @type boolean + */ +const isBuildTime = !!process.env.BUILD_TIME && isPositiveAnswer(process.env.BUILD_TIME) + module.exports = { isTestMode, isMockMode, isDevMode, - isProfilingMode + isProfilingMode, + isBuildTime } diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json index aef3daa9a..e6da63df2 100644 --- a/frontend/tsconfig.json +++ b/frontend/tsconfig.json @@ -1,36 +1,42 @@ { - "compilerOptions" : { - "target" : "esnext", - "lib" : [ + "compilerOptions": { + "target": "esnext", + "lib": [ "dom", "dom.iterable", "esnext" ], - "allowJs" : true, - "skipLibCheck" : true, - "strict" : true, - "forceConsistentCasingInFileNames" : true, - "noEmit" : true, - "esModuleInterop" : true, - "module" : "esnext", - "moduleResolution" : "node", - "resolveJsonModule" : true, - "isolatedModules" : true, - "jsx" : "preserve", - "incremental" : true, - "types" : [ + "allowJs": true, + "skipLibCheck": true, + "strict": true, + "forceConsistentCasingInFileNames": true, + "noEmit": true, + "esModuleInterop": true, + "module": "esnext", + "moduleResolution": "node", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "preserve", + "incremental": true, + "types": [ "node", "@testing-library/jest-dom", "@types/jest" + ], + "plugins": [ + { + "name": "next" + } ] }, - "include" : [ + "include": [ "src/external-types/images/index.d.ts", "next-env.d.ts", "**/*.ts", - "**/*.tsx" + "**/*.tsx", + ".next/types/**/*.ts" ], - "exclude" : [ + "exclude": [ "node_modules", "cypress", "cypress.config.ts", diff --git a/yarn.lock b/yarn.lock index 25066e038..3f5c3ab3f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -17959,9 +17959,9 @@ __metadata: languageName: node linkType: hard -"vega-canvas@patch:vega-canvas@npm%3A1.2.7#./.yarn/patches/vega-canvas-npm-1.2.7-df0c331091.patch::locator=hedgedoc%40workspace%3A.": +"vega-canvas@patch:vega-canvas@npm%3A1.2.7#./.yarn/patches/remove-vega-canvas-node.patch::locator=hedgedoc%40workspace%3A.": version: 1.2.7 - resolution: "vega-canvas@patch:vega-canvas@npm%3A1.2.7#./.yarn/patches/vega-canvas-npm-1.2.7-df0c331091.patch::version=1.2.7&hash=713695&locator=hedgedoc%40workspace%3A." + resolution: "vega-canvas@patch:vega-canvas@npm%3A1.2.7#./.yarn/patches/remove-vega-canvas-node.patch::version=1.2.7&hash=713695&locator=hedgedoc%40workspace%3A." checksum: c933998d0402278195becacf3880958810d77fb0eaa0225d2c4d687845cd9db9b442c8e0ded6219638abb057864dde1dd903884c7e5c1c762f91d9bdbe89f83c languageName: node linkType: hard