This commit is contained in:
2025-12-11 21:06:25 +01:00
parent c49ea2598d
commit ec3ddc3a5c
227 changed files with 3496 additions and 2083 deletions

View File

@@ -5,11 +5,19 @@ import Button from '@/components/ui/Button';
import {
getJoinTeamUseCase,
getLeaveTeamUseCase,
getGetDriverTeamUseCase,
getTeamMembershipRepository,
} from '@/lib/di-container';
import { useEffectiveDriverId } from '@/lib/currentDriver';
import type { TeamMembership } from '@gridpilot/racing';
type TeamMembershipStatus = 'active' | 'pending' | 'inactive';
interface TeamMembership {
teamId: string;
driverId: string;
role: 'owner' | 'manager' | 'driver';
status: TeamMembershipStatus;
joinedAt: Date | string;
}
interface JoinTeamButtonProps {
teamId: string;
@@ -25,25 +33,12 @@ export default function JoinTeamButton({
const [loading, setLoading] = useState(false);
const currentDriverId = useEffectiveDriverId();
const [membership, setMembership] = useState<TeamMembership | null>(null);
const [currentTeamName, setCurrentTeamName] = useState<string | null>(null);
const [currentTeamId, setCurrentTeamId] = useState<string | null>(null);
useEffect(() => {
const load = async () => {
const membershipRepo = getTeamMembershipRepository();
const m = await membershipRepo.getMembership(teamId, currentDriverId);
setMembership(m);
const driverTeamUseCase = getGetDriverTeamUseCase();
await driverTeamUseCase.execute({ driverId: currentDriverId });
const viewModel = driverTeamUseCase.presenter.getViewModel();
if (viewModel.result) {
setCurrentTeamId(viewModel.result.team.id);
setCurrentTeamName(viewModel.result.team.name);
} else {
setCurrentTeamId(null);
setCurrentTeamName(null);
}
setMembership(m as TeamMembership | null);
};
void load();
}, [teamId, currentDriverId]);
@@ -117,15 +112,6 @@ export default function JoinTeamButton({
);
}
// Already on another team
if (currentTeamId && currentTeamId !== teamId) {
return (
<Button variant="secondary" disabled>
Already on {currentTeamName}
</Button>
);
}
// Can join
return (
<Button

View File

@@ -39,7 +39,13 @@ export default function TeamAdmin({ team, onUpdate }: TeamAdminProps) {
const load = async () => {
setLoading(true);
try {
const viewModel = await loadTeamAdminViewModel(team as any);
const viewModel = await loadTeamAdminViewModel({
id: team.id,
name: team.name,
tag: team.tag,
description: team.description,
ownerId: team.ownerId,
});
setJoinRequests(viewModel.requests);
const driversById: Record<string, DriverDTO> = {};

View File

@@ -29,9 +29,9 @@ interface TeamCardProps {
totalRaces?: number;
performanceLevel?: 'beginner' | 'intermediate' | 'advanced' | 'pro';
isRecruiting?: boolean;
specialization?: 'endurance' | 'sprint' | 'mixed';
specialization?: 'endurance' | 'sprint' | 'mixed' | undefined;
region?: string;
languages?: string[];
languages?: string[] | undefined;
leagues?: string[];
onClick?: () => void;
}

View File

@@ -7,8 +7,7 @@ import {
getTeamRosterViewModel,
type TeamRosterViewModel,
} from '@/lib/presenters/TeamRosterPresenter';
type TeamRole = 'owner' | 'manager' | 'driver';
import type { TeamRole } from '@gridpilot/racing/domain/types/TeamMembership';
interface TeamMembershipSummary {
driverId: string;
@@ -39,7 +38,14 @@ export default function TeamRoster({
const load = async () => {
setLoading(true);
try {
const vm = await getTeamRosterViewModel(memberships);
const fullMemberships = memberships.map((m) => ({
teamId,
driverId: m.driverId,
role: m.role,
joinedAt: m.joinedAt,
status: 'active' as const,
}));
const vm = await getTeamRosterViewModel(fullMemberships);
setViewModel(vm);
} finally {
setLoading(false);
@@ -64,6 +70,19 @@ export default function TeamRoster({
return role.charAt(0).toUpperCase() + role.slice(1);
};
function getRoleOrder(role: TeamRole): number {
switch (role) {
case 'owner':
return 0;
case 'manager':
return 1;
case 'driver':
return 2;
default:
return 3;
}
}
const sortedMembers = viewModel
? [...viewModel.members].sort((a, b) => {
switch (sortBy) {
@@ -73,8 +92,7 @@ export default function TeamRoster({
return ratingB - ratingA;
}
case 'role': {
const roleOrder: Record<TeamRole, number> = { owner: 0, manager: 1, driver: 2 };
return roleOrder[a.role] - roleOrder[b.role];
return getRoleOrder(a.role) - getRoleOrder(b.role);
}
case 'name': {
return a.driver.name.localeCompare(b.driver.name);
@@ -110,7 +128,7 @@ export default function TeamRoster({
<label className="text-sm text-gray-400">Sort by:</label>
<select
value={sortBy}
onChange={(e) => setSortBy(e.target.value as any)}
onChange={(e) => setSortBy(e.target.value as typeof sortBy)}
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>