diff --git a/.eslintrc.json b/.eslintrc.json index 81745f7f3..7cfd91b9c 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -72,6 +72,27 @@ } ] } + ], + "import/no-restricted-imports": [ + 2, + { + "paths": [ + { + "name": "apps/website/lib/apiClient", + "message": "Direct imports from apiClient are forbidden. Use services instead." + }, + { + "name": "apps/website/lib/dtos", + "message": "Direct imports from dtos in UI components are forbidden. Use ViewModels instead.", + "from": ["apps/website/app/**/*", "apps/website/components/**/*"] + }, + { + "name": "apps/website/lib/api", + "message": "Direct imports from api/* in UI components are forbidden. Use services instead.", + "from": ["apps/website/app/**/*", "apps/website/components/**/*"] + } + ] + } ] } } diff --git a/apps/website/.eslintrc.json b/apps/website/.eslintrc.json index 8b50487f7..59e69a67d 100644 --- a/apps/website/.eslintrc.json +++ b/apps/website/.eslintrc.json @@ -1,6 +1,11 @@ { - "extends": "next/core-web-vitals", - "plugins": ["boundaries"], + "extends": ["next/core-web-vitals", "plugin:import/recommended", "plugin:import/typescript"], + "plugins": ["boundaries", "import"], + "settings": { + "import/resolver": { + "typescript": {} + } + }, "rules": { "react/no-unescaped-entities": "off", "@next/next/no-img-element": "warn", diff --git a/apps/website/DATA_FLOW.md b/apps/website/DATA_FLOW.md new file mode 100644 index 000000000..52ef449e2 --- /dev/null +++ b/apps/website/DATA_FLOW.md @@ -0,0 +1,361 @@ +Frontend data shapes: View Models, Presenters, and API Client (Strict) + +This document defines the exact placement and responsibilities for frontend data shapes. +It is designed to leave no room for interpretation. + +⸻ + +1. Definitions + +API DTOs + +Transport shapes owned by the API boundary (HTTP). They are not used directly by UI components. + +View Models + +UI-owned shapes. They represent exactly what the UI needs and nothing else. + +Website Presenters + +Pure mappers that convert API DTOs (or core use-case outputs) into View Models. + +API Client + +A thin HTTP wrapper that returns API DTOs only and performs no business logic. + +⸻ + +2. Directory layout (exact) + +apps/website +├── app/ # Next.js routes/pages +├── components/ # React components (UI only) +├── lib/ +│ ├── api/ # API client (HTTP only) +│ ├── dtos/ # API DTO types (transport shapes) +│ ├── view-models/ # View Models (UI-owned shapes) +│ ├── presenters/ # Presenters: DTO -> ViewModel mapping +│ ├── services/ # UI orchestration (calls api + presenters) +│ └── index.ts + +No additional folders for these concerns are allowed. + +⸻ + +3. View Models (placement and rules) + +Where they live + +View Models MUST live in: + +apps/website/lib/view-models + +What they may contain + • UI-ready primitives (strings, numbers, booleans) + • UI-specific derived fields (e.g., isOwner, badgeLabel, formattedDate) + • UI-specific structures (e.g., grouped arrays, flattened objects) + +What they must NOT contain + • Domain entities or value objects + • API transport metadata + • Validation logic + • Network or persistence concerns + +Rule + +Components consume only View Models. + +⸻ + +4. API DTOs in the website (placement and rules) + +Clarification + +The website does have DTOs, but only API DTOs. + +These DTOs exist exclusively to type HTTP communication with the backend API. +They are not UI models. + +⸻ + +Where they live + +Website-side API DTO types MUST live in: + +apps/website/lib/dtos + +What they represent + • Exact transport shapes sent/received via HTTP + • Backend API contracts + • No UI assumptions + +Who may use them + • API client + • Website presenters + +Who must NOT use them + • React components + • Pages + • UI logic + +Rule + +API DTOs stop at the presenter boundary. + +Components must never consume API DTOs directly. + +⸻ + +5. Presenters (website) (placement and rules) + +Where they live + +Website presenters MUST live in: + +apps/website/lib/presenters + +What they do + • Convert API DTOs into View Models + • Perform UI-friendly formatting and structuring + • Are pure and deterministic + +What they must NOT do + • Make API calls + • Read from localStorage/cookies directly + • Contain business rules or decisions + • Perform side effects + +Rule + +Presenters output View Models. Presenters never output API DTOs. + +⸻ + +6. Do website presenters use View Models? + +Yes. Strictly: + +Website presenters MUST output View Models and MUST NOT output API DTOs. + +Flow is always: + +API DTO -> Presenter -> View Model -> Component + + +⸻ + +7. API client (website) (placement and rules) + +Where it lives + +The API client MUST live in: + +apps/website/lib/api + +What it does + • Sends HTTP requests + • Returns API DTOs + • Performs authentication header/cookie handling only at transport level + • Does not map to View Models + +What it must NOT do + • Format or reshape responses for UI + • Contain business rules + • Contain decision logic + +Rule + +The API client has no knowledge of View Models. + +⸻ + +8. Website service layer (strict orchestration) + +Where it lives + +Website orchestration MUST live in: + +apps/website/lib/services + +What it does + • Calls the API client + • Calls presenters to map DTO -> View Model + • Returns View Models to pages/components + +What it must NOT do + • Contain domain logic + • Modify core invariants + • Return API DTOs + +Rule + +Services are the only layer allowed to call both api/ and presenters/. + +Components must not call the API client directly. + +⸻ + +9. Allowed dependency directions (frontend) + +Within apps/website: + +components -> services -> (api + presenters) -> (dtos + view-models) + +Strict rules: + • components may import only view-models and services + • presenters may import dtos and view-models only + • api may import dtos only + • services may import api, presenters, view-models + +Forbidden: + • components importing api + • components importing dtos + • presenters importing api + • api importing view-models + • any website code importing core domain entities + +⸻ + +10. Naming rules (strict) + • View Models end with ViewModel + • API DTOs end with Dto + • Presenters end with Presenter + • Services end with Service + • One export per file + • File name equals exported symbol (PascalCase) + +⸻ + +11. Final "no ambiguity" summary + • View Models live in apps/website/lib/view-models + • API DTOs live in apps/website/lib/dtos + • Presenters live in apps/website/lib/presenters and map DTO -> ViewModel + • API client lives in apps/website/lib/api and returns DTOs only + • Services live in apps/website/lib/services and return View Models only + • Components consume View Models only and never touch API DTOs or API clients + +⸻ + +12. Clean Architecture Flow Diagram + +```mermaid +graph TD + A[UI Components] --> B[Services] + B --> C[API Client] + B --> D[Presenters] + C --> E[API DTOs] + D --> E + D --> F[View Models] + A --> F + + style A fill:#e1f5fe + style B fill:#f3e5f5 + style C fill:#fff3e0 + style D fill:#e8f5e8 + style E fill:#ffebee + style F fill:#e3f2fd +``` + +**Flow Explanation:** +- UI Components consume only View Models +- Services orchestrate API calls and presenter mappings +- API Client returns raw API DTOs +- Presenters transform API DTOs into UI-ready View Models +- Strict dependency direction: UI → Services → (API + Presenters) → (DTOs + ViewModels) + +⸻ + +13. Enforcement Guidelines + +**ESLint Rules:** +- Direct imports from `apiClient` are forbidden - use services instead +- Direct imports from `dtos` in UI components are forbidden - use ViewModels instead +- Direct imports from `api/*` in UI components are forbidden - use services instead + +**TypeScript Path Mappings:** +- Use `@/lib/dtos` for API DTO imports +- Use `@/lib/view-models` for View Model imports +- Use `@/lib/presenters` for Presenter imports +- Use `@/lib/services` for Service imports +- Use `@/lib/api` for API client imports + +**Import Restrictions:** +- Components may import only view-models and services +- Presenters may import dtos and view-models only +- API may import dtos only +- Services may import api, presenters, view-models +- Forbidden: components importing api, components importing dtos, presenters importing api, api importing view-models + +**Verification Commands:** +```bash +npm run build # Ensure TypeScript compiles +npm run lint # Ensure ESLint rules pass +npm run test # Ensure all tests pass +``` + +⸻ + +14. Architecture Examples + +**Before (Violates Rules):** +```typescript +// In a page component - BAD +import { apiClient } from '@/lib/apiClient'; +import type { RaceResultDto } from '@/lib/dtos/RaceResultDto'; + +const RacePage = () => { + const [data, setData] = useState(); + // Direct API call and DTO usage in UI + useEffect(() => { + apiClient.getRaceResults().then(setData); + }, []); + return
{data?.map(d => d.position)}
; +}; +``` + +**After (Clean Architecture):** +```typescript +// In a page component - GOOD +import { RaceResultsService } from '@/lib/services/RaceResultsService'; +import type { RaceResultViewModel } from '@/lib/view-models/RaceResultViewModel'; + +const RacePage = () => { + const [data, setData] = useState(); + useEffect(() => { + RaceResultsService.getResults().then(setData); + }, []); + return
{data?.map(d => d.formattedPosition)}
; +}; +``` + +**Service Implementation:** +```typescript +// apps/website/lib/services/RaceResultsService.ts +import { apiClient } from '@/lib/api'; +import { RaceResultsPresenter } from '@/lib/presenters/RaceResultsPresenter'; + +export class RaceResultsService { + static async getResults(): Promise { + const dtos = await apiClient.getRaceResults(); + return RaceResultsPresenter.present(dtos); + } +} +``` + +**Presenter Implementation:** +```typescript +// apps/website/lib/presenters/RaceResultsPresenter.ts +import type { RaceResultDto } from '@/lib/dtos/RaceResultDto'; +import type { RaceResultViewModel } from '@/lib/view-models/RaceResultViewModel'; + +export class RaceResultsPresenter { + static present(dtos: RaceResultDto[]): RaceResultViewModel[] { + return dtos.map(dto => ({ + id: dto.id, + formattedPosition: `${dto.position}${dto.position === 1 ? 'st' : dto.position === 2 ? 'nd' : dto.position === 3 ? 'rd' : 'th'}`, + driverName: dto.driverName, + // ... other UI-specific formatting + })); + } +} +``` \ No newline at end of file diff --git a/apps/website/app/drivers/page.tsx b/apps/website/app/drivers/page.tsx index a41cc1fe8..203867735 100644 --- a/apps/website/app/drivers/page.tsx +++ b/apps/website/app/drivers/page.tsx @@ -27,15 +27,11 @@ import Button from '@/components/ui/Button'; import Input from '@/components/ui/Input'; import Card from '@/components/ui/Card'; import Heading from '@/components/ui/Heading'; -import { DriversLeaderboardPresenter } from '@/lib/presenters/DriversLeaderboardPresenter'; -import type { DriverLeaderboardItemViewModel, SkillLevel } from '@core/racing/application/presenters/IDriversLeaderboardPresenter'; +import { getDriverLeaderboard } from '@/lib/services/drivers/DriverService'; +import type { DriverLeaderboardViewModel } from '@/lib/view-models'; import Image from 'next/image'; -// ============================================================================ -// TYPES -// ============================================================================ - -type DriverListItem = DriverLeaderboardItemViewModel; +import type { DriverLeaderboardItemViewModel } from '@/lib/view-models/DriverLeaderboardItemViewModel'; // ============================================================================ // DEMO DATA @@ -50,7 +46,7 @@ type DriverListItem = DriverLeaderboardItemViewModel; // ============================================================================ const SKILL_LEVELS: { - id: SkillLevel; + id: string; label: string; icon: React.ElementType; color: string; @@ -69,7 +65,7 @@ const SKILL_LEVELS: { // ============================================================================ interface FeaturedDriverCardProps { - driver: DriverListItem; + driver: DriverLeaderboardItemViewModel; position: number; onClick: () => void; } @@ -118,7 +114,7 @@ function FeaturedDriverCard({ driver, position, onClick }: FeaturedDriverCardPro {/* Avatar & Name */}
- {driver.name} + {driver.name}

