Files
gridpilot.gg/apps/website/components/leagues/LeagueMembers.tsx
2026-01-18 13:26:35 +01:00

229 lines
8.4 KiB
TypeScript

'use client';
import { useEffectiveDriverId } from '@/hooks/useEffectiveDriverId';
import { useInject } from '@/lib/di/hooks/useInject';
import { LEAGUE_MEMBERSHIP_SERVICE_TOKEN, DRIVER_SERVICE_TOKEN } from '@/lib/di/tokens';
import { routes } from '@/lib/routing/RouteConfig';
import type { LeagueMembership } from '@/lib/types/LeagueMembership';
import type { MembershipRole } from '@/lib/types/MembershipRole';
import { DriverViewModel } from '@/lib/view-models/DriverViewModel';
import { useCallback, useEffect, useState } from 'react';
import { Box } from '@/ui/Box';
import { Text } from '@/ui/Text';
import { Select } from '@/ui/Select';
import { Button } from '@/ui/Button';
import { LeagueMemberTable } from '@/components/leagues/LeagueMemberTable';
import { LeagueMemberRow } from '@/components/leagues/LeagueMemberRow';
import { MinimalEmptyState } from '@/components/shared/state/EmptyState';
interface LeagueMembersProps {
leagueId: string;
onRemoveMember?: (driverId: string) => void;
onUpdateRole?: (driverId: string, role: MembershipRole) => void;
showActions?: boolean;
}
export function LeagueMembers({
leagueId,
onRemoveMember,
onUpdateRole,
showActions = false
}: LeagueMembersProps) {
const [members, setMembers] = useState<LeagueMembership[]>([]);
const [driversById, setDriversById] = useState<Record<string, DriverViewModel>>({});
const [loading, setLoading] = useState(true);
const [sortBy, setSortBy] = useState<'role' | 'name' | 'date' | 'rating' | 'points' | 'wins'>('rating');
const currentDriverId = useEffectiveDriverId();
const leagueMembershipService = useInject(LEAGUE_MEMBERSHIP_SERVICE_TOKEN);
const driverService = useInject(DRIVER_SERVICE_TOKEN);
const loadMembers = useCallback(async () => {
setLoading(true);
try {
await leagueMembershipService.fetchLeagueMemberships(leagueId);
const membershipData = leagueMembershipService.getLeagueMembers(leagueId);
setMembers(membershipData);
const uniqueDriverIds = Array.from(new Set(membershipData.map((m: LeagueMembership) => m.driverId)));
if (uniqueDriverIds.length > 0) {
const result = await driverService.findByIds(uniqueDriverIds);
if (result.isOk()) {
const driverDtos = result.unwrap();
const byId: Record<string, DriverViewModel> = {};
for (const dto of driverDtos) {
byId[dto.id] = new DriverViewModel({ ...dto, avatarUrl: dto.avatarUrl ?? null });
}
setDriversById(byId);
}
} else {
setDriversById({});
}
} catch (error) {
console.error('Failed to load members:', error);
} finally {
setLoading(false);
}
}, [leagueId, leagueMembershipService, driverService]);
useEffect(() => {
loadMembers();
}, [loadMembers]);
const getDriverName = (driverId: string): string => {
const driver = driversById[driverId];
return driver?.name || 'Unknown Driver';
};
const getRoleOrder = (role: MembershipRole): number => {
const order: Record<MembershipRole, number> = { owner: 0, admin: 1, steward: 2, member: 3 };
return order[role];
};
const getDriverStats = (): { rating: number; wins: number; overallRank: number } | null => {
return null;
};
// eslint-disable-next-line gridpilot-rules/component-no-data-manipulation
const sortedMembers = [...members].sort((a, b) => {
switch (sortBy) {
case 'role':
return getRoleOrder(a.role) - getRoleOrder(b.role);
case 'name':
return getDriverName(a.driverId).localeCompare(getDriverName(b.driverId));
case 'date':
return new Date(b.joinedAt).getTime() - new Date(a.joinedAt).getTime();
case 'rating': {
const statsA = getDriverStats();
const statsB = getDriverStats();
return (statsB?.rating || 0) - (statsA?.rating || 0);
}
case 'points':
return 0;
case 'wins': {
const statsA = getDriverStats();
const statsB = getDriverStats();
return (statsB?.wins || 0) - (statsA?.wins || 0);
}
default:
return 0;
}
});
const getRoleVariant = (role: MembershipRole): 'default' | 'primary' | 'success' | 'warning' | 'danger' | 'info' => {
switch (role) {
case 'owner': return 'warning';
case 'admin': return 'primary';
case 'steward': return 'info';
case 'member': return 'primary';
default: return 'default';
}
};
if (loading) {
return (
<Box textAlign="center" py={8}>
<Text color="text-gray-400">Loading members...</Text>
</Box>
);
}
if (members.length === 0) {
return (
<MinimalEmptyState
title="No members found"
description="This league doesn't have any members yet."
/>
);
}
return (
<Box>
{/* Sort Controls */}
<Box display="flex" alignItems="center" justifyContent="between" mb={4}>
<Text size="sm" color="text-gray-400">
{members.length} {members.length === 1 ? 'member' : 'members'}
</Text>
<Box display="flex" alignItems="center" gap={2}>
<Text as="label" size="sm" color="text-gray-400">Sort by:</Text>
<Select
value={sortBy}
onChange={(e: React.ChangeEvent<HTMLSelectElement>) => setSortBy(e.target.value as typeof sortBy)}
options={[
{ value: 'rating', label: 'Rating' },
{ value: 'points', label: 'Points' },
{ value: 'wins', label: 'Wins' },
{ value: 'role', label: 'Role' },
{ value: 'name', label: 'Name' },
{ value: 'date', label: 'Join Date' },
]}
fullWidth={false}
/>
</Box>
</Box>
{/* Members Table */}
<Box overflow="auto">
<LeagueMemberTable showActions={showActions}>
{sortedMembers.map((member, index) => {
const isCurrentUser = member.driverId === currentDriverId;
const cannotModify = member.role === 'owner';
const driverStats = getDriverStats();
const isTopPerformer = index < 3 && sortBy === 'rating';
const driver = driversById[member.driverId];
const ratingAndWinsMeta =
driverStats && typeof driverStats.rating === 'number'
? `Rating ${driverStats.rating}${driverStats.wins ?? 0} wins`
: null;
return (
<LeagueMemberRow
key={member.driverId}
driver={driver}
driverId={member.driverId}
isCurrentUser={isCurrentUser}
isTopPerformer={isTopPerformer}
role={member.role}
roleVariant={getRoleVariant(member.role)}
joinedAt={member.joinedAt}
rating={driverStats?.rating}
rank={driverStats?.overallRank}
wins={driverStats?.wins}
href={routes.driver.detail(member.driverId)}
meta={ratingAndWinsMeta}
actions={showActions && !cannotModify && !isCurrentUser ? (
<Box display="flex" alignItems="center" justifyContent="end" gap={2}>
{onUpdateRole && (
<Select
value={member.role}
onChange={(e: React.ChangeEvent<HTMLSelectElement>) => onUpdateRole(member.driverId, e.target.value as MembershipRole)}
options={[
{ value: 'member', label: 'Member' },
{ value: 'steward', label: 'Steward' },
{ value: 'admin', label: 'Admin' },
]}
fullWidth={false}
// eslint-disable-next-line gridpilot-rules/component-classification
className="text-xs py-1 px-2"
/>
)}
{onRemoveMember && (
<Button
variant="ghost"
onClick={() => onRemoveMember(member.driverId)}
size="sm"
color="text-error-red"
>
Remove
</Button>
)}
</Box>
) : (showActions && cannotModify ? <Text size="xs" color="text-gray-500"></Text> : undefined)}
/>
);
})}
</LeagueMemberTable>
</Box>
</Box>
);
}