Saltar al contenido principal

🧭 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:

  1. PARTICIPANT - Participantes de hackathons
  2. JUDGE - Jueces que evalúan proyectos
  3. ORGANIZER - Organizadores de hackathons
  4. ADMIN - Administradores del sistema
  5. 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

  1. Selector de Workspace muestra ambos workspaces disponibles
  2. El usuario puede cambiar entre ellos fácilmente
  3. Cada workspace mantiene su estado independiente
  4. 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

  1. Separación Clara: Cada rol tiene su espacio dedicado
  2. Escalable: Fácil agregar nuevos roles/workspaces
  3. No Abruma: El usuario solo ve lo relevante para su contexto actual
  4. Flexible: Usuarios con múltiples roles pueden cambiar fácilmente
  5. Mantenible: Código organizado por workspace
  6. UX Mejorada: Navegación intuitiva y contextual

🚀 Plan de Implementación

Fase 1: Refactorizar Estructura Actual

  1. Crear carpetas de workspaces (/participant, /sponsor, etc.)
  2. Mover páginas existentes a sus workspaces correspondientes
  3. Crear layouts por workspace

Fase 2: Componentes de Navegación

  1. Crear WorkspaceSelector
  2. Crear sidebars por workspace
  3. Actualizar navbar para incluir selector

Fase 3: Dashboard Central

  1. Crear dashboard inteligente que redirige o muestra resumen
  2. Implementar QuickActions por workspace

Fase 4: Migración Gradual

  1. Migrar páginas existentes a nuevos workspaces
  2. Mantener compatibilidad con rutas antiguas (redirects)
  3. Actualizar todos los links internos

📝 Notas Importantes

  • Rutas Públicas: Mantener /hackathons como 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.