Scaffold basic workshop frontend.

This commit is contained in:
2023-12-04 14:02:37 +13:00
parent f7c9392f1c
commit 89ea336647
80 changed files with 27173 additions and 0 deletions

24
pages/404.js Normal file
View File

@ -0,0 +1,24 @@
import Link from '@/components/Link'
export default function FourZeroFour() {
return (
<div className="flex flex-col items-start justify-start md:justify-center md:items-center md:flex-row md:space-x-6 md:mt-24">
<div className="pt-6 pb-8 space-x-2 md:space-y-5">
<h1 className="text-6xl font-extrabold leading-9 tracking-tight text-gray-900 dark:text-gray-100 md:text-8xl md:leading-14 md:border-r-2 md:px-6">
404
</h1>
</div>
<div className="max-w-md">
<p className="mb-4 text-xl font-bold leading-normal md:text-2xl">
Sorry we couldn't find this page.
</p>
<p className="mb-8">But dont worry, you can find plenty of other things on our homepage.</p>
<Link href="/">
<button className="inline px-4 py-2 text-sm font-medium leading-5 text-white transition-colors duration-150 bg-blue-600 border border-transparent rounded-lg shadow focus:outline-none focus:shadow-outline-blue hover:bg-blue-700 dark:hover:bg-blue-500">
Back to homepage
</button>
</Link>
</div>
</div>
)
}

31
pages/_app.js Normal file
View File

@ -0,0 +1,31 @@
import '@/css/tailwind.css'
import '@/css/prism.css'
import 'katex/dist/katex.css'
import '@fontsource/inter/variable-full.css'
import { ThemeProvider } from 'next-themes'
import Head from 'next/head'
import siteMetadata from '@/data/siteMetadata'
import Analytics from '@/components/analytics'
import LayoutWrapper from '@/components/LayoutWrapper'
import { ClientReload } from '@/components/ClientReload'
const isDevelopment = process.env.NODE_ENV === 'development'
const isSocket = process.env.SOCKET
export default function App({ Component, pageProps }) {
return (
<ThemeProvider attribute="class" defaultTheme={siteMetadata.theme}>
<Head>
<meta content="width=device-width, initial-scale=1" name="viewport" />
</Head>
{isDevelopment && isSocket && <ClientReload />}
<Analytics />
<LayoutWrapper>
<Component {...pageProps} />
</LayoutWrapper>
</ThemeProvider>
)
}

38
pages/_document.js Normal file
View File

@ -0,0 +1,38 @@
import Document, { Html, Head, Main, NextScript } from 'next/document'
class MyDocument extends Document {
render() {
return (
<Html lang="en">
<Head>
<link rel="apple-touch-icon" sizes="180x180" href="/static/favicons/apple-touch-icon.png?v=2" />
<link rel="icon" type="image/png" sizes="32x32" href="/static/favicons/favicon-32x32.png?v=2" />
<link rel="icon" type="image/png" sizes="16x16" href="/static/favicons/favicon-16x16.png?v=2" />
<link rel="manifest" href="/static/favicons/site.webmanifest?v=2" />
<link rel="mask-icon" href="/static/favicons/safari-pinned-tab.svg?v=2" color="#5bbad5" />
<link rel="shortcut icon" href="/static/favicons/favicon.ico?v=2" />
<meta name="msapplication-TileColor" content="#2b5797" />
<meta name="msapplication-config" content="/static/favicons/browserconfig.xml?v=2" />
<meta name="theme-color" content="#ffffff" />
<link rel="alternate" type="application/rss+xml" href="/feed.xml" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossOrigin="anonymous" />
<link
href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&display=swap"
rel="stylesheet"
/>
<link
rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/katex@0.13.11/dist/katex.min.css"
integrity="sha384-Um5gpz1odJg5Z4HAmzPtgZKdTBHZdw8S29IecapCSB31ligYPhHQZMIlWLYQGVoc"
crossOrigin="anonymous"
/>
</Head>
<body className="antialiased text-black bg-white dark:bg-gray-900 dark:text-white">
<Main />
<NextScript />
</body>
</Html>
)
}
}
export default MyDocument

21
pages/about.js Normal file
View File

