♻️ 使用 NextJS 重构 #1
							
								
								
									
										2
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							| @@ -36,3 +36,5 @@ yarn-error.log* | |||||||
| next-env.d.ts | next-env.d.ts | ||||||
|  |  | ||||||
| bun.lockb | bun.lockb | ||||||
|  |  | ||||||
|  | .env | ||||||
							
								
								
									
										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_NAME = "Goatshed"; | ||||||
| export const SITE_DESCRIPTION = "山羊寒舍,在这里最终智羊工作室的最新动态。"; | export const SITE_DESCRIPTION = "山羊寒舍,在这里最终智羊工作室的最新动态。"; | ||||||
| export const SITE_URL = "https://smartsheep.studio" | export const SITE_URL = "https://smartsheep.studio"; | ||||||
|  |  | ||||||
| export const RELATED_ACCOUNTS: RelatedAccount[] = [ | export const RELATED_ACCOUNTS: RelatedAccount[] = [ | ||||||
|   { icon: <GitHubIcon />, platform: "GitHub", accountName: "@smartsheep-hq", link: "https://github.com/smartsheep-hq" }, |   { 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, |     description: SITE_DESCRIPTION, | ||||||
|     site_url: SITE_URL, |     site_url: SITE_URL, | ||||||
|     feed_url: `${SITE_URL}/feed`, |     feed_url: `${SITE_URL}/feed`, | ||||||
|     language: "zh-CN" |     language: "zh-CN", | ||||||
|   }); |   }); | ||||||
|  |  | ||||||
|   getSortedPosts().forEach((item) => { |   getSortedPosts().forEach((item) => { | ||||||
| @@ -22,7 +22,7 @@ export async function GET() { | |||||||
|  |  | ||||||
|   return new Response(feed.xml(), { |   return new Response(feed.xml(), { | ||||||
|     headers: { |     headers: { | ||||||
|       "content-type": "application/xml" |       "content-type": "application/xml", | ||||||
|     } |     }, | ||||||
|   }); |   }); | ||||||
| } | } | ||||||
| @@ -14,6 +14,7 @@ import "@fontsource/roboto/700.css"; | |||||||
| import "./globals.css"; | import "./globals.css"; | ||||||
|  |  | ||||||
| import AppShell from "@/components/AppShell"; | import AppShell from "@/components/AppShell"; | ||||||
|  | import NextTopLoader from "nextjs-toploader"; | ||||||
|  |  | ||||||
| export const runtime = "edge"; | export const runtime = "edge"; | ||||||
|  |  | ||||||
| @@ -33,6 +34,7 @@ export default function RootLayout({ children }: Readonly<{ | |||||||
|     <body> |     <body> | ||||||
|     <AppRouterCacheProvider> |     <AppRouterCacheProvider> | ||||||
|       <CssBaseline /> |       <CssBaseline /> | ||||||
|  |       <NextTopLoader showAtBottom color="#4a5099" /> | ||||||
|       <ThemeProvider theme={theme}> |       <ThemeProvider theme={theme}> | ||||||
|         <AppShell>{children}</AppShell> |         <AppShell>{children}</AppShell> | ||||||
|       </ThemeProvider> |       </ThemeProvider> | ||||||
|   | |||||||
							
								
								
									
										31
									
								
								app/page.tsx
									
									
									
									
									
								
							
							
						
						
									
										31
									
								
								app/page.tsx
									
									
									
									
									
								
							| @@ -2,14 +2,15 @@ import { | |||||||
|   Avatar, |   Avatar, | ||||||
|   Box, |   Box, | ||||||
|   Button, |   Button, | ||||||
|   Card, colors, |   Card, | ||||||
|  |   colors, | ||||||
|   Container, |   Container, | ||||||
|   Grid, |   Grid, | ||||||
|   List, |   List, | ||||||
|   ListItemAvatar, |   ListItemAvatar, | ||||||
|   ListItemButton, |   ListItemButton, | ||||||
|   ListItemText, |   ListItemText, | ||||||
|   Typography |   Typography, | ||||||
| } from "@mui/material"; | } from "@mui/material"; | ||||||
| import { RELATED_ACCOUNTS } from "@/app/consts"; | import { RELATED_ACCOUNTS } from "@/app/consts"; | ||||||
| import Image from "next/image"; | import Image from "next/image"; | ||||||
| @@ -18,19 +19,18 @@ import Link from "next/link"; | |||||||
| export default function Home() { | export default function Home() { | ||||||
|   return ( |   return ( | ||||||
|     <Container sx={{ scrollBehavior: "smooth", px: 5 }}> |     <Container sx={{ scrollBehavior: "smooth", px: 5 }}> | ||||||
|       <Grid |       <Grid container id="introduce" alignItems="center" sx={{ height: "calc(100vh - 64px)" }}> | ||||||
|         container |  | ||||||
|         id="introduce" |  | ||||||
|         alignItems="center" |  | ||||||
|         sx={{ height: "calc(100vh - 64px)" }} |  | ||||||
|       > |  | ||||||
|         <Grid item xs={12} sm={6} sx={{ textAlign: { xs: "center", sm: "initial" } }}> |         <Grid item xs={12} sm={6} sx={{ textAlign: { xs: "center", sm: "initial" } }}> | ||||||
|           <Typography variant="h1" gutterBottom>你好呀 👋</Typography> |           <Typography variant="h1" gutterBottom> | ||||||
|  |             你好呀 👋 | ||||||
|  |           </Typography> | ||||||
|           <Typography paragraph> |           <Typography paragraph> | ||||||
|             欢迎来到 SmartSheep Studio 的官方网站!在这里了解,订阅,跟踪我们的最新消息。 |             欢迎来到 SmartSheep Studio 的官方网站!在这里了解,订阅,跟踪我们的最新消息。 | ||||||
|             接触我们最大的官方社区,并且尝试最新产品,参与各种活动,提供反馈,让我们更好的服务您。 |             接触我们最大的官方社区,并且尝试最新产品,参与各种活动,提供反馈,让我们更好的服务您。 | ||||||
|           </Typography> |           </Typography> | ||||||
|           <Button variant="contained" href="#about-us" size="large">探索更多</Button> |           <Button variant="contained" href="#about-us" size="large"> | ||||||
|  |             探索更多 | ||||||
|  |           </Button> | ||||||
|         </Grid> |         </Grid> | ||||||
|         <Grid |         <Grid | ||||||
|           item |           item | ||||||
| @@ -44,12 +44,7 @@ export default function Home() { | |||||||
|         </Grid> |         </Grid> | ||||||
|       </Grid> |       </Grid> | ||||||
|  |  | ||||||
|       <Grid |       <Grid container id="about-us" alignItems="center" sx={{ height: "calc(100vh - 64px)" }}> | ||||||
|         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" } }}> |         <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 } }}> |           <Card sx={{ flexGrow: 1, mr: { xs: 0, sm: 4, md: 8 } }}> | ||||||
|             <List sx={{ width: "100%", bgcolor: "background.paper" }}> |             <List sx={{ width: "100%", bgcolor: "background.paper" }}> | ||||||
| @@ -67,7 +62,9 @@ export default function Home() { | |||||||
|           </Card> |           </Card> | ||||||
|         </Grid> |         </Grid> | ||||||
|         <Grid item xs={12} sm={6} sx={{ textAlign: { xs: "center", sm: "initial" } }}> |         <Grid item xs={12} sm={6} sx={{ textAlign: { xs: "center", sm: "initial" } }}> | ||||||
|           <Typography variant="h1" gutterBottom>关于我们</Typography> |           <Typography variant="h1" gutterBottom> | ||||||
|  |             关于我们 | ||||||
|  |           </Typography> | ||||||
|           <Typography paragraph> |           <Typography paragraph> | ||||||
|             我们是一群充满活力、对开源充满热情的开发者。成立于 2019 年。自那年起我们一直在开发让人喜欢的开源软件。 |             我们是一群充满活力、对开源充满热情的开发者。成立于 2019 年。自那年起我们一直在开发让人喜欢的开源软件。 | ||||||
|             在我们这里,“取之于开源,用之于开源” 不仅是原则,更是我们信仰的座右铭。 |             在我们这里,“取之于开源,用之于开源” 不仅是原则,更是我们信仰的座右铭。 | ||||||
|   | |||||||
| @@ -1,20 +1,32 @@ | |||||||
| import { Box, Card, CardContent, CardMedia, Divider, Typography } from "@mui/material"; | import { Box, Card, CardContent, CardMedia, Chip, Divider, Stack, Typography } from "@mui/material"; | ||||||
| import { getSinglePost } from "@/content/posts"; | import { client } from "@/sanity/lib/client"; | ||||||
| import Image from "next/image"; |  | ||||||
| import PostContent from "@/components/posts/PostContent"; | import PostContent from "@/components/posts/PostContent"; | ||||||
|  | import Image from "next/image"; | ||||||
|  |  | ||||||
| export default function PostDetailPage({ params }: { params: { id: string } }) { | export default async function PostDetailPage({ params }: { params: { id: string } }) { | ||||||
|   const post = getSinglePost(params.id); |   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 ( |   return ( | ||||||
|     <Card> |     <Card> | ||||||
|       { |       { | ||||||
|         post.thumbnail && |         post.mainImage && | ||||||
|         <CardMedia sx={{ height: 360, position: "relative" }} title={post.title}> |         <CardMedia sx={{ height: 360, position: "relative" }} title={post.mainImage.alt}> | ||||||
|           <Image |           <Image | ||||||
|             fill |             fill | ||||||
|             src={post.thumbnail} |             src={post.mainImage.asset.url} | ||||||
|             alt={post.title} |             alt={post.mainImage.alt} | ||||||
|             style={{ objectFit: "cover" }} |             style={{ objectFit: "cover" }} | ||||||
|           /> |           /> | ||||||
|         </CardMedia> |         </CardMedia> | ||||||
| @@ -22,16 +34,20 @@ export default function PostDetailPage({ params }: { params: { id: string } }) { | |||||||
|  |  | ||||||
|       <CardContent sx={{ paddingX: 5, paddingY: 3 }}> |       <CardContent sx={{ paddingX: 5, paddingY: 3 }}> | ||||||
|         <Box> |         <Box> | ||||||
|           <Typography gutterBottom variant="h2"> |           <Typography variant="h2"> | ||||||
|             {post.title} |             {post.title} | ||||||
|           </Typography> |           </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"> |           <Typography color="text.secondary" variant="body2"> | ||||||
|             {post.description ?? "No description yet."} |             {post.description ?? "No description yet."} | ||||||
|           </Typography> |           </Typography> | ||||||
|         </Box> |         </Box> | ||||||
|         <Divider sx={{ my: 5 }} /> |         <Divider sx={{ my: 2.5, mx: -5 }} /> | ||||||
|         <Box component="article" className="prose max-w-none" sx={{ minWidth: 0 }}> |         <Box component="article" className="prose max-w-none" sx={{ minWidth: 0 }}> | ||||||
|           <PostContent content={post.content ?? ""} /> |           <PostContent content={post.body} /> | ||||||
|         </Box> |         </Box> | ||||||
|       </CardContent> |       </CardContent> | ||||||
|     </Card> |     </Card> | ||||||
|   | |||||||
| @@ -1,14 +1,14 @@ | |||||||
| import { Box, Container } from "@mui/material"; | import { Box, Container } from "@mui/material"; | ||||||
| import { ReactNode } from "react"; | import { ReactNode } from "react"; | ||||||
|  |  | ||||||
| export default function PostLayout({children}: Readonly<{ | export default function PostLayout({ | ||||||
|  |   children, | ||||||
|  | }: Readonly<{ | ||||||
|   children: ReactNode; |   children: ReactNode; | ||||||
| }>) { | }>) { | ||||||
|   return ( |   return ( | ||||||
|     <Container sx={{ display: "flex", justifyContent: "center", gap: 4, py: 2 }}> |     <Container sx={{ display: "flex", justifyContent: "center", gap: 4, py: 2 }}> | ||||||
|       <Box sx={{ flexGrow: 1, maxWidth: 720 }}> |       <Box sx={{ flexGrow: 1, maxWidth: 720 }}>{children}</Box> | ||||||
|         {children} |  | ||||||
|       </Box> |  | ||||||
|     </Container> |     </Container> | ||||||
|   ) |   ); | ||||||
| } | } | ||||||
| @@ -1,36 +1,52 @@ | |||||||
| import { Button, Card, CardActions, CardContent, CardMedia, Typography } from "@mui/material"; | import { Button, Card, CardActions, CardContent, CardMedia, Chip, Stack, Typography } from "@mui/material"; | ||||||
| import { getSortedPosts } from "@/content/posts"; | import { client } from "@/sanity/lib/client"; | ||||||
| import Image from "next/image"; | import Image from "next/image"; | ||||||
| import Link from "next/link"; | import Link from "next/link"; | ||||||
|  |  | ||||||
| export default function PostList() { | export default async function PostList() { | ||||||
|   const posts = getSortedPosts(); |   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 ( |   return ( | ||||||
|     posts.map((post) => ( |     posts.map((post) => ( | ||||||
|       <Card key={post.id} sx={{ width: "100%" }}> |       <Card key={post.slug.current} sx={{ width: "100%" }}> | ||||||
|         { |         { | ||||||
|           post.thumbnail && |           post.mainImage && | ||||||
|           <CardMedia sx={{ height: 160, position: "relative" }} title={post.title}> |           <CardMedia sx={{ height: 160, position: "relative" }} title={post.mainImage.alt}> | ||||||
|             <Image |             <Image | ||||||
|               fill |               fill | ||||||
|               src={post.thumbnail} |               src={post.mainImage.asset.url} | ||||||
|               alt={post.title} |               alt={post.mainImage.alt} | ||||||
|               style={{ objectFit: "cover" }} |               style={{ objectFit: "cover" }} | ||||||
|             /> |             /> | ||||||
|           </CardMedia> |           </CardMedia> | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         <CardContent sx={{ paddingX: 5, paddingY: 3 }}> |         <CardContent sx={{ paddingX: 5, paddingY: 3 }}> | ||||||
|           <Typography gutterBottom variant="h3"> |           <Typography variant="h3"> | ||||||
|             {post.title} |             {post.title} | ||||||
|           </Typography> |           </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"> |           <Typography variant="body2" color="text.secondary"> | ||||||
|             {post.description ?? "No description yet."} |             {post.description ? post.description : "No description yet."} | ||||||
|           </Typography> |           </Typography> | ||||||
|         </CardContent> |         </CardContent> | ||||||
|         <CardActions sx={{ paddingX: 4, paddingBottom: 2 }}> |         <CardActions sx={{ paddingX: 4, paddingBottom: 2 }}> | ||||||
|           <Link href={`/p/${post.id}`} passHref> |           <Link href={`/p/${post.slug.current}`} passHref> | ||||||
|             <Button>Read more</Button> |             <Button>Read more</Button> | ||||||
|           </Link> |           </Link> | ||||||
|         </CardActions> |         </CardActions> | ||||||
|   | |||||||
| @@ -10,20 +10,20 @@ export default function sitemap(): MetadataRoute.Sitemap { | |||||||
|       url: `${SITE_URL}/`, |       url: `${SITE_URL}/`, | ||||||
|       lastModified: new Date(), |       lastModified: new Date(), | ||||||
|       changeFrequency: "weekly", |       changeFrequency: "weekly", | ||||||
|       priority: 1 |       priority: 1, | ||||||
|     }, |     }, | ||||||
|     { |     { | ||||||
|       url: `${SITE_URL}/posts`, |       url: `${SITE_URL}/posts`, | ||||||
|       lastModified: new Date(), |       lastModified: new Date(), | ||||||
|       changeFrequency: "daily", |       changeFrequency: "daily", | ||||||
|       priority: 0.8 |       priority: 0.8, | ||||||
|     }, |     }, | ||||||
|  |  | ||||||
|     ...posts.map((item: Post) => ({ |     ...posts.map((item: Post) => ({ | ||||||
|       url: `${SITE_URL}/posts/${item.id}`, |       url: `${SITE_URL}/posts/${item.id}`, | ||||||
|       lastModified: item.date, |       lastModified: item.date, | ||||||
|       changeFrequency: "daily" as any, |       changeFrequency: "daily" as any, | ||||||
|       priority: 0.75 |       priority: 0.75, | ||||||
|     })) |     })), | ||||||
|   ]; |   ]; | ||||||
| } | } | ||||||
| @@ -5,11 +5,11 @@ import { createTheme } from "@mui/material/styles"; | |||||||
| export const theme = createTheme({ | export const theme = createTheme({ | ||||||
|   palette: { |   palette: { | ||||||
|     primary: { |     primary: { | ||||||
|       main: "#49509e" |       main: "#49509e", | ||||||
|     }, |     }, | ||||||
|     secondary: { |     secondary: { | ||||||
|       main: "#d43630" |       main: "#d43630", | ||||||
|     } |     }, | ||||||
|   }, |   }, | ||||||
|   typography: { |   typography: { | ||||||
|     h1: { fontSize: "2.5rem" }, |     h1: { fontSize: "2.5rem" }, | ||||||
|   | |||||||
| @@ -8,7 +8,9 @@ import { | |||||||
|   AppBarProps as MuiAppBarProps, |   AppBarProps as MuiAppBarProps, | ||||||
|   useScrollTrigger, |   useScrollTrigger, | ||||||
|   IconButton, |   IconButton, | ||||||
|   styled, Box, useMediaQuery |   styled, | ||||||
|  |   Box, | ||||||
|  |   useMediaQuery, | ||||||
| } from "@mui/material"; | } from "@mui/material"; | ||||||
| import { ReactElement, ReactNode, useEffect, useState } from "react"; | import { ReactElement, ReactNode, useEffect, useState } from "react"; | ||||||
| import { SITE_NAME } from "@/app/consts"; | import { SITE_NAME } from "@/app/consts"; | ||||||
| @@ -17,13 +19,10 @@ import MenuIcon from "@mui/icons-material/Menu"; | |||||||
| import Image from "next/image"; | import Image from "next/image"; | ||||||
| import Link from "next/link"; | import Link from "next/link"; | ||||||
|  |  | ||||||
| function HideOnScroll(props: { | function HideOnScroll(props: { window?: () => Window; children: ReactElement }) { | ||||||
|   window?: () => Window; |  | ||||||
|   children: ReactElement; |  | ||||||
| }) { |  | ||||||
|   const { children, window } = props; |   const { children, window } = props; | ||||||
|   const trigger = useScrollTrigger({ |   const trigger = useScrollTrigger({ | ||||||
|     target: window ? window() : undefined |     target: window ? window() : undefined, | ||||||
|   }); |   }); | ||||||
|  |  | ||||||
|   return ( |   return ( | ||||||
| @@ -38,24 +37,25 @@ interface AppBarProps extends MuiAppBarProps { | |||||||
| } | } | ||||||
|  |  | ||||||
| const ShellAppBar = styled(MuiAppBar, { | const ShellAppBar = styled(MuiAppBar, { | ||||||
|   shouldForwardProp: (prop) => prop !== "open" |   shouldForwardProp: (prop) => prop !== "open", | ||||||
| })<AppBarProps>(({ theme, open }) => { | })<AppBarProps>(({ theme, open }) => { | ||||||
|   const isMobile = useMediaQuery(isMobileQuery); |   const isMobile = useMediaQuery(isMobileQuery); | ||||||
|  |  | ||||||
|   return ({ |   return { | ||||||
|     transition: theme.transitions.create(["margin", "width"], { |     transition: theme.transitions.create(["margin", "width"], { | ||||||
|       easing: theme.transitions.easing.sharp, |       easing: theme.transitions.easing.sharp, | ||||||
|       duration: theme.transitions.duration.leavingScreen |       duration: theme.transitions.duration.leavingScreen, | ||||||
|     }), |     }), | ||||||
|     ...(!isMobile && open && { |     ...(!isMobile && | ||||||
|  |       open && { | ||||||
|         width: `calc(100% - ${DRAWER_WIDTH}px)`, |         width: `calc(100% - ${DRAWER_WIDTH}px)`, | ||||||
|         transition: theme.transitions.create(["margin", "width"], { |         transition: theme.transitions.create(["margin", "width"], { | ||||||
|           easing: theme.transitions.easing.easeOut, |           easing: theme.transitions.easing.easeOut, | ||||||
|         duration: theme.transitions.duration.enteringScreen |           duration: theme.transitions.duration.enteringScreen, | ||||||
|         }), |         }), | ||||||
|       marginRight: DRAWER_WIDTH |         marginRight: DRAWER_WIDTH, | ||||||
|     }) |       }), | ||||||
|   }); |   }; | ||||||
| }); | }); | ||||||
|  |  | ||||||
| const AppMain = styled("main", { shouldForwardProp: (prop) => prop !== "open" })<{ | const AppMain = styled("main", { shouldForwardProp: (prop) => prop !== "open" })<{ | ||||||
| @@ -63,28 +63,26 @@ const AppMain = styled("main", { shouldForwardProp: (prop) => prop !== "open" }) | |||||||
| }>(({ theme, open }) => { | }>(({ theme, open }) => { | ||||||
|   const isMobile = useMediaQuery(isMobileQuery); |   const isMobile = useMediaQuery(isMobileQuery); | ||||||
|  |  | ||||||
|   return ({ |   return { | ||||||
|     flexGrow: 1, |     flexGrow: 1, | ||||||
|     transition: theme.transitions.create("margin", { |     transition: theme.transitions.create("margin", { | ||||||
|       easing: theme.transitions.easing.sharp, |       easing: theme.transitions.easing.sharp, | ||||||
|       duration: theme.transitions.duration.leavingScreen |       duration: theme.transitions.duration.leavingScreen, | ||||||
|     }), |     }), | ||||||
|     marginRight: -DRAWER_WIDTH, |     marginRight: -DRAWER_WIDTH, | ||||||
|     ...(!isMobile && open && { |     ...(!isMobile && | ||||||
|  |       open && { | ||||||
|         transition: theme.transitions.create("margin", { |         transition: theme.transitions.create("margin", { | ||||||
|           easing: theme.transitions.easing.easeOut, |           easing: theme.transitions.easing.easeOut, | ||||||
|         duration: theme.transitions.duration.enteringScreen |           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; |   let documentWindow: Window; | ||||||
|  |  | ||||||
|   const isMobile = useMediaQuery(isMobileQuery); |   const isMobile = useMediaQuery(isMobileQuery); | ||||||
| @@ -110,9 +108,7 @@ export default function AppShell({ children }: { | |||||||
|             </IconButton> |             </IconButton> | ||||||
|  |  | ||||||
|             <Typography variant="h6" component="div" sx={{ flexGrow: 1, fontSize: "1.2rem" }}> |             <Typography variant="h6" component="div" sx={{ flexGrow: 1, fontSize: "1.2rem" }}> | ||||||
|               <Link href="/"> |               <Link href="/">{SITE_NAME}</Link> | ||||||
|                 {SITE_NAME} |  | ||||||
|               </Link> |  | ||||||
|             </Typography> |             </Typography> | ||||||
|  |  | ||||||
|             <IconButton |             <IconButton | ||||||
|   | |||||||
| @@ -12,7 +12,7 @@ import { | |||||||
|   ListItemIcon, |   ListItemIcon, | ||||||
|   ListItemText, |   ListItemText, | ||||||
|   styled, |   styled, | ||||||
|   useMediaQuery |   useMediaQuery, | ||||||
| } from "@mui/material"; | } from "@mui/material"; | ||||||
| import { theme } from "@/app/theme"; | import { theme } from "@/app/theme"; | ||||||
| import { ReactNode } from "react"; | import { ReactNode } from "react"; | ||||||
| @@ -42,27 +42,25 @@ export const AppNavigationHeader = styled("div")(({ theme }) => ({ | |||||||
|   padding: theme.spacing(0, 1), |   padding: theme.spacing(0, 1), | ||||||
|   justifyContent: "flex-start", |   justifyContent: "flex-start", | ||||||
|   height: 64, |   height: 64, | ||||||
|   ...theme.mixins.toolbar |   ...theme.mixins.toolbar, | ||||||
| })); | })); | ||||||
|  |  | ||||||
| export function AppNavigation({ showClose, onClose }: { | export function AppNavigation({ showClose, onClose }: { showClose?: boolean; onClose: () => void }) { | ||||||
|   showClose?: boolean, |  | ||||||
|   onClose: () => void |  | ||||||
| }) { |  | ||||||
|   return ( |   return ( | ||||||
|     <> |     <> | ||||||
|       <AppNavigationHeader> |       <AppNavigationHeader> | ||||||
|         { |         {showClose && ( | ||||||
|           showClose && |  | ||||||
|           <IconButton onClick={onClose}> |           <IconButton onClick={onClose}> | ||||||
|             {theme.direction === "rtl" ? <ChevronLeftIcon /> : <ChevronRightIcon />} |             {theme.direction === "rtl" ? <ChevronLeftIcon /> : <ChevronRightIcon />} | ||||||
|           </IconButton> |           </IconButton> | ||||||
|         } |         )} | ||||||
|       </AppNavigationHeader> |       </AppNavigationHeader> | ||||||
|       <Divider /> |       <Divider /> | ||||||
|       <List> |       <List> | ||||||
|         {NAVIGATION_ITEMS.map((item, idx) => { |         {NAVIGATION_ITEMS.map((item, idx) => { | ||||||
|           return item.divider ? <Divider key={idx} /> : ( |           return item.divider ? ( | ||||||
|  |             <Divider key={idx} /> | ||||||
|  |           ) : ( | ||||||
|             <Link key={idx} href={item.link ?? "/"} passHref> |             <Link key={idx} href={item.link ?? "/"} passHref> | ||||||
|               <ListItemButton> |               <ListItemButton> | ||||||
|                 <ListItemIcon>{item.icon}</ListItemIcon> |                 <ListItemIcon>{item.icon}</ListItemIcon> | ||||||
| @@ -78,10 +76,7 @@ export function AppNavigation({ showClose, onClose }: { | |||||||
|  |  | ||||||
| export const isMobileQuery = theme.breakpoints.down("md"); | export const isMobileQuery = theme.breakpoints.down("md"); | ||||||
|  |  | ||||||
| export default function NavigationDrawer({ open, onClose }: { | export default function NavigationDrawer({ open, onClose }: { open: boolean; onClose: () => void }) { | ||||||
|   open: boolean, |  | ||||||
|   onClose: () => void, |  | ||||||
| }) { |  | ||||||
|   const isMobile = useMediaQuery(isMobileQuery); |   const isMobile = useMediaQuery(isMobileQuery); | ||||||
|  |  | ||||||
|   return isMobile ? ( |   return isMobile ? ( | ||||||
| @@ -96,8 +91,8 @@ export default function NavigationDrawer({ open, onClose }: { | |||||||
|         sx={{ |         sx={{ | ||||||
|           "& .MuiDrawer-paper": { |           "& .MuiDrawer-paper": { | ||||||
|             boxSizing: "border-box", |             boxSizing: "border-box", | ||||||
|             width: DRAWER_WIDTH |             width: DRAWER_WIDTH, | ||||||
|           } |           }, | ||||||
|         }} |         }} | ||||||
|       > |       > | ||||||
|         <AppNavigation onClose={onClose} /> |         <AppNavigation onClose={onClose} /> | ||||||
| @@ -112,8 +107,8 @@ export default function NavigationDrawer({ open, onClose }: { | |||||||
|         width: DRAWER_WIDTH, |         width: DRAWER_WIDTH, | ||||||
|         flexShrink: 0, |         flexShrink: 0, | ||||||
|         "& .MuiDrawer-paper": { |         "& .MuiDrawer-paper": { | ||||||
|           width: DRAWER_WIDTH |           width: DRAWER_WIDTH, | ||||||
|         } |         }, | ||||||
|       }} |       }} | ||||||
|     > |     > | ||||||
|       <AppNavigation showClose onClose={onClose} /> |       <AppNavigation showClose onClose={onClose} /> | ||||||
|   | |||||||
| @@ -1,7 +1,7 @@ | |||||||
| "use client"; | "use client"; | ||||||
|  |  | ||||||
| import Markdown from "react-markdown"; | import { PortableText } from "@portabletext/react"; | ||||||
|  |  | ||||||
| export default function PostContent({ content }: { content: string }) { | export default function PostContent({ content }: { content: any }) { | ||||||
|   return <Markdown>{content}</Markdown>; |   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: [ |     remotePatterns: [ | ||||||
|       { |       { | ||||||
|         protocol: "https", |         protocol: "https", | ||||||
|         hostname: "**" |         hostname: "**", | ||||||
|       } |       }, | ||||||
|     ] |     ], | ||||||
|   }, |   }, | ||||||
|   async rewrites() { |   async rewrites() { | ||||||
|     return [ |     return [ | ||||||
| @@ -15,9 +15,9 @@ const nextConfig = { | |||||||
|       { source: "/rss.xml", destination: "/feed.xml" }, |       { source: "/rss.xml", destination: "/feed.xml" }, | ||||||
|       { source: "/feed.xml", destination: "/feed" }, |       { source: "/feed.xml", destination: "/feed" }, | ||||||
|  |  | ||||||
|       { source: "/p/:id", destination: "/posts/:id" } |       { source: "/p/:id", destination: "/posts/:id" }, | ||||||
|     ]; |     ]; | ||||||
|   } |   }, | ||||||
| }; | }; | ||||||
|  |  | ||||||
| export default nextConfig; | export default nextConfig; | ||||||
|   | |||||||
							
								
								
									
										18
									
								
								package.json
									
									
									
									
									
								
							
							
						
						
									
										18
									
								
								package.json
									
									
									
									
									
								
							| @@ -19,17 +19,29 @@ | |||||||
|     "@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", |     "@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", |     "@types/mdx": "^2.0.11", | ||||||
|     "gray-matter": "^4.0.3", |     "gray-matter": "^4.0.3", | ||||||
|     "next": "14.1.0", |     "next": "^14.1", | ||||||
|     "react": "^18", |     "next-sanity": "7.1.4", | ||||||
|  |     "nextjs-toploader": "^1.6.6", | ||||||
|  |     "react": "^18.2", | ||||||
|     "react-dom": "^18", |     "react-dom": "^18", | ||||||
|     "react-markdown": "^9.0.1", |     "react-markdown": "^9.0.1", | ||||||
|     "rss": "^1.2.2" |     "rss": "^1.2.2", | ||||||
|  |     "sanity": "^3.25", | ||||||
|  |     "styled-components": "^6.0" | ||||||
|   }, |   }, | ||||||
|   "devDependencies": { |   "devDependencies": { | ||||||
|     "@tailwindcss/typography": "^0.5.10", |     "@tailwindcss/typography": "^0.5.10", | ||||||
|     "@types/node": "^20", |     "@types/node": "^20", | ||||||
|  |     "@types/nprogress": "^0.2.3", | ||||||
|     "@types/react": "^18", |     "@types/react": "^18", | ||||||
|     "@types/react-dom": "^18", |     "@types/react-dom": "^18", | ||||||
|     "@types/rss": "^0.0.32", |     "@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, |     "isolatedModules": true, | ||||||
|     "jsx": "preserve", |     "jsx": "preserve", | ||||||
|     "incremental": true, |     "incremental": true, | ||||||
|  |     "target": "ES2017", | ||||||
|     "plugins": [ |     "plugins": [ | ||||||
|       { |       { | ||||||
|         "name": "next" |         "name": "next" | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user