diff --git a/bun.lock b/bun.lock index ad2a5ac2..e02ba835 100644 --- a/bun.lock +++ b/bun.lock @@ -11,8 +11,8 @@ "@floating-ui/react-dom": "2.1.6", "@fontsource/inter": "^5.2.8", "@fontsource/lexend-deca": "5.0.14", + "@fontsource/montserrat": "5.0.19", "@sentry/react": "^10.36.0", - "blurhash": "^2.0.5", "fast-blurhash": "^1.1.4", "image-conversion": "^2.1.1", "react": "^19.2.3", diff --git a/package.json b/package.json index 7dd0d182..9a58de9c 100644 --- a/package.json +++ b/package.json @@ -24,8 +24,8 @@ "@floating-ui/react-dom": "2.1.6", "@fontsource/inter": "^5.2.8", "@fontsource/lexend-deca": "5.0.14", + "@fontsource/montserrat": "5.0.19", "@sentry/react": "^10.36.0", - "blurhash": "^2.0.5", "fast-blurhash": "^1.1.4", "image-conversion": "^2.1.1", "react": "^19.2.3", diff --git a/src/components/Elements/MainModal/scss/marketplace/_main.scss b/src/components/Elements/MainModal/scss/marketplace/_main.scss index 7c6a5732..6d6deea0 100644 --- a/src/components/Elements/MainModal/scss/marketplace/_main.scss +++ b/src/components/Elements/MainModal/scss/marketplace/_main.scss @@ -184,6 +184,64 @@ } } +.itemPage { + display: flex; + flex-flow: row; + justify-content: space-between; + align-items: flex-start; + gap: 30px; + flex-wrap: wrap; + + .itemContent { + display: flex; + flex-flow: column; + gap: 25px; + flex: 1; + + img { + width: 100%; + height: auto; + border-radius: 12px; + } + + table { + table-layout: fixed; + width: 100%; + word-wrap: break-word !important; + font-size: 16px; + border-collapse: collapse; + } + + .itemTop { + display: flex; + align-items: center; + justify-content: center; + width: 28px; + height: 28px; + border-radius: 50%; + background-color: rgba(100, 100, 100, 0.9); + cursor: help; + + svg { + color: white; + font-size: 16px; + } + } + + &:hover .item-installed-badge { + transform: scale(1.05); + } + + &.item-sideloaded { + cursor: default; + + &:hover { + transform: none; + } + } + } +} + .emptyItems { width: 100%; height: 100%; diff --git a/src/components/Form/Settings/Dropdown/Dropdown.jsx b/src/components/Form/Settings/Dropdown/Dropdown.jsx index 0e26f3fa..d4585d45 100644 --- a/src/components/Form/Settings/Dropdown/Dropdown.jsx +++ b/src/components/Form/Settings/Dropdown/Dropdown.jsx @@ -1,6 +1,5 @@ import variables from 'config/variables'; import { memo, useState, useCallback, useRef, useEffect } from 'react'; -import { createPortal } from 'react-dom'; import { MdExpandMore, MdCheck, MdRefresh } from 'react-icons/md'; import { toast } from 'react-toastify'; @@ -9,51 +8,25 @@ import EventBus from 'utils/eventbus'; import './Dropdown.scss'; const Dropdown = memo((props) => { - const [value, setValue] = useState(localStorage.getItem(props.name) || props.items[0]?.value); + const [value, setValue] = useState( + localStorage.getItem(props.name) || props.items[0]?.value, + ); const [isOpen, setIsOpen] = useState(false); - const [isClosing, setIsClosing] = useState(false); const [focusedIndex, setFocusedIndex] = useState(-1); - const [menuPosition, setMenuPosition] = useState({ top: 0, left: 0, width: 0 }); const containerRef = useRef(null); - const controlRef = useRef(null); - const menuRef = useRef(null); const optionsRef = useRef([]); - const closeDropdown = useCallback(() => { - setIsClosing(true); - setTimeout(() => { - setIsOpen(false); - setIsClosing(false); - setFocusedIndex(-1); - }, 200); // Match animation duration - }, []); - useEffect(() => { const handleClickOutside = (event) => { - if ( - containerRef.current && - !containerRef.current.contains(event.target) && - menuRef.current && - !menuRef.current.contains(event.target) - ) { - closeDropdown(); + if (containerRef.current && !containerRef.current.contains(event.target)) { + setIsOpen(false); + setFocusedIndex(-1); } }; document.addEventListener('mousedown', handleClickOutside); return () => document.removeEventListener('mousedown', handleClickOutside); - }, [closeDropdown]); - - useEffect(() => { - if (isOpen && controlRef.current) { - const rect = controlRef.current.getBoundingClientRect(); - setMenuPosition({ - top: rect.bottom + 4, - left: rect.left, - width: rect.width, - }); - } - }, [isOpen]); + }, []); const onChange = useCallback( (newValue) => { @@ -64,7 +37,8 @@ const Dropdown = memo((props) => { variables.stats.postEvent('setting', `${props.name} from ${value} to ${newValue}`); setValue(newValue); - closeDropdown(); + setIsOpen(false); + setFocusedIndex(-1); if (!props.noSetting) { localStorage.setItem(props.name, newValue); @@ -95,23 +69,18 @@ const Dropdown = memo((props) => { case 'Enter': case ' ': e.preventDefault(); - if (isOpen) { - closeDropdown(); - } else { - setIsOpen(true); - } + setIsOpen(!isOpen); break; case 'Escape': - closeDropdown(); + setIsOpen(false); + setFocusedIndex(-1); break; case 'ArrowDown': e.preventDefault(); if (!isOpen) { setIsOpen(true); } else { - setFocusedIndex((prev) => - prev < props.items.filter((i) => i !== null).length - 1 ? prev + 1 : prev, - ); + setFocusedIndex((prev) => (prev < props.items.filter((i) => i !== null).length - 1 ? prev + 1 : prev)); } break; case 'ArrowUp': @@ -157,16 +126,8 @@ const Dropdown = memo((props) => { )}
{ - if (props.disabled) return; - if (isOpen) { - closeDropdown(); - } else { - setIsOpen(true); - } - }} + onClick={() => !props.disabled && setIsOpen(!isOpen)} onKeyDown={handleKeyDown} role="button" aria-haspopup="listbox" @@ -177,39 +138,27 @@ const Dropdown = memo((props) => { {selectedItem?.text || value}
- {(isOpen || isClosing) && - createPortal( -
- {props.items.map((item, index) => - item !== null ? ( -
(optionsRef.current[index] = el)} - className={`dropdown-option ${value === item.value ? 'selected' : ''} ${index === focusedIndex ? 'focused' : ''}`} - onClick={() => onChange(item.value)} - onKeyDown={(e) => handleOptionKeyDown(e, item)} - role="option" - aria-selected={value === item.value} - tabIndex={0} - > - {item.text} - {value === item.value && } -
- ) : null, - )} -
, - document.body, - )} + {isOpen && ( +
+ {props.items.map((item, index) => + item !== null ? ( +
(optionsRef.current[index] = el)} + className={`dropdown-option ${value === item.value ? 'selected' : ''} ${index === focusedIndex ? 'focused' : ''}`} + onClick={() => onChange(item.value)} + onKeyDown={(e) => handleOptionKeyDown(e, item)} + role="option" + aria-selected={value === item.value} + tabIndex={0} + > + {item.text} + {value === item.value && } +
+ ) : null, + )} +
+ )} ); }); diff --git a/src/components/Form/Settings/Dropdown/Dropdown.scss b/src/components/Form/Settings/Dropdown/Dropdown.scss index 29fcf32e..637fad37 100644 --- a/src/components/Form/Settings/Dropdown/Dropdown.scss +++ b/src/components/Form/Settings/Dropdown/Dropdown.scss @@ -13,25 +13,15 @@ } } -@include keyframes(dropdownSlideOut) { - 0% { - opacity: 1; - transform: translateY(0); - } - - 100% { - opacity: 0; - transform: translateY(-10px); - } -} - .dropdown { position: relative; width: 300px; + margin-top: 10px; gap: 8px; display: flex; flex-flow: column; + &.disabled { opacity: 0.5; cursor: not-allowed; @@ -131,99 +121,98 @@ transform: rotate(180deg); } } -} -// Dropdown menu and options are now portaled to body, so they need to be at root level -.dropdown-menu { - max-height: 250px; - overflow-y: auto; - z-index: 10000; - @include animation(dropdownSlideIn 0.2s ease-out); + .dropdown-menu { + position: absolute; + top: calc(100% + 4px); + left: 0; + right: 0; + max-height: 250px; + overflow-y: auto; + z-index: 9999; + @include animation(dropdownSlideIn 0.2s ease-out); - &.closing { - @include animation(dropdownSlideOut 0.2s ease-out); - } - - @include themed { - background: t($modal-background); - border: 1px solid t($modal-sidebarActive); - border-radius: t($borderRadius); - box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); - } - - &::-webkit-scrollbar { - width: 6px; - } - - &::-webkit-scrollbar-track { @include themed { - background: t($modal-sidebar); - } - } - - &::-webkit-scrollbar-thumb { - @include themed { - background: t($modal-sidebarActive); - border-radius: 3px; + background: t($modal-background); + border: 1px solid t($modal-sidebarActive); + border-radius: t($borderRadius); + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); } - &:hover { + &::-webkit-scrollbar { + width: 6px; + } + + &::-webkit-scrollbar-track { @include themed { - background: t($color); + background: t($modal-sidebar); + } + } + + &::-webkit-scrollbar-thumb { + @include themed { + background: t($modal-sidebarActive); + border-radius: 3px; + } + + &:hover { + @include themed { + background: t($color); + } + } + } + } + + .dropdown-option { + display: flex; + align-items: center; + justify-content: space-between; + gap: 8px; + padding: 12px 16px; + cursor: pointer; + transition: all 0.15s ease; + outline: none; + + @include themed { + color: t($color); + + &:hover { + background: t($modal-sidebarActive); + padding-left: 20px; + } + + &.selected { + background: t($modal-sidebar); + font-weight: 500; + } + + &.focused { + background: t($modal-sidebarActive); + border-left: 2px solid t($link); + } + } + + .dropdown-option-text { + flex: 1; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + + .dropdown-option-check { + flex-shrink: 0; + font-size: 14px; + width: 20px; + height: 20px; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + + @include themed { + background: t($link); + color: white; } } } } - -.dropdown-option { - display: flex; - align-items: center; - justify-content: space-between; - gap: 8px; - padding: 12px 16px; - cursor: pointer; - transition: all 0.15s ease; - outline: none; - - @include themed { - color: t($color); - - &:hover { - background: t($modal-sidebarActive); - padding-left: 20px; - } - - &.selected { - background: t($modal-sidebar); - font-weight: 500; - } - - &.focused { - background: t($modal-sidebarActive); - border-left: 2px solid t($link); - } - } - - .dropdown-option-text { - flex: 1; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - } - - .dropdown-option-check { - flex-shrink: 0; - font-size: 14px; - width: 20px; - height: 20px; - border-radius: 50%; - display: flex; - align-items: center; - justify-content: center; - - @include themed { - background: t($link); - color: white; - } - } -} diff --git a/src/features/marketplace/components/Items/Items.jsx b/src/features/marketplace/components/Items/Items.jsx index ee1ef7f4..3884311f 100644 --- a/src/features/marketplace/components/Items/Items.jsx +++ b/src/features/marketplace/components/Items/Items.jsx @@ -1,13 +1,16 @@ import variables from 'config/variables'; import React, { memo, useState, useMemo } from 'react'; import { + MdAutoFixHigh, + MdOutlineArrowForward, + MdOutlineOpenInNew, MdCheckCircle, MdOutlineUploadFile, MdClose, } from 'react-icons/md'; import placeholderIcon from 'assets/icons/marketplace-placeholder.png'; -import { Tooltip } from 'components/Elements'; +import { Button, Tooltip } from 'components/Elements'; import Dropdown from '../../../../components/Form/Settings/Dropdown/Dropdown'; function filterItems(item, filter, categoryFilter) { @@ -57,6 +60,40 @@ function getTypeTranslationKey(type) { function ItemCard({ item, toggleFunction, type, onCollection, isCurator, isInstalled, isAdded, onUninstall }) { 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}`; + +function getTypeTranslationKey(type) { + const typeMap = { + photos: 'photo_packs', + quotes: 'quote_packs', + settings: 'preset_settings', + }; + return typeMap[type] || type; +} + +function ItemCard({ item, toggleFunction, type, onCollection, isCurator, isInstalled, isAdded, onUninstall }) { + item._onCollection = onCollection; + + const isSideloaded = item.sideload === true; + const isSideloaded = item.sideload === true; return ( @@ -92,7 +129,7 @@ function ItemCard({ item, toggleFunction, type, onCollection, isCurator, isInsta )} {isInstalled && item.colour && !isSideloaded && ( -
+
)} diff --git a/src/features/quote/options/QuoteOptions.jsx b/src/features/quote/options/QuoteOptions.jsx index 155f1d3d..88e7a413 100644 --- a/src/features/quote/options/QuoteOptions.jsx +++ b/src/features/quote/options/QuoteOptions.jsx @@ -31,14 +31,6 @@ const QuoteOptions = ({ currentSubSection, onSubSectionChange, sectionName }) => } return type; }); - - // Migration: Force authorDetails on for users upgrading from older versions - useState(() => { - if (localStorage.getItem('authorDetails') === null) { - localStorage.setItem('authorDetails', 'true'); - } - }); - const [customQuote, setCustomQuote] = useState(getCustom()); const handleCustomQuote = (e, text, index, type) => {