website cleanup

This commit is contained in:
2025-12-24 21:44:58 +01:00
parent 9b683a59d3
commit d78854a4c6
277 changed files with 6141 additions and 2693 deletions

View File

@@ -3,6 +3,7 @@
import Button from '@/components/ui/Button';
import Input from '@/components/ui/Input';
import { useEffectiveDriverId } from '@/hooks/useEffectiveDriverId';
import { useServices } from '@/lib/services/ServiceProvider';
import { useRouter } from 'next/navigation';
import { useState } from 'react';
@@ -13,6 +14,7 @@ interface CreateTeamFormProps {
export default function CreateTeamForm({ onCancel, onSuccess }: CreateTeamFormProps) {
const router = useRouter();
const { teamService } = useServices();
const [formData, setFormData] = useState({
name: '',
tag: '',
@@ -57,16 +59,13 @@ export default function CreateTeamForm({ onCancel, onSuccess }: CreateTeamFormPr
setSubmitting(true);
try {
const useCase = getCreateTeamUseCase();
const result = await useCase.execute({
const result = await teamService.createTeam({
name: formData.name,
tag: formData.tag.toUpperCase(),
description: formData.description,
ownerId: currentDriverId,
leagues: [],
});
const teamId = result.team.id;
const teamId = result.id;
if (onSuccess) {
onSuccess(teamId);
@@ -169,4 +168,4 @@ export default function CreateTeamForm({ onCancel, onSuccess }: CreateTeamFormPr
</div>
</form>
);
}
}

View File

@@ -4,6 +4,7 @@ import type { TeamSummaryViewModel } from '@/lib/view-models/TeamSummaryViewMode
import TeamCard from './TeamCard';
type SkillLevel = 'pro' | 'advanced' | 'intermediate' | 'beginner';
type TeamSpecialization = 'endurance' | 'sprint' | 'mixed';
interface SkillLevelConfig {
id: SkillLevel;
@@ -35,6 +36,13 @@ export default function SkillLevelSection({
if (teams.length === 0) return null;
const specialization = (teamSpecialization: string | undefined): TeamSpecialization | undefined => {
if (teamSpecialization === 'endurance' || teamSpecialization === 'sprint' || teamSpecialization === 'mixed') {
return teamSpecialization;
}
return undefined;
};
return (
<div className="mb-8">
{/* Section Header */}
@@ -81,12 +89,12 @@ export default function SkillLevelSection({
name={team.name}
description={team.description ?? ''}
memberCount={team.memberCount}
rating={team.rating}
rating={null}
totalWins={team.totalWins}
totalRaces={team.totalRaces}
performanceLevel={team.performanceLevel}
performanceLevel={team.performanceLevel as SkillLevel}
isRecruiting={team.isRecruiting}
specialization={team.specialization}
specialization={specialization(team.specialization)}
region={team.region ?? ''}
languages={team.languages}
onClick={() => onTeamClick(team.id)}
@@ -95,4 +103,4 @@ export default function SkillLevelSection({
</div>
</div>
);
}
}

View File

@@ -5,21 +5,19 @@ import Card from '@/components/ui/Card';
import Button from '@/components/ui/Button';
import Input from '@/components/ui/Input';
import { useServices } from '@/lib/services/ServiceProvider';
import type { DriverDTO } from '@core/racing/application/dto/DriverDTO';
import type { DriverDTO } from '@/lib/types/generated/DriverDTO';
import type { TeamJoinRequestViewModel } from '@/lib/view-models/TeamJoinRequestViewModel';
import type { TeamDetailsViewModel } from '@/lib/view-models/TeamDetailsViewModel';
import type { UpdateTeamViewModel } from '@/lib/view-models/UpdateTeamViewModel';
interface TeamAdminProps {
team: {
id: string;
name: string;
tag: string;
description: string;
ownerId: string;
};
team: Pick<TeamDetailsViewModel, 'id' | 'name' | 'tag' | 'description' | 'ownerId'>;
onUpdate: () => void;
}
export default function TeamAdmin({ team, onUpdate }: TeamAdminProps) {
const [joinRequests, setJoinRequests] = useState<TeamAdminJoinRequestViewModel[]>([]);
const { teamJoinService, teamService } = useServices();
const [joinRequests, setJoinRequests] = useState<TeamJoinRequestViewModel[]>([]);
const [requestDrivers, setRequestDrivers] = useState<Record<string, DriverDTO>>({});
const [loading, setLoading] = useState(true);
const [editMode, setEditMode] = useState(false);
@@ -33,22 +31,13 @@ export default function TeamAdmin({ team, onUpdate }: TeamAdminProps) {
const load = async () => {
setLoading(true);
try {
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> = {};
for (const request of viewModel.requests) {
if (request.driver) {
driversById[request.driverId] = request.driver;
}
}
setRequestDrivers(driversById);
// Current build only supports read-only join requests. Driver hydration is
// not provided by the API response, so we only display driverId.
const currentUserId = team.ownerId;
const isOwner = true;
const requests = await teamJoinService.getJoinRequests(team.id, currentUserId, isOwner);
setJoinRequests(requests);
setRequestDrivers({});
} finally {
setLoading(false);
}
@@ -59,16 +48,8 @@ export default function TeamAdmin({ team, onUpdate }: TeamAdminProps) {
const handleApprove = async (requestId: string) => {
try {
const updated = await approveTeamJoinRequestAndReload(requestId, team.id);
setJoinRequests(updated);
const driversById: Record<string, DriverDTO> = {};
for (const request of updated) {
if (request.driver) {
driversById[request.driverId] = request.driver;
}
}
setRequestDrivers(driversById);
onUpdate();
void requestId;
await teamJoinService.approveJoinRequest();
} catch (error) {
alert(error instanceof Error ? error.message : 'Failed to approve request');
}
@@ -76,15 +57,8 @@ export default function TeamAdmin({ team, onUpdate }: TeamAdminProps) {
const handleReject = async (requestId: string) => {
try {
const updated = await rejectTeamJoinRequestAndReload(requestId, team.id);
setJoinRequests(updated);
const driversById: Record<string, DriverDTO> = {};
for (const request of updated) {
if (request.driver) {
driversById[request.driverId] = request.driver;
}
}
setRequestDrivers(driversById);
void requestId;
await teamJoinService.rejectJoinRequest();
} catch (error) {
alert(error instanceof Error ? error.message : 'Failed to reject request');
}
@@ -92,13 +66,16 @@ export default function TeamAdmin({ team, onUpdate }: TeamAdminProps) {
const handleSaveChanges = async () => {
try {
await updateTeamDetails({
teamId: team.id,
const result: UpdateTeamViewModel = await teamService.updateTeam(team.id, {
name: editedTeam.name,
tag: editedTeam.tag,
description: editedTeam.description,
updatedByDriverId: team.ownerId,
});
if (!result.success) {
throw new Error(result.successMessage);
}
setEditMode(false);
onUpdate();
} catch (error) {
@@ -201,40 +178,37 @@ export default function TeamAdmin({ team, onUpdate }: TeamAdminProps) {
) : joinRequests.length > 0 ? (
<div className="space-y-3">
{joinRequests.map((request) => {
const driver = requestDrivers[request.driverId] ?? request.driver;
if (!driver) return null;
const driver = requestDrivers[request.driverId] ?? null;
return (
<div
key={request.id}
key={request.requestId}
className="flex items-center justify-between p-4 rounded-lg bg-deep-graphite border border-charcoal-outline"
>
<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)}
{(driver?.name ?? request.driverId).charAt(0)}
</div>
<div className="flex-1">
<h4 className="text-white font-medium">{driver.name}</h4>
<h4 className="text-white font-medium">{driver?.name ?? request.driverId}</h4>
<p className="text-sm text-gray-400">
{driver.country} Requested {new Date(request.requestedAt).toLocaleDateString()}
{driver?.country ?? 'Unknown'} Requested {new Date(request.requestedAt).toLocaleDateString()}
</p>
{request.message && (
<p className="text-sm text-gray-300 mt-1 italic">
"{request.message}"
</p>
)}
{/* Request message is not part of current API contract */}
</div>
</div>
<div className="flex gap-2">
<Button
variant="primary"
onClick={() => handleApprove(request.id)}
onClick={() => handleApprove(request.requestId)}
disabled
>
Approve
</Button>
<Button
variant="danger"
onClick={() => handleReject(request.id)}
onClick={() => handleReject(request.requestId)}
disabled
>
Reject
</Button>
@@ -266,4 +240,4 @@ export default function TeamAdmin({ team, onUpdate }: TeamAdminProps) {
</Card>
</div>
);
}
}

View File

@@ -17,6 +17,8 @@ import {
Languages,
} from 'lucide-react';
import { useServices } from '@/lib/services/ServiceProvider';
interface TeamCardProps {
id: string;
name: string;
@@ -77,8 +79,8 @@ export default function TeamCard({
languages,
onClick,
}: TeamCardProps) {
const imageService = getImageService();
const logoUrl = logo || imageService.getTeamLogo(id);
const { mediaService } = useServices();
const logoUrl = logo || mediaService.getTeamLogo(id);
const performanceBadge = getPerformanceBadge(performanceLevel);
const specializationBadge = getSpecializationBadge(specialization);
@@ -206,4 +208,4 @@ export default function TeamCard({
</div>
</div>
);
}
}

View File

@@ -1,5 +1,6 @@
'use client';
import { useServices } from '@/lib/services/ServiceProvider';
import { useRouter } from 'next/navigation';
import Image from 'next/image';
@@ -25,8 +26,8 @@ export default function TeamLadderRow({
totalRaces,
}: TeamLadderRowProps) {
const router = useRouter();
const imageService = getImageService();
const logo = teamLogoUrl ?? imageService.getTeamLogo(teamId);
const { mediaService } = useServices();
const logo = teamLogoUrl ?? mediaService.getTeamLogo(teamId);
const handleClick = () => {
router.push(`/teams/${teamId}`);
@@ -74,4 +75,4 @@ export default function TeamLadderRow({
</td>
</tr>
);
}
}

View File

@@ -161,7 +161,7 @@ export default function TeamLeaderboardPreview({
{/* Rating */}
<div className="text-right">
<p className="text-purple-400 font-mono font-semibold">
{(team as any).rating?.toLocaleString() || '—'}
{'—'}
</p>
<p className="text-xs text-gray-500">Rating</p>
</div>
@@ -172,4 +172,4 @@ export default function TeamLeaderboardPreview({
</div>
</div>
);
}
}

View File

@@ -4,13 +4,11 @@ import { useState, useEffect } from 'react';
import Card from '@/components/ui/Card';
import DriverIdentity from '@/components/drivers/DriverIdentity';
import { useServices } from '@/lib/services/ServiceProvider';
import type { TeamRole } from '@core/racing/domain/types/TeamMembership';
import type { TeamMemberViewModel } from '@/lib/view-models/TeamMemberViewModel';
interface TeamMembershipSummary {
driverId: string;
role: TeamRole;
joinedAt: Date;
}
type TeamRole = 'owner' | 'admin' | 'member';
type TeamMembershipSummary = Pick<TeamMemberViewModel, 'driverId' | 'role' | 'joinedAt'>;
interface TeamRosterProps {
teamId: string;
@@ -64,7 +62,7 @@ export default function TeamRoster({
switch (role) {
case 'owner':
return 'bg-warning-amber/20 text-warning-amber';
case 'manager':
case 'admin':
return 'bg-primary-blue/20 text-primary-blue';
default:
return 'bg-charcoal-outline text-gray-300';
@@ -79,9 +77,9 @@ export default function TeamRoster({
switch (role) {
case 'owner':
return 0;
case 'manager':
case 'admin':
return 1;
case 'driver':
case 'member':
return 2;
default:
return 3;
@@ -192,8 +190,8 @@ export default function TeamRoster({
onChangeRole?.(driver.id, e.target.value as TeamRole)
}
>
<option value="driver">Driver</option>
<option value="manager">Manager</option>
<option value="member">Member</option>
<option value="admin">Admin</option>
</select>
<button
@@ -214,4 +212,4 @@ export default function TeamRoster({
)}
</Card>
);
}
}

View File

@@ -111,7 +111,7 @@ export default function TopThreePodium({ teams, onClick }: TopThreePodiumProps)
<button
key={team.id}
type="button"
onClick={() => onTeamClick(team.id)}
onClick={() => onClick(team.id)}
className="flex flex-col items-center group"
>
{/* Team card */}
@@ -142,7 +142,7 @@ export default function TopThreePodium({ teams, onClick }: TopThreePodiumProps)
{/* Rating */}
<p className={`text-lg md:text-xl font-mono font-bold ${getPositionColor(position)} text-center`}>
{(team as any).rating?.toLocaleString() || '—'}
{'—'}
</p>
{/* Stats row */}
@@ -172,4 +172,4 @@ export default function TopThreePodium({ teams, onClick }: TopThreePodiumProps)
</div>
</div>
);
}
}