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 (
+
+
+
+
+
+
+
+

+ {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 }) => (