diff --git a/apps/api/src/domain/team/TeamController.ts b/apps/api/src/domain/team/TeamController.ts index d13cdb2eb..aff3b89f2 100644 --- a/apps/api/src/domain/team/TeamController.ts +++ b/apps/api/src/domain/team/TeamController.ts @@ -5,6 +5,7 @@ import { TeamService } from './TeamService'; import { CreateTeamInputDTO } from './dtos/CreateTeamInputDTO'; import { CreateTeamOutputDTO } from './dtos/CreateTeamOutputDTO'; import { GetAllTeamsOutputDTO } from './dtos/GetAllTeamsOutputDTO'; +import { GetTeamsLeaderboardOutputDTO } from './dtos/GetTeamsLeaderboardOutputDTO'; import { GetDriverTeamOutputDTO } from './dtos/GetDriverTeamOutputDTO'; import { GetTeamDetailsOutputDTO } from './dtos/GetTeamDetailsOutputDTO'; import { GetTeamJoinRequestsOutputDTO } from './dtos/GetTeamJoinRequestsOutputDTO'; @@ -32,6 +33,14 @@ export class TeamController { return await this.teamService.getAll(); } + @Public() + @Get('leaderboard') + @ApiOperation({ summary: 'Get teams leaderboard' }) + @ApiResponse({ status: 200, description: 'Teams leaderboard data', type: GetTeamsLeaderboardOutputDTO }) + async getLeaderboard(): Promise { + return await this.teamService.getLeaderboard(); + } + @Public() @Get(':teamId') @ApiOperation({ summary: 'Get team details' }) diff --git a/apps/api/src/domain/team/TeamProviders.ts b/apps/api/src/domain/team/TeamProviders.ts index 144237cd2..aabc20545 100644 --- a/apps/api/src/domain/team/TeamProviders.ts +++ b/apps/api/src/domain/team/TeamProviders.ts @@ -18,6 +18,7 @@ import { TEAM_REPOSITORY_TOKEN, TEAM_STATS_REPOSITORY_TOKEN, UPDATE_TEAM_USE_CASE_TOKEN, + GET_TEAMS_LEADERBOARD_USE_CASE_TOKEN, } from './TeamTokens'; export { @@ -34,6 +35,7 @@ import type { DriverRepository } from '@core/racing/domain/repositories/DriverRe import type { TeamMembershipRepository } from '@core/racing/domain/repositories/TeamMembershipRepository'; import type { TeamRepository } from '@core/racing/domain/repositories/TeamRepository'; import type { TeamStatsRepository } from '@core/racing/domain/repositories/TeamStatsRepository'; +import type { DriverStatsRepository } from '@core/racing/domain/repositories/DriverStatsRepository'; import type { Logger } from '@core/shared/domain/Logger'; // Import concrete implementations @@ -52,9 +54,11 @@ import { GetTeamMembershipUseCase } from '@core/racing/application/use-cases/Get import { GetTeamMembersUseCase } from '@core/racing/application/use-cases/GetTeamMembersUseCase'; import { JoinTeamUseCase } from '@core/racing/application/use-cases/JoinTeamUseCase'; import { UpdateTeamUseCase } from '@core/racing/application/use-cases/UpdateTeamUseCase'; +import { GetTeamsLeaderboardUseCase } from '@core/racing/application/use-cases/GetTeamsLeaderboardUseCase'; // Import presenters import { AllTeamsPresenter } from './presenters/AllTeamsPresenter'; +import { TeamsLeaderboardPresenter } from './presenters/TeamsLeaderboardPresenter'; export const TeamProviders: Provider[] = [ { @@ -100,6 +104,10 @@ export const TeamProviders: Provider[] = [ }, inject: [MEDIA_RESOLVER_TOKEN], }, + { + provide: TeamsLeaderboardPresenter, + useClass: TeamsLeaderboardPresenter, + }, // Use Cases { provide: GET_ALL_TEAMS_USE_CASE_TOKEN, @@ -155,4 +163,13 @@ export const TeamProviders: Provider[] = [ new JoinTeamUseCase(teamRepo, membershipRepo, logger), inject: [TEAM_REPOSITORY_TOKEN, TEAM_MEMBERSHIP_REPOSITORY_TOKEN, LOGGER_TOKEN], }, + { + provide: GET_TEAMS_LEADERBOARD_USE_CASE_TOKEN, + useFactory: (teamRepo: TeamRepository, membershipRepo: TeamMembershipRepository, driverStatsRepo: DriverStatsRepository, logger: Logger) => + new GetTeamsLeaderboardUseCase(teamRepo, membershipRepo, (driverId) => { + const stats = driverStatsRepo.getDriverStatsSync?.(driverId); + return stats ? { rating: stats.rating, wins: stats.wins, totalRaces: stats.totalRaces } : null; + }, logger), + inject: [TEAM_REPOSITORY_TOKEN, TEAM_MEMBERSHIP_REPOSITORY_TOKEN, 'IDriverStatsRepository', LOGGER_TOKEN], + }, ]; diff --git a/apps/api/src/domain/team/TeamService.test.ts b/apps/api/src/domain/team/TeamService.test.ts index 4c553f00a..145e157ac 100644 --- a/apps/api/src/domain/team/TeamService.test.ts +++ b/apps/api/src/domain/team/TeamService.test.ts @@ -134,6 +134,8 @@ describe('TeamService', () => { new GetDriverTeamUseCase(teamRepository as never, membershipRepository as never, logger), new GetTeamMembershipUseCase(membershipRepository as never, logger), { execute: vi.fn() } as never, // joinTeamUseCase + { execute: vi.fn() } as never, // getTeamsLeaderboardUseCase + { reset: vi.fn(), present: vi.fn(), getViewModel: vi.fn() } as never, // teamsLeaderboardPresenter logger ); }); diff --git a/apps/api/src/domain/team/TeamService.ts b/apps/api/src/domain/team/TeamService.ts index 8418dd38c..c1df4328e 100644 --- a/apps/api/src/domain/team/TeamService.ts +++ b/apps/api/src/domain/team/TeamService.ts @@ -7,6 +7,7 @@ import { GetTeamDetailsOutputDTO } from './dtos/GetTeamDetailsOutputDTO'; import { GetTeamJoinRequestsOutputDTO } from './dtos/GetTeamJoinRequestsOutputDTO'; import { GetTeamMembershipOutputDTO } from './dtos/GetTeamMembershipOutputDTO'; import { GetTeamMembersOutputDTO } from './dtos/GetTeamMembersOutputDTO'; +import { GetTeamsLeaderboardOutputDTO } from './dtos/GetTeamsLeaderboardOutputDTO'; import { UpdateTeamInputDTO } from './dtos/UpdateTeamInputDTO'; import { UpdateTeamOutputDTO } from './dtos/UpdateTeamOutputDTO'; @@ -21,6 +22,7 @@ import { GetTeamDetailsUseCase } from '@core/racing/application/use-cases/GetTea import { GetTeamJoinRequestsUseCase } from '@core/racing/application/use-cases/GetTeamJoinRequestsUseCase'; import { GetTeamMembershipUseCase } from '@core/racing/application/use-cases/GetTeamMembershipUseCase'; import { GetTeamMembersUseCase } from '@core/racing/application/use-cases/GetTeamMembersUseCase'; +import { GetTeamsLeaderboardUseCase } from '@core/racing/application/use-cases/GetTeamsLeaderboardUseCase'; import { JoinTeamUseCase } from '@core/racing/application/use-cases/JoinTeamUseCase'; import { UpdateTeamInput, UpdateTeamUseCase } from '@core/racing/application/use-cases/UpdateTeamUseCase'; @@ -32,12 +34,15 @@ import { GET_TEAM_DETAILS_USE_CASE_TOKEN, GET_TEAM_JOIN_REQUESTS_USE_CASE_TOKEN, GET_TEAM_MEMBERS_USE_CASE_TOKEN, - GET_TEAM_MEMBERSHIP_USE_CASE_TOKEN, + GET_TEAM_MEMBERS_USE_CASE_TOKEN as GET_TEAM_MEMBERSHIP_USE_CASE_TOKEN, + GET_TEAMS_LEADERBOARD_USE_CASE_TOKEN, JOIN_TEAM_USE_CASE_TOKEN, LOGGER_TOKEN, UPDATE_TEAM_USE_CASE_TOKEN, } from './TeamTokens'; +import { TeamsLeaderboardPresenter } from './presenters/TeamsLeaderboardPresenter'; + @Injectable() export class TeamService { constructor( @@ -50,6 +55,8 @@ export class TeamService { @Inject(GET_DRIVER_TEAM_USE_CASE_TOKEN) private readonly getDriverTeamUseCase: GetDriverTeamUseCase, @Inject(GET_TEAM_MEMBERSHIP_USE_CASE_TOKEN) private readonly getTeamMembershipUseCase: GetTeamMembershipUseCase, @Inject(JOIN_TEAM_USE_CASE_TOKEN) private readonly joinTeamUseCase: JoinTeamUseCase, + @Inject(GET_TEAMS_LEADERBOARD_USE_CASE_TOKEN) private readonly getTeamsLeaderboardUseCase: GetTeamsLeaderboardUseCase, + @Inject(TeamsLeaderboardPresenter) private readonly teamsLeaderboardPresenter: TeamsLeaderboardPresenter, @Inject(LOGGER_TOKEN) private readonly logger: Logger, ) {} @@ -85,6 +92,20 @@ export class TeamService { }; } + async getLeaderboard(): Promise { + this.logger.debug('[TeamService] Fetching teams leaderboard.'); + + const result = await this.getTeamsLeaderboardUseCase.execute({ leagueId: 'global' }); + if (result.isErr()) { + this.logger.error('Error fetching teams leaderboard', new Error(result.unwrapErr().details?.message || 'Unknown error')); + return { teams: [], recruitingCount: 0, groupsBySkillLevel: { beginner: [], intermediate: [], advanced: [], pro: [] }, topTeams: [] }; + } + + this.teamsLeaderboardPresenter.reset(); + this.teamsLeaderboardPresenter.present(result.unwrap()); + return this.teamsLeaderboardPresenter.getViewModel()!; + } + async getDetails(teamId: string, userId?: string): Promise { this.logger.debug(`[TeamService] Fetching team details for teamId: ${teamId}, userId: ${userId}`); diff --git a/apps/api/src/domain/team/TeamTokens.ts b/apps/api/src/domain/team/TeamTokens.ts index 6356a1154..bd4203fb4 100644 --- a/apps/api/src/domain/team/TeamTokens.ts +++ b/apps/api/src/domain/team/TeamTokens.ts @@ -16,4 +16,5 @@ export const CREATE_TEAM_USE_CASE_TOKEN = Symbol('CREATE_TEAM_USE_CASE_TOKEN'); export const UPDATE_TEAM_USE_CASE_TOKEN = Symbol('UPDATE_TEAM_USE_CASE_TOKEN'); export const GET_DRIVER_TEAM_USE_CASE_TOKEN = Symbol('GET_DRIVER_TEAM_USE_CASE_TOKEN'); export const GET_TEAM_MEMBERSHIP_USE_CASE_TOKEN = Symbol('GET_TEAM_MEMBERSHIP_USE_CASE_TOKEN'); -export const JOIN_TEAM_USE_CASE_TOKEN = Symbol('JOIN_TEAM_USE_CASE_TOKEN'); \ No newline at end of file +export const JOIN_TEAM_USE_CASE_TOKEN = Symbol('JOIN_TEAM_USE_CASE_TOKEN'); +export const GET_TEAMS_LEADERBOARD_USE_CASE_TOKEN = Symbol('GET_TEAMS_LEADERBOARD_USE_CASE_TOKEN'); diff --git a/apps/api/src/domain/team/dtos/TeamLeaderboardItemDTO.ts b/apps/api/src/domain/team/dtos/TeamLeaderboardItemDTO.ts index fb0c08860..b0637bba0 100644 --- a/apps/api/src/domain/team/dtos/TeamLeaderboardItemDTO.ts +++ b/apps/api/src/domain/team/dtos/TeamLeaderboardItemDTO.ts @@ -9,6 +9,12 @@ export class TeamLeaderboardItemDTO { @ApiProperty() name!: string; + @ApiProperty() + tag!: string; + + @ApiProperty({ nullable: true }) + logoUrl!: string | null; + @ApiProperty() memberCount!: number; diff --git a/apps/api/src/domain/team/presenters/TeamsLeaderboardPresenter.ts b/apps/api/src/domain/team/presenters/TeamsLeaderboardPresenter.ts index 400fe66fd..c84659288 100644 --- a/apps/api/src/domain/team/presenters/TeamsLeaderboardPresenter.ts +++ b/apps/api/src/domain/team/presenters/TeamsLeaderboardPresenter.ts @@ -14,6 +14,8 @@ export class TeamsLeaderboardPresenter implements UseCaseOutputPort ({ id: item.team.id, name: item.team.name.toString(), + tag: item.team.tag.toString(), + logoUrl: null, // MediaResolver would be needed here memberCount: item.memberCount, rating: item.rating, totalWins: item.totalWins, @@ -28,6 +30,8 @@ export class TeamsLeaderboardPresenter implements UseCaseOutputPort ({ id: item.team.id, name: item.team.name.toString(), + tag: item.team.tag.toString(), + logoUrl: null, memberCount: item.memberCount, rating: item.rating, totalWins: item.totalWins, @@ -40,6 +44,8 @@ export class TeamsLeaderboardPresenter implements UseCaseOutputPort ({ id: item.team.id, name: item.team.name.toString(), + tag: item.team.tag.toString(), + logoUrl: null, memberCount: item.memberCount, rating: item.rating, totalWins: item.totalWins, @@ -52,6 +58,8 @@ export class TeamsLeaderboardPresenter implements UseCaseOutputPort ({ id: item.team.id, name: item.team.name.toString(), + tag: item.team.tag.toString(), + logoUrl: null, memberCount: item.memberCount, rating: item.rating, totalWins: item.totalWins, @@ -64,6 +72,8 @@ export class TeamsLeaderboardPresenter implements UseCaseOutputPort ({ id: item.team.id, name: item.team.name.toString(), + tag: item.team.tag.toString(), + logoUrl: null, memberCount: item.memberCount, rating: item.rating, totalWins: item.totalWins, @@ -77,6 +87,8 @@ export class TeamsLeaderboardPresenter implements UseCaseOutputPort ({ id: item.team.id, name: item.team.name.toString(), + tag: item.team.tag.toString(), + logoUrl: null, memberCount: item.memberCount, rating: item.rating, totalWins: item.totalWins, diff --git a/apps/website/app/leaderboards/page.tsx b/apps/website/app/leaderboards/page.tsx index 18adaf523..297c47eb4 100644 --- a/apps/website/app/leaderboards/page.tsx +++ b/apps/website/app/leaderboards/page.tsx @@ -9,7 +9,7 @@ import { JsonLd } from '@/ui/JsonLd'; export const metadata: Metadata = MetadataHelper.generate({ title: 'Global Leaderboards', - description: 'See who leads the pack on GridPilot. Comprehensive global leaderboards for drivers and teams, featuring performance rankings and career statistics.', + description: 'Global performance rankings for drivers and teams on GridPilot. Comprehensive leaderboards featuring competitive results and career statistics.', path: '/leaderboards', }); diff --git a/apps/website/app/leaderboards/teams/page.tsx b/apps/website/app/leaderboards/teams/page.tsx new file mode 100644 index 000000000..acbe051c6 --- /dev/null +++ b/apps/website/app/leaderboards/teams/page.tsx @@ -0,0 +1,33 @@ +import { notFound, redirect } from 'next/navigation'; +import { TeamRankingsPageQuery } from '@/lib/page-queries/TeamRankingsPageQuery'; +import { TeamRankingsPageClient } from '@/client-wrapper/TeamRankingsPageClient'; +import { routes } from '@/lib/routing/RouteConfig'; +import { logger } from '@/lib/infrastructure/logging/logger'; +import { Metadata } from 'next'; +import { MetadataHelper } from '@/lib/seo/MetadataHelper'; + +export const metadata: Metadata = MetadataHelper.generate({ + title: 'Team Leaderboard', + description: 'Global team rankings on GridPilot. See the top performing sim racing teams and their competitive statistics.', + path: '/leaderboards/teams', +}); + +export default async function TeamLeaderboardPage() { + const result = await TeamRankingsPageQuery.execute(); + + if (result.isErr()) { + const error = result.getError(); + + if (error === 'notFound') { + notFound(); + } else if (error === 'redirect') { + redirect(routes.public.home); + } else { + logger.error('Team rankings error:', undefined, { error }); + notFound(); + } + } + + const viewData = result.unwrap(); + return ; +} diff --git a/apps/website/app/page.tsx b/apps/website/app/page.tsx index 649115143..c0c97d49d 100644 --- a/apps/website/app/page.tsx +++ b/apps/website/app/page.tsx @@ -8,8 +8,8 @@ import { MetadataHelper } from '@/lib/seo/MetadataHelper'; import { JsonLd } from '@/ui/JsonLd'; export const metadata: Metadata = MetadataHelper.generate({ - title: 'Professional iRacing League Management Platform', - description: 'Experience the pinnacle of sim racing organization. GridPilot provides obsessive detail in race management, automated standings, and professional-grade tools for serious iRacing leagues.', + title: 'iRacing League Management Infrastructure', + description: 'Infrastructure for iRacing league management. Automated standings, race management, and stewarding tools.', path: '/', }); diff --git a/apps/website/app/teams/leaderboard/page.tsx b/apps/website/app/teams/leaderboard/page.tsx index d757fb1df..bd3e6bcbb 100644 --- a/apps/website/app/teams/leaderboard/page.tsx +++ b/apps/website/app/teams/leaderboard/page.tsx @@ -1,23 +1,6 @@ -import { notFound } from 'next/navigation'; -import { TeamLeaderboardPageQuery } from '@/lib/page-queries/TeamLeaderboardPageQuery'; -import { TeamLeaderboardPageWrapper } from '@/client-wrapper/TeamLeaderboardPageWrapper'; -import { Metadata } from 'next'; -import { MetadataHelper } from '@/lib/seo/MetadataHelper'; +import { redirect } from 'next/navigation'; +import { routes } from '@/lib/routing/RouteConfig'; -export const metadata: Metadata = MetadataHelper.generate({ - title: 'Team Leaderboard', - description: 'The definitive ranking of sim racing teams on GridPilot. Compare team performance, championship points, and overall standing in the professional iRacing community.', - path: '/teams/leaderboard', -}); - -export default async function TeamLeaderboardPage() { - const query = new TeamLeaderboardPageQuery(); - const result = await query.execute(); - - if (result.isErr()) { - notFound(); - } - - const data = result.unwrap(); - return ; +export default function TeamLeaderboardRedirect() { + redirect(routes.leaderboards.teams); } diff --git a/apps/website/client-wrapper/LeaderboardsPageClient.tsx b/apps/website/client-wrapper/LeaderboardsPageClient.tsx index 05bb29c9a..1dd736991 100644 --- a/apps/website/client-wrapper/LeaderboardsPageClient.tsx +++ b/apps/website/client-wrapper/LeaderboardsPageClient.tsx @@ -23,7 +23,7 @@ export function LeaderboardsPageClient({ viewData }: ClientWrapperProps { - router.push(routes.team.leaderboard); + router.push(routes.leaderboards.teams); }; return ( diff --git a/apps/website/client-wrapper/TeamRankingsPageClient.tsx b/apps/website/client-wrapper/TeamRankingsPageClient.tsx new file mode 100644 index 000000000..8b1f48711 --- /dev/null +++ b/apps/website/client-wrapper/TeamRankingsPageClient.tsx @@ -0,0 +1,39 @@ +'use client'; + +import React, { useState } from 'react'; +import { useRouter } from 'next/navigation'; +import { TeamRankingsTemplate } from '@/templates/TeamRankingsTemplate'; +import { routes } from '@/lib/routing/RouteConfig'; +import type { TeamRankingsViewData } from '@/lib/view-data/TeamRankingsViewData'; +import { ClientWrapperProps } from '@/lib/contracts/components/ComponentContracts'; + +export function TeamRankingsPageClient({ viewData }: ClientWrapperProps) { + const router = useRouter(); + const [searchQuery, setSearchQuery] = useState(''); + + const handleTeamClick = (id: string) => { + router.push(routes.team.detail(id)); + }; + + const handleBackToLeaderboards = () => { + router.push(routes.leaderboards.root); + }; + + const filteredTeams = viewData.teams.filter(team => + team.name.toLowerCase().includes(searchQuery.toLowerCase()) || + team.tag.toLowerCase().includes(searchQuery.toLowerCase()) + ); + + return ( + + ); +} diff --git a/apps/website/components/errors/AppErrorBoundaryView.tsx b/apps/website/components/errors/AppErrorBoundaryView.tsx index 58c5a7fe8..e3f75d2e4 100644 --- a/apps/website/components/errors/AppErrorBoundaryView.tsx +++ b/apps/website/components/errors/AppErrorBoundaryView.tsx @@ -41,7 +41,7 @@ export function AppErrorBoundaryView({ title, description, children }: AppErrorB {title} - + {description} diff --git a/apps/website/components/errors/ErrorDetailsBlock.tsx b/apps/website/components/errors/ErrorDetailsBlock.tsx index 93fe637a9..7c742695a 100644 --- a/apps/website/components/errors/ErrorDetailsBlock.tsx +++ b/apps/website/components/errors/ErrorDetailsBlock.tsx @@ -46,7 +46,7 @@ export function ErrorDetailsBlock({ error }: ErrorDetailsBlockProps) { - + {error.stack || 'No stack trace available'} {error.digest && `\n\nDigest: ${error.digest}`} diff --git a/apps/website/components/home/Hero.tsx b/apps/website/components/home/Hero.tsx index 2a747280d..38ae5c797 100644 --- a/apps/website/components/home/Hero.tsx +++ b/apps/website/components/home/Hero.tsx @@ -11,9 +11,9 @@ export function Hero() { return ( ); diff --git a/apps/website/components/home/ValuePillars.tsx b/apps/website/components/home/ValuePillars.tsx index d8d396d8c..43bd13e0e 100644 --- a/apps/website/components/home/ValuePillars.tsx +++ b/apps/website/components/home/ValuePillars.tsx @@ -24,8 +24,8 @@ export function ValuePillars() { icon: Gavel, }, { - title: "Professional Presence", - description: "A clean, modern home for your league. Schedules, standings, and rosters that build prestige and attract drivers.", + title: "League Identity", + description: "Public schedules, standings, and rosters for league members.", icon: Layout, }, ]; diff --git a/apps/website/components/leaderboards/RankingsTable.tsx b/apps/website/components/leaderboards/RankingsTable.tsx index 53ca2d5ee..fbc688e0c 100644 --- a/apps/website/components/leaderboards/RankingsTable.tsx +++ b/apps/website/components/leaderboards/RankingsTable.tsx @@ -30,7 +30,6 @@ export function RankingsTable({ drivers, onDriverClick }: RankingsTableProps) { if (drivers.length === 0) { return ( - 🔍 No drivers found There are no drivers in the system yet diff --git a/apps/website/components/leaderboards/TeamLeaderboardTable.tsx b/apps/website/components/leaderboards/TeamLeaderboardTable.tsx new file mode 100644 index 000000000..27e958d6a --- /dev/null +++ b/apps/website/components/leaderboards/TeamLeaderboardTable.tsx @@ -0,0 +1,48 @@ +import React from 'react'; +import { TeamRankingRow } from './TeamRankingRow'; +import { LeaderboardTableShell } from '@/ui/LeaderboardTableShell'; + +interface LeaderboardTeam { + id: string; + name: string; + logoUrl?: string; + position: number; + rating: number; + totalWins: number; + totalRaces: number; + memberCount: number; +} + +interface TeamLeaderboardTableProps { + teams: LeaderboardTeam[]; + onTeamClick?: (id: string) => void; +} + +export function TeamLeaderboardTable({ teams, onTeamClick }: TeamLeaderboardTableProps) { + const columns = [ + { key: 'rank', label: 'Rank', width: '8rem' }, + { key: 'team', label: 'Team' }, + { key: 'rating', label: 'Rating', align: 'center' as const }, + { key: 'wins', label: 'Wins', align: 'center' as const }, + { key: 'races', label: 'Races', align: 'center' as const }, + ]; + + return ( + + {teams.map((team) => ( + onTeamClick?.(team.id)} + /> + ))} + + ); +} diff --git a/apps/website/components/leagues/LeagueListItem.tsx b/apps/website/components/leagues/LeagueListItem.tsx index eb06d7d3c..5101f9949 100644 --- a/apps/website/components/leagues/LeagueListItem.tsx +++ b/apps/website/components/leagues/LeagueListItem.tsx @@ -28,7 +28,7 @@ export function LeagueListItem({ league, isAdmin }: LeagueListItemProps) { league.membershipRole && ( Your role:{' '} - {league.membershipRole} + {league.membershipRole} ) } diff --git a/apps/website/components/leagues/LeagueSummaryCard.tsx b/apps/website/components/leagues/LeagueSummaryCard.tsx index 44f97ab69..c45639440 100644 --- a/apps/website/components/leagues/LeagueSummaryCard.tsx +++ b/apps/website/components/leagues/LeagueSummaryCard.tsx @@ -27,21 +27,22 @@ export function LeagueSummaryCard({ href, }: LeagueSummaryCardProps) { return ( - + - + League - + {name} @@ -54,7 +55,7 @@ export function LeagueSummaryCard({ block mb={4} lineClamp={2} - style={{ height: '2.5rem' }} + height="2.5rem" > {description} @@ -62,7 +63,7 @@ export function LeagueSummaryCard({ - + Max Drivers @@ -70,14 +71,14 @@ export function LeagueSummaryCard({ {maxDrivers} - + Format {qualifyingFormat} diff --git a/apps/website/components/shared/Accordion.tsx b/apps/website/components/shared/Accordion.tsx index 88b661fc5..3a5fceb47 100644 --- a/apps/website/components/shared/Accordion.tsx +++ b/apps/website/components/shared/Accordion.tsx @@ -35,7 +35,7 @@ export const Accordion = ({ }; return ( - + - - + + {teamName} - + {role} @@ -56,7 +61,7 @@ export function TeamMembershipCard({ - + ); diff --git a/apps/website/components/teams/TeamsHeader.tsx b/apps/website/components/teams/TeamsHeader.tsx index 1ce88f3c0..1d27d64aa 100644 --- a/apps/website/components/teams/TeamsHeader.tsx +++ b/apps/website/components/teams/TeamsHeader.tsx @@ -18,15 +18,16 @@ export function TeamsHeader({ title, subtitle, action }: TeamsHeaderProps) { alignItems={{ md: 'end' }} justifyContent="space-between" gap={6} - className="border-b border-[var(--ui-color-border-muted)] pb-8" + borderBottom="1px solid var(--ui-color-border-muted)" + paddingBottom={8} > - - - + + + {title} {subtitle && ( - + {subtitle} )} diff --git a/apps/website/lib/api/teams/TeamsApiClient.ts b/apps/website/lib/api/teams/TeamsApiClient.ts index a8dd93c7f..431328ecb 100644 --- a/apps/website/lib/api/teams/TeamsApiClient.ts +++ b/apps/website/lib/api/teams/TeamsApiClient.ts @@ -1,4 +1,5 @@ import type { GetAllTeamsOutputDTO } from '@/lib/types/generated/GetAllTeamsOutputDTO'; +import type { GetTeamsLeaderboardOutputDTO } from '@/lib/types/generated/GetTeamsLeaderboardOutputDTO'; import type { GetTeamDetailsOutputDTO } from '@/lib/types/generated/GetTeamDetailsOutputDTO'; import type { GetTeamMembersOutputDTO } from '@/lib/types/generated/GetTeamMembersOutputDTO'; import type { GetTeamJoinRequestsOutputDTO } from '@/lib/types/generated/GetTeamJoinRequestsOutputDTO'; @@ -21,6 +22,11 @@ export class TeamsApiClient extends BaseApiClient { return this.get('/teams/all'); } + /** Get teams leaderboard */ + getLeaderboard(): Promise { + return this.get('/teams/leaderboard'); + } + /** Get team details */ getDetails(teamId: string): Promise { return this.get(`/teams/${teamId}`); diff --git a/apps/website/lib/builders/view-data/LeaderboardsViewDataBuilder.ts b/apps/website/lib/builders/view-data/LeaderboardsViewDataBuilder.ts index c50fb36b1..b00795ee2 100644 --- a/apps/website/lib/builders/view-data/LeaderboardsViewDataBuilder.ts +++ b/apps/website/lib/builders/view-data/LeaderboardsViewDataBuilder.ts @@ -1,10 +1,10 @@ import type { DriverLeaderboardItemDTO } from '@/lib/types/generated/DriverLeaderboardItemDTO'; -import type { TeamListItemDTO } from '@/lib/types/generated/TeamListItemDTO'; +import type { GetTeamsLeaderboardOutputDTO } from '@/lib/types/generated/GetTeamsLeaderboardOutputDTO'; import type { LeaderboardsViewData } from '@/lib/view-data/LeaderboardsViewData'; export class LeaderboardsViewDataBuilder { static build( - apiDto: { drivers: { drivers: DriverLeaderboardItemDTO[] }; teams: { teams: TeamListItemDTO[] } } + apiDto: { drivers: { drivers: DriverLeaderboardItemDTO[] }; teams: GetTeamsLeaderboardOutputDTO } ): LeaderboardsViewData { return { drivers: apiDto.drivers.drivers.slice(0, 10).map(driver => ({ @@ -18,19 +18,19 @@ export class LeaderboardsViewDataBuilder { avatarUrl: driver.avatarUrl || '', position: driver.rank, })), - teams: apiDto.teams.teams.slice(0, 10).map((team, index) => ({ + teams: apiDto.teams.topTeams.map((team, index) => ({ id: team.id, name: team.name, tag: team.tag, memberCount: team.memberCount, - category: team.category, + category: undefined, totalWins: team.totalWins || 0, logoUrl: team.logoUrl || '', position: index + 1, isRecruiting: team.isRecruiting, performanceLevel: team.performanceLevel || 'N/A', - rating: team.rating, + rating: team.rating || 0, })), }; } -} \ No newline at end of file +} diff --git a/apps/website/lib/builders/view-data/TeamRankingsViewDataBuilder.ts b/apps/website/lib/builders/view-data/TeamRankingsViewDataBuilder.ts new file mode 100644 index 000000000..0c57cfae9 --- /dev/null +++ b/apps/website/lib/builders/view-data/TeamRankingsViewDataBuilder.ts @@ -0,0 +1,27 @@ +import type { GetTeamsLeaderboardOutputDTO } from '@/lib/types/generated/GetTeamsLeaderboardOutputDTO'; +import type { TeamRankingsViewData } from '@/lib/view-data/TeamRankingsViewData'; + +export class TeamRankingsViewDataBuilder { + static build(apiDto: GetTeamsLeaderboardOutputDTO): TeamRankingsViewData { + const allTeams = apiDto.teams.map((team, index) => ({ + id: team.id, + name: team.name, + tag: team.tag, + memberCount: team.memberCount, + category: undefined, + totalWins: team.totalWins || 0, + logoUrl: team.logoUrl || '', + position: index + 1, + isRecruiting: team.isRecruiting, + performanceLevel: team.performanceLevel || 'N/A', + rating: team.rating || 0, + totalRaces: team.totalRaces || 0, + })); + + return { + teams: allTeams, + podium: allTeams.slice(0, 3), + recruitingCount: apiDto.recruitingCount, + }; + } +} diff --git a/apps/website/lib/page-queries/TeamRankingsPageQuery.ts b/apps/website/lib/page-queries/TeamRankingsPageQuery.ts new file mode 100644 index 000000000..28e944eaa --- /dev/null +++ b/apps/website/lib/page-queries/TeamRankingsPageQuery.ts @@ -0,0 +1,31 @@ +import { PageQuery } from '@/lib/contracts/page-queries/PageQuery'; +import { Result } from '@/lib/contracts/Result'; +import { TeamRankingsViewDataBuilder } from '@/lib/builders/view-data/TeamRankingsViewDataBuilder'; +import type { TeamRankingsViewData } from '@/lib/view-data/TeamRankingsViewData'; +import { TeamRankingsService } from '@/lib/services/leaderboards/TeamRankingsService'; +import { mapToPresentationError, type PresentationError } from '@/lib/contracts/page-queries/PresentationError'; + +/** + * Team Rankings page query + * Returns Result + */ +export class TeamRankingsPageQuery implements PageQuery { + async execute(): Promise> { + const service = new TeamRankingsService(); + + const serviceResult = await service.getTeamRankings(); + + if (serviceResult.isErr()) { + return Result.err(mapToPresentationError(serviceResult.getError())); + } + + const apiDto = serviceResult.unwrap(); + const viewData = TeamRankingsViewDataBuilder.build(apiDto); + return Result.ok(viewData); + } + + static async execute(): Promise> { + const query = new TeamRankingsPageQuery(); + return query.execute(); + } +} diff --git a/apps/website/lib/routing/RouteConfig.ts b/apps/website/lib/routing/RouteConfig.ts index 63e47db5f..3c32fb02b 100644 --- a/apps/website/lib/routing/RouteConfig.ts +++ b/apps/website/lib/routing/RouteConfig.ts @@ -93,6 +93,7 @@ export interface RouteGroup { leaderboards: { root: string; drivers: string; + teams: string; }; error: { notFound: string; @@ -119,7 +120,7 @@ export interface RouteGroup { * } * ``` */ -export const routes: RouteGroup & { leaderboards: { root: string; drivers: string } } = { +export const routes: RouteGroup & { leaderboards: { root: string; drivers: string; teams: string } } = { auth: { login: '/auth/login', signup: '/auth/signup', @@ -192,6 +193,7 @@ export const routes: RouteGroup & { leaderboards: { root: string; drivers: strin leaderboards: { root: '/leaderboards', drivers: '/leaderboards/drivers', + teams: '/leaderboards/teams', }, error: { notFound: '/404', diff --git a/apps/website/lib/services/leaderboards/LeaderboardsService.ts b/apps/website/lib/services/leaderboards/LeaderboardsService.ts index 733c70ef2..f59837798 100644 --- a/apps/website/lib/services/leaderboards/LeaderboardsService.ts +++ b/apps/website/lib/services/leaderboards/LeaderboardsService.ts @@ -20,7 +20,7 @@ export class LeaderboardsService implements Service { const [driverResult, teamResult] = await Promise.all([ driversApiClient.getLeaderboard(), - teamsApiClient.getAll() + teamsApiClient.getLeaderboard() ]); if (!driverResult) { diff --git a/apps/website/lib/services/leaderboards/TeamRankingsService.ts b/apps/website/lib/services/leaderboards/TeamRankingsService.ts new file mode 100644 index 000000000..2d36f23ec --- /dev/null +++ b/apps/website/lib/services/leaderboards/TeamRankingsService.ts @@ -0,0 +1,50 @@ +import { TeamsApiClient } from '@/lib/api/teams/TeamsApiClient'; +import { Result } from '@/lib/contracts/Result'; +import { Service, DomainError } from '@/lib/contracts/services/Service'; +import { ConsoleErrorReporter } from '@/lib/infrastructure/logging/ConsoleErrorReporter'; +import { ConsoleLogger } from '@/lib/infrastructure/logging/ConsoleLogger'; +import { getWebsiteApiBaseUrl } from '@/lib/config/apiBaseUrl'; +import { ApiError } from '@/lib/api/base/ApiError'; +import type { GetTeamsLeaderboardOutputDTO } from '@/lib/types/generated/GetTeamsLeaderboardOutputDTO'; + +export class TeamRankingsService implements Service { + async getTeamRankings(): Promise> { + try { + const baseUrl = getWebsiteApiBaseUrl(); + const errorReporter = new ConsoleErrorReporter(); + const logger = new ConsoleLogger(); + + const teamsApiClient = new TeamsApiClient(baseUrl, errorReporter, logger); + + const teamResult = await teamsApiClient.getLeaderboard(); + + if (!teamResult) { + return Result.err({ type: 'notFound', message: 'No team leaderboard data available' }); + } + + return Result.ok(teamResult); + } catch (error) { + if (error instanceof ApiError) { + switch (error.type) { + case 'NOT_FOUND': + return Result.err({ type: 'notFound', message: error.message }); + case 'AUTH_ERROR': + return Result.err({ type: 'unauthorized', message: error.message }); + case 'SERVER_ERROR': + return Result.err({ type: 'serverError', message: error.message }); + case 'NETWORK_ERROR': + case 'TIMEOUT_ERROR': + return Result.err({ type: 'networkError', message: error.message }); + default: + return Result.err({ type: 'unknown', message: error.message }); + } + } + + if (error instanceof Error) { + return Result.err({ type: 'unknown', message: error.message }); + } + + return Result.err({ type: 'unknown', message: 'Team rankings fetch failed' }); + } + } +} diff --git a/apps/website/lib/types/LeaderboardsData.ts b/apps/website/lib/types/LeaderboardsData.ts index 344dc75a7..ab20a7970 100644 --- a/apps/website/lib/types/LeaderboardsData.ts +++ b/apps/website/lib/types/LeaderboardsData.ts @@ -1,7 +1,7 @@ import type { DriverLeaderboardItemDTO } from '@/lib/types/generated/DriverLeaderboardItemDTO'; -import type { TeamListItemDTO } from '@/lib/types/generated/TeamListItemDTO'; +import type { GetTeamsLeaderboardOutputDTO } from '@/lib/types/generated/GetTeamsLeaderboardOutputDTO'; export interface LeaderboardsData { drivers: { drivers: DriverLeaderboardItemDTO[] }; - teams: { teams: TeamListItemDTO[] }; -} \ No newline at end of file + teams: GetTeamsLeaderboardOutputDTO; +} diff --git a/apps/website/lib/types/generated/TeamLeaderboardItemDTO.ts b/apps/website/lib/types/generated/TeamLeaderboardItemDTO.ts index aee48616c..908cfe013 100644 --- a/apps/website/lib/types/generated/TeamLeaderboardItemDTO.ts +++ b/apps/website/lib/types/generated/TeamLeaderboardItemDTO.ts @@ -8,6 +8,8 @@ export interface TeamLeaderboardItemDTO { id: string; name: string; + tag: string; + logoUrl?: string; memberCount: number; rating?: number; totalWins: number; diff --git a/apps/website/lib/view-data/LeaderboardTeamItem.ts b/apps/website/lib/view-data/LeaderboardTeamItem.ts index 4cf2bcbe1..bd670e668 100644 --- a/apps/website/lib/view-data/LeaderboardTeamItem.ts +++ b/apps/website/lib/view-data/LeaderboardTeamItem.ts @@ -5,9 +5,10 @@ export interface LeaderboardTeamItem { memberCount: number; category?: string; totalWins: number; + totalRaces?: number; logoUrl: string; position: number; isRecruiting: boolean; performanceLevel: string; rating?: number; -} \ No newline at end of file +} diff --git a/apps/website/lib/view-data/TeamRankingsViewData.ts b/apps/website/lib/view-data/TeamRankingsViewData.ts new file mode 100644 index 000000000..642a3f7ae --- /dev/null +++ b/apps/website/lib/view-data/TeamRankingsViewData.ts @@ -0,0 +1,7 @@ +import type { LeaderboardTeamItem } from './LeaderboardTeamItem'; + +export interface TeamRankingsViewData { + teams: LeaderboardTeamItem[]; + podium: LeaderboardTeamItem[]; + recruitingCount: number; +} diff --git a/apps/website/templates/DriverRankingsTemplate.tsx b/apps/website/templates/DriverRankingsTemplate.tsx index 601791114..c8a7ef840 100644 --- a/apps/website/templates/DriverRankingsTemplate.tsx +++ b/apps/website/templates/DriverRankingsTemplate.tsx @@ -5,7 +5,7 @@ import { Trophy, ChevronLeft } from 'lucide-react'; import { Container } from '@/ui/Container'; import { PageHeader } from '@/ui/PageHeader'; import { RankingsPodium } from '@/components/leaderboards/RankingsPodium'; -import { RankingsTable } from '@/components/leaderboards/RankingsTable'; +import { LeaderboardTable } from '@/components/leaderboards/LeaderboardTable'; import { Button } from '@/ui/Button'; import { Icon } from '@/ui/Icon'; import { LeaderboardFiltersBar } from '@/components/leaderboards/LeaderboardFiltersBar'; @@ -65,11 +65,13 @@ export function DriverRankingsTemplate({ /> {/* Leaderboard Table */} - ({ ...d, rating: Number(d.rating), - wins: Number(d.wins) + wins: Number(d.wins), + racesCompleted: d.racesCompleted || 0, + avatarUrl: d.avatarUrl || '' }))} onDriverClick={onDriverClick} /> diff --git a/apps/website/templates/DriversTemplate.tsx b/apps/website/templates/DriversTemplate.tsx index 12f09cb65..8ea8c1899 100644 --- a/apps/website/templates/DriversTemplate.tsx +++ b/apps/website/templates/DriversTemplate.tsx @@ -31,7 +31,6 @@ export function DriversTemplate({ return (
({ + ...t, + logoUrl: t.logoUrl || '' + }))} onTeamClick={onTeamClick} - onViewFullLeaderboard={onNavigateToTeams} + onNavigateToTeams={onNavigateToTeams} /> diff --git a/apps/website/templates/TeamRankingsTemplate.tsx b/apps/website/templates/TeamRankingsTemplate.tsx new file mode 100644 index 000000000..795f16550 --- /dev/null +++ b/apps/website/templates/TeamRankingsTemplate.tsx @@ -0,0 +1,63 @@ +'use client'; + +import React from 'react'; +import { Users, ChevronLeft } from 'lucide-react'; +import { Container } from '@/ui/Container'; +import { PageHeader } from '@/ui/PageHeader'; +import { TeamLeaderboardTable } from '@/components/leaderboards/TeamLeaderboardTable'; +import { Button } from '@/ui/Button'; +import { Icon } from '@/ui/Icon'; +import { LeaderboardFiltersBar } from '@/components/leaderboards/LeaderboardFiltersBar'; +import type { TeamRankingsViewData } from '@/lib/view-data/TeamRankingsViewData'; + +interface TeamRankingsTemplateProps { + viewData: TeamRankingsViewData; + searchQuery: string; + onSearchChange: (query: string) => void; + onTeamClick?: (id: string) => void; + onBackToLeaderboards?: () => void; +} + +export function TeamRankingsTemplate({ + viewData, + searchQuery, + onSearchChange, + onTeamClick, + onBackToLeaderboards, +}: TeamRankingsTemplateProps): React.ReactElement { + return ( + + } + > + Back to Leaderboards + + ) + } + /> + + + + ({ + ...t, + totalRaces: t.totalRaces || 0, + rating: t.rating || 0 + }))} + onTeamClick={onTeamClick} + /> + + ); +} diff --git a/apps/website/templates/layout/GlobalSidebarTemplate.tsx b/apps/website/templates/layout/GlobalSidebarTemplate.tsx index 638fa3b8d..3a1c44539 100644 --- a/apps/website/templates/layout/GlobalSidebarTemplate.tsx +++ b/apps/website/templates/layout/GlobalSidebarTemplate.tsx @@ -24,11 +24,9 @@ export function GlobalSidebarTemplate(_props: GlobalSidebarViewData) { position="sticky" top="56px" height="calc(100vh - 56px)" - style={{ - borderRight: '1px solid var(--ui-color-border-default)', - boxShadow: 'inset -1px 0 0 0 rgba(255, 255, 255, 0.01)', - background: 'linear-gradient(180deg, #0d0d0e 0%, #0a0a0b 100%)', - }} + borderRight={true} + shadow="inset -1px 0 0 0 rgba(255, 255, 255, 0.01)" + bg="linear-gradient(180deg, #0d0d0e 0%, #0a0a0b 100%)" > diff --git a/apps/website/ui/Badge.tsx b/apps/website/ui/Badge.tsx index 5371e2910..bb7aca9b9 100644 --- a/apps/website/ui/Badge.tsx +++ b/apps/website/ui/Badge.tsx @@ -8,12 +8,17 @@ export interface BadgeProps { children: ReactNode; variant?: 'primary' | 'secondary' | 'success' | 'warning' | 'critical' | 'info' | 'outline' | 'default' | 'danger'; size?: 'xs' | 'sm' | 'md'; - style?: React.CSSProperties; icon?: LucideIcon; rounded?: string; bg?: string; color?: string; borderColor?: string; + transform?: 'none' | 'capitalize' | 'uppercase' | 'lowercase' | string; +} + +/** @internal */ +interface InternalBadgeProps extends BadgeProps { + style?: React.CSSProperties; } export const Badge = ({ @@ -25,8 +30,9 @@ export const Badge = ({ rounded, bg, color, - borderColor -}: BadgeProps) => { + borderColor, + transform +}: InternalBadgeProps) => { const variantClasses = { primary: 'bg-[var(--ui-color-intent-primary)] text-white', secondary: 'bg-[var(--ui-color-bg-surface)] text-[var(--ui-color-text-med)] border border-[var(--ui-color-border-default)]', @@ -50,6 +56,8 @@ export const Badge = ({ variantClasses[variant], sizeClasses[size], rounded ? (rounded === 'full' ? 'rounded-full' : `rounded-${rounded}`) : 'rounded-none', + bg?.startsWith('bg-') ? bg : '', + color?.startsWith('text-') ? color : '', ].join(' '); const style: React.CSSProperties = { @@ -57,6 +65,7 @@ export const Badge = ({ ...(bg ? { backgroundColor: bg.startsWith('bg-') ? undefined : bg } : {}), ...(color ? { color: color.startsWith('text-') ? undefined : color } : {}), ...(borderColor ? { borderColor: borderColor.startsWith('border-') ? undefined : borderColor, borderStyle: 'solid', borderWidth: '1px' } : {}), + ...(transform ? { textTransform: transform as any } : {}), }; const content = icon ? ( diff --git a/apps/website/ui/Box.tsx b/apps/website/ui/Box.tsx index bc74d9b06..ae619dfb2 100644 --- a/apps/website/ui/Box.tsx +++ b/apps/website/ui/Box.tsx @@ -120,8 +120,6 @@ export interface BoxProps { role?: React.AriaRole; tabIndex?: number; // Internal use only - style?: React.CSSProperties; - className?: string; borderTop?: string | boolean; borderBottom?: string | boolean; borderLeft?: string | boolean; @@ -140,6 +138,7 @@ export interface BoxProps { groupHoverWidth?: string; animate?: any; blur?: string; + backdropBlur?: string; pointerEvents?: string; bgOpacity?: number; borderWidth?: string | number; @@ -157,6 +156,10 @@ export interface BoxProps { translateX?: string; translateY?: string; translate?: string; + rotate?: string; + scale?: string | number; + perspective?: string | number; + whiteSpace?: 'normal' | 'nowrap' | 'pre' | 'pre-line' | 'pre-wrap'; cursor?: string; fontSize?: string | ResponsiveValue; fontWeight?: string | number; @@ -230,6 +233,10 @@ export interface BoxProps { onPointerDown?: React.PointerEventHandler; onPointerMove?: React.PointerEventHandler; onPointerUp?: React.PointerEventHandler; + /** @deprecated DO NOT USE. Use semantic props instead. */ + className?: string; + /** @deprecated DO NOT USE. Use semantic props instead. */ + style?: React.CSSProperties; } export const Box = forwardRef(( @@ -292,6 +299,7 @@ export const Box = forwardRef(( groupHoverWidth, animate, blur, + backdropBlur, pointerEvents, bgOpacity, borderWidth, @@ -309,6 +317,10 @@ export const Box = forwardRef(( translateX, translateY, translate, + rotate, + scale, + perspective, + whiteSpace, cursor, fontSize, fontWeight, @@ -483,11 +495,15 @@ export const Box = forwardRef(( group ? 'group' : '', animate === 'spin' ? 'animate-spin' : (animate === 'pulse' ? 'animate-pulse' : ''), blur ? `blur-${blur}` : '', + backdropBlur ? `backdrop-blur-${backdropBlur}` : '', pointerEvents ? `pointer-events-${pointerEvents}` : '', hideScrollbar ? 'scrollbar-hide' : '', truncate ? 'truncate' : '', clickable ? 'cursor-pointer' : '', lineClamp ? `line-clamp-${lineClamp}` : '', + (bg || backgroundColor)?.startsWith('bg-') ? (bg || backgroundColor) : '', + borderColor?.startsWith('border-') ? borderColor : '', + color?.startsWith('text-') ? color : '', className ].filter(Boolean).join(' '); @@ -521,6 +537,13 @@ export const Box = forwardRef(( ...(weight ? { fontWeight: weight } : {}), ...(shadow ? { boxShadow: shadow.startsWith('shadow-') ? undefined : shadow } : {}), ...(transform === true ? { transform: 'auto' } : (typeof transform === 'string' ? { transform } : {})), + ...(translateX ? { translateX } : {}), + ...(translateY ? { translateY } : {}), + ...(translate ? { translate } : {}), + ...(rotate ? { rotate } : {}), + ...(scale ? { scale } : {}), + ...(perspective ? { perspective } : {}), + ...(whiteSpace ? { whiteSpace } : {}), ...(typeof fill === 'string' ? { fill } : (fill === true ? { fill: 'currentColor' } : {})), ...(stroke ? { stroke } : {}), ...(strokeWidth ? { strokeWidth } : {}), diff --git a/apps/website/ui/ControlBar.tsx b/apps/website/ui/ControlBar.tsx index aa4163512..0aca51c53 100644 --- a/apps/website/ui/ControlBar.tsx +++ b/apps/website/ui/ControlBar.tsx @@ -19,10 +19,8 @@ export const ControlBar = ({ children, leftContent }: ControlBarProps) => { zIndex={100} borderBottom={true} backgroundColor="rgba(10, 10, 11, 0.92)" - className="backdrop-blur-xl" - style={{ - boxShadow: '0 1px 0 0 rgba(255, 255, 255, 0.05), 0 10px 30px rgba(0, 0, 0, 0.35)', - }} + backdropBlur="xl" + shadow="0 1px 0 0 rgba(255, 255, 255, 0.05), 0 10px 30px rgba(0, 0, 0, 0.35)" > {leftContent && ( diff --git a/apps/website/ui/ErrorPageContainer.tsx b/apps/website/ui/ErrorPageContainer.tsx index d224a7a49..3905b7d7e 100644 --- a/apps/website/ui/ErrorPageContainer.tsx +++ b/apps/website/ui/ErrorPageContainer.tsx @@ -26,7 +26,16 @@ export const ErrorPageContainer = ({ children, size = 'md', variant = 'default' position="relative" overflow="hidden" > - + {children} diff --git a/apps/website/ui/HorizontalStatItem.tsx b/apps/website/ui/HorizontalStatItem.tsx index 1ebb5a556..79478ab83 100644 --- a/apps/website/ui/HorizontalStatItem.tsx +++ b/apps/website/ui/HorizontalStatItem.tsx @@ -17,7 +17,7 @@ export const HorizontalStatItem = ({ return ( {label} - {value} + {value} ); }; diff --git a/apps/website/ui/MiniStat.tsx b/apps/website/ui/MiniStat.tsx index bf927415c..6402b9fd9 100644 --- a/apps/website/ui/MiniStat.tsx +++ b/apps/website/ui/MiniStat.tsx @@ -16,8 +16,8 @@ export const MiniStat = ({ }: MiniStatProps) => { return ( - {value} - {label} + {value} + {label} ); }; diff --git a/apps/website/ui/PageHeader.tsx b/apps/website/ui/PageHeader.tsx index 48b8d7b7c..f819eb016 100644 --- a/apps/website/ui/PageHeader.tsx +++ b/apps/website/ui/PageHeader.tsx @@ -10,40 +10,44 @@ import { Surface } from './Surface'; import { Text } from './Text'; interface PageHeaderProps { - icon: LucideIcon; title: string; description?: string; action?: React.ReactNode; - iconGradient?: string; - iconBorder?: string; } export function PageHeader({ - icon, title, description, action, - iconGradient = 'from-iron-gray to-deep-graphite', - iconBorder = 'border-charcoal-outline', }: PageHeaderProps) { return ( - - - - - - - - - {title} - {description && ( - {description} - )} - - + + + + + {title} - {action && {action}} - + {description && ( + + {description} + + )} + + + {action && ( + + {action} + + )} ); } diff --git a/apps/website/ui/RaceSummary.tsx b/apps/website/ui/RaceSummary.tsx index 156a91063..de08a9d5d 100644 --- a/apps/website/ui/RaceSummary.tsx +++ b/apps/website/ui/RaceSummary.tsx @@ -15,7 +15,7 @@ export const RaceSummary = ({ track, meta, date }: RaceSummaryProps) => { {meta} - + {date} diff --git a/apps/website/ui/Section.tsx b/apps/website/ui/Section.tsx index 66b2dddf1..4830786eb 100644 --- a/apps/website/ui/Section.tsx +++ b/apps/website/ui/Section.tsx @@ -8,6 +8,9 @@ export interface SectionProps { id?: string; minHeight?: string; py?: number; + fullWidth?: boolean; + maxWidth?: string; + /** @deprecated DO NOT USE. Use semantic props instead. */ className?: string; } @@ -18,7 +21,9 @@ export const Section = ({ id, minHeight, py, - className + className, + fullWidth = false, + maxWidth = '80rem' }: SectionProps) => { const variantClasses = { default: 'bg-[var(--ui-color-bg-base)]', @@ -44,9 +49,13 @@ export const Section = ({ ...(minHeight ? { minHeight } : {}), ...(py !== undefined ? { paddingTop: `${py * 0.25}rem`, paddingBottom: `${py * 0.25}rem` } : {}) }}> - - {children} - + {fullWidth ? ( + children + ) : ( + + {children} + + )} ); }; diff --git a/apps/website/ui/Text.tsx b/apps/website/ui/Text.tsx index 2ede0f207..ebc1d9035 100644 --- a/apps/website/ui/Text.tsx +++ b/apps/website/ui/Text.tsx @@ -15,8 +15,6 @@ export interface TextProps { leading?: 'none' | 'tight' | 'snug' | 'normal' | 'relaxed' | 'loose' | any; block?: boolean; truncate?: boolean; - className?: string; - style?: CSSProperties; mt?: number | any; mb?: number | any; ml?: number | any; @@ -33,6 +31,9 @@ export interface TextProps { display?: string | ResponsiveValue; opacity?: number; maxWidth?: string | number; + maxHeight?: string | number; + overflow?: string; + whiteSpace?: 'normal' | 'nowrap' | 'pre' | 'pre-line' | 'pre-wrap'; mx?: string | number; pl?: number; px?: number; @@ -55,8 +56,13 @@ export interface TextProps { gap?: number; cursor?: string; width?: string | number; + height?: string | number; htmlFor?: string; - transform?: string; + transform?: 'none' | 'capitalize' | 'uppercase' | 'lowercase' | string; + /** @deprecated DO NOT USE. Use semantic props instead. */ + className?: string; + /** @deprecated DO NOT USE. Use semantic props instead. */ + style?: CSSProperties; } /** @@ -93,6 +99,9 @@ export const Text = forwardRef(({ display, opacity, maxWidth, + maxHeight, + overflow, + whiteSpace, mx, pl, px, @@ -115,6 +124,7 @@ export const Text = forwardRef(({ gap, cursor, width, + height, htmlFor, transform, }, ref) => { @@ -200,6 +210,7 @@ export const Text = forwardRef(({ italic ? 'italic' : '', animate === 'pulse' ? 'animate-pulse' : '', capitalize ? 'capitalize' : '', + color?.startsWith('text-') ? color : '', className, ].filter(Boolean).join(' '); @@ -210,8 +221,12 @@ export const Text = forwardRef(({ ...(gap !== undefined ? { gap: `${gap * 0.25}rem` } : {}), ...(cursor ? { cursor } : {}), ...(width !== undefined ? { width } : {}), + ...(height !== undefined ? { height } : {}), ...(opacity !== undefined ? { opacity } : {}), ...(maxWidth !== undefined ? { maxWidth } : {}), + ...(maxHeight !== undefined ? { maxHeight } : {}), + ...(overflow !== undefined ? { overflow } : {}), + ...(whiteSpace !== undefined ? { whiteSpace } : {}), ...(mx === 'auto' ? { marginLeft: 'auto', marginRight: 'auto' } : {}), ...(pl !== undefined ? { paddingLeft: `${pl * 0.25}rem` } : {}), ...(px !== undefined ? { paddingLeft: `${px * 0.25}rem`, paddingRight: `${px * 0.25}rem` } : {}), @@ -224,7 +239,7 @@ export const Text = forwardRef(({ ...(mr !== undefined ? { marginRight: typeof mr === 'number' ? `${mr * 0.25}rem` : mr } : {}), ...(marginTop !== undefined ? { marginTop: typeof marginTop === 'number' ? `${marginTop * 0.25}rem` : marginTop } : {}), ...(marginBottom !== undefined ? { marginBottom: typeof marginBottom === 'number' ? `${marginBottom * 0.25}rem` : marginBottom } : {}), - ...(color ? { color } : {}), + ...(color ? { color: color.startsWith('text-') ? undefined : color } : {}), ...(letterSpacing ? { letterSpacing } : {}), ...(lineHeight ? { lineHeight } : {}), ...(flexGrow !== undefined ? { flexGrow } : {}), diff --git a/apps/website/ui/Toast.tsx b/apps/website/ui/Toast.tsx index 0ba19faa1..0d29f7026 100644 --- a/apps/website/ui/Toast.tsx +++ b/apps/website/ui/Toast.tsx @@ -35,17 +35,15 @@ export const Toast = ({ return ( - + {progress !== undefined && ( - + )}