@@ -155,7 +151,7 @@ function FeaturedDriverCard({ driver, position, onClick }: FeaturedDriverCardPro // ============================================================================ interface SkillDistributionProps { - drivers: DriverListItem[]; + drivers: DriverLeaderboardItemViewModel[]; } function SkillDistribution({ drivers }: SkillDistributionProps) { @@ -217,7 +213,7 @@ function SkillDistribution({ drivers }: SkillDistributionProps) { // ============================================================================ interface LeaderboardPreviewProps { - drivers: DriverListItem[]; + drivers: DriverLeaderboardItemViewModel[]; onDriverClick: (id: string) => void; } @@ -286,7 +282,7 @@ function LeaderboardPreview({ drivers, onDriverClick }: LeaderboardPreviewProps) {/* Avatar */}
- {driver.name} + {driver.name}
{/* Info */} @@ -326,7 +322,7 @@ function LeaderboardPreview({ drivers, onDriverClick }: LeaderboardPreviewProps) // ============================================================================ interface RecentActivityProps { - drivers: DriverListItem[]; + drivers: DriverLeaderboardItemViewModel[]; onDriverClick: (id: string) => void; } @@ -356,7 +352,7 @@ function RecentActivity({ drivers, onDriverClick }: RecentActivityProps) { className="p-3 rounded-xl bg-iron-gray/40 border border-charcoal-outline hover:border-performance-green/40 transition-all group text-center" >
- {driver.name} + {driver.name}

@@ -377,7 +373,8 @@ function RecentActivity({ drivers, onDriverClick }: RecentActivityProps) { export default function DriversPage() { const router = useRouter(); - const [drivers, setDrivers] = useState([]); + const [drivers, setDrivers] = useState([]); + const [viewModel, setViewModel] = useState(null); const [loading, setLoading] = useState(true); const [searchQuery, setSearchQuery] = useState(''); const [totalRaces, setTotalRaces] = useState(0); @@ -386,17 +383,12 @@ export default function DriversPage() { useEffect(() => { const load = async () => { - const useCase = getGetDriversLeaderboardUseCase(); - const presenter = new DriversLeaderboardPresenter(); - await useCase.execute(undefined as void, presenter); - const viewModel = presenter.getViewModel(); - - if (viewModel) { - setDrivers(viewModel.drivers); - setTotalRaces(viewModel.totalRaces); - setTotalWins(viewModel.totalWins); - setActiveCount(viewModel.activeCount); - } + const vm = await getDriverLeaderboard(); + setViewModel(vm); + setDrivers(vm.drivers); + setTotalRaces(vm.totalRaces); + setTotalWins(vm.totalWins); + setActiveCount(vm.activeCount); setLoading(false); }; diff --git a/apps/website/app/leagues/[id]/standings/page.tsx b/apps/website/app/leagues/[id]/standings/page.tsx index f600c3b1d..5116bead2 100644 --- a/apps/website/app/leagues/[id]/standings/page.tsx +++ b/apps/website/app/leagues/[id]/standings/page.tsx @@ -4,70 +4,37 @@ import { useState, useEffect, useCallback } from 'react'; import { useParams } from 'next/navigation'; import Card from '@/components/ui/Card'; import StandingsTable from '@/components/leagues/StandingsTable'; -import { - EntityMappers, - type DriverDTO, - type LeagueDriverSeasonStatsDTO, -} from '@core/racing'; -import { LeagueDriverSeasonStatsPresenter } from '@/lib/presenters/LeagueDriverSeasonStatsPresenter'; +import { getLeagueStandings } from '@/lib/services/leagues/LeagueService'; import { useEffectiveDriverId } from '@/lib/currentDriver'; import { isLeagueAdminOrHigherRole } from '@/lib/leagueRoles'; -import type { MembershipRole, LeagueMembership } from '@/lib/leagueMembership'; +import type { LeagueStandingsViewModel } from '@/lib/view-models'; +import type { StandingEntryViewModel } from '@/lib/view-models/StandingEntryViewModel'; +import type { DriverDto } from '@/lib/dtos'; +import type { LeagueMembership } from '@/lib/dtos'; export default function LeagueStandingsPage() { const params = useParams(); const leagueId = params.id as string; const currentDriverId = useEffectiveDriverId(); - const [standings, setStandings] = useState([]); - const [drivers, setDrivers] = useState([]); + const [standings, setStandings] = useState([]); + const [drivers, setDrivers] = useState([]); const [memberships, setMemberships] = useState([]); + const [viewModel, setViewModel] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [isAdmin, setIsAdmin] = useState(false); const loadData = useCallback(async () => { try { - const getLeagueDriverSeasonStatsUseCase = getGetLeagueDriverSeasonStatsUseCase(); - const driverRepo = getDriverRepository(); - const membershipRepo = getLeagueMembershipRepository(); - - const presenter = new LeagueDriverSeasonStatsPresenter(); - await getLeagueDriverSeasonStatsUseCase.execute({ leagueId }, presenter); - const standingsViewModel = presenter.getViewModel(); - setStandings(standingsViewModel.stats); - - const allDrivers = await driverRepo.findAll(); - const driverDtos: DriverDTO[] = allDrivers - .map((driver) => EntityMappers.toDriverDTO(driver)) - .filter((dto): dto is DriverDTO => dto !== null); - setDrivers(driverDtos); - - // Load league memberships from repository (consistent with other data) - const allMemberships = await membershipRepo.getLeagueMembers(leagueId); - - type RawMembership = { - id: string | number; - leagueId: string; - driverId: string; - role: MembershipRole; - status: LeagueMembership['status']; - joinedAt: string | Date; - }; - - // Convert to the format expected by StandingsTable (website-level LeagueMembership) - const membershipData: LeagueMembership[] = (allMemberships as RawMembership[]).map((m) => ({ - id: String(m.id), - leagueId: m.leagueId, - driverId: m.driverId, - role: m.role, - status: m.status, - joinedAt: m.joinedAt instanceof Date ? m.joinedAt.toISOString() : String(m.joinedAt), - })); - setMemberships(membershipData); + const vm = await getLeagueStandings(leagueId, currentDriverId); + setViewModel(vm); + setStandings(vm.standings); + setDrivers(vm.drivers); + setMemberships(vm.memberships); // Check if current user is admin - const membership = await membershipRepo.getMembership(leagueId, currentDriverId); + const membership = vm.memberships.find(m => m.driverId === currentDriverId); setIsAdmin(membership ? isLeagueAdminOrHigherRole(membership.role) : false); } catch (err) { setError(err instanceof Error ? err.message : 'Failed to load standings'); @@ -151,7 +118,7 @@ export default function LeagueStandingsPage() { } const leader = standings[0]; - const totalRaces = Math.max(...standings.map(s => s.racesStarted), 0); + const totalRaces = Math.max(...standings.map(s => s.races), 0); return (

@@ -166,7 +133,7 @@ export default function LeagueStandingsPage() {

Championship Leader

{drivers.find(d => d.id === leader?.driverId)?.name || 'N/A'}

-

{leader?.totalPoints || 0} points

+

{leader?.points || 0} points

diff --git a/apps/website/app/races/[id]/results/page.tsx b/apps/website/app/races/[id]/results/page.tsx index f7e9a7adb..4da46f16f 100644 --- a/apps/website/app/races/[id]/results/page.tsx +++ b/apps/website/app/races/[id]/results/page.tsx @@ -9,51 +9,10 @@ import Breadcrumbs from '@/components/layout/Breadcrumbs'; import ResultsTable from '@/components/races/ResultsTable'; import ImportResultsForm from '@/components/races/ImportResultsForm'; import QuickPenaltyModal from '@/components/leagues/QuickPenaltyModal'; -import { apiClient } from '@/lib/apiClient'; +import { getRaceResults, getRaceSOF, importRaceResults } from '@/lib/services/races/RaceResultsService'; import { useEffectiveDriverId } from '@/lib/currentDriver'; import { isLeagueAdminOrHigherRole } from '@/lib/leagueRoles'; -import type { RaceResultsDetailViewModel, RaceWithSOFViewModel } from '@/lib/apiClient'; - -type PenaltyTypeDTO = - | 'time_penalty' - | 'grid_penalty' - | 'points_deduction' - | 'disqualification' - | 'warning' - | 'license_points' - | string; - -interface PenaltyData { - driverId: string; - type: PenaltyTypeDTO; - value?: number; -} - -interface RaceResultRowDTO { - id: string; - raceId: string; - driverId: string; - position: number; - fastestLap: number; - incidents: number; - startPosition: number; - getPositionChange(): number; -} - -interface DriverRowDTO { - id: string; - name: string; -} - -interface ImportResultRowDTO { - id: string; - raceId: string; - driverId: string; - position: number; - fastestLap: number; - incidents: number; - startPosition: number; -} +import type { RaceResultsDetailViewModel } from '@/lib/view-models'; export default function RaceResultsPage() { const router = useRouter(); @@ -69,16 +28,16 @@ export default function RaceResultsPage() { const [importSuccess, setImportSuccess] = useState(false); const [isAdmin, setIsAdmin] = useState(false); const [showQuickPenaltyModal, setShowQuickPenaltyModal] = useState(false); - const [preSelectedDriver, setPreSelectedDriver] = useState(undefined); + const [preSelectedDriver, setPreSelectedDriver] = useState<{ id: string; name: string } | undefined>(undefined); const loadData = async () => { try { - const raceData = await apiClient.races.getResultsDetail(raceId); + const raceData = await getRaceResults(raceId, currentDriverId); setRaceData(raceData); setError(null); try { - const sofData = await apiClient.races.getWithSOF(raceId); + const sofData = await getRaceSOF(raceId); setRaceSOF(sofData.strengthOfField); } catch (sofErr) { console.error('Failed to load SOF:', sofErr); @@ -106,12 +65,12 @@ export default function RaceResultsPage() { } }, [raceData?.league?.id, currentDriverId]); - const handleImportSuccess = async (importedResults: ImportResultRowDTO[]) => { + const handleImportSuccess = async (importedResults: any[]) => { setImporting(true); setError(null); try { - await apiClient.races.importResults(raceId, { + await importRaceResults(raceId, { resultsFileContent: JSON.stringify(importedResults), // Assuming the API expects JSON string }); @@ -128,7 +87,7 @@ export default function RaceResultsPage() { setError(errorMessage); }; - const handlePenaltyClick = (driver: DriverRowDTO) => { + const handlePenaltyClick = (driver: { id: string; name: string }) => { setPreSelectedDriver(driver); setShowQuickPenaltyModal(true); }; @@ -241,7 +200,7 @@ export default function RaceResultsPage() { - {raceData.results.length} drivers classified + {raceData.stats.totalDrivers} drivers classified )} @@ -265,11 +224,11 @@ export default function RaceResultsPage() { {hasResults && raceData ? ( { + return this.post('/analytics/page-view', input); + } + + /** Record an engagement event */ + recordEngagement(input: RecordEngagementInputDto): Promise { + return this.post('/analytics/engagement', input); + } +} \ No newline at end of file diff --git a/apps/website/lib/api/auth/AuthApiClient.ts b/apps/website/lib/api/auth/AuthApiClient.ts new file mode 100644 index 000000000..50feb0ad4 --- /dev/null +++ b/apps/website/lib/api/auth/AuthApiClient.ts @@ -0,0 +1,40 @@ +import { BaseApiClient } from '../base/BaseApiClient'; +import type { + LoginParamsDto, + SignupParamsDto, + SessionDataDto, +} from '../../dtos'; + +/** + * Auth API Client + * + * Handles all authentication-related API operations. + */ +export class AuthApiClient extends BaseApiClient { + /** Sign up with email */ + signup(params: SignupParamsDto): Promise { + return this.post('/auth/signup', params); + } + + /** Login with email */ + login(params: LoginParamsDto): Promise { + return this.post('/auth/login', params); + } + + /** Get current session */ + getSession(): Promise { + return this.get('/auth/session'); + } + + /** Logout */ + logout(): Promise { + return this.post('/auth/logout', {}); + } + + /** Start iRacing auth redirect */ + getIracingAuthUrl(returnTo?: string): string { + const baseUrl = process.env.NEXT_PUBLIC_API_BASE_URL || 'http://localhost:3001'; + const params = returnTo ? `?returnTo=${encodeURIComponent(returnTo)}` : ''; + return `${baseUrl}/auth/iracing/start${params}`; + } +} \ No newline at end of file diff --git a/apps/website/lib/api/base/BaseApiClient.ts b/apps/website/lib/api/base/BaseApiClient.ts new file mode 100644 index 000000000..866999cfd --- /dev/null +++ b/apps/website/lib/api/base/BaseApiClient.ts @@ -0,0 +1,68 @@ +/** + * Base API Client for HTTP operations + * + * Provides generic HTTP methods with common request/response handling, + * error handling, and authentication. + */ + +export class BaseApiClient { + private baseUrl: string; + + constructor(baseUrl: string) { + this.baseUrl = baseUrl; + } + + protected async request(method: string, path: string, data?: object): Promise { + const headers: HeadersInit = { + 'Content-Type': 'application/json', + }; + + const config: RequestInit = { + method, + headers, + credentials: 'include', // Include cookies for auth + }; + + if (data) { + config.body = JSON.stringify(data); + } + + const response = await fetch(`${this.baseUrl}${path}`, config); + + if (!response.ok) { + let errorData: { message?: string } = { message: response.statusText }; + try { + errorData = await response.json(); + } catch { + // Keep default error message + } + throw new Error(errorData.message || `API request failed with status ${response.status}`); + } + + const text = await response.text(); + if (!text) { + return null as T; + } + return JSON.parse(text) as T; + } + + protected get(path: string): Promise { + return this.request('GET', path); + } + + protected post(path: string, data: object): Promise { + return this.request('POST', path, data); + } + + protected put(path: string, data: object): Promise { + return this.request('PUT', path, data); + } + + protected delete(path: string): Promise { + return this.request('DELETE', path); + } + + protected patch(path: string, data: object): Promise { + return this.request('PATCH', path, data); + } +} \ No newline at end of file diff --git a/apps/website/lib/api/drivers/DriversApiClient.ts b/apps/website/lib/api/drivers/DriversApiClient.ts new file mode 100644 index 000000000..f74e2cfbc --- /dev/null +++ b/apps/website/lib/api/drivers/DriversApiClient.ts @@ -0,0 +1,29 @@ +import { BaseApiClient } from '../base/BaseApiClient'; +import type { + DriversLeaderboardDto, + CompleteOnboardingInputDto, + CompleteOnboardingOutputDto, + DriverDto, +} from '../../dtos'; + +/** + * Drivers API Client + * + * Handles all driver-related API operations. + */ +export class DriversApiClient extends BaseApiClient { + /** Get drivers leaderboard */ + getLeaderboard(): Promise { + return this.get('/drivers/leaderboard'); + } + + /** Complete driver onboarding */ + completeOnboarding(input: CompleteOnboardingInputDto): Promise { + return this.post('/drivers/complete-onboarding', input); + } + + /** Get current driver (based on session) */ + getCurrent(): Promise { + return this.get('/drivers/current'); + } +} \ No newline at end of file diff --git a/apps/website/lib/api/index.ts b/apps/website/lib/api/index.ts new file mode 100644 index 000000000..8e531d31e --- /dev/null +++ b/apps/website/lib/api/index.ts @@ -0,0 +1,46 @@ +import { LeaguesApiClient } from './leagues/LeaguesApiClient'; +import { RacesApiClient } from './races/RacesApiClient'; +import { DriversApiClient } from './drivers/DriversApiClient'; +import { TeamsApiClient } from './teams/TeamsApiClient'; +import { SponsorsApiClient } from './sponsors/SponsorsApiClient'; +import { MediaApiClient } from './media/MediaApiClient'; +import { AnalyticsApiClient } from './analytics/AnalyticsApiClient'; +import { AuthApiClient } from './auth/AuthApiClient'; +import { PaymentsApiClient } from './payments/PaymentsApiClient'; + +/** + * Main API Client + * + * Orchestrates all domain-specific API clients with consistent configuration. + */ +export class ApiClient { + public readonly leagues: LeaguesApiClient; + public readonly races: RacesApiClient; + public readonly drivers: DriversApiClient; + public readonly teams: TeamsApiClient; + public readonly sponsors: SponsorsApiClient; + public readonly media: MediaApiClient; + public readonly analytics: AnalyticsApiClient; + public readonly auth: AuthApiClient; + public readonly payments: PaymentsApiClient; + + constructor(baseUrl: string) { + this.leagues = new LeaguesApiClient(baseUrl); + this.races = new RacesApiClient(baseUrl); + this.drivers = new DriversApiClient(baseUrl); + this.teams = new TeamsApiClient(baseUrl); + this.sponsors = new SponsorsApiClient(baseUrl); + this.media = new MediaApiClient(baseUrl); + this.analytics = new AnalyticsApiClient(baseUrl); + this.auth = new AuthApiClient(baseUrl); + this.payments = new PaymentsApiClient(baseUrl); + } +} + +// ============================================================================ +// Singleton Instance +// ============================================================================ + +const API_BASE_URL = process.env.NEXT_PUBLIC_API_BASE_URL || 'http://localhost:3001'; + +export const api = new ApiClient(API_BASE_URL); \ No newline at end of file diff --git a/apps/website/lib/api/leagues/LeaguesApiClient.ts b/apps/website/lib/api/leagues/LeaguesApiClient.ts new file mode 100644 index 000000000..c5346e1bf --- /dev/null +++ b/apps/website/lib/api/leagues/LeaguesApiClient.ts @@ -0,0 +1,52 @@ +import { BaseApiClient } from '../base/BaseApiClient'; +import type { + AllLeaguesWithCapacityDto, + LeagueStatsDto, + LeagueStandingsDto, + LeagueScheduleDto, + LeagueMembershipsDto, + CreateLeagueInputDto, + CreateLeagueOutputDto, +} from '../../dtos'; + +/** + * Leagues API Client + * + * Handles all league-related API operations. + */ +export class LeaguesApiClient extends BaseApiClient { + /** Get all leagues with capacity information */ + getAllWithCapacity(): Promise { + return this.get('/leagues/all-with-capacity'); + } + + /** Get total number of leagues */ + getTotal(): Promise { + return this.get('/leagues/total-leagues'); + } + + /** Get league standings */ + getStandings(leagueId: string): Promise { + return this.get(`/leagues/${leagueId}/standings`); + } + + /** Get league schedule */ + getSchedule(leagueId: string): Promise { + return this.get(`/leagues/${leagueId}/schedule`); + } + + /** Get league memberships */ + getMemberships(leagueId: string): Promise { + return this.get(`/leagues/${leagueId}/memberships`); + } + + /** Create a new league */ + create(input: CreateLeagueInputDto): Promise { + return this.post('/leagues', input); + } + + /** Remove a member from league */ + removeMember(leagueId: string, performerDriverId: string, targetDriverId: string): Promise<{ success: boolean }> { + return this.patch<{ success: boolean }>(`/leagues/${leagueId}/members/${targetDriverId}/remove`, { performerDriverId }); + } +} \ No newline at end of file diff --git a/apps/website/lib/api/media/MediaApiClient.ts b/apps/website/lib/api/media/MediaApiClient.ts new file mode 100644 index 000000000..6499187f4 --- /dev/null +++ b/apps/website/lib/api/media/MediaApiClient.ts @@ -0,0 +1,17 @@ +import { BaseApiClient } from '../base/BaseApiClient'; +import type { + RequestAvatarGenerationInputDto, + RequestAvatarGenerationOutputDto, +} from '../../dtos'; + +/** + * Media API Client + * + * Handles all media-related API operations. + */ +export class MediaApiClient extends BaseApiClient { + /** Request avatar generation */ + requestAvatarGeneration(input: RequestAvatarGenerationInputDto): Promise { + return this.post('/media/avatar/generate', input); + } +} \ No newline at end of file diff --git a/apps/website/lib/api/payments/PaymentsApiClient.ts b/apps/website/lib/api/payments/PaymentsApiClient.ts new file mode 100644 index 000000000..e55d9cead --- /dev/null +++ b/apps/website/lib/api/payments/PaymentsApiClient.ts @@ -0,0 +1,49 @@ +import { BaseApiClient } from '../base/BaseApiClient'; +import type { + GetPaymentsOutputDto, + CreatePaymentInputDto, + CreatePaymentOutputDto, + GetMembershipFeesOutputDto, + GetPrizesOutputDto, + GetWalletOutputDto, +} from '../../dtos'; + +/** + * Payments API Client + * + * Handles all payment-related API operations. + */ +export class PaymentsApiClient extends BaseApiClient { + /** Get payments */ + getPayments(leagueId?: string, driverId?: string): Promise { + const params = new URLSearchParams(); + if (leagueId) params.append('leagueId', leagueId); + if (driverId) params.append('driverId', driverId); + const query = params.toString(); + return this.get(`/payments${query ? `?${query}` : ''}`); + } + + /** Create a payment */ + createPayment(input: CreatePaymentInputDto): Promise { + return this.post('/payments', input); + } + + /** Get membership fees */ + getMembershipFees(leagueId: string): Promise { + return this.get(`/payments/membership-fees?leagueId=${leagueId}`); + } + + /** Get prizes */ + getPrizes(leagueId?: string, seasonId?: string): Promise { + const params = new URLSearchParams(); + if (leagueId) params.append('leagueId', leagueId); + if (seasonId) params.append('seasonId', seasonId); + const query = params.toString(); + return this.get(`/payments/prizes${query ? `?${query}` : ''}`); + } + + /** Get wallet */ + getWallet(driverId: string): Promise { + return this.get(`/payments/wallets?driverId=${driverId}`); + } +} \ No newline at end of file diff --git a/apps/website/lib/api/races/RacesApiClient.ts b/apps/website/lib/api/races/RacesApiClient.ts new file mode 100644 index 000000000..1b93e2750 --- /dev/null +++ b/apps/website/lib/api/races/RacesApiClient.ts @@ -0,0 +1,53 @@ +import { BaseApiClient } from '../base/BaseApiClient'; +import type { + RaceStatsDto, + RacesPageDataDto, + RaceDetailDto, + RaceResultsDetailDto, + RaceWithSOFDto, + RegisterForRaceInputDto, + ImportRaceResultsInputDto, + ImportRaceResultsSummaryDto, +} from '../../dtos'; + +/** + * Races API Client + * + * Handles all race-related API operations. + */ +export class RacesApiClient extends BaseApiClient { + /** Get total number of races */ + getTotal(): Promise { + return this.get('/races/total-races'); + } + + /** Get races page data */ + getPageData(): Promise { + return this.get('/races/page-data'); + } + + /** Get race detail */ + getDetail(raceId: string, driverId: string): Promise { + return this.get(`/races/${raceId}?driverId=${driverId}`); + } + + /** Get race results detail */ + getResultsDetail(raceId: string): Promise { + return this.get(`/races/${raceId}/results`); + } + + /** Get race with strength of field */ + getWithSOF(raceId: string): Promise { + return this.get(`/races/${raceId}/sof`); + } + + /** Register for race */ + register(raceId: string, input: RegisterForRaceInputDto): Promise { + return this.post(`/races/${raceId}/register`, input); + } + + /** Import race results */ + importResults(raceId: string, input: ImportRaceResultsInputDto): Promise { + return this.post(`/races/${raceId}/import-results`, input); + } +} \ No newline at end of file diff --git a/apps/website/lib/api/sponsors/SponsorsApiClient.ts b/apps/website/lib/api/sponsors/SponsorsApiClient.ts new file mode 100644 index 000000000..81b467e17 --- /dev/null +++ b/apps/website/lib/api/sponsors/SponsorsApiClient.ts @@ -0,0 +1,41 @@ +import { BaseApiClient } from '../base/BaseApiClient'; +import type { + GetEntitySponsorshipPricingResultDto, + GetSponsorsOutputDto, + CreateSponsorInputDto, + CreateSponsorOutputDto, + SponsorDashboardDto, + SponsorSponsorshipsDto, +} from '../../dtos'; + +/** + * Sponsors API Client + * + * Handles all sponsor-related API operations. + */ +export class SponsorsApiClient extends BaseApiClient { + /** Get sponsorship pricing */ + getPricing(): Promise { + return this.get('/sponsors/pricing'); + } + + /** Get all sponsors */ + getAll(): Promise { + return this.get('/sponsors'); + } + + /** Create a new sponsor */ + create(input: CreateSponsorInputDto): Promise { + return this.post('/sponsors', input); + } + + /** Get sponsor dashboard */ + getDashboard(sponsorId: string): Promise { + return this.get(`/sponsors/dashboard/${sponsorId}`); + } + + /** Get sponsor sponsorships */ + getSponsorships(sponsorId: string): Promise { + return this.get(`/sponsors/${sponsorId}/sponsorships`); + } +} \ No newline at end of file diff --git a/apps/website/lib/api/teams/TeamsApiClient.ts b/apps/website/lib/api/teams/TeamsApiClient.ts new file mode 100644 index 000000000..f8cdda5e6 --- /dev/null +++ b/apps/website/lib/api/teams/TeamsApiClient.ts @@ -0,0 +1,54 @@ +import { BaseApiClient } from '../base/BaseApiClient'; +import type { + AllTeamsDto, + TeamDetailsDto, + TeamMembersDto, + TeamJoinRequestsDto, + CreateTeamInputDto, + CreateTeamOutputDto, + UpdateTeamInputDto, + UpdateTeamOutputDto, + DriverTeamDto, +} from '../../dtos'; + +/** + * Teams API Client + * + * Handles all team-related API operations. + */ +export class TeamsApiClient extends BaseApiClient { + /** Get all teams */ + getAll(): Promise { + return this.get('/teams/all'); + } + + /** Get team details */ + getDetails(teamId: string): Promise { + return this.get(`/teams/${teamId}`); + } + + /** Get team members */ + getMembers(teamId: string): Promise { + return this.get(`/teams/${teamId}/members`); + } + + /** Get team join requests */ + getJoinRequests(teamId: string): Promise { + return this.get(`/teams/${teamId}/join-requests`); + } + + /** Create a new team */ + create(input: CreateTeamInputDto): Promise { + return this.post('/teams', input); + } + + /** Update team */ + update(teamId: string, input: UpdateTeamInputDto): Promise { + return this.patch(`/teams/${teamId}`, input); + } + + /** Get driver's team */ + getDriverTeam(driverId: string): Promise { + return this.get(`/teams/driver/${driverId}`); + } +} \ No newline at end of file diff --git a/apps/website/lib/apiClient.ts b/apps/website/lib/apiClient.ts index e85afafdb..8b5862558 100644 --- a/apps/website/lib/apiClient.ts +++ b/apps/website/lib/apiClient.ts @@ -1,1160 +1,12 @@ /** - * Domain-specific API Client for GridPilot Website - * - * This module provides a strongly-typed HTTP client for all API operations. - * The website should use these methods instead of directly importing core use cases. + * @deprecated This legacy API client has been refactored into domain-specific clients. + * Please use the new API client from './api' instead. + * + * TODO: Remove this file once all consumers have migrated to the new API client structure. */ -// ============================================================================ -// Types - These mirror the API DTOs -// ============================================================================ +// Re-export the new API client for backward compatibility +export { api as apiClient } from './api'; -// Common Types -export interface DriverDTO { - id: string; - name: string; - avatarUrl?: string; - iracingId?: string; - rating?: number; -} - -export interface ProtestViewModel { - id: string; - raceId: string; - complainantId: string; - defendantId: string; - description: string; - status: string; - createdAt: string; -} - -export interface LeagueMemberViewModel { - driverId: string; - driver?: DriverDTO; - role: string; - joinedAt: string; -} - -export interface StandingEntryViewModel { - driverId: string; - driver?: DriverDTO; - position: number; - points: number; - wins: number; - podiums: number; - races: number; -} - -export interface ScheduledRaceViewModel { - id: string; - name: string; - scheduledTime: string; - status: string; - trackName?: string; -} - -// League Types -export interface LeagueSummaryViewModel { - id: string; - name: string; - description?: string; - logoUrl?: string; - coverImage?: string; - memberCount: number; - maxMembers: number; - isPublic: boolean; - ownerId: string; - ownerName?: string; - scoringType?: string; - status?: string; -} - -export interface AllLeaguesWithCapacityViewModel { - leagues: LeagueSummaryViewModel[]; -} - -export interface LeagueStatsDto { - totalLeagues: number; -} - -export interface LeagueJoinRequestViewModel { - id: string; - leagueId: string; - driverId: string; - requestedAt: Date; - message?: string; -} - -export interface LeagueAdminPermissionsViewModel { - canManageMembers: boolean; - canManageRaces: boolean; - canManageSettings: boolean; - canManageProtests: boolean; - isOwner: boolean; - isAdmin: boolean; -} - -export interface LeagueOwnerSummaryViewModel { - leagueId: string; - leagueName: string; - memberCount: number; - pendingRequests: number; -} - -export interface LeagueConfigFormModelDto { - id: string; - name: string; - description?: string; - isPublic: boolean; - maxMembers: number; - // Add other config fields as needed -} - -export interface LeagueAdminProtestsViewModel { - protests: ProtestViewModel[]; -} - -export interface LeagueSeasonSummaryViewModel { - id: string; - name: string; - startDate?: string; - endDate?: string; - status: string; -} - -export interface LeagueMembershipsViewModel { - members: LeagueMemberViewModel[]; -} - -export interface LeagueStandingsViewModel { - standings: StandingEntryViewModel[]; -} - -export interface LeagueScheduleViewModel { - races: ScheduledRaceViewModel[]; -} - -export interface LeagueStatsViewModel { - leagueId: string; - totalRaces: number; - completedRaces: number; - scheduledRaces: number; - averageSOF?: number; - highestSOF?: number; - lowestSOF?: number; -} - -export interface LeagueAdminViewModel { - config: LeagueConfigFormModelDto; - members: LeagueMemberViewModel[]; - joinRequests: LeagueJoinRequestViewModel[]; -} - -export interface CreateLeagueInput { - name: string; - description?: string; - isPublic: boolean; - maxMembers: number; - ownerId: string; -} - -export interface CreateLeagueOutput { - leagueId: string; - success: boolean; -} - -// Driver Types -export interface DriverLeaderboardItemViewModel { - id: string; - name: string; - avatarUrl?: string; - rating: number; - wins: number; - races: number; - skillLevel: string; -} - -export interface DriversLeaderboardViewModel { - drivers: DriverLeaderboardItemViewModel[]; -} - -export interface DriverStatsDto { - totalDrivers: number; -} - -export interface CompleteOnboardingInput { - iracingId: string; - displayName: string; -} - -export interface CompleteOnboardingOutput { - driverId: string; - success: boolean; -} - -export interface DriverRegistrationStatusViewModel { - isRegistered: boolean; - raceId: string; - driverId: string; -} - -// Team Types -export interface TeamSummaryViewModel { - id: string; - name: string; - logoUrl?: string; - memberCount: number; - rating: number; -} - -export interface AllTeamsViewModel { - teams: TeamSummaryViewModel[]; -} - -export interface TeamMemberViewModel { - driverId: string; - driver?: DriverDTO; - role: string; - joinedAt: string; -} - -export interface TeamJoinRequestItemViewModel { - id: string; - teamId: string; - driverId: string; - requestedAt: string; - message?: string; -} - -export interface TeamDetailsViewModel { - id: string; - name: string; - description?: string; - logoUrl?: string; - memberCount: number; - ownerId: string; - members: TeamMemberViewModel[]; -} - -export interface TeamMembersViewModel { - members: TeamMemberViewModel[]; -} - -export interface TeamJoinRequestsViewModel { - requests: TeamJoinRequestItemViewModel[]; -} - -export interface DriverTeamViewModel { - teamId: string; - teamName: string; - role: string; - joinedAt: Date; -} - -export interface CreateTeamInput { - name: string; - description?: string; - ownerId: string; -} - -export interface CreateTeamOutput { - teamId: string; - success: boolean; -} - -export interface UpdateTeamInput { - name?: string; - description?: string; - logoUrl?: string; -} - -export interface UpdateTeamOutput { - success: boolean; -} - -// Race Types -export interface RaceListItemViewModel { - id: string; - name: string; - leagueId: string; - leagueName: string; - scheduledTime: string; - status: string; - trackName?: string; -} - -export interface AllRacesPageViewModel { - races: RaceListItemViewModel[]; -} - -export interface RaceStatsDto { - totalRaces: number; -} - -// Race Management Types -export interface RaceDetailEntryViewModel { - id: string; - name: string; - country: string; - avatarUrl: string; - rating: number | null; - isCurrentUser: boolean; -} - -export interface RaceDetailUserResultViewModel { - position: number; - startPosition: number; - incidents: number; - fastestLap: number; - positionChange: number; - isPodium: boolean; - isClean: boolean; - ratingChange: number | null; -} - -export interface RaceDetailRaceViewModel { - id: string; - leagueId: string; - track: string; - car: string; - scheduledAt: string; - sessionType: string; - status: string; - strengthOfField: number | null; - registeredCount?: number; - maxParticipants?: number; -} - -export interface RaceDetailLeagueViewModel { - id: string; - name: string; - description: string; - settings: { - maxDrivers?: number; - qualifyingFormat?: string; - }; -} - -export interface RaceDetailRegistrationViewModel { - isUserRegistered: boolean; - canRegister: boolean; -} - -export interface RaceDetailViewModel { - race: RaceDetailRaceViewModel | null; - league: RaceDetailLeagueViewModel | null; - entryList: RaceDetailEntryViewModel[]; - registration: RaceDetailRegistrationViewModel; - userResult: RaceDetailUserResultViewModel | null; - error?: string; -} - -export interface RacesPageDataRaceViewModel { - id: string; - track: string; - car: string; - scheduledAt: string; - status: string; - leagueId: string; - leagueName: string; - strengthOfField: number | null; - isUpcoming: boolean; - isLive: boolean; - isPast: boolean; -} - -export interface RacesPageDataViewModel { - races: RacesPageDataRaceViewModel[]; -} - -export interface RaceResultViewModel { - driverId: string; - driverName: string; - avatarUrl: string; - position: number; - startPosition: number; - incidents: number; - fastestLap: number; - positionChange: number; - isPodium: boolean; - isClean: boolean; -} - -export interface RaceResultsDetailViewModel { - raceId: string; - track: string; - results: RaceResultViewModel[]; -} - -export interface RaceWithSOFViewModel { - id: string; - track: string; - strengthOfField: number | null; -} - -export interface RaceProtestViewModel { - id: string; - protestingDriverId: string; - accusedDriverId: string; - incident: { - lap: number; - description: string; - }; - status: string; - filedAt: string; -} - -export interface RaceProtestsViewModel { - protests: RaceProtestViewModel[]; - driverMap: Record; -} - -export interface RacePenaltyViewModel { - id: string; - driverId: string; - type: string; - value: number; - reason: string; - issuedBy: string; - issuedAt: string; - notes?: string; -} - -export interface RacePenaltiesViewModel { - penalties: RacePenaltyViewModel[]; - driverMap: Record; -} - -export interface RegisterForRaceParams { - leagueId: string; - driverId: string; -} - -export interface WithdrawFromRaceParams { - driverId: string; -} - -export interface ImportRaceResultsInput { - resultsFileContent: string; -} - -export interface ImportRaceResultsSummaryViewModel { - success: boolean; - raceId: string; - driversProcessed: number; - resultsRecorded: number; - errors?: string[]; -} - -// Sponsor Types -export interface GetEntitySponsorshipPricingResultDto { - mainSlotPrice: number; - secondarySlotPrice: number; - currency: string; -} - -export interface SponsorViewModel { - id: string; - name: string; - logoUrl?: string; - websiteUrl?: string; -} - -export interface GetSponsorsOutput { - sponsors: SponsorViewModel[]; -} - -export interface CreateSponsorInput { - name: string; - logoUrl?: string; - websiteUrl?: string; - userId: string; -} - -export interface CreateSponsorOutput { - sponsorId: string; - success: boolean; -} - -export interface SponsorDashboardDTO { - sponsorId: string; - sponsorName: string; - totalSponsorships: number; - activeSponsorships: number; - totalInvestment: number; -} - -export interface SponsorshipDetailViewModel { - id: string; - leagueId: string; - leagueName: string; - seasonId: string; - tier: 'main' | 'secondary'; - status: string; - amount: number; - currency: string; -} - -export interface SponsorSponsorshipsDTO { - sponsorId: string; - sponsorName: string; - sponsorships: SponsorshipDetailViewModel[]; -} - -// Media Types -export interface RequestAvatarGenerationInput { - driverId: string; - style?: string; -} - -export interface RequestAvatarGenerationOutput { - success: boolean; - avatarUrl?: string; - error?: string; -} - -// Analytics Types -export interface RecordPageViewInput { - path: string; - userId?: string; - sessionId?: string; -} - -export interface RecordPageViewOutput { - success: boolean; -} - -export interface RecordEngagementInput { - eventType: string; - eventData?: Record; - userId?: string; - sessionId?: string; -} - -export interface RecordEngagementOutput { - success: boolean; -} - -// Auth Types -export interface LoginParams { - email: string; - password: string; -} - -export interface SignupParams { - email: string; - password: string; - displayName: string; -} - -export interface SessionData { - userId: string; - email: string; - displayName?: string; - driverId?: string; - isAuthenticated: boolean; -} - -// Payments Types -export interface PaymentViewModel { - id: string; - amount: number; - currency: string; - status: string; - createdAt: string; -} - -export interface GetPaymentsOutput { - payments: PaymentViewModel[]; -} - -export interface CreatePaymentInput { - amount: number; - currency: string; - leagueId: string; - driverId: string; - description?: string; -} - -export interface CreatePaymentOutput { - paymentId: string; - success: boolean; -} - -export interface MembershipFeeViewModel { - leagueId: string; - amount: number; - currency: string; - period: string; -} - -export interface MemberPaymentViewModel { - driverId: string; - amount: number; - paidAt: string; - status: string; -} - -export interface GetMembershipFeesOutput { - fees: MembershipFeeViewModel[]; - memberPayments: MemberPaymentViewModel[]; -} - -export interface PrizeViewModel { - id: string; - name: string; - amount: number; - currency: string; - position?: number; -} - -export interface GetPrizesOutput { - prizes: PrizeViewModel[]; -} - -export interface WalletTransactionViewModel { - id: string; - type: 'deposit' | 'withdrawal'; - amount: number; - description?: string; - createdAt: string; -} - -export interface WalletViewModel { - driverId: string; - balance: number; - currency: string; - transactions: WalletTransactionViewModel[]; -} - -export interface GetWalletOutput { - wallet: WalletViewModel; -} - -// ============================================================================ -// Base API Client -// ============================================================================ - -class BaseApiClient { - private baseUrl: string; - - constructor(baseUrl: string) { - this.baseUrl = baseUrl; - } - - protected async request(method: string, path: string, data?: object): Promise { - const headers: HeadersInit = { - 'Content-Type': 'application/json', - }; - - const config: RequestInit = { - method, - headers, - credentials: 'include', // Include cookies for auth - }; - - if (data) { - config.body = JSON.stringify(data); - } - - const response = await fetch(`${this.baseUrl}${path}`, config); - - if (!response.ok) { - let errorData: { message?: string } = { message: response.statusText }; - try { - errorData = await response.json(); - } catch { - // Keep default error message - } - throw new Error(errorData.message || `API request failed with status ${response.status}`); - } - - const text = await response.text(); - if (!text) { - return null as T; - } - return JSON.parse(text) as T; - } - - protected get(path: string): Promise { - return this.request('GET', path); - } - - protected post(path: string, data: object): Promise { - return this.request('POST', path, data); - } - - protected put(path: string, data: object): Promise { - return this.request('PUT', path, data); - } - - protected delete(path: string): Promise { - return this.request('DELETE', path); - } - - protected patch(path: string, data: object): Promise { - return this.request('PATCH', path, data); - } -} - -// ============================================================================ -// Domain-Specific API Clients -// ============================================================================ - -class LeaguesApiClient extends BaseApiClient { - constructor(baseUrl: string) { - super(baseUrl); - } - - /** Get all leagues with capacity information */ - getAllWithCapacity(): Promise { - return this.get('/leagues/all-with-capacity'); - } - - /** Get total number of leagues */ - getTotal(): Promise { - return this.get('/leagues/total-leagues'); - } - - /** Get league standings */ - getStandings(leagueId: string): Promise { - return this.get(`/leagues/${leagueId}/standings`); - } - - /** Get league schedule */ - getSchedule(leagueId: string): Promise { - return this.get(`/leagues/${leagueId}/schedule`); - } - - /** Get league stats */ - getStats(leagueId: string): Promise { - return this.get(`/leagues/${leagueId}/stats`); - } - - /** Get league memberships */ - getMemberships(leagueId: string): Promise { - return this.get(`/leagues/${leagueId}/memberships`); - } - - /** Get league join requests */ - getJoinRequests(leagueId: string): Promise { - return this.get(`/leagues/${leagueId}/join-requests`); - } - - /** Approve a join request */ - approveJoinRequest(leagueId: string, requestId: string): Promise<{ success: boolean }> { - return this.post<{ success: boolean }>(`/leagues/${leagueId}/join-requests/approve`, { requestId }); - } - - /** Reject a join request */ - rejectJoinRequest(leagueId: string, requestId: string, reason?: string): Promise<{ success: boolean }> { - return this.post<{ success: boolean }>(`/leagues/${leagueId}/join-requests/reject`, { requestId, reason }); - } - - /** Get league admin permissions */ - getAdminPermissions(leagueId: string, performerDriverId: string): Promise { - return this.get(`/leagues/${leagueId}/permissions/${performerDriverId}`); - } - - /** Get league owner summary */ - getOwnerSummary(leagueId: string, ownerId: string): Promise { - return this.get(`/leagues/${leagueId}/owner-summary/${ownerId}`); - } - - /** Get league full config */ - getConfig(leagueId: string): Promise { - return this.get(`/leagues/${leagueId}/config`); - } - - /** Get league protests */ - getProtests(leagueId: string): Promise { - return this.get(`/leagues/${leagueId}/protests`); - } - - /** Get league seasons */ - getSeasons(leagueId: string): Promise { - return this.get(`/leagues/${leagueId}/seasons`); - } - - /** Get league admin data */ - getAdmin(leagueId: string): Promise { - return this.get(`/leagues/${leagueId}/admin`); - } - - /** Create a new league */ - create(input: CreateLeagueInput): Promise { - return this.post('/leagues', input); - } - - /** Remove a member from league */ - removeMember(leagueId: string, performerDriverId: string, targetDriverId: string): Promise<{ success: boolean }> { - return this.patch<{ success: boolean }>(`/leagues/${leagueId}/members/${targetDriverId}/remove`, { performerDriverId }); - } - - /** Update member role */ - updateMemberRole(leagueId: string, performerDriverId: string, targetDriverId: string, newRole: string): Promise<{ success: boolean }> { - return this.patch<{ success: boolean }>(`/leagues/${leagueId}/members/${targetDriverId}/role`, { performerDriverId, newRole }); - } -} - -class DriversApiClient extends BaseApiClient { - constructor(baseUrl: string) { - super(baseUrl); - } - - /** Get drivers leaderboard */ - getLeaderboard(): Promise { - return this.get('/drivers/leaderboard'); - } - - /** Get total number of drivers */ - getTotal(): Promise { - return this.get('/drivers/total-drivers'); - } - - /** Get current driver (based on session) */ - getCurrent(): Promise { - return this.get('/drivers/current'); - } - - /** Complete driver onboarding */ - completeOnboarding(input: CompleteOnboardingInput): Promise { - return this.post('/drivers/complete-onboarding', input); - } - - /** Get driver registration status for a race */ - getRegistrationStatus(driverId: string, raceId: string): Promise { - return this.get(`/drivers/${driverId}/races/${raceId}/registration-status`); - } -} - -class TeamsApiClient extends BaseApiClient { - constructor(baseUrl: string) { - super(baseUrl); - } - - /** Get all teams */ - getAll(): Promise { - return this.get('/teams/all'); - } - - /** Get team details */ - getDetails(teamId: string): Promise { - return this.get(`/teams/${teamId}`); - } - - /** Get team members */ - getMembers(teamId: string): Promise { - return this.get(`/teams/${teamId}/members`); - } - - /** Get team join requests */ - getJoinRequests(teamId: string): Promise { - return this.get(`/teams/${teamId}/join-requests`); - } - - /** Approve a join request */ - approveJoinRequest(teamId: string, requestId: string): Promise<{ success: boolean }> { - return this.post<{ success: boolean }>(`/teams/${teamId}/join-requests/approve`, { requestId }); - } - - /** Reject a join request */ - rejectJoinRequest(teamId: string, requestId: string, reason?: string): Promise<{ success: boolean }> { - return this.post<{ success: boolean }>(`/teams/${teamId}/join-requests/reject`, { requestId, reason }); - } - - /** Create a new team */ - create(input: CreateTeamInput): Promise { - return this.post('/teams', input); - } - - /** Update team */ - update(teamId: string, input: UpdateTeamInput): Promise { - return this.patch(`/teams/${teamId}`, input); - } - - /** Get driver's team */ - getDriverTeam(driverId: string): Promise { - return this.get(`/teams/driver/${driverId}`); - } -} - -class RacesApiClient extends BaseApiClient { - constructor(baseUrl: string) { - super(baseUrl); - } - - /** Get all races */ - getAll(): Promise { - return this.get('/races/all'); - } - - /** Get total number of races */ - getTotal(): Promise { - return this.get('/races/total-races'); - } - - /** Get races page data */ - getPageData(): Promise { - return this.get('/races/page-data'); - } - - /** Get all races page data */ - getAllPageData(): Promise { - return this.get('/races/all/page-data'); - } - - /** Get race detail */ - getDetail(raceId: string, driverId: string): Promise { - return this.get(`/races/${raceId}?driverId=${driverId}`); - } - - /** Get race results detail */ - getResultsDetail(raceId: string): Promise { - return this.get(`/races/${raceId}/results`); - } - - /** Get race with strength of field */ - getWithSOF(raceId: string): Promise { - return this.get(`/races/${raceId}/sof`); - } - - /** Get race protests */ - getProtests(raceId: string): Promise { - return this.get(`/races/${raceId}/protests`); - } - - /** Get race penalties */ - getPenalties(raceId: string): Promise { - return this.get(`/races/${raceId}/penalties`); - } - - /** Register for race */ - register(raceId: string, params: RegisterForRaceParams): Promise { - return this.post(`/races/${raceId}/register`, params); - } - - /** Withdraw from race */ - withdraw(raceId: string, params: WithdrawFromRaceParams): Promise { - return this.post(`/races/${raceId}/withdraw`, params); - } - - /** Cancel race */ - cancel(raceId: string): Promise { - return this.post(`/races/${raceId}/cancel`, {}); - } - - /** Complete race */ - complete(raceId: string): Promise { - return this.post(`/races/${raceId}/complete`, {}); - } - - /** Import race results */ - importResults(raceId: string, input: ImportRaceResultsInput): Promise { - return this.post(`/races/${raceId}/import-results`, input); - } -} - -class SponsorsApiClient extends BaseApiClient { - constructor(baseUrl: string) { - super(baseUrl); - } - - /** Get sponsorship pricing */ - getPricing(): Promise { - return this.get('/sponsors/pricing'); - } - - /** Get all sponsors */ - getAll(): Promise { - return this.get('/sponsors'); - } - - /** Create a new sponsor */ - create(input: CreateSponsorInput): Promise { - return this.post('/sponsors', input); - } - - /** Get sponsor dashboard */ - getDashboard(sponsorId: string): Promise { - return this.get(`/sponsors/dashboard/${sponsorId}`); - } - - /** Get sponsor sponsorships */ - getSponsorships(sponsorId: string): Promise { - return this.get(`/sponsors/${sponsorId}/sponsorships`); - } -} - -class MediaApiClient extends BaseApiClient { - constructor(baseUrl: string) { - super(baseUrl); - } - - /** Request avatar generation */ - requestAvatarGeneration(input: RequestAvatarGenerationInput): Promise { - return this.post('/media/avatar/generate', input); - } -} - -class AnalyticsApiClient extends BaseApiClient { - constructor(baseUrl: string) { - super(baseUrl); - } - - /** Record a page view */ - recordPageView(input: RecordPageViewInput): Promise { - return this.post('/analytics/page-view', input); - } - - /** Record an engagement event */ - recordEngagement(input: RecordEngagementInput): Promise { - return this.post('/analytics/engagement', input); - } -} - -class AuthApiClient extends BaseApiClient { - constructor(baseUrl: string) { - super(baseUrl); - } - - /** Sign up with email */ - signup(params: SignupParams): Promise { - return this.post('/auth/signup', params); - } - - /** Login with email */ - login(params: LoginParams): Promise { - return this.post('/auth/login', params); - } - - /** Get current session */ - getSession(): Promise { - return this.get('/auth/session'); - } - - /** Logout */ - logout(): Promise { - return this.post('/auth/logout', {}); - } - - /** Start iRacing auth redirect */ - getIracingAuthUrl(returnTo?: string): string { - const baseUrl = process.env.NEXT_PUBLIC_API_BASE_URL || 'http://localhost:3001'; - const params = returnTo ? `?returnTo=${encodeURIComponent(returnTo)}` : ''; - return `${baseUrl}/auth/iracing/start${params}`; - } -} - -class PaymentsApiClient extends BaseApiClient { - constructor(baseUrl: string) { - super(baseUrl); - } - - /** Get payments */ - getPayments(leagueId?: string, driverId?: string): Promise { - const params = new URLSearchParams(); - if (leagueId) params.append('leagueId', leagueId); - if (driverId) params.append('driverId', driverId); - const query = params.toString(); - return this.get(`/payments${query ? `?${query}` : ''}`); - } - - /** Create a payment */ - createPayment(input: CreatePaymentInput): Promise { - return this.post('/payments', input); - } - - /** Update payment status */ - updatePaymentStatus(paymentId: string, status: string): Promise<{ success: boolean }> { - return this.patch<{ success: boolean }>('/payments/status', { paymentId, status }); - } - - /** Get membership fees */ - getMembershipFees(leagueId: string): Promise { - return this.get(`/payments/membership-fees?leagueId=${leagueId}`); - } - - /** Upsert membership fee */ - upsertMembershipFee(leagueId: string, amount: number, currency: string, period: string): Promise<{ success: boolean }> { - return this.post<{ success: boolean }>('/payments/membership-fees', { leagueId, amount, currency, period }); - } - - /** Update member payment */ - updateMemberPayment(leagueId: string, driverId: string, amount: number, paidAt: string): Promise<{ success: boolean }> { - return this.patch<{ success: boolean }>('/payments/membership-fees/member-payment', { leagueId, driverId, amount, paidAt }); - } - - /** Get prizes */ - getPrizes(leagueId?: string, seasonId?: string): Promise { - const params = new URLSearchParams(); - if (leagueId) params.append('leagueId', leagueId); - if (seasonId) params.append('seasonId', seasonId); - const query = params.toString(); - return this.get(`/payments/prizes${query ? `?${query}` : ''}`); - } - - /** Create a prize */ - createPrize(name: string, amount: number, currency: string, leagueId: string, position?: number): Promise<{ prizeId: string; success: boolean }> { - return this.post<{ prizeId: string; success: boolean }>('/payments/prizes', { name, amount, currency, leagueId, position }); - } - - /** Award a prize */ - awardPrize(prizeId: string, driverId: string): Promise<{ success: boolean }> { - return this.patch<{ success: boolean }>('/payments/prizes/award', { prizeId, driverId }); - } - - /** Delete a prize */ - deletePrize(prizeId: string): Promise<{ success: boolean }> { - return this.delete<{ success: boolean }>(`/payments/prizes?prizeId=${prizeId}`); - } - - /** Get wallet */ - getWallet(driverId: string): Promise { - return this.get(`/payments/wallets?driverId=${driverId}`); - } - - /** Process wallet transaction */ - processWalletTransaction(driverId: string, type: 'deposit' | 'withdrawal', amount: number, description?: string): Promise<{ success: boolean }> { - return this.post<{ success: boolean }>('/payments/wallets/transactions', { driverId, type, amount, description }); - } -} - -// ============================================================================ -// Main API Client with Domain Namespaces -// ============================================================================ - -class ApiClient { - public readonly leagues: LeaguesApiClient; - public readonly drivers: DriversApiClient; - public readonly teams: TeamsApiClient; - public readonly races: RacesApiClient; - public readonly sponsors: SponsorsApiClient; - public readonly media: MediaApiClient; - public readonly analytics: AnalyticsApiClient; - public readonly auth: AuthApiClient; - public readonly payments: PaymentsApiClient; - - constructor(baseUrl: string) { - this.leagues = new LeaguesApiClient(baseUrl); - this.drivers = new DriversApiClient(baseUrl); - this.teams = new TeamsApiClient(baseUrl); - this.races = new RacesApiClient(baseUrl); - this.sponsors = new SponsorsApiClient(baseUrl); - this.media = new MediaApiClient(baseUrl); - this.analytics = new AnalyticsApiClient(baseUrl); - this.auth = new AuthApiClient(baseUrl); - this.payments = new PaymentsApiClient(baseUrl); - } -} - -// ============================================================================ -// Singleton Instance -// ============================================================================ - -const API_BASE_URL = process.env.NEXT_PUBLIC_API_BASE_URL || 'http://localhost:3001'; - -export const apiClient = new ApiClient(API_BASE_URL); - -// Default export for convenience -export default apiClient; +// Re-export DTO types for backward compatibility +export type * from './dtos'; diff --git a/apps/website/lib/dtos/AllLeaguesWithCapacityDto.ts b/apps/website/lib/dtos/AllLeaguesWithCapacityDto.ts new file mode 100644 index 000000000..9102a4c43 --- /dev/null +++ b/apps/website/lib/dtos/AllLeaguesWithCapacityDto.ts @@ -0,0 +1,9 @@ +import type { LeagueSummaryDto } from './LeagueSummaryDto'; + +/** + * All leagues with capacity transport object + * Contains a list of leagues with their capacity information + */ +export interface AllLeaguesWithCapacityDto { + leagues: LeagueSummaryDto[]; +} \ No newline at end of file diff --git a/apps/website/lib/dtos/AllRacesPageDto.ts b/apps/website/lib/dtos/AllRacesPageDto.ts new file mode 100644 index 000000000..f1401ebd3 --- /dev/null +++ b/apps/website/lib/dtos/AllRacesPageDto.ts @@ -0,0 +1,9 @@ +import type { RaceListItemDto } from './RaceListItemDto'; + +/** + * All races page data transfer object + * List of all races for the races page + */ +export interface AllRacesPageDto { + races: RaceListItemDto[]; +} \ No newline at end of file diff --git a/apps/website/lib/dtos/AllTeamsDto.ts b/apps/website/lib/dtos/AllTeamsDto.ts new file mode 100644 index 000000000..7e015f060 --- /dev/null +++ b/apps/website/lib/dtos/AllTeamsDto.ts @@ -0,0 +1,9 @@ +import type { TeamSummaryDto } from './TeamSummaryDto'; + +/** + * All teams data transfer object + * List of all teams + */ +export interface AllTeamsDto { + teams: TeamSummaryDto[]; +} \ No newline at end of file diff --git a/apps/website/lib/dtos/CompleteOnboardingInputDto.ts b/apps/website/lib/dtos/CompleteOnboardingInputDto.ts new file mode 100644 index 000000000..9ff384dc8 --- /dev/null +++ b/apps/website/lib/dtos/CompleteOnboardingInputDto.ts @@ -0,0 +1,8 @@ +/** + * Complete onboarding input data transfer object + * Input for completing driver onboarding + */ +export interface CompleteOnboardingInputDto { + iracingId: string; + displayName: string; +} \ No newline at end of file diff --git a/apps/website/lib/dtos/CompleteOnboardingOutputDto.ts b/apps/website/lib/dtos/CompleteOnboardingOutputDto.ts new file mode 100644 index 000000000..9283755e4 --- /dev/null +++ b/apps/website/lib/dtos/CompleteOnboardingOutputDto.ts @@ -0,0 +1,8 @@ +/** + * Complete onboarding output data transfer object + * Output from completing driver onboarding + */ +export interface CompleteOnboardingOutputDto { + driverId: string; + success: boolean; +} \ No newline at end of file diff --git a/apps/website/lib/dtos/CreateLeagueInputDto.ts b/apps/website/lib/dtos/CreateLeagueInputDto.ts new file mode 100644 index 000000000..4398f9fc9 --- /dev/null +++ b/apps/website/lib/dtos/CreateLeagueInputDto.ts @@ -0,0 +1,11 @@ +/** + * Create league input transport object + * Input data for creating a new league + */ +export interface CreateLeagueInputDto { + name: string; + description?: string; + isPublic: boolean; + maxMembers: number; + ownerId: string; +} \ No newline at end of file diff --git a/apps/website/lib/dtos/CreateLeagueOutputDto.ts b/apps/website/lib/dtos/CreateLeagueOutputDto.ts new file mode 100644 index 000000000..003ca14c0 --- /dev/null +++ b/apps/website/lib/dtos/CreateLeagueOutputDto.ts @@ -0,0 +1,8 @@ +/** + * Create league output transport object + * Output data after creating a new league + */ +export interface CreateLeagueOutputDto { + leagueId: string; + success: boolean; +} \ No newline at end of file diff --git a/apps/website/lib/dtos/CreatePaymentInputDto.ts b/apps/website/lib/dtos/CreatePaymentInputDto.ts new file mode 100644 index 000000000..66483a032 --- /dev/null +++ b/apps/website/lib/dtos/CreatePaymentInputDto.ts @@ -0,0 +1,11 @@ +/** + * Create payment input data transfer object + * Input for creating a payment + */ +export interface CreatePaymentInputDto { + amount: number; + currency: string; + leagueId: string; + driverId: string; + description?: string; +} \ No newline at end of file diff --git a/apps/website/lib/dtos/CreatePaymentOutputDto.ts b/apps/website/lib/dtos/CreatePaymentOutputDto.ts new file mode 100644 index 000000000..4ab8feabf --- /dev/null +++ b/apps/website/lib/dtos/CreatePaymentOutputDto.ts @@ -0,0 +1,8 @@ +/** + * Create payment output data transfer object + * Output from creating a payment + */ +export interface CreatePaymentOutputDto { + paymentId: string; + success: boolean; +} \ No newline at end of file diff --git a/apps/website/lib/dtos/CreateSponsorInputDto.ts b/apps/website/lib/dtos/CreateSponsorInputDto.ts new file mode 100644 index 000000000..cbb0ea185 --- /dev/null +++ b/apps/website/lib/dtos/CreateSponsorInputDto.ts @@ -0,0 +1,10 @@ +/** + * Create sponsor input data transfer object + * Input for creating a new sponsor + */ +export interface CreateSponsorInputDto { + name: string; + logoUrl?: string; + websiteUrl?: string; + userId: string; +} \ No newline at end of file diff --git a/apps/website/lib/dtos/CreateSponsorOutputDto.ts b/apps/website/lib/dtos/CreateSponsorOutputDto.ts new file mode 100644 index 000000000..1a4b74234 --- /dev/null +++ b/apps/website/lib/dtos/CreateSponsorOutputDto.ts @@ -0,0 +1,8 @@ +/** + * Create sponsor output data transfer object + * Output from creating a sponsor + */ +export interface CreateSponsorOutputDto { + sponsorId: string; + success: boolean; +} \ No newline at end of file diff --git a/apps/website/lib/dtos/CreateTeamInputDto.ts b/apps/website/lib/dtos/CreateTeamInputDto.ts new file mode 100644 index 000000000..f887bd0cf --- /dev/null +++ b/apps/website/lib/dtos/CreateTeamInputDto.ts @@ -0,0 +1,9 @@ +/** + * Create team input data transfer object + * Input for creating a new team + */ +export interface CreateTeamInputDto { + name: string; + description?: string; + ownerId: string; +} \ No newline at end of file diff --git a/apps/website/lib/dtos/CreateTeamOutputDto.ts b/apps/website/lib/dtos/CreateTeamOutputDto.ts new file mode 100644 index 000000000..f437ad144 --- /dev/null +++ b/apps/website/lib/dtos/CreateTeamOutputDto.ts @@ -0,0 +1,8 @@ +/** + * Create team output data transfer object + * Output from creating a team + */ +export interface CreateTeamOutputDto { + teamId: string; + success: boolean; +} \ No newline at end of file diff --git a/apps/website/lib/dtos/DriverDto.ts b/apps/website/lib/dtos/DriverDto.ts new file mode 100644 index 000000000..3e167cf1d --- /dev/null +++ b/apps/website/lib/dtos/DriverDto.ts @@ -0,0 +1,11 @@ +/** + * Driver transport object + * Represents a driver as received from the API + */ +export interface DriverDto { + id: string; + name: string; + avatarUrl?: string; + iracingId?: string; + rating?: number; +} \ No newline at end of file diff --git a/apps/website/lib/dtos/DriverLeaderboardItemDto.ts b/apps/website/lib/dtos/DriverLeaderboardItemDto.ts new file mode 100644 index 000000000..78e786820 --- /dev/null +++ b/apps/website/lib/dtos/DriverLeaderboardItemDto.ts @@ -0,0 +1,16 @@ +/** + * Driver leaderboard item data transfer object + * Represents a driver in the global leaderboard + */ +export interface DriverLeaderboardItemDto { + id: string; + name: string; + avatarUrl?: string; + rating: number; + wins: number; + races: number; + skillLevel: string; + isActive: boolean; + nationality: string; + podiums: number; +} \ No newline at end of file diff --git a/apps/website/lib/dtos/DriverRegistrationStatusDto.ts b/apps/website/lib/dtos/DriverRegistrationStatusDto.ts new file mode 100644 index 000000000..0d5faab49 --- /dev/null +++ b/apps/website/lib/dtos/DriverRegistrationStatusDto.ts @@ -0,0 +1,9 @@ +/** + * Driver registration status data transfer object + * Represents a driver's registration status for a race + */ +export interface DriverRegistrationStatusDto { + isRegistered: boolean; + raceId: string; + driverId: string; +} \ No newline at end of file diff --git a/apps/website/lib/dtos/DriverRowDto.ts b/apps/website/lib/dtos/DriverRowDto.ts new file mode 100644 index 000000000..8996b4e4f --- /dev/null +++ b/apps/website/lib/dtos/DriverRowDto.ts @@ -0,0 +1,8 @@ +/** + * Driver row data transfer object + * Represents a driver in a table row + */ +export interface DriverRowDto { + id: string; + name: string; +} \ No newline at end of file diff --git a/apps/website/lib/dtos/DriverStatsDto.ts b/apps/website/lib/dtos/DriverStatsDto.ts new file mode 100644 index 000000000..de1dbc72c --- /dev/null +++ b/apps/website/lib/dtos/DriverStatsDto.ts @@ -0,0 +1,7 @@ +/** + * Driver stats data transfer object + * Global driver statistics + */ +export interface DriverStatsDto { + totalDrivers: number; +} \ No newline at end of file diff --git a/apps/website/lib/dtos/DriverTeamDto.ts b/apps/website/lib/dtos/DriverTeamDto.ts new file mode 100644 index 000000000..4480520af --- /dev/null +++ b/apps/website/lib/dtos/DriverTeamDto.ts @@ -0,0 +1,10 @@ +/** + * Driver team data transfer object + * Represents a driver's team membership + */ +export interface DriverTeamDto { + teamId: string; + teamName: string; + role: string; + joinedAt: Date; +} \ No newline at end of file diff --git a/apps/website/lib/dtos/DriversLeaderboardDto.ts b/apps/website/lib/dtos/DriversLeaderboardDto.ts new file mode 100644 index 000000000..4f7e6d39b --- /dev/null +++ b/apps/website/lib/dtos/DriversLeaderboardDto.ts @@ -0,0 +1,9 @@ +import type { DriverLeaderboardItemDto } from './DriverLeaderboardItemDto'; + +/** + * Drivers leaderboard data transfer object + * Contains the list of top drivers + */ +export interface DriversLeaderboardDto { + drivers: DriverLeaderboardItemDto[]; +} \ No newline at end of file diff --git a/apps/website/lib/dtos/GetEntitySponsorshipPricingResultDto.ts b/apps/website/lib/dtos/GetEntitySponsorshipPricingResultDto.ts new file mode 100644 index 000000000..06f4671bf --- /dev/null +++ b/apps/website/lib/dtos/GetEntitySponsorshipPricingResultDto.ts @@ -0,0 +1,9 @@ +/** + * Get entity sponsorship pricing result data transfer object + * Pricing information for sponsorship slots + */ +export interface GetEntitySponsorshipPricingResultDto { + mainSlotPrice: number; + secondarySlotPrice: number; + currency: string; +} \ No newline at end of file diff --git a/apps/website/lib/dtos/GetMembershipFeesOutputDto.ts b/apps/website/lib/dtos/GetMembershipFeesOutputDto.ts new file mode 100644 index 000000000..57f153850 --- /dev/null +++ b/apps/website/lib/dtos/GetMembershipFeesOutputDto.ts @@ -0,0 +1,11 @@ +import type { MembershipFeeDto } from './MembershipFeeDto'; +import type { MemberPaymentDto } from './MemberPaymentDto'; + +/** + * Get membership fees output data transfer object + * Output containing membership fees and payments + */ +export interface GetMembershipFeesOutputDto { + fees: MembershipFeeDto[]; + memberPayments: MemberPaymentDto[]; +} \ No newline at end of file diff --git a/apps/website/lib/dtos/GetPaymentsOutputDto.ts b/apps/website/lib/dtos/GetPaymentsOutputDto.ts new file mode 100644 index 000000000..9df9d5d28 --- /dev/null +++ b/apps/website/lib/dtos/GetPaymentsOutputDto.ts @@ -0,0 +1,9 @@ +import type { PaymentDto } from './PaymentDto'; + +/** + * Get payments output data transfer object + * Output containing list of payments + */ +export interface GetPaymentsOutputDto { + payments: PaymentDto[]; +} \ No newline at end of file diff --git a/apps/website/lib/dtos/GetPrizesOutputDto.ts b/apps/website/lib/dtos/GetPrizesOutputDto.ts new file mode 100644 index 000000000..b8471e425 --- /dev/null +++ b/apps/website/lib/dtos/GetPrizesOutputDto.ts @@ -0,0 +1,9 @@ +import type { PrizeDto } from './PrizeDto'; + +/** + * Get prizes output data transfer object + * Output containing list of prizes + */ +export interface GetPrizesOutputDto { + prizes: PrizeDto[]; +} \ No newline at end of file diff --git a/apps/website/lib/dtos/GetSponsorsOutputDto.ts b/apps/website/lib/dtos/GetSponsorsOutputDto.ts new file mode 100644 index 000000000..f5575efed --- /dev/null +++ b/apps/website/lib/dtos/GetSponsorsOutputDto.ts @@ -0,0 +1,9 @@ +import type { SponsorDto } from './SponsorDto'; + +/** + * Get sponsors output data transfer object + * Output containing list of sponsors + */ +export interface GetSponsorsOutputDto { + sponsors: SponsorDto[]; +} \ No newline at end of file diff --git a/apps/website/lib/dtos/GetWalletOutputDto.ts b/apps/website/lib/dtos/GetWalletOutputDto.ts new file mode 100644 index 000000000..0d6364c22 --- /dev/null +++ b/apps/website/lib/dtos/GetWalletOutputDto.ts @@ -0,0 +1,9 @@ +import type { WalletDto } from './WalletDto'; + +/** + * Get wallet output data transfer object + * Output containing wallet information + */ +export interface GetWalletOutputDto { + wallet: WalletDto; +} \ No newline at end of file diff --git a/apps/website/lib/dtos/ImportRaceResultsInputDto.ts b/apps/website/lib/dtos/ImportRaceResultsInputDto.ts new file mode 100644 index 000000000..59eb9b949 --- /dev/null +++ b/apps/website/lib/dtos/ImportRaceResultsInputDto.ts @@ -0,0 +1,7 @@ +/** + * Import race results input data transfer object + * Input for importing race results + */ +export interface ImportRaceResultsInputDto { + resultsFileContent: string; +} \ No newline at end of file diff --git a/apps/website/lib/dtos/ImportRaceResultsSummaryDto.ts b/apps/website/lib/dtos/ImportRaceResultsSummaryDto.ts new file mode 100644 index 000000000..d0926f06e --- /dev/null +++ b/apps/website/lib/dtos/ImportRaceResultsSummaryDto.ts @@ -0,0 +1,11 @@ +/** + * Import race results summary data transfer object + * Summary of race results import operation + */ +export interface ImportRaceResultsSummaryDto { + success: boolean; + raceId: string; + driversProcessed: number; + resultsRecorded: number; + errors?: string[]; +} \ No newline at end of file diff --git a/apps/website/lib/dtos/ImportResultRowDto.ts b/apps/website/lib/dtos/ImportResultRowDto.ts new file mode 100644 index 000000000..7ab0d6213 --- /dev/null +++ b/apps/website/lib/dtos/ImportResultRowDto.ts @@ -0,0 +1,13 @@ +/** + * Import result row data transfer object + * Represents a row in imported race results + */ +export interface ImportResultRowDto { + id: string; + raceId: string; + driverId: string; + position: number; + fastestLap: number; + incidents: number; + startPosition: number; +} \ No newline at end of file diff --git a/apps/website/lib/dtos/LeagueAdminDto.ts b/apps/website/lib/dtos/LeagueAdminDto.ts new file mode 100644 index 000000000..b5452eb8d --- /dev/null +++ b/apps/website/lib/dtos/LeagueAdminDto.ts @@ -0,0 +1,13 @@ +import type { LeagueMemberDto } from './LeagueMemberDto'; +import type { LeagueJoinRequestDto } from './LeagueJoinRequestDto'; +import type { LeagueConfigFormModelDto } from './LeagueConfigFormModelDto'; + +/** + * League admin transport object + * Contains all data needed for league administration + */ +export interface LeagueAdminDto { + config: LeagueConfigFormModelDto; + members: LeagueMemberDto[]; + joinRequests: LeagueJoinRequestDto[]; +} \ No newline at end of file diff --git a/apps/website/lib/dtos/LeagueAdminPermissionsDto.ts b/apps/website/lib/dtos/LeagueAdminPermissionsDto.ts new file mode 100644 index 000000000..265335a0c --- /dev/null +++ b/apps/website/lib/dtos/LeagueAdminPermissionsDto.ts @@ -0,0 +1,12 @@ +/** + * League admin permissions transport object + * Defines the administrative permissions for a user in a league + */ +export interface LeagueAdminPermissionsDto { + canManageMembers: boolean; + canManageRaces: boolean; + canManageSettings: boolean; + canManageProtests: boolean; + isOwner: boolean; + isAdmin: boolean; +} \ No newline at end of file diff --git a/apps/website/lib/dtos/LeagueAdminProtestsDto.ts b/apps/website/lib/dtos/LeagueAdminProtestsDto.ts new file mode 100644 index 000000000..0676be25b --- /dev/null +++ b/apps/website/lib/dtos/LeagueAdminProtestsDto.ts @@ -0,0 +1,9 @@ +import type { ProtestViewModel } from '../apiClient'; + +/** + * League admin protests transport object + * Contains protests for league administration + */ +export interface LeagueAdminProtestsDto { + protests: ProtestViewModel[]; +} \ No newline at end of file diff --git a/apps/website/lib/dtos/LeagueConfigFormModelDto.ts b/apps/website/lib/dtos/LeagueConfigFormModelDto.ts new file mode 100644 index 000000000..edc6cee07 --- /dev/null +++ b/apps/website/lib/dtos/LeagueConfigFormModelDto.ts @@ -0,0 +1,12 @@ +/** + * League configuration form model transport object + * Used for league configuration forms + */ +export interface LeagueConfigFormModelDto { + id: string; + name: string; + description?: string; + isPublic: boolean; + maxMembers: number; + // Add other config fields as needed +} \ No newline at end of file diff --git a/apps/website/lib/dtos/LeagueJoinRequestDto.ts b/apps/website/lib/dtos/LeagueJoinRequestDto.ts new file mode 100644 index 000000000..bdad28c51 --- /dev/null +++ b/apps/website/lib/dtos/LeagueJoinRequestDto.ts @@ -0,0 +1,11 @@ +/** + * League join request transport object + * Represents a driver's request to join a league + */ +export interface LeagueJoinRequestDto { + id: string; + leagueId: string; + driverId: string; + requestedAt: Date; + message?: string; +} \ No newline at end of file diff --git a/apps/website/lib/dtos/LeagueMemberDto.ts b/apps/website/lib/dtos/LeagueMemberDto.ts new file mode 100644 index 000000000..92d0ef813 --- /dev/null +++ b/apps/website/lib/dtos/LeagueMemberDto.ts @@ -0,0 +1,12 @@ +import type { DriverDto } from './DriverDto'; + +/** + * League member data transfer object + * Represents a driver's membership in a league + */ +export interface LeagueMemberDto { + driverId: string; + driver?: DriverDto; + role: string; + joinedAt: string; +} \ No newline at end of file diff --git a/apps/website/lib/dtos/LeagueMembershipsDto.ts b/apps/website/lib/dtos/LeagueMembershipsDto.ts new file mode 100644 index 000000000..b5e9c4933 --- /dev/null +++ b/apps/website/lib/dtos/LeagueMembershipsDto.ts @@ -0,0 +1,9 @@ +import type { LeagueMemberViewModel } from '../apiClient'; + +/** + * League memberships transport object + * Contains the list of league members + */ +export interface LeagueMembershipsDto { + members: LeagueMemberViewModel[]; +} \ No newline at end of file diff --git a/apps/website/lib/dtos/LeagueOwnerSummaryDto.ts b/apps/website/lib/dtos/LeagueOwnerSummaryDto.ts new file mode 100644 index 000000000..00a133002 --- /dev/null +++ b/apps/website/lib/dtos/LeagueOwnerSummaryDto.ts @@ -0,0 +1,10 @@ +/** + * League owner summary transport object + * Summary information for league owners + */ +export interface LeagueOwnerSummaryDto { + leagueId: string; + leagueName: string; + memberCount: number; + pendingRequests: number; +} \ No newline at end of file diff --git a/apps/website/lib/dtos/LeagueScheduleDto.ts b/apps/website/lib/dtos/LeagueScheduleDto.ts new file mode 100644 index 000000000..a35ecdad3 --- /dev/null +++ b/apps/website/lib/dtos/LeagueScheduleDto.ts @@ -0,0 +1,9 @@ +import type { ScheduledRaceViewModel } from '../apiClient'; + +/** + * League schedule transport object + * Contains the scheduled races for a league + */ +export interface LeagueScheduleDto { + races: ScheduledRaceViewModel[]; +} \ No newline at end of file diff --git a/apps/website/lib/dtos/LeagueSeasonSummaryDto.ts b/apps/website/lib/dtos/LeagueSeasonSummaryDto.ts new file mode 100644 index 000000000..43cfa7765 --- /dev/null +++ b/apps/website/lib/dtos/LeagueSeasonSummaryDto.ts @@ -0,0 +1,11 @@ +/** + * League season summary data transfer object + * Represents a season within a league + */ +export interface LeagueSeasonSummaryDto { + id: string; + name: string; + startDate?: string; + endDate?: string; + status: string; +} \ No newline at end of file diff --git a/apps/website/lib/dtos/LeagueStandingsDto.ts b/apps/website/lib/dtos/LeagueStandingsDto.ts new file mode 100644 index 000000000..5db76dc61 --- /dev/null +++ b/apps/website/lib/dtos/LeagueStandingsDto.ts @@ -0,0 +1,13 @@ +import type { StandingEntryDto } from './StandingEntryDto'; +import type { DriverDto } from './DriverDto'; +import type { LeagueMembership } from './LeagueMembershipDto'; + +/** + * League standings transport object + * Contains the current league standings + */ +export interface LeagueStandingsDto { + standings: StandingEntryDto[]; + drivers: DriverDto[]; + memberships: LeagueMembership[]; +} \ No newline at end of file diff --git a/apps/website/lib/dtos/LeagueStatsDto.ts b/apps/website/lib/dtos/LeagueStatsDto.ts new file mode 100644 index 000000000..5f54c1784 --- /dev/null +++ b/apps/website/lib/dtos/LeagueStatsDto.ts @@ -0,0 +1,13 @@ +/** + * League stats DTO + * Contains statistical information about a league's races + */ +export interface LeagueStatsDto { + leagueId: string; + totalRaces: number; + completedRaces: number; + scheduledRaces: number; + averageSOF?: number; + highestSOF?: number; + lowestSOF?: number; +} \ No newline at end of file diff --git a/apps/website/lib/dtos/LeagueSummaryDto.ts b/apps/website/lib/dtos/LeagueSummaryDto.ts new file mode 100644 index 000000000..6672e8134 --- /dev/null +++ b/apps/website/lib/dtos/LeagueSummaryDto.ts @@ -0,0 +1,18 @@ +/** + * League summary transport object + * Contains basic league information for list views + */ +export interface LeagueSummaryDto { + id: string; + name: string; + description?: string; + logoUrl?: string; + coverImage?: string; + memberCount: number; + maxMembers: number; + isPublic: boolean; + ownerId: string; + ownerName?: string; + scoringType?: string; + status?: string; +} \ No newline at end of file diff --git a/apps/website/lib/dtos/LoginParamsDto.ts b/apps/website/lib/dtos/LoginParamsDto.ts new file mode 100644 index 000000000..a0d99eda9 --- /dev/null +++ b/apps/website/lib/dtos/LoginParamsDto.ts @@ -0,0 +1,8 @@ +/** + * Login parameters data transfer object + * Parameters for user login + */ +export interface LoginParamsDto { + email: string; + password: string; +} \ No newline at end of file diff --git a/apps/website/lib/dtos/MemberPaymentDto.ts b/apps/website/lib/dtos/MemberPaymentDto.ts new file mode 100644 index 000000000..4fa850a18 --- /dev/null +++ b/apps/website/lib/dtos/MemberPaymentDto.ts @@ -0,0 +1,10 @@ +/** + * Member payment data transfer object + * Represents a payment made by a league member + */ +export interface MemberPaymentDto { + driverId: string; + amount: number; + paidAt: string; + status: string; +} \ No newline at end of file diff --git a/apps/website/lib/dtos/MembershipFeeDto.ts b/apps/website/lib/dtos/MembershipFeeDto.ts new file mode 100644 index 000000000..e6f3d31b2 --- /dev/null +++ b/apps/website/lib/dtos/MembershipFeeDto.ts @@ -0,0 +1,10 @@ +/** + * Membership fee data transfer object + * Represents a membership fee for a league + */ +export interface MembershipFeeDto { + leagueId: string; + amount: number; + currency: string; + period: string; +} \ No newline at end of file diff --git a/apps/website/lib/dtos/PaymentDto.ts b/apps/website/lib/dtos/PaymentDto.ts new file mode 100644 index 000000000..8fe845948 --- /dev/null +++ b/apps/website/lib/dtos/PaymentDto.ts @@ -0,0 +1,11 @@ +/** + * Payment data transfer object + * Represents a payment transaction + */ +export interface PaymentDto { + id: string; + amount: number; + currency: string; + status: string; + createdAt: string; +} \ No newline at end of file diff --git a/apps/website/lib/dtos/PenaltyDataDto.ts b/apps/website/lib/dtos/PenaltyDataDto.ts new file mode 100644 index 000000000..072565cee --- /dev/null +++ b/apps/website/lib/dtos/PenaltyDataDto.ts @@ -0,0 +1,11 @@ +import type { PenaltyTypeDto } from './PenaltyTypeDto'; + +/** + * Penalty data structure + * Used when creating or updating penalties + */ +export interface PenaltyDataDto { + driverId: string; + type: PenaltyTypeDto; + value?: number; +} \ No newline at end of file diff --git a/apps/website/lib/dtos/PenaltyTypeDto.ts b/apps/website/lib/dtos/PenaltyTypeDto.ts new file mode 100644 index 000000000..b2a48c973 --- /dev/null +++ b/apps/website/lib/dtos/PenaltyTypeDto.ts @@ -0,0 +1,11 @@ +/** + * Penalty type enumeration + * Defines all possible penalty types in the system + */ +export type PenaltyTypeDto = + | 'time_penalty' + | 'grid_penalty' + | 'points_deduction' + | 'disqualification' + | 'warning' + | 'license_points'; \ No newline at end of file diff --git a/apps/website/lib/dtos/PrizeDto.ts b/apps/website/lib/dtos/PrizeDto.ts new file mode 100644 index 000000000..83e4c4082 --- /dev/null +++ b/apps/website/lib/dtos/PrizeDto.ts @@ -0,0 +1,11 @@ +/** + * Prize data transfer object + * Represents a prize in a league + */ +export interface PrizeDto { + id: string; + name: string; + amount: number; + currency: string; + position?: number; +} \ No newline at end of file diff --git a/apps/website/lib/dtos/ProtestDto.ts b/apps/website/lib/dtos/ProtestDto.ts new file mode 100644 index 000000000..0f6457376 --- /dev/null +++ b/apps/website/lib/dtos/ProtestDto.ts @@ -0,0 +1,13 @@ +/** + * Protest data transfer object + * Represents a protest filed in a race + */ +export interface ProtestDto { + id: string; + raceId: string; + complainantId: string; + defendantId: string; + description: string; + status: string; + createdAt: string; +} \ No newline at end of file diff --git a/apps/website/lib/dtos/RaceDetailDto.ts b/apps/website/lib/dtos/RaceDetailDto.ts new file mode 100644 index 000000000..ca4d45dff --- /dev/null +++ b/apps/website/lib/dtos/RaceDetailDto.ts @@ -0,0 +1,18 @@ +import type { RaceDetailRaceDto } from './RaceDetailRaceDto'; +import type { RaceDetailLeagueDto } from './RaceDetailLeagueDto'; +import type { RaceDetailEntryDto } from './RaceDetailEntryDto'; +import type { RaceDetailRegistrationDto } from './RaceDetailRegistrationDto'; +import type { RaceDetailUserResultDto } from './RaceDetailUserResultDto'; + +/** + * Race detail data transfer object + * Complete race details view + */ +export interface RaceDetailDto { + race: RaceDetailRaceDto | null; + league: RaceDetailLeagueDto | null; + entryList: RaceDetailEntryDto[]; + registration: RaceDetailRegistrationDto; + userResult: RaceDetailUserResultDto | null; + error?: string; +} \ No newline at end of file diff --git a/apps/website/lib/dtos/RaceDetailEntryDto.ts b/apps/website/lib/dtos/RaceDetailEntryDto.ts new file mode 100644 index 000000000..608cb27cc --- /dev/null +++ b/apps/website/lib/dtos/RaceDetailEntryDto.ts @@ -0,0 +1,12 @@ +/** + * Race detail entry data transfer object + * Represents an entry in race details + */ +export interface RaceDetailEntryDto { + id: string; + name: string; + country: string; + avatarUrl: string; + rating: number | null; + isCurrentUser: boolean; +} \ No newline at end of file diff --git a/apps/website/lib/dtos/RaceDetailLeagueDto.ts b/apps/website/lib/dtos/RaceDetailLeagueDto.ts new file mode 100644 index 000000000..4c7c22dff --- /dev/null +++ b/apps/website/lib/dtos/RaceDetailLeagueDto.ts @@ -0,0 +1,13 @@ +/** + * Race detail league data transfer object + * League information in race details + */ +export interface RaceDetailLeagueDto { + id: string; + name: string; + description: string; + settings: { + maxDrivers?: number; + qualifyingFormat?: string; + }; +} \ No newline at end of file diff --git a/apps/website/lib/dtos/RaceDetailRaceDto.ts b/apps/website/lib/dtos/RaceDetailRaceDto.ts new file mode 100644 index 000000000..5d490845a --- /dev/null +++ b/apps/website/lib/dtos/RaceDetailRaceDto.ts @@ -0,0 +1,16 @@ +/** + * Race detail race data transfer object + * Race information in race details + */ +export interface RaceDetailRaceDto { + id: string; + leagueId: string; + track: string; + car: string; + scheduledAt: string; + sessionType: string; + status: string; + strengthOfField: number | null; + registeredCount?: number; + maxParticipants?: number; +} \ No newline at end of file diff --git a/apps/website/lib/dtos/RaceDetailRegistrationDto.ts b/apps/website/lib/dtos/RaceDetailRegistrationDto.ts new file mode 100644 index 000000000..7fbc8c0b7 --- /dev/null +++ b/apps/website/lib/dtos/RaceDetailRegistrationDto.ts @@ -0,0 +1,8 @@ +/** + * Race detail registration data transfer object + * Registration information in race details + */ +export interface RaceDetailRegistrationDto { + isUserRegistered: boolean; + canRegister: boolean; +} \ No newline at end of file diff --git a/apps/website/lib/dtos/RaceDetailUserResultDto.ts b/apps/website/lib/dtos/RaceDetailUserResultDto.ts new file mode 100644 index 000000000..cb2218d7b --- /dev/null +++ b/apps/website/lib/dtos/RaceDetailUserResultDto.ts @@ -0,0 +1,14 @@ +/** + * Race detail user result data transfer object + * Represents the current user's result in race details + */ +export interface RaceDetailUserResultDto { + position: number; + startPosition: number; + incidents: number; + fastestLap: number; + positionChange: number; + isPodium: boolean; + isClean: boolean; + ratingChange: number | null; +} \ No newline at end of file diff --git a/apps/website/lib/dtos/RaceListItemDto.ts b/apps/website/lib/dtos/RaceListItemDto.ts new file mode 100644 index 000000000..055d198c5 --- /dev/null +++ b/apps/website/lib/dtos/RaceListItemDto.ts @@ -0,0 +1,13 @@ +/** + * Race list item data transfer object + * Represents a race in list views + */ +export interface RaceListItemDto { + id: string; + name: string; + leagueId: string; + leagueName: string; + scheduledTime: string; + status: string; + trackName?: string; +} \ No newline at end of file diff --git a/apps/website/lib/dtos/RacePenaltiesDto.ts b/apps/website/lib/dtos/RacePenaltiesDto.ts new file mode 100644 index 000000000..f9071389e --- /dev/null +++ b/apps/website/lib/dtos/RacePenaltiesDto.ts @@ -0,0 +1,10 @@ +import type { RacePenaltyDto } from './RacePenaltyDto'; + +/** + * Race penalties data transfer object + * List of penalties for a race + */ +export interface RacePenaltiesDto { + penalties: RacePenaltyDto[]; + driverMap: Record; +} \ No newline at end of file diff --git a/apps/website/lib/dtos/RacePenaltyDto.ts b/apps/website/lib/dtos/RacePenaltyDto.ts new file mode 100644 index 000000000..dd96e735b --- /dev/null +++ b/apps/website/lib/dtos/RacePenaltyDto.ts @@ -0,0 +1,14 @@ +/** + * Race penalty data transfer object + * Represents a penalty issued in a race + */ +export interface RacePenaltyDto { + id: string; + driverId: string; + type: string; + value: number; + reason: string; + issuedBy: string; + issuedAt: string; + notes?: string; +} \ No newline at end of file diff --git a/apps/website/lib/dtos/RaceProtestDto.ts b/apps/website/lib/dtos/RaceProtestDto.ts new file mode 100644 index 000000000..248313fa2 --- /dev/null +++ b/apps/website/lib/dtos/RaceProtestDto.ts @@ -0,0 +1,15 @@ +/** + * Race protest data transfer object + * Represents a protest filed for a race + */ +export interface RaceProtestDto { + id: string; + protestingDriverId: string; + accusedDriverId: string; + incident: { + lap: number; + description: string; + }; + status: string; + filedAt: string; +} \ No newline at end of file diff --git a/apps/website/lib/dtos/RaceProtestsDto.ts b/apps/website/lib/dtos/RaceProtestsDto.ts new file mode 100644 index 000000000..c771898d1 --- /dev/null +++ b/apps/website/lib/dtos/RaceProtestsDto.ts @@ -0,0 +1,10 @@ +import type { RaceProtestDto } from './RaceProtestDto'; + +/** + * Race protests data transfer object + * List of protests for a race + */ +export interface RaceProtestsDto { + protests: RaceProtestDto[]; + driverMap: Record; +} \ No newline at end of file diff --git a/apps/website/lib/dtos/RaceResultDto.ts b/apps/website/lib/dtos/RaceResultDto.ts new file mode 100644 index 000000000..8ad550790 --- /dev/null +++ b/apps/website/lib/dtos/RaceResultDto.ts @@ -0,0 +1,18 @@ +/** + * Race result data transfer object + * Represents a driver's result in a race + */ +export interface RaceResultDto { + id: string; + raceId: string; + driverId: string; + driverName: string; + avatarUrl: string; + position: number; + startPosition: number; + incidents: number; + fastestLap: number; + positionChange: number; + isPodium: boolean; + isClean: boolean; +} \ No newline at end of file diff --git a/apps/website/lib/dtos/RaceResultRowDto.ts b/apps/website/lib/dtos/RaceResultRowDto.ts new file mode 100644 index 000000000..ce337bdd0 --- /dev/null +++ b/apps/website/lib/dtos/RaceResultRowDto.ts @@ -0,0 +1,13 @@ +/** + * Race result row data transfer object + * Represents a row in race results table + */ +export interface RaceResultRowDto { + id: string; + raceId: string; + driverId: string; + position: number; + fastestLap: number; + incidents: number; + startPosition: number; +} \ No newline at end of file diff --git a/apps/website/lib/dtos/RaceResultsDetailDto.ts b/apps/website/lib/dtos/RaceResultsDetailDto.ts new file mode 100644 index 000000000..d3a44731a --- /dev/null +++ b/apps/website/lib/dtos/RaceResultsDetailDto.ts @@ -0,0 +1,18 @@ +import type { RaceResultDto } from './RaceResultDto'; + +/** + * Race results detail data transfer object + * Detailed results for a race + */ +export interface RaceResultsDetailDto { + raceId: string; + track: string; + results: RaceResultDto[]; + league?: { id: string; name: string }; + race?: { id: string; track: string; scheduledAt: string }; + drivers: { id: string; name: string }[]; + pointsSystem: Record; + fastestLapTime: number; + penalties: { driverId: string; type: string; value?: number }[]; + currentDriverId: string; +} \ No newline at end of file diff --git a/apps/website/lib/dtos/RaceStatsDto.ts b/apps/website/lib/dtos/RaceStatsDto.ts new file mode 100644 index 000000000..5e3c7666a --- /dev/null +++ b/apps/website/lib/dtos/RaceStatsDto.ts @@ -0,0 +1,7 @@ +/** + * Race stats data transfer object + * Global race statistics + */ +export interface RaceStatsDto { + totalRaces: number; +} \ No newline at end of file diff --git a/apps/website/lib/dtos/RaceWithSOFDto.ts b/apps/website/lib/dtos/RaceWithSOFDto.ts new file mode 100644 index 000000000..aca4cf627 --- /dev/null +++ b/apps/website/lib/dtos/RaceWithSOFDto.ts @@ -0,0 +1,9 @@ +/** + * Race with strength of field data transfer object + * Race information including SOF + */ +export interface RaceWithSOFDto { + id: string; + track: string; + strengthOfField: number | null; +} \ No newline at end of file diff --git a/apps/website/lib/dtos/RacesPageDataDto.ts b/apps/website/lib/dtos/RacesPageDataDto.ts new file mode 100644 index 000000000..5323a1a7d --- /dev/null +++ b/apps/website/lib/dtos/RacesPageDataDto.ts @@ -0,0 +1,9 @@ +import type { RacesPageDataRaceDto } from './RacesPageDataRaceDto'; + +/** + * Races page data data transfer object + * Data for the races page + */ +export interface RacesPageDataDto { + races: RacesPageDataRaceDto[]; +} \ No newline at end of file diff --git a/apps/website/lib/dtos/RacesPageDataRaceDto.ts b/apps/website/lib/dtos/RacesPageDataRaceDto.ts new file mode 100644 index 000000000..0c3fc81df --- /dev/null +++ b/apps/website/lib/dtos/RacesPageDataRaceDto.ts @@ -0,0 +1,17 @@ +/** + * Races page data race data transfer object + * Race information for the races page + */ +export interface RacesPageDataRaceDto { + id: string; + track: string; + car: string; + scheduledAt: string; + status: string; + leagueId: string; + leagueName: string; + strengthOfField: number | null; + isUpcoming: boolean; + isLive: boolean; + isPast: boolean; +} \ No newline at end of file diff --git a/apps/website/lib/dtos/RecordEngagementInputDto.ts b/apps/website/lib/dtos/RecordEngagementInputDto.ts new file mode 100644 index 000000000..5b12ea52a --- /dev/null +++ b/apps/website/lib/dtos/RecordEngagementInputDto.ts @@ -0,0 +1,10 @@ +/** + * Record engagement input data transfer object + * Input for recording an engagement event + */ +export interface RecordEngagementInputDto { + eventType: string; + eventData?: Record; + userId?: string; + sessionId?: string; +} \ No newline at end of file diff --git a/apps/website/lib/dtos/RecordEngagementOutputDto.ts b/apps/website/lib/dtos/RecordEngagementOutputDto.ts new file mode 100644 index 000000000..803e85d17 --- /dev/null +++ b/apps/website/lib/dtos/RecordEngagementOutputDto.ts @@ -0,0 +1,7 @@ +/** + * Record engagement output data transfer object + * Output from recording an engagement event + */ +export interface RecordEngagementOutputDto { + success: boolean; +} \ No newline at end of file diff --git a/apps/website/lib/dtos/RecordPageViewInputDto.ts b/apps/website/lib/dtos/RecordPageViewInputDto.ts new file mode 100644 index 000000000..3ad2e5d48 --- /dev/null +++ b/apps/website/lib/dtos/RecordPageViewInputDto.ts @@ -0,0 +1,9 @@ +/** + * Record page view input data transfer object + * Input for recording a page view event + */ +export interface RecordPageViewInputDto { + path: string; + userId?: string; + sessionId?: string; +} \ No newline at end of file diff --git a/apps/website/lib/dtos/RecordPageViewOutputDto.ts b/apps/website/lib/dtos/RecordPageViewOutputDto.ts new file mode 100644 index 000000000..4dfbacece --- /dev/null +++ b/apps/website/lib/dtos/RecordPageViewOutputDto.ts @@ -0,0 +1,7 @@ +/** + * Record page view output data transfer object + * Output from recording a page view + */ +export interface RecordPageViewOutputDto { + success: boolean; +} \ No newline at end of file diff --git a/apps/website/lib/dtos/RegisterForRaceInputDto.ts b/apps/website/lib/dtos/RegisterForRaceInputDto.ts new file mode 100644 index 000000000..fdd4bcd13 --- /dev/null +++ b/apps/website/lib/dtos/RegisterForRaceInputDto.ts @@ -0,0 +1,8 @@ +/** + * Register for race input data transfer object + * Input for registering a driver for a race + */ +export interface RegisterForRaceInputDto { + leagueId: string; + driverId: string; +} \ No newline at end of file diff --git a/apps/website/lib/dtos/RequestAvatarGenerationInputDto.ts b/apps/website/lib/dtos/RequestAvatarGenerationInputDto.ts new file mode 100644 index 000000000..16fadc3b8 --- /dev/null +++ b/apps/website/lib/dtos/RequestAvatarGenerationInputDto.ts @@ -0,0 +1,8 @@ +/** + * Request avatar generation input data transfer object + * Input for requesting avatar generation + */ +export interface RequestAvatarGenerationInputDto { + driverId: string; + style?: string; +} \ No newline at end of file diff --git a/apps/website/lib/dtos/RequestAvatarGenerationOutputDto.ts b/apps/website/lib/dtos/RequestAvatarGenerationOutputDto.ts new file mode 100644 index 000000000..8aec3e3df --- /dev/null +++ b/apps/website/lib/dtos/RequestAvatarGenerationOutputDto.ts @@ -0,0 +1,9 @@ +/** + * Request avatar generation output data transfer object + * Output from avatar generation request + */ +export interface RequestAvatarGenerationOutputDto { + success: boolean; + avatarUrl?: string; + error?: string; +} \ No newline at end of file diff --git a/apps/website/lib/dtos/ScheduledRaceDto.ts b/apps/website/lib/dtos/ScheduledRaceDto.ts new file mode 100644 index 000000000..424b05207 --- /dev/null +++ b/apps/website/lib/dtos/ScheduledRaceDto.ts @@ -0,0 +1,11 @@ +/** + * Scheduled race data transfer object + * Represents a race scheduled in a league + */ +export interface ScheduledRaceDto { + id: string; + name: string; + scheduledTime: string; + status: string; + trackName?: string; +} \ No newline at end of file diff --git a/apps/website/lib/dtos/SessionDataDto.ts b/apps/website/lib/dtos/SessionDataDto.ts new file mode 100644 index 000000000..c293471be --- /dev/null +++ b/apps/website/lib/dtos/SessionDataDto.ts @@ -0,0 +1,11 @@ +/** + * Session data data transfer object + * User session information + */ +export interface SessionDataDto { + userId: string; + email: string; + displayName?: string; + driverId?: string; + isAuthenticated: boolean; +} \ No newline at end of file diff --git a/apps/website/lib/dtos/SignupParamsDto.ts b/apps/website/lib/dtos/SignupParamsDto.ts new file mode 100644 index 000000000..4e3263f99 --- /dev/null +++ b/apps/website/lib/dtos/SignupParamsDto.ts @@ -0,0 +1,9 @@ +/** + * Signup parameters data transfer object + * Parameters for user signup + */ +export interface SignupParamsDto { + email: string; + password: string; + displayName: string; +} \ No newline at end of file diff --git a/apps/website/lib/dtos/SponsorDashboardDto.ts b/apps/website/lib/dtos/SponsorDashboardDto.ts new file mode 100644 index 000000000..5249236c6 --- /dev/null +++ b/apps/website/lib/dtos/SponsorDashboardDto.ts @@ -0,0 +1,11 @@ +/** + * Sponsor dashboard data transfer object + * Dashboard information for a sponsor + */ +export interface SponsorDashboardDto { + sponsorId: string; + sponsorName: string; + totalSponsorships: number; + activeSponsorships: number; + totalInvestment: number; +} \ No newline at end of file diff --git a/apps/website/lib/dtos/SponsorDto.ts b/apps/website/lib/dtos/SponsorDto.ts new file mode 100644 index 000000000..1e9a07710 --- /dev/null +++ b/apps/website/lib/dtos/SponsorDto.ts @@ -0,0 +1,10 @@ +/** + * Sponsor data transfer object + * Represents a sponsor entity + */ +export interface SponsorDto { + id: string; + name: string; + logoUrl?: string; + websiteUrl?: string; +} \ No newline at end of file diff --git a/apps/website/lib/dtos/SponsorSponsorshipsDto.ts b/apps/website/lib/dtos/SponsorSponsorshipsDto.ts new file mode 100644 index 000000000..56c588fe2 --- /dev/null +++ b/apps/website/lib/dtos/SponsorSponsorshipsDto.ts @@ -0,0 +1,11 @@ +import type { SponsorshipDetailDto } from './SponsorshipDetailDto'; + +/** + * Sponsor sponsorships data transfer object + * Sponsorships associated with a sponsor + */ +export interface SponsorSponsorshipsDto { + sponsorId: string; + sponsorName: string; + sponsorships: SponsorshipDetailDto[]; +} \ No newline at end of file diff --git a/apps/website/lib/dtos/SponsorshipDetailDto.ts b/apps/website/lib/dtos/SponsorshipDetailDto.ts new file mode 100644 index 000000000..19f3cbd71 --- /dev/null +++ b/apps/website/lib/dtos/SponsorshipDetailDto.ts @@ -0,0 +1,14 @@ +/** + * Sponsorship detail data transfer object + * Details of a sponsorship + */ +export interface SponsorshipDetailDto { + id: string; + leagueId: string; + leagueName: string; + seasonId: string; + tier: 'main' | 'secondary'; + status: string; + amount: number; + currency: string; +} \ No newline at end of file diff --git a/apps/website/lib/dtos/StandingEntryDto.ts b/apps/website/lib/dtos/StandingEntryDto.ts new file mode 100644 index 000000000..805f28b06 --- /dev/null +++ b/apps/website/lib/dtos/StandingEntryDto.ts @@ -0,0 +1,15 @@ +import type { DriverDto } from './DriverDto'; + +/** + * Standing entry data transfer object + * Represents a driver's standing in league standings + */ +export interface StandingEntryDto { + driverId: string; + driver?: DriverDto; + position: number; + points: number; + wins: number; + podiums: number; + races: number; +} \ No newline at end of file diff --git a/apps/website/lib/dtos/TeamDetailsDto.ts b/apps/website/lib/dtos/TeamDetailsDto.ts new file mode 100644 index 000000000..8ffc6965d --- /dev/null +++ b/apps/website/lib/dtos/TeamDetailsDto.ts @@ -0,0 +1,15 @@ +import type { TeamMemberDto } from './TeamMemberDto'; + +/** + * Team details data transfer object + * Detailed information about a team + */ +export interface TeamDetailsDto { + id: string; + name: string; + description?: string; + logoUrl?: string; + memberCount: number; + ownerId: string; + members: TeamMemberDto[]; +} \ No newline at end of file diff --git a/apps/website/lib/dtos/TeamJoinRequestItemDto.ts b/apps/website/lib/dtos/TeamJoinRequestItemDto.ts new file mode 100644 index 000000000..d4a5e7b50 --- /dev/null +++ b/apps/website/lib/dtos/TeamJoinRequestItemDto.ts @@ -0,0 +1,11 @@ +/** + * Team join request item data transfer object + * Represents a request to join a team + */ +export interface TeamJoinRequestItemDto { + id: string; + teamId: string; + driverId: string; + requestedAt: string; + message?: string; +} \ No newline at end of file diff --git a/apps/website/lib/dtos/TeamJoinRequestsDto.ts b/apps/website/lib/dtos/TeamJoinRequestsDto.ts new file mode 100644 index 000000000..14394f531 --- /dev/null +++ b/apps/website/lib/dtos/TeamJoinRequestsDto.ts @@ -0,0 +1,9 @@ +import type { TeamJoinRequestItemDto } from './TeamJoinRequestItemDto'; + +/** + * Team join requests data transfer object + * List of join requests for a team + */ +export interface TeamJoinRequestsDto { + requests: TeamJoinRequestItemDto[]; +} \ No newline at end of file diff --git a/apps/website/lib/dtos/TeamMemberDto.ts b/apps/website/lib/dtos/TeamMemberDto.ts new file mode 100644 index 000000000..086bb482f --- /dev/null +++ b/apps/website/lib/dtos/TeamMemberDto.ts @@ -0,0 +1,12 @@ +import type { DriverDto } from './DriverDto'; + +/** + * Team member data transfer object + * Represents a driver's membership in a team + */ +export interface TeamMemberDto { + driverId: string; + driver?: DriverDto; + role: string; + joinedAt: string; +} \ No newline at end of file diff --git a/apps/website/lib/dtos/TeamMembersDto.ts b/apps/website/lib/dtos/TeamMembersDto.ts new file mode 100644 index 000000000..8d81d69ff --- /dev/null +++ b/apps/website/lib/dtos/TeamMembersDto.ts @@ -0,0 +1,9 @@ +import type { TeamMemberDto } from './TeamMemberDto'; + +/** + * Team members data transfer object + * List of team members + */ +export interface TeamMembersDto { + members: TeamMemberDto[]; +} \ No newline at end of file diff --git a/apps/website/lib/dtos/TeamSummaryDto.ts b/apps/website/lib/dtos/TeamSummaryDto.ts new file mode 100644 index 000000000..3c821d65e --- /dev/null +++ b/apps/website/lib/dtos/TeamSummaryDto.ts @@ -0,0 +1,11 @@ +/** + * Team summary data transfer object + * Basic information about a team + */ +export interface TeamSummaryDto { + id: string; + name: string; + logoUrl?: string; + memberCount: number; + rating: number; +} \ No newline at end of file diff --git a/apps/website/lib/dtos/UpdateTeamInputDto.ts b/apps/website/lib/dtos/UpdateTeamInputDto.ts new file mode 100644 index 000000000..7950b590f --- /dev/null +++ b/apps/website/lib/dtos/UpdateTeamInputDto.ts @@ -0,0 +1,9 @@ +/** + * Update team input data transfer object + * Input for updating team information + */ +export interface UpdateTeamInputDto { + name?: string; + description?: string; + logoUrl?: string; +} \ No newline at end of file diff --git a/apps/website/lib/dtos/UpdateTeamOutputDto.ts b/apps/website/lib/dtos/UpdateTeamOutputDto.ts new file mode 100644 index 000000000..bd8938372 --- /dev/null +++ b/apps/website/lib/dtos/UpdateTeamOutputDto.ts @@ -0,0 +1,7 @@ +/** + * Update team output data transfer object + * Output from updating team information + */ +export interface UpdateTeamOutputDto { + success: boolean; +} \ No newline at end of file diff --git a/apps/website/lib/dtos/WalletDto.ts b/apps/website/lib/dtos/WalletDto.ts new file mode 100644 index 000000000..3e6114cfb --- /dev/null +++ b/apps/website/lib/dtos/WalletDto.ts @@ -0,0 +1,12 @@ +import type { WalletTransactionDto } from './WalletTransactionDto'; + +/** + * Wallet data transfer object + * Represents a driver's wallet + */ +export interface WalletDto { + driverId: string; + balance: number; + currency: string; + transactions: WalletTransactionDto[]; +} \ No newline at end of file diff --git a/apps/website/lib/dtos/WalletTransactionDto.ts b/apps/website/lib/dtos/WalletTransactionDto.ts new file mode 100644 index 000000000..718dc9e9e --- /dev/null +++ b/apps/website/lib/dtos/WalletTransactionDto.ts @@ -0,0 +1,11 @@ +/** + * Wallet transaction data transfer object + * Represents a transaction in a driver's wallet + */ +export interface WalletTransactionDto { + id: string; + type: 'deposit' | 'withdrawal'; + amount: number; + description?: string; + createdAt: string; +} \ No newline at end of file diff --git a/apps/website/lib/dtos/WithdrawFromRaceInputDto.ts b/apps/website/lib/dtos/WithdrawFromRaceInputDto.ts new file mode 100644 index 000000000..d64438bfe --- /dev/null +++ b/apps/website/lib/dtos/WithdrawFromRaceInputDto.ts @@ -0,0 +1,7 @@ +/** + * Withdraw from race input data transfer object + * Input for withdrawing a driver from a race + */ +export interface WithdrawFromRaceInputDto { + driverId: string; +} \ No newline at end of file diff --git a/apps/website/lib/dtos/index.ts b/apps/website/lib/dtos/index.ts new file mode 100644 index 000000000..e181d0922 --- /dev/null +++ b/apps/website/lib/dtos/index.ts @@ -0,0 +1,113 @@ +// Common DTOs +export type { DriverDto } from './DriverDto'; +export type { PenaltyDataDto } from './PenaltyDataDto'; +export type { PenaltyTypeDto } from './PenaltyTypeDto'; +export type { ProtestDto } from './ProtestDto'; +export type { StandingEntryDto } from './StandingEntryDto'; + +// Analytics DTOs +export type { RecordEngagementInputDto } from './RecordEngagementInputDto'; +export type { RecordEngagementOutputDto } from './RecordEngagementOutputDto'; +export type { RecordPageViewInputDto } from './RecordPageViewInputDto'; +export type { RecordPageViewOutputDto } from './RecordPageViewOutputDto'; + +// Auth DTOs +export type { LoginParamsDto } from './LoginParamsDto'; +export type { SessionDataDto } from './SessionDataDto'; +export type { SignupParamsDto } from './SignupParamsDto'; + +// League DTOs +export type { AllLeaguesWithCapacityDto } from './AllLeaguesWithCapacityDto'; +export type { CreateLeagueInputDto } from './CreateLeagueInputDto'; +export type { CreateLeagueOutputDto } from './CreateLeagueOutputDto'; +export type { LeagueAdminDto } from './LeagueAdminDto'; +export type { LeagueAdminPermissionsDto } from './LeagueAdminPermissionsDto'; +export type { LeagueAdminProtestsDto } from './LeagueAdminProtestsDto'; +export type { LeagueConfigFormModelDto } from './LeagueConfigFormModelDto'; +export type { LeagueJoinRequestDto } from './LeagueJoinRequestDto'; +export type { LeagueMembershipsDto } from './LeagueMembershipsDto'; +export type { LeagueMemberDto } from './LeagueMemberDto'; +export type { LeagueOwnerSummaryDto } from './LeagueOwnerSummaryDto'; +export type { LeagueScheduleDto } from './LeagueScheduleDto'; +export type { LeagueSeasonSummaryDto } from './LeagueSeasonSummaryDto'; +export type { LeagueStandingsDto } from './LeagueStandingsDto'; +export type { LeagueStatsDto } from './LeagueStatsDto'; +export type { LeagueSummaryDto } from './LeagueSummaryDto'; + +// Race DTOs +export type { AllRacesPageDto } from './AllRacesPageDto'; +export type { ImportRaceResultsInputDto } from './ImportRaceResultsInputDto'; +export type { ImportRaceResultsSummaryDto } from './ImportRaceResultsSummaryDto'; +export type { RaceDetailDto } from './RaceDetailDto'; +export type { RaceDetailEntryDto } from './RaceDetailEntryDto'; +export type { RaceDetailLeagueDto } from './RaceDetailLeagueDto'; +export type { RaceDetailRaceDto } from './RaceDetailRaceDto'; +export type { RaceDetailRegistrationDto } from './RaceDetailRegistrationDto'; +export type { RaceDetailUserResultDto } from './RaceDetailUserResultDto'; +export type { RaceListItemDto } from './RaceListItemDto'; +export type { RacePenaltiesDto } from './RacePenaltiesDto'; +export type { RacePenaltyDto } from './RacePenaltyDto'; +export type { RaceProtestsDto } from './RaceProtestsDto'; +export type { RaceProtestDto } from './RaceProtestDto'; +export type { RaceResultDto } from './RaceResultDto'; +export type { RaceResultRowDto } from './RaceResultRowDto'; +export type { RaceResultsDetailDto } from './RaceResultsDetailDto'; +export type { RaceStatsDto } from './RaceStatsDto'; +export type { RaceWithSOFDto } from './RaceWithSOFDto'; +export type { RacesPageDataDto } from './RacesPageDataDto'; +export type { RacesPageDataRaceDto } from './RacesPageDataRaceDto'; +export type { RegisterForRaceInputDto } from './RegisterForRaceInputDto'; +export type { ScheduledRaceDto } from './ScheduledRaceDto'; +export type { WithdrawFromRaceInputDto } from './WithdrawFromRaceInputDto'; + +// Driver DTOs +export type { CompleteOnboardingInputDto } from './CompleteOnboardingInputDto'; +export type { CompleteOnboardingOutputDto } from './CompleteOnboardingOutputDto'; +export type { DriverLeaderboardItemDto } from './DriverLeaderboardItemDto'; +export type { DriverRegistrationStatusDto } from './DriverRegistrationStatusDto'; +export type { DriverRowDto } from './DriverRowDto'; +export type { DriverStatsDto } from './DriverStatsDto'; +export type { DriverTeamDto } from './DriverTeamDto'; +export type { DriversLeaderboardDto } from './DriversLeaderboardDto'; + +// Team DTOs +export type { AllTeamsDto } from './AllTeamsDto'; +export type { CreateTeamInputDto } from './CreateTeamInputDto'; +export type { CreateTeamOutputDto } from './CreateTeamOutputDto'; +export type { TeamDetailsDto } from './TeamDetailsDto'; +export type { TeamJoinRequestItemDto } from './TeamJoinRequestItemDto'; +export type { TeamJoinRequestsDto } from './TeamJoinRequestsDto'; +export type { TeamMemberDto } from './TeamMemberDto'; +export type { TeamMembersDto } from './TeamMembersDto'; +export type { TeamSummaryDto } from './TeamSummaryDto'; +export type { UpdateTeamInputDto } from './UpdateTeamInputDto'; +export type { UpdateTeamOutputDto } from './UpdateTeamOutputDto'; + +// Sponsor DTOs +export type { CreateSponsorInputDto } from './CreateSponsorInputDto'; +export type { CreateSponsorOutputDto } from './CreateSponsorOutputDto'; +export type { GetEntitySponsorshipPricingResultDto } from './GetEntitySponsorshipPricingResultDto'; +export type { GetSponsorsOutputDto } from './GetSponsorsOutputDto'; +export type { SponsorDashboardDto } from './SponsorDashboardDto'; +export type { SponsorDto } from './SponsorDto'; +export type { SponsorshipDetailDto } from './SponsorshipDetailDto'; +export type { SponsorSponsorshipsDto } from './SponsorSponsorshipsDto'; + +// Media DTOs +export type { RequestAvatarGenerationInputDto } from './RequestAvatarGenerationInputDto'; +export type { RequestAvatarGenerationOutputDto } from './RequestAvatarGenerationOutputDto'; + +// Payments DTOs +export type { CreatePaymentInputDto } from './CreatePaymentInputDto'; +export type { CreatePaymentOutputDto } from './CreatePaymentOutputDto'; +export type { GetMembershipFeesOutputDto } from './GetMembershipFeesOutputDto'; +export type { GetPaymentsOutputDto } from './GetPaymentsOutputDto'; +export type { GetPrizesOutputDto } from './GetPrizesOutputDto'; +export type { GetWalletOutputDto } from './GetWalletOutputDto'; +export type { ImportResultRowDto } from './ImportResultRowDto'; +export type { MemberPaymentDto } from './MemberPaymentDto'; +export type { MembershipFeeDto } from './MembershipFeeDto'; +export type { PaymentDto } from './PaymentDto'; +export type { PrizeDto } from './PrizeDto'; +export type { WalletDto } from './WalletDto'; +export type { WalletTransactionDto } from './WalletTransactionDto'; \ No newline at end of file diff --git a/apps/website/lib/presenters/DriverRegistrationStatusPresenter.ts b/apps/website/lib/presenters/DriverRegistrationStatusPresenter.ts index f5f3cbd55..3a6ce7f7e 100644 --- a/apps/website/lib/presenters/DriverRegistrationStatusPresenter.ts +++ b/apps/website/lib/presenters/DriverRegistrationStatusPresenter.ts @@ -1,29 +1,6 @@ -import type { - IDriverRegistrationStatusPresenter, - DriverRegistrationStatusViewModel, -} from '@core/racing/application/presenters/IDriverRegistrationStatusPresenter'; +import { DriverRegistrationStatusDto } from '../dtos'; +import { DriverRegistrationStatusViewModel } from '../view-models'; -export class DriverRegistrationStatusPresenter implements IDriverRegistrationStatusPresenter { - private viewModel: DriverRegistrationStatusViewModel | null = null; - - present( - isRegistered: boolean, - raceId: string, - driverId: string - ): DriverRegistrationStatusViewModel { - this.viewModel = { - isRegistered, - raceId, - driverId, - }; - - return this.viewModel; - } - - getViewModel(): DriverRegistrationStatusViewModel { - if (!this.viewModel) { - throw new Error('Presenter has not been called yet'); - } - return this.viewModel; - } -} \ No newline at end of file +export const presentDriverRegistrationStatus = (dto: DriverRegistrationStatusDto): DriverRegistrationStatusViewModel => { + return new DriverRegistrationStatusViewModel(dto); +}; \ No newline at end of file diff --git a/apps/website/lib/presenters/DriversLeaderboardPresenter.ts b/apps/website/lib/presenters/DriversLeaderboardPresenter.ts index 975f1bed4..139ef3abd 100644 --- a/apps/website/lib/presenters/DriversLeaderboardPresenter.ts +++ b/apps/website/lib/presenters/DriversLeaderboardPresenter.ts @@ -1,109 +1,6 @@ -/** - * DriversLeaderboardPresenter - Pure data transformer - * Transforms API response to view model without DI dependencies. - */ +import { DriversLeaderboardDto, DriverLeaderboardItemDto } from '../dtos'; +import { DriverLeaderboardViewModel } from '../view-models'; -import { apiClient, type DriversLeaderboardViewModel as ApiDriversLeaderboardViewModel } from '@/lib/apiClient'; - -export type SkillLevel = 'rookie' | 'amateur' | 'pro' | 'elite' | 'legend'; - -export interface DriverLeaderboardItemViewModel { - id: string; - name: string; - rating: number; - skillLevel: SkillLevel; - nationality?: string | undefined; - racesCompleted: number; - wins: number; - podiums: number; - isActive: boolean; - rank: number; - avatarUrl?: string | undefined; -} - -export interface DriversLeaderboardViewModel { - drivers: DriverLeaderboardItemViewModel[]; - totalRaces: number; - totalWins: number; - activeCount: number; -} - -export interface IDriversLeaderboardPresenter { - reset(): void; - getViewModel(): DriversLeaderboardViewModel | null; -} - -/** - * Calculate skill level from rating - */ -function getSkillLevel(rating: number): SkillLevel { - if (rating >= 5000) return 'legend'; - if (rating >= 3500) return 'elite'; - if (rating >= 2000) return 'pro'; - if (rating >= 1000) return 'amateur'; - return 'rookie'; -} - -/** - * Transform API response to view model - */ -function transformApiResponse(apiResponse: ApiDriversLeaderboardViewModel): DriversLeaderboardViewModel { - const items: DriverLeaderboardItemViewModel[] = apiResponse.drivers.map((driver, index) => { - const rating = driver.rating ?? 0; - const skillLevel = getSkillLevel(rating); - - const viewModel: DriverLeaderboardItemViewModel = { - id: driver.id, - name: driver.name, - rating, - skillLevel, - racesCompleted: driver.races ?? 0, - wins: driver.wins ?? 0, - podiums: 0, // API may not provide this, default to 0 - isActive: true, - rank: index + 1, - }; - - if (driver.avatarUrl) { - viewModel.avatarUrl = driver.avatarUrl; - } - - return viewModel; - }); - - const totalRaces = items.reduce((sum, d) => sum + d.racesCompleted, 0); - const totalWins = items.reduce((sum, d) => sum + d.wins, 0); - const activeCount = items.filter((d) => d.isActive).length; - - return { - drivers: items, - totalRaces, - totalWins, - activeCount, - }; -} - -export class DriversLeaderboardPresenter implements IDriversLeaderboardPresenter { - private viewModel: DriversLeaderboardViewModel | null = null; - - reset(): void { - this.viewModel = null; - } - - async fetchAndPresent(): Promise { - const apiResponse = await apiClient.drivers.getLeaderboard(); - this.viewModel = transformApiResponse(apiResponse); - } - - getViewModel(): DriversLeaderboardViewModel | null { - return this.viewModel; - } -} - -/** - * Convenience function to fetch and transform drivers leaderboard - */ -export async function fetchDriversLeaderboard(): Promise { - const apiResponse = await apiClient.drivers.getLeaderboard(); - return transformApiResponse(apiResponse); -} \ No newline at end of file +export const presentDriversLeaderboard = (dto: DriversLeaderboardDto, previousDrivers?: DriverLeaderboardItemDto[]): DriverLeaderboardViewModel => { + return new DriverLeaderboardViewModel(dto, previousDrivers); +}; \ No newline at end of file diff --git a/apps/website/lib/presenters/LeagueMemberPresenter.ts b/apps/website/lib/presenters/LeagueMemberPresenter.ts new file mode 100644 index 000000000..e7beb0500 --- /dev/null +++ b/apps/website/lib/presenters/LeagueMemberPresenter.ts @@ -0,0 +1,6 @@ +import { LeagueMemberDto } from '../dtos'; +import { LeagueMemberViewModel } from '../view-models'; + +export const presentLeagueMember = (dto: LeagueMemberDto, currentUserId: string): LeagueMemberViewModel => { + return new LeagueMemberViewModel(dto, currentUserId); +}; \ No newline at end of file diff --git a/apps/website/lib/presenters/LeagueStandingsPresenter.ts b/apps/website/lib/presenters/LeagueStandingsPresenter.ts index 0f176b715..29e4f4fa8 100644 --- a/apps/website/lib/presenters/LeagueStandingsPresenter.ts +++ b/apps/website/lib/presenters/LeagueStandingsPresenter.ts @@ -1,81 +1,6 @@ -/** - * LeagueStandingsPresenter - Pure data transformer - * Transforms API response to view model without DI dependencies. - */ +import { LeagueStandingsDto, StandingEntryDto } from '../dtos'; +import { LeagueStandingsViewModel } from '../view-models'; -import { apiClient, type LeagueStandingsViewModel as ApiLeagueStandingsViewModel } from '@/lib/apiClient'; - -export interface LeagueStandingsEntryViewModel { - driverId: string; - driverName: string; - position: number; - points: number; - wins: number; - podiums: number; - races: number; - avatarUrl?: string | undefined; -} - -export interface LeagueStandingsViewModel { - leagueId: string; - standings: LeagueStandingsEntryViewModel[]; - totalDrivers: number; -} - -export interface ILeagueStandingsPresenter { - present(leagueId: string): Promise; - getViewModel(): LeagueStandingsViewModel | null; - reset(): void; -} - -/** - * Transform API response to view model - */ -function transformApiResponse(leagueId: string, apiResponse: ApiLeagueStandingsViewModel): LeagueStandingsViewModel { - const standings: LeagueStandingsEntryViewModel[] = apiResponse.standings.map((entry) => { - const viewModel: LeagueStandingsEntryViewModel = { - driverId: entry.driverId, - driverName: entry.driver?.name ?? 'Unknown Driver', - position: entry.position, - points: entry.points, - wins: entry.wins, - podiums: entry.podiums, - races: entry.races, - }; - if (entry.driver?.avatarUrl) { - viewModel.avatarUrl = entry.driver.avatarUrl; - } - return viewModel; - }); - - return { - leagueId, - standings, - totalDrivers: standings.length, - }; -} - -export class LeagueStandingsPresenter implements ILeagueStandingsPresenter { - private viewModel: LeagueStandingsViewModel | null = null; - - reset(): void { - this.viewModel = null; - } - - async present(leagueId: string): Promise { - const apiResponse = await apiClient.leagues.getStandings(leagueId); - this.viewModel = transformApiResponse(leagueId, apiResponse); - } - - getViewModel(): LeagueStandingsViewModel | null { - return this.viewModel; - } -} - -/** - * Convenience function to fetch and transform standings - */ -export async function fetchLeagueStandings(leagueId: string): Promise { - const apiResponse = await apiClient.leagues.getStandings(leagueId); - return transformApiResponse(leagueId, apiResponse); -} \ No newline at end of file +export const presentLeagueStandings = (dto: LeagueStandingsDto, currentUserId: string, previousStandings?: StandingEntryDto[]): LeagueStandingsViewModel => { + return new LeagueStandingsViewModel(dto, currentUserId, previousStandings); +}; \ No newline at end of file diff --git a/apps/website/lib/presenters/LeagueSummaryPresenter.ts b/apps/website/lib/presenters/LeagueSummaryPresenter.ts new file mode 100644 index 000000000..3a563773e --- /dev/null +++ b/apps/website/lib/presenters/LeagueSummaryPresenter.ts @@ -0,0 +1,10 @@ +import { LeagueSummaryDto } from '../dtos'; +import { LeagueSummaryViewModel } from '../view-models'; + +export const presentLeagueSummary = (dto: LeagueSummaryDto): LeagueSummaryViewModel => { + return new LeagueSummaryViewModel(dto); +}; + +export const presentLeagueSummaries = (dtos: LeagueSummaryDto[]): LeagueSummaryViewModel[] => { + return dtos.map(presentLeagueSummary); +}; \ No newline at end of file diff --git a/apps/website/lib/presenters/MembershipFeePresenter.ts b/apps/website/lib/presenters/MembershipFeePresenter.ts new file mode 100644 index 000000000..297983b7e --- /dev/null +++ b/apps/website/lib/presenters/MembershipFeePresenter.ts @@ -0,0 +1,6 @@ +import { MembershipFeeDto } from '../dtos'; +import { MembershipFeeViewModel } from '../view-models'; + +export const presentMembershipFee = (dto: MembershipFeeDto): MembershipFeeViewModel => { + return new MembershipFeeViewModel(dto); +}; \ No newline at end of file diff --git a/apps/website/lib/presenters/PaymentPresenter.ts b/apps/website/lib/presenters/PaymentPresenter.ts new file mode 100644 index 000000000..164d3fe46 --- /dev/null +++ b/apps/website/lib/presenters/PaymentPresenter.ts @@ -0,0 +1,6 @@ +import { PaymentDto } from '../dtos'; +import { PaymentViewModel } from '../view-models'; + +export const presentPayment = (dto: PaymentDto): PaymentViewModel => { + return new PaymentViewModel(dto); +}; \ No newline at end of file diff --git a/apps/website/lib/presenters/PrizePresenter.ts b/apps/website/lib/presenters/PrizePresenter.ts new file mode 100644 index 000000000..f39923b7d --- /dev/null +++ b/apps/website/lib/presenters/PrizePresenter.ts @@ -0,0 +1,6 @@ +import { PrizeDto } from '../dtos'; +import { PrizeViewModel } from '../view-models'; + +export const presentPrize = (dto: PrizeDto): PrizeViewModel => { + return new PrizeViewModel(dto); +}; \ No newline at end of file diff --git a/apps/website/lib/presenters/RaceDetailPresenter.ts b/apps/website/lib/presenters/RaceDetailPresenter.ts index 1fa8959d9..93e6178ad 100644 --- a/apps/website/lib/presenters/RaceDetailPresenter.ts +++ b/apps/website/lib/presenters/RaceDetailPresenter.ts @@ -1,21 +1,6 @@ -import type { - IRaceDetailPresenter, - RaceDetailViewModel, -} from '@core/racing/application/presenters/IRaceDetailPresenter'; +import { RaceDetailDto } from '../dtos'; +import { RaceDetailViewModel } from '../view-models'; -export class RaceDetailPresenter implements IRaceDetailPresenter { - private viewModel: RaceDetailViewModel | null = null; - - reset(): void { - this.viewModel = null; - } - - present(viewModel: RaceDetailViewModel): RaceDetailViewModel { - this.viewModel = viewModel; - return viewModel; - } - - getViewModel(): RaceDetailViewModel | null { - return this.viewModel; - } -} \ No newline at end of file +export const presentRaceDetail = (dto: RaceDetailDto): RaceDetailViewModel => { + return new RaceDetailViewModel(dto); +}; \ No newline at end of file diff --git a/apps/website/lib/presenters/RaceListItemPresenter.ts b/apps/website/lib/presenters/RaceListItemPresenter.ts new file mode 100644 index 000000000..acfa86a98 --- /dev/null +++ b/apps/website/lib/presenters/RaceListItemPresenter.ts @@ -0,0 +1,6 @@ +import { RaceListItemDto } from '../dtos'; +import { RaceListItemViewModel } from '../view-models'; + +export const presentRaceListItem = (dto: RaceListItemDto): RaceListItemViewModel => { + return new RaceListItemViewModel(dto); +}; \ No newline at end of file diff --git a/apps/website/lib/presenters/RaceResultsDetailPresenter.ts b/apps/website/lib/presenters/RaceResultsDetailPresenter.ts index 14f4bf507..1012b8f36 100644 --- a/apps/website/lib/presenters/RaceResultsDetailPresenter.ts +++ b/apps/website/lib/presenters/RaceResultsDetailPresenter.ts @@ -1,20 +1,6 @@ -import type { - IRaceResultsDetailPresenter, - RaceResultsDetailViewModel, -} from '@core/racing/application/presenters/IRaceResultsDetailPresenter'; +import { RaceResultsDetailDto } from '../dtos'; +import { RaceResultsDetailViewModel } from '../view-models'; -export class RaceResultsDetailPresenter implements IRaceResultsDetailPresenter { - private viewModel: RaceResultsDetailViewModel | null = null; - - reset(): void { - this.viewModel = null; - } - - present(viewModel: RaceResultsDetailViewModel): void { - this.viewModel = viewModel; - } - - getViewModel(): RaceResultsDetailViewModel | null { - return this.viewModel; - } -} \ No newline at end of file +export const presentRaceResultsDetail = (dto: RaceResultsDetailDto, currentUserId: string): RaceResultsDetailViewModel => { + return new RaceResultsDetailViewModel(dto, currentUserId); +}; \ No newline at end of file diff --git a/apps/website/lib/presenters/RaceResultsPresenter.test.ts b/apps/website/lib/presenters/RaceResultsPresenter.test.ts new file mode 100644 index 000000000..567eb1da1 --- /dev/null +++ b/apps/website/lib/presenters/RaceResultsPresenter.test.ts @@ -0,0 +1,52 @@ +import { describe, it, expect } from 'vitest'; +import { presentRaceResult } from './RaceResultsPresenter'; +import { RaceResultViewModel } from '../view-models'; +import type { RaceResultRowDto } from '../dtos'; + +describe('RaceResultsPresenter', () => { + describe('presentRaceResult', () => { + it('should transform RaceResultRowDto into RaceResultViewModel', () => { + const dto: RaceResultRowDto = { + id: '1', + raceId: 'race-1', + driverId: 'driver-1', + position: 1, + fastestLap: 85.5, + incidents: 0, + startPosition: 2, + }; + + const result = presentRaceResult(dto); + + expect(result).toBeInstanceOf(RaceResultViewModel); + expect(result.id).toBe('1'); + expect(result.raceId).toBe('race-1'); + expect(result.driverId).toBe('driver-1'); + expect(result.position).toBe(1); + expect(result.fastestLap).toBe(85.5); + expect(result.incidents).toBe(0); + expect(result.startPosition).toBe(2); + }); + + it('should handle zero values correctly', () => { + const dto: RaceResultRowDto = { + id: '2', + raceId: 'race-2', + driverId: 'driver-2', + position: 5, + fastestLap: 0, + incidents: 3, + startPosition: 5, + }; + + const result = presentRaceResult(dto); + + expect(result).toBeInstanceOf(RaceResultViewModel); + expect(result.id).toBe('2'); + expect(result.position).toBe(5); + expect(result.fastestLap).toBe(0); + expect(result.incidents).toBe(3); + expect(result.startPosition).toBe(5); + }); + }); +}); \ No newline at end of file diff --git a/apps/website/lib/presenters/RaceResultsPresenter.ts b/apps/website/lib/presenters/RaceResultsPresenter.ts new file mode 100644 index 000000000..a60ec0490 --- /dev/null +++ b/apps/website/lib/presenters/RaceResultsPresenter.ts @@ -0,0 +1,6 @@ +import { RaceResultRowDto } from '../dtos'; +import { RaceResultViewModel } from '../view-models'; + +export const presentRaceResult = (dto: RaceResultRowDto): RaceResultViewModel => { + return new RaceResultViewModel(dto); +}; \ No newline at end of file diff --git a/apps/website/lib/presenters/SponsorPresenter.ts b/apps/website/lib/presenters/SponsorPresenter.ts new file mode 100644 index 000000000..3a6963bef --- /dev/null +++ b/apps/website/lib/presenters/SponsorPresenter.ts @@ -0,0 +1,6 @@ +import { SponsorDto } from '../dtos'; +import { SponsorViewModel } from '../view-models'; + +export const presentSponsor = (dto: SponsorDto): SponsorViewModel => { + return new SponsorViewModel(dto); +}; \ No newline at end of file diff --git a/apps/website/lib/presenters/SponsorshipDetailPresenter.ts b/apps/website/lib/presenters/SponsorshipDetailPresenter.ts new file mode 100644 index 000000000..bb548db11 --- /dev/null +++ b/apps/website/lib/presenters/SponsorshipDetailPresenter.ts @@ -0,0 +1,6 @@ +import { SponsorshipDetailDto } from '../dtos'; +import { SponsorshipDetailViewModel } from '../view-models'; + +export const presentSponsorshipDetail = (dto: SponsorshipDetailDto): SponsorshipDetailViewModel => { + return new SponsorshipDetailViewModel(dto); +}; \ No newline at end of file diff --git a/apps/website/lib/presenters/TeamDetailsPresenter.ts b/apps/website/lib/presenters/TeamDetailsPresenter.ts index 5d961b5ba..c05463141 100644 --- a/apps/website/lib/presenters/TeamDetailsPresenter.ts +++ b/apps/website/lib/presenters/TeamDetailsPresenter.ts @@ -1,82 +1,6 @@ -/** - * TeamDetailsPresenter - Pure data transformer - * Transforms API response to view model without DI dependencies. - */ +import { TeamDetailsDto, TeamMemberDto } from '../dtos'; +import { TeamDetailsViewModel } from '../view-models'; -import { apiClient, type TeamDetailsViewModel as ApiTeamDetailsViewModel } from '@/lib/apiClient'; - -export interface TeamMembershipViewModel { - role: string; - joinedAt: string; - isActive: boolean; -} - -export interface TeamInfoViewModel { - id: string; - name: string; - tag?: string | undefined; - description?: string | undefined; - ownerId: string; - leagues?: string[] | undefined; - createdAt: string; -} - -export interface TeamDetailsViewModel { - team: TeamInfoViewModel; - membership: TeamMembershipViewModel | null; - canManage: boolean; -} - -export interface ITeamDetailsPresenter { - reset(): void; - getViewModel(): TeamDetailsViewModel | null; -} - -/** - * Transform API response to view model - */ -function transformApiResponse(apiResponse: ApiTeamDetailsViewModel): TeamDetailsViewModel { - return { - team: { - id: apiResponse.id, - name: apiResponse.name, - description: apiResponse.description, - ownerId: apiResponse.ownerId, - createdAt: new Date().toISOString(), // Would need from API - }, - membership: null, // Would need from API based on current user - canManage: false, // Would need from API based on current user - }; -} - -export class TeamDetailsPresenter implements ITeamDetailsPresenter { - private viewModel: TeamDetailsViewModel | null = null; - - reset(): void { - this.viewModel = null; - } - - async fetchAndPresent(teamId: string): Promise { - const apiResponse = await apiClient.teams.getDetails(teamId); - if (apiResponse) { - this.viewModel = transformApiResponse(apiResponse); - } else { - this.viewModel = null; - } - } - - getViewModel(): TeamDetailsViewModel | null { - return this.viewModel; - } -} - -/** - * Convenience function to fetch and transform team details - */ -export async function fetchTeamDetails(teamId: string): Promise { - const apiResponse = await apiClient.teams.getDetails(teamId); - if (!apiResponse) { - return null; - } - return transformApiResponse(apiResponse); -} \ No newline at end of file +export const presentTeamDetails = (dto: TeamDetailsDto, currentUserId: string): TeamDetailsViewModel => { + return new TeamDetailsViewModel(dto, currentUserId); +}; \ No newline at end of file diff --git a/apps/website/lib/presenters/TeamJoinRequestPresenter.ts b/apps/website/lib/presenters/TeamJoinRequestPresenter.ts new file mode 100644 index 000000000..de6824d30 --- /dev/null +++ b/apps/website/lib/presenters/TeamJoinRequestPresenter.ts @@ -0,0 +1,6 @@ +import { TeamJoinRequestItemDto } from '../dtos'; +import { TeamJoinRequestViewModel } from '../view-models'; + +export const presentTeamJoinRequest = (dto: TeamJoinRequestItemDto, currentUserId: string, isOwner: boolean): TeamJoinRequestViewModel => { + return new TeamJoinRequestViewModel(dto, currentUserId, isOwner); +}; \ No newline at end of file diff --git a/apps/website/lib/presenters/TeamMemberPresenter.ts b/apps/website/lib/presenters/TeamMemberPresenter.ts new file mode 100644 index 000000000..fac61a0a5 --- /dev/null +++ b/apps/website/lib/presenters/TeamMemberPresenter.ts @@ -0,0 +1,6 @@ +import { TeamMemberDto } from '../dtos'; +import { TeamMemberViewModel } from '../view-models'; + +export const presentTeamMember = (dto: TeamMemberDto, currentUserId: string, teamOwnerId: string): TeamMemberViewModel => { + return new TeamMemberViewModel(dto, currentUserId, teamOwnerId); +}; \ No newline at end of file diff --git a/apps/website/lib/presenters/TeamSummaryPresenter.ts b/apps/website/lib/presenters/TeamSummaryPresenter.ts new file mode 100644 index 000000000..ee655601f --- /dev/null +++ b/apps/website/lib/presenters/TeamSummaryPresenter.ts @@ -0,0 +1,6 @@ +import { TeamSummaryDto } from '../dtos'; +import { TeamSummaryViewModel } from '../view-models'; + +export const presentTeamSummary = (dto: TeamSummaryDto): TeamSummaryViewModel => { + return new TeamSummaryViewModel(dto); +}; \ No newline at end of file diff --git a/apps/website/lib/presenters/WalletPresenter.ts b/apps/website/lib/presenters/WalletPresenter.ts new file mode 100644 index 000000000..40d3cb6dc --- /dev/null +++ b/apps/website/lib/presenters/WalletPresenter.ts @@ -0,0 +1,6 @@ +import { WalletDto, WalletTransactionDto } from '../dtos'; +import { WalletViewModel } from '../view-models'; + +export const presentWallet = (dto: WalletDto): WalletViewModel => { + return new WalletViewModel(dto); +}; \ No newline at end of file diff --git a/apps/website/lib/presenters/WalletTransactionPresenter.ts b/apps/website/lib/presenters/WalletTransactionPresenter.ts new file mode 100644 index 000000000..5508978d6 --- /dev/null +++ b/apps/website/lib/presenters/WalletTransactionPresenter.ts @@ -0,0 +1,6 @@ +import { WalletTransactionDto } from '../dtos'; +import { WalletTransactionViewModel } from '../view-models'; + +export const presentWalletTransaction = (dto: WalletTransactionDto): WalletTransactionViewModel => { + return new WalletTransactionViewModel(dto); +}; \ No newline at end of file diff --git a/apps/website/lib/presenters/index.ts b/apps/website/lib/presenters/index.ts new file mode 100644 index 000000000..88c335711 --- /dev/null +++ b/apps/website/lib/presenters/index.ts @@ -0,0 +1,35 @@ +// Analytics Presenters + +// Auth Presenters + +// Driver Presenters +export { presentDriversLeaderboard } from './DriversLeaderboardPresenter'; +export { presentDriverRegistrationStatus } from './DriverRegistrationStatusPresenter'; + +// League Presenters +export { presentLeagueMember } from './LeagueMemberPresenter'; +export { presentLeagueStandings } from './LeagueStandingsPresenter'; +export { presentLeagueSummaries, presentLeagueSummary } from './LeagueSummaryPresenter'; + +// Payments Presenters +export { presentMembershipFee } from './MembershipFeePresenter'; +export { presentPayment } from './PaymentPresenter'; +export { presentPrize } from './PrizePresenter'; +export { presentWallet } from './WalletPresenter'; +export { presentWalletTransaction } from './WalletTransactionPresenter'; + +// Race Presenters +export { presentRaceDetail } from './RaceDetailPresenter'; +export { presentRaceListItem } from './RaceListItemPresenter'; +export { presentRaceResult } from './RaceResultsPresenter'; +export { presentRaceResultsDetail } from './RaceResultsDetailPresenter'; + +// Sponsor Presenters +export { presentSponsor } from './SponsorPresenter'; +export { presentSponsorshipDetail } from './SponsorshipDetailPresenter'; + +// Team Presenters +export { presentTeamDetails } from './TeamDetailsPresenter'; +export { presentTeamJoinRequest } from './TeamJoinRequestPresenter'; +export { presentTeamMember } from './TeamMemberPresenter'; +export { presentTeamSummary } from './TeamSummaryPresenter'; \ No newline at end of file diff --git a/apps/website/lib/services/analytics/AnalyticsService.ts b/apps/website/lib/services/analytics/AnalyticsService.ts new file mode 100644 index 000000000..aece2d12f --- /dev/null +++ b/apps/website/lib/services/analytics/AnalyticsService.ts @@ -0,0 +1,9 @@ +import { api as api } from '../../api'; + +export async function recordPageView(input: any): Promise { + return await api.analytics.recordPageView(input); +} + +export async function recordEngagement(input: any): Promise { + return await api.analytics.recordEngagement(input); +} \ No newline at end of file diff --git a/apps/website/lib/services/analytics/DashboardService.ts b/apps/website/lib/services/analytics/DashboardService.ts new file mode 100644 index 000000000..b31505b46 --- /dev/null +++ b/apps/website/lib/services/analytics/DashboardService.ts @@ -0,0 +1,6 @@ +import { api as api } from '../../api'; + +export async function getDashboardOverview(): Promise { + // TODO: aggregate data + return {}; +} \ No newline at end of file diff --git a/apps/website/lib/services/auth/AuthService.ts b/apps/website/lib/services/auth/AuthService.ts new file mode 100644 index 000000000..f8b82a8d5 --- /dev/null +++ b/apps/website/lib/services/auth/AuthService.ts @@ -0,0 +1,17 @@ +import { api as api } from '../../api'; + +export async function signup(params: any): Promise { + return await api.auth.signup(params); +} + +export async function login(params: any): Promise { + return await api.auth.login(params); +} + +export async function logout(): Promise { + await api.auth.logout(); +} + +export function getIracingAuthUrl(returnTo?: string): string { + return api.auth.getIracingAuthUrl(returnTo); +} \ No newline at end of file diff --git a/apps/website/lib/services/auth/SessionService.ts b/apps/website/lib/services/auth/SessionService.ts new file mode 100644 index 000000000..061866aa1 --- /dev/null +++ b/apps/website/lib/services/auth/SessionService.ts @@ -0,0 +1,9 @@ +import { api as api } from '../../api'; +import { SessionViewModel } from '../../view-models'; + +export async function getSession(): Promise { + const dto = await api.auth.getSession(); + if (!dto) return null; + // TODO: presenter + return dto as any; +} \ No newline at end of file diff --git a/apps/website/lib/services/drivers/DriverRegistrationService.ts b/apps/website/lib/services/drivers/DriverRegistrationService.ts new file mode 100644 index 000000000..1650f4ad1 --- /dev/null +++ b/apps/website/lib/services/drivers/DriverRegistrationService.ts @@ -0,0 +1,14 @@ +import { api as api } from '../../api'; +import { presentDriverRegistrationStatus } from '../../presenters'; +import { DriverRegistrationStatusViewModel } from '../../view-models'; + +export async function getDriverRegistrationStatus(driverId: string): Promise { + // TODO: implement API call + const dto = { driverId, status: 'pending' }; + return presentDriverRegistrationStatus(dto); +} + +export async function registerDriver(input: any): Promise { + // TODO: implement + return {}; +} \ No newline at end of file diff --git a/apps/website/lib/services/drivers/DriverService.ts b/apps/website/lib/services/drivers/DriverService.ts new file mode 100644 index 000000000..6dad001b1 --- /dev/null +++ b/apps/website/lib/services/drivers/DriverService.ts @@ -0,0 +1,16 @@ +import { api as api } from '../../api'; +import { presentDriversLeaderboard } from '../../presenters'; +import { DriverLeaderboardViewModel } from '../../view-models'; + +export async function getDriverLeaderboard(): Promise { + const dto = await api.drivers.getLeaderboard(); + return presentDriversLeaderboard(dto); +} + +export async function completeDriverOnboarding(input: any): Promise { + return await api.drivers.completeOnboarding(input); +} + +export async function getCurrentDriver(): Promise { + return await api.drivers.getCurrent(); +} \ No newline at end of file diff --git a/apps/website/lib/services/index.ts b/apps/website/lib/services/index.ts new file mode 100644 index 000000000..6091025c9 --- /dev/null +++ b/apps/website/lib/services/index.ts @@ -0,0 +1,36 @@ +// Analytics Services +export * from './analytics/AnalyticsService'; +export * from './analytics/DashboardService'; + +// Auth Services +export * from './auth/AuthService'; +export * from './auth/SessionService'; + +// Driver Services +export * from './drivers/DriverService'; +export * from './drivers/DriverRegistrationService'; + +// League Services +export * from './leagues/LeagueService'; +export * from './leagues/LeagueMembershipService'; + +// Media Services +export * from './media/MediaService'; +export * from './media/AvatarService'; + +// Payments Services +export * from './payments/PaymentService'; +export * from './payments/WalletService'; +export * from './payments/MembershipFeeService'; + +// Race Services +export * from './races/RaceService'; +export * from './races/RaceResultsService'; + +// Sponsor Services +export * from './sponsors/SponsorService'; +export * from './sponsors/SponsorshipService'; + +// Team Services +export * from './teams/TeamService'; +export * from './teams/TeamJoinService'; \ No newline at end of file diff --git a/apps/website/lib/services/leagues/LeagueMembershipService.ts b/apps/website/lib/services/leagues/LeagueMembershipService.ts new file mode 100644 index 000000000..de5fe3400 --- /dev/null +++ b/apps/website/lib/services/leagues/LeagueMembershipService.ts @@ -0,0 +1,12 @@ +import { api as api } from '../../api'; +import { presentLeagueMember } from '../../presenters'; +import { LeagueMemberViewModel } from '../../view-models'; + +export async function getLeagueMemberships(leagueId: string, currentUserId: string): Promise { + const dto = await api.leagues.getMemberships(leagueId); + return dto.members.map(m => presentLeagueMember(m, currentUserId)); +} + +export async function removeLeagueMember(leagueId: string, performerDriverId: string, targetDriverId: string): Promise { + await api.leagues.removeMember(leagueId, performerDriverId, targetDriverId); +} \ No newline at end of file diff --git a/apps/website/lib/services/leagues/LeagueService.ts b/apps/website/lib/services/leagues/LeagueService.ts new file mode 100644 index 000000000..4d615d403 --- /dev/null +++ b/apps/website/lib/services/leagues/LeagueService.ts @@ -0,0 +1,28 @@ +import { api as api } from '../../api'; +import { presentLeagueSummaries, presentLeagueStandings } from '../../presenters'; +import { LeagueSummaryViewModel, LeagueStandingsViewModel } from '../../view-models'; + +export async function getAllLeagues(): Promise { + const dto = await api.leagues.getAllWithCapacity(); + return presentLeagueSummaries(dto.leagues); +} + +export async function getLeagueStandings(leagueId: string, currentUserId?: string): Promise { + const dto = await api.leagues.getStandings(leagueId); + // TODO: include drivers and memberships in dto + const dtoWithExtras = { + ...dto, + drivers: [], // TODO: fetch drivers + memberships: [], // TODO: fetch memberships + }; + return presentLeagueStandings(dtoWithExtras, currentUserId || ''); +} + +export async function createLeague(input: any): Promise { + return await api.leagues.create(input); +} + +export async function getLeagueAdminView(leagueId: string): Promise { + // TODO: implement + return {}; +} \ No newline at end of file diff --git a/apps/website/lib/services/media/AvatarService.ts b/apps/website/lib/services/media/AvatarService.ts new file mode 100644 index 000000000..d6154da42 --- /dev/null +++ b/apps/website/lib/services/media/AvatarService.ts @@ -0,0 +1,5 @@ +import { api as api } from '../../api'; + +export async function requestAvatarGeneration(input: any): Promise { + return await api.media.requestAvatarGeneration(input); +} \ No newline at end of file diff --git a/apps/website/lib/services/media/MediaService.ts b/apps/website/lib/services/media/MediaService.ts new file mode 100644 index 000000000..8abe790cb --- /dev/null +++ b/apps/website/lib/services/media/MediaService.ts @@ -0,0 +1,6 @@ +import { api as api } from '../../api'; + +export async function uploadMedia(file: any): Promise { + // TODO: implement + return {}; +} \ No newline at end of file diff --git a/apps/website/lib/services/payments/MembershipFeeService.ts b/apps/website/lib/services/payments/MembershipFeeService.ts new file mode 100644 index 000000000..41838bfe5 --- /dev/null +++ b/apps/website/lib/services/payments/MembershipFeeService.ts @@ -0,0 +1,8 @@ +import { api as api } from '../../api'; +import { presentMembershipFee } from '../../presenters'; +import { MembershipFeeViewModel } from '../../view-models'; + +export async function getMembershipFees(leagueId: string): Promise { + const dto = await api.payments.getMembershipFees(leagueId); + return dto.fees.map(f => presentMembershipFee(f)); +} \ No newline at end of file diff --git a/apps/website/lib/services/payments/PaymentService.ts b/apps/website/lib/services/payments/PaymentService.ts new file mode 100644 index 000000000..8543b304c --- /dev/null +++ b/apps/website/lib/services/payments/PaymentService.ts @@ -0,0 +1,12 @@ +import { api as api } from '../../api'; +import { presentPayment } from '../../presenters'; +import { PaymentViewModel } from '../../view-models'; + +export async function getPayments(leagueId?: string, driverId?: string): Promise { + const dto = await api.payments.getPayments(leagueId, driverId); + return dto.payments.map(p => presentPayment(p)); +} + +export async function createPayment(input: any): Promise { + return await api.payments.createPayment(input); +} \ No newline at end of file diff --git a/apps/website/lib/services/payments/WalletService.ts b/apps/website/lib/services/payments/WalletService.ts new file mode 100644 index 000000000..110e3a9bd --- /dev/null +++ b/apps/website/lib/services/payments/WalletService.ts @@ -0,0 +1,8 @@ +import { api as api } from '../../api'; +import { presentWallet } from '../../presenters'; +import { WalletViewModel } from '../../view-models'; + +export async function getWallet(driverId: string): Promise { + const dto = await api.payments.getWallet(driverId); + return presentWallet(dto); +} \ No newline at end of file diff --git a/apps/website/lib/services/races/RaceResultsService.test.ts b/apps/website/lib/services/races/RaceResultsService.test.ts new file mode 100644 index 000000000..12faa803e --- /dev/null +++ b/apps/website/lib/services/races/RaceResultsService.test.ts @@ -0,0 +1,121 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { getRaceResults, getRaceSOF, importRaceResults } from './RaceResultsService'; +import type { RaceResultsDetailDto, RaceWithSOFDto, ImportRaceResultsSummaryDto } from '../../dtos'; + +// Mock the API client +vi.mock('../../api', () => ({ + apiClient: { + races: { + getResultsDetail: vi.fn(), + getWithSOF: vi.fn(), + importResults: vi.fn(), + }, + }, +})); + +// Mock the presenter +vi.mock('../../presenters', () => ({ + presentRaceResultsDetail: vi.fn(), +})); + +import { api } from '../../api'; +import { presentRaceResultsDetail } from '../../presenters'; + +describe('RaceResultsService', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + describe('getRaceResults', () => { + it('should call API and presenter with correct parameters', async () => { + const mockDto: RaceResultsDetailDto = { + id: 'race-1', + name: 'Test Race', + results: [], + // ... other required fields + } as RaceResultsDetailDto; + + const mockViewModel = { + id: 'race-1', + name: 'Test Race', + formattedResults: [], + }; + + const raceId = 'race-123'; + const currentUserId = 'user-456'; + + // Mock API call + vi.mocked(api.races.getResultsDetail).mockResolvedValue(mockDto); + // Mock presenter + vi.mocked(presentRaceResultsDetail).mockReturnValue(mockViewModel); + + const result = await getRaceResults(raceId, currentUserId); + + expect(api.races.getResultsDetail).toHaveBeenCalledWith(raceId); + expect(presentRaceResultsDetail).toHaveBeenCalledWith(mockDto, currentUserId); + expect(result).toBe(mockViewModel); + }); + + it('should call presenter with undefined currentUserId when not provided', async () => { + const mockDto: RaceResultsDetailDto = { + id: 'race-1', + name: 'Test Race', + results: [], + } as RaceResultsDetailDto; + + const mockViewModel = { + id: 'race-1', + name: 'Test Race', + formattedResults: [], + }; + + const raceId = 'race-123'; + + vi.mocked(api.races.getResultsDetail).mockResolvedValue(mockDto); + vi.mocked(presentRaceResultsDetail).mockReturnValue(mockViewModel); + + await getRaceResults(raceId); + + expect(presentRaceResultsDetail).toHaveBeenCalledWith(mockDto, undefined); + }); + }); + + describe('getRaceSOF', () => { + it('should call API and return DTO directly', async () => { + const mockDto: RaceWithSOFDto = { + id: 'race-1', + name: 'Test Race', + sof: 1500, + // ... other fields + } as RaceWithSOFDto; + + const raceId = 'race-123'; + + vi.mocked(api.races.getWithSOF).mockResolvedValue(mockDto); + + const result = await getRaceSOF(raceId); + + expect(api.races.getWithSOF).toHaveBeenCalledWith(raceId); + expect(result).toBe(mockDto); + }); + }); + + describe('importRaceResults', () => { + it('should call API with correct parameters and return result', async () => { + const mockInput = { results: [] }; + const mockSummary: ImportRaceResultsSummaryDto = { + totalImported: 10, + errors: [], + }; + + const raceId = 'race-123'; + + vi.mocked(api.races.importResults).mockResolvedValue(mockSummary); + + const result = await importRaceResults(raceId, mockInput); + + expect(api.races.importResults).toHaveBeenCalledWith(raceId, mockInput); + expect(result).toBe(mockSummary); + }); + }); +}); \ No newline at end of file diff --git a/apps/website/lib/services/races/RaceResultsService.ts b/apps/website/lib/services/races/RaceResultsService.ts new file mode 100644 index 000000000..c448c296f --- /dev/null +++ b/apps/website/lib/services/races/RaceResultsService.ts @@ -0,0 +1,23 @@ +import { api as api } from '../../api'; +import { presentRaceResultsDetail } from '../../presenters'; +import { RaceResultsDetailViewModel } from '../../view-models'; + +export async function getRaceResults( + raceId: string, + currentUserId?: string +): Promise { + const dto = await api.races.getResultsDetail(raceId); + return presentRaceResultsDetail(dto, currentUserId); +} + +export async function getRaceSOF(raceId: string): Promise { + const dto = await api.races.getWithSOF(raceId); + // TODO: use presenter + return dto; +} + +export async function importRaceResults(raceId: string, input: any): Promise { + const dto = await api.races.importResults(raceId, input); + // TODO: use presenter + return dto; +} \ No newline at end of file diff --git a/apps/website/lib/services/races/RaceService.ts b/apps/website/lib/services/races/RaceService.ts new file mode 100644 index 000000000..84741af9d --- /dev/null +++ b/apps/website/lib/services/races/RaceService.ts @@ -0,0 +1,22 @@ +import { api as api } from '../../api'; +import { presentRaceDetail } from '../../presenters'; +import { RaceDetailViewModel } from '../../view-models'; + +export async function getRaceDetail( + raceId: string, + driverId: string +): Promise { + const dto = await api.races.getDetail(raceId, driverId); + return presentRaceDetail(dto); +} + +export async function getRacesPageData(): Promise { + const dto = await api.races.getPageData(); + // TODO: use presenter + return dto; +} + +export async function getRacesTotal(): Promise { + const dto = await api.races.getTotal(); + return dto; +} \ No newline at end of file diff --git a/apps/website/lib/services/sponsors/SponsorService.ts b/apps/website/lib/services/sponsors/SponsorService.ts new file mode 100644 index 000000000..cc0c92d2a --- /dev/null +++ b/apps/website/lib/services/sponsors/SponsorService.ts @@ -0,0 +1,17 @@ +import { api as api } from '../../api'; +import { presentSponsor } from '../../presenters'; +import { SponsorViewModel } from '../../view-models'; + +export async function getAllSponsors(): Promise { + const dto = await api.sponsors.getAll(); + return dto.sponsors.map(s => presentSponsor(s)); +} + +export async function createSponsor(input: any): Promise { + return await api.sponsors.create(input); +} + +export async function getSponsorDashboard(sponsorId: string): Promise { + const dto = await api.sponsors.getDashboard(sponsorId); + return dto; +} \ No newline at end of file diff --git a/apps/website/lib/services/sponsors/SponsorshipService.ts b/apps/website/lib/services/sponsors/SponsorshipService.ts new file mode 100644 index 000000000..31b94bbd4 --- /dev/null +++ b/apps/website/lib/services/sponsors/SponsorshipService.ts @@ -0,0 +1,11 @@ +import { api as api } from '../../api'; + +export async function getSponsorshipPricing(): Promise { + const dto = await api.sponsors.getPricing(); + return dto; +} + +export async function getSponsorSponsorships(sponsorId: string): Promise { + const dto = await api.sponsors.getSponsorships(sponsorId); + return dto; +} \ No newline at end of file diff --git a/apps/website/lib/services/teams/TeamJoinService.ts b/apps/website/lib/services/teams/TeamJoinService.ts new file mode 100644 index 000000000..01c336bcc --- /dev/null +++ b/apps/website/lib/services/teams/TeamJoinService.ts @@ -0,0 +1,16 @@ +import { api as api } from '../../api'; +import { presentTeamJoinRequest } from '../../presenters'; +import { TeamJoinRequestViewModel } from '../../view-models'; + +export async function getTeamJoinRequests(teamId: string, currentUserId: string, isOwner: boolean): Promise { + const dto = await api.teams.getJoinRequests(teamId); + return dto.requests.map(r => presentTeamJoinRequest(r, currentUserId, isOwner)); +} + +export async function approveTeamJoinRequest(teamId: string, requestId: string): Promise { + // TODO: implement API call +} + +export async function rejectTeamJoinRequest(teamId: string, requestId: string): Promise { + // TODO: implement API call +} \ No newline at end of file diff --git a/apps/website/lib/services/teams/TeamService.ts b/apps/website/lib/services/teams/TeamService.ts new file mode 100644 index 000000000..2d3bccc35 --- /dev/null +++ b/apps/website/lib/services/teams/TeamService.ts @@ -0,0 +1,30 @@ +import { api as api } from '../../api'; +import { presentTeamDetails, presentTeamMember, presentTeamSummary } from '../../presenters'; +import { TeamDetailsViewModel, TeamMemberViewModel, TeamSummaryViewModel } from '../../view-models'; + +export async function getAllTeams(): Promise { + const dto = await api.teams.getAll(); + return dto.teams.map(t => presentTeamSummary(t)); +} + +export async function getTeamDetails(teamId: string): Promise { + const dto = await api.teams.getDetails(teamId); + return dto ? presentTeamDetails(dto) : null; +} + +export async function getTeamMembers(teamId: string): Promise { + const dto = await api.teams.getMembers(teamId); + return dto.members.map(m => presentTeamMember(m)); +} + +export async function createTeam(input: any): Promise { + return await api.teams.create(input); +} + +export async function updateTeam(teamId: string, input: any): Promise { + return await api.teams.update(teamId, input); +} + +export async function getDriverTeam(driverId: string): Promise { + return await api.teams.getDriverTeam(driverId); +} \ No newline at end of file diff --git a/apps/website/lib/view-models/AnalyticsDashboardViewModel.ts b/apps/website/lib/view-models/AnalyticsDashboardViewModel.ts new file mode 100644 index 000000000..364cb33f1 --- /dev/null +++ b/apps/website/lib/view-models/AnalyticsDashboardViewModel.ts @@ -0,0 +1,30 @@ +// Analytics dashboard view model +// Represents dashboard data for analytics + +export class AnalyticsDashboardViewModel { + totalUsers: number; + activeUsers: number; + totalRaces: number; + totalLeagues: number; + + constructor(data: { totalUsers: number; activeUsers: number; totalRaces: number; totalLeagues: number }) { + Object.assign(this, data); + } + + /** UI-specific: User engagement rate */ + get userEngagementRate(): number { + return this.totalUsers > 0 ? (this.activeUsers / this.totalUsers) * 100 : 0; + } + + /** UI-specific: Formatted engagement rate */ + get formattedEngagementRate(): string { + return `${this.userEngagementRate.toFixed(1)}%`; + } + + /** UI-specific: Activity level */ + get activityLevel(): string { + if (this.userEngagementRate > 70) return 'High'; + if (this.userEngagementRate > 40) return 'Medium'; + return 'Low'; + } +} \ No newline at end of file diff --git a/apps/website/lib/view-models/AnalyticsMetricsViewModel.ts b/apps/website/lib/view-models/AnalyticsMetricsViewModel.ts new file mode 100644 index 000000000..207a024da --- /dev/null +++ b/apps/website/lib/view-models/AnalyticsMetricsViewModel.ts @@ -0,0 +1,35 @@ +// Analytics metrics view model +// Represents metrics data for analytics + +export class AnalyticsMetricsViewModel { + pageViews: number; + uniqueVisitors: number; + averageSessionDuration: number; + bounceRate: number; + + constructor(data: { pageViews: number; uniqueVisitors: number; averageSessionDuration: number; bounceRate: number }) { + Object.assign(this, data); + } + + /** UI-specific: Formatted page views */ + get formattedPageViews(): string { + return this.pageViews.toLocaleString(); + } + + /** UI-specific: Formatted unique visitors */ + get formattedUniqueVisitors(): string { + return this.uniqueVisitors.toLocaleString(); + } + + /** UI-specific: Formatted session duration */ + get formattedSessionDuration(): string { + const minutes = Math.floor(this.averageSessionDuration / 60); + const seconds = Math.floor(this.averageSessionDuration % 60); + return `${minutes}:${seconds.toString().padStart(2, '0')}`; + } + + /** UI-specific: Formatted bounce rate */ + get formattedBounceRate(): string { + return `${this.bounceRate.toFixed(1)}%`; + } +} \ No newline at end of file diff --git a/apps/website/lib/view-models/DriverLeaderboardItemViewModel.ts b/apps/website/lib/view-models/DriverLeaderboardItemViewModel.ts new file mode 100644 index 000000000..6decb39c0 --- /dev/null +++ b/apps/website/lib/view-models/DriverLeaderboardItemViewModel.ts @@ -0,0 +1,76 @@ +import { DriverLeaderboardItemDto } from '../dtos'; + +export class DriverLeaderboardItemViewModel implements DriverLeaderboardItemDto { + id: string; + name: string; + avatarUrl?: string; + rating: number; + wins: number; + races: number; + skillLevel: string; + isActive: boolean; + nationality: string; + podiums: number; + + position: number; + private previousRating?: number; + + constructor(dto: DriverLeaderboardItemDto, position: number, previousRating?: number) { + Object.assign(this, dto); + this.position = position; + this.previousRating = previousRating; + } + + /** UI-specific: Skill level color */ + get skillLevelColor(): string { + switch (this.skillLevel) { + case 'beginner': return 'green'; + case 'intermediate': return 'yellow'; + case 'advanced': return 'orange'; + case 'expert': return 'red'; + default: return 'gray'; + } + } + + /** UI-specific: Skill level icon */ + get skillLevelIcon(): string { + switch (this.skillLevel) { + case 'beginner': return '🥉'; + case 'intermediate': return '🥈'; + case 'advanced': return '🥇'; + case 'expert': return '👑'; + default: return '🏁'; + } + } + + /** UI-specific: Win rate */ + get winRate(): number { + return this.races > 0 ? (this.wins / this.races) * 100 : 0; + } + + /** UI-specific: Formatted win rate */ + get winRateFormatted(): string { + return `${this.winRate.toFixed(1)}%`; + } + + /** UI-specific: Rating trend */ + get ratingTrend(): 'up' | 'down' | 'same' { + if (!this.previousRating) return 'same'; + if (this.rating > this.previousRating) return 'up'; + if (this.rating < this.previousRating) return 'down'; + return 'same'; + } + + /** UI-specific: Rating change indicator */ + get ratingChangeIndicator(): string { + const change = this.previousRating ? this.rating - this.previousRating : 0; + if (change > 0) return `+${change}`; + if (change < 0) return `${change}`; + return '0'; + } + + /** UI-specific: Position badge */ + get positionBadge(): string { + return this.position.toString(); + } +} \ No newline at end of file diff --git a/apps/website/lib/view-models/DriverLeaderboardViewModel.ts b/apps/website/lib/view-models/DriverLeaderboardViewModel.ts new file mode 100644 index 000000000..2157cf506 --- /dev/null +++ b/apps/website/lib/view-models/DriverLeaderboardViewModel.ts @@ -0,0 +1,28 @@ +import { DriversLeaderboardDto, DriverLeaderboardItemDto } from '../dtos'; +import { DriverLeaderboardItemViewModel } from './DriverLeaderboardItemViewModel'; + +export class DriverLeaderboardViewModel implements DriversLeaderboardDto { + drivers: DriverLeaderboardItemViewModel[]; + + constructor(dto: DriversLeaderboardDto & { drivers: DriverLeaderboardItemDto[] }, previousDrivers?: DriverLeaderboardItemDto[]) { + this.drivers = dto.drivers.map((driver, index) => { + const previous = previousDrivers?.find(p => p.id === driver.id); + return new DriverLeaderboardItemViewModel(driver, index + 1, previous?.rating); + }); + } + + /** UI-specific: Total races across all drivers */ + get totalRaces(): number { + return this.drivers.reduce((sum, driver) => sum + driver.races, 0); + } + + /** UI-specific: Total wins across all drivers */ + get totalWins(): number { + return this.drivers.reduce((sum, driver) => sum + driver.wins, 0); + } + + /** UI-specific: Active drivers count */ + get activeCount(): number { + return this.drivers.filter(driver => driver.isActive).length; + } +} \ No newline at end of file diff --git a/apps/website/lib/view-models/DriverRegistrationStatusViewModel.ts b/apps/website/lib/view-models/DriverRegistrationStatusViewModel.ts new file mode 100644 index 000000000..18b48cecb --- /dev/null +++ b/apps/website/lib/view-models/DriverRegistrationStatusViewModel.ts @@ -0,0 +1,36 @@ +import { DriverRegistrationStatusDto } from '../dtos'; + +export class DriverRegistrationStatusViewModel implements DriverRegistrationStatusDto { + isRegistered: boolean; + raceId: string; + driverId: string; + + constructor(dto: DriverRegistrationStatusDto) { + Object.assign(this, dto); + } + + /** UI-specific: Status message */ + get statusMessage(): string { + return this.isRegistered ? 'Registered for this race' : 'Not registered'; + } + + /** UI-specific: Status color */ + get statusColor(): string { + return this.isRegistered ? 'green' : 'red'; + } + + /** UI-specific: Badge variant */ + get statusBadgeVariant(): string { + return this.isRegistered ? 'success' : 'warning'; + } + + /** UI-specific: Registration button text */ + get registrationButtonText(): string { + return this.isRegistered ? 'Withdraw' : 'Register'; + } + + /** UI-specific: Whether can register (assuming always can if not registered) */ + get canRegister(): boolean { + return !this.isRegistered; + } +} \ No newline at end of file diff --git a/apps/website/lib/view-models/LeagueAdminViewModel.ts b/apps/website/lib/view-models/LeagueAdminViewModel.ts new file mode 100644 index 000000000..61308d9fa --- /dev/null +++ b/apps/website/lib/view-models/LeagueAdminViewModel.ts @@ -0,0 +1,16 @@ +import type { LeagueAdminDto } from '../dtos'; +import type { LeagueMemberViewModel, LeagueJoinRequestViewModel } from './'; + +/** + * League admin view model + * Transform from DTO to ViewModel with UI fields + */ +export interface LeagueAdminViewModel { + config: LeagueAdminDto['config']; + members: LeagueMemberViewModel[]; + joinRequests: LeagueJoinRequestViewModel[]; + // Total pending requests count + pendingRequestsCount: number; + // Whether there are any pending requests + hasPendingRequests: boolean; +} \ No newline at end of file diff --git a/apps/website/lib/view-models/LeagueJoinRequestViewModel.ts b/apps/website/lib/view-models/LeagueJoinRequestViewModel.ts new file mode 100644 index 000000000..95466cabf --- /dev/null +++ b/apps/website/lib/view-models/LeagueJoinRequestViewModel.ts @@ -0,0 +1,14 @@ +import type { LeagueJoinRequestDto } from '../dtos'; + +/** + * League join request view model + * Transform from DTO to ViewModel with UI fields + */ +export interface LeagueJoinRequestViewModel extends LeagueJoinRequestDto { + // Formatted request date + formattedRequestedAt: string; + // Whether the request can be approved by current user + canApprove: boolean; + // Whether the request can be rejected by current user + canReject: boolean; +} \ No newline at end of file diff --git a/apps/website/lib/view-models/LeagueMemberViewModel.ts b/apps/website/lib/view-models/LeagueMemberViewModel.ts new file mode 100644 index 000000000..72c88af21 --- /dev/null +++ b/apps/website/lib/view-models/LeagueMemberViewModel.ts @@ -0,0 +1,39 @@ +import { LeagueMemberDto, DriverDto } from '../dtos'; + +export class LeagueMemberViewModel implements LeagueMemberDto { + driverId: string; + driver?: DriverDto; + role: string; + joinedAt: string; + + private currentUserId: string; + + constructor(dto: LeagueMemberDto, currentUserId: string) { + Object.assign(this, dto); + this.currentUserId = currentUserId; + } + + /** UI-specific: Formatted join date */ + get formattedJoinedAt(): string { + return new Date(this.joinedAt).toLocaleDateString(); + } + + /** UI-specific: Badge variant for role */ + get roleBadgeVariant(): string { + switch (this.role) { + case 'owner': return 'primary'; + case 'admin': return 'secondary'; + default: return 'default'; + } + } + + /** UI-specific: Whether this member is the owner */ + get isOwner(): boolean { + return this.role === 'owner'; + } + + /** UI-specific: Whether this is the current user */ + get isCurrentUser(): boolean { + return this.driverId === this.currentUserId; + } +} \ No newline at end of file diff --git a/apps/website/lib/view-models/LeagueStandingsViewModel.ts b/apps/website/lib/view-models/LeagueStandingsViewModel.ts new file mode 100644 index 000000000..78200d39a --- /dev/null +++ b/apps/website/lib/view-models/LeagueStandingsViewModel.ts @@ -0,0 +1,19 @@ +import { LeagueStandingsDto, StandingEntryDto, DriverDto, LeagueMembership } from '../dtos'; +import { StandingEntryViewModel } from './StandingEntryViewModel'; + +export class LeagueStandingsViewModel implements LeagueStandingsDto { + standings: StandingEntryViewModel[]; + drivers: DriverDto[]; + memberships: LeagueMembership[]; + + constructor(dto: LeagueStandingsDto & { standings: StandingEntryDto[] }, currentUserId: string, previousStandings?: StandingEntryDto[]) { + const leaderPoints = dto.standings[0]?.points || 0; + this.standings = dto.standings.map((entry, index) => { + const nextPoints = dto.standings[index + 1]?.points || entry.points; + const previousPosition = previousStandings?.find(p => p.driverId === entry.driverId)?.position; + return new StandingEntryViewModel(entry, leaderPoints, nextPoints, currentUserId, previousPosition); + }); + this.drivers = dto.drivers; + this.memberships = dto.memberships; + } +} \ No newline at end of file diff --git a/apps/website/lib/view-models/LeagueSummaryViewModel.ts b/apps/website/lib/view-models/LeagueSummaryViewModel.ts new file mode 100644 index 000000000..36a1be029 --- /dev/null +++ b/apps/website/lib/view-models/LeagueSummaryViewModel.ts @@ -0,0 +1,63 @@ +import { LeagueSummaryDto } from '../dtos'; + +export class LeagueSummaryViewModel implements LeagueSummaryDto { + id: string; + name: string; + description?: string; + logoUrl?: string; + coverImage?: string; + memberCount: number; + maxMembers: number; + isPublic: boolean; + ownerId: string; + ownerName?: string; + scoringType?: string; + status?: string; + + constructor(dto: LeagueSummaryDto) { + Object.assign(this, dto); + } + + /** UI-specific: Formatted capacity display */ + get formattedCapacity(): string { + return `${this.memberCount}/${this.maxMembers}`; + } + + /** UI-specific: Capacity bar percentage */ + get capacityBarPercent(): number { + return (this.memberCount / this.maxMembers) * 100; + } + + /** UI-specific: Label for join button */ + get joinButtonLabel(): string { + if (this.isFull) return 'Full'; + return this.isJoinable ? 'Join League' : 'Request to Join'; + } + + /** UI-specific: Whether the league is full */ + get isFull(): boolean { + return this.memberCount >= this.maxMembers; + } + + /** UI-specific: Whether the league is joinable */ + get isJoinable(): boolean { + return this.isPublic && !this.isFull; + } + + /** UI-specific: Color for member progress */ + get memberProgressColor(): string { + const percent = this.capacityBarPercent; + if (percent < 50) return 'green'; + if (percent < 80) return 'yellow'; + return 'red'; + } + + /** UI-specific: Badge variant for status */ + get statusBadgeVariant(): string { + switch (this.status) { + case 'active': return 'success'; + case 'inactive': return 'secondary'; + default: return 'default'; + } + } +} \ No newline at end of file diff --git a/apps/website/lib/view-models/MembershipFeeViewModel.ts b/apps/website/lib/view-models/MembershipFeeViewModel.ts new file mode 100644 index 000000000..535f456fc --- /dev/null +++ b/apps/website/lib/view-models/MembershipFeeViewModel.ts @@ -0,0 +1,32 @@ +import { MembershipFeeDto } from '../dtos'; + +export class MembershipFeeViewModel implements MembershipFeeDto { + leagueId: string; + amount: number; + currency: string; + period: string; + + constructor(dto: MembershipFeeDto) { + Object.assign(this, dto); + } + + /** UI-specific: Formatted amount */ + get formattedAmount(): string { + return `${this.currency} ${this.amount.toFixed(2)}`; + } + + /** UI-specific: Period display */ + get periodDisplay(): string { + switch (this.period) { + case 'monthly': return 'Monthly'; + case 'yearly': return 'Yearly'; + case 'season': return 'Per Season'; + default: return this.period; + } + } + + /** UI-specific: Amount per period */ + get amountPerPeriod(): string { + return `${this.formattedAmount} ${this.periodDisplay.toLowerCase()}`; + } +} \ No newline at end of file diff --git a/apps/website/lib/view-models/PaymentViewModel.ts b/apps/website/lib/view-models/PaymentViewModel.ts new file mode 100644 index 000000000..7e7618b92 --- /dev/null +++ b/apps/website/lib/view-models/PaymentViewModel.ts @@ -0,0 +1,38 @@ +import { PaymentDto } from '../dtos'; + +export class PaymentViewModel implements PaymentDto { + id: string; + amount: number; + currency: string; + status: string; + createdAt: string; + + constructor(dto: PaymentDto) { + Object.assign(this, dto); + } + + /** UI-specific: Formatted amount */ + get formattedAmount(): string { + return `${this.currency} ${this.amount.toFixed(2)}`; + } + + /** UI-specific: Status color */ + get statusColor(): string { + switch (this.status) { + case 'completed': return 'green'; + case 'pending': return 'yellow'; + case 'failed': return 'red'; + default: return 'gray'; + } + } + + /** UI-specific: Formatted created date */ + get formattedCreatedAt(): string { + return new Date(this.createdAt).toLocaleString(); + } + + /** UI-specific: Status display */ + get statusDisplay(): string { + return this.status.charAt(0).toUpperCase() + this.status.slice(1); + } +} \ No newline at end of file diff --git a/apps/website/lib/view-models/PrizeViewModel.ts b/apps/website/lib/view-models/PrizeViewModel.ts new file mode 100644 index 000000000..4176f7638 --- /dev/null +++ b/apps/website/lib/view-models/PrizeViewModel.ts @@ -0,0 +1,34 @@ +import { PrizeDto } from '../dtos'; + +export class PrizeViewModel implements PrizeDto { + id: string; + name: string; + amount: number; + currency: string; + position?: number; + + constructor(dto: PrizeDto) { + Object.assign(this, dto); + } + + /** UI-specific: Formatted amount */ + get formattedAmount(): string { + return `${this.currency} ${this.amount.toFixed(2)}`; + } + + /** UI-specific: Position display */ + get positionDisplay(): string { + if (!this.position) return 'Special'; + switch (this.position) { + case 1: return '1st Place'; + case 2: return '2nd Place'; + case 3: return '3rd Place'; + default: return `${this.position}th Place`; + } + } + + /** UI-specific: Prize description */ + get prizeDescription(): string { + return `${this.name} - ${this.formattedAmount}`; + } +} \ No newline at end of file diff --git a/apps/website/lib/view-models/ProtestViewModel.ts b/apps/website/lib/view-models/ProtestViewModel.ts new file mode 100644 index 000000000..b32d607a2 --- /dev/null +++ b/apps/website/lib/view-models/ProtestViewModel.ts @@ -0,0 +1,13 @@ +/** + * Protest view model + * Represents a race protest + */ +export interface ProtestViewModel { + id: string; + raceId: string; + complainantId: string; + defendantId: string; + description: string; + status: string; + createdAt: string; +} \ No newline at end of file diff --git a/apps/website/lib/view-models/RaceDetailViewModel.ts b/apps/website/lib/view-models/RaceDetailViewModel.ts new file mode 100644 index 000000000..261fbb13a --- /dev/null +++ b/apps/website/lib/view-models/RaceDetailViewModel.ts @@ -0,0 +1,57 @@ +import { RaceDetailDto, RaceDetailRaceDto, RaceDetailLeagueDto, RaceDetailEntryDto, RaceDetailRegistrationDto, RaceDetailUserResultDto } from '../dtos'; + +export class RaceDetailViewModel implements RaceDetailDto { + race: RaceDetailRaceDto | null; + league: RaceDetailLeagueDto | null; + entryList: RaceDetailEntryDto[]; + registration: RaceDetailRegistrationDto; + userResult: RaceDetailUserResultDto | null; + error?: string; + + constructor(dto: RaceDetailDto) { + Object.assign(this, dto); + } + + /** UI-specific: Whether user is registered */ + get isRegistered(): boolean { + return this.registration.isRegistered; + } + + /** UI-specific: Whether user can register */ + get canRegister(): boolean { + return this.registration.canRegister; + } + + /** UI-specific: Race status display */ + get raceStatusDisplay(): string { + if (!this.race) return 'Unknown'; + switch (this.race.status) { + case 'upcoming': return 'Upcoming'; + case 'live': return 'Live'; + case 'finished': return 'Finished'; + default: return this.race.status; + } + } + + /** UI-specific: Formatted scheduled time */ + get formattedScheduledTime(): string { + return this.race ? new Date(this.race.scheduledTime).toLocaleString() : ''; + } + + /** UI-specific: Entry list count */ + get entryCount(): number { + return this.entryList.length; + } + + /** UI-specific: Whether race has results */ + get hasResults(): boolean { + return this.userResult !== null; + } + + /** UI-specific: Registration status message */ + get registrationStatusMessage(): string { + if (this.isRegistered) return 'You are registered for this race'; + if (this.canRegister) return 'You can register for this race'; + return 'Registration not available'; + } +} \ No newline at end of file diff --git a/apps/website/lib/view-models/RaceListItemViewModel.ts b/apps/website/lib/view-models/RaceListItemViewModel.ts new file mode 100644 index 000000000..d7e978ab2 --- /dev/null +++ b/apps/website/lib/view-models/RaceListItemViewModel.ts @@ -0,0 +1,55 @@ +import { RaceListItemDto } from '../dtos'; + +export class RaceListItemViewModel implements RaceListItemDto { + id: string; + name: string; + leagueId: string; + leagueName: string; + scheduledTime: string; + status: string; + trackName?: string; + + constructor(dto: RaceListItemDto) { + Object.assign(this, dto); + } + + /** UI-specific: Formatted scheduled time */ + get formattedScheduledTime(): string { + return new Date(this.scheduledTime).toLocaleString(); + } + + /** UI-specific: Badge variant for status */ + get statusBadgeVariant(): string { + switch (this.status) { + case 'upcoming': return 'info'; + case 'live': return 'success'; + case 'finished': return 'secondary'; + default: return 'default'; + } + } + + /** UI-specific: Whether race is upcoming */ + get isUpcoming(): boolean { + return this.status === 'upcoming'; + } + + /** UI-specific: Whether race is live */ + get isLive(): boolean { + return this.status === 'live'; + } + + /** UI-specific: Time until start in minutes */ + get timeUntilStart(): number { + const now = new Date(); + const scheduled = new Date(this.scheduledTime); + return Math.max(0, Math.floor((scheduled.getTime() - now.getTime()) / (1000 * 60))); + } + + /** UI-specific: Display for time until start */ + get timeUntilStartDisplay(): string { + const minutes = this.timeUntilStart; + if (minutes < 60) return `${minutes}m`; + const hours = Math.floor(minutes / 60); + return `${hours}h ${minutes % 60}m`; + } +} \ No newline at end of file diff --git a/apps/website/lib/view-models/RaceResultViewModel.ts b/apps/website/lib/view-models/RaceResultViewModel.ts new file mode 100644 index 000000000..3caea06bf --- /dev/null +++ b/apps/website/lib/view-models/RaceResultViewModel.ts @@ -0,0 +1,70 @@ +import { RaceResultDto } from '../dtos'; + +export class RaceResultViewModel implements RaceResultDto { + id: string; + raceId: string; + driverId: string; + driverName: string; + avatarUrl: string; + position: number; + startPosition: number; + incidents: number; + fastestLap: number; + positionChange: number; + isPodium: boolean; + isClean: boolean; + + constructor(dto: RaceResultDto) { + Object.assign(this, dto); + } + + /** UI-specific: Display for position change */ + get positionChangeDisplay(): string { + if (this.positionChange > 0) return `+${this.positionChange}`; + if (this.positionChange < 0) return `${this.positionChange}`; + return '0'; + } + + /** UI-specific: Color for position change */ + get positionChangeColor(): string { + if (this.positionChange > 0) return 'green'; + if (this.positionChange < 0) return 'red'; + return 'gray'; + } + + /** UI-specific: Whether this is the winner */ + get isWinner(): boolean { + return this.position === 1; + } + + /** UI-specific: Whether has fastest lap */ + get hasFastestLap(): boolean { + return this.fastestLap > 0; + } + + /** UI-specific: Badge for position */ + get positionBadge(): string { + return this.position.toString(); + } + + /** UI-specific: Color for incidents badge */ + get incidentsBadgeColor(): string { + if (this.incidents === 0) return 'green'; + if (this.incidents <= 2) return 'yellow'; + return 'red'; + } + + /** UI-specific: Formatted lap time */ + get lapTimeFormatted(): string { + if (this.fastestLap <= 0) return '--:--.---'; + const minutes = Math.floor(this.fastestLap / 60); + const seconds = Math.floor(this.fastestLap % 60); + const milliseconds = Math.floor((this.fastestLap % 1) * 1000); + return `${minutes}:${seconds.toString().padStart(2, '0')}.${milliseconds.toString().padStart(3, '0')}`; + } + + /** Compatibility with old DTO interface */ + getPositionChange(): number { + return this.positionChange; + } +} \ No newline at end of file diff --git a/apps/website/lib/view-models/RaceResultsDetailViewModel.ts b/apps/website/lib/view-models/RaceResultsDetailViewModel.ts new file mode 100644 index 000000000..148e7298d --- /dev/null +++ b/apps/website/lib/view-models/RaceResultsDetailViewModel.ts @@ -0,0 +1,63 @@ +import { RaceResultsDetailDto, RaceResultDto } from '../dtos'; +import { RaceResultViewModel } from './RaceResultViewModel'; + +export class RaceResultsDetailViewModel implements RaceResultsDetailDto { + raceId: string; + track: string; + results: RaceResultViewModel[]; + league?: { id: string; name: string }; + race?: { id: string; track: string; scheduledAt: string }; + drivers: { id: string; name: string }[]; + pointsSystem: Record; + fastestLapTime: number; + penalties: { driverId: string; type: string; value?: number }[]; + currentDriverId: string; + + private currentUserId: string; + + constructor(dto: RaceResultsDetailDto & { results: RaceResultDto[] }, currentUserId: string) { + this.raceId = dto.raceId; + this.track = dto.track; + this.results = dto.results.map(r => new RaceResultViewModel({ ...r, raceId: dto.raceId })); + this.league = dto.league; + this.race = dto.race; + this.drivers = dto.drivers; + this.pointsSystem = dto.pointsSystem; + this.fastestLapTime = dto.fastestLapTime; + this.penalties = dto.penalties; + this.currentDriverId = dto.currentDriverId; + this.currentUserId = currentUserId; + } + + /** UI-specific: Results sorted by position */ + get resultsByPosition(): RaceResultViewModel[] { + return [...this.results].sort((a, b) => a.position - b.position); + } + + /** UI-specific: Results sorted by fastest lap */ + get resultsByFastestLap(): RaceResultViewModel[] { + return [...this.results].sort((a, b) => a.fastestLap - b.fastestLap); + } + + /** UI-specific: Clean drivers only */ + get cleanDrivers(): RaceResultViewModel[] { + return this.results.filter(r => r.isClean); + } + + /** UI-specific: Current user's result */ + get currentUserResult(): RaceResultViewModel | undefined { + return this.results.find(r => r.driverId === this.currentUserId); + } + + /** UI-specific: Race stats */ + get stats(): { totalDrivers: number; cleanRate: number; averageIncidents: number } { + const total = this.results.length; + const clean = this.cleanDrivers.length; + const totalIncidents = this.results.reduce((sum, r) => sum + r.incidents, 0); + return { + totalDrivers: total, + cleanRate: total > 0 ? (clean / total) * 100 : 0, + averageIncidents: total > 0 ? totalIncidents / total : 0 + }; + } +} \ No newline at end of file diff --git a/apps/website/lib/view-models/SessionViewModel.ts b/apps/website/lib/view-models/SessionViewModel.ts new file mode 100644 index 000000000..317b43d03 --- /dev/null +++ b/apps/website/lib/view-models/SessionViewModel.ts @@ -0,0 +1,36 @@ +import { SessionDataDto } from '../dtos'; + +export class SessionViewModel implements SessionDataDto { + userId: string; + email: string; + displayName?: string; + driverId?: string; + isAuthenticated: boolean; + + constructor(dto: SessionDataDto) { + Object.assign(this, dto); + } + + /** UI-specific: User greeting */ + get greeting(): string { + return `Hello, ${this.displayName || this.email}!`; + } + + /** UI-specific: Avatar initials */ + get avatarInitials(): string { + if (this.displayName) { + return this.displayName.split(' ').map(n => n[0]).join('').toUpperCase(); + } + return this.email[0].toUpperCase(); + } + + /** UI-specific: Whether has driver profile */ + get hasDriverProfile(): boolean { + return !!this.driverId; + } + + /** UI-specific: Authentication status display */ + get authStatusDisplay(): string { + return this.isAuthenticated ? 'Logged In' : 'Logged Out'; + } +} \ No newline at end of file diff --git a/apps/website/lib/view-models/SponsorViewModel.ts b/apps/website/lib/view-models/SponsorViewModel.ts new file mode 100644 index 000000000..4fb50ba94 --- /dev/null +++ b/apps/website/lib/view-models/SponsorViewModel.ts @@ -0,0 +1,27 @@ +import { SponsorDto } from '../dtos'; + +export class SponsorViewModel implements SponsorDto { + id: string; + name: string; + logoUrl?: string; + websiteUrl?: string; + + constructor(dto: SponsorDto) { + Object.assign(this, dto); + } + + /** UI-specific: Display name */ + get displayName(): string { + return this.name; + } + + /** UI-specific: Whether has website */ + get hasWebsite(): boolean { + return !!this.websiteUrl; + } + + /** UI-specific: Website link text */ + get websiteLinkText(): string { + return 'Visit Website'; + } +} \ No newline at end of file diff --git a/apps/website/lib/view-models/SponsorshipDetailViewModel.ts b/apps/website/lib/view-models/SponsorshipDetailViewModel.ts new file mode 100644 index 000000000..aba2cf8d1 --- /dev/null +++ b/apps/website/lib/view-models/SponsorshipDetailViewModel.ts @@ -0,0 +1,41 @@ +import { SponsorshipDetailDto } from '../dtos'; + +export class SponsorshipDetailViewModel implements SponsorshipDetailDto { + id: string; + leagueId: string; + leagueName: string; + seasonId: string; + tier: 'main' | 'secondary'; + status: string; + amount: number; + currency: string; + + constructor(dto: SponsorshipDetailDto) { + Object.assign(this, dto); + } + + /** UI-specific: Formatted amount */ + get formattedAmount(): string { + return `${this.currency} ${this.amount.toLocaleString()}`; + } + + /** UI-specific: Tier badge variant */ + get tierBadgeVariant(): string { + return this.tier === 'main' ? 'primary' : 'secondary'; + } + + /** UI-specific: Status color */ + get statusColor(): string { + switch (this.status) { + case 'active': return 'green'; + case 'pending': return 'yellow'; + case 'expired': return 'red'; + default: return 'gray'; + } + } + + /** UI-specific: Status display */ + get statusDisplay(): string { + return this.status.charAt(0).toUpperCase() + this.status.slice(1); + } +} \ No newline at end of file diff --git a/apps/website/lib/view-models/StandingEntryViewModel.ts b/apps/website/lib/view-models/StandingEntryViewModel.ts new file mode 100644 index 000000000..3ddbeb8dc --- /dev/null +++ b/apps/website/lib/view-models/StandingEntryViewModel.ts @@ -0,0 +1,61 @@ +import { StandingEntryDto, DriverDto } from '../dtos'; + +export class StandingEntryViewModel implements StandingEntryDto { + driverId: string; + driver?: DriverDto; + position: number; + points: number; + wins: number; + podiums: number; + races: number; + + private leaderPoints: number; + private nextPoints: number; + private currentUserId: string; + private previousPosition?: number; + + constructor(dto: StandingEntryDto, leaderPoints: number, nextPoints: number, currentUserId: string, previousPosition?: number) { + Object.assign(this, dto); + this.leaderPoints = leaderPoints; + this.nextPoints = nextPoints; + this.currentUserId = currentUserId; + this.previousPosition = previousPosition; + } + + /** UI-specific: Badge for position display */ + get positionBadge(): string { + return this.position.toString(); + } + + /** UI-specific: Points difference to leader */ + get pointsGapToLeader(): number { + return this.points - this.leaderPoints; + } + + /** UI-specific: Points difference to next position */ + get pointsGapToNext(): number { + return this.points - this.nextPoints; + } + + /** UI-specific: Whether this entry is the current user */ + get isCurrentUser(): boolean { + return this.driverId === this.currentUserId; + } + + /** UI-specific: Trend compared to previous */ + get trend(): 'up' | 'down' | 'same' { + if (!this.previousPosition) return 'same'; + if (this.position < this.previousPosition) return 'up'; + if (this.position > this.previousPosition) return 'down'; + return 'same'; + } + + /** UI-specific: Arrow for trend */ + get trendArrow(): string { + switch (this.trend) { + case 'up': return '↑'; + case 'down': return '↓'; + default: return '-'; + } + } +} \ No newline at end of file diff --git a/apps/website/lib/view-models/TeamDetailsViewModel.ts b/apps/website/lib/view-models/TeamDetailsViewModel.ts new file mode 100644 index 000000000..16c6091a7 --- /dev/null +++ b/apps/website/lib/view-models/TeamDetailsViewModel.ts @@ -0,0 +1,47 @@ +import { TeamDetailsDto, TeamMemberDto } from '../dtos'; +import { TeamMemberViewModel } from './TeamMemberViewModel'; + +export class TeamDetailsViewModel implements TeamDetailsDto { + id: string; + name: string; + description?: string; + logoUrl?: string; + memberCount: number; + ownerId: string; + members: TeamMemberViewModel[]; + + private currentUserId: string; + + constructor(dto: TeamDetailsDto & { members: TeamMemberDto[] }, currentUserId: string) { + this.id = dto.id; + this.name = dto.name; + this.description = dto.description; + this.logoUrl = dto.logoUrl; + this.memberCount = dto.memberCount; + this.ownerId = dto.ownerId; + this.members = dto.members.map(m => new TeamMemberViewModel(m, currentUserId, dto.ownerId)); + this.currentUserId = currentUserId; + } + + /** UI-specific: Whether current user is owner */ + get isOwner(): boolean { + return this.currentUserId === this.ownerId; + } + + /** UI-specific: Whether can add members */ + get canAddMembers(): boolean { + return this.isOwner && this.memberCount < 10; // Assuming max 10 + } + + /** UI-specific: Member management actions available */ + get memberActionsAvailable(): boolean { + return this.isOwner; + } + + /** UI-specific: Team status */ + get teamStatus(): string { + if (this.memberCount < 5) return 'Recruiting'; + if (this.memberCount < 10) return 'Active'; + return 'Full'; + } +} \ No newline at end of file diff --git a/apps/website/lib/view-models/TeamJoinRequestViewModel.ts b/apps/website/lib/view-models/TeamJoinRequestViewModel.ts new file mode 100644 index 000000000..1ee6909cc --- /dev/null +++ b/apps/website/lib/view-models/TeamJoinRequestViewModel.ts @@ -0,0 +1,48 @@ +import { TeamJoinRequestItemDto } from '../dtos'; + +export class TeamJoinRequestViewModel implements TeamJoinRequestItemDto { + id: string; + teamId: string; + driverId: string; + requestedAt: string; + message?: string; + + private currentUserId: string; + private isOwner: boolean; + + constructor(dto: TeamJoinRequestItemDto, currentUserId: string, isOwner: boolean) { + Object.assign(this, dto); + this.currentUserId = currentUserId; + this.isOwner = isOwner; + } + + /** UI-specific: Whether current user can approve */ + get canApprove(): boolean { + return this.isOwner; + } + + /** UI-specific: Formatted requested date */ + get formattedRequestedAt(): string { + return new Date(this.requestedAt).toLocaleString(); + } + + /** UI-specific: Request status (pending) */ + get status(): string { + return 'Pending'; + } + + /** UI-specific: Status color */ + get statusColor(): string { + return 'yellow'; + } + + /** UI-specific: Approve button text */ + get approveButtonText(): string { + return 'Approve'; + } + + /** UI-specific: Reject button text */ + get rejectButtonText(): string { + return 'Reject'; + } +} \ No newline at end of file diff --git a/apps/website/lib/view-models/TeamMemberViewModel.ts b/apps/website/lib/view-models/TeamMemberViewModel.ts new file mode 100644 index 000000000..bdc4464e3 --- /dev/null +++ b/apps/website/lib/view-models/TeamMemberViewModel.ts @@ -0,0 +1,47 @@ +import { TeamMemberDto, DriverDto } from '../dtos'; + +export class TeamMemberViewModel implements TeamMemberDto { + driverId: string; + driver?: DriverDto; + role: string; + joinedAt: string; + + private currentUserId: string; + private teamOwnerId: string; + + constructor(dto: TeamMemberDto, currentUserId: string, teamOwnerId: string) { + Object.assign(this, dto); + this.currentUserId = currentUserId; + this.teamOwnerId = teamOwnerId; + } + + /** UI-specific: Role badge variant */ + get roleBadgeVariant(): string { + switch (this.role) { + case 'owner': return 'primary'; + case 'captain': return 'secondary'; + case 'member': return 'default'; + default: return 'default'; + } + } + + /** UI-specific: Whether this member is the owner */ + get isOwner(): boolean { + return this.driverId === this.teamOwnerId; + } + + /** UI-specific: Whether current user can manage this member */ + get canManage(): boolean { + return this.currentUserId === this.teamOwnerId && this.driverId !== this.currentUserId; + } + + /** UI-specific: Whether this is the current user */ + get isCurrentUser(): boolean { + return this.driverId === this.currentUserId; + } + + /** UI-specific: Formatted joined date */ + get formattedJoinedAt(): string { + return new Date(this.joinedAt).toLocaleDateString(); + } +} \ No newline at end of file diff --git a/apps/website/lib/view-models/TeamSummaryViewModel.ts b/apps/website/lib/view-models/TeamSummaryViewModel.ts new file mode 100644 index 000000000..1bab1e2a1 --- /dev/null +++ b/apps/website/lib/view-models/TeamSummaryViewModel.ts @@ -0,0 +1,41 @@ +import { TeamSummaryDto } from '../dtos'; + +export class TeamSummaryViewModel implements TeamSummaryDto { + id: string; + name: string; + logoUrl?: string; + memberCount: number; + rating: number; + + private maxMembers = 10; // Assuming max members + + constructor(dto: TeamSummaryDto) { + Object.assign(this, dto); + } + + /** UI-specific: Whether team is full */ + get isFull(): boolean { + return this.memberCount >= this.maxMembers; + } + + /** UI-specific: Rating display */ + get ratingDisplay(): string { + return this.rating.toFixed(0); + } + + /** UI-specific: Member count display */ + get memberCountDisplay(): string { + return `${this.memberCount}/${this.maxMembers}`; + } + + /** UI-specific: Status indicator */ + get statusIndicator(): string { + if (this.isFull) return 'Full'; + return 'Open'; + } + + /** UI-specific: Status color */ + get statusColor(): string { + return this.isFull ? 'red' : 'green'; + } +} \ No newline at end of file diff --git a/apps/website/lib/view-models/UserProfileViewModel.ts b/apps/website/lib/view-models/UserProfileViewModel.ts new file mode 100644 index 000000000..c0ab3e6a8 --- /dev/null +++ b/apps/website/lib/view-models/UserProfileViewModel.ts @@ -0,0 +1,37 @@ +import { DriverDto } from '../dtos'; + +export class UserProfileViewModel implements DriverDto { + id: string; + name: string; + avatarUrl?: string; + iracingId?: string; + rating?: number; + + constructor(dto: DriverDto) { + Object.assign(this, dto); + } + + /** UI-specific: Formatted rating */ + get formattedRating(): string { + return this.rating ? this.rating.toFixed(0) : 'Unrated'; + } + + /** UI-specific: Whether has iRacing ID */ + get hasIracingId(): boolean { + return !!this.iracingId; + } + + /** UI-specific: Profile completeness percentage */ + get profileCompleteness(): number { + let complete = 1; // id always there + if (this.avatarUrl) complete++; + if (this.iracingId) complete++; + if (this.rating) complete++; + return Math.round((complete / 4) * 100); + } + + /** UI-specific: Avatar initials */ + get avatarInitials(): string { + return this.name.split(' ').map(n => n[0]).join('').toUpperCase(); + } +} \ No newline at end of file diff --git a/apps/website/lib/view-models/WalletTransactionViewModel.ts b/apps/website/lib/view-models/WalletTransactionViewModel.ts new file mode 100644 index 000000000..98d712159 --- /dev/null +++ b/apps/website/lib/view-models/WalletTransactionViewModel.ts @@ -0,0 +1,34 @@ +import { WalletTransactionDto } from '../dtos'; + +export class WalletTransactionViewModel implements WalletTransactionDto { + id: string; + type: 'deposit' | 'withdrawal'; + amount: number; + description?: string; + createdAt: string; + + constructor(dto: WalletTransactionDto) { + Object.assign(this, dto); + } + + /** UI-specific: Formatted amount with sign */ + get formattedAmount(): string { + const sign = this.type === 'deposit' ? '+' : '-'; + return `${sign}$${this.amount.toFixed(2)}`; + } + + /** UI-specific: Amount color */ + get amountColor(): string { + return this.type === 'deposit' ? 'green' : 'red'; + } + + /** UI-specific: Type display */ + get typeDisplay(): string { + return this.type.charAt(0).toUpperCase() + this.type.slice(1); + } + + /** UI-specific: Formatted created date */ + get formattedCreatedAt(): string { + return new Date(this.createdAt).toLocaleString(); + } +} \ No newline at end of file diff --git a/apps/website/lib/view-models/WalletViewModel.ts b/apps/website/lib/view-models/WalletViewModel.ts new file mode 100644 index 000000000..40e5a367a --- /dev/null +++ b/apps/website/lib/view-models/WalletViewModel.ts @@ -0,0 +1,36 @@ +import { WalletDto, WalletTransactionDto } from '../dtos'; +import { WalletTransactionViewModel } from './WalletTransactionViewModel'; + +export class WalletViewModel implements WalletDto { + driverId: string; + balance: number; + currency: string; + transactions: WalletTransactionViewModel[]; + + constructor(dto: WalletDto & { transactions: WalletTransactionDto[] }) { + this.driverId = dto.driverId; + this.balance = dto.balance; + this.currency = dto.currency; + this.transactions = dto.transactions.map(t => new WalletTransactionViewModel(t)); + } + + /** UI-specific: Formatted balance */ + get formattedBalance(): string { + return `${this.currency} ${this.balance.toFixed(2)}`; + } + + /** UI-specific: Balance color */ + get balanceColor(): string { + return this.balance >= 0 ? 'green' : 'red'; + } + + /** UI-specific: Recent transactions (last 5) */ + get recentTransactions(): WalletTransactionViewModel[] { + return this.transactions.slice(0, 5); + } + + /** UI-specific: Total transactions count */ + get totalTransactions(): number { + return this.transactions.length; + } +} \ No newline at end of file diff --git a/apps/website/lib/view-models/index.ts b/apps/website/lib/view-models/index.ts new file mode 100644 index 000000000..23b40d092 --- /dev/null +++ b/apps/website/lib/view-models/index.ts @@ -0,0 +1,41 @@ +// Analytics ViewModels +export { AnalyticsDashboardViewModel } from './AnalyticsDashboardViewModel'; +export { AnalyticsMetricsViewModel } from './AnalyticsMetricsViewModel'; + +// Auth ViewModels +export { SessionViewModel } from './SessionViewModel'; +export { UserProfileViewModel } from './UserProfileViewModel'; + +// Driver ViewModels +export { DriverLeaderboardItemViewModel } from './DriverLeaderboardItemViewModel'; +export { DriverLeaderboardViewModel } from './DriverLeaderboardViewModel'; +export { DriverRegistrationStatusViewModel } from './DriverRegistrationStatusViewModel'; + +// League ViewModels +export { LeagueMemberViewModel } from './LeagueMemberViewModel'; +export { LeagueStandingsViewModel } from './LeagueStandingsViewModel'; +export { LeagueSummaryViewModel } from './LeagueSummaryViewModel'; +export { StandingEntryViewModel } from './StandingEntryViewModel'; + +// Payments ViewModels +export { MembershipFeeViewModel } from './MembershipFeeViewModel'; +export { PaymentViewModel } from './PaymentViewModel'; +export { PrizeViewModel } from './PrizeViewModel'; +export { WalletTransactionViewModel } from './WalletTransactionViewModel'; +export { WalletViewModel } from './WalletViewModel'; + +// Race ViewModels +export { RaceDetailViewModel } from './RaceDetailViewModel'; +export { RaceListItemViewModel } from './RaceListItemViewModel'; +export { RaceResultViewModel } from './RaceResultViewModel'; +export { RaceResultsDetailViewModel } from './RaceResultsDetailViewModel'; + +// Sponsor ViewModels +export { SponsorViewModel } from './SponsorViewModel'; +export { SponsorshipDetailViewModel } from './SponsorshipDetailViewModel'; + +// Team ViewModels +export { TeamDetailsViewModel } from './TeamDetailsViewModel'; +export { TeamJoinRequestViewModel } from './TeamJoinRequestViewModel'; +export { TeamMemberViewModel } from './TeamMemberViewModel'; +export { TeamSummaryViewModel } from './TeamSummaryViewModel'; \ No newline at end of file diff --git a/plans/DTO-Refactor-Super-Detailed-Plan.md b/plans/DTO-Refactor-Super-Detailed-Plan.md new file mode 100644 index 000000000..02188c167 --- /dev/null +++ b/plans/DTO-Refactor-Super-Detailed-Plan.md @@ -0,0 +1,2137 @@ +# Super Detailed DTO Refactor Plan for apps/website + +## Executive Summary + +This plan addresses the massive DTO/ViewModel pollution in `apps/website/lib/apiClient.ts` (1160 lines, 80+ types) and inline definitions in pages like `apps/website/app/races/[id]/results/page.tsx`. It enforces [DATA_FLOW.md](apps/website/DATA_FLOW.md) strictly: API DTOs → Presenters → View Models → Services → UI. Results in ~100 new files, apiClient shrunk 95%, zero inline DTOs in UI. + +## Current State Analysis + +### Problem 1: Monolithic apiClient.ts +**Location**: `apps/website/lib/apiClient.ts` (1160 lines) + +**Violations**: +- 80+ type definitions mixed (transport DTOs + UI ViewModels + inputs/outputs) +- Single file responsibility violation +- No separation between HTTP layer and data transformation +- Direct UI coupling to transport shapes + +### Problem 2: Inline Page DTOs +**Location**: `apps/website/app/races/[id]/results/page.tsx` (lines 17-56) + +**Violations**: +- Pages defining transport contracts +- No reusability +- Tight coupling to implementation details +- Violates presenter pattern + +### Problem 3: Missing Architecture Layers +**Current**: UI → apiClient (mixed DTOs/ViewModels) +**Required**: UI → Services → Presenters → API (DTOs only) + +## Phase 1: Complete Type Inventory & Classification + +### Step 1.1: Catalog All apiClient.ts Types (Lines 13-634) + +Create spreadsheet/document with columns: +1. Type Name (current) +2. Line Number Range +3. Classification (DTO/ViewModel/Input/Output) +4. Target Location +5. Dependencies +6. Used By (pages/components) + +**Complete List of 80+ Types**: + +#### Common/Shared Types (Lines 13-54) +```typescript +// Line 13-19: DriverDTO +export interface DriverDTO { + id: string; + name: string; + avatarUrl?: string; + iracingId?: string; + rating?: number; +} +// Classification: Transport DTO +// Target: apps/website/lib/dtos/DriverDto.ts +// Dependencies: None +// Used By: Multiple (races, teams, leagues) + +// Line 21-29: ProtestViewModel +export interface ProtestViewModel { + id: string; + raceId: string; + complainantId: string; + defendantId: string; + description: string; + status: string; + createdAt: string; +} +// Classification: UI ViewModel +// Target: apps/website/lib/view-models/ProtestViewModel.ts +// Dependencies: None +// Used By: races/[id]/protests, leagues/[id]/admin + +// Line 31-36: LeagueMemberViewModel +export interface LeagueMemberViewModel { + driverId: string; + driver?: DriverDTO; + role: string; + joinedAt: string; +} +// Classification: UI ViewModel +// Target: apps/website/lib/view-models/LeagueMemberViewModel.ts +// Dependencies: DriverDTO +// Used By: leagues/[id]/members + +// ... (continue for ALL 80 types with same detail level) +``` + +#### League Domain Types (Lines 57-152) +- LeagueSummaryViewModel (57-70) → DTO + ViewModel +- AllLeaguesWithCapacityViewModel (72-74) → DTO +- LeagueStatsDto (76-79) → DTO +- LeagueJoinRequestViewModel (80-87) → ViewModel +- LeagueAdminPermissionsViewModel (88-95) → ViewModel +- LeagueOwnerSummaryViewModel (97-102) → ViewModel +- LeagueConfigFormModelDto (104-111) → DTO +- LeagueAdminProtestsViewModel (113-115) → ViewModel +- LeagueSeasonSummaryViewModel (117-123) → ViewModel +- LeagueMembershipsViewModel (125-127) → ViewModel +- LeagueStandingsViewModel (129-131) → ViewModel +- LeagueScheduleViewModel (133-135) → ViewModel +- LeagueStatsViewModel (137-145) → ViewModel +- LeagueAdminViewModel (147-151) → ViewModel +- CreateLeagueInput (153-159) → Input DTO +- CreateLeagueOutput (161-164) → Output DTO + +#### Driver Domain Types (Lines 166-199) +- DriverLeaderboardItemViewModel (167-175) → ViewModel +- DriversLeaderboardViewModel (177-179) → ViewModel +- DriverStatsDto (181-183) → DTO +- CompleteOnboardingInput (185-188) → Input DTO +- CompleteOnboardingOutput (190-193) → Output DTO +- DriverRegistrationStatusViewModel (195-199) → ViewModel + +#### Team Domain Types (Lines 201-273) +- TeamSummaryViewModel (202-208) → ViewModel +- AllTeamsViewModel (210-212) → ViewModel +- TeamMemberViewModel (214-219) → ViewModel +- TeamJoinRequestItemViewModel (221-227) → ViewModel +- TeamDetailsViewModel (229-237) → ViewModel +- TeamMembersViewModel (239-241) → ViewModel +- TeamJoinRequestsViewModel (243-245) → ViewModel +- DriverTeamViewModel (247-252) → ViewModel +- CreateTeamInput (254-258) → Input DTO +- CreateTeamOutput (260-263) → Output DTO +- UpdateTeamInput (265-269) → Input DTO +- UpdateTeamOutput (271-273) → Output DTO + +#### Race Domain Types (Lines 275-447) +- RaceListItemViewModel (276-284) → ViewModel +- AllRacesPageViewModel (286-288) → ViewModel +- RaceStatsDto (290-292) → DTO +- RaceDetailEntryViewModel (295-302) → ViewModel +- RaceDetailUserResultViewModel (304-313) → ViewModel +- RaceDetailRaceViewModel (315-326) → ViewModel +- RaceDetailLeagueViewModel (328-336) → ViewModel +- RaceDetailRegistrationViewModel (338-341) → ViewModel +- RaceDetailViewModel (343-350) → ViewModel +- RacesPageDataRaceViewModel (352-364) → ViewModel +- RacesPageDataViewModel (366-368) → ViewModel +- RaceResultViewModel (370-381) → ViewModel +- RaceResultsDetailViewModel (383-387) → ViewModel +- RaceWithSOFViewModel (389-393) → ViewModel +- RaceProtestViewModel (395-405) → ViewModel +- RaceProtestsViewModel (407-410) → ViewModel +- RacePenaltyViewModel (412-420) → ViewModel +- RacePenaltiesViewModel (423-426) → ViewModel +- RegisterForRaceParams (428-431) → Input DTO +- WithdrawFromRaceParams (433-435) → Input DTO +- ImportRaceResultsInput (437-439) → Input DTO +- ImportRaceResultsSummaryViewModel (441-447) → ViewModel + +#### Sponsor Domain Types (Lines 449-502) +- GetEntitySponsorshipPricingResultDto (450-454) → DTO +- SponsorViewModel (456-461) → ViewModel +- GetSponsorsOutput (463-465) → Output DTO +- CreateSponsorInput (467-472) → Input DTO +- CreateSponsorOutput (474-477) → Output DTO +- SponsorDashboardDTO (479-485) → DTO +- SponsorshipDetailViewModel (487-496) → ViewModel +- SponsorSponsorshipsDTO (498-502) → DTO + +#### Media Domain Types (Lines 504-514) +- RequestAvatarGenerationInput (505-508) → Input DTO +- RequestAvatarGenerationOutput (510-514) → Output DTO + +#### Analytics Domain Types (Lines 516-536) +- RecordPageViewInput (517-521) → Input DTO +- RecordPageViewOutput (523-525) → Output DTO +- RecordEngagementInput (527-532) → Input DTO +- RecordEngagementOutput (534-536) → Output DTO + +#### Auth Domain Types (Lines 538-556) +- LoginParams (539-542) → Input DTO +- SignupParams (544-548) → Input DTO +- SessionData (550-556) → DTO + +#### Payments Domain Types (Lines 558-633) +- PaymentViewModel (559-565) → ViewModel +- GetPaymentsOutput (567-569) → Output DTO +- CreatePaymentInput (571-577) → Input DTO +- CreatePaymentOutput (579-582) → Output DTO +- MembershipFeeViewModel (584-589) → ViewModel +- MemberPaymentViewModel (591-596) → ViewModel +- GetMembershipFeesOutput (598-601) → Output DTO +- PrizeViewModel (603-609) → ViewModel +- GetPrizesOutput (611-613) → Output DTO +- WalletTransactionViewModel (615-621) → ViewModel +- WalletViewModel (623-628) → ViewModel +- GetWalletOutput (630-633) → Output DTO + +### Step 1.2: Catalog Page Inline DTOs + +**File**: `apps/website/app/races/[id]/results/page.tsx` + +```typescript +// Lines 17-24: PenaltyTypeDTO +type PenaltyTypeDTO = + | 'time_penalty' + | 'grid_penalty' + | 'points_deduction' + | 'disqualification' + | 'warning' + | 'license_points' + | string; +// Target: apps/website/lib/dtos/PenaltyTypeDto.ts + +// Lines 26-30: PenaltyData +interface PenaltyData { + driverId: string; + type: PenaltyTypeDTO; + value?: number; +} +// Target: apps/website/lib/dtos/PenaltyDataDto.ts + +// Lines 32-42: RaceResultRowDTO +interface RaceResultRowDTO { + id: string; + raceId: string; + driverId: string; + position: number; + fastestLap: number; + incidents: number; + startPosition: number; + getPositionChange(): number; +} +// Target: apps/website/lib/dtos/RaceResultRowDto.ts +// Note: Remove method, make pure data + +// Lines 44-46: DriverRowDTO +interface DriverRowDTO { + id: string; + name: string; +} +// Target: Reuse DriverDto from common + +// Lines 48-56: ImportResultRowDTO +interface ImportResultRowDTO { + id: string; + raceId: string; + driverId: string; + position: number; + fastestLap: number; + incidents: number; + startPosition: number; +} +// Target: apps/website/lib/dtos/ImportResultRowDto.ts +``` + +**Action Items**: +1. Scan all files in `apps/website/app/` for inline type/interface definitions +2. Create extraction plan for each +3. Document dependencies and usage + +## Phase 2: Directory Structure Creation + +### Step 2.1: Create Base Directories + +```bash +# Execute these commands in order: +mkdir -p apps/website/lib/dtos +mkdir -p apps/website/lib/view-models +mkdir -p apps/website/lib/presenters +mkdir -p apps/website/lib/services +mkdir -p apps/website/lib/api/base +mkdir -p apps/website/lib/api/leagues +mkdir -p apps/website/lib/api/drivers +mkdir -p apps/website/lib/api/teams +mkdir -p apps/website/lib/api/races +mkdir -p apps/website/lib/api/sponsors +mkdir -p apps/website/lib/api/media +mkdir -p apps/website/lib/api/analytics +mkdir -p apps/website/lib/api/auth +mkdir -p apps/website/lib/api/payments +``` + +### Step 2.2: Create Placeholder Index Files + +```typescript +// apps/website/lib/dtos/index.ts +// This file will be populated in Phase 3 +export {}; + +// apps/website/lib/view-models/index.ts +// This file will be populated in Phase 4 +export {}; + +// apps/website/lib/presenters/index.ts +// This file will be populated in Phase 6 +export {}; + +// apps/website/lib/services/index.ts +// This file will be populated in Phase 7 +export {}; + +// apps/website/lib/api/index.ts +// This file will be populated in Phase 5 +export {}; +``` + +## Phase 3: Extract DTOs (60+ Files) + +### Step 3.1: Common DTOs + +#### apps/website/lib/dtos/DriverDto.ts +```typescript +/** + * Driver transport object + * Represents a driver as received from the API + */ +export interface DriverDto { + id: string; + name: string; + avatarUrl?: string; + iracingId?: string; + rating?: number; +} +``` + +#### apps/website/lib/dtos/PenaltyTypeDto.ts +```typescript +/** + * Penalty type enumeration + * Defines all possible penalty types in the system + */ +export type PenaltyTypeDto = + | 'time_penalty' + | 'grid_penalty' + | 'points_deduction' + | 'disqualification' + | 'warning' + | 'license_points'; +``` + +#### apps/website/lib/dtos/PenaltyDataDto.ts +```typescript +import type { PenaltyTypeDto } from './PenaltyTypeDto'; + +/** + * Penalty data structure + * Used when creating or updating penalties + */ +export interface PenaltyDataDto { + driverId: string; + type: PenaltyTypeDto; + value?: number; +} +``` + +### Step 3.2: League DTOs + +#### apps/website/lib/dtos/LeagueStatsDto.ts +```typescript +/** + * League statistics transport object + */ +export interface LeagueStatsDto { + totalLeagues: number; +} +``` + +#### apps/website/lib/dtos/LeagueSummaryDto.ts +```typescript +/** + * League summary transport object + * Contains basic league information for list views + */ +export interface LeagueSummaryDto { + id: string; + name: string; + description?: string; + logoUrl?: string; + coverImage?: string; + memberCount: number; + maxMembers: number; + isPublic: boolean; + ownerId: string; + ownerName?: string; + scoringType?: string; + status?: string; +} +``` + +#### apps/website/lib/dtos/CreateLeagueInputDto.ts +```typescript +/** + * Create league input + * Data required to create a new league + */ +export interface CreateLeagueInputDto { + name: string; + description?: string; + isPublic: boolean; + maxMembers: number; + ownerId: string; +} +``` + +#### apps/website/lib/dtos/CreateLeagueOutputDto.ts +```typescript +/** + * Create league output + * Response from league creation + */ +export interface CreateLeagueOutputDto { + leagueId: string; + success: boolean; +} +``` + +### Step 3.3: Race DTOs + +#### apps/website/lib/dtos/RaceStatsDto.ts +```typescript +/** + * Race statistics transport object + */ +export interface RaceStatsDto { + totalRaces: number; +} +``` + +#### apps/website/lib/dtos/RaceResultRowDto.ts +```typescript +/** + * Individual race result transport object + * Pure data, no methods + */ +export interface RaceResultRowDto { + id: string; + raceId: string; + driverId: string; + position: number; + fastestLap: number; + incidents: number; + startPosition: number; +} +``` + +#### apps/website/lib/dtos/RaceResultsDetailDto.ts +```typescript +import type { RaceResultRowDto } from './RaceResultRowDto'; + +/** + * Complete race results transport object + */ +export interface RaceResultsDetailDto { + raceId: string; + track: string; + results: RaceResultRowDto[]; +} +``` + +#### apps/website/lib/dtos/RegisterForRaceInputDto.ts +```typescript +/** + * Register for race input + */ +export interface RegisterForRaceInputDto { + leagueId: string; + driverId: string; +} +``` + +### Step 3.4: Driver DTOs + +#### apps/website/lib/dtos/DriverStatsDto.ts +```typescript +/** + * Driver statistics transport object + */ +export interface DriverStatsDto { + totalDrivers: number; +} +``` + +#### apps/website/lib/dtos/CompleteOnboardingInputDto.ts +```typescript +/** + * Complete onboarding input + */ +export interface CompleteOnboardingInputDto { + iracingId: string; + displayName: string; +} +``` + +#### apps/website/lib/dtos/CompleteOnboardingOutputDto.ts +```typescript +/** + * Complete onboarding output + */ +export interface CompleteOnboardingOutputDto { + driverId: string; + success: boolean; +} +``` + +### Step 3.5: Barrel Export (apps/website/lib/dtos/index.ts) + +```typescript +// Common +export * from './DriverDto'; +export * from './PenaltyTypeDto'; +export * from './PenaltyDataDto'; + +// League +export * from './LeagueStatsDto'; +export * from './LeagueSummaryDto'; +export * from './CreateLeagueInputDto'; +export * from './CreateLeagueOutputDto'; +// ... add all league DTOs + +// Race +export * from './RaceStatsDto'; +export * from './RaceResultRowDto'; +export * from './RaceResultsDetailDto'; +export * from './RegisterForRaceInputDto'; +// ... add all race DTOs + +// Driver +export * from './DriverStatsDto'; +export * from './CompleteOnboardingInputDto'; +export * from './CompleteOnboardingOutputDto'; +// ... add all driver DTOs + +// Team, Sponsor, Media, Analytics, Auth, Payments... +// Continue for all domains +``` + +**Total DTO Files**: ~60-70 files + +## Phase 4: Create View Models (30+ Files) + +### Step 4.1: Understanding ViewModel Pattern + +**What ViewModels Add**: +1. UI-specific derived fields +2. Computed properties +3. Display formatting +4. UI state indicators +5. Grouped/sorted data for rendering + +**What ViewModels DO NOT Have**: +1. Business logic +2. Validation rules +3. API calls +4. Side effects + +### Step 4.2: League ViewModels + +#### apps/website/lib/view-models/LeagueSummaryViewModel.ts +```typescript +import type { LeagueSummaryDto } from '../dtos'; + +/** + * League summary view model + * Extends DTO with UI-specific computed properties + */ +export interface LeagueSummaryViewModel extends LeagueSummaryDto { + // Formatted capacity display (e.g., "25/50") + formattedCapacity: string; + + // Percentage for progress bars (0-100) + capacityBarPercent: number; + + // Button label based on state + joinButtonLabel: string; + + // Quick check flags + isFull: boolean; + isJoinable: boolean; + + // Color indicator for UI + memberProgressColor: 'green' | 'yellow' | 'red'; + + // Badge type for status display + statusBadgeVariant: 'success' | 'warning' | 'info'; +} +``` + +#### apps/website/lib/view-models/LeagueStandingsViewModel.ts +```typescript +import type { DriverDto } from '../dtos'; + +/** + * Single standings entry view model + */ +export interface StandingEntryViewModel { + // From DTO + driverId: string; + driver?: DriverDto; + position: number; + points: number; + wins: number; + podiums: number; + races: number; + + // UI additions + positionBadge: 'gold' | 'silver' | 'bronze' | 'default'; + pointsGapToLeader: number; + pointsGapToNext: number; + isCurrentUser: boolean; + trend: 'up' | 'down' | 'stable'; + trendArrow: '↑' | '↓' | '→'; +} + +/** + * Complete standings view model + */ +export interface LeagueStandingsViewModel { + standings: StandingEntryViewModel[]; + totalEntries: number; + currentUserPosition?: number; +} +``` + +### Step 4.3: Race ViewModels + +#### apps/website/lib/view-models/RaceResultViewModel.ts +```typescript +/** + * Individual race result view model + * Extends result data with UI-specific fields + */ +export interface RaceResultViewModel { + // From DTO + driverId: string; + driverName: string; + avatarUrl: string; + position: number; + startPosition: number; + incidents: number; + fastestLap: number; + + // Computed UI fields + positionChange: number; + positionChangeDisplay: string; // "+3", "-2", "0" + positionChangeColor: 'green' | 'red' | 'gray'; + + // Status flags + isPodium: boolean; + isWinner: boolean; + isClean: boolean; + hasFastestLap: boolean; + + // Display helpers + positionBadge: 'gold' | 'silver' | 'bronze' | 'default'; + incidentsBadgeColor: 'green' | 'yellow' | 'red'; + lapTimeFormatted: string; // "1:23.456" +} +``` + +#### apps/website/lib/view-models/RaceResultsDetailViewModel.ts +```typescript +import type { RaceResultViewModel } from './RaceResultViewModel'; + +/** + * Complete race results view model + * Includes statistics and sorted views + */ +export interface RaceResultsDetailViewModel { + raceId: string; + track: string; + results: RaceResultViewModel[]; + + // Statistics for display + stats: { + totalFinishers: number; + podiumFinishers: number; + cleanRaces: number; + averageIncidents: number; + fastestLapTime: number; + fastestLapDriver: string; + }; + + // Sorted views for different displays + resultsByPosition: RaceResultViewModel[]; + resultsByFastestLap: RaceResultViewModel[]; + cleanDrivers: RaceResultViewModel[]; + + // User-specific data + currentUserResult?: RaceResultViewModel; + currentUserHighlighted: boolean; +} +``` + +### Step 4.4: Driver ViewModels + +#### apps/website/lib/view-models/DriverLeaderboardViewModel.ts +```typescript +/** + * Single leaderboard entry view model + */ +export interface DriverLeaderboardItemViewModel { + // From DTO + id: string; + name: string; + avatarUrl?: string; + rating: number; + wins: number; + races: number; + + // UI additions + skillLevel: string; + skillLevelColor: string; + skillLevelIcon: string; + winRate: number; + winRateFormatted: string; // "45.2%" + ratingTrend: 'up' | 'down' | 'stable'; + ratingChangeIndicator: string; // "+50", "-20" + position: number; + positionBadge: 'gold' | 'silver' | 'bronze' | 'default'; +} + +/** + * Complete leaderboard view model + */ +export interface DriversLeaderboardViewModel { + drivers: DriverLeaderboardItemViewModel[]; + totalDrivers: number; + currentPage: number; + pageSize: number; + hasMore: boolean; +} +``` + +### Step 4.5: Barrel Export (apps/website/lib/view-models/index.ts) + +```typescript +// League +export * from './LeagueSummaryViewModel'; +export * from './LeagueStandingsViewModel'; +export * from './LeagueMemberViewModel'; +// ... all league ViewModels + +// Race +export * from './RaceResultViewModel'; +export * from './RaceResultsDetailViewModel'; +export * from './RaceListItemViewModel'; +// ... all race ViewModels + +// Driver +export * from './DriverLeaderboardViewModel'; +export * from './DriverRegistrationStatusViewModel'; +// ... all driver ViewModels + +// Team, etc. +``` + +**Total ViewModel Files**: ~30-40 files + +## Phase 5: API Client Refactor (10+ Files) + +### Step 5.1: Base API Client + +#### apps/website/lib/api/base/BaseApiClient.ts +```typescript +/** + * Base HTTP client for all API communication + * Provides common request/response handling + */ +export class BaseApiClient { + private baseUrl: string; + + constructor(baseUrl: string) { + this.baseUrl = baseUrl; + } + + /** + * Generic request handler + * @param method HTTP method + * @param path API path + * @param data Request body (optional) + * @returns Response data + */ + protected async request( + method: string, + path: string, + data?: object + ): Promise { + const headers: HeadersInit = { + 'Content-Type': 'application/json', + }; + + const config: RequestInit = { + method, + headers, + credentials: 'include', // Include cookies for auth + }; + + if (data) { + config.body = JSON.stringify(data); + } + + const response = await fetch(`${this.baseUrl}${path}`, config); + + if (!response.ok) { + let errorData: { message?: string } = { message: response.statusText }; + try { + errorData = await response.json(); + } catch { + // Keep default error message + } + throw new Error( + errorData.message || `API request failed with status ${response.status}` + ); + } + + const text = await response.text(); + if (!text) { + return null as T; + } + return JSON.parse(text) as T; + } + + protected get(path: string): Promise { + return this.request('GET', path); + } + + protected post(path: string, data: object): Promise { + return this.request('POST', path, data); + } + + protected put(path: string, data: object): Promise { + return this.request('PUT', path, data); + } + + protected delete(path: string): Promise { + return this.request('DELETE', path); + } + + protected patch(path: string, data: object): Promise { + return this.request('PATCH', path, data); + } +} +``` + +### Step 5.2: Leagues API Client + +#### apps/website/lib/api/leagues/LeaguesApiClient.ts +```typescript +import { BaseApiClient } from '../base/BaseApiClient'; +import type { + LeagueSummaryDto, + LeagueStatsDto, + LeagueStandingsDto, + LeagueScheduleDto, + LeagueMembershipsDto, + CreateLeagueInputDto, + CreateLeagueOutputDto, +} from '../../dtos'; + +/** + * Leagues API client + * Handles all league-related HTTP operations + * Returns DTOs only - no UI transformation + */ +export class LeaguesApiClient extends BaseApiClient { + /** + * Get all leagues with capacity information + * @returns List of leagues with member counts + */ + async getAllWithCapacity(): Promise<{ leagues: LeagueSummaryDto[] }> { + return this.get<{ leagues: LeagueSummaryDto[] }>( + '/leagues/all-with-capacity' + ); + } + + /** + * Get total number of leagues + * @returns League statistics + */ + async getTotal(): Promise { + return this.get('/leagues/total-leagues'); + } + + /** + * Get league standings + * @param leagueId League identifier + * @returns Current standings + */ + async getStandings(leagueId: string): Promise { + return this.get(`/leagues/${leagueId}/standings`); + } + + /** + * Get league schedule + * @param leagueId League identifier + * @returns Scheduled races + */ + async getSchedule(leagueId: string): Promise { + return this.get(`/leagues/${leagueId}/schedule`); + } + + /** + * Get league memberships + * @param leagueId League identifier + * @returns Current members + */ + async getMemberships(leagueId: string): Promise { + return this.get(`/leagues/${leagueId}/memberships`); + } + + /** + * Create a new league + * @param input League creation data + * @returns Created league info + */ + async create(input: CreateLeagueInputDto): Promise { + return this.post('/leagues', input); + } + + /** + * Remove a member from league + * @param leagueId League identifier + * @param performerDriverId Driver performing the action + * @param targetDriverId Driver to remove + * @returns Success status + */ + async removeMember( + leagueId: string, + performerDriverId: string, + targetDriverId: string + ): Promise<{ success: boolean }> { + return this.patch<{ success: boolean }>( + `/leagues/${leagueId}/members/${targetDriverId}/remove`, + { performerDriverId } + ); + } +} +``` + +### Step 5.3: Races API Client + +#### apps/website/lib/api/races/RacesApiClient.ts +```typescript +import { BaseApiClient } from '../base/BaseApiClient'; +import type { + RaceStatsDto, + RacesPageDataDto, + RaceDetailDto, + RaceResultsDetailDto, + RaceWithSOFDto, + RegisterForRaceInputDto, + ImportRaceResultsInputDto, + ImportRaceResultsSummaryDto, +} from '../../dtos'; + +/** + * Races API client + * Handles all race-related HTTP operations + */ +export class RacesApiClient extends BaseApiClient { + /** + * Get total number of races + */ + async getTotal(): Promise { + return this.get('/races/total-races'); + } + + /** + * Get races page data + */ + async getPageData(): Promise { + return this.get('/races/page-data'); + } + + /** + * Get race detail + * @param raceId Race identifier + * @param driverId Driver identifier for personalization + */ + async getDetail(raceId: string, driverId: string): Promise { + return this.get(`/races/${raceId}?driverId=${driverId}`); + } + + /** + * Get race results detail + * @param raceId Race identifier + */ + async getResultsDetail(raceId: string): Promise { + return this.get(`/races/${raceId}/results`); + } + + /** + * Get race with strength of field + * @param raceId Race identifier + */ + async getWithSOF(raceId: string): Promise { + return this.get(`/races/${raceId}/sof`); + } + + /** + * Register for race + * @param raceId Race identifier + * @param input Registration data + */ + async register(raceId: string, input: RegisterForRaceInputDto): Promise { + return this.post(`/races/${raceId}/register`, input); + } + + /** + * Import race results + * @param raceId Race identifier + * @param input Results file content + */ + async importResults( + raceId: string, + input: ImportRaceResultsInputDto + ): Promise { + return this.post( + `/races/${raceId}/import-results`, + input + ); + } +} +``` + +### Step 5.4: Main API Client + +#### apps/website/lib/api/index.ts +```typescript +import { LeaguesApiClient } from './leagues/LeaguesApiClient'; +import { RacesApiClient } from './races/RacesApiClient'; +import { DriversApiClient } from './drivers/DriversApiClient'; +import { TeamsApiClient } from './teams/TeamsApiClient'; +import { SponsorsApiClient } from './sponsors/SponsorsApiClient'; +import { MediaApiClient } from './media/MediaApiClient'; +import { AnalyticsApiClient } from './analytics/AnalyticsApiClient'; +import { AuthApiClient } from './auth/AuthApiClient'; +import { PaymentsApiClient } from './payments/PaymentsApiClient'; + +/** + * Main API client with domain-specific namespaces + * Single point of access for all HTTP operations + */ +export class ApiClient { + public readonly leagues: LeaguesApiClient; + public readonly races: RacesApiClient; + public readonly drivers: DriversApiClient; + public readonly teams: TeamsApiClient; + public readonly sponsors: SponsorsApiClient; + public readonly media: MediaApiClient; + public readonly analytics: AnalyticsApiClient; + public readonly auth: AuthApiClient; + public readonly payments: PaymentsApiClient; + + constructor(baseUrl: string) { + this.leagues = new LeaguesApiClient(baseUrl); + this.races = new RacesApiClient(baseUrl); + this.drivers = new DriversApiClient(baseUrl); + this.teams = new TeamsApiClient(baseUrl); + this.sponsors = new SponsorsApiClient(baseUrl); + this.media = new MediaApiClient(baseUrl); + this.analytics = new AnalyticsApiClient(baseUrl); + this.auth = new AuthApiClient(baseUrl); + this.payments = new PaymentsApiClient(baseUrl); + } +} + +// Singleton instance +const API_BASE_URL = process.env.NEXT_PUBLIC_API_BASE_URL || 'http://localhost:3001'; +export const api = new ApiClient(API_BASE_URL); + +// Default export for convenience +export default api; +``` + +### Step 5.5: Legacy apiClient.ts Replacement + +#### apps/website/lib/apiClient.ts +```typescript +/** + * Legacy API client re-export + * Maintained for backward compatibility during migration + * TODO: Remove this file once all imports are updated + */ +export { api as apiClient, api as default } from './api'; +export type * from './dtos'; +export type * from './view-models'; +``` + +**Total API Files**: ~12 files (1 base + 9 domain + 1 main + 1 legacy) + +## Phase 6: Create Presenters (20+ Files) + +### Step 6.1: Understanding Presenter Pattern + +**Presenter Responsibilities**: +1. Transform DTO → ViewModel +2. Compute derived fields +3. Format data for display +4. Apply UI-specific logic + +**Presenter Rules**: +1. Pure functions (no side effects) +2. Deterministic (same input = same output) +3. No API calls +4. No state mutation +5. Testable in isolation + +### Step 6.2: League Presenters + +#### apps/website/lib/presenters/leagues/LeagueSummaryPresenter.ts +```typescript +import type { LeagueSummaryDto } from '../../dtos'; +import type { LeagueSummaryViewModel } from '../../view-models'; + +/** + * League summary presenter + * Transforms league DTO into UI-ready view model + */ +export const presentLeagueSummary = ( + dto: LeagueSummaryDto +): LeagueSummaryViewModel => { + const capacityPercent = (dto.memberCount / dto.maxMembers) * 100; + + return { + ...dto, + formattedCapacity: `${dto.memberCount}/${dto.maxMembers}`, + capacityBarPercent: Math.min(capacityPercent, 100), + joinButtonLabel: getJoinButtonLabel(dto), + isFull: dto.memberCount >= dto.maxMembers, + isJoinable: dto.isPublic && dto.memberCount < dto.maxMembers, + memberProgressColor: getMemberProgressColor(capacityPercent), + statusBadgeVariant: getStatusBadgeVariant(dto.status), + }; +}; + +/** + * Determine join button label based on league state + */ +function getJoinButtonLabel(dto: LeagueSummaryDto): string { + if (dto.memberCount >= dto.maxMembers) return 'Full'; + if (!dto.isPublic) return 'Private'; + return 'Join League'; +} + +/** + * Determine progress bar color based on capacity + */ +function getMemberProgressColor(percent: number): 'green' | 'yellow' | 'red' { + if (percent < 70) return 'green'; + if (percent < 90) return 'yellow'; + return 'red'; +} + +/** + * Determine status badge variant + */ +function getStatusBadgeVariant( + status?: string +): 'success' | 'warning' | 'info' { + if (!status) return 'info'; + if (status === 'active') return 'success'; + if (status === 'pending') return 'warning'; + return 'info'; +} + +/** + * Batch presenter for league lists + */ +export const presentLeagueSummaries = ( + dtos: LeagueSummaryDto[] +): LeagueSummaryViewModel[] => { + return dtos.map(presentLeagueSummary); +}; +``` + +#### apps/website/lib/presenters/leagues/LeagueStandingsPresenter.ts +```typescript +import type { StandingEntryDto, DriverDto } from '../../dtos'; +import type { + StandingEntryViewModel, + LeagueStandingsViewModel, +} from '../../view-models'; + +/** + * Single standings entry presenter + */ +export const presentStandingEntry = ( + dto: StandingEntryDto, + leaderPoints: number, + previousPoints: number, + isCurrentUser: boolean +): StandingEntryViewModel => { + return { + ...dto, + positionBadge: getPositionBadge(dto.position), + pointsGapToLeader: leaderPoints - dto.points, + pointsGapToNext: previousPoints - dto.points, + isCurrentUser, + trend: getTrend(dto.position), // Would need historical data + trendArrow: getTrendArrow(dto.position), + }; +}; + +/** + * Complete standings presenter + */ +export const presentLeagueStandings = ( + standings: StandingEntryDto[], + currentUserId?: string +): LeagueStandingsViewModel => { + const sorted = [...standings].sort((a, b) => a.position - b.position); + const leaderPoints = sorted[0]?.points ?? 0; + + const viewModels = sorted.map((entry, index) => { + const previousPoints = index > 0 ? sorted[index - 1].points : leaderPoints; + const isCurrentUser = entry.driverId === currentUserId; + return presentStandingEntry(entry, leaderPoints, previousPoints, isCurrentUser); + }); + + return { + standings: viewModels, + totalEntries: standings.length, + currentUserPosition: viewModels.find((s) => s.isCurrentUser)?.position, + }; +}; + +function getPositionBadge( + position: number +): 'gold' | 'silver' | 'bronze' | 'default' { + if (position === 1) return 'gold'; + if (position === 2) return 'silver'; + if (position === 3) return 'bronze'; + return 'default'; +} + +function getTrend(position: number): 'up' | 'down' | 'stable' { + // Placeholder - would need historical data + return 'stable'; +} + +function getTrendArrow(position: number): '↑' | '↓' | '→' { + const trend = getTrend(position); + if (trend === 'up') return '↑'; + if (trend === 'down') return '↓'; + return '→'; +} +``` + +### Step 6.3: Race Presenters + +#### apps/website/lib/presenters/races/RaceResultsPresenter.ts +```typescript +import type { RaceResultRowDto, RaceResultsDetailDto } from '../../dtos'; +import type { + RaceResultViewModel, + RaceResultsDetailViewModel, +} from '../../view-models'; + +/** + * Single race result presenter + */ +export const presentRaceResult = ( + dto: RaceResultRowDto, + fastestLapTime: number, + isCurrentUser: boolean +): RaceResultViewModel => { + const positionChange = dto.position - dto.startPosition; + + return { + driverId: dto.driverId, + driverName: '', // Would be populated from driver data + avatarUrl: '', + position: dto.position, + startPosition: dto.startPosition, + incidents: dto.incidents, + fastestLap: dto.fastestLap, + + // Computed fields + positionChange, + positionChangeDisplay: formatPositionChange(positionChange), + positionChangeColor: getPositionChangeColor(positionChange), + + // Status flags + isPodium: dto.position <= 3, + isWinner: dto.position === 1, + isClean: dto.incidents === 0, + hasFastestLap: dto.fastestLap === fastestLapTime, + + // Display helpers + positionBadge: getPositionBadge(dto.position), + incidentsBadgeColor: getIncidentsBadgeColor(dto.incidents), + lapTimeFormatted: formatLapTime(dto.fastestLap), + }; +}; + +/** + * Complete race results presenter + */ +export const presentRaceResultsDetail = ( + dto: RaceResultsDetailDto, + currentUserId?: string +): RaceResultsDetailViewModel => { + const fastestLapTime = Math.min(...dto.results.map((r) => r.fastestLap)); + + const results = dto.results.map((r) => + presentRaceResult(r, fastestLapTime, r.driverId === currentUserId) + ); + + const sortedByPosition = [...results].sort((a, b) => a.position - b.position); + const sortedByFastestLap = [...results].sort((a, b) => a.fastestLap - b.fastestLap); + const cleanDrivers = results.filter((r) => r.isClean); + + const currentUserResult = results.find((r) => r.driverId === currentUserId); + + return { + raceId: dto.raceId, + track: dto.track, + results, + + stats: { + totalFinishers: results.length, + podiumFinishers: results.filter((r) => r.isPodium).length, + cleanRaces: cleanDrivers.length, + averageIncidents: + results.reduce((sum, r) => sum + r.incidents, 0) / results.length, + fastestLapTime, + fastestLapDriver: + sortedByFastestLap[0]?.driverName ?? 'Unknown', + }, + + resultsByPosition: sortedByPosition, + resultsByFastestLap: sortedByFastestLap, + cleanDrivers, + + currentUserResult, + currentUserHighlighted: !!currentUserResult, + }; +}; + +function formatPositionChange(change: number): string { + if (change > 0) return `+${change}`; + return change.toString(); +} + +function getPositionChangeColor( + change: number +): 'green' | 'red' | 'gray' { + if (change > 0) return 'green'; + if (change < 0) return 'red'; + return 'gray'; +} + +function getPositionBadge( + position: number +): 'gold' | 'silver' | 'bronze' | 'default' { + if (position === 1) return 'gold'; + if (position === 2) return 'silver'; + if (position === 3) return 'bronze'; + return 'default'; +} + +function getIncidentsBadgeColor( + incidents: number +): 'green' | 'yellow' | 'red' { + if (incidents === 0) return 'green'; + if (incidents <= 4) return 'yellow'; + return 'red'; +} + +function formatLapTime(milliseconds: number): string { + const minutes = Math.floor(milliseconds / 60000); + const seconds = Math.floor((milliseconds % 60000) / 1000); + const ms = milliseconds % 1000; + return `${minutes}:${seconds.toString().padStart(2, '0')}.${ms.toString().padStart(3, '0')}`; +} +``` + +### Step 6.4: Driver Presenters + +#### apps/website/lib/presenters/drivers/DriverLeaderboardPresenter.ts +```typescript +import type { DriverLeaderboardItemDto } from '../../dtos'; +import type { + DriverLeaderboardItemViewModel, + DriversLeaderboardViewModel, +} from '../../view-models'; + +/** + * Single leaderboard entry presenter + */ +export const presentDriverLeaderboardItem = ( + dto: DriverLeaderboardItemDto, + position: number +): DriverLeaderboardItemViewModel => { + const winRate = dto.races > 0 ? (dto.wins / dto.races) * 100 : 0; + + return { + ...dto, + skillLevel: getSkillLevel(dto.rating), + skillLevelColor: getSkillLevelColor(dto.rating), + skillLevelIcon: getSkillLevelIcon(dto.rating), + winRate, + winRateFormatted: `${winRate.toFixed(1)}%`, + ratingTrend: 'stable', // Would need historical data + ratingChangeIndicator: '+0', // Would need historical data + position, + positionBadge: getPositionBadge(position), + }; +}; + +/** + * Complete leaderboard presenter + */ +export const presentDriversLeaderboard = ( + dtos: DriverLeaderboardItemDto[], + page: number = 1, + pageSize: number = 50 +): DriversLeaderboardViewModel => { + const sorted = [...dtos].sort((a, b) => b.rating - a.rating); + const drivers = sorted.map((dto, index) => + presentDriverLeaderboardItem(dto, index + 1) + ); + + return { + drivers, + totalDrivers: dtos.length, + currentPage: page, + pageSize, + hasMore: false, // Would be based on actual pagination + }; +}; + +function getSkillLevel(rating: number): string { + if (rating >= 2000) return 'Pro'; + if (rating >= 1500) return 'Advanced'; + if (rating >= 1000) return 'Intermediate'; + return 'Rookie'; +} + +function getSkillLevelColor(rating: number): string { + if (rating >= 2000) return 'purple'; + if (rating >= 1500) return 'blue'; + if (rating >= 1000) return 'green'; + return 'gray'; +} + +function getSkillLevelIcon(rating: number): string { + if (rating >= 2000) return '⭐'; + if (rating >= 1500) return '🔷'; + if (rating >= 1000) return '🟢'; + return '⚪'; +} + +function getPositionBadge( + position: number +): 'gold' | 'silver' | 'bronze' | 'default' { + if (position === 1) return 'gold'; + if (position === 2) return 'silver'; + if (position === 3) return 'bronze'; + return 'default'; +} +``` + +### Step 6.5: Barrel Export (apps/website/lib/presenters/index.ts) + +```typescript +// Leagues +export * from './leagues/LeagueSummaryPresenter'; +export * from './leagues/LeagueStandingsPresenter'; + +// Races +export * from './races/RaceResultsPresenter'; + +// Drivers +export * from './drivers/DriverLeaderboardPresenter'; + +// Teams, etc. +``` + +**Total Presenter Files**: ~20-25 files + +## Phase 7: Create Services (15+ Files) + +### Step 7.1: Understanding Service Pattern + +**Service Responsibilities**: +1. Orchestrate API calls +2. Call presenters for transformation +3. Combine multiple data sources +4. Return ViewModels to UI +5. Handle errors appropriately + +**Service Rules**: +1. May call multiple API endpoints +2. Must use presenters for DTO→ViewModel +3. Return ViewModels only (never DTOs) +4. May have async operations +5. May throw/handle errors + +### Step 7.2: Race Services + +#### apps/website/lib/services/races/RaceResultsService.ts +```typescript +import { api } from '../../api'; +import { presentRaceResultsDetail } from '../../presenters/races/RaceResultsPresenter'; +import type { RaceResultsDetailViewModel } from '../../view-models'; + +/** + * Get race results with full view model + * @param raceId Race identifier + * @param currentUserId Optional current user for highlighting + * @returns Complete race results view model + */ +export async function getRaceResults( + raceId: string, + currentUserId?: string +): Promise { + const dto = await api.races.getResultsDetail(raceId); + return presentRaceResultsDetail(dto, currentUserId); +} + +/** + * Get race strength of field + * @param raceId Race identifier + * @returns SOF value + */ +export async function getRaceSOF(raceId: string): Promise { + const dto = await api.races.getWithSOF(raceId); + return dto.strengthOfField ?? 0; +} + +/** + * Import race results and refresh + * @param raceId Race identifier + * @param fileContent Results file content + * @returns Import summary + */ +export async function importRaceResults( + raceId: string, + fileContent: string +): Promise<{ success: boolean; message: string }> { + try { + const summary = await api.races.importResults(raceId, { + resultsFileContent: fileContent, + }); + + return { + success: summary.success, + message: `Imported ${summary.resultsRecorded} results for ${summary.driversProcessed} drivers`, + }; + } catch (error) { + return { + success: false, + message: error instanceof Error ? error.message : 'Import failed', + }; + } +} +``` + +### Step 7.3: League Services + +#### apps/website/lib/services/leagues/LeagueService.ts +```typescript +import { api } from '../../api'; +import { + presentLeagueSummaries, + presentLeagueStandings, +} from '../../presenters'; +import type { + LeagueSummaryViewModel, + LeagueStandingsViewModel, +} from '../../view-models'; +import type { CreateLeagueInputDto } from '../../dtos'; + +/** + * Get all leagues with UI-ready data + * @returns List of league view models + */ +export async function getAllLeagues(): Promise { + const dto = await api.leagues.getAllWithCapacity(); + return presentLeagueSummaries(dto.leagues); +} + +/** + * Get league standings with computed data + * @param leagueId League identifier + * @param currentUserId Optional current user for highlighting + * @returns Standings view model + */ +export async function getLeagueStandings( + leagueId: string, + currentUserId?: string +): Promise { + const dto = await api.leagues.getStandings(leagueId); + return presentLeagueStandings(dto.standings, currentUserId); +} + +/** + * Create a new league + * @param input League creation data + * @returns Created league ID + */ +export async function createLeague( + input: Omit, + ownerId: string +): Promise { + const result = await api.leagues.create({ ...input, ownerId }); + + if (!result.success) { + throw new Error('Failed to create league'); + } + + return result.leagueId; +} + +/** + * Get complete league admin view + * Combines multiple API calls + */ +export async function getLeagueAdminView( + leagueId: string, + performerId: string +) { + const [config, members, standings, schedule] = await Promise.all([ + api.leagues.getConfig(leagueId), + api.leagues.getMemberships(leagueId), + api.leagues.getStandings(leagueId), + api.leagues.getSchedule(leagueId), + ]); + + return { + config, + members: members.members, + standings: presentLeagueStandings(standings.standings, performerId), + schedule: schedule.races, + }; +} +``` + +### Step 7.4: Driver Services + +#### apps/website/lib/services/drivers/DriverService.ts +```typescript +import { api } from '../../api'; +import { presentDriversLeaderboard } from '../../presenters'; +import type { DriversLeaderboardViewModel } from '../../view-models'; +import type { CompleteOnboardingInputDto } from '../../dtos'; + +/** + * Get driver leaderboard with computed rankings + * @returns Leaderboard view model + */ +export async function getDriversLeaderboard(): Promise { + const dto = await api.drivers.getLeaderboard(); + return presentDriversLeaderboard(dto.drivers); +} + +/** + * Complete driver onboarding + * @param iracingId iRacing ID + * @param displayName Display name + * @returns New driver ID + */ +export async function completeDriverOnboarding( + iracingId: string, + displayName: string +): Promise { + const input: CompleteOnboardingInputDto = { iracingId, displayName }; + const result = await api.drivers.completeOnboarding(input); + + if (!result.success) { + throw new Error('Failed to complete onboarding'); + } + + return result.driverId; +} + +/** + * Get current driver info + * @returns Current driver or null + */ +export async function getCurrentDriver() { + return api.drivers.getCurrent(); +} +``` + +### Step 7.5: Barrel Export (apps/website/lib/services/index.ts) + +```typescript +// Races +export * from './races/RaceResultsService'; + +// Leagues +export * from './leagues/LeagueService'; + +// Drivers +export * from './drivers/DriverService'; + +// Teams, etc. +``` + +**Total Service Files**: ~15-20 files + +## Phase 8: Update Pages (All app/ Pages) + +### Step 8.1: Update races/[id]/results/page.tsx + +**Before (lines 1-300)**: +```typescript +'use client'; + +import { useState, useEffect } from 'react'; +import { apiClient } from '@/lib/apiClient'; +import type { RaceResultsDetailViewModel } from '@/lib/apiClient'; + +// Inline DTOs (DELETE THESE) +type PenaltyTypeDTO = 'time_penalty' | 'grid_penalty' | ...; +interface PenaltyData { ... } +interface RaceResultRowDTO { ... } + +export default function RaceResultsPage() { + const [raceData, setRaceData] = useState(null); + + const loadData = async () => { + const data = await apiClient.races.getResultsDetail(raceId); + setRaceData(data); + }; + + // ... +} +``` + +**After**: +```typescript +'use client'; + +import { useState, useEffect } from 'react'; +import { getRaceResults, getRaceSOF } from '@/lib/services/races/RaceResultsService'; +import type { RaceResultsDetailViewModel } from '@/lib/view-models'; + +// No inline DTOs! + +export default function RaceResultsPage() { + const [raceData, setRaceData] = useState(null); + const [raceSOF, setRaceSOF] = useState(null); + + const loadData = async () => { + try { + // Use service, not apiClient + const data = await getRaceResults(raceId, currentDriverId); + setRaceData(data); + + const sof = await getRaceSOF(raceId); + setRaceSOF(sof); + } catch (error) { + setError(error instanceof Error ? error.message : 'Failed to load data'); + } + }; + + // Component now uses ViewModel fields: + // - raceData.stats.totalFinishers + // - raceData.stats.podiumFinishers + // - raceData.currentUserResult?.positionBadge + // - raceData.resultsByPosition +} +``` + +### Step 8.2: Update leagues/[id]/standings/page.tsx + +```typescript +'use client'; + +import { useState, useEffect } from 'react'; +import { getLeagueStandings } from '@/lib/services/leagues/LeagueService'; +import type { LeagueStandingsViewModel } from '@/lib/view-models'; + +export default function LeagueStandingsPage() { + const [standings, setStandings] = useState(null); + + useEffect(() => { + async function loadStandings() { + const data = await getLeagueStandings(leagueId, currentUserId); + setStandings(data); + } + loadStandings(); + }, [leagueId, currentUserId]); + + return ( +
+ {standings?.standings.map((entry) => ( +
+ {entry.positionBadge} + {entry.driver?.name} + {entry.points} + {entry.trendArrow} + {entry.pointsGapToLeader} behind +
+ ))} +
+ ); +} +``` + +### Step 8.3: Update drivers/leaderboard/page.tsx + +```typescript +'use client'; + +import { useState, useEffect } from 'react'; +import { getDriversLeaderboard } from '@/lib/services/drivers/DriverService'; +import type { DriversLeaderboardViewModel } from '@/lib/view-models'; + +export default function DriversLeaderboardPage() { + const [leaderboard, setLeaderboard] = useState(null); + + useEffect(() => { + async function loadLeaderboard() { + const data = await getDriversLeaderboard(); + setLeaderboard(data); + } + loadLeaderboard(); + }, []); + + return ( +
+ {leaderboard?.drivers.map((driver) => ( +
+ {driver.position} + {driver.positionBadge} + {driver.name} + + {driver.skillLevel} {driver.skillLevelIcon} + + {driver.rating} + {driver.winRateFormatted} +
+ ))} +
+ ); +} +``` + +### Step 8.4: Search & Replace Pattern + +```bash +# Find all apiClient direct imports +grep -r "from '@/lib/apiClient'" apps/website/app/ + +# Find all pages with inline type definitions +grep -r "^type \|^interface " apps/website/app/**/*.tsx + +# Replace pattern (manual review required): +# 1. Import from services, not apiClient +# 2. Import types from view-models, not dtos +# 3. Remove inline types +# 4. Use service functions +# 5. Use ViewModel fields in JSX +``` + +**Pages to Update** (estimated 15-20): +- races/page.tsx +- races/[id]/page.tsx +- races/[id]/results/page.tsx +- leagues/page.tsx +- leagues/[id]/page.tsx +- leagues/[id]/standings/page.tsx +- leagues/[id]/members/page.tsx +- drivers/leaderboard/page.tsx +- teams/page.tsx +- teams/[id]/page.tsx +- onboarding/page.tsx +- dashboard/page.tsx +- profile/settings/page.tsx + +## Phase 9: Barrels & Naming Enforcement + +### Step 9.1: Final Barrel Exports + +All index.ts files should follow this pattern: + +```typescript +// apps/website/lib/dtos/index.ts +// Export all DTOs alphabetically by domain + +// Common +export * from './DriverDto'; +export * from './PenaltyDataDto'; +export * from './PenaltyTypeDto'; + +// Analytics +export * from './RecordEngagementInputDto'; +export * from './RecordEngagementOutputDto'; +export * from './RecordPageViewInputDto'; +export * from './RecordPageViewOutputDto'; + +// Auth +export * from './LoginParamsDto'; +export * from './SessionDataDto'; +export * from './SignupParamsDto'; + +// (Continue for all domains...) +``` + +### Step 9.2: Naming Convention Audit + +**Checklist**: +- [ ] All DTO files end with `Dto.ts` +- [ ] All ViewModel files end with `ViewModel.ts` +- [ ] All Presenter files end with `Presenter.ts` +- [ ] All Service files end with `Service.ts` +- [ ] All files are PascalCase +- [ ] All exports match filename +- [ ] One export per file + +**Automated Check**: +```bash +# Find files not following naming convention +find apps/website/lib/dtos -type f ! -name "*Dto.ts" ! -name "index.ts" +find apps/website/lib/view-models -type f ! -name "*ViewModel.ts" ! -name "index.ts" +find apps/website/lib/presenters -type f ! -name "*Presenter.ts" ! -name "index.ts" +find apps/website/lib/services -type f ! -name "*Service.ts" ! -name "index.ts" +``` + +## Phase 10: Enforcement & Validation + +### Step 10.1: ESLint Rules + +#### .eslintrc.json additions +```json +{ + "rules": { + "no-restricted-imports": [ + "error", + { + "patterns": [ + { + "group": ["**/apiClient"], + "message": "Import from specific services instead of apiClient" + }, + { + "group": ["**/dtos"], + "message": "UI components should not import DTOs directly. Use ViewModels instead." + }, + { + "group": ["**/api/*"], + "message": "UI components should use services, not API clients directly" + } + ] + } + ] + } +} +``` + +### Step 10.2: TypeScript Path Mappings + +#### tsconfig.json additions +```json +{ + "compilerOptions": { + "paths": { + "@/lib/dtos": ["./apps/website/lib/dtos"], + "@/lib/view-models": ["./apps/website/lib/view-models"], + "@/lib/presenters": ["./apps/website/lib/presenters"], + "@/lib/services": ["./apps/website/lib/services"], + "@/lib/api": ["./apps/website/lib/api"] + } + } +} +``` + +### Step 10.3: DATA_FLOW.md Mermaid Diagram + +Add to DATA_FLOW.md: + +```markdown +## Architecture Diagram + +```mermaid +graph TD + UI[UI Components/Pages] --> Services[Services Layer] + Services --> API[API Clients] + Services --> Presenters[Presenters Layer] + API --> DTOs[DTOs Transport] + Presenters --> DTOs + Presenters --> ViewModels[ViewModels UI] + Services --> ViewModels + UI --> ViewModels + + style UI fill:#e1f5ff + style Services fill:#fff4e1 + style Presenters fill:#f0e1ff + style API fill:#e1ffe1 + style DTOs fill:#ffe1e1 + style ViewModels fill:#e1f5ff +``` + +**Dependency Rules**: +- ✅ UI → Services → (API + Presenters) → (DTOs + ViewModels) +- ❌ UI ↛ API +- ❌ UI ↛ DTOs +- ❌ Presenters ↛ API +- ❌ API ↛ ViewModels +``` + +### Step 10.4: Testing + +#### Unit Test Example: Presenter +```typescript +// apps/website/lib/presenters/races/RaceResultsPresenter.test.ts +import { presentRaceResult } from './RaceResultsPresenter'; +import type { RaceResultRowDto } from '../../dtos'; + +describe('presentRaceResult', () => { + it('should compute position change correctly', () => { + const dto: RaceResultRowDto = { + id: '1', + raceId: 'race-1', + driverId: 'driver-1', + position: 3, + startPosition: 8, + fastestLap: 90000, + incidents: 0, + }; + + const result = presentRaceResult(dto, 89000, false); + + expect(result.positionChange).toBe(-5); + expect(result.positionChangeDisplay).toBe('+5'); + expect(result.positionChangeColor).toBe('green'); + }); + + it('should identify podium finishes', () => { + const dto: RaceResultRowDto = { + id: '1', + raceId: 'race-1', + driverId: 'driver-1', + position: 2, + startPosition: 2, + fastestLap: 90000, + incidents: 0, + }; + + const result = presentRaceResult(dto, 89000, false); + + expect(result.isPodium).toBe(true); + expect(result.positionBadge).toBe('silver'); + }); +}); +``` + +#### Integration Test Example: Service +```typescript +// apps/website/lib/services/races/RaceResultsService.test.ts +import { getRaceResults } from './RaceResultsService'; +import { api } from '../../api'; + +jest.mock('../../api'); + +describe('getRaceResults', () => { + it('should return view model with computed fields', async () => { + const mockDto = { + raceId: 'race-1', + track: 'Spa', + results: [ + { + id: '1', + raceId: 'race-1', + driverId: 'driver-1', + position: 1, + startPosition: 3, + fastestLap: 89000, + incidents: 0, + }, + ], + }; + + (api.races.getResultsDetail as jest.Mock).mockResolvedValue(mockDto); + + const result = await getRaceResults('race-1'); + + expect(result.stats.totalFinishers).toBe(1); + expect(result.stats.podiumFinishers).toBe(1); + expect(result.resultsByPosition).toHaveLength(1); + expect(result.resultsByPosition[0].positionBadge).toBe('gold'); + }); +}); +``` + +### Step 10.5: Verification Checklist + +**Final Checklist**: +- [ ] All 60+ DTO files created +- [ ] All 30+ ViewModel files created +- [ ] All 10+ API client files created +- [ ] All 20+ Presenter files created +- [ ] All 15+ Service files created +- [ ] All pages updated to use services +- [ ] No inline DTOs in pages +- [ ] All barrel exports complete +- [ ] ESLint rules enforced +- [ ] TypeScript compiles +- [ ] All tests passing +- [ ] DATA_FLOW.md updated +- [ ] Documentation complete +- [ ] Original apiClient.ts marked deprecated + +**Build Verification**: +```bash +# Ensure clean build +npm run build + +# Run all tests +npm run test + +# ESLint check +npm run lint + +# Type check +npm run type-check +``` + +## Summary + +**Total Changes**: +- **Files Created**: ~150 +- **Files Modified**: ~20 pages +- **Files Deleted**: None (apiClient.ts kept for compatibility) +- **Lines of Code**: +8000, -1160 (apiClient.ts) +- **apiClient.ts Size Reduction**: 95% +- **Architecture Compliance**: 100% + +**Benefits**: +1. ✅ Strict layer separation +2. ✅ No inline DTOs +3. ✅ Reusable ViewModels +4. ✅ Testable presenters +5. ✅ Clear data flow +6. ✅ Maintainable structure +7. ✅ Type safety +8. ✅ Enforced conventions \ No newline at end of file diff --git a/plans/type-inventory.md b/plans/type-inventory.md new file mode 100644 index 000000000..5dc35cdc7 --- /dev/null +++ b/plans/type-inventory.md @@ -0,0 +1,144 @@ +# Type Inventory & Classification + +## Overview +This document inventories all types extracted from `apps/website/lib/apiClient.ts` (lines 13-634), classifies them according to clean architecture principles, and specifies their target locations. + +## Classification Rules +- **DTOs**: Transport objects from API, pure data, no methods +- **ViewModels**: UI-specific with computed properties, formatting, display logic +- **Input DTOs**: Request parameters for API calls +- **Output DTOs**: API response wrappers + +## Types from apiClient.ts + +| Type Name | Line Range | Classification | Target Location | Dependencies | Used By | +|-----------|------------|----------------|-----------------|--------------|---------| +| DriverDTO | 13-19 | DTO | apps/website/lib/dtos/DriverDto.ts | none | Various pages (driver profiles, leaderboards, etc.) | +| ProtestViewModel | 21-29 | ViewModel | apps/website/lib/viewModels/ProtestViewModel.ts | none | League stewarding pages | +| LeagueMemberViewModel | 31-36 | ViewModel | apps/website/lib/viewModels/LeagueMemberViewModel.ts | DriverDTO | League member lists | +| StandingEntryViewModel | 38-46 | ViewModel | apps/website/lib/viewModels/StandingEntryViewModel.ts | DriverDTO | League standings pages | +| ScheduledRaceViewModel | 48-54 | ViewModel | apps/website/lib/viewModels/ScheduledRaceViewModel.ts | none | League schedule pages | +| LeagueSummaryViewModel | 57-70 | ViewModel | apps/website/lib/viewModels/LeagueSummaryViewModel.ts | none | League lists, dashboards | +| AllLeaguesWithCapacityViewModel | 72-74 | ViewModel | apps/website/lib/viewModels/AllLeaguesWithCapacityViewModel.ts | LeagueSummaryViewModel | League discovery pages | +| LeagueStatsDto | 76-78 | DTO | apps/website/lib/dtos/LeagueStatsDto.ts | none | League stats displays | +| LeagueJoinRequestViewModel | 80-86 | ViewModel | apps/website/lib/viewModels/LeagueJoinRequestViewModel.ts | none | League admin join requests | +| LeagueAdminPermissionsViewModel | 88-95 | ViewModel | apps/website/lib/viewModels/LeagueAdminPermissionsViewModel.ts | none | League admin checks | +| LeagueOwnerSummaryViewModel | 97-102 | ViewModel | apps/website/lib/viewModels/LeagueOwnerSummaryViewModel.ts | none | League owner dashboards | +| LeagueConfigFormModelDto | 104-111 | DTO | apps/website/lib/dtos/LeagueConfigFormModelDto.ts | none | League configuration forms | +| LeagueAdminProtestsViewModel | 113-115 | ViewModel | apps/website/lib/viewModels/LeagueAdminProtestsViewModel.ts | ProtestViewModel | League protest admin | +| LeagueSeasonSummaryViewModel | 117-123 | ViewModel | apps/website/lib/viewModels/LeagueSeasonSummaryViewModel.ts | none | League season lists | +| LeagueMembershipsViewModel | 125-127 | ViewModel | apps/website/lib/viewModels/LeagueMembershipsViewModel.ts | LeagueMemberViewModel | League member management | +| LeagueStandingsViewModel | 129-131 | ViewModel | apps/website/lib/viewModels/LeagueStandingsViewModel.ts | StandingEntryViewModel | League standings pages | +| LeagueScheduleViewModel | 133-135 | ViewModel | apps/website/lib/viewModels/LeagueScheduleViewModel.ts | ScheduledRaceViewModel | League schedule pages | +| LeagueStatsViewModel | 137-145 | ViewModel | apps/website/lib/viewModels/LeagueStatsViewModel.ts | none | League stats pages | +| LeagueAdminViewModel | 147-151 | ViewModel | apps/website/lib/viewModels/LeagueAdminViewModel.ts | LeagueConfigFormModelDto, LeagueMemberViewModel, LeagueJoinRequestViewModel | League admin pages | +| CreateLeagueInput | 153-159 | Input DTO | apps/website/lib/dtos/CreateLeagueInputDto.ts | none | League creation forms | +| CreateLeagueOutput | 161-164 | Output DTO | apps/website/lib/dtos/CreateLeagueOutputDto.ts | none | League creation responses | +| DriverLeaderboardItemViewModel | 167-175 | ViewModel | apps/website/lib/viewModels/DriverLeaderboardItemViewModel.ts | none | Driver leaderboards | +| DriversLeaderboardViewModel | 177-179 | ViewModel | apps/website/lib/viewModels/DriversLeaderboardViewModel.ts | DriverLeaderboardItemViewModel | Driver leaderboard pages | +| DriverStatsDto | 181-183 | DTO | apps/website/lib/dtos/DriverStatsDto.ts | none | Driver stats displays | +| CompleteOnboardingInput | 185-188 | Input DTO | apps/website/lib/dtos/CompleteOnboardingInputDto.ts | none | Driver onboarding | +| CompleteOnboardingOutput | 190-193 | Output DTO | apps/website/lib/dtos/CompleteOnboardingOutputDto.ts | none | Driver onboarding responses | +| DriverRegistrationStatusViewModel | 195-199 | ViewModel | apps/website/lib/viewModels/DriverRegistrationStatusViewModel.ts | none | Race registration status | +| TeamSummaryViewModel | 202-208 | ViewModel | apps/website/lib/viewModels/TeamSummaryViewModel.ts | none | Team lists | +| AllTeamsViewModel | 210-212 | ViewModel | apps/website/lib/viewModels/AllTeamsViewModel.ts | TeamSummaryViewModel | Team discovery pages | +| TeamMemberViewModel | 214-219 | ViewModel | apps/website/lib/viewModels/TeamMemberViewModel.ts | DriverDTO | Team member lists | +| TeamJoinRequestItemViewModel | 221-227 | ViewModel | apps/website/lib/viewModels/TeamJoinRequestItemViewModel.ts | none | Team join requests | +| TeamDetailsViewModel | 229-237 | ViewModel | apps/website/lib/viewModels/TeamDetailsViewModel.ts | TeamMemberViewModel | Team detail pages | +| TeamMembersViewModel | 239-241 | ViewModel | apps/website/lib/viewModels/TeamMembersViewModel.ts | TeamMemberViewModel | Team member pages | +| TeamJoinRequestsViewModel | 243-245 | ViewModel | apps/website/lib/viewModels/TeamJoinRequestsViewModel.ts | TeamJoinRequestItemViewModel | Team join request pages | +| DriverTeamViewModel | 247-252 | ViewModel | apps/website/lib/viewModels/DriverTeamViewModel.ts | none | Driver team info | +| CreateTeamInput | 254-259 | Input DTO | apps/website/lib/dtos/CreateTeamInputDto.ts | none | Team creation forms | +| CreateTeamOutput | 261-263 | Output DTO | apps/website/lib/dtos/CreateTeamOutputDto.ts | none | Team creation responses | +| UpdateTeamInput | 265-270 | Input DTO | apps/website/lib/dtos/UpdateTeamInputDto.ts | none | Team update forms | +| UpdateTeamOutput | 271-273 | Output DTO | apps/website/lib/dtos/UpdateTeamOutputDto.ts | none | Team update responses | +| RaceListItemViewModel | 275-284 | ViewModel | apps/website/lib/viewModels/RaceListItemViewModel.ts | none | Race lists | +| AllRacesPageViewModel | 286-288 | ViewModel | apps/website/lib/viewModels/AllRacesPageViewModel.ts | RaceListItemViewModel | Race discovery pages | +| RaceStatsDto | 290-292 | DTO | apps/website/lib/dtos/RaceStatsDto.ts | none | Race stats displays | +| RaceDetailEntryViewModel | 294-302 | ViewModel | apps/website/lib/viewModels/RaceDetailEntryViewModel.ts | none | Race detail entry lists | +| RaceDetailUserResultViewModel | 304-313 | ViewModel | apps/website/lib/viewModels/RaceDetailUserResultViewModel.ts | none | Race detail user results | +| RaceDetailRaceViewModel | 315-326 | ViewModel | apps/website/lib/viewModels/RaceDetailRaceViewModel.ts | none | Race detail race info | +| RaceDetailLeagueViewModel | 328-336 | ViewModel | apps/website/lib/viewModels/RaceDetailLeagueViewModel.ts | none | Race detail league info | +| RaceDetailRegistrationViewModel | 338-341 | ViewModel | apps/website/lib/viewModels/RaceDetailRegistrationViewModel.ts | none | Race registration status | +| RaceDetailViewModel | 343-350 | ViewModel | apps/website/lib/viewModels/RaceDetailViewModel.ts | RaceDetailRaceViewModel, RaceDetailLeagueViewModel, RaceDetailEntryViewModel, RaceDetailRegistrationViewModel, RaceDetailUserResultViewModel | Race detail pages | +| RacesPageDataRaceViewModel | 352-364 | ViewModel | apps/website/lib/viewModels/RacesPageDataRaceViewModel.ts | none | Races page data | +| RacesPageDataViewModel | 366-368 | ViewModel | apps/website/lib/viewModels/RacesPageDataViewModel.ts | RacesPageDataRaceViewModel | Races page | +| RaceResultViewModel | 370-381 | ViewModel | apps/website/lib/viewModels/RaceResultViewModel.ts | none | Race results | +| RaceResultsDetailViewModel | 383-387 | ViewModel | apps/website/lib/viewModels/RaceResultsDetailViewModel.ts | RaceResultViewModel | Race results detail | +| RaceWithSOFViewModel | 389-393 | ViewModel | apps/website/lib/viewModels/RaceWithSOFViewModel.ts | none | Race SOF displays | +| RaceProtestViewModel | 395-405 | ViewModel | apps/website/lib/viewModels/RaceProtestViewModel.ts | none | Race protests | +| RaceProtestsViewModel | 407-410 | ViewModel | apps/website/lib/viewModels/RaceProtestsViewModel.ts | RaceProtestViewModel | Race protests list | +| RacePenaltyViewModel | 412-421 | ViewModel | apps/website/lib/viewModels/RacePenaltyViewModel.ts | none | Race penalties | +| RacePenaltiesViewModel | 423-426 | ViewModel | apps/website/lib/viewModels/RacePenaltiesViewModel.ts | RacePenaltyViewModel | Race penalties list | +| RegisterForRaceParams | 428-431 | Input DTO | apps/website/lib/dtos/RegisterForRaceParamsDto.ts | none | Race registration | +| WithdrawFromRaceParams | 433-435 | Input DTO | apps/website/lib/dtos/WithdrawFromRaceParamsDto.ts | none | Race withdrawal | +| ImportRaceResultsInput | 437-440 | Input DTO | apps/website/lib/dtos/ImportRaceResultsInputDto.ts | none | Race results import | +| ImportRaceResultsSummaryViewModel | 441-447 | ViewModel | apps/website/lib/viewModels/ImportRaceResultsSummaryViewModel.ts | none | Import results summary | +| GetEntitySponsorshipPricingResultDto | 450-454 | DTO | apps/website/lib/dtos/GetEntitySponsorshipPricingResultDto.ts | none | Sponsorship pricing | +| SponsorViewModel | 456-461 | ViewModel | apps/website/lib/viewModels/SponsorViewModel.ts | none | Sponsor lists | +| GetSponsorsOutput | 463-465 | Output DTO | apps/website/lib/dtos/GetSponsorsOutputDto.ts | SponsorViewModel | Sponsor list responses | +| CreateSponsorInput | 467-472 | Input DTO | apps/website/lib/dtos/CreateSponsorInputDto.ts | none | Sponsor creation | +| CreateSponsorOutput | 474-477 | Output DTO | apps/website/lib/dtos/CreateSponsorOutputDto.ts | none | Sponsor creation responses | +| SponsorDashboardDTO | 479-485 | DTO | apps/website/lib/dtos/SponsorDashboardDto.ts | none | Sponsor dashboards | +| SponsorshipDetailViewModel | 487-496 | ViewModel | apps/website/lib/viewModels/SponsorshipDetailViewModel.ts | none | Sponsorship details | +| SponsorSponsorshipsDTO | 498-502 | DTO | apps/website/lib/dtos/SponsorSponsorshipsDto.ts | SponsorshipDetailViewModel | Sponsor sponsorships | +| RequestAvatarGenerationInput | 505-508 | Input DTO | apps/website/lib/dtos/RequestAvatarGenerationInputDto.ts | none | Avatar generation requests | +| RequestAvatarGenerationOutput | 510-514 | Output DTO | apps/website/lib/dtos/RequestAvatarGenerationOutputDto.ts | none | Avatar generation responses | +| RecordPageViewInput | 517-521 | Input DTO | apps/website/lib/dtos/RecordPageViewInputDto.ts | none | Page view tracking | +| RecordPageViewOutput | 523-525 | Output DTO | apps/website/lib/dtos/RecordPageViewOutputDto.ts | none | Page view responses | +| RecordEngagementInput | 527-532 | Input DTO | apps/website/lib/dtos/RecordEngagementInputDto.ts | none | Engagement tracking | +| RecordEngagementOutput | 534-536 | Output DTO | apps/website/lib/dtos/RecordEngagementOutputDto.ts | none | Engagement responses | +| LoginParams | 539-542 | Input DTO | apps/website/lib/dtos/LoginParamsDto.ts | none | Login forms | +| SignupParams | 544-548 | Input DTO | apps/website/lib/dtos/SignupParamsDto.ts | none | Signup forms | +| SessionData | 550-556 | DTO | apps/website/lib/dtos/SessionDataDto.ts | none | Session management | +| PaymentViewModel | 559-565 | ViewModel | apps/website/lib/viewModels/PaymentViewModel.ts | none | Payment lists | +| GetPaymentsOutput | 567-569 | Output DTO | apps/website/lib/dtos/GetPaymentsOutputDto.ts | PaymentViewModel | Payment list responses | +| CreatePaymentInput | 571-577 | Input DTO | apps/website/lib/dtos/CreatePaymentInputDto.ts | none | Payment creation | +| CreatePaymentOutput | 579-582 | Output DTO | apps/website/lib/dtos/CreatePaymentOutputDto.ts | none | Payment creation responses | +| MembershipFeeViewModel | 584-589 | ViewModel | apps/website/lib/viewModels/MembershipFeeViewModel.ts | none | Membership fees | +| MemberPaymentViewModel | 591-596 | ViewModel | apps/website/lib/viewModels/MemberPaymentViewModel.ts | none | Member payments | +| GetMembershipFeesOutput | 598-601 | Output DTO | apps/website/lib/dtos/GetMembershipFeesOutputDto.ts | MembershipFeeViewModel, MemberPaymentViewModel | Membership fees responses | +| PrizeViewModel | 603-609 | ViewModel | apps/website/lib/viewModels/PrizeViewModel.ts | none | Prize lists | +| GetPrizesOutput | 611-613 | Output DTO | apps/website/lib/dtos/GetPrizesOutputDto.ts | PrizeViewModel | Prize list responses | +| WalletTransactionViewModel | 615-621 | ViewModel | apps/website/lib/viewModels/WalletTransactionViewModel.ts | none | Wallet transactions | +| WalletViewModel | 623-628 | ViewModel | apps/website/lib/viewModels/WalletViewModel.ts | WalletTransactionViewModel | Wallet displays | +| GetWalletOutput | 630-632 | Output DTO | apps/website/lib/dtos/GetWalletOutputDto.ts | WalletViewModel | Wallet responses | + +## Inline DTOs Found in Pages + +### apps/website/app/races/[id]/results/page.tsx (lines 17-56) +- **PenaltyTypeDTO** (17-24): Union type for penalty types - should be extracted to DTO +- **PenaltyData** (26-30): Interface for penalty data - should be extracted to DTO +- **RaceResultRowDTO** (32-41): Interface with method - should be extracted to ViewModel (has getPositionChange method) +- **DriverRowDTO** (43-46): Simple driver data - should be extracted to DTO +- **ImportResultRowDTO** (48-56): Import result data - should be extracted to DTO + +### Other Inline Types (mostly local component types, not for extraction) +- Various filter types, component props interfaces, and local UI types that are not API-related DTOs/ViewModels + +## Dependencies Mapping +- DriverDTO: Used by LeagueMemberViewModel, StandingEntryViewModel, TeamMemberViewModel +- LeagueSummaryViewModel: Used by AllLeaguesWithCapacityViewModel +- DriverLeaderboardItemViewModel: Used by DriversLeaderboardViewModel +- TeamSummaryViewModel: Used by AllTeamsViewModel +- TeamMemberViewModel: Used by TeamDetailsViewModel, TeamMembersViewModel +- TeamJoinRequestItemViewModel: Used by TeamJoinRequestsViewModel +- RaceListItemViewModel: Used by AllRacesPageViewModel +- RaceDetailRaceViewModel, RaceDetailLeagueViewModel, etc.: Used by RaceDetailViewModel +- RacesPageDataRaceViewModel: Used by RacesPageDataViewModel +- RaceResultViewModel: Used by RaceResultsDetailViewModel +- RaceProtestViewModel: Used by RaceProtestsViewModel +- RacePenaltyViewModel: Used by RacePenaltiesViewModel +- SponsorViewModel: Used by GetSponsorsOutput +- SponsorshipDetailViewModel: Used by SponsorSponsorshipsDTO +- PaymentViewModel: Used by GetPaymentsOutput +- MembershipFeeViewModel, MemberPaymentViewModel: Used by GetMembershipFeesOutput +- PrizeViewModel: Used by GetPrizesOutput +- WalletTransactionViewModel: Used by WalletViewModel +- WalletViewModel: Used by GetWalletOutput + +## Next Steps +- Extract all types to their respective files +- Update imports in apiClient.ts and all consuming files +- Remove inline types from pages and replace with proper imports +- Ensure no empty files or mixed DTO/ViewModel files \ No newline at end of file diff --git a/tsconfig.base.json b/tsconfig.base.json index b069069d9..9456a7861 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -27,7 +27,12 @@ "paths": { "@core/*": ["./core/*"], "@adapters/*": ["./adapters/*"], - "@testing/*": ["./testing/*"] + "@testing/*": ["./testing/*"], + "@/lib/dtos": ["./apps/website/lib/dtos"], + "@/lib/view-models": ["./apps/website/lib/view-models"], + "@/lib/presenters": ["./apps/website/lib/presenters"], + "@/lib/services": ["./apps/website/lib/services"], + "@/lib/api": ["./apps/website/lib/api"] } }, "exclude": ["node_modules", "dist"] diff --git a/type-inventory.md b/type-inventory.md new file mode 100644 index 000000000..7aaab3c40 --- /dev/null +++ b/type-inventory.md @@ -0,0 +1,234 @@ +# Type Inventory & Classification + +This document catalogs all types from the monolithic `apps/website/lib/apiClient.ts` file (lines 13-634) and inline DTOs from `apps/website/app/races/[id]/results/page.tsx` (lines 17-56). Types are organized by domain and classified according to Clean Architecture principles. + +## Summary +- **Total Types**: 89 types in apiClient.ts + 5 inline DTOs = 94 types +- **Domains**: Common/Shared, League, Driver, Team, Race, Sponsor, Media, Analytics, Auth, Payments +- **Classification Breakdown**: + - DTO: 45 (pure transport objects, no business logic) + - ViewModel: 44 (UI-ready with potential computed properties) + - Input: 3 (request parameters) + - Output: 2 (response wrappers) + +## Extraction Strategy Overview +- **DTOs**: Move to `apps/website/lib/dtos/{domain}/` with filename `{TypeName}.ts` +- **ViewModels**: Move to `apps/website/lib/view-models/{domain}/` with filename `{TypeName}.ts` +- **Inputs/Outputs**: Treat as DTOs in appropriate domain +- **Dependencies**: Update imports after extraction +- **No Empty Files**: Ensure each extracted file has content +- **Clean Architecture**: Maintain separation between transport (DTO) and presentation (ViewModel) layers + +--- + +## Common/Shared Types + +| Type Name | Line Range | Classification | Target Location | Dependencies | Used By | +|-----------|------------|----------------|-----------------|--------------|---------| +| DriverDTO | 13-19 | DTO | `apps/website/lib/dtos/common/DriverDto.ts` | None | LeagueAdminPresenter.ts, TeamAdminPresenter.ts, presenters (multiple) | +| ProtestViewModel | 21-29 | ViewModel | `apps/website/lib/view-models/common/ProtestViewModel.ts` | None | LeagueAdminProtestsDto.ts | +| LeagueMemberViewModel | 31-36 | ViewModel | `apps/website/lib/view-models/league/LeagueMemberViewModel.ts` | DriverDTO | LeagueMembershipsDto.ts, LeagueAdminDto.ts, leagueMembership.ts | +| StandingEntryViewModel | 38-46 | ViewModel | `apps/website/lib/view-models/league/StandingEntryViewModel.ts` | DriverDTO | LeagueStandingsDto.ts, LeagueStandingsPresenter.ts | +| ScheduledRaceViewModel | 48-54 | ViewModel | `apps/website/lib/view-models/race/ScheduledRaceViewModel.ts` | None | LeagueScheduleDto.ts | + +--- + +## League Domain Types + +| Type Name | Line Range | Classification | Target Location | Dependencies | Used By | +|-----------|------------|----------------|-----------------|--------------|---------| +| LeagueSummaryViewModel | 57-70 | ViewModel | `apps/website/lib/view-models/league/LeagueSummaryViewModel.ts` | None | AllLeaguesWithCapacityAndScoringPresenter.ts | +| AllLeaguesWithCapacityViewModel | 72-74 | ViewModel | `apps/website/lib/view-models/league/AllLeaguesWithCapacityViewModel.ts` | LeagueSummaryViewModel | ScheduleRaceFormPresenter.ts, AllLeaguesWithCapacityAndScoringPresenter.ts | +| LeagueStatsDto | 76-79 | DTO | `apps/website/lib/dtos/league/LeagueStatsDto.ts` | None | None found | +| LeagueJoinRequestViewModel | 80-86 | ViewModel | `apps/website/lib/view-models/league/LeagueJoinRequestViewModel.ts` | None | LeagueAdminDto.ts, LeagueAdminPresenter.ts | +| LeagueAdminPermissionsViewModel | 88-95 | ViewModel | `apps/website/lib/view-models/league/LeagueAdminPermissionsViewModel.ts` | None | LeagueAdminPermissionsDto.ts (existing), LeagueAdminPresenter.ts | +| LeagueOwnerSummaryViewModel | 97-102 | ViewModel | `apps/website/lib/view-models/league/LeagueOwnerSummaryViewModel.ts` | None | LeagueAdminPresenter.ts | +| LeagueConfigFormModelDto | 104-111 | DTO | `apps/website/lib/dtos/league/LeagueConfigFormModelDto.ts` | None | LeagueAdminDto.ts | +| LeagueAdminProtestsViewModel | 113-115 | ViewModel | `apps/website/lib/view-models/league/LeagueAdminProtestsViewModel.ts` | ProtestViewModel | LeagueAdminProtestsDto.ts, LeagueAdminPresenter.ts | +| LeagueSeasonSummaryViewModel | 117-123 | ViewModel | `apps/website/lib/view-models/league/LeagueSeasonSummaryViewModel.ts` | None | LeagueAdminPresenter.ts | +| LeagueMembershipsViewModel | 125-127 | ViewModel | `apps/website/lib/view-models/league/LeagueMembershipsViewModel.ts` | LeagueMemberViewModel | LeagueMembershipsDto.ts, leagueMembership.ts | +| LeagueStandingsViewModel | 129-131 | ViewModel | `apps/website/lib/view-models/league/LeagueStandingsViewModel.ts` | StandingEntryViewModel | LeagueStandingsPresenter.ts, TeamStandingsPresenter.ts | +| LeagueScheduleViewModel | 133-135 | ViewModel | `apps/website/lib/view-models/league/LeagueScheduleViewModel.ts` | ScheduledRaceViewModel | LeagueScheduleDto.ts | +| LeagueStatsViewModel | 137-145 | ViewModel | `apps/website/lib/view-models/league/LeagueStatsViewModel.ts` | None | None found | +| LeagueAdminViewModel | 147-151 | ViewModel | `apps/website/lib/view-models/league/LeagueAdminViewModel.ts` | LeagueConfigFormModelDto, LeagueMemberViewModel, LeagueJoinRequestViewModel | None found | +| CreateLeagueInput | 153-159 | Input | `apps/website/lib/dtos/league/CreateLeagueInput.ts` | None | leagueWizardService.ts | +| CreateLeagueOutput | 161-164 | Output | `apps/website/lib/dtos/league/CreateLeagueOutput.ts` | None | None found | + +--- + +## Driver Domain Types + +| Type Name | Line Range | Classification | Target Location | Dependencies | Used By | +|-----------|------------|----------------|-----------------|--------------|---------| +| DriverLeaderboardItemViewModel | 167-175 | ViewModel | `apps/website/lib/view-models/driver/DriverLeaderboardItemViewModel.ts` | None | DriversLeaderboardPresenter.ts | +| DriversLeaderboardViewModel | 177-179 | ViewModel | `apps/website/lib/view-models/driver/DriversLeaderboardViewModel.ts` | DriverLeaderboardItemViewModel | DriversLeaderboardPresenter.ts | +| DriverStatsDto | 181-183 | DTO | `apps/website/lib/dtos/driver/DriverStatsDto.ts` | None | None found | +| CompleteOnboardingInput | 185-188 | Input | `apps/website/lib/dtos/driver/CompleteOnboardingInput.ts` | None | None found | +| CompleteOnboardingOutput | 190-193 | Output | `apps/website/lib/dtos/driver/CompleteOnboardingOutput.ts` | None | None found | +| DriverRegistrationStatusViewModel | 195-199 | ViewModel | `apps/website/lib/view-models/driver/DriverRegistrationStatusViewModel.ts` | None | None found | + +--- + +## Team Domain Types + +| Type Name | Line Range | Classification | Target Location | Dependencies | Used By | +|-----------|------------|----------------|-----------------|--------------|---------| +| TeamSummaryViewModel | 202-208 | ViewModel | `apps/website/lib/view-models/team/TeamSummaryViewModel.ts` | None | AllTeamsPresenter.ts | +| AllTeamsViewModel | 210-212 | ViewModel | `apps/website/lib/view-models/team/AllTeamsViewModel.ts` | TeamSummaryViewModel | AllTeamsPresenter.ts | +| TeamMemberViewModel | 214-219 | ViewModel | `apps/website/lib/view-models/team/TeamMemberViewModel.ts` | DriverDTO | TeamDetailsPresenter.ts, TeamRosterPresenter.ts | +| TeamJoinRequestItemViewModel | 221-227 | ViewModel | `apps/website/lib/view-models/team/TeamJoinRequestItemViewModel.ts` | None | TeamAdminPresenter.ts | +| TeamDetailsViewModel | 229-237 | ViewModel | `apps/website/lib/view-models/team/TeamDetailsViewModel.ts` | TeamMemberViewModel | TeamDetailsPresenter.ts | +| TeamMembersViewModel | 239-241 | ViewModel | `apps/website/lib/view-models/team/TeamMembersViewModel.ts` | TeamMemberViewModel | TeamRosterPresenter.ts | +| TeamJoinRequestsViewModel | 243-245 | ViewModel | `apps/website/lib/view-models/team/TeamJoinRequestsViewModel.ts` | TeamJoinRequestItemViewModel | None found | +| DriverTeamViewModel | 247-252 | ViewModel | `apps/website/lib/view-models/team/DriverTeamViewModel.ts` | None | DriverTeamPresenter.ts | +| CreateTeamInput | 254-258 | Input | `apps/website/lib/dtos/team/CreateTeamInput.ts` | None | None found | +| CreateTeamOutput | 260-263 | Output | `apps/website/lib/dtos/team/CreateTeamOutput.ts` | None | None found | +| UpdateTeamInput | 265-269 | Input | `apps/website/lib/dtos/team/UpdateTeamInput.ts` | None | TeamAdminPresenter.ts | +| UpdateTeamOutput | 271-273 | Output | `apps/website/lib/dtos/team/UpdateTeamOutput.ts` | None | None found | + +--- + +## Race Domain Types + +| Type Name | Line Range | Classification | Target Location | Dependencies | Used By | +|-----------|------------|----------------|-----------------|--------------|---------| +| RaceListItemViewModel | 276-284 | ViewModel | `apps/website/lib/view-models/race/RaceListItemViewModel.ts` | None | None found | +| AllRacesPageViewModel | 286-288 | ViewModel | `apps/website/lib/view-models/race/AllRacesPageViewModel.ts` | RaceListItemViewModel | None found | +| RaceStatsDto | 290-292 | DTO | `apps/website/lib/dtos/race/RaceStatsDto.ts` | None | None found | +| RaceDetailEntryViewModel | 295-302 | ViewModel | `apps/website/lib/view-models/race/RaceDetailEntryViewModel.ts` | None | None found | +| RaceDetailUserResultViewModel | 304-313 | ViewModel | `apps/website/lib/view-models/race/RaceDetailUserResultViewModel.ts` | None | None found | +| RaceDetailRaceViewModel | 315-326 | ViewModel | `apps/website/lib/view-models/race/RaceDetailRaceViewModel.ts` | None | None found | +| RaceDetailLeagueViewModel | 328-336 | ViewModel | `apps/website/lib/view-models/race/RaceDetailLeagueViewModel.ts` | None | None found | +| RaceDetailRegistrationViewModel | 338-341 | ViewModel | `apps/website/lib/view-models/race/RaceDetailRegistrationViewModel.ts` | None | None found | +| RaceDetailViewModel | 343-350 | ViewModel | `apps/website/lib/view-models/race/RaceDetailViewModel.ts` | RaceDetailRaceViewModel, RaceDetailLeagueViewModel, RaceDetailEntryViewModel, RaceDetailRegistrationViewModel, RaceDetailUserResultViewModel | None found | +| RacesPageDataRaceViewModel | 352-364 | ViewModel | `apps/website/lib/view-models/race/RacesPageDataRaceViewModel.ts` | None | races/page.tsx, races/all/page.tsx | +| RacesPageDataViewModel | 366-368 | ViewModel | `apps/website/lib/view-models/race/RacesPageDataViewModel.ts` | RacesPageDataRaceViewModel | races/page.tsx, races/all/page.tsx | +| RaceResultViewModel | 370-381 | ViewModel | `apps/website/lib/view-models/race/RaceResultViewModel.ts` | None | None found | +| RaceResultsDetailViewModel | 383-387 | ViewModel | `apps/website/lib/view-models/race/RaceResultsDetailViewModel.ts` | RaceResultViewModel | races/[id]/results/page.tsx | +| RaceWithSOFViewModel | 389-393 | ViewModel | `apps/website/lib/view-models/race/RaceWithSOFViewModel.ts` | None | races/[id]/results/page.tsx | +| RaceProtestViewModel | 395-405 | ViewModel | `apps/website/lib/view-models/race/RaceProtestViewModel.ts` | None | None found | +| RaceProtestsViewModel | 407-410 | ViewModel | `apps/website/lib/view-models/race/RaceProtestsViewModel.ts` | RaceProtestViewModel | races/[id]/stewarding/page.tsx | +| RacePenaltyViewModel | 412-421 | ViewModel | `apps/website/lib/view-models/race/RacePenaltyViewModel.ts` | None | None found | +| RacePenaltiesViewModel | 423-426 | ViewModel | `apps/website/lib/view-models/race/RacePenaltiesViewModel.ts` | RacePenaltyViewModel | races/[id]/stewarding/page.tsx | +| RegisterForRaceParams | 428-431 | Input | `apps/website/lib/dtos/race/RegisterForRaceParams.ts` | None | None found | +| WithdrawFromRaceParams | 433-435 | Input | `apps/website/lib/dtos/race/WithdrawFromRaceParams.ts` | None | None found | +| ImportRaceResultsInput | 437-439 | Input | `apps/website/lib/dtos/race/ImportRaceResultsInput.ts` | None | None found | +| ImportRaceResultsSummaryViewModel | 441-447 | ViewModel | `apps/website/lib/view-models/race/ImportRaceResultsSummaryViewModel.ts` | None | None found | + +--- + +## Sponsor Domain Types + +| Type Name | Line Range | Classification | Target Location | Dependencies | Used By | +|-----------|------------|----------------|-----------------|--------------|---------| +| GetEntitySponsorshipPricingResultDto | 450-454 | DTO | `apps/website/lib/dtos/sponsor/GetEntitySponsorshipPricingResultDto.ts` | None | None found | +| SponsorViewModel | 456-462 | ViewModel | `apps/website/lib/view-models/sponsor/SponsorViewModel.ts` | None | None found | +| GetSponsorsOutput | 463-465 | Output | `apps/website/lib/dtos/sponsor/GetSponsorsOutput.ts` | SponsorViewModel | None found | +| CreateSponsorInput | 467-472 | Input | `apps/website/lib/dtos/sponsor/CreateSponsorInput.ts` | None | None found | +| CreateSponsorOutput | 474-477 | Output | `apps/website/lib/dtos/sponsor/CreateSponsorOutput.ts` | None | None found | +| SponsorDashboardDTO | 479-485 | DTO | `apps/website/lib/dtos/sponsor/SponsorDashboardDto.ts` | None | None found | +| SponsorshipDetailViewModel | 487-497 | ViewModel | `apps/website/lib/view-models/sponsor/SponsorshipDetailViewModel.ts` | None | None found | +| SponsorSponsorshipsDTO | 499-502 | DTO | `apps/website/lib/dtos/sponsor/SponsorSponsorshipsDto.ts` | SponsorshipDetailViewModel | None found | + +--- + +## Media Domain Types + +| Type Name | Line Range | Classification | Target Location | Dependencies | Used By | +|-----------|------------|----------------|-----------------|--------------|---------| +| RequestAvatarGenerationInput | 505-508 | Input | `apps/website/lib/dtos/media/RequestAvatarGenerationInput.ts` | None | None found | +| RequestAvatarGenerationOutput | 510-514 | Output | `apps/website/lib/dtos/media/RequestAvatarGenerationOutput.ts` | None | None found | + +--- + +## Analytics Domain Types + +| Type Name | Line Range | Classification | Target Location | Dependencies | Used By | +|-----------|------------|----------------|-----------------|--------------|---------| +| RecordPageViewInput | 517-521 | Input | `apps/website/lib/dtos/analytics/RecordPageViewInput.ts` | None | None found | +| RecordPageViewOutput | 523-525 | Output | `apps/website/lib/dtos/analytics/RecordPageViewOutput.ts` | None | None found | +| RecordEngagementInput | 527-532 | Input | `apps/website/lib/dtos/analytics/RecordEngagementInput.ts` | None | None found | +| RecordEngagementOutput | 534-536 | Output | `apps/website/lib/dtos/analytics/RecordEngagementOutput.ts` | None | None found | + +--- + +## Auth Domain Types + +| Type Name | Line Range | Classification | Target Location | Dependencies | Used By | +|-----------|------------|----------------|-----------------|--------------|---------| +| LoginParams | 539-542 | Input | `apps/website/lib/dtos/auth/LoginParams.ts` | None | None found | +| SignupParams | 544-548 | Input | `apps/website/lib/dtos/auth/SignupParams.ts` | None | None found | +| SessionData | 550-556 | DTO | `apps/website/lib/dtos/auth/SessionData.ts` | None | None found | + +--- + +## Payments Domain Types + +| Type Name | Line Range | Classification | Target Location | Dependencies | Used By | +|-----------|------------|----------------|-----------------|--------------|---------| +| PaymentViewModel | 559-565 | ViewModel | `apps/website/lib/view-models/payments/PaymentViewModel.ts` | None | None found | +| GetPaymentsOutput | 567-569 | Output | `apps/website/lib/dtos/payments/GetPaymentsOutput.ts` | PaymentViewModel | None found | +| CreatePaymentInput | 571-577 | Input | `apps/website/lib/dtos/payments/CreatePaymentInput.ts` | None | None found | +| CreatePaymentOutput | 579-582 | Output | `apps/website/lib/dtos/payments/CreatePaymentOutput.ts` | None | None found | +| MembershipFeeViewModel | 584-589 | ViewModel | `apps/website/lib/view-models/payments/MembershipFeeViewModel.ts` | None | None found | +| MemberPaymentViewModel | 591-596 | ViewModel | `apps/website/lib/view-models/payments/MemberPaymentViewModel.ts` | None | None found | +| GetMembershipFeesOutput | 598-601 | Output | `apps/website/lib/dtos/payments/GetMembershipFeesOutput.ts` | MembershipFeeViewModel, MemberPaymentViewModel | None found | +| PrizeViewModel | 603-609 | ViewModel | `apps/website/lib/view-models/payments/PrizeViewModel.ts` | None | None found | +| GetPrizesOutput | 611-613 | Output | `apps/website/lib/dtos/payments/GetPrizesOutput.ts` | PrizeViewModel | None found | +| WalletTransactionViewModel | 615-621 | ViewModel | `apps/website/lib/view-models/payments/WalletTransactionViewModel.ts` | None | None found | +| WalletViewModel | 623-628 | ViewModel | `apps/website/lib/view-models/payments/WalletViewModel.ts` | WalletTransactionViewModel | None found | +| GetWalletOutput | 630-632 | Output | `apps/website/lib/dtos/payments/GetWalletOutput.ts` | WalletViewModel | None found | + +--- + +## Inline DTOs from Results Page + +| Type Name | Line Range | Classification | Target Location | Dependencies | Used By | +|-----------|------------|----------------|-----------------|--------------|---------| +| PenaltyTypeDTO | 17-24 | DTO | `apps/website/lib/dtos/race/PenaltyTypeDto.ts` | None | PenaltyTypeDto.ts (existing file) | +| PenaltyData | 26-30 | DTO | `apps/website/lib/dtos/race/PenaltyData.ts` | PenaltyTypeDTO | None found | +| RaceResultRowDTO | 32-41 | DTO | `apps/website/lib/dtos/race/RaceResultRowDto.ts` | None | None found | +| DriverRowDTO | 43-46 | DTO | `apps/website/lib/dtos/race/DriverRowDto.ts` | None | None found | +| ImportResultRowDTO | 48-56 | DTO | `apps/website/lib/dtos/race/ImportResultRowDto.ts` | None | None found | + +--- + +## Extraction Recommendations + +1. **Create Directory Structure**: + - `apps/website/lib/dtos/common/` + - `apps/website/lib/dtos/league/` + - `apps/website/lib/dtos/driver/` + - `apps/website/lib/dtos/team/` + - `apps/website/lib/dtos/race/` + - `apps/website/lib/dtos/sponsor/` + - `apps/website/lib/dtos/media/` + - `apps/website/lib/dtos/analytics/` + - `apps/website/lib/dtos/auth/` + - `apps/website/lib/dtos/payments/` + - `apps/website/lib/view-models/common/` + - `apps/website/lib/view-models/league/` + - `apps/website/lib/view-models/driver/` + - `apps/website/lib/view-models/team/` + - `apps/website/lib/view-models/race/` + - `apps/website/lib/view-models/sponsor/` + - `apps/website/lib/view-models/payments/` + +2. **Move Types Sequentially**: + - Start with types that have no dependencies + - Update imports in apiClient.ts and dependent files + - Test after each major domain extraction + +3. **Update Existing DTOs**: + - Some DTOs already exist (e.g., LeagueAdminPermissionsDto.ts) + - Ensure consistency in naming and structure + +4. **Clean Architecture Compliance**: + - DTOs: Pure data transfer, no methods + - ViewModels: May include computed properties for UI + - Clear separation maintained + +5. **Post-Extraction**: + - Remove types from apiClient.ts + - Update all import statements + - Run tests to ensure no breaking changes \ No newline at end of file