feat(Items): enhance ItemCard with installed badge and gradient background; optimize performance with memoization

This commit is contained in:
alexsparkes
2025-11-01 18:26:20 +00:00
parent 9727346ff1
commit 127b2492a5
2 changed files with 127 additions and 53 deletions

View File

@@ -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);
}
}
}

View File

@@ -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 (
<div className="item" onClick={() => toggleFunction(item)} key={item.name}>
<img
className="item-back"
alt=""
draggable={false}
src={item.icon_url}
onError={(e) => {
e.target.onerror = null;
e.target.src = placeholderIcon;
}}
aria-hidden="true"
/>
<div
className="item"
onClick={() => toggleFunction(item)}
key={item.name}
style={getGradientStyle()}
>
{isInstalled && item.colour && (
<div className="item-installed-badge" style={getBadgeStyle()}>
<MdCheckCircle />
</div>
)}
<img
className="item-icon"
alt="icon"
@@ -63,11 +105,19 @@ function ItemCard({ item, toggleFunction, type, onCollection, isCurator }) {
''
)}
{type === 'all' && !onCollection ? (
<span className="card-type">
{variables.getMessage('modals.main.marketplace.' + item.type)}
</span>
) : null}
<div className="card-chips">
{type === 'all' && !onCollection ? (
<span className="card-type">
{variables.getMessage('modals.main.marketplace.' + item.type)}
</span>
) : null}
{/* {item.in_collections && item.in_collections.length > 0 && !onCollection ? (
<span className="card-collection">
{item.in_collections[0]}
</span>
) : null} */}
</div>
</div>
</div>
);
@@ -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}
/>
))}