Унификация отображения ошибок: LoadingError для загрузки, Toast для действий
All checks were successful
Build and Push Docker Image / build-and-push (push) Successful in 34s
All checks were successful
Build and Push Docker Image / build-and-push (push) Successful in 34s
This commit is contained in:
@@ -99,6 +99,7 @@ function AppContent() {
|
|||||||
const [currentWeekError, setCurrentWeekError] = useState(null)
|
const [currentWeekError, setCurrentWeekError] = useState(null)
|
||||||
const [fullStatisticsError, setFullStatisticsError] = useState(null)
|
const [fullStatisticsError, setFullStatisticsError] = useState(null)
|
||||||
const [prioritiesError, setPrioritiesError] = useState(null)
|
const [prioritiesError, setPrioritiesError] = useState(null)
|
||||||
|
const [tasksError, setTasksError] = useState(null)
|
||||||
|
|
||||||
// Состояние для кнопки Refresh (если она есть)
|
// Состояние для кнопки Refresh (если она есть)
|
||||||
const [isRefreshing, setIsRefreshing] = useState(false)
|
const [isRefreshing, setIsRefreshing] = useState(false)
|
||||||
@@ -344,6 +345,7 @@ function AppContent() {
|
|||||||
} else {
|
} else {
|
||||||
setTasksLoading(true)
|
setTasksLoading(true)
|
||||||
}
|
}
|
||||||
|
setTasksError(null)
|
||||||
const response = await authFetch('/api/tasks')
|
const response = await authFetch('/api/tasks')
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error('Ошибка загрузки данных')
|
throw new Error('Ошибка загрузки данных')
|
||||||
@@ -352,6 +354,7 @@ function AppContent() {
|
|||||||
setTasksData(jsonData)
|
setTasksData(jsonData)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Ошибка загрузки списка задач:', err)
|
console.error('Ошибка загрузки списка задач:', err)
|
||||||
|
setTasksError(err.message || 'Ошибка загрузки данных')
|
||||||
} finally {
|
} finally {
|
||||||
if (isBackground) {
|
if (isBackground) {
|
||||||
setTasksBackgroundLoading(false)
|
setTasksBackgroundLoading(false)
|
||||||
@@ -834,6 +837,8 @@ function AppContent() {
|
|||||||
data={tasksData}
|
data={tasksData}
|
||||||
loading={tasksLoading}
|
loading={tasksLoading}
|
||||||
backgroundLoading={tasksBackgroundLoading}
|
backgroundLoading={tasksBackgroundLoading}
|
||||||
|
error={tasksError}
|
||||||
|
onRetry={() => fetchTasksData(false)}
|
||||||
onRefresh={(isBackground = false) => fetchTasksData(isBackground)}
|
onRefresh={(isBackground = false) => fetchTasksData(isBackground)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import ProjectProgressBar from './ProjectProgressBar'
|
import ProjectProgressBar from './ProjectProgressBar'
|
||||||
|
import LoadingError from './LoadingError'
|
||||||
import { getAllProjectsSorted, getProjectColor } from '../utils/projectUtils'
|
import { getAllProjectsSorted, getProjectColor } from '../utils/projectUtils'
|
||||||
|
|
||||||
function CurrentWeek({ onProjectClick, data, loading, error, onRetry, allProjectsData, onNavigate }) {
|
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)) {
|
if (error && (!data || projectsData.length === 0)) {
|
||||||
return (
|
return <LoadingError onRetry={onRetry} />
|
||||||
<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>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Процент выполнения берем только из данных API
|
// Процент выполнения берем только из данных API
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import {
|
|||||||
} from 'chart.js'
|
} from 'chart.js'
|
||||||
import { Line } from 'react-chartjs-2'
|
import { Line } from 'react-chartjs-2'
|
||||||
import WeekProgressChart from './WeekProgressChart'
|
import WeekProgressChart from './WeekProgressChart'
|
||||||
|
import LoadingError from './LoadingError'
|
||||||
import { getAllProjectsSorted, getProjectColor, sortProjectsLikeCurrentWeek } from '../utils/projectUtils'
|
import { getAllProjectsSorted, getProjectColor, sortProjectsLikeCurrentWeek } from '../utils/projectUtils'
|
||||||
import './Integrations.css'
|
import './Integrations.css'
|
||||||
|
|
||||||
@@ -129,20 +130,7 @@ function FullStatistics({ selectedProject, onClearSelection, data, loading, erro
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (error && !chartData) {
|
if (error && !chartData) {
|
||||||
return (
|
return <LoadingError onRetry={onRetry} />
|
||||||
<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>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const chartOptions = {
|
const chartOptions = {
|
||||||
|
|||||||
54
play-life-web/src/components/LoadingError.css
Normal file
54
play-life-web/src/components/LoadingError.css
Normal 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);
|
||||||
|
}
|
||||||
|
|
||||||
23
play-life-web/src/components/LoadingError.jsx
Normal file
23
play-life-web/src/components/LoadingError.jsx
Normal 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
|
||||||
|
|
||||||
@@ -20,6 +20,8 @@ import {
|
|||||||
import { CSS } from '@dnd-kit/utilities'
|
import { CSS } from '@dnd-kit/utilities'
|
||||||
import { getAllProjectsSorted, getProjectColor } from '../utils/projectUtils'
|
import { getAllProjectsSorted, getProjectColor } from '../utils/projectUtils'
|
||||||
import { useAuth } from './auth/AuthContext'
|
import { useAuth } from './auth/AuthContext'
|
||||||
|
import LoadingError from './LoadingError'
|
||||||
|
import Toast from './Toast'
|
||||||
import './Integrations.css'
|
import './Integrations.css'
|
||||||
|
|
||||||
// API endpoints (используем относительные пути, проксирование настроено в nginx/vite)
|
// API endpoints (используем относительные пути, проксирование настроено в nginx/vite)
|
||||||
@@ -29,20 +31,20 @@ const PROJECT_MOVE_API_URL = '/project/move'
|
|||||||
const PROJECT_CREATE_API_URL = '/project/create'
|
const PROJECT_CREATE_API_URL = '/project/create'
|
||||||
|
|
||||||
// Компонент экрана добавления проекта
|
// Компонент экрана добавления проекта
|
||||||
function AddProjectScreen({ onClose, onSuccess }) {
|
function AddProjectScreen({ onClose, onSuccess, onError }) {
|
||||||
const { authFetch } = useAuth()
|
const { authFetch } = useAuth()
|
||||||
const [projectName, setProjectName] = useState('')
|
const [projectName, setProjectName] = useState('')
|
||||||
const [isSubmitting, setIsSubmitting] = useState(false)
|
const [isSubmitting, setIsSubmitting] = useState(false)
|
||||||
const [error, setError] = useState(null)
|
const [validationError, setValidationError] = useState(null)
|
||||||
|
|
||||||
const handleSubmit = async () => {
|
const handleSubmit = async () => {
|
||||||
if (!projectName.trim()) {
|
if (!projectName.trim()) {
|
||||||
setError('Введите название проекта')
|
setValidationError('Введите название проекта')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
setIsSubmitting(true)
|
setIsSubmitting(true)
|
||||||
setError(null)
|
setValidationError(null)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await authFetch(PROJECT_CREATE_API_URL, {
|
const response = await authFetch(PROJECT_CREATE_API_URL, {
|
||||||
@@ -61,7 +63,9 @@ function AddProjectScreen({ onClose, onSuccess }) {
|
|||||||
onSuccess()
|
onSuccess()
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Ошибка создания проекта:', err)
|
console.error('Ошибка создания проекта:', err)
|
||||||
setError(err.message || 'Ошибка при создании проекта')
|
if (onError) {
|
||||||
|
onError(err.message || 'Ошибка при создании проекта')
|
||||||
|
}
|
||||||
} finally {
|
} finally {
|
||||||
setIsSubmitting(false)
|
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 [newProjectName, setNewProjectName] = useState('')
|
||||||
const [isSubmitting, setIsSubmitting] = useState(false)
|
const [isSubmitting, setIsSubmitting] = useState(false)
|
||||||
const [error, setError] = useState(null)
|
const [validationError, setValidationError] = useState(null)
|
||||||
|
|
||||||
const handleProjectClick = (projectName) => {
|
const handleProjectClick = (projectName) => {
|
||||||
setNewProjectName(projectName)
|
setNewProjectName(projectName)
|
||||||
@@ -137,12 +141,12 @@ function MoveProjectScreen({ project, allProjects, onClose, onSuccess }) {
|
|||||||
|
|
||||||
const handleSubmit = async () => {
|
const handleSubmit = async () => {
|
||||||
if (!newProjectName.trim()) {
|
if (!newProjectName.trim()) {
|
||||||
setError('Введите название проекта')
|
setValidationError('Введите название проекта')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
setIsSubmitting(true)
|
setIsSubmitting(true)
|
||||||
setError(null)
|
setValidationError(null)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const projectId = project.id ?? project.name
|
const projectId = project.id ?? project.name
|
||||||
@@ -163,7 +167,9 @@ function MoveProjectScreen({ project, allProjects, onClose, onSuccess }) {
|
|||||||
onSuccess()
|
onSuccess()
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Ошибка переноса проекта:', err)
|
console.error('Ошибка переноса проекта:', err)
|
||||||
setError(err.message || 'Ошибка при переносе проекта')
|
if (onError) {
|
||||||
|
onError(err.message || 'Ошибка при переносе проекта')
|
||||||
|
}
|
||||||
} finally {
|
} finally {
|
||||||
setIsSubmitting(false)
|
setIsSubmitting(false)
|
||||||
}
|
}
|
||||||
@@ -210,8 +216,8 @@ function MoveProjectScreen({ project, allProjects, onClose, onSuccess }) {
|
|||||||
placeholder="Введите новое название проекта"
|
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"
|
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 && (
|
{validationError && (
|
||||||
<div className="mt-2 text-sm text-red-600">{error}</div>
|
<div className="mt-2 text-sm text-red-600">{validationError}</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -375,6 +381,7 @@ function ProjectPriorityManager({ allProjectsData, currentWeekData, shouldLoad,
|
|||||||
const [projectsLoading, setProjectsLoading] = useState(false)
|
const [projectsLoading, setProjectsLoading] = useState(false)
|
||||||
const [projectsError, setProjectsError] = useState(null)
|
const [projectsError, setProjectsError] = useState(null)
|
||||||
const [hasDataCache, setHasDataCache] = useState(false) // Отслеживаем наличие кеша
|
const [hasDataCache, setHasDataCache] = useState(false) // Отслеживаем наличие кеша
|
||||||
|
const [toastMessage, setToastMessage] = useState(null)
|
||||||
|
|
||||||
// Уведомляем родительский компонент об изменении состояния загрузки
|
// Уведомляем родительский компонент об изменении состояния загрузки
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -848,7 +855,7 @@ function ProjectPriorityManager({ allProjectsData, currentWeekData, shouldLoad,
|
|||||||
fetchProjects()
|
fetchProjects()
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Ошибка удаления проекта:', error)
|
console.error('Ошибка удаления проекта:', error)
|
||||||
setProjectsError(error.message || 'Ошибка удаления проекта')
|
setToastMessage({ text: error.message || 'Ошибка удаления проекта', type: 'error' })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -876,18 +883,7 @@ function ProjectPriorityManager({ allProjectsData, currentWeekData, shouldLoad,
|
|||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
{projectsError && (!maxPriority.length && !mediumPriority.length && !lowPriority.length) && (
|
{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">
|
<LoadingError onRetry={fetchProjects} />
|
||||||
<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>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{projectsLoading && (!maxPriority.length && !mediumPriority.length && !lowPriority.length) ? (
|
{projectsLoading && (!maxPriority.length && !mediumPriority.length && !lowPriority.length) ? (
|
||||||
@@ -1013,6 +1009,9 @@ function ProjectPriorityManager({ allProjectsData, currentWeekData, shouldLoad,
|
|||||||
setSelectedProject(null)
|
setSelectedProject(null)
|
||||||
fetchProjects()
|
fetchProjects()
|
||||||
}}
|
}}
|
||||||
|
onError={(errorMessage) => {
|
||||||
|
setToastMessage({ text: errorMessage, type: 'error' })
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -1024,6 +1023,17 @@ function ProjectPriorityManager({ allProjectsData, currentWeekData, shouldLoad,
|
|||||||
setShowAddScreen(false)
|
setShowAddScreen(false)
|
||||||
fetchProjects()
|
fetchProjects()
|
||||||
}}
|
}}
|
||||||
|
onError={(errorMessage) => {
|
||||||
|
setToastMessage({ text: errorMessage, type: 'error' })
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{toastMessage && (
|
||||||
|
<Toast
|
||||||
|
message={toastMessage.text}
|
||||||
|
type={toastMessage.type}
|
||||||
|
onClose={() => setToastMessage(null)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import React, { useState, useEffect, useCallback, useMemo } from 'react'
|
import React, { useState, useEffect, useCallback, useMemo } from 'react'
|
||||||
import { useAuth } from './auth/AuthContext'
|
import { useAuth } from './auth/AuthContext'
|
||||||
|
import LoadingError from './LoadingError'
|
||||||
|
import Toast from './Toast'
|
||||||
import './TaskDetail.css'
|
import './TaskDetail.css'
|
||||||
|
|
||||||
const API_URL = '/api/tasks'
|
const API_URL = '/api/tasks'
|
||||||
@@ -379,6 +381,7 @@ function TaskDetail({ taskId, onClose, onRefresh, onTaskCompleted }) {
|
|||||||
const [selectedSubtasks, setSelectedSubtasks] = useState(new Set())
|
const [selectedSubtasks, setSelectedSubtasks] = useState(new Set())
|
||||||
const [progressionValue, setProgressionValue] = useState('')
|
const [progressionValue, setProgressionValue] = useState('')
|
||||||
const [isCompleting, setIsCompleting] = useState(false)
|
const [isCompleting, setIsCompleting] = useState(false)
|
||||||
|
const [toastMessage, setToastMessage] = useState(null)
|
||||||
|
|
||||||
const fetchTaskDetail = useCallback(async () => {
|
const fetchTaskDetail = useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
@@ -479,7 +482,7 @@ function TaskDetail({ taskId, onClose, onRefresh, onTaskCompleted }) {
|
|||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Error completing task:', err)
|
console.error('Error completing task:', err)
|
||||||
alert(err.message || 'Ошибка при выполнении задачи')
|
setToastMessage({ text: err.message || 'Ошибка при выполнении задачи', type: 'error' })
|
||||||
} finally {
|
} finally {
|
||||||
setIsCompleting(false)
|
setIsCompleting(false)
|
||||||
}
|
}
|
||||||
@@ -547,8 +550,8 @@ function TaskDetail({ taskId, onClose, onRefresh, onTaskCompleted }) {
|
|||||||
<div className="loading">Загрузка...</div>
|
<div className="loading">Загрузка...</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{error && (
|
{error && !loading && (
|
||||||
<div className="error-message">{error}</div>
|
<LoadingError onRetry={fetchTaskDetail} />
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{!loading && !error && taskDetail && (
|
{!loading && !error && taskDetail && (
|
||||||
@@ -646,6 +649,13 @@ function TaskDetail({ taskId, onClose, onRefresh, onTaskCompleted }) {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{toastMessage && (
|
||||||
|
<Toast
|
||||||
|
message={toastMessage.text}
|
||||||
|
type={toastMessage.type}
|
||||||
|
onClose={() => setToastMessage(null)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import React, { useState, useEffect, useRef } from 'react'
|
import React, { useState, useEffect, useRef } from 'react'
|
||||||
import { useAuth } from './auth/AuthContext'
|
import { useAuth } from './auth/AuthContext'
|
||||||
|
import Toast from './Toast'
|
||||||
import './TaskForm.css'
|
import './TaskForm.css'
|
||||||
|
|
||||||
const API_URL = '/api/tasks'
|
const API_URL = '/api/tasks'
|
||||||
@@ -17,7 +18,8 @@ function TaskForm({ onNavigate, taskId }) {
|
|||||||
const [subtasks, setSubtasks] = useState([])
|
const [subtasks, setSubtasks] = useState([])
|
||||||
const [projects, setProjects] = useState([])
|
const [projects, setProjects] = useState([])
|
||||||
const [loading, setLoading] = useState(false)
|
const [loading, setLoading] = useState(false)
|
||||||
const [error, setError] = useState('')
|
const [error, setError] = useState('') // Только для валидации
|
||||||
|
const [toastMessage, setToastMessage] = useState(null)
|
||||||
const [loadingTask, setLoadingTask] = useState(false)
|
const [loadingTask, setLoadingTask] = useState(false)
|
||||||
const [isDeleting, setIsDeleting] = useState(false)
|
const [isDeleting, setIsDeleting] = useState(false)
|
||||||
const debounceTimer = useRef(null)
|
const debounceTimer = useRef(null)
|
||||||
@@ -534,7 +536,7 @@ function TaskForm({ onNavigate, taskId }) {
|
|||||||
// Возвращаемся к списку задач
|
// Возвращаемся к списку задач
|
||||||
onNavigate?.('tasks')
|
onNavigate?.('tasks')
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(err.message)
|
setToastMessage({ text: err.message || 'Ошибка при сохранении задачи', type: 'error' })
|
||||||
console.error('Error saving task:', err)
|
console.error('Error saving task:', err)
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
@@ -567,7 +569,7 @@ function TaskForm({ onNavigate, taskId }) {
|
|||||||
onNavigate?.('tasks')
|
onNavigate?.('tasks')
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Error deleting task:', err)
|
console.error('Error deleting task:', err)
|
||||||
setError('Ошибка при удалении задачи')
|
setToastMessage({ text: err.message || 'Ошибка при удалении задачи', type: 'error' })
|
||||||
setIsDeleting(false)
|
setIsDeleting(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -864,7 +866,10 @@ function TaskForm({ onNavigate, taskId }) {
|
|||||||
))}
|
))}
|
||||||
</div>
|
</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">
|
<div className="form-actions">
|
||||||
<button type="submit" disabled={loading || isDeleting} className="submit-button">
|
<button type="submit" disabled={loading || isDeleting} className="submit-button">
|
||||||
@@ -893,6 +898,13 @@ function TaskForm({ onNavigate, taskId }) {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
{toastMessage && (
|
||||||
|
<Toast
|
||||||
|
message={toastMessage.text}
|
||||||
|
type={toastMessage.type}
|
||||||
|
onClose={() => setToastMessage(null)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
import React, { useState, useEffect, useMemo, useRef } from 'react'
|
import React, { useState, useEffect, useMemo, useRef } from 'react'
|
||||||
import { useAuth } from './auth/AuthContext'
|
import { useAuth } from './auth/AuthContext'
|
||||||
import TaskDetail from './TaskDetail'
|
import TaskDetail from './TaskDetail'
|
||||||
|
import LoadingError from './LoadingError'
|
||||||
import Toast from './Toast'
|
import Toast from './Toast'
|
||||||
import './TaskList.css'
|
import './TaskList.css'
|
||||||
|
|
||||||
const API_URL = '/api/tasks'
|
const API_URL = '/api/tasks'
|
||||||
|
|
||||||
function TaskList({ onNavigate, data, loading, backgroundLoading, onRefresh }) {
|
function TaskList({ onNavigate, data, loading, backgroundLoading, error, onRetry, onRefresh }) {
|
||||||
const { authFetch } = useAuth()
|
const { authFetch } = useAuth()
|
||||||
// Инициализируем tasks из data, если data есть, иначе пустой массив
|
// Инициализируем tasks из data, если data есть, иначе пустой массив
|
||||||
const [tasks, setTasks] = useState(() => data && Array.isArray(data) ? data : [])
|
const [tasks, setTasks] = useState(() => data && Array.isArray(data) ? data : [])
|
||||||
@@ -351,7 +352,7 @@ function TaskList({ onNavigate, data, loading, backgroundLoading, onRefresh }) {
|
|||||||
setPostponeDate('')
|
setPostponeDate('')
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Error postponing task:', err)
|
console.error('Error postponing task:', err)
|
||||||
alert(err.message || 'Ошибка при переносе задачи')
|
setToast({ message: err.message || 'Ошибка при переносе задачи', type: 'error' })
|
||||||
} finally {
|
} finally {
|
||||||
setIsPostponing(false)
|
setIsPostponing(false)
|
||||||
}
|
}
|
||||||
@@ -632,6 +633,15 @@ function TaskList({ onNavigate, data, loading, backgroundLoading, onRefresh }) {
|
|||||||
const hasDataInState = tasks && Array.isArray(tasks) && tasks.length > 0
|
const hasDataInState = tasks && Array.isArray(tasks) && tasks.length > 0
|
||||||
const hasData = hasDataInProps || hasDataInState
|
const hasData = hasDataInProps || hasDataInState
|
||||||
|
|
||||||
|
// Показываем ошибку загрузки, если есть ошибка и нет данных
|
||||||
|
if (error && !hasData && !loading) {
|
||||||
|
return (
|
||||||
|
<div className="task-list">
|
||||||
|
<LoadingError onRetry={onRetry} />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
// Показываем загрузку только если:
|
// Показываем загрузку только если:
|
||||||
// 1. Идет загрузка (loading = true)
|
// 1. Идет загрузка (loading = true)
|
||||||
// 2. Это не фоновая загрузка (backgroundLoading = false)
|
// 2. Это не фоновая загрузка (backgroundLoading = false)
|
||||||
@@ -657,6 +667,7 @@ function TaskList({ onNavigate, data, loading, backgroundLoading, onRefresh }) {
|
|||||||
{toast && (
|
{toast && (
|
||||||
<Toast
|
<Toast
|
||||||
message={toast.message}
|
message={toast.message}
|
||||||
|
type={toast.type || 'success'}
|
||||||
onClose={() => setToast(null)}
|
onClose={() => setToast(null)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
@@ -721,7 +732,7 @@ function TaskList({ onNavigate, data, loading, backgroundLoading, onRefresh }) {
|
|||||||
taskId={selectedTaskForDetail}
|
taskId={selectedTaskForDetail}
|
||||||
onClose={handleCloseDetail}
|
onClose={handleCloseDetail}
|
||||||
onRefresh={onRefresh}
|
onRefresh={onRefresh}
|
||||||
onTaskCompleted={() => setToast({ message: 'Задача выполнена' })}
|
onTaskCompleted={() => setToast({ message: 'Задача выполнена', type: 'success' })}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import React, { useState, useEffect } from 'react'
|
import React, { useState, useEffect } from 'react'
|
||||||
import { useAuth } from './auth/AuthContext'
|
import { useAuth } from './auth/AuthContext'
|
||||||
|
import LoadingError from './LoadingError'
|
||||||
import './Integrations.css'
|
import './Integrations.css'
|
||||||
|
|
||||||
function TelegramIntegration({ onNavigate }) {
|
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 (
|
return (
|
||||||
<div className="p-4 md:p-6">
|
<div className="p-4 md:p-6">
|
||||||
<button className="close-x-button" onClick={() => onNavigate?.('profile')} title="Закрыть">
|
<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>
|
<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">
|
<div className="bg-white rounded-lg shadow-md p-6 mb-6">
|
||||||
<h2 className="text-lg font-semibold mb-4">Статус подключения</h2>
|
<h2 className="text-lg font-semibold mb-4">Статус подключения</h2>
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import React, { useState, useEffect, useRef } from 'react'
|
import React, { useState, useEffect, useRef } from 'react'
|
||||||
import { useAuth } from './auth/AuthContext'
|
import { useAuth } from './auth/AuthContext'
|
||||||
|
import LoadingError from './LoadingError'
|
||||||
import './TestConfigSelection.css'
|
import './TestConfigSelection.css'
|
||||||
|
|
||||||
const API_URL = '/api'
|
const API_URL = '/api'
|
||||||
@@ -167,7 +168,7 @@ function TestConfigSelection({ onNavigate, refreshTrigger = 0 }) {
|
|||||||
if (error) {
|
if (error) {
|
||||||
return (
|
return (
|
||||||
<div className="config-selection">
|
<div className="config-selection">
|
||||||
<div className="error-message">{error}</div>
|
<LoadingError onRetry={fetchTestConfigsAndDictionaries} />
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import React, { useState, useEffect, useRef } from 'react'
|
import React, { useState, useEffect, useRef } from 'react'
|
||||||
import { useAuth } from './auth/AuthContext'
|
import { useAuth } from './auth/AuthContext'
|
||||||
|
import LoadingError from './LoadingError'
|
||||||
import './TestWords.css'
|
import './TestWords.css'
|
||||||
import './Integrations.css'
|
import './Integrations.css'
|
||||||
|
|
||||||
@@ -625,7 +626,62 @@ function TestWords({ onNavigate, wordCount: initialWordCount, configId: initialC
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{error && (
|
{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 && (() => {
|
{!loading && !error && !isFinishingRef.current && currentWord && (() => {
|
||||||
const word = currentWord
|
const word = currentWord
|
||||||
|
|||||||
@@ -14,6 +14,15 @@
|
|||||||
transition: all 0.3s ease-out;
|
transition: all 0.3s ease-out;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.toast-success {
|
||||||
|
background: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toast-error {
|
||||||
|
background: #fef2f2;
|
||||||
|
border: 1px solid #fecaca;
|
||||||
|
}
|
||||||
|
|
||||||
.toast-visible {
|
.toast-visible {
|
||||||
transform: translateX(-50%) translateY(0);
|
transform: translateX(-50%) translateY(0);
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
@@ -32,3 +41,7 @@
|
|||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.toast-error .toast-message {
|
||||||
|
color: #991b1b;
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import React, { useEffect, useState } from 'react'
|
import React, { useEffect, useState } from 'react'
|
||||||
import './Toast.css'
|
import './Toast.css'
|
||||||
|
|
||||||
function Toast({ message, onClose, duration = 3000 }) {
|
function Toast({ message, onClose, duration = 3000, type = 'success' }) {
|
||||||
const [isVisible, setIsVisible] = useState(true)
|
const [isVisible, setIsVisible] = useState(true)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -18,7 +18,7 @@ function Toast({ message, onClose, duration = 3000 }) {
|
|||||||
if (!isVisible) return null
|
if (!isVisible) return null
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`toast ${isVisible ? 'toast-visible' : ''}`}>
|
<div className={`toast toast-${type} ${isVisible ? 'toast-visible' : ''}`}>
|
||||||
<div className="toast-content">
|
<div className="toast-content">
|
||||||
<span className="toast-message">{message}</span>
|
<span className="toast-message">{message}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import React, { useState, useEffect } from 'react'
|
import React, { useState, useEffect } from 'react'
|
||||||
import { useAuth } from './auth/AuthContext'
|
import { useAuth } from './auth/AuthContext'
|
||||||
|
import LoadingError from './LoadingError'
|
||||||
|
import Toast from './Toast'
|
||||||
import './Integrations.css'
|
import './Integrations.css'
|
||||||
|
|
||||||
function TodoistIntegration({ onNavigate }) {
|
function TodoistIntegration({ onNavigate }) {
|
||||||
@@ -9,6 +11,8 @@ function TodoistIntegration({ onNavigate }) {
|
|||||||
const [loading, setLoading] = useState(true)
|
const [loading, setLoading] = useState(true)
|
||||||
const [error, setError] = useState('')
|
const [error, setError] = useState('')
|
||||||
const [message, setMessage] = useState('')
|
const [message, setMessage] = useState('')
|
||||||
|
const [toastMessage, setToastMessage] = useState(null)
|
||||||
|
const [isLoadingError, setIsLoadingError] = useState(false)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
checkStatus()
|
checkStatus()
|
||||||
@@ -23,7 +27,7 @@ function TodoistIntegration({ onNavigate }) {
|
|||||||
window.history.replaceState({}, '', window.location.pathname)
|
window.history.replaceState({}, '', window.location.pathname)
|
||||||
} else if (status === 'error') {
|
} else if (status === 'error') {
|
||||||
const errorMsg = params.get('message') || 'Произошла ошибка'
|
const errorMsg = params.get('message') || 'Произошла ошибка'
|
||||||
setError(errorMsg)
|
setToastMessage({ text: errorMsg, type: 'error' })
|
||||||
window.history.replaceState({}, '', window.location.pathname)
|
window.history.replaceState({}, '', window.location.pathname)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -45,6 +49,7 @@ function TodoistIntegration({ onNavigate }) {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error checking status:', error)
|
console.error('Error checking status:', error)
|
||||||
setError(error.message || 'Не удалось проверить статус')
|
setError(error.message || 'Не удалось проверить статус')
|
||||||
|
setIsLoadingError(true)
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
}
|
}
|
||||||
@@ -69,7 +74,7 @@ function TodoistIntegration({ onNavigate }) {
|
|||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error connecting Todoist:', error)
|
console.error('Error connecting Todoist:', error)
|
||||||
setError(error.message || 'Не удалось подключить Todoist')
|
setToastMessage({ text: error.message || 'Не удалось подключить Todoist', type: 'error' })
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -91,15 +96,26 @@ function TodoistIntegration({ onNavigate }) {
|
|||||||
}
|
}
|
||||||
setConnected(false)
|
setConnected(false)
|
||||||
setTodoistEmail('')
|
setTodoistEmail('')
|
||||||
setMessage('Todoist отключен')
|
setToastMessage({ text: 'Todoist отключен', type: 'success' })
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error disconnecting:', error)
|
console.error('Error disconnecting:', error)
|
||||||
setError(error.message || 'Не удалось отключить Todoist')
|
setToastMessage({ text: error.message || 'Не удалось отключить Todoist', type: 'error' })
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false)
|
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 (
|
return (
|
||||||
<div className="p-4 md:p-6">
|
<div className="p-4 md:p-6">
|
||||||
<button className="close-x-button" onClick={() => onNavigate?.('profile')} title="Закрыть">
|
<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>
|
<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 ? (
|
{loading ? (
|
||||||
<div className="fixed inset-0 flex justify-center items-center">
|
<div className="fixed inset-0 flex justify-center items-center">
|
||||||
<div className="flex flex-col items-center">
|
<div className="flex flex-col items-center">
|
||||||
@@ -192,6 +196,13 @@ function TodoistIntegration({ onNavigate }) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
{toastMessage && (
|
||||||
|
<Toast
|
||||||
|
message={toastMessage.text}
|
||||||
|
type={toastMessage.type}
|
||||||
|
onClose={() => setToastMessage(null)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import React, { useState, useEffect } from 'react'
|
import React, { useState, useEffect } from 'react'
|
||||||
import { useAuth } from './auth/AuthContext'
|
import { useAuth } from './auth/AuthContext'
|
||||||
|
import LoadingError from './LoadingError'
|
||||||
import './WordList.css'
|
import './WordList.css'
|
||||||
|
|
||||||
const API_URL = '/api'
|
const API_URL = '/api'
|
||||||
@@ -176,7 +177,11 @@ function WordList({ onNavigate, dictionaryId, isNewDictionary, refreshTrigger =
|
|||||||
if (error) {
|
if (error) {
|
||||||
return (
|
return (
|
||||||
<div className="word-list">
|
<div className="word-list">
|
||||||
<div className="error-message">{error}</div>
|
<LoadingError onRetry={() => {
|
||||||
|
if (hasValidDictionary(currentDictionaryId)) {
|
||||||
|
fetchWordsForDictionary(currentDictionaryId)
|
||||||
|
}
|
||||||
|
}} />
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user