Refactor marketplace components to use hooks and context

- Converted ItemPage from a class component to a functional component using hooks for state management.
- Introduced MarketplaceContext for managing marketplace-related state and actions.
- Created custom hooks: useMarketplaceData for fetching and sorting marketplace items, useMarketplaceInstall for handling installation and uninstallation of items, and useMarketplaceNavigation for managing navigation within the marketplace.
- Implemented BrowsePage and CollectionPage components to display items and collections with improved structure and functionality.
- Enhanced error handling and loading states across the marketplace features.
This commit is contained in:
alexsparkes
2025-11-03 14:26:12 +00:00
parent 59824eb6ef
commit 2c376c7506
9 changed files with 1108 additions and 1038 deletions

View File

@@ -0,0 +1,13 @@
import { createContext, useContext } from 'react';
const MarketplaceContext = createContext(null);
export const useMarketplace = () => {
const context = useContext(MarketplaceContext);
if (!context) {
throw new Error('useMarketplace must be used within a MarketplaceProvider');
}
return context;
};
export { MarketplaceContext };

View File

@@ -0,0 +1,104 @@
import { useState, useEffect, useRef } from 'react';
import variables from 'config/variables';
const API_V2_BASE = `${variables.constants.API_URL}/marketplace`;
export const useMarketplaceData = (type, deepLinkData) => {
const [items, setItems] = useState([]);
const [collections, setCollections] = useState([]);
const [displayedCollection, setDisplayedCollection] = useState({});
const [sortType, setSortType] = useState('a-z');
const [loading, setLoading] = useState(false);
const controllerRef = useRef(new AbortController());
const sortItems = (itemsToSort, sortValue) => {
if (!itemsToSort) return [];
const sorted = [...itemsToSort];
const value = sortValue || localStorage.getItem('sortMarketplace') || 'a-z';
switch (value) {
case 'a-z':
sorted.sort((a, b) => {
if (a.display_name < b.display_name) return -1;
if (a.display_name > b.display_name) return 1;
return 0;
});
break;
case 'z-a':
sorted.sort((a, b) => {
if (a.display_name < b.display_name) return -1;
if (a.display_name > b.display_name) return 1;
return 0;
});
sorted.reverse();
break;
default:
break;
}
return sorted;
};
const fetchItems = async () => {
setLoading(true);
try {
const dataURL =
type === 'collections' ? `${API_V2_BASE}/collections` : `${API_V2_BASE}/items/${type}`;
const [itemsResponse, collectionsResponse] = await Promise.all([
fetch(dataURL, { signal: controllerRef.current.signal }),
fetch(`${API_V2_BASE}/collections`, { signal: controllerRef.current.signal }),
]);
const itemsData = await itemsResponse.json();
const collectionsData = await collectionsResponse.json();
if (controllerRef.current.signal.aborted) return;
const sortedItems = sortItems(itemsData.data, sortType);
setItems(sortedItems);
setCollections(collectionsData.data);
setDisplayedCollection(
collectionsData.data[Math.floor(Math.random() * collectionsData.data.length)] || {},
);
} catch (error) {
if (!controllerRef.current.signal.aborted) {
console.error('Failed to fetch marketplace data:', error);
}
} finally {
setLoading(false);
}
};
const changeSort = (value) => {
localStorage.setItem('sortMarketplace', value);
const sorted = sortItems(items, value);
setItems(sorted);
setSortType(value);
variables.stats.postEvent('marketplace', 'Sort');
};
useEffect(() => {
if (navigator.onLine === false || localStorage.getItem('offlineMode') === 'true') {
return;
}
fetchItems();
return () => {
controllerRef.current.abort();
};
}, [type]);
return {
items,
collections,
displayedCollection,
sortType,
loading,
changeSort,
refetch: fetchItems,
};
};

View File

@@ -0,0 +1,71 @@
import { useState, useRef } from 'react';
import { toast } from 'react-toastify';
import variables from 'config/variables';
import { install, uninstall } from 'utils/marketplace';
const API_V2_BASE = `${variables.constants.API_URL}/marketplace`;
export const useMarketplaceInstall = () => {
const [busy, setBusy] = useState(false);
const controllerRef = useRef(new AbortController());
const installItem = (type, data) => {
install(type, data);
toast(variables.getMessage('toasts.installed'));
variables.stats.postEvent('marketplace-item', `${data.display_name || data.name} installed`);
variables.stats.postEvent('marketplace', 'Install');
};
const uninstallItem = (type, name) => {
uninstall(type, name);
toast(variables.getMessage('toasts.uninstalled'));
variables.stats.postEvent('marketplace-item', `${name} uninstalled`);
variables.stats.postEvent('marketplace', 'Uninstall');
};
const installCollection = async (items) => {
setBusy(true);
try {
const installed = JSON.parse(localStorage.getItem('installed')) || [];
for (const item of items) {
// Skip if already installed
if (installed.some((i) => i.name === item.display_name)) {
continue;
}
// Fetch full item data
const itemEndpoint = item.id
? `${API_V2_BASE}/item/${item.id}`
: `${API_V2_BASE}/item/${item.type}/${item.name}`;
const response = await fetch(itemEndpoint, {
signal: controllerRef.current.signal,
});
const { data } = await response.json();
// Install item
install(data.type, data, false, true);
variables.stats.postEvent('marketplace-item', `${item.display_name} installed`);
variables.stats.postEvent('marketplace', 'Install');
}
toast(variables.getMessage('toasts.installed'));
window.location.reload();
} catch (error) {
if (!controllerRef.current.signal.aborted) {
console.error('Failed to install collection:', error);
toast(variables.getMessage('toasts.error'));
}
} finally {
setBusy(false);
}
};
return {
busy,
installItem,
uninstallItem,
installCollection,
};
};

View File

