website refactor
This commit is contained in:
@@ -28,5 +28,5 @@ export default async function AdminPage() {
|
|||||||
const output = result.unwrap();
|
const output = result.unwrap();
|
||||||
|
|
||||||
// Pass to client wrapper for UI interactions
|
// Pass to client wrapper for UI interactions
|
||||||
return <AdminDashboardWrapper initialViewData={output} />;
|
return <AdminDashboardWrapper viewData={output} />;
|
||||||
}
|
}
|
||||||
@@ -30,5 +30,5 @@ export default async function AdminUsersPage() {
|
|||||||
const output = result.unwrap();
|
const output = result.unwrap();
|
||||||
|
|
||||||
// Pass to client wrapper for UI interactions
|
// Pass to client wrapper for UI interactions
|
||||||
return <AdminUsersWrapper initialViewData={output} />;
|
return <AdminUsersWrapper viewData={output} />;
|
||||||
}
|
}
|
||||||
@@ -25,7 +25,7 @@ export default async function LeagueStewardingPage({ params }: Props) {
|
|||||||
leagueId={leagueId}
|
leagueId={leagueId}
|
||||||
currentDriverId=""
|
currentDriverId=""
|
||||||
onRefetch={() => {}}
|
onRefetch={() => {}}
|
||||||
data={{
|
viewData={{
|
||||||
leagueId,
|
leagueId,
|
||||||
totalPending: 0,
|
totalPending: 0,
|
||||||
totalResolved: 0,
|
totalResolved: 0,
|
||||||
@@ -39,7 +39,7 @@ export default async function LeagueStewardingPage({ params }: Props) {
|
|||||||
const data = result.unwrap();
|
const data = result.unwrap();
|
||||||
|
|
||||||
return <StewardingPageClient
|
return <StewardingPageClient
|
||||||
data={data}
|
viewData={data}
|
||||||
leagueId={leagueId}
|
leagueId={leagueId}
|
||||||
currentDriverId="" // Should be fetched or passed
|
currentDriverId="" // Should be fetched or passed
|
||||||
onRefetch={() => {}} // Should be handled
|
onRefetch={() => {}} // Should be handled
|
||||||
|
|||||||
@@ -21,5 +21,5 @@ export default async function Page({ params }: Props) {
|
|||||||
|
|
||||||
const viewData = result.isOk() ? result.unwrap() : null;
|
const viewData = result.isOk() ? result.unwrap() : null;
|
||||||
|
|
||||||
return <ProtestDetailPageClient initialViewData={viewData} />;
|
return <ProtestDetailPageClient viewData={viewData as any} />;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,8 +13,12 @@ export default async function AvatarsPage() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<MediaPageClient
|
<MediaPageClient
|
||||||
initialAssets={assets}
|
viewData={{
|
||||||
categories={categories}
|
assets,
|
||||||
|
categories,
|
||||||
|
title: 'Avatars',
|
||||||
|
description: 'Choose your driver avatar'
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,8 +13,12 @@ export default async function LeaguesMediaPage() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<MediaPageClient
|
<MediaPageClient
|
||||||
initialAssets={assets}
|
viewData={{
|
||||||
categories={categories}
|
assets,
|
||||||
|
categories,
|
||||||
|
title: 'League Media',
|
||||||
|
description: 'Manage league logos and covers'
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,8 +23,12 @@ export default async function MediaPage() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<MediaPageClient
|
<MediaPageClient
|
||||||
initialAssets={assets}
|
viewData={{
|
||||||
categories={categories}
|
assets,
|
||||||
|
categories,
|
||||||
|
title: 'Media Library',
|
||||||
|
description: 'Manage all your racing assets'
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,8 +13,12 @@ export default async function SponsorsMediaPage() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<MediaPageClient
|
<MediaPageClient
|
||||||
initialAssets={assets}
|
viewData={{
|
||||||
categories={categories}
|
assets,
|
||||||
|
categories,
|
||||||
|
title: 'Sponsor Media',
|
||||||
|
description: 'Manage sponsor logos'
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,8 +13,12 @@ export default async function TeamsMediaPage() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<MediaPageClient
|
<MediaPageClient
|
||||||
initialAssets={assets}
|
viewData={{
|
||||||
categories={categories}
|
assets,
|
||||||
|
categories,
|
||||||
|
title: 'Team Media',
|
||||||
|
description: 'Manage team logos'
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,8 +13,12 @@ export default async function TracksMediaPage() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<MediaPageClient
|
<MediaPageClient
|
||||||
initialAssets={assets}
|
viewData={{
|
||||||
categories={categories}
|
assets,
|
||||||
|
categories,
|
||||||
|
title: 'Track Media',
|
||||||
|
description: 'Manage track images'
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -68,9 +68,9 @@ export default function RaceStewardingPage({ params }: RaceStewardingPageProps)
|
|||||||
isLoading={isLoading}
|
isLoading={isLoading}
|
||||||
error={error}
|
error={error}
|
||||||
retry={fetchData}
|
retry={fetchData}
|
||||||
Template={({ data }) => (
|
Template={({ viewData }) => (
|
||||||
<RaceStewardingTemplate
|
<RaceStewardingTemplate
|
||||||
viewData={data as RaceStewardingViewData}
|
viewData={viewData}
|
||||||
isLoading={false}
|
isLoading={false}
|
||||||
error={null}
|
error={null}
|
||||||
onBack={handleBack}
|
onBack={handleBack}
|
||||||
|
|||||||
@@ -16,5 +16,5 @@ export default async function Page() {
|
|||||||
|
|
||||||
const viewData = result.isOk() ? result.unwrap() : null;
|
const viewData = result.isOk() ? result.unwrap() : null;
|
||||||
|
|
||||||
return <RacesAllPageClient initialViewData={viewData} />;
|
return <RacesAllPageClient viewData={viewData as any} />;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -259,7 +259,7 @@ export default function SponsorSignupPage() {
|
|||||||
</SponsorHero>
|
</SponsorHero>
|
||||||
|
|
||||||
{/* Platform Stats */}
|
{/* Platform Stats */}
|
||||||
<Box maxWidth="6xl" mx="auto" px={4} mt={-8} position="relative" zIndex={10}>
|
<Box maxWidth="6xl" mx="auto" px={4} style={{ marginTop: '-2rem' }} position="relative" zIndex={10}>
|
||||||
<Box display="grid" gridCols={{ base: 2, md: 4 }} gap={4}>
|
<Box display="grid" gridCols={{ base: 2, md: 4 }} gap={4}>
|
||||||
{PLATFORM_STATS.map((stat, index) => (
|
{PLATFORM_STATS.map((stat, index) => (
|
||||||
<motion.div
|
<motion.div
|
||||||
|
|||||||
@@ -11,5 +11,5 @@ export default async function TeamLeaderboardPage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const data = result.unwrap();
|
const data = result.unwrap();
|
||||||
return <TeamLeaderboardPageWrapper data={data.teams} />;
|
return <TeamLeaderboardPageWrapper viewData={data} />;
|
||||||
}
|
}
|
||||||
|
|||||||
30
apps/website/build_output.txt
Normal file
30
apps/website/build_output.txt
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
|
||||||
|
> @gridpilot/website@0.1.0 build
|
||||||
|
> next build
|
||||||
|
|
||||||
|
▲ Next.js 15.5.9
|
||||||
|
- Environments: .env.local
|
||||||
|
|
||||||
|
Creating an optimized production build ...
|
||||||
|
✓ Compiled successfully in 3.7s
|
||||||
|
Skipping linting
|
||||||
|
Checking validity of types ...
|
||||||
|
Failed to compile.
|
||||||
|
|
||||||
|
./hooks/league/useCreateLeague.ts:7:17
|
||||||
|
Type error: Return type of exported function has or is using name 'CreateLeagueInput' from external module "/Users/marcmintel/Projects/gridpilot/apps/website/hooks/league/useCreateLeagueWithBlockers" but cannot be named.
|
||||||
|
|
||||||
|
[0m [90m 5 |[39m [90m * This wrapper maintains backward compatibility while using the new blocker-aware hook[39m
|
||||||
|
[90m 6 |[39m [90m */[39m
|
||||||
|
[31m[1m>[22m[39m[90m 7 |[39m [36mexport[39m [36mfunction[39m useCreateLeague() {
|
||||||
|
[90m |[39m [31m[1m^[22m[39m
|
||||||
|
[90m 8 |[39m [36mreturn[39m useCreateLeagueWithBlockers()[33m;[39m
|
||||||
|
[90m 9 |[39m }[0m
|
||||||
|
Next.js build worker exited with code: 1 and signal: null
|
||||||
|
npm error Lifecycle script `build` failed with error:
|
||||||
|
npm error code 1
|
||||||
|
npm error path /Users/marcmintel/Projects/gridpilot/apps/website
|
||||||
|
npm error workspace @gridpilot/website@0.1.0
|
||||||
|
npm error location /Users/marcmintel/Projects/gridpilot/apps/website
|
||||||
|
npm error command failed
|
||||||
|
npm error command sh -c next build
|
||||||
@@ -175,7 +175,7 @@ export function LeagueAdminSchedulePageClient() {
|
|||||||
<SharedStack gap={6}>
|
<SharedStack gap={6}>
|
||||||
<SharedCard>
|
<SharedCard>
|
||||||
<SharedBox p={6} textAlign="center">
|
<SharedBox p={6} textAlign="center">
|
||||||
<Heading level={3}>Admin Access Required</Heading>
|
<SharedHeading level={3}>Admin Access Required</SharedHeading>
|
||||||
<SharedBox mt={2}>
|
<SharedBox mt={2}>
|
||||||
<SharedText size="sm" color="text-gray-400">Only league admins can manage the schedule.</SharedText>
|
<SharedText size="sm" color="text-gray-400">Only league admins can manage the schedule.</SharedText>
|
||||||
</SharedBox>
|
</SharedBox>
|
||||||
@@ -190,10 +190,10 @@ export function LeagueAdminSchedulePageClient() {
|
|||||||
data={templateData}
|
data={templateData}
|
||||||
isLoading={isLoading}
|
isLoading={isLoading}
|
||||||
error={null}
|
error={null}
|
||||||
Template={({ data }) => (
|
Template={({ viewData }) => (
|
||||||
<>
|
<>
|
||||||
<LeagueAdminScheduleTemplate
|
<LeagueAdminScheduleTemplate
|
||||||
viewData={data}
|
viewData={viewData}
|
||||||
onSeasonChange={handleSeasonChange}
|
onSeasonChange={handleSeasonChange}
|
||||||
onPublishToggle={handlePublishToggle}
|
onPublishToggle={handlePublishToggle}
|
||||||
onAddOrSave={handleAddOrSave}
|
onAddOrSave={handleAddOrSave}
|
||||||
|
|||||||
@@ -1,11 +1,10 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { SponsorLeagueDetailTemplate } from '@/templates/SponsorLeagueDetailTemplate';
|
import { SponsorLeagueDetailTemplate, type SponsorLeagueDetailViewData } from '@/templates/SponsorLeagueDetailTemplate';
|
||||||
import { ClientWrapperProps } from '@/lib/contracts/components/ComponentContracts';
|
import { ClientWrapperProps } from '@/lib/contracts/components/ComponentContracts';
|
||||||
import { ViewData } from '@/lib/contracts/view-data/ViewData';
|
|
||||||
|
|
||||||
export function SponsorLeagueDetailPageClient({ viewData }: ClientWrapperProps<ViewData>) {
|
export function SponsorLeagueDetailPageClient({ viewData }: ClientWrapperProps<SponsorLeagueDetailViewData>) {
|
||||||
const [activeTab, setActiveTab] = useState<'overview' | 'drivers' | 'races' | 'sponsor'>('overview');
|
const [activeTab, setActiveTab] = useState<'overview' | 'drivers' | 'races' | 'sponsor'>('overview');
|
||||||
const [selectedTier, setSelectedTier] = useState<'main' | 'secondary'>('main');
|
const [selectedTier, setSelectedTier] = useState<'main' | 'secondary'>('main');
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
import { Flag, Star, Trophy, Users } from 'lucide-react';
|
import { Flag, Star, Trophy, Users } from 'lucide-react';
|
||||||
import { Avatar } from '../../ui/Avatar';
|
import { Avatar } from '../../ui/Avatar';
|
||||||
import { Badge } from '../../ui/Badge';
|
import { Badge } from '../../ui/Badge';
|
||||||
|
import { Box } from '../../ui/Box';
|
||||||
import { Heading } from '../../ui/Heading';
|
import { Heading } from '../../ui/Heading';
|
||||||
import { Icon } from '../../ui/Icon';
|
import { Icon } from '../../ui/Icon';
|
||||||
import { ProfileHero, ProfileAvatar, ProfileStatsGroup, ProfileStat } from '../../ui/ProfileHero';
|
import { ProfileHero, ProfileAvatar, ProfileStatsGroup, ProfileStat } from '../../ui/ProfileHero';
|
||||||
import { BadgeGroup } from '../../ui/BadgeGroup';
|
import { BadgeGroup } from '../../ui/BadgeGroup';
|
||||||
import { QuickStatCard, QuickStatItem } from '../../ui/QuickStatCard';
|
import { QuickStatCard, QuickStatItem } from '../../ui/QuickStatCard';
|
||||||
|
import { Stack } from '../../ui/Stack';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
interface DashboardHeroProps {
|
interface DashboardHeroProps {
|
||||||
@@ -27,7 +29,7 @@ export function DashboardHero({
|
|||||||
}: DashboardHeroProps) {
|
}: DashboardHeroProps) {
|
||||||
return (
|
return (
|
||||||
<ProfileHero glowColor="aqua">
|
<ProfileHero glowColor="aqua">
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: '2rem', flexWrap: 'wrap' }}>
|
<Stack direction="row" align="center" gap={8} wrap>
|
||||||
{/* Avatar Section */}
|
{/* Avatar Section */}
|
||||||
<ProfileAvatar
|
<ProfileAvatar
|
||||||
badge={<Icon icon={Star} size={5} intent="high" />}
|
badge={<Icon icon={Star} size={5} intent="high" />}
|
||||||
@@ -40,7 +42,7 @@ export function DashboardHero({
|
|||||||
</ProfileAvatar>
|
</ProfileAvatar>
|
||||||
|
|
||||||
{/* Info Section */}
|
{/* Info Section */}
|
||||||
<div style={{ flex: 1, minWidth: '200px' }}>
|
<Box flex={1} minWidth="200px">
|
||||||
<Heading level={1}>{driverName}</Heading>
|
<Heading level={1}>{driverName}</Heading>
|
||||||
|
|
||||||
<ProfileStatsGroup>
|
<ProfileStatsGroup>
|
||||||
@@ -60,14 +62,14 @@ export function DashboardHero({
|
|||||||
Team Redline
|
Team Redline
|
||||||
</Badge>
|
</Badge>
|
||||||
</BadgeGroup>
|
</BadgeGroup>
|
||||||
</div>
|
</Box>
|
||||||
|
|
||||||
{/* Quick Stats */}
|
{/* Quick Stats */}
|
||||||
<QuickStatCard>
|
<QuickStatCard>
|
||||||
<QuickStatItem label="Podiums" value="12" />
|
<QuickStatItem label="Podiums" value="12" />
|
||||||
<QuickStatItem label="Wins" value="4" />
|
<QuickStatItem label="Wins" value="4" />
|
||||||
</QuickStatCard>
|
</QuickStatCard>
|
||||||
</div>
|
</Stack>
|
||||||
</ProfileHero>
|
</ProfileHero>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { routes } from '@/lib/routing/RouteConfig';
|
|||||||
import { Button } from '@/ui/Button';
|
import { Button } from '@/ui/Button';
|
||||||
import { Icon } from '@/ui/Icon';
|
import { Icon } from '@/ui/Icon';
|
||||||
import { Link } from '@/ui/Link';
|
import { Link } from '@/ui/Link';
|
||||||
|
import { Stack } from '@/ui/Stack';
|
||||||
import { StatGrid } from '@/ui/StatGrid';
|
import { StatGrid } from '@/ui/StatGrid';
|
||||||
import { Flag, Medal, Target, Trophy, User, Users } from 'lucide-react';
|
import { Flag, Medal, Target, Trophy, User, Users } from 'lucide-react';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
@@ -26,7 +27,7 @@ interface DashboardHeroProps {
|
|||||||
|
|
||||||
export function DashboardHero({ currentDriver, activeLeaguesCount }: DashboardHeroProps) {
|
export function DashboardHero({ currentDriver, activeLeaguesCount }: DashboardHeroProps) {
|
||||||
return (
|
return (
|
||||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '2rem' }}>
|
<Stack gap={8}>
|
||||||
<UiDashboardHero
|
<UiDashboardHero
|
||||||
driverName={currentDriver.name}
|
driverName={currentDriver.name}
|
||||||
avatarUrl={currentDriver.avatarUrl}
|
avatarUrl={currentDriver.avatarUrl}
|
||||||
@@ -36,7 +37,7 @@ export function DashboardHero({ currentDriver, activeLeaguesCount }: DashboardHe
|
|||||||
winRate={Math.round((currentDriver.wins / currentDriver.totalRaces) * 100) || 0}
|
winRate={Math.round((currentDriver.wins / currentDriver.totalRaces) * 100) || 0}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div style={{ display: 'flex', gap: '1rem', flexWrap: 'wrap' }}>
|
<Stack direction="row" gap={4} wrap>
|
||||||
<Link href={routes.public.leagues}>
|
<Link href={routes.public.leagues}>
|
||||||
<Button variant="secondary" icon={<Icon icon={Flag} size={4} />}>
|
<Button variant="secondary" icon={<Icon icon={Flag} size={4} />}>
|
||||||
Browse Leagues
|
Browse Leagues
|
||||||
@@ -47,7 +48,7 @@ export function DashboardHero({ currentDriver, activeLeaguesCount }: DashboardHe
|
|||||||
View Profile
|
View Profile
|
||||||
</Button>
|
</Button>
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</Stack>
|
||||||
|
|
||||||
<StatGrid
|
<StatGrid
|
||||||
variant="box"
|
variant="box"
|
||||||
@@ -59,6 +60,6 @@ export function DashboardHero({ currentDriver, activeLeaguesCount }: DashboardHe
|
|||||||
{ icon: Users, label: 'Active Leagues', value: activeLeaguesCount, intent: 'telemetry' },
|
{ icon: Users, label: 'Active Leagues', value: activeLeaguesCount, intent: 'telemetry' },
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
</div>
|
</Stack>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { useNotifications } from '@/components/notifications/NotificationProvide
|
|||||||
import type { NotificationVariant } from '@/components/notifications/notificationTypes';
|
import type { NotificationVariant } from '@/components/notifications/notificationTypes';
|
||||||
import { useEffectiveDriverId } from "@/hooks/useEffectiveDriverId";
|
import { useEffectiveDriverId } from "@/hooks/useEffectiveDriverId";
|
||||||
import { ApiConnectionMonitor } from '@/lib/api/base/ApiConnectionMonitor';
|
import { ApiConnectionMonitor } from '@/lib/api/base/ApiConnectionMonitor';
|
||||||
import { CircuitBreakerRegistry } from '@/lib/api/base/CircuitBreakerRegistry';
|
import { CircuitBreakerRegistry } from '@/lib/api/base/RetryHandler';
|
||||||
import { getGlobalErrorHandler } from '@/lib/infrastructure/GlobalErrorHandler';
|
import { getGlobalErrorHandler } from '@/lib/infrastructure/GlobalErrorHandler';
|
||||||
import { Activity, AlertTriangle, ChevronDown, ChevronUp, MessageSquare, Wrench, X } from 'lucide-react';
|
import { Activity, AlertTriangle, ChevronDown, ChevronUp, MessageSquare, Wrench, X } from 'lucide-react';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
|
|||||||
@@ -2,11 +2,12 @@
|
|||||||
|
|
||||||
import { getGlobalReplaySystem } from '@/lib/infrastructure/ErrorReplay';
|
import { getGlobalReplaySystem } from '@/lib/infrastructure/ErrorReplay';
|
||||||
import { Button } from '@/ui/Button';
|
import { Button } from '@/ui/Button';
|
||||||
|
import { Box } from '@/ui/Box';
|
||||||
import { Icon } from '@/ui/Icon';
|
import { Icon } from '@/ui/Icon';
|
||||||
import { IconButton } from '@/ui/IconButton';
|
import { IconButton } from '@/ui/IconButton';
|
||||||
import { Stack } from '@/ui/Stack';
|
import { Stack } from '@/ui/Stack';
|
||||||
import { Text } from '@/ui/Text';
|
import { Text } from '@/ui/Text';
|
||||||
import { Box, Clock, Copy, Download, Play, Trash2 } from 'lucide-react';
|
import { Clock, Copy, Download, Play, Trash2 } from 'lucide-react';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
interface ReplayEntry {
|
interface ReplayEntry {
|
||||||
|
|||||||
@@ -11,8 +11,7 @@ import { Group } from '@/ui/Group';
|
|||||||
import { Stack } from '@/ui/Stack';
|
import { Stack } from '@/ui/Stack';
|
||||||
import { ControlBar } from '@/ui/ControlBar';
|
import { ControlBar } from '@/ui/ControlBar';
|
||||||
import { Trophy } from 'lucide-react';
|
import { Trophy } from 'lucide-react';
|
||||||
import { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import React from 'react';
|
|
||||||
|
|
||||||
interface RaceHistoryProps {
|
interface RaceHistoryProps {
|
||||||
driverId: string;
|
driverId: string;
|
||||||
|
|||||||
@@ -3,10 +3,12 @@ import { Badge } from '@/ui/Badge';
|
|||||||
|
|
||||||
interface RatingBadgeProps {
|
interface RatingBadgeProps {
|
||||||
rating: number;
|
rating: number;
|
||||||
size?: 'sm' | 'md';
|
size?: 'sm' | 'md' | 'lg';
|
||||||
}
|
}
|
||||||
|
|
||||||
export function RatingBadge({ rating, size = 'md' }: RatingBadgeProps) {
|
export function RatingBadge({ rating, size = 'md' }: RatingBadgeProps) {
|
||||||
|
const badgeSize = size === 'lg' ? 'md' : size;
|
||||||
|
|
||||||
const getVariant = (val: number): 'warning' | 'primary' | 'success' | 'default' => {
|
const getVariant = (val: number): 'warning' | 'primary' | 'success' | 'default' => {
|
||||||
if (val >= 2500) return 'warning';
|
if (val >= 2500) return 'warning';
|
||||||
if (val >= 2000) return 'primary'; // Simplified
|
if (val >= 2000) return 'primary'; // Simplified
|
||||||
@@ -18,7 +20,7 @@ export function RatingBadge({ rating, size = 'md' }: RatingBadgeProps) {
|
|||||||
return (
|
return (
|
||||||
<Badge
|
<Badge
|
||||||
variant={getVariant(rating)}
|
variant={getVariant(rating)}
|
||||||
size={size}
|
size={badgeSize}
|
||||||
>
|
>
|
||||||
{rating.toLocaleString()}
|
{rating.toLocaleString()}
|
||||||
</Badge>
|
</Badge>
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ export function SafetyRatingBadge({ rating, size = 'md' }: SafetyRatingBadgeProp
|
|||||||
border
|
border
|
||||||
bg={getBgColor(rating)}
|
bg={getBgColor(rating)}
|
||||||
borderColor={getBorderColor(rating)}
|
borderColor={getBorderColor(rating)}
|
||||||
{...sizeProps[size]}
|
{...(sizeProps[size] as any)}
|
||||||
>
|
>
|
||||||
<Shield size={iconSizes[size]} color={iconColors[colorClass as keyof typeof iconColors]} />
|
<Shield size={iconSizes[size]} color={iconColors[colorClass as keyof typeof iconColors]} />
|
||||||
<Text
|
<Text
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
import React, { Component, ReactNode, ErrorInfo, useState, version } from 'react';
|
import React, { Component, ReactNode, ErrorInfo, useState, version } from 'react';
|
||||||
import { ApiError } from '@/lib/api/base/ApiError';
|
import { ApiError } from '@/lib/api/base/ApiError';
|
||||||
import { getGlobalErrorHandler } from '@/lib/infrastructure/GlobalErrorHandler';
|
import { getGlobalErrorHandler } from '@/lib/infrastructure/GlobalErrorHandler';
|
||||||
import { DevErrorPanel } from './DevErrorPanel';
|
import { DevErrorPanel } from '@/ui/DevErrorPanel';
|
||||||
import { ErrorDisplay } from '@/ui/ErrorDisplay';
|
import { ErrorDisplay } from '@/ui/ErrorDisplay';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { Stack } from '@/ui/Stack';
|
||||||
import React, { ReactNode } from 'react';
|
import React, { ReactNode } from 'react';
|
||||||
|
|
||||||
interface ActivityFeedListProps {
|
interface ActivityFeedListProps {
|
||||||
@@ -6,8 +7,8 @@ interface ActivityFeedListProps {
|
|||||||
|
|
||||||
export function ActivityFeedList({ children }: ActivityFeedListProps) {
|
export function ActivityFeedList({ children }: ActivityFeedListProps) {
|
||||||
return (
|
return (
|
||||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '1rem' }}>
|
<Stack gap={4}>
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</Stack>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { FeedItemCard } from '@/components/feed/FeedItemCard';
|
import { FeedItemCard } from '@/components/feed/FeedItemCard';
|
||||||
import { FeedEmptyState } from '@/ui/FeedEmptyState';
|
import { FeedEmptyState } from '@/ui/FeedEmptyState';
|
||||||
|
import { Stack } from '@/ui/Stack';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
interface FeedItemData {
|
interface FeedItemData {
|
||||||
@@ -23,10 +24,10 @@ export function FeedList({ items }: FeedListProps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '1rem' }}>
|
<Stack gap={4}>
|
||||||
{items.map(item => (
|
{items.map(item => (
|
||||||
<FeedItemCard key={item.id} item={item} />
|
<FeedItemCard key={item.id} item={item} />
|
||||||
))}
|
))}
|
||||||
</div>
|
</Stack>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { routes } from '@/lib/routing/RouteConfig';
|
import { routes } from '@/lib/routing/RouteConfig';
|
||||||
import { Stack } from '@/ui/Stack';
|
import { Stack } from '@/ui/Stack';
|
||||||
import { Calendar, Home, Layout, Settings, Trophy, Users } from 'lucide-react';
|
import { Calendar, Home, Layout, Settings, Trophy, Users } from 'lucide-react';
|
||||||
import { NavLink } from './NavLink';
|
import { NavLink } from '@/ui/NavLink';
|
||||||
|
|
||||||
interface AuthedNavProps {
|
interface AuthedNavProps {
|
||||||
pathname: string;
|
pathname: string;
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { routes } from '@/lib/routing/RouteConfig';
|
import { routes } from '@/lib/routing/RouteConfig';
|
||||||
import { Stack } from '@/ui/Stack';
|
import { Stack } from '@/ui/Stack';
|
||||||
import { Calendar, Home, Layout, Trophy, Users } from 'lucide-react';
|
import { Calendar, Home, Layout, Trophy, Users } from 'lucide-react';
|
||||||
import { NavLink } from './NavLink';
|
import { NavLink } from '@/ui/NavLink';
|
||||||
|
|
||||||
interface PublicNavProps {
|
interface PublicNavProps {
|
||||||
pathname: string;
|
pathname: string;
|
||||||
|
|||||||
@@ -5,10 +5,12 @@ import React from 'react';
|
|||||||
|
|
||||||
interface RankBadgeProps {
|
interface RankBadgeProps {
|
||||||
rank: number;
|
rank: number;
|
||||||
size?: 'sm' | 'md';
|
size?: 'sm' | 'md' | 'lg';
|
||||||
}
|
}
|
||||||
|
|
||||||
export function RankBadge({ rank, size = 'md' }: RankBadgeProps) {
|
export function RankBadge({ rank, size = 'md' }: RankBadgeProps) {
|
||||||
|
const badgeSize = size === 'lg' ? 'md' : size;
|
||||||
|
|
||||||
const getVariant = (rank: number): 'warning' | 'primary' | 'info' | 'default' => {
|
const getVariant = (rank: number): 'warning' | 'primary' | 'info' | 'default' => {
|
||||||
if (rank <= 3) return 'warning';
|
if (rank <= 3) return 'warning';
|
||||||
if (rank <= 10) return 'primary';
|
if (rank <= 10) return 'primary';
|
||||||
@@ -28,7 +30,7 @@ export function RankBadge({ rank, size = 'md' }: RankBadgeProps) {
|
|||||||
const medal = getMedalEmoji(rank);
|
const medal = getMedalEmoji(rank);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Badge variant={getVariant(rank)} size={size}>
|
<Badge variant={getVariant(rank)} size={badgeSize}>
|
||||||
<Group gap={1}>
|
<Group gap={1}>
|
||||||
{medal && <Text size="xs">{medal}</Text>}
|
{medal && <Text size="xs">{medal}</Text>}
|
||||||
<Text size="xs" weight="bold">#{rank}</Text>
|
<Text size="xs" weight="bold">#{rank}</Text>
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { DriverIdentity } from '@/ui/DriverIdentity';
|
import { DriverIdentity } from '@/ui/DriverIdentity';
|
||||||
import { DriverViewModel } from '@/lib/view-models/DriverViewModel';
|
import { DriverViewModel } from '@/lib/view-models/DriverViewModel';
|
||||||
import { Badge } from '@/ui/Badge';
|
import { Badge } from '@/ui/Badge';
|
||||||
|
import { Box } from '@/ui/Box';
|
||||||
import { TableCell, TableRow } from '@/ui/Table';
|
import { TableCell, TableRow } from '@/ui/Table';
|
||||||
import { Text } from '@/ui/Text';
|
import { Text } from '@/ui/Text';
|
||||||
import { DateDisplay } from '@/lib/display-objects/DateDisplay';
|
import { DateDisplay } from '@/lib/display-objects/DateDisplay';
|
||||||
@@ -41,7 +42,7 @@ export function LeagueMemberRow({
|
|||||||
return (
|
return (
|
||||||
<TableRow variant={isTopPerformer ? 'highlight' : 'default'}>
|
<TableRow variant={isTopPerformer ? 'highlight' : 'default'}>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: '0.5rem' }}>
|
<Box display="flex" alignItems="center" gap={2}>
|
||||||
{driver ? (
|
{driver ? (
|
||||||
<DriverIdentity
|
<DriverIdentity
|
||||||
driver={driver}
|
driver={driver}
|
||||||
@@ -59,7 +60,7 @@ export function LeagueMemberRow({
|
|||||||
{isTopPerformer && (
|
{isTopPerformer && (
|
||||||
<Text size="xs">⭐</Text>
|
<Text size="xs">⭐</Text>
|
||||||
)}
|
)}
|
||||||
</div>
|
</Box>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<Text variant="primary" weight="medium">
|
<Text variant="primary" weight="medium">
|
||||||
|
|||||||
@@ -17,8 +17,7 @@ import { Text } from '@/ui/Text';
|
|||||||
import { Box } from '@/ui/Box';
|
import { Box } from '@/ui/Box';
|
||||||
import { Group } from '@/ui/Group';
|
import { Group } from '@/ui/Group';
|
||||||
import { ControlBar } from '@/ui/ControlBar';
|
import { ControlBar } from '@/ui/ControlBar';
|
||||||
import { useCallback, useEffect, useState } from 'react';
|
import React, { useCallback, useEffect, useState } from 'react';
|
||||||
import React from 'react';
|
|
||||||
|
|
||||||
interface LeagueMembersProps {
|
interface LeagueMembersProps {
|
||||||
leagueId: string;
|
leagueId: string;
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import type { LeagueScheduleRaceViewModel } from '@/lib/view-models/LeagueSchedu
|
|||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
|
|
||||||
// Shared state components
|
// Shared state components
|
||||||
import { StateContainer } from '@/ui/StateContainer';
|
import { StateContainer } from '@/components/shared/state/StateContainer';
|
||||||
import { useLeagueSchedule } from "@/hooks/league/useLeagueSchedule";
|
import { useLeagueSchedule } from "@/hooks/league/useLeagueSchedule";
|
||||||
import { Button } from '@/ui/Button';
|
import { Button } from '@/ui/Button';
|
||||||
import { Heading } from '@/ui/Heading';
|
import { Heading } from '@/ui/Heading';
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { Text } from '@/ui/Text';
|
import { Text } from '@/ui/Text';
|
||||||
import { useState } from 'react';
|
|
||||||
import { MediaCard } from '@/ui/MediaCard';
|
import { MediaCard } from '@/ui/MediaCard';
|
||||||
import { MediaFiltersBar } from './MediaFiltersBar';
|
import { MediaFiltersBar } from './MediaFiltersBar';
|
||||||
import { Grid } from '@/ui/Grid';
|
import { Grid } from '@/ui/Grid';
|
||||||
@@ -10,7 +9,7 @@ import { MediaViewerModal } from './MediaViewerModal';
|
|||||||
import { SectionHeader } from '@/ui/SectionHeader';
|
import { SectionHeader } from '@/ui/SectionHeader';
|
||||||
import { EmptyState } from '@/ui/EmptyState';
|
import { EmptyState } from '@/ui/EmptyState';
|
||||||
import { Search } from 'lucide-react';
|
import { Search } from 'lucide-react';
|
||||||
import React from 'react';
|
import React, { useState } from 'react';
|
||||||
|
|
||||||
export interface MediaAsset {
|
export interface MediaAsset {
|
||||||
id: string;
|
id: string;
|
||||||
|
|||||||
@@ -235,7 +235,7 @@ function AnimatedRating({ shouldReduceMotion, value }: { shouldReduceMotion: boo
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack as={motion.span} weight="bold" color="text-primary-accent" font="mono" size={{ base: 'sm', sm: 'base' }}>
|
<Stack as={motion.span} weight="bold" color="text-primary-accent" font="mono" size={{ base: 'sm', sm: 'base' }}>
|
||||||
{shouldReduceMotion ? value : <Stack as={motion.span}>{rounded}</Stack>}
|
{shouldReduceMotion ? value : <motion.span>{rounded}</motion.span>}
|
||||||
</Stack>
|
</Stack>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -265,7 +265,7 @@ function AnimatedCounter({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack weight="bold" color="text-white" font="mono" size={{ base: 'sm', sm: 'base', md: 'lg' }}>
|
<Stack weight="bold" color="text-white" font="mono" size={{ base: 'sm', sm: 'base', md: 'lg' }}>
|
||||||
{shouldReduceMotion ? value : <Stack as={motion.span}>{rounded}</Stack>}{suffix}
|
{shouldReduceMotion ? value : <motion.span>{rounded}</motion.span>}{suffix}
|
||||||
</Stack>
|
</Stack>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -270,7 +270,7 @@ function AnimatedPoints({
|
|||||||
weight="bold"
|
weight="bold"
|
||||||
color="text-white"
|
color="text-white"
|
||||||
>
|
>
|
||||||
{shouldReduceMotion ? points : <Stack as={motion.span}>{spring}</Stack>}
|
{shouldReduceMotion ? points : <motion.span>{spring}</motion.span>}
|
||||||
</Stack>
|
</Stack>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ interface Stat {
|
|||||||
label: string;
|
label: string;
|
||||||
value: string | number;
|
value: string | number;
|
||||||
intent?: 'primary' | 'telemetry' | 'success' | 'critical';
|
intent?: 'primary' | 'telemetry' | 'success' | 'critical';
|
||||||
|
color?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ProfileStatGridProps {
|
interface ProfileStatGridProps {
|
||||||
@@ -17,6 +18,7 @@ export function ProfileStatGrid({ stats }: ProfileStatGridProps) {
|
|||||||
label: stat.label,
|
label: stat.label,
|
||||||
value: stat.value,
|
value: stat.value,
|
||||||
intent: stat.intent || 'primary',
|
intent: stat.intent || 'primary',
|
||||||
|
color: stat.color,
|
||||||
icon: Bug // Default icon if none provided, but StatBox requires one
|
icon: Bug // Default icon if none provided, but StatBox requires one
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
|||||||
@@ -190,7 +190,8 @@ export function UserPill() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Box position="relative" display="inline-flex" alignItems="center" data-user-pill>
|
<Box position="relative" display="inline-flex" alignItems="center" data-user-pill>
|
||||||
<button
|
<Box
|
||||||
|
as="button"
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setIsMenuOpen((open) => !open)}
|
onClick={() => setIsMenuOpen((open) => !open)}
|
||||||
style={{
|
style={{
|
||||||
@@ -237,7 +238,7 @@ export function UserPill() {
|
|||||||
|
|
||||||
{/* Chevron */}
|
{/* Chevron */}
|
||||||
<Icon icon={ChevronDown} size={3.5} intent="low" />
|
<Icon icon={ChevronDown} size={3.5} intent="low" />
|
||||||
</button>
|
</Box>
|
||||||
|
|
||||||
<UserDropdown isOpen={isMenuOpen}>
|
<UserDropdown isOpen={isMenuOpen}>
|
||||||
<UserDropdownHeader variant={isDemo ? 'demo' : 'default'}>
|
<UserDropdownHeader variant={isDemo ? 'demo' : 'default'}>
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ export function LatestResultsSidebar({ results }: LatestResultsSidebarProps) {
|
|||||||
<RaceSummaryItem
|
<RaceSummaryItem
|
||||||
track={result.track}
|
track={result.track}
|
||||||
meta={`${result.winnerName} • ${result.car}`}
|
meta={`${result.winnerName} • ${result.car}`}
|
||||||
date={scheduledAt}
|
dateLabel={scheduledAt.toLocaleDateString()}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -24,22 +24,22 @@ interface RaceListItemProps {
|
|||||||
export function RaceListItem({ race, onClick }: RaceListItemProps) {
|
export function RaceListItem({ race, onClick }: RaceListItemProps) {
|
||||||
const statusConfig = {
|
const statusConfig = {
|
||||||
scheduled: {
|
scheduled: {
|
||||||
icon: Clock,
|
iconName: 'Clock',
|
||||||
variant: 'primary' as const,
|
variant: 'primary' as const,
|
||||||
label: 'Scheduled',
|
label: 'Scheduled',
|
||||||
},
|
},
|
||||||
running: {
|
running: {
|
||||||
icon: PlayCircle,
|
iconName: 'PlayCircle',
|
||||||
variant: 'success' as const,
|
variant: 'success' as const,
|
||||||
label: 'LIVE',
|
label: 'LIVE',
|
||||||
},
|
},
|
||||||
completed: {
|
completed: {
|
||||||
icon: CheckCircle2,
|
iconName: 'CheckCircle2',
|
||||||
variant: 'default' as const,
|
variant: 'default' as const,
|
||||||
label: 'Completed',
|
label: 'Completed',
|
||||||
},
|
},
|
||||||
cancelled: {
|
cancelled: {
|
||||||
icon: XCircle,
|
iconName: 'XCircle',
|
||||||
variant: 'warning' as const,
|
variant: 'warning' as const,
|
||||||
label: 'Cancelled',
|
label: 'Cancelled',
|
||||||
},
|
},
|
||||||
@@ -64,11 +64,13 @@ export function RaceListItem({ race, onClick }: RaceListItemProps) {
|
|||||||
dayLabel={date.getDate().toString()}
|
dayLabel={date.getDate().toString()}
|
||||||
timeLabel={formatTime(race.scheduledAt)}
|
timeLabel={formatTime(race.scheduledAt)}
|
||||||
status={race.status}
|
status={race.status}
|
||||||
|
statusLabel={config.label}
|
||||||
|
statusVariant={config.variant}
|
||||||
|
statusIconName={config.iconName}
|
||||||
leagueName={race.leagueName}
|
leagueName={race.leagueName}
|
||||||
leagueHref={race.leagueId ? routes.league.detail(race.leagueId) : undefined}
|
leagueHref={race.leagueId ? routes.league.detail(race.leagueId) : undefined}
|
||||||
strengthOfField={race.strengthOfField}
|
strengthOfField={race.strengthOfField}
|
||||||
onClick={() => onClick(race.id)}
|
onClick={() => onClick(race.id)}
|
||||||
statusConfig={config}
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { Box } from '@/ui/Box';
|
import { Box } from '@/ui/Box';
|
||||||
import { Panel } from '@/ui/Panel';
|
import { Panel } from '@/ui/Panel';
|
||||||
|
import { Stack } from '@/ui/Stack';
|
||||||
import { StatusDot } from '@/ui/StatusDot';
|
import { StatusDot } from '@/ui/StatusDot';
|
||||||
import { Text } from '@/ui/Text';
|
import { Text } from '@/ui/Text';
|
||||||
|
|
||||||
@@ -30,16 +31,16 @@ export function SessionSummaryPanel({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Panel title="Session Summary" className={className}>
|
<Panel title="Session Summary" className={className}>
|
||||||
<Box display="flex" flexDirection="col" gap={3}>
|
<Stack gap={3}>
|
||||||
<Box display="flex" align="center" justify="between">
|
<Stack direction="row" align="center" justify="between">
|
||||||
<Text weight="bold" size="lg">{title}</Text>
|
<Text weight="bold" size="lg">{title}</Text>
|
||||||
<Box display="flex" align="center" gap={2}>
|
<Stack direction="row" align="center" gap={2}>
|
||||||
<StatusDot color={statusColor} pulse={status === 'live'} size={2} />
|
<StatusDot color={statusColor} pulse={status === 'live'} size={2} />
|
||||||
<Text size="xs" uppercase weight="bold" style={{ color: statusColor }}>
|
<Text size="xs" uppercase weight="bold" style={{ color: statusColor }}>
|
||||||
{status}
|
{status}
|
||||||
</Text>
|
</Text>
|
||||||
</Box>
|
</Stack>
|
||||||
</Box>
|
</Stack>
|
||||||
|
|
||||||
<Box display="grid" gridCols={2} gap={4} borderTop borderStyle="solid" borderColor="border-gray/10" pt={3}>
|
<Box display="grid" gridCols={2} gap={4} borderTop borderStyle="solid" borderColor="border-gray/10" pt={3}>
|
||||||
{startTime && (
|
{startTime && (
|
||||||
@@ -61,7 +62,7 @@ export function SessionSummaryPanel({
|
|||||||
</Box>
|
</Box>
|
||||||
)}
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Stack>
|
||||||
</Panel>
|
</Panel>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { Box } from '@/ui/Box';
|
import { Box } from '@/ui/Box';
|
||||||
|
import { Stack } from '@/ui/Stack';
|
||||||
import { Text } from '@/ui/Text';
|
import { Text } from '@/ui/Text';
|
||||||
|
|
||||||
interface TelemetryItem {
|
interface TelemetryItem {
|
||||||
@@ -25,7 +26,7 @@ export function TelemetryStrip({ items, className = '' }: TelemetryStripProps) {
|
|||||||
className={`bg-graphite-black/80 border-y border-border-gray/30 py-2 px-4 flex items-center gap-8 overflow-x-auto no-scrollbar ${className}`}
|
className={`bg-graphite-black/80 border-y border-border-gray/30 py-2 px-4 flex items-center gap-8 overflow-x-auto no-scrollbar ${className}`}
|
||||||
>
|
>
|
||||||
{items.map((item, index) => (
|
{items.map((item, index) => (
|
||||||
<Box key={index} display="flex" align="center" gap={2} className="whitespace-nowrap">
|
<Stack key={index} direction="row" align="center" gap={2} className="whitespace-nowrap">
|
||||||
<Text size="xs" color="text-gray-500" uppercase weight="bold" letterSpacing="widest">
|
<Text size="xs" color="text-gray-500" uppercase weight="bold" letterSpacing="widest">
|
||||||
{item.label}
|
{item.label}
|
||||||
</Text>
|
</Text>
|
||||||
@@ -42,7 +43,7 @@ export function TelemetryStrip({ items, className = '' }: TelemetryStripProps) {
|
|||||||
{item.trend === 'up' ? '↑' : item.trend === 'down' ? '↓' : '•'}
|
{item.trend === 'up' ? '↑' : item.trend === 'down' ? '↓' : '•'}
|
||||||
</Text>
|
</Text>
|
||||||
)}
|
)}
|
||||||
</Box>
|
</Stack>
|
||||||
))}
|
))}
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
import { Icon } from '@/ui/Icon';
|
import { Icon } from '@/ui/Icon';
|
||||||
import { Text } from '@/ui/Text';
|
import { Text } from '@/ui/Text';
|
||||||
import { Stack } from '@/ui/Stack';
|
import { Stack } from '@/ui/Stack';
|
||||||
|
import { Grid } from '@/ui/Grid';
|
||||||
import { Droplets, Sun, Thermometer, Wind, type LucideIcon } from 'lucide-react';
|
import { Droplets, Sun, Thermometer, Wind, type LucideIcon } from 'lucide-react';
|
||||||
|
|
||||||
interface TrackConditionsPanelProps {
|
interface TrackConditionsPanelProps {
|
||||||
@@ -26,7 +27,7 @@ export function TrackConditionsPanel({
|
|||||||
Track Conditions
|
Track Conditions
|
||||||
</Text>
|
</Text>
|
||||||
|
|
||||||
<Stack display="grid" gridCols={2} mdCols={4} gap={4}>
|
<Grid cols={2} mdCols={4} gap={4}>
|
||||||
<ConditionItem
|
<ConditionItem
|
||||||
icon={Thermometer}
|
icon={Thermometer}
|
||||||
label="Air Temp"
|
label="Air Temp"
|
||||||
@@ -51,7 +52,7 @@ export function TrackConditionsPanel({
|
|||||||
value={windSpeed}
|
value={windSpeed}
|
||||||
color="text-gray-400"
|
color="text-gray-400"
|
||||||
/>
|
/>
|
||||||
</Stack>
|
</Grid>
|
||||||
|
|
||||||
<Stack mt={4} pt={4} borderTop borderColor="border-outline-steel" bgOpacity={0.5} display="flex" alignItems="center" gap={3}>
|
<Stack mt={4} pt={4} borderTop borderColor="border-outline-steel" bgOpacity={0.5} display="flex" alignItems="center" gap={3}>
|
||||||
<Icon icon={Sun} size={4} color="#FFBE4D" />
|
<Icon icon={Sun} size={4} color="#FFBE4D" />
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ export function UpcomingRacesSidebar({ races }: UpcomingRacesSidebarProps) {
|
|||||||
key={race.id}
|
key={race.id}
|
||||||
track={race.track}
|
track={race.track}
|
||||||
meta={race.car}
|
meta={race.car}
|
||||||
date={scheduledAt}
|
dateLabel={scheduledAt.toLocaleDateString()}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|||||||
@@ -130,7 +130,8 @@ export function RangeField({
|
|||||||
<Group justify="between" gap={3}>
|
<Group justify="between" gap={3}>
|
||||||
<Text as="label" size="xs" weight="medium" variant="low">{label}</Text>
|
<Text as="label" size="xs" weight="medium" variant="low">{label}</Text>
|
||||||
<Box display="flex" alignItems="center" gap={2} flex={1} maxWidth="200px">
|
<Box display="flex" alignItems="center" gap={2} flex={1} maxWidth="200px">
|
||||||
<div
|
<Box
|
||||||
|
as="div"
|
||||||
ref={sliderRef}
|
ref={sliderRef}
|
||||||
style={{
|
style={{
|
||||||
position: 'relative',
|
position: 'relative',
|
||||||
@@ -173,7 +174,7 @@ export function RangeField({
|
|||||||
boxShadow: isDragging ? '0 0 12px rgba(25, 140, 255, 0.5)' : undefined
|
boxShadow: isDragging ? '0 0 12px rgba(25, 140, 255, 0.5)' : undefined
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</Box>
|
||||||
<Box flexShrink={0}>
|
<Box flexShrink={0}>
|
||||||
<Group gap={1}>
|
<Group gap={1}>
|
||||||
<Text size="sm" weight="semibold" variant="high" textAlign="right" width="2rem">{clampedValue}</Text>
|
<Text size="sm" weight="semibold" variant="high" textAlign="right" width="2rem">{clampedValue}</Text>
|
||||||
@@ -202,7 +203,8 @@ export function RangeField({
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Custom slider */}
|
{/* Custom slider */}
|
||||||
<div
|
<Box
|
||||||
|
as="div"
|
||||||
ref={sliderRef}
|
ref={sliderRef}
|
||||||
style={{
|
style={{
|
||||||
position: 'relative',
|
position: 'relative',
|
||||||
@@ -265,12 +267,13 @@ export function RangeField({
|
|||||||
boxShadow: isDragging ? '0 0 16px rgba(25, 140, 255, 0.6)' : '0 2px 8px rgba(0,0,0,0.3)'
|
boxShadow: isDragging ? '0 0 16px rgba(25, 140, 255, 0.6)' : '0 2px 8px rgba(0,0,0,0.3)'
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</Box>
|
||||||
|
|
||||||
{/* Value input and quick presets */}
|
{/* Value input and quick presets */}
|
||||||
<Group justify="between" align="center" gap={3}>
|
<Group justify="between" align="center" gap={3}>
|
||||||
<Group gap={2}>
|
<Group gap={2}>
|
||||||
<input
|
<Box
|
||||||
|
as="input"
|
||||||
ref={inputRef}
|
ref={inputRef}
|
||||||
type="number"
|
type="number"
|
||||||
min={min}
|
min={min}
|
||||||
@@ -301,7 +304,8 @@ export function RangeField({
|
|||||||
{quickPresets.length > 0 && (
|
{quickPresets.length > 0 && (
|
||||||
<Group gap={1}>
|
<Group gap={1}>
|
||||||
{quickPresets.slice(0, 3).map((preset) => (
|
{quickPresets.slice(0, 3).map((preset) => (
|
||||||
<button
|
<Box
|
||||||
|
as="button"
|
||||||
key={preset}
|
key={preset}
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
@@ -321,7 +325,7 @@ export function RangeField({
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{preset}
|
{preset}
|
||||||
</button>
|
</Box>
|
||||||
))}
|
))}
|
||||||
</Group>
|
</Group>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import { LoadingSpinner } from '@/ui/LoadingSpinner';
|
|||||||
import { Badge } from '@/ui/Badge';
|
import { Badge } from '@/ui/Badge';
|
||||||
|
|
||||||
import { ProgressLine } from '@/ui/ProgressLine';
|
import { ProgressLine } from '@/ui/ProgressLine';
|
||||||
|
import { SharedEmptyState } from './SharedEmptyState';
|
||||||
|
|
||||||
export {
|
export {
|
||||||
Pagination as SharedPagination,
|
Pagination as SharedPagination,
|
||||||
@@ -38,5 +39,6 @@ export {
|
|||||||
Skeleton as SharedSkeleton,
|
Skeleton as SharedSkeleton,
|
||||||
LoadingSpinner as SharedLoadingSpinner,
|
LoadingSpinner as SharedLoadingSpinner,
|
||||||
Badge as SharedBadge,
|
Badge as SharedBadge,
|
||||||
ProgressLine as SharedProgressLine
|
ProgressLine as SharedProgressLine,
|
||||||
|
SharedEmptyState
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -84,8 +84,7 @@ export function TransactionTable({ transactions, onDownload }: TransactionTableP
|
|||||||
return (
|
return (
|
||||||
<Grid
|
<Grid
|
||||||
key={tx.id}
|
key={tx.id}
|
||||||
cols={1}
|
cols={{ base: 1, md: 12 }}
|
||||||
mdCols={12}
|
|
||||||
gap={4}
|
gap={4}
|
||||||
p={4}
|
p={4}
|
||||||
alignItems="center"
|
alignItems="center"
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useCreateTeam } from "@/hooks/team/useCreateTeam";
|
import { useCreateTeam } from "@/hooks/team/useCreateTeam";
|
||||||
|
import { Box } from '@/ui/Box';
|
||||||
import { Button } from '@/ui/Button';
|
import { Button } from '@/ui/Button';
|
||||||
import { InfoBanner } from '@/ui/InfoBanner';
|
import { InfoBanner } from '@/ui/InfoBanner';
|
||||||
import { Input } from '@/ui/Input';
|
import { Input } from '@/ui/Input';
|
||||||
@@ -79,7 +80,7 @@ export function CreateTeamForm({ onCancel, onSuccess, onNavigate }: CreateTeamFo
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form onSubmit={handleSubmit}>
|
<Box as="form" onSubmit={handleSubmit}>
|
||||||
<Group direction="col" align="stretch" gap={6}>
|
<Group direction="col" align="stretch" gap={6}>
|
||||||
<Input
|
<Input
|
||||||
label="Team Name *"
|
label="Team Name *"
|
||||||
@@ -142,6 +143,6 @@ export function CreateTeamForm({ onCancel, onSuccess, onNavigate }: CreateTeamFo
|
|||||||
)}
|
)}
|
||||||
</Group>
|
</Group>
|
||||||
</Group>
|
</Group>
|
||||||
</form>
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,8 +19,7 @@ import { SectionHeader } from '@/ui/SectionHeader';
|
|||||||
import { Box } from '@/ui/Box';
|
import { Box } from '@/ui/Box';
|
||||||
import { Group } from '@/ui/Group';
|
import { Group } from '@/ui/Group';
|
||||||
import { Stack } from '@/ui/Stack';
|
import { Stack } from '@/ui/Stack';
|
||||||
import { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import React from 'react';
|
|
||||||
|
|
||||||
interface TeamAdminProps {
|
interface TeamAdminProps {
|
||||||
team: {
|
team: {
|
||||||
|
|||||||
@@ -10,7 +10,13 @@ export function useForgotPassword(
|
|||||||
const authService = useInject(AUTH_SERVICE_TOKEN);
|
const authService = useInject(AUTH_SERVICE_TOKEN);
|
||||||
|
|
||||||
return useMutation<{ message: string; magicLink?: string }, ApiError, ForgotPasswordDTO>({
|
return useMutation<{ message: string; magicLink?: string }, ApiError, ForgotPasswordDTO>({
|
||||||
mutationFn: (params) => authService.forgotPassword(params),
|
mutationFn: async (params) => {
|
||||||
|
const result = await authService.forgotPassword(params);
|
||||||
|
if (result.isErr()) {
|
||||||
|
throw result.getError();
|
||||||
|
}
|
||||||
|
return result.unwrap();
|
||||||
|
},
|
||||||
...options,
|
...options,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -11,7 +11,13 @@ export function useLogin(
|
|||||||
const authService = useInject(AUTH_SERVICE_TOKEN);
|
const authService = useInject(AUTH_SERVICE_TOKEN);
|
||||||
|
|
||||||
return useMutation<SessionViewModel, ApiError, LoginParamsDTO>({
|
return useMutation<SessionViewModel, ApiError, LoginParamsDTO>({
|
||||||
mutationFn: (params) => authService.login(params),
|
mutationFn: async (params) => {
|
||||||
|
const result = await authService.login(params);
|
||||||
|
if (result.isErr()) {
|
||||||
|
throw result.getError();
|
||||||
|
}
|
||||||
|
return result.unwrap();
|
||||||
|
},
|
||||||
...options,
|
...options,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -10,7 +10,13 @@ export function useResetPassword(
|
|||||||
const authService = useInject(AUTH_SERVICE_TOKEN);
|
const authService = useInject(AUTH_SERVICE_TOKEN);
|
||||||
|
|
||||||
return useMutation<{ message: string }, ApiError, ResetPasswordDTO>({
|
return useMutation<{ message: string }, ApiError, ResetPasswordDTO>({
|
||||||
mutationFn: (params) => authService.resetPassword(params),
|
mutationFn: async (params) => {
|
||||||
|
const result = await authService.resetPassword(params);
|
||||||
|
if (result.isErr()) {
|
||||||
|
throw result.getError();
|
||||||
|
}
|
||||||
|
return result.unwrap();
|
||||||
|
},
|
||||||
...options,
|
...options,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -11,7 +11,13 @@ export function useSignup(
|
|||||||
const authService = useInject(AUTH_SERVICE_TOKEN);
|
const authService = useInject(AUTH_SERVICE_TOKEN);
|
||||||
|
|
||||||
return useMutation<SessionViewModel, ApiError, SignupParamsDTO>({
|
return useMutation<SessionViewModel, ApiError, SignupParamsDTO>({
|
||||||
mutationFn: (params) => authService.signup(params),
|
mutationFn: async (params) => {
|
||||||
|
const result = await authService.signup(params);
|
||||||
|
if (result.isErr()) {
|
||||||
|
throw result.getError();
|
||||||
|
}
|
||||||
|
return result.unwrap();
|
||||||
|
},
|
||||||
...options,
|
...options,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -17,7 +17,10 @@ export function useDriverProfilePageData(driverId: string) {
|
|||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
if (!driverId) return [];
|
if (!driverId) return [];
|
||||||
|
|
||||||
const allTeams = await teamService.getAllTeams();
|
const allTeamsResult = await teamService.getAllTeams();
|
||||||
|
if (allTeamsResult.isErr()) return [];
|
||||||
|
const allTeams = allTeamsResult.unwrap();
|
||||||
|
|
||||||
let teamMemberships: Array<{
|
let teamMemberships: Array<{
|
||||||
team: { id: string; name: string };
|
team: { id: string; name: string };
|
||||||
role: string;
|
role: string;
|
||||||
@@ -25,7 +28,10 @@ export function useDriverProfilePageData(driverId: string) {
|
|||||||
}> = [];
|
}> = [];
|
||||||
|
|
||||||
for (const team of allTeams) {
|
for (const team of allTeams) {
|
||||||
const teamMembers = await teamService.getTeamMembers(team.id, driverId, '');
|
const teamMembersResult = await teamService.getTeamMembers(team.id, driverId, '');
|
||||||
|
if (teamMembersResult.isErr()) continue;
|
||||||
|
const teamMembers = teamMembersResult.unwrap();
|
||||||
|
|
||||||
const membership = teamMembers?.find(member => member.driverId === driverId);
|
const membership = teamMembers?.find(member => member.driverId === driverId);
|
||||||
if (membership) {
|
if (membership) {
|
||||||
teamMemberships.push({
|
teamMemberships.push({
|
||||||
|
|||||||
@@ -5,14 +5,14 @@ import { ApiError } from '@/lib/api/base/ApiError';
|
|||||||
import { CreateLeagueInputDTO } from '@/lib/types/generated/CreateLeagueInputDTO';
|
import { CreateLeagueInputDTO } from '@/lib/types/generated/CreateLeagueInputDTO';
|
||||||
import { CreateLeagueOutputDTO } from '@/lib/types/generated/CreateLeagueOutputDTO';
|
import { CreateLeagueOutputDTO } from '@/lib/types/generated/CreateLeagueOutputDTO';
|
||||||
|
|
||||||
interface CreateLeagueInput {
|
export interface CreateLeagueInput {
|
||||||
name: string;
|
name: string;
|
||||||
description: string;
|
description: string;
|
||||||
maxDrivers: number;
|
maxDrivers: number;
|
||||||
scoringPresetId: string;
|
scoringPresetId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface CreateLeagueResult {
|
export interface CreateLeagueResult {
|
||||||
success: boolean;
|
success: boolean;
|
||||||
leagueId: string;
|
leagueId: string;
|
||||||
error?: string;
|
error?: string;
|
||||||
|
|||||||
@@ -22,9 +22,13 @@ export function useLeagueDetail({ leagueId, queryOptions }: UseLeagueDetailOptio
|
|||||||
return useQuery<LeagueWithCapacityAndScoringDTO, ApiError>({
|
return useQuery<LeagueWithCapacityAndScoringDTO, ApiError>({
|
||||||
queryKey: ['league-detail', leagueId],
|
queryKey: ['league-detail', leagueId],
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
const result = await leagueService.getAllLeagues() as AllLeaguesWithCapacityAndScoringDTO;
|
const result = await leagueService.getAllLeagues();
|
||||||
|
if (result.isErr()) {
|
||||||
|
throw result.getError();
|
||||||
|
}
|
||||||
|
const data = result.unwrap();
|
||||||
// Filter for the specific league
|
// Filter for the specific league
|
||||||
const leagues = Array.isArray(result?.leagues) ? result.leagues : [];
|
const leagues = Array.isArray(data?.leagues) ? data.leagues : [];
|
||||||
const league = leagues.find(l => l.id === leagueId);
|
const league = leagues.find(l => l.id === leagueId);
|
||||||
if (!league) {
|
if (!league) {
|
||||||
throw new ApiError('League not found', 'NOT_FOUND', {
|
throw new ApiError('League not found', 'NOT_FOUND', {
|
||||||
@@ -46,8 +50,11 @@ export function useLeagueMemberships({ leagueId, queryOptions }: UseLeagueMember
|
|||||||
queryKey: ['league-memberships', leagueId],
|
queryKey: ['league-memberships', leagueId],
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
const result = await leagueService.getLeagueMemberships(leagueId);
|
const result = await leagueService.getLeagueMemberships(leagueId);
|
||||||
|
if (result.isErr()) {
|
||||||
|
throw result.getError();
|
||||||
|
}
|
||||||
// The DTO already has the correct structure with members property
|
// The DTO already has the correct structure with members property
|
||||||
return result;
|
return result.unwrap();
|
||||||
},
|
},
|
||||||
...queryOptions,
|
...queryOptions,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ export function useLeagueSponsorshipsPageData(leagueId: string, currentDriverId:
|
|||||||
return usePageDataMultiple({
|
return usePageDataMultiple({
|
||||||
league: {
|
league: {
|
||||||
queryKey: ['leagueDetail', leagueId, currentDriverId],
|
queryKey: ['leagueDetail', leagueId, currentDriverId],
|
||||||
queryFn: () => leagueService.getLeagueDetail(leagueId),
|
queryFn: () => leagueService.getLeagueDetailData(leagueId),
|
||||||
},
|
},
|
||||||
membership: {
|
membership: {
|
||||||
queryKey: ['leagueMembership', leagueId, currentDriverId],
|
queryKey: ['leagueMembership', leagueId, currentDriverId],
|
||||||
|
|||||||
@@ -17,25 +17,24 @@ export function useLeagueWalletPageData(leagueId: string) {
|
|||||||
// Transform DTO to ViewModel at client boundary
|
// Transform DTO to ViewModel at client boundary
|
||||||
const transactions = dto.transactions.map(t => new WalletTransactionViewModel({
|
const transactions = dto.transactions.map(t => new WalletTransactionViewModel({
|
||||||
id: t.id,
|
id: t.id,
|
||||||
type: t.type,
|
type: t.type as any,
|
||||||
description: t.description,
|
description: t.description,
|
||||||
amount: t.amount,
|
amount: t.amount,
|
||||||
fee: t.fee,
|
fee: 0,
|
||||||
netAmount: t.netAmount,
|
netAmount: t.amount,
|
||||||
date: new globalThis.Date(t.date),
|
date: new globalThis.Date(t.createdAt),
|
||||||
status: t.status,
|
status: t.status,
|
||||||
reference: t.reference,
|
|
||||||
}));
|
}));
|
||||||
return new LeagueWalletViewModel({
|
return new LeagueWalletViewModel({
|
||||||
balance: dto.balance,
|
balance: dto.balance,
|
||||||
currency: dto.currency,
|
currency: dto.currency,
|
||||||
totalRevenue: dto.totalRevenue,
|
totalRevenue: dto.balance, // Fallback
|
||||||
totalFees: dto.totalFees,
|
totalFees: 0,
|
||||||
totalWithdrawals: dto.totalWithdrawals,
|
totalWithdrawals: 0,
|
||||||
pendingPayouts: dto.pendingPayouts,
|
pendingPayouts: 0,
|
||||||
transactions,
|
transactions,
|
||||||
canWithdraw: dto.canWithdraw,
|
canWithdraw: true,
|
||||||
withdrawalBlockReason: dto.withdrawalBlockReason,
|
withdrawalBlockReason: undefined,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
enabled: !!leagueId,
|
enabled: !!leagueId,
|
||||||
|
|||||||
@@ -11,4 +11,22 @@ export class MedalDisplay {
|
|||||||
static getMedalIcon(position: number): string | null {
|
static getMedalIcon(position: number): string | null {
|
||||||
return position <= 3 ? '🏆' : null;
|
return position <= 3 ? '🏆' : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static getBg(position: number): string {
|
||||||
|
switch (position) {
|
||||||
|
case 1: return 'bg-warning-amber';
|
||||||
|
case 2: return 'bg-gray-300';
|
||||||
|
case 3: return 'bg-orange-700';
|
||||||
|
default: return 'bg-gray-800';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static getColor(position: number): string {
|
||||||
|
switch (position) {
|
||||||
|
case 1: return 'text-warning-amber';
|
||||||
|
case 2: return 'text-gray-300';
|
||||||
|
case 3: return 'text-orange-700';
|
||||||
|
default: return 'text-gray-400';
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -52,4 +52,22 @@ export class SponsorService implements Service {
|
|||||||
async getSponsorshipPricing(): Promise<any> {
|
async getSponsorshipPricing(): Promise<any> {
|
||||||
return this.apiClient.getPricing();
|
return this.apiClient.getPricing();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getAvailableLeagues(): Promise<Result<any[], DomainError>> {
|
||||||
|
try {
|
||||||
|
const data = await this.apiClient.getAvailableLeagues();
|
||||||
|
return Result.ok(data);
|
||||||
|
} catch (error) {
|
||||||
|
return Result.err({ type: 'serverError', message: error instanceof Error ? error.message : 'Unknown error' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async getLeagueDetail(leagueId: string): Promise<Result<any, DomainError>> {
|
||||||
|
try {
|
||||||
|
const data = await this.apiClient.getLeagueDetail(leagueId);
|
||||||
|
return Result.ok(data);
|
||||||
|
} catch (error) {
|
||||||
|
return Result.err({ type: 'serverError', message: error instanceof Error ? error.message : 'Unknown error' });
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
// Export the DTO type that WalletTransactionViewModel expects
|
// Export the DTO type that WalletTransactionViewModel expects
|
||||||
export type FullTransactionDto = {
|
export type FullTransactionDto = {
|
||||||
id: string;
|
id: string;
|
||||||
type: 'sponsorship' | 'membership' | 'withdrawal' | 'prize';
|
type: 'sponsorship' | 'membership' | 'withdrawal' | 'prize' | 'deposit';
|
||||||
description: string;
|
description: string;
|
||||||
amount: number;
|
amount: number;
|
||||||
fee: number;
|
fee: number;
|
||||||
@@ -13,7 +13,7 @@ export type FullTransactionDto = {
|
|||||||
|
|
||||||
export class WalletTransactionViewModel {
|
export class WalletTransactionViewModel {
|
||||||
id: string;
|
id: string;
|
||||||
type: 'sponsorship' | 'membership' | 'withdrawal' | 'prize';
|
type: 'sponsorship' | 'membership' | 'withdrawal' | 'prize' | 'deposit';
|
||||||
description: string;
|
description: string;
|
||||||
amount: number;
|
amount: number;
|
||||||
fee: number;
|
fee: number;
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|||||||
|
|
||||||
const nextConfig = {
|
const nextConfig = {
|
||||||
reactStrictMode: true,
|
reactStrictMode: true,
|
||||||
|
output: 'standalone',
|
||||||
// Fix for monorepos: point tracing to repo root (portable across machines/containers)
|
// Fix for monorepos: point tracing to repo root (portable across machines/containers)
|
||||||
outputFileTracingRoot: path.join(__dirname, '../..'),
|
outputFileTracingRoot: path.join(__dirname, '../..'),
|
||||||
images: {
|
images: {
|
||||||
|
|||||||
@@ -8,12 +8,13 @@ import {
|
|||||||
SharedLink,
|
SharedLink,
|
||||||
SharedText,
|
SharedText,
|
||||||
SharedStack,
|
SharedStack,
|
||||||
SharedContainer
|
SharedContainer,
|
||||||
|
SharedIcon
|
||||||
} from '@/components/shared/UIComponents';
|
} from '@/components/shared/UIComponents';
|
||||||
import { ChevronRight } from 'lucide-react';
|
import { ChevronRight } from 'lucide-react';
|
||||||
import { TemplateProps } from '@/lib/contracts/components/ComponentContracts';
|
import { TemplateProps } from '@/lib/contracts/components/ComponentContracts';
|
||||||
|
|
||||||
export function LeagueDetailTemplate({ viewData }: TemplateProps<LeagueDetailViewData>) {
|
export function LeagueDetailTemplate({ viewData, children, tabs }: TemplateProps<LeagueDetailViewData> & { children?: React.ReactNode, tabs?: any[] }) {
|
||||||
return (
|
return (
|
||||||
<SharedContainer size="lg">
|
<SharedContainer size="lg">
|
||||||
<SharedBox paddingY={8}>
|
<SharedBox paddingY={8}>
|
||||||
@@ -27,11 +28,10 @@ export function LeagueDetailTemplate({ viewData }: TemplateProps<LeagueDetailVie
|
|||||||
<SharedText size="sm" color="text-white">{viewData.name}</SharedText>
|
<SharedText size="sm" color="text-white">{viewData.name}</SharedText>
|
||||||
</SharedStack>
|
</SharedStack>
|
||||||
</SharedBox>
|
</SharedBox>
|
||||||
|
{children}
|
||||||
{/* ... rest of the template ... */}
|
{/* ... rest of the template ... */}
|
||||||
</SharedStack>
|
</SharedStack>
|
||||||
</SharedBox>
|
</SharedBox>
|
||||||
</SharedContainer>
|
</SharedContainer>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
import { SharedIcon } from '@/components/shared/UIComponents';
|
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ export function TeamDetailTemplate({
|
|||||||
return (
|
return (
|
||||||
<Box bg="base-black" minH="screen" display="flex" center>
|
<Box bg="base-black" minH="screen" display="flex" center>
|
||||||
<Stack align="center" gap={4}>
|
<Stack align="center" gap={4}>
|
||||||
<Box w="12" h="12" border={4} borderColor="primary-accent" borderOpacity={0.2} borderTop borderTopColor="primary-accent" rounded="full" animate="spin" />
|
<Box w="12" h="12" border borderColor="primary-accent" borderOpacity={0.2} borderTop borderTopColor="primary-accent" rounded="full" animate="spin" />
|
||||||
<Text color="text-gray-500" font="mono" size="xs" uppercase letterSpacing="widest">Synchronizing Telemetry...</Text>
|
<Text color="text-gray-500" font="mono" size="xs" uppercase letterSpacing="widest">Synchronizing Telemetry...</Text>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { TeamGrid } from '@/components/teams/TeamGrid';
|
|||||||
import { TeamLeaderboardPreview } from '@/components/teams/TeamLeaderboardPreviewWrapper';
|
import { TeamLeaderboardPreview } from '@/components/teams/TeamLeaderboardPreviewWrapper';
|
||||||
import { TeamsDirectoryHeader } from '@/components/teams/TeamsDirectoryHeader';
|
import { TeamsDirectoryHeader } from '@/components/teams/TeamsDirectoryHeader';
|
||||||
import { TeamsDirectory, TeamsDirectorySection } from '@/components/teams/TeamsDirectory';
|
import { TeamsDirectory, TeamsDirectorySection } from '@/components/teams/TeamsDirectory';
|
||||||
import { SharedEmptyState } from '@/components/shared/SharedEmptyState';
|
import { SharedEmptyState } from '@/components/shared/UIComponents';
|
||||||
import type { TeamsViewData } from '@/lib/view-data/TeamsViewData';
|
import type { TeamsViewData } from '@/lib/view-data/TeamsViewData';
|
||||||
import { Users } from 'lucide-react';
|
import { Users } from 'lucide-react';
|
||||||
import { TemplateProps } from '@/lib/contracts/components/ComponentContracts';
|
import { TemplateProps } from '@/lib/contracts/components/ComponentContracts';
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { AuthedNav } from '@/components/layout/AuthedNav';
|
|||||||
import { PublicNav } from '@/components/layout/PublicNav';
|
import { PublicNav } from '@/components/layout/PublicNav';
|
||||||
import { useCurrentSession } from '@/hooks/auth/useCurrentSession';
|
import { useCurrentSession } from '@/hooks/auth/useCurrentSession';
|
||||||
import { Box } from '@/ui/Box';
|
import { Box } from '@/ui/Box';
|
||||||
import { DashboardRail } from '@/ui/DashboardRail';
|
import { DashboardRail } from '@/components/dashboard/DashboardRail';
|
||||||
import { Text } from '@/ui/Text';
|
import { Text } from '@/ui/Text';
|
||||||
import { usePathname } from 'next/navigation';
|
import { usePathname } from 'next/navigation';
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { BrandMark } from '@/components/layout/BrandMark';
|
import { BrandMark } from '@/ui/BrandMark';
|
||||||
import { HeaderActions } from '@/components/layout/HeaderActions';
|
import { HeaderActions } from '@/components/layout/HeaderActions';
|
||||||
import { PublicNav } from '@/components/layout/PublicNav';
|
import { PublicNav } from '@/components/layout/PublicNav';
|
||||||
import { useCurrentSession } from '@/hooks/auth/useCurrentSession';
|
import { useCurrentSession } from '@/hooks/auth/useCurrentSession';
|
||||||
|
|||||||
@@ -70,13 +70,17 @@
|
|||||||
"include": [
|
"include": [
|
||||||
"app/",
|
"app/",
|
||||||
"components/",
|
"components/",
|
||||||
|
"ui/",
|
||||||
|
"templates/",
|
||||||
|
"hooks/",
|
||||||
|
"client-wrapper/",
|
||||||
"lib/",
|
"lib/",
|
||||||
"next-env.d.ts",
|
"next-env.d.ts",
|
||||||
"env.d.ts",
|
"env.d.ts",
|
||||||
"types/",
|
"types/",
|
||||||
"utilities/",
|
"utilities/",
|
||||||
".next/types/**/*.ts"
|
".next/types/**/*.ts"
|
||||||
, "hooks/sponsor/useSponsorMode.ts" ],
|
],
|
||||||
"exclude": [
|
"exclude": [
|
||||||
"**/*.test.ts",
|
"**/*.test.ts",
|
||||||
"**/*.test.tsx",
|
"**/*.test.tsx",
|
||||||
|
|||||||
@@ -9,19 +9,35 @@ export interface AccordionProps {
|
|||||||
title: string;
|
title: string;
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
defaultOpen?: boolean;
|
defaultOpen?: boolean;
|
||||||
|
isOpen?: boolean;
|
||||||
|
onToggle?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Accordion = ({
|
export const Accordion = ({
|
||||||
title,
|
title,
|
||||||
children,
|
children,
|
||||||
defaultOpen = false
|
defaultOpen = false,
|
||||||
|
isOpen: controlledIsOpen,
|
||||||
|
onToggle
|
||||||
}: AccordionProps) => {
|
}: AccordionProps) => {
|
||||||
const [isOpen, setIsOpen] = useState(defaultOpen);
|
const [internalIsOpen, setInternalIsOpen] = useState(defaultOpen);
|
||||||
|
|
||||||
|
const isControlled = controlledIsOpen !== undefined;
|
||||||
|
const isOpen = isControlled ? controlledIsOpen : internalIsOpen;
|
||||||
|
|
||||||
|
const handleToggle = () => {
|
||||||
|
if (onToggle) {
|
||||||
|
onToggle();
|
||||||
|
}
|
||||||
|
if (!isControlled) {
|
||||||
|
setInternalIsOpen(!internalIsOpen);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Surface variant="muted" rounded="lg" style={{ border: '1px solid var(--ui-color-border-default)', overflow: 'hidden' }}>
|
<Surface variant="muted" rounded="lg" style={{ border: '1px solid var(--ui-color-border-default)', overflow: 'hidden' }}>
|
||||||
<button
|
<button
|
||||||
onClick={() => setIsOpen(!isOpen)}
|
onClick={handleToggle}
|
||||||
className="w-full flex items-center justify-between px-4 py-3 hover:bg-white/5 transition-colors"
|
className="w-full flex items-center justify-between px-4 py-3 hover:bg-white/5 transition-colors"
|
||||||
>
|
>
|
||||||
<Text weight="bold" size="sm" variant="high">
|
<Text weight="bold" size="sm" variant="high">
|
||||||
|
|||||||
@@ -6,37 +6,43 @@ import { Surface } from './Surface';
|
|||||||
export interface AvatarProps {
|
export interface AvatarProps {
|
||||||
src?: string;
|
src?: string;
|
||||||
alt?: string;
|
alt?: string;
|
||||||
size?: 'sm' | 'md' | 'lg' | 'xl';
|
size?: 'sm' | 'md' | 'lg' | 'xl' | number;
|
||||||
fallback?: string;
|
fallback?: string;
|
||||||
|
className?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Avatar = ({
|
export const Avatar = ({
|
||||||
src,
|
src,
|
||||||
alt,
|
alt,
|
||||||
size = 'md',
|
size = 'md',
|
||||||
fallback
|
fallback,
|
||||||
|
className
|
||||||
}: AvatarProps) => {
|
}: AvatarProps) => {
|
||||||
const sizeMap = {
|
const sizeMap: Record<string, string> = {
|
||||||
sm: '2rem',
|
sm: '2rem',
|
||||||
md: '3rem',
|
md: '3rem',
|
||||||
lg: '4rem',
|
lg: '4rem',
|
||||||
xl: '6rem'
|
xl: '6rem'
|
||||||
};
|
};
|
||||||
|
|
||||||
const iconSizeMap = {
|
const iconSizeMap: Record<string, number> = {
|
||||||
sm: 3,
|
sm: 3,
|
||||||
md: 5,
|
md: 5,
|
||||||
lg: 8,
|
lg: 8,
|
||||||
xl: 12
|
xl: 12
|
||||||
} as const;
|
};
|
||||||
|
|
||||||
|
const finalSize = typeof size === 'number' ? `${size / 4}rem` : sizeMap[size];
|
||||||
|
const finalIconSize = typeof size === 'number' ? Math.round(size / 8) : iconSizeMap[size];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Surface
|
<Surface
|
||||||
variant="muted"
|
variant="muted"
|
||||||
rounded="full"
|
rounded="full"
|
||||||
|
className={className}
|
||||||
style={{
|
style={{
|
||||||
width: sizeMap[size],
|
width: finalSize,
|
||||||
height: sizeMap[size],
|
height: finalSize,
|
||||||
overflow: 'hidden',
|
overflow: 'hidden',
|
||||||
border: '2px solid var(--ui-color-border-default)'
|
border: '2px solid var(--ui-color-border-default)'
|
||||||
}}
|
}}
|
||||||
@@ -52,7 +58,7 @@ export const Avatar = ({
|
|||||||
{fallback ? (
|
{fallback ? (
|
||||||
<span className="text-sm font-bold text-[var(--ui-color-text-med)]">{fallback}</span>
|
<span className="text-sm font-bold text-[var(--ui-color-text-med)]">{fallback}</span>
|
||||||
) : (
|
) : (
|
||||||
<Icon icon={User} size={iconSizeMap[size]} intent="low" />
|
<Icon icon={User} size={finalIconSize as any} intent="low" />
|
||||||
)}
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -7,17 +7,25 @@ import { Stack } from './Stack';
|
|||||||
export interface BadgeProps {
|
export interface BadgeProps {
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
variant?: 'primary' | 'secondary' | 'success' | 'warning' | 'critical' | 'info' | 'outline' | 'default' | 'danger';
|
variant?: 'primary' | 'secondary' | 'success' | 'warning' | 'critical' | 'info' | 'outline' | 'default' | 'danger';
|
||||||
size?: 'sm' | 'md';
|
size?: 'xs' | 'sm' | 'md';
|
||||||
style?: React.CSSProperties;
|
style?: React.CSSProperties;
|
||||||
icon?: LucideIcon;
|
icon?: LucideIcon;
|
||||||
|
rounded?: string;
|
||||||
|
bg?: string;
|
||||||
|
color?: string;
|
||||||
|
borderColor?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Badge = ({
|
export const Badge = ({
|
||||||
children,
|
children,
|
||||||
variant = 'primary',
|
variant = 'primary',
|
||||||
size = 'md',
|
size = 'md',
|
||||||
style,
|
style: styleProp,
|
||||||
icon
|
icon,
|
||||||
|
rounded,
|
||||||
|
bg,
|
||||||
|
color,
|
||||||
|
borderColor
|
||||||
}: BadgeProps) => {
|
}: BadgeProps) => {
|
||||||
const variantClasses = {
|
const variantClasses = {
|
||||||
primary: 'bg-[var(--ui-color-intent-primary)] text-white',
|
primary: 'bg-[var(--ui-color-intent-primary)] text-white',
|
||||||
@@ -32,16 +40,25 @@ export const Badge = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const sizeClasses = {
|
const sizeClasses = {
|
||||||
|
xs: 'px-1 py-0 text-[9px]',
|
||||||
sm: 'px-1.5 py-0.5 text-[10px]',
|
sm: 'px-1.5 py-0.5 text-[10px]',
|
||||||
md: 'px-2 py-0.5 text-xs',
|
md: 'px-2 py-0.5 text-xs',
|
||||||
};
|
};
|
||||||
|
|
||||||
const classes = [
|
const classes = [
|
||||||
'inline-flex items-center justify-center font-bold uppercase tracking-wider rounded-none',
|
'inline-flex items-center justify-center font-bold uppercase tracking-wider',
|
||||||
variantClasses[variant],
|
variantClasses[variant],
|
||||||
sizeClasses[size],
|
sizeClasses[size],
|
||||||
|
rounded ? (rounded === 'full' ? 'rounded-full' : `rounded-${rounded}`) : 'rounded-none',
|
||||||
].join(' ');
|
].join(' ');
|
||||||
|
|
||||||
|
const style: React.CSSProperties = {
|
||||||
|
...styleProp,
|
||||||
|
...(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' } : {}),
|
||||||
|
};
|
||||||
|
|
||||||
const content = icon ? (
|
const content = icon ? (
|
||||||
<Stack direction="row" align="center" gap={1}>
|
<Stack direction="row" align="center" gap={1}>
|
||||||
<Icon icon={icon} size={3} />
|
<Icon icon={icon} size={3} />
|
||||||
|
|||||||
@@ -1,21 +1,32 @@
|
|||||||
import { Box } from './Box';
|
import { Box } from './Box';
|
||||||
|
|
||||||
|
|
||||||
|
export interface TabOption {
|
||||||
|
id: string;
|
||||||
|
label: string;
|
||||||
|
icon?: React.ReactNode;
|
||||||
|
count?: number;
|
||||||
|
}
|
||||||
|
|
||||||
export interface BorderTabsProps {
|
export interface BorderTabsProps {
|
||||||
tabs: TabOption[];
|
tabs: TabOption[];
|
||||||
activeTabId: string;
|
activeTabId?: string;
|
||||||
onTabChange: (id: string) => void;
|
onTabChange: (id: string) => void;
|
||||||
|
activeTab?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const BorderTabs = ({
|
export const BorderTabs = ({
|
||||||
tabs,
|
tabs,
|
||||||
activeTabId,
|
activeTabId,
|
||||||
onTabChange
|
onTabChange,
|
||||||
|
activeTab
|
||||||
}: BorderTabsProps) => {
|
}: BorderTabsProps) => {
|
||||||
|
const finalActiveTabId = activeTabId || activeTab || '';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box display="flex" borderBottom>
|
<Box display="flex" borderBottom>
|
||||||
{tabs.map((tab) => {
|
{tabs.map((tab) => {
|
||||||
const isActive = tab.id === activeTabId;
|
const isActive = tab.id === finalActiveTabId;
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
key={tab.id}
|
key={tab.id}
|
||||||
@@ -29,6 +40,11 @@ export const BorderTabs = ({
|
|||||||
<Box display="flex" alignItems="center" gap={2}>
|
<Box display="flex" alignItems="center" gap={2}>
|
||||||
{tab.icon}
|
{tab.icon}
|
||||||
{tab.label}
|
{tab.label}
|
||||||
|
{tab.count !== undefined && (
|
||||||
|
<Box as="span" px={1.5} py={0.5} rounded="full" bg="white/10" fontSize="10px">
|
||||||
|
{tab.count}
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
{isActive && (
|
{isActive && (
|
||||||
<Box
|
<Box
|
||||||
|
|||||||
@@ -52,12 +52,12 @@ export interface BoxProps<T extends ElementType> {
|
|||||||
|
|
||||||
// Aliases (Deprecated - use full names)
|
// Aliases (Deprecated - use full names)
|
||||||
m?: Spacing | ResponsiveSpacing;
|
m?: Spacing | ResponsiveSpacing;
|
||||||
mt?: Spacing | ResponsiveSpacing;
|
mt?: Spacing | 'auto' | ResponsiveSpacing;
|
||||||
mb?: Spacing | ResponsiveSpacing;
|
mb?: Spacing | 'auto' | ResponsiveSpacing;
|
||||||
ml?: Spacing | 'auto' | ResponsiveSpacing;
|
ml?: Spacing | 'auto' | ResponsiveSpacing;
|
||||||
mr?: Spacing | 'auto' | ResponsiveSpacing;
|
mr?: Spacing | 'auto' | ResponsiveSpacing;
|
||||||
mx?: Spacing | 'auto' | ResponsiveSpacing;
|
mx?: Spacing | 'auto' | ResponsiveSpacing;
|
||||||
my?: Spacing | ResponsiveSpacing;
|
my?: Spacing | 'auto' | ResponsiveSpacing;
|
||||||
p?: Spacing | ResponsiveSpacing;
|
p?: Spacing | ResponsiveSpacing;
|
||||||
pt?: Spacing | ResponsiveSpacing;
|
pt?: Spacing | ResponsiveSpacing;
|
||||||
pb?: Spacing | ResponsiveSpacing;
|
pb?: Spacing | ResponsiveSpacing;
|
||||||
@@ -216,6 +216,19 @@ export interface BoxProps<T extends ElementType> {
|
|||||||
backgroundSize?: string;
|
backgroundSize?: string;
|
||||||
backgroundPosition?: string;
|
backgroundPosition?: string;
|
||||||
backgroundImage?: string;
|
backgroundImage?: string;
|
||||||
|
htmlFor?: string;
|
||||||
|
minH?: string | number | ResponsiveValue<string | number>;
|
||||||
|
borderOpacity?: number;
|
||||||
|
rows?: number;
|
||||||
|
backgroundColor?: string;
|
||||||
|
outline?: string;
|
||||||
|
focusBorderColor?: string;
|
||||||
|
href?: string;
|
||||||
|
name?: string;
|
||||||
|
borderTopColor?: string;
|
||||||
|
onPointerDown?: React.PointerEventHandler<any>;
|
||||||
|
onPointerMove?: React.PointerEventHandler<any>;
|
||||||
|
onPointerUp?: React.PointerEventHandler<any>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Box = forwardRef(<T extends ElementType = 'div'>(
|
export const Box = forwardRef(<T extends ElementType = 'div'>(
|
||||||
@@ -354,6 +367,19 @@ export const Box = forwardRef(<T extends ElementType = 'div'>(
|
|||||||
backgroundSize,
|
backgroundSize,
|
||||||
backgroundPosition,
|
backgroundPosition,
|
||||||
backgroundImage,
|
backgroundImage,
|
||||||
|
htmlFor,
|
||||||
|
minH,
|
||||||
|
borderOpacity,
|
||||||
|
rows,
|
||||||
|
backgroundColor,
|
||||||
|
outline,
|
||||||
|
focusBorderColor,
|
||||||
|
href,
|
||||||
|
name,
|
||||||
|
borderTopColor,
|
||||||
|
onPointerDown,
|
||||||
|
onPointerMove,
|
||||||
|
onPointerUp,
|
||||||
...props
|
...props
|
||||||
}: BoxProps<T>,
|
}: BoxProps<T>,
|
||||||
ref: ForwardedRef<HTMLElement>
|
ref: ForwardedRef<HTMLElement>
|
||||||
@@ -417,7 +443,7 @@ export const Box = forwardRef(<T extends ElementType = 'div'>(
|
|||||||
getResponsiveClasses('max-w', maxWidth),
|
getResponsiveClasses('max-w', maxWidth),
|
||||||
getResponsiveClasses('min-w', minWidth),
|
getResponsiveClasses('min-w', minWidth),
|
||||||
getResponsiveClasses('max-h', maxHeight),
|
getResponsiveClasses('max-h', maxHeight),
|
||||||
getResponsiveClasses('min-h', minHeight),
|
getResponsiveClasses('min-h', minHeight || minH),
|
||||||
getResponsiveClasses('', display),
|
getResponsiveClasses('', display),
|
||||||
center ? 'flex items-center justify-center' : '',
|
center ? 'flex items-center justify-center' : '',
|
||||||
overflow ? (overflow.includes(':') ? overflow : `overflow-${overflow}`) : '',
|
overflow ? (overflow.includes(':') ? overflow : `overflow-${overflow}`) : '',
|
||||||
@@ -464,6 +490,7 @@ export const Box = forwardRef(<T extends ElementType = 'div'>(
|
|||||||
...(typeof minWidth === 'string' ? { minWidth } : {}),
|
...(typeof minWidth === 'string' ? { minWidth } : {}),
|
||||||
...(typeof maxHeight === 'string' ? { maxHeight } : {}),
|
...(typeof maxHeight === 'string' ? { maxHeight } : {}),
|
||||||
...(typeof minHeight === 'string' ? { minHeight } : {}),
|
...(typeof minHeight === 'string' ? { minHeight } : {}),
|
||||||
|
...(typeof minH === 'string' || typeof minH === 'number' ? { minHeight: minH } : {}),
|
||||||
...(aspectRatio ? { aspectRatio } : {}),
|
...(aspectRatio ? { aspectRatio } : {}),
|
||||||
...(typeof top === 'string' || typeof top === 'number' ? { top } : {}),
|
...(typeof top === 'string' || typeof top === 'number' ? { top } : {}),
|
||||||
...(typeof right === 'string' || typeof right === 'number' ? { right } : {}),
|
...(typeof right === 'string' || typeof right === 'number' ? { right } : {}),
|
||||||
@@ -473,7 +500,9 @@ export const Box = forwardRef(<T extends ElementType = 'div'>(
|
|||||||
...(typeof borderBottom === 'string' ? { borderBottom } : (borderBottom === true ? { borderBottom: '1px solid var(--ui-color-border-default)' } : {})),
|
...(typeof borderBottom === 'string' ? { borderBottom } : (borderBottom === true ? { borderBottom: '1px solid var(--ui-color-border-default)' } : {})),
|
||||||
...(typeof borderLeft === 'string' ? { borderLeft } : (borderLeft === true ? { borderLeft: '1px solid var(--ui-color-border-default)' } : {})),
|
...(typeof borderLeft === 'string' ? { borderLeft } : (borderLeft === true ? { borderLeft: '1px solid var(--ui-color-border-default)' } : {})),
|
||||||
...(typeof borderRight === 'string' ? { borderRight } : (borderRight === true ? { borderRight: '1px solid var(--ui-color-border-default)' } : {})),
|
...(typeof borderRight === 'string' ? { borderRight } : (borderRight === true ? { borderRight: '1px solid var(--ui-color-border-default)' } : {})),
|
||||||
...(bg ? { background: bg.startsWith('bg-') ? undefined : bg } : {}),
|
...(borderTopColor ? { borderTopColor: borderTopColor.startsWith('border-') ? undefined : borderTopColor } : {}),
|
||||||
|
...(borderOpacity !== undefined ? { borderOpacity } : {}),
|
||||||
|
...(bg || backgroundColor ? { background: (bg || backgroundColor)!.startsWith('bg-') ? undefined : (bg || backgroundColor) } : {}),
|
||||||
...(rounded === true ? { borderRadius: 'var(--ui-radius-md)' } : (typeof rounded === 'string' ? { borderRadius: rounded.includes('rem') || rounded.includes('px') ? rounded : `var(--ui-radius-${rounded})` } : {})),
|
...(rounded === true ? { borderRadius: 'var(--ui-radius-md)' } : (typeof rounded === 'string' ? { borderRadius: rounded.includes('rem') || rounded.includes('px') ? rounded : `var(--ui-radius-${rounded})` } : {})),
|
||||||
...(borderColor ? { borderColor: borderColor.startsWith('border-') ? undefined : borderColor } : {}),
|
...(borderColor ? { borderColor: borderColor.startsWith('border-') ? undefined : borderColor } : {}),
|
||||||
...(border === true ? { border: '1px solid var(--ui-color-border-default)' } : (typeof border === 'string' ? { border } : {})),
|
...(border === true ? { border: '1px solid var(--ui-color-border-default)' } : (typeof border === 'string' ? { border } : {})),
|
||||||
@@ -531,6 +560,13 @@ export const Box = forwardRef(<T extends ElementType = 'div'>(
|
|||||||
muted={muted}
|
muted={muted}
|
||||||
playsInline={playsInline}
|
playsInline={playsInline}
|
||||||
viewBox={viewBox}
|
viewBox={viewBox}
|
||||||
|
htmlFor={htmlFor}
|
||||||
|
rows={rows}
|
||||||
|
href={href}
|
||||||
|
name={name}
|
||||||
|
onPointerDown={onPointerDown}
|
||||||
|
onPointerMove={onPointerMove}
|
||||||
|
onPointerUp={onPointerUp}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
|
|||||||
@@ -19,7 +19,32 @@ export interface ButtonProps {
|
|||||||
rel?: string;
|
rel?: string;
|
||||||
title?: string;
|
title?: string;
|
||||||
style?: React.CSSProperties;
|
style?: React.CSSProperties;
|
||||||
rounded?: boolean;
|
rounded?: boolean | string;
|
||||||
|
className?: string;
|
||||||
|
bg?: string;
|
||||||
|
color?: string;
|
||||||
|
w?: string | number;
|
||||||
|
h?: string | number;
|
||||||
|
px?: number;
|
||||||
|
borderColor?: string;
|
||||||
|
mt?: number;
|
||||||
|
position?: 'static' | 'relative' | 'absolute' | 'fixed' | 'sticky';
|
||||||
|
letterSpacing?: string;
|
||||||
|
hoverBorderColor?: string;
|
||||||
|
fontSize?: string;
|
||||||
|
p?: number;
|
||||||
|
minHeight?: string;
|
||||||
|
transition?: boolean;
|
||||||
|
ring?: string;
|
||||||
|
transform?: string;
|
||||||
|
hoverScale?: boolean;
|
||||||
|
overflow?: string;
|
||||||
|
borderWidth?: string;
|
||||||
|
aspectRatio?: string;
|
||||||
|
border?: boolean;
|
||||||
|
shadow?: string;
|
||||||
|
display?: string;
|
||||||
|
center?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Button = forwardRef<HTMLButtonElement | HTMLAnchorElement, ButtonProps>(({
|
export const Button = forwardRef<HTMLButtonElement | HTMLAnchorElement, ButtonProps>(({
|
||||||
@@ -39,8 +64,34 @@ export const Button = forwardRef<HTMLButtonElement | HTMLAnchorElement, ButtonPr
|
|||||||
title,
|
title,
|
||||||
style: styleProp,
|
style: styleProp,
|
||||||
rounded = false,
|
rounded = false,
|
||||||
|
className,
|
||||||
|
bg,
|
||||||
|
color,
|
||||||
|
w,
|
||||||
|
h,
|
||||||
|
px,
|
||||||
|
borderColor,
|
||||||
|
mt,
|
||||||
|
position,
|
||||||
|
letterSpacing,
|
||||||
|
hoverBorderColor,
|
||||||
|
fontSize,
|
||||||
|
p,
|
||||||
|
minHeight,
|
||||||
|
transition,
|
||||||
|
ring,
|
||||||
|
transform,
|
||||||
|
hoverScale,
|
||||||
|
overflow,
|
||||||
|
borderWidth,
|
||||||
|
aspectRatio,
|
||||||
|
border,
|
||||||
|
shadow,
|
||||||
|
display,
|
||||||
|
center,
|
||||||
}, ref) => {
|
}, ref) => {
|
||||||
const baseClasses = 'inline-flex items-center justify-center transition-all duration-150 ease-in-out focus-visible:outline focus-visible:outline-1 focus-visible:outline-offset-2 active:opacity-80 uppercase tracking-widest font-bold';
|
const baseClasses = 'inline-flex items-center justify-center focus-visible:outline focus-visible:outline-1 focus-visible:outline-offset-2 active:opacity-80 uppercase tracking-widest font-bold';
|
||||||
|
const transitionClasses = transition !== false ? 'transition-all duration-150 ease-in-out' : '';
|
||||||
|
|
||||||
const variantClasses = {
|
const variantClasses = {
|
||||||
primary: 'bg-[var(--ui-color-intent-primary)] text-white hover:opacity-90 focus-visible:outline-[var(--ui-color-intent-primary)] shadow-[0_0_15px_rgba(25,140,255,0.3)] hover:shadow-[0_0_25px_rgba(25,140,255,0.5)]',
|
primary: 'bg-[var(--ui-color-intent-primary)] text-white hover:opacity-90 focus-visible:outline-[var(--ui-color-intent-primary)] shadow-[0_0_15px_rgba(25,140,255,0.3)] hover:shadow-[0_0_25px_rgba(25,140,255,0.5)]',
|
||||||
@@ -60,17 +111,45 @@ export const Button = forwardRef<HTMLButtonElement | HTMLAnchorElement, ButtonPr
|
|||||||
|
|
||||||
const disabledClasses = (disabled || isLoading) ? 'opacity-40 cursor-not-allowed' : 'cursor-pointer';
|
const disabledClasses = (disabled || isLoading) ? 'opacity-40 cursor-not-allowed' : 'cursor-pointer';
|
||||||
const widthClasses = fullWidth ? 'w-full' : '';
|
const widthClasses = fullWidth ? 'w-full' : '';
|
||||||
const roundedClasses = rounded ? 'rounded-full' : 'rounded-none';
|
const roundedClasses = rounded === true ? 'rounded-full' : (typeof rounded === 'string' ? `rounded-${rounded}` : 'rounded-none');
|
||||||
|
|
||||||
const classes = [
|
const classes = [
|
||||||
baseClasses,
|
baseClasses,
|
||||||
|
transitionClasses,
|
||||||
variantClasses[variant],
|
variantClasses[variant],
|
||||||
sizeClasses[size],
|
sizeClasses[size],
|
||||||
disabledClasses,
|
disabledClasses,
|
||||||
widthClasses,
|
widthClasses,
|
||||||
roundedClasses,
|
roundedClasses,
|
||||||
|
ring,
|
||||||
|
hoverScale ? 'hover:scale-105' : '',
|
||||||
|
display === 'flex' ? 'flex' : '',
|
||||||
|
center ? 'items-center justify-center' : '',
|
||||||
|
className,
|
||||||
].filter(Boolean).join(' ');
|
].filter(Boolean).join(' ');
|
||||||
|
|
||||||
|
const style: React.CSSProperties = {
|
||||||
|
...styleProp,
|
||||||
|
...(bg ? { backgroundColor: bg.startsWith('bg-') ? undefined : bg } : {}),
|
||||||
|
...(color ? { color: color.startsWith('text-') ? undefined : color } : {}),
|
||||||
|
...(w ? { width: w } : {}),
|
||||||
|
...(h ? { height: h } : {}),
|
||||||
|
...(px ? { paddingLeft: `${px * 0.25}rem`, paddingRight: `${px * 0.25}rem` } : {}),
|
||||||
|
...(borderColor ? { borderColor: borderColor.startsWith('border-') ? undefined : borderColor, borderStyle: 'solid', borderWidth: '1px' } : {}),
|
||||||
|
...(mt ? { marginTop: `${mt * 0.25}rem` } : {}),
|
||||||
|
...(position ? { position } : {}),
|
||||||
|
...(letterSpacing ? { letterSpacing } : {}),
|
||||||
|
...(fontSize ? { fontSize } : {}),
|
||||||
|
...(p !== undefined ? { padding: `${p * 0.25}rem` } : {}),
|
||||||
|
...(minHeight ? { minHeight } : {}),
|
||||||
|
...(transform ? { transform } : {}),
|
||||||
|
...(overflow ? { overflow } : {}),
|
||||||
|
...(borderWidth ? { borderWidth } : {}),
|
||||||
|
...(aspectRatio ? { aspectRatio } : {}),
|
||||||
|
...(border ? { border: '1px solid var(--ui-color-border-default)' } : {}),
|
||||||
|
...(shadow ? { boxShadow: shadow.startsWith('shadow-') ? undefined : shadow } : {}),
|
||||||
|
};
|
||||||
|
|
||||||
const content = (
|
const content = (
|
||||||
<Box display="flex" alignItems="center" gap={2}>
|
<Box display="flex" alignItems="center" gap={2}>
|
||||||
{isLoading && <Icon icon={Loader2} size={size === 'sm' ? 3 : 4} animate="spin" />}
|
{isLoading && <Icon icon={Loader2} size={size === 'sm' ? 3 : 4} animate="spin" />}
|
||||||
@@ -88,7 +167,7 @@ export const Button = forwardRef<HTMLButtonElement | HTMLAnchorElement, ButtonPr
|
|||||||
rel={rel}
|
rel={rel}
|
||||||
className={classes}
|
className={classes}
|
||||||
onClick={onClick as MouseEventHandler<HTMLAnchorElement>}
|
onClick={onClick as MouseEventHandler<HTMLAnchorElement>}
|
||||||
style={styleProp}
|
style={style}
|
||||||
title={title}
|
title={title}
|
||||||
>
|
>
|
||||||
{content}
|
{content}
|
||||||
@@ -103,7 +182,7 @@ export const Button = forwardRef<HTMLButtonElement | HTMLAnchorElement, ButtonPr
|
|||||||
className={classes}
|
className={classes}
|
||||||
onClick={onClick as MouseEventHandler<HTMLButtonElement>}
|
onClick={onClick as MouseEventHandler<HTMLButtonElement>}
|
||||||
disabled={disabled || isLoading}
|
disabled={disabled || isLoading}
|
||||||
style={styleProp}
|
style={style}
|
||||||
title={title}
|
title={title}
|
||||||
>
|
>
|
||||||
{content}
|
{content}
|
||||||
|
|||||||
@@ -8,7 +8,9 @@ export interface CategoryIconProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const CategoryIcon = ({
|
export const CategoryIcon = ({
|
||||||
category}: CategoryIconProps) => {
|
category,
|
||||||
|
size = 24
|
||||||
|
}: CategoryIconProps) => {
|
||||||
// Map categories to icons if needed, for now just use Tag
|
// Map categories to icons if needed, for now just use Tag
|
||||||
return (
|
return (
|
||||||
<Box
|
<Box
|
||||||
|
|||||||
@@ -4,11 +4,17 @@ import { Box } from './Box';
|
|||||||
export interface ContainerProps {
|
export interface ContainerProps {
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
size?: 'sm' | 'md' | 'lg' | 'xl' | 'full';
|
size?: 'sm' | 'md' | 'lg' | 'xl' | 'full';
|
||||||
|
py?: number;
|
||||||
|
position?: 'static' | 'relative' | 'absolute' | 'fixed' | 'sticky';
|
||||||
|
zIndex?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Container = ({
|
export const Container = ({
|
||||||
children,
|
children,
|
||||||
size = 'lg'
|
size = 'lg',
|
||||||
|
py,
|
||||||
|
position,
|
||||||
|
zIndex
|
||||||
}: ContainerProps) => {
|
}: ContainerProps) => {
|
||||||
const sizeMap = {
|
const sizeMap = {
|
||||||
sm: '40rem',
|
sm: '40rem',
|
||||||
@@ -19,7 +25,7 @@ export const Container = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box marginX="auto" maxWidth={sizeMap[size]} paddingX={4} fullWidth>
|
<Box marginX="auto" maxWidth={sizeMap[size]} paddingX={4} py={py as any} fullWidth position={position} zIndex={zIndex}>
|
||||||
{children}
|
{children}
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -5,11 +5,13 @@ import { Container } from './Container';
|
|||||||
export interface ContentViewportProps {
|
export interface ContentViewportProps {
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
padding?: 'none' | 'sm' | 'md' | 'lg';
|
padding?: 'none' | 'sm' | 'md' | 'lg';
|
||||||
|
fullWidth?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ContentViewport = ({
|
export const ContentViewport = ({
|
||||||
children,
|
children,
|
||||||
padding = 'md'
|
padding = 'md',
|
||||||
|
fullWidth = false
|
||||||
}: ContentViewportProps) => {
|
}: ContentViewportProps) => {
|
||||||
const paddingMap: Record<string, any> = {
|
const paddingMap: Record<string, any> = {
|
||||||
none: 0,
|
none: 0,
|
||||||
@@ -20,7 +22,7 @@ export const ContentViewport = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Box as="main" flex={1} overflow="auto">
|
<Box as="main" flex={1} overflow="auto">
|
||||||
<Container size="xl">
|
<Container size={fullWidth ? 'full' : 'xl'}>
|
||||||
<Box paddingY={paddingMap[padding]}>
|
<Box paddingY={paddingMap[padding]}>
|
||||||
{children}
|
{children}
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ export function DashboardHero({
|
|||||||
borderColor="border-[#23272B]"
|
borderColor="border-[#23272B]"
|
||||||
>
|
>
|
||||||
<Avatar
|
<Avatar
|
||||||
src={avatarUrl}
|
src={avatarUrl || undefined}
|
||||||
alt={driverName}
|
alt={driverName}
|
||||||
size={120}
|
size={120}
|
||||||
className="rounded-xl"
|
className="rounded-xl"
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { Card } from '@/ui/Card';
|
|||||||
import { Heading } from '@/ui/Heading';
|
import { Heading } from '@/ui/Heading';
|
||||||
import { Icon } from '@/ui/Icon';
|
import { Icon } from '@/ui/Icon';
|
||||||
import { Text } from '@/ui/Text';
|
import { Text } from '@/ui/Text';
|
||||||
|
import { Copy, RefreshCw, Terminal, X } from 'lucide-react';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
interface DevErrorPanelProps {
|
interface DevErrorPanelProps {
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { Avatar } from '@/ui/Avatar';
|
import { Avatar } from '@/ui/Avatar';
|
||||||
|
import { Badge } from '@/ui/Badge';
|
||||||
import { Box } from '@/ui/Box';
|
import { Box } from '@/ui/Box';
|
||||||
import { Link } from '@/ui/Link';
|
import { Link } from '@/ui/Link';
|
||||||
import { Text } from '@/ui/Text';
|
import { Text } from '@/ui/Text';
|
||||||
|
|||||||
@@ -148,3 +148,7 @@ export function EmptyState({
|
|||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function MinimalEmptyState(props: Omit<EmptyStateProps, 'variant'>) {
|
||||||
|
return <EmptyState {...props} variant="minimal" />;
|
||||||
|
}
|
||||||
|
|||||||
@@ -12,7 +12,8 @@ export interface FilterOption {
|
|||||||
export interface FilterGroupProps {
|
export interface FilterGroupProps {
|
||||||
options: FilterOption[];
|
options: FilterOption[];
|
||||||
activeId: string;
|
activeId: string;
|
||||||
onChange: (id: string) => void;
|
onChange?: (id: string) => void;
|
||||||
|
onSelect?: (id: string) => void;
|
||||||
label?: string;
|
label?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -20,8 +21,11 @@ export const FilterGroup = ({
|
|||||||
options,
|
options,
|
||||||
activeId,
|
activeId,
|
||||||
onChange,
|
onChange,
|
||||||
|
onSelect,
|
||||||
label
|
label
|
||||||
}: FilterGroupProps) => {
|
}: FilterGroupProps) => {
|
||||||
|
const finalOnChange = onChange || onSelect || (() => {});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box display="flex" flexDirection="col" gap={2}>
|
<Box display="flex" flexDirection="col" gap={2}>
|
||||||
{label && (
|
{label && (
|
||||||
@@ -37,7 +41,7 @@ export const FilterGroup = ({
|
|||||||
key={option.id}
|
key={option.id}
|
||||||
variant={isActive ? 'primary' : 'ghost'}
|
variant={isActive ? 'primary' : 'ghost'}
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={() => onChange(option.id)}
|
onClick={() => finalOnChange(option.id)}
|
||||||
fullWidth
|
fullWidth
|
||||||
>
|
>
|
||||||
<Box display="flex" alignItems="center" gap={2}>
|
<Box display="flex" alignItems="center" gap={2}>
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
|
import { Box } from './Box';
|
||||||
import { Button, ButtonProps } from './Button';
|
import { Button, ButtonProps } from './Button';
|
||||||
|
|
||||||
export interface FloatingActionProps extends ButtonProps {
|
export interface FloatingActionProps extends Omit<ButtonProps, 'position'> {
|
||||||
position?: 'bottom-left' | 'bottom-right' | 'top-left' | 'top-right';
|
position?: 'bottom-left' | 'bottom-right' | 'top-left' | 'top-right';
|
||||||
title?: string;
|
title?: string;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,10 +4,13 @@ import { Text } from './Text';
|
|||||||
|
|
||||||
export interface GoalCardProps {
|
export interface GoalCardProps {
|
||||||
title: string;
|
title: string;
|
||||||
current: number;
|
current?: number;
|
||||||
target: number;
|
target?: number;
|
||||||
unit: string;
|
unit?: string;
|
||||||
icon: string;
|
icon: string;
|
||||||
|
goalLabel?: string;
|
||||||
|
currentValue?: number;
|
||||||
|
maxValue?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const GoalCard = ({
|
export const GoalCard = ({
|
||||||
@@ -15,9 +18,15 @@ export const GoalCard = ({
|
|||||||
current,
|
current,
|
||||||
target,
|
target,
|
||||||
unit,
|
unit,
|
||||||
icon
|
icon,
|
||||||
|
goalLabel,
|
||||||
|
currentValue,
|
||||||
|
maxValue
|
||||||
}: GoalCardProps) => {
|
}: GoalCardProps) => {
|
||||||
const percentage = Math.min(Math.max((current / target) * 100, 0), 100);
|
const finalCurrent = currentValue ?? current ?? 0;
|
||||||
|
const finalTarget = maxValue ?? target ?? 100;
|
||||||
|
const finalUnit = goalLabel ?? unit ?? '';
|
||||||
|
const percentage = Math.min(Math.max((finalCurrent / finalTarget) * 100, 0), 100);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card variant="default">
|
<Card variant="default">
|
||||||
@@ -25,7 +34,7 @@ export const GoalCard = ({
|
|||||||
<Text size="2xl">{icon}</Text>
|
<Text size="2xl">{icon}</Text>
|
||||||
<Box>
|
<Box>
|
||||||
<Text weight="bold" variant="high">{title}</Text>
|
<Text weight="bold" variant="high">{title}</Text>
|
||||||
<Text size="sm" variant="low">{unit}</Text>
|
<Text size="sm" variant="low">{finalUnit}</Text>
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
@@ -40,7 +49,7 @@ export const GoalCard = ({
|
|||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<Text size="xs" variant="low">
|
<Text size="xs" variant="low">
|
||||||
{current} / {target} {unit}
|
{finalCurrent} / {finalTarget} {finalUnit}
|
||||||
</Text>
|
</Text>
|
||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -17,6 +17,8 @@ export interface GridProps<T extends ElementType> extends BoxProps<T> {
|
|||||||
children?: ReactNode;
|
children?: ReactNode;
|
||||||
cols?: number | ResponsiveValue<number>;
|
cols?: number | ResponsiveValue<number>;
|
||||||
gap?: number | string | ResponsiveValue<number | string>;
|
gap?: number | string | ResponsiveValue<number | string>;
|
||||||
|
mdCols?: number;
|
||||||
|
lgCols?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Grid = forwardRef(<T extends ElementType = 'div'>(
|
export const Grid = forwardRef(<T extends ElementType = 'div'>(
|
||||||
@@ -25,16 +27,22 @@ export const Grid = forwardRef(<T extends ElementType = 'div'>(
|
|||||||
cols = 1,
|
cols = 1,
|
||||||
gap = 4,
|
gap = 4,
|
||||||
as,
|
as,
|
||||||
|
mdCols,
|
||||||
|
lgCols,
|
||||||
...props
|
...props
|
||||||
}: GridProps<T>,
|
}: GridProps<T>,
|
||||||
ref: ForwardedRef<HTMLElement>
|
ref: ForwardedRef<HTMLElement>
|
||||||
) => {
|
) => {
|
||||||
|
const finalCols = typeof cols === 'object' ? { ...cols } : { base: cols };
|
||||||
|
if (mdCols) finalCols.md = mdCols;
|
||||||
|
if (lgCols) finalCols.lg = lgCols;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box
|
<Box
|
||||||
as={as}
|
as={as}
|
||||||
ref={ref}
|
ref={ref}
|
||||||
display="grid"
|
display="grid"
|
||||||
gridCols={cols}
|
gridCols={finalCols}
|
||||||
gap={gap}
|
gap={gap}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ export interface GridItemProps<T extends ElementType> extends BoxProps<T> {
|
|||||||
children?: ReactNode;
|
children?: ReactNode;
|
||||||
colSpan?: number | ResponsiveValue<number>;
|
colSpan?: number | ResponsiveValue<number>;
|
||||||
rowSpan?: number | ResponsiveValue<number>;
|
rowSpan?: number | ResponsiveValue<number>;
|
||||||
|
mdSpan?: number;
|
||||||
|
lgSpan?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const GridItem = forwardRef(<T extends ElementType = 'div'>(
|
export const GridItem = forwardRef(<T extends ElementType = 'div'>(
|
||||||
@@ -14,15 +16,21 @@ export const GridItem = forwardRef(<T extends ElementType = 'div'>(
|
|||||||
colSpan,
|
colSpan,
|
||||||
rowSpan,
|
rowSpan,
|
||||||
as,
|
as,
|
||||||
|
mdSpan,
|
||||||
|
lgSpan,
|
||||||
...props
|
...props
|
||||||
}: GridItemProps<T>,
|
}: GridItemProps<T>,
|
||||||
ref: ForwardedRef<HTMLElement>
|
ref: ForwardedRef<HTMLElement>
|
||||||
) => {
|
) => {
|
||||||
|
const finalColSpan = typeof colSpan === 'object' ? { ...colSpan } : { base: colSpan };
|
||||||
|
if (mdSpan) finalColSpan.md = mdSpan;
|
||||||
|
if (lgSpan) finalColSpan.lg = lgSpan;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box
|
<Box
|
||||||
as={as}
|
as={as}
|
||||||
ref={ref}
|
ref={ref}
|
||||||
colSpan={colSpan}
|
colSpan={finalColSpan as any}
|
||||||
// rowSpan is not directly supported by Box yet, but we can add it if needed
|
// rowSpan is not directly supported by Box yet, but we can add it if needed
|
||||||
// or use style
|
// or use style
|
||||||
{...props}
|
{...props}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { Box } from './Box';
|
|||||||
|
|
||||||
export interface GroupProps {
|
export interface GroupProps {
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
direction?: 'row' | 'col';
|
direction?: 'row' | 'col' | 'column';
|
||||||
align?: 'start' | 'center' | 'end' | 'stretch' | 'baseline';
|
align?: 'start' | 'center' | 'end' | 'stretch' | 'baseline';
|
||||||
justify?: 'start' | 'center' | 'end' | 'between' | 'around' | 'evenly';
|
justify?: 'start' | 'center' | 'end' | 'between' | 'around' | 'evenly';
|
||||||
gap?: number;
|
gap?: number;
|
||||||
@@ -20,10 +20,12 @@ export const Group = ({
|
|||||||
wrap = false,
|
wrap = false,
|
||||||
fullWidth = false,
|
fullWidth = false,
|
||||||
}: GroupProps) => {
|
}: GroupProps) => {
|
||||||
|
const finalDirection = direction === 'column' ? 'col' : direction;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box
|
<Box
|
||||||
display="flex"
|
display="flex"
|
||||||
flexDirection={direction}
|
flexDirection={finalDirection}
|
||||||
alignItems={align}
|
alignItems={align}
|
||||||
justifyContent={justify}
|
justifyContent={justify}
|
||||||
gap={gap}
|
gap={gap}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { ReactNode, forwardRef } from 'react';
|
import { ReactNode, forwardRef } from 'react';
|
||||||
import { Box, BoxProps, ResponsiveValue } from './Box';
|
import { Box, BoxProps, ResponsiveValue, Spacing } from './Box';
|
||||||
|
|
||||||
export interface HeadingProps extends BoxProps<any> {
|
export interface HeadingProps extends BoxProps<any> {
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
@@ -7,6 +7,11 @@ export interface HeadingProps extends BoxProps<any> {
|
|||||||
weight?: 'normal' | 'medium' | 'semibold' | 'bold';
|
weight?: 'normal' | 'medium' | 'semibold' | 'bold';
|
||||||
align?: 'left' | 'center' | 'right';
|
align?: 'left' | 'center' | 'right';
|
||||||
fontSize?: string | ResponsiveValue<string>;
|
fontSize?: string | ResponsiveValue<string>;
|
||||||
|
icon?: ReactNode;
|
||||||
|
groupHoverColor?: string;
|
||||||
|
uppercase?: boolean;
|
||||||
|
letterSpacing?: string;
|
||||||
|
mb?: Spacing;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Heading = forwardRef<HTMLHeadingElement, HeadingProps>(({
|
export const Heading = forwardRef<HTMLHeadingElement, HeadingProps>(({
|
||||||
@@ -15,6 +20,11 @@ export const Heading = forwardRef<HTMLHeadingElement, HeadingProps>(({
|
|||||||
weight = 'bold',
|
weight = 'bold',
|
||||||
align = 'left',
|
align = 'left',
|
||||||
fontSize,
|
fontSize,
|
||||||
|
icon,
|
||||||
|
groupHoverColor,
|
||||||
|
uppercase,
|
||||||
|
letterSpacing,
|
||||||
|
mb,
|
||||||
...props
|
...props
|
||||||
}, ref) => {
|
}, ref) => {
|
||||||
const Tag = `h${level}` as const;
|
const Tag = `h${level}` as const;
|
||||||
@@ -40,11 +50,23 @@ export const Heading = forwardRef<HTMLHeadingElement, HeadingProps>(({
|
|||||||
weightClasses[weight],
|
weightClasses[weight],
|
||||||
fontSize ? '' : sizeClasses[level],
|
fontSize ? '' : sizeClasses[level],
|
||||||
align === 'center' ? 'text-center' : (align === 'right' ? 'text-right' : 'text-left'),
|
align === 'center' ? 'text-center' : (align === 'right' ? 'text-right' : 'text-left'),
|
||||||
|
uppercase ? 'uppercase' : '',
|
||||||
].join(' ');
|
].join(' ');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box as={Tag} ref={ref} className={classes} fontSize={typeof fontSize === 'string' ? fontSize : undefined} {...props}>
|
<Box
|
||||||
{children}
|
as={Tag}
|
||||||
|
ref={ref}
|
||||||
|
className={classes}
|
||||||
|
fontSize={typeof fontSize === 'string' ? fontSize : undefined}
|
||||||
|
letterSpacing={letterSpacing}
|
||||||
|
mb={mb}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<Box display="flex" alignItems="center" gap={2}>
|
||||||
|
{icon}
|
||||||
|
{children}
|
||||||
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -7,21 +7,31 @@ import { Text } from './Text';
|
|||||||
export interface HorizontalStatCardProps {
|
export interface HorizontalStatCardProps {
|
||||||
label: string;
|
label: string;
|
||||||
value: string | number;
|
value: string | number;
|
||||||
icon: LucideIcon;
|
icon: LucideIcon | React.ReactNode;
|
||||||
intent?: 'primary' | 'success' | 'warning' | 'critical' | 'telemetry';
|
intent?: 'primary' | 'success' | 'warning' | 'critical' | 'telemetry';
|
||||||
|
subValue?: string;
|
||||||
|
iconBgColor?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const HorizontalStatCard = ({
|
export const HorizontalStatCard = ({
|
||||||
label,
|
label,
|
||||||
value,
|
value,
|
||||||
icon,
|
icon,
|
||||||
intent = 'primary'
|
intent = 'primary',
|
||||||
|
subValue,
|
||||||
|
iconBgColor
|
||||||
}: HorizontalStatCardProps) => {
|
}: HorizontalStatCardProps) => {
|
||||||
|
const isLucideIcon = typeof icon === 'function' || (typeof icon === 'object' && icon !== null && 'render' in icon);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card variant="default">
|
<Card variant="default">
|
||||||
<Box display="flex" alignItems="center" gap={4}>
|
<Box display="flex" alignItems="center" gap={4}>
|
||||||
<Box padding={3} rounded="lg" bg="var(--ui-color-bg-surface-muted)">
|
<Box padding={3} rounded="lg" bg={iconBgColor || "var(--ui-color-bg-surface-muted)"}>
|
||||||
<Icon icon={icon} size={5} intent={intent} />
|
{isLucideIcon ? (
|
||||||
|
<Icon icon={icon as LucideIcon} size={5} intent={intent} />
|
||||||
|
) : (
|
||||||
|
icon
|
||||||
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
<Box>
|
<Box>
|
||||||
<Text size="xs" weight="bold" variant="low" uppercase>
|
<Text size="xs" weight="bold" variant="low" uppercase>
|
||||||
@@ -30,6 +40,11 @@ export const HorizontalStatCard = ({
|
|||||||
<Text size="xl" weight="bold" variant="high" block marginTop={0.5}>
|
<Text size="xl" weight="bold" variant="high" block marginTop={0.5}>
|
||||||
{value}
|
{value}
|
||||||
</Text>
|
</Text>
|
||||||
|
{subValue && (
|
||||||
|
<Text size="xs" variant="low" block marginTop={0.5}>
|
||||||
|
{subValue}
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
@@ -5,17 +5,19 @@ export interface HorizontalStatItemProps {
|
|||||||
label: string;
|
label: string;
|
||||||
value: string | number;
|
value: string | number;
|
||||||
intent?: 'primary' | 'success' | 'warning' | 'critical' | 'high' | 'med' | 'low';
|
intent?: 'primary' | 'success' | 'warning' | 'critical' | 'high' | 'med' | 'low';
|
||||||
|
color?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const HorizontalStatItem = ({
|
export const HorizontalStatItem = ({
|
||||||
label,
|
label,
|
||||||
value,
|
value,
|
||||||
intent = 'high'
|
intent = 'high',
|
||||||
|
color
|
||||||
}: HorizontalStatItemProps) => {
|
}: HorizontalStatItemProps) => {
|
||||||
return (
|
return (
|
||||||
<Box display="flex" alignItems="center" justifyContent="between" paddingY={2}>
|
<Box display="flex" alignItems="center" justifyContent="between" paddingY={2}>
|
||||||
<Text size="sm" variant="low">{label}</Text>
|
<Text size="sm" variant="low">{label}</Text>
|
||||||
<Text weight="semibold" variant={intent}>{value}</Text>
|
<Text weight="semibold" variant={color ? undefined : intent} style={color ? { color } : undefined}>{value}</Text>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ export interface ImagePlaceholderProps {
|
|||||||
aspectRatio?: string;
|
aspectRatio?: string;
|
||||||
variant?: 'default' | 'loading' | 'error';
|
variant?: 'default' | 'loading' | 'error';
|
||||||
message?: string;
|
message?: string;
|
||||||
|
rounded?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ImagePlaceholder = ({
|
export const ImagePlaceholder = ({
|
||||||
@@ -18,7 +19,8 @@ export const ImagePlaceholder = ({
|
|||||||
animate = 'pulse',
|
animate = 'pulse',
|
||||||
aspectRatio,
|
aspectRatio,
|
||||||
variant = 'default',
|
variant = 'default',
|
||||||
message
|
message,
|
||||||
|
rounded
|
||||||
}: ImagePlaceholderProps) => {
|
}: ImagePlaceholderProps) => {
|
||||||
const icons = {
|
const icons = {
|
||||||
default: ImageIcon,
|
default: ImageIcon,
|
||||||
@@ -36,7 +38,7 @@ export const ImagePlaceholder = ({
|
|||||||
alignItems="center"
|
alignItems="center"
|
||||||
justifyContent="center"
|
justifyContent="center"
|
||||||
bg="var(--ui-color-bg-surface-muted)"
|
bg="var(--ui-color-bg-surface-muted)"
|
||||||
style={{ borderRadius: 'var(--ui-radius-md)' }}
|
style={{ borderRadius: rounded === 'none' ? '0' : (rounded ? `var(--ui-radius-${rounded})` : 'var(--ui-radius-md)') }}
|
||||||
gap={2}
|
gap={2}
|
||||||
>
|
>
|
||||||
<Icon
|
<Icon
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { HelpCircle, X } from 'lucide-react';
|
|||||||
import React, { ReactNode, useEffect, useRef, useState } from 'react';
|
import React, { ReactNode, useEffect, useRef, useState } from 'react';
|
||||||
import { createPortal } from 'react-dom';
|
import { createPortal } from 'react-dom';
|
||||||
import { Box } from './Box';
|
import { Box } from './Box';
|
||||||
|
import { Heading } from './Heading';
|
||||||
import { Icon } from './Icon';
|
import { Icon } from './Icon';
|
||||||
import { IconButton } from './IconButton';
|
import { IconButton } from './IconButton';
|
||||||
import { Surface } from './Surface';
|
import { Surface } from './Surface';
|
||||||
|
|||||||
@@ -5,21 +5,26 @@ import { Text } from './Text';
|
|||||||
export interface InputProps extends Omit<InputHTMLAttributes<HTMLInputElement>, 'size'> {
|
export interface InputProps extends Omit<InputHTMLAttributes<HTMLInputElement>, 'size'> {
|
||||||
label?: string;
|
label?: string;
|
||||||
error?: string;
|
error?: string;
|
||||||
|
errorMessage?: string;
|
||||||
hint?: string;
|
hint?: string;
|
||||||
fullWidth?: boolean;
|
fullWidth?: boolean;
|
||||||
size?: 'sm' | 'md' | 'lg';
|
size?: 'sm' | 'md' | 'lg';
|
||||||
icon?: React.ReactNode;
|
icon?: React.ReactNode;
|
||||||
|
variant?: 'default' | 'error';
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Input = forwardRef<HTMLInputElement, InputProps>(({
|
export const Input = forwardRef<HTMLInputElement, InputProps>(({
|
||||||
label,
|
label,
|
||||||
error,
|
error,
|
||||||
|
errorMessage,
|
||||||
hint,
|
hint,
|
||||||
fullWidth = false,
|
fullWidth = false,
|
||||||
size = 'md',
|
size = 'md',
|
||||||
icon,
|
icon,
|
||||||
|
variant = 'default',
|
||||||
...props
|
...props
|
||||||
}, ref) => {
|
}, ref) => {
|
||||||
|
const finalError = error || errorMessage;
|
||||||
const sizeClasses = {
|
const sizeClasses = {
|
||||||
sm: 'px-3 py-1.5 text-xs',
|
sm: 'px-3 py-1.5 text-xs',
|
||||||
md: 'px-4 py-2 text-sm',
|
md: 'px-4 py-2 text-sm',
|
||||||
@@ -27,7 +32,7 @@ export const Input = forwardRef<HTMLInputElement, InputProps>(({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const baseClasses = 'bg-[var(--ui-color-bg-surface)] border border-[var(--ui-color-border-default)] text-[var(--ui-color-text-high)] placeholder-[var(--ui-color-text-low)] focus:outline-none focus:border-[var(--ui-color-intent-primary)] transition-colors';
|
const baseClasses = 'bg-[var(--ui-color-bg-surface)] border border-[var(--ui-color-border-default)] text-[var(--ui-color-text-high)] placeholder-[var(--ui-color-text-low)] focus:outline-none focus:border-[var(--ui-color-intent-primary)] transition-colors';
|
||||||
const errorClasses = error ? 'border-[var(--ui-color-intent-critical)]' : '';
|
const errorClasses = (finalError || variant === 'error') ? 'border-[var(--ui-color-intent-critical)]' : '';
|
||||||
const widthClasses = fullWidth ? 'w-full' : '';
|
const widthClasses = fullWidth ? 'w-full' : '';
|
||||||
|
|
||||||
const classes = [
|
const classes = [
|
||||||
@@ -59,14 +64,14 @@ export const Input = forwardRef<HTMLInputElement, InputProps>(({
|
|||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
{error && (
|
{finalError && (
|
||||||
<Box marginTop={1}>
|
<Box marginTop={1}>
|
||||||
<Text size="xs" variant="critical">
|
<Text size="xs" variant="critical">
|
||||||
{error}
|
{finalError}
|
||||||
</Text>
|
</Text>
|
||||||
</Box>
|
</Box>
|
||||||
)}
|
)}
|
||||||
{hint && !error && (
|
{hint && !finalError && (
|
||||||
<Box marginTop={1}>
|
<Box marginTop={1}>
|
||||||
<Text size="xs" variant="low">
|
<Text size="xs" variant="low">
|
||||||
{hint}
|
{hint}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { LucideIcon } from 'lucide-react';
|
import { LucideIcon } from 'lucide-react';
|
||||||
import { ReactNode } from 'react';
|
import { ReactNode } from 'react';
|
||||||
import { Box } from './Box';
|
import { Box } from './Box';
|
||||||
|
import { Button } from './Button';
|
||||||
import { Icon } from './Icon';
|
import { Icon } from './Icon';
|
||||||
import { Surface } from './Surface';
|
import { Surface } from './Surface';
|
||||||
import { Text } from './Text';
|
import { Text } from './Text';
|
||||||
@@ -12,6 +13,10 @@ export interface LeaderboardPreviewShellProps {
|
|||||||
intent?: 'primary' | 'success' | 'warning' | 'critical' | 'telemetry';
|
intent?: 'primary' | 'success' | 'warning' | 'critical' | 'telemetry';
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
footer?: ReactNode;
|
footer?: ReactNode;
|
||||||
|
onViewFull?: () => void;
|
||||||
|
iconColor?: string;
|
||||||
|
iconBgGradient?: string;
|
||||||
|
viewFullLabel?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const LeaderboardPreviewShell = ({
|
export const LeaderboardPreviewShell = ({
|
||||||
@@ -20,15 +25,19 @@ export const LeaderboardPreviewShell = ({
|
|||||||
icon,
|
icon,
|
||||||
intent = 'primary',
|
intent = 'primary',
|
||||||
children,
|
children,
|
||||||
footer
|
footer,
|
||||||
|
onViewFull,
|
||||||
|
iconColor,
|
||||||
|
iconBgGradient,
|
||||||
|
viewFullLabel
|
||||||
}: LeaderboardPreviewShellProps) => {
|
}: LeaderboardPreviewShellProps) => {
|
||||||
return (
|
return (
|
||||||
<Surface variant="default" rounded="xl" style={{ border: '1px solid var(--ui-color-border-default)', overflow: 'hidden' }}>
|
<Surface variant="default" rounded="xl" style={{ border: '1px solid var(--ui-color-border-default)', overflow: 'hidden' }}>
|
||||||
<Box padding={6} borderBottom>
|
<Box padding={6} borderBottom>
|
||||||
<Box display="flex" alignItems="center" justifyContent="between" marginBottom={4}>
|
<Box display="flex" alignItems="center" justifyContent="between" marginBottom={4}>
|
||||||
<Box display="flex" alignItems="center" gap={4}>
|
<Box display="flex" alignItems="center" gap={4}>
|
||||||
<Box padding={2} rounded="lg" bg="var(--ui-color-bg-surface-muted)">
|
<Box padding={2} rounded="lg" bg={iconBgGradient || "var(--ui-color-bg-surface-muted)"} style={iconColor ? { color: iconColor } : undefined}>
|
||||||
<Icon icon={icon} size={5} intent={intent} />
|
<Icon icon={icon} size={5} intent={iconColor ? undefined : intent} />
|
||||||
</Box>
|
</Box>
|
||||||
<Box>
|
<Box>
|
||||||
<Text size="lg" weight="bold" variant="high" block>
|
<Text size="lg" weight="bold" variant="high" block>
|
||||||
@@ -41,6 +50,11 @@ export const LeaderboardPreviewShell = ({
|
|||||||
)}
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
|
{onViewFull && (
|
||||||
|
<Button variant="ghost" size="sm" onClick={onViewFull}>
|
||||||
|
{viewFullLabel || 'View Full'}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
|
|||||||
@@ -4,9 +4,10 @@ import { Surface } from './Surface';
|
|||||||
|
|
||||||
export interface LeaderboardTableShellProps {
|
export interface LeaderboardTableShellProps {
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
|
columns?: any[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export const LeaderboardTableShell = ({ children }: LeaderboardTableShellProps) => {
|
export const LeaderboardTableShell = ({ children, columns }: LeaderboardTableShellProps) => {
|
||||||
return (
|
return (
|
||||||
<Surface variant="default" rounded="xl" style={{ border: '1px solid var(--ui-color-border-default)', overflow: 'hidden' }}>
|
<Surface variant="default" rounded="xl" style={{ border: '1px solid var(--ui-color-border-default)', overflow: 'hidden' }}>
|
||||||
<Box>
|
<Box>
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { ChevronRight } from 'lucide-react';
|
|||||||
import { ReactNode } from 'react';
|
import { ReactNode } from 'react';
|
||||||
import { Box } from './Box';
|
import { Box } from './Box';
|
||||||
import { Card } from './Card';
|
import { Card } from './Card';
|
||||||
|
import { Icon } from './Icon';
|
||||||
import { Image } from './Image';
|
import { Image } from './Image';
|
||||||
import { Text } from './Text';
|
import { Text } from './Text';
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,20 @@ export interface LinkProps extends AnchorHTMLAttributes<HTMLAnchorElement> {
|
|||||||
block?: boolean;
|
block?: boolean;
|
||||||
hoverColor?: string;
|
hoverColor?: string;
|
||||||
transition?: boolean;
|
transition?: boolean;
|
||||||
|
pb?: number;
|
||||||
|
truncate?: boolean;
|
||||||
|
hoverTextColor?: string;
|
||||||
|
display?: string;
|
||||||
|
alignItems?: string;
|
||||||
|
gap?: number;
|
||||||
|
rounded?: string;
|
||||||
|
bg?: string;
|
||||||
|
px?: number;
|
||||||
|
py?: number;
|
||||||
|
border?: boolean;
|
||||||
|
borderColor?: string;
|
||||||
|
shadow?: string;
|
||||||
|
hoverBorderColor?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Link = forwardRef<HTMLAnchorElement, LinkProps>(({
|
export const Link = forwardRef<HTMLAnchorElement, LinkProps>(({
|
||||||
@@ -22,6 +36,20 @@ export const Link = forwardRef<HTMLAnchorElement, LinkProps>(({
|
|||||||
block = false,
|
block = false,
|
||||||
hoverColor,
|
hoverColor,
|
||||||
transition = true,
|
transition = true,
|
||||||
|
pb,
|
||||||
|
truncate,
|
||||||
|
hoverTextColor,
|
||||||
|
display,
|
||||||
|
alignItems,
|
||||||
|
gap,
|
||||||
|
rounded,
|
||||||
|
bg,
|
||||||
|
px,
|
||||||
|
py,
|
||||||
|
border,
|
||||||
|
borderColor,
|
||||||
|
shadow,
|
||||||
|
hoverBorderColor,
|
||||||
...props
|
...props
|
||||||
}, ref) => {
|
}, ref) => {
|
||||||
const variantClasses = {
|
const variantClasses = {
|
||||||
@@ -43,12 +71,24 @@ export const Link = forwardRef<HTMLAnchorElement, LinkProps>(({
|
|||||||
block ? 'block' : 'inline',
|
block ? 'block' : 'inline',
|
||||||
variantClasses[variant],
|
variantClasses[variant],
|
||||||
underlineClasses[underline],
|
underlineClasses[underline],
|
||||||
|
truncate ? 'truncate' : '',
|
||||||
].join(' ');
|
].join(' ');
|
||||||
|
|
||||||
const style: React.CSSProperties = {
|
const style: React.CSSProperties = {
|
||||||
...(size ? { fontSize: size } : {}),
|
...(size ? { fontSize: size } : {}),
|
||||||
...(weight ? { fontWeight: weight } : {}),
|
...(weight ? { fontWeight: weight } : {}),
|
||||||
...(letterSpacing ? { letterSpacing } : {}),
|
...(letterSpacing ? { letterSpacing } : {}),
|
||||||
|
...(pb ? { paddingBottom: `${pb * 0.25}rem` } : {}),
|
||||||
|
...(display ? { display } : {}),
|
||||||
|
...(alignItems ? { alignItems } : {}),
|
||||||
|
...(gap ? { gap: `${gap * 0.25}rem` } : {}),
|
||||||
|
...(rounded ? { borderRadius: rounded === 'full' ? '9999px' : `var(--ui-radius-${rounded})` } : {}),
|
||||||
|
...(bg ? { backgroundColor: bg.startsWith('bg-') ? undefined : bg } : {}),
|
||||||
|
...(px ? { paddingLeft: `${px * 0.25}rem`, paddingRight: `${px * 0.25}rem` } : {}),
|
||||||
|
...(py ? { paddingTop: `${py * 0.25}rem`, paddingBottom: `${py * 0.25}rem` } : {}),
|
||||||
|
...(border ? { border: '1px solid var(--ui-color-border-default)' } : {}),
|
||||||
|
...(borderColor ? { borderColor: borderColor.startsWith('border-') ? undefined : borderColor } : {}),
|
||||||
|
...(shadow ? { boxShadow: shadow.startsWith('shadow-') ? undefined : shadow } : {}),
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -5,16 +5,18 @@ export interface MiniStatProps {
|
|||||||
label: string;
|
label: string;
|
||||||
value: string | number;
|
value: string | number;
|
||||||
intent?: 'primary' | 'success' | 'warning' | 'critical' | 'high' | 'med' | 'low';
|
intent?: 'primary' | 'success' | 'warning' | 'critical' | 'high' | 'med' | 'low';
|
||||||
|
color?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const MiniStat = ({
|
export const MiniStat = ({
|
||||||
label,
|
label,
|
||||||
value,
|
value,
|
||||||
intent = 'high'
|
intent = 'high',
|
||||||
|
color
|
||||||
}: MiniStatProps) => {
|
}: MiniStatProps) => {
|
||||||
return (
|
return (
|
||||||
<Box textAlign="center" padding={2} rounded="lg" bg="var(--ui-color-bg-surface-muted)">
|
<Box textAlign="center" padding={2} rounded="lg" bg="var(--ui-color-bg-surface-muted)">
|
||||||
<Text size="lg" weight="bold" variant={intent} block>{value}</Text>
|
<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="xs" variant="low" block style={{ fontSize: '10px' }}>{label}</Text>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { LucideIcon } from 'lucide-react';
|
||||||
import { Box } from './Box';
|
import { Box } from './Box';
|
||||||
import { Icon } from './Icon';
|
import { Icon } from './Icon';
|
||||||
import { Text } from './Text';
|
import { Text } from './Text';
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
|
import { LucideIcon } from 'lucide-react';
|
||||||
import { ReactNode } from 'react';
|
import { ReactNode } from 'react';
|
||||||
import { Box } from './Box';
|
import { Box } from './Box';
|
||||||
|
import { Button } from './Button';
|
||||||
import { Heading } from './Heading';
|
import { Heading } from './Heading';
|
||||||
|
import { Icon } from './Icon';
|
||||||
import { Surface } from './Surface';
|
import { Surface } from './Surface';
|
||||||
import { Text } from './Text';
|
import { Text } from './Text';
|
||||||
|
|
||||||
@@ -9,13 +12,22 @@ export interface PageHeroProps {
|
|||||||
description?: string;
|
description?: string;
|
||||||
children?: ReactNode;
|
children?: ReactNode;
|
||||||
image?: ReactNode;
|
image?: ReactNode;
|
||||||
|
icon?: LucideIcon;
|
||||||
|
actions?: Array<{
|
||||||
|
label: string;
|
||||||
|
onClick: () => void;
|
||||||
|
variant?: 'primary' | 'secondary';
|
||||||
|
icon?: LucideIcon;
|
||||||
|
}>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const PageHero = ({
|
export const PageHero = ({
|
||||||
title,
|
title,
|
||||||
description,
|
description,
|
||||||
children,
|
children,
|
||||||
image
|
image,
|
||||||
|
icon,
|
||||||
|
actions
|
||||||
}: PageHeroProps) => {
|
}: PageHeroProps) => {
|
||||||
return (
|
return (
|
||||||
<Surface
|
<Surface
|
||||||
@@ -26,12 +38,33 @@ export const PageHero = ({
|
|||||||
>
|
>
|
||||||
<Box display="flex" flexDirection={{ base: 'col', lg: 'row' }} alignItems="center" gap={8}>
|
<Box display="flex" flexDirection={{ base: 'col', lg: 'row' }} alignItems="center" gap={8}>
|
||||||
<Box flex={1}>
|
<Box flex={1}>
|
||||||
<Heading level={1} marginBottom={4}>{title}</Heading>
|
<Box display="flex" alignItems="center" gap={4} marginBottom={4}>
|
||||||
|
{icon && (
|
||||||
|
<Box padding={3} rounded="lg" bg="var(--ui-color-bg-surface-muted)">
|
||||||
|
<Icon icon={icon} size={8} intent="primary" />
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
<Heading level={1}>{title}</Heading>
|
||||||
|
</Box>
|
||||||
{description && (
|
{description && (
|
||||||
<Text size="lg" variant="low" marginBottom={6} block>
|
<Text size="lg" variant="low" marginBottom={6} block>
|
||||||
{description}
|
{description}
|
||||||
</Text>
|
</Text>
|
||||||
)}
|
)}
|
||||||
|
{actions && (
|
||||||
|
<Box display="flex" gap={4} marginBottom={children ? 6 : 0}>
|
||||||
|
{actions.map((action) => (
|
||||||
|
<Button
|
||||||
|
key={action.label}
|
||||||
|
variant={action.variant || 'primary'}
|
||||||
|
onClick={action.onClick}
|
||||||
|
icon={action.icon ? <Icon icon={action.icon} size={4} /> : undefined}
|
||||||
|
>
|
||||||
|
{action.label}
|
||||||
|
</Button>
|
||||||
|
))}
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
{children}
|
{children}
|
||||||
</Box>
|
</Box>
|
||||||
{image && (
|
{image && (
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user