add author selection to blog create/edit pages and backend improvements for author handling

This commit is contained in:
2025-07-11 14:12:22 +02:00
parent 5b1827809c
commit 5c02fd6d57
3 changed files with 102 additions and 11 deletions

View File

@@ -17,6 +17,13 @@ import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import { Switch } from '@/components/ui/switch'
import { Textarea } from '@/components/ui/textarea'
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select'
import { api } from '@/trpc/react'
import { useParams } from 'next/navigation'
import { useRouter } from 'next/navigation'
@@ -35,12 +42,16 @@ export default function EditBlogPostPage() {
const [excerpt, setExcerpt] = useState('')
const [published, setPublished] = useState(false)
const [updateSlug, setUpdateSlug] = useState(false)
const [authorId, setAuthorId] = useState<string | undefined>(undefined)
const [isSubmitting, setIsSubmitting] = useState(false)
const [isLoading, setIsLoading] = useState(true)
// Fetch post data
const { data: posts, isLoading: isFetching } = api.blog.getAll.useQuery()
// Fetch all users for author selection
const { data: users, isLoading: isLoadingUsers } = api.blog.getAllUsers.useQuery()
useEffect(() => {
if (posts) {
const currentPost = posts.find((p) => p.id === id)
@@ -49,6 +60,7 @@ export default function EditBlogPostPage() {
setContent(currentPost.content)
setExcerpt(currentPost.excerpt || '')
setPublished(currentPost.published)
setAuthorId(currentPost.authorId)
setIsLoading(false)
} else {
toast.error('Blog post not found')
@@ -104,6 +116,7 @@ export default function EditBlogPostPage() {
excerpt: excerpt || undefined,
published,
updateSlug,
authorId,
})
}
@@ -172,6 +185,31 @@ export default function EditBlogPostPage() {
/>
</div>
<div className='space-y-2'>
<Label htmlFor='author'>Author</Label>
<Select
value={authorId}
onValueChange={setAuthorId}
>
<SelectTrigger id='author'>
<SelectValue placeholder='Select an author' />
</SelectTrigger>
<SelectContent>
{isLoadingUsers ? (
<SelectItem value='loading' disabled>
Loading...
</SelectItem>
) : (
users?.map((user) => (
<SelectItem key={user.id} value={user.id}>
{user.name}
</SelectItem>
))
)}
</SelectContent>
</Select>
</div>
<div className='space-y-2'>
<Label htmlFor='content'>Content</Label>
<MarkdownEditor

View File

@@ -10,6 +10,13 @@ import { Textarea } from '@/components/ui/textarea'
import { Switch } from '@/components/ui/switch'
import { MarkdownEditor } from '@/components/markdown-editor'
import { toast } from 'sonner'
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select'
export default function NewBlogPostPage() {
const router = useRouter()
@@ -17,8 +24,12 @@ export default function NewBlogPostPage() {
const [content, setContent] = useState('')
const [excerpt, setExcerpt] = useState('')
const [published, setPublished] = useState(false)
const [authorId, setAuthorId] = useState<string | undefined>(undefined)
const [isSubmitting, setIsSubmitting] = useState(false)
// Fetch all users for author selection
const { data: users, isLoading: isLoadingUsers } = api.blog.getAllUsers.useQuery()
const createPost = api.blog.create.useMutation({
onSuccess: () => {
toast.success('Blog post created successfully')
@@ -33,31 +44,32 @@ export default function NewBlogPostPage() {
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault()
if (!title.trim()) {
toast.error('Title is required')
return
}
if (!content.trim()) {
toast.error('Content is required')
return
}
setIsSubmitting(true)
createPost.mutate({
title,
content,
excerpt: excerpt || undefined,
published,
authorId,
})
}
return (
<div className="container py-10">
<h1 className="mb-8 text-4xl font-bold">Create New Blog Post</h1>
<form onSubmit={handleSubmit} className="space-y-8">
<div className="space-y-2">
<Label htmlFor="title">Title</Label>
@@ -69,7 +81,7 @@ export default function NewBlogPostPage() {
required
/>
</div>
<div className="space-y-2">
<Label htmlFor="excerpt">Excerpt (optional)</Label>
<Textarea
@@ -80,7 +92,32 @@ export default function NewBlogPostPage() {
className="h-24"
/>
</div>
<div className="space-y-2">
<Label htmlFor="author">Author</Label>
<Select
value={authorId}
onValueChange={setAuthorId}
>
<SelectTrigger id="author">
<SelectValue placeholder="Select an author (defaults to you)" />
</SelectTrigger>
<SelectContent>
{isLoadingUsers ? (
<SelectItem value="loading" disabled>
Loading...
</SelectItem>
) : (
users?.map((user) => (
<SelectItem key={user.id} value={user.id}>
{user.name}
</SelectItem>
))
)}
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label htmlFor="content">Content</Label>
<MarkdownEditor
@@ -90,7 +127,7 @@ export default function NewBlogPostPage() {
minHeight="500px"
/>
</div>
<div className="flex items-center space-x-2">
<Switch
id="published"
@@ -99,7 +136,7 @@ export default function NewBlogPostPage() {
/>
<Label htmlFor="published">Publish immediately</Label>
</div>
<div className="flex gap-4">
<Button
type="submit"
@@ -118,4 +155,4 @@ export default function NewBlogPostPage() {
</form>
</div>
)
}
}

View File

@@ -20,6 +20,19 @@ function generateSlug(title: string): string {
}
export const blogRouter = createTRPCRouter({
// Get all users that can be authors (admin only)
getAllUsers: adminProcedure.query(async () => {
const allUsers = await db.query.users.findMany({
columns: {
id: true,
name: true,
image: true,
},
orderBy: (users, { asc }) => [asc(users.name)],
})
return allUsers
}),
// Get all published blog posts (public)
getAllPublished: publicProcedure.query(async () => {
const posts = await db.query.blogPosts.findMany({
@@ -90,6 +103,7 @@ export const blogRouter = createTRPCRouter({
content: z.string().min(1),
excerpt: z.string().optional(),
published: z.boolean().default(false),
authorId: z.string().optional(),
})
)
.mutation(async ({ ctx, input }) => {
@@ -115,7 +129,7 @@ export const blogRouter = createTRPCRouter({
content: input.content,
excerpt: input.excerpt || null,
published: input.published,
authorId: ctx.session.user.id,
authorId: input.authorId || ctx.session.user.id,
})
.returning()
@@ -132,6 +146,7 @@ export const blogRouter = createTRPCRouter({
excerpt: z.string().optional(),
published: z.boolean(),
updateSlug: z.boolean().default(false),
authorId: z.string().optional(),
})
)
.mutation(async ({ input }) => {
@@ -171,6 +186,7 @@ export const blogRouter = createTRPCRouter({
content: input.content,
excerpt: input.excerpt || null,
published: input.published,
...(input.authorId && { authorId: input.authorId }),
})
.where(eq(blogPosts.id, input.id))
.returning()