@@ -0,0 +1,223 @@
import { useState, useRef, useCallback } from 'react';
import { toast } from 'react-toastify';
import variables from 'config/variables';
import { urlParser } from 'utils/marketplace';
import { updateHash } from 'utils/deepLinking';
const API_V2_BASE = `${variables.constants.API_URL}/marketplace`;
export const useMarketplaceNavigation = (onProductView, onResetToAll) => {
const [currentView, setCurrentView] = useState('browse'); // 'browse', 'collection', 'item'
const [currentItem, setCurrentItem] = useState(null);
const [currentCollection, setCurrentCollection] = useState(null);
const [relatedItems, setRelatedItems] = useState([]);
const controllerRef = useRef(new AbortController());
const navigateToItem = useCallback(
async (data, type) => {
try {
let itemType = type;
if (type === 'all' || type === 'collections') {
itemType = data.type;
}
// Fetch item details
const itemEndpoint = data.id
? `${API_V2_BASE}/item/${data.id}`
: `${API_V2_BASE}/item/${itemType}/${data.name}`;
const response = await fetch(itemEndpoint, {
signal: controllerRef.current.signal,
});
const info = await response.json();
if (!info || !info.data) {
throw new Error('Invalid item data received');
}
// Fetch related items
let related = [];
if (info.data?.id) {
try {
const relatedResponse = await fetch(`${API_V2_BASE}/item/${info.data.id}/related`, {
signal: controllerRef.current.signal,
});
const relatedData = await relatedResponse.json();
related = relatedData.data?.related || [];
} catch (error) {
console.warn('Failed to fetch related items:', error);
}
// Track view
fetch(`${API_V2_BASE}/item/${info.data.id}/view`, {
method: 'POST',
signal: controllerRef.current.signal,
}).catch(() => {});
}
// Check if installed
const installed = JSON.parse(localStorage.getItem('installed')) || [];
const isInstalled = installed.some((item) => item.name === info.data.name);
const installedVersion = installed.find((item) => item.name === info.data.name)?.version;
const itemData = {
id: info.data.id,
onCollection: data._onCollection,
type: info.data.type,
display_name: info.data.name || info.data.display_name,
author: info.data.author,
description: urlParser(info.data.description.replace(/\n/g, '<br>')),
version: info.data.version,
icon: info.data.screenshot_url,
data: info.data,
addonInstalled: isInstalled,
addonInstalledVersion: installedVersion,
api_name: data.name,
};
setCurrentItem(itemData);
setRelatedItems(related);
setCurrentView('item');
// Update hash
if (info.data?.id) {
if (currentCollection?.name) {
updateHash(`#discover/collection/${currentCollection.name}/${info.data.id}`);
} else {
updateHash(`#discover/${info.data.type}/${info.data.id}`);
}
}
// Notify parent
if (onProductView) {
onProductView({
id: info.data.id,
type: info.data.type,
name: info.data.display_name || info.data.name,
fromCollection: !!currentCollection,
collectionName: currentCollection?.name,
collectionTitle: currentCollection?.title,
collectionDescription: currentCollection?.description,
collectionImg: currentCollection?.img,
onBack: () => navigateBack(),
onBackToAll: () => navigateToMain(true),
});
}
document.querySelector('#modal')?.scrollTo(0, 0);
variables.stats.postEvent('marketplace-item', `${itemData.display_name} viewed`);
} catch (error) {
if (!controllerRef.current.signal.aborted) {
console.error('Failed to fetch item:', error);
toast(variables.getMessage('toasts.error'));
}
}
},
[currentCollection, onProductView],
);
const navigateToCollection = useCallback(
async (collectionName) => {
try {
const response = await fetch(`${API_V2_BASE}/collection/${collectionName}`, {
signal: controllerRef.current.signal,
});
const collection = await response.json();
if (controllerRef.current.signal.aborted) return;
const collectionData = {
name: collectionName,
title: collection.data.display_name,
description: collection.data.description,
img: collection.data.img,
items: collection.data.items,
};
setCurrentCollection(collectionData);
setCurrentView('collection');
setCurrentItem(null);
setRelatedItems([]);
// Update hash
updateHash(`#discover/collection/${collectionName}`);
// Notify parent
if (onProductView) {
onProductView({
id: collectionName,
type: 'collection',
name: collection.data.display_name,
isCollection: true,
fromCollection: false,
collectionName: collectionName,
collectionTitle: collection.data.display_name,
onBack: () => navigateToMain(false),
onBackToAll: () => navigateToMain(true),
});
}
} catch (error) {
if (!controllerRef.current.signal.aborted) {
console.error('Failed to fetch collection:', error);
toast(variables.getMessage('toasts.error'));
}
}
},
[onProductView],
);
const navigateBack = useCallback(() => {
if (currentView === 'item' && currentCollection) {
// Go back to collection
setCurrentView('collection');
setCurrentItem(null);
setRelatedItems([]);
updateHash(`#discover/collection/${currentCollection.name}`);
} else {
// Go back to browse
navigateToMain(false);
}
if (onProductView) {
onProductView(null);
}
}, [currentView, currentCollection, onProductView]);
const navigateToMain = useCallback(
(clearCollection) => {
if (clearCollection) {
setCurrentCollection(null);
}
setCurrentView('browse');
setCurrentItem(null);
setRelatedItems([]);
// Update hash based on collection state
if (!clearCollection && currentCollection) {
updateHash(`#discover/collection/${currentCollection.name}`);
} else {
updateHash('#discover/all');
}
if (onProductView) {
onProductView(null);
}
if (clearCollection && onResetToAll) {
onResetToAll();
}
},
[currentCollection, onProductView, onResetToAll],
);
return {
currentView,
currentItem,
currentCollection,
relatedItems,
navigateToItem,
navigateToCollection,
navigateBack,
navigateToMain,
};
};

View File

