'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 } from 'lucide-react' import { PartialBlock } from '@blocknote/core' // Dynamic import to avoid SSR issues with BlockNote const BlockNoteEditor = dynamic(() => import('@/components/BlockNoteEditor'), { ssr: false, loading: () => (
Loading editor...
), }) // Form schema 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 CreateTopicPage() { return ( ) } function CreateTopicContent() { const router = useRouter() const params = useParams() const { user } = useAuth() const { toast } = useToast() const topicId = params?.id as string const isEditMode = topicId && topicId !== 'new' const [availableTags, setAvailableTags] = useState([]) const [isLoading, setIsLoading] = useState(true) const [isSubmitting, setIsSubmitting] = useState(false) const [topicData, setTopicData] = useState(null) // 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 available tags and topic data if editing useEffect(() => { const fetchData = async () => { try { // Fetch available tags const tagsResponse = await fetch('/api/tags') if (tagsResponse.ok) { const tagsData = await tagsResponse.json() if (tagsData.success) { setAvailableTags(tagsData.data || []) } } // If editing, fetch topic data if (isEditMode) { const response = await fetch(`/api/topic/${topicId}`, { credentials: 'include', }) if (!response.ok) { if (response.status === 404) { toast({ title: 'Topic not found', description: 'The topic you are trying to edit does not exist.', variant: 'destructive', }) router.push('/dashboard') return } if (response.status === 403) { toast({ title: 'Access denied', description: 'You can only edit your own topics.', variant: 'destructive', }) router.push('/dashboard') return } throw new Error('Failed to fetch topic data') } const result = await response.json() if (!result.success) { throw new Error(result.error?.message || 'Failed to fetch topic data') } const topic = result.data setTopicData(topic) // Populate form with existing data form.reset({ title: topic.title, excerpt: topic.excerpt, content: topic.content, contentRTE: topic.contentRTE || [], contentImages: topic.contentImages || [], coverImage: topic.coverImage, tags: topic.tags || [], featured: topic.featured || false, isDraft: topic.isDraft || false, }) // Set editor content editorContentRef.current = { contentRTE: topic.contentRTE || [], contentImages: topic.contentImages || [], content: topic.content, } } } catch (error) { console.error('Failed to fetch data:', error) toast({ title: 'Error', description: isEditMode ? 'Failed to load topic data for editing.' : 'Failed to load existing tags, but you can still create new ones', variant: 'destructive', }) if (isEditMode) { router.push('/dashboard') } } finally { setIsLoading(false) } } fetchData() }, [isEditMode, topicId, 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 onSubmit = async (data: any, isDraft: boolean = false) => { try { setIsSubmitting(true) // Create FormData object (like dev-portfolio) const formData = new FormData() // 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 if (key === 'isDraft') { // Skip isDraft here - we handle it separately below } 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(isDraft)) // 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)) // Use dev-portfolio pattern: POST with topicId query param for updates const url = isEditMode ? `/api/topic?topicId=${topicId}` : '/api/topic' const response = await fetch(url, { method: 'POST', body: formData, credentials: 'include', // Include authentication cookies }) const result = await response.json() if (!result.success) { throw new Error(result.error?.message || `Failed to ${isEditMode ? 'update' : 'create'} topic`) } const statusMessage = isDraft ? 'saved as draft' : 'published' toast({ title: 'Success!', description: `Topic ${isEditMode ? 'updated' : 'created'} and ${statusMessage}!`, }) // Redirect based on draft status - drafts go to dashboard, published go to topic page const slug = result.data.slug || topicData?.slug router.push(isDraft ? '/dashboard' : `/topics/${slug}`) } catch (error) { console.error(`Failed to ${isEditMode ? 'update' : 'create'} topic:`, error) toast({ title: 'Error', description: error instanceof Error ? error.message : `Failed to ${isEditMode ? 'update' : 'create'} topic`, variant: 'destructive', }) } finally { setIsSubmitting(false) } } const handleSaveAsDraft = () => { const data = form.getValues() onSubmit(data, true) } if (isLoading) { return (

Loading...

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

{isEditMode ? 'Edit Topic' : 'Create Topic'}

{isEditMode && topicData && (

Editing: {topicData.title}

)}
{/* Form */}
onSubmit(data, false))} className="max-w-4xl mx-auto space-y-8"> {/* Header Fields */}
{/* Title */} ( Title )} /> {/* Author + Date Row */}
Author Publication Date
{/* Excerpt */} ( Excerpt