226 lines
8.1 KiB
TypeScript
226 lines
8.1 KiB
TypeScript
'use client';
|
|
|
|
import { LeagueMemberRow } from '@/components/leagues/LeagueMemberRow';
|
|
import { LeagueMemberTable } from '@/components/leagues/LeagueMemberTable';
|
|
import { EmptyState } from '@/ui/EmptyState';
|
|
import { LoadingWrapper } from '@/ui/LoadingWrapper';
|
|
import { useEffectiveDriverId } from '@/hooks/useEffectiveDriverId';
|
|
import { useInject } from '@/lib/di/hooks/useInject';
|
|
import { DRIVER_SERVICE_TOKEN, LEAGUE_MEMBERSHIP_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 { Button } from '@/ui/Button';
|
|
import { Select } from '@/ui/Select';
|
|
import { Text } from '@/ui/Text';
|
|
import { Box } from '@/ui/Box';
|
|
import { Group } from '@/ui/Group';
|
|
import { ControlBar } from '@/ui/ControlBar';
|
|
import React, { useCallback, useEffect, useState } from 'react';
|
|
|
|
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;
|
|
};
|
|
|
|
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 <LoadingWrapper variant="spinner" message="Loading members..." />;
|
|
}
|
|
|
|
if (members.length === 0) {
|
|
return (
|
|
<EmptyState
|
|
title="No members found"
|
|
description="This league doesn't have any members yet."
|
|
variant="minimal"
|
|
/>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<Box>
|
|
<ControlBar
|
|
leftContent={
|
|
<Text size="sm" variant="low">
|
|
{members.length} {members.length === 1 ? 'member' : 'members'}
|
|
</Text>
|
|
}
|
|
>
|
|
<Group gap={2}>
|
|
<Text size="sm" variant="low">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}
|
|
/>
|
|
</Group>
|
|
</ControlBar>
|
|
|
|
<Box overflowX="auto" marginTop={4}>
|
|
<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 ? (
|
|
<Group gap={2} justify="end">
|
|
{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}
|
|
/>
|
|
)}
|
|
{onRemoveMember && (
|
|
<Button
|
|
variant="ghost"
|
|
onClick={() => onRemoveMember(member.driverId)}
|
|
size="sm"
|
|
>
|
|
<Text variant="critical">Remove</Text>
|
|
</Button>
|
|
)}
|
|
</Group>
|
|
) : (showActions && cannotModify ? <Text size="xs" variant="low">—</Text> : undefined)}
|
|
/>
|
|
);
|
|
})}
|
|
</LeagueMemberTable>
|
|
</Box>
|
|
</Box>
|
|
);
|
|
}
|