api client refactor

This commit is contained in:
2025-12-17 18:01:47 +01:00
parent bab55955e1
commit 4177644b18
190 changed files with 6403 additions and 1624 deletions

View File

@@ -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/**/*"]
}
]
}
]
}
}

View File

@@ -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",

361
apps/website/DATA_FLOW.md Normal file
View File

@@ -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<RaceResultDto[]>();
// Direct API call and DTO usage in UI
useEffect(() => {
apiClient.getRaceResults().then(setData);
}, []);
return <div>{data?.map(d => d.position)}</div>;
};
```
**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<RaceResultViewModel[]>();
useEffect(() => {
RaceResultsService.getResults().then(setData);
}, []);
return <div>{data?.map(d => d.formattedPosition)}</div>;
};
```
**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<RaceResultViewModel[]> {
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
}));
}
}
```

View File

@@ -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 */}
<div className="flex items-center gap-4 mb-4">
<div className="relative w-16 h-16 rounded-full overflow-hidden border-2 border-charcoal-outline group-hover:border-primary-blue transition-colors">
<Image src={driver.avatarUrl} alt={driver.name} fill className="object-cover" />
<Image src={driver.avatarUrl || '/avatars/default.png'} alt={driver.name} fill className="object-cover" />
</div>
<div>
<h3 className="text-lg font-semibold text-white group-hover:text-primary-blue transition-colors">
@@ -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 */}
<div className="relative w-9 h-9 rounded-full overflow-hidden border-2 border-charcoal-outline">
<Image src={driver.avatarUrl} alt={driver.name} fill className="object-cover" />
<Image src={driver.avatarUrl || '/avatars/default.png'} alt={driver.name} fill className="object-cover" />
</div>
{/* 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"
>
<div className="relative w-12 h-12 mx-auto rounded-full overflow-hidden border-2 border-charcoal-outline mb-2">
<Image src={driver.avatarUrl} alt={driver.name} fill className="object-cover" />
<Image src={driver.avatarUrl || '/avatars/default.png'} alt={driver.name} fill className="object-cover" />
<div className="absolute bottom-0 right-0 w-3 h-3 rounded-full bg-performance-green border-2 border-iron-gray" />
</div>
<p className="text-sm font-medium text-white truncate group-hover:text-performance-green transition-colors">
@@ -377,7 +373,8 @@ function RecentActivity({ drivers, onDriverClick }: RecentActivityProps) {
export default function DriversPage() {
const router = useRouter();
const [drivers, setDrivers] = useState<DriverListItem[]>([]);
const [drivers, setDrivers] = useState<DriverLeaderboardItemViewModel[]>([]);
const [viewModel, setViewModel] = useState<DriverLeaderboardViewModel | null>(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);
};

View File

@@ -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<LeagueDriverSeasonStatsDTO[]>([]);
const [drivers, setDrivers] = useState<DriverDTO[]>([]);
const [standings, setStandings] = useState<StandingEntryViewModel[]>([]);
const [drivers, setDrivers] = useState<DriverDto[]>([]);
const [memberships, setMemberships] = useState<LeagueMembership[]>([]);
const [viewModel, setViewModel] = useState<LeagueStandingsViewModel | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(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 (
<div className="space-y-6">
@@ -166,7 +133,7 @@ export default function LeagueStandingsPage() {
<div>
<p className="text-xs text-gray-400 mb-1">Championship Leader</p>
<p className="font-bold text-white">{drivers.find(d => d.id === leader?.driverId)?.name || 'N/A'}</p>
<p className="text-sm text-yellow-400 font-medium">{leader?.totalPoints || 0} points</p>
<p className="text-sm text-yellow-400 font-medium">{leader?.points || 0} points</p>
</div>
</div>
</Card>

View File

@@ -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<DriverRowDTO | undefined>(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() {
</span>
<span className="flex items-center gap-2">
<Users className="w-4 h-4" />
{raceData.results.length} drivers classified
{raceData.stats.totalDrivers} drivers classified
</span>
</>
)}
@@ -265,11 +224,11 @@ export default function RaceResultsPage() {
<Card>
{hasResults && raceData ? (
<ResultsTable
results={raceData.results as any}
drivers={raceData.drivers as any}
results={raceData.resultsByPosition}
drivers={raceData.drivers}
pointsSystem={raceData.pointsSystem ?? {}}
fastestLapTime={raceData.fastestLapTime ?? 0}
penalties={raceData.penalties as any}
penalties={raceData.penalties}
currentDriverId={raceData.currentDriverId ?? ''}
isAdmin={isAdmin}
onPenaltyClick={handlePenaltyClick}

View File

@@ -0,0 +1,24 @@
import { BaseApiClient } from '../base/BaseApiClient';
import type {
RecordPageViewInputDto,
RecordPageViewOutputDto,
RecordEngagementInputDto,
RecordEngagementOutputDto,
} from '../../dtos';
/**
* Analytics API Client
*
* Handles all analytics-related API operations.
*/
export class AnalyticsApiClient extends BaseApiClient {
/** Record a page view */
recordPageView(input: RecordPageViewInputDto): Promise<RecordPageViewOutputDto> {
return this.post<RecordPageViewOutputDto>('/analytics/page-view', input);
}
/** Record an engagement event */
recordEngagement(input: RecordEngagementInputDto): Promise<RecordEngagementOutputDto> {
return this.post<RecordEngagementOutputDto>('/analytics/engagement', input);
}
}

View File

@@ -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<SessionDataDto> {
return this.post<SessionDataDto>('/auth/signup', params);
}
/** Login with email */
login(params: LoginParamsDto): Promise<SessionDataDto> {
return this.post<SessionDataDto>('/auth/login', params);
}
/** Get current session */
getSession(): Promise<SessionDataDto | null> {
return this.get<SessionDataDto | null>('/auth/session');
}
/** Logout */
logout(): Promise<void> {
return this.post<void>('/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}`;
}
}

