Lots of catalog updates

This commit is contained in:
Bill 2021-05-10 13:11:16 -04:00
parent 4ef0a1d6d0
commit a1605537c8
92 changed files with 1614 additions and 78 deletions

View File

@ -0,0 +1,21 @@
import { CatalogProductOfferData, FurnitureType, IFurnitureData } from 'nitro-renderer';
import { GetSessionDataManager } from './GetSessionDataManager';
export function GetFurnitureDataForProductOffer(offer: CatalogProductOfferData): IFurnitureData
{
if(!offer) return null;
let furniData: IFurnitureData = null;
switch((offer.productType.toUpperCase()))
{
case FurnitureType.FLOOR:
furniData = GetSessionDataManager().getFloorItemData(offer.furniClassId);
break;
case FurnitureType.WALL:
furniData = GetSessionDataManager().getWallItemData(offer.furniClassId);
break;
}
return furniData;
}

View File

@ -0,0 +1,9 @@
import { IProductData } from 'nitro-renderer';
import { GetSessionDataManager } from './GetSessionDataManager';
export function GetProductDataForLocalization(localizationId: string): IProductData
{
if(!localizationId) return null;
return GetSessionDataManager().getProductData(localizationId);
}

View File

@ -1,4 +1,6 @@
export * from './GetFurnitureDataForProductOffer';
export * from './GetRoomSession';
export * from './GetRoomSessionManager';
export * from './GetSessionDataManager';
export * from './SendChatTypingMessage';
export * from './StartRoomSession';

View File

