Filtro funcionando primera versión

This commit is contained in:
2025-12-25 18:52:37 -06:00
parent 2f4f445ff0
commit d0b80b77f5
14 changed files with 1007 additions and 134 deletions

View File

@@ -1,6 +1,7 @@
import { useState } from 'react'
//import { supabase } from '@/lib/supabase'
import { Input } from '../ui/Input'
// import { supabase } from '@/lib/supabase'
import { LoginInput } from '../ui/LoginInput'
import { SubmitButton } from '../ui/SubmitButton'
export function ExternalLoginForm() {
@@ -8,7 +9,7 @@ export function ExternalLoginForm() {
const [password, setPassword] = useState('')
const submit = async () => {
/*await supabase.auth.signInWithPassword({
/* await supabase.auth.signInWithPassword({
email,
password,
})*/
@@ -17,13 +18,17 @@ export function ExternalLoginForm() {
return (
<form
className="space-y-4"
onSubmit={e => {
onSubmit={(e) => {
e.preventDefault()
submit()
}}
>
<Input label="Correo electrónico" value={email} onChange={setEmail} />
<Input
<LoginInput
label="Correo electrónico"
value={email}
onChange={setEmail}
/>
<LoginInput
label="Contraseña"
type="password"
value={password}

View File

@@ -1,6 +1,7 @@
import { useState } from 'react'
//import { supabase } from '@/lib/supabase'
import { Input } from '../ui/Input'
// import { supabase } from '@/lib/supabase'
import { LoginInput } from '../ui/LoginInput'
import { SubmitButton } from '../ui/SubmitButton'
export function InternalLoginForm() {
@@ -8,7 +9,7 @@ export function InternalLoginForm() {
const [password, setPassword] = useState('')
const submit = async () => {
/*await supabase.auth.signInWithPassword({
/* await supabase.auth.signInWithPassword({
email: `${clave}@ulsa.mx`,
password,
})*/
@@ -17,13 +18,13 @@ export function InternalLoginForm() {
return (
<form
className="space-y-4"
onSubmit={e => {
onSubmit={(e) => {
e.preventDefault()
submit()
}}
>
<Input label="Clave ULSA" value={clave} onChange={setClave} />
<Input
<LoginInput label="Clave ULSA" value={clave} onChange={setClave} />
<LoginInput
label="Contraseña"
type="password"
value={password}

View File

@@ -2,28 +2,38 @@ import { SearchIcon } from 'lucide-react'
import { useId } from 'react'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
const InputSearchIconDemo = () => {
type Props = {
value: string
onChange: (value: string) => void
placeholder?: string
className?: string
}
const BarraBusqueda: React.FC<Props> = ({
value,
onChange,
placeholder = 'Buscar…',
className,
}) => {
const id = useId()
return (
<div className="w-full max-w-xs space-y-2">
<Label htmlFor={id}>Search input with icon and button</Label>
<div className="relative">
<div className="text-muted-foreground pointer-events-none absolute inset-y-0 left-0 flex items-center justify-center pl-3 peer-disabled:opacity-50">
<SearchIcon className="size-4" />
<span className="sr-only">Search</span>
</div>
<Input
id={id}
type="search"
placeholder="Search..."
className="peer px-9 [&::-webkit-search-cancel-button]:appearance-none [&::-webkit-search-decoration]:appearance-none [&::-webkit-search-results-button]:appearance-none [&::-webkit-search-results-decoration]:appearance-none"
/>
<div className={['relative', className].filter(Boolean).join(' ')}>
<div className="text-muted-foreground pointer-events-none absolute inset-y-0 left-0 flex items-center justify-center pl-3 peer-disabled:opacity-50">
<SearchIcon className="size-4" />
<span className="sr-only">Buscar</span>
</div>
<Input
id={id}
type="search"
value={value}
onChange={(e) => onChange(e.target.value)}
placeholder={placeholder}
className="peer px-9 [&::-webkit-search-cancel-button]:appearance-none [&::-webkit-search-decoration]:appearance-none [&::-webkit-search-results-button]:appearance-none [&::-webkit-search-results-decoration]:appearance-none"
/>
</div>
)
}
export default InputSearchIconDemo
export default BarraBusqueda

View File

@@ -0,0 +1,93 @@
'use client'
import { CheckIcon, ChevronDown } from 'lucide-react'
import { useState } from 'react'
import { Button } from '@/components/ui/button'
import {
Command,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
CommandList,
} from '@/components/ui/command'
import {
Popover,
PopoverContent,
PopoverTrigger,
} from '@/components/ui/popover'
import { cn } from '@/lib/utils'
export type Option = { value: string; label: string }
type Props = {
options: Array<Option>
value: string | null
onChange: (value: string) => void
placeholder?: string
className?: string
ariaLabel?: string
}
const Filtro: React.FC<Props> = ({
options,
value,
onChange,
placeholder = 'Seleccionar…',
className,
ariaLabel,
}) => {
const [open, setOpen] = useState(false)
const label = value
? (options.find((o) => o.value === value)?.label ?? placeholder)
: placeholder
return (
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild>
<Button
variant="outline"
role="combobox"
aria-expanded={open}
className={cn('w-full justify-between', className)}
aria-label={ariaLabel ?? 'Filtro combobox'}
>
{label}
<ChevronDown className="opacity-50" />
</Button>
</PopoverTrigger>
<PopoverContent className="p-0">
<Command>
<CommandInput placeholder="Buscar…" className="h-9" />
<CommandList>
<CommandEmpty>Sin resultados.</CommandEmpty>
<CommandGroup>
{options.map((opt) => (
<CommandItem
key={opt.value}
value={opt.value}
onSelect={(currentValue) => {
onChange(currentValue)
setOpen(false)
}}
>
{opt.label}
<CheckIcon
className={cn(
'ml-auto',
value === opt.value ? 'opacity-100' : 'opacity-0',
)}
/>
</CommandItem>
))}
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
)
}
export default Filtro

View File

@@ -1,28 +1,21 @@
interface InputProps {
label: string
value: string
onChange: (value: string) => void
type?: string
}
import * as React from "react"
export function Input({
label,
value,
onChange,
type = 'text',
}: InputProps) {
import { cn } from "@/lib/utils"
function Input({ className, type, ...props }: React.ComponentProps<"input">) {
return (
<div className="space-y-1">
<label className="text-sm font-medium text-gray-700">
{label}
</label>
<input
type={type}
value={value}
onChange={e => onChange(e.target.value)}
className="w-full rounded-lg border border-gray-300 px-3 py-2
focus:outline-none focus:ring-2 focus:ring-[#7b0f1d]"
/>
</div>
<input
type={type}
data-slot="input"
className={cn(
"file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
"aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
className
)}
{...props}
/>
)
}
export { Input }

View File

@@ -0,0 +1,25 @@
interface InputProps {
label: string
value: string
onChange: (value: string) => void
type?: string
}
export function LoginInput({
label,
value,
onChange,
type = 'text',
}: InputProps) {
return (
<div className="space-y-1">
<label className="text-sm font-medium text-gray-700">{label}</label>
<input
type={type}
value={value}
onChange={(e) => onChange(e.target.value)}
className="w-full rounded-lg border border-gray-300 px-3 py-2 focus:ring-2 focus:ring-[#7b0f1d] focus:outline-none"
/>
</div>
)
}

View File

@@ -0,0 +1,62 @@
import * as React from "react"
import { Slot } from "@radix-ui/react-slot"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const buttonVariants = cva(
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
{
variants: {
variant: {
default: "bg-primary text-primary-foreground hover:bg-primary/90",
destructive:
"bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
outline:
"border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
secondary:
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
ghost:
"hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
default: "h-9 px-4 py-2 has-[>svg]:px-3",
sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
icon: "size-9",
"icon-sm": "size-8",
"icon-lg": "size-10",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
)
function Button({
className,
variant = "default",
size = "default",
asChild = false,
...props
}: React.ComponentProps<"button"> &
VariantProps<typeof buttonVariants> & {
asChild?: boolean
}) {
const Comp = asChild ? Slot : "button"
return (
<Comp
data-slot="button"
data-variant={variant}
data-size={size}
className={cn(buttonVariants({ variant, size, className }))}
{...props}
/>
)
}
export { Button, buttonVariants }

View File

@@ -0,0 +1,182 @@
import * as React from "react"
import { Command as CommandPrimitive } from "cmdk"
import { SearchIcon } from "lucide-react"
import { cn } from "@/lib/utils"
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog"
function Command({
className,
...props
}: React.ComponentProps<typeof CommandPrimitive>) {
return (
<CommandPrimitive
data-slot="command"
className={cn(
"bg-popover text-popover-foreground flex h-full w-full flex-col overflow-hidden rounded-md",
className
)}
{...props}
/>
)
}
function CommandDialog({
title = "Command Palette",
description = "Search for a command to run...",
children,
className,
showCloseButton = true,
...props
}: React.ComponentProps<typeof Dialog> & {
title?: string
description?: string
className?: string
showCloseButton?: boolean
}) {
return (
<Dialog {...props}>
<DialogHeader className="sr-only">
<DialogTitle>{title}</DialogTitle>
<DialogDescription>{description}</DialogDescription>
</DialogHeader>
<DialogContent
className={cn("overflow-hidden p-0", className)}
showCloseButton={showCloseButton}
>
<Command className="[&_[cmdk-group-heading]]:text-muted-foreground **:data-[slot=command-input-wrapper]:h-12 [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group]]:px-2 [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5">
{children}
</Command>
</DialogContent>
</Dialog>
)
}
function CommandInput({
className,
...props
}: React.ComponentProps<typeof CommandPrimitive.Input>) {
return (
<div
data-slot="command-input-wrapper"
className="flex h-9 items-center gap-2 border-b px-3"
>
<SearchIcon className="size-4 shrink-0 opacity-50" />
<CommandPrimitive.Input
data-slot="command-input"
className={cn(
"placeholder:text-muted-foreground flex h-10 w-full rounded-md bg-transparent py-3 text-sm outline-hidden disabled:cursor-not-allowed disabled:opacity-50",
className
)}
{...props}
/>
</div>
)
}
function CommandList({
className,
...props
}: React.ComponentProps<typeof CommandPrimitive.List>) {
return (
<CommandPrimitive.List
data-slot="command-list"
className={cn(
"max-h-[300px] scroll-py-1 overflow-x-hidden overflow-y-auto",
className
)}
{...props}
/>
)
}
function CommandEmpty({
...props
}: React.ComponentProps<typeof CommandPrimitive.Empty>) {
return (
<CommandPrimitive.Empty
data-slot="command-empty"
className="py-6 text-center text-sm"
{...props}
/>
)
}
function CommandGroup({
className,
...props
}: React.ComponentProps<typeof CommandPrimitive.Group>) {
return (
<CommandPrimitive.Group
data-slot="command-group"
className={cn(
"text-foreground [&_[cmdk-group-heading]]:text-muted-foreground overflow-hidden p-1 [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium",
className
)}
{...props}
/>
)
}
function CommandSeparator({
className,
...props
}: React.ComponentProps<typeof CommandPrimitive.Separator>) {
return (
<CommandPrimitive.Separator
data-slot="command-separator"
className={cn("bg-border -mx-1 h-px", className)}
{...props}
/>
)
}
function CommandItem({
className,
...props
}: React.ComponentProps<typeof CommandPrimitive.Item>) {
return (
<CommandPrimitive.Item
data-slot="command-item"
className={cn(
"data-[selected=true]:bg-accent data-[selected=true]:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
)}
{...props}
/>
)
}
function CommandShortcut({
className,
...props
}: React.ComponentProps<"span">) {
return (
<span
data-slot="command-shortcut"
className={cn(
"text-muted-foreground ml-auto text-xs tracking-widest",
className
)}
{...props}
/>
)
}
export {
Command,
CommandDialog,
CommandInput,
CommandList,
CommandEmpty,
CommandGroup,
CommandItem,
CommandShortcut,
CommandSeparator,
}

View File

@@ -0,0 +1,143 @@
"use client"
import * as React from "react"
import * as DialogPrimitive from "@radix-ui/react-dialog"
import { XIcon } from "lucide-react"
import { cn } from "@/lib/utils"
function Dialog({
...props
}: React.ComponentProps<typeof DialogPrimitive.Root>) {
return <DialogPrimitive.Root data-slot="dialog" {...props} />
}
function DialogTrigger({
...props
}: React.ComponentProps<typeof DialogPrimitive.Trigger>) {
return <DialogPrimitive.Trigger data-slot="dialog-trigger" {...props} />
}
function DialogPortal({
...props
}: React.ComponentProps<typeof DialogPrimitive.Portal>) {
return <DialogPrimitive.Portal data-slot="dialog-portal" {...props} />
}
function DialogClose({
...props
}: React.ComponentProps<typeof DialogPrimitive.Close>) {
return <DialogPrimitive.Close data-slot="dialog-close" {...props} />
}
function DialogOverlay({
className,
...props
}: React.ComponentProps<typeof DialogPrimitive.Overlay>) {
return (
<DialogPrimitive.Overlay
data-slot="dialog-overlay"
className={cn(
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
className
)}
{...props}
/>
)
}
function DialogContent({
className,
children,
showCloseButton = true,
...props
}: React.ComponentProps<typeof DialogPrimitive.Content> & {
showCloseButton?: boolean
}) {
return (
<DialogPortal data-slot="dialog-portal">
<DialogOverlay />
<DialogPrimitive.Content
data-slot="dialog-content"
className={cn(
"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 outline-none sm:max-w-lg",
className
)}
{...props}
>
{children}
{showCloseButton && (
<DialogPrimitive.Close
data-slot="dialog-close"
className="ring-offset-background focus:ring-ring data-[state=open]:bg-accent data-[state=open]:text-muted-foreground absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4"
>
<XIcon />
<span className="sr-only">Close</span>
</DialogPrimitive.Close>
)}
</DialogPrimitive.Content>
</DialogPortal>
)
}
function DialogHeader({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="dialog-header"
className={cn("flex flex-col gap-2 text-center sm:text-left", className)}
{...props}
/>
)
}
function DialogFooter({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="dialog-footer"
className={cn(
"flex flex-col-reverse gap-2 sm:flex-row sm:justify-end",
className
)}
{...props}
/>
)
}
function DialogTitle({
className,
...props
}: React.ComponentProps<typeof DialogPrimitive.Title>) {
return (
<DialogPrimitive.Title
data-slot="dialog-title"
className={cn("text-lg leading-none font-semibold", className)}
{...props}
/>
)
}
function DialogDescription({
className,
...props
}: React.ComponentProps<typeof DialogPrimitive.Description>) {
return (
<DialogPrimitive.Description
data-slot="dialog-description"
className={cn("text-muted-foreground text-sm", className)}
{...props}
/>
)
}
export {
Dialog,
DialogClose,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogOverlay,
DialogPortal,
DialogTitle,
DialogTrigger,
}

View File

@@ -0,0 +1,22 @@
import * as React from "react"
import * as LabelPrimitive from "@radix-ui/react-label"
import { cn } from "@/lib/utils"
function Label({
className,
...props
}: React.ComponentProps<typeof LabelPrimitive.Root>) {
return (
<LabelPrimitive.Root
data-slot="label"
className={cn(
"flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50",
className
)}
{...props}
/>
)
}
export { Label }

View File

@@ -0,0 +1,46 @@
import * as React from "react"
import * as PopoverPrimitive from "@radix-ui/react-popover"
import { cn } from "@/lib/utils"
function Popover({
...props
}: React.ComponentProps<typeof PopoverPrimitive.Root>) {
return <PopoverPrimitive.Root data-slot="popover" {...props} />
}
function PopoverTrigger({
...props
}: React.ComponentProps<typeof PopoverPrimitive.Trigger>) {
return <PopoverPrimitive.Trigger data-slot="popover-trigger" {...props} />
}
function PopoverContent({
className,
align = "center",
sideOffset = 4,
...props
}: React.ComponentProps<typeof PopoverPrimitive.Content>) {
return (
<PopoverPrimitive.Portal>
<PopoverPrimitive.Content
data-slot="popover-content"
align={align}
sideOffset={sideOffset}
className={cn(
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-72 origin-(--radix-popover-content-transform-origin) rounded-md border p-4 shadow-md outline-hidden",
className
)}
{...props}
/>
</PopoverPrimitive.Portal>
)
}
function PopoverAnchor({
...props
}: React.ComponentProps<typeof PopoverPrimitive.Anchor>) {
return <PopoverPrimitive.Anchor data-slot="popover-anchor" {...props} />
}
export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor }

View File

@@ -9,8 +9,14 @@ import {
Plus,
Scale,
Stethoscope,
X,
} from 'lucide-react'
import { useMemo, useState } from 'react'
import type { Option } from '@/components/planes/Filtro'
import BarraBusqueda from '@/components/planes/BarraBusqueda'
import Filtro from '@/components/planes/Filtro'
import PlanEstudiosCard from '@/components/planes/PlanEstudiosCard'
export const Route = createFileRoute('/planes')({
@@ -18,6 +24,196 @@ export const Route = createFileRoute('/planes')({
})
function RouteComponent() {
type Facultad = { id: string; nombre: string; color: string }
type Carrera = { id: string; nombre: string; facultadId: string }
type Plan = {
id: string
Icono: any
nombrePrograma: string
nivel: string
ciclos: string
facultadId: string
carreraId: string
estado:
| 'Aprobado'
| 'Pendiente'
| 'En proceso'
| 'Revisión expertos'
| 'Actualización'
claseColorEstado: string
}
// Simulación: datos provenientes de Supabase (hardcode)
const facultades: Array<Facultad> = [
{ id: 'ing', nombre: 'Facultad de Ingeniería', color: '#2563eb' },
{ id: 'med', nombre: 'Facultad de Medicina', color: '#dc2626' },
{ id: 'neg', nombre: 'Facultad de Negocios', color: '#059669' },
{
id: 'arq',
nombre: 'Facultad Mexicana de Arquitectura, Diseño y Comunicación',
color: '#ea580c',
},
{
id: 'sal',
nombre: 'Escuela de Altos Estudios en Salud',
color: '#0891b2',
},
{ id: 'der', nombre: 'Facultad de Derecho', color: '#7c3aed' },
{ id: 'qui', nombre: 'Facultad de Ciencias Químicas', color: '#65a30d' },
]
const carreras: Array<Carrera> = [
{
id: 'sis',
nombre: 'Ingeniería en Sistemas Computacionales',
facultadId: 'ing',
},
{ id: 'medico', nombre: 'Médico Cirujano', facultadId: 'med' },
{ id: 'act', nombre: 'Licenciatura en Actuaría', facultadId: 'neg' },
{ id: 'arq', nombre: 'Licenciatura en Arquitectura', facultadId: 'arq' },
{ id: 'fisio', nombre: 'Licenciatura en Fisioterapia', facultadId: 'sal' },
{ id: 'der', nombre: 'Licenciatura en Derecho', facultadId: 'der' },
{ id: 'qfb', nombre: 'Químico Farmacéutico Biólogo', facultadId: 'qui' },
]
const estados: Array<Option> = [
{ value: 'todos', label: 'Todos los estados' },
{ value: 'Aprobado', label: 'Aprobado' },
{ value: 'Pendiente', label: 'Pendiente' },
{ value: 'En proceso', label: 'En proceso' },
{ value: 'Revisión expertos', label: 'Revisión expertos' },
{ value: 'Actualización', label: 'Actualización' },
]
const planes: Array<Plan> = [
{
id: 'p1',
Icono: Laptop,
nombrePrograma: 'Ingeniería en Sistemas Computacionales',
nivel: 'Licenciatura',
ciclos: '8 semestres',
facultadId: 'ing',
carreraId: 'sis',
estado: 'Revisión expertos',
claseColorEstado: 'bg-amber-600',
},
{
id: 'p2',
Icono: Stethoscope,
nombrePrograma: 'Médico Cirujano',
nivel: 'Licenciatura',
ciclos: '10 semestres',
facultadId: 'med',
carreraId: 'medico',
estado: 'Aprobado',
claseColorEstado: 'bg-emerald-600',
},
{
id: 'p3',
Icono: Calculator,
nombrePrograma: 'Licenciatura en Actuaría',
nivel: 'Licenciatura',
ciclos: '9 semestres',
facultadId: 'neg',
carreraId: 'act',
estado: 'Aprobado',
claseColorEstado: 'bg-emerald-600',
},
{
id: 'p4',
Icono: PencilRuler,
nombrePrograma: 'Licenciatura en Arquitectura',
nivel: 'Licenciatura',
ciclos: '10 semestres',
facultadId: 'arq',
carreraId: 'arq',
estado: 'En proceso',
claseColorEstado: 'bg-orange-500',
},
{
id: 'p5',
Icono: Activity,
nombrePrograma: 'Licenciatura en Fisioterapia',
nivel: 'Licenciatura',
ciclos: '8 semestres',
facultadId: 'sal',
carreraId: 'fisio',
estado: 'Revisión expertos',
claseColorEstado: 'bg-amber-600',
},
{
id: 'p6',
Icono: Scale,
nombrePrograma: 'Licenciatura en Derecho',
nivel: 'Licenciatura',
ciclos: '10 semestres',
facultadId: 'der',
carreraId: 'der',
estado: 'Pendiente',
claseColorEstado: 'bg-yellow-500',
},
{
id: 'p7',
Icono: FlaskConical,
nombrePrograma: 'Químico Farmacéutico Biólogo',
nivel: 'Licenciatura',
ciclos: '9 semestres',
facultadId: 'qui',
carreraId: 'qfb',
estado: 'Actualización',
claseColorEstado: 'bg-lime-600',
},
]
// Estado de filtros
const [search, setSearch] = useState('')
const [facultadSel, setFacultadSel] = useState<string>('todas')
const [carreraSel, setCarreraSel] = useState<string>('todas')
const [estadoSel, setEstadoSel] = useState<string>('todos')
// Opciones para filtros
const facultadesOptions: Array<Option> = useMemo(
() => [
{ value: 'todas', label: 'Todas las facultades' },
...facultades.map((f) => ({ value: f.id, label: f.nombre })),
],
[facultades],
)
const carrerasOptions: Array<Option> = useMemo(() => {
const list =
facultadSel === 'todas'
? carreras
: carreras.filter((c) => c.facultadId === facultadSel)
return [
{ value: 'todas', label: 'Todas las carreras' },
...list.map((c) => ({ value: c.id, label: c.nombre })),
]
}, [carreras, facultadSel])
// Filtrado de planes
const filteredPlans = useMemo(() => {
const term = search.trim().toLowerCase()
return planes.filter((p) => {
const matchName = term
? p.nombrePrograma.toLowerCase().includes(term)
: true
const matchFac =
facultadSel === 'todas' ? true : p.facultadId === facultadSel
const matchCar =
carreraSel === 'todas' ? true : p.carreraId === carreraSel
const matchEst = estadoSel === 'todos' ? true : p.estado === estadoSel
return matchName && matchFac && matchCar && matchEst
})
}, [planes, search, facultadSel, carreraSel, estadoSel])
const resetFilters = () => {
setSearch('')
setFacultadSel('todas')
setCarreraSel('todas')
setEstadoSel('todos')
}
return (
<main className="bg-background min-h-screen w-full">
<div className="mx-auto flex w-full max-w-7xl flex-col gap-4 px-4 py-6 md:px-6 lg:px-8">
@@ -50,89 +246,80 @@ function RouteComponent() {
Nuevo plan de estudios
</button>
</div>
<div className="flex flex-col items-stretch gap-2 lg:flex-row lg:items-center">
<div className="min-w-0 flex-1">
<BarraBusqueda
value={search}
onChange={setSearch}
placeholder="Buscar por programa…"
/>
</div>
<div className="flex flex-col items-stretch justify-between gap-2 lg:flex-row lg:items-center">
<div className="w-full lg:w-44">
<Filtro
options={facultadesOptions}
value={facultadSel}
onChange={(v) => {
setFacultadSel(v)
// Reset carrera si ya no pertenece
setCarreraSel('todas')
}}
placeholder="Facultad"
ariaLabel="Filtro por facultad"
/>
</div>
<div className="w-full lg:w-44">
<Filtro
options={carrerasOptions}
value={carreraSel}
onChange={setCarreraSel}
placeholder="Carrera"
ariaLabel="Filtro por carrera"
/>
</div>
<div className="w-full lg:w-44">
<Filtro
options={estados}
value={estadoSel}
onChange={setEstadoSel}
placeholder="Estado"
ariaLabel="Filtro por estado"
/>
</div>
<button
type="button"
onClick={resetFilters}
className={
'ring-offset-background focus-visible:ring-ring bg-secondary text-secondary-foreground hover:bg-secondary/90 inline-flex h-9 items-center justify-center gap-2 rounded-md px-4 text-sm font-medium whitespace-nowrap shadow-md transition-colors focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none disabled:pointer-events-none disabled:opacity-50'
}
title="Reiniciar filtros"
aria-label="Reiniciar filtros"
>
<X className="h-4 w-4" />
Limpiar
</button>
</div>
</div>
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
<PlanEstudiosCard
Icono={Laptop}
nombrePrograma="Ingeniería en Sistemas Computacionales"
nivel="Licenciatura"
ciclos="8 semestres"
facultad="Facultad de Ingeniería"
estado="Revisión expertos"
claseColorEstado="bg-amber-600"
colorFacultad="#2563eb"
onClick={() => console.log('Navegar a Sistemas...')}
/>
<PlanEstudiosCard
Icono={Stethoscope}
nombrePrograma="Médico Cirujano"
nivel="Licenciatura"
ciclos="10 semestres"
facultad="Facultad de Medicina"
estado="Aprobado"
claseColorEstado="bg-emerald-600"
colorFacultad="#dc2626"
/>
<PlanEstudiosCard
Icono={Calculator}
nombrePrograma="Licenciatura en Actuaría"
nivel="Licenciatura"
ciclos="9 semestres"
facultad="Facultad de Negocios"
estado="Aprobado"
claseColorEstado="bg-emerald-600"
colorFacultad="#059669"
onClick={() => console.log('Ver Actuaría')}
/>
<PlanEstudiosCard
Icono={PencilRuler}
nombrePrograma="Licenciatura en Arquitectura"
nivel="Licenciatura"
ciclos="10 semestres"
facultad="Facultad Mexicana de Arquitectura, Diseño y Comunicación"
estado="En proceso"
claseColorEstado="bg-orange-500"
colorFacultad="#ea580c"
onClick={() => console.log('Ver Arquitectura')}
/>
<PlanEstudiosCard
Icono={Activity}
nombrePrograma="Licenciatura en Fisioterapia"
nivel="Licenciatura"
ciclos="8 semestres"
facultad="Escuela de Altos Estudios en Salud"
estado="Revisión expertos"
claseColorEstado="bg-amber-600"
colorFacultad="#0891b2"
onClick={() => console.log('Ver Fisioterapia')}
/>
<PlanEstudiosCard
Icono={Scale}
nombrePrograma="Licenciatura en Derecho"
nivel="Licenciatura"
ciclos="10 semestres"
facultad="Facultad de Derecho"
estado="Pendiente"
claseColorEstado="bg-yellow-500"
colorFacultad="#7c3aed"
onClick={() => console.log('Ver Derecho')}
/>
<PlanEstudiosCard
Icono={FlaskConical}
nombrePrograma="Químico Farmacéutico Biólogo"
nivel="Licenciatura"
ciclos="9 semestres"
facultad="Facultad de Ciencias Químicas"
estado="Actualización"
claseColorEstado="bg-lime-600"
colorFacultad="#65a30d"
onClick={() => console.log('Ver QFB')}
/>
{filteredPlans.map((p) => {
const fac = facultades.find((f) => f.id === p.facultadId)!
return (
<PlanEstudiosCard
key={p.id}
Icono={p.Icono}
nombrePrograma={p.nombrePrograma}
nivel={p.nivel}
ciclos={p.ciclos}
facultad={fac.nombre}
estado={p.estado}
claseColorEstado={p.claseColorEstado}
colorFacultad={fac.color}
onClick={() => console.log('Ver', p.nombrePrograma)}
/>
)
})}
</div>
</div>
</div>