extract components from website

This commit is contained in:
2025-12-21 13:55:31 +01:00
parent 13d8563feb
commit b52474d792
65 changed files with 3234 additions and 1361 deletions

View File

@@ -0,0 +1,53 @@
import { Trophy, Sparkles, Search } from 'lucide-react';
import Heading from '@/components/ui/Heading';
import Button from '@/components/ui/Button';
import Card from '@/components/ui/Card';
interface EmptyStateProps {
title: string;
description: string;
icon?: React.ElementType;
actionIcon?: React.ElementType;
actionLabel?: string;
onAction?: () => void;
children?: React.ReactNode;
className?: string;
}
export function EmptyState({
title,
description,
icon: Icon = Trophy,
actionIcon: ActionIcon = Sparkles,
actionLabel,
onAction,
children,
className,
}: EmptyStateProps) {
return (
<Card className={`text-center py-16 ${className || ''}`}>
<div className="max-w-md mx-auto">
<div className="flex h-16 w-16 mx-auto items-center justify-center rounded-2xl bg-primary-blue/10 border border-primary-blue/20 mb-6">
<Icon className="w-8 h-8 text-primary-blue" />
</div>
<Heading level={2} className="text-2xl mb-3">
{title}
</Heading>
<p className="text-gray-400 mb-8">
{description}
</p>
{children}
{actionLabel && onAction && (
<Button
variant="primary"
onClick={onAction}
className="flex items-center gap-2 mx-auto"
>
<ActionIcon className="w-4 h-4" />
{actionLabel}
</Button>
)}
</div>
</Card>
);
}

View File

@@ -0,0 +1,85 @@
import { Trophy, Plus } from 'lucide-react';
import Heading from '@/components/ui/Heading';
import Button from '@/components/ui/Button';
interface StatItem {
value: number;
label: string;
color: string;
animate?: boolean;
}
interface HeroSectionProps {
icon?: React.ElementType;
title: string;
description: string;
stats?: StatItem[];
ctaLabel?: string;
ctaDescription?: string;
onCtaClick?: () => void;
className?: string;
}
export function HeroSection({
icon: Icon = Trophy,
title,
description,
stats = [],
ctaLabel = "Create League",
ctaDescription = "Set up your own racing series",
onCtaClick,
className,
}: HeroSectionProps) {
return (
<div className={`relative mb-10 py-10 px-8 rounded-2xl bg-gradient-to-br from-iron-gray/80 via-deep-graphite to-iron-gray/60 border border-charcoal-outline/50 overflow-hidden ${className || ''}`}>
{/* Background decoration */}
<div className="absolute top-0 right-0 w-96 h-96 bg-primary-blue/5 rounded-full blur-3xl" />
<div className="absolute bottom-0 left-0 w-64 h-64 bg-neon-aqua/5 rounded-full blur-3xl" />
<div className="relative z-10 flex flex-col lg:flex-row lg:items-center lg:justify-between gap-8">
<div className="max-w-2xl">
<div className="flex items-center gap-3 mb-4">
<div className="flex h-12 w-12 items-center justify-center rounded-xl bg-gradient-to-br from-primary-blue/20 to-primary-blue/5 border border-primary-blue/20">
<Icon className="w-6 h-6 text-primary-blue" />
</div>
<Heading level={1} className="text-3xl lg:text-4xl">
{title}
</Heading>
</div>
<p className="text-gray-400 text-lg leading-relaxed mb-6">
{description}
</p>
{/* Stats */}
{stats.length > 0 && (
<div className="flex flex-wrap gap-6">
{stats.map((stat, index) => (
<div key={index} className="flex items-center gap-2">
<div className={`w-2 h-2 rounded-full ${stat.color} ${stat.animate ? 'animate-pulse' : ''}`} />
<span className="text-sm text-gray-400">
<span className="text-white font-semibold">{stat.value}</span> {stat.label}
</span>
</div>
))}
</div>
)}
</div>
{/* CTA */}
{onCtaClick && (
<div className="flex flex-col gap-4">
<Button
variant="primary"
onClick={onCtaClick}
className="flex items-center gap-2 px-6 py-3"
>
<Plus className="w-5 h-5" />
<span>{ctaLabel}</span>
</Button>
<p className="text-xs text-gray-500 text-center">{ctaDescription}</p>
</div>
)}
</div>
</div>
);
}

