feat(MainModal): enhance deep linking and navigation handling for collections and products

fix(Tabs): improve sidebar visibility logic and margin adjustment
feat(ModalTopBar): update breadcrumb logic for collection and product views
feat(Added): add button to navigate to discover page with deep linking
fix(Browse): refine error handling and update hash for collection navigation
This commit is contained in:
alexsparkes
2025-10-31 11:15:20 +00:00
parent 389590a04d
commit 94c92f7216
6 changed files with 309 additions and 214 deletions

View File

@@ -5,7 +5,7 @@ import './scss/index.scss';
import ModalLoader from './components/ModalLoader';
import ModalTopBar from './components/ModalTopBar';
import { TAB_TYPES } from './constants/tabConfig';
import { updateHash, onHashChange } from 'utils/deepLinking';
import { updateHash, parseDeepLink } from 'utils/deepLinking';
const Settings = lazy(() => import('../../../features/misc/views/Settings'));
const Library = lazy(() => import('../../../features/misc/views/Library'));
@@ -25,58 +25,88 @@ function MainModal({ modalClose, deepLinkData }) {
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]);
// Clear hash when modal closes
useEffect(() => {
// Listen for hash changes while modal is open
const cleanup = onHashChange((linkData) => {
if (linkData && linkData.tab !== currentTab) {
setCurrentTab(linkData.tab);
return () => {
// When modal unmounts, clear the hash
if (window.location.hash) {
window.history.replaceState(null, null, window.location.pathname);
}
});
};
}, []);
return cleanup;
useEffect(() => {
// Listen for browser back/forward navigation via popstate
const handlePopState = () => {
const linkData = window.location.hash ? parseDeepLink(window.location.hash) : null;
if (linkData) {
// Update tab if different
if (linkData.tab && linkData.tab !== currentTab) {
setCurrentTab(linkData.tab);
}
// Handle product and collection navigation
if (linkData.itemId && linkData.collection && linkData.fromCollection) {
// Product viewed from within a collection
// First set collection state, then navigate to product
setNavigationTrigger({
type: 'collection',
data: linkData.collection,
timestamp: Date.now(),
});
// Small delay to ensure collection state is set before navigating to product
setTimeout(() => {
setNavigationTrigger({
type: 'product',
data: {
id: linkData.itemId,
type: linkData.category,
},
timestamp: Date.now(),
});
}, 100);
} else if (linkData.itemId) {
// Product navigation (standalone)
setNavigationTrigger({
type: 'product',
data: {
id: linkData.itemId,
type: linkData.category,
},
timestamp: Date.now(),
});
} else if (linkData.collection) {
// Collection page navigation
setNavigationTrigger({
type: 'collection',
data: linkData.collection,
timestamp: Date.now(),
});
} else {
// Back to main view (clear collection state)
setProductView(null);
setNavigationTrigger({
type: 'main',
data: { clearCollection: true },
timestamp: Date.now(),
});
}
}
};
window.addEventListener('popstate', handlePopState);
return () => window.removeEventListener('popstate', handlePopState);
}, [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
if (newTab === TAB_TYPES.DISCOVER) {
@@ -112,23 +142,8 @@ function MainModal({ modalClose, deepLinkData }) {
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
}
// URL hash is already updated by child components (Browse.jsx)
// Browser history automatically tracks hash changes
};
const handleResetDiscoverToAll = () => {
@@ -137,115 +152,17 @@ function MainModal({ modalClose, deepLinkData }) {
};
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}`);
}
}
}
window.history.back();
};
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}`);
}
}
}
window.history.forward();
};
const canGoBack = historyIndex > 0;
const canGoForward = historyIndex < navigationHistory.length - 1;
// Browser manages history state, so we always show buttons enabled
// Browser will handle whether there's actually history to go back/forward
const canGoBack = true;
const canGoForward = true;
const TabComponent = TAB_COMPONENTS[currentTab] || Settings;
@@ -264,6 +181,7 @@ function MainModal({ modalClose, deepLinkData }) {
/>
<Suspense fallback={<ModalLoader />}>
<TabComponent
key={currentTab}
changeTab={handleChangeTab}
deepLinkData={deepLinkData}
currentTab={currentTab}

View File

@@ -52,7 +52,7 @@ const Tabs = ({ children, navbar = false, currentTab: activeTab, onSectionChange
return (
<div style={{ display: 'flex', width: '100%', minHeight: '100%' }}>
{showSidebar && (
{showSidebar ? (
<div className="modalSidebar">
{children.map((tab, index) => (
<Tab
@@ -65,8 +65,8 @@ const Tabs = ({ children, navbar = false, currentTab: activeTab, onSectionChange
))}
<ReminderInfo isVisible={showReminder} onHide={handleHideReminder} />
</div>
)}
<div className="modalTabContent">
) : null}
<div className="modalTabContent" style={{ marginLeft: showSidebar ? '1rem' : '0' }}>
{children.map((tab, index) => {
if (tab.props.label !== currentTab) {
return null;

View File

@@ -43,18 +43,43 @@ function ModalTopBar({
});
if (productView) {
// Show: Discover > Category > Product
const categoryKey = MARKETPLACE_TYPE_TO_KEY[productView.type];
if (categoryKey) {
console.log('ModalTopBar productView:', productView);
console.log('fromCollection:', productView.fromCollection, 'isCollection:', productView.isCollection, 'collectionTitle:', productView.collectionTitle);
// If viewing a collection page itself (not a product within it)
if (productView.isCollection) {
// Show: Discover > Collection Name
breadcrumbPath.push({
label: variables.getMessage(categoryKey),
onClick: productView.onBack || null,
label: productView.collectionTitle || productView.name,
onClick: null, // Current page - not clickable
});
} else {
// Viewing a product
// Show: Discover > Collection/Category > Product
if (productView.fromCollection && productView.collectionTitle) {
console.log('Showing collection breadcrumb:', productView.collectionTitle);
// If from a collection, show collection name
breadcrumbPath.push({
label: productView.collectionTitle,
onClick: productView.onBack || null,
});
} else {
console.log('Showing category breadcrumb');
// Otherwise show category
const categoryKey = MARKETPLACE_TYPE_TO_KEY[productView.type];
if (categoryKey) {
breadcrumbPath.push({
label: variables.getMessage(categoryKey),
onClick: productView.onBack || null,
});
}
}
// Add product name as final breadcrumb
breadcrumbPath.push({
label: productView.name,
onClick: null, // Current item - not clickable
});
}
breadcrumbPath.push({
label: productView.name,
onClick: null, // Current item - not clickable
});
} else if (currentSection) {
// Show: Tab > Section
breadcrumbPath.push({