Scaffold basic workshop frontend.
This commit is contained in:
24
pages/404.js
Normal file
24
pages/404.js
Normal 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
31
pages/_app.js
Normal 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
38
pages/_document.js
Normal 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
21
pages/about.js
Normal 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
91
pages/index.js
Normal 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 →
|
||||
</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 →
|
||||
</Link>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
35
pages/projects.js
Normal file
35
pages/projects.js
Normal 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
44
pages/tags.js
Normal 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
53
pages/tags/[tag].js
Normal 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
31
pages/workshop.js
Normal 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"
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
70
pages/workshop/[...slug].js
Normal file
70
pages/workshop/[...slug].js
Normal 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>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
56
pages/workshop/page/[page].js
Normal file
56
pages/workshop/page/[page].js
Normal 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"
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user