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,19 +1,22 @@
'use client';
import Button from '@/ui/Button';
import Input from '@/ui/Input';
import { useEffectiveDriverId } from "@/lib/hooks/useEffectiveDriverId";
import { useCreateTeam } from "@/lib/hooks/team";
import { useRouter } from 'next/navigation';
import { useState } from 'react';
import React, { useState } from 'react';
import { Button } from '@/ui/Button';
import { Input } from '@/ui/Input';
import { TextArea } from '@/ui/TextArea';
import { useCreateTeam } from "@/hooks/team/useCreateTeam";
import { Box } from '@/ui/Box';
import { Stack } from '@/ui/Stack';
import { Text } from '@/ui/Text';
import { InfoBanner } from '@/ui/InfoBanner';
interface CreateTeamFormProps {
onCancel?: () => void;
onSuccess?: (teamId: string) => void;
onNavigate: (teamId: string) => void;
}
export default function CreateTeamForm({ onCancel, onSuccess }: CreateTeamFormProps) {
const router = useRouter();
export function CreateTeamForm({ onCancel, onSuccess, onNavigate }: CreateTeamFormProps) {
const createTeamMutation = useCreateTeam();
const [formData, setFormData] = useState({
name: '',
@@ -21,7 +24,6 @@ export default function CreateTeamForm({ onCancel, onSuccess }: CreateTeamFormPr
description: '',
});
const [errors, setErrors] = useState<Record<string, string>>({});
const currentDriverId = useEffectiveDriverId();
const validateForm = () => {
const newErrors: Record<string, string> = {};
@@ -67,7 +69,7 @@ export default function CreateTeamForm({ onCancel, onSuccess }: CreateTeamFormPr
if (onSuccess) {
onSuccess(teamId);
} else {
router.push(`/teams/${teamId}`);
onNavigate(teamId);
}
},
onError: (error) => {
@@ -78,93 +80,72 @@ export default function CreateTeamForm({ onCancel, onSuccess }: CreateTeamFormPr
};
return (
<form onSubmit={handleSubmit} className="space-y-6">
<div>
<label className="block text-sm font-medium text-gray-400 mb-2">
Team Name *
</label>
<Box as="form" onSubmit={handleSubmit}>
<Stack gap={6}>
<Input
label="Team Name *"
type="text"
value={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
placeholder="Enter team name..."
disabled={createTeamMutation.isPending}
variant={errors.name ? 'error' : 'default'}
errorMessage={errors.name}
/>
{errors.name && (
<p className="text-danger-red text-xs mt-1">{errors.name}</p>
)}
</div>
<div>
<label className="block text-sm font-medium text-gray-400 mb-2">
Team Tag *
</label>
<Input
label="Team Tag *"
type="text"
value={formData.tag}
onChange={(e) => setFormData({ ...formData, tag: e.target.value.toUpperCase() })}
placeholder="e.g., APEX"
maxLength={4}
disabled={createTeamMutation.isPending}
variant={errors.tag ? 'error' : 'default'}
errorMessage={errors.tag}
/>
<p className="text-xs text-gray-500 mt-1">Max 4 characters</p>
{errors.tag && (
<p className="text-danger-red text-xs mt-1">{errors.tag}</p>
)}
</div>
<div>
<label className="block text-sm font-medium text-gray-400 mb-2">
Description *
</label>
<textarea
className="w-full px-3 py-3 bg-iron-gray border-0 rounded-md text-white ring-1 ring-inset ring-charcoal-outline focus:ring-2 focus:ring-primary-blue transition-all duration-150 text-sm resize-none"
<TextArea
label="Description *"
rows={4}
value={formData.description}
onChange={(e) => setFormData({ ...formData, description: e.target.value })}
placeholder="Describe your team's goals and racing style..."
disabled={createTeamMutation.isPending}
variant={errors.description ? 'error' : 'default'}
errorMessage={errors.description}
/>
{errors.description && (
<p className="text-danger-red text-xs mt-1">{errors.description}</p>
)}
</div>
<div className="bg-deep-graphite border border-charcoal-outline rounded-lg p-4">
<div className="flex items-start gap-3">
<div className="text-2xl"></div>
<div className="flex-1">
<h4 className="text-white font-medium mb-1">About Team Creation</h4>
<ul className="text-sm text-gray-400 space-y-1">
<li> You will be assigned as the team owner</li>
<li> You can invite other drivers to join your team</li>
<li> Team standings are calculated across leagues</li>
<li> This is alpha data - it resets on page reload</li>
</ul>
</div>
</div>
</div>
<InfoBanner title="About Team Creation">
<Stack as="ul" gap={1}>
<Text as="li" size="sm" color="text-gray-400"> You will be assigned as the team owner</Text>
<Text as="li" size="sm" color="text-gray-400"> You can invite other drivers to join your team</Text>
<Text as="li" size="sm" color="text-gray-400"> Team standings are calculated across leagues</Text>
<Text as="li" size="sm" color="text-gray-400"> This is alpha data - it resets on page reload</Text>
</Stack>
</InfoBanner>
<div className="flex gap-3">
<Button
type="submit"
variant="primary"
disabled={createTeamMutation.isPending}
className="flex-1"
>
{createTeamMutation.isPending ? 'Creating Team...' : 'Create Team'}
</Button>
{onCancel && (
<Box display="flex" gap={3}>
<Button
type="button"
variant="secondary"
onClick={onCancel}
type="submit"
variant="primary"
disabled={createTeamMutation.isPending}
fullWidth
>
Cancel
{createTeamMutation.isPending ? 'Creating Team...' : 'Create Team'}
</Button>
)}
</div>
</form>
{onCancel && (
<Button
type="button"
variant="secondary"
onClick={onCancel}
disabled={createTeamMutation.isPending}
>
Cancel
</Button>
)}
</Box>
</Stack>
</Box>
);
}
}

View File

@@ -1,48 +1,15 @@
import Image from 'next/image';
import { UserPlus, Users, Trophy } from 'lucide-react';
import { getMediaUrl } from '@/lib/utilities/media';
'use client';
const SKILL_LEVELS: {
id: string;
label: string;
icon: React.ElementType;
color: string;
bgColor: string;
borderColor: string;
}[] = [
{
id: 'pro',
label: 'Pro',
icon: () => null,
color: 'text-yellow-400',
bgColor: 'bg-yellow-400/10',
borderColor: 'border-yellow-400/30',
},
{
id: 'advanced',
label: 'Advanced',
icon: () => null,
color: 'text-purple-400',
bgColor: 'bg-purple-400/10',
borderColor: 'border-purple-400/30',
},
{
id: 'intermediate',
label: 'Intermediate',
icon: () => null,
color: 'text-primary-blue',
bgColor: 'bg-primary-blue/10',
borderColor: 'border-primary-blue/30',
},
{
id: 'beginner',
label: 'Beginner',
icon: () => null,
color: 'text-green-400',
bgColor: 'bg-green-400/10',
borderColor: 'border-green-400/30',
},
];
import React from 'react';
import { UserPlus } from 'lucide-react';
import { getMediaUrl } from '@/lib/utilities/media';
import { Box } from '@/ui/Box';
import { Stack } from '@/ui/Stack';
import { Text } from '@/ui/Text';
import { Heading } from '@/ui/Heading';
import { Icon } from '@/ui/Icon';
import { RecruitingTeamGrid } from '@/ui/RecruitingTeamGrid';
import { RecruitingTeamCard } from '@/ui/RecruitingTeamCard';
interface FeaturedRecruitingProps {
teams: Array<{
@@ -59,75 +26,46 @@ interface FeaturedRecruitingProps {
onTeamClick: (id: string) => void;
}
export default function FeaturedRecruiting({ teams, onTeamClick }: FeaturedRecruitingProps) {
export function FeaturedRecruiting({ teams, onTeamClick }: FeaturedRecruitingProps) {
const recruitingTeams = teams.filter((t) => t.isRecruiting).slice(0, 4);
if (recruitingTeams.length === 0) return null;
return (
<div className="mb-10">
<div className="flex items-center gap-3 mb-4">
<div className="flex h-10 w-10 items-center justify-center rounded-xl bg-performance-green/10 border border-performance-green/20">
<UserPlus className="w-5 h-5 text-performance-green" />
</div>
<div>
<h2 className="text-lg font-semibold text-white">Looking for Drivers</h2>
<p className="text-xs text-gray-500">Teams actively recruiting new members</p>
</div>
</div>
<Box mb={10}>
<Stack direction="row" align="center" gap={3} mb={4}>
<Box
display="flex"
center
width="10"
height="10"
rounded="xl"
bg="bg-performance-green/10"
border={true}
borderColor="border-performance-green/20"
>
<Icon icon={UserPlus} size={5} color="text-performance-green" />
</Box>
<Box>
<Heading level={2}>Looking for Drivers</Heading>
<Text size="xs" color="text-gray-500">Teams actively recruiting new members</Text>
</Box>
</Stack>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
{recruitingTeams.map((team) => {
const levelConfig = SKILL_LEVELS.find((l) => l.id === team.performanceLevel);
return (
<button
key={team.id}
type="button"
onClick={() => onTeamClick(team.id)}
className="p-4 rounded-xl bg-iron-gray/60 border border-charcoal-outline hover:border-performance-green/40 transition-all duration-200 text-left group"
>
<div className="flex items-start justify-between mb-3">
<div className="flex h-8 w-8 items-center justify-center rounded-lg bg-charcoal-outline border border-charcoal-outline overflow-hidden">
<Image
src={team.logoUrl || getMediaUrl('team-logo', team.id)}
alt={team.name}
width={32}
height={32}
className="w-full h-full object-cover"
/>
</div>
<span className="flex items-center gap-1 px-2 py-0.5 rounded-full text-[10px] bg-performance-green/10 text-performance-green border border-performance-green/20">
<div className="w-1.5 h-1.5 rounded-full bg-performance-green animate-pulse" />
Recruiting
</span>
</div>
<h3 className="text-white font-semibold mb-1 group-hover:text-performance-green transition-colors line-clamp-1">
{team.name}
</h3>
<p className="text-xs text-gray-500 line-clamp-2 mb-3">{team.description}</p>
<div className="flex items-center gap-2 text-xs text-gray-400 flex-wrap">
{team.category && (
<span className="flex items-center gap-1 text-purple-400">
<span className="w-1.5 h-1.5 rounded-full bg-purple-400"></span>
{team.category}
</span>
)}
<span className="flex items-center gap-1">
<Users className="w-3 h-3" />
{team.memberCount}
</span>
<span className="flex items-center gap-1">
<Trophy className="w-3 h-3" />
{team.totalWins} wins
</span>
</div>
</button>
);
})}
</div>
</div>
<RecruitingTeamGrid>
{recruitingTeams.map((team) => (
<RecruitingTeamCard
key={team.id}
name={team.name}
description={team.description}
logoUrl={team.logoUrl || getMediaUrl('team-logo', team.id)}
category={team.category}
memberCount={team.memberCount}
totalWins={team.totalWins}
onClick={() => onTeamClick(team.id)}
/>
))}
</RecruitingTeamGrid>
</Box>
);
}
}

View File

@@ -1,9 +1,9 @@
'use client';
import Button from '@/ui/Button';
import { useEffectiveDriverId } from "@/lib/hooks/useEffectiveDriverId";
import { useTeamMembership, useJoinTeam, useLeaveTeam } from "@/lib/hooks/team";
import { useState } from 'react';
import React from 'react';
import { Button } from '@/ui/Button';
import { useEffectiveDriverId } from "@/hooks/useEffectiveDriverId";
import { useTeamMembership, useJoinTeam, useLeaveTeam } from "@/hooks/team";
interface JoinTeamButtonProps {
teamId: string;
@@ -11,13 +11,12 @@ interface JoinTeamButtonProps {
onUpdate?: () => void;
}
export default function JoinTeamButton({
export function JoinTeamButton({
teamId,
requiresApproval = false,
onUpdate,
}: JoinTeamButtonProps) {
const currentDriverId = useEffectiveDriverId();
const [showConfirmation, setShowConfirmation] = useState(false);
// Use hooks for data fetching
const { data: membership, isLoading: loadingMembership } = useTeamMembership(teamId, currentDriverId || '');
@@ -32,7 +31,6 @@ export default function JoinTeamButton({
const leaveTeamMutation = useLeaveTeam({
onSuccess: () => {
onUpdate?.();
setShowConfirmation(false);
},
});
@@ -106,4 +104,4 @@ export default function JoinTeamButton({
: 'Join Team'}
</Button>
);
}
}

View File

@@ -1,6 +1,11 @@
import { useState } from 'react';
import { ChevronRight, Users, Trophy, UserPlus } from 'lucide-react';
import { TeamCard } from './TeamCard';
'use client';
import React, { useState } from 'react';
import { LucideIcon } from 'lucide-react';
import { TeamCard } from '@/ui/TeamCardWrapper';
import { SkillLevelHeader } from '@/ui/SkillLevelHeader';
import { TeamGrid } from '@/ui/TeamGrid';
import { Box } from '@/ui/Box';
type SkillLevel = 'pro' | 'advanced' | 'intermediate' | 'beginner';
type TeamSpecialization = 'endurance' | 'sprint' | 'mixed';
@@ -8,7 +13,7 @@ type TeamSpecialization = 'endurance' | 'sprint' | 'mixed';
interface SkillLevelConfig {
id: SkillLevel;
label: string;
icon: React.ElementType;
icon: LucideIcon;
color: string;
bgColor: string;
borderColor: string;
@@ -37,7 +42,7 @@ interface SkillLevelSectionProps {
defaultExpanded?: boolean;
}
export default function SkillLevelSection({
export function SkillLevelSection({
level,
teams,
onTeamClick,
@@ -46,7 +51,6 @@ export default function SkillLevelSection({
const [isExpanded, setIsExpanded] = useState(defaultExpanded);
const recruitingTeams = teams.filter((t) => t.isRecruiting);
const displayedTeams = isExpanded ? teams : teams.slice(0, 3);
const Icon = level.icon;
if (teams.length === 0) return null;
@@ -58,44 +62,22 @@ export default function SkillLevelSection({
};
return (
<div className="mb-8">
{/* Section Header */}
<div className="flex items-center justify-between mb-4">
<div className="flex items-center gap-3">
<div className={`flex h-11 w-11 items-center justify-center rounded-xl ${level.bgColor} border ${level.borderColor}`}>
<Icon className={`w-5 h-5 ${level.color}`} />
</div>
<div>
<div className="flex items-center gap-2">
<h2 className="text-xl font-bold text-white">{level.label}</h2>
<span className="px-2 py-0.5 rounded-full text-xs bg-charcoal-outline/50 text-gray-400">
{teams.length} {teams.length === 1 ? 'team' : 'teams'}
</span>
{recruitingTeams.length > 0 && (
<span className="flex items-center gap-1 px-2 py-0.5 rounded-full text-xs bg-performance-green/10 text-performance-green border border-performance-green/20">
<UserPlus className="w-3 h-3" />
{recruitingTeams.length} recruiting
</span>
)}
</div>
<p className="text-sm text-gray-500">{level.description}</p>
</div>
</div>
<Box mb={8}>
<SkillLevelHeader
label={level.label}
icon={level.icon}
bgColor={level.bgColor}
borderColor={level.borderColor}
color={level.color}
description={level.description}
teamCount={teams.length}
recruitingCount={recruitingTeams.length}
isExpanded={isExpanded}
onToggle={() => setIsExpanded(!isExpanded)}
showToggle={teams.length > 3}
/>
{teams.length > 3 && (
<button
type="button"
onClick={() => setIsExpanded(!isExpanded)}
className="flex items-center gap-1 px-3 py-1.5 rounded-lg text-sm text-gray-400 hover:text-white hover:bg-iron-gray/50 transition-all"
>
{isExpanded ? 'Show less' : `View all ${teams.length}`}
<ChevronRight className={`w-4 h-4 transition-transform ${isExpanded ? 'rotate-90' : ''}`} />
</button>
)}
</div>
{/* Teams Grid */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
<TeamGrid>
{displayedTeams.map((team) => (
<TeamCard
key={team.id}
@@ -116,7 +98,7 @@ export default function SkillLevelSection({
onClick={() => onTeamClick(team.id)}
/>
))}
</div>
</div>
</TeamGrid>
</Box>
);
}
}

View File

@@ -1,14 +0,0 @@
interface StatItemProps {
label: string;
value: string;
color: string;
}
export default function StatItem({ label, value, color }: StatItemProps) {
return (
<div className="flex items-center justify-between">
<span className="text-gray-400 text-sm">{label}</span>
<span className={`font-semibold ${color}`}>{value}</span>
</div>
);
}

View File

@@ -1,10 +1,19 @@
'use client';
import { useState } from 'react';
import Card from '@/ui/Card';
import Button from '@/ui/Button';
import Input from '@/ui/Input';
import { useTeamJoinRequests, useUpdateTeam, useApproveJoinRequest, useRejectJoinRequest } from "@/lib/hooks/team";
import React, { useState } from 'react';
import { Card } from '@/ui/Card';
import { Button } from '@/ui/Button';
import { Input } from '@/ui/Input';
import { TextArea } from '@/ui/TextArea';
import { Box } from '@/ui/Box';
import { Stack } from '@/ui/Stack';
import { Text } from '@/ui/Text';
import { Heading } from '@/ui/Heading';
import { JoinRequestList } from '@/ui/JoinRequestList';
import { JoinRequestItem } from '@/ui/JoinRequestItem';
import { DangerZone } from '@/ui/DangerZone';
import { MinimalEmptyState } from '@/ui/EmptyState';
import { useTeamJoinRequests, useUpdateTeam, useApproveJoinRequest, useRejectJoinRequest } from "@/hooks/team";
import type { TeamJoinRequestViewModel } from '@/lib/view-models/TeamJoinRequestViewModel';
interface TeamAdminProps {
@@ -18,7 +27,7 @@ interface TeamAdminProps {
onUpdate: () => void;
}
export default function TeamAdmin({ team, onUpdate }: TeamAdminProps) {
export function TeamAdmin({ team, onUpdate }: TeamAdminProps) {
const [editMode, setEditMode] = useState(false);
const [editedTeam, setEditedTeam] = useState({
name: team.name,
@@ -62,13 +71,13 @@ export default function TeamAdmin({ team, onUpdate }: TeamAdminProps) {
},
});
const handleApprove = (requestId: string) => {
const handleApprove = () => {
// Note: The current API doesn't support approving specific requests
// This would need the requestId to be passed to the service
approveJoinRequestMutation.mutate();
};
const handleReject = (requestId: string) => {
const handleReject = () => {
// Note: The current API doesn't support rejecting specific requests
// This would need the requestId to be passed to the service
rejectJoinRequestMutation.mutate();
@@ -86,56 +95,51 @@ export default function TeamAdmin({ team, onUpdate }: TeamAdminProps) {
};
return (
<div className="space-y-6">
<Stack gap={6}>
<Card>
<div className="flex items-center justify-between mb-6">
<h3 className="text-xl font-semibold text-white">Team Settings</h3>
<Box display="flex" alignItems="center" justifyContent="between" mb={6}>
<Heading level={3}>Team Settings</Heading>
{!editMode && (
<Button variant="secondary" onClick={() => setEditMode(true)}>
Edit Details
</Button>
)}
</div>
</Box>
{editMode ? (
<div className="space-y-4">
<div>
<label className="block text-sm font-medium text-gray-400 mb-2">
<Stack gap={4}>
<Box>
<Text as="label" size="sm" weight="medium" color="text-gray-400" block mb={2}>
Team Name
</label>
</Text>
<Input
type="text"
value={editedTeam.name}
onChange={(e) => setEditedTeam({ ...editedTeam, name: e.target.value })}
/>
</div>
</Box>
<div>
<label className="block text-sm font-medium text-gray-400 mb-2">
<Box>
<Text as="label" size="sm" weight="medium" color="text-gray-400" block mb={2}>
Team Tag
</label>
</Text>
<Input
type="text"
value={editedTeam.tag}
onChange={(e) => setEditedTeam({ ...editedTeam, tag: e.target.value })}
maxLength={4}
/>
<p className="text-xs text-gray-500 mt-1">Max 4 characters</p>
</div>
<Text size="xs" color="text-gray-500" block mt={1}>Max 4 characters</Text>
</Box>
<div>
<label className="block text-sm font-medium text-gray-400 mb-2">
Description
</label>
<textarea
className="w-full px-3 py-3 bg-iron-gray border-0 rounded-md text-white ring-1 ring-inset ring-charcoal-outline focus:ring-2 focus:ring-primary-blue transition-all duration-150 text-sm resize-none"
rows={4}
value={editedTeam.description}
onChange={(e) => setEditedTeam({ ...editedTeam, description: e.target.value })}
/>
</div>
<TextArea
label="Description"
rows={4}
value={editedTeam.description}
onChange={(e) => setEditedTeam({ ...editedTeam, description: e.target.value })}
/>
<div className="flex gap-2">
<Stack direction="row" gap={2}>
<Button variant="primary" onClick={handleSaveChanges} disabled={updateTeamMutation.isPending}>
{updateTeamMutation.isPending ? 'Saving...' : 'Save Changes'}
</Button>
@@ -152,93 +156,63 @@ export default function TeamAdmin({ team, onUpdate }: TeamAdminProps) {
>
Cancel
</Button>
</div>
</div>
</Stack>
</Stack>
) : (
<div className="space-y-4">
<div>
<div className="text-sm text-gray-400">Team Name</div>
<div className="text-white font-medium">{team.name}</div>
</div>
<div>
<div className="text-sm text-gray-400">Team Tag</div>
<div className="text-white font-medium">{team.tag}</div>
</div>
<div>
<div className="text-sm text-gray-400">Description</div>
<div className="text-white">{team.description}</div>
</div>
</div>
<Stack gap={4}>
<Box>
<Text size="sm" color="text-gray-400" block>Team Name</Text>
<Text color="text-white" weight="medium" block>{team.name}</Text>
</Box>
<Box>
<Text size="sm" color="text-gray-400" block>Team Tag</Text>
<Text color="text-white" weight="medium" block>{team.tag}</Text>
</Box>
<Box>
<Text size="sm" color="text-gray-400" block>Description</Text>
<Text color="text-white" block>{team.description}</Text>
</Box>
</Stack>
)}
</Card>
<Card>
<h3 className="text-xl font-semibold text-white mb-6">Join Requests</h3>
<Heading level={3} mb={6}>Join Requests</Heading>
{loading ? (
<div className="text-center py-8 text-gray-400">Loading requests...</div>
<Box textAlign="center" py={8}>
<Text color="text-gray-400">Loading requests...</Text>
</Box>
) : joinRequests.length > 0 ? (
<div className="space-y-3">
{joinRequests.map((request: TeamJoinRequestViewModel) => {
// Note: Driver hydration is not provided by the API response
// so we only display driverId
return (
<div
key={request.requestId}
className="flex items-center justify-between p-4 rounded-lg bg-deep-graphite border border-charcoal-outline"
>
<div className="flex items-center gap-4 flex-1">
<div className="w-12 h-12 rounded-full bg-primary-blue/20 flex items-center justify-center text-lg font-bold text-white">
{request.driverId.charAt(0)}
</div>
<div className="flex-1">
<h4 className="text-white font-medium">{request.driverId}</h4>
<p className="text-sm text-gray-400">
Requested {new Date(request.requestedAt).toLocaleDateString()}
</p>
</div>
</div>
<div className="flex gap-2">
<Button
variant="primary"
onClick={() => handleApprove(request.requestId)}
disabled={approveJoinRequestMutation.isPending}
>
{approveJoinRequestMutation.isPending ? 'Approving...' : 'Approve'}
</Button>
<Button
variant="danger"
onClick={() => handleReject(request.requestId)}
disabled={rejectJoinRequestMutation.isPending}
>
{rejectJoinRequestMutation.isPending ? 'Rejecting...' : 'Reject'}
</Button>
</div>
</div>
);
})}
</div>
<JoinRequestList>
{joinRequests.map((request: TeamJoinRequestViewModel) => (
<JoinRequestItem
key={request.requestId}
driverId={request.driverId}
requestedAt={request.requestedAt}
onApprove={() => handleApprove()}
onReject={() => handleReject()}
isApproving={approveJoinRequestMutation.isPending}
isRejecting={rejectJoinRequestMutation.isPending}
/>
))}
</JoinRequestList>
) : (
<div className="text-center py-8 text-gray-400">
No pending join requests
</div>
<MinimalEmptyState
title="No pending join requests"
description="When drivers request to join your team, they will appear here."
/>
)}
</Card>
<Card>
<h3 className="text-xl font-semibold text-white mb-4">Danger Zone</h3>
<div className="space-y-4">
<div className="p-4 rounded-lg bg-danger-red/10 border border-danger-red/30">
<h4 className="text-white font-medium mb-2">Disband Team</h4>
<p className="text-sm text-gray-400 mb-4">
Permanently delete this team. This action cannot be undone.
</p>
<Button variant="danger" disabled>
Disband Team (Coming Soon)
</Button>
</div>
</div>
</Card>
</div>
<DangerZone
title="Disband Team"
description="Permanently delete this team. This action cannot be undone."
>
<Button variant="danger" disabled>
Disband Team (Coming Soon)
</Button>
</DangerZone>
</Stack>
);
}
}

View File

@@ -1,215 +0,0 @@
'use client';
import React from 'react';
import {
Users,
ChevronRight,
UserPlus,
Zap,
Clock,
Globe,
Languages,
Crown,
Star,
TrendingUp,
Shield
} from 'lucide-react';
import { Card } from '@/ui/Card';
import { Box } from '@/ui/Box';
import { Stack } from '@/ui/Stack';
import { Text } from '@/ui/Text';
import { Heading } from '@/ui/Heading';
import { Badge } from '@/ui/Badge';
import { Image } from '@/ui/Image';
import PlaceholderImage from '@/ui/PlaceholderImage';
interface TeamCardProps {
id: string;
name: string;
description?: string;
logo?: string;
memberCount: number;
rating?: number | null;
totalWins?: number;
totalRaces?: number;
performanceLevel?: 'beginner' | 'intermediate' | 'advanced' | 'pro';
isRecruiting?: boolean;
specialization?: 'endurance' | 'sprint' | 'mixed' | undefined;
region?: string;
languages?: string[] | undefined;
leagues?: string[];
category?: string;
onClick?: () => void;
}
function getPerformanceBadge(level?: string) {
switch (level) {
case 'pro':
return { icon: Crown, label: 'Pro', variant: 'warning' as const };
case 'advanced':
return { icon: Star, label: 'Advanced', variant: 'primary' as const };
case 'intermediate':
return { icon: TrendingUp, label: 'Intermediate', variant: 'info' as const };
case 'beginner':
return { icon: Shield, label: 'Beginner', variant: 'success' as const };
default:
return null;
}
}
function getSpecializationBadge(specialization?: string) {
switch (specialization) {
case 'endurance':
return { icon: Clock, label: 'Endurance', color: '#f97316' };
case 'sprint':
return { icon: Zap, label: 'Sprint', color: '#00f2ff' };
default:
return null;
}
}
export function TeamCard({
name,
description,
logo,
memberCount,
rating,
totalWins,
totalRaces,
performanceLevel,
isRecruiting,
specialization,
region,
languages,
category,
onClick,
}: TeamCardProps) {
const performanceBadge = getPerformanceBadge(performanceLevel);
const specializationBadge = getSpecializationBadge(specialization);
return (
<Box onClick={onClick} style={{ height: '100%', cursor: 'pointer' }}>
<Card style={{ height: '100%', padding: 0, display: 'flex', flexDirection: 'column', overflow: 'hidden' }}>
{/* Header with Logo */}
<Box style={{ padding: '1rem', paddingBottom: 0 }}>
<Stack direction="row" align="start" gap={4}>
{/* Logo */}
<Box style={{ width: '3.5rem', height: '3.5rem', borderRadius: '0.75rem', backgroundColor: '#262626', display: 'flex', alignItems: 'center', justifyContent: 'center', overflow: 'hidden', border: '1px solid #262626' }}>
{logo ? (
<Image
src={logo}
alt={name}
width={56}
height={56}
style={{ width: '100%', height: '100%', objectFit: 'cover' }}
/>
) : (
<PlaceholderImage size={56} />
)}
</Box>
{/* Title & Badges */}
<Box style={{ flex: 1, minWidth: 0 }}>
<Stack direction="row" align="start" justify="between" gap={2}>
<Heading level={3} style={{ fontSize: '1rem', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>
{name}
</Heading>
{isRecruiting && (
<Badge variant="success">
<UserPlus style={{ width: '0.75rem', height: '0.75rem' }} />
Recruiting
</Badge>
)}
</Stack>
{/* Performance Level & Category */}
<Stack direction="row" align="center" gap={2} wrap style={{ marginTop: '0.375rem' }}>
{performanceBadge && (
<Badge variant={performanceBadge.variant}>
<performanceBadge.icon style={{ width: '0.75rem', height: '0.75rem' }} />
{performanceBadge.label}
</Badge>
)}
{specializationBadge && (
<Stack direction="row" align="center" gap={1}>
<specializationBadge.icon style={{ width: '0.75rem', height: '0.75rem', color: specializationBadge.color }} />
<Text size="xs" color="text-gray-500">{specializationBadge.label}</Text>
</Stack>
)}
{category && (
<Badge variant="primary">
<Box style={{ width: '0.5rem', height: '0.5rem', borderRadius: '9999px', backgroundColor: '#a855f7' }} />
{category}
</Badge>
)}
</Stack>
</Box>
</Stack>
</Box>
{/* Content */}
<Box style={{ padding: '1rem', display: 'flex', flexDirection: 'column', flex: 1 }}>
{/* Description */}
<Text size="xs" color="text-gray-500" style={{ marginBottom: '0.75rem', display: '-webkit-box', WebkitLineClamp: 2, WebkitBoxOrient: 'vertical', overflow: 'hidden' }}>
{description || 'No description available'}
</Text>
{/* Region & Languages */}
{(region || (languages && languages.length > 0)) && (
<Stack direction="row" align="center" gap={2} wrap style={{ marginBottom: '0.75rem' }}>
{region && (
<Box style={{ display: 'flex', alignItems: 'center', gap: '0.375rem', padding: '0.25rem 0.5rem', borderRadius: '0.375rem', backgroundColor: 'rgba(38, 38, 38, 0.5)', border: '1px solid rgba(38, 38, 38, 0.3)' }}>
<Globe style={{ width: '0.75rem', height: '0.75rem', color: '#00f2ff' }} />
<Text size="xs" color="text-gray-400">{region}</Text>
</Box>
)}
{languages && languages.length > 0 && (
<Box style={{ display: 'flex', alignItems: 'center', gap: '0.375rem', padding: '0.25rem 0.5rem', borderRadius: '0.375rem', backgroundColor: 'rgba(38, 38, 38, 0.5)', border: '1px solid rgba(38, 38, 38, 0.3)' }}>
<Languages style={{ width: '0.75rem', height: '0.75rem', color: '#a855f7' }} />
<Text size="xs" color="text-gray-400">
{languages.slice(0, 2).join(', ')}
{languages.length > 2 && ` +${languages.length - 2}`}
</Text>
</Box>
)}
</Stack>
)}
{/* Stats Grid */}
<Box style={{ display: 'grid', gridTemplateColumns: 'repeat(3, minmax(0, 1fr))', gap: '0.5rem', marginBottom: '1rem' }}>
<StatItem label="Rating" value={typeof rating === 'number' ? Math.round(rating).toLocaleString() : '—'} color="#3b82f6" />
<StatItem label="Wins" value={totalWins ?? 0} color="#10b981" />
<StatItem label="Races" value={totalRaces ?? 0} color="white" />
</Box>
{/* Spacer */}
<Box style={{ flex: 1 }} />
{/* Footer */}
<Box style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', paddingTop: '0.75rem', borderTop: '1px solid rgba(38, 38, 38, 0.5)', marginTop: 'auto' }}>
<Stack direction="row" align="center" gap={2}>
<Users style={{ width: '0.75rem', height: '0.75rem', color: '#737373' }} />
<Text size="xs" color="text-gray-500">
{memberCount} {memberCount === 1 ? 'member' : 'members'}
</Text>
</Stack>
<Stack direction="row" align="center" gap={1}>
<Text size="xs" color="text-gray-500">View</Text>
<ChevronRight style={{ width: '0.75rem', height: '0.75rem', color: '#737373' }} />
</Stack>
</Box>
</Box>
</Card>
</Box>
);
}
function StatItem({ label, value, color }: { label: string, value: string | number, color: string }) {
return (
<Box style={{ textAlign: 'center', padding: '0.5rem', borderRadius: '0.5rem', backgroundColor: 'rgba(38, 38, 38, 0.3)' }}>
<Text size="xs" color="text-gray-500" style={{ display: 'block', marginBottom: '0.125rem' }}>{label}</Text>
<Text size="sm" weight="semibold" style={{ color }}>{value}</Text>
</Box>
);
}

View File

@@ -1,83 +0,0 @@
'use client';
import React from 'react';
import { ExternalLink } from 'lucide-react';
import { Card } from '@/ui/Card';
import { Box } from '@/ui/Box';
import { Stack } from '@/ui/Stack';
import { Text } from '@/ui/Text';
import { Heading } from '@/ui/Heading';
import { Image } from '@/ui/Image';
import { Surface } from '@/ui/Surface';
import { Icon } from '@/ui/Icon';
import JoinTeamButton from '@/components/teams/JoinTeamButton';
import { getMediaUrl } from '@/lib/utilities/media';
interface TeamHeroProps {
team: {
id: string;
name: string;
tag: string | null;
description?: string;
category?: string | null;
createdAt?: string;
leagues: any[];
};
memberCount: number;
onUpdate: () => void;
}
export function TeamHero({ team, memberCount, onUpdate }: TeamHeroProps) {
return (
<Card>
<Stack direction="row" align="start" justify="between" wrap gap={6}>
<Stack direction="row" align="start" gap={6} wrap style={{ flex: 1 }}>
<Surface variant="muted" rounded="lg" padding={1} style={{ width: '6rem', height: '6rem', overflow: 'hidden', backgroundColor: '#262626' }}>
<Image
src={getMediaUrl('team-logo', team.id)}
alt={team.name}
width={96}
height={96}
style={{ width: '100%', height: '100%', objectFit: 'cover' }}
/>
</Surface>
<Box style={{ flex: 1, minWidth: 0 }}>
<Stack direction="row" align="center" gap={3} mb={2}>
<Heading level={1}>{team.name}</Heading>
{team.tag && (
<Surface variant="muted" rounded="full" padding={1} style={{ backgroundColor: '#262626', paddingLeft: '0.5rem', paddingRight: '0.5rem' }}>
<Text size="xs" color="text-gray-300">[{team.tag}]</Text>
</Surface>
)}
</Stack>
<Text color="text-gray-300" block mb={4} style={{ maxWidth: '42rem' }}>{team.description}</Text>
<Stack direction="row" align="center" gap={4} wrap style={{ fontSize: '0.875rem', color: '#9ca3af' }}>
<Text size="sm">{memberCount} {memberCount === 1 ? 'member' : 'members'}</Text>
{team.category && (
<Stack direction="row" align="center" gap={1.5}>
<Box style={{ width: '0.5rem', height: '0.5rem', borderRadius: '9999px', backgroundColor: '#a855f7' }} />
<Text size="sm" color="text-purple-400">{team.category}</Text>
</Stack>
)}
{team.createdAt && (
<Text size="sm">
Founded {new Date(team.createdAt).toLocaleDateString('en-US', { month: 'short', year: 'numeric' })}
</Text>
)}
{team.leagues && team.leagues.length > 0 && (
<Text size="sm">
Active in {team.leagues.length} {team.leagues.length === 1 ? 'league' : 'leagues'}
</Text>
)}
</Stack>
</Box>
</Stack>
<JoinTeamButton teamId={team.id} onUpdate={onUpdate} />
</Stack>
</Card>
);
}

View File

@@ -1,177 +0,0 @@
'use client';
import {
Users,
Search,
Plus,
Crown,
Star,
TrendingUp,
Shield,
UserPlus,
} from 'lucide-react';
import Button from '@/ui/Button';
import Heading from '@/ui/Heading';
import type { TeamSummaryViewModel } from '@/lib/view-models/TeamSummaryViewModel';
type SkillLevel = 'pro' | 'advanced' | 'intermediate' | 'beginner';
interface SkillLevelConfig {
id: SkillLevel;
label: string;
icon: React.ElementType;
color: string;
bgColor: string;
borderColor: string;
description: string;
}
const SKILL_LEVELS: SkillLevelConfig[] = [
{
id: 'pro',
label: 'Pro',
icon: Crown,
color: 'text-yellow-400',
bgColor: 'bg-yellow-400/10',
borderColor: 'border-yellow-400/30',
description: 'Elite competition, sponsored teams',
},
{
id: 'advanced',
label: 'Advanced',
icon: Star,
color: 'text-purple-400',
bgColor: 'bg-purple-400/10',
borderColor: 'border-purple-400/30',
description: 'Competitive racing, high consistency',
},
{
id: 'intermediate',
label: 'Intermediate',
icon: TrendingUp,
color: 'text-primary-blue',
bgColor: 'bg-primary-blue/10',
borderColor: 'border-primary-blue/30',
description: 'Growing skills, regular practice',
},
{
id: 'beginner',
label: 'Beginner',
icon: Shield,
color: 'text-green-400',
bgColor: 'bg-green-400/10',
borderColor: 'border-green-400/30',
description: 'Learning the basics, friendly environment',
},
];
interface TeamHeroSectionProps {
teams: TeamSummaryViewModel[];
teamsByLevel: Record<string, TeamSummaryViewModel[]>;
recruitingCount: number;
onShowCreateForm: () => void;
onBrowseTeams: () => void;
onSkillLevelClick: (level: SkillLevel) => void;
}
export default function TeamHeroSection({
teams,
teamsByLevel,
recruitingCount,
onShowCreateForm,
onBrowseTeams,
onSkillLevelClick,
}: TeamHeroSectionProps) {
return (
<div className="relative mb-10 overflow-hidden">
{/* Main Hero Card */}
<div className="relative py-12 px-8 rounded-2xl bg-gradient-to-br from-purple-900/30 via-iron-gray/80 to-deep-graphite border border-purple-500/20">
{/* Background decorations */}
<div className="absolute top-0 right-0 w-80 h-80 bg-purple-500/10 rounded-full blur-3xl" />
<div className="absolute bottom-0 left-1/4 w-64 h-64 bg-neon-aqua/5 rounded-full blur-3xl" />
<div className="absolute top-1/2 right-1/4 w-48 h-48 bg-yellow-400/5 rounded-full blur-2xl" />
<div className="relative z-10">
<div className="flex flex-col lg:flex-row lg:items-start lg:justify-between gap-8">
<div className="max-w-xl">
{/* Badge */}
<div className="inline-flex items-center gap-2 px-3 py-1 rounded-full bg-purple-500/10 border border-purple-500/20 text-purple-400 text-xs font-medium mb-4">
<Users className="w-3.5 h-3.5" />
Team Racing
</div>
<Heading level={1} className="text-4xl lg:text-5xl mb-4">
Find Your
<span className="text-purple-400"> Crew</span>
</Heading>
<p className="text-gray-400 text-lg leading-relaxed mb-6">
Solo racing is great. Team racing is unforgettable. Join a team that matches your skill level and ambitions.
</p>
{/* Quick Stats */}
<div className="flex flex-wrap gap-4 mb-6">
<div className="flex items-center gap-2 px-3 py-2 rounded-lg bg-iron-gray/50 border border-charcoal-outline">
<Users className="w-4 h-4 text-purple-400" />
<span className="text-white font-semibold">{teams.length}</span>
<span className="text-gray-500 text-sm">Teams</span>
</div>
<div className="flex items-center gap-2 px-3 py-2 rounded-lg bg-iron-gray/50 border border-charcoal-outline">
<UserPlus className="w-4 h-4 text-performance-green" />
<span className="text-white font-semibold">{recruitingCount}</span>
<span className="text-gray-500 text-sm">Recruiting</span>
</div>
</div>
{/* CTA Buttons */}
<div className="flex flex-wrap gap-3">
<Button
variant="primary"
onClick={onShowCreateForm}
className="flex items-center gap-2 px-5 py-2.5 bg-purple-600 hover:bg-purple-500"
>
<Plus className="w-4 h-4" />
Create Team
</Button>
<Button
variant="secondary"
onClick={onBrowseTeams}
className="flex items-center gap-2"
>
<Search className="w-4 h-4" />
Browse Teams
</Button>
</div>
</div>
{/* Skill Level Quick Nav */}
<div className="lg:w-72">
<p className="text-xs text-gray-500 uppercase tracking-wider mb-3">Find Your Level</p>
<div className="space-y-2">
{SKILL_LEVELS.map((level) => {
const LevelIcon = level.icon;
const count = teamsByLevel[level.id]?.length || 0;
return (
<button
key={level.id}
type="button"
onClick={() => onSkillLevelClick(level.id)}
className={`w-full flex items-center justify-between p-3 rounded-lg ${level.bgColor} border ${level.borderColor} hover:scale-[1.02] transition-all duration-200`}
>
<div className="flex items-center gap-2">
<LevelIcon className={`w-4 h-4 ${level.color}`} />
<span className="text-white font-medium">{level.label}</span>
</div>
<span className="text-gray-400 text-sm">{count} teams</span>
</button>
);
})}
</div>
</div>
</div>
</div>
</div>
</div>
);
}

View File

@@ -1,77 +0,0 @@
'use client';
import { useRouter } from 'next/navigation';
import Image from 'next/image';
import { getMediaUrl } from '@/lib/utilities/media';
export interface TeamLadderRowProps {
rank: number;
teamId: string;
teamName: string;
teamLogoUrl?: string;
memberCount: number;
teamRating: number | null;
totalWins: number;
totalRaces: number;
}
export default function TeamLadderRow({
rank,
teamId,
teamName,
teamLogoUrl,
memberCount,
teamRating,
totalWins,
totalRaces,
}: TeamLadderRowProps) {
const router = useRouter();
const logo = teamLogoUrl ?? getMediaUrl('team-logo', teamId);
const handleClick = () => {
router.push(`/teams/${teamId}`);
};
return (
<tr
onClick={handleClick}
className="cursor-pointer border-b border-charcoal-outline/60 hover:bg-iron-gray/30 transition-colors"
>
<td className="py-3 px-4 text-sm text-gray-300 font-semibold">#{rank}</td>
<td className="py-3 px-4">
<div className="flex items-center gap-3">
<div className="w-8 h-8 rounded-md overflow-hidden bg-charcoal-outline flex-shrink-0">
<Image
src={logo}
alt={teamName}
width={32}
height={32}
className="w-full h-full object-cover"
/>
</div>
<div className="flex flex-col">
<span className="text-sm font-semibold text-white truncate">
{teamName}
</span>
</div>
</div>
</td>
<td className="py-3 px-4 text-sm">
<span className="text-primary-blue font-semibold">
{teamRating !== null ? Math.round(teamRating) : '—'}
</span>
</td>
<td className="py-3 px-4 text-sm">
<span className="text-green-400 font-semibold">{totalWins}</span>
</td>
<td className="py-3 px-4 text-sm">
<span className="text-white">{totalRaces}</span>
</td>
<td className="py-3 px-4 text-sm">
<span className="text-gray-300">
{memberCount} {memberCount === 1 ? 'member' : 'members'}
</span>
</td>
</tr>
);
}

View File

@@ -1,183 +0,0 @@
'use client';
import React from 'react';
import { Award, ChevronRight, Crown, Trophy, Users } from 'lucide-react';
import { Button } from '@/ui/Button';
import { Box } from '@/ui/Box';
import { Stack } from '@/ui/Stack';
import { Text } from '@/ui/Text';
import { Heading } from '@/ui/Heading';
import { Image } from '@/ui/Image';
import { getMediaUrl } from '@/lib/utilities/media';
interface TeamLeaderboardPreviewProps {
topTeams: Array<{
id: string;
name: string;
logoUrl?: string;
category?: string;
memberCount: number;
totalWins: number;
isRecruiting: boolean;
rating?: number;
performanceLevel: string;
}>;
onTeamClick: (id: string) => void;
onViewFullLeaderboard: () => void;
}
export function TeamLeaderboardPreview({
topTeams,
onTeamClick,
onViewFullLeaderboard
}: TeamLeaderboardPreviewProps) {
const getMedalColor = (position: number) => {
switch (position) {
case 0: return '#facc15';
case 1: return '#d1d5db';
case 2: return '#d97706';
default: return '#6b7280';
}
};
const getMedalBg = (position: number) => {
switch (position) {
case 0: return 'rgba(250, 204, 21, 0.1)';
case 1: return 'rgba(209, 213, 219, 0.1)';
case 2: return 'rgba(217, 119, 6, 0.1)';
default: return 'rgba(38, 38, 38, 0.5)';
}
};
const getMedalBorder = (position: number) => {
switch (position) {
case 0: return 'rgba(250, 204, 21, 0.3)';
case 1: return 'rgba(209, 213, 219, 0.3)';
case 2: return 'rgba(217, 119, 6, 0.3)';
default: return 'rgba(38, 38, 38, 1)';
}
};
if (topTeams.length === 0) return null;
return (
<Box style={{ marginBottom: '3rem' }}>
{/* Header */}
<Stack direction="row" align="center" justify="between" style={{ marginBottom: '1rem' }}>
<Stack direction="row" align="center" gap={3}>
<Box style={{ display: 'flex', height: '2.75rem', width: '2.75rem', alignItems: 'center', justifyContent: 'center', borderRadius: '0.75rem', background: 'linear-gradient(to bottom right, rgba(250, 204, 21, 0.2), rgba(217, 119, 6, 0.1))', border: '1px solid rgba(250, 204, 21, 0.3)' }}>
<Award style={{ width: '1.25rem', height: '1.25rem', color: '#facc15' }} />
</Box>
<Box>
<Heading level={2}>Top Teams</Heading>
<Text size="sm" color="text-gray-500">Highest rated racing teams</Text>
</Box>
</Stack>
<Button
variant="secondary"
onClick={onViewFullLeaderboard}
style={{ display: 'flex', alignItems: 'center', gap: '0.5rem', fontSize: '0.875rem' }}
>
View Full Leaderboard
<ChevronRight style={{ width: '1rem', height: '1rem' }} />
</Button>
</Stack>
{/* Compact Leaderboard */}
<Box style={{ borderRadius: '0.75rem', backgroundColor: 'rgba(38, 38, 38, 0.3)', border: '1px solid #262626', overflow: 'hidden' }}>
<Stack gap={0}>
{topTeams.map((team, index) => (
<Box
key={team.id}
as="button"
type="button"
onClick={() => onTeamClick(team.id)}
style={{
display: 'flex',
alignItems: 'center',
gap: '1rem',
padding: '0.75rem 1rem',
width: '100%',
textAlign: 'left',
backgroundColor: 'transparent',
border: 'none',
cursor: 'pointer',
borderBottom: index < topTeams.length - 1 ? '1px solid rgba(38, 38, 38, 0.5)' : 'none'
}}
>
{/* Position */}
<Box
style={{
display: 'flex',
height: '2rem',
width: '2rem',
alignItems: 'center',
justifyContent: 'center',
borderRadius: '9999px',
fontSize: '0.75rem',
fontWeight: 'bold',
border: `1px solid ${getMedalBorder(index)}`,
backgroundColor: getMedalBg(index),
color: getMedalColor(index)
}}
>
{index < 3 ? (
<Crown style={{ width: '0.875rem', height: '0.875rem' }} />
) : (
index + 1
)}
</Box>
{/* Team Info */}
<Box style={{ display: 'flex', height: '2.25rem', width: '2.25rem', alignItems: 'center', justifyContent: 'center', borderRadius: '0.5rem', backgroundColor: '#262626', border: '1px solid #262626', overflow: 'hidden' }}>
<Image
src={team.logoUrl || getMediaUrl('team-logo', team.id)}
alt={team.name}
width={36}
height={36}
style={{ width: '100%', height: '100%', objectFit: 'cover' }}
/>
</Box>
<Box style={{ flex: 1, minWidth: 0 }}>
<Text weight="medium" color="text-white" style={{ display: 'block', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>
{team.name}
</Text>
<Stack direction="row" align="center" gap={2} wrap style={{ marginTop: '0.125rem' }}>
{team.category && (
<Stack direction="row" align="center" gap={1}>
<Box style={{ width: '0.375rem', height: '0.375rem', borderRadius: '9999px', backgroundColor: '#a855f7' }} />
<Text size="xs" color="text-purple-400">{team.category}</Text>
</Stack>
)}
<Stack direction="row" align="center" gap={1}>
<Users style={{ width: '0.75rem', height: '0.75rem', color: '#737373' }} />
<Text size="xs" color="text-gray-500">{team.memberCount}</Text>
</Stack>
<Stack direction="row" align="center" gap={1}>
<Trophy style={{ width: '0.75rem', height: '0.75rem', color: '#737373' }} />
<Text size="xs" color="text-gray-500">{team.totalWins} wins</Text>
</Stack>
{team.isRecruiting && (
<Stack direction="row" align="center" gap={1}>
<Box style={{ width: '0.375rem', height: '0.375rem', borderRadius: '9999px', backgroundColor: '#10b981' }} />
<Text size="xs" color="text-performance-green">Recruiting</Text>
</Stack>
)}
</Stack>
</Box>
{/* Rating */}
<Box style={{ textAlign: 'right' }}>
<Text font="mono" weight="semibold" color="text-purple-400" style={{ display: 'block' }}>
{typeof team.rating === 'number' ? Math.round(team.rating).toLocaleString() : '—'}
</Text>
<Text size="xs" color="text-gray-500">Rating</Text>
</Box>
</Box>
))}
</Stack>
</Box>
</Box>
);
}

View File

@@ -1,30 +0,0 @@
/**
* TeamLogo
*
* Pure UI component for displaying team logos.
* Renders an optimized image with fallback on error.
*/
import Image from 'next/image';
export interface TeamLogoProps {
teamId: string;
alt: string;
className?: string;
}
export function TeamLogo({ teamId, alt, className = '' }: TeamLogoProps) {
return (
<Image
src={`/media/teams/${teamId}/logo`}
alt={alt}
width={100}
height={100}
className={`object-contain ${className}`}
onError={(e) => {
// Fallback to default logo
(e.target as HTMLImageElement).src = '/default-team-logo.png';
}}
/>
);
}

View File

@@ -1,6 +1,6 @@
import React from 'react';
import Link from 'next/link';
import { Users, ChevronRight } from 'lucide-react';
import { TeamMembershipCard as UiTeamMembershipCard } from '@/ui/TeamMembershipCard';
import { routes } from '@/lib/routing/RouteConfig';
interface TeamMembership {
teamId: string;
@@ -14,29 +14,13 @@ interface TeamMembershipCardProps {
membership: TeamMembership;
}
export default function TeamMembershipCard({ membership }: TeamMembershipCardProps) {
export function TeamMembershipCard({ membership }: TeamMembershipCardProps) {
return (
<Link
href={`/teams/${membership.teamId}`}
className="flex items-center gap-4 p-4 rounded-xl bg-iron-gray/30 border border-charcoal-outline hover:border-purple-400/30 hover:bg-iron-gray/50 transition-all group"
>
<div className="flex h-12 w-12 items-center justify-center rounded-xl bg-purple-600/20 border border-purple-600/30">
<Users className="w-6 h-6 text-purple-400" />
</div>
<div className="flex-1 min-w-0">
<p className="text-white font-semibold truncate group-hover:text-purple-400 transition-colors">
{membership.teamName}
</p>
<div className="flex items-center gap-2 text-xs text-gray-400">
<span className="px-2 py-0.5 rounded-full bg-purple-600/20 text-purple-400 capitalize">
{membership.role}
</span>
<span>
Since {new Date(membership.joinedAt).toLocaleDateString('en-US', { month: 'short', year: 'numeric' })}
</span>
</div>
</div>
<ChevronRight className="w-4 h-4 text-gray-500 group-hover:text-purple-400 transition-colors" />
</Link>
<UiTeamMembershipCard
teamName={membership.teamName}
role={membership.role}
joinedAt={membership.joinedAt}
href={routes.team.detail(membership.teamId)}
/>
);
}
}

View File

@@ -1,118 +0,0 @@
'use client';
import React from 'react';
import { Medal, Users } from 'lucide-react';
import { Box } from '@/ui/Box';
import { Stack } from '@/ui/Stack';
import { Text } from '@/ui/Text';
import { Image } from '@/ui/Image';
import { Icon } from '@/ui/Icon';
import { Surface } from '@/ui/Surface';
import { getMediaUrl } from '@/lib/utilities/media';
interface Team {
id: string;
name: string;
logoUrl?: string;
performanceLevel: string;
category?: string;
region?: string;
languages?: string[];
isRecruiting?: boolean;
memberCount: number;
totalWins: number;
totalRaces: number;
}
interface TeamRankingsTableProps {
teams: Team[];
sortBy: string;
onTeamClick: (id: string) => void;
}
export function TeamRankingsTable({ teams, sortBy, onTeamClick }: TeamRankingsTableProps) {
return (
<Surface variant="muted" rounded="xl" border className="overflow-hidden">
{/* Table Header */}
<Box display="grid" className="grid-cols-12 gap-4 px-4 py-3 bg-iron-gray/50 border-b border-charcoal-outline text-[10px] font-medium text-gray-500 uppercase tracking-wider">
<Box className="col-span-1 text-center">Rank</Box>
<Box className="col-span-5">Team</Box>
<Box className="col-span-2 text-center hidden lg:block">Members</Box>
<Box className="col-span-2 text-center">Rating</Box>
<Box className="col-span-2 text-center">Wins</Box>
</Box>
{/* Table Body */}
<Stack gap={0}>
{teams.map((team, index) => {
return (
<Box
key={team.id}
as="button"
type="button"
onClick={() => onTeamClick(team.id)}
display="grid"
className={`grid-cols-12 gap-4 p-4 w-full text-left bg-transparent border-0 cursor-pointer hover:bg-iron-gray/20 transition-colors ${
index < teams.length - 1 ? 'border-b border-charcoal-outline/50' : ''
}`}
>
{/* Position */}
<Box className="col-span-1 flex items-center justify-center">
<Box width={9} height={9} rounded="full" display="flex" center backgroundColor="charcoal-outline">
{index < 3 ? <Icon icon={Medal} size={4} color={index === 0 ? 'text-yellow-400' : index === 1 ? 'text-gray-300' : 'text-amber-600'} /> : <Text size="xs" color="text-gray-400">{index + 1}</Text>}
</Box>
</Box>
{/* Team Info */}
<Box className="col-span-5 flex items-center gap-3">
<Box width={10} height={10} rounded="lg" className="overflow-hidden" border borderColor="charcoal-outline">
<Image
src={team.logoUrl || getMediaUrl('team-logo', team.id)}
alt={team.name}
width={40}
height={40}
className="w-full h-full object-cover"
/>
</Box>
<Box className="min-w-0 flex-1">
<Text weight="semibold" color="text-white" block className="truncate">{team.name}</Text>
<Stack direction="row" align="center" gap={2} mt={1} wrap>
<Text size="xs" color="text-gray-500">{team.performanceLevel}</Text>
{team.category && (
<Stack direction="row" align="center" gap={1}>
<Box width={1.5} height={1.5} rounded="full" backgroundColor="primary-blue" opacity={0.5} />
<Text size="xs" color="text-primary-blue">{team.category}</Text>
</Stack>
)}
</Stack>
</Box>
</Box>
{/* Members */}
<Box className="col-span-2 flex items-center justify-center hidden lg:flex">
<Stack direction="row" align="center" gap={1.5}>
<Icon icon={Users} size={3.5} color="text-gray-500" />
<Text size="sm" color="text-gray-400">{team.memberCount}</Text>
</Stack>
</Box>
{/* Rating */}
<Box className="col-span-2 flex items-center justify-center">
<Text font="mono" weight="semibold" color={sortBy === 'rating' ? 'text-primary-blue' : 'text-white'}>
0
</Text>
</Box>
{/* Wins */}
<Box className="col-span-2 flex items-center justify-center">
<Text font="mono" weight="semibold" color={sortBy === 'wins' ? 'text-primary-blue' : 'text-white'}>
{team.totalWins}
</Text>
</Box>
</Box>
);
})}
</Stack>
</Surface>
);
}

View File

@@ -1,21 +1,23 @@
'use client';
import React, { useMemo, useState } from 'react';
import { Card } from '@/ui/Card';
import { DriverIdentity } from '@/components/drivers/DriverIdentity';
import { useTeamRoster } from "@/lib/hooks/team";
import { useState } from 'react';
import { useTeamRoster } from "@/hooks/team/useTeamRoster";
import type { DriverViewModel } from '@/lib/view-models/DriverViewModel';
import { Box } from '@/ui/Box';
import { Stack } from '@/ui/Stack';
import { Text } from '@/ui/Text';
import { Heading } from '@/ui/Heading';
import { Select } from '@/ui/Select';
import { Surface } from '@/ui/Surface';
import { Badge } from '@/ui/Badge';
import { Button } from '@/ui/Button';
import { routes } from '@/lib/routing/RouteConfig';
import { TeamRosterList } from '@/ui/TeamRosterList';
import { TeamRosterItem } from '@/ui/TeamRosterItem';
import { MinimalEmptyState } from '@/ui/EmptyState';
import { sortMembers } from '@/lib/utilities/roster-utils';
type TeamRole = 'owner' | 'admin' | 'member';
type TeamMemberRole = 'owner' | 'manager' | 'member';
export type TeamRole = 'owner' | 'admin' | 'member';
export type TeamMemberRole = 'owner' | 'manager' | 'member';
interface TeamRosterProps {
teamId: string;
@@ -32,6 +34,18 @@ interface TeamRosterProps {
onChangeRole?: (driverId: string, newRole: TeamRole) => void;
}
interface TeamMember {
driver: {
id: string;
name: string;
country: string;
};
role: TeamMemberRole;
joinedAt: string;
rating: number | null;
overallRank: number | null;
}
export function TeamRoster({
teamId,
memberships,
@@ -44,51 +58,25 @@ export function TeamRoster({
// Use hook for data fetching
const { data: teamMembers = [], isLoading: loading } = useTeamRoster(memberships);
const getRoleLabel = (role: TeamRole | TeamMemberRole) => {
const getRoleLabel = useMemo(() => (role: TeamRole | TeamMemberRole) => {
// Convert manager to admin for display
const displayRole = role === 'manager' ? 'admin' : role;
return displayRole.charAt(0).toUpperCase() + displayRole.slice(1);
};
}, []);
function getRoleOrder(role: TeamMemberRole): number {
switch (role) {
case 'owner':
return 0;
case 'manager':
return 1;
case 'member':
return 2;
default:
return 3;
}
}
const sortedMembers = useMemo(() => {
return sortMembers(teamMembers as unknown as TeamMember[], sortBy);
}, [teamMembers, sortBy]);
const sortedMembers = [...teamMembers].sort((a, b) => {
switch (sortBy) {
case 'rating': {
const ratingA = a.rating ?? 0;
const ratingB = b.rating ?? 0;
return ratingB - ratingA;
}
case 'role': {
return getRoleOrder(a.role) - getRoleOrder(b.role);
}
case 'name': {
return a.driver.name.localeCompare(b.driver.name);
}
default:
return 0;
}
});
const teamAverageRating = teamMembers.length > 0
? teamMembers.reduce((sum: number, m: any) => sum + (m.rating || 0), 0) / teamMembers.length
: 0;
const teamAverageRating = useMemo(() => {
if (teamMembers.length === 0) return 0;
return teamMembers.reduce((sum: number, m: { rating?: number | null }) => sum + (m.rating || 0), 0) / teamMembers.length;
}, [teamMembers]);
if (loading) {
return (
<Card>
<Box textAlign="center" py={8}>
<Box display="flex" justifyContent="center" py={8}>
<Text color="text-gray-400">Loading roster...</Text>
</Box>
</Card>
@@ -97,7 +85,7 @@ export function TeamRoster({
return (
<Card>
<Stack direction="row" align="center" justify="between" mb={6} wrap>
<Stack direction="row" align="center" justify="between" mb={6} wrap gap={4}>
<Box>
<Heading level={3}>Team Roster</Heading>
<Text size="sm" color="text-gray-400" block mt={1}>
@@ -108,7 +96,7 @@ export function TeamRoster({
<Stack direction="row" align="center" gap={2}>
<Text size="sm" color="text-gray-400">Sort by:</Text>
<Box width={32}>
<Box width="32">
<Select
value={sortBy}
onChange={(e) => setSortBy(e.target.value as typeof sortBy)}
@@ -117,61 +105,32 @@ export function TeamRoster({
{ value: 'role', label: 'Role' },
{ value: 'name', label: 'Name' },
]}
className="py-1 text-sm"
/>
</Box>
</Stack>
</Stack>
<Stack gap={3}>
{sortedMembers.map((member) => {
const { driver, role, joinedAt, rating, overallRank } = member;
{sortedMembers.length > 0 ? (
<TeamRosterList>
{sortedMembers.map((member) => {
const { driver, role, joinedAt, rating, overallRank } = member;
// Convert manager to admin for display purposes
const displayRole: TeamRole = role === 'manager' ? 'admin' : (role as TeamRole);
const canManageMembership = isAdmin && role !== 'owner';
// Convert manager to admin for display purposes
const displayRole: TeamRole = role === 'manager' ? 'admin' : (role as TeamRole);
const canManageMembership = isAdmin && role !== 'owner';
return (
<Surface
key={driver.id}
variant="dark"
rounded="lg"
border
padding={4}
>
<Stack direction="row" align="center" justify="between" wrap gap={4}>
<DriverIdentity
driver={driver as DriverViewModel}
href={`/drivers/${driver.id}?from=team&teamId=${teamId}`}
contextLabel={getRoleLabel(role)}
meta={
<Text size="xs" color="text-gray-400">
{driver.country} Joined {new Date(joinedAt).toLocaleDateString()}
</Text>
}
size="md"
/>
{rating !== null && (
<Stack direction="row" align="center" gap={6}>
<Box textAlign="center">
<Text size="lg" weight="bold" color="text-primary-blue" block>
{rating}
</Text>
<Text size="xs" color="text-gray-400">Rating</Text>
</Box>
{overallRank !== null && (
<Box textAlign="center">
<Text size="sm" color="text-gray-300" block>#{overallRank}</Text>
<Text size="xs" color="text-gray-500">Rank</Text>
</Box>
)}
</Stack>
)}
{canManageMembership && (
<Stack direction="row" align="center" gap={2}>
<Box width={32}>
return (
<TeamRosterItem
key={driver.id}
driver={driver as DriverViewModel}
href={`${routes.driver.detail(driver.id)}?from=team&teamId=${teamId}`}
roleLabel={getRoleLabel(role)}
joinedAt={joinedAt}
rating={rating}
overallRank={overallRank}
actions={canManageMembership ? (
<>
<Box width="32">
<Select
value={displayRole}
onChange={(e) =>
@@ -181,7 +140,6 @@ export function TeamRoster({
{ value: 'member', label: 'Member' },
{ value: 'admin', label: 'Admin' },
]}
className="text-sm"
/>
</Box>
@@ -192,18 +150,17 @@ export function TeamRoster({
>
Remove
</Button>
</Stack>
)}
</Stack>
</Surface>
);
})}
</Stack>
{memberships.length === 0 && (
<Box textAlign="center" py={8}>
<Text color="text-gray-400">No team members yet.</Text>
</Box>
</>
) : undefined}
/>
);
})}
</TeamRosterList>
) : (
<MinimalEmptyState
title="No team members yet"
description="When drivers join your team, they will appear here."
/>
)}
</Card>
);

View File

@@ -1,30 +0,0 @@
'use client';
import { Search } from 'lucide-react';
import { Input } from '@/ui/Input';
import { Box } from '@/ui/Box';
import { Stack } from '@/ui/Stack';
import { Icon } from '@/ui/Icon';
interface TeamSearchBarProps {
searchQuery: string;
onSearchChange: (query: string) => void;
}
export function TeamSearchBar({ searchQuery, onSearchChange }: TeamSearchBarProps) {
return (
<Box id="teams-list" mb={6} className="scroll-mt-8">
<Stack direction="row" gap={4} wrap>
<Box flex={1}>
<Input
type="text"
placeholder="Search teams by name, description, region, or language..."
value={searchQuery}
onChange={(e) => onSearchChange(e.target.value)}
icon={<Icon icon={Search} size={5} color="text-gray-500" />}
/>
</Box>
</Stack>
</Box>
);
}

View File

@@ -1,14 +1,13 @@
'use client';
import { Card } from '@/ui/Card';
import { useTeamStandings } from "@/lib/hooks/team";
import { Stack } from '@/ui/Stack';
import { useTeamStandings } from "@/hooks/team/useTeamStandings";
import { Box } from '@/ui/Box';
import { Text } from '@/ui/Text';
import { Heading } from '@/ui/Heading';
import { Badge } from '@/ui/Badge';
import { Grid } from '@/ui/Grid';
import { Surface } from '@/ui/Surface';
import { StandingsList } from '@/ui/StandingsList';
import { LoadingWrapper } from '@/ui/LoadingWrapper';
import { EmptyState } from '@/ui/EmptyState';
import { Trophy } from 'lucide-react';
interface TeamStandingsProps {
teamId: string;
@@ -21,9 +20,7 @@ export function TeamStandings({ teamId, leagues }: TeamStandingsProps) {
if (loading) {
return (
<Card>
<Box textAlign="center" py={8}>
<Text color="text-gray-400">Loading standings...</Text>
</Box>
<LoadingWrapper message="Loading standings..." />
</Card>
);
}
@@ -36,54 +33,14 @@ export function TeamStandings({ teamId, leagues }: TeamStandingsProps) {
</Heading>
</Box>
<Stack gap={4}>
{standings.map((standing: any) => (
<Surface
key={standing.leagueId}
variant="dark"
rounded="lg"
border
padding={4}
>
<Stack direction="row" align="center" justify="between" mb={3}>
<Heading level={4}>
{standing.leagueName}
</Heading>
<Badge variant="primary">
P{standing.position}
</Badge>
</Stack>
<Grid cols={3} gap={4}>
<Box textAlign="center">
<Text size="2xl" weight="bold" color="text-white" block>
{standing.points}
</Text>
<Text size="xs" color="text-gray-400">Points</Text>
</Box>
<Box textAlign="center">
<Text size="2xl" weight="bold" color="text-white" block>
{standing.wins}
</Text>
<Text size="xs" color="text-gray-400">Wins</Text>
</Box>
<Box textAlign="center">
<Text size="2xl" weight="bold" color="text-white" block>
{standing.racesCompleted}
</Text>
<Text size="xs" color="text-gray-400">Races</Text>
</Box>
</Grid>
</Surface>
))}
</Stack>
{standings.length === 0 && (
<Box textAlign="center" py={8}>
<Text color="text-gray-400">
No standings available yet.
</Text>
</Box>
{standings.length > 0 ? (
<StandingsList standings={standings} />
) : (
<EmptyState
icon={Trophy}
title="No standings available"
description="This team hasn't participated in any leagues yet."
/>
)}
</Card>
);

View File

@@ -1,160 +0,0 @@
import Image from 'next/image';
import { Trophy, Crown, Users } from 'lucide-react';
import type { TeamSummaryViewModel } from '@/lib/view-models/TeamSummaryViewModel';
import { getMediaUrl } from '@/lib/utilities/media';
import { Box } from '@/ui/Box';
import { Stack } from '@/ui/Stack';
import { Text } from '@/ui/Text';
import { Heading } from '@/ui/Heading';
import { Icon } from '@/ui/Icon';
import { Surface } from '@/ui/Surface';
import { Button } from '@/ui/Button';
interface TopThreePodiumProps {
teams: TeamSummaryViewModel[];
onClick: (id: string) => void;
}
export function TopThreePodium({ teams, onClick }: TopThreePodiumProps) {
const top3 = teams.slice(0, 3) as [TeamSummaryViewModel, TeamSummaryViewModel, TeamSummaryViewModel];
if (teams.length < 3) return null;
// Display order: 2nd, 1st, 3rd
const podiumOrder: [TeamSummaryViewModel, TeamSummaryViewModel, TeamSummaryViewModel] = [
top3[1],
top3[0],
top3[2],
];
const podiumHeights = ['h-28', 'h-36', 'h-20'];
const podiumPositions = [2, 1, 3];
const getPositionColor = (position: number) => {
switch (position) {
case 1:
return 'text-yellow-400';
case 2:
return 'text-gray-300';
case 3:
return 'text-amber-600';
default:
return 'text-gray-500';
}
};
const getVariant = (position: number): any => {
switch (position) {
case 1:
return 'gradient-gold';
case 2:
return 'default';
case 3:
return 'gradient-purple';
default:
return 'muted';
}
};
return (
<Surface variant="muted" rounded="2xl" border padding={8} mb={10}>
<Box display="flex" center mb={8}>
<Stack direction="row" align="center" gap={2}>
<Icon icon={Trophy} size={6} color="text-yellow-400" />
<Heading level={2}>Top 3 Teams</Heading>
</Stack>
</Box>
<Stack direction="row" align="end" justify="center" gap={8}>
{podiumOrder.map((team, index) => {
const position = podiumPositions[index] ?? 0;
return (
<Stack key={team.id} align="center">
{/* Team card */}
<Button
variant="ghost"
onClick={() => onClick(team.id)}
className="p-0 h-auto hover:scale-105 transition-transform"
>
<Surface
variant={getVariant(position)}
rounded="xl"
border
padding={4}
position="relative"
mb={4}
>
{/* Crown for 1st place */}
{position === 1 && (
<Box position="absolute" top="-4" left="50%" style={{ transform: 'translateX(-50%)' }}>
<Box position="relative">
<Icon icon={Crown} size={8} color="text-yellow-400" className="animate-pulse" />
<Box position="absolute" inset="0" backgroundColor="yellow-400" opacity={0.3} className="blur-md rounded-full" />
</Box>
</Box>
)}
{/* Team logo */}
<Box height={20} width={20} display="flex" center rounded="xl" backgroundColor="charcoal-outline" border borderColor="charcoal-outline" className="overflow-hidden" mb={3}>
<Image
src={team.logoUrl || getMediaUrl('team-logo', team.id)}
alt={team.name}
width={80}
height={80}
className="w-full h-full object-cover"
/>
</Box>
{/* Team name */}
<Text weight="bold" size="sm" color="text-white" align="center" block className="max-w-[120px] truncate group-hover:text-primary-blue transition-colors">
{team.name}
</Text>
{/* Category */}
{team.category && (
<Text size="xs" color="text-primary-blue" align="center" block mt={1}>
{team.category}
</Text>
)}
{/* Rating placeholder */}
<Text size="xl" weight="bold" className={`${getPositionColor(position)}`} align="center" block mt={1}>
</Text>
{/* Stats row */}
<Stack direction="row" align="center" justify="center" gap={3} mt={2}>
<Stack direction="row" align="center" gap={1}>
<Icon icon={Trophy} size={3} color="text-performance-green" />
<Text size="xs" color="text-gray-400">{team.totalWins}</Text>
</Stack>
<Stack direction="row" align="center" gap={1}>
<Icon icon={Users} size={3} color="text-primary-blue" />
<Text size="xs" color="text-gray-400">{team.memberCount}</Text>
</Stack>
</Stack>
</Surface>
</Button>
{/* Podium stand */}
<Surface
variant={getVariant(position)}
rounded="none"
className={`rounded-t-lg ${podiumHeights[index]}`}
border
width="28"
display="flex"
padding={3}
>
<Box display="flex" center fullWidth>
<Text size="3xl" weight="bold" className={getPositionColor(position)}>
{position}
</Text>
</Box>
</Surface>
</Stack>
);
})}
</Stack>
</Surface>
);
}

View File

@@ -1,75 +0,0 @@
import {
Handshake,
MessageCircle,
Calendar,
Trophy,
LucideIcon,
} from 'lucide-react';
import { Box } from '@/ui/Box';
import { Text } from '@/ui/Text';
import { Heading } from '@/ui/Heading';
import { Grid } from '@/ui/Grid';
import { Surface } from '@/ui/Surface';
import { Icon } from '@/ui/Icon';
interface Benefit {
icon: LucideIcon;
title: string;
description: string;
}
export function WhyJoinTeamSection() {
const benefits: Benefit[] = [
{
icon: Handshake,
title: 'Shared Strategy',
description: 'Develop setups together, share telemetry, and coordinate pit strategies for endurance races.',
},
{
icon: MessageCircle,
title: 'Team Communication',
description: 'Discord integration, voice chat during races, and dedicated team channels.',
},
{
icon: Calendar,
title: 'Coordinated Schedule',
description: 'Team calendars, practice sessions, and organized race attendance.',
},
{
icon: Trophy,
title: 'Team Championships',
description: 'Compete in team-based leagues and build your collective reputation.',
},
];
return (
<Box mb={12}>
<Box textAlign="center" mb={8}>
<Box mb={2}>
<Heading level={2}>Why Join a Team?</Heading>
</Box>
<Text color="text-gray-400">Racing is better when you have teammates to share the journey</Text>
</Box>
<Grid cols={4} gap={4}>
{benefits.map((benefit) => (
<Surface
key={benefit.title}
variant="muted"
rounded="xl"
border
padding={5}
>
<Box height={10} width={10} display="flex" center rounded="lg" backgroundColor="primary-blue" opacity={0.1} border borderColor="primary-blue" mb={3}>
<Icon icon={benefit.icon} size={5} color="text-primary-blue" />
</Box>
<Box mb={1}>
<Heading level={3}>{benefit.title}</Heading>
</Box>
<Text size="sm" color="text-gray-500">{benefit.description}</Text>
</Surface>
))}
</Grid>
</Box>
);
}