Some checks failed
Build and Push Docker Image / build-and-push (push) Failing after 16s
Features: - User registration and login with JWT tokens - All data is now user-specific (multi-tenancy) - Profile page with integrations and logout - Automatic migration of existing data to first user Backend changes: - Added users and refresh_tokens tables - Added user_id to all data tables (projects, entries, nodes, dictionaries, words, progress, configs, telegram_integrations, weekly_goals) - JWT authentication middleware - claimOrphanedData() for data migration Frontend changes: - AuthContext for state management - Login/Register forms - Profile page (replaced Integrations) - All API calls use authFetch with Bearer token Migration notes: - On first deploy, backend automatically adds user_id columns - First user to login claims all existing data
166 lines
4.8 KiB
JavaScript
166 lines
4.8 KiB
JavaScript
import React, { useState } from 'react'
|
||
import { useAuth } from './auth/AuthContext'
|
||
import './AddWords.css'
|
||
|
||
const API_URL = '/api'
|
||
|
||
function AddWords({ onNavigate, dictionaryId, dictionaryName }) {
|
||
const { authFetch } = useAuth()
|
||
const [markdownText, setMarkdownText] = useState('')
|
||
const [message, setMessage] = useState('')
|
||
const [loading, setLoading] = useState(false)
|
||
|
||
// Hide add button if dictionary name is not set
|
||
const canAddWords = dictionaryName && dictionaryName.trim() !== ''
|
||
|
||
const parseMarkdownTable = (text) => {
|
||
const lines = text.split('\n')
|
||
const words = []
|
||
let foundTable = false
|
||
let headerFound = false
|
||
|
||
for (let i = 0; i < lines.length; i++) {
|
||
const line = lines[i].trim()
|
||
|
||
// Skip empty lines
|
||
if (!line) continue
|
||
|
||
// Look for table start (markdown table with |)
|
||
if (line.includes('|') && line.includes('Слово')) {
|
||
foundTable = true
|
||
headerFound = true
|
||
continue
|
||
}
|
||
|
||
// Skip separator line (|---|---|)
|
||
if (foundTable && line.match(/^\|[\s\-|:]+\|$/)) {
|
||
continue
|
||
}
|
||
|
||
// Parse table rows
|
||
if (foundTable && headerFound && line.includes('|')) {
|
||
const cells = line
|
||
.split('|')
|
||
.map(cell => (cell || '').trim())
|
||
.filter(cell => cell && cell.length > 0)
|
||
|
||
if (cells.length >= 2) {
|
||
// Remove markdown formatting (**bold**, etc.)
|
||
const word = cells[0].replace(/\*\*/g, '').trim()
|
||
const translation = cells[1].replace(/\*\*/g, '').trim()
|
||
|
||
if (word && translation) {
|
||
words.push({
|
||
name: word,
|
||
translation: translation,
|
||
description: ''
|
||
})
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
return words
|
||
}
|
||
|
||
const handleSubmit = async (e) => {
|
||
e.preventDefault()
|
||
setMessage('')
|
||
setLoading(true)
|
||
|
||
try {
|
||
const words = parseMarkdownTable(markdownText)
|
||
|
||
if (words.length === 0) {
|
||
setMessage('Не удалось найти слова в таблице. Убедитесь, что таблица содержит колонки "Слово" и "Перевод".')
|
||
setLoading(false)
|
||
return
|
||
}
|
||
|
||
// Add dictionary_id to each word if dictionaryId is provided
|
||
const wordsWithDictionary = words.map(word => ({
|
||
...word,
|
||
dictionary_id: dictionaryId !== undefined && dictionaryId !== null ? dictionaryId : undefined
|
||
}))
|
||
|
||
const response = await authFetch(`${API_URL}/words`, {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
},
|
||
body: JSON.stringify({ words: wordsWithDictionary }),
|
||
})
|
||
|
||
if (!response.ok) {
|
||
throw new Error('Ошибка при добавлении слов')
|
||
}
|
||
|
||
const data = await response.json()
|
||
const addedCount = data?.added || 0
|
||
setMessage(`Успешно добавлено ${addedCount} слов(а)!`)
|
||
setMarkdownText('')
|
||
} catch (error) {
|
||
setMessage(`Ошибка: ${error.message}`)
|
||
} finally {
|
||
setLoading(false)
|
||
}
|
||
}
|
||
|
||
const handleClose = () => {
|
||
onNavigate?.('words', dictionaryId !== undefined && dictionaryId !== null ? { dictionaryId } : {})
|
||
}
|
||
|
||
return (
|
||
<div className="add-words">
|
||
<button className="close-x-button" onClick={handleClose}>
|
||
✕
|
||
</button>
|
||
<h2>Добавить слова</h2>
|
||
<p className="description">
|
||
Вставьте текст в формате Markdown с таблицей, содержащей колонки "Слово" и "Перевод"
|
||
</p>
|
||
|
||
<form onSubmit={handleSubmit}>
|
||
<textarea
|
||
className="markdown-input"
|
||
value={markdownText}
|
||
onChange={(e) => setMarkdownText(e.target.value)}
|
||
placeholder={`Вот таблица, содержащая только слова и их перевод:
|
||
|
||
| Слово (Word) | Перевод |
|
||
| --- | --- |
|
||
| **Adventure** | Приключение |
|
||
| **Challenge** | Вызов, сложная задача |
|
||
| **Decision** | Решение |`}
|
||
rows={15}
|
||
/>
|
||
|
||
{canAddWords && (
|
||
<button
|
||
type="submit"
|
||
className="submit-button"
|
||
disabled={loading || !markdownText.trim()}
|
||
>
|
||
{loading ? 'Добавление...' : 'Добавить слова'}
|
||
</button>
|
||
)}
|
||
|
||
{!canAddWords && (
|
||
<div className="message error">
|
||
Сначала установите название словаря на экране списка слов
|
||
</div>
|
||
)}
|
||
</form>
|
||
|
||
{message && (
|
||
<div className={`message ${message.includes('Ошибка') ? 'error' : 'success'}`}>
|
||
{message}
|
||
</div>
|
||
)}
|
||
</div>
|
||
)
|
||
}
|
||
|
||
export default AddWords
|
||
|