website refactor

This commit is contained in:
2026-01-16 01:32:55 +01:00
parent a98e3e3166
commit b533de8486
23 changed files with 651 additions and 159 deletions

View File

@@ -2,8 +2,8 @@
import { LeagueCard } from '@/components/leagues/LeagueCardWrapper';
import { routes } from '@/lib/routing/RouteConfig';
import { LeagueSummaryViewModelBuilder } from '@/lib/builders/view-models/LeagueSummaryViewModelBuilder';
import type { LeaguesViewData } from '@/lib/view-data/LeaguesViewData';
import type { LeagueSummaryViewModel } from '@/lib/view-models/LeagueSummaryViewModel';
import { Box } from '@/ui/Box';
import { Button } from '@/ui/Button';
import { Card } from '@/ui/Card';
@@ -375,27 +375,7 @@ function LeagueSlider({
hideScrollbar
>
{leagues.map((league) => {
// TODO wtf we have builders for this
// Convert ViewData to ViewModel for LeagueCard
const viewModel: LeagueSummaryViewModel = {
id: league.id,
name: league.name,
description: league.description ?? '',
logoUrl: league.logoUrl,
ownerId: league.ownerId,
createdAt: league.createdAt,
maxDrivers: league.maxDrivers,
usedDriverSlots: league.usedDriverSlots,
maxTeams: league.maxTeams ?? 0,
usedTeamSlots: league.usedTeamSlots ?? 0,
structureSummary: league.structureSummary,
timingSummary: league.timingSummary,
category: league.category ?? undefined,
scoring: league.scoring ? {
...league.scoring,
primaryChampionshipType: league.scoring.primaryChampionshipType as 'driver' | 'team' | 'nations' | 'trophy',
} : undefined,
};
const viewModel = LeagueSummaryViewModelBuilder.build(league);
return (
<Box key={league.id} flexShrink={0} w="320px" h="full">
@@ -621,26 +601,7 @@ export function LeaguesPageClient({
</Box>
<Grid cols={1} mdCols={2} lgCols={3} gap={6}>
{categoryFilteredLeagues.map((league) => {
// Convert ViewData to ViewModel for LeagueCard
const viewModel: LeagueSummaryViewModel = {
id: league.id,
name: league.name,
description: league.description ?? '',
logoUrl: league.logoUrl,
ownerId: league.ownerId,
createdAt: league.createdAt,
maxDrivers: league.maxDrivers,
usedDriverSlots: league.usedDriverSlots,
maxTeams: league.maxTeams ?? 0,
usedTeamSlots: league.usedTeamSlots ?? 0,
structureSummary: league.structureSummary,
timingSummary: league.timingSummary,
category: league.category ?? undefined,
scoring: league.scoring ? {
...league.scoring,
primaryChampionshipType: league.scoring.primaryChampionshipType as 'driver' | 'team' | 'nations' | 'trophy',
} : undefined,
};
const viewModel = LeagueSummaryViewModelBuilder.build(league);
return (
<GridItem key={league.id}>
@@ -677,4 +638,4 @@ export function LeaguesPageClient({
)}
</Container>
);
}
}

View File

@@ -1,6 +1,7 @@
import { notFound } from 'next/navigation';
import { LeagueDetailPageQuery } from '@/lib/page-queries/LeagueDetailPageQuery';
import { LeagueDetailTemplate } from '@/templates/LeagueDetailTemplate';
import { LeagueDetailViewDataBuilder } from '@/lib/builders/view-data/LeagueDetailViewDataBuilder';
import { Text } from '@/ui/Text';
export default async function LeagueLayout({
@@ -42,7 +43,16 @@ export default async function LeagueLayout({
);
}
const viewData = result.unwrap();
const data = result.unwrap();
const viewData = LeagueDetailViewDataBuilder.build({
league: data.league,
owner: null,
scoringConfig: null,
memberships: { members: [] },
races: [],
sponsors: [],
});
// Define tab configuration
const baseTabs = [

View File

@@ -1,7 +1,7 @@
'use client';
import { useState } from 'react';
import { useParams } from 'next/navigation';
import { useParams, useRouter } from 'next/navigation';
import { PageWrapper } from '@/components/shared/state/PageWrapper';
import { LeagueAdminScheduleTemplate } from '@/templates/LeagueAdminScheduleTemplate';
import {
@@ -10,9 +10,13 @@ import {
useLeagueAdminSchedule
} from "@/hooks/league/useLeagueScheduleAdminPageData";
import { useEffectiveDriverId } from "@/hooks/useEffectiveDriverId";
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { useInject } from '@/lib/di/hooks/useInject';
import { LEAGUE_SERVICE_TOKEN } from '@/lib/di/tokens';
import {
publishScheduleAction,
unpublishScheduleAction,
createRaceAction,
updateRaceAction,
deleteRaceAction
} from './actions';
import { Box } from '@/ui/Box';
import { Stack } from '@/ui/Stack';
import { Text } from '@/ui/Text';
@@ -23,10 +27,8 @@ export function LeagueAdminSchedulePageClient() {
const params = useParams();
const leagueId = params.id as string;
const currentDriverId = useEffectiveDriverId() || '';
const queryClient = useQueryClient();
const router = useRouter();
const leagueService = useInject(LEAGUE_SERVICE_TOKEN);
// Form state
const [seasonId, setSeasonId] = useState<string>('');
const [track, setTrack] = useState('');
@@ -34,6 +36,11 @@ export function LeagueAdminSchedulePageClient() {
const [scheduledAtIso, setScheduledAtIso] = useState('');
const [editingRaceId, setEditingRaceId] = useState<string | null>(null);
// Action state
const [isPublishing, setIsPublishing] = useState(false);
const [isSaving, setIsSaving] = useState(false);
const [deletingRaceId, setDeletingRaceId] = useState<string | null>(null);
// Check admin status using domain hook
const { data: isAdmin, isLoading: isAdminLoading } = useLeagueAdminStatus(leagueId, currentDriverId);
@@ -48,62 +55,6 @@ export function LeagueAdminSchedulePageClient() {
// Load schedule using domain hook
const { data: schedule, isLoading: scheduleLoading } = useLeagueAdminSchedule(leagueId, selectedSeasonId, !!isAdmin);
// Mutations
const publishMutation = useMutation({
mutationFn: async () => {
if (!schedule || !selectedSeasonId) return null;
return schedule.published
? await leagueService.unpublishAdminSchedule(leagueId, selectedSeasonId)
: await leagueService.publishAdminSchedule(leagueId, selectedSeasonId);
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['adminSchedule', leagueId, selectedSeasonId] });
},
});
const saveMutation = useMutation({
mutationFn: async () => {
if (!selectedSeasonId || !scheduledAtIso) return null;
if (!editingRaceId) {
return await leagueService.createAdminScheduleRace(leagueId, selectedSeasonId, {
track,
car,
scheduledAtIso,
});
} else {
return await leagueService.updateAdminScheduleRace(leagueId, selectedSeasonId, editingRaceId, {
...(track ? { track } : {}),
...(car ? { car } : {}),
...(scheduledAtIso ? { scheduledAtIso } : {}),
});
}
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['adminSchedule', leagueId, selectedSeasonId] });
// Reset form
setTrack('');
setCar('');
setScheduledAtIso('');
setEditingRaceId(null);
},
});
const deleteMutation = useMutation({
mutationFn: async (raceId: string) => {
return await leagueService.deleteAdminScheduleRace(leagueId, selectedSeasonId, raceId);
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['adminSchedule', leagueId, selectedSeasonId] });
},
});
// Derived states
const isLoading = isAdminLoading || seasonsLoading || scheduleLoading;
const isPublishing = publishMutation.isPending;
const isSaving = saveMutation.isPending;
const isDeleting = deleteMutation.variables || null;
// Handlers
const handleSeasonChange = (newSeasonId: string) => {
setSeasonId(newSeasonId);
@@ -113,13 +64,51 @@ export function LeagueAdminSchedulePageClient() {
setScheduledAtIso('');
};
const handlePublishToggle = () => {
publishMutation.mutate();
const handlePublishToggle = async () => {
if (!schedule || !selectedSeasonId) return;
setIsPublishing(true);
try {
const result = schedule.published
? await unpublishScheduleAction(leagueId, selectedSeasonId)
: await publishScheduleAction(leagueId, selectedSeasonId);
if (result.isOk()) {
router.refresh();
} else {
alert(result.getError());
}
} finally {
setIsPublishing(false);
}
};
const handleAddOrSave = () => {
if (!scheduledAtIso) return;
saveMutation.mutate();
const handleAddOrSave = async () => {
if (!selectedSeasonId || !scheduledAtIso) return;
setIsSaving(true);
try {
const result = !editingRaceId
? await createRaceAction(leagueId, selectedSeasonId, { track, car, scheduledAtIso })
: await updateRaceAction(leagueId, selectedSeasonId, editingRaceId, {
...(track ? { track } : {}),
...(car ? { car } : {}),
...(scheduledAtIso ? { scheduledAtIso } : {}),
});
if (result.isOk()) {
// Reset form
setTrack('');
setCar('');
setScheduledAtIso('');
setEditingRaceId(null);
router.refresh();
} else {
alert(result.getError());
}
} finally {
setIsSaving(false);
}
};
const handleEdit = (raceId: string) => {
@@ -128,15 +117,27 @@ export function LeagueAdminSchedulePageClient() {
if (!race) return;
setEditingRaceId(raceId);
setTrack('');
setCar('');
setTrack(race.track || '');
setCar(race.car || '');
setScheduledAtIso(race.scheduledAt.toISOString());
};
const handleDelete = (raceId: string) => {
const handleDelete = async (raceId: string) => {
if (!selectedSeasonId) return;
const confirmed = window.confirm('Delete this race?');
if (!confirmed) return;
deleteMutation.mutate(raceId);
setDeletingRaceId(raceId);
try {
const result = await deleteRaceAction(leagueId, selectedSeasonId, raceId);
if (result.isOk()) {
router.refresh();
} else {
alert(result.getError());
}
} finally {
setDeletingRaceId(null);
}
};
const handleCancelEdit = () => {
@@ -146,6 +147,9 @@ export function LeagueAdminSchedulePageClient() {
setScheduledAtIso('');
};
// Derived states
const isLoading = isAdminLoading || seasonsLoading || scheduleLoading;
// Prepare template data
const templateData = schedule && seasonsData && selectedSeasonId
? {
@@ -200,7 +204,7 @@ export function LeagueAdminSchedulePageClient() {
editingRaceId={editingRaceId}
isPublishing={isPublishing}
isSaving={isSaving}
isDeleting={isDeleting}
isDeleting={deletingRaceId}
setTrack={setTrack}
setCar={setCar}
setScheduledAtIso={setScheduledAtIso}
@@ -221,4 +225,4 @@ export function LeagueAdminSchedulePageClient() {
}}
/>
);
}
}

View File

@@ -0,0 +1,70 @@
'use server';
import { revalidatePath } from 'next/cache';
import { Result } from '@/lib/contracts/Result';
import { ScheduleAdminMutation } from '@/lib/mutations/leagues/ScheduleAdminMutation';
import { routes } from '@/lib/routing/RouteConfig';
export async function publishScheduleAction(leagueId: string, seasonId: string): Promise<Result<void, string>> {
const mutation = new ScheduleAdminMutation();
const result = await mutation.publishSchedule(leagueId, seasonId);
if (result.isOk()) {
revalidatePath(routes.league.schedule(leagueId));
}
return result;
}
export async function unpublishScheduleAction(leagueId: string, seasonId: string): Promise<Result<void, string>> {
const mutation = new ScheduleAdminMutation();
const result = await mutation.unpublishSchedule(leagueId, seasonId);
if (result.isOk()) {
revalidatePath(routes.league.schedule(leagueId));
}
return result;
}
export async function createRaceAction(
leagueId: string,
seasonId: string,
input: { track: string; car: string; scheduledAtIso: string }
): Promise<Result<void, string>> {
const mutation = new ScheduleAdminMutation();
const result = await mutation.createRace(leagueId, seasonId, input);
if (result.isOk()) {
revalidatePath(routes.league.schedule(leagueId));
}
return result;
}
export async function updateRaceAction(
leagueId: string,
seasonId: string,
raceId: string,
input: Partial<{ track: string; car: string; scheduledAtIso: string }>
): Promise<Result<void, string>> {
const mutation = new ScheduleAdminMutation();
const result = await mutation.updateRace(leagueId, seasonId, raceId, input);
if (result.isOk()) {
revalidatePath(routes.league.schedule(leagueId));
}
return result;
}
export async function deleteRaceAction(leagueId: string, seasonId: string, raceId: string): Promise<Result<void, string>> {
const mutation = new ScheduleAdminMutation();
const result = await mutation.deleteRace(leagueId, seasonId, raceId);
if (result.isOk()) {
revalidatePath(routes.league.schedule(leagueId));
}
return result;
}