refactor: migrate to rsc and app dir
This commit is contained in:
95
app/Main.tsx
Normal file
95
app/Main.tsx
Normal file
@ -0,0 +1,95 @@
|
||||
'use client'
|
||||
|
||||
import Link from '@/components/Link'
|
||||
// import { PageSEO } from '@/components/SEO'
|
||||
import Tag from '@/components/Tag'
|
||||
import siteMetadata from '@/data/siteMetadata'
|
||||
import { formatDate } from 'pliny/utils/formatDate'
|
||||
import { NewsletterForm } from 'pliny/ui/NewsletterForm'
|
||||
|
||||
const MAX_DISPLAY = 5
|
||||
|
||||
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="space-y-2 pb-8 pt-6 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">
|
||||
Latest
|
||||
</h1>
|
||||
<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((post) => {
|
||||
const { slug, date, title, summary, tags } = post
|
||||
return (
|
||||
<li key={slug} className="py-12">
|
||||
<article>
|
||||
<div className="space-y-2 xl:grid xl:grid-cols-4 xl:items-baseline xl:space-y-0">
|
||||
<dl>
|
||||
<dt className="sr-only">Published on</dt>
|
||||
<dd className="text-base font-medium leading-6 text-gray-500 dark:text-gray-400">
|
||||
<time dateTime={date}>{formatDate(date, siteMetadata.locale)}</time>
|
||||
</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={`/blog/${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 max-w-none text-gray-500 dark:text-gray-400">
|
||||
{summary}
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-base font-medium leading-6">
|
||||
<Link
|
||||
href={`/blog/${slug}`}
|
||||
className="text-primary-500 hover:text-primary-600 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="/blog"
|
||||
className="text-primary-500 hover:text-primary-600 dark:hover:text-primary-400"
|
||||
aria-label="All posts"
|
||||
>
|
||||
All Posts →
|
||||
</Link>
|
||||
</div>
|
||||
)}
|
||||
{siteMetadata.newsletter?.provider && (
|
||||
<div className="flex items-center justify-center pt-4">
|
||||
<NewsletterForm />
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
16
app/about/About.tsx
Normal file
16
app/about/About.tsx
Normal file
@ -0,0 +1,16 @@
|
||||
'use client'
|
||||
|
||||
import { MDXLayoutRenderer } from 'pliny/mdx-components'
|
||||
import { MDXComponents } from '@/components/MDXComponents'
|
||||
|
||||
const DEFAULT_LAYOUT = 'AuthorLayout'
|
||||
|
||||
export default function About({ author }) {
|
||||
return (
|
||||
<MDXLayoutRenderer
|
||||
layout={author.layout || DEFAULT_LAYOUT}
|
||||
content={author}
|
||||
MDXComponents={MDXComponents}
|
||||
/>
|
||||
)
|
||||
}
|
7
app/about/page.tsx
Normal file
7
app/about/page.tsx
Normal file
@ -0,0 +1,7 @@
|
||||
import { allAuthors } from 'contentlayer/generated'
|
||||
import About from './About'
|
||||
|
||||
export default function Page() {
|
||||
const author = allAuthors.find((p) => p.slug === 'default')
|
||||
return <About author={author} />
|
||||
}
|
14
app/api/newsletter2/route.ts
Normal file
14
app/api/newsletter2/route.ts
Normal file
@ -0,0 +1,14 @@
|
||||
import { NextResponse } from 'next/server'
|
||||
import { NewsletterAPI } from 'pliny/newsletter'
|
||||
import siteMetadata from '@/data/siteMetadata'
|
||||
|
||||
export async function POST(request: Request) {
|
||||
const res = await request.json()
|
||||
console.log(res) // { email: 'test@example.com' }
|
||||
|
||||
return NextResponse.json({ res })
|
||||
// return NewsletterAPI({
|
||||
// // @ts-ignore
|
||||
// provider: siteMetadata.newsletter.provider,
|
||||
// })
|
||||
}
|
21
app/blog/[...slug]/Blog.tsx
Normal file
21
app/blog/[...slug]/Blog.tsx
Normal file
@ -0,0 +1,21 @@
|
||||
'use client'
|
||||
|
||||
import { MDXLayoutRenderer } from 'pliny/mdx-components'
|
||||
import { MDXComponents } from '@/components/MDXComponents'
|
||||
import type { Blog } from 'contentlayer/generated'
|
||||
|
||||
const DEFAULT_LAYOUT = 'PostLayout'
|
||||
|
||||
export default function Blog({ post, authorDetails, prev, next }) {
|
||||
return (
|
||||
<MDXLayoutRenderer
|
||||
layout={post.layout || DEFAULT_LAYOUT}
|
||||
content={post}
|
||||
MDXComponents={MDXComponents}
|
||||
toc={post.toc}
|
||||
authorDetails={authorDetails}
|
||||
prev={prev}
|
||||
next={next}
|
||||
/>
|
||||
)
|
||||
}
|
44
app/blog/[...slug]/page.tsx
Normal file
44
app/blog/[...slug]/page.tsx
Normal file
@ -0,0 +1,44 @@
|
||||
import PageTitle from '@/components/PageTitle'
|
||||
import { sortedBlogPost, coreContent } from 'pliny/utils/contentlayer'
|
||||
import { allBlogs, allAuthors } from 'contentlayer/generated'
|
||||
import type { Authors, Blog } from 'contentlayer/generated'
|
||||
import BlogPage from './Blog'
|
||||
|
||||
export const generateStaticParams = async () => {
|
||||
const paths = allBlogs.map((p) => ({ slug: p.slug.split('/') }))
|
||||
|
||||
return paths
|
||||
}
|
||||
|
||||
export default function Page({ params }: { params: { slug: string[] } }) {
|
||||
const slug = params.slug.join('/')
|
||||
const sortedPosts = sortedBlogPost(allBlogs) as Blog[]
|
||||
const postIndex = sortedPosts.findIndex((p) => p.slug === slug)
|
||||
const prevContent = sortedPosts[postIndex + 1] || null
|
||||
const prev = prevContent ? coreContent(prevContent) : null
|
||||
const nextContent = sortedPosts[postIndex - 1] || null
|
||||
const next = nextContent ? coreContent(nextContent) : null
|
||||
const post = sortedPosts.find((p) => p.slug === slug)
|
||||
const authorList = post?.authors || ['default']
|
||||
const authorDetails = authorList.map((author) => {
|
||||
const authorResults = allAuthors.find((p) => p.slug === author)
|
||||
return coreContent(authorResults as Authors)
|
||||
})
|
||||
|
||||
return (
|
||||
<>
|
||||
{post && 'draft' in post && post.draft === true ? (
|
||||
<div className="mt-24 text-center">
|
||||
<PageTitle>
|
||||
Under Construction{' '}
|
||||
<span role="img" aria-label="roadwork sign">
|
||||
🚧
|
||||
</span>
|
||||
</PageTitle>
|
||||
</div>
|
||||
) : (
|
||||
<BlogPage post={post} authorDetails={authorDetails} prev={prev} next={next} />
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
31
app/blog/page.tsx
Normal file
31
app/blog/page.tsx
Normal file
@ -0,0 +1,31 @@
|
||||
import ListLayout from '@/layouts/ListLayout'
|
||||
import { sortedBlogPost } from 'pliny/utils/contentlayer'
|
||||
import { allBlogs } from 'contentlayer/generated'
|
||||
import type { Blog } from 'contentlayer/generated'
|
||||
|
||||
const POSTS_PER_PAGE = 5
|
||||
|
||||
export default function BlogPage() {
|
||||
const posts = sortedBlogPost(allBlogs) as Blog[]
|
||||
const pageNumber = 1
|
||||
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 (
|
||||
<>
|
||||
{/* <PageSEO title={`Blog - ${siteMetadata.author}`} description={siteMetadata.description} /> */}
|
||||
<ListLayout
|
||||
posts={posts}
|
||||
initialDisplayPosts={initialDisplayPosts}
|
||||
pagination={pagination}
|
||||
title="All Posts"
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
41
app/blog/page/[page]/page.tsx
Normal file
41
app/blog/page/[page]/page.tsx
Normal file
@ -0,0 +1,41 @@
|
||||
// import { PageSEO } from '@/components/SEO'
|
||||
import siteMetadata from '@/data/siteMetadata'
|
||||
import ListLayout from '@/layouts/ListLayout'
|
||||
import { allCoreContent, sortedBlogPost } from 'pliny/utils/contentlayer'
|
||||
import { allBlogs } from 'contentlayer/generated'
|
||||
import type { Blog } from 'contentlayer/generated'
|
||||
|
||||
const POSTS_PER_PAGE = 5
|
||||
|
||||
export const generateStaticParams = async () => {
|
||||
const totalPosts = allBlogs
|
||||
const totalPages = Math.ceil(totalPosts.length / POSTS_PER_PAGE)
|
||||
const paths = Array.from({ length: totalPages }, (_, i) => ({ page: (i + 1).toString() }))
|
||||
|
||||
return paths
|
||||
}
|
||||
|
||||
export default function Page({ params }: { params: { page: string } }) {
|
||||
const posts = sortedBlogPost(allBlogs) as Blog[]
|
||||
const pageNumber = parseInt(params.page as string)
|
||||
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 (
|
||||
<>
|
||||
{/* <PageSEO title={siteMetadata.title} description={siteMetadata.description} /> */}
|
||||
<ListLayout
|
||||
posts={allCoreContent(posts)}
|
||||
initialDisplayPosts={allCoreContent(initialDisplayPosts)}
|
||||
pagination={pagination}
|
||||
title="All Posts"
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
29
app/globals.css
Normal file
29
app/globals.css
Normal file
@ -0,0 +1,29 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
.task-list-item::before {
|
||||
@apply hidden;
|
||||
}
|
||||
|
||||
.task-list-item {
|
||||
@apply list-none;
|
||||
}
|
||||
|
||||
.footnotes {
|
||||
@apply mt-12 border-t border-gray-200 pt-8 dark:border-gray-700;
|
||||
}
|
||||
|
||||
.data-footnote-backref {
|
||||
@apply no-underline;
|
||||
}
|
||||
|
||||
.csl-entry {
|
||||
@apply my-5;
|
||||
}
|
||||
|
||||
/* https://stackoverflow.com/questions/61083813/how-to-avoid-internal-autofill-selected-style-to-be-applied */
|
||||
input:-webkit-autofill,
|
||||
input:-webkit-autofill:focus {
|
||||
transition: background-color 600000s 0s, color 600000s 0s;
|
||||
}
|
16
app/head.tsx
Normal file
16
app/head.tsx
Normal file
@ -0,0 +1,16 @@
|
||||
export default function Head() {
|
||||
return (
|
||||
<>
|
||||
<link rel="apple-touch-icon" sizes="76x76" href="/static/favicons/apple-touch-icon.png" />
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="/static/favicons/favicon-32x32.png" />
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="/static/favicons/favicon-16x16.png" />
|
||||
<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" />
|
||||
<link rel="alternate" type="application/rss+xml" href="/feed.xml" />
|
||||
<meta content="width=device-width, initial-scale=1" name="viewport" />
|
||||
</>
|
||||
)
|
||||
}
|
35
app/layout.tsx
Normal file
35
app/layout.tsx
Normal file
@ -0,0 +1,35 @@
|
||||
import './globals.css'
|
||||
|
||||
import { Inter } from 'next/font/google'
|
||||
import { Analytics } from 'pliny/analytics'
|
||||
import { SearchProvider } from 'pliny/search'
|
||||
import Header from '@/components/Header'
|
||||
import SectionContainer from '@/components/SectionContainer'
|
||||
import Footer from '@/components/Footer'
|
||||
import siteMetadata from '@/data/siteMetadata'
|
||||
import { ThemeProviders } from './theme-providers'
|
||||
|
||||
const inter = Inter({
|
||||
subsets: ['latin'],
|
||||
})
|
||||
|
||||
export default function RootLayout({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<html lang={siteMetadata.language} className={`${inter.className} scroll-smooth`}>
|
||||
<body className="bg-white text-black antialiased dark:bg-gray-900 dark:text-white">
|
||||
<ThemeProviders>
|
||||
{/* <Analytics analyticsConfig={siteMetadata.analytics} /> */}
|
||||
<SectionContainer>
|
||||
<div className="flex h-screen flex-col justify-between font-sans">
|
||||
{/* <SearchProvider searchConfig={siteMetadata.search}> */}
|
||||
<Header />
|
||||
<main className="mb-auto">{children}</main>
|
||||
{/* </SearchProvider> */}
|
||||
<Footer />
|
||||
</div>
|
||||
</SectionContainer>
|
||||
</ThemeProviders>
|
||||
</body>
|
||||
</html>
|
||||
)
|
||||
}
|
31
app/not-found.tsx
Normal file
31
app/not-found.tsx
Normal file
@ -0,0 +1,31 @@
|
||||
import Link from '@/components/Link'
|
||||
// import { PageSEO } from '@/components/SEO'
|
||||
|
||||
export default function NotFound() {
|
||||
return (
|
||||
<>
|
||||
{/* <PageSEO title="Page Not Found" description="Sorry we couldn't find this page :(" /> */}
|
||||
<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 pb-8 pt-6 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="/"
|
||||
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
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
10
app/page.tsx
Normal file
10
app/page.tsx
Normal file
@ -0,0 +1,10 @@
|
||||
import { sortedBlogPost, allCoreContent } from 'pliny/utils/contentlayer'
|
||||
import { allBlogs } from 'contentlayer/generated'
|
||||
import type { Blog } from 'contentlayer/generated'
|
||||
import Main from './Main'
|
||||
|
||||
export default async function Page() {
|
||||
const sortedPosts = sortedBlogPost(allBlogs) as Blog[]
|
||||
const posts = allCoreContent(sortedPosts)
|
||||
return <Main posts={posts} />
|
||||
}
|
33
app/projects/page.tsx
Normal file
33
app/projects/page.tsx
Normal file
@ -0,0 +1,33 @@
|
||||
import siteMetadata from '@/data/siteMetadata'
|
||||
import projectsData from '@/data/projectsData'
|
||||
import Card from '@/components/Card'
|
||||
|
||||
export default function Projects() {
|
||||
return (
|
||||
<>
|
||||
<div className="divide-y divide-gray-200 dark:divide-gray-700">
|
||||
<div className="space-y-2 pb-8 pt-6 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="-m-4 flex flex-wrap">
|
||||
{projectsData.map((d) => (
|
||||
<Card
|
||||
key={d.title}
|
||||
title={d.title}
|
||||
description={d.description}
|
||||
imgSrc={d.imgSrc}
|
||||
href={d.href}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
41
app/tags/page.tsx
Normal file
41
app/tags/page.tsx
Normal file
@ -0,0 +1,41 @@
|
||||
import { getAllTags } from 'pliny/utils/contentlayer'
|
||||
import Link from '@/components/Link'
|
||||
// import { PageSEO } from '@/components/SEO'
|
||||
import Tag from '@/components/Tag'
|
||||
import siteMetadata from '@/data/siteMetadata'
|
||||
import { kebabCase } from 'pliny/utils/kebabCase'
|
||||
import { allBlogs } from 'contentlayer/generated'
|
||||
|
||||
export default async function Page() {
|
||||
const tags = await getAllTags(allBlogs)
|
||||
const sortedTags = Object.keys(tags).sort((a, b) => tags[b] - tags[a])
|
||||
return (
|
||||
<>
|
||||
{/* <PageSEO title={`Tags - ${siteMetadata.author}`} description="Things I blog about" /> */}
|
||||
<div className="flex flex-col items-start justify-start divide-y divide-gray-200 dark:divide-gray-700 md:mt-24 md:flex-row md:items-center md:justify-center md:space-x-6 md:divide-y-0">
|
||||
<div className="space-x-2 pb-8 pt-6 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:border-r-2 md:px-6 md:text-6xl md:leading-14">
|
||||
Tags
|
||||
</h1>
|
||||
</div>
|
||||
<div className="flex max-w-lg flex-wrap">
|
||||
{Object.keys(tags).length === 0 && 'No tags found.'}
|
||||
{sortedTags.map((t) => {
|
||||
return (
|
||||
<div key={t} className="mb-2 mr-5 mt-2">
|
||||
<Tag text={t} />
|
||||
<Link
|
||||
href={`/tags/${kebabCase(t)}`}
|
||||
className="-ml-2 text-sm font-semibold uppercase text-gray-600 dark:text-gray-300"
|
||||
aria-label={`View posts tagged ${t}`}
|
||||
>
|
||||
{` (${tags[t]})`}
|
||||
</Link>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
12
app/theme-providers.tsx
Normal file
12
app/theme-providers.tsx
Normal file
@ -0,0 +1,12 @@
|
||||
'use client'
|
||||
|
||||
import { ThemeProvider } from 'next-themes'
|
||||
import siteMetadata from '@/data/siteMetadata'
|
||||
|
||||
export function ThemeProviders({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<ThemeProvider attribute="class" defaultTheme={siteMetadata.theme} enableSystem>
|
||||
{children}
|
||||
</ThemeProvider>
|
||||
)
|
||||
}
|
Reference in New Issue
Block a user