import React, { useState } from 'react'; import { useParams, useNavigate } from 'react-router-dom'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { useForm, Controller } from 'react-hook-form'; import { zodResolver } from '@hookform/resolvers/zod'; import { z } from 'zod'; import { toast } from 'sonner'; import { CKEditor } from '@ckeditor/ckeditor5-react'; import ClassicEditor from '@ckeditor/ckeditor5-build-classic'; import { api } from '../lib/api'; import type { ApiResponse } from '../lib/api'; import type { Blog } from '../types'; import { useAuthStore } from '../stores/authStore'; import PageHeader from '../components/ui/PageHeader'; import SearchableSelect from '../components/ui/SearchableSelect'; const schema = z.object({ title: z.string().min(3, 'عنوان الزامی است'), summary: z.string().optional(), body: z.string().min(10, 'محتوا الزامی است'), tags: z.string().optional(), status: z.enum(['draft', 'published']), image_url: z.string().optional(), }); type FormData = z.infer; async function uploadBlogImage(file: File): Promise { const token = useAuthStore.getState().token; const fd = new FormData(); fd.append('file', file); const res = await fetch('/file/upload/clinic_pro/blog/field_image', { method: 'POST', headers: { ...(token ? { Authorization: `Bearer ${token}` } : {}) }, body: fd, }); const json = await res.json().catch(() => ({})); if (res.status === 401) { useAuthStore.getState().logout(); window.location.replace('/admin/login'); throw new Error('نشست منقضی شده است'); } if (!res.ok) throw new Error(json?.errors?.[0]?.message ?? 'خطا در آپلود تصویر'); return json.data.image_url as string; } export default function BlogFormPage() { const { uuid } = useParams<{ uuid: string }>(); const navigate = useNavigate(); const qc = useQueryClient(); const isEdit = !!uuid; const [uploading, setUploading] = useState(false); const { data, isLoading } = useQuery({ queryKey: ['blog', uuid], queryFn: () => api.get>(`/api/v1/blog/${uuid}`), enabled: isEdit, }); // detail endpoint is double-nested: success(['data' => $blog->toArray()]) const blog = data?.data?.data; const { register, handleSubmit, control, watch, setValue, formState: { errors, isSubmitting } } = useForm({ resolver: zodResolver(schema), defaultValues: { status: 'draft' }, values: blog ? { title: blog.title, summary: blog.summary ?? '', body: blog.body, tags: blog.tags?.join(', ') ?? '', status: blog.status, image_url: blog.image_url ?? '', } : undefined, }); const imageUrl = watch('image_url'); const buildPayload = (d: FormData) => ({ title: d.title, body: d.body, summary: d.summary, status: d.status, image_url: d.image_url ?? '', tags: d.tags ? d.tags.split(',').map((t) => t.trim()).filter(Boolean) : [], }); const createMutation = useMutation({ mutationFn: (d: FormData) => api.post>('/api/v1/blog', buildPayload(d)), onSuccess: () => { toast.success('مقاله ذخیره شد'); qc.invalidateQueries({ queryKey: ['blogs'] }); navigate('/admin/blogs'); }, onError: (err: Error) => toast.error(err.message), }); const updateMutation = useMutation({ mutationFn: (d: FormData) => api.patch>(`/api/v1/blog/${uuid}`, buildPayload(d)), onSuccess: () => { toast.success('مقاله بروزرسانی شد'); qc.invalidateQueries({ queryKey: ['blogs'] }); qc.invalidateQueries({ queryKey: ['blog', uuid] }); navigate('/admin/blogs'); }, onError: (err: Error) => toast.error(err.message), }); const onSubmit = (d: FormData) => { if (isEdit) updateMutation.mutate(d); else createMutation.mutate(d); }; const onImageSelect = async (e: React.ChangeEvent) => { const file = e.target.files?.[0]; if (!file) return; setUploading(true); try { const url = await uploadBlogImage(file); setValue('image_url', url, { shouldValidate: true }); toast.success('تصویر آپلود شد'); } catch (err) { toast.error((err as Error).message); } finally { setUploading(false); e.target.value = ''; } }; if (isEdit && isLoading) { return (
{Array.from({ length: 5 }).map((_, i) => (
))}
); } return (
{errors.title &&

{errors.title.message}

}