@ -0,0 +1,21 @@
import { MDXLayoutRenderer } from '@/components/MDXComponents'
import { getFileBySlug } from '@/lib/mdx'
const DEFAULT_LAYOUT = 'AuthorLayout'
export async function getStaticProps() {
const authorDetails = await getFileBySlug('authors', ['default'])
return { props: { authorDetails } }
}
export default function About({ authorDetails }) {
const { mdxSource, frontMatter } = authorDetails
return (
<MDXLayoutRenderer
layout={frontMatter.layout || DEFAULT_LAYOUT}
mdxSource={mdxSource}
frontMatter={frontMatter}
/>
)
}

91
pages/index.js Normal file
View File

@ -0,0 +1,91 @@
import Link from '@/components/Link'
import { PageSEO } from '@/components/SEO'
import Tag from '@/components/Tag'
import siteMetadata from '@/data/siteMetadata'
import { getAllFilesFrontMatter } from '@/lib/mdx'
import formatDate from '@/lib/utils/formatDate'
const MAX_DISPLAY = 5
export async function getStaticProps() {
const posts = await getAllFilesFrontMatter('workshop')
return { props: { posts } }
}
export default function Home({ posts }) {
return (
<>
<PageSEO title={siteMetadata.title} description={siteMetadata.description} />
<div className="divide-y divide-gray-200 dark:divide-gray-700">
<div className="pt-6 pb-8 space-y-2 md:space-y-5">
<p className="text-lg leading-7 text-gray-500 dark:text-gray-400">
{siteMetadata.description}
</p>
</div>
<ul className="divide-y divide-gray-200 dark:divide-gray-700">
{!posts.length && 'No posts found.'}
{posts.slice(0, MAX_DISPLAY).map((frontMatter) => {
const { slug, date, title, summary, tags, exercise } = frontMatter
return (
<li key={slug} className="py-12">
<article>
<div className="space-y-2 xl:grid xl:grid-cols-4 xl:space-y-0 xl:items-baseline">
<dl>
<dt className="sr-only">Published on</dt>
<dd className="text-base font-medium leading-6 text-gray-500 dark:text-gray-400">
<div>Exercise {exercise}</div>
</dd>
</dl>
<div className="space-y-5 xl:col-span-3">
<div className="space-y-6">
<div>
<h2 className="text-2xl font-bold leading-8 tracking-tight">
<Link
href={`/workshop/${slug}`}
className="text-gray-900 dark:text-gray-100"
>
{title}
</Link>
</h2>
<div className="flex flex-wrap">
{tags.map((tag) => (
<Tag key={tag} text={tag} />
))}
</div>
</div>
<div className="prose text-gray-500 max-w-none dark:text-gray-400">
{summary}
</div>
</div>
<div className="text-base font-medium leading-6">
<Link
href={`/workshop/${slug}`}
className="text-primary-800 dark:text-primary-700 hover:text-primary-900 dark:hover:text-primary-400"
aria-label={`Read "${title}"`}
>
Read more &rarr;
</Link>
</div>
</div>
</div>
</article>
</li>
)
})}
</ul>
</div>
{posts.length > MAX_DISPLAY && (
<div className="flex justify-end text-base font-medium leading-6">
<Link
href="/workshop"
className="text-primary-800 dark:text-primary-700 hover:text-primary-900 dark:hover:text-primary-400"
aria-label="all posts"
>
All Posts &rarr;
</Link>
</div>
)}
</>
)
}

35
pages/projects.js Normal file
View File

@ -0,0 +1,35 @@
import siteMetadata from '@/data/siteMetadata'
import projectsData from '@/data/projectsData'
import Card from '@/components/Card'
import { PageSEO } from '@/components/SEO'
export default function Projects() {
return (
<>
<PageSEO title={`Projects - ${siteMetadata.author}`} description={siteMetadata.description} />
<div className="divide-y divide-gray-200 dark:divide-gray-700">
<div className="pt-6 pb-8 space-y-2 md:space-y-5">
<h1 className="text-3xl font-extrabold leading-9 tracking-tight text-gray-900 dark:text-gray-100 sm:text-4xl sm:leading-10 md:text-6xl md:leading-14">
Projects
</h1>
<p className="text-lg leading-7 text-gray-500 dark:text-gray-400">
Showcase your projects with a hero image (16 x 9)
</p>
</div>
<div className="container py-12">
<div className="flex flex-wrap -m-4">
{projectsData.map((d) => (
<Card
key={d.title}
title={d.title}
description={d.description}
imgSrc={d.imgSrc}
href={d.href}
/>
))}
</div>
</div>
</div>
</>
)
}

