chore: import codes

This commit is contained in:
2026-04-12 12:50:14 -04:00
parent 4bcb4898e8
commit 65ae3c6788
241 changed files with 48834 additions and 1 deletions
+271
View File
@@ -0,0 +1,271 @@
'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 (
<div className="flex flex-col gap-4 sm:gap-6 lg:gap-8">
<div className="flex items-center justify-between">
<div>
<h1 className="text-lg sm:text-xl font-semibold text-neutral-900 dark:text-white">Modifier {label}</h1>
<p className="mt-1 text-xs text-neutral-400">Modifier un élément existant</p>
</div>
<Button variant="secondary" size="sm" onClick={() => router.push(`/admin/posts/${postType}/list`)}>
Retour
</Button>
</div>
{loading ? (
<Card><div className="animate-pulse h-40 bg-neutral-200 dark:bg-neutral-800 rounded" /></Card>
) : (
<form onSubmit={handleSubmit} className="space-y-6">
<Card>
<div className="space-y-4">
<h2 className="text-lg font-semibold text-neutral-900 dark:text-white">{label}</h2>
<PostFormFields
fields={typeConfig?.fields || []}
formData={formData}
onChange={handleChange}
errors={errors}
slugValue={typeConfig?.slugField ? formData[typeConfig.slugField] : undefined}
onSlugFocus={() => setSlugTouched(true)}
categories={categories}
uploading={uploading}
onImageChange={handleImageChange}
/>
</div>
</Card>
<div className="flex items-center justify-end gap-3">
<Button type="button" variant="secondary" onClick={() => router.push(`/admin/posts/${postType}/list`)} disabled={saving}>
Annuler
</Button>
<Button type="submit" variant="success" loading={saving} disabled={saving}>
{saving ? 'Enregistrement...' : 'Enregistrer'}
</Button>
</div>
</form>
)}
</div>
);
};
function capitalize(str) {
if (!str) return '';
return str.charAt(0).toUpperCase() + str.slice(1);
}
export default PostEditPage;