From 18ffff547349537225ef4b4d840deac38dcca030 Mon Sep 17 00:00:00 2001 From: Oronemu Date: Sun, 4 Jan 2026 03:17:17 +0700 Subject: [PATCH] Fix games list --- backend/app/api/v1/challenges.py | 49 ++- frontend/src/App.tsx | 2 - .../src/pages/admin/AdminDisputesPage.tsx | 312 ------------------ frontend/src/pages/admin/AdminLayout.tsx | 4 +- frontend/src/pages/admin/index.ts | 1 - 5 files changed, 47 insertions(+), 321 deletions(-) delete mode 100644 frontend/src/pages/admin/AdminDisputesPage.tsx diff --git a/backend/app/api/v1/challenges.py b/backend/app/api/v1/challenges.py index e234f8a..677d31c 100644 --- a/backend/app/api/v1/challenges.py +++ b/backend/app/api/v1/challenges.py @@ -99,7 +99,10 @@ async def list_challenges(game_id: int, current_user: CurrentUser, db: DbSession @router.get("/marathons/{marathon_id}/challenges", response_model=list[ChallengeResponse]) async def list_marathon_challenges(marathon_id: int, current_user: CurrentUser, db: DbSession): - """List all challenges for a marathon (from all approved games). Participants only.""" + """List all challenges for a marathon (from all approved games). Participants only. + Also includes virtual challenges for playthrough-type games.""" + from app.models.game import GameType + # Check marathon exists result = await db.execute(select(Marathon).where(Marathon.id == marathon_id)) marathon = result.scalar_one_or_none() @@ -111,7 +114,7 @@ async def list_marathon_challenges(marathon_id: int, current_user: CurrentUser, if not current_user.is_admin and not participant: raise HTTPException(status_code=403, detail="You are not a participant of this marathon") - # Get all approved challenges from approved games in this marathon + # Get all approved challenges from approved games (challenges type) in this marathon result = await db.execute( select(Challenge) .join(Game, Challenge.game_id == Game.id) @@ -125,7 +128,47 @@ async def list_marathon_challenges(marathon_id: int, current_user: CurrentUser, ) challenges = result.scalars().all() - return [build_challenge_response(c, c.game) for c in challenges] + responses = [build_challenge_response(c, c.game) for c in challenges] + + # Also get playthrough-type games and create virtual challenges for them + result = await db.execute( + select(Game) + .where( + Game.marathon_id == marathon_id, + Game.status == GameStatus.APPROVED.value, + Game.game_type == GameType.PLAYTHROUGH.value, + ) + .order_by(Game.title) + ) + playthrough_games = result.scalars().all() + + for game in playthrough_games: + # Create virtual challenge response for playthrough game + virtual_challenge = ChallengeResponse( + id=-game.id, # Negative ID to distinguish from real challenges + title=f"Прохождение: {game.title}", + description=game.playthrough_description or "Пройдите игру", + type="completion", + difficulty="medium", + points=game.playthrough_points or 0, + estimated_time=None, + proof_type=game.playthrough_proof_type or "screenshot", + proof_hint=game.playthrough_proof_hint, + game=GameShort( + id=game.id, + title=game.title, + cover_url=None, + download_url=game.download_url, + game_type=game.game_type + ), + is_generated=False, + created_at=game.created_at, + status="approved", + proposed_by=None, + ) + responses.append(virtual_challenge) + + return responses @router.post("/games/{game_id}/challenges", response_model=ChallengeResponse) diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 94c0bb1..5c990ed 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -32,7 +32,6 @@ import { AdminDashboardPage, AdminUsersPage, AdminMarathonsPage, - AdminDisputesPage, AdminLogsPage, AdminBroadcastPage, AdminContentPage, @@ -209,7 +208,6 @@ function App() { } /> } /> } /> - } /> } /> } /> } /> diff --git a/frontend/src/pages/admin/AdminDisputesPage.tsx b/frontend/src/pages/admin/AdminDisputesPage.tsx deleted file mode 100644 index 0924b6d..0000000 --- a/frontend/src/pages/admin/AdminDisputesPage.tsx +++ /dev/null @@ -1,312 +0,0 @@ -import { useState, useEffect } from 'react' -import { adminApi } from '@/api' -import type { AdminDispute } from '@/types' -import { GlassCard, NeonButton } from '@/components/ui' -import { useToast } from '@/store/toast' -import { - AlertTriangle, Loader2, CheckCircle, XCircle, Clock, - ThumbsUp, ThumbsDown, User, Trophy, ExternalLink -} from 'lucide-react' -import { Link } from 'react-router-dom' - -export function AdminDisputesPage() { - const toast = useToast() - const [disputes, setDisputes] = useState([]) - const [isLoading, setIsLoading] = useState(true) - const [filter, setFilter] = useState<'pending' | 'open' | 'all'>('pending') - const [resolvingId, setResolvingId] = useState(null) - - useEffect(() => { - loadDisputes() - }, [filter]) - - const loadDisputes = async () => { - setIsLoading(true) - try { - const data = await adminApi.listDisputes(filter) - setDisputes(data) - } catch (err) { - toast.error('Не удалось загрузить оспаривания') - } finally { - setIsLoading(false) - } - } - - const handleResolve = async (disputeId: number, isValid: boolean) => { - setResolvingId(disputeId) - try { - await adminApi.resolveDispute(disputeId, isValid) - toast.success(isValid ? 'Пруф подтверждён' : 'Пруф отклонён') - await loadDisputes() - } catch (err) { - toast.error('Не удалось разрешить диспут') - } finally { - setResolvingId(null) - } - } - - const formatDate = (dateString: string) => { - return new Date(dateString).toLocaleString('ru-RU', { - day: 'numeric', - month: 'short', - 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 getStatusBadge = (status: string) => { - switch (status) { - case 'open': - return ( - - - Голосование - - ) - case 'pending_admin': - return ( - - - Ожидает решения - - ) - case 'valid': - return ( - - - Валидно - - ) - case 'invalid': - return ( - - - Невалидно - - ) - } - } - - const pendingCount = disputes.filter(d => d.status === 'pending_admin').length - - return ( -
- {/* Header */} -
-
-

- Оспаривания -

-

- Управление диспутами и проверка пруфов -

-
- {pendingCount > 0 && ( -
- {pendingCount} - ожида{pendingCount === 1 ? 'ет' : 'ют'} решения -
- )} -
- - {/* Filters */} -
- - - -
- - {/* Loading */} - {isLoading ? ( -
- -
- ) : disputes.length === 0 ? ( - -
- -
-

- {filter === 'pending' ? 'Нет оспариваний, ожидающих решения' : - filter === 'open' ? 'Нет оспариваний в стадии голосования' : - 'Нет оспариваний'} -

-
- ) : ( -
- {disputes.map((dispute) => ( - -
- {/* Left side - Info */} -
-
-
- -
-
-

- {dispute.challenge_title} -

-
- - {dispute.marathon_title} -
-
-
- - {/* Participants */} -
-
- - Автор: - {dispute.participant_nickname} -
-
- - Оспорил: - {dispute.raised_by_nickname} -
-
- - {/* Reason */} -
-

Причина:

-

{dispute.reason}

-
- - {/* Votes & Time */} -
-
-
- - {dispute.votes_valid} -
- / -
- - {dispute.votes_invalid} -
-
- - {formatDate(dispute.created_at)} - {dispute.status === 'open' && ( - <> - - - - {getTimeRemaining(dispute.expires_at)} - - - )} -
-
- - {/* Right side - Status & Actions */} -
- {getStatusBadge(dispute.status)} - - {/* Link to assignment */} - {dispute.assignment_id && ( - - - Открыть - - )} - - {/* Resolution buttons - show for open and pending_admin */} - {(dispute.status === 'open' || dispute.status === 'pending_admin') && ( -
- {/* Vote recommendation for pending disputes */} - {dispute.status === 'pending_admin' && ( -
- Рекомендация: {dispute.votes_invalid > dispute.votes_valid ? ( - невалидно - ) : ( - валидно - )} -
- )} -
- handleResolve(dispute.id, true)} - isLoading={resolvingId === dispute.id} - disabled={resolvingId !== null} - icon={} - > - Валидно - - handleResolve(dispute.id, false)} - isLoading={resolvingId === dispute.id} - disabled={resolvingId !== null} - icon={} - > - Невалидно - -
-
- )} -
-
-
- ))} -
- )} -
- ) -} diff --git a/frontend/src/pages/admin/AdminLayout.tsx b/frontend/src/pages/admin/AdminLayout.tsx index 39878cb..23e6b16 100644 --- a/frontend/src/pages/admin/AdminLayout.tsx +++ b/frontend/src/pages/admin/AdminLayout.tsx @@ -12,15 +12,13 @@ import { Shield, MessageCircle, Sparkles, - Lock, - AlertTriangle + Lock } from 'lucide-react' const navItems = [ { to: '/admin', icon: LayoutDashboard, label: 'Дашборд', end: true }, { to: '/admin/users', icon: Users, label: 'Пользователи' }, { to: '/admin/marathons', icon: Trophy, label: 'Марафоны' }, - { to: '/admin/disputes', icon: AlertTriangle, label: 'Оспаривания' }, { to: '/admin/logs', icon: ScrollText, label: 'Логи' }, { to: '/admin/broadcast', icon: Send, label: 'Рассылка' }, { to: '/admin/content', icon: FileText, label: 'Контент' }, diff --git a/frontend/src/pages/admin/index.ts b/frontend/src/pages/admin/index.ts index ec6e772..e7822be 100644 --- a/frontend/src/pages/admin/index.ts +++ b/frontend/src/pages/admin/index.ts @@ -2,7 +2,6 @@ export { AdminLayout } from './AdminLayout' export { AdminDashboardPage } from './AdminDashboardPage' export { AdminUsersPage } from './AdminUsersPage' export { AdminMarathonsPage } from './AdminMarathonsPage' -export { AdminDisputesPage } from './AdminDisputesPage' export { AdminLogsPage } from './AdminLogsPage' export { AdminBroadcastPage } from './AdminBroadcastPage' export { AdminContentPage } from './AdminContentPage'