2026-01-13 18:22:02 +03:00
|
|
|
import React, { useState, useEffect, useRef } from 'react'
|
|
|
|
|
import { useAuth } from './auth/AuthContext'
|
|
|
|
|
import LoadingError from './LoadingError'
|
|
|
|
|
import './DictionaryList.css'
|
|
|
|
|
|
|
|
|
|
const API_URL = '/api'
|
|
|
|
|
|
|
|
|
|
function DictionaryList({ onNavigate, refreshTrigger = 0 }) {
|
|
|
|
|
const { authFetch } = useAuth()
|
|
|
|
|
const [dictionaries, setDictionaries] = useState([])
|
|
|
|
|
const [loading, setLoading] = useState(true)
|
|
|
|
|
const [error, setError] = useState('')
|
|
|
|
|
const [selectedDictionary, setSelectedDictionary] = useState(null)
|
|
|
|
|
const isInitializedRef = useRef(false)
|
|
|
|
|
const dictionariesRef = useRef([])
|
|
|
|
|
|
|
|
|
|
// Обновляем ref при изменении состояния
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
dictionariesRef.current = dictionaries
|
|
|
|
|
}, [dictionaries])
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
fetchDictionaries()
|
|
|
|
|
}, [refreshTrigger])
|
|
|
|
|
|
|
|
|
|
const fetchDictionaries = async () => {
|
|
|
|
|
try {
|
|
|
|
|
// Показываем загрузку только при первой инициализации или если нет данных для отображения
|
|
|
|
|
const isFirstLoad = !isInitializedRef.current
|
|
|
|
|
const hasData = !isFirstLoad && dictionariesRef.current.length > 0
|
|
|
|
|
if (!hasData) {
|
|
|
|
|
setLoading(true)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const response = await authFetch(`${API_URL}/test-configs-and-dictionaries`)
|
|
|
|
|
if (!response.ok) {
|
|
|
|
|
throw new Error('Ошибка при загрузке словарей')
|
|
|
|
|
}
|
|
|
|
|
const data = await response.json()
|
|
|
|
|
setDictionaries(Array.isArray(data.dictionaries) ? data.dictionaries : [])
|
|
|
|
|
setError('')
|
|
|
|
|
isInitializedRef.current = true
|
|
|
|
|
} catch (err) {
|
|
|
|
|
setError(err.message)
|
|
|
|
|
setDictionaries([])
|
|
|
|
|
isInitializedRef.current = true
|
|
|
|
|
} finally {
|
|
|
|
|
setLoading(false)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const handleDictionarySelect = (dict) => {
|
|
|
|
|
onNavigate?.('words', { dictionaryId: dict.id })
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const handleDictionaryMenuClick = (dict, e) => {
|
|
|
|
|
e.stopPropagation()
|
|
|
|
|
setSelectedDictionary(dict)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const handleDictionaryDelete = async () => {
|
|
|
|
|
if (!selectedDictionary) return
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
const response = await authFetch(`${API_URL}/dictionaries/${selectedDictionary.id}`, {
|
|
|
|
|
method: 'DELETE',
|
|
|
|
|
headers: {
|
|
|
|
|
'Content-Type': 'application/json',
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
if (!response.ok) {
|
|
|
|
|
const errorText = await response.text()
|
|
|
|
|
console.error('Delete error:', response.status, errorText)
|
|
|
|
|
throw new Error(`Ошибка при удалении словаря: ${response.status}`)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
setSelectedDictionary(null)
|
|
|
|
|
// Refresh dictionaries list
|
|
|
|
|
await fetchDictionaries()
|
|
|
|
|
} catch (err) {
|
|
|
|
|
console.error('Delete failed:', err)
|
|
|
|
|
setError(err.message)
|
|
|
|
|
setSelectedDictionary(null)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const closeDictionaryModal = () => {
|
|
|
|
|
setSelectedDictionary(null)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div className="dictionary-list">
|
2026-01-14 18:03:29 +03:00
|
|
|
{/* Кнопка закрытия */}
|
2026-01-20 21:33:23 +03:00
|
|
|
<button
|
2026-01-14 18:03:29 +03:00
|
|
|
className="dictionary-close-button"
|
2026-01-13 18:22:02 +03:00
|
|
|
onClick={() => onNavigate?.('profile')}
|
2026-01-14 18:03:29 +03:00
|
|
|
title="Закрыть"
|
2026-01-13 18:22:02 +03:00
|
|
|
>
|
2026-01-14 18:03:29 +03:00
|
|
|
✕
|
2026-01-13 18:22:02 +03:00
|
|
|
</button>
|
2026-01-20 21:33:23 +03:00
|
|
|
{/* Показываем загрузку только при первой инициализации и если нет данных для отображения */}
|
|
|
|
|
{loading && !isInitializedRef.current && dictionaries.length === 0 ? (
|
|
|
|
|
<div className="fixed inset-0 bottom-20 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>
|
|
|
|
|
) : error ? (
|
|
|
|
|
<LoadingError onRetry={fetchDictionaries} />
|
|
|
|
|
) : (
|
|
|
|
|
<div className="dictionaries-grid">
|
2026-01-13 18:22:02 +03:00
|
|
|
{dictionaries.map((dict) => (
|
|
|
|
|
<div
|
|
|
|
|
key={dict.id}
|
|
|
|
|
className="dictionary-card"
|
|
|
|
|
onClick={() => handleDictionarySelect(dict)}
|
|
|
|
|
>
|
|
|
|
|
<button
|
|
|
|
|
onClick={(e) => handleDictionaryMenuClick(dict, e)}
|
|
|
|
|
className="dictionary-menu-button"
|
|
|
|
|
title="Меню"
|
|
|
|
|
>
|
|
|
|
|
⋮
|
|
|
|
|
</button>
|
|
|
|
|
<div className="dictionary-words-count">
|
|
|
|
|
{dict.wordsCount}
|
|
|
|
|
</div>
|
|
|
|
|
<div className="dictionary-name">{dict.name}</div>
|
|
|
|
|
</div>
|
|
|
|
|
))}
|
|
|
|
|
<button
|
|
|
|
|
onClick={() => onNavigate?.('words', { dictionaryId: null, isNewDictionary: true })}
|
|
|
|
|
className="add-dictionary-button"
|
|
|
|
|
>
|
|
|
|
|
<div className="add-dictionary-icon">+</div>
|
|
|
|
|
<div className="add-dictionary-text">Добавить</div>
|
|
|
|
|
</button>
|
2026-01-20 21:33:23 +03:00
|
|
|
</div>
|
|
|
|
|
)}
|
2026-01-13 18:22:02 +03:00
|
|
|
|
|
|
|
|
{selectedDictionary && (
|
|
|
|
|
<div className="dictionary-modal-overlay" onClick={closeDictionaryModal}>
|
|
|
|
|
<div className="dictionary-modal" onClick={(e) => e.stopPropagation()}>
|
|
|
|
|
<div className="dictionary-modal-header">
|
|
|
|
|
<h3>{selectedDictionary.name}</h3>
|
|
|
|
|
</div>
|
|
|
|
|
<div className="dictionary-modal-actions">
|
|
|
|
|
<button className="dictionary-modal-delete" onClick={handleDictionaryDelete}>
|
|
|
|
|
Удалить
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export default DictionaryList
|
|
|
|
|
|