refactor: 改用 TypeScript。close #1.

This commit is contained in:
2022-10-07 13:55:39 +08:00
parent 11b9017a07
commit 1dfd5e5271
84 changed files with 3459 additions and 3836 deletions

View File

@ -1,31 +0,0 @@
import Link from '@/components/Link'
import { PageSEO } from '@/components/SEO'
import siteMetadata from '@/data/siteMetadata'
export default function FourZeroFour() {
return (
<>
<PageSEO title={`Page Not Found - ${siteMetadata.title}`} />
<div className="flex flex-col items-start justify-start md:mt-24 md:flex-row md:items-center md:justify-center md:space-x-6">
<div className="space-x-2 pt-6 pb-8 md:space-y-5">
<h1 className="text-6xl font-extrabold leading-9 tracking-tight text-gray-900 dark:text-gray-100 md:border-r-2 md:px-6 md:text-8xl md:leading-14">
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 don't worry, you can find plenty of other things on our homepage.
</p>
<Link href="/">
<button className="focus:shadow-outline-blue inline rounded-lg border border-transparent bg-blue-600 px-4 py-2 text-sm font-medium leading-5 text-white shadow transition-colors duration-150 hover:bg-blue-700 focus:outline-none dark:hover:bg-blue-500">
Back to homepage
</button>
</Link>
</div>
</div>
</>
)
}

24
pages/404.tsx 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:mt-24 md:flex-row md:items-center md:justify-center md:space-x-6">
<div className="space-x-2 pt-6 pb-8 md:space-y-5">
<h1 className="text-6xl font-extrabold leading-9 tracking-tight text-gray-900 dark:text-gray-100 md:border-r-2 md:px-6 md:text-8xl md:leading-14">
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="focus:shadow-outline-blue inline rounded-lg border border-transparent bg-blue-600 px-4 py-2 text-sm font-medium leading-5 text-white shadow transition-colors duration-150 hover:bg-blue-700 focus:outline-none dark:hover:bg-blue-500">
Back to homepage
</button>
</Link>
</div>
</div>
)
}

View File

