website refactor
This commit is contained in:
@@ -2,21 +2,22 @@
|
||||
|
||||
import { JoinRequestItem } from '@/components/leagues/JoinRequestItem';
|
||||
import { JoinRequestList } from '@/components/leagues/JoinRequestList';
|
||||
import { MinimalEmptyState } from '@/components/shared/state/EmptyState';
|
||||
import { EmptyState } from '@/ui/EmptyState';
|
||||
import { LoadingWrapper } from '@/ui/LoadingWrapper';
|
||||
import { useApproveJoinRequest } from "@/hooks/team/useApproveJoinRequest";
|
||||
import { useRejectJoinRequest } from "@/hooks/team/useRejectJoinRequest";
|
||||
import { useTeamJoinRequests } from "@/hooks/team/useTeamJoinRequests";
|
||||
import { useUpdateTeam } from "@/hooks/team/useUpdateTeam";
|
||||
import type { TeamJoinRequestViewModel } from '@/lib/view-models/TeamJoinRequestViewModel';
|
||||
import { Button } from '@/ui/Button';
|
||||
import { Card } from '@/ui/Card';
|
||||
import { Panel } from '@/ui/Panel';
|
||||
import { DangerZone } from '@/ui/DangerZone';
|
||||
import { Heading } from '@/ui/Heading';
|
||||
import { Input } from '@/ui/Input';
|
||||
import { Stack } from '@/ui/primitives/Stack';
|
||||
import { Text } from '@/ui/Text';
|
||||
import { TextArea } from '@/ui/TextArea';
|
||||
import { SectionHeader } from '@/ui/SectionHeader';
|
||||
import { useState } from 'react';
|
||||
import React from 'react';
|
||||
|
||||
interface TeamAdminProps {
|
||||
team: {
|
||||
@@ -74,14 +75,10 @@ export function TeamAdmin({ team, onUpdate }: TeamAdminProps) {
|
||||
});
|
||||
|
||||
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 = () => {
|
||||
// Note: The current API doesn't support rejecting specific requests
|
||||
// This would need the requestId to be passed to the service
|
||||
rejectJoinRequestMutation.mutate();
|
||||
};
|
||||
|
||||
@@ -97,42 +94,30 @@ export function TeamAdmin({ team, onUpdate }: TeamAdminProps) {
|
||||
};
|
||||
|
||||
return (
|
||||
<Stack gap={6}>
|
||||
<Card>
|
||||
<Stack display="flex" alignItems="center" justifyContent="between" mb={6}>
|
||||
<Heading level={3}>Team Settings</Heading>
|
||||
{!editMode && (
|
||||
<Button variant="secondary" onClick={() => setEditMode(true)}>
|
||||
Edit Details
|
||||
</Button>
|
||||
)}
|
||||
</Stack>
|
||||
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '1.5rem' }}>
|
||||
<Panel
|
||||
title="Team Settings"
|
||||
actions={!editMode && (
|
||||
<Button variant="secondary" size="sm" onClick={() => setEditMode(true)}>
|
||||
Edit Details
|
||||
</Button>
|
||||
)}
|
||||
>
|
||||
{editMode ? (
|
||||
<Stack gap={4}>
|
||||
<Stack>
|
||||
<Text as="label" size="sm" weight="medium" color="text-gray-400" block mb={2}>
|
||||
Team Name
|
||||
</Text>
|
||||
<Input
|
||||
type="text"
|
||||
value={editedTeam.name}
|
||||
onChange={(e) => setEditedTeam({ ...editedTeam, name: e.target.value })}
|
||||
/>
|
||||
</Stack>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '1rem' }}>
|
||||
<Input
|
||||
label="Team Name"
|
||||
value={editedTeam.name}
|
||||
onChange={(e) => setEditedTeam({ ...editedTeam, name: e.target.value })}
|
||||
/>
|
||||
|
||||
<Stack>
|
||||
<Text as="label" size="sm" weight="medium" color="text-gray-400" block mb={2}>
|
||||
Team Tag
|
||||
</Text>
|
||||
<Input
|
||||
type="text"
|
||||
value={editedTeam.tag}
|
||||
onChange={(e) => setEditedTeam({ ...editedTeam, tag: e.target.value })}
|
||||
maxLength={4}
|
||||
/>
|
||||
<Text size="xs" color="text-gray-500" block mt={1}>Max 4 characters</Text>
|
||||
</Stack>
|
||||
<Input
|
||||
label="Team Tag"
|
||||
value={editedTeam.tag}
|
||||
onChange={(e) => setEditedTeam({ ...editedTeam, tag: e.target.value })}
|
||||
maxLength={4}
|
||||
hint="Max 4 characters"
|
||||
/>
|
||||
|
||||
<TextArea
|
||||
label="Description"
|
||||
@@ -141,7 +126,7 @@ export function TeamAdmin({ team, onUpdate }: TeamAdminProps) {
|
||||
onChange={(e) => setEditedTeam({ ...editedTeam, description: e.target.value })}
|
||||
/>
|
||||
|
||||
<Stack direction="row" gap={2}>
|
||||
<div style={{ display: 'flex', gap: '0.5rem' }}>
|
||||
<Button variant="primary" onClick={handleSaveChanges} disabled={updateTeamMutation.isPending}>
|
||||
{updateTeamMutation.isPending ? 'Saving...' : 'Save Changes'}
|
||||
</Button>
|
||||
@@ -158,33 +143,29 @@ export function TeamAdmin({ team, onUpdate }: TeamAdminProps) {
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<Stack gap={4}>
|
||||
<Stack>
|
||||
<Text size="sm" color="text-gray-400" block>Team Name</Text>
|
||||
<Text color="text-white" weight="medium" block>{team.name}</Text>
|
||||
</Stack>
|
||||
<Stack>
|
||||
<Text size="sm" color="text-gray-400" block>Team Tag</Text>
|
||||
<Text color="text-white" weight="medium" block>{team.tag}</Text>
|
||||
</Stack>
|
||||
<Stack>
|
||||
<Text size="sm" color="text-gray-400" block>Description</Text>
|
||||
<Text color="text-white" block>{team.description}</Text>
|
||||
</Stack>
|
||||
</Stack>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '1rem' }}>
|
||||
<div>
|
||||
<Text size="sm" variant="low" block marginBottom={1}>Team Name</Text>
|
||||
<Text variant="high" weight="medium" block>{team.name}</Text>
|
||||
</div>
|
||||
<div>
|
||||
<Text size="sm" variant="low" block marginBottom={1}>Team Tag</Text>
|
||||
<Text variant="high" weight="medium" block>{team.tag}</Text>
|
||||
</div>
|
||||
<div>
|
||||
<Text size="sm" variant="low" block marginBottom={1}>Description</Text>
|
||||
<Text variant="high" block>{team.description}</Text>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<Heading level={3} mb={6}>Join Requests</Heading>
|
||||
</Panel>
|
||||
|
||||
<Panel title="Join Requests">
|
||||
{loading ? (
|
||||
<Stack textAlign="center" py={8}>
|
||||
<Text color="text-gray-400">Loading requests...</Text>
|
||||
</Stack>
|
||||
<LoadingWrapper variant="spinner" message="Loading requests..." />
|
||||
) : joinRequests.length > 0 ? (
|
||||
<JoinRequestList>
|
||||
{joinRequests.map((request: TeamJoinRequestViewModel) => (
|
||||
@@ -200,12 +181,13 @@ export function TeamAdmin({ team, onUpdate }: TeamAdminProps) {
|
||||
))}
|
||||
</JoinRequestList>
|
||||
) : (
|
||||
<MinimalEmptyState
|
||||
<EmptyState
|
||||
title="No pending join requests"
|
||||
description="When drivers request to join your team, they will appear here."
|
||||
variant="minimal"
|
||||
/>
|
||||
)}
|
||||
</Card>
|
||||
</Panel>
|
||||
|
||||
<DangerZone
|
||||
title="Disband Team"
|
||||
@@ -215,6 +197,6 @@ export function TeamAdmin({ team, onUpdate }: TeamAdminProps) {
|
||||
Disband Team (Coming Soon)
|
||||
</Button>
|
||||
</DangerZone>
|
||||
</Stack>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,19 +1,9 @@
|
||||
'use client';
|
||||
|
||||
|
||||
import { Badge } from '@/ui/Badge';
|
||||
import { Card } from '@/ui/Card';
|
||||
import { Heading } from '@/ui/Heading';
|
||||
import { Icon } from '@/ui/Icon';
|
||||
import { Image } from '@/ui/Image';
|
||||
import { PlaceholderImage } from '@/ui/PlaceholderImage';
|
||||
import { Stack } from '@/ui/primitives/Stack';
|
||||
import { Text } from '@/ui/Text';
|
||||
import {
|
||||
ChevronRight,
|
||||
Globe,
|
||||
Users
|
||||
} from 'lucide-react';
|
||||
import { ReactNode } from 'react';
|
||||
import { TeamCard as UITeamCard } from '@/ui/TeamCard';
|
||||
import React, { ReactNode } from 'react';
|
||||
|
||||
interface TeamCardProps {
|
||||
name: string;
|
||||
@@ -41,132 +31,37 @@ export function TeamCard({
|
||||
categoryBadge,
|
||||
region,
|
||||
languagesContent,
|
||||
statsContent: _statsContent,
|
||||
onClick,
|
||||
}: TeamCardProps) {
|
||||
return (
|
||||
<Stack onClick={onClick} h="full" cursor={onClick ? 'pointer' : 'default'} className="group">
|
||||
<Card h="full" p={0} display="flex" flexDirection="col" overflow="hidden" className="bg-panel-gray/40 border-border-gray/50 hover:border-primary-accent/30 hover:bg-panel-gray/60 transition-all duration-300">
|
||||
{/* Header with Logo */}
|
||||
<Stack p={5} pb={0}>
|
||||
<Stack direction="row" align="start" gap={4}>
|
||||
{/* Logo */}
|
||||
<Stack
|
||||
w="16"
|
||||
h="16"
|
||||
rounded="none"
|
||||
bg="graphite-black"
|
||||
display="flex"
|
||||
center
|
||||
overflow="hidden"
|
||||
border
|
||||
borderColor="border-gray/50"
|
||||
className="relative"
|
||||
>
|
||||
{logo ? (
|
||||
<Image
|
||||
src={logo}
|
||||
alt={name}
|
||||
width={64}
|
||||
height={64}
|
||||
fullWidth
|
||||
fullHeight
|
||||
objectFit="cover"
|
||||
/>
|
||||
) : (
|
||||
<PlaceholderImage size={64} />
|
||||
)}
|
||||
<Stack position="absolute" top="-1px" left="-1px" w="2" h="2" borderTop borderLeft borderColor="primary-accent/30" />
|
||||
</Stack>
|
||||
|
||||
{/* Title & Badges */}
|
||||
<Stack flexGrow={1} minWidth="0">
|
||||
<Stack direction="row" align="start" justify="between" gap={2}>
|
||||
<Heading level={4} weight="bold" fontSize="lg" className="tracking-tight group-hover:text-primary-accent transition-colors">
|
||||
{name}
|
||||
</Heading>
|
||||
{isRecruiting && (
|
||||
<Badge variant="success" size="xs">
|
||||
RECRUITING
|
||||
</Badge>
|
||||
)}
|
||||
</Stack>
|
||||
|
||||
{/* Performance Level & Category */}
|
||||
<Stack direction="row" align="center" gap={2} wrap mt={2}>
|
||||
{performanceBadge}
|
||||
{specializationContent}
|
||||
{categoryBadge}
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Stack>
|
||||
|
||||
{/* Content */}
|
||||
<Stack p={5} display="flex" flexDirection="col" flexGrow={1}>
|
||||
{/* Description */}
|
||||
<Text
|
||||
size="xs"
|
||||
color="text-gray-500"
|
||||
mb={4}
|
||||
lineClamp={2}
|
||||
block
|
||||
leading="relaxed"
|
||||
style={{ height: '2.5rem' }}
|
||||
>
|
||||
{description || 'No description available'}
|
||||
</Text>
|
||||
|
||||
{/* Region & Languages */}
|
||||
{(region || languagesContent) && (
|
||||
<Stack direction="row" align="center" gap={2} wrap mb={4}>
|
||||
{region && (
|
||||
<Stack
|
||||
display="flex"
|
||||
alignItems="center"
|
||||
gap={2}
|
||||
px={2}
|
||||
py={1}
|
||||
rounded="none"
|
||||
bg="panel-gray/20"
|
||||
border
|
||||
borderColor="border-gray/30"
|
||||
>
|
||||
<Icon icon={Globe} size={3} color="text-primary-accent" />
|
||||
<Text size="xs" color="text-gray-400" weight="bold" className="uppercase tracking-widest">{region}</Text>
|
||||
</Stack>
|
||||
)}
|
||||
{languagesContent}
|
||||
</Stack>
|
||||
)}
|
||||
|
||||
{/* Spacer */}
|
||||
<Stack flexGrow={1} />
|
||||
|
||||
{/* Footer */}
|
||||
<Stack
|
||||
display="flex"
|
||||
alignItems="center"
|
||||
justifyContent="between"
|
||||
pt={4}
|
||||
borderTop
|
||||
borderColor="border-gray/30"
|
||||
mt="auto"
|
||||
>
|
||||
<Stack direction="row" align="center" gap={2}>
|
||||
<Icon icon={Users} size={3} color="text-gray-500" />
|
||||
<Text size="xs" color="text-gray-500" font="mono">
|
||||
{memberCount} {memberCount === 1 ? 'MEMBER' : 'MEMBERS'}
|
||||
</Text>
|
||||
</Stack>
|
||||
|
||||
<Stack direction="row" align="center" gap={1} className="group-hover:text-primary-accent transition-colors">
|
||||
<Text size="xs" color="text-gray-500" weight="bold" className="uppercase tracking-widest">VIEW</Text>
|
||||
<Icon icon={ChevronRight} size={3} color="text-gray-500" className="transition-transform group-hover:translate-x-0.5" />
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Card>
|
||||
</Stack>
|
||||
<UITeamCard
|
||||
name={name}
|
||||
description={description}
|
||||
memberCount={memberCount}
|
||||
isRecruiting={isRecruiting}
|
||||
region={region}
|
||||
onClick={onClick}
|
||||
logo={
|
||||
logo ? (
|
||||
<Image
|
||||
src={logo}
|
||||
alt={name}
|
||||
width={64}
|
||||
height={64}
|
||||
objectFit="cover"
|
||||
/>
|
||||
) : (
|
||||
<PlaceholderImage size={64} />
|
||||
)
|
||||
}
|
||||
badges={
|
||||
<React.Fragment>
|
||||
{performanceBadge}
|
||||
{specializationContent}
|
||||
{categoryBadge}
|
||||
{languagesContent}
|
||||
</React.Fragment>
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
'use client';
|
||||
|
||||
import { Badge } from '@/ui/Badge';
|
||||
import { Button } from '@/ui/Button';
|
||||
import { Icon } from '@/ui/Icon';
|
||||
import { Input } from '@/ui/Input';
|
||||
import { Stack } from '@/ui/primitives/Stack';
|
||||
import { Text } from '@/ui/Text';
|
||||
import { ControlBar } from '@/ui/ControlBar';
|
||||
import { SegmentedControl } from '@/ui/SegmentedControl';
|
||||
import { Hash, LucideIcon, Percent, Search, Star, Trophy } from 'lucide-react';
|
||||
import React from 'react';
|
||||
|
||||
type SkillLevel = 'pro' | 'advanced' | 'intermediate' | 'beginner';
|
||||
type SortBy = 'rating' | 'wins' | 'winRate' | 'races';
|
||||
@@ -45,23 +49,24 @@ export function TeamFilter({
|
||||
onSortChange,
|
||||
}: TeamFilterProps) {
|
||||
return (
|
||||
<Stack mb={6} gap={4}>
|
||||
{/* Search and Level Filter Row */}
|
||||
<Stack direction="row" align="center" gap={4} wrap>
|
||||
<Stack maxWidth="448px" fullWidth>
|
||||
<Input
|
||||
type="text"
|
||||
placeholder="Search teams..."
|
||||
value={searchQuery}
|
||||
onChange={(e) => onSearchChange(e.target.value)}
|
||||
icon={<Icon icon={Search} size={5} color="text-gray-500" />}
|
||||
/>
|
||||
</Stack>
|
||||
|
||||
{/* Level Filter */}
|
||||
<Stack direction="row" align="center" gap={2} wrap>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '1rem', marginBottom: '1.5rem' }}>
|
||||
<ControlBar
|
||||
leftContent={
|
||||
<div style={{ maxWidth: '448px', width: '100%' }}>
|
||||
<Input
|
||||
type="text"
|
||||
placeholder="Search teams..."
|
||||
value={searchQuery}
|
||||
onChange={(e) => onSearchChange(e.target.value)}
|
||||
icon={<Icon icon={Search} size={5} intent="low" />}
|
||||
fullWidth
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '0.5rem', flexWrap: 'wrap' }}>
|
||||
<Button
|
||||
variant={filterLevel === 'all' ? 'race-final' : 'secondary'}
|
||||
variant={filterLevel === 'all' ? 'primary' : 'secondary'}
|
||||
size="sm"
|
||||
onClick={() => onFilterLevelChange('all')}
|
||||
>
|
||||
@@ -84,31 +89,21 @@ export function TeamFilter({
|
||||
</Button>
|
||||
);
|
||||
})}
|
||||
</Stack>
|
||||
</Stack>
|
||||
</div>
|
||||
</ControlBar>
|
||||
|
||||
{/* Sort Options */}
|
||||
<Stack direction="row" align="center" gap={2}>
|
||||
<Text size="sm" color="text-gray-400">Sort by:</Text>
|
||||
<Stack p={1} rounded="lg" border={true} borderColor="border-charcoal-outline" bg="bg-iron-gray" bgOpacity={0.5}>
|
||||
<Stack direction="row" align="center" gap={1}>
|
||||
{SORT_OPTIONS.map((option) => {
|
||||
const isActive = sortBy === option.id;
|
||||
return (
|
||||
<Button
|
||||
key={option.id}
|
||||
variant={isActive ? 'race-final' : 'ghost'}
|
||||
size="sm"
|
||||
onClick={() => onSortChange(option.id)}
|
||||
icon={<Icon icon={option.icon} size={3.5} />}
|
||||
>
|
||||
{option.label}
|
||||
</Button>
|
||||
);
|
||||
})}
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Stack>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '0.5rem' }}>
|
||||
<Text size="sm" variant="low">Sort by:</Text>
|
||||
<SegmentedControl
|
||||
options={SORT_OPTIONS.map(opt => ({
|
||||
id: opt.id,
|
||||
label: opt.label,
|
||||
icon: <Icon icon={opt.icon} size={3.5} />
|
||||
}))}
|
||||
activeId={sortBy}
|
||||
onChange={(id) => onSortChange(id as SortBy)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,73 +1,52 @@
|
||||
|
||||
|
||||
'use client';
|
||||
|
||||
import { SkillLevelButton } from '@/components/drivers/SkillLevelButton';
|
||||
import { TeamHeroSection as UiTeamHeroSection } from '@/components/teams/TeamHeroSection';
|
||||
import { TeamHeroStats } from '@/components/teams/TeamHeroStats';
|
||||
import type { TeamSummaryViewModel } from '@/lib/view-models/TeamSummaryViewModel';
|
||||
import { Button } from '@/ui/Button';
|
||||
import { Icon } from '@/ui/Icon';
|
||||
import { Stack } from '@/ui/primitives/Stack';
|
||||
import { TeamHero } from '@/ui/TeamHero';
|
||||
import { Text } from '@/ui/Text';
|
||||
import {
|
||||
Crown,
|
||||
LucideIcon,
|
||||
Plus,
|
||||
Search,
|
||||
Shield,
|
||||
Star,
|
||||
TrendingUp,
|
||||
} from 'lucide-react';
|
||||
import React from 'react';
|
||||
|
||||
type SkillLevel = 'pro' | 'advanced' | 'intermediate' | 'beginner';
|
||||
|
||||
interface SkillLevelConfig {
|
||||
id: SkillLevel;
|
||||
label: string;
|
||||
icon: LucideIcon;
|
||||
color: string;
|
||||
bgColor: string;
|
||||
borderColor: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
const SKILL_LEVELS: SkillLevelConfig[] = [
|
||||
const SKILL_LEVELS = [
|
||||
{
|
||||
id: 'pro',
|
||||
label: 'Pro',
|
||||
icon: Crown,
|
||||
color: 'text-warning-amber',
|
||||
bgColor: 'bg-yellow-400/10',
|
||||
borderColor: 'border-yellow-400/30',
|
||||
description: 'Elite competition, sponsored teams',
|
||||
intent: 'warning' as const,
|
||||
},
|
||||
{
|
||||
id: 'advanced',
|
||||
label: 'Advanced',
|
||||
icon: Star,
|
||||
color: 'text-purple-400',
|
||||
bgColor: 'bg-purple-900/10',
|
||||
borderColor: 'border-purple-900/30',
|
||||
description: 'Competitive racing, high consistency',
|
||||
intent: 'primary' as const,
|
||||
},
|
||||
{
|
||||
id: 'intermediate',
|
||||
label: 'Intermediate',
|
||||
icon: TrendingUp,
|
||||
color: 'text-primary-blue',
|
||||
bgColor: 'bg-blue-900/10',
|
||||
borderColor: 'border-blue-900/30',
|
||||
description: 'Growing skills, regular practice',
|
||||
intent: 'telemetry' as const,
|
||||
},
|
||||
{
|
||||
id: 'beginner',
|
||||
label: 'Beginner',
|
||||
icon: Shield,
|
||||
color: 'text-performance-green',
|
||||
bgColor: 'bg-green-900/10',
|
||||
borderColor: 'border-green-900/30',
|
||||
description: 'Learning the basics, friendly environment',
|
||||
intent: 'success' as const,
|
||||
},
|
||||
];
|
||||
] as const;
|
||||
|
||||
interface TeamHeroSectionProps {
|
||||
teams: TeamSummaryViewModel[];
|
||||
@@ -87,24 +66,23 @@ export function TeamHeroSection({
|
||||
onSkillLevelClick,
|
||||
}: TeamHeroSectionProps) {
|
||||
return (
|
||||
<UiTeamHeroSection
|
||||
<TeamHero
|
||||
title={
|
||||
<>
|
||||
<React.Fragment>
|
||||
Find Your
|
||||
<Text color="text-purple-400"> Crew</Text>
|
||||
</>
|
||||
<Text as="span" variant="primary"> Crew</Text>
|
||||
</React.Fragment>
|
||||
}
|
||||
description="Solo racing is great. Team racing is unforgettable. Join a team that matches your skill level and ambitions."
|
||||
statsContent={
|
||||
stats={
|
||||
<TeamHeroStats teamCount={teams.length} recruitingCount={recruitingCount} />
|
||||
}
|
||||
actionsContent={
|
||||
<>
|
||||
actions={
|
||||
<React.Fragment>
|
||||
<Button
|
||||
variant="primary"
|
||||
onClick={onShowCreateForm}
|
||||
icon={<Icon icon={Plus} size={4} />}
|
||||
bg="bg-purple-600"
|
||||
>
|
||||
Create Team
|
||||
</Button>
|
||||
@@ -115,14 +93,14 @@ export function TeamHeroSection({
|
||||
>
|
||||
Browse Teams
|
||||
</Button>
|
||||
</>
|
||||
</React.Fragment>
|
||||
}
|
||||
sideContent={
|
||||
<>
|
||||
<Text size="xs" color="text-gray-500" weight="medium" block mb={3} uppercase letterSpacing="0.05em">
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '1rem' }}>
|
||||
<Text size="xs" variant="low" weight="bold" uppercase>
|
||||
Find Your Level
|
||||
</Text>
|
||||
<Stack gap={2}>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '0.5rem' }}>
|
||||
{SKILL_LEVELS.map((level) => {
|
||||
const count = teamsByLevel[level.id]?.length || 0;
|
||||
|
||||
@@ -131,16 +109,13 @@ export function TeamHeroSection({
|
||||
key={level.id}
|
||||
label={level.label}
|
||||
icon={level.icon}
|
||||
color={level.color}
|
||||
bgColor={level.bgColor}
|
||||
borderColor={level.borderColor}
|
||||
count={count}
|
||||
onClick={() => onSkillLevelClick(level.id)}
|
||||
onClick={() => onSkillLevelClick(level.id as SkillLevel)}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</Stack>
|
||||
</>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -1,12 +1,7 @@
|
||||
import { getMediaUrl } from '@/lib/utilities/media';
|
||||
import type { TeamSummaryViewModel } from '@/lib/view-models/TeamSummaryViewModel';
|
||||
import { Button } from '@/ui/Button';
|
||||
import { Icon } from '@/ui/Icon';
|
||||
import { Image } from '@/ui/Image';
|
||||
import { Podium, PodiumItem } from '@/ui/Podium';
|
||||
import { Stack } from '@/ui/primitives/Stack';
|
||||
import { Text } from '@/ui/Text';
|
||||
import { Crown, Trophy, Users } from 'lucide-react';
|
||||
import { Podium } from '@/ui/Podium';
|
||||
import React from 'react';
|
||||
|
||||
interface TeamPodiumProps {
|
||||
teams: TeamSummaryViewModel[];
|
||||
@@ -14,130 +9,20 @@ interface TeamPodiumProps {
|
||||
}
|
||||
|
||||
export function TeamPodium({ teams, onClick }: TeamPodiumProps) {
|
||||
const top3 = teams.slice(0, 3) as [TeamSummaryViewModel, TeamSummaryViewModel, TeamSummaryViewModel];
|
||||
if (teams.length < 3) return null;
|
||||
const top3 = teams.slice(0, 3);
|
||||
if (top3.length < 3) return null;
|
||||
|
||||
// Display order: 2nd, 1st, 3rd
|
||||
const podiumOrder: [TeamSummaryViewModel, TeamSummaryViewModel, TeamSummaryViewModel] = [
|
||||
top3[1],
|
||||
top3[0],
|
||||
top3[2],
|
||||
];
|
||||
const podiumHeights = ['28', '36', '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 getBgColor = (position: number) => {
|
||||
switch (position) {
|
||||
case 1:
|
||||
return 'bg-gradient-to-br from-primary-blue/20 via-iron-gray/80 to-deep-graphite';
|
||||
case 2:
|
||||
return 'bg-iron-gray';
|
||||
case 3:
|
||||
return 'bg-gradient-to-br from-purple-600/20 via-iron-gray/80 to-deep-graphite';
|
||||
default:
|
||||
return 'bg-iron-gray/50';
|
||||
}
|
||||
};
|
||||
const entries = top3.map((team, index) => ({
|
||||
name: team.name,
|
||||
avatar: team.logoUrl || getMediaUrl('team-logo', team.id),
|
||||
value: `${team.totalWins} Wins`,
|
||||
position: (index + 1) as 1 | 2 | 3
|
||||
}));
|
||||
|
||||
return (
|
||||
<Podium title="Top 3 Teams">
|
||||
{podiumOrder.map((team, index) => {
|
||||
const position = podiumPositions[index] ?? 0;
|
||||
|
||||
return (
|
||||
<PodiumItem
|
||||
key={team.id}
|
||||
position={position}
|
||||
height={podiumHeights[index] || '20'}
|
||||
bgColor={getBgColor(position)}
|
||||
positionColor={getPositionColor(position)}
|
||||
cardContent={
|
||||
<Button
|
||||
variant="ghost"
|
||||
onClick={() => onClick(team.id)}
|
||||
h="auto"
|
||||
mb={4}
|
||||
p={0}
|
||||
transition
|
||||
>
|
||||
<Stack
|
||||
bg={getBgColor(position)}
|
||||
rounded="xl"
|
||||
border={true}
|
||||
borderColor="border-charcoal-outline"
|
||||
p={4}
|
||||
position="relative"
|
||||
>
|
||||
{/* Crown for 1st place */}
|
||||
{position === 1 && (
|
||||
<Stack position="absolute" top="-4" left="1/2" translateX="-1/2">
|
||||
<Stack position="relative">
|
||||
<Stack animate="pulse">
|
||||
<Icon icon={Crown} size={8} color="text-warning-amber" />
|
||||
</Stack>
|
||||
<Stack position="absolute" inset="0" bg="bg-yellow-400" bgOpacity={0.3} blur="md" rounded="full" />
|
||||
</Stack>
|
||||
</Stack>
|
||||
)}
|
||||
|
||||
{/* Team logo */}
|
||||
<Stack h="20" w="20" display="flex" center rounded="xl" bg="bg-deep-graphite" border={true} borderColor="border-charcoal-outline" overflow="hidden" mb={3}>
|
||||
<Image
|
||||
src={team.logoUrl || getMediaUrl('team-logo', team.id)}
|
||||
alt={team.name}
|
||||
width={80}
|
||||
height={80}
|
||||
objectFit="cover"
|
||||
/>
|
||||
</Stack>
|
||||
|
||||
{/* Team name */}
|
||||
<Text weight="bold" size="sm" color="text-white" align="center" block truncate maxWidth="28">
|
||||
{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" color={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>
|
||||
</Stack>
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</Podium>
|
||||
<Podium
|
||||
title="Top 3 Teams"
|
||||
entries={entries}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
|
||||
|
||||
'use client';
|
||||
|
||||
import { Icon } from '@/ui/Icon';
|
||||
import { Input } from '@/ui/Input';
|
||||
import { Stack } from '@/ui/primitives/Stack';
|
||||
import { Search } from 'lucide-react';
|
||||
import React from 'react';
|
||||
|
||||
interface TeamSearchBarProps {
|
||||
searchQuery: string;
|
||||
@@ -12,18 +14,15 @@ interface TeamSearchBarProps {
|
||||
|
||||
export function TeamSearchBar({ searchQuery, onSearchChange }: TeamSearchBarProps) {
|
||||
return (
|
||||
<Stack id="teams-list" mb={6}>
|
||||
<Stack direction="row" gap={4} wrap>
|
||||
<Stack flexGrow={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="var(--text-gray-500)" />}
|
||||
/>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Stack>
|
||||
<div style={{ marginBottom: '1.5rem' }}>
|
||||
<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} intent="low" />}
|
||||
fullWidth
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,12 +1,7 @@
|
||||
import { getMediaUrl } from '@/lib/utilities/media';
|
||||
import type { TeamSummaryViewModel } from '@/lib/view-models/TeamSummaryViewModel';
|
||||
import { Button } from '@/ui/Button';
|
||||
import { Icon } from '@/ui/Icon';
|
||||
import { Image } from '@/ui/Image';
|
||||
import { Podium, PodiumItem } from '@/ui/Podium';
|
||||
import { Stack } from '@/ui/primitives/Stack';
|
||||
import { Text } from '@/ui/Text';
|
||||
import { Crown, Trophy, Users } from 'lucide-react';
|
||||
import { Podium } from '@/ui/Podium';
|
||||
import React from 'react';
|
||||
|
||||
interface TopThreePodiumProps {
|
||||
teams: TeamSummaryViewModel[];
|
||||
@@ -14,130 +9,20 @@ interface TopThreePodiumProps {
|
||||
}
|
||||
|
||||
export function TopThreePodium({ teams, onClick }: TopThreePodiumProps) {
|
||||
const top3 = teams.slice(0, 3) as [TeamSummaryViewModel, TeamSummaryViewModel, TeamSummaryViewModel];
|
||||
if (teams.length < 3) return null;
|
||||
const top3 = teams.slice(0, 3);
|
||||
if (top3.length < 3) return null;
|
||||
|
||||
// Display order: 2nd, 1st, 3rd
|
||||
const podiumOrder: [TeamSummaryViewModel, TeamSummaryViewModel, TeamSummaryViewModel] = [
|
||||
top3[1],
|
||||
top3[0],
|
||||
top3[2],
|
||||
];
|
||||
const podiumHeights = ['28', '36', '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 getBgColor = (position: number) => {
|
||||
switch (position) {
|
||||
case 1:
|
||||
return 'bg-gradient-to-br from-primary-blue/20 via-iron-gray/80 to-deep-graphite';
|
||||
case 2:
|
||||
return 'bg-iron-gray';
|
||||
case 3:
|
||||
return 'bg-gradient-to-br from-purple-600/20 via-iron-gray/80 to-deep-graphite';
|
||||
default:
|
||||
return 'bg-iron-gray/50';
|
||||
}
|
||||
};
|
||||
const entries = top3.map((team, index) => ({
|
||||
name: team.name,
|
||||
avatar: team.logoUrl || getMediaUrl('team-logo', team.id),
|
||||
value: `${team.totalWins} Wins`,
|
||||
position: (index + 1) as 1 | 2 | 3
|
||||
}));
|
||||
|
||||
return (
|
||||
<Podium title="Top 3 Teams">
|
||||
{podiumOrder.map((team, index) => {
|
||||
const position = podiumPositions[index] ?? 0;
|
||||
|
||||
return (
|
||||
<PodiumItem
|
||||
key={team.id}
|
||||
position={position}
|
||||
height={podiumHeights[index] || '20'}
|
||||
bgColor={getBgColor(position)}
|
||||
positionColor={getPositionColor(position)}
|
||||
cardContent={
|
||||
<Button
|
||||
variant="ghost"
|
||||
onClick={() => onClick(team.id)}
|
||||
h="auto"
|
||||
mb={4}
|
||||
p={0}
|
||||
transition
|
||||
>
|
||||
<Stack
|
||||
bg={getBgColor(position)}
|
||||
rounded="xl"
|
||||
border
|
||||
borderColor="border-charcoal-outline"
|
||||
p={4}
|
||||
position="relative"
|
||||
>
|
||||
{/* Crown for 1st place */}
|
||||
{position === 1 && (
|
||||
<Stack position="absolute" top="-4" left="1/2" translateX="-1/2">
|
||||
<Stack position="relative">
|
||||
<Stack animate="pulse">
|
||||
<Icon icon={Crown} size={8} color="var(--warning-amber)" />
|
||||
</Stack>
|
||||
<Stack position="absolute" inset="0" bg="bg-yellow-400" bgOpacity={0.3} blur="md" rounded="full" />
|
||||
</Stack>
|
||||
</Stack>
|
||||
)}
|
||||
|
||||
{/* Team logo */}
|
||||
<Stack h="20" w="20" display="flex" center rounded="xl" bg="bg-deep-graphite" border borderColor="border-charcoal-outline" overflow="hidden" mb={3}>
|
||||
<Image
|
||||
src={team.logoUrl || getMediaUrl('team-logo', team.id)}
|
||||
alt={team.name}
|
||||
width={80}
|
||||
height={80}
|
||||
objectFit="cover"
|
||||
/>
|
||||
</Stack>
|
||||
|
||||
{/* Team name */}
|
||||
<Text weight="bold" size="sm" color="text-white" textAlign="center" block truncate maxWidth="28">
|
||||
{team.name}
|
||||
</Text>
|
||||
|
||||
{/* Category */}
|
||||
{team.category && (
|
||||
<Text size="xs" color="text-primary-blue" textAlign="center" block mt={1}>
|
||||
{team.category}
|
||||
</Text>
|
||||
)}
|
||||
|
||||
{/* Rating placeholder */}
|
||||
<Text size="xl" weight="bold" color={getPositionColor(position)} textAlign="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="var(--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="var(--primary-blue)" />
|
||||
<Text size="xs" color="text-gray-400">{team.memberCount}</Text>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</Podium>
|
||||
<Podium
|
||||
title="Top 3 Teams"
|
||||
entries={entries}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user