finish implementing draft features in blog
This commit is contained in:
		| @@ -3,8 +3,15 @@ | |||||||
| import { useState } from "react"; | import { useState } from "react"; | ||||||
| import { useRouter } from "next/navigation"; | import { useRouter } from "next/navigation"; | ||||||
| import { Summary } from "../types"; | import { Summary } from "../types"; | ||||||
|  | import { publishArticle, unpublishArticle } from "../action"; | ||||||
|  |  | ||||||
| export default function PostSummary({ metadata }: { metadata: Summary }) { | export default function PostSummary({ | ||||||
|  |   metadata, | ||||||
|  |   loggedIn, | ||||||
|  | }: { | ||||||
|  |   metadata: Summary; | ||||||
|  |   loggedIn: boolean; | ||||||
|  | }) { | ||||||
|   const [elementColor, setElementColor] = useState("#999"); |   const [elementColor, setElementColor] = useState("#999"); | ||||||
|   const router = useRouter(); |   const router = useRouter(); | ||||||
|  |  | ||||||
| @@ -77,8 +84,61 @@ export default function PostSummary({ metadata }: { metadata: Summary }) { | |||||||
|             gap: 4, |             gap: 4, | ||||||
|           }} |           }} | ||||||
|         > |         > | ||||||
|           <span>Published: </span> |           {metadata.is_draft && ( | ||||||
|           <span>{metadata.publishedDate.toLocaleDateString()}</span> |             <> | ||||||
|  |               <span>draft - </span> | ||||||
|  |               <div | ||||||
|  |                 style={{ | ||||||
|  |                   cursor: "pointer", | ||||||
|  |                   color: "#999", | ||||||
|  |                   transition: "color 0.3s linear", | ||||||
|  |                 }} | ||||||
|  |                 onMouseOver={(e) => { | ||||||
|  |                   e.currentTarget.style.color = "#eee"; | ||||||
|  |                 }} | ||||||
|  |                 onMouseLeave={(e) => { | ||||||
|  |                   e.currentTarget.style.color = "#999"; | ||||||
|  |                 }} | ||||||
|  |                 onClick={async () => { | ||||||
|  |                   await publishArticle(metadata.slug); | ||||||
|  |                   router.refresh(); | ||||||
|  |                 }} | ||||||
|  |               > | ||||||
|  |                 move to published | ||||||
|  |               </div> | ||||||
|  |             </> | ||||||
|  |           )} | ||||||
|  |           {!metadata.is_draft && ( | ||||||
|  |             <> | ||||||
|  |               <div> | ||||||
|  |                 <div> | ||||||
|  |                   <span>published: </span> | ||||||
|  |                   <span>{metadata.publishedDate.toLocaleDateString()}</span> | ||||||
|  |                 </div> | ||||||
|  |                 {loggedIn && ( | ||||||
|  |                   <div | ||||||
|  |                     style={{ | ||||||
|  |                       cursor: "pointer", | ||||||
|  |                       color: "#999", | ||||||
|  |                       transition: "color 0.3s linear", | ||||||
|  |                     }} | ||||||
|  |                     onMouseOver={(e) => { | ||||||
|  |                       e.currentTarget.style.color = "#eee"; | ||||||
|  |                     }} | ||||||
|  |                     onMouseLeave={(e) => { | ||||||
|  |                       e.currentTarget.style.color = "#999"; | ||||||
|  |                     }} | ||||||
|  |                     onClick={async () => { | ||||||
|  |                       await unpublishArticle(metadata.slug); | ||||||
|  |                       router.refresh(); | ||||||
|  |                     }} | ||||||
|  |                   > | ||||||
|  |                     move to drafts | ||||||
|  |                   </div> | ||||||
|  |                 )} | ||||||
|  |               </div> | ||||||
|  |             </> | ||||||
|  |           )} | ||||||
|         </div> |         </div> | ||||||
|       </div> |       </div> | ||||||
|       <div |       <div | ||||||
|   | |||||||
| @@ -1,3 +1,5 @@ | |||||||
|  | "use server"; | ||||||
|  |  | ||||||
| import { getSummaries, getTags } from "../action"; | import { getSummaries, getTags } from "../action"; | ||||||
| import { titleFont } from "@/components/fonts"; | import { titleFont } from "@/components/fonts"; | ||||||
| import { notFound } from "next/navigation"; | import { notFound } from "next/navigation"; | ||||||
| @@ -5,17 +7,7 @@ import React from "react"; | |||||||
| import PostSummary from "./PostSummary"; | import PostSummary from "./PostSummary"; | ||||||
| import Pagination from "./Pagination"; | import Pagination from "./Pagination"; | ||||||
| import TagOverview from "./TagOverview"; | import TagOverview from "./TagOverview"; | ||||||
|  | import { isLoggedIn } from "@/components/auth"; | ||||||
| async function getPageNumber( |  | ||||||
|   searchParams: Promise<{ page: string | undefined }> |  | ||||||
| ) { |  | ||||||
|   const { page } = await searchParams; |  | ||||||
|   const result = page ? parseInt(page) : 1; |  | ||||||
|   if (isNaN(result) || result < 1) { |  | ||||||
|     notFound(); |  | ||||||
|   } |  | ||||||
|   return result; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| export default async function Blog({ | export default async function Blog({ | ||||||
|   params, |   params, | ||||||
| @@ -38,7 +30,8 @@ export default async function Blog({ | |||||||
|   if (pageNumber > numberOfPages) { |   if (pageNumber > numberOfPages) { | ||||||
|     notFound(); |     notFound(); | ||||||
|   } |   } | ||||||
|   const tags = await getTags(); |   const loggedIn = await isLoggedIn(); | ||||||
|  |   const tags = await getTags(loggedIn); | ||||||
|  |  | ||||||
|   return ( |   return ( | ||||||
|     <> |     <> | ||||||
| @@ -53,9 +46,23 @@ export default async function Blog({ | |||||||
|         <span style={{ fontSize: 12 }}>...occasionally</span> |         <span style={{ fontSize: 12 }}>...occasionally</span> | ||||||
|       </div> |       </div> | ||||||
|       <TagOverview tags={tags} currentTag={currentTag} /> |       <TagOverview tags={tags} currentTag={currentTag} /> | ||||||
|       {metadata.length > 0 && |       {metadata | ||||||
|         metadata.map((m) => <PostSummary metadata={m} key={m.slug} />)} |         .filter((m) => loggedIn || !m.is_draft) | ||||||
|  |         .map((m) => ( | ||||||
|  |           <PostSummary metadata={m} key={m.slug} loggedIn={loggedIn} /> | ||||||
|  |         ))} | ||||||
|       <Pagination numberOfPages={numberOfPages} pageNumber={pageNumber} /> |       <Pagination numberOfPages={numberOfPages} pageNumber={pageNumber} /> | ||||||
|     </> |     </> | ||||||
|   ); |   ); | ||||||
| } | } | ||||||
|  |  | ||||||
|  | async function getPageNumber( | ||||||
|  |   searchParams: Promise<{ page: string | undefined }> | ||||||
|  | ) { | ||||||
|  |   const { page } = await searchParams; | ||||||
|  |   const result = page ? parseInt(page) : 1; | ||||||
|  |   if (isNaN(result) || result < 1) { | ||||||
|  |     notFound(); | ||||||
|  |   } | ||||||
|  |   return result; | ||||||
|  | } | ||||||
|   | |||||||
| @@ -4,11 +4,17 @@ import { notFound } from "next/navigation"; | |||||||
| import { Post, Summary } from "./types"; | import { Post, Summary } from "./types"; | ||||||
| import { PrismaClient } from "@prisma/client"; | import { PrismaClient } from "@prisma/client"; | ||||||
|  |  | ||||||
| export async function getTags(): Promise<string[]> { | export async function getTags(loggedIn: boolean): Promise<string[]> { | ||||||
|   const prisma = new PrismaClient(); |   const prisma = new PrismaClient(); | ||||||
|   const tags = (await prisma.tag.findMany({ select: { name: true } })).map( |   const filter = loggedIn | ||||||
|     (tag) => tag.name |     ? undefined | ||||||
|   ); |     : { posts: { some: { is_draft: false } } }; | ||||||
|  |   const tags = ( | ||||||
|  |     await prisma.tag.findMany({ | ||||||
|  |       select: { name: true }, | ||||||
|  |       where: filter, | ||||||
|  |     }) | ||||||
|  |   ).map((tag) => tag.name); | ||||||
|   return tags; |   return tags; | ||||||
| } | } | ||||||
|  |  | ||||||
| @@ -51,7 +57,7 @@ export async function getSummaries( | |||||||
|       ...filter, |       ...filter, | ||||||
|       omit: { contentMarkdown: true, contentRendered: true }, |       omit: { contentMarkdown: true, contentRendered: true }, | ||||||
|       include: { tags: { select: { name: true } } }, |       include: { tags: { select: { name: true } } }, | ||||||
|       orderBy: { publishedDate: "desc" }, |       orderBy: { publishedDate: "asc" }, | ||||||
|       skip: PAGE_SIZE * (pageNumber - 1), |       skip: PAGE_SIZE * (pageNumber - 1), | ||||||
|       take: PAGE_SIZE, |       take: PAGE_SIZE, | ||||||
|     }) |     }) | ||||||
| @@ -60,3 +66,16 @@ export async function getSummaries( | |||||||
|   }); |   }); | ||||||
|   return { metadata: posts, numberOfPages: numberOfPages }; |   return { metadata: posts, numberOfPages: numberOfPages }; | ||||||
| } | } | ||||||
|  |  | ||||||
|  | export async function publishArticle(slug: string) { | ||||||
|  |   const prisma = new PrismaClient(); | ||||||
|  |   await prisma.post.update({ | ||||||
|  |     where: { slug }, | ||||||
|  |     data: { is_draft: false, publishedDate: new Date() }, | ||||||
|  |   }); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export async function unpublishArticle(slug: string) { | ||||||
|  |   const prisma = new PrismaClient(); | ||||||
|  |   await prisma.post.update({ where: { slug }, data: { is_draft: true } }); | ||||||
|  | } | ||||||
|   | |||||||
| @@ -2,7 +2,7 @@ | |||||||
|  |  | ||||||
| import { PrismaClient } from "@prisma/client"; | import { PrismaClient } from "@prisma/client"; | ||||||
| import argon2 from "argon2"; | import argon2 from "argon2"; | ||||||
| import { setSession } from "../write/auth"; | import { setSession } from "@/components/auth"; | ||||||
| import { redirect, RedirectType } from "next/navigation"; | import { redirect, RedirectType } from "next/navigation"; | ||||||
|  |  | ||||||
| export async function handleLogin(data: FormData) { | export async function handleLogin(data: FormData) { | ||||||
|   | |||||||
| @@ -1,7 +1,7 @@ | |||||||
| "use server"; | "use server"; | ||||||
|  |  | ||||||
| import { redirect } from "next/navigation"; | import { redirect } from "next/navigation"; | ||||||
| import { isLoggedIn } from "../write/auth"; | import { isLoggedIn } from "@/components/auth"; | ||||||
| import FormComponent from "./Form"; | import FormComponent from "./Form"; | ||||||
|  |  | ||||||
| export default async function Login() { | export default async function Login() { | ||||||
|   | |||||||
| @@ -3,8 +3,15 @@ | |||||||
| import { useRouter } from "next/navigation"; | import { useRouter } from "next/navigation"; | ||||||
| import * as Types from "../../types"; | import * as Types from "../../types"; | ||||||
| import "highlight.js/styles/github-dark.css"; | import "highlight.js/styles/github-dark.css"; | ||||||
|  | import { publishArticle, unpublishArticle } from "../../action"; | ||||||
|  |  | ||||||
| export default function PostDisplay({ post }: { post: Types.Post }) { | export default function PostDisplay({ | ||||||
|  |   post, | ||||||
|  |   loggedIn, | ||||||
|  | }: { | ||||||
|  |   post: Types.Post; | ||||||
|  |   loggedIn: boolean; | ||||||
|  | }) { | ||||||
|   const router = useRouter(); |   const router = useRouter(); | ||||||
|   return ( |   return ( | ||||||
|     <> |     <> | ||||||
| @@ -60,8 +67,61 @@ export default function PostDisplay({ post }: { post: Types.Post }) { | |||||||
|               gap: 4, |               gap: 4, | ||||||
|             }} |             }} | ||||||
|           > |           > | ||||||
|             <span>Published: </span> |             {post.is_draft && ( | ||||||
|             <span>{post.publishedDate.toLocaleDateString()}</span> |               <> | ||||||
|  |                 <span>draft - </span> | ||||||
|  |                 <div | ||||||
|  |                   style={{ | ||||||
|  |                     cursor: "pointer", | ||||||
|  |                     color: "#999", | ||||||
|  |                     transition: "color 0.3s linear", | ||||||
|  |                   }} | ||||||
|  |                   onMouseOver={(e) => { | ||||||
|  |                     e.currentTarget.style.color = "#eee"; | ||||||
|  |                   }} | ||||||
|  |                   onMouseLeave={(e) => { | ||||||
|  |                     e.currentTarget.style.color = "#999"; | ||||||
|  |                   }} | ||||||
|  |                   onClick={async () => { | ||||||
|  |                     await publishArticle(post.slug); | ||||||
|  |                     router.refresh(); | ||||||
|  |                   }} | ||||||
|  |                 > | ||||||
|  |                   move to published | ||||||
|  |                 </div> | ||||||
|  |               </> | ||||||
|  |             )} | ||||||
|  |             {!post.is_draft && ( | ||||||
|  |               <> | ||||||
|  |                 <div> | ||||||
|  |                   <div> | ||||||
|  |                     <span>published: </span> | ||||||
|  |                     <span>{post.publishedDate.toLocaleDateString()}</span> | ||||||
|  |                   </div> | ||||||
|  |                   {loggedIn && ( | ||||||
|  |                     <div | ||||||
|  |                       style={{ | ||||||
|  |                         cursor: "pointer", | ||||||
|  |                         color: "#999", | ||||||
|  |                         transition: "color 0.3s linear", | ||||||
|  |                       }} | ||||||
|  |                       onMouseOver={(e) => { | ||||||
|  |                         e.currentTarget.style.color = "#eee"; | ||||||
|  |                       }} | ||||||
|  |                       onMouseLeave={(e) => { | ||||||
|  |                         e.currentTarget.style.color = "#999"; | ||||||
|  |                       }} | ||||||
|  |                       onClick={async () => { | ||||||
|  |                         await unpublishArticle(post.slug); | ||||||
|  |                         router.refresh(); | ||||||
|  |                       }} | ||||||
|  |                     > | ||||||
|  |                       move to drafts | ||||||
|  |                     </div> | ||||||
|  |                   )} | ||||||
|  |                 </div> | ||||||
|  |               </> | ||||||
|  |             )} | ||||||
|           </div> |           </div> | ||||||
|         </div> |         </div> | ||||||
|         <div |         <div | ||||||
|   | |||||||
| @@ -1,6 +1,7 @@ | |||||||
| import { notFound } from "next/navigation"; | import { notFound } from "next/navigation"; | ||||||
| import { getPost } from "../../action"; | import { getPost } from "../../action"; | ||||||
| import PostDisplay from "./PostDisplay"; | import PostDisplay from "./PostDisplay"; | ||||||
|  | import { isLoggedIn } from "@/components/auth"; | ||||||
|  |  | ||||||
| export default async function Post({ | export default async function Post({ | ||||||
|   params, |   params, | ||||||
| @@ -12,9 +13,10 @@ export default async function Post({ | |||||||
|   if (!post) { |   if (!post) { | ||||||
|     notFound(); |     notFound(); | ||||||
|   } |   } | ||||||
|  |   const loggedIn = await isLoggedIn(); | ||||||
|   return ( |   return ( | ||||||
|     <> |     <> | ||||||
|       <PostDisplay post={post} /> |       <PostDisplay post={post} loggedIn={loggedIn} /> | ||||||
|     </> |     </> | ||||||
|   ); |   ); | ||||||
| } | } | ||||||
|   | |||||||
| @@ -2,7 +2,7 @@ import { notFound, redirect } from "next/navigation"; | |||||||
| import { getPost } from "../../action"; | import { getPost } from "../../action"; | ||||||
| import { Post } from "../../types"; | import { Post } from "../../types"; | ||||||
| import Write from "../Write"; | import Write from "../Write"; | ||||||
| import { isLoggedIn } from "../auth"; | import { isLoggedIn } from "@/components/auth"; | ||||||
|  |  | ||||||
| export default async function WritePage({ | export default async function WritePage({ | ||||||
|   params, |   params, | ||||||
|   | |||||||
| @@ -1,5 +1,3 @@ | |||||||
| /* eslint-disable react/no-unescaped-entities */ |  | ||||||
|  |  | ||||||
| "use client"; | "use client"; | ||||||
|  |  | ||||||
| import SubmitContact from "@/components/contact"; | import SubmitContact from "@/components/contact"; | ||||||
|   | |||||||
| @@ -1,5 +1,3 @@ | |||||||
| /* eslint-disable react/no-unescaped-entities */ |  | ||||||
|  |  | ||||||
| "use client"; | "use client"; | ||||||
|  |  | ||||||
| import { SiGitea } from "@icons-pack/react-simple-icons"; | import { SiGitea } from "@icons-pack/react-simple-icons"; | ||||||
|   | |||||||
| @@ -28,17 +28,16 @@ export async function decrypt( | |||||||
|   } |   } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export async function isLoggedIn() { | export async function isLoggedIn(): Promise<boolean> { | ||||||
|   const cookieStore = (await cookies()).get("session")?.value; |   const cookieStore = (await cookies()).get("session")?.value; | ||||||
|   const session = await decrypt(cookieStore); |   const session = await decrypt(cookieStore); | ||||||
|   if (session != null && session.admin) { |   if (session != null && session.admin) { | ||||||
|     setSession(); |  | ||||||
|     return true; |     return true; | ||||||
|   } |   } | ||||||
|   return false; |   return false; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export async function setSession() { | export async function setSession(): Promise<void> { | ||||||
|   const expiresAt = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000); |   const expiresAt = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000); | ||||||
|   (await cookies()).set("session", await encrypt({ admin: true }), { |   (await cookies()).set("session", await encrypt({ admin: true }), { | ||||||
|     httpOnly: true, |     httpOnly: true, | ||||||
		Reference in New Issue
	
	Block a user