266e33556d
Stopgap staging multi-user sur wallet partagé (cf. brief_2026-06-15_shared-wallet-shim). Distribution / import du wallet : - AccessGateScreen : barrière d'accès ON PAR DÉFAUT (désactivable via globalThis.__FESTIPOD_ACCESS_GATE_DISABLED__ pour tests/dev). Fournit le FICHIER .ngw + le mot de passe + un guide en 3 étapes (import assisté sur nextgraph.eu — le broker hébergé n'autorise pas l'import inline pendant l'auth web-app). - sharedWallet.ts + build.ts : fichier copié en /shared-wallet.ngw, mot de passe gravé. - Ancien LoginScreen (/login) retiré ; atterrissage post-login -> /home. - NextGraphContext : dé-piégeage de l'état "connecting" au retour (pageshow/bfcache). Couche multistore stopgap : storeRegistry, isolation, AccountContext, FestipodDataContext. Tests e2e multi-navigateur : - browserPool + world.openBrowser : contextes frais isolés, 2 axes orthogonaux (nb de navigateurs × modèle de wallet own/shared). - @humain : parcours humain complet (télécharge -> importe le fichier sur nextgraph.eu -> Entrer -> pseudo -> accueil). - Bypass de la barrière pour @e2e via context.addInitScript. - Convention @wip exclue via cucumber.json. Docs (concepts) : nextgraph-platform (knowledge_broker-import-constraint, decision_2026-06-17_assisted-wallet-import), bdd-testing (knowledge_multibrowser-harness). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
157 lines
5.1 KiB
TypeScript
157 lines
5.1 KiB
TypeScript
import React, { createContext, useContext, useState, useEffect, useCallback } from 'react';
|
|
|
|
// ============================================================================
|
|
// Route types
|
|
// ============================================================================
|
|
|
|
type Route =
|
|
| { page: 'welcome' }
|
|
| { page: 'home' }
|
|
| { page: 'events' }
|
|
| { page: 'create-event' }
|
|
| { page: 'event-detail'; eventId: string }
|
|
| { page: 'update-event'; eventId: string }
|
|
| { page: 'invite'; eventId: string }
|
|
| { page: 'participants'; eventId: string }
|
|
| { page: 'meeting-points'; eventId: string }
|
|
| { page: 'profile' }
|
|
| { page: 'edit-profile' }
|
|
| { page: 'friends' }
|
|
| { page: 'share-profile' }
|
|
| { page: 'connect' }
|
|
| { page: 'user-profile'; userId: string }
|
|
| { page: 'settings' };
|
|
|
|
export type { Route };
|
|
|
|
export interface RouteParams {
|
|
eventId?: string;
|
|
userId?: string;
|
|
}
|
|
|
|
// ============================================================================
|
|
// Path parsing & generation
|
|
// ============================================================================
|
|
|
|
function parsePath(pathname: string): Route {
|
|
const path = pathname.replace(/\/+$/, '') || '/';
|
|
|
|
if (path === '/' || path === '') return { page: 'welcome' };
|
|
if (path === '/home') return { page: 'home' };
|
|
if (path === '/events') return { page: 'events' };
|
|
if (path === '/events/new') return { page: 'create-event' };
|
|
if (path === '/settings') return { page: 'settings' };
|
|
if (path === '/profile') return { page: 'profile' };
|
|
if (path === '/profile/edit') return { page: 'edit-profile' };
|
|
if (path === '/profile/friends') return { page: 'friends' };
|
|
if (path === '/profile/share') return { page: 'share-profile' };
|
|
if (path === '/profile/connect') return { page: 'connect' };
|
|
|
|
// /events/:id/...
|
|
const eventMatch = path.match(/^\/events\/([^/]+)(?:\/(.+))?$/);
|
|
if (eventMatch) {
|
|
const eventId = eventMatch[1]!;
|
|
const sub = eventMatch[2];
|
|
if (!sub) return { page: 'event-detail', eventId };
|
|
if (sub === 'edit') return { page: 'update-event', eventId };
|
|
if (sub === 'invite') return { page: 'invite', eventId };
|
|
if (sub === 'participants') return { page: 'participants', eventId };
|
|
if (sub === 'meeting-points') return { page: 'meeting-points', eventId };
|
|
}
|
|
|
|
// /users/:id
|
|
const userMatch = path.match(/^\/users\/([^/]+)$/);
|
|
if (userMatch) {
|
|
return { page: 'user-profile', userId: userMatch[1]! };
|
|
}
|
|
|
|
return { page: 'welcome' };
|
|
}
|
|
|
|
export function routeToPath(route: Route): string {
|
|
switch (route.page) {
|
|
case 'welcome': return '/';
|
|
case 'home': return '/home';
|
|
case 'events': return '/events';
|
|
case 'create-event': return '/events/new';
|
|
case 'event-detail': return `/events/${route.eventId}`;
|
|
case 'update-event': return `/events/${route.eventId}/edit`;
|
|
case 'invite': return `/events/${route.eventId}/invite`;
|
|
case 'participants': return `/events/${route.eventId}/participants`;
|
|
case 'meeting-points': return `/events/${route.eventId}/meeting-points`;
|
|
case 'profile': return '/profile';
|
|
case 'edit-profile': return '/profile/edit';
|
|
case 'friends': return '/profile/friends';
|
|
case 'share-profile': return '/profile/share';
|
|
case 'connect': return '/profile/connect';
|
|
case 'user-profile': return `/users/${route.userId}`;
|
|
case 'settings': return '/settings';
|
|
}
|
|
}
|
|
|
|
// ============================================================================
|
|
// Router context
|
|
// ============================================================================
|
|
|
|
interface RouterContextValue {
|
|
route: Route;
|
|
navigate: (path: string) => void;
|
|
goBack: () => void;
|
|
params: RouteParams;
|
|
}
|
|
|
|
const RouterContext = createContext<RouterContextValue | null>(null);
|
|
|
|
export function RouterProvider({ children }: { children: React.ReactNode }) {
|
|
const [route, setRoute] = useState<Route>(() => parsePath(window.location.pathname));
|
|
|
|
useEffect(() => {
|
|
const handlePopState = () => {
|
|
setRoute(parsePath(window.location.pathname));
|
|
};
|
|
window.addEventListener('popstate', handlePopState);
|
|
return () => window.removeEventListener('popstate', handlePopState);
|
|
}, []);
|
|
|
|
const navigate = useCallback((path: string) => {
|
|
window.history.pushState(null, '', path);
|
|
setRoute(parsePath(path));
|
|
}, []);
|
|
|
|
const goBack = useCallback(() => {
|
|
window.history.back();
|
|
}, []);
|
|
|
|
const params: RouteParams = {};
|
|
if ('eventId' in route) params.eventId = route.eventId;
|
|
if ('userId' in route) params.userId = route.userId;
|
|
|
|
return (
|
|
<RouterContext.Provider value={{ route, navigate, goBack, params }}>
|
|
{children}
|
|
</RouterContext.Provider>
|
|
);
|
|
}
|
|
|
|
// ============================================================================
|
|
// Hooks
|
|
// ============================================================================
|
|
|
|
export function useRouter() {
|
|
const context = useContext(RouterContext);
|
|
if (!context) throw new Error('useRouter must be used within a RouterProvider');
|
|
return context;
|
|
}
|
|
|
|
export function useNavigate() {
|
|
return useRouter().navigate;
|
|
}
|
|
|
|
export function useGoBack() {
|
|
return useRouter().goBack;
|
|
}
|
|
|
|
export function useParams(): RouteParams {
|
|
return useRouter().params;
|
|
}
|