wip
This commit is contained in:
205
apps/website/components/teams/TeamRoster.tsx
Normal file
205
apps/website/components/teams/TeamRoster.tsx
Normal file
@@ -0,0 +1,205 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import Card from '@/components/ui/Card';
|
||||
import { getDriverRepository, getDriverStats } from '@/lib/di-container';
|
||||
import { EntityMappers, DriverDTO } from '@gridpilot/racing/application/mappers/EntityMappers';
|
||||
import { TeamMembership, TeamRole } from '@gridpilot/racing/application';
|
||||
|
||||
interface TeamRosterProps {
|
||||
teamId: string;
|
||||
memberships: TeamMembership[];
|
||||
isAdmin: boolean;
|
||||
onRemoveMember?: (driverId: string) => void;
|
||||
onChangeRole?: (driverId: string, newRole: TeamRole) => void;
|
||||
}
|
||||
|
||||
export default function TeamRoster({
|
||||
teamId,
|
||||
memberships,
|
||||
isAdmin,
|
||||
onRemoveMember,
|
||||
onChangeRole,
|
||||
}: TeamRosterProps) {
|
||||
const [drivers, setDrivers] = useState<Record<string, DriverDTO>>({});
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [sortBy, setSortBy] = useState<'role' | 'rating' | 'name'>('rating');
|
||||
|
||||
useEffect(() => {
|
||||
const loadDrivers = async () => {
|
||||
const driverRepo = getDriverRepository();
|
||||
const allDrivers = await driverRepo.findAll();
|
||||
const driverMap: Record<string, DriverDTO> = {};
|
||||
|
||||
for (const membership of memberships) {
|
||||
const driver = allDrivers.find(d => d.id === membership.driverId);
|
||||
if (driver) {
|
||||
const dto = EntityMappers.toDriverDTO(driver);
|
||||
if (dto) {
|
||||
driverMap[membership.driverId] = dto;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
setDrivers(driverMap);
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
loadDrivers();
|
||||
}, [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 sortedMemberships = [...memberships].sort((a, b) => {
|
||||
switch (sortBy) {
|
||||
case 'rating': {
|
||||
const statsA = getDriverStats(a.driverId);
|
||||
const statsB = getDriverStats(b.driverId);
|
||||
return (statsB?.rating || 0) - (statsA?.rating || 0);
|
||||
}
|
||||
case 'role': {
|
||||
const roleOrder = { owner: 0, manager: 1, driver: 2 };
|
||||
return roleOrder[a.role] - roleOrder[b.role];
|
||||
}
|
||||
case 'name': {
|
||||
const driverA = drivers[a.driverId];
|
||||
const driverB = drivers[b.driverId];
|
||||
return (driverA?.name || '').localeCompare(driverB?.name || '');
|
||||
}
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
});
|
||||
|
||||
const teamAverageRating = memberships.length > 0
|
||||
? Math.round(
|
||||
memberships.reduce((sum, m) => {
|
||||
const stats = getDriverStats(m.driverId);
|
||||
return sum + (stats?.rating || 0);
|
||||
}, 0) / memberships.length
|
||||
)
|
||||
: 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">
|
||||
{sortedMemberships.map((membership) => {
|
||||
const driver = drivers[membership.driverId];
|
||||
const driverStats = getDriverStats(membership.driverId);
|
||||
if (!driver) return null;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={membership.driverId}
|
||||
className="flex items-center justify-between p-4 rounded-lg bg-deep-graphite border border-charcoal-outline hover:border-charcoal-outline/60 transition-colors"
|
||||
>
|
||||
<div className="flex items-center gap-4 flex-1">
|
||||
<div className="w-12 h-12 rounded-full bg-primary-blue/20 flex items-center justify-center text-lg font-bold text-white">
|
||||
{driver.name.charAt(0)}
|
||||
</div>
|
||||
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<h4 className="text-white font-medium">{driver.name}</h4>
|
||||
<span className={`px-2 py-1 rounded text-xs font-medium ${getRoleBadgeColor(membership.role)}`}>
|
||||
{getRoleLabel(membership.role)}
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-sm text-gray-400">
|
||||
{driver.country} • Joined {new Date(membership.joinedAt).toLocaleDateString()}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{driverStats && (
|
||||
<div className="flex items-center gap-6 text-center">
|
||||
<div>
|
||||
<div className="text-lg font-bold text-primary-blue">{driverStats.rating}</div>
|
||||
<div className="text-xs text-gray-400">Rating</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-sm text-gray-300">#{driverStats.overallRank}</div>
|
||||
<div className="text-xs text-gray-500">Rank</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{isAdmin && membership.role !== 'owner' && (
|
||||
<div className="flex items-center gap-2">
|
||||
{onChangeRole && (
|
||||
<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={membership.role}
|
||||
onChange={(e) => onChangeRole(membership.driverId, e.target.value as TeamRole)}
|
||||
>
|
||||
<option value="driver">Driver</option>
|
||||
<option value="manager">Manager</option>
|
||||
</select>
|
||||
)}
|
||||
|
||||
{onRemoveMember && (
|
||||
<button
|
||||
onClick={() => onRemoveMember(membership.driverId)}
|
||||
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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user