4.22.0: Табы в добавлении слов, форма по одному
All checks were successful
Build and Push Docker Image / build-and-push (push) Successful in 1m10s

This commit is contained in:
poignatov
2026-02-05 16:19:53 +03:00
parent 9c814d62b2
commit d6d40f4f86
5 changed files with 263 additions and 19 deletions

View File

@@ -1 +1 @@
4.21.1
4.22.0

View File

@@ -1,6 +1,6 @@
{
"name": "play-life-web",
"version": "4.21.1",
"version": "4.22.0",
"type": "module",
"scripts": {
"dev": "vite",

View File

@@ -869,6 +869,11 @@ function AppContent() {
return // Уже загружали
}
// Обновляем список слов при возврате из экрана добавления слов
if (prevActiveTabRef.current === 'add-words' && activeTab === 'words') {
setWordsRefreshTrigger(prev => prev + 1)
}
if (isFirstLoad) {
// Первая загрузка таба
lastLoadedTabRef.current = tabKey

View File

@@ -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) {

View File

@@ -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,13 +118,31 @@ function AddWords({ onNavigate, dictionaryId, dictionaryName }) {
setLoading(true)
try {
const words = parseMarkdownTable(markdownText)
let words = []
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
const wordsWithDictionary = words.map(word => ({
@@ -130,7 +165,13 @@ function AddWords({ onNavigate, dictionaryId, dictionaryName }) {
const data = await response.json()
const addedCount = data?.added || 0
setMessage(`Успешно добавлено ${addedCount} слов(а)!`)
// 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,11 +214,29 @@ function AddWords({ onNavigate, dictionaryId, dictionaryName }) {
</button>
<h2>Добавить слова</h2>
<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}>
<div className="tab-content">
{activeTab === 'table' && (
<>
<p className="description">
Вставьте текст в формате Markdown с таблицей, содержащей колонки "Слово" и "Перевод"
</p>
<form onSubmit={handleSubmit}>
<textarea
className="markdown-input"
value={markdownText}
@@ -180,12 +250,56 @@ function AddWords({ onNavigate, dictionaryId, dictionaryName }) {
| **Decision** | Решение |`}
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>