Files
play-life/play-life-web/src/components/WordList.jsx
poignatov c911950cc1
All checks were successful
Build and Push Docker Image / build-and-push (push) Successful in 1m3s
6.4.8: Фикс кнопки назад при создании словаря
2026-03-08 19:56:25 +03:00

336 lines
11 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import React, { useState, useEffect } from 'react'
import { useAuth } from './auth/AuthContext'
import LoadingError from './LoadingError'
import './WordList.css'
const API_URL = '/api'
function WordList({ onNavigate, dictionaryId, isNewDictionary, refreshTrigger = 0 }) {
const { authFetch } = useAuth()
const [words, setWords] = useState([])
const [loading, setLoading] = useState(true)
const [error, setError] = useState('')
const [dictionary, setDictionary] = useState(null)
const [dictionaryName, setDictionaryName] = useState('')
const [originalDictionaryName, setOriginalDictionaryName] = useState('')
const [isSavingName, setIsSavingName] = useState(false)
const [selectedWord, setSelectedWord] = useState(null)
// Normalize undefined to null for clarity: new dictionary if dictionaryId is null or undefined
const [currentDictionaryId, setCurrentDictionaryId] = useState(dictionaryId ?? null)
// isNewDict is computed from currentDictionaryId: new dictionary if currentDictionaryId == null
const isNewDict = currentDictionaryId == null
// Helper function to check if dictionary exists and is not new
const hasValidDictionary = (dictId) => {
return dictId !== undefined && dictId !== null
}
useEffect(() => {
// Normalize undefined to null: if dictionaryId is undefined, treat it as null (new dictionary)
const normalizedDictionaryId = dictionaryId ?? null
setCurrentDictionaryId(normalizedDictionaryId)
if (normalizedDictionaryId == null) {
setLoading(false)
setDictionary(null)
setDictionaryName('')
setOriginalDictionaryName('')
setWords([])
} else if (hasValidDictionary(normalizedDictionaryId)) {
fetchDictionary(normalizedDictionaryId)
fetchWords(normalizedDictionaryId)
} else {
setLoading(false)
setWords([])
}
}, [dictionaryId, refreshTrigger])
const fetchDictionary = async (dictId) => {
try {
const response = await authFetch(`${API_URL}/dictionaries`)
if (!response.ok) {
throw new Error('Ошибка при загрузке словарей')
}
const dictionaries = await response.json()
const dict = dictionaries.find(d => d.id === dictId)
if (dict) {
setDictionary(dict)
setDictionaryName(dict.name)
setOriginalDictionaryName(dict.name)
}
} catch (err) {
console.error('Error fetching dictionary:', err)
}
}
const fetchWords = async (dictId) => {
if (!hasValidDictionary(dictId)) {
setWords([])
setLoading(false)
return
}
await fetchWordsForDictionary(dictId)
}
const fetchWordsForDictionary = async (dictId) => {
try {
setLoading(true)
const url = `${API_URL}/words?dictionary_id=${dictId}`
const response = await authFetch(url)
if (!response.ok) {
throw new Error('Ошибка при загрузке слов')
}
const data = await response.json()
setWords(Array.isArray(data) ? data : [])
setError('')
} catch (err) {
setError(err.message)
setWords([])
} finally {
setLoading(false)
}
}
const handleWordClick = (word) => {
setSelectedWord(word)
}
const handleDeleteWord = async () => {
if (!selectedWord) return
if (!window.confirm('Вы уверены, что хотите удалить это слово?')) {
return
}
try {
const response = await authFetch(`${API_URL}/words/${selectedWord.id}`, {
method: 'DELETE',
})
if (!response.ok) {
throw new Error('Ошибка при удалении слова')
}
// Remove word from local state
setWords(words.filter(word => word.id !== selectedWord.id))
setSelectedWord(null)
} catch (err) {
setError(err.message)
}
}
const handleResetProgress = async () => {
if (!selectedWord) return
if (!window.confirm('Вы уверены, что хотите сбросить прогресс этого слова?')) {
return
}
try {
const response = await authFetch(`${API_URL}/words/${selectedWord.id}/reset-progress`, {
method: 'POST',
})
if (!response.ok) {
throw new Error('Ошибка при сбросе прогресса')
}
// Update word in local state - reset progress fields
setWords(words.map(word =>
word.id === selectedWord.id
? { ...word, success: 0, failure: 0, last_success_at: null, last_failure_at: null }
: word
))
setSelectedWord(null)
} catch (err) {
setError(err.message)
}
}
const handleNameChange = (e) => {
setDictionaryName(e.target.value)
}
const handleNameSave = async () => {
if (!dictionaryName.trim()) {
return
}
setIsSavingName(true)
try {
if (!hasValidDictionary(currentDictionaryId)) {
// Create new dictionary
const response = await authFetch(`${API_URL}/dictionaries`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ name: dictionaryName.trim() }),
})
if (!response.ok) {
throw new Error('Ошибка при создании словаря')
}
const newDict = await response.json()
const newDictionaryId = newDict.id
// Update local state
setOriginalDictionaryName(newDict.name)
setDictionaryName(newDict.name)
setDictionary(newDict)
setCurrentDictionaryId(newDictionaryId)
// Reinitialize screen: fetch dictionary info and words for the new dictionary
await fetchDictionary(newDictionaryId)
await fetchWordsForDictionary(newDictionaryId)
// Update navigation to use the new dictionary ID and name (replace history entry so back goes to dictionaries)
onNavigate?.('words', { dictionaryId: newDictionaryId, dictionaryName: newDict.name }, { replace: true })
} else if (hasValidDictionary(currentDictionaryId)) {
// Update existing dictionary (rename)
const response = await authFetch(`${API_URL}/dictionaries/${currentDictionaryId}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ name: dictionaryName.trim() }),
})
if (!response.ok) {
throw new Error('Ошибка при обновлении словаря')
}
setOriginalDictionaryName(dictionaryName.trim())
if (dictionary) {
setDictionary({ ...dictionary, name: dictionaryName.trim() })
}
}
} catch (err) {
setError(err.message)
} finally {
setIsSavingName(false)
}
}
// Show save button only if name is not empty and has changed
const showSaveButton = dictionaryName.trim() !== '' && dictionaryName.trim() !== originalDictionaryName
if (error && !loading) {
return (
<div className="word-list">
<LoadingError onRetry={() => {
if (hasValidDictionary(currentDictionaryId)) {
fetchWordsForDictionary(currentDictionaryId)
}
}} />
</div>
)
}
return (
<div className="word-list">
<button
onClick={() => window.history.back()}
className="close-x-button"
title="Закрыть"
>
</button>
{loading && (
<div className="fixed inset-0 flex justify-center items-center">
<div className="flex flex-col items-center">
<div className="w-12 h-12 border-4 border-indigo-200 border-t-indigo-600 rounded-full animate-spin mb-4"></div>
<div className="text-gray-600 font-medium">Загрузка...</div>
</div>
</div>
)}
{/* Dictionary name input */}
<div className="dictionary-name-input-container">
<input
type="text"
className="dictionary-name-input"
value={dictionaryName}
onChange={handleNameChange}
placeholder="Введите название словаря"
/>
{showSaveButton && (
<button
className="dictionary-name-save-button"
onClick={handleNameSave}
disabled={isSavingName}
title="Сохранить название"
>
</button>
)}
</div>
{/* Show add button and words list only if dictionaryId exists and is not new */}
{hasValidDictionary(currentDictionaryId) && (
<>
{(!words || words.length === 0) ? (
<>
<div className="empty-state">
<p>Слов пока нет. Добавьте слова через экран "Добавить слова".</p>
</div>
</>
) : (
<>
<div className="words-grid">
{words.map((word) => (
<div
key={word.id}
className="word-card"
onClick={() => handleWordClick(word)}
>
<div className="word-content">
<div className="word-header">
<h3 className="word-name">{word.name}</h3>
</div>
<div className="word-translation">{word.translation}</div>
{word.description && (
<div className="word-description">{word.description}</div>
)}
</div>
<div className="word-stats">
<span className="stat-success">{word.success || 0}</span>
<span className="stat-separator"> | </span>
<span className="stat-failure">{word.failure || 0}</span>
</div>
</div>
))}
</div>
</>
)}
</>
)}
{/* Модальное окно для действий со словом */}
{selectedWord && (
<div className="word-modal-overlay" onClick={() => setSelectedWord(null)}>
<div className="word-modal" onClick={(e) => e.stopPropagation()}>
<div className="word-modal-header">
<h3>{selectedWord.name}</h3>
</div>
<div className="word-modal-actions">
<button className="word-modal-reset" onClick={handleResetProgress}>
Сбросить
</button>
<button className="word-modal-delete" onClick={handleDeleteWord}>
Удалить
</button>
</div>
</div>
</div>
)}
</div>
)
}
export default WordList