View File

@@ -0,0 +1,59 @@
import React from 'react';
import Card from '@/components/ui/Card';
import { StandingEntryViewModel } from '@/lib/view-models/StandingEntryViewModel';
import { DriverViewModel } from '@/lib/view-models';
interface LeagueChampionshipStatsProps {
standings: StandingEntryViewModel[];
drivers: DriverViewModel[];
}
export default function LeagueChampionshipStats({ standings, drivers }: LeagueChampionshipStatsProps) {
if (standings.length === 0) return null;
const leader = standings[0];
const totalRaces = Math.max(...standings.map(s => s.races), 0);
return (
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<Card>
<div className="flex items-center gap-3">
<div className="w-12 h-12 rounded-full bg-yellow-500/10 flex items-center justify-center">
<span className="text-2xl">🏆</span>
</div>
<div>
<p className="text-xs text-gray-400 mb-1">Championship Leader</p>
<p className="font-bold text-white">{drivers.find(d => d.id === leader?.driverId)?.name || 'N/A'}</p>
<p className="text-sm text-yellow-400 font-medium">{leader?.points || 0} points</p>
</div>
</div>
</Card>
<Card>
<div className="flex items-center gap-3">
<div className="w-12 h-12 rounded-full bg-primary-blue/10 flex items-center justify-center">
<span className="text-2xl">🏁</span>
</div>
<div>
<p className="text-xs text-gray-400 mb-1">Races Completed</p>
<p className="text-2xl font-bold text-white">{totalRaces}</p>
<p className="text-sm text-gray-400">Season in progress</p>
</div>
</div>
</Card>
<Card>
<div className="flex items-center gap-3">
<div className="w-12 h-12 rounded-full bg-performance-green/10 flex items-center justify-center">
<span className="text-2xl">👥</span>
</div>
<div>
<p className="text-xs text-gray-400 mb-1">Active Drivers</p>
<p className="text-2xl font-bold text-white">{standings.length}</p>
<p className="text-sm text-gray-400">Competing for points</p>
</div>
</div>
</Card>
</div>
);
}

View File

