From 0431fc830d22dee1663bd213bf8c4ae958dd5890 Mon Sep 17 00:00:00 2001 From: David Ralph Date: Sat, 3 Jan 2026 17:25:34 +0000 Subject: [PATCH] feat(marketplace): use web ui --- src/components/Elements/MainModal/Main.jsx | 3 + .../Elements/MainModal/backend/Tabs.jsx | 4 +- .../MainModal/components/ModalTopBar.jsx | 24 +- src/config/constants.js | 1 + .../components/Collection/Collection.jsx | 46 -- .../components/Collection/index.jsx | 1 - .../components/Elements/Carousel/Carousel.jsx | 86 ---- .../Elements/Carousel/carousel.scss | 83 ---- .../components/Elements/Carousel/index.jsx | 1 - .../Elements/InstallButton/InstallButton.jsx | 113 ----- .../Elements/InstallButton/index.jsx | 1 - .../Elements/InstallButton/installButton.scss | 166 ------- .../marketplace/components/Elements/index.jsx | 1 - .../hooks/useMarketplaceInstall.js | 2 +- .../contexts/MarketplaceContext.jsx | 13 - .../marketplace/hooks/useMarketplaceData.js | 104 ----- .../hooks/useMarketplaceNavigation.js | 223 --------- src/features/marketplace/views/Added.jsx | 55 +-- src/features/marketplace/views/Browse.jsx | 222 --------- src/features/marketplace/views/BrowsePage.jsx | 83 ---- .../marketplace/views/CollectionPage.jsx | 60 --- src/features/marketplace/views/Create.jsx | 35 -- src/features/marketplace/views/ItemPage.jsx | 423 ------------------ .../marketplace/views/components/InfoItem.jsx | 11 - .../views/components/OverviewTab.jsx | 134 ------ .../views/components/PhotosTab.jsx | 13 - .../views/components/PresetsTab.jsx | 33 -- .../views/components/QuotesTab.jsx | 31 -- .../views/components/WarningBanner.jsx | 32 -- .../marketplace/views/components/index.js | 9 - src/features/misc/views/Discover.jsx | 294 +++++++++--- src/features/misc/views/Library.jsx | 2 - 32 files changed, 266 insertions(+), 2043 deletions(-) delete mode 100644 src/features/marketplace/components/Collection/Collection.jsx delete mode 100644 src/features/marketplace/components/Collection/index.jsx delete mode 100644 src/features/marketplace/components/Elements/Carousel/Carousel.jsx delete mode 100644 src/features/marketplace/components/Elements/Carousel/carousel.scss delete mode 100644 src/features/marketplace/components/Elements/Carousel/index.jsx delete mode 100644 src/features/marketplace/components/Elements/InstallButton/InstallButton.jsx delete mode 100644 src/features/marketplace/components/Elements/InstallButton/index.jsx delete mode 100644 src/features/marketplace/components/Elements/InstallButton/installButton.scss rename src/features/marketplace/{ => components}/hooks/useMarketplaceInstall.js (99%) delete mode 100644 src/features/marketplace/contexts/MarketplaceContext.jsx delete mode 100644 src/features/marketplace/hooks/useMarketplaceData.js delete mode 100644 src/features/marketplace/hooks/useMarketplaceNavigation.js delete mode 100644 src/features/marketplace/views/Browse.jsx delete mode 100644 src/features/marketplace/views/BrowsePage.jsx delete mode 100644 src/features/marketplace/views/CollectionPage.jsx delete mode 100644 src/features/marketplace/views/Create.jsx delete mode 100644 src/features/marketplace/views/ItemPage.jsx delete mode 100644 src/features/marketplace/views/components/InfoItem.jsx delete mode 100644 src/features/marketplace/views/components/OverviewTab.jsx delete mode 100644 src/features/marketplace/views/components/PhotosTab.jsx delete mode 100644 src/features/marketplace/views/components/PresetsTab.jsx delete mode 100644 src/features/marketplace/views/components/QuotesTab.jsx delete mode 100644 src/features/marketplace/views/components/WarningBanner.jsx delete mode 100644 src/features/marketplace/views/components/index.js 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) => ( -
-
- Marketplace example screenshot -
-
- ))} -
-
- {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')} -
-
-
{content}
-
- - ); - - // 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} -
- -
- - {}} - 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 = ( - -
- - )} - {props.data.data.in_collections?.length > 0 && ( -
- - {variables.getMessage('modals.main.marketplace.product.part_of')} - - - props.toggleFunction('collection', props.data.data.in_collections[0].name) - } - > - {props.data.data.in_collections[0].display_name} - -
- )} - - -
-
-
- } - 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 && ( - product { - 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 ( - <> - - - - - - - {Object.entries(settings) - .slice(0, count) - .map(([key, value]) => ( - - - - - ))} - -
{variables.getMessage('modals.main.marketplace.product.setting')}{variables.getMessage('modals.main.marketplace.product.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 ( - <> - - - - - - - {quotes.slice(0, count).map((quote, index) => ( - - - - - ))} - -
{variables.getMessage('modals.main.settings.sections.quote.title')}{variables.getMessage('modals.main.settings.sections.quote.author')}
{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')} +
+ )} +