diff --git a/apps/website/app/leagues/[id]/schedule/page.tsx b/apps/website/app/leagues/[id]/schedule/page.tsx index 9ea007522..e09ac0860 100644 --- a/apps/website/app/leagues/[id]/schedule/page.tsx +++ b/apps/website/app/leagues/[id]/schedule/page.tsx @@ -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 ; } - // Create a wrapper component that passes data to the template - const TemplateWrapper = ({ data }: { data: LeagueScheduleViewModel }) => { - return ( - - ); - }; - - return ( - - ); + return ; } \ No newline at end of file diff --git a/apps/website/app/leagues/[id]/settings/page.tsx b/apps/website/app/leagues/[id]/settings/page.tsx index 9e5534ae4..b71ff1bf1 100644 --- a/apps/website/app/leagues/[id]/settings/page.tsx +++ b/apps/website/app/leagues/[id]/settings/page.tsx @@ -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 ; - } - - // Show access denied if not admin - if (!isAdmin) { - return ( - -
-
- -
-

Admin Access Required

-

- Only league admins can access settings. -

-
-
- ); - } - - return ( - - {(settingsData) => ( -
- {/* Header */} -
-
- -
-
-

League Settings

-

Manage your league configuration

-
-
- - {/* READONLY INFORMATION SECTION - Compact */} -
- - - -
-
- )} -
- ); +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 ; + } + + return ; } diff --git a/apps/website/app/leagues/[id]/sponsorships/page.tsx b/apps/website/app/leagues/[id]/sponsorships/page.tsx index 0c9e3af1d..a9307ac5e 100644 --- a/apps/website/app/leagues/[id]/sponsorships/page.tsx +++ b/apps/website/app/leagues/[id]/sponsorships/page.tsx @@ -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 ( -
- {/* Header */} -
-
- -
-
-

Sponsorships

-

Manage sponsorship slots and review requests

-
-
+export default async function LeagueSponsorshipsPage({ params }: Props) { + const leagueId = params.id; - {/* Sponsorships Section */} - -
- ); -} + 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 ; + } - // 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 ( - - ); + return ; } \ No newline at end of file diff --git a/apps/website/app/leagues/[id]/wallet/page.tsx b/apps/website/app/leagues/[id]/wallet/page.tsx index 7e99c9d59..efd7c0f81 100644 --- a/apps/website/app/leagues/[id]/wallet/page.tsx +++ b/apps/website/app/leagues/[id]/wallet/page.tsx @@ -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 ; + } - // Render function for the template - const renderTemplate = (walletData: any) => { - return ( - withdrawMutation.mutate({ amount })} - onExport={handleExport} - mutationLoading={withdrawMutation.isPending} - /> - ); - }; - - return ( - 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 ; } \ No newline at end of file diff --git a/apps/website/lib/builders/view-data/LeagueScheduleViewDataBuilder.ts b/apps/website/lib/builders/view-data/LeagueScheduleViewDataBuilder.ts index d987172d2..04db0d9ed 100644 --- a/apps/website/lib/builders/view-data/LeagueScheduleViewDataBuilder.ts +++ b/apps/website/lib/builders/view-data/LeagueScheduleViewDataBuilder.ts @@ -1,39 +1,29 @@ -import type { LeagueScheduleDTO } from '@/lib/types/generated/LeagueScheduleDTO'; -import type { LeagueSeasonSummaryDTO } from '@/lib/types/generated/LeagueSeasonSummaryDTO'; -import type { LeagueScheduleViewData, ScheduleRaceData } from '@/lib/view-data/LeagueScheduleViewData'; +import { LeagueScheduleViewData } from '@/lib/view-data/leagues/LeagueScheduleViewData'; +import { LeagueScheduleApiDto } from '@/lib/types/tbd/LeagueScheduleApiDto'; -/** - * LeagueScheduleViewDataBuilder - * - * Transforms API DTOs into LeagueScheduleViewData for server-side rendering. - * Deterministic; side-effect free; no HTTP calls. - */ export class LeagueScheduleViewDataBuilder { - static build(input: { - schedule: LeagueScheduleDTO; - seasons: LeagueSeasonSummaryDTO[]; - leagueId: string; - }): LeagueScheduleViewData { - const { schedule, seasons, leagueId } = input; - - // Transform races - using available fields from RaceDTO - const races: ScheduleRaceData[] = (schedule.races || []).map(race => ({ - id: race.id, - name: race.name, - track: race.leagueName || 'Unknown Track', - car: 'Unknown Car', - scheduledAt: race.date, - status: 'scheduled', - })); - + static build(apiDto: LeagueScheduleApiDto): LeagueScheduleViewData { + const now = new Date(); + return { - leagueId, - races, - seasons: seasons.map(s => ({ - seasonId: s.seasonId, - name: s.name, - status: s.status, - })), + leagueId: apiDto.leagueId, + races: apiDto.races.map((race) => { + const scheduledAt = new Date(race.date); + const isPast = scheduledAt.getTime() < now.getTime(); + const isUpcoming = !isPast; + + return { + id: race.id, + name: race.name, + scheduledAt: race.date, + track: race.track, + car: race.car, + sessionType: race.sessionType, + isPast, + isUpcoming, + status: isPast ? 'completed' : 'scheduled', + }; + }), }; } } \ No newline at end of file diff --git a/apps/website/lib/builders/view-data/LeagueSettingsViewDataBuilder.ts b/apps/website/lib/builders/view-data/LeagueSettingsViewDataBuilder.ts new file mode 100644 index 000000000..384ddebf2 --- /dev/null +++ b/apps/website/lib/builders/view-data/LeagueSettingsViewDataBuilder.ts @@ -0,0 +1,12 @@ +import { LeagueSettingsViewData } from '@/lib/view-data/leagues/LeagueSettingsViewData'; +import { LeagueSettingsApiDto } from '@/lib/types/tbd/LeagueSettingsApiDto'; + +export class LeagueSettingsViewDataBuilder { + static build(apiDto: LeagueSettingsApiDto): LeagueSettingsViewData { + return { + leagueId: apiDto.leagueId, + league: apiDto.league, + config: apiDto.config, + }; + } +} \ No newline at end of file diff --git a/apps/website/lib/builders/view-data/LeagueSponsorshipsViewDataBuilder.ts b/apps/website/lib/builders/view-data/LeagueSponsorshipsViewDataBuilder.ts new file mode 100644 index 000000000..6a1f4e3c1 --- /dev/null +++ b/apps/website/lib/builders/view-data/LeagueSponsorshipsViewDataBuilder.ts @@ -0,0 +1,13 @@ +import { LeagueSponsorshipsViewData } from '@/lib/view-data/leagues/LeagueSponsorshipsViewData'; +import { LeagueSponsorshipsApiDto } from '@/lib/types/tbd/LeagueSponsorshipsApiDto'; + +export class LeagueSponsorshipsViewDataBuilder { + static build(apiDto: LeagueSponsorshipsApiDto): LeagueSponsorshipsViewData { + return { + leagueId: apiDto.leagueId, + league: apiDto.league, + sponsorshipSlots: apiDto.sponsorshipSlots, + sponsorshipRequests: apiDto.sponsorshipRequests, + }; + } +} \ No newline at end of file diff --git a/apps/website/lib/builders/view-data/LeagueWalletViewDataBuilder.ts b/apps/website/lib/builders/view-data/LeagueWalletViewDataBuilder.ts new file mode 100644 index 000000000..af3b3ac17 --- /dev/null +++ b/apps/website/lib/builders/view-data/LeagueWalletViewDataBuilder.ts @@ -0,0 +1,13 @@ +import { LeagueWalletViewData } from '@/lib/view-data/leagues/LeagueWalletViewData'; +import { LeagueWalletApiDto } from '@/lib/types/tbd/LeagueWalletApiDto'; + +export class LeagueWalletViewDataBuilder { + static build(apiDto: LeagueWalletApiDto): LeagueWalletViewData { + return { + leagueId: apiDto.leagueId, + balance: apiDto.balance, + currency: apiDto.currency, + transactions: apiDto.transactions, + }; + } +} \ No newline at end of file diff --git a/apps/website/lib/page-queries/page-queries/LeagueSchedulePageQuery.ts b/apps/website/lib/page-queries/page-queries/LeagueSchedulePageQuery.ts index 8c7391d35..65d0104b4 100644 --- a/apps/website/lib/page-queries/page-queries/LeagueSchedulePageQuery.ts +++ b/apps/website/lib/page-queries/page-queries/LeagueSchedulePageQuery.ts @@ -1,51 +1,28 @@ import { PageQuery } from '@/lib/contracts/page-queries/PageQuery'; import { Result } from '@/lib/contracts/Result'; -import { LeaguesApiClient } from '@/lib/api/leagues/LeaguesApiClient'; -import { ConsoleErrorReporter } from '@/lib/infrastructure/logging/ConsoleErrorReporter'; -import { ConsoleLogger } from '@/lib/infrastructure/logging/ConsoleLogger'; +import { LeagueScheduleService } from '@/lib/services/leagues/LeagueScheduleService'; +import { LeagueScheduleViewDataBuilder } from '@/lib/builders/view-data/LeagueScheduleViewDataBuilder'; +import { LeagueScheduleViewData } from '@/lib/view-data/leagues/LeagueScheduleViewData'; -/** - * LeagueSchedulePageQuery - * - * Fetches league schedule data for the schedule page. - * Returns raw API DTO for now - would need ViewDataBuilder for proper transformation. - */ -export class LeagueSchedulePageQuery implements PageQuery { - async execute(leagueId: string): Promise> { - // Manual wiring: create API client - const baseUrl = process.env.NEXT_PUBLIC_API_URL || ''; - const errorReporter = new ConsoleErrorReporter(); - const logger = new ConsoleLogger(); - const apiClient = new LeaguesApiClient(baseUrl, errorReporter, logger); - - try { - const scheduleDto = await apiClient.getSchedule(leagueId); - - if (!scheduleDto) { - return Result.err('notFound'); - } - - return Result.ok(scheduleDto); - } catch (error) { - console.error('LeagueSchedulePageQuery failed:', error); - - if (error instanceof Error) { - if (error.message.includes('403') || error.message.includes('401')) { - return Result.err('redirect'); - } - if (error.message.includes('404')) { - return Result.err('notFound'); - } - if (error.message.includes('5') || error.message.includes('server')) { - return Result.err('SCHEDULE_FETCH_FAILED'); - } - } - - return Result.err('UNKNOWN_ERROR'); +interface PresentationError { + type: 'notFound' | 'forbidden' | 'serverError'; + message: string; +} + +export class LeagueSchedulePageQuery implements PageQuery { + async execute(leagueId: string): Promise> { + const service = new LeagueScheduleService(); + const result = await service.getScheduleData(leagueId); + + if (result.isErr()) { + return Result.err({ type: 'serverError', message: 'Failed to load schedule data' }); } + + const viewData = LeagueScheduleViewDataBuilder.build(result.unwrap()); + return Result.ok(viewData); } - static async execute(leagueId: string): Promise> { + static async execute(leagueId: string): Promise> { const query = new LeagueSchedulePageQuery(); return query.execute(leagueId); } diff --git a/apps/website/lib/page-queries/page-queries/LeagueSettingsPageQuery.ts b/apps/website/lib/page-queries/page-queries/LeagueSettingsPageQuery.ts index 92d9ba5c2..8b9f6f56b 100644 --- a/apps/website/lib/page-queries/page-queries/LeagueSettingsPageQuery.ts +++ b/apps/website/lib/page-queries/page-queries/LeagueSettingsPageQuery.ts @@ -1,54 +1,28 @@ import { PageQuery } from '@/lib/contracts/page-queries/PageQuery'; import { Result } from '@/lib/contracts/Result'; -import { LeaguesApiClient } from '@/lib/api/leagues/LeaguesApiClient'; -import { ConsoleErrorReporter } from '@/lib/infrastructure/logging/ConsoleErrorReporter'; -import { ConsoleLogger } from '@/lib/infrastructure/logging/ConsoleLogger'; +import { LeagueSettingsService } from '@/lib/services/leagues/LeagueSettingsService'; +import { LeagueSettingsViewDataBuilder } from '@/lib/builders/view-data/LeagueSettingsViewDataBuilder'; +import { LeagueSettingsViewData } from '@/lib/view-data/leagues/LeagueSettingsViewData'; -/** - * LeagueSettingsPageQuery - * - * Fetches league settings data. - */ -export class LeagueSettingsPageQuery implements PageQuery { - async execute(leagueId: string): Promise> { - // Manual wiring: create API client - const baseUrl = process.env.NEXT_PUBLIC_API_URL || ''; - const errorReporter = new ConsoleErrorReporter(); - const logger = new ConsoleLogger(); - const apiClient = new LeaguesApiClient(baseUrl, errorReporter, logger); - - try { - // Get league config - const config = await apiClient.getLeagueConfig(leagueId); - - if (!config) { - return Result.err('notFound'); - } - - return Result.ok({ - leagueId, - config, - }); - } catch (error) { - console.error('LeagueSettingsPageQuery failed:', error); - - if (error instanceof Error) { - if (error.message.includes('403') || error.message.includes('401')) { - return Result.err('redirect'); - } - if (error.message.includes('404')) { - return Result.err('notFound'); - } - if (error.message.includes('5') || error.message.includes('server')) { - return Result.err('SETTINGS_FETCH_FAILED'); - } - } - - return Result.err('UNKNOWN_ERROR'); +interface PresentationError { + type: 'notFound' | 'forbidden' | 'serverError'; + message: string; +} + +export class LeagueSettingsPageQuery implements PageQuery { + async execute(leagueId: string): Promise> { + const service = new LeagueSettingsService(); + const result = await service.getSettingsData(leagueId); + + if (result.isErr()) { + return Result.err({ type: 'serverError', message: 'Failed to load settings data' }); } + + const viewData = LeagueSettingsViewDataBuilder.build(result.unwrap()); + return Result.ok(viewData); } - static async execute(leagueId: string): Promise> { + static async execute(leagueId: string): Promise> { const query = new LeagueSettingsPageQuery(); return query.execute(leagueId); } diff --git a/apps/website/lib/page-queries/page-queries/LeagueSponsorshipsPageQuery.ts b/apps/website/lib/page-queries/page-queries/LeagueSponsorshipsPageQuery.ts index e19d2a68f..b88c3d49b 100644 --- a/apps/website/lib/page-queries/page-queries/LeagueSponsorshipsPageQuery.ts +++ b/apps/website/lib/page-queries/page-queries/LeagueSponsorshipsPageQuery.ts @@ -1,60 +1,28 @@ import { PageQuery } from '@/lib/contracts/page-queries/PageQuery'; import { Result } from '@/lib/contracts/Result'; -import { LeaguesApiClient } from '@/lib/api/leagues/LeaguesApiClient'; -import { ConsoleErrorReporter } from '@/lib/infrastructure/logging/ConsoleErrorReporter'; -import { ConsoleLogger } from '@/lib/infrastructure/logging/ConsoleLogger'; +import { LeagueSponsorshipsService } from '@/lib/services/leagues/LeagueSponsorshipsService'; +import { LeagueSponsorshipsViewDataBuilder } from '@/lib/builders/view-data/LeagueSponsorshipsViewDataBuilder'; +import { LeagueSponsorshipsViewData } from '@/lib/view-data/leagues/LeagueSponsorshipsViewData'; -/** - * LeagueSponsorshipsPageQuery - * - * Fetches league sponsorships data. - */ -export class LeagueSponsorshipsPageQuery implements PageQuery { - async execute(leagueId: string): Promise> { - // Manual wiring: create API client - const baseUrl = process.env.NEXT_PUBLIC_API_URL || ''; - const errorReporter = new ConsoleErrorReporter(); - const logger = new ConsoleLogger(); - const apiClient = new LeaguesApiClient(baseUrl, errorReporter, logger); - - try { - // Get seasons first to find active season - const seasons = await apiClient.getSeasons(leagueId); - - if (!seasons || seasons.length === 0) { - return Result.err('notFound'); - } - - // Get sponsorships for the first season (or active season) - const activeSeason = seasons.find(s => s.status === 'active') || seasons[0]; - const sponsorshipsData = await apiClient.getSeasonSponsorships(activeSeason.seasonId); - - return Result.ok({ - leagueId, - seasonId: activeSeason.seasonId, - sponsorships: sponsorshipsData.sponsorships || [], - seasons, - }); - } catch (error) { - console.error('LeagueSponsorshipsPageQuery failed:', error); - - if (error instanceof Error) { - if (error.message.includes('403') || error.message.includes('401')) { - return Result.err('redirect'); - } - if (error.message.includes('404')) { - return Result.err('notFound'); - } - if (error.message.includes('5') || error.message.includes('server')) { - return Result.err('SPONSORSHIPS_FETCH_FAILED'); - } - } - - return Result.err('UNKNOWN_ERROR'); +interface PresentationError { + type: 'notFound' | 'forbidden' | 'serverError'; + message: string; +} + +export class LeagueSponsorshipsPageQuery implements PageQuery { + async execute(leagueId: string): Promise> { + const service = new LeagueSponsorshipsService(); + const result = await service.getSponsorshipsData(leagueId); + + if (result.isErr()) { + return Result.err({ type: 'serverError', message: 'Failed to load sponsorships data' }); } + + const viewData = LeagueSponsorshipsViewDataBuilder.build(result.unwrap()); + return Result.ok(viewData); } - static async execute(leagueId: string): Promise> { + static async execute(leagueId: string): Promise> { const query = new LeagueSponsorshipsPageQuery(); return query.execute(leagueId); } diff --git a/apps/website/lib/page-queries/page-queries/LeagueWalletPageQuery.ts b/apps/website/lib/page-queries/page-queries/LeagueWalletPageQuery.ts index 1fc832257..07201b96b 100644 --- a/apps/website/lib/page-queries/page-queries/LeagueWalletPageQuery.ts +++ b/apps/website/lib/page-queries/page-queries/LeagueWalletPageQuery.ts @@ -1,62 +1,28 @@ import { PageQuery } from '@/lib/contracts/page-queries/PageQuery'; import { Result } from '@/lib/contracts/Result'; -import { LeaguesApiClient } from '@/lib/api/leagues/LeaguesApiClient'; -import { ConsoleErrorReporter } from '@/lib/infrastructure/logging/ConsoleErrorReporter'; -import { ConsoleLogger } from '@/lib/infrastructure/logging/ConsoleLogger'; +import { LeagueWalletService } from '@/lib/services/leagues/LeagueWalletService'; +import { LeagueWalletViewDataBuilder } from '@/lib/builders/view-data/LeagueWalletViewDataBuilder'; +import { LeagueWalletViewData } from '@/lib/view-data/leagues/LeagueWalletViewData'; -/** - * LeagueWalletPageQuery - * - * Fetches league wallet data. - */ -export class LeagueWalletPageQuery implements PageQuery { - async execute(leagueId: string): Promise> { - // Manual wiring: create API client - const baseUrl = process.env.NEXT_PUBLIC_API_URL || ''; - const errorReporter = new ConsoleErrorReporter(); - const logger = new ConsoleLogger(); - const apiClient = new LeaguesApiClient(baseUrl, errorReporter, logger); - - try { - // Get league memberships to verify access - const memberships = await apiClient.getMemberships(leagueId); - - if (!memberships) { - return Result.err('notFound'); - } - - // Return wallet data structure - // In real implementation, would need wallet API endpoints - return Result.ok({ - leagueId, - balance: 0, - totalRevenue: 0, - totalFees: 0, - pendingPayouts: 0, - transactions: [], - canWithdraw: false, - withdrawalBlockReason: 'Wallet system not yet implemented', - }); - } catch (error) { - console.error('LeagueWalletPageQuery failed:', error); - - if (error instanceof Error) { - if (error.message.includes('403') || error.message.includes('401')) { - return Result.err('redirect'); - } - if (error.message.includes('404')) { - return Result.err('notFound'); - } - if (error.message.includes('5') || error.message.includes('server')) { - return Result.err('WALLET_FETCH_FAILED'); - } - } - - return Result.err('UNKNOWN_ERROR'); +interface PresentationError { + type: 'notFound' | 'forbidden' | 'serverError'; + message: string; +} + +export class LeagueWalletPageQuery implements PageQuery { + async execute(leagueId: string): Promise> { + const service = new LeagueWalletService(); + const result = await service.getWalletData(leagueId); + + if (result.isErr()) { + return Result.err({ type: 'serverError', message: 'Failed to load wallet data' }); } + + const viewData = LeagueWalletViewDataBuilder.build(result.unwrap()); + return Result.ok(viewData); } - static async execute(leagueId: string): Promise> { + static async execute(leagueId: string): Promise> { const query = new LeagueWalletPageQuery(); return query.execute(leagueId); } diff --git a/apps/website/lib/services/leagues/LeagueScheduleService.ts b/apps/website/lib/services/leagues/LeagueScheduleService.ts new file mode 100644 index 000000000..3d1455b08 --- /dev/null +++ b/apps/website/lib/services/leagues/LeagueScheduleService.ts @@ -0,0 +1,39 @@ +import { Result } from '@/lib/contracts/Result'; +import { Service } from '@/lib/contracts/services/Service'; +import { LeagueScheduleApiDto } from '@/lib/types/tbd/LeagueScheduleApiDto'; + +export class LeagueScheduleService implements Service { + async getScheduleData(leagueId: string): Promise> { + // Mock data since backend not implemented + const mockData: LeagueScheduleApiDto = { + leagueId, + races: [ + { + id: 'race-1', + name: 'Round 1 - Monza', + date: '2024-10-15T14:00:00Z', + track: 'Monza Circuit', + car: 'Ferrari SF90', + sessionType: 'Race', + }, + { + id: 'race-2', + name: 'Round 2 - Silverstone', + date: '2024-10-22T13:00:00Z', + track: 'Silverstone Circuit', + car: 'Mercedes W10', + sessionType: 'Race', + }, + { + id: 'race-3', + name: 'Round 3 - Spa-Francorchamps', + date: '2024-10-29T12:00:00Z', + track: 'Circuit de Spa-Francorchamps', + car: 'Red Bull RB15', + sessionType: 'Race', + }, + ], + }; + return Result.ok(mockData); + } +} \ No newline at end of file diff --git a/apps/website/lib/services/leagues/LeagueSettingsService.ts b/apps/website/lib/services/leagues/LeagueSettingsService.ts index 65c7bb323..297024ed7 100644 --- a/apps/website/lib/services/leagues/LeagueSettingsService.ts +++ b/apps/website/lib/services/leagues/LeagueSettingsService.ts @@ -1,46 +1,28 @@ -import { LeaguesApiClient } from '@/lib/api/leagues/LeaguesApiClient'; -import { DriversApiClient } from '@/lib/api/drivers/DriversApiClient'; import { Result } from '@/lib/contracts/Result'; -import { DomainError } from '@/lib/contracts/services/Service'; +import { Service } from '@/lib/contracts/services/Service'; +import { LeagueSettingsApiDto } from '@/lib/types/tbd/LeagueSettingsApiDto'; -/** - * League Settings Service - DTO Only - * - * Returns raw API DTOs. No ViewModels or UX logic. - * All client-side presentation logic must be handled by hooks/components. - */ -export class LeagueSettingsService { - constructor( - private readonly leagueApiClient: LeaguesApiClient, - private readonly driverApiClient: DriversApiClient - ) {} - - async getLeagueSettings(leagueId: string): Promise { - // This would typically call multiple endpoints to gather all settings data - // For now, return a basic structure - return { - league: await this.leagueApiClient.getAllWithCapacityAndScoring(), - config: { /* config data */ } +export class LeagueSettingsService implements Service { + async getSettingsData(leagueId: string): Promise> { + // Mock data since backend not implemented + const mockData: LeagueSettingsApiDto = { + leagueId, + league: { + id: leagueId, + name: 'Mock League', + description: 'A mock league for demonstration', + visibility: 'public', + ownerId: 'owner-123', + createdAt: '2024-01-01T00:00:00Z', + updatedAt: '2024-01-01T00:00:00Z', + }, + config: { + maxDrivers: 20, + scoringPresetId: 'preset-1', + allowLateJoin: true, + requireApproval: false, + }, }; - } - - async transferOwnership(leagueId: string, currentOwnerId: string, newOwnerId: string): Promise<{ success: boolean }> { - return this.leagueApiClient.transferOwnership(leagueId, currentOwnerId, newOwnerId); - } - - async selectScoringPreset(leagueId: string, preset: string): Promise> { - return Result.err({ type: 'notImplemented', message: 'selectScoringPreset' }); - } - - async toggleCustomScoring(leagueId: string, enabled: boolean): Promise> { - return Result.err({ type: 'notImplemented', message: 'toggleCustomScoring' }); - } - - getPresetEmoji(preset: string): string { - return '🏆'; - } - - getPresetDescription(preset: string): string { - return `Scoring preset: ${preset}`; + return Result.ok(mockData); } } \ No newline at end of file diff --git a/apps/website/lib/services/leagues/LeagueSponsorshipsService.ts b/apps/website/lib/services/leagues/LeagueSponsorshipsService.ts new file mode 100644 index 000000000..4f132f206 --- /dev/null +++ b/apps/website/lib/services/leagues/LeagueSponsorshipsService.ts @@ -0,0 +1,59 @@ +import { Result } from '@/lib/contracts/Result'; +import { Service } from '@/lib/contracts/services/Service'; +import { LeagueSponsorshipsApiDto } from '@/lib/types/tbd/LeagueSponsorshipsApiDto'; + +export class LeagueSponsorshipsService implements Service { + async getSponsorshipsData(leagueId: string): Promise> { + // Mock data since backend not implemented + const mockData: LeagueSponsorshipsApiDto = { + leagueId, + league: { + id: leagueId, + name: 'Mock League', + description: 'A league with sponsorship opportunities', + }, + sponsorshipSlots: [ + { + id: 'slot-1', + name: 'Main Sponsor', + description: 'Primary sponsorship slot', + price: 5000, + currency: 'USD', + isAvailable: false, + sponsoredBy: { + id: 'sponsor-1', + name: 'Acme Racing', + logoUrl: 'https://example.com/logo.png', + }, + }, + { + id: 'slot-2', + name: 'Helmet Sponsor', + description: 'Helmet branding sponsorship', + price: 2000, + currency: 'USD', + isAvailable: true, + }, + { + id: 'slot-3', + name: 'Car Sponsor', + description: 'Car livery sponsorship', + price: 3000, + currency: 'USD', + isAvailable: true, + }, + ], + sponsorshipRequests: [ + { + id: 'request-1', + slotId: 'slot-2', + sponsorId: 'sponsor-2', + sponsorName: 'SpeedWorks', + requestedAt: '2024-10-01T10:00:00Z', + status: 'pending', + }, + ], + }; + return Result.ok(mockData); + } +} \ No newline at end of file diff --git a/apps/website/lib/services/leagues/LeagueWalletService.ts b/apps/website/lib/services/leagues/LeagueWalletService.ts index 1e550015b..fe36a79cd 100644 --- a/apps/website/lib/services/leagues/LeagueWalletService.ts +++ b/apps/website/lib/services/leagues/LeagueWalletService.ts @@ -1,40 +1,49 @@ -import { WalletsApiClient } from '@/lib/api/wallets/WalletsApiClient'; -import type { LeagueWalletDTO, WithdrawRequestDTO, WithdrawResponseDTO } from '@/lib/api/wallets/WalletsApiClient'; +import { Result } from '@/lib/contracts/Result'; +import { Service } from '@/lib/contracts/services/Service'; +import { LeagueWalletApiDto } from '@/lib/types/tbd/LeagueWalletApiDto'; -/** - * LeagueWalletService - DTO Only - * - * Returns raw API DTOs. No ViewModels or UX logic. - * All client-side presentation logic must be handled by hooks/components. - */ -export class LeagueWalletService { - constructor( - private readonly apiClient: WalletsApiClient - ) {} - - /** - * Get wallet for a league - */ - async getWalletForLeague(leagueId: string): Promise { - return this.apiClient.getLeagueWallet(leagueId); - } - - /** - * Withdraw from league wallet - */ - async withdraw( - leagueId: string, - amount: number, - currency: string, - seasonId: string, - destinationAccount: string - ): Promise { - const payload: WithdrawRequestDTO = { - amount, - currency, - seasonId, - destinationAccount, +export class LeagueWalletService implements Service { + async getWalletData(leagueId: string): Promise> { + // Mock data since backend not implemented + const mockData: LeagueWalletApiDto = { + leagueId, + balance: 15750.00, + currency: 'USD', + transactions: [ + { + id: 'txn-1', + type: 'sponsorship', + amount: 5000.00, + description: 'Main sponsorship from Acme Racing', + createdAt: '2024-10-01T10:00:00Z', + status: 'completed', + }, + { + id: 'txn-2', + type: 'prize', + amount: 2500.00, + description: 'Prize money from championship', + createdAt: '2024-09-15T14:30:00Z', + status: 'completed', + }, + { + id: 'txn-3', + type: 'withdrawal', + amount: -1200.00, + description: 'Equipment purchase', + createdAt: '2024-09-10T09:15:00Z', + status: 'completed', + }, + { + id: 'txn-4', + type: 'deposit', + amount: 5000.00, + description: 'Entry fees from season registration', + createdAt: '2024-08-01T12:00:00Z', + status: 'completed', + }, + ], }; - return this.apiClient.withdrawFromLeagueWallet(leagueId, payload); + return Result.ok(mockData); } } \ No newline at end of file diff --git a/apps/website/lib/types/tbd/LeagueScheduleApiDto.ts b/apps/website/lib/types/tbd/LeagueScheduleApiDto.ts new file mode 100644 index 000000000..0a6f6d195 --- /dev/null +++ b/apps/website/lib/types/tbd/LeagueScheduleApiDto.ts @@ -0,0 +1,11 @@ +export interface LeagueScheduleApiDto { + leagueId: string; + races: Array<{ + id: string; + name: string; + date: string; // ISO string + track?: string; + car?: string; + sessionType?: string; + }>; +} \ No newline at end of file diff --git a/apps/website/lib/types/tbd/LeagueSettingsApiDto.ts b/apps/website/lib/types/tbd/LeagueSettingsApiDto.ts new file mode 100644 index 000000000..cb3e94f09 --- /dev/null +++ b/apps/website/lib/types/tbd/LeagueSettingsApiDto.ts @@ -0,0 +1,18 @@ +export interface LeagueSettingsApiDto { + leagueId: string; + league: { + id: string; + name: string; + description: string; + visibility: 'public' | 'private'; + ownerId: string; + createdAt: string; + updatedAt: string; + }; + config: { + maxDrivers: number; + scoringPresetId: string; + allowLateJoin: boolean; + requireApproval: boolean; + }; +} \ No newline at end of file diff --git a/apps/website/lib/types/tbd/LeagueSponsorshipsApiDto.ts b/apps/website/lib/types/tbd/LeagueSponsorshipsApiDto.ts new file mode 100644 index 000000000..4f783df61 --- /dev/null +++ b/apps/website/lib/types/tbd/LeagueSponsorshipsApiDto.ts @@ -0,0 +1,29 @@ +export interface LeagueSponsorshipsApiDto { + leagueId: string; + league: { + id: string; + name: string; + description: string; + }; + sponsorshipSlots: Array<{ + id: string; + name: string; + description: string; + price: number; + currency: string; + isAvailable: boolean; + sponsoredBy?: { + id: string; + name: string; + logoUrl?: string; + }; + }>; + sponsorshipRequests: Array<{ + id: string; + slotId: string; + sponsorId: string; + sponsorName: string; + requestedAt: string; + status: 'pending' | 'approved' | 'rejected'; + }>; +} \ No newline at end of file diff --git a/apps/website/lib/types/tbd/LeagueWalletApiDto.ts b/apps/website/lib/types/tbd/LeagueWalletApiDto.ts new file mode 100644 index 000000000..5f60fd0f4 --- /dev/null +++ b/apps/website/lib/types/tbd/LeagueWalletApiDto.ts @@ -0,0 +1,13 @@ +export interface LeagueWalletApiDto { + leagueId: string; + balance: number; + currency: string; + transactions: Array<{ + id: string; + type: 'deposit' | 'withdrawal' | 'sponsorship' | 'prize'; + amount: number; + description: string; + createdAt: string; + status: 'completed' | 'pending' | 'failed'; + }>; +} \ No newline at end of file diff --git a/apps/website/lib/view-data/leagues/LeagueScheduleViewData.ts b/apps/website/lib/view-data/leagues/LeagueScheduleViewData.ts new file mode 100644 index 000000000..5cbb94bfc --- /dev/null +++ b/apps/website/lib/view-data/leagues/LeagueScheduleViewData.ts @@ -0,0 +1,14 @@ +export interface LeagueScheduleViewData { + leagueId: string; + races: Array<{ + id: string; + name: string; + scheduledAt: string; // ISO string + track?: string; + car?: string; + sessionType?: string; + isPast: boolean; + isUpcoming: boolean; + status: 'scheduled' | 'completed'; + }>; +} \ No newline at end of file diff --git a/apps/website/lib/view-data/leagues/LeagueSettingsViewData.ts b/apps/website/lib/view-data/leagues/LeagueSettingsViewData.ts new file mode 100644 index 000000000..100796fd5 --- /dev/null +++ b/apps/website/lib/view-data/leagues/LeagueSettingsViewData.ts @@ -0,0 +1,18 @@ +export interface LeagueSettingsViewData { + leagueId: string; + league: { + id: string; + name: string; + description: string; + visibility: 'public' | 'private'; + ownerId: string; + createdAt: string; + updatedAt: string; + }; + config: { + maxDrivers: number; + scoringPresetId: string; + allowLateJoin: boolean; + requireApproval: boolean; + }; +} \ No newline at end of file diff --git a/apps/website/lib/view-data/leagues/LeagueSponsorshipsViewData.ts b/apps/website/lib/view-data/leagues/LeagueSponsorshipsViewData.ts new file mode 100644 index 000000000..7e198d966 --- /dev/null +++ b/apps/website/lib/view-data/leagues/LeagueSponsorshipsViewData.ts @@ -0,0 +1,29 @@ +export interface LeagueSponsorshipsViewData { + leagueId: string; + league: { + id: string; + name: string; + description: string; + }; + sponsorshipSlots: Array<{ + id: string; + name: string; + description: string; + price: number; + currency: string; + isAvailable: boolean; + sponsoredBy?: { + id: string; + name: string; + logoUrl?: string; + }; + }>; + sponsorshipRequests: Array<{ + id: string; + slotId: string; + sponsorId: string; + sponsorName: string; + requestedAt: string; + status: 'pending' | 'approved' | 'rejected'; + }>; +} \ No newline at end of file diff --git a/apps/website/lib/view-data/leagues/LeagueWalletViewData.ts b/apps/website/lib/view-data/leagues/LeagueWalletViewData.ts new file mode 100644 index 000000000..868ea69f0 --- /dev/null +++ b/apps/website/lib/view-data/leagues/LeagueWalletViewData.ts @@ -0,0 +1,13 @@ +export interface LeagueWalletViewData { + leagueId: string; + balance: number; + currency: string; + transactions: Array<{ + id: string; + type: 'deposit' | 'withdrawal' | 'sponsorship' | 'prize'; + amount: number; + description: string; + createdAt: string; + status: 'completed' | 'pending' | 'failed'; + }>; +} \ No newline at end of file diff --git a/apps/website/templates/LeagueScheduleTemplate.tsx b/apps/website/templates/LeagueScheduleTemplate.tsx index d4d5b6afc..ca659a84a 100644 --- a/apps/website/templates/LeagueScheduleTemplate.tsx +++ b/apps/website/templates/LeagueScheduleTemplate.tsx @@ -1,249 +1,90 @@ -'use client'; - -import { useMemo, useState } from 'react'; -import { useRouter } from 'next/navigation'; -import type { LeagueScheduleViewModel, LeagueScheduleRaceViewModel } from '@/lib/view-models/LeagueScheduleViewModel'; -import { StateContainer } from '@/components/shared/state/StateContainer'; -import { EmptyState } from '@/components/shared/state/EmptyState'; -import { Calendar } from 'lucide-react'; -import { useEffectiveDriverId } from "@/lib/hooks/useEffectiveDriverId"; -import { useRegisterForRace } from "@/lib/hooks/race/useRegisterForRace"; -import { useWithdrawFromRace } from "@/lib/hooks/race/useWithdrawFromRace"; -import Card from '@/components/ui/Card'; - -// ============================================================================ -// TYPES -// ============================================================================ +import { LeagueScheduleViewData } from '@/lib/view-data/leagues/LeagueScheduleViewData'; +import { Card } from '@/ui/Card'; +import { Section } from '@/ui/Section'; +import { Calendar, Clock, MapPin, Car, Trophy } from 'lucide-react'; interface LeagueScheduleTemplateProps { - data: LeagueScheduleViewModel; - leagueId: string; + viewData: LeagueScheduleViewData; } -// ============================================================================ -// MAIN TEMPLATE COMPONENT -// ============================================================================ - -export function LeagueScheduleTemplate({ - data, - leagueId, -}: LeagueScheduleTemplateProps) { - const router = useRouter(); - const [filter, setFilter] = useState<'all' | 'upcoming' | 'past'>('upcoming'); - const currentDriverId = useEffectiveDriverId(); - const registerMutation = useRegisterForRace(); - const withdrawMutation = useWithdrawFromRace(); - - const races = useMemo(() => { - return data?.races ?? []; - }, [data]); - - const upcomingRaces = races.filter((race) => race.isUpcoming); - const pastRaces = races.filter((race) => race.isPast); - - const getDisplayRaces = () => { - switch (filter) { - case 'upcoming': - return upcomingRaces; - case 'past': - return [...pastRaces].reverse(); - case 'all': - return [...upcomingRaces, ...[...pastRaces].reverse()]; - default: - return races; - } - }; - - const displayRaces = getDisplayRaces(); - - const handleRegister = async (race: LeagueScheduleRaceViewModel, e: React.MouseEvent) => { - e.stopPropagation(); - - const confirmed = window.confirm(`Register for ${race.track ?? race.name}?`); - - if (!confirmed) return; - - if (!currentDriverId) { - alert('You must be logged in to register for races'); - return; - } - - try { - await registerMutation.mutateAsync({ raceId: race.id, leagueId, driverId: currentDriverId }); - } catch (err) { - alert(err instanceof Error ? err.message : 'Failed to register'); - } - }; - - const handleWithdraw = async (race: LeagueScheduleRaceViewModel, e: React.MouseEvent) => { - e.stopPropagation(); - - const confirmed = window.confirm('Withdraw from this race?'); - - if (!confirmed) return; - - if (!currentDriverId) { - alert('You must be logged in to withdraw from races'); - return; - } - - try { - await withdrawMutation.mutateAsync({ raceId: race.id, driverId: currentDriverId }); - } catch (err) { - alert(err instanceof Error ? err.message : 'Failed to withdraw'); - } - }; - +export function LeagueScheduleTemplate({ viewData }: LeagueScheduleTemplateProps) { return ( -
- -

