Files
play-life/play-life-web/src/App.jsx
poignatov 932dba8682
All checks were successful
Build and Push Docker Image / build-and-push (push) Successful in 34s
Унификация отображения ошибок: LoadingError для загрузки, Toast для действий
2026-01-11 15:51:28 +03:00

979 lines
40 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useState, useEffect, useCallback, useRef } from 'react'
import CurrentWeek from './components/CurrentWeek'
import FullStatistics from './components/FullStatistics'
import ProjectPriorityManager from './components/ProjectPriorityManager'
import WordList from './components/WordList'
import AddWords from './components/AddWords'
import TestConfigSelection from './components/TestConfigSelection'
import AddConfig from './components/AddConfig'
import TestWords from './components/TestWords'
import Profile from './components/Profile'
import TaskList from './components/TaskList'
import TaskForm from './components/TaskForm.jsx'
import TodoistIntegration from './components/TodoistIntegration'
import TelegramIntegration from './components/TelegramIntegration'
import { AuthProvider, useAuth } from './components/auth/AuthContext'
import AuthScreen from './components/auth/AuthScreen'
import PWAUpdatePrompt from './components/PWAUpdatePrompt'
// API endpoints (используем относительные пути, проксирование настроено в nginx/vite)
const CURRENT_WEEK_API_URL = '/playlife-feed'
const FULL_STATISTICS_API_URL = '/d2dc349a-0d13-49b2-a8f0-1ab094bfba9b'
// Определяем основные табы (без крестика) и глубокие табы (с крестиком)
const mainTabs = ['current', 'test-config', 'tasks', 'profile']
const deepTabs = ['add-words', 'add-config', 'test', 'task-form', 'words', 'todoist-integration', 'telegram-integration', 'full', 'priorities']
function AppContent() {
const { authFetch, isAuthenticated, loading: authLoading } = useAuth()
// Show loading while checking auth
if (authLoading) {
return (
<div className="min-h-screen flex items-center justify-center bg-gradient-to-br from-slate-900 via-purple-900 to-slate-900">
<div className="text-white text-xl">Загрузка...</div>
</div>
)
}
// Show auth screen if not authenticated
if (!isAuthenticated) {
return <AuthScreen />
}
const [activeTab, setActiveTab] = useState('current')
const [selectedProject, setSelectedProject] = useState(null)
const [loadedTabs, setLoadedTabs] = useState({
current: false,
priorities: false,
full: false,
words: false,
'add-words': false,
'test-config': false,
'add-config': false,
test: false,
tasks: false,
'task-form': false,
profile: false,
'todoist-integration': false,
'telegram-integration': false,
})
// Отслеживаем, какие табы уже были загружены (для предотвращения повторных загрузок)
const [tabsInitialized, setTabsInitialized] = useState({
current: false,
priorities: false,
full: false,
words: false,
'add-words': false,
'test-config': false,
'add-config': false,
test: false,
tasks: false,
'task-form': false,
profile: false,
'todoist-integration': false,
'telegram-integration': false,
})
// Параметры для навигации между вкладками
const [tabParams, setTabParams] = useState({})
// Кеширование данных
const [currentWeekData, setCurrentWeekData] = useState(null)
const [fullStatisticsData, setFullStatisticsData] = useState(null)
const [tasksData, setTasksData] = useState(null)
// Состояния загрузки для каждого таба (показываются только при первой загрузке)
const [currentWeekLoading, setCurrentWeekLoading] = useState(false)
const [fullStatisticsLoading, setFullStatisticsLoading] = useState(false)
const [prioritiesLoading, setPrioritiesLoading] = useState(false)
const [tasksLoading, setTasksLoading] = useState(false)
// Состояния фоновой загрузки (не показываются визуально)
const [currentWeekBackgroundLoading, setCurrentWeekBackgroundLoading] = useState(false)
const [fullStatisticsBackgroundLoading, setFullStatisticsBackgroundLoading] = useState(false)
const [prioritiesBackgroundLoading, setPrioritiesBackgroundLoading] = useState(false)
const [tasksBackgroundLoading, setTasksBackgroundLoading] = useState(false)
// Ошибки
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)
const [prioritiesRefreshTrigger, setPrioritiesRefreshTrigger] = useState(0)
const [testConfigRefreshTrigger, setTestConfigRefreshTrigger] = useState(0)
const [wordsRefreshTrigger, setWordsRefreshTrigger] = useState(0)
// Восстанавливаем последний выбранный таб после перезагрузки
const [isInitialized, setIsInitialized] = useState(false)
// Инициализация из URL (только для глубоких табов) или localStorage
useEffect(() => {
if (isInitialized) return
try {
// Проверяем URL только для глубоких табов
const urlParams = new URLSearchParams(window.location.search)
const tabFromUrl = urlParams.get('tab')
const validTabs = ['current', 'priorities', 'full', 'words', 'add-words', 'test-config', 'add-config', 'test', 'tasks', 'task-form', 'profile', 'todoist-integration', 'telegram-integration']
if (tabFromUrl && validTabs.includes(tabFromUrl) && deepTabs.includes(tabFromUrl)) {
// Если в URL есть глубокий таб, восстанавливаем его
setActiveTab(tabFromUrl)
setLoadedTabs(prev => ({ ...prev, [tabFromUrl]: true }))
// Восстанавливаем параметры из URL
const params = {}
urlParams.forEach((value, key) => {
if (key !== 'tab') {
try {
params[key] = JSON.parse(value)
} catch {
params[key] = value
}
}
})
if (Object.keys(params).length > 0) {
setTabParams(params)
// Если это экран full с selectedProject, восстанавливаем его
if (tabFromUrl === 'full' && params.selectedProject) {
setSelectedProject(params.selectedProject)
}
}
} else {
// Если в URL нет глубокого таба, проверяем localStorage для основного таба
const savedTab = window.localStorage?.getItem('activeTab')
if (savedTab && validTabs.includes(savedTab)) {
setActiveTab(savedTab)
setLoadedTabs(prev => ({ ...prev, [savedTab]: true }))
}
// Очищаем URL от параметров таба, если это основной таб
if (tabFromUrl && mainTabs.includes(tabFromUrl)) {
const url = new URL(window.location)
url.searchParams.delete('tab')
url.searchParams.forEach((value, key) => {
url.searchParams.delete(key)
})
window.history.replaceState({}, '', url)
}
}
setIsInitialized(true)
} catch (err) {
console.warn('Не удалось прочитать активный таб', err)
setIsInitialized(true)
}
}, [isInitialized])
const markTabAsLoaded = useCallback((tab) => {
setLoadedTabs(prev => (prev[tab] ? prev : { ...prev, [tab]: true }))
}, [])
// Функция для обновления URL (только для глубоких табов)
const updateUrl = useCallback((tab, params = {}, previousTab = null) => {
if (!deepTabs.includes(tab)) {
// Для основных табов не обновляем URL
return
}
const url = new URL(window.location)
url.searchParams.set('tab', tab)
// Удаляем старые параметры таба
const keysToRemove = []
url.searchParams.forEach((value, key) => {
if (key !== 'tab') {
keysToRemove.push(key)
}
})
keysToRemove.forEach(key => url.searchParams.delete(key))
// Добавляем новые параметры
Object.entries(params).forEach(([key, value]) => {
if (value !== undefined && value !== null) {
url.searchParams.set(key, typeof value === 'object' ? JSON.stringify(value) : value)
}
})
// Сохраняем предыдущий таб в state для восстановления при "Назад"
window.history.pushState({ tab, params, previousTab }, '', url)
}, []) // deepTabs - константа, не нужно в зависимостях
// Функция для очистки URL (при возврате к основному табу)
const clearUrl = useCallback((tab = null, usePushState = false) => {
const url = new URL(window.location)
const hasTabParam = url.searchParams.has('tab')
if (hasTabParam) {
url.searchParams.delete('tab')
url.searchParams.forEach((value, key) => {
url.searchParams.delete(key)
})
// Сохраняем текущий таб в state для восстановления при "Назад"
if (usePushState && tab) {
window.history.pushState({ tab }, '', url)
} else {
window.history.replaceState(tab ? { tab } : {}, '', url)
}
} else if (tab) {
// Если URL уже чистый, но нужно сохранить state таба
if (usePushState) {
window.history.pushState({ tab }, '', url)
} else {
window.history.replaceState({ tab }, '', url)
}
}
}, [])
// Функция для обновления URL без создания новой записи в истории (для обновления параметров того же таба)
const replaceUrl = useCallback((tab, params = {}) => {
if (!deepTabs.includes(tab)) {
return
}
const url = new URL(window.location)
url.searchParams.set('tab', tab)
// Удаляем старые параметры таба
const keysToRemove = []
url.searchParams.forEach((value, key) => {
if (key !== 'tab') {
keysToRemove.push(key)
}
})
keysToRemove.forEach(key => url.searchParams.delete(key))
// Добавляем новые параметры
Object.entries(params).forEach(([key, value]) => {
if (value !== undefined && value !== null) {
url.searchParams.set(key, typeof value === 'object' ? JSON.stringify(value) : value)
}
})
// Сохраняем текущий state, чтобы не потерять previousTab
const currentState = window.history.state || {}
window.history.replaceState({ ...currentState, tab, params }, '', url)
}, [])
const fetchCurrentWeekData = useCallback(async (isBackground = false) => {
try {
if (isBackground) {
setCurrentWeekBackgroundLoading(true)
} else {
setCurrentWeekLoading(true)
}
setCurrentWeekError(null)
console.log('Fetching current week data from:', CURRENT_WEEK_API_URL)
const response = await authFetch(CURRENT_WEEK_API_URL)
if (!response.ok) {
throw new Error('Ошибка загрузки данных')
}
const jsonData = await response.json()
// Обрабатываем ответ: приходит массив с одним объектом [{total: ..., projects: [...]}]
let projects = []
let total = null
if (Array.isArray(jsonData) && jsonData.length > 0) {
// Если ответ - массив, проверяем первый элемент
const firstItem = jsonData[0]
if (firstItem && typeof firstItem === 'object') {
// Если первый элемент - объект с полями total и projects
if (firstItem.projects && Array.isArray(firstItem.projects)) {
projects = firstItem.projects
total = firstItem.total !== undefined ? firstItem.total : null
} else {
// Если это просто массив проектов
projects = jsonData
}
} else {
// Если это массив проектов напрямую
projects = jsonData
}
} else if (jsonData && typeof jsonData === 'object' && !Array.isArray(jsonData)) {
// Если ответ - объект напрямую
projects = jsonData.projects || jsonData.data || []
total = jsonData.total !== undefined ? jsonData.total : null
}
setCurrentWeekData({
projects: Array.isArray(projects) ? projects : [],
total: total
})
} catch (err) {
setCurrentWeekError(err.message)
console.error('Ошибка загрузки данных текущей недели:', err)
} finally {
if (isBackground) {
setCurrentWeekBackgroundLoading(false)
} else {
setCurrentWeekLoading(false)
}
}
}, [authFetch])
const fetchFullStatisticsData = useCallback(async (isBackground = false) => {
try {
if (isBackground) {
setFullStatisticsBackgroundLoading(true)
} else {
setFullStatisticsLoading(true)
}
setFullStatisticsError(null)
const response = await authFetch(FULL_STATISTICS_API_URL)
if (!response.ok) {
throw new Error('Ошибка загрузки данных')
}
const jsonData = await response.json()
setFullStatisticsData(jsonData)
} catch (err) {
setFullStatisticsError(err.message)
console.error('Ошибка загрузки данных полной статистики:', err)
} finally {
if (isBackground) {
setFullStatisticsBackgroundLoading(false)
} else {
setFullStatisticsLoading(false)
}
}
}, [authFetch])
const fetchTasksData = useCallback(async (isBackground = false) => {
try {
if (isBackground) {
setTasksBackgroundLoading(true)
} else {
setTasksLoading(true)
}
setTasksError(null)
const response = await authFetch('/api/tasks')
if (!response.ok) {
throw new Error('Ошибка загрузки данных')
}
const jsonData = await response.json()
setTasksData(jsonData)
} catch (err) {
console.error('Ошибка загрузки списка задач:', err)
setTasksError(err.message || 'Ошибка загрузки данных')
} finally {
if (isBackground) {
setTasksBackgroundLoading(false)
} else {
setTasksLoading(false)
}
}
}, [authFetch])
// Используем ref для отслеживания инициализации табов (чтобы избежать лишних пересозданий функции)
const tabsInitializedRef = useRef({
current: false,
priorities: false,
full: false,
words: false,
'add-words': false,
'test-config': false,
'add-config': false,
test: false,
tasks: false,
'task-form': false,
profile: false,
'todoist-integration': false,
'telegram-integration': false,
})
// Используем ref для отслеживания кеша (чтобы не зависеть от состояния в useCallback)
const cacheRef = useRef({
current: null,
full: null,
tasks: null,
})
// Обновляем ref при изменении данных
useEffect(() => {
cacheRef.current.current = currentWeekData
}, [currentWeekData])
useEffect(() => {
cacheRef.current.full = fullStatisticsData
}, [fullStatisticsData])
useEffect(() => {
cacheRef.current.tasks = tasksData
}, [tasksData])
// Функция для загрузки данных таба
const loadTabData = useCallback((tab, isBackground = false) => {
if (tab === 'current') {
const hasCache = cacheRef.current.current !== null
const isInitialized = tabsInitializedRef.current.current
if (!isInitialized) {
// Первая загрузка таба - загружаем с индикатором
fetchCurrentWeekData(false)
tabsInitializedRef.current.current = true
setTabsInitialized(prev => ({ ...prev, current: true }))
} else if (hasCache && isBackground) {
// Возврат на таб с кешем - фоновая загрузка
fetchCurrentWeekData(true)
}
// Если нет кеша и это не первая загрузка - ничего не делаем (данные уже загружаются)
} else if (tab === 'full') {
const hasCache = cacheRef.current.full !== null
const isInitialized = tabsInitializedRef.current.full
if (!isInitialized) {
// Первая загрузка таба - загружаем с индикатором
fetchFullStatisticsData(false)
tabsInitializedRef.current.full = true
setTabsInitialized(prev => ({ ...prev, full: true }))
} else if (hasCache && isBackground) {
// Возврат на таб с кешем - фоновая загрузка
fetchFullStatisticsData(true)
}
} else if (tab === 'priorities') {
const isInitialized = tabsInitializedRef.current.priorities
if (!isInitialized) {
// Первая загрузка таба
setPrioritiesRefreshTrigger(prev => prev + 1)
tabsInitializedRef.current.priorities = true
setTabsInitialized(prev => ({ ...prev, priorities: true }))
} else if (isBackground) {
// Возврат на таб - фоновая загрузка
setPrioritiesRefreshTrigger(prev => prev + 1)
}
} else if (tab === 'test-config') {
const isInitialized = tabsInitializedRef.current['test-config']
if (!isInitialized) {
// Первая загрузка таба
setTestConfigRefreshTrigger(prev => prev + 1)
tabsInitializedRef.current['test-config'] = true
setTabsInitialized(prev => ({ ...prev, 'test-config': true }))
} else if (isBackground) {
// Возврат на таб - фоновая загрузка
setTestConfigRefreshTrigger(prev => prev + 1)
}
} else if (tab === 'tasks') {
const hasCache = cacheRef.current.tasks !== null
const isInitialized = tabsInitializedRef.current.tasks
if (!isInitialized) {
// Первая загрузка таба - загружаем с индикатором
fetchTasksData(false)
tabsInitializedRef.current.tasks = true
setTabsInitialized(prev => ({ ...prev, tasks: true }))
} else if (hasCache && isBackground) {
// Возврат на таб с кешем - фоновая загрузка
fetchTasksData(true)
}
}
}, [fetchCurrentWeekData, fetchFullStatisticsData, fetchTasksData])
// Функция для обновления всех данных (для кнопки Refresh, если она есть)
const refreshAllData = useCallback(async () => {
setIsRefreshing(true)
setPrioritiesError(null)
setCurrentWeekError(null)
setFullStatisticsError(null)
// Триггерим обновление приоритетов
setPrioritiesRefreshTrigger(prev => prev + 1)
// Загружаем все данные параллельно (не фоново)
await Promise.all([
fetchCurrentWeekData(false),
fetchFullStatisticsData(false),
])
setIsRefreshing(false)
}, [fetchCurrentWeekData, fetchFullStatisticsData])
// Обработчик кнопки "назад" в браузере (только для глубоких табов)
useEffect(() => {
const handlePopState = (event) => {
const validTabs = ['current', 'priorities', 'full', 'words', 'add-words', 'test-config', 'add-config', 'test', 'tasks', 'task-form', 'profile', 'todoist-integration', 'telegram-integration']
// Проверяем state текущей записи истории (куда мы вернулись)
if (event.state && event.state.tab) {
const { tab, params = {} } = event.state
if (validTabs.includes(tab)) {
setActiveTab(tab)
setTabParams(params)
markTabAsLoaded(tab)
// Если это экран full с selectedProject, восстанавливаем его
if (tab === 'full' && params.selectedProject) {
setSelectedProject(params.selectedProject)
} else if (tab === 'full') {
setSelectedProject(null)
}
return
}
}
// Если state пустой или не содержит таб, пытаемся восстановить из URL
const urlParams = new URLSearchParams(window.location.search)
const tabFromUrl = urlParams.get('tab')
if (tabFromUrl && validTabs.includes(tabFromUrl) && deepTabs.includes(tabFromUrl)) {
// Если в URL есть глубокий таб, восстанавливаем его
setActiveTab(tabFromUrl)
markTabAsLoaded(tabFromUrl)
const params = {}
urlParams.forEach((value, key) => {
if (key !== 'tab') {
try {
params[key] = JSON.parse(value)
} catch {
params[key] = value
}
}
})
setTabParams(params)
// Если это экран full с selectedProject, восстанавливаем его
if (tabFromUrl === 'full' && params.selectedProject) {
setSelectedProject(params.selectedProject)
}
} else {
// Если в URL нет глубокого таба, значит мы вернулись на основной таб
// Проверяем state - если там есть tab, используем его
if (event.state && event.state.tab && validTabs.includes(event.state.tab)) {
setActiveTab(event.state.tab)
setTabParams({})
markTabAsLoaded(event.state.tab)
setSelectedProject(null)
clearUrl(event.state.tab)
} else {
// Если state пустой, используем сохраненный таб из localStorage
const savedTab = window.localStorage?.getItem('activeTab')
const validMainTab = savedTab && validTabs.includes(savedTab) ? savedTab : 'current'
setActiveTab(validMainTab)
setTabParams({})
markTabAsLoaded(validMainTab)
setSelectedProject(null)
clearUrl(validMainTab)
}
}
}
window.addEventListener('popstate', handlePopState)
return () => {
window.removeEventListener('popstate', handlePopState)
}
}, [markTabAsLoaded, clearUrl]) // mainTabs и deepTabs - константы, не нужно в зависимостях
// Обновляем данные при возвращении экрана в фокус (фоново)
useEffect(() => {
const handleFocus = () => {
if (document.visibilityState === 'visible') {
// Загружаем данные активного таба фоново
loadTabData(activeTab, true)
}
}
window.addEventListener('focus', handleFocus)
document.addEventListener('visibilitychange', handleFocus)
return () => {
window.removeEventListener('focus', handleFocus)
document.removeEventListener('visibilitychange', handleFocus)
}
}, [activeTab, loadTabData])
const handleProjectClick = (projectName) => {
setSelectedProject(projectName)
markTabAsLoaded('full')
setTabParams({ selectedProject: projectName })
updateUrl('full', { selectedProject: projectName }, activeTab)
setActiveTab('full')
}
const handleTabChange = (tab, params = {}) => {
if (tab === 'full' && activeTab === 'full') {
// При повторном клике на "Полная статистика" сбрасываем выбранный проект
setSelectedProject(null)
setTabParams({})
updateUrl('full', {}, activeTab)
} else if (tab !== activeTab || tab === 'task-form') {
// Для task-form всегда обновляем параметры, даже если это тот же таб
markTabAsLoaded(tab)
// Определяем, является ли текущий таб глубоким
const isCurrentTabDeep = deepTabs.includes(activeTab)
const isNewTabDeep = deepTabs.includes(tab)
const isCurrentTabMain = mainTabs.includes(activeTab)
const isNewTabMain = mainTabs.includes(tab)
// Сбрасываем tabParams при переходе с add-config на другой таб
if (activeTab === 'add-config' && tab !== 'add-config') {
setTabParams({})
if (isNewTabMain) {
clearUrl()
} else if (isNewTabDeep) {
updateUrl(tab, {}, activeTab)
}
} else {
// Для task-form явно удаляем taskId, если он undefined
if (tab === 'task-form' && params.taskId === undefined) {
setTabParams({})
if (isNewTabMain) {
clearUrl()
} else if (isNewTabDeep) {
updateUrl(tab, {}, activeTab)
}
} else {
setTabParams(params)
// Обновляем URL только для глубоких табов
if (isNewTabDeep) {
// Сохраняем текущий таб как предыдущий при переходе на глубокий таб
updateUrl(tab, params, activeTab)
} else if (isNewTabMain && isCurrentTabDeep) {
// При переходе с глубокого таба на основной - очищаем URL и сохраняем таб в state
clearUrl(tab)
} else if (isNewTabMain && isCurrentTabMain) {
// При переходе между основными табами - сохраняем таб в state без изменения URL, НЕ создаем новую запись в истории
clearUrl(tab, false)
}
}
}
setActiveTab(tab)
if (tab === 'current') {
setSelectedProject(null)
}
// Обновляем список слов при возврате из экрана добавления слов
if (activeTab === 'add-words' && tab === 'words') {
setWordsRefreshTrigger(prev => prev + 1)
}
// Обновляем список задач при возврате из экрана редактирования
// Используем фоновую загрузку, чтобы не показывать индикатор загрузки
if (activeTab === 'task-form' && tab === 'tasks') {
fetchTasksData(true)
}
// Загрузка данных произойдет в useEffect при изменении activeTab
}
}
// Обработчик навигации для компонентов
const handleNavigate = (tab, params = {}) => {
handleTabChange(tab, params)
}
// Загружаем данные при открытии таба (когда таб становится активным)
const prevActiveTabRef = useRef(null)
const lastLoadedTabRef = useRef(null) // Отслеживаем последний загруженный таб, чтобы избежать двойной загрузки
useEffect(() => {
if (!activeTab || !loadedTabs[activeTab]) return
const isFirstLoad = !tabsInitializedRef.current[activeTab]
const isReturningToTab = prevActiveTabRef.current !== null && prevActiveTabRef.current !== activeTab
// Проверяем, не загружали ли мы уже этот таб в этом рендере
const tabKey = `${activeTab}-${isFirstLoad ? 'first' : 'return'}`
if (lastLoadedTabRef.current === tabKey) {
return // Уже загружали
}
if (isFirstLoad) {
// Первая загрузка таба
lastLoadedTabRef.current = tabKey
loadTabData(activeTab, false)
} else if (isReturningToTab) {
// Возврат на таб - фоновая загрузка
lastLoadedTabRef.current = tabKey
loadTabData(activeTab, true)
}
prevActiveTabRef.current = activeTab
}, [activeTab, loadedTabs, loadTabData])
// Определяем общее состояние загрузки и ошибок для кнопки Refresh
const isAnyLoading = currentWeekLoading || fullStatisticsLoading || prioritiesLoading || isRefreshing
const hasAnyError = currentWeekError || fullStatisticsError || prioritiesError
// Сохраняем выбранный таб, чтобы восстановить его после перезагрузки
useEffect(() => {
try {
window.localStorage?.setItem('activeTab', activeTab)
} catch (err) {
console.warn('Не удалось сохранить активный таб в localStorage', err)
}
}, [activeTab])
// Определяем, нужно ли скрывать нижнюю панель (для fullscreen экранов)
const isFullscreenTab = activeTab === 'test' || activeTab === 'add-words' || activeTab === 'add-config' || activeTab === 'task-form' || activeTab === 'todoist-integration' || activeTab === 'telegram-integration' || activeTab === 'full' || activeTab === 'priorities'
// Определяем отступы для контейнера
const getContainerPadding = () => {
if (!isFullscreenTab) {
// Для tasks и profile на широких экранах увеличиваем отступ
if (activeTab === 'tasks' || activeTab === 'profile') {
return 'p-4 md:p-8'
}
return 'p-4 md:p-6'
}
// Для экрана статистики используем такие же отступы как для приоритетов
if (activeTab === 'full') {
return 'px-4 md:px-8 py-0'
}
// Для экрана приоритетов используем такие же отступы как для profile
if (activeTab === 'priorities') {
return 'px-4 md:px-8 py-0'
}
// Для остальных fullscreen экранов без отступов
return 'p-0'
}
return (
<div className="flex flex-col min-h-screen min-h-dvh">
<div className={`flex-1 ${isFullscreenTab ? 'pb-0' : 'pb-20'}`}>
<div className={`max-w-7xl mx-auto ${getContainerPadding()}`}>
{loadedTabs.current && (
<div className={activeTab === 'current' ? 'block' : 'hidden'}>
<CurrentWeek
onProjectClick={handleProjectClick}
data={currentWeekData}
loading={currentWeekLoading}
error={currentWeekError}
onRetry={fetchCurrentWeekData}
allProjectsData={fullStatisticsData}
onNavigate={handleNavigate}
/>
</div>
)}
{loadedTabs.priorities && (
<div className={activeTab === 'priorities' ? 'block' : 'hidden'}>
<ProjectPriorityManager
allProjectsData={fullStatisticsData}
currentWeekData={currentWeekData}
shouldLoad={activeTab === 'priorities' && loadedTabs.priorities}
onLoadingChange={setPrioritiesLoading}
onErrorChange={setPrioritiesError}
refreshTrigger={prioritiesRefreshTrigger}
onNavigate={handleNavigate}
/>
</div>
)}
{loadedTabs.full && (
<div className={activeTab === 'full' ? 'block' : 'hidden'}>
<FullStatistics
selectedProject={selectedProject}
onClearSelection={() => {
setSelectedProject(null)
setTabParams({})
replaceUrl('full', {})
}}
data={fullStatisticsData}
loading={fullStatisticsLoading}
error={fullStatisticsError}
onRetry={fetchFullStatisticsData}
currentWeekData={currentWeekData}
onNavigate={handleNavigate}
/>
</div>
)}
{loadedTabs.words && (
<div className={activeTab === 'words' ? 'block' : 'hidden'}>
<WordList
onNavigate={handleNavigate}
dictionaryId={tabParams.dictionaryId}
isNewDictionary={tabParams.isNewDictionary}
refreshTrigger={wordsRefreshTrigger}
/>
</div>
)}
{loadedTabs['add-words'] && (
<div className={activeTab === 'add-words' ? 'block' : 'hidden'}>
<AddWords
onNavigate={handleNavigate}
dictionaryId={tabParams.dictionaryId}
dictionaryName={tabParams.dictionaryName}
/>
</div>
)}
{loadedTabs['test-config'] && (
<div className={activeTab === 'test-config' ? 'block' : 'hidden'}>
<TestConfigSelection
onNavigate={handleNavigate}
refreshTrigger={testConfigRefreshTrigger}
/>
</div>
)}
{loadedTabs['add-config'] && (
<div className={activeTab === 'add-config' ? 'block' : 'hidden'}>
<AddConfig
key={tabParams.config?.id || 'new'}
onNavigate={handleNavigate}
editingConfig={tabParams.config}
/>
</div>
)}
{loadedTabs.test && (
<div className={activeTab === 'test' ? 'block' : 'hidden'}>
<TestWords
onNavigate={handleNavigate}
wordCount={tabParams.wordCount}
configId={tabParams.configId}
maxCards={tabParams.maxCards}
/>
</div>
)}
{loadedTabs.tasks && (
<div className={activeTab === 'tasks' ? 'block' : 'hidden'}>
<TaskList
onNavigate={handleNavigate}
data={tasksData}
loading={tasksLoading}
backgroundLoading={tasksBackgroundLoading}
error={tasksError}
onRetry={() => fetchTasksData(false)}
onRefresh={(isBackground = false) => fetchTasksData(isBackground)}
/>
</div>
)}
{loadedTabs['task-form'] && (
<div className={activeTab === 'task-form' ? 'block' : 'hidden'}>
<TaskForm
key={tabParams.taskId || 'new'}
onNavigate={handleNavigate}
taskId={tabParams.taskId}
/>
</div>
)}
{loadedTabs.profile && (
<div className={activeTab === 'profile' ? 'block' : 'hidden'}>
<Profile onNavigate={handleNavigate} />
</div>
)}
{loadedTabs['todoist-integration'] && (
<div className={activeTab === 'todoist-integration' ? 'block' : 'hidden'}>
<TodoistIntegration onNavigate={handleNavigate} />
</div>
)}
{loadedTabs['telegram-integration'] && (
<div className={activeTab === 'telegram-integration' ? 'block' : 'hidden'}>
<TelegramIntegration onNavigate={handleNavigate} />
</div>
)}
</div>
</div>
{!isFullscreenTab && (
<div className="fixed bottom-0 left-0 right-0 flex bg-white/90 backdrop-blur-md border-t border-white/20 flex-shrink-0 overflow-x-auto shadow-lg z-10 justify-center items-center w-full" style={{ paddingBottom: 'env(safe-area-inset-bottom)' }}>
<div className="flex">
<button
onClick={() => handleTabChange('current')}
className={`min-w-max whitespace-nowrap px-4 py-4 transition-all duration-300 relative ${
activeTab === 'current'
? 'text-indigo-700 bg-white/50'
: 'text-gray-600 hover:text-indigo-600 hover:bg-white/30'
}`}
title="Неделя"
>
<span className="relative z-10 flex items-center justify-center">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<rect x="3" y="4" width="18" height="18" rx="2" ry="2"></rect>
<line x1="16" y1="2" x2="16" y2="6"></line>
<line x1="8" y1="2" x2="8" y2="6"></line>
<line x1="3" y1="10" x2="21" y2="10"></line>
</svg>
</span>
{activeTab === 'current' && (
<div className="absolute top-0 left-0 right-0 h-1 bg-gradient-to-r from-indigo-500 to-purple-500"></div>
)}
</button>
<button
onClick={() => handleTabChange('test-config')}
className={`min-w-max whitespace-nowrap px-4 py-4 transition-all duration-300 relative ${
activeTab === 'test-config' || activeTab === 'test'
? 'text-indigo-700 bg-white/50'
: 'text-gray-600 hover:text-indigo-600 hover:bg-white/30'
}`}
title="Тест"
>
<span className="relative z-10 flex items-center justify-center">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20"></path>
<path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z"></path>
<path d="M8 7h6"></path>
<path d="M8 11h4"></path>
</svg>
</span>
{(activeTab === 'test-config' || activeTab === 'test') && (
<div className="absolute top-0 left-0 right-0 h-1 bg-gradient-to-r from-indigo-500 to-purple-500"></div>
)}
</button>
<button
onClick={() => handleTabChange('tasks')}
className={`min-w-max whitespace-nowrap px-4 py-4 transition-all duration-300 relative ${
activeTab === 'tasks' || activeTab === 'task-form'
? 'text-indigo-700 bg-white/50'
: 'text-gray-600 hover:text-indigo-600 hover:bg-white/30'
}`}
title="Задачи"
>
<span className="relative z-10 flex items-center justify-center">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="M9 11l3 3L22 4"></path>
<path d="M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11"></path>
</svg>
</span>
{(activeTab === 'tasks' || activeTab === 'task-form') && (
<div className="absolute top-0 left-0 right-0 h-1 bg-gradient-to-r from-indigo-500 to-purple-500"></div>
)}
</button>
<button
onClick={() => handleTabChange('profile')}
className={`min-w-max whitespace-nowrap px-4 py-4 transition-all duration-300 relative ${
activeTab === 'profile'
? 'text-indigo-700 bg-white/50'
: 'text-gray-600 hover:text-indigo-600 hover:bg-white/30'
}`}
title="Профиль"
>
<span className="relative z-10 flex items-center justify-center">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"></path>
<circle cx="12" cy="7" r="4"></circle>
</svg>
</span>
{activeTab === 'profile' && (
<div className="absolute top-0 left-0 right-0 h-1 bg-gradient-to-r from-indigo-500 to-purple-500"></div>
)}
</button>
</div>
</div>
)}
</div>
)
}
function App() {
return (
<AuthProvider>
<AppContent />
<PWAUpdatePrompt />
</AuthProvider>
)
}
export default App