Merge pull request #76 from billsonnn/more-layout-changes

So many changes
This commit is contained in:
Bill 2022-02-16 04:43:36 -05:00 committed by GitHub
commit fcd22a7e9e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
660 changed files with 21838 additions and 20735 deletions

26
.gitignore vendored
View File

@ -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
View File

@ -0,0 +1 @@
*.scss

View File

@ -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"
}
}

View File

@ -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";

View File

@ -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);
}

View File

@ -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';

View File

@ -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
];
}
}

View File

@ -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;

View File

@ -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
];
}

View File

@ -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);

View 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);
}

View File

@ -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';

View File

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 11 KiB

View File

@ -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';

View File

@ -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
View 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
View 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
View 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 } />;
}

View 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
View 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
View 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
View 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
View 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>
);
}

View 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
View 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
View 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
View 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';

View 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>
);
}

View 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 } />;
}

View File

@ -0,0 +1,2 @@
export * from './LayoutGridItem';
export * from './LayoutImage';

View File

@ -0,0 +1 @@
export type AlignItemType = 'start' | 'end' | 'center' | 'baseline' | 'stretch';

View File

@ -0,0 +1 @@
export type AlignSelfType = 'start' | 'end' | 'center' | 'baseline' | 'stretch';

View File

@ -0,0 +1 @@
export type ButtonSizeType = 'lg' | 'sm';

View File

@ -0,0 +1 @@
export type ColorVariantType = 'primary' | 'success' | 'danger' | 'secondary' | 'link' | 'black' | 'white' | 'dark' | 'warning' | 'muted';

View File

@ -0,0 +1 @@
export type ColumnSizesType = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12;

View File

@ -0,0 +1 @@
export type FontSizeType = 1 | 2 | 3 | 4 | 5;

View File

@ -0,0 +1 @@
export type FontWeightType = 'bold' | 'bolder' | 'normal' | 'light' | 'lighter';

View File

@ -0,0 +1 @@
export type JustifyContentType = 'start' | 'end' | 'center' | 'between' | 'around' | 'evenly';

View File

@ -0,0 +1 @@
export type OverflowType = 'hidden' | 'auto' | 'unset';

View File

@ -0,0 +1 @@
export type PositionType = 'static' | 'relative' | 'fixed' | 'absolute' | 'sticky';

View File

@ -0,0 +1 @@
export type SpacingType = 0 | 1 | 2 | 3 | 4 | 5;

11
src/common/types/index.ts Normal file
View 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';

View 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);
}

View File

@ -0,0 +1 @@
export * from './CreateTransitionToIcon';

View File

@ -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;
}

View File

@ -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>
);

View File

@ -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 =>
{

View File

@ -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>
)
}

View File

@ -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>
);
}

View File

@ -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>
);
}

View File

@ -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>
);
}

View File

@ -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>
);
};

View File

@ -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>
);
}

View File

@ -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;
}
}
}
}

View File

@ -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>
);

View 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 } />
}

View File

@ -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>
);
}

View File

@ -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>
);
}

View File

@ -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>
);
}

View File

@ -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>
);
}

View File

@ -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>
);
}

View File

@ -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>
);
}

View File

@ -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>
);
}

View 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);

View File

@ -85,89 +85,41 @@
}
.nitro-camera-editor {
width: 600px;
width: $camera-editor-width;
height: $camera-editor-height;
.content-area {
min-height: 441px;
height: 441px;
resize: vertical;
.picture-preview {
width: 320px;
height: 320px;
}
.picture-preview {
width: 320px;
height: 320px;
.layout-grid-item {
height: 60px !important;
max-height: 60px !important;
}
.slider {
background: linear-gradient(180deg, transparent, black);
text-shadow: 1px 1px rgba(0, 0, 0, .5);
}
.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;

View File

@ -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';

View File

@ -0,0 +1,5 @@
export class CameraEditorTabs
{
public static readonly COLORMATRIX: string = 'colormatrix';
public static readonly COMPOSITE: string = 'composite';
}

View File

@ -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>
);
}

View File

@ -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>
);
}

View File

@ -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>
);

View File

@ -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>
);
}

View File

@ -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>
);
}

View 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);

View 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;
}

View 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;
}
}

View 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>
);
}

View 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;
}
}

View 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;
}
}

View File

@ -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