✨ Posts
This commit is contained in:
parent
05ae2783cf
commit
bd6f24c286
@ -34,7 +34,7 @@ export default function Home() {
|
||||
</Grid>
|
||||
<Grid item xs={12} md={6} sx={{ display: "flex", justifyContent: "end" }}>
|
||||
<Box>
|
||||
<Image src="smartsheep.svg" alt="Logo" width={256} height={256} />
|
||||
<Image src="/smartsheep.svg" alt="Logo" width={256} height={256} />
|
||||
</Box>
|
||||
</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 MenuIcon from "@mui/icons-material/Menu";
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
|
||||
function HideOnScroll(props: {
|
||||
window?: () => Window;
|
||||
@ -105,11 +106,13 @@ export default function AppShell({ children }: {
|
||||
aria-label="menu"
|
||||
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>
|
||||
|
||||
<Typography variant="h6" component="div" sx={{ flexGrow: 1 }}>
|
||||
{SITE_NAME}
|
||||
<Link href="/">
|
||||
{SITE_NAME}
|
||||
</Link>
|
||||
</Typography>
|
||||
|
||||
<IconButton
|
||||
|
@ -2,15 +2,12 @@
|
||||
|
||||
import ChevronLeftIcon from "@mui/icons-material/ChevronLeft";
|
||||
import ChevronRightIcon from "@mui/icons-material/ChevronRight";
|
||||
import InboxIcon from "@mui/icons-material/MoveToInbox";
|
||||
import MailIcon from "@mui/icons-material/Mail";
|
||||
import {
|
||||
Box,
|
||||
Divider,
|
||||
Drawer,
|
||||
IconButton,
|
||||
List,
|
||||
ListItem,
|
||||
ListItemButton,
|
||||
ListItemIcon,
|
||||
ListItemText,
|
||||
@ -20,6 +17,7 @@ import {
|
||||
import { theme } from "@/app/theme";
|
||||
import { ReactNode } from "react";
|
||||
import HomeIcon from "@mui/icons-material/Home";
|
||||
import ArticleIcon from "@mui/icons-material/Article";
|
||||
import Link from "next/link";
|
||||
|
||||
export interface NavigationItem {
|
||||
@ -30,7 +28,8 @@ export interface NavigationItem {
|
||||
|
||||
export const DRAWER_WIDTH = 320;
|
||||
export const NAVIGATION_ITEMS: NavigationItem[] = [
|
||||
{ icon: <HomeIcon />, title: "首页", link: "/" }
|
||||
{ icon: <HomeIcon />, title: "首页", link: "/" },
|
||||
{ icon: <ArticleIcon />, title: "新闻", link: "/posts" },
|
||||
];
|
||||
|
||||
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} */
|
||||
const nextConfig = {};
|
||||
/** @type {import("next").NextConfig} */
|
||||
const nextConfig = {
|
||||
pageExtensions: ["js", "jsx", "mdx", "ts", "tsx"],
|
||||
images: {
|
||||
remotePatterns: [
|
||||
{
|
||||
protocol: "https",
|
||||
hostname: "**",
|
||||
},
|
||||
],
|
||||
}
|
||||
};
|
||||
|
||||
export default nextConfig;
|
||||
|
15
package.json
15
package.json
@ -13,23 +13,32 @@
|
||||
"@emotion/react": "^11.11.3",
|
||||
"@emotion/styled": "^11.11.0",
|
||||
"@fontsource/roboto": "^5.0.8",
|
||||
"@mdx-js/loader": "^3.0.1",
|
||||
"@mdx-js/react": "^3.0.1",
|
||||
"@mui/icons-material": "^5.15.10",
|
||||
"@mui/material": "^5.15.10",
|
||||
"@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",
|
||||
"react": "^18",
|
||||
"react-dom": "^18"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^5",
|
||||
"@tailwindcss/typography": "^0.5.10",
|
||||
"@types/node": "^20",
|
||||
"@types/react": "^18",
|
||||
"@types/react-dom": "^18",
|
||||
"autoprefixer": "^10.0.1",
|
||||
"eslint": "^8",
|
||||
"eslint-config-next": "14.1.0",
|
||||
"postcss": "^8",
|
||||
"tailwindcss": "^3.3.0",
|
||||
"eslint": "^8",
|
||||
"eslint-config-next": "14.1.0"
|
||||
"typescript": "^5"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^17.0.0 || ^18.0.0",
|
||||
|
@ -6,15 +6,6 @@ const config: Config = {
|
||||
"./components/**/*.{js,ts,jsx,tsx,mdx}",
|
||||
"./app/**/*.{js,ts,jsx,tsx,mdx}",
|
||||
],
|
||||
theme: {
|
||||
extend: {
|
||||
backgroundImage: {
|
||||
"gradient-radial": "radial-gradient(var(--tw-gradient-stops))",
|
||||
"gradient-conic":
|
||||
"conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))",
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: [],
|
||||
plugins: [require("@tailwindcss/typography")],
|
||||
};
|
||||
export default config;
|
||||
|
Loading…
Reference in New Issue
Block a user