View File

@@ -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<T>(method: string, path: string, data?: object): Promise<T> {
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<T>(path: string): Promise<T> {
return this.request<T>('GET', path);
}
protected post<T>(path: string, data: object): Promise<T> {
return this.request<T>('POST', path, data);
}
protected put<T>(path: string, data: object): Promise<T> {
return this.request<T>('PUT', path, data);
}
protected delete<T>(path: string): Promise<T> {
return this.request<T>('DELETE', path);
}
protected patch<T>(path: string, data: object): Promise<T> {
return this.request<T>('PATCH', path, data);
}
}

View File

@@ -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<DriversLeaderboardDto> {
return this.get<DriversLeaderboardDto>('/drivers/leaderboard');
}
/** Complete driver onboarding */
completeOnboarding(input: CompleteOnboardingInputDto): Promise<CompleteOnboardingOutputDto> {
return this.post<CompleteOnboardingOutputDto>('/drivers/complete-onboarding', input);
}
/** Get current driver (based on session) */
getCurrent(): Promise<DriverDto | null> {
return this.get<DriverDto | null>('/drivers/current');
}
}

View File

@@ -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);

View File

@@ -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<AllLeaguesWithCapacityDto> {
return this.get<AllLeaguesWithCapacityDto>('/leagues/all-with-capacity');
}
/** Get total number of leagues */
getTotal(): Promise<LeagueStatsDto> {
return this.get<LeagueStatsDto>('/leagues/total-leagues');
}
/** Get league standings */
getStandings(leagueId: string): Promise<LeagueStandingsDto> {
return this.get<LeagueStandingsDto>(`/leagues/${leagueId}/standings`);
}
/** Get league schedule */
getSchedule(leagueId: string): Promise<LeagueScheduleDto> {
return this.get<LeagueScheduleDto>(`/leagues/${leagueId}/schedule`);
}
/** Get league memberships */
getMemberships(leagueId: string): Promise<LeagueMembershipsDto> {
return this.get<LeagueMembershipsDto>(`/leagues/${leagueId}/memberships`);
}
/** Create a new league */
create(input: CreateLeagueInputDto): Promise<CreateLeagueOutputDto> {
return this.post<CreateLeagueOutputDto>('/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 });
}
}

View File

@@ -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<RequestAvatarGenerationOutputDto> {
return this.post<RequestAvatarGenerationOutputDto>('/media/avatar/generate', input);
}
}

View File

@@ -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<GetPaymentsOutputDto> {
const params = new URLSearchParams();
if (leagueId) params.append('leagueId', leagueId);
if (driverId) params.append('driverId', driverId);
const query = params.toString();
return this.get<GetPaymentsOutputDto>(`/payments${query ? `?${query}` : ''}`);
}
/** Create a payment */
createPayment(input: CreatePaymentInputDto): Promise<CreatePaymentOutputDto> {
return this.post<CreatePaymentOutputDto>('/payments', input);
}
/** Get membership fees */
getMembershipFees(leagueId: string): Promise<GetMembershipFeesOutputDto> {
return this.get<GetMembershipFeesOutputDto>(`/payments/membership-fees?leagueId=${leagueId}`);
}
/** Get prizes */
getPrizes(leagueId?: string, seasonId?: string): Promise<GetPrizesOutputDto> {
const params = new URLSearchParams();
if (leagueId) params.append('leagueId', leagueId);
if (seasonId) params.append('seasonId', seasonId);
const query = params.toString();
return this.get<GetPrizesOutputDto>(`/payments/prizes${query ? `?${query}` : ''}`);
}
/** Get wallet */
getWallet(driverId: string): Promise<GetWalletOutputDto> {
return this.get<GetWalletOutputDto>(`/payments/wallets?driverId=${driverId}`);
}
}

