From 0e34c746a49fd9aca96a5f3cd1964dd0eb4f5538 Mon Sep 17 00:00:00 2001 From: Peter Beverloo Date: Sat, 2 Mar 2024 18:18:02 +0000 Subject: [PATCH] Migrate the content system to use
& friends --- app/admin/components/RemoteDataTable.tsx | 4 +- app/admin/components/Section.tsx | 13 +- app/admin/content/ContentEditor.tsx | 128 ++++++++++-------- app/admin/content/[id]/page.tsx | 13 +- app/admin/content/page.tsx | 24 ++-- .../[slug]/[team]/content/[id]/page.tsx | 17 +-- .../events/[slug]/[team]/content/page.tsx | 27 ++-- 7 files changed, 106 insertions(+), 120 deletions(-) diff --git a/app/admin/components/RemoteDataTable.tsx b/app/admin/components/RemoteDataTable.tsx index d0359508..7efad351 100644 --- a/app/admin/components/RemoteDataTable.tsx +++ b/app/admin/components/RemoteDataTable.tsx @@ -393,8 +393,8 @@ export function RemoteDataTable< return ( <> - - + + {error} diff --git a/app/admin/components/Section.tsx b/app/admin/components/Section.tsx index aa242fc4..7aed5b16 100644 --- a/app/admin/components/Section.tsx +++ b/app/admin/components/Section.tsx @@ -7,12 +7,19 @@ import Stack from '@mui/material/Stack'; import { SectionHeader, type SectionHeaderProps } from './SectionHeader'; /** - * Props accepted by the
component. + * Props accepted by the
component, that are directly owned by the component. The `SectionOwnProps` are included, and either a valid + * header or an explicit, boolean indication that no header should be included. + */ +export type SectionProps = SectionOwnProps & (SectionHeaderProps | { noHeader: true }); + /** * The
component represents a visually separated section of a page in the administration * area. The component is designed to be compatible with server-side rendering once the MUI library @@ -30,7 +37,7 @@ export function Section(props: React.PropsWithChildren) { return ( - + { !('noHeader' in sectionHeaderProps) && } {children} ); diff --git a/app/admin/content/ContentEditor.tsx b/app/admin/content/ContentEditor.tsx index b886259e..8b07f8f6 100644 --- a/app/admin/content/ContentEditor.tsx +++ b/app/admin/content/ContentEditor.tsx @@ -9,6 +9,7 @@ import { useCallback, useEffect, useRef, useState } from 'react'; import { type FieldValues, FormContainer, TextFieldElement } from 'react-hook-form-mui'; import Alert from '@mui/material/Alert'; +import Box from '@mui/material/Box'; import Collapse from '@mui/material/Collapse'; import Grid from '@mui/material/Unstable_Grid2'; import LoadingButton from '@mui/lab/LoadingButton'; @@ -19,6 +20,8 @@ import Typography from '@mui/material/Typography'; import type { MDXEditorMethods } from '@mdxeditor/editor'; import type { ContentRowModel, ContentScope } from '@app/api/admin/content/[[...id]]/route'; +import { Section } from '../components/Section'; +import { SectionHeader, type SectionHeaderProps } from '../components/SectionHeader'; import { Temporal, formatDate } from '@lib/Temporal'; import { callApi } from '@lib/callApi'; import { validateContentPath } from './ContentCreate'; @@ -34,7 +37,7 @@ const ContentEditorMdx = dynamic(() => import('./ContentEditorMdx'), { ssr: fals /** * Props accepted by the component. */ -export interface ContentEditorProps { +export interface ContentEditorProps extends SectionHeaderProps { /** * Unique ID of the content that should be loaded. Will automatically be fetched from the API. */ @@ -63,6 +66,8 @@ export interface ContentEditorProps { * to the bundle size, while still being available when it needs to be. */ export function ContentEditor(props: React.PropsWithChildren) { + const { children, contentId, pathHidden, pathPrefix, scope, ...sectionHeaderProps } = props; + const ref = useRef(null); const [ defaultValues, setDefaultValues ] = useState(); @@ -80,10 +85,10 @@ export function ContentEditor(props: React.PropsWithChildren throw new Error('Cannot locate the Markdown content on this page'); const response = await callApi('put', '/api/admin/content/:id', { - id: props.contentId, - context: props.scope, + id: contentId, + context: scope, row: { - id: props.contentId, + id: contentId, content: ref.current.getMarkdown(), path: data.path ?? defaultValues?.path, title: data.title, @@ -103,15 +108,15 @@ export function ContentEditor(props: React.PropsWithChildren } finally { setLoading(false); } - }, [ defaultValues, props.contentId, props.scope, ref ]); + }, [ defaultValues, contentId, scope, ref ]); const [ contentProtected, setContentProtected ] = useState(false); const [ markdown, setMarkdown ] = useState(); useEffect(() => { callApi('get', '/api/admin/content/:id', { - id: props.contentId, - context: props.scope + id: contentId, + context: scope }).then(response => { if (response.success) { setDefaultValues({ @@ -128,71 +133,76 @@ export function ContentEditor(props: React.PropsWithChildren setError(response.error ?? 'Unable to load the content from the server'); } }); - }, [ props.contentId, props.scope ]); + }, [ contentId, scope ]); if (!defaultValues) { return ( - - {props.children} - +
+ {children} + {error} - - - - - - + + + + + + + +
); } return ( - - {props.children} - - - - - { !props.pathHidden && + +
+ {children} + - - { props.pathPrefix && - - {props.pathPrefix} - } - - - } - - - - - - - - - Save changes - - { error && - - {error} - } - { success && - - {success} - } - - + + + { !pathHidden && + + + { pathPrefix && + + {pathPrefix} + } + + + } + +
+
+ +
+
+ + + Save changes + + { error && + + {error} + } + { success && + + {success} + } + +
+
); } diff --git a/app/admin/content/[id]/page.tsx b/app/admin/content/[id]/page.tsx index ff2f62aa..a2ad5b6d 100644 --- a/app/admin/content/[id]/page.tsx +++ b/app/admin/content/[id]/page.tsx @@ -3,12 +3,10 @@ import type { Metadata } from 'next'; -import Alert from '@mui/material/Alert'; -import Typography from '@mui/material/Typography'; - import type { NextRouterParams } from '@lib/NextRouterParams'; import { ContentEditor } from '@app/admin/content/ContentEditor'; import { Privilege } from '@lib/auth/Privileges'; +import { SectionIntroduction } from '@app/admin/components/SectionIntroduction'; import { createGlobalScope } from '@app/admin/content/ContentScope'; import { requireAuthenticationContext } from '@lib/auth/AuthenticationContext'; @@ -25,14 +23,11 @@ export default async function ContentEntryPage(props: NextRouterParams<'id'>) { const scope = createGlobalScope(); return ( - - - Page editor - - + + You are updating global content, any changes you save will be published immediately. - + ); } diff --git a/app/admin/content/page.tsx b/app/admin/content/page.tsx index b4515d86..81a66a4d 100644 --- a/app/admin/content/page.tsx +++ b/app/admin/content/page.tsx @@ -3,13 +3,11 @@ import type { Metadata } from 'next'; -import Alert from '@mui/material/Alert'; -import Paper from '@mui/material/Paper'; -import Typography from '@mui/material/Typography'; - import { Privilege, can } from '@lib/auth/Privileges'; import { ContentCreate } from './ContentCreate'; import { ContentList } from './ContentList'; +import { Section } from '@app/admin/components/Section'; +import { SectionIntroduction } from '../components/SectionIntroduction'; import { createGlobalScope } from './ContentScope'; import { requireAuthenticationContext } from '@lib/auth/AuthenticationContext'; @@ -28,22 +26,16 @@ export default async function ContentPage() { return ( <> - - - Pages - +
- - - - Create a new page - - +
+
+ You can create new global content. These pages will however not automatically be published, and rely on code changes. - + - +
); } diff --git a/app/admin/events/[slug]/[team]/content/[id]/page.tsx b/app/admin/events/[slug]/[team]/content/[id]/page.tsx index 62abd6ba..2aa81f35 100644 --- a/app/admin/events/[slug]/[team]/content/[id]/page.tsx +++ b/app/admin/events/[slug]/[team]/content/[id]/page.tsx @@ -1,11 +1,9 @@ // Copyright 2023 Peter Beverloo & AnimeCon. All rights reserved. // Use of this source code is governed by a MIT license that can be found in the LICENSE file. -import Alert from '@mui/material/Alert'; -import Typography from '@mui/material/Typography'; - import type { NextRouterParams } from '@lib/NextRouterParams'; import { ContentEditor } from '@app/admin/content/ContentEditor'; +import { SectionIntroduction } from '@app/admin/components/SectionIntroduction'; import { createEventScope } from '@app/admin/content/ContentScope'; import { generateEventMetadataFn } from '../../../generateEventMetadataFn'; import { verifyAccessAndFetchPageInfo } from '@app/admin/events/verifyAccessAndFetchPageInfo'; @@ -22,17 +20,12 @@ export default async function EventContentEntryPage(props: NextRouterParams<'slu const scope = createEventScope(event.id, team.id); return ( - - - Page editor - - ({team.slug} for {event.shortName}) - - - + + You are editing content on {team.slug}, any changes that you save will be published immediately. - + ); } diff --git a/app/admin/events/[slug]/[team]/content/page.tsx b/app/admin/events/[slug]/[team]/content/page.tsx index 78026f14..57783e94 100644 --- a/app/admin/events/[slug]/[team]/content/page.tsx +++ b/app/admin/events/[slug]/[team]/content/page.tsx @@ -1,14 +1,12 @@ // Copyright 2023 Peter Beverloo & AnimeCon. All rights reserved. // Use of this source code is governed by a MIT license that can be found in the LICENSE file. -import Alert from '@mui/material/Alert'; -import Paper from '@mui/material/Paper'; -import Typography from '@mui/material/Typography'; - import type { NextRouterParams } from '@lib/NextRouterParams'; import { ContentCreate } from '@app/admin/content/ContentCreate'; import { ContentList } from '@app/admin/content/ContentList'; import { Privilege, can } from '@lib/auth/Privileges'; +import { Section } from '@app/admin/components/Section'; +import { SectionIntroduction } from '@app/admin/components/SectionIntroduction'; import { createEventScope } from '@app/admin/content/ContentScope'; import { generateEventMetadataFn } from '../../generateEventMetadataFn'; import { verifyAccessAndFetchPageInfo } from '@app/admin/events/verifyAccessAndFetchPageInfo'; @@ -26,26 +24,17 @@ export default async function EventContentPage(props: NextRouterParams<'slug' | return ( <> - - - Pages - - ({team.slug} for {event.shortName}) - - +
- - - - Create a new page - - +
+
+ You can create a new page for the {team.name}, which will immediately be published on {team.slug}. - + - +
); }