@ -80,6 +80,10 @@ $light: #DFDFDF !default;
$dark: $gray-900 !default;
// scss-docs-end theme-color-variables
.bg-primary-split {
background: repeating-linear-gradient(#1E7295, #1E7295 49.9%, #185D79 50.1%, #185D79 100%);
}
// scss-docs-start theme-colors-map
$theme-colors: (
"primary": $primary,
@ -364,7 +368,7 @@ $container-padding-x: $grid-gutter-width / 2 !default;
// Define common padding and border radius sizes and more.
// scss-docs-start border-variables
$border-width: 1.5px !default;
$border-width: 1px !default;
$border-widths: (
1: 1px,
2: 2px,

View File

@ -8,3 +8,19 @@
@import './grid';
@import './icons';
@import './utils';
.btn-sm {
min-height: 28px;
}
/* Chrome, Safari, Edge, Opera */
input::-webkit-outer-spin-button,
input::-webkit-inner-spin-button {
-webkit-appearance: none;
margin: 0;
}
/* Firefox */
input[type=number] {
-moz-appearance: textfield;
}

View File

@ -18,6 +18,10 @@
text-shadow: 0px 4px 4px rgba(0, 0, 0, 0.25);
}
.min-height-0 {
min-height: 0 !important;
}
ul {
&.columns-3 {

View File

@ -2,6 +2,10 @@ import { NitroEvent } from 'nitro-renderer';
export class CatalogEvent extends NitroEvent
{
public static SHOW_CATALOG: string = 'IE_SHOW_CATALOG';
public static TOGGLE_CATALOG: string = 'IE_TOGGLE_CATALOG';
public static SHOW_CATALOG: string = 'CE_SHOW_CATALOG';
public static HIDE_CATALOG: string = 'CE_HIDE_CATALOG';
public static TOGGLE_CATALOG: string = 'CE_TOGGLE_CATALOG';
public static PURCHASE_SUCCESS: string = 'CE_PURCHASE_SUCCESS';
public static PURCHASE_FAILED: string = 'CE_PURCHASE_FAILED';
public static SOLD_OUT: string = 'CE_SOLD_OUT';
}

View File

@ -0,0 +1,18 @@
import { CatalogEvent } from './CatalogEvent';
export class CatalogPurchaseFailureEvent extends CatalogEvent
{
private _code: number;
constructor(code: number)
{
super(CatalogEvent.PURCHASE_FAILED);
this._code = code;
}
public get code(): number
{
return this._code;
}
}

View File

@ -0,0 +1,9 @@
import { CatalogEvent } from './CatalogEvent';
export class CatalogPurchaseSoldOutEvent extends CatalogEvent
{
constructor()
{
super(CatalogEvent.SOLD_OUT);
}
}

View File

@ -0,0 +1,19 @@
import { CatalogPurchaseData } from 'nitro-renderer';
import { CatalogEvent } from './CatalogEvent';
export class CatalogPurchasedEvent extends CatalogEvent
{
private _purchase: CatalogPurchaseData;
constructor(purchase: CatalogPurchaseData)
{
super(CatalogEvent.PURCHASE_SUCCESS);
this._purchase = purchase;
}
public get purchase(): CatalogPurchaseData
{
return this._purchase;
}
}

View File

@ -1 +1,4 @@
export * from './CatalogEvent';
export * from './CatalogPurchasedEvent';
export * from './CatalogPurchaseFailureEvent';
export * from './CatalogPurchaseSoldOutEvent';

View File

@ -1,12 +1,14 @@
import { createRef, MouseEvent, useEffect } from 'react';
import { FC, MouseEvent, useEffect, useRef } from 'react';
import Draggable from 'react-draggable';
import { DraggableWindowProps } from './DraggableWindow.types';
const currentWindows: HTMLDivElement[] = [];
export function DraggableWindow(props: DraggableWindowProps): JSX.Element
export const DraggableWindow: FC<DraggableWindowProps> = props =>
{
const elementRef = createRef<HTMLDivElement>();
const { disableDrag = false } = props;
const elementRef = useRef<HTMLDivElement>();
function bringToTop(): void
{
@ -51,8 +53,8 @@ export function DraggableWindow(props: DraggableWindowProps): JSX.Element
bringToTop();
const left = ((document.body.clientWidth - element.clientWidth) / 2);
const top = ((document.body.clientHeight - element.clientHeight) / 2);
const left = ((element.parentElement.clientWidth - element.clientWidth) / 2);
const top = ((element.parentElement.clientHeight - element.clientHeight) / 2);
element.style.left = `${ left }px`;
element.style.top = `${ top }px`;
@ -65,11 +67,20 @@ export function DraggableWindow(props: DraggableWindowProps): JSX.Element
}
}, [ elementRef ]);
return (
<Draggable handle={ props.handle } { ...props.draggableOptions }>
function getWindowContent(): JSX.Element
{
return (
<div ref={ elementRef } className="position-absolute draggable-window" onMouseDownCapture={ onMouseDown }>
{ props.children }
</div>
);
}
if(disableDrag) return getWindowContent();
return (
<Draggable handle={ props.handle } { ...props.draggableOptions }>
{ getWindowContent() }
</Draggable>
);
}

View File

@ -5,5 +5,6 @@ export interface DraggableWindowProps
{
handle: string;
draggableOptions?: Partial<DraggableProps>;
disableDrag?: boolean;
children?: ReactNode;
}

View File

@ -35,4 +35,5 @@ $grid-active-border-color: $white;
@import './utils/Styles';
@import './App';
@import './hooks/Styles';
@import './layout/Layout';
@import './views/Styles';

View File

@ -0,0 +1,2 @@
@import './card/NitroCardView';
@import './loading-spinner/LoadingSpinnerView';

View File

@ -0,0 +1,8 @@
$nitro-card-header-height: 33px;
$nitro-card-tabs-height: 33px;
$nitro-card-top-height: $nitro-card-header-height + $nitro-card-tabs-height;
@import './content/NitroCardContentView';
@import './header/NitroCardHeaderView';
@import './simple-header/NitroCardSimpleHeaderView';
@import './tabs/NitroCardTabsView';

View File

@ -4,11 +4,11 @@ import { NitroCardViewProps } from './NitroCardView.types';
export const NitroCardView: FC<NitroCardViewProps> = props =>
{
const { className = '' } = props;
const { className = '', disableDrag = false } = props;
return (
<DraggableWindow handle=".drag-handler">
<div className={ 'nitro-card d-flex flex-column ' + className }>
<DraggableWindow handle=".drag-handler" disableDrag= { disableDrag }>
<div className={ 'nitro-card d-flex flex-column rounded border shadow overflow-hidden ' + className }>
{ props.children }
</div>
</DraggableWindow>

View File

@ -1,4 +1,5 @@
export interface NitroCardViewProps
{
className?: string;
disableDrag?: boolean;
}

View File

@ -4,7 +4,7 @@ import { NitroCardContentViewProps } from './NitroCardContextView.types';
export const NitroCardContentView: FC<NitroCardContentViewProps> = props =>
{
return (
<div className="bg-light rounded-bottom border border-top-0 p-2 shadow overflow-hidden content-area">
<div className="bg-light p-2 content-area">
{ props.children }
</div>
);

View File

@ -0,0 +1,4 @@
.nitro-card-header {
min-height: $nitro-card-header-height;
max-height: $nitro-card-header-height;
}

View File

@ -6,7 +6,7 @@ export const NitroCardHeaderView: FC<NitroCardHeaderViewProps> = props =>
const { headerText = null, onCloseClick = null } = props;
return (
<div className="drag-handler d-flex align-items-center bg-primary border border-bottom-0 rounded-top px-2 py-1">
<div className="drag-handler d-flex align-items-center bg-primary px-2 py-1 nitro-card-header">
<div className="d-flex flex-grow-1 justify-content-center align-items-center">
<div className="h4 m-0 text-white text-shadow">{ headerText }</div>
</div>

View File

@ -0,0 +1,19 @@
import { FC } from 'react';
import { NitroCardSimpleHeaderViewProps } from './NitroCardSimpleHeaderView.types';
export const NitroCardSimpleHeaderView: FC<NitroCardSimpleHeaderViewProps> = props =>
{
const { headerText = null, onCloseClick = null } = props;
return (
<div className="d-flex align-items-center bg-light">
<div className="col-1"></div>
<div className="d-flex bg-primary-split flex-grow-1 justify-content-center align-items-center border border-top-0 rounded-bottom px-2 py-1">
<div className="h5 m-0 text-white text-shadow">{ headerText }</div>
</div>
<div className="d-flex col-1 justify-content-center align-items-center">
<i className="fas fa-times cursor-pointer" onClick={ onCloseClick }></i>
</div>
</div>
);
}

View File

@ -0,0 +1,7 @@
import { MouseEvent } from 'react';
export interface NitroCardSimpleHeaderViewProps
{
headerText: string;
onCloseClick: (event: MouseEvent) => void;
}

View File

@ -0,0 +1,2 @@
export * from './NitroCardSimpleHeaderView';
export * from './NitroCardSimpleHeaderView.types';

View File

@ -0,0 +1,6 @@
.nitro-card-tabs {
min-height: $nitro-card-tabs-height;
max-height: $nitro-card-tabs-height;
}
@import './tabs-item/NitroCardTabsItemView';

View File

@ -4,7 +4,7 @@ import { NitroCardTabsViewProps } from './NitroCardTabsView.types';
export const NitroCardTabsView: FC<NitroCardTabsViewProps> = props =>
{
return (
<ul className="nav nav-tabs justify-content-center bg-secondary border-start border-end px-2 pt-1">
<ul className="nav nav-tabs justify-content-center bg-secondary px-2 pt-1 nitro-card-tabs">
{ props.children }
</ul>
);

View File

@ -1 +1,2 @@
export * from './card';
export * from './loading-spinner/LoadingSpinnerView';

View File

@ -0,0 +1,42 @@
.spinner-container {
display: flex;
align-items: center;
justify-content: center;
.spinner {
margin: 2px;
width: 10px;
height: 10px;
border: $border-width solid $white;
background-color: rgba($white, 0.8);
border-radius: 100%;
display: inline-block;
-webkit-animation: sk-bouncedelay 1.4s infinite ease-in-out both;
animation: sk-bouncedelay 1.4s infinite ease-in-out both;
&:nth-child(1) {
-webkit-animation-delay: -0.32s;
animation-delay: -0.32s;
}
&:nth-child(2) {
-webkit-animation-delay: -0.16s;
animation-delay: -0.16s;
}
}
}
@-webkit-keyframes sk-bouncedelay {
0%, 80%, 100% { -webkit-transform: scale(0) }
40% { -webkit-transform: scale(1.0) }
}
@keyframes sk-bouncedelay {
0%, 80%, 100% {
-webkit-transform: scale(0);
transform: scale(0);
} 40% {
-webkit-transform: scale(1.0);
transform: scale(1.0);
}
}

View File

@ -0,0 +1,12 @@
import { FC } from 'react';
export const LoadingSpinnerView: FC = props =>
{
return (
<div className="spinner-container">
<div className="spinner"></div>
<div className="spinner"></div>
<div className="spinner"></div>
</div>
);
}

View File

@ -1,6 +1,6 @@
.nitro-currency-icon {
background-position: center;
background-repeat: no-repeat;
width: 25px;
height: 25px;
width: 20px;
height: 20px;
}

View File

@ -2,9 +2,10 @@
@import './badge-image/BadgeImage';
@import './catalog/CatalogView';
@import './catalog-icon/CatalogIconView';
@import './friend-list/FriendListView';
@import './hotel-view/HotelView';
@import './inventory/InventoryView';
@import './friend-list/FriendListView';
@import './limited-edition/LimitedEdition';
@import './loading/LoadingView';
@import './main/MainView';
@import './navigator/NavigatorView';

View File

@ -1,5 +1,9 @@
import { CatalogPageEvent, CatalogPagesEvent } from 'nitro-renderer';
import { CatalogPageEvent, CatalogPagesEvent, CatalogPurchaseEvent, CatalogPurchaseFailedEvent, CatalogPurchaseUnavailableEvent, CatalogSearchEvent, CatalogSoldOutEvent } from 'nitro-renderer';
import { FC, useCallback } from 'react';
import { CatalogPurchaseFailureEvent } from '../../events';
import { CatalogPurchasedEvent } from '../../events/catalog/CatalogPurchasedEvent';
import { CatalogPurchaseSoldOutEvent } from '../../events/catalog/CatalogPurchaseSoldOutEvent';
import { dispatchUiEvent } from '../../hooks/events/ui/ui-event';
import { CreateMessageHook } from '../../hooks/messages/message-event';
import { CatalogMessageHandlerProps } from './CatalogMessageHandler.types';
import { useCatalogContext } from './context/CatalogContext';
@ -33,8 +37,51 @@ export const CatalogMessageHandler: FC<CatalogMessageHandlerProps> = props =>
});
}, [ dispatchCatalogState ]);
const onCatalogPurchaseEvent = useCallback((event: CatalogPurchaseEvent) =>
{
const parser = event.getParser();
dispatchUiEvent(new CatalogPurchasedEvent(parser.offer));
}, []);
const onCatalogPurchaseFailedEvent = useCallback((event: CatalogPurchaseFailedEvent) =>
{
const parser = event.getParser();
dispatchUiEvent(new CatalogPurchaseFailureEvent(parser.code));
}, []);
const onCatalogPurchaseUnavailableEvent = useCallback((event: CatalogPurchaseUnavailableEvent) =>
{
const parser = event.getParser();
}, []);
const onCatalogSoldOutEvent = useCallback((event: CatalogSoldOutEvent) =>
{
const parser = event.getParser();
dispatchUiEvent(new CatalogPurchaseSoldOutEvent());
}, []);
const onCatalogSearchEvent = useCallback((event: CatalogSearchEvent) =>
{
const parser = event.getParser();
dispatchCatalogState({
type: CatalogActions.SET_CATALOG_ACTIVE_OFFER,
payload: {
activeOffer: parser.offer
}
});
}, [ dispatchCatalogState ]);
CreateMessageHook(CatalogPagesEvent, onCatalogPagesEvent);
CreateMessageHook(CatalogPageEvent, onCatalogPageEvent);
CreateMessageHook(CatalogPurchaseEvent, onCatalogPurchaseEvent);
CreateMessageHook(CatalogPurchaseFailedEvent, onCatalogPurchaseFailedEvent);
CreateMessageHook(CatalogPurchaseUnavailableEvent, onCatalogPurchaseUnavailableEvent);
CreateMessageHook(CatalogSoldOutEvent, onCatalogSoldOutEvent);
CreateMessageHook(CatalogSearchEvent, onCatalogSearchEvent);
return null;
}

View File

@ -1,5 +1,10 @@
.nitro-catalog {
width: 600px;
width: 620px;
.content-area {
height: 330px;
max-height: 330px;
}
}
@import './views/CatalogViews';

View File

@ -1,5 +1,6 @@
import { CatalogModeComposer, ICatalogPageData } from 'nitro-renderer';
import { CatalogModeComposer, ICatalogPageData, RoomPreviewer } from 'nitro-renderer';
import { FC, useCallback, useEffect, useReducer, useState } from 'react';
import { GetRoomEngine } from '../../api';
import { CatalogEvent } from '../../events';
import { useUiEvent } from '../../hooks/events/ui/ui-event';
import { SendMessageHook } from '../../hooks/messages/message-event';
@ -8,16 +9,17 @@ import { LocalizeText } from '../../utils/LocalizeText';
import { CatalogMessageHandler } from './CatalogMessageHandler';
import { CatalogMode, CatalogViewProps } from './CatalogView.types';
import { CatalogContextProvider } from './context/CatalogContext';
import { CatalogReducer, initialCatalog } from './reducers/CatalogReducer';
import { CatalogActions, CatalogReducer, initialCatalog } from './reducers/CatalogReducer';
import { CatalogNavigationView } from './views/navigation/CatalogNavigationView';
import { CatalogPageView } from './views/page/CatalogPageView';
import { CatalogSearchView } from './views/search/CatalogSearchView';
export const CatalogView: FC<CatalogViewProps> = props =>
{
const [ isVisible, setIsVisible ] = useState(false);
const [ currentTab, setCurrentTab ] = useState<ICatalogPageData>(null);
const [ roomPreviewer, setRoomPreviewer ] = useState<RoomPreviewer>(null);
const [ catalogState, dispatchCatalogState ] = useReducer(CatalogReducer, initialCatalog);
const { root = null } = catalogState;
const { root = null, currentTab = null, searchResult = null } = catalogState;
const onCatalogEvent = useCallback((event: CatalogEvent) =>
{
@ -26,6 +28,9 @@ export const CatalogView: FC<CatalogViewProps> = props =>
case CatalogEvent.SHOW_CATALOG:
setIsVisible(true);
return;
case CatalogEvent.HIDE_CATALOG:
setIsVisible(false);
return;
case CatalogEvent.TOGGLE_CATALOG:
setIsVisible(value => !value);
return;
@ -33,6 +38,7 @@ export const CatalogView: FC<CatalogViewProps> = props =>
}, []);
useUiEvent(CatalogEvent.SHOW_CATALOG, onCatalogEvent);
useUiEvent(CatalogEvent.HIDE_CATALOG, onCatalogEvent);
useUiEvent(CatalogEvent.TOGGLE_CATALOG, onCatalogEvent);
useEffect(() =>
@ -43,12 +49,35 @@ export const CatalogView: FC<CatalogViewProps> = props =>
{
SendMessageHook(new CatalogModeComposer(CatalogMode.MODE_NORMAL));
}
else
{
setCurrentTab(catalogState.root.children[0]);
}
}, [ isVisible, catalogState.root ]);
useEffect(() =>
{
setRoomPreviewer(new RoomPreviewer(GetRoomEngine(), ++RoomPreviewer.PREVIEW_COUNTER));
return () =>
{
setRoomPreviewer(prevValue =>
{
prevValue.dispose();
return null;
});
}
}, []);
function setCurrentTab(page: ICatalogPageData): void
{
dispatchCatalogState({
type: CatalogActions.SET_CATALOG_CURRENT_TAB,
payload: {
currentTab: page
}
});
}
const currentNavigationPage = ((searchResult && searchResult.page) || currentTab);
return (
<CatalogContextProvider value={ { catalogState, dispatchCatalogState } }>
<CatalogMessageHandler />
@ -62,12 +91,13 @@ export const CatalogView: FC<CatalogViewProps> = props =>
}) }
</NitroCardTabsView>
<NitroCardContentView>
<div className="row">
<div className="row h-100">
<div className="col-3">
<CatalogNavigationView page={ currentTab } />
<CatalogSearchView />
<CatalogNavigationView page={ currentNavigationPage } />
</div>
<div className="col">
<CatalogPageView />
<CatalogPageView roomPreviewer={ roomPreviewer } />
</div>
</div>
</NitroCardContentView>

View File

@ -0,0 +1,26 @@
export class FurniCategory
{
public static DEFAULT: number = 1;
public static WALL_PAPER: number = 2;
public static FLOOR: number = 3;
public static LANDSCAPE: number = 4;
public static POST_IT: number = 5;
public static POSTER: number = 6;
public static SOUND_SET: number = 7;
public static TRAX_SONG: number = 8;
public static PRESENT: number = 9;
public static ECOTRON_BOX: number = 10;
public static TROPHY: number = 11;
public static CREDIT_FURNI: number = 12;
public static PET_SHAMPOO: number = 13;
public static PET_CUSTOM_PART: number = 14;
public static PET_CUSTOM_PART_SHAMPOO: number = 15;
public static PET_SADDLE: number = 16;
public static GUILD_FURNI: number = 17;
public static GAME_FURNI: number = 18;
public static MONSTERPLANT_SEED: number = 19;
public static MONSTERPLANT_REVIVAL: number = 20;
public static MONSTERPLANT_REBREED: number = 21;
public static MONSTERPLANT_FERTILIZE: number = 22;
public static FIGURE_PURCHASABLE_SET: number = 23;
}

View File

@ -0,0 +1,11 @@
export class ProductTypeEnum
{
public static WALL: string = 'i';
public static FLOOR: string = 's';
public static EFFECT: string = 'e';
public static HABBO_CLUB: string = 'h';
public static BADGE: string = 'b';
public static GAME_TOKEN: string = 'GAME_TOKEN';
public static PET: string = 'p';
public static ROBOT: string = 'r';
}

View File

@ -1,34 +1,46 @@
import { ICatalogPageData, ICatalogPageParser } from 'nitro-renderer';
import { CatalogPageOfferData, ICatalogPageData, ICatalogPageParser } from 'nitro-renderer';
import { Reducer } from 'react';
import { ICatalogOffers, ICatalogSearchResult, SetOffersToNodes } from '../utils/CatalogUtilities';
export interface ICatalogState
{
needsCatalogUpdate: boolean;
root: ICatalogPageData;
offerRoot: ICatalogOffers;
currentTab: ICatalogPageData;
pageParser: ICatalogPageParser;
activeOffer: CatalogPageOfferData;
searchResult: ICatalogSearchResult;
}
export interface ICatalogAction
{
type: string;
payload: {
flag?: boolean;
root?: ICatalogPageData;
offerRoot?: ICatalogOffers;
currentTab?: ICatalogPageData;
pageParser?: ICatalogPageParser;
activeOffer?: CatalogPageOfferData;
searchResult?: ICatalogSearchResult;
}
}
export class CatalogActions
{
public static SET_NEEDS_UPDATE: string = 'CA_SET_NEEDS_UPDATE';
public static SET_CATALOG_ROOT: string = 'CA_SET_CATALOG_ROOT';
public static SET_CATALOG_CURRENT_TAB: string = 'CA_SET_CATALOG_CURRENT_TAB';
public static SET_CATALOG_PAGE_PARSER: string = 'CA_SET_CATALOG_PAGE';
public static SET_CATALOG_ACTIVE_OFFER: string = 'CA_SET_ACTIVE_OFFER';
public static SET_SEARCH_RESULT: string = 'CA_SET_SEARCH_RESULT';
}
export const initialCatalog: ICatalogState = {
needsCatalogUpdate: true,
root: null,
pageParser: null
offerRoot: null,
currentTab: null,
pageParser: null,
activeOffer: null,
searchResult: null
}
export const CatalogReducer: Reducer<ICatalogState, ICatalogAction> = (state, action) =>
@ -37,13 +49,57 @@ export const CatalogReducer: Reducer<ICatalogState, ICatalogAction> = (state, ac
{
case CatalogActions.SET_CATALOG_ROOT: {
const root = (action.payload.root || state.root || null);
const currentTab = ((root && (root.children.length > 0) && root.children[0]) || null);
return { ...state, root };
const offerRoot: ICatalogOffers = {};
SetOffersToNodes(offerRoot, root);
return { ...state, root, offerRoot, currentTab };
}
case CatalogActions.SET_CATALOG_CURRENT_TAB: {
const currentTab = (action.payload.currentTab || state.currentTab || null);
const searchResult = null;
return { ...state, currentTab, searchResult };
}
case CatalogActions.SET_CATALOG_PAGE_PARSER: {
const pageParser = (action.payload.pageParser || state.pageParser || null);
const pageParser = action.payload.pageParser;
return { ...state, pageParser };
let activeOffer = null;
if(state.activeOffer)
{
for(const offer of pageParser.offers)
{
if(offer.offerId !== state.activeOffer.offerId) continue;
activeOffer = offer;
break;
}
}
if(!activeOffer) activeOffer = ((pageParser && (pageParser.offers.length > 0) && pageParser.offers[0]) || null);
const searchResult = state.searchResult;
if(searchResult)
{
searchResult.furniture = null;
}
return { ...state, pageParser, activeOffer, searchResult };
}
case CatalogActions.SET_CATALOG_ACTIVE_OFFER: {
const activeOffer = (action.payload.activeOffer || state.activeOffer || null);
return { ...state, activeOffer };
}
case CatalogActions.SET_SEARCH_RESULT: {
const searchResult = (action.payload.searchResult || null);
return { ...state, searchResult };
}
default:
return state;

View File

@ -0,0 +1,67 @@
import { CatalogPageOfferData, ICatalogPageData, IFurnitureData } from 'nitro-renderer';
import { GetProductDataForLocalization } from '../../../api/nitro/session/GetProductDataForLocalization';
export interface ICatalogOffers
{
[key: string]: ICatalogPageData[];
}
export interface ICatalogSearchResult
{
page: ICatalogPageData;
furniture: IFurnitureData[];
}
export function GetOfferName(offer: CatalogPageOfferData): string
{
const productData = GetProductDataForLocalization(offer.localizationId);
if(productData) return productData.name;
return offer.localizationId;
}
export function GetOfferNodes(offers: ICatalogOffers, offerId: number): ICatalogPageData[]
{
const pages = offers[offerId.toString()];
const allowedPages: ICatalogPageData[] = [];
if(pages && pages.length)
{
for(const page of pages)
{
if(!page.visible) continue;
allowedPages.push(page);
}
}
return allowedPages;
}
export function SetOffersToNodes(offers: ICatalogOffers, pageData: ICatalogPageData): void
{
if(pageData.offerIds && pageData.offerIds.length)
{
for(const offerId of pageData.offerIds)
{
let existing = offers[offerId.toString()];
if(!existing)
{
existing = [];
offers[offerId.toString()] = existing;
}
if(existing.indexOf(pageData) >= 0) continue;
existing.push(pageData);
}
}
if(pageData.children && pageData.children.length)
{
for(const child of pageData.children) SetOffersToNodes(offers, child);
}
}

View File

@ -1,6 +1,3 @@
.nitro-catalog {
}
@import './navigation/CatalogNavigationView';
@import './page/CatalogPageView';
@import './search/CatalogSearchView';

View File

@ -3,8 +3,8 @@
background-color: $grid-bg-color !important;
.navigation-container {
height: 275px;
max-height: 275px;
height: 270px;
max-height: 270px;
overflow-y: auto;
}
}

View File

@ -1,13 +1,23 @@
import { FC } from 'react';
import { ICatalogPageData } from 'nitro-renderer';
import { FC, useEffect } from 'react';
import { CatalogNavigationViewProps } from './CatalogNavigationView.types';
import { CatalogNavigationSetView } from './set/CatalogNavigationSetView';
export let ACTIVE_PAGES: ICatalogPageData[] = [];
export const CatalogNavigationView: FC<CatalogNavigationViewProps> = props =>
{
const { page = null } = props;
useEffect(() =>
{
if(!page) return;
ACTIVE_PAGES = [ page ];
}, [ page ]);
return (
<div className="nitro-catalog-navigation border border-2 rounded overflow-hidden">
<div className="border border-2 rounded overflow-hidden nitro-catalog-navigation">
<div className="navigation-container m-1">
<CatalogNavigationSetView page={ page } isFirstSet={ true } />
</div>

View File

@ -12,17 +12,19 @@ export const CatalogNavigationItemView: FC<CatalogNavigationItemViewProps> = pro
useEffect(() =>
{
if(!isActive) return;
if(!isActive || !page) return;
SendMessageHook(GetCatalogPageComposer(page.pageId, -1, CatalogMode.MODE_NORMAL));
}, [ isActive, page ]);
function select(): void
{
if(!page) return;
setActiveChild(prevValue =>
{
if(prevValue === page) return null;
return page;
});
}
@ -31,7 +33,7 @@ export const CatalogNavigationItemView: FC<CatalogNavigationItemViewProps> = pro
<div className="col pe-1 pb-1 catalog-navigation-item-container">
<div className="d-flex align-items-center cursor-pointer catalog-navigation-item" onClick={ select }>
<CatalogIconView icon={ page.icon } />
<div className="flex-grow-1 text-black text-nowrap text-truncate overflow-hidden 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') } /> }
</div>
{ isActive && page.children && (page.children.length > 0) &&

View File

@ -1,5 +1,6 @@
import { ICatalogPageData } from 'nitro-renderer';
import { FC, useEffect, useState } from 'react';
import { ACTIVE_PAGES } from '../CatalogNavigationView';
import { CatalogNavigationItemView } from '../item/CatalogNavigationItemView';
import { CatalogNavigationSetViewProps } from './CatalogNavigationSetView.types';
@ -10,10 +11,22 @@ export const CatalogNavigationSetView: FC<CatalogNavigationSetViewProps> = props
useEffect(() =>
{
if(!isFirstSet) return;
if(!isFirstSet || !page || (page.pageId === -1)) return;
if(page && page.children.length) setActiveChild(page.children[0]);
}, [ page, isFirstSet ]);
useEffect(() =>
{
if(!activeChild) return;
const index = (ACTIVE_PAGES.push(activeChild) - 1);
return () =>
{
ACTIVE_PAGES.splice(index, (ACTIVE_PAGES.length - index));
}
}, [ activeChild ]);
return (
<div className="row row-cols-1 g-0 catalog-navigation-set-container w-100">

View File

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

View File

@ -2,20 +2,18 @@ import { FC } from 'react';
import { useCatalogContext } from '../../context/CatalogContext';
import { CatalogPageViewProps } from './CatalogPageView.types';
import { GetCatalogLayout } from './layout/GetCatalogLayout';
import { CatalogLayoutSearchResultView } from './search-result/CatalogLayoutSearchResultView';
export const CatalogPageView: FC<CatalogPageViewProps> = props =>
{
const { roomPreviewer = null } = props;
const { catalogState = null } = useCatalogContext();
const { pageParser = null } = catalogState;
const { pageParser = null, searchResult = null } = catalogState;
return (
<div className="row h-100">
<div className="col-7">
{ pageParser && GetCatalogLayout(pageParser) }
</div>
<div className="col">
preview area
</div>
</div>
);
if(searchResult && searchResult.furniture)
{
return <CatalogLayoutSearchResultView roomPreviewer={ roomPreviewer } furnitureDatas={ searchResult.furniture } />;
}
return ((pageParser && GetCatalogLayout(pageParser, roomPreviewer)) || null);
}

View File

@ -1,4 +1,6 @@
import { RoomPreviewer } from 'nitro-renderer';
export interface CatalogPageViewProps
{
roomPreviewer: RoomPreviewer;
}

View File

@ -1,6 +1,7 @@
import { ICatalogPageParser } from 'nitro-renderer';
import { ICatalogPageParser, RoomPreviewer } from 'nitro-renderer';
export interface CatalogLayoutProps
{
pageParser: ICatalogPageParser;
roomPreviewer: RoomPreviewer;
pageParser?: ICatalogPageParser;
}

View File

@ -1,7 +1,7 @@
import { ICatalogPageParser } from 'nitro-renderer';
import { ICatalogPageParser, RoomPreviewer } from 'nitro-renderer';
import { CatalogLayoutDefaultView } from './default/CatalogLayoutDefaultView';
export function GetCatalogLayout(pageParser: ICatalogPageParser): JSX.Element
export function GetCatalogLayout(pageParser: ICatalogPageParser, roomPreviewer: RoomPreviewer): JSX.Element
{
switch(pageParser.catalogType)
{
@ -37,6 +37,6 @@ export function GetCatalogLayout(pageParser: ICatalogPageParser): JSX.Element
return null;
case 'default_3x3':
default:
return <CatalogLayoutDefaultView pageParser={ pageParser } />
return <CatalogLayoutDefaultView roomPreviewer={ roomPreviewer } pageParser={ pageParser } />
}
}

View File

@ -1,13 +1,123 @@
import { FC } from 'react';
import { Vector3d } from 'nitro-renderer';
import { FC, useEffect } from 'react';
import { GetAvatarRenderManager, GetFurnitureDataForProductOffer, GetSessionDataManager } from '../../../../../../api';
import { LimitedEditionCompletePlateView } from '../../../../../limited-edition/complete-plate/LimitedEditionCompletePlateView';
import { RoomPreviewerView } from '../../../../../room-previewer/RoomPreviewerView';
import { useCatalogContext } from '../../../../context/CatalogContext';
import { FurniCategory } from '../../../../enums/FurniCategory';
import { ProductTypeEnum } from '../../../../enums/ProductTypeEnum';
import { GetOfferName } from '../../../../utils/CatalogUtilities';
import { CatalogPageOffersView } from '../../offers/CatalogPageOffersView';
import { CatalogPurchaseView } from '../../purchase/CatalogPurchaseView';
import { CatalogLayoutDefaultViewProps } from './CatalogLayoutDefaultView.types';
export const CatalogLayoutDefaultView: FC<CatalogLayoutDefaultViewProps> = props =>
{
const { pageParser = null } = props;
const { roomPreviewer = null, pageParser = null } = props;
const { catalogState } = useCatalogContext();
const { activeOffer = null } = catalogState;
useEffect(() =>
{
if(!roomPreviewer) return;
if(!activeOffer)
{
roomPreviewer && roomPreviewer.reset(false);
return;
}
const product = activeOffer.products[0];
if(!product) return;
const furniData = GetFurnitureDataForProductOffer(product);
if(!furniData && product.productType !== ProductTypeEnum.ROBOT) return;
switch(product.productType)
{
case ProductTypeEnum.ROBOT: {
roomPreviewer.updateObjectRoom('default', 'default', 'default');
const figure = GetAvatarRenderManager().getFigureStringWithFigureIds(product.extraParam, 'm', []);
roomPreviewer.addAvatarIntoRoom(figure, 0);
return;
}
case ProductTypeEnum.FLOOR: {
roomPreviewer.updateObjectRoom('default', 'default', 'default');
if(furniData.specialType === FurniCategory.FIGURE_PURCHASABLE_SET)
{
const setIds: number[] = [];
const sets = furniData.customParams.split(',');
for(const set of sets)
{
const setId = parseInt(set);
if(GetAvatarRenderManager().isValidFigureSetForGender(setId, GetSessionDataManager().gender)) setIds.push(setId);
}
const figure = GetAvatarRenderManager().getFigureStringWithFigureIds(GetSessionDataManager().figure, GetSessionDataManager().gender, setIds);
roomPreviewer.addAvatarIntoRoom(figure, 0);
}
else
{
roomPreviewer.addFurnitureIntoRoom(product.furniClassId, new Vector3d(90));
}
return;
}
case ProductTypeEnum.WALL:
switch(furniData.className)
{
case 'floor':
roomPreviewer.reset(false);
roomPreviewer.updateObjectRoom(product.extraParam);
break;
case 'wallpaper':
roomPreviewer.reset(false);
roomPreviewer.updateObjectRoom(null, product.extraParam);
break;
case 'landscape':
roomPreviewer.reset(false);
roomPreviewer.updateObjectRoom(null, null, product.extraParam);
break;
default:
roomPreviewer.updateObjectRoom('default', 'default', 'default');
roomPreviewer.addWallItemIntoRoom(product.furniClassId, new Vector3d(90), product.extraParam);
return;
}
// const windowData = Nitro.instance.sessionDataManager.getWallItemDataByName('ads_twi_windw');
// if(windowData)
// {
// this._roomPreviewer.addWallItemIntoRoom(windowData.id, new Vector3d(90), windowData.customParams)
// }
return;
}
}, [ roomPreviewer, activeOffer ]);
const product = ((activeOffer && activeOffer.products[0]) || null);
return (
<div className="d-flex">
{ pageParser && pageParser.localization.texts[0] }
<div className="row h-100">
<div className="col-7">
<CatalogPageOffersView offers={ pageParser.offers } />
</div>
{ product &&
<div className="position-relative d-flex flex-column col">
<RoomPreviewerView roomPreviewer={ roomPreviewer } height={ 140 } />
{ product.uniqueLimitedItem &&
<LimitedEditionCompletePlateView uniqueLimitedItemsLeft={ product.uniqueLimitedItemsLeft } uniqueLimitedSeriesSize={ product.uniqueLimitedSeriesSize } /> }
<div className="fs-6 text-black mt-1 overflow-hidden">{ GetOfferName(activeOffer) }</div>
<CatalogPurchaseView offer={ activeOffer } pageId={ pageParser.pageId } />
</div> }
</div>
);
}

View File

@ -0,0 +1,25 @@
.catalog-offer-item-container {
height: 48px;
max-height: 48px;
.catalog-offer-item {
width: 100%;
height: 100%;
border-color: $grid-border-color !important;
background-color: $grid-bg-color !important;
background-position: center;
background-repeat: no-repeat;
overflow: hidden;
&.active {
border-color: $grid-active-border-color !important;
background-color: $grid-active-bg-color !important;
}
.badge {
top: 2px;
right: 2px;
font-size: 8px;
}
}
}

View File

@ -0,0 +1,67 @@
import { FurnitureType, MouseEventType } from 'nitro-renderer';
import { FC, MouseEvent, useCallback } from 'react';
import { GetRoomEngine, GetSessionDataManager } from '../../../../../api';
import { LimitedEditionStyledNumberView } from '../../../../limited-edition/styled-number/LimitedEditionStyledNumberView';
import { useCatalogContext } from '../../../context/CatalogContext';
import { CatalogActions } from '../../../reducers/CatalogReducer';
import { CatalogPageOfferViewProps } from './CatalogPageOfferView.types';
export const CatalogPageOfferView: FC<CatalogPageOfferViewProps> = props =>
{
const { isActive = false, offer = null } = props;
const { dispatchCatalogState = null } = useCatalogContext();
const onMouseEvent = useCallback((event: MouseEvent) =>
{
switch(event.type)
{
case MouseEventType.MOUSE_DOWN:
dispatchCatalogState({
type: CatalogActions.SET_CATALOG_ACTIVE_OFFER,
payload: {
activeOffer: offer
}
});
return;
case MouseEventType.MOUSE_UP:
return;
case MouseEventType.ROLL_OUT:
return;
}
}, [ offer, dispatchCatalogState ]);
const product = ((offer.products && offer.products[0]) || null);
if(!product) return null;
function getIconUrl(): string
{
const productType = product.productType.toUpperCase();
switch(productType)
{
case FurnitureType.BADGE:
return GetSessionDataManager().getBadgeUrl(product.extraParam);
case FurnitureType.FLOOR:
return GetRoomEngine().getFurnitureFloorIconUrl(product.furniClassId);
case FurnitureType.WALL:
return GetRoomEngine().getFurnitureWallIconUrl(product.furniClassId, product.extraParam);
}
return '';
}
const imageUrl = `url(${ getIconUrl() })`;
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 }} onMouseDown={ onMouseEvent } onMouseUp={ onMouseEvent } onMouseOut={ onMouseEvent }>
{ (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,7 @@
import { CatalogPageOfferData } from 'nitro-renderer';
export interface CatalogPageOfferViewProps
{
isActive: boolean;
offer: CatalogPageOfferData;
}

View File

@ -0,0 +1,5 @@
.catalog-offers-container {
height: 314px;
max-height: 314px;
overflow-y: auto;
}

View File

@ -0,0 +1,20 @@
import { FC } from 'react';
import { useCatalogContext } from '../../../context/CatalogContext';
import { CatalogPageOfferView } from '../offer/CatalogPageOfferView';
import { CatalogPageOffersViewProps } from './CatalogPageOffersView.types';
export const CatalogPageOffersView: FC<CatalogPageOffersViewProps> = props =>
{
const { offers = [] } = props;
const { catalogState } = useCatalogContext();
const { activeOffer = null } = catalogState;
return (
<div className="row row-cols-5 align-content-start g-0 mb-n1 w-100 catalog-offers-container">
{ offers && (offers.length > 0) && offers.map((offer, index) =>
{
return <CatalogPageOfferView key={ index } isActive={ (activeOffer === offer) } offer={ offer } />
}) }
</div>
);
}

View File

@ -0,0 +1,6 @@
import { CatalogPageOfferData } from 'nitro-renderer';
export interface CatalogPageOffersViewProps
{
offers: CatalogPageOfferData[];
}

View File

@ -0,0 +1,8 @@
.quantity-input {
min-height: 20px;
max-height: 20px;
width: 29px;
padding: 3px 5px;
}
@import './purchase-button/CatalogPurchaseButtonView';

View File

@ -0,0 +1,75 @@
import { FC, useEffect, useState } from 'react';
import { CurrencyIcon } from '../../../../../utils/currency-icon/CurrencyIcon';
import { LocalizeText } from '../../../../../utils/LocalizeText';
import { CatalogPurchaseViewProps } from './CatalogPurchaseView.types';
import { CatalogPurchaseButtonView } from './purchase-button/CatalogPurchaseButtonView';
export const CatalogPurchaseView: FC<CatalogPurchaseViewProps> = props =>
{
const { offer = null, pageId = -1 } = props;
const [ quantity, setQuantity ] = useState(1);
useEffect(() =>
{
setQuantity(1);
}, [ offer ]);
function increaseQuantity(): void
{
let newQuantity = quantity + 1;
if(newQuantity > 99) newQuantity = 99
setQuantity(newQuantity);
}
function decreaseQuantity(): void
{
let newQuantity = quantity - 1;
if(newQuantity <= 0) newQuantity = 1;
setQuantity(newQuantity);
}
function updateQuantity(amount: number): void
{
if(isNaN(amount) || (amount <= 0)) amount = 1;
if(amount > 99) amount = 99;
setQuantity(amount);
}
return (
<div className="d-flex flex-column flex-grow-1 justify-content-end">
<div className="d-flex align-items-end">
<div className="flex-grow-1 align-items-end">
<span className="text-black">{ LocalizeText('catalog.bundlewidget.price') }</span>
{ offer.bundlePurchaseAllowed &&
<div className="d-flex align-items-center">
<i className="fas fa-caret-left cursor-pointer me-1 text-black" onClick={ decreaseQuantity } />
<input type="number" className="form-control form-control-sm quantity-input" value={ quantity } onChange={ event => updateQuantity(event.target.valueAsNumber)} />
<i className="fas fa-caret-right cursor-pointer ms-1 text-black" onClick={ increaseQuantity } />
</div> }
</div>
<div className="d-flex flex-column">
{ (offer.priceCredits > 0) &&
<div className="d-flex align-items-center justify-content-end">
<span className="text-black ms-1">{ offer.priceCredits * quantity }</span>
<CurrencyIcon type={ -1 } />
</div> }
{ (offer.priceActivityPoints > 0) &&
<div className="d-flex align-items-center justify-content-end">
<span className="text-black ms-1">{ offer.priceActivityPoints * quantity }</span>
<CurrencyIcon type={ offer.priceActivityPointsType } />
</div> }
</div>
</div>
<div className="d-flex flex-column mt-1">
<CatalogPurchaseButtonView className="btn-sm w-100" offer={ offer } pageId={ pageId } quantity={ quantity } />
{ offer.giftable && <button type="button" className="btn btn-secondary btn-sm w-100 mt-1">{ LocalizeText('catalog.purchase_confirmation.gift') }</button> }
</div>
</div>
);
}

View File

@ -0,0 +1,7 @@
import { CatalogPageOfferData } from 'nitro-renderer';
export interface CatalogPurchaseViewProps
{
offer: CatalogPageOfferData;
pageId: number;
}

View File

@ -0,0 +1,73 @@
import { CatalogPurchaseComposer } from 'nitro-renderer';
import { FC, useCallback, useEffect, useState } from 'react';
import { CatalogEvent } from '../../../../../../events';
import { useUiEvent } from '../../../../../../hooks/events/ui/ui-event';
import { SendMessageHook } from '../../../../../../hooks/messages/message-event';
import { LoadingSpinnerView } from '../../../../../../layout';
import { LocalizeText } from '../../../../../../utils/LocalizeText';
import { GetCurrencyAmount } from '../../../../../purse/utils/CurrencyHelper';
import { CatalogPurchaseButtonViewProps, CatalogPurchaseState } from './CatalogPurchaseButtonView.types';
export const CatalogPurchaseButtonView: FC<CatalogPurchaseButtonViewProps> = props =>
{
const { className = '', offer = null, pageId = -1, extra = null, quantity = 1 } = props;
const [ purchaseState, setPurchaseState ] = useState(CatalogPurchaseState.NONE);
const onCatalogEvent = useCallback((event: CatalogEvent) =>
{
switch(event.type)
{
case CatalogEvent.PURCHASE_SUCCESS:
setPurchaseState(CatalogPurchaseState.NONE);
return;
case CatalogEvent.SOLD_OUT:
setPurchaseState(CatalogPurchaseState.SOLD_OUT);
return;
}
}, []);
useUiEvent(CatalogEvent.PURCHASE_SUCCESS, onCatalogEvent);
useUiEvent(CatalogEvent.SOLD_OUT, onCatalogEvent);
useEffect(() =>
{
setPurchaseState(CatalogPurchaseState.NONE);
}, [ offer, quantity ]);
const purchase = useCallback(() =>
{
setPurchaseState(CatalogPurchaseState.PURCHASE);
SendMessageHook(new CatalogPurchaseComposer(pageId, offer.offerId, extra, quantity));
}, [ pageId, offer, extra, quantity ]);
const product = offer.products[0];
if(product && product.uniqueLimitedItem && !product.uniqueLimitedItemsLeft)
{
return <button type="button" className={ 'btn btn-danger ' + className } disabled>{ LocalizeText('catalog.alert.limited_edition_sold_out.title') }</button>;
}
if((offer.priceCredits * quantity) > GetCurrencyAmount(-1))
{
return <button type="button" className={ 'btn btn-danger ' + className } disabled>{ LocalizeText('catalog.alert.notenough.title') }</button>;
}
if((offer.priceActivityPoints * quantity) > GetCurrencyAmount(offer.priceActivityPointsType))
{
return <button type="button" className={ 'btn btn-danger ' + className } disabled>{ LocalizeText('catalog.alert.notenough.activitypoints.title.' + offer.priceActivityPointsType) }</button>;
}
switch(purchaseState)
{
case CatalogPurchaseState.CONFIRM:
return <button type="button" className={ 'btn btn-warning ' + className } onClick={ purchase }>{ LocalizeText('catalog.marketplace.confirm_title') }</button>;
case CatalogPurchaseState.PURCHASE:
return <button type="button" className={ 'btn btn-primary ' + className } disabled><LoadingSpinnerView /></button>;
case CatalogPurchaseState.SOLD_OUT:
return <button type="button" className={ 'btn btn-danger ' + className } disabled>{ LocalizeText('generic.failed') + ' - ' + LocalizeText('catalog.alert.limited_edition_sold_out.title') }</button>;
case CatalogPurchaseState.NONE:
default:
return <button type="button" className={ 'btn btn-success ' + className } onClick={ event => setPurchaseState(CatalogPurchaseState.CONFIRM) }>{ LocalizeText('buy') }</button>
}
}

View File

@ -0,0 +1,20 @@
import { CatalogPageOfferData } from 'nitro-renderer';
export interface CatalogPurchaseButtonViewProps
{
className?: string;
offer: CatalogPageOfferData;
pageId: number;
extra?: string;
quantity?: number;
}
export class CatalogPurchaseState
{
public static NONE = 0;
public static CONFIRM = 1;
public static PURCHASE = 2;
public static NO_CREDITS = 3;
public static NO_POINTS = 4;
public static SOLD_OUT = 5;
}

View File

@ -0,0 +1,2 @@
@import './offer/CatalogSearchResultOfferView';
@import './offers/CatalogSearchResultOffersView';

View File

@ -0,0 +1,123 @@
import { Vector3d } from 'nitro-renderer';
import { FC, useEffect } from 'react';
import { GetAvatarRenderManager, GetFurnitureDataForProductOffer, GetSessionDataManager } from '../../../../../api';
import { LimitedEditionCompletePlateView } from '../../../../limited-edition/complete-plate/LimitedEditionCompletePlateView';
import { RoomPreviewerView } from '../../../../room-previewer/RoomPreviewerView';
import { useCatalogContext } from '../../../context/CatalogContext';
import { FurniCategory } from '../../../enums/FurniCategory';
import { ProductTypeEnum } from '../../../enums/ProductTypeEnum';
import { GetOfferName } from '../../../utils/CatalogUtilities';
import { CatalogPurchaseView } from '../purchase/CatalogPurchaseView';
import { CatalogLayoutSearchResultViewProps } from './CatalogLayoutSearchResultView.types';
import { CatalogSearchResultOffersView } from './offers/CatalogSearchResultOffersView';
export const CatalogLayoutSearchResultView: FC<CatalogLayoutSearchResultViewProps> = props =>
{
const { roomPreviewer = null, furnitureDatas = null } = props;
const { catalogState } = useCatalogContext();
const { activeOffer = null } = catalogState;
useEffect(() =>
{
if(!roomPreviewer) return;
if(!activeOffer)
{
roomPreviewer && roomPreviewer.reset(false);
return;
}
const product = activeOffer.products[0];
if(!product) return;
const furniData = GetFurnitureDataForProductOffer(product);
if(!furniData && product.productType !== ProductTypeEnum.ROBOT) return;
switch(product.productType)
{
case ProductTypeEnum.ROBOT: {
roomPreviewer.updateObjectRoom('default', 'default', 'default');
const figure = GetAvatarRenderManager().getFigureStringWithFigureIds(product.extraParam, 'm', []);
roomPreviewer.addAvatarIntoRoom(figure, 0);
return;
}
case ProductTypeEnum.FLOOR: {
roomPreviewer.updateObjectRoom('default', 'default', 'default');
if(furniData.specialType === FurniCategory.FIGURE_PURCHASABLE_SET)
{
const setIds: number[] = [];
const sets = furniData.customParams.split(',');
for(const set of sets)
{
const setId = parseInt(set);
if(GetAvatarRenderManager().isValidFigureSetForGender(setId, GetSessionDataManager().gender)) setIds.push(setId);
}
const figure = GetAvatarRenderManager().getFigureStringWithFigureIds(GetSessionDataManager().figure, GetSessionDataManager().gender, setIds);
roomPreviewer.addAvatarIntoRoom(figure, 0);
}
else
{
roomPreviewer.addFurnitureIntoRoom(product.furniClassId, new Vector3d(90));
}
return;
}
case ProductTypeEnum.WALL:
switch(furniData.className)
{
case 'floor':
roomPreviewer.reset(false);
roomPreviewer.updateObjectRoom(product.extraParam);
break;
case 'wallpaper':
roomPreviewer.reset(false);
roomPreviewer.updateObjectRoom(null, product.extraParam);
break;
case 'landscape':
roomPreviewer.reset(false);
roomPreviewer.updateObjectRoom(null, null, product.extraParam);
break;
default:
roomPreviewer.updateObjectRoom('default', 'default', 'default');
roomPreviewer.addWallItemIntoRoom(product.furniClassId, new Vector3d(90), product.extraParam);
return;
}
// const windowData = Nitro.instance.sessionDataManager.getWallItemDataByName('ads_twi_windw');
// if(windowData)
// {
// this._roomPreviewer.addWallItemIntoRoom(windowData.id, new Vector3d(90), windowData.customParams)
// }
return;
}
}, [ roomPreviewer, activeOffer ]);
const product = ((activeOffer && activeOffer.products[0]) || null);
return (
<div className="row h-100">
<div className="col-7">
<CatalogSearchResultOffersView offers={ furnitureDatas } />
</div>
{ product &&
<div className="position-relative d-flex flex-column col">
<RoomPreviewerView roomPreviewer={ roomPreviewer } height={ 140 } />
{ product.uniqueLimitedItem &&
<LimitedEditionCompletePlateView uniqueLimitedItemsLeft={ product.uniqueLimitedItemsLeft } uniqueLimitedSeriesSize={ product.uniqueLimitedSeriesSize } /> }
<div className="fs-6 text-black mt-1 overflow-hidden">{ GetOfferName(activeOffer) }</div>
<CatalogPurchaseView offer={ activeOffer } pageId={ -1 } />
</div> }
</div>
);
}

View File

@ -0,0 +1,7 @@
import { IFurnitureData, RoomPreviewer } from 'nitro-renderer';
export interface CatalogLayoutSearchResultViewProps
{
roomPreviewer: RoomPreviewer;
furnitureDatas: IFurnitureData[];
}

View File

@ -0,0 +1,49 @@
import { CatalogSearchComposer, FurnitureType, MouseEventType } from 'nitro-renderer';
import { FC, MouseEvent, useCallback } from 'react';
import { GetRoomEngine, GetSessionDataManager } from '../../../../../../api';
import { SendMessageHook } from '../../../../../../hooks/messages/message-event';
import { CatalogSearchResultOfferViewProps } from './CatalogSearchResultOfferView.types';
export const CatalogSearchResultOfferView: FC<CatalogSearchResultOfferViewProps> = props =>
{
const { isActive = false, offer = null } = props;
const onMouseEvent = useCallback((event: MouseEvent) =>
{
switch(event.type)
{
case MouseEventType.MOUSE_DOWN:
SendMessageHook(new CatalogSearchComposer(offer.purchaseOfferId));
return;
case MouseEventType.MOUSE_UP:
return;
case MouseEventType.ROLL_OUT:
return;
}
}, [ offer ]);
function getIconUrl(): string
{
const productType = offer.type.toUpperCase();
switch(productType)
{
case FurnitureType.BADGE:
return GetSessionDataManager().getBadgeUrl(offer.customParams);
case FurnitureType.FLOOR:
return GetRoomEngine().getFurnitureFloorIconUrl(offer.id);
case FurnitureType.WALL:
return GetRoomEngine().getFurnitureWallIconUrl(offer.id, offer.customParams);
}
return '';
}
const imageUrl = `url(${ getIconUrl() })`;
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 ' : '') } style={ { backgroundImage: imageUrl }} onMouseDown={ onMouseEvent } onMouseUp={ onMouseEvent } onMouseOut={ onMouseEvent } />
</div>
);
}

View File

@ -0,0 +1,7 @@
import { IFurnitureData } from 'nitro-renderer';
export interface CatalogSearchResultOfferViewProps
{
isActive: boolean;
offer: IFurnitureData;
}

View File

@ -0,0 +1,5 @@
.catalog-offers-container {
height: 314px;
max-height: 314px;
overflow-y: auto;
}

View File

@ -0,0 +1,22 @@
import { FC } from 'react';
import { useCatalogContext } from '../../../../context/CatalogContext';
import { CatalogSearchResultOfferView } from '../offer/CatalogSearchResultOfferView';
import { CatalogSearchResultOffersViewProps } from './CatalogSearchResultOffersView.types';
export const CatalogSearchResultOffersView: FC<CatalogSearchResultOffersViewProps> = props =>
{
const { offers = [] } = props;
const { catalogState } = useCatalogContext();
const { activeOffer = null } = catalogState;
return (
<div className="row row-cols-5 align-content-start g-0 mb-n1 w-100 catalog-offers-container">
{ offers && (offers.length > 0) && offers.map((offer, index) =>
{
const isActive = (activeOffer && (activeOffer.products[0].furniClassId === offer.id));
return <CatalogSearchResultOfferView key={ index } isActive={ isActive } offer={ offer } />
}) }
</div>
);
}

View File

@ -0,0 +1,6 @@
import { IFurnitureData } from 'nitro-renderer';
export interface CatalogSearchResultOffersViewProps
{
offers: IFurnitureData[];
}

View File

@ -0,0 +1,125 @@
import { ICatalogPageData, IFurnitureData } from 'nitro-renderer';
import { FC, useCallback, useEffect, useState } from 'react';
import { GetSessionDataManager } from '../../../../api';
import { LocalizeText } from '../../../../utils/LocalizeText';
import { useCatalogContext } from '../../context/CatalogContext';
import { CatalogActions } from '../../reducers/CatalogReducer';
import { GetOfferNodes } from '../../utils/CatalogUtilities';
import { CatalogSearchViewProps } from './CatalogSearchView.types';
export const CatalogSearchView: FC<CatalogSearchViewProps> = props =>
{
const [ searchValue, setSearchValue ] = useState('');
const { catalogState = null, dispatchCatalogState = null } = useCatalogContext();
const { offerRoot = null, searchResult = null } = catalogState;
useEffect(() =>
{
setSearchValue(prevValue =>
{
if(!searchResult && prevValue && prevValue.length) return '';
return prevValue;
});
}, [ searchResult ]);
const processSearch = useCallback((search: string) =>
{
if(!search || !search.length || !offerRoot)
{
dispatchCatalogState({
type: CatalogActions.SET_SEARCH_RESULT,
payload: {
searchResult: null
}
});
return;
}
search = search.toLocaleLowerCase();
const furnitureData = GetSessionDataManager().getAllFurnitureData({
loadFurnitureData: null
});
if(!furnitureData) return;
const foundPages: ICatalogPageData[] = [];
const foundFurniture: IFurnitureData[] = [];
for(const furniture of furnitureData)
{
if((furniture.purchaseOfferId === -1) && (furniture.rentOfferId === -1)) continue;
const pages = [
...GetOfferNodes(offerRoot, furniture.purchaseOfferId),
...GetOfferNodes(offerRoot, furniture.rentOfferId)
];
if(!pages.length) continue;
const searchValue = [ furniture.className, furniture.name ].join(' ').toLocaleLowerCase();
if(searchValue.indexOf(search) === -1) continue;
foundPages.push(...pages);
foundFurniture.push(furniture);
}
const uniquePages = foundPages.filter((value, index, self) =>
{
return (self.indexOf(value) === index);
});
const catalogPage: ICatalogPageData = {
visible: true,
icon: 0,
pageId: -1,
pageName: LocalizeText('generic.search'),
localization: LocalizeText('generic.search'),
children: [ ...uniquePages ],
offerIds: []
};
dispatchCatalogState({
type: CatalogActions.SET_SEARCH_RESULT,
payload: {
searchResult: {
page: catalogPage,
furniture: foundFurniture
}
}
});
}, [ offerRoot, dispatchCatalogState ]);
useEffect(() =>
{
if(!searchValue)
{
processSearch(searchValue);
return;
}
const timeout = setTimeout(() => processSearch(searchValue), 300);
return () =>
{
clearTimeout(timeout);
}
}, [ searchValue, processSearch ]);
return (
<div className="d-flex mb-1">
<div className="d-flex flex-grow-1 me-1">
<input type="text" className="form-control form-control-sm" placeholder="search" value={ searchValue } onChange={ event => setSearchValue(event.target.value) } />
</div>
<div className="d-flex">
<button type="button" className="btn btn-primary btn-sm">
<i className="fas fa-search"></i>
</button>
</div>
</div>
);
}

View File

@ -0,0 +1,4 @@
export interface CatalogSearchViewProps
{
}

View File

@ -1,7 +1,5 @@
.nitro-inventory {
width: 475px;
// height: 300px;
// max-height: 300px;
.content-area {
height: 243.5px;

View File

@ -1,3 +1,9 @@
.limited-edition-info-container {
position: absolute;
top: 5px;
right: 15px;
}
@import './item/InventoryFurnitureItemView';
@import './results/InventoryFurnitureResultsView';
@import './search/InventoryFurnitureSearchView';

View File

@ -3,6 +3,7 @@ import { FC, useEffect, useState } from 'react';
import { GetRoomEngine } from '../../../../api';
import { SendMessageHook } from '../../../../hooks/messages/message-event';
import { LocalizeText } from '../../../../utils/LocalizeText';
import { LimitedEditionCompactPlateView } from '../../../limited-edition/compact-plate/LimitedEditionCompactPlateView';
import { RoomPreviewerView } from '../../../room-previewer/RoomPreviewerView';
import { useInventoryContext } from '../../context/InventoryContext';
import { InventoryFurnitureActions } from '../../reducers/InventoryFurnitureReducer';
@ -120,8 +121,12 @@ export const InventoryFurnitureView: FC<InventoryFurnitureViewProps> = props =>
<InventoryFurnitureSearchView groupItems={ groupItems } setGroupItems={ setFilteredGroupItems } />
<InventoryFurnitureResultsView groupItems={ filteredGroupItems } />
</div>
<div className="d-flex flex-column col justify-space-between">
<div className="position-relative d-flex flex-column col justify-space-between">
<RoomPreviewerView roomPreviewer={ roomPreviewer } height={ 140 } />
{ groupItem && groupItem.stuffData.isUnique &&
<div className="position-absolute limited-edition-info-container">
<LimitedEditionCompactPlateView uniqueNumber={ groupItem.stuffData.uniqueNumber } uniqueSeries={ groupItem.stuffData.uniqueSeries } />
</div> }
{ groupItem && <div className="d-flex flex-column flex-grow-1">
<p className="flex-grow-1 fs-6 text-black my-2">{ groupItem.name }</p>
{ !!roomSession && <button type="button" className="btn btn-success" onClick={ event => attemptItemPlacement(groupItem) }>{ LocalizeText('inventory.furni.placetoroom') }</button> }

View File

@ -1,5 +1,6 @@
import { MouseEventType } from 'nitro-renderer';
import { FC, MouseEvent, useCallback, useState } from 'react';
import { LimitedEditionStyledNumberView } from '../../../../limited-edition/styled-number/LimitedEditionStyledNumberView';
import { useInventoryContext } from '../../../context/InventoryContext';
import { InventoryFurnitureActions } from '../../../reducers/InventoryFurnitureReducer';
import { attemptItemPlacement } from '../../../utils/FurnitureUtilities';
@ -39,8 +40,12 @@ export const InventoryFurnitureItemView: FC<InventoryFurnitureItemViewProps> = p
return (
<div className="col pe-1 pb-1 inventory-furniture-item-container">
<div className={ 'position-relative border border-2 rounded inventory-furniture-item cursor-pointer ' + (isActive && 'active') } style={ { backgroundImage: imageUrl }} onMouseDown={ onMouseEvent } onMouseUp={ onMouseEvent } onMouseOut={ onMouseEvent }>
<div className={ 'position-relative border border-2 rounded inventory-furniture-item cursor-pointer ' + (isActive ? 'active ' : '') + (groupItem.stuffData.isUnique ? 'unique-item ' : '') } style={ { backgroundImage: imageUrl }} onMouseDown={ onMouseEvent } onMouseUp={ onMouseEvent } onMouseOut={ onMouseEvent }>
<span className="position-absolute badge border bg-danger px-1 rounded-circle">{ groupItem.getUnlockedCount() }</span>
{ groupItem.stuffData.isUnique &&
<div className="position-absolute unique-item-counter">
<LimitedEditionStyledNumberView value={ groupItem.stuffData.uniqueNumber } />
</div> }
</div>
</div>
);

View File

@ -0,0 +1,46 @@
.unique-item {
background: url("../../assets/images/unique/grid-bg.png") center no-repeat, rgba(0, 0, 0, 0.1);
&:before {
position: absolute;
content: ' ';
width: 100%;
height: 100%;
background: url("../../assets/images/unique/grid-bg-glass.png") center no-repeat;
}
&.sold-out:before {
background: url("../../assets/images/unique/grid-bg-sold-out.png") center no-repeat, url("../../assets/images/unique/grid-bg-glass.png") center no-repeat;
}
.unique-item-counter {
margin: 0 auto;
display: flex;
justify-content: center;
align-items: center;
bottom: 1px;
width: 100%;
height: 9px;
background: url("../../assets/images/unique/grid-count-bg.png") center no-repeat;
}
}
.unique-sold-out-blocker {
width: 364px;
height: 30px;
background: url("../../assets/images/unique/catalog-info-sold-out.png");
div {
float: right;
width: 140px;
text-align: center;
font-weight: bold;
margin-top: 5px;
margin-right: 17px;
color: #000;
}
}
@import './compact-plate/LimitedEditionCompactPlateView';
@import './complete-plate/LimitedEditionCompletePlateView';
@import './styled-number/LimitedEditionStyledNumberView';

View File

@ -0,0 +1,17 @@
.unique-compact-plate {
display: flex;
flex-direction: column;
justify-content: flex-end;
align-items: center;
right: 16px;
width: 34px;
height: 37px;
background: url("../../../assets/images/unique/inventory-info-amount-bg.png");
div {
display: flex;
justify-content: center;
align-items: center;
height: 9.5px;
}
}

View File

@ -0,0 +1,19 @@
import { FC } from 'react';
import { LimitedEditionStyledNumberView } from '../styled-number/LimitedEditionStyledNumberView';
import { LimitedEditionCompactPlateViewProps } from './LimitedEditionCompactPlateView.types';
export const LimitedEditionCompactPlateView: FC<LimitedEditionCompactPlateViewProps> = props =>
{
const { uniqueNumber = 0, uniqueSeries = 0 } = props;
return (
<div className="unique-compact-plate" style={ { zIndex: 1 } }>
<div>
<LimitedEditionStyledNumberView value={ uniqueNumber } />
</div>
<div>
<LimitedEditionStyledNumberView value={ uniqueSeries } />
</div>
</div>
);
}

View File

@ -0,0 +1,5 @@
export interface LimitedEditionCompactPlateViewProps
{
uniqueNumber: number;
uniqueSeries: number;
}

View File

@ -0,0 +1,20 @@
.unique-complete-plate {
top: 145px;
left: 10px;
width: 170px;
height: 29px;
background: url("../../../assets/images/unique/catalog-info-amount-bg.png");
div {
position: relative;
padding-left: 45px;
padding-right: 20px;
font-size: 10px;
color: #000;
div {
position: absolute;
right: 0px;
}
}
}

View File

@ -0,0 +1,26 @@
import { FC } from 'react';
import { LocalizeText } from '../../../utils/LocalizeText';
import { LimitedEditionStyledNumberView } from '../styled-number/LimitedEditionStyledNumberView';
import { LimitedEditionCompletePlateViewProps } from './LimitedEditionCompletePlateView.types';
export const LimitedEditionCompletePlateView: FC<LimitedEditionCompletePlateViewProps> = props =>
{
const { uniqueLimitedItemsLeft = 0, uniqueLimitedSeriesSize = 0 } = props;
return (
<div className="unique-complete-plate mt-1 mx-auto" style={ { zIndex: 1 } }>
<div>
<div>
<LimitedEditionStyledNumberView value={ uniqueLimitedItemsLeft } />
</div>
{ LocalizeText('unique.items.left') }
</div>
<div>
<div>
<LimitedEditionStyledNumberView value={ uniqueLimitedSeriesSize } />
</div>
{ LocalizeText('unique.items.number.sold') }
</div>
</div>
);
}

View File

@ -0,0 +1,5 @@
export interface LimitedEditionCompletePlateViewProps
{
uniqueLimitedItemsLeft: number;
uniqueLimitedSeriesSize: number;
}

View File

@ -0,0 +1,62 @@
.limited-edition-number {
display: inline-block;
outline: 0;
height: 5px;
margin-right: 1px;
background-image: url('../../../assets/images/unique/numbers.png');
background-repeat: no-repeat;
&:last-child {
margin-right: 0px;
}
&.n-0 {
width: 4px;
background-position: -1px 0px;
}
&.n-1 {
width: 2px;
background-position: -6px 0px;
}
&.n-2 {
width: 4px;
background-position: -9px 0px;
}
&.n-3 {
width: 4px;
background-position: -14px 0px;
}
&.n-4 {
width: 4px;
background-position: -19px 0px;
}
&.n-5 {
width: 4px;
background-position: -24px 0px;
}
&.n-6 {
width: 4px;
background-position: -29px 0px;
}
&.n-7 {
width: 4px;
background-position: -34px 0px;
}
&.n-8 {
width: 4px;
background-position: -39px 0px;
}
&.n-9 {
width: 4px;
background-position: -44px 0px;
}
}

View File

@ -0,0 +1,18 @@
import { FC } from 'react';
import { LimitedEditionStyledNumberViewProps } from './LimitedEditionStyledNumberView.types';
export const LimitedEditionStyledNumberView: FC<LimitedEditionStyledNumberViewProps> = props =>
{
const { value = 0 } = props;
const numbers = value.toString().split('');
return (
<>
{ numbers.map((number, index) =>
{
return <i key={ index } className={ 'limited-edition-number n-' + number } />;
})}
</>
);
}

View File

@ -0,0 +1,4 @@
export interface LimitedEditionStyledNumberViewProps
{
value: number;
}

View File

@ -7,6 +7,7 @@ import { CurrencyView } from './currency/CurrencyView';
import { PurseMessageHandler } from './PurseMessageHandler';
import { PurseViewProps } from './PurseView.types';
import { initialPurse, PurseReducer } from './reducers/PurseReducer';
import { SetLastCurrencies } from './utils/CurrencyHelper';
export const PurseView: FC<PurseViewProps> = props =>
{
@ -23,6 +24,8 @@ export const PurseView: FC<PurseViewProps> = props =>
SendMessageHook(new UserCurrencyComposer());
}, []);
SetLastCurrencies(currencies);
return (
<PurseContextProvider value={ { purseState, dispatchPurseState }}>
<PurseMessageHandler />

View File

@ -0,0 +1,20 @@
import { Currency } from './Currency';
let lastCurrencies: Currency[] = [];
export function SetLastCurrencies(currencies: Currency[]): void
{
lastCurrencies = currencies;
}
export function GetCurrencyAmount(type: number): number
{
for(const currency of lastCurrencies)
{
if(currency.type !== type) continue;
return currency.amount;
}
return 0;
}

View File

@ -7,6 +7,12 @@
border-radius: $border-radius;
height: 100%;
&.border-0 {
&::after {
content: none;
}
}
&::after {
position: absolute;
content: '';

View File

@ -26,6 +26,8 @@ export function RoomView(props: RoomViewProps): JSX.Element
return;
}
Nitro.instance.renderer.resize(window.innerWidth, window.innerHeight);
const canvasId = 1;
const displayObject = GetRoomEngine().getRoomInstanceDisplay(roomSession.roomId, canvasId, Nitro.instance.width, Nitro.instance.height, RoomGeometry.SCALE_ZOOMED_IN);