@@ -1,667 +1,222 @@
import variables from 'config/variables';
import { PureComponent } from 'react';
import { toast } from 'react-toastify';
import {
MdWifiOff,
MdLocalMall,
MdClose,
MdSearch,
MdOutlineArrowForward,
MdLibraryAdd,
} from 'react-icons/md';
import { useState, useEffect } from 'react';
import { MdWifiOff, MdLocalMall } from 'react-icons/md';
import ItemPage from './ItemPage';
import Items from '../components/Items/Items';
import { Button } from 'components/Elements';
import CollectionPage from './CollectionPage';
import BrowsePage from './BrowsePage';
import InstallButton from '../components/Elements/InstallButton';
import { install, urlParser, uninstall } from 'utils/marketplace';
import { updateHash } from 'utils/deepLinking';
import { useMarketplaceData } from '../hooks/useMarketplaceData';
import { useMarketplaceNavigation } from '../hooks/useMarketplaceNavigation';
import { useMarketplaceInstall } from '../hooks/useMarketplaceInstall';
import { MarketplaceContext } from '../contexts/MarketplaceContext';
// API v2 base URL
const API_V2_BASE = `${variables.constants.API_URL}/marketplace`;
const Marketplace = ({ type, onProductView, onResetToAll, deepLinkData, navigationTrigger }) => {
const [filter, setFilter] = useState('');
class Marketplace extends PureComponent {
constructor(props) {
super(props);
this.state = {
items: [],
relatedItems: [],
button: '',
done: false,
item: {},
collection: false,
filter: '',
};
this.buttons = {
uninstall: (
<InstallButton
onClick={() => this.manage('uninstall')}
isInstalled={true}
label={variables.getMessage('modals.main.marketplace.product.buttons.remove')}
/>
),
install: (
<InstallButton
onClick={() => this.manage('install')}
isInstalled={false}
label={variables.getMessage('modals.main.marketplace.product.buttons.addtomue')}
/>
),
};
this.controller = new AbortController();
}
// Custom hooks for data, navigation, and installation
const { items, displayedCollection, loading, changeSort } = useMarketplaceData(type, deepLinkData);
const {
currentView,
currentItem,
currentCollection,
relatedItems,
navigateToItem,
navigateToCollection,
navigateBack,
navigateToMain,
} = useMarketplaceNavigation(onProductView, onResetToAll);
const { busy, installItem, uninstallItem, installCollection } = useMarketplaceInstall();
async toggle(pageType, data) {
if (pageType === 'item') {
let info;
let relatedItems = [];
const handleManage = (action) => {
if (!currentItem) return;
// get item info using API v2
try {
let type = this.props.type;
if (type === 'all' || type === 'collections') {
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 (
await fetch(itemEndpoint, {
signal: this.controller.signal,
})
).json();
// 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) {
console.error('Failed to fetch item:', 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')) || [];
if (installed.some((item) => item.name === info.data.name)) {
button = this.buttons.uninstall;
addonInstalled = true;
for (let i = 0; i < installed.length; i++) {
if (installed[i].name === info.data.name) {
addonInstalledVersion = installed[i].version;
break;
}
}
}
this.setState({
item: {
id: info.data.id, // Store item ID for deep linking
onCollection: data._onCollection,
type: info.data.type,
display_name: info.data.name || info.data.display_name,
author: info.data.author,
description: urlParser(info.data.description.replace(/\n/g, '<br>')),
version: info.data.version,
icon: info.data.screenshot_url,
data: info.data,
addonInstalled,
addonInstalledVersion,
api_name: data.name,
},
relatedItems,
button: button,
});
// Update URL hash with item ID for deep linking
if (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) {
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: () => {
// 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;
variables.stats.postEvent('marketplace-item', `${this.state.item.display_name} viewed`);
} else if (pageType === 'collection') {
this.setState({ done: false, item: {} });
// Use API v2 for collections
const collection = await (
await fetch(`${API_V2_BASE}/collection/${data}`, {
signal: this.controller.signal,
})
).json();
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);
}
if (action === 'install') {
installItem(currentItem.type, currentItem.data);
} else {
this.setState({ item: {}, relatedItems: [] });
// 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) {
this.props.onProductView(null);
}
uninstallItem(currentItem.type, currentItem.display_name);
}
}
};
async getItems() {
this.setState({ done: false });
// Install/uninstall buttons
const buttons = {
uninstall: (
<InstallButton
onClick={() => handleManage('uninstall')}
isInstalled={true}
label={variables.getMessage('modals.main.marketplace.product.buttons.remove')}
/>
),
install: (
<InstallButton
onClick={() => handleManage('install')}
isInstalled={false}
label={variables.getMessage('modals.main.marketplace.product.buttons.addtomue')}
/>
),
};
// Use API v2 endpoints
const dataURL =
this.props.type === 'collections'
? `${API_V2_BASE}/collections`
: `${API_V2_BASE}/items/${this.props.type}`;
// Get appropriate button based on installation status
const getButton = () => {
if (!currentItem) return buttons.install;
return currentItem.addonInstalled ? buttons.uninstall : buttons.install;
};
const { data } = await (await fetch(dataURL, { signal: this.controller.signal })).json();
const collections = await (
await fetch(`${API_V2_BASE}/collections`, {
signal: this.controller.signal,
})
).json();
// Handle deep linking on mount
useEffect(() => {
if (deepLinkData) {
const { itemId, collection, category } = deepLinkData;
if (this.controller.signal.aborted === true) {
return;
}
const sorted = this.sortMarketplace(data, false);
this.setState({
items: sorted.items,
sortType: sorted.sortType,
oldItems: sorted.items,
collections: collections.data,
displayedCollection:
collections.data[Math.floor(Math.random() * collections.data.length)] || [],
done: true,
});
}
manage(type) {
if (type === 'install') {
install(this.state.item.type, this.state.item.data);
} else {
uninstall(this.state.item.type, this.state.item.display_name);
}
toast(variables.getMessage('toasts.' + type + 'ed'));
this.setState({ button: type === 'install' ? this.buttons.uninstall : this.buttons.install });
variables.stats.postEvent(
'marketplace-item',
`${this.state.item.display_name} ${type === 'install' ? 'installed' : 'uninstalled'}`,
);
variables.stats.postEvent('marketplace', type === 'install' ? 'Install' : 'Uninstall');
}
async installCollection() {
this.setState({ busy: true });
try {
const installed = JSON.parse(localStorage.getItem('installed'));
for (const item of this.state.items) {
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 (
await fetch(itemEndpoint, {
signal: this.controller.signal,
})
).json();
install(data.type, data, false, true);
variables.stats.postEvent('marketplace-item', `${item.display_name} installed}`);
variables.stats.postEvent('marketplace', 'Install');
}
toast(variables.getMessage('toasts.installed'));
window.location.reload();
} catch (error) {
console.error(error);
toast(variables.getMessage('toasts.error'));
} finally {
this.setState({ busy: false });
}
}
sortMarketplace(data, sendEvent) {
const value = localStorage.getItem('sortMarketplace') || 'a-z';
let items = data || this.state.items;
if (!items) {
return;
}
switch (value) {
case 'a-z': {
// sort by name key alphabetically
const sorted = items.sort((a, b) => {
if (a.display_name < b.display_name) {
return -1;
}
if (a.display_name > b.display_name) {
return 1;
}
return 0;
});
items = sorted;
break;
}
case 'z-a':
items.sort();
items.reverse();
break;
default:
break;
}
if (sendEvent) {
variables.stats.postEvent('marketplace', 'Sort');
}
return { items: items, sortType: value };
}
changeSort(value) {
localStorage.setItem('sortMarketplace', value);
this.setState(this.sortMarketplace(null, true));
}
returnToMain() {
this.setState({
items: this.state.oldItems,
collection: false,
collectionName: null,
collectionTitle: null,
collectionDescription: null,
collectionImg: null,
});
updateHash(`#discover/${this.props.type}`);
}
componentDidMount() {
if (navigator.onLine === false || localStorage.getItem('offlineMode') === 'true') {
return;
}
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);
navigateToCollection(collection);
} else if (itemId) {
// Open specific item by ID
this.toggle('item', { id: itemId, type: category });
navigateToItem({ id: itemId, type: category }, type);
}
}, 500);
}
}
}, [deepLinkData, navigateToCollection, navigateToItem, type]);
componentDidUpdate(prevProps) {
// Handle navigation trigger changes
if (
this.props.navigationTrigger &&
this.props.navigationTrigger !== prevProps.navigationTrigger
) {
const { type, data } = this.props.navigationTrigger;
// Handle navigation trigger changes
useEffect(() => {
if (navigationTrigger) {
const { type: navType, data } = navigationTrigger;
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)
// 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');
}
if (navType === 'product' && data) {
navigateToItem({ id: data.id, type: data.type }, type);
} else if (navType === 'collection' && data) {
navigateToCollection(data);
} else if (navType === 'main' || navType === 'section') {
const clearCollection = data?.clearCollection && currentCollection;
navigateToMain(clearCollection);
}
}
}
}, [navigationTrigger, navigateToItem, navigateToCollection, navigateToMain, currentCollection, type]);
componentWillUnmount() {
// stop making requests
this.controller.abort();
}
// Context value to share with child components
const contextValue = {
currentView,
currentItem,
currentCollection,
items,
navigateToItem: (data) => navigateToItem(data, type),
navigateToCollection,
navigateBack,
navigateToMain,
installItem,
uninstallItem,
};
render() {
const errorMessage = (msg) => {
return (
<>
<div className="flexTopMarketplace">
<span className="mainTitle">
{variables.getMessage('modals.main.navbar.marketplace')}
</span>
</div>
<div className="emptyItems">
<div className="emptyMessage">{msg}</div>
</div>
</>
);
};
// Error message component
const renderError = (content) => (
<>
<div className="flexTopMarketplace">
<span className="mainTitle">{variables.getMessage('modals.main.navbar.marketplace')}</span>
</div>
<div className="emptyItems">
<div className="emptyMessage">{content}</div>
</div>
</>
);
if (navigator.onLine === false || localStorage.getItem('offlineMode') === 'true') {
return errorMessage(
<>
<MdWifiOff />
<span className="title">
{variables.getMessage('modals.main.marketplace.offline.title')}
</span>
<span className="subtitle">
{variables.getMessage('modals.main.marketplace.offline.description')}
</span>
</>,
);
}
if (this.state.done === false) {
return errorMessage(
<div className="loaderHolder">
<div id="loader"></div>
<span className="subtitle">{variables.getMessage('modals.main.loading')}</span>
</div>,
);
}
if (!this.state.items || this.state.items?.length === 0) {
this.getItems();
return errorMessage(
<>
<MdLocalMall />
<span className="title">{variables.getMessage('modals.main.addons.empty.title')}</span>
<span className="subtitle">
{variables.getMessage('modals.main.marketplace.no_items')}
</span>
</>,
);
}
if (this.state.item.display_name) {
return (
<ItemPage
data={this.state.item}
button={this.state.button}
toggleFunction={(...args) => this.toggle(...args)}
addonInstalled={this.state.item.addonInstalled}
addonInstalledVersion={this.state.item.addonInstalledVersion}
icon={this.state.item.screenshot_url}
relatedItems={this.state.relatedItems}
/>
);
}
return (
// Offline check
if (navigator.onLine === false || localStorage.getItem('offlineMode') === 'true') {
return renderError(
<>
{this.state.collection === true ? (
<>
<div
className="collectionPage"
style={{
backgroundImage: `linear-gradient(to bottom, transparent, black), url('${this.state.collectionImg}')`,
}}
>
<div className="nice-tag">
{variables.getMessage('modals.main.marketplace.collection')}
</div>
<div className="content">
<span className="mainTitle">{this.state.collectionTitle}</span>
<span className="subtitle">{this.state.collectionDescription}</span>
</div>
<Button
type="collection"
onClick={() => this.installCollection()}
disabled={this.state.busy}
icon={<MdLibraryAdd />}
label={
this.state.busy
? variables.getMessage('modals.main.marketplace.installing')
: variables.getMessage('modals.main.marketplace.add_all')
}
/>
</div>
</>
) : (
<>
{/* <div className="flexTopMarketplace">
<span className="mainTitle">
{variables.getMessage('modals.main.navbar.marketplace')}
</span>
</div> */}
<div className="headerExtras marketplaceCondition">
{this.props.type !== 'collections' && (
<div>
<form className="marketplaceSearch">
<input
label={variables.getMessage('widgets.search')}
placeholder={variables.getMessage('widgets.search')}
name="filter"
id="filter"
value={this.state.filter}
onChange={(event) => this.setState({ filter: event.target.value })}
/>
<MdSearch />
</form>
</div>
)}
</div>
</>
)}
{this.props.type === 'collections' && !this.state.collection ? (
this.state.items.map((item) =>
!item.news ? (
<div
key={item.name}
className="collection"
style={
item.news
? { backgroundColor: item.background_colour }
: {
backgroundImage: `linear-gradient(to left, #000, transparent, #000), url('${item.img}')`,
}
}
>
<div className="content">
<span className="title">{item.display_name}</span>
<span className="subtitle">{item.description}</span>
</div>
<Button
type="collection"
onClick={() => this.toggle('collection', item.name)}
icon={<MdOutlineArrowForward />}
label={variables.getMessage('modals.main.marketplace.explore_collection')}
iconPlacement="right"
/>
</div>
) : null,
)
) : (
<Items
filterOptions={true}
type={this.props.type}
items={this.state.items}
collection={this.state.displayedCollection}
onCollection={this.state.collection}
toggleFunction={(input) => this.toggle('item', input)}
collectionFunction={(input) => this.toggle('collection', input)}
filter={this.state.filter}
onSortChange={(value) => this.changeSort(value)}
showCreateYourOwn={true}
/>
)}
</>
<MdWifiOff />
<span className="title">
{variables.getMessage('modals.main.marketplace.offline.title')}
</span>
<span className="subtitle">
{variables.getMessage('modals.main.marketplace.offline.description')}
</span>
</>,
);
}
}
// Loading state
if (loading) {
return renderError(
<div className="loaderHolder">
<div id="loader"></div>
<span className="subtitle">{variables.getMessage('modals.main.loading')}</span>
</div>,
);
}
// Empty state
if (!items || items.length === 0) {
return renderError(
<>
<MdLocalMall />
<span className="title">{variables.getMessage('modals.main.addons.empty.title')}</span>
<span className="subtitle">
{variables.getMessage('modals.main.marketplace.no_items')}
</span>
</>,
);
}
// Render item view
if (currentView === 'item' && currentItem) {
return (
<MarketplaceContext.Provider value={contextValue}>
<ItemPage
data={currentItem}
button={getButton()}
toggleFunction={(pageType, data) => {
if (pageType === 'collection') {
navigateToCollection(data);
} else if (pageType === 'main') {
navigateBack();
} else {
navigateToItem(data, type);
}
}}
addonInstalled={currentItem.addonInstalled}
addonInstalledVersion={currentItem.addonInstalledVersion}
icon={currentItem.icon}
relatedItems={relatedItems}
/>
</MarketplaceContext.Provider>
);
}
// Render collection view
if (currentView === 'collection' && currentCollection) {
return (
<MarketplaceContext.Provider value={contextValue}>
<CollectionPage
collectionName={currentCollection.name}
collectionTitle={currentCollection.title}
collectionDescription={currentCollection.description}
collectionImg={currentCollection.img}
items={currentCollection.items}
busy={busy}
onInstallCollection={() => installCollection(currentCollection.items)}
onItemClick={(item) => navigateToItem(item, type)}
onSortChange={changeSort}
/>
</MarketplaceContext.Provider>
);
}
// Render browse view
return (
<MarketplaceContext.Provider value={contextValue}>
<BrowsePage
type={type}
items={items}
featuredCollection={displayedCollection}
filter={filter}
onFilterChange={(event) => setFilter(event.target.value)}
onItemClick={(item) => navigateToItem(item, type)}
onCollectionClick={navigateToCollection}
onSortChange={changeSort}
/>
</MarketplaceContext.Provider>
);
};
export default Marketplace;

