'use client' import { useState, useEffect, useRef } from 'react' import { useRouter, useParams } from 'next/navigation' import { useForm } from 'react-hook-form' import { zodResolver } from '@hookform/resolvers/zod' import { z } from 'zod' import dynamic from 'next/dynamic' import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage, } from '@/components/ui/form' import { Input } from '@/components/ui/input' import { Button } from '@/components/ui/button' import { Textarea } from '@/components/ui/textarea' import { Checkbox } from '@/components/ui/checkbox' import { Header } from '@/components/header' import { Footer } from '@/components/footer' import ImageUpload from '@/components/topics/ImageUpload' import SearchableTagSelect, { Tag } from '@/components/topics/SearchableTagSelect' import { RequireAuth } from '@/components/auth/RequireAuth' import { useAuth } from '@/contexts/AuthContext' import { useToast } from '@/hooks/use-toast' import { ArrowLeft, Save, FileText, BookOpen, Trash2 } from 'lucide-react' import { PartialBlock } from '@blocknote/core' import { ITopic } from '@/models/topic' // Dynamic import to avoid SSR issues with BlockNote const BlockNoteEditor = dynamic(() => import('@/components/BlockNoteEditor'), { ssr: false, loading: () => (
Loading editor...
), }) // Form schema (same as create page) const topicFormSchema = z.object({ title: z.string().min(1, 'Title is required').max(100, 'Title must be 100 characters or less'), excerpt: z.string().min(1, 'Excerpt is required').max(200, 'Excerpt must be 200 characters or less'), content: z.string().min(1, 'Content is required'), contentRTE: z.array(z.any()).optional(), contentImages: z.array(z.string()).optional(), coverImage: z.union([ z.instanceof(File).refine(file => file.size <= 5 * 1024 * 1024, 'Cover image must be less than 5MB'), z.string() ]).optional(), tags: z .array( z.object({ id: z.string().optional(), name: z.string().min(1, 'Tag name is required'), }) ) .min(1, 'At least one tag is required') .max(5, 'Maximum 5 tags allowed'), featured: z.boolean().optional().default(false), isDraft: z.boolean().optional().default(false), }) export default function EditTopicPage() { return ( ) } function EditTopicContent() { const router = useRouter() const params = useParams() const { user } = useAuth() const { toast } = useToast() const [topic, setTopic] = useState(null) const [availableTags, setAvailableTags] = useState([]) const [isLoading, setIsLoading] = useState(true) const [isSubmitting, setIsSubmitting] = useState(false) const [isDeleting, setIsDeleting] = useState(false) // Editor content ref for storing RTE data const editorContentRef = useRef({ contentRTE: [] as PartialBlock[], contentImages: [] as string[], content: '', }) // Initialize form const form = useForm({ resolver: zodResolver(topicFormSchema), defaultValues: { title: '', excerpt: '', content: '', contentRTE: [] as any[], contentImages: [] as string[], coverImage: '', tags: [] as { name: string; id?: string }[], featured: false, isDraft: false, }, }) // Load topic and tags useEffect(() => { const fetchData = async () => { try { // Fetch topic by slug const topicResponse = await fetch(`/api/topic/slug/${params.slug}`) if (!topicResponse.ok) { throw new Error('Topic not found') } const topicData = await topicResponse.json() if (!topicData.success) { throw new Error('Topic not found') } const topicPost = topicData.data as ITopic // Check if user owns this topic if (topicPost.authorId !== user?.id) { toast({ title: 'Error', description: 'You are not authorized to edit this topic', variant: 'destructive', }) router.push('/topics') return } setTopic(topicPost) // Set form values form.reset({ title: topicPost.title, excerpt: topicPost.excerpt, content: topicPost.content, contentRTE: topicPost.contentRTE as any[], contentImages: topicPost.contentImages || [], coverImage: topicPost.coverImage, tags: topicPost.tags, featured: topicPost.featured, isDraft: topicPost.isDraft, }) // Set editor content editorContentRef.current = { contentRTE: topicPost.contentRTE as PartialBlock[], contentImages: topicPost.contentImages || [], content: topicPost.content, } // Fetch available tags const tagsResponse = await fetch('/api/tags') if (tagsResponse.ok) { const tagsData = await tagsResponse.json() if (tagsData.success) { setAvailableTags(tagsData.data || []) } } } catch (error) { console.error('Failed to fetch topic:', error) toast({ title: 'Error', description: 'Failed to load topic post', variant: 'destructive', }) router.push('/topics') } finally { setIsLoading(false) } } if (params.slug && user) { fetchData() } }, [params.slug, user, form, router, toast]) // Handle tag creation const handleCreateTag = async (tagName: string) => { const newTag: Tag = { name: tagName, id: `temp-${Date.now()}`, } setAvailableTags(prev => [...prev, newTag]) return newTag } // Handle editor content changes const handleEditorChange = (blocks: any[], html: string) => { editorContentRef.current = { contentRTE: blocks, contentImages: [], // Will be populated by editor if it tracks images content: html, } // Update form values form.setValue('content', html) form.setValue('contentRTE', blocks) form.setValue('contentImages', editorContentRef.current.contentImages) } // Handle form submission const handleSubmit = async (data: any, saveAsDraft = false) => { if (!topic) return try { setIsSubmitting(true) // Create FormData object (like dev-portfolio) const formData = new FormData() // Add topic ID for update formData.append('topicId', topic.id) // Append all form fields to FormData Object.entries(data).forEach(([key, value]) => { if (key === 'tags') { // Handle tags array - ensure each tag has id and name const validTags = (value as Tag[]).map((tag) => ({ id: tag.id, name: tag.name, })) formData.append('tags', JSON.stringify(validTags)) } else if (key === 'coverImage') { // Only append if it's a new File object if (value instanceof File) { formData.append('coverImage', value) } // Skip if it's a string (existing image URL) or empty object } else { // Convert all other values to string formData.append(key, String(value)) } }) // Add additional fields formData.append('author', user?.name || 'Anonymous') formData.append('authorId', user?.id || '') formData.append('isDraft', String(saveAsDraft)) // Handle content fields formData.delete('content') formData.delete('contentRTE') formData.delete('contentImages') formData.append('contentRTE', JSON.stringify(editorContentRef.current.contentRTE)) formData.append('content', editorContentRef.current.content) formData.append('contentImages', JSON.stringify(editorContentRef.current.contentImages)) const response = await fetch(`/api/topic?topicId=${topic.id}`, { method: 'POST', body: formData, credentials: 'include', }) const result = await response.json() if (!result.success) { throw new Error(result.error?.message || 'Failed to update topic') } toast({ title: 'Success!', description: saveAsDraft ? 'Topic updated and saved as draft' : 'Topic updated successfully', }) // Redirect to the updated topic router.push(`/topics/${result.data.slug}`) } catch (error) { console.error('Failed to update topic:', error) toast({ title: 'Error', description: error instanceof Error ? error.message : 'Failed to update topic', variant: 'destructive', }) } finally { setIsSubmitting(false) } } // Handle topic deletion const handleDelete = async () => { if (!topic) return if (!confirm('Are you sure you want to delete this topic? This action cannot be undone.')) { return } try { setIsDeleting(true) const response = await fetch(`/api/topic?id=${topic.id}`, { method: 'DELETE', credentials: 'include', }) const result = await response.json() if (!result.success) { throw new Error(result.error?.message || 'Failed to delete topic') } toast({ title: 'Success!', description: 'Topic deleted successfully', }) router.push('/topics') } catch (error) { console.error('Failed to delete topic:', error) toast({ title: 'Error', description: error instanceof Error ? error.message : 'Failed to delete topic', variant: 'destructive', }) } finally { setIsDeleting(false) } } const onSubmit = (data: any) => { handleSubmit(data, data.isDraft) } const handleSaveAsDraft = () => { const formData = form.getValues() handleSubmit(formData, true) } if (isLoading) { return (

Loading topic...

) } if (!topic) { return (

Topic not found

) } return (
{/* Page Header */}

Edit Topic

{/* Form */}
{/* Header Fields */}
{/* Title */} ( Title )} /> {/* Author + Date Row */}
Author Publication Date
{/* Excerpt */} ( Excerpt