♻️ Use sanity
This commit is contained in:
		
							
								
								
									
										4
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										4
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							| @@ -35,4 +35,6 @@ yarn-error.log* | ||||
| *.tsbuildinfo | ||||
| next-env.d.ts | ||||
|  | ||||
| bun.lockb | ||||
| bun.lockb | ||||
|  | ||||
| .env | ||||
| @@ -2,4 +2,4 @@ | ||||
|   "tabWidth": 2, | ||||
|   "singleQuote": false, | ||||
|   "printWidth": 120 | ||||
| } | ||||
| } | ||||
|   | ||||
							
								
								
									
										17
									
								
								app/console/[[...index]]/page.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								app/console/[[...index]]/page.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,17 @@ | ||||
| 'use client' | ||||
|  | ||||
| /** | ||||
|  * This route is responsible for the built-in authoring environment using Sanity Studio. | ||||
|  * All routes under your studio path is handled by this file using Next.js' catch-all routes: | ||||
|  * https://nextjs.org/docs/routing/dynamic-routes#catch-all-routes | ||||
|  * | ||||
|  * You can learn more about the next-sanity package here: | ||||
|  * https://github.com/sanity-io/next-sanity | ||||
|  */ | ||||
|  | ||||
| import { NextStudio } from 'next-sanity/studio' | ||||
| import config from '../../../sanity.config' | ||||
|  | ||||
| export default function StudioPage() { | ||||
|   return <NextStudio config={config} /> | ||||
| } | ||||
| @@ -12,10 +12,20 @@ export interface RelatedAccount { | ||||
|  | ||||
| export const SITE_NAME = "Goatshed"; | ||||
| export const SITE_DESCRIPTION = "山羊寒舍,在这里最终智羊工作室的最新动态。"; | ||||
| export const SITE_URL = "https://smartsheep.studio" | ||||
| export const SITE_URL = "https://smartsheep.studio"; | ||||
|  | ||||
| export const RELATED_ACCOUNTS: RelatedAccount[] = [ | ||||
|   { icon: <GitHubIcon />, platform: "GitHub", accountName: "@smartsheep-hq", link: "https://github.com/smartsheep-hq" }, | ||||
|   { icon: <TwitterIcon />, platform: "Twitter", accountName: "@littlesheepovo", link: "https://twitter.com/littlesheepovo" }, | ||||
|   { icon: <CoffeeIcon />, platform: "Ko-fi", accountName: "@littlesheep2code", link: "https://ko-fi.com/littlesheep2code" }, | ||||
| ]; | ||||
|   { | ||||
|     icon: <TwitterIcon />, | ||||
|     platform: "Twitter", | ||||
|     accountName: "@littlesheepovo", | ||||
|     link: "https://twitter.com/littlesheepovo", | ||||
|   }, | ||||
|   { | ||||
|     icon: <CoffeeIcon />, | ||||
|     platform: "Ko-fi", | ||||
|     accountName: "@littlesheep2code", | ||||
|     link: "https://ko-fi.com/littlesheep2code", | ||||
|   }, | ||||
| ]; | ||||
|   | ||||
| @@ -8,7 +8,7 @@ export async function GET() { | ||||
|     description: SITE_DESCRIPTION, | ||||
|     site_url: SITE_URL, | ||||
|     feed_url: `${SITE_URL}/feed`, | ||||
|     language: "zh-CN" | ||||
|     language: "zh-CN", | ||||
|   }); | ||||
|  | ||||
|   getSortedPosts().forEach((item) => { | ||||
| @@ -22,7 +22,7 @@ export async function GET() { | ||||
|  | ||||
|   return new Response(feed.xml(), { | ||||
|     headers: { | ||||
|       "content-type": "application/xml" | ||||
|     } | ||||
|       "content-type": "application/xml", | ||||
|     }, | ||||
|   }); | ||||
| } | ||||
| } | ||||
|   | ||||
| @@ -1,3 +1,3 @@ | ||||
| @tailwind base; | ||||
| @tailwind components; | ||||
| @tailwind utilities; | ||||
| @tailwind utilities; | ||||
|   | ||||
| @@ -14,6 +14,7 @@ import "@fontsource/roboto/700.css"; | ||||
| import "./globals.css"; | ||||
|  | ||||
| import AppShell from "@/components/AppShell"; | ||||
| import NextTopLoader from "nextjs-toploader"; | ||||
|  | ||||
| export const runtime = "edge"; | ||||
|  | ||||
| @@ -33,6 +34,7 @@ export default function RootLayout({ children }: Readonly<{ | ||||
|     <body> | ||||
|     <AppRouterCacheProvider> | ||||
|       <CssBaseline /> | ||||
|       <NextTopLoader showAtBottom color="#4a5099" /> | ||||
|       <ThemeProvider theme={theme}> | ||||
|         <AppShell>{children}</AppShell> | ||||
|       </ThemeProvider> | ||||
|   | ||||
							
								
								
									
										31
									
								
								app/page.tsx
									
									
									
									
									
								
							
							
						
						
									
										31
									
								
								app/page.tsx
									
									
									
									
									
								
							| @@ -2,14 +2,15 @@ import { | ||||
|   Avatar, | ||||
|   Box, | ||||
|   Button, | ||||
|   Card, colors, | ||||
|   Card, | ||||
|   colors, | ||||
|   Container, | ||||
|   Grid, | ||||
|   List, | ||||
|   ListItemAvatar, | ||||
|   ListItemButton, | ||||
|   ListItemText, | ||||
|   Typography | ||||
|   Typography, | ||||
| } from "@mui/material"; | ||||
| import { RELATED_ACCOUNTS } from "@/app/consts"; | ||||
| import Image from "next/image"; | ||||
| @@ -18,19 +19,18 @@ import Link from "next/link"; | ||||
| export default function Home() { | ||||
|   return ( | ||||
|     <Container sx={{ scrollBehavior: "smooth", px: 5 }}> | ||||
|       <Grid | ||||
|         container | ||||
|         id="introduce" | ||||
|         alignItems="center" | ||||
|         sx={{ height: "calc(100vh - 64px)" }} | ||||
|       > | ||||
|       <Grid container id="introduce" alignItems="center" sx={{ height: "calc(100vh - 64px)" }}> | ||||
|         <Grid item xs={12} sm={6} sx={{ textAlign: { xs: "center", sm: "initial" } }}> | ||||
|           <Typography variant="h1" gutterBottom>你好呀 👋</Typography> | ||||
|           <Typography variant="h1" gutterBottom> | ||||
|             你好呀 👋 | ||||
|           </Typography> | ||||
|           <Typography paragraph> | ||||
|             欢迎来到 SmartSheep Studio 的官方网站!在这里了解,订阅,跟踪我们的最新消息。 | ||||
|             接触我们最大的官方社区,并且尝试最新产品,参与各种活动,提供反馈,让我们更好的服务您。 | ||||
|           </Typography> | ||||
|           <Button variant="contained" href="#about-us" size="large">探索更多</Button> | ||||
|           <Button variant="contained" href="#about-us" size="large"> | ||||
|             探索更多 | ||||
|           </Button> | ||||
|         </Grid> | ||||
|         <Grid | ||||
|           item | ||||
| @@ -44,12 +44,7 @@ export default function Home() { | ||||
|         </Grid> | ||||
|       </Grid> | ||||
|  | ||||
|       <Grid | ||||
|         container | ||||
|         id="about-us" | ||||
|         alignItems="center" | ||||
|         sx={{ height: "calc(100vh - 64px)" }} | ||||
|       > | ||||
|       <Grid container id="about-us" alignItems="center" sx={{ height: "calc(100vh - 64px)" }}> | ||||
|         <Grid item xs={12} sm={6} sx={{ display: "flex", justifyContent: { xs: "center", sm: "end" } }}> | ||||
|           <Card sx={{ flexGrow: 1, mr: { xs: 0, sm: 4, md: 8 } }}> | ||||
|             <List sx={{ width: "100%", bgcolor: "background.paper" }}> | ||||
| @@ -67,7 +62,9 @@ export default function Home() { | ||||
|           </Card> | ||||
|         </Grid> | ||||
|         <Grid item xs={12} sm={6} sx={{ textAlign: { xs: "center", sm: "initial" } }}> | ||||
|           <Typography variant="h1" gutterBottom>关于我们</Typography> | ||||
|           <Typography variant="h1" gutterBottom> | ||||
|             关于我们 | ||||
|           </Typography> | ||||
|           <Typography paragraph> | ||||
|             我们是一群充满活力、对开源充满热情的开发者。成立于 2019 年。自那年起我们一直在开发让人喜欢的开源软件。 | ||||
|             在我们这里,“取之于开源,用之于开源” 不仅是原则,更是我们信仰的座右铭。 | ||||
|   | ||||
| @@ -1,20 +1,32 @@ | ||||
| import { Box, Card, CardContent, CardMedia, Divider, Typography } from "@mui/material"; | ||||
| import { getSinglePost } from "@/content/posts"; | ||||
| import Image from "next/image"; | ||||
| import { Box, Card, CardContent, CardMedia, Chip, Divider, Stack, Typography } from "@mui/material"; | ||||
| import { client } from "@/sanity/lib/client"; | ||||
| import PostContent from "@/components/posts/PostContent"; | ||||
| import Image from "next/image"; | ||||
|  | ||||
| export default function PostDetailPage({ params }: { params: { id: string } }) { | ||||
|   const post = getSinglePost(params.id); | ||||
| export default async function PostDetailPage({ params }: { params: { id: string } }) { | ||||
|   const post = await client.fetch<any>(`*[_type == "post" && slug.current == $slug][0] { | ||||
|     title, description, slug, body, author, publishedAt, | ||||
|     mainImage { | ||||
|       asset -> { | ||||
|         _id, | ||||
|         url | ||||
|       }, | ||||
|       alt | ||||
|     }, | ||||
|     "categories": categories[]->title, | ||||
|     "author_name": author->name, | ||||
|     "author_image": author->image | ||||
|   }`, { slug: params.id }); | ||||
|  | ||||
|   return ( | ||||
|     <Card> | ||||
|       { | ||||
|         post.thumbnail && | ||||
|         <CardMedia sx={{ height: 360, position: "relative" }} title={post.title}> | ||||
|         post.mainImage && | ||||
|         <CardMedia sx={{ height: 360, position: "relative" }} title={post.mainImage.alt}> | ||||
|           <Image | ||||
|             fill | ||||
|             src={post.thumbnail} | ||||
|             alt={post.title} | ||||
|             src={post.mainImage.asset.url} | ||||
|             alt={post.mainImage.alt} | ||||
|             style={{ objectFit: "cover" }} | ||||
|           /> | ||||
|         </CardMedia> | ||||
| @@ -22,18 +34,22 @@ export default function PostDetailPage({ params }: { params: { id: string } }) { | ||||
|  | ||||
|       <CardContent sx={{ paddingX: 5, paddingY: 3 }}> | ||||
|         <Box> | ||||
|           <Typography gutterBottom variant="h2"> | ||||
|           <Typography variant="h2"> | ||||
|             {post.title} | ||||
|           </Typography> | ||||
|  | ||||
|           <Stack direction="row" sx={{ mx: -0.5, mt: 1, mb: 1.2 }}> | ||||
|             {post.categories.map((category: string, idx: number) => <Chip size="small" label={category} key={idx} />)} | ||||
|           </Stack> | ||||
|           <Typography color="text.secondary" variant="body2"> | ||||
|             {post.description ?? "No description yet."} | ||||
|           </Typography> | ||||
|         </Box> | ||||
|         <Divider sx={{ my: 5 }} /> | ||||
|         <Divider sx={{ my: 2.5, mx: -5 }} /> | ||||
|         <Box component="article" className="prose max-w-none" sx={{ minWidth: 0 }}> | ||||
|           <PostContent content={post.content ?? ""} /> | ||||
|           <PostContent content={post.body} /> | ||||
|         </Box> | ||||
|       </CardContent> | ||||
|     </Card> | ||||
|   ); | ||||
| } | ||||
| } | ||||
|   | ||||
| @@ -1,14 +1,14 @@ | ||||
| import { Box, Container } from "@mui/material"; | ||||
| import { ReactNode } from "react"; | ||||
|  | ||||
| export default function PostLayout({children}: Readonly<{ | ||||
| export default function PostLayout({ | ||||
|   children, | ||||
| }: Readonly<{ | ||||
|   children: ReactNode; | ||||
| }>) { | ||||
|   return ( | ||||
|     <Container sx={{ display: "flex", justifyContent: "center", gap: 4, py: 2 }}> | ||||
|       <Box sx={{ flexGrow: 1, maxWidth: 720 }}> | ||||
|         {children} | ||||
|       </Box> | ||||
|       <Box sx={{ flexGrow: 1, maxWidth: 720 }}>{children}</Box> | ||||
|     </Container> | ||||
|   ) | ||||
| } | ||||
|   ); | ||||
| } | ||||
|   | ||||
| @@ -1,36 +1,52 @@ | ||||
| import { Button, Card, CardActions, CardContent, CardMedia, Typography } from "@mui/material"; | ||||
| import { getSortedPosts } from "@/content/posts"; | ||||
| import { Button, Card, CardActions, CardContent, CardMedia, Chip, Stack, Typography } from "@mui/material"; | ||||
| import { client } from "@/sanity/lib/client"; | ||||
| import Image from "next/image"; | ||||
| import Link from "next/link"; | ||||
|  | ||||
| export default function PostList() { | ||||
|   const posts = getSortedPosts(); | ||||
| export default async function PostList() { | ||||
|   const posts = await client.fetch<any[]>(`*[_type == "post"] { | ||||
|     title, description, slug, author, publishedAt, | ||||
|     mainImage { | ||||
|       asset -> { | ||||
|         _id, | ||||
|         url | ||||
|       }, | ||||
|       alt | ||||
|     }, | ||||
|     "categories": categories[]->title, | ||||
|     "author_name": author->name, | ||||
|     "author_image": author->image | ||||
|   }`); | ||||
|  | ||||
|   return ( | ||||
|     posts.map((post) => ( | ||||
|       <Card key={post.id} sx={{ width: "100%" }}> | ||||
|       <Card key={post.slug.current} sx={{ width: "100%" }}> | ||||
|         { | ||||
|           post.thumbnail && | ||||
|           <CardMedia sx={{ height: 160, position: "relative" }} title={post.title}> | ||||
|           post.mainImage && | ||||
|           <CardMedia sx={{ height: 160, position: "relative" }} title={post.mainImage.alt}> | ||||
|             <Image | ||||
|               fill | ||||
|               src={post.thumbnail} | ||||
|               alt={post.title} | ||||
|               src={post.mainImage.asset.url} | ||||
|               alt={post.mainImage.alt} | ||||
|               style={{ objectFit: "cover" }} | ||||
|             /> | ||||
|           </CardMedia> | ||||
|         } | ||||
|  | ||||
|         <CardContent sx={{ paddingX: 5, paddingY: 3 }}> | ||||
|           <Typography gutterBottom variant="h3"> | ||||
|           <Typography variant="h3"> | ||||
|             {post.title} | ||||
|           </Typography> | ||||
|  | ||||
|           <Stack direction="row" sx={{ mx: -0.5, mt: 1, mb: 1.2 }}> | ||||
|             {post.categories.map((category: string, idx: number) => <Chip size="small" label={category} key={idx} />)} | ||||
|           </Stack> | ||||
|           <Typography variant="body2" color="text.secondary"> | ||||
|             {post.description ?? "No description yet."} | ||||
|             {post.description ? post.description : "No description yet."} | ||||
|           </Typography> | ||||
|         </CardContent> | ||||
|         <CardActions sx={{ paddingX: 4, paddingBottom: 2 }}> | ||||
|           <Link href={`/p/${post.id}`} passHref> | ||||
|           <Link href={`/p/${post.slug.current}`} passHref> | ||||
|             <Button>Read more</Button> | ||||
|           </Link> | ||||
|         </CardActions> | ||||
|   | ||||
| @@ -10,20 +10,20 @@ export default function sitemap(): MetadataRoute.Sitemap { | ||||
|       url: `${SITE_URL}/`, | ||||
|       lastModified: new Date(), | ||||
|       changeFrequency: "weekly", | ||||
|       priority: 1 | ||||
|       priority: 1, | ||||
|     }, | ||||
|     { | ||||
|       url: `${SITE_URL}/posts`, | ||||
|       lastModified: new Date(), | ||||
|       changeFrequency: "daily", | ||||
|       priority: 0.8 | ||||
|       priority: 0.8, | ||||
|     }, | ||||
|  | ||||
|     ...posts.map((item: Post) => ({ | ||||
|       url: `${SITE_URL}/posts/${item.id}`, | ||||
|       lastModified: item.date, | ||||
|       changeFrequency: "daily" as any, | ||||
|       priority: 0.75 | ||||
|     })) | ||||
|       priority: 0.75, | ||||
|     })), | ||||
|   ]; | ||||
| } | ||||
| } | ||||
|   | ||||
| @@ -5,11 +5,11 @@ import { createTheme } from "@mui/material/styles"; | ||||
| export const theme = createTheme({ | ||||
|   palette: { | ||||
|     primary: { | ||||
|       main: "#49509e" | ||||
|       main: "#49509e", | ||||
|     }, | ||||
|     secondary: { | ||||
|       main: "#d43630" | ||||
|     } | ||||
|       main: "#d43630", | ||||
|     }, | ||||
|   }, | ||||
|   typography: { | ||||
|     h1: { fontSize: "2.5rem" }, | ||||
|   | ||||
| @@ -8,7 +8,9 @@ import { | ||||
|   AppBarProps as MuiAppBarProps, | ||||
|   useScrollTrigger, | ||||
|   IconButton, | ||||
|   styled, Box, useMediaQuery | ||||
|   styled, | ||||
|   Box, | ||||
|   useMediaQuery, | ||||
| } from "@mui/material"; | ||||
| import { ReactElement, ReactNode, useEffect, useState } from "react"; | ||||
| import { SITE_NAME } from "@/app/consts"; | ||||
| @@ -17,13 +19,10 @@ import MenuIcon from "@mui/icons-material/Menu"; | ||||
| import Image from "next/image"; | ||||
| import Link from "next/link"; | ||||
|  | ||||
| function HideOnScroll(props: { | ||||
|   window?: () => Window; | ||||
|   children: ReactElement; | ||||
| }) { | ||||
| function HideOnScroll(props: { window?: () => Window; children: ReactElement }) { | ||||
|   const { children, window } = props; | ||||
|   const trigger = useScrollTrigger({ | ||||
|     target: window ? window() : undefined | ||||
|     target: window ? window() : undefined, | ||||
|   }); | ||||
|  | ||||
|   return ( | ||||
| @@ -38,24 +37,25 @@ interface AppBarProps extends MuiAppBarProps { | ||||
| } | ||||
|  | ||||
| const ShellAppBar = styled(MuiAppBar, { | ||||
|   shouldForwardProp: (prop) => prop !== "open" | ||||
|   shouldForwardProp: (prop) => prop !== "open", | ||||
| })<AppBarProps>(({ theme, open }) => { | ||||
|   const isMobile = useMediaQuery(isMobileQuery); | ||||
|  | ||||
|   return ({ | ||||
|   return { | ||||
|     transition: theme.transitions.create(["margin", "width"], { | ||||
|       easing: theme.transitions.easing.sharp, | ||||
|       duration: theme.transitions.duration.leavingScreen | ||||
|       duration: theme.transitions.duration.leavingScreen, | ||||
|     }), | ||||
|     ...(!isMobile && open && { | ||||
|       width: `calc(100% - ${DRAWER_WIDTH}px)`, | ||||
|       transition: theme.transitions.create(["margin", "width"], { | ||||
|         easing: theme.transitions.easing.easeOut, | ||||
|         duration: theme.transitions.duration.enteringScreen | ||||
|     ...(!isMobile && | ||||
|       open && { | ||||
|         width: `calc(100% - ${DRAWER_WIDTH}px)`, | ||||
|         transition: theme.transitions.create(["margin", "width"], { | ||||
|           easing: theme.transitions.easing.easeOut, | ||||
|           duration: theme.transitions.duration.enteringScreen, | ||||
|         }), | ||||
|         marginRight: DRAWER_WIDTH, | ||||
|       }), | ||||
|       marginRight: DRAWER_WIDTH | ||||
|     }) | ||||
|   }); | ||||
|   }; | ||||
| }); | ||||
|  | ||||
| const AppMain = styled("main", { shouldForwardProp: (prop) => prop !== "open" })<{ | ||||
| @@ -63,28 +63,26 @@ const AppMain = styled("main", { shouldForwardProp: (prop) => prop !== "open" }) | ||||
| }>(({ theme, open }) => { | ||||
|   const isMobile = useMediaQuery(isMobileQuery); | ||||
|  | ||||
|   return ({ | ||||
|   return { | ||||
|     flexGrow: 1, | ||||
|     transition: theme.transitions.create("margin", { | ||||
|       easing: theme.transitions.easing.sharp, | ||||
|       duration: theme.transitions.duration.leavingScreen | ||||
|       duration: theme.transitions.duration.leavingScreen, | ||||
|     }), | ||||
|     marginRight: -DRAWER_WIDTH, | ||||
|     ...(!isMobile && open && { | ||||
|       transition: theme.transitions.create("margin", { | ||||
|         easing: theme.transitions.easing.easeOut, | ||||
|         duration: theme.transitions.duration.enteringScreen | ||||
|     ...(!isMobile && | ||||
|       open && { | ||||
|         transition: theme.transitions.create("margin", { | ||||
|           easing: theme.transitions.easing.easeOut, | ||||
|           duration: theme.transitions.duration.enteringScreen, | ||||
|         }), | ||||
|         marginRight: 0, | ||||
|       }), | ||||
|       marginRight: 0 | ||||
|     }), | ||||
|     position: "relative" | ||||
|   }); | ||||
|     position: "relative", | ||||
|   }; | ||||
| }); | ||||
|  | ||||
|  | ||||
| export default function AppShell({ children }: { | ||||
|   children: ReactNode, | ||||
| }) { | ||||
| export default function AppShell({ children }: { children: ReactNode }) { | ||||
|   let documentWindow: Window; | ||||
|  | ||||
|   const isMobile = useMediaQuery(isMobileQuery); | ||||
| @@ -110,9 +108,7 @@ export default function AppShell({ children }: { | ||||
|             </IconButton> | ||||
|  | ||||
|             <Typography variant="h6" component="div" sx={{ flexGrow: 1, fontSize: "1.2rem" }}> | ||||
|               <Link href="/"> | ||||
|                 {SITE_NAME} | ||||
|               </Link> | ||||
|               <Link href="/">{SITE_NAME}</Link> | ||||
|             </Typography> | ||||
|  | ||||
|             <IconButton | ||||
| @@ -140,4 +136,4 @@ export default function AppShell({ children }: { | ||||
|       </Box> | ||||
|     </> | ||||
|   ); | ||||
| } | ||||
| } | ||||
|   | ||||
| @@ -12,7 +12,7 @@ import { | ||||
|   ListItemIcon, | ||||
|   ListItemText, | ||||
|   styled, | ||||
|   useMediaQuery | ||||
|   useMediaQuery, | ||||
| } from "@mui/material"; | ||||
| import { theme } from "@/app/theme"; | ||||
| import { ReactNode } from "react"; | ||||
| @@ -42,27 +42,25 @@ export const AppNavigationHeader = styled("div")(({ theme }) => ({ | ||||
|   padding: theme.spacing(0, 1), | ||||
|   justifyContent: "flex-start", | ||||
|   height: 64, | ||||
|   ...theme.mixins.toolbar | ||||
|   ...theme.mixins.toolbar, | ||||
| })); | ||||
|  | ||||
| export function AppNavigation({ showClose, onClose }: { | ||||
|   showClose?: boolean, | ||||
|   onClose: () => void | ||||
| }) { | ||||
| export function AppNavigation({ showClose, onClose }: { showClose?: boolean; onClose: () => void }) { | ||||
|   return ( | ||||
|     <> | ||||
|       <AppNavigationHeader> | ||||
|         { | ||||
|           showClose && | ||||
|         {showClose && ( | ||||
|           <IconButton onClick={onClose}> | ||||
|             {theme.direction === "rtl" ? <ChevronLeftIcon /> : <ChevronRightIcon />} | ||||
|           </IconButton> | ||||
|         } | ||||
|         )} | ||||
|       </AppNavigationHeader> | ||||
|       <Divider /> | ||||
|       <List> | ||||
|         {NAVIGATION_ITEMS.map((item, idx) => { | ||||
|           return item.divider ? <Divider key={idx} /> : ( | ||||
|           return item.divider ? ( | ||||
|             <Divider key={idx} /> | ||||
|           ) : ( | ||||
|             <Link key={idx} href={item.link ?? "/"} passHref> | ||||
|               <ListItemButton> | ||||
|                 <ListItemIcon>{item.icon}</ListItemIcon> | ||||
| @@ -78,10 +76,7 @@ export function AppNavigation({ showClose, onClose }: { | ||||
|  | ||||
| export const isMobileQuery = theme.breakpoints.down("md"); | ||||
|  | ||||
| export default function NavigationDrawer({ open, onClose }: { | ||||
|   open: boolean, | ||||
|   onClose: () => void, | ||||
| }) { | ||||
| export default function NavigationDrawer({ open, onClose }: { open: boolean; onClose: () => void }) { | ||||
|   const isMobile = useMediaQuery(isMobileQuery); | ||||
|  | ||||
|   return isMobile ? ( | ||||
| @@ -96,8 +91,8 @@ export default function NavigationDrawer({ open, onClose }: { | ||||
|         sx={{ | ||||
|           "& .MuiDrawer-paper": { | ||||
|             boxSizing: "border-box", | ||||
|             width: DRAWER_WIDTH | ||||
|           } | ||||
|             width: DRAWER_WIDTH, | ||||
|           }, | ||||
|         }} | ||||
|       > | ||||
|         <AppNavigation onClose={onClose} /> | ||||
| @@ -112,11 +107,11 @@ export default function NavigationDrawer({ open, onClose }: { | ||||
|         width: DRAWER_WIDTH, | ||||
|         flexShrink: 0, | ||||
|         "& .MuiDrawer-paper": { | ||||
|           width: DRAWER_WIDTH | ||||
|         } | ||||
|           width: DRAWER_WIDTH, | ||||
|         }, | ||||
|       }} | ||||
|     > | ||||
|       <AppNavigation showClose onClose={onClose} /> | ||||
|     </Drawer> | ||||
|   ); | ||||
| } | ||||
| } | ||||
|   | ||||
| @@ -1,7 +1,7 @@ | ||||
| "use client"; | ||||
|  | ||||
| import Markdown from "react-markdown"; | ||||
| import { PortableText } from "@portabletext/react"; | ||||
|  | ||||
| export default function PostContent({ content }: { content: string }) { | ||||
|   return <Markdown>{content}</Markdown>; | ||||
| } | ||||
| export default function PostContent({ content }: { content: any }) { | ||||
|   return <PortableText value={content} />; | ||||
| } | ||||
|   | ||||
| @@ -1,47 +0,0 @@ | ||||
| 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; | ||||
| } | ||||
| @@ -1,14 +0,0 @@ | ||||
| --- | ||||
| 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. | ||||
| @@ -5,9 +5,9 @@ const nextConfig = { | ||||
|     remotePatterns: [ | ||||
|       { | ||||
|         protocol: "https", | ||||
|         hostname: "**" | ||||
|       } | ||||
|     ] | ||||
|         hostname: "**", | ||||
|       }, | ||||
|     ], | ||||
|   }, | ||||
|   async rewrites() { | ||||
|     return [ | ||||
| @@ -15,9 +15,9 @@ const nextConfig = { | ||||
|       { source: "/rss.xml", destination: "/feed.xml" }, | ||||
|       { source: "/feed.xml", destination: "/feed" }, | ||||
|  | ||||
|       { source: "/p/:id", destination: "/posts/:id" } | ||||
|       { source: "/p/:id", destination: "/posts/:id" }, | ||||
|     ]; | ||||
|   } | ||||
|   }, | ||||
| }; | ||||
|  | ||||
| export default nextConfig; | ||||
|   | ||||
							
								
								
									
										18
									
								
								package.json
									
									
									
									
									
								
							
							
						
						
									
										18
									
								
								package.json
									
									
									
									
									
								
							| @@ -19,17 +19,29 @@ | ||||
|     "@mui/material": "^5.15.10", | ||||
|     "@mui/material-nextjs": "^5.15.11", | ||||
|     "@next/mdx": "^14.1.0", | ||||
|     "@portabletext/react": "^3.0.11", | ||||
|     "@sanity/client": "^6.12.4", | ||||
|     "@sanity/icons": "^2.8", | ||||
|     "@sanity/image-url": "1", | ||||
|     "@sanity/types": "^3.25", | ||||
|     "@sanity/ui": "^2.0", | ||||
|     "@sanity/vision": "3", | ||||
|     "@types/mdx": "^2.0.11", | ||||
|     "gray-matter": "^4.0.3", | ||||
|     "next": "14.1.0", | ||||
|     "react": "^18", | ||||
|     "next": "^14.1", | ||||
|     "next-sanity": "7.1.4", | ||||
|     "nextjs-toploader": "^1.6.6", | ||||
|     "react": "^18.2", | ||||
|     "react-dom": "^18", | ||||
|     "react-markdown": "^9.0.1", | ||||
|     "rss": "^1.2.2" | ||||
|     "rss": "^1.2.2", | ||||
|     "sanity": "^3.25", | ||||
|     "styled-components": "^6.0" | ||||
|   }, | ||||
|   "devDependencies": { | ||||
|     "@tailwindcss/typography": "^0.5.10", | ||||
|     "@types/node": "^20", | ||||
|     "@types/nprogress": "^0.2.3", | ||||
|     "@types/react": "^18", | ||||
|     "@types/react-dom": "^18", | ||||
|     "@types/rss": "^0.0.32", | ||||
|   | ||||
							
								
								
									
										10
									
								
								sanity.cli.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								sanity.cli.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,10 @@ | ||||
| /** | ||||
|  * This configuration file lets you run `$ sanity [command]` in this folder | ||||
|  * Go to https://www.sanity.io/docs/cli to learn more. | ||||
|  **/ | ||||
| import { defineCliConfig } from "sanity/cli"; | ||||
|  | ||||
| const projectId = process.env.NEXT_PUBLIC_SANITY_PROJECT_ID; | ||||
| const dataset = process.env.NEXT_PUBLIC_SANITY_DATASET; | ||||
|  | ||||
| export default defineCliConfig({ api: { projectId, dataset } }); | ||||
							
								
								
									
										25
									
								
								sanity.config.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								sanity.config.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,25 @@ | ||||
| /** | ||||
|  * This configuration is used to for the Sanity Studio that’s mounted on the `/pages/studio/page.tsx` route | ||||
|  */ | ||||
|  | ||||
| import { visionTool } from "@sanity/vision"; | ||||
| import { defineConfig } from "sanity"; | ||||
| import { structureTool } from "sanity/structure"; | ||||
|  | ||||
| // Go to https://www.sanity.io/docs/api-versioning to learn how API versioning works | ||||
| import { apiVersion, dataset, projectId } from "./sanity/env"; | ||||
| import { schema } from "./sanity/schema"; | ||||
|  | ||||
| export default defineConfig({ | ||||
|   basePath: "/console", | ||||
|   projectId, | ||||
|   dataset, | ||||
|   // Add and edit the content schema in the './sanity/schema' folder | ||||
|   schema, | ||||
|   plugins: [ | ||||
|     structureTool(), | ||||
|     // Vision is a tool that lets you query your content with GROQ in the studio | ||||
|     // https://www.sanity.io/docs/the-vision-plugin | ||||
|     visionTool({ defaultApiVersion: apiVersion }), | ||||
|   ], | ||||
| }); | ||||
							
								
								
									
										21
									
								
								sanity/env.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								sanity/env.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,21 @@ | ||||
| export const apiVersion = process.env.NEXT_PUBLIC_SANITY_API_VERSION || "2024-02-24"; | ||||
|  | ||||
| export const dataset = assertValue( | ||||
|   process.env.NEXT_PUBLIC_SANITY_DATASET, | ||||
|   "Missing environment variable: NEXT_PUBLIC_SANITY_DATASET", | ||||
| ); | ||||
|  | ||||
| export const projectId = assertValue( | ||||
|   process.env.NEXT_PUBLIC_SANITY_PROJECT_ID, | ||||
|   "Missing environment variable: NEXT_PUBLIC_SANITY_PROJECT_ID", | ||||
| ); | ||||
|  | ||||
| export const useCdn = false; | ||||
|  | ||||
| function assertValue<T>(v: T | undefined, errorMessage: string): T { | ||||
|   if (v === undefined) { | ||||
|     throw new Error(errorMessage); | ||||
|   } | ||||
|  | ||||
|   return v; | ||||
| } | ||||
							
								
								
									
										10
									
								
								sanity/lib/client.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								sanity/lib/client.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,10 @@ | ||||
| import { createClient } from "next-sanity"; | ||||
|  | ||||
| import { apiVersion, dataset, projectId, useCdn } from "../env"; | ||||
|  | ||||
| export const client = createClient({ | ||||
|   apiVersion, | ||||
|   dataset, | ||||
|   projectId, | ||||
|   useCdn, | ||||
| }); | ||||
							
								
								
									
										13
									
								
								sanity/lib/image.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								sanity/lib/image.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,13 @@ | ||||
| import createImageUrlBuilder from "@sanity/image-url"; | ||||
| import type { Image } from "sanity"; | ||||
|  | ||||
| import { dataset, projectId } from "../env"; | ||||
|  | ||||
| const imageBuilder = createImageUrlBuilder({ | ||||
|   projectId: projectId || "", | ||||
|   dataset: dataset || "", | ||||
| }); | ||||
|  | ||||
| export const urlForImage = (source: Image) => { | ||||
|   return imageBuilder?.image(source).auto("format").fit("max").url(); | ||||
| }; | ||||
							
								
								
									
										10
									
								
								sanity/schema.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								sanity/schema.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,10 @@ | ||||
| import { type SchemaTypeDefinition } from "sanity"; | ||||
|  | ||||
| import blockContent from "./schemaTypes/blockContent"; | ||||
| import category from "./schemaTypes/category"; | ||||
| import post from "./schemaTypes/post"; | ||||
| import author from "./schemaTypes/author"; | ||||
|  | ||||
| export const schema: { types: SchemaTypeDefinition[] } = { | ||||
|   types: [post, author, category, blockContent], | ||||
| }; | ||||
							
								
								
									
										57
									
								
								sanity/schemaTypes/author.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										57
									
								
								sanity/schemaTypes/author.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,57 @@ | ||||
| import { defineField, defineType } from "sanity"; | ||||
|  | ||||
| export default defineType({ | ||||
|   name: "author", | ||||
|   title: "Author", | ||||
|   type: "document", | ||||
|   fields: [ | ||||
|     defineField({ | ||||
|       name: "name", | ||||
|       title: "Name", | ||||
|       type: "string", | ||||
|     }), | ||||
|     defineField({ | ||||
|       name: "slug", | ||||
|       title: "Slug", | ||||
|       type: "slug", | ||||
|       options: { | ||||
|         source: "name", | ||||
|         maxLength: 96, | ||||
|       }, | ||||
|     }), | ||||
|     defineField({ | ||||
|       name: "image", | ||||
|       title: "Image", | ||||
|       type: "image", | ||||
|       options: { | ||||
|         hotspot: true, | ||||
|       }, | ||||
|       fields: [ | ||||
|         { | ||||
|           name: "alt", | ||||
|           type: "string", | ||||
|           title: "Alternative Text", | ||||
|         }, | ||||
|       ], | ||||
|     }), | ||||
|     defineField({ | ||||
|       name: "bio", | ||||
|       title: "Bio", | ||||
|       type: "array", | ||||
|       of: [ | ||||
|         { | ||||
|           title: "Block", | ||||
|           type: "block", | ||||
|           styles: [{ title: "Normal", value: "normal" }], | ||||
|           lists: [], | ||||
|         }, | ||||
|       ], | ||||
|     }), | ||||
|   ], | ||||
|   preview: { | ||||
|     select: { | ||||
|       title: "name", | ||||
|       media: "image", | ||||
|     }, | ||||
|   }, | ||||
| }); | ||||
							
								
								
									
										75
									
								
								sanity/schemaTypes/blockContent.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										75
									
								
								sanity/schemaTypes/blockContent.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,75 @@ | ||||
| import { defineType, defineArrayMember } from "sanity"; | ||||
|  | ||||
| /** | ||||
|  * This is the schema type for block content used in the post document type | ||||
|  * Importing this type into the studio configuration's `schema` property | ||||
|  * lets you reuse it in other document types with: | ||||
|  *  { | ||||
|  *    name: 'someName', | ||||
|  *    title: 'Some title', | ||||
|  *    type: 'blockContent' | ||||
|  *  } | ||||
|  */ | ||||
|  | ||||
| export default defineType({ | ||||
|   title: "Block Content", | ||||
|   name: "blockContent", | ||||
|   type: "array", | ||||
|   of: [ | ||||
|     defineArrayMember({ | ||||
|       title: "Block", | ||||
|       type: "block", | ||||
|       // Styles let you define what blocks can be marked up as. The default | ||||
|       // set corresponds with HTML tags, but you can set any title or value | ||||
|       // you want, and decide how you want to deal with it where you want to | ||||
|       // use your content. | ||||
|       styles: [ | ||||
|         { title: "Normal", value: "normal" }, | ||||
|         { title: "H1", value: "h1" }, | ||||
|         { title: "H2", value: "h2" }, | ||||
|         { title: "H3", value: "h3" }, | ||||
|         { title: "H4", value: "h4" }, | ||||
|         { title: "Quote", value: "blockquote" }, | ||||
|       ], | ||||
|       lists: [{ title: "Bullet", value: "bullet" }], | ||||
|       // Marks let you mark up inline text in the Portable Text Editor | ||||
|       marks: { | ||||
|         // Decorators usually describe a single property – e.g. a typographic | ||||
|         // preference or highlighting | ||||
|         decorators: [ | ||||
|           { title: "Strong", value: "strong" }, | ||||
|           { title: "Emphasis", value: "em" }, | ||||
|         ], | ||||
|         // Annotations can be any object structure – e.g. a link or a footnote. | ||||
|         annotations: [ | ||||
|           { | ||||
|             title: "URL", | ||||
|             name: "link", | ||||
|             type: "object", | ||||
|             fields: [ | ||||
|               { | ||||
|                 title: "URL", | ||||
|                 name: "href", | ||||
|                 type: "url", | ||||
|               }, | ||||
|             ], | ||||
|           }, | ||||
|         ], | ||||
|       }, | ||||
|     }), | ||||
|     // You can add additional types here. Note that you can't use | ||||
|     // primitive types such as 'string' and 'number' in the same array | ||||
|     // as a block type. | ||||
|     defineArrayMember({ | ||||
|       type: "image", | ||||
|       options: { hotspot: true }, | ||||
|       fields: [ | ||||
|         { | ||||
|           name: "alt", | ||||
|           type: "string", | ||||
|           title: "Alternative Text", | ||||
|         }, | ||||
|       ], | ||||
|     }), | ||||
|   ], | ||||
| }); | ||||
							
								
								
									
										19
									
								
								sanity/schemaTypes/category.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								sanity/schemaTypes/category.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,19 @@ | ||||
| import { defineField, defineType } from "sanity"; | ||||
|  | ||||
| export default defineType({ | ||||
|   name: "category", | ||||
|   title: "Category", | ||||
|   type: "document", | ||||
|   fields: [ | ||||
|     defineField({ | ||||
|       name: "title", | ||||
|       title: "Title", | ||||
|       type: "string", | ||||
|     }), | ||||
|     defineField({ | ||||
|       name: "description", | ||||
|       title: "Description", | ||||
|       type: "text", | ||||
|     }), | ||||
|   ], | ||||
| }); | ||||
							
								
								
									
										77
									
								
								sanity/schemaTypes/post.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										77
									
								
								sanity/schemaTypes/post.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,77 @@ | ||||
| import { defineField, defineType } from "sanity"; | ||||
|  | ||||
| export default defineType({ | ||||
|   name: "post", | ||||
|   title: "Post", | ||||
|   type: "document", | ||||
|   fields: [ | ||||
|     defineField({ | ||||
|       name: "title", | ||||
|       title: "Title", | ||||
|       type: "string", | ||||
|     }), | ||||
|     defineField({ | ||||
|       name: "description", | ||||
|       title: "Description", | ||||
|       type: "string", | ||||
|     }), | ||||
|     defineField({ | ||||
|       name: "slug", | ||||
|       title: "Slug", | ||||
|       type: "slug", | ||||
|       options: { | ||||
|         source: "title", | ||||
|         maxLength: 96, | ||||
|       }, | ||||
|     }), | ||||
|     defineField({ | ||||
|       name: "author", | ||||
|       title: "Author", | ||||
|       type: "reference", | ||||
|       to: { type: "author" }, | ||||
|     }), | ||||
|     defineField({ | ||||
|       name: "mainImage", | ||||
|       title: "Main image", | ||||
|       type: "image", | ||||
|       options: { | ||||
|         hotspot: true, | ||||
|       }, | ||||
|       fields: [ | ||||
|         { | ||||
|           name: "alt", | ||||
|           type: "string", | ||||
|           title: "Alternative Text", | ||||
|         }, | ||||
|       ], | ||||
|     }), | ||||
|     defineField({ | ||||
|       name: "categories", | ||||
|       title: "Categories", | ||||
|       type: "array", | ||||
|       of: [{ type: "reference", to: { type: "category" } }], | ||||
|     }), | ||||
|     defineField({ | ||||
|       name: "publishedAt", | ||||
|       title: "Published at", | ||||
|       type: "datetime", | ||||
|     }), | ||||
|     defineField({ | ||||
|       name: "body", | ||||
|       title: "Body", | ||||
|       type: "blockContent", | ||||
|     }), | ||||
|   ], | ||||
|  | ||||
|   preview: { | ||||
|     select: { | ||||
|       title: "title", | ||||
|       author: "author.name", | ||||
|       media: "mainImage", | ||||
|     }, | ||||
|     prepare(selection) { | ||||
|       const { author } = selection; | ||||
|       return { ...selection, subtitle: author && `by ${author}` }; | ||||
|     }, | ||||
|   }, | ||||
| }); | ||||
| @@ -12,6 +12,7 @@ | ||||
|     "isolatedModules": true, | ||||
|     "jsx": "preserve", | ||||
|     "incremental": true, | ||||
|     "target": "ES2017", | ||||
|     "plugins": [ | ||||
|       { | ||||
|         "name": "next" | ||||
|   | ||||
		Reference in New Issue
	
	Block a user