website refactor
This commit is contained in:
@@ -24,37 +24,35 @@ const CATEGORIES: Category[] = [
|
|||||||
id: 'all',
|
id: 'all',
|
||||||
label: 'All',
|
label: 'All',
|
||||||
icon: Globe,
|
icon: Globe,
|
||||||
description: 'Browse all available leagues',
|
description: 'All available competition infrastructure.',
|
||||||
filter: () => true,
|
filter: () => true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'popular',
|
id: 'popular',
|
||||||
label: 'Popular',
|
label: 'Popular',
|
||||||
icon: Flame,
|
icon: Flame,
|
||||||
description: 'Most active leagues right now',
|
description: 'High utilization infrastructure.',
|
||||||
filter: (league) => {
|
filter: (league) => {
|
||||||
const fillRate = (league.usedDriverSlots ?? 0) / (league.maxDrivers ?? 1);
|
const fillRate = (league.usedDriverSlots ?? 0) / (league.maxDrivers ?? 1);
|
||||||
return fillRate > 0.7;
|
return fillRate > 0.7;
|
||||||
},
|
},
|
||||||
color: 'text-orange-400',
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'new',
|
id: 'new',
|
||||||
label: 'New',
|
label: 'New',
|
||||||
icon: Sparkles,
|
icon: Sparkles,
|
||||||
description: 'Fresh leagues looking for members',
|
description: 'Recently deployed infrastructure.',
|
||||||
filter: (league) => {
|
filter: (league) => {
|
||||||
const oneWeekAgo = new Date();
|
const oneWeekAgo = new Date();
|
||||||
oneWeekAgo.setDate(oneWeekAgo.getDate() - 7);
|
oneWeekAgo.setDate(oneWeekAgo.getDate() - 7);
|
||||||
return new Date(league.createdAt) > oneWeekAgo;
|
return new Date(league.createdAt) > oneWeekAgo;
|
||||||
},
|
},
|
||||||
color: 'text-green-500',
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'openSlots',
|
id: 'openSlots',
|
||||||
label: 'Open Slots',
|
label: 'Open',
|
||||||
icon: Target,
|
icon: Target,
|
||||||
description: 'Leagues with available spots',
|
description: 'Infrastructure with available capacity.',
|
||||||
filter: (league) => {
|
filter: (league) => {
|
||||||
if (league.maxTeams && league.maxTeams > 0) {
|
if (league.maxTeams && league.maxTeams > 0) {
|
||||||
const usedTeams = league.usedTeamSlots ?? 0;
|
const usedTeams = league.usedTeamSlots ?? 0;
|
||||||
@@ -64,41 +62,40 @@ const CATEGORIES: Category[] = [
|
|||||||
const max = league.maxDrivers ?? 0;
|
const max = league.maxDrivers ?? 0;
|
||||||
return max > 0 && used < max;
|
return max > 0 && used < max;
|
||||||
},
|
},
|
||||||
color: 'text-cyan-400',
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'driver',
|
id: 'driver',
|
||||||
label: 'Driver',
|
label: 'Driver',
|
||||||
icon: Trophy,
|
icon: Trophy,
|
||||||
description: 'Compete as an individual',
|
description: 'Individual competition format.',
|
||||||
filter: (league) => league.scoring?.primaryChampionshipType === 'driver',
|
filter: (league) => league.scoring?.primaryChampionshipType === 'driver',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'team',
|
id: 'team',
|
||||||
label: 'Team',
|
label: 'Team',
|
||||||
icon: Users,
|
icon: Users,
|
||||||
description: 'Race together as a team',
|
description: 'Team-based competition format.',
|
||||||
filter: (league) => league.scoring?.primaryChampionshipType === 'team',
|
filter: (league) => league.scoring?.primaryChampionshipType === 'team',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'nations',
|
id: 'nations',
|
||||||
label: 'Nations',
|
label: 'Nations',
|
||||||
icon: Flag,
|
icon: Flag,
|
||||||
description: 'Represent your country',
|
description: 'National representation format.',
|
||||||
filter: (league) => league.scoring?.primaryChampionshipType === 'nations',
|
filter: (league) => league.scoring?.primaryChampionshipType === 'nations',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'trophy',
|
id: 'trophy',
|
||||||
label: 'Trophy',
|
label: 'Trophy',
|
||||||
icon: Award,
|
icon: Award,
|
||||||
description: 'Special championship events',
|
description: 'Special event infrastructure.',
|
||||||
filter: (league) => league.scoring?.primaryChampionshipType === 'trophy',
|
filter: (league) => league.scoring?.primaryChampionshipType === 'trophy',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'endurance',
|
id: 'endurance',
|
||||||
label: 'Endurance',
|
label: 'Endurance',
|
||||||
icon: Timer,
|
icon: Timer,
|
||||||
description: 'Long-distance racing',
|
description: 'Long-duration competition.',
|
||||||
filter: (league) =>
|
filter: (league) =>
|
||||||
league.scoring?.scoringPresetId?.includes('endurance') ??
|
league.scoring?.scoringPresetId?.includes('endurance') ??
|
||||||
league.timingSummary?.includes('h Race') ??
|
league.timingSummary?.includes('h Race') ??
|
||||||
@@ -108,7 +105,7 @@ const CATEGORIES: Category[] = [
|
|||||||
id: 'sprint',
|
id: 'sprint',
|
||||||
label: 'Sprint',
|
label: 'Sprint',
|
||||||
icon: Clock,
|
icon: Clock,
|
||||||
description: 'Quick, intense races',
|
description: 'Short-duration competition.',
|
||||||
filter: (league) =>
|
filter: (league) =>
|
||||||
(league.scoring?.scoringPresetId?.includes('sprint') ?? false) &&
|
(league.scoring?.scoringPresetId?.includes('sprint') ?? false) &&
|
||||||
!(league.scoring?.scoringPresetId?.includes('endurance') ?? false),
|
!(league.scoring?.scoringPresetId?.includes('endurance') ?? false),
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import { notFound } from 'next/navigation';
|
import { notFound } from 'next/navigation';
|
||||||
import { LeaguesPageClient } from '@/client-wrapper/LeaguesPageClient';
|
import { LeaguesPageClient } from './LeaguesPageClient';
|
||||||
import { LeaguesPageQuery } from '@/lib/page-queries/LeaguesPageQuery';
|
import { LeaguesPageQuery } from '@/lib/page-queries/LeaguesPageQuery';
|
||||||
|
|
||||||
export default async function Page() {
|
export default async function Page() {
|
||||||
|
|||||||
@@ -8,8 +8,9 @@ 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 { Surface } from '@/ui/Surface';
|
import { Surface } from '@/ui/Surface';
|
||||||
|
import { Stack } from '@/ui/Stack';
|
||||||
import { LeagueCard as UILeagueCard, LeagueCardStats, LeagueCardFooter } from '@/ui/LeagueCard';
|
import { LeagueCard as UILeagueCard, LeagueCardStats, LeagueCardFooter } from '@/ui/LeagueCard';
|
||||||
import { Calendar as LucideCalendar } from 'lucide-react';
|
import { Calendar, Users, Activity } from 'lucide-react';
|
||||||
import React, { ReactNode } from 'react';
|
import React, { ReactNode } from 'react';
|
||||||
|
|
||||||
interface LeagueCardProps {
|
interface LeagueCardProps {
|
||||||
@@ -54,80 +55,81 @@ export function LeagueCard({
|
|||||||
coverUrl={coverUrl}
|
coverUrl={coverUrl}
|
||||||
logo={
|
logo={
|
||||||
<Surface
|
<Surface
|
||||||
width="3rem"
|
width="3.5rem"
|
||||||
height="3rem"
|
height="3.5rem"
|
||||||
rounded="md"
|
rounded="lg"
|
||||||
overflow="hidden"
|
overflow="hidden"
|
||||||
border
|
border
|
||||||
variant="dark"
|
variant="precision"
|
||||||
>
|
>
|
||||||
{logoUrl ? (
|
{logoUrl ? (
|
||||||
<Image
|
<Image
|
||||||
src={logoUrl}
|
src={logoUrl}
|
||||||
alt={`${name} logo`}
|
alt={`${name} logo`}
|
||||||
width={48}
|
width={56}
|
||||||
height={48}
|
height={56}
|
||||||
objectFit="cover"
|
objectFit="cover"
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<PlaceholderImage size={48} />
|
<PlaceholderImage size={56} />
|
||||||
)}
|
)}
|
||||||
</Surface>
|
</Surface>
|
||||||
}
|
}
|
||||||
badges={
|
badges={
|
||||||
<React.Fragment>
|
<Group gap={2}>
|
||||||
{badges}
|
{badges}
|
||||||
{championshipBadge}
|
{championshipBadge}
|
||||||
</React.Fragment>
|
</Group>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<Box marginBottom={1}>
|
<Stack gap={4} fullHeight>
|
||||||
<Group gap={2}>
|
<Stack gap={1}>
|
||||||
<Box width="0.25rem" height="1rem" bg="var(--ui-color-intent-primary)" />
|
<Group gap={2} align="center">
|
||||||
<Heading level={3} weight="bold" truncate>{name}</Heading>
|
<Box width="2px" height="1rem" bg="var(--ui-color-intent-telemetry)" />
|
||||||
</Group>
|
<Heading level={3} weight="bold" truncate>{name}</Heading>
|
||||||
</Box>
|
|
||||||
|
|
||||||
<Text size="xs" variant="low" lineClamp={2} style={{ height: '2.5rem' }} block leading="relaxed" marginBottom={4}>
|
|
||||||
{description || 'No description available'}
|
|
||||||
</Text>
|
|
||||||
|
|
||||||
<LeagueCardStats
|
|
||||||
label={slotLabel}
|
|
||||||
value={`${usedSlots}/${maxSlots || '∞'}`}
|
|
||||||
percentage={fillPercentage}
|
|
||||||
intent={fillPercentage >= 90 ? 'warning' : fillPercentage >= 70 ? 'primary' : 'success'}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{hasOpenSlots && (
|
|
||||||
<Box marginBottom={4}>
|
|
||||||
<Surface
|
|
||||||
display="flex"
|
|
||||||
alignItems="center"
|
|
||||||
gap={1.5}
|
|
||||||
paddingX={2}
|
|
||||||
paddingY={1}
|
|
||||||
bg="rgba(25, 140, 255, 0.05)"
|
|
||||||
border="1px solid rgba(25, 140, 255, 0.2)"
|
|
||||||
rounded="sm"
|
|
||||||
width="fit-content"
|
|
||||||
>
|
|
||||||
<Box width="0.375rem" height="0.375rem" rounded="full" bg="var(--ui-color-intent-primary)" />
|
|
||||||
<Text size="xs" variant="primary" weight="bold" uppercase>{openSlotsCount} OPEN</Text>
|
|
||||||
</Surface>
|
|
||||||
</Box>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<LeagueCardFooter>
|
|
||||||
{timingSummary && (
|
|
||||||
<Group gap={2}>
|
|
||||||
<Icon icon={LucideCalendar} size={3} intent="low" />
|
|
||||||
<Text size="xs" variant="low" font="mono">
|
|
||||||
{timingSummary.split('•')[1]?.trim() || timingSummary}
|
|
||||||
</Text>
|
|
||||||
</Group>
|
</Group>
|
||||||
)}
|
<Text size="xs" variant="low" lineClamp={2} block leading="relaxed">
|
||||||
</LeagueCardFooter>
|
{description || 'No infrastructure description provided.'}
|
||||||
|
</Text>
|
||||||
|
</Stack>
|
||||||
|
|
||||||
|
<Box>
|
||||||
|
<LeagueCardStats
|
||||||
|
label={slotLabel}
|
||||||
|
value={`${usedSlots} / ${maxSlots || '∞'}`}
|
||||||
|
percentage={fillPercentage}
|
||||||
|
intent={fillPercentage >= 90 ? 'warning' : fillPercentage >= 70 ? 'primary' : 'success'}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{hasOpenSlots && (
|
||||||
|
<Group gap={2} align="center">
|
||||||
|
<Icon icon={Activity} size={3} intent="success" />
|
||||||
|
<Text size="xs" variant="success" weight="bold" uppercase font="mono">
|
||||||
|
{openSlotsCount} slots available
|
||||||
|
</Text>
|
||||||
|
</Group>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<LeagueCardFooter>
|
||||||
|
<Stack gap={2}>
|
||||||
|
{timingSummary && (
|
||||||
|
<Group gap={2}>
|
||||||
|
<Icon icon={Calendar} size={3} intent="low" />
|
||||||
|
<Text size="xs" variant="low" font="mono">
|
||||||
|
{timingSummary.split('•')[1]?.trim() || timingSummary}
|
||||||
|
</Text>
|
||||||
|
</Group>
|
||||||
|
)}
|
||||||
|
<Group gap={2}>
|
||||||
|
<Icon icon={Users} size={3} intent="low" />
|
||||||
|
<Text size="xs" variant="low" font="mono">
|
||||||
|
{usedSlots} active participants
|
||||||
|
</Text>
|
||||||
|
</Group>
|
||||||
|
</Stack>
|
||||||
|
</LeagueCardFooter>
|
||||||
|
</Stack>
|
||||||
</UILeagueCard>
|
</UILeagueCard>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ import {
|
|||||||
Flag,
|
Flag,
|
||||||
Award,
|
Award,
|
||||||
Sparkles,
|
Sparkles,
|
||||||
|
Gamepad2,
|
||||||
|
Layers,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import type { LeagueSummaryViewModel } from '@/lib/view-models/LeagueSummaryViewModel';
|
import type { LeagueSummaryViewModel } from '@/lib/view-models/LeagueSummaryViewModel';
|
||||||
import { getMediaUrl } from '@/lib/utilities/media';
|
import { getMediaUrl } from '@/lib/utilities/media';
|
||||||
@@ -152,24 +154,24 @@ export function LeagueCard({ league, onClick }: LeagueCardProps) {
|
|||||||
badges={
|
badges={
|
||||||
<>
|
<>
|
||||||
{isNew && (
|
{isNew && (
|
||||||
<Badge variant="success" icon={Sparkles}>
|
<Badge variant="success" icon={Sparkles} size="sm">
|
||||||
NEW
|
NEW
|
||||||
</Badge>
|
</Badge>
|
||||||
)}
|
)}
|
||||||
{league.scoring?.gameName && (
|
{league.scoring?.gameName && (
|
||||||
<Badge variant={gameVariant}>
|
<Badge variant={gameVariant} icon={Gamepad2} size="sm">
|
||||||
{league.scoring.gameName}
|
{league.scoring.gameName}
|
||||||
</Badge>
|
</Badge>
|
||||||
)}
|
)}
|
||||||
{league.category && (
|
{league.category && (
|
||||||
<Badge variant={categoryVariant}>
|
<Badge variant={categoryVariant} icon={Layers} size="sm">
|
||||||
{categoryLabel}
|
{categoryLabel}
|
||||||
</Badge>
|
</Badge>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
championshipBadge={
|
championshipBadge={
|
||||||
<Badge variant="default" icon={ChampionshipIcon}>
|
<Badge variant="outline" icon={ChampionshipIcon} size="sm">
|
||||||
{championshipLabel}
|
{championshipLabel}
|
||||||
</Badge>
|
</Badge>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
import { LeagueCard } from '@/components/leagues/LeagueCardWrapper';
|
import { LeagueCard } from '@/components/leagues/LeagueCardWrapper';
|
||||||
import type { LeaguesViewData } from '@/lib/view-data/LeaguesViewData';
|
import type { LeaguesViewData } from '@/lib/view-data/LeaguesViewData';
|
||||||
import { LeagueSummaryViewModel } from '@/lib/view-models/LeagueSummaryViewModel';
|
import { LeagueSummaryViewModel } from '@/lib/view-models/LeagueSummaryViewModel';
|
||||||
import { Heading } from '@/ui/Heading';
|
import { PageHeader } from '@/ui/PageHeader';
|
||||||
import { Input } from '@/ui/Input';
|
import { Input } from '@/ui/Input';
|
||||||
import { Button } from '@/ui/Button';
|
import { Button } from '@/ui/Button';
|
||||||
import { Group } from '@/ui/Group';
|
import { Group } from '@/ui/Group';
|
||||||
@@ -12,11 +12,16 @@ import { Container } from '@/ui/Container';
|
|||||||
import { Text } from '@/ui/Text';
|
import { Text } from '@/ui/Text';
|
||||||
import { Icon } from '@/ui/Icon';
|
import { Icon } from '@/ui/Icon';
|
||||||
import { Section } from '@/ui/Section';
|
import { Section } from '@/ui/Section';
|
||||||
import { StatusDot } from '@/ui/StatusDot';
|
import { ControlBar } from '@/ui/ControlBar';
|
||||||
|
import { SegmentedControl } from '@/ui/SegmentedControl';
|
||||||
|
import { MetricCard } from '@/ui/MetricCard';
|
||||||
|
import { Stack } from '@/ui/Stack';
|
||||||
|
import { Box } from '@/ui/Box';
|
||||||
import {
|
import {
|
||||||
Plus,
|
Plus,
|
||||||
Search,
|
Search,
|
||||||
Trophy,
|
Trophy,
|
||||||
|
Filter,
|
||||||
type LucideIcon,
|
type LucideIcon,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
@@ -68,29 +73,14 @@ export function LeaguesTemplate({
|
|||||||
onClearFilters,
|
onClearFilters,
|
||||||
}: LeaguesTemplateProps) {
|
}: LeaguesTemplateProps) {
|
||||||
return (
|
return (
|
||||||
<Container size="xl" py={12}>
|
<Container size="xl" py={8}>
|
||||||
<Group direction="column" gap={16} fullWidth>
|
<Stack gap={8}>
|
||||||
{/* Hero */}
|
{/* Header Section */}
|
||||||
<Group direction={{ base: 'column', md: 'row' } as any} align={{ base: 'start', md: 'end' } as any} justify="between" gap={8} fullWidth>
|
<PageHeader
|
||||||
<Group direction="column" gap={4}>
|
icon={Trophy}
|
||||||
<Group direction="row" align="center" gap={3}>
|
title="Leagues"
|
||||||
<Icon icon={Trophy} size={6} intent="primary" />
|
description="Infrastructure for competitive sim racing."
|
||||||
<Text size="xs" weight="bold" uppercase letterSpacing="widest" color="text-primary-accent">Competition Hub</Text>
|
action={
|
||||||
</Group>
|
|
||||||
<Heading level={1} size="5xl" weight="bold">
|
|
||||||
Find Your <Text as="span" color="text-primary-accent">Grid</Text>
|
|
||||||
</Heading>
|
|
||||||
<Text variant="low" maxWidth="md">
|
|
||||||
From casual sprints to epic endurance battles — discover the perfect league for your racing style.
|
|
||||||
</Text>
|
|
||||||
</Group>
|
|
||||||
|
|
||||||
<Group direction="row" align="center" gap={4}>
|
|
||||||
<Group direction="column" align="end">
|
|
||||||
<Text size="2xl" weight="bold" font="mono">{viewData.leagues.length}</Text>
|
|
||||||
<Text weight="bold" variant="low" uppercase letterSpacing="widest" size="xs">Active Leagues</Text>
|
|
||||||
</Group>
|
|
||||||
<StatusDot intent="telemetry" size="lg" />
|
|
||||||
<Button
|
<Button
|
||||||
onClick={onCreateLeague}
|
onClick={onCreateLeague}
|
||||||
variant="primary"
|
variant="primary"
|
||||||
@@ -99,40 +89,62 @@ export function LeaguesTemplate({
|
|||||||
>
|
>
|
||||||
Create League
|
Create League
|
||||||
</Button>
|
</Button>
|
||||||
</Group>
|
}
|
||||||
</Group>
|
/>
|
||||||
|
|
||||||
{/* Search & Filters */}
|
{/* Stats Overview */}
|
||||||
<Group direction="column" gap={8} fullWidth>
|
<Grid cols={{ base: 1, md: 3 }} gap={4}>
|
||||||
<Input
|
<MetricCard
|
||||||
type="text"
|
label="Active Leagues"
|
||||||
placeholder="Search leagues by name, description, or game..."
|
value={viewData.leagues.length}
|
||||||
value={searchQuery}
|
icon={Trophy}
|
||||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => onSearchChange(e.target.value)}
|
intent="telemetry"
|
||||||
icon={<Search size={20} />}
|
|
||||||
/>
|
/>
|
||||||
|
<MetricCard
|
||||||
|
label="Total Drivers"
|
||||||
|
value={viewData.leagues.reduce((acc, l) => acc + (l.usedDriverSlots ?? 0), 0)}
|
||||||
|
icon={Trophy}
|
||||||
|
intent="primary"
|
||||||
|
/>
|
||||||
|
<MetricCard
|
||||||
|
label="Open Slots"
|
||||||
|
value={viewData.leagues.reduce((acc, l) => acc + Math.max(0, (l.maxDrivers ?? 0) - (l.usedDriverSlots ?? 0)), 0)}
|
||||||
|
icon={Trophy}
|
||||||
|
intent="success"
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
<Group direction="row" wrap gap={2} fullWidth>
|
{/* Control Bar */}
|
||||||
{categories.map((category) => {
|
<ControlBar
|
||||||
const isActive = activeCategory === category.id;
|
leftContent={
|
||||||
const CategoryIcon = category.icon;
|
<Group gap={4} align="center">
|
||||||
return (
|
<Icon icon={Filter} size={4} intent="low" />
|
||||||
<Button
|
<SegmentedControl
|
||||||
key={category.id}
|
options={categories.map(c => ({
|
||||||
onClick={() => onCategoryChange(category.id)}
|
id: c.id,
|
||||||
variant={isActive ? 'primary' : 'secondary'}
|
label: c.label,
|
||||||
size="sm"
|
icon: <Icon icon={c.icon} size={3} />
|
||||||
icon={<CategoryIcon size={14} />}
|
}))}
|
||||||
>
|
activeId={activeCategory}
|
||||||
{category.label}
|
onChange={(id) => onCategoryChange(id as CategoryId)}
|
||||||
</Button>
|
/>
|
||||||
);
|
</Group>
|
||||||
})}
|
}
|
||||||
</Group>
|
>
|
||||||
</Group>
|
<Box width="300px">
|
||||||
|
<Input
|
||||||
|
type="text"
|
||||||
|
placeholder="Search infrastructure..."
|
||||||
|
value={searchQuery}
|
||||||
|
onChange={(e: React.ChangeEvent<HTMLInputElement>) => onSearchChange(e.target.value)}
|
||||||
|
icon={<Search size={16} />}
|
||||||
|
size="sm"
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
</ControlBar>
|
||||||
|
|
||||||
{/* Grid */}
|
{/* Results */}
|
||||||
<Group direction="column" fullWidth>
|
<Stack gap={6}>
|
||||||
{filteredLeagues.length > 0 ? (
|
{filteredLeagues.length > 0 ? (
|
||||||
<Grid cols={{ base: 1, md: 2, lg: 3 }} gap={6}>
|
<Grid cols={{ base: 1, md: 2, lg: 3 }} gap={6}>
|
||||||
{filteredLeagues.map((league) => (
|
{filteredLeagues.map((league) => (
|
||||||
@@ -145,22 +157,23 @@ export function LeaguesTemplate({
|
|||||||
</Grid>
|
</Grid>
|
||||||
) : (
|
) : (
|
||||||
<Section variant="dark" padding="lg">
|
<Section variant="dark" padding="lg">
|
||||||
<Group direction="column" align="center" justify="center" fullWidth>
|
<Stack align="center" justify="center" gap={4}>
|
||||||
<Icon icon={Search} size={12} intent="low" />
|
<Icon icon={Search} size={12} intent="low" />
|
||||||
<Heading level={3} weight="bold">No Leagues Found</Heading>
|
<Stack align="center" gap={1}>
|
||||||
<Text variant="low" size="sm">Try adjusting your search or filters</Text>
|
<Text size="lg" weight="bold">No results found</Text>
|
||||||
|
<Text variant="low" size="sm">Adjust filters to find matching infrastructure.</Text>
|
||||||
|
</Stack>
|
||||||
<Button
|
<Button
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
onClick={onClearFilters}
|
onClick={onClearFilters}
|
||||||
style={{ marginTop: '1.5rem' }}
|
|
||||||
>
|
>
|
||||||
Clear All Filters
|
Reset Filters
|
||||||
</Button>
|
</Button>
|
||||||
</Group>
|
</Stack>
|
||||||
</Section>
|
</Section>
|
||||||
)}
|
)}
|
||||||
</Group>
|
</Stack>
|
||||||
</Group>
|
</Stack>
|
||||||
</Container>
|
</Container>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ import { Card } from './Card';
|
|||||||
import { Icon } from './Icon';
|
import { Icon } from './Icon';
|
||||||
import { Image } from './Image';
|
import { Image } from './Image';
|
||||||
import { Text } from './Text';
|
import { Text } from './Text';
|
||||||
|
import { Stack } from './Stack';
|
||||||
|
import { Group } from './Group';
|
||||||
|
|
||||||
export interface LeagueCardProps {
|
export interface LeagueCardProps {
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
@@ -17,23 +19,37 @@ export interface LeagueCardProps {
|
|||||||
export const LeagueCard = ({ children, onClick, coverUrl, logo, badges }: LeagueCardProps) => {
|
export const LeagueCard = ({ children, onClick, coverUrl, logo, badges }: LeagueCardProps) => {
|
||||||
return (
|
return (
|
||||||
<Card
|
<Card
|
||||||
variant="dark"
|
variant="precision"
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
style={{ position: 'relative', cursor: onClick ? 'pointer' : 'default', overflow: 'hidden', height: '100%' }}
|
style={{ height: '100%', display: 'flex', flexDirection: 'column' }}
|
||||||
|
padding="none"
|
||||||
>
|
>
|
||||||
<Box height="8rem" position="relative" style={{ overflow: 'hidden' }}>
|
<Box height="10rem" position="relative" overflow="hidden">
|
||||||
<Image src={coverUrl} alt="Cover" fullWidth fullHeight objectFit="cover" style={{ opacity: 0.6 }} />
|
<Image
|
||||||
<Box position="absolute" inset={0} style={{ background: 'linear-gradient(to top, var(--ui-color-bg-base), transparent)' }} />
|
src={coverUrl}
|
||||||
<Box position="absolute" top={3} left={3} display="flex" gap={2}>
|
alt="Cover"
|
||||||
{badges}
|
fullWidth
|
||||||
|
fullHeight
|
||||||
|
objectFit="cover"
|
||||||
|
style={{ opacity: 0.4, filter: 'grayscale(0.2)' }}
|
||||||
|
/>
|
||||||
|
<Box
|
||||||
|
position="absolute"
|
||||||
|
inset={0}
|
||||||
|
style={{ background: 'linear-gradient(to top, var(--ui-color-bg-base), transparent)' }}
|
||||||
|
/>
|
||||||
|
<Box position="absolute" top={4} left={4}>
|
||||||
|
<Group gap={2}>
|
||||||
|
{badges}
|
||||||
|
</Group>
|
||||||
</Box>
|
</Box>
|
||||||
{logo && (
|
{logo && (
|
||||||
<Box position="absolute" left={4} bottom={-6} zIndex={10}>
|
<Box position="absolute" left={6} bottom={-4} zIndex={10}>
|
||||||
{logo}
|
{logo}
|
||||||
</Box>
|
</Box>
|
||||||
)}
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
<Box padding={4} paddingTop={8} display="flex" flexDirection="col" fullHeight>
|
<Box padding={6} paddingTop={10} display="flex" flexDirection="col" flex={1}>
|
||||||
{children}
|
{children}
|
||||||
</Box>
|
</Box>
|
||||||
</Card>
|
</Card>
|
||||||
@@ -44,7 +60,7 @@ export interface LeagueCardStatsProps {
|
|||||||
label: string;
|
label: string;
|
||||||
value: string;
|
value: string;
|
||||||
percentage: number;
|
percentage: number;
|
||||||
intent?: 'primary' | 'success' | 'warning';
|
intent?: 'primary' | 'success' | 'warning' | 'telemetry';
|
||||||
}
|
}
|
||||||
|
|
||||||
export const LeagueCardStats = ({ label, value, percentage, intent = 'primary' }: LeagueCardStatsProps) => {
|
export const LeagueCardStats = ({ label, value, percentage, intent = 'primary' }: LeagueCardStatsProps) => {
|
||||||
@@ -52,17 +68,27 @@ export const LeagueCardStats = ({ label, value, percentage, intent = 'primary' }
|
|||||||
primary: 'var(--ui-color-intent-primary)',
|
primary: 'var(--ui-color-intent-primary)',
|
||||||
success: 'var(--ui-color-intent-success)',
|
success: 'var(--ui-color-intent-success)',
|
||||||
warning: 'var(--ui-color-intent-warning)',
|
warning: 'var(--ui-color-intent-warning)',
|
||||||
|
telemetry: 'var(--ui-color-intent-telemetry)',
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box marginBottom={4}>
|
<Box marginBottom={6}>
|
||||||
<Box display="flex" alignItems="center" justifyContent="between" marginBottom={1.5}>
|
<Stack gap={2}>
|
||||||
<Text size="xs" variant="low" weight="bold" uppercase>{label}</Text>
|
<Group justify="between" align="end">
|
||||||
<Text size="xs" variant="med" font="mono">{value}</Text>
|
<Text size="xs" variant="low" weight="bold" uppercase letterSpacing="widest">{label}</Text>
|
||||||
</Box>
|
<Text size="sm" variant="high" font="mono" weight="bold">{value}</Text>
|
||||||
<Box height="4px" bg="var(--ui-color-bg-surface-muted)" rounded="full" style={{ overflow: 'hidden' }}>
|
</Group>
|
||||||
<Box height="100%" bg={intentColors[intent]} style={{ width: `${Math.min(percentage, 100)}%` }} />
|
<Box height="2px" bg="var(--ui-color-bg-surface-muted)" rounded="full" overflow="hidden">
|
||||||
</Box>
|
<Box
|
||||||
|
height="100%"
|
||||||
|
bg={intentColors[intent]}
|
||||||
|
style={{
|
||||||
|
width: `${Math.min(percentage, 100)}%`,
|
||||||
|
boxShadow: `0 0 8px ${intentColors[intent]}44`
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
</Stack>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -72,11 +98,19 @@ export interface LeagueCardFooterProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const LeagueCardFooter = ({ children }: LeagueCardFooterProps) => (
|
export const LeagueCardFooter = ({ children }: LeagueCardFooterProps) => (
|
||||||
<Box marginTop="auto" paddingTop={3} style={{ borderTop: '1px solid var(--ui-color-border-muted)', opacity: 0.5 }} display="flex" alignItems="center" justifyContent="between">
|
<Box
|
||||||
{children}
|
marginTop="auto"
|
||||||
<Box display="flex" alignItems="center" gap={1}>
|
paddingTop={4}
|
||||||
<Text size="xs" variant="low" weight="bold" uppercase>VIEW</Text>
|
style={{ borderTop: '1px solid var(--ui-color-border-muted)' }}
|
||||||
<Icon icon={ChevronRight} size={3} intent="low" />
|
>
|
||||||
</Box>
|
<Group justify="between" align="center">
|
||||||
|
<Box flex={1}>
|
||||||
|
{children}
|
||||||
|
</Box>
|
||||||
|
<Group gap={1} align="center">
|
||||||
|
<Text size="xs" variant="low" weight="bold" uppercase letterSpacing="widest">ACCESS</Text>
|
||||||
|
<Icon icon={ChevronRight} size={3} intent="low" />
|
||||||
|
</Group>
|
||||||
|
</Group>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user