Dashboard v1 y configuraciones adicionales de prettier

This commit is contained in:
2025-12-18 19:49:50 -06:00
parent e40ea49031
commit ad197c9aad
11 changed files with 1825 additions and 6007 deletions

7223
bun.lock

File diff suppressed because it is too large Load Diff

View File

@@ -12,6 +12,9 @@
"check": "prettier --write . && eslint --fix"
},
"dependencies": {
"@radix-ui/react-avatar": "^1.1.11",
"@radix-ui/react-separator": "^1.1.8",
"@radix-ui/react-slot": "^1.2.4",
"@tailwindcss/vite": "^4.0.6",
"@tanstack/react-devtools": "^0.7.0",
"@tanstack/react-query": "^5.66.5",
@@ -21,7 +24,7 @@
"@tanstack/router-plugin": "^1.132.0",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"lucide-react": "^0.544.0",
"lucide-react": "^0.561.0",
"react": "^19.2.0",
"react-dom": "^19.2.0",
"tailwind-merge": "^3.0.2",
@@ -37,8 +40,11 @@
"@types/react": "^19.2.0",
"@types/react-dom": "^19.2.0",
"@vitejs/plugin-react": "^5.0.4",
"eslint-config-prettier": "^10.1.8",
"eslint-plugin-import": "^2.32.0",
"jsdom": "^27.0.0",
"prettier": "^3.5.3",
"prettier-plugin-tailwindcss": "^0.7.2",
"typescript": "^5.7.2",
"vite": "^7.1.7",
"vitest": "^3.0.5",

View File

@@ -4,7 +4,8 @@
const config = {
semi: false,
singleQuote: true,
trailingComma: "all",
};
trailingComma: 'all',
plugins: ['prettier-plugin-tailwindcss'],
}
export default config;
export default config

View File