View File

@@ -0,0 +1,83 @@
import variables from 'config/variables';
import { MdSearch, MdOutlineArrowForward } from 'react-icons/md';
import { Button } from 'components/Elements';
import Items from '../components/Items/Items';
const BrowsePage = ({
type,
items,
featuredCollection,
filter,
onFilterChange,
onItemClick,
onCollectionClick,
onSortChange,
}) => {
return (
<>
<div className="headerExtras marketplaceCondition">
{type !== 'collections' && (
<div>
<form className="marketplaceSearch">
<input
label={variables.getMessage('widgets.search')}
placeholder={variables.getMessage('widgets.search')}
name="filter"
id="filter"
value={filter}
onChange={onFilterChange}
/>
<MdSearch />
</form>
</div>
)}
</div>
{type === 'collections' ? (
items.map((item) =>
!item.news ? (
<div
key={item.name}
className="collection"
style={
item.news
? { backgroundColor: item.background_colour }
: {
backgroundImage: `linear-gradient(to left, #000, transparent, #000), url('${item.img}')`,
}
}
>
<div className="content">
<span className="title">{item.display_name}</span>
<span className="subtitle">{item.description}</span>
</div>
<Button
type="collection"
onClick={() => onCollectionClick(item.name)}
icon={<MdOutlineArrowForward />}
label={variables.getMessage('modals.main.marketplace.explore_collection')}
iconPlacement="right"
/>
</div>
) : null,
)
) : (
<Items
filterOptions={true}
type={type}
items={items}
collection={featuredCollection}
onCollection={false}
toggleFunction={onItemClick}
collectionFunction={onCollectionClick}
filter={filter}
onSortChange={onSortChange}
showCreateYourOwn={true}
/>
)}
</>
);
};
export { BrowsePage as default, BrowsePage };

