extract components from website
This commit is contained in:
@@ -23,58 +23,15 @@ import {
|
||||
|
||||
import Card from '@/components/ui/Card';
|
||||
import Button from '@/components/ui/Button';
|
||||
import { StatCard } from '@/components/dashboard/StatCard';
|
||||
import { LeagueStandingItem } from '@/components/dashboard/LeagueStandingItem';
|
||||
import { UpcomingRaceItem } from '@/components/dashboard/UpcomingRaceItem';
|
||||
import { FriendItem } from '@/components/dashboard/FriendItem';
|
||||
import { FeedItemRow } from '@/components/dashboard/FeedItemRow';
|
||||
|
||||
import { useDashboardOverview } from '@/hooks/useDashboardService';
|
||||
|
||||
|
||||
// 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): string {
|
||||
const time = typeof timestamp === 'string' ? new Date(timestamp) : timestamp;
|
||||
const diffMs = Date.now() - time.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';
|
||||
}
|
||||
import { getCountryFlag } from '@/lib/utilities/country';
|
||||
import { getGreeting, timeUntil, timeAgo } from '@/lib/utilities/time';
|
||||
|
||||
import { DashboardFeedItemSummaryViewModel } from '@/lib/view-models/DashboardOverviewViewModel';
|
||||
|
||||
@@ -176,50 +133,10 @@ export default function DashboardPage() {
|
||||
|
||||
{/* 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">{consistency}%</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">{activeLeaguesCount}</p>
|
||||
<p className="text-xs text-gray-500">Active Leagues</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<StatCard icon={Trophy} value={wins} label="Wins" color="bg-performance-green/20 text-performance-green" />
|
||||
<StatCard icon={Medal} value={podiums} label="Podiums" color="bg-warning-amber/20 text-warning-amber" />
|
||||
<StatCard icon={Target} value={`${consistency}%`} label="Consistency" color="bg-primary-blue/20 text-primary-blue" />
|
||||
<StatCard icon={Users} value={activeLeaguesCount} label="Active Leagues" color="bg-purple-500/20 text-purple-400" />
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
@@ -300,38 +217,14 @@ export default function DashboardPage() {
|
||||
</div>
|
||||
<div className="space-y-3">
|
||||
{leagueStandingsSummaries.map(({ leagueId, leagueName, position, points, totalDrivers }) => (
|
||||
<Link
|
||||
<LeagueStandingItem
|
||||
key={leagueId}
|
||||
href={`/leagues/${leagueId}/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">
|
||||
{leagueName}
|
||||
</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>
|
||||
leagueId={leagueId}
|
||||
leagueName={leagueName}
|
||||
position={position}
|
||||
points={points}
|
||||
totalDrivers={totalDrivers}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</Card>
|
||||
@@ -376,30 +269,16 @@ export default function DashboardPage() {
|
||||
</div>
|
||||
{upcomingRaces.length > 0 ? (
|
||||
<div className="space-y-3">
|
||||
{upcomingRaces.slice(0, 5).map((race) => {
|
||||
const isMyRace = race.isMyLeague;
|
||||
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>
|
||||
);
|
||||
})}
|
||||
{upcomingRaces.slice(0, 5).map((race) => (
|
||||
<UpcomingRaceItem
|
||||
key={race.id}
|
||||
id={race.id}
|
||||
track={race.track}
|
||||
car={race.car}
|
||||
scheduledAt={race.scheduledAt}
|
||||
isMyLeague={race.isMyLeague}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-gray-500 text-sm text-center py-4">No upcoming races</p>
|
||||
@@ -418,25 +297,13 @@ export default function DashboardPage() {
|
||||
{friends.length > 0 ? (
|
||||
<div className="space-y-2">
|
||||
{friends.slice(0, 6).map((friend) => (
|
||||
<Link
|
||||
<FriendItem
|
||||
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={friend.avatarUrl}
|
||||
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>
|
||||
id={friend.id}
|
||||
name={friend.name}
|
||||
avatarUrl={friend.avatarUrl}
|
||||
country={friend.country}
|
||||
/>
|
||||
))}
|
||||
{friends.length > 6 && (
|
||||
<Link
|
||||
@@ -465,40 +332,3 @@ export default function DashboardPage() {
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
// Feed Item Row Component
|
||||
function FeedItemRow({ item }: { item: DashboardFeedItemSummaryViewModel }) {
|
||||
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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user