6.4.0: Экран товаров (Shopping List)
All checks were successful
Build and Push Docker Image / build-and-push (push) Successful in 1m22s
All checks were successful
Build and Push Docker Image / build-and-push (push) Successful in 1m22s
This commit is contained in:
@@ -14,6 +14,10 @@ import WishlistForm from './components/WishlistForm'
|
||||
import WishlistDetail from './components/WishlistDetail'
|
||||
import BoardForm from './components/BoardForm'
|
||||
import BoardJoinPreview from './components/BoardJoinPreview'
|
||||
import ShoppingList from './components/ShoppingList'
|
||||
import ShoppingItemForm from './components/ShoppingItemForm'
|
||||
import ShoppingBoardForm from './components/ShoppingBoardForm'
|
||||
import ShoppingBoardJoinPreview from './components/ShoppingBoardJoinPreview'
|
||||
import TodoistIntegration from './components/TodoistIntegration'
|
||||
import TelegramIntegration from './components/TelegramIntegration'
|
||||
import FitbitIntegration from './components/FitbitIntegration'
|
||||
@@ -29,8 +33,8 @@ 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', 'fitbit-integration', 'full', 'priorities', 'tracking', 'tracking-access', 'tracking-invite']
|
||||
const mainTabs = ['current', 'tasks', 'wishlist', 'shopping', 'profile']
|
||||
const deepTabs = ['add-words', 'test', 'task-form', 'wishlist-form', 'wishlist-detail', 'board-form', 'board-join', 'words', 'dictionaries', 'todoist-integration', 'telegram-integration', 'fitbit-integration', 'full', 'priorities', 'tracking', 'tracking-access', 'tracking-invite', 'shopping-item-form', 'shopping-board-form', 'shopping-board-join']
|
||||
|
||||
/**
|
||||
* Гарантирует базовую запись истории для главного экрана перед глубоким табом.
|
||||
@@ -77,8 +81,12 @@ function AppContent() {
|
||||
tracking: false,
|
||||
'tracking-access': false,
|
||||
'tracking-invite': false,
|
||||
shopping: false,
|
||||
'shopping-item-form': false,
|
||||
'shopping-board-form': false,
|
||||
'shopping-board-join': false,
|
||||
})
|
||||
|
||||
|
||||
// Отслеживаем, какие табы уже были загружены (для предотвращения повторных загрузок)
|
||||
const [tabsInitialized, setTabsInitialized] = useState({
|
||||
current: false,
|
||||
@@ -102,6 +110,10 @@ function AppContent() {
|
||||
tracking: false,
|
||||
'tracking-access': false,
|
||||
'tracking-invite': false,
|
||||
shopping: false,
|
||||
'shopping-item-form': false,
|
||||
'shopping-board-form': false,
|
||||
'shopping-board-join': false,
|
||||
})
|
||||
|
||||
// Параметры для навигации между вкладками
|
||||
@@ -149,6 +161,7 @@ function AppContent() {
|
||||
const [dictionariesRefreshTrigger, setDictionariesRefreshTrigger] = useState(0)
|
||||
const [wordsRefreshTrigger, setWordsRefreshTrigger] = useState(0)
|
||||
const [wishlistRefreshTrigger, setWishlistRefreshTrigger] = useState(0)
|
||||
const [shoppingRefreshTrigger, setShoppingRefreshTrigger] = useState(0)
|
||||
|
||||
|
||||
|
||||
@@ -227,6 +240,20 @@ function AppContent() {
|
||||
}
|
||||
}
|
||||
|
||||
// Проверяем путь /shopping-invite/:token для присоединения к shopping доске
|
||||
if (path.startsWith('/shopping-invite/')) {
|
||||
const token = path.replace('/shopping-invite/', '')
|
||||
if (token) {
|
||||
const url = '/?tab=shopping-board-join&inviteToken=' + token
|
||||
ensureBaseHistory('shopping-board-join', { inviteToken: token }, url)
|
||||
setActiveTab('shopping-board-join')
|
||||
setLoadedTabs(prev => ({ ...prev, 'shopping-board-join': true }))
|
||||
setTabParams({ inviteToken: token })
|
||||
setIsInitialized(true)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Проверяем путь /tracking/invite/:token
|
||||
if (path.startsWith('/tracking/invite/')) {
|
||||
const token = path.replace('/tracking/invite/', '')
|
||||
@@ -262,8 +289,8 @@ function AppContent() {
|
||||
|
||||
// Проверяем URL только для глубоких табов
|
||||
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', 'fitbit-integration', 'tracking', 'tracking-access', 'tracking-invite']
|
||||
|
||||
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', 'fitbit-integration', 'tracking', 'tracking-access', 'tracking-invite', 'shopping', 'shopping-item-form', 'shopping-board-form', 'shopping-board-join']
|
||||
|
||||
if (tabFromUrl && validTabs.includes(tabFromUrl) && deepTabs.includes(tabFromUrl) && window.history.length > 1) {
|
||||
// Восстанавливаем глубокий таб из URL только если есть история (не рестарт PWA)
|
||||
const params = {}
|
||||
@@ -754,7 +781,7 @@ function AppContent() {
|
||||
return
|
||||
}
|
||||
|
||||
const validTabs = ['current', 'priorities', 'full', 'words', 'add-words', 'dictionaries', 'test', 'tasks', 'task-form', 'wishlist', 'wishlist-form', 'wishlist-detail', 'profile', 'todoist-integration', 'telegram-integration', 'tracking', 'tracking-access', 'tracking-invite']
|
||||
const validTabs = ['current', 'priorities', 'full', 'words', 'add-words', 'dictionaries', 'test', 'tasks', 'task-form', 'wishlist', 'wishlist-form', 'wishlist-detail', 'profile', 'todoist-integration', 'telegram-integration', 'tracking', 'tracking-access', 'tracking-invite', 'shopping', 'shopping-item-form', 'shopping-board-form', 'shopping-board-join']
|
||||
|
||||
// Проверяем state текущей записи истории (куда мы вернулись)
|
||||
if (event.state && event.state.tab) {
|
||||
@@ -858,8 +885,8 @@ function AppContent() {
|
||||
setSelectedProject(null)
|
||||
setTabParams({})
|
||||
updateUrl('full', {}, activeTab)
|
||||
} else if (tab !== activeTab || tab === 'task-form' || tab === 'wishlist-form' || (tab === 'words' && Object.keys(params).length > 0)) {
|
||||
// Для task-form и wishlist-form всегда обновляем параметры, даже если это тот же таб
|
||||
} else if (tab !== activeTab || tab === 'task-form' || tab === 'wishlist-form' || tab === 'shopping-item-form' || (tab === 'words' && Object.keys(params).length > 0)) {
|
||||
// Для task-form, wishlist-form и shopping-item-form всегда обновляем параметры, даже если это тот же таб
|
||||
markTabAsLoaded(tab)
|
||||
|
||||
// Определяем, является ли текущий таб глубоким
|
||||
@@ -889,7 +916,7 @@ function AppContent() {
|
||||
// Проверяем, была ли последняя запись в истории от модального окна
|
||||
const currentState = window.history.state || {}
|
||||
const isFromModal = currentState.modalOpen === true
|
||||
const isNavigatingToForm = tab === 'task-form' || tab === 'wishlist-form'
|
||||
const isNavigatingToForm = tab === 'task-form' || tab === 'wishlist-form' || tab === 'shopping-item-form'
|
||||
|
||||
if (isFromModal && isNavigatingToForm) {
|
||||
// Заменяем запись модального окна на запись формы редактирования
|
||||
@@ -945,7 +972,7 @@ function AppContent() {
|
||||
if ((tab === 'wishlist-form' || tab === 'wishlist-detail') && activeTab !== tab) {
|
||||
setPreviousTab(activeTab)
|
||||
}
|
||||
|
||||
|
||||
// Обновляем список желаний при возврате из экрана редактирования
|
||||
if (activeTab === 'wishlist-form' && tab !== 'wishlist-form') {
|
||||
// Сохраняем boardId из параметров или текущих tabParams
|
||||
@@ -958,13 +985,27 @@ function AppContent() {
|
||||
setWishlistRefreshTrigger(prev => prev + 1)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Обновляем список желаний при возврате из экрана детализации
|
||||
if (activeTab === 'wishlist-detail' && tab !== 'wishlist-detail') {
|
||||
if (tab === 'wishlist') {
|
||||
setWishlistRefreshTrigger(prev => prev + 1)
|
||||
}
|
||||
}
|
||||
|
||||
// Сохраняем предыдущий таб при открытии shopping-item-form
|
||||
if (tab === 'shopping-item-form' && activeTab !== tab) {
|
||||
setPreviousTab(activeTab)
|
||||
}
|
||||
|
||||
// Обновляем список товаров при возврате из экрана редактирования
|
||||
if ((activeTab === 'shopping-item-form' || activeTab === 'shopping-board-form') && tab === 'shopping') {
|
||||
const savedBoardId = params.boardId || tabParams.boardId
|
||||
if (savedBoardId) {
|
||||
setTabParams(prev => ({ ...prev, boardId: savedBoardId }))
|
||||
}
|
||||
setShoppingRefreshTrigger(prev => prev + 1)
|
||||
}
|
||||
// Загрузка данных произойдет в useEffect при изменении activeTab
|
||||
}
|
||||
}
|
||||
@@ -1063,7 +1104,7 @@ 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 === 'fitbit-integration' || activeTab === 'full' || activeTab === 'priorities' || activeTab === 'words' || activeTab === 'dictionaries' || activeTab === 'tracking' || activeTab === 'tracking-access' || activeTab === 'tracking-invite'
|
||||
const isFullscreenTab = activeTab === 'test' || activeTab === 'add-words' || activeTab === 'task-form' || activeTab === 'wishlist-form' || activeTab === 'wishlist-detail' || activeTab === 'todoist-integration' || activeTab === 'telegram-integration' || activeTab === 'fitbit-integration' || activeTab === 'full' || activeTab === 'priorities' || activeTab === 'words' || activeTab === 'dictionaries' || activeTab === 'tracking' || activeTab === 'tracking-access' || activeTab === 'tracking-invite' || activeTab === 'shopping-item-form' || activeTab === 'shopping-board-form' || activeTab === 'shopping-board-join'
|
||||
|
||||
// Функция для получения классов скролл-контейнера для каждого таба
|
||||
// Каждый таб имеет свой изолированный скролл-контейнер для автоматического сохранения позиции скролла
|
||||
@@ -1075,7 +1116,7 @@ function AppContent() {
|
||||
|
||||
// Определяем padding для каждого таба
|
||||
let paddingClasses = ''
|
||||
if (tabName === 'current' || tabName === 'tasks' || tabName === 'wishlist' || tabName === 'profile') {
|
||||
if (tabName === 'current' || tabName === 'tasks' || tabName === 'wishlist' || tabName === 'shopping' || tabName === 'profile') {
|
||||
paddingClasses = 'pb-20'
|
||||
} else if (tabName === 'words' || tabName === 'dictionaries') {
|
||||
paddingClasses = 'pb-16'
|
||||
@@ -1086,7 +1127,7 @@ function AppContent() {
|
||||
|
||||
// Функция для определения отступов внутреннего контейнера
|
||||
const getInnerContainerClasses = (tabName) => {
|
||||
if (tabName === 'tasks' || tabName === 'wishlist' || tabName === 'profile') {
|
||||
if (tabName === 'tasks' || tabName === 'wishlist' || tabName === 'shopping' || tabName === 'profile') {
|
||||
return 'max-w-7xl mx-auto p-4 md:p-8'
|
||||
}
|
||||
if (tabName === 'current') {
|
||||
@@ -1301,6 +1342,59 @@ function AppContent() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{loadedTabs.shopping && (
|
||||
<div className={getTabContainerClasses('shopping')}>
|
||||
<div className={getInnerContainerClasses('shopping')}>
|
||||
<ShoppingList
|
||||
onNavigate={handleNavigate}
|
||||
refreshTrigger={shoppingRefreshTrigger}
|
||||
isActive={activeTab === 'shopping'}
|
||||
initialBoardId={tabParams.boardId}
|
||||
boardDeleted={tabParams.boardDeleted}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{loadedTabs['shopping-item-form'] && (
|
||||
<div className={getTabContainerClasses('shopping-item-form')}>
|
||||
<div className={getInnerContainerClasses('shopping-item-form')}>
|
||||
<ShoppingItemForm
|
||||
key={tabParams.itemId || 'new'}
|
||||
onNavigate={handleNavigate}
|
||||
itemId={tabParams.itemId}
|
||||
boardId={tabParams.boardId}
|
||||
onSaved={() => setShoppingRefreshTrigger(prev => prev + 1)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{loadedTabs['shopping-board-form'] && (
|
||||
<div className={getTabContainerClasses('shopping-board-form')}>
|
||||
<div className={getInnerContainerClasses('shopping-board-form')}>
|
||||
<ShoppingBoardForm
|
||||
key={tabParams.boardId || 'new'}
|
||||
onNavigate={handleNavigate}
|
||||
boardId={tabParams.boardId}
|
||||
onSaved={() => setShoppingRefreshTrigger(prev => prev + 1)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{loadedTabs['shopping-board-join'] && (
|
||||
<div className={getTabContainerClasses('shopping-board-join')}>
|
||||
<div className={getInnerContainerClasses('shopping-board-join')}>
|
||||
<ShoppingBoardJoinPreview
|
||||
key={tabParams.inviteToken}
|
||||
onNavigate={handleNavigate}
|
||||
inviteToken={tabParams.inviteToken}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{loadedTabs.profile && (
|
||||
<div className={getTabContainerClasses('profile')}>
|
||||
<div className={getInnerContainerClasses('profile')}>
|
||||
@@ -1423,6 +1517,42 @@ function AppContent() {
|
||||
</button>
|
||||
)}
|
||||
|
||||
{/* Кнопка добавления товара (только для таба shopping) */}
|
||||
{!isFullscreenTab && activeTab === 'shopping' && (
|
||||
<button
|
||||
onClick={() => {
|
||||
let boardId = tabParams.boardId
|
||||
if (!boardId) {
|
||||
try {
|
||||
const saved = localStorage.getItem('shopping_selected_board_id')
|
||||
if (saved) {
|
||||
const parsed = parseInt(saved, 10)
|
||||
if (!isNaN(parsed)) boardId = parsed
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error loading boardId from localStorage:', err)
|
||||
}
|
||||
}
|
||||
handleNavigate('shopping-item-form', { itemId: undefined, boardId: boardId })
|
||||
}}
|
||||
className="fixed bottom-16 right-4 z-20 bg-gradient-to-r from-indigo-600 to-purple-600 hover:from-indigo-700 hover:to-purple-700 text-white w-[61px] h-[61px] rounded-2xl shadow-lg transition-all duration-200 hover:scale-105 flex items-center justify-center"
|
||||
title="Добавить товар"
|
||||
>
|
||||
<svg
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<path d="M12 5v14M5 12h14" />
|
||||
</svg>
|
||||
</button>
|
||||
)}
|
||||
|
||||
{/* Кнопка добавления записи (только для таба current - экран прогресса) */}
|
||||
{!isFullscreenTab && activeTab === 'current' && (
|
||||
<button
|
||||
@@ -1538,6 +1668,26 @@ function AppContent() {
|
||||
<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('shopping')}
|
||||
className={`min-w-max whitespace-nowrap px-4 py-4 transition-all duration-300 relative ${
|
||||
activeTab === 'shopping' || activeTab === 'shopping-item-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">
|
||||
<circle cx="9" cy="21" r="1"></circle>
|
||||
<circle cx="20" cy="21" r="1"></circle>
|
||||
<path d="M1 1h4l2.68 13.39a2 2 0 0 0 2 1.61h9.72a2 2 0 0 0 2-1.61L23 6H6"></path>
|
||||
</svg>
|
||||
</span>
|
||||
{(activeTab === 'shopping' || activeTab === 'shopping-item-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('wishlist')}
|
||||
className={`min-w-max whitespace-nowrap px-4 py-4 transition-all duration-300 relative ${
|
||||
|
||||
Reference in New Issue
Block a user