Files
gridpilot.gg/apps/website/components/teams/TeamRoster.tsx
2026-01-19 14:07:49 +01:00

172 lines
5.7 KiB
TypeScript

'use client';
import { MinimalEmptyState } from '@/ui/EmptyState';
import { TeamRosterItem } from '@/components/teams/TeamRosterItem';
import { TeamRosterList } from '@/components/teams/TeamRosterList';
import { useTeamRoster } from "@/hooks/team/useTeamRoster";
import { routes } from '@/lib/routing/RouteConfig';
import { sortMembers } from '@/lib/utilities/roster-utils';
import type { DriverViewModel } from '@/lib/view-models/DriverViewModel';
import { Button } from '@/ui/Button';
import { Card } from '@/ui/Card';
import { Heading } from '@/ui/Heading';
import { Stack } from '@/ui/Stack';
import { Select } from '@/ui/Select';
import { Text } from '@/ui/Text';
import { useMemo, useState } from 'react';
import { MemberDisplay } from '@/lib/display-objects/MemberDisplay';
import { RatingDisplay } from '@/lib/display-objects/RatingDisplay';
import { DateDisplay } from '@/lib/display-objects/DateDisplay';
export type TeamRole = 'owner' | 'admin' | 'member';
export 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;
}
interface TeamMember {
driver: {
id: string;
name: string;
country: string;
};
role: TeamMemberRole;
joinedAt: string;
rating: number | null;
overallRank: number | null;
}
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 = useMemo(() => (role: TeamRole | TeamMemberRole) => {
// Convert manager to admin for display
const displayRole = role === 'manager' ? 'admin' : role;
return displayRole.charAt(0).toUpperCase() + displayRole.slice(1);
}, []);
const sortedMembers = useMemo(() => {
return sortMembers(teamMembers as unknown as TeamMember[], sortBy);
}, [teamMembers, sortBy]);
const teamAverageRatingLabel = useMemo(() => {
if (teamMembers.length === 0) return '—';
const avg = teamMembers.reduce((sum: number, m: { rating?: number | null }) => sum + (m.rating || 0), 0) / teamMembers.length;
return RatingDisplay.format(avg);
}, [teamMembers]);
if (loading) {
return (
<Card>
<Stack display="flex" justifyContent="center" py={8}>
<Text color="text-gray-400">Loading roster...</Text>
</Stack>
</Card>
);
}
return (
<Card>
<Stack direction="row" align="center" justify="between" mb={6} wrap gap={4}>
<Stack>
<Heading level={3}>Team Roster</Heading>
<Text size="sm" color="text-gray-400" block mt={1}>
{MemberDisplay.formatCount(memberships.length)} Avg Rating:{' '}
<Text color="text-primary-blue" weight="medium">{teamAverageRatingLabel}</Text>
</Text>
</Stack>
<Stack direction="row" align="center" gap={2}>
<Text size="sm" color="text-gray-400">Sort by:</Text>
<Stack 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' },
]}
/>
</Stack>
</Stack>
</Stack>
{sortedMembers.length > 0 ? (
<TeamRosterList>
{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 (
<TeamRosterItem
key={driver.id}
driver={driver as DriverViewModel}
href={`${routes.driver.detail(driver.id)}?from=team&teamId=${teamId}`}
roleLabel={getRoleLabel(role)}
joinedAtLabel={DateDisplay.formatShort(joinedAt)}
ratingLabel={RatingDisplay.format(rating)}
overallRankLabel={overallRank !== null ? `#${overallRank}` : null}
actions={canManageMembership ? (
<>
<Stack width="32">
<Select
value={displayRole}
onChange={(e) =>
onChangeRole?.(driver.id, e.target.value as TeamRole)
}
options={[
{ value: 'member', label: 'Member' },
{ value: 'admin', label: 'Admin' },
]}
/>
</Stack>
<Button
variant="danger"
size="sm"
onClick={() => onRemoveMember?.(driver.id)}
>
Remove
</Button>
</>
) : undefined}
/>
);
})}
</TeamRosterList>
) : (
<MinimalEmptyState
title="No team members yet"
description="When drivers join your team, they will appear here."
/>
)}
</Card>
);
}