🧭 Arquitectura de Navegación por Roles - PuntoHack
Fecha: 1 de enero, 2025
Objetivo: Organizar la navegación de múltiples roles sin abrumar la UI
📊 Análisis de la Situación Actual
Roles Existentes:
- PARTICIPANT - Participantes de hackathons
- JUDGE - Jueces que evalúan proyectos
- ORGANIZER - Organizadores de hackathons
- ADMIN - Administradores del sistema
- SPONSOR - Sponsors y organizaciones
Roles Adicionales (dentro de organizaciones):
- OWNER (OrganizationMember) - Dueño de organización
- ADMIN (OrganizationMember) - Admin de organización
- MEMBER (OrganizationMember) - Miembro de organización
Problema Identificado:
- Múltiples roles pueden confundir la navegación
- Un usuario puede tener múltiples roles (ej: SPONSOR + ORGANIZER)
- La navegación actual puede volverse abrumadora
🎯 Solución Propuesta: Sistema de "Workspaces" o "Contextos"
Concepto Principal:
Cada rol tiene su propio "workspace" o contexto de navegación, y el usuario puede cambiar entre ellos fácilmente.
Estructura de Navegación:
/dashboard (Hub central - redirige según rol principal)
├── /participant (Workspace de Participante)
│ ├── /hackathons
│ ├── /my-teams
│ └── /my-submissions
│
├── /judge (Workspace de Juez)
│ ├── /assignments
│ ├── /hackathons
│ └── /evaluations
│
├── /organizer (Workspace de Organizador)
│ ├── /hackathons
│ ├── /participants
│ └── /judges
│
├── /sponsor (Workspace de Sponsor)
│ ├── /organizations
│ ├── /sponsorships
│ ├── /challenges
│ └── /shortlist
│
└── /admin (Workspace de Admin)
├── /users
├── /hackathons
└── /system
🏗️ Implementación Propuesta
1. Componente de Selector de Workspace
Un componente en el navbar que permite cambiar entre workspaces disponibles:
// src/components/workspace-selector.tsx
'use client';
interface Workspace {
id: string;
name: string;
role: Role;
icon: React.ReactNode;
path: string;
badge?: number; // Para notificaciones
}
export function WorkspaceSelector() {
const { profile } = useProfile();
const router = useRouter();
const pathname = usePathname();
// Determinar workspaces disponibles según roles del usuario
const availableWorkspaces = useMemo(() => {
const workspaces: Workspace[] = [];
if (profile.role === Role.PARTICIPANT || profile.role === Role.ADMIN) {
workspaces.push({
id: 'participant',
name: 'Participante',
role: Role.PARTICIPANT,
icon: <UsersIcon />,
path: '/participant',
});
}
if (profile.role === Role.JUDGE || profile.role === Role.ADMIN) {
workspaces.push({
id: 'judge',
name: 'Juez',
role: Role.JUDGE,
icon: <ScaleIcon />,
path: '/judge',
});
}
if (profile.role === Role.ORGANIZER || profile.role === Role.ADMIN) {
workspaces.push({
id: 'organizer',
name: 'Organizador',
role: Role.ORGANIZER,
icon: <CalendarIcon />,
path: '/organizer',
});
}
if (profile.role === Role.SPONSOR || profile.role === Role.ADMIN) {
workspaces.push({
id: 'sponsor',
name: 'Sponsor',
role: Role.SPONSOR,
icon: <BuildingIcon />,
path: '/sponsor',
});
}
if (profile.role === Role.ADMIN) {
workspaces.push({
id: 'admin',
name: 'Administrador',
role: Role.ADMIN,
icon: <ShieldIcon />,
path: '/admin',
});
}
return workspaces;
}, [profile]);
// Determinar workspace actual basado en pathname
const currentWorkspace = useMemo(() => {
if (pathname.startsWith('/participant')) return 'participant';
if (pathname.startsWith('/judge')) return 'judge';
if (pathname.startsWith('/organizer')) return 'organizer';
if (pathname.startsWith('/sponsor')) return 'sponsor';
if (pathname.startsWith('/admin')) return 'admin';
return null;
}, [pathname]);
return (
<Menu as="div" className="relative">
<Menu.Button className="flex items-center space-x-2 px-3 py-2 rounded-lg hover:bg-gray-100">
<span className="font-medium">{currentWorkspace?.name || 'Seleccionar'}</span>
<ChevronDownIcon className="w-4 h-4" />
</Menu.Button>
<Menu.Items className="absolute left-0 mt-2 w-56 bg-white rounded-lg shadow-lg border border-gray-200">
{availableWorkspaces.map((workspace) => (
<Menu.Item key={workspace.id}>
<Link
href={workspace.path}
className={`flex items-center space-x-3 px-4 py-3 hover:bg-gray-50 ${
currentWorkspace === workspace.id ? 'bg-blue-50' : ''
}`}
>
{workspace.icon}
<span>{workspace.name}</span>
{workspace.badge && (
<span className="ml-auto bg-red-500 text-white text-xs rounded-full px-2 py-1">
{workspace.badge}
</span>
)}
</Link>
</Menu.Item>
))}
</Menu.Items>
</Menu>
);
}
2. Sidebar Dinámico por Workspace
Cada workspace tiene su propio sidebar con navegación específica:
// src/components/sidebars/participant-sidebar.tsx
export function ParticipantSidebar() {
return (
<nav className="space-y-1">
<NavLink href="/participant/hackathons" icon={<CalendarIcon />}>
Hackathons
</NavLink>
<NavLink href="/participant/my-teams" icon={<UsersIcon />}>
Mis Equipos
</NavLink>
<NavLink href="/participant/my-submissions" icon={<DocumentIcon />}>
Mis Proyectos
</NavLink>
</nav>
);
}
// src/components/sidebars/sponsor-sidebar.tsx
export function SponsorSidebar() {
const { organizations } = useOrganizations();
return (
<nav className="space-y-1">
<NavLink href="/sponsor/organizations" icon={<BuildingIcon />}>
Organizaciones
</NavLink>
<NavLink href="/sponsor/sponsorships" icon={<HandshakeIcon />}>
Sponsorships
</NavLink>
<NavLink href="/sponsor/challenges" icon={<TrophyIcon />}>
Challenges
</NavLink>
<NavLink href="/sponsor/shortlist" icon={<StarIcon />}>
Shortlist
</NavLink>
{/* Organizaciones del usuario */}
{organizations.length > 0 && (
<div className="mt-6">
<h3 className="px-3 text-xs font-semibold text-gray-500 uppercase">
Mis Organizaciones
</h3>
<div className="mt-2 space-y-1">
{organizations.map((org) => (
<NavLink
key={org.id}
href={`/sponsor/organizations/${org.id}`}
icon={<BuildingOfficeIcon />}
>
{org.name}
</NavLink>
))}
</div>
</div>
)}
</nav>
);
}
3. Layout por Workspace
Cada workspace tiene su propio layout:
// src/app/sponsor/layout.tsx
export default function SponsorLayout({ children }: { children: React.ReactNode }) {
return (
<div className="flex h-screen bg-gray-50">
{/* Sidebar */}
<aside className="w-64 bg-white border-r border-gray-200">
<div className="p-4">
<h2 className="text-lg font-semibold">Sponsor</h2>
</div>
<SponsorSidebar />
</aside>
{/* Contenido principal */}
<main className="flex-1 overflow-y-auto">
{children}
</main>
</div>
);
}
4. Dashboard Central Inteligente
El dashboard principal detecta el rol y muestra solo lo relevante:
// src/app/dashboard/page.tsx
export default function DashboardPage() {
const { profile } = useProfile();
// Determinar workspace por defecto según rol principal
const defaultWorkspace = useMemo(() => {
if (profile.role === Role.ADMIN) return '/admin';
if (profile.role === Role.SPONSOR) return '/sponsor';
if (profile.role === Role.ORGANIZER) return '/organizer';
if (profile.role === Role.JUDGE) return '/judge';
return '/participant';
}, [profile.role]);
// Redirigir al workspace por defecto
useEffect(() => {
router.push(defaultWorkspace);
}, [defaultWorkspace]);
// O mostrar un dashboard con resumen de todos los workspaces
return (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{availableWorkspaces.map((workspace) => (
<WorkspaceCard key={workspace.id} workspace={workspace} />
))}
</div>
);
}
🎨 Patrones de Diseño Recomendados
1. Separación Visual Clara
- Cada workspace tiene su propio color/theme
- Sidebar específico por workspace
- Breadcrumbs que muestran el contexto actual
2. Navegación Jerárquica
Navbar (Global)
├── Workspace Selector (Cambiar contexto)
├── Notificaciones (Global)
└── Perfil (Global)
Sidebar (Por workspace)
└── Navegación específica del workspace
Contenido Principal
└── Páginas del workspace
3. Breadcrumbs Contextuales
// Muestra el contexto actual
Sponsor > Organizaciones > Tech Corp > Challenges
4. Atajos Rápidos
Un componente de "Quick Actions" en el dashboard que muestra acciones rápidas según el workspace actual:
<QuickActions>
{currentWorkspace === 'sponsor' && (
<>
<Action href="/sponsor/organizations/create">Crear Organización</Action>
<Action href="/sponsor/sponsorships/create">Crear Sponsorship</Action>
</>
)}
</QuickActions>
📁 Estructura de Carpetas Propuesta
src/app/
├── dashboard/ # Hub central
├── participant/ # Workspace de Participante
│ ├── hackathons/
│ ├── my-teams/
│ └── my-submissions/
├── judge/ # Workspace de Juez
│ ├── assignments/
│ ├── hackathons/
│ └── evaluations/
├── organizer/ # Workspace de Organizador
│ ├── hackathons/
│ ├── participants/
│ └── judges/
├── sponsor/ # Workspace de Sponsor
│ ├── organizations/
│ ├── sponsorships/
│ ├── challenges/
│ └── shortlist/
└── admin/ # Workspace de Admin
├── users/
├── hackathons/
└── system/
🔄 Manejo de Múltiples Roles
Usuario con Múltiples Roles:
Ejemplo: Un usuario es SPONSOR + ORGANIZER
- Selector de Workspace muestra ambos workspaces disponibles
- El usuario puede cambiar entre ellos fácilmente
- Cada workspace mantiene su estado independiente
- Las notificaciones se agrupan por workspace
Roles Anidados (OrganizationMember):
Para roles dentro de organizaciones (OWNER, ADMIN, MEMBER):
// Dentro del workspace de Sponsor
/sponsor/organizations/[orgId]/
├── dashboard/ # Solo OWNER/ADMIN
├── members/ # Solo OWNER/ADMIN
├── sponsorships/ # Todos los miembros
└── challenges/ # Todos los miembros
La validación de roles anidados se hace en el componente:
// src/app/sponsor/organizations/[orgId]/members/page.tsx
export default function OrganizationMembersPage() {
const { organization, memberRole } = useOrganizationContext();
// Solo OWNER y ADMIN pueden ver esta página
if (memberRole !== 'OWNER' && memberRole !== 'ADMIN') {
return <Unauthorized />;
}
// ...
}
✅ Ventajas de Esta Arquitectura
- Separación Clara: Cada rol tiene su espacio dedicado
- Escalable: Fácil agregar nuevos roles/workspaces
- No Abruma: El usuario solo ve lo relevante para su contexto actual
- Flexible: Usuarios con múltiples roles pueden cambiar fácilmente
- Mantenible: Código organizado por workspace
- UX Mejorada: Navegación intuitiva y contextual
🚀 Plan de Implementación
Fase 1: Refactorizar Estructura Actual
- Crear carpetas de workspaces (
/participant,/sponsor, etc.) - Mover páginas existentes a sus workspaces correspondientes
- Crear layouts por workspace
Fase 2: Componentes de Navegación
- Crear
WorkspaceSelector - Crear sidebars por workspace
- Actualizar navbar para incluir selector
Fase 3: Dashboard Central
- Crear dashboard inteligente que redirige o muestra resumen
- Implementar
QuickActionspor workspace
Fase 4: Migración Gradual
- Migrar páginas existentes a nuevos workspaces
- Mantener compatibilidad con rutas antiguas (redirects)
- Actualizar todos los links internos
📝 Notas Importantes
- Rutas Públicas: Mantener
/hackathonscomo ruta pública (no dentro de workspace) - Compatibilidad: Agregar redirects de rutas antiguas a nuevas
- Testing: Asegurar que todos los tests sigan funcionando
- Documentación: Actualizar documentación de rutas
Conclusión: Esta arquitectura permite escalar a muchos roles sin abrumar la UI, manteniendo cada contexto separado y organizado.