View File

@@ -0,0 +1,60 @@
import variables from 'config/variables';
import { MdLibraryAdd } from 'react-icons/md';
import { Button } from 'components/Elements';
import Items from '../components/Items/Items';
const CollectionPage = ({
collectionName,
collectionTitle,
collectionDescription,
collectionImg,
items,
busy,
onInstallCollection,
onItemClick,
onSortChange,
}) => {
return (
<>
<div
className="collectionPage"
style={{
backgroundImage: `linear-gradient(to bottom, transparent, black), url('${collectionImg}')`,
}}
>
<div className="nice-tag">{variables.getMessage('modals.main.marketplace.collection')}</div>
<div className="content">
<span className="mainTitle">{collectionTitle}</span>
<span className="subtitle">{collectionDescription}</span>
</div>
<Button
type="collection"
onClick={onInstallCollection}
disabled={busy}
icon={<MdLibraryAdd />}
label={
busy
? variables.getMessage('modals.main.marketplace.installing')
: variables.getMessage('modals.main.marketplace.add_all')
}
/>
</div>
<Items
filterOptions={true}
items={items}
collection={null}
onCollection={true}
toggleFunction={onItemClick}
collectionFunction={() => {}}
filter={''}
onSortChange={onSortChange}
showCreateYourOwn={false}
/>
</>
);
};
export { CollectionPage as default, CollectionPage };

View File