44
pages/tags.js Normal file
View File

@ -0,0 +1,44 @@
import Link from '@/components/Link'
import { PageSEO } from '@/components/SEO'
import Tag from '@/components/Tag'
import siteMetadata from '@/data/siteMetadata'
import { getAllTags } from '@/lib/tags'
import kebabCase from '@/lib/utils/kebabCase'
export async function getStaticProps() {
const tags = await getAllTags('workshop')
return { props: { tags } }
}
export default function Tags({ tags }) {
const sortedTags = Object.keys(tags).sort((a, b) => tags[b] - tags[a])
return (
<>
<PageSEO title={`Tags - ${siteMetadata.author}`} description="Things I write about" />
<div className="flex flex-col items-start justify-start divide-y divide-gray-200 dark:divide-gray-700 md:justify-center md:items-center md:divide-y-0 md:flex-row md:space-x-6 md:mt-24">
<div className="pt-6 pb-8 space-x-2 md:space-y-5">
<h1 className="text-3xl font-extrabold leading-9 tracking-tight text-gray-900 dark:text-gray-100 sm:text-4xl sm:leading-10 md:text-6xl md:leading-14 md:border-r-2 md:px-6">
Tags
</h1>
</div>
<div className="flex flex-wrap max-w-lg">
{Object.keys(tags).length === 0 && 'No tags found.'}
{sortedTags.map((t) => {
return (
<div key={t} className="mt-2 mb-2 mr-5">
<Tag text={t} />
<Link
href={`/tags/${kebabCase(t)}`}
className="-ml-2 text-sm font-semibold text-gray-600 uppercase dark:text-gray-300"
>
{` (${tags[t]})`}
</Link>
</div>
)
})}
</div>
</div>
</>
)
}

53
pages/tags/[tag].js Normal file
View File

@ -0,0 +1,53 @@
import { TagSEO } from '@/components/SEO'
import siteMetadata from '@/data/siteMetadata'
import ListLayout from '@/layouts/ListLayout'
import generateRss from '@/lib/generate-rss'
import { getAllFilesFrontMatter } from '@/lib/mdx'
import { getAllTags } from '@/lib/tags'
import kebabCase from '@/lib/utils/kebabCase'
import fs from 'fs'
import path from 'path'
const root = process.cwd()
export async function getStaticPaths() {
const tags = await getAllTags('workshop')
return {
paths: Object.keys(tags).map((tag) => ({
params: {
tag,
},
})),
fallback: false,
}
}
export async function getStaticProps({ params }) {
const allPosts = await getAllFilesFrontMatter('workshop')
const filteredPosts = allPosts.filter(
(post) => post.draft !== true && post.tags.map((t) => kebabCase(t)).includes(params.tag)
)
// rss
const rss = generateRss(filteredPosts, `tags/${params.tag}/feed.xml`)
const rssPath = path.join(root, 'public', 'tags', params.tag)
fs.mkdirSync(rssPath, { recursive: true })
fs.writeFileSync(path.join(rssPath, 'feed.xml'), rss)
return { props: { posts: filteredPosts, tag: params.tag } }
}
export default function Tag({ posts, tag }) {
// Capitalize first letter and convert space to dash
const title = tag[0].toUpperCase() + tag.split(' ').join('-').slice(1)
return (
<>
<TagSEO
title={`${tag} - ${siteMetadata.author}`}
description={`${tag} tags - ${siteMetadata.author}`}
/>
<ListLayout posts={posts} title={title} />
</>
)
}

31
pages/workshop.js Normal file
View File

