'use client'; import React, { useState, useEffect } from 'react'; import { useRouter, usePathname } from 'next/navigation'; import { Button, Card } from '../../../shared/components'; import { useToast } from '@hykocx/zen/toast'; import { formatDateForInput, formatDateTimeForInput } from '../../../shared/lib/dates.js'; import PostFormFields from './PostFormFields.js'; function slugifyTitle(title) { if (!title || typeof title !== 'string') return ''; return title .toLowerCase() .trim() .normalize('NFD') .replace(/[\u0300-\u036f]/g, '') .replace(/\s+/g, '-') .replace(/[^a-z0-9-]/g, '') .replace(/-+/g, '-') .replace(/^-|-$/g, ''); } function getParamsFromPath(pathname) { const segments = (pathname || '').split('/').filter(Boolean); // /admin/posts/{type}/edit/{id} → segments[2], segments[4] return { postType: segments[2] || '', postId: segments[4] || '' }; } const PostEditPage = () => { const router = useRouter(); const pathname = usePathname(); const toast = useToast(); const { postType, postId } = getParamsFromPath(pathname); const [typeConfig, setTypeConfig] = useState(null); const [categories, setCategories] = useState([]); const [loading, setLoading] = useState(true); const [saving, setSaving] = useState(false); const [uploading, setUploading] = useState(false); const [formData, setFormData] = useState({}); const [errors, setErrors] = useState({}); const [slugTouched, setSlugTouched] = useState(false); useEffect(() => { if (postType && postId) loadConfig(); }, [postType, postId]); // Only sync title → slug when title content changes (not when slug is cleared) useEffect(() => { if (!typeConfig || slugTouched) return; const titleField = typeConfig.titleField; const slugField = typeConfig.slugField; if (titleField && slugField && formData[titleField]) { setFormData(prev => ({ ...prev, [slugField]: slugifyTitle(prev[titleField]) })); } }, [formData[typeConfig?.titleField], typeConfig]); const loadConfig = async () => { try { const [configRes, postRes] = await Promise.all([ fetch('/zen/api/admin/posts/config', { credentials: 'include' }), fetch(`/zen/api/admin/posts/posts?type=${postType}&id=${postId}`, { credentials: 'include' }) ]); const configData = await configRes.json(); const postData = await postRes.json(); if (!configData.success || !configData.config.types[postType]) { toast.error('Type de post introuvable'); return; } const config = configData.config.types[postType]; setTypeConfig(config); if (!postData.success || !postData.post) { toast.error('Post introuvable'); router.push(`/admin/posts/${postType}/list`); return; } const post = postData.post; // Populate form data from post const initial = {}; for (const field of config.fields) { if (field.type === 'slug') { initial[field.name] = post.slug || ''; } else if (field.type === 'category') { initial[field.name] = post.category_id ? String(post.category_id) : ''; } else if (field.type === 'date') { initial[field.name] = post[field.name] ? formatDateForInput(post[field.name]) : ''; } else if (field.type === 'datetime') { initial[field.name] = post[field.name] ? formatDateTimeForInput(post[field.name]) : ''; } else if (field.type === 'relation') { // Relations come as [{ id, title, slug }] from getPostById initial[field.name] = Array.isArray(post[field.name]) ? post[field.name] : []; } else { initial[field.name] = post[field.name] || ''; } } setFormData(initial); setSlugTouched(true); // Don't auto-generate slug on edit if (config.hasCategory) loadCategories(); } catch (error) { console.error('Error loading post:', error); toast.error('Impossible de charger le post'); } finally { setLoading(false); } }; const loadCategories = async () => { try { const response = await fetch( `/zen/api/admin/posts/categories?type=${postType}&limit=1000&is_active=true`, { credentials: 'include' } ); const data = await response.json(); if (data.success) setCategories(data.categories || []); } catch (error) { console.error('Error loading categories:', error); } }; const handleChange = (field, value) => { setFormData(prev => ({ ...prev, [field]: value })); if (field === typeConfig?.slugField) setSlugTouched(value !== ''); if (errors[field]) setErrors(prev => ({ ...prev, [field]: null })); }; const handleImageChange = async (fieldName, e) => { const file = e.target?.files?.[0]; if (!file) return; try { setUploading(true); const fd = new FormData(); fd.append('file', file); const response = await fetch('/zen/api/admin/posts/upload-image', { method: 'POST', credentials: 'include', body: fd }); const data = await response.json(); if (data.success && data.key) { setFormData(prev => ({ ...prev, [fieldName]: data.key })); toast.success('Image téléchargée'); } else { toast.error(data.error || 'Échec du téléchargement'); } } catch (error) { console.error('Error uploading image:', error); toast.error('Échec du téléchargement'); } finally { setUploading(false); } }; const validateForm = () => { const newErrors = {}; if (typeConfig?.titleField && !formData[typeConfig.titleField]?.trim()) { newErrors[typeConfig.titleField] = 'Ce champ est requis'; } setErrors(newErrors); return Object.keys(newErrors).length === 0; }; const handleSubmit = async (e) => { e.preventDefault(); if (!validateForm()) return; try { setSaving(true); const payload = { ...formData }; if (typeConfig?.hasCategory) { const catField = typeConfig.fields.find(f => f.type === 'category'); if (catField) { payload[catField.name] = payload[catField.name] ? parseInt(payload[catField.name]) : null; } } // Convert relation fields to arrays of IDs for (const field of (typeConfig?.fields || []).filter(f => f.type === 'relation')) { const items = payload[field.name]; payload[field.name] = Array.isArray(items) ? items.map(i => (typeof i === 'object' ? i.id : i)) : []; } // Convert datetime fields to ISO 8601 UTC for (const field of (typeConfig?.fields || []).filter(f => f.type === 'datetime')) { const val = payload[field.name]; if (val && !val.includes('Z')) payload[field.name] = val + ':00.000Z'; } const response = await fetch(`/zen/api/admin/posts/posts?type=${postType}&id=${postId}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, credentials: 'include', body: JSON.stringify(payload) }); const data = await response.json(); if (data.success) { toast.success('Post mis à jour avec succès'); router.push(`/admin/posts/${postType}/list`); } else { toast.error(data.error || data.message || 'Échec de la mise à jour'); } } catch (error) { console.error('Error updating post:', error); toast.error('Échec de la mise à jour'); } finally { setSaving(false); } }; const label = typeConfig?.label || capitalize(postType); return (

Modifier — {label}

Modifier un élément existant

{loading ? (
) : (

{label}

setSlugTouched(true)} categories={categories} uploading={uploading} onImageChange={handleImageChange} />
)}
); }; function capitalize(str) { if (!str) return ''; return str.charAt(0).toUpperCase() + str.slice(1); } export default PostEditPage;