import variables from 'config/variables'; import { MARKETPLACE_URL } from 'config/constants'; import { memo, useEffect, useRef, useState } from 'react'; import { updateHash } from 'utils/deepLinking'; import { MdOutlineWifiOff } from 'react-icons/md'; import Modal from 'react-modal'; import Tabs from 'components/Elements/MainModal/backend/Tabs'; import { useMarketplaceInstall } from 'features/marketplace/components/hooks/useMarketplaceInstall'; import Lightbox from 'features/marketplace/components/Elements/Lightbox/Lightbox'; function DiscoverContent({ category, onBreadcrumbsChange, deepLinkData }) { const iframeRef = useRef(null); const [isLoading, setIsLoading] = useState(true); const [showLightbox, setShowLightbox] = useState(false); const [lightboxImg, setLightboxImg] = useState(null); const { installItem, uninstallItem } = useMarketplaceInstall(); // Check for offline mode const offlineMode = localStorage.getItem('offlineMode') === 'true'; // Check for preview mode const isPreviewMode = localStorage.getItem('showWelcome') === 'true'; const previewParam = isPreviewMode ? '&preview=true' : ''; const isOffline = navigator.onLine === false || offlineMode; // Clear breadcrumbs when component unmounts (navigating away from discover) useEffect(() => { return () => { if (onBreadcrumbsChange) { onBreadcrumbsChange([]); } }; }, [onBreadcrumbsChange]); // Helper function to resolve auto theme const getResolvedTheme = () => { const theme = localStorage.getItem('theme') || 'auto'; if (theme === 'auto') { return window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } return theme; }; useEffect(() => { // Skip category navigation if we have a deep link item to navigate to if (deepLinkData?.itemId) { return; } // Show loader when category changes setIsLoading(true); // Clear breadcrumbs when navigating to a new category if (onBreadcrumbsChange) { onBreadcrumbsChange([]); } // Get current theme const theme = getResolvedTheme(); const themeParam = `&theme=${theme}`; // 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${previewParam}${themeParam}`; } else { iframeRef.current.src = `${MARKETPLACE_URL}?embed=true&type=${category}${previewParam}${themeParam}`; } } }, [category, onBreadcrumbsChange, previewParam, deepLinkData]); 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 current theme const theme = getResolvedTheme(); const themeParam = `&theme=${theme}`; // 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${previewParam}${themeParam}`; } else { // Fallback if item not found in localStorage iframeRef.current.src = `${MARKETPLACE_URL}/packs/${itemId}?embed=true${previewParam}${themeParam}`; } } }; // 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, previewParam]); // Handle deep link item navigation on mount useEffect(() => { if (deepLinkData?.itemId && iframeRef.current) { setIsLoading(true); const theme = getResolvedTheme(); const themeParam = `&theme=${theme}`; // Map category to URL path segment const pathMap = { photo_packs: 'packs', quote_packs: 'packs', preset_settings: 'presets', }; const pathSegment = deepLinkData.category ? pathMap[deepLinkData.category] || 'packs' : 'packs'; iframeRef.current.src = `${MARKETPLACE_URL}/${pathSegment}/${deepLinkData.itemId}?embed=true${previewParam}${themeParam}`; } }, [deepLinkData, previewParam]); // Send navigation commands to iframe when hash changes externally (e.g., browser back/forward) useEffect(() => { const handleHashChange = () => { if (!iframeRef.current?.contentWindow) return; const hash = window.location.hash; if (!hash || !hash.startsWith('#discover')) return; // Parse hash to determine target path // e.g., #discover/photo_packs/123 -> /marketplace/packs/123 // e.g., #discover/preset_settings/456 -> /marketplace/presets/456 // e.g., #discover/collections -> /marketplace/collections // e.g., #discover/collection/featured -> /marketplace/collection/featured const parts = hash.slice(1).split('/'); if (parts.length < 2) return; let targetPath = '/marketplace'; if (parts[1] === 'collections') { targetPath = '/marketplace/collections'; } else if (parts[1] === 'collection' && parts[2]) { targetPath = `/marketplace/collection/${parts[2]}`; } else if (parts[2]) { // Item view - map category to path const pathMap = { photo_packs: 'packs', quote_packs: 'packs', preset_settings: 'presets', }; const pathSegment = pathMap[parts[1]] || 'packs'; targetPath = `/marketplace/${pathSegment}/${parts[2]}`; } else if (parts[1] !== 'all') { // Category filter targetPath = `/marketplace?type=${parts[1]}`; } // Send navigation command to iframe const theme = getResolvedTheme(); iframeRef.current.contentWindow.postMessage( { type: 'marketplace:navigate', payload: { path: targetPath }, }, MARKETPLACE_URL, ); }; // Listen for hash changes from browser navigation window.addEventListener('hashchange', handleHashChange); return () => window.removeEventListener('hashchange', handleHashChange); }, []); useEffect(() => { // Listen for postMessage events from the iframe const handleMessage = (event) => { // Verify the origin if needed const marketplaceOrigin = new URL(MARKETPLACE_URL).origin; if (event.origin !== marketplaceOrigin) { return; } const { type, payload } = event.data; switch (type) { case 'marketplace:item:install': if (payload?.item) { // Fetch fresh data from API to ensure we get latest version with blur_hash const itemId = payload.item.id || payload.item.name; const itemType = payload.item.type; fetch(`${variables.constants.API_URL}/marketplace/item/${itemId}`) .then((res) => res.json()) .then(({ data }) => { // Install with fresh data from API installItem(data.type, data); }) .catch((error) => { console.error('Failed to fetch item from API, using iframe data:', error); // Fallback to iframe data if API fetch fails installItem(itemType, payload.item); }) .finally(() => { // Send confirmation back to iframe if (iframeRef.current?.contentWindow) { iframeRef.current.contentWindow.postMessage( { type: 'marketplace:item:installed', payload: { id: itemId, installed: true }, }, MARKETPLACE_URL, ); } }); } break; case 'marketplace:item:uninstall': if (payload?.item) { uninstallItem(payload.item.type, payload.item.name || payload.item.display_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); // Scroll modal content to top when navigating to an item if (payload.breadcrumbs.length > 0) { const modalContent = document.querySelector('.modalTabContent'); if (modalContent) { modalContent.scrollTop = 0; } } } break; case 'marketplace:lightbox': if (payload?.photo) { setLightboxImg(payload.photo.url); setShowLightbox(true); } break; case 'marketplace:navigation': // Update parent URL when iframe navigates if (payload?.path) { // Parse the path to extract relevant info // e.g., /marketplace/packs/123 -> #discover/photo_packs/123 // e.g., /marketplace/presets/456 -> #discover/preset_settings/456 // e.g., /marketplace/collections -> #discover/collections // e.g., /marketplace/collection/featured -> #discover/collection/featured const path = payload.path; if (path.includes('/packs/')) { const itemId = path.split('/packs/')[1]?.split('?')[0]; if (itemId) { // Determine type from installed items or default to photo_packs const installed = JSON.parse(localStorage.getItem('installed')) || []; const item = installed.find((i) => i.id === itemId); const category = item?.type || 'photo_packs'; updateHash(`#discover/${category}/${itemId}`); } } else if (path.includes('/presets/')) { const itemId = path.split('/presets/')[1]?.split('?')[0]; if (itemId) { updateHash(`#discover/preset_settings/${itemId}`); } } else if (path.includes('/collection/')) { const collectionId = path.split('/collection/')[1]?.split('?')[0]; if (collectionId) { updateHash(`#discover/collection/${collectionId}`); } } else if (path.includes('/collections')) { updateHash('#discover/collections'); } else if (path === '/marketplace' || path === '/marketplace/') { // Extract type from search params if present const searchParams = new URLSearchParams(payload.search || ''); const type = searchParams.get('type') || 'all'; updateHash(`#discover/${type}`); } } break; default: break; } }; window.addEventListener('message', handleMessage); return () => { window.removeEventListener('message', handleMessage); }; }, [installItem, uninstallItem, onBreadcrumbsChange]); const handleLoad = () => { setIsLoading(false); // Clear breadcrumbs when iframe loads - if on an item page, iframe will send new breadcrumbs if (onBreadcrumbsChange) { onBreadcrumbsChange([]); } // Send theme to iframe after it loads if (iframeRef.current?.contentWindow) { const theme = getResolvedTheme(); iframeRef.current.contentWindow.postMessage( { type: 'marketplace:theme', payload: { theme }, }, MARKETPLACE_URL, ); } }; // Show offline error message if offline if (isOffline) { return (
{variables.getMessage('modals.main.marketplace.offline.description')}