mirror of
https://github.com/mue/mue.git
synced 2026-07-20 15:34:10 +02:00
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:
13
src/features/marketplace/contexts/MarketplaceContext.jsx
Normal file
13
src/features/marketplace/contexts/MarketplaceContext.jsx
Normal 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 };
|
||||
104
src/features/marketplace/hooks/useMarketplaceData.js
Normal file
104
src/features/marketplace/hooks/useMarketplaceData.js
Normal 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,
|
||||
};
|
||||
};
|
||||
71
src/features/marketplace/hooks/useMarketplaceInstall.js
Normal file
71
src/features/marketplace/hooks/useMarketplaceInstall.js
Normal 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,
|
||||
};
|
||||
};
|
||||
223
src/features/marketplace/hooks/useMarketplaceNavigation.js
Normal file
223
src/features/marketplace/hooks/useMarketplaceNavigation.js
Normal 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,
|
||||
};
|
||||
};
|
||||
@@ -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;
|
||||
|
||||
83
src/features/marketplace/views/BrowsePage.jsx
Normal file
83
src/features/marketplace/views/BrowsePage.jsx
Normal 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 };
|
||||
60
src/features/marketplace/views/CollectionPage.jsx
Normal file
60
src/features/marketplace/views/CollectionPage.jsx
Normal 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 };
|
||||
@@ -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 };
|
||||
|
||||
Reference in New Issue
Block a user