4.13.6: Рефакторинг архитектуры табов
All checks were successful
Build and Push Docker Image / build-and-push (push) Successful in 1m17s
All checks were successful
Build and Push Docker Image / build-and-push (push) Successful in 1m17s
This commit is contained in:
@@ -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",
|
||||||
|
|||||||
@@ -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'
|
||||||
|
|
||||||
|
// Определяем padding для каждого таба
|
||||||
|
let paddingClasses = ''
|
||||||
|
if (tabName === 'current' || tabName === 'tasks' || tabName === 'wishlist' || tabName === 'profile') {
|
||||||
|
paddingClasses = 'pb-20'
|
||||||
|
} else if (tabName === 'words' || tabName === 'dictionaries') {
|
||||||
|
paddingClasses = 'pb-16'
|
||||||
}
|
}
|
||||||
return 'p-4 md:p-6'
|
|
||||||
|
return `${baseClasses} ${paddingClasses} ${visibilityClasses}`.trim()
|
||||||
}
|
}
|
||||||
// Для экрана статистики используем такие же отступы как для приоритетов
|
|
||||||
if (activeTab === 'full') {
|
// Функция для определения отступов внутреннего контейнера
|
||||||
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={activeTab === 'board-form' ? 'block' : 'hidden'}>
|
<div className={getTabContainerClasses('board-form')}>
|
||||||
|
<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>
|
||||||
|
|
||||||
{/* Кнопка добавления задачи (только для таба задач) */}
|
{/* Кнопка добавления задачи (только для таба задач) */}
|
||||||
|
|||||||
Reference in New Issue
Block a user