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",
|
"name": "play-life-web",
|
||||||
"version": "4.21.1",
|
"version": "4.22.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
|
|||||||
@@ -869,6 +869,11 @@ function AppContent() {
|
|||||||
return // Уже загружали
|
return // Уже загружали
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Обновляем список слов при возврате из экрана добавления слов
|
||||||
|
if (prevActiveTabRef.current === 'add-words' && activeTab === 'words') {
|
||||||
|
setWordsRefreshTrigger(prev => prev + 1)
|
||||||
|
}
|
||||||
|
|
||||||
if (isFirstLoad) {
|
if (isFirstLoad) {
|
||||||
// Первая загрузка таба
|
// Первая загрузка таба
|
||||||
lastLoadedTabRef.current = tabKey
|
lastLoadedTabRef.current = tabKey
|
||||||
|
|||||||
@@ -40,7 +40,131 @@
|
|||||||
border-color: #3498db;
|
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 {
|
.submit-button {
|
||||||
|
width: 100%;
|
||||||
background-color: #3498db;
|
background-color: #3498db;
|
||||||
color: white;
|
color: white;
|
||||||
border: none;
|
border: none;
|
||||||
@@ -49,6 +173,7 @@
|
|||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: background-color 0.2s;
|
transition: background-color 0.2s;
|
||||||
|
margin-bottom: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.submit-button:hover:not(:disabled) {
|
.submit-button:hover:not(:disabled) {
|
||||||
|
|||||||
@@ -6,7 +6,9 @@ const API_URL = '/api'
|
|||||||
|
|
||||||
function AddWords({ onNavigate, dictionaryId, dictionaryName }) {
|
function AddWords({ onNavigate, dictionaryId, dictionaryName }) {
|
||||||
const { authFetch } = useAuth()
|
const { authFetch } = useAuth()
|
||||||
|
const [activeTab, setActiveTab] = useState('words')
|
||||||
const [markdownText, setMarkdownText] = useState('')
|
const [markdownText, setMarkdownText] = useState('')
|
||||||
|
const [wordPairs, setWordPairs] = useState([{ word: '', translation: '' }])
|
||||||
const [message, setMessage] = useState('')
|
const [message, setMessage] = useState('')
|
||||||
const [loading, setLoading] = useState(false)
|
const [loading, setLoading] = useState(false)
|
||||||
const [currentDictionaryName, setCurrentDictionaryName] = useState(dictionaryName || '')
|
const [currentDictionaryName, setCurrentDictionaryName] = useState(dictionaryName || '')
|
||||||
@@ -45,6 +47,21 @@ function AddWords({ onNavigate, dictionaryId, dictionaryName }) {
|
|||||||
// Hide add button if dictionary name is not set
|
// Hide add button if dictionary name is not set
|
||||||
const canAddWords = currentDictionaryName && currentDictionaryName.trim() !== ''
|
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 parseMarkdownTable = (text) => {
|
||||||
const lines = text.split('\n')
|
const lines = text.split('\n')
|
||||||
const words = []
|
const words = []
|
||||||
@@ -101,12 +118,30 @@ function AddWords({ onNavigate, dictionaryId, dictionaryName }) {
|
|||||||
setLoading(true)
|
setLoading(true)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const words = parseMarkdownTable(markdownText)
|
let words = []
|
||||||
|
|
||||||
if (words.length === 0) {
|
if (activeTab === 'table') {
|
||||||
setMessage('Не удалось найти слова в таблице. Убедитесь, что таблица содержит колонки "Слово" и "Перевод".')
|
words = parseMarkdownTable(markdownText)
|
||||||
setLoading(false)
|
if (words.length === 0) {
|
||||||
return
|
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
|
// 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 data = await response.json()
|
||||||
const addedCount = data?.added || 0
|
const addedCount = data?.added || 0
|
||||||
setMessage(`Успешно добавлено ${addedCount} слов(а)!`)
|
setMessage(`Успешно добавлено ${addedCount} слов(а)!`)
|
||||||
setMarkdownText('')
|
|
||||||
|
// Reset form based on active tab
|
||||||
|
if (activeTab === 'table') {
|
||||||
|
setMarkdownText('')
|
||||||
|
} else if (activeTab === 'words') {
|
||||||
|
setWordPairs([{ word: '', translation: '' }])
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
setMessage(`Ошибка: ${error.message}`)
|
setMessage(`Ошибка: ${error.message}`)
|
||||||
} finally {
|
} 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 = () => {
|
const handleClose = () => {
|
||||||
window.history.back()
|
window.history.back()
|
||||||
}
|
}
|
||||||
@@ -162,30 +214,92 @@ function AddWords({ onNavigate, dictionaryId, dictionaryName }) {
|
|||||||
✕
|
✕
|
||||||
</button>
|
</button>
|
||||||
<h2>Добавить слова</h2>
|
<h2>Добавить слова</h2>
|
||||||
<p className="description">
|
|
||||||
Вставьте текст в формате Markdown с таблицей, содержащей колонки "Слово" и "Перевод"
|
<div className="tabs-container">
|
||||||
</p>
|
<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}>
|
<form onSubmit={handleSubmit}>
|
||||||
<textarea
|
<div className="tab-content">
|
||||||
className="markdown-input"
|
{activeTab === 'table' && (
|
||||||
value={markdownText}
|
<>
|
||||||
onChange={(e) => setMarkdownText(e.target.value)}
|
<p className="description">
|
||||||
placeholder={`Вот таблица, содержащая только слова и их перевод:
|
Вставьте текст в формате Markdown с таблицей, содержащей колонки "Слово" и "Перевод"
|
||||||
|
</p>
|
||||||
|
<textarea
|
||||||
|
className="markdown-input"
|
||||||
|
value={markdownText}
|
||||||
|
onChange={(e) => setMarkdownText(e.target.value)}
|
||||||
|
placeholder={`Вот таблица, содержащая только слова и их перевод:
|
||||||
|
|
||||||
| Слово (Word) | Перевод |
|
| Слово (Word) | Перевод |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
| **Adventure** | Приключение |
|
| **Adventure** | Приключение |
|
||||||
| **Challenge** | Вызов, сложная задача |
|
| **Challenge** | Вызов, сложная задача |
|
||||||
| **Decision** | Решение |`}
|
| **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 && (
|
{canAddWords && (
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
className="submit-button"
|
className="submit-button"
|
||||||
disabled={loading || !markdownText.trim()}
|
disabled={loading || !canSubmit()}
|
||||||
>
|
>
|
||||||
{loading ? 'Добавление...' : 'Добавить слова'}
|
{loading ? 'Добавление...' : 'Добавить слова'}
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
Reference in New Issue
Block a user