Skip to content

Commit

Permalink
Migrate the content system to use <Section> & friends
Browse files Browse the repository at this point in the history
  • Loading branch information
beverloo committed Mar 2, 2024
1 parent 0b0160d commit 0e34c74
Show file tree
Hide file tree
Showing 7 changed files with 106 additions and 120 deletions.
4 changes: 2 additions & 2 deletions app/admin/components/RemoteDataTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -393,8 +393,8 @@ export function RemoteDataTable<

return (
<>
<Collapse in={!!error}>
<Alert severity="error" sx={{ mb: 2 }}>
<Collapse in={!!error} unmountOnExit>
<Alert severity="error">
{error}
</Alert>
</Collapse>
Expand Down
13 changes: 10 additions & 3 deletions app/admin/components/Section.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,19 @@ import Stack from '@mui/material/Stack';
import { SectionHeader, type SectionHeaderProps } from './SectionHeader';

/**
* Props accepted by the <Section> component.
* Props accepted by the <Section> component, that are directly owned by the <Section< component.
* Other props, e.g. that of the header, will be included.
*/
export interface SectionProps extends SectionHeaderProps {
interface SectionOwnProps {
// TODO: Additional options go here.
}

/**
* Props accepted by the <Section> 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 <Section> 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
Expand All @@ -30,7 +37,7 @@ export function Section(props: React.PropsWithChildren<SectionProps>) {

return (
<Paper component={Stack} direction="column" spacing={2} sx={{ p: 2 }}>
<SectionHeader {...sectionHeaderProps} />
{ !('noHeader' in sectionHeaderProps) && <SectionHeader {...sectionHeaderProps} /> }
{children}
</Paper>
);
Expand Down
128 changes: 69 additions & 59 deletions app/admin/content/ContentEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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';
Expand All @@ -34,7 +37,7 @@ const ContentEditorMdx = dynamic(() => import('./ContentEditorMdx'), { ssr: fals
/**
* Props accepted by the <ContentEditor> 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.
*/
Expand Down Expand Up @@ -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<ContentEditorProps>) {
const { children, contentId, pathHidden, pathPrefix, scope, ...sectionHeaderProps } = props;

const ref = useRef<MDXEditorMethods>(null);

const [ defaultValues, setDefaultValues ] = useState<ContentRowModel>();
Expand All @@ -80,10 +85,10 @@ export function ContentEditor(props: React.PropsWithChildren<ContentEditorProps>
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,
Expand All @@ -103,15 +108,15 @@ export function ContentEditor(props: React.PropsWithChildren<ContentEditorProps>
} finally {
setLoading(false);
}
}, [ defaultValues, props.contentId, props.scope, ref ]);
}, [ defaultValues, contentId, scope, ref ]);

const [ contentProtected, setContentProtected ] = useState<boolean>(false);
const [ markdown, setMarkdown ] = useState<string>();