@ -0,0 +1,31 @@
import { getAllFilesFrontMatter } from '@/lib/mdx'
import siteMetadata from '@/data/siteMetadata'
import ListLayout from '@/layouts/ListLayout'
import { PageSEO } from '@/components/SEO'
export const POSTS_PER_PAGE = 5
export async function getStaticProps() {
const posts = await getAllFilesFrontMatter('workshop')
const initialDisplayPosts = posts.slice(0, POSTS_PER_PAGE)
const pagination = {
currentPage: 1,
totalPages: Math.ceil(posts.length / POSTS_PER_PAGE),
}
return { props: { initialDisplayPosts, posts, pagination } }
}
export default function Workshop({ posts, initialDisplayPosts, pagination }) {
return (
<>
<PageSEO title={`${siteMetadata.description}`}/>
<ListLayout
posts={posts}
initialDisplayPosts={initialDisplayPosts}
pagination={pagination}
title="All exercises"
/>
</>
)
}

View File

@ -0,0 +1,70 @@
import fs from 'fs'
import PageTitle from '@/components/PageTitle'
import generateRss from '@/lib/generate-rss'
import { MDXLayoutRenderer } from '@/components/MDXComponents'
import { formatSlug, getAllFilesFrontMatter, getFileBySlug, getFiles } from '@/lib/mdx'
const DEFAULT_LAYOUT = 'PostLayout'
export async function getStaticPaths() {
const posts = getFiles('workshop')
return {
paths: posts.map((p) => ({
params: {
slug: formatSlug(p).split('/'),
},
})),
fallback: false,
}
}
export async function getStaticProps({ params }) {
const allPosts = await getAllFilesFrontMatter('workshop')
const postIndex = allPosts.findIndex((post) => formatSlug(post.slug) === params.slug.join('/'))
const prev = allPosts[postIndex - 1] || null
const next = allPosts[postIndex + 1] || null
const post = await getFileBySlug('workshop', params.slug.join('/'))
const authorList = post.frontMatter.authors || ['default']
const authorPromise = authorList.map(async (author) => {
const authorResults = await getFileBySlug('authors', [author])
return authorResults.frontMatter
})
const authorDetails = await Promise.all(authorPromise)
// rss
if (allPosts.length > 0) {
const rss = generateRss(allPosts)
fs.writeFileSync('./public/feed.xml', rss)
}
return { props: { post, authorDetails, prev, next } }
}
export default function Workshop({ post, authorDetails, prev, next }) {
const { mdxSource, toc, frontMatter } = post
return (
<>
{frontMatter.draft !== true ? (
<MDXLayoutRenderer
layout={frontMatter.layout || DEFAULT_LAYOUT}
toc={toc}
mdxSource={mdxSource}
frontMatter={frontMatter}
authorDetails={authorDetails}
prev={prev}
next={next}
/>
) : (
<div className="mt-24 text-center">
<PageTitle>
Under Construction{' '}
<span role="img" aria-label="roadwork sign">
🚧
</span>
</PageTitle>
</div>
)}
</>
)
}

View File

@ -0,0 +1,56 @@
import { PageSEO } from '@/components/SEO'
import siteMetadata from '@/data/siteMetadata'
import { getAllFilesFrontMatter } from '@/lib/mdx'
import ListLayout from '@/layouts/ListLayout'
import { POSTS_PER_PAGE } from '..'
export async function getStaticPaths() {
const totalPosts = await getAllFilesFrontMatter('workshop')
const totalPages = Math.ceil(totalPosts.length / POSTS_PER_PAGE)
const paths = Array.from({ length: totalPages }, (_, i) => ({
params: { page: (i + 1).toString() },
}))
return {
paths,
fallback: false,
}
}
export async function getStaticProps(context) {
const {
params: { page },
} = context
const posts = await getAllFilesFrontMatter('workshop')
const pageNumber = parseInt(page)
const initialDisplayPosts = posts.slice(
POSTS_PER_PAGE * (pageNumber - 1),
POSTS_PER_PAGE * pageNumber
)
const pagination = {
currentPage: pageNumber,
totalPages: Math.ceil(posts.length / POSTS_PER_PAGE),
}
return {
props: {
posts,
initialDisplayPosts,
pagination,
},
}
}
export default function PostPage({ posts, initialDisplayPosts, pagination }) {
return (
<>
<PageSEO title={siteMetadata.title} description={siteMetadata.description} />
<ListLayout
posts={posts}
initialDisplayPosts={initialDisplayPosts}
pagination={pagination}
title="All workshops"
/>
</>
)
}