601 lines
27 KiB
TypeScript
601 lines
27 KiB
TypeScript
import { redirect } from 'next/navigation';
|
|
import Image from 'next/image';
|
|
import Link from 'next/link';
|
|
import {
|
|
Calendar,
|
|
Trophy,
|
|
Users,
|
|
Star,
|
|
Clock,
|
|
Flag,
|
|
TrendingUp,
|
|
ChevronRight,
|
|
Zap,
|
|
Target,
|
|
Award,
|
|
Activity,
|
|
Play,
|
|
Bell,
|
|
Medal,
|
|
Crown,
|
|
Heart,
|
|
MessageCircle,
|
|
UserPlus,
|
|
} from 'lucide-react';
|
|
|
|
import Card from '@/components/ui/Card';
|
|
import Button from '@/components/ui/Button';
|
|
import { getAuthService } from '@/lib/auth';
|
|
import {
|
|
getFeedRepository,
|
|
getRaceRepository,
|
|
getResultRepository,
|
|
getDriverRepository,
|
|
getLeagueRepository,
|
|
getStandingRepository,
|
|
getSocialRepository,
|
|
getDriverStats,
|
|
getImageService,
|
|
getLeagueMembershipRepository,
|
|
} from '@/lib/di-container';
|
|
import type { FeedItem } from '@gridpilot/social/domain/entities/FeedItem';
|
|
import type { Race } from '@gridpilot/racing/domain/entities/Race';
|
|
import type { Driver } from '@gridpilot/racing/domain/entities/Driver';
|
|
|
|
export const dynamic = 'force-dynamic';
|
|
|
|
// Helper functions
|
|
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 '🏁';
|
|
}
|
|
|
|
function timeUntil(date: Date): string {
|
|
const now = new Date();
|
|
const diffMs = date.getTime() - now.getTime();
|
|
|
|
if (diffMs < 0) return 'Started';
|
|
|
|
const diffHours = Math.floor(diffMs / (1000 * 60 * 60));
|
|
const diffDays = Math.floor(diffHours / 24);
|
|
|
|
if (diffDays > 0) {
|
|
return `${diffDays}d ${diffHours % 24}h`;
|
|
}
|
|
if (diffHours > 0) {
|
|
const diffMinutes = Math.floor((diffMs % (1000 * 60 * 60)) / (1000 * 60));
|
|
return `${diffHours}h ${diffMinutes}m`;
|
|
}
|
|
const diffMinutes = Math.floor(diffMs / (1000 * 60));
|
|
return `${diffMinutes}m`;
|
|
}
|
|
|
|
function timeAgo(timestamp: Date): string {
|
|
const diffMs = Date.now() - timestamp.getTime();
|
|
const diffMinutes = Math.floor(diffMs / 60000);
|
|
if (diffMinutes < 1) return 'Just now';
|
|
if (diffMinutes < 60) return `${diffMinutes}m ago`;
|
|
const diffHours = Math.floor(diffMinutes / 60);
|
|
if (diffHours < 24) return `${diffHours}h ago`;
|
|
const diffDays = Math.floor(diffHours / 24);
|
|
return `${diffDays}d ago`;
|
|
}
|
|
|
|
function getGreeting(): string {
|
|
const hour = new Date().getHours();
|
|
if (hour < 12) return 'Good morning';
|
|
if (hour < 18) return 'Good afternoon';
|
|
return 'Good evening';
|
|
}
|
|
|
|
export default async function DashboardPage() {
|
|
const authService = getAuthService();
|
|
const session = await authService.getCurrentSession();
|
|
|
|
if (!session) {
|
|
redirect('/auth/iracing?returnTo=/dashboard');
|
|
}
|
|
|
|
const feedRepository = getFeedRepository();
|
|
const raceRepository = getRaceRepository();
|
|
const resultRepository = getResultRepository();
|
|
const driverRepository = getDriverRepository();
|
|
const leagueRepository = getLeagueRepository();
|
|
const standingRepository = getStandingRepository();
|
|
const socialRepository = getSocialRepository();
|
|
const leagueMembershipRepository = getLeagueMembershipRepository();
|
|
const imageService = getImageService();
|
|
|
|
const currentDriverId = session.user.primaryDriverId ?? '';
|
|
const currentDriver = await driverRepository.findById(currentDriverId);
|
|
|
|
const [feedItems, allRaces, allResults, allLeagues, friends] = await Promise.all([
|
|
feedRepository.getFeedForDriver(currentDriverId),
|
|
raceRepository.findAll(),
|
|
resultRepository.findAll(),
|
|
leagueRepository.findAll(),
|
|
socialRepository.getFriends(currentDriverId),
|
|
]);
|
|
|
|
// Get driver's leagues by checking membership in each league
|
|
const driverLeagues: typeof allLeagues = [];
|
|
for (const league of allLeagues) {
|
|
const membership = await leagueMembershipRepository.getMembership(league.id, currentDriverId);
|
|
if (membership && membership.status === 'active') {
|
|
driverLeagues.push(league);
|
|
}
|
|
}
|
|
const driverLeagueIds = driverLeagues.map(l => l.id);
|
|
|
|
// Upcoming races (prioritize driver's leagues)
|
|
const upcomingRaces = allRaces
|
|
.filter((race) => race.status === 'scheduled')
|
|
.sort((a, b) => a.scheduledAt.getTime() - b.scheduledAt.getTime());
|
|
|
|
const myUpcomingRaces = upcomingRaces.filter(r => driverLeagueIds.includes(r.leagueId));
|
|
const otherUpcomingRaces = upcomingRaces.filter(r => !driverLeagueIds.includes(r.leagueId));
|
|
const nextRace = myUpcomingRaces[0] || otherUpcomingRaces[0];
|
|
|
|
// Recent results for driver
|
|
const driverResults = allResults.filter(r => r.driverId === currentDriverId);
|
|
const recentResults = driverResults.slice(0, 5);
|
|
|
|
// Get stats
|
|
const driverStats = getDriverStats(currentDriverId);
|
|
|
|
// Get standings for driver's leagues
|
|
const leagueStandings = await Promise.all(
|
|
driverLeagues.slice(0, 3).map(async (league) => {
|
|
const standings = await standingRepository.findByLeagueId(league.id);
|
|
const driverStanding = standings.find(s => s.driverId === currentDriverId);
|
|
return {
|
|
league,
|
|
position: driverStanding?.position ?? 0,
|
|
points: driverStanding?.points ?? 0,
|
|
totalDrivers: standings.length,
|
|
};
|
|
})
|
|
);
|
|
|
|
// Calculate quick stats
|
|
const totalRaces = driverStats?.totalRaces ?? 0;
|
|
const wins = driverStats?.wins ?? 0;
|
|
const podiums = driverStats?.podiums ?? 0;
|
|
const rating = driverStats?.rating ?? 1500;
|
|
const globalRank = driverStats?.overallRank ?? 0;
|
|
|
|
return (
|
|
<main className="min-h-screen bg-deep-graphite">
|
|
{/* Hero Section */}
|
|
<section className="relative overflow-hidden">
|
|
{/* Background Pattern */}
|
|
<div className="absolute inset-0 bg-gradient-to-br from-primary-blue/10 via-deep-graphite to-purple-600/5" />
|
|
<div className="absolute inset-0 opacity-5">
|
|
<div className="absolute inset-0" style={{
|
|
backgroundImage: `url("data:image/svg+xml,%3Csvg width='60' height='60' viewBox='0 0 60 60' xmlns='http://www.w3.org/2000/svg'%3E%3Cg fill='none' fill-rule='evenodd'%3E%3Cg fill='%23ffffff' fill-opacity='0.4'%3E%3Cpath d='M36 34v-4h-2v4h-4v2h4v4h2v-4h4v-2h-4zm0-30V0h-2v4h-4v2h4v4h2V6h4V4h-4zM6 34v-4H4v4H0v2h4v4h2v-4h4v-2H6zM6 4V0H4v4H0v2h4v4h2V6h4V4H6z'/%3E%3C/g%3E%3C/g%3E%3C/svg%3E")`,
|
|
}} />
|
|
</div>
|
|
|
|
<div className="relative max-w-7xl mx-auto px-6 py-10">
|
|
<div className="flex flex-col lg:flex-row lg:items-center lg:justify-between gap-8">
|
|
{/* Welcome Message */}
|
|
<div className="flex items-start gap-5">
|
|
{currentDriver && (
|
|
<div className="relative">
|
|
<div className="w-20 h-20 rounded-2xl bg-gradient-to-br from-primary-blue to-purple-600 p-0.5 shadow-xl shadow-primary-blue/20">
|
|
<div className="w-full h-full rounded-xl overflow-hidden bg-iron-gray">
|
|
<Image
|
|
src={imageService.getDriverAvatar(currentDriverId)}
|
|
alt={currentDriver.name}
|
|
width={80}
|
|
height={80}
|
|
className="w-full h-full object-cover"
|
|
/>
|
|
</div>
|
|
</div>
|
|
<div className="absolute -bottom-1 -right-1 w-5 h-5 rounded-full bg-performance-green border-3 border-deep-graphite" />
|
|
</div>
|
|
)}
|
|
<div>
|
|
<p className="text-gray-400 text-sm mb-1">{getGreeting()},</p>
|
|
<h1 className="text-3xl md:text-4xl font-bold text-white mb-2">
|
|
{currentDriver?.name ?? 'Racer'}
|
|
<span className="ml-3 text-2xl">{currentDriver ? getCountryFlag(currentDriver.country) : '🏁'}</span>
|
|
</h1>
|
|
<div className="flex flex-wrap items-center gap-3">
|
|
<div className="flex items-center gap-1.5 px-3 py-1 rounded-full bg-primary-blue/10 border border-primary-blue/30">
|
|
<Star className="w-3.5 h-3.5 text-primary-blue" />
|
|
<span className="text-sm font-semibold text-primary-blue">{rating}</span>
|
|
</div>
|
|
<div className="flex items-center gap-1.5 px-3 py-1 rounded-full bg-yellow-400/10 border border-yellow-400/30">
|
|
<Trophy className="w-3.5 h-3.5 text-yellow-400" />
|
|
<span className="text-sm font-semibold text-yellow-400">#{globalRank}</span>
|
|
</div>
|
|
<span className="text-xs text-gray-500">{totalRaces} races completed</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Quick Actions */}
|
|
<div className="flex flex-wrap gap-3">
|
|
<Link href="/leagues">
|
|
<Button variant="secondary" className="flex items-center gap-2">
|
|
<Flag className="w-4 h-4" />
|
|
Browse Leagues
|
|
</Button>
|
|
</Link>
|
|
<Link href="/profile">
|
|
<Button variant="primary" className="flex items-center gap-2">
|
|
<Activity className="w-4 h-4" />
|
|
View Profile
|
|
</Button>
|
|
</Link>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Quick Stats Row */}
|
|
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 mt-8">
|
|
<div className="p-4 rounded-xl bg-iron-gray/50 border border-charcoal-outline backdrop-blur-sm">
|
|
<div className="flex items-center gap-3">
|
|
<div className="flex h-10 w-10 items-center justify-center rounded-lg bg-performance-green/20">
|
|
<Trophy className="w-5 h-5 text-performance-green" />
|
|
</div>
|
|
<div>
|
|
<p className="text-2xl font-bold text-white">{wins}</p>
|
|
<p className="text-xs text-gray-500">Wins</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div className="p-4 rounded-xl bg-iron-gray/50 border border-charcoal-outline backdrop-blur-sm">
|
|
<div className="flex items-center gap-3">
|
|
<div className="flex h-10 w-10 items-center justify-center rounded-lg bg-warning-amber/20">
|
|
<Medal className="w-5 h-5 text-warning-amber" />
|
|
</div>
|
|
<div>
|
|
<p className="text-2xl font-bold text-white">{podiums}</p>
|
|
<p className="text-xs text-gray-500">Podiums</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div className="p-4 rounded-xl bg-iron-gray/50 border border-charcoal-outline backdrop-blur-sm">
|
|
<div className="flex items-center gap-3">
|
|
<div className="flex h-10 w-10 items-center justify-center rounded-lg bg-primary-blue/20">
|
|
<Target className="w-5 h-5 text-primary-blue" />
|
|
</div>
|
|
<div>
|
|
<p className="text-2xl font-bold text-white">{driverStats?.consistency ?? 0}%</p>
|
|
<p className="text-xs text-gray-500">Consistency</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div className="p-4 rounded-xl bg-iron-gray/50 border border-charcoal-outline backdrop-blur-sm">
|
|
<div className="flex items-center gap-3">
|
|
<div className="flex h-10 w-10 items-center justify-center rounded-lg bg-purple-500/20">
|
|
<Users className="w-5 h-5 text-purple-400" />
|
|
</div>
|
|
<div>
|
|
<p className="text-2xl font-bold text-white">{driverLeagues.length}</p>
|
|
<p className="text-xs text-gray-500">Active Leagues</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
{/* Main Content */}
|
|
<section className="max-w-7xl mx-auto px-6 py-8">
|
|
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
|
{/* Left Column - Main Content */}
|
|
<div className="lg:col-span-2 space-y-6">
|
|
{/* Next Race Card */}
|
|
{nextRace && (
|
|
<Card className="relative overflow-hidden bg-gradient-to-br from-iron-gray to-iron-gray/80 border-primary-blue/30">
|
|
<div className="absolute top-0 right-0 w-40 h-40 bg-gradient-to-bl from-primary-blue/20 to-transparent rounded-bl-full" />
|
|
<div className="relative">
|
|
<div className="flex items-center gap-2 mb-4">
|
|
<div className="flex items-center gap-2 px-3 py-1 rounded-full bg-primary-blue/20 border border-primary-blue/30">
|
|
<Play className="w-3.5 h-3.5 text-primary-blue" />
|
|
<span className="text-xs font-semibold text-primary-blue uppercase tracking-wider">Next Race</span>
|
|
</div>
|
|
{myUpcomingRaces.includes(nextRace) && (
|
|
<span className="px-2 py-0.5 rounded-full bg-performance-green/20 text-performance-green text-xs font-medium">
|
|
Your League
|
|
</span>
|
|
)}
|
|
</div>
|
|
|
|
<div className="flex flex-col md:flex-row md:items-end md:justify-between gap-4">
|
|
<div>
|
|
<h2 className="text-2xl font-bold text-white mb-2">{nextRace.track}</h2>
|
|
<p className="text-gray-400 mb-3">{nextRace.car}</p>
|
|
<div className="flex flex-wrap items-center gap-4 text-sm">
|
|
<span className="flex items-center gap-1.5 text-gray-400">
|
|
<Calendar className="w-4 h-4" />
|
|
{nextRace.scheduledAt.toLocaleDateString('en-US', {
|
|
weekday: 'long',
|
|
month: 'short',
|
|
day: 'numeric',
|
|
})}
|
|
</span>
|
|
<span className="flex items-center gap-1.5 text-gray-400">
|
|
<Clock className="w-4 h-4" />
|
|
{nextRace.scheduledAt.toLocaleTimeString('en-US', {
|
|
hour: '2-digit',
|
|
minute: '2-digit',
|
|
})}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex flex-col items-end gap-3">
|
|
<div className="text-right">
|
|
<p className="text-xs text-gray-500 uppercase tracking-wider mb-1">Starts in</p>
|
|
<p className="text-3xl font-bold text-primary-blue font-mono">{timeUntil(nextRace.scheduledAt)}</p>
|
|
</div>
|
|
<Link href={`/races/${nextRace.id}`}>
|
|
<Button variant="primary" className="flex items-center gap-2">
|
|
View Details
|
|
<ChevronRight className="w-4 h-4" />
|
|
</Button>
|
|
</Link>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</Card>
|
|
)}
|
|
|
|
{/* League Standings Preview */}
|
|
{leagueStandings.length > 0 && (
|
|
<Card>
|
|
<div className="flex items-center justify-between mb-4">
|
|
<h2 className="text-lg font-semibold text-white flex items-center gap-2">
|
|
<Award className="w-5 h-5 text-yellow-400" />
|
|
Your Championship Standings
|
|
</h2>
|
|
<Link href="/profile/leagues" className="text-sm text-primary-blue hover:underline flex items-center gap-1">
|
|
View all <ChevronRight className="w-4 h-4" />
|
|
</Link>
|
|
</div>
|
|
<div className="space-y-3">
|
|
{leagueStandings.map(({ league, position, points, totalDrivers }) => (
|
|
<Link
|
|
key={league.id}
|
|
href={`/leagues/${league.id}/standings`}
|
|
className="flex items-center gap-4 p-4 rounded-xl bg-deep-graphite border border-charcoal-outline hover:border-primary-blue/30 transition-colors group"
|
|
>
|
|
<div className={`flex h-12 w-12 items-center justify-center rounded-xl font-bold text-xl ${
|
|
position === 1 ? 'bg-yellow-400/20 text-yellow-400' :
|
|
position === 2 ? 'bg-gray-300/20 text-gray-300' :
|
|
position === 3 ? 'bg-orange-400/20 text-orange-400' :
|
|
'bg-iron-gray text-gray-400'
|
|
}`}>
|
|
{position > 0 ? `P${position}` : '-'}
|
|
</div>
|
|
<div className="flex-1 min-w-0">
|
|
<p className="text-white font-semibold truncate group-hover:text-primary-blue transition-colors">
|
|
{league.name}
|
|
</p>
|
|
<p className="text-sm text-gray-500">
|
|
{points} points • {totalDrivers} drivers
|
|
</p>
|
|
</div>
|
|
<div className="flex items-center gap-2">
|
|
{position <= 3 && position > 0 && (
|
|
<Crown className={`w-5 h-5 ${
|
|
position === 1 ? 'text-yellow-400' :
|
|
position === 2 ? 'text-gray-300' :
|
|
'text-orange-400'
|
|
}`} />
|
|
)}
|
|
<ChevronRight className="w-5 h-5 text-gray-500 group-hover:text-primary-blue transition-colors" />
|
|
</div>
|
|
</Link>
|
|
))}
|
|
</div>
|
|
</Card>
|
|
)}
|
|
|
|
{/* Activity Feed */}
|
|
<Card>
|
|
<div className="flex items-center justify-between mb-4">
|
|
<h2 className="text-lg font-semibold text-white flex items-center gap-2">
|
|
<Activity className="w-5 h-5 text-neon-aqua" />
|
|
Recent Activity
|
|
</h2>
|
|
</div>
|
|
{feedItems.length > 0 ? (
|
|
<div className="space-y-4">
|
|
{feedItems.slice(0, 5).map((item) => (
|
|
<FeedItemRow key={item.id} item={item} imageService={imageService} />
|
|
))}
|
|
</div>
|
|
) : (
|
|
<div className="text-center py-8">
|
|
<Activity className="w-12 h-12 text-gray-600 mx-auto mb-3" />
|
|
<p className="text-gray-400 mb-2">No activity yet</p>
|
|
<p className="text-sm text-gray-500">Join leagues and add friends to see activity here</p>
|
|
</div>
|
|
)}
|
|
</Card>
|
|
</div>
|
|
|
|
{/* Right Column - Sidebar */}
|
|
<div className="space-y-6">
|
|
{/* Upcoming Races */}
|
|
<Card>
|
|
<div className="flex items-center justify-between mb-4">
|
|
<h3 className="text-lg font-semibold text-white flex items-center gap-2">
|
|
<Calendar className="w-5 h-5 text-red-400" />
|
|
Upcoming Races
|
|
</h3>
|
|
<Link href="/races" className="text-xs text-primary-blue hover:underline">
|
|
View all
|
|
</Link>
|
|
</div>
|
|
{upcomingRaces.length > 0 ? (
|
|
<div className="space-y-3">
|
|
{upcomingRaces.slice(0, 5).map((race) => {
|
|
const isMyRace = driverLeagueIds.includes(race.leagueId);
|
|
return (
|
|
<Link
|
|
key={race.id}
|
|
href={`/races/${race.id}`}
|
|
className="block p-3 rounded-lg bg-deep-graphite border border-charcoal-outline hover:border-primary-blue/30 transition-colors"
|
|
>
|
|
<div className="flex items-start justify-between gap-2 mb-2">
|
|
<p className="text-white font-medium text-sm truncate">{race.track}</p>
|
|
{isMyRace && (
|
|
<span className="flex-shrink-0 w-2 h-2 rounded-full bg-performance-green" title="Your league" />
|
|
)}
|
|
</div>
|
|
<p className="text-xs text-gray-500 truncate mb-2">{race.car}</p>
|
|
<div className="flex items-center justify-between text-xs">
|
|
<span className="text-gray-400">
|
|
{race.scheduledAt.toLocaleDateString('en-US', { month: 'short', day: 'numeric' })}
|
|
</span>
|
|
<span className="text-primary-blue font-medium">{timeUntil(race.scheduledAt)}</span>
|
|
</div>
|
|
</Link>
|
|
);
|
|
})}
|
|
</div>
|
|
) : (
|
|
<p className="text-gray-500 text-sm text-center py-4">No upcoming races</p>
|
|
)}
|
|
</Card>
|
|
|
|
{/* Friends */}
|
|
<Card>
|
|
<div className="flex items-center justify-between mb-4">
|
|
<h3 className="text-lg font-semibold text-white flex items-center gap-2">
|
|
<Users className="w-5 h-5 text-purple-400" />
|
|
Friends
|
|
</h3>
|
|
<span className="text-xs text-gray-500">{friends.length} friends</span>
|
|
</div>
|
|
{friends.length > 0 ? (
|
|
<div className="space-y-2">
|
|
{friends.slice(0, 6).map((friend) => (
|
|
<Link
|
|
key={friend.id}
|
|
href={`/drivers/${friend.id}`}
|
|
className="flex items-center gap-3 p-2 rounded-lg hover:bg-deep-graphite transition-colors"
|
|
>
|
|
<div className="w-9 h-9 rounded-full overflow-hidden bg-gradient-to-br from-primary-blue to-purple-600">
|
|
<Image
|
|
src={imageService.getDriverAvatar(friend.id)}
|
|
alt={friend.name}
|
|
width={36}
|
|
height={36}
|
|
className="w-full h-full object-cover"
|
|
/>
|
|
</div>
|
|
<div className="flex-1 min-w-0">
|
|
<p className="text-white text-sm font-medium truncate">{friend.name}</p>
|
|
<p className="text-xs text-gray-500">{getCountryFlag(friend.country)}</p>
|
|
</div>
|
|
</Link>
|
|
))}
|
|
{friends.length > 6 && (
|
|
<Link
|
|
href="/profile"
|
|
className="block text-center py-2 text-sm text-primary-blue hover:underline"
|
|
>
|
|
+{friends.length - 6} more
|
|
</Link>
|
|
)}
|
|
</div>
|
|
) : (
|
|
<div className="text-center py-6">
|
|
<UserPlus className="w-10 h-10 text-gray-600 mx-auto mb-2" />
|
|
<p className="text-sm text-gray-400 mb-2">No friends yet</p>
|
|
<Link href="/drivers">
|
|
<Button variant="secondary" className="text-xs">
|
|
Find Drivers
|
|
</Button>
|
|
</Link>
|
|
</div>
|
|
)}
|
|
</Card>
|
|
|
|
{/* Quick Links */}
|
|
<Card className="bg-gradient-to-br from-iron-gray to-iron-gray/80">
|
|
<h3 className="text-lg font-semibold text-white mb-4 flex items-center gap-2">
|
|
<Zap className="w-5 h-5 text-warning-amber" />
|
|
Quick Links
|
|
</h3>
|
|
<div className="space-y-2">
|
|
<Link
|
|
href="/leaderboards"
|
|
className="flex items-center gap-3 p-3 rounded-lg bg-deep-graphite/50 border border-charcoal-outline hover:border-yellow-400/30 transition-colors"
|
|
>
|
|
<Trophy className="w-5 h-5 text-yellow-400" />
|
|
<span className="text-white text-sm">Leaderboards</span>
|
|
<ChevronRight className="w-4 h-4 text-gray-500 ml-auto" />
|
|
</Link>
|
|
<Link
|
|
href="/teams"
|
|
className="flex items-center gap-3 p-3 rounded-lg bg-deep-graphite/50 border border-charcoal-outline hover:border-purple-400/30 transition-colors"
|
|
>
|
|
<Users className="w-5 h-5 text-purple-400" />
|
|
<span className="text-white text-sm">Teams</span>
|
|
<ChevronRight className="w-4 h-4 text-gray-500 ml-auto" />
|
|
</Link>
|
|
<Link
|
|
href="/leagues/create"
|
|
className="flex items-center gap-3 p-3 rounded-lg bg-deep-graphite/50 border border-charcoal-outline hover:border-primary-blue/30 transition-colors"
|
|
>
|
|
<Flag className="w-5 h-5 text-primary-blue" />
|
|
<span className="text-white text-sm">Create League</span>
|
|
<ChevronRight className="w-4 h-4 text-gray-500 ml-auto" />
|
|
</Link>
|
|
</div>
|
|
</Card>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
</main>
|
|
);
|
|
}
|
|
|
|
// Feed Item Row Component
|
|
function FeedItemRow({ item, imageService }: { item: FeedItem; imageService: any }) {
|
|
const getActivityIcon = (type: string) => {
|
|
if (type.includes('win')) return { icon: Trophy, color: 'text-yellow-400 bg-yellow-400/10' };
|
|
if (type.includes('podium')) return { icon: Medal, color: 'text-warning-amber bg-warning-amber/10' };
|
|
if (type.includes('join')) return { icon: UserPlus, color: 'text-performance-green bg-performance-green/10' };
|
|
if (type.includes('friend')) return { icon: Heart, color: 'text-pink-400 bg-pink-400/10' };
|
|
if (type.includes('league')) return { icon: Flag, color: 'text-primary-blue bg-primary-blue/10' };
|
|
if (type.includes('race')) return { icon: Play, color: 'text-red-400 bg-red-400/10' };
|
|
return { icon: Activity, color: 'text-gray-400 bg-gray-400/10' };
|
|
};
|
|
|
|
const { icon: Icon, color } = getActivityIcon(item.type);
|
|
|
|
return (
|
|
<div className="flex gap-3 p-3 rounded-lg bg-deep-graphite/50 border border-charcoal-outline">
|
|
<div className={`flex h-10 w-10 items-center justify-center rounded-lg ${color} flex-shrink-0`}>
|
|
<Icon className="w-5 h-5" />
|
|
</div>
|
|
<div className="flex-1 min-w-0">
|
|
<p className="text-sm text-white">{item.headline}</p>
|
|
{item.body && (
|
|
<p className="text-xs text-gray-500 mt-1 line-clamp-2">{item.body}</p>
|
|
)}
|
|
<p className="text-xs text-gray-500 mt-1">{timeAgo(item.timestamp)}</p>
|
|
</div>
|
|
{item.ctaHref && (
|
|
<Link href={item.ctaHref} className="flex-shrink-0">
|
|
<Button variant="secondary" className="text-xs px-3 py-1.5">
|
|
{item.ctaLabel || 'View'}
|
|
</Button>
|
|
</Link>
|
|
)}
|
|
</div>
|
|
);
|
|
} |