Rediseña panel y auth con la identidad de la landing B2C

- Vista de leads en tarjetas + tabla con toggle (tarjetas por defecto, preferencia persistida)
- Galería de trabajos: gestión en /panel/galeria y bloque público en el funnel
- Selector de tema por reformista (presets + color de marca opcional) aplicado a la landing
- Login y registro rediseñados a pantalla partida 50/50 con foto de reforma
- Enlace "Entrar" funcional en la cabecera del funnel; elimina Navbar muerto
- Unifica tipografía y botones del panel con los tokens de la landing

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Carlos Narro
2026-06-01 13:51:00 +02:00
parent a91fe5ce2c
commit 1ea5d70675
30 changed files with 2797 additions and 283 deletions

View File

@@ -0,0 +1,56 @@
'use server';
import { and, eq } from 'drizzle-orm';
import { revalidatePath } from 'next/cache';
import { db } from '@/db';
import { galeriaFotos } from '@/db/schema';
import { getCurrentTenantId as getTenantId } from '@/lib/auth/current-user';
import { GALERIA_MAX_FOTOS } from '@/lib/galeria';
const GALERIA_MAX_BYTES = 2_000_000;
const GALERIA_TIPOS = ['image/png', 'image/jpeg', 'image/webp'];
export type GaleriaResult = { ok: boolean; error?: string };
export async function subirFotoGaleria(
_prev: GaleriaResult | null,
formData: FormData
): Promise<GaleriaResult> {
const tenantId = await getTenantId();
const file = formData.get('foto');
if (!(file instanceof File) || file.size === 0) {
return { ok: false, error: 'Selecciona una imagen.' };
}
if (!GALERIA_TIPOS.includes(file.type)) {
return { ok: false, error: 'Formato no válido. Usa PNG, JPG o WEBP.' };
}
if (file.size > GALERIA_MAX_BYTES) {
return { ok: false, error: 'La imagen no puede superar los 2 MB.' };
}
const existentes = await db
.select({ id: galeriaFotos.id })
.from(galeriaFotos)
.where(eq(galeriaFotos.tenantId, tenantId));
if (existentes.length >= GALERIA_MAX_FOTOS) {
return { ok: false, error: `Has alcanzado el máximo de ${GALERIA_MAX_FOTOS} fotos.` };
}
const titulo = String(formData.get('titulo') ?? '').trim() || null;
const base64 = Buffer.from(await file.arrayBuffer()).toString('base64');
const dataUri = `data:${file.type};base64,${base64}`;
await db
.insert(galeriaFotos)
.values({ tenantId, url: dataUri, titulo, orden: existentes.length });
revalidatePath('/panel/galeria');
return { ok: true };
}
export async function eliminarFotoGaleria(id: string) {
const tenantId = await getTenantId();
await db
.delete(galeriaFotos)
.where(and(eq(galeriaFotos.id, id), eq(galeriaFotos.tenantId, tenantId)));
revalidatePath('/panel/galeria');
}