From 89ea336647b0bde74ebb9880ff7a91d5d3428fa2 Mon Sep 17 00:00:00 2001 From: James Blair Date: Mon, 4 Dec 2023 14:02:37 +1300 Subject: [PATCH] Scaffold basic workshop frontend. --- .gitignore | 3 + components/Card.js | 51 + components/ClientReload.js | 23 + components/Footer.js | 17 + components/Image.js | 6 + components/LayoutWrapper.js | 54 + components/Link.js | 23 + components/MDXComponents.js | 32 + components/MobileNav.js | 78 + components/PageTitle.js | 7 + components/Pagination.js | 36 + components/Pre.js | 71 + components/Quote.js | 13 + components/SEO.js | 150 + components/ScrollTopAndComment.js | 61 + components/SectionContainer.js | 3 + components/TOCInline.js | 64 + components/Tag.js | 14 + components/ThemeSwitch.js | 38 + components/YoutubeEmbed.js | 22 + components/analytics/GoogleAnalytics.js | 36 + components/analytics/Plausible.js | 27 + components/analytics/SimpleAnalytics.js | 25 + components/analytics/index.js | 18 + components/comments/Disqus.js | 37 + components/comments/Giscus.js | 50 + components/comments/Utterances.js | 45 + components/comments/index.js | 54 + components/social-icons/facebook.svg | 1 + components/social-icons/github.svg | 1 + components/social-icons/index.js | 39 + components/social-icons/linkedin.svg | 1 + components/social-icons/mail.svg | 4 + components/social-icons/twitter.svg | 1 + components/social-icons/youtube.svg | 1 + css/prism.css | 158 + css/tailwind.css | 63 + data/authors/default.md | 8 + data/authors/sparrowhawk.md | 12 + data/blog/.gitignore | 0 data/headerNavLinks.js | 6 + data/logo.svg | 19 + data/projectsData.js | 11 + data/siteMetadata.js | 69 + data/workshop/exercise1.mdx | 189 + jsconfig.json | 12 + layouts/AuthorLayout.js | 41 + layouts/ListLayout.js | 90 + layouts/PostLayout.js | 142 + layouts/PostSimple.js | 71 + lib/generate-rss.js | 32 + lib/mdx.js | 136 + lib/remark-code-title.js | 32 + lib/remark-extract-frontmatter.js | 10 + lib/remark-img-to-jsx.js | 35 + lib/remark-toc-headings.js | 15 + lib/tags.js | 30 + lib/utils/files.js | 20 + lib/utils/formatDate.js | 14 + lib/utils/htmlEscaper.js | 23 + lib/utils/kebabCase.js | 8 + next.config.js | 43 + package-lock.json | 23859 ++++++++++++++++++++++ package.json | 75 + pages/404.js | 24 + pages/_app.js | 31 + pages/_document.js | 38 + pages/about.js | 21 + pages/index.js | 91 + pages/projects.js | 35 + pages/tags.js | 44 + pages/tags/[tag].js | 53 + pages/workshop.js | 31 + pages/workshop/[...slug].js | 70 + pages/workshop/page/[page].js | 56 + postcss.config.js | 6 + prettier.config.js | 9 + scripts/compose.js | 121 + scripts/generate-sitemap.js | 51 + tailwind.config.js | 163 + 80 files changed, 27173 insertions(+) create mode 100644 .gitignore create mode 100644 components/Card.js create mode 100644 components/ClientReload.js create mode 100644 components/Footer.js create mode 100644 components/Image.js create mode 100644 components/LayoutWrapper.js create mode 100644 components/Link.js create mode 100644 components/MDXComponents.js create mode 100644 components/MobileNav.js create mode 100644 components/PageTitle.js create mode 100644 components/Pagination.js create mode 100644 components/Pre.js create mode 100644 components/Quote.js create mode 100644 components/SEO.js create mode 100644 components/ScrollTopAndComment.js create mode 100644 components/SectionContainer.js create mode 100644 components/TOCInline.js create mode 100644 components/Tag.js create mode 100644 components/ThemeSwitch.js create mode 100644 components/YoutubeEmbed.js create mode 100644 components/analytics/GoogleAnalytics.js create mode 100644 components/analytics/Plausible.js create mode 100644 components/analytics/SimpleAnalytics.js create mode 100644 components/analytics/index.js create mode 100644 components/comments/Disqus.js create mode 100644 components/comments/Giscus.js create mode 100644 components/comments/Utterances.js create mode 100644 components/comments/index.js create mode 100644 components/social-icons/facebook.svg create mode 100644 components/social-icons/github.svg create mode 100644 components/social-icons/index.js create mode 100644 components/social-icons/linkedin.svg create mode 100644 components/social-icons/mail.svg create mode 100644 components/social-icons/twitter.svg create mode 100644 components/social-icons/youtube.svg create mode 100644 css/prism.css create mode 100644 css/tailwind.css create mode 100644 data/authors/default.md create mode 100644 data/authors/sparrowhawk.md create mode 100644 data/blog/.gitignore create mode 100644 data/headerNavLinks.js create mode 100644 data/logo.svg create mode 100644 data/projectsData.js create mode 100644 data/siteMetadata.js create mode 100644 data/workshop/exercise1.mdx create mode 100644 jsconfig.json create mode 100644 layouts/AuthorLayout.js create mode 100644 layouts/ListLayout.js create mode 100644 layouts/PostLayout.js create mode 100644 layouts/PostSimple.js create mode 100644 lib/generate-rss.js create mode 100644 lib/mdx.js create mode 100644 lib/remark-code-title.js create mode 100644 lib/remark-extract-frontmatter.js create mode 100644 lib/remark-img-to-jsx.js create mode 100644 lib/remark-toc-headings.js create mode 100644 lib/tags.js create mode 100644 lib/utils/files.js create mode 100644 lib/utils/formatDate.js create mode 100644 lib/utils/htmlEscaper.js create mode 100644 lib/utils/kebabCase.js create mode 100644 next.config.js create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 pages/404.js create mode 100644 pages/_app.js create mode 100644 pages/_document.js create mode 100644 pages/about.js create mode 100644 pages/index.js create mode 100644 pages/projects.js create mode 100644 pages/tags.js create mode 100644 pages/tags/[tag].js create mode 100644 pages/workshop.js create mode 100644 pages/workshop/[...slug].js create mode 100644 pages/workshop/page/[page].js create mode 100644 postcss.config.js create mode 100644 prettier.config.js create mode 100644 scripts/compose.js create mode 100644 scripts/generate-sitemap.js create mode 100644 tailwind.config.js diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..06954be --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +node_modules +public +.next diff --git a/components/Card.js b/components/Card.js new file mode 100644 index 0000000..15fca9f --- /dev/null +++ b/components/Card.js @@ -0,0 +1,51 @@ +import Image from './Image' +import Link from './Link' + +const Card = ({ title, description, imgSrc, href }) => ( +
+
+ {href ? ( + + {title} + + ) : ( + {title} + )} +
+

+ {href ? ( + + {title} + + ) : ( + title + )} +

+

{description}

+ {href && ( + + Learn more → + + )} +
+
+
+) + +export default Card diff --git a/components/ClientReload.js b/components/ClientReload.js new file mode 100644 index 0000000..babfba5 --- /dev/null +++ b/components/ClientReload.js @@ -0,0 +1,23 @@ +import { useEffect } from 'react' +import Router from 'next/router' + +/** + * Client-side complement to next-remote-watch + * Re-triggers getStaticProps when watched mdx files change + * + */ +export const ClientReload = () => { + // Exclude socket.io from prod bundle + useEffect(() => { + import('socket.io-client').then((module) => { + const socket = module.io() + socket.on('reload', (data) => { + Router.replace(Router.asPath, undefined, { + scroll: false, + }) + }) + }) + }, []) + + return null +} diff --git a/components/Footer.js b/components/Footer.js new file mode 100644 index 0000000..7ce6738 --- /dev/null +++ b/components/Footer.js @@ -0,0 +1,17 @@ +import Link from './Link' +import siteMetadata from '@/data/siteMetadata' +import SocialIcon from '@/components/social-icons' + +export default function Footer() { + return ( + + ) +} diff --git a/components/Image.js b/components/Image.js new file mode 100644 index 0000000..0959091 --- /dev/null +++ b/components/Image.js @@ -0,0 +1,6 @@ +import NextImage from 'next/image' + +// eslint-disable-next-line jsx-a11y/alt-text +const Image = ({ ...rest }) => + +export default Image diff --git a/components/LayoutWrapper.js b/components/LayoutWrapper.js new file mode 100644 index 0000000..602e27a --- /dev/null +++ b/components/LayoutWrapper.js @@ -0,0 +1,54 @@ +import siteMetadata from '@/data/siteMetadata' +import headerNavLinks from '@/data/headerNavLinks' +import Logo from '@/data/logo.svg' +import Link from './Link' +import SectionContainer from './SectionContainer' +import Footer from './Footer' +import MobileNav from './MobileNav' +import ThemeSwitch from './ThemeSwitch' + +const LayoutWrapper = ({ children }) => { + return ( + +
+
+
+ +
+
+ +
+ {typeof siteMetadata.headerTitle === 'string' ? ( +
+ {siteMetadata.headerTitle} +
+ ) : ( + siteMetadata.headerTitle + )} +
+ +
+
+
+ {headerNavLinks.map((link) => ( + + {link.title} + + ))} +
+ + +
+
+
{children}
+
+
+
+ ) +} + +export default LayoutWrapper diff --git a/components/Link.js b/components/Link.js new file mode 100644 index 0000000..185eec9 --- /dev/null +++ b/components/Link.js @@ -0,0 +1,23 @@ +/* eslint-disable jsx-a11y/anchor-has-content */ +import Link from 'next/link' + +const CustomLink = ({ href, ...rest }) => { + const isInternalLink = href && href.startsWith('/') + const isAnchorLink = href && href.startsWith('#') + + if (isInternalLink) { + return ( + + + + ) + } + + if (isAnchorLink) { + return + } + + return +} + +export default CustomLink diff --git a/components/MDXComponents.js b/components/MDXComponents.js new file mode 100644 index 0000000..6ad2e0f --- /dev/null +++ b/components/MDXComponents.js @@ -0,0 +1,32 @@ +/* eslint-disable react/display-name */ +import { useMemo } from 'react' +import { getMDXComponent } from 'mdx-bundler/client' +import Image from './Image' +import CustomLink from './Link' +import TOCInline from './TOCInline' +import Pre from './Pre' +import Quote from './Quote' +import YoutubeEmbed from './YoutubeEmbed' + +import Zoom from 'react-medium-image-zoom' +import 'react-medium-image-zoom/dist/styles.css' + +export const MDXComponents = { + Image, + TOCInline, + a: CustomLink, + pre: Pre, + Quote, + YoutubeEmbed, + Zoom, + wrapper: ({ components, layout, ...rest }) => { + const Layout = require(`../layouts/${layout}`).default + return + }, +} + +export const MDXLayoutRenderer = ({ layout, mdxSource, ...rest }) => { + const MDXLayout = useMemo(() => getMDXComponent(mdxSource), [mdxSource]) + + return +} diff --git a/components/MobileNav.js b/components/MobileNav.js new file mode 100644 index 0000000..3ed6cad --- /dev/null +++ b/components/MobileNav.js @@ -0,0 +1,78 @@ +import { useState } from 'react' +import Link from './Link' +import headerNavLinks from '@/data/headerNavLinks' + +const MobileNav = () => { + const [navShow, setNavShow] = useState(false) + + const onToggleNav = () => { + setNavShow((status) => { + if (status) { + document.body.style.overflow = 'auto' + } else { + // Prevent scrolling + document.body.style.overflow = 'hidden' + } + return !status + }) + } + + return ( +
+ +
+ + +
+
+ ) +} + +export default MobileNav diff --git a/components/PageTitle.js b/components/PageTitle.js new file mode 100644 index 0000000..82419b5 --- /dev/null +++ b/components/PageTitle.js @@ -0,0 +1,7 @@ +export default function PageTitle({ children }) { + return ( +

+ {children} +

+ ) +} diff --git a/components/Pagination.js b/components/Pagination.js new file mode 100644 index 0000000..6c45781 --- /dev/null +++ b/components/Pagination.js @@ -0,0 +1,36 @@ +import Link from '@/components/Link' + +export default function Pagination({ totalPages, currentPage }) { + const prevPage = parseInt(currentPage) - 1 > 0 + const nextPage = parseInt(currentPage) + 1 <= parseInt(totalPages) + + return ( +
+ +
+ ) +} diff --git a/components/Pre.js b/components/Pre.js new file mode 100644 index 0000000..995f99b --- /dev/null +++ b/components/Pre.js @@ -0,0 +1,71 @@ +import { useState, useRef } from 'react' + +const Pre = (props) => { + const textInput = useRef(null) + const [hovered, setHovered] = useState(false) + const [copied, setCopied] = useState(false) + + const onEnter = () => { + setHovered(true) + } + const onExit = () => { + setHovered(false) + setCopied(false) + } + const onCopy = () => { + setCopied(true) + navigator.clipboard.writeText(textInput.current.innerText) + setTimeout(() => { + setCopied(false) + }, 2000) + } + + return ( +
+ {hovered && ( + + )} + +
{props.children}
+
+ ) +} + +export default Pre diff --git a/components/Quote.js b/components/Quote.js new file mode 100644 index 0000000..496f1af --- /dev/null +++ b/components/Quote.js @@ -0,0 +1,13 @@ +import PropTypes from 'prop-types' + +const Quote = ({ quote }) => ( +
+

{ quote }

+
+) + +Quote.propTypes = { + quote: PropTypes.string, +} + +export default Quote diff --git a/components/SEO.js b/components/SEO.js new file mode 100644 index 0000000..0da0f31 --- /dev/null +++ b/components/SEO.js @@ -0,0 +1,150 @@ +import Head from 'next/head' +import { useRouter } from 'next/router' +import siteMetadata from '@/data/siteMetadata' + +const CommonSEO = ({ title, description, ogType, ogImage, twImage }) => { + const router = useRouter() + return ( + + {title} + + + + + + + + {ogImage.constructor.name === 'Array' ? ( + ogImage.map(({ url }) => ) + ) : ( + + )} + + + + + + + ) +} + +export const PageSEO = ({ title, description }) => { + const ogImageUrl = siteMetadata.siteUrl + siteMetadata.socialBanner + const twImageUrl = siteMetadata.siteUrl + siteMetadata.socialBanner + return ( + + ) +} + +export const TagSEO = ({ title, description }) => { + const ogImageUrl = siteMetadata.siteUrl + siteMetadata.socialBanner + const twImageUrl = siteMetadata.siteUrl + siteMetadata.socialBanner + const router = useRouter() + return ( + <> + + + + + + ) +} + +export const BlogSEO = ({ authorDetails, title, summary, date, lastmod, url, images = [] }) => { + const router = useRouter() + const publishedAt = new Date(date).toISOString() + const modifiedAt = new Date(lastmod || date).toISOString() + let imagesArr = + images.length === 0 + ? [siteMetadata.socialBanner] + : typeof images === 'string' + ? [images] + : images + + const featuredImages = imagesArr.map((img) => { + return { + '@type': 'ImageObject', + url: `${siteMetadata.siteUrl}${img}`, + } + }) + + let authorList + if (authorDetails) { + authorList = authorDetails.map((author) => { + return { + '@type': 'Person', + name: author.name, + } + }) + } else { + authorList = { + '@type': 'Person', + name: siteMetadata.author, + } + } + + const structuredData = { + '@context': 'https://schema.org', + '@type': 'Article', + mainEntityOfPage: { + '@type': 'WebPage', + '@id': url, + }, + headline: title, + image: featuredImages, + datePublished: publishedAt, + dateModified: modifiedAt, + author: authorList, + publisher: { + '@type': 'Organization', + name: siteMetadata.author, + logo: { + '@type': 'ImageObject', + url: `${siteMetadata.siteUrl}${siteMetadata.siteLogo}`, + }, + }, + description: summary, + } + + const twImageUrl = featuredImages[0].url + + return ( + <> + + + {date && } + {lastmod && } + + + + ) +} + +export default GAScript + +// https://developers.google.com/analytics/devguides/collection/gtagjs/events +export const logEvent = (action, category, label, value) => { + window.gtag?.('event', action, { + event_category: category, + event_label: label, + value: value, + }) +} diff --git a/components/analytics/Plausible.js b/components/analytics/Plausible.js new file mode 100644 index 0000000..5c2a356 --- /dev/null +++ b/components/analytics/Plausible.js @@ -0,0 +1,27 @@ +import Script from 'next/script' + +import siteMetadata from '@/data/siteMetadata' + +const PlausibleScript = () => { + return ( + <> + + + ) +} + +export default PlausibleScript + +// https://plausible.io/docs/custom-event-goals +export const logEvent = (eventName, ...rest) => { + return window.plausible?.(eventName, ...rest) +} diff --git a/components/analytics/SimpleAnalytics.js b/components/analytics/SimpleAnalytics.js new file mode 100644 index 0000000..139d459 --- /dev/null +++ b/components/analytics/SimpleAnalytics.js @@ -0,0 +1,25 @@ +import Script from 'next/script' + +const SimpleAnalyticsScript = () => { + return ( + <> + +