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