import { useState, useEffect } from 'react' import { useParams, useNavigate } from 'react-router-dom' import { assignmentsApi } from '@/api' import type { AssignmentDetail } from '@/types' import { GlassCard, NeonButton } from '@/components/ui' import { useAuthStore } from '@/store/auth' import { useToast } from '@/store/toast' import { ArrowLeft, Loader2, ExternalLink, Image, MessageSquare, ThumbsUp, ThumbsDown, AlertTriangle, Clock, CheckCircle, XCircle, Send, Flag, Gamepad2, Zap, Trophy } from 'lucide-react' export function AssignmentDetailPage() { const { id } = useParams<{ id: string }>() const navigate = useNavigate() const user = useAuthStore((state) => state.user) const toast = useToast() const [assignment, setAssignment] = useState(null) const [isLoading, setIsLoading] = useState(true) const [error, setError] = useState(null) const [proofMediaBlobUrl, setProofMediaBlobUrl] = useState(null) const [proofMediaType, setProofMediaType] = useState<'image' | 'video' | null>(null) // Dispute creation const [showDisputeForm, setShowDisputeForm] = useState(false) const [disputeReason, setDisputeReason] = useState('') const [isCreatingDispute, setIsCreatingDispute] = useState(false) // Comment const [commentText, setCommentText] = useState('') const [isAddingComment, setIsAddingComment] = useState(false) // Voting const [isVoting, setIsVoting] = useState(false) useEffect(() => { loadAssignment() return () => { // Cleanup blob URL on unmount if (proofMediaBlobUrl) { URL.revokeObjectURL(proofMediaBlobUrl) } } }, [id]) const loadAssignment = async () => { if (!id) return setIsLoading(true) setError(null) try { const data = await assignmentsApi.getDetail(parseInt(id)) setAssignment(data) // Load proof media if exists if (data.proof_image_url) { try { const { url, type } = await assignmentsApi.getProofMediaUrl(parseInt(id)) setProofMediaBlobUrl(url) setProofMediaType(type) } catch { // Ignore error, media just won't show } } } catch (err: unknown) { const error = err as { response?: { data?: { detail?: string } } } setError(error.response?.data?.detail || 'Не удалось загрузить данные') } finally { setIsLoading(false) } } const handleCreateDispute = async () => { if (!id || !disputeReason.trim()) return setIsCreatingDispute(true) try { await assignmentsApi.createDispute(parseInt(id), disputeReason) setDisputeReason('') setShowDisputeForm(false) await loadAssignment() } catch (err: unknown) { const error = err as { response?: { data?: { detail?: string } } } toast.error(error.response?.data?.detail || 'Не удалось создать оспаривание') } finally { setIsCreatingDispute(false) } } const handleVote = async (vote: boolean) => { if (!assignment?.dispute) return setIsVoting(true) try { await assignmentsApi.vote(assignment.dispute.id, vote) await loadAssignment() } catch (err: unknown) { const error = err as { response?: { data?: { detail?: string } } } toast.error(error.response?.data?.detail || 'Не удалось проголосовать') } finally { setIsVoting(false) } } const handleAddComment = async () => { if (!assignment?.dispute || !commentText.trim()) return setIsAddingComment(true) try { await assignmentsApi.addComment(assignment.dispute.id, commentText) setCommentText('') await loadAssignment() } catch (err: unknown) { const error = err as { response?: { data?: { detail?: string } } } toast.error(error.response?.data?.detail || 'Не удалось добавить комментарий') } finally { setIsAddingComment(false) } } const formatDate = (dateString: string) => { return new Date(dateString).toLocaleString('ru-RU', { day: 'numeric', month: 'long', year: 'numeric', hour: '2-digit', minute: '2-digit', }) } const getTimeRemaining = (expiresAt: string) => { const now = new Date() const expires = new Date(expiresAt) const diff = expires.getTime() - now.getTime() if (diff <= 0) return 'Истекло' const hours = Math.floor(diff / (1000 * 60 * 60)) const minutes = Math.floor((diff % (1000 * 60 * 60)) / (1000 * 60)) return `${hours}ч ${minutes}м` } const getStatusConfig = (status: string) => { switch (status) { case 'completed': return { color: 'bg-green-500/20 text-green-400 border-green-500/30', icon: , text: 'Выполнено', } case 'dropped': return { color: 'bg-red-500/20 text-red-400 border-red-500/30', icon: , text: 'Пропущено', } case 'returned': return { color: 'bg-orange-500/20 text-orange-400 border-orange-500/30', icon: , text: 'Возвращено', } default: return { color: 'bg-neon-500/20 text-neon-400 border-neon-500/30', icon: , text: 'Активно', } } } if (isLoading) { return (

Загрузка...

) } if (error || !assignment) { return (

{error || 'Задание не найдено'}

navigate(-1)}> Назад
) } const dispute = assignment.dispute const status = getStatusConfig(assignment.status) return (
{/* Header */}

Детали выполнения

Просмотр доказательства

{/* Challenge info */}

{assignment.challenge.game.title}

{assignment.challenge.title}

{status.icon} {status.text}

{assignment.challenge.description}

+{assignment.challenge.points} очков {assignment.challenge.difficulty} {assignment.challenge.estimated_time && ( ~{assignment.challenge.estimated_time} мин )}

Выполнил:{' '} {assignment.participant.nickname}

{assignment.completed_at && (

Дата:{' '} {formatDate(assignment.completed_at)}

)} {assignment.points_earned > 0 && (

Получено очков:{' '} {assignment.points_earned}

)}
{/* Proof section */}

Доказательство

Пруф выполнения задания

{/* Proof media (image or video) */} {assignment.proof_image_url && (
{proofMediaBlobUrl ? ( proofMediaType === 'video' ? (
)} {/* Proof URL */} {assignment.proof_url && ( )} {/* Proof comment */} {assignment.proof_comment && (

Комментарий:

{assignment.proof_comment}

)} {!assignment.proof_image_url && !assignment.proof_url && (

Пруф не предоставлен

)}
{/* Dispute button */} {assignment.can_dispute && !dispute && !showDisputeForm && ( )} {/* Dispute creation form */} {showDisputeForm && !dispute && (

Оспорить выполнение

У участников будет 24 часа для голосования