diff --git a/VERSION b/VERSION index 203e6d5..d20cc2b 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -3.8.9 +3.8.10 diff --git a/play-life-web/src/App.jsx b/play-life-web/src/App.jsx index 171940e..f78c5af 100644 --- a/play-life-web/src/App.jsx +++ b/play-life-web/src/App.jsx @@ -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)} /> diff --git a/play-life-web/src/components/CurrentWeek.jsx b/play-life-web/src/components/CurrentWeek.jsx index c1a5d89..db2411a 100644 --- a/play-life-web/src/components/CurrentWeek.jsx +++ b/play-life-web/src/components/CurrentWeek.jsx @@ -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 ( -
-
-
Ошибка загрузки
-
{error}
-
- -
- ) + return } // Процент выполнения берем только из данных API diff --git a/play-life-web/src/components/FullStatistics.jsx b/play-life-web/src/components/FullStatistics.jsx index 439507a..0932ed0 100644 --- a/play-life-web/src/components/FullStatistics.jsx +++ b/play-life-web/src/components/FullStatistics.jsx @@ -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 ( -
-
-
Ошибка загрузки
-
{error}
-
- -
- ) + return } const chartOptions = { diff --git a/play-life-web/src/components/LoadingError.css b/play-life-web/src/components/LoadingError.css new file mode 100644 index 0000000..392b61f --- /dev/null +++ b/play-life-web/src/components/LoadingError.css @@ -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); +} + diff --git a/play-life-web/src/components/LoadingError.jsx b/play-life-web/src/components/LoadingError.jsx new file mode 100644 index 0000000..1cb2917 --- /dev/null +++ b/play-life-web/src/components/LoadingError.jsx @@ -0,0 +1,23 @@ +import React from 'react' +import './LoadingError.css' + +function LoadingError({ onRetry }) { + return ( +
+
+
Ошибка, повторите позже
+ {onRetry && ( + + )} +
+
+ ) +} + +export default LoadingError + diff --git a/play-life-web/src/components/ProjectPriorityManager.jsx b/play-life-web/src/components/ProjectPriorityManager.jsx index d865da8..e0931bf 100644 --- a/play-life-web/src/components/ProjectPriorityManager.jsx +++ b/play-life-web/src/components/ProjectPriorityManager.jsx @@ -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 && ( -
{error}
+ {validationError && ( +
{validationError}
)} @@ -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, )} {projectsError && (!maxPriority.length && !mediumPriority.length && !lowPriority.length) && ( -
-
Не удалось загрузить проекты
-
- {projectsError} - -
-
+ )} {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 && ( + setToastMessage(null)} /> )} diff --git a/play-life-web/src/components/TaskDetail.jsx b/play-life-web/src/components/TaskDetail.jsx index 98fa624..90bb5f0 100644 --- a/play-life-web/src/components/TaskDetail.jsx +++ b/play-life-web/src/components/TaskDetail.jsx @@ -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 }) {
Загрузка...
)} - {error && ( -
{error}
+ {error && !loading && ( + )} {!loading && !error && taskDetail && ( @@ -646,6 +649,13 @@ function TaskDetail({ taskId, onClose, onRefresh, onTaskCompleted }) { )} + {toastMessage && ( + setToastMessage(null)} + /> + )} ) } diff --git a/play-life-web/src/components/TaskForm.jsx b/play-life-web/src/components/TaskForm.jsx index 515f435..4efc584 100644 --- a/play-life-web/src/components/TaskForm.jsx +++ b/play-life-web/src/components/TaskForm.jsx @@ -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 }) { ))} - {error &&
{error}
} + {/* Показываем ошибку валидации только если это ошибка валидации, не ошибка действия */} + {error && (error.includes('обязательно') || error.includes('должны быть заполнены') || error.includes('нельзя одновременно')) && ( +
{error}
+ )}
+ {toastMessage && ( + setToastMessage(null)} + /> + )} ) } diff --git a/play-life-web/src/components/TaskList.jsx b/play-life-web/src/components/TaskList.jsx index 2516fcf..7ad52cf 100644 --- a/play-life-web/src/components/TaskList.jsx +++ b/play-life-web/src/components/TaskList.jsx @@ -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 ( +
+ +
+ ) + } + // Показываем загрузку только если: // 1. Идет загрузка (loading = true) // 2. Это не фоновая загрузка (backgroundLoading = false) @@ -657,6 +667,7 @@ function TaskList({ onNavigate, data, loading, backgroundLoading, onRefresh }) { {toast && ( 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' })} /> )} diff --git a/play-life-web/src/components/TelegramIntegration.jsx b/play-life-web/src/components/TelegramIntegration.jsx index 124dc42..140095f 100644 --- a/play-life-web/src/components/TelegramIntegration.jsx +++ b/play-life-web/src/components/TelegramIntegration.jsx @@ -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 ( +
+ + +
+ ) + } + return (
+ +
+ ) + } + return (
) } diff --git a/play-life-web/src/components/WordList.jsx b/play-life-web/src/components/WordList.jsx index 2aa2b9a..efb5617 100644 --- a/play-life-web/src/components/WordList.jsx +++ b/play-life-web/src/components/WordList.jsx @@ -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 (
-
{error}
+ { + if (hasValidDictionary(currentDictionaryId)) { + fetchWordsForDictionary(currentDictionaryId) + } + }} />
) }