@@ -0,0 +1,114 @@
import React, { useState } from 'react';
import DriverSummaryPill from '@/components/profile/DriverSummaryPill';
import Button from '@/components/ui/Button';
import { UserCog } from 'lucide-react';
import { LeagueSettingsViewModel } from '@/lib/view-models/LeagueSettingsViewModel';
interface LeagueOwnershipTransferProps {
settings: LeagueSettingsViewModel;
currentDriverId: string;
onTransferOwnership: (newOwnerId: string) => Promise<void>;
}
export default function LeagueOwnershipTransfer({
settings,
currentDriverId,
onTransferOwnership
}: LeagueOwnershipTransferProps) {
const [showTransferDialog, setShowTransferDialog] = useState(false);
const [selectedNewOwner, setSelectedNewOwner] = useState<string>('');
const [transferring, setTransferring] = useState(false);
const handleTransferOwnership = async () => {
if (!selectedNewOwner) return;
setTransferring(true);
try {
await onTransferOwnership(selectedNewOwner);
setShowTransferDialog(false);
setSelectedNewOwner('');
} catch (err) {
console.error('Failed to transfer ownership:', err);
alert(err instanceof Error ? err.message : 'Failed to transfer ownership');
} finally {
setTransferring(false);
}
};
const ownerSummary = settings.owner;
return (
<div className="space-y-4">
{/* League Owner */}
<div className="rounded-xl border border-charcoal-outline bg-gradient-to-br from-iron-gray/40 to-iron-gray/20 p-5">
<h3 className="text-sm font-semibold text-gray-400 mb-3">League Owner</h3>
{ownerSummary ? (
<DriverSummaryPill
driver={ownerSummary.driver}
rating={ownerSummary.rating}
rank={ownerSummary.rank}
/>
) : (
<p className="text-sm text-gray-500">Loading owner details...</p>
)}
</div>
{/* Transfer Ownership - Owner Only */}
{settings.league.ownerId === currentDriverId && settings.members.length > 0 && (
<div className="rounded-xl border border-charcoal-outline bg-gradient-to-br from-iron-gray/40 to-iron-gray/20 p-5">
<div className="flex items-center gap-2 mb-3">
<UserCog className="w-4 h-4 text-gray-400" />
<h3 className="text-sm font-semibold text-gray-400">Transfer Ownership</h3>
</div>
<p className="text-xs text-gray-500 mb-4">
Transfer league ownership to another active member. You will become an admin.
</p>
{!showTransferDialog ? (
<Button
variant="secondary"
onClick={() => setShowTransferDialog(true)}
>
Transfer Ownership
</Button>
) : (
<div className="space-y-3">
<select
value={selectedNewOwner}
onChange={(e) => setSelectedNewOwner(e.target.value)}
className="w-full rounded-lg border border-charcoal-outline bg-charcoal-card px-3 py-2 text-sm text-white focus:border-primary-blue focus:outline-none"
>
<option value="">Select new owner...</option>
{settings.members.map((member) => (
<option key={member.driver.id} value={member.driver.id}>
{member.driver.name}
</option>
))}
</select>
<div className="flex gap-2">
<Button
variant="primary"
onClick={handleTransferOwnership}
disabled={!selectedNewOwner || transferring}
>
{transferring ? 'Transferring...' : 'Confirm Transfer'}
</Button>
<Button
variant="secondary"
onClick={() => {
setShowTransferDialog(false);
setSelectedNewOwner('');
}}
disabled={transferring}
>
Cancel
</Button>
</div>
</div>
)}
</div>
)}
</div>
);
}

View File

