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

@@ -4,8 +4,10 @@ import { useState, FormEvent } from 'react';
import { useRouter } from 'next/navigation';
import Input from '../ui/Input';
import Button from '../ui/Button';
import { useServices } from '@/lib/services/ServiceProvider';
import { useCreateLeague } from '@/hooks/league/useCreateLeague';
import { useAuth } from '@/lib/auth/AuthContext';
import { useInject } from '@/lib/di/hooks/useInject';
import { DRIVER_SERVICE_TOKEN } from '@/lib/di/tokens';
interface FormErrors {
name?: string;
@@ -17,7 +19,6 @@ interface FormErrors {
export default function CreateLeagueForm() {
const router = useRouter();
const [loading, setLoading] = useState(false);
const [errors, setErrors] = useState<FormErrors>({});
const [formData, setFormData] = useState({
@@ -51,12 +52,13 @@ export default function CreateLeagueForm() {
};
const { session } = useAuth();
const { driverService, leagueService } = useServices();
const driverService = useInject(DRIVER_SERVICE_TOKEN);
const createLeagueMutation = useCreateLeague();
const handleSubmit = async (e: FormEvent) => {
e.preventDefault();
if (loading) return;
if (createLeagueMutation.isPending) return;
if (!validateForm()) return;
@@ -65,15 +67,12 @@ export default function CreateLeagueForm() {
return;
}
setLoading(true);
try {
// Get current driver
const currentDriver = await driverService.getDriverProfile(session.user.userId);
if (!currentDriver) {
setErrors({ submit: 'No driver profile found. Please create a profile first.' });
setLoading(false);
return;
}
@@ -85,14 +84,13 @@ export default function CreateLeagueForm() {
ownerId: session.user.userId,
};
const result = await leagueService.createLeague(input);
const result = await createLeagueMutation.mutateAsync(input);
router.push(`/leagues/${result.leagueId}`);
router.refresh();
} catch (error) {
setErrors({
submit: error instanceof Error ? error.message : 'Failed to create league'
});
setLoading(false);
}
};
@@ -112,7 +110,7 @@ export default function CreateLeagueForm() {
errorMessage={errors.name}
placeholder="European GT Championship"
maxLength={100}
disabled={loading}
disabled={createLeagueMutation.isPending}
/>
<p className="mt-1 text-xs text-gray-500 text-right">
{formData.name.length}/100
@@ -130,7 +128,7 @@ export default function CreateLeagueForm() {
placeholder="Weekly GT3 racing with professional drivers"
maxLength={500}
rows={4}
disabled={loading}
disabled={createLeagueMutation.isPending}
className="block w-full rounded-md border-0 px-4 py-3 bg-iron-gray text-white shadow-sm ring-1 ring-inset ring-charcoal-outline placeholder:text-gray-500 focus:ring-2 focus:ring-inset focus:ring-primary-blue transition-all duration-150 sm:text-sm sm:leading-6 resize-none"
/>
<p className="mt-1 text-xs text-gray-500 text-right">
@@ -149,7 +147,7 @@ export default function CreateLeagueForm() {
id="pointsSystem"
value={formData.pointsSystem}
onChange={(e) => setFormData({ ...formData, pointsSystem: e.target.value as 'f1-2024' | 'indycar' })}
disabled={loading}
disabled={createLeagueMutation.isPending}
className="block w-full rounded-md border-0 px-4 py-3 bg-iron-gray text-white shadow-sm ring-1 ring-inset ring-charcoal-outline focus:ring-2 focus:ring-inset focus:ring-primary-blue transition-all duration-150 sm:text-sm sm:leading-6"
>
<option value="f1-2024">F1 2024</option>
@@ -170,7 +168,7 @@ export default function CreateLeagueForm() {
errorMessage={errors.sessionDuration}
min={1}
max={240}
disabled={loading}
disabled={createLeagueMutation.isPending}
/>
</div>
@@ -183,10 +181,10 @@ export default function CreateLeagueForm() {
<Button
type="submit"
variant="primary"
disabled={loading}
disabled={createLeagueMutation.isPending}
className="w-full"
>
{loading ? 'Creating League...' : 'Create League'}
{createLeagueMutation.isPending ? 'Creating League...' : 'Create League'}
</Button>
</form>
</>

View File

@@ -3,7 +3,7 @@
import { useEffectiveDriverId } from '@/hooks/useEffectiveDriverId';
import { getMembership } from '@/lib/leagueMembership';
import { useState } from 'react';
import { useServices } from '@/lib/services/ServiceProvider';
import { useLeagueMembershipMutation } from '@/hooks/league/useLeagueMembershipMutation';
import Button from '../ui/Button';
interface JoinLeagueButtonProps {
@@ -18,16 +18,16 @@ export default function JoinLeagueButton({
onMembershipChange,
}: JoinLeagueButtonProps) {
const currentDriverId = useEffectiveDriverId();
const membership = getMembership(leagueId, currentDriverId);
const { leagueMembershipService } = useServices();
const membership = currentDriverId ? getMembership(leagueId, currentDriverId) : null;
const { joinLeague, leaveLeague } = useLeagueMembershipMutation();
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [showConfirmDialog, setShowConfirmDialog] = useState(false);
const [dialogAction, setDialogAction] = useState<'join' | 'leave' | 'request'>('join');
const handleJoin = async () => {
setLoading(true);
if (!currentDriverId) return;
setError(null);
try {
if (isInviteOnly) {
@@ -36,33 +36,30 @@ export default function JoinLeagueButton({
);
}
await leagueMembershipService.joinLeague(leagueId, currentDriverId);
await joinLeague.mutateAsync({ leagueId, driverId: currentDriverId });
onMembershipChange?.();
setShowConfirmDialog(false);
} catch (err) {
setError(err instanceof Error ? err.message : 'Failed to join league');
} finally {
setLoading(false);
}
};
const handleLeave = async () => {
setLoading(true);
if (!currentDriverId) return;
setError(null);
try {
if (membership?.role === 'owner') {
throw new Error('League owner cannot leave the league');
}
await leagueMembershipService.leaveLeague(leagueId, currentDriverId);
await leaveLeague.mutateAsync({ leagueId, driverId: currentDriverId });
onMembershipChange?.();
setShowConfirmDialog(false);
} catch (err) {
setError(err instanceof Error ? err.message : 'Failed to leave league');
} finally {
setLoading(false);
}
};
@@ -93,7 +90,7 @@ export default function JoinLeagueButton({
return 'danger';
};
const isDisabled = membership?.role === 'owner' || loading;
const isDisabled = membership?.role === 'owner' || joinLeague.isPending || leaveLeague.isPending;
return (
<>
@@ -109,7 +106,7 @@ export default function JoinLeagueButton({
disabled={isDisabled}
className="w-full"
>
{loading ? 'Processing...' : getButtonText()}
{(joinLeague.isPending || leaveLeague.isPending) ? 'Processing...' : getButtonText()}
</Button>
{error && (
@@ -142,15 +139,15 @@ export default function JoinLeagueButton({
<Button
variant={dialogAction === 'leave' ? 'danger' : 'primary'}
onClick={dialogAction === 'leave' ? handleLeave : handleJoin}
disabled={loading}
disabled={joinLeague.isPending || leaveLeague.isPending}
className="flex-1"
>
{loading ? 'Processing...' : 'Confirm'}
{(joinLeague.isPending || leaveLeague.isPending) ? 'Processing...' : 'Confirm'}
</Button>
<Button
variant="secondary"
onClick={closeDialog}
disabled={loading}
disabled={joinLeague.isPending || leaveLeague.isPending}
className="flex-1"
>
Cancel

View File

@@ -1,11 +1,9 @@
'use client';
import { Calendar, Award, UserPlus, UserMinus, Shield, Flag, AlertTriangle } from 'lucide-react';
import { useEffect, useState } from 'react';
import { useServices } from '@/lib/services/ServiceProvider';
import type { RaceListItemViewModel } from '@/lib/view-models/RaceListItemViewModel';
import { useLeagueRaces } from '@/hooks/league/useLeagueRaces';
export type LeagueActivity =
export type LeagueActivity =
| { type: 'race_completed'; raceId: string; raceName: string; timestamp: Date }
| { type: 'race_scheduled'; raceId: string; raceName: string; timestamp: Date }
| { type: 'penalty_applied'; penaltyId: string; driverName: string; reason: string; points: number; timestamp: Date }
@@ -32,60 +30,45 @@ function timeAgo(timestamp: Date): string {
}
export default function LeagueActivityFeed({ leagueId, limit = 10 }: LeagueActivityFeedProps) {
const { raceService, driverService } = useServices();
const [activities, setActivities] = useState<LeagueActivity[]>([]);
const [loading, setLoading] = useState(true);
const { data: raceList = [], isLoading } = useLeagueRaces(leagueId);
useEffect(() => {
async function loadActivities() {
try {
const raceList = await raceService.findByLeagueId(leagueId);
const activities: LeagueActivity[] = [];
if (!isLoading && raceList.length > 0) {
const completedRaces = raceList
.filter((r) => r.status === 'completed')
.sort((a, b) => new Date(b.scheduledAt).getTime() - new Date(a.scheduledAt).getTime())
.slice(0, 5);
const completedRaces = raceList
.filter((r) => r.status === 'completed')
.sort((a, b) => new Date(b.scheduledAt).getTime() - new Date(a.scheduledAt).getTime())
.slice(0, 5);
const upcomingRaces = raceList
.filter((r) => r.status === 'scheduled')
.sort((a, b) => new Date(b.scheduledAt).getTime() - new Date(a.scheduledAt).getTime())
.slice(0, 3);
const upcomingRaces = raceList
.filter((r) => r.status === 'scheduled')
.sort((a, b) => new Date(b.scheduledAt).getTime() - new Date(a.scheduledAt).getTime())
.slice(0, 3);
const activityList: LeagueActivity[] = [];
for (const race of completedRaces) {
activityList.push({
type: 'race_completed',
raceId: race.id,
raceName: `${race.track} - ${race.car}`,
timestamp: new Date(race.scheduledAt),
});
}
for (const race of upcomingRaces) {
activityList.push({
type: 'race_scheduled',
raceId: race.id,
raceName: `${race.track} - ${race.car}`,
timestamp: new Date(new Date(race.scheduledAt).getTime() - 7 * 24 * 60 * 60 * 1000), // Simulate schedule announcement
});
}
// Sort all activities by timestamp
activityList.sort((a, b) => b.timestamp.getTime() - a.timestamp.getTime());
setActivities(activityList.slice(0, limit));
} catch (err) {
console.error('Failed to load activities:', err);
} finally {
setLoading(false);
}
for (const race of completedRaces) {
activities.push({
type: 'race_completed',
raceId: race.id,
raceName: `${race.track} - ${race.car}`,
timestamp: new Date(race.scheduledAt),
});
}
loadActivities();
}, [leagueId, limit, raceService, driverService]);
for (const race of upcomingRaces) {
activities.push({
type: 'race_scheduled',
raceId: race.id,
raceName: `${race.track} - ${race.car}`,
timestamp: new Date(new Date(race.scheduledAt).getTime() - 7 * 24 * 60 * 60 * 1000), // Simulate schedule announcement
});
}
if (loading) {
// Sort all activities by timestamp
activities.sort((a, b) => b.timestamp.getTime() - a.timestamp.getTime());
activities.splice(limit); // Limit results
}
if (isLoading) {
return (
<div className="text-center text-gray-400 py-8">
Loading activities...

View File

@@ -2,13 +2,14 @@
import DriverIdentity from '../drivers/DriverIdentity';
import { useEffectiveDriverId } from '../../hooks/useEffectiveDriverId';
import { useServices } from '../../lib/services/ServiceProvider';
import { useInject } from '@/lib/di/hooks/useInject';
import { LEAGUE_MEMBERSHIP_SERVICE_TOKEN, DRIVER_SERVICE_TOKEN } from '@/lib/di/tokens';
import type { LeagueMembership } from '@/lib/types/LeagueMembership';
import type { MembershipRole } from '@/lib/types/MembershipRole';
import { DriverViewModel } from '@/lib/view-models/DriverViewModel';
import { useCallback, useEffect, useState } from 'react';
// Migrated to useServices-based website services; legacy EntityMapper removed.
// Migrated to useInject-based DI; legacy EntityMapper removed.
interface LeagueMembersProps {
leagueId: string;
@@ -28,7 +29,8 @@ export default function LeagueMembers({
const [loading, setLoading] = useState(true);
const [sortBy, setSortBy] = useState<'role' | 'name' | 'date' | 'rating' | 'points' | 'wins'>('rating');
const currentDriverId = useEffectiveDriverId();
const { leagueMembershipService, driverService } = useServices();
const leagueMembershipService = useInject(LEAGUE_MEMBERSHIP_SERVICE_TOKEN);
const driverService = useInject(DRIVER_SERVICE_TOKEN);
const loadMembers = useCallback(async () => {
setLoading(true);

View File

@@ -1,16 +1,16 @@
'use client';
import { useEffectiveDriverId } from '@/hooks/useEffectiveDriverId';
import { useRegisterForRace, useWithdrawFromRace } from '@/hooks/useRaceService';
import { useRegisterForRace } from '@/hooks/race/useRegisterForRace';
import { useWithdrawFromRace } from '@/hooks/race/useWithdrawFromRace';
import { useRouter } from 'next/navigation';
import { useMemo, useState } from 'react';
import type { LeagueScheduleRaceViewModel } from '@/lib/view-models/LeagueScheduleViewModel';
// Shared state components
import { useDataFetching } from '@/components/shared/hooks/useDataFetching';
import { StateContainer } from '@/components/shared/state/StateContainer';
import { EmptyState } from '@/components/shared/state/EmptyState';
import { useServices } from '@/lib/services/ServiceProvider';
import { useLeagueSchedule } from '@/hooks/league/useLeagueSchedule';
import { Calendar } from 'lucide-react';
interface LeagueScheduleProps {
@@ -22,12 +22,8 @@ export default function LeagueSchedule({ leagueId }: LeagueScheduleProps) {
const [filter, setFilter] = useState<'all' | 'upcoming' | 'past'>('upcoming');
const currentDriverId = useEffectiveDriverId();
const { leagueService } = useServices();
const { data: schedule, isLoading, error, retry } = useDataFetching({
queryKey: ['leagueSchedule', leagueId],
queryFn: () => leagueService.getLeagueSchedule(leagueId),
});
const { data: schedule, isLoading, error, retry } = useLeagueSchedule(leagueId);
const registerMutation = useRegisterForRace();
const withdrawMutation = useWithdrawFromRace();

View File

@@ -1,13 +1,16 @@
'use client';
import { Award, DollarSign, Star, X } from 'lucide-react';
import { useCallback, useEffect, useState } from 'react';
import { useState } from 'react';
import PendingSponsorshipRequests, { type PendingRequestDTO } from '../sponsors/PendingSponsorshipRequests';
import Button from '../ui/Button';
import Input from '../ui/Input';
import { useEffectiveDriverId } from '@/hooks/useEffectiveDriverId';
import { useServices } from '@/lib/services/ServiceProvider';
import { useLeagueSeasons } from '@/hooks/league/useLeagueSeasons';
import { useSponsorshipRequests } from '@/hooks/league/useSponsorshipRequests';
import { useInject } from '@/lib/di/hooks/useInject';
import { SPONSORSHIP_SERVICE_TOKEN } from '@/lib/di/tokens';
interface SponsorshipSlot {
tier: 'main' | 'secondary';
@@ -29,7 +32,8 @@ export function LeagueSponsorshipsSection({
readOnly = false
}: LeagueSponsorshipsSectionProps) {
const currentDriverId = useEffectiveDriverId();
const { sponsorshipService, leagueService } = useServices();
const sponsorshipService = useInject(SPONSORSHIP_SERVICE_TOKEN);
const [slots, setSlots] = useState<SponsorshipSlot[]>([
{ tier: 'main', price: 500, isOccupied: false },
{ tier: 'secondary', price: 200, isOccupied: false },
@@ -37,73 +41,21 @@ export function LeagueSponsorshipsSection({
]);
const [editingIndex, setEditingIndex] = useState<number | null>(null);
const [tempPrice, setTempPrice] = useState<string>('');
const [pendingRequests, setPendingRequests] = useState<PendingRequestDTO[]>([]);
const [requestsLoading, setRequestsLoading] = useState(false);
const [seasonId, setSeasonId] = useState<string | undefined>(propSeasonId);
// Load season ID if not provided
useEffect(() => {
async function loadSeasonId() {
if (propSeasonId) {
setSeasonId(propSeasonId);
return;
}
try {
const seasons = await leagueService.getLeagueSeasons(leagueId);
const activeSeason = seasons.find((s) => s.status === 'active') ?? seasons[0];
if (activeSeason) setSeasonId(activeSeason.seasonId);
} catch (err) {
console.error('Failed to load season:', err);
}
}
loadSeasonId();
}, [leagueId, propSeasonId, leagueService]);
const { data: seasons = [], isLoading: seasonsLoading } = useLeagueSeasons(leagueId);
const activeSeason = seasons.find((s) => s.status === 'active') ?? seasons[0];
const seasonId = propSeasonId || activeSeason?.seasonId;
// Load pending sponsorship requests
const loadPendingRequests = useCallback(async () => {
if (!seasonId) return;
setRequestsLoading(true);
try {
const requests = await sponsorshipService.getPendingSponsorshipRequests({
entityType: 'season',
entityId: seasonId,
});
// Convert service view-models to component DTO type (UI-only)
setPendingRequests(
requests.map(
(r): PendingRequestDTO => ({
id: r.id,
sponsorId: r.sponsorId,
sponsorName: r.sponsorName,
sponsorLogo: r.sponsorLogo,
tier: r.tier,
offeredAmount: r.offeredAmount,
currency: r.currency,
formattedAmount: r.formattedAmount,
message: r.message,
createdAt: r.createdAt,
platformFee: r.platformFee,
netAmount: r.netAmount,
}),
),
);
} catch (err) {
console.error('Failed to load pending requests:', err);
} finally {
setRequestsLoading(false);
}
}, [seasonId, sponsorshipService]);
useEffect(() => {
loadPendingRequests();
}, [loadPendingRequests]);
const { data: pendingRequests = [], isLoading: requestsLoading, refetch: refetchRequests } = useSponsorshipRequests('season', seasonId || '');
const handleAcceptRequest = async (requestId: string) => {
if (!currentDriverId) return;
try {
await sponsorshipService.acceptSponsorshipRequest(requestId, currentDriverId);
await loadPendingRequests();
await refetchRequests();
} catch (err) {
console.error('Failed to accept request:', err);
alert(err instanceof Error ? err.message : 'Failed to accept request');
@@ -111,9 +63,11 @@ export function LeagueSponsorshipsSection({
};
const handleRejectRequest = async (requestId: string, reason?: string) => {
if (!currentDriverId) return;
try {
await sponsorshipService.rejectSponsorshipRequest(requestId, currentDriverId, reason);
await loadPendingRequests();
await refetchRequests();
} catch (err) {
console.error('Failed to reject request:', err);
alert(err instanceof Error ? err.message : 'Failed to reject request');

View File

@@ -3,7 +3,7 @@
import React, { useState } from 'react';
import { useRouter } from 'next/navigation';
import Button from '@/components/ui/Button';
import { useServices } from '@/lib/services/ServiceProvider';
import { usePenaltyMutation } from '@/hooks/league/usePenaltyMutation';
import { AlertTriangle, Clock, Flag, Zap } from 'lucide-react';
interface DriverOption {
@@ -41,16 +41,14 @@ export default function QuickPenaltyModal({ raceId, drivers, onClose, preSelecte
const [infractionType, setInfractionType] = useState<string>('');
const [severity, setSeverity] = useState<string>('');
const [notes, setNotes] = useState<string>('');
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const router = useRouter();
const { penaltyService } = useServices();
const penaltyMutation = usePenaltyMutation();
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!selectedRaceId || !selectedDriver || !infractionType || !severity) return;
setLoading(true);
setError(null);
try {
@@ -64,15 +62,14 @@ export default function QuickPenaltyModal({ raceId, drivers, onClose, preSelecte
if (notes.trim()) {
command.notes = notes.trim();
}
await penaltyService.applyPenalty(command);
await penaltyMutation.mutateAsync(command);
// Refresh the page to show updated results
router.refresh();
onClose();
} catch (err) {
setError(err instanceof Error ? err.message : 'Failed to apply penalty');
} finally {
setLoading(false);
}
};
@@ -206,7 +203,7 @@ export default function QuickPenaltyModal({ raceId, drivers, onClose, preSelecte
variant="secondary"
onClick={onClose}
className="flex-1"
disabled={loading}
disabled={penaltyMutation.isPending}
>
Cancel
</Button>
@@ -214,9 +211,9 @@ export default function QuickPenaltyModal({ raceId, drivers, onClose, preSelecte
type="submit"
variant="primary"
className="flex-1"
disabled={loading || !selectedRaceId || !selectedDriver || !infractionType || !severity}
disabled={penaltyMutation.isPending || !selectedRaceId || !selectedDriver || !infractionType || !severity}
>
{loading ? 'Applying...' : 'Apply Penalty'}
{penaltyMutation.isPending ? 'Applying...' : 'Apply Penalty'}
</Button>
</div>
</form>

View File

@@ -1,10 +1,10 @@
'use client';
import { useState, useEffect } from 'react';
import { useState } from 'react';
import { useRouter } from 'next/navigation';
import Button from '../ui/Button';
import Input from '../ui/Input';
import { useServices } from '@/lib/services/ServiceProvider';
import { useAllLeagues } from '@/hooks/league/useAllLeagues';
import type { LeagueSummaryViewModel } from '@/lib/view-models/LeagueSummaryViewModel';
interface ScheduleRaceFormData {
@@ -35,10 +35,7 @@ export default function ScheduleRaceForm({
onCancel
}: ScheduleRaceFormProps) {
const router = useRouter();
const { leagueService, raceService } = useServices();
const [leagues, setLeagues] = useState<LeagueSummaryViewModel[]>([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const { data: leagues = [], isLoading, error } = useAllLeagues();
const [formData, setFormData] = useState<ScheduleRaceFormData>({
leagueId: preSelectedLeagueId || '',
@@ -51,18 +48,6 @@ export default function ScheduleRaceForm({
const [validationErrors, setValidationErrors] = useState<Record<string, string>>({});
useEffect(() => {
const loadLeagues = async () => {
try {
const allLeagues = await leagueService.getAllLeagues();
setLeagues(allLeagues);
} catch (err) {
setError(err instanceof Error ? err.message : 'Failed to load leagues');
}
};
void loadLeagues();
}, [leagueService]);
const validateForm = (): boolean => {
const errors: Record<string, string> = {};
@@ -107,9 +92,6 @@ export default function ScheduleRaceForm({
return;
}
setLoading(true);
setError(null);
try {
// Create race using the race service
// Note: This assumes the race service has a create method
@@ -137,9 +119,8 @@ export default function ScheduleRaceForm({
router.push(`/races/${createdRace.id}`);
}
} catch (err) {
setError(err instanceof Error ? err.message : 'Failed to create race');
} finally {
setLoading(false);
// Error handling is now done through the component state
console.error('Failed to create race:', err);
}
};
@@ -160,7 +141,7 @@ export default function ScheduleRaceForm({
<form onSubmit={handleSubmit} className="space-y-6">
{error && (
<div className="p-4 rounded-lg bg-red-500/10 border border-red-500/30 text-red-400">
{error}
{error.message}
</div>
)}
@@ -310,10 +291,10 @@ export default function ScheduleRaceForm({
<Button
type="submit"
variant="primary"
disabled={loading}
disabled={isLoading}
className="flex-1"
>
{loading ? 'Creating...' : 'Schedule Race'}
{isLoading ? 'Creating...' : 'Schedule Race'}
</Button>
{onCancel && (
@@ -321,7 +302,7 @@ export default function ScheduleRaceForm({
type="button"
variant="secondary"
onClick={onCancel}
disabled={loading}
disabled={isLoading}
>
Cancel
</Button>