View File

@@ -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<RaceStatsDto> {
return this.get<RaceStatsDto>('/races/total-races');
}
/** Get races page data */
getPageData(): Promise<RacesPageDataDto> {
return this.get<RacesPageDataDto>('/races/page-data');
}
/** Get race detail */
getDetail(raceId: string, driverId: string): Promise<RaceDetailDto> {
return this.get<RaceDetailDto>(`/races/${raceId}?driverId=${driverId}`);
}
/** Get race results detail */
getResultsDetail(raceId: string): Promise<RaceResultsDetailDto> {
return this.get<RaceResultsDetailDto>(`/races/${raceId}/results`);
}
/** Get race with strength of field */
getWithSOF(raceId: string): Promise<RaceWithSOFDto> {
return this.get<RaceWithSOFDto>(`/races/${raceId}/sof`);
}
/** Register for race */
register(raceId: string, input: RegisterForRaceInputDto): Promise<void> {
return this.post<void>(`/races/${raceId}/register`, input);
}
/** Import race results */
importResults(raceId: string, input: ImportRaceResultsInputDto): Promise<ImportRaceResultsSummaryDto> {
return this.post<ImportRaceResultsSummaryDto>(`/races/${raceId}/import-results`, input);
}
}

View File

@@ -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<GetEntitySponsorshipPricingResultDto> {
return this.get<GetEntitySponsorshipPricingResultDto>('/sponsors/pricing');
}
/** Get all sponsors */
getAll(): Promise<GetSponsorsOutputDto> {
return this.get<GetSponsorsOutputDto>('/sponsors');
}
/** Create a new sponsor */
create(input: CreateSponsorInputDto): Promise<CreateSponsorOutputDto> {
return this.post<CreateSponsorOutputDto>('/sponsors', input);
}
/** Get sponsor dashboard */
getDashboard(sponsorId: string): Promise<SponsorDashboardDto | null> {
return this.get<SponsorDashboardDto | null>(`/sponsors/dashboard/${sponsorId}`);
}
/** Get sponsor sponsorships */
getSponsorships(sponsorId: string): Promise<SponsorSponsorshipsDto | null> {
return this.get<SponsorSponsorshipsDto | null>(`/sponsors/${sponsorId}/sponsorships`);
}
}

View File

@@ -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<AllTeamsDto> {
return this.get<AllTeamsDto>('/teams/all');
}
/** Get team details */
getDetails(teamId: string): Promise<TeamDetailsDto | null> {
return this.get<TeamDetailsDto | null>(`/teams/${teamId}`);
}
/** Get team members */
getMembers(teamId: string): Promise<TeamMembersDto> {
return this.get<TeamMembersDto>(`/teams/${teamId}/members`);
}
/** Get team join requests */
getJoinRequests(teamId: string): Promise<TeamJoinRequestsDto> {
return this.get<TeamJoinRequestsDto>(`/teams/${teamId}/join-requests`);
}
/** Create a new team */
create(input: CreateTeamInputDto): Promise<CreateTeamOutputDto> {
return this.post<CreateTeamOutputDto>('/teams', input);
}
/** Update team */
update(teamId: string, input: UpdateTeamInputDto): Promise<UpdateTeamOutputDto> {
return this.patch<UpdateTeamOutputDto>(`/teams/${teamId}`, input);
}
/** Get driver's team */
getDriverTeam(driverId: string): Promise<DriverTeamDto | null> {
return this.get<DriverTeamDto | null>(`/teams/driver/${driverId}`);
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -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[];
}

View File

@@ -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[];
}

View File

@@ -0,0 +1,9 @@
import type { TeamSummaryDto } from './TeamSummaryDto';
/**
* All teams data transfer object
* List of all teams
*/
export interface AllTeamsDto {
teams: TeamSummaryDto[];
}

View File

@@ -0,0 +1,8 @@
/**
* Complete onboarding input data transfer object
* Input for completing driver onboarding
*/
export interface CompleteOnboardingInputDto {
iracingId: string;
displayName: string;
}

View File

@@ -0,0 +1,8 @@
/**
* Complete onboarding output data transfer object
* Output from completing driver onboarding
*/
export interface CompleteOnboardingOutputDto {
driverId: string;
success: boolean;
}

View File

@@ -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;
}

View File

@@ -0,0 +1,8 @@
/**
* Create league output transport object
* Output data after creating a new league
*/
export interface CreateLeagueOutputDto {
leagueId: string;
success: boolean;
}

View File

@@ -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;
}

View File

