This commit is contained in:
2026-04-23 11:08:45 +02:00
parent c527391259
commit daab745c13
13 changed files with 504 additions and 25 deletions

View File

@@ -25,7 +25,8 @@ export default function AdminSideBar() {
<Link href={"/admin/music"}> Manage Music </Link>
</SimpleSidebarGroup>
<SimpleSidebarGroup lable="Blog">
<Link href={"/"}> Some Blog Action </Link>
<Link href={"/admin/blog/create"}> Create Post </Link>
<Link href={"/admin/blog/list"}> Post List </Link>
</SimpleSidebarGroup>
<SimpleSidebarGroup lable="Chat">
<Link href={"/admin/chat"}> System Prompt </Link>

View File

@@ -0,0 +1,11 @@
'use client'
import { trpc } from '~/app/_trpc/Client'
import { useParams } from 'next/navigation'
import CreateUpdateBlogForm from '../_components/CreateUpdateForm'
export default function Page() {
const { slug } = useParams<{ slug: string }>()
const { data } = trpc.blog.bySlug.useQuery(slug)
if (data) return <CreateUpdateBlogForm entity={data} />
return <></>
}

View File

@@ -0,0 +1,93 @@
'use client'
import { zodResolver } from '@hookform/resolvers/zod'
import { useForm } from 'react-hook-form'
import { z } from 'zod'
import { trpc } from '~/app/_trpc/Client'
import { FormScaffold } from '~/app/_components/Form/Components'
import { FormMutationContextProvider } from '~/app/_components/Form/Components/MutationProvider'
import { useState } from 'react'
import { TextInputFormField, MdeFormField } from '~/app/_components/Form/Fields'
import { usePathname, useRouter } from 'next/navigation'
import { useTheme } from 'next-themes'
import type { RouterOutputs } from '~/server/routers/_app'
type BlogPost = RouterOutputs['blog']['bySlug']
const blogPostSchema = z.object({
slug: z.string().min(1),
title: z.string().min(1),
date: z.string().optional(),
description: z.string().optional(),
content: z.string(),
})
export default function CreateUpdateBlogForm(params: { className?: string, entity?: BlogPost }) {
const [slug, setSlug] = useState<string | undefined>(params.entity?.slug)
const [originalSlug, setOriginalSlug] = useState<string | undefined>(params.entity?.slug)
const { theme } = useTheme()
const form = useForm<z.infer<typeof blogPostSchema>>({
resolver: zodResolver(blogPostSchema),
defaultValues: {
slug: params.entity?.slug ?? '',
title: params.entity?.title ?? '',
date: params.entity?.date ?? '',
description: params.entity?.description ?? '',
content: params.entity?.content ?? '',
},
})
const path = usePathname()
const router = useRouter()
const createMutation = trpc.blog.insert.useMutation({
onSuccess: (data) => {
if (data[0]) {
setSlug(data[0].slug)
setOriginalSlug(data[0].slug)
}
},
})
const updateMutation = trpc.blog.update.useMutation({
onSuccess: (data) => {
if (data[0]) {
setSlug(data[0].slug)
setOriginalSlug(data[0].slug)
}
},
})
const deleteMutation = trpc.blog.delete.useMutation({
onSuccess: () => {
if (path.includes('list')) { router.refresh(); return }
router.back()
},
})
function onSubmit(values: z.infer<typeof blogPostSchema>) {
if (slug && originalSlug) {
updateMutation.mutate({ ...values, originalSlug })
} else {
createMutation.mutate(values)
}
}
return (
<FormMutationContextProvider value={{
createMutation: createMutation,
updateMutation: updateMutation,
deleteMutation: deleteMutation,
}}>
<FormScaffold
form={form}
onSubmit={onSubmit}
title='Blog Post'
id={slug}
className={params.className}
>
<TextInputFormField control={form.control} name='slug' label='Slug' />
<TextInputFormField control={form.control} name='title' label='Title' />
<TextInputFormField control={form.control} name='date' label='Date (YYYY-MM-DD)' />
<TextInputFormField control={form.control} name='description' label='Description' />
<MdeFormField control={form.control} name='content' label='Content' dataColorMode={(theme as 'dark' | 'light') ?? 'dark'} />
</FormScaffold>
</FormMutationContextProvider>
)
}

View File

@@ -0,0 +1,6 @@
'use client'
import CreateUpdateBlogForm from '../_components/CreateUpdateForm'
export default function Page() {
return <CreateUpdateBlogForm />
}

View File

@@ -0,0 +1,40 @@
'use client'
import Link from 'next/link'
import { trpc } from '~/app/_trpc/Client'
import { useGSAP } from '@gsap/react'
import { useRef } from 'react'
import * as Card from '~/components/ui/card'
import { useGsapContext } from '~/app/_providers/GsapProvicer'
import { CollapsibleForm } from '~/app/_components/Form/Components'
import CreateUpdateBlogForm from '../_components/CreateUpdateForm'
export default function BlogListPage() {
const posts = trpc.blog.list.useQuery(undefined, { refetchInterval: 5000 })
const gsap = useGsapContext()
const container = useRef<HTMLDivElement>(null)
useGSAP(() => {
gsap?.from('.gsapan', { x: -100, opacity: 0, duration: 0.5, stagger: { each: 0.3 } })
}, { scope: container, dependencies: [posts.status], revertOnUpdate: true })
return (
<div ref={container} className='w-5/6 lg:w-1/2 flex flex-col gap-3'>
{posts.data == undefined ?
<div className='gsapan' /> :
<>
{posts.data.map((post) => (
<Card.Card className='gsapan' key={post.slug}>
<Link href={`/admin/blog/${post.slug}`}>
<Card.CardHeader>
<Card.CardTitle>{post.title}</Card.CardTitle>
{post.date && <p className='text-sm text-muted-foreground'>{post.date}</p>}
{post.description && <p className='text-sm text-muted-foreground'>{post.description}</p>}
</Card.CardHeader>
</Link>
</Card.Card>
))}
<CollapsibleForm entityName='Blog Post' form={CreateUpdateBlogForm} />
</>
}
</div>
)
}

View File

@@ -0,0 +1,40 @@
import { notFound } from "next/navigation";
import { MDXRemote } from "next-mdx-remote/rsc";
import { TRPCError } from "@trpc/server";
import { servTrpc } from "~/app/_trpc/ServerClient";
type Props = {
params: Promise<{ slug: string }>;
};
export default async function BlogPostPage({ params }: Props) {
const { slug } = await params;
let post: Awaited<ReturnType<typeof servTrpc.blog.bySlug>>;
try {
post = await servTrpc.blog.bySlug(slug);
} catch (e) {
if (e instanceof TRPCError && e.code === "NOT_FOUND") notFound();
throw e;
}
return (
<main className="mx-auto max-w-2xl px-4 py-12">
<header className="mb-8">
<h1 className="text-3xl font-bold">{post.title}</h1>
{post.date && (
<time className="text-muted-foreground text-sm">
{new Date(post.date).toLocaleDateString("en-US", {
year: "numeric",
month: "long",
day: "numeric",
})}
</time>
)}
</header>
<article className="prose dark:prose-invert max-w-none">
<MDXRemote source={post.content} />
</article>
</main>
);
}

View File

@@ -1,10 +1,3 @@
'use client'
export default function RootLayout({
children,
}: Readonly<{ children: React.ReactNode}>) {
return (
<>
{children}
</>
)
export default function BlogLayout({ children }: { children: React.ReactNode }) {
return <>{children}</>;
}

View File

@@ -1,12 +1,37 @@
'use client'
import Link from "next/link";
import { servTrpc } from "~/app/_trpc/ServerClient";
import { usePathname } from "next/navigation"
export default async function BlogPage() {
const posts = await servTrpc.blog.list();
export default function Page() {
const pathName = usePathname()
return (
<div>
{pathName}
</div>
)
<main className="mx-auto max-w-2xl px-4 py-12">
<h1 className="mb-8 text-3xl font-bold">Blog</h1>
{posts.length === 0 ? (
<p className="text-muted-foreground">No posts yet.</p>
) : (
<ul className="space-y-6">
{posts.map((post) => (
<li key={post.slug}>
<Link href={`/blog/${post.slug}`} className="group block">
<h2 className="text-xl font-semibold group-hover:underline">{post.title}</h2>
{post.date && (
<time className="text-muted-foreground text-sm">
{new Date(post.date).toLocaleDateString("en-US", {
year: "numeric",
month: "long",
day: "numeric",
})}
</time>
)}
{post.description && (
<p className="text-muted-foreground mt-1">{post.description}</p>
)}
</Link>
</li>
))}
</ul>
)}
</main>
);
}