@@ -0,0 +1,204 @@
import React, { useState, useEffect, useRef, useCallback } from 'react';
import { ChevronLeft, ChevronRight } from 'lucide-react';
import type { LeagueSummaryViewModel } from '@/lib/view-models/LeagueSummaryViewModel';
import LeagueCard from '@/components/leagues/LeagueCard';
interface LeagueSliderProps {
title: string;
icon: React.ElementType;
description: string;
leagues: LeagueSummaryViewModel[];
onLeagueClick: (id: string) => void;
autoScroll?: boolean;
iconColor?: string;
scrollSpeedMultiplier?: number;
scrollDirection?: 'left' | 'right';
}
export const LeagueSlider = ({
title,
icon: Icon,
description,
leagues,
onLeagueClick,
autoScroll = true,
iconColor = 'text-primary-blue',
scrollSpeedMultiplier = 1,
scrollDirection = 'right',
}: LeagueSliderProps) => {
const scrollRef = useRef<HTMLDivElement>(null);
const [canScrollLeft, setCanScrollLeft] = useState(false);
const [canScrollRight, setCanScrollRight] = useState(true);
const [isHovering, setIsHovering] = useState(false);
const animationRef = useRef<number | null>(null);
const scrollPositionRef = useRef(0);
const checkScrollButtons = useCallback(() => {
if (scrollRef.current) {
const { scrollLeft, scrollWidth, clientWidth } = scrollRef.current;
setCanScrollLeft(scrollLeft > 0);
setCanScrollRight(scrollLeft < scrollWidth - clientWidth - 10);
}
}, []);
const scroll = useCallback((direction: 'left' | 'right') => {
if (scrollRef.current) {
const cardWidth = 340;
const scrollAmount = direction === 'left' ? -cardWidth : cardWidth;
// Update the ref so auto-scroll continues from new position
scrollPositionRef.current = scrollRef.current.scrollLeft + scrollAmount;
scrollRef.current.scrollBy({ left: scrollAmount, behavior: 'smooth' });
}
}, []);
// Initialize scroll position for left-scrolling sliders
useEffect(() => {
if (scrollDirection === 'left' && scrollRef.current) {
const { scrollWidth, clientWidth } = scrollRef.current;
scrollPositionRef.current = scrollWidth - clientWidth;
scrollRef.current.scrollLeft = scrollPositionRef.current;
}
}, [scrollDirection, leagues.length]);
// Smooth continuous auto-scroll using requestAnimationFrame with variable speed and direction
useEffect(() => {
// Allow scroll even with just 2 leagues (minimum threshold = 1)
if (!autoScroll || leagues.length <= 1) return;
const scrollContainer = scrollRef.current;
if (!scrollContainer) return;
let lastTimestamp = 0;
// Base speed with multiplier for variation between sliders
const baseSpeed = 0.025;
const scrollSpeed = baseSpeed * scrollSpeedMultiplier;
const directionMultiplier = scrollDirection === 'left' ? -1 : 1;
const animate = (timestamp: number) => {
if (!isHovering && scrollContainer) {
const delta = lastTimestamp ? timestamp - lastTimestamp : 0;
lastTimestamp = timestamp;
scrollPositionRef.current += scrollSpeed * delta * directionMultiplier;
const { scrollWidth, clientWidth } = scrollContainer;
const maxScroll = scrollWidth - clientWidth;
// Handle wrap-around for both directions
if (scrollDirection === 'right' && scrollPositionRef.current >= maxScroll) {
scrollPositionRef.current = 0;
} else if (scrollDirection === 'left' && scrollPositionRef.current <= 0) {
scrollPositionRef.current = maxScroll;
}
scrollContainer.scrollLeft = scrollPositionRef.current;
} else {
lastTimestamp = timestamp;
}
animationRef.current = requestAnimationFrame(animate);
};
animationRef.current = requestAnimationFrame(animate);
return () => {
if (animationRef.current) {
cancelAnimationFrame(animationRef.current);
}
};
}, [autoScroll, leagues.length, isHovering, scrollSpeedMultiplier, scrollDirection]);
// Sync scroll position when user manually scrolls
useEffect(() => {
const scrollContainer = scrollRef.current;
if (!scrollContainer) return;
const handleScroll = () => {
scrollPositionRef.current = scrollContainer.scrollLeft;
checkScrollButtons();
};
scrollContainer.addEventListener('scroll', handleScroll);
return () => scrollContainer.removeEventListener('scroll', handleScroll);
}, [checkScrollButtons]);
if (leagues.length === 0) return null;
return (
<div className="mb-10">
{/* Section header */}
<div className="flex items-center justify-between mb-4">
<div className="flex items-center gap-3">
<div className={`flex h-10 w-10 items-center justify-center rounded-xl bg-iron-gray border border-charcoal-outline`}>
<Icon className={`w-5 h-5 ${iconColor}`} />
</div>
<div>
<h2 className="text-lg font-semibold text-white">{title}</h2>
<p className="text-xs text-gray-500">{description}</p>
</div>
<span className="ml-2 px-2 py-0.5 rounded-full text-xs bg-charcoal-outline/50 text-gray-400">
{leagues.length}
</span>
</div>
{/* Navigation arrows */}
<div className="flex items-center gap-2">
<button
type="button"
onClick={() => scroll('left')}
disabled={!canScrollLeft}
className={`flex h-8 w-8 items-center justify-center rounded-lg transition-all ${
canScrollLeft
? 'bg-iron-gray border border-charcoal-outline text-white hover:border-primary-blue hover:text-primary-blue'
: 'bg-iron-gray/30 border border-charcoal-outline/30 text-gray-600 cursor-not-allowed'
}`}
>
<ChevronLeft className="w-4 h-4" />
</button>
<button
type="button"
onClick={() => scroll('right')}
disabled={!canScrollRight}
className={`flex h-8 w-8 items-center justify-center rounded-lg transition-all ${
canScrollRight
? 'bg-iron-gray border border-charcoal-outline text-white hover:border-primary-blue hover:text-primary-blue'
: 'bg-iron-gray/30 border border-charcoal-outline/30 text-gray-600 cursor-not-allowed'
}`}
>
<ChevronRight className="w-4 h-4" />
</button>
</div>
</div>
{/* Scrollable container with fade edges */}
<div className="relative">
{/* Left fade gradient */}
<div className="absolute left-0 top-0 bottom-4 w-12 bg-gradient-to-r from-deep-graphite to-transparent z-10 pointer-events-none" />
{/* Right fade gradient */}
<div className="absolute right-0 top-0 bottom-4 w-12 bg-gradient-to-l from-deep-graphite to-transparent z-10 pointer-events-none" />
<div
ref={scrollRef}
onMouseEnter={() => setIsHovering(true)}
onMouseLeave={() => setIsHovering(false)}
className="flex gap-4 overflow-x-auto pb-4 px-4"
style={{
scrollbarWidth: 'none',
msOverflowStyle: 'none',
}}
>
<style jsx>{`
div::-webkit-scrollbar {
display: none;
}
`}</style>
{leagues.map((league) => (
<div key={league.id} className="flex-shrink-0 w-[320px] h-full">
<LeagueCard league={league} onClick={() => onLeagueClick(league.id)} />
</div>
))}
</div>
</div>
</div>
);
};

