Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions assets/icons/delete.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions assets/icons/info.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion frontend/build/addons.asset.php
Original file line number Diff line number Diff line change
@@ -1 +1 @@
<?php return array('dependencies' => array('react', 'react-dom'), 'version' => 'fd3b74144a72f90a0449');
<?php return array('dependencies' => array('react', 'react-dom'), 'version' => '5313a9a7ab78caac3bd1');
2 changes: 1 addition & 1 deletion frontend/build/addons.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion frontend/build/index.asset.php
Original file line number Diff line number Diff line change
@@ -1 +1 @@
<?php return array('dependencies' => array('react', 'react-dom'), 'version' => '012415a6045d000c7431');
<?php return array('dependencies' => array('react', 'react-dom'), 'version' => '315197e65e3e6daa36d3');
2 changes: 1 addition & 1 deletion frontend/build/index.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion frontend/build/index.js.map

Large diffs are not rendered by default.

11 changes: 4 additions & 7 deletions frontend/src/AddonsApp.jsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import React from "react";
import Addons from "./components/Addons";
import LoadingOverlay from "./components/LoadingOverlay";
import MarketplaceLayout from "./components/MarketplaceLayout";
import { MarketplaceProvider } from "./context/MarketplaceContext";

const AddonsApp = ({ apiBaseUrl, useWPHandlers, wpConfig, enableDefaultStyles, assetsBaseUrl }) => {
Expand All @@ -12,12 +12,9 @@ const AddonsApp = ({ apiBaseUrl, useWPHandlers, wpConfig, enableDefaultStyles, a
enableDefaultStyles={enableDefaultStyles}
assetsBaseUrl={assetsBaseUrl}
>
<LoadingOverlay />
<div className="gv-activated">
<div className="marketplace-container gv-layout-product gv-w-max-container gv-mx-auto gv-p-fluid">
<Addons />
</div>
</div>
<MarketplaceLayout>
<Addons />
</MarketplaceLayout>
</MarketplaceProvider>
);
};
Expand Down
65 changes: 15 additions & 50 deletions frontend/src/MarketplaceApp.jsx
Original file line number Diff line number Diff line change
@@ -1,65 +1,30 @@
import React, { useState, useEffect } from "react";
import React from "react";
import Marketplace from "./components/MarketPlace";
import ProductBanner from "./components/ProductBanner";
import FeaturedCarousel from "./components/FeaturedCarousel";
import LoadingOverlay from "./components/LoadingOverlay";
import MarketplaceLayout from "./components/MarketplaceLayout";
import { MarketplaceProvider, useMarketplace } from "./context/MarketplaceContext";

// Inner component that can access the context
const MarketplaceContent = () => {
const { allPluginsActivated, catalogError, catalogLoading, isWpVersionSupported } = useMarketplace();
const {
allPluginsActivated,
catalogError,
catalogLoading,
isWpVersionSupported,
currentPluginSlug
} = useMarketplace();

const isSupportedWpVersion = isWpVersionSupported('6.2');

// Track detail page visibility with state
const [isDetailPage, setIsDetailPage] = useState(
typeof window !== "undefined" && new URLSearchParams(window.location.search).get("plugin")
);

// Listen for URL changes (both popstate and custom events)
useEffect(() => {
const checkDetailPage = () => {
const hasPlugin = typeof window !== "undefined" && new URLSearchParams(window.location.search).get("plugin");
setIsDetailPage(!!hasPlugin);
};

// Listen for browser back/forward
window.addEventListener('popstate', checkDetailPage);

// Listen for programmatic URL changes (from pushState)
const originalPushState = window.history.pushState;
window.history.pushState = function(...args) {
originalPushState.apply(this, args);
checkDetailPage();
};

// Listen for programmatic URL changes (from replaceState)
const originalReplaceState = window.history.replaceState;
window.history.replaceState = function(...args) {
originalReplaceState.apply(this, args);
checkDetailPage();
};

return () => {
window.removeEventListener('popstate', checkDetailPage);
window.history.pushState = originalPushState;
window.history.replaceState = originalReplaceState;
};
}, []);
const isDetailPage = !!currentPluginSlug;

return (
<>
<LoadingOverlay />
<div className="gv-activated">
<div className="marketplace-container gv-layout-product gv-surface-dim gv-w-max-container gv-mx-auto gv-p-fluid ">

{!isDetailPage && !catalogError && isSupportedWpVersion && <ProductBanner loading={catalogLoading} />}
{!isDetailPage && !allPluginsActivated && isSupportedWpVersion && <FeaturedCarousel loading={catalogLoading} />}
<MarketplaceLayout className="gv-surface-dim">
{!isDetailPage && !catalogError && isSupportedWpVersion && <ProductBanner loading={catalogLoading} />}
{!isDetailPage && !allPluginsActivated && isSupportedWpVersion && <FeaturedCarousel loading={catalogLoading} />}

<Marketplace />
</div>
</div>
</>
<Marketplace />
</MarketplaceLayout>
);
};

Expand Down
45 changes: 13 additions & 32 deletions frontend/src/components/Addons.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,12 @@ export default function Addons() {
setCatalogError,
catalogLoading,
setCatalogLoading,
currentPluginSlug,
shouldShowProvision,
isSpecialPlugin,
shouldShowPlugin,
isWpVersionSupported
isWpVersionSupported,
openDeleteModal
} = useMarketplace();

const [selectedPlugin, setSelectedPlugin] = useState(null);
Expand Down Expand Up @@ -60,11 +62,6 @@ export default function Addons() {
const assetBase = assetsBaseUrl || (typeof window.marketplaceConfig !== "undefined" && window.marketplaceConfig?.assetsBaseUrl) || "";
const iconBase = assetBase ? `${assetBase}assets/icons/` : "";

// Determine if a plugin slug is in the URL
const pluginFromQuery = typeof window !== "undefined"
? new URLSearchParams(window.location.search).get("plugin")
: null;

// Get marketplace page URL
const getMarketplaceUrl = (slug) => {
const adminUrl = typeof window !== "undefined" && window.marketplaceConfig?.wpConfig?.adminUrl
Expand Down Expand Up @@ -198,31 +195,15 @@ export default function Addons() {
});
}, [apiBaseUrl, setPlugins, setUiI18n, setCatalogError, setCatalogLoading, shouldShowPlugin]);

