Унификация отображения ошибок: LoadingError для загрузки, Toast для действий
All checks were successful
Build and Push Docker Image / build-and-push (push) Successful in 34s

This commit is contained in:
poignatov
2026-01-11 15:51:28 +03:00
parent 8023fb9108
commit 932dba8682
17 changed files with 284 additions and 91 deletions

View File

@@ -99,6 +99,7 @@ function AppContent() {
const [currentWeekError, setCurrentWeekError] = useState(null)
const [fullStatisticsError, setFullStatisticsError] = useState(null)
const [prioritiesError, setPrioritiesError] = useState(null)
const [tasksError, setTasksError] = useState(null)
// Состояние для кнопки Refresh (если она есть)
const [isRefreshing, setIsRefreshing] = useState(false)
@@ -344,6 +345,7 @@ function AppContent() {
} else {
setTasksLoading(true)
}
setTasksError(null)
const response = await authFetch('/api/tasks')
if (!response.ok) {
throw new Error('Ошибка загрузки данных')
@@ -352,6 +354,7 @@ function AppContent() {
setTasksData(jsonData)
} catch (err) {
console.error('Ошибка загрузки списка задач:', err)
setTasksError(err.message || 'Ошибка загрузки данных')
} finally {
if (isBackground) {
setTasksBackgroundLoading(false)
@@ -834,6 +837,8 @@ function AppContent() {
data={tasksData}
loading={tasksLoading}
backgroundLoading={tasksBackgroundLoading}
error={tasksError}
onRetry={() => fetchTasksData(false)}
onRefresh={(isBackground = false) => fetchTasksData(isBackground)}
/>
</div>

View File

@@ -1,4 +1,5 @@
import ProjectProgressBar from './ProjectProgressBar'
import LoadingError from './LoadingError'
import { getAllProjectsSorted, getProjectColor } from '../utils/projectUtils'
function CurrentWeek({ onProjectClick, data, loading, error, onRetry, allProjectsData, onNavigate }) {
@@ -18,20 +19,7 @@ function CurrentWeek({ onProjectClick, data, loading, error, onRetry, allProject
}
if (error && (!data || projectsData.length === 0)) {
return (
<div className="flex flex-col items-center justify-center py-16">
<div className="bg-red-50 border border-red-200 rounded-lg p-6 mb-4 max-w-md">
<div className="text-red-700 font-semibold mb-2">Ошибка загрузки</div>
<div className="text-red-600 text-sm">{error}</div>
</div>
<button
onClick={onRetry}
className="px-6 py-3 bg-gradient-to-r from-indigo-600 to-purple-600 text-white rounded-lg hover:from-indigo-700 hover:to-purple-700 transition-all duration-200 shadow-lg hover:shadow-xl font-semibold"
>
Попробовать снова
</button>
</div>
)
return <LoadingError onRetry={onRetry} />
}
// Процент выполнения берем только из данных API

View File

@@ -12,6 +12,7 @@ import {
} from 'chart.js'
import { Line } from 'react-chartjs-2'
import WeekProgressChart from './WeekProgressChart'
import LoadingError from './LoadingError'
import { getAllProjectsSorted, getProjectColor, sortProjectsLikeCurrentWeek } from '../utils/projectUtils'
import './Integrations.css'
@@ -129,20 +130,7 @@ function FullStatistics({ selectedProject, onClearSelection, data, loading, erro
}
if (error && !chartData) {
return (
<div className="flex flex-col items-center justify-center py-16">
<div className="bg-red-50 border border-red-200 rounded-lg p-6 mb-4 max-w-md">
<div className="text-red-700 font-semibold mb-2">Ошибка загрузки</div>
<div className="text-red-600 text-sm">{error}</div>
</div>
<button
onClick={onRetry}
className="px-6 py-3 bg-gradient-to-r from-indigo-600 to-purple-600 text-white rounded-lg hover:from-indigo-700 hover:to-purple-700 transition-all duration-200 shadow-lg hover:shadow-xl font-semibold"
>
Попробовать снова
</button>
</div>
)
return <LoadingError onRetry={onRetry} />
}
const chartOptions = {

View File

@@ -0,0 +1,54 @@
.loading-error-container {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 80px; /* Отступ для нижнего бара */
display: flex;
justify-content: center;
align-items: center;
padding: 1rem;
}
/* Учитываем safe-area для мобильных устройств */
@supports (padding-bottom: env(safe-area-inset-bottom)) {
.loading-error-container {
bottom: calc(80px + env(safe-area-inset-bottom, 0px));
}
}
.loading-error-content {
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
gap: 1rem;
}
.loading-error-text {
color: #374151;
font-size: 1.125rem;
font-weight: 500;
}
.loading-error-button {
padding: 0.75rem 1.5rem;
background: linear-gradient(to right, #4f46e5, #9333ea);
color: white;
border-radius: 0.5rem;
font-weight: 600;
border: none;
cursor: pointer;
transition: all 0.2s;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
}
.loading-error-button:hover {
background: linear-gradient(to right, #4338ca, #7e22ce);
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
}
.loading-error-button:active {
transform: scale(0.98);
}

View File

@@ -0,0 +1,23 @@
import React from 'react'
import './LoadingError.css'
function LoadingError({ onRetry }) {
return (
<div className="loading-error-container">
<div className="loading-error-content">
<div className="loading-error-text">Ошибка, повторите позже</div>
{onRetry && (
<button
onClick={onRetry}
className="loading-error-button"
>
Повторить
</button>
)}
</div>
</div>
)
}
export default LoadingError

View File

@@ -20,6 +20,8 @@ import {
import { CSS } from '@dnd-kit/utilities'
import { getAllProjectsSorted, getProjectColor } from '../utils/projectUtils'
import { useAuth } from './auth/AuthContext'
import LoadingError from './LoadingError'
import Toast from './Toast'
import './Integrations.css'
// API endpoints (используем относительные пути, проксирование настроено в nginx/vite)
@@ -29,20 +31,20 @@ const PROJECT_MOVE_API_URL = '/project/move'
const PROJECT_CREATE_API_URL = '/project/create'
// Компонент экрана добавления проекта
function AddProjectScreen({ onClose, onSuccess }) {
function AddProjectScreen({ onClose, onSuccess, onError }) {
const { authFetch } = useAuth()
const [projectName, setProjectName] = useState('')
const [isSubmitting, setIsSubmitting] = useState(false)
const [error, setError] = useState(null)
const [validationError, setValidationError] = useState(null)
const handleSubmit = async () => {
if (!projectName.trim()) {
setError('Введите название проекта')
setValidationError('Введите название проекта')
return
}
setIsSubmitting(true)
setError(null)
setValidationError(null)
try {
const response = await authFetch(PROJECT_CREATE_API_URL, {
@@ -61,7 +63,9 @@ function AddProjectScreen({ onClose, onSuccess }) {
onSuccess()
} catch (err) {
console.error('Ошибка создания проекта:', err)
setError(err.message || 'Ошибка при создании проекта')
if (onError) {
onError(err.message || 'Ошибка при создании проекта')
}
} finally {
setIsSubmitting(false)
}
@@ -126,10 +130,10 @@ function AddProjectScreen({ onClose, onSuccess }) {
}
// Компонент экрана переноса проекта
function MoveProjectScreen({ project, allProjects, onClose, onSuccess }) {
function MoveProjectScreen({ project, allProjects, onClose, onSuccess, onError }) {
const [newProjectName, setNewProjectName] = useState('')
const [isSubmitting, setIsSubmitting] = useState(false)
const [error, setError] = useState(null)
const [validationError, setValidationError] = useState(null)
const handleProjectClick = (projectName) => {
setNewProjectName(projectName)
@@ -137,12 +141,12 @@ function MoveProjectScreen({ project, allProjects, onClose, onSuccess }) {
const handleSubmit = async () => {
if (!newProjectName.trim()) {
setError('Введите название проекта')
setValidationError('Введите название проекта')
return
}
setIsSubmitting(true)
setError(null)
setValidationError(null)
try {
const projectId = project.id ?? project.name
@@ -163,7 +167,9 @@ function MoveProjectScreen({ project, allProjects, onClose, onSuccess }) {
onSuccess()
} catch (err) {
console.error('Ошибка переноса проекта:', err)
setError(err.message || 'Ошибка при переносе проекта')
if (onError) {
onError(err.message || 'Ошибка при переносе проекта')
}
} finally {
setIsSubmitting(false)
}
@@ -210,8 +216,8 @@ function MoveProjectScreen({ project, allProjects, onClose, onSuccess }) {
placeholder="Введите новое название проекта"
className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-transparent"
/>
{error && (
<div className="mt-2 text-sm text-red-600">{error}</div>
{validationError && (
<div className="mt-2 text-sm text-red-600">{validationError}</div>
)}
</div>
@@ -375,6 +381,7 @@ function ProjectPriorityManager({ allProjectsData, currentWeekData, shouldLoad,
const [projectsLoading, setProjectsLoading] = useState(false)
const [projectsError, setProjectsError] = useState(null)
const [hasDataCache, setHasDataCache] = useState(false) // Отслеживаем наличие кеша
const [toastMessage, setToastMessage] = useState(null)
// Уведомляем родительский компонент об изменении состояния загрузки
useEffect(() => {
@@ -848,7 +855,7 @@ function ProjectPriorityManager({ allProjectsData, currentWeekData, shouldLoad,
fetchProjects()
} catch (error) {
console.error('Ошибка удаления проекта:', error)
setProjectsError(error.message || 'Ошибка удаления проекта')
setToastMessage({ text: error.message || 'Ошибка удаления проекта', type: 'error' })
}
}
@@ -876,18 +883,7 @@ function ProjectPriorityManager({ allProjectsData, currentWeekData, shouldLoad,
</button>
)}
{projectsError && (!maxPriority.length && !mediumPriority.length && !lowPriority.length) && (
<div className="mb-4 rounded-lg border border-red-200 bg-red-50 p-4 text-sm text-red-700 shadow-sm flex-shrink-0">
<div className="font-semibold">Не удалось загрузить проекты</div>
<div className="mt-2 flex flex-wrap items-center justify-between gap-3">
<span className="text-red-600">{projectsError}</span>
<button
onClick={() => fetchProjects()}
className="rounded-md bg-red-600 px-3 py-1 text-white shadow hover:bg-red-700 transition"
>
Повторить
</button>
</div>
</div>
<LoadingError onRetry={fetchProjects} />
)}
{projectsLoading && (!maxPriority.length && !mediumPriority.length && !lowPriority.length) ? (
@@ -1013,6 +1009,9 @@ function ProjectPriorityManager({ allProjectsData, currentWeekData, shouldLoad,
setSelectedProject(null)
fetchProjects()
}}
onError={(errorMessage) => {
setToastMessage({ text: errorMessage, type: 'error' })
}}
/>
)}
@@ -1024,6 +1023,17 @@ function ProjectPriorityManager({ allProjectsData, currentWeekData, shouldLoad,
setShowAddScreen(false)
fetchProjects()
}}
onError={(errorMessage) => {
setToastMessage({ text: errorMessage, type: 'error' })
}}
/>
)}
{toastMessage && (
<Toast
message={toastMessage.text}
type={toastMessage.type}
onClose={() => setToastMessage(null)}
/>
)}
</div>

View File

@@ -1,5 +1,7 @@
import React, { useState, useEffect, useCallback, useMemo } from 'react'
import { useAuth } from './auth/AuthContext'
import LoadingError from './LoadingError'
import Toast from './Toast'
import './TaskDetail.css'
const API_URL = '/api/tasks'
@@ -379,6 +381,7 @@ function TaskDetail({ taskId, onClose, onRefresh, onTaskCompleted }) {
const [selectedSubtasks, setSelectedSubtasks] = useState(new Set())
const [progressionValue, setProgressionValue] = useState('')
const [isCompleting, setIsCompleting] = useState(false)
const [toastMessage, setToastMessage] = useState(null)
const fetchTaskDetail = useCallback(async () => {
try {
@@ -479,7 +482,7 @@ function TaskDetail({ taskId, onClose, onRefresh, onTaskCompleted }) {
}
} catch (err) {
console.error('Error completing task:', err)
alert(err.message || 'Ошибка при выполнении задачи')
setToastMessage({ text: err.message || 'Ошибка при выполнении задачи', type: 'error' })
} finally {
setIsCompleting(false)
}
@@ -547,8 +550,8 @@ function TaskDetail({ taskId, onClose, onRefresh, onTaskCompleted }) {
<div className="loading">Загрузка...</div>
)}
{error && (
<div className="error-message">{error}</div>
{error && !loading && (
<LoadingError onRetry={fetchTaskDetail} />
)}
{!loading && !error && taskDetail && (
@@ -646,6 +649,13 @@ function TaskDetail({ taskId, onClose, onRefresh, onTaskCompleted }) {
)}
</div>
</div>
{toastMessage && (
<Toast
message={toastMessage.text}
type={toastMessage.type}
onClose={() => setToastMessage(null)}
/>
)}
</div>
)
}

View File

@@ -1,5 +1,6 @@
import React, { useState, useEffect, useRef } from 'react'
import { useAuth } from './auth/AuthContext'
import Toast from './Toast'
import './TaskForm.css'
const API_URL = '/api/tasks'
@@ -17,7 +18,8 @@ function TaskForm({ onNavigate, taskId }) {
const [subtasks, setSubtasks] = useState([])
const [projects, setProjects] = useState([])
const [loading, setLoading] = useState(false)
const [error, setError] = useState('')
const [error, setError] = useState('') // Только для валидации
const [toastMessage, setToastMessage] = useState(null)
const [loadingTask, setLoadingTask] = useState(false)
const [isDeleting, setIsDeleting] = useState(false)
const debounceTimer = useRef(null)
@@ -534,7 +536,7 @@ function TaskForm({ onNavigate, taskId }) {
// Возвращаемся к списку задач
onNavigate?.('tasks')
} catch (err) {
setError(err.message)
setToastMessage({ text: err.message || 'Ошибка при сохранении задачи', type: 'error' })
console.error('Error saving task:', err)
} finally {
setLoading(false)
@@ -567,7 +569,7 @@ function TaskForm({ onNavigate, taskId }) {
onNavigate?.('tasks')
} catch (err) {
console.error('Error deleting task:', err)
setError('Ошибка при удалении задачи')
setToastMessage({ text: err.message || 'Ошибка при удалении задачи', type: 'error' })
setIsDeleting(false)
}
}
@@ -864,7 +866,10 @@ function TaskForm({ onNavigate, taskId }) {
))}
</div>
{error && <div className="error-message">{error}</div>}
{/* Показываем ошибку валидации только если это ошибка валидации, не ошибка действия */}
{error && (error.includes('обязательно') || error.includes('должны быть заполнены') || error.includes('нельзя одновременно')) && (
<div className="error-message">{error}</div>
)}
<div className="form-actions">
<button type="submit" disabled={loading || isDeleting} className="submit-button">
@@ -893,6 +898,13 @@ function TaskForm({ onNavigate, taskId }) {
)}
</div>
</form>
{toastMessage && (
<Toast
message={toastMessage.text}
type={toastMessage.type}
onClose={() => setToastMessage(null)}
/>
)}
</div>
)
}

View File

@@ -1,12 +1,13 @@
import React, { useState, useEffect, useMemo, useRef } from 'react'
import { useAuth } from './auth/AuthContext'
import TaskDetail from './TaskDetail'
import LoadingError from './LoadingError'
import Toast from './Toast'
import './TaskList.css'
const API_URL = '/api/tasks'
function TaskList({ onNavigate, data, loading, backgroundLoading, onRefresh }) {
function TaskList({ onNavigate, data, loading, backgroundLoading, error, onRetry, onRefresh }) {
const { authFetch } = useAuth()
// Инициализируем tasks из data, если data есть, иначе пустой массив
const [tasks, setTasks] = useState(() => data && Array.isArray(data) ? data : [])
@@ -351,7 +352,7 @@ function TaskList({ onNavigate, data, loading, backgroundLoading, onRefresh }) {
setPostponeDate('')
} catch (err) {
console.error('Error postponing task:', err)
alert(err.message || 'Ошибка при переносе задачи')
setToast({ message: err.message || 'Ошибка при переносе задачи', type: 'error' })
} finally {
setIsPostponing(false)
}
@@ -632,6 +633,15 @@ function TaskList({ onNavigate, data, loading, backgroundLoading, onRefresh }) {
const hasDataInState = tasks && Array.isArray(tasks) && tasks.length > 0
const hasData = hasDataInProps || hasDataInState
// Показываем ошибку загрузки, если есть ошибка и нет данных
if (error && !hasData && !loading) {
return (
<div className="task-list">
<LoadingError onRetry={onRetry} />
</div>
)
}
// Показываем загрузку только если:
// 1. Идет загрузка (loading = true)
// 2. Это не фоновая загрузка (backgroundLoading = false)
@@ -657,6 +667,7 @@ function TaskList({ onNavigate, data, loading, backgroundLoading, onRefresh }) {
{toast && (
<Toast
message={toast.message}
type={toast.type || 'success'}
onClose={() => setToast(null)}
/>
)}
@@ -721,7 +732,7 @@ function TaskList({ onNavigate, data, loading, backgroundLoading, onRefresh }) {
taskId={selectedTaskForDetail}
onClose={handleCloseDetail}
onRefresh={onRefresh}
onTaskCompleted={() => setToast({ message: 'Задача выполнена' })}
onTaskCompleted={() => setToast({ message: 'Задача выполнена', type: 'success' })}
/>
)}

View File

@@ -1,5 +1,6 @@
import React, { useState, useEffect } from 'react'
import { useAuth } from './auth/AuthContext'
import LoadingError from './LoadingError'
import './Integrations.css'
function TelegramIntegration({ onNavigate }) {
@@ -50,6 +51,17 @@ function TelegramIntegration({ onNavigate }) {
)
}
if (error && !integration) {
return (
<div className="p-4 md:p-6">
<button className="close-x-button" onClick={() => onNavigate?.('profile')} title="Закрыть">
</button>
<LoadingError onRetry={fetchIntegration} />
</div>
)
}
return (
<div className="p-4 md:p-6">
<button className="close-x-button" onClick={() => onNavigate?.('profile')} title="Закрыть">
@@ -58,12 +70,6 @@ function TelegramIntegration({ onNavigate }) {
<h1 className="text-2xl font-bold mb-6">Telegram интеграция</h1>
{error && (
<div className="mb-4 p-3 bg-red-50 border border-red-200 rounded-lg text-red-700">
{error}
</div>
)}
<div className="bg-white rounded-lg shadow-md p-6 mb-6">
<h2 className="text-lg font-semibold mb-4">Статус подключения</h2>

View File

@@ -1,5 +1,6 @@
import React, { useState, useEffect, useRef } from 'react'
import { useAuth } from './auth/AuthContext'
import LoadingError from './LoadingError'
import './TestConfigSelection.css'
const API_URL = '/api'
@@ -167,7 +168,7 @@ function TestConfigSelection({ onNavigate, refreshTrigger = 0 }) {
if (error) {
return (
<div className="config-selection">
<div className="error-message">{error}</div>
<LoadingError onRetry={fetchTestConfigsAndDictionaries} />
</div>
)
}

View File

@@ -1,5 +1,6 @@
import React, { useState, useEffect, useRef } from 'react'
import { useAuth } from './auth/AuthContext'
import LoadingError from './LoadingError'
import './TestWords.css'
import './Integrations.css'
@@ -625,7 +626,62 @@ function TestWords({ onNavigate, wordCount: initialWordCount, configId: initialC
</div>
)}
{error && (
<div className="test-error">{error}</div>
<LoadingError onRetry={() => {
setError('')
setLoading(true)
// Перезагружаем слова
const loadWords = async () => {
try {
if (configId === null) {
throw new Error('config_id обязателен для запуска теста')
}
const url = `${API_URL}/test/words?config_id=${configId}`
const response = await authFetch(url)
if (!response.ok) {
throw new Error('Ошибка при загрузке слов')
}
const data = await response.json()
if (!Array.isArray(data) || data.length === 0) {
throw new Error('Недостаточно слов для теста')
}
const stats = {}
data.forEach(word => {
stats[word.id] = {
success: word.success || 0,
failure: word.failure || 0,
lastSuccessAt: word.last_success_at || null,
lastFailureAt: word.last_failure_at || null
}
})
setWords(data)
const wordsCount = data.length
const cardsCount = maxCards !== null && maxCards > 0 ? maxCards : wordsCount
const n = Math.max(1, Math.floor(0.7 * cardsCount / wordsCount))
let wordPool = []
for (let i = 0; i < n; i++) {
wordPool.push(...data)
}
wordPool = redistributeWordsEvenly(wordPool, data)
setTestWords(wordPool)
setWordStats(stats)
wordStatsRef.current = stats
setShowPreview(true)
setCardsShown(0)
} catch (err) {
setError(err.message)
} finally {
setLoading(false)
}
}
loadWords()
}} />
)}
{!loading && !error && !isFinishingRef.current && currentWord && (() => {
const word = currentWord

View File

@@ -14,6 +14,15 @@
transition: all 0.3s ease-out;
}
.toast-success {
background: white;
}
.toast-error {
background: #fef2f2;
border: 1px solid #fecaca;
}
.toast-visible {
transform: translateX(-50%) translateY(0);
opacity: 1;
@@ -32,3 +41,7 @@
line-height: 1.5;
}
.toast-error .toast-message {
color: #991b1b;
}

View File

@@ -1,7 +1,7 @@
import React, { useEffect, useState } from 'react'
import './Toast.css'
function Toast({ message, onClose, duration = 3000 }) {
function Toast({ message, onClose, duration = 3000, type = 'success' }) {
const [isVisible, setIsVisible] = useState(true)
useEffect(() => {
@@ -18,7 +18,7 @@ function Toast({ message, onClose, duration = 3000 }) {
if (!isVisible) return null
return (
<div className={`toast ${isVisible ? 'toast-visible' : ''}`}>
<div className={`toast toast-${type} ${isVisible ? 'toast-visible' : ''}`}>
<div className="toast-content">
<span className="toast-message">{message}</span>
</div>

View File

@@ -1,5 +1,7 @@
import React, { useState, useEffect } from 'react'
import { useAuth } from './auth/AuthContext'
import LoadingError from './LoadingError'
import Toast from './Toast'
import './Integrations.css'
function TodoistIntegration({ onNavigate }) {
@@ -9,6 +11,8 @@ function TodoistIntegration({ onNavigate }) {
const [loading, setLoading] = useState(true)
const [error, setError] = useState('')
const [message, setMessage] = useState('')
const [toastMessage, setToastMessage] = useState(null)
const [isLoadingError, setIsLoadingError] = useState(false)
useEffect(() => {
checkStatus()
@@ -23,7 +27,7 @@ function TodoistIntegration({ onNavigate }) {
window.history.replaceState({}, '', window.location.pathname)
} else if (status === 'error') {
const errorMsg = params.get('message') || 'Произошла ошибка'
setError(errorMsg)
setToastMessage({ text: errorMsg, type: 'error' })
window.history.replaceState({}, '', window.location.pathname)
}
}
@@ -45,6 +49,7 @@ function TodoistIntegration({ onNavigate }) {
} catch (error) {
console.error('Error checking status:', error)
setError(error.message || 'Не удалось проверить статус')
setIsLoadingError(true)
} finally {
setLoading(false)
}
@@ -69,7 +74,7 @@ function TodoistIntegration({ onNavigate }) {
}
} catch (error) {
console.error('Error connecting Todoist:', error)
setError(error.message || 'Не удалось подключить Todoist')
setToastMessage({ text: error.message || 'Не удалось подключить Todoist', type: 'error' })
setLoading(false)
}
}
@@ -91,15 +96,26 @@ function TodoistIntegration({ onNavigate }) {
}
setConnected(false)
setTodoistEmail('')
setMessage('Todoist отключен')
setToastMessage({ text: 'Todoist отключен', type: 'success' })
} catch (error) {
console.error('Error disconnecting:', error)
setError(error.message || 'Не удалось отключить Todoist')
setToastMessage({ text: error.message || 'Не удалось отключить Todoist', type: 'error' })
} finally {
setLoading(false)
}
}
if (isLoadingError && !loading) {
return (
<div className="p-4 md:p-6">
<button className="close-x-button" onClick={() => onNavigate?.('profile')} title="Закрыть">
</button>
<LoadingError onRetry={checkStatus} />
</div>
)
}
return (
<div className="p-4 md:p-6">
<button className="close-x-button" onClick={() => onNavigate?.('profile')} title="Закрыть">
@@ -108,18 +124,6 @@ function TodoistIntegration({ onNavigate }) {
<h1 className="text-2xl font-bold mb-6">Todoist интеграция</h1>
{message && (
<div className="bg-green-50 border border-green-200 rounded-lg p-4 mb-6 text-green-800">
{message}
</div>
)}
{error && (
<div className="bg-red-50 border border-red-200 rounded-lg p-4 mb-6 text-red-800">
{error}
</div>
)}
{loading ? (
<div className="fixed inset-0 flex justify-center items-center">
<div className="flex flex-col items-center">
@@ -192,6 +196,13 @@ function TodoistIntegration({ onNavigate }) {
</div>
</div>
)}
{toastMessage && (
<Toast
message={toastMessage.text}
type={toastMessage.type}
onClose={() => setToastMessage(null)}
/>
)}
</div>
)
}

View File

@@ -1,5 +1,6 @@
import React, { useState, useEffect } from 'react'
import { useAuth } from './auth/AuthContext'
import LoadingError from './LoadingError'
import './WordList.css'
const API_URL = '/api'
@@ -176,7 +177,11 @@ function WordList({ onNavigate, dictionaryId, isNewDictionary, refreshTrigger =
if (error) {
return (
<div className="word-list">
<div className="error-message">{error}</div>
<LoadingError onRetry={() => {
if (hasValidDictionary(currentDictionaryId)) {
fetchWordsForDictionary(currentDictionaryId)
}
}} />
</div>
)
}