/** * Website-local racing façade * * This module provides synchronous helper functions used by the alpha website * without depending on legacy exports from @gridpilot/racing/application. * It maintains simple in-memory state for memberships, teams, and registrations. */ import type { LeagueMembership as DomainLeagueMembership, MembershipRole, MembershipStatus, } from '@gridpilot/racing/domain/entities/LeagueMembership'; import { getDriverAvatar, getTeamLogo, getLeagueBanner, memberships as seedMemberships, leagues as seedLeagues } from '@gridpilot/testing-support'; export type { MembershipRole, MembershipStatus }; export interface LeagueMembership extends Omit { joinedAt: string; } // Lightweight league join request model for the website export interface JoinRequest { id: string; leagueId: string; driverId: string; message?: string; requestedAt: string; } import type { Team, TeamJoinRequest, TeamMembership, TeamRole, TeamMembershipStatus, } from '@gridpilot/racing/domain/entities/Team'; export type { Team, TeamJoinRequest, TeamMembership, TeamRole, TeamMembershipStatus }; /** * Identity helpers * * For the alpha website we treat a single demo driver as the "current" user. */ const CURRENT_DRIVER_ID = 'driver-1'; export function getCurrentDriverId(): string { return CURRENT_DRIVER_ID; } /** * In-memory stores */ const leagueMemberships = new Map(); const leagueJoinRequests = new Map(); const teams = new Map(); const teamMemberships = new Map(); const teamJoinRequests = new Map(); const raceRegistrations = new Map>(); /** * Helper utilities */ function ensureLeagueMembershipArray(leagueId: string): LeagueMembership[] { let list = leagueMemberships.get(leagueId); if (!list) { list = []; leagueMemberships.set(leagueId, list); } return list; } function ensureTeamMembershipArray(teamId: string): TeamMembership[] { let list = teamMemberships.get(teamId); if (!list) { list = []; teamMemberships.set(teamId, list); } return list; } function ensureRaceRegistrationSet(raceId: string): Set { let set = raceRegistrations.get(raceId); if (!set) { set = new Set(); raceRegistrations.set(raceId, set); } return set; } let idCounter = 1; function generateId(prefix: string): string { return `${prefix}-${idCounter++}`; } // Initialize league memberships from static seed data (function initializeLeagueMembershipsFromSeed() { if (leagueMemberships.size > 0) { return; } const membershipsByLeague = new Map(); // Create base active memberships from seed for (const membership of seedMemberships) { const list = membershipsByLeague.get(membership.leagueId) ?? []; const joinedAt = new Date(2024, 0, 1 + (idCounter % 28)).toISOString(); list.push({ leagueId: membership.leagueId, driverId: membership.driverId, role: 'member', status: 'active', joinedAt, }); membershipsByLeague.set(membership.leagueId, list); } // Ensure league owners are represented as owners in memberships for (const league of seedLeagues) { const list = membershipsByLeague.get(league.id) ?? []; const existingOwnerMembership = list.find((m) => m.driverId === league.ownerId); if (existingOwnerMembership) { existingOwnerMembership.role = 'owner'; } else { const joinedAt = new Date(2024, 0, 1 + (idCounter % 28)).toISOString(); list.unshift({ leagueId: league.id, driverId: league.ownerId, role: 'owner', status: 'active', joinedAt, }); } membershipsByLeague.set(league.id, list); } // Store into facade-local maps for (const [leagueId, list] of membershipsByLeague.entries()) { leagueMemberships.set(leagueId, list); } })(); export function getDriverAvatarUrl(driverId: string): string { return getDriverAvatar(driverId); } export function getTeamLogoUrl(teamId: string): string { return getTeamLogo(teamId); } export function getLeagueBannerUrl(leagueId: string): string { return getLeagueBanner(leagueId); } /** * League membership API */ export function getMembership(leagueId: string, driverId: string): LeagueMembership | null { const list = leagueMemberships.get(leagueId); if (!list) return null; return list.find((m) => m.driverId === driverId) ?? null; } export function getLeagueMembers(leagueId: string): LeagueMembership[] { return [...(leagueMemberships.get(leagueId) ?? [])]; } export function joinLeague(leagueId: string, driverId: string): void { const existing = getMembership(leagueId, driverId); if (existing && existing.status === 'active') { throw new Error('Already a member of this league'); } const list = ensureLeagueMembershipArray(leagueId); const now = new Date(); if (existing) { existing.status = 'active'; existing.joinedAt = now.toISOString(); return; } list.push({ leagueId, driverId, role: list.length === 0 ? 'owner' : 'member', status: 'active', joinedAt: now.toISOString(), }); } export function leaveLeague(leagueId: string, driverId: string): void { const list = ensureLeagueMembershipArray(leagueId); const membership = list.find((m) => m.driverId === driverId); if (!membership) { throw new Error('Not a member of this league'); } if (membership.role === 'owner') { throw new Error('League owner cannot leave the league'); } const idx = list.indexOf(membership); if (idx >= 0) { list.splice(idx, 1); } } export function requestToJoin(leagueId: string, driverId: string): void { const existing = getMembership(leagueId, driverId); if (existing && existing.status === 'active') { throw new Error('Already a member of this league'); } const requests = leagueJoinRequests.get(leagueId) ?? []; const now = new Date().toISOString(); const request: JoinRequest = { id: generateId('league-request'), leagueId, driverId, requestedAt: now, }; requests.push(request); leagueJoinRequests.set(leagueId, requests); } export function isOwnerOrAdmin(leagueId: string, driverId: string): boolean { const membership = getMembership(leagueId, driverId); if (!membership) return false; return membership.role === 'owner' || membership.role === 'admin'; } /** * League admin API (join requests and membership management) */ export function getJoinRequests(leagueId: string): JoinRequest[] { return [...(leagueJoinRequests.get(leagueId) ?? [])]; } export function approveJoinRequest(requestId: string): void { for (const [leagueId, requests] of leagueJoinRequests.entries()) { const idx = requests.findIndex((r) => r.id === requestId); if (idx >= 0) { const request = requests[idx]; requests.splice(idx, 1); leagueJoinRequests.set(leagueId, requests); joinLeague(leagueId, request.driverId); return; } } throw new Error('Join request not found'); } export function rejectJoinRequest(requestId: string): void { for (const [leagueId, requests] of leagueJoinRequests.entries()) { const idx = requests.findIndex((r) => r.id === requestId); if (idx >= 0) { requests.splice(idx, 1); leagueJoinRequests.set(leagueId, requests); return; } } throw new Error('Join request not found'); } export function removeMember(leagueId: string, driverId: string, performedBy: string): void { const performer = getMembership(leagueId, performedBy); if (!performer || (performer.role !== 'owner' && performer.role !== 'admin')) { throw new Error('Only owners or admins can remove members'); } const list = ensureLeagueMembershipArray(leagueId); const membership = list.find((m) => m.driverId === driverId); if (!membership) { throw new Error('Member not found'); } if (membership.role === 'owner') { throw new Error('Cannot remove the league owner'); } const idx = list.indexOf(membership); if (idx >= 0) { list.splice(idx, 1); } } export function updateMemberRole( leagueId: string, driverId: string, newRole: MembershipRole, performedBy: string, ): void { const performer = getMembership(leagueId, performedBy); if (!performer || performer.role !== 'owner') { throw new Error('Only the league owner can update roles'); } const list = ensureLeagueMembershipArray(leagueId); const membership = list.find((m) => m.driverId === driverId); if (!membership) { throw new Error('Member not found'); } if (membership.role === 'owner') { throw new Error('Cannot change the owner role'); } membership.role = newRole; } /** * Team API */ export function createTeam(initial: Pick): Team { const id = generateId('team'); const now = new Date(); const team: Team = { id, name: initial.name, tag: initial.tag, description: initial.description, leagues: initial.leagues, ownerId: CURRENT_DRIVER_ID, createdAt: now, }; teams.set(id, team); const members = ensureTeamMembershipArray(id); members.push({ teamId: id, driverId: CURRENT_DRIVER_ID, role: 'owner', status: 'active', joinedAt: now, }); return team; } export function getAllTeams(): Team[] { return [...teams.values()]; } export function getTeam(teamId: string): Team | null { return teams.get(teamId) ?? null; } export function updateTeam(teamId: string, updates: Partial>, updatedBy: string): void { const team = teams.get(teamId); if (!team) { throw new Error('Team not found'); } const membership = getTeamMembership(teamId, updatedBy); if (!membership || (membership.role !== 'owner' && membership.role !== 'manager')) { throw new Error('Only owners or managers can update team'); } teams.set(teamId, { ...team, ...updates, }); } export function getTeamMembers(teamId: string): TeamMembership[] { return [...(teamMemberships.get(teamId) ?? [])]; } export function getTeamMembership(teamId: string, driverId: string): TeamMembership | null { const list = teamMemberships.get(teamId); if (!list) return null; return list.find((m) => m.driverId === driverId) ?? null; } export function getDriverTeam(driverId: string): { team: Team; membership: TeamMembership } | null { for (const [teamId, memberships] of teamMemberships.entries()) { const membership = memberships.find((m) => m.driverId === driverId && m.status === 'active'); if (membership) { const team = teams.get(teamId); if (team) { return { team, membership }; } } } return null; } export function isTeamOwnerOrManager(teamId: string, driverId: string): boolean { const membership = getTeamMembership(teamId, driverId); if (!membership) return false; return membership.role === 'owner' || membership.role === 'manager'; } export function joinTeam(teamId: string, driverId: string): void { const team = teams.get(teamId); if (!team) { throw new Error('Team not found'); } const existing = getTeamMembership(teamId, driverId); if (existing && existing.status === 'active') { throw new Error('Already a member of this team'); } const list = ensureTeamMembershipArray(teamId); const now = new Date(); if (existing) { existing.status = 'active'; existing.joinedAt = now; return; } list.push({ teamId, driverId, role: list.length === 0 ? 'owner' : 'driver', status: 'active', joinedAt: now, }); } export function leaveTeam(teamId: string, driverId: string): void { const list = ensureTeamMembershipArray(teamId); const membership = list.find((m) => m.driverId === driverId); if (!membership) { throw new Error('Not a member of this team'); } if (membership.role === 'owner') { throw new Error('Team owner cannot leave the team'); } const idx = list.indexOf(membership); if (idx >= 0) { list.splice(idx, 1); } } export function requestToJoinTeam(teamId: string, driverId: string, message?: string): void { const existing = getTeamMembership(teamId, driverId); if (existing && existing.status === 'active') { throw new Error('Already a member of this team'); } const requests = teamJoinRequests.get(teamId) ?? []; const now = new Date(); const request: TeamJoinRequest = { id: generateId('team-request'), teamId, driverId, message, requestedAt: now, }; requests.push(request); teamJoinRequests.set(teamId, requests); } export function getTeamJoinRequests(teamId: string): TeamJoinRequest[] { return [...(teamJoinRequests.get(teamId) ?? [])]; } export function approveTeamJoinRequest(requestId: string): void { for (const [teamId, requests] of teamJoinRequests.entries()) { const idx = requests.findIndex((r) => r.id === requestId); if (idx >= 0) { const request = requests[idx]; requests.splice(idx, 1); teamJoinRequests.set(teamId, requests); joinTeam(teamId, request.driverId); return; } } throw new Error('Join request not found'); } export function rejectTeamJoinRequest(requestId: string): void { for (const [teamId, requests] of teamJoinRequests.entries()) { const idx = requests.findIndex((r) => r.id === requestId); if (idx >= 0) { requests.splice(idx, 1); teamJoinRequests.set(teamId, requests); return; } } throw new Error('Join request not found'); } export function removeTeamMember(teamId: string, driverId: string, performedBy: string): void { const performerMembership = getTeamMembership(teamId, performedBy); if (!performerMembership || (performerMembership.role !== 'owner' && performerMembership.role !== 'manager')) { throw new Error('Only owners or managers can remove members'); } const list = ensureTeamMembershipArray(teamId); const membership = list.find((m) => m.driverId === driverId); if (!membership) { throw new Error('Member not found'); } if (membership.role === 'owner') { throw new Error('Cannot remove the team owner'); } const idx = list.indexOf(membership); if (idx >= 0) { list.splice(idx, 1); } } export function updateTeamMemberRole(teamId: string, driverId: string, newRole: TeamRole, performedBy: string): void { const performerMembership = getTeamMembership(teamId, performedBy); if (!performerMembership || (performerMembership.role !== 'owner' && performerMembership.role !== 'manager')) { throw new Error('Only owners or managers can update roles'); } const membership = getTeamMembership(teamId, driverId); if (!membership) { throw new Error('Member not found'); } if (membership.role === 'owner') { throw new Error('Cannot change the owner role'); } membership.role = newRole; } /** * Race registration API */ export function isRegistered(raceId: string, driverId: string): boolean { const set = raceRegistrations.get(raceId); if (!set) return false; return set.has(driverId); } export function registerForRace(raceId: string, driverId: string, _leagueId: string): void { const set = ensureRaceRegistrationSet(raceId); if (set.has(driverId)) { throw new Error('Already registered for this race'); } set.add(driverId); } export function withdrawFromRace(raceId: string, driverId: string): void { const set = raceRegistrations.get(raceId); if (!set || !set.has(driverId)) { throw new Error('Not registered for this race'); } set.delete(driverId); } export function getRegisteredDrivers(raceId: string): string[] { const set = raceRegistrations.get(raceId); if (!set) return []; return [...set.values()]; }