feat: implement asignaturas management with dynamic routing and UI updates
This commit is contained in:
@@ -1,9 +0,0 @@
|
||||
import { createFileRoute } from '@tanstack/react-router'
|
||||
|
||||
export const Route = createFileRoute('/_authenticated/asignaturas')({
|
||||
component: RouteComponent,
|
||||
})
|
||||
|
||||
function RouteComponent() {
|
||||
return <div>Hello "/_authenticated/asignaturas"!</div>
|
||||
}
|
||||
142
src/routes/_authenticated/asignaturas/$planId.tsx
Normal file
142
src/routes/_authenticated/asignaturas/$planId.tsx
Normal file
@@ -0,0 +1,142 @@
|
||||
import { createFileRoute, useRouter } from "@tanstack/react-router"
|
||||
import { supabase } from "@/auth/supabase"
|
||||
import * as Icons from "lucide-react"
|
||||
import { useMemo, useState } from "react"
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
|
||||
type Asignatura = {
|
||||
id: string
|
||||
nombre: string
|
||||
semestre: number | null
|
||||
creditos: number | null
|
||||
horas_teoricas: number | null
|
||||
horas_practicas: number | null
|
||||
}
|
||||
|
||||
type ModalData = {
|
||||
planId: string
|
||||
planNombre: string
|
||||
asignaturas: Asignatura[]
|
||||
}
|
||||
|
||||
export const Route = createFileRoute("/_authenticated/asignaturas/$planId")({
|
||||
component: ModalComponent,
|
||||
loader: async ({ params }): Promise<ModalData> => {
|
||||
const planId = params.planId
|
||||
|
||||
const { data: plan, error: planErr } = await supabase
|
||||
.from("plan_estudios")
|
||||
.select("id, nombre")
|
||||
.eq("id", planId)
|
||||
.single()
|
||||
if (planErr || !plan) throw planErr ?? new Error("Plan no encontrado")
|
||||
|
||||
const { data: asignaturas, error: aErr } = await supabase
|
||||
.from("asignaturas")
|
||||
.select("id, nombre, semestre, creditos, horas_teoricas, horas_practicas")
|
||||
.eq("plan_id", planId)
|
||||
.order("semestre", { ascending: true })
|
||||
.order("nombre", { ascending: true })
|
||||
|
||||
if (aErr) throw aErr
|
||||
|
||||
return {
|
||||
planId,
|
||||
planNombre: plan.nombre,
|
||||
asignaturas: (asignaturas ?? []) as Asignatura[],
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
function ModalComponent() {
|
||||
const { planId, planNombre, asignaturas } = Route.useLoaderData() as ModalData
|
||||
const router = useRouter()
|
||||
const [q, setQ] = useState("")
|
||||
|
||||
const filtered = useMemo(() => {
|
||||
const t = q.trim().toLowerCase()
|
||||
if (!t) return asignaturas
|
||||
return asignaturas.filter(a =>
|
||||
[a.nombre, a.semestre, a.creditos]
|
||||
.filter(Boolean)
|
||||
.some(v => String(v).toLowerCase().includes(t))
|
||||
)
|
||||
}, [q, asignaturas])
|
||||
|
||||
// Agrupar por semestre
|
||||
const groups = useMemo(() => {
|
||||
const m = new Map<number | string, Asignatura[]>()
|
||||
for (const a of filtered) {
|
||||
const k = a.semestre ?? "—"
|
||||
if (!m.has(k)) m.set(k, [])
|
||||
m.get(k)!.push(a)
|
||||
}
|
||||
return Array.from(m.entries()).sort(([a], [b]) => {
|
||||
if (a === "—") return 1
|
||||
if (b === "—") return -1
|
||||
return Number(a) - Number(b)
|
||||
})
|
||||
}, [filtered])
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
open
|
||||
onOpenChange={() =>
|
||||
router.navigate({ to: "/plan/$planId", params: { planId }, replace: true })
|
||||
}
|
||||
>
|
||||
<DialogContent className="w-[min(92vw,900px)]">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center gap-2">
|
||||
<Icons.BookOpen className="w-5 h-5" />
|
||||
Asignaturas · <span className="font-normal">{planNombre}</span>
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="flex items-center gap-2 pb-3">
|
||||
<Input
|
||||
value={q}
|
||||
onChange={(e) => setQ(e.target.value)}
|
||||
placeholder="Buscar por nombre, semestre…"
|
||||
className="w-full"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="max-h-[65vh] overflow-auto pr-1">
|
||||
{groups.length === 0 && (
|
||||
<div className="text-sm text-neutral-500 py-8 text-center">Sin asignaturas</div>
|
||||
)}
|
||||
|
||||
<div className="space-y-5">
|
||||
{groups.map(([sem, items]) => (
|
||||
<div key={String(sem)}>
|
||||
<div className="mb-2 text-xs font-semibold text-neutral-500">
|
||||
Semestre {sem}
|
||||
</div>
|
||||
<ul className="grid gap-2 sm:grid-cols-2">
|
||||
{items.map(a => (
|
||||
<li key={a.id} className="rounded-xl border p-3 bg-white/70 dark:bg-neutral-900/60">
|
||||
<div className="font-medium truncate">{a.nombre}</div>
|
||||
<div className="mt-1 flex flex-wrap items-center gap-2 text-xs text-neutral-600">
|
||||
{a.creditos != null && (
|
||||
<Badge variant="outline" className="bg-white/60">Créditos: {a.creditos}</Badge>
|
||||
)}
|
||||
{(a.horas_teoricas ?? 0) + (a.horas_practicas ?? 0) > 0 && (
|
||||
<Badge variant="secondary" className="bg-neutral-100 text-neutral-800 dark:bg-neutral-800 dark:text-neutral-100">
|
||||
Hrs T/P: {a.horas_teoricas ?? 0}/{a.horas_practicas ?? 0}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import { createFileRoute, Link } from '@tanstack/react-router'
|
||||
import * as Icons from 'lucide-react'
|
||||
import { supabase } from '@/auth/supabase'
|
||||
import { useMemo } from 'react'
|
||||
import { useTheme } from '@/components/theme-provider'
|
||||
|
||||
type Facultad = { id: string; nombre: string; icon: string; color?: string | null }
|
||||
type Plan = {
|
||||
@@ -121,13 +122,14 @@ function gradientFrom(color?: string | null) {
|
||||
}
|
||||
|
||||
// ====== UI helpers ======
|
||||
function ProgressRing({ pct }: { pct: number }) {
|
||||
function ProgressRing({ pct, color }: { pct: number, color: string }) {
|
||||
const r = 42, c = 2 * Math.PI * r
|
||||
const offset = c * (1 - Math.min(Math.max(pct, 0), 100) / 100)
|
||||
// Puedes ajustar el color del stroke según el tema
|
||||
return (
|
||||
<div className="flex items-center gap-4">
|
||||
<svg width="112" height="112" viewBox="0 0 112 112" className="drop-shadow">
|
||||
<circle cx="56" cy="56" r={r} fill="none" stroke="rgba(0,0,0,.08)" strokeWidth="12" />
|
||||
<circle cx="56" cy="56" r={r} fill="none" stroke={color} strokeWidth="12" />
|
||||
<circle cx="56" cy="56" r={r} fill="none" stroke="currentColor" strokeWidth="12"
|
||||
strokeDasharray={c} strokeDashoffset={offset} strokeLinecap="round"
|
||||
transform="rotate(-90 56 56)" />
|
||||
@@ -143,7 +145,7 @@ function ProgressRing({ pct }: { pct: number }) {
|
||||
function HealthItem({ label, value, to }: { label: string; value: number; to: string }) {
|
||||
const warn = value > 0
|
||||
return (
|
||||
<Link to={to} className={`flex items-center justify-between rounded-xl px-4 py-3 border ${warn ? 'border-amber-300 bg-amber-50 text-amber-800' : 'border-neutral-200 bg-white'}`}>
|
||||
<Link to={to} className={`flex items-center justify-between rounded-xl px-4 py-3 border ${warn ? 'border-amber-300 bg-amber-50 text-amber-800' : 'border-neutral-200'}`}>
|
||||
<span className="text-sm">{label}</span>
|
||||
<span className="text-lg font-semibold tabular-nums">{value}</span>
|
||||
</Link>
|
||||
@@ -158,7 +160,7 @@ function RouteComponent() {
|
||||
return (
|
||||
<div className="p-6 space-y-8">
|
||||
{/* Header */}
|
||||
<div className="relative rounded-3xl overflow-hidden text-white shadow-xl" style={headerBg}>
|
||||
<div className="relative rounded-3xl overflow-hidden shadow-xl text-white" style={headerBg}>
|
||||
<div className="absolute inset-0 opacity-20" style={{ background: 'radial-gradient(800px 300px at 20% -10%, #fff, transparent 60%)' }} />
|
||||
<div className="relative p-8 flex items-center gap-5">
|
||||
<HeaderIcon className="w-16 h-16 md:w-20 md:h-20 drop-shadow" />
|
||||
@@ -179,14 +181,14 @@ function RouteComponent() {
|
||||
|
||||
{/* Calidad + Salud */}
|
||||
<div className="grid gap-6 lg:grid-cols-3">
|
||||
<div className="rounded-2xl bg-white shadow-lg ring-1 ring-black/5 p-5 lg:col-span-2">
|
||||
<div className="rounded-2xl shadow-lg ring-1 ring-black/5 p-5 lg:col-span-2">
|
||||
<div className="font-semibold mb-3">Calidad de planes</div>
|
||||
<ProgressRing pct={calidadPlanesPct} />
|
||||
<ProgressRing pct={calidadPlanesPct} color={facultad.color || 'white'}/>
|
||||
<div className="mt-3 text-sm text-neutral-600">
|
||||
Considera <span className="font-medium">objetivo general, perfiles, sistema de evaluación y créditos</span>.
|
||||
</div>
|
||||
</div>
|
||||
<div className="rounded-2xl bg-white shadow-lg ring-1 ring-black/5 p-5">
|
||||
<div className="rounded-2xl shadow-lg ring-1 ring-black/5 p-5">
|
||||
<div className="font-semibold mb-3">Salud de asignaturas</div>
|
||||
<div className="space-y-2">
|
||||
<HealthItem label="Sin bibliografía" value={saludAsignaturas.sinBibliografia} to={`/_authenticated/asignaturas?facultadId=${facultad.id}&f=sinBibliografia`} />
|
||||
@@ -197,7 +199,7 @@ function RouteComponent() {
|
||||
</div>
|
||||
|
||||
{/* Actividad reciente */}
|
||||
<div className="rounded-2xl bg-white shadow-lg ring-1 ring-black/5 p-5">
|
||||
<div className="rounded-2xl shadow-lg ring-1 ring-black/5 p-5">
|
||||
<div className="font-semibold mb-3">Actividad reciente</div>
|
||||
<ul className="space-y-2">
|
||||
{recientes.length === 0 && <li className="text-sm text-neutral-500">Sin actividad</li>}
|
||||
@@ -221,12 +223,12 @@ function RouteComponent() {
|
||||
// Tarjeta métrica (igual a tu StatTile)
|
||||
function Metric({ to, label, value, Icon }:{ to: string; label: string; value: number; Icon: any }) {
|
||||
return (
|
||||
<Link to={to} className="group rounded-2xl bg-white shadow-lg ring-1 ring-black/5 p-5 flex items-center justify-between hover:-translate-y-0.5 transition-all">
|
||||
<Link to={to} className="group rounded-2xl shadow-lg ring-1 ring-black/5 p-5 flex items-center justify-between hover:-translate-y-0.5 transition-all">
|
||||
<div>
|
||||
<div className="text-sm text-neutral-500">{label}</div>
|
||||
<div className="text-3xl font-bold tabular-nums">{value}</div>
|
||||
</div>
|
||||
<div className="p-3 rounded-xl bg-neutral-100 group-hover:bg-neutral-200">
|
||||
<div className="p-3 rounded-xl bg-neutral-100 dark:bg-neutral-800 group-hover:bg-neutral-200">
|
||||
<Icon className="w-7 h-7" />
|
||||
</div>
|
||||
</Link>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { createFileRoute, Link, useParams } from '@tanstack/react-router'
|
||||
import { createFileRoute, Link } from '@tanstack/react-router'
|
||||
import { supabase, useSupabaseAuth } from '@/auth/supabase'
|
||||
import * as Icons from 'lucide-react'
|
||||
import { useEffect, useMemo, useRef, useState } from 'react'
|
||||
@@ -25,7 +25,8 @@ type PlanFull = {
|
||||
estado: string | null; fecha_creacion: string | null;
|
||||
carreras: { id: string; nombre: string; facultades?: { id: string; nombre: string; color?: string | null; icon?: string | null } | null } | null
|
||||
}
|
||||
type LoaderData = { plan: PlanFull; asignaturasCount: number }
|
||||
type AsignaturaLite = { id: string; nombre: string; semestre: number | null; creditos: number | null }
|
||||
type LoaderData = { plan: PlanFull; asignaturasCount: number; asignaturasPreview: AsignaturaLite[] }
|
||||
|
||||
/* ============== ROUTE ============== */
|
||||
export const Route = createFileRoute('/_authenticated/plan/$planId')({
|
||||
@@ -49,7 +50,19 @@ export const Route = createFileRoute('/_authenticated/plan/$planId')({
|
||||
.select('*', { count: 'exact', head: true })
|
||||
.eq('plan_id', params.planId)
|
||||
|
||||
return { plan: plan as unknown as PlanFull, asignaturasCount: count ?? 0 }
|
||||
const { data: asignaturasPreview } = await supabase
|
||||
.from('asignaturas')
|
||||
.select('id, nombre, semestre, creditos')
|
||||
.eq('plan_id', params.planId)
|
||||
.order('semestre', { ascending: true })
|
||||
.order('nombre', { ascending: true })
|
||||
.limit(8)
|
||||
|
||||
return {
|
||||
plan: plan as unknown as PlanFull,
|
||||
asignaturasCount: count ?? 0,
|
||||
asignaturasPreview: (asignaturasPreview ?? []) as AsignaturaLite[],
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
@@ -112,7 +125,7 @@ function GradientMesh({ color }: { color?: string | null }) {
|
||||
|
||||
/* ============== PAGE ============== */
|
||||
function RouteComponent() {
|
||||
const { plan, asignaturasCount } = Route.useLoaderData() as LoaderData
|
||||
const { plan, asignaturasCount, asignaturasPreview } = Route.useLoaderData() as LoaderData
|
||||
const auth = useSupabaseAuth()
|
||||
const showFacultad = auth.claims?.role === 'lci' || auth.claims?.role === 'vicerrectoria'
|
||||
const showCarrera = auth.claims?.role === 'secretario_academico'
|
||||
@@ -144,7 +157,7 @@ function RouteComponent() {
|
||||
// Stats y campos con ScrollTrigger
|
||||
if (statsRef.current) {
|
||||
const ctx = gsap.context(() => {
|
||||
gsap.from('.kv', {
|
||||
gsap.from('.academics', {
|
||||
y: 14, opacity: 0, stagger: .08, duration: .4,
|
||||
scrollTrigger: { trigger: statsRef.current, start: 'top 85%' }
|
||||
})
|
||||
@@ -204,8 +217,8 @@ function RouteComponent() {
|
||||
</Badge>
|
||||
)}
|
||||
<Link
|
||||
to="/asignaturas"
|
||||
search={{ planId: plan.id }}
|
||||
to="/asignaturas/$planId"
|
||||
params={{ planId: plan.id }}
|
||||
className="inline-flex items-center gap-2 rounded-xl border px-3 py-2 text-sm hover:bg-neutral-50"
|
||||
>
|
||||
<Icons.BookOpen className="w-4 h-4" /> Ver asignaturas
|
||||
@@ -220,23 +233,58 @@ function RouteComponent() {
|
||||
{/* stats */}
|
||||
<CardContent
|
||||
ref={statsRef}
|
||||
className="relative z-10 grid gap-3 [grid-template-columns:repeat(auto-fit,minmax(180px,1fr))]"
|
||||
>
|
||||
<StatCard label="Nivel" value={plan.nivel ?? "—"} Icon={Icons.GraduationCap} accent={facColor} />
|
||||
<StatCard label="Duración" value={plan.duracion ?? "—"} Icon={Icons.Clock} accent={facColor} />
|
||||
<StatCard label="Créditos" value={fmt(plan.total_creditos)} Icon={Icons.Coins} accent={facColor} />
|
||||
<StatCard label="Asignaturas" value={fmt(asignaturasCount)} Icon={Icons.BookOpen} accent={facColor} />
|
||||
<StatCard
|
||||
label="Creado"
|
||||
value={plan.fecha_creacion ? new Date(plan.fecha_creacion).toLocaleDateString() : "—"}
|
||||
Icon={Icons.CalendarDays}
|
||||
accent={facColor}
|
||||
/>
|
||||
<div className="academics relative z-10 grid gap-3 [grid-template-columns:repeat(auto-fit,minmax(180px,1fr))]">
|
||||
|
||||
<StatCard label="Nivel" value={plan.nivel ?? "—"} Icon={Icons.GraduationCap} accent={facColor} />
|
||||
<StatCard label="Duración" value={plan.duracion ?? "—"} Icon={Icons.Clock} accent={facColor} />
|
||||
<StatCard label="Créditos" value={fmt(plan.total_creditos)} Icon={Icons.Coins} accent={facColor} />
|
||||
<StatCard
|
||||
label="Creado"
|
||||
value={plan.fecha_creacion ? new Date(plan.fecha_creacion).toLocaleDateString() : "—"}
|
||||
Icon={Icons.CalendarDays}
|
||||
accent={facColor}
|
||||
/>
|
||||
</div>
|
||||
|
||||
</CardContent>
|
||||
|
||||
</Card>
|
||||
|
||||
<AcademicSections planId={plan.id} plan={plan} color={fac?.color} />
|
||||
<div className="academics">
|
||||
<AcademicSections planId={plan.id} plan={plan} color={fac?.color} />
|
||||
</div>
|
||||
<Card className="border shadow-sm">
|
||||
<CardHeader className="flex items-center justify-between gap-2">
|
||||
<CardTitle className="text-base">Asignaturas ({asignaturasCount})</CardTitle>
|
||||
|
||||
{/* Abre el modal enmascarado */}
|
||||
<Link
|
||||
to="/asignaturas/$planId"
|
||||
params={{ planId: plan.id }}
|
||||
className="inline-flex items-center gap-2 rounded-xl border px-3 py-2 text-sm hover:bg-neutral-50"
|
||||
>
|
||||
<Icons.BookOpen className="w-4 h-4" /> Ver todas
|
||||
</Link>
|
||||
</CardHeader>
|
||||
|
||||
<CardContent className="flex flex-wrap gap-2">
|
||||
{asignaturasPreview.length === 0 && (
|
||||
<div className="text-sm text-neutral-500">Sin asignaturas</div>
|
||||
)}
|
||||
{asignaturasPreview.map(a => (
|
||||
<Link
|
||||
key={a.id}
|
||||
to="/asignaturas/$planId"
|
||||
params={{ planId: plan.id }}
|
||||
className="rounded-full border px-3 py-1 text-xs bg-white/70 hover:bg-white transition"
|
||||
title={a.nombre}
|
||||
>
|
||||
{a.semestre ? `S${a.semestre} · ` : ''}{a.nombre}
|
||||
</Link>
|
||||
))}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,112 +0,0 @@
|
||||
import { createFileRoute, useRouter } from "@tanstack/react-router"
|
||||
import { supabase } from "@/auth/supabase"
|
||||
import * as Icons from "lucide-react"
|
||||
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
|
||||
|
||||
type PlanDetail = {
|
||||
id: string
|
||||
nombre: string
|
||||
nivel: string | null
|
||||
duracion: string | null
|
||||
total_creditos: number | null
|
||||
estado: string | null
|
||||
carreras: {
|
||||
id: string
|
||||
nombre: string
|
||||
facultades?: { id: string; nombre: string; color?: string | null; icon?: string | null } | null
|
||||
} | null
|
||||
}
|
||||
|
||||
export const Route = createFileRoute('/_authenticated/plan/$planId/modal')({
|
||||
component: RouteComponent,
|
||||
loader: async ({ params }) => {
|
||||
const { data, error } = await supabase
|
||||
.from('plan_estudios')
|
||||
.select(`
|
||||
id, nombre, nivel, duracion, total_creditos, estado,
|
||||
carreras ( id, nombre, facultades:facultades ( id, nombre, color, icon ) )
|
||||
`)
|
||||
.eq('id', params.planId)
|
||||
.single()
|
||||
if (error) throw error
|
||||
return data
|
||||
},
|
||||
})
|
||||
|
||||
function gradientFrom(color?: string | null) {
|
||||
const base = (color && /^#([0-9a-f]{3}|[0-9a-f]{6})$/i.test(color)) ? color : "#2563eb"
|
||||
return `linear-gradient(135deg, ${base} 0%, ${base}CC 45%, ${base}99 75%, ${base}66 100%)`
|
||||
}
|
||||
|
||||
function RouteComponent() {
|
||||
const plan = Route.useLoaderData() as PlanDetail
|
||||
const router = useRouter()
|
||||
const fac = plan.carreras?.facultades
|
||||
const IconComp = (fac?.icon && (Icons as any)[fac.icon]) || Icons.Building2
|
||||
const headerBg = { background: gradientFrom(fac?.color) }
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
open
|
||||
onOpenChange={() =>
|
||||
router.navigate({
|
||||
to: '/plan/$planId',
|
||||
params: { planId: plan.id },
|
||||
replace: true,
|
||||
})
|
||||
}
|
||||
>
|
||||
<DialogContent className="max-w-2xl p-0 overflow-hidden" aria-describedby="">
|
||||
{/* Header con color/ícono de facultad */}
|
||||
<div className="p-6 text-white" style={headerBg}>
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="inline-flex items-center justify-center rounded-2xl bg-white/15 backdrop-blur px-3 py-2">
|
||||
<IconComp className="w-6 h-6" />
|
||||
</span>
|
||||
<div className="min-w-0">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="truncate">{plan.nombre}</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="text-xs opacity-90 truncate">
|
||||
{plan.carreras?.nombre ?? "—"} {fac?.nombre ? `· ${fac?.nombre}` : ""}
|
||||
</div>
|
||||
</div>
|
||||
{plan.estado && (
|
||||
<Badge variant="outline" className="ml-auto bg-white/10 text-white border-white/40">
|
||||
{plan.estado}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Cuerpo */}
|
||||
<div className="p-6 space-y-4">
|
||||
<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">Facultad:</span> <span className="font-medium">{fac?.nombre ?? "—"}</span></div>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2">
|
||||
<a
|
||||
href={`/_authenticated/planes/${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
|
||||
</a>
|
||||
<a
|
||||
href={`/_authenticated/asignaturas?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
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
@@ -122,7 +122,7 @@ function RouteComponent() {
|
||||
return (
|
||||
<Link
|
||||
key={p.id}
|
||||
to="/plan/$planId/modal"
|
||||
to="/plan/$planId"
|
||||
mask={{ to: '/plan/$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 }}
|
||||
|
||||
Reference in New Issue
Block a user