4.13.6: Рефакторинг архитектуры табов
All checks were successful
Build and Push Docker Image / build-and-push (push) Successful in 1m17s

This commit is contained in:
poignatov
2026-02-04 14:35:47 +03:00
parent 794947ea89
commit 8023319ee4
3 changed files with 112 additions and 106 deletions

View File

@@ -1 +1 @@
4.13.5 4.13.6

View File

@@ -1,6 +1,6 @@
{ {
"name": "play-life-web", "name": "play-life-web",
"version": "4.13.5", "version": "4.13.6",
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",

View File

@@ -130,8 +130,6 @@ function AppContent() {
const [wordsRefreshTrigger, setWordsRefreshTrigger] = useState(0) const [wordsRefreshTrigger, setWordsRefreshTrigger] = useState(0)
const [wishlistRefreshTrigger, setWishlistRefreshTrigger] = useState(0) const [wishlistRefreshTrigger, setWishlistRefreshTrigger] = useState(0)
// Состояние для сохранения позиции скролла каждого таба
const [scrollPositions, setScrollPositions] = useState({})
// Восстанавливаем последний выбранный таб после перезагрузки // Восстанавливаем последний выбранный таб после перезагрузки
@@ -710,15 +708,6 @@ function AppContent() {
setTabParams({}) setTabParams({})
updateUrl('full', {}, activeTab) updateUrl('full', {}, activeTab)
} else if (tab !== activeTab || tab === 'task-form' || tab === 'wishlist-form' || (tab === 'words' && Object.keys(params).length > 0)) { } else if (tab !== activeTab || tab === 'task-form' || tab === 'wishlist-form' || (tab === 'words' && Object.keys(params).length > 0)) {
// Сохраняем позицию скролла для текущего таба перед переходом
const scrollContainer = document.querySelector('.flex-1.overflow-y-auto')
if (scrollContainer && mainTabs.includes(activeTab)) {
setScrollPositions(prev => ({
...prev,
[activeTab]: scrollContainer.scrollTop
}))
}
// Для task-form и wishlist-form всегда обновляем параметры, даже если это тот же таб // Для task-form и wishlist-form всегда обновляем параметры, даже если это тот же таб
markTabAsLoaded(tab) markTabAsLoaded(tab)
@@ -865,20 +854,6 @@ function AppContent() {
} }
}, [selectedProject, activeTab]) }, [selectedProject, activeTab])
// Восстанавливаем позицию скролла при возвращении на таб
useEffect(() => {
if (mainTabs.includes(activeTab) && scrollPositions[activeTab] !== undefined) {
// Используем setTimeout, чтобы DOM успел обновиться
const timeoutId = setTimeout(() => {
const scrollContainer = document.querySelector('.flex-1.overflow-y-auto')
if (scrollContainer) {
scrollContainer.scrollTop = scrollPositions[activeTab]
}
}, 0)
return () => clearTimeout(timeoutId)
}
}, [activeTab, scrollPositions])
// Определяем общее состояние загрузки и ошибок для кнопки Refresh // Определяем общее состояние загрузки и ошибок для кнопки Refresh
@@ -897,45 +872,47 @@ function AppContent() {
// Определяем, нужно ли скрывать нижнюю панель (для fullscreen экранов) // Определяем, нужно ли скрывать нижнюю панель (для 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 === 'words' || activeTab === 'dictionaries' 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 === 'words' || activeTab === 'dictionaries'
// Определяем отступы для контейнера // Функция для получения классов скролл-контейнера для каждого таба
const getContainerPadding = () => { // Каждый таб имеет свой изолированный скролл-контейнер для автоматического сохранения позиции скролла
if (!isFullscreenTab) { const getTabContainerClasses = (tabName) => {
// Для tasks, wishlist и profile на широких экранах увеличиваем отступ const isActive = activeTab === tabName
if (activeTab === 'tasks' || activeTab === 'wishlist' || activeTab === 'profile') { const baseClasses = 'absolute inset-0 overflow-y-auto'
return 'p-4 md:p-8' // Активный таб: z-10 (сверху), неактивные: z-0 + invisible + opacity-0 (мгновенное скрытие)
} const visibilityClasses = isActive ? 'z-10' : 'z-0 invisible opacity-0 pointer-events-none'
return 'p-4 md:p-6'
// Определяем padding для каждого таба
let paddingClasses = ''
if (tabName === 'current' || tabName === 'tasks' || tabName === 'wishlist' || tabName === 'profile') {
paddingClasses = 'pb-20'
} else if (tabName === 'words' || tabName === 'dictionaries') {
paddingClasses = 'pb-16'
} }
// Для экрана статистики используем такие же отступы как для приоритетов
if (activeTab === 'full') { return `${baseClasses} ${paddingClasses} ${visibilityClasses}`.trim()
return 'px-4 md:px-8 py-0' }
// Функция для определения отступов внутреннего контейнера
const getInnerContainerClasses = (tabName) => {
if (tabName === 'tasks' || tabName === 'wishlist' || tabName === 'profile') {
return 'max-w-7xl mx-auto p-4 md:p-8'
} }
// Для экрана приоритетов используем такие же отступы как для profile if (tabName === 'current') {
if (activeTab === 'priorities') { return 'max-w-7xl mx-auto p-4 md:p-6'
return 'px-4 md:px-8 py-0'
} }
// Для экрана словарей используем такие же отступы как для приоритетов if (tabName === 'full' || tabName === 'priorities' || tabName === 'dictionaries' || tabName === 'words') {
if (activeTab === 'dictionaries') { return 'max-w-7xl mx-auto px-4 md:px-8 py-0'
return 'px-4 md:px-8 py-0'
} }
// Для экрана списка слов используем такие же отступы как для словарей // Fullscreen табы без отступов
if (activeTab === 'words') { return 'max-w-7xl mx-auto p-0'
return 'px-4 md:px-8 py-0'
}
// Для экрана желаний используем такие же отступы как для словарей
if (activeTab === 'wishlist') {
return 'px-4 md:px-8 py-0'
}
// Для остальных fullscreen экранов без отступов
return 'p-0'
} }
return ( return (
<div className="flex flex-col h-screen h-dvh overflow-hidden"> <div className="flex flex-col h-screen h-dvh overflow-hidden">
<div className={`flex-1 overflow-y-auto ${isFullscreenTab ? (activeTab === 'words' || activeTab === 'dictionaries' ? 'pb-16' : 'pb-0') : activeTab === 'tasks' || activeTab === 'wishlist' ? 'pb-20' : 'pb-20'}`}> {/* Контейнер табов - каждый таб имеет свой изолированный скролл */}
<div className={`max-w-7xl mx-auto ${getContainerPadding()}`}> <div className="flex-1 relative overflow-hidden">
{loadedTabs.current && ( {loadedTabs.current && (
<div className={activeTab === 'current' ? 'block' : 'hidden'}> <div className={getTabContainerClasses('current')}>
<div className={getInnerContainerClasses('current')}>
<CurrentWeek <CurrentWeek
onProjectClick={handleProjectClick} onProjectClick={handleProjectClick}
data={currentWeekData} data={currentWeekData}
@@ -946,10 +923,12 @@ function AppContent() {
onNavigate={handleNavigate} onNavigate={handleNavigate}
/> />
</div> </div>
)} </div>
)}
{loadedTabs.priorities && ( {loadedTabs.priorities && (
<div className={activeTab === 'priorities' ? 'block' : 'hidden'}> <div className={getTabContainerClasses('priorities')}>
<div className={getInnerContainerClasses('priorities')}>
<ProjectPriorityManager <ProjectPriorityManager
allProjectsData={fullStatisticsData} allProjectsData={fullStatisticsData}
currentWeekData={currentWeekData} currentWeekData={currentWeekData}
@@ -960,10 +939,12 @@ function AppContent() {
onNavigate={handleNavigate} onNavigate={handleNavigate}
/> />
</div> </div>
)} </div>
)}
{loadedTabs.full && ( {loadedTabs.full && (
<div className={activeTab === 'full' ? 'block' : 'hidden'}> <div className={getTabContainerClasses('full')}>
<div className={getInnerContainerClasses('full')}>
<FullStatistics <FullStatistics
selectedProject={selectedProject} selectedProject={selectedProject}
onClearSelection={() => { onClearSelection={() => {
@@ -985,10 +966,12 @@ function AppContent() {
activeTab={activeTab} activeTab={activeTab}
/> />
</div> </div>
)} </div>
)}
{loadedTabs.words && ( {loadedTabs.words && (
<div className={activeTab === 'words' ? 'block' : 'hidden'}> <div className={getTabContainerClasses('words')}>
<div className={getInnerContainerClasses('words')}>
<WordList <WordList
onNavigate={handleNavigate} onNavigate={handleNavigate}
dictionaryId={tabParams.dictionaryId} dictionaryId={tabParams.dictionaryId}
@@ -996,29 +979,35 @@ function AppContent() {
refreshTrigger={wordsRefreshTrigger} refreshTrigger={wordsRefreshTrigger}
/> />
</div> </div>
)} </div>
)}
{loadedTabs['add-words'] && ( {loadedTabs['add-words'] && (
<div className={activeTab === 'add-words' ? 'block' : 'hidden'}> <div className={getTabContainerClasses('add-words')}>
<div className={getInnerContainerClasses('add-words')}>
<AddWords <AddWords
onNavigate={handleNavigate} onNavigate={handleNavigate}
dictionaryId={tabParams.dictionaryId} dictionaryId={tabParams.dictionaryId}
dictionaryName={tabParams.dictionaryName} dictionaryName={tabParams.dictionaryName}
/> />
</div> </div>
)} </div>
)}
{loadedTabs.dictionaries && ( {loadedTabs.dictionaries && (
<div className={activeTab === 'dictionaries' ? 'block' : 'hidden'}> <div className={getTabContainerClasses('dictionaries')}>
<div className={getInnerContainerClasses('dictionaries')}>
<DictionaryList <DictionaryList
onNavigate={handleNavigate} onNavigate={handleNavigate}
refreshTrigger={dictionariesRefreshTrigger} refreshTrigger={dictionariesRefreshTrigger}
/> />
</div> </div>
)} </div>
)}
{loadedTabs.test && ( {loadedTabs.test && (
<div className={activeTab === 'test' ? 'block' : 'hidden'}> <div className={getTabContainerClasses('test')}>
<div className={getInnerContainerClasses('test')}>
<TestWords <TestWords
onNavigate={handleNavigate} onNavigate={handleNavigate}
wordCount={tabParams.wordCount} wordCount={tabParams.wordCount}
@@ -1027,10 +1016,12 @@ function AppContent() {
taskId={tabParams.taskId} taskId={tabParams.taskId}
/> />
</div> </div>
)} </div>
)}
{loadedTabs.tasks && ( {loadedTabs.tasks && (
<div className={activeTab === 'tasks' ? 'block' : 'hidden'}> <div className={getTabContainerClasses('tasks')}>
<div className={getInnerContainerClasses('tasks')}>
<TaskList <TaskList
onNavigate={handleNavigate} onNavigate={handleNavigate}
data={tasksData} data={tasksData}
@@ -1041,10 +1032,12 @@ function AppContent() {
onRefresh={(isBackground = false) => fetchTasksData(isBackground)} onRefresh={(isBackground = false) => fetchTasksData(isBackground)}
/> />
</div> </div>
)} </div>
)}
{loadedTabs['task-form'] && ( {loadedTabs['task-form'] && (
<div className={activeTab === 'task-form' ? 'block' : 'hidden'}> <div className={getTabContainerClasses('task-form')}>
<div className={getInnerContainerClasses('task-form')}>
<TaskForm <TaskForm
key={tabParams.taskId || 'new'} key={tabParams.taskId || 'new'}
onNavigate={handleNavigate} onNavigate={handleNavigate}
@@ -1055,10 +1048,12 @@ function AppContent() {
returnWishlistId={tabParams.returnWishlistId} returnWishlistId={tabParams.returnWishlistId}
/> />
</div> </div>
)} </div>
)}
{loadedTabs.wishlist && ( {loadedTabs.wishlist && (
<div className={activeTab === 'wishlist' ? 'block' : 'hidden'}> <div className={getTabContainerClasses('wishlist')}>
<div className={getInnerContainerClasses('wishlist')}>
<Wishlist <Wishlist
onNavigate={handleNavigate} onNavigate={handleNavigate}
refreshTrigger={wishlistRefreshTrigger} refreshTrigger={wishlistRefreshTrigger}
@@ -1067,10 +1062,12 @@ function AppContent() {
boardDeleted={tabParams.boardDeleted} boardDeleted={tabParams.boardDeleted}
/> />
</div> </div>
)} </div>
)}
{loadedTabs['wishlist-form'] && ( {loadedTabs['wishlist-form'] && (
<div className={activeTab === 'wishlist-form' ? 'block' : 'hidden'}> <div className={getTabContainerClasses('wishlist-form')}>
<div className={getInnerContainerClasses('wishlist-form')}>
<WishlistForm <WishlistForm
key={`${tabParams.wishlistId || 'new'}-${tabParams.editConditionIndex ?? ''}-${tabParams.newTaskId ?? ''}-${tabParams.boardId ?? ''}`} key={`${tabParams.wishlistId || 'new'}-${tabParams.editConditionIndex ?? ''}-${tabParams.newTaskId ?? ''}-${tabParams.boardId ?? ''}`}
onNavigate={handleNavigate} onNavigate={handleNavigate}
@@ -1080,11 +1077,12 @@ function AppContent() {
boardId={tabParams.boardId} boardId={tabParams.boardId}
/> />
</div> </div>
)} </div>
)}
{loadedTabs['board-form'] && (
{loadedTabs['board-form'] && ( <div className={getTabContainerClasses('board-form')}>
<div className={activeTab === 'board-form' ? 'block' : 'hidden'}> <div className={getInnerContainerClasses('board-form')}>
<BoardForm <BoardForm
key={tabParams.boardId || 'new'} key={tabParams.boardId || 'new'}
onNavigate={handleNavigate} onNavigate={handleNavigate}
@@ -1092,36 +1090,44 @@ function AppContent() {
onSaved={() => setWishlistRefreshTrigger(prev => prev + 1)} onSaved={() => setWishlistRefreshTrigger(prev => prev + 1)}
/> />
</div> </div>
)} </div>
)}
{loadedTabs['board-join'] && ( {loadedTabs['board-join'] && (
<div className={activeTab === 'board-join' ? 'block' : 'hidden'}> <div className={getTabContainerClasses('board-join')}>
<div className={getInnerContainerClasses('board-join')}>
<BoardJoinPreview <BoardJoinPreview
key={tabParams.inviteToken} key={tabParams.inviteToken}
onNavigate={handleNavigate} onNavigate={handleNavigate}
inviteToken={tabParams.inviteToken} inviteToken={tabParams.inviteToken}
/> />
</div> </div>
)} </div>
)}
{loadedTabs.profile && ( {loadedTabs.profile && (
<div className={activeTab === 'profile' ? 'block' : 'hidden'}> <div className={getTabContainerClasses('profile')}>
<div className={getInnerContainerClasses('profile')}>
<Profile onNavigate={handleNavigate} /> <Profile onNavigate={handleNavigate} />
</div> </div>
)} </div>
)}
{loadedTabs['todoist-integration'] && ( {loadedTabs['todoist-integration'] && (
<div className={activeTab === 'todoist-integration' ? 'block' : 'hidden'}> <div className={getTabContainerClasses('todoist-integration')}>
<div className={getInnerContainerClasses('todoist-integration')}>
<TodoistIntegration onNavigate={handleNavigate} /> <TodoistIntegration onNavigate={handleNavigate} />
</div> </div>
)} </div>
)}
{loadedTabs['telegram-integration'] && ( {loadedTabs['telegram-integration'] && (
<div className={activeTab === 'telegram-integration' ? 'block' : 'hidden'}> <div className={getTabContainerClasses('telegram-integration')}>
<div className={getInnerContainerClasses('telegram-integration')}>
<TelegramIntegration onNavigate={handleNavigate} /> <TelegramIntegration onNavigate={handleNavigate} />
</div> </div>
)} </div>
</div> )}
</div> </div>
{/* Кнопка добавления задачи (только для таба задач) */} {/* Кнопка добавления задачи (только для таба задач) */}