website refactor

This commit is contained in:
2026-01-15 17:12:24 +01:00
parent c3b308e960
commit f035cfe7ce
468 changed files with 24378 additions and 17324 deletions

View File

@@ -1,34 +0,0 @@
import { Card } from '@/ui/Card';
import { Button } from '@/ui/Button';
import { Box } from '@/ui/Box';
import { Text } from '@/ui/Text';
import { Heading } from '@/ui/Heading';
export function FeedEmptyState() {
return (
<Card className="bg-iron-gray/80 border-dashed border-charcoal-outline">
<Box textAlign="center" py={10}>
<Text size="3xl" block mb={3}>🏁</Text>
<Box mb={2}>
<Heading level={3}>
Your feed is warming up
</Heading>
</Box>
<Box maxWidth="md" mx="auto" mb={4}>
<Text size="sm" color="text-gray-400">
As leagues, teams, and friends start racing, this feed will show their latest results,
signups, and highlights.
</Text>
</Box>
<Button
as="a"
href="/leagues"
variant="secondary"
size="sm"
>
Explore leagues
</Button>
</Box>
</Card>
);
}

View File

@@ -1,7 +1,8 @@
import { useEffect, useState } from 'react';
import Card from '@/ui/Card';
import Button from '@/ui/Button';
import Image from 'next/image';
'use client';
import React, { useEffect, useState } from 'react';
import { Button } from '@/ui/Button';
import { FeedItem } from '@/ui/FeedItem';
interface FeedItemData {
id: string;
@@ -26,9 +27,7 @@ function timeAgo(timestamp: Date | string): string {
return `${diffDays} d ago`;
}
async function resolveActor(_item: FeedItemData) {
// Actor resolution is not wired through the API in this build.
// Keep rendering deterministic and decoupled (no core repos).
async function resolveActor() {
return null;
}
@@ -36,14 +35,14 @@ interface FeedItemCardProps {
item: FeedItemData;
}
export default function FeedItemCard({ item }: FeedItemCardProps) {
export function FeedItemCard({ item }: FeedItemCardProps) {
const [actor, setActor] = useState<{ name: string; avatarUrl: string } | null>(null);
useEffect(() => {
let cancelled = false;
void (async () => {
const resolved = await resolveActor(item);
const resolved = await resolveActor();
if (!cancelled) {
setActor(resolved);
}
@@ -55,51 +54,25 @@ export default function FeedItemCard({ item }: FeedItemCardProps) {
}, [item]);
return (
<div className="flex gap-4">
<div className="flex-shrink-0">
{actor ? (
<div className="w-10 h-10 rounded-full overflow-hidden bg-charcoal-outline">
<Image
src={actor.avatarUrl}
alt={actor.name}
width={40}
height={40}
className="w-full h-full object-cover"
/>
</div>
) : (
<Card className="w-10 h-10 flex items-center justify-center rounded-full bg-primary-blue/10 border-primary-blue/40 p-0">
<span className="text-xs text-primary-blue font-semibold">
{item.type.startsWith('friend') ? 'FR' : 'LG'}
</span>
</Card>
)}
</div>
<div className="flex-1 min-w-0">
<div className="flex items-start justify-between gap-2">
<div>
<p className="text-sm text-white">{item.headline}</p>
{item.body && (
<p className="text-xs text-gray-400 mt-1">{item.body}</p>
)}
</div>
<span className="text-[11px] text-gray-500 whitespace-nowrap">
{timeAgo(item.timestamp)}
</span>
</div>
{item.ctaHref && item.ctaLabel && (
<div className="mt-3">
<Button
as="a"
href={item.ctaHref}
variant="secondary"
className="text-xs px-4 py-2"
>
{item.ctaLabel}
</Button>
</div>
)}
</div>
</div>
<FeedItem
actorName={actor?.name}
actorAvatarUrl={actor?.avatarUrl}
typeLabel={item.type.startsWith('friend') ? 'FR' : 'LG'}
headline={item.headline}
body={item.body}
timeAgo={timeAgo(item.timestamp)}
cta={item.ctaHref && item.ctaLabel ? (
<Button
as="a"
href={item.ctaHref}
variant="secondary"
size="sm"
px={4}
py={2}
>
{item.ctaLabel}
</Button>
) : undefined}
/>
);
}
}

View File

@@ -1,66 +0,0 @@
import Card from '@/ui/Card';
import FeedList from '@/components/feed/FeedList';
import UpcomingRacesSidebar from '@/components/races/UpcomingRacesSidebar';
import LatestResultsSidebar from '@/components/races/LatestResultsSidebar';
interface FeedItemData {
id: string;
type: string;
headline: string;
body?: string;
timestamp: string;
formattedTime: string;
ctaHref?: string;
ctaLabel?: string;
}
type FeedUpcomingRace = {
id: string;
track: string;
car: string;
scheduledAt: string | Date;
};
type FeedLatestResult = {
raceId: string;
track: string;
car: string;
winnerName: string;
scheduledAt: string | Date;
};
interface FeedLayoutProps {
feedItems: FeedItemData[];
upcomingRaces: FeedUpcomingRace[];
latestResults: FeedLatestResult[];
}
export default function FeedLayout({
feedItems,
upcomingRaces,
latestResults
}: FeedLayoutProps) {
return (
<section className="max-w-7xl mx-auto mt-16 mb-20">
<div className="flex flex-col gap-8 lg:grid lg:grid-cols-3">
<div className="lg:col-span-2 space-y-4">
<div className="flex items-baseline justify-between gap-4">
<div>
<h2 className="text-2xl font-semibold text-white">Activity</h2>
<p className="text-sm text-gray-400">
See what your friends and leagues are doing right now.
</p>
</div>
</div>
<Card className="bg-iron-gray/80">
<FeedList items={feedItems} />
</Card>
</div>
<aside className="space-y-6">
<UpcomingRacesSidebar races={upcomingRaces} />
<LatestResultsSidebar results={latestResults} />
</aside>
</div>
</section>
);
}

View File

@@ -1,31 +0,0 @@
import FeedEmptyState from '@/components/feed/FeedEmptyState';
import FeedItemCard from '@/components/feed/FeedItemCard';
interface FeedItemData {
id: string;
type: string;
headline: string;
body?: string;
timestamp: string;
formattedTime: string;
ctaHref?: string;
ctaLabel?: string;
}
interface FeedListProps {
items: FeedItemData[];
}
export default function FeedList({ items }: FeedListProps) {
if (!items.length) {
return <FeedEmptyState />;
}
return (
<div className="space-y-4">
{items.map(item => (
<FeedItemCard key={item.id} item={item} />
))}
</div>
);
}