This commit is contained in:
2026-01-03 00:12:07 +07:00
parent d295ff2aff
commit 7a3576aec0
18 changed files with 844 additions and 125 deletions

View File

@@ -5,7 +5,7 @@ import type { Marathon, Assignment, Game, ActiveEvent, SwapCandidate, MySwapRequ
import { NeonButton, GlassCard, StatsCard } 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, Zap, Flame, Target } from 'lucide-react'
import { Loader2, Upload, X, Gamepad2, ArrowLeftRight, Check, XCircle, Clock, Send, Trophy, Users, ArrowLeft, AlertTriangle, Zap, Flame, Target, Download } from 'lucide-react'
import { useToast } from '@/store/toast'
import { useConfirm } from '@/store/confirm'
@@ -25,7 +25,7 @@ export function PlayPage() {
const [activeEvent, setActiveEvent] = useState<ActiveEvent | null>(null)
const [isLoading, setIsLoading] = useState(true)
const [proofFile, setProofFile] = useState<File | null>(null)
const [proofFiles, setProofFiles] = useState<File[]>([])
const [proofUrl, setProofUrl] = useState('')
const [comment, setComment] = useState('')
const [isCompleting, setIsCompleting] = useState(false)
@@ -57,7 +57,7 @@ export function PlayPage() {
// Bonus challenge completion
const [expandedBonusId, setExpandedBonusId] = useState<number | null>(null)
const [bonusProofFile, setBonusProofFile] = useState<File | null>(null)
const [bonusProofFiles, setBonusProofFiles] = useState<File[]>([])
const [bonusProofUrl, setBonusProofUrl] = useState('')
const [bonusComment, setBonusComment] = useState('')
const [isCompletingBonus, setIsCompletingBonus] = useState(false)
@@ -232,12 +232,12 @@ export function PlayPage() {
// For playthrough: allow file, URL, or comment
// For challenges: require file or URL
if (currentAssignment.is_playthrough) {
if (!proofFile && !proofUrl && !comment) {
if (proofFiles.length === 0 && !proofUrl && !comment) {
toast.warning('Пожалуйста, предоставьте доказательство (файл, ссылку или комментарий)')
return
}
} else {
if (!proofFile && !proofUrl) {
if (proofFiles.length === 0 && !proofUrl) {
toast.warning('Пожалуйста, предоставьте доказательство (файл или ссылку)')
return
}
@@ -246,12 +246,12 @@ export function PlayPage() {
setIsCompleting(true)
try {
const result = await wheelApi.complete(currentAssignment.id, {
proof_file: proofFile || undefined,
proof_files: proofFiles.length > 0 ? proofFiles : undefined,
proof_url: proofUrl || undefined,
comment: comment || undefined,
})
toast.success(`Выполнено! +${result.points_earned} очков (бонус серии: +${result.streak_bonus})`)
setProofFile(null)
setProofFiles([])
setProofUrl('')
setComment('')
await loadData()
@@ -291,7 +291,7 @@ export function PlayPage() {
const handleBonusComplete = async (bonusId: number) => {
if (!currentAssignment) return
if (!bonusProofFile && !bonusProofUrl && !bonusComment) {
if (bonusProofFiles.length === 0 && !bonusProofUrl && !bonusComment) {
toast.warning('Прикрепите файл, ссылку или комментарий')
return
}
@@ -302,13 +302,13 @@ export function PlayPage() {
currentAssignment.id,
bonusId,
{
proof_file: bonusProofFile || undefined,
proof_files: bonusProofFiles.length > 0 ? bonusProofFiles : undefined,
proof_url: bonusProofUrl || undefined,
comment: bonusComment || undefined,
}
)
toast.success(`Бонус отмечен! +${result.points_earned} очков начислится при завершении игры`)
setBonusProofFile(null)
setBonusProofFiles([])
setBonusProofUrl('')
setBonusComment('')
setExpandedBonusId(null)
@@ -965,11 +965,28 @@ export function PlayPage() {
<div className="mb-4">
<p className="text-gray-400 text-sm mb-1">Игра</p>
<p className="text-xl font-bold text-white">
{currentAssignment.is_playthrough
? currentAssignment.game?.title
: currentAssignment.challenge?.game.title}
</p>
<div className="flex items-center justify-between gap-3 flex-wrap">
<p className="text-xl font-bold text-white">
{currentAssignment.is_playthrough
? currentAssignment.game?.title
: currentAssignment.challenge?.game.title}
</p>
{(currentAssignment.is_playthrough
? currentAssignment.game?.download_url
: currentAssignment.challenge?.game.download_url) && (
<a
href={currentAssignment.is_playthrough
? currentAssignment.game?.download_url
: currentAssignment.challenge?.game.download_url}
target="_blank"
rel="noopener noreferrer"
className="px-3 py-1.5 bg-neon-500/20 text-neon-400 rounded-lg text-sm font-medium border border-neon-500/30 flex items-center gap-1.5 hover:bg-neon-500/30 transition-colors"
>
<Download className="w-4 h-4" />
Скачать игру
</a>
)}
</div>
</div>
{currentAssignment.is_playthrough ? (
@@ -1023,7 +1040,7 @@ export function PlayPage() {
onClick={() => {
if (bonus.status === 'pending') {
setExpandedBonusId(expandedBonusId === bonus.id ? null : bonus.id)
setBonusProofFile(null)
setBonusProofFiles([])
setBonusProofUrl('')
setBonusComment('')
if (bonusFileInputRef.current) bonusFileInputRef.current.value = ''
@@ -1062,24 +1079,40 @@ export function PlayPage() {
ref={bonusFileInputRef}
type="file"
accept="image/*,video/*"
multiple
className="hidden"
onChange={(e) => {
e.stopPropagation()
validateAndSetFile(e.target.files?.[0] || null, setBonusProofFile, bonusFileInputRef)
const files = Array.from(e.target.files || [])
setBonusProofFiles(prev => [...prev, ...files])
e.target.value = ''
}}
/>
{bonusProofFile ? (
<div className="flex items-center gap-2 p-2 bg-dark-700/50 rounded-lg border border-dark-600">
<span className="text-white text-sm flex-1 truncate">{bonusProofFile.name}</span>
{bonusProofFiles.length > 0 ? (
<div className="space-y-2">
{bonusProofFiles.map((file, index) => (
<div key={index} className="flex items-center gap-2 p-2 bg-dark-700/50 rounded-lg border border-dark-600">
<span className="text-white text-sm flex-1 truncate">{file.name}</span>
<button
onClick={(e) => {
e.stopPropagation()
setBonusProofFiles(prev => prev.filter((_, i) => i !== index))
}}
className="p-1 rounded text-gray-400 hover:text-white hover:bg-dark-600 transition-colors"
>
<X className="w-3 h-3" />
</button>
</div>
))}
<button
onClick={(e) => {
e.stopPropagation()
setBonusProofFile(null)
if (bonusFileInputRef.current) bonusFileInputRef.current.value = ''
bonusFileInputRef.current?.click()
}}
className="p-1 rounded text-gray-400 hover:text-white hover:bg-dark-600 transition-colors"
className="w-full p-2 border border-dashed border-neon-500/30 rounded-lg text-neon-400 hover:border-neon-500/50 hover:bg-neon-500/5 transition-all text-sm flex items-center justify-center gap-2"
>
<X className="w-3 h-3" />
<Upload className="w-4 h-4" />
Добавить еще файл
</button>
</div>
) : (
@@ -1121,7 +1154,7 @@ export function PlayPage() {
handleBonusComplete(bonus.id)
}}
isLoading={isCompletingBonus}
disabled={!bonusProofFile && !bonusProofUrl && !bonusComment}
disabled={bonusProofFiles.length === 0 && !bonusProofUrl && !bonusComment}
icon={<Check className="w-3 h-3" />}
>
Выполнено
@@ -1131,7 +1164,7 @@ export function PlayPage() {
variant="outline"
onClick={(e) => {
e.stopPropagation()
setBonusProofFile(null)
setBonusProofFiles([])
setBonusProofUrl('')
setBonusComment('')
setExpandedBonusId(null)
@@ -1202,19 +1235,37 @@ export function PlayPage() {
ref={fileInputRef}
type="file"
accept="image/*,video/*"
multiple
className="hidden"
onChange={(e) => validateAndSetFile(e.target.files?.[0] || null, setProofFile, fileInputRef)}
onChange={(e) => {
const files = Array.from(e.target.files || [])
setProofFiles(prev => [...prev, ...files])
// Reset input to allow selecting same files again
e.target.value = ''
}}
/>
{proofFile ? (
<div className="flex items-center gap-2 p-3 bg-dark-700/50 rounded-xl border border-dark-600">
<span className="text-white flex-1 truncate">{proofFile.name}</span>
<button
onClick={() => setProofFile(null)}
className="p-1.5 rounded-lg text-gray-400 hover:text-white hover:bg-dark-600 transition-colors"
{proofFiles.length > 0 ? (
<div className="space-y-2">
{proofFiles.map((file, index) => (
<div key={index} className="flex items-center gap-2 p-3 bg-dark-700/50 rounded-xl border border-dark-600">
<span className="text-white flex-1 truncate">{file.name}</span>
<button
onClick={() => setProofFiles(proofFiles.filter((_, i) => i !== index))}
className="p-1.5 rounded-lg text-gray-400 hover:text-white hover:bg-dark-600 transition-colors"
>
<X className="w-4 h-4" />
</button>
</div>
))}
<NeonButton
variant="outline"
className="w-full"
onClick={() => fileInputRef.current?.click()}
icon={<Upload className="w-4 h-4" />}
>
<X className="w-4 h-4" />
</button>
Добавить ещё файлы
</NeonButton>
</div>
) : (
<div>
@@ -1224,10 +1275,10 @@ export function PlayPage() {
onClick={() => fileInputRef.current?.click()}
icon={<Upload className="w-4 h-4" />}
>
Выбрать файл
Выбрать файлы
</NeonButton>
<p className="text-xs text-gray-500 mt-2 text-center">
Макс. 15 МБ для изображений, 30 МБ для видео
Можно выбрать несколько файлов. Макс. 15 МБ для изображений, 30 МБ для видео
</p>
</div>
)}
@@ -1257,8 +1308,8 @@ export function PlayPage() {
onClick={handleComplete}
isLoading={isCompleting}
disabled={currentAssignment.is_playthrough
? (!proofFile && !proofUrl && !comment)
: (!proofFile && !proofUrl)
? (proofFiles.length === 0 && !proofUrl && !comment)
: (proofFiles.length === 0 && !proofUrl)
}
icon={<Check className="w-4 h-4" />}
>