feat: Add dashboard and asignaturas routes with corresponding components
This commit is contained in:
9
src/routes/_authenticated/asignaturas.tsx
Normal file
9
src/routes/_authenticated/asignaturas.tsx
Normal file
@@ -0,0 +1,9 @@
|
||||
import { createFileRoute } from '@tanstack/react-router'
|
||||
|
||||
export const Route = createFileRoute('/_authenticated/asignaturas')({
|
||||
component: RouteComponent,
|
||||
})
|
||||
|
||||
function RouteComponent() {
|
||||
return <div>Hello "/_authenticated/asignaturas"!</div>
|
||||
}
|
||||
9
src/routes/_authenticated/dashboard.tsx
Normal file
9
src/routes/_authenticated/dashboard.tsx
Normal file
@@ -0,0 +1,9 @@
|
||||
import { createFileRoute } from '@tanstack/react-router'
|
||||
|
||||
export const Route = createFileRoute('/_authenticated/dashboard')({
|
||||
component: RouteComponent,
|
||||
})
|
||||
|
||||
function RouteComponent() {
|
||||
return <div>Hello "/_authenticated/dashboard"!</div>
|
||||
}
|
||||
@@ -203,7 +203,7 @@ function RouteComponent() {
|
||||
{recientes.length === 0 && <li className="text-sm text-neutral-500">Sin actividad</li>}
|
||||
{recientes.map((r) => (
|
||||
<li key={`${r.tipo}-${r.id}`} className="flex items-center justify-between gap-3">
|
||||
<Link to={`/ _authenticated/${r.tipo === 'plan' ? 'planes' : 'asignaturas'}/${r.id}`} className="truncate hover:underline">
|
||||
<Link to={`/${r.tipo === 'plan' ? 'planes' : 'asignaturas'}/${r.id}`} className="truncate hover:underline">
|
||||
<span className="inline-flex items-center gap-2">
|
||||
{r.tipo === 'plan' ? <Icons.ScrollText className="w-4 h-4" /> : <Icons.BookOpen className="w-4 h-4" />}
|
||||
{r.nombre ?? '—'}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { createFileRoute, useRouter, Link } from "@tanstack/react-router"
|
||||
import { useEffect, useMemo, useState } from "react"
|
||||
import { useMemo, useState } from "react"
|
||||
import { supabase, useSupabaseAuth } from "@/auth/supabase"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { Button } from "@/components/ui/button"
|
||||
@@ -7,7 +7,6 @@ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import * as Icons from "lucide-react"
|
||||
import { Plus, RefreshCcw, Building2, ScrollText, BookOpen } from "lucide-react"
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog"
|
||||
|
||||
export type PlanDeEstudios = {
|
||||
id: string; nombre: string; nivel: string | null; duracion: string | null;
|
||||
@@ -38,6 +37,7 @@ export const Route = createFileRoute("/_authenticated/planes")({
|
||||
if (error) throw new Error(error.message)
|
||||
return (data ?? []) as PlanRow[]
|
||||
},
|
||||
|
||||
})
|
||||
|
||||
/* ---------- helpers de estilo suave ---------- */
|
||||
@@ -62,10 +62,9 @@ function RouteComponent() {
|
||||
const [q, setQ] = useState("")
|
||||
const data = Route.useLoaderData() as PlanRow[]
|
||||
const router = useRouter()
|
||||
const search = Route.useSearch<{ planId?: string }>() // usaremos ?planId=... para el modal
|
||||
|
||||
const showFacultad = auth.claims?.role === "lci" || auth.claims?.role === "vicerrectoria"
|
||||
const showCarrera = auth.claims?.role === "secretario_academico"
|
||||
const showCarrera = showFacultad || auth.claims?.role === "secretario_academico"
|
||||
|
||||
const filtered = useMemo(() => {
|
||||
const term = q.trim().toLowerCase()
|
||||
@@ -106,11 +105,7 @@ function RouteComponent() {
|
||||
return (
|
||||
<Link
|
||||
key={p.id}
|
||||
// Runtime navega con ?planId=... (abrimos el modal),
|
||||
// pero la URL se enmascara SIN el search param:
|
||||
to="/planes/$planId/modal"
|
||||
search={{ planId: p.id }}
|
||||
mask={{ to: '/planes/$planId', params: { planId: p.id } }}
|
||||
className="group relative overflow-hidden rounded-3xl bg-white shadow-sm ring-1 transition-all hover:shadow-md hover:-translate-y-0.5"
|
||||
params={{ planId: p.id }}
|
||||
style={styles}
|
||||
@@ -131,12 +126,12 @@ function RouteComponent() {
|
||||
|
||||
<div className="flex items-center gap-2 text-xs">
|
||||
{showCarrera && p.carreras?.nombre && (
|
||||
<Badge variant="secondary" className="border text-neutral-700 bg-white/70">
|
||||
<Badge variant="secondary" className="border text-neutral-700 bg-white/70 w-fit">
|
||||
<ScrollText className="mr-1 h-3 w-3" /> {p.carreras?.nombre}
|
||||
</Badge>
|
||||
)}
|
||||
{showFacultad && fac?.nombre && (
|
||||
<Badge variant="outline" className="bg-white/60" style={{ borderColor: styles.borderColor }}>
|
||||
<Badge variant="outline" className="bg-white/60 w-fit" style={{ borderColor: styles.borderColor }}>
|
||||
<BookOpen className="mr-1 h-3 w-3" /> {fac?.nombre}
|
||||
</Badge>
|
||||
)}
|
||||
@@ -157,95 +152,6 @@ function RouteComponent() {
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* MODAL: se muestra si existe ?planId=... */}
|
||||
<PlanPreviewModal planId={search?.planId} onClose={() =>
|
||||
router.navigate({ to: "/planes", replace: true })
|
||||
} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
/* ---------- Modal (carga ligera por id) ---------- */
|
||||
function PlanPreviewModal({ planId, onClose }: { planId?: string; onClose: () => void }) {
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [plan, setPlan] = useState<null | {
|
||||
id: string; nombre: string; nivel: string | null; duracion: string | null;
|
||||
total_creditos: number | null; estado: string | null;
|
||||
carreras: { nombre: string; facultades?: { nombre: string; color?: string | null; icon?: string | null } | null } | null
|
||||
}>(null)
|
||||
|
||||
useEffect(() => {
|
||||
let alive = true
|
||||
async function fetchPlan() {
|
||||
if (!planId) return
|
||||
setLoading(true)
|
||||
const { data, error } = await supabase
|
||||
.from("plan_estudios")
|
||||
.select(`
|
||||
id, nombre, nivel, duracion, total_creditos, estado,
|
||||
carreras (
|
||||
nombre,
|
||||
facultades:facultades ( nombre, color, icon )
|
||||
)
|
||||
`)
|
||||
.eq("id", planId)
|
||||
.single()
|
||||
if (!alive) return
|
||||
if (!error) setPlan(data as any)
|
||||
setLoading(false)
|
||||
}
|
||||
fetchPlan()
|
||||
return () => { alive = false }
|
||||
}, [planId])
|
||||
|
||||
const fac = plan?.carreras?.facultades
|
||||
const [r, g, b] = hexToRgb(fac?.color)
|
||||
const headerStyle = { background: `linear-gradient(135deg, rgba(${r},${g},${b},.14), rgba(${r},${g},${b},.06))` }
|
||||
|
||||
return (
|
||||
<Dialog open={!!planId} onOpenChange={() => onClose()}>
|
||||
<DialogContent className="max-w-2xl p-0 overflow-hidden">
|
||||
<div className="p-6" style={headerStyle}>
|
||||
<DialogHeader className="space-y-1">
|
||||
<DialogTitle>{plan?.nombre ?? "Cargando…"}</DialogTitle>
|
||||
<div className="text-xs text-neutral-600">
|
||||
{plan?.carreras?.nombre ?? "—"} {fac?.nombre ? `· ${fac?.nombre}` : ""}
|
||||
</div>
|
||||
</DialogHeader>
|
||||
</div>
|
||||
|
||||
<div className="p-6 space-y-4">
|
||||
{loading && <div className="text-sm text-neutral-500">Cargando…</div>}
|
||||
{!loading && plan && (
|
||||
<>
|
||||
<div className="grid grid-cols-2 gap-3 text-sm">
|
||||
<div><span className="text-neutral-500">Nivel:</span> <span className="font-medium">{plan.nivel ?? "—"}</span></div>
|
||||
<div><span className="text-neutral-500">Duración:</span> <span className="font-medium">{plan.duracion ?? "—"}</span></div>
|
||||
<div><span className="text-neutral-500">Créditos:</span> <span className="font-medium">{plan.total_creditos ?? "—"}</span></div>
|
||||
<div><span className="text-neutral-500">Estado:</span> <span className="font-medium">{plan.estado ?? "—"}</span></div>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2">
|
||||
<Link
|
||||
to="/_authenticated/planes/$planId"
|
||||
params={{ planId: plan.id }}
|
||||
className="inline-flex items-center gap-2 rounded-xl bg-black text-white px-4 py-2 hover:opacity-90"
|
||||
>
|
||||
<Icons.FileText className="w-4 h-4" /> Ver ficha
|
||||
</Link>
|
||||
<Link
|
||||
to="/_authenticated/asignaturas"
|
||||
search={{ planId: plan.id }}
|
||||
className="inline-flex items-center gap-2 rounded-xl border px-4 py-2 hover:bg-neutral-50"
|
||||
>
|
||||
<Icons.BookOpen className="w-4 h-4" /> Ver asignaturas
|
||||
</Link>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -33,7 +33,7 @@ export const Route = createFileRoute("/_authenticated/planes/$planId/modal")({
|
||||
.eq("id", params.planId)
|
||||
.single()
|
||||
if (error) throw error
|
||||
return data as PlanDetail
|
||||
return data
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
Reference in New Issue
Block a user