feat(ADM-018): completed feature

This commit is contained in:
chattie
2026-08-17 22:23:10 +02:00
parent cf1c69fc8b
commit d595b4871f
871 changed files with 47411 additions and 281 deletions

View File

@@ -0,0 +1,97 @@
'use client';
import { useState, useEffect, useCallback } from 'react';
import { cmsApi } from '@/lib/api-client';
interface Page { id: string; slug: string; title: string; body: string; status: string; createdAt: string; updatedAt: string; }
export default function CmsPage() {
const [items, setItems] = useState<Page[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState('');
const [showForm, setShowForm] = useState(false);
const [msg, setMsg] = useState('');
const [form, setForm] = useState({ slug: '', title: '', body: '' });
const load = useCallback(async () => {
setLoading(true);
try { const d = await cmsApi.list() as { items: Page[] }; setItems(d.items ?? []); }
catch (e) { setError(e instanceof Error ? e.message : 'Error'); }
finally { setLoading(false); }
}, []);
useEffect(() => { load(); }, [load]);
const handleSave = async () => {
setMsg('');
try {
await cmsApi.create({ slug: form.slug, title: form.title, body: form.body });
setMsg('Página creada');
setShowForm(false);
setForm({ slug: '', title: '', body: '' });
load();
} catch (e) { setMsg(e instanceof Error ? e.message : 'Error al crear'); }
};
const togglePublish = async (id: string, currentStatus: string) => {
try {
if (currentStatus === 'published') await cmsApi.unpublish(id);
else await cmsApi.publish(id);
load();
} catch (e) { alert(e instanceof Error ? e.message : 'Error'); }
};
return (
<div className="p-8 space-y-6">
<div className="flex items-center justify-between">
<h1 className="text-2xl font-bold text-gray-900">Páginas CMS</h1>
<button onClick={() => setShowForm(true)} className="px-4 py-2 bg-[#2D6A4F] hover:bg-[#1B4332] text-white text-sm font-semibold rounded-xl">+ Nueva página</button>
</div>
{msg && <div className={`p-4 rounded-xl text-sm ${msg.startsWith('Error') ? 'bg-red-50 text-red-700' : 'bg-green-50 text-green-700'}`}>{msg}</div>}
{showForm && (
<div className="bg-white border border-gray-200 rounded-xl p-6 space-y-4">
<h2 className="font-semibold text-gray-900">Nueva página</h2>
{[['slug','Slug *','text'],['title','Título *','text']].map(([k,label,t]) => (
<div key={k}>
<label className="block text-sm font-medium text-gray-700 mb-1">{label}</label>
<input value={(form as Record<string,string>)[k]} onChange={e => setForm({...form,[k]:e.target.value})} className="w-full px-4 py-2.5 border border-gray-300 rounded-xl text-sm focus:ring-2 focus:ring-[#2D6A4F] outline-none" />
</div>
))}
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Contenido *</label>
<textarea value={form.body} onChange={e => setForm({...form,body:e.target.value})} rows={6} className="w-full px-4 py-2.5 border border-gray-300 rounded-xl text-sm focus:ring-2 focus:ring-[#2D6A4F] outline-none resize-none font-mono" />
</div>
<div className="flex gap-3">
<button onClick={handleSave} disabled={!form.slug || !form.title || !form.body} className="px-5 py-2.5 bg-[#2D6A4F] hover:bg-[#1B4332] disabled:opacity-50 text-white text-sm font-semibold rounded-xl">Crear</button>
<button onClick={() => setShowForm(false)} className="px-5 py-2.5 border border-gray-300 text-gray-600 text-sm rounded-xl">Cancelar</button>
</div>
</div>
)}
<div className="space-y-3">
{loading ? <div className="p-12 text-center text-gray-400">Cargando...</div> :
error ? <div className="p-8 text-center text-red-600">{error}</div> :
items.length === 0 ? <div className="p-12 text-center text-gray-400">No hay páginas</div> :
items.map(p => (
<div key={p.id} className="bg-white border border-gray-200 rounded-xl p-4">
<div className="flex items-center justify-between">
<div>
<p className="font-semibold text-gray-900 text-sm">{p.title}</p>
<p className="text-xs text-gray-400 font-mono">/{p.slug}</p>
</div>
<div className="flex items-center gap-3">
<span className={`px-2 py-0.5 rounded-full text-xs font-medium ${p.status === 'published' ? 'bg-green-100 text-green-700' : 'bg-amber-100 text-amber-700'}`}>
{p.status === 'published' ? 'Publicada' : 'Borrador'}
</span>
<button onClick={() => togglePublish(p.id, p.status)} className="text-xs text-[#2D6A4F] hover:underline">
{p.status === 'published' ? 'Despublicar' : 'Publicar'}
</button>
</div>
</div>
</div>
))}
</div>
</div>
);
}