Добавлена поддержка обмена играми с типом прохождения (playthrough)

- Обновлены схемы SwapCandidate и SwapRequestChallengeInfo для поддержки прохождений
- get_swap_candidates теперь возвращает и челленджи, и прохождения
- accept_swap_request теперь корректно меняет challenge_id, game_id, is_playthrough и bonus_assignments
- Обновлён UI для отображения прохождений в списке кандидатов и запросах обмена

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-21 15:56:34 +03:00
parent 58c390c768
commit 9f79daf796
4 changed files with 192 additions and 59 deletions

View File

@@ -1054,7 +1054,14 @@ export function PlayPage() {
<p className="text-center text-gray-500 py-8">Нет доступных заданий для копирования</p>
) : (
<div className="space-y-2">
{copycatCandidates.map((candidate) => (
{copycatCandidates.map((candidate) => {
const displayTitle = candidate.is_playthrough
? `Прохождение: ${candidate.game_title}`
: candidate.challenge_title || ''
const displayDetails = candidate.is_playthrough
? `${candidate.playthrough_points || 0} очков`
: `${candidate.game_title}${candidate.challenge_points} очков • ${candidate.challenge_difficulty}`
return (
<button
key={candidate.participant_id}
onClick={() => handleUseCopycat(candidate.participant_id)}
@@ -1062,12 +1069,13 @@ export function PlayPage() {
className="w-full p-3 bg-dark-700/50 hover:bg-dark-700 rounded-xl text-left transition-colors border border-dark-600 hover:border-cyan-500/30 disabled:opacity-50"
>
<p className="text-white font-medium">{candidate.user.nickname}</p>
<p className="text-cyan-400 text-sm">{candidate.challenge_title}</p>
<p className="text-cyan-400 text-sm">{displayTitle}</p>
<p className="text-gray-500 text-xs">
{candidate.game_title} {candidate.challenge_points} очков {candidate.challenge_difficulty}
{displayDetails}
</p>
</button>
))}
)
})}
</div>
)}
</GlassCard>
@@ -1832,7 +1840,14 @@ export function PlayPage() {
Входящие запросы ({swapRequests.incoming.length})
</h4>
<div className="space-y-3">
{swapRequests.incoming.map((request) => (
{swapRequests.incoming.map((request) => {
const challengeTitle = request.from_challenge.is_playthrough
? `Прохождение: ${request.from_challenge.game_title}`
: request.from_challenge.title || ''
const challengeDetails = request.from_challenge.is_playthrough
? `${request.from_challenge.playthrough_points || 0} очков`
: `${request.from_challenge.game_title}${request.from_challenge.points} очков`
return (
<div
key={request.id}
className="p-4 bg-yellow-500/10 border border-yellow-500/30 rounded-xl"
@@ -1843,10 +1858,10 @@ export function PlayPage() {
{request.from_user.nickname} предлагает обмен
</p>
<p className="text-yellow-400 text-sm mt-1">
Вы получите: <span className="font-medium">{request.from_challenge.title}</span>
Вы получите: <span className="font-medium">{challengeTitle}</span>
</p>
<p className="text-gray-400 text-xs">
{request.from_challenge.game_title} {request.from_challenge.points} очков
{challengeDetails}
</p>
</div>
<div className="flex flex-col gap-2">
@@ -1873,7 +1888,8 @@ export function PlayPage() {
</div>
</div>
</div>
))}
)
})}
</div>
</div>
)}
@@ -1886,7 +1902,11 @@ export function PlayPage() {
Отправленные запросы ({swapRequests.outgoing.length})
</h4>
<div className="space-y-3">
{swapRequests.outgoing.map((request) => (
{swapRequests.outgoing.map((request) => {
const challengeTitle = request.to_challenge.is_playthrough
? `Прохождение: ${request.to_challenge.game_title}`
: request.to_challenge.title || ''
return (
<div
key={request.id}
className="p-4 bg-accent-500/10 border border-accent-500/30 rounded-xl"
@@ -1897,7 +1917,7 @@ export function PlayPage() {
Запрос к {request.to_user.nickname}
</p>
<p className="text-accent-400 text-sm mt-1">
Вы получите: <span className="font-medium">{request.to_challenge.title}</span>
Вы получите: <span className="font-medium">{challengeTitle}</span>
</p>
<p className="text-gray-500 text-xs mt-1">
Ожидание подтверждения...
@@ -1915,7 +1935,8 @@ export function PlayPage() {
</NeonButton>
</div>
</div>
))}
)
})}
</div>
</div>
)}
@@ -1943,7 +1964,14 @@ export function PlayPage() {
!swapRequests.outgoing.some(r => r.to_user.id === c.user.id) &&
!swapRequests.incoming.some(r => r.from_user.id === c.user.id)
)
.map((candidate) => (
.map((candidate) => {
const displayTitle = candidate.is_playthrough
? `Прохождение: ${candidate.game_title}`
: candidate.challenge_title || ''
const displayDetails = candidate.is_playthrough
? `${candidate.playthrough_points || 0} очков`
: `${candidate.game_title}${candidate.challenge_points} очков • ${candidate.challenge_difficulty}`
return (
<div
key={candidate.participant_id}
className="p-4 bg-dark-700/50 rounded-xl border border-dark-600"
@@ -1954,10 +1982,10 @@ export function PlayPage() {
{candidate.user.nickname}
</p>
<p className="text-neon-400 text-sm font-medium truncate">
{candidate.challenge_title}
{displayTitle}
</p>
<p className="text-gray-400 text-xs mt-1">
{candidate.game_title} {candidate.challenge_points} очков {candidate.challenge_difficulty}
{displayDetails}
</p>
</div>
<NeonButton
@@ -1966,7 +1994,7 @@ export function PlayPage() {
onClick={() => handleSendSwapRequest(
candidate.participant_id,
candidate.user.nickname,
candidate.challenge_title
displayTitle
)}
isLoading={sendingRequestTo === candidate.participant_id}
disabled={sendingRequestTo !== null}
@@ -1976,7 +2004,8 @@ export function PlayPage() {
</NeonButton>
</div>
</div>
))}
)
})}
</div>
)}
</div>

View File

@@ -321,10 +321,16 @@ export interface DroppedAssignment {
export interface SwapCandidate {
participant_id: number
user: User
challenge_title: string
challenge_description: string
challenge_points: number
challenge_difficulty: Difficulty
is_playthrough: boolean
// Challenge fields (used when is_playthrough=false)
challenge_title: string | null
challenge_description: string | null
challenge_points: number | null
challenge_difficulty: Difficulty | null
// Playthrough fields (used when is_playthrough=true)
playthrough_description: string | null
playthrough_points: number | null
// Common field
game_title: string
}
@@ -332,10 +338,16 @@ export interface SwapCandidate {
export type SwapRequestStatus = 'pending' | 'accepted' | 'declined' | 'cancelled'
export interface SwapRequestChallengeInfo {
title: string
description: string
points: number
difficulty: string
is_playthrough: boolean
// Challenge fields (used when is_playthrough=false)
title: string | null
description: string | null
points: number | null
difficulty: string | null
// Playthrough fields (used when is_playthrough=true)
playthrough_description: string | null
playthrough_points: number | null
// Common field
game_title: string
}