-
+ {stat.icon ? (
+
+ ) : (
+
+ )}
{stat.value} {stat.label}
@@ -84,14 +93,19 @@ export const HeroSection = ({
{actions && actions.length > 0 && (
{actions.map((action, index) => (
-
- {action.label}
-
+
+
+ {action.icon && }
+ {action.label}
+
+ {action.description && (
+
{action.description}
+ )}
+
))}
)}
diff --git a/apps/website/components/shared/StatusBadge.tsx b/apps/website/components/shared/StatusBadge.tsx
index a8f6e2546..f0ede38df 100644
--- a/apps/website/components/shared/StatusBadge.tsx
+++ b/apps/website/components/shared/StatusBadge.tsx
@@ -1,5 +1,6 @@
import React from 'react';
import { LucideIcon } from 'lucide-react';
+import { StatusBadge as UIStatusBadge } from '@/ui/StatusBadge';
interface StatusBadgeProps {
status: string;
@@ -16,51 +17,42 @@ interface StatusBadgeProps {
export const StatusBadge = ({ status, config, className = '' }: StatusBadgeProps) => {
const defaultConfig = {
scheduled: {
- icon: () => null,
- color: 'text-primary-blue',
- bg: 'bg-primary-blue/10',
- border: 'border-primary-blue/30',
+ icon: undefined,
+ variant: 'info' as const,
label: 'Scheduled',
},
running: {
- icon: () => null,
- color: 'text-performance-green',
- bg: 'bg-performance-green/10',
- border: 'border-performance-green/30',
+ icon: undefined,
+ variant: 'success' as const,
label: 'LIVE',
},
completed: {
- icon: () => null,
- color: 'text-gray-400',
- bg: 'bg-gray-500/10',
- border: 'border-gray-500/30',
+ icon: undefined,
+ variant: 'neutral' as const,
label: 'Completed',
},
cancelled: {
- icon: () => null,
- color: 'text-warning-amber',
- bg: 'bg-warning-amber/10',
- border: 'border-warning-amber/30',
+ icon: undefined,
+ variant: 'warning' as const,
label: 'Cancelled',
},
};
- const badgeConfig = config || defaultConfig[status as keyof typeof defaultConfig] || {
- icon: () => null,
- color: 'text-gray-400',
- bg: 'bg-gray-500/10',
- border: 'border-gray-500/30',
- label: status,
- };
-
- const Icon = badgeConfig.icon;
+ const badgeConfig = config
+ ? { ...config, variant: 'info' as const } // Fallback variant if config is provided
+ : defaultConfig[status as keyof typeof defaultConfig] || {
+ icon: undefined,
+ variant: 'neutral' as const,
+ label: status,
+ };
return (
-
- {Icon && }
-
- {badgeConfig.label}
-
-
+
+ {badgeConfig.label}
+
);
};
\ No newline at end of file
diff --git a/apps/website/components/shared/state/EmptyState.tsx b/apps/website/components/shared/state/EmptyState.tsx
index f6ea7095a..d435908f8 100644
--- a/apps/website/components/shared/state/EmptyState.tsx
+++ b/apps/website/components/shared/state/EmptyState.tsx
@@ -2,7 +2,7 @@
import React from 'react';
import { EmptyStateProps, EmptyStateAction } from './types';
-import Button from '@/ui/Button';
+import { Button } from '@/ui/Button';
// Illustration components (simple SVG representations)
const Illustrations = {
diff --git a/apps/website/components/social/FriendPill.tsx b/apps/website/components/social/FriendPill.tsx
deleted file mode 100644
index c02673d69..000000000
--- a/apps/website/components/social/FriendPill.tsx
+++ /dev/null
@@ -1,44 +0,0 @@
-import React from 'react';
-import Image from 'next/image';
-import Link from 'next/link';
-import { getMediaUrl } from '@/lib/utilities/media';
-
-interface Friend {
- id: string;
- name: string;
- country: string;
-}
-
-interface FriendPillProps {
- friend: Friend;
-}
-
-function getCountryFlag(countryCode: string): string {
- const code = countryCode.toUpperCase();
- if (code.length === 2) {
- const codePoints = [...code].map(char => 127397 + char.charCodeAt(0));
- return String.fromCodePoint(...codePoints);
- }
- return '🏁';
-}
-
-export default function FriendPill({ friend }: FriendPillProps) {
- return (
-
-
-
-
-
{friend.name}
-
{getCountryFlag(friend.country)}
-
- );
-}
\ No newline at end of file
diff --git a/apps/website/components/social/SocialHandles.tsx b/apps/website/components/social/SocialHandles.tsx
deleted file mode 100644
index 0dd10bfb8..000000000
--- a/apps/website/components/social/SocialHandles.tsx
+++ /dev/null
@@ -1,63 +0,0 @@
-import React from 'react';
-import { MessageCircle, Twitter, Youtube, Twitch } from 'lucide-react';
-import type { DriverProfileSocialHandleViewModel } from '@/lib/view-models/DriverProfileViewModel';
-
-interface SocialHandlesProps {
- socialHandles: DriverProfileSocialHandleViewModel[];
-}
-
-function getSocialIcon(platform: DriverProfileSocialHandleViewModel['platform']) {
- switch (platform) {
- case 'twitter':
- return Twitter;
- case 'youtube':
- return Youtube;
- case 'twitch':
- return Twitch;
- case 'discord':
- return MessageCircle;
- }
-}
-
-function getSocialColor(platform: DriverProfileSocialHandleViewModel['platform']) {
- switch (platform) {
- case 'twitter':
- return 'hover:text-sky-400 hover:bg-sky-400/10';
- case 'youtube':
- return 'hover:text-red-500 hover:bg-red-500/10';
- case 'twitch':
- return 'hover:text-purple-400 hover:bg-purple-400/10';
- case 'discord':
- return 'hover:text-indigo-400 hover:bg-indigo-400/10';
- }
-}
-
-export default function SocialHandles({ socialHandles }: SocialHandlesProps) {
- if (socialHandles.length === 0) return null;
-
- return (
-
-
-
Connect:
- {socialHandles.map((social) => {
- const Icon = getSocialIcon(social.platform);
- return (
-
-
- {social.handle}
-
-
-
-
- );
- })}
-
-
- );
-}
\ No newline at end of file
diff --git a/apps/website/ui/TeamLogo.tsx b/apps/website/components/teams/TeamLogo.tsx
similarity index 100%
rename from apps/website/ui/TeamLogo.tsx
rename to apps/website/components/teams/TeamLogo.tsx
diff --git a/apps/website/components/teams/TeamRankingsTable.tsx b/apps/website/components/teams/TeamRankingsTable.tsx
index 5779bae22..28d4fec1f 100644
--- a/apps/website/components/teams/TeamRankingsTable.tsx
+++ b/apps/website/components/teams/TeamRankingsTable.tsx
@@ -1,7 +1,7 @@
'use client';
import React from 'react';
-import { Medal, Users, Globe, Languages } from 'lucide-react';
+import { Medal, Users } from 'lucide-react';
import { Box } from '@/ui/Box';
import { Stack } from '@/ui/Stack';
import { Text } from '@/ui/Text';
@@ -32,66 +32,56 @@ interface TeamRankingsTableProps {
export function TeamRankingsTable({ teams, sortBy, onTeamClick }: TeamRankingsTableProps) {
return (
-
+
{/* Table Header */}
-
- Rank
- Team
- Members
- Rating
- Wins
+
+ Rank
+ Team
+ Members
+ Rating
+ Wins
{/* Table Body */}
{teams.map((team, index) => {
- const winRate = team.totalRaces > 0 ? ((team.totalWins / team.totalRaces) * 100).toFixed(1) : '0.0';
-
return (
onTeamClick(team.id)}
- style={{
- display: 'grid',
- gridTemplateColumns: 'repeat(12, minmax(0, 1fr))',
- gap: '1rem',
- padding: '1rem',
- width: '100%',
- textAlign: 'left',
- backgroundColor: 'transparent',
- border: 'none',
- cursor: 'pointer',
- borderBottom: index < teams.length - 1 ? '1px solid rgba(38, 38, 38, 0.5)' : 'none'
- }}
+ display="grid"
+ className={`grid-cols-12 gap-4 p-4 w-full text-left bg-transparent border-0 cursor-pointer hover:bg-iron-gray/20 transition-colors ${
+ index < teams.length - 1 ? 'border-b border-charcoal-outline/50' : ''
+ }`}
>
{/* Position */}
-
-
- {index < 3 ? : index + 1}
-
+
+
+ {index < 3 ? : {index + 1} }
+
{/* Team Info */}
-
-
+
+
-
- {team.name}
+
+ {team.name}
{team.performanceLevel}
{team.category && (
-
- {team.category}
+
+ {team.category}
)}
@@ -99,22 +89,22 @@ export function TeamRankingsTable({ teams, sortBy, onTeamClick }: TeamRankingsTa
{/* Members */}
-
+
-
+
{team.memberCount}
{/* Rating */}
-
+
0
{/* Wins */}
-
+
{team.totalWins}
@@ -123,6 +113,6 @@ export function TeamRankingsTable({ teams, sortBy, onTeamClick }: TeamRankingsTa
);
})}
-
+
);
}
diff --git a/apps/website/components/teams/TeamRoster.tsx b/apps/website/components/teams/TeamRoster.tsx
index 7c105f27d..666940175 100644
--- a/apps/website/components/teams/TeamRoster.tsx
+++ b/apps/website/components/teams/TeamRoster.tsx
@@ -1,10 +1,18 @@
'use client';
-import Card from '@/ui/Card';
+import { Card } from '@/ui/Card';
import { DriverIdentity } from '@/components/drivers/DriverIdentity';
import { useTeamRoster } from "@/lib/hooks/team";
import { useState } from 'react';
import type { DriverViewModel } from '@/lib/view-models/DriverViewModel';
+import { Box } from '@/ui/Box';
+import { Stack } from '@/ui/Stack';
+import { Text } from '@/ui/Text';
+import { Heading } from '@/ui/Heading';
+import { Select } from '@/ui/Select';
+import { Surface } from '@/ui/Surface';
+import { Badge } from '@/ui/Badge';
+import { Button } from '@/ui/Button';
type TeamRole = 'owner' | 'admin' | 'member';
type TeamMemberRole = 'owner' | 'manager' | 'member';
@@ -24,7 +32,7 @@ interface TeamRosterProps {
onChangeRole?: (driverId: string, newRole: TeamRole) => void;
}
-export default function TeamRoster({
+export function TeamRoster({
teamId,
memberships,
isAdmin,
@@ -36,17 +44,6 @@ export default function TeamRoster({
// Use hook for data fetching
const { data: teamMembers = [], isLoading: loading } = useTeamRoster(memberships);
- const getRoleBadgeColor = (role: TeamRole) => {
- switch (role) {
- case 'owner':
- return 'bg-warning-amber/20 text-warning-amber';
- case 'admin':
- return 'bg-primary-blue/20 text-primary-blue';
- default:
- return 'bg-charcoal-outline text-gray-300';
- }
- };
-
const getRoleLabel = (role: TeamRole | TeamMemberRole) => {
// Convert manager to admin for display
const displayRole = role === 'manager' ? 'admin' : role;
@@ -85,43 +82,48 @@ export default function TeamRoster({
});
const teamAverageRating = teamMembers.length > 0
- ? teamMembers.reduce((sum, m) => sum + (m.rating || 0), 0) / teamMembers.length
+ ? teamMembers.reduce((sum: number, m: any) => sum + (m.rating || 0), 0) / teamMembers.length
: 0;
if (loading) {
return (
- Loading roster...
+
+ Loading roster...
+
);
}
return (
-
-
-
Team Roster
-
+
+
+ Team Roster
+
{memberships.length} {memberships.length === 1 ? 'member' : 'members'} • Avg Rating:{' '}
- {teamAverageRating}
-
-
+
{teamAverageRating.toFixed(0)}
+
+
-
- Sort by:
- setSortBy(e.target.value as typeof sortBy)}
- className="px-3 py-1 bg-deep-graphite border border-charcoal-outline rounded text-white text-sm focus:outline-none focus:ring-2 focus:ring-primary-blue"
- >
- Rating
- Role
- Name
-
-
-
+
+ Sort by:
+
+ setSortBy(e.target.value as typeof sortBy)}
+ options={[
+ { value: 'rating', label: 'Rating' },
+ { value: 'role', label: 'Role' },
+ { value: 'name', label: 'Name' },
+ ]}
+ className="py-1 text-sm"
+ />
+
+
+
-
+
{sortedMembers.map((member) => {
const { driver, role, joinedAt, rating, overallRank } = member;
@@ -130,68 +132,79 @@ export default function TeamRoster({
const canManageMembership = isAdmin && role !== 'owner';
return (
-
-
- {driver.country} • Joined {new Date(joinedAt).toLocaleDateString()}
-
- }
- size="md"
- />
+
+
+ {driver.country} • Joined {new Date(joinedAt).toLocaleDateString()}
+
+ }
+ size="md"
+ />
- {rating !== null && (
-
-
-
- {rating}
-
-
Rating
-
- {overallRank !== null && (
-
-
#{overallRank}
-
Rank
-
- )}
-
- )}
+ {rating !== null && (
+
+
+
+ {rating}
+
+ Rating
+
+ {overallRank !== null && (
+
+ #{overallRank}
+ Rank
+
+ )}
+
+ )}
- {canManageMembership && (
-
-
- onChangeRole?.(driver.id, e.target.value as TeamRole)
- }
- >
- Member
- Admin
-
+ {canManageMembership && (
+
+
+
+ onChangeRole?.(driver.id, e.target.value as TeamRole)
+ }
+ options={[
+ { value: 'member', label: 'Member' },
+ { value: 'admin', label: 'Admin' },
+ ]}
+ className="text-sm"
+ />
+
- onRemoveMember?.(driver.id)}
- className="px-3 py-2 bg-danger-red/20 hover:bg-danger-red/30 text-danger-red rounded text-sm font-medium transition-colors"
- >
- Remove
-
-
- )}
-
+ onRemoveMember?.(driver.id)}
+ >
+ Remove
+
+
+ )}
+
+
);
})}
-
+
{memberships.length === 0 && (
- No team members yet.
+
+ No team members yet.
+
)}
);
-}
\ No newline at end of file
+}
diff --git a/apps/website/components/teams/TeamSearchBar.tsx b/apps/website/components/teams/TeamSearchBar.tsx
index d15cea5fc..45ba9a64a 100644
--- a/apps/website/components/teams/TeamSearchBar.tsx
+++ b/apps/website/components/teams/TeamSearchBar.tsx
@@ -1,28 +1,30 @@
'use client';
import { Search } from 'lucide-react';
-import Input from '@/ui/Input';
+import { Input } from '@/ui/Input';
+import { Box } from '@/ui/Box';
+import { Stack } from '@/ui/Stack';
+import { Icon } from '@/ui/Icon';
interface TeamSearchBarProps {
searchQuery: string;
onSearchChange: (query: string) => void;
}
-export default function TeamSearchBar({ searchQuery, onSearchChange }: TeamSearchBarProps) {
+export function TeamSearchBar({ searchQuery, onSearchChange }: TeamSearchBarProps) {
return (
-
-
-
-
+
+
+
onSearchChange(e.target.value)}
- className="pl-11"
+ icon={ }
/>
-
-
-
+
+
+
);
-}
\ No newline at end of file
+}
diff --git a/apps/website/components/teams/TeamStandings.tsx b/apps/website/components/teams/TeamStandings.tsx
index d8c348f0a..1c86a7f0a 100644
--- a/apps/website/components/teams/TeamStandings.tsx
+++ b/apps/website/components/teams/TeamStandings.tsx
@@ -1,64 +1,90 @@
'use client';
-import Card from '@/ui/Card';
+import { Card } from '@/ui/Card';
import { useTeamStandings } from "@/lib/hooks/team";
+import { Stack } from '@/ui/Stack';
+import { Box } from '@/ui/Box';
+import { Text } from '@/ui/Text';
+import { Heading } from '@/ui/Heading';
+import { Badge } from '@/ui/Badge';
+import { Grid } from '@/ui/Grid';
+import { Surface } from '@/ui/Surface';
interface TeamStandingsProps {
teamId: string;
leagues: string[];
}
-export default function TeamStandings({ teamId, leagues }: TeamStandingsProps) {
+export function TeamStandings({ teamId, leagues }: TeamStandingsProps) {
const { data: standings = [], isLoading: loading } = useTeamStandings(teamId, leagues);
if (loading) {
return (
- Loading standings...
+
+ Loading standings...
+
);
}
return (
- League Standings
+
+
+ League Standings
+
+
-
+
{standings.map((standing: any) => (
-
-
-
{standing.leagueName}
-
+
+
+ {standing.leagueName}
+
+
P{standing.position}
-
-
+
+
-
-
-
{standing.points}
-
Points
-
-
-
{standing.wins}
-
Wins
-
-
-
{standing.racesCompleted}
-
Races
-
-
-
+
+
+
+ {standing.points}
+
+ Points
+
+
+
+ {standing.wins}
+
+ Wins
+
+
+
+ {standing.racesCompleted}
+
+ Races
+
+
+
))}
-
+
{standings.length === 0 && (
-
- No standings available yet.
-
+
+
+ No standings available yet.
+
+
)}
);
-}
\ No newline at end of file
+}
diff --git a/apps/website/components/teams/TopThreePodium.tsx b/apps/website/components/teams/TopThreePodium.tsx
index 461077104..375107c6d 100644
--- a/apps/website/components/teams/TopThreePodium.tsx
+++ b/apps/website/components/teams/TopThreePodium.tsx
@@ -2,50 +2,20 @@ import Image from 'next/image';
import { Trophy, Crown, Users } from 'lucide-react';
import type { TeamSummaryViewModel } from '@/lib/view-models/TeamSummaryViewModel';
import { getMediaUrl } from '@/lib/utilities/media';
-
-const SKILL_LEVELS: {
- id: string;
- icon: React.ElementType;
- color: string;
- bgColor: string;
- borderColor: string;
-}[] = [
- {
- id: 'pro',
- icon: () => null,
- color: 'text-yellow-400',
- bgColor: 'bg-yellow-400/10',
- borderColor: 'border-yellow-400/30',
- },
- {
- id: 'advanced',
- icon: () => null,
- color: 'text-purple-400',
- bgColor: 'bg-purple-400/10',
- borderColor: 'border-purple-400/30',
- },
- {
- id: 'intermediate',
- icon: () => null,
- color: 'text-primary-blue',
- bgColor: 'bg-primary-blue/10',
- borderColor: 'border-primary-blue/30',
- },
- {
- id: 'beginner',
- icon: () => null,
- color: 'text-green-400',
- bgColor: 'bg-green-400/10',
- borderColor: 'border-green-400/30',
- },
-];
+import { Box } from '@/ui/Box';
+import { Stack } from '@/ui/Stack';
+import { Text } from '@/ui/Text';
+import { Heading } from '@/ui/Heading';
+import { Icon } from '@/ui/Icon';
+import { Surface } from '@/ui/Surface';
+import { Button } from '@/ui/Button';
interface TopThreePodiumProps {
teams: TeamSummaryViewModel[];
onClick: (id: string) => void;
}
-export default function TopThreePodium({ teams, onClick }: TopThreePodiumProps) {
+export function TopThreePodium({ teams, onClick }: TopThreePodiumProps) {
const top3 = teams.slice(0, 3) as [TeamSummaryViewModel, TeamSummaryViewModel, TeamSummaryViewModel];
if (teams.length < 3) return null;
@@ -71,118 +41,120 @@ export default function TopThreePodium({ teams, onClick }: TopThreePodiumProps)
}
};
- const getGradient = (position: number) => {
+ const getVariant = (position: number): any => {
switch (position) {
case 1:
- return 'from-yellow-400/30 via-yellow-500/20 to-yellow-600/10';
+ return 'gradient-gold';
case 2:
- return 'from-gray-300/30 via-gray-400/20 to-gray-500/10';
+ return 'default';
case 3:
- return 'from-amber-500/30 via-amber-600/20 to-amber-700/10';
+ return 'gradient-purple';
default:
- return 'from-gray-600/30 to-gray-700/10';
- }
- };
-
- const getBorderColor = (position: number) => {
- switch (position) {
- case 1:
- return 'border-yellow-400/50';
- case 2:
- return 'border-gray-300/50';
- case 3:
- return 'border-amber-600/50';
- default:
- return 'border-charcoal-outline';
+ return 'muted';
}
};
return (
-
-
-
-
Top 3 Teams
-
+
+
+
+
+ Top 3 Teams
+
+
-
+
{podiumOrder.map((team, index) => {
const position = podiumPositions[index] ?? 0;
- const levelConfig = SKILL_LEVELS.find((l) => l.id === team.performanceLevel);
return (
- onClick(team.id)}
- className="flex flex-col items-center group"
- >
+
{/* Team card */}
- onClick(team.id)}
+ className="p-0 h-auto hover:scale-105 transition-transform"
>
- {/* Crown for 1st place */}
- {position === 1 && (
-
- )}
+
+ {/* Crown for 1st place */}
+ {position === 1 && (
+
+
+
+
+
+
+ )}
- {/* Team logo */}
-
-
-
+ {/* Team logo */}
+
+
+
- {/* Team name */}
-
- {team.name}
-
+ {/* Team name */}
+
+ {team.name}
+
- {/* Category */}
- {team.category && (
-
- {team.category}
-
- )}
+ {/* Category */}
+ {team.category && (
+
+ {team.category}
+
+ )}
- {/* Rating */}
-
- {'—'}
-
+ {/* Rating placeholder */}
+
+ —
+
- {/* Stats row */}
-
-
-
- {team.totalWins}
-
-
-
- {team.memberCount}
-
-
-
+ {/* Stats row */}
+
+
+
+ {team.totalWins}
+
+
+
+ {team.memberCount}
+
+
+
+
{/* Podium stand */}
-
-
- {position}
-
-
-
+
+
+ {position}
+
+
+
+
);
})}
-
-
+
+
);
-}
\ No newline at end of file
+}
diff --git a/apps/website/components/teams/WhyJoinTeamSection.tsx b/apps/website/components/teams/WhyJoinTeamSection.tsx
index c7bc049fd..7b2d1acde 100644
--- a/apps/website/components/teams/WhyJoinTeamSection.tsx
+++ b/apps/website/components/teams/WhyJoinTeamSection.tsx
@@ -3,10 +3,23 @@ import {
MessageCircle,
Calendar,
Trophy,
+ LucideIcon,
} from 'lucide-react';
+import { Box } from '@/ui/Box';
+import { Text } from '@/ui/Text';
+import { Heading } from '@/ui/Heading';
+import { Grid } from '@/ui/Grid';
+import { Surface } from '@/ui/Surface';
+import { Icon } from '@/ui/Icon';
-export default function WhyJoinTeamSection() {
- const benefits = [
+interface Benefit {
+ icon: LucideIcon;
+ title: string;
+ description: string;
+}
+
+export function WhyJoinTeamSection() {
+ const benefits: Benefit[] = [
{
icon: Handshake,
title: 'Shared Strategy',
@@ -30,26 +43,33 @@ export default function WhyJoinTeamSection() {
];
return (
-
-
-
Why Join a Team?
-
Racing is better when you have teammates to share the journey
-
+
+
+
+ Why Join a Team?
+
+ Racing is better when you have teammates to share the journey
+
-
+
{benefits.map((benefit) => (
-
-
-
-
-
{benefit.title}
-
{benefit.description}
-
+
+
+
+
+ {benefit.title}
+
+ {benefit.description}
+
))}
-
-
+
+
);
-}
\ No newline at end of file
+}
diff --git a/apps/website/lib/display-objects/AchievementDisplay.ts b/apps/website/lib/display-objects/AchievementDisplay.ts
new file mode 100644
index 000000000..db0f83b16
--- /dev/null
+++ b/apps/website/lib/display-objects/AchievementDisplay.ts
@@ -0,0 +1,21 @@
+export class AchievementDisplay {
+ static getRarityColor(rarity: string) {
+ switch (rarity.toLowerCase()) {
+ case 'common':
+ return { text: 'text-gray-400', surface: 'muted' as const, icon: '#9ca3af' };
+ case 'rare':
+ return { text: 'text-primary-blue', surface: 'gradient-blue' as const, icon: '#3b82f6' };
+ case 'epic':
+ return { text: 'text-purple-400', surface: 'gradient-purple' as const, icon: '#a855f7' };
+ case 'legendary':
+ return { text: 'text-yellow-400', surface: 'gradient-gold' as const, icon: '#facc15' };
+ default:
+ return { text: 'text-gray-400', surface: 'muted' as const, icon: '#9ca3af' };
+ }
+ }
+
+ static formatDate(date: Date): string {
+ const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
+ return `${months[date.getMonth()]} ${date.getDate()}, ${date.getFullYear()}`;
+ }
+}
diff --git a/apps/website/templates/DriverProfileTemplate.tsx b/apps/website/templates/DriverProfileTemplate.tsx
index 23b04669c..52e5fda3a 100644
--- a/apps/website/templates/DriverProfileTemplate.tsx
+++ b/apps/website/templates/DriverProfileTemplate.tsx
@@ -10,20 +10,19 @@ import { Text } from '@/ui/Text';
import { Button } from '@/ui/Button';
import { Container } from '@/ui/Container';
import { LoadingSpinner } from '@/ui/LoadingSpinner';
-import Breadcrumbs from '@/components/layout/Breadcrumbs';
+import { Breadcrumbs } from '@/components/layout/Breadcrumbs';
import { ProfileHero } from '@/components/profile/ProfileHero';
import { ProfileBio } from '@/components/profile/ProfileBio';
import { TeamMembershipGrid } from '@/components/profile/TeamMembershipGrid';
import { PerformanceOverview } from '@/components/profile/PerformanceOverview';
-import { ProfileTabs } from '@/components/profile/ProfileTabs';
import { CareerStats } from '@/components/profile/CareerStats';
import { RacingProfile } from '@/components/profile/RacingProfile';
import { AchievementGrid } from '@/components/profile/AchievementGrid';
import { FriendsPreview } from '@/components/profile/FriendsPreview';
+import RatingBreakdown from '@/components/drivers/RatingBreakdown';
+import { ProfileTabs, type ProfileTab } from '@/components/profile/ProfileTabs';
import type { DriverProfileViewData } from '../../../lib/types/view-data/DriverProfileViewData';
-type ProfileTab = 'overview' | 'stats';
-
interface DriverProfileTemplateProps {
viewData: DriverProfileViewData;
isLoading?: boolean;
@@ -184,6 +183,14 @@ export function DriverProfileTemplate({
This driver hasn't completed any races yet
)}
+
+ {activeTab === 'ratings' && (
+
+ )}
);
diff --git a/apps/website/templates/DriversTemplate.tsx b/apps/website/templates/DriversTemplate.tsx
index b271231d3..321d5b2c5 100644
--- a/apps/website/templates/DriversTemplate.tsx
+++ b/apps/website/templates/DriversTemplate.tsx
@@ -19,7 +19,8 @@ import { SkillDistribution } from '@/components/drivers/SkillDistribution';
import { CategoryDistribution } from '@/components/drivers/CategoryDistribution';
import { LeaderboardPreview } from '@/components/drivers/LeaderboardPreview';
import { RecentActivity } from '@/components/drivers/RecentActivity';
-import { DriversHero } from '@/components/drivers/DriversHero';
+import { HeroSection } from '@/components/shared/HeroSection';
+import { Users, Trophy } from 'lucide-react';
import { DriversSearch } from '@/components/drivers/DriversSearch';
import { EmptyState } from '@/components/shared/state/EmptyState';
import type { DriversViewData } from '@/lib/types/view-data/DriversViewData';
@@ -53,12 +54,24 @@ export function DriversTemplate({
{/* Hero Section */}
-
{/* Search */}
diff --git a/apps/website/templates/HomeTemplate.tsx b/apps/website/templates/HomeTemplate.tsx
index 54d524c1c..dc2833470 100644
--- a/apps/website/templates/HomeTemplate.tsx
+++ b/apps/website/templates/HomeTemplate.tsx
@@ -26,6 +26,7 @@ import { Surface } from '@/ui/Surface';
import { getMediaUrl } from '@/lib/utilities/media';
import { routes } from '@/lib/routing/RouteConfig';
import { FeatureItem, ResultItem, StepItem } from '@/components/landing/LandingItems';
+import { ModeGuard } from '@/components/shared/ModeGuard';
export interface HomeViewData {
isAlpha: boolean;
@@ -152,7 +153,7 @@ export function HomeTemplate({ viewData }: HomeTemplateProps) {
/>
{/* Alpha-only discovery section */}
- {viewData.isAlpha && (
+
@@ -266,7 +267,7 @@ export function HomeTemplate({ viewData }: HomeTemplateProps) {
- )}
+
diff --git a/apps/website/templates/LeagueDetailTemplate.tsx b/apps/website/templates/LeagueDetailTemplate.tsx
index 460dc7a42..401568da3 100644
--- a/apps/website/templates/LeagueDetailTemplate.tsx
+++ b/apps/website/templates/LeagueDetailTemplate.tsx
@@ -6,7 +6,7 @@ import { Stack } from '@/ui/Stack';
import { Text } from '@/ui/Text';
import { Container } from '@/ui/Container';
import { Heading } from '@/ui/Heading';
-import Breadcrumbs from '@/components/layout/Breadcrumbs';
+import { Breadcrumbs } from '@/components/layout/Breadcrumbs';
import { LeagueTabs } from '@/components/leagues/LeagueTabs';
import type { LeagueDetailViewData } from '@/lib/view-data/LeagueDetailViewData';
diff --git a/apps/website/templates/LeagueRulebookTemplate.tsx b/apps/website/templates/LeagueRulebookTemplate.tsx
index 7bae8868c..4a4fbcb78 100644
--- a/apps/website/templates/LeagueRulebookTemplate.tsx
+++ b/apps/website/templates/LeagueRulebookTemplate.tsx
@@ -13,6 +13,7 @@ import PointsTable from '@/components/leagues/PointsTable';
import { RulebookTabs, type RulebookSection } from '@/components/leagues/RulebookTabs';
import type { LeagueRulebookViewData } from '@/lib/view-data/LeagueRulebookViewData';
import { Surface } from '@/ui/Surface';
+import { Clock } from 'lucide-react';
interface LeagueRulebookTemplateProps {
viewData: LeagueRulebookViewData;
@@ -81,6 +82,34 @@ export function LeagueRulebookTemplate({
+ {/* Weekend Structure */}
+
+
+
+
+ Weekend Structure & Timings
+
+
+
+ Practice
+ 20 min
+
+
+ Qualifying
+ 30 min
+
+
+ Sprint
+ —
+
+
+ Main Race
+ 40 min
+
+
+
+
+
{/* Points Table */}
diff --git a/apps/website/templates/LeagueScheduleTemplate.tsx b/apps/website/templates/LeagueScheduleTemplate.tsx
index 7ce10e4cf..308638b28 100644
--- a/apps/website/templates/LeagueScheduleTemplate.tsx
+++ b/apps/website/templates/LeagueScheduleTemplate.tsx
@@ -1,16 +1,12 @@
'use client';
import React from 'react';
-import { Card } from '@/ui/Card';
import { Box } from '@/ui/Box';
import { Stack } from '@/ui/Stack';
import { Text } from '@/ui/Text';
import { Heading } from '@/ui/Heading';
-import { Icon } from '@/ui/Icon';
-import { Calendar } from 'lucide-react';
import type { LeagueScheduleViewData } from '@/lib/view-data/leagues/LeagueScheduleViewData';
-import { ScheduleRaceCard } from '@/components/leagues/ScheduleRaceCard';
-import { Surface } from '@/ui/Surface';
+import LeagueSchedule from '@/components/leagues/LeagueSchedule';
interface LeagueScheduleTemplateProps {
viewData: LeagueScheduleViewData;
@@ -26,25 +22,7 @@ export function LeagueScheduleTemplate({ viewData }: LeagueScheduleTemplateProps
- {viewData.races.length === 0 ? (
-
-
-
-
-
-
- No Races Scheduled
- The race schedule will appear here once events are added.
-
-
-
- ) : (
-
- {viewData.races.map((race) => (
-
- ))}
-
- )}
+
);
}
diff --git a/apps/website/templates/LeagueSponsorshipsTemplate.tsx b/apps/website/templates/LeagueSponsorshipsTemplate.tsx
index 2bb61577f..ff30134fd 100644
--- a/apps/website/templates/LeagueSponsorshipsTemplate.tsx
+++ b/apps/website/templates/LeagueSponsorshipsTemplate.tsx
@@ -1,6 +1,6 @@
'use client';
-import React from 'react';
+import React, { useState } from 'react';
import { Card } from '@/ui/Card';
import { Box } from '@/ui/Box';
import { Stack } from '@/ui/Stack';
@@ -9,107 +9,145 @@ import { Heading } from '@/ui/Heading';
import { Grid } from '@/ui/Grid';
import { Icon } from '@/ui/Icon';
import { Surface } from '@/ui/Surface';
-import { Building, Clock } from 'lucide-react';
+import { Building, Clock, Palette } from 'lucide-react';
import type { LeagueSponsorshipsViewData } from '@/lib/view-data/leagues/LeagueSponsorshipsViewData';
import { SponsorshipSlotCard } from '@/components/leagues/SponsorshipSlotCard';
import { SponsorshipRequestCard } from '@/components/leagues/SponsorshipRequestCard';
+import LeagueDecalPlacementEditor from '@/components/leagues/LeagueDecalPlacementEditor';
interface LeagueSponsorshipsTemplateProps {
viewData: LeagueSponsorshipsViewData;
}
export function LeagueSponsorshipsTemplate({ viewData }: LeagueSponsorshipsTemplateProps) {
+ const [activeTab, setActiveTab] = useState<'overview' | 'editor'>('overview');
+
return (
-
- Sponsorships
-
- Manage sponsorship slots and review requests
-
-
-
-
- {/* Sponsorship Slots */}
-
-
-
-
-
-
-
- Sponsorship Slots
- Available sponsorship opportunities
-
-
-
- {viewData.sponsorshipSlots.length === 0 ? (
-
-
- No sponsorship slots available
-
- ) : (
-
- {viewData.sponsorshipSlots.map((slot) => (
-
- ))}
-
- )}
-
-
-
- {/* Sponsorship Requests */}
-
-
-
-
-
-
-
- Sponsorship Requests
- Pending and processed sponsorship applications
-
-
-
- {viewData.sponsorshipRequests.length === 0 ? (
-
-
- No sponsorship requests
-
- ) : (
-
- {viewData.sponsorshipRequests.map((request) => {
- const slot = viewData.sponsorshipSlots.find(s => s.id === request.slotId);
- return (
-
- );
- })}
-
- )}
-
-
-
- {/* Note about management */}
-
-
-
-
-
-
- Sponsorship Management
-
- Interactive management features for approving requests and managing slots will be implemented in future updates.
-
-
-
-
+
+
+ Sponsorships
+
+ Manage sponsorship slots and review requests
+
+
+
+ setActiveTab('overview')}
+ className={`px-4 py-2 rounded-lg text-sm font-medium transition-colors ${
+ activeTab === 'overview'
+ ? 'bg-primary-blue text-white'
+ : 'bg-iron-gray text-gray-400 hover:text-white'
+ }`}
+ >
+ Overview
+
+ setActiveTab('editor')}
+ className={`px-4 py-2 rounded-lg text-sm font-medium transition-colors ${
+ activeTab === 'editor'
+ ? 'bg-primary-blue text-white'
+ : 'bg-iron-gray text-gray-400 hover:text-white'
+ }`}
+ >
+ Livery Editor
+
+
+
+ {activeTab === 'overview' ? (
+
+ {/* Sponsorship Slots */}
+
+
+
+
+
+
+
+ Sponsorship Slots
+ Available sponsorship opportunities
+
+
+
+ {viewData.sponsorshipSlots.length === 0 ? (
+
+
+ No sponsorship slots available
+
+ ) : (
+
+ {viewData.sponsorshipSlots.map((slot) => (
+
+ ))}
+
+ )}
+
+
+
+ {/* Sponsorship Requests */}
+
+
+
+
+
+
+
+ Sponsorship Requests
+ Pending and processed sponsorship applications
+
+
+
+ {viewData.sponsorshipRequests.length === 0 ? (
+
+
+ No sponsorship requests
+
+ ) : (
+
+ {viewData.sponsorshipRequests.map((request) => {
+ const slot = viewData.sponsorshipSlots.find(s => s.id === request.slotId);
+ return (
+
+ );
+ })}
+
+ )}
+
+
+
+ ) : (
+
+
+
+
+
+
+
+ League Livery Editor
+ Configure where sponsor decals appear on league cars
+
+
+
+ {
+ console.log('Placements saved:', placements);
+ }}
+ />
+
+
+ )}
);
}
diff --git a/apps/website/templates/LeagueWalletTemplate.tsx b/apps/website/templates/LeagueWalletTemplate.tsx
index 6a478237f..5bf0c90f9 100644
--- a/apps/website/templates/LeagueWalletTemplate.tsx
+++ b/apps/website/templates/LeagueWalletTemplate.tsx
@@ -8,9 +8,10 @@ import { Text } from '@/ui/Text';
import { Heading } from '@/ui/Heading';
import { Icon } from '@/ui/Icon';
import { Surface } from '@/ui/Surface';
-import { Wallet, Calendar } from 'lucide-react';
+import { Wallet, Calendar, DollarSign } from 'lucide-react';
import type { LeagueWalletViewData } from '@/lib/view-data/leagues/LeagueWalletViewData';
import { TransactionRow } from '@/components/leagues/TransactionRow';
+import { LeagueMembershipFeesSection } from '@/components/leagues/LeagueMembershipFeesSection';
interface LeagueWalletTemplateProps {
viewData: LeagueWalletViewData;
@@ -70,6 +71,22 @@ export function LeagueWalletTemplate({ viewData }: LeagueWalletTemplateProps) {
+ {/* Membership Fees */}
+
+
+
+
+
+
+
+ Membership Fees
+ Configure how drivers pay for participation
+
+
+
+
+
+
{/* Note about features */}
diff --git a/apps/website/templates/ProfileTemplate.tsx b/apps/website/templates/ProfileTemplate.tsx
index c7f140641..fb637d86c 100644
--- a/apps/website/templates/ProfileTemplate.tsx
+++ b/apps/website/templates/ProfileTemplate.tsx
@@ -25,7 +25,7 @@ import {
History,
User,
} from 'lucide-react';
-import Breadcrumbs from '@/components/layout/Breadcrumbs';
+import { Breadcrumbs } from '@/components/layout/Breadcrumbs';
export type ProfileTab = 'overview' | 'history' | 'stats';
diff --git a/apps/website/templates/RaceDetailTemplate.tsx b/apps/website/templates/RaceDetailTemplate.tsx
index 33638fddb..9353142df 100644
--- a/apps/website/templates/RaceDetailTemplate.tsx
+++ b/apps/website/templates/RaceDetailTemplate.tsx
@@ -1,7 +1,7 @@
'use client';
import React from 'react';
-import Breadcrumbs from '@/components/layout/Breadcrumbs';
+import { Breadcrumbs } from '@/components/layout/Breadcrumbs';
import { Button } from '@/ui/Button';
import { Box } from '@/ui/Box';
import { Stack } from '@/ui/Stack';
diff --git a/apps/website/templates/RaceResultsTemplate.tsx b/apps/website/templates/RaceResultsTemplate.tsx
index a99847b7c..88d5fe94c 100644
--- a/apps/website/templates/RaceResultsTemplate.tsx
+++ b/apps/website/templates/RaceResultsTemplate.tsx
@@ -1,7 +1,7 @@
'use client';
import React from 'react';
-import Breadcrumbs from '@/components/layout/Breadcrumbs';
+import { Breadcrumbs } from '@/components/layout/Breadcrumbs';
import { Button } from '@/ui/Button';
import { Card } from '@/ui/Card';
import { Box } from '@/ui/Box';
diff --git a/apps/website/templates/RaceStewardingTemplate.tsx b/apps/website/templates/RaceStewardingTemplate.tsx
index 57cde6848..6ce871a0d 100644
--- a/apps/website/templates/RaceStewardingTemplate.tsx
+++ b/apps/website/templates/RaceStewardingTemplate.tsx
@@ -1,7 +1,7 @@
'use client';
import React from 'react';
-import Breadcrumbs from '@/components/layout/Breadcrumbs';
+import { Breadcrumbs } from '@/components/layout/Breadcrumbs';
import RaceStewardingStats from '@/components/races/RaceStewardingStats';
import { StewardingTabs } from '@/components/races/StewardingTabs';
import { ProtestCard } from '@/components/races/ProtestCard';
diff --git a/apps/website/templates/RacesAllTemplate.tsx b/apps/website/templates/RacesAllTemplate.tsx
index e2fc08766..0cd10f3dd 100644
--- a/apps/website/templates/RacesAllTemplate.tsx
+++ b/apps/website/templates/RacesAllTemplate.tsx
@@ -4,7 +4,7 @@ import React, { useMemo, useEffect } from 'react';
import { Card } from '@/ui/Card';
import { Button } from '@/ui/Button';
import { Heading } from '@/ui/Heading';
-import Breadcrumbs from '@/components/layout/Breadcrumbs';
+import { Breadcrumbs } from '@/components/layout/Breadcrumbs';
import {
Flag,
SlidersHorizontal,
diff --git a/apps/website/templates/TeamDetailTemplate.tsx b/apps/website/templates/TeamDetailTemplate.tsx
index 9077a01c0..8d969fd3a 100644
--- a/apps/website/templates/TeamDetailTemplate.tsx
+++ b/apps/website/templates/TeamDetailTemplate.tsx
@@ -1,6 +1,6 @@
'use client';
-import Breadcrumbs from '@/components/layout/Breadcrumbs';
+import { Breadcrumbs } from '@/components/layout/Breadcrumbs';
import { SlotTemplates } from '@/components/sponsors/SlotTemplates';
import SponsorInsightsCard from '@/components/sponsors/SponsorInsightsCard';
import { useSponsorMode } from '@/hooks/sponsor/useSponsorMode';
diff --git a/apps/website/ui/Box.tsx b/apps/website/ui/Box.tsx
index e1514a91b..455f5c9e8 100644
--- a/apps/website/ui/Box.tsx
+++ b/apps/website/ui/Box.tsx
@@ -2,33 +2,74 @@ import React, { forwardRef, ForwardedRef, ElementType, ComponentPropsWithoutRef
type Spacing = 0 | 0.5 | 1 | 1.5 | 2 | 2.5 | 3 | 3.5 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 14 | 16 | 20 | 24 | 28 | 32 | 36 | 40 | 44 | 48 | 52 | 56 | 60 | 64 | 72 | 80 | 96;
-interface BoxProps {
+interface ResponsiveSpacing {
+ base?: Spacing;
+ md?: Spacing;
+ lg?: Spacing;
+}
+
+export interface BoxProps {
as?: T;
children?: React.ReactNode;
className?: string;
center?: boolean;
fullWidth?: boolean;
fullHeight?: boolean;
- m?: Spacing;
- mt?: Spacing;
- mb?: Spacing;
- ml?: Spacing;
- mr?: Spacing;
- mx?: Spacing | 'auto';
- my?: Spacing;
- p?: Spacing;
- pt?: Spacing;
- pb?: Spacing;
- pl?: Spacing;
- pr?: Spacing;
- px?: Spacing;
- py?: Spacing;
+ m?: Spacing | ResponsiveSpacing;
+ mt?: Spacing | ResponsiveSpacing;
+ mb?: Spacing | ResponsiveSpacing;
+ ml?: Spacing | ResponsiveSpacing;
+ mr?: Spacing | ResponsiveSpacing;
+ mx?: Spacing | 'auto' | ResponsiveSpacing;
+ my?: Spacing | ResponsiveSpacing;
+ p?: Spacing | ResponsiveSpacing;
+ pt?: Spacing | ResponsiveSpacing;
+ pb?: Spacing | ResponsiveSpacing;
+ pl?: Spacing | ResponsiveSpacing;
+ pr?: Spacing | ResponsiveSpacing;
+ px?: Spacing | ResponsiveSpacing;
+ py?: Spacing | ResponsiveSpacing;
display?: 'block' | 'inline-block' | 'flex' | 'inline-flex' | 'grid' | 'none';
+ flexDirection?: 'row' | 'row-reverse' | 'col' | 'col-reverse';
+ alignItems?: 'start' | 'center' | 'end' | 'stretch' | 'baseline';
+ justifyContent?: 'start' | 'center' | 'end' | 'between' | 'around' | 'evenly';
+ flexWrap?: 'wrap' | 'nowrap' | 'wrap-reverse';
+ flexShrink?: number;
+ flexGrow?: number;
+ gridCols?: number | string;
+ responsiveGridCols?: {
+ base?: number | string;
+ md?: number | string;
+ lg?: number | string;
+ };
+ gap?: Spacing;
position?: 'relative' | 'absolute' | 'fixed' | 'sticky';
+ top?: Spacing | string;
+ bottom?: Spacing | string;
+ left?: Spacing | string;
+ right?: Spacing | string;
overflow?: 'visible' | 'hidden' | 'scroll' | 'auto';
maxWidth?: string;
+ zIndex?: number;
+ w?: string | ResponsiveValue;
+ h?: string | ResponsiveValue;
+ width?: string;
+ height?: string;
+ rounded?: 'none' | 'sm' | 'md' | 'lg' | 'xl' | '2xl' | 'full';
+ border?: boolean;
+ borderColor?: string;
+ bg?: string;
+ shadow?: string;
+ hoverBorderColor?: string;
+ transition?: boolean;
}
+type ResponsiveValue = {
+ base?: T;
+ md?: T;
+ lg?: T;
+};
+
export const Box = forwardRef((
{
as,
@@ -41,8 +82,27 @@ export const Box = forwardRef((
p, pt, pb, pl, pr, px, py,
display,
position,
+ top,
+ bottom,
+ left,
+ right,
overflow,
maxWidth,
+ zIndex,
+ gridCols,
+ responsiveGridCols,
+ gap,
+ w,
+ h,
+ rounded,
+ border,
+ borderColor,
+ bg,
+ shadow,
+ flexShrink,
+ flexGrow,
+ hoverBorderColor,
+ transition,
...props
}: BoxProps & ComponentPropsWithoutRef,
ref: ForwardedRef
@@ -57,31 +117,83 @@ export const Box = forwardRef((
'auto': 'auto'
};
+ const getSpacingClass = (prefix: string, value: Spacing | 'auto' | ResponsiveSpacing | undefined) => {
+ if (value === undefined) return '';
+ if (typeof value === 'object') {
+ const classes = [];
+ if (value.base !== undefined) classes.push(`${prefix}-${spacingMap[value.base]}`);
+ if (value.md !== undefined) classes.push(`md:${prefix}-${spacingMap[value.md]}`);
+ if (value.lg !== undefined) classes.push(`lg:${prefix}-${spacingMap[value.lg]}`);
+ return classes.join(' ');
+ }
+ return `${prefix}-${spacingMap[value]}`;
+ };
+
+ const getResponsiveClasses = (prefix: string, value: string | ResponsiveValue | undefined) => {
+ if (value === undefined) return '';
+ if (typeof value === 'object') {
+ const classes = [];
+ if (value.base !== undefined) classes.push(`${prefix}-${value.base}`);
+ if (value.md !== undefined) classes.push(`md:${prefix}-${value.md}`);
+ if (value.lg !== undefined) classes.push(`lg:${prefix}-${value.lg}`);
+ return classes.join(' ');
+ }
+ return `${prefix}-${value}`;
+ };
+
const classes = [
center ? 'flex items-center justify-center' : '',
fullWidth ? 'w-full' : '',
fullHeight ? 'h-full' : '',
- m !== undefined ? `m-${spacingMap[m]}` : '',
- mt !== undefined ? `mt-${spacingMap[mt]}` : '',
- mb !== undefined ? `mb-${spacingMap[mb]}` : '',
- ml !== undefined ? `ml-${spacingMap[ml]}` : '',
- mr !== undefined ? `mr-${spacingMap[mr]}` : '',
- mx !== undefined ? `mx-${spacingMap[mx]}` : '',
- my !== undefined ? `my-${spacingMap[my]}` : '',
- p !== undefined ? `p-${spacingMap[p]}` : '',
- pt !== undefined ? `pt-${spacingMap[pt]}` : '',
- pb !== undefined ? `pb-${spacingMap[pb]}` : '',
- pl !== undefined ? `pl-${spacingMap[pl]}` : '',
- pr !== undefined ? `pr-${spacingMap[pr]}` : '',
- px !== undefined ? `px-${spacingMap[px]}` : '',
- py !== undefined ? `py-${spacingMap[py]}` : '',
+ getSpacingClass('m', m),
+ getSpacingClass('mt', mt),
+ getSpacingClass('mb', mb),
+ getSpacingClass('ml', ml),
+ getSpacingClass('mr', mr),
+ getSpacingClass('mx', mx),
+ getSpacingClass('my', my),
+ getSpacingClass('p', p),
+ getSpacingClass('pt', pt),
+ getSpacingClass('pb', pb),
+ getSpacingClass('pl', pl),
+ getSpacingClass('pr', pr),
+ getSpacingClass('px', px),
+ getSpacingClass('py', py),
+ getResponsiveClasses('w', w),
+ getResponsiveClasses('h', h),
+ rounded ? `rounded-${rounded}` : '',
+ border ? 'border' : '',
+ borderColor ? borderColor : '',
+ bg ? bg : '',
+ shadow ? shadow : '',
+ flexShrink !== undefined ? `flex-shrink-${flexShrink}` : '',
+ flexGrow !== undefined ? `flex-grow-${flexGrow}` : '',
+ hoverBorderColor ? `hover:${hoverBorderColor}` : '',
+ transition ? 'transition-all' : '',
display ? display : '',
+ gridCols ? `grid-cols-${gridCols}` : '',
+ responsiveGridCols?.base ? `grid-cols-${responsiveGridCols.base}` : '',
+ responsiveGridCols?.md ? `md:grid-cols-${responsiveGridCols.md}` : '',
+ responsiveGridCols?.lg ? `lg:grid-cols-${responsiveGridCols.lg}` : '',
+ gap !== undefined ? `gap-${spacingMap[gap]}` : '',
position ? position : '',
+ top !== undefined && spacingMap[top as any] ? `top-${spacingMap[top as any]}` : '',
+ bottom !== undefined && spacingMap[bottom as any] ? `bottom-${spacingMap[bottom as any]}` : '',
+ left !== undefined && spacingMap[left as any] ? `left-${spacingMap[left as any]}` : '',
+ right !== undefined && spacingMap[right as any] ? `right-${spacingMap[right as any]}` : '',
overflow ? `overflow-${overflow}` : '',
+ zIndex !== undefined ? `z-${zIndex}` : '',
className
].filter(Boolean).join(' ');
- const style = maxWidth ? { maxWidth, ...((props as Record).style as object || {}) } : (props as Record).style;
+ const style: React.CSSProperties = {
+ ...(maxWidth ? { maxWidth } : {}),
+ ...(top !== undefined && !spacingMap[top as any] ? { top } : {}),
+ ...(bottom !== undefined && !spacingMap[bottom as any] ? { bottom } : {}),
+ ...(left !== undefined && !spacingMap[left as any] ? { left } : {}),
+ ...(right !== undefined && !spacingMap[right as any] ? { right } : {}),
+ ...((props as Record).style as object || {})
+ };
return (
} className={classes} {...props} style={style as React.CSSProperties}>
diff --git a/apps/website/ui/Button.tsx b/apps/website/ui/Button.tsx
index 1cc2edfc8..253ae446a 100644
--- a/apps/website/ui/Button.tsx
+++ b/apps/website/ui/Button.tsx
@@ -5,7 +5,7 @@ interface ButtonProps extends ButtonHTMLAttributes {
children: ReactNode;
onClick?: MouseEventHandler;
className?: string;
- variant?: 'primary' | 'secondary' | 'danger' | 'ghost' | 'race-performance' | 'race-final';
+ variant?: 'primary' | 'secondary' | 'danger' | 'ghost' | 'race-performance' | 'race-final' | 'discord';
size?: 'sm' | 'md' | 'lg';
disabled?: boolean;
type?: 'button' | 'submit' | 'reset';
@@ -13,9 +13,11 @@ interface ButtonProps extends ButtonHTMLAttributes {
fullWidth?: boolean;
as?: 'button' | 'a';
href?: string;
+ target?: string;
+ rel?: string;
}
-export function Button({
+export const Button = React.forwardRef(({
children,
onClick,
className = '',
@@ -27,8 +29,10 @@ export function Button({
fullWidth = false,
as = 'button',
href,
+ target,
+ rel,
...props
-}: ButtonProps) {
+}, ref) => {
const baseClasses = 'inline-flex items-center rounded-lg transition-all duration-75 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 hover:scale-[1.02] active:scale-95';
const variantClasses = {
@@ -37,7 +41,8 @@ export function Button({
danger: 'bg-red-600 text-white hover:bg-red-700 focus-visible:outline-red-600',
ghost: 'bg-transparent text-gray-400 hover:bg-gray-800 focus-visible:outline-gray-400',
'race-performance': 'bg-gradient-to-r from-yellow-400 to-orange-500 text-white shadow-[0_0_15px_rgba(251,191,36,0.4)] hover:from-yellow-500 hover:to-orange-600 focus-visible:outline-yellow-400',
- 'race-final': 'bg-gradient-to-r from-purple-400 to-pink-500 text-white shadow-[0_0_15px_rgba(168,85,247,0.4)] hover:from-purple-500 hover:to-pink-600 focus-visible:outline-purple-400'
+ 'race-final': 'bg-gradient-to-r from-purple-400 to-pink-500 text-white shadow-[0_0_15px_rgba(168,85,247,0.4)] hover:from-purple-500 hover:to-pink-600 focus-visible:outline-purple-400',
+ discord: 'bg-[#5865F2] text-white hover:bg-[#4752C4] shadow-[0_0_20px_rgba(88,101,242,0.3)] hover:shadow-[0_0_30px_rgba(88,101,242,0.6)] focus-visible:outline-[#5865F2]'
};
const sizeClasses = {
@@ -69,6 +74,8 @@ export function Button({
return (
)}
>
@@ -79,6 +86,7 @@ export function Button({
return (
);
-}
+});
+
+Button.displayName = 'Button';
diff --git a/apps/website/ui/Card.tsx b/apps/website/ui/Card.tsx
index bf023b97f..4cf53a905 100644
--- a/apps/website/ui/Card.tsx
+++ b/apps/website/ui/Card.tsx
@@ -1,19 +1,20 @@
import React, { ReactNode, MouseEventHandler, HTMLAttributes } from 'react';
+import { Box, BoxProps } from './Box';
type Spacing = 0 | 0.5 | 1 | 1.5 | 2 | 2.5 | 3 | 3.5 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 14 | 16 | 20 | 24 | 28 | 32 | 36 | 40 | 44 | 48 | 52 | 56 | 60 | 64 | 72 | 80 | 96;
-interface CardProps extends HTMLAttributes {
+interface CardProps extends Omit, 'children' | 'className'> {
children: ReactNode;
className?: string;
onClick?: MouseEventHandler;
variant?: 'default' | 'highlight';
- p?: Spacing;
- px?: Spacing;
- py?: Spacing;
- pt?: Spacing;
- pb?: Spacing;
- pl?: Spacing;
- pr?: Spacing;
+ p?: Spacing | any;
+ px?: Spacing | any;
+ py?: Spacing | any;
+ pt?: Spacing | any;
+ pb?: Spacing | any;
+ pl?: Spacing | any;
+ pr?: Spacing | any;
}
export function Card({
@@ -53,8 +54,8 @@ export function Card({
].filter(Boolean).join(' ');
return (
-
+
{children}
-
+
);
}
diff --git a/apps/website/ui/DashboardLayoutWrapper.tsx b/apps/website/ui/DashboardLayoutWrapper.tsx
index 5b704acee..7874b0869 100644
--- a/apps/website/ui/DashboardLayoutWrapper.tsx
+++ b/apps/website/ui/DashboardLayoutWrapper.tsx
@@ -1,4 +1,6 @@
-import React, { ReactNode } from 'react';
+import { ReactNode } from 'react';
+
+// TODO very useless component
interface DashboardLayoutWrapperProps {
children: ReactNode;
diff --git a/apps/website/ui/Input.tsx b/apps/website/ui/Input.tsx
index 3135133ab..f0feab77e 100644
--- a/apps/website/ui/Input.tsx
+++ b/apps/website/ui/Input.tsx
@@ -5,16 +5,31 @@ import { Box } from './Box';
interface InputProps extends React.InputHTMLAttributes {
variant?: 'default' | 'error';
errorMessage?: string;
+ icon?: React.ReactNode;
}
export const Input = forwardRef(
- ({ className = '', variant = 'default', errorMessage, ...props }, ref) => {
+ ({ className = '', variant = 'default', errorMessage, icon, ...props }, ref) => {
const baseClasses = 'px-3 py-2 border rounded-lg text-white bg-deep-graphite focus:outline-none focus:border-primary-blue transition-colors w-full';
const variantClasses = (variant === 'error' || errorMessage) ? 'border-racing-red' : 'border-charcoal-outline';
- const classes = `${baseClasses} ${variantClasses} ${className}`;
+ const iconClasses = icon ? 'pl-10' : '';
+ const classes = `${baseClasses} ${variantClasses} ${iconClasses} ${className}`;
return (
-
+
+ {icon && (
+
+ {icon}
+
+ )}
{errorMessage && (
diff --git a/apps/website/ui/Stack.tsx b/apps/website/ui/Stack.tsx
index d12331edf..7b00f1997 100644
--- a/apps/website/ui/Stack.tsx
+++ b/apps/website/ui/Stack.tsx
@@ -1,9 +1,9 @@
import React, { ReactNode, HTMLAttributes } from 'react';
-import { Box } from './Box';
+import { Box, BoxProps } from './Box';
type Spacing = 0 | 0.5 | 1 | 1.5 | 2 | 2.5 | 3 | 3.5 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 14 | 16 | 20 | 24 | 28 | 32 | 36 | 40 | 44 | 48 | 52 | 56 | 60 | 64 | 72 | 80 | 96;
-interface StackProps extends HTMLAttributes {
+interface StackProps extends Omit, 'children' | 'className' | 'gap'> {
children: ReactNode;
className?: string;
direction?: 'row' | 'col';
@@ -12,19 +12,20 @@ interface StackProps extends HTMLAttributes {
justify?: 'start' | 'center' | 'end' | 'between' | 'around';
wrap?: boolean;
center?: boolean;
- m?: Spacing;
- mt?: Spacing;
- mb?: Spacing;
- ml?: Spacing;
- mr?: Spacing;
- p?: Spacing;
- pt?: Spacing;
- pb?: Spacing;
- pl?: Spacing;
- pr?: Spacing;
- px?: Spacing;
- py?: Spacing;
+ m?: Spacing | any;
+ mt?: Spacing | any;
+ mb?: Spacing | any;
+ ml?: Spacing | any;
+ mr?: Spacing | any;
+ p?: Spacing | any;
+ pt?: Spacing | any;
+ pb?: Spacing | any;
+ pl?: Spacing | any;
+ pr?: Spacing | any;
+ px?: Spacing | any;
+ py?: Spacing | any;
rounded?: 'none' | 'sm' | 'md' | 'lg' | 'xl' | '2xl' | 'full';
+ style?: React.CSSProperties;
}
export function Stack({
diff --git a/apps/website/ui/Surface.tsx b/apps/website/ui/Surface.tsx
index 5bdec188f..ce90467de 100644
--- a/apps/website/ui/Surface.tsx
+++ b/apps/website/ui/Surface.tsx
@@ -1,17 +1,20 @@
-import React, { ReactNode, HTMLAttributes } from 'react';
-import { Box } from './Box';
+import React, { ReactNode, ElementType, ComponentPropsWithoutRef } from 'react';
+import { Box, BoxProps } from './Box';
-interface SurfaceProps extends HTMLAttributes {
+interface SurfaceProps extends Omit, 'children' | 'className' | 'display'> {
+ as?: T;
children: ReactNode;
- variant?: 'default' | 'muted' | 'dark' | 'glass' | 'gradient-blue' | 'gradient-gold' | 'gradient-purple';
+ variant?: 'default' | 'muted' | 'dark' | 'glass' | 'gradient-blue' | 'gradient-gold' | 'gradient-purple' | 'gradient-green' | 'discord' | 'discord-inner';
rounded?: 'none' | 'sm' | 'md' | 'lg' | 'xl' | '2xl' | 'full';
border?: boolean;
padding?: number;
className?: string;
display?: 'block' | 'inline-block' | 'flex' | 'inline-flex' | 'grid' | 'none';
+ shadow?: 'none' | 'sm' | 'md' | 'lg' | 'xl' | 'discord' | string;
}
-export function Surface({
+export function Surface({
+ as,
children,
variant = 'default',
rounded = 'lg',
@@ -19,19 +22,32 @@ export function Surface({
padding = 0,
className = '',
display,
+ shadow = 'none',
...props
-}: SurfaceProps) {
- const variantClasses = {
+}: SurfaceProps & ComponentPropsWithoutRef) {
+ const variantClasses: Record = {
default: 'bg-iron-gray',
muted: 'bg-iron-gray/50',
dark: 'bg-deep-graphite',
glass: 'bg-deep-graphite/60 backdrop-blur-md',
'gradient-blue': 'bg-gradient-to-br from-primary-blue/20 via-iron-gray/80 to-deep-graphite',
'gradient-gold': 'bg-gradient-to-br from-yellow-600/20 via-iron-gray/80 to-deep-graphite',
- 'gradient-purple': 'bg-gradient-to-br from-purple-600/20 via-iron-gray/80 to-deep-graphite'
+ 'gradient-purple': 'bg-gradient-to-br from-purple-600/20 via-iron-gray/80 to-deep-graphite',
+ 'gradient-green': 'bg-gradient-to-br from-green-600/20 via-iron-gray/80 to-deep-graphite',
+ 'discord': 'bg-gradient-to-b from-deep-graphite to-iron-gray',
+ 'discord-inner': 'bg-gradient-to-br from-iron-gray via-deep-graphite to-iron-gray'
};
- const roundedClasses = {
+ const shadowClasses: Record = {
+ none: '',
+ sm: 'shadow-sm',
+ md: 'shadow-md',
+ lg: 'shadow-lg',
+ xl: 'shadow-xl',
+ discord: 'shadow-[0_0_80px_rgba(88,101,242,0.15)]'
+ };
+
+ const roundedClasses: Record = {
none: 'rounded-none',
sm: 'rounded-sm',
md: 'rounded-md',
@@ -58,6 +74,7 @@ export function Surface({
roundedClasses[rounded],
border ? 'border border-charcoal-outline' : '',
paddingClasses[padding] || 'p-0',
+ shadowClasses[shadow],
display ? display : '',
className
].filter(Boolean).join(' ');
diff --git a/apps/website/ui/Text.tsx b/apps/website/ui/Text.tsx
index a30f98931..6d993227b 100644
--- a/apps/website/ui/Text.tsx
+++ b/apps/website/ui/Text.tsx
@@ -1,25 +1,29 @@
-import React, { ReactNode, HTMLAttributes } from 'react';
+import React, { ReactNode, ElementType, ComponentPropsWithoutRef } from 'react';
+import { Box, BoxProps } from './Box';
type Spacing = 0 | 0.5 | 1 | 1.5 | 2 | 2.5 | 3 | 3.5 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 14 | 16 | 20 | 24 | 28 | 32 | 36 | 40 | 44 | 48 | 52 | 56 | 60 | 64 | 72 | 80 | 96;
-interface TextProps extends HTMLAttributes {
+interface TextProps extends Omit, 'children' | 'className'> {
+ as?: T;
children: ReactNode;
className?: string;
size?: 'xs' | 'sm' | 'base' | 'lg' | 'xl' | '2xl' | '3xl' | '4xl';
- weight?: 'normal' | 'medium' | 'semibold' | 'bold';
+ weight?: 'light' | 'normal' | 'medium' | 'semibold' | 'bold';
color?: string;
font?: 'mono' | 'sans';
align?: 'left' | 'center' | 'right';
truncate?: boolean;
+ leading?: 'none' | 'tight' | 'snug' | 'normal' | 'relaxed' | 'loose';
style?: React.CSSProperties;
block?: boolean;
- ml?: Spacing;
- mr?: Spacing;
- mt?: Spacing;
- mb?: Spacing;
+ ml?: Spacing | any;
+ mr?: Spacing | any;
+ mt?: Spacing | any;
+ mb?: Spacing | any;
}
-export function Text({
+export function Text({
+ as,
children,
className = '',
size = 'base',
@@ -28,12 +32,15 @@ export function Text({
font = 'sans',
align = 'left',
truncate = false,
+ leading,
style,
block = false,
ml, mr, mt, mb,
...props
-}: TextProps) {
- const sizeClasses = {
+}: TextProps & ComponentPropsWithoutRef) {
+ const Tag = (as as ElementType) || 'span';
+
+ const sizeClasses: Record = {
xs: 'text-xs',
sm: 'text-sm',
base: 'text-base',
@@ -44,7 +51,8 @@ export function Text({
'4xl': 'text-4xl'
};
- const weightClasses = {
+ const weightClasses: Record = {
+ light: 'font-light',
normal: 'font-normal',
medium: 'font-medium',
semibold: 'font-semibold',
@@ -69,12 +77,22 @@ export function Text({
48: '48', 52: '52', 56: '56', 60: '60', 64: '64', 72: '72', 80: '80', 96: '96'
};
+ const leadingClasses: Record = {
+ none: 'leading-none',
+ tight: 'leading-tight',
+ snug: 'leading-snug',
+ normal: 'leading-normal',
+ relaxed: 'leading-relaxed',
+ loose: 'leading-loose'
+ };
+
const classes = [
block ? 'block' : 'inline',
sizeClasses[size],
weightClasses[weight],
fontClasses[font],
alignClasses[align],
+ leading ? leadingClasses[leading] : '',
color,
truncate ? 'truncate' : '',
ml !== undefined ? `ml-${spacingMap[ml]}` : '',
@@ -84,5 +102,5 @@ export function Text({
className
].filter(Boolean).join(' ');
- return {children} ;
+ return {children} ;
}
diff --git a/apps/website/ui/icons/DiscordIcon.tsx b/apps/website/ui/icons/DiscordIcon.tsx
new file mode 100644
index 000000000..c8effbfaa
--- /dev/null
+++ b/apps/website/ui/icons/DiscordIcon.tsx
@@ -0,0 +1,21 @@
+import React from 'react';
+
+interface DiscordIconProps {
+ className?: string;
+ size?: number | string;
+ color?: string;
+}
+
+export function DiscordIcon({ className = '', size = 24, color }: DiscordIconProps) {
+ return (
+
+
+
+ );
+}