feat(F-048): completed feature
This commit is contained in:
@@ -4,13 +4,35 @@ import { cmsApi } from '@/lib/api-client';
|
||||
|
||||
interface Page { id: string; slug: string; title: string; body: string; status: string; createdAt: string; updatedAt: string; }
|
||||
|
||||
// ── Iconos (patrón /categories) ───────────────────────────────────────────────
|
||||
const PencilIcon = (
|
||||
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" />
|
||||
</svg>
|
||||
);
|
||||
const EyeIcon = (
|
||||
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M2.036 12.322a1.012 1.012 0 010-.639C3.423 7.51 7.36 4.5 12 4.5c4.638 0 8.573 3.007 9.963 7.178.07.207.07.431 0 .639C20.577 16.49 16.64 19.5 12 19.5c-4.638 0-8.573-3.007-9.963-7.178z" />
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
|
||||
</svg>
|
||||
);
|
||||
const EyeOffIcon = (
|
||||
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M3.98 8.223A10.477 10.477 0 001.934 12C3.226 16.338 7.244 19.5 12 19.5c.993 0 1.953-.138 2.863-.395M6.228 6.228A10.45 10.45 0 0112 4.5c4.756 0 8.773 3.162 10.065 7.498a10.523 10.523 0 01-4.293 5.774M6.228 6.228L3 3m3.228 3.228l3.65 3.65m7.894 7.894L21 21m-3.228-3.228l-3.65-3.65m0 0a3 3 0 10-4.243-4.243m4.242 4.242L9.88 9.88" />
|
||||
</svg>
|
||||
);
|
||||
|
||||
const EMPTY_FORM = { slug: '', title: '', body: '' };
|
||||
|
||||
export default function CmsPage() {
|
||||
const [items, setItems] = useState<Page[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState('');
|
||||
const [showForm, setShowForm] = useState(false);
|
||||
const [editing, setEditing] = useState<Page | null>(null);
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [msg, setMsg] = useState('');
|
||||
const [form, setForm] = useState({ slug: '', title: '', body: '' });
|
||||
const [form, setForm] = useState(EMPTY_FORM);
|
||||
|
||||
const load = useCallback(async () => {
|
||||
setLoading(true);
|
||||
@@ -21,15 +43,25 @@ export default function CmsPage() {
|
||||
|
||||
useEffect(() => { load(); }, [load]);
|
||||
|
||||
const openCreate = () => { setEditing(null); setForm(EMPTY_FORM); setShowForm(true); };
|
||||
const openEdit = (p: Page) => { setEditing(p); setForm({ slug: p.slug, title: p.title, body: p.body }); setShowForm(true); };
|
||||
|
||||
const handleSave = async () => {
|
||||
setMsg('');
|
||||
setSaving(true); setMsg('');
|
||||
try {
|
||||
await cmsApi.create({ slug: form.slug, title: form.title, body: form.body });
|
||||
setMsg('Página creada');
|
||||
if (editing) {
|
||||
await cmsApi.update(editing.id, { slug: form.slug, title: form.title, body: form.body });
|
||||
setMsg('Página actualizada');
|
||||
} else {
|
||||
await cmsApi.create({ slug: form.slug, title: form.title, body: form.body });
|
||||
setMsg('Página creada');
|
||||
}
|
||||
setShowForm(false);
|
||||
setForm({ slug: '', title: '', body: '' });
|
||||
setEditing(null);
|
||||
setForm(EMPTY_FORM);
|
||||
load();
|
||||
} catch (e) { setMsg(e instanceof Error ? e.message : 'Error al crear'); }
|
||||
} catch (e) { setMsg(e instanceof Error ? e.message : 'Error al guardar'); }
|
||||
finally { setSaving(false); }
|
||||
};
|
||||
|
||||
const togglePublish = async (id: string, currentStatus: string) => {
|
||||
@@ -44,15 +76,15 @@ export default function CmsPage() {
|
||||
<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>
|
||||
<button onClick={openCreate} 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]) => (
|
||||
<h2 className="font-semibold text-gray-900">{editing ? 'Editar página' : 'Nueva página'}</h2>
|
||||
{[['slug','Slug *','text'],['title','Título *','text']].map(([k,label]) => (
|
||||
<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" />
|
||||
@@ -63,8 +95,8 @@ export default function CmsPage() {
|
||||
<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>
|
||||
<button onClick={handleSave} disabled={saving || !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">{saving ? 'Guardando...' : (editing ? 'Guardar cambios' : 'Crear')}</button>
|
||||
<button onClick={() => { setShowForm(false); setEditing(null); }} className="px-5 py-2.5 border border-gray-300 text-gray-600 text-sm rounded-xl">Cancelar</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
@@ -84,9 +116,23 @@ export default function CmsPage() {
|
||||
<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>
|
||||
{/* Acciones con iconos (FIX-13) */}
|
||||
<div className="flex gap-2">
|
||||
<button onClick={() => openEdit(p)} title="Editar" aria-label="Editar"
|
||||
className="p-1.5 text-gray-400 hover:text-[#2D6A4F] hover:bg-green-50 rounded-lg transition-colors">
|
||||
{PencilIcon}
|
||||
</button>
|
||||
<button onClick={() => togglePublish(p.id, p.status)}
|
||||
title={p.status === 'published' ? 'Despublicar' : 'Publicar'}
|
||||
aria-label={p.status === 'published' ? 'Despublicar' : 'Publicar'}
|
||||
className={`p-1.5 rounded-lg transition-colors ${
|
||||
p.status === 'published'
|
||||
? 'text-amber-500 hover:text-amber-600 hover:bg-amber-50'
|
||||
: 'text-gray-400 hover:text-[#2D6A4F] hover:bg-green-50'
|
||||
}`}>
|
||||
{p.status === 'published' ? EyeOffIcon : EyeIcon}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user