Refactor App component styles and remove highlights section

- Updated background gradient colors for a lighter theme.
- Changed button styles to improve visibility.
- Removed the highlights section to streamline the layout.
- Adjusted text colors for better contrast and readability.
This commit is contained in:
2025-08-26 15:24:53 -06:00
parent 602c5dbb31
commit 56b0dc8a62
12 changed files with 2240 additions and 1188 deletions
+252 -35
View File
@@ -1,7 +1,13 @@
import { createFileRoute, Link } from '@tanstack/react-router'
import { createFileRoute, Link, useRouter } from '@tanstack/react-router'
import * as Icons from 'lucide-react'
import { supabase } from '@/auth/supabase'
import { useMemo } from 'react'
import { useMemo, useState } from 'react'
import { Button } from '@/components/ui/button'
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from '@/components/ui/dialog'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import { Select, SelectTrigger, SelectContent, SelectItem, SelectValue } from '@/components/ui/select'
import { toast } from 'sonner'
type Facultad = {
id: string
@@ -17,7 +23,6 @@ export const Route = createFileRoute('/_authenticated/facultades')({
.from('facultades')
.select('id, nombre, icon, color')
.order('nombre')
if (error) {
console.error(error)
return { facultades: [] as Facultad[] }
@@ -26,55 +31,267 @@ export const Route = createFileRoute('/_authenticated/facultades')({
},
})
/* ----------- Paleta curada ----------- */
const PALETTE: { name: string; hex: `#${string}` }[] = [
{ name: 'Indigo', hex: '#4F46E5' },
{ name: 'Blue', hex: '#2563EB' },
{ name: 'Sky', hex: '#0EA5E9' },
{ name: 'Teal', hex: '#14B8A6' },
{ name: 'Emerald', hex: '#10B981' },
{ name: 'Lime', hex: '#84CC16' },
{ name: 'Amber', hex: '#F59E0B' },
{ name: 'Orange', hex: '#F97316' },
{ name: 'Red', hex: '#EF4444' },
{ name: 'Rose', hex: '#F43F5E' },
{ name: 'Violet', hex: '#7C3AED' },
{ name: 'Purple', hex: '#9333EA' },
{ name: 'Fuchsia', hex: '#C026D3' },
{ name: 'Slate', hex: '#334155' },
{ name: 'Zinc', hex: '#3F3F46' },
{ name: 'Neutral', hex: '#404040' },
]
/* Un set corto y útil de íconos Lucide */
const ICON_CHOICES = [
'Building2', 'Building', 'School', 'University', 'Landmark', 'Library', 'Layers',
'Atom', 'FlaskConical', 'Microscope', 'Cpu', 'Hammer', 'Palette', 'Shapes', 'BookOpen', 'GraduationCap'
] as const
type IconName = typeof ICON_CHOICES[number]
function gradientFrom(color?: string | null) {
const base = (color && /^#([0-9a-f]{6}|[0-9a-f]{3})$/i.test(color)) ? color : '#2563eb' // azul por defecto
// degradado elegante con transparencia
const base = (color && /^#([0-9a-f]{6}|[0-9a-f]{3})$/i.test(color)) ? color : '#2563eb'
return `linear-gradient(135deg, ${base} 0%, ${base}CC 40%, ${base}99 70%, ${base}66 100%)`
}
function RouteComponent() {
const { facultades } = Route.useLoaderData() as { facultades: Facultad[] }
const router = useRouter()
const [createOpen, setCreateOpen] = useState(false)
const [editOpen, setEditOpen] = useState(false)
const [saving, setSaving] = useState(false)
const [form, setForm] = useState<{ nombre: string; icon: IconName; color: `#${string}` }>(
{ nombre: '', icon: 'Building2', color: '#2563EB' }
)
const [editing, setEditing] = useState<Facultad | null>(null)
function openCreate() {
setForm({ nombre: '', icon: 'Building2', color: '#2563EB' })
setCreateOpen(true)
}
function openEdit(f: Facultad) {
setEditing(f)
setForm({
nombre: f.nombre,
icon: (ICON_CHOICES.includes(f.icon as IconName) ? f.icon : 'Building2') as IconName,
color: ((f.color && /^#([0-9a-f]{6}|[0-9a-f]{3})$/i.test(f.color)) ? (f.color as `#${string}`) : '#2563EB')
})
setEditOpen(true)
}
async function doCreate() {
if (!form.nombre.trim()) { toast.error('Nombre requerido'); return }
setSaving(true)
const { error } = await supabase.from('facultades')
.insert({ nombre: form.nombre.trim(), icon: form.icon, color: form.color })
setSaving(false)
if (error) { console.error(error); toast.error('No se pudo crear'); return }
toast.success('Facultad creada ✨')
setCreateOpen(false)
router.invalidate()
}
async function doEdit() {
if (!editing) return
if (!form.nombre.trim()) { toast.error('Nombre requerido'); return }
setSaving(true)
const { error } = await supabase.from('facultades')
.update({ nombre: form.nombre.trim(), icon: form.icon, color: form.color })
.eq('id', editing.id)
setSaving(false)
if (error) { console.error(error); toast.error('No se pudo guardar'); return }
toast.success('Cambios guardados ✅')
setEditOpen(false)
setEditing(null)
router.invalidate()
}
return (
<div className="p-6">
<div className="p-6 space-y-4">
{/* Header */}
<div className="flex items-center justify-between">
<h1 className="text-xl font-bold flex items-center gap-2">
<Icons.Building2 className="w-5 h-5" /> Facultades
</h1>
<div className="flex items-center gap-2">
<Button variant="outline" onClick={() => router.invalidate()}>
<Icons.RefreshCcw className="w-4 h-4 mr-2" /> Recargar
</Button>
<Button onClick={openCreate}>
<Icons.Plus className="w-4 h-4 mr-2" /> Nueva facultad
</Button>
</div>
</div>
{/* Grid */}
<div className="grid gap-6 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
{facultades.map((fac) => {
const LucideIcon = (Icons as any)[fac.icon] || Icons.Building
const bg = useMemo(() => ({ background: gradientFrom(fac.color) }), [fac.color])
const LucideIcon = (Icons as any)[fac.icon] || Icons.Building2
const bg = { background: gradientFrom(fac.color) } // ← sin useMemo aquí
return (
<Link
key={fac.id}
to="/facultad/$facultadId"
params={{ facultadId: fac.id }}
aria-label={`Administrar ${fac.nombre}`}
className="group relative block rounded-3xl overflow-hidden shadow-xl focus:outline-none focus-visible:ring-4 ring-white/60"
style={bg}
>
{/* capa brillo */}
<div className="absolute inset-0 opacity-0 group-hover:opacity-15 transition-opacity" style={{
background: 'radial-gradient(1200px 400px at 20% -20%, rgba(255,255,255,.45), transparent 60%)'
}} />
{/* contenido */}
<div className="relative h-56 sm:h-64 lg:h-72 p-6 flex flex-col justify-between text-white">
<LucideIcon className="w-20 h-20 md:w-24 md:h-24 drop-shadow-md" />
<div className="flex items-end justify-between">
<h3 className="text-xl md:text-2xl font-bold drop-shadow-sm pr-2">
{fac.nombre}
</h3>
<Icons.ArrowRight className="w-6 h-6 opacity-0 translate-x-2 group-hover:opacity-100 group-hover:translate-x-0 transition-all" />
<div key={fac.id} className="group relative rounded-3xl overflow-hidden shadow-xl border">
<Link
to="/facultad/$facultadId"
params={{ facultadId: fac.id }}
aria-label={`Administrar ${fac.nombre}`}
className="block focus:outline-none focus-visible:ring-4 ring-white/60"
style={bg}
>
<div
className="absolute inset-0 opacity-0 group-hover:opacity-15 transition-opacity"
style={{ background: 'radial-gradient(1200px 400px at 20% -20%, rgba(255,255,255,.45), transparent 60%)' }}
/>
<div className="relative h-56 sm:h-64 lg:h-72 p-6 flex flex-col justify-between text-white">
<LucideIcon className="w-20 h-20 md:w-24 md:h-24 drop-shadow-md" />
<div className="flex items-end justify-between">
<h3 className="text-xl md:text-2xl font-bold drop-shadow-sm pr-2">{fac.nombre}</h3>
<Icons.ArrowRight className="w-6 h-6 opacity-0 translate-x-2 group-hover:opacity-100 group-hover:translate-x-0 transition-all" />
</div>
</div>
</div>
</Link>
{/* borde dinámico al hover */}
<div className="absolute inset-0 ring-0 group-hover:ring-4 group-active:ring-4 ring-white/40 transition-[ring-width]" />
{/* animación sutil */}
<div className="absolute inset-0 scale-100 group-hover:scale-[1.02] group-active:scale-[0.99] transition-transform duration-300" />
</Link>
<div className="absolute top-3 right-3">
<Button
size="icon"
variant="secondary"
className="backdrop-blur bg-white/80 hover:bg-white"
onClick={() => openEdit(fac)}
>
<Icons.Pencil className="w-4 h-4" />
</Button>
</div>
</div>
)
})}
</div>
{/* Dialog Crear */}
<Dialog open={createOpen} onOpenChange={setCreateOpen}>
<DialogContent className="sm:max-w-lg">
<DialogHeader>
<DialogTitle>Nueva facultad</DialogTitle>
</DialogHeader>
<FormFields form={form} setForm={setForm} />
<DialogFooter>
<Button variant="outline" onClick={() => setCreateOpen(false)}>Cancelar</Button>
<Button onClick={doCreate} disabled={saving}>{saving ? 'Guardando…' : 'Crear'}</Button>
</DialogFooter>
</DialogContent>
</Dialog>
{/* Dialog Editar */}
<Dialog open={editOpen} onOpenChange={(o) => { setEditOpen(o); if (!o) setEditing(null) }}>
<DialogContent className="sm:max-w-lg">
<DialogHeader>
<DialogTitle>Editar facultad</DialogTitle>
</DialogHeader>
<FormFields form={form} setForm={setForm} />
<DialogFooter>
<Button variant="outline" onClick={() => { setEditOpen(false); setEditing(null) }}>Cancelar</Button>
<Button onClick={doEdit} disabled={saving}>{saving ? 'Guardando…' : 'Guardar'}</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
)
}
/* ----------- Subcomponentes ----------- */
function FormFields({
form, setForm
}: {
form: { nombre: string; icon: IconName; color: `#${string}` }
setForm: React.Dispatch<React.SetStateAction<{ nombre: string; icon: IconName; color: `#${string}` }>>
}) {
const PreviewIcon = (Icons as any)[form.icon] || Icons.Building2
const bg = useMemo(() => ({ background: gradientFrom(form.color) }), [form.color])
return (
<div className="grid gap-4">
{/* Preview */}
<div className="rounded-2xl overflow-hidden border">
<div className="h-36 p-4 flex items-end justify-between text-white" style={bg}>
<PreviewIcon className="w-14 h-14 drop-shadow-md" />
<span className="text-xs bg-white/20 px-2 py-1 rounded">Vista previa</span>
</div>
</div>
{/* Nombre */}
<div className="space-y-1">
<Label>Nombre</Label>
<Input
value={form.nombre}
onChange={(e) => setForm(s => ({ ...s, nombre: e.target.value }))}
placeholder="Facultad de Ingeniería"
/>
</div>
{/* Icono */}
<div className="space-y-1">
<Label>Ícono</Label>
<Select value={form.icon} onValueChange={(v) => setForm(s => ({ ...s, icon: v as IconName }))}>
<SelectTrigger><SelectValue placeholder="Selecciona ícono" /></SelectTrigger>
<SelectContent className="max-h-72">
{ICON_CHOICES.map(k => {
const Ico = (Icons as any)[k]
return (
<SelectItem key={k} value={k}>
<span className="inline-flex items-center gap-2"><Ico className="w-4 h-4" /> {k}</span>
</SelectItem>
)
})}
</SelectContent>
</Select>
</div>
{/* Color (paleta curada) */}
<div className="space-y-2">
<Label>Color</Label>
<ColorGrid
value={form.color}
onChange={(hex) => setForm(s => ({ ...s, color: hex }))}
/>
</div>
</div>
)
}
function ColorGrid({ value, onChange }: { value: `#${string}`; onChange: (hex: `#${string}`) => void }) {
return (
<div className="grid grid-cols-8 gap-2">
{PALETTE.map(c => (
<button
key={c.hex}
type="button"
onClick={() => onChange(c.hex)}
className={`relative h-9 rounded-xl ring-1 ring-black/10 transition
${value === c.hex ? 'outline outline-2 outline-offset-2 outline-black/70' : 'hover:scale-[1.03]'}`}
style={{ background: c.hex }}
title={c.name}
aria-label={c.name}
>
{value === c.hex && (
<Icons.Check className="absolute right-1.5 bottom-1.5 w-4 h-4 text-white drop-shadow" />
)}
</button>
))}
</div>
)
}