feat: Implement faculty management routes and UI components

- Added a new route for managing faculties with a grid display of faculties.
- Created a detailed view for each faculty including metrics and recent activities.
- Introduced a new loader for fetching faculty data and associated plans and subjects.
- Enhanced the existing plans route to include a modal for plan details.
- Updated the login and index pages with improved UI and styling.
- Integrated a progress ring component to visualize the quality of plans.
- Applied a new font style across the application for consistency.
This commit is contained in:
2025-08-20 19:09:31 -06:00
parent b33a016ee2
commit 51faa98022
17 changed files with 1279 additions and 108 deletions
+80
View File
@@ -0,0 +1,80 @@
import { createFileRoute, Link } from '@tanstack/react-router'
import * as Icons from 'lucide-react'
import { supabase } from '@/auth/supabase'
import { useMemo } from 'react'
type Facultad = {
id: string
nombre: string
icon: string
color?: string | null
}
export const Route = createFileRoute('/_authenticated/facultades')({
component: RouteComponent,
loader: async () => {
const { data, error } = await supabase
.from('facultades')
.select('id, nombre, icon, color')
.order('nombre')
if (error) {
console.error(error)
return { facultades: [] as Facultad[] }
}
return { facultades: (data ?? []) as Facultad[] }
},
})
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
return `linear-gradient(135deg, ${base} 0%, ${base}CC 40%, ${base}99 70%, ${base}66 100%)`
}
function RouteComponent() {
const { facultades } = Route.useLoaderData() as { facultades: Facultad[] }
return (
<div className="p-6">
<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])
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>
</div>
{/* 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>
</div>
)
}