style(marketplace): potential marketplace page redesign

This commit is contained in:
alexsparkes
2025-11-01 00:50:33 +00:00
parent b303d02492
commit 9727346ff1
11 changed files with 437 additions and 56 deletions

View File

@@ -3,7 +3,7 @@ import Tooltip from 'components/Elements/Tooltip/Tooltip';
const Button = forwardRef( const Button = forwardRef(
( (
{ icon, label, type, iconPlacement, onClick, active, disabled, tooltipTitle, tooltipKey, href }, { icon, label, type, iconPlacement, onClick, active, disabled, tooltipTitle, tooltipKey, href, style },
ref, ref,
) => { ) => {
let className; let className;
@@ -43,14 +43,14 @@ const Button = forwardRef(
} }
const button = ( const button = (
<button className={className} onClick={onClick} ref={ref} disabled={disabled}> <button className={className} onClick={onClick} ref={ref} disabled={disabled} style={style}>
{icon} {icon}
{label} {label}
</button> </button>
); );
const linkButton = ( const linkButton = (
<a className={className} onClick={onClick} ref={ref} disabled={disabled} href={href}> <a className={className} onClick={onClick} ref={ref} disabled={disabled} href={href} style={style}>
{icon} {icon}
{label} {label}
</a> </a>
@@ -66,6 +66,7 @@ const Button = forwardRef(
href={href} href={href}
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
style={style}
> >
{icon} {icon}
{label} {label}

View File

@@ -178,6 +178,7 @@ a.btn-collection {
height: 40px; height: 40px;
display: grid; display: grid;
place-content: center; place-content: center;
border-radius: 8px !important;
@include modal-button(standard); @include modal-button(standard);
} }

View File

@@ -0,0 +1,113 @@
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);

View File

@@ -0,0 +1 @@
export { default } from './InstallButton';

View File

@@ -0,0 +1,166 @@
.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);
}
}

View File

@@ -10,6 +10,7 @@ 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, urlParser } from 'utils/marketplace';
import { updateHash } from 'utils/deepLinking'; import { updateHash } from 'utils/deepLinking';
@@ -173,9 +174,9 @@ const Added = memo(() => {
}, []); // eslint-disable-line react-hooks/exhaustive-deps }, []); // eslint-disable-line react-hooks/exhaustive-deps
const button = item.display_name ? ( const button = item.display_name ? (
<Button <InstallButton
type="settings"
onClick={uninstallItem} onClick={uninstallItem}
isInstalled={true}
label={variables.getMessage('modals.main.marketplace.product.buttons.remove')} label={variables.getMessage('modals.main.marketplace.product.buttons.remove')}
/> />
) : ''; ) : '';

View File

