Filtro funcionando primera versión
This commit is contained in:
@@ -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}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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
|
||||
|
||||
93
src/components/planes/Filtro.tsx
Normal file
93
src/components/planes/Filtro.tsx
Normal 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
|
||||
@@ -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 }
|
||||
|
||||
25
src/components/ui/LoginInput.tsx
Normal file
25
src/components/ui/LoginInput.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
62
src/components/ui/button.tsx
Normal file
62
src/components/ui/button.tsx
Normal 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 }
|
||||
182
src/components/ui/command.tsx
Normal file
182
src/components/ui/command.tsx
Normal 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,
|
||||
}
|
||||
143
src/components/ui/dialog.tsx
Normal file
143
src/components/ui/dialog.tsx
Normal 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,
|
||||
}
|
||||
22
src/components/ui/label.tsx
Normal file
22
src/components/ui/label.tsx
Normal 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 }
|
||||
46
src/components/ui/popover.tsx
Normal file
46
src/components/ui/popover.tsx
Normal 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 }
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user