@ -5,6 +5,7 @@ import 'katex/dist/katex.css'
import '@fontsource/inter/variable-full.css'
import { ThemeProvider } from 'next-themes'
import type { AppProps } from 'next/app'
import Head from 'next/head'
import siteMetadata from '@/data/siteMetadata'
@ -15,7 +16,7 @@ import { ClientReload } from '@/components/ClientReload'
const isDevelopment = process.env.NODE_ENV === 'development'
const isSocket = process.env.SOCKET
export default function App({ Component, pageProps }) {
export default function App({ Component, pageProps }: AppProps) {
return (
<ThemeProvider attribute="class" defaultTheme={siteMetadata.theme}>
<Head>

View File

@ -1,4 +1,5 @@
import Document, { Html, Head, Main, NextScript } from 'next/document'
class MyDocument extends Document {
render() {
return (
@ -20,8 +21,7 @@ class MyDocument extends Document {
<link rel="manifest" href="/static/favicons/site.webmanifest" />
<link rel="mask-icon" href="/static/favicons/safari-pinned-tab.svg" color="#5bbad5" />
<meta name="msapplication-TileColor" content="#000000" />
<meta name="theme-color" media="(prefers-color-scheme: light)" content="#fff" />
<meta name="theme-color" media="(prefers-color-scheme: dark)" content="#000" />
<meta name="theme-color" content="#000000" />
<link rel="alternate" type="application/rss+xml" href="/feed.xml" />
</Head>
<body className="bg-white text-black antialiased dark:bg-gray-900 dark:text-white">

View File

@ -1,21 +0,0 @@
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}
/>
)
}

27
pages/about.tsx Normal file
View File

@ -0,0 +1,27 @@
import { MDXLayoutRenderer } from '@/components/MDXComponents'
import { getFileBySlug } from '@/lib/mdx'
import { GetStaticProps, InferGetStaticPropsType } from 'next'
import { AuthorFrontMatter } from 'types/AuthorFrontMatter'
const DEFAULT_LAYOUT = 'AuthorLayout'
// @ts-ignore
export const getStaticProps: GetStaticProps<{
authorDetails: { mdxSource: string; frontMatter: AuthorFrontMatter }
}> = async () => {
const authorDetails = await getFileBySlug<AuthorFrontMatter>('authors', ['default'])
const { mdxSource, frontMatter } = authorDetails
return { props: { authorDetails: { mdxSource, frontMatter } } }
}
export default function About({ authorDetails }: InferGetStaticPropsType<typeof getStaticProps>) {
const { mdxSource, frontMatter } = authorDetails
return (
<MDXLayoutRenderer
layout={frontMatter.layout || DEFAULT_LAYOUT}
mdxSource={mdxSource}
frontMatter={frontMatter}
/>
)
}

View File

@ -1,5 +1,7 @@
import { NextApiRequest, NextApiResponse } from 'next'
// eslint-disable-next-line import/no-anonymous-default-export
export default async (req, res) => {
export default async (req: NextApiRequest, res: NextApiResponse) => {
const { email } = req.body
if (!email) {
return res.status(400).json({ error: 'Email is required' })

View File

@ -1,5 +1,7 @@
import { NextApiRequest, NextApiResponse } from 'next'
/* eslint-disable import/no-anonymous-default-export */
export default async (req, res) => {
export default async (req: NextApiRequest, res: NextApiResponse) => {
const { email } = req.body
if (!email) {

View File

@ -1,33 +0,0 @@
// eslint-disable-next-line import/no-anonymous-default-export
export default async (req, res) => {
const { email } = req.body
if (!email) {
return res.status(400).json({ error: 'Email is required' })
}
try {
const API_URL = process.env.EMAILOCTOPUS_API_URL
const API_KEY = process.env.EMAILOCTOPUS_API_KEY
const LIST_ID = process.env.EMAILOCTOPUS_LIST_ID
const data = { email_address: email, api_key: API_KEY }
const API_ROUTE = `${API_URL}lists/${LIST_ID}/contacts`
const response = await fetch(API_ROUTE, {
body: JSON.stringify(data),
headers: {
'Content-Type': 'application/json',
},
method: 'POST',
})
if (response.status >= 400) {
return res.status(500).json({ error: `There was an error subscribing to the list.` })
}
return res.status(201).json({ error: '' })
} catch (error) {
return res.status(500).json({ error: error.message || error.toString() })
}
}

View File

@ -1,5 +1,7 @@
import { NextApiRequest, NextApiResponse } from 'next'
/* eslint-disable import/no-anonymous-default-export */
export default async (req, res) => {
export default async (req: NextApiRequest, res: NextApiResponse) => {
const { email } = req.body
if (!email) {
return res.status(400).json({ error: 'Email is required' })

View File

@ -1,3 +1,4 @@
import { NextApiRequest, NextApiResponse } from 'next'
import mailchimp from '@mailchimp/mailchimp_marketing'
mailchimp.setConfig({
@ -6,7 +7,7 @@ mailchimp.setConfig({
})
// eslint-disable-next-line import/no-anonymous-default-export
export default async (req, res) => {
export default async (req: NextApiRequest, res: NextApiResponse) => {
const { email } = req.body
if (!email) {
@ -14,7 +15,7 @@ export default async (req, res) => {
}
try {
const test = await mailchimp.lists.addListMember(process.env.MAILCHIMP_AUDIENCE_ID, {
await mailchimp.lists.addListMember(process.env.MAILCHIMP_AUDIENCE_ID, {
email_address: email,
status: 'subscribed',
})

View File

@ -1,30 +0,0 @@
// eslint-disable-next-line import/no-anonymous-default-export
export default async (req, res) => {
const { email } = req.body
if (!email) {
return res.status(400).json({ error: 'Email is required' })
}
try {
const API_KEY = process.env.REVUE_API_KEY
const revueRoute = `${process.env.REVUE_API_URL}subscribers`
const response = await fetch(revueRoute, {
method: 'POST',
headers: {
Authorization: `Token ${API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({ email, double_opt_in: false }),
})
if (response.status >= 400) {
return res.status(500).json({ error: `There was an error subscribing to the list.` })
}
return res.status(201).json({ error: '' })
} catch (error) {
return res.status(500).json({ error: error.message || error.toString() })
}
}

View File

@ -2,10 +2,16 @@ import { getAllFilesFrontMatter } from '@/lib/mdx'
import siteMetadata from '@/data/siteMetadata'
import ListLayout from '@/layouts/ListLayout'
import { PageSEO } from '@/components/SEO'
import { GetStaticProps, InferGetStaticPropsType } from 'next'
import { ComponentProps } from 'react'
export const POSTS_PER_PAGE = 5
export async function getStaticProps() {
export const getStaticProps: GetStaticProps<{
posts: ComponentProps<typeof ListLayout>['posts']
initialDisplayPosts: ComponentProps<typeof ListLayout>['initialDisplayPosts']
pagination: ComponentProps<typeof ListLayout>['pagination']
}> = async () => {
const posts = await getAllFilesFrontMatter('blog')
const initialDisplayPosts = posts.slice(0, POSTS_PER_PAGE)
const pagination = {
@ -16,7 +22,11 @@ export async function getStaticProps() {
return { props: { initialDisplayPosts, posts, pagination } }
}
export default function Blog({ posts, initialDisplayPosts, pagination }) {
export default function Blog({
posts,
initialDisplayPosts,
pagination,
}: InferGetStaticPropsType<typeof getStaticProps>) {
return (
<>
<PageSEO title={`Blog - ${siteMetadata.author}`} description={siteMetadata.description} />

View File

@ -3,6 +3,10 @@ import PageTitle from '@/components/PageTitle'
import generateRss from '@/lib/generate-rss'
import { MDXLayoutRenderer } from '@/components/MDXComponents'
import { formatSlug, getAllFilesFrontMatter, getFileBySlug, getFiles } from '@/lib/mdx'
import { GetStaticProps, InferGetStaticPropsType } from 'next'
import { AuthorFrontMatter } from 'types/AuthorFrontMatter'
import { PostFrontMatter } from 'types/PostFrontMatter'
import { Toc } from 'types/Toc'
const DEFAULT_LAYOUT = 'PostLayout'
@ -18,15 +22,23 @@ export async function getStaticPaths() {
}
}
export async function getStaticProps({ params }) {
// @ts-ignore
export const getStaticProps: GetStaticProps<{
post: { mdxSource: string; toc: Toc; frontMatter: PostFrontMatter }
authorDetails: AuthorFrontMatter[]
prev?: { slug: string; title: string }
next?: { slug: string; title: string }
}> = async ({ params }) => {
const slug = (params.slug as string[]).join('/')
const allPosts = await getAllFilesFrontMatter('blog')
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('blog', params.slug.join('/'))
const postIndex = allPosts.findIndex((post) => formatSlug(post.slug) === slug)
const prev: { slug: string; title: string } = allPosts[postIndex + 1] || null
const next: { slug: string; title: string } = allPosts[postIndex - 1] || null
const post = await getFileBySlug<PostFrontMatter>('blog', slug)
// @ts-ignore
const authorList = post.frontMatter.authors || ['default']
const authorPromise = authorList.map(async (author) => {
const authorResults = await getFileBySlug('authors', [author])
const authorResults = await getFileBySlug<AuthorFrontMatter>('authors', [author])
return authorResults.frontMatter
})
const authorDetails = await Promise.all(authorPromise)
@ -37,15 +49,27 @@ export async function getStaticProps({ params }) {
fs.writeFileSync('./public/feed.xml', rss)
}
return { props: { post, authorDetails, prev, next } }
return {
props: {
post,
authorDetails,
prev,
next,
},
}
}
export default function Blog({ post, authorDetails, prev, next }) {
export default function Blog({
post,
authorDetails,
prev,
next,
}: InferGetStaticPropsType<typeof getStaticProps>) {
const { mdxSource, toc, frontMatter } = post
return (
<>
{frontMatter.draft !== true ? (
{'draft' in frontMatter && frontMatter.draft !== true ? (
<MDXLayoutRenderer
layout={frontMatter.layout || DEFAULT_LAYOUT}
toc={toc}

View File

@ -3,8 +3,10 @@ import siteMetadata from '@/data/siteMetadata'
import { getAllFilesFrontMatter } from '@/lib/mdx'
import ListLayout from '@/layouts/ListLayout'
import { POSTS_PER_PAGE } from '../../blog'
import { GetStaticPaths, GetStaticProps, InferGetStaticPropsType } from 'next'
import { PostFrontMatter } from 'types/PostFrontMatter'
export async function getStaticPaths() {
export const getStaticPaths: GetStaticPaths<{ page: string }> = async () => {
const totalPosts = await getAllFilesFrontMatter('blog')
const totalPages = Math.ceil(totalPosts.length / POSTS_PER_PAGE)
const paths = Array.from({ length: totalPages }, (_, i) => ({
@ -17,12 +19,16 @@ export async function getStaticPaths() {
}
}
export async function getStaticProps(context) {
export const getStaticProps: GetStaticProps<{
posts: PostFrontMatter[]
initialDisplayPosts: PostFrontMatter[]
pagination: { currentPage: number; totalPages: number }
}> = async (context) => {
const {
params: { page },
} = context
const posts = await getAllFilesFrontMatter('blog')
const pageNumber = parseInt(page)
const pageNumber = parseInt(page as string)
const initialDisplayPosts = posts.slice(
POSTS_PER_PAGE * (pageNumber - 1),
POSTS_PER_PAGE * pageNumber
@ -41,7 +47,11 @@ export async function getStaticProps(context) {
}
}
export default function PostPage({ posts, initialDisplayPosts, pagination }) {
export default function PostPage({
posts,
initialDisplayPosts,
pagination,
}: InferGetStaticPropsType<typeof getStaticProps>) {
return (
<>
<PageSEO title={siteMetadata.title} description={siteMetadata.description} />

View File

@ -4,25 +4,26 @@ import Tag from '@/components/Tag'
import siteMetadata from '@/data/siteMetadata'
import { getAllFilesFrontMatter } from '@/lib/mdx'
import formatDate from '@/lib/utils/formatDate'
import { GetStaticProps, InferGetStaticPropsType } from 'next'
import { PostFrontMatter } from 'types/PostFrontMatter'
import NewsletterForm from '@/components/NewsletterForm'
const MAX_DISPLAY = 5
export async function getStaticProps() {
export const getStaticProps: GetStaticProps<{ posts: PostFrontMatter[] }> = async () => {
const posts = await getAllFilesFrontMatter('blog')
return { props: { posts } }
}
export default function Home({ posts }) {
export default function Home({ posts }: InferGetStaticPropsType<typeof getStaticProps>) {
return (
<>
<PageSEO title={siteMetadata.title} description={siteMetadata.description} />
<div className="divide-y divide-gray-200 dark:divide-gray-700">
<div className="space-y-2 pt-6 pb-8 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">
Hi.
Latest
</h1>
<p className="text-lg leading-7 text-gray-500 dark:text-gray-400">
{siteMetadata.description}

View File

@ -13,7 +13,7 @@ export default function Projects() {
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">

View File

@ -4,14 +4,15 @@ import Tag from '@/components/Tag'
import siteMetadata from '@/data/siteMetadata'
import { getAllTags } from '@/lib/tags'
import kebabCase from '@/lib/utils/kebabCase'
import { GetStaticProps, InferGetStaticPropsType } from 'next'
export async function getStaticProps() {
export const getStaticProps: GetStaticProps<{ tags: Record<string, number> }> = async () => {
const tags = await getAllTags('blog')
return { props: { tags } }
}
export default function Tags({ tags }) {
export default function Tags({ tags }: InferGetStaticPropsType<typeof getStaticProps>) {
const sortedTags = Object.keys(tags).sort((a, b) => tags[b] - tags[a])
return (
<>

View File

@ -6,7 +6,9 @@ import { getAllFilesFrontMatter } from '@/lib/mdx'
import { getAllTags } from '@/lib/tags'
import kebabCase from '@/lib/utils/kebabCase'
import fs from 'fs'
import { GetStaticProps, InferGetStaticPropsType } from 'next'
import path from 'path'
import { PostFrontMatter } from 'types/PostFrontMatter'
const root = process.cwd()
@ -23,30 +25,33 @@ export async function getStaticPaths() {
}
}
export async function getStaticProps({ params }) {
export const getStaticProps: GetStaticProps<{ posts: PostFrontMatter[]; tag: string }> = async (
context
) => {
const tag = context.params.tag as string
const allPosts = await getAllFilesFrontMatter('blog')
const filteredPosts = allPosts.filter(
(post) => post.draft !== true && post.tags.map((t) => kebabCase(t)).includes(params.tag)
(post) => post.draft !== true && post.tags.map((t) => kebabCase(t)).includes(tag)
)
// rss
if (filteredPosts.length > 0) {
const rss = generateRss(filteredPosts, `tags/${params.tag}/feed.xml`)
const rssPath = path.join(root, 'public', 'tags', params.tag)
const rss = generateRss(filteredPosts, `tags/${tag}/feed.xml`)
const rssPath = path.join(root, 'public', 'tags', tag)
fs.mkdirSync(rssPath, { recursive: true })
fs.writeFileSync(path.join(rssPath, 'feed.xml'), rss)
}
return { props: { posts: filteredPosts, tag: params.tag } }
return { props: { posts: filteredPosts, tag } }
}
export default function Tag({ posts, tag }) {
export default function Tag({ posts, tag }: InferGetStaticPropsType<typeof getStaticProps>) {
// Capitalize first letter and convert space to dash
const title = tag[0].toUpperCase() + tag.split(' ').join('-').slice(1)
return (
<>
<TagSEO
title={`${tag} - ${siteMetadata.author}`}
title={`${tag} - ${siteMetadata.title}`}
description={`${tag} tags - ${siteMetadata.author}`}
/>
<ListLayout posts={posts} title={title} />