Files
gridpilot.gg/apps/website/components/teams/TeamRoster.tsx
2025-12-11 00:57:32 +01:00

194 lines
6.2 KiB
TypeScript

'use client';
import { useState, useEffect } from 'react';
import Card from '@/components/ui/Card';
import DriverIdentity from '@/components/drivers/DriverIdentity';
import {
getTeamRosterViewModel,
type TeamRosterViewModel,
} from '@/lib/presenters/TeamRosterPresenter';
type TeamRole = 'owner' | 'manager' | 'driver';
interface TeamMembershipSummary {
driverId: string;
role: TeamRole;
joinedAt: Date;
}
interface TeamRosterProps {
teamId: string;
memberships: TeamMembershipSummary[];
isAdmin: boolean;
onRemoveMember?: (driverId: string) => void;
onChangeRole?: (driverId: string, newRole: TeamRole) => void;
}
export default function TeamRoster({
teamId,
memberships,
isAdmin,
onRemoveMember,
onChangeRole,
}: TeamRosterProps) {
const [viewModel, setViewModel] = useState<TeamRosterViewModel | null>(null);
const [loading, setLoading] = useState(true);
const [sortBy, setSortBy] = useState<'role' | 'rating' | 'name'>('rating');
useEffect(() => {
const load = async () => {
setLoading(true);
try {
const vm = await getTeamRosterViewModel(memberships);
setViewModel(vm);
} finally {
setLoading(false);
}
};
void load();
}, [memberships]);
const getRoleBadgeColor = (role: TeamRole) => {
switch (role) {
case 'owner':
return 'bg-warning-amber/20 text-warning-amber';
case 'manager':
return 'bg-primary-blue/20 text-primary-blue';
default:
return 'bg-charcoal-outline text-gray-300';
}
};
const getRoleLabel = (role: TeamRole) => {
return role.charAt(0).toUpperCase() + role.slice(1);
};
const sortedMembers = viewModel
? [...viewModel.members].sort((a, b) => {
switch (sortBy) {
case 'rating': {
const ratingA = a.rating ?? 0;
const ratingB = b.rating ?? 0;
return ratingB - ratingA;
}
case 'role': {
const roleOrder: Record<TeamRole, number> = { owner: 0, manager: 1, driver: 2 };
return roleOrder[a.role] - roleOrder[b.role];
}
case 'name': {
return a.driver.name.localeCompare(b.driver.name);
}
default:
return 0;
}
})
: [];
const teamAverageRating = viewModel?.averageRating ?? 0;
if (loading) {
return (
<Card>
<div className="text-center py-8 text-gray-400">Loading roster...</div>
</Card>
);
}
return (
<Card>
<div className="flex items-center justify-between mb-6">
<div>
<h3 className="text-xl font-semibold text-white">Team Roster</h3>
<p className="text-sm text-gray-400 mt-1">
{memberships.length} {memberships.length === 1 ? 'member' : 'members'} Avg Rating:{' '}
<span className="text-primary-blue font-medium">{teamAverageRating}</span>
</p>
</div>
<div className="flex items-center gap-2">
<label className="text-sm text-gray-400">Sort by:</label>
<select
value={sortBy}
onChange={(e) => setSortBy(e.target.value as any)}
className="px-3 py-1 bg-deep-graphite border border-charcoal-outline rounded text-white text-sm focus:outline-none focus:ring-2 focus:ring-primary-blue"
>
<option value="rating">Rating</option>
<option value="role">Role</option>
<option value="name">Name</option>
</select>
</div>
</div>
<div className="space-y-3">
{sortedMembers.map((member) => {
const { driver, role, joinedAt, rating, overallRank } = member;
const canManageMembership = isAdmin && role !== 'owner';
return (
<div
key={driver.id}
className="flex items-center justify-between p-4 rounded-lg bg-deep-graphite border border-charcoal-outline hover:border-charcoal-outline/60 transition-colors"
>
<DriverIdentity
driver={driver}
href={`/drivers/${driver.id}?from=team&teamId=${teamId}`}
contextLabel={getRoleLabel(role)}
meta={
<span>
{driver.country} Joined {new Date(joinedAt).toLocaleDateString()}
</span>
}
size="md"
/>
{rating !== null && (
<div className="flex items-center gap-6 text-center">
<div>
<div className="text-lg font-bold text-primary-blue">
{rating}
</div>
<div className="text-xs text-gray-400">Rating</div>
</div>
{overallRank !== null && (
<div>
<div className="text-sm text-gray-300">#{overallRank}</div>
<div className="text-xs text-gray-500">Rank</div>
</div>
)}
</div>
)}
{canManageMembership && (
<div className="flex items-center gap-2">
<select
className="px-3 py-2 bg-iron-gray border-0 rounded text-white ring-1 ring-inset ring-charcoal-outline focus:ring-2 focus:ring-primary-blue transition-all duration-150 text-sm"
value={role}
onChange={(e) =>
onChangeRole?.(driver.id, e.target.value as TeamRole)
}
>
<option value="driver">Driver</option>
<option value="manager">Manager</option>
</select>
<button
onClick={() => onRemoveMember?.(driver.id)}
className="px-3 py-2 bg-danger-red/20 hover:bg-danger-red/30 text-danger-red rounded text-sm font-medium transition-colors"
>
Remove
</button>
</div>
)}
</div>
);
})}
</div>
{memberships.length === 0 && (
<div className="text-center py-8 text-gray-400">No team members yet.</div>
)}
</Card>
);
}