mirror of
https://github.com/billsonnn/nitro-react.git
synced 2024-11-26 23:50:52 +01:00
Merge pull request #76 from billsonnn/more-layout-changes
So many changes
This commit is contained in:
commit
fcd22a7e9e
26
.gitignore
vendored
26
.gitignore
vendored
@ -1,20 +1,7 @@
|
||||
# See http://help.github.com/ignore-files/ for more about ignoring files.
|
||||
|
||||
# compiled output
|
||||
/dist
|
||||
/tmp
|
||||
/out-tsc
|
||||
# Only exists if Bazel was run
|
||||
/bazel-out
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
|
||||
# profiling files
|
||||
chrome-profiler-events*.json
|
||||
speed-measure-plugin*.json
|
||||
|
||||
# IDEs and editors
|
||||
/.idea
|
||||
.project
|
||||
.classpath
|
||||
@ -22,30 +9,21 @@ speed-measure-plugin*.json
|
||||
*.launch
|
||||
.settings/
|
||||
*.sublime-workspace
|
||||
|
||||
# IDE - VSCode
|
||||
.vscode/*
|
||||
!.vscode/settings.json
|
||||
!.vscode/tasks.json
|
||||
!.vscode/launch.json
|
||||
!.vscode/extensions.json
|
||||
.history/*
|
||||
|
||||
# misc
|
||||
/.sass-cache
|
||||
/connect.lock
|
||||
/coverage
|
||||
/libpeerconnection.log
|
||||
npm-debug.log
|
||||
yarn-error.log
|
||||
testem.log
|
||||
/typings
|
||||
*.log
|
||||
.git
|
||||
|
||||
# System Files
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Nitro
|
||||
/build
|
||||
*.zip
|
||||
.env
|
||||
|
1
.prettierignore
Normal file
1
.prettierignore
Normal file
@ -0,0 +1 @@
|
||||
*.scss
|
@ -11,7 +11,10 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@craco/craco": "^6.3.0",
|
||||
"@nitrots/nitro-renderer": "^1.1.2",
|
||||
"@fortawesome/fontawesome-svg-core": "^1.2.36",
|
||||
"@fortawesome/free-solid-svg-icons": "^5.15.4",
|
||||
"@fortawesome/react-fontawesome": "^0.1.16",
|
||||
"@nitrots/nitro-renderer": "file:../nitro-renderer",
|
||||
"animate.css": "^4.1.1",
|
||||
"classnames": "^2.3.1",
|
||||
"node-sass": "^6.0.1",
|
||||
@ -32,6 +35,7 @@
|
||||
"@types/react-slider": "^1.3.1",
|
||||
"@types/react-transition-group": "^4.4.2",
|
||||
"@types/react-virtualized": "^9.21.13",
|
||||
"@types/styled-components": "^5.1.15",
|
||||
"@typescript-eslint/eslint-plugin": "^4.29.1"
|
||||
}
|
||||
}
|
||||
|
20
src/App.scss
20
src/App.scss
@ -17,8 +17,8 @@ $grid-active-border-color: $white;
|
||||
|
||||
$toolbar-height: 55px;
|
||||
|
||||
$achievement-width: 350px;
|
||||
$achievement-height: 370px;
|
||||
$achievement-width: 375px;
|
||||
$achievement-height: 425px;
|
||||
|
||||
$avatar-editor-width: 620px;
|
||||
$avatar-editor-height: 374px;
|
||||
@ -50,8 +50,8 @@ $chat-history-height: 300px;
|
||||
$friends-list-width: 250px;
|
||||
$friends-list-height: 300px;
|
||||
|
||||
$help-width: 275px;
|
||||
$help-height: 450px;
|
||||
$help-width: 450px;
|
||||
$help-height: 250px;
|
||||
|
||||
$nitropedia-width: 400px;
|
||||
$nitropedia-height: 400px;
|
||||
@ -59,10 +59,22 @@ $nitropedia-height: 400px;
|
||||
$messenger-width: 500px;
|
||||
$messenger-height: 370px;
|
||||
|
||||
$marketplace-post-offer-width: 430px;
|
||||
$marketplace-post-offer-height: 250px;
|
||||
|
||||
$camera-editor-width: 600px;
|
||||
$camera-editor-height: 500px;
|
||||
|
||||
$camera-checkout-width: 350px;
|
||||
|
||||
$room-info-width: 325px;
|
||||
|
||||
.nitro-app {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
@import './common';
|
||||
@import "./layout/Layout";
|
||||
@import './components';
|
||||
@import "./views/Styles";
|
||||
|
@ -2,5 +2,7 @@ import { GetNitroInstance } from './GetNitroInstance';
|
||||
|
||||
export function CreateLinkEvent(link: string): void
|
||||
{
|
||||
link = (link.startsWith('event:') ? link.substring(6) : link);
|
||||
|
||||
GetNitroInstance().createLinkEvent(link);
|
||||
}
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { IFurnitureData, IGetImageListener, NitroEvent, NitroRenderTexture, PetFigureData, RoomObjectCategory, RoomObjectVariable, RoomSessionPresentEvent, RoomWidgetEnum, TextureUtils, Vector3d } from '@nitrots/nitro-renderer';
|
||||
import { GetSessionDataManager, IsOwnerOfFurniture } from '../../..';
|
||||
import { GetRoomEngine, LocalizeText } from '../../../..';
|
||||
import { ProductTypeEnum } from '../../../../../views/catalog/common/ProductTypeEnum';
|
||||
import { ProductTypeEnum } from '../../../../../components/catalog/common/ProductTypeEnum';
|
||||
import { RoomWidgetUpdateEvent, RoomWidgetUpdatePresentDataEvent } from '../events';
|
||||
import { RoomWidgetFurniToWidgetMessage, RoomWidgetPresentOpenMessage } from '../messages';
|
||||
import { RoomWidgetMessage } from '../messages/RoomWidgetMessage';
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { NitroEvent, RoomEngineUseProductEvent, RoomObjectCategory, RoomObjectType, RoomObjectVariable, RoomSessionDanceEvent, RoomSessionUserDataUpdateEvent, RoomWidgetEnum } from '@nitrots/nitro-renderer';
|
||||
import { GetRoomEngine, GetRoomSession, GetSessionDataManager, IsOwnerOfFurniture } from '../../../..';
|
||||
import { FurniCategory } from '../../../../../views/inventory/common/FurniCategory';
|
||||
import { NitroEvent, RoomEngineUseProductEvent, RoomObjectCategory, RoomObjectType, RoomObjectVariable, RoomSessionDanceEvent, RoomSessionPetStatusUpdateEvent, RoomSessionUserDataUpdateEvent, RoomWidgetEnum } from '@nitrots/nitro-renderer';
|
||||
import { GetRoomEngine, GetSessionDataManager, IsOwnerOfFurniture } from '../../../..';
|
||||
import { FurniCategory } from '../../../../../components/inventory/common/FurniCategory';
|
||||
import { RoomWidgetAvatarInfoEvent, RoomWidgetUpdateDanceStatusEvent, RoomWidgetUpdateEvent, RoomWidgetUpdateUserDataEvent, RoomWidgetUseProductBubbleEvent, UseProductItem } from '../events';
|
||||
import { RoomWidgetAvatarExpressionMessage, RoomWidgetChangePostureMessage, RoomWidgetDanceMessage, RoomWidgetMessage, RoomWidgetRoomObjectMessage, RoomWidgetUseProductMessage, RoomWidgetUserActionMessage } from '../messages';
|
||||
import { RoomWidgetHandler } from './RoomWidgetHandler';
|
||||
@ -19,7 +19,7 @@ export class RoomWidgetAvatarInfoHandler extends RoomWidgetHandler
|
||||
|
||||
let isDancing = false;
|
||||
|
||||
const userData = GetRoomSession().userDataManager.getUserData(GetSessionDataManager().userId);
|
||||
const userData = this.container.roomSession.userDataManager.getUserData(GetSessionDataManager().userId);
|
||||
|
||||
if(userData && (userData.roomIndex === danceEvent.roomIndex)) isDancing = (danceEvent.danceId !== 0);
|
||||
|
||||
@ -30,6 +30,9 @@ export class RoomWidgetAvatarInfoHandler extends RoomWidgetHandler
|
||||
case RoomEngineUseProductEvent.USE_PRODUCT_FROM_ROOM:
|
||||
this.processUsableRoomObject((event as RoomEngineUseProductEvent).objectId);
|
||||
return;
|
||||
case RoomSessionPetStatusUpdateEvent.PET_STATUS_UPDATE:
|
||||
this.processRoomSessionPetStatusUpdateEvent((event as RoomSessionPetStatusUpdateEvent));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
@ -44,28 +47,39 @@ export class RoomWidgetAvatarInfoHandler extends RoomWidgetHandler
|
||||
case RoomWidgetRoomObjectMessage.GET_OWN_CHARACTER_INFO:
|
||||
this.processOwnCharacterInfo();
|
||||
break;
|
||||
case RoomWidgetUserActionMessage.START_NAME_CHANGE:
|
||||
// habbo help - start name change
|
||||
break;
|
||||
case RoomWidgetUserActionMessage.REQUEST_PET_UPDATE:
|
||||
break;
|
||||
case RoomWidgetUseProductMessage.PET_PRODUCT: {
|
||||
const productMessage = (message as RoomWidgetUseProductMessage);
|
||||
|
||||
this.container.roomSession.usePetProduct(productMessage.objectId, productMessage.petId);
|
||||
break;
|
||||
}
|
||||
case RoomWidgetUserActionMessage.HARVEST_PET:
|
||||
this.container.roomSession.harvestPet(userId);
|
||||
break;
|
||||
case RoomWidgetUserActionMessage.COMPOST_PLANT:
|
||||
this.container.roomSession.compostPlant(userId);
|
||||
break;
|
||||
case RoomWidgetDanceMessage.DANCE: {
|
||||
const danceMessage = (message as RoomWidgetDanceMessage);
|
||||
|
||||
GetRoomSession().sendDanceMessage(danceMessage.style);
|
||||
this.container.roomSession.sendDanceMessage(danceMessage.style);
|
||||
break;
|
||||
}
|
||||
case RoomWidgetAvatarExpressionMessage.AVATAR_EXPRESSION: {
|
||||
const expressionMessage = (message as RoomWidgetAvatarExpressionMessage);
|
||||
|
||||
GetRoomSession().sendExpressionMessage(expressionMessage.animation.ordinal)
|
||||
this.container.roomSession.sendExpressionMessage(expressionMessage.animation.ordinal)
|
||||
break;
|
||||
}
|
||||
case RoomWidgetChangePostureMessage.CHANGE_POSTURE: {
|
||||
const postureMessage = (message as RoomWidgetChangePostureMessage);
|
||||
|
||||
GetRoomSession().sendPostureMessage(postureMessage.posture);
|
||||
break;
|
||||
}
|
||||
case RoomWidgetUseProductMessage.PET_PRODUCT: {
|
||||
const productMessage = (message as RoomWidgetUseProductMessage);
|
||||
|
||||
GetRoomSession().usePetProduct(productMessage.objectId, productMessage.petId);
|
||||
this.container.roomSession.sendPostureMessage(postureMessage.posture);
|
||||
break;
|
||||
}
|
||||
}
|
||||
@ -78,9 +92,11 @@ export class RoomWidgetAvatarInfoHandler extends RoomWidgetHandler
|
||||
const userId = GetSessionDataManager().userId;
|
||||
const userName = GetSessionDataManager().userName;
|
||||
const allowNameChange = GetSessionDataManager().canChangeName;
|
||||
const userData = GetRoomSession().userDataManager.getUserData(userId);
|
||||
const userData = this.container.roomSession.userDataManager.getUserData(userId);
|
||||
|
||||
if(userData) this.container.eventDispatcher.dispatchEvent(new RoomWidgetAvatarInfoEvent(userId, userName, userData.type, userData.roomIndex, allowNameChange));
|
||||
if(!userData) return;
|
||||
|
||||
this.container.eventDispatcher.dispatchEvent(new RoomWidgetAvatarInfoEvent(userId, userName, userData.type, userData.roomIndex, allowNameChange));
|
||||
}
|
||||
|
||||
private processUsableRoomObject(objectId: number): void
|
||||
@ -151,6 +167,11 @@ export class RoomWidgetAvatarInfoHandler extends RoomWidgetHandler
|
||||
if(useProductBubbles.length) this.container.eventDispatcher.dispatchEvent(new RoomWidgetUseProductBubbleEvent(RoomWidgetUseProductBubbleEvent.USE_PRODUCT_BUBBLES, useProductBubbles));
|
||||
}
|
||||
|
||||
private processRoomSessionPetStatusUpdateEvent(event: RoomSessionPetStatusUpdateEvent): void
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public get type(): string
|
||||
{
|
||||
return RoomWidgetEnum.AVATAR_INFO;
|
||||
@ -162,18 +183,29 @@ export class RoomWidgetAvatarInfoHandler extends RoomWidgetHandler
|
||||
RoomSessionUserDataUpdateEvent.USER_DATA_UPDATED,
|
||||
RoomSessionDanceEvent.RSDE_DANCE,
|
||||
RoomEngineUseProductEvent.USE_PRODUCT_FROM_INVENTORY,
|
||||
RoomEngineUseProductEvent.USE_PRODUCT_FROM_ROOM
|
||||
RoomEngineUseProductEvent.USE_PRODUCT_FROM_ROOM,
|
||||
RoomSessionPetStatusUpdateEvent.PET_STATUS_UPDATE
|
||||
];
|
||||
}
|
||||
|
||||
// UserNameUpdateEvent.UNUE_NAME_UPDATED
|
||||
// RoomSessionNestBreedingSuccessEvent.RSPFUE_NEST_BREEDING_SUCCESS
|
||||
// RoomSessionPetLevelUpdateEvent.RSPLUE_PET_LEVEL_UPDATE
|
||||
|
||||
public get messageTypes(): string[]
|
||||
{
|
||||
return [
|
||||
RoomWidgetRoomObjectMessage.GET_OWN_CHARACTER_INFO,
|
||||
RoomWidgetUserActionMessage.START_NAME_CHANGE,
|
||||
RoomWidgetUserActionMessage.REQUEST_PET_UPDATE,
|
||||
RoomWidgetUseProductMessage.PET_PRODUCT,
|
||||
RoomWidgetUserActionMessage.REQUEST_BREED_PET,
|
||||
RoomWidgetUserActionMessage.HARVEST_PET,
|
||||
RoomWidgetUserActionMessage.REVIVE_PET,
|
||||
RoomWidgetUserActionMessage.COMPOST_PLANT,
|
||||
RoomWidgetDanceMessage.DANCE,
|
||||
RoomWidgetAvatarExpressionMessage.AVATAR_EXPRESSION,
|
||||
RoomWidgetChangePostureMessage.CHANGE_POSTURE,
|
||||
RoomWidgetUseProductMessage.PET_PRODUCT
|
||||
];
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { AvatarExpressionEnum, HabboClubLevelEnum, NitroEvent, RoomControllerLevel, RoomSessionChatEvent, RoomSettingsComposer, RoomWidgetEnum, RoomZoomEvent, TextureUtils } from '@nitrots/nitro-renderer';
|
||||
import { AvatarExpressionEnum, HabboClubLevelEnum, NitroEvent, RoomControllerLevel, RoomRotatingEffect, RoomSessionChatEvent, RoomSettingsComposer, RoomShakingEffect, RoomWidgetEnum, RoomZoomEvent, TextureUtils } from '@nitrots/nitro-renderer';
|
||||
import { GetConfiguration, GetNitroInstance } from '../../..';
|
||||
import { GetRoomEngine, GetSessionDataManager, LocalizeText } from '../../../..';
|
||||
import { FloorplanEditorEvent } from '../../../../../events/floorplan-editor/FloorplanEditorEvent';
|
||||
@ -66,6 +66,17 @@ export class RoomWidgetChatInputHandler extends RoomWidgetHandler
|
||||
|
||||
switch(firstPart.toLowerCase())
|
||||
{
|
||||
case ':shake':
|
||||
RoomShakingEffect.init(2500, 5000);
|
||||
RoomShakingEffect.turnVisualizationOn();
|
||||
|
||||
return null;
|
||||
|
||||
case ':rotate':
|
||||
RoomRotatingEffect.init(2500, 5000);
|
||||
RoomRotatingEffect.turnVisualizationOn();
|
||||
|
||||
return null;
|
||||
case ':d':
|
||||
case ';d':
|
||||
if(GetSessionDataManager().clubLevel === HabboClubLevelEnum.VIP)
|
||||
@ -110,6 +121,7 @@ export class RoomWidgetChatInputHandler extends RoomWidgetHandler
|
||||
|
||||
return null;
|
||||
case ':iddqd':
|
||||
case ':flip':
|
||||
GetRoomEngine().events.dispatchEvent(new RoomZoomEvent(this.container.roomSession.roomId, -1, true));
|
||||
|
||||
return null;
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { IFurnitureData, NitroEvent, ObjectDataFactory, PetFigureData, PetRespectComposer, PetSupplementComposer, PetType, RoomControllerLevel, RoomModerationSettings, RoomObjectCategory, RoomObjectOperationType, RoomObjectType, RoomObjectVariable, RoomSessionPetInfoUpdateEvent, RoomSessionUserBadgesEvent, RoomTradingLevelEnum, RoomUnitDropHandItemComposer, RoomUnitGiveHandItemComposer, RoomUnitGiveHandItemPetComposer, RoomUserData, RoomWidgetEnum, RoomWidgetEnumItemExtradataParameter, Vector3d } from '@nitrots/nitro-renderer';
|
||||
import { IFurnitureData, NitroEvent, ObjectDataFactory, PetFigureData, PetRespectComposer, PetSupplementComposer, PetType, RoomControllerLevel, RoomModerationSettings, RoomObjectCategory, RoomObjectOperationType, RoomObjectType, RoomObjectVariable, RoomSessionPetInfoUpdateEvent, RoomSessionUserBadgesEvent, RoomSessionUserFigureUpdateEvent, RoomTradingLevelEnum, RoomUnitDropHandItemComposer, RoomUnitGiveHandItemComposer, RoomUnitGiveHandItemPetComposer, RoomUserData, RoomWidgetEnum, RoomWidgetEnumItemExtradataParameter, Vector3d } from '@nitrots/nitro-renderer';
|
||||
import { GetNitroInstance, GetRoomEngine, GetSessionDataManager, IsOwnerOfFurniture } from '../../../..';
|
||||
import { InventoryTradeRequestEvent, WiredSelectObjectEvent } from '../../../../../events';
|
||||
import { FriendsSendFriendRequestEvent } from '../../../../../events/friends/FriendsSendFriendRequestEvent';
|
||||
@ -24,6 +24,9 @@ export class RoomWidgetInfostandHandler extends RoomWidgetHandler
|
||||
case RoomSessionUserBadgesEvent.RSUBE_BADGES:
|
||||
this.container.eventDispatcher.dispatchEvent(event);
|
||||
return;
|
||||
case RoomSessionUserFigureUpdateEvent.USER_FIGURE:
|
||||
this.processRoomSessionUserFigureUpdateEvent((event as RoomSessionUserFigureUpdateEvent));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
@ -661,6 +664,17 @@ export class RoomWidgetInfostandHandler extends RoomWidgetHandler
|
||||
this.container.eventDispatcher.dispatchEvent(infostandEvent);
|
||||
}
|
||||
|
||||
private processRoomSessionUserFigureUpdateEvent(event: RoomSessionUserFigureUpdateEvent): void
|
||||
{
|
||||
const userData = this.container.roomSession.userDataManager.getUserDataByIndex(event.userId);
|
||||
|
||||
if(!userData) return;
|
||||
|
||||
// update active infostand figure
|
||||
// update motto
|
||||
// update activity points
|
||||
}
|
||||
|
||||
private checkGuildSetting(event: RoomWidgetUpdateInfostandUserEvent): boolean
|
||||
{
|
||||
if(event.isGuildRoom) return (event.roomControllerLevel >= RoomControllerLevel.GUILD_ADMIN);
|
||||
@ -766,7 +780,8 @@ export class RoomWidgetInfostandHandler extends RoomWidgetHandler
|
||||
{
|
||||
return [
|
||||
RoomSessionPetInfoUpdateEvent.PET_INFO,
|
||||
RoomSessionUserBadgesEvent.RSUBE_BADGES
|
||||
RoomSessionUserBadgesEvent.RSUBE_BADGES,
|
||||
RoomSessionUserFigureUpdateEvent.USER_FIGURE
|
||||
];
|
||||
}
|
||||
|
||||
|
@ -1,12 +1,12 @@
|
||||
import { IFurnitureData } from '@nitrots/nitro-renderer';
|
||||
import { GetSessionDataManager } from '.';
|
||||
import { ProductTypeEnum } from '../../../views/catalog/common/ProductTypeEnum';
|
||||
import { ProductTypeEnum } from '../../../components/catalog/common/ProductTypeEnum';
|
||||
|
||||
export function GetFurnitureData(furniClassId: number, productType: string): IFurnitureData
|
||||
{
|
||||
let furniData: IFurnitureData = null;
|
||||
|
||||
switch(productType.toUpperCase())
|
||||
switch(productType.toLowerCase())
|
||||
{
|
||||
case ProductTypeEnum.FLOOR:
|
||||
furniData = GetSessionDataManager().getFloorItemData(furniClassId);
|
||||
|
16
src/api/nitro/session/IsOwnerOfFloorFurniture.ts
Normal file
16
src/api/nitro/session/IsOwnerOfFloorFurniture.ts
Normal file
@ -0,0 +1,16 @@
|
||||
import { RoomObjectCategory, RoomObjectVariable } from '@nitrots/nitro-renderer';
|
||||
import { GetRoomSession } from '.';
|
||||
import { GetRoomEngine } from '..';
|
||||
import { GetSessionDataManager } from '../../../api';
|
||||
|
||||
export function IsOwnerOfFloorFurniture(id: number): boolean
|
||||
{
|
||||
const roomObject = GetRoomEngine().getRoomObject(GetRoomSession().roomId, id, RoomObjectCategory.FLOOR);
|
||||
|
||||
if(!roomObject || !roomObject.model) return false;
|
||||
|
||||
const userId = GetSessionDataManager().userId;
|
||||
const objectOwnerId = roomObject.model.getValue<number>(RoomObjectVariable.FURNITURE_OWNER_ID);
|
||||
|
||||
return (userId === objectOwnerId);
|
||||
}
|
@ -14,6 +14,7 @@ export * from './GetSessionDataManager';
|
||||
export * from './GoToDesktop';
|
||||
export * from './HasHabboClub';
|
||||
export * from './HasHabboVip';
|
||||
export * from './IsOwnerOfFloorFurniture';
|
||||
export * from './IsOwnerOfFurniture';
|
||||
export * from './IsRidingHorse';
|
||||
export * from './StartRoomSession';
|
||||
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 11 KiB |
@ -1,9 +1,5 @@
|
||||
@import './fonts';
|
||||
@import './bootstrap/bootstrap';
|
||||
@import './fontawesome/fontawesome';
|
||||
@import './fontawesome/solid';
|
||||
@import './fontawesome/brands';
|
||||
@import './fontawesome/regular';
|
||||
@import '../node_modules/animate.css/animate.min.css';
|
||||
@import './scrollbars';
|
||||
@import './slider';
|
||||
|
@ -47,6 +47,10 @@ ul {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.cursor-not-allowed {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.pointer-events-none {
|
||||
pointer-events: none;
|
||||
}
|
||||
@ -78,3 +82,7 @@ ul {
|
||||
.z-index-1 {
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.flex-basis-fit-content {
|
||||
flex-basis: fit-content;
|
||||
}
|
||||
|
28
src/common/AutoGrid.tsx
Normal file
28
src/common/AutoGrid.tsx
Normal file
@ -0,0 +1,28 @@
|
||||
import { CSSProperties, FC, useMemo } from 'react';
|
||||
import { Grid, GridProps } from './Grid';
|
||||
|
||||
export interface AutoGridProps extends GridProps
|
||||
{
|
||||
columnMinWidth?: number;
|
||||
columnMinHeight?: number;
|
||||
}
|
||||
|
||||
export const AutoGrid: FC<AutoGridProps> = props =>
|
||||
{
|
||||
const { columnMinWidth = 40, columnMinHeight = 40, columnCount = 0, fullHeight = false, overflow = 'auto', style = {}, ...rest } = props;
|
||||
|
||||
const getStyle = useMemo(() =>
|
||||
{
|
||||
let newStyle: CSSProperties = {};
|
||||
|
||||
newStyle['--nitro-grid-column-min-height'] = (columnMinHeight + 'px');
|
||||
|
||||
if(columnCount > 1) newStyle.gridTemplateColumns = `repeat(auto-fill, minmax(${ columnMinWidth }px, 1fr))`;
|
||||
|
||||
if(Object.keys(style).length) newStyle = { ...newStyle, ...style };
|
||||
|
||||
return newStyle;
|
||||
}, [ columnMinWidth, columnMinHeight, columnCount, style ]);
|
||||
|
||||
return <Grid columnCount={ columnCount } fullHeight={ fullHeight } overflow={ overflow } style={ getStyle } { ...rest } />;
|
||||
}
|
67
src/common/Base.tsx
Normal file
67
src/common/Base.tsx
Normal file
@ -0,0 +1,67 @@
|
||||
import { CSSProperties, DetailedHTMLProps, FC, HTMLAttributes, LegacyRef, useMemo } from 'react';
|
||||
import { ColorVariantType, OverflowType, PositionType } from './types';
|
||||
|
||||
export interface BaseProps<T = HTMLElement> extends DetailedHTMLProps<HTMLAttributes<T>, T>
|
||||
{
|
||||
innerRef?: LegacyRef<T>;
|
||||
fit?: boolean;
|
||||
grow?: boolean;
|
||||
shrink?: boolean;
|
||||
fullWidth?: boolean;
|
||||
fullHeight?: boolean;
|
||||
overflow?: OverflowType;
|
||||
position?: PositionType;
|
||||
pointer?: boolean;
|
||||
textColor?: ColorVariantType;
|
||||
classNames?: string[];
|
||||
}
|
||||
|
||||
export const Base: FC<BaseProps<HTMLDivElement>> = props =>
|
||||
{
|
||||
const { ref = null, innerRef = null, fit = false, grow = false, shrink = false, fullWidth = false, fullHeight = false, overflow = null, position = null, pointer = false, textColor = null, classNames = [], className = '', style = {}, ...rest } = props;
|
||||
|
||||
const getClassNames = useMemo(() =>
|
||||
{
|
||||
const newClassNames: string[] = [];
|
||||
|
||||
if(fit || fullWidth) newClassNames.push('w-100');
|
||||
|
||||
if(fit || fullHeight) newClassNames.push('h-100');
|
||||
|
||||
if(grow) newClassNames.push('flex-grow-1');
|
||||
|
||||
if(shrink) newClassNames.push('flex-shrink-0');
|
||||
|
||||
if(overflow) newClassNames.push('overflow-' + overflow);
|
||||
|
||||
if(position) newClassNames.push('position-' + position);
|
||||
|
||||
if(pointer) newClassNames.push('cursor-pointer');
|
||||
|
||||
if(textColor) newClassNames.push('text-' + textColor);
|
||||
|
||||
if(classNames.length) newClassNames.push(...classNames);
|
||||
|
||||
return newClassNames;
|
||||
}, [ fit, grow, shrink, fullWidth, fullHeight, overflow, position, pointer, textColor, classNames ]);
|
||||
|
||||
const getClassName = useMemo(() =>
|
||||
{
|
||||
let newClassName = getClassNames.join(' ');
|
||||
|
||||
if(className.length) newClassName += (' ' + className);
|
||||
|
||||
return newClassName;
|
||||
}, [ getClassNames, className ]);
|
||||
|
||||
const getStyle = useMemo(() =>
|
||||
{
|
||||
let newStyle: CSSProperties = {};
|
||||
|
||||
if(Object.keys(style).length) newStyle = { ...newStyle, ...style };
|
||||
|
||||
return newStyle;
|
||||
}, [ style ]);
|
||||
|
||||
return <div ref={ innerRef } className={ getClassName } style={ getStyle } { ...rest } />;
|
||||
}
|
35
src/common/Button.tsx
Normal file
35
src/common/Button.tsx
Normal file
@ -0,0 +1,35 @@
|
||||
import { FC, useMemo } from 'react';
|
||||
import { Flex, FlexProps } from './Flex';
|
||||
import { ButtonSizeType, ColorVariantType } from './types';
|
||||
|
||||
export interface ButtonProps extends FlexProps
|
||||
{
|
||||
variant?: ColorVariantType;
|
||||
size?: ButtonSizeType;
|
||||
active?: boolean;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
export const Button: FC<ButtonProps> = props =>
|
||||
{
|
||||
const { variant = 'primary', size = 'sm', active = false, disabled = false, classNames = [], ...rest } = props;
|
||||
|
||||
const getClassNames = useMemo(() =>
|
||||
{
|
||||
const newClassNames: string[] = [ 'btn' ];
|
||||
|
||||
if(variant) newClassNames.push('btn-' + variant);
|
||||
|
||||
if(size) newClassNames.push('btn-' + size);
|
||||
|
||||
if(active) newClassNames.push('active');
|
||||
|
||||
if(disabled) newClassNames.push('disabled');
|
||||
|
||||
if(classNames.length) newClassNames.push(...classNames);
|
||||
|
||||
return newClassNames;
|
||||
}, [ variant, size, active, disabled, classNames ]);
|
||||
|
||||
return <Flex center classNames={ getClassNames } { ...rest } />;
|
||||
}
|
22
src/common/ButtonGroup.tsx
Normal file
22
src/common/ButtonGroup.tsx
Normal file
@ -0,0 +1,22 @@
|
||||
import { FC, useMemo } from 'react';
|
||||
import { Base, BaseProps } from './Base';
|
||||
|
||||
export interface ButtonGroupProps extends BaseProps<HTMLDivElement>
|
||||
{
|
||||
}
|
||||
|
||||
export const ButtonGroup: FC<ButtonGroupProps> = props =>
|
||||
{
|
||||
const { classNames = [], ...rest } = props;
|
||||
|
||||
const getClassNames = useMemo(() =>
|
||||
{
|
||||
const newClassNames: string[] = [ 'btn-group' ];
|
||||
|
||||
if(classNames.length) newClassNames.push(...classNames);
|
||||
|
||||
return newClassNames;
|
||||
}, [ classNames ]);
|
||||
|
||||
return <Base classNames={ getClassNames } { ...rest } />;
|
||||
}
|
36
src/common/Column.tsx
Normal file
36
src/common/Column.tsx
Normal file
@ -0,0 +1,36 @@
|
||||
import { FC, useMemo } from 'react';
|
||||
import { Flex, FlexProps } from './Flex';
|
||||
import { useGridContext } from './GridContext';
|
||||
import { ColumnSizesType } from './types';
|
||||
|
||||
export interface ColumnProps extends FlexProps
|
||||
{
|
||||
size?: ColumnSizesType;
|
||||
column?: boolean;
|
||||
}
|
||||
|
||||
export const Column: FC<ColumnProps> = props =>
|
||||
{
|
||||
const { size = 0, column = true, gap = 2, classNames = [], ...rest } = props;
|
||||
const { isCssGrid = false } = useGridContext();
|
||||
|
||||
const getClassNames = useMemo(() =>
|
||||
{
|
||||
const newClassNames: string[] = [];
|
||||
|
||||
if(size)
|
||||
{
|
||||
let colClassName = `col-${ size }`;
|
||||
|
||||
if(isCssGrid) colClassName = `g-${ colClassName }`;
|
||||
|
||||
newClassNames.push(colClassName);
|
||||
}
|
||||
|
||||
if(classNames.length) newClassNames.push(...classNames);
|
||||
|
||||
return newClassNames;
|
||||
}, [ size, isCssGrid, classNames ]);
|
||||
|
||||
return <Flex classNames={ getClassNames } column={ column } gap={ gap } { ...rest } />;
|
||||
}
|
54
src/common/Flex.tsx
Normal file
54
src/common/Flex.tsx
Normal file
@ -0,0 +1,54 @@
|
||||
import { FC, useMemo } from 'react';
|
||||
import { Base, BaseProps } from './Base';
|
||||
import { AlignItemType, AlignSelfType, JustifyContentType, SpacingType } from './types';
|
||||
|
||||
export interface FlexProps extends BaseProps<HTMLDivElement>
|
||||
{
|
||||
inline?: boolean;
|
||||
column?: boolean;
|
||||
reverse?: boolean;
|
||||
gap?: SpacingType;
|
||||
center?: boolean;
|
||||
alignSelf?: AlignSelfType;
|
||||
alignItems?: AlignItemType;
|
||||
justifyContent?: JustifyContentType;
|
||||
}
|
||||
|
||||
export const Flex: FC<FlexProps> = props =>
|
||||
{
|
||||
const { inline = false, column = undefined, reverse = false, gap = null, center = false, alignSelf = null, alignItems = null, justifyContent = null, classNames = [], ...rest } = props;
|
||||
|
||||
const getClassNames = useMemo(() =>
|
||||
{
|
||||
const newClassNames: string[] = [];
|
||||
|
||||
if(inline) newClassNames.push('d-inline-flex');
|
||||
else newClassNames.push('d-flex');
|
||||
|
||||
if(column)
|
||||
{
|
||||
if(reverse) newClassNames.push('flex-column-reverse');
|
||||
else newClassNames.push('flex-column');
|
||||
}
|
||||
else
|
||||
{
|
||||
if(reverse) newClassNames.push('flex-row-reverse');
|
||||
}
|
||||
|
||||
if(gap) newClassNames.push('gap-' + gap);
|
||||
|
||||
if(alignSelf) newClassNames.push('align-self-' + alignSelf);
|
||||
|
||||
if(alignItems) newClassNames.push('align-items-' + alignItems);
|
||||
|
||||
if(justifyContent) newClassNames.push('justify-content-' + justifyContent);
|
||||
|
||||
if(!alignItems && !justifyContent && center) newClassNames.push('align-items-center', 'justify-content-center');
|
||||
|
||||
if(classNames.length) newClassNames.push(...classNames);
|
||||
|
||||
return newClassNames;
|
||||
}, [ inline, column, reverse, gap, center, alignSelf, alignItems, justifyContent, classNames ]);
|
||||
|
||||
return <Base classNames={ getClassNames } { ...rest } />;
|
||||
}
|
22
src/common/FormGroup.tsx
Normal file
22
src/common/FormGroup.tsx
Normal file
@ -0,0 +1,22 @@
|
||||
import { FC, useMemo } from 'react';
|
||||
import { Flex, FlexProps } from './Flex';
|
||||
|
||||
export interface FormGroupProps extends FlexProps
|
||||
{
|
||||
}
|
||||
|
||||
export const FormGroup: FC<FormGroupProps> = props =>
|
||||
{
|
||||
const { classNames = [], ...rest } = props;
|
||||
|
||||
const getClassNames = useMemo(() =>
|
||||
{
|
||||
const newClassNames: string[] = [ 'form-group' ];
|
||||
|
||||
if(classNames.length) newClassNames.push(...classNames);
|
||||
|
||||
return newClassNames;
|
||||
}, [ classNames ]);
|
||||
|
||||
return <Flex classNames={ getClassNames } { ...rest } />;
|
||||
}
|
52
src/common/Grid.tsx
Normal file
52
src/common/Grid.tsx
Normal file
@ -0,0 +1,52 @@
|
||||
import { FC, useMemo } from 'react';
|
||||
import { CSSProperties } from 'styled-components';
|
||||
import { Base, BaseProps } from './Base';
|
||||
import { GridContextProvider } from './GridContext';
|
||||
import { SpacingType } from './types';
|
||||
|
||||
export interface GridProps extends BaseProps<HTMLDivElement>
|
||||
{
|
||||
inline?: boolean;
|
||||
gap?: SpacingType;
|
||||
maxContent?: boolean;
|
||||
columnCount?: number;
|
||||
}
|
||||
|
||||
export const Grid: FC<GridProps> = props =>
|
||||
{
|
||||
const { inline = false, gap = 2, maxContent = false, columnCount = 0, fullHeight = true, classNames = [], style = {}, ...rest } = props;
|
||||
|
||||
const getClassNames = useMemo(() =>
|
||||
{
|
||||
const newClassNames: string[] = [];
|
||||
|
||||
if(inline) newClassNames.push('inline-grid');
|
||||
else newClassNames.push('grid');
|
||||
|
||||
if(gap) newClassNames.push('gap-' + gap);
|
||||
else if(gap === 0) newClassNames.push('gap-0');
|
||||
|
||||
if(classNames.length) newClassNames.push(...classNames);
|
||||
|
||||
return newClassNames;
|
||||
}, [ inline, gap, classNames ]);
|
||||
|
||||
const getStyle = useMemo(() =>
|
||||
{
|
||||
let newStyle: CSSProperties = {};
|
||||
|
||||
if(columnCount) newStyle['--bs-columns'] = columnCount.toString();
|
||||
|
||||
if(maxContent) newStyle.gridTemplateRows = 'max-content';
|
||||
|
||||
if(Object.keys(style).length) newStyle = { ...newStyle, ...style };
|
||||
|
||||
return newStyle;
|
||||
}, [ columnCount, maxContent, style ]);
|
||||
|
||||
return (
|
||||
<GridContextProvider value={ { isCssGrid: true } }>
|
||||
<Base fullHeight={ fullHeight } classNames={ getClassNames } style={ getStyle } { ...rest } />
|
||||
</GridContextProvider>
|
||||
);
|
||||
}
|
17
src/common/GridContext.tsx
Normal file
17
src/common/GridContext.tsx
Normal file
@ -0,0 +1,17 @@
|
||||
import { createContext, FC, ProviderProps, useContext } from 'react';
|
||||
|
||||
export interface IGridContext
|
||||
{
|
||||
isCssGrid: boolean;
|
||||
}
|
||||
|
||||
const GridContext = createContext<IGridContext>({
|
||||
isCssGrid: false
|
||||
});
|
||||
|
||||
export const GridContextProvider: FC<ProviderProps<IGridContext>> = props =>
|
||||
{
|
||||
return <GridContext.Provider value={ props.value }>{ props.children }</GridContext.Provider>
|
||||
}
|
||||
|
||||
export const useGridContext = () => useContext(GridContext);
|
51
src/common/Text.tsx
Normal file
51
src/common/Text.tsx
Normal file
@ -0,0 +1,51 @@
|
||||
import { FC, useMemo } from 'react';
|
||||
import { Base, BaseProps } from './Base';
|
||||
import { ColorVariantType, FontSizeType, FontWeightType } from './types';
|
||||
|
||||
export interface TextProps extends BaseProps<HTMLDivElement>
|
||||
{
|
||||
variant?: ColorVariantType;
|
||||
fontWeight?: FontWeightType;
|
||||
fontSize?: FontSizeType;
|
||||
bold?: boolean;
|
||||
underline?: boolean;
|
||||
italics?: boolean;
|
||||
truncate?: boolean;
|
||||
center?: boolean;
|
||||
textEnd?: boolean;
|
||||
small?: boolean;
|
||||
}
|
||||
|
||||
export const Text: FC<TextProps> = props =>
|
||||
{
|
||||
const { variant = 'black', fontWeight = null, fontSize = 0, bold = false, underline = false, italics = false, truncate = false, center = false, textEnd = false, small = false, ...rest } = props;
|
||||
|
||||
const getClassNames = useMemo(() =>
|
||||
{
|
||||
const newClassNames: string[] = [ 'd-inline' ];
|
||||
|
||||
if(variant) newClassNames.push('text-' + variant);
|
||||
|
||||
if(bold) newClassNames.push('fw-bold');
|
||||
|
||||
if(fontWeight) newClassNames.push('fw-' + fontWeight);
|
||||
|
||||
if(fontSize) newClassNames.push('fs-' + fontSize);
|
||||
|
||||
if(underline) newClassNames.push('text-decoration-underline');
|
||||
|
||||
if(italics) newClassNames.push('fst-italic');
|
||||
|
||||
if(truncate) newClassNames.push('text-truncate');
|
||||
|
||||
if(center) newClassNames.push('text-center');
|
||||
|
||||
if(textEnd) newClassNames.push('text-end');
|
||||
|
||||
if(small) newClassNames.push('small');
|
||||
|
||||
return newClassNames;
|
||||
}, [ variant, fontWeight, fontSize, bold, underline, italics, truncate, center, textEnd, small ]);
|
||||
|
||||
return <Base classNames={ getClassNames } { ...rest } />;
|
||||
}
|
28
src/common/index.scss
Normal file
28
src/common/index.scss
Normal file
@ -0,0 +1,28 @@
|
||||
.layout-grid-item {
|
||||
height: var(--nitro-grid-column-min-height, 45px);
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
background-color: $grid-bg-color;
|
||||
|
||||
&.active {
|
||||
border-color: $grid-active-border-color !important;
|
||||
background-color: $grid-active-bg-color !important;
|
||||
}
|
||||
|
||||
&.disabled {
|
||||
cursor: not-allowed;
|
||||
|
||||
img {
|
||||
opacity: .5;
|
||||
filter: grayscale(1);
|
||||
}
|
||||
}
|
||||
|
||||
&.unseen {
|
||||
background-color: rgba($success, 0.4);
|
||||
}
|
||||
|
||||
.avatar-image {
|
||||
background-position-y: -35px;
|
||||
}
|
||||
}
|
12
src/common/index.ts
Normal file
12
src/common/index.ts
Normal file
@ -0,0 +1,12 @@
|
||||
export * from './Base';
|
||||
export * from './Button';
|
||||
export * from './ButtonGroup';
|
||||
export * from './Column';
|
||||
export * from './Flex';
|
||||
export * from './FormGroup';
|
||||
export * from './Grid';
|
||||
export * from './GridContext';
|
||||
export * from './layout';
|
||||
export * from './Text';
|
||||
export * from './types';
|
||||
export * from './utils';
|
72
src/common/layout/LayoutGridItem.tsx
Normal file
72
src/common/layout/LayoutGridItem.tsx
Normal file
@ -0,0 +1,72 @@
|
||||
import { FC, useMemo } from 'react';
|
||||
import { ItemCountView } from '../../views/shared/item-count/ItemCountView';
|
||||
import { LimitedEditionStyledNumberView } from '../../views/shared/limited-edition/LimitedEditionStyledNumberView';
|
||||
import { Base } from '../Base';
|
||||
import { Column, ColumnProps } from '../Column';
|
||||
|
||||
export interface LayoutGridItemProps extends ColumnProps
|
||||
{
|
||||
itemImage?: string;
|
||||
itemColor?: string;
|
||||
itemActive?: boolean;
|
||||
itemCount?: number;
|
||||
itemCountMinimum?: number;
|
||||
itemUniqueSoldout?: boolean;
|
||||
itemUniqueNumber?: number;
|
||||
itemUnseen?: boolean;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
export const LayoutGridItem: FC<LayoutGridItemProps> = props =>
|
||||
{
|
||||
const { itemImage = undefined, itemColor = undefined, itemActive = false, itemCount = 1, itemCountMinimum = 1, itemUniqueSoldout = false, itemUniqueNumber = -2, itemUnseen = false, disabled = false, center = true, column = true, style = {}, classNames = [], position = 'relative', overflow = 'hidden', children = null, ...rest } = props;
|
||||
|
||||
const getClassNames = useMemo(() =>
|
||||
{
|
||||
const newClassNames: string[] = [ 'layout-grid-item', 'border', 'border-2', 'border-muted', 'rounded' ];
|
||||
|
||||
if(itemActive) newClassNames.push('active');
|
||||
|
||||
if(itemUniqueSoldout || (itemUniqueNumber > 0)) newClassNames.push('unique-item');
|
||||
|
||||
if(itemUniqueSoldout) newClassNames.push('sold-out');
|
||||
|
||||
if(itemUnseen) newClassNames.push('unseen');
|
||||
|
||||
if(disabled) newClassNames.push('disabled')
|
||||
|
||||
if(itemImage === null) newClassNames.push('icon', 'loading-icon');
|
||||
|
||||
if(classNames.length) newClassNames.push(...classNames);
|
||||
|
||||
return newClassNames;
|
||||
}, [ itemActive, itemUniqueSoldout, itemUniqueNumber, itemUnseen, disabled, itemImage, classNames ]);
|
||||
|
||||
const getStyle = useMemo(() =>
|
||||
{
|
||||
let newStyle = { ...style };
|
||||
|
||||
if(itemImage) newStyle.backgroundImage = `url(${ itemImage })`;
|
||||
|
||||
if(itemColor) newStyle.backgroundColor = itemColor;
|
||||
|
||||
if(Object.keys(style).length) newStyle = { ...newStyle, ...style };
|
||||
|
||||
return newStyle;
|
||||
}, [ style, itemImage, itemColor ]);
|
||||
|
||||
return (
|
||||
<Column center={ center } pointer position={ position } overflow={ overflow } column={ column } classNames={ getClassNames } style={ getStyle } { ...rest }>
|
||||
{ (itemCount > itemCountMinimum) &&
|
||||
<ItemCountView count={ itemCount } /> }
|
||||
{ (itemUniqueNumber > 0) &&
|
||||
<>
|
||||
<Base fit className="unique-bg-override" style={ { backgroundImage: `url(${ itemImage })` } } />
|
||||
<div className="position-absolute bottom-0 unique-item-counter">
|
||||
<LimitedEditionStyledNumberView value={ itemUniqueNumber } />
|
||||
</div>
|
||||
</> }
|
||||
{ children }
|
||||
</Column>
|
||||
);
|
||||
}
|
23
src/common/layout/LayoutImage.tsx
Normal file
23
src/common/layout/LayoutImage.tsx
Normal file
@ -0,0 +1,23 @@
|
||||
import { FC, useMemo } from 'react';
|
||||
import { Base, BaseProps } from '../Base';
|
||||
|
||||
export interface LayoutImageProps extends BaseProps<HTMLDivElement>
|
||||
{
|
||||
imageUrl?: string;
|
||||
}
|
||||
|
||||
export const LayoutImage: FC<LayoutImageProps> = props =>
|
||||
{
|
||||
const { imageUrl = null, fit = true, style = null, ...rest } = props;
|
||||
|
||||
const getStyle = useMemo(() =>
|
||||
{
|
||||
const newStyle = { ...style };
|
||||
|
||||
if(imageUrl) newStyle.background = `url(${ imageUrl }) center no-repeat`;
|
||||
|
||||
return newStyle;
|
||||
}, [ style, imageUrl ]);
|
||||
|
||||
return <Base fit={ fit } style={ getStyle } { ...rest } />;
|
||||
}
|
2
src/common/layout/index.ts
Normal file
2
src/common/layout/index.ts
Normal file
@ -0,0 +1,2 @@
|
||||
export * from './LayoutGridItem';
|
||||
export * from './LayoutImage';
|
1
src/common/types/AlignItemType.ts
Normal file
1
src/common/types/AlignItemType.ts
Normal file
@ -0,0 +1 @@
|
||||
export type AlignItemType = 'start' | 'end' | 'center' | 'baseline' | 'stretch';
|
1
src/common/types/AlignSelfType.ts
Normal file
1
src/common/types/AlignSelfType.ts
Normal file
@ -0,0 +1 @@
|
||||
export type AlignSelfType = 'start' | 'end' | 'center' | 'baseline' | 'stretch';
|
1
src/common/types/ButtonSizeType.ts
Normal file
1
src/common/types/ButtonSizeType.ts
Normal file
@ -0,0 +1 @@
|
||||
export type ButtonSizeType = 'lg' | 'sm';
|
1
src/common/types/ColorVariantType.ts
Normal file
1
src/common/types/ColorVariantType.ts
Normal file
@ -0,0 +1 @@
|
||||
export type ColorVariantType = 'primary' | 'success' | 'danger' | 'secondary' | 'link' | 'black' | 'white' | 'dark' | 'warning' | 'muted';
|
1
src/common/types/ColumnSizesType.ts
Normal file
1
src/common/types/ColumnSizesType.ts
Normal file
@ -0,0 +1 @@
|
||||
export type ColumnSizesType = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12;
|
1
src/common/types/FontSizeType.ts
Normal file
1
src/common/types/FontSizeType.ts
Normal file
@ -0,0 +1 @@
|
||||
export type FontSizeType = 1 | 2 | 3 | 4 | 5;
|
1
src/common/types/FontWeightType.ts
Normal file
1
src/common/types/FontWeightType.ts
Normal file
@ -0,0 +1 @@
|
||||
export type FontWeightType = 'bold' | 'bolder' | 'normal' | 'light' | 'lighter';
|
1
src/common/types/JustifyContentType.ts
Normal file
1
src/common/types/JustifyContentType.ts
Normal file
@ -0,0 +1 @@
|
||||
export type JustifyContentType = 'start' | 'end' | 'center' | 'between' | 'around' | 'evenly';
|
1
src/common/types/OverflowType.ts
Normal file
1
src/common/types/OverflowType.ts
Normal file
@ -0,0 +1 @@
|
||||
export type OverflowType = 'hidden' | 'auto' | 'unset';
|
1
src/common/types/PositionType.ts
Normal file
1
src/common/types/PositionType.ts
Normal file
@ -0,0 +1 @@
|
||||
export type PositionType = 'static' | 'relative' | 'fixed' | 'absolute' | 'sticky';
|
1
src/common/types/SpacingType.ts
Normal file
1
src/common/types/SpacingType.ts
Normal file
@ -0,0 +1 @@
|
||||
export type SpacingType = 0 | 1 | 2 | 3 | 4 | 5;
|
11
src/common/types/index.ts
Normal file
11
src/common/types/index.ts
Normal file
@ -0,0 +1,11 @@
|
||||
export * from './AlignItemType';
|
||||
export * from './AlignSelfType';
|
||||
export * from './ButtonSizeType';
|
||||
export * from './ColorVariantType';
|
||||
export * from './ColumnSizesType';
|
||||
export * from './FontSizeType';
|
||||
export * from './FontWeightType';
|
||||
export * from './JustifyContentType';
|
||||
export * from './OverflowType';
|
||||
export * from './PositionType';
|
||||
export * from './SpacingType';
|
14
src/common/utils/CreateTransitionToIcon.ts
Normal file
14
src/common/utils/CreateTransitionToIcon.ts
Normal file
@ -0,0 +1,14 @@
|
||||
import { NitroToolbarAnimateIconEvent } from '@nitrots/nitro-renderer';
|
||||
import { GetRoomEngine } from '../../api';
|
||||
|
||||
export const CreateTransitionToIcon = (image: HTMLImageElement, fromElement: HTMLElement, icon: string) =>
|
||||
{
|
||||
const bounds = fromElement.getBoundingClientRect();
|
||||
const x = (bounds.x + (bounds.width / 2));
|
||||
const y = (bounds.y + (bounds.height / 2));
|
||||
const event = new NitroToolbarAnimateIconEvent(image, x, y);
|
||||
|
||||
event.iconName = icon;
|
||||
|
||||
GetRoomEngine().events.dispatchEvent(event);
|
||||
}
|
1
src/common/utils/index.ts
Normal file
1
src/common/utils/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from './CreateTransitionToIcon';
|
@ -3,19 +3,6 @@
|
||||
height: $achievement-height;
|
||||
}
|
||||
|
||||
.nitro-achievements-category-grid {
|
||||
--nitro-grid-column-min-width: 80px !important;
|
||||
|
||||
.grid-item {
|
||||
height: 80px;
|
||||
max-height: 80px;
|
||||
|
||||
.achievement-score {
|
||||
top: 50px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.nitro-achievements-back-arrow {
|
||||
background: url('../../assets/images/achievements/back-arrow.png') no-repeat center;
|
||||
width: 33px;
|
||||
@ -23,6 +10,6 @@
|
||||
}
|
||||
|
||||
.nitro-achievements-badge-image {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
width: 80px !important;
|
||||
height: 80px !important;
|
||||
}
|
@ -1,18 +1,21 @@
|
||||
import { AchievementData, AchievementEvent, AchievementsEvent, AchievementsScoreEvent, RequestAchievementsMessageComposer } from '@nitrots/nitro-renderer';
|
||||
import { FC, useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { LocalizeText } from '../../api';
|
||||
import { GetConfiguration, LocalizeText } from '../../api';
|
||||
import { Base } from '../../common/Base';
|
||||
import { Column } from '../../common/Column';
|
||||
import { Flex } from '../../common/Flex';
|
||||
import { Text } from '../../common/Text';
|
||||
import { AchievementsUIEvent, AchievementsUIUnseenCountEvent } from '../../events/achievements';
|
||||
import { BatchUpdates, CreateMessageHook, dispatchUiEvent, SendMessageHook } from '../../hooks';
|
||||
import { useUiEvent } from '../../hooks/events';
|
||||
import { NitroCardContentView, NitroCardHeaderView, NitroCardSubHeaderView, NitroCardView, NitroLayoutFlexColumn, NitroLayoutGrid, NitroLayoutGridColumn } from '../../layout';
|
||||
import { NitroCardContentView, NitroCardHeaderView, NitroCardSubHeaderView, NitroCardView } from '../../layout';
|
||||
import { NitroLayoutBase } from '../../layout/base';
|
||||
import { AchievementsViewProps } from './AchievementsView.types';
|
||||
import { AchievementCategory } from './common/AchievementCategory';
|
||||
import { AchievementUtilities } from './common/AchievementUtilities';
|
||||
import { AchievementsCategoryListView } from './views/category-list/AchievementsCategoryListView';
|
||||
import { AchievementCategoryView } from './views/category/AchievementCategoryView';
|
||||
|
||||
export const AchievementsView: FC<AchievementsViewProps> = props =>
|
||||
export const AchievementsView: FC<{}> = props =>
|
||||
{
|
||||
const [ isVisible, setIsVisible ] = useState(false);
|
||||
const [ isInitalized, setIsInitalized ] = useState(false);
|
||||
@ -170,6 +173,15 @@ export const AchievementsView: FC<AchievementsViewProps> = props =>
|
||||
return achievementCategories.find(existing => (existing.code === selectedCategoryCode));
|
||||
}, [ achievementCategories, selectedCategoryCode ]);
|
||||
|
||||
const getCategoryIcon = useMemo(() =>
|
||||
{
|
||||
if(!getSelectedCategory) return null;
|
||||
|
||||
const imageUrl = GetConfiguration<string>('achievements.images.url');
|
||||
|
||||
return imageUrl.replace('%image%', `achicon_${ getSelectedCategory.code }`);
|
||||
}, [ getSelectedCategory ]);
|
||||
|
||||
const setAchievementSeen = useCallback((code: string, achievementId: number) =>
|
||||
{
|
||||
const newCategories = [ ...achievementCategories ];
|
||||
@ -207,38 +219,27 @@ export const AchievementsView: FC<AchievementsViewProps> = props =>
|
||||
<NitroCardView uniqueKey="achievements" className="nitro-achievements" simple={ true }>
|
||||
<NitroCardHeaderView headerText={ LocalizeText('inventory.achievements') } onCloseClick={ event => setIsVisible(false) } />
|
||||
{ getSelectedCategory &&
|
||||
<NitroCardSubHeaderView className="justify-content-center align-items-center cursor-pointer" gap={ 3 }>
|
||||
<NitroCardSubHeaderView position="relative" className="justify-content-center align-items-center cursor-pointer" gap={ 3 }>
|
||||
<NitroLayoutBase onClick={ event => setSelectedCategoryCode(null) } className="nitro-achievements-back-arrow" />
|
||||
<NitroLayoutFlexColumn className="flex-grow-1">
|
||||
<NitroLayoutBase className="fs-4 text-black fw-bold">
|
||||
{ LocalizeText(`quests.${ getSelectedCategory.code }.name`) }
|
||||
</NitroLayoutBase>
|
||||
<NitroLayoutBase className="text-black">
|
||||
{ LocalizeText('achievements.details.categoryprogress', [ 'progress', 'limit' ], [ getSelectedCategory.getProgress().toString(), getSelectedCategory.getMaxProgress().toString() ]) }
|
||||
</NitroLayoutBase>
|
||||
</NitroLayoutFlexColumn>
|
||||
<Column grow gap={ 0 }>
|
||||
<Text fontSize={ 4 } fontWeight="bold" className="text-small">{ LocalizeText(`quests.${ getSelectedCategory.code }.name`) }</Text>
|
||||
<Text>{ LocalizeText('achievements.details.categoryprogress', [ 'progress', 'limit' ], [ getSelectedCategory.getProgress().toString(), getSelectedCategory.getMaxProgress().toString() ]) }</Text>
|
||||
</Column>
|
||||
</NitroCardSubHeaderView> }
|
||||
<NitroCardContentView>
|
||||
<NitroLayoutGrid>
|
||||
<NitroLayoutGridColumn size={ 12 }>
|
||||
{ !getSelectedCategory &&
|
||||
<>
|
||||
<AchievementsCategoryListView categories={ achievementCategories } selectedCategoryCode={ selectedCategoryCode } setSelectedCategoryCode={ setSelectedCategoryCode } />
|
||||
<NitroLayoutFlexColumn className="flex-grow-1 justify-content-end" gap={ 2 }>
|
||||
<NitroLayoutBase className="progress">
|
||||
<NitroLayoutBase className="progress-bar" style={ { width: (scaledProgressPercent + '%') }}>
|
||||
{ LocalizeText('achievements.categories.totalprogress', [ 'progress', 'limit' ], [ getProgress.toString(), getMaxProgress.toString() ]) }
|
||||
</NitroLayoutBase>
|
||||
</NitroLayoutBase>
|
||||
<NitroLayoutBase className="bg-muted text-black text-center rounded">
|
||||
{ LocalizeText('achievements.categories.score', [ 'score' ], [ achievementScore.toString() ]) }
|
||||
</NitroLayoutBase>
|
||||
</NitroLayoutFlexColumn>
|
||||
</> }
|
||||
{ getSelectedCategory &&
|
||||
<AchievementCategoryView category={ getSelectedCategory } setAchievementSeen={ setAchievementSeen } /> }
|
||||
</NitroLayoutGridColumn>
|
||||
</NitroLayoutGrid>
|
||||
{ !getSelectedCategory &&
|
||||
<>
|
||||
<AchievementsCategoryListView categories={ achievementCategories } selectedCategoryCode={ selectedCategoryCode } setSelectedCategoryCode={ setSelectedCategoryCode } />
|
||||
<Column grow justifyContent="end">
|
||||
<Base className="progress" position="relative">
|
||||
<Flex fit center position="absolute" className="text-black">{ LocalizeText('achievements.categories.totalprogress', [ 'progress', 'limit' ], [ getProgress.toString(), getMaxProgress.toString() ]) }</Flex>
|
||||
<Base className="progress-bar bg-success" style={ { width: (scaledProgressPercent + '%') }} />
|
||||
</Base>
|
||||
<Text className="bg-muted rounded p-1" center>{ LocalizeText('achievements.categories.score', [ 'score' ], [ achievementScore.toString() ]) }</Text>
|
||||
</Column>
|
||||
</> }
|
||||
{ getSelectedCategory &&
|
||||
<AchievementCategoryView category={ getSelectedCategory } setAchievementSeen={ setAchievementSeen } /> }
|
||||
</NitroCardContentView>
|
||||
</NitroCardView>
|
||||
);
|
@ -1,7 +1,14 @@
|
||||
import { AchievementData } from '@nitrots/nitro-renderer';
|
||||
import { FC } from 'react';
|
||||
import { BadgeImageView } from '../../../shared/badge-image/BadgeImageView';
|
||||
import { BaseProps } from '../../../../common/Base';
|
||||
import { BadgeImageView } from '../../../../views/shared/badge-image/BadgeImageView';
|
||||
import { AchievementUtilities } from '../../common/AchievementUtilities';
|
||||
import { AchievementBadgeViewProps } from './AchievementBadgeView.types';
|
||||
|
||||
export interface AchievementBadgeViewProps extends BaseProps<HTMLDivElement>
|
||||
{
|
||||
achievement: AchievementData;
|
||||
scale?: number;
|
||||
}
|
||||
|
||||
export const AchievementBadgeView: FC<AchievementBadgeViewProps> = props =>
|
||||
{
|
@ -0,0 +1,66 @@
|
||||
import { AchievementData } from '@nitrots/nitro-renderer';
|
||||
import { FC } from 'react';
|
||||
import { LocalizeBadgeDescription, LocalizeBadgeName, LocalizeText } from '../../../../api';
|
||||
import { Base } from '../../../../common/Base';
|
||||
import { Column } from '../../../../common/Column';
|
||||
import { Flex } from '../../../../common/Flex';
|
||||
import { Text } from '../../../../common/Text';
|
||||
import { CurrencyIcon } from '../../../../views/shared/currency-icon/CurrencyIcon';
|
||||
import { AchievementUtilities } from '../../common/AchievementUtilities';
|
||||
import { GetAchievementLevel } from '../../common/GetAchievementLevel';
|
||||
import { GetScaledProgressPercent } from '../../common/GetScaledProgressPercent';
|
||||
import { AchievementBadgeView } from '../achievement-badge/AchievementBadgeView';
|
||||
|
||||
export interface AchievementDetailsViewProps
|
||||
{
|
||||
achievement: AchievementData;
|
||||
}
|
||||
|
||||
export const AchievementDetailsView: FC<AchievementDetailsViewProps> = props =>
|
||||
{
|
||||
const { achievement = null } = props;
|
||||
|
||||
if(!achievement) return null;
|
||||
|
||||
const achievementLevel = GetAchievementLevel(achievement);
|
||||
const scaledProgressPercent = GetScaledProgressPercent(achievement);
|
||||
|
||||
return (
|
||||
<Flex shrink className="bg-muted rounded p-2 text-black" gap={ 2 } overflow="hidden">
|
||||
<Column center>
|
||||
<AchievementBadgeView className="nitro-achievements-badge-image" achievement={ achievement } scale={ 2 } />
|
||||
<Text fontWeight="bold">
|
||||
{ LocalizeText('achievements.details.level', [ 'level', 'limit' ], [ achievementLevel.toString(), achievement.levelCount.toString() ]) }
|
||||
</Text>
|
||||
</Column>
|
||||
<Column fullWidth justifyContent="center" overflow="hidden">
|
||||
<Column gap={ 1 }>
|
||||
<Text fontWeight="bold" truncate>
|
||||
{ LocalizeBadgeName(AchievementUtilities.getBadgeCode(achievement)) }
|
||||
</Text>
|
||||
<Text truncate>
|
||||
{ LocalizeBadgeDescription(AchievementUtilities.getBadgeCode(achievement)) }
|
||||
</Text>
|
||||
</Column>
|
||||
{ ((achievement.levelRewardPoints > 0) || (achievement.scoreLimit > 0)) &&
|
||||
<Column gap={ 1 }>
|
||||
{ (achievement.levelRewardPoints > 0) &&
|
||||
<Flex alignItems="center" gap={ 1 }>
|
||||
<Text truncate className="small">
|
||||
{ LocalizeText('achievements.details.reward') }
|
||||
</Text>
|
||||
<Flex center className="fw-bold small" gap={ 1 }>
|
||||
{ achievement.levelRewardPoints }
|
||||
<CurrencyIcon type={ achievement.levelRewardPointType } />
|
||||
</Flex>
|
||||
</Flex> }
|
||||
{ (achievement.scoreLimit > 0) &&
|
||||
<Base className="progress" position="relative">
|
||||
<Flex fit center position="absolute" className="text-black"> { LocalizeText('achievements.details.progress', [ 'progress', 'limit' ], [ (achievement.currentPoints + achievement.scoreAtStartOfLevel).toString(), (achievement.scoreLimit + achievement.scoreAtStartOfLevel).toString() ]) }</Flex>
|
||||
<Base className="progress-bar" style={ { width: (scaledProgressPercent + '%') }} />
|
||||
</Base> }
|
||||
</Column> }
|
||||
</Column>
|
||||
</Flex>
|
||||
)
|
||||
}
|
@ -0,0 +1,23 @@
|
||||
import { AchievementData } from '@nitrots/nitro-renderer';
|
||||
import { FC } from 'react';
|
||||
import { LayoutGridItem, LayoutGridItemProps } from '../../../../common/layout/LayoutGridItem';
|
||||
import { AchievementBadgeView } from '../achievement-badge/AchievementBadgeView';
|
||||
|
||||
export interface AchievementListItemViewProps extends LayoutGridItemProps
|
||||
{
|
||||
achievement: AchievementData;
|
||||
}
|
||||
|
||||
export const AchievementListItemView: FC<AchievementListItemViewProps> = props =>
|
||||
{
|
||||
const { achievement = null, children = null, ...rest } = props;
|
||||
|
||||
if(!achievement) return null;
|
||||
|
||||
return (
|
||||
<LayoutGridItem itemCount={ achievement.unseen } itemCountMinimum={ 0 } { ...rest }>
|
||||
<AchievementBadgeView achievement={ achievement } />
|
||||
{ children }
|
||||
</LayoutGridItem>
|
||||
);
|
||||
}
|
@ -0,0 +1,26 @@
|
||||
import { AchievementData } from '@nitrots/nitro-renderer';
|
||||
import { Dispatch, FC, SetStateAction } from 'react';
|
||||
import { AutoGrid } from '../../../../common/AutoGrid';
|
||||
import { AchievementListItemView } from './AchievementListItemView';
|
||||
|
||||
export interface AchievementListViewProps
|
||||
{
|
||||
achievements: AchievementData[];
|
||||
selectedAchievementId: number;
|
||||
setSelectedAchievementId: Dispatch<SetStateAction<number>>;
|
||||
}
|
||||
|
||||
export const AchievementListView: FC<AchievementListViewProps> = props =>
|
||||
{
|
||||
const { achievements = null, selectedAchievementId = 0, setSelectedAchievementId = null, children = null } = props;
|
||||
|
||||
return (
|
||||
<AutoGrid columnCount={ 6 } columnMinWidth={ 50 } columnMinHeight={ 50 }>
|
||||
{ achievements && (achievements.length > 0) && achievements.map((achievement, index) =>
|
||||
{
|
||||
return <AchievementListItemView key={ index } achievement={ achievement } itemActive={ (selectedAchievementId === achievement.achievementId) } onClick={ event => setSelectedAchievementId(achievement.achievementId) } />;
|
||||
}) }
|
||||
{ children }
|
||||
</AutoGrid>
|
||||
);
|
||||
}
|
@ -0,0 +1,44 @@
|
||||
import { FC, useCallback, useMemo } from 'react';
|
||||
import { GetConfiguration, LocalizeText } from '../../../../api';
|
||||
import { LayoutGridItem, LayoutGridItemProps } from '../../../../common/layout/LayoutGridItem';
|
||||
import { LayoutImage } from '../../../../common/layout/LayoutImage';
|
||||
import { Text } from '../../../../common/Text';
|
||||
import { AchievementCategory } from '../../common/AchievementCategory';
|
||||
|
||||
export interface AchievementCategoryListItemViewProps extends LayoutGridItemProps
|
||||
{
|
||||
category: AchievementCategory;
|
||||
}
|
||||
|
||||
export const AchievementsCategoryListItemView: FC<AchievementCategoryListItemViewProps> = props =>
|
||||
{
|
||||
const { category = null, ...rest } = props;
|
||||
|
||||
const progress = category.getProgress();
|
||||
const maxProgress = category.getMaxProgress();
|
||||
|
||||
const getCategoryImage = useMemo(() =>
|
||||
{
|
||||
const imageUrl = GetConfiguration<string>('achievements.images.url');
|
||||
|
||||
return imageUrl.replace('%image%', `achcategory_${ category.code }_${ ((progress > 0) ? 'active' : 'inactive') }`);
|
||||
}, [ category, progress ]);
|
||||
|
||||
const getTotalUnseen = useCallback(() =>
|
||||
{
|
||||
let unseen = 0;
|
||||
|
||||
for(const achievement of category.achievements) unseen += achievement.unseen;
|
||||
|
||||
return unseen;
|
||||
}, [ category ]);
|
||||
|
||||
return (
|
||||
<LayoutGridItem itemCount={ getTotalUnseen() } itemCountMinimum={ 0 } gap={ 1 } { ...rest }>
|
||||
<Text fullWidth center className="small pt-1">{ LocalizeText(`quests.${ category.code }.name`) }</Text>
|
||||
<LayoutImage position="relative" imageUrl={ getCategoryImage }>
|
||||
<Text fullWidth center position="absolute" variant="white" style={ { fontSize: 12, bottom: 9 } }>{ progress } / { maxProgress }</Text>
|
||||
</LayoutImage>
|
||||
</LayoutGridItem>
|
||||
);
|
||||
}
|
@ -0,0 +1,23 @@
|
||||
import { Dispatch, FC, SetStateAction } from 'react';
|
||||
import { AutoGrid } from '../../../../common/AutoGrid';
|
||||
import { AchievementCategory } from '../../common/AchievementCategory';
|
||||
import { AchievementsCategoryListItemView } from './AchievementsCategoryListItemView';
|
||||
|
||||
export interface AchievementsCategoryListViewProps
|
||||
{
|
||||
categories: AchievementCategory[];
|
||||
selectedCategoryCode: string;
|
||||
setSelectedCategoryCode: Dispatch<SetStateAction<string>>;
|
||||
}
|
||||
|
||||
export const AchievementsCategoryListView: FC<AchievementsCategoryListViewProps> = props =>
|
||||
{
|
||||
const { categories = null, selectedCategoryCode = null, setSelectedCategoryCode = null, children = null } = props;
|
||||
|
||||
return (
|
||||
<AutoGrid columnMinWidth={ 90 } columnMinHeight={ 100 }>
|
||||
{ categories && (categories.length > 0) && categories.map((category, index) => <AchievementsCategoryListItemView key={ index } category={ category } itemActive={ (selectedCategoryCode === category.code) } onClick={ event => setSelectedCategoryCode(category.code) } /> ) }
|
||||
{ children }
|
||||
</AutoGrid>
|
||||
);
|
||||
};
|
@ -1,8 +1,14 @@
|
||||
import { FC, useEffect, useMemo, useState } from 'react';
|
||||
import { NitroLayoutFlexColumn } from '../../../../layout';
|
||||
import { Column } from '../../../../common/Column';
|
||||
import { AchievementCategory } from '../../common/AchievementCategory';
|
||||
import { AchievementDetailsView } from '../achievement-details/AchievementDetailsView';
|
||||
import { AchievementListView } from '../achievement-list/AchievementListView';
|
||||
import { AchievementCategoryViewProps } from './AchievementCategoryView.types';
|
||||
|
||||
export class AchievementCategoryViewProps
|
||||
{
|
||||
category: AchievementCategory;
|
||||
setAchievementSeen: (code: string, achievementId: number) => void;
|
||||
}
|
||||
|
||||
export const AchievementCategoryView: FC<AchievementCategoryViewProps> = props =>
|
||||
{
|
||||
@ -42,10 +48,10 @@ export const AchievementCategoryView: FC<AchievementCategoryViewProps> = props =
|
||||
if(!category) return null;
|
||||
|
||||
return (
|
||||
<NitroLayoutFlexColumn className="justify-content-between h-100" gap={ 2 }>
|
||||
<Column fullHeight justifyContent="between">
|
||||
<AchievementListView achievements={ category.achievements } selectedAchievementId={ selectedAchievementId } setSelectedAchievementId={ setSelectedAchievementId } />
|
||||
{ getSelectedAchievement &&
|
||||
<AchievementDetailsView achievement={ getSelectedAchievement } /> }
|
||||
</NitroLayoutFlexColumn>
|
||||
</Column>
|
||||
);
|
||||
}
|
@ -192,7 +192,7 @@
|
||||
}
|
||||
|
||||
|
||||
&.spotlight {
|
||||
&.spotlight-icon {
|
||||
width: 130px;
|
||||
height: 305px;
|
||||
background-position: -5px -5px;
|
||||
@ -287,71 +287,3 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.nitro-wardrobe-grid {
|
||||
--nitro-grid-column-min-width: 80px !important;
|
||||
|
||||
.grid-item {
|
||||
height: 140px;
|
||||
max-height: 140px;
|
||||
background-color: $ghost;
|
||||
|
||||
&:after {
|
||||
position: absolute;
|
||||
content: '';
|
||||
top: 75%;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
border-radius: 50%;
|
||||
background-color: $gray-chateau;
|
||||
box-shadow: 0 0 8px 2px rgba($white,.6);
|
||||
transform: scale(2);
|
||||
}
|
||||
|
||||
.avatar-image {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
background-position-y: -23px !important;
|
||||
z-index: 4;
|
||||
}
|
||||
|
||||
.figure-button-container {
|
||||
background-color: $gray-chateau;
|
||||
z-index: 3;
|
||||
}
|
||||
}
|
||||
|
||||
.grid-item-container {
|
||||
height: 142px !important;
|
||||
max-height: 142px !important;
|
||||
|
||||
.grid-item {
|
||||
background-color: $ghost;
|
||||
|
||||
.avatar-image {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
background-position-y: -23px !important;
|
||||
z-index: 3;
|
||||
}
|
||||
|
||||
.figure-button-container {
|
||||
background-color: $gray-chateau;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
&:after {
|
||||
position: absolute;
|
||||
content: '';
|
||||
height: 50%;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
background-color: $gray-chateau;
|
||||
box-shadow: 0 0 8px 2px rgba($white,.6);
|
||||
z-index: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,11 +1,15 @@
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { AvatarEditorFigureCategory, FigureSetIdsMessageEvent, GetWardrobeMessageComposer, IAvatarFigureContainer, UserFigureComposer, UserWardrobePageEvent } from '@nitrots/nitro-renderer';
|
||||
import { FC, useCallback, useEffect, useState } from 'react';
|
||||
import { GetAvatarRenderManager, GetClubMemberLevel, GetSessionDataManager, LocalizeText } from '../../api';
|
||||
import { Button } from '../../common/Button';
|
||||
import { ButtonGroup } from '../../common/ButtonGroup';
|
||||
import { Column } from '../../common/Column';
|
||||
import { Grid } from '../../common/Grid';
|
||||
import { AvatarEditorEvent } from '../../events/avatar-editor';
|
||||
import { CreateMessageHook, SendMessageHook } from '../../hooks';
|
||||
import { useUiEvent } from '../../hooks/events/ui/ui-event';
|
||||
import { NitroCardContentView, NitroCardHeaderView, NitroCardTabsItemView, NitroCardTabsView, NitroCardView, NitroLayoutGrid, NitroLayoutGridColumn } from '../../layout';
|
||||
import { AvatarEditorViewProps } from './AvatarEditorView.types';
|
||||
import { NitroCardContentView, NitroCardHeaderView, NitroCardTabsItemView, NitroCardTabsView, NitroCardView } from '../../layout';
|
||||
import { AvatarEditorAction } from './common/AvatarEditorAction';
|
||||
import { AvatarEditorUtilities } from './common/AvatarEditorUtilities';
|
||||
import { BodyModel } from './common/BodyModel';
|
||||
@ -15,7 +19,6 @@ import { HeadModel } from './common/HeadModel';
|
||||
import { IAvatarEditorCategoryModel } from './common/IAvatarEditorCategoryModel';
|
||||
import { LegModel } from './common/LegModel';
|
||||
import { TorsoModel } from './common/TorsoModel';
|
||||
import { AvatarEditorFigureActionsView } from './views/figure-actions/AvatarEditorFigureActionsView';
|
||||
import { AvatarEditorFigurePreviewView } from './views/figure-preview/AvatarEditorFigurePreviewView';
|
||||
import { AvatarEditorModelView } from './views/model/AvatarEditorModelView';
|
||||
import { AvatarEditorWardrobeView } from './views/wardrobe/AvatarEditorWardrobeView';
|
||||
@ -23,12 +26,8 @@ import { AvatarEditorWardrobeView } from './views/wardrobe/AvatarEditorWardrobeV
|
||||
const DEFAULT_MALE_FIGURE: string = 'hr-100.hd-180-7.ch-215-66.lg-270-79.sh-305-62.ha-1002-70.wa-2007';
|
||||
const DEFAULT_FEMALE_FIGURE: string = 'hr-515-33.hd-600-1.ch-635-70.lg-716-66-62.sh-735-68';
|
||||
const MAX_SAVED_FIGURES: number = 10;
|
||||
const ACTION_CLEAR = 'action_clear';
|
||||
const ACTION_RESET = 'action_reset';
|
||||
const ACTION_RANDOMIZE = 'action_randomize';
|
||||
const ACTION_SAVE = 'action_save';
|
||||
|
||||
export const AvatarEditorView: FC<AvatarEditorViewProps> = props =>
|
||||
export const AvatarEditorView: FC<{}> = props =>
|
||||
{
|
||||
const [ isVisible, setIsVisible ] = useState(false);
|
||||
const [ figures, setFigures ] = useState<Map<string, FigureData>>(null);
|
||||
@ -285,18 +284,33 @@ export const AvatarEditorView: FC<AvatarEditorViewProps> = props =>
|
||||
</NitroCardTabsItemView>
|
||||
</NitroCardTabsView>
|
||||
<NitroCardContentView>
|
||||
<NitroLayoutGrid>
|
||||
<NitroLayoutGridColumn size={ 9 }>
|
||||
<Grid>
|
||||
<Column size={ 9 } overflow="hidden">
|
||||
{ (activeCategory && !isWardrobeVisible) &&
|
||||
<AvatarEditorModelView model={ activeCategory } gender={ figureData.gender } setGender={ setGender } /> }
|
||||
{ isWardrobeVisible &&
|
||||
<AvatarEditorWardrobeView figureData={ figureData } savedFigures={ savedFigures } setSavedFigures={ setSavedFigures } loadAvatarInEditor={ loadAvatarInEditor } /> }
|
||||
</NitroLayoutGridColumn>
|
||||
<NitroLayoutGridColumn overflow="hidden" size={ 3 }>
|
||||
</Column>
|
||||
<Column size={ 3 } overflow="hidden">
|
||||
<AvatarEditorFigurePreviewView figureData={ figureData } />
|
||||
<AvatarEditorFigureActionsView processAction={ processAction } />
|
||||
</NitroLayoutGridColumn>
|
||||
</NitroLayoutGrid>
|
||||
<Column grow gap={ 2 }>
|
||||
<ButtonGroup>
|
||||
<Button variant="secondary" size="sm" onClick={ event => processAction(AvatarEditorAction.ACTION_RESET) }>
|
||||
<FontAwesomeIcon icon="undo" />
|
||||
</Button>
|
||||
<Button variant="secondary" size="sm" onClick={ event => processAction(AvatarEditorAction.ACTION_CLEAR) }>
|
||||
<FontAwesomeIcon icon="trash" />
|
||||
</Button>
|
||||
<Button variant="secondary" size="sm" onClick={ event => processAction(AvatarEditorAction.ACTION_RANDOMIZE) }>
|
||||
<FontAwesomeIcon icon="dice" />
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
<Button className="w-100" variant="success" size="sm" onClick={ event => processAction(AvatarEditorAction.ACTION_SAVE) }>
|
||||
{ LocalizeText('avatareditor.save') }
|
||||
</Button>
|
||||
</Column>
|
||||
</Column>
|
||||
</Grid>
|
||||
</NitroCardContentView>
|
||||
</NitroCardView>
|
||||
);
|
30
src/components/avatar-editor/views/AvatarEditorIcon.tsx
Normal file
30
src/components/avatar-editor/views/AvatarEditorIcon.tsx
Normal file
@ -0,0 +1,30 @@
|
||||
import { FC, useMemo } from 'react';
|
||||
import { Base, BaseProps } from '../../../common/Base';
|
||||
|
||||
type AvatarIconType = 'male' | 'female' | 'clear' | 'sellable' | string;
|
||||
|
||||
export interface AvatarEditorIconProps extends BaseProps<HTMLDivElement>
|
||||
{
|
||||
icon: AvatarIconType;
|
||||
selected?: boolean;
|
||||
}
|
||||
|
||||
export const AvatarEditorIcon: FC<AvatarEditorIconProps> = props =>
|
||||
{
|
||||
const { icon = null, selected = false, classNames = [], children = null, ...rest } = props;
|
||||
|
||||
const getClassNames = useMemo(() =>
|
||||
{
|
||||
const newClassNames: string[] = [ 'nitro-avatar-editor-spritesheet' ];
|
||||
|
||||
if(icon && icon.length) newClassNames.push(icon + '-icon');
|
||||
|
||||
if(selected) newClassNames.push('selected');
|
||||
|
||||
if(classNames.length) newClassNames.push(...classNames);
|
||||
|
||||
return newClassNames;
|
||||
}, [ icon, selected, classNames ]);
|
||||
|
||||
return <Base classNames={ getClassNames } { ...rest } />
|
||||
}
|
@ -1,9 +1,15 @@
|
||||
import { AvatarDirectionAngle } from '@nitrots/nitro-renderer';
|
||||
import { FC, useCallback, useEffect, useState } from 'react';
|
||||
import { NitroLayoutFlexColumn } from '../../../../layout';
|
||||
import { NitroLayoutBase } from '../../../../layout/base';
|
||||
import { AvatarImageView } from '../../../shared/avatar-image/AvatarImageView';
|
||||
import { AvatarEditorFigurePreviewViewProps } from './AvatarEditorFigurePreviewView.types';
|
||||
import { Base } from '../../../../common/Base';
|
||||
import { Column } from '../../../../common/Column';
|
||||
import { AvatarImageView } from '../../../../views/shared/avatar-image/AvatarImageView';
|
||||
import { FigureData } from '../../common/FigureData';
|
||||
import { AvatarEditorIcon } from '../AvatarEditorIcon';
|
||||
|
||||
export interface AvatarEditorFigurePreviewViewProps
|
||||
{
|
||||
figureData: FigureData;
|
||||
}
|
||||
|
||||
export const AvatarEditorFigurePreviewView: FC<AvatarEditorFigurePreviewViewProps> = props =>
|
||||
{
|
||||
@ -43,14 +49,14 @@ export const AvatarEditorFigurePreviewView: FC<AvatarEditorFigurePreviewViewProp
|
||||
}, [ figureData, rerender ] );
|
||||
|
||||
return (
|
||||
<NitroLayoutFlexColumn className="figure-preview-container" overflow="hidden" position="relative">
|
||||
<Column className="figure-preview-container" overflow="hidden" position="relative">
|
||||
<AvatarImageView figure={ figureData.getFigureString() } direction={ figureData.direction } scale={ 2 } />
|
||||
<NitroLayoutBase className="nitro-avatar-editor-spritesheet spotlight" />
|
||||
<NitroLayoutBase className="avatar-shadow" />
|
||||
<NitroLayoutBase className="arrow-container">
|
||||
<div className="nitro-avatar-editor-spritesheet arrow-left-icon" onClick={ event => rotateFigure(figureData.direction + 1) } />
|
||||
<div className="nitro-avatar-editor-spritesheet arrow-right-icon" onClick={ event => rotateFigure(figureData.direction - 1) } />
|
||||
</NitroLayoutBase>
|
||||
</NitroLayoutFlexColumn>
|
||||
<AvatarEditorIcon icon="spotlight" />
|
||||
<Base className="avatar-shadow" />
|
||||
<Base className="arrow-container">
|
||||
<AvatarEditorIcon pointer icon="arrow-left" onClick={ event => rotateFigure(figureData.direction + 1) } />
|
||||
<AvatarEditorIcon pointer icon="arrow-right" onClick={ event => rotateFigure(figureData.direction - 1) } />
|
||||
</Base>
|
||||
</Column>
|
||||
);
|
||||
}
|
@ -0,0 +1,40 @@
|
||||
import { FC, useCallback, useEffect, useState } from 'react';
|
||||
import { LayoutGridItem, LayoutGridItemProps } from '../../../../common/layout/LayoutGridItem';
|
||||
import { CurrencyIcon } from '../../../../views/shared/currency-icon/CurrencyIcon';
|
||||
import { AvatarEditorGridPartItem } from '../../common/AvatarEditorGridPartItem';
|
||||
import { AvatarEditorIcon } from '../AvatarEditorIcon';
|
||||
|
||||
export interface AvatarEditorFigureSetItemViewProps extends LayoutGridItemProps
|
||||
{
|
||||
partItem: AvatarEditorGridPartItem;
|
||||
}
|
||||
|
||||
export const AvatarEditorFigureSetItemView: FC<AvatarEditorFigureSetItemViewProps> = props =>
|
||||
{
|
||||
const { partItem = null, children = null, ...rest } = props;
|
||||
const [ updateId, setUpdateId ] = useState(-1);
|
||||
|
||||
const rerender = useCallback(() =>
|
||||
{
|
||||
setUpdateId(prevValue => (prevValue + 1));
|
||||
}, []);
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
partItem.notify = rerender;
|
||||
|
||||
return () =>
|
||||
{
|
||||
partItem.notify = null;
|
||||
}
|
||||
})
|
||||
|
||||
return (
|
||||
<LayoutGridItem itemImage={ (partItem.isClear ? undefined : partItem.imageUrl) } itemActive={ partItem.isSelected } { ...rest }>
|
||||
{ partItem.isHC && <CurrencyIcon className="position-absolute end-1 bottom-1" type={ 'hc' } /> }
|
||||
{ partItem.isClear && <AvatarEditorIcon icon="clear" /> }
|
||||
{ partItem.isSellable && <AvatarEditorIcon icon="sellable" position="absolute" className="end-1 bottom-1" /> }
|
||||
{ children }
|
||||
</LayoutGridItem>
|
||||
);
|
||||
}
|
@ -0,0 +1,38 @@
|
||||
import { Dispatch, FC, SetStateAction, useCallback } from 'react';
|
||||
import { AutoGrid } from '../../../../common/AutoGrid';
|
||||
import { AvatarEditorGridPartItem } from '../../common/AvatarEditorGridPartItem';
|
||||
import { CategoryData } from '../../common/CategoryData';
|
||||
import { IAvatarEditorCategoryModel } from '../../common/IAvatarEditorCategoryModel';
|
||||
import { AvatarEditorFigureSetItemView } from './AvatarEditorFigureSetItemView';
|
||||
|
||||
export interface AvatarEditorFigureSetViewProps
|
||||
{
|
||||
model: IAvatarEditorCategoryModel;
|
||||
category: CategoryData;
|
||||
setMaxPaletteCount: Dispatch<SetStateAction<number>>;
|
||||
}
|
||||
|
||||
export const AvatarEditorFigureSetView: FC<AvatarEditorFigureSetViewProps> = props =>
|
||||
{
|
||||
const { model = null, category = null, setMaxPaletteCount = null } = props;
|
||||
|
||||
const selectPart = useCallback((item: AvatarEditorGridPartItem) =>
|
||||
{
|
||||
const index = category.parts.indexOf(item);
|
||||
|
||||
if(index === -1) return;
|
||||
|
||||
model.selectPart(category.name, index);
|
||||
|
||||
const partItem = category.getCurrentPart();
|
||||
|
||||
setMaxPaletteCount(partItem.maxColorIndex || 1);
|
||||
}, [ model, category, setMaxPaletteCount ]);
|
||||
|
||||
return (
|
||||
<AutoGrid grow columnCount={ 3 } columnMinHeight={ 50 } overflow="auto">
|
||||
{ (category.parts.length > 0) && category.parts.map((item, index) =>
|
||||
<AvatarEditorFigureSetItemView key={ index } partItem={ item } onClick={ event => selectPart(item) } />) }
|
||||
</AutoGrid>
|
||||
);
|
||||
}
|
@ -1,10 +1,19 @@
|
||||
import { FC, useCallback, useEffect, useState } from 'react';
|
||||
import { NitroLayoutFlex, NitroLayoutGrid, NitroLayoutGridColumn } from '../../../../layout';
|
||||
import { Dispatch, FC, SetStateAction, useCallback, useEffect, useState } from 'react';
|
||||
import { Column } from '../../../../common/Column';
|
||||
import { Flex } from '../../../../common/Flex';
|
||||
import { Grid } from '../../../../common/Grid';
|
||||
import { CategoryData } from '../../common/CategoryData';
|
||||
import { FigureData } from '../../common/FigureData';
|
||||
import { IAvatarEditorCategoryModel } from '../../common/IAvatarEditorCategoryModel';
|
||||
import { AvatarEditorIcon } from '../AvatarEditorIcon';
|
||||
import { AvatarEditorFigureSetView } from '../figure-set/AvatarEditorFigureSetView';
|
||||
import { AvatarEditorPaletteSetView } from '../palette-set/AvatarEditorPaletteSetView';
|
||||
import { AvatarEditorModelViewProps } from './AvatarEditorModelView.types';
|
||||
export interface AvatarEditorModelViewProps
|
||||
{
|
||||
model: IAvatarEditorCategoryModel;
|
||||
gender: string;
|
||||
setGender: Dispatch<SetStateAction<string>>;
|
||||
}
|
||||
|
||||
export const AvatarEditorModelView: FC<AvatarEditorModelViewProps> = props =>
|
||||
{
|
||||
@ -47,37 +56,37 @@ export const AvatarEditorModelView: FC<AvatarEditorModelViewProps> = props =>
|
||||
if(!model || !activeCategory) return null;
|
||||
|
||||
return (
|
||||
<NitroLayoutGrid>
|
||||
<NitroLayoutGridColumn size={ 2 }>
|
||||
<Grid>
|
||||
<Column size={ 2 }>
|
||||
{ model.canSetGender &&
|
||||
<>
|
||||
<NitroLayoutFlex className="justify-content-center align-items-center category-item cursor-pointer" onClick={ event => setGender(FigureData.MALE) }>
|
||||
<div className={ `nitro-avatar-editor-spritesheet male-icon ${ (gender === FigureData.MALE) ? ' selected' : ''}` } />
|
||||
</NitroLayoutFlex>
|
||||
<NitroLayoutFlex className="justify-content-center align-items-center category-item cursor-pointer" onClick={ event => setGender(FigureData.FEMALE) }>
|
||||
<div className={ `nitro-avatar-editor-spritesheet female-icon ${ (gender === FigureData.FEMALE) ? ' selected' : ''}` } />
|
||||
</NitroLayoutFlex>
|
||||
<Flex center pointer className="category-item" onClick={ event => setGender(FigureData.MALE) }>
|
||||
<AvatarEditorIcon icon="male" selected={ (gender === FigureData.MALE) } />
|
||||
</Flex>
|
||||
<Flex center pointer className="category-item" onClick={ event => setGender(FigureData.FEMALE) }>
|
||||
<AvatarEditorIcon icon="female" selected={ (gender === FigureData.FEMALE) } />
|
||||
</Flex>
|
||||
</> }
|
||||
{ !model.canSetGender && model.categories && (model.categories.size > 0) && Array.from(model.categories.keys()).map(name =>
|
||||
{
|
||||
const category = model.categories.get(name);
|
||||
|
||||
return (
|
||||
<NitroLayoutFlex key={ name } className="justify-content-center align-items-center category-item cursor-pointer" onClick={ event => selectCategory(name) }>
|
||||
<div className={ `nitro-avatar-editor-spritesheet ${ category.name }-icon ${ (activeCategory === category) ? ' selected' : ''}` } />
|
||||
</NitroLayoutFlex>
|
||||
<Flex center pointer key={ name } className="category-item" onClick={ event => selectCategory(name) }>
|
||||
<AvatarEditorIcon icon={ category.name } selected={ (activeCategory === category) } />
|
||||
</Flex>
|
||||
);
|
||||
})}
|
||||
</NitroLayoutGridColumn>
|
||||
<NitroLayoutGridColumn size={ 5 }>
|
||||
</Column>
|
||||
<Column size={ 5 } overflow="hidden">
|
||||
<AvatarEditorFigureSetView model={ model } category={ activeCategory } setMaxPaletteCount={ setMaxPaletteCount } />
|
||||
</NitroLayoutGridColumn>
|
||||
<NitroLayoutGridColumn size={ 5 }>
|
||||
</Column>
|
||||
<Column size={ 5 } overflow="hidden">
|
||||
{ (maxPaletteCount >= 1) &&
|
||||
<AvatarEditorPaletteSetView model={ model } category={ activeCategory } paletteSet={ activeCategory.getPalette(0) } paletteIndex={ 0 } /> }
|
||||
{ (maxPaletteCount === 2) &&
|
||||
<AvatarEditorPaletteSetView model={ model } category={ activeCategory } paletteSet={ activeCategory.getPalette(1) } paletteIndex={ 1 } className="mt-1" /> }
|
||||
</NitroLayoutGridColumn>
|
||||
</NitroLayoutGrid>
|
||||
<AvatarEditorPaletteSetView model={ model } category={ activeCategory } paletteSet={ activeCategory.getPalette(1) } paletteIndex={ 1 } /> }
|
||||
</Column>
|
||||
</Grid>
|
||||
);
|
||||
}
|
@ -0,0 +1,34 @@
|
||||
import { FC, useCallback, useEffect, useState } from 'react';
|
||||
import { LayoutGridItem, LayoutGridItemProps } from '../../../../common/layout/LayoutGridItem';
|
||||
import { CurrencyIcon } from '../../../../views/shared/currency-icon/CurrencyIcon';
|
||||
import { AvatarEditorGridColorItem } from '../../common/AvatarEditorGridColorItem';
|
||||
|
||||
export interface AvatarEditorPaletteSetItemProps extends LayoutGridItemProps
|
||||
{
|
||||
colorItem: AvatarEditorGridColorItem;
|
||||
}
|
||||
|
||||
export const AvatarEditorPaletteSetItem: FC<AvatarEditorPaletteSetItemProps> = props =>
|
||||
{
|
||||
const { colorItem = null, children = null, ...rest } = props;
|
||||
const [ updateId, setUpdateId ] = useState(-1);
|
||||
|
||||
const rerender = useCallback(() =>
|
||||
{
|
||||
setUpdateId(prevValue => (prevValue + 1));
|
||||
}, []);
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
colorItem.notify = rerender;
|
||||
|
||||
return () => colorItem.notify = null;
|
||||
});
|
||||
|
||||
return (
|
||||
<LayoutGridItem itemColor={ colorItem.color } itemActive={ colorItem.isSelected } { ...rest }>
|
||||
{ colorItem.isHC && <CurrencyIcon className="position-absolute end-1 bottom-1" type={ 'hc' } /> }
|
||||
{ children }
|
||||
</LayoutGridItem>
|
||||
);
|
||||
}
|
@ -0,0 +1,35 @@
|
||||
import { FC, useCallback } from 'react';
|
||||
import { AutoGrid } from '../../../../common/AutoGrid';
|
||||
import { AvatarEditorGridColorItem } from '../../common/AvatarEditorGridColorItem';
|
||||
import { CategoryData } from '../../common/CategoryData';
|
||||
import { IAvatarEditorCategoryModel } from '../../common/IAvatarEditorCategoryModel';
|
||||
import { AvatarEditorPaletteSetItem } from './AvatarEditorPaletteSetItemView';
|
||||
|
||||
export interface AvatarEditorPaletteSetViewProps
|
||||
{
|
||||
model: IAvatarEditorCategoryModel;
|
||||
category: CategoryData;
|
||||
paletteSet: AvatarEditorGridColorItem[];
|
||||
paletteIndex: number;
|
||||
}
|
||||
|
||||
export const AvatarEditorPaletteSetView: FC<AvatarEditorPaletteSetViewProps> = props =>
|
||||
{
|
||||
const { model = null, category = null, paletteSet = [], paletteIndex = -1 } = props;
|
||||
|
||||
const selectColor = useCallback((item: AvatarEditorGridColorItem) =>
|
||||
{
|
||||
const index = paletteSet.indexOf(item);
|
||||
|
||||
if(index === -1) return;
|
||||
|
||||
model.selectColor(category.name, index, paletteIndex);
|
||||
}, [ model, category, paletteSet, paletteIndex ]);
|
||||
|
||||
return (
|
||||
<AutoGrid grow columnCount={ 4 } columnMinWidth={ 30 } overflow="auto">
|
||||
{ (paletteSet.length > 0) && paletteSet.map((item, index) =>
|
||||
<AvatarEditorPaletteSetItem key={ index } colorItem={ item } onClick={ event => selectColor(item) } />) }
|
||||
</AutoGrid>
|
||||
);
|
||||
}
|
@ -1,13 +1,21 @@
|
||||
import { SaveWardrobeOutfitMessageComposer } from '@nitrots/nitro-renderer';
|
||||
import { FC, useCallback, useMemo } from 'react';
|
||||
import { IAvatarFigureContainer, SaveWardrobeOutfitMessageComposer } from '@nitrots/nitro-renderer';
|
||||
import { Dispatch, FC, SetStateAction, useCallback, useMemo } from 'react';
|
||||
import { Button } from 'react-bootstrap';
|
||||
import { GetAvatarRenderManager, GetSessionDataManager } from '../../../../api';
|
||||
import { AutoGrid } from '../../../../common/AutoGrid';
|
||||
import { LayoutGridItem } from '../../../../common/layout/LayoutGridItem';
|
||||
import { SendMessageHook } from '../../../../hooks';
|
||||
import { NitroCardGridItemView } from '../../../../layout/card/grid/item/NitroCardGridItemView';
|
||||
import { NitroCardGridView } from '../../../../layout/card/grid/NitroCardGridView';
|
||||
import { AvatarImageView } from '../../../shared/avatar-image/AvatarImageView';
|
||||
import { CurrencyIcon } from '../../../shared/currency-icon/CurrencyIcon';
|
||||
import { AvatarEditorWardrobeViewProps } from './AvatarEditorWardrobeView.types';
|
||||
import { AvatarImageView } from '../../../../views/shared/avatar-image/AvatarImageView';
|
||||
import { CurrencyIcon } from '../../../../views/shared/currency-icon/CurrencyIcon';
|
||||
import { FigureData } from '../../common/FigureData';
|
||||
|
||||
export interface AvatarEditorWardrobeViewProps
|
||||
{
|
||||
figureData: FigureData;
|
||||
savedFigures: [ IAvatarFigureContainer, string ][];
|
||||
setSavedFigures: Dispatch<SetStateAction<[ IAvatarFigureContainer, string][]>>;
|
||||
loadAvatarInEditor: (figure: string, gender: string, reset?: boolean) => void;
|
||||
}
|
||||
|
||||
export const AvatarEditorWardrobeView: FC<AvatarEditorWardrobeViewProps> = props =>
|
||||
{
|
||||
@ -50,14 +58,15 @@ export const AvatarEditorWardrobeView: FC<AvatarEditorWardrobeViewProps> = props
|
||||
if(figureContainer) clubLevel = GetAvatarRenderManager().getFigureClubLevel(figureContainer, gender);
|
||||
|
||||
items.push(
|
||||
<NitroCardGridItemView key={ index } className="flex-column justify-content-end position-relative">
|
||||
{ figureContainer && <AvatarImageView figure={ figureContainer.getFigureString() } gender={ gender } direction={ 2 } /> }
|
||||
<LayoutGridItem key={ index } position="relative" justifyContent="end" alignItems="center">
|
||||
{ figureContainer &&
|
||||
<AvatarImageView figure={ figureContainer.getFigureString() } gender={ gender } direction={ 2 } position="absolute" className="bottom-3" /> }
|
||||
{ (clubLevel > 0) && <CurrencyIcon className="position-absolute top-1 start-1" type="hc" /> }
|
||||
<div className="d-flex w-100 figure-button-container p-1">
|
||||
<Button variant="link" size="sm" className="w-100" onClick={ event => saveFigureAtWardrobeIndex(index) }>Save</Button>
|
||||
{ figureContainer && <Button variant="link" size="sm" className="w-100" onClick={ event => wearFigureAtIndex(index) } disabled={ (clubLevel > GetSessionDataManager().clubLevel) }>Use</Button> }
|
||||
</div>
|
||||
</NitroCardGridItemView>
|
||||
</LayoutGridItem>
|
||||
);
|
||||
});
|
||||
|
||||
@ -65,8 +74,8 @@ export const AvatarEditorWardrobeView: FC<AvatarEditorWardrobeViewProps> = props
|
||||
}, [ savedFigures, saveFigureAtWardrobeIndex, wearFigureAtIndex ]);
|
||||
|
||||
return (
|
||||
<NitroCardGridView className="nitro-wardrobe-grid">
|
||||
<AutoGrid grow columnCount={ 5 } overflow="auto" columnMinWidth={ 80 } columnMinHeight={ 140 }>
|
||||
{ figures }
|
||||
</NitroCardGridView>
|
||||
</AutoGrid>
|
||||
);
|
||||
}
|
24
src/components/camera/CameraWidgetContext.tsx
Normal file
24
src/components/camera/CameraWidgetContext.tsx
Normal file
@ -0,0 +1,24 @@
|
||||
import { createContext, Dispatch, FC, ProviderProps, SetStateAction, useContext } from 'react';
|
||||
import { CameraPicture } from './common/CameraPicture';
|
||||
|
||||
export interface ICameraWidgetContext
|
||||
{
|
||||
cameraRoll: CameraPicture[],
|
||||
setCameraRoll: Dispatch<SetStateAction<CameraPicture[]>>;
|
||||
selectedPictureIndex: number,
|
||||
setSelectedPictureIndex: Dispatch<SetStateAction<number>>;
|
||||
}
|
||||
|
||||
const CameraWidgetContext = createContext<ICameraWidgetContext>({
|
||||
cameraRoll: null,
|
||||
setCameraRoll: null,
|
||||
selectedPictureIndex: null,
|
||||
setSelectedPictureIndex: null
|
||||
});
|
||||
|
||||
export const CameraWidgetContextProvider: FC<ProviderProps<ICameraWidgetContext>> = props =>
|
||||
{
|
||||
return <CameraWidgetContext.Provider value={ props.value }>{ props.children }</CameraWidgetContext.Provider>
|
||||
}
|
||||
|
||||
export const useCameraWidgetContext = () => useContext(CameraWidgetContext);
|
@ -85,89 +85,41 @@
|
||||
}
|
||||
|
||||
.nitro-camera-editor {
|
||||
width: 600px;
|
||||
|
||||
.content-area {
|
||||
min-height: 441px;
|
||||
height: 441px;
|
||||
resize: vertical;
|
||||
|
||||
.picture-preview {
|
||||
width: 320px;
|
||||
height: 320px;
|
||||
width: $camera-editor-width;
|
||||
height: $camera-editor-height;
|
||||
|
||||
.slider {
|
||||
background: linear-gradient(180deg, transparent, black);
|
||||
text-shadow: 1px 1px rgba(0, 0, 0, .5);
|
||||
}
|
||||
.picture-preview {
|
||||
width: 320px;
|
||||
height: 320px;
|
||||
}
|
||||
|
||||
.layout-grid-item {
|
||||
height: 60px !important;
|
||||
max-height: 60px !important;
|
||||
}
|
||||
|
||||
.effect-thumbnail-image {
|
||||
|
||||
img {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
image-rendering: auto;
|
||||
object-fit: contain;
|
||||
}
|
||||
}
|
||||
|
||||
.effect-grid {
|
||||
|
||||
.grid-item-container {
|
||||
height: 60px !important;
|
||||
max-height: 60px !important;
|
||||
|
||||
.grid-item {
|
||||
overflow: unset;
|
||||
|
||||
.remove-effect {
|
||||
position: absolute;
|
||||
top: -1px;
|
||||
right: -1px;
|
||||
padding: 2px;
|
||||
font-size: 11px;
|
||||
line-height: 11px;
|
||||
min-height: unset;
|
||||
}
|
||||
|
||||
.effect-thumbnail-image {
|
||||
|
||||
img {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
image-rendering: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.effects {
|
||||
height: 363px;
|
||||
min-height: 363px;
|
||||
max-height: 363px;
|
||||
|
||||
.btn-remove-effect {
|
||||
z-index: 10;
|
||||
min-height: 0px;
|
||||
border-radius: 100px;
|
||||
width: 20px;
|
||||
}
|
||||
|
||||
.effect-thumbnail {
|
||||
border-color: $grid-border-color !important;
|
||||
background-color: $grid-bg-color;
|
||||
height: 60px;
|
||||
|
||||
&.active {
|
||||
border-color: $grid-active-border-color !important;
|
||||
background-color: $grid-active-bg-color;
|
||||
}
|
||||
|
||||
.effect-thumbnail-image {
|
||||
img {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
}
|
||||
}
|
||||
}
|
||||
.remove-effect {
|
||||
position: absolute;
|
||||
top: 1px;
|
||||
right: 1px;
|
||||
padding: 2px;
|
||||
font-size: 10px;
|
||||
min-height: unset;
|
||||
}
|
||||
}
|
||||
|
||||
.nitro-camera-checkout {
|
||||
width: 388px;
|
||||
width: $camera-checkout-width;
|
||||
|
||||
.picture-preview {
|
||||
width: 320px;
|
@ -6,8 +6,8 @@ import { useRoomSessionManagerEvent } from '../../hooks';
|
||||
import { useCameraEvent } from '../../hooks/events/nitro/camera/camera-event';
|
||||
import { useUiEvent } from '../../hooks/events/ui/ui-event';
|
||||
import { CreateMessageHook, SendMessageHook } from '../../hooks/messages/message-event';
|
||||
import { CameraWidgetContextProvider } from './CameraWidgetContext';
|
||||
import { CameraPicture } from './common/CameraPicture';
|
||||
import { CameraWidgetContextProvider } from './context/CameraWidgetContext';
|
||||
import { CameraWidgetCaptureView } from './views/capture/CameraWidgetCaptureView';
|
||||
import { CameraWidgetCheckoutView } from './views/checkout/CameraWidgetCheckoutView';
|
||||
import { CameraWidgetEditorView } from './views/editor/CameraWidgetEditorView';
|
5
src/components/camera/common/CameraEditorTabs.ts
Normal file
5
src/components/camera/common/CameraEditorTabs.ts
Normal file
@ -0,0 +1,5 @@
|
||||
export class CameraEditorTabs
|
||||
{
|
||||
public static readonly COLORMATRIX: string = 'colormatrix';
|
||||
public static readonly COMPOSITE: string = 'composite';
|
||||
}
|
@ -1,11 +1,21 @@
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { NitroRectangle, TextureUtils } from '@nitrots/nitro-renderer';
|
||||
import { FC, useCallback, useRef } from 'react';
|
||||
import { GetRoomEngine, GetRoomSession, LocalizeText } from '../../../../api';
|
||||
import { CAMERA_SHUTTER, PlaySound } from '../../../../api/utils/PlaySound';
|
||||
import { Column } from '../../../../common/Column';
|
||||
import { Flex } from '../../../../common/Flex';
|
||||
import { DraggableWindow } from '../../../../layout';
|
||||
import { NotificationUtilities } from '../../../../views/notification-center/common/NotificationUtilities';
|
||||
import { useCameraWidgetContext } from '../../CameraWidgetContext';
|
||||
import { CameraPicture } from '../../common/CameraPicture';
|
||||
import { useCameraWidgetContext } from '../../context/CameraWidgetContext';
|
||||
import { CameraWidgetCaptureViewProps } from './CameraWidgetCaptureView.types';
|
||||
|
||||
export interface CameraWidgetCaptureViewProps
|
||||
{
|
||||
onClose: () => void;
|
||||
onEdit: () => void;
|
||||
onDelete: () => void;
|
||||
}
|
||||
|
||||
const CAMERA_ROLL_LIMIT: number = 5;
|
||||
|
||||
@ -40,7 +50,7 @@ export const CameraWidgetCaptureView: FC<CameraWidgetCaptureViewProps> = props =
|
||||
|
||||
if(clone.length >= CAMERA_ROLL_LIMIT)
|
||||
{
|
||||
alert(LocalizeText('camera.full.body'));
|
||||
NotificationUtilities.simpleAlert(LocalizeText('camera.full.body'));
|
||||
|
||||
clone.pop();
|
||||
}
|
||||
@ -52,12 +62,12 @@ export const CameraWidgetCaptureView: FC<CameraWidgetCaptureViewProps> = props =
|
||||
}, [ cameraRoll, selectedPictureIndex, getCameraBounds, setCameraRoll, setSelectedPictureIndex ]);
|
||||
|
||||
return (
|
||||
<DraggableWindow>
|
||||
<div className="d-flex flex-column justify-content-center align-items-center nitro-camera-capture">
|
||||
<DraggableWindow uniqueKey="nitro-camera-capture">
|
||||
<Column center className="nitro-camera-capture" gap={ 0 }>
|
||||
{ selectedPicture && <img alt="" className="camera-area" src={ selectedPicture.imageUrl } /> }
|
||||
<div className="camera-canvas drag-handler">
|
||||
<div className="position-absolute header-close" onClick={ onClose }>
|
||||
<i className="fas fa-times" />
|
||||
<FontAwesomeIcon icon="times" />
|
||||
</div>
|
||||
{ !selectedPicture && <div ref={ elementRef } className="camera-area camera-view-finder" /> }
|
||||
{ selectedPicture &&
|
||||
@ -72,13 +82,13 @@ export const CameraWidgetCaptureView: FC<CameraWidgetCaptureViewProps> = props =
|
||||
</div>
|
||||
</div>
|
||||
{ (cameraRoll.length > 0) &&
|
||||
<div className="camera-roll d-flex justify-content-center py-2">
|
||||
<Flex gap={ 2 } justifyContent="center" className="camera-roll d-flex justify-content-center py-2">
|
||||
{ cameraRoll.map((picture, index) =>
|
||||
{
|
||||
return <img alt="" key={ index } className={ (index < (cameraRoll.length - 1) ? 'me-2' : '') } src={ picture.imageUrl } onClick={ event => setSelectedPictureIndex(index) } />;
|
||||
return <img alt="" key={ index } src={ picture.imageUrl } onClick={ event => setSelectedPictureIndex(index) } />;
|
||||
}) }
|
||||
</div> }
|
||||
</div>
|
||||
</Flex> }
|
||||
</Column>
|
||||
</DraggableWindow>
|
||||
);
|
||||
}
|
@ -0,0 +1,175 @@
|
||||
import { CameraPublishStatusMessageEvent, CameraPurchaseOKMessageEvent, CameraStorageUrlMessageEvent, PublishPhotoMessageComposer, PurchasePhotoMessageComposer } from '@nitrots/nitro-renderer';
|
||||
import { FC, useCallback, useEffect, useState } from 'react';
|
||||
import { GetConfiguration, GetRoomEngine, LocalizeText } from '../../../../api';
|
||||
import { Button } from '../../../../common/Button';
|
||||
import { Column } from '../../../../common/Column';
|
||||
import { Flex } from '../../../../common/Flex';
|
||||
import { LayoutImage } from '../../../../common/layout/LayoutImage';
|
||||
import { Text } from '../../../../common/Text';
|
||||
import { InventoryEvent } from '../../../../events';
|
||||
import { BatchUpdates, CreateMessageHook, dispatchUiEvent, SendMessageHook } from '../../../../hooks';
|
||||
import { NitroCardContentView, NitroCardHeaderView, NitroCardView } from '../../../../layout';
|
||||
import { CurrencyIcon } from '../../../../views/shared/currency-icon/CurrencyIcon';
|
||||
|
||||
export interface CameraWidgetCheckoutViewProps
|
||||
{
|
||||
base64Url: string;
|
||||
onCloseClick: () => void;
|
||||
onCancelClick: () => void;
|
||||
price: { credits: number, duckets: number, publishDucketPrice: number };
|
||||
}
|
||||
|
||||
export const CameraWidgetCheckoutView: FC<CameraWidgetCheckoutViewProps> = props =>
|
||||
{
|
||||
const { base64Url = null, onCloseClick = null, onCancelClick = null, price = null } = props;
|
||||
const [ pictureUrl, setPictureUrl ] = useState<string>(null);
|
||||
const [ publishUrl, setPublishUrl ] = useState<string>(null);
|
||||
const [ picturesBought, setPicturesBought ] = useState(0);
|
||||
const [ wasPicturePublished, setWasPicturePublished ] = useState(false);
|
||||
const [ isWaiting, setIsWaiting ] = useState(false);
|
||||
const [ publishCooldown, setPublishCooldown ] = useState(0);
|
||||
|
||||
const onCameraPurchaseOKMessageEvent = useCallback((event: CameraPurchaseOKMessageEvent) =>
|
||||
{
|
||||
BatchUpdates(() =>
|
||||
{
|
||||
setPicturesBought(value => (value + 1));
|
||||
setIsWaiting(false);
|
||||
});
|
||||
}, []);
|
||||
|
||||
CreateMessageHook(CameraPurchaseOKMessageEvent, onCameraPurchaseOKMessageEvent);
|
||||
|
||||
const onCameraPublishStatusMessageEvent = useCallback((event: CameraPublishStatusMessageEvent) =>
|
||||
{
|
||||
const parser = event.getParser();
|
||||
|
||||
BatchUpdates(() =>
|
||||
{
|
||||
setPublishUrl(parser.extraDataId);
|
||||
setPublishCooldown(parser.secondsToWait);
|
||||
setWasPicturePublished(parser.ok);
|
||||
setIsWaiting(false);
|
||||
});
|
||||
}, []);
|
||||
|
||||
CreateMessageHook(CameraPublishStatusMessageEvent, onCameraPublishStatusMessageEvent);
|
||||
|
||||
const onCameraStorageUrlMessageEvent = useCallback((event: CameraStorageUrlMessageEvent) =>
|
||||
{
|
||||
const parser = event.getParser();
|
||||
|
||||
setPictureUrl(GetConfiguration<string>('camera.url') + '/' + parser.url);
|
||||
}, []);
|
||||
|
||||
CreateMessageHook(CameraStorageUrlMessageEvent, onCameraStorageUrlMessageEvent);
|
||||
|
||||
const processAction = (type: string, value: string | number = null) =>
|
||||
{
|
||||
switch(type)
|
||||
{
|
||||
case 'close':
|
||||
onCloseClick();
|
||||
return;
|
||||
case 'buy':
|
||||
if(isWaiting) return;
|
||||
|
||||
setIsWaiting(true);
|
||||
SendMessageHook(new PurchasePhotoMessageComposer(''));
|
||||
return;
|
||||
case 'publish':
|
||||
if(isWaiting) return;
|
||||
|
||||
setIsWaiting(true);
|
||||
SendMessageHook(new PublishPhotoMessageComposer());
|
||||
return;
|
||||
case 'cancel':
|
||||
onCancelClick();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
if(!base64Url) return;
|
||||
|
||||
GetRoomEngine().saveBase64AsScreenshot(base64Url);
|
||||
}, [ base64Url ]);
|
||||
|
||||
if(!price) return null;
|
||||
|
||||
return (
|
||||
<NitroCardView className="nitro-camera-checkout" simple={ true }>
|
||||
<NitroCardHeaderView headerText={ LocalizeText('camera.confirm_phase.title') } onCloseClick={ event => processAction('close') } />
|
||||
<NitroCardContentView>
|
||||
<Flex center>
|
||||
{ (pictureUrl && pictureUrl.length) &&
|
||||
<LayoutImage fit={ false } className="picture-preview border" imageUrl={ pictureUrl } /> }
|
||||
{ (!pictureUrl || !pictureUrl.length) &&
|
||||
<Flex center className="picture-preview border">
|
||||
<Text bold>{ LocalizeText('camera.loading') }</Text>
|
||||
</Flex> }
|
||||
</Flex>
|
||||
<Flex justifyContent="between" alignItems="center" className="bg-muted rounded p-2">
|
||||
<Column gap={ 1 }>
|
||||
<Text bold>
|
||||
{ LocalizeText('camera.purchase.header') }
|
||||
</Text>
|
||||
{ ((price.credits > 0) || (price.duckets > 0)) &&
|
||||
<Flex gap={ 1 }>
|
||||
<Text>{ LocalizeText('catalog.purchase.confirmation.dialog.cost') }</Text>
|
||||
{ (price.credits > 0) &&
|
||||
<Flex gap={ 1 }>
|
||||
<Text bold>{ price.credits }</Text>
|
||||
<CurrencyIcon type={ -1 } />
|
||||
</Flex> }
|
||||
{ (price.duckets > 0) &&
|
||||
<Flex gap={ 1 }>
|
||||
<Text bold>{ price.duckets }</Text>
|
||||
<CurrencyIcon type={ 5 } />
|
||||
</Flex> }
|
||||
</Flex> }
|
||||
{ (picturesBought > 0) &&
|
||||
<Text>
|
||||
<Text bold>{ LocalizeText('camera.purchase.count.info') }</Text> { picturesBought }
|
||||
<u className="ms-1 cursor-pointer" onClick={ () => dispatchUiEvent(new InventoryEvent(InventoryEvent.SHOW_INVENTORY)) }>{ LocalizeText('camera.open.inventory') }</u>
|
||||
</Text> }
|
||||
</Column>
|
||||
<Flex alignItems="center">
|
||||
<Button variant="success" disabled={ isWaiting } onClick={ event => processAction('buy') }>{ LocalizeText(!picturesBought ? 'buy' : 'camera.buy.another.button.text') }</Button>
|
||||
</Flex>
|
||||
</Flex>
|
||||
<Flex justifyContent="between" alignItems="center" className="bg-muted rounded p-2">
|
||||
<Column gap={ 1 }>
|
||||
<Text bold>
|
||||
{ LocalizeText(wasPicturePublished ? 'camera.publish.successful' : 'camera.publish.explanation') }
|
||||
</Text>
|
||||
<Text>
|
||||
{ LocalizeText(wasPicturePublished ? 'camera.publish.success.short.info' : 'camera.publish.detailed.explanation') }
|
||||
</Text>
|
||||
{ wasPicturePublished && <a href={ publishUrl } rel="noreferrer" target="_blank">{ LocalizeText('camera.link.to.published') }</a> }
|
||||
{ !wasPicturePublished && (price.publishDucketPrice > 0) &&
|
||||
<Flex gap={ 1 }>
|
||||
<Text>{ LocalizeText('catalog.purchase.confirmation.dialog.cost') }</Text>
|
||||
<Flex gap={ 1 }>
|
||||
<Text bold>{ price.publishDucketPrice }</Text>
|
||||
<CurrencyIcon type={ 5 } />
|
||||
</Flex>
|
||||
</Flex> }
|
||||
{ (publishCooldown > 0) && <div className="mt-1 text-center fw-bold">{ LocalizeText('camera.publish.wait', [ 'minutes' ], [ Math.ceil( publishCooldown / 60).toString() ]) }</div> }
|
||||
</Column>
|
||||
{ !wasPicturePublished &&
|
||||
<Flex className="d-flex align-items-end">
|
||||
<Button variant="success" size="sm" disabled={ (isWaiting || (publishCooldown > 0)) } onClick={ event => processAction('publish') }>
|
||||
{ LocalizeText('camera.publish.button.text') }
|
||||
</Button>
|
||||
</Flex> }
|
||||
</Flex>
|
||||
<Text center>{ LocalizeText('camera.warning.disclaimer') }</Text>
|
||||
<Flex justifyContent="end">
|
||||
<Button onClick={ event => processAction('cancel') }>{ LocalizeText('generic.cancel') }</Button>
|
||||
</Flex>
|
||||
</NitroCardContentView>
|
||||
</NitroCardView>
|
||||
);
|
||||
}
|
@ -1,13 +1,32 @@
|
||||
import { IRoomCameraWidgetSelectedEffect, RoomCameraWidgetSelectedEffect } from '@nitrots/nitro-renderer';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { IRoomCameraWidgetEffect, IRoomCameraWidgetSelectedEffect, RoomCameraWidgetSelectedEffect } from '@nitrots/nitro-renderer';
|
||||
import { FC, useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import ReactSlider from 'react-slider';
|
||||
import { GetRoomCameraWidgetManager, LocalizeText } from '../../../../api';
|
||||
import { Button } from '../../../../common/Button';
|
||||
import { ButtonGroup } from '../../../../common/ButtonGroup';
|
||||
import { Column } from '../../../../common/Column';
|
||||
import { Flex } from '../../../../common/Flex';
|
||||
import { Grid } from '../../../../common/Grid';
|
||||
import { LayoutImage } from '../../../../common/layout/LayoutImage';
|
||||
import { Text } from '../../../../common/Text';
|
||||
import { NitroCardContentView, NitroCardHeaderView, NitroCardTabsItemView, NitroCardTabsView, NitroCardView } from '../../../../layout';
|
||||
import { CameraEditorTabs } from '../../common/CameraEditorTabs';
|
||||
import { CameraPicture } from '../../common/CameraPicture';
|
||||
import { CameraPictureThumbnail } from '../../common/CameraPictureThumbnail';
|
||||
import { CameraWidgetEditorTabs, CameraWidgetEditorViewProps } from './CameraWidgetEditorView.types';
|
||||
import { CameraWidgetEffectListView } from './effect-list/CameraWidgetEffectListView';
|
||||
|
||||
const TABS: string[] = [ CameraWidgetEditorTabs.COLORMATRIX, CameraWidgetEditorTabs.COMPOSITE ];
|
||||
export interface CameraWidgetEditorViewProps
|
||||
{
|
||||
picture: CameraPicture;
|
||||
availableEffects: IRoomCameraWidgetEffect[];
|
||||
myLevel: number;
|
||||
onClose: () => void;
|
||||
onCancel: () => void;
|
||||
onCheckout: (pictureUrl: string) => void;
|
||||
}
|
||||
|
||||
const TABS: string[] = [ CameraEditorTabs.COLORMATRIX, CameraEditorTabs.COMPOSITE ];
|
||||
|
||||
export const CameraWidgetEditorView: FC<CameraWidgetEditorViewProps> = props =>
|
||||
{
|
||||
@ -30,7 +49,7 @@ export const CameraWidgetEditorView: FC<CameraWidgetEditorViewProps> = props =>
|
||||
|
||||
const getEffectList = useCallback(() =>
|
||||
{
|
||||
if(currentTab === CameraWidgetEditorTabs.COLORMATRIX)
|
||||
if(currentTab === CameraEditorTabs.COLORMATRIX)
|
||||
{
|
||||
return getColorMatrixEffects;
|
||||
}
|
||||
@ -170,45 +189,49 @@ export const CameraWidgetEditorView: FC<CameraWidgetEditorViewProps> = props =>
|
||||
}) }
|
||||
</NitroCardTabsView>
|
||||
<NitroCardContentView>
|
||||
<div className="row h-100">
|
||||
<div className="col-5 d-flex flex-column h-100">
|
||||
<Grid>
|
||||
<Column size={ 5 } overflow="hidden">
|
||||
<CameraWidgetEffectListView myLevel={ myLevel } selectedEffects={ selectedEffects } effects={ getEffectList() } thumbnails={ effectsThumbnails } processAction={ processAction } />
|
||||
</div>
|
||||
<div className="col-7 d-flex flex-column h-100">
|
||||
<div className="picture-preview">
|
||||
<img alt="" src={ getCurrentPictureUrl } />
|
||||
</div>
|
||||
{ selectedEffectName &&
|
||||
<div className="w-100 p-2 d-flex flex-column justify-content-center slider">
|
||||
<div className="w-100 text-center">{ LocalizeText('camera.effect.name.' + selectedEffectName) }</div>
|
||||
<ReactSlider
|
||||
className={ 'nitro-slider' }
|
||||
min={ 0 }
|
||||
max={ 1 }
|
||||
step={ 0.01 }
|
||||
value={ getCurrentEffect.alpha }
|
||||
onChange={ event => setSelectedEffectAlpha(event) }
|
||||
renderThumb={ (props, state) => <div { ...props }>{ state.valueNow }</div> } />
|
||||
</div> }
|
||||
<div className="d-flex justify-content-between mt-2">
|
||||
<div className="btn-group">
|
||||
<button className="btn btn-primary" onClick={ event => processAction('clear_effects') }>
|
||||
<i className="fas fa-trash"></i>
|
||||
</button>
|
||||
<button className="btn btn-primary" onClick={ event => processAction('download') }>
|
||||
<i className="fas fa-save"></i>
|
||||
</button>
|
||||
<button className="btn btn-primary" onClick={ event => processAction('zoom') }>
|
||||
<i className={ `fas fa-search-${ isZoomed ? 'minus': 'plus' }` } />
|
||||
</button>
|
||||
</div>
|
||||
<div className="d-flex justify-content-end">
|
||||
<button className="btn btn-primary me-2" onClick={ event => processAction('cancel') }>{ LocalizeText('generic.cancel') }</button>
|
||||
<button className="btn btn-success" onClick={ event => processAction('checkout') }>{ LocalizeText('camera.preview.button.text') }</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Column>
|
||||
<Column size={ 7 } justifyContent="between" overflow="hidden">
|
||||
<Column center>
|
||||
<LayoutImage fit={ false } imageUrl={ getCurrentPictureUrl } className="picture-preview" />
|
||||
{ selectedEffectName &&
|
||||
<Column center fullWidth gap={ 1 }>
|
||||
<Text>{ LocalizeText('camera.effect.name.' + selectedEffectName) }</Text>
|
||||
<ReactSlider
|
||||
className={ 'nitro-slider' }
|
||||
min={ 0 }
|
||||
max={ 1 }
|
||||
step={ 0.01 }
|
||||
value={ getCurrentEffect.alpha }
|
||||
onChange={ event => setSelectedEffectAlpha(event) }
|
||||
renderThumb={ (props, state) => <div { ...props }>{ state.valueNow }</div> } />
|
||||
</Column> }
|
||||
</Column>
|
||||
<Flex justifyContent="between">
|
||||
<ButtonGroup>
|
||||
<Button size="sm" onClick={ event => processAction('clear_effects') }>
|
||||
<FontAwesomeIcon icon="trash" />
|
||||
</Button>
|
||||
<Button size="sm" onClick={ event => processAction('download') }>
|
||||
<FontAwesomeIcon icon="save" />
|
||||
</Button>
|
||||
<Button size="sm" onClick={ event => processAction('zoom') }>
|
||||
<FontAwesomeIcon icon={ isZoomed ? 'search-minus' : 'search-plus' } />
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
<Flex gap={ 1 }>
|
||||
<Button size="sm" onClick={ event => processAction('cancel') }>
|
||||
{ LocalizeText('generic.cancel') }
|
||||
</Button>
|
||||
<Button size="sm" onClick={ event => processAction('checkout') }>
|
||||
{ LocalizeText('camera.preview.button.text') }
|
||||
</Button>
|
||||
</Flex>
|
||||
</Flex>
|
||||
</Column>
|
||||
</Grid>
|
||||
</NitroCardContentView>
|
||||
</NitroCardView>
|
||||
);
|
@ -0,0 +1,42 @@
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { IRoomCameraWidgetEffect } from '@nitrots/nitro-renderer';
|
||||
import { FC } from 'react';
|
||||
import { LocalizeText } from '../../../../../api';
|
||||
import { Button } from '../../../../../common/Button';
|
||||
import { LayoutGridItem } from '../../../../../common/layout/LayoutGridItem';
|
||||
import { Text } from '../../../../../common/Text';
|
||||
|
||||
export interface CameraWidgetEffectListItemViewProps
|
||||
{
|
||||
effect: IRoomCameraWidgetEffect;
|
||||
thumbnailUrl: string;
|
||||
isActive: boolean;
|
||||
isLocked: boolean;
|
||||
selectEffect: () => void;
|
||||
removeEffect: () => void;
|
||||
}
|
||||
|
||||
export const CameraWidgetEffectListItemView: FC<CameraWidgetEffectListItemViewProps> = props =>
|
||||
{
|
||||
const { effect = null, thumbnailUrl = null, isActive = false, isLocked = false, selectEffect = null, removeEffect = null } = props;
|
||||
|
||||
return (
|
||||
<LayoutGridItem title={ LocalizeText(!isLocked ? (`camera.effect.name.${ effect.name }`) : `camera.effect.required.level ${ effect.minLevel }`) } itemActive={ isActive } onClick={ event => (!isActive && selectEffect()) }>
|
||||
{ isActive &&
|
||||
<Button variant="danger" size="sm" className="rounded-circle remove-effect" onClick={ removeEffect }>
|
||||
<FontAwesomeIcon icon="times" />
|
||||
</Button> }
|
||||
{ !isLocked && (thumbnailUrl && thumbnailUrl.length > 0) &&
|
||||
<div className="effect-thumbnail-image border">
|
||||
<img alt="" src={ thumbnailUrl } />
|
||||
</div> }
|
||||
{ isLocked &&
|
||||
<Text center bold>
|
||||
<div>
|
||||
<FontAwesomeIcon icon="lock" />
|
||||
</div>
|
||||
{ effect.minLevel }
|
||||
</Text> }
|
||||
</LayoutGridItem>
|
||||
);
|
||||
}
|
@ -1,14 +1,24 @@
|
||||
import { IRoomCameraWidgetEffect, IRoomCameraWidgetSelectedEffect } from '@nitrots/nitro-renderer';
|
||||
import { FC } from 'react';
|
||||
import { NitroCardGridView } from '../../../../../layout';
|
||||
import { CameraWidgetEffectListItemView } from '../effect-list-item/CameraWidgetEffectListItemView';
|
||||
import { CameraWidgetEffectListViewProps } from './CameraWidgetEffectListView.types';
|
||||
import { AutoGrid } from '../../../../../common/AutoGrid';
|
||||
import { CameraPictureThumbnail } from '../../../common/CameraPictureThumbnail';
|
||||
import { CameraWidgetEffectListItemView } from './CameraWidgetEffectListItemView';
|
||||
|
||||
export interface CameraWidgetEffectListViewProps
|
||||
{
|
||||
myLevel: number;
|
||||
selectedEffects: IRoomCameraWidgetSelectedEffect[];
|
||||
effects: IRoomCameraWidgetEffect[];
|
||||
thumbnails: CameraPictureThumbnail[];
|
||||
processAction: (type: string, name: string) => void;
|
||||
}
|
||||
|
||||
export const CameraWidgetEffectListView: FC<CameraWidgetEffectListViewProps> = props =>
|
||||
{
|
||||
const { myLevel = 0, selectedEffects = [], effects = [], thumbnails = [], processAction = null } = props;
|
||||
|
||||
return (
|
||||
<NitroCardGridView className="effect-grid" columns={ 3 }>
|
||||
<AutoGrid columnCount={ 2 } columnMinHeight={ 60 }>
|
||||
{ effects && (effects.length > 0) && effects.map((effect, index) =>
|
||||
{
|
||||
const thumbnailUrl = (thumbnails.find(thumbnail => (thumbnail.effectName === effect.name)));
|
||||
@ -16,6 +26,6 @@ export const CameraWidgetEffectListView: FC<CameraWidgetEffectListViewProps> = p
|
||||
|
||||
return <CameraWidgetEffectListItemView key={ index } effect={ effect } thumbnailUrl={ ((thumbnailUrl && thumbnailUrl.thumbnailUrl) || null) } isActive={ isActive } isLocked={ (effect.minLevel > myLevel) } selectEffect={ () => processAction('select_effect', effect.name) } removeEffect={ () => processAction('remove_effect', effect.name) } />
|
||||
}) }
|
||||
</NitroCardGridView>
|
||||
</AutoGrid>
|
||||
);
|
||||
}
|
83
src/components/catalog/CatalogContext.tsx
Normal file
83
src/components/catalog/CatalogContext.tsx
Normal file
@ -0,0 +1,83 @@
|
||||
import { FrontPageItem, RoomPreviewer } from '@nitrots/nitro-renderer';
|
||||
import { createContext, Dispatch, FC, ProviderProps, SetStateAction, useContext } from 'react';
|
||||
import { ICatalogNode } from './common/ICatalogNode';
|
||||
import { ICatalogOptions } from './common/ICatalogOptions';
|
||||
import { ICatalogPage } from './common/ICatalogPage';
|
||||
import { IPageLocalization } from './common/IPageLocalization';
|
||||
import { IPurchasableOffer } from './common/IPurchasableOffer';
|
||||
import { IPurchaseOptions } from './common/IPurchaseOptions';
|
||||
import { SearchResult } from './common/SearchResult';
|
||||
|
||||
interface ICatalogContext
|
||||
{
|
||||
isVisible: boolean;
|
||||
isBusy: boolean;
|
||||
setIsBusy: Dispatch<SetStateAction<boolean>>;
|
||||
pageId: number;
|
||||
currentType: string;
|
||||
setCurrentType: Dispatch<SetStateAction<string>>;
|
||||
rootNode: ICatalogNode;
|
||||
setRootNode: Dispatch<SetStateAction<ICatalogNode>>;
|
||||
offersToNodes: Map<number, ICatalogNode[]>;
|
||||
setOffersToNodes: Dispatch<SetStateAction<Map<number, ICatalogNode[]>>>;
|
||||
currentPage: ICatalogPage;
|
||||
setCurrentPage: Dispatch<SetStateAction<ICatalogPage>>;
|
||||
currentOffer: IPurchasableOffer;
|
||||
setCurrentOffer: Dispatch<SetStateAction<IPurchasableOffer>>;
|
||||
activeNodes: ICatalogNode[];
|
||||
setActiveNodes: Dispatch<SetStateAction<ICatalogNode[]>>;
|
||||
searchResult: SearchResult;
|
||||
setSearchResult: Dispatch<SetStateAction<SearchResult>>;
|
||||
frontPageItems: FrontPageItem[];
|
||||
setFrontPageItems: Dispatch<SetStateAction<FrontPageItem[]>>;
|
||||
roomPreviewer: RoomPreviewer;
|
||||
purchaseOptions: IPurchaseOptions;
|
||||
setPurchaseOptions: Dispatch<SetStateAction<IPurchaseOptions>>;
|
||||
catalogOptions: ICatalogOptions;
|
||||
setCatalogOptions: Dispatch<SetStateAction<ICatalogOptions>>;
|
||||
resetState: () => void;
|
||||
getNodesByOfferId: (offerId: number, flag?: boolean) => ICatalogNode[];
|
||||
loadCatalogPage: (pageId: number, offerId: number) => void;
|
||||
showCatalogPage: (pageId: number, layoutCode: string, localization: IPageLocalization, offers: IPurchasableOffer[], offerId: number, acceptSeasonCurrencyAsCredits: boolean) => void;
|
||||
activateNode: (targetNode: ICatalogNode) => void;
|
||||
}
|
||||
|
||||
const CatalogContext = createContext<ICatalogContext>({
|
||||
isVisible: null,
|
||||
isBusy: null,
|
||||
setIsBusy: null,
|
||||
pageId: null,
|
||||
currentType: null,
|
||||
setCurrentType: null,
|
||||
rootNode: null,
|
||||
setRootNode: null,
|
||||
offersToNodes: null,
|
||||
setOffersToNodes: null,
|
||||
currentPage: null,
|
||||
setCurrentPage: null,
|
||||
currentOffer: null,
|
||||
setCurrentOffer: null,
|
||||
activeNodes: null,
|
||||
setActiveNodes: null,
|
||||
searchResult: null,
|
||||
setSearchResult: null,
|
||||
frontPageItems: null,
|
||||
setFrontPageItems: null,
|
||||
roomPreviewer: null,
|
||||
purchaseOptions: null,
|
||||
setPurchaseOptions: null,
|
||||
catalogOptions: null,
|
||||
setCatalogOptions: null,
|
||||
resetState: null,
|
||||
getNodesByOfferId: null,
|
||||
loadCatalogPage: null,
|
||||
showCatalogPage: null,
|
||||
activateNode: null
|
||||
});
|
||||
|
||||
export const CatalogContextProvider: FC<ProviderProps<ICatalogContext>> = props =>
|
||||
{
|
||||
return <CatalogContext.Provider value={ props.value }>{ props.children }</CatalogContext.Provider>
|
||||
}
|
||||
|
||||
export const useCatalogContext = () => useContext(CatalogContext);
|
333
src/components/catalog/CatalogMessageHandler.tsx
Normal file
333
src/components/catalog/CatalogMessageHandler.tsx
Normal file
@ -0,0 +1,333 @@
|
||||
import { ApproveNameMessageEvent, CatalogPageMessageEvent, CatalogPagesListEvent, CatalogPublishedMessageEvent, ClubGiftInfoEvent, GiftReceiverNotFoundEvent, GiftWrappingConfigurationEvent, HabboClubOffersMessageEvent, LimitedEditionSoldOutEvent, MarketplaceConfigurationEvent, MarketplaceMakeOfferResult, NodeData, ProductOfferEvent, PurchaseErrorMessageEvent, PurchaseNotAllowedMessageEvent, PurchaseOKMessageEvent, SellablePetPalettesMessageEvent, UserSubscriptionEvent } from '@nitrots/nitro-renderer';
|
||||
import { GuildMembershipsMessageEvent } from '@nitrots/nitro-renderer/src/nitro/communication/messages/incoming/user/GuildMembershipsMessageEvent';
|
||||
import { FC, useCallback } from 'react';
|
||||
import { GetFurnitureData, GetProductDataForLocalization, LocalizeText } from '../../api';
|
||||
import { CatalogNameResultEvent, CatalogPurchaseFailureEvent, CatalogPurchaseNotAllowedEvent, CatalogSetExtraPurchaseParameterEvent } from '../../events';
|
||||
import { CatalogGiftReceiverNotFoundEvent } from '../../events/catalog/CatalogGiftReceiverNotFoundEvent';
|
||||
import { CatalogPurchasedEvent } from '../../events/catalog/CatalogPurchasedEvent';
|
||||
import { CatalogPurchaseSoldOutEvent } from '../../events/catalog/CatalogPurchaseSoldOutEvent';
|
||||
import { BatchUpdates } from '../../hooks';
|
||||
import { dispatchUiEvent } from '../../hooks/events/ui/ui-event';
|
||||
import { CreateMessageHook } from '../../hooks/messages/message-event';
|
||||
import { NotificationAlertType } from '../../views/notification-center/common/NotificationAlertType';
|
||||
import { NotificationUtilities } from '../../views/notification-center/common/NotificationUtilities';
|
||||
import { useCatalogContext } from './CatalogContext';
|
||||
import { CatalogNode } from './common/CatalogNode';
|
||||
import { CatalogPetPalette } from './common/CatalogPetPalette';
|
||||
import { CatalogType } from './common/CatalogType';
|
||||
import { GiftWrappingConfiguration } from './common/GiftWrappingConfiguration';
|
||||
import { ICatalogNode } from './common/ICatalogNode';
|
||||
import { IProduct } from './common/IProduct';
|
||||
import { IPurchasableOffer } from './common/IPurchasableOffer';
|
||||
import { Offer } from './common/Offer';
|
||||
import { PageLocalization } from './common/PageLocalization';
|
||||
import { Product } from './common/Product';
|
||||
import { ProductTypeEnum } from './common/ProductTypeEnum';
|
||||
import { SubscriptionInfo } from './common/SubscriptionInfo';
|
||||
|
||||
export const CatalogMessageHandler: FC<{}> = props =>
|
||||
{
|
||||
const { setIsBusy, pageId, currentType, setRootNode, setOffersToNodes, currentPage, setCurrentOffer, setFrontPageItems, resetState, showCatalogPage, setCatalogOptions = null } = useCatalogContext();
|
||||
|
||||
const onCatalogPagesListEvent = useCallback((event: CatalogPagesListEvent) =>
|
||||
{
|
||||
const parser = event.getParser();
|
||||
const offers: Map<number, ICatalogNode[]> = new Map();
|
||||
|
||||
const getCatalogNode = (node: NodeData, depth: number, parent: ICatalogNode) =>
|
||||
{
|
||||
const catalogNode = (new CatalogNode(node, depth, parent) as ICatalogNode);
|
||||
|
||||
for(const offerId of catalogNode.offerIds)
|
||||
{
|
||||
if(offers.has(offerId)) offers.get(offerId).push(catalogNode);
|
||||
else offers.set(offerId, [ catalogNode ]);
|
||||
}
|
||||
|
||||
depth++;
|
||||
|
||||
for(const child of node.children) catalogNode.addChild(getCatalogNode(child, depth, catalogNode));
|
||||
|
||||
return catalogNode;
|
||||
}
|
||||
|
||||
BatchUpdates(() =>
|
||||
{
|
||||
setRootNode(getCatalogNode(parser.root, 0, null));
|
||||
setOffersToNodes(offers);
|
||||
});
|
||||
}, [ setRootNode, setOffersToNodes ]);
|
||||
|
||||
const onCatalogPageMessageEvent = useCallback((event: CatalogPageMessageEvent) =>
|
||||
{
|
||||
const parser = event.getParser();
|
||||
|
||||
if(parser.catalogType !== currentType) return;
|
||||
|
||||
const purchasableOffers: IPurchasableOffer[] = [];
|
||||
|
||||
for(const offer of parser.offers)
|
||||
{
|
||||
const products: IProduct[] = [];
|
||||
const productData = GetProductDataForLocalization(offer.localizationId);
|
||||
|
||||
for(const product of offer.products)
|
||||
{
|
||||
const furnitureData = GetFurnitureData(product.furniClassId, product.productType);
|
||||
|
||||
products.push(new Product(product.productType, product.furniClassId, product.extraParam, product.productCount, productData, furnitureData, product.uniqueLimitedItem, product.uniqueLimitedSeriesSize, product.uniqueLimitedItemsLeft));
|
||||
}
|
||||
|
||||
if(!products.length) continue;
|
||||
|
||||
const purchasableOffer = new Offer(offer.offerId, offer.localizationId, offer.rent, offer.priceCredits, offer.priceActivityPoints, offer.priceActivityPointsType, offer.giftable, offer.clubLevel, products, offer.bundlePurchaseAllowed);
|
||||
|
||||
if((currentType === CatalogType.NORMAL) || ((purchasableOffer.pricingModel !== Offer.PRICING_MODEL_BUNDLE) && (purchasableOffer.pricingModel !== Offer.PRICING_MODEL_MULTI))) purchasableOffers.push(purchasableOffer);
|
||||
}
|
||||
|
||||
BatchUpdates(() =>
|
||||
{
|
||||
if(parser.frontPageItems && parser.frontPageItems.length) setFrontPageItems(parser.frontPageItems);
|
||||
|
||||
setIsBusy(false);
|
||||
|
||||
if(pageId === parser.pageId)
|
||||
{
|
||||
showCatalogPage(parser.pageId, parser.layoutCode, new PageLocalization(parser.localization.images.concat(), parser.localization.texts.concat()), purchasableOffers, parser.offerId, parser.acceptSeasonCurrencyAsCredits);
|
||||
}
|
||||
});
|
||||
}, [ currentType, pageId, setFrontPageItems, setIsBusy, showCatalogPage ]);
|
||||
|
||||
const onPurchaseOKMessageEvent = useCallback((event: PurchaseOKMessageEvent) =>
|
||||
{
|
||||
const parser = event.getParser();
|
||||
|
||||
dispatchUiEvent(new CatalogPurchasedEvent(parser.offer));
|
||||
}, []);
|
||||
|
||||
const onPurchaseErrorMessageEvent = useCallback((event: PurchaseErrorMessageEvent) =>
|
||||
{
|
||||
const parser = event.getParser();
|
||||
|
||||
dispatchUiEvent(new CatalogPurchaseFailureEvent(parser.code));
|
||||
}, []);
|
||||
|
||||
const onPurchaseNotAllowedMessageEvent = useCallback((event: PurchaseNotAllowedMessageEvent) =>
|
||||
{
|
||||
const parser = event.getParser();
|
||||
|
||||
dispatchUiEvent(new CatalogPurchaseNotAllowedEvent(parser.code));
|
||||
}, []);
|
||||
|
||||
const onLimitedEditionSoldOutEvent = useCallback((event: LimitedEditionSoldOutEvent) =>
|
||||
{
|
||||
const parser = event.getParser();
|
||||
|
||||
dispatchUiEvent(new CatalogPurchaseSoldOutEvent());
|
||||
}, []);
|
||||
|
||||
const onProductOfferEvent = useCallback((event: ProductOfferEvent) =>
|
||||
{
|
||||
const parser = event.getParser();
|
||||
const offerData = parser.offer;
|
||||
|
||||
if(!offerData || !offerData.products.length) return;
|
||||
|
||||
const offerProductData = offerData.products[0];
|
||||
|
||||
if(offerProductData.uniqueLimitedItem)
|
||||
{
|
||||
// update unique
|
||||
}
|
||||
|
||||
const products: IProduct[] = [];
|
||||
const productData = GetProductDataForLocalization(offerData.localizationId);
|
||||
|
||||
for(const product of offerData.products)
|
||||
{
|
||||
const furnitureData = GetFurnitureData(product.furniClassId, product.productType);
|
||||
|
||||
products.push(new Product(product.productType, product.furniClassId, product.extraParam, product.productCount, productData, furnitureData, product.uniqueLimitedItem, product.uniqueLimitedSeriesSize, product.uniqueLimitedItemsLeft));
|
||||
}
|
||||
|
||||
const offer = new Offer(offerData.offerId, offerData.localizationId, offerData.rent, offerData.priceCredits, offerData.priceActivityPoints, offerData.priceActivityPointsType, offerData.giftable, offerData.clubLevel, products, offerData.bundlePurchaseAllowed);
|
||||
|
||||
if(!((currentType === CatalogType.NORMAL) || ((offer.pricingModel !== Offer.PRICING_MODEL_BUNDLE) && (offer.pricingModel !== Offer.PRICING_MODEL_MULTI)))) return;
|
||||
|
||||
offer.page = currentPage;
|
||||
|
||||
setCurrentOffer(offer);
|
||||
|
||||
if(offer.product && (offer.product.productType === ProductTypeEnum.WALL))
|
||||
{
|
||||
dispatchUiEvent(new CatalogSetExtraPurchaseParameterEvent(offer.product.extraParam));
|
||||
}
|
||||
|
||||
// (this._isObjectMoverRequested) && (this._purchasableOffer)
|
||||
}, [ currentType, currentPage, setCurrentOffer ]);
|
||||
|
||||
const onSellablePetPalettesMessageEvent = useCallback((event: SellablePetPalettesMessageEvent) =>
|
||||
{
|
||||
const parser = event.getParser();
|
||||
const petPalette = new CatalogPetPalette(parser.productCode, parser.palettes.slice());
|
||||
|
||||
setCatalogOptions(prevValue =>
|
||||
{
|
||||
const petPalettes = [];
|
||||
|
||||
if(prevValue.petPalettes) petPalettes.push(...prevValue.petPalettes);
|
||||
|
||||
for(let i = 0; i < petPalettes.length; i++)
|
||||
{
|
||||
const palette = petPalettes[i];
|
||||
|
||||
if(palette.breed === petPalette.breed)
|
||||
{
|
||||
petPalettes.splice(i, 1);
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
petPalettes.push(petPalette);
|
||||
|
||||
return { ...prevValue, petPalettes };
|
||||
});
|
||||
}, [ setCatalogOptions ]);
|
||||
|
||||
const onApproveNameMessageEvent = useCallback((event: ApproveNameMessageEvent) =>
|
||||
{
|
||||
const parser = event.getParser();
|
||||
|
||||
dispatchUiEvent(new CatalogNameResultEvent(parser.result, parser.validationInfo));
|
||||
}, []);
|
||||
|
||||
const onGiftReceiverNotFoundEvent = useCallback(() =>
|
||||
{
|
||||
dispatchUiEvent(new CatalogGiftReceiverNotFoundEvent());
|
||||
}, []);
|
||||
|
||||
const onHabboClubOffersMessageEvent = useCallback((event: HabboClubOffersMessageEvent) =>
|
||||
{
|
||||
const parser = event.getParser();
|
||||
|
||||
setCatalogOptions(prevValue =>
|
||||
{
|
||||
const clubOffers = parser.offers;
|
||||
|
||||
return { ...prevValue, clubOffers };
|
||||
});
|
||||
}, [ setCatalogOptions ]);
|
||||
|
||||
const onGuildMembershipsMessageEvent = useCallback((event: GuildMembershipsMessageEvent) =>
|
||||
{
|
||||
const parser = event.getParser();
|
||||
|
||||
setCatalogOptions(prevValue =>
|
||||
{
|
||||
const groups = parser.groups;
|
||||
|
||||
return { ...prevValue, groups };
|
||||
});
|
||||
}, [ setCatalogOptions ]);
|
||||
|
||||
const onUserSubscriptionEvent = useCallback((event: UserSubscriptionEvent) =>
|
||||
{
|
||||
const parser = event.getParser();
|
||||
|
||||
setCatalogOptions(prevValue =>
|
||||
{
|
||||
const subscriptionInfo = new SubscriptionInfo(
|
||||
Math.max(0, parser.daysToPeriodEnd),
|
||||
Math.max(0, parser.periodsSubscribedAhead),
|
||||
parser.isVip,
|
||||
parser.pastClubDays,
|
||||
parser.pastVipDays);
|
||||
|
||||
return { ...prevValue, subscriptionInfo };
|
||||
});
|
||||
}, [ setCatalogOptions ]);
|
||||
|
||||
const onCatalogPublishedMessageEvent = useCallback((event: CatalogPublishedMessageEvent) =>
|
||||
{
|
||||
resetState();
|
||||
}, [ resetState ]);
|
||||
|
||||
const onGiftWrappingConfigurationEvent = useCallback((event: GiftWrappingConfigurationEvent) =>
|
||||
{
|
||||
const parser = event.getParser();
|
||||
|
||||
setCatalogOptions(prevValue =>
|
||||
{
|
||||
const giftConfiguration = new GiftWrappingConfiguration(parser);
|
||||
|
||||
return { ...prevValue, giftConfiguration };
|
||||
});
|
||||
}, [ setCatalogOptions ]);
|
||||
|
||||
const onMarketplaceMakeOfferResult = useCallback((event: MarketplaceMakeOfferResult) =>
|
||||
{
|
||||
const parser = event.getParser();
|
||||
|
||||
if(!parser) return;
|
||||
|
||||
let title = '';
|
||||
if(parser.result === 1)
|
||||
{
|
||||
title = LocalizeText('inventory.marketplace.result.title.success');
|
||||
}
|
||||
else
|
||||
{
|
||||
title = LocalizeText('inventory.marketplace.result.title.failure');
|
||||
}
|
||||
|
||||
const message = LocalizeText(`inventory.marketplace.result.${parser.result}`);
|
||||
|
||||
NotificationUtilities.simpleAlert(message, NotificationAlertType.DEFAULT, null, null, title);
|
||||
}, []);
|
||||
|
||||
const onMarketplaceConfigurationEvent = useCallback((event: MarketplaceConfigurationEvent) =>
|
||||
{
|
||||
const parser = event.getParser();
|
||||
|
||||
setCatalogOptions(prevValue =>
|
||||
{
|
||||
const marketplaceConfiguration = parser;
|
||||
|
||||
return { ...prevValue, marketplaceConfiguration };
|
||||
});
|
||||
}, [ setCatalogOptions ]);
|
||||
|
||||
const onClubGiftInfoEvent = useCallback((event: ClubGiftInfoEvent) =>
|
||||
{
|
||||
const parser = event.getParser();
|
||||
|
||||
setCatalogOptions(prevValue =>
|
||||
{
|
||||
const clubGifts = parser;
|
||||
|
||||
return { ...prevValue, clubGifts };
|
||||
});
|
||||
}, [ setCatalogOptions ]);
|
||||
|
||||
CreateMessageHook(CatalogPagesListEvent, onCatalogPagesListEvent);
|
||||
CreateMessageHook(CatalogPageMessageEvent, onCatalogPageMessageEvent);
|
||||
CreateMessageHook(PurchaseOKMessageEvent, onPurchaseOKMessageEvent);
|
||||
CreateMessageHook(PurchaseErrorMessageEvent, onPurchaseErrorMessageEvent);
|
||||
CreateMessageHook(PurchaseNotAllowedMessageEvent, onPurchaseNotAllowedMessageEvent);
|
||||
CreateMessageHook(LimitedEditionSoldOutEvent, onLimitedEditionSoldOutEvent);
|
||||
CreateMessageHook(ProductOfferEvent, onProductOfferEvent);
|
||||
CreateMessageHook(GuildMembershipsMessageEvent, onGuildMembershipsMessageEvent);
|
||||
CreateMessageHook(SellablePetPalettesMessageEvent, onSellablePetPalettesMessageEvent);
|
||||
CreateMessageHook(ApproveNameMessageEvent, onApproveNameMessageEvent);
|
||||
CreateMessageHook(GiftReceiverNotFoundEvent, onGiftReceiverNotFoundEvent);
|
||||
CreateMessageHook(HabboClubOffersMessageEvent, onHabboClubOffersMessageEvent);
|
||||
CreateMessageHook(UserSubscriptionEvent, onUserSubscriptionEvent);
|
||||
CreateMessageHook(CatalogPublishedMessageEvent, onCatalogPublishedMessageEvent);
|
||||
CreateMessageHook(GiftWrappingConfigurationEvent, onGiftWrappingConfigurationEvent);
|
||||
CreateMessageHook(ClubGiftInfoEvent, onClubGiftInfoEvent);
|
||||
CreateMessageHook(MarketplaceMakeOfferResult, onMarketplaceMakeOfferResult);
|
||||
CreateMessageHook(MarketplaceConfigurationEvent, onMarketplaceConfigurationEvent);
|
||||
|
||||
return null;
|
||||
}
|
124
src/components/catalog/CatalogView.scss
Normal file
124
src/components/catalog/CatalogView.scss
Normal file
@ -0,0 +1,124 @@
|
||||
.nitro-catalog {
|
||||
width: $catalog-width;
|
||||
height: $catalog-height;
|
||||
|
||||
font[size="16"] {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.catalog-search-button {
|
||||
min-width: 30px;
|
||||
width: 30px;
|
||||
}
|
||||
|
||||
.quantity-input {
|
||||
min-height: 17px;
|
||||
height: 17px;
|
||||
width: 20px;
|
||||
padding: 0 4px;
|
||||
text-align: right;
|
||||
}
|
||||
}
|
||||
|
||||
.catalog-icon-image {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
min-width: 20px;
|
||||
min-height: 20px;
|
||||
}
|
||||
|
||||
.nitro-catalog-gift {
|
||||
width: 325px;
|
||||
|
||||
.gift-preview {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.gift-color {
|
||||
width: 15px;
|
||||
height: 15px;
|
||||
border-radius: $border-radius;
|
||||
}
|
||||
}
|
||||
|
||||
.nitro-catalog-navigation-grid-container {
|
||||
border-radius: 0.25rem;
|
||||
border-color: #B6BEC5 !important;
|
||||
background-color: #CDD3D9;
|
||||
border: 2px solid;
|
||||
|
||||
.layout-grid-item {
|
||||
font-size: $font-size-sm;
|
||||
height: 23px !important;
|
||||
border-color: unset !important;
|
||||
background-color: #CDD3D9;
|
||||
border: 0 !important;
|
||||
padding: 1px 3px;
|
||||
|
||||
.svg-inline--fa {
|
||||
color: $black;
|
||||
font-size: 10px;
|
||||
padding: 1px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.nitro-catalog-layout-info-loyalty {
|
||||
|
||||
.info-loyalty-content {
|
||||
background-repeat: no-repeat;
|
||||
background-position: top right;
|
||||
background-image: url('../../assets/images/catalog/diamond_info_illustration.gif');
|
||||
padding-right:123px;
|
||||
}
|
||||
|
||||
.info-image {
|
||||
width: 123px;
|
||||
height:350px;
|
||||
background-image: url('../../assets/images/catalog/diamond_info_illustration.gif');
|
||||
}
|
||||
}
|
||||
|
||||
.nitro-catalog-layout-vip-buy-grid {
|
||||
|
||||
.layout-grid-item {
|
||||
height: 50px !important;
|
||||
max-height: 50px !important;
|
||||
|
||||
.icon-hc-banner {
|
||||
width: 68px;
|
||||
height: 40px;
|
||||
background: url("../../assets/images/catalog/hc_big.png") center no-repeat;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.nitro-catalog-layout-marketplace-grid {
|
||||
|
||||
.layout-grid-item {
|
||||
height: 75px !important;
|
||||
max-height: 75px !important;
|
||||
}
|
||||
}
|
||||
|
||||
.nitro-catalog-layout-vip-gifts-grid {
|
||||
|
||||
.layout-grid-item {
|
||||
height: 55px !important;
|
||||
max-height: 55px !important;
|
||||
}
|
||||
}
|
||||
|
||||
.nitro-catalog-layout-marketplace-post-offer {
|
||||
width: $marketplace-post-offer-width;
|
||||
height: $marketplace-post-offer-height;
|
||||
}
|
||||
|
||||
.nitro-catalog-layout-bundle-grid {
|
||||
|
||||
.layout-grid-item {
|
||||
background-color: transparent;
|
||||
}
|
||||
}
|
448
src/components/catalog/CatalogView.tsx
Normal file
448
src/components/catalog/CatalogView.tsx
Normal file
@ -0,0 +1,448 @@
|
||||
import { FrontPageItem, GetCatalogIndexComposer, GetCatalogPageComposer, GetClubGiftInfo, GetGiftWrappingConfigurationComposer, GetMarketplaceConfigurationMessageComposer, ILinkEventTracker, RoomPreviewer } from '@nitrots/nitro-renderer';
|
||||
import { FC, useCallback, useEffect, useState } from 'react';
|
||||
import { AddEventLinkTracker, GetRoomEngine, LocalizeText, RemoveLinkEventTracker } from '../../api';
|
||||
import { CREDITS, PlaySound } from '../../api/utils/PlaySound';
|
||||
import { Column } from '../../common/Column';
|
||||
import { Grid } from '../../common/Grid';
|
||||
import { CatalogPurchasedEvent } from '../../events';
|
||||
import { BatchUpdates } from '../../hooks';
|
||||
import { useUiEvent } from '../../hooks/events/ui/ui-event';
|
||||
import { SendMessageHook } from '../../hooks/messages/message-event';
|
||||
import { NitroCardContentView, NitroCardHeaderView, NitroCardTabsItemView, NitroCardTabsView, NitroCardView } from '../../layout';
|
||||
import { CatalogContextProvider } from './CatalogContext';
|
||||
import { CatalogMessageHandler } from './CatalogMessageHandler';
|
||||
import { CatalogPage } from './common/CatalogPage';
|
||||
import { CatalogType } from './common/CatalogType';
|
||||
import { ICatalogNode } from './common/ICatalogNode';
|
||||
import { ICatalogOptions } from './common/ICatalogOptions';
|
||||
import { ICatalogPage } from './common/ICatalogPage';
|
||||
import { IPageLocalization } from './common/IPageLocalization';
|
||||
import { IPurchasableOffer } from './common/IPurchasableOffer';
|
||||
import { IPurchaseOptions } from './common/IPurchaseOptions';
|
||||
import { RequestedPage } from './common/RequestedPage';
|
||||
import { SearchResult } from './common/SearchResult';
|
||||
import { CatalogGiftView } from './views/gift/CatalogGiftView';
|
||||
import { CatalogNavigationView } from './views/navigation/CatalogNavigationView';
|
||||
import { GetCatalogLayout } from './views/page/layout/GetCatalogLayout';
|
||||
import { MarketplacePostOfferView } from './views/page/layout/marketplace/MarketplacePostOfferView';
|
||||
|
||||
const REQUESTED_PAGE = new RequestedPage();
|
||||
|
||||
export const CatalogView: FC<{}> = props =>
|
||||
{
|
||||
const [ isVisible, setIsVisible ] = useState(false);
|
||||
const [ isBusy, setIsBusy ] = useState(false);
|
||||
const [ pageId, setPageId ] = useState(-1);
|
||||
const [ previousPageId, setPreviousPageId ] = useState(-1);
|
||||
const [ currentType, setCurrentType ] = useState(CatalogType.NORMAL);
|
||||
const [ rootNode, setRootNode ] = useState<ICatalogNode>(null);
|
||||
const [ offersToNodes, setOffersToNodes ] = useState<Map<number, ICatalogNode[]>>(null);
|
||||
const [ currentPage, setCurrentPage ] = useState<ICatalogPage>(null);
|
||||
const [ currentOffer, setCurrentOffer ] = useState<IPurchasableOffer>(null);
|
||||
const [ activeNodes, setActiveNodes ] = useState<ICatalogNode[]>([]);
|
||||
const [ searchResult, setSearchResult ] = useState<SearchResult>(null);
|
||||
const [ frontPageItems, setFrontPageItems ] = useState<FrontPageItem[]>([]);
|
||||
const [ roomPreviewer, setRoomPreviewer ] = useState<RoomPreviewer>(null);
|
||||
const [ navigationHidden, setNavigationHidden ] = useState(false);
|
||||
const [ purchaseOptions, setPurchaseOptions ] = useState<IPurchaseOptions>({});
|
||||
const [ catalogOptions, setCatalogOptions ] = useState<ICatalogOptions>({});
|
||||
|
||||
const resetState = useCallback(() =>
|
||||
{
|
||||
BatchUpdates(() =>
|
||||
{
|
||||
setPageId(-1);
|
||||
setPreviousPageId(-1);
|
||||
setRootNode(null);
|
||||
setOffersToNodes(null);
|
||||
setCurrentPage(null);
|
||||
setCurrentOffer(null);
|
||||
setActiveNodes([]);
|
||||
setSearchResult(null);
|
||||
setFrontPageItems([]);
|
||||
setIsVisible(true);
|
||||
});
|
||||
}, []);
|
||||
|
||||
const getNodeById = useCallback((id: number, node: ICatalogNode) =>
|
||||
{
|
||||
if((node.pageId === id) && (node !== rootNode)) return node;
|
||||
|
||||
for(const child of node.children)
|
||||
{
|
||||
const found = (getNodeById(id, child) as ICatalogNode);
|
||||
|
||||
if(found) return found;
|
||||
}
|
||||
|
||||
return null;
|
||||
}, [ rootNode ]);
|
||||
|
||||
const getNodeByName = useCallback((name: string, node: ICatalogNode) =>
|
||||
{
|
||||
if((node.pageName === name) && (node !== rootNode)) return node;
|
||||
|
||||
for(const child of node.children)
|
||||
{
|
||||
const found = (getNodeByName(name, child) as ICatalogNode);
|
||||
|
||||
if(found) return found;
|
||||
}
|
||||
|
||||
return null;
|
||||
}, [ rootNode ]);
|
||||
|
||||
const getNodesByOfferId = useCallback((offerId: number, flag: boolean = false) =>
|
||||
{
|
||||
if(!offersToNodes || !offersToNodes.size) return null;
|
||||
|
||||
if(flag)
|
||||
{
|
||||
const nodes: ICatalogNode[] = [];
|
||||
const offers = offersToNodes.get(offerId);
|
||||
|
||||
if(offers && offers.length) for(const offer of offers) (offer.isVisible && nodes.push(offer));
|
||||
|
||||
if(nodes.length) return nodes;
|
||||
}
|
||||
|
||||
return offersToNodes.get(offerId);
|
||||
}, [ offersToNodes ]);
|
||||
|
||||
const loadCatalogPage = useCallback((pageId: number, offerId: number) =>
|
||||
{
|
||||
if(pageId < 0) return;
|
||||
|
||||
BatchUpdates(() =>
|
||||
{
|
||||
setIsBusy(true);
|
||||
setPageId(pageId);
|
||||
});
|
||||
|
||||
if(pageId > -1) SendMessageHook(new GetCatalogPageComposer(pageId, offerId, currentType));
|
||||
}, [ currentType ]);
|
||||
|
||||
const showCatalogPage = useCallback((pageId: number, layoutCode: string, localization: IPageLocalization, offers: IPurchasableOffer[], offerId: number, acceptSeasonCurrencyAsCredits: boolean) =>
|
||||
{
|
||||
const catalogPage = (new CatalogPage(pageId, layoutCode, localization, offers, acceptSeasonCurrencyAsCredits) as ICatalogPage);
|
||||
|
||||
BatchUpdates(() =>
|
||||
{
|
||||
setCurrentPage(catalogPage);
|
||||
setPreviousPageId(prevValue => ((pageId !== -1) ? pageId : prevValue));
|
||||
setNavigationHidden(false);
|
||||
|
||||
if((offerId > -1) && catalogPage.offers.length)
|
||||
{
|
||||
for(const offer of catalogPage.offers)
|
||||
{
|
||||
if(offer.offerId !== offerId) continue;
|
||||
|
||||
setCurrentOffer(offer)
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
}, []);
|
||||
|
||||
const activateNode = useCallback((targetNode: ICatalogNode, offerId: number = -1) =>
|
||||
{
|
||||
if(targetNode.parent.pageName === 'root')
|
||||
{
|
||||
if(targetNode.children.length)
|
||||
{
|
||||
for(const child of targetNode.children)
|
||||
{
|
||||
if(!child.isVisible) continue;
|
||||
|
||||
targetNode = child;
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const nodes: ICatalogNode[] = [];
|
||||
|
||||
let node = targetNode;
|
||||
|
||||
while(node && (node.pageName !== 'root'))
|
||||
{
|
||||
nodes.push(node);
|
||||
|
||||
node = node.parent;
|
||||
}
|
||||
|
||||
nodes.reverse();
|
||||
|
||||
setActiveNodes(prevValue =>
|
||||
{
|
||||
const isActive = (prevValue.indexOf(targetNode) >= 0);
|
||||
const isOpen = targetNode.isOpen;
|
||||
|
||||
for(const existing of prevValue)
|
||||
{
|
||||
existing.deactivate();
|
||||
|
||||
if(nodes.indexOf(existing) === -1) existing.close();
|
||||
}
|
||||
|
||||
for(const n of nodes)
|
||||
{
|
||||
n.activate();
|
||||
|
||||
if(n.parent) n.open();
|
||||
|
||||
if((n === targetNode.parent) && n.children.length) n.open();
|
||||
}
|
||||
|
||||
if(isActive && isOpen) targetNode.close();
|
||||
else targetNode.open();
|
||||
|
||||
return nodes;
|
||||
});
|
||||
|
||||
if(targetNode.pageId > -1) loadCatalogPage(targetNode.pageId, offerId);
|
||||
}, [ setActiveNodes, loadCatalogPage ]);
|
||||
|
||||
const openPageById = useCallback((id: number) =>
|
||||
{
|
||||
BatchUpdates(() =>
|
||||
{
|
||||
setSearchResult(null);
|
||||
|
||||
if(!isVisible)
|
||||
{
|
||||
REQUESTED_PAGE.requestById = id;
|
||||
|
||||
setIsVisible(true);
|
||||
}
|
||||
else
|
||||
{
|
||||
const node = getNodeById(id, rootNode);
|
||||
|
||||
if(node) activateNode(node);
|
||||
}
|
||||
});
|
||||
}, [ isVisible, rootNode, getNodeById, activateNode ]);
|
||||
|
||||
const openPageByName = useCallback((name: string) =>
|
||||
{
|
||||
BatchUpdates(() =>
|
||||
{
|
||||
setSearchResult(null);
|
||||
|
||||
if(!isVisible)
|
||||
{
|
||||
REQUESTED_PAGE.requestByName = name;
|
||||
|
||||
setIsVisible(true);
|
||||
}
|
||||
else
|
||||
{
|
||||
const node = getNodeByName(name, rootNode);
|
||||
|
||||
if(node) activateNode(node);
|
||||
}
|
||||
});
|
||||
}, [ isVisible, rootNode, getNodeByName, activateNode ]);
|
||||
|
||||
const openPageByOfferId = useCallback((offerId: number) =>
|
||||
{
|
||||
BatchUpdates(() =>
|
||||
{
|
||||
setSearchResult(null);
|
||||
|
||||
if(!isVisible)
|
||||
{
|
||||
REQUESTED_PAGE.requestedByOfferId = offerId;
|
||||
|
||||
setIsVisible(true);
|
||||
}
|
||||
else
|
||||
{
|
||||
const nodes = getNodesByOfferId(offerId);
|
||||
|
||||
if(!nodes || !nodes.length) return;
|
||||
|
||||
activateNode(nodes[0], offerId);
|
||||
}
|
||||
});
|
||||
}, [ isVisible, getNodesByOfferId, activateNode ]);
|
||||
|
||||
const onCatalogPurchasedEvent = useCallback((event: CatalogPurchasedEvent) =>
|
||||
{
|
||||
PlaySound(CREDITS);
|
||||
}, []);
|
||||
|
||||
useUiEvent(CatalogPurchasedEvent.PURCHASE_SUCCESS, onCatalogPurchasedEvent);
|
||||
|
||||
const linkReceived = useCallback((url: string) =>
|
||||
{
|
||||
const parts = url.split('/');
|
||||
|
||||
if(parts.length < 2) return;
|
||||
|
||||
switch(parts[1])
|
||||
{
|
||||
case 'show':
|
||||
setIsVisible(true);
|
||||
return;
|
||||
case 'hide':
|
||||
setIsVisible(false);
|
||||
return;
|
||||
case 'toggle':
|
||||
setIsVisible(prevValue => !prevValue);
|
||||
return;
|
||||
case 'open':
|
||||
if(parts.length > 2)
|
||||
{
|
||||
if(parts.length === 4)
|
||||
{
|
||||
switch(parts[2])
|
||||
{
|
||||
case 'offerId':
|
||||
openPageByOfferId(parseInt(parts[3]));
|
||||
return;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
openPageByName(parts[2]);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
setIsVisible(true);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
}, [ openPageByOfferId, openPageByName ]);
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
const linkTracker: ILinkEventTracker = {
|
||||
linkReceived,
|
||||
eventUrlPrefix: 'catalog/'
|
||||
};
|
||||
|
||||
AddEventLinkTracker(linkTracker);
|
||||
|
||||
return () => RemoveLinkEventTracker(linkTracker);
|
||||
}, [ linkReceived ]);
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
setRoomPreviewer(new RoomPreviewer(GetRoomEngine(), ++RoomPreviewer.PREVIEW_COUNTER));
|
||||
|
||||
return () =>
|
||||
{
|
||||
setRoomPreviewer(prevValue =>
|
||||
{
|
||||
prevValue.dispose();
|
||||
|
||||
return null;
|
||||
});
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
if(!isVisible || rootNode) return;
|
||||
|
||||
SendMessageHook(new GetMarketplaceConfigurationMessageComposer());
|
||||
SendMessageHook(new GetGiftWrappingConfigurationComposer());
|
||||
SendMessageHook(new GetClubGiftInfo());
|
||||
SendMessageHook(new GetCatalogIndexComposer(currentType));
|
||||
}, [ isVisible, rootNode, currentType ]);
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
if(!isVisible || !rootNode) return;
|
||||
|
||||
switch(REQUESTED_PAGE.requestType)
|
||||
{
|
||||
case RequestedPage.REQUEST_TYPE_NONE:
|
||||
if(activeNodes && activeNodes.length) return;
|
||||
|
||||
if(rootNode.isBranch)
|
||||
{
|
||||
for(const child of rootNode.children)
|
||||
{
|
||||
if(child && child.isVisible)
|
||||
{
|
||||
activateNode(child);
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
return;
|
||||
case RequestedPage.REQUEST_TYPE_ID:
|
||||
openPageById(REQUESTED_PAGE.requestById);
|
||||
REQUESTED_PAGE.resetRequest();
|
||||
return;
|
||||
case RequestedPage.REQUEST_TYPE_OFFER:
|
||||
openPageByOfferId(REQUESTED_PAGE.requestedByOfferId);
|
||||
REQUESTED_PAGE.resetRequest();
|
||||
return;
|
||||
case RequestedPage.REQUEST_TYPE_NAME:
|
||||
openPageByName(REQUESTED_PAGE.requestByName);
|
||||
REQUESTED_PAGE.resetRequest();
|
||||
return;
|
||||
}
|
||||
}, [ isVisible, rootNode, activeNodes, activateNode, openPageById, openPageByOfferId, openPageByName ]);
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
if(!searchResult && currentPage && (currentPage.pageId === -1)) openPageById(previousPageId);
|
||||
}, [ searchResult, currentPage, previousPageId, openPageById ]);
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
return () => setCurrentOffer(null);
|
||||
}, [ currentPage ]);
|
||||
|
||||
return (
|
||||
<CatalogContextProvider value={ { isVisible, isBusy, setIsBusy, pageId, currentType, setCurrentType, rootNode, setRootNode, offersToNodes, setOffersToNodes, currentPage, setCurrentPage, currentOffer, setCurrentOffer, activeNodes, setActiveNodes, searchResult, setSearchResult, frontPageItems, setFrontPageItems, roomPreviewer, purchaseOptions, setPurchaseOptions, catalogOptions, setCatalogOptions, resetState, getNodesByOfferId, loadCatalogPage, showCatalogPage, activateNode } }>
|
||||
<CatalogMessageHandler />
|
||||
{ isVisible &&
|
||||
<NitroCardView uniqueKey="catalog" className="nitro-catalog">
|
||||
<NitroCardHeaderView headerText={ LocalizeText('catalog.title') } onCloseClick={ event => { setIsVisible(false); } } />
|
||||
<NitroCardTabsView>
|
||||
{ rootNode && (rootNode.children.length > 0) && rootNode.children.map(child =>
|
||||
{
|
||||
if(!child.isVisible) return null;
|
||||
|
||||
return (
|
||||
<NitroCardTabsItemView key={ child.pageId } isActive={ child.isActive } onClick={ event =>
|
||||
{
|
||||
if(searchResult) setSearchResult(null);
|
||||
|
||||
activateNode(child);
|
||||
} }>
|
||||
{ child.localization }
|
||||
</NitroCardTabsItemView>
|
||||
);
|
||||
}) }
|
||||
</NitroCardTabsView>
|
||||
<NitroCardContentView>
|
||||
<Grid>
|
||||
{ !navigationHidden &&
|
||||
<Column size={ 3 } overflow="hidden">
|
||||
{ activeNodes && (activeNodes.length > 0) &&
|
||||
<CatalogNavigationView node={ activeNodes[0] } /> }
|
||||
</Column> }
|
||||
<Column size={ !navigationHidden ? 9 : 12 } overflow="hidden">
|
||||
{ GetCatalogLayout(currentPage, () => setNavigationHidden(true)) }
|
||||
</Column>
|
||||
</Grid>
|
||||
</NitroCardContentView>
|
||||
</NitroCardView> }
|
||||
<CatalogGiftView />
|
||||
<MarketplacePostOfferView />
|
||||
</CatalogContextProvider>
|
||||
);
|
||||
}
|
124
src/components/catalog/common/CatalogNode.ts
Normal file
124
src/components/catalog/common/CatalogNode.ts
Normal file
@ -0,0 +1,124 @@
|
||||
import { NodeData } from '@nitrots/nitro-renderer';
|
||||
import { ICatalogNode } from './ICatalogNode';
|
||||
|
||||
export class CatalogNode implements ICatalogNode
|
||||
{
|
||||
private _depth: number = 0;
|
||||
private _localization: string = '';
|
||||
private _pageId: number = -1;
|
||||
private _pageName: string = '';
|
||||
private _iconId: number = 0;
|
||||
private _children: ICatalogNode[];
|
||||
private _offerIds: number[];
|
||||
private _parent: ICatalogNode;
|
||||
private _isVisible: boolean;
|
||||
private _isActive: boolean;
|
||||
private _isOpen: boolean;
|
||||
|
||||
constructor(node: NodeData, depth: number, parent: ICatalogNode)
|
||||
{
|
||||
this._depth = depth;
|
||||
this._parent = parent;
|
||||
this._localization = node.localization;
|
||||
this._pageId = node.pageId;
|
||||
this._pageName = node.pageName;
|
||||
this._iconId = node.icon;
|
||||
this._children = [];
|
||||
this._offerIds = node.offerIds;
|
||||
this._isVisible = node.visible;
|
||||
this._isActive = false;
|
||||
this._isOpen = false;
|
||||
}
|
||||
|
||||
public activate(): void
|
||||
{
|
||||
this._isActive = true;
|
||||
}
|
||||
|
||||
public deactivate(): void
|
||||
{
|
||||
this._isActive = false;
|
||||
}
|
||||
|
||||
public open(): void
|
||||
{
|
||||
this._isOpen = true;
|
||||
}
|
||||
|
||||
public close(): void
|
||||
{
|
||||
this._isOpen = false;
|
||||
}
|
||||
|
||||
public addChild(child: ICatalogNode):void
|
||||
{
|
||||
if(!child) return;
|
||||
|
||||
this._children.push(child);
|
||||
}
|
||||
|
||||
public get depth(): number
|
||||
{
|
||||
return this._depth;
|
||||
}
|
||||
|
||||
public get isBranch(): boolean
|
||||
{
|
||||
return (this._children.length > 0);
|
||||
}
|
||||
|
||||
public get isLeaf(): boolean
|
||||
{
|
||||
return (this._children.length === 0);
|
||||
}
|
||||
|
||||
public get localization(): string
|
||||
{
|
||||
return this._localization;
|
||||
}
|
||||
|
||||
public get pageId(): number
|
||||
{
|
||||
return this._pageId;
|
||||
}
|
||||
|
||||
public get pageName(): string
|
||||
{
|
||||
return this._pageName;
|
||||
}
|
||||
|
||||
public get iconId(): number
|
||||
{
|
||||
return this._iconId;
|
||||
}
|
||||
|
||||
public get children(): ICatalogNode[]
|
||||
{
|
||||
return this._children;
|
||||
}
|
||||
|
||||
public get offerIds(): number[]
|
||||
{
|
||||
return this._offerIds;
|
||||
}
|
||||
|
||||
public get parent(): ICatalogNode
|
||||
{
|
||||
return this._parent;
|
||||
}
|
||||
|
||||
public get isVisible(): boolean
|
||||
{
|
||||
return this._isVisible;
|
||||
}
|
||||
|
||||
public get isActive(): boolean
|
||||
{
|
||||
return this._isActive;
|
||||
}
|
||||
|
||||
public get isOpen(): boolean
|
||||
{
|
||||
return this._isOpen;
|
||||
}
|
||||
}
|
59
src/components/catalog/common/CatalogPage.ts
Normal file
59
src/components/catalog/common/CatalogPage.ts
Normal file
@ -0,0 +1,59 @@
|
||||
import { ICatalogPage } from './ICatalogPage';
|
||||
import { IPageLocalization } from './IPageLocalization';
|
||||
import { IPurchasableOffer } from './IPurchasableOffer';
|
||||
|
||||
export class CatalogPage implements ICatalogPage
|
||||
{
|
||||
public static MODE_NORMAL: number = 0;
|
||||
|
||||
private _pageId: number;
|
||||
private _layoutCode: string;
|
||||
private _localization: IPageLocalization;
|
||||
private _offers: IPurchasableOffer[];
|
||||
private _acceptSeasonCurrencyAsCredits: boolean;
|
||||
private _mode: number;
|
||||
|
||||
constructor(pageId: number, layoutCode: string, localization: IPageLocalization, offers: IPurchasableOffer[], acceptSeasonCurrencyAsCredits: boolean, mode: number = -1)
|
||||
{
|
||||
this._pageId = pageId;
|
||||
this._layoutCode = layoutCode;
|
||||
this._localization = localization;
|
||||
this._offers = offers;
|
||||
this._acceptSeasonCurrencyAsCredits = acceptSeasonCurrencyAsCredits;
|
||||
|
||||
for(const offer of offers) (offer.page = this);
|
||||
|
||||
if(mode === -1) this._mode = CatalogPage.MODE_NORMAL;
|
||||
else this._mode = mode;
|
||||
}
|
||||
|
||||
public get pageId(): number
|
||||
{
|
||||
return this._pageId;
|
||||
}
|
||||
|
||||
public get layoutCode(): string
|
||||
{
|
||||
return this._layoutCode;
|
||||
}
|
||||
|
||||
public get localization(): IPageLocalization
|
||||
{
|
||||
return this._localization;
|
||||
}
|
||||
|
||||
public get offers(): IPurchasableOffer[]
|
||||
{
|
||||
return this._offers;
|
||||
}
|
||||
|
||||
public get acceptSeasonCurrencyAsCredits(): boolean
|
||||
{
|
||||
return this._acceptSeasonCurrencyAsCredits;
|
||||
}
|
||||
|
||||
public get mode(): number
|
||||
{
|
||||
return this._mode;
|
||||
}
|
||||
}
|
@ -3,7 +3,7 @@ import { SellablePetPaletteData } from '@nitrots/nitro-renderer';
|
||||
export class CatalogPetPalette
|
||||
{
|
||||
constructor(
|
||||
public breed: string,
|
||||
public palettes: SellablePetPaletteData[]
|
||||
public readonly breed: string,
|
||||
public readonly palettes: SellablePetPaletteData[]
|
||||
) {}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user