@@ -0,0 +1,8 @@
/**
* Create payment output data transfer object
* Output from creating a payment
*/
export interface CreatePaymentOutputDto {
paymentId: string;
success: boolean;
}

View File

@@ -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;
}

View File

@@ -0,0 +1,8 @@
/**
* Create sponsor output data transfer object
* Output from creating a sponsor
*/
export interface CreateSponsorOutputDto {
sponsorId: string;
success: boolean;
}

View File

@@ -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;
}

View File

@@ -0,0 +1,8 @@
/**
* Create team output data transfer object
* Output from creating a team
*/
export interface CreateTeamOutputDto {
teamId: string;
success: boolean;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -0,0 +1,8 @@
/**
* Driver row data transfer object
* Represents a driver in a table row
*/
export interface DriverRowDto {
id: string;
name: string;
}

View File

@@ -0,0 +1,7 @@
/**
* Driver stats data transfer object
* Global driver statistics
*/
export interface DriverStatsDto {
totalDrivers: number;
}

View File

@@ -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;
}

View File

@@ -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[];
}

View File

@@ -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;
}

View File

@@ -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[];
}

View File

@@ -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[];
}

View File

@@ -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[];
}

View File

@@ -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[];
}

View File

@@ -0,0 +1,9 @@
import type { WalletDto } from './WalletDto';
/**
* Get wallet output data transfer object
* Output containing wallet information
*/
export interface GetWalletOutputDto {
wallet: WalletDto;
}

View File

@@ -0,0 +1,7 @@
/**
* Import race results input data transfer object
* Input for importing race results
*/
export interface ImportRaceResultsInputDto {
resultsFileContent: string;
}

View File

@@ -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[];
}

View File

@@ -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;
}

View File

@@ -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[];
}

View File

@@ -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;
}

View File

@@ -0,0 +1,9 @@
import type { ProtestViewModel } from '../apiClient';
/**
* League admin protests transport object
* Contains protests for league administration
*/
export interface LeagueAdminProtestsDto {
protests: ProtestViewModel[];
}

View File

@@ -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
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -0,0 +1,9 @@
import type { LeagueMemberViewModel } from '../apiClient';
/**
* League memberships transport object
* Contains the list of league members
*/
export interface LeagueMembershipsDto {
members: LeagueMemberViewModel[];
}

View File

@@ -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;
}

View File

@@ -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[];
}

View File

@@ -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;
}

View File

@@ -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[];
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -0,0 +1,8 @@
/**
* Login parameters data transfer object
* Parameters for user login
*/
export interface LoginParamsDto {
email: string;
password: string;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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';

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
};
}

View File

@@ -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;
}

View File

@@ -0,0 +1,8 @@
/**
* Race detail registration data transfer object
* Registration information in race details
*/
export interface RaceDetailRegistrationDto {
isUserRegistered: boolean;
canRegister: boolean;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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<string, string>;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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<string, string>;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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<number, number>;
fastestLapTime: number;
penalties: { driverId: string; type: string; value?: number }[];
currentDriverId: string;
}

View File

@@ -0,0 +1,7 @@
/**
* Race stats data transfer object
* Global race statistics
*/
export interface RaceStatsDto {
totalRaces: number;
}

View File

@@ -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;
}

View File

@@ -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[];
}

View File

@@ -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;
}

View File

@@ -0,0 +1,10 @@
/**
* Record engagement input data transfer object
* Input for recording an engagement event
*/
export interface RecordEngagementInputDto {
eventType: string;
eventData?: Record<string, unknown>;
userId?: string;
sessionId?: string;
}

View File

@@ -0,0 +1,7 @@
/**
* Record engagement output data transfer object
* Output from recording an engagement event
*/
export interface RecordEngagementOutputDto {
success: boolean;
}

View File

@@ -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;
}

View File

@@ -0,0 +1,7 @@
/**
* Record page view output data transfer object
* Output from recording a page view
*/
export interface RecordPageViewOutputDto {
success: boolean;
}

View File

@@ -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;
}

View File

@@ -0,0 +1,8 @@
/**
* Request avatar generation input data transfer object
* Input for requesting avatar generation
*/
export interface RequestAvatarGenerationInputDto {
driverId: string;
style?: string;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -0,0 +1,9 @@
/**
* Signup parameters data transfer object
* Parameters for user signup
*/
export interface SignupParamsDto {
email: string;
password: string;
displayName: string;
}

View File

@@ -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;
}

View File

@@ -0,0 +1,10 @@
/**
* Sponsor data transfer object
* Represents a sponsor entity
*/
export interface SponsorDto {
id: string;
name: string;
logoUrl?: string;
websiteUrl?: string;
}

View File

@@ -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[];
}

View File

@@ -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;
}

Some files were not shown because too many files have changed in this diff Show More