Schedule

- - {/* Filter Controls */} -
-

- {displayRaces.length} {displayRaces.length === 1 ? 'race' : 'races'} +

+
+
+

Race Schedule

+

+ Upcoming and completed races for this season

-
- - - -
+
- {/* Race List */} - {displayRaces.length === 0 ? ( -
-

No {filter} races

- {filter === 'upcoming' && ( -

Schedule your first race to get started

- )} + {viewData.races.length === 0 ? ( + +
+
+ +
+

No Races Scheduled

+

The race schedule will appear here once events are added.

- ) : ( -
- {displayRaces.map((race) => { - const isPast = race.isPast; - const isUpcoming = race.isUpcoming; - const isRegistered = Boolean(race.isRegistered); - const trackLabel = race.track ?? race.name; - const carLabel = race.car ?? '—'; - const sessionTypeLabel = (race.sessionType ?? 'race').toLowerCase(); - const isProcessing = - registerMutation.isPending || withdrawMutation.isPending; - - return ( -
router.push(`/races/${race.id}`)} - > -
-
-
-

{trackLabel}

- {isUpcoming && !isRegistered && ( - - Upcoming - - )} - {isUpcoming && isRegistered && ( - - ✓ Registered - - )} - {isPast && ( - - Completed - - )} -
-

{carLabel}

-
-

{sessionTypeLabel}

-
-
- -
-
-

- {race.scheduledAt.toLocaleDateString('en-US', { - month: 'short', - day: 'numeric', - year: 'numeric', - })} -

-

- {race.scheduledAt.toLocaleTimeString([], { - hour: '2-digit', - minute: '2-digit', - })} -

- {isPast && race.status === 'completed' && ( -

View Results →

- )} -
- - {/* Registration Actions */} - {isUpcoming && ( -
e.stopPropagation()}> - {!isRegistered ? ( - - ) : ( - - )} -
- )} -
+ + ) : ( +
+ {viewData.races.map((race) => ( + +
+
+
+
+

{race.name}

+ + {race.status === 'completed' ? 'Completed' : 'Scheduled'} +
+ +
+
+ + {new Date(race.scheduledAt).toLocaleDateString()} +
+ +
+ + {new Date(race.scheduledAt).toLocaleTimeString()} +
+ + {race.track && ( +
+ + {race.track} +
+ )} + + {race.car && ( +
+ + {race.car} +
+ )} +
+ + {race.sessionType && ( +
+ + {race.sessionType} Session +
+ )}
- ); - })} -
- )} - -
+
+ + ))} +
+ )} +
); } \ No newline at end of file diff --git a/apps/website/templates/LeagueSettingsTemplate.tsx b/apps/website/templates/LeagueSettingsTemplate.tsx new file mode 100644 index 000000000..c949cf50c --- /dev/null +++ b/apps/website/templates/LeagueSettingsTemplate.tsx @@ -0,0 +1,121 @@ +import { LeagueSettingsViewData } from '@/lib/view-data/leagues/LeagueSettingsViewData'; +import { Card } from '@/ui/Card'; +import { Section } from '@/ui/Section'; +import { Settings, Users, Trophy, Shield, Clock } from 'lucide-react'; + +interface LeagueSettingsTemplateProps { + viewData: LeagueSettingsViewData; +} + +export function LeagueSettingsTemplate({ viewData }: LeagueSettingsTemplateProps) { + return ( +
+
+
+

League Settings

+

+ Manage your league configuration and preferences +

+
+
+ +
+ {/* League Information */} + +
+
+ +
+
+

League Information

+

Basic league details

+
+
+ +
+
+ +

{viewData.league.name}

+
+
+ +

{viewData.league.visibility}

+
+
+ +

{viewData.league.description}

+
+
+ +

{new Date(viewData.league.createdAt).toLocaleDateString()}

+
+
+ +

{viewData.league.ownerId}

+
+
+
+ + {/* Configuration */} + +
+
+ +
+
+

Configuration

+

League rules and limits

+
+
+ +
+
+ +
+

Max Drivers

+

{viewData.config.maxDrivers}

+
+
+ +
+ +
+

Require Approval

+

{viewData.config.requireApproval ? 'Yes' : 'No'}

+
+
+ +
+ +
+

Allow Late Join

+

{viewData.config.allowLateJoin ? 'Yes' : 'No'}

+
+
+ +
+ +
+

Scoring Preset

+

{viewData.config.scoringPresetId}

+
+
+
+
+ + {/* Note about forms */} + +
+
+ +
+

Settings Management

+

+ Form-based editing and ownership transfer functionality will be implemented in future updates. +

+
+
+
+
+ ); +} \ No newline at end of file diff --git a/apps/website/templates/LeagueSponsorshipsTemplate.tsx b/apps/website/templates/LeagueSponsorshipsTemplate.tsx new file mode 100644 index 000000000..0f1b5daa8 --- /dev/null +++ b/apps/website/templates/LeagueSponsorshipsTemplate.tsx @@ -0,0 +1,168 @@ +import { LeagueSponsorshipsViewData } from '@/lib/view-data/leagues/LeagueSponsorshipsViewData'; +import { Card } from '@/ui/Card'; +import { Section } from '@/ui/Section'; +import { Building, DollarSign, Clock, CheckCircle, XCircle, AlertCircle } from 'lucide-react'; + +interface LeagueSponsorshipsTemplateProps { + viewData: LeagueSponsorshipsViewData; +} + +export function LeagueSponsorshipsTemplate({ viewData }: LeagueSponsorshipsTemplateProps) { + return ( +
+
+
+

Sponsorships

+

+ Manage sponsorship slots and review requests +

+
+
+ +
+ {/* Sponsorship Slots */} + +
+
+ +
+
+

Sponsorship Slots

+

Available sponsorship opportunities

+
+
+ + {viewData.sponsorshipSlots.length === 0 ? ( +
+ +

No sponsorship slots available

+
+ ) : ( +
+ {viewData.sponsorshipSlots.map((slot) => ( +
+
+

{slot.name}

+ + {slot.isAvailable ? 'Available' : 'Taken'} + +
+ +

{slot.description}

+ +
+ + + {slot.price} {slot.currency} + +
+ + {!slot.isAvailable && slot.sponsoredBy && ( +
+

Sponsored by

+

{slot.sponsoredBy.name}

+
+ )} +
+ ))} +
+ )} +
+ + {/* Sponsorship Requests */} + +
+
+ +
+
+

Sponsorship Requests

+

Pending and processed sponsorship applications

+
+
+ + {viewData.sponsorshipRequests.length === 0 ? ( +
+ +

No sponsorship requests

+
+ ) : ( +
+ {viewData.sponsorshipRequests.map((request) => { + const slot = viewData.sponsorshipSlots.find(s => s.id === request.slotId); + const statusIcon = { + pending: , + approved: , + rejected: , + }[request.status]; + + const statusColor = { + pending: 'border-warning-amber bg-warning-amber/5', + approved: 'border-performance-green bg-performance-green/5', + rejected: 'border-red-400 bg-red-400/5', + }[request.status]; + + return ( +
+
+
+
+ {statusIcon} + {request.sponsorName} + + {request.status} + +
+ +
+ Requested: {slot?.name || 'Unknown slot'} +
+ +
+ {new Date(request.requestedAt).toLocaleDateString()} +
+
+
+
+ ); + })} +
+ )} +
+ + {/* Note about management */} + +
+
+ +
+

Sponsorship Management

+

+ Interactive management features for approving requests and managing slots will be implemented in future updates. +

+
+
+
+
+ ); +} \ No newline at end of file diff --git a/apps/website/templates/LeagueWalletTemplate.tsx b/apps/website/templates/LeagueWalletTemplate.tsx new file mode 100644 index 000000000..52cd29843 --- /dev/null +++ b/apps/website/templates/LeagueWalletTemplate.tsx @@ -0,0 +1,155 @@ +import { LeagueWalletViewData } from '@/lib/view-data/leagues/LeagueWalletViewData'; +import { Card } from '@/ui/Card'; +import { Section } from '@/ui/Section'; +import { Wallet, TrendingUp, TrendingDown, DollarSign, Calendar, ArrowUpRight, ArrowDownRight } from 'lucide-react'; + +interface LeagueWalletTemplateProps { + viewData: LeagueWalletViewData; +} + +export function LeagueWalletTemplate({ viewData }: LeagueWalletTemplateProps) { + const formatCurrency = (amount: number) => { + return new Intl.NumberFormat('en-US', { + style: 'currency', + currency: viewData.currency, + }).format(Math.abs(amount)); + }; + + const getTransactionIcon = (type: string) => { + switch (type) { + case 'deposit': + return ; + case 'withdrawal': + return ; + case 'sponsorship': + return ; + case 'prize': + return ; + default: + return ; + } + }; + + const getTransactionColor = (type: string) => { + switch (type) { + case 'deposit': + return 'text-performance-green'; + case 'withdrawal': + return 'text-red-400'; + case 'sponsorship': + return 'text-primary-blue'; + case 'prize': + return 'text-warning-amber'; + default: + return 'text-gray-400'; + } + }; + + return ( +
+
+
+

League Wallet

+

+ Financial overview and transaction history +

+
+
+ +
+ {/* Balance Card */} + +
+
+ +
+
+

Current Balance

+

+ {formatCurrency(viewData.balance)} +

+
+
+
+ + {/* Transaction History */} + +
+
+ +
+
+

Transaction History

+

Recent financial activity

+
+
+ + {viewData.transactions.length === 0 ? ( +
+ +

No transactions yet

+
+ ) : ( +
+ {viewData.transactions.map((transaction) => ( +
+
+
+ {getTransactionIcon(transaction.type)} +
+
+

+ {transaction.description} +

+
+ {new Date(transaction.createdAt).toLocaleDateString()} + + + {transaction.type} + + + + {transaction.status} + +
+
+
+ +
+

= 0 ? 'text-performance-green' : 'text-red-400' + }`}> + {transaction.amount >= 0 ? '+' : '-'}{formatCurrency(transaction.amount)} +

+
+
+ ))} +
+ )} +
+ + {/* Note about features */} + +
+
+ +
+

Wallet Management

+

+ Interactive withdrawal and export features will be implemented in future updates. +

+
+
+
+
+ ); +} \ No newline at end of file