All checks were successful
Build and Push Docker Image / build-and-push (push) Successful in 34s
287 lines
9.1 KiB
JavaScript
287 lines
9.1 KiB
JavaScript
import React, { useState, useEffect, useRef } from 'react'
|
|
import { useAuth } from './auth/AuthContext'
|
|
import LoadingError from './LoadingError'
|
|
import './TestConfigSelection.css'
|
|
|
|
const API_URL = '/api'
|
|
|
|
function TestConfigSelection({ onNavigate, refreshTrigger = 0 }) {
|
|
const { authFetch } = useAuth()
|
|
const [configs, setConfigs] = useState([])
|
|
const [dictionaries, setDictionaries] = useState([])
|
|
const [loading, setLoading] = useState(true)
|
|
const [error, setError] = useState('')
|
|
const [selectedConfig, setSelectedConfig] = useState(null)
|
|
const [selectedDictionary, setSelectedDictionary] = useState(null)
|
|
const [longPressTimer, setLongPressTimer] = useState(null)
|
|
const isInitializedRef = useRef(false)
|
|
const configsRef = useRef([])
|
|
const dictionariesRef = useRef([])
|
|
|
|
// Обновляем ref при изменении состояния
|
|
useEffect(() => {
|
|
configsRef.current = configs
|
|
}, [configs])
|
|
|
|
useEffect(() => {
|
|
dictionariesRef.current = dictionaries
|
|
}, [dictionaries])
|
|
|
|
useEffect(() => {
|
|
fetchTestConfigsAndDictionaries()
|
|
}, [refreshTrigger])
|
|
|
|
|
|
const fetchTestConfigsAndDictionaries = async () => {
|
|
try {
|
|
// Показываем загрузку только при первой инициализации или если нет данных для отображения
|
|
const isFirstLoad = !isInitializedRef.current
|
|
const hasData = !isFirstLoad && (configsRef.current.length > 0 || 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()
|
|
setConfigs(Array.isArray(data.configs) ? data.configs : [])
|
|
setDictionaries(Array.isArray(data.dictionaries) ? data.dictionaries : [])
|
|
setError('')
|
|
isInitializedRef.current = true
|
|
} catch (err) {
|
|
setError(err.message)
|
|
setConfigs([])
|
|
setDictionaries([])
|
|
isInitializedRef.current = true
|
|
} finally {
|
|
setLoading(false)
|
|
}
|
|
}
|
|
|
|
const handleConfigSelect = (config) => {
|
|
onNavigate?.('test', {
|
|
wordCount: config.words_count,
|
|
configId: config.id,
|
|
maxCards: config.max_cards || null
|
|
})
|
|
}
|
|
|
|
const handleDictionarySelect = (dict) => {
|
|
// For now, navigate to words list
|
|
// In the future, we might want to filter by dictionary_id
|
|
onNavigate?.('words', { dictionaryId: dict.id })
|
|
}
|
|
|
|
const handleConfigMenuClick = (config, e) => {
|
|
e.stopPropagation()
|
|
setSelectedConfig(config)
|
|
}
|
|
|
|
const handleDictionaryMenuClick = (dict, e) => {
|
|
e.stopPropagation()
|
|
setSelectedDictionary(dict)
|
|
}
|
|
|
|
const handleEdit = () => {
|
|
if (selectedConfig) {
|
|
onNavigate?.('add-config', { config: selectedConfig })
|
|
setSelectedConfig(null)
|
|
}
|
|
}
|
|
|
|
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 fetchTestConfigsAndDictionaries()
|
|
} catch (err) {
|
|
console.error('Delete failed:', err)
|
|
setError(err.message)
|
|
setSelectedDictionary(null)
|
|
}
|
|
}
|
|
|
|
const handleDelete = async () => {
|
|
if (!selectedConfig) return
|
|
|
|
try {
|
|
const response = await authFetch(`${API_URL}/configs/${selectedConfig.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}`)
|
|
}
|
|
|
|
setSelectedConfig(null)
|
|
// Refresh configs and dictionaries list
|
|
await fetchTestConfigsAndDictionaries()
|
|
} catch (err) {
|
|
console.error('Delete failed:', err)
|
|
setError(err.message)
|
|
setSelectedConfig(null)
|
|
}
|
|
}
|
|
|
|
const closeModal = () => {
|
|
setSelectedConfig(null)
|
|
}
|
|
|
|
// Показываем загрузку только при первой инициализации и если нет данных для отображения
|
|
const shouldShowLoading = loading && !isInitializedRef.current && configs.length === 0 && dictionaries.length === 0
|
|
|
|
if (shouldShowLoading) {
|
|
return (
|
|
<div className="config-selection">
|
|
<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>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
if (error) {
|
|
return (
|
|
<div className="config-selection">
|
|
<LoadingError onRetry={fetchTestConfigsAndDictionaries} />
|
|
</div>
|
|
)
|
|
}
|
|
|
|
return (
|
|
<div className="config-selection">
|
|
{/* Секция тестов */}
|
|
<div className="section-divider">
|
|
<h2 className="section-title">Тесты</h2>
|
|
</div>
|
|
<div className="configs-grid">
|
|
{configs.map((config) => (
|
|
<div
|
|
key={config.id}
|
|
className="config-card"
|
|
onClick={() => handleConfigSelect(config)}
|
|
>
|
|
<button
|
|
onClick={(e) => handleConfigMenuClick(config, e)}
|
|
className="card-menu-button"
|
|
title="Меню"
|
|
>
|
|
⋮
|
|
</button>
|
|
<div className="config-words-count">
|
|
{config.words_count}
|
|
</div>
|
|
{config.max_cards && (
|
|
<div className="config-max-cards">
|
|
{config.max_cards}
|
|
</div>
|
|
)}
|
|
<div className="config-name">{config.name}</div>
|
|
</div>
|
|
))}
|
|
<button onClick={() => onNavigate?.('add-config')} className="add-config-button">
|
|
<div className="add-config-icon">+</div>
|
|
<div className="add-config-text">Добавить</div>
|
|
</button>
|
|
</div>
|
|
|
|
{/* Секция словарей */}
|
|
<div className="dictionaries-section">
|
|
<div className="section-divider">
|
|
<h2 className="section-title">Словари</h2>
|
|
</div>
|
|
<div className="configs-grid">
|
|
{dictionaries.map((dict) => (
|
|
<div
|
|
key={dict.id}
|
|
className="dictionary-card"
|
|
onClick={() => handleDictionarySelect(dict)}
|
|
>
|
|
<button
|
|
onClick={(e) => handleDictionaryMenuClick(dict, e)}
|
|
className="card-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-config-icon">+</div>
|
|
<div className="add-config-text">Добавить</div>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
|
|
{selectedConfig && (
|
|
<div className="config-modal-overlay" onClick={closeModal}>
|
|
<div className="config-modal" onClick={(e) => e.stopPropagation()}>
|
|
<div className="config-modal-header">
|
|
<h3>{selectedConfig.name}</h3>
|
|
</div>
|
|
<div className="config-modal-actions">
|
|
<button className="config-modal-edit" onClick={handleEdit}>
|
|
Редактировать
|
|
</button>
|
|
<button className="config-modal-delete" onClick={handleDelete}>
|
|
Удалить
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{selectedDictionary && (
|
|
<div className="config-modal-overlay" onClick={() => setSelectedDictionary(null)}>
|
|
<div className="config-modal" onClick={(e) => e.stopPropagation()}>
|
|
<div className="config-modal-header">
|
|
<h3>{selectedDictionary.name}</h3>
|
|
</div>
|
|
<div className="config-modal-actions">
|
|
<button className="config-modal-delete" onClick={handleDictionaryDelete}>
|
|
Удалить
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
)
|
|
}
|
|
|
|
export default TestConfigSelection
|
|
|