🆙 100% Guild Furni Catalog Page

This commit is contained in:
duckietm
2026-06-11 13:16:29 +02:00
parent 40864cf880
commit de38371069
4 changed files with 77 additions and 32 deletions
@@ -1,5 +1,5 @@
import { MouseEventType } from '@nitrots/nitro-renderer'; import { MouseEventType } from '@nitrots/nitro-renderer';
import { CSSProperties, FC, MouseEvent, useMemo, useState } from 'react'; import { FC, MouseEvent, useMemo, useState } from 'react';
import { FaHeart } from 'react-icons/fa'; import { FaHeart } from 'react-icons/fa';
import { CatalogType, GetConfigurationValue, IPurchasableOffer, Offer, ProductTypeEnum } from '../../../../../api'; import { CatalogType, GetConfigurationValue, IPurchasableOffer, Offer, ProductTypeEnum } from '../../../../../api';
import { LayoutAvatarImageView, LayoutGridItem, LayoutGridItemProps } from '../../../../../common'; import { LayoutAvatarImageView, LayoutGridItem, LayoutGridItemProps } from '../../../../../common';
@@ -114,10 +114,9 @@ export const CatalogGridOfferView: FC<CatalogGridOfferViewProps> = props =>
return ( return (
<LayoutGridItem <LayoutGridItem
className={ `group/tile relative ${ itemActive ? 'is-active' : '' } ${ tintColor ? 'has-guild-tint' : '' }` } className={ `group/tile relative ${ itemActive ? 'is-active' : '' }` }
gap={ 1 } gap={ 1 }
itemActive={ itemActive } itemActive={ itemActive }
style={ tintColor ? ({ '--guild-tint': tintColor } as CSSProperties) : undefined }
itemCount={ ((offer.pricingModel === Offer.PRICING_MODEL_MULTI) ? product.productCount : 1) } itemCount={ ((offer.pricingModel === Offer.PRICING_MODEL_MULTI) ? product.productCount : 1) }
itemUniqueNumber={ product.uniqueLimitedItemSeriesSize } itemUniqueNumber={ product.uniqueLimitedItemSeriesSize }
itemUniqueSoldout={ (product.uniqueLimitedItemSeriesSize && !product.uniqueLimitedItemsLeft) } itemUniqueSoldout={ (product.uniqueLimitedItemSeriesSize && !product.uniqueLimitedItemsLeft) }
@@ -132,6 +131,7 @@ export const CatalogGridOfferView: FC<CatalogGridOfferViewProps> = props =>
className="nitro-catalog-classic-grid-offer-icon" className="nitro-catalog-classic-grid-offer-icon"
src={ iconUrl } src={ iconUrl }
draggable={ false } draggable={ false }
style={ tintColor ? { filter: 'url(#guild-furni-recolor)', transform: 'translateZ(0)' } : undefined }
onError={ event => onError={ event =>
{ {
const fallbackIconUrl = product.getIconUrl(offer); const fallbackIconUrl = product.getIconUrl(offer);
@@ -1,10 +1,11 @@
import { StringDataType } from '@nitrots/nitro-renderer'; import { StringDataType } from '@nitrots/nitro-renderer';
import { FC, useMemo } from 'react'; import { FC, useEffect, useMemo, useState } from 'react';
import { FaExchangeAlt, FaSyncAlt } from 'react-icons/fa'; import { FaExchangeAlt, FaSyncAlt } from 'react-icons/fa';
import { Column } from '../../../../../common'; import { Column } from '../../../../../common';
import { useCatalogData, useCatalogUiState, useUserGroups } from '../../../../../hooks'; import { useCatalogData, useCatalogUiState, useUserGroups } from '../../../../../hooks';
import { CatalogFirstProductSelectorWidgetView } from '../widgets/CatalogFirstProductSelectorWidgetView'; import { CatalogFirstProductSelectorWidgetView } from '../widgets/CatalogFirstProductSelectorWidgetView';
import { CatalogGuildBadgeWidgetView } from '../widgets/CatalogGuildBadgeWidgetView'; import { CatalogGuildBadgeWidgetView } from '../widgets/CatalogGuildBadgeWidgetView';
import { CatalogGuildFurniRecolorFilter } from '../widgets/CatalogGuildFurniRecolorFilter';
import { CatalogGuildSelectorWidgetView } from '../widgets/CatalogGuildSelectorWidgetView'; import { CatalogGuildSelectorWidgetView } from '../widgets/CatalogGuildSelectorWidgetView';
import { CatalogItemGridWidgetView } from '../widgets/CatalogItemGridWidgetView'; import { CatalogItemGridWidgetView } from '../widgets/CatalogItemGridWidgetView';
import { CatalogPurchaseWidgetView } from '../widgets/CatalogPurchaseWidgetView'; import { CatalogPurchaseWidgetView } from '../widgets/CatalogPurchaseWidgetView';
@@ -18,28 +19,40 @@ export const CatalogLayouGuildCustomFurniView: FC<CatalogLayoutProps> = () =>
const { purchaseOptions = null } = useCatalogUiState(); const { purchaseOptions = null } = useCatalogUiState();
const { data: groups = null } = useUserGroups(); const { data: groups = null } = useUserGroups();
const hasGroups = !!(groups && groups.length); const hasGroups = !!(groups && groups.length);
const [ groupColors, setGroupColors ] = useState<{ colorA: string; colorB: string } | null>(null);
const tintColor = useMemo(() => useEffect(() =>
{ {
const previewStuffData = purchaseOptions?.previewStuffData ?? null; const previewStuffData = purchaseOptions?.previewStuffData ?? null;
if(!previewStuffData) return null; if(!previewStuffData) return;
const colorA = (previewStuffData as StringDataType).getValue(3); const colorA = (previewStuffData as StringDataType).getValue(3);
const colorB = (previewStuffData as StringDataType).getValue(4); const colorB = (previewStuffData as StringDataType).getValue(4);
if(!colorA || !colorA.length) return null; if(!colorA || !colorA.length) return;
if(colorB && colorB.length && (colorB !== colorA)) const next = { colorA, colorB: (colorB && colorB.length) ? colorB : colorA };
{
return `linear-gradient(90deg, #${ colorA } 0 50%, #${ colorB } 50% 100%)`; setGroupColors(prev => (prev && (prev.colorA === next.colorA) && (prev.colorB === next.colorB)) ? prev : next);
} }, [ purchaseOptions ]);
const tintColor = useMemo(() =>
{
if(!groupColors) return null;
const { colorA, colorB } = groupColors;
if(colorB && (colorB !== colorA)) return `linear-gradient(90deg, #${ colorA } 0 50%, #${ colorB } 50% 100%)`;
return `#${ colorA }`; return `#${ colorA }`;
}, [ purchaseOptions ]); }, [ groupColors ]);
return ( return (
<> <>
{ !!groupColors &&
<CatalogGuildFurniRecolorFilter colorA={ groupColors.colorA } colorB={ groupColors.colorB } /> }
<CatalogFirstProductSelectorWidgetView /> <CatalogFirstProductSelectorWidgetView />
<Column fullHeight gap={ 1 } overflow="hidden"> <Column fullHeight gap={ 1 } overflow="hidden">
{ !!currentOffer && { !!currentOffer &&
@@ -0,0 +1,52 @@
import { memo, useMemo } from 'react';
interface CatalogGuildFurniRecolorFilterProps
{
colorA?: string;
colorB?: string;
}
export const GUILD_FURNI_RECOLOR_FILTER_ID = 'guild-furni-recolor';
const OUTLINE_LEVEL = 0.08;
const toUnit = (hex: string, offset: number): number =>
{
const value = parseInt(hex.substr(offset, 2), 16);
return (isNaN(value) ? 0 : value) / 255;
};
export const CatalogGuildFurniRecolorFilter = memo((props: CatalogGuildFurniRecolorFilterProps) =>
{
const { colorA = null, colorB = null } = props;
const tables = useMemo(() =>
{
if(!colorA || (colorA.length < 6) || !colorB || (colorB.length < 6)) return null;
const aR = toUnit(colorA, 0), aG = toUnit(colorA, 2), aB = toUnit(colorA, 4);
const bR = toUnit(colorB, 0), bG = toUnit(colorB, 2), bB = toUnit(colorB, 4);
return {
r: `${ OUTLINE_LEVEL } ${ bR } ${ bR } ${ aR } ${ aR } ${ aR }`,
g: `${ OUTLINE_LEVEL } ${ bG } ${ bG } ${ aG } ${ aG } ${ aG }`,
b: `${ OUTLINE_LEVEL } ${ bB } ${ bB } ${ aB } ${ aB } ${ aB }`
};
}, [ colorA, colorB ]);
if(!tables) return null;
return (
<svg aria-hidden="true" focusable="false" width="0" height="0" style={ { position: 'absolute', width: 0, height: 0 } }>
<filter id={ GUILD_FURNI_RECOLOR_FILTER_ID } colorInterpolationFilters="sRGB">
<feComponentTransfer>
<feFuncR type="table" tableValues={ tables.r } />
<feFuncG type="table" tableValues={ tables.g } />
<feFuncB type="table" tableValues={ tables.b } />
</feComponentTransfer>
</filter>
</svg>
);
});
CatalogGuildFurniRecolorFilter.displayName = 'CatalogGuildFurniRecolorFilter';
-20
View File
@@ -866,10 +866,6 @@
border: 2px solid #62c4e8 !important; border: 2px solid #62c4e8 !important;
box-shadow: none !important; box-shadow: none !important;
} }
.nitro-catalog-classic-window .layout-grid-item.has-guild-tint:not(.has-highlight):not(.is-active) {
background: var(--guild-tint) !important;
}
} }
.nitro-catalog-classic-window .layout-grid-item.has-highlight { .nitro-catalog-classic-window .layout-grid-item.has-highlight {
@@ -1474,9 +1470,6 @@
} }
.nitro-catalog-classic-window *::-webkit-scrollbar-thumb { .nitro-catalog-classic-window *::-webkit-scrollbar-thumb {
/* Grip: a single 2px #a0a0a0 stripe in an 8px-wide centered band,
repeated every 5px (2px stripe + 3px body gap).
Outline: 1px black border, then a 2px white inset frame inside it. */
min-height: 24px !important; min-height: 24px !important;
background: background:
url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='8' height='5'><rect x='0' y='0' width='8' height='2' fill='%23a0a0a0'/></svg>") center top / 8px 5px repeat-y, url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='8' height='5'><rect x='0' y='0' width='8' height='2' fill='%23a0a0a0'/></svg>") center top / 8px 5px repeat-y,
@@ -1498,9 +1491,6 @@
#bcbcbc !important; #bcbcbc !important;
} }
/* Arrow buttons: light grey cap with rounded OUTER corners (up button
rounded at the top, down button rounded at the bottom), 1px black
border, dark chevron via inline SVG. */
.nitro-catalog-classic-window *::-webkit-scrollbar-button:single-button:vertical:decrement { .nitro-catalog-classic-window *::-webkit-scrollbar-button:single-button:vertical:decrement {
display: block !important; display: block !important;
width: 18px !important; width: 18px !important;
@@ -1617,13 +1607,6 @@
display: none !important; display: none !important;
} }
/* Stack the navigation above the furni/preview layout and let the
whole content area scroll. The previous grid used
`grid-template-rows: auto minmax(0, 1fr)`, but on iOS Safari the
flex height chain is indefinite, so the 1fr layout-shell row
collapsed to 0 and only the sidebar (category list) was visible.
A flex column sized to content + a scrollable content-shell is
device-robust. */
.nitro-catalog-classic-stage, .nitro-catalog-classic-stage,
.nitro-catalog-classic-stage.is-navigation-hidden { .nitro-catalog-classic-stage.is-navigation-hidden {
display: flex; display: flex;
@@ -1639,9 +1622,6 @@
max-height: 30vh; max-height: 30vh;
} }
/* The default layout's children (preview, grid, buy bar) are
absolutely positioned against a fixed ~460px box, so give the
shell a definite height and never clip it on mobile. */
.nitro-catalog-classic-layout-shell { .nitro-catalog-classic-layout-shell {
flex: 0 0 auto; flex: 0 0 auto;
width: 100%; width: 100%;