mirror of
https://github.com/ershisan99/www.git
synced 2026-01-06 05:12:06 +00:00
add author selection to blog create/edit pages and backend improvements for author handling
This commit is contained in:
@@ -17,6 +17,13 @@ import { Input } from '@/components/ui/input'
|
|||||||
import { Label } from '@/components/ui/label'
|
import { Label } from '@/components/ui/label'
|
||||||
import { Switch } from '@/components/ui/switch'
|
import { Switch } from '@/components/ui/switch'
|
||||||
import { Textarea } from '@/components/ui/textarea'
|
import { Textarea } from '@/components/ui/textarea'
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from '@/components/ui/select'
|
||||||
import { api } from '@/trpc/react'
|
import { api } from '@/trpc/react'
|
||||||
import { useParams } from 'next/navigation'
|
import { useParams } from 'next/navigation'
|
||||||
import { useRouter } from 'next/navigation'
|
import { useRouter } from 'next/navigation'
|
||||||
@@ -35,12 +42,16 @@ export default function EditBlogPostPage() {
|
|||||||
const [excerpt, setExcerpt] = useState('')
|
const [excerpt, setExcerpt] = useState('')
|
||||||
const [published, setPublished] = useState(false)
|
const [published, setPublished] = useState(false)
|
||||||
const [updateSlug, setUpdateSlug] = useState(false)
|
const [updateSlug, setUpdateSlug] = useState(false)
|
||||||
|
const [authorId, setAuthorId] = useState<string | undefined>(undefined)
|
||||||
const [isSubmitting, setIsSubmitting] = useState(false)
|
const [isSubmitting, setIsSubmitting] = useState(false)
|
||||||
const [isLoading, setIsLoading] = useState(true)
|
const [isLoading, setIsLoading] = useState(true)
|
||||||
|
|
||||||
// Fetch post data
|
// Fetch post data
|
||||||
const { data: posts, isLoading: isFetching } = api.blog.getAll.useQuery()
|
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(() => {
|
useEffect(() => {
|
||||||
if (posts) {
|
if (posts) {
|
||||||
const currentPost = posts.find((p) => p.id === id)
|
const currentPost = posts.find((p) => p.id === id)
|
||||||
@@ -49,6 +60,7 @@ export default function EditBlogPostPage() {
|
|||||||
setContent(currentPost.content)
|
setContent(currentPost.content)
|
||||||
setExcerpt(currentPost.excerpt || '')
|
setExcerpt(currentPost.excerpt || '')
|
||||||
setPublished(currentPost.published)
|
setPublished(currentPost.published)
|
||||||
|
setAuthorId(currentPost.authorId)
|
||||||
setIsLoading(false)
|
setIsLoading(false)
|
||||||
} else {
|
} else {
|
||||||
toast.error('Blog post not found')
|
toast.error('Blog post not found')
|
||||||
@@ -104,6 +116,7 @@ export default function EditBlogPostPage() {
|
|||||||
excerpt: excerpt || undefined,
|
excerpt: excerpt || undefined,
|
||||||
published,
|
published,
|
||||||
updateSlug,
|
updateSlug,
|
||||||
|
authorId,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -172,6 +185,31 @@ export default function EditBlogPostPage() {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</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'>
|
<div className='space-y-2'>
|
||||||
<Label htmlFor='content'>Content</Label>
|
<Label htmlFor='content'>Content</Label>
|
||||||
<MarkdownEditor
|
<MarkdownEditor
|
||||||
|
|||||||
@@ -10,6 +10,13 @@ import { Textarea } from '@/components/ui/textarea'
|
|||||||
import { Switch } from '@/components/ui/switch'
|
import { Switch } from '@/components/ui/switch'
|
||||||
import { MarkdownEditor } from '@/components/markdown-editor'
|
import { MarkdownEditor } from '@/components/markdown-editor'
|
||||||
import { toast } from 'sonner'
|
import { toast } from 'sonner'
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from '@/components/ui/select'
|
||||||
|
|
||||||
export default function NewBlogPostPage() {
|
export default function NewBlogPostPage() {
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
@@ -17,8 +24,12 @@ export default function NewBlogPostPage() {
|
|||||||
const [content, setContent] = useState('')
|
const [content, setContent] = useState('')
|
||||||
const [excerpt, setExcerpt] = useState('')
|
const [excerpt, setExcerpt] = useState('')
|
||||||
const [published, setPublished] = useState(false)
|
const [published, setPublished] = useState(false)
|
||||||
|
const [authorId, setAuthorId] = useState<string | undefined>(undefined)
|
||||||
const [isSubmitting, setIsSubmitting] = useState(false)
|
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({
|
const createPost = api.blog.create.useMutation({
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
toast.success('Blog post created successfully')
|
toast.success('Blog post created successfully')
|
||||||
@@ -33,31 +44,32 @@ export default function NewBlogPostPage() {
|
|||||||
|
|
||||||
const handleSubmit = async (e: React.FormEvent) => {
|
const handleSubmit = async (e: React.FormEvent) => {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
|
|
||||||
if (!title.trim()) {
|
if (!title.trim()) {
|
||||||
toast.error('Title is required')
|
toast.error('Title is required')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!content.trim()) {
|
if (!content.trim()) {
|
||||||
toast.error('Content is required')
|
toast.error('Content is required')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
setIsSubmitting(true)
|
setIsSubmitting(true)
|
||||||
|
|
||||||
createPost.mutate({
|
createPost.mutate({
|
||||||
title,
|
title,
|
||||||
content,
|
content,
|
||||||
excerpt: excerpt || undefined,
|
excerpt: excerpt || undefined,
|
||||||
published,
|
published,
|
||||||
|
authorId,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="container py-10">
|
<div className="container py-10">
|
||||||
<h1 className="mb-8 text-4xl font-bold">Create New Blog Post</h1>
|
<h1 className="mb-8 text-4xl font-bold">Create New Blog Post</h1>
|
||||||
|
|
||||||
<form onSubmit={handleSubmit} className="space-y-8">
|
<form onSubmit={handleSubmit} className="space-y-8">
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="title">Title</Label>
|
<Label htmlFor="title">Title</Label>
|
||||||
@@ -69,7 +81,7 @@ export default function NewBlogPostPage() {
|
|||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="excerpt">Excerpt (optional)</Label>
|
<Label htmlFor="excerpt">Excerpt (optional)</Label>
|
||||||
<Textarea
|
<Textarea
|
||||||
@@ -80,7 +92,32 @@ export default function NewBlogPostPage() {
|
|||||||
className="h-24"
|
className="h-24"
|
||||||
/>
|
/>
|
||||||
</div>
|
</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">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="content">Content</Label>
|
<Label htmlFor="content">Content</Label>
|
||||||
<MarkdownEditor
|
<MarkdownEditor
|
||||||
@@ -90,7 +127,7 @@ export default function NewBlogPostPage() {
|
|||||||
minHeight="500px"
|
minHeight="500px"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-2">
|
||||||
<Switch
|
<Switch
|
||||||
id="published"
|
id="published"
|
||||||
@@ -99,7 +136,7 @@ export default function NewBlogPostPage() {
|
|||||||
/>
|
/>
|
||||||
<Label htmlFor="published">Publish immediately</Label>
|
<Label htmlFor="published">Publish immediately</Label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex gap-4">
|
<div className="flex gap-4">
|
||||||
<Button
|
<Button
|
||||||
type="submit"
|
type="submit"
|
||||||
@@ -118,4 +155,4 @@ export default function NewBlogPostPage() {
|
|||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,6 +20,19 @@ function generateSlug(title: string): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const blogRouter = createTRPCRouter({
|
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)
|
// Get all published blog posts (public)
|
||||||
getAllPublished: publicProcedure.query(async () => {
|
getAllPublished: publicProcedure.query(async () => {
|
||||||
const posts = await db.query.blogPosts.findMany({
|
const posts = await db.query.blogPosts.findMany({
|
||||||
@@ -90,6 +103,7 @@ export const blogRouter = createTRPCRouter({
|
|||||||
content: z.string().min(1),
|
content: z.string().min(1),
|
||||||
excerpt: z.string().optional(),
|
excerpt: z.string().optional(),
|
||||||
published: z.boolean().default(false),
|
published: z.boolean().default(false),
|
||||||
|
authorId: z.string().optional(),
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
.mutation(async ({ ctx, input }) => {
|
.mutation(async ({ ctx, input }) => {
|
||||||
@@ -115,7 +129,7 @@ export const blogRouter = createTRPCRouter({
|
|||||||
content: input.content,
|
content: input.content,
|
||||||
excerpt: input.excerpt || null,
|
excerpt: input.excerpt || null,
|
||||||
published: input.published,
|
published: input.published,
|
||||||
authorId: ctx.session.user.id,
|
authorId: input.authorId || ctx.session.user.id,
|
||||||
})
|
})
|
||||||
.returning()
|
.returning()
|
||||||
|
|
||||||
@@ -132,6 +146,7 @@ export const blogRouter = createTRPCRouter({
|
|||||||
excerpt: z.string().optional(),
|
excerpt: z.string().optional(),
|
||||||
published: z.boolean(),
|
published: z.boolean(),
|
||||||
updateSlug: z.boolean().default(false),
|
updateSlug: z.boolean().default(false),
|
||||||
|
authorId: z.string().optional(),
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
.mutation(async ({ input }) => {
|
.mutation(async ({ input }) => {
|
||||||
@@ -171,6 +186,7 @@ export const blogRouter = createTRPCRouter({
|
|||||||
content: input.content,
|
content: input.content,
|
||||||
excerpt: input.excerpt || null,
|
excerpt: input.excerpt || null,
|
||||||
published: input.published,
|
published: input.published,
|
||||||
|
...(input.authorId && { authorId: input.authorId }),
|
||||||
})
|
})
|
||||||
.where(eq(blogPosts.id, input.id))
|
.where(eq(blogPosts.id, input.id))
|
||||||
.returning()
|
.returning()
|
||||||
|
|||||||
Reference in New Issue
Block a user