mirror of
https://github.com/mue/mue.git
synced 2026-07-19 06:54:10 +02:00
feat(marketplace): use web ui
This commit is contained in:
@@ -26,6 +26,7 @@ function MainModal({ modalClose, deepLinkData }) {
|
|||||||
const [productView, setProductView] = useState(null);
|
const [productView, setProductView] = useState(null);
|
||||||
const [resetDiscoverToAll, setResetDiscoverToAll] = useState(false);
|
const [resetDiscoverToAll, setResetDiscoverToAll] = useState(false);
|
||||||
const [navigationTrigger, setNavigationTrigger] = useState(null);
|
const [navigationTrigger, setNavigationTrigger] = useState(null);
|
||||||
|
const [iframeBreadcrumbs, setIframeBreadcrumbs] = useState([]);
|
||||||
|
|
||||||
// Clear product view when changing tabs
|
// Clear product view when changing tabs
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -172,6 +173,7 @@ function MainModal({ modalClose, deepLinkData }) {
|
|||||||
currentTab={currentTab}
|
currentTab={currentTab}
|
||||||
currentSection={currentSection}
|
currentSection={currentSection}
|
||||||
productView={productView}
|
productView={productView}
|
||||||
|
iframeBreadcrumbs={iframeBreadcrumbs}
|
||||||
onTabChange={handleChangeTab}
|
onTabChange={handleChangeTab}
|
||||||
onClose={modalClose}
|
onClose={modalClose}
|
||||||
onBack={handleBack}
|
onBack={handleBack}
|
||||||
@@ -187,6 +189,7 @@ function MainModal({ modalClose, deepLinkData }) {
|
|||||||
currentTab={currentTab}
|
currentTab={currentTab}
|
||||||
onSectionChange={handleSectionChange}
|
onSectionChange={handleSectionChange}
|
||||||
onProductView={handleProductView}
|
onProductView={handleProductView}
|
||||||
|
onBreadcrumbsChange={setIframeBreadcrumbs}
|
||||||
resetToAll={resetDiscoverToAll}
|
resetToAll={resetDiscoverToAll}
|
||||||
onResetToAll={handleResetDiscoverToAll}
|
onResetToAll={handleResetDiscoverToAll}
|
||||||
navigationTrigger={navigationTrigger}
|
navigationTrigger={navigationTrigger}
|
||||||
|
|||||||
@@ -47,8 +47,8 @@ const Tabs = ({ children, navbar = false, currentTab: activeTab, onSectionChange
|
|||||||
setShowReminder(false);
|
setShowReminder(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Only show sidebar for Settings tab
|
// Show sidebar for Settings and Discover tabs
|
||||||
const showSidebar = activeTab === TAB_TYPES.SETTINGS;
|
const showSidebar = activeTab === TAB_TYPES.SETTINGS || activeTab === TAB_TYPES.DISCOVER;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ display: 'flex', width: '100%', minHeight: '100%' }}>
|
<div style={{ display: 'flex', width: '100%', minHeight: '100%' }}>
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ function ModalTopBar({
|
|||||||
currentTab,
|
currentTab,
|
||||||
currentSection,
|
currentSection,
|
||||||
productView,
|
productView,
|
||||||
|
iframeBreadcrumbs,
|
||||||
onTabChange,
|
onTabChange,
|
||||||
onClose,
|
onClose,
|
||||||
onBack,
|
onBack,
|
||||||
@@ -34,7 +35,7 @@ function ModalTopBar({
|
|||||||
: '';
|
: '';
|
||||||
|
|
||||||
// Determine breadcrumb path with click handlers
|
// Determine breadcrumb path with click handlers
|
||||||
const breadcrumbPath = [];
|
let breadcrumbPath = [];
|
||||||
|
|
||||||
if (currentTabLabel) {
|
if (currentTabLabel) {
|
||||||
breadcrumbPath.push({
|
breadcrumbPath.push({
|
||||||
@@ -42,7 +43,26 @@ function ModalTopBar({
|
|||||||
onClick: productView ? productView.onBackToAll : null, // Clickable if viewing a product
|
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('ModalTopBar productView:', productView);
|
||||||
console.log('fromCollection:', productView.fromCollection, 'isCollection:', productView.isCollection, 'collectionTitle:', productView.collectionTitle);
|
console.log('fromCollection:', productView.fromCollection, 'isCollection:', productView.isCollection, 'collectionTitle:', productView.collectionTitle);
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ export const OPENSTREETMAP_URL = 'https://www.openstreetmap.org';
|
|||||||
|
|
||||||
// Mue URLs
|
// Mue URLs
|
||||||
export const WEBSITE_URL = 'https://muetab.com';
|
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 PRIVACY_URL = 'https://muetab.com/privacy';
|
||||||
export const TRANSLATIONS_URL = 'https://muetab.com/docs/translations';
|
export const TRANSLATIONS_URL = 'https://muetab.com/docs/translations';
|
||||||
export const WEBLATE_URL = 'https://hosted.weblate.org/projects/mue/mue-tab/';
|
export const WEBLATE_URL = 'https://hosted.weblate.org/projects/mue/mue-tab/';
|
||||||
|
|||||||
@@ -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 (
|
|
||||||
<div className="collection" style={getStyle()}>
|
|
||||||
<div className="content">
|
|
||||||
<span className="title">{display_name} using component</span>
|
|
||||||
<span className="subtitle">{description ? description.substr(0, 75) : ''}</span>
|
|
||||||
</div>
|
|
||||||
{collection.news === true ? (
|
|
||||||
<a
|
|
||||||
className="btn-collection"
|
|
||||||
href={collection.news_link}
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
>
|
|
||||||
{variables.getMessage('modals.main.marketplace.learn_more')} <MdOutlineOpenInNew />
|
|
||||||
</a>
|
|
||||||
) : (
|
|
||||||
<Button
|
|
||||||
type="collection"
|
|
||||||
onClick={() => collectionFunction(collection.name)}
|
|
||||||
icon={<MdOutlineArrowForward />}
|
|
||||||
label={variables.getMessage('modals.main.marketplace.explore_collection')}
|
|
||||||
iconPlacement={'right'}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export { Collection as default, Collection };
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
export * from './Collection';
|
|
||||||
@@ -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 (
|
|
||||||
<div className="carousel">
|
|
||||||
<div className="carousel_viewport" ref={emblaRef}>
|
|
||||||
<div className="carousel_container">
|
|
||||||
{data?.slice(0, 5).map((photo, index) => (
|
|
||||||
<div className="carousel_slide" key={index}>
|
|
||||||
<div className="carousel_slide_inner">
|
|
||||||
<img src={photo.url.default} alt="Marketplace example screenshot" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{data.length > 1 && (
|
|
||||||
<>
|
|
||||||
<button
|
|
||||||
className="carousel_button prev btn-icon"
|
|
||||||
onClick={() => scroll('prev')}
|
|
||||||
disabled={!prevBtnEnabled}
|
|
||||||
>
|
|
||||||
<MdOutlineArrowBackIos />
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
className="carousel_button next btn-icon"
|
|
||||||
onClick={() => scroll('next')}
|
|
||||||
disabled={!nextBtnEnabled}
|
|
||||||
>
|
|
||||||
<MdOutlineArrowForwardIos />
|
|
||||||
</button>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const Carousel = memo(EmblaCarousel);
|
|
||||||
|
|
||||||
export { Carousel as default, Carousel };
|
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
export * from './Carousel';
|
|
||||||
@@ -1,113 +0,0 @@
|
|||||||
import { memo } from 'react';
|
|
||||||
import { MdClose } from 'react-icons/md';
|
|
||||||
import './installButton.scss';
|
|
||||||
|
|
||||||
const MueLogo = () => (
|
|
||||||
<svg
|
|
||||||
width="24"
|
|
||||||
height="24"
|
|
||||||
viewBox="0 0 500 500"
|
|
||||||
fill="none"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
className="mueLogo"
|
|
||||||
>
|
|
||||||
<circle cx="250" cy="250" r="250" fill="url(#paint0_linear_1870_2)" />
|
|
||||||
<path
|
|
||||||
d="M291.02 132.002V164.879H362.13V235.422H395.302V132.002H291.02Z"
|
|
||||||
fill="url(#paint1_linear_1870_2)"
|
|
||||||
/>
|
|
||||||
<path
|
|
||||||
d="M314.592 241.186H285.201V270.542H258.081V241.186H228.669V214.119H258.081V184.784H285.201V214.119H314.592V241.186ZM354.25 171.261H283.307V146.651H164.332V308.676H378.929V242.089H354.25V171.261Z"
|
|
||||||
fill="url(#paint2_linear_1870_2)"
|
|
||||||
/>
|
|
||||||
<path
|
|
||||||
d="M156.949 176.811H134.164V338.836H348.761V316.031H156.949V176.811Z"
|
|
||||||
fill="url(#paint3_linear_1870_2)"
|
|
||||||
/>
|
|
||||||
<path
|
|
||||||
d="M126.785 206.975H104V369H318.597V346.195H126.785V206.975Z"
|
|
||||||
fill="url(#paint4_linear_1870_2)"
|
|
||||||
/>
|
|
||||||
<defs>
|
|
||||||
<linearGradient
|
|
||||||
id="paint0_linear_1870_2"
|
|
||||||
x1="462"
|
|
||||||
y1="120"
|
|
||||||
x2="29.5"
|
|
||||||
y2="383"
|
|
||||||
gradientUnits="userSpaceOnUse"
|
|
||||||
>
|
|
||||||
<stop stopColor="#FF5C25" />
|
|
||||||
<stop offset="0.484375" stopColor="#D21A11" />
|
|
||||||
<stop offset="1" stopColor="#FF456E" />
|
|
||||||
</linearGradient>
|
|
||||||
<linearGradient
|
|
||||||
id="paint1_linear_1870_2"
|
|
||||||
x1="343.161"
|
|
||||||
y1="235.422"
|
|
||||||
x2="343.161"
|
|
||||||
y2="132.002"
|
|
||||||
gradientUnits="userSpaceOnUse"
|
|
||||||
>
|
|
||||||
<stop stopColor="#F18D91" />
|
|
||||||
<stop offset="1" stopColor="#FBD3C6" />
|
|
||||||
</linearGradient>
|
|
||||||
<linearGradient
|
|
||||||
id="paint2_linear_1870_2"
|
|
||||||
x1="271.631"
|
|
||||||
y1="308.676"
|
|
||||||
x2="271.631"
|
|
||||||
y2="146.651"
|
|
||||||
gradientUnits="userSpaceOnUse"
|
|
||||||
>
|
|
||||||
<stop stopColor="#F18D91" />
|
|
||||||
<stop offset="1" stopColor="#FBD3C6" />
|
|
||||||
</linearGradient>
|
|
||||||
<linearGradient
|
|
||||||
id="paint3_linear_1870_2"
|
|
||||||
x1="241.463"
|
|
||||||
y1="338.836"
|
|
||||||
x2="241.463"
|
|
||||||
y2="176.811"
|
|
||||||
gradientUnits="userSpaceOnUse"
|
|
||||||
>
|
|
||||||
<stop stopColor="#F18D91" />
|
|
||||||
<stop offset="1" stopColor="#FBD3C6" />
|
|
||||||
</linearGradient>
|
|
||||||
<linearGradient
|
|
||||||
id="paint4_linear_1870_2"
|
|
||||||
x1="211.298"
|
|
||||||
y1="369"
|
|
||||||
x2="211.298"
|
|
||||||
y2="206.975"
|
|
||||||
gradientUnits="userSpaceOnUse"
|
|
||||||
>
|
|
||||||
<stop stopColor="#F18D91" />
|
|
||||||
<stop offset="1" stopColor="#FBD3C6" />
|
|
||||||
</linearGradient>
|
|
||||||
</defs>
|
|
||||||
</svg>
|
|
||||||
);
|
|
||||||
|
|
||||||
const InstallButton = ({ onClick, isInstalled, label }) => {
|
|
||||||
return (
|
|
||||||
<button
|
|
||||||
className={`installButton ${isInstalled ? 'installed' : 'notInstalled'}`}
|
|
||||||
onClick={onClick}
|
|
||||||
>
|
|
||||||
<span className="buttonContent">
|
|
||||||
<span className="labelText">{label}</span>
|
|
||||||
<span className="iconWrapper">
|
|
||||||
<span className={`icon installIcon ${isInstalled ? 'hide' : 'show'}`}>
|
|
||||||
<MueLogo />
|
|
||||||
</span>
|
|
||||||
<span className={`icon removeIcon ${isInstalled ? 'show' : 'hide'}`}>
|
|
||||||
<MdClose />
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
</button>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default memo(InstallButton);
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
export { default } from './InstallButton';
|
|
||||||
@@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,3 +1,2 @@
|
|||||||
export * from './Carousel';
|
|
||||||
export * from './SideloadFailedModal';
|
export * from './SideloadFailedModal';
|
||||||
export * from './Lightbox';
|
export * from './Lightbox';
|
||||||
|
|||||||
@@ -68,4 +68,4 @@ export const useMarketplaceInstall = () => {
|
|||||||
uninstallItem,
|
uninstallItem,
|
||||||
installCollection,
|
installCollection,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@@ -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 };
|
|
||||||
@@ -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,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
@@ -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, '<br>')),
|
|
||||||
version: info.data.version,
|
|
||||||
icon: info.data.screenshot_url,
|
|
||||||
data: info.data,
|
|
||||||
addonInstalled: isInstalled,
|
|
||||||
addonInstalledVersion: installedVersion,
|
|
||||||
api_name: data.name,
|
|
||||||
};
|
|
||||||
|
|
||||||
setCurrentItem(itemData);
|
|
||||||
setRelatedItems(related);
|
|
||||||
setCurrentView('item');
|
|
||||||
|
|
||||||
// Update hash
|
|
||||||
if (info.data?.id) {
|
|
||||||
if (currentCollection?.name) {
|
|
||||||
updateHash(`#discover/collection/${currentCollection.name}/${info.data.id}`);
|
|
||||||
} else {
|
|
||||||
updateHash(`#discover/${info.data.type}/${info.data.id}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Notify parent
|
|
||||||
if (onProductView) {
|
|
||||||
onProductView({
|
|
||||||
id: info.data.id,
|
|
||||||
type: info.data.type,
|
|
||||||
name: info.data.display_name || info.data.name,
|
|
||||||
fromCollection: !!currentCollection,
|
|
||||||
collectionName: currentCollection?.name,
|
|
||||||
collectionTitle: currentCollection?.title,
|
|
||||||
collectionDescription: currentCollection?.description,
|
|
||||||
collectionImg: currentCollection?.img,
|
|
||||||
onBack: () => navigateBack(),
|
|
||||||
onBackToAll: () => navigateToMain(true),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
document.querySelector('#modal')?.scrollTo(0, 0);
|
|
||||||
variables.stats.postEvent('marketplace-item', `${itemData.display_name} viewed`);
|
|
||||||
} catch (error) {
|
|
||||||
if (!controllerRef.current.signal.aborted) {
|
|
||||||
console.error('Failed to fetch item:', error);
|
|
||||||
toast(variables.getMessage('toasts.error'));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[currentCollection, onProductView],
|
|
||||||
);
|
|
||||||
|
|
||||||
const navigateToCollection = useCallback(
|
|
||||||
async (collectionName) => {
|
|
||||||
try {
|
|
||||||
const response = await fetch(`${API_V2_BASE}/collection/${collectionName}`, {
|
|
||||||
signal: controllerRef.current.signal,
|
|
||||||
});
|
|
||||||
const collection = await response.json();
|
|
||||||
|
|
||||||
if (controllerRef.current.signal.aborted) return;
|
|
||||||
|
|
||||||
const collectionData = {
|
|
||||||
name: collectionName,
|
|
||||||
title: collection.data.display_name,
|
|
||||||
description: collection.data.description,
|
|
||||||
img: collection.data.img,
|
|
||||||
items: collection.data.items,
|
|
||||||
};
|
|
||||||
|
|
||||||
setCurrentCollection(collectionData);
|
|
||||||
setCurrentView('collection');
|
|
||||||
setCurrentItem(null);
|
|
||||||
setRelatedItems([]);
|
|
||||||
|
|
||||||
// Update hash
|
|
||||||
updateHash(`#discover/collection/${collectionName}`);
|
|
||||||
|
|
||||||
// Notify parent
|
|
||||||
if (onProductView) {
|
|
||||||
onProductView({
|
|
||||||
id: collectionName,
|
|
||||||
type: 'collection',
|
|
||||||
name: collection.data.display_name,
|
|
||||||
isCollection: true,
|
|
||||||
fromCollection: false,
|
|
||||||
collectionName: collectionName,
|
|
||||||
collectionTitle: collection.data.display_name,
|
|
||||||
onBack: () => navigateToMain(false),
|
|
||||||
onBackToAll: () => navigateToMain(true),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
if (!controllerRef.current.signal.aborted) {
|
|
||||||
console.error('Failed to fetch collection:', error);
|
|
||||||
toast(variables.getMessage('toasts.error'));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[onProductView],
|
|
||||||
);
|
|
||||||
|
|
||||||
const navigateBack = useCallback(() => {
|
|
||||||
if (currentView === 'item' && currentCollection) {
|
|
||||||
// Go back to collection
|
|
||||||
setCurrentView('collection');
|
|
||||||
setCurrentItem(null);
|
|
||||||
setRelatedItems([]);
|
|
||||||
updateHash(`#discover/collection/${currentCollection.name}`);
|
|
||||||
} else {
|
|
||||||
// Go back to browse
|
|
||||||
navigateToMain(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (onProductView) {
|
|
||||||
onProductView(null);
|
|
||||||
}
|
|
||||||
}, [currentView, currentCollection, onProductView]);
|
|
||||||
|
|
||||||
const navigateToMain = useCallback(
|
|
||||||
(clearCollection) => {
|
|
||||||
if (clearCollection) {
|
|
||||||
setCurrentCollection(null);
|
|
||||||
}
|
|
||||||
setCurrentView('browse');
|
|
||||||
setCurrentItem(null);
|
|
||||||
setRelatedItems([]);
|
|
||||||
|
|
||||||
// Update hash based on collection state
|
|
||||||
if (!clearCollection && currentCollection) {
|
|
||||||
updateHash(`#discover/collection/${currentCollection.name}`);
|
|
||||||
} else {
|
|
||||||
updateHash('#discover/all');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (onProductView) {
|
|
||||||
onProductView(null);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (clearCollection && onResetToAll) {
|
|
||||||
onResetToAll();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[currentCollection, onProductView, onResetToAll],
|
|
||||||
);
|
|
||||||
|
|
||||||
return {
|
|
||||||
currentView,
|
|
||||||
currentItem,
|
|
||||||
currentCollection,
|
|
||||||
relatedItems,
|
|
||||||
navigateToItem,
|
|
||||||
navigateToCollection,
|
|
||||||
navigateBack,
|
|
||||||
navigateToMain,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
@@ -5,32 +5,19 @@ import { toast } from 'react-toastify';
|
|||||||
import Modal from 'react-modal';
|
import Modal from 'react-modal';
|
||||||
|
|
||||||
import { SideloadFailedModal } from '../components/Elements/SideloadFailedModal/SideloadFailedModal';
|
import { SideloadFailedModal } from '../components/Elements/SideloadFailedModal/SideloadFailedModal';
|
||||||
import ItemPage from './ItemPage';
|
|
||||||
import Items from '../components/Items/Items';
|
import Items from '../components/Items/Items';
|
||||||
import { Dropdown, FileUpload } from 'components/Form/Settings';
|
import { Dropdown, FileUpload } from 'components/Form/Settings';
|
||||||
import { Header, CustomActions } from 'components/Layout/Settings';
|
import { Header, CustomActions } from 'components/Layout/Settings';
|
||||||
import { Button } from 'components/Elements';
|
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';
|
import { updateHash } from 'utils/deepLinking';
|
||||||
|
|
||||||
const Added = memo(() => {
|
const Added = memo(() => {
|
||||||
const [installed, setInstalled] = useState(JSON.parse(localStorage.getItem('installed')));
|
const [installed, setInstalled] = useState(JSON.parse(localStorage.getItem('installed')));
|
||||||
const [item, setItem] = useState({});
|
|
||||||
const [showFailed, setShowFailed] = useState(false);
|
const [showFailed, setShowFailed] = useState(false);
|
||||||
const [failedReason, setFailedReason] = useState('');
|
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) => {
|
const installAddon = useCallback((input) => {
|
||||||
let failedReasonText = '';
|
let failedReasonText = '';
|
||||||
if (!input.name) {
|
if (!input.name) {
|
||||||
@@ -83,22 +70,15 @@ const Added = memo(() => {
|
|||||||
|
|
||||||
const toggle = useCallback((type, data) => {
|
const toggle = useCallback((type, data) => {
|
||||||
if (type === 'item') {
|
if (type === 'item') {
|
||||||
const installedItems = JSON.parse(localStorage.getItem('installed'));
|
// Navigate to discover tab with the item
|
||||||
const info = { data: installedItems.find((i) => i.name === data.name) };
|
const itemId = data.name;
|
||||||
|
updateHash(`#discover/all?item=${itemId}`);
|
||||||
setItem({
|
|
||||||
type: info.data.type,
|
// Trigger navigation
|
||||||
display_name: info.data.name,
|
const event = new window.Event('popstate');
|
||||||
author: info.data.author,
|
window.dispatchEvent(event);
|
||||||
description: urlParser(info.data.description.replace(/\n/g, '<br>')),
|
|
||||||
version: info.data.version,
|
|
||||||
icon: info.data.screenshot_url,
|
|
||||||
data: info.data,
|
|
||||||
});
|
|
||||||
|
|
||||||
variables.stats.postEvent('marketplace', 'ItemPage viewed');
|
variables.stats.postEvent('marketplace', 'ItemPage viewed');
|
||||||
} else {
|
|
||||||
setItem({});
|
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
@@ -173,14 +153,6 @@ const Added = memo(() => {
|
|||||||
sortAddons(localStorage.getItem('sortAddons'), false);
|
sortAddons(localStorage.getItem('sortAddons'), false);
|
||||||
}, []); // eslint-disable-line react-hooks/exhaustive-deps
|
}, []); // eslint-disable-line react-hooks/exhaustive-deps
|
||||||
|
|
||||||
const button = item.display_name ? (
|
|
||||||
<InstallButton
|
|
||||||
onClick={uninstallItem}
|
|
||||||
isInstalled={true}
|
|
||||||
label={variables.getMessage('modals.main.marketplace.product.buttons.remove')}
|
|
||||||
/>
|
|
||||||
) : '';
|
|
||||||
|
|
||||||
const sideLoadBackendElements = () => (
|
const sideLoadBackendElements = () => (
|
||||||
<>
|
<>
|
||||||
<Modal
|
<Modal
|
||||||
@@ -240,17 +212,6 @@ const Added = memo(() => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (item.display_name) {
|
|
||||||
return (
|
|
||||||
<ItemPage
|
|
||||||
data={item}
|
|
||||||
button={button}
|
|
||||||
addons={true}
|
|
||||||
toggleFunction={() => toggle()}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Header title={variables.getMessage('modals.main.addons.added')} report={false}>
|
<Header title={variables.getMessage('modals.main.addons.added')} report={false}>
|
||||||
|
|||||||
@@ -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: (
|
|
||||||
<InstallButton
|
|
||||||
onClick={() => handleManage('uninstall')}
|
|
||||||
isInstalled={true}
|
|
||||||
label={variables.getMessage('modals.main.marketplace.product.buttons.remove')}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
install: (
|
|
||||||
<InstallButton
|
|
||||||
onClick={() => handleManage('install')}
|
|
||||||
isInstalled={false}
|
|
||||||
label={variables.getMessage('modals.main.marketplace.product.buttons.addtomue')}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
};
|
|
||||||
|
|
||||||
// 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) => (
|
|
||||||
<>
|
|
||||||
<div className="flexTopMarketplace">
|
|
||||||
<span className="mainTitle">{variables.getMessage('modals.main.navbar.marketplace')}</span>
|
|
||||||
</div>
|
|
||||||
<div className="emptyItems">
|
|
||||||
<div className="emptyMessage">{content}</div>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
|
|
||||||
// Offline check
|
|
||||||
if (navigator.onLine === false || localStorage.getItem('offlineMode') === 'true') {
|
|
||||||
return renderError(
|
|
||||||
<>
|
|
||||||
<MdWifiOff />
|
|
||||||
<span className="title">
|
|
||||||
{variables.getMessage('modals.main.marketplace.offline.title')}
|
|
||||||
</span>
|
|
||||||
<span className="subtitle">
|
|
||||||
{variables.getMessage('modals.main.marketplace.offline.description')}
|
|
||||||
</span>
|
|
||||||
</>,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Loading state
|
|
||||||
if (loading) {
|
|
||||||
return renderError(
|
|
||||||
<div className="loaderHolder">
|
|
||||||
<div id="loader"></div>
|
|
||||||
<span className="subtitle">{variables.getMessage('modals.main.loading')}</span>
|
|
||||||
</div>,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Empty state
|
|
||||||
if (!items || items.length === 0) {
|
|
||||||
return renderError(
|
|
||||||
<>
|
|
||||||
<MdLocalMall />
|
|
||||||
<span className="title">{variables.getMessage('modals.main.addons.empty.title')}</span>
|
|
||||||
<span className="subtitle">
|
|
||||||
{variables.getMessage('modals.main.marketplace.no_items')}
|
|
||||||
</span>
|
|
||||||
</>,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Render item view
|
|
||||||
if (currentView === 'item' && currentItem) {
|
|
||||||
return (
|
|
||||||
<MarketplaceContext.Provider value={contextValue}>
|
|
||||||
<ItemPage
|
|
||||||
data={currentItem}
|
|
||||||
button={getButton()}
|
|
||||||
toggleFunction={(pageType, data) => {
|
|
||||||
if (pageType === 'collection') {
|
|
||||||
navigateToCollection(data);
|
|
||||||
} else if (pageType === 'main') {
|
|
||||||
navigateBack();
|
|
||||||
} else {
|
|
||||||
navigateToItem(data, type);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
addonInstalled={currentItem.addonInstalled}
|
|
||||||
addonInstalledVersion={currentItem.addonInstalledVersion}
|
|
||||||
icon={currentItem.icon}
|
|
||||||
relatedItems={relatedItems}
|
|
||||||
/>
|
|
||||||
</MarketplaceContext.Provider>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Render collection view
|
|
||||||
if (currentView === 'collection' && currentCollection) {
|
|
||||||
return (
|
|
||||||
<MarketplaceContext.Provider value={contextValue}>
|
|
||||||
<CollectionPage
|
|
||||||
collectionName={currentCollection.name}
|
|
||||||
collectionTitle={currentCollection.title}
|
|
||||||
collectionDescription={currentCollection.description}
|
|
||||||
collectionImg={currentCollection.img}
|
|
||||||
items={currentCollection.items}
|
|
||||||
busy={busy}
|
|
||||||
onInstallCollection={() => installCollection(currentCollection.items)}
|
|
||||||
onItemClick={(item) => navigateToItem(item, type)}
|
|
||||||
onSortChange={changeSort}
|
|
||||||
/>
|
|
||||||
</MarketplaceContext.Provider>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Render browse view
|
|
||||||
return (
|
|
||||||
<MarketplaceContext.Provider value={contextValue}>
|
|
||||||
<BrowsePage
|
|
||||||
type={type}
|
|
||||||
items={items}
|
|
||||||
featuredCollection={displayedCollection}
|
|
||||||
filter={filter}
|
|
||||||
onFilterChange={(event) => setFilter(event.target.value)}
|
|
||||||
onItemClick={(item) => navigateToItem(item, type)}
|
|
||||||
onCollectionClick={navigateToCollection}
|
|
||||||
onSortChange={changeSort}
|
|
||||||
/>
|
|
||||||
</MarketplaceContext.Provider>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Marketplace;
|
|
||||||
@@ -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 (
|
|
||||||
<>
|
|
||||||
<div className="headerExtras marketplaceCondition">
|
|
||||||
{type !== 'collections' && (
|
|
||||||
<div>
|
|
||||||
<form className="marketplaceSearch">
|
|
||||||
<input
|
|
||||||
label={variables.getMessage('widgets.search')}
|
|
||||||
placeholder={variables.getMessage('widgets.search')}
|
|
||||||
name="filter"
|
|
||||||
id="filter"
|
|
||||||
value={filter}
|
|
||||||
onChange={onFilterChange}
|
|
||||||
/>
|
|
||||||
<MdSearch />
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{type === 'collections' ? (
|
|
||||||
items.map((item) =>
|
|
||||||
!item.news ? (
|
|
||||||
<div
|
|
||||||
key={item.name}
|
|
||||||
className="collection"
|
|
||||||
style={
|
|
||||||
item.news
|
|
||||||
? { backgroundColor: item.background_colour }
|
|
||||||
: {
|
|
||||||
backgroundImage: `linear-gradient(to left, #000, transparent, #000), url('${item.img}')`,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<div className="content">
|
|
||||||
<span className="title">{item.display_name}</span>
|
|
||||||
<span className="subtitle">{item.description}</span>
|
|
||||||
</div>
|
|
||||||
<Button
|
|
||||||
type="collection"
|
|
||||||
onClick={() => onCollectionClick(item.name)}
|
|
||||||
icon={<MdOutlineArrowForward />}
|
|
||||||
label={variables.getMessage('modals.main.marketplace.explore_collection')}
|
|
||||||
iconPlacement="right"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
) : null,
|
|
||||||
)
|
|
||||||
) : (
|
|
||||||
<Items
|
|
||||||
filterOptions={true}
|
|
||||||
type={type}
|
|
||||||
items={items}
|
|
||||||
collection={featuredCollection}
|
|
||||||
onCollection={false}
|
|
||||||
toggleFunction={onItemClick}
|
|
||||||
collectionFunction={onCollectionClick}
|
|
||||||
filter={filter}
|
|
||||||
onSortChange={onSortChange}
|
|
||||||
showCreateYourOwn={true}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export { BrowsePage as default, BrowsePage };
|
|
||||||
@@ -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 (
|
|
||||||
<>
|
|
||||||
<div
|
|
||||||
className="collectionPage"
|
|
||||||
style={{
|
|
||||||
backgroundImage: `linear-gradient(to bottom, transparent, black), url('${collectionImg}')`,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div className="nice-tag">{variables.getMessage('modals.main.marketplace.collection')}</div>
|
|
||||||
<div className="content">
|
|
||||||
<span className="mainTitle">{collectionTitle}</span>
|
|
||||||
<span className="subtitle">{collectionDescription}</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Button
|
|
||||||
type="collection"
|
|
||||||
onClick={onInstallCollection}
|
|
||||||
disabled={busy}
|
|
||||||
icon={<MdLibraryAdd />}
|
|
||||||
label={
|
|
||||||
busy
|
|
||||||
? variables.getMessage('modals.main.marketplace.installing')
|
|
||||||
: variables.getMessage('modals.main.marketplace.add_all')
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Items
|
|
||||||
filterOptions={true}
|
|
||||||
items={items}
|
|
||||||
collection={null}
|
|
||||||
onCollection={true}
|
|
||||||
toggleFunction={onItemClick}
|
|
||||||
collectionFunction={() => {}}
|
|
||||||
filter={''}
|
|
||||||
onSortChange={onSortChange}
|
|
||||||
showCreateYourOwn={false}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export { CollectionPage as default, CollectionPage };
|
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
|
|
||||||
import variables from 'config/variables';
|
|
||||||
import { MdOutlineExtensionOff } from 'react-icons/md';
|
|
||||||
import { Button } from 'components/Elements';
|
|
||||||
|
|
||||||
const Create = () => {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<div className="flexTopMarketplace">
|
|
||||||
<span className="mainTitle">
|
|
||||||
{variables.getMessage('modals.main.addons.create.title')}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className="emptyItems">
|
|
||||||
<div className="emptyNewMessage">
|
|
||||||
<MdOutlineExtensionOff />
|
|
||||||
<span className="title">
|
|
||||||
{variables.getMessage('modals.main.addons.create.moved_title')}
|
|
||||||
</span>
|
|
||||||
<span className="subtitle">
|
|
||||||
{variables.getMessage('modals.main.addons.create.moved_description')}
|
|
||||||
</span>
|
|
||||||
<div className="createButtons">
|
|
||||||
<Button
|
|
||||||
type="settings"
|
|
||||||
label={variables.getMessage('modals.main.addons.create.moved_button')}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export { Create as default, Create };
|
|
||||||
@@ -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 ? (
|
|
||||||
<style>{`
|
|
||||||
/* Icon buttons styling */
|
|
||||||
.iconButtons .btn-icon {
|
|
||||||
background: ${mainColor} !important;
|
|
||||||
background-image: none !important;
|
|
||||||
border-color: ${mainColor} !important;
|
|
||||||
box-shadow: 0 0 0 1px ${mainColor}, 0 4px 12px ${mainColor}40 !important;
|
|
||||||
color: ${textColor} !important;
|
|
||||||
}
|
|
||||||
.iconButtons .btn-icon:hover {
|
|
||||||
background: ${mainColor} !important;
|
|
||||||
filter: brightness(${isLight ? '0.95' : '1.15'});
|
|
||||||
transform: translateY(-2px);
|
|
||||||
box-shadow: 0 0 0 1px ${mainColor}, 0 6px 20px ${mainColor}60 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ItemInfo background gradient */
|
|
||||||
.itemInfo {
|
|
||||||
background: linear-gradient(135deg, ${mainColor}ee 0%, ${mainColor}aa 50%, ${mainColor}66 100%) !important;
|
|
||||||
box-shadow: 0 8px 32px ${mainColor}40 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Icon shadow */
|
|
||||||
.itemInfo .icon {
|
|
||||||
box-shadow: 0 8px 32px ${mainColor}80, 0 0 0 1px ${mainColor}40 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Install button styling */
|
|
||||||
.itemInfo .installButton {
|
|
||||||
background: ${mainColor} !important;
|
|
||||||
background-image: linear-gradient(135deg, ${mainColor} 0%, ${mainColor}dd 100%) !important;
|
|
||||||
box-shadow: 0 4px 16px ${mainColor}60 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.itemInfo .installButton:hover {
|
|
||||||
background: ${mainColor} !important;
|
|
||||||
filter: brightness(${isLight ? '0.95' : '1.15'});
|
|
||||||
box-shadow: 0 6px 24px ${mainColor}80 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Install button text and icon color */
|
|
||||||
.itemInfo .installButton span,
|
|
||||||
.itemInfo .installButton svg {
|
|
||||||
color: ${textColor} !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Mue logo - circle matches text color, paths match button color */
|
|
||||||
.itemInfo .installButton .mueLogo circle {
|
|
||||||
fill: ${textColor} !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.itemInfo .installButton .mueLogo path {
|
|
||||||
fill: ${mainColor} !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Remove the default gradient animation when themed */
|
|
||||||
.itemInfo .installButton.installed {
|
|
||||||
background: ${mainColor}aa !important;
|
|
||||||
background-image: none !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.itemInfo .installButton.installed:hover {
|
|
||||||
background: ${mainColor}99 !important;
|
|
||||||
}
|
|
||||||
`}</style>
|
|
||||||
) : null;
|
|
||||||
|
|
||||||
if (!props.data.display_name) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
let updateButton;
|
|
||||||
if (showUpdateButton) {
|
|
||||||
updateButton = (
|
|
||||||
<Fragment key="update">
|
|
||||||
<Button
|
|
||||||
type="settings"
|
|
||||||
onClick={() => updateAddon()}
|
|
||||||
label={variables.getMessage('modals.main.addons.product.buttons.update_addon')}
|
|
||||||
/>
|
|
||||||
</Fragment>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// prevent console error
|
|
||||||
let iconsrc = props.data.icon;
|
|
||||||
if (!props.data.icon) {
|
|
||||||
iconsrc = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Modal
|
|
||||||
closeTimeoutMS={300}
|
|
||||||
isOpen={shareModal}
|
|
||||||
className="Modal mainModal"
|
|
||||||
overlayClassName="Overlay"
|
|
||||||
ariaHideApp={false}
|
|
||||||
onRequestClose={() => setShareModal(false)}
|
|
||||||
>
|
|
||||||
<ShareModal
|
|
||||||
data={variables.constants.API_URL + '/marketplace/share/' + btoa(props.data.api_name)}
|
|
||||||
modalClose={() => setShareModal(false)}
|
|
||||||
/>
|
|
||||||
</Modal>
|
|
||||||
{/* <Header
|
|
||||||
title={
|
|
||||||
props.addons
|
|
||||||
? variables.getMessage('modals.main.addons.added')
|
|
||||||
: props.data.onCollection && props.data.data.in_collections?.length > 0
|
|
||||||
? props.data.data.in_collections[0].display_name
|
|
||||||
: variables.getMessage('modals.main.navbar.marketplace')
|
|
||||||
}
|
|
||||||
secondaryTitle={
|
|
||||||
props.data.data.sideload
|
|
||||||
? props.data.data.name
|
|
||||||
: props.data.data.display_name
|
|
||||||
}
|
|
||||||
report={false}
|
|
||||||
goBack={props.toggleFunction}
|
|
||||||
/> */}
|
|
||||||
<div className="itemPage">
|
|
||||||
<aside className="itemInfo">
|
|
||||||
<div className="front">
|
|
||||||
<img
|
|
||||||
className="icon"
|
|
||||||
alt="icon"
|
|
||||||
draggable={false}
|
|
||||||
src={props.data.data.icon_url}
|
|
||||||
onError={(e) => {
|
|
||||||
e.target.onerror = null;
|
|
||||||
e.target.src = placeholderIcon;
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
{localStorage.getItem('welcomePreview') !== 'true' ? (
|
|
||||||
<>
|
|
||||||
{props.button}
|
|
||||||
{updateButton}
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<p style={{ textAlign: 'center' }}>
|
|
||||||
{variables.getMessage('modals.main.marketplace.product.buttons.not_available_preview')}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
{props.data.data.sideload !== true && (
|
|
||||||
<>
|
|
||||||
{themedStyles}
|
|
||||||
<div className="iconButtons">
|
|
||||||
<Button
|
|
||||||
type="icon"
|
|
||||||
onClick={() => setShareModal(true)}
|
|
||||||
icon={<MdIosShare />}
|
|
||||||
tooltipTitle={variables.getMessage('widgets.quote.share')}
|
|
||||||
tooltipKey="share"
|
|
||||||
/>
|
|
||||||
<Button
|
|
||||||
type="icon"
|
|
||||||
onClick={() =>
|
|
||||||
window.open(
|
|
||||||
variables.constants.REPORT_ITEM +
|
|
||||||
props.data.data.display_name.split(' ').join('+'),
|
|
||||||
'_blank',
|
|
||||||
)
|
|
||||||
}
|
|
||||||
icon={<MdFlag />}
|
|
||||||
tooltipTitle={variables.getMessage('modals.main.marketplace.product.buttons.report')}
|
|
||||||
tooltipKey="report"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
{props.data.data.in_collections?.length > 0 && (
|
|
||||||
<div className="inCollection">
|
|
||||||
<span className="subtitle">
|
|
||||||
{variables.getMessage('modals.main.marketplace.product.part_of')}
|
|
||||||
</span>
|
|
||||||
<span
|
|
||||||
className="title"
|
|
||||||
onClick={() =>
|
|
||||||
props.toggleFunction('collection', props.data.data.in_collections[0].name)
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{props.data.data.in_collections[0].display_name}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</aside>
|
|
||||||
<div className="itemContent">
|
|
||||||
<div className="itemTop">
|
|
||||||
<div className="subHeader">
|
|
||||||
<InfoItem
|
|
||||||
icon={<MdAccountCircle />}
|
|
||||||
header={variables.getMessage('modals.main.marketplace.product.created_by')}
|
|
||||||
text={props.data.author}
|
|
||||||
/>
|
|
||||||
<WarningBanner data={props.data.data} shortLocale={shortLocale} />
|
|
||||||
</div>
|
|
||||||
<div className="itemTabs">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className={`itemTab ${activeTab === 'overview' ? 'active' : ''}`}
|
|
||||||
onClick={() => setActiveTab('overview')}
|
|
||||||
>
|
|
||||||
{variables.getMessage('modals.main.marketplace.product.overview_tab') || 'Overview'}
|
|
||||||
</button>
|
|
||||||
{hasQuotes && (
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className={`itemTab ${activeTab === 'quotes' ? 'active' : ''}`}
|
|
||||||
onClick={() => setActiveTab('quotes')}
|
|
||||||
>
|
|
||||||
{variables.getMessage('modals.main.marketplace.product.quotes_tab') || 'Quotes'}
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{hasPhotos && (
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className={`itemTab ${activeTab === 'photos' ? 'active' : ''}`}
|
|
||||||
onClick={() => setActiveTab('photos')}
|
|
||||||
>
|
|
||||||
{variables.getMessage('modals.main.marketplace.product.photos_tab') || 'Photos'}
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{hasSettings && (
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className={`itemTab ${activeTab === 'presets' ? 'active' : ''}`}
|
|
||||||
onClick={() => setActiveTab('presets')}
|
|
||||||
>
|
|
||||||
{variables.getMessage('modals.main.marketplace.product.presets_tab') || 'Presets'}
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="tabContent">
|
|
||||||
{activeTab === 'overview' && (
|
|
||||||
<OverviewTab
|
|
||||||
data={props.data.data}
|
|
||||||
description={props.data.description}
|
|
||||||
iconsrc={iconsrc}
|
|
||||||
shortLocale={shortLocale}
|
|
||||||
languageNames={languageNames}
|
|
||||||
formattedDate={formattedDate}
|
|
||||||
getName={getName}
|
|
||||||
count={count}
|
|
||||||
onIncrementCount={(type) => incrementCount(type)}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{activeTab === 'quotes' && hasQuotes && (
|
|
||||||
<QuotesTab
|
|
||||||
quotes={quotes}
|
|
||||||
count={count}
|
|
||||||
onIncrementCount={(type) => incrementCount(type)}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{activeTab === 'photos' && hasPhotos && <PhotosTab photos={photos} />}
|
|
||||||
{activeTab === 'presets' && hasSettings && (
|
|
||||||
<PresetsTab
|
|
||||||
settings={settings}
|
|
||||||
count={count}
|
|
||||||
onIncrementCount={(type) => incrementCount(type)}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{/* {moreByCurator.length > 1 && (
|
|
||||||
<div className="moreFromCurator">
|
|
||||||
<span className="title">
|
|
||||||
{variables.getMessage('modals.main.marketplace.product.more_from_curator', {
|
|
||||||
name: props.data.author,
|
|
||||||
})}
|
|
||||||
</span>
|
|
||||||
<div>
|
|
||||||
<Items
|
|
||||||
isCurator={true}
|
|
||||||
type={props.data.data.type}
|
|
||||||
items={moreByCurator}
|
|
||||||
onCollection={state.collection}
|
|
||||||
toggleFunction={(input) => props.toggleFunction('item', input)}
|
|
||||||
collectionFunction={(input) => props.toggleFunction('collection', input)}
|
|
||||||
filter={''}
|
|
||||||
moreByCreator={true}
|
|
||||||
showCreateYourOwn={false}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)} */}
|
|
||||||
{props.relatedItems && props.relatedItems.length > 0 && (
|
|
||||||
<div className="moreFromCurator">
|
|
||||||
<span className="title">
|
|
||||||
{variables.getMessage('modals.main.marketplace.you_might_also_like') ||
|
|
||||||
'You might also like'}
|
|
||||||
</span>
|
|
||||||
<div>
|
|
||||||
<Items
|
|
||||||
type={props.data.data.type}
|
|
||||||
items={props.relatedItems}
|
|
||||||
onCollection={false}
|
|
||||||
toggleFunction={(input) => props.toggleFunction('item', input)}
|
|
||||||
collectionFunction={(input) => props.toggleFunction('collection', input)}
|
|
||||||
filter={''}
|
|
||||||
moreByCreator={false}
|
|
||||||
showCreateYourOwn={false}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export { ItemPage as default, ItemPage };
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
const InfoItem = ({ icon, header, text }) => (
|
|
||||||
<div className="infoItem">
|
|
||||||
{icon}
|
|
||||||
<div className="text">
|
|
||||||
<span className="header">{header}</span>
|
|
||||||
<span>{text}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
export default InfoItem;
|
|
||||||
@@ -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 && (
|
|
||||||
<img
|
|
||||||
alt="product"
|
|
||||||
draggable={false}
|
|
||||||
src={iconsrc}
|
|
||||||
onError={(e) => {
|
|
||||||
e.target.onerror = null;
|
|
||||||
e.target.src = placeholderIcon;
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Description */}
|
|
||||||
<div className="marketplaceDescription">
|
|
||||||
<span className="title">
|
|
||||||
{variables.getMessage('modals.main.marketplace.product.description')}
|
|
||||||
</span>
|
|
||||||
<span className="subtitle" dangerouslySetInnerHTML={{ __html: description }} />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Quote pack statistics */}
|
|
||||||
{hasQuotes && (
|
|
||||||
<div className="itemHighlights">
|
|
||||||
<div className="highlightCard">
|
|
||||||
<span className="highlightLabel">{totalQuotesLabel}</span>
|
|
||||||
<span className="highlightValue">{formatNumber(totalQuotes)}</span>
|
|
||||||
</div>
|
|
||||||
<div className="highlightCard">
|
|
||||||
<span className="highlightLabel">{uniqueAuthorsLabel}</span>
|
|
||||||
<span className="highlightValue">{formatNumber(uniqueAuthorsCount)}</span>
|
|
||||||
</div>
|
|
||||||
<div className="highlightCard">
|
|
||||||
<span className="highlightLabel">{averageCharactersLabel}</span>
|
|
||||||
<span className="highlightValue">{formatNumber(averageCharacters)}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Details section */}
|
|
||||||
<div className="marketplaceDescription marketplaceDetails">
|
|
||||||
<span className="title">
|
|
||||||
{variables.getMessage('modals.main.marketplace.product.details')}
|
|
||||||
</span>
|
|
||||||
<div className="moreInfo">
|
|
||||||
{data.updated_at && (
|
|
||||||
<InfoItem
|
|
||||||
icon={<MdCalendarMonth />}
|
|
||||||
header={variables.getMessage('modals.main.marketplace.product.updated_at')}
|
|
||||||
text={formattedDate}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{hasQuotes && (
|
|
||||||
<InfoItem
|
|
||||||
icon={<MdFormatQuote />}
|
|
||||||
header={variables.getMessage('modals.main.marketplace.product.no_quotes')}
|
|
||||||
text={quotes.length}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{hasPhotos && (
|
|
||||||
<InfoItem
|
|
||||||
icon={<MdImage />}
|
|
||||||
header={variables.getMessage('modals.main.marketplace.product.no_images')}
|
|
||||||
text={photos.length}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{hasQuotes && data.language && (
|
|
||||||
<InfoItem
|
|
||||||
icon={<MdTranslate />}
|
|
||||||
header={variables.getMessage('modals.main.settings.sections.language.title')}
|
|
||||||
text={languageNames.of(data.language)}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<InfoItem
|
|
||||||
icon={<MdStyle />}
|
|
||||||
header={variables.getMessage('modals.main.settings.sections.background.type.title')}
|
|
||||||
text={
|
|
||||||
variables.getMessage('modals.main.marketplace.' + getName(data.type)) || 'marketplace'
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default OverviewTab;
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
import { Carousel } from '../../components/Elements/Carousel';
|
|
||||||
|
|
||||||
const PhotosTab = ({ photos }) => {
|
|
||||||
return (
|
|
||||||
<div className="carousel">
|
|
||||||
<div className="carousel_container">
|
|
||||||
<Carousel data={photos} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default PhotosTab;
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
import variables from 'config/variables';
|
|
||||||
|
|
||||||
const PresetsTab = ({ settings, count, onIncrementCount }) => {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<table>
|
|
||||||
<tbody>
|
|
||||||
<tr>
|
|
||||||
<th>{variables.getMessage('modals.main.marketplace.product.setting')}</th>
|
|
||||||
<th>{variables.getMessage('modals.main.marketplace.product.value')}</th>
|
|
||||||
</tr>
|
|
||||||
{Object.entries(settings)
|
|
||||||
.slice(0, count)
|
|
||||||
.map(([key, value]) => (
|
|
||||||
<tr key={key}>
|
|
||||||
<td>{key}</td>
|
|
||||||
<td>{value}</td>
|
|
||||||
</tr>
|
|
||||||
))}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
<div className="showMoreItems">
|
|
||||||
<span className="link" onClick={() => onIncrementCount('settings')}>
|
|
||||||
{count !== Object.keys(settings).length
|
|
||||||
? variables.getMessage('modals.main.marketplace.product.show_all')
|
|
||||||
: variables.getMessage('modals.main.marketplace.product.show_less')}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default PresetsTab;
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
import variables from 'config/variables';
|
|
||||||
|
|
||||||
const QuotesTab = ({ quotes, count, onIncrementCount }) => {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<table>
|
|
||||||
<tbody>
|
|
||||||
<tr>
|
|
||||||
<th>{variables.getMessage('modals.main.settings.sections.quote.title')}</th>
|
|
||||||
<th>{variables.getMessage('modals.main.settings.sections.quote.author')}</th>
|
|
||||||
</tr>
|
|
||||||
{quotes.slice(0, count).map((quote, index) => (
|
|
||||||
<tr key={index}>
|
|
||||||
<td>{quote.quote}</td>
|
|
||||||
<td>{quote.author}</td>
|
|
||||||
</tr>
|
|
||||||
))}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
<div className="showMoreItems">
|
|
||||||
<span className="link" onClick={() => onIncrementCount('quotes')}>
|
|
||||||
{count !== quotes.length
|
|
||||||
? variables.getMessage('modals.main.marketplace.product.show_all')
|
|
||||||
: variables.getMessage('modals.main.marketplace.product.show_less')}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default QuotesTab;
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
import variables from 'config/variables';
|
|
||||||
import { MdOutlineWarning } from 'react-icons/md';
|
|
||||||
|
|
||||||
const WarningBanner = ({ data, shortLocale }) => {
|
|
||||||
const template = (message) => (
|
|
||||||
<div className="itemWarning">
|
|
||||||
<MdOutlineWarning />
|
|
||||||
<div className="text">
|
|
||||||
<span className="header">Warning</span>
|
|
||||||
<span>{message}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
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;
|
|
||||||
@@ -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';
|
|
||||||
@@ -1,75 +1,239 @@
|
|||||||
import variables from 'config/variables';
|
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';
|
function DiscoverContent({ category, onBreadcrumbsChange }) {
|
||||||
import MarketplaceTab from '../../marketplace/views/Browse';
|
const iframeRef = useRef(null);
|
||||||
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
|
const { installItem, uninstallItem } = useMarketplaceInstall();
|
||||||
|
|
||||||
function Discover({
|
// Check for offline mode
|
||||||
changeTab,
|
const offlineMode = localStorage.getItem('offlineMode') === 'true';
|
||||||
deepLinkData,
|
const isOffline = navigator.onLine === false || offlineMode;
|
||||||
currentTab,
|
|
||||||
onSectionChange,
|
useEffect(() => {
|
||||||
onProductView,
|
// Show loader when category changes
|
||||||
resetToAll,
|
setIsLoading(true);
|
||||||
onResetToAll,
|
// Clear breadcrumbs when navigating to a new category
|
||||||
navigationTrigger,
|
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 (
|
||||||
|
<div className="emptyItems">
|
||||||
|
<div className="emptyMessage">
|
||||||
|
<MdOutlineWifiOff />
|
||||||
|
<h1>{variables.getMessage('modals.main.marketplace.offline.title')}</h1>
|
||||||
|
<p className="description">
|
||||||
|
{variables.getMessage('modals.main.marketplace.offline.description')}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{ position: 'relative', width: '100%', minHeight: '100vh' }}>
|
||||||
|
{isLoading && (
|
||||||
|
<div className="loaderHolder" style={{
|
||||||
|
position: 'absolute',
|
||||||
|
top: '50%',
|
||||||
|
left: '50%',
|
||||||
|
transform: 'translate(-50%, -50%)',
|
||||||
|
zIndex: 10
|
||||||
|
}}>
|
||||||
|
<div id="loader"></div>
|
||||||
|
<span className="subtitle">{variables.getMessage('modals.main.loading')}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<iframe
|
||||||
|
ref={iframeRef}
|
||||||
|
src={category === 'collections' ? `${MARKETPLACE_URL}/collections?embed=true` : `${MARKETPLACE_URL}?embed=true&type=${category}`}
|
||||||
|
onLoad={handleLoad}
|
||||||
|
scrolling="no"
|
||||||
|
style={{
|
||||||
|
width: '100%',
|
||||||
|
height: '2000px',
|
||||||
|
minHeight: '100vh',
|
||||||
|
border: 'none',
|
||||||
|
opacity: isLoading ? 0 : 1,
|
||||||
|
transition: 'opacity 0.2s ease-in-out',
|
||||||
|
overflow: 'hidden',
|
||||||
|
}}
|
||||||
|
title="Marketplace"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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 (
|
return (
|
||||||
<Tabs
|
<Tabs
|
||||||
changeTab={(type) => changeTab(type)}
|
changeTab={(type) => props.changeTab(type)}
|
||||||
current="discover"
|
current="discover"
|
||||||
currentTab={currentTab}
|
currentTab={props.currentTab}
|
||||||
onSectionChange={onSectionChange}
|
onSectionChange={props.onSectionChange}
|
||||||
resetToFirst={resetToAll}
|
|
||||||
>
|
>
|
||||||
<div label={variables.getMessage('modals.main.marketplace.all')} name="all">
|
{sections.map(({ label, name }) => (
|
||||||
<MarketplaceTab
|
<div key={name} label={variables.getMessage(label)} name={name}>
|
||||||
type="all"
|
<DiscoverContent category={name} onBreadcrumbsChange={props.onBreadcrumbsChange} />
|
||||||
deepLinkData={deepLinkData}
|
</div>
|
||||||
onProductView={onProductView}
|
))}
|
||||||
onResetToAll={onResetToAll}
|
|
||||||
navigationTrigger={navigationTrigger}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div label={variables.getMessage('modals.main.marketplace.photo_packs')} name="photo_packs">
|
|
||||||
<MarketplaceTab
|
|
||||||
type="photo_packs"
|
|
||||||
deepLinkData={deepLinkData}
|
|
||||||
onProductView={onProductView}
|
|
||||||
onResetToAll={onResetToAll}
|
|
||||||
navigationTrigger={navigationTrigger}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div label={variables.getMessage('modals.main.marketplace.quote_packs')} name="quote_packs">
|
|
||||||
<MarketplaceTab
|
|
||||||
type="quote_packs"
|
|
||||||
deepLinkData={deepLinkData}
|
|
||||||
onProductView={onProductView}
|
|
||||||
onResetToAll={onResetToAll}
|
|
||||||
navigationTrigger={navigationTrigger}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
label={variables.getMessage('modals.main.marketplace.preset_settings')}
|
|
||||||
name="preset_settings"
|
|
||||||
>
|
|
||||||
<MarketplaceTab
|
|
||||||
type="preset_settings"
|
|
||||||
deepLinkData={deepLinkData}
|
|
||||||
onProductView={onProductView}
|
|
||||||
onResetToAll={onResetToAll}
|
|
||||||
navigationTrigger={navigationTrigger}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div label={variables.getMessage('modals.main.marketplace.collections')} name="collections">
|
|
||||||
<MarketplaceTab
|
|
||||||
type="collections"
|
|
||||||
deepLinkData={deepLinkData}
|
|
||||||
onProductView={onProductView}
|
|
||||||
onResetToAll={onResetToAll}
|
|
||||||
navigationTrigger={navigationTrigger}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</Tabs>
|
</Tabs>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import { memo } from 'react';
|
|||||||
import Tabs from 'components/Elements/MainModal/backend/Tabs';
|
import Tabs from 'components/Elements/MainModal/backend/Tabs';
|
||||||
|
|
||||||
import Added from '../../marketplace/views/Added';
|
import Added from '../../marketplace/views/Added';
|
||||||
import Create from '../../marketplace/views/Create';
|
|
||||||
|
|
||||||
function Library(props) {
|
function Library(props) {
|
||||||
return (
|
return (
|
||||||
@@ -17,7 +16,6 @@ function Library(props) {
|
|||||||
<Added />
|
<Added />
|
||||||
</div>
|
</div>
|
||||||
<div label={variables.getMessage('modals.main.addons.create.title')} name="create">
|
<div label={variables.getMessage('modals.main.addons.create.title')} name="create">
|
||||||
<Create />
|
|
||||||
</div>
|
</div>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user