Files
game-marathon/frontend/src/pages/InvitePage.tsx
2025-12-17 02:03:33 +07:00

227 lines
8.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useState, useEffect } from 'react'
import { useParams, useNavigate, Link } from 'react-router-dom'
import { marathonsApi } from '@/api'
import type { MarathonPublicInfo } from '@/types'
import { useAuthStore } from '@/store/auth'
import { NeonButton, GlassCard } from '@/components/ui'
import { Users, Loader2, Trophy, UserPlus, LogIn, Gamepad2, AlertCircle, Sparkles, Crown } from 'lucide-react'
export function InvitePage() {
const { code } = useParams<{ code: string }>()
const navigate = useNavigate()
const { isAuthenticated, setPendingInviteCode } = useAuthStore()
const [marathon, setMarathon] = useState<MarathonPublicInfo | null>(null)
const [isLoading, setIsLoading] = useState(true)
const [isJoining, setIsJoining] = useState(false)
const [error, setError] = useState<string | null>(null)
useEffect(() => {
loadMarathon()
}, [code])
const loadMarathon = async () => {
if (!code) return
try {
const data = await marathonsApi.getByCode(code)
setMarathon(data)
} catch {
setError('Марафон не найден или ссылка недействительна')
} finally {
setIsLoading(false)
}
}
const handleJoin = async () => {
if (!code) return
setIsJoining(true)
try {
const joined = await marathonsApi.join(code)
navigate(`/marathons/${joined.id}`)
} catch (err: unknown) {
const apiError = err as { response?: { data?: { detail?: string } } }
const detail = apiError.response?.data?.detail
if (detail === 'Already joined this marathon') {
// Already a member, just redirect
navigate(`/marathons/${marathon?.id}`)
} else {
setError(detail || 'Не удалось присоединиться')
}
} finally {
setIsJoining(false)
}
}
const handleAuthRedirect = (path: string) => {
if (code) {
setPendingInviteCode(code)
}
navigate(path)
}
if (isLoading) {
return (
<div className="flex flex-col items-center justify-center py-24">
<Loader2 className="w-12 h-12 animate-spin text-neon-500 mb-4" />
<p className="text-gray-400">Загрузка приглашения...</p>
</div>
)
}
if (error || !marathon) {
return (
<div className="max-w-md mx-auto">
<GlassCard className="text-center py-12">
<div className="w-16 h-16 mx-auto mb-4 rounded-2xl bg-red-500/10 border border-red-500/30 flex items-center justify-center">
<AlertCircle className="w-8 h-8 text-red-400" />
</div>
<h2 className="text-xl font-bold text-white mb-2">Ошибка</h2>
<p className="text-gray-400 mb-6">{error || 'Марафон не найден'}</p>
<Link to="/marathons">
<NeonButton variant="outline">К списку марафонов</NeonButton>
</Link>
</GlassCard>
</div>
)
}
const getStatusConfig = (status: string) => {
switch (status) {
case 'preparing':
return {
color: 'bg-yellow-500/20 text-yellow-400 border-yellow-500/30',
text: 'Подготовка',
dot: 'bg-yellow-500',
}
case 'active':
return {
color: 'bg-neon-500/20 text-neon-400 border-neon-500/30',
text: 'Активен',
dot: 'bg-neon-500 animate-pulse',
}
case 'finished':
return {
color: 'bg-gray-500/20 text-gray-400 border-gray-500/30',
text: 'Завершён',
dot: 'bg-gray-500',
}
default:
return {
color: 'bg-gray-500/20 text-gray-400 border-gray-500/30',
text: status,
dot: 'bg-gray-500',
}
}
}
const status = getStatusConfig(marathon.status)
return (
<div className="min-h-[70vh] flex items-center justify-center px-4">
{/* Background effects */}
<div className="fixed inset-0 overflow-hidden pointer-events-none">
<div className="absolute top-1/4 -left-32 w-96 h-96 bg-neon-500/10 rounded-full blur-[100px]" />
<div className="absolute bottom-1/4 -right-32 w-96 h-96 bg-accent-500/10 rounded-full blur-[100px]" />
</div>
<div className="relative w-full max-w-md">
<GlassCard variant="neon" className="animate-scale-in">
{/* Header */}
<div className="text-center mb-8">
<div className="w-20 h-20 mx-auto mb-4 rounded-2xl bg-gradient-to-br from-neon-500/20 to-accent-500/20 border border-neon-500/30 flex items-center justify-center">
<Trophy className="w-10 h-10 text-neon-400" />
</div>
<h1 className="text-xl font-bold text-white mb-1">Приглашение в марафон</h1>
<p className="text-gray-400 text-sm">Вас пригласили присоединиться</p>
</div>
{/* Marathon info */}
<div className="glass rounded-xl p-5 mb-6">
<div className="flex items-start gap-4">
<div className="w-14 h-14 rounded-xl bg-gradient-to-br from-neon-500/20 to-accent-500/20 flex items-center justify-center border border-neon-500/20 flex-shrink-0">
<Gamepad2 className="w-7 h-7 text-neon-400" />
</div>
<div className="flex-1 min-w-0">
<h2 className="text-xl font-bold text-white mb-1 truncate">{marathon.title}</h2>
{marathon.description && (
<p className="text-gray-400 text-sm line-clamp-2 mb-3">{marathon.description}</p>
)}
<div className="flex flex-wrap items-center gap-3">
<span className={`px-2.5 py-1 rounded-full text-xs font-medium border flex items-center gap-1.5 ${status.color}`}>
<span className={`w-1.5 h-1.5 rounded-full ${status.dot}`} />
{status.text}
</span>
<span className="flex items-center gap-1.5 text-sm text-gray-400">
<Users className="w-4 h-4" />
{marathon.participants_count}
</span>
</div>
</div>
</div>
{/* Organizer */}
<div className="mt-4 pt-4 border-t border-dark-600 flex items-center gap-2 text-sm text-gray-500">
<Crown className="w-4 h-4 text-yellow-500" />
<span>Организатор:</span>
<span className="text-gray-300">{marathon.creator_nickname}</span>
</div>
</div>
{/* Actions */}
{marathon.status === 'finished' ? (
<div className="text-center">
<div className="w-12 h-12 mx-auto mb-3 rounded-xl bg-gray-500/10 flex items-center justify-center">
<AlertCircle className="w-6 h-6 text-gray-400" />
</div>
<p className="text-gray-400 mb-4">Этот марафон уже завершён</p>
<Link to="/marathons">
<NeonButton variant="outline" className="w-full">
К списку марафонов
</NeonButton>
</Link>
</div>
) : isAuthenticated ? (
<NeonButton
className="w-full"
size="lg"
onClick={handleJoin}
isLoading={isJoining}
icon={<Sparkles className="w-5 h-5" />}
>
Присоединиться
</NeonButton>
) : (
<div className="space-y-4">
<p className="text-center text-gray-400 text-sm">
Чтобы присоединиться, войдите или зарегистрируйтесь
</p>
<NeonButton
className="w-full"
size="lg"
onClick={() => handleAuthRedirect('/login')}
icon={<LogIn className="w-5 h-5" />}
>
Войти
</NeonButton>
<NeonButton
variant="outline"
className="w-full"
onClick={() => handleAuthRedirect('/register')}
icon={<UserPlus className="w-5 h-5" />}
>
Зарегистрироваться
</NeonButton>
</div>
)}
</GlassCard>
{/* Decorative elements */}
<div className="absolute -top-4 -left-4 w-24 h-24 border border-neon-500/20 rounded-2xl -z-10" />
<div className="absolute -bottom-4 -right-4 w-32 h-32 border border-accent-500/20 rounded-2xl -z-10" />
</div>
</div>
)
}