+ ) : null}
+
{children.map((tab, index) => {
if (tab.props.label !== currentTab) {
return null;
diff --git a/src/components/Elements/MainModal/components/ModalTopBar.jsx b/src/components/Elements/MainModal/components/ModalTopBar.jsx
index 1928300a..df2495e9 100644
--- a/src/components/Elements/MainModal/components/ModalTopBar.jsx
+++ b/src/components/Elements/MainModal/components/ModalTopBar.jsx
@@ -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({
diff --git a/src/features/marketplace/views/Added.jsx b/src/features/marketplace/views/Added.jsx
index f2ec749c..5404f96f 100644
--- a/src/features/marketplace/views/Added.jsx
+++ b/src/features/marketplace/views/Added.jsx
@@ -1,6 +1,6 @@
import variables from 'config/variables';
import { memo, useState, useEffect, useCallback } from 'react';
-import { MdUpdate, MdOutlineExtensionOff, MdSendTimeExtension } from 'react-icons/md';
+import { MdUpdate, MdOutlineExtensionOff, MdSendTimeExtension, MdExplore } from 'react-icons/md';
import { toast } from 'react-toastify';
import Modal from 'react-modal';
@@ -12,6 +12,7 @@ import { Header, CustomActions } from 'components/Layout/Settings';
import { Button } from 'components/Elements';
import { install, uninstall, urlParser } from 'utils/marketplace';
+import { updateHash } from 'utils/deepLinking';
const Added = memo(() => {
const [installed, setInstalled] = useState(JSON.parse(localStorage.getItem('installed')));
@@ -203,6 +204,13 @@ const Added = memo(() => {
>
);
+ const goToDiscover = useCallback(() => {
+ updateHash('#discover/all');
+ // Trigger a popstate event to update the UI
+ const event = new window.Event('popstate');
+ window.dispatchEvent(event);
+ }, []);
+
if (installed.length === 0) {
return (
<>
@@ -219,6 +227,12 @@ const Added = memo(() => {
{variables.getMessage('modals.main.addons.empty.description')}
+ }
+ label="Get Some"
+ />
>
diff --git a/src/features/marketplace/views/Browse.jsx b/src/features/marketplace/views/Browse.jsx
index 4c859fbc..0b2abfea 100644
--- a/src/features/marketplace/views/Browse.jsx
+++ b/src/features/marketplace/views/Browse.jsx
@@ -12,7 +12,6 @@ import {
import ItemPage from './ItemPage';
import Items from '../components/Items/Items';
-import { Header } from 'components/Layout/Settings';
import { Button } from 'components/Elements';
import { install, urlParser, uninstall } from 'utils/marketplace';
@@ -99,20 +98,28 @@ class Marketplace extends PureComponent {
// Error caught but only used for flow control
if (this.controller.signal.aborted === false) {
console.error('Failed to fetch item:', error);
- return toast(variables.getMessage('toasts.error'));
+ toast(variables.getMessage('toasts.error'));
}
+ return;
}
if (this.controller.signal.aborted === true) {
return;
}
+ // Verify we have valid data before proceeding
+ if (!info || !info.data) {
+ console.error('Invalid item data received:', info);
+ toast(variables.getMessage('toasts.error'));
+ return;
+ }
+
// check if already installed
let button = this.buttons.install;
let addonInstalled = false;
let addonInstalledVersion;
- const installed = JSON.parse(localStorage.getItem('installed'));
+ const installed = JSON.parse(localStorage.getItem('installed')) || [];
if (installed.some((item) => item.name === info.data.name)) {
button = this.buttons.uninstall;
@@ -146,24 +153,49 @@ class Marketplace extends PureComponent {
// Update URL hash with item ID for deep linking
if (info.data?.id) {
- updateHash(`#discover/${info.data.type}/${info.data.id}`);
+ // If viewing from a collection, include collection in URL
+ if (this.state.collection && this.state.collectionName) {
+ updateHash(`#discover/collection/${this.state.collectionName}/${info.data.id}`);
+ } else {
+ updateHash(`#discover/${info.data.type}/${info.data.id}`);
+ }
}
// Notify parent about product view for breadcrumbs
if (this.props.onProductView) {
- this.props.onProductView({
+ const productViewData = {
id: info.data.id,
type: info.data.type,
name: info.data.display_name || info.data.name,
+ fromCollection: this.state.collection,
+ collectionName: this.state.collectionName,
+ collectionTitle: this.state.collectionTitle,
+ collectionDescription: this.state.collectionDescription,
+ collectionImg: this.state.collectionImg,
onBack: () => this.toggle('main'),
onBackToAll: () => {
- this.toggle('main');
- updateHash('#discover/all');
- if (this.props.onResetToAll) {
- this.props.onResetToAll();
- }
+ // Clear collection state before toggling to main
+ this.setState(
+ {
+ collection: false,
+ collectionName: null,
+ collectionTitle: null,
+ collectionDescription: null,
+ collectionImg: null,
+ },
+ () => {
+ // Call after state is updated
+ updateHash('#discover/all');
+ this.toggle('main');
+ if (this.props.onResetToAll) {
+ this.props.onResetToAll();
+ }
+ },
+ );
},
- });
+ };
+ console.log('Product view data being sent to parent:', productViewData);
+ this.props.onProductView(productViewData);
}
document.querySelector('#modal').scrollTop = 0;
@@ -177,21 +209,82 @@ class Marketplace extends PureComponent {
})
).json();
- this.setState({
+ const collectionState = {
items: collection.data.items,
+ collectionName: data, // Store the collection name/slug for navigation
collectionTitle: collection.data.display_name,
collectionDescription: collection.data.description,
collectionImg: collection.data.img,
collection: true,
done: true,
- });
+ };
+ console.log('Setting collection state:', collectionState);
+ this.setState(collectionState);
// Update hash for collection deep linking
updateHash(`#discover/collection/${data}`);
+
+ // Notify parent about collection view for breadcrumbs
+ if (this.props.onProductView) {
+ const collectionViewData = {
+ id: data,
+ type: 'collection',
+ name: collection.data.display_name,
+ isCollection: true,
+ fromCollection: false,
+ collectionName: data,
+ collectionTitle: collection.data.display_name,
+ onBack: () => {
+ this.setState(
+ {
+ collection: false,
+ collectionName: null,
+ collectionTitle: null,
+ collectionDescription: null,
+ collectionImg: null,
+ },
+ () => {
+ // Call toggle after state is updated
+ updateHash('#discover/all');
+ this.toggle('main');
+ },
+ );
+ },
+ onBackToAll: () => {
+ // Clear collection state and return to main view
+ this.setState(
+ {
+ collection: false,
+ collectionName: null,
+ collectionTitle: null,
+ collectionDescription: null,
+ collectionImg: null,
+ },
+ () => {
+ // Call toggle after state is updated
+ updateHash('#discover/all');
+ this.toggle('main');
+ if (this.props.onResetToAll) {
+ this.props.onResetToAll();
+ }
+ },
+ );
+ },
+ };
+ console.log('Collection view data being sent to parent:', collectionViewData);
+ this.props.onProductView(collectionViewData);
+ }
} else {
this.setState({ item: {}, relatedItems: [] });
- // Clear hash when returning to main view
- updateHash(`#discover/${this.props.type}`);
+
+ // Update hash based on current view state
+ if (this.state.collection && this.state.collectionName) {
+ // If returning to a collection view, preserve collection hash
+ updateHash(`#discover/collection/${this.state.collectionName}`);
+ } else {
+ // Clear hash when returning to main view
+ updateHash(`#discover/${this.props.type}`);
+ }
// Clear product view for breadcrumbs
if (this.props.onProductView) {
@@ -325,7 +418,15 @@ class Marketplace extends PureComponent {
}
returnToMain() {
- this.setState({ items: this.state.oldItems, collection: false });
+ this.setState({
+ items: this.state.oldItems,
+ collection: false,
+ collectionName: null,
+ collectionTitle: null,
+ collectionDescription: null,
+ collectionImg: null,
+ });
+ updateHash(`#discover/${this.props.type}`);
}
componentDidMount() {
@@ -363,9 +464,28 @@ class Marketplace extends PureComponent {
if (type === 'product' && data) {
// Navigate to product
this.toggle('item', { id: data.id, type: data.type });
+ } else if (type === 'collection' && data) {
+ // Navigate to collection
+ this.toggle('collection', data);
} else if (type === 'main' || type === 'section') {
// Navigate to main view (browse/listing page)
- this.toggle('main');
+ // Check if we need to clear collection state first
+ if (data && data.clearCollection && this.state.collection) {
+ this.setState(
+ {
+ collection: false,
+ collectionName: null,
+ collectionTitle: null,
+ collectionDescription: null,
+ collectionImg: null,
+ },
+ () => {
+ this.toggle('main');
+ },
+ );
+ } else {
+ this.toggle('main');
+ }
}
}
}
@@ -445,12 +565,6 @@ class Marketplace extends PureComponent {
<>
{this.state.collection === true ? (
<>
- this.returnToMain()}
- />
{ tab: 'marketplace', itemId: 'f41219846700' }
- * #marketplace/preset_settings/f41219846700 -> { tab: 'marketplace', category: 'preset_settings', itemId: 'f41219846700' }
- * #marketplace/collection/featured -> { tab: 'marketplace', collection: 'featured' }
+ * #discover/f41219846700 -> { tab: 'discover', itemId: 'f41219846700' }
+ * #discover/preset_settings/f41219846700 -> { tab: 'discover', category: 'preset_settings', itemId: 'f41219846700' }
+ * #discover/collection/featured -> { tab: 'discover', collection: 'featured' }
+ * #discover/collection/featured/f41219846700 -> { tab: 'discover', collection: 'featured', itemId: 'f41219846700' }
+ * #marketplace/74ef53ceed0b -> { tab: 'discover', itemId: '74ef53ceed0b' } (marketplace is aliased to discover)
* #settings/appearance -> { tab: 'settings', section: 'appearance' }
* #addons -> { tab: 'addons' }
*
* Legacy format (still supported):
- * #marketplace/quote_packs/digital-stoicism -> converted to item lookup
+ * #discover/quote_packs/digital-stoicism -> converted to item lookup
*/
export const parseDeepLink = (hash = window.location.hash) => {
if (!hash || hash === '#') {
@@ -34,25 +36,36 @@ export const parseDeepLink = (hash = window.location.hash) => {
itemId: parts[2],
};
+ // Handle marketplace as an alias for discover (for backward compatibility with external URLs)
+ if (result.tab === 'marketplace') {
+ result.tab = 'discover';
+ }
+
// Validate tab
const validTabs = Object.values(TAB_TYPES);
if (!validTabs.includes(result.tab)) {
return null;
}
- // Handle marketplace-specific parsing
- if (result.tab === 'marketplace') {
+ // Handle discover-specific parsing (marketplace URLs are aliased to discover)
+ if (result.tab === 'discover') {
// Check if it's a collection
if (result.section === 'collection') {
result.collection = result.itemId;
- result.itemId = null;
+ // Check if there's a 4th part (item ID within collection)
+ if (parts[3]) {
+ result.itemId = parts[3];
+ result.fromCollection = true;
+ } else {
+ result.itemId = null;
+ }
}
// Check if section is a category (preset_settings, photo_packs, quote_packs)
else if (['preset_settings', 'photo_packs', 'quote_packs', 'all'].includes(result.section)) {
result.category = result.section;
// Third part is the item ID (already in result.itemId)
}
- // If only one part after marketplace, assume it's an item ID
+ // If only one part after tab, assume it's an item ID
else if (result.section && !result.itemId) {
result.itemId = result.section;
result.section = null;
@@ -64,20 +77,25 @@ export const parseDeepLink = (hash = window.location.hash) => {
/**
* Create a deep link hash
- * @param {string} tab - The main tab (settings, marketplace, addons)
+ * @param {string} tab - The main tab (settings, discover, addons)
* @param {object} options - Additional options
- * @param {string} options.itemId - Item ID for marketplace items (v2 format)
- * @param {string} options.category - Category for marketplace items (optional)
- * @param {string} options.collection - Collection name for marketplace
+ * @param {string} options.itemId - Item ID for discover/marketplace items (v2 format)
+ * @param {string} options.category - Category for discover/marketplace items (optional)
+ * @param {string} options.collection - Collection name for discover/marketplace
+ * @param {boolean} options.fromCollection - If item is being viewed from within a collection
* @param {string} options.section - Section within the tab
* @returns {string} Hash string
*/
export const createDeepLink = (tab, options = {}) => {
let hash = `#${tab}`;
- if (tab === 'marketplace') {
- // Collection link
- if (options.collection) {
+ if (tab === 'discover') {
+ // Collection with item (item viewed from within a collection)
+ if (options.collection && options.itemId && options.fromCollection) {
+ hash += `/collection/${options.collection}/${options.itemId}`;
+ }
+ // Collection link (just the collection page)
+ else if (options.collection) {
hash += `/collection/${options.collection}`;
}
// Item with category
@@ -101,10 +119,16 @@ export const createDeepLink = (tab, options = {}) => {
/**
* Update URL hash without triggering page reload
+ * @param {string} hash - The hash to set
+ * @param {boolean} pushToHistory - If true, adds to browser history (default: true)
*/
-export const updateHash = (hash) => {
+export const updateHash = (hash, pushToHistory = true) => {
if (window.history.pushState) {
- window.history.pushState(null, null, hash);
+ if (pushToHistory) {
+ window.history.pushState(null, null, hash);
+ } else {
+ window.history.replaceState(null, null, hash);
+ }
} else {
window.location.hash = hash;
}