useEffect(() => {
callApi('get', '/api/admin/content/:id', {
id: props.contentId,
context: props.scope
id: contentId,
context: scope
}).then(response => {
if (response.success) {
setDefaultValues({
Expand All @@ -128,71 +133,76 @@ export function ContentEditor(props: React.PropsWithChildren<ContentEditorProps>
setError(response.error ?? 'Unable to load the content from the server');
}
});
}, [ props.contentId, props.scope ]);
}, [ contentId, scope ]);

if (!defaultValues) {
return (
<Paper sx={{ p: 2 }}>
{props.children}
<Collapse in={!!error}>
<Section {...sectionHeaderProps}>
{children}
<Collapse in={!!error} unmountOnExit>
<Alert severity="error" sx={{ mb: 2 }}>
{error}
</Alert>
</Collapse>
<Skeleton variant="text" animation="wave" width="80%" height={16} />
<Skeleton variant="text" animation="wave" width="60%" height={16} />
<Skeleton variant="text" animation="wave" width="70%" height={16} />
<Skeleton variant="text" animation="wave" width="70%" height={16} />
<Skeleton variant="text" animation="wave" width="40%" height={16} />
</Paper>
<Box>
<Skeleton variant="text" animation="wave" width="80%" height={16} />
<Skeleton variant="text" animation="wave" width="60%" height={16} />
<Skeleton variant="text" animation="wave" width="70%" height={16} />
<Skeleton variant="text" animation="wave" width="70%" height={16} />
<Skeleton variant="text" animation="wave" width="40%" height={16} />
</Box>
</Section>
);
}

return (
<FormContainer defaultValues={defaultValues} onSuccess={handleSave}>
<Paper sx={{ p: 2 }}>
{props.children}
<Grid container spacing={2}>
<Grid xs={12}>
<TextFieldElement name="title" label="Content title" fullWidth size="small"
required />
</Grid>
{ !props.pathHidden &&
<Stack direction="column" spacing={2}>
<Section {...sectionHeaderProps}>
{children}
<Grid container spacing={2} sx={{ margin: '8px -8px -8px -8px !important' }}>
<Grid xs={12}>
<Stack direction="row" spacing={1}>
{ props.pathPrefix &&
<Typography sx={{ pt: '9px' }}>
{props.pathPrefix}
</Typography> }
<TextFieldElement name="path" label="Content path" fullWidth
size="small" required={!contentProtected}
validation={{
validate:
contentProtected ? undefined
: validateContentPath
}} InputProps={{ readOnly: !!contentProtected }}/>
</Stack>
</Grid> }
</Grid>
</Paper>
<Paper sx={{ mt: 2, p: 2, pb: '0.1px' }}>
<ContentEditorMdx innerRef={ref} markdown={markdown} />
</Paper>
<Paper sx={{ mt: 2, p: 2 }}>
<Stack direction="row" spacing={2} alignItems="center">
<LoadingButton loading={!!loading} variant="contained" type="submit">
Save changes
</LoadingButton>
{ error &&
<Typography sx={{ color: 'error.main' }}>
{error}
</Typography> }
{ success &&
<Typography sx={{ color: 'success.main' }}>
{success}
</Typography> }
</Stack>
</Paper>
<TextFieldElement name="title" label="Content title" fullWidth
size="small" required />
</Grid>
{ !pathHidden &&
<Grid xs={12}>
<Stack direction="row" spacing={1}>
{ pathPrefix &&
<Typography sx={{ pt: '9px' }}>
{pathPrefix}
</Typography> }
<TextFieldElement name="path" label="Content path" fullWidth
size="small" required={!contentProtected}
validation={{
validate:
contentProtected ? undefined
: validateContentPath
}} InputProps={{
readOnly: !!contentProtected }} />
</Stack>
</Grid> }
</Grid>
</Section>
<Section noHeader>
<ContentEditorMdx innerRef={ref} markdown={markdown} />
</Section>
<Section noHeader>
<Stack direction="row" spacing={2} alignItems="center">
<LoadingButton loading={!!loading} variant="contained" type="submit">
Save changes
</LoadingButton>
{ error &&
<Typography sx={{ color: 'error.main' }}>
{error}
</Typography> }
{ success &&
<Typography sx={{ color: 'success.main' }}>
{success}
</Typography> }
</Stack>
</Section>
</Stack>
</FormContainer>
);
}
13 changes: 4 additions & 9 deletions app/admin/content/[id]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand All @@ -25,14 +23,11 @@ export default async function ContentEntryPage(props: NextRouterParams<'id'>) {
const scope = createGlobalScope();

return (
<ContentEditor contentId={parseInt(props.params.id)} scope={scope}>
<Typography variant="h5" sx={{ pb: 1 }}>
Page editor
</Typography>
<Alert severity="info" sx={{ mb: 2 }}>
<ContentEditor contentId={parseInt(props.params.id)} scope={scope} title="Page editor">
<SectionIntroduction>
You are updating <strong>global content</strong>, any changes you save will be
published immediately.
</Alert>
</SectionIntroduction>
</ContentEditor>
);
}
Expand Down
24 changes: 8 additions & 16 deletions app/admin/content/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand All @@ -28,22 +26,16 @@ export default async function ContentPage() {

return (
<>
<Paper sx={{ p: 2 }}>
<Typography variant="h5" sx={{ pb: 1 }}>
Pages
</Typography>
<Section title="Pages">
<ContentList enableAuthorLink={enableAuthorLink} scope={scope} />
</Paper>
<Paper sx={{ p: 2 }}>
<Typography variant="h5" sx={{ pb: 1 }}>
Create a new page
</Typography>
<Alert severity="info" sx={{ mb: 2 }}>
</Section>
<Section title="Create a new page">
<SectionIntroduction>
You can create new <strong>global content</strong>. These pages will however not
automatically be published, and rely on code changes.
</Alert>
</SectionIntroduction>
<ContentCreate scope={scope} />
</Paper>
</Section>
</>
);
}
Expand Down
17 changes: 5 additions & 12 deletions app/admin/events/[slug]/[team]/content/[id]/page.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -22,17 +20,12 @@ export default async function EventContentEntryPage(props: NextRouterParams<'slu
const scope = createEventScope(event.id, team.id);

return (
<ContentEditor contentId={parseInt(props.params.id)} pathPrefix={pathPrefix} scope={scope}>
<Typography variant="h5" sx={{ pb: 1 }}>
Page editor
<Typography component="span" variant="h5" color="action.active" sx={{ pl: 1 }}>
({team.slug} for {event.shortName})
</Typography>
</Typography>
<Alert severity="info" sx={{ mb: 2 }}>
<ContentEditor contentId={parseInt(props.params.id)} pathPrefix={pathPrefix} scope={scope}
title="Page editor" subtitle={team.slug}>
<SectionIntroduction>
You are editing content on <strong>{team.slug}</strong>, any changes that you save
will be published immediately.
</Alert>
</SectionIntroduction>
</ContentEditor>
);
}
Expand Down
Loading

0 comments on commit 0e34c74

Please sign in to comment.