/
ravil_yakhin
/
WordMaster
Обзор
Документация
Войти
/
ravil_yakhin
/
WordMaster
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
CI/CD
Аналитика
Безопасность
master
script.js
794 строки
34 KB
ravil_yakhin
upload files
09 янв 2026, 15:39
09 янв 2026, 15:39
72be431
Код
Авторство
О чём код?
document.addEventListener('DOMContentLoaded', () => { const app = new WordMasterApp(); app.init(); }); class WordMasterApp { constructor() { this.currentUser = null; this.selectedCategory = null; this.selectedWord = null; this.currentTrainingSession = null; } init() { this.attachAuthListeners(); this.attachNavigationListeners(); this.attachModalListeners(); this.attachActionListeners(); this.checkIfLoggedIn(); } checkIfLoggedIn() { const db = api.getDB(); if (db.currentUserId) { const user = db.users.find(u => u.id === db.currentUserId); if (user) { this.loginUser(user); return; } } this.showAuthModal(); } showAuthModal() { document.getElementById('authModal').classList.add('active'); document.getElementById('mainApp').classList.add('hidden'); } showMainApp() { document.getElementById('authModal').classList.remove('active'); document.getElementById('mainApp').classList.remove('hidden'); } loginUser(user) { this.currentUser = user; document.getElementById('currentUser').textContent = `${user.username}`; this.showMainApp(); this.loadDashboard(); } attachAuthListeners() { document.querySelectorAll('.tab-btn').forEach(btn => { btn.addEventListener('click', (e) => { document.querySelectorAll('.tab-btn').forEach(b => b.classList.remove('active')); document.querySelectorAll('.auth-form').forEach(f => f.classList.remove('active')); e.target.classList.add('active'); const tab = e.target.dataset.tab; if (tab === 'login') { document.getElementById('loginForm').classList.add('active'); } else { document.getElementById('registerForm').classList.add('active'); } }); }); document.getElementById('loginForm').addEventListener('submit', (e) => { e.preventDefault(); const email = document.getElementById('loginEmail').value; const password = document.getElementById('loginPassword').value; const response = api.login(email, password); if (response.status === 200) { this.loginUser(response.data); document.getElementById('loginForm').reset(); } else { this.showError('loginError', response.error.join(', ')); } }); document.getElementById('registerForm').addEventListener('submit', (e) => { e.preventDefault(); const username = document.getElementById('registerUsername').value; const email = document.getElementById('registerEmail').value; const password = document.getElementById('registerPassword').value; const passwordConfirm = document.getElementById('registerPasswordConfirm').value; const response = api.register(username, email, password, passwordConfirm); if (response.status === 201) { this.loginUser(response.data); document.getElementById('registerForm').reset(); } else { this.showError('registerError', response.error.join(', ')); } }); document.getElementById('logoutBtn').addEventListener('click', () => { api.logout(); this.currentUser = null; this.showAuthModal(); document.getElementById('loginForm').reset(); document.getElementById('registerForm').reset(); }); } attachNavigationListeners() { document.querySelectorAll('.nav-item').forEach(item => { item.addEventListener('click', (e) => { document.querySelectorAll('.nav-item').forEach(i => i.classList.remove('active')); e.currentTarget.classList.add('active'); const section = e.currentTarget.dataset.section; this.showSection(section); }); }); } showSection(sectionName) { document.querySelectorAll('.section').forEach(s => s.classList.remove('active')); document.getElementById(sectionName + 'Section').classList.add('active'); if (sectionName === 'categories') { this.loadCategories(); } else if (sectionName === 'words') { this.loadWords(); } else if (sectionName === 'examples') { this.loadExamples(); } else if (sectionName === 'notes') { this.loadNotes(); } else if (sectionName === 'training') { this.loadTrainings(); } } attachModalListeners() { document.getElementById('closeModalBtn').addEventListener('click', () => { document.getElementById('formModal').classList.remove('active'); }); document.getElementById('formModal').addEventListener('click', (e) => { if (e.target.id === 'formModal') { document.getElementById('formModal').classList.remove('active'); } }); } openModal(title, formFields, onSubmit) { document.getElementById('formTitle').textContent = title; const form = document.getElementById('dynamicForm'); form.innerHTML = ''; formFields.forEach(field => { const group = document.createElement('div'); group.className = 'form-group'; group.innerHTML = ` <label for="field_${field.name}">${field.label}</label> ${field.type === 'textarea' ? `<textarea id="field_${field.name}" name="${field.name}" required></textarea>` : field.type === 'select' ? `<select id="field_${field.name}" name="${field.name}" required> <option value="">Выберите...</option> ${field.options.map(opt => `<option value="${opt.value}">${opt.label}</option>`).join('')} </select>` : `<input type="${field.type}" id="field_${field.name}" name="${field.name}" required>` } `; form.appendChild(group); }); const actionsDiv = document.createElement('div'); actionsDiv.className = 'form-actions'; actionsDiv.innerHTML = ` <button type="submit" class="btn btn-primary">Сохранить</button> <button type="button" class="btn btn-secondary" id="cancelBtn">Отмена</button> `; form.appendChild(actionsDiv); form.onsubmit = (e) => { e.preventDefault(); const data = {}; formFields.forEach(field => { data[field.name] = document.getElementById(`field_${field.name}`).value; }); onSubmit(data); document.getElementById('formModal').classList.remove('active'); }; document.getElementById('cancelBtn').addEventListener('click', () => { document.getElementById('formModal').classList.remove('active'); }); document.getElementById('formModal').classList.add('active'); } attachActionListeners() { document.getElementById('addCategoryBtn').addEventListener('click', () => { this.openModal('Добавить категорию', [ { name: 'name', label: 'Название категории', type: 'text' }, { name: 'description', label: 'Описание', type: 'textarea' } ], (data) => { const response = api.createCategory(data.name, data.description); if (response.status === 201) { this.loadCategories(); } else { alert('Ошибка: ' + response.error.join(', ')); } }); }); document.getElementById('addWordBtn').addEventListener('click', () => { const categoriesResp = api.getCategories(); const categoryOptions = categoriesResp.data.map(c => ({ value: c.id, label: c.category_name })); if (categoryOptions.length === 0) { alert('Сначала создайте категорию!'); return; } this.openModal('Добавить слово', [ { name: 'english', label: 'Английское слово', type: 'text' }, { name: 'russian', label: 'Русский перевод', type: 'text' }, { name: 'category', label: 'Категория', type: 'select', options: categoryOptions }, { name: 'difficulty', label: 'Сложность', type: 'select', options: api.getDifficultyLevels().data.map(d => ({ value: d.id, label: d.name })) }, { name: 'status', label: 'Статус', type: 'select', options: api.getWordStatuses().data.map(s => ({ value: s.id, label: s.name })) } ], (data) => { const response = api.createWord(data.english, data.russian, parseInt(data.category), parseInt(data.difficulty), parseInt(data.status)); if (response.status === 201) { this.loadWords(); } else { alert('Ошибка: ' + response.error.join(', ')); } }); }); document.getElementById('addExampleBtn').addEventListener('click', () => { const wordsResp = api.getWords(); const wordOptions = wordsResp.data.map(w => ({ value: w.id, label: w.english_word })); if (wordOptions.length === 0) { alert('Сначала добавьте слова!'); return; } this.openModal('Добавить пример', [ { name: 'word', label: 'Слово', type: 'select', options: wordOptions }, { name: 'english', label: 'Пример на английском', type: 'textarea' }, { name: 'russian', label: 'Перевод примера', type: 'textarea' } ], (data) => { const response = api.createExampleSentence(parseInt(data.word), data.english, data.russian); if (response.status === 201) { this.loadExamples(); } else { alert('Ошибка: ' + response.error.join(', ')); } }); }); document.getElementById('addNoteBtn').addEventListener('click', () => { const categoriesResp = api.getCategories(); const categoryOptions = categoriesResp.data.map(c => ({ value: c.id, label: c.category_name })); if (categoryOptions.length === 0) { alert('Сначала создайте категорию!'); return; } this.openModal('Добавить заметку', [ { name: 'category', label: 'Категория', type: 'select', options: categoryOptions }, { name: 'text', label: 'Текст заметки', type: 'textarea' } ], (data) => { const response = api.createNote(data.text, parseInt(data.category)); if (response.status === 201) { this.loadNotes(); } else { alert('Ошибка: ' + response.error.join(', ')); } }); }); document.getElementById('startTrainingBtn').addEventListener('click', () => { const categoriesResp = api.getCategories(); const categoryOptions = categoriesResp.data.map(c => ({ value: c.id, label: c.category_name })); if (categoryOptions.length === 0) { alert('Сначала создайте категорию и добавьте слова!'); return; } this.openModal('Начать тренировку', [ { name: 'category', label: 'Выберите категорию', type: 'select', options: categoryOptions } ], (data) => { const response = api.createTrainingSession(parseInt(data.category)); if (response.status === 201) { this.startTraining(response.data); } else { alert('Ошибка: ' + response.error.join(', ')); } }); }); document.getElementById('sendApiBtn').addEventListener('click', () => { this.testAPI(); }); } loadDashboard() { this.loadCategories(); this.updateFilters(); } loadCategories() { const response = api.getCategories(); const list = document.getElementById('categoriesList'); list.innerHTML = ''; if (response.status !== 200) { list.innerHTML = '<p>Ошибка загрузки категорий</p>'; return; } if (response.data.length === 0) { list.innerHTML = '<p>Нет категорий. Создайте новую!</p>'; return; } response.data.forEach(category => { const wordsCount = api.getWords(category.id).data.length; const card = document.createElement('div'); card.className = 'item-card'; card.innerHTML = ` <div class="item-header"> <div> <div class="item-title">${this.escapeHtml(category.category_name)}</div> <div class="item-subtitle">${this.escapeHtml(category.description)}</div> <div class="item-subtitle">Слов: ${wordsCount}</div> </div> </div> <div class="item-actions"> <button class="btn btn-secondary btn-small" data-action="edit-category" data-id="${category.id}">Редактировать</button> <button class="btn btn-danger btn-small" data-action="delete-category" data-id="${category.id}">Удалить</button> </div> `; list.appendChild(card); }); this.attachDataActions(); } loadWords() { const response = api.getWords(); const list = document.getElementById('wordsList'); list.innerHTML = ''; if (response.status !== 200) { list.innerHTML = '<p>Ошибка загрузки слов</p>'; return; } if (response.data.length === 0) { list.innerHTML = '<p>Нет слов. Добавьте новое!</p>'; return; } const statuses = api.getWordStatuses().data; const difficulties = api.getDifficultyLevels().data; response.data.forEach(word => { const status = statuses.find(s => s.id === word.status_id); const difficulty = difficulties.find(d => d.id === word.difficulty_id); const category = api.getCategories().data.find(c => c.id === word.category_id); const card = document.createElement('div'); card.className = 'item-card'; card.innerHTML = ` <div class="item-header"> <div> <div class="item-title">${this.escapeHtml(word.english_word)} — ${this.escapeHtml(word.russian_word)}</div> <div class="item-subtitle">Категория: ${category ? this.escapeHtml(category.category_name) : 'N/A'}</div> <div style="margin-top: 8px; display: flex; gap: 8px;"> <span class="item-badge ${this.getStatusBadgeClass(status.name)}">${status.name}</span> <span class="item-badge ${this.getDifficultyBadgeClass(difficulty.name)}">${difficulty.name}</span> </div> </div> </div> <div class="item-actions"> <button class="btn btn-secondary btn-small" data-action="edit-word" data-id="${word.id}">Редактировать</button> <button class="btn btn-danger btn-small" data-action="delete-word" data-id="${word.id}">Удалить</button> </div> `; list.appendChild(card); }); this.attachDataActions(); } loadExamples() { const response = api.getWords(); const list = document.getElementById('examplesList'); list.innerHTML = ''; if (response.status !== 200 || response.data.length === 0) { list.innerHTML = '<p>Нет слов с примерами</p>'; return; } response.data.forEach(word => { const examplesResp = api.getExampleSentences(word.id); if (examplesResp.data && examplesResp.data.length > 0) { examplesResp.data.forEach(example => { const card = document.createElement('div'); card.className = 'item-card'; card.innerHTML = ` <div class="item-header"> <div> <div class="item-title">Слово: ${this.escapeHtml(word.english_word)}</div> <div class="item-subtitle">EN: ${this.escapeHtml(example.sentence_en)}</div> <div class="item-subtitle">RU: ${this.escapeHtml(example.sentence_ru)}</div> </div> </div> <div class="item-actions"> <button class="btn btn-danger btn-small" data-action="delete-example" data-id="${example.id}">Удалить</button> </div> `; list.appendChild(card); }); } }); if (list.children.length === 0) { list.innerHTML = '<p>Нет примеров. Добавьте новый!</p>'; } this.attachDataActions(); } loadNotes() { const response = api.getNotes(); const list = document.getElementById('notesList'); list.innerHTML = ''; if (response.status !== 200) { list.innerHTML = '<p>Ошибка загрузки заметок</p>'; return; } if (response.data.length === 0) { list.innerHTML = '<p>Нет заметок. Добавьте новую!</p>'; return; } response.data.forEach(note => { const category = api.getCategories().data.find(c => c.id === note.category_id); const card = document.createElement('div'); card.className = 'item-card'; card.innerHTML = ` <div class="item-header"> <div> <div class="item-subtitle">Категория: ${category ? this.escapeHtml(category.category_name) : 'N/A'}</div> <div class="item-title">${this.escapeHtml(note.note_text)}</div> </div> </div> <div class="item-actions"> <button class="btn btn-danger btn-small" data-action="delete-note" data-id="${note.id}">Удалить</button> </div> `; list.appendChild(card); }); this.attachDataActions(); } loadTrainings() { const response = api.getTrainingSessions(); const list = document.getElementById('trainingList'); list.innerHTML = ''; if (response.status !== 200) { list.innerHTML = '<p>Ошибка загрузки тренировок</p>'; return; } if (response.data.length === 0) { list.innerHTML = '<p>Нет завершённых тренировок</p>'; return; } response.data.forEach(session => { const category = api.getCategories().data.find(c => c.id === session.category_id); const percentage = session.words_total > 0 ? Math.round((session.words_correct / session.words_total) * 100) : 0; const card = document.createElement('div'); card.className = 'item-card'; card.innerHTML = ` <div class="item-header"> <div> <div class="item-title">Категория: ${category ? this.escapeHtml(category.category_name) : 'N/A'}</div> <div class="item-subtitle">Дата: ${new Date(session.started_at).toLocaleString('ru-RU')}</div> <div class="item-subtitle">Результат: ${session.words_correct}/${session.words_total} (${percentage}%)</div> </div> </div> `; list.appendChild(card); }); this.attachDataActions(); } updateFilters() { const categoriesResp = api.getCategories(); const categoryFilter = document.getElementById('categoryFilter'); categoryFilter.innerHTML = '<option value="">Все категории</option>'; categoriesResp.data.forEach(cat => { const option = document.createElement('option'); option.value = cat.id; option.textContent = cat.category_name; categoryFilter.appendChild(option); }); } attachDataActions() { document.querySelectorAll('[data-action]').forEach(btn => { btn.addEventListener('click', (e) => { const action = e.target.dataset.action; const id = parseInt(e.target.dataset.id); if (action === 'delete-category') { if (confirm('Удалить категорию и все её слова?')) { api.deleteCategory(id); this.loadCategories(); } } else if (action === 'edit-category') { const cat = api.getCategories().data.find(c => c.id === id); if (cat) { this.openModal('Редактировать категорию', [ { name: 'name', label: 'Название', type: 'text' }, { name: 'description', label: 'Описание', type: 'textarea' } ], (data) => { api.updateCategory(id, data.name, data.description); this.loadCategories(); }); setTimeout(() => { document.getElementById('field_name').value = cat.category_name; document.getElementById('field_description').value = cat.description; }, 100); } } else if (action === 'delete-word') { if (confirm('Удалить слово?')) { api.deleteWord(id); this.loadWords(); } } else if (action === 'edit-word') { const word = api.getWords().data.find(w => w.id === id); if (word) { const difficulties = api.getDifficultyLevels().data; const statuses = api.getWordStatuses().data; this.openModal('Редактировать слово', [ { name: 'english', label: 'Английское слово', type: 'text' }, { name: 'russian', label: 'Русский перевод', type: 'text' }, { name: 'difficulty', label: 'Сложность', type: 'select', options: difficulties.map(d => ({ value: d.id, label: d.name })) }, { name: 'status', label: 'Статус', type: 'select', options: statuses.map(s => ({ value: s.id, label: s.name })) } ], (data) => { api.updateWord(id, data.english, data.russian, parseInt(data.difficulty), parseInt(data.status)); this.loadWords(); }); setTimeout(() => { document.getElementById('field_english').value = word.english_word; document.getElementById('field_russian').value = word.russian_word; document.getElementById('field_difficulty').value = word.difficulty_id; document.getElementById('field_status').value = word.status_id; }, 100); } } else if (action === 'delete-example') { if (confirm('Удалить пример?')) { api.deleteExampleSentence(id); this.loadExamples(); } } else if (action === 'delete-note') { if (confirm('Удалить заметку?')) { api.deleteNote(id); this.loadNotes(); } } }); }); } startTraining(session) { this.currentTrainingSession = session; const list = document.getElementById('trainingList'); list.innerHTML = this.getTrainingHTML(session); document.querySelector('#trainingSection .section-header h2').textContent = `Тренировка: ${session.words[0] ? 'загружается...' : 'загружена'}`; } getTrainingHTML(session) { const words = session.words || []; const totalWords = words.length; let currentIndex = 0; const container = document.createElement('div'); container.className = 'training-quiz'; const render = () => { if (currentIndex >= totalWords) { api.finishTrainingSession(session.id, session.words_correct || 0); container.innerHTML = ` <div> <h3>Тренировка завершена!</h3> <p>Результат: ${session.words_correct || 0}/${totalWords}</p> <button class="btn btn-primary" id="closeTrainingBtn">Вернуться</button> </div> `; document.getElementById('closeTrainingBtn').addEventListener('click', () => { this.loadTrainings(); }); return; } const word = words[currentIndex]; const wrongOptions = words.filter((_, i) => i !== currentIndex).slice(0, 3).map(w => w.russian_word); const options = [word.russian_word, ...wrongOptions].sort(() => Math.random() - 0.5); const progressPercent = ((currentIndex + 1) / totalWords) * 100; container.innerHTML = ` <div class="quiz-progress"> <div class="quiz-progress-bar" style="width: ${progressPercent}%"></div> </div> <div class="quiz-question">${currentIndex + 1}/${totalWords} - Переведите: "${word.english_word}"</div> <div class="quiz-options"> ${options.map((opt, idx) => ` <button class="quiz-option" data-answer="${opt}" data-correct="${opt === word.russian_word}">${opt}</button> `).join('')} </div> `; document.querySelectorAll('.quiz-option').forEach(btn => { btn.addEventListener('click', () => { const isCorrect = btn.dataset.correct === 'true'; if (isCorrect) { session.words_correct = (session.words_correct || 0) + 1; btn.classList.add('correct'); api.createTrainingResult(session.id, word.id, true); } else { btn.classList.add('incorrect'); document.querySelector('[data-answer="' + word.russian_word + '"]').classList.add('correct'); api.createTrainingResult(session.id, word.id, false); } document.querySelectorAll('.quiz-option').forEach(b => b.disabled = true); setTimeout(() => { currentIndex++; render(); }, 1000); }); }); }; render(); return container; } testAPI() { const method = document.getElementById('apiMethod').value; const endpoint = document.getElementById('apiEndpoint').value; const payload = document.getElementById('apiPayload').value; let result; try { if (method === 'GET') { result = this.executeAPICall(endpoint, 'GET'); } else if (method === 'POST') { const data = payload ? JSON.parse(payload) : {}; result = this.executeAPICall(endpoint, 'POST', data); } else if (method === 'PUT') { const data = payload ? JSON.parse(payload) : {}; result = this.executeAPICall(endpoint, 'PUT', data); } else if (method === 'DELETE') { result = this.executeAPICall(endpoint, 'DELETE'); } document.getElementById('apiResponse').textContent = JSON.stringify(result, null, 2); api.logRequest(method, endpoint, result.status, ''); this.updateAPILogs(); } catch (error) { document.getElementById('apiResponse').textContent = 'Ошибка: ' + error.message; api.logRequest(method, endpoint, 500, error.message); this.updateAPILogs(); } } executeAPICall(endpoint, method, data = null) { if (endpoint === '/api/categories') { if (method === 'GET') return api.getCategories(); if (method === 'POST') return api.createCategory(data.name, data.description); } else if (endpoint.match(/\/api\/categories\/\d+/)) { const id = parseInt(endpoint.split('/').pop()); if (method === 'GET') return api.getCategoryById(id); if (method === 'PUT') return api.updateCategory(id, data.name, data.description); if (method === 'DELETE') return api.deleteCategory(id); } else if (endpoint === '/api/words') { if (method === 'GET') return api.getWords(); if (method === 'POST') return api.createWord(data.english_word, data.russian_word, data.category_id, data.difficulty_id, data.status_id); } else if (endpoint.match(/\/api\/words\/\d+/)) { const id = parseInt(endpoint.split('/').pop()); if (method === 'GET') return api.getWordById(id); if (method === 'PUT') return api.updateWord(id, data.english_word, data.russian_word, data.difficulty_id, data.status_id); if (method === 'DELETE') return api.deleteWord(id); } else if (endpoint === '/api/notes') { if (method === 'GET') return api.getNotes(); if (method === 'POST') return api.createNote(data.note_text, data.category_id); } else if (endpoint.match(/\/api\/notes\/\d+/)) { const id = parseInt(endpoint.split('/').pop()); if (method === 'DELETE') return api.deleteNote(id); } else if (endpoint === '/api/examples') { if (method === 'POST') return api.createExampleSentence(data.word_id, data.sentence_en, data.sentence_ru); } else if (endpoint.match(/\/api\/examples\/\d+/)) { const id = parseInt(endpoint.split('/').pop()); if (method === 'DELETE') return api.deleteExampleSentence(id); } else if (endpoint === '/api/training-sessions') { if (method === 'GET') return api.getTrainingSessions(); if (method === 'POST') return api.createTrainingSession(data.category_id); } else if (endpoint === '/api/word-statuses') { return api.getWordStatuses(); } else if (endpoint === '/api/difficulty-levels') { return api.getDifficultyLevels(); } return { status: 404, error: 'Endpoint не найден' }; } updateAPILogs() { const logsList = document.getElementById('apiLogs'); logsList.innerHTML = ''; api.logs.forEach(log => { const logItem = document.createElement('div'); logItem.className = 'log-item ' + (log.includes('201') || log.includes('200') ? 'success' : 'error'); logItem.textContent = log; logsList.appendChild(logItem); }); } getStatusBadgeClass(status) { const statusMap = { 'Новое': 'badge-new', 'В процессе': 'badge-learning', 'Выучено': 'badge-learned', 'Забыто': 'badge-forgotten' }; return statusMap[status] || 'badge-new'; } getDifficultyBadgeClass(difficulty) { const diffMap = { 'Лёгкое': 'badge-easy', 'Среднее': 'badge-medium', 'Сложное': 'badge-hard' }; return diffMap[difficulty] || 'badge-medium'; } showError(elementId, message) { const errorEl = document.getElementById(elementId); errorEl.textContent = message; errorEl.classList.add('show'); setTimeout(() => { errorEl.classList.remove('show'); }, 3000); } escapeHtml(text) { const div = document.createElement('div'); div.textContent = text; return div.innerHTML; } }