✨ Posts
This commit is contained in:
parent
05ae2783cf
commit
bd6f24c286
@ -34,7 +34,7 @@ export default function Home() {
|
|||||||
</Grid>
|
</Grid>
|
||||||
<Grid item xs={12} md={6} sx={{ display: "flex", justifyContent: "end" }}>
|
<Grid item xs={12} md={6} sx={{ display: "flex", justifyContent: "end" }}>
|
||||||
<Box>
|
<Box>
|
||||||
<Image src="smartsheep.svg" alt="Logo" width={256} height={256} />
|
<Image src="/smartsheep.svg" alt="Logo" width={256} height={256} />
|
||||||
</Box>
|
</Box>
|
||||||
</Grid>
|
</Grid>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
39
app/posts/[id]/page.tsx
Normal file
39
app/posts/[id]/page.tsx
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
import { Box, Card, CardContent, CardMedia, Divider, Typography } from "@mui/material";
|
||||||
|
import { getSinglePost } from "@/content/posts";
|
||||||
|
import Image from "next/image";
|
||||||
|
import PostContent from "@/components/posts/PostContent";
|
||||||
|
|
||||||
|
export default function PostDetailPage({ params }: { params: { id: string } }) {
|
||||||
|
const post = getSinglePost(params.id);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card>
|
||||||
|
{
|
||||||
|
post.thumbnail &&
|
||||||
|
<CardMedia sx={{ height: 360, position: "relative" }} title={post.title}>
|
||||||
|
<Image
|
||||||
|
fill
|
||||||
|
src={post.thumbnail}
|
||||||
|
alt={post.title}
|
||||||
|
style={{ objectFit: "cover" }}
|
||||||
|
/>
|
||||||
|
</CardMedia>
|
||||||
|
}
|
||||||
|
|
||||||
|
<CardContent sx={{ paddingX: 5, paddingY: 3 }}>
|
||||||
|
<Box>
|
||||||
|
<Typography gutterBottom variant="h5" component="h1">
|
||||||
|
{post.title}
|
||||||
|
</Typography>
|
||||||
|
<Typography color="text.secondary" variant="body2">
|
||||||
|
{post.description ?? "No description yet."}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
<Divider sx={{ my: 5 }} />
|
||||||
|
<Box component="article" sx={{ minWidth: 0 }}>
|
||||||
|
<PostContent content={post.content ?? ""} />
|
||||||
|
</Box>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
14
app/posts/layout.tsx
Normal file
14
app/posts/layout.tsx
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
import { Box, Container } from "@mui/material";
|
||||||
|
import { ReactNode } from "react";
|
||||||
|
|
||||||
|
export default function PostLayout({children}: Readonly<{
|
||||||
|
children: ReactNode;
|
||||||
|
}>) {
|
||||||
|
return (
|
||||||
|
<Container sx={{ display: "flex", justifyContent: "center", gap: 4, py: 4 }}>
|
||||||
|
<Box sx={{ flexGrow: 1, maxWidth: 720 }}>
|
||||||
|
{children}
|
||||||
|
</Box>
|
||||||
|
</Container>
|
||||||
|
)
|
||||||
|
}
|
40
app/posts/page.tsx
Normal file
40
app/posts/page.tsx
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
import { Button, Card, CardActions, CardContent, CardMedia, Typography } from "@mui/material";
|
||||||
|
import { getSortedPosts } from "@/content/posts";
|
||||||
|
import Image from "next/image";
|
||||||
|
import Link from "next/link";
|
||||||
|
|
||||||
|
export default function PostList() {
|
||||||
|
const posts = getSortedPosts();
|
||||||
|
|
||||||
|
return (
|
||||||
|
posts.map((post) => (
|
||||||
|
<Card key={post.id} sx={{ width: "100%" }}>
|
||||||
|
{
|
||||||
|
post.thumbnail &&
|
||||||
|
<CardMedia sx={{ height: 160, position: "relative" }} title={post.title}>
|
||||||
|
<Image
|
||||||
|
fill
|
||||||
|
src={post.thumbnail}
|
||||||
|
alt={post.title}
|
||||||
|
style={{ objectFit: "cover" }}
|
||||||
|
/>
|
||||||
|
</CardMedia>
|
||||||
|
}
|
||||||
|
|
||||||
|
<CardContent sx={{ paddingX: 5, paddingY: 3 }}>
|
||||||
|
<Typography gutterBottom variant="h5" component="h2">
|
||||||
|
{post.title}
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body2" color="text.secondary">
|
||||||
|
{post.description ?? "No description yet."}
|
||||||
|
</Typography>
|
||||||
|
</CardContent>
|
||||||
|
<CardActions sx={{ paddingX: 4, paddingBottom: 2 }}>
|
||||||
|
<Link href={`/posts/${post.id}`} passHref>
|
||||||
|
<Button>Read more</Button>
|
||||||
|
</Link>
|
||||||
|
</CardActions>
|
||||||
|
</Card>
|
||||||
|
))
|
||||||
|
);
|
||||||
|
}
|
@ -15,6 +15,7 @@ import { SITE_NAME } from "@/app/consts";
|
|||||||
import NavigationDrawer, { DRAWER_WIDTH, AppNavigationHeader, isMobileQuery } from "@/components/NavigationDrawer";
|
import NavigationDrawer, { DRAWER_WIDTH, AppNavigationHeader, isMobileQuery } from "@/components/NavigationDrawer";
|
||||||
import MenuIcon from "@mui/icons-material/Menu";
|
import MenuIcon from "@mui/icons-material/Menu";
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
|
import Link from "next/link";
|
||||||
|
|
||||||
function HideOnScroll(props: {
|
function HideOnScroll(props: {
|
||||||
window?: () => Window;
|
window?: () => Window;
|
||||||
@ -105,11 +106,13 @@ export default function AppShell({ children }: {
|
|||||||
aria-label="menu"
|
aria-label="menu"
|
||||||
sx={{ ml: isMobile ? 0.5 : 0, mr: 2 }}
|
sx={{ ml: isMobile ? 0.5 : 0, mr: 2 }}
|
||||||
>
|
>
|
||||||
<Image src="smartsheep.svg" alt="Logo" width={32} height={32} />
|
<Image src="/smartsheep.svg" alt="Logo" width={32} height={32} />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
|
|
||||||
<Typography variant="h6" component="div" sx={{ flexGrow: 1 }}>
|
<Typography variant="h6" component="div" sx={{ flexGrow: 1 }}>
|
||||||
{SITE_NAME}
|
<Link href="/">
|
||||||
|
{SITE_NAME}
|
||||||
|
</Link>
|
||||||
</Typography>
|
</Typography>
|
||||||
|
|
||||||
<IconButton
|
<IconButton
|
||||||
|
@ -2,15 +2,12 @@
|
|||||||
|
|
||||||
import ChevronLeftIcon from "@mui/icons-material/ChevronLeft";
|
import ChevronLeftIcon from "@mui/icons-material/ChevronLeft";
|
||||||
import ChevronRightIcon from "@mui/icons-material/ChevronRight";
|
import ChevronRightIcon from "@mui/icons-material/ChevronRight";
|
||||||
import InboxIcon from "@mui/icons-material/MoveToInbox";
|
|
||||||
import MailIcon from "@mui/icons-material/Mail";
|
|
||||||
import {
|
import {
|
||||||
Box,
|
Box,
|
||||||
Divider,
|
Divider,
|
||||||
Drawer,
|
Drawer,
|
||||||
IconButton,
|
IconButton,
|
||||||
List,
|
List,
|
||||||
ListItem,
|
|
||||||
ListItemButton,
|
ListItemButton,
|
||||||
ListItemIcon,
|
ListItemIcon,
|
||||||
ListItemText,
|
ListItemText,
|
||||||
@ -20,6 +17,7 @@ import {
|
|||||||
import { theme } from "@/app/theme";
|
import { theme } from "@/app/theme";
|
||||||
import { ReactNode } from "react";
|
import { ReactNode } from "react";
|
||||||
import HomeIcon from "@mui/icons-material/Home";
|
import HomeIcon from "@mui/icons-material/Home";
|
||||||
|
import ArticleIcon from "@mui/icons-material/Article";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
|
|
||||||
export interface NavigationItem {
|
export interface NavigationItem {
|
||||||
@ -30,7 +28,8 @@ export interface NavigationItem {
|
|||||||
|
|
||||||
export const DRAWER_WIDTH = 320;
|
export const DRAWER_WIDTH = 320;
|
||||||
export const NAVIGATION_ITEMS: NavigationItem[] = [
|
export const NAVIGATION_ITEMS: NavigationItem[] = [
|
||||||
{ icon: <HomeIcon />, title: "首页", link: "/" }
|
{ icon: <HomeIcon />, title: "首页", link: "/" },
|
||||||
|
{ icon: <ArticleIcon />, title: "新闻", link: "/posts" },
|
||||||
];
|
];
|
||||||
|
|
||||||
export const AppNavigationHeader = styled("div")(({ theme }) => ({
|
export const AppNavigationHeader = styled("div")(({ theme }) => ({
|
||||||
|
7
components/posts/PostContent.tsx
Normal file
7
components/posts/PostContent.tsx
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import MuiMarkdown from "mui-markdown";
|
||||||
|
|
||||||
|
export default function PostContent({ content }: { content: string }) {
|
||||||
|
return <MuiMarkdown>{content}</MuiMarkdown>;
|
||||||
|
}
|
47
content/posts.ts
Normal file
47
content/posts.ts
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
import fs from "fs";
|
||||||
|
import path from "path";
|
||||||
|
import matter from "gray-matter";
|
||||||
|
|
||||||
|
const postsDirectory = path.join(process.cwd(), "content", "posts");
|
||||||
|
|
||||||
|
export interface Post {
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
thumbnail?: string;
|
||||||
|
description?: string;
|
||||||
|
content?: string;
|
||||||
|
date: Date;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getSortedPosts() {
|
||||||
|
const fileNames = fs.readdirSync(postsDirectory);
|
||||||
|
const allPostsData: Post[] = fileNames.map((fileName) => {
|
||||||
|
const id = fileName.replace(/\.md$/, "");
|
||||||
|
|
||||||
|
const fullPath = path.join(postsDirectory, fileName);
|
||||||
|
const fileContents = fs.readFileSync(fullPath, "utf8");
|
||||||
|
|
||||||
|
const matterResult = matter(fileContents);
|
||||||
|
|
||||||
|
return {
|
||||||
|
id,
|
||||||
|
...matterResult.data
|
||||||
|
} as Post;
|
||||||
|
});
|
||||||
|
|
||||||
|
return allPostsData.sort((a, b) => {
|
||||||
|
return a.date < b.date ? 1 : -1;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getSinglePost(id: string) {
|
||||||
|
const fullPath = path.join(postsDirectory, id + ".md");
|
||||||
|
const fileContents = fs.readFileSync(fullPath, "utf8");
|
||||||
|
const matterResult = matter(fileContents);
|
||||||
|
|
||||||
|
return {
|
||||||
|
id,
|
||||||
|
content: matterResult.content,
|
||||||
|
...matterResult.data,
|
||||||
|
} as Post;
|
||||||
|
}
|
14
content/posts/initial.md
Normal file
14
content/posts/initial.md
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
---
|
||||||
|
thumbnail: 'https://images.unsplash.com/photo-1707344088547-3cf7cea5ca49?q=80&w=2970&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDF8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D'
|
||||||
|
title: 'Two Forms of Pre-rendering'
|
||||||
|
date: '2020-01-01'
|
||||||
|
---
|
||||||
|
|
||||||
|
# Woah!
|
||||||
|
|
||||||
|
Next.js has two forms of pre-rendering: **Static Generation** and **Server-side Rendering**. The difference is in **when** it generates the HTML for a page.
|
||||||
|
|
||||||
|
- **Static Generation** is the pre-rendering method that generates the HTML at **build time**. The pre-rendered HTML is then _reused_ on each request.
|
||||||
|
- **Server-side Rendering** is the pre-rendering method that generates the HTML on **each request**.
|
||||||
|
|
||||||
|
Importantly, Next.js lets you **choose** which pre-rendering form to use for each page. You can create a "hybrid" Next.js app by using Static Generation for most pages and using Server-side Rendering for others.
|
@ -1,4 +1,14 @@
|
|||||||
/** @type {import('next').NextConfig} */
|
/** @type {import("next").NextConfig} */
|
||||||
const nextConfig = {};
|
const nextConfig = {
|
||||||
|
pageExtensions: ["js", "jsx", "mdx", "ts", "tsx"],
|
||||||
|
images: {
|
||||||
|
remotePatterns: [
|
||||||
|
{
|
||||||
|
protocol: "https",
|
||||||
|
hostname: "**",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
export default nextConfig;
|
export default nextConfig;
|
||||||
|
15
package.json
15
package.json
@ -13,23 +13,32 @@
|
|||||||
"@emotion/react": "^11.11.3",
|
"@emotion/react": "^11.11.3",
|
||||||
"@emotion/styled": "^11.11.0",
|
"@emotion/styled": "^11.11.0",
|
||||||
"@fontsource/roboto": "^5.0.8",
|
"@fontsource/roboto": "^5.0.8",
|
||||||
|
"@mdx-js/loader": "^3.0.1",
|
||||||
|
"@mdx-js/react": "^3.0.1",
|
||||||
"@mui/icons-material": "^5.15.10",
|
"@mui/icons-material": "^5.15.10",
|
||||||
"@mui/material": "^5.15.10",
|
"@mui/material": "^5.15.10",
|
||||||
"@mui/material-nextjs": "^5.15.11",
|
"@mui/material-nextjs": "^5.15.11",
|
||||||
|
"@next/mdx": "^14.1.0",
|
||||||
|
"@types/mdx": "^2.0.11",
|
||||||
|
"gray-matter": "^4.0.3",
|
||||||
|
"html-react-parser": "^5.1.7",
|
||||||
|
"marked": "^12.0.0",
|
||||||
|
"mui-markdown": "^1.1.13",
|
||||||
"next": "14.1.0",
|
"next": "14.1.0",
|
||||||
"react": "^18",
|
"react": "^18",
|
||||||
"react-dom": "^18"
|
"react-dom": "^18"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"typescript": "^5",
|
"@tailwindcss/typography": "^0.5.10",
|
||||||
"@types/node": "^20",
|
"@types/node": "^20",
|
||||||
"@types/react": "^18",
|
"@types/react": "^18",
|
||||||
"@types/react-dom": "^18",
|
"@types/react-dom": "^18",
|
||||||
"autoprefixer": "^10.0.1",
|
"autoprefixer": "^10.0.1",
|
||||||
|
"eslint": "^8",
|
||||||
|
"eslint-config-next": "14.1.0",
|
||||||
"postcss": "^8",
|
"postcss": "^8",
|
||||||
"tailwindcss": "^3.3.0",
|
"tailwindcss": "^3.3.0",
|
||||||
"eslint": "^8",
|
"typescript": "^5"
|
||||||
"eslint-config-next": "14.1.0"
|
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"react": "^17.0.0 || ^18.0.0",
|
"react": "^17.0.0 || ^18.0.0",
|
||||||
|
@ -6,15 +6,6 @@ const config: Config = {
|
|||||||
"./components/**/*.{js,ts,jsx,tsx,mdx}",
|
"./components/**/*.{js,ts,jsx,tsx,mdx}",
|
||||||
"./app/**/*.{js,ts,jsx,tsx,mdx}",
|
"./app/**/*.{js,ts,jsx,tsx,mdx}",
|
||||||
],
|
],
|
||||||
theme: {
|
plugins: [require("@tailwindcss/typography")],
|
||||||
extend: {
|
|
||||||
backgroundImage: {
|
|
||||||
"gradient-radial": "radial-gradient(var(--tw-gradient-stops))",
|
|
||||||
"gradient-conic":
|
|
||||||
"conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
plugins: [],
|
|
||||||
};
|
};
|
||||||
export default config;
|
export default config;
|
||||||
|
Loading…
Reference in New Issue
Block a user