mirror of
https://github.com/duckietm/Nitro-V3.git
synced 2026-06-19 23:16:21 +00:00
🆙 Take #3 desktop view catalog is now 100%
This commit is contained in:
@@ -1,8 +1,8 @@
|
||||
import { ColorConverter } from '@nitrots/nitro-renderer';
|
||||
import { FC, useMemo, useState } from 'react';
|
||||
import { FaFillDrip } from 'react-icons/fa';
|
||||
import { IPurchasableOffer, SanitizeHtml } from '../../../../../api';
|
||||
import { AutoGrid, Button, Column, Grid, LayoutGridItem, Text } from '../../../../../common';
|
||||
import { FaExchangeAlt, FaFillDrip, FaSyncAlt } from 'react-icons/fa';
|
||||
import { IPurchasableOffer, ProductTypeEnum, SanitizeHtml } from '../../../../../api';
|
||||
import { AutoGrid, Button, Column, LayoutGridItem, Text } from '../../../../../common';
|
||||
import { useCatalogData, useCatalogUiState } from '../../../../../hooks';
|
||||
import { CatalogGridOfferView } from '../common/CatalogGridOfferView';
|
||||
import { CatalogAddOnBadgeWidgetView } from '../widgets/CatalogAddOnBadgeWidgetView';
|
||||
@@ -22,7 +22,7 @@ export const CatalogLayoutColorGroupingView: FC<CatalogLayoutColorGroupViewProps
|
||||
{
|
||||
const { page = null } = props;
|
||||
const [ colorableItems, setColorableItems ] = useState<Map<string, number[]>>(new Map<string, number[]>());
|
||||
const { currentOffer = null } = useCatalogData();
|
||||
const { currentOffer = null, roomPreviewer = null } = useCatalogData();
|
||||
const { setCurrentOffer = null } = useCatalogUiState();
|
||||
const [ colorsShowing, setColorsShowing ] = useState<boolean>(false);
|
||||
|
||||
@@ -132,46 +132,59 @@ export const CatalogLayoutColorGroupingView: FC<CatalogLayoutColorGroupViewProps
|
||||
}, [ page.offers ]);
|
||||
|
||||
return (
|
||||
<Grid>
|
||||
<Column overflow="hidden" size={ 7 }>
|
||||
<AutoGrid columnCount={ 5 }>
|
||||
{ (!colorsShowing || !currentOffer || !colorableItems.has(currentOffer.product.furnitureData.className)) &&
|
||||
offers.map((offer, index) => <CatalogGridOfferView key={ index } itemActive={ (currentOffer && (currentOffer.product.furnitureData.hasIndexedColor ? currentOffer.product.furnitureData.className === offer.product.furnitureData.className : currentOffer.offerId === offer.offerId)) } offer={ offer } selectOffer={ selectOffer } />)
|
||||
}
|
||||
{ (colorsShowing && currentOffer && colorableItems.has(currentOffer.product.furnitureData.className)) &&
|
||||
colorableItems.get(currentOffer.product.furnitureData.className).map((color, index) => <LayoutGridItem key={ index } itemHighlight className="clear-bg" itemActive={ (currentOffer.product.furnitureData.colorIndex === index) } itemColor={ ColorConverter.int2rgb(color) } onClick={ event => selectColor(index, currentOffer.product.furnitureData.className) } />)
|
||||
}
|
||||
</AutoGrid>
|
||||
</Column>
|
||||
<Column center={ !currentOffer } overflow="hidden" size={ 5 }>
|
||||
{ !currentOffer &&
|
||||
<>
|
||||
{ !!page.localization.getImage(1) && <img alt="" src={ page.localization.getImage(1) } /> }
|
||||
<Text center dangerouslySetInnerHTML={ { __html: SanitizeHtml(page.localization.getText(0)) } } />
|
||||
</> }
|
||||
{ currentOffer &&
|
||||
<>
|
||||
<div className="relative overflow-hidden">
|
||||
<CatalogViewProductWidgetView />
|
||||
<CatalogAddOnBadgeWidgetView className="bg-muted rounded bottom-1 inset-e-1" position="absolute" />
|
||||
{ currentOffer.product.furnitureData.hasIndexedColor &&
|
||||
<Button className="bottom-1 inset-s-1" position="absolute" onClick={ event => setColorsShowing(prev => !prev) }>
|
||||
<FaFillDrip className="fa-icon" />
|
||||
</Button> }
|
||||
<Column overflow="hidden">
|
||||
{ /* Top: two visible rows of furni tiles. Tile is 70px tall
|
||||
and the AutoGrid handles its own overflow if there are
|
||||
more than two rows worth of offers. */ }
|
||||
<div className="shrink-0" style={ { maxHeight: 154 } }>
|
||||
{ (!colorsShowing || !currentOffer || !colorableItems.has(currentOffer.product.furnitureData.className)) &&
|
||||
<AutoGrid columnCount={ 7 } columnMinHeight={ 70 } columnMinWidth={ 45 }>
|
||||
{ offers.map((offer, index) => <CatalogGridOfferView key={ index } itemActive={ (currentOffer && (currentOffer.product.furnitureData.hasIndexedColor ? currentOffer.product.furnitureData.className === offer.product.furnitureData.className : currentOffer.offerId === offer.offerId)) } offer={ offer } selectOffer={ selectOffer } />) }
|
||||
</AutoGrid> }
|
||||
{ (colorsShowing && currentOffer && colorableItems.has(currentOffer.product.furnitureData.className)) &&
|
||||
<div className="nitro-catalog-classic-color-swatches flex flex-wrap gap-1 p-2 overflow-auto">
|
||||
{ colorableItems.get(currentOffer.product.furnitureData.className).map((color, index) => <LayoutGridItem key={ index } itemHighlight className="clear-bg" itemActive={ (currentOffer.product.furnitureData.colorIndex === index) } itemColor={ ColorConverter.int2rgb(color) } onClick={ event => selectColor(index, currentOffer.product.furnitureData.className) } />) }
|
||||
</div> }
|
||||
</div>
|
||||
|
||||
{ /* Bottom: preview pane stacked under the grid. Mirrors the
|
||||
default-3x3 split (preview on the left, offer info on the
|
||||
right) so the rotate/state buttons and Buy/Gift actions
|
||||
sit where the user expects. */ }
|
||||
{ !currentOffer &&
|
||||
<Column center grow overflow="hidden">
|
||||
{ !!page.localization.getImage(1) && <img alt="" src={ page.localization.getImage(1) } /> }
|
||||
<Text center dangerouslySetInnerHTML={ { __html: SanitizeHtml(page.localization.getText(0)) } } />
|
||||
</Column> }
|
||||
{ currentOffer &&
|
||||
<div className="nitro-catalog-classic-offer-panel flex flex-col items-center grow overflow-hidden gap-2">
|
||||
<div className="nitro-catalog-classic-offer-preview relative flex items-center justify-center overflow-hidden">
|
||||
{ (currentOffer.product.productType !== ProductTypeEnum.BADGE) &&
|
||||
<>
|
||||
<button className="nitro-catalog-classic-preview-btn nitro-catalog-classic-preview-rotate" onClick={ () => roomPreviewer?.changeRoomObjectDirection() }>
|
||||
<FaSyncAlt />
|
||||
</button>
|
||||
<button className="nitro-catalog-classic-preview-btn nitro-catalog-classic-preview-state" onClick={ () => roomPreviewer?.changeRoomObjectState() }>
|
||||
<FaExchangeAlt />
|
||||
</button>
|
||||
</> }
|
||||
<CatalogViewProductWidgetView />
|
||||
<CatalogAddOnBadgeWidgetView className="bg-muted rounded bottom-1 inset-e-1" position="absolute" />
|
||||
{ currentOffer.product.furnitureData.hasIndexedColor &&
|
||||
<Button className="bottom-1 inset-s-1" position="absolute" onClick={ event => setColorsShowing(prev => !prev) }>
|
||||
<FaFillDrip className="fa-icon" />
|
||||
</Button> }
|
||||
</div>
|
||||
<div className="w-full max-w-[360px] flex flex-col gap-2 px-1">
|
||||
<CatalogLimitedItemWidgetView />
|
||||
<Text truncate>{ currentOffer.localizationName }</Text>
|
||||
<div className="flex justify-between items-center">
|
||||
<CatalogSpinnerWidgetView />
|
||||
<CatalogTotalPriceWidget alignItems="end" justifyContent="end" />
|
||||
</div>
|
||||
<Column className="grow!" gap={ 1 }>
|
||||
<CatalogLimitedItemWidgetView />
|
||||
<Text truncate className="grow!">{ currentOffer.localizationName }</Text>
|
||||
<div className="flex justify-between">
|
||||
<div className="flex flex-col gap-1">
|
||||
<CatalogSpinnerWidgetView />
|
||||
</div>
|
||||
<CatalogTotalPriceWidget alignItems="end" justifyContent="end" />
|
||||
</div>
|
||||
<CatalogPurchaseWidgetView />
|
||||
</Column>
|
||||
</> }
|
||||
</Column>
|
||||
</Grid>
|
||||
<CatalogPurchaseWidgetView />
|
||||
</div>
|
||||
</div> }
|
||||
</Column>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -291,7 +291,7 @@ export const CatalogLayoutPetView: FC<CatalogLayoutProps> = props =>
|
||||
{ LocalizeText('catalog.pets.back.breeds') }
|
||||
</button> }
|
||||
</div>
|
||||
<div className="grid grid-cols-6 gap-1">
|
||||
<div className={ colorsShowing ? 'nitro-catalog-classic-color-swatches flex flex-wrap gap-1 p-2 overflow-auto' : 'grid grid-cols-6 gap-1' }>
|
||||
{ !colorsShowing && (sellablePalettes.length > 0) && sellablePalettes.map((palette, index) => (
|
||||
<LayoutGridItem
|
||||
key={ index }
|
||||
@@ -303,10 +303,12 @@ export const CatalogLayoutPetView: FC<CatalogLayoutProps> = props =>
|
||||
</LayoutGridItem>
|
||||
)) }
|
||||
{ colorsShowing && (sellableColors.length > 0) && sellableColors.map((colorSet, index) => (
|
||||
<div
|
||||
<LayoutGridItem
|
||||
key={ index }
|
||||
className={ `w-full aspect-square rounded border-2 cursor-pointer transition-all ${ selectedColorIndex === index ? 'border-primary scale-110 shadow-md' : 'border-card-grid-item-border hover:border-primary/50' }` }
|
||||
style={ { backgroundColor: `#${ ColorConverter.int2rgb(colorSet[0]) }` } }
|
||||
itemHighlight
|
||||
className="clear-bg"
|
||||
itemActive={ (selectedColorIndex === index) }
|
||||
itemColor={ ColorConverter.int2rgb(colorSet[0]) }
|
||||
onClick={ () => setSelectedColorIndex(index) }
|
||||
/>
|
||||
)) }
|
||||
|
||||
@@ -22,85 +22,98 @@ export const CatalogViewProductWidgetView: FC<{}> = props =>
|
||||
roomPreviewer.updateObjectRoom('default', 'default', 'default');
|
||||
roomPreviewer.updateRoomWallsAndFloorVisibility(true, true);
|
||||
|
||||
switch(product.productType)
|
||||
const populate = () =>
|
||||
{
|
||||
case ProductTypeEnum.FLOOR: {
|
||||
if(!product.furnitureData) return;
|
||||
switch(product.productType)
|
||||
{
|
||||
case ProductTypeEnum.FLOOR: {
|
||||
if(!product.furnitureData) return;
|
||||
|
||||
const furniData = GetSessionDataManager().getFloorItemData(product.furnitureData.id);
|
||||
const isPurchasableClothing = (product.furnitureData.specialType === FurniCategory.FIGURE_PURCHASABLE_SET);
|
||||
const hasResolvableFigureSets = (() =>
|
||||
{
|
||||
if(!furniData || !furniData.customParams || !furniData.customParams.length) return false;
|
||||
|
||||
const parts = furniData.customParams.split(',').map(value => parseInt(value));
|
||||
|
||||
for(const part of parts)
|
||||
const furniData = GetSessionDataManager().getFloorItemData(product.furnitureData.id);
|
||||
const isPurchasableClothing = (product.furnitureData.specialType === FurniCategory.FIGURE_PURCHASABLE_SET);
|
||||
const hasResolvableFigureSets = (() =>
|
||||
{
|
||||
if(isNaN(part)) continue;
|
||||
if(!furniData || !furniData.customParams || !furniData.customParams.length) return false;
|
||||
|
||||
if(GetAvatarRenderManager().structureData?.getFigurePartSet(part)) return true;
|
||||
}
|
||||
const parts = furniData.customParams.split(',').map(value => parseInt(value));
|
||||
|
||||
return false;
|
||||
})();
|
||||
for(const part of parts)
|
||||
{
|
||||
if(isNaN(part)) continue;
|
||||
|
||||
if(isPurchasableClothing || hasResolvableFigureSets)
|
||||
{
|
||||
const customParts = furniData.customParams.split(',').map(value => parseInt(value));
|
||||
const figureSets: number[] = [];
|
||||
if(GetAvatarRenderManager().structureData?.getFigurePartSet(part)) return true;
|
||||
}
|
||||
|
||||
for(const part of customParts)
|
||||
return false;
|
||||
})();
|
||||
|
||||
if(isPurchasableClothing || hasResolvableFigureSets)
|
||||
{
|
||||
if(isNaN(part)) continue;
|
||||
const customParts = furniData.customParams.split(',').map(value => parseInt(value));
|
||||
const figureSets: number[] = [];
|
||||
|
||||
if(GetAvatarRenderManager().isValidFigureSetForGender(part, GetSessionDataManager().gender)) figureSets.push(part);
|
||||
for(const part of customParts)
|
||||
{
|
||||
if(isNaN(part)) continue;
|
||||
|
||||
if(GetAvatarRenderManager().isValidFigureSetForGender(part, GetSessionDataManager().gender)) figureSets.push(part);
|
||||
}
|
||||
|
||||
const figureString = BuildPurchasableClothingFigure(GetSessionDataManager().figure, figureSets);
|
||||
|
||||
roomPreviewer.addAvatarIntoRoom(figureString, product.productClassId);
|
||||
}
|
||||
|
||||
const figureString = BuildPurchasableClothingFigure(GetSessionDataManager().figure, figureSets);
|
||||
|
||||
roomPreviewer.addAvatarIntoRoom(figureString, product.productClassId);
|
||||
}
|
||||
else
|
||||
{
|
||||
roomPreviewer.addFurnitureIntoRoom(product.productClassId, new Vector3d(90), previewStuffData, product.extraParam);
|
||||
}
|
||||
return;
|
||||
}
|
||||
case ProductTypeEnum.WALL: {
|
||||
if(!product.furnitureData) return;
|
||||
|
||||
roomPreviewer.updateRoomWallsAndFloorVisibility(true, true);
|
||||
|
||||
switch(product.furnitureData.specialType)
|
||||
{
|
||||
case FurniCategory.FLOOR:
|
||||
roomPreviewer.updateObjectRoom(product.extraParam);
|
||||
return;
|
||||
case FurniCategory.WALL_PAPER:
|
||||
roomPreviewer.updateObjectRoom(null, product.extraParam);
|
||||
return;
|
||||
case FurniCategory.LANDSCAPE: {
|
||||
roomPreviewer.updateObjectRoom(null, null, product.extraParam);
|
||||
|
||||
const furniData = GetSessionDataManager().getWallItemDataByName('window_double_default');
|
||||
|
||||
if(furniData) roomPreviewer.addWallItemIntoRoom(furniData.id, new Vector3d(90), furniData.customParams);
|
||||
return;
|
||||
else
|
||||
{
|
||||
roomPreviewer.addFurnitureIntoRoom(product.productClassId, new Vector3d(90), previewStuffData, product.extraParam);
|
||||
}
|
||||
default:
|
||||
roomPreviewer.updateObjectRoom('default', 'default', 'default');
|
||||
roomPreviewer.addWallItemIntoRoom(product.productClassId, new Vector3d(90), product.extraParam);
|
||||
return;
|
||||
return;
|
||||
}
|
||||
case ProductTypeEnum.WALL: {
|
||||
if(!product.furnitureData) return;
|
||||
|
||||
roomPreviewer.updateRoomWallsAndFloorVisibility(true, true);
|
||||
|
||||
switch(product.furnitureData.specialType)
|
||||
{
|
||||
case FurniCategory.FLOOR:
|
||||
roomPreviewer.updateObjectRoom(product.extraParam);
|
||||
return;
|
||||
case FurniCategory.WALL_PAPER:
|
||||
roomPreviewer.updateObjectRoom(null, product.extraParam);
|
||||
return;
|
||||
case FurniCategory.LANDSCAPE: {
|
||||
roomPreviewer.updateObjectRoom(null, null, product.extraParam);
|
||||
|
||||
const furniData = GetSessionDataManager().getWallItemDataByName('window_double_default');
|
||||
|
||||
if(furniData) roomPreviewer.addWallItemIntoRoom(furniData.id, new Vector3d(90), furniData.customParams);
|
||||
return;
|
||||
}
|
||||
default:
|
||||
roomPreviewer.updateObjectRoom('default', 'default', 'default');
|
||||
roomPreviewer.addWallItemIntoRoom(product.productClassId, new Vector3d(90), product.extraParam);
|
||||
return;
|
||||
}
|
||||
}
|
||||
case ProductTypeEnum.ROBOT:
|
||||
roomPreviewer.addAvatarIntoRoom(product.extraParam, 0);
|
||||
return;
|
||||
case ProductTypeEnum.EFFECT:
|
||||
roomPreviewer.addAvatarIntoRoom(GetSessionDataManager().figure, product.productClassId);
|
||||
return;
|
||||
}
|
||||
case ProductTypeEnum.ROBOT:
|
||||
roomPreviewer.addAvatarIntoRoom(product.extraParam, 0);
|
||||
return;
|
||||
case ProductTypeEnum.EFFECT:
|
||||
roomPreviewer.addAvatarIntoRoom(GetSessionDataManager().figure, product.productClassId);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
populate();
|
||||
|
||||
// RoomPreviewer.addFurnitureIntoRoom / addAvatarIntoRoom flip
|
||||
// _automaticStateChange to true, which makes the ticker advance
|
||||
// the room object's state every AUTOMATIC_STATE_CHANGE_INTERVAL.
|
||||
// In the catalog we want the preview to sit still until the
|
||||
// user clicks the state button explicitly - turn it back off
|
||||
// after populate() runs.
|
||||
roomPreviewer.setAutomaticStateChange(false);
|
||||
}, [ currentOffer, previewStuffData, roomPreviewer ]);
|
||||
|
||||
if(!currentOffer) return null;
|
||||
@@ -119,5 +132,11 @@ export const CatalogViewProductWidgetView: FC<{}> = props =>
|
||||
);
|
||||
}
|
||||
|
||||
return <LayoutRoomPreviewerView height={ 240 } roomPreviewer={ roomPreviewer } />;
|
||||
// Re-mount the previewer whenever the offer changes so the render
|
||||
// latch / texture handle in LayoutRoomPreviewerView resets cleanly.
|
||||
// Without this a single broken offer (e.g. blackhole's Pixi filter
|
||||
// crash) latches the previewer permanently and every following
|
||||
// offer paints nothing - the singleton roomPreviewer + 240px height
|
||||
// keep the same component mounted otherwise.
|
||||
return <LayoutRoomPreviewerView key={ currentOffer?.offerId } height={ 240 } roomPreviewer={ roomPreviewer } />;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user