Add modals

This commit is contained in:
2025-12-16 01:50:40 +07:00
parent 87ecd9756c
commit 574140e67d
11 changed files with 439 additions and 40 deletions

View File

@@ -4,6 +4,7 @@ import { assignmentsApi } from '@/api'
import type { AssignmentDetail } from '@/types'
import { Card, CardContent, Button } 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,
@@ -14,6 +15,7 @@ export function AssignmentDetailPage() {
const { id } = useParams<{ id: string }>()
const navigate = useNavigate()
const user = useAuthStore((state) => state.user)
const toast = useToast()
const [assignment, setAssignment] = useState<AssignmentDetail | null>(null)
const [isLoading, setIsLoading] = useState(true)
@@ -78,7 +80,7 @@ export function AssignmentDetailPage() {
await loadAssignment()
} catch (err: unknown) {
const error = err as { response?: { data?: { detail?: string } } }
alert(error.response?.data?.detail || 'Не удалось создать оспаривание')
toast.error(error.response?.data?.detail || 'Не удалось создать оспаривание')
} finally {
setIsCreatingDispute(false)
}
@@ -93,7 +95,7 @@ export function AssignmentDetailPage() {
await loadAssignment()
} catch (err: unknown) {
const error = err as { response?: { data?: { detail?: string } } }
alert(error.response?.data?.detail || 'Не удалось проголосовать')
toast.error(error.response?.data?.detail || 'Не удалось проголосовать')
} finally {
setIsVoting(false)
}
@@ -109,7 +111,7 @@ export function AssignmentDetailPage() {
await loadAssignment()
} catch (err: unknown) {
const error = err as { response?: { data?: { detail?: string } } }
alert(error.response?.data?.detail || 'Не удалось добавить комментарий')
toast.error(error.response?.data?.detail || 'Не удалось добавить комментарий')
} finally {
setIsAddingComment(false)
}

View File

@@ -4,6 +4,8 @@ import { marathonsApi, gamesApi } from '@/api'
import type { Marathon, Game, Challenge, ChallengePreview } from '@/types'
import { Button, Input, Card, CardContent, CardHeader, CardTitle } from '@/components/ui'
import { useAuthStore } from '@/store/auth'
import { useToast } from '@/store/toast'
import { useConfirm } from '@/store/confirm'
import {
Plus, Trash2, Sparkles, Play, Loader2, Gamepad2, X, Save, Eye,
ChevronDown, ChevronUp, Edit2, Check, CheckCircle, XCircle, Clock, User, ArrowLeft
@@ -13,6 +15,8 @@ export function LobbyPage() {
const { id } = useParams<{ id: string }>()
const navigate = useNavigate()
const user = useAuthStore((state) => state.user)
const toast = useToast()
const confirm = useConfirm()
const [marathon, setMarathon] = useState<Marathon | null>(null)
const [games, setGames] = useState<Game[]>([])
@@ -99,7 +103,14 @@ export function LobbyPage() {
}
const handleDeleteGame = async (gameId: number) => {
if (!confirm('Удалить эту игру?')) return
const confirmed = await confirm({
title: 'Удалить игру?',
message: 'Игра и все её челленджи будут удалены.',
confirmText: 'Удалить',
cancelText: 'Отмена',
variant: 'danger',
})
if (!confirmed) return
try {
await gamesApi.delete(gameId)
@@ -122,7 +133,14 @@ export function LobbyPage() {
}
const handleRejectGame = async (gameId: number) => {
if (!confirm('Отклонить эту игру?')) return
const confirmed = await confirm({
title: 'Отклонить игру?',
message: 'Игра будет удалена из списка ожидающих.',
confirmText: 'Отклонить',
cancelText: 'Отмена',
variant: 'danger',
})
if (!confirmed) return
setModeratingGameId(gameId)
try {
@@ -157,7 +175,14 @@ export function LobbyPage() {
}
const handleDeleteChallenge = async (challengeId: number, gameId: number) => {
if (!confirm('Удалить это задание?')) return
const confirmed = await confirm({
title: 'Удалить задание?',
message: 'Это действие нельзя отменить.',
confirmText: 'Удалить',
cancelText: 'Отмена',
variant: 'danger',
})
if (!confirmed) return
try {
await gamesApi.deleteChallenge(challengeId)
@@ -227,7 +252,16 @@ export function LobbyPage() {
}
const handleStartMarathon = async () => {
if (!id || !confirm('Начать марафон? После этого нельзя будет добавить новые игры.')) return
if (!id) return
const confirmed = await confirm({
title: 'Начать марафон?',
message: 'После старта нельзя будет добавить новые игры.',
confirmText: 'Начать',
cancelText: 'Отмена',
variant: 'warning',
})
if (!confirmed) return
setIsStarting(true)
try {
@@ -235,7 +269,7 @@ export function LobbyPage() {
navigate(`/marathons/${id}/play`)
} catch (err: unknown) {
const error = err as { response?: { data?: { detail?: string } } }
alert(error.response?.data?.detail || 'Не удалось запустить марафон')
toast.error(error.response?.data?.detail || 'Не удалось запустить марафон')
} finally {
setIsStarting(false)
}

View File

@@ -4,6 +4,8 @@ import { marathonsApi, eventsApi, challengesApi } from '@/api'
import type { Marathon, ActiveEvent, Challenge } from '@/types'
import { Button, Card, CardContent } from '@/components/ui'
import { useAuthStore } from '@/store/auth'
import { useToast } from '@/store/toast'
import { useConfirm } from '@/store/confirm'
import { EventBanner } from '@/components/EventBanner'
import { EventControl } from '@/components/EventControl'
import { ActivityFeed, type ActivityFeedRef } from '@/components/ActivityFeed'
@@ -14,6 +16,8 @@ export function MarathonPage() {
const { id } = useParams<{ id: string }>()
const navigate = useNavigate()
const user = useAuthStore((state) => state.user)
const toast = useToast()
const confirm = useConfirm()
const [marathon, setMarathon] = useState<Marathon | null>(null)
const [activeEvent, setActiveEvent] = useState<ActiveEvent | null>(null)
const [challenges, setChallenges] = useState<Challenge[]>([])
@@ -83,7 +87,16 @@ export function MarathonPage() {
}
const handleDelete = async () => {
if (!marathon || !confirm('Вы уверены, что хотите удалить этот марафон? Это действие нельзя отменить.')) return
if (!marathon) return
const confirmed = await confirm({
title: 'Удалить марафон?',
message: 'Все данные марафона будут удалены безвозвратно.',
confirmText: 'Удалить',
cancelText: 'Отмена',
variant: 'danger',
})
if (!confirmed) return
setIsDeleting(true)
try {
@@ -91,7 +104,7 @@ export function MarathonPage() {
navigate('/marathons')
} catch (error) {
console.error('Failed to delete marathon:', error)
alert('Не удалось удалить марафон')
toast.error('Не удалось удалить марафон')
} finally {
setIsDeleting(false)
}
@@ -106,7 +119,7 @@ export function MarathonPage() {
setMarathon(updated)
} catch (err: unknown) {
const error = err as { response?: { data?: { detail?: string } } }
alert(error.response?.data?.detail || 'Не удалось присоединиться')
toast.error(error.response?.data?.detail || 'Не удалось присоединиться')
} finally {
setIsJoining(false)
}

View File

@@ -6,9 +6,13 @@ import { Button, Card, CardContent } from '@/components/ui'
import { SpinWheel } from '@/components/SpinWheel'
import { EventBanner } from '@/components/EventBanner'
import { Loader2, Upload, X, Gamepad2, ArrowLeftRight, Check, XCircle, Clock, Send, Trophy, Users, ArrowLeft, AlertTriangle } from 'lucide-react'
import { useToast } from '@/store/toast'
import { useConfirm } from '@/store/confirm'
export function PlayPage() {
const { id } = useParams<{ id: string }>()
const toast = useToast()
const confirm = useConfirm()
const [marathon, setMarathon] = useState<Marathon | null>(null)
const [currentAssignment, setCurrentAssignment] = useState<Assignment | null>(null)
@@ -99,7 +103,7 @@ export function PlayPage() {
setGameChoiceChallenges(challenges)
} catch (error) {
console.error('Failed to load game choice challenges:', error)
alert('Не удалось загрузить челленджи для этой игры')
toast.error('Не удалось загрузить челленджи для этой игры')
} finally {
setIsLoadingChallenges(false)
}
@@ -181,7 +185,7 @@ export function PlayPage() {
return result.game
} catch (err: unknown) {
const error = err as { response?: { data?: { detail?: string } } }
alert(error.response?.data?.detail || 'Не удалось крутить')
toast.error(error.response?.data?.detail || 'Не удалось крутить')
return null
}
}
@@ -196,7 +200,7 @@ export function PlayPage() {
const handleComplete = async () => {
if (!currentAssignment) return
if (!proofFile && !proofUrl) {
alert('Пожалуйста, предоставьте доказательство (файл или ссылку)')
toast.warning('Пожалуйста, предоставьте доказательство (файл или ссылку)')
return
}
@@ -208,7 +212,7 @@ export function PlayPage() {
comment: comment || undefined,
})
alert(`Выполнено! +${result.points_earned} очков (бонус серии: +${result.streak_bonus})`)
toast.success(`Выполнено! +${result.points_earned} очков (бонус серии: +${result.streak_bonus})`)
// Reset form
setProofFile(null)
@@ -219,7 +223,7 @@ export function PlayPage() {
await loadData()
} catch (err: unknown) {
const error = err as { response?: { data?: { detail?: string } } }
alert(error.response?.data?.detail || 'Не удалось выполнить')
toast.error(error.response?.data?.detail || 'Не удалось выполнить')
} finally {
setIsCompleting(false)
}
@@ -229,18 +233,25 @@ export function PlayPage() {
if (!currentAssignment) return
const penalty = spinResult?.drop_penalty || 0
if (!confirm(`Пропустить это задание? Вы потеряете ${penalty} очков.`)) return
const confirmed = await confirm({
title: 'Пропустить задание?',
message: `Вы потеряете ${penalty} очков.`,
confirmText: 'Пропустить',
cancelText: 'Отмена',
variant: 'warning',
})
if (!confirmed) return
setIsDropping(true)
try {
const result = await wheelApi.drop(currentAssignment.id)
alert(`Пропущено. Штраф: -${result.penalty} очков`)
toast.info(`Пропущено. Штраф: -${result.penalty} очков`)
setSpinResult(null)
await loadData()
} catch (err: unknown) {
const error = err as { response?: { data?: { detail?: string } } }
alert(error.response?.data?.detail || 'Не удалось пропустить')
toast.error(error.response?.data?.detail || 'Не удалось пропустить')
} finally {
setIsDropping(false)
}
@@ -249,7 +260,7 @@ export function PlayPage() {
const handleEventComplete = async () => {
if (!eventAssignment?.assignment) return
if (!eventProofFile && !eventProofUrl) {
alert('Пожалуйста, предоставьте доказательство (файл или ссылку)')
toast.warning('Пожалуйста, предоставьте доказательство (файл или ссылку)')
return
}
@@ -261,7 +272,7 @@ export function PlayPage() {
comment: eventComment || undefined,
})
alert(`Выполнено! +${result.points_earned} очков`)
toast.success(`Выполнено! +${result.points_earned} очков`)
// Reset form
setEventProofFile(null)
@@ -271,7 +282,7 @@ export function PlayPage() {
await loadData()
} catch (err: unknown) {
const error = err as { response?: { data?: { detail?: string } } }
alert(error.response?.data?.detail || 'Не удалось выполнить')
toast.error(error.response?.data?.detail || 'Не удалось выполнить')
} finally {
setIsEventCompleting(false)
}
@@ -286,22 +297,27 @@ export function PlayPage() {
if (!id) return
const hasActiveAssignment = !!currentAssignment
const confirmMessage = hasActiveAssignment
? 'Выбрать этот челлендж? Текущее задание будет заменено без штрафа.'
: 'Выбрать этот челлендж?'
if (!confirm(confirmMessage)) return
const confirmed = await confirm({
title: 'Выбрать челлендж?',
message: hasActiveAssignment
? 'Текущее задание будет заменено без штрафа.'
: 'Вы уверены, что хотите выбрать этот челлендж?',
confirmText: 'Выбрать',
cancelText: 'Отмена',
variant: 'info',
})
if (!confirmed) return
setIsSelectingChallenge(true)
try {
const result = await eventsApi.selectGameChoiceChallenge(parseInt(id), challengeId)
alert(result.message)
toast.success(result.message)
setSelectedGameId(null)
setGameChoiceChallenges(null)
await loadData()
} catch (err: unknown) {
const error = err as { response?: { data?: { detail?: string } } }
alert(error.response?.data?.detail || 'Не удалось выбрать челлендж')
toast.error(error.response?.data?.detail || 'Не удалось выбрать челлендж')
} finally {
setIsSelectingChallenge(false)
}
@@ -310,17 +326,24 @@ export function PlayPage() {
const handleSendSwapRequest = async (participantId: number, participantName: string, theirChallenge: string) => {
if (!id) return
if (!confirm(`Отправить запрос на обмен с ${participantName}?\n\nВы предлагаете обменяться на: "${theirChallenge}"\n\n${participantName} должен будет подтвердить обмен.`)) return
const confirmed = await confirm({
title: 'Отправить запрос на обмен?',
message: `Вы предлагаете обменяться с ${participantName} на:\n"${theirChallenge}"\n\n${participantName} должен будет подтвердить обмен.`,
confirmText: 'Отправить',
cancelText: 'Отмена',
variant: 'info',
})
if (!confirmed) return
setSendingRequestTo(participantId)
try {
await eventsApi.createSwapRequest(parseInt(id), participantId)
alert('Запрос на обмен отправлен! Ожидайте подтверждения.')
toast.success('Запрос на обмен отправлен! Ожидайте подтверждения.')
await loadSwapRequests()
await loadSwapCandidates()
} catch (err: unknown) {
const error = err as { response?: { data?: { detail?: string } } }
alert(error.response?.data?.detail || 'Не удалось отправить запрос')
toast.error(error.response?.data?.detail || 'Не удалось отправить запрос')
} finally {
setSendingRequestTo(null)
}
@@ -329,16 +352,23 @@ export function PlayPage() {
const handleAcceptSwapRequest = async (requestId: number) => {
if (!id) return
if (!confirm('Принять обмен? Задания будут обменяны сразу после подтверждения.')) return
const confirmed = await confirm({
title: 'Принять обмен?',
message: 'Задания будут обменяны сразу после подтверждения.',
confirmText: 'Принять',
cancelText: 'Отмена',
variant: 'info',
})
if (!confirmed) return
setProcessingRequestId(requestId)
try {
await eventsApi.acceptSwapRequest(parseInt(id), requestId)
alert('Обмен выполнен!')
toast.success('Обмен выполнен!')
await loadData()
} catch (err: unknown) {
const error = err as { response?: { data?: { detail?: string } } }
alert(error.response?.data?.detail || 'Не удалось выполнить обмен')
toast.error(error.response?.data?.detail || 'Не удалось выполнить обмен')
} finally {
setProcessingRequestId(null)
}
@@ -353,7 +383,7 @@ export function PlayPage() {
await loadSwapRequests()
} catch (err: unknown) {
const error = err as { response?: { data?: { detail?: string } } }
alert(error.response?.data?.detail || 'Не удалось отклонить запрос')
toast.error(error.response?.data?.detail || 'Не удалось отклонить запрос')
} finally {
setProcessingRequestId(null)
}
@@ -369,7 +399,7 @@ export function PlayPage() {
await loadSwapCandidates()
} catch (err: unknown) {
const error = err as { response?: { data?: { detail?: string } } }
alert(error.response?.data?.detail || 'Не удалось отменить запрос')
toast.error(error.response?.data?.detail || 'Не удалось отменить запрос')
} finally {
setProcessingRequestId(null)
}