@@ -1,5 +1,5 @@
import variables from 'config/variables';
import { PureComponent, Fragment } from 'react';
import { useState, Fragment } from 'react';
import { toast } from 'react-toastify';
import { MdIosShare, MdFlag, MdAccountCircle } from 'react-icons/md';
import Modal from 'react-modal';
@@ -21,48 +21,23 @@ import PresetsTab from './components/PresetsTab';
import InfoItem from './components/InfoItem';
import WarningBanner from './components/WarningBanner';
class ItemPage extends PureComponent {
constructor(props) {
super(props);
this.state = {
showUpdateButton:
this.props.addonInstalled === true &&
this.props.addonInstalledVersion !== this.props.data.version,
shareModal: false,
count: 5,
moreByCurator: [],
activeTab: 'overview',
};
}
const ItemPage = (props) => {
const [showUpdateButton, setShowUpdateButton] = useState(
props.addonInstalled === true && props.addonInstalledVersion !== props.data.version,
);
const [shareModal, setShareModal] = useState(false);
const [count, setCount] = useState(5);
const [activeTab, setActiveTab] = useState('overview');
async getCurator(name) {
try {
const { data } = await (
await fetch(`${variables.constants.API_URL}/marketplace/curator/${name}`)
).json();
this.setState({
moreByCurator: data.items,
});
} catch (e) {
console.error(e);
}
}
componentDidMount() {
this.getCurator(this.props.data.author);
}
updateAddon() {
uninstall(this.props.data.type, this.props.data.display_name);
install(this.props.data.type, this.props.data);
const updateAddon = () => {
uninstall(props.data.type, props.data.display_name);
install(props.data.type, props.data);
toast(variables.getMessage('toasts.updated'));
this.setState({
showUpdateButton: false,
});
}
setShowUpdateButton(false);
};
incrementCount(type) {
const data = this.props.data.data;
const incrementCount = (type) => {
const data = props.data.data;
let length;
if (type === 'quotes' && Array.isArray(data.quotes)) {
@@ -73,390 +48,376 @@ class ItemPage extends PureComponent {
return;
}
const newCount = this.state.count !== length ? length : 5;
this.setState({ count: newCount });
}
const newCount = count !== length ? length : 5;
setCount(newCount);
};
getName(name) {
const getName = (name) => {
const nameMappings = {
photos: 'photo_packs',
quotes: 'quote_packs',
settings: 'preset_settings',
};
return nameMappings[name] || name;
};
const locale = localStorage.getItem('language');
const shortLocale = locale.includes('_') ? locale.split('_')[0] : locale;
const languageNames = new Intl.DisplayNames([shortLocale], { type: 'language' });
// Extract colour from data (British spelling as used in API)
const mainColor = props.data.data.colour;
// Helper function to determine if a color is light or dark
const isLightColor = (hexColor) => {
if (!hexColor) return false;
// Remove # if present
const hex = hexColor.replace('#', '');
// Convert to RGB
const r = parseInt(hex.substr(0, 2), 16);
const g = parseInt(hex.substr(2, 2), 16);
const b = parseInt(hex.substr(4, 2), 16);
// Calculate relative luminance (perceived brightness)
const luminance = (0.299 * r + 0.587 * g + 0.114 * b) / 255;
return luminance > 0.6; // If > 0.6, it's a light color
};
const isLight = isLightColor(mainColor);
const textColor = isLight ? '#000000' : '#ffffff';
const quotes = Array.isArray(props.data.data.quotes) ? props.data.data.quotes : [];
const photos = Array.isArray(props.data.data.photos) ? props.data.data.photos : [];
const settings = props.data.data.settings;
const hasPhotos = photos.length > 0;
const hasQuotes = quotes.length > 0;
const hasSettings = !!settings;
// Format date for details section
let formattedDate = '';
if (props.data.data.updated_at) {
const dateObj = new Date(props.data.data.updated_at);
formattedDate = new Intl.DateTimeFormat(shortLocale, {
year: 'numeric',
month: 'long',
day: '2-digit',
}).format(dateObj);
}
render() {
const locale = localStorage.getItem('language');
const shortLocale = locale.includes('_') ? locale.split('_')[0] : locale;
const languageNames = new Intl.DisplayNames([shortLocale], { type: 'language' });
// Create dynamic styles for theming with the main color
const themedStyles = mainColor ? (
<style>{`
/* Icon buttons styling */
.iconButtons .btn-icon {
background: ${mainColor} !important;
background-image: none !important;
border-color: ${mainColor} !important;
box-shadow: 0 0 0 1px ${mainColor}, 0 4px 12px ${mainColor}40 !important;
color: ${textColor} !important;
}
.iconButtons .btn-icon:hover {
background: ${mainColor} !important;
filter: brightness(${isLight ? '0.95' : '1.15'});
transform: translateY(-2px);
box-shadow: 0 0 0 1px ${mainColor}, 0 6px 20px ${mainColor}60 !important;
}
// Extract colour from data (British spelling as used in API)
const mainColor = this.props.data.data.colour;
/* ItemInfo background gradient */
.itemInfo {
background: linear-gradient(135deg, ${mainColor}ee 0%, ${mainColor}aa 50%, ${mainColor}66 100%) !important;
box-shadow: 0 8px 32px ${mainColor}40 !important;
}
// Helper function to determine if a color is light or dark
const isLightColor = (hexColor) => {
if (!hexColor) return false;
// Remove # if present
const hex = hexColor.replace('#', '');
// Convert to RGB
const r = parseInt(hex.substr(0, 2), 16);
const g = parseInt(hex.substr(2, 2), 16);
const b = parseInt(hex.substr(4, 2), 16);
// Calculate relative luminance (perceived brightness)
const luminance = (0.299 * r + 0.587 * g + 0.114 * b) / 255;
return luminance > 0.6; // If > 0.6, it's a light color
};
/* Icon shadow */
.itemInfo .icon {
box-shadow: 0 8px 32px ${mainColor}80, 0 0 0 1px ${mainColor}40 !important;
}
const isLight = isLightColor(mainColor);
const textColor = isLight ? '#000000' : '#ffffff';
/* Install button styling */
.itemInfo .installButton {
background: ${mainColor} !important;
background-image: linear-gradient(135deg, ${mainColor} 0%, ${mainColor}dd 100%) !important;
box-shadow: 0 4px 16px ${mainColor}60 !important;
}
const { activeTab } = this.state;
const quotes = Array.isArray(this.props.data.data.quotes) ? this.props.data.data.quotes : [];
const photos = Array.isArray(this.props.data.data.photos) ? this.props.data.data.photos : [];
const settings = this.props.data.data.settings;
const hasPhotos = photos.length > 0;
const hasQuotes = quotes.length > 0;
const hasSettings = !!settings;
.itemInfo .installButton:hover {
background: ${mainColor} !important;
filter: brightness(${isLight ? '0.95' : '1.15'});
box-shadow: 0 6px 24px ${mainColor}80 !important;
}
// Format date for details section
let formattedDate = '';
if (this.props.data.data.updated_at) {
const dateObj = new Date(this.props.data.data.updated_at);
formattedDate = new Intl.DateTimeFormat(shortLocale, {
year: 'numeric',
month: 'long',
day: '2-digit',
}).format(dateObj);
}
/* Install button text and icon color */
.itemInfo .installButton span,
.itemInfo .installButton svg {
color: ${textColor} !important;
}
// Create dynamic styles for theming with the main color
const themedStyles = mainColor ? (
<style>{`
/* Icon buttons styling */
.iconButtons .btn-icon {
background: ${mainColor} !important;
background-image: none !important;
border-color: ${mainColor} !important;
box-shadow: 0 0 0 1px ${mainColor}, 0 4px 12px ${mainColor}40 !important;
color: ${textColor} !important;
}
.iconButtons .btn-icon:hover {
background: ${mainColor} !important;
filter: brightness(${isLight ? '0.95' : '1.15'});
transform: translateY(-2px);
box-shadow: 0 0 0 1px ${mainColor}, 0 6px 20px ${mainColor}60 !important;
}
/* Mue logo - circle matches text color, paths match button color */
.itemInfo .installButton .mueLogo circle {
fill: ${textColor} !important;
}
/* ItemInfo background gradient */
.itemInfo {
background: linear-gradient(135deg, ${mainColor}ee 0%, ${mainColor}aa 50%, ${mainColor}66 100%) !important;
box-shadow: 0 8px 32px ${mainColor}40 !important;
}
.itemInfo .installButton .mueLogo path {
fill: ${mainColor} !important;
}
/* Icon shadow */
.itemInfo .icon {
box-shadow: 0 8px 32px ${mainColor}80, 0 0 0 1px ${mainColor}40 !important;
}
/* Remove the default gradient animation when themed */
.itemInfo .installButton.installed {
background: ${mainColor}aa !important;
background-image: none !important;
}
/* Install button styling */
.itemInfo .installButton {
background: ${mainColor} !important;
background-image: linear-gradient(135deg, ${mainColor} 0%, ${mainColor}dd 100%) !important;
box-shadow: 0 4px 16px ${mainColor}60 !important;
}
.itemInfo .installButton.installed:hover {
background: ${mainColor}99 !important;
}
`}</style>
) : null;
.itemInfo .installButton:hover {
background: ${mainColor} !important;
filter: brightness(${isLight ? '0.95' : '1.15'});
box-shadow: 0 6px 24px ${mainColor}80 !important;
}
if (!props.data.display_name) {
return null;
}
/* Install button text and icon color */
.itemInfo .installButton span,
.itemInfo .installButton svg {
color: ${textColor} !important;
}
/* Mue logo - circle matches text color, paths match button color */
.itemInfo .installButton .mueLogo circle {
fill: ${textColor} !important;
}
.itemInfo .installButton .mueLogo path {
fill: ${mainColor} !important;
}
/* Remove the default gradient animation when themed */
.itemInfo .installButton.installed {
background: ${mainColor}aa !important;
background-image: none !important;
}
.itemInfo .installButton.installed:hover {
background: ${mainColor}99 !important;
}
`}</style>
) : null;
if (!this.props.data.display_name) {
return null;
}
let updateButton;
if (this.state.showUpdateButton) {
updateButton = (
<Fragment key="update">
<Button
type="settings"
onClick={() => this.updateAddon()}
label={variables.getMessage('modals.main.addons.product.buttons.update_addon')}
/>
</Fragment>
);
}
// prevent console error
let iconsrc = this.props.data.icon;
if (!this.props.data.icon) {
iconsrc = null;
}
return (
<>
<Modal
closeTimeoutMS={300}
isOpen={this.state.shareModal}
className="Modal mainModal"
overlayClassName="Overlay"
ariaHideApp={false}
onRequestClose={() => this.setState({ shareModal: false })}
>
<ShareModal
data={
variables.constants.API_URL + '/marketplace/share/' + btoa(this.props.data.api_name)
}
modalClose={() => this.setState({ shareModal: false })}
/>
</Modal>
{/* <Header
title={
this.props.addons
? variables.getMessage('modals.main.addons.added')
: this.props.data.onCollection && this.props.data.data.in_collections?.length > 0
? this.props.data.data.in_collections[0].display_name
: variables.getMessage('modals.main.navbar.marketplace')
}
secondaryTitle={
this.props.data.data.sideload
? this.props.data.data.name
: this.props.data.data.display_name
}
report={false}
goBack={this.props.toggleFunction}
/> */}
<div className="itemPage">
<aside className="itemInfo">
<div className="front">
<img
className="icon"
alt="icon"
draggable={false}
src={this.props.data.data.icon_url}
onError={(e) => {
e.target.onerror = null;
e.target.src = placeholderIcon;
}}
/>
{localStorage.getItem('welcomePreview') !== 'true' ? (
<>
{this.props.button}
{updateButton}
</>
) : (
<p style={{ textAlign: 'center' }}>
{variables.getMessage(
'modals.main.marketplace.product.buttons.not_available_preview',
)}
</p>
)}
{this.props.data.data.sideload !== true && (
<>
{themedStyles}
<div className="iconButtons">
<Button
type="icon"
onClick={() => this.setState({ shareModal: true })}
icon={<MdIosShare />}
tooltipTitle={variables.getMessage('widgets.quote.share')}
tooltipKey="share"
/>
<Button
type="icon"
onClick={() =>
window.open(
variables.constants.REPORT_ITEM +
this.props.data.data.display_name.split(' ').join('+'),
'_blank',
)
}
icon={<MdFlag />}
tooltipTitle={variables.getMessage(
'modals.main.marketplace.product.buttons.report',
)}
tooltipKey="report"
/>
</div>
</>
)}
{this.props.data.data.in_collections?.length > 0 && (
<div className="inCollection">
<span className="subtitle">
{variables.getMessage('modals.main.marketplace.product.part_of')}
</span>
<span
className="title"
onClick={() =>
this.props.toggleFunction(
'collection',
this.props.data.data.in_collections[0].name,
)
}
>
{this.props.data.data.in_collections[0].display_name}
</span>
</div>
)}
</div>
</aside>
<div className="itemContent">
<div className="itemTop">
<div className="subHeader">
<InfoItem
icon={<MdAccountCircle />}
header={variables.getMessage('modals.main.marketplace.product.created_by')}
text={this.props.data.author}
/>
<WarningBanner data={this.props.data.data} shortLocale={shortLocale} />
</div>
<div className="itemTabs">
<button
type="button"
className={`itemTab ${activeTab === 'overview' ? 'active' : ''}`}
onClick={() => this.setState({ activeTab: 'overview' })}
>
{variables.getMessage('modals.main.marketplace.product.overview_tab') ||
'Overview'}
</button>
{hasQuotes && (
<button
type="button"
className={`itemTab ${activeTab === 'quotes' ? 'active' : ''}`}
onClick={() => this.setState({ activeTab: 'quotes' })}
>
{variables.getMessage('modals.main.marketplace.product.quotes_tab') || 'Quotes'}
</button>
)}
{hasPhotos && (
<button
type="button"
className={`itemTab ${activeTab === 'photos' ? 'active' : ''}`}
onClick={() => this.setState({ activeTab: 'photos' })}
>
{variables.getMessage('modals.main.marketplace.product.photos_tab') || 'Photos'}
</button>
)}
{hasSettings && (
<button
type="button"
className={`itemTab ${activeTab === 'presets' ? 'active' : ''}`}
onClick={() => this.setState({ activeTab: 'presets' })}
>
{variables.getMessage('modals.main.marketplace.product.presets_tab') ||
'Presets'}
</button>
)}
</div>
</div>
<div className="tabContent">
{activeTab === 'overview' && (
<OverviewTab
data={this.props.data.data}
description={this.props.data.description}
iconsrc={iconsrc}
shortLocale={shortLocale}
languageNames={languageNames}
formattedDate={formattedDate}
getName={this.getName}
count={this.state.count}
onIncrementCount={(type) => this.incrementCount(type)}
/>
)}
{activeTab === 'quotes' && hasQuotes && (
<QuotesTab
quotes={quotes}
count={this.state.count}
onIncrementCount={(type) => this.incrementCount(type)}
/>
)}
{activeTab === 'photos' && hasPhotos && <PhotosTab photos={photos} />}
{activeTab === 'presets' && hasSettings && (
<PresetsTab
settings={settings}
count={this.state.count}
onIncrementCount={(type) => this.incrementCount(type)}
/>
)}
</div>
</div>
</div>
{/* {moreByCurator.length > 1 && (
<div className="moreFromCurator">
<span className="title">
{variables.getMessage('modals.main.marketplace.product.more_from_curator', {
name: this.props.data.author,
})}
</span>
<div>
<Items
isCurator={true}
type={this.props.data.data.type}
items={moreByCurator}
onCollection={this.state.collection}
toggleFunction={(input) => this.props.toggleFunction('item', input)}
collectionFunction={(input) => this.props.toggleFunction('collection', input)}
filter={''}
moreByCreator={true}
showCreateYourOwn={false}
/>
</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>
)}
</>
let updateButton;
if (showUpdateButton) {
updateButton = (
<Fragment key="update">
<Button
type="settings"
onClick={() => updateAddon()}
label={variables.getMessage('modals.main.addons.product.buttons.update_addon')}
/>
</Fragment>
);
}
}
// prevent console error
let iconsrc = props.data.icon;
if (!props.data.icon) {
iconsrc = null;
}
return (
<>
<Modal
closeTimeoutMS={300}
isOpen={shareModal}
className="Modal mainModal"
overlayClassName="Overlay"
ariaHideApp={false}
onRequestClose={() => setShareModal(false)}
>
<ShareModal
data={variables.constants.API_URL + '/marketplace/share/' + btoa(props.data.api_name)}
modalClose={() => setShareModal(false)}
/>
</Modal>
{/* <Header
title={
props.addons
? variables.getMessage('modals.main.addons.added')
: props.data.onCollection && props.data.data.in_collections?.length > 0
? props.data.data.in_collections[0].display_name
: variables.getMessage('modals.main.navbar.marketplace')
}
secondaryTitle={
props.data.data.sideload
? props.data.data.name
: props.data.data.display_name
}
report={false}
goBack={props.toggleFunction}
/> */}
<div className="itemPage">
<aside className="itemInfo">
<div className="front">
<img
className="icon"
alt="icon"
draggable={false}
src={props.data.data.icon_url}
onError={(e) => {
e.target.onerror = null;
e.target.src = placeholderIcon;
}}
/>
{localStorage.getItem('welcomePreview') !== 'true' ? (
<>
{props.button}
{updateButton}
</>
) : (
<p style={{ textAlign: 'center' }}>
{variables.getMessage('modals.main.marketplace.product.buttons.not_available_preview')}
</p>
)}
{props.data.data.sideload !== true && (
<>
{themedStyles}
<div className="iconButtons">
<Button
type="icon"
onClick={() => setShareModal(true)}
icon={<MdIosShare />}
tooltipTitle={variables.getMessage('widgets.quote.share')}
tooltipKey="share"
/>
<Button
type="icon"
onClick={() =>
window.open(
variables.constants.REPORT_ITEM +
props.data.data.display_name.split(' ').join('+'),
'_blank',
)
}
icon={<MdFlag />}
tooltipTitle={variables.getMessage('modals.main.marketplace.product.buttons.report')}
tooltipKey="report"
/>
</div>
</>
)}
{props.data.data.in_collections?.length > 0 && (
<div className="inCollection">
<span className="subtitle">
{variables.getMessage('modals.main.marketplace.product.part_of')}
</span>
<span
className="title"
onClick={() =>
props.toggleFunction('collection', props.data.data.in_collections[0].name)
}
>
{props.data.data.in_collections[0].display_name}
</span>
</div>
)}
</div>
</aside>
<div className="itemContent">
<div className="itemTop">
<div className="subHeader">
<InfoItem
icon={<MdAccountCircle />}
header={variables.getMessage('modals.main.marketplace.product.created_by')}
text={props.data.author}
/>
<WarningBanner data={props.data.data} shortLocale={shortLocale} />
</div>
<div className="itemTabs">
<button
type="button"
className={`itemTab ${activeTab === 'overview' ? 'active' : ''}`}
onClick={() => setActiveTab('overview')}
>
{variables.getMessage('modals.main.marketplace.product.overview_tab') || 'Overview'}
</button>
{hasQuotes && (
<button
type="button"
className={`itemTab ${activeTab === 'quotes' ? 'active' : ''}`}
onClick={() => setActiveTab('quotes')}
>
{variables.getMessage('modals.main.marketplace.product.quotes_tab') || 'Quotes'}
</button>
)}
{hasPhotos && (
<button
type="button"
className={`itemTab ${activeTab === 'photos' ? 'active' : ''}`}
onClick={() => setActiveTab('photos')}
>
{variables.getMessage('modals.main.marketplace.product.photos_tab') || 'Photos'}
</button>
)}
{hasSettings && (
<button
type="button"
className={`itemTab ${activeTab === 'presets' ? 'active' : ''}`}
onClick={() => setActiveTab('presets')}
>
{variables.getMessage('modals.main.marketplace.product.presets_tab') || 'Presets'}
</button>
)}
</div>
</div>
<div className="tabContent">
{activeTab === 'overview' && (
<OverviewTab
data={props.data.data}
description={props.data.description}
iconsrc={iconsrc}
shortLocale={shortLocale}
languageNames={languageNames}
formattedDate={formattedDate}
getName={getName}
count={count}
onIncrementCount={(type) => incrementCount(type)}
/>
)}
{activeTab === 'quotes' && hasQuotes && (
<QuotesTab
quotes={quotes}
count={count}
onIncrementCount={(type) => incrementCount(type)}
/>
)}
{activeTab === 'photos' && hasPhotos && <PhotosTab photos={photos} />}
{activeTab === 'presets' && hasSettings && (
<PresetsTab
settings={settings}
count={count}
onIncrementCount={(type) => incrementCount(type)}
/>
)}
</div>
</div>
</div>
{/* {moreByCurator.length > 1 && (
<div className="moreFromCurator">
<span className="title">
{variables.getMessage('modals.main.marketplace.product.more_from_curator', {
name: props.data.author,
})}
</span>
<div>
<Items
isCurator={true}
type={props.data.data.type}
items={moreByCurator}
onCollection={state.collection}
toggleFunction={(input) => props.toggleFunction('item', input)}
collectionFunction={(input) => props.toggleFunction('collection', input)}
filter={''}
moreByCreator={true}
showCreateYourOwn={false}
/>
</div>
</div>
)} */}
{props.relatedItems && 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={props.data.data.type}
items={props.relatedItems}
onCollection={false}
toggleFunction={(input) => props.toggleFunction('item', input)}
collectionFunction={(input) => props.toggleFunction('collection', input)}
filter={''}
moreByCreator={false}
showCreateYourOwn={false}
/>
</div>
</div>
)}
</>
);
};
export { ItemPage as default, ItemPage };