Add catalog bundles

This commit is contained in:
Bill 2021-05-12 05:06:57 -04:00
parent d783b9b2d1
commit 5c73d8d459
14 changed files with 167 additions and 84 deletions

View File

@ -65,7 +65,12 @@ export const CatalogReducer: Reducer<ICatalogState, ICatalogAction> = (state, ac
} }
case CatalogActions.SET_CATALOG_PAGE_PARSER: { case CatalogActions.SET_CATALOG_PAGE_PARSER: {
const pageParser = action.payload.pageParser; const pageParser = action.payload.pageParser;
const activeOffer = null; let activeOffer = null;
if(pageParser.layoutCode === 'single_bundle')
{
activeOffer = ((pageParser.offers && pageParser.offers[0]) || null);
}
const searchResult = state.searchResult; const searchResult = state.searchResult;

View File

@ -1,4 +1,4 @@
import { FC, useEffect } from 'react'; import { FC, useEffect, useState } from 'react';
import { GetCatalogPageComposer } from '../../../../../api/catalog/GetCatalogPageComposer'; import { GetCatalogPageComposer } from '../../../../../api/catalog/GetCatalogPageComposer';
import { SendMessageHook } from '../../../../../hooks/messages/message-event'; import { SendMessageHook } from '../../../../../hooks/messages/message-event';
import { CatalogIconView } from '../../../../catalog-icon/CatalogIconView'; import { CatalogIconView } from '../../../../catalog-icon/CatalogIconView';
@ -9,11 +9,14 @@ import { CatalogNavigationItemViewProps } from './CatalogNavigationItemView.type
export const CatalogNavigationItemView: FC<CatalogNavigationItemViewProps> = props => export const CatalogNavigationItemView: FC<CatalogNavigationItemViewProps> = props =>
{ {
const { page = null, isActive = false, setActiveChild = null } = props; const { page = null, isActive = false, setActiveChild = null } = props;
const [ isExpanded, setIsExpanded ] = useState(false);
useEffect(() => useEffect(() =>
{ {
if(!isActive || !page) return; if(!isActive || !page) return;
setIsExpanded(true);
SendMessageHook(GetCatalogPageComposer(page.pageId, -1, CatalogMode.MODE_NORMAL)); SendMessageHook(GetCatalogPageComposer(page.pageId, -1, CatalogMode.MODE_NORMAL));
}, [ isActive, page ]); }, [ isActive, page ]);
@ -21,12 +24,15 @@ export const CatalogNavigationItemView: FC<CatalogNavigationItemViewProps> = pro
{ {
if(!page) return; if(!page) return;
setActiveChild(prevValue => setActiveChild(page);
{
if(prevValue === page) return null;
return page; if(page.children && page.children.length)
}); {
setIsExpanded(prevValue =>
{
return !prevValue;
});
}
} }
return ( return (
@ -34,9 +40,9 @@ export const CatalogNavigationItemView: FC<CatalogNavigationItemViewProps> = pro
<div className={ 'd-flex align-items-center cursor-pointer catalog-navigation-item ' + (isActive ? 'active ': '') } onClick={ select }> <div className={ 'd-flex align-items-center cursor-pointer catalog-navigation-item ' + (isActive ? 'active ': '') } onClick={ select }>
<CatalogIconView icon={ page.icon } /> <CatalogIconView icon={ page.icon } />
<div className="flex-grow-1 text-black text-truncate px-1">{ page.localization }</div> <div className="flex-grow-1 text-black text-truncate px-1">{ page.localization }</div>
{ (page.children.length > 0) && <i className={ 'fas fa-caret-' + (isActive ? 'up' : 'down') } /> } { (page.children.length > 0) && <i className={ 'fas fa-caret-' + (isExpanded ? 'up' : 'down') } /> }
</div> </div>
{ isActive && page.children && (page.children.length > 0) && { isActive && isExpanded && page.children && (page.children.length > 0) &&
<div className="d-flex flex-column mt-1"> <div className="d-flex flex-column mt-1">
<CatalogNavigationSetView page={ page } /> <CatalogNavigationSetView page={ page } />
</div> } </div> }

View File

@ -1,5 +1,6 @@
@import './layout/CatalogLayout'; @import './layout/CatalogLayout';
@import './offer/CatalogPageOfferView'; @import './offer/CatalogPageOfferView';
@import './offers/CatalogPageOffersView'; @import './offers/CatalogPageOffersView';
@import './product/CatalogProductView';
@import './purchase/CatalogPurchaseView'; @import './purchase/CatalogPurchaseView';
@import './search-result/CatalogLayoutSearchResultView'; @import './search-result/CatalogLayoutSearchResultView';

View File

@ -1,3 +1,4 @@
@import './default/CatalogLayoutDefaultView'; @import './default/CatalogLayoutDefaultView';
@import './single-bundle/CatalogLayoutSingleBundleView';
@import './spaces-new/CatalogLayoutSpacesView'; @import './spaces-new/CatalogLayoutSpacesView';
@import './trophies/CatalogLayoutTrophiesView'; @import './trophies/CatalogLayoutTrophiesView';

View File

@ -1,5 +1,6 @@
import { ICatalogPageParser, RoomPreviewer } from 'nitro-renderer'; import { ICatalogPageParser, RoomPreviewer } from 'nitro-renderer';
import { CatalogLayoutDefaultView } from './default/CatalogLayoutDefaultView'; import { CatalogLayoutDefaultView } from './default/CatalogLayoutDefaultView';
import { CatalogLayoutSingleBundleView } from './single-bundle/CatalogLayoutSingleBundleView';
import { CatalogLayoutSpacesView } from './spaces-new/CatalogLayoutSpacesView'; import { CatalogLayoutSpacesView } from './spaces-new/CatalogLayoutSpacesView';
import { CatalogLayoutTrophiesView } from './trophies/CatalogLayoutTrophiesView'; import { CatalogLayoutTrophiesView } from './trophies/CatalogLayoutTrophiesView';
@ -31,6 +32,8 @@ export function GetCatalogLayout(pageParser: ICatalogPageParser, roomPreviewer:
return null; return null;
case 'marketplace': case 'marketplace':
return null; return null;
case 'single_bundle':
return <CatalogLayoutSingleBundleView roomPreviewer={ roomPreviewer } pageParser={ pageParser } />;
case 'spaces_new': case 'spaces_new':
return <CatalogLayoutSpacesView roomPreviewer={ roomPreviewer } pageParser={ pageParser } />; return <CatalogLayoutSpacesView roomPreviewer={ roomPreviewer } pageParser={ pageParser } />;
case 'trophies': case 'trophies':

View File

@ -10,7 +10,7 @@ import { CatalogLayoutDefaultViewProps } from './CatalogLayoutDefaultView.types'
export const CatalogLayoutDefaultView: FC<CatalogLayoutDefaultViewProps> = props => export const CatalogLayoutDefaultView: FC<CatalogLayoutDefaultViewProps> = props =>
{ {
const { roomPreviewer = null, pageParser = null } = props; const { roomPreviewer = null, pageParser = null } = props;
const { catalogState } = useCatalogContext(); const { catalogState = null } = useCatalogContext();
const { activeOffer = null } = catalogState; const { activeOffer = null } = catalogState;
const product = ((activeOffer && activeOffer.products[0]) || null); const product = ((activeOffer && activeOffer.products[0]) || null);
@ -25,7 +25,7 @@ export const CatalogLayoutDefaultView: FC<CatalogLayoutDefaultViewProps> = props
<div className="d-block mb-2"> <div className="d-block mb-2">
<img alt="" src={ GetCatalogPageImage(pageParser, 1) } /> <img alt="" src={ GetCatalogPageImage(pageParser, 1) } />
</div> </div>
<span className="text-center text-black lh-sm">{ GetCatalogPageText(pageParser, 0) }</span> <div className="fs-6 text-center text-black lh-sm overflow-hidden">{ GetCatalogPageText(pageParser, 0) }</div>
</div> } </div> }
{ product && { product &&
<div className="position-relative d-flex flex-column col"> <div className="position-relative d-flex flex-column col">

View File

@ -0,0 +1,6 @@
.nitro-catalog-layout-single-bundle {
.single-bundle-items-container {
}
}

View File

@ -0,0 +1,33 @@
import { FC } from 'react';
import { useCatalogContext } from '../../../../context/CatalogContext';
import { GetCatalogPageImage, GetCatalogPageText } from '../../../../utils/CatalogUtilities';
import { CatalogProductView } from '../../product/CatalogProductView';
import { CatalogPurchaseView } from '../../purchase/CatalogPurchaseView';
import { CatalogLayoutSingleBundleViewProps } from './CatalogLayoutSingleBundleView.types';
export const CatalogLayoutSingleBundleView: FC<CatalogLayoutSingleBundleViewProps> = props =>
{
const { roomPreviewer = null, pageParser = null } = props;
const { catalogState = null, dispatchCatalogState = null } = useCatalogContext();
const { activeOffer = null } = catalogState;
return (
<div className="row h-100 nitro-catalog-layout-single-bundle">
<div className="col-7">
<div className="row row-cols-5 align-content-start g-0 mb-n1 w-100 catalog-offers-container single-bundle-items-container">
{ activeOffer && activeOffer.products && (activeOffer.products.length > 0) && activeOffer.products.map((product, index) =>
{
return <CatalogProductView key={ index } isActive={ false } product={ product } />
}) }
</div>
</div>
<div className="position-relative d-flex flex-column col justify-content-center align-items-center">
<div className="d-block mb-2">
<img alt="" src={ GetCatalogPageImage(pageParser, 1) } />
</div>
<div className="fs-6 text-center text-black lh-sm overflow-hidden">{ GetCatalogPageText(pageParser, 0) }</div>
{ activeOffer && <CatalogPurchaseView offer={ activeOffer } pageId={ pageParser.pageId } /> }
</div>
</div>
);
}

View File

@ -0,0 +1,6 @@
import { CatalogLayoutProps } from '../CatalogLayout.types';
export interface CatalogLayoutSingleBundleViewProps extends CatalogLayoutProps
{
}

View File

@ -1,18 +1,13 @@
import { FurnitureType, MouseEventType } from 'nitro-renderer'; import { MouseEventType } from 'nitro-renderer';
import { FC, MouseEvent, useCallback, useState } from 'react'; import { FC, MouseEvent, useCallback } from 'react';
import { GetRoomEngine, GetSessionDataManager } from '../../../../../api';
import { GetConfiguration } from '../../../../../utils/GetConfiguration';
import { AvatarImageView } from '../../../../avatar-image/AvatarImageView';
import { LimitedEditionStyledNumberView } from '../../../../limited-edition/styled-number/LimitedEditionStyledNumberView';
import { useCatalogContext } from '../../../context/CatalogContext'; import { useCatalogContext } from '../../../context/CatalogContext';
import { ProductTypeEnum } from '../../../enums/ProductTypeEnum';
import { CatalogActions } from '../../../reducers/CatalogReducer'; import { CatalogActions } from '../../../reducers/CatalogReducer';
import { CatalogProductView } from '../product/CatalogProductView';
import { CatalogPageOfferViewProps } from './CatalogPageOfferView.types'; import { CatalogPageOfferViewProps } from './CatalogPageOfferView.types';
export const CatalogPageOfferView: FC<CatalogPageOfferViewProps> = props => export const CatalogPageOfferView: FC<CatalogPageOfferViewProps> = props =>
{ {
const { isActive = false, offer = null } = props; const { isActive = false, offer = null } = props;
const [ isMouseDown, setMouseDown ] = useState(false);
const { dispatchCatalogState = null } = useCatalogContext(); const { dispatchCatalogState = null } = useCatalogContext();
const onMouseEvent = useCallback((event: MouseEvent) => const onMouseEvent = useCallback((event: MouseEvent) =>
@ -36,67 +31,5 @@ export const CatalogPageOfferView: FC<CatalogPageOfferViewProps> = props =>
if(!product) return null; if(!product) return null;
function getIconUrl(): string return <CatalogProductView isActive={ isActive } product={ product } onMouseEvent={ onMouseEvent } />
{
const productType = product.productType.toUpperCase();
switch(productType)
{
case FurnitureType.BADGE:
return GetSessionDataManager().getBadgeUrl(product.extraParam);
case FurnitureType.ROBOT:
return null;
case FurnitureType.FLOOR:
return GetRoomEngine().getFurnitureFloorIconUrl(product.furniClassId);
case FurnitureType.WALL: {
const furniData = GetSessionDataManager().getWallItemData(product.furniClassId);
let iconName = '';
if(furniData)
{
switch(furniData.className)
{
case 'floor':
iconName = ['th', furniData.className, product.extraParam].join('_');
break;
case 'wallpaper':
iconName = ['th', 'wall', product.extraParam].join('_');
break;
case 'landscape':
iconName = ['th', furniData.className, product.extraParam.replace('.', '_'), '001'].join('_');
break;
}
if(iconName !== '')
{
const assetUrl = GetConfiguration<string>('catalog.asset.url');
return `${ assetUrl }/${ iconName }.png`;
}
}
return GetRoomEngine().getFurnitureWallIconUrl(product.furniClassId, product.extraParam);
}
}
return '';
}
const iconUrl = getIconUrl();
const imageUrl = (iconUrl && iconUrl.length) ? `url(${ iconUrl })` : null;
return (
<div className="col pe-1 pb-1 catalog-offer-item-container">
<div className={ 'position-relative border border-2 rounded catalog-offer-item cursor-pointer ' + (isActive ? 'active ' : '') + (product.uniqueLimitedItem ? 'unique-item ' : '') + ((product.uniqueLimitedItem && !product.uniqueLimitedItemsLeft) ? 'sold-out ' : '') } style={ { backgroundImage: imageUrl }} onClick={ onMouseEvent }>
{ !imageUrl && (product.productType === ProductTypeEnum.ROBOT) &&
<AvatarImageView figure={ product.extraParam } direction={ 3 } headOnly={ true } /> }
{ (product.productCount > 1) && <span className="position-absolute badge border bg-danger px-1 rounded-circle">{ product.productCount }</span> }
{ product.uniqueLimitedItem &&
<div className="position-absolute unique-item-counter">
<LimitedEditionStyledNumberView value={ product.uniqueLimitedSeriesSize } />
</div> }
</div>
</div>
);
} }

View File

@ -0,0 +1,80 @@
import { FurnitureType } from 'nitro-renderer';
import { FC, useMemo } from 'react';
import { GetRoomEngine, GetSessionDataManager } from '../../../../../api';
import { GetConfiguration } from '../../../../../utils/GetConfiguration';
import { AvatarImageView } from '../../../../avatar-image/AvatarImageView';
import { LimitedEditionStyledNumberView } from '../../../../limited-edition/styled-number/LimitedEditionStyledNumberView';
import { ProductTypeEnum } from '../../../enums/ProductTypeEnum';
import { CatalogProductViewProps } from './CatalogProductView.types';
export const CatalogProductView: FC<CatalogProductViewProps> = props =>
{
const { isActive = false, product = null, onMouseEvent = null } = props;
const iconUrl = useMemo(() =>
{
if(!product) return null;
const productType = product.productType.toUpperCase();
switch(productType)
{
case FurnitureType.BADGE:
return GetSessionDataManager().getBadgeUrl(product.extraParam);
case FurnitureType.ROBOT:
return null;
case FurnitureType.FLOOR:
return GetRoomEngine().getFurnitureFloorIconUrl(product.furniClassId);
case FurnitureType.WALL: {
const furniData = GetSessionDataManager().getWallItemData(product.furniClassId);
let iconName = '';
if(furniData)
{
switch(furniData.className)
{
case 'floor':
iconName = ['th', furniData.className, product.extraParam].join('_');
break;
case 'wallpaper':
iconName = ['th', 'wall', product.extraParam].join('_');
break;
case 'landscape':
iconName = ['th', furniData.className, product.extraParam.replace('.', '_'), '001'].join('_');
break;
}
if(iconName !== '')
{
const assetUrl = GetConfiguration<string>('catalog.asset.url');
return `${ assetUrl }/${ iconName }.png`;
}
}
return GetRoomEngine().getFurnitureWallIconUrl(product.furniClassId, product.extraParam);
}
}
return null;
}, [ product ]);
if(!product) return null;
const imageUrl = (iconUrl && iconUrl.length) ? `url(${ iconUrl })` : null;
return (
<div className="col pe-1 pb-1 catalog-offer-item-container">
<div className={ 'position-relative border border-2 rounded catalog-offer-item cursor-pointer ' + (isActive ? 'active ' : '') + (product.uniqueLimitedItem ? 'unique-item ' : '') + ((product.uniqueLimitedItem && !product.uniqueLimitedItemsLeft) ? 'sold-out ' : '') } style={ { backgroundImage: imageUrl }} onClick={ onMouseEvent }>
{ !imageUrl && (product.productType === ProductTypeEnum.ROBOT) &&
<AvatarImageView figure={ product.extraParam } direction={ 3 } headOnly={ true } /> }
{ (product.productCount > 1) && <span className="position-absolute badge border bg-danger px-1 rounded-circle">{ product.productCount }</span> }
{ product.uniqueLimitedItem &&
<div className="position-absolute unique-item-counter">
<LimitedEditionStyledNumberView value={ product.uniqueLimitedSeriesSize } />
</div> }
</div>
</div>
);
}

View File

@ -0,0 +1,9 @@
import { CatalogProductOfferData } from 'nitro-renderer';
import { MouseEventHandler } from 'react';
export interface CatalogProductViewProps
{
isActive: boolean;
product: CatalogProductOfferData;
onMouseEvent?: MouseEventHandler<Element>;
}

View File

@ -44,7 +44,7 @@ export const CatalogPurchaseView: FC<CatalogPurchaseViewProps> = props =>
const extraData = ((extra && extra.length) ? extra : (offer?.products[0]?.extraParam || null)); const extraData = ((extra && extra.length) ? extra : (offer?.products[0]?.extraParam || null));
return ( return (
<div className="d-flex flex-column flex-grow-1 justify-content-end"> <div className="d-flex flex-column flex-grow-1 justify-content-end w-100">
<div className="d-flex align-items-end"> <div className="d-flex align-items-end">
<div className="flex-grow-1 align-items-end"> <div className="flex-grow-1 align-items-end">
<span className="text-black">{ LocalizeText('catalog.bundlewidget.price') }</span> <span className="text-black">{ LocalizeText('catalog.bundlewidget.price') }</span>