From 127b2492a51b0ac13bc33415d0a910bc0280367a Mon Sep 17 00:00:00 2001 From: alexsparkes Date: Sat, 1 Nov 2025 18:26:20 +0000 Subject: [PATCH] feat(Items): enhance ItemCard with installed badge and gradient background; optimize performance with memoization --- .../MainModal/scss/marketplace/_main.scss | 83 +++++++++------- .../marketplace/components/Items/Items.jsx | 97 +++++++++++++++---- 2 files changed, 127 insertions(+), 53 deletions(-) diff --git a/src/components/Elements/MainModal/scss/marketplace/_main.scss b/src/components/Elements/MainModal/scss/marketplace/_main.scss index 80cc95f8..f0a71bf4 100644 --- a/src/components/Elements/MainModal/scss/marketplace/_main.scss +++ b/src/components/Elements/MainModal/scss/marketplace/_main.scss @@ -9,13 +9,6 @@ flex-flow: row !important; } - .item-back { - margin: 0 !important; - filter: blur(40px) saturate(180%) brightness(90%) !important; - height: 15px !important; - width: 200px !important; - } - .item-icon { margin: 0 !important; } @@ -38,26 +31,24 @@ border-radius: 12px; width: auto; cursor: pointer; - transition: 0.5s; + transition: transform 300ms cubic-bezier(0.4, 0, 0.2, 1); display: flex; flex-flow: column; - align-items: center; - text-align: center; + align-items: flex-start; gap: 15px; background-position: center; background-repeat: no-repeat; background-size: cover; + padding: 1.5rem; + will-change: transform; + transform: translate3d(0, 0, 0); // Force GPU acceleration @include themed { background-color: t($modal-secondaryColour); box-shadow: 0 0 0 1px t($modal-sidebarActive); &:hover { - background-color: t($modal-sidebarActive); - - img { - background-color: t($modal-sidebarActive); - } + transform: translate3d(0, -5px, 0); } } @@ -65,34 +56,19 @@ margin-top: 7px; } - .item-back { - filter: blur(60px) saturate(180%) brightness(90%); - position: absolute; - object-fit: cover !important; - height: 90px; - width: 100px; - border-radius: 100px; - transition: 0.5s; - margin-top: 30px; - } - .item-icon { object-fit: cover !important; height: 60px !important; width: 60px !important; border-radius: 12px; transition: 0.5s; - margin-top: 25px; } .card-details { z-index: 1; - padding: 10px; - margin-bottom: 24px; display: flex; flex-flow: column; - gap: 5px; - align-items: center; + align-items: flex-start; .card-title { font-size: 18px; @@ -106,8 +82,16 @@ } } - .card-type { - margin-top: 8px; + .card-chips { + margin-top: 20px; + display: flex; + flex-flow: row; + gap: 8px; + flex-wrap: wrap; + } + + .card-type, + .card-collection { font-size: 12px; font-weight: bolder; @@ -120,6 +104,39 @@ background-color: rgb(255 255 255 / 10%); border: 1px solid rgb(209 213 219 / 30%); } + + .card-collection { + @include themed { + background-color: rgb(255 255 255 / 15%); + border: 1px solid rgb(209 213 219 / 40%); + } + } + } + + .item-installed-badge { + position: absolute; + top: 12px; + right: 12px; + display: flex; + align-items: center; + justify-content: center; + width: 32px; + height: 32px; + border-radius: 50%; + border: 2px solid rgba(255, 255, 255, 0.4); + box-shadow: 0 2px 6px rgba(0, 0, 0, 0.3); + z-index: 2; + pointer-events: none; + will-change: transform; + + svg { + color: white; + font-size: 20px; + } + } + + &:hover .item-installed-badge { + transform: scale(1.05); } } } diff --git a/src/features/marketplace/components/Items/Items.jsx b/src/features/marketplace/components/Items/Items.jsx index 9969f863..93fa0383 100644 --- a/src/features/marketplace/components/Items/Items.jsx +++ b/src/features/marketplace/components/Items/Items.jsx @@ -1,6 +1,6 @@ import variables from 'config/variables'; -import React, { memo, useState } from 'react'; -import { MdAutoFixHigh, MdOutlineArrowForward, MdOutlineOpenInNew } from 'react-icons/md'; +import React, { memo, useState, useMemo } from 'react'; +import { MdAutoFixHigh, MdOutlineArrowForward, MdOutlineOpenInNew, MdCheckCircle } from 'react-icons/md'; import placeholderIcon from 'assets/icons/marketplace-placeholder.png'; import { Button } from 'components/Elements'; @@ -28,21 +28,63 @@ function filterItems(item, filter, categoryFilter) { return textMatch && item.type === categoryMap[categoryFilter]; } -function ItemCard({ item, toggleFunction, type, onCollection, isCurator }) { +function ItemCard({ item, toggleFunction, type, onCollection, isCurator, isInstalled }) { item._onCollection = onCollection; + + // Convert hex color to RGB for gradient with opacity + const hexToRgb = (hex) => { + const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex); + return result + ? { + r: parseInt(result[1], 16), + g: parseInt(result[2], 16), + b: parseInt(result[3], 16), + } + : null; + }; + + const getGradientStyle = () => { + if (!item.colour) return {}; + + const rgb = hexToRgb(item.colour); + if (!rgb) return {}; + + const baseColor = `${rgb.r}, ${rgb.g}, ${rgb.b}`; + + return { + '--item-gradient0': `rgba(${baseColor}, 0.38)`, + '--item-gradient10': `rgba(${baseColor}, 0.35)`, + '--item-gradient75': `rgba(${baseColor}, 0.14)`, + '--item-gradient100': `rgba(${baseColor}, 0.06)`, + backgroundImage: `radial-gradient(circle at center 25%, var(--item-gradient0) 0%, var(--item-gradient10) 10%, var(--item-gradient75) 75%, var(--item-gradient100) 100%)`, + }; + }; + + const getBadgeStyle = () => { + if (!item.colour) return {}; + + const rgb = hexToRgb(item.colour); + if (!rgb) return {}; + + const baseColor = `${rgb.r}, ${rgb.g}, ${rgb.b}`; + + return { + backgroundColor: `rgba(${baseColor}, 0.9)`, + }; + }; + return ( -
toggleFunction(item)} key={item.name}> - { - e.target.onerror = null; - e.target.src = placeholderIcon; - }} - aria-hidden="true" - /> +
toggleFunction(item)} + key={item.name} + style={getGradientStyle()} + > + {isInstalled && item.colour && ( +
+ +
+ )} icon - {variables.getMessage('modals.main.marketplace.' + item.type)} - - ) : null} +
+ {type === 'all' && !onCollection ? ( + + {variables.getMessage('modals.main.marketplace.' + item.type)} + + ) : null} + + {/* {item.in_collections && item.in_collections.length > 0 && !onCollection ? ( + + {item.in_collections[0]} + + ) : null} */} +
); @@ -90,6 +140,12 @@ function Items({ const [selectedCategory, setSelectedCategory] = useState('all'); const [sortType, setSortType] = useState(localStorage.getItem('sortMarketplace') || 'a-z'); + // Cache installed items lookup - only parse localStorage once + const installedNames = useMemo(() => { + const installed = JSON.parse(localStorage.getItem('installed')) || []; + return new Set(installed.map((item) => item.name)); + }, []); + const filterCategories = [ { id: 'all', label: 'All' }, { id: 'quotes', label: 'Quotes' }, @@ -182,6 +238,7 @@ function Items({ toggleFunction={toggleFunction} type={type} onCollection={onCollection} + isInstalled={installedNames.has(item.name)} key={index} /> ))}