website refactor

This commit is contained in:
2026-01-14 13:39:24 +01:00
parent faa4c3309e
commit 8b67295442
28 changed files with 1082 additions and 851 deletions

View File

@@ -1,111 +1,31 @@
import { PageWrapper } from '@/components/shared/state/PageWrapper';
import { LeagueSchedulePageQuery } from '@/lib/page-queries/page-queries/LeagueSchedulePageQuery';
import { LeagueScheduleTemplate } from '@/templates/LeagueScheduleTemplate';
import { PageDataFetcher } from '@/lib/page/PageDataFetcher';
import { LeagueService } from '@/lib/services/leagues/LeagueService';
import { LeaguesApiClient } from '@/lib/api/leagues/LeaguesApiClient';
import { DriversApiClient } from '@/lib/api/drivers/DriversApiClient';
import { SponsorsApiClient } from '@/lib/api/sponsors/SponsorsApiClient';
import { RacesApiClient } from '@/lib/api/races/RacesApiClient';
import { EnhancedErrorReporter } from '@/lib/infrastructure/EnhancedErrorReporter';
import { ConsoleLogger } from '@/lib/infrastructure/logging/ConsoleLogger';
import { getWebsiteApiBaseUrl } from '@/lib/config/apiBaseUrl';
import { notFound } from 'next/navigation';
import { LeagueScheduleViewModel, LeagueScheduleRaceViewModel } from '@/lib/view-models/LeagueScheduleViewModel';
import type { LeagueScheduleDTO } from '@/lib/types/generated/LeagueScheduleDTO';
import type { RaceDTO } from '@/lib/types/generated/RaceDTO';
interface Props {
params: { id: string };
}
function mapRaceDtoToViewModel(race: RaceDTO): LeagueScheduleRaceViewModel {
const scheduledAt = race.date ? new Date(race.date) : new Date(0);
const now = new Date();
const isPast = scheduledAt.getTime() < now.getTime();
const isUpcoming = !isPast;
export default async function LeagueSchedulePage({ params }: Props) {
const leagueId = params.id;
return {
id: race.id,
name: race.name,
scheduledAt,
isPast,
isUpcoming,
status: isPast ? 'completed' : 'scheduled',
track: undefined,
car: undefined,
sessionType: undefined,
isRegistered: undefined,
};
}
function mapScheduleDtoToViewModel(dto: LeagueScheduleDTO): LeagueScheduleViewModel {
const races = dto.races.map(mapRaceDtoToViewModel);
return new LeagueScheduleViewModel(races);
}
export default async function Page({ params }: Props) {
// Validate params
if (!params.id) {
if (!leagueId) {
notFound();
}
// Fetch data using PageDataFetcher.fetchManual for multiple dependencies
const data = await PageDataFetcher.fetchManual(async () => {
// Create dependencies
const baseUrl = getWebsiteApiBaseUrl();
const logger = new ConsoleLogger();
const errorReporter = new EnhancedErrorReporter(logger, {
showUserNotifications: true,
logToConsole: true,
reportToExternal: process.env.NODE_ENV === 'production',
});
const result = await LeagueSchedulePageQuery.execute(leagueId);
// Create API clients
const leaguesApiClient = new LeaguesApiClient(baseUrl, errorReporter, logger);
const driversApiClient = new DriversApiClient(baseUrl, errorReporter, logger);
const sponsorsApiClient = new SponsorsApiClient(baseUrl, errorReporter, logger);
const racesApiClient = new RacesApiClient(baseUrl, errorReporter, logger);
// Create service
const service = new LeagueService(
leaguesApiClient,
driversApiClient,
sponsorsApiClient,
racesApiClient
);
// Fetch data
const result = await service.getLeagueSchedule(params.id);
if (!result) {
throw new Error('League schedule not found');
if (result.isErr()) {
const error = result.getError();
if (error.type === 'notFound') {
notFound();
}
return mapScheduleDtoToViewModel(result);
});
if (!data) {
notFound();
// For serverError, show the template with empty data
return <LeagueScheduleTemplate viewData={{
leagueId,
races: [],
}} />;
}
// Create a wrapper component that passes data to the template
const TemplateWrapper = ({ data }: { data: LeagueScheduleViewModel }) => {
return (
<LeagueScheduleTemplate
data={data}
leagueId={params.id}
/>
);
};
return (
<PageWrapper
data={data}
Template={TemplateWrapper}
loading={{ variant: 'skeleton', message: 'Loading schedule...' }}
errorConfig={{ variant: 'full-screen' }}
empty={{
title: 'Schedule not found',
description: 'The schedule for this league is not available.',
}}
/>
);
return <LeagueScheduleTemplate viewData={result.unwrap()} />;
}

View File

@@ -1,105 +1,45 @@
'use client';
import { LeagueSettingsPageQuery } from '@/lib/page-queries/page-queries/LeagueSettingsPageQuery';
import { LeagueSettingsTemplate } from '@/templates/LeagueSettingsTemplate';
import { notFound } from 'next/navigation';
import { ReadonlyLeagueInfo } from '@/components/leagues/ReadonlyLeagueInfo';
import LeagueOwnershipTransfer from '@/components/leagues/LeagueOwnershipTransfer';
import Card from '@/components/ui/Card';
import { useEffectiveDriverId } from "@/lib/hooks/useEffectiveDriverId";
import { useParams, useRouter } from 'next/navigation';
// Shared state components
import { StateContainer } from '@/components/shared/state/StateContainer';
import { LoadingWrapper } from '@/components/shared/state/LoadingWrapper';
import { useLeagueAdminStatus } from "@/lib/hooks/league/useLeagueAdminStatus";
import { useLeagueSettings } from "@/lib/hooks/league/useLeagueSettings";
import { useInject } from '@/lib/di/hooks/useInject';
import { LEAGUE_SETTINGS_SERVICE_TOKEN } from '@/lib/di/tokens';
import { AlertTriangle, Settings } from 'lucide-react';
export default function LeagueSettingsPage() {
const params = useParams();
const leagueId = params.id as string;
const currentDriverId = useEffectiveDriverId();
const leagueSettingsService = useInject(LEAGUE_SETTINGS_SERVICE_TOKEN);
const router = useRouter();
// Check admin status using DI + React-Query
const { data: isAdmin, isLoading: adminLoading } = useLeagueAdminStatus(leagueId, currentDriverId ?? '');
// Load settings (only if admin) using DI + React-Query
const { data: settings, isLoading: settingsLoading, error, retry } = useLeagueSettings(leagueId, { enabled: !!isAdmin });
const handleTransferOwnership = async (newOwnerId: string) => {
try {
await leagueSettingsService.transferOwnership(leagueId, currentDriverId ?? '', newOwnerId);
router.refresh();
} catch (err) {
throw err; // Let the component handle the error
}
};
// Show loading for admin check
if (adminLoading) {
return <LoadingWrapper variant="full-screen" message="Checking permissions..." />;
}
// Show access denied if not admin
if (!isAdmin) {
return (
<Card>
<div className="text-center py-12">
<div className="w-16 h-16 mx-auto mb-4 rounded-full bg-iron-gray/50 flex items-center justify-center">
<AlertTriangle className="w-8 h-8 text-warning-amber" />
</div>
<h3 className="text-lg font-medium text-white mb-2">Admin Access Required</h3>
<p className="text-sm text-gray-400">
Only league admins can access settings.
</p>
</div>
</Card>
);
}
return (
<StateContainer
data={settings}
isLoading={settingsLoading}
error={error}
retry={retry}
config={{
loading: { variant: 'spinner', message: 'Loading settings...' },
error: { variant: 'full-screen' },
empty: {
icon: Settings,
title: 'No settings available',
description: 'Unable to load league configuration.',
}
}}
>
{(settingsData) => (
<div className="space-y-6">
{/* Header */}
<div className="flex items-center gap-3">
<div className="flex h-12 w-12 items-center justify-center rounded-xl bg-primary-blue/10">
<Settings className="w-6 h-6 text-primary-blue" />
</div>
<div>
<h1 className="text-2xl font-bold text-white">League Settings</h1>
<p className="text-sm text-gray-400">Manage your league configuration</p>
</div>
</div>
{/* READONLY INFORMATION SECTION - Compact */}
<div className="space-y-4">
<ReadonlyLeagueInfo league={settingsData!.league} configForm={settingsData!.config} />
<LeagueOwnershipTransfer
settings={settingsData!}
currentDriverId={currentDriverId ?? ''}
onTransferOwnership={handleTransferOwnership}
/>
</div>
</div>
)}
</StateContainer>
);
interface Props {
params: { id: string };
}
export default async function LeagueSettingsPage({ params }: Props) {
const leagueId = params.id;
if (!leagueId) {
notFound();
}
const result = await LeagueSettingsPageQuery.execute(leagueId);
if (result.isErr()) {
const error = result.getError();
if (error.type === 'notFound') {
notFound();
}
// For serverError, show the template with empty data
return <LeagueSettingsTemplate viewData={{
leagueId,
league: {
id: leagueId,
name: 'Unknown League',
description: 'League information unavailable',
visibility: 'private',
ownerId: 'unknown',
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
},
config: {
maxDrivers: 0,
scoringPresetId: 'unknown',
allowLateJoin: false,
requireApproval: false,
},
}} />;
}
return <LeagueSettingsTemplate viewData={result.unwrap()} />;
}

View File

@@ -1,79 +1,37 @@
'use client';
import { LeagueSponsorshipsPageQuery } from '@/lib/page-queries/page-queries/LeagueSponsorshipsPageQuery';
import { LeagueSponsorshipsTemplate } from '@/templates/LeagueSponsorshipsTemplate';
import { notFound } from 'next/navigation';
import { LeagueSponsorshipsSection } from '@/components/leagues/LeagueSponsorshipsSection';
import { StatefulPageWrapper } from '@/components/shared/state/StatefulPageWrapper';
import { useEffectiveDriverId } from "@/lib/hooks/useEffectiveDriverId";
import { LeagueRoleUtility } from '@/lib/utilities/LeagueRoleUtility';
import { useLeagueSponsorshipsPageData } from "@/lib/hooks/league/useLeagueSponsorshipsPageData";
import { ApiError } from '@/lib/api/base/ApiError';
import { Building } from 'lucide-react';
import { useParams } from 'next/navigation';
interface SponsorshipsData {
league: any;
isAdmin: boolean;
interface Props {
params: { id: string };
}
function SponsorshipsTemplate({ data }: { data: SponsorshipsData }) {
return (
<div className="space-y-6">
{/* Header */}
<div className="flex items-center gap-3">
<div className="flex h-12 w-12 items-center justify-center rounded-xl bg-primary-blue/10">
<Building className="w-6 h-6 text-primary-blue" />
</div>
<div>
<h1 className="text-2xl font-bold text-white">Sponsorships</h1>
<p className="text-sm text-gray-400">Manage sponsorship slots and review requests</p>
</div>
</div>
export default async function LeagueSponsorshipsPage({ params }: Props) {
const leagueId = params.id;
{/* Sponsorships Section */}
<LeagueSponsorshipsSection
leagueId={data.league.id}
readOnly={false}
/>
</div>
);
}
if (!leagueId) {
notFound();
}
export default function LeagueSponsorshipsPage() {
const params = useParams();
const leagueId = params.id as string;
const currentDriverId = useEffectiveDriverId() || '';
const result = await LeagueSponsorshipsPageQuery.execute(leagueId);
// Fetch data using domain hook
const { data, isLoading, error, refetch } = useLeagueSponsorshipsPageData(leagueId, currentDriverId);
if (result.isErr()) {
const error = result.getError();
if (error.type === 'notFound') {
notFound();
}
// For serverError, show the template with empty data
return <LeagueSponsorshipsTemplate viewData={{
leagueId,
league: {
id: leagueId,
name: 'Unknown League',
description: 'League information unavailable',
},
sponsorshipSlots: [],
sponsorshipRequests: [],
}} />;
}
// Transform data for the template
const transformedData: SponsorshipsData | undefined = data?.league && data.membership !== null
? {
league: data.league,
isAdmin: LeagueRoleUtility.isLeagueAdminOrHigherRole(data.membership?.role || 'member'),
}
: undefined;
// Check if user is not admin to show appropriate state
const isNotAdmin = transformedData && !transformedData.isAdmin;
return (
<StatefulPageWrapper
data={transformedData}
isLoading={isLoading}
error={error as ApiError | null}
retry={refetch}
Template={SponsorshipsTemplate}
loading={{ variant: 'skeleton', message: 'Loading sponsorships...' }}
errorConfig={{ variant: 'full-screen' }}
empty={isNotAdmin ? {
icon: Building,
title: 'Admin Access Required',
description: 'Only league admins can manage sponsorships.',
} : {
icon: Building,
title: 'League not found',
description: 'The league may have been deleted or is no longer accessible.',
}}
/>
);
return <LeagueSponsorshipsTemplate viewData={result.unwrap()} />;
}

View File

@@ -1,52 +1,33 @@
'use client';
import { LeagueWalletPageQuery } from '@/lib/page-queries/page-queries/LeagueWalletPageQuery';
import { LeagueWalletTemplate } from '@/templates/LeagueWalletTemplate';
import { notFound } from 'next/navigation';
import { useParams } from 'next/navigation';
import { useLeagueWalletPageData, useLeagueWalletWithdrawal } from "@/lib/hooks/league/useLeagueWalletPageData";
import { PageWrapper } from '@/components/shared/state/PageWrapper';
import { WalletTemplate } from './WalletTemplate';
import { Wallet } from 'lucide-react';
interface Props {
params: { id: string };
}
export default function LeagueWalletPage() {
const params = useParams();
const leagueId = params.id as string;
export default async function LeagueWalletPage({ params }: Props) {
const leagueId = params.id;
// Query for wallet data using domain hook
const { data, isLoading, error, refetch } = useLeagueWalletPageData(leagueId);
if (!leagueId) {
notFound();
}
// Mutation for withdrawal using domain hook
const withdrawMutation = useLeagueWalletWithdrawal(leagueId, data, refetch);
const result = await LeagueWalletPageQuery.execute(leagueId);
// Export handler (placeholder)
const handleExport = () => {
alert('Export functionality coming soon!');
};
if (result.isErr()) {
const error = result.getError();
if (error.type === 'notFound') {
notFound();
}
// For serverError, show the template with empty data
return <LeagueWalletTemplate viewData={{
leagueId,
balance: 0,
currency: 'USD',
transactions: [],
}} />;
}
// Render function for the template
const renderTemplate = (walletData: any) => {
return (
<WalletTemplate
data={walletData}
onWithdraw={(amount) => withdrawMutation.mutate({ amount })}
onExport={handleExport}
mutationLoading={withdrawMutation.isPending}
/>
);
};
return (
<PageWrapper
data={data}
isLoading={isLoading}
error={error}
retry={refetch}
Template={({ data }) => renderTemplate(data)}
loading={{ variant: 'skeleton', message: 'Loading wallet...' }}
errorConfig={{ variant: 'full-screen' }}
empty={{
icon: Wallet,
title: 'No wallet data available',
description: 'Wallet data will appear here once loaded',
}}
/>
);
return <LeagueWalletTemplate viewData={result.unwrap()} />;
}