import { useEffect, useRef, useState } from 'react' import DOMPurify from 'dompurify' import { useNavigate } from '@tanstack/react-router' import { useForm } from 'react-hook-form' import { zodResolver } from '@hookform/resolvers/zod' import { z } from 'zod' import { ArrowLeft, Loader2, Save, Trash2, X } from 'lucide-react' import { Button } from '@components/ui/button' import { Input } from '@components/ui/input' import { Textarea } from '@components/ui/textarea' import { Switch } from '@components/ui/switch' import { Badge } from '@components/ui/badge' import { Card, CardContent, CardDescription, CardHeader, CardTitle, } from '@components/ui/card' import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage, } from '@components/ui/form' import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, AlertDialogTrigger, } from '@components/ui/alert-dialog' import { Skeleton } from '@components/ui/skeleton' import BundledEditor from '../BundledEditor' import type { Editor } from 'tinymce' import { useAdminArticle, useCreateArticle, useDeleteArticle, useUpdateArticle, } from '@/hooks/use-articles' const articleSchema = z.object({ title: z.string().min(1, 'Title is required').max(200, 'Title is too long'), preview: z .string() .min(1, 'Preview is required') .max(500, 'Preview must be 500 characters or less'), content: z.string().min(1, 'Content is required'), coverImage: z .string() .url('Must be a valid URL') .optional() .or(z.literal('')), tags: z.array(z.string()), published: z.boolean(), }) type ArticleFormValues = z.infer interface ArticleEditorProps { articleId?: string } export function ArticleEditor({ articleId }: ArticleEditorProps) { const navigate = useNavigate() const isEdit = !!articleId const { data: article, isLoading } = useAdminArticle(articleId || '') const createArticle = useCreateArticle() const updateArticle = useUpdateArticle(articleId || '') const deleteArticle = useDeleteArticle() const [tagInput, setTagInput] = useState('') const form = useForm({ resolver: zodResolver(articleSchema), defaultValues: { title: '', preview: '', content: '', coverImage: '', tags: [], published: false, }, }) useEffect(() => { if (article) { const sanitizedContent = DOMPurify.sanitize(article.content) form.reset({ title: article.title, preview: article.preview, content: sanitizedContent, coverImage: article.coverImage || '', tags: article.tags, published: article.published, }) } }, [article, form]) const onSubmit = (data: ArticleFormValues) => { const payload = { ...data, coverImage: data.coverImage || undefined, } if (isEdit) { updateArticle.mutate(payload, { onSuccess: () => navigate({ to: '/admin/articles' }), }) } else { createArticle.mutate(payload, { onSuccess: () => navigate({ to: '/admin/articles' }), }) } } const handleDelete = () => { if (articleId) { deleteArticle.mutate(articleId, { onSuccess: () => navigate({ to: '/admin/articles' }), }) } } const handleAddTag = () => { const tag = tagInput.trim().toLowerCase() if (tag && !form.getValues('tags').includes(tag)) { form.setValue('tags', [...form.getValues('tags'), tag]) setTagInput('') } } const handleRemoveTag = (tagToRemove: string) => { form.setValue( 'tags', form.getValues('tags').filter((tag) => tag !== tagToRemove), ) } const handleKeyDown = (e: React.KeyboardEvent) => { if (e.key === 'Enter') { e.preventDefault() handleAddTag() } } const editorRef = useRef(null) const isPending = createArticle.isPending || updateArticle.isPending if (isEdit && isLoading) { return (
) } return (

{isEdit ? 'Edit Article' : 'New Article'}

{isEdit ? 'Update your article' : 'Create a new blog post'}

{isEdit && ( Delete Article Are you sure you want to delete this article? This action cannot be undone. Cancel Delete )}
Content Write your article content ( Title )} /> ( Preview