ac3e7061ac
Rediseño visual: - Nueva identidad UABC/editorial (DESIGN.md), tipografía Mulish, tokens verde/dorado/negro sobre bone-white, patrón de puntos verde en canvas, radios 8/12/30/9999, sin sombras, hairline 1px, tarjetas con leve transparencia + blur suave + borde verde muy sutil. - 45 archivos .astro/.tsx alineados al nuevo sistema (Track A/B/C/D/E). - Limpieza post-critique: eliminado triple CTA en home alumno, .card anidados en admin, aria-live decorativo, 26 aliases legacy en global.css, border-l verde en nav (patrón AI-slop), classList.remove obsoletos. Auditoría de llamadas a BD (para 20-50 usuarios concurrentes): - P0-1: middleware con cache in-memory de profile (30s) + is_banned (30s) por uid; exclusión de assets estáticos (/_astro/*, /audio/*, favicon). Invalidación en signout, PATCH /profile, baneos. - P0-2: eliminado count pendientes duplicado en AppLayout; badge se hidrata client-side (BadgeSolicitudes con fetch inicial). - P0-3: migración 0008 REPLICA IDENTITY FULL en solicitudes + delta preciso en BadgeSolicitudes (con fallback a refetch). - P1: admin/index dedupe (4 queries → 2 con recientes.filter), catalogo reusa profile del middleware (3 queries → 1), inventario paralelo, estadisticas con cache 60s en RAM (stats-cache.ts), migración 0009 con 3 índices (fecha, vencidos parcial, audit compuesto). Fixes P0 de UI post-critique: - estadisticas.astro: ReferenceError DIAS restaurado. - solicitudes (3 vistas): error rendering sanitizado (no expone mensaje Postgres crudo al admin). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014xaxs8cGcQe9NwbMqA1gvn
69 lines
2.1 KiB
Plaintext
69 lines
2.1 KiB
Plaintext
---
|
|
import AppLayout from '@/layouts/AppLayout.astro';
|
|
import PerfilForm from '@/components/profile/PerfilForm.tsx';
|
|
import Avatar from '@/components/profile/Avatar.tsx';
|
|
import { avatarInfo } from '@/lib/avatar';
|
|
|
|
const user = Astro.locals.user!;
|
|
const profile = Astro.locals.profile!;
|
|
|
|
const googleAvatarUrl =
|
|
(user.user_metadata as Record<string, unknown> | undefined)?.avatar_url as string | undefined ||
|
|
(user.user_metadata as Record<string, unknown> | undefined)?.picture as string | undefined ||
|
|
null;
|
|
|
|
const { data: maestrosData } = await Astro.locals.supabase
|
|
.from('maestros')
|
|
.select('id, nombre')
|
|
.eq('activo', true)
|
|
.eq('es_tutor', true)
|
|
.order('nombre');
|
|
const maestros = (maestrosData ?? []) as { id: number; nombre: string }[];
|
|
|
|
const rolLabel =
|
|
profile.rol === 'admin' ? 'Administrador' : profile.rol === 'docente' ? 'Docente' : 'Alumno';
|
|
|
|
const info = avatarInfo({
|
|
email: profile.email,
|
|
nombre: profile.nombre,
|
|
fotoPath: profile.foto_path,
|
|
googleAvatarUrl,
|
|
});
|
|
---
|
|
<AppLayout title="Mi perfil — LabPréstamos">
|
|
<div class="max-w-xl mx-auto space-y-6">
|
|
<header class="card p-6 flex items-center gap-5">
|
|
<Avatar client:load info={info} size={80} className="w-20 h-20 rounded-full" />
|
|
<div class="min-w-0 flex-1">
|
|
<span class="eyebrow mb-1">Mi perfil</span>
|
|
<h1 class="text-2xl md:text-3xl leading-tight truncate">
|
|
{profile.nombre ?? profile.email.split('@')[0]}
|
|
</h1>
|
|
<p class="text-sm text-graphite/70 truncate">{profile.email}</p>
|
|
<span class="badge mt-2">
|
|
{rolLabel}
|
|
</span>
|
|
</div>
|
|
</header>
|
|
|
|
<section class="card p-6">
|
|
<h2 class="text-xl md:text-2xl mb-4">Datos del perfil</h2>
|
|
<PerfilForm
|
|
client:load
|
|
userId={user.id}
|
|
email={profile.email}
|
|
nombre={profile.nombre}
|
|
rol={profile.rol}
|
|
initialProfile={{
|
|
matricula: profile.matricula,
|
|
semestre: profile.semestre,
|
|
tutor_id: profile.tutor_id,
|
|
foto_path: profile.foto_path,
|
|
}}
|
|
maestros={maestros}
|
|
googleAvatarUrl={googleAvatarUrl}
|
|
/>
|
|
</section>
|
|
</div>
|
|
</AppLayout>
|