This commit is contained in:
2025-12-14 02:38:35 +07:00
commit 5343a8f2c3
84 changed files with 7406 additions and 0 deletions

View File

@@ -0,0 +1,158 @@
import { useState, useEffect } from 'react'
import { Link } from 'react-router-dom'
import { marathonsApi } from '@/api'
import type { MarathonListItem } from '@/types'
import { Button, Card, CardContent } from '@/components/ui'
import { Plus, Users, Calendar, Loader2 } from 'lucide-react'
import { format } from 'date-fns'
export function MarathonsPage() {
const [marathons, setMarathons] = useState<MarathonListItem[]>([])
const [isLoading, setIsLoading] = useState(true)
const [joinCode, setJoinCode] = useState('')
const [joinError, setJoinError] = useState<string | null>(null)
const [isJoining, setIsJoining] = useState(false)
useEffect(() => {
loadMarathons()
}, [])
const loadMarathons = async () => {
try {
const data = await marathonsApi.list()
setMarathons(data)
} catch (error) {
console.error('Failed to load marathons:', error)
} finally {
setIsLoading(false)
}
}
const handleJoin = async () => {
if (!joinCode.trim()) return
setJoinError(null)
setIsJoining(true)
try {
await marathonsApi.join(joinCode.trim())
setJoinCode('')
await loadMarathons()
} catch (err: unknown) {
const error = err as { response?: { data?: { detail?: string } } }
setJoinError(error.response?.data?.detail || 'Не удалось присоединиться')
} finally {
setIsJoining(false)
}
}
const getStatusColor = (status: string) => {
switch (status) {
case 'preparing':
return 'bg-yellow-500/20 text-yellow-500'
case 'active':
return 'bg-green-500/20 text-green-500'
case 'finished':
return 'bg-gray-500/20 text-gray-400'
default:
return 'bg-gray-500/20 text-gray-400'
}
}
const getStatusText = (status: string) => {
switch (status) {
case 'preparing':
return 'Подготовка'
case 'active':
return 'Активен'
case 'finished':
return 'Завершён'
default:
return status
}
}
if (isLoading) {
return (
<div className="flex justify-center py-12">
<Loader2 className="w-8 h-8 animate-spin text-primary-500" />
</div>
)
}
return (
<div className="max-w-4xl mx-auto">
<div className="flex justify-between items-center mb-8">
<h1 className="text-2xl font-bold text-white">Мои марафоны</h1>
<Link to="/marathons/create">
<Button>
<Plus className="w-4 h-4 mr-2" />
Создать марафон
</Button>
</Link>
</div>
{/* Join marathon */}
<Card className="mb-8">
<CardContent>
<h3 className="font-medium text-white mb-3">Присоединиться к марафону</h3>
<div className="flex gap-3">
<input
type="text"
value={joinCode}
onChange={(e) => setJoinCode(e.target.value)}
placeholder="Введите код приглашения"
className="input flex-1"
/>
<Button onClick={handleJoin} isLoading={isJoining}>
Присоединиться
</Button>
</div>
{joinError && <p className="mt-2 text-sm text-red-500">{joinError}</p>}
</CardContent>
</Card>
{/* Marathon list */}
{marathons.length === 0 ? (
<Card>
<CardContent className="text-center py-8">
<p className="text-gray-400 mb-4">У вас пока нет марафонов</p>
<Link to="/marathons/create">
<Button>Создать первый марафон</Button>
</Link>
</CardContent>
</Card>
) : (
<div className="space-y-4">
{marathons.map((marathon) => (
<Link key={marathon.id} to={`/marathons/${marathon.id}`}>
<Card className="hover:bg-gray-700/50 transition-colors cursor-pointer">
<CardContent className="flex items-center justify-between">
<div>
<h3 className="text-lg font-medium text-white mb-1">
{marathon.title}
</h3>
<div className="flex items-center gap-4 text-sm text-gray-400">
<span className="flex items-center gap-1">
<Users className="w-4 h-4" />
{marathon.participants_count} участников
</span>
{marathon.start_date && (
<span className="flex items-center gap-1">
<Calendar className="w-4 h-4" />
{format(new Date(marathon.start_date), 'MMM d, yyyy')}
</span>
)}
</div>
</div>
<span className={`px-3 py-1 rounded-full text-sm ${getStatusColor(marathon.status)}`}>
{getStatusText(marathon.status)}
</span>
</CardContent>
</Card>
</Link>
))}
</div>
)}
</div>
)
}