♻️ 使用 NextJS 重构 #1
| @@ -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 }}> | ||||||
|  |               <Link href="/"> | ||||||
|                 {SITE_NAME} |                 {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; | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user