Commit 4cba5204 authored by Stefan Probst's avatar Stefan Probst
Browse files

feat: add contribute pages cms

parent 3a9a0821
Pipeline #208656 failed with stages
in 5 minutes and 25 seconds
const createBundleAnalyzerPlugin = require('@next/bundle-analyzer')
const createSvgPlugin = require('@stefanprobst/next-svg')
const createPrevalPlugin = require('next-plugin-preval/config')
const withBundleAnalyzer = createBundleAnalyzerPlugin({
enabled: process.env.ANALYZE === 'true',
......@@ -24,6 +25,7 @@ const withMdx = (nextConfig = {}) => {
},
}
}
const withSvg = createSvgPlugin({
svgo: {
plugins: [
......@@ -37,6 +39,8 @@ const withSvg = createSvgPlugin({
},
})
const withPreval = createPrevalPlugin()
const nextConfig = {
async headers() {
return [
......@@ -56,6 +60,6 @@ const nextConfig = {
reactStrictMode: true,
}
const plugins = [withBundleAnalyzer, withMdx, withSvg]
const plugins = [withBundleAnalyzer, withMdx, withSvg, withPreval]
module.exports = plugins.reduce((acc, plugin) => plugin(acc), nextConfig)
......@@ -100,6 +100,22 @@ collections:
- name: body
label: Text
widget: markdown
- name: contribute-pages
label: Contribute pages
label_singular: Contribute page
editor:
preview: false
format: frontmatter
extension: mdx
folder: content/contribute-pages
create: true
delete: false
fields:
- name: title
label: Title
- name: body
label: Text
widget: markdown
- name: settings
label: Settings
editor:
......
......@@ -11,10 +11,12 @@ import { useGetItemCategories, useGetLoggedInUser } from '@/api/sshoc'
import type { ItemCategory, ItemSearchQuery } from '@/api/sshoc/types'
import { useAuth } from '@/modules/auth/AuthContext'
import ProtectedView from '@/modules/auth/ProtectedView'
import FullWidth from '@/modules/layout/FullWidth'
import GridLayout from '@/modules/layout/GridLayout'
import HStack from '@/modules/layout/HStack'
import VStack from '@/modules/layout/VStack'
import styles from '@/modules/page/PageHeader.module.css'
import contributeLinks from '@/utils/contributePages.preval'
import { createUrlFromPath } from '@/utils/createUrlFromPath'
import { getRedirectPath } from '@/utils/getRedirectPath'
import { getScalarQueryParameter } from '@/utils/getScalarQueryParameter'
......@@ -23,8 +25,6 @@ import type { UrlObject } from '@/utils/useActiveLink'
import { useActiveLink } from '@/utils/useActiveLink'
import { Svg as Logo } from '@@/assets/images/logo-with-text.svg'
import FullWidth from '../layout/FullWidth'
/**
* Page header.
*/
......@@ -117,6 +117,38 @@ function MainNavigation(): JSX.Element {
)}
</Menu>
</li>
{contributeLinks.length > 0 ? (
<Fragment>
<Separator />
<li className="relative z-10 inline-flex">
<Menu>
{({ open }) => (
<Fragment>
<MenuButton isOpen={open}>Contribute</MenuButton>
<FadeIn show={open}>
<MenuPopover static>
{contributeLinks.map(({ label, pathname }) => {
return (
<Menu.Item key={pathname}>
{({ active }) => (
<MenuLink
href={{ pathname }}
highlighted={active}
>
{label}
</MenuLink>
)}
</Menu.Item>
)
})}
</MenuPopover>
</FadeIn>
</Fragment>
)}
</Menu>
</li>
</Fragment>
) : null}
<Separator />
<li className="relative z-10 inline-flex">
<Menu>
......
import { promises as fs } from 'fs'
import matter from 'gray-matter'
import type {
GetStaticPathsResult,
GetStaticPropsContext,
GetStaticPropsResult,
} from 'next'
import * as path from 'path'
import type { ParsedUrlQuery } from 'querystring'
import toHtml from 'rehype-stringify'
import fromMarkdown from 'remark-parse'
import toHast from 'remark-rehype'
import unified from 'unified'
import { getLastUpdatedTimestamp } from '@/api/git'
import ContributeScreen from '@/screens/contribute/ContributeScreen'
interface PageParams extends ParsedUrlQuery {
id: string
}
export interface PageProps {
lastUpdatedAt: string
html: string
metadata: { title: string }
id: string
pages: Array<{ pathname: string; label: string }>
}
export default function ContributePage(props: PageProps): JSX.Element {
return <ContributeScreen {...props} />
}
const extension = '.mdx'
const folder = path.join(process.cwd(), 'content', 'contribute-pages')
const processor = unified().use(fromMarkdown).use(toHast).use(toHtml)
export async function getStaticPaths(): Promise<
GetStaticPathsResult<PageParams>
> {
const ids = await getContributePageIds()
const paths = ids.map((id) => {
return { params: { id } }
})
return {
paths,
fallback: false,
}
}
export async function getStaticProps(
context: GetStaticPropsContext<PageParams>,
): Promise<GetStaticPropsResult<PageProps>> {
const params = context.params as PageParams
const id = params.id
let lastUpdatedAt
try {
lastUpdatedAt = (await getLastUpdatedTimestamp(id)).toISOString()
} catch {
lastUpdatedAt = new Date().toISOString()
}
const { metadata, html } = await getContributePageById(id)
const pages = await getContributePageRoutes()
return {
props: {
lastUpdatedAt,
html,
metadata,
id,
pages,
},
}
}
async function getContributePageIds() {
const ids = (await fs.readdir(folder, 'utf-8')).map((fileName) => {
return fileName.slice(0, -extension.length)
})
return ids
}
async function getContributePageById(id: string) {
const { data, content } = await getContributePageMetadataById(id)
const html = String(await processor.process(content))
return {
metadata: data as { title: string },
html,
}
}
async function getContributePageMetadataById(id: string) {
const filePath = path.join(folder, id + extension)
return matter(await fs.readFile(filePath, 'utf-8'))
}
async function getContributePageRoutes() {
const ids = await getContributePageIds()
return Promise.all(
ids.map(async (id) => {
const { data } = await getContributePageMetadataById(id)
return { pathname: `/contribute/${id}`, label: data.title }
}),
)
}
.grid {
grid-template-rows: auto auto 1fr;
}
.leftBleed {
grid-column: 1 / span 2;
}
.rightBleed {
grid-column: -3 / span 2;
}
.mainColumn {
grid-column: -10 / span 7;
grid-row: 2 / span 2;
}
.sideColumn {
grid-column: 3 / span 3;
}
import cx from 'clsx'
import Link from 'next/link'
import type { PropsWithChildren } from 'react'
import { Fragment } from 'react'
import GridLayout from '@/modules/layout/GridLayout'
import Mdx from '@/modules/markdown/Mdx'
import Metadata from '@/modules/metadata/Metadata'
import Breadcrumbs from '@/modules/ui/Breadcrumbs'
import Header from '@/modules/ui/Header'
import LastUpdatedAt from '@/modules/ui/LastUpdatedAt'
import Triangle from '@/modules/ui/Triangle'
import { Title } from '@/modules/ui/typography/Title'
import styles from '@/screens/contribute/ContributeLayout.module.css'
import type { UrlObject } from '@/utils/useActiveLink'
import { useActiveLink } from '@/utils/useActiveLink'
/**
* Shared contribute screen layout.
*/
export default function ContributeLayout({
breadcrumb,
title,
lastUpdatedAt,
children,
links,
}: PropsWithChildren<{
title: string
breadcrumb: { pathname: string; label: string }
lastUpdatedAt: string
links: Array<{ label: string; pathname: string }>
}>): JSX.Element {
return (
<Fragment>
<Metadata title={title} />
<GridLayout className={styles.grid}>
<Header image={'/assets/images/contribute/clouds@2x.png'}>
<Breadcrumbs links={[{ pathname: '/', label: 'Home' }, breadcrumb]} />
</Header>
<b
className={cx(
'border-b bg-gray-50 border-gray-200',
styles.leftBleed,
)}
/>
<div
className={cx(
'border-b bg-gray-50 border-gray-200',
styles.sideColumn,
)}
>
<p className="p-6 text-lg font-bold">Find out more</p>
</div>
<SideColumn>
<nav aria-label="Page navigation">
<ul className="pl-6">
{links.map(({ label, pathname }) => {
return (
<li key={pathname}>
<NavLink href={{ pathname }}>{label}</NavLink>
</li>
)
})}
</ul>
</nav>
</SideColumn>
<MainColumn>
<div className="mx-auto space-y-6 max-w-80ch">
<Title>{title}</Title>
<Mdx>{children}</Mdx>
<LastUpdatedAt date={lastUpdatedAt} />
</div>
</MainColumn>
</GridLayout>
</Fragment>
)
}
/**
* Main column layout.
*/
function MainColumn({ children }: PropsWithChildren<unknown>) {
const classNames = {
section: cx('px-6 pb-12', styles.mainColumn),
bleed: cx(styles.rightBleed),
}
return (
<Fragment>
<section className={classNames.section}>{children}</section>
<b className={classNames.bleed} />
</Fragment>
)
}
/**
* Side column layout.
*/
function SideColumn({ children }: PropsWithChildren<unknown>) {
const classNames = {
section: cx('bg-gray-50 pb-12', styles.sideColumn),
bleed: cx('bg-gray-50', styles.leftBleed),
}
return (
<Fragment>
<b className={classNames.bleed} />
<section className={classNames.section}>{children}</section>
</Fragment>
)
}
function NavLink({ href, children }: PropsWithChildren<{ href: UrlObject }>) {
const isActive = useActiveLink(href)
const classNames = cx(
'px-8 py-10 inline-flex border-l-4 w-full justify-between',
isActive
? 'border-gray-800 bg-gray-200'
: 'text-primary-800 border-gray-200 hover:bg-gray-100',
)
return (
<Link href={href}>
<a className={classNames}>
<span>{children}</span>
<span className="transform -rotate-90">
<Triangle />
</span>
</a>
</Link>
)
}
import type { PageProps } from '@/pages/contribute/[id]'
import ContributeLayout from '@/screens/contribute/ContributeLayout'
/**
* Contribute screen.
*/
export default function ContributeScreen({
lastUpdatedAt,
metadata,
html,
id,
pages,
}: PageProps): JSX.Element {
return (
<ContributeLayout
breadcrumb={{ pathname: `/contribute/${id}`, label: metadata.title }}
title={metadata.title}
lastUpdatedAt={lastUpdatedAt}
links={pages}
>
<div dangerouslySetInnerHTML={{ __html: html }} />
</ContributeLayout>
)
}
import { promises as fs } from 'fs'
import matter from 'gray-matter'
import preval from 'next-plugin-preval'
import * as path from 'path'
const extension = '.mdx'
const folder = path.join(process.cwd(), 'content', 'contribute-pages')
/**
* There is currently not good mechanism in Next.js to statically provider
* async data to all pages (e.g. via getStaticProps in pages/_app.tsx).
* So we preval at build time to generate json data which can be imported.
*/
async function getContributePageRoutes() {
const ids = await getContributePageIds()
return Promise.all(
ids.map(async (id) => {
const { data } = await getContributePageMetadataById(id)
return { pathname: `/contribute/${id}`, label: data.title }
}),
)
}
async function getContributePageIds() {
const ids = (await fs.readdir(folder, 'utf-8')).map((fileName) => {
return fileName.slice(0, -extension.length)
})
return ids
}
async function getContributePageMetadataById(id: string) {
const filePath = path.join(folder, id + extension)
return matter(await fs.readFile(filePath, 'utf-8'))
}
export default preval(getContributePageRoutes())
This diff is collapsed.
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment