diff --git a/assets/admin/pages/BlogFormPage.tsx b/assets/admin/pages/BlogFormPage.tsx index ed513c34..19c85b14 100644 --- a/assets/admin/pages/BlogFormPage.tsx +++ b/assets/admin/pages/BlogFormPage.tsx @@ -1,30 +1,54 @@ -import React from 'react'; +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(), - content: z.string().min(10, 'محتوا الزامی است'), + 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], @@ -34,26 +58,34 @@ export default function BlogFormPage() { const blog = data?.data; - const { register, handleSubmit, control, reset, formState: { errors, isSubmitting } } = useForm({ + const { register, handleSubmit, control, watch, setValue, formState: { errors, isSubmitting } } = useForm({ resolver: zodResolver(schema), defaultValues: { status: 'draft' }, values: blog ? { title: blog.title, summary: blog.summary ?? '', - content: blog.content, + 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', { - ...d, - tags: d.tags ? d.tags.split(',').map((t) => t.trim()).filter(Boolean) : [], - }), + mutationFn: (d: FormData) => api.post>('/api/v1/blog', buildPayload(d)), onSuccess: () => { toast.success('مقاله ذخیره شد'); qc.invalidateQueries({ queryKey: ['blogs'] }); @@ -63,11 +95,7 @@ export default function BlogFormPage() { }); const updateMutation = useMutation({ - mutationFn: (d: FormData) => - api.patch>(`/api/v1/blog/${uuid}`, { - ...d, - tags: d.tags ? d.tags.split(',').map((t) => t.trim()).filter(Boolean) : [], - }), + mutationFn: (d: FormData) => api.patch>(`/api/v1/blog/${uuid}`, buildPayload(d)), onSuccess: () => { toast.success('مقاله بروزرسانی شد'); qc.invalidateQueries({ queryKey: ['blogs'] }); @@ -82,6 +110,22 @@ export default function BlogFormPage() { 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 (
@@ -122,13 +166,55 @@ export default function BlogFormPage() {
-