feat(blog): wire comment moderation and writers

This commit is contained in:
2026-06-11 21:22:03 +03:30
parent f424225abc
commit 53d989f730
3 changed files with 432 additions and 114 deletions

View File

@@ -4,6 +4,7 @@ import { useEffect, useMemo, useRef, useState } from "react";
import { ArrowLeft, ArrowRight, FolderUp, ImageUp, Loader2, Save, Send, Trash2 } from "lucide-react";
import { useRouter } from "next/navigation";
import Markdown from "@/components/Markdown";
import { useAuth } from "@/contexts/AuthContext";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
@@ -27,6 +28,7 @@ const emptyForm: Types.PostCreateSchema = {
excerpt: "",
category_id: null,
tag_ids: [],
writer_ids: [],
status: "draft",
is_featured: false,
seo_title: "",
@@ -40,12 +42,14 @@ const emptyForm: Types.PostCreateSchema = {
export default function AdminBlogEditor({ postId }: Props) {
const router = useRouter();
const { user } = useAuth();
const { toast } = useToast();
const featuredInputRef = useRef<HTMLInputElement | null>(null);
const [form, setForm] = useState<Types.PostCreateSchema>(emptyForm);
const [post, setPost] = useState<Types.PostDetailSchema | null>(null);
const [categories, setCategories] = useState<Types.CategorySchema[]>([]);
const [tags, setTags] = useState<Types.TagSchema[]>([]);
const [users, setUsers] = useState<NonNullable<Types.PostListSchema["writers"]>>([]);
const [loading, setLoading] = useState(Boolean(postId));
const [saving, setSaving] = useState(false);
const [uploadingFeatured, setUploadingFeatured] = useState(false);
@@ -53,6 +57,7 @@ export default function AdminBlogEditor({ postId }: Props) {
const isNew = postId == null;
const featuredImage = post?.absolute_featured_image_preview_url || post?.absolute_featured_image_url || post?.featured_image;
const canPersistPost = form.title.trim() && form.content.trim();
const canAssignWriters = Boolean(user?.is_staff || user?.is_superuser || user?.can_review_blog_posts);
useEffect(() => {
Promise.all([api.getCategories(), api.getTags()])
@@ -63,6 +68,13 @@ export default function AdminBlogEditor({ postId }: Props) {
.catch(() => undefined);
}, []);
useEffect(() => {
if (!canAssignWriters) return;
api.listBlogWriters()
.then((data) => setUsers(data))
.catch(() => undefined);
}, [canAssignWriters]);
useEffect(() => {
if (!postId) return;
setLoading(true);
@@ -75,6 +87,7 @@ export default function AdminBlogEditor({ postId }: Props) {
excerpt: data.excerpt ?? "",
category_id: data.category?.id ?? null,
tag_ids: data.tags.map((tag) => tag.id),
writer_ids: data.writers?.map((writer) => writer.id) ?? [data.author.id],
status: data.status as Types.PostCreateSchema["status"],
is_featured: data.is_featured,
seo_title: data.seo_title ?? "",
@@ -97,6 +110,7 @@ export default function AdminBlogEditor({ postId }: Props) {
}, [postId, toast]);
const selectedTagIds = useMemo(() => form.tag_ids ?? [], [form.tag_ids]);
const selectedWriterIds = useMemo(() => form.writer_ids ?? [], [form.writer_ids]);
const updateForm = <K extends keyof Types.PostCreateSchema>(key: K, value: Types.PostCreateSchema[K]) => {
setForm((prev) => ({ ...prev, [key]: value }));
@@ -322,6 +336,37 @@ export default function AdminBlogEditor({ postId }: Props) {
})}
</div>
</div>
{canAssignWriters ? (
<div>
<Label className="mb-2 block text-right">نویسندگان</Label>
<div className="flex flex-wrap justify-end gap-2">
{users.map((writer) => {
const selected = selectedWriterIds.includes(writer.id);
const fullName = [writer.first_name, writer.last_name].filter(Boolean).join(" ") || writer.username;
return (
<Button
key={writer.id}
type="button"
size="sm"
variant={selected ? "default" : "outline"}
onClick={() => {
updateForm(
"writer_ids",
selected ? selectedWriterIds.filter((id) => id !== writer.id) : [...selectedWriterIds, writer.id],
);
}}
>
{fullName}
</Button>
);
})}
</div>
<p className="mt-2 text-right text-xs text-muted-foreground">
مالک اصلی نوشته تغییر نمیکند؛ این گزینه فقط لیست نویسندگان عمومی را تنظیم میکند.
</p>
</div>
) : null}
</CardContent>
</Card>