Files
gridpilot.gg/apps/website/components/teams/TeamRoster.tsx
2026-01-15 01:26:30 +01:00

211 lines
6.6 KiB
TypeScript

'use client';
import { Card } from '@/ui/Card';
import { DriverIdentity } from '@/components/drivers/DriverIdentity';
import { useTeamRoster } from "@/lib/hooks/team";
import { useState } from 'react';
import type { DriverViewModel } from '@/lib/view-models/DriverViewModel';
import { Box } from '@/ui/Box';
import { Stack } from '@/ui/Stack';
import { Text } from '@/ui/Text';
import { Heading } from '@/ui/Heading';
import { Select } from '@/ui/Select';
import { Surface } from '@/ui/Surface';
import { Badge } from '@/ui/Badge';
import { Button } from '@/ui/Button';
type TeamRole = 'owner' | 'admin' | 'member';
type TeamMemberRole = 'owner' | 'manager' | 'member';
interface TeamRosterProps {
teamId: string;
memberships: Array<{
driverId: string;
driverName: string;
role: 'owner' | 'manager' | 'member';
joinedAt: string;
isActive: boolean;
avatarUrl: string;
}>;
isAdmin: boolean;
onRemoveMember?: (driverId: string) => void;
onChangeRole?: (driverId: string, newRole: TeamRole) => void;
}
export function TeamRoster({
teamId,
memberships,
isAdmin,
onRemoveMember,
onChangeRole,
}: TeamRosterProps) {
const [sortBy, setSortBy] = useState<'role' | 'rating' | 'name'>('rating');
// Use hook for data fetching
const { data: teamMembers = [], isLoading: loading } = useTeamRoster(memberships);
const getRoleLabel = (role: TeamRole | TeamMemberRole) => {
// Convert manager to admin for display
const displayRole = role === 'manager' ? 'admin' : role;
return displayRole.charAt(0).toUpperCase() + displayRole.slice(1);
};
function getRoleOrder(role: TeamMemberRole): number {
switch (role) {
case 'owner':
return 0;
case 'manager':
return 1;
case 'member':
return 2;
default:
return 3;
}
}
const sortedMembers = [...teamMembers].sort((a, b) => {
switch (sortBy) {
case 'rating': {
const ratingA = a.rating ?? 0;
const ratingB = b.rating ?? 0;
return ratingB - ratingA;
}
case 'role': {
return getRoleOrder(a.role) - getRoleOrder(b.role);
}
case 'name': {
return a.driver.name.localeCompare(b.driver.name);
}
default:
return 0;
}
});
const teamAverageRating = teamMembers.length > 0
? teamMembers.reduce((sum: number, m: any) => sum + (m.rating || 0), 0) / teamMembers.length
: 0;
if (loading) {
return (
<Card>
<Box textAlign="center" py={8}>
<Text color="text-gray-400">Loading roster...</Text>
</Box>
</Card>
);
}
return (
<Card>
<Stack direction="row" align="center" justify="between" mb={6} wrap>
<Box>
<Heading level={3}>Team Roster</Heading>
<Text size="sm" color="text-gray-400" block mt={1}>
{memberships.length} {memberships.length === 1 ? 'member' : 'members'} Avg Rating:{' '}
<Text color="text-primary-blue" weight="medium">{teamAverageRating.toFixed(0)}</Text>
</Text>
</Box>
<Stack direction="row" align="center" gap={2}>
<Text size="sm" color="text-gray-400">Sort by:</Text>
<Box width={32}>
<Select
value={sortBy}
onChange={(e) => setSortBy(e.target.value as typeof sortBy)}
options={[
{ value: 'rating', label: 'Rating' },
{ value: 'role', label: 'Role' },
{ value: 'name', label: 'Name' },
]}
className="py-1 text-sm"
/>
</Box>
</Stack>
</Stack>
<Stack gap={3}>
{sortedMembers.map((member) => {
const { driver, role, joinedAt, rating, overallRank } = member;
// Convert manager to admin for display purposes
const displayRole: TeamRole = role === 'manager' ? 'admin' : (role as TeamRole);
const canManageMembership = isAdmin && role !== 'owner';
return (
<Surface
key={driver.id}
variant="dark"
rounded="lg"
border
padding={4}
>
<Stack direction="row" align="center" justify="between" wrap gap={4}>
<DriverIdentity
driver={driver as DriverViewModel}
href={`/drivers/${driver.id}?from=team&teamId=${teamId}`}
contextLabel={getRoleLabel(role)}
meta={
<Text size="xs" color="text-gray-400">
{driver.country} Joined {new Date(joinedAt).toLocaleDateString()}
</Text>
}
size="md"
/>
{rating !== null && (
<Stack direction="row" align="center" gap={6}>
<Box textAlign="center">
<Text size="lg" weight="bold" color="text-primary-blue" block>
{rating}
</Text>
<Text size="xs" color="text-gray-400">Rating</Text>
</Box>
{overallRank !== null && (
<Box textAlign="center">
<Text size="sm" color="text-gray-300" block>#{overallRank}</Text>
<Text size="xs" color="text-gray-500">Rank</Text>
</Box>
)}
</Stack>
)}
{canManageMembership && (
<Stack direction="row" align="center" gap={2}>
<Box width={32}>
<Select
value={displayRole}
onChange={(e) =>
onChangeRole?.(driver.id, e.target.value as TeamRole)
}
options={[
{ value: 'member', label: 'Member' },
{ value: 'admin', label: 'Admin' },
]}
className="text-sm"
/>
</Box>
<Button
variant="danger"
size="sm"
onClick={() => onRemoveMember?.(driver.id)}
>
Remove
</Button>
</Stack>
)}
</Stack>
</Surface>
);
})}
</Stack>
{memberships.length === 0 && (
<Box textAlign="center" py={8}>
<Text color="text-gray-400">No team members yet.</Text>
</Box>
)}
</Card>
);
}