Initial commit
This commit is contained in:
278
play-life-web/src/components/TestConfigSelection.jsx
Normal file
278
play-life-web/src/components/TestConfigSelection.jsx
Normal file
@@ -0,0 +1,278 @@
|
||||
import React, { useState, useEffect, useRef } from 'react'
|
||||
import './TestConfigSelection.css'
|
||||
|
||||
const API_URL = '/api'
|
||||
|
||||
function TestConfigSelection({ onNavigate, refreshTrigger = 0 }) {
|
||||
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 fetch(`${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 fetch(`${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 fetch(`${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="loading">Загрузка...</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<div className="config-selection">
|
||||
<div className="error-message">{error}</div>
|
||||
</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', { 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
|
||||
|
||||
Reference in New Issue
Block a user