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 DictionaryList from './components/DictionaryList' import TestWords from './components/TestWords' import Profile from './components/Profile' import TaskList from './components/TaskList' import TaskForm from './components/TaskForm.jsx' import Wishlist from './components/Wishlist' import WishlistForm from './components/WishlistForm' import WishlistDetail from './components/WishlistDetail' import BoardForm from './components/BoardForm' import BoardJoinPreview from './components/BoardJoinPreview' 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', 'tasks', 'wishlist', 'profile'] const deepTabs = ['add-words', 'test', 'task-form', 'wishlist-form', 'wishlist-detail', 'board-form', 'board-join', 'words', 'dictionaries', 'todoist-integration', 'telegram-integration', 'full', 'priorities'] function AppContent() { const { authFetch, isAuthenticated, loading: authLoading } = useAuth() // Show loading while checking auth if (authLoading) { return (
Загрузка...
) } // Show auth screen if not authenticated if (!isAuthenticated) { return } 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, dictionaries: false, test: false, tasks: false, 'task-form': false, wishlist: false, 'wishlist-form': false, 'wishlist-detail': false, 'board-form': false, 'board-join': false, profile: false, 'todoist-integration': false, 'telegram-integration': false, }) // Отслеживаем, какие табы уже были загружены (для предотвращения повторных загрузок) const [tabsInitialized, setTabsInitialized] = useState({ current: false, priorities: false, full: false, words: false, 'add-words': false, dictionaries: false, test: false, tasks: false, 'task-form': false, wishlist: false, 'wishlist-form': false, 'wishlist-detail': false, 'board-form': false, 'board-join': 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 [dictionariesRefreshTrigger, setDictionariesRefreshTrigger] = useState(0) const [wordsRefreshTrigger, setWordsRefreshTrigger] = useState(0) const [wishlistRefreshTrigger, setWishlistRefreshTrigger] = useState(0) // Восстанавливаем последний выбранный таб после перезагрузки const [isInitialized, setIsInitialized] = useState(false) // Инициализация из URL (только для глубоких табов) или localStorage useEffect(() => { if (isInitialized) return try { // Проверяем путь /invite/:token для присоединения к доске const path = window.location.pathname if (path.startsWith('/invite/')) { const token = path.replace('/invite/', '') if (token) { setActiveTab('board-join') setLoadedTabs(prev => ({ ...prev, 'board-join': true })) setTabParams({ inviteToken: token }) setIsInitialized(true) // Очищаем путь, оставляем только параметры window.history.replaceState({}, '', '/?tab=board-join&inviteToken=' + token) return } } // Проверяем URL только для глубоких табов const urlParams = new URLSearchParams(window.location.search) const tabFromUrl = urlParams.get('tab') const validTabs = ['current', 'priorities', 'full', 'words', 'add-words', 'dictionaries', 'test', 'tasks', 'task-form', 'wishlist', 'wishlist-form', 'wishlist-detail', 'board-form', 'board-join', '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) 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, dictionaries: 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 === 'dictionaries') { const isInitialized = tabsInitializedRef.current['dictionaries'] if (!isInitialized) { // Первая загрузка таба setDictionariesRefreshTrigger(prev => prev + 1) tabsInitializedRef.current['dictionaries'] = true setTabsInitialized(prev => ({ ...prev, 'dictionaries': true })) } else if (isBackground) { // Возврат на таб - фоновая загрузка setDictionariesRefreshTrigger(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', 'dictionaries', 'test', 'tasks', 'task-form', 'wishlist', 'wishlist-form', 'wishlist-detail', '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' || tab === 'wishlist-form') { // Для task-form и wishlist-form всегда обновляем параметры, даже если это тот же таб markTabAsLoaded(tab) // Определяем, является ли текущий таб глубоким const isCurrentTabDeep = deepTabs.includes(activeTab) const isNewTabDeep = deepTabs.includes(tab) const isCurrentTabMain = mainTabs.includes(activeTab) const isNewTabMain = mainTabs.includes(tab) { // Для task-form и wishlist-form явно удаляем параметры, только если нет никаких параметров // task-form может иметь taskId (редактирование), wishlistId (создание из желания), или returnTo (возврат после создания) const isTaskFormWithNoParams = tab === 'task-form' && params.taskId === undefined && params.wishlistId === undefined && params.returnTo === undefined const isWishlistFormWithNoParams = tab === 'wishlist-form' && params.wishlistId === undefined && params.newTaskId === undefined && params.boardId === undefined if (isTaskFormWithNoParams || isWishlistFormWithNoParams) { 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) } // Обновляем список желаний при возврате из экрана редактирования if (activeTab === 'wishlist-form' && tab === 'wishlist') { // Сохраняем boardId из параметров или текущих tabParams const savedBoardId = params.boardId || tabParams.boardId // Параметры уже установлены в строке 649, но мы можем их обновить, чтобы сохранить boardId if (savedBoardId) { setTabParams(prev => ({ ...prev, boardId: savedBoardId })) } setWishlistRefreshTrigger(prev => prev + 1) } // Загрузка данных произойдет в 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 === 'task-form' || activeTab === 'wishlist-form' || activeTab === 'wishlist-detail' || activeTab === 'todoist-integration' || activeTab === 'telegram-integration' || activeTab === 'full' || activeTab === 'priorities' || activeTab === 'dictionaries' // Определяем отступы для контейнера 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 (
{loadedTabs.current && (
)} {loadedTabs.priorities && (
)} {loadedTabs.full && (
{ setSelectedProject(null) setTabParams({}) replaceUrl('full', {}) }} data={fullStatisticsData} loading={fullStatisticsLoading} error={fullStatisticsError} onRetry={fetchFullStatisticsData} currentWeekData={currentWeekData} onNavigate={handleNavigate} />
)} {loadedTabs.words && (
)} {loadedTabs['add-words'] && (
)} {loadedTabs.dictionaries && (
)} {loadedTabs.test && (
)} {loadedTabs.tasks && (
fetchTasksData(false)} onRefresh={(isBackground = false) => fetchTasksData(isBackground)} />
)} {loadedTabs['task-form'] && (
)} {loadedTabs.wishlist && (
)} {loadedTabs['wishlist-form'] && (
)} {loadedTabs['wishlist-detail'] && (
setWishlistRefreshTrigger(prev => prev + 1)} />
)} {loadedTabs['board-form'] && (
setWishlistRefreshTrigger(prev => prev + 1)} />
)} {loadedTabs['board-join'] && (
)} {loadedTabs.profile && (
)} {loadedTabs['todoist-integration'] && (
)} {loadedTabs['telegram-integration'] && (
)}
{!isFullscreenTab && (
)}
) } function App() { return ( ) } export default App