diff --git a/src/components/Elements/MainModal/Main.jsx b/src/components/Elements/MainModal/Main.jsx index 5489d1e5..92d1f712 100644 --- a/src/components/Elements/MainModal/Main.jsx +++ b/src/components/Elements/MainModal/Main.jsx @@ -1,4 +1,5 @@ import { Suspense, lazy, useState, memo, useEffect } from 'react'; +import variables from 'config/variables'; import './scss/index.scss'; import ModalLoader from './components/ModalLoader'; @@ -21,6 +22,18 @@ function MainModal({ modalClose, deepLinkData }) { // Initialize with deep link tab if provided, otherwise default to settings const initialTab = deepLinkData?.tab || TAB_TYPES.SETTINGS; const [currentTab, setCurrentTab] = useState(initialTab); + const [currentSection, setCurrentSection] = useState(''); + const [productView, setProductView] = useState(null); + const [resetDiscoverToAll, setResetDiscoverToAll] = useState(false); + const [navigationHistory, setNavigationHistory] = useState([]); + const [historyIndex, setHistoryIndex] = useState(-1); + const [navigationTrigger, setNavigationTrigger] = useState(null); + const [skipNextHistoryAdd, setSkipNextHistoryAdd] = useState(0); + + // Clear product view when changing tabs + useEffect(() => { + setProductView(null); + }, [currentTab]); useEffect(() => { // Listen for hash changes while modal is open @@ -33,22 +46,232 @@ function MainModal({ modalClose, deepLinkData }) { return cleanup; }, [currentTab]); + const addToHistory = (state) => { + // Check if this state is different from the current one + const currentState = navigationHistory[historyIndex]; + const isDifferent = !currentState || + currentState.tab !== state.tab || + currentState.section !== state.section || + JSON.stringify(currentState.product) !== JSON.stringify(state.product); + + if (isDifferent) { + const newHistory = navigationHistory.slice(0, historyIndex + 1); + newHistory.push(state); + setNavigationHistory(newHistory); + setHistoryIndex(newHistory.length - 1); + console.log('Added to history:', state, 'New history:', newHistory); + } else { + console.log('Skipping duplicate history entry:', state); + } + }; + const handleChangeTab = (newTab) => { + // Only add to history if not navigating via history + if (skipNextHistoryAdd === 0) { + addToHistory({ + tab: newTab, + section: '', + product: null, + }); + } else { + setSkipNextHistoryAdd(0); // Reset skip counter + } + setCurrentTab(newTab); // Update URL hash when tab changes - updateHash(`#${newTab}`); + if (newTab === TAB_TYPES.DISCOVER) { + updateHash(`#${newTab}/all`); + } else if (newTab === TAB_TYPES.LIBRARY) { + updateHash(`#${newTab}/added`); + } else { + updateHash(`#${newTab}`); + } }; + const handleSectionChange = (section) => { + setCurrentSection(section); + // Update URL hash when section changes + if (currentTab === TAB_TYPES.DISCOVER) { + // For Discover tab, update with the section type + const sectionMap = { + [variables.getMessage('modals.main.marketplace.all')]: 'all', + [variables.getMessage('modals.main.marketplace.photo_packs')]: 'photo_packs', + [variables.getMessage('modals.main.marketplace.quote_packs')]: 'quote_packs', + [variables.getMessage('modals.main.marketplace.preset_settings')]: 'preset_settings', + [variables.getMessage('modals.main.marketplace.collections')]: 'collections', + }; + const sectionKey = sectionMap[section]; + if (sectionKey) { + updateHash(`#${currentTab}/${sectionKey}`); + } + } + + // Don't add section changes to history - they're automatic + // Only track user-initiated navigation (tab switches and product views) + }; + + const handleProductView = (product) => { + setProductView(product); + + // Add to navigation history only if not skipping + // Store only essential product info, not full object + if (product && skipNextHistoryAdd === 0) { + addToHistory({ + tab: currentTab, + section: currentSection, + product: { + type: product.type, + name: product.name, + id: product.id, + }, + }); + } else if (skipNextHistoryAdd > 0) { + console.log('Skipping product view history add'); + setSkipNextHistoryAdd(0); // Reset after use + } + }; + + const handleResetDiscoverToAll = () => { + setResetDiscoverToAll(true); + setTimeout(() => setResetDiscoverToAll(false), 100); + }; + + const handleBack = () => { + if (historyIndex > 0) { + const newIndex = historyIndex - 1; + const previousState = navigationHistory[newIndex]; + console.log('Going back to:', previousState); + + setHistoryIndex(newIndex); + + // Set skip flag BEFORE changing any state + setSkipNextHistoryAdd(1); + + // Change tab if different + if (previousState.tab !== currentTab) { + setCurrentTab(previousState.tab); + } + + setCurrentSection(previousState.section); + setProductView(previousState.product); + + // Trigger navigation in child components + if (previousState.product) { + // Viewing a product + setNavigationTrigger({ + type: 'product', + data: previousState.product, + timestamp: Date.now(), + }); + updateHash(`#${previousState.tab}/${previousState.product.type}/${previousState.product.id}`); + } else { + // Viewing main view + setNavigationTrigger({ + type: 'main', + data: null, + timestamp: Date.now(), + }); + + if (previousState.tab === TAB_TYPES.DISCOVER) { + const sectionMap = { + [variables.getMessage('modals.main.marketplace.all')]: 'all', + [variables.getMessage('modals.main.marketplace.photo_packs')]: 'photo_packs', + [variables.getMessage('modals.main.marketplace.quote_packs')]: 'quote_packs', + [variables.getMessage('modals.main.marketplace.preset_settings')]: 'preset_settings', + [variables.getMessage('modals.main.marketplace.collections')]: 'collections', + }; + const sectionKey = sectionMap[previousState.section] || 'all'; + updateHash(`#${previousState.tab}/${sectionKey}`); + } else if (previousState.tab === TAB_TYPES.LIBRARY) { + updateHash(`#${previousState.tab}/added`); + } else { + updateHash(`#${previousState.tab}`); + } + } + } + }; + + const handleForward = () => { + if (historyIndex < navigationHistory.length - 1) { + const newIndex = historyIndex + 1; + const nextState = navigationHistory[newIndex]; + console.log('Going forward to:', nextState); + + setHistoryIndex(newIndex); + + // Set skip flag BEFORE changing any state + setSkipNextHistoryAdd(1); + + // Change tab if different + if (nextState.tab !== currentTab) { + setCurrentTab(nextState.tab); + } + + setCurrentSection(nextState.section); + setProductView(nextState.product); + + // Trigger navigation in child components + if (nextState.product) { + setNavigationTrigger({ + type: 'product', + data: nextState.product, + timestamp: Date.now(), + }); + updateHash(`#${nextState.tab}/${nextState.product.type}/${nextState.product.id}`); + } else { + setNavigationTrigger({ + type: 'main', + data: null, + timestamp: Date.now(), + }); + + if (nextState.tab === TAB_TYPES.DISCOVER) { + const sectionMap = { + [variables.getMessage('modals.main.marketplace.all')]: 'all', + [variables.getMessage('modals.main.marketplace.photo_packs')]: 'photo_packs', + [variables.getMessage('modals.main.marketplace.quote_packs')]: 'quote_packs', + [variables.getMessage('modals.main.marketplace.preset_settings')]: 'preset_settings', + [variables.getMessage('modals.main.marketplace.collections')]: 'collections', + }; + const sectionKey = sectionMap[nextState.section] || 'all'; + updateHash(`#${nextState.tab}/${sectionKey}`); + } else if (nextState.tab === TAB_TYPES.LIBRARY) { + updateHash(`#${nextState.tab}/added`); + } else { + updateHash(`#${nextState.tab}`); + } + } + } + }; + + const canGoBack = historyIndex > 0; + const canGoForward = historyIndex < navigationHistory.length - 1; + const TabComponent = TAB_COMPONENTS[currentTab] || Settings; return (
- + }>
diff --git a/src/components/Elements/MainModal/backend/Tabs.jsx b/src/components/Elements/MainModal/backend/Tabs.jsx index 15fd3633..beddb71f 100644 --- a/src/components/Elements/MainModal/backend/Tabs.jsx +++ b/src/components/Elements/MainModal/backend/Tabs.jsx @@ -1,11 +1,11 @@ import variables from 'config/variables'; -import { useState } from 'react'; +import { useState, useEffect } from 'react'; import Tab from './Tab'; import ReminderInfo from '../components/ReminderInfo'; import ErrorBoundary from '../../../../features/misc/modals/ErrorBoundary'; import { TAB_TYPES } from '../constants/tabConfig'; -const Tabs = ({ children, navbar = false, currentTab: activeTab }) => { +const Tabs = ({ children, navbar = false, currentTab: activeTab, onSectionChange, resetToFirst }) => { const [currentTab, setCurrentTab] = useState(children[0]?.props.label); const [currentName, setCurrentName] = useState(children[0]?.props.name); const [showReminder, setShowReminder] = useState(localStorage.getItem('showReminder') === 'true'); @@ -17,8 +17,31 @@ const Tabs = ({ children, navbar = false, currentTab: activeTab }) => { setCurrentTab(tab); setCurrentName(name); + + // Notify parent of section change + if (onSectionChange) { + onSectionChange(tab); + } }; + // Notify parent of initial section on mount + useEffect(() => { + if (onSectionChange && currentTab) { + onSectionChange(currentTab); + } + }, []); + + // Reset to first tab when requested + useEffect(() => { + if (resetToFirst) { + setCurrentTab(children[0]?.props.label); + setCurrentName(children[0]?.props.name); + if (onSectionChange) { + onSectionChange(children[0]?.props.label); + } + } + }, [resetToFirst]); + const handleHideReminder = () => { localStorage.setItem('showReminder', 'false'); setShowReminder(false); diff --git a/src/components/Elements/MainModal/components/ModalTopBar.jsx b/src/components/Elements/MainModal/components/ModalTopBar.jsx index 856cd26a..7693f364 100644 --- a/src/components/Elements/MainModal/components/ModalTopBar.jsx +++ b/src/components/Elements/MainModal/components/ModalTopBar.jsx @@ -1,18 +1,121 @@ import variables from 'config/variables'; -import { MdClose } from 'react-icons/md'; +import { MdClose, MdChevronRight, MdArrowBack, MdArrowForward } from 'react-icons/md'; import { Tooltip, Button } from 'components/Elements'; import { NAVBAR_BUTTONS } from '../constants/tabConfig'; -function ModalTopBar({ currentTab, onTabChange, onClose }) { +// Map marketplace types to translation keys +const MARKETPLACE_TYPE_TO_KEY = { + photo_packs: 'modals.main.marketplace.photo_packs', + photos: 'modals.main.marketplace.photo_packs', + quote_packs: 'modals.main.marketplace.quote_packs', + quotes: 'modals.main.marketplace.quote_packs', + preset_settings: 'modals.main.marketplace.preset_settings', + settings: 'modals.main.marketplace.preset_settings', + collections: 'modals.main.marketplace.collections', + all: 'modals.main.marketplace.all', +}; + +function ModalTopBar({ + currentTab, + currentSection, + productView, + onTabChange, + onClose, + onBack, + onForward, + canGoBack, + canGoForward, +}) { + // Get the current tab label + const currentTabButton = NAVBAR_BUTTONS.find(({ tab }) => tab === currentTab); + const currentTabLabel = currentTabButton + ? variables.getMessage(currentTabButton.messageKey) + : ''; + + // Determine breadcrumb path with click handlers + const breadcrumbPath = []; + + if (currentTabLabel) { + breadcrumbPath.push({ + label: currentTabLabel, + onClick: productView ? productView.onBackToAll : null, // Clickable if viewing a product + }); + + if (productView) { + // Show: Discover > Category > Product + const categoryKey = MARKETPLACE_TYPE_TO_KEY[productView.type]; + if (categoryKey) { + breadcrumbPath.push({ + label: variables.getMessage(categoryKey), + onClick: productView.onBack || null, + }); + } + breadcrumbPath.push({ + label: productView.name, + onClick: null, // Current item - not clickable + }); + } else if (currentSection) { + // Show: Tab > Section + breadcrumbPath.push({ + label: currentSection, + onClick: null, // Current section - not clickable + }); + } + } + return (
+
+ + + + + + +
Mue + {breadcrumbPath.length > 0 && ( +
+ {breadcrumbPath.map((item, index) => { + const isLast = index === breadcrumbPath.length - 1; + const isClickable = item.onClick !== null; + + return ( + + + {item.label} + + {!isLast && } + + ); + })} +
+ )}
diff --git a/src/components/Elements/MainModal/scss/modules/_topBar.scss b/src/components/Elements/MainModal/scss/modules/_topBar.scss index af8fc866..5b44babf 100644 --- a/src/components/Elements/MainModal/scss/modules/_topBar.scss +++ b/src/components/Elements/MainModal/scss/modules/_topBar.scss @@ -11,11 +11,94 @@ display: flex; align-items: center; flex-shrink: 0; + gap: 1rem; + + .navigationButtons { + display: flex; + gap: 0.25rem; + + .navButton { + display: grid; + place-items: center; + padding: 0.5rem; + border-radius: 8px; + cursor: pointer; + border: none; + background: none; + transition: 0.3s; + + @include themed { + color: t($color); + } + + svg { + font-size: 1.2rem; + } + + &:hover:not(:disabled) { + @include themed { + background: t($modal-sidebarActive); + } + } + + &:disabled { + opacity: 0.3; + cursor: not-allowed; + } + } + } .topBarLogo { height: 32px; width: auto; } + + .breadcrumbs { + display: flex; + align-items: center; + gap: 0; + font-size: 0.95rem; + font-weight: 500; + + .breadcrumb-segment { + display: flex; + align-items: center; + // gap: 0.5rem; + } + + .breadcrumb-item { + @include themed { + color: t($subColor); + } + white-space: nowrap; + transition: opacity 0.2s; + } + + .breadcrumb-clickable { + cursor: pointer; + + &:hover { + opacity: 0.7; + text-decoration: underline; + } + } + + .breadcrumb-separator { + @include themed { + color: t($subColor); + } + opacity: 0.5; + font-size: 1.2rem; + margin: 0 0.25rem; + } + + .breadcrumb-current { + @include themed { + color: t($color); + } + font-weight: 600; + } + } } .topBarRight { diff --git a/src/features/marketplace/views/Browse.jsx b/src/features/marketplace/views/Browse.jsx index 4030cfc0..28414e6a 100644 --- a/src/features/marketplace/views/Browse.jsx +++ b/src/features/marketplace/views/Browse.jsx @@ -147,7 +147,24 @@ class Marketplace extends PureComponent { // Update URL hash with item ID for deep linking if (info.data?.id) { - updateHash(`#marketplace/${info.data.type}/${info.data.id}`); + updateHash(`#discover/${info.data.type}/${info.data.id}`); + } + + // Notify parent about product view for breadcrumbs + if (this.props.onProductView) { + this.props.onProductView({ + id: info.data.id, + type: info.data.type, + name: info.data.display_name || info.data.name, + onBack: () => this.toggle('main'), + onBackToAll: () => { + this.toggle('main'); + updateHash('#discover/all'); + if (this.props.onResetToAll) { + this.props.onResetToAll(); + } + }, + }); } document.querySelector('#modal').scrollTop = 0; @@ -171,11 +188,16 @@ class Marketplace extends PureComponent { }); // Update hash for collection deep linking - updateHash(`#marketplace/collection/${data}`); + updateHash(`#discover/collection/${data}`); } else { this.setState({ item: {}, relatedItems: [] }); // Clear hash when returning to main view - updateHash('#marketplace'); + updateHash(`#discover/${this.props.type}`); + + // Clear product view for breadcrumbs + if (this.props.onProductView) { + this.props.onProductView(null); + } } } @@ -331,6 +353,21 @@ class Marketplace extends PureComponent { } } + componentDidUpdate(prevProps) { + // Handle navigation trigger changes + if (this.props.navigationTrigger && this.props.navigationTrigger !== prevProps.navigationTrigger) { + const { type, data } = this.props.navigationTrigger; + + if (type === 'product' && data) { + // Navigate to product + this.toggle('item', { id: data.id, type: data.type }); + } else if (type === 'main' || type === 'section') { + // Navigate to main view (browse/listing page) + this.toggle('main'); + } + } + } + componentWillUnmount() { // stop making requests this.controller.abort(); diff --git a/src/features/marketplace/views/ItemPage.jsx b/src/features/marketplace/views/ItemPage.jsx index 94bca164..187ae0c9 100644 --- a/src/features/marketplace/views/ItemPage.jsx +++ b/src/features/marketplace/views/ItemPage.jsx @@ -185,7 +185,7 @@ class ItemPage extends PureComponent { modalClose={() => this.setState({ shareModal: false })} /> -
+ /> */}
diff --git a/src/features/misc/views/Discover.jsx b/src/features/misc/views/Discover.jsx index 2c444c50..380d4cda 100644 --- a/src/features/misc/views/Discover.jsx +++ b/src/features/misc/views/Discover.jsx @@ -4,26 +4,71 @@ import { memo } from 'react'; import Tabs from '../../../components/Elements/MainModal/backend/Tabs'; import MarketplaceTab from '../../marketplace/views/Browse'; -function Discover({ changeTab, deepLinkData, currentTab }) { +function Discover({ + changeTab, + deepLinkData, + currentTab, + onSectionChange, + onProductView, + resetToAll, + onResetToAll, + navigationTrigger, +}) { return ( - changeTab(type)} current="discover" currentTab={currentTab}> + changeTab(type)} + current="discover" + currentTab={currentTab} + onSectionChange={onSectionChange} + resetToFirst={resetToAll} + >
- +
- +
- +
- +
- +
); diff --git a/src/features/misc/views/Library.jsx b/src/features/misc/views/Library.jsx index 976396d0..180567c1 100644 --- a/src/features/misc/views/Library.jsx +++ b/src/features/misc/views/Library.jsx @@ -7,7 +7,12 @@ import Create from '../../marketplace/views/Create'; function Library(props) { return ( - props.changeTab(type)} current="library" currentTab={props.currentTab}> + props.changeTab(type)} + current="library" + currentTab={props.currentTab} + onSectionChange={props.onSectionChange} + >
diff --git a/src/features/misc/views/Settings.jsx b/src/features/misc/views/Settings.jsx index 2b7ae694..e5ac493e 100644 --- a/src/features/misc/views/Settings.jsx +++ b/src/features/misc/views/Settings.jsx @@ -90,7 +90,12 @@ const sections = [ function Settings(props) { return ( - props.changeTab(type)} current="settings" currentTab={props.currentTab}> + props.changeTab(type)} + current="settings" + currentTab={props.currentTab} + onSectionChange={props.onSectionChange} + > {sections.map(({ label, name, component: Component }) => (