Add modals
This commit is contained in:
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user