website refactor
This commit is contained in:
52
apps/website/components/feed/ActivityFeed.tsx
Normal file
52
apps/website/components/feed/ActivityFeed.tsx
Normal file
@@ -0,0 +1,52 @@
|
||||
import React from 'react';
|
||||
import { Activity } from 'lucide-react';
|
||||
import { Card } from '@/ui/Card';
|
||||
import { Heading } from '@/ui/Heading';
|
||||
import { ActivityItem } from '@/ui/ActivityItem';
|
||||
import { Icon } from '@/ui/Icon';
|
||||
import { ActivityFeedList } from '@/components/feed/ActivityFeedList';
|
||||
import { MinimalEmptyState } from '@/components/shared/state/EmptyState';
|
||||
|
||||
interface FeedItem {
|
||||
id: string;
|
||||
headline: string;
|
||||
body?: string;
|
||||
formattedTime: string;
|
||||
ctaHref?: string;
|
||||
ctaLabel?: string;
|
||||
}
|
||||
|
||||
interface ActivityFeedProps {
|
||||
items: FeedItem[];
|
||||
hasItems: boolean;
|
||||
}
|
||||
|
||||
export function ActivityFeed({ items, hasItems }: ActivityFeedProps) {
|
||||
return (
|
||||
<Card>
|
||||
<Heading level={2} icon={<Icon icon={Activity} size={5} color="var(--primary-blue)" />} mb={4}>
|
||||
Recent Activity
|
||||
</Heading>
|
||||
{hasItems ? (
|
||||
<ActivityFeedList>
|
||||
{items.slice(0, 5).map((item) => (
|
||||
<ActivityItem
|
||||
key={item.id}
|
||||
headline={item.headline}
|
||||
body={item.body}
|
||||
formattedTime={item.formattedTime}
|
||||
ctaHref={item.ctaHref}
|
||||
ctaLabel={item.ctaLabel}
|
||||
/>
|
||||
))}
|
||||
</ActivityFeedList>
|
||||
) : (
|
||||
<MinimalEmptyState
|
||||
icon={Activity}
|
||||
title="No activity yet"
|
||||
description="Join leagues and add friends to see activity here"
|
||||
/>
|
||||
)}
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
50
apps/website/components/feed/ActivityFeedItem.tsx
Normal file
50
apps/website/components/feed/ActivityFeedItem.tsx
Normal file
@@ -0,0 +1,50 @@
|
||||
|
||||
|
||||
import { ReactNode } from 'react';
|
||||
import { Box } from '@/ui/Box';
|
||||
import { Surface } from '@/ui/Surface';
|
||||
import { Text } from '@/ui/Text';
|
||||
|
||||
interface ActivityFeedItemProps {
|
||||
icon: ReactNode;
|
||||
content: ReactNode;
|
||||
timestamp: string;
|
||||
}
|
||||
|
||||
export function ActivityFeedItem({
|
||||
icon,
|
||||
content,
|
||||
timestamp,
|
||||
}: ActivityFeedItemProps) {
|
||||
return (
|
||||
<Box
|
||||
display="flex"
|
||||
alignItems="start"
|
||||
gap={3}
|
||||
py={3}
|
||||
borderBottom
|
||||
style={{ borderColor: 'rgba(38, 38, 38, 0.3)' }}
|
||||
className="last:border-0"
|
||||
>
|
||||
<Surface
|
||||
variant="muted"
|
||||
w="8"
|
||||
h="8"
|
||||
rounded="full"
|
||||
display="flex"
|
||||
center
|
||||
flexShrink={0}
|
||||
>
|
||||
{icon}
|
||||
</Surface>
|
||||
<Box style={{ flex: 1, minWidth: 0 }}>
|
||||
<Text size="sm" leading="relaxed" block>
|
||||
{content}
|
||||
</Text>
|
||||
<Text size="xs" color="text-gray-500" mt={1} block>
|
||||
{timestamp}
|
||||
</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
14
apps/website/components/feed/ActivityFeedList.tsx
Normal file
14
apps/website/components/feed/ActivityFeedList.tsx
Normal file
@@ -0,0 +1,14 @@
|
||||
import React, { ReactNode } from 'react';
|
||||
import { Stack } from '@/ui/Stack';
|
||||
|
||||
interface ActivityFeedListProps {
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
export function ActivityFeedList({ children }: ActivityFeedListProps) {
|
||||
return (
|
||||
<Stack gap={4}>
|
||||
{children}
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
66
apps/website/components/feed/FeedLayout.tsx
Normal file
66
apps/website/components/feed/FeedLayout.tsx
Normal file
@@ -0,0 +1,66 @@
|
||||
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 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>
|
||||
);
|
||||
}
|
||||
33
apps/website/components/feed/FeedList.tsx
Normal file
33
apps/website/components/feed/FeedList.tsx
Normal file
@@ -0,0 +1,33 @@
|
||||
import React from 'react';
|
||||
import { FeedEmptyState } from '@/ui/FeedEmptyState';
|
||||
import { FeedItemCard } from '@/components/feed/FeedItemCard';
|
||||
import { Stack } from '@/ui/Stack';
|
||||
|
||||
interface FeedItemData {
|
||||
id: string;
|
||||
type: string;
|
||||
headline: string;
|
||||
body?: string;
|
||||
timestamp: string;
|
||||
formattedTime: string;
|
||||
ctaHref?: string;
|
||||
ctaLabel?: string;
|
||||
}
|
||||
|
||||
interface FeedListProps {
|
||||
items: FeedItemData[];
|
||||
}
|
||||
|
||||
export function FeedList({ items }: FeedListProps) {
|
||||
if (!items.length) {
|
||||
return <FeedEmptyState />;
|
||||
}
|
||||
|
||||
return (
|
||||
<Stack gap={4}>
|
||||
{items.map(item => (
|
||||
<FeedItemCard key={item.id} item={item} />
|
||||
))}
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
84
apps/website/components/feed/RecentActivity.tsx
Normal file
84
apps/website/components/feed/RecentActivity.tsx
Normal file
@@ -0,0 +1,84 @@
|
||||
|
||||
|
||||
import { mediaConfig } from '@/lib/config/mediaConfig';
|
||||
import { ActiveDriverCard } from '@/ui/ActiveDriverCard';
|
||||
import { Box } from '@/ui/Box';
|
||||
import { Heading } from '@/ui/Heading';
|
||||
import { Icon } from '@/ui/Icon';
|
||||
import { Text } from '@/ui/Text';
|
||||
import { Activity } from 'lucide-react';
|
||||
|
||||
const SKILL_LEVELS = [
|
||||
{ id: 'pro', label: 'Pro', color: 'text-yellow-400' },
|
||||
{ id: 'advanced', label: 'Advanced', color: 'text-purple-400' },
|
||||
{ id: 'intermediate', label: 'Intermediate', color: 'text-primary-blue' },
|
||||
{ id: 'beginner', label: 'Beginner', color: 'text-green-400' },
|
||||
];
|
||||
|
||||
const CATEGORIES = [
|
||||
{ id: 'beginner', label: 'Beginner', color: 'text-green-400' },
|
||||
{ id: 'intermediate', label: 'Intermediate', color: 'text-primary-blue' },
|
||||
{ id: 'advanced', label: 'Advanced', color: 'text-purple-400' },
|
||||
{ id: 'pro', label: 'Pro', color: 'text-yellow-400' },
|
||||
{ id: 'endurance', label: 'Endurance', color: 'text-orange-400' },
|
||||
{ id: 'sprint', label: 'Sprint', color: 'text-red-400' },
|
||||
];
|
||||
|
||||
interface RecentActivityProps {
|
||||
drivers: {
|
||||
id: string;
|
||||
name: string;
|
||||
avatarUrl?: string;
|
||||
isActive: boolean;
|
||||
skillLevel?: string;
|
||||
category?: string;
|
||||
}[];
|
||||
onDriverClick: (id: string) => void;
|
||||
}
|
||||
|
||||
export function RecentActivity({ drivers, onDriverClick }: RecentActivityProps) {
|
||||
const activeDrivers = drivers.filter((d) => d.isActive).slice(0, 6);
|
||||
|
||||
return (
|
||||
<Box mb={10}>
|
||||
<Box display="flex" alignItems="center" gap={3} mb={4}>
|
||||
<Box
|
||||
display="flex"
|
||||
h="10"
|
||||
w="10"
|
||||
alignItems="center"
|
||||
justifyContent="center"
|
||||
rounded="xl"
|
||||
bg="bg-performance-green/10"
|
||||
border
|
||||
borderColor="border-performance-green/20"
|
||||
>
|
||||
<Icon icon={Activity} size={5} color="rgb(16, 185, 129)" />
|
||||
</Box>
|
||||
<Box>
|
||||
<Heading level={2}>Active Drivers</Heading>
|
||||
<Text size="xs" color="text-gray-500">Currently competing in leagues</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
<Box display="grid" responsiveGridCols={{ base: 2, md: 3, lg: 6 }} gap={3}>
|
||||
{activeDrivers.map((driver) => {
|
||||
const levelConfig = SKILL_LEVELS.find((l) => l.id === driver.skillLevel);
|
||||
const categoryConfig = CATEGORIES.find((c) => c.id === driver.category);
|
||||
return (
|
||||
<ActiveDriverCard
|
||||
key={driver.id}
|
||||
name={driver.name}
|
||||
avatarUrl={driver.avatarUrl || mediaConfig.avatars.defaultFallback}
|
||||
categoryLabel={categoryConfig?.label}
|
||||
categoryColor={categoryConfig?.color}
|
||||
skillLevelLabel={levelConfig?.label}
|
||||
skillLevelColor={levelConfig?.color}
|
||||
onClick={() => onDriverClick(driver.id)}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user