mirror of
https://github.com/mue/mue.git
synced 2026-06-08 22:18:40 +02:00
feat: implement deep linking support for marketplace items and tabs
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -5,6 +5,7 @@ node_modules/
|
|||||||
build/
|
build/
|
||||||
.idea/
|
.idea/
|
||||||
dist/
|
dist/
|
||||||
|
/docs
|
||||||
|
|
||||||
# Files
|
# Files
|
||||||
package-lock.json
|
package-lock.json
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
import variables from 'config/variables';
|
import variables from 'config/variables';
|
||||||
import { Suspense, lazy, useState, memo } from 'react';
|
import { Suspense, lazy, useState, memo, useEffect } from 'react';
|
||||||
import { MdClose } from 'react-icons/md';
|
import { MdClose } from 'react-icons/md';
|
||||||
|
|
||||||
import './scss/index.scss';
|
import './scss/index.scss';
|
||||||
import { Tooltip } from 'components/Elements';
|
import { Tooltip } from 'components/Elements';
|
||||||
import ModalLoader from './components/ModalLoader';
|
import ModalLoader from './components/ModalLoader';
|
||||||
import { TAB_TYPES } from './constants/tabConfig';
|
import { TAB_TYPES } from './constants/tabConfig';
|
||||||
|
import { updateHash, onHashChange } from 'utils/deepLinking';
|
||||||
|
|
||||||
const Settings = lazy(() => import('../../../features/misc/views/Settings'));
|
const Settings = lazy(() => import('../../../features/misc/views/Settings'));
|
||||||
const Addons = lazy(() => import('../../../features/misc/views/Addons'));
|
const Addons = lazy(() => import('../../../features/misc/views/Addons'));
|
||||||
@@ -18,8 +19,27 @@ const TAB_COMPONENTS = {
|
|||||||
[TAB_TYPES.MARKETPLACE]: Marketplace,
|
[TAB_TYPES.MARKETPLACE]: Marketplace,
|
||||||
};
|
};
|
||||||
|
|
||||||
function MainModal({ modalClose }) {
|
function MainModal({ modalClose, deepLinkData }) {
|
||||||
const [currentTab, setCurrentTab] = useState(TAB_TYPES.SETTINGS);
|
// Initialize with deep link tab if provided, otherwise default to settings
|
||||||
|
const initialTab = deepLinkData?.tab || TAB_TYPES.SETTINGS;
|
||||||
|
const [currentTab, setCurrentTab] = useState(initialTab);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// Listen for hash changes while modal is open
|
||||||
|
const cleanup = onHashChange((linkData) => {
|
||||||
|
if (linkData && linkData.tab !== currentTab) {
|
||||||
|
setCurrentTab(linkData.tab);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return cleanup;
|
||||||
|
}, [currentTab]);
|
||||||
|
|
||||||
|
const handleChangeTab = (newTab) => {
|
||||||
|
setCurrentTab(newTab);
|
||||||
|
// Update URL hash when tab changes
|
||||||
|
updateHash(`#${newTab}`);
|
||||||
|
};
|
||||||
|
|
||||||
const TabComponent = TAB_COMPONENTS[currentTab] || Settings;
|
const TabComponent = TAB_COMPONENTS[currentTab] || Settings;
|
||||||
|
|
||||||
@@ -35,7 +55,7 @@ function MainModal({ modalClose }) {
|
|||||||
</span>
|
</span>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<Suspense fallback={<ModalLoader />}>
|
<Suspense fallback={<ModalLoader />}>
|
||||||
<TabComponent changeTab={setCurrentTab} />
|
<TabComponent changeTab={handleChangeTab} deepLinkData={deepLinkData} />
|
||||||
</Suspense>
|
</Suspense>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -17,11 +17,23 @@ import { Header } from 'components/Layout/Settings';
|
|||||||
import { Button } from 'components/Elements';
|
import { Button } from 'components/Elements';
|
||||||
|
|
||||||
import { install, urlParser, uninstall } from 'utils/marketplace';
|
import { install, urlParser, uninstall } from 'utils/marketplace';
|
||||||
|
import { updateHash } from 'utils/deepLinking';
|
||||||
|
|
||||||
|
// API v2 base URL
|
||||||
|
const API_V2_BASE = `${variables.constants.API_URL}/marketplace`;
|
||||||
|
|
||||||
class Marketplace extends PureComponent {
|
class Marketplace extends PureComponent {
|
||||||
constructor() {
|
constructor(props) {
|
||||||
super();
|
super(props);
|
||||||
this.state = { items: [], button: '', done: false, item: {}, collection: false, filter: '' };
|
this.state = {
|
||||||
|
items: [],
|
||||||
|
relatedItems: [],
|
||||||
|
button: '',
|
||||||
|
done: false,
|
||||||
|
item: {},
|
||||||
|
collection: false,
|
||||||
|
filter: '',
|
||||||
|
};
|
||||||
this.buttons = {
|
this.buttons = {
|
||||||
uninstall: (
|
uninstall: (
|
||||||
<Button
|
<Button
|
||||||
@@ -46,19 +58,48 @@ class Marketplace extends PureComponent {
|
|||||||
async toggle(pageType, data) {
|
async toggle(pageType, data) {
|
||||||
if (pageType === 'item') {
|
if (pageType === 'item') {
|
||||||
let info;
|
let info;
|
||||||
// get item info
|
let relatedItems = [];
|
||||||
|
|
||||||
|
// get item info using API v2
|
||||||
try {
|
try {
|
||||||
let type = this.props.type;
|
let type = this.props.type;
|
||||||
if (type === 'all' || type === 'collections') {
|
if (type === 'all' || type === 'collections') {
|
||||||
type = data.type;
|
type = data.type;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// API v2: Fetch by ID if available, otherwise by name
|
||||||
|
const itemEndpoint = data.id
|
||||||
|
? `${API_V2_BASE}/item/${data.id}`
|
||||||
|
: `${API_V2_BASE}/item/${type}/${data.name}`;
|
||||||
|
|
||||||
info = await (
|
info = await (
|
||||||
await fetch(`${variables.constants.API_URL}/marketplace/item/${type}/${data.name}`, {
|
await fetch(itemEndpoint, {
|
||||||
signal: this.controller.signal,
|
signal: this.controller.signal,
|
||||||
})
|
})
|
||||||
).json();
|
).json();
|
||||||
} catch (e) {
|
|
||||||
|
// Fetch related items using API v2
|
||||||
|
if (info.data?.id) {
|
||||||
|
try {
|
||||||
|
const relatedResponse = await fetch(`${API_V2_BASE}/item/${info.data.id}/related`, {
|
||||||
|
signal: this.controller.signal,
|
||||||
|
});
|
||||||
|
const relatedData = await relatedResponse.json();
|
||||||
|
relatedItems = relatedData.data?.related || [];
|
||||||
|
} catch (relatedError) {
|
||||||
|
console.warn('Failed to fetch related items:', relatedError);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Track view using API v2
|
||||||
|
fetch(`${API_V2_BASE}/item/${info.data.id}/view`, {
|
||||||
|
method: 'POST',
|
||||||
|
signal: this.controller.signal,
|
||||||
|
}).catch(() => {}); // Silent fail for analytics
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
// Error caught but only used for flow control
|
||||||
if (this.controller.signal.aborted === false) {
|
if (this.controller.signal.aborted === false) {
|
||||||
|
console.error('Failed to fetch item:', error);
|
||||||
return toast(variables.getMessage('toasts.error'));
|
return toast(variables.getMessage('toasts.error'));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -87,12 +128,12 @@ class Marketplace extends PureComponent {
|
|||||||
|
|
||||||
this.setState({
|
this.setState({
|
||||||
item: {
|
item: {
|
||||||
|
id: info.data.id, // Store item ID for deep linking
|
||||||
onCollection: data._onCollection,
|
onCollection: data._onCollection,
|
||||||
type: info.data.type,
|
type: info.data.type,
|
||||||
display_name: info.data.name,
|
display_name: info.data.name || info.data.display_name,
|
||||||
author: info.data.author,
|
author: info.data.author,
|
||||||
description: urlParser(info.data.description.replace(/\n/g, '<br>')),
|
description: urlParser(info.data.description.replace(/\n/g, '<br>')),
|
||||||
//updated: info.updated,
|
|
||||||
version: info.data.version,
|
version: info.data.version,
|
||||||
icon: info.data.screenshot_url,
|
icon: info.data.screenshot_url,
|
||||||
data: info.data,
|
data: info.data,
|
||||||
@@ -100,17 +141,26 @@ class Marketplace extends PureComponent {
|
|||||||
addonInstalledVersion,
|
addonInstalledVersion,
|
||||||
api_name: data.name,
|
api_name: data.name,
|
||||||
},
|
},
|
||||||
|
relatedItems,
|
||||||
button: button,
|
button: button,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Update URL hash with item ID for deep linking
|
||||||
|
if (info.data?.id) {
|
||||||
|
updateHash(`#marketplace/${info.data.type}/${info.data.id}`);
|
||||||
|
}
|
||||||
|
|
||||||
document.querySelector('#modal').scrollTop = 0;
|
document.querySelector('#modal').scrollTop = 0;
|
||||||
variables.stats.postEvent('marketplace-item', `${this.state.item.display_name} viewed`);
|
variables.stats.postEvent('marketplace-item', `${this.state.item.display_name} viewed`);
|
||||||
} else if (pageType === 'collection') {
|
} else if (pageType === 'collection') {
|
||||||
this.setState({ done: false, item: {} });
|
this.setState({ done: false, item: {} });
|
||||||
|
// Use API v2 for collections
|
||||||
const collection = await (
|
const collection = await (
|
||||||
await fetch(`${variables.constants.API_URL}/marketplace/collection/${data}`, {
|
await fetch(`${API_V2_BASE}/collection/${data}`, {
|
||||||
signal: this.controller.signal,
|
signal: this.controller.signal,
|
||||||
})
|
})
|
||||||
).json();
|
).json();
|
||||||
|
|
||||||
this.setState({
|
this.setState({
|
||||||
items: collection.data.items,
|
items: collection.data.items,
|
||||||
collectionTitle: collection.data.display_name,
|
collectionTitle: collection.data.display_name,
|
||||||
@@ -119,20 +169,28 @@ class Marketplace extends PureComponent {
|
|||||||
collection: true,
|
collection: true,
|
||||||
done: true,
|
done: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Update hash for collection deep linking
|
||||||
|
updateHash(`#marketplace/collection/${data}`);
|
||||||
} else {
|
} else {
|
||||||
this.setState({ item: {} });
|
this.setState({ item: {}, relatedItems: [] });
|
||||||
|
// Clear hash when returning to main view
|
||||||
|
updateHash('#marketplace');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async getItems() {
|
async getItems() {
|
||||||
this.setState({ done: false });
|
this.setState({ done: false });
|
||||||
|
|
||||||
|
// Use API v2 endpoints
|
||||||
const dataURL =
|
const dataURL =
|
||||||
this.props.type === 'collections'
|
this.props.type === 'collections'
|
||||||
? variables.constants.API_URL + '/marketplace/collections'
|
? `${API_V2_BASE}/collections`
|
||||||
: variables.constants.API_URL + '/marketplace/items/' + this.props.type;
|
: `${API_V2_BASE}/items/${this.props.type}`;
|
||||||
|
|
||||||
const { data } = await (await fetch(dataURL, { signal: this.controller.signal })).json();
|
const { data } = await (await fetch(dataURL, { signal: this.controller.signal })).json();
|
||||||
const collections = await (
|
const collections = await (
|
||||||
await fetch(variables.constants.API_URL + '/marketplace/collections', {
|
await fetch(`${API_V2_BASE}/collections`, {
|
||||||
signal: this.controller.signal,
|
signal: this.controller.signal,
|
||||||
})
|
})
|
||||||
).json();
|
).json();
|
||||||
@@ -177,11 +235,18 @@ class Marketplace extends PureComponent {
|
|||||||
const installed = JSON.parse(localStorage.getItem('installed'));
|
const installed = JSON.parse(localStorage.getItem('installed'));
|
||||||
for (const item of this.state.items) {
|
for (const item of this.state.items) {
|
||||||
if (installed.some((i) => i.name === item.display_name)) continue; // don't install if already installed
|
if (installed.some((i) => i.name === item.display_name)) continue; // don't install if already installed
|
||||||
|
|
||||||
|
// Use API v2 - fetch by ID if available, otherwise by name
|
||||||
|
const itemEndpoint = item.id
|
||||||
|
? `${API_V2_BASE}/item/${item.id}`
|
||||||
|
: `${API_V2_BASE}/item/${item.type}/${item.name}`;
|
||||||
|
|
||||||
const { data } = await (
|
const { data } = await (
|
||||||
await fetch(`${variables.constants.API_URL}/marketplace/item/${item.type}/${item.name}`, {
|
await fetch(itemEndpoint, {
|
||||||
signal: this.controller.signal,
|
signal: this.controller.signal,
|
||||||
})
|
})
|
||||||
).json();
|
).json();
|
||||||
|
|
||||||
install(data.type, data, false, true);
|
install(data.type, data, false, true);
|
||||||
variables.stats.postEvent('marketplace-item', `${item.display_name} installed}`);
|
variables.stats.postEvent('marketplace-item', `${item.display_name} installed}`);
|
||||||
variables.stats.postEvent('marketplace', 'Install');
|
variables.stats.postEvent('marketplace', 'Install');
|
||||||
@@ -248,6 +313,22 @@ class Marketplace extends PureComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.getItems();
|
this.getItems();
|
||||||
|
|
||||||
|
// Handle deep link data if provided
|
||||||
|
if (this.props.deepLinkData) {
|
||||||
|
const { itemId, collection, category } = this.props.deepLinkData;
|
||||||
|
|
||||||
|
// Wait for items to load, then open the specific item or collection
|
||||||
|
setTimeout(() => {
|
||||||
|
if (collection) {
|
||||||
|
// Open collection
|
||||||
|
this.toggle('collection', collection);
|
||||||
|
} else if (itemId) {
|
||||||
|
// Open specific item by ID
|
||||||
|
this.toggle('item', { id: itemId, type: category });
|
||||||
|
}
|
||||||
|
}, 500);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
@@ -316,6 +397,7 @@ class Marketplace extends PureComponent {
|
|||||||
addonInstalled={this.state.item.addonInstalled}
|
addonInstalled={this.state.item.addonInstalled}
|
||||||
addonInstalledVersion={this.state.item.addonInstalledVersion}
|
addonInstalledVersion={this.state.item.addonInstalledVersion}
|
||||||
icon={this.state.item.screenshot_url}
|
icon={this.state.item.screenshot_url}
|
||||||
|
relatedItems={this.state.relatedItems}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -405,7 +405,7 @@ class ItemPage extends PureComponent {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{moreByCurator.length > 1 && (
|
{/* {moreByCurator.length > 1 && (
|
||||||
<div className="moreFromCurator">
|
<div className="moreFromCurator">
|
||||||
<span className="title">
|
<span className="title">
|
||||||
{variables.getMessage('modals.main.marketplace.product.more_from_curator', {
|
{variables.getMessage('modals.main.marketplace.product.more_from_curator', {
|
||||||
@@ -426,6 +426,26 @@ class ItemPage extends PureComponent {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
)} */}
|
||||||
|
{this.props.relatedItems && this.props.relatedItems.length > 0 && (
|
||||||
|
<div className="moreFromCurator">
|
||||||
|
<span className="title">
|
||||||
|
{variables.getMessage('modals.main.marketplace.you_might_also_like') ||
|
||||||
|
'You might also like'}
|
||||||
|
</span>
|
||||||
|
<div>
|
||||||
|
<Items
|
||||||
|
type={this.props.data.data.type}
|
||||||
|
items={this.props.relatedItems}
|
||||||
|
onCollection={false}
|
||||||
|
toggleFunction={(input) => this.props.toggleFunction('item', input)}
|
||||||
|
collectionFunction={(input) => this.props.toggleFunction('collection', input)}
|
||||||
|
filter={''}
|
||||||
|
moreByCreator={false}
|
||||||
|
showCreateYourOwn={false}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import Navbar from '../../navbar/Navbar';
|
|||||||
import Preview from '../../helpers/preview/Preview';
|
import Preview from '../../helpers/preview/Preview';
|
||||||
|
|
||||||
import EventBus from 'utils/eventbus';
|
import EventBus from 'utils/eventbus';
|
||||||
|
import { parseDeepLink, shouldAutoOpenModal } from 'utils/deepLinking';
|
||||||
|
|
||||||
import Welcome from 'features/welcome/Welcome';
|
import Welcome from 'features/welcome/Welcome';
|
||||||
|
|
||||||
@@ -19,10 +20,22 @@ export default class Modals extends PureComponent {
|
|||||||
welcomeModal: false,
|
welcomeModal: false,
|
||||||
appsModal: false,
|
appsModal: false,
|
||||||
preview: false,
|
preview: false,
|
||||||
|
deepLinkData: null,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
|
// Check for deep link first (has priority)
|
||||||
|
if (shouldAutoOpenModal()) {
|
||||||
|
const deepLinkData = parseDeepLink();
|
||||||
|
this.setState({
|
||||||
|
mainModal: true,
|
||||||
|
deepLinkData,
|
||||||
|
});
|
||||||
|
variables.stats.postEvent('modal', `Opened via deep link: ${deepLinkData.tab}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
localStorage.getItem('showWelcome') === 'true' &&
|
localStorage.getItem('showWelcome') === 'true' &&
|
||||||
window.location.search !== '?nointro=true'
|
window.location.search !== '?nointro=true'
|
||||||
@@ -90,7 +103,10 @@ export default class Modals extends PureComponent {
|
|||||||
overlayClassName="Overlay"
|
overlayClassName="Overlay"
|
||||||
ariaHideApp={false}
|
ariaHideApp={false}
|
||||||
>
|
>
|
||||||
<MainModal modalClose={() => this.toggleModal('mainModal', false)} />
|
<MainModal
|
||||||
|
modalClose={() => this.toggleModal('mainModal', false)}
|
||||||
|
deepLinkData={this.state.deepLinkData}
|
||||||
|
/>
|
||||||
</Modal>
|
</Modal>
|
||||||
<Modal
|
<Modal
|
||||||
closeTimeoutMS={300}
|
closeTimeoutMS={300}
|
||||||
|
|||||||
@@ -4,26 +4,26 @@ import { memo } from 'react';
|
|||||||
import Tabs from '../../../components/Elements/MainModal/backend/Tabs';
|
import Tabs from '../../../components/Elements/MainModal/backend/Tabs';
|
||||||
import MarketplaceTab from '../../marketplace/views/Browse';
|
import MarketplaceTab from '../../marketplace/views/Browse';
|
||||||
|
|
||||||
function Marketplace(props) {
|
function Marketplace({ changeTab, deepLinkData }) {
|
||||||
return (
|
return (
|
||||||
<Tabs changeTab={(type) => props.changeTab(type)} current="marketplace">
|
<Tabs changeTab={(type) => changeTab(type)} current="marketplace">
|
||||||
<div label={variables.getMessage('modals.main.marketplace.all')} name="all">
|
<div label={variables.getMessage('modals.main.marketplace.all')} name="all">
|
||||||
<MarketplaceTab type="all" />
|
<MarketplaceTab type="all" deepLinkData={deepLinkData} />
|
||||||
</div>
|
</div>
|
||||||
<div label={variables.getMessage('modals.main.marketplace.photo_packs')} name="photo_packs">
|
<div label={variables.getMessage('modals.main.marketplace.photo_packs')} name="photo_packs">
|
||||||
<MarketplaceTab type="photo_packs" />
|
<MarketplaceTab type="photo_packs" deepLinkData={deepLinkData} />
|
||||||
</div>
|
</div>
|
||||||
<div label={variables.getMessage('modals.main.marketplace.quote_packs')} name="quote_packs">
|
<div label={variables.getMessage('modals.main.marketplace.quote_packs')} name="quote_packs">
|
||||||
<MarketplaceTab type="quote_packs" />
|
<MarketplaceTab type="quote_packs" deepLinkData={deepLinkData} />
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
label={variables.getMessage('modals.main.marketplace.preset_settings')}
|
label={variables.getMessage('modals.main.marketplace.preset_settings')}
|
||||||
name="preset_settings"
|
name="preset_settings"
|
||||||
>
|
>
|
||||||
<MarketplaceTab type="preset_settings" />
|
<MarketplaceTab type="preset_settings" deepLinkData={deepLinkData} />
|
||||||
</div>
|
</div>
|
||||||
<div label={variables.getMessage('modals.main.marketplace.collections')} name="collections">
|
<div label={variables.getMessage('modals.main.marketplace.collections')} name="collections">
|
||||||
<MarketplaceTab type="collections" />
|
<MarketplaceTab type="collections" deepLinkData={deepLinkData} />
|
||||||
</div>
|
</div>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
);
|
);
|
||||||
|
|||||||
154
src/utils/deepLinking.js
Normal file
154
src/utils/deepLinking.js
Normal file
@@ -0,0 +1,154 @@
|
|||||||
|
/**
|
||||||
|
* Deep linking utilities for opening specific marketplace items or tabs
|
||||||
|
* from external sources (e.g., website)
|
||||||
|
*
|
||||||
|
* Updated for Marketplace API v2 with item IDs
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { TAB_TYPES } from '../components/Elements/MainModal/constants/tabConfig';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse hash from URL
|
||||||
|
* Examples (NEW API v2 format):
|
||||||
|
* #marketplace/f41219846700 -> { tab: 'marketplace', itemId: 'f41219846700' }
|
||||||
|
* #marketplace/preset_settings/f41219846700 -> { tab: 'marketplace', category: 'preset_settings', itemId: 'f41219846700' }
|
||||||
|
* #marketplace/collection/featured -> { tab: 'marketplace', collection: 'featured' }
|
||||||
|
* #settings/appearance -> { tab: 'settings', section: 'appearance' }
|
||||||
|
* #addons -> { tab: 'addons' }
|
||||||
|
*
|
||||||
|
* Legacy format (still supported):
|
||||||
|
* #marketplace/quote_packs/digital-stoicism -> converted to item lookup
|
||||||
|
*/
|
||||||
|
export const parseDeepLink = (hash = window.location.hash) => {
|
||||||
|
if (!hash || hash === '#') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove the # symbol
|
||||||
|
const path = hash.slice(1);
|
||||||
|
const parts = path.split('/');
|
||||||
|
|
||||||
|
const result = {
|
||||||
|
tab: parts[0],
|
||||||
|
section: parts[1],
|
||||||
|
itemId: parts[2],
|
||||||
|
};
|
||||||
|
|
||||||
|
// Validate tab
|
||||||
|
const validTabs = Object.values(TAB_TYPES);
|
||||||
|
if (!validTabs.includes(result.tab)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle marketplace-specific parsing
|
||||||
|
if (result.tab === 'marketplace') {
|
||||||
|
// Check if it's a collection
|
||||||
|
if (result.section === 'collection') {
|
||||||
|
result.collection = result.itemId;
|
||||||
|
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
|
||||||
|
else if (result.section && !result.itemId) {
|
||||||
|
result.itemId = result.section;
|
||||||
|
result.section = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a deep link hash
|
||||||
|
* @param {string} tab - The main tab (settings, marketplace, 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.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) {
|
||||||
|
hash += `/collection/${options.collection}`;
|
||||||
|
}
|
||||||
|
// Item with category
|
||||||
|
else if (options.itemId && options.category) {
|
||||||
|
hash += `/${options.category}/${options.itemId}`;
|
||||||
|
}
|
||||||
|
// Item without category (direct ID)
|
||||||
|
else if (options.itemId) {
|
||||||
|
hash += `/${options.itemId}`;
|
||||||
|
}
|
||||||
|
// Category only
|
||||||
|
else if (options.category) {
|
||||||
|
hash += `/${options.category}`;
|
||||||
|
}
|
||||||
|
} else if (options.section) {
|
||||||
|
hash += `/${options.section}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return hash;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update URL hash without triggering page reload
|
||||||
|
*/
|
||||||
|
export const updateHash = (hash) => {
|
||||||
|
if (window.history.pushState) {
|
||||||
|
window.history.pushState(null, null, hash);
|
||||||
|
} else {
|
||||||
|
window.location.hash = hash;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Listen for hash changes
|
||||||
|
*/
|
||||||
|
export const onHashChange = (callback) => {
|
||||||
|
const handler = () => {
|
||||||
|
const deepLink = parseDeepLink();
|
||||||
|
if (deepLink) {
|
||||||
|
callback(deepLink);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener('hashchange', handler);
|
||||||
|
|
||||||
|
// Return cleanup function
|
||||||
|
return () => window.removeEventListener('hashchange', handler);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if extension should open modal on load based on hash
|
||||||
|
*/
|
||||||
|
export const shouldAutoOpenModal = () => {
|
||||||
|
const deepLink = parseDeepLink();
|
||||||
|
return deepLink !== null;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert item name to ID (for backward compatibility)
|
||||||
|
* This requires fetching from API to resolve name -> ID
|
||||||
|
* @param {string} category - Item category
|
||||||
|
* @param {string} name - Item name
|
||||||
|
* @returns {Promise<string|null>} Item ID or null
|
||||||
|
*/
|
||||||
|
export const resolveItemNameToId = async (category, name, apiUrl) => {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${apiUrl}/v2/marketplace/item/${category}/${name}`);
|
||||||
|
const data = await response.json();
|
||||||
|
return data?.data?.id || null;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to resolve item name to ID:', error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user