From d6d40f4f86f2f464331dc154904f7b7012cae936 Mon Sep 17 00:00:00 2001 From: poignatov Date: Thu, 5 Feb 2026 16:19:53 +0300 Subject: [PATCH] =?UTF-8?q?4.22.0:=20=D0=A2=D0=B0=D0=B1=D1=8B=20=D0=B2=20?= =?UTF-8?q?=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB=D0=B5=D0=BD=D0=B8=D0=B8=20?= =?UTF-8?q?=D1=81=D0=BB=D0=BE=D0=B2,=20=D1=84=D0=BE=D1=80=D0=BC=D0=B0=20?= =?UTF-8?q?=D0=BF=D0=BE=20=D0=BE=D0=B4=D0=BD=D0=BE=D0=BC=D1=83?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- VERSION | 2 +- play-life-web/package.json | 2 +- play-life-web/src/App.jsx | 5 + play-life-web/src/components/AddWords.css | 125 ++++++++++++++++++ play-life-web/src/components/AddWords.jsx | 148 +++++++++++++++++++--- 5 files changed, 263 insertions(+), 19 deletions(-) diff --git a/VERSION b/VERSION index 79b1bb6..d7638f3 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -4.21.1 +4.22.0 diff --git a/play-life-web/package.json b/play-life-web/package.json index 30d4d4b..de5e057 100644 --- a/play-life-web/package.json +++ b/play-life-web/package.json @@ -1,6 +1,6 @@ { "name": "play-life-web", - "version": "4.21.1", + "version": "4.22.0", "type": "module", "scripts": { "dev": "vite", diff --git a/play-life-web/src/App.jsx b/play-life-web/src/App.jsx index 84ea96c..d9bdbac 100644 --- a/play-life-web/src/App.jsx +++ b/play-life-web/src/App.jsx @@ -869,6 +869,11 @@ function AppContent() { return // Уже загружали } + // Обновляем список слов при возврате из экрана добавления слов + if (prevActiveTabRef.current === 'add-words' && activeTab === 'words') { + setWordsRefreshTrigger(prev => prev + 1) + } + if (isFirstLoad) { // Первая загрузка таба lastLoadedTabRef.current = tabKey diff --git a/play-life-web/src/components/AddWords.css b/play-life-web/src/components/AddWords.css index cd7c725..86f9c21 100644 --- a/play-life-web/src/components/AddWords.css +++ b/play-life-web/src/components/AddWords.css @@ -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) { diff --git a/play-life-web/src/components/AddWords.jsx b/play-life-web/src/components/AddWords.jsx index 979ab8d..791e8fe 100644 --- a/play-life-web/src/components/AddWords.jsx +++ b/play-life-web/src/components/AddWords.jsx @@ -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 }) { ✕

Добавить слова

-

- Вставьте текст в формате Markdown с таблицей, содержащей колонки "Слово" и "Перевод" -

+
+ + +
+
-