website refactor
This commit is contained in:
@@ -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<GetTeamsLeaderboardOutputDTO> {
|
||||
return await this.teamService.getLeaderboard();
|
||||
}
|
||||
|
||||
@Public()
|
||||
@Get(':teamId')
|
||||
@ApiOperation({ summary: 'Get team details' })
|
||||
|
||||
@@ -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],
|
||||
},
|
||||
];
|
||||
|
||||
@@ -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
|
||||
);
|
||||
});
|
||||
|
||||
@@ -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<GetTeamsLeaderboardOutputDTO> {
|
||||
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<GetTeamDetailsOutputDTO | null> {
|
||||
this.logger.debug(`[TeamService] Fetching team details for teamId: ${teamId}, userId: ${userId}`);
|
||||
|
||||
|
||||
@@ -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');
|
||||
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');
|
||||
|
||||
@@ -9,6 +9,12 @@ export class TeamLeaderboardItemDTO {
|
||||
@ApiProperty()
|
||||
name!: string;
|
||||
|
||||
@ApiProperty()
|
||||
tag!: string;
|
||||
|
||||
@ApiProperty({ nullable: true })
|
||||
logoUrl!: string | null;
|
||||
|
||||
@ApiProperty()
|
||||
memberCount!: number;
|
||||
|
||||
|
||||
@@ -14,6 +14,8 @@ export class TeamsLeaderboardPresenter implements UseCaseOutputPort<GetTeamsLead
|
||||
teams: result.items.map(item => ({
|
||||
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<GetTeamsLead
|
||||
beginner: result.groupsBySkillLevel.beginner.map(item => ({
|
||||
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<GetTeamsLead
|
||||
intermediate: result.groupsBySkillLevel.intermediate.map(item => ({
|
||||
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<GetTeamsLead
|
||||
advanced: result.groupsBySkillLevel.advanced.map(item => ({
|
||||
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<GetTeamsLead
|
||||
pro: result.groupsBySkillLevel.pro.map(item => ({
|
||||
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<GetTeamsLead
|
||||
topTeams: result.topItems.map(item => ({
|
||||
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,
|
||||
|
||||
@@ -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',
|
||||
});
|
||||
|
||||
|
||||
33
apps/website/app/leaderboards/teams/page.tsx
Normal file
33
apps/website/app/leaderboards/teams/page.tsx
Normal file
@@ -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 <TeamRankingsPageClient viewData={viewData} />;
|
||||
}
|
||||
@@ -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: '/',
|
||||
});
|
||||
|
||||
|
||||
@@ -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 <TeamLeaderboardPageWrapper viewData={data} />;
|
||||
export default function TeamLeaderboardRedirect() {
|
||||
redirect(routes.leaderboards.teams);
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@ export function LeaderboardsPageClient({ viewData }: ClientWrapperProps<Leaderbo
|
||||
};
|
||||
|
||||
const handleNavigateToTeams = () => {
|
||||
router.push(routes.team.leaderboard);
|
||||
router.push(routes.leaderboards.teams);
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
39
apps/website/client-wrapper/TeamRankingsPageClient.tsx
Normal file
39
apps/website/client-wrapper/TeamRankingsPageClient.tsx
Normal file
@@ -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<TeamRankingsViewData>) {
|
||||
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 (
|
||||
<TeamRankingsTemplate
|
||||
viewData={{
|
||||
...viewData,
|
||||
teams: filteredTeams
|
||||
}}
|
||||
searchQuery={searchQuery}
|
||||
onSearchChange={setSearchQuery}
|
||||
onTeamClick={handleTeamClick}
|
||||
onBackToLeaderboards={handleBackToLeaderboards}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -41,7 +41,7 @@ export function AppErrorBoundaryView({ title, description, children }: AppErrorB
|
||||
<Heading level={1} weight="bold">
|
||||
{title}
|
||||
</Heading>
|
||||
<Text variant="low" align="center" style={{ maxWidth: '32rem' }} leading="relaxed">
|
||||
<Text variant="low" align="center" maxWidth="32rem" leading="relaxed">
|
||||
{description}
|
||||
</Text>
|
||||
</Stack>
|
||||
|
||||
@@ -46,7 +46,7 @@ export function ErrorDetailsBlock({ error }: ErrorDetailsBlockProps) {
|
||||
<Accordion title="Technical Logs">
|
||||
<Stack gap={4}>
|
||||
<Card variant="outline">
|
||||
<Text font="mono" size="xs" variant="low" block leading="relaxed" style={{ maxHeight: '12rem', overflow: 'auto' }}>
|
||||
<Text font="mono" size="xs" variant="low" block leading="relaxed" maxHeight="12rem" overflow="auto">
|
||||
{error.stack || 'No stack trace available'}
|
||||
{error.digest && `\n\nDigest: ${error.digest}`}
|
||||
</Text>
|
||||
|
||||
@@ -11,9 +11,9 @@ export function Hero() {
|
||||
return (
|
||||
<LandingHero
|
||||
subtitle="Sim Racing Infrastructure"
|
||||
title="Professional League Management. Engineered for Control."
|
||||
description="GridPilot eliminates the administrative overhead of running iRacing leagues. No spreadsheets. No manual points. No protest chaos. Just pure competition, structured for growth."
|
||||
primaryAction={{ label: 'Create Your League', href: '#' }}
|
||||
title="League Management Infrastructure."
|
||||
description="Automated results, standings, and stewarding for iRacing leagues. No manual data entry. Structured race management."
|
||||
primaryAction={{ label: 'Create League', href: '#' }}
|
||||
secondaryAction={{ label: 'View Demo', href: '#' }}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
];
|
||||
|
||||
@@ -30,7 +30,6 @@ export function RankingsTable({ drivers, onDriverClick }: RankingsTableProps) {
|
||||
if (drivers.length === 0) {
|
||||
return (
|
||||
<Stack py={16} align="center" bg="bg-iron-gray/30" border borderColor="border-charcoal-outline" rounded="xl">
|
||||
<Text size="4xl" block mb={4}>🔍</Text>
|
||||
<Text color="text-gray-400" block mb={2}>No drivers found</Text>
|
||||
<Text size="sm" color="text-gray-500">There are no drivers in the system yet</Text>
|
||||
</Stack>
|
||||
|
||||
@@ -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 (
|
||||
<LeaderboardTableShell columns={columns}>
|
||||
{teams.map((team) => (
|
||||
<TeamRankingRow
|
||||
key={team.id}
|
||||
rank={team.position}
|
||||
id={team.id}
|
||||
name={team.name}
|
||||
logoUrl={team.logoUrl}
|
||||
rating={team.rating}
|
||||
wins={team.totalWins}
|
||||
races={team.totalRaces}
|
||||
memberCount={team.memberCount}
|
||||
onClick={() => onTeamClick?.(team.id)}
|
||||
/>
|
||||
))}
|
||||
</LeaderboardTableShell>
|
||||
);
|
||||
}
|
||||
@@ -28,7 +28,7 @@ export function LeagueListItem({ league, isAdmin }: LeagueListItemProps) {
|
||||
league.membershipRole && (
|
||||
<Text size="xs" variant="low">
|
||||
Your role:{' '}
|
||||
<Text as="span" variant="med" style={{ textTransform: 'capitalize' }}>{league.membershipRole}</Text>
|
||||
<Text as="span" variant="med" transform="capitalize">{league.membershipRole}</Text>
|
||||
</Text>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -27,21 +27,22 @@ export function LeagueSummaryCard({
|
||||
href,
|
||||
}: LeagueSummaryCardProps) {
|
||||
return (
|
||||
<Card p={0} style={{ overflow: 'hidden' }}>
|
||||
<Card p={0} overflow="hidden">
|
||||
<Stack p={4}>
|
||||
<Stack direction="row" align="center" gap={4} mb={4}>
|
||||
<LeagueLogo leagueId={id} alt={name} size={56} />
|
||||
<Stack style={{ flex: 1, minWidth: 0 }}>
|
||||
<Stack flex={1} minWidth="0">
|
||||
<Text
|
||||
size="xs"
|
||||
color="text-gray-500"
|
||||
style={{ textTransform: 'uppercase', letterSpacing: '0.05em' }}
|
||||
transform="uppercase"
|
||||
letterSpacing="0.05em"
|
||||
block
|
||||
mb={0.5}
|
||||
>
|
||||
League
|
||||
</Text>
|
||||
<Heading level={3} style={{ fontSize: '1rem' }}>
|
||||
<Heading level={3} fontSize="1rem">
|
||||
{name}
|
||||
</Heading>
|
||||
</Stack>
|
||||
@@ -54,7 +55,7 @@ export function LeagueSummaryCard({
|
||||
block
|
||||
mb={4}
|
||||
lineClamp={2}
|
||||
style={{ height: '2.5rem' }}
|
||||
height="2.5rem"
|
||||
>
|
||||
{description}
|
||||
</Text>
|
||||
@@ -62,7 +63,7 @@ export function LeagueSummaryCard({
|
||||
|
||||
<Stack mb={4}>
|
||||
<Grid cols={2} gap={3}>
|
||||
<Card variant="outline" rounded="lg" p={3} className="bg-graphite-black">
|
||||
<Card variant="outline" rounded="lg" p={3} bg="bg-graphite-black">
|
||||
<Text size="xs" color="text-gray-500" block mb={1}>
|
||||
Max Drivers
|
||||
</Text>
|
||||
@@ -70,14 +71,14 @@ export function LeagueSummaryCard({
|
||||
{maxDrivers}
|
||||
</Text>
|
||||
</Card>
|
||||
<Card variant="outline" rounded="lg" p={3} className="bg-graphite-black">
|
||||
<Card variant="outline" rounded="lg" p={3} bg="bg-graphite-black">
|
||||
<Text size="xs" color="text-gray-500" block mb={1}>
|
||||
Format
|
||||
</Text>
|
||||
<Text
|
||||
weight="medium"
|
||||
color="text-white"
|
||||
style={{ textTransform: 'capitalize' }}
|
||||
transform="capitalize"
|
||||
>
|
||||
{qualifyingFormat}
|
||||
</Text>
|
||||
|
||||
@@ -35,7 +35,7 @@ export const Accordion = ({
|
||||
};
|
||||
|
||||
return (
|
||||
<Surface variant="muted" rounded="lg" style={{ border: '1px solid var(--ui-color-border-default)', overflow: 'hidden' }}>
|
||||
<Surface variant="muted" rounded="lg" border={true} overflow="hidden">
|
||||
<Box
|
||||
as="button"
|
||||
onClick={handleToggle}
|
||||
|
||||
@@ -88,16 +88,14 @@ export const Modal = ({
|
||||
variant="default"
|
||||
rounded="lg"
|
||||
shadow="xl"
|
||||
style={{
|
||||
width: '100%',
|
||||
maxWidth: sizeMap[size],
|
||||
maxHeight: '90vh',
|
||||
overflow: 'hidden',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
position: 'relative',
|
||||
border: '1px solid var(--ui-color-border-default)'
|
||||
}}
|
||||
width="100%"
|
||||
maxWidth={sizeMap[size]}
|
||||
maxHeight="90vh"
|
||||
overflow="hidden"
|
||||
display="flex"
|
||||
flexDirection="column"
|
||||
position="relative"
|
||||
border={true}
|
||||
>
|
||||
<Box
|
||||
display="flex"
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
|
||||
|
||||
import { Badge } from '@/ui/Badge';
|
||||
import { Icon } from '@/ui/Icon';
|
||||
import { Link } from '@/ui/Link';
|
||||
@@ -28,8 +26,14 @@ export function TeamMembershipCard({
|
||||
padding={4}
|
||||
rounded="xl"
|
||||
border
|
||||
style={{ borderColor: 'rgba(38, 38, 38, 0.8)' }}
|
||||
className="flex items-center gap-4 hover:border-purple-400/30 hover:bg-iron-gray/50 transition-all group"
|
||||
borderColor="rgba(38, 38, 38, 0.8)"
|
||||
display="flex"
|
||||
alignItems="center"
|
||||
gap={4}
|
||||
hoverBorderColor="rgba(168, 85, 247, 0.3)"
|
||||
hoverBg="iron-gray/50"
|
||||
transition
|
||||
group
|
||||
>
|
||||
<Surface
|
||||
variant="muted"
|
||||
@@ -38,17 +42,18 @@ export function TeamMembershipCard({
|
||||
display="flex"
|
||||
center
|
||||
rounded="xl"
|
||||
style={{ backgroundColor: 'rgba(147, 51, 234, 0.1)', borderColor: 'rgba(147, 51, 234, 0.2)' }}
|
||||
bg="rgba(147, 51, 234, 0.1)"
|
||||
borderColor="rgba(147, 51, 234, 0.2)"
|
||||
border
|
||||
>
|
||||
<Icon icon={Users} size={6} color="var(--neon-purple)" />
|
||||
</Surface>
|
||||
<Stack style={{ flex: 1, minWidth: 0 }}>
|
||||
<Text weight="semibold" color="text-white" className="truncate group-hover:text-purple-400 transition-colors" block>
|
||||
<Stack flex="1" minWidth="0">
|
||||
<Text weight="semibold" color="text-white" truncate groupHoverTextColor="purple-400" block>
|
||||
{teamName}
|
||||
</Text>
|
||||
<Stack direction="row" align="center" gap={2} mt={1}>
|
||||
<Badge variant="primary" style={{ backgroundColor: 'rgba(147, 51, 234, 0.1)', color: 'var(--neon-purple)', textTransform: 'capitalize' }}>
|
||||
<Badge variant="primary" bg="rgba(147, 51, 234, 0.1)" color="var(--neon-purple)" transform="capitalize">
|
||||
{role}
|
||||
</Badge>
|
||||
<Text size="xs" color="text-gray-400">
|
||||
@@ -56,7 +61,7 @@ export function TeamMembershipCard({
|
||||
</Text>
|
||||
</Stack>
|
||||
</Stack>
|
||||
<Icon icon={ChevronRight} size={4} color="var(--text-gray-500)" className="group-hover:text-purple-400 transition-colors" />
|
||||
<Icon icon={ChevronRight} size={4} color="var(--text-gray-500)" />
|
||||
</Surface>
|
||||
</Link>
|
||||
);
|
||||
|
||||
@@ -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}
|
||||
>
|
||||
<Box className="space-y-2">
|
||||
<Box display="flex" alignItems="center" gap={3}>
|
||||
<Box width={1} height={8} className="bg-[var(--ui-color-intent-primary)]" />
|
||||
<Box>
|
||||
<Box display="flex" alignItems="center" gap={3} marginBottom={2}>
|
||||
<Box width={1} height={8} backgroundColor="var(--ui-color-intent-primary)" />
|
||||
<Heading level={1} weight="bold" uppercase>{title}</Heading>
|
||||
</Box>
|
||||
{subtitle && (
|
||||
<Text variant="low" size="lg" uppercase mono className="tracking-[0.2em]">
|
||||
<Text variant="low" size="lg" uppercase mono letterSpacing="0.2em">
|
||||
{subtitle}
|
||||
</Text>
|
||||
)}
|
||||
|
||||
@@ -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<GetAllTeamsOutputDTO>('/teams/all');
|
||||
}
|
||||
|
||||
/** Get teams leaderboard */
|
||||
getLeaderboard(): Promise<GetTeamsLeaderboardOutputDTO> {
|
||||
return this.get<GetTeamsLeaderboardOutputDTO>('/teams/leaderboard');
|
||||
}
|
||||
|
||||
/** Get team details */
|
||||
getDetails(teamId: string): Promise<GetTeamDetailsOutputDTO | null> {
|
||||
return this.get<GetTeamDetailsOutputDTO | null>(`/teams/${teamId}`);
|
||||
|
||||
@@ -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,
|
||||
})),
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
31
apps/website/lib/page-queries/TeamRankingsPageQuery.ts
Normal file
31
apps/website/lib/page-queries/TeamRankingsPageQuery.ts
Normal file
@@ -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<TeamRankingsViewData, PresentationError>
|
||||
*/
|
||||
export class TeamRankingsPageQuery implements PageQuery<TeamRankingsViewData, void, PresentationError> {
|
||||
async execute(): Promise<Result<TeamRankingsViewData, PresentationError>> {
|
||||
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<Result<TeamRankingsViewData, PresentationError>> {
|
||||
const query = new TeamRankingsPageQuery();
|
||||
return query.execute();
|
||||
}
|
||||
}
|
||||
@@ -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',
|
||||
|
||||
@@ -20,7 +20,7 @@ export class LeaderboardsService implements Service {
|
||||
|
||||
const [driverResult, teamResult] = await Promise.all([
|
||||
driversApiClient.getLeaderboard(),
|
||||
teamsApiClient.getAll()
|
||||
teamsApiClient.getLeaderboard()
|
||||
]);
|
||||
|
||||
if (!driverResult) {
|
||||
|
||||
@@ -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<Result<GetTeamsLeaderboardOutputDTO, DomainError>> {
|
||||
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' });
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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[] };
|
||||
}
|
||||
teams: GetTeamsLeaderboardOutputDTO;
|
||||
}
|
||||
|
||||
@@ -8,6 +8,8 @@
|
||||
export interface TeamLeaderboardItemDTO {
|
||||
id: string;
|
||||
name: string;
|
||||
tag: string;
|
||||
logoUrl?: string;
|
||||
memberCount: number;
|
||||
rating?: number;
|
||||
totalWins: number;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
7
apps/website/lib/view-data/TeamRankingsViewData.ts
Normal file
7
apps/website/lib/view-data/TeamRankingsViewData.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import type { LeaderboardTeamItem } from './LeaderboardTeamItem';
|
||||
|
||||
export interface TeamRankingsViewData {
|
||||
teams: LeaderboardTeamItem[];
|
||||
podium: LeaderboardTeamItem[];
|
||||
recruitingCount: number;
|
||||
}
|
||||
@@ -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 */}
|
||||
<RankingsTable
|
||||
<LeaderboardTable
|
||||
drivers={viewData.drivers.map(d => ({
|
||||
...d,
|
||||
rating: Number(d.rating),
|
||||
wins: Number(d.wins)
|
||||
wins: Number(d.wins),
|
||||
racesCompleted: d.racesCompleted || 0,
|
||||
avatarUrl: d.avatarUrl || ''
|
||||
}))}
|
||||
onDriverClick={onDriverClick}
|
||||
/>
|
||||
|
||||
@@ -31,7 +31,6 @@ export function DriversTemplate({
|
||||
return (
|
||||
<main>
|
||||
<PageHeader
|
||||
icon={Users}
|
||||
title="Drivers"
|
||||
description="Global driver roster and statistics."
|
||||
action={
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import { DriverLeaderboardPreview } from '@/components/leaderboards/DriverLeaderboardPreview';
|
||||
import { TeamLeaderboardPreview } from '@/components/teams/TeamLeaderboardPreviewWrapper';
|
||||
import { TeamLeaderboardPreview } from '@/components/leaderboards/TeamLeaderboardPreview';
|
||||
import type { LeaderboardsViewData } from '@/lib/view-data/LeaderboardsViewData';
|
||||
import { Section } from '@/ui/Section';
|
||||
import { PageHero } from '@/ui/PageHero';
|
||||
@@ -28,7 +28,7 @@ export function LeaderboardsTemplate({
|
||||
<Section variant="default" padding="lg">
|
||||
<PageHero
|
||||
title="Global Standings"
|
||||
description="Consolidated performance metrics for drivers and teams. Data-driven rankings based on competitive results and technical consistency."
|
||||
description="Performance metrics for drivers and teams. Rankings are calculated based on competitive results and consistency across all events."
|
||||
icon={Activity}
|
||||
actions={[
|
||||
{
|
||||
@@ -53,9 +53,12 @@ export function LeaderboardsTemplate({
|
||||
onNavigateToDrivers={onNavigateToDrivers}
|
||||
/>
|
||||
<TeamLeaderboardPreview
|
||||
topTeams={viewData.teams}
|
||||
teams={viewData.teams.map(t => ({
|
||||
...t,
|
||||
logoUrl: t.logoUrl || ''
|
||||
}))}
|
||||
onTeamClick={onTeamClick}
|
||||
onViewFullLeaderboard={onNavigateToTeams}
|
||||
onNavigateToTeams={onNavigateToTeams}
|
||||
/>
|
||||
</FeatureGrid>
|
||||
</Section>
|
||||
|
||||
63
apps/website/templates/TeamRankingsTemplate.tsx
Normal file
63
apps/website/templates/TeamRankingsTemplate.tsx
Normal file
@@ -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 (
|
||||
<Container size="lg" py={8}>
|
||||
<PageHeader
|
||||
title="Team Leaderboard"
|
||||
description="Global rankings of all teams based on performance and consistency"
|
||||
icon={Users}
|
||||
action={
|
||||
onBackToLeaderboards && (
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={onBackToLeaderboards}
|
||||
icon={<Icon icon={ChevronLeft} size={4} />}
|
||||
>
|
||||
Back to Leaderboards
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
/>
|
||||
|
||||
<LeaderboardFiltersBar
|
||||
searchQuery={searchQuery}
|
||||
onSearchChange={onSearchChange}
|
||||
placeholder="Search teams..."
|
||||
/>
|
||||
|
||||
<TeamLeaderboardTable
|
||||
teams={viewData.teams.map(t => ({
|
||||
...t,
|
||||
totalRaces: t.totalRaces || 0,
|
||||
rating: t.rating || 0
|
||||
}))}
|
||||
onTeamClick={onTeamClick}
|
||||
/>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
@@ -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%)"
|
||||
>
|
||||
<DashboardRail>
|
||||
<Box py={8} fullWidth>
|
||||
|
||||
@@ -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 ? (
|
||||
|
||||
@@ -120,8 +120,6 @@ export interface BoxProps<T extends ElementType> {
|
||||
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<T extends ElementType> {
|
||||
groupHoverWidth?: string;
|
||||
animate?: any;
|
||||
blur?: string;
|
||||
backdropBlur?: string;
|
||||
pointerEvents?: string;
|
||||
bgOpacity?: number;
|
||||
borderWidth?: string | number;
|
||||
@@ -157,6 +156,10 @@ export interface BoxProps<T extends ElementType> {
|
||||
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<string>;
|
||||
fontWeight?: string | number;
|
||||
@@ -230,6 +233,10 @@ export interface BoxProps<T extends ElementType> {
|
||||
onPointerDown?: React.PointerEventHandler<any>;
|
||||
onPointerMove?: React.PointerEventHandler<any>;
|
||||
onPointerUp?: React.PointerEventHandler<any>;
|
||||
/** @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(<T extends ElementType = 'div'>(
|
||||
@@ -292,6 +299,7 @@ export const Box = forwardRef(<T extends ElementType = 'div'>(
|
||||
groupHoverWidth,
|
||||
animate,
|
||||
blur,
|
||||
backdropBlur,
|
||||
pointerEvents,
|
||||
bgOpacity,
|
||||
borderWidth,
|
||||
@@ -309,6 +317,10 @@ export const Box = forwardRef(<T extends ElementType = 'div'>(
|
||||
translateX,
|
||||
translateY,
|
||||
translate,
|
||||
rotate,
|
||||
scale,
|
||||
perspective,
|
||||
whiteSpace,
|
||||
cursor,
|
||||
fontSize,
|
||||
fontWeight,
|
||||
@@ -483,11 +495,15 @@ export const Box = forwardRef(<T extends ElementType = 'div'>(
|
||||
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(<T extends ElementType = 'div'>(
|
||||
...(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 } : {}),
|
||||
|
||||
@@ -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)"
|
||||
>
|
||||
<Box display="flex" alignItems="center" justifyContent="between" h="full" width="full" px={4}>
|
||||
{leftContent && (
|
||||
|
||||
@@ -26,7 +26,16 @@ export const ErrorPageContainer = ({ children, size = 'md', variant = 'default'
|
||||
position="relative"
|
||||
overflow="hidden"
|
||||
>
|
||||
<Surface variant={variant} rounded="xl" padding={8} style={{ maxWidth: sizeMap[size], width: '100%', border: '1px solid var(--ui-color-border-default)', position: 'relative', zIndex: 10 }}>
|
||||
<Surface
|
||||
variant={variant}
|
||||
rounded="xl"
|
||||
padding={8}
|
||||
maxWidth={sizeMap[size]}
|
||||
width="100%"
|
||||
border={true}
|
||||
position="relative"
|
||||
zIndex={10}
|
||||
>
|
||||
{children}
|
||||
</Surface>
|
||||
</Box>
|
||||
|
||||
@@ -17,7 +17,7 @@ export const HorizontalStatItem = ({
|
||||
return (
|
||||
<Box display="flex" alignItems="center" justifyContent="between" paddingY={2}>
|
||||
<Text size="sm" variant="low">{label}</Text>
|
||||
<Text weight="semibold" variant={color ? undefined : intent} style={color ? { color } : undefined}>{value}</Text>
|
||||
<Text weight="semibold" variant={color ? undefined : intent} color={color}>{value}</Text>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -16,8 +16,8 @@ export const MiniStat = ({
|
||||
}: MiniStatProps) => {
|
||||
return (
|
||||
<Box textAlign="center" padding={2} rounded="lg" bg="var(--ui-color-bg-surface-muted)">
|
||||
<Text size="lg" weight="bold" variant={color ? undefined : intent} style={color ? { color } : undefined} block>{value}</Text>
|
||||
<Text size="xs" variant="low" block style={{ fontSize: '10px' }}>{label}</Text>
|
||||
<Text size="lg" weight="bold" variant={color ? undefined : intent} color={color} block>{value}</Text>
|
||||
<Text size="xs" variant="low" block fontSize="10px">{label}</Text>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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 (
|
||||
<Box mb={8}>
|
||||
<Stack direction="row" align="center" justify="between" wrap gap={4}>
|
||||
<Box>
|
||||
<Stack direction="row" align="center" gap={4}>
|
||||
<Surface variant="muted" rounded="xl" border padding={3} className={`bg-gradient-to-br ${iconGradient} ${iconBorder}`}>
|
||||
<Icon icon={icon} size={7} color="#d1d5db" />
|
||||
</Surface>
|
||||
<Box>
|
||||
<Heading level={1}>{title}</Heading>
|
||||
{description && (
|
||||
<Text color="text-gray-400" block mt={1}>{description}</Text>
|
||||
)}
|
||||
</Box>
|
||||
</Stack>
|
||||
<Box
|
||||
marginBottom={12}
|
||||
display="flex"
|
||||
flexDirection={{ base: 'col', md: 'row' }}
|
||||
alignItems={{ md: 'end' }}
|
||||
justifyContent="space-between"
|
||||
gap={6}
|
||||
borderBottom="1px solid var(--ui-color-border-muted)"
|
||||
paddingBottom={8}
|
||||
>
|
||||
<Box>
|
||||
<Box display="flex" alignItems="center" gap={3} marginBottom={2}>
|
||||
<Box width={1} height={8} backgroundColor="var(--ui-color-intent-primary)" />
|
||||
<Heading level={1} weight="bold" uppercase>{title}</Heading>
|
||||
</Box>
|
||||
{action && <Box>{action}</Box>}
|
||||
</Stack>
|
||||
{description && (
|
||||
<Text variant="low" size="lg" uppercase mono letterSpacing="0.2em">
|
||||
{description}
|
||||
</Text>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
{action && (
|
||||
<Box display="flex" alignItems="center">
|
||||
{action}
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ export const RaceSummary = ({ track, meta, date }: RaceSummaryProps) => {
|
||||
<Text size="xs" variant="low" block truncate>{meta}</Text>
|
||||
</Box>
|
||||
<Box textAlign="right">
|
||||
<Text size="xs" variant="low" style={{ whiteSpace: 'nowrap' }}>
|
||||
<Text size="xs" variant="low" whiteSpace="nowrap">
|
||||
{date}
|
||||
</Text>
|
||||
</Box>
|
||||
|
||||
@@ -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` } : {})
|
||||
}}>
|
||||
<Box marginX="auto" maxWidth="80rem" paddingX={4}>
|
||||
{children}
|
||||
</Box>
|
||||
{fullWidth ? (
|
||||
children
|
||||
) : (
|
||||
<Box marginX="auto" maxWidth={maxWidth} paddingX={4}>
|
||||
{children}
|
||||
</Box>
|
||||
)}
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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<string | any>;
|
||||
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<HTMLElement, TextProps>(({
|
||||
display,
|
||||
opacity,
|
||||
maxWidth,
|
||||
maxHeight,
|
||||
overflow,
|
||||
whiteSpace,
|
||||
mx,
|
||||
pl,
|
||||
px,
|
||||
@@ -115,6 +124,7 @@ export const Text = forwardRef<HTMLElement, TextProps>(({
|
||||
gap,
|
||||
cursor,
|
||||
width,
|
||||
height,
|
||||
htmlFor,
|
||||
transform,
|
||||
}, ref) => {
|
||||
@@ -200,6 +210,7 @@ export const Text = forwardRef<HTMLElement, TextProps>(({
|
||||
italic ? 'italic' : '',
|
||||
animate === 'pulse' ? 'animate-pulse' : '',
|
||||
capitalize ? 'capitalize' : '',
|
||||
color?.startsWith('text-') ? color : '',
|
||||
className,
|
||||
].filter(Boolean).join(' ');
|
||||
|
||||
@@ -210,8 +221,12 @@ export const Text = forwardRef<HTMLElement, TextProps>(({
|
||||
...(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<HTMLElement, TextProps>(({
|
||||
...(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 } : {}),
|
||||
|
||||
@@ -35,17 +35,15 @@ export const Toast = ({
|
||||
|
||||
return (
|
||||
<Box
|
||||
style={{
|
||||
transform: isVisible && !isExiting ? 'translateX(0)' : 'translateX(100%)',
|
||||
opacity: isVisible && !isExiting ? 1 : 0,
|
||||
transition: 'all 0.3s ease-in-out',
|
||||
width: '24rem'
|
||||
}}
|
||||
translateX={isVisible && !isExiting ? '0' : '100%'}
|
||||
opacity={isVisible && !isExiting ? 1 : 0}
|
||||
transition="all 0.3s ease-in-out"
|
||||
width="24rem"
|
||||
>
|
||||
<Surface variant="muted" rounded="xl" shadow="xl" style={{ border: `1px solid ${intentColors[intent]}33`, overflow: 'hidden' }}>
|
||||
<Surface variant="muted" rounded="xl" shadow="xl" borderColor={`${intentColors[intent]}33`} border={true} overflow="hidden">
|
||||
{progress !== undefined && (
|
||||
<Box height="1px" bg="rgba(255,255,255,0.1)">
|
||||
<Box height="100%" bg={intentColors[intent]} style={{ width: `${progress}%`, transition: 'width 0.1s linear' }} />
|
||||
<Box height="100%" bg={intentColors[intent]} width={`${progress}%`} transition="width 0.1s linear" />
|
||||
</Box>
|
||||
)}
|
||||
<Box padding={4} display="flex" gap={3}>
|
||||
|
||||
Reference in New Issue
Block a user