Navegación y Routing
App Router Structure
Layout Principal
// app/layout.tsx - Layout principal
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="es">
<body>
<Providers>
<ChakraProvider theme={theme}>
{children}
</ChakraProvider>
</Providers>
</body>
</html>
);
}
Layout Admin
// app/admin/layout.tsx - Layout admin
export default function AdminLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<Box minH="100vh">
<Sidebar />
<Box ml={{ base: 0, xl: "300px" }}>
<Navbar />
<Box p={4}>
{children}
</Box>
</Box>
</Box>
);
}
Estructura de Rutas
Rutas Públicas
app/
├── page.tsx # / (landing page)
├── auth/
│ ├── sign-in/
│ │ └── page.tsx # /auth/sign-in
│ ├── forgot-password/
│ │ └── page.tsx # /auth/forgot-password
│ ├── reset-password/
│ │ └── page.tsx # /auth/reset-password
│ └── plans/
│ └── page.tsx # /auth/plans
Rutas Protegidas
app/admin/
├── page.tsx # /admin (dashboard)
├── properties/
│ ├── page.tsx # /admin/properties
│ ├── create/
│ │ └── page.tsx # /admin/properties/create
│ └── [id]/
│ ├── page.tsx # /admin/properties/[id]
│ └── edit/
│ └── page.tsx # /admin/properties/[id]/edit
├── users/
│ ├── page.tsx # /admin/users
│ └── [id]/
│ └── page.tsx # /admin/users/[id]
├── contracts/
│ ├── page.tsx # /admin/contracts
│ └── [id]/
│ └── page.tsx # /admin/contracts/[id]
└── settings/
└── page.tsx # /admin/settings
Navegación Dinámica
Sidebar Component
// src/components/sidebar/Sidebar.tsx
const Sidebar = () => {
const menuItems = useSelector(getMenuItems);
const { isActive: hasActiveSubscription } = useSubscription();
// Filtrar menús por permisos
const filteredMenuItems = menuItems.filter((item) => {
if (!item.permissions_required) return true;
const requiredProfiles = item.permissions_required
.split(',')
.map(profile => profile.trim().toLowerCase());
return activeProfiles.some(userProfile =>
requiredProfiles.includes(userProfile.toLowerCase())
);
});
// Convertir a rutas
const routes: IRoute[] = filteredMenuItems.map((item) => ({
name: item.menu_title,
layout: "/admin",
path: item.path,
icon: iconMap[item.icon] || null,
component: () => <div>{item.menu_title}</div>
}));
return <SidebarContent routes={routes} />;
};
Route Guard
// components/RouteGuard.tsx
export const RouteGuard = ({ children }: { children: React.ReactNode }) => {
const { isAuthenticated } = useAuth();
const pathname = usePathname();
useEffect(() => {
if (!isAuthenticated && pathname.startsWith('/admin')) {
redirect('/auth/sign-in');
}
}, [isAuthenticated, pathname]);
if (!isAuthenticated && pathname.startsWith('/admin')) {
return <Spinner />;
}
return <>{children}</>;
};
Programmatic Navigation
useRouter Hook
import { useRouter } from 'next/navigation';
export const PropertyCard = ({ property }: { property: Property }) => {
const router = useRouter();
const handleViewProperty = () => {
router.push(`/admin/properties/${property.id}`);
};
const handleEditProperty = () => {
router.push(`/admin/properties/${property.id}/edit`);
};
return (
<Card>
<CardBody>
<Button onClick={handleViewProperty}>Ver</Button>
<Button onClick={handleEditProperty}>Editar</Button>
</CardBody>
</Card>
);
};
Navigation Helper
// utils/navigation.ts
export const navigationUtils = {
goToProperty: (id: string | number) => `/admin/properties/${id}`,
goToUser: (id: string | number) => `/admin/users/${id}`,
goToContract: (id: string | number) => `/admin/contracts/${id}`,
goToCreateProperty: () => '/admin/properties/create',
goToDashboard: () => '/admin',
goToLogin: () => '/auth/sign-in'
};
URL Parameters
Dynamic Routes
// app/admin/properties/[id]/page.tsx
interface PropertyPageProps {
params: { id: string };
searchParams: { [key: string]: string | string[] | undefined };
}
export default function PropertyPage({ params }: PropertyPageProps) {
const propertyId = params.id;
return (
<div>
<h1>Propiedad {propertyId}</h1>
{/* Contenido de la propiedad */}
</div>
);
}
Query Parameters
import { useSearchParams } from 'next/navigation';
export const PropertiesPage = () => {
const searchParams = useSearchParams();
const filter = searchParams.get('filter') || '';
const page = Number(searchParams.get('page')) || 1;
return (
<div>
{/* Lista de propiedades filtrada */}
</div>
);
};
Breadcrumbs
Breadcrumb Component
// components/Breadcrumb.tsx
export const Breadcrumb = () => {
const pathname = usePathname();
const segments = pathname.split('/').filter(Boolean);
const breadcrumbItems = segments.map((segment, index) => {
const href = '/' + segments.slice(0, index + 1).join('/');
const label = segment.charAt(0).toUpperCase() + segment.slice(1);
return { href, label };
});
return (
<ChakraBreadcrumb>
<BreadcrumbItem>
<BreadcrumbLink href="/admin">Dashboard</BreadcrumbLink>
</BreadcrumbItem>
{breadcrumbItems.map((item, index) => (
<BreadcrumbItem key={index} isCurrentPage={index === breadcrumbItems.length - 1}>
{index === breadcrumbItems.length - 1 ? (
<BreadcrumbText>{item.label}</BreadcrumbText>
) : (
<BreadcrumbLink href={item.href}>{item.label}</BreadcrumbLink>
)}
</BreadcrumbItem>
))}
</ChakraBreadcrumb>
);
};
Navigation State
Active Route Tracking
// hooks/useActiveRoute.ts
export const useActiveRoute = () => {
const pathname = usePathname();
const dispatch = useDispatch();
useEffect(() => {
dispatch(setActiveRoute(pathname));
}, [pathname, dispatch]);
return pathname;
};
Menu State Management
// components/sidebar/SidebarItem.tsx
export const SidebarItem = ({ route }: { route: IRoute }) => {
const pathname = usePathname();
const isActive = pathname === route.layout + route.path;
return (
<Link href={route.layout + route.path}>
<Box
bg={isActive ? 'brand.500' : 'transparent'}
color={isActive ? 'white' : 'gray.600'}
p={3}
borderRadius="md"
_hover={{ bg: isActive ? 'brand.600' : 'gray.100' }}
>
{route.icon && <Icon as={route.icon} mr={3} />}
{route.name}
</Box>
</Link>
);
};
Route Configuration
Route Types
// types/navigation.ts
export interface IRoute {
name: string;
layout: string;
path: string;
icon?: React.ComponentType;
component?: React.ComponentType;
permissions?: string[];
children?: IRoute[];
}
Route Definitions
// config/routes.ts
export const adminRoutes: IRoute[] = [
{
name: 'Dashboard',
layout: '/admin',
path: '',
icon: HomeIcon,
permissions: ['owner', 'tenant', 'admin']
},
{
name: 'Propiedades',
layout: '/admin',
path: '/properties',
icon: BuildingIcon,
permissions: ['owner', 'admin']
},
{
name: 'Usuarios',
layout: '/admin',
path: '/users',
icon: UsersIcon,
permissions: ['admin']
}
];