This commit is contained in:
2025-12-04 11:54:23 +01:00
parent c0fdae3d3c
commit 9d5caa87f3
83 changed files with 1579 additions and 2151 deletions

View File

@@ -0,0 +1,25 @@
import Card from '@/components/ui/Card';
import Button from '@/components/ui/Button';
export default function FeedEmptyState() {
return (
<Card className="bg-iron-gray/80 border-dashed border-charcoal-outline text-center py-10">
<div className="text-3xl mb-3">🏁</div>
<h3 className="text-lg font-semibold text-white mb-2">
Your feed is warming up
</h3>
<p className="text-sm text-gray-400 mb-4 max-w-md mx-auto">
As leagues, teams, and friends start racing, this feed will show their latest results,
signups, and highlights.
</p>
<Button
as="a"
href="/leagues"
variant="secondary"
className="text-xs px-4 py-2"
>
Explore leagues
</Button>
</Card>
);
}

View File

@@ -0,0 +1,83 @@
import Card from '@/components/ui/Card';
import Button from '@/components/ui/Button';
import type { FeedItem } from '@gridpilot/social/domain/entities/FeedItem';
import { friends } from '@gridpilot/testing-support';
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} min 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 getActor(item: FeedItem) {
if (item.actorFriendId) {
const friend = friends.find(f => f.driverId === item.actorFriendId);
if (friend) {
return {
name: friend.displayName,
avatarUrl: friend.avatarUrl
};
}
}
return null;
}
interface FeedItemCardProps {
item: FeedItem;
}
export default function FeedItemCard({ item }: FeedItemCardProps) {
const actor = getActor(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">
<img
src={actor.avatarUrl}
alt={actor.name}
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>
);
}

View File

@@ -0,0 +1,43 @@
import Card from '@/components/ui/Card';
import type { FeedItem } from '@gridpilot/social/domain/entities/FeedItem';
import type { Race } from '@gridpilot/racing/domain/entities/Race';
import type { RaceWithResultsDTO } from '@gridpilot/testing-support';
import FeedList from '@/components/feed/FeedList';
import UpcomingRacesSidebar from '@/components/races/UpcomingRacesSidebar';
import LatestResultsSidebar from '@/components/races/LatestResultsSidebar';
interface FeedLayoutProps {
feedItems: FeedItem[];
upcomingRaces: Race[];
latestResults: RaceWithResultsDTO[];
}
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

@@ -0,0 +1,21 @@
import FeedEmptyState from '@/components/feed/FeedEmptyState';
import FeedItemCard from '@/components/feed/FeedItemCard';
import type { FeedItem } from '@gridpilot/social/domain/entities/FeedItem';
interface FeedListProps {
items: FeedItem[];
}
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>
);
}