diff --git a/src/components/Elements/MainModal/Main.jsx b/src/components/Elements/MainModal/Main.jsx
index 6463601a..82b98547 100644
--- a/src/components/Elements/MainModal/Main.jsx
+++ b/src/components/Elements/MainModal/Main.jsx
@@ -26,6 +26,7 @@ function MainModal({ modalClose, deepLinkData }) {
const [productView, setProductView] = useState(null);
const [resetDiscoverToAll, setResetDiscoverToAll] = useState(false);
const [navigationTrigger, setNavigationTrigger] = useState(null);
+ const [iframeBreadcrumbs, setIframeBreadcrumbs] = useState([]);
// Clear product view when changing tabs
useEffect(() => {
@@ -172,6 +173,7 @@ function MainModal({ modalClose, deepLinkData }) {
currentTab={currentTab}
currentSection={currentSection}
productView={productView}
+ iframeBreadcrumbs={iframeBreadcrumbs}
onTabChange={handleChangeTab}
onClose={modalClose}
onBack={handleBack}
@@ -187,6 +189,7 @@ function MainModal({ modalClose, deepLinkData }) {
currentTab={currentTab}
onSectionChange={handleSectionChange}
onProductView={handleProductView}
+ onBreadcrumbsChange={setIframeBreadcrumbs}
resetToAll={resetDiscoverToAll}
onResetToAll={handleResetDiscoverToAll}
navigationTrigger={navigationTrigger}
diff --git a/src/components/Elements/MainModal/backend/Tabs.jsx b/src/components/Elements/MainModal/backend/Tabs.jsx
index 631238a1..dcfcaa7e 100644
--- a/src/components/Elements/MainModal/backend/Tabs.jsx
+++ b/src/components/Elements/MainModal/backend/Tabs.jsx
@@ -47,8 +47,8 @@ const Tabs = ({ children, navbar = false, currentTab: activeTab, onSectionChange
setShowReminder(false);
};
- // Only show sidebar for Settings tab
- const showSidebar = activeTab === TAB_TYPES.SETTINGS;
+ // Show sidebar for Settings and Discover tabs
+ const showSidebar = activeTab === TAB_TYPES.SETTINGS || activeTab === TAB_TYPES.DISCOVER;
return (
diff --git a/src/components/Elements/MainModal/components/ModalTopBar.jsx b/src/components/Elements/MainModal/components/ModalTopBar.jsx
index df2495e9..6f551853 100644
--- a/src/components/Elements/MainModal/components/ModalTopBar.jsx
+++ b/src/components/Elements/MainModal/components/ModalTopBar.jsx
@@ -20,6 +20,7 @@ function ModalTopBar({
currentTab,
currentSection,
productView,
+ iframeBreadcrumbs,
onTabChange,
onClose,
onBack,
@@ -34,7 +35,7 @@ function ModalTopBar({
: '';
// Determine breadcrumb path with click handlers
- const breadcrumbPath = [];
+ let breadcrumbPath = [];
if (currentTabLabel) {
breadcrumbPath.push({
@@ -42,7 +43,26 @@ function ModalTopBar({
onClick: productView ? productView.onBackToAll : null, // Clickable if viewing a product
});
- if (productView) {
+ // Check if we have iframe breadcrumbs (from Discover iframe)
+ // If so, only use the last item (the item name) and keep our section
+ if (iframeBreadcrumbs && iframeBreadcrumbs.length > 0) {
+ // Get the last breadcrumb item (the item name)
+ const lastCrumb = iframeBreadcrumbs[iframeBreadcrumbs.length - 1];
+
+ // Add current section if available
+ if (currentSection) {
+ breadcrumbPath.push({
+ label: currentSection,
+ onClick: () => onBack(), // Clickable to go back
+ });
+ }
+
+ // Add the item name from iframe
+ breadcrumbPath.push({
+ label: lastCrumb.label,
+ onClick: null, // Current item - not clickable
+ });
+ } else if (productView) {
console.log('ModalTopBar productView:', productView);
console.log('fromCollection:', productView.fromCollection, 'isCollection:', productView.isCollection, 'collectionTitle:', productView.collectionTitle);
diff --git a/src/config/constants.js b/src/config/constants.js
index aecfc114..200bacb9 100644
--- a/src/config/constants.js
+++ b/src/config/constants.js
@@ -6,6 +6,7 @@ export const OPENSTREETMAP_URL = 'https://www.openstreetmap.org';
// Mue URLs
export const WEBSITE_URL = 'https://muetab.com';
+export const MARKETPLACE_URL = 'https://marketplace.muetab.com';
export const PRIVACY_URL = 'https://muetab.com/privacy';
export const TRANSLATIONS_URL = 'https://muetab.com/docs/translations';
export const WEBLATE_URL = 'https://hosted.weblate.org/projects/mue/mue-tab/';
diff --git a/src/features/marketplace/components/Collection/Collection.jsx b/src/features/marketplace/components/Collection/Collection.jsx
deleted file mode 100644
index 7b45042d..00000000
--- a/src/features/marketplace/components/Collection/Collection.jsx
+++ /dev/null
@@ -1,46 +0,0 @@
-import { MdOutlineArrowForward, MdOutlineOpenInNew } from 'react-icons/md';
-import { Button } from 'components/Elements';
-import variables from 'config/variables';
-
-const Collection = ({ collection, collectionFunction }) => {
- const { news, background_colour, img, display_name, description } = collection;
-
- const getStyle = () => {
- if (news) {
- return { backgroundColor: background_colour };
- }
-
- return {
- backgroundImage: `linear-gradient(to left, #000, transparent, #000), url('${img}')`,
- };
- };
-
- return (
-
-
- {display_name} using component
- {description ? description.substr(0, 75) : ''}
-
- {collection.news === true ? (
-
- {variables.getMessage('modals.main.marketplace.learn_more')}
-
- ) : (
-
- );
-};
-
-export { Collection as default, Collection };
diff --git a/src/features/marketplace/components/Collection/index.jsx b/src/features/marketplace/components/Collection/index.jsx
deleted file mode 100644
index 11e99ee3..00000000
--- a/src/features/marketplace/components/Collection/index.jsx
+++ /dev/null
@@ -1 +0,0 @@
-export * from './Collection';
diff --git a/src/features/marketplace/components/Elements/Carousel/Carousel.jsx b/src/features/marketplace/components/Elements/Carousel/Carousel.jsx
deleted file mode 100644
index 9302dd17..00000000
--- a/src/features/marketplace/components/Elements/Carousel/Carousel.jsx
+++ /dev/null
@@ -1,86 +0,0 @@
-import React, { useState, useEffect, useCallback, useRef, memo } from 'react';
-import { MdOutlineArrowForwardIos, MdOutlineArrowBackIos } from 'react-icons/md';
-import useEmblaCarousel from 'embla-carousel-react';
-import Autoplay from 'embla-carousel-autoplay';
-
-import './carousel.scss';
-
-function EmblaCarousel({ data }) {
- const autoplay = useRef(
- Autoplay({ delay: 3000, stopOnInteraction: false }, (emblaRoot) => emblaRoot.parentElement),
- );
-
- const [emblaRef, emblaApi] = useEmblaCarousel({ loop: false }, [autoplay.current]);
- const [prevBtnEnabled, setPrevBtnEnabled] = useState(false);
- const [nextBtnEnabled, setNextBtnEnabled] = useState(false);
-
- const scroll = useCallback(
- (direction) => {
- if (!emblaApi) {
- return;
- }
-
- if (direction === 'next') {
- emblaApi.scrollNext();
- } else {
- emblaApi.scrollPrev();
- }
- autoplay.current.reset();
- },
- [emblaApi],
- );
-
- const onSelect = useCallback(() => {
- if (!emblaApi) {
- return;
- }
- setPrevBtnEnabled(emblaApi.canScrollPrev());
- setNextBtnEnabled(emblaApi.canScrollNext());
- }, [emblaApi]);
-
- useEffect(() => {
- if (!emblaApi) {
- return;
- }
- onSelect();
- emblaApi.on('select', onSelect);
- }, [emblaApi, onSelect]);
-
- return (
-
-
-
- {data?.slice(0, 5).map((photo, index) => (
-
-
-

-
-
- ))}
-
-
- {data.length > 1 && (
- <>
-
-
- >
- )}
-
- );
-}
-
-const Carousel = memo(EmblaCarousel);
-
-export { Carousel as default, Carousel };
diff --git a/src/features/marketplace/components/Elements/Carousel/carousel.scss b/src/features/marketplace/components/Elements/Carousel/carousel.scss
deleted file mode 100644
index 46edb938..00000000
--- a/src/features/marketplace/components/Elements/Carousel/carousel.scss
+++ /dev/null
@@ -1,83 +0,0 @@
-.carousel {
- position: relative;
- width: 100%;
- margin-left: 5px;
-}
-
-.carousel_viewport {
- overflow: hidden;
- width: 100%;
-
- &.is-draggable {
- cursor: move;
- }
-
- &.is-dragging {
- cursor: grabbing;
- }
-
- border-radius: 15px !important;
-}
-
-.carousel_container {
- display: flex;
- -webkit-touch-callout: none;
- user-select: none;
- -webkit-tap-highlight-color: transparent;
- margin-left: -10px;
-}
-
-.carousel_slide {
- position: relative;
- min-width: 100%;
- padding-left: 10px;
-
- img {
- position: absolute;
- display: block;
- top: 50%;
- left: 50%;
- width: auto;
- min-height: 100%;
- min-width: 100%;
- max-width: none;
- transform: translate(-50%, -50%);
- }
-}
-
-.carousel_slide_inner {
- position: relative;
- overflow: hidden;
- height: 250px;
-}
-
-.carousel_button {
- outline: 0;
- cursor: pointer;
- touch-action: manipulation;
- position: absolute;
- z-index: 1;
- top: 50%;
- transform: translateY(-50%);
- border: 0;
- width: 30px !important;
- height: 30px !important;
- display: grid;
- place-items: center;
- justify-content: center;
- align-items: center;
- padding: 0;
-
- &:disabled {
- cursor: default;
- opacity: 0.3;
- }
-
- &.prev {
- left: 27px;
- }
-
- &.next {
- right: 27px;
- }
-}
diff --git a/src/features/marketplace/components/Elements/Carousel/index.jsx b/src/features/marketplace/components/Elements/Carousel/index.jsx
deleted file mode 100644
index c0ab1996..00000000
--- a/src/features/marketplace/components/Elements/Carousel/index.jsx
+++ /dev/null
@@ -1 +0,0 @@
-export * from './Carousel';
diff --git a/src/features/marketplace/components/Elements/InstallButton/InstallButton.jsx b/src/features/marketplace/components/Elements/InstallButton/InstallButton.jsx
deleted file mode 100644
index 313c656c..00000000
--- a/src/features/marketplace/components/Elements/InstallButton/InstallButton.jsx
+++ /dev/null
@@ -1,113 +0,0 @@
-import { memo } from 'react';
-import { MdClose } from 'react-icons/md';
-import './installButton.scss';
-
-const MueLogo = () => (
-
-);
-
-const InstallButton = ({ onClick, isInstalled, label }) => {
- return (
-
- );
-};
-
-export default memo(InstallButton);
diff --git a/src/features/marketplace/components/Elements/InstallButton/index.jsx b/src/features/marketplace/components/Elements/InstallButton/index.jsx
deleted file mode 100644
index 99716e36..00000000
--- a/src/features/marketplace/components/Elements/InstallButton/index.jsx
+++ /dev/null
@@ -1 +0,0 @@
-export { default } from './InstallButton';
diff --git a/src/features/marketplace/components/Elements/InstallButton/installButton.scss b/src/features/marketplace/components/Elements/InstallButton/installButton.scss
deleted file mode 100644
index 76ac26d3..00000000
--- a/src/features/marketplace/components/Elements/InstallButton/installButton.scss
+++ /dev/null
@@ -1,166 +0,0 @@
-.installButton {
- display: inline-flex;
- align-items: center;
- justify-content: center;
- gap: 10px;
- padding: 12px 24px;
- font-size: 16px;
- font-weight: 600;
- border: none;
- border-radius: 8px;
- cursor: pointer;
- transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
- position: relative;
- overflow: hidden;
- min-width: 180px;
-
- .buttonContent {
- display: flex;
- align-items: center;
- gap: 10px;
- position: relative;
- z-index: 1;
- }
-
- .iconWrapper {
- position: relative;
- width: 24px;
- height: 24px;
- display: flex;
- align-items: center;
- justify-content: center;
- }
-
- .icon {
- position: absolute;
- display: flex;
- align-items: center;
- justify-content: center;
- transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
-
- &.show {
- opacity: 1;
- transform: rotate(0deg) scale(1);
- }
-
- &.hide {
- opacity: 0;
- transform: rotate(90deg) scale(0.3);
- }
- }
-
- .mueLogo {
- width: 24px;
- height: 24px;
- filter: drop-shadow(0 2px 4px rgba(0, 0, 0, 0.1));
- }
-
- .removeIcon {
- svg {
- width: 24px;
- height: 24px;
- }
- }
-
- .labelText {
- transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
- }
-
- // Install state (not installed yet)
- &.notInstalled {
- background: linear-gradient(135deg, #ff5c25 0%, #d21a11 48%, #ff456e 100%);
- color: white;
- // box-shadow: 0 4px 12px rgba(255, 92, 37, 0.3);
-
- &:hover {
- // box-shadow: 0 6px 16px rgba(255, 92, 37, 0.4);
-
- .mueLogo {
- animation: logoFloat 0.6s ease-in-out;
- }
-
- // .labelText {
- // transform: translateX(2px);
- // }
- }
-
- &:active {
- box-shadow: 0 2px 8px rgba(255, 92, 37, 0.2);
- }
- }
-
- // Installed state (already installed)
- &.installed {
- background: rgba(255, 255, 255, 0.1);
- color: white;
- // border: 2px solid rgba(255, 255, 255, 0.2);
- // backdrop-filter: blur(10px);
-
- &:hover {
- background: rgba(255, 92, 37, 0.2);
- border-color: rgba(255, 92, 37, 0.4);
- // transform: translateY(-2px);
- box-shadow: 0 6px 16px rgba(255, 92, 37, 0.2);
-
- .removeIcon {
- animation: rotateIcon 0.4s ease-in-out;
- }
-
- // .labelText {
- // transform: translateX(2px);
- // }
- }
-
- &:active {
- transform: translateY(0);
- }
- }
-
- // Loading state (optional - for future use)
- &:disabled {
- opacity: 0.6;
- cursor: not-allowed;
- transform: none !important;
-
- &:hover {
- transform: none !important;
- }
- }
-}
-
-@keyframes logoFloat {
- 0%,
- 100% {
- transform: translateY(0);
- }
- 50% {
- transform: translateY(-4px);
- }
-}
-
-@keyframes rotateIcon {
- 0% {
- transform: rotate(0deg);
- }
- 100% {
- transform: rotate(90deg);
- }
-}
-
-// Smooth state transition animation
-.installButton.notInstalled.transitioning,
-.installButton.installed.transitioning {
- animation: buttonPulse 0.5s ease-out;
-}
-
-@keyframes buttonPulse {
- 0% {
- transform: scale(1);
- }
- 50% {
- transform: scale(1.05);
- }
- 100% {
- transform: scale(1);
- }
-}
diff --git a/src/features/marketplace/components/Elements/index.jsx b/src/features/marketplace/components/Elements/index.jsx
index d1c2dfe2..19ce4aad 100644
--- a/src/features/marketplace/components/Elements/index.jsx
+++ b/src/features/marketplace/components/Elements/index.jsx
@@ -1,3 +1,2 @@
-export * from './Carousel';
export * from './SideloadFailedModal';
export * from './Lightbox';
diff --git a/src/features/marketplace/hooks/useMarketplaceInstall.js b/src/features/marketplace/components/hooks/useMarketplaceInstall.js
similarity index 99%
rename from src/features/marketplace/hooks/useMarketplaceInstall.js
rename to src/features/marketplace/components/hooks/useMarketplaceInstall.js
index 2252f190..0696c441 100644
--- a/src/features/marketplace/hooks/useMarketplaceInstall.js
+++ b/src/features/marketplace/components/hooks/useMarketplaceInstall.js
@@ -68,4 +68,4 @@ export const useMarketplaceInstall = () => {
uninstallItem,
installCollection,
};
-};
+};
\ No newline at end of file
diff --git a/src/features/marketplace/contexts/MarketplaceContext.jsx b/src/features/marketplace/contexts/MarketplaceContext.jsx
deleted file mode 100644
index c98654db..00000000
--- a/src/features/marketplace/contexts/MarketplaceContext.jsx
+++ /dev/null
@@ -1,13 +0,0 @@
-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 };
diff --git a/src/features/marketplace/hooks/useMarketplaceData.js b/src/features/marketplace/hooks/useMarketplaceData.js
deleted file mode 100644
index 7fa9ca98..00000000
--- a/src/features/marketplace/hooks/useMarketplaceData.js
+++ /dev/null
@@ -1,104 +0,0 @@
-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,
- };
-};
diff --git a/src/features/marketplace/hooks/useMarketplaceNavigation.js b/src/features/marketplace/hooks/useMarketplaceNavigation.js
deleted file mode 100644
index 0497ee25..00000000
--- a/src/features/marketplace/hooks/useMarketplaceNavigation.js
+++ /dev/null
@@ -1,223 +0,0 @@
-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, '
')),
- 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,
- };
-};
diff --git a/src/features/marketplace/views/Added.jsx b/src/features/marketplace/views/Added.jsx
index 73f2e49b..bad7efc8 100644
--- a/src/features/marketplace/views/Added.jsx
+++ b/src/features/marketplace/views/Added.jsx
@@ -5,32 +5,19 @@ import { toast } from 'react-toastify';
import Modal from 'react-modal';
import { SideloadFailedModal } from '../components/Elements/SideloadFailedModal/SideloadFailedModal';
-import ItemPage from './ItemPage';
import Items from '../components/Items/Items';
import { Dropdown, FileUpload } from 'components/Form/Settings';
import { Header, CustomActions } from 'components/Layout/Settings';
import { Button } from 'components/Elements';
-import InstallButton from '../components/Elements/InstallButton';
-import { install, uninstall, urlParser } from 'utils/marketplace';
+import { install, uninstall } from 'utils/marketplace';
import { updateHash } from 'utils/deepLinking';
const Added = memo(() => {
const [installed, setInstalled] = useState(JSON.parse(localStorage.getItem('installed')));
- const [item, setItem] = useState({});
const [showFailed, setShowFailed] = useState(false);
const [failedReason, setFailedReason] = useState('');
- const uninstallItem = useCallback(() => {
- uninstall(item.type, item.display_name);
- toast(variables.getMessage('toasts.uninstalled'));
-
- setItem({});
- setInstalled(JSON.parse(localStorage.getItem('installed')));
-
- variables.stats.postEvent('marketplace', 'Uninstall');
- }, [item.type, item.display_name]);
-
const installAddon = useCallback((input) => {
let failedReasonText = '';
if (!input.name) {
@@ -83,22 +70,15 @@ const Added = memo(() => {
const toggle = useCallback((type, data) => {
if (type === 'item') {
- const installedItems = JSON.parse(localStorage.getItem('installed'));
- const info = { data: installedItems.find((i) => i.name === data.name) };
-
- setItem({
- type: info.data.type,
- display_name: info.data.name,
- author: info.data.author,
- description: urlParser(info.data.description.replace(/\n/g, '
')),
- version: info.data.version,
- icon: info.data.screenshot_url,
- data: info.data,
- });
+ // Navigate to discover tab with the item
+ const itemId = data.name;
+ updateHash(`#discover/all?item=${itemId}`);
+
+ // Trigger navigation
+ const event = new window.Event('popstate');
+ window.dispatchEvent(event);
variables.stats.postEvent('marketplace', 'ItemPage viewed');
- } else {
- setItem({});
}
}, []);
@@ -173,14 +153,6 @@ const Added = memo(() => {
sortAddons(localStorage.getItem('sortAddons'), false);
}, []); // eslint-disable-line react-hooks/exhaustive-deps
- const button = item.display_name ? (
-
- ) : '';
-
const sideLoadBackendElements = () => (
<>
{
);
}
- if (item.display_name) {
- return (
- toggle()}
- />
- );
- }
-
return (
<>
diff --git a/src/features/marketplace/views/Browse.jsx b/src/features/marketplace/views/Browse.jsx
deleted file mode 100644
index 4670c342..00000000
--- a/src/features/marketplace/views/Browse.jsx
+++ /dev/null
@@ -1,222 +0,0 @@
-import variables from 'config/variables';
-import { useState, useEffect } from 'react';
-import { MdWifiOff, MdLocalMall } from 'react-icons/md';
-
-import ItemPage from './ItemPage';
-import CollectionPage from './CollectionPage';
-import BrowsePage from './BrowsePage';
-import InstallButton from '../components/Elements/InstallButton';
-
-import { useMarketplaceData } from '../hooks/useMarketplaceData';
-import { useMarketplaceNavigation } from '../hooks/useMarketplaceNavigation';
-import { useMarketplaceInstall } from '../hooks/useMarketplaceInstall';
-import { MarketplaceContext } from '../contexts/MarketplaceContext';
-
-const Marketplace = ({ type, onProductView, onResetToAll, deepLinkData, navigationTrigger }) => {
- const [filter, setFilter] = useState('');
-
- // 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();
-
- const handleManage = (action) => {
- if (!currentItem) return;
-
- if (action === 'install') {
- installItem(currentItem.type, currentItem.data);
- } else {
- uninstallItem(currentItem.type, currentItem.display_name);
- }
- };
-
- // Install/uninstall buttons
- const buttons = {
- uninstall: (
- handleManage('uninstall')}
- isInstalled={true}
- label={variables.getMessage('modals.main.marketplace.product.buttons.remove')}
- />
- ),
- install: (
- handleManage('install')}
- isInstalled={false}
- label={variables.getMessage('modals.main.marketplace.product.buttons.addtomue')}
- />
- ),
- };
-
- // Get appropriate button based on installation status
- const getButton = () => {
- if (!currentItem) return buttons.install;
- return currentItem.addonInstalled ? buttons.uninstall : buttons.install;
- };
-
- // Handle deep linking on mount
- useEffect(() => {
- if (deepLinkData) {
- const { itemId, collection, category } = deepLinkData;
-
- setTimeout(() => {
- if (collection) {
- navigateToCollection(collection);
- } else if (itemId) {
- navigateToItem({ id: itemId, type: category }, type);
- }
- }, 500);
- }
- }, [deepLinkData, navigateToCollection, navigateToItem, type]);
-
- // Handle navigation trigger changes
- useEffect(() => {
- if (navigationTrigger) {
- const { type: navType, data } = navigationTrigger;
-
- 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]);
-
- // Context value to share with child components
- const contextValue = {
- currentView,
- currentItem,
- currentCollection,
- items,
- navigateToItem: (data) => navigateToItem(data, type),
- navigateToCollection,
- navigateBack,
- navigateToMain,
- installItem,
- uninstallItem,
- };
-
- // Error message component
- const renderError = (content) => (
- <>
-
- {variables.getMessage('modals.main.navbar.marketplace')}
-
-
- >
- );
-
- // Offline check
- if (navigator.onLine === false || localStorage.getItem('offlineMode') === 'true') {
- return renderError(
- <>
-
-
- {variables.getMessage('modals.main.marketplace.offline.title')}
-
-
- {variables.getMessage('modals.main.marketplace.offline.description')}
-
- >,
- );
- }
-
- // Loading state
- if (loading) {
- return renderError(
-
-
-
{variables.getMessage('modals.main.loading')}
-
,
- );
- }
-
- // Empty state
- if (!items || items.length === 0) {
- return renderError(
- <>
-
- {variables.getMessage('modals.main.addons.empty.title')}
-
- {variables.getMessage('modals.main.marketplace.no_items')}
-
- >,
- );
- }
-
- // Render item view
- if (currentView === 'item' && currentItem) {
- return (
-
- {
- 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}
- />
-
- );
- }
-
- // Render collection view
- if (currentView === 'collection' && currentCollection) {
- return (
-
- installCollection(currentCollection.items)}
- onItemClick={(item) => navigateToItem(item, type)}
- onSortChange={changeSort}
- />
-
- );
- }
-
- // Render browse view
- return (
-
- setFilter(event.target.value)}
- onItemClick={(item) => navigateToItem(item, type)}
- onCollectionClick={navigateToCollection}
- onSortChange={changeSort}
- />
-
- );
-};
-
-export default Marketplace;
diff --git a/src/features/marketplace/views/BrowsePage.jsx b/src/features/marketplace/views/BrowsePage.jsx
deleted file mode 100644
index b4ca5ac2..00000000
--- a/src/features/marketplace/views/BrowsePage.jsx
+++ /dev/null
@@ -1,83 +0,0 @@
-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 (
- <>
-
- {type !== 'collections' && (
-
-
-
- )}
-
-
- {type === 'collections' ? (
- items.map((item) =>
- !item.news ? (
-
-
- {item.display_name}
- {item.description}
-
-
- ) : null,
- )
- ) : (
-
- )}
- >
- );
-};
-
-export { BrowsePage as default, BrowsePage };
diff --git a/src/features/marketplace/views/CollectionPage.jsx b/src/features/marketplace/views/CollectionPage.jsx
deleted file mode 100644
index 226f7c99..00000000
--- a/src/features/marketplace/views/CollectionPage.jsx
+++ /dev/null
@@ -1,60 +0,0 @@
-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 (
- <>
-
-
{variables.getMessage('modals.main.marketplace.collection')}
-
- {collectionTitle}
- {collectionDescription}
-
-
-
}
- label={
- busy
- ? variables.getMessage('modals.main.marketplace.installing')
- : variables.getMessage('modals.main.marketplace.add_all')
- }
- />
-
-
- {}}
- filter={''}
- onSortChange={onSortChange}
- showCreateYourOwn={false}
- />
- >
- );
-};
-
-export { CollectionPage as default, CollectionPage };
diff --git a/src/features/marketplace/views/Create.jsx b/src/features/marketplace/views/Create.jsx
deleted file mode 100644
index 393101a4..00000000
--- a/src/features/marketplace/views/Create.jsx
+++ /dev/null
@@ -1,35 +0,0 @@
-
-import variables from 'config/variables';
-import { MdOutlineExtensionOff } from 'react-icons/md';
-import { Button } from 'components/Elements';
-
-const Create = () => {
- return (
- <>
-
-
- {variables.getMessage('modals.main.addons.create.title')}
-
-
-
-
-
-
- {variables.getMessage('modals.main.addons.create.moved_title')}
-
-
- {variables.getMessage('modals.main.addons.create.moved_description')}
-
-
-
-
-
-
- >
- );
-};
-
-export { Create as default, Create };
diff --git a/src/features/marketplace/views/ItemPage.jsx b/src/features/marketplace/views/ItemPage.jsx
deleted file mode 100644
index 1f25181d..00000000
--- a/src/features/marketplace/views/ItemPage.jsx
+++ /dev/null
@@ -1,423 +0,0 @@
-import variables from 'config/variables';
-import { useState, Fragment } from 'react';
-import { toast } from 'react-toastify';
-import { MdIosShare, MdFlag, MdAccountCircle } from 'react-icons/md';
-import Modal from 'react-modal';
-
-import { Button } from 'components/Elements';
-
-import { install, uninstall } from 'utils/marketplace';
-import { ShareModal } from 'components/Elements';
-import placeholderIcon from 'assets/icons/marketplace-placeholder.png';
-import { Items } from '../components/Items/Items';
-
-// Tab components
-import OverviewTab from './components/OverviewTab';
-import QuotesTab from './components/QuotesTab';
-import PhotosTab from './components/PhotosTab';
-import PresetsTab from './components/PresetsTab';
-
-// Helper components
-import InfoItem from './components/InfoItem';
-import WarningBanner from './components/WarningBanner';
-
-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');
-
- const updateAddon = () => {
- uninstall(props.data.type, props.data.display_name);
- install(props.data.type, props.data);
- toast(variables.getMessage('toasts.updated'));
- setShowUpdateButton(false);
- };
-
- const incrementCount = (type) => {
- const data = props.data.data;
- let length;
-
- if (type === 'quotes' && Array.isArray(data.quotes)) {
- length = data.quotes.length;
- } else if (type === 'settings' && data.settings) {
- length = Object.keys(data.settings).length;
- } else {
- return;
- }
-
- const newCount = count !== length ? length : 5;
- setCount(newCount);
- };
-
- 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);
- }
-
- // Create dynamic styles for theming with the main color
- const themedStyles = mainColor ? (
-
- ) : null;
-
- if (!props.data.display_name) {
- return null;
- }
-
- let updateButton;
- if (showUpdateButton) {
- updateButton = (
-
-
- );
- }
-
- // prevent console error
- let iconsrc = props.data.icon;
- if (!props.data.icon) {
- iconsrc = null;
- }
-
- return (
- <>
- setShareModal(false)}
- >
- setShareModal(false)}
- />
-
- {/* 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}
- /> */}
-
-
-
-
-
- }
- header={variables.getMessage('modals.main.marketplace.product.created_by')}
- text={props.data.author}
- />
-
-
-
-
- {hasQuotes && (
-
- )}
-
- {hasPhotos && (
-
- )}
-
- {hasSettings && (
-
- )}
-
-
-
- {activeTab === 'overview' && (
-
incrementCount(type)}
- />
- )}
- {activeTab === 'quotes' && hasQuotes && (
- incrementCount(type)}
- />
- )}
- {activeTab === 'photos' && hasPhotos && }
- {activeTab === 'presets' && hasSettings && (
- incrementCount(type)}
- />
- )}
-
-
-
- {/* {moreByCurator.length > 1 && (
-
-
- {variables.getMessage('modals.main.marketplace.product.more_from_curator', {
- name: props.data.author,
- })}
-
-
- props.toggleFunction('item', input)}
- collectionFunction={(input) => props.toggleFunction('collection', input)}
- filter={''}
- moreByCreator={true}
- showCreateYourOwn={false}
- />
-
-
- )} */}
- {props.relatedItems && props.relatedItems.length > 0 && (
-
-
- {variables.getMessage('modals.main.marketplace.you_might_also_like') ||
- 'You might also like'}
-
-
- props.toggleFunction('item', input)}
- collectionFunction={(input) => props.toggleFunction('collection', input)}
- filter={''}
- moreByCreator={false}
- showCreateYourOwn={false}
- />
-
-
- )}
- >
- );
-};
-
-export { ItemPage as default, ItemPage };
diff --git a/src/features/marketplace/views/components/InfoItem.jsx b/src/features/marketplace/views/components/InfoItem.jsx
deleted file mode 100644
index 397b0942..00000000
--- a/src/features/marketplace/views/components/InfoItem.jsx
+++ /dev/null
@@ -1,11 +0,0 @@
-const InfoItem = ({ icon, header, text }) => (
-
- {icon}
-
- {header}
- {text}
-
-
-);
-
-export default InfoItem;
diff --git a/src/features/marketplace/views/components/OverviewTab.jsx b/src/features/marketplace/views/components/OverviewTab.jsx
deleted file mode 100644
index e15c1c5b..00000000
--- a/src/features/marketplace/views/components/OverviewTab.jsx
+++ /dev/null
@@ -1,134 +0,0 @@
-import variables from 'config/variables';
-import { MdCalendarMonth, MdFormatQuote, MdImage, MdTranslate, MdStyle } from 'react-icons/md';
-import { Carousel } from '../../components/Elements/Carousel';
-import placeholderIcon from 'assets/icons/marketplace-placeholder.png';
-import InfoItem from './InfoItem';
-
-const OverviewTab = ({
- data,
- description,
- iconsrc,
- shortLocale,
- languageNames,
- formattedDate,
- getName,
- count,
- onIncrementCount,
-}) => {
- const quotes = Array.isArray(data.quotes) ? data.quotes : [];
- const photos = Array.isArray(data.photos) ? data.photos : [];
- const hasPhotos = photos.length > 0;
- const hasQuotes = quotes.length > 0;
- const hasSettings = data.settings;
-
- // Quote statistics
- const totalQuotes = hasQuotes ? quotes.length : 0;
- const uniqueAuthorsCount = hasQuotes ? new Set(quotes.map((quote) => quote.author)).size : 0;
- const averageCharacters = hasQuotes
- ? Math.round(
- quotes.reduce(
- (accumulator, quote) => accumulator + (quote.quote ? quote.quote.length : 0),
- 0,
- ) / quotes.length,
- )
- : 0;
-
- const totalQuotesLabel =
- variables.getMessage('modals.main.marketplace.product.no_quotes') || 'Total quotes';
- const uniqueAuthorsLabel =
- variables.getMessage('modals.main.marketplace.product.unique_authors') || 'Unique authors';
- const averageCharactersLabel =
- variables.getMessage('modals.main.marketplace.product.average_characters') || 'Avg. characters';
-
- const formatNumber = (value) =>
- typeof value === 'number' ? value.toLocaleString(shortLocale) : value;
-
- return (
- <>
- {/* Preview image for settings presets */}
- {hasSettings && (
-
{
- e.target.onerror = null;
- e.target.src = placeholderIcon;
- }}
- />
- )}
-
- {/* Description */}
-
-
- {variables.getMessage('modals.main.marketplace.product.description')}
-
-
-
-
- {/* Quote pack statistics */}
- {hasQuotes && (
-
-
- {totalQuotesLabel}
- {formatNumber(totalQuotes)}
-
-
- {uniqueAuthorsLabel}
- {formatNumber(uniqueAuthorsCount)}
-
-
- {averageCharactersLabel}
- {formatNumber(averageCharacters)}
-
-
- )}
-
- {/* Details section */}
-
-
- {variables.getMessage('modals.main.marketplace.product.details')}
-
-
- {data.updated_at && (
- }
- header={variables.getMessage('modals.main.marketplace.product.updated_at')}
- text={formattedDate}
- />
- )}
- {hasQuotes && (
- }
- header={variables.getMessage('modals.main.marketplace.product.no_quotes')}
- text={quotes.length}
- />
- )}
- {hasPhotos && (
- }
- header={variables.getMessage('modals.main.marketplace.product.no_images')}
- text={photos.length}
- />
- )}
- {hasQuotes && data.language && (
- }
- header={variables.getMessage('modals.main.settings.sections.language.title')}
- text={languageNames.of(data.language)}
- />
- )}
- }
- header={variables.getMessage('modals.main.settings.sections.background.type.title')}
- text={
- variables.getMessage('modals.main.marketplace.' + getName(data.type)) || 'marketplace'
- }
- />
-
-
- >
- );
-};
-
-export default OverviewTab;
diff --git a/src/features/marketplace/views/components/PhotosTab.jsx b/src/features/marketplace/views/components/PhotosTab.jsx
deleted file mode 100644
index 8dc37dcd..00000000
--- a/src/features/marketplace/views/components/PhotosTab.jsx
+++ /dev/null
@@ -1,13 +0,0 @@
-import { Carousel } from '../../components/Elements/Carousel';
-
-const PhotosTab = ({ photos }) => {
- return (
-
- );
-};
-
-export default PhotosTab;
diff --git a/src/features/marketplace/views/components/PresetsTab.jsx b/src/features/marketplace/views/components/PresetsTab.jsx
deleted file mode 100644
index 21cb62da..00000000
--- a/src/features/marketplace/views/components/PresetsTab.jsx
+++ /dev/null
@@ -1,33 +0,0 @@
-import variables from 'config/variables';
-
-const PresetsTab = ({ settings, count, onIncrementCount }) => {
- return (
- <>
-
-
-
- | {variables.getMessage('modals.main.marketplace.product.setting')} |
- {variables.getMessage('modals.main.marketplace.product.value')} |
-
- {Object.entries(settings)
- .slice(0, count)
- .map(([key, value]) => (
-
- | {key} |
- {value} |
-
- ))}
-
-
-
- onIncrementCount('settings')}>
- {count !== Object.keys(settings).length
- ? variables.getMessage('modals.main.marketplace.product.show_all')
- : variables.getMessage('modals.main.marketplace.product.show_less')}
-
-
- >
- );
-};
-
-export default PresetsTab;
diff --git a/src/features/marketplace/views/components/QuotesTab.jsx b/src/features/marketplace/views/components/QuotesTab.jsx
deleted file mode 100644
index fb248ecf..00000000
--- a/src/features/marketplace/views/components/QuotesTab.jsx
+++ /dev/null
@@ -1,31 +0,0 @@
-import variables from 'config/variables';
-
-const QuotesTab = ({ quotes, count, onIncrementCount }) => {
- return (
- <>
-
-
-
- | {variables.getMessage('modals.main.settings.sections.quote.title')} |
- {variables.getMessage('modals.main.settings.sections.quote.author')} |
-
- {quotes.slice(0, count).map((quote, index) => (
-
- | {quote.quote} |
- {quote.author} |
-
- ))}
-
-
-
- onIncrementCount('quotes')}>
- {count !== quotes.length
- ? variables.getMessage('modals.main.marketplace.product.show_all')
- : variables.getMessage('modals.main.marketplace.product.show_less')}
-
-
- >
- );
-};
-
-export default QuotesTab;
diff --git a/src/features/marketplace/views/components/WarningBanner.jsx b/src/features/marketplace/views/components/WarningBanner.jsx
deleted file mode 100644
index 56bd476e..00000000
--- a/src/features/marketplace/views/components/WarningBanner.jsx
+++ /dev/null
@@ -1,32 +0,0 @@
-import variables from 'config/variables';
-import { MdOutlineWarning } from 'react-icons/md';
-
-const WarningBanner = ({ data, shortLocale }) => {
- const template = (message) => (
-
-
-
- Warning
- {message}
-
-
- );
-
- if (data.sideload === true) {
- return template(variables.getMessage('modals.main.marketplace.product.sideload_warning'));
- }
-
- if (data.image_api === true) {
- return template(variables.getMessage('modals.main.marketplace.product.third_party_api'));
- }
-
- if (data.language !== undefined && data.language !== null) {
- if (shortLocale !== data.language) {
- return template(variables.getMessage('modals.main.marketplace.product.not_in_language'));
- }
- }
-
- return null;
-};
-
-export default WarningBanner;
diff --git a/src/features/marketplace/views/components/index.js b/src/features/marketplace/views/components/index.js
deleted file mode 100644
index ff2eac0e..00000000
--- a/src/features/marketplace/views/components/index.js
+++ /dev/null
@@ -1,9 +0,0 @@
-// Tab components
-export { default as OverviewTab } from './OverviewTab';
-export { default as QuotesTab } from './QuotesTab';
-export { default as PhotosTab } from './PhotosTab';
-export { default as PresetsTab } from './PresetsTab';
-
-// Helper components
-export { default as InfoItem } from './InfoItem';
-export { default as WarningBanner } from './WarningBanner';
diff --git a/src/features/misc/views/Discover.jsx b/src/features/misc/views/Discover.jsx
index 380d4cda..b02c8ebd 100644
--- a/src/features/misc/views/Discover.jsx
+++ b/src/features/misc/views/Discover.jsx
@@ -1,75 +1,239 @@
import variables from 'config/variables';
-import { memo } from 'react';
+import { MARKETPLACE_URL } from 'config/constants';
+import { memo, useEffect, useRef, useState } from 'react';
+import { MdOutlineWifiOff } from 'react-icons/md';
+import Tabs from 'components/Elements/MainModal/backend/Tabs';
+import { useMarketplaceInstall } from 'features/marketplace/components/hooks/useMarketplaceInstall';
-import Tabs from '../../../components/Elements/MainModal/backend/Tabs';
-import MarketplaceTab from '../../marketplace/views/Browse';
+function DiscoverContent({ category, onBreadcrumbsChange }) {
+ const iframeRef = useRef(null);
+ const [isLoading, setIsLoading] = useState(true);
+ const { installItem, uninstallItem } = useMarketplaceInstall();
-function Discover({
- changeTab,
- deepLinkData,
- currentTab,
- onSectionChange,
- onProductView,
- resetToAll,
- onResetToAll,
- navigationTrigger,
-}) {
+ // Check for offline mode
+ const offlineMode = localStorage.getItem('offlineMode') === 'true';
+ const isOffline = navigator.onLine === false || offlineMode;
+
+ useEffect(() => {
+ // Show loader when category changes
+ setIsLoading(true);
+ // Clear breadcrumbs when navigating to a new category
+ if (onBreadcrumbsChange) {
+ onBreadcrumbsChange([]);
+ }
+
+ // Update iframe src with category
+ if (iframeRef.current) {
+ // Collections use path-based routing, others use query params
+ if (category === 'collections') {
+ iframeRef.current.src = `${MARKETPLACE_URL}/collections?embed=true`;
+ } else {
+ iframeRef.current.src = `${MARKETPLACE_URL}?embed=true&type=${category}`;
+ }
+ }
+ }, [category, onBreadcrumbsChange]);
+
+ useEffect(() => {
+ // Check for item parameter in URL and update iframe
+ const checkAndLoadItem = () => {
+ const hash = window.location.hash;
+ const urlParams = new URLSearchParams(hash.split('?')[1]);
+ const itemId = urlParams.get('item');
+
+ if (itemId && iframeRef.current) {
+ setIsLoading(true);
+
+ // Get item from localStorage to determine type
+ const installed = JSON.parse(localStorage.getItem('installed')) || [];
+ const item = installed.find((i) => i.name === itemId);
+
+ if (item) {
+ // Map item type to URL path
+ const pathMap = {
+ photo_packs: 'packs',
+ quote_packs: 'packs',
+ preset_settings: 'presets',
+ };
+
+ const pathSegment = pathMap[item.type] || 'packs';
+ const itemIdToUse = item.id || itemId;
+
+ // Navigate to /packs/{id} or /presets/{id}
+ iframeRef.current.src = `${MARKETPLACE_URL}/${pathSegment}/${itemIdToUse}?embed=true`;
+ } else {
+ // Fallback if item not found in localStorage
+ iframeRef.current.src = `${MARKETPLACE_URL}/packs/${itemId}?embed=true`;
+ }
+ }
+ };
+
+ // Check on mount and when category changes
+ checkAndLoadItem();
+
+ // Listen for hash changes and popstate (from history.pushState)
+ window.addEventListener('hashchange', checkAndLoadItem);
+ window.addEventListener('popstate', checkAndLoadItem);
+
+ return () => {
+ window.removeEventListener('hashchange', checkAndLoadItem);
+ window.removeEventListener('popstate', checkAndLoadItem);
+ };
+ }, [category]);
+
+ useEffect(() => {
+ // Listen for postMessage events from the iframe
+ const handleMessage = (event) => {
+ // Verify the origin if needed
+ if (event.origin !== MARKETPLACE_URL) {
+ return;
+ }
+
+ const { type, payload } = event.data;
+
+ switch (type) {
+ case 'marketplace:item:install':
+ if (payload?.item) {
+ installItem(payload.item.type, payload.item);
+ // Send confirmation back to iframe
+ if (iframeRef.current?.contentWindow) {
+ iframeRef.current.contentWindow.postMessage(
+ {
+ type: 'marketplace:item:installed',
+ payload: { id: payload.item.id || payload.item.name, installed: true },
+ },
+ MARKETPLACE_URL
+ );
+ }
+ }
+ break;
+
+ case 'marketplace:item:uninstall':
+ if (payload?.item) {
+ uninstallItem(payload.item.type, payload.item.display_name || payload.item.name);
+ // Send confirmation back to iframe
+ if (iframeRef.current?.contentWindow) {
+ iframeRef.current.contentWindow.postMessage(
+ {
+ type: 'marketplace:item:installed',
+ payload: { id: payload.item.id || payload.item.name, installed: false },
+ },
+ MARKETPLACE_URL
+ );
+ }
+ }
+ break;
+
+ case 'marketplace:item:check-installed':
+ if (payload?.id) {
+ // Check if item is installed
+ const installed = JSON.parse(localStorage.getItem('installed')) || [];
+ const isInstalled = installed.some((item) => item.id === payload.id);
+
+ // Send status back to iframe
+ if (iframeRef.current?.contentWindow) {
+ iframeRef.current.contentWindow.postMessage(
+ {
+ type: 'marketplace:item:installed',
+ payload: { id: payload.id, installed: isInstalled },
+ },
+ MARKETPLACE_URL
+ );
+ }
+ }
+ break;
+
+ case 'marketplace:breadcrumbs':
+ if (payload?.breadcrumbs && onBreadcrumbsChange) {
+ onBreadcrumbsChange(payload.breadcrumbs);
+ }
+ break;
+
+ default:
+ break;
+ }
+ };
+
+ window.addEventListener('message', handleMessage);
+
+ return () => {
+ window.removeEventListener('message', handleMessage);
+ };
+ }, [installItem, uninstallItem, onBreadcrumbsChange]);
+
+ const handleLoad = () => {
+ setIsLoading(false);
+ };
+
+ // Show offline error message if offline
+ if (isOffline) {
+ return (
+
+
+
+
{variables.getMessage('modals.main.marketplace.offline.title')}
+
+ {variables.getMessage('modals.main.marketplace.offline.description')}
+
+
+
+ );
+ }
+
+ return (
+
+ {isLoading && (
+
+
+
{variables.getMessage('modals.main.loading')}
+
+ )}
+
+
+ );
+}
+
+const sections = [
+ { label: 'modals.main.marketplace.all', name: 'all' },
+ { label: 'modals.main.marketplace.photo_packs', name: 'photo_packs' },
+ { label: 'modals.main.marketplace.quote_packs', name: 'quote_packs' },
+ { label: 'modals.main.marketplace.preset_settings', name: 'preset_settings' },
+ { label: 'modals.main.marketplace.collections', name: 'collections' },
+];
+
+function Discover(props) {
return (
changeTab(type)}
+ changeTab={(type) => props.changeTab(type)}
current="discover"
- currentTab={currentTab}
- onSectionChange={onSectionChange}
- resetToFirst={resetToAll}
+ currentTab={props.currentTab}
+ onSectionChange={props.onSectionChange}
>
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+ {sections.map(({ label, name }) => (
+
+
+
+ ))}
);
}
diff --git a/src/features/misc/views/Library.jsx b/src/features/misc/views/Library.jsx
index 180567c1..1167bee9 100644
--- a/src/features/misc/views/Library.jsx
+++ b/src/features/misc/views/Library.jsx
@@ -3,7 +3,6 @@ import { memo } from 'react';
import Tabs from 'components/Elements/MainModal/backend/Tabs';
import Added from '../../marketplace/views/Added';
-import Create from '../../marketplace/views/Create';
function Library(props) {
return (
@@ -17,7 +16,6 @@ function Library(props) {