mirror of
https://github.com/mue/mue.git
synced 2026-07-09 21:45:26 +02:00
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:
@@ -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}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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({
|
||||
|
||||
Reference in New Issue
Block a user