5.0.9: Ожидание бэкенда при завершении теста
All checks were successful
Build and Push Docker Image / build-and-push (push) Successful in 1m5s

This commit is contained in:
poignatov
2026-02-09 16:07:36 +03:00
parent 72da547b80
commit 29bd50acab
3 changed files with 121 additions and 83 deletions

View File

@@ -1 +1 @@
5.0.8
5.0.9

View File

@@ -1,6 +1,6 @@
{
"name": "play-life-web",
"version": "5.0.8",
"version": "5.0.9",
"type": "module",
"scripts": {
"dev": "vite",

View File

@@ -1,4 +1,4 @@
import React, { useState, useEffect, useRef } from 'react'
import React, { useState, useEffect, useRef, useCallback } from 'react'
import { useAuth } from './auth/AuthContext'
import LoadingError from './LoadingError'
import './TestWords.css'
@@ -26,11 +26,14 @@ function TestWords({ onNavigate, wordCount: initialWordCount, configId: initialC
const [error, setError] = useState('')
const [showPreview, setShowPreview] = useState(false) // Показывать ли экран предпросмотра
const [showResults, setShowResults] = useState(false) // Показывать ли экран результатов
const [isClosing, setIsClosing] = useState(false) // Ожидание завершения сохранения при нажатии «Закончить»
const isFinishingRef = useRef(false)
const wordStatsRef = useRef({})
const processingRef = useRef(false)
const cardsShownRef = useRef(0) // Синхронный счётчик для избежания race condition
const saveProgressPromiseRef = useRef(null) // Промис сохранения прогресса (null = ещё не запущено)
const savePayloadRef = useRef(null) // Данные для сохранения при нажатии «Закончить» (если сохранение ещё не запускали)
// Функция равномерного распределения слов в пуле с гарантией максимального расстояния между одинаковыми словами
// excludeFirstWordId - ID слова, которое не должно быть первым в пуле (только что показанная карточка)
@@ -221,6 +224,8 @@ function TestWords({ onNavigate, wordCount: initialWordCount, configId: initialC
setShowResults(false) // Сбрасываем экран результатов при загрузке нового теста
isFinishingRef.current = false
processingRef.current = false
saveProgressPromiseRef.current = null
savePayloadRef.current = null
setLoading(true)
const loadWords = async () => {
@@ -307,21 +312,12 @@ function TestWords({ onNavigate, wordCount: initialWordCount, configId: initialC
})
}
// Завершение теста
const finishTest = async () => {
// Завершение теста: показываем результаты сразу, сохранение на бэкенде идёт в фоне (промис в ref для ожидания в handleFinish)
const finishTest = () => {
if (isFinishingRef.current) return
isFinishingRef.current = true
// Сразу показываем экран результатов, чтобы предотвратить показ новых карточек
setShowResults(true)
// Отправляем статистику на бэкенд
try {
// Получаем актуальные данные из состояния
const currentStats = wordStatsRef.current
// Отправляем все слова, которые были в тесте, с их текущими значениями
// Бэкенд сам обновит только измененные поля
const updates = words.map(word => {
const stats = currentStats[word.id] || {
success: word.success || 0,
@@ -339,7 +335,9 @@ function TestWords({ onNavigate, wordCount: initialWordCount, configId: initialC
})
if (updates.length === 0) {
console.log('No words to send - empty test')
saveProgressPromiseRef.current = Promise.resolve()
savePayloadRef.current = null
setShowResults(true)
return
}
@@ -348,12 +346,10 @@ function TestWords({ onNavigate, wordCount: initialWordCount, configId: initialC
requestBody.config_id = configId
}
console.log('Sending test progress to backend:', {
wordsCount: updates.length,
configId: configId,
requestBody
})
savePayloadRef.current = { requestBody, configId, taskId }
const doSave = async () => {
try {
const response = await authFetch(`${API_URL}/test/progress`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
@@ -365,10 +361,8 @@ function TestWords({ onNavigate, wordCount: initialWordCount, configId: initialC
throw new Error(`Server responded with status ${response.status}: ${errorText}`)
}
const responseData = await response.json().catch(() => ({}))
console.log('Test progress saved successfully:', responseData)
await response.json().catch(() => ({}))
// Если есть taskId, выполняем задачу
if (taskId) {
try {
const completeResponse = await authFetch(`${API_URL}/tasks/${taskId}/complete`, {
@@ -377,9 +371,7 @@ function TestWords({ onNavigate, wordCount: initialWordCount, configId: initialC
body: JSON.stringify({}),
})
if (completeResponse.ok) {
console.log('Task completed successfully')
} else {
if (!completeResponse.ok) {
console.error('Failed to complete task:', await completeResponse.text())
}
} catch (taskErr) {
@@ -388,10 +380,14 @@ function TestWords({ onNavigate, wordCount: initialWordCount, configId: initialC
}
} catch (err) {
console.error('Failed to save progress:', err)
// Можно показать уведомление пользователю, но не блокируем показ результатов
}
}
const promise = doSave()
saveProgressPromiseRef.current = promise
setShowResults(true)
}
// Берём карточку из пула (getAndDelete) и показываем её
const showNextCard = () => {
// Проверяем, не завершился ли тест
@@ -566,8 +562,46 @@ function TestWords({ onNavigate, wordCount: initialWordCount, configId: initialC
showNextCard()
}
const handleFinish = () => {
const runSaveFromPayload = useCallback(async (payload) => {
const { requestBody, taskId: payloadTaskId } = payload
const progressRes = await authFetch(`${API_URL}/test/progress`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(requestBody),
})
if (!progressRes.ok) {
const text = await progressRes.text()
throw new Error(`Progress save failed: ${progressRes.status} ${text}`)
}
if (payloadTaskId) {
const completeRes = await authFetch(`${API_URL}/tasks/${payloadTaskId}/complete`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({}),
})
if (!completeRes.ok) {
const text = await completeRes.text()
throw new Error(`Task complete failed: ${completeRes.status} ${text}`)
}
}
}, [authFetch])
const handleFinish = async () => {
if (isClosing) return
setIsClosing(true)
try {
let promise = saveProgressPromiseRef.current
if (promise == null && savePayloadRef.current) {
promise = runSaveFromPayload(savePayloadRef.current)
saveProgressPromiseRef.current = promise
}
await (promise ?? Promise.resolve())
onNavigate?.('tasks')
} catch (err) {
console.error('Failed to save before close:', err)
} finally {
setIsClosing(false)
}
}
const getRandomSide = (word) => {
@@ -630,8 +664,12 @@ function TestWords({ onNavigate, wordCount: initialWordCount, configId: initialC
})}
</div>
<div className="results-actions">
<button className="test-finish-button" onClick={handleFinish}>
Закончить
<button
className="test-finish-button"
onClick={handleFinish}
disabled={isClosing}
>
{isClosing ? 'Завершение…' : 'Закончить'}
</button>
</div>
</div>