// After plugins load, select plugin from query if present
// After plugins load, select plugin from URL if present
useEffect(() => {
if (pluginFromQuery && plugins.length) {
const match = plugins.find(p => p.slug === pluginFromQuery);
if (currentPluginSlug && plugins.length) {
const match = plugins.find(p => p.slug === currentPluginSlug);
if (match) setSelectedPlugin(match);
} else if (!pluginFromQuery) {
} else if (!currentPluginSlug) {
setSelectedPlugin(null);
}
}, [pluginFromQuery, plugins]);

// Listen for browser back/forward navigation
useEffect(() => {
const handlePopState = () => {
const currentPluginParam = new URLSearchParams(window.location.search).get("plugin");
if (!currentPluginParam) {
setSelectedPlugin(null);
} else if (plugins.length) {
const match = plugins.find(p => p.slug === currentPluginParam);
if (match) setSelectedPlugin(match);
}
};

window.addEventListener('popstate', handlePopState);
return () => window.removeEventListener('popstate', handlePopState);
}, [plugins]);
}, [currentPluginSlug, plugins]);

useEffect(() => {
const handleClickOutside = (event) => {
Expand All @@ -244,7 +225,7 @@ export default function Addons() {

// Track addons page visit when plugins are loaded and no plugin detail is shown
useEffect(() => {
if (!catalogLoading && !catalogError && plugins.length > 0 && !pluginFromQuery && !hasTrackedAddonsVisit.current) {
if (!catalogLoading && !catalogError && plugins.length > 0 && !currentPluginSlug && !hasTrackedAddonsVisit.current) {
// Capture timestamp when content is rendered to the page
contentRenderTimestamp.current = Date.now();

Expand All @@ -266,7 +247,7 @@ export default function Addons() {

hasTrackedAddonsVisit.current = true;
}
}, [catalogLoading, catalogError, plugins.length, pluginFromQuery]);
}, [catalogLoading, catalogError, plugins.length, currentPluginSlug]);


// Determine which detail component to use
Expand Down Expand Up @@ -594,10 +575,10 @@ export default function Addons() {
onClick={(e) => {
e.preventDefault();
setOpenMenuIndex(null);
handlePluginAction('delete', plugin, 'addons');
openDeleteModal(plugin);
}}
>
<gv-icon aria-hidden="true" src={`${iconBase}cancel.svg`}></gv-icon>
<gv-icon aria-hidden="true" src={`${iconBase}delete.svg`}></gv-icon>
<span>{uiI18n?.deleteButton || 'Delete'}</span>
</a>
)}
Expand All @@ -621,7 +602,7 @@ export default function Addons() {
<SuccessToast />

{/* Render detail overlay when plugin is selected */}
{selectedPlugin && !pluginFromQuery && (() => {
{selectedPlugin && !currentPluginSlug && (() => {
const DetailComponent = shouldUseRankMathDetail(selectedPlugin) ? ProductDetailRankMath : ProductDetail;
return (
<DetailComponent
Expand Down
51 changes: 51 additions & 0 deletions frontend/src/components/Breadcrumbs.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import React from "react";

const Breadcrumbs = ({ iconBase, label, onClose, className = "", disabled = false, children }) => {
const handleBack = (e) => {
e.preventDefault();
if (disabled) return;
// First check if history is available and has navigable records
if (typeof window !== "undefined" && window.history && window.history.length > 1) {
try {
window.history.back();
} catch (error) {
// If history.back() fails, fallback to onClose
if (onClose) {
onClose();
}
}
} else if (onClose) {
// Fallback to onClose if history is not available or empty
onClose();
}
};

return (
<nav className={`gv-breadcrumbs gv-area-nav ${className}`}>
<a
href="#"
onClick={handleBack}
className="gv-flex gv-items-center gv-gap-xs"
role="button"
aria-label="Go back"
style={{
opacity: disabled ? 0.5 : 1,
pointerEvents: disabled ? 'none' : 'auto',
cursor: disabled ? 'not-allowed' : 'pointer'
}}
aria-disabled={disabled ? 'true' : 'false'}
>
<img
style={{ minWidth: "24px" }}
className="gv-tile"
src={`${iconBase}arrow_back.svg`}
alt="Back"
/>
<span>{label}</span>
</a>
{children}
</nav>
);
};

export default Breadcrumbs;
92 changes: 92 additions & 0 deletions frontend/src/components/DeleteModal.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import React, { useEffect } from 'react';
import { createPortal } from 'react-dom';
import {formatMessage, replacePercentWrapper, HtmlRenderer} from '../utils/common.utils';
import { useMarketplace } from '../context/MarketplaceContext';

const DeleteModal = () => {
const { deleteModalState, closeDeleteModal, handlePluginAction, assetsBaseUrl, uiI18n } = useMarketplace();
const { isOpen, plugin } = deleteModalState;

const assetBase = assetsBaseUrl || (typeof window.marketplaceConfig !== "undefined" && window.marketplaceConfig?.assetsBaseUrl) || "";
const iconBase = assetBase ? `${assetBase}assets/icons/` : "";

useEffect(() => {
const handleKeyDown = (event) => {
if (event.key === 'Escape') {
closeDeleteModal();
}
};

if (isOpen) {
window.addEventListener('keydown', handleKeyDown);
document.body.style.overflow = 'hidden';
}

return () => {
window.removeEventListener('keydown', handleKeyDown);
document.body.style.overflow = '';
};
}, [isOpen, closeDeleteModal]);

if (!isOpen || !plugin) return null;

const handleOutsideClick = (e) => {
if (e.target.classList.contains('gv-modal')) {
closeDeleteModal();
}
};

const handleConfirm = () => {
handlePluginAction('delete', plugin, 'addons');
closeDeleteModal();
};

const isPremium = plugin.licenseType === "premium";
const pluginName = plugin.name;

return (
<div className="gv-modal" onClick={handleOutsideClick}>
<div
className="gv-modal-content"
role="dialog"
aria-labelledby="id-modal-title"
aria-modal="true"
onClick={(e) => e.stopPropagation()}
>
<button type="button" className="gv-modal-close" aria-label="Close" onClick={closeDeleteModal}>
<gv-icon aria-hidden="true" src={`${iconBase}close.svg`}></gv-icon>
</button>
<div className="gv-modal-body">
<h2 id="id-modal-title" className="gv-modal-title">
{formatMessage(uiI18n?.deletePlugin, '{0}', pluginName)}?
</h2>
<p>
{formatMessage(uiI18n?.notifications?.deleteModalConfirm, '{0}', pluginName)}
</p>
{isPremium && (<div className="gv-notice gv-notice-info">
<gv-icon
class="gv-notice-icon"
aria-hidden="true"
src={`${iconBase}/info.svg`}
></gv-icon>
<p className="gv-notice-content">
<HtmlRenderer htmlString={replacePercentWrapper(formatMessage(uiI18n?.notifications?.deleteModalInfo, '{0}', pluginName), '<u><a href="https://www.one.com/admin/" target="_blank">', '</a></u>')} />

</p>
</div>
)}
</div>
<div className="gv-button-group">
<button type="button" className="gv-button gv-button-cancel" onClick={closeDeleteModal}>
{uiI18n?.cancel}
</button>
<button type="button" className="gv-button gv-button-destructive" onClick={handleConfirm}>
{uiI18n?.deleteButton}
</button>
</div>
</div>
</div>
);
};

export default DeleteModal;
Loading
Loading