@@ -13,6 +13,7 @@ import {
import ItemPage from './ItemPage'; import ItemPage from './ItemPage';
import Items from '../components/Items/Items'; import Items from '../components/Items/Items';
import { Button } from 'components/Elements'; import { Button } from 'components/Elements';
import InstallButton from '../components/Elements/InstallButton';
import { install, urlParser, uninstall } from 'utils/marketplace'; import { install, urlParser, uninstall } from 'utils/marketplace';
import { updateHash } from 'utils/deepLinking'; import { updateHash } from 'utils/deepLinking';
@@ -34,18 +35,16 @@ class Marketplace extends PureComponent {
}; };
this.buttons = { this.buttons = {
uninstall: ( uninstall: (
<Button <InstallButton
type="settings"
onClick={() => this.manage('uninstall')} onClick={() => this.manage('uninstall')}
icon={<MdClose />} isInstalled={true}
label={variables.getMessage('modals.main.marketplace.product.buttons.remove')} label={variables.getMessage('modals.main.marketplace.product.buttons.remove')}
/> />
), ),
install: ( install: (
<Button <InstallButton
type="settings"
onClick={() => this.manage('install')} onClick={() => this.manage('install')}
icon={<MdLibraryAdd />} isInstalled={false}
label={variables.getMessage('modals.main.marketplace.product.buttons.addtomue')} label={variables.getMessage('modals.main.marketplace.product.buttons.addtomue')}
/> />
), ),

View File

@@ -97,6 +97,95 @@ class ItemPage extends PureComponent {
.sort(() => 0.5 - Math.random()) .sort(() => 0.5 - Math.random())
.slice(0, 3); .slice(0, 3);
// Extract colour from data (British spelling as used in API)
const mainColor = this.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';
// 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 (!this.props.data.display_name) { if (!this.props.data.display_name) {
return null; return null;
} }
@@ -330,12 +419,7 @@ class ItemPage extends PureComponent {
</div> </div>
</div> </div>
</div> </div>
<div <div className="itemInfo">
className="itemInfo"
style={{
backgroundImage: `url("${this.props.data.data.icon_url}")`,
}}
>
<div className="front"> <div className="front">
<img <img
className="icon" className="icon"
@@ -357,30 +441,33 @@ class ItemPage extends PureComponent {
</p> </p>
)} )}
{this.props.data.data.sideload !== true && ( {this.props.data.data.sideload !== true && (
<div className="iconButtons"> <>
<Button {themedStyles}
type="icon" <div className="iconButtons">
onClick={() => this.setState({ shareModal: true })} <Button
icon={<MdIosShare />} type="icon"
tooltipTitle={variables.getMessage('widgets.quote.share')} onClick={() => this.setState({ shareModal: true })}
tooltipKey="share" icon={<MdIosShare />}
/> tooltipTitle={variables.getMessage('widgets.quote.share')}
<Button tooltipKey="share"
type="icon" />
onClick={() => <Button
window.open( type="icon"
variables.constants.REPORT_ITEM + onClick={() =>
this.props.data.data.display_name.split(' ').join('+'), window.open(
'_blank', variables.constants.REPORT_ITEM +
) this.props.data.data.display_name.split(' ').join('+'),
} '_blank',
icon={<MdFlag />} )
tooltipTitle={variables.getMessage( }
'modals.main.marketplace.product.buttons.report', icon={<MdFlag />}
)} tooltipTitle={variables.getMessage(
tooltipKey="report" 'modals.main.marketplace.product.buttons.report',
/> )}
</div> tooltipKey="report"
/>
</div>
</>
)} )}
{this.props.data.data.in_collections?.length > 0 && ( {this.props.data.data.in_collections?.length > 0 && (
<div> <div>

View File

@@ -553,7 +553,7 @@
"part_of": "Part of", "part_of": "Part of",
"explore": "Explore", "explore": "Explore",
"buttons": { "buttons": {
"addtomue": "Add To Mue", "addtomue": "Add To",
"remove": "Remove", "remove": "Remove",
"update_addon": "Update Add-on", "update_addon": "Update Add-on",
"back": "Back", "back": "Back",

View File

@@ -1,16 +1,14 @@
import EventBus from 'utils/eventbus'; import EventBus from 'utils/eventbus';
// todo: relocate these 2 functions // todo: relocate this function
function sleep(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
function showReminder() { function showReminder() {
document.querySelector('.reminder-info').style.display = 'flex'; document.querySelector('.reminder-info').style.display = 'flex';
localStorage.setItem('showReminder', true); localStorage.setItem('showReminder', true);
} }
export function install(type, input, sideload, collection) { export function install(type, input, sideload, collection) {
let refreshEvent = null;
switch (type) { switch (type) {
case 'settings': { case 'settings': {
localStorage.removeItem('backup_settings'); localStorage.removeItem('backup_settings');
@@ -40,12 +38,10 @@ export function install(type, input, sideload, collection) {
} }
localStorage.setItem('backgroundType', 'photo_pack'); localStorage.setItem('backgroundType', 'photo_pack');
localStorage.removeItem('backgroundchange'); localStorage.removeItem('backgroundchange');
EventBus.emit('refresh', 'background'); // Clear image queue to ensure fresh background loads
// TODO: make this legitimately good and work without a reload - currently we just refresh localStorage.removeItem('imageQueue');
sleep(4000); // Set refresh event to emit after installed data is saved
if (!collection) { refreshEvent = 'backgroundrefresh';
window.location.reload();
}
break; break;
} }
@@ -61,7 +57,7 @@ export function install(type, input, sideload, collection) {
} }
localStorage.setItem('quoteType', 'quote_pack'); localStorage.setItem('quoteType', 'quote_pack');
localStorage.removeItem('quotechange'); localStorage.removeItem('quotechange');
EventBus.emit('refresh', 'quote'); refreshEvent = 'quote';
break; break;
} }
@@ -78,4 +74,9 @@ export function install(type, input, sideload, collection) {
installed.push(input); installed.push(input);
localStorage.setItem('installed', JSON.stringify(installed)); localStorage.setItem('installed', JSON.stringify(installed));
// Emit refresh event after all data is saved
if (refreshEvent) {
EventBus.emit('refresh', refreshEvent);
}
} }

View File

@@ -8,6 +8,8 @@ function showReminder() {
export function uninstall(type, name) { export function uninstall(type, name) {
let installedContents, packContents; let installedContents, packContents;
let refreshEvent = null;
switch (type) { switch (type) {
case 'settings': { case 'settings': {
const oldSettings = JSON.parse(localStorage.getItem('backup_settings')); const oldSettings = JSON.parse(localStorage.getItem('backup_settings'));
@@ -39,7 +41,7 @@ export function uninstall(type, name) {
localStorage.removeItem('quote_packs'); localStorage.removeItem('quote_packs');
} }
localStorage.removeItem('quotechange'); localStorage.removeItem('quotechange');
EventBus.emit('refresh', 'marketplacequoteuninstall'); refreshEvent = 'marketplacequoteuninstall';
break; break;
case 'photos': case 'photos':
@@ -55,12 +57,16 @@ export function uninstall(type, name) {
}); });
localStorage.setItem('photo_packs', JSON.stringify(installedContents)); localStorage.setItem('photo_packs', JSON.stringify(installedContents));
if (installedContents.length === 0) { if (installedContents.length === 0) {
// Switch back to old background type or default to mue api
localStorage.setItem('backgroundType', localStorage.getItem('oldBackgroundType') || 'api'); localStorage.setItem('backgroundType', localStorage.getItem('oldBackgroundType') || 'api');
localStorage.removeItem('oldBackgroundType'); localStorage.removeItem('oldBackgroundType');
localStorage.removeItem('photo_packs'); localStorage.removeItem('photo_packs');
} }
localStorage.removeItem('backgroundchange'); localStorage.removeItem('backgroundchange');
EventBus.emit('refresh', 'marketplacebackgrounduninstall'); // Clear image queue to ensure fresh background loads
localStorage.removeItem('imageQueue');
// Set refresh event to emit after installed data is saved
refreshEvent = 'marketplacebackgrounduninstall';
break; break;
default: default:
@@ -76,4 +82,9 @@ export function uninstall(type, name) {
} }
localStorage.setItem('installed', JSON.stringify(installed)); localStorage.setItem('installed', JSON.stringify(installed));
// Emit refresh event after all data is saved
if (refreshEvent) {
EventBus.emit('refresh', refreshEvent);
}
} }