Files
reformix-hackaton/mvp/b2c/src/app/panel/opiniones/page.tsx
Carlos Narro bf9e72064b Alinear panel y auth con la identidad B2B "Architectural Warmth"
Sustituye la paleta negra/azul B2C del panel del reformista por el verde
de marca, neutros cálidos y titulares en Instrument Serif de la landing B2B.
Añade tokens --color-primary-*, --color-stone-50 y --font-display al @theme.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-01 20:01:57 +02:00

126 lines
5.0 KiB
TypeScript

import { getTestimoniosPanel, type TestimonioPanel } from '@/db/tenant-queries';
import { formatFecha } from '@/lib/funnel';
import { publicarTestimonio, ocultarTestimonio, eliminarTestimonio } from './actions';
export const dynamic = 'force-dynamic';
const ESTADO_BADGE: Record<TestimonioPanel['estado'], { label: string; className: string }> = {
pendiente: { label: 'Pendiente', className: 'bg-amber-100 text-amber-700' },
publicado: { label: 'Publicado', className: 'bg-green-100 text-green-700' },
oculto: { label: 'Oculto', className: 'bg-gray-100 text-gray-500' },
};
function Estrellas({ rating }: { rating: number }) {
return (
<div className="flex gap-0.5" aria-label={`${rating} de 5 estrellas`}>
{[1, 2, 3, 4, 5].map((n) => (
<svg
key={n}
viewBox="0 0 24 24"
className={`w-4 h-4 ${n <= rating ? 'fill-yellow-400' : 'fill-gray-200'}`}
aria-hidden="true"
>
<path d="M12 17.27 18.18 21l-1.64-7.03L22 9.24l-7.19-.61L12 2 9.19 8.63 2 9.24l5.46 4.73L5.82 21z" />
</svg>
))}
</div>
);
}
export default async function OpinionesPage() {
const testimonios = await getTestimoniosPanel();
return (
<div className="space-y-8 max-w-3xl">
<div>
<h1 className="font-display text-3xl tracking-tight text-black">Opiniones</h1>
<p className="text-sm text-gray-500 mt-1">
Las opiniones que te dejan tus clientes. Aprueba las que quieras mostrar en tu funnel; solo
las publicadas aparecen en tu página.
</p>
</div>
{testimonios.length === 0 ? (
<div className="bg-white rounded-xl border border-gray-200 p-8 text-center text-sm text-gray-400">
Aún no tienes opiniones. Solicítalas a tus clientes desde la ficha de un lead ganado.
</div>
) : (
<ul className="flex flex-col gap-4">
{testimonios.map((t) => {
const badge = ESTADO_BADGE[t.estado];
return (
<li
key={t.id}
className="bg-white rounded-xl border border-gray-200 p-5 flex flex-col gap-3"
>
<div className="flex items-start justify-between gap-4">
<div className="flex flex-col gap-1">
<div className="flex items-center gap-2">
<span className="font-bold text-black">{t.nombre}</span>
<span
className={`px-2 py-0.5 rounded-full text-xs font-semibold ${badge.className}`}
>
{badge.label}
</span>
</div>
{t.contexto && <span className="text-xs text-gray-500">{t.contexto}</span>}
</div>
<Estrellas rating={t.rating} />
</div>
<p className="text-sm text-gray-700 leading-relaxed italic">{t.texto}</p>
{t.fotos.length > 0 && (
<div className="flex flex-wrap gap-2">
{t.fotos.map((url, i) => (
// eslint-disable-next-line @next/next/no-img-element
<img
key={i}
src={url}
alt=""
className="w-20 h-20 object-cover rounded-lg border border-gray-200"
/>
))}
</div>
)}
<div className="flex flex-wrap items-center gap-2 pt-2 border-t border-gray-100">
<span className="text-xs text-gray-400 mr-auto">{formatFecha(t.createdAt)}</span>
{t.estado !== 'publicado' && (
<form action={publicarTestimonio.bind(null, t.id)}>
<button
type="submit"
className="px-3 py-1.5 rounded-lg bg-green-600 text-white text-xs font-semibold hover:bg-green-700"
>
Publicar
</button>
</form>
)}
{t.estado === 'publicado' && (
<form action={ocultarTestimonio.bind(null, t.id)}>
<button
type="submit"
className="px-3 py-1.5 rounded-lg border border-gray-300 text-gray-700 text-xs font-semibold hover:border-gray-500"
>
Ocultar
</button>
</form>
)}
<form action={eliminarTestimonio.bind(null, t.id)}>
<button
type="submit"
className="px-3 py-1.5 rounded-lg text-red-500 text-xs font-semibold hover:underline"
>
Eliminar
</button>
</form>
</div>
</li>
);
})}
</ul>
)}
</div>
);
}