4.22.0: Табы в добавлении слов, форма по одному
All checks were successful
Build and Push Docker Image / build-and-push (push) Successful in 1m10s
All checks were successful
Build and Push Docker Image / build-and-push (push) Successful in 1m10s
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "play-life-web",
|
||||
"version": "4.21.1",
|
||||
"version": "4.22.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
||||
@@ -869,6 +869,11 @@ function AppContent() {
|
||||
return // Уже загружали
|
||||
}
|
||||
|
||||
// Обновляем список слов при возврате из экрана добавления слов
|
||||
if (prevActiveTabRef.current === 'add-words' && activeTab === 'words') {
|
||||
setWordsRefreshTrigger(prev => prev + 1)
|
||||
}
|
||||
|
||||
if (isFirstLoad) {
|
||||
// Первая загрузка таба
|
||||
lastLoadedTabRef.current = tabKey
|
||||
|
||||
@@ -40,7 +40,131 @@
|
||||
border-color: #3498db;
|
||||
}
|
||||
|
||||
.tabs-container {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 1.5rem;
|
||||
border-bottom: 2px solid #e0e0e0;
|
||||
}
|
||||
|
||||
.tab-button {
|
||||
background: none;
|
||||
border: none;
|
||||
padding: 0.75rem 1.5rem;
|
||||
font-size: 1rem;
|
||||
color: #666;
|
||||
cursor: pointer;
|
||||
border-bottom: 3px solid transparent;
|
||||
margin-bottom: -2px;
|
||||
transition: color 0.2s, border-color 0.2s;
|
||||
}
|
||||
|
||||
.tab-button:hover {
|
||||
color: #3498db;
|
||||
}
|
||||
|
||||
.tab-button.active {
|
||||
color: #3498db;
|
||||
border-bottom-color: #3498db;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.tab-content {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.word-pairs-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.word-pair-item {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 0.75rem;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.word-input,
|
||||
.translation-input {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
padding: 0.75rem;
|
||||
border: 2px solid #ddd;
|
||||
border-radius: 4px;
|
||||
font-size: 1rem;
|
||||
transition: border-color 0.2s;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.word-input:focus,
|
||||
.translation-input:focus {
|
||||
outline: none;
|
||||
border-color: #3498db;
|
||||
}
|
||||
|
||||
.remove-pair-button {
|
||||
background: transparent;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
color: #9ca3af;
|
||||
cursor: pointer;
|
||||
width: 2rem;
|
||||
min-width: 2rem;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: all 0.2s ease;
|
||||
box-sizing: border-box;
|
||||
flex-shrink: 0;
|
||||
align-self: stretch;
|
||||
}
|
||||
|
||||
.remove-pair-button svg {
|
||||
display: block;
|
||||
margin: 0;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.remove-pair-button:hover {
|
||||
background-color: rgba(239, 68, 68, 0.1);
|
||||
color: #ef4444;
|
||||
}
|
||||
|
||||
.remove-pair-button:active {
|
||||
background-color: rgba(239, 68, 68, 0.2);
|
||||
transform: scale(0.95);
|
||||
}
|
||||
|
||||
.add-pair-button {
|
||||
width: 100%;
|
||||
padding: 0.75rem;
|
||||
background-color: #f8f9fa;
|
||||
border: 2px dashed #ddd;
|
||||
border-radius: 4px;
|
||||
font-size: 1.5rem;
|
||||
line-height: 1;
|
||||
color: #666;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s, border-color 0.2s, color 0.2s;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-sizing: border-box;
|
||||
min-height: calc(0.75rem * 2 + 1rem + 4px);
|
||||
}
|
||||
|
||||
.add-pair-button:hover {
|
||||
background-color: #e9ecef;
|
||||
border-color: #3498db;
|
||||
color: #3498db;
|
||||
}
|
||||
|
||||
.submit-button {
|
||||
width: 100%;
|
||||
background-color: #3498db;
|
||||
color: white;
|
||||
border: none;
|
||||
@@ -49,6 +173,7 @@
|
||||
font-size: 1rem;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.submit-button:hover:not(:disabled) {
|
||||
|
||||
@@ -6,7 +6,9 @@ const API_URL = '/api'
|
||||
|
||||
function AddWords({ onNavigate, dictionaryId, dictionaryName }) {
|
||||
const { authFetch } = useAuth()
|
||||
const [activeTab, setActiveTab] = useState('words')
|
||||
const [markdownText, setMarkdownText] = useState('')
|
||||
const [wordPairs, setWordPairs] = useState([{ word: '', translation: '' }])
|
||||
const [message, setMessage] = useState('')
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [currentDictionaryName, setCurrentDictionaryName] = useState(dictionaryName || '')
|
||||
@@ -45,6 +47,21 @@ function AddWords({ onNavigate, dictionaryId, dictionaryName }) {
|
||||
// Hide add button if dictionary name is not set
|
||||
const canAddWords = currentDictionaryName && currentDictionaryName.trim() !== ''
|
||||
|
||||
const handleAddPair = () => {
|
||||
setWordPairs([...wordPairs, { word: '', translation: '' }])
|
||||
}
|
||||
|
||||
const handleRemovePair = (index) => {
|
||||
const newPairs = wordPairs.filter((_, i) => i !== index)
|
||||
setWordPairs(newPairs)
|
||||
}
|
||||
|
||||
const handlePairChange = (index, field, value) => {
|
||||
const newPairs = [...wordPairs]
|
||||
newPairs[index][field] = value
|
||||
setWordPairs(newPairs)
|
||||
}
|
||||
|
||||
const parseMarkdownTable = (text) => {
|
||||
const lines = text.split('\n')
|
||||
const words = []
|
||||
@@ -101,12 +118,30 @@ function AddWords({ onNavigate, dictionaryId, dictionaryName }) {
|
||||
setLoading(true)
|
||||
|
||||
try {
|
||||
const words = parseMarkdownTable(markdownText)
|
||||
let words = []
|
||||
|
||||
if (words.length === 0) {
|
||||
setMessage('Не удалось найти слова в таблице. Убедитесь, что таблица содержит колонки "Слово" и "Перевод".')
|
||||
setLoading(false)
|
||||
return
|
||||
if (activeTab === 'table') {
|
||||
words = parseMarkdownTable(markdownText)
|
||||
if (words.length === 0) {
|
||||
setMessage('Не удалось найти слова в таблице. Убедитесь, что таблица содержит колонки "Слово" и "Перевод".')
|
||||
setLoading(false)
|
||||
return
|
||||
}
|
||||
} else if (activeTab === 'words') {
|
||||
// Filter out empty pairs and convert to words format
|
||||
words = wordPairs
|
||||
.filter(pair => pair.word.trim() && pair.translation.trim())
|
||||
.map(pair => ({
|
||||
name: pair.word.trim(),
|
||||
translation: pair.translation.trim(),
|
||||
description: ''
|
||||
}))
|
||||
|
||||
if (words.length === 0) {
|
||||
setMessage('Добавьте хотя бы одно слово с переводом.')
|
||||
setLoading(false)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Add dictionary_id to each word if dictionaryId is provided
|
||||
@@ -130,7 +165,13 @@ function AddWords({ onNavigate, dictionaryId, dictionaryName }) {
|
||||
const data = await response.json()
|
||||
const addedCount = data?.added || 0
|
||||
setMessage(`Успешно добавлено ${addedCount} слов(а)!`)
|
||||
setMarkdownText('')
|
||||
|
||||
// Reset form based on active tab
|
||||
if (activeTab === 'table') {
|
||||
setMarkdownText('')
|
||||
} else if (activeTab === 'words') {
|
||||
setWordPairs([{ word: '', translation: '' }])
|
||||
}
|
||||
} catch (error) {
|
||||
setMessage(`Ошибка: ${error.message}`)
|
||||
} finally {
|
||||
@@ -138,6 +179,17 @@ function AddWords({ onNavigate, dictionaryId, dictionaryName }) {
|
||||
}
|
||||
}
|
||||
|
||||
// Check if form can be submitted
|
||||
const canSubmit = () => {
|
||||
if (!canAddWords) return false
|
||||
if (activeTab === 'table') {
|
||||
return markdownText.trim().length > 0
|
||||
} else if (activeTab === 'words') {
|
||||
return wordPairs.some(pair => pair.word.trim() && pair.translation.trim())
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
const handleClose = () => {
|
||||
window.history.back()
|
||||
}
|
||||
@@ -162,30 +214,92 @@ function AddWords({ onNavigate, dictionaryId, dictionaryName }) {
|
||||
✕
|
||||
</button>
|
||||
<h2>Добавить слова</h2>
|
||||
<p className="description">
|
||||
Вставьте текст в формате Markdown с таблицей, содержащей колонки "Слово" и "Перевод"
|
||||
</p>
|
||||
|
||||
<div className="tabs-container">
|
||||
<button
|
||||
className={`tab-button ${activeTab === 'words' ? 'active' : ''}`}
|
||||
onClick={() => setActiveTab('words')}
|
||||
>
|
||||
Слова
|
||||
</button>
|
||||
<button
|
||||
className={`tab-button ${activeTab === 'table' ? 'active' : ''}`}
|
||||
onClick={() => setActiveTab('table')}
|
||||
>
|
||||
Таблица
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleSubmit}>
|
||||
<textarea
|
||||
className="markdown-input"
|
||||
value={markdownText}
|
||||
onChange={(e) => setMarkdownText(e.target.value)}
|
||||
placeholder={`Вот таблица, содержащая только слова и их перевод:
|
||||
<div className="tab-content">
|
||||
{activeTab === 'table' && (
|
||||
<>
|
||||
<p className="description">
|
||||
Вставьте текст в формате Markdown с таблицей, содержащей колонки "Слово" и "Перевод"
|
||||
</p>
|
||||
<textarea
|
||||
className="markdown-input"
|
||||
value={markdownText}
|
||||
onChange={(e) => setMarkdownText(e.target.value)}
|
||||
placeholder={`Вот таблица, содержащая только слова и их перевод:
|
||||
|
||||
| Слово (Word) | Перевод |
|
||||
| --- | --- |
|
||||
| **Adventure** | Приключение |
|
||||
| **Challenge** | Вызов, сложная задача |
|
||||
| **Decision** | Решение |`}
|
||||
rows={15}
|
||||
/>
|
||||
rows={15}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
{activeTab === 'words' && (
|
||||
<div className="word-pairs-list">
|
||||
{wordPairs.map((pair, index) => (
|
||||
<div key={index} className="word-pair-item">
|
||||
<input
|
||||
type="text"
|
||||
className="word-input"
|
||||
placeholder="Слово"
|
||||
value={pair.word}
|
||||
onChange={(e) => handlePairChange(index, 'word', e.target.value)}
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
className="translation-input"
|
||||
placeholder="Перевод"
|
||||
value={pair.translation}
|
||||
onChange={(e) => handlePairChange(index, 'translation', e.target.value)}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
className="remove-pair-button"
|
||||
onClick={() => handleRemovePair(index)}
|
||||
title="Удалить"
|
||||
>
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<line x1="18" y1="6" x2="6" y2="18"></line>
|
||||
<line x1="6" y1="6" x2="18" y2="18"></line>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
<button
|
||||
type="button"
|
||||
className="add-pair-button"
|
||||
onClick={handleAddPair}
|
||||
>
|
||||
+
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{canAddWords && (
|
||||
<button
|
||||
type="submit"
|
||||
className="submit-button"
|
||||
disabled={loading || !markdownText.trim()}
|
||||
disabled={loading || !canSubmit()}
|
||||
>
|
||||
{loading ? 'Добавление...' : 'Добавить слова'}
|
||||
</button>
|
||||
|
||||
Reference in New Issue
Block a user