feat(modal): new navigation with back and forth arrow

This commit is contained in:
alexsparkes
2025-10-29 23:46:15 +00:00
parent 3f1aa9f5cd
commit aa6b1a3be3
9 changed files with 544 additions and 20 deletions

View File

@@ -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 (
<div className="frame">
<ModalTopBar currentTab={currentTab} onTabChange={handleChangeTab} onClose={modalClose} />
<ModalTopBar
currentTab={currentTab}
currentSection={currentSection}
productView={productView}
onTabChange={handleChangeTab}
onClose={modalClose}
onBack={handleBack}
onForward={handleForward}
canGoBack={canGoBack}
canGoForward={canGoForward}
/>
<Suspense fallback={<ModalLoader />}>
<TabComponent
changeTab={handleChangeTab}
deepLinkData={deepLinkData}
currentTab={currentTab}
onSectionChange={handleSectionChange}
onProductView={handleProductView}
resetToAll={resetDiscoverToAll}
onResetToAll={handleResetDiscoverToAll}
navigationTrigger={navigationTrigger}
/>
</Suspense>
</div>

View File

@@ -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);

View File

@@ -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 (
<div className="modalTopBar">
<div className="topBarLeft">
<div className="navigationButtons">
<Tooltip title="Back" key="backTooltip">
<button
className="navButton"
onClick={onBack}
disabled={!canGoBack}
aria-label="Navigate back"
>
<MdArrowBack />
</button>
</Tooltip>
<Tooltip title="Forward" key="forwardTooltip">
<button
className="navButton"
onClick={onForward}
disabled={!canGoForward}
aria-label="Navigate forward"
>
<MdArrowForward />
</button>
</Tooltip>
</div>
<img
src="src/assets/icons/mue_about.png"
alt="Mue"
className="topBarLogo"
draggable={false}
/>
{breadcrumbPath.length > 0 && (
<div className="breadcrumbs">
{breadcrumbPath.map((item, index) => {
const isLast = index === breadcrumbPath.length - 1;
const isClickable = item.onClick !== null;
return (
<span key={index} className="breadcrumb-segment">
<span
className={`breadcrumb-item ${isLast ? 'breadcrumb-current' : ''} ${
isClickable ? 'breadcrumb-clickable' : ''
}`}
onClick={item.onClick}
>
{item.label}
</span>
{!isLast && <MdChevronRight className="breadcrumb-separator" />}
</span>
);
})}
</div>
)}
</div>
<div className="topBarRight">
<div className="topBarNavigation">

View File

@@ -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 {

View File

@@ -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();

View File

@@ -185,7 +185,7 @@ class ItemPage extends PureComponent {
modalClose={() => this.setState({ shareModal: false })}
/>
</Modal>
<Header
{/* <Header
title={
this.props.addons
? variables.getMessage('modals.main.addons.added')
@@ -200,7 +200,7 @@ class ItemPage extends PureComponent {
}
report={false}
goBack={this.props.toggleFunction}
/>
/> */}
<div className="itemPage">
<div className="itemShowcase">
<div className="subHeader">

View File

@@ -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 (
<Tabs changeTab={(type) => changeTab(type)} current="discover" currentTab={currentTab}>
<Tabs
changeTab={(type) => changeTab(type)}
current="discover"
currentTab={currentTab}
onSectionChange={onSectionChange}
resetToFirst={resetToAll}
>
<div label={variables.getMessage('modals.main.marketplace.all')} name="all">
<MarketplaceTab type="all" deepLinkData={deepLinkData} />
<MarketplaceTab
type="all"
deepLinkData={deepLinkData}
onProductView={onProductView}
onResetToAll={onResetToAll}
navigationTrigger={navigationTrigger}
/>
</div>
<div label={variables.getMessage('modals.main.marketplace.photo_packs')} name="photo_packs">
<MarketplaceTab type="photo_packs" deepLinkData={deepLinkData} />
<MarketplaceTab
type="photo_packs"
deepLinkData={deepLinkData}
onProductView={onProductView}
onResetToAll={onResetToAll}
navigationTrigger={navigationTrigger}
/>
</div>
<div label={variables.getMessage('modals.main.marketplace.quote_packs')} name="quote_packs">
<MarketplaceTab type="quote_packs" deepLinkData={deepLinkData} />
<MarketplaceTab
type="quote_packs"
deepLinkData={deepLinkData}
onProductView={onProductView}
onResetToAll={onResetToAll}
navigationTrigger={navigationTrigger}
/>
</div>
<div
label={variables.getMessage('modals.main.marketplace.preset_settings')}
name="preset_settings"
>
<MarketplaceTab type="preset_settings" deepLinkData={deepLinkData} />
<MarketplaceTab
type="preset_settings"
deepLinkData={deepLinkData}
onProductView={onProductView}
onResetToAll={onResetToAll}
navigationTrigger={navigationTrigger}
/>
</div>
<div label={variables.getMessage('modals.main.marketplace.collections')} name="collections">
<MarketplaceTab type="collections" deepLinkData={deepLinkData} />
<MarketplaceTab
type="collections"
deepLinkData={deepLinkData}
onProductView={onProductView}
onResetToAll={onResetToAll}
navigationTrigger={navigationTrigger}
/>
</div>
</Tabs>
);

View File

@@ -7,7 +7,12 @@ import Create from '../../marketplace/views/Create';
function Library(props) {
return (
<Tabs changeTab={(type) => props.changeTab(type)} current="library" currentTab={props.currentTab}>
<Tabs
changeTab={(type) => props.changeTab(type)}
current="library"
currentTab={props.currentTab}
onSectionChange={props.onSectionChange}
>
<div label={variables.getMessage('modals.main.addons.added')} name="added">
<Added />
</div>

View File

@@ -90,7 +90,12 @@ const sections = [
function Settings(props) {
return (
<Tabs changeTab={(type) => props.changeTab(type)} current="settings" currentTab={props.currentTab}>
<Tabs
changeTab={(type) => props.changeTab(type)}
current="settings"
currentTab={props.currentTab}
onSectionChange={props.onSectionChange}
>
{sections.map(({ label, name, component: Component }) => (
<div key={name} label={variables.getMessage(label)} name={name}>
<Component />