View File

@@ -0,0 +1,45 @@
import { Search } from 'lucide-react';
import Button from '@/components/ui/Button';
import Card from '@/components/ui/Card';
interface NoResultsStateProps {
icon?: React.ElementType;
message?: string;
searchQuery?: string;
actionLabel?: string;
onAction?: () => void;
children?: React.ReactNode;
className?: string;
}
export function NoResultsState({
icon: Icon = Search,
message,
searchQuery,
actionLabel = "Clear filters",
onAction,
children,
className
}: NoResultsStateProps) {
const defaultMessage = message || `No leagues found${searchQuery ? ` matching "${searchQuery}"` : ' in this category'}`;
return (
<Card className={`text-center py-12 ${className || ''}`}>
<div className="flex flex-col items-center gap-4">
<Icon className="w-10 h-10 text-gray-600" />
<p className="text-gray-400">
{defaultMessage}
</p>
{children}
{actionLabel && onAction && (
<Button
variant="secondary"
onClick={onAction}
>
{actionLabel}
</Button>
)}
</div>
</Card>
);
}

View File

@@ -0,0 +1,55 @@
import React from 'react';
import Card from '@/components/ui/Card';
interface PointsTableProps {
title?: string;
points: { position: number; points: number }[];
}
export default function PointsTable({ title = 'Points Distribution', points }: PointsTableProps) {
return (
<Card>
<h2 className="text-lg font-semibold text-white mb-4">{title}</h2>
<div className="overflow-x-auto">
<table className="w-full text-sm">
<thead>
<tr className="border-b border-charcoal-outline">
<th className="text-left py-3 px-4 font-medium text-gray-400">Position</th>
<th className="text-right py-3 px-4 font-medium text-gray-400">Points</th>
</tr>
</thead>
<tbody>
{points.map(({ position, points: pts }) => (
<tr
key={position}
className={`border-b border-charcoal-outline/50 transition-colors hover:bg-iron-gray/30 ${
position <= 3 ? 'bg-iron-gray/20' : ''
}`}
>
<td className="py-3 px-4">
<div className="flex items-center gap-3">
<div className={`w-7 h-7 rounded-full flex items-center justify-center text-xs font-bold ${
position === 1 ? 'bg-yellow-500 text-black' :
position === 2 ? 'bg-gray-400 text-black' :
position === 3 ? 'bg-amber-600 text-white' :
'bg-charcoal-outline text-white'
}`}>
{position}
</div>
<span className="text-white font-medium">
{position === 1 ? '1st' : position === 2 ? '2nd' : position === 3 ? '3rd' : `${position}th`}
</span>
</div>
</td>
<td className="py-3 px-4 text-right">
<span className="text-white font-semibold tabular-nums">{pts}</span>
<span className="text-gray-500 ml-1">pts</span>
</td>
</tr>
))}
</tbody>
</table>
</div>
</Card>
);
}