@@ -8,10 +8,11 @@ export default function Header() {
return (
<>
<header className="p-4 flex items-center bg-gray-800 text-white shadow-lg">
{/* Cambiar el color de fondo por bg-background/40 y agregar backdrop-blur para efecto glassmorphism */}
<header className="sticky top-0 z-50 flex w-full items-center bg-gray-800 p-4 text-white shadow-lg">
<button
onClick={() => setIsOpen(true)}
className="p-2 hover:bg-gray-700 rounded-lg transition-colors"
className="rounded-lg p-2 transition-colors hover:bg-gray-700"
aria-label="Open menu"
>
<Menu size={24} />
@@ -28,26 +29,26 @@ export default function Header() {
</header>
<aside
className={`fixed top-0 left-0 h-full w-80 bg-gray-900 text-white shadow-2xl z-50 transform transition-transform duration-300 ease-in-out flex flex-col ${
className={`fixed top-0 left-0 z-50 flex h-full w-80 transform flex-col bg-gray-900 text-white shadow-2xl transition-transform duration-300 ease-in-out ${
isOpen ? 'translate-x-0' : '-translate-x-full'
}`}
>
<div className="flex items-center justify-between p-4 border-b border-gray-700">
<div className="flex items-center justify-between border-b border-gray-700 p-4">
<h2 className="text-xl font-bold">Navigation</h2>
<button
onClick={() => setIsOpen(false)}
className="p-2 hover:bg-gray-800 rounded-lg transition-colors"
className="rounded-lg p-2 transition-colors hover:bg-gray-800"
aria-label="Close menu"
>
<X size={24} />
</button>
</div>
<nav className="flex-1 p-4 overflow-y-auto">
<nav className="flex-1 overflow-y-auto p-4">
<Link
to="/"
onClick={() => setIsOpen(false)}
className="flex items-center gap-3 p-3 rounded-lg hover:bg-gray-800 transition-colors mb-2"
className="mb-2 flex items-center gap-3 rounded-lg p-3 transition-colors hover:bg-gray-800"
activeProps={{
className:
'flex items-center gap-3 p-3 rounded-lg bg-cyan-600 hover:bg-cyan-700 transition-colors mb-2',
@@ -62,7 +63,7 @@ export default function Header() {
<Link
to="/demo/tanstack-query"
onClick={() => setIsOpen(false)}
className="flex items-center gap-3 p-3 rounded-lg hover:bg-gray-800 transition-colors mb-2"
className="mb-2 flex items-center gap-3 rounded-lg p-3 transition-colors hover:bg-gray-800"
activeProps={{
className:
'flex items-center gap-3 p-3 rounded-lg bg-cyan-600 hover:bg-cyan-700 transition-colors mb-2',

View File

@@ -0,0 +1,84 @@
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
import { Badge } from '@/components/ui/badge'
interface DashboardHeaderProps {
name: string
role: string
department: string
greeting?: string
}
export default function DashboardHeader({
name,
role,
department,
greeting = 'Buenas noches,',
}: DashboardHeaderProps) {
// Generamos la URL de DiceBear dinámicamente con el nombre
const dicebearUrl = `https://api.dicebear.com/9.x/initials/svg?seed=${encodeURIComponent(name)}`
// Calculamos iniciales de respaldo por si falla la imagen
const initials = name
.split(' ')
.map((n) => n[0])
.slice(0, 2)
.join('')
.toUpperCase()
return (
<div className="flex flex-col justify-between gap-4 rounded-xl border p-6 shadow-md md:flex-row md:items-center">
<div className="flex flex-row items-center gap-4">
{/* 1. Avatar de DiceBear usando el componente de Shadcn */}
<Avatar className="border-background h-12 w-12 border-2 shadow-sm">
<AvatarImage src={dicebearUrl} alt={name} />
<AvatarFallback>{initials}</AvatarFallback>
</Avatar>
<div className="flex flex-col">
{/* Saludo con texto secundario */}
<p className="text-muted-foreground text-sm">{greeting}</p>
{/* Nombre destacado */}
<h2 className="text-foreground text-lg font-bold tracking-tight">
{name}
</h2>
<div className="mt-1 flex flex-wrap items-center gap-2">
{/* 2. El "Banner" (Badge) para el puesto */}
<Badge
variant="secondary"
className="rounded-md px-2 py-0 text-xs font-semibold"
>
{role}
</Badge>
{/* Departamento */}
<span className="text-muted-foreground text-xs font-medium">
{department}
</span>
</div>
</div>
</div>
<div className="xs:flex-nowrap flex flex-row flex-wrap gap-6">
<div className="bg-muted flex flex-row items-center gap-3 rounded-lg px-4 py-2">
<div className="bg-primary/10 flex h-10 w-10 items-center justify-center rounded-lg">
Icono
</div>
<div>
<p>4</p>
<p>Planes activos</p>
</div>
</div>
<div className="bg-muted flex flex-row items-center gap-3 rounded-lg px-4 py-2">
<div className="bg-primary/10 flex h-10 w-10 items-center justify-center rounded-sm">
Icono
</div>
<div>
<p>3</p>
<p>Revisiones pendientes</p>
</div>
</div>
</div>
</div>
)
}

View File

@@ -0,0 +1,86 @@
import { ArrowRight, type LucideIcon } from 'lucide-react'
import { Badge } from '@/components/ui/badge'
import { Card, CardContent, CardFooter, CardHeader } from '@/components/ui/card'
import { cn } from '@/lib/utils' // Asegúrate de tener tu utilidad cn
interface PlanEstudiosCardProps {
/** El componente del ícono importado de lucide-react (ej. BookOpen) */
Icono: LucideIcon
nombrePrograma: string
nivel: string
ciclos: string | number // Acepta "8" o "8 semestres"
facultad: string
estado: string
/** Código hex o variable CSS (ej. "#ef4444" o "var(--primary)") */
claseColorEstado?: string
colorFacultad: string
/** Opcional: para manejar el click en la tarjeta */
onClick?: () => void
}
export default function PlanEstudiosCard({
Icono,
nombrePrograma,
nivel,
ciclos,
facultad,
estado,
claseColorEstado = '',
colorFacultad,
onClick,
}: PlanEstudiosCardProps) {
return (
<Card
onClick={onClick}
className={cn(
'group relative flex h-full cursor-pointer flex-col justify-between overflow-hidden border-l-4 transition-all hover:shadow-lg',
)}
// Aplicamos el color de la facultad dinámicamente al borde y un fondo muy sutil
style={{
borderLeftColor: colorFacultad,
backgroundColor: `color-mix(in srgb, ${colorFacultad}, transparent 95%)`, // Truco CSS moderno para fondo tintado
}}
>
<CardHeader className="pb-2">
{/* Ícono con el color de la facultad */}
<div
className="mb-2 w-fit rounded-md p-2"
style={{
backgroundColor: `color-mix(in srgb, ${colorFacultad}, transparent 85%)`,
}}
>
<Icono size={24} style={{ color: colorFacultad }} />
</div>
{/* Título del Programa */}
<h4 className="line-clamp-2 text-lg leading-tight font-bold tracking-tight">
{nombrePrograma}
</h4>
</CardHeader>
<CardContent className="text-muted-foreground space-y-1 pb-4 text-sm">
<p className="text-foreground font-medium">
{nivel} {ciclos}
</p>
<p>{facultad}</p>
</CardContent>
<CardFooter className="bg-background/50 flex items-center justify-between border-t px-6 py-3 backdrop-blur-sm">
<Badge className={`text-sm font-semibold ${claseColorEstado}`}>
{estado}
</Badge>
{/* <span className="text-foreground/80 text-sm font-semibold">
{estado}
</span> */}
{/* Flecha animada */}
<div
className="rounded-full p-1 transition-transform duration-300 group-hover:translate-x-1"
style={{ color: colorFacultad }}
>
<ArrowRight size={20} />
</div>
</CardFooter>
</Card>
)
}

View File

@@ -0,0 +1,51 @@
import * as React from "react"
import * as AvatarPrimitive from "@radix-ui/react-avatar"
import { cn } from "@/lib/utils"
function Avatar({
className,
...props
}: React.ComponentProps<typeof AvatarPrimitive.Root>) {
return (
<AvatarPrimitive.Root
data-slot="avatar"
className={cn(
"relative flex size-8 shrink-0 overflow-hidden rounded-full",
className
)}
{...props}
/>
)
}
function AvatarImage({
className,
...props
}: React.ComponentProps<typeof AvatarPrimitive.Image>) {
return (
<AvatarPrimitive.Image
data-slot="avatar-image"
className={cn("aspect-square size-full", className)}
{...props}
/>
)
}
function AvatarFallback({
className,
...props
}: React.ComponentProps<typeof AvatarPrimitive.Fallback>) {
return (
<AvatarPrimitive.Fallback
data-slot="avatar-fallback"
className={cn(
"bg-muted flex size-full items-center justify-center rounded-full",
className
)}
{...props}
/>
)
}
export { Avatar, AvatarImage, AvatarFallback }

View File

@@ -0,0 +1,46 @@
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 badgeVariants = cva(
"inline-flex items-center justify-center rounded-full border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-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 transition-[color,box-shadow] overflow-hidden",
{
variants: {
variant: {
default:
"border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90",
secondary:
"border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90",
destructive:
"border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
outline:
"text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground",
},
},
defaultVariants: {
variant: "default",
},
}
)
function Badge({
className,
variant,
asChild = false,
...props
}: React.ComponentProps<"span"> &
VariantProps<typeof badgeVariants> & { asChild?: boolean }) {
const Comp = asChild ? Slot : "span"
return (
<Comp
data-slot="badge"
className={cn(badgeVariants({ variant }), className)}
{...props}
/>
)
}
export { Badge, badgeVariants }

View File

@@ -0,0 +1,92 @@
import * as React from "react"
import { cn } from "@/lib/utils"
function Card({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card"
className={cn(
"bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm",
className
)}
{...props}
/>
)
}
function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-header"
className={cn(
"@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-2 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6",
className
)}
{...props}
/>
)
}
function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-title"
className={cn("leading-none font-semibold", className)}
{...props}
/>
)
}
function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-description"
className={cn("text-muted-foreground text-sm", className)}
{...props}
/>
)
}
function CardAction({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-action"
className={cn(
"col-start-2 row-span-2 row-start-1 self-start justify-self-end",
className
)}
{...props}
/>
)
}
function CardContent({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-content"
className={cn("px-6", className)}
{...props}
/>
)
}
function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-footer"
className={cn("flex items-center px-6 [.border-t]:pt-6", className)}
{...props}
/>
)
}
export {
Card,
CardHeader,
CardFooter,
CardTitle,
CardAction,
CardDescription,
CardContent,
}

