di usage in website

This commit is contained in:
2026-01-06 19:36:03 +01:00
parent 589b55a87e
commit e589c30bf8
191 changed files with 6367 additions and 4253 deletions

View File

@@ -3,7 +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 { useCreateTeam } from '@/hooks/team';
import { useRouter } from 'next/navigation';
import { useState } from 'react';
@@ -14,14 +14,13 @@ interface CreateTeamFormProps {
export default function CreateTeamForm({ onCancel, onSuccess }: CreateTeamFormProps) {
const router = useRouter();
const { teamService } = useServices();
const createTeamMutation = useCreateTeam();
const [formData, setFormData] = useState({
name: '',
tag: '',
description: '',
});
const [errors, setErrors] = useState<Record<string, string>>({});
const [submitting, setSubmitting] = useState(false);
const currentDriverId = useEffectiveDriverId();
const validateForm = () => {
@@ -56,26 +55,26 @@ export default function CreateTeamForm({ onCancel, onSuccess }: CreateTeamFormPr
return;
}
setSubmitting(true);
try {
const result = await teamService.createTeam({
createTeamMutation.mutate(
{
name: formData.name,
tag: formData.tag.toUpperCase(),
description: formData.description,
});
const teamId = result.id;
if (onSuccess) {
onSuccess(teamId);
} else {
router.push(`/teams/${teamId}`);
},
{
onSuccess: (result) => {
const teamId = result.id;
if (onSuccess) {
onSuccess(teamId);
} else {
router.push(`/teams/${teamId}`);
}
},
onError: (error) => {
alert(error instanceof Error ? error.message : 'Failed to create team');
},
}
} catch (error) {
alert(error instanceof Error ? error.message : 'Failed to create team');
setSubmitting(false);
}
);
};
return (
@@ -89,7 +88,7 @@ export default function CreateTeamForm({ onCancel, onSuccess }: CreateTeamFormPr
value={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
placeholder="Enter team name..."
disabled={submitting}
disabled={createTeamMutation.isPending}
/>
{errors.name && (
<p className="text-danger-red text-xs mt-1">{errors.name}</p>
@@ -106,7 +105,7 @@ export default function CreateTeamForm({ onCancel, onSuccess }: CreateTeamFormPr
onChange={(e) => setFormData({ ...formData, tag: e.target.value.toUpperCase() })}
placeholder="e.g., APEX"
maxLength={4}
disabled={submitting}
disabled={createTeamMutation.isPending}
/>
<p className="text-xs text-gray-500 mt-1">Max 4 characters</p>
{errors.tag && (
@@ -124,7 +123,7 @@ export default function CreateTeamForm({ onCancel, onSuccess }: CreateTeamFormPr
value={formData.description}
onChange={(e) => setFormData({ ...formData, description: e.target.value })}
placeholder="Describe your team's goals and racing style..."
disabled={submitting}
disabled={createTeamMutation.isPending}
/>
{errors.description && (
<p className="text-danger-red text-xs mt-1">{errors.description}</p>
@@ -150,17 +149,17 @@ export default function CreateTeamForm({ onCancel, onSuccess }: CreateTeamFormPr
<Button
type="submit"
variant="primary"
disabled={submitting}
disabled={createTeamMutation.isPending}
className="flex-1"
>
{submitting ? 'Creating Team...' : 'Create Team'}
{createTeamMutation.isPending ? 'Creating Team...' : 'Create Team'}
</Button>
{onCancel && (
<Button
type="button"
variant="secondary"
onClick={onCancel}
disabled={submitting}
disabled={createTeamMutation.isPending}
>
Cancel
</Button>
@@ -168,4 +167,4 @@ export default function CreateTeamForm({ onCancel, onSuccess }: CreateTeamFormPr
</div>
</form>
);
}
}

View File

@@ -2,18 +2,8 @@
import Button from '@/components/ui/Button';
import { useEffectiveDriverId } from '@/hooks/useEffectiveDriverId';
import { useEffect, useState } from 'react';
import { useServices } from '@/lib/services/ServiceProvider';
type TeamMembershipStatus = 'active' | 'pending' | 'inactive';
interface TeamMembership {
teamId: string;
driverId: string;
role: 'owner' | 'manager' | 'driver';
status: TeamMembershipStatus;
joinedAt: Date | string;
}
import { useTeamMembership, useJoinTeam, useLeaveTeam } from '@/hooks/team';
import { useState } from 'react';
interface JoinTeamButtonProps {
teamId: string;
@@ -26,76 +16,63 @@ export default function JoinTeamButton({
requiresApproval = false,
onUpdate,
}: JoinTeamButtonProps) {
const [loading, setLoading] = useState(false);
const currentDriverId = useEffectiveDriverId();
const [membership, setMembership] = useState<TeamMembership | null>(null);
const { teamService, teamJoinService } = useServices();
const [showConfirmation, setShowConfirmation] = useState(false);
useEffect(() => {
const load = async () => {
try {
const m = await teamService.getMembership(teamId, currentDriverId);
setMembership(m as TeamMembership | null);
} catch (error) {
console.error('Failed to load membership:', error);
}
};
void load();
}, [teamId, currentDriverId, teamService]);
// Use hooks for data fetching
const { data: membership, isLoading: loadingMembership } = useTeamMembership(teamId, currentDriverId || '');
const handleJoin = async () => {
setLoading(true);
try {
if (requiresApproval) {
const existing = await teamService.getMembership(teamId, currentDriverId);
if (existing) {
throw new Error('Already a member or have a pending request');
}
// Note: Team join request functionality would need to be added to teamService
// For now, we'll use a placeholder
console.log('Saving join request:', {
id: `team-request-${Date.now()}`,
teamId,
driverId: currentDriverId,
requestedAt: new Date(),
});
alert('Join request sent! Wait for team approval.');
} else {
// Note: Team join functionality would need to be added to teamService
// For now, we'll use a placeholder
console.log('Joining team:', { teamId, driverId: currentDriverId });
alert('Successfully joined team!');
}
// Use hooks for mutations
const joinTeamMutation = useJoinTeam({
onSuccess: () => {
onUpdate?.();
} catch (error) {
alert(error instanceof Error ? error.message : 'Failed to join team');
} finally {
setLoading(false);
},
});
const leaveTeamMutation = useLeaveTeam({
onSuccess: () => {
onUpdate?.();
setShowConfirmation(false);
},
});
const handleJoin = () => {
if (!currentDriverId) {
alert('Please log in to join a team');
return;
}
joinTeamMutation.mutate({
teamId,
driverId: currentDriverId,
requiresApproval,
});
};
const handleLeave = async () => {
const handleLeave = () => {
if (!currentDriverId) {
alert('Please log in to leave a team');
return;
}
if (!confirm('Are you sure you want to leave this team?')) {
return;
}
setLoading(true);
try {
// Note: Leave team functionality would need to be added to teamService
// For now, we'll use a placeholder
console.log('Leaving team:', { teamId, driverId: currentDriverId });
alert('Successfully left team');
onUpdate?.();
} catch (error) {
alert(error instanceof Error ? error.message : 'Failed to leave team');
} finally {
setLoading(false);
}
leaveTeamMutation.mutate({
teamId,
driverId: currentDriverId,
});
};
// Loading state
if (loadingMembership) {
return (
<Button variant="primary" disabled>
Loading...
</Button>
);
}
// Already a member
if (membership && membership.status === 'active') {
if (membership && membership.isActive) {
if (membership.role === 'owner') {
return (
<Button variant="secondary" disabled>
@@ -108,9 +85,9 @@ export default function JoinTeamButton({
<Button
variant="danger"
onClick={handleLeave}
disabled={loading}
disabled={leaveTeamMutation.isPending}
>
{loading ? 'Leaving...' : 'Leave Team'}
{leaveTeamMutation.isPending ? 'Leaving...' : 'Leave Team'}
</Button>
);
}
@@ -120,9 +97,9 @@ export default function JoinTeamButton({
<Button
variant="primary"
onClick={handleJoin}
disabled={loading}
disabled={joinTeamMutation.isPending || !currentDriverId}
>
{loading
{joinTeamMutation.isPending
? 'Processing...'
: requiresApproval
? 'Request to Join'

View File

@@ -1,14 +1,12 @@
'use client';
import { useState, useEffect } from 'react';
import { useState } from 'react';
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 { DriverViewModel } from '@/lib/view-models/DriverViewModel';
import { useTeamJoinRequests, useUpdateTeam, useApproveJoinRequest, useRejectJoinRequest } from '@/hooks/team';
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: Pick<TeamDetailsViewModel, 'id' | 'name' | 'tag' | 'description' | 'ownerId'>;
@@ -16,10 +14,6 @@ interface TeamAdminProps {
}
export default function TeamAdmin({ team, onUpdate }: TeamAdminProps) {
const { teamJoinService, teamService } = useServices();
const [joinRequests, setJoinRequests] = useState<TeamJoinRequestViewModel[]>([]);
const [requestDrivers, setRequestDrivers] = useState<Record<string, DriverViewModel>>({});
const [loading, setLoading] = useState(true);
const [editMode, setEditMode] = useState(false);
const [editedTeam, setEditedTeam] = useState({
name: team.name,
@@ -27,60 +21,63 @@ export default function TeamAdmin({ team, onUpdate }: TeamAdminProps) {
description: team.description,
});
useEffect(() => {
const load = async () => {
setLoading(true);
try {
// 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);
}
};
// Use hooks for data fetching
const { data: joinRequests = [], isLoading: loading } = useTeamJoinRequests(
team.id,
team.ownerId,
true
);
void load();
}, [team.id, team.name, team.tag, team.description, team.ownerId]);
// Use hooks for mutations
const updateTeamMutation = useUpdateTeam({
onSuccess: () => {
setEditMode(false);
onUpdate();
},
onError: (error) => {
alert(error instanceof Error ? error.message : 'Failed to update team');
},
});
const handleApprove = async (requestId: string) => {
try {
void requestId;
await teamJoinService.approveJoinRequest();
} catch (error) {
const approveJoinRequestMutation = useApproveJoinRequest({
onSuccess: () => {
onUpdate();
},
onError: (error) => {
alert(error instanceof Error ? error.message : 'Failed to approve request');
}
};
},
});
const handleReject = async (requestId: string) => {
try {
void requestId;
await teamJoinService.rejectJoinRequest();
} catch (error) {
const rejectJoinRequestMutation = useRejectJoinRequest({
onSuccess: () => {
onUpdate();
},
onError: (error) => {
alert(error instanceof Error ? error.message : 'Failed to reject request');
}
},
});
const handleApprove = (requestId: string) => {
// Note: The current API doesn't support approving specific requests
// This would need the requestId to be passed to the service
approveJoinRequestMutation.mutate();
};
const handleSaveChanges = async () => {
try {
const result: UpdateTeamViewModel = await teamService.updateTeam(team.id, {
const handleReject = (requestId: string) => {
// Note: The current API doesn't support rejecting specific requests
// This would need the requestId to be passed to the service
rejectJoinRequestMutation.mutate();
};
const handleSaveChanges = () => {
updateTeamMutation.mutate({
teamId: team.id,
input: {
name: editedTeam.name,
tag: editedTeam.tag,
description: editedTeam.description,
});
if (!result.success) {
throw new Error(result.successMessage);
}
setEditMode(false);
onUpdate();
} catch (error) {
alert(error instanceof Error ? error.message : 'Failed to update team');
}
},
});
};
return (
@@ -134,8 +131,8 @@ export default function TeamAdmin({ team, onUpdate }: TeamAdminProps) {
</div>
<div className="flex gap-2">
<Button variant="primary" onClick={handleSaveChanges}>
Save Changes
<Button variant="primary" onClick={handleSaveChanges} disabled={updateTeamMutation.isPending}>
{updateTeamMutation.isPending ? 'Saving...' : 'Save Changes'}
</Button>
<Button
variant="secondary"
@@ -177,9 +174,9 @@ export default function TeamAdmin({ team, onUpdate }: TeamAdminProps) {
<div className="text-center py-8 text-gray-400">Loading requests...</div>
) : joinRequests.length > 0 ? (
<div className="space-y-3">
{joinRequests.map((request) => {
const driver = requestDrivers[request.driverId] ?? null;
{joinRequests.map((request: TeamJoinRequestViewModel) => {
// Note: Driver hydration is not provided by the API response
// so we only display driverId
return (
<div
key={request.requestId}
@@ -187,30 +184,29 @@ export default function TeamAdmin({ team, onUpdate }: TeamAdminProps) {
>
<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 ?? request.driverId).charAt(0)}
{request.driverId.charAt(0)}
</div>
<div className="flex-1">
<h4 className="text-white font-medium">{driver?.name ?? request.driverId}</h4>
<h4 className="text-white font-medium">{request.driverId}</h4>
<p className="text-sm text-gray-400">
{driver?.country ?? 'Unknown'} Requested {new Date(request.requestedAt).toLocaleDateString()}
Requested {new Date(request.requestedAt).toLocaleDateString()}
</p>
{/* Request message is not part of current API contract */}
</div>
</div>
<div className="flex gap-2">
<Button
variant="primary"
onClick={() => handleApprove(request.requestId)}
disabled
disabled={approveJoinRequestMutation.isPending}
>
Approve
{approveJoinRequestMutation.isPending ? 'Approving...' : 'Approve'}
</Button>
<Button
variant="danger"
onClick={() => handleReject(request.requestId)}
disabled
disabled={rejectJoinRequestMutation.isPending}
>
Reject
{rejectJoinRequestMutation.isPending ? 'Rejecting...' : 'Reject'}
</Button>
</div>
</div>
@@ -240,4 +236,4 @@ export default function TeamAdmin({ team, onUpdate }: TeamAdminProps) {
</Card>
</div>
);
}
}

View File

@@ -1,18 +1,17 @@
'use client';
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 { TeamMemberViewModel } from '@/lib/view-models/TeamMemberViewModel';
import { useTeamRoster } from '@/hooks/team';
import { useState } from 'react';
import type { DriverViewModel } from '@/lib/view-models/DriverViewModel';
type TeamRole = 'owner' | 'admin' | 'member';
type TeamMembershipSummary = Pick<TeamMemberViewModel, 'driverId' | 'role' | 'joinedAt'>;
type TeamMemberRole = 'owner' | 'manager' | 'member';
interface TeamRosterProps {
teamId: string;
memberships: TeamMembershipSummary[];
memberships: any[];
isAdmin: boolean;
onRemoveMember?: (driverId: string) => void;
onChangeRole?: (driverId: string, newRole: TeamRole) => void;
@@ -25,38 +24,10 @@ export default function TeamRoster({
onRemoveMember,
onChangeRole,
}: TeamRosterProps) {
const { teamService, driverService } = useServices();
const [teamMembers, setTeamMembers] = useState<any[]>([]);
const [loading, setLoading] = useState(true);
const [sortBy, setSortBy] = useState<'role' | 'rating' | 'name'>('rating');
useEffect(() => {
const load = async () => {
setLoading(true);
try {
// Get driver details for each membership
const membersWithDetails = await Promise.all(
memberships.map(async (m) => {
const driver = await driverService.findById(m.driverId);
return {
driver: driver || { id: m.driverId, name: 'Unknown Driver', country: 'Unknown', position: 'N/A', races: '0', impressions: '0', team: 'None' },
role: m.role,
joinedAt: m.joinedAt,
rating: null, // DriverDTO doesn't include rating
overallRank: null, // DriverDTO doesn't include overallRank
};
})
);
setTeamMembers(membersWithDetails);
} catch (error) {
console.error('Failed to load team roster:', error);
} finally {
setLoading(false);
}
};
void load();
}, [memberships, teamService, driverService]);
// Use hook for data fetching
const { data: teamMembers = [], isLoading: loading } = useTeamRoster(memberships);
const getRoleBadgeColor = (role: TeamRole) => {
switch (role) {
@@ -69,15 +40,17 @@ export default function TeamRoster({
}
};
const getRoleLabel = (role: TeamRole) => {
return role.charAt(0).toUpperCase() + role.slice(1);
const getRoleLabel = (role: TeamRole | TeamMemberRole) => {
// Convert manager to admin for display
const displayRole = role === 'manager' ? 'admin' : role;
return displayRole.charAt(0).toUpperCase() + displayRole.slice(1);
};
function getRoleOrder(role: TeamRole): number {
function getRoleOrder(role: TeamMemberRole): number {
switch (role) {
case 'owner':
return 0;
case 'admin':
case 'manager':
return 1;
case 'member':
return 2;
@@ -145,6 +118,8 @@ export default function TeamRoster({
{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 (
@@ -153,7 +128,7 @@ export default function TeamRoster({
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}
driver={driver as DriverViewModel}
href={`/drivers/${driver.id}?from=team&teamId=${teamId}`}
contextLabel={getRoleLabel(role)}
meta={
@@ -185,7 +160,7 @@ export default function TeamRoster({
<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}
value={displayRole}
onChange={(e) =>
onChangeRole?.(driver.id, e.target.value as TeamRole)
}
@@ -212,4 +187,4 @@ export default function TeamRoster({
)}
</Card>
);
}
}

View File

@@ -1,8 +1,7 @@
'use client';
import { useState, useEffect } from 'react';
import Card from '@/components/ui/Card';
import { useServices } from '@/lib/services/ServiceProvider';
import { useTeamStandings } from '@/hooks/team';
interface TeamStandingsProps {
teamId: string;
@@ -10,32 +9,7 @@ interface TeamStandingsProps {
}
export default function TeamStandings({ teamId, leagues }: TeamStandingsProps) {
const { leagueService } = useServices();
const [standings, setStandings] = useState<any[]>([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
const load = async () => {
try {
// For demo purposes, create mock standings
const mockStandings = leagues.map(leagueId => ({
leagueId,
leagueName: `League ${leagueId}`,
position: Math.floor(Math.random() * 10) + 1,
points: Math.floor(Math.random() * 100),
wins: Math.floor(Math.random() * 5),
racesCompleted: Math.floor(Math.random() * 10),
}));
setStandings(mockStandings);
} catch (error) {
console.error('Failed to load standings:', error);
} finally {
setLoading(false);
}
};
void load();
}, [teamId, leagues]);
const { data: standings = [], isLoading: loading } = useTeamStandings(teamId, leagues);
if (loading) {
return (
@@ -50,7 +24,7 @@ export default function TeamStandings({ teamId, leagues }: TeamStandingsProps) {
<h3 className="text-xl font-semibold text-white mb-6">League Standings</h3>
<div className="space-y-4">
{standings.map((standing) => (
{standings.map((standing: any) => (
<div
key={standing.leagueId}
className="p-4 rounded-lg bg-deep-graphite border border-charcoal-outline"