View File

@@ -0,0 +1,95 @@
import { useState } from 'react';
import { Search, Filter } from 'lucide-react';
import Input from '@/components/ui/Input';
import Button from '@/components/ui/Button';
interface Category {
id: string;
label: string;
icon: React.ElementType;
description: string;
color?: string;
}
interface SearchAndFilterBarProps {
searchQuery: string;
onSearchChange: (query: string) => void;
activeCategory: string;
onCategoryChange: (category: string) => void;
categories: Category[];
leaguesByCategory: Record<string, any[]>;
className?: string;
}
export function SearchAndFilterBar({
searchQuery,
onSearchChange,
activeCategory,
onCategoryChange,
categories,
leaguesByCategory,
className,
}: SearchAndFilterBarProps) {
const [showFilters, setShowFilters] = useState(false);
return (
<div className={`mb-6 ${className || ''}`}>
<div className="flex flex-col lg:flex-row gap-4">
{/* Search */}
<div className="flex-1 relative">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-5 h-5 text-gray-500" />
<Input
type="text"
placeholder="Search leagues by name, description, or game..."
value={searchQuery}
onChange={(e) => onSearchChange(e.target.value)}
className="pl-11"
/>
</div>
{/* Filter toggle (mobile) */}
<Button
type="button"
variant="secondary"
onClick={() => setShowFilters(!showFilters)}
className="lg:hidden flex items-center gap-2"
>
<Filter className="w-4 h-4" />
Filters
</Button>
</div>
{/* Category Tabs */}
<div className={`mt-4 ${showFilters ? 'block' : 'hidden lg:block'}`}>
<div className="flex flex-wrap gap-2">
{categories.map((category) => {
const Icon = category.icon;
const count = leaguesByCategory[category.id]?.length || 0;
const isActive = activeCategory === category.id;
return (
<button
key={category.id}
type="button"
onClick={() => onCategoryChange(category.id)}
className={`flex items-center gap-1.5 px-3 py-1.5 rounded-full text-xs font-medium transition-all duration-200 ${
isActive
? 'bg-primary-blue text-white shadow-[0_0_15px_rgba(25,140,255,0.3)]'
: 'bg-iron-gray/60 text-gray-400 border border-charcoal-outline hover:border-gray-500 hover:text-white'
}`}
>
<Icon className={`w-3.5 h-3.5 ${!isActive && category.color ? category.color : ''}`} />
<span>{category.label}</span>
{count > 0 && (
<span className={`px-1.5 py-0.5 rounded-full text-[10px] ${isActive ? 'bg-white/20' : 'bg-charcoal-outline/50'}`}>
{count}
</span>
)}
</button>
);
})}
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,36 @@
import React from 'react';
import { CheckCircle, Clock, Gavel } from 'lucide-react';
interface StewardingStatsProps {
totalPending: number;
totalResolved: number;
totalPenalties: number;
}
export default function StewardingStats({ totalPending, totalResolved, totalPenalties }: StewardingStatsProps) {
return (
<div className="grid grid-cols-3 gap-4 mb-6">
<div className="rounded-lg bg-iron-gray/50 border border-charcoal-outline p-4">
<div className="flex items-center gap-2 text-warning-amber mb-1">
<Clock className="w-4 h-4" />
<span className="text-xs font-medium uppercase">Pending Review</span>
</div>
<div className="text-2xl font-bold text-white">{totalPending}</div>
</div>
<div className="rounded-lg bg-iron-gray/50 border border-charcoal-outline p-4">
<div className="flex items-center gap-2 text-performance-green mb-1">
<CheckCircle className="w-4 h-4" />
<span className="text-xs font-medium uppercase">Resolved</span>
</div>
<div className="text-2xl font-bold text-white">{totalResolved}</div>
</div>
<div className="rounded-lg bg-iron-gray/50 border border-charcoal-outline p-4">
<div className="flex items-center gap-2 text-red-400 mb-1">
<Gavel className="w-4 h-4" />
<span className="text-xs font-medium uppercase">Penalties</span>
</div>
<div className="text-2xl font-bold text-white">{totalPenalties}</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,91 @@
import React from 'react';
import {
ArrowDownLeft,
ArrowUpRight,
CheckCircle,
Clock,
CreditCard,
DollarSign,
TrendingUp,
XCircle
} from 'lucide-react';
interface Transaction {
id: string;
amount: number;
type: 'sponsorship' | 'membership' | 'withdrawal' | 'prize';
status: 'completed' | 'pending' | 'failed';
description: string;
reference?: string;
formattedDate: string;
formattedAmount: string;
fee: number;
}
interface TransactionRowProps {
transaction: Transaction;
}
export default function TransactionRow({ transaction }: TransactionRowProps) {
const isIncoming = transaction.amount > 0;
const typeIcons = {
sponsorship: DollarSign,
membership: CreditCard,
withdrawal: ArrowUpRight,
prize: TrendingUp,
};
const TypeIcon = typeIcons[transaction.type];
const statusConfig = {
completed: { color: 'text-performance-green', bg: 'bg-performance-green/10', icon: CheckCircle },
pending: { color: 'text-warning-amber', bg: 'bg-warning-amber/10', icon: Clock },
failed: { color: 'text-racing-red', bg: 'bg-racing-red/10', icon: XCircle },
};
const status = statusConfig[transaction.status];
const StatusIcon = status.icon;
return (
<div className="flex items-center justify-between p-4 border-b border-charcoal-outline last:border-b-0 hover:bg-iron-gray/30 transition-colors">
<div className="flex items-center gap-4">
<div className={`flex h-10 w-10 items-center justify-center rounded-lg ${isIncoming ? 'bg-performance-green/10' : 'bg-iron-gray/50'}`}>
{isIncoming ? (
<ArrowDownLeft className="w-5 h-5 text-performance-green" />
) : (
<ArrowUpRight className="w-5 h-5 text-gray-400" />
)}
</div>
<div>
<div className="flex items-center gap-2">
<span className="font-medium text-white">{transaction.description}</span>
<span className={`px-2 py-0.5 rounded text-xs ${status.bg} ${status.color}`}>
{transaction.status}
</span>
</div>
<div className="flex items-center gap-2 text-xs text-gray-500 mt-1">
<TypeIcon className="w-3 h-3" />
<span className="capitalize">{transaction.type}</span>
{transaction.reference && (
<>
<span></span>
<span>{transaction.reference}</span>
</>
)}
<span></span>
<span>{transaction.formattedDate}</span>
</div>
</div>
</div>
<div className="text-right">
<div className={`font-semibold ${isIncoming ? 'text-performance-green' : 'text-white'}`}>
{transaction.formattedAmount}
</div>
{transaction.fee > 0 && (
<div className="text-xs text-gray-500">
Fee: ${transaction.fee.toFixed(2)}
</div>
)}
</div>
</div>
);
}