View File

@@ -9,9 +9,15 @@
// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified.
import { Route as rootRouteImport } from './routes/__root'
import { Route as DashboardRouteImport } from './routes/dashboard'
import { Route as IndexRouteImport } from './routes/index'
import { Route as DemoTanstackQueryRouteImport } from './routes/demo/tanstack-query'
const DashboardRoute = DashboardRouteImport.update({
id: '/dashboard',
path: '/dashboard',
getParentRoute: () => rootRouteImport,
} as any)
const IndexRoute = IndexRouteImport.update({
id: '/',
path: '/',
@@ -25,32 +31,43 @@ const DemoTanstackQueryRoute = DemoTanstackQueryRouteImport.update({
export interface FileRoutesByFullPath {
'/': typeof IndexRoute
'/dashboard': typeof DashboardRoute
'/demo/tanstack-query': typeof DemoTanstackQueryRoute
}
export interface FileRoutesByTo {
'/': typeof IndexRoute
'/dashboard': typeof DashboardRoute
'/demo/tanstack-query': typeof DemoTanstackQueryRoute
}
export interface FileRoutesById {
__root__: typeof rootRouteImport
'/': typeof IndexRoute
'/dashboard': typeof DashboardRoute
'/demo/tanstack-query': typeof DemoTanstackQueryRoute
}
export interface FileRouteTypes {
fileRoutesByFullPath: FileRoutesByFullPath
fullPaths: '/' | '/demo/tanstack-query'
fullPaths: '/' | '/dashboard' | '/demo/tanstack-query'
fileRoutesByTo: FileRoutesByTo
to: '/' | '/demo/tanstack-query'
id: '__root__' | '/' | '/demo/tanstack-query'
to: '/' | '/dashboard' | '/demo/tanstack-query'
id: '__root__' | '/' | '/dashboard' | '/demo/tanstack-query'
fileRoutesById: FileRoutesById
}
export interface RootRouteChildren {
IndexRoute: typeof IndexRoute
DashboardRoute: typeof DashboardRoute
DemoTanstackQueryRoute: typeof DemoTanstackQueryRoute
}
declare module '@tanstack/react-router' {
interface FileRoutesByPath {
'/dashboard': {
id: '/dashboard'
path: '/dashboard'
fullPath: '/dashboard'
preLoaderRoute: typeof DashboardRouteImport
parentRoute: typeof rootRouteImport
}
'/': {
id: '/'
path: '/'
@@ -70,6 +87,7 @@ declare module '@tanstack/react-router' {
const rootRouteChildren: RootRouteChildren = {
IndexRoute: IndexRoute,
DashboardRoute: DashboardRoute,
DemoTanstackQueryRoute: DemoTanstackQueryRoute,
}
export const routeTree = rootRouteImport

190
src/routes/dashboard.tsx Normal file
View File

@@ -0,0 +1,190 @@
import DashboardHeader from '@/components/dashboard/DashboardHeader'
import {
ArrowRight,
BookOpenText,
Laptop,
Stethoscope,
Scale,
Calculator,
FlaskConical,
Activity,
PencilRuler,
ClipboardCheck,
} from 'lucide-react'
import PlanEstudiosCard from '@/components/plan_estudios/PlanEstudiosCard'
import { createFileRoute } from '@tanstack/react-router'
export const Route = createFileRoute('/dashboard')({
component: RouteComponent,
})
function RouteComponent() {
return (
// 1. min-h-screen para asegurar que llene la pantalla verticalmente
// 2. bg-background para asegurar consistencia con el tema
<main className="bg-background min-h-screen w-full">
{/* 1. max-w-7xl: El tope de anchura.
2. w-full: Para que ocupe el 100% hasta llegar al tope.
3. mx-auto: Para centrarse.
4. px-4 md:px-6: Padding RESPONSIVO interno (seguro para móviles y desktop).
5. py-6: Padding vertical (opcional, para separarse del header).
*/}
<div className="mx-auto flex w-full max-w-7xl flex-col gap-4 px-4 py-6 md:px-6 lg:px-8">
<DashboardHeader
name="Dr. Carlos Mendoza"
role="Jefe de Carrera"
department="Facultad de Ingeniería"
/>
<div className="mt-6 grid gap-6 lg:grid-cols-3">
{/* --- Sección de Mis Planes de Estudio --- */}
<div className="flex flex-col gap-4 lg:col-span-2">
{/* --- Título de sección y enlace a página --- */}
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<div className="text-primary">
<BookOpenText className="h-6 w-6" strokeWidth={2} />
</div>
<h3 className="text-foreground text-xl font-bold tracking-tight">
Mis Planes de Estudio
</h3>
</div>
{/* Usamos 'group' para animar la flecha al hacer hover en el texto */}
<a
href="/planes"
className="group text-muted-foreground hover:text-primary flex items-center gap-1.5 text-sm font-medium transition-colors"
>
<span>Ver todos</span>
{/* La flecha se mueve a la derecha al hacer hover en el grupo */}
<ArrowRight className="h-4 w-4 transition-transform duration-300 group-hover:translate-x-1" />
</a>
</div>
<div className="grid gap-4 sm:grid-cols-2">
<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')}
/>
</div>
</div>
{/* --- Sección de Mis Revisiones --- */}
<div className="flex flex-col gap-4">
{/* --- Título de sección y enlace a página --- */}
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<div className="text-primary">
<ClipboardCheck className="h-6 w-6" strokeWidth={2} />
</div>
<h3 className="text-foreground text-xl font-bold tracking-tight">
Mis Revisiones
</h3>
</div>
<a
href="/revisiones"
className="group text-muted-foreground hover:text-primary flex items-center gap-1.5 text-sm font-medium transition-colors"
>
<span>Ver todas</span>
<ArrowRight className="h-4 w-4 transition-transform duration-300 group-hover:translate-x-1" />
</a>
</div>
{/* --- Lista de revisiones (simplificada para este ejemplo) --- */}
<div className="flex flex-col gap-4">
<div className="min-h-20 rounded-lg border p-4 shadow-md">
Revision 1
</div>
<div className="min-h-20 rounded-lg border p-4 shadow-md">
Revision 2
</div>
<div className="min-h-20 rounded-lg border p-4 shadow-md">
Revision 3
</div>
</div>
</div>
</div>
</div>
</main>
)
}