Fallback elegante de vista no encontrada
close #44: Se creó la NotFoundPage y se utiliza en __root con el notFoundComponent. Se agregó la lógica del loader tanto de plan de estudios como de asignaturas. Se agregó el NotFoundComponent para el detalle de plan de estudios y el de asignaturas
This commit is contained in:
@@ -118,7 +118,10 @@ export function WizardControls({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex grow items-center justify-between">
|
||||||
|
<Button variant="secondary" onClick={onPrev} disabled={disablePrev}>
|
||||||
|
Anterior
|
||||||
|
</Button>
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
{errorMessage && (
|
{errorMessage && (
|
||||||
<span className="text-destructive text-sm font-medium">
|
<span className="text-destructive text-sm font-medium">
|
||||||
@@ -126,20 +129,15 @@ export function WizardControls({
|
|||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex gap-4">
|
{isLastStep ? (
|
||||||
<Button variant="secondary" onClick={onPrev} disabled={disablePrev}>
|
<Button onClick={handleCreate} disabled={disableCreate}>
|
||||||
Anterior
|
Crear plan
|
||||||
</Button>
|
</Button>
|
||||||
{isLastStep ? (
|
) : (
|
||||||
<Button onClick={handleCreate} disabled={disableCreate}>
|
<Button onClick={onNext} disabled={disableNext}>
|
||||||
Crear plan
|
Siguiente
|
||||||
</Button>
|
</Button>
|
||||||
) : (
|
)}
|
||||||
<Button onClick={onNext} disabled={disableNext}>
|
|
||||||
Siguiente
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
44
src/components/ui/NotFoundPage.tsx
Normal file
44
src/components/ui/NotFoundPage.tsx
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
import { Link, useRouter } from '@tanstack/react-router'
|
||||||
|
import { FileQuestion, Home, ArrowLeft } from 'lucide-react'
|
||||||
|
|
||||||
|
import { Button } from './button'
|
||||||
|
|
||||||
|
interface NotFoundPageProps {
|
||||||
|
title?: string
|
||||||
|
message?: string
|
||||||
|
children?: React.ReactNode
|
||||||
|
}
|
||||||
|
|
||||||
|
export function NotFoundPage({
|
||||||
|
title = 'Página no encontrada',
|
||||||
|
message = 'Lo sentimos, no pudimos encontrar lo que buscabas. Es posible que la página haya sido movida o eliminada.',
|
||||||
|
children,
|
||||||
|
}: NotFoundPageProps) {
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex min-h-[60vh] flex-col items-center justify-center p-4 text-center">
|
||||||
|
<div className="bg-muted mb-6 rounded-full p-6">
|
||||||
|
<FileQuestion className="text-muted-foreground h-12 w-12" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h1 className="mb-2 text-3xl font-bold tracking-tight">{title}</h1>
|
||||||
|
<p className="text-muted-foreground mb-8 max-w-125">{message}</p>
|
||||||
|
|
||||||
|
<div className="flex flex-col gap-2 sm:flex-row">
|
||||||
|
<Button variant="outline" onClick={() => router.history.back()}>
|
||||||
|
<ArrowLeft className="mr-2 h-4 w-4" />
|
||||||
|
Regresar
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button asChild>
|
||||||
|
<Link to="/">
|
||||||
|
<Home className="mr-2 h-4 w-4" />
|
||||||
|
Ir al inicio
|
||||||
|
</Link>
|
||||||
|
</Button>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -123,6 +123,8 @@ export async function plans_list(
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function plans_get(planId: UUID): Promise<PlanEstudio> {
|
export async function plans_get(planId: UUID): Promise<PlanEstudio> {
|
||||||
|
console.log('plans_get')
|
||||||
|
|
||||||
const supabase = supabaseBrowser()
|
const supabase = supabaseBrowser()
|
||||||
|
|
||||||
const { data, error } = await supabase
|
const { data, error } = await supabase
|
||||||
@@ -350,7 +352,7 @@ export async function plans_update_fields(
|
|||||||
patch: PlansUpdateFieldsPatch,
|
patch: PlansUpdateFieldsPatch,
|
||||||
): Promise<PlanEstudio> {
|
): Promise<PlanEstudio> {
|
||||||
const supabase = supabaseBrowser()
|
const supabase = supabaseBrowser()
|
||||||
|
|
||||||
const { data, error } = await supabase
|
const { data, error } = await supabase
|
||||||
.from('planes_estudio')
|
.from('planes_estudio')
|
||||||
.update(patch)
|
.update(patch)
|
||||||
|
|||||||
@@ -53,7 +53,10 @@ export function usePlanes(filters: PlanListFilters) {
|
|||||||
export function usePlan(planId: UUID | null | undefined) {
|
export function usePlan(planId: UUID | null | undefined) {
|
||||||
return useQuery({
|
return useQuery({
|
||||||
queryKey: planId ? qk.plan(planId) : ['planes', 'detail', null],
|
queryKey: planId ? qk.plan(planId) : ['planes', 'detail', null],
|
||||||
queryFn: () => plans_get(planId as UUID),
|
queryFn: () => {
|
||||||
|
console.log('usePlan')
|
||||||
|
return plans_get(planId as UUID)
|
||||||
|
},
|
||||||
enabled: Boolean(planId),
|
enabled: Boolean(planId),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -169,10 +169,6 @@ export interface FileRoutesByTo {
|
|||||||
'/login': typeof LoginRoute
|
'/login': typeof LoginRoute
|
||||||
'/planes': typeof PlanesListaRouteRouteWithChildren
|
'/planes': typeof PlanesListaRouteRouteWithChildren
|
||||||
'/demo/tanstack-query': typeof DemoTanstackQueryRoute
|
'/demo/tanstack-query': typeof DemoTanstackQueryRoute
|
||||||
<<<<<<< HEAD
|
|
||||||
'/planes/PlanesListRoute': typeof PlanesPlanesListRouteRoute
|
|
||||||
=======
|
|
||||||
>>>>>>> 4950f7efbf664bbd31ac8a673fe594af5baf07f6
|
|
||||||
'/planes/$planId': typeof PlanesPlanIdIndexRoute
|
'/planes/$planId': typeof PlanesPlanIdIndexRoute
|
||||||
'/planes/nuevo': typeof PlanesListaNuevoRoute
|
'/planes/nuevo': typeof PlanesListaNuevoRoute
|
||||||
'/planes/$planId/asignaturas/$asignaturaId': typeof PlanesPlanIdAsignaturasAsignaturaIdRouteRoute
|
'/planes/$planId/asignaturas/$asignaturaId': typeof PlanesPlanIdAsignaturasAsignaturaIdRouteRoute
|
||||||
@@ -325,15 +321,7 @@ declare module '@tanstack/react-router' {
|
|||||||
'/planes/$planId/': {
|
'/planes/$planId/': {
|
||||||
id: '/planes/$planId/'
|
id: '/planes/$planId/'
|
||||||
path: '/planes/$planId'
|
path: '/planes/$planId'
|
||||||
<<<<<<< HEAD
|
|
||||||
<<<<<<< HEAD
|
|
||||||
fullPath: '/planes/$planId/'
|
fullPath: '/planes/$planId/'
|
||||||
=======
|
|
||||||
fullPath: '/planes/$planId'
|
|
||||||
>>>>>>> 4950f7efbf664bbd31ac8a673fe594af5baf07f6
|
|
||||||
=======
|
|
||||||
fullPath: '/planes/$planId/'
|
|
||||||
>>>>>>> cbe4e54 (Se cierran incidencias #10, #21, #24, #25; se añade generación manual de planes)
|
|
||||||
preLoaderRoute: typeof PlanesPlanIdIndexRouteImport
|
preLoaderRoute: typeof PlanesPlanIdIndexRouteImport
|
||||||
parentRoute: typeof rootRouteImport
|
parentRoute: typeof rootRouteImport
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ import TanStackQueryDevtools from '../integrations/tanstack-query/devtools'
|
|||||||
|
|
||||||
import type { QueryClient } from '@tanstack/react-query'
|
import type { QueryClient } from '@tanstack/react-query'
|
||||||
|
|
||||||
|
import { NotFoundPage } from '@/components/ui/NotFoundPage'
|
||||||
|
|
||||||
interface MyRouterContext {
|
interface MyRouterContext {
|
||||||
queryClient: QueryClient
|
queryClient: QueryClient
|
||||||
}
|
}
|
||||||
@@ -31,6 +33,8 @@ export const Route = createRootRouteWithContext<MyRouterContext>()({
|
|||||||
</>
|
</>
|
||||||
),
|
),
|
||||||
|
|
||||||
|
notFoundComponent: () => <NotFoundPage />,
|
||||||
|
|
||||||
errorComponent: ({ error, reset }) => {
|
errorComponent: ({ error, reset }) => {
|
||||||
return (
|
return (
|
||||||
<div className="flex min-h-[50vh] flex-col items-center justify-center space-y-4 p-6 text-center">
|
<div className="flex min-h-[50vh] flex-col items-center justify-center space-y-4 p-6 text-center">
|
||||||
|
|||||||
@@ -160,7 +160,7 @@ function DatosGeneralesPage() {
|
|||||||
<Textarea
|
<Textarea
|
||||||
value={editValue}
|
value={editValue}
|
||||||
onChange={(e) => setEditValue(e.target.value)}
|
onChange={(e) => setEditValue(e.target.value)}
|
||||||
className="min-h-[120px]"
|
className="min-h-30"
|
||||||
/>
|
/>
|
||||||
<div className="flex justify-end gap-2">
|
<div className="flex justify-end gap-2">
|
||||||
<Button
|
<Button
|
||||||
@@ -180,7 +180,7 @@ function DatosGeneralesPage() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="min-h-[100px]">
|
<div className="min-h-25">
|
||||||
{campo.value ? (
|
{campo.value ? (
|
||||||
<div className="text-sm leading-relaxed text-slate-600">
|
<div className="text-sm leading-relaxed text-slate-600">
|
||||||
{campo.tipo === 'lista' ? (
|
{campo.tipo === 'lista' ? (
|
||||||
|
|||||||
@@ -139,7 +139,7 @@ function MateriasPage() {
|
|||||||
|
|
||||||
{/* Barra de Filtros Avanzada */}
|
{/* Barra de Filtros Avanzada */}
|
||||||
<div className="flex flex-wrap items-center gap-3 rounded-xl border bg-slate-50 p-4">
|
<div className="flex flex-wrap items-center gap-3 rounded-xl border bg-slate-50 p-4">
|
||||||
<div className="relative min-w-[240px] flex-1">
|
<div className="relative min-w-60 flex-1">
|
||||||
<Search className="text-muted-foreground absolute top-1/2 left-3 h-4 w-4 -translate-y-1/2" />
|
<Search className="text-muted-foreground absolute top-1/2 left-3 h-4 w-4 -translate-y-1/2" />
|
||||||
<Input
|
<Input
|
||||||
placeholder="Buscar por nombre o clave..."
|
placeholder="Buscar por nombre o clave..."
|
||||||
@@ -153,7 +153,7 @@ function MateriasPage() {
|
|||||||
<Filter className="text-muted-foreground mr-1 h-4 w-4" />
|
<Filter className="text-muted-foreground mr-1 h-4 w-4" />
|
||||||
|
|
||||||
<Select value={filterTipo} onValueChange={setFilterTipo}>
|
<Select value={filterTipo} onValueChange={setFilterTipo}>
|
||||||
<SelectTrigger className="w-[140px] bg-white">
|
<SelectTrigger className="w-35 bg-white">
|
||||||
<SelectValue placeholder="Tipo" />
|
<SelectValue placeholder="Tipo" />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
@@ -164,7 +164,7 @@ function MateriasPage() {
|
|||||||
</Select>
|
</Select>
|
||||||
|
|
||||||
<Select value={filterEstado} onValueChange={setFilterEstado}>
|
<Select value={filterEstado} onValueChange={setFilterEstado}>
|
||||||
<SelectTrigger className="w-[140px] bg-white">
|
<SelectTrigger className="w-35 bg-white">
|
||||||
<SelectValue placeholder="Estado" />
|
<SelectValue placeholder="Estado" />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
@@ -176,7 +176,7 @@ function MateriasPage() {
|
|||||||
</Select>
|
</Select>
|
||||||
|
|
||||||
<Select value={filterLinea} onValueChange={setFilterLinea}>
|
<Select value={filterLinea} onValueChange={setFilterLinea}>
|
||||||
<SelectTrigger className="w-[180px] bg-white">
|
<SelectTrigger className="w-45 bg-white">
|
||||||
<SelectValue placeholder="Línea" />
|
<SelectValue placeholder="Línea" />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
@@ -196,14 +196,14 @@ function MateriasPage() {
|
|||||||
<Table>
|
<Table>
|
||||||
<TableHeader>
|
<TableHeader>
|
||||||
<TableRow className="bg-slate-50/50">
|
<TableRow className="bg-slate-50/50">
|
||||||
<TableHead className="w-[120px]">Clave</TableHead>
|
<TableHead className="w-30">Clave</TableHead>
|
||||||
<TableHead>Nombre</TableHead>
|
<TableHead>Nombre</TableHead>
|
||||||
<TableHead className="text-center">Créditos</TableHead>
|
<TableHead className="text-center">Créditos</TableHead>
|
||||||
<TableHead className="text-center">Ciclo</TableHead>
|
<TableHead className="text-center">Ciclo</TableHead>
|
||||||
<TableHead>Línea Curricular</TableHead>
|
<TableHead>Línea Curricular</TableHead>
|
||||||
<TableHead>Tipo</TableHead>
|
<TableHead>Tipo</TableHead>
|
||||||
<TableHead>Estado</TableHead>
|
<TableHead>Estado</TableHead>
|
||||||
<TableHead className="w-[50px]"></TableHead>
|
<TableHead className="w-12.5"></TableHead>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { createFileRoute, Outlet, Link } from '@tanstack/react-router'
|
import { createFileRoute, Outlet, Link, notFound } from '@tanstack/react-router'
|
||||||
import {
|
import {
|
||||||
ChevronLeft,
|
ChevronLeft,
|
||||||
GraduationCap,
|
GraduationCap,
|
||||||
@@ -17,10 +17,37 @@ import {
|
|||||||
DropdownMenuItem,
|
DropdownMenuItem,
|
||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
} from '@/components/ui/dropdown-menu'
|
} from '@/components/ui/dropdown-menu'
|
||||||
|
import { NotFoundPage } from '@/components/ui/NotFoundPage'
|
||||||
import { Skeleton } from '@/components/ui/skeleton'
|
import { Skeleton } from '@/components/ui/skeleton'
|
||||||
|
import { plans_get } from '@/data/api/plans.api'
|
||||||
import { usePlan } from '@/data/hooks/usePlans'
|
import { usePlan } from '@/data/hooks/usePlans'
|
||||||
|
import { qk } from '@/data/query/keys'
|
||||||
|
|
||||||
export const Route = createFileRoute('/planes/$planId/_detalle')({
|
export const Route = createFileRoute('/planes/$planId/_detalle')({
|
||||||
|
loader: async ({ context: { queryClient }, params: { planId } }) => {
|
||||||
|
try {
|
||||||
|
console.log('loader')
|
||||||
|
|
||||||
|
await queryClient.ensureQueryData({
|
||||||
|
queryKey: qk.plan(planId),
|
||||||
|
queryFn: () => plans_get(planId),
|
||||||
|
})
|
||||||
|
} catch (e: any) {
|
||||||
|
// PGRST116: The result contains 0 rows
|
||||||
|
if (e?.code === 'PGRST116') {
|
||||||
|
throw notFound()
|
||||||
|
}
|
||||||
|
throw e
|
||||||
|
}
|
||||||
|
},
|
||||||
|
notFoundComponent: () => {
|
||||||
|
return (
|
||||||
|
<NotFoundPage
|
||||||
|
title="Plan de Estudios no encontrado"
|
||||||
|
message="El plan de estudios que intentas consultar no existe o no tienes permisos para verlo."
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
},
|
||||||
component: RouteComponent,
|
component: RouteComponent,
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -86,11 +113,11 @@ function RouteComponent() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mx-auto max-w-[1600px] space-y-8 p-8">
|
<div className="mx-auto max-w-400 space-y-8 p-8">
|
||||||
{/* Header del Plan */}
|
{/* Header del Plan */}
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
/* ===== SKELETON ===== */
|
/* ===== SKELETON ===== */
|
||||||
<div className="mx-auto max-w-[1600px] p-8">
|
<div className="mx-auto max-w-400 p-8">
|
||||||
<div className="grid grid-cols-1 gap-6 lg:grid-cols-2">
|
<div className="grid grid-cols-1 gap-6 lg:grid-cols-2">
|
||||||
{Array.from({ length: 6 }).map((_, i) => (
|
{Array.from({ length: 6 }).map((_, i) => (
|
||||||
<DatosGeneralesSkeleton key={i} />
|
<DatosGeneralesSkeleton key={i} />
|
||||||
@@ -234,7 +261,7 @@ function InfoCard({
|
|||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={`flex h-[72px] w-full items-center gap-4 rounded-xl border border-slate-200/60 bg-slate-50/50 p-4 shadow-sm transition-all ${
|
className={`flex h-18 w-full items-center gap-4 rounded-xl border border-slate-200/60 bg-slate-50/50 p-4 shadow-sm transition-all ${
|
||||||
isEditable
|
isEditable
|
||||||
? 'cursor-pointer hover:border-teal-200 hover:bg-white focus:outline-none focus-visible:ring-2 focus-visible:ring-teal-500/40'
|
? 'cursor-pointer hover:border-teal-200 hover:bg-white focus:outline-none focus-visible:ring-2 focus-visible:ring-teal-500/40'
|
||||||
: ''
|
: ''
|
||||||
|
|||||||
@@ -1,6 +1,31 @@
|
|||||||
import { createFileRoute, Outlet } from '@tanstack/react-router'
|
import { createFileRoute, Outlet, notFound } from '@tanstack/react-router'
|
||||||
|
|
||||||
|
import { NotFoundPage } from '@/components/ui/NotFoundPage'
|
||||||
|
import { plans_get } from '@/data/api/plans.api'
|
||||||
|
import { qk } from '@/data/query/keys'
|
||||||
|
|
||||||
export const Route = createFileRoute('/planes/$planId/asignaturas')({
|
export const Route = createFileRoute('/planes/$planId/asignaturas')({
|
||||||
|
loader: async ({ context: { queryClient }, params: { planId } }) => {
|
||||||
|
try {
|
||||||
|
await queryClient.ensureQueryData({
|
||||||
|
queryKey: qk.plan(planId),
|
||||||
|
queryFn: () => plans_get(planId),
|
||||||
|
})
|
||||||
|
} catch (e: any) {
|
||||||
|
if (e?.code === 'PGRST116') {
|
||||||
|
throw notFound()
|
||||||
|
}
|
||||||
|
throw e
|
||||||
|
}
|
||||||
|
},
|
||||||
|
notFoundComponent: () => {
|
||||||
|
return (
|
||||||
|
<NotFoundPage
|
||||||
|
title="Plan de Estudios no encontrado"
|
||||||
|
message="El plan de estudios que intentas consultar no existe o no tienes permisos para verlo."
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
},
|
||||||
component: AsignaturasLayout,
|
component: AsignaturasLayout,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user