website refactor
This commit is contained in:
@@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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';
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -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)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user