You've already forked Epicnabbo-Catalogus-Updated-Daily
Add coolui test
This commit is contained in:
@@ -0,0 +1,119 @@
|
||||
import { AddLinkEventTracker, GetCommunication, HabboWebTools, ILinkEventTracker, RemoveLinkEventTracker, RoomSessionEvent } from '@nitrots/nitro-renderer';
|
||||
import { AnimatePresence, motion } from 'framer-motion';
|
||||
import { FC, useEffect, useState } from 'react';
|
||||
import { useNitroEvent } from '../hooks';
|
||||
import { AchievementsView } from './achievements/AchievementsView';
|
||||
import { AvatarEditorView } from './avatar-editor';
|
||||
import { CameraWidgetView } from './camera/CameraWidgetView';
|
||||
import { CampaignView } from './campaign/CampaignView';
|
||||
import { CatalogView } from './catalog/CatalogView';
|
||||
import { ChatHistoryView } from './chat-history/ChatHistoryView';
|
||||
import { FloorplanEditorView } from './floorplan-editor/FloorplanEditorView';
|
||||
import { FriendsView } from './friends/FriendsView';
|
||||
import { GameCenterView } from './game-center/GameCenterView';
|
||||
import { GroupsView } from './groups/GroupsView';
|
||||
import { GuideToolView } from './guide-tool/GuideToolView';
|
||||
import { HcCenterView } from './hc-center/HcCenterView';
|
||||
import { HelpView } from './help/HelpView';
|
||||
import { HotelView } from './hotel-view/HotelView';
|
||||
import { InventoryView } from './inventory/InventoryView';
|
||||
import { ModToolsView } from './mod-tools/ModToolsView';
|
||||
import { NavigatorView } from './navigator/NavigatorView';
|
||||
import { NitrobubbleHiddenView } from './nitrobubblehidden/NitrobubbleHiddenView';
|
||||
import { NitropediaView } from './nitropedia/NitropediaView';
|
||||
import { RightSideView } from './right-side/RightSideView';
|
||||
import { RoomView } from './room/RoomView';
|
||||
import { ToolbarView } from './toolbar/ToolbarView';
|
||||
import { UserProfileView } from './user-profile/UserProfileView';
|
||||
import { UserSettingsView } from './user-settings/UserSettingsView';
|
||||
import { WiredView } from './wired/WiredView';
|
||||
|
||||
export const MainView: FC<{}> = props =>
|
||||
{
|
||||
const [ isReady, setIsReady ] = useState(false);
|
||||
const [ landingViewVisible, setLandingViewVisible ] = useState(true);
|
||||
|
||||
useNitroEvent<RoomSessionEvent>(RoomSessionEvent.CREATED, event => setLandingViewVisible(false));
|
||||
useNitroEvent<RoomSessionEvent>(RoomSessionEvent.ENDED, event => setLandingViewVisible(event.openLandingView));
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
setIsReady(true);
|
||||
|
||||
GetCommunication().connection.ready();
|
||||
}, []);
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
const linkTracker: ILinkEventTracker = {
|
||||
linkReceived: (url: string) =>
|
||||
{
|
||||
const parts = url.split('/');
|
||||
|
||||
if(parts.length < 2) return;
|
||||
|
||||
switch(parts[1])
|
||||
{
|
||||
case 'open':
|
||||
if(parts.length > 2)
|
||||
{
|
||||
switch(parts[2])
|
||||
{
|
||||
case 'credits':
|
||||
//HabboWebTools.openWebPageAndMinimizeClient(this._windowManager.getProperty(ExternalVariables.WEB_SHOP_RELATIVE_URL));
|
||||
break;
|
||||
default: {
|
||||
const name = parts[2];
|
||||
HabboWebTools.openHabblet(name);
|
||||
}
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
},
|
||||
eventUrlPrefix: 'habblet/'
|
||||
};
|
||||
|
||||
AddLinkEventTracker(linkTracker);
|
||||
|
||||
return () => RemoveLinkEventTracker(linkTracker);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<AnimatePresence>
|
||||
{ landingViewVisible &&
|
||||
<motion.div
|
||||
initial={ { opacity: 0 }}
|
||||
animate={ { opacity: 1 }}
|
||||
exit={ { opacity: 0 }}>
|
||||
<HotelView />
|
||||
</motion.div> }
|
||||
</AnimatePresence>
|
||||
<ToolbarView isInRoom={ !landingViewVisible } />
|
||||
<ModToolsView />
|
||||
<RoomView />
|
||||
<ChatHistoryView />
|
||||
<WiredView />
|
||||
<AvatarEditorView />
|
||||
<AchievementsView />
|
||||
<NavigatorView />
|
||||
<NitrobubbleHiddenView />
|
||||
<InventoryView />
|
||||
<CatalogView />
|
||||
<FriendsView />
|
||||
<RightSideView />
|
||||
<UserSettingsView />
|
||||
<UserProfileView />
|
||||
<GroupsView />
|
||||
<CameraWidgetView />
|
||||
<HelpView />
|
||||
<NitropediaView />
|
||||
<GuideToolView />
|
||||
<HcCenterView />
|
||||
<CampaignView />
|
||||
<GameCenterView />
|
||||
<FloorplanEditorView />
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,19 @@
|
||||
import { AchievementData } from '@nitrots/nitro-renderer';
|
||||
import { FC } from 'react';
|
||||
import { AchievementUtilities } from '../../api';
|
||||
import { BaseProps, LayoutBadgeImageView } from '../../common';
|
||||
|
||||
interface AchievementBadgeViewProps extends BaseProps<HTMLDivElement>
|
||||
{
|
||||
achievement: AchievementData;
|
||||
scale?: number;
|
||||
}
|
||||
|
||||
export const AchievementBadgeView: FC<AchievementBadgeViewProps> = props =>
|
||||
{
|
||||
const { achievement = null, scale = 1, ...rest } = props;
|
||||
|
||||
if(!achievement) return null;
|
||||
|
||||
return <LayoutBadgeImageView badgeCode={ AchievementUtilities.getAchievementBadgeCode(achievement) } isGrayscale={ !AchievementUtilities.getAchievementHasStarted(achievement) } scale={ scale } { ...rest } />;
|
||||
};
|
||||
@@ -0,0 +1,42 @@
|
||||
import { FC, useEffect } from 'react';
|
||||
import { AchievementCategory } from '../../api';
|
||||
import { Column } from '../../common';
|
||||
import { useAchievements } from '../../hooks';
|
||||
import { AchievementDetailsView } from './AchievementDetailsView';
|
||||
import { AchievementListView } from './achievement-list';
|
||||
|
||||
interface AchievementCategoryViewProps {
|
||||
category: AchievementCategory;
|
||||
}
|
||||
|
||||
export const AchievementCategoryView: FC<AchievementCategoryViewProps> = (
|
||||
props,
|
||||
) =>
|
||||
{
|
||||
const { category = null } = props;
|
||||
const { selectedAchievement = null, setSelectedAchievementId = null } =
|
||||
useAchievements();
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
if(!category) return;
|
||||
|
||||
if(!selectedAchievement)
|
||||
{
|
||||
setSelectedAchievementId(
|
||||
category?.achievements?.[0]?.achievementId,
|
||||
);
|
||||
}
|
||||
}, [category, selectedAchievement, setSelectedAchievementId]);
|
||||
|
||||
if(!category) return null;
|
||||
|
||||
return (
|
||||
<Column fullHeight justifyContent="between">
|
||||
<AchievementListView achievements={category.achievements} />
|
||||
{!!selectedAchievement && (
|
||||
<AchievementDetailsView achievement={selectedAchievement} />
|
||||
)}
|
||||
</Column>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,53 @@
|
||||
import { AchievementData } from '@nitrots/nitro-renderer';
|
||||
import { FC } from 'react';
|
||||
import { AchievementUtilities, LocalizeBadgeDescription, LocalizeBadgeName, LocalizeText } from '../../api';
|
||||
import { Column, Flex, LayoutCurrencyIcon, LayoutProgressBar, Text } from '../../common';
|
||||
import { AchievementBadgeView } from './AchievementBadgeView';
|
||||
|
||||
interface AchievementDetailsViewProps
|
||||
{
|
||||
achievement: AchievementData;
|
||||
}
|
||||
|
||||
export const AchievementDetailsView: FC<AchievementDetailsViewProps> = props =>
|
||||
{
|
||||
const { achievement = null } = props;
|
||||
|
||||
if(!achievement) return null;
|
||||
|
||||
return (
|
||||
<Flex shrink className="bg-muted rounded p-2 text-black" gap={ 2 } overflow="hidden">
|
||||
<Column center gap={ 1 }>
|
||||
<AchievementBadgeView achievement={ achievement } className="nitro-achievements-relative w-[40px] h-[40px] bg-no-repeat bg-center" scale={ 2 } />
|
||||
<Text fontWeight="bold">
|
||||
{ LocalizeText('achievements.details.level', [ 'level', 'limit' ], [ AchievementUtilities.getAchievementLevel(achievement).toString(), achievement.levelCount.toString() ]) }
|
||||
</Text>
|
||||
</Column>
|
||||
<Column fullWidth justifyContent="center" overflow="hidden">
|
||||
<div className="flex flex-col gap-1">
|
||||
<Text truncate fontWeight="bold">
|
||||
{ LocalizeBadgeName(AchievementUtilities.getAchievementBadgeCode(achievement)) }
|
||||
</Text>
|
||||
<Text textBreak>
|
||||
{ LocalizeBadgeDescription(AchievementUtilities.getAchievementBadgeCode(achievement)) }
|
||||
</Text>
|
||||
</div>
|
||||
{ ((achievement.levelRewardPoints > 0) || (achievement.scoreLimit > 0)) &&
|
||||
<div className="flex flex-col gap-1">
|
||||
{ (achievement.levelRewardPoints > 0) &&
|
||||
<div className="flex items-center gap-1">
|
||||
<Text truncate className="small">
|
||||
{ LocalizeText('achievements.details.reward') }
|
||||
</Text>
|
||||
<Flex center className="font-bold small" gap={ 1 }>
|
||||
{ achievement.levelRewardPoints }
|
||||
<LayoutCurrencyIcon type={ achievement.levelRewardPointType } />
|
||||
</Flex>
|
||||
</div> }
|
||||
{ (achievement.scoreLimit > 0) &&
|
||||
<LayoutProgressBar maxProgress={ (achievement.scoreLimit + achievement.scoreAtStartOfLevel) } progress={ (achievement.currentPoints + achievement.scoreAtStartOfLevel) } text={ LocalizeText('achievements.details.progress', [ 'progress', 'limit' ], [ (achievement.currentPoints + achievement.scoreAtStartOfLevel).toString(), (achievement.scoreLimit + achievement.scoreAtStartOfLevel).toString() ]) } /> }
|
||||
</div> }
|
||||
</Column>
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,143 @@
|
||||
import
|
||||
{
|
||||
AddLinkEventTracker,
|
||||
ILinkEventTracker,
|
||||
RemoveLinkEventTracker,
|
||||
} from '@nitrots/nitro-renderer';
|
||||
import { FC, useEffect, useState } from 'react';
|
||||
import { AchievementUtilities, LocalizeText } from '../../api';
|
||||
import { Column, LayoutImage, LayoutProgressBar, Text } from '../../common';
|
||||
import { useAchievements } from '../../hooks';
|
||||
import { NitroCard } from '../../layout';
|
||||
import { AchievementCategoryView } from './AchievementCategoryView';
|
||||
import { AchievementsCategoryListView } from './category-list';
|
||||
|
||||
export const AchievementsView: FC<{}> = (props) =>
|
||||
{
|
||||
const [isVisible, setIsVisible] = useState(false);
|
||||
const {
|
||||
achievementCategories = [],
|
||||
selectedCategoryCode = null,
|
||||
setSelectedCategoryCode = null,
|
||||
achievementScore = 0,
|
||||
getProgress = 0,
|
||||
getMaxProgress = 0,
|
||||
selectedCategory = null,
|
||||
} = useAchievements();
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
const linkTracker: ILinkEventTracker = {
|
||||
linkReceived: (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;
|
||||
}
|
||||
},
|
||||
eventUrlPrefix: 'achievements/',
|
||||
};
|
||||
|
||||
AddLinkEventTracker(linkTracker);
|
||||
|
||||
return () => RemoveLinkEventTracker(linkTracker);
|
||||
}, []);
|
||||
|
||||
if(!isVisible) return null;
|
||||
|
||||
return (
|
||||
<NitroCard className="w-[375px] h-[405px]" uniqueKey="achievements">
|
||||
<NitroCard.Header
|
||||
headerText={LocalizeText('inventory.achievements')}
|
||||
onCloseClick={(event) => setIsVisible(false)}
|
||||
/>
|
||||
{selectedCategory && (
|
||||
<div className="relative flex items-center justify-center gap-3 p-1 cursor-pointer container-fluid bg-muted">
|
||||
<div
|
||||
className="bg-[url('@/assets/images/achievements/back-arrow.png')] bg-center no-repeat w-[33px] h-[34px]"
|
||||
onClick={(event) => setSelectedCategoryCode(null)}
|
||||
/>
|
||||
<Column className="!flex-grow" gap={0}>
|
||||
<Text
|
||||
className="text-small"
|
||||
fontSize={4}
|
||||
fontWeight="bold"
|
||||
>
|
||||
{LocalizeText(
|
||||
`quests.${selectedCategory.code}.name`
|
||||
)}
|
||||
</Text>
|
||||
<Text>
|
||||
{LocalizeText(
|
||||
'achievements.details.categoryprogress',
|
||||
['progress', 'limit'],
|
||||
[
|
||||
selectedCategory.getProgress().toString(),
|
||||
selectedCategory
|
||||
.getMaxProgress()
|
||||
.toString(),
|
||||
]
|
||||
)}
|
||||
</Text>
|
||||
</Column>
|
||||
<LayoutImage
|
||||
imageUrl={AchievementUtilities.getAchievementCategoryImageUrl(
|
||||
selectedCategory,
|
||||
null,
|
||||
true
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<NitroCard.Content>
|
||||
{!selectedCategory && (
|
||||
<>
|
||||
<AchievementsCategoryListView
|
||||
categories={achievementCategories}
|
||||
selectedCategoryCode={selectedCategoryCode}
|
||||
setSelectedCategoryCode={setSelectedCategoryCode}
|
||||
/>
|
||||
<div
|
||||
className="flex flex-col justify-end flex-grow gap-1"
|
||||
>
|
||||
<Text center small>
|
||||
{LocalizeText(
|
||||
'achievements.categories.score',
|
||||
['score'],
|
||||
[achievementScore.toString()]
|
||||
)}
|
||||
</Text>
|
||||
<LayoutProgressBar
|
||||
maxProgress={getMaxProgress}
|
||||
progress={getProgress}
|
||||
text={LocalizeText(
|
||||
'achievements.categories.totalprogress',
|
||||
['progress', 'limit'],
|
||||
[
|
||||
getProgress.toString(),
|
||||
getMaxProgress.toString(),
|
||||
]
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{selectedCategory && (
|
||||
<AchievementCategoryView category={selectedCategory} />
|
||||
)}
|
||||
</NitroCard.Content>
|
||||
</NitroCard>
|
||||
);
|
||||
};
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
import { AchievementData } from '@nitrots/nitro-renderer';
|
||||
import { FC } from 'react';
|
||||
import { LayoutGridItem } from '../../../common';
|
||||
import { useAchievements } from '../../../hooks';
|
||||
import { AchievementBadgeView } from '../AchievementBadgeView';
|
||||
|
||||
interface AchievementListItemViewProps
|
||||
{
|
||||
achievement: AchievementData;
|
||||
}
|
||||
|
||||
export const AchievementListItemView: FC<AchievementListItemViewProps> = props =>
|
||||
{
|
||||
const { achievement = null } = props;
|
||||
const { selectedAchievement = null, setSelectedAchievementId = null } = useAchievements();
|
||||
|
||||
if(!achievement) return null;
|
||||
|
||||
return (
|
||||
<LayoutGridItem itemActive={ (selectedAchievement === achievement) } itemUnseen={ (achievement.unseen > 0) } onClick={ event => setSelectedAchievementId(achievement.achievementId) }>
|
||||
<AchievementBadgeView achievement={ achievement } />
|
||||
</LayoutGridItem>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,20 @@
|
||||
import { AchievementData } from '@nitrots/nitro-renderer';
|
||||
import { FC } from 'react';
|
||||
import { AutoGrid } from '../../../common';
|
||||
import { AchievementListItemView } from './AchievementListItemView';
|
||||
|
||||
interface AchievementListViewProps
|
||||
{
|
||||
achievements: AchievementData[];
|
||||
}
|
||||
|
||||
export const AchievementListView: FC<AchievementListViewProps> = props =>
|
||||
{
|
||||
const { achievements = null } = props;
|
||||
|
||||
return (
|
||||
<AutoGrid columnCount={ 6 } columnMinHeight={ 50 } columnMinWidth={ 50 }>
|
||||
{ achievements && (achievements.length > 0) && achievements.map((achievement, index) => <AchievementListItemView key={ index } achievement={ achievement } />) }
|
||||
</AutoGrid>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from './AchievementListItemView';
|
||||
export * from './AchievementListView';
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
import { Dispatch, FC, SetStateAction } from 'react';
|
||||
import { AchievementUtilities, IAchievementCategory, LocalizeText } from '../../../api';
|
||||
import { LayoutBackgroundImage, LayoutGridItem, Text } from '../../../common';
|
||||
|
||||
interface AchievementCategoryListItemViewProps
|
||||
{
|
||||
category: IAchievementCategory;
|
||||
selectedCategoryCode: string;
|
||||
setSelectedCategoryCode: Dispatch<SetStateAction<string>>;
|
||||
}
|
||||
|
||||
export const AchievementsCategoryListItemView: FC<AchievementCategoryListItemViewProps> = props =>
|
||||
{
|
||||
const { category = null, selectedCategoryCode = null, setSelectedCategoryCode = null } = props;
|
||||
|
||||
if(!category) return null;
|
||||
|
||||
const progress = AchievementUtilities.getAchievementCategoryProgress(category);
|
||||
const maxProgress = AchievementUtilities.getAchievementCategoryMaxProgress(category);
|
||||
const getCategoryImage = AchievementUtilities.getAchievementCategoryImageUrl(category, progress);
|
||||
const getTotalUnseen = AchievementUtilities.getAchievementCategoryTotalUnseen(category);
|
||||
|
||||
return (
|
||||
<LayoutGridItem gap={ 1 } itemActive={ (selectedCategoryCode === category.code) } itemCount={ getTotalUnseen } itemCountMinimum={ 0 } onClick={ event => setSelectedCategoryCode(category.code) }>
|
||||
<Text center fullWidth small className="pt-1">{ LocalizeText(`quests.${ category.code }.name`) }</Text>
|
||||
<LayoutBackgroundImage imageUrl={ getCategoryImage } position="relative">
|
||||
<Text center fullWidth position="absolute" style={ { fontSize: 12, bottom: 9 } } variant="white">{ progress } / { maxProgress }</Text>
|
||||
</LayoutBackgroundImage>
|
||||
</LayoutGridItem>
|
||||
);
|
||||
};
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
import { Dispatch, FC, SetStateAction } from 'react';
|
||||
import { IAchievementCategory } from '../../../api';
|
||||
import { AutoGrid } from '../../../common';
|
||||
import { AchievementsCategoryListItemView } from './AchievementsCategoryListItemView';
|
||||
|
||||
interface AchievementsCategoryListViewProps
|
||||
{
|
||||
categories: IAchievementCategory[];
|
||||
selectedCategoryCode: string;
|
||||
setSelectedCategoryCode: Dispatch<SetStateAction<string>>;
|
||||
}
|
||||
|
||||
export const AchievementsCategoryListView: FC<AchievementsCategoryListViewProps> = props =>
|
||||
{
|
||||
const { categories = null, selectedCategoryCode = null, setSelectedCategoryCode = null } = props;
|
||||
|
||||
return (
|
||||
<AutoGrid columnCount={ 3 } columnMinHeight={ 100 } columnMinWidth={ 90 }>
|
||||
{ categories && (categories.length > 0) && categories.map((category, index) => <AchievementsCategoryListItemView key={ index } category={ category } selectedCategoryCode={ selectedCategoryCode } setSelectedCategoryCode={ setSelectedCategoryCode } />) }
|
||||
</AutoGrid>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from './AchievementsCategoryListItemView';
|
||||
export * from './AchievementsCategoryListView';
|
||||
@@ -0,0 +1,6 @@
|
||||
export * from './AchievementBadgeView';
|
||||
export * from './AchievementCategoryView';
|
||||
export * from './AchievementDetailsView';
|
||||
export * from './AchievementsView';
|
||||
export * from './achievement-list';
|
||||
export * from './category-list';
|
||||
@@ -0,0 +1,40 @@
|
||||
import { AvatarDirectionAngle } from '@nitrots/nitro-renderer';
|
||||
import { FC, useState } from 'react';
|
||||
import { LayoutAvatarImageView } from '../../common';
|
||||
import { useAvatarEditor } from '../../hooks';
|
||||
import { AvatarEditorIcon } from './AvatarEditorIcon';
|
||||
|
||||
const DEFAULT_DIRECTION: number = 4;
|
||||
|
||||
export const AvatarEditorFigurePreviewView: FC<{}> = props =>
|
||||
{
|
||||
const [ direction, setDirection ] = useState<number>(DEFAULT_DIRECTION);
|
||||
const { getFigureString = null } = useAvatarEditor();
|
||||
|
||||
const rotateFigure = (newDirection: number) =>
|
||||
{
|
||||
if(direction < AvatarDirectionAngle.MIN_DIRECTION)
|
||||
{
|
||||
newDirection = (AvatarDirectionAngle.MAX_DIRECTION + (direction + 1));
|
||||
}
|
||||
|
||||
if(direction > AvatarDirectionAngle.MAX_DIRECTION)
|
||||
{
|
||||
newDirection = (direction - (AvatarDirectionAngle.MAX_DIRECTION + 1));
|
||||
}
|
||||
|
||||
setDirection(newDirection);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col figure-preview-container overflow-hidden relative">
|
||||
<LayoutAvatarImageView direction={ direction } figure={ getFigureString } scale={ 2 } />
|
||||
<AvatarEditorIcon className="avatar-spotlight" icon="spotlight" />
|
||||
<div className="avatar-shadow" />
|
||||
<div className="arrow-container">
|
||||
<AvatarEditorIcon icon="arrow-left" onClick={ event => rotateFigure(direction + 1) } />
|
||||
<AvatarEditorIcon icon="arrow-right" onClick={ event => rotateFigure(direction - 1) } />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,46 @@
|
||||
import { DetailedHTMLProps, HTMLAttributes, PropsWithChildren, forwardRef } from 'react';
|
||||
import { classNames } from '../../layout';
|
||||
|
||||
type AvatarIconType = 'male' | 'female' | 'clear' | 'sellable';
|
||||
|
||||
export const AvatarEditorIcon = forwardRef<HTMLDivElement, PropsWithChildren<{
|
||||
icon: AvatarIconType;
|
||||
selected?: boolean;
|
||||
}> & DetailedHTMLProps<HTMLAttributes<HTMLDivElement>, HTMLDivElement>>((props, ref) =>
|
||||
{
|
||||
const { icon = null, selected = false, className = null, ...rest } = props;
|
||||
|
||||
/*
|
||||
switch (icon)
|
||||
{
|
||||
case 'male':
|
||||
|
||||
|
||||
break;
|
||||
|
||||
case 'arrow-left':
|
||||
|
||||
break;
|
||||
|
||||
default:
|
||||
//statements;
|
||||
break;
|
||||
|
||||
}
|
||||
*/
|
||||
return (
|
||||
<div
|
||||
ref={ ref }
|
||||
|
||||
className={ classNames(
|
||||
'nitro-avatar-editor-spritesheet',
|
||||
'cursor-pointer',
|
||||
`${ icon }-icon`,
|
||||
selected && 'selected',
|
||||
className
|
||||
) }
|
||||
{ ...rest } />
|
||||
);
|
||||
});
|
||||
|
||||
AvatarEditorIcon.displayName = 'AvatarEditorIcon';
|
||||
@@ -0,0 +1,80 @@
|
||||
import { AvatarEditorFigureCategory, AvatarFigurePartType, FigureDataContainer } from '@nitrots/nitro-renderer';
|
||||
import { FC, useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { IAvatarEditorCategory } from '../../api';
|
||||
import { useAvatarEditor } from '../../hooks';
|
||||
import { AvatarEditorIcon } from './AvatarEditorIcon';
|
||||
import { AvatarEditorFigureSetView } from './figure-set';
|
||||
import { AvatarEditorPaletteSetView } from './palette-set';
|
||||
|
||||
export const AvatarEditorModelView: FC<{
|
||||
name: string,
|
||||
categories: IAvatarEditorCategory[]
|
||||
}> = props =>
|
||||
{
|
||||
const { name = '', categories = [] } = props;
|
||||
const [ didChange, setDidChange ] = useState<boolean>(false);
|
||||
const [ activeSetType, setActiveSetType ] = useState<string>('');
|
||||
const { maxPaletteCount = 1, gender = null, setGender = null, selectedColorParts = null, getFirstSelectableColor = null, selectEditorColor = null } = useAvatarEditor();
|
||||
|
||||
const activeCategory = useMemo(() =>
|
||||
{
|
||||
return categories.find(category => category.setType === activeSetType) ?? null;
|
||||
}, [ categories, activeSetType ]);
|
||||
|
||||
const selectSet = useCallback((setType: string) =>
|
||||
{
|
||||
const selectedPalettes = selectedColorParts[setType];
|
||||
|
||||
if(!selectedPalettes || !selectedPalettes.length) selectEditorColor(setType, 0, getFirstSelectableColor(setType));
|
||||
|
||||
setActiveSetType(setType);
|
||||
}, [ getFirstSelectableColor, selectEditorColor, selectedColorParts ]);
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
if(!categories || !categories.length || !didChange) return;
|
||||
|
||||
selectSet(categories[0]?.setType);
|
||||
setDidChange(false);
|
||||
}, [ categories, didChange, selectSet ]);
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
setDidChange(true);
|
||||
}, [ categories ]);
|
||||
|
||||
if(!activeCategory) return null;
|
||||
|
||||
return (
|
||||
<div className="grid grid-cols-12 gap-2 overflow-hidden">
|
||||
<div className="flex flex-col col-span-2">
|
||||
{ (name === AvatarEditorFigureCategory.GENERIC) &&
|
||||
<>
|
||||
<div className="category-item items-center justify-center cursor-pointer flex" onClick={ event => setGender(AvatarFigurePartType.MALE) }>
|
||||
<AvatarEditorIcon icon="male" selected={ gender === FigureDataContainer.MALE } />
|
||||
</div>
|
||||
<div className="category-item items-center justify-center cursor-pointer flex" onClick={ event => setGender(AvatarFigurePartType.FEMALE) }>
|
||||
<AvatarEditorIcon icon="female" selected={ gender === FigureDataContainer.FEMALE } />
|
||||
</div>
|
||||
</> }
|
||||
{ (name !== AvatarEditorFigureCategory.GENERIC) && (categories.length > 0) && categories.map(category =>
|
||||
{
|
||||
return (
|
||||
<div key={ category.setType } className="category-item items-center justify-center cursor-pointer flex" onClick={ event => selectSet(category.setType) }>
|
||||
<AvatarEditorIcon icon={ category.setType } selected={ (activeSetType === category.setType) } />
|
||||
</div>
|
||||
);
|
||||
}) }
|
||||
</div>
|
||||
<div className="flex flex-col overflow-hidden col-span-5">
|
||||
<AvatarEditorFigureSetView category={ activeCategory } columnCount={ 3 } />
|
||||
</div>
|
||||
<div className="flex flex-col overflow-hidden col-span-5">
|
||||
{ (maxPaletteCount >= 1) &&
|
||||
<AvatarEditorPaletteSetView category={ activeCategory } columnCount={ 5 } paletteIndex={ 0 } /> }
|
||||
{ (maxPaletteCount === 2) &&
|
||||
<AvatarEditorPaletteSetView category={ activeCategory } columnCount={ 5 } paletteIndex={ 1 } /> }
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,122 @@
|
||||
import { AddLinkEventTracker, AvatarEditorFigureCategory, GetSessionDataManager, ILinkEventTracker, RemoveLinkEventTracker, UserFigureComposer } from '@nitrots/nitro-renderer';
|
||||
import { FC, useEffect, useState } from 'react';
|
||||
import { FaDice, FaRedo, FaTrash } from 'react-icons/fa';
|
||||
import { AvatarEditorAction, LocalizeText, SendMessageComposer } from '../../api';
|
||||
import { Button, Grid, NitroCardContentView, NitroCardHeaderView, NitroCardTabsItemView, NitroCardTabsView, NitroCardView } from '../../common';
|
||||
import { useAvatarEditor } from '../../hooks';
|
||||
import { AvatarEditorFigurePreviewView } from './AvatarEditorFigurePreviewView';
|
||||
import { AvatarEditorModelView } from './AvatarEditorModelView';
|
||||
import { AvatarEditorWardrobeView } from './AvatarEditorWardrobeView';
|
||||
|
||||
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';
|
||||
|
||||
export const AvatarEditorView: FC<{}> = props =>
|
||||
{
|
||||
const [ isVisible, setIsVisible ] = useState(false);
|
||||
const { setIsVisible: setEditorVisibility, avatarModels, activeModelKey, setActiveModelKey, loadAvatarData, getFigureStringWithFace, gender, figureSetIds = [], randomizeCurrentFigure = null, getFigureString = null } = useAvatarEditor();
|
||||
|
||||
const processAction = (action: string) =>
|
||||
{
|
||||
switch(action)
|
||||
{
|
||||
case AvatarEditorAction.ACTION_RESET:
|
||||
loadAvatarData(GetSessionDataManager().figure, GetSessionDataManager().gender);
|
||||
return;
|
||||
case AvatarEditorAction.ACTION_CLEAR:
|
||||
loadAvatarData(getFigureStringWithFace(0, false), gender);
|
||||
return;
|
||||
case AvatarEditorAction.ACTION_RANDOMIZE:
|
||||
randomizeCurrentFigure();
|
||||
return;
|
||||
case AvatarEditorAction.ACTION_SAVE:
|
||||
SendMessageComposer(new UserFigureComposer(gender, getFigureString));
|
||||
setIsVisible(false);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
const linkTracker: ILinkEventTracker = {
|
||||
linkReceived: (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;
|
||||
}
|
||||
},
|
||||
eventUrlPrefix: 'avatar-editor/'
|
||||
};
|
||||
|
||||
AddLinkEventTracker(linkTracker);
|
||||
|
||||
return () => RemoveLinkEventTracker(linkTracker);
|
||||
}, []);
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
setEditorVisibility(isVisible);
|
||||
}, [ isVisible, setEditorVisibility ]);
|
||||
|
||||
if(!isVisible) return null;
|
||||
|
||||
return (
|
||||
<NitroCardView className="w-[620px] h-[374px] nitro-avatar-editor" uniqueKey="avatar-editor">
|
||||
<NitroCardHeaderView headerText={ LocalizeText('avatareditor.title') } onCloseClick={ event => setIsVisible(false) } />
|
||||
<NitroCardTabsView>
|
||||
{ Object.keys(avatarModels).map(modelKey =>
|
||||
{
|
||||
const isActive = (activeModelKey === modelKey);
|
||||
|
||||
return (
|
||||
<NitroCardTabsItemView key={ modelKey } isActive={ isActive } onClick={ event => setActiveModelKey(modelKey) }>
|
||||
{ LocalizeText(`avatareditor.category.${ modelKey }`) }
|
||||
</NitroCardTabsItemView>
|
||||
);
|
||||
}) }
|
||||
</NitroCardTabsView>
|
||||
<NitroCardContentView>
|
||||
<Grid className="grid gap-2 overflow-hidden">
|
||||
<div className="flex flex-col col-span-9 overflow-hidden">
|
||||
{ ((activeModelKey.length > 0) && (activeModelKey !== AvatarEditorFigureCategory.WARDROBE)) &&
|
||||
<AvatarEditorModelView categories={ avatarModels[activeModelKey] } name={ activeModelKey } /> }
|
||||
{ (activeModelKey === AvatarEditorFigureCategory.WARDROBE) &&
|
||||
<AvatarEditorWardrobeView /> }
|
||||
</div>
|
||||
<div className="flex flex-col col-span-3 overflow-hidden gap-1">
|
||||
<AvatarEditorFigurePreviewView />
|
||||
<div className="flex flex-col !flex-grow gap-1">
|
||||
<div className="relative inline-flex align-middle">
|
||||
<Button className="flex-auto " variant="secondary" onClick={ event => processAction(AvatarEditorAction.ACTION_RESET) }>
|
||||
<FaRedo className="fa-icon" />
|
||||
</Button>
|
||||
<Button className="flex-auto" variant="secondary" onClick={ event => processAction(AvatarEditorAction.ACTION_CLEAR) }>
|
||||
<FaTrash className="fa-icon" />
|
||||
</Button>
|
||||
<Button className="flex-auto" variant="secondary" onClick={ event => processAction(AvatarEditorAction.ACTION_RANDOMIZE) }>
|
||||
<FaDice className="fa-icon" />
|
||||
</Button>
|
||||
</div>
|
||||
<Button className="w-full" variant="success" onClick={ event => processAction(AvatarEditorAction.ACTION_SAVE) }>
|
||||
{ LocalizeText('avatareditor.save') }
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Grid>
|
||||
</NitroCardContentView>
|
||||
</NitroCardView>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,61 @@
|
||||
import { GetAvatarRenderManager, IAvatarFigureContainer, SaveWardrobeOutfitMessageComposer } from '@nitrots/nitro-renderer';
|
||||
import { FC, useCallback } from 'react';
|
||||
import { GetClubMemberLevel, GetConfigurationValue, LocalizeText, SendMessageComposer } from '../../api';
|
||||
import { Button, LayoutAvatarImageView, LayoutCurrencyIcon } from '../../common';
|
||||
import { useAvatarEditor } from '../../hooks';
|
||||
import { InfiniteGrid } from '../../layout';
|
||||
|
||||
export const AvatarEditorWardrobeView: FC<{}> = props =>
|
||||
{
|
||||
const { savedFigures = [], setSavedFigures = null, loadAvatarData = null, getFigureString = null, gender = null } = useAvatarEditor();
|
||||
|
||||
const hcDisabled = GetConfigurationValue<boolean>('hc.disabled', false);
|
||||
|
||||
const wearFigureAtIndex = useCallback((index: number) =>
|
||||
{
|
||||
if((index >= savedFigures.length) || (index < 0)) return;
|
||||
|
||||
const [ figure, gender ] = savedFigures[index];
|
||||
|
||||
loadAvatarData(figure.getFigureString(), gender);
|
||||
}, [ savedFigures, loadAvatarData ]);
|
||||
|
||||
const saveFigureAtWardrobeIndex = useCallback((index: number) =>
|
||||
{
|
||||
if((index >= savedFigures.length) || (index < 0)) return;
|
||||
|
||||
const newFigures = [ ...savedFigures ];
|
||||
|
||||
const figure = getFigureString;
|
||||
|
||||
newFigures[index] = [ GetAvatarRenderManager().createFigureContainer(figure), gender ];
|
||||
|
||||
setSavedFigures(newFigures);
|
||||
SendMessageComposer(new SaveWardrobeOutfitMessageComposer((index + 1), figure, gender));
|
||||
}, [ getFigureString, gender, savedFigures, setSavedFigures ]);
|
||||
|
||||
return (
|
||||
<InfiniteGrid columnCount={ 5 } estimateSize={ 140 } itemRender={ (item: [ IAvatarFigureContainer, string ], index: number) =>
|
||||
{
|
||||
const [ figureContainer, gender ] = item;
|
||||
|
||||
let clubLevel = 0;
|
||||
|
||||
if(figureContainer) clubLevel = GetAvatarRenderManager().getFigureClubLevel(figureContainer, gender);
|
||||
|
||||
return (
|
||||
<InfiniteGrid.Item className="nitro-avatar-editor-wardrobe-figure-preview">
|
||||
{ figureContainer &&
|
||||
<LayoutAvatarImageView direction={ 2 } figure={ figureContainer.getFigureString() } gender={ gender } /> }
|
||||
<div className="avatar-shadow" />
|
||||
{ !hcDisabled && (clubLevel > 0) && <LayoutCurrencyIcon className="absolute top-1 start-1" type="hc" /> }
|
||||
<div className="flex gap-1 button-container">
|
||||
<Button fullWidth variant="link" onClick={ event => saveFigureAtWardrobeIndex(index) }>{ LocalizeText('avatareditor.wardrobe.save') }</Button>
|
||||
{ figureContainer &&
|
||||
<Button fullWidth disabled={ (clubLevel > GetClubMemberLevel()) } variant="link" onClick={ event => wearFigureAtIndex(index) }>{ LocalizeText('widget.generic_usable.button.use') }</Button> }
|
||||
</div>
|
||||
</InfiniteGrid.Item>
|
||||
);
|
||||
} } items={ savedFigures } overscan={ 5 } />
|
||||
);
|
||||
};
|
||||
+56
@@ -0,0 +1,56 @@
|
||||
import { AvatarFigurePartType } from '@nitrots/nitro-renderer';
|
||||
import { FC, useEffect, useState } from 'react';
|
||||
import { AvatarEditorThumbnailsHelper, GetConfigurationValue, IAvatarEditorCategoryPartItem } from '../../../api';
|
||||
import { LayoutCurrencyIcon, LayoutGridItemProps } from '../../../common';
|
||||
import { useAvatarEditor } from '../../../hooks';
|
||||
import { InfiniteGrid } from '../../../layout';
|
||||
import { AvatarEditorIcon } from '../AvatarEditorIcon';
|
||||
|
||||
export const AvatarEditorFigureSetItemView: FC<{
|
||||
setType: string;
|
||||
partItem: IAvatarEditorCategoryPartItem;
|
||||
isSelected: boolean;
|
||||
width?: string;
|
||||
} & LayoutGridItemProps> = props =>
|
||||
{
|
||||
const { setType = null, partItem = null, isSelected = false, width = '100%', ...rest } = props;
|
||||
const [ assetUrl, setAssetUrl ] = useState<string>('');
|
||||
const { selectedColorParts = null, getFigureStringWithFace = null } = useAvatarEditor();
|
||||
|
||||
const isHC = !GetConfigurationValue<boolean>('hc.disabled', false) && ((partItem.partSet?.clubLevel ?? 0) > 0);
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
if(!setType || !setType.length || !partItem) return;
|
||||
|
||||
const loadImage = async () =>
|
||||
{
|
||||
const isHC = !GetConfigurationValue<boolean>('hc.disabled', false) && ((partItem.partSet?.clubLevel ?? 0) > 0);
|
||||
|
||||
let url: string = null;
|
||||
|
||||
if(setType === AvatarFigurePartType.HEAD)
|
||||
{
|
||||
url = await AvatarEditorThumbnailsHelper.buildForFace(getFigureStringWithFace(partItem.id), isHC);
|
||||
}
|
||||
else
|
||||
{
|
||||
url = await AvatarEditorThumbnailsHelper.build(setType, partItem, partItem.usesColor, selectedColorParts[setType] ?? null, isHC);
|
||||
}
|
||||
|
||||
if(url && url.length) setAssetUrl(url);
|
||||
};
|
||||
|
||||
loadImage();
|
||||
}, [ setType, partItem, selectedColorParts, getFigureStringWithFace ]);
|
||||
|
||||
if(!partItem) return null;
|
||||
|
||||
return (
|
||||
<InfiniteGrid.Item itemActive={ isSelected } itemImage={ (partItem.isClear ? undefined : assetUrl) } style={ { flex: '1', backgroundPosition: (setType === AvatarFigurePartType.HEAD) ? 'center -35px' : 'center' } } { ...rest }>
|
||||
{ !partItem.isClear && isHC && <LayoutCurrencyIcon className="absolute end-1 bottom-1" type="hc" /> }
|
||||
{ partItem.isClear && <AvatarEditorIcon icon="clear" /> }
|
||||
{ !partItem.isClear && partItem.partSet.isSellable && <AvatarEditorIcon className="end-1 bottom-1 absolute" icon="sellable" /> }
|
||||
</InfiniteGrid.Item>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,41 @@
|
||||
import { FC } from 'react';
|
||||
import { IAvatarEditorCategory, IAvatarEditorCategoryPartItem } from '../../../api';
|
||||
import { useAvatarEditor } from '../../../hooks';
|
||||
import { InfiniteGrid } from '../../../layout';
|
||||
import { AvatarEditorFigureSetItemView } from './AvatarEditorFigureSetItemView';
|
||||
|
||||
export const AvatarEditorFigureSetView: FC<{
|
||||
category: IAvatarEditorCategory;
|
||||
columnCount: number;
|
||||
}> = props =>
|
||||
{
|
||||
const { category = null, columnCount = 3 } = props;
|
||||
const { selectedParts = null, selectEditorPart } = useAvatarEditor();
|
||||
|
||||
const isPartItemSelected = (partItem: IAvatarEditorCategoryPartItem) =>
|
||||
{
|
||||
if(!category || !category.setType || !selectedParts) return false;
|
||||
|
||||
if(!selectedParts[category.setType])
|
||||
{
|
||||
if(partItem.isClear) return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
const partId = selectedParts[category.setType];
|
||||
|
||||
return (partId === partItem.id);
|
||||
};
|
||||
|
||||
return (
|
||||
<InfiniteGrid<IAvatarEditorCategoryPartItem> columnCount={ columnCount } itemRender={ (item: IAvatarEditorCategoryPartItem) =>
|
||||
{
|
||||
if(!item) return null;
|
||||
|
||||
return (
|
||||
<AvatarEditorFigureSetItemView isSelected={ isPartItemSelected(item) } partItem={ item } setType={ category.setType } width={ `calc(100% / ${ columnCount }` } onClick={ event => selectEditorPart(category.setType, item.partSet?.id ?? -1) } />
|
||||
);
|
||||
} } items={ category.partItems } overscan={ columnCount } />
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from './AvatarEditorFigureSetItemView';
|
||||
export * from './AvatarEditorFigureSetView';
|
||||
@@ -0,0 +1,7 @@
|
||||
export * from './AvatarEditorFigurePreviewView';
|
||||
export * from './AvatarEditorIcon';
|
||||
export * from './AvatarEditorModelView';
|
||||
export * from './AvatarEditorView';
|
||||
export * from './AvatarEditorWardrobeView';
|
||||
export * from './figure-set';
|
||||
export * from './palette-set';
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
import { ColorConverter, IPartColor } from '@nitrots/nitro-renderer';
|
||||
import { FC } from 'react';
|
||||
import { GetConfigurationValue } from '../../../api';
|
||||
import { LayoutCurrencyIcon, LayoutGridItemProps } from '../../../common';
|
||||
import { InfiniteGrid } from '../../../layout';
|
||||
|
||||
export const AvatarEditorPaletteSetItem: FC<{
|
||||
setType: string;
|
||||
partColor: IPartColor;
|
||||
isSelected: boolean;
|
||||
width?: string;
|
||||
} & LayoutGridItemProps> = props =>
|
||||
{
|
||||
const { setType = null, partColor = null, isSelected = false, width = '100%', ...rest } = props;
|
||||
|
||||
if(!partColor) return null;
|
||||
|
||||
const isHC = !GetConfigurationValue<boolean>('hc.disabled', false) && (partColor.clubLevel > 0);
|
||||
|
||||
return (
|
||||
<InfiniteGrid.Item itemHighlight className="clear-bg" itemActive={ isSelected } itemColor={ ColorConverter.int2rgb(partColor.rgb) } { ...rest }>
|
||||
{ isHC && <LayoutCurrencyIcon className="absolute end-1 bottom-1" type="hc" /> }
|
||||
</InfiniteGrid.Item>
|
||||
);
|
||||
};
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
import { IPartColor } from '@nitrots/nitro-renderer';
|
||||
import { FC } from 'react';
|
||||
import { IAvatarEditorCategory } from '../../../api';
|
||||
import { useAvatarEditor } from '../../../hooks';
|
||||
import { InfiniteGrid } from '../../../layout';
|
||||
import { AvatarEditorPaletteSetItem } from './AvatarEditorPaletteSetItemView';
|
||||
|
||||
export const AvatarEditorPaletteSetView: FC<{
|
||||
category: IAvatarEditorCategory;
|
||||
paletteIndex: number;
|
||||
columnCount: number;
|
||||
}> = props =>
|
||||
{
|
||||
const { category = null, paletteIndex = -1, columnCount = 3 } = props;
|
||||
const { selectedColorParts = null, selectEditorColor = null } = useAvatarEditor();
|
||||
|
||||
const isPartColorSelected = (partColor: IPartColor) =>
|
||||
{
|
||||
if(!category || !category.setType || !selectedColorParts || !selectedColorParts[category.setType] || !selectedColorParts[category.setType][paletteIndex]) return false;
|
||||
|
||||
const selectedColorPart = selectedColorParts[category.setType][paletteIndex];
|
||||
|
||||
return (selectedColorPart.id === partColor.id);
|
||||
};
|
||||
|
||||
return (
|
||||
<InfiniteGrid<IPartColor> columnCount={ columnCount } itemRender={ (item: IPartColor) =>
|
||||
{
|
||||
if(!item) return null;
|
||||
|
||||
return (
|
||||
<AvatarEditorPaletteSetItem isSelected={ isPartColorSelected(item) } partColor={ item } setType={ category.setType } width={ `calc(100% / ${ columnCount }` } onClick={ event => selectEditorColor(category.setType, paletteIndex, item.id) } />
|
||||
);
|
||||
} } items={ category.colorItems[paletteIndex] } overscan={ columnCount } />
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from './AvatarEditorPaletteSetItemView';
|
||||
export * from './AvatarEditorPaletteSetView';
|
||||
@@ -0,0 +1,111 @@
|
||||
import { GetSessionDataManager, HabboClubLevelEnum} from '@nitrots/nitro-renderer';
|
||||
import { Dispatch, FC, SetStateAction, useCallback, useMemo, useState } from 'react';
|
||||
import { Base, Grid, Flex, NitroCardView, NitroCardHeaderView, NitroCardTabsView, NitroCardTabsItemView, NitroCardContentView, Text, LayoutCurrencyIcon } from '../../common';
|
||||
import { useRoom } from '../../hooks';
|
||||
import { GetClubMemberLevel, GetConfigurationValue } from '../../api';
|
||||
|
||||
interface ItemData {
|
||||
id: number;
|
||||
isHcOnly: boolean;
|
||||
minRank: number;
|
||||
isAmbassadorOnly: boolean;
|
||||
selectable: boolean;
|
||||
}
|
||||
|
||||
interface BackgroundsViewProps {
|
||||
setIsVisible: Dispatch<SetStateAction<boolean>>;
|
||||
selectedBackground: number;
|
||||
setSelectedBackground: Dispatch<SetStateAction<number>>;
|
||||
selectedStand: number;
|
||||
setSelectedStand: Dispatch<SetStateAction<number>>;
|
||||
selectedOverlay: number;
|
||||
setSelectedOverlay: Dispatch<SetStateAction<number>>;
|
||||
}
|
||||
|
||||
const TABS = ['backgrounds', 'stands', 'overlays'] as const;
|
||||
type TabType = typeof TABS[number];
|
||||
|
||||
export const BackgroundsView: FC<BackgroundsViewProps> = ({
|
||||
setIsVisible,
|
||||
selectedBackground,
|
||||
setSelectedBackground,
|
||||
selectedStand,
|
||||
setSelectedStand,
|
||||
selectedOverlay,
|
||||
setSelectedOverlay
|
||||
}) => {
|
||||
const [activeTab, setActiveTab] = useState<TabType>('backgrounds');
|
||||
const { roomSession } = useRoom();
|
||||
|
||||
const userData = useMemo(() => ({
|
||||
isHcMember: GetClubMemberLevel() >= HabboClubLevelEnum.CLUB,
|
||||
securityLevel: GetSessionDataManager().canChangeName,
|
||||
isAmbassador: GetSessionDataManager().isAmbassador
|
||||
}), []);
|
||||
|
||||
const processData = useCallback((configData: any[], dataType: string): ItemData[] => {
|
||||
if (!configData?.length) return [];
|
||||
|
||||
return configData
|
||||
.filter(item => {
|
||||
const meetsRank = userData.securityLevel >= item.minRank;
|
||||
const ambassadorEligible = !item.isAmbassadorOnly || userData.isAmbassador;
|
||||
return item.isHcOnly || (meetsRank && ambassadorEligible);
|
||||
})
|
||||
.map(item => ({ id: item[`${dataType}Id`], ...item, selectable: !item.isHcOnly || userData.isHcMember }));
|
||||
}, [userData]);
|
||||
|
||||
const allData = useMemo(() => ({
|
||||
backgrounds: processData(GetConfigurationValue('backgrounds.data'), 'background'),
|
||||
stands: processData(GetConfigurationValue('stands.data'), 'stand'),
|
||||
overlays: processData(GetConfigurationValue('overlays.data'), 'overlay')
|
||||
}), [processData]);
|
||||
|
||||
const handleSelection = useCallback((id: number) => {
|
||||
if (!roomSession) return;
|
||||
|
||||
const setters = { backgrounds: setSelectedBackground, stands: setSelectedStand, overlays: setSelectedOverlay };
|
||||
|
||||
const currentValues = { backgrounds: selectedBackground, stands: selectedStand, overlays: selectedOverlay };
|
||||
|
||||
setters[activeTab](id);
|
||||
const newValues = { ...currentValues, [activeTab]: id };
|
||||
roomSession.sendBackgroundMessage( newValues.backgrounds, newValues.stands, newValues.overlays );
|
||||
}, [activeTab, roomSession, selectedBackground, selectedStand, selectedOverlay, setSelectedBackground, setSelectedStand, setSelectedOverlay]);
|
||||
|
||||
const renderItem = useCallback((item: ItemData, type: string) => (
|
||||
<Flex
|
||||
pointer
|
||||
position="relative"
|
||||
key={item.id}
|
||||
onClick={() => item.selectable && handleSelection(item.id)}
|
||||
className={item.selectable ? '' : 'non-selectable'}
|
||||
>
|
||||
<Base className={`profile-${type} ${type}-${item.id}`} />
|
||||
{item.isHcOnly && <LayoutCurrencyIcon position="absolute" className="top-1 end-1" type="hc" />}
|
||||
</Flex>
|
||||
), [handleSelection]);
|
||||
|
||||
return (
|
||||
<NitroCardView uniqueKey="backgrounds" className="absolute min-w-[535px] max-w-[535px] min-h-[389px] max-h-[389px]">
|
||||
<NitroCardHeaderView headerText="Profile Background" onCloseClick={() => setIsVisible(false)} />
|
||||
<NitroCardTabsView>
|
||||
{TABS.map(tab => (
|
||||
<NitroCardTabsItemView
|
||||
key={tab}
|
||||
isActive={activeTab === tab}
|
||||
onClick={() => setActiveTab(tab)}
|
||||
>
|
||||
{tab.charAt(0).toUpperCase() + tab.slice(1)}
|
||||
</NitroCardTabsItemView>
|
||||
))}
|
||||
</NitroCardTabsView>
|
||||
<NitroCardContentView gap={1}>
|
||||
<Text bold center>Select an Option</Text>
|
||||
<Grid gap={1} columnCount={7} overflow="auto">
|
||||
{allData[activeTab].map(item => renderItem(item, activeTab.slice(0, -1)))}
|
||||
</Grid>
|
||||
</NitroCardContentView>
|
||||
</NitroCardView>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,97 @@
|
||||
import { AddLinkEventTracker, ILinkEventTracker, RemoveLinkEventTracker, RoomSessionEvent } from '@nitrots/nitro-renderer';
|
||||
import { FC, useEffect, useState } from 'react';
|
||||
import { useCamera, useNitroEvent } from '../../hooks';
|
||||
import { CameraWidgetCaptureView } from './views/CameraWidgetCaptureView';
|
||||
import { CameraWidgetCheckoutView } from './views/CameraWidgetCheckoutView';
|
||||
import { CameraWidgetEditorView } from './views/editor/CameraWidgetEditorView';
|
||||
|
||||
const MODE_NONE: number = 0;
|
||||
const MODE_CAPTURE: number = 1;
|
||||
const MODE_EDITOR: number = 2;
|
||||
const MODE_CHECKOUT: number = 3;
|
||||
|
||||
export const CameraWidgetView: FC<{}> = props =>
|
||||
{
|
||||
const [ mode, setMode ] = useState<number>(MODE_NONE);
|
||||
const [ base64Url, setSavedPictureUrl ] = useState<string>(null);
|
||||
const { availableEffects = [], selectedPictureIndex = -1, cameraRoll = [], setCameraRoll = null, myLevel = 0, price = { credits: 0, duckets: 0, publishDucketPrice: 0 } } = useCamera();
|
||||
|
||||
|
||||
const processAction = (type: string) =>
|
||||
{
|
||||
switch(type)
|
||||
{
|
||||
case 'close':
|
||||
setMode(MODE_NONE);
|
||||
return;
|
||||
case 'edit':
|
||||
setMode(MODE_EDITOR);
|
||||
return;
|
||||
case 'delete':
|
||||
setCameraRoll(prevValue =>
|
||||
{
|
||||
const clone = [ ...prevValue ];
|
||||
|
||||
clone.splice(selectedPictureIndex, 1);
|
||||
|
||||
return clone;
|
||||
});
|
||||
return;
|
||||
case 'editor_cancel':
|
||||
setMode(MODE_CAPTURE);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
const checkoutPictureUrl = (pictureUrl: string) =>
|
||||
{
|
||||
setSavedPictureUrl(pictureUrl);
|
||||
setMode(MODE_CHECKOUT);
|
||||
};
|
||||
|
||||
useNitroEvent<RoomSessionEvent>(RoomSessionEvent.ENDED, event => setMode(MODE_NONE));
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
const linkTracker: ILinkEventTracker = {
|
||||
linkReceived: (url: string) =>
|
||||
{
|
||||
const parts = url.split('/');
|
||||
|
||||
if(parts.length < 2) return;
|
||||
|
||||
switch(parts[1])
|
||||
{
|
||||
case 'show':
|
||||
setMode(MODE_CAPTURE);
|
||||
return;
|
||||
case 'hide':
|
||||
setMode(MODE_NONE);
|
||||
return;
|
||||
case 'toggle':
|
||||
setMode(prevValue =>
|
||||
{
|
||||
if(!prevValue) return MODE_CAPTURE;
|
||||
else return MODE_NONE;
|
||||
});
|
||||
return;
|
||||
}
|
||||
},
|
||||
eventUrlPrefix: 'camera/'
|
||||
};
|
||||
|
||||
AddLinkEventTracker(linkTracker);
|
||||
|
||||
return () => RemoveLinkEventTracker(linkTracker);
|
||||
}, []);
|
||||
|
||||
if(mode === MODE_NONE) return null;
|
||||
|
||||
return (
|
||||
<>
|
||||
{ (mode === MODE_CAPTURE) && <CameraWidgetCaptureView onClose={ () => processAction('close') } onDelete={ () => processAction('delete') } onEdit={ () => processAction('edit') } /> }
|
||||
{ (mode === MODE_EDITOR) && <CameraWidgetEditorView availableEffects={ availableEffects } myLevel={ myLevel } picture={ cameraRoll[selectedPictureIndex] } onCancel={ () => processAction('editor_cancel') } onCheckout={ checkoutPictureUrl } onClose={ () => processAction('close') } /> }
|
||||
{ (mode === MODE_CHECKOUT) && <CameraWidgetCheckoutView base64Url={ base64Url } price={ price } onCancelClick={ () => processAction('editor_cancel') } onCloseClick={ () => processAction('close') }></CameraWidgetCheckoutView> }
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,4 @@
|
||||
export * from './CameraWidgetView';
|
||||
export * from './views';
|
||||
export * from './views/editor';
|
||||
export * from './views/editor/effect-list';
|
||||
@@ -0,0 +1,90 @@
|
||||
import { GetRoomEngine, NitroRectangle, TextureUtils } from '@nitrots/nitro-renderer';
|
||||
import { FC, useRef } from 'react';
|
||||
import { FaTimes } from 'react-icons/fa';
|
||||
import { CameraPicture, GetRoomSession, LocalizeText, PlaySound, SoundNames } from '../../../api';
|
||||
import { Button, Column, DraggableWindow } from '../../../common';
|
||||
import { useCamera, useNotification } from '../../../hooks';
|
||||
|
||||
export interface CameraWidgetCaptureViewProps
|
||||
{
|
||||
onClose: () => void;
|
||||
onEdit: () => void;
|
||||
onDelete: () => void;
|
||||
}
|
||||
|
||||
const CAMERA_ROLL_LIMIT: number = 5;
|
||||
|
||||
export const CameraWidgetCaptureView: FC<CameraWidgetCaptureViewProps> = props =>
|
||||
{
|
||||
const { onClose = null, onEdit = null, onDelete = null } = props;
|
||||
const { cameraRoll = null, setCameraRoll = null, selectedPictureIndex = -1, setSelectedPictureIndex = null } = useCamera();
|
||||
const { simpleAlert = null } = useNotification();
|
||||
const elementRef = useRef<HTMLDivElement>();
|
||||
|
||||
const selectedPicture = ((selectedPictureIndex > -1) ? cameraRoll[selectedPictureIndex] : null);
|
||||
|
||||
const getCameraBounds = () =>
|
||||
{
|
||||
if(!elementRef || !elementRef.current) return null;
|
||||
|
||||
const frameBounds = elementRef.current.getBoundingClientRect();
|
||||
|
||||
return new NitroRectangle(Math.floor(frameBounds.x), Math.floor(frameBounds.y), Math.floor(frameBounds.width), Math.floor(frameBounds.height));
|
||||
};
|
||||
|
||||
const takePicture = async () =>
|
||||
{
|
||||
if(selectedPictureIndex > -1)
|
||||
{
|
||||
setSelectedPictureIndex(-1);
|
||||
return;
|
||||
}
|
||||
|
||||
const texture = GetRoomEngine().createTextureFromRoom(GetRoomSession().roomId, 1, getCameraBounds());
|
||||
|
||||
const clone = [ ...cameraRoll ];
|
||||
|
||||
if(clone.length >= CAMERA_ROLL_LIMIT)
|
||||
{
|
||||
simpleAlert(LocalizeText('camera.full.body'));
|
||||
|
||||
clone.pop();
|
||||
}
|
||||
|
||||
PlaySound(SoundNames.CAMERA_SHUTTER);
|
||||
clone.push(new CameraPicture(texture, await TextureUtils.generateImageUrl(texture)));
|
||||
|
||||
setCameraRoll(clone);
|
||||
};
|
||||
|
||||
return (
|
||||
<DraggableWindow uniqueKey="nitro-camera-capture">
|
||||
<Column center className="relative" gap={ 0 }>
|
||||
{ selectedPicture && <img alt="" className="absolute top-[37px] left-[10px] w-[320px] h-[320px]" src={ selectedPicture.imageUrl } /> }
|
||||
<div className="relative w-[340px] h-[462px] bg-[url('@/assets/images/room-widgets/camera-widget/camera-spritesheet.png')] bg-[-1px_-1px] drag-handler">
|
||||
<div className="absolute top-[8px] right-[8px] rounded-[.25rem] [box-shadow:0_0_0_1.5px_#fff] border-[2px] border-[solid] border-[#921911] bg-[repeating-linear-gradient(rgb(245,_80,_65),_rgb(245,_80,_65)_50%,_rgb(194,_48,_39)_50%,_rgb(194,_48,_39)_100%)] cursor-pointer leading-none px-[3px] py-px" onClick={ onClose }>
|
||||
<FaTimes className="fa-icon" />
|
||||
</div>
|
||||
{ !selectedPicture && <div ref={ elementRef } className="absolute top-[37px] left-[10px] w-[320px] h-[320px] bg-[url('@/assets/images/room-widgets/camera-widget/camera-spritesheet.png')] bg-[-343px_-1px]" /> }
|
||||
{ selectedPicture &&
|
||||
<div className="absolute top-[37px] left-[10px] w-[320px] h-[320px] ">
|
||||
<div className="bg-[rgba(0,_0,_0,_0.5)] w-full absolute bottom-0 py-2 text-center inline-flex justify-center">
|
||||
<Button className="me-3" title={ LocalizeText('camera.editor.button.tooltip') } variant="success" onClick={ onEdit }>{ LocalizeText('camera.editor.button.text') }</Button>
|
||||
<Button variant="danger" onClick={ onDelete }>{ LocalizeText('camera.delete.button.text') }</Button>
|
||||
</div>
|
||||
</div> }
|
||||
<div className="flex justify-center">
|
||||
<div className="w-[94px] h-[94px] cursor-pointer mt-[362px] bg-[url('@/assets/images/room-widgets/camera-widget/camera-spritesheet.png')] bg-[-343px_-321px]" title={ LocalizeText('camera.take.photo.button.tooltip') } onClick={ takePicture } />
|
||||
</div>
|
||||
</div>
|
||||
{ (cameraRoll.length > 0) &&
|
||||
<div className="w-[330px] bg-[#bab8b4] rounded-bl-[10px] rounded-br-[10px] border-[1px] border-[solid] border-[black] [box-shadow:inset_1px_0px_white,_inset_-1px_-1px_white] flex justify-center py-2">
|
||||
{ cameraRoll.map((picture, index) =>
|
||||
{
|
||||
return <img key={ index } alt="" className="w-[56px] h-[56px] border-[1px] border-[solid] border-[black] object-contain [image-rendering:initial]" src={ picture.imageUrl } onClick={ event => setSelectedPictureIndex(index) } />;
|
||||
}) }
|
||||
</div> }
|
||||
</Column>
|
||||
</DraggableWindow>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,159 @@
|
||||
import { CameraPublishStatusMessageEvent, CameraPurchaseOKMessageEvent, CameraStorageUrlMessageEvent, CreateLinkEvent, GetRoomEngine, PublishPhotoMessageComposer, PurchasePhotoMessageComposer } from '@nitrots/nitro-renderer';
|
||||
import { FC, useEffect, useMemo, useState } from 'react';
|
||||
import { GetConfigurationValue, LocalizeText, SendMessageComposer } from '../../../api';
|
||||
import { Button, Column, LayoutCurrencyIcon, LayoutImage, NitroCardContentView, NitroCardHeaderView, NitroCardView, Text } from '../../../common';
|
||||
import { useMessageEvent } from '../../../hooks';
|
||||
|
||||
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 publishDisabled = useMemo(() => GetConfigurationValue<boolean>('camera.publish.disabled', false), []);
|
||||
|
||||
useMessageEvent<CameraPurchaseOKMessageEvent>(CameraPurchaseOKMessageEvent, event =>
|
||||
{
|
||||
setPicturesBought(value => (value + 1));
|
||||
setIsWaiting(false);
|
||||
});
|
||||
|
||||
useMessageEvent<CameraPublishStatusMessageEvent>(CameraPublishStatusMessageEvent, event =>
|
||||
{
|
||||
const parser = event.getParser();
|
||||
|
||||
setPublishUrl(parser.extraDataId);
|
||||
setPublishCooldown(parser.secondsToWait);
|
||||
setWasPicturePublished(parser.ok);
|
||||
setIsWaiting(false);
|
||||
});
|
||||
|
||||
useMessageEvent<CameraStorageUrlMessageEvent>(CameraStorageUrlMessageEvent, event =>
|
||||
{
|
||||
const parser = event.getParser();
|
||||
|
||||
setPictureUrl(GetConfigurationValue<string>('camera.url') + '/' + parser.url);
|
||||
});
|
||||
|
||||
const processAction = (type: string, value: string | number = null) =>
|
||||
{
|
||||
switch(type)
|
||||
{
|
||||
case 'close':
|
||||
onCloseClick();
|
||||
return;
|
||||
case 'buy':
|
||||
if(isWaiting) return;
|
||||
|
||||
setIsWaiting(true);
|
||||
SendMessageComposer(new PurchasePhotoMessageComposer(''));
|
||||
return;
|
||||
case 'publish':
|
||||
if(isWaiting) return;
|
||||
|
||||
setIsWaiting(true);
|
||||
SendMessageComposer(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" theme="primary-slim">
|
||||
<NitroCardHeaderView headerText={ LocalizeText('camera.confirm_phase.title') } onCloseClick={ event => processAction('close') } />
|
||||
<NitroCardContentView>
|
||||
<div className="flex items-center justify-center">
|
||||
{ (pictureUrl && pictureUrl.length) &&
|
||||
<LayoutImage className="picture-preview border" imageUrl={ pictureUrl } /> }
|
||||
{ (!pictureUrl || !pictureUrl.length) &&
|
||||
<div className="flex items-center justify-center picture-preview border">
|
||||
<Text bold>{ LocalizeText('camera.loading') }</Text>
|
||||
</div> }
|
||||
</div>
|
||||
<div className="flex items-center bg-muted rounded p-2 justify-between">
|
||||
<Column gap={ 1 } size={ publishDisabled ? 10 : 6 }>
|
||||
<Text bold>
|
||||
{ LocalizeText('camera.purchase.header') }
|
||||
</Text>
|
||||
{ ((price.credits > 0) || (price.duckets > 0)) &&
|
||||
<div className="flex gap-1">
|
||||
<Text>{ LocalizeText('catalog.purchase.confirmation.dialog.cost') }</Text>
|
||||
{ (price.credits > 0) &&
|
||||
<div className="flex gap-1">
|
||||
<Text bold>{ price.credits }</Text>
|
||||
<LayoutCurrencyIcon type={ -1 } />
|
||||
</div> }
|
||||
{ (price.duckets > 0) &&
|
||||
<div className="flex gap-1">
|
||||
<Text bold>{ price.duckets }</Text>
|
||||
<LayoutCurrencyIcon type={ 5 } />
|
||||
</div> }
|
||||
</div> }
|
||||
{ (picturesBought > 0) &&
|
||||
<Text>
|
||||
<Text bold>{ LocalizeText('camera.purchase.count.info') }</Text> { picturesBought }
|
||||
<u className="ms-1 cursor-pointer" onClick={ () => CreateLinkEvent('inventory/toggle') }>{ LocalizeText('camera.open.inventory') }</u>
|
||||
</Text> }
|
||||
</Column>
|
||||
<div className="flex items-center">
|
||||
<Button disabled={ isWaiting } variant="success" onClick={ event => processAction('buy') }>{ LocalizeText(!picturesBought ? 'buy' : 'camera.buy.another.button.text') }</Button>
|
||||
</div>
|
||||
</div>
|
||||
{ !publishDisabled &&
|
||||
<div className="flex items-center justify-between bg-muted rounded p-2">
|
||||
<div className="flex flex-col 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) &&
|
||||
<div className="flex gap-1">
|
||||
<Text>{ LocalizeText('catalog.purchase.confirmation.dialog.cost') }</Text>
|
||||
<div className="flex gap-1">
|
||||
<Text bold>{ price.publishDucketPrice }</Text>
|
||||
<LayoutCurrencyIcon type={ 5 } />
|
||||
</div>
|
||||
</div> }
|
||||
{ (publishCooldown > 0) && <div className="mt-1 text-center font-bold ">{ LocalizeText('camera.publish.wait', [ 'minutes' ], [ Math.ceil(publishCooldown / 60).toString() ]) }</div> }
|
||||
</div>
|
||||
{ !wasPicturePublished &&
|
||||
<div className="flex align-items-end">
|
||||
<Button disabled={ (isWaiting || (publishCooldown > 0)) } variant="success" onClick={ event => processAction('publish') }>
|
||||
{ LocalizeText('camera.publish.button.text') }
|
||||
</Button>
|
||||
</div> }
|
||||
</div> }
|
||||
<Text center>{ LocalizeText('camera.warning.disclaimer') }</Text>
|
||||
<div className="flex justify-end">
|
||||
<Button onClick={ event => processAction('cancel') }>{ LocalizeText('generic.cancel') }</Button>
|
||||
</div>
|
||||
</NitroCardContentView>
|
||||
</NitroCardView>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,68 @@
|
||||
import { GetRoomEngine, RoomObjectCategory, RoomObjectVariable } from '@nitrots/nitro-renderer';
|
||||
import { FC, useEffect, useState } from 'react';
|
||||
import { FaArrowLeft, FaArrowRight } from 'react-icons/fa';
|
||||
import { GetUserProfile, IPhotoData, LocalizeText } from '../../../api';
|
||||
import { Flex, Grid, Text } from '../../../common';
|
||||
|
||||
export interface CameraWidgetShowPhotoViewProps {
|
||||
currentIndex: number;
|
||||
currentPhotos: IPhotoData[];
|
||||
onClick?: () => void;
|
||||
}
|
||||
|
||||
export const CameraWidgetShowPhotoView: FC<CameraWidgetShowPhotoViewProps> = props => {
|
||||
const { currentIndex = -1, currentPhotos = null, onClick = null } = props;
|
||||
const [imageIndex, setImageIndex] = useState(0);
|
||||
|
||||
const currentImage = currentPhotos && currentPhotos.length ? currentPhotos[imageIndex] : null;
|
||||
|
||||
const next = () => {
|
||||
setImageIndex(prevValue => {
|
||||
let newIndex = prevValue + 1;
|
||||
if (newIndex >= currentPhotos.length) newIndex = 0;
|
||||
return newIndex;
|
||||
});
|
||||
};
|
||||
|
||||
const previous = () => {
|
||||
setImageIndex(prevValue => {
|
||||
let newIndex = prevValue - 1;
|
||||
if (newIndex < 0) newIndex = currentPhotos.length - 1;
|
||||
return newIndex;
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setImageIndex(currentIndex);
|
||||
}, [currentIndex]);
|
||||
|
||||
if (!currentImage) return null;
|
||||
|
||||
const getUserData = (roomId: number, objectId: number, type: string): number | string =>
|
||||
{
|
||||
const roomObject = GetRoomEngine().getRoomObject(roomId, objectId, RoomObjectCategory.WALL);
|
||||
if (!roomObject) return;
|
||||
return type == 'username' ? roomObject.model.getValue<number>(RoomObjectVariable.FURNITURE_OWNER_NAME) : roomObject.model.getValue<number>(RoomObjectVariable.FURNITURE_OWNER_ID);
|
||||
}
|
||||
|
||||
return (
|
||||
<Grid style={{ display: 'flex', flexDirection: 'column' }}>
|
||||
<Flex center className="picture-preview border border-black" style={currentImage.w ? { backgroundImage: 'url(' + currentImage.w + ')' } : {}} onClick={onClick}>
|
||||
{!currentImage.w && <Text bold>{LocalizeText('camera.loading')}</Text>}
|
||||
</Flex>
|
||||
{currentImage.m && currentImage.m.length && <Text center>{currentImage.m}</Text>}
|
||||
<div className="flex items-center center justify-between">
|
||||
<Text>{currentImage.n || ''}</Text>
|
||||
<Text onClick={() => GetUserProfile(Number(getUserData(currentImage.s, Number(currentImage.u), 'id')))}> { getUserData(currentImage.s, Number(currentImage.u), 'username') } </Text>
|
||||
<Text className="cursor-pointer" onClick={() => GetUserProfile(currentImage.oi)}>{currentImage.o}</Text>
|
||||
<Text>{new Date(currentImage.t * 1000).toLocaleDateString()}</Text>
|
||||
</div>
|
||||
{currentPhotos.length > 1 && (
|
||||
<Flex className="picture-preview-buttons">
|
||||
<FaArrowLeft onClick={previous} />
|
||||
<FaArrowRight className="cursor-pointer"onClick={next} />
|
||||
</Flex>
|
||||
)}
|
||||
</Grid>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,222 @@
|
||||
import { GetRoomCameraWidgetManager, IRoomCameraWidgetEffect, IRoomCameraWidgetSelectedEffect, NitroLogger, RoomCameraWidgetSelectedEffect } from '@nitrots/nitro-renderer';
|
||||
import { FC, useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { FaSave, FaSearchMinus, FaSearchPlus, FaTrash } from 'react-icons/fa';
|
||||
import ReactSlider from 'react-slider';
|
||||
import { CameraEditorTabs, CameraPicture, CameraPictureThumbnail, LocalizeText } from '../../../../api';
|
||||
import { Button, Column, Flex, Grid, LayoutImage, NitroCardContentView, NitroCardHeaderView, NitroCardTabsItemView, NitroCardTabsView, NitroCardView, Text } from '../../../../common';
|
||||
import { CameraWidgetEffectListView } from './effect-list';
|
||||
|
||||
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 => {
|
||||
const { picture = null, availableEffects = null, myLevel = 1, onClose = null, onCancel = null, onCheckout = null } = props;
|
||||
const [ currentTab, setCurrentTab ] = useState(TABS[0]);
|
||||
const [ selectedEffectName, setSelectedEffectName ] = useState<string>(null);
|
||||
const [ selectedEffects, setSelectedEffects ] = useState<IRoomCameraWidgetSelectedEffect[]>([]);
|
||||
const [ effectsThumbnails, setEffectsThumbnails ] = useState<CameraPictureThumbnail[]>([]);
|
||||
const [ isZoomed, setIsZoomed ] = useState(false);
|
||||
const [ currentPictureUrl, setCurrentPictureUrl ] = useState<string>('');
|
||||
const isBusy = useRef<boolean>(false);
|
||||
|
||||
const getColorMatrixEffects = useMemo(() => {
|
||||
return availableEffects.filter(effect => effect.colorMatrix);
|
||||
}, [ availableEffects ]);
|
||||
|
||||
const getCompositeEffects = useMemo(() => {
|
||||
return availableEffects.filter(effect => effect.texture);
|
||||
}, [ availableEffects ]);
|
||||
|
||||
const getEffectList = useCallback(() => {
|
||||
return currentTab === CameraEditorTabs.COLORMATRIX ? getColorMatrixEffects : getCompositeEffects;
|
||||
}, [ currentTab, getColorMatrixEffects, getCompositeEffects ]);
|
||||
|
||||
const getSelectedEffectIndex = useCallback((name: string) => {
|
||||
if (!name || !name.length || !selectedEffects || !selectedEffects.length) return -1;
|
||||
return selectedEffects.findIndex(effect => effect.effect.name === name);
|
||||
}, [ selectedEffects ]);
|
||||
|
||||
const getCurrentEffectIndex = useMemo(() => {
|
||||
return getSelectedEffectIndex(selectedEffectName);
|
||||
}, [ selectedEffectName, getSelectedEffectIndex ]);
|
||||
|
||||
const getCurrentEffect = useMemo(() => {
|
||||
if (!selectedEffectName) return null;
|
||||
return selectedEffects[getCurrentEffectIndex] || null;
|
||||
}, [ selectedEffectName, getCurrentEffectIndex, selectedEffects ]);
|
||||
|
||||
const setSelectedEffectAlpha = useCallback((alpha: number) => {
|
||||
const index = getCurrentEffectIndex;
|
||||
if (index === -1) return;
|
||||
|
||||
setSelectedEffects(prevValue => {
|
||||
const clone = [ ...prevValue ];
|
||||
const currentEffect = clone[index];
|
||||
clone[index] = new RoomCameraWidgetSelectedEffect(currentEffect.effect, alpha);
|
||||
return clone;
|
||||
});
|
||||
}, [ getCurrentEffectIndex ]);
|
||||
|
||||
const processAction = useCallback((type: string, effectName: string = null) => {
|
||||
switch (type) {
|
||||
case 'close':
|
||||
onClose();
|
||||
return;
|
||||
case 'cancel':
|
||||
onCancel();
|
||||
return;
|
||||
case 'checkout':
|
||||
onCheckout(currentPictureUrl);
|
||||
return;
|
||||
case 'change_tab':
|
||||
setCurrentTab(String(effectName));
|
||||
return;
|
||||
case 'select_effect': {
|
||||
const existingIndex = getSelectedEffectIndex(effectName);
|
||||
if (existingIndex >= 0) return;
|
||||
|
||||
const effect = availableEffects.find(effect => effect.name === effectName);
|
||||
if (!effect) return;
|
||||
|
||||
setSelectedEffects(prevValue => [ ...prevValue, new RoomCameraWidgetSelectedEffect(effect, 1) ]);
|
||||
setSelectedEffectName(effect.name);
|
||||
return;
|
||||
}
|
||||
case 'remove_effect': {
|
||||
const existingIndex = getSelectedEffectIndex(effectName);
|
||||
if (existingIndex === -1) return;
|
||||
|
||||
setSelectedEffects(prevValue => {
|
||||
const clone = [ ...prevValue ];
|
||||
clone.splice(existingIndex, 1);
|
||||
return clone;
|
||||
});
|
||||
|
||||
if (selectedEffectName === effectName) setSelectedEffectName(null);
|
||||
return;
|
||||
}
|
||||
case 'clear_effects':
|
||||
setSelectedEffectName(null);
|
||||
setSelectedEffects([]);
|
||||
return;
|
||||
case 'download': {
|
||||
(async () => {
|
||||
const image = new Image();
|
||||
image.src = currentPictureUrl;
|
||||
const newWindow = window.open('');
|
||||
newWindow.document.write(image.outerHTML);
|
||||
})();
|
||||
return;
|
||||
}
|
||||
case 'zoom':
|
||||
setIsZoomed(prev => !prev);
|
||||
return;
|
||||
}
|
||||
}, [ availableEffects, selectedEffectName, currentPictureUrl, getSelectedEffectIndex, onCancel, onCheckout, onClose ]);
|
||||
|
||||
useEffect(() => {
|
||||
const processThumbnails = async () => {
|
||||
const renderedEffects = await Promise.all(
|
||||
availableEffects.map(effect =>
|
||||
GetRoomCameraWidgetManager().applyEffects(picture.texture, [ new RoomCameraWidgetSelectedEffect(effect, 1) ], false)
|
||||
)
|
||||
);
|
||||
setEffectsThumbnails(renderedEffects.map((image, index) => new CameraPictureThumbnail(availableEffects[index].name, image.src)));
|
||||
};
|
||||
processThumbnails();
|
||||
}, [ picture, availableEffects ]);
|
||||
|
||||
useEffect(() => {
|
||||
GetRoomCameraWidgetManager()
|
||||
.applyEffects(picture.texture, selectedEffects, false) // Remove isZoomed from here
|
||||
.then(imageElement => {
|
||||
setCurrentPictureUrl(imageElement.src);
|
||||
})
|
||||
.catch(error => {
|
||||
NitroLogger.error('Failed to apply effects to picture', error);
|
||||
});
|
||||
}, [ picture, selectedEffects ]); // Remove isZoomed from dependency array
|
||||
|
||||
return (
|
||||
<NitroCardView className="w-[600px] h-[500px]">
|
||||
<NitroCardHeaderView headerText={ LocalizeText('camera.editor.button.text') } onCloseClick={ event => processAction('close') } />
|
||||
<NitroCardTabsView>
|
||||
{ TABS.map(tab => (
|
||||
<NitroCardTabsItemView key={ tab } isActive={ currentTab === tab } onClick={ event => processAction('change_tab', tab) }>
|
||||
<i className={ 'nitro-icon icon-camera-' + tab }></i>
|
||||
</NitroCardTabsItemView>
|
||||
)) }
|
||||
</NitroCardTabsView>
|
||||
<NitroCardContentView>
|
||||
<Grid>
|
||||
<Column overflow="hidden" size={ 5 }>
|
||||
<CameraWidgetEffectListView
|
||||
myLevel={ myLevel }
|
||||
selectedEffects={ selectedEffects }
|
||||
effects={ getEffectList() }
|
||||
thumbnails={ effectsThumbnails }
|
||||
processAction={ processAction }
|
||||
/>
|
||||
</Column>
|
||||
<Column justifyContent="between" overflow="hidden" size={ 7 }>
|
||||
<Column center>
|
||||
<LayoutImage
|
||||
style={{
|
||||
width: '320px',
|
||||
height: '320px',
|
||||
backgroundImage: `url(${currentPictureUrl})`,
|
||||
backgroundPosition: isZoomed ? 'center' : 'top left',
|
||||
backgroundSize: isZoomed ? 'contain' : 'auto', // Zoom only affects display
|
||||
backgroundRepeat: 'no-repeat'
|
||||
}}
|
||||
/>
|
||||
{ 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.strength }
|
||||
onChange={ event => setSelectedEffectAlpha(event) }
|
||||
renderThumb={ ({ key, ...props }, state) => <div key={ key } { ...props }>{ state.valueNow }</div> }
|
||||
/>
|
||||
</Column>
|
||||
) }
|
||||
</Column>
|
||||
<div className="flex justify-between">
|
||||
<div className="relative inline-flex align-middle">
|
||||
<Button onClick={ event => processAction('clear_effects') }>
|
||||
<FaTrash className="fa-icon" />
|
||||
</Button>
|
||||
<Button onClick={ event => processAction('download') }>
|
||||
<FaSave className="fa-icon" />
|
||||
</Button>
|
||||
<Button onClick={ event => processAction('zoom') }>
|
||||
{ isZoomed ? <FaSearchMinus className="fa-icon" /> : <FaSearchPlus className="fa-icon" /> }
|
||||
</Button>
|
||||
</div>
|
||||
<div className="flex gap-1">
|
||||
<Button onClick={ event => processAction('cancel') }>
|
||||
{ LocalizeText('generic.cancel') }
|
||||
</Button>
|
||||
<Button onClick={ event => processAction('checkout') }>
|
||||
{ LocalizeText('camera.preview.button.text') }
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Column>
|
||||
</Grid>
|
||||
</NitroCardContentView>
|
||||
</NitroCardView>
|
||||
);
|
||||
};
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
import { IRoomCameraWidgetEffect } from '@nitrots/nitro-renderer';
|
||||
import { FC } from 'react';
|
||||
import { FaLock, FaTimes } from 'react-icons/fa';
|
||||
import { LocalizeText } from '../../../../../api';
|
||||
import { Button, LayoutGridItem, Text } from '../../../../../common';
|
||||
|
||||
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 itemActive={ isActive } title={ LocalizeText(!isLocked ? (`camera.effect.name.${ effect.name }`) : `camera.effect.required.level ${ effect.minLevel }`) } onClick={ event => (!isActive && selectEffect()) }>
|
||||
{ isActive &&
|
||||
<Button className="rounded-circle remove-effect" variant="danger" onClick={ removeEffect }>
|
||||
<FaTimes className="fa-icon" />
|
||||
</Button> }
|
||||
{ !isLocked && (thumbnailUrl && thumbnailUrl.length > 0) &&
|
||||
<div className="effect-thumbnail-image border">
|
||||
<img alt="" src={ thumbnailUrl } />
|
||||
</div> }
|
||||
{ isLocked &&
|
||||
<Text bold center>
|
||||
<div>
|
||||
<FaLock className="fa-icon" />
|
||||
</div>
|
||||
{ effect.minLevel }
|
||||
</Text> }
|
||||
</LayoutGridItem>
|
||||
);
|
||||
};
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
import { IRoomCameraWidgetEffect, IRoomCameraWidgetSelectedEffect } from '@nitrots/nitro-renderer';
|
||||
import { FC } from 'react';
|
||||
import { CameraPictureThumbnail } from '../../../../../api';
|
||||
import { Grid } from '../../../../../common';
|
||||
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 (
|
||||
<Grid columnCount={ 3 } overflow="auto">
|
||||
{ effects && (effects.length > 0) && effects.map((effect, index) =>
|
||||
{
|
||||
const thumbnailUrl = (thumbnails.find(thumbnail => (thumbnail.effectName === effect.name)));
|
||||
const isActive = (selectedEffects.findIndex(selectedEffect => (selectedEffect.effect.name === effect.name)) > -1);
|
||||
|
||||
// return <CameraWidgetEffectListItemView key={ index } effect={ effect } isActive={ isActive } isLocked={ (effect.minLevel > myLevel) } removeEffect={ () => processAction('remove_effect', effect.name) } selectEffect={ () => processAction('select_effect', effect.name) } thumbnailUrl={ ((thumbnailUrl && thumbnailUrl.thumbnailUrl) || null) } />;
|
||||
|
||||
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) } />
|
||||
}) }
|
||||
</Grid>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from './CameraWidgetEffectListItemView';
|
||||
export * from './CameraWidgetEffectListView';
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from './CameraWidgetEditorView';
|
||||
export * from './effect-list';
|
||||
@@ -0,0 +1,5 @@
|
||||
export * from './CameraWidgetCaptureView';
|
||||
export * from './CameraWidgetCheckoutView';
|
||||
export * from './CameraWidgetShowPhotoView';
|
||||
export * from './editor';
|
||||
export * from './editor/effect-list';
|
||||
@@ -0,0 +1,53 @@
|
||||
import { GetRoomEngine, GetSessionDataManager } from '@nitrots/nitro-renderer';
|
||||
import { FC } from 'react';
|
||||
import { CalendarItemState, GetConfigurationValue, ICalendarItem } from '../../api';
|
||||
import { Column, Flex, LayoutImage } from '../../common';
|
||||
|
||||
interface CalendarItemViewProps
|
||||
{
|
||||
itemId: number;
|
||||
state: number;
|
||||
active?: boolean;
|
||||
product?: ICalendarItem;
|
||||
onClick: (itemId: number) => void;
|
||||
}
|
||||
|
||||
export const CalendarItemView: FC<CalendarItemViewProps> = props =>
|
||||
{
|
||||
const { itemId = -1, state = null, product = null, active = false, onClick = null } = props;
|
||||
|
||||
const getFurnitureIcon = (name: string) =>
|
||||
{
|
||||
let furniData = GetSessionDataManager().getFloorItemDataByName(name);
|
||||
let url = null;
|
||||
|
||||
if(furniData) url = GetRoomEngine().getFurnitureFloorIconUrl(furniData.id);
|
||||
else
|
||||
{
|
||||
furniData = GetSessionDataManager().getWallItemDataByName(name);
|
||||
|
||||
if(furniData) url = GetRoomEngine().getFurnitureWallIconUrl(furniData.id);
|
||||
}
|
||||
|
||||
return url;
|
||||
};
|
||||
|
||||
return (
|
||||
<Column center fit pointer className={ `campaign-spritesheet campaign-day-generic-bg rounded calendar-item ${ active ? 'active' : '' }` } onClick={ () => onClick(itemId) }>
|
||||
{ (state === CalendarItemState.STATE_UNLOCKED) &&
|
||||
<Flex center className="campaign-spritesheet unlocked-bg">
|
||||
<Flex center className="campaign-spritesheet campaign-opened">
|
||||
{ product &&
|
||||
<LayoutImage imageUrl={ product.customImage ? GetConfigurationValue<string>('image.library.url') + product.customImage : getFurnitureIcon(product.productName) } /> }
|
||||
</Flex>
|
||||
</Flex> }
|
||||
{ (state !== CalendarItemState.STATE_UNLOCKED) &&
|
||||
<Flex center className="campaign-spritesheet locked-bg">
|
||||
{ (state === CalendarItemState.STATE_LOCKED_AVAILABLE) &&
|
||||
<div className="campaign-spritesheet available" /> }
|
||||
{ ((state === CalendarItemState.STATE_LOCKED_EXPIRED) || (state === CalendarItemState.STATE_LOCKED_FUTURE)) &&
|
||||
<div className="campaign-spritesheet unavailable" /> }
|
||||
</Flex> }
|
||||
</Column>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,144 @@
|
||||
import { GetSessionDataManager } from '@nitrots/nitro-renderer';
|
||||
import { FC, useState } from 'react';
|
||||
import { CalendarItemState, ICalendarItem, LocalizeText } from '../../api';
|
||||
import { Button, Column, Grid, NitroCardContentView, NitroCardHeaderView, NitroCardView, Text } from '../../common';
|
||||
import { CalendarItemView } from './CalendarItemView';
|
||||
|
||||
interface CalendarViewProps
|
||||
{
|
||||
onClose(): void;
|
||||
openPackage(id: number, asStaff: boolean): void;
|
||||
receivedProducts: Map<number, ICalendarItem>;
|
||||
campaignName: string;
|
||||
currentDay: number;
|
||||
numDays: number;
|
||||
openedDays: number[];
|
||||
missedDays: number[];
|
||||
}
|
||||
|
||||
const TOTAL_SHOWN_ITEMS = 5;
|
||||
|
||||
export const CalendarView: FC<CalendarViewProps> = props =>
|
||||
{
|
||||
const { onClose = null, campaignName = null, currentDay = null, numDays = null, missedDays = null, openedDays = null, openPackage = null, receivedProducts = null } = props;
|
||||
const [ selectedDay, setSelectedDay ] = useState(currentDay);
|
||||
const [ index, setIndex ] = useState(Math.max(0, (selectedDay - 1)));
|
||||
|
||||
const getDayState = (day: number) =>
|
||||
{
|
||||
if(openedDays.includes(day)) return CalendarItemState.STATE_UNLOCKED;
|
||||
|
||||
if(day > currentDay) return CalendarItemState.STATE_LOCKED_FUTURE;
|
||||
|
||||
if(missedDays.includes(day)) return CalendarItemState.STATE_LOCKED_EXPIRED;
|
||||
|
||||
return CalendarItemState.STATE_LOCKED_AVAILABLE;
|
||||
};
|
||||
|
||||
const dayMessage = (day: number) =>
|
||||
{
|
||||
const state = getDayState(day);
|
||||
|
||||
switch(state)
|
||||
{
|
||||
case CalendarItemState.STATE_UNLOCKED:
|
||||
return LocalizeText('campaign.calendar.info.unlocked');
|
||||
case CalendarItemState.STATE_LOCKED_FUTURE:
|
||||
return LocalizeText('campaign.calendar.info.future');
|
||||
case CalendarItemState.STATE_LOCKED_EXPIRED:
|
||||
return LocalizeText('campaign.calendar.info.expired');
|
||||
default:
|
||||
return LocalizeText('campaign.calendar.info.available.desktop');
|
||||
}
|
||||
};
|
||||
|
||||
const onClickNext = () =>
|
||||
{
|
||||
const nextDay = (selectedDay + 1);
|
||||
|
||||
if(nextDay === numDays) return;
|
||||
|
||||
setSelectedDay(nextDay);
|
||||
|
||||
if((index + TOTAL_SHOWN_ITEMS) < (nextDay + 1)) setIndex(index + 1);
|
||||
};
|
||||
|
||||
const onClickPrev = () =>
|
||||
{
|
||||
const prevDay = (selectedDay - 1);
|
||||
|
||||
if(prevDay < 0) return;
|
||||
|
||||
setSelectedDay(prevDay);
|
||||
|
||||
if(index > prevDay) setIndex(index - 1);
|
||||
};
|
||||
|
||||
const onClickItem = (item: number) =>
|
||||
{
|
||||
if(selectedDay === item)
|
||||
{
|
||||
const state = getDayState(item);
|
||||
|
||||
if(state === CalendarItemState.STATE_LOCKED_AVAILABLE) openPackage(item, false);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
setSelectedDay(item);
|
||||
};
|
||||
|
||||
const forceOpen = () =>
|
||||
{
|
||||
const id = selectedDay;
|
||||
const state = getDayState(id);
|
||||
|
||||
if(state !== CalendarItemState.STATE_UNLOCKED) openPackage(id, true);
|
||||
};
|
||||
|
||||
return (
|
||||
<NitroCardView className="nitro-campaign-calendar" theme="primary-slim">
|
||||
<NitroCardHeaderView headerText={ LocalizeText(`campaign.calendar.${ campaignName }.title`) } onCloseClick={ onClose } />
|
||||
<NitroCardContentView>
|
||||
<Grid alignItems="center" fullHeight={ false } justifyContent="between">
|
||||
<Column size={ 1 } />
|
||||
<Column size={ 10 }>
|
||||
<div className="flex items-center gap-1 justify-between">
|
||||
<div className="flex flex-col gap-1">
|
||||
<Text fontSize={ 3 }>{ LocalizeText('campaign.calendar.heading.day', [ 'number' ], [ (selectedDay + 1).toString() ]) }</Text>
|
||||
<Text>{ dayMessage(selectedDay) }</Text>
|
||||
</div>
|
||||
<div>
|
||||
{ GetSessionDataManager().isModerator &&
|
||||
<Button variant="danger" onClick={ forceOpen }>Force open</Button> }
|
||||
</div>
|
||||
</div>
|
||||
</Column>
|
||||
<Column size={ 1 } />
|
||||
</Grid>
|
||||
<div className="flex h-full gap-2">
|
||||
<div className="flex items-center justify-center">
|
||||
<div className="campaign-spritesheet prev cursor-pointer" onClick={ onClickPrev } />
|
||||
</div>
|
||||
<Column center fullWidth>
|
||||
<Grid fit columnCount={ TOTAL_SHOWN_ITEMS } gap={ 1 }>
|
||||
{ [ ...Array(TOTAL_SHOWN_ITEMS) ].map((e, i) =>
|
||||
{
|
||||
const day = (index + i);
|
||||
|
||||
return (
|
||||
<Column key={ i } overflow="hidden">
|
||||
<CalendarItemView active={ (selectedDay === day) } itemId={ day } product={ receivedProducts.has(day) ? receivedProducts.get(day) : null } state={ getDayState(day) } onClick={ onClickItem } />
|
||||
</Column>
|
||||
);
|
||||
}) }
|
||||
</Grid>
|
||||
</Column>
|
||||
<div className="flex items-center justify-center">
|
||||
<div className="campaign-spritesheet next cursor-pointer" onClick={ onClickNext } />
|
||||
</div>
|
||||
</div>
|
||||
</NitroCardContentView>
|
||||
</NitroCardView>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,101 @@
|
||||
import { AddLinkEventTracker, CampaignCalendarData, CampaignCalendarDataMessageEvent, CampaignCalendarDoorOpenedMessageEvent, ILinkEventTracker, OpenCampaignCalendarDoorAsStaffComposer, OpenCampaignCalendarDoorComposer, RemoveLinkEventTracker } from '@nitrots/nitro-renderer';
|
||||
import { FC, useEffect, useState } from 'react';
|
||||
import { CalendarItem, SendMessageComposer } from '../../api';
|
||||
import { useMessageEvent } from '../../hooks';
|
||||
import { CalendarView } from './CalendarView';
|
||||
|
||||
export const CampaignView: FC<{}> = props =>
|
||||
{
|
||||
const [ calendarData, setCalendarData ] = useState<CampaignCalendarData>(null);
|
||||
const [ lastOpenAttempt, setLastOpenAttempt ] = useState<number>(-1);
|
||||
const [ receivedProducts, setReceivedProducts ] = useState<Map<number, CalendarItem>>(new Map());
|
||||
const [ isCalendarOpen, setCalendarOpen ] = useState(false);
|
||||
|
||||
const openPackage = (id: number, asStaff = false) =>
|
||||
{
|
||||
if(!calendarData) return;
|
||||
|
||||
setLastOpenAttempt(id);
|
||||
|
||||
if(asStaff)
|
||||
{
|
||||
SendMessageComposer(new OpenCampaignCalendarDoorAsStaffComposer(calendarData.campaignName, id));
|
||||
}
|
||||
|
||||
else
|
||||
{
|
||||
SendMessageComposer(new OpenCampaignCalendarDoorComposer(calendarData.campaignName, id));
|
||||
}
|
||||
};
|
||||
|
||||
useMessageEvent<CampaignCalendarDataMessageEvent>(CampaignCalendarDataMessageEvent, event =>
|
||||
{
|
||||
const parser = event.getParser();
|
||||
|
||||
if(!parser) return;
|
||||
|
||||
setCalendarData(parser.calendarData);
|
||||
});
|
||||
|
||||
useMessageEvent<CampaignCalendarDoorOpenedMessageEvent>(CampaignCalendarDoorOpenedMessageEvent, event =>
|
||||
{
|
||||
const parser = event.getParser();
|
||||
|
||||
if(!parser) return;
|
||||
|
||||
const lastAttempt = lastOpenAttempt;
|
||||
|
||||
if(parser.doorOpened)
|
||||
{
|
||||
setCalendarData(prev =>
|
||||
{
|
||||
const copy = prev.clone();
|
||||
copy.openedDays.push(lastOpenAttempt);
|
||||
|
||||
return copy;
|
||||
});
|
||||
|
||||
setReceivedProducts(prev =>
|
||||
{
|
||||
const copy = new Map(prev);
|
||||
copy.set(lastAttempt, new CalendarItem(parser.productName, parser.customImage,parser.furnitureClassName));
|
||||
|
||||
return copy;
|
||||
});
|
||||
}
|
||||
|
||||
setLastOpenAttempt(-1);
|
||||
});
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
const linkTracker: ILinkEventTracker = {
|
||||
linkReceived: (url: string) =>
|
||||
{
|
||||
const value = url.split('/');
|
||||
|
||||
if(value.length < 2) return;
|
||||
|
||||
switch(value[1])
|
||||
{
|
||||
case 'calendar':
|
||||
setCalendarOpen(true);
|
||||
break;
|
||||
}
|
||||
},
|
||||
eventUrlPrefix: 'openView/'
|
||||
};
|
||||
|
||||
AddLinkEventTracker(linkTracker);
|
||||
|
||||
return () => RemoveLinkEventTracker(linkTracker);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
{ (calendarData && isCalendarOpen) &&
|
||||
<CalendarView campaignName={ calendarData.campaignName } currentDay={ calendarData.currentDay } missedDays={ calendarData.missedDays } numDays={ calendarData.campaignDays } openedDays={ calendarData.openedDays } openPackage={ openPackage } receivedProducts={ receivedProducts } onClose={ () => setCalendarOpen(false) } />
|
||||
}
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,111 @@
|
||||
import { AddLinkEventTracker, ILinkEventTracker, RemoveLinkEventTracker } from '@nitrots/nitro-renderer';
|
||||
import { FC, useEffect } from 'react';
|
||||
import { GetConfigurationValue, LocalizeText } from '../../api';
|
||||
import { Column, Grid, NitroCardContentView, NitroCardHeaderView, NitroCardTabsItemView, NitroCardTabsView, NitroCardView } from '../../common';
|
||||
import { useCatalog } from '../../hooks';
|
||||
import { CatalogIconView } from './views/catalog-icon/CatalogIconView';
|
||||
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';
|
||||
|
||||
export const CatalogView: FC<{}> = props =>
|
||||
{
|
||||
const { isVisible = false, setIsVisible = null, rootNode = null, currentPage = null, navigationHidden = false, setNavigationHidden = null, activeNodes = [], searchResult = null, setSearchResult = null, openPageByName = null, openPageByOfferId = null, activateNode = null, getNodeById } = useCatalog();
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
const linkTracker: ILinkEventTracker = {
|
||||
linkReceived: (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;
|
||||
}
|
||||
},
|
||||
eventUrlPrefix: 'catalog/'
|
||||
};
|
||||
|
||||
AddLinkEventTracker(linkTracker);
|
||||
|
||||
return () => RemoveLinkEventTracker(linkTracker);
|
||||
}, [ setIsVisible, openPageByOfferId, openPageByName ]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{ isVisible &&
|
||||
<NitroCardView className="w-[630px] h-[400px]" style={ GetConfigurationValue('catalog.headers') ? { width: 710 } : {} } uniqueKey="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);
|
||||
} } >
|
||||
<div className={ `flex items-center gap-${ GetConfigurationValue('catalog.tab.icons') ? 1 : 0 }` }>
|
||||
{ GetConfigurationValue('catalog.tab.icons') && <CatalogIconView icon={ child.iconId } /> }
|
||||
{ child.localization }
|
||||
</div>
|
||||
</NitroCardTabsItemView>
|
||||
);
|
||||
}) }
|
||||
</NitroCardTabsView>
|
||||
<NitroCardContentView>
|
||||
<Grid>
|
||||
{ !navigationHidden &&
|
||||
<Column overflow="hidden" size={ 3 }>
|
||||
{ activeNodes && (activeNodes.length > 0) &&
|
||||
<CatalogNavigationView node={ activeNodes[0] } /> }
|
||||
</Column> }
|
||||
<Column overflow="hidden" size={ !navigationHidden ? 9 : 12 }>
|
||||
{ GetCatalogLayout(currentPage, () => setNavigationHidden(true)) }
|
||||
</Column>
|
||||
</Grid>
|
||||
</NitroCardContentView>
|
||||
</NitroCardView> }
|
||||
<CatalogGiftView />
|
||||
<MarketplacePostOfferView />
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,10 @@
|
||||
import { FC } from 'react';
|
||||
|
||||
export const CatalogPurchaseConfirmView: FC<{}> = props =>
|
||||
{
|
||||
const {} = props;
|
||||
|
||||
return (
|
||||
<div></div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,25 @@
|
||||
import { FC, useEffect, useState } from 'react';
|
||||
import { GetConfigurationValue } from '../../../../api';
|
||||
|
||||
export interface CatalogHeaderViewProps
|
||||
{
|
||||
imageUrl?: string;
|
||||
}
|
||||
|
||||
export const CatalogHeaderView: FC<CatalogHeaderViewProps> = props =>
|
||||
{
|
||||
const { imageUrl = null } = props;
|
||||
const [ displayImageUrl, setDisplayImageUrl ] = useState('');
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
setDisplayImageUrl(imageUrl ?? GetConfigurationValue<string>('catalog.asset.image.url').replace('%name%', 'catalog_header_roombuilder'));
|
||||
}, [ imageUrl ]);
|
||||
|
||||
return <div className="flex justify-center items-center w-full nitro-catalog-header">
|
||||
<img src={ displayImageUrl } onError={ ({ currentTarget }) =>
|
||||
{
|
||||
currentTarget.src = GetConfigurationValue<string>('catalog.asset.image.url').replace('%name%', 'catalog_header_roombuilder');
|
||||
} } />
|
||||
</div>;
|
||||
};
|
||||
@@ -0,0 +1,20 @@
|
||||
import { FC, useMemo } from 'react';
|
||||
import { GetConfigurationValue } from '../../../../api';
|
||||
import { LayoutImage } from '../../../../common';
|
||||
|
||||
export interface CatalogIconViewProps
|
||||
{
|
||||
icon: number;
|
||||
}
|
||||
|
||||
export const CatalogIconView: FC<CatalogIconViewProps> = props =>
|
||||
{
|
||||
const { icon = 0 } = props;
|
||||
|
||||
const getIconUrl = useMemo(() =>
|
||||
{
|
||||
return ((GetConfigurationValue<string>('catalog.asset.icon.url')).replace('%name%', icon.toString()));
|
||||
}, [ icon ]);
|
||||
|
||||
return <LayoutImage imageUrl={ getIconUrl } style={ { width: 20, height: 20 } } />;
|
||||
};
|
||||
+47
@@ -0,0 +1,47 @@
|
||||
import { GetEventDispatcher, NitroToolbarAnimateIconEvent, RoomPreviewer, TextureUtils, ToolbarIconEnum } from '@nitrots/nitro-renderer';
|
||||
import { FC, useRef } from 'react';
|
||||
import { LayoutRoomPreviewerView } from '../../../../common';
|
||||
import { CatalogPurchasedEvent } from '../../../../events';
|
||||
import { useUiEvent } from '../../../../hooks';
|
||||
|
||||
export const CatalogRoomPreviewerView: FC<{
|
||||
roomPreviewer: RoomPreviewer;
|
||||
height?: number;
|
||||
}> = props =>
|
||||
{
|
||||
const { roomPreviewer = null } = props;
|
||||
const elementRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useUiEvent(CatalogPurchasedEvent.PURCHASE_SUCCESS, event =>
|
||||
{
|
||||
if(!elementRef) return;
|
||||
|
||||
const renderTexture = roomPreviewer.getRoomObjectCurrentImage();
|
||||
|
||||
if(!renderTexture) return;
|
||||
|
||||
(async () =>
|
||||
{
|
||||
const image = await TextureUtils.generateImage(renderTexture);
|
||||
|
||||
if(!image) return;
|
||||
|
||||
const bounds = elementRef.current.getBoundingClientRect();
|
||||
|
||||
const x = (bounds.x + (bounds.width / 2));
|
||||
const y = (bounds.y + (bounds.height / 2));
|
||||
|
||||
const animateEvent = new NitroToolbarAnimateIconEvent(image, x, y);
|
||||
|
||||
animateEvent.iconName = ToolbarIconEnum.INVENTORY;
|
||||
|
||||
GetEventDispatcher().dispatchEvent(animateEvent);
|
||||
})();
|
||||
});
|
||||
|
||||
return (
|
||||
<div ref={ elementRef }>
|
||||
<LayoutRoomPreviewerView { ...props } />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,290 @@
|
||||
import { GetSessionDataManager, GiftReceiverNotFoundEvent, PurchaseFromCatalogAsGiftComposer } from '@nitrots/nitro-renderer';
|
||||
import { ChangeEvent, FC, useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { FaChevronLeft, FaChevronRight } from 'react-icons/fa';
|
||||
import { ColorUtils, LocalizeText, MessengerFriend, ProductTypeEnum, SendMessageComposer } from '../../../../api';
|
||||
import { Button, Column, Flex, FormGroup, LayoutCurrencyIcon, LayoutFurniImageView, LayoutGiftTagView, NitroCardContentView, NitroCardHeaderView, NitroCardView, Text } from '../../../../common';
|
||||
import { CatalogEvent, CatalogInitGiftEvent, CatalogPurchasedEvent } from '../../../../events';
|
||||
import { useCatalog, useFriends, useMessageEvent, useUiEvent } from '../../../../hooks';
|
||||
import { classNames } from '../../../../layout';
|
||||
|
||||
export const CatalogGiftView: FC<{}> = props =>
|
||||
{
|
||||
const [ isVisible, setIsVisible ] = useState<boolean>(false);
|
||||
const [ pageId, setPageId ] = useState<number>(0);
|
||||
const [ offerId, setOfferId ] = useState<number>(0);
|
||||
const [ extraData, setExtraData ] = useState<string>('');
|
||||
const [ receiverName, setReceiverName ] = useState<string>('');
|
||||
const [ showMyFace, setShowMyFace ] = useState<boolean>(true);
|
||||
const [ message, setMessage ] = useState<string>('');
|
||||
const [ colors, setColors ] = useState<{ id: number, color: string }[]>([]);
|
||||
const [ selectedBoxIndex, setSelectedBoxIndex ] = useState<number>(0);
|
||||
const [ selectedRibbonIndex, setSelectedRibbonIndex ] = useState<number>(0);
|
||||
const [ selectedColorId, setSelectedColorId ] = useState<number>(0);
|
||||
const [ maxBoxIndex, setMaxBoxIndex ] = useState<number>(0);
|
||||
const [ maxRibbonIndex, setMaxRibbonIndex ] = useState<number>(0);
|
||||
const [ receiverNotFound, setReceiverNotFound ] = useState<boolean>(false);
|
||||
const { catalogOptions = null } = useCatalog();
|
||||
const { friends } = useFriends();
|
||||
const { giftConfiguration = null } = catalogOptions;
|
||||
const [ boxTypes, setBoxTypes ] = useState<number[]>([]);
|
||||
const [ suggestions, setSuggestions ] = useState([]);
|
||||
const [ isAutocompleteVisible, setIsAutocompleteVisible ] = useState(true);
|
||||
|
||||
const onClose = useCallback(() =>
|
||||
{
|
||||
setIsVisible(false);
|
||||
setPageId(0);
|
||||
setOfferId(0);
|
||||
setExtraData('');
|
||||
setReceiverName('');
|
||||
setShowMyFace(true);
|
||||
setMessage('');
|
||||
setSelectedBoxIndex(0);
|
||||
setSelectedRibbonIndex(0);
|
||||
setIsAutocompleteVisible(false);
|
||||
setSuggestions([]);
|
||||
|
||||
if(colors.length) setSelectedColorId(colors[0].id);
|
||||
}, [ colors ]);
|
||||
|
||||
const isBoxDefault = useMemo(() =>
|
||||
{
|
||||
return giftConfiguration ? (giftConfiguration.defaultStuffTypes.findIndex(s => (s === boxTypes[selectedBoxIndex])) > -1) : false;
|
||||
}, [ boxTypes, giftConfiguration, selectedBoxIndex ]);
|
||||
|
||||
const boxExtraData = useMemo(() =>
|
||||
{
|
||||
if(!giftConfiguration) return '';
|
||||
|
||||
return ((boxTypes[selectedBoxIndex] * 1000) + giftConfiguration.ribbonTypes[selectedRibbonIndex]).toString();
|
||||
}, [ giftConfiguration, selectedBoxIndex, selectedRibbonIndex, boxTypes ]);
|
||||
|
||||
const isColorable = useMemo(() =>
|
||||
{
|
||||
if(!giftConfiguration) return false;
|
||||
|
||||
if(isBoxDefault) return false;
|
||||
|
||||
const boxType = boxTypes[selectedBoxIndex];
|
||||
|
||||
return (boxType === 8 || (boxType >= 3 && boxType <= 6)) ? false : true;
|
||||
}, [ giftConfiguration, selectedBoxIndex, isBoxDefault, boxTypes ]);
|
||||
|
||||
const colourId = useMemo(() =>
|
||||
{
|
||||
return isBoxDefault ? boxTypes[selectedBoxIndex] : selectedColorId;
|
||||
}, [ isBoxDefault, boxTypes, selectedBoxIndex, selectedColorId ]);
|
||||
|
||||
const allFriends = friends.filter((friend: MessengerFriend) => friend.id !== -1);
|
||||
|
||||
const onTextChanged = (e: ChangeEvent<HTMLInputElement>) =>
|
||||
{
|
||||
const value = e.target.value;
|
||||
|
||||
let suggestions = [];
|
||||
|
||||
if(value.length > 0)
|
||||
{
|
||||
suggestions = allFriends.sort().filter((friend: MessengerFriend) => friend.name.includes(value));
|
||||
}
|
||||
|
||||
setReceiverName(value);
|
||||
setIsAutocompleteVisible(true);
|
||||
setSuggestions(suggestions);
|
||||
};
|
||||
|
||||
const selectedReceiverName = (friendName: string) =>
|
||||
{
|
||||
setReceiverName(friendName);
|
||||
setIsAutocompleteVisible(false);
|
||||
};
|
||||
|
||||
const handleAction = useCallback((action: string) =>
|
||||
{
|
||||
switch(action)
|
||||
{
|
||||
case 'prev_box':
|
||||
setSelectedBoxIndex(value => (value === 0 ? maxBoxIndex : value - 1));
|
||||
return;
|
||||
case 'next_box':
|
||||
setSelectedBoxIndex(value => (value === maxBoxIndex ? 0 : value + 1));
|
||||
return;
|
||||
case 'prev_ribbon':
|
||||
setSelectedRibbonIndex(value => (value === 0 ? maxRibbonIndex : value - 1));
|
||||
return;
|
||||
case 'next_ribbon':
|
||||
setSelectedRibbonIndex(value => (value === maxRibbonIndex ? 0 : value + 1));
|
||||
return;
|
||||
case 'buy':
|
||||
if(!receiverName || (receiverName.length === 0))
|
||||
{
|
||||
setReceiverNotFound(true);
|
||||
return;
|
||||
}
|
||||
|
||||
SendMessageComposer(new PurchaseFromCatalogAsGiftComposer(pageId, offerId, extraData, receiverName, message, colourId, selectedBoxIndex, selectedRibbonIndex, showMyFace));
|
||||
return;
|
||||
}
|
||||
}, [ colourId, extraData, maxBoxIndex, maxRibbonIndex, message, offerId, pageId, receiverName, selectedBoxIndex, selectedRibbonIndex, showMyFace ]);
|
||||
|
||||
useMessageEvent<GiftReceiverNotFoundEvent>(GiftReceiverNotFoundEvent, event => setReceiverNotFound(true));
|
||||
|
||||
useUiEvent([
|
||||
CatalogPurchasedEvent.PURCHASE_SUCCESS,
|
||||
CatalogEvent.INIT_GIFT ], event =>
|
||||
{
|
||||
switch(event.type)
|
||||
{
|
||||
case CatalogPurchasedEvent.PURCHASE_SUCCESS:
|
||||
onClose();
|
||||
return;
|
||||
case CatalogEvent.INIT_GIFT:
|
||||
const castedEvent = (event as CatalogInitGiftEvent);
|
||||
|
||||
onClose();
|
||||
|
||||
setPageId(castedEvent.pageId);
|
||||
setOfferId(castedEvent.offerId);
|
||||
setExtraData(castedEvent.extraData);
|
||||
setIsVisible(true);
|
||||
return;
|
||||
}
|
||||
});
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
setReceiverNotFound(false);
|
||||
}, [ receiverName ]);
|
||||
|
||||
const createBoxTypes = useCallback(() =>
|
||||
{
|
||||
if(!giftConfiguration) return;
|
||||
|
||||
setBoxTypes(prev =>
|
||||
{
|
||||
let newPrev = [ ...giftConfiguration.boxTypes ];
|
||||
|
||||
newPrev.push(giftConfiguration.defaultStuffTypes[Math.floor((Math.random() * (giftConfiguration.defaultStuffTypes.length - 1)))]);
|
||||
|
||||
setMaxBoxIndex(newPrev.length - 1);
|
||||
setMaxRibbonIndex(newPrev.length - 1);
|
||||
|
||||
return newPrev;
|
||||
});
|
||||
}, [ giftConfiguration ]);
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
if(!giftConfiguration) return;
|
||||
|
||||
const newColors: { id: number, color: string }[] = [];
|
||||
|
||||
for(const colorId of giftConfiguration.stuffTypes)
|
||||
{
|
||||
const giftData = GetSessionDataManager().getFloorItemData(colorId);
|
||||
|
||||
if(!giftData) continue;
|
||||
|
||||
if(giftData.colors && giftData.colors.length > 0) newColors.push({ id: colorId, color: ColorUtils.makeColorNumberHex(giftData.colors[0]) });
|
||||
}
|
||||
|
||||
createBoxTypes();
|
||||
|
||||
if(newColors.length)
|
||||
{
|
||||
setSelectedColorId(newColors[0].id);
|
||||
setColors(newColors);
|
||||
}
|
||||
}, [ giftConfiguration, createBoxTypes ]);
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
if(!isVisible) return;
|
||||
|
||||
createBoxTypes();
|
||||
}, [ createBoxTypes, isVisible ]);
|
||||
|
||||
if(!giftConfiguration || !giftConfiguration.isEnabled || !isVisible) return null;
|
||||
|
||||
const boxName = 'catalog.gift_wrapping_new.box.' + (isBoxDefault ? 'default' : boxTypes[selectedBoxIndex]);
|
||||
const ribbonName = `catalog.gift_wrapping_new.ribbon.${ selectedRibbonIndex }`;
|
||||
const priceText = 'catalog.gift_wrapping_new.' + (isBoxDefault ? 'freeprice' : 'price');
|
||||
|
||||
return (
|
||||
<NitroCardView className="nitro-catalog-gift" theme="primary-slim" uniqueKey="catalog-gift">
|
||||
<NitroCardHeaderView headerText={ LocalizeText('catalog.gift_wrapping.title') } onCloseClick={ onClose } />
|
||||
<NitroCardContentView className="text-black">
|
||||
<FormGroup column>
|
||||
<Text>{ LocalizeText('catalog.gift_wrapping.receiver') }</Text>
|
||||
<input className={ classNames('form-control form-control-sm', receiverNotFound && 'is-invalid') } type="text" value={ receiverName } onChange={ (e) => onTextChanged(e) } />
|
||||
{ (suggestions.length > 0 && isAutocompleteVisible) &&
|
||||
<Column className="autocomplete-gift-container">
|
||||
{ suggestions.map((friend: MessengerFriend) => (
|
||||
<div key={ friend.id } className="autocomplete-gift-item" onClick={ (e) => selectedReceiverName(friend.name) }>{ friend.name }</div>
|
||||
)) }
|
||||
</Column>
|
||||
}
|
||||
{ receiverNotFound &&
|
||||
<div className="invalid-feedback">{ LocalizeText('catalog.gift_wrapping.receiver_not_found.title') }</div> }
|
||||
</FormGroup>
|
||||
<LayoutGiftTagView editable={ true } figure={ GetSessionDataManager().figure } message={ message } userName={ GetSessionDataManager().userName } onChange={ (value) => setMessage(value) } />
|
||||
<div className="form-check">
|
||||
<input checked={ showMyFace } className="form-check-input" name="showMyFace" type="checkbox" onChange={ (e) => setShowMyFace(value => !value) } />
|
||||
<label className="form-check-label">{ LocalizeText('catalog.gift_wrapping.show_face.title') }</label>
|
||||
</div>
|
||||
<div className="items-center gap-2">
|
||||
{ selectedColorId &&
|
||||
<div className="gift-preview">
|
||||
<LayoutFurniImageView extraData={ boxExtraData } productClassId={ colourId } productType={ ProductTypeEnum.FLOOR } />
|
||||
</div> }
|
||||
<div className="flex flex-col gap-1">
|
||||
<div className="flex gap-2">
|
||||
<div className="relative inline-flex align-middle">
|
||||
<Button variant="primary" onClick={ () => handleAction('prev_box') }>
|
||||
<FaChevronLeft className="fa-icon" />
|
||||
</Button>
|
||||
<Button variant="primary" onClick={ () => handleAction('next_box') }>
|
||||
<FaChevronRight className="fa-icon" />
|
||||
</Button>
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
<Text fontWeight="bold">{ LocalizeText(boxName) }</Text>
|
||||
<div className="flex items-center gap-1">
|
||||
{ LocalizeText(priceText, [ 'price' ], [ giftConfiguration.price.toString() ]) }
|
||||
<LayoutCurrencyIcon type={ -1 } />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Flex alignItems="center" className={ isColorable ? '' : 'opacity-50 pointer-events-none' } gap={ 2 }>
|
||||
<div className="relative inline-flex align-middle">
|
||||
<Button variant="primary" onClick={ () => handleAction('prev_ribbon') }>
|
||||
<FaChevronLeft className="fa-icon" />
|
||||
</Button>
|
||||
<Button variant="primary" onClick={ () => handleAction('next_ribbon') }>
|
||||
<FaChevronRight className="fa-icon" />
|
||||
</Button>
|
||||
</div>
|
||||
<Text fontWeight="bold">{ LocalizeText(ribbonName) }</Text>
|
||||
</Flex>
|
||||
</div>
|
||||
</div>
|
||||
<Column className={ isColorable ? '' : 'opacity-50 pointer-events-none' } gap={ 1 }>
|
||||
<Text fontWeight="bold">
|
||||
{ LocalizeText('catalog.gift_wrapping.pick_color') }
|
||||
</Text>
|
||||
<div className="relative inline-flex align-middle w-full">
|
||||
{ colors.map(color => <Button key={ color.id } active={ (color.id === selectedColorId) } disabled={ !isColorable } style={ { backgroundColor: color.color } } variant="dark" onClick={ () => setSelectedColorId(color.id) } />) }
|
||||
</div>
|
||||
</Column>
|
||||
<div className="flex items-center justify-between">
|
||||
<Button className="text-black" variant="link" onClick={ onClose }>
|
||||
{ LocalizeText('cancel') }
|
||||
</Button>
|
||||
<Button variant="success" onClick={ () => handleAction('buy') }>
|
||||
{ LocalizeText('catalog.gift_wrapping.give_gift') }
|
||||
</Button>
|
||||
</div>
|
||||
</NitroCardContentView>
|
||||
</NitroCardView>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,35 @@
|
||||
import { FC } from 'react';
|
||||
import { FaCaretDown, FaCaretUp } from 'react-icons/fa';
|
||||
import { ICatalogNode } from '../../../../api';
|
||||
import { LayoutGridItem, Text } from '../../../../common';
|
||||
import { useCatalog } from '../../../../hooks';
|
||||
import { CatalogIconView } from '../catalog-icon/CatalogIconView';
|
||||
import { CatalogNavigationSetView } from './CatalogNavigationSetView';
|
||||
|
||||
export interface CatalogNavigationItemViewProps
|
||||
{
|
||||
node: ICatalogNode;
|
||||
child?: boolean;
|
||||
}
|
||||
|
||||
export const CatalogNavigationItemView: FC<CatalogNavigationItemViewProps> = props =>
|
||||
{
|
||||
const { node = null, child = false } = props;
|
||||
const { activateNode = null } = useCatalog();
|
||||
|
||||
return (
|
||||
<div className={ child ? 'pl-[5px] border-s-2 border-[#b6bec5]' : '' }>
|
||||
<LayoutGridItem className={ ' !h-[23px] bg-[#cdd3d9] !border-[0] px-[3px] py-px text-sm' } column={ false } gap={ 1 } itemActive={ node.isActive } onClick={ event => activateNode(node) }>
|
||||
<CatalogIconView icon={ node.iconId } />
|
||||
<Text truncate className="!flex-grow">{ node.localization }</Text>
|
||||
{ node.isBranch &&
|
||||
<>
|
||||
{ node.isOpen && <FaCaretUp className="fa-icon" /> }
|
||||
{ !node.isOpen && <FaCaretDown className="fa-icon" /> }
|
||||
</> }
|
||||
</LayoutGridItem>
|
||||
{ node.isOpen && node.isBranch &&
|
||||
<CatalogNavigationSetView child={ true } node={ node } /> }
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,25 @@
|
||||
import { FC } from 'react';
|
||||
import { ICatalogNode } from '../../../../api';
|
||||
import { CatalogNavigationItemView } from './CatalogNavigationItemView';
|
||||
|
||||
export interface CatalogNavigationSetViewProps
|
||||
{
|
||||
node: ICatalogNode;
|
||||
child?: boolean;
|
||||
}
|
||||
|
||||
export const CatalogNavigationSetView: FC<CatalogNavigationSetViewProps> = props =>
|
||||
{
|
||||
const { node = null, child = false } = props;
|
||||
|
||||
return (
|
||||
<>
|
||||
{ node && (node.children.length > 0) && node.children.map((n, index) =>
|
||||
{
|
||||
if(!n.isVisible) return null;
|
||||
|
||||
return <CatalogNavigationItemView key={ index } child={ child } node={ n } />;
|
||||
}) }
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,34 @@
|
||||
import { FC } from 'react';
|
||||
import { ICatalogNode } from '../../../../api';
|
||||
import { AutoGrid, Column } from '../../../../common';
|
||||
import { useCatalog } from '../../../../hooks';
|
||||
import { CatalogSearchView } from '../page/common/CatalogSearchView';
|
||||
import { CatalogNavigationItemView } from './CatalogNavigationItemView';
|
||||
import { CatalogNavigationSetView } from './CatalogNavigationSetView';
|
||||
|
||||
export interface CatalogNavigationViewProps
|
||||
{
|
||||
node: ICatalogNode;
|
||||
}
|
||||
|
||||
export const CatalogNavigationView: FC<CatalogNavigationViewProps> = props =>
|
||||
{
|
||||
const { node = null } = props;
|
||||
const { searchResult = null } = useCatalog();
|
||||
|
||||
return (
|
||||
<>
|
||||
<CatalogSearchView />
|
||||
<Column fullHeight className="!border-[#b6bec5] bg-[#cdd3d9] border-[2px] border-[solid] rounded p-1" overflow="hidden">
|
||||
<AutoGrid columnCount={ 1 } gap={ 1 } id="nitro-catalog-main-navigation">
|
||||
{ searchResult && (searchResult.filteredNodes.length > 0) && searchResult.filteredNodes.map((n, index) =>
|
||||
{
|
||||
return <CatalogNavigationItemView key={ index } node={ n } />;
|
||||
}) }
|
||||
{ !searchResult &&
|
||||
<CatalogNavigationSetView node={ node } /> }
|
||||
</AutoGrid>
|
||||
</Column>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,59 @@
|
||||
import { MouseEventType } from '@nitrots/nitro-renderer';
|
||||
import { FC, MouseEvent, useMemo, useState } from 'react';
|
||||
import { IPurchasableOffer, Offer, ProductTypeEnum } from '../../../../../api';
|
||||
import { LayoutAvatarImageView, LayoutGridItem, LayoutGridItemProps } from '../../../../../common';
|
||||
import { useCatalog, useInventoryFurni } from '../../../../../hooks';
|
||||
|
||||
interface CatalogGridOfferViewProps extends LayoutGridItemProps
|
||||
{
|
||||
offer: IPurchasableOffer;
|
||||
selectOffer: (offer: IPurchasableOffer) => void;
|
||||
}
|
||||
|
||||
export const CatalogGridOfferView: FC<CatalogGridOfferViewProps> = props =>
|
||||
{
|
||||
const { offer = null, selectOffer = null, itemActive = false, ...rest } = props;
|
||||
const [ isMouseDown, setMouseDown ] = useState(false);
|
||||
const { requestOfferToMover = null } = useCatalog();
|
||||
const { isVisible = false } = useInventoryFurni();
|
||||
|
||||
const iconUrl = useMemo(() =>
|
||||
{
|
||||
if(offer.pricingModel === Offer.PRICING_MODEL_BUNDLE)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return offer.product.getIconUrl(offer);
|
||||
}, [ offer ]);
|
||||
|
||||
const onMouseEvent = (event: MouseEvent) =>
|
||||
{
|
||||
switch(event.type)
|
||||
{
|
||||
case MouseEventType.MOUSE_DOWN:
|
||||
selectOffer(offer);
|
||||
setMouseDown(true);
|
||||
return;
|
||||
case MouseEventType.MOUSE_UP:
|
||||
setMouseDown(false);
|
||||
return;
|
||||
case MouseEventType.ROLL_OUT:
|
||||
if(!isMouseDown || !itemActive || !isVisible) return;
|
||||
|
||||
requestOfferToMover(offer);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
const product = offer.product;
|
||||
|
||||
if(!product) return null;
|
||||
|
||||
return (
|
||||
<LayoutGridItem itemActive={ itemActive } itemCount={ ((offer.pricingModel === Offer.PRICING_MODEL_MULTI) ? product.productCount : 1) } itemImage={ iconUrl } itemUniqueNumber={ product.uniqueLimitedItemSeriesSize } itemUniqueSoldout={ (product.uniqueLimitedItemSeriesSize && !product.uniqueLimitedItemsLeft) } onMouseDown={ onMouseEvent } onMouseOut={ onMouseEvent } onMouseUp={ onMouseEvent } { ...rest }>
|
||||
{ (offer.product.productType === ProductTypeEnum.ROBOT) &&
|
||||
<LayoutAvatarImageView direction={ 3 } figure={ offer.product.extraParam } headOnly={ true } /> }
|
||||
</LayoutGridItem>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,67 @@
|
||||
import { RedeemVoucherMessageComposer, VoucherRedeemErrorMessageEvent, VoucherRedeemOkMessageEvent } from '@nitrots/nitro-renderer';
|
||||
import { FC, useState } from 'react';
|
||||
import { FaTag } from 'react-icons/fa';
|
||||
import { LocalizeText, SendMessageComposer } from '../../../../../api';
|
||||
import { Button } from '../../../../../common';
|
||||
import { useMessageEvent, useNotification } from '../../../../../hooks';
|
||||
import { NitroInput } from '../../../../../layout';
|
||||
|
||||
export interface CatalogRedeemVoucherViewProps
|
||||
{
|
||||
text: string;
|
||||
}
|
||||
|
||||
export const CatalogRedeemVoucherView: FC<CatalogRedeemVoucherViewProps> = props =>
|
||||
{
|
||||
const { text = null } = props;
|
||||
const [ voucher, setVoucher ] = useState<string>('');
|
||||
const [ isWaiting, setIsWaiting ] = useState(false);
|
||||
const { simpleAlert = null } = useNotification();
|
||||
|
||||
const redeemVoucher = () =>
|
||||
{
|
||||
if(!voucher || !voucher.length || isWaiting) return;
|
||||
|
||||
SendMessageComposer(new RedeemVoucherMessageComposer(voucher));
|
||||
|
||||
setIsWaiting(true);
|
||||
};
|
||||
|
||||
useMessageEvent<VoucherRedeemOkMessageEvent>(VoucherRedeemOkMessageEvent, event =>
|
||||
{
|
||||
const parser = event.getParser();
|
||||
|
||||
let message = LocalizeText('catalog.alert.voucherredeem.ok.description');
|
||||
|
||||
if(parser.productName) message = LocalizeText('catalog.alert.voucherredeem.ok.description.furni', [ 'productName', 'productDescription' ], [ parser.productName, parser.productDescription ]);
|
||||
|
||||
simpleAlert(message, null, null, null, LocalizeText('catalog.alert.voucherredeem.ok.title'));
|
||||
|
||||
setIsWaiting(false);
|
||||
setVoucher('');
|
||||
});
|
||||
|
||||
useMessageEvent<VoucherRedeemErrorMessageEvent>(VoucherRedeemErrorMessageEvent, event =>
|
||||
{
|
||||
const parser = event.getParser();
|
||||
|
||||
simpleAlert(LocalizeText(`catalog.alert.voucherredeem.error.description.${ parser.errorCode }`), null, null, null, LocalizeText('catalog.alert.voucherredeem.error.title'));
|
||||
|
||||
setIsWaiting(false);
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="flex gap-1">
|
||||
|
||||
|
||||
|
||||
<NitroInput
|
||||
placeholder={ text }
|
||||
value={ voucher }
|
||||
onChange={ event => setVoucher(event.target.value) } />
|
||||
<Button disabled={ isWaiting } variant="primary" onClick={ redeemVoucher }>
|
||||
<FaTag className="fa-icon" />
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,106 @@
|
||||
import { GetSessionDataManager, IFurnitureData } from '@nitrots/nitro-renderer';
|
||||
import { FC, useEffect, useState } from 'react';
|
||||
import { FaSearch, FaTimes } from 'react-icons/fa';
|
||||
import { CatalogPage, CatalogType, FilterCatalogNode, FurnitureOffer, GetOfferNodes, ICatalogNode, ICatalogPage, IPurchasableOffer, LocalizeText, PageLocalization, SearchResult } from '../../../../../api';
|
||||
import { Button, Flex } from '../../../../../common';
|
||||
import { useCatalog } from '../../../../../hooks';
|
||||
import { NitroInput } from '../../../../../layout';
|
||||
|
||||
export const CatalogSearchView: FC<{}> = props =>
|
||||
{
|
||||
const [ searchValue, setSearchValue ] = useState('');
|
||||
const { currentType = null, rootNode = null, offersToNodes = null, searchResult = null, setSearchResult = null, setCurrentPage = null } = useCatalog();
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
let search = searchValue?.toLocaleLowerCase().replace(' ', '');
|
||||
|
||||
if(!search || !search.length)
|
||||
{
|
||||
setSearchResult(null);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const timeout = setTimeout(() =>
|
||||
{
|
||||
const furnitureDatas = GetSessionDataManager().getAllFurnitureData();
|
||||
|
||||
if(!furnitureDatas || !furnitureDatas.length) return;
|
||||
|
||||
const foundFurniture: IFurnitureData[] = [];
|
||||
const foundFurniLines: string[] = [];
|
||||
|
||||
for(const furniture of furnitureDatas)
|
||||
{
|
||||
if((currentType === CatalogType.BUILDER) && !furniture.availableForBuildersClub) continue;
|
||||
|
||||
if((currentType === CatalogType.NORMAL) && furniture.excludeDynamic) continue;
|
||||
|
||||
const searchValues = [ furniture.className, furniture.name, furniture.description ].join(' ').replace(/ /gi, '').toLowerCase();
|
||||
|
||||
if((currentType === CatalogType.BUILDER) && (furniture.purchaseOfferId === -1) && (furniture.rentOfferId === -1))
|
||||
{
|
||||
if((furniture.furniLine !== '') && (foundFurniLines.indexOf(furniture.furniLine) < 0))
|
||||
{
|
||||
if(searchValues.indexOf(search) >= 0) foundFurniLines.push(furniture.furniLine);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
const foundNodes = [
|
||||
...GetOfferNodes(offersToNodes, furniture.purchaseOfferId),
|
||||
...GetOfferNodes(offersToNodes, furniture.rentOfferId)
|
||||
];
|
||||
|
||||
if(foundNodes.length)
|
||||
{
|
||||
if(searchValues.indexOf(search) >= 0) foundFurniture.push(furniture);
|
||||
|
||||
if(foundFurniture.length === 250) break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const offers: IPurchasableOffer[] = [];
|
||||
|
||||
for(const furniture of foundFurniture) offers.push(new FurnitureOffer(furniture));
|
||||
|
||||
let nodes: ICatalogNode[] = [];
|
||||
|
||||
FilterCatalogNode(search, foundFurniLines, rootNode, nodes);
|
||||
|
||||
setSearchResult(new SearchResult(search, offers, nodes.filter(node => (node.isVisible))));
|
||||
setCurrentPage((new CatalogPage(-1, 'default_3x3', new PageLocalization([], []), offers, false, 1) as ICatalogPage));
|
||||
}, 300);
|
||||
|
||||
return () => clearTimeout(timeout);
|
||||
}, [ offersToNodes, currentType, rootNode, searchValue, setCurrentPage, setSearchResult ]);
|
||||
|
||||
return (
|
||||
<div className="flex gap-1">
|
||||
<Flex fullWidth alignItems="center" position="relative">
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<NitroInput
|
||||
placeholder={ LocalizeText('generic.search') }
|
||||
value={ searchValue }
|
||||
onChange={ event => setSearchValue(event.target.value) } />
|
||||
|
||||
|
||||
</Flex>
|
||||
{ (!searchValue || !searchValue.length) &&
|
||||
<Button className="catalog-search-button" variant="primary">
|
||||
<FaSearch className="fa-icon" />
|
||||
</Button> }
|
||||
{ searchValue && !!searchValue.length &&
|
||||
<Button className="catalog-search-button" variant="primary" onClick={ event => setSearchValue('') }>
|
||||
<FaTimes className="fa-icon" />
|
||||
</Button> }
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,7 @@
|
||||
import { ICatalogPage } from '../../../../../api';
|
||||
|
||||
export interface CatalogLayoutProps
|
||||
{
|
||||
page: ICatalogPage;
|
||||
hideNavigation: () => void;
|
||||
}
|
||||
+54
@@ -0,0 +1,54 @@
|
||||
import { FC } from 'react';
|
||||
import { LocalizeText } from '../../../../../api';
|
||||
import { Column, Grid, Text } from '../../../../../common';
|
||||
import { useCatalog } from '../../../../../hooks';
|
||||
import { CatalogBadgeSelectorWidgetView } from '../widgets/CatalogBadgeSelectorWidgetView';
|
||||
import { CatalogFirstProductSelectorWidgetView } from '../widgets/CatalogFirstProductSelectorWidgetView';
|
||||
import { CatalogItemGridWidgetView } from '../widgets/CatalogItemGridWidgetView';
|
||||
import { CatalogLimitedItemWidgetView } from '../widgets/CatalogLimitedItemWidgetView';
|
||||
import { CatalogPurchaseWidgetView } from '../widgets/CatalogPurchaseWidgetView';
|
||||
import { CatalogTotalPriceWidget } from '../widgets/CatalogTotalPriceWidget';
|
||||
import { CatalogViewProductWidgetView } from '../widgets/CatalogViewProductWidgetView';
|
||||
import { CatalogLayoutProps } from './CatalogLayout.types';
|
||||
|
||||
export const CatalogLayoutBadgeDisplayView: FC<CatalogLayoutProps> = props =>
|
||||
{
|
||||
const { page = null } = props;
|
||||
const { currentOffer = null } = useCatalog();
|
||||
|
||||
return (
|
||||
<>
|
||||
<CatalogFirstProductSelectorWidgetView />
|
||||
<Grid>
|
||||
<Column overflow="hidden" size={ 7 }>
|
||||
<CatalogItemGridWidgetView shrink />
|
||||
<Column gap={ 1 } overflow="hidden">
|
||||
<Text shrink truncate fontWeight="bold">{ LocalizeText('catalog_selectbadge') }</Text>
|
||||
<CatalogBadgeSelectorWidgetView />
|
||||
</Column>
|
||||
</Column>
|
||||
<Column center={ !currentOffer } overflow="hidden" size={ 5 }>
|
||||
{ !currentOffer &&
|
||||
<>
|
||||
{ !!page.localization.getImage(1) && <img alt="" src={ page.localization.getImage(1) } /> }
|
||||
<Text center dangerouslySetInnerHTML={ { __html: page.localization.getText(0) } } />
|
||||
</> }
|
||||
{ currentOffer &&
|
||||
<>
|
||||
<div className="relative overflow-hidden">
|
||||
<CatalogViewProductWidgetView />
|
||||
</div>
|
||||
<Column className="!flex-grow" gap={ 1 }>
|
||||
<CatalogLimitedItemWidgetView />
|
||||
<Text truncate className="!flex-grow">{ currentOffer.localizationName }</Text>
|
||||
<div className="flex justify-end">
|
||||
<CatalogTotalPriceWidget alignItems="end" />
|
||||
</div>
|
||||
<CatalogPurchaseWidgetView />
|
||||
</Column>
|
||||
</> }
|
||||
</Column>
|
||||
</Grid>
|
||||
</>
|
||||
);
|
||||
};
|
||||
+176
@@ -0,0 +1,176 @@
|
||||
import { ColorConverter } from '@nitrots/nitro-renderer';
|
||||
import { FC, useMemo, useState } from 'react';
|
||||
import { FaFillDrip } from 'react-icons/fa';
|
||||
import { IPurchasableOffer } from '../../../../../api';
|
||||
import { AutoGrid, Button, Column, Grid, LayoutGridItem, Text } from '../../../../../common';
|
||||
import { useCatalog } from '../../../../../hooks';
|
||||
import { CatalogGridOfferView } from '../common/CatalogGridOfferView';
|
||||
import { CatalogAddOnBadgeWidgetView } from '../widgets/CatalogAddOnBadgeWidgetView';
|
||||
import { CatalogLimitedItemWidgetView } from '../widgets/CatalogLimitedItemWidgetView';
|
||||
import { CatalogPurchaseWidgetView } from '../widgets/CatalogPurchaseWidgetView';
|
||||
import { CatalogSpinnerWidgetView } from '../widgets/CatalogSpinnerWidgetView';
|
||||
import { CatalogTotalPriceWidget } from '../widgets/CatalogTotalPriceWidget';
|
||||
import { CatalogViewProductWidgetView } from '../widgets/CatalogViewProductWidgetView';
|
||||
import { CatalogLayoutProps } from './CatalogLayout.types';
|
||||
|
||||
export interface CatalogLayoutColorGroupViewProps extends CatalogLayoutProps
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
export const CatalogLayoutColorGroupingView: FC<CatalogLayoutColorGroupViewProps> = props =>
|
||||
{
|
||||
const { page = null } = props;
|
||||
const [ colorableItems, setColorableItems ] = useState<Map<string, number[]>>(new Map<string, number[]>());
|
||||
const { currentOffer = null, setCurrentOffer = null } = useCatalog();
|
||||
const [ colorsShowing, setColorsShowing ] = useState<boolean>(false);
|
||||
|
||||
const sortByColorIndex = (a: IPurchasableOffer, b: IPurchasableOffer) =>
|
||||
{
|
||||
if(((!(a.product.furnitureData.colorIndex)) || (!(b.product.furnitureData.colorIndex))))
|
||||
{
|
||||
return 1;
|
||||
}
|
||||
if(a.product.furnitureData.colorIndex > b.product.furnitureData.colorIndex)
|
||||
{
|
||||
return 1;
|
||||
}
|
||||
if(a == b)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
return -1;
|
||||
};
|
||||
|
||||
const sortyByFurnitureClassName = (a: IPurchasableOffer, b: IPurchasableOffer) =>
|
||||
{
|
||||
if(a.product.furnitureData.className > b.product.furnitureData.className)
|
||||
{
|
||||
return 1;
|
||||
}
|
||||
if(a == b)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
return -1;
|
||||
};
|
||||
|
||||
const selectOffer = (offer: IPurchasableOffer) =>
|
||||
{
|
||||
offer.activate();
|
||||
setCurrentOffer(offer);
|
||||
};
|
||||
|
||||
const selectColor = (colorIndex: number, productName: string) =>
|
||||
{
|
||||
const fullName = `${ productName }*${ colorIndex }`;
|
||||
const index = page.offers.findIndex(offer => offer.product.furnitureData.fullName === fullName);
|
||||
if(index > -1)
|
||||
{
|
||||
selectOffer(page.offers[index]);
|
||||
}
|
||||
};
|
||||
|
||||
const offers = useMemo(() =>
|
||||
{
|
||||
const offers: IPurchasableOffer[] = [];
|
||||
const addedColorableItems = new Map<string, boolean>();
|
||||
const updatedColorableItems = new Map<string, number[]>();
|
||||
|
||||
page.offers.sort(sortByColorIndex);
|
||||
|
||||
page.offers.forEach(offer =>
|
||||
{
|
||||
if(!offer.product) return;
|
||||
|
||||
const furniData = offer.product.furnitureData;
|
||||
|
||||
if(!furniData || !furniData.hasIndexedColor)
|
||||
{
|
||||
offers.push(offer);
|
||||
}
|
||||
else
|
||||
{
|
||||
const name = furniData.className;
|
||||
const colorIndex = furniData.colorIndex;
|
||||
|
||||
if(!updatedColorableItems.has(name))
|
||||
{
|
||||
updatedColorableItems.set(name, []);
|
||||
}
|
||||
|
||||
let selectedColor = 0xFFFFFF;
|
||||
|
||||
if(furniData.colors)
|
||||
{
|
||||
for(let color of furniData.colors)
|
||||
{
|
||||
if(color !== 0xFFFFFF) // skip the white colors
|
||||
{
|
||||
selectedColor = color;
|
||||
}
|
||||
}
|
||||
|
||||
if(updatedColorableItems.get(name).indexOf(selectedColor) === -1)
|
||||
{
|
||||
updatedColorableItems.get(name)[colorIndex] = selectedColor;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if(!addedColorableItems.has(name))
|
||||
{
|
||||
offers.push(offer);
|
||||
addedColorableItems.set(name, true);
|
||||
}
|
||||
}
|
||||
});
|
||||
offers.sort(sortyByFurnitureClassName);
|
||||
setColorableItems(updatedColorableItems);
|
||||
return offers;
|
||||
}, [ page.offers ]);
|
||||
|
||||
return (
|
||||
<Grid>
|
||||
<Column overflow="hidden" size={ 7 }>
|
||||
<AutoGrid columnCount={ 5 }>
|
||||
{ (!colorsShowing || !currentOffer || !colorableItems.has(currentOffer.product.furnitureData.className)) &&
|
||||
offers.map((offer, index) => <CatalogGridOfferView key={ index } itemActive={ (currentOffer && (currentOffer.product.furnitureData.hasIndexedColor ? currentOffer.product.furnitureData.className === offer.product.furnitureData.className : currentOffer.offerId === offer.offerId)) } offer={ offer } selectOffer={ selectOffer } />)
|
||||
}
|
||||
{ (colorsShowing && currentOffer && colorableItems.has(currentOffer.product.furnitureData.className)) &&
|
||||
colorableItems.get(currentOffer.product.furnitureData.className).map((color, index) => <LayoutGridItem key={ index } itemHighlight className="clear-bg" itemActive={ (currentOffer.product.furnitureData.colorIndex === index) } itemColor={ ColorConverter.int2rgb(color) } onClick={ event => selectColor(index, currentOffer.product.furnitureData.className) } />)
|
||||
}
|
||||
</AutoGrid>
|
||||
</Column>
|
||||
<Column center={ !currentOffer } overflow="hidden" size={ 5 }>
|
||||
{ !currentOffer &&
|
||||
<>
|
||||
{ !!page.localization.getImage(1) && <img alt="" src={ page.localization.getImage(1) } /> }
|
||||
<Text center dangerouslySetInnerHTML={ { __html: page.localization.getText(0) } } />
|
||||
</> }
|
||||
{ currentOffer &&
|
||||
<>
|
||||
<div className="relative overflow-hidden">
|
||||
<CatalogViewProductWidgetView />
|
||||
<CatalogAddOnBadgeWidgetView className="bg-muted rounded bottom-1 end-1" position="absolute" />
|
||||
{ currentOffer.product.furnitureData.hasIndexedColor &&
|
||||
<Button className="bottom-1 start-1" position="absolute" onClick={ event => setColorsShowing(prev => !prev) }>
|
||||
<FaFillDrip className="fa-icon" />
|
||||
</Button> }
|
||||
</div>
|
||||
<Column className="!flex-grow" gap={ 1 }>
|
||||
<CatalogLimitedItemWidgetView />
|
||||
<Text truncate className="!flex-grow">{ currentOffer.localizationName }</Text>
|
||||
<div className="flex justify-between">
|
||||
<div className="flex flex-col gap-1">
|
||||
<CatalogSpinnerWidgetView />
|
||||
</div>
|
||||
<CatalogTotalPriceWidget alignItems="end" justifyContent="end" />
|
||||
</div>
|
||||
<CatalogPurchaseWidgetView />
|
||||
</Column>
|
||||
</> }
|
||||
</Column>
|
||||
</Grid>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,61 @@
|
||||
import { FC } from 'react';
|
||||
import { GetConfigurationValue, ProductTypeEnum } from '../../../../../api';
|
||||
import { Column, Flex, Grid, LayoutImage, Text } from '../../../../../common';
|
||||
import { useCatalog } from '../../../../../hooks';
|
||||
import { CatalogHeaderView } from '../../catalog-header/CatalogHeaderView';
|
||||
import { CatalogAddOnBadgeWidgetView } from '../widgets/CatalogAddOnBadgeWidgetView';
|
||||
import { CatalogItemGridWidgetView } from '../widgets/CatalogItemGridWidgetView';
|
||||
import { CatalogLimitedItemWidgetView } from '../widgets/CatalogLimitedItemWidgetView';
|
||||
import { CatalogPurchaseWidgetView } from '../widgets/CatalogPurchaseWidgetView';
|
||||
import { CatalogSpinnerWidgetView } from '../widgets/CatalogSpinnerWidgetView';
|
||||
import { CatalogTotalPriceWidget } from '../widgets/CatalogTotalPriceWidget';
|
||||
import { CatalogViewProductWidgetView } from '../widgets/CatalogViewProductWidgetView';
|
||||
import { CatalogLayoutProps } from './CatalogLayout.types';
|
||||
|
||||
export const CatalogLayoutDefaultView: FC<CatalogLayoutProps> = props =>
|
||||
{
|
||||
const { page = null } = props;
|
||||
const { currentOffer = null, currentPage = null } = useCatalog();
|
||||
|
||||
return (
|
||||
<>
|
||||
<Grid>
|
||||
<Column overflow="hidden" size={ 7 }>
|
||||
{ GetConfigurationValue('catalog.headers') &&
|
||||
<CatalogHeaderView imageUrl={ currentPage.localization.getImage(0) } /> }
|
||||
<CatalogItemGridWidgetView />
|
||||
</Column>
|
||||
<Column center={ !currentOffer } overflow="hidden" size={ 5 }>
|
||||
{ !currentOffer &&
|
||||
<>
|
||||
{ !!page.localization.getImage(1) &&
|
||||
<LayoutImage imageUrl={ page.localization.getImage(1) } /> }
|
||||
<Text center dangerouslySetInnerHTML={ { __html: page.localization.getText(0) } } />
|
||||
</> }
|
||||
{ currentOffer &&
|
||||
<>
|
||||
<Flex center overflow="hidden" style={ { height: 140 } }>
|
||||
{ (currentOffer.product.productType !== ProductTypeEnum.BADGE) &&
|
||||
<>
|
||||
<CatalogViewProductWidgetView />
|
||||
<CatalogAddOnBadgeWidgetView className="bg-muted rounded bottom-1 end-1" />
|
||||
</> }
|
||||
{ (currentOffer.product.productType === ProductTypeEnum.BADGE) && <CatalogAddOnBadgeWidgetView className="scale-2" /> }
|
||||
</Flex>
|
||||
<Column grow gap={ 1 }>
|
||||
<CatalogLimitedItemWidgetView />
|
||||
<Text grow truncate>{ currentOffer.localizationName }</Text>
|
||||
<div className="flex justify-between">
|
||||
<div className="flex flex-col gap-1">
|
||||
<CatalogSpinnerWidgetView />
|
||||
</div>
|
||||
<CatalogTotalPriceWidget alignItems="end" justifyContent="end" />
|
||||
</div>
|
||||
<CatalogPurchaseWidgetView />
|
||||
</Column>
|
||||
</> }
|
||||
</Column>
|
||||
</Grid>
|
||||
</>
|
||||
);
|
||||
};
|
||||
+48
@@ -0,0 +1,48 @@
|
||||
import { FC } from 'react';
|
||||
import { Column, Grid, Text } from '../../../../../common';
|
||||
import { useCatalog } from '../../../../../hooks';
|
||||
import { CatalogGuildBadgeWidgetView } from '../widgets/CatalogGuildBadgeWidgetView';
|
||||
import { CatalogGuildSelectorWidgetView } from '../widgets/CatalogGuildSelectorWidgetView';
|
||||
import { CatalogItemGridWidgetView } from '../widgets/CatalogItemGridWidgetView';
|
||||
import { CatalogPurchaseWidgetView } from '../widgets/CatalogPurchaseWidgetView';
|
||||
import { CatalogTotalPriceWidget } from '../widgets/CatalogTotalPriceWidget';
|
||||
import { CatalogViewProductWidgetView } from '../widgets/CatalogViewProductWidgetView';
|
||||
import { CatalogLayoutProps } from './CatalogLayout.types';
|
||||
|
||||
export const CatalogLayouGuildCustomFurniView: FC<CatalogLayoutProps> = props =>
|
||||
{
|
||||
const { page = null } = props;
|
||||
const { currentOffer = null } = useCatalog();
|
||||
|
||||
return (
|
||||
<Grid>
|
||||
<Column overflow="hidden" size={ 7 }>
|
||||
<CatalogItemGridWidgetView />
|
||||
</Column>
|
||||
<Column center={ !currentOffer } overflow="hidden" size={ 5 }>
|
||||
{ !currentOffer &&
|
||||
<>
|
||||
{ !!page.localization.getImage(1) && <img alt="" src={ page.localization.getImage(1) } /> }
|
||||
<Text center dangerouslySetInnerHTML={ { __html: page.localization.getText(0) } } />
|
||||
</> }
|
||||
{ currentOffer &&
|
||||
<>
|
||||
<div className="relative overflow-hidden">
|
||||
<CatalogViewProductWidgetView />
|
||||
<CatalogGuildBadgeWidgetView className="bottom-1 end-1" position="absolute" />
|
||||
</div>
|
||||
<Column grow gap={ 1 }>
|
||||
<Text truncate>{ currentOffer.localizationName }</Text>
|
||||
<div className="!flex-grow">
|
||||
<CatalogGuildSelectorWidgetView />
|
||||
</div>
|
||||
<div className="flex justify-end">
|
||||
<CatalogTotalPriceWidget alignItems="end" />
|
||||
</div>
|
||||
<CatalogPurchaseWidgetView />
|
||||
</Column>
|
||||
</> }
|
||||
</Column>
|
||||
</Grid>
|
||||
);
|
||||
};
|
||||
+49
@@ -0,0 +1,49 @@
|
||||
import { CatalogGroupsComposer } from '@nitrots/nitro-renderer';
|
||||
import { FC, useEffect, useState } from 'react';
|
||||
import { SendMessageComposer } from '../../../../../api';
|
||||
import { Column, Grid, Text } from '../../../../../common';
|
||||
import { useCatalog } from '../../../../../hooks';
|
||||
import { CatalogFirstProductSelectorWidgetView } from '../widgets/CatalogFirstProductSelectorWidgetView';
|
||||
import { CatalogGuildSelectorWidgetView } from '../widgets/CatalogGuildSelectorWidgetView';
|
||||
import { CatalogPurchaseWidgetView } from '../widgets/CatalogPurchaseWidgetView';
|
||||
import { CatalogTotalPriceWidget } from '../widgets/CatalogTotalPriceWidget';
|
||||
import { CatalogLayoutProps } from './CatalogLayout.types';
|
||||
|
||||
export const CatalogLayouGuildForumView: FC<CatalogLayoutProps> = props =>
|
||||
{
|
||||
const { page = null } = props;
|
||||
const [ selectedGroupIndex, setSelectedGroupIndex ] = useState<number>(0);
|
||||
const { currentOffer = null, setCurrentOffer = null, catalogOptions = null } = useCatalog();
|
||||
const { groups = null } = catalogOptions;
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
SendMessageComposer(new CatalogGroupsComposer());
|
||||
}, [ page ]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<CatalogFirstProductSelectorWidgetView />
|
||||
<Grid>
|
||||
<Column className="bg-muted rounded p-2 text-black" overflow="hidden" size={ 7 }>
|
||||
<div className="overflow-auto" dangerouslySetInnerHTML={ { __html: page.localization.getText(1) } } />
|
||||
</Column>
|
||||
<Column gap={ 1 } overflow="hidden" size={ 5 }>
|
||||
{ !!currentOffer &&
|
||||
<>
|
||||
<Column grow gap={ 1 }>
|
||||
<Text truncate>{ currentOffer.localizationName }</Text>
|
||||
<div className="!flex-grow">
|
||||
<CatalogGuildSelectorWidgetView />
|
||||
</div>
|
||||
<div className="flex justify-end">
|
||||
<CatalogTotalPriceWidget alignItems="end" />
|
||||
</div>
|
||||
<CatalogPurchaseWidgetView noGiftOption={ true } />
|
||||
</Column>
|
||||
</> }
|
||||
</Column>
|
||||
</Grid>
|
||||
</>
|
||||
);
|
||||
};
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
import { CreateLinkEvent } from '@nitrots/nitro-renderer';
|
||||
import { FC } from 'react';
|
||||
import { LocalizeText } from '../../../../../api';
|
||||
import { Button } from '../../../../../common/Button';
|
||||
import { Column } from '../../../../../common/Column';
|
||||
import { Grid } from '../../../../../common/Grid';
|
||||
import { LayoutImage } from '../../../../../common/layout/LayoutImage';
|
||||
import { CatalogLayoutProps } from './CatalogLayout.types';
|
||||
|
||||
export const CatalogLayouGuildFrontpageView: FC<CatalogLayoutProps> = props =>
|
||||
{
|
||||
const { page = null } = props;
|
||||
|
||||
return (
|
||||
<Grid>
|
||||
<Column className="bg-muted rounded p-2 text-black" overflow="hidden" size={ 7 }>
|
||||
<div dangerouslySetInnerHTML={ { __html: page.localization.getText(2) } } />
|
||||
<div className="overflow-auto" dangerouslySetInnerHTML={ { __html: page.localization.getText(0) } } />
|
||||
<div dangerouslySetInnerHTML={ { __html: page.localization.getText(1) } } />
|
||||
</Column>
|
||||
<Column center overflow="hidden" size={ 5 }>
|
||||
<LayoutImage imageUrl={ page.localization.getImage(1) } />
|
||||
<Button onClick={ () => CreateLinkEvent('groups/create') }>
|
||||
{ LocalizeText('catalog.start.guild.purchase.button') }
|
||||
</Button>
|
||||
</Column>
|
||||
</Grid>
|
||||
);
|
||||
};
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
import { FC } from 'react';
|
||||
import { CatalogLayoutProps } from './CatalogLayout.types';
|
||||
|
||||
export const CatalogLayoutInfoLoyaltyView: FC<CatalogLayoutProps> = props =>
|
||||
{
|
||||
const { page = null } = props;
|
||||
|
||||
return (
|
||||
<div className="h-full nitro-catalog-layout-info-loyalty text-black flex flex-row">
|
||||
<div className="overflow-auto h-full flex flex-col info-loyalty-content">
|
||||
<div dangerouslySetInnerHTML={ { __html: page.localization.getText(0) } } />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,8 @@
|
||||
import { FC } from 'react';
|
||||
import { CatalogLayoutProps } from './CatalogLayout.types';
|
||||
import { CatalogLayoutPets3View } from './CatalogLayoutPets3View';
|
||||
|
||||
export const CatalogLayoutPets2View: FC<CatalogLayoutProps> = props =>
|
||||
{
|
||||
return <CatalogLayoutPets3View { ...props } />;
|
||||
};
|
||||
@@ -0,0 +1,25 @@
|
||||
import { FC } from 'react';
|
||||
import { Column } from '../../../../../common';
|
||||
import { CatalogLayoutProps } from './CatalogLayout.types';
|
||||
|
||||
export const CatalogLayoutPets3View: FC<CatalogLayoutProps> = props =>
|
||||
{
|
||||
const { page = null } = props;
|
||||
|
||||
const imageUrl = page.localization.getImage(1);
|
||||
|
||||
return (
|
||||
<Column grow className="bg-muted rounded text-black p-2" overflow="hidden">
|
||||
<div className="items-center gap-2">
|
||||
{ imageUrl && <img alt="" src={ imageUrl } /> }
|
||||
<div className="fs-5" dangerouslySetInnerHTML={ { __html: page.localization.getText(1) } } />
|
||||
</div>
|
||||
<Column grow alignItems="center" overflow="auto">
|
||||
<div dangerouslySetInnerHTML={ { __html: page.localization.getText(2) } } />
|
||||
</Column>
|
||||
<div className="flex items-center">
|
||||
<div className="font-bold " dangerouslySetInnerHTML={ { __html: page.localization.getText(3) } } />
|
||||
</div>
|
||||
</Column>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,116 @@
|
||||
import { GetRoomAdPurchaseInfoComposer, GetUserEventCatsMessageComposer, PurchaseRoomAdMessageComposer, RoomAdPurchaseInfoEvent, RoomEntryData } from '@nitrots/nitro-renderer';
|
||||
import { FC, useEffect, useState } from 'react';
|
||||
import { LocalizeText, SendMessageComposer } from '../../../../../api';
|
||||
import { Button, Column, Text } from '../../../../../common';
|
||||
import { useCatalog, useMessageEvent, useNavigator, useRoomPromote } from '../../../../../hooks';
|
||||
import { NitroInput } from '../../../../../layout';
|
||||
import { CatalogLayoutProps } from './CatalogLayout.types';
|
||||
|
||||
export const CatalogLayoutRoomAdsView: FC<CatalogLayoutProps> = props =>
|
||||
{
|
||||
const { page = null } = props;
|
||||
const [ eventName, setEventName ] = useState<string>('');
|
||||
const [ eventDesc, setEventDesc ] = useState<string>('');
|
||||
const [ roomId, setRoomId ] = useState<number>(-1);
|
||||
const [ availableRooms, setAvailableRooms ] = useState<RoomEntryData[]>([]);
|
||||
const [ extended, setExtended ] = useState<boolean>(false);
|
||||
const [ categoryId, setCategoryId ] = useState<number>(1);
|
||||
const { categories = null } = useNavigator();
|
||||
const { setIsVisible = null } = useCatalog();
|
||||
const { promoteInformation, isExtended, setIsExtended } = useRoomPromote();
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
if(isExtended)
|
||||
{
|
||||
setRoomId(promoteInformation.data.flatId);
|
||||
setEventName(promoteInformation.data.eventName);
|
||||
setEventDesc(promoteInformation.data.eventDescription);
|
||||
setCategoryId(promoteInformation.data.categoryId);
|
||||
setExtended(isExtended); // This is for sending to packet
|
||||
setIsExtended(false); // This is from hook useRoomPromotte
|
||||
}
|
||||
|
||||
}, [ isExtended, eventName, eventDesc, categoryId, promoteInformation.data, setIsExtended ]);
|
||||
|
||||
const resetData = () =>
|
||||
{
|
||||
setRoomId(-1);
|
||||
setEventName('');
|
||||
setEventDesc('');
|
||||
setCategoryId(1);
|
||||
setIsExtended(false);
|
||||
setIsVisible(false);
|
||||
};
|
||||
|
||||
const purchaseAd = () =>
|
||||
{
|
||||
const pageId = page.pageId;
|
||||
const offerId = page.offers.length >= 1 ? page.offers[0].offerId : -1;
|
||||
const flatId = roomId;
|
||||
const name = eventName;
|
||||
const desc = eventDesc;
|
||||
const catId = categoryId;
|
||||
|
||||
SendMessageComposer(new PurchaseRoomAdMessageComposer(pageId, offerId, flatId, name, extended, desc, catId));
|
||||
resetData();
|
||||
};
|
||||
|
||||
useMessageEvent<RoomAdPurchaseInfoEvent>(RoomAdPurchaseInfoEvent, event =>
|
||||
{
|
||||
const parser = event.getParser();
|
||||
|
||||
if(!parser) return;
|
||||
|
||||
setAvailableRooms(parser.rooms);
|
||||
});
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
SendMessageComposer(new GetRoomAdPurchaseInfoComposer());
|
||||
// TODO: someone needs to fix this for morningstar
|
||||
SendMessageComposer(new GetUserEventCatsMessageComposer());
|
||||
}, []);
|
||||
|
||||
return (<>
|
||||
<Text bold center>{ LocalizeText('roomad.catalog_header') }</Text>
|
||||
<Column className="text-black" overflow="hidden" size={ 12 }>
|
||||
<div>{ LocalizeText('roomad.catalog_text', [ 'duration' ], [ '120' ]) }</div>
|
||||
<div className="p-1 rounded bg-muted">
|
||||
<Column gap={ 2 }>
|
||||
<Text bold>{ LocalizeText('navigator.category') }</Text>
|
||||
<select className="form-select form-select-sm" disabled={ extended } value={ categoryId } onChange={ event => setCategoryId(parseInt(event.target.value)) }>
|
||||
{ categories && categories.map((cat, index) => <option key={ index } value={ cat.id }>{ LocalizeText(cat.name) }</option>) }
|
||||
</select>
|
||||
</Column>
|
||||
<div className="flex flex-col gap-1">
|
||||
<Text bold>{ LocalizeText('roomad.catalog_name') }</Text>
|
||||
<NitroInput maxLength={ 64 } readOnly={ extended } value={ eventName } onChange={ event => setEventName(event.target.value) } />
|
||||
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
<Text bold>{ LocalizeText('roomad.catalog_description') }</Text>
|
||||
<textarea className="min-h-[calc(1.5em+ .5rem+2px)] px-[.5rem] py-[.25rem] rounded-[.2rem] form-control-sm" maxLength={ 64 } readOnly={ extended } value={ eventDesc } onChange={ event => setEventDesc(event.target.value) } />
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
<Text bold>{ LocalizeText('roomad.catalog_roomname') }</Text>
|
||||
<select className="form-select form-select-sm" disabled={ extended } value={ roomId } onChange={ event => setRoomId(parseInt(event.target.value)) }>
|
||||
<option disabled value={ -1 }>{ LocalizeText('roomad.catalog_roomname') }</option>
|
||||
{ availableRooms && availableRooms.map((room, index) => <option key={ index } value={ room.roomId }>{ room.roomName }</option>) }
|
||||
</select>
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
<Button disabled={ (!eventName || !eventDesc || roomId === -1) } variant={ (!eventName || !eventDesc || roomId === -1) ? 'danger' : 'success' } onClick={ purchaseAd }>{ extended ? LocalizeText('roomad.extend.event') : LocalizeText('buy') }</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Column>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
interface INavigatorCategory
|
||||
{
|
||||
id: number;
|
||||
name: string;
|
||||
visible: boolean;
|
||||
}
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
import { FC } from 'react';
|
||||
import { Column, Grid, Text } from '../../../../../common';
|
||||
import { CatalogAddOnBadgeWidgetView } from '../widgets/CatalogAddOnBadgeWidgetView';
|
||||
import { CatalogBundleGridWidgetView } from '../widgets/CatalogBundleGridWidgetView';
|
||||
import { CatalogFirstProductSelectorWidgetView } from '../widgets/CatalogFirstProductSelectorWidgetView';
|
||||
import { CatalogPurchaseWidgetView } from '../widgets/CatalogPurchaseWidgetView';
|
||||
import { CatalogSimplePriceWidgetView } from '../widgets/CatalogSimplePriceWidgetView';
|
||||
import { CatalogLayoutProps } from './CatalogLayout.types';
|
||||
|
||||
export const CatalogLayoutRoomBundleView: FC<CatalogLayoutProps> = props =>
|
||||
{
|
||||
const { page = null } = props;
|
||||
|
||||
return (
|
||||
<>
|
||||
<CatalogFirstProductSelectorWidgetView />
|
||||
<Grid>
|
||||
<Column overflow="hidden" size={ 7 }>
|
||||
{ !!page.localization.getText(2) &&
|
||||
<Text dangerouslySetInnerHTML={ { __html: page.localization.getText(2) } } /> }
|
||||
<Column grow className="bg-muted p-2 rounded" overflow="hidden">
|
||||
<CatalogBundleGridWidgetView fullWidth className="nitro-catalog-layout-bundle-grid" />
|
||||
</Column>
|
||||
</Column>
|
||||
<Column gap={ 1 } overflow="hidden" size={ 5 }>
|
||||
{ !!page.localization.getText(1) &&
|
||||
<Text center small overflow="auto">{ page.localization.getText(1) }</Text> }
|
||||
<Column grow gap={ 0 } overflow="hidden" position="relative">
|
||||
{ !!page.localization.getImage(1) &&
|
||||
<img alt="" className="!flex-grow" src={ page.localization.getImage(1) } /> }
|
||||
<CatalogAddOnBadgeWidgetView className="bg-muted rounded bottom-0 start-0" position="absolute" />
|
||||
<CatalogSimplePriceWidgetView />
|
||||
</Column>
|
||||
<div className="flex flex-col gap-1">
|
||||
<CatalogPurchaseWidgetView />
|
||||
</div>
|
||||
</Column>
|
||||
</Grid>
|
||||
</>
|
||||
);
|
||||
};
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
import { FC } from 'react';
|
||||
import { Column, Grid, Text } from '../../../../../common';
|
||||
import { CatalogAddOnBadgeWidgetView } from '../widgets/CatalogAddOnBadgeWidgetView';
|
||||
import { CatalogBundleGridWidgetView } from '../widgets/CatalogBundleGridWidgetView';
|
||||
import { CatalogFirstProductSelectorWidgetView } from '../widgets/CatalogFirstProductSelectorWidgetView';
|
||||
import { CatalogPurchaseWidgetView } from '../widgets/CatalogPurchaseWidgetView';
|
||||
import { CatalogSimplePriceWidgetView } from '../widgets/CatalogSimplePriceWidgetView';
|
||||
import { CatalogLayoutProps } from './CatalogLayout.types';
|
||||
|
||||
export const CatalogLayoutSingleBundleView: FC<CatalogLayoutProps> = props =>
|
||||
{
|
||||
const { page = null } = props;
|
||||
|
||||
return (
|
||||
<>
|
||||
<CatalogFirstProductSelectorWidgetView />
|
||||
<Grid>
|
||||
<Column overflow="hidden" size={ 7 }>
|
||||
{ !!page.localization.getText(2) &&
|
||||
<Text dangerouslySetInnerHTML={ { __html: page.localization.getText(2) } } /> }
|
||||
<Column grow className="bg-muted p-2 rounded" overflow="hidden">
|
||||
<CatalogBundleGridWidgetView fullWidth className="nitro-catalog-layout-bundle-grid" />
|
||||
</Column>
|
||||
</Column>
|
||||
<Column gap={ 1 } overflow="hidden" size={ 5 }>
|
||||
{ !!page.localization.getText(1) &&
|
||||
<Text center small overflow="auto">{ page.localization.getText(1) }</Text> }
|
||||
<Column grow gap={ 0 } overflow="hidden" position="relative">
|
||||
{ !!page.localization.getImage(1) &&
|
||||
<img alt="" className="!flex-grow" src={ page.localization.getImage(1) } /> }
|
||||
<CatalogAddOnBadgeWidgetView className="bg-muted rounded bottom-0 start-0" position="absolute" />
|
||||
<CatalogSimplePriceWidgetView />
|
||||
</Column>
|
||||
<div className="flex flex-col gap-1">
|
||||
<CatalogPurchaseWidgetView />
|
||||
</div>
|
||||
</Column>
|
||||
</Grid>
|
||||
</>
|
||||
);
|
||||
};
|
||||
+113
@@ -0,0 +1,113 @@
|
||||
import { GetOfficialSongIdMessageComposer, GetSoundManager, MusicPriorities, OfficialSongIdMessageEvent } from '@nitrots/nitro-renderer';
|
||||
import { FC, useEffect, useState } from 'react';
|
||||
import { GetConfigurationValue, LocalizeText, ProductTypeEnum, SendMessageComposer } from '../../../../../api';
|
||||
import { Button, Column, Grid, LayoutImage, Text } from '../../../../../common';
|
||||
import { useCatalog, useMessageEvent } from '../../../../../hooks';
|
||||
import { CatalogHeaderView } from '../../catalog-header/CatalogHeaderView';
|
||||
import { CatalogAddOnBadgeWidgetView } from '../widgets/CatalogAddOnBadgeWidgetView';
|
||||
import { CatalogItemGridWidgetView } from '../widgets/CatalogItemGridWidgetView';
|
||||
import { CatalogLimitedItemWidgetView } from '../widgets/CatalogLimitedItemWidgetView';
|
||||
import { CatalogPurchaseWidgetView } from '../widgets/CatalogPurchaseWidgetView';
|
||||
import { CatalogSpinnerWidgetView } from '../widgets/CatalogSpinnerWidgetView';
|
||||
import { CatalogTotalPriceWidget } from '../widgets/CatalogTotalPriceWidget';
|
||||
import { CatalogViewProductWidgetView } from '../widgets/CatalogViewProductWidgetView';
|
||||
import { CatalogLayoutProps } from './CatalogLayout.types';
|
||||
|
||||
export const CatalogLayoutSoundMachineView: FC<CatalogLayoutProps> = props =>
|
||||
{
|
||||
const { page = null } = props;
|
||||
const [ songId, setSongId ] = useState(-1);
|
||||
const [ officialSongId, setOfficialSongId ] = useState('');
|
||||
const { currentOffer = null, currentPage = null } = useCatalog();
|
||||
|
||||
const previewSong = (previewSongId: number) => GetSoundManager().musicController?.playSong(previewSongId, MusicPriorities.PRIORITY_PURCHASE_PREVIEW, 15, 0, 0, 0);
|
||||
|
||||
useMessageEvent<OfficialSongIdMessageEvent>(OfficialSongIdMessageEvent, event =>
|
||||
{
|
||||
const parser = event.getParser();
|
||||
|
||||
if(parser.officialSongId !== officialSongId) return;
|
||||
|
||||
setSongId(parser.songId);
|
||||
});
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
if(!currentOffer) return;
|
||||
|
||||
const product = currentOffer.product;
|
||||
|
||||
if(!product) return;
|
||||
|
||||
if(product.extraParam.length > 0)
|
||||
{
|
||||
const id = parseInt(product.extraParam);
|
||||
|
||||
if(id > 0)
|
||||
{
|
||||
setSongId(id);
|
||||
}
|
||||
else
|
||||
{
|
||||
setOfficialSongId(product.extraParam);
|
||||
SendMessageComposer(new GetOfficialSongIdMessageComposer(product.extraParam));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
setOfficialSongId('');
|
||||
setSongId(-1);
|
||||
}
|
||||
|
||||
return () => GetSoundManager().musicController?.stop(MusicPriorities.PRIORITY_PURCHASE_PREVIEW);
|
||||
}, [ currentOffer ]);
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
return () => GetSoundManager().musicController?.stop(MusicPriorities.PRIORITY_PURCHASE_PREVIEW);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Grid>
|
||||
<Column overflow="hidden" size={ 7 }>
|
||||
{ GetConfigurationValue('catalog.headers') &&
|
||||
<CatalogHeaderView imageUrl={ currentPage.localization.getImage(0) } /> }
|
||||
<CatalogItemGridWidgetView />
|
||||
</Column>
|
||||
<Column center={ !currentOffer } overflow="hidden" size={ 5 }>
|
||||
{ !currentOffer &&
|
||||
<>
|
||||
{ !!page.localization.getImage(1) &&
|
||||
<LayoutImage imageUrl={ page.localization.getImage(1) } /> }
|
||||
<Text center dangerouslySetInnerHTML={ { __html: page.localization.getText(0) } } />
|
||||
</> }
|
||||
{ currentOffer &&
|
||||
<>
|
||||
<div className="flex items-center justify-center overflow-hidden" style={ { height: 140 } }>
|
||||
{ (currentOffer.product.productType !== ProductTypeEnum.BADGE) &&
|
||||
<>
|
||||
<CatalogViewProductWidgetView />
|
||||
<CatalogAddOnBadgeWidgetView className="bg-muted rounded bottom-1 end-1" />
|
||||
</> }
|
||||
{ (currentOffer.product.productType === ProductTypeEnum.BADGE) && <CatalogAddOnBadgeWidgetView className="scale-2" /> }
|
||||
</div>
|
||||
<Column grow gap={ 1 }>
|
||||
<CatalogLimitedItemWidgetView />
|
||||
<Text grow truncate>{ currentOffer.localizationName }</Text>
|
||||
{ songId > -1 && <Button onClick={ () => previewSong(songId) }>{ LocalizeText('play_preview_button') }</Button>
|
||||
}
|
||||
<div className="flex justify-between">
|
||||
<div className="flex flex-col gap-1">
|
||||
<CatalogSpinnerWidgetView />
|
||||
</div>
|
||||
<CatalogTotalPriceWidget alignItems="end" justifyContent="end" />
|
||||
</div>
|
||||
<CatalogPurchaseWidgetView />
|
||||
</Column>
|
||||
</> }
|
||||
</Column>
|
||||
</Grid>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,47 @@
|
||||
import { FC, useEffect } from 'react';
|
||||
import { Column, Grid, Text } from '../../../../../common';
|
||||
import { useCatalog } from '../../../../../hooks';
|
||||
import { CatalogPurchaseWidgetView } from '../widgets/CatalogPurchaseWidgetView';
|
||||
import { CatalogSpacesWidgetView } from '../widgets/CatalogSpacesWidgetView';
|
||||
import { CatalogTotalPriceWidget } from '../widgets/CatalogTotalPriceWidget';
|
||||
import { CatalogViewProductWidgetView } from '../widgets/CatalogViewProductWidgetView';
|
||||
import { CatalogLayoutProps } from './CatalogLayout.types';
|
||||
|
||||
export const CatalogLayoutSpacesView: FC<CatalogLayoutProps> = props =>
|
||||
{
|
||||
const { page = null } = props;
|
||||
const { currentOffer = null, roomPreviewer = null } = useCatalog();
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
roomPreviewer.updatePreviewObjectBoundingRectangle();
|
||||
}, [ roomPreviewer ]);
|
||||
|
||||
return (
|
||||
<Grid>
|
||||
<Column overflow="hidden" size={ 7 }>
|
||||
<CatalogSpacesWidgetView />
|
||||
</Column>
|
||||
<Column center={ !currentOffer } overflow="hidden" size={ 5 }>
|
||||
{ !currentOffer &&
|
||||
<>
|
||||
{ !!page.localization.getImage(1) && <img alt="" src={ page.localization.getImage(1) } /> }
|
||||
<Text center dangerouslySetInnerHTML={ { __html: page.localization.getText(0) } } />
|
||||
</> }
|
||||
{ currentOffer &&
|
||||
<>
|
||||
<div className="relative overflow-hidden">
|
||||
<CatalogViewProductWidgetView />
|
||||
</div>
|
||||
<Column grow gap={ 1 }>
|
||||
<Text grow truncate>{ currentOffer.localizationName }</Text>
|
||||
<div className="flex justify-end">
|
||||
<CatalogTotalPriceWidget alignItems="end" />
|
||||
</div>
|
||||
<CatalogPurchaseWidgetView />
|
||||
</Column>
|
||||
</> }
|
||||
</Column>
|
||||
</Grid>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,56 @@
|
||||
import { FC, useEffect, useState } from 'react';
|
||||
import { Column, Grid, Text } from '../../../../../common';
|
||||
import { useCatalog } from '../../../../../hooks';
|
||||
import { CatalogItemGridWidgetView } from '../widgets/CatalogItemGridWidgetView';
|
||||
import { CatalogPurchaseWidgetView } from '../widgets/CatalogPurchaseWidgetView';
|
||||
import { CatalogTotalPriceWidget } from '../widgets/CatalogTotalPriceWidget';
|
||||
import { CatalogViewProductWidgetView } from '../widgets/CatalogViewProductWidgetView';
|
||||
import { CatalogLayoutProps } from './CatalogLayout.types';
|
||||
|
||||
export const CatalogLayoutTrophiesView: FC<CatalogLayoutProps> = props =>
|
||||
{
|
||||
const { page = null } = props;
|
||||
const [ trophyText, setTrophyText ] = useState<string>('');
|
||||
const { currentOffer = null, setPurchaseOptions = null } = useCatalog();
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
if(!currentOffer) return;
|
||||
|
||||
setPurchaseOptions(prevValue =>
|
||||
{
|
||||
const newValue = { ...prevValue };
|
||||
|
||||
newValue.extraData = trophyText;
|
||||
|
||||
return newValue;
|
||||
});
|
||||
}, [ currentOffer, trophyText, setPurchaseOptions ]);
|
||||
|
||||
return (
|
||||
<Grid>
|
||||
<Column overflow="hidden" size={ 7 }>
|
||||
<CatalogItemGridWidgetView />
|
||||
<textarea className="!flex-grow form-control w-full" defaultValue={ trophyText || '' } onChange={ event => setTrophyText(event.target.value) } />
|
||||
</Column>
|
||||
<Column center={ !currentOffer } overflow="hidden" size={ 5 }>
|
||||
{ !currentOffer &&
|
||||
<>
|
||||
{ !!page.localization.getImage(1) && <img alt="" src={ page.localization.getImage(1) } /> }
|
||||
<Text center dangerouslySetInnerHTML={ { __html: page.localization.getText(0) } } />
|
||||
</> }
|
||||
{ currentOffer &&
|
||||
<>
|
||||
<CatalogViewProductWidgetView />
|
||||
<Column grow gap={ 1 }>
|
||||
<Text grow truncate>{ currentOffer.localizationName }</Text>
|
||||
<div className="flex justify-end">
|
||||
<CatalogTotalPriceWidget alignItems="end" />
|
||||
</div>
|
||||
<CatalogPurchaseWidgetView />
|
||||
</Column>
|
||||
</> }
|
||||
</Column>
|
||||
</Grid>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,191 @@
|
||||
import { ClubOfferData, GetClubOffersMessageComposer, PurchaseFromCatalogComposer } from '@nitrots/nitro-renderer';
|
||||
import { FC, useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { CatalogPurchaseState, LocalizeText, SendMessageComposer } from '../../../../../api';
|
||||
import { AutoGrid, Button, Column, Flex, Grid, LayoutCurrencyIcon, LayoutGridItem, LayoutLoadingSpinnerView, Text } from '../../../../../common';
|
||||
import { CatalogEvent, CatalogPurchaseFailureEvent, CatalogPurchasedEvent } from '../../../../../events';
|
||||
import { useCatalog, usePurse, useUiEvent } from '../../../../../hooks';
|
||||
import { CatalogLayoutProps } from './CatalogLayout.types';
|
||||
|
||||
export const CatalogLayoutVipBuyView: FC<CatalogLayoutProps> = props =>
|
||||
{
|
||||
const [ pendingOffer, setPendingOffer ] = useState<ClubOfferData>(null);
|
||||
const [ purchaseState, setPurchaseState ] = useState(CatalogPurchaseState.NONE);
|
||||
const { currentPage = null, catalogOptions = null } = useCatalog();
|
||||
const { purse = null, getCurrencyAmount = null } = usePurse();
|
||||
const { clubOffers = null } = catalogOptions;
|
||||
|
||||
const onCatalogEvent = useCallback((event: CatalogEvent) =>
|
||||
{
|
||||
switch(event.type)
|
||||
{
|
||||
case CatalogPurchasedEvent.PURCHASE_SUCCESS:
|
||||
setPurchaseState(CatalogPurchaseState.NONE);
|
||||
return;
|
||||
case CatalogPurchaseFailureEvent.PURCHASE_FAILED:
|
||||
setPurchaseState(CatalogPurchaseState.FAILED);
|
||||
return;
|
||||
}
|
||||
}, []);
|
||||
|
||||
useUiEvent(CatalogPurchasedEvent.PURCHASE_SUCCESS, onCatalogEvent);
|
||||
useUiEvent(CatalogPurchaseFailureEvent.PURCHASE_FAILED, onCatalogEvent);
|
||||
|
||||
const getOfferText = useCallback((offer: ClubOfferData) =>
|
||||
{
|
||||
let offerText = '';
|
||||
|
||||
if(offer.months > 0)
|
||||
{
|
||||
offerText = LocalizeText('catalog.vip.item.header.months', [ 'num_months' ], [ offer.months.toString() ]);
|
||||
}
|
||||
|
||||
if(offer.extraDays > 0)
|
||||
{
|
||||
if(offerText !== '') offerText += ' ';
|
||||
|
||||
offerText += (' ' + LocalizeText('catalog.vip.item.header.days', [ 'num_days' ], [ offer.extraDays.toString() ]));
|
||||
}
|
||||
|
||||
return offerText;
|
||||
}, []);
|
||||
|
||||
const getPurchaseHeader = useCallback(() =>
|
||||
{
|
||||
if(!purse) return '';
|
||||
|
||||
const extensionOrSubscription = (purse.clubDays > 0 || purse.clubPeriods > 0) ? 'extension.' : 'subscription.';
|
||||
const daysOrMonths = ((pendingOffer.months === 0) ? 'days' : 'months');
|
||||
const daysOrMonthsText = ((pendingOffer.months === 0) ? pendingOffer.extraDays : pendingOffer.months);
|
||||
const locale = LocalizeText('catalog.vip.buy.confirm.' + extensionOrSubscription + daysOrMonths);
|
||||
|
||||
return locale.replace('%NUM_' + daysOrMonths.toUpperCase() + '%', daysOrMonthsText.toString());
|
||||
}, [ pendingOffer, purse ]);
|
||||
|
||||
const getPurchaseValidUntil = useCallback(() =>
|
||||
{
|
||||
let locale = LocalizeText('catalog.vip.buy.confirm.end_date');
|
||||
|
||||
locale = locale.replace('%month%', pendingOffer.month.toString());
|
||||
locale = locale.replace('%day%', pendingOffer.day.toString());
|
||||
locale = locale.replace('%year%', pendingOffer.year.toString());
|
||||
|
||||
return locale;
|
||||
}, [ pendingOffer ]);
|
||||
|
||||
const getSubscriptionDetails = useMemo(() =>
|
||||
{
|
||||
const clubDays = purse.clubDays;
|
||||
const clubPeriods = purse.clubPeriods;
|
||||
const totalDays = (clubPeriods * 31) + clubDays;
|
||||
|
||||
return LocalizeText('catalog.vip.extend.info', [ 'days' ], [ totalDays.toString() ]);
|
||||
}, [ purse ]);
|
||||
|
||||
const purchaseSubscription = useCallback(() =>
|
||||
{
|
||||
if(!pendingOffer) return;
|
||||
|
||||
setPurchaseState(CatalogPurchaseState.PURCHASE);
|
||||
SendMessageComposer(new PurchaseFromCatalogComposer(currentPage.pageId, pendingOffer.offerId, null, 1));
|
||||
}, [ pendingOffer, currentPage ]);
|
||||
|
||||
const setOffer = useCallback((offer: ClubOfferData) =>
|
||||
{
|
||||
setPurchaseState(CatalogPurchaseState.NONE);
|
||||
setPendingOffer(offer);
|
||||
}, []);
|
||||
|
||||
const getPurchaseButton = useCallback(() =>
|
||||
{
|
||||
if(!pendingOffer) return null;
|
||||
|
||||
if(pendingOffer.priceCredits > getCurrencyAmount(-1))
|
||||
{
|
||||
return <Button fullWidth variant="danger">{ LocalizeText('catalog.alert.notenough.title') }</Button>;
|
||||
}
|
||||
|
||||
if(pendingOffer.priceActivityPoints > getCurrencyAmount(pendingOffer.priceActivityPointsType))
|
||||
{
|
||||
return <Button fullWidth variant="danger">{ LocalizeText('catalog.alert.notenough.activitypoints.title.' + pendingOffer.priceActivityPointsType) }</Button>;
|
||||
}
|
||||
|
||||
switch(purchaseState)
|
||||
{
|
||||
case CatalogPurchaseState.CONFIRM:
|
||||
return <Button fullWidth variant="warning" onClick={ purchaseSubscription }>{ LocalizeText('catalog.marketplace.confirm_title') }</Button>;
|
||||
case CatalogPurchaseState.PURCHASE:
|
||||
return <Button disabled fullWidth variant="primary"><LayoutLoadingSpinnerView /></Button>;
|
||||
case CatalogPurchaseState.FAILED:
|
||||
return <Button disabled fullWidth variant="danger">{ LocalizeText('generic.failed') }</Button>;
|
||||
case CatalogPurchaseState.NONE:
|
||||
default:
|
||||
return <Button fullWidth variant="success" onClick={ () => setPurchaseState(CatalogPurchaseState.CONFIRM) }>{ LocalizeText('buy') }</Button>;
|
||||
}
|
||||
}, [ pendingOffer, purchaseState, purchaseSubscription, getCurrencyAmount ]);
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
if(!clubOffers) SendMessageComposer(new GetClubOffersMessageComposer(1));
|
||||
}, [ clubOffers ]);
|
||||
|
||||
return (
|
||||
<Grid>
|
||||
<Column fullHeight justifyContent="between" overflow="hidden" size={ 7 }>
|
||||
<AutoGrid className="nitro-catalog-layout-vip-buy-grid" columnCount={ 1 }>
|
||||
{ clubOffers && (clubOffers.length > 0) && clubOffers.map((offer, index) =>
|
||||
{
|
||||
return (
|
||||
<LayoutGridItem key={ index } alignItems="center" center={ false } className="p-1" column={ false } itemActive={ pendingOffer === offer } justifyContent="between" onClick={ () => setOffer(offer) }>
|
||||
<i className="icon-hc-banner" />
|
||||
<Column gap={ 0 } justifyContent="end">
|
||||
<Text textEnd>{ getOfferText(offer) }</Text>
|
||||
<Flex gap={ 1 } justifyContent="end">
|
||||
{ (offer.priceCredits > 0) &&
|
||||
<Flex alignItems="center" gap={ 1 } justifyContent="end">
|
||||
<Text>{ offer.priceCredits }</Text>
|
||||
<LayoutCurrencyIcon type={ -1 } />
|
||||
</Flex> }
|
||||
{ (offer.priceActivityPoints > 0) &&
|
||||
<Flex alignItems="center" gap={ 1 } justifyContent="end">
|
||||
<Text>{ offer.priceActivityPoints }</Text>
|
||||
<LayoutCurrencyIcon type={ offer.priceActivityPointsType } />
|
||||
</Flex> }
|
||||
</Flex>
|
||||
</Column>
|
||||
</LayoutGridItem>
|
||||
);
|
||||
}) }
|
||||
</AutoGrid>
|
||||
<Text center dangerouslySetInnerHTML={ { __html: LocalizeText('catalog.vip.buy.hccenter') } }></Text>
|
||||
</Column>
|
||||
<Column overflow="hidden" size={ 5 }>
|
||||
<Column center fullHeight overflow="hidden">
|
||||
{ currentPage.localization.getImage(1) && <img alt="" src={ currentPage.localization.getImage(1) } /> }
|
||||
<Text center dangerouslySetInnerHTML={ { __html: getSubscriptionDetails } } overflow="auto" />
|
||||
</Column>
|
||||
{ pendingOffer &&
|
||||
<Column fullWidth grow justifyContent="end">
|
||||
<Flex alignItems="end">
|
||||
<Column grow gap={ 0 }>
|
||||
<Text fontWeight="bold">{ getPurchaseHeader() }</Text>
|
||||
<Text>{ getPurchaseValidUntil() }</Text>
|
||||
</Column>
|
||||
<div className="flex flex-col gap-1">
|
||||
{ (pendingOffer.priceCredits > 0) &&
|
||||
<Flex alignItems="center" gap={ 1 } justifyContent="end">
|
||||
<Text>{ pendingOffer.priceCredits }</Text>
|
||||
<LayoutCurrencyIcon type={ -1 } />
|
||||
</Flex> }
|
||||
{ (pendingOffer.priceActivityPoints > 0) &&
|
||||
<Flex alignItems="center" gap={ 1 } justifyContent="end">
|
||||
<Text>{ pendingOffer.priceActivityPoints }</Text>
|
||||
<LayoutCurrencyIcon type={ pendingOffer.priceActivityPointsType } />
|
||||
</Flex> }
|
||||
</div>
|
||||
</Flex>
|
||||
{ getPurchaseButton() }
|
||||
</Column> }
|
||||
</Column>
|
||||
</Grid>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,80 @@
|
||||
import { ICatalogPage } from '../../../../../api';
|
||||
import { CatalogLayoutProps } from './CatalogLayout.types';
|
||||
import { CatalogLayoutBadgeDisplayView } from './CatalogLayoutBadgeDisplayView';
|
||||
import { CatalogLayoutColorGroupingView } from './CatalogLayoutColorGroupingView';
|
||||
import { CatalogLayoutDefaultView } from './CatalogLayoutDefaultView';
|
||||
import { CatalogLayouGuildCustomFurniView } from './CatalogLayoutGuildCustomFurniView';
|
||||
import { CatalogLayouGuildForumView } from './CatalogLayoutGuildForumView';
|
||||
import { CatalogLayouGuildFrontpageView } from './CatalogLayoutGuildFrontpageView';
|
||||
import { CatalogLayoutInfoLoyaltyView } from './CatalogLayoutInfoLoyaltyView';
|
||||
import { CatalogLayoutPets2View } from './CatalogLayoutPets2View';
|
||||
import { CatalogLayoutPets3View } from './CatalogLayoutPets3View';
|
||||
import { CatalogLayoutRoomAdsView } from './CatalogLayoutRoomAdsView';
|
||||
import { CatalogLayoutRoomBundleView } from './CatalogLayoutRoomBundleView';
|
||||
import { CatalogLayoutSingleBundleView } from './CatalogLayoutSingleBundleView';
|
||||
import { CatalogLayoutSoundMachineView } from './CatalogLayoutSoundMachineView';
|
||||
import { CatalogLayoutSpacesView } from './CatalogLayoutSpacesView';
|
||||
import { CatalogLayoutTrophiesView } from './CatalogLayoutTrophiesView';
|
||||
import { CatalogLayoutVipBuyView } from './CatalogLayoutVipBuyView';
|
||||
import { CatalogLayoutFrontpage4View } from './frontpage4/CatalogLayoutFrontpage4View';
|
||||
import { CatalogLayoutMarketplaceOwnItemsView } from './marketplace/CatalogLayoutMarketplaceOwnItemsView';
|
||||
import { CatalogLayoutMarketplacePublicItemsView } from './marketplace/CatalogLayoutMarketplacePublicItemsView';
|
||||
import { CatalogLayoutPetView } from './pets/CatalogLayoutPetView';
|
||||
import { CatalogLayoutVipGiftsView } from './vip-gifts/CatalogLayoutVipGiftsView';
|
||||
|
||||
export const GetCatalogLayout = (page: ICatalogPage, hideNavigation: () => void) =>
|
||||
{
|
||||
if(!page) return null;
|
||||
|
||||
const layoutProps: CatalogLayoutProps = { page, hideNavigation };
|
||||
|
||||
switch(page.layoutCode)
|
||||
{
|
||||
case 'frontpage_featured':
|
||||
return null;
|
||||
case 'frontpage4':
|
||||
return <CatalogLayoutFrontpage4View { ...layoutProps } />;
|
||||
case 'pets':
|
||||
return <CatalogLayoutPetView { ...layoutProps } />;
|
||||
case 'pets2':
|
||||
return <CatalogLayoutPets2View { ...layoutProps } />;
|
||||
case 'pets3':
|
||||
return <CatalogLayoutPets3View { ...layoutProps } />;
|
||||
case 'vip_buy':
|
||||
return <CatalogLayoutVipBuyView { ...layoutProps } />;
|
||||
case 'guild_frontpage':
|
||||
return <CatalogLayouGuildFrontpageView { ...layoutProps } />;
|
||||
case 'guild_forum':
|
||||
return <CatalogLayouGuildForumView { ...layoutProps } />;
|
||||
case 'guild_custom_furni':
|
||||
return <CatalogLayouGuildCustomFurniView { ...layoutProps } />;
|
||||
case 'club_gifts':
|
||||
return <CatalogLayoutVipGiftsView { ...layoutProps } />;
|
||||
case 'marketplace_own_items':
|
||||
return <CatalogLayoutMarketplaceOwnItemsView { ...layoutProps } />;
|
||||
case 'marketplace':
|
||||
return <CatalogLayoutMarketplacePublicItemsView { ...layoutProps } />;
|
||||
case 'single_bundle':
|
||||
return <CatalogLayoutSingleBundleView { ...layoutProps } />;
|
||||
case 'room_bundle':
|
||||
return <CatalogLayoutRoomBundleView { ...layoutProps } />;
|
||||
case 'spaces_new':
|
||||
return <CatalogLayoutSpacesView { ...layoutProps } />;
|
||||
case 'trophies':
|
||||
return <CatalogLayoutTrophiesView { ...layoutProps } />;
|
||||
case 'info_loyalty':
|
||||
return <CatalogLayoutInfoLoyaltyView { ...layoutProps } />;
|
||||
case 'badge_display':
|
||||
return <CatalogLayoutBadgeDisplayView { ...layoutProps } />;
|
||||
case 'roomads':
|
||||
return <CatalogLayoutRoomAdsView { ...layoutProps } />;
|
||||
case 'default_3x3_color_grouping':
|
||||
return <CatalogLayoutColorGroupingView { ...layoutProps } />;
|
||||
case 'soundmachine':
|
||||
return <CatalogLayoutSoundMachineView { ...layoutProps } />;
|
||||
case 'bots':
|
||||
case 'default_3x3':
|
||||
default:
|
||||
return <CatalogLayoutDefaultView { ...layoutProps } />;
|
||||
}
|
||||
};
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
import { FrontPageItem } from '@nitrots/nitro-renderer';
|
||||
import { FC, useMemo } from 'react';
|
||||
import { GetConfigurationValue } from '../../../../../../api';
|
||||
import { LayoutBackgroundImage, LayoutBackgroundImageProps } from '../../../../../../common';
|
||||
import { Text } from '../../../../../../common/Text';
|
||||
|
||||
export interface CatalogLayoutFrontPageItemViewProps extends LayoutBackgroundImageProps
|
||||
{
|
||||
item: FrontPageItem;
|
||||
}
|
||||
|
||||
export const CatalogLayoutFrontPageItemView: FC<CatalogLayoutFrontPageItemViewProps> = props =>
|
||||
{
|
||||
const { item = null, position = 'relative', pointer = true, overflow = 'hidden', fullHeight = true, classNames = [], children = null, ...rest } = props;
|
||||
|
||||
const getClassNames = useMemo(() =>
|
||||
{
|
||||
const newClassNames: string[] = [ 'rounded', 'nitro-front-page-item' ];
|
||||
|
||||
if(classNames.length) newClassNames.push(...classNames);
|
||||
|
||||
return newClassNames;
|
||||
}, [ classNames ]);
|
||||
|
||||
if(!item) return null;
|
||||
|
||||
const imageUrl = (GetConfigurationValue<string>('image.library.url') + item.itemPromoImage);
|
||||
|
||||
return (
|
||||
<LayoutBackgroundImage classNames={ getClassNames } fullHeight={ fullHeight } imageUrl={ imageUrl } overflow={ overflow } pointer={ pointer } position={ position } { ...rest }>
|
||||
<Text className="bg-dark rounded p-2 m-2 bottom-0" position="absolute" variant="white">
|
||||
{ item.itemName }
|
||||
</Text>
|
||||
{ children }
|
||||
</LayoutBackgroundImage>
|
||||
);
|
||||
};
|
||||
+49
@@ -0,0 +1,49 @@
|
||||
import { CreateLinkEvent, FrontPageItem } from '@nitrots/nitro-renderer';
|
||||
import { FC, useCallback, useEffect } from 'react';
|
||||
import { Column, Grid } from '../../../../../../common';
|
||||
import { useCatalog } from '../../../../../../hooks';
|
||||
import { CatalogRedeemVoucherView } from '../../common/CatalogRedeemVoucherView';
|
||||
import { CatalogLayoutProps } from '../CatalogLayout.types';
|
||||
import { CatalogLayoutFrontPageItemView } from './CatalogLayoutFrontPageItemView';
|
||||
|
||||
export const CatalogLayoutFrontpage4View: FC<CatalogLayoutProps> = props =>
|
||||
{
|
||||
const { page = null, hideNavigation = null } = props;
|
||||
const { frontPageItems = [] } = useCatalog();
|
||||
|
||||
const selectItem = useCallback((item: FrontPageItem) =>
|
||||
{
|
||||
switch(item.type)
|
||||
{
|
||||
case FrontPageItem.ITEM_CATALOGUE_PAGE:
|
||||
CreateLinkEvent(`catalog/open/${ item.catalogPageLocation }`);
|
||||
return;
|
||||
case FrontPageItem.ITEM_PRODUCT_OFFER:
|
||||
CreateLinkEvent(`catalog/open/${ item.productOfferId }`);
|
||||
return;
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
hideNavigation();
|
||||
}, [ page, hideNavigation ]);
|
||||
|
||||
return (
|
||||
<Grid>
|
||||
<Column size={ 4 }>
|
||||
{ frontPageItems[0] &&
|
||||
<CatalogLayoutFrontPageItemView item={ frontPageItems[0] } onClick={ event => selectItem(frontPageItems[0]) } /> }
|
||||
</Column>
|
||||
<Column size={ 8 }>
|
||||
{ frontPageItems[1] &&
|
||||
<CatalogLayoutFrontPageItemView item={ frontPageItems[1] } onClick={ event => selectItem(frontPageItems[1]) } /> }
|
||||
{ frontPageItems[2] &&
|
||||
<CatalogLayoutFrontPageItemView item={ frontPageItems[2] } onClick={ event => selectItem(frontPageItems[2]) } /> }
|
||||
{ frontPageItems[3] &&
|
||||
<CatalogLayoutFrontPageItemView item={ frontPageItems[3] } onClick={ event => selectItem(frontPageItems[3]) } /> }
|
||||
<CatalogRedeemVoucherView text={ page.localization.getText(1) } />
|
||||
</Column>
|
||||
</Grid>
|
||||
);
|
||||
};
|
||||
+83
@@ -0,0 +1,83 @@
|
||||
import { FC, useCallback, useMemo } from 'react';
|
||||
import { GetImageIconUrlForProduct, LocalizeText, MarketPlaceOfferState, MarketplaceOfferData, ProductTypeEnum } from '../../../../../../api';
|
||||
import { Button, Column, LayoutGridItem, Text } from '../../../../../../common';
|
||||
|
||||
export interface MarketplaceItemViewProps
|
||||
{
|
||||
offerData: MarketplaceOfferData;
|
||||
type?: number;
|
||||
onClick(offerData: MarketplaceOfferData): void;
|
||||
}
|
||||
|
||||
export const OWN_OFFER = 1;
|
||||
export const PUBLIC_OFFER = 2;
|
||||
|
||||
export const CatalogLayoutMarketplaceItemView: FC<MarketplaceItemViewProps> = props =>
|
||||
{
|
||||
const { offerData = null, type = PUBLIC_OFFER, onClick = null } = props;
|
||||
|
||||
const getMarketplaceOfferTitle = useMemo(() =>
|
||||
{
|
||||
if(!offerData) return '';
|
||||
|
||||
// desc
|
||||
return LocalizeText(((offerData.furniType === 2) ? 'wallItem' : 'roomItem') + `.name.${ offerData.furniId }`);
|
||||
}, [ offerData ]);
|
||||
|
||||
const offerTime = useCallback( () =>
|
||||
{
|
||||
if(!offerData) return '';
|
||||
|
||||
if(offerData.status === MarketPlaceOfferState.SOLD) return LocalizeText('catalog.marketplace.offer.sold');
|
||||
|
||||
if(offerData.timeLeftMinutes <= 0) return LocalizeText('catalog.marketplace.offer.expired');
|
||||
|
||||
const time = Math.max(1, offerData.timeLeftMinutes);
|
||||
const hours = Math.floor(time / 60);
|
||||
const minutes = time - (hours * 60);
|
||||
|
||||
let text = minutes + ' ' + LocalizeText('catalog.marketplace.offer.minutes');
|
||||
if(hours > 0)
|
||||
{
|
||||
text = hours + ' ' + LocalizeText('catalog.marketplace.offer.hours') + ' ' + text;
|
||||
}
|
||||
|
||||
return LocalizeText('catalog.marketplace.offer.time_left', [ 'time' ], [ text ] );
|
||||
}, [ offerData ]);
|
||||
|
||||
return (
|
||||
<LayoutGridItem shrink alignItems="center" center={ false } className="p-1" column={ false }>
|
||||
<Column style={ { width: 40, height: 40 } }>
|
||||
<LayoutGridItem column={ false } itemImage={ GetImageIconUrlForProduct(((offerData.furniType === MarketplaceOfferData.TYPE_FLOOR) ? ProductTypeEnum.FLOOR : ProductTypeEnum.WALL), offerData.furniId, offerData.extraData) } itemUniqueNumber={ offerData.isUniqueLimitedItem ? offerData.stuffData.uniqueNumber : 0 } />
|
||||
</Column>
|
||||
<Column grow gap={ 0 }>
|
||||
<Text fontWeight="bold">{ getMarketplaceOfferTitle }</Text>
|
||||
{ (type === OWN_OFFER) &&
|
||||
<>
|
||||
<Text>{ LocalizeText('catalog.marketplace.offer.price_own_item', [ 'price' ], [ offerData.price.toString() ]) }</Text>
|
||||
<Text>{ offerTime() }</Text>
|
||||
</> }
|
||||
{ (type === PUBLIC_OFFER) &&
|
||||
<>
|
||||
<Text>{ LocalizeText('catalog.marketplace.offer.price_public_item', [ 'price', 'average' ], [ offerData.price.toString(), ((offerData.averagePrice > 0) ? offerData.averagePrice.toString() : '-') ]) }</Text>
|
||||
<Text>{ LocalizeText('catalog.marketplace.offer_count', [ 'count' ], [ offerData.offerCount.toString() ]) }</Text>
|
||||
</> }
|
||||
</Column>
|
||||
<div className="flex flex-col gap-1">
|
||||
{ ((type === OWN_OFFER) && (offerData.status !== MarketPlaceOfferState.SOLD)) &&
|
||||
<Button variant="secondary" onClick={ () => onClick(offerData) }>
|
||||
{ LocalizeText('catalog.marketplace.offer.pick') }
|
||||
</Button> }
|
||||
{ type === PUBLIC_OFFER &&
|
||||
<>
|
||||
<Button variant="secondary" onClick={ () => onClick(offerData) }>
|
||||
{ LocalizeText('buy') }
|
||||
</Button>
|
||||
<Button disabled variant="secondary">
|
||||
{ LocalizeText('catalog.marketplace.view_more') }
|
||||
</Button>
|
||||
</> }
|
||||
</div>
|
||||
</LayoutGridItem>
|
||||
);
|
||||
};
|
||||
+102
@@ -0,0 +1,102 @@
|
||||
import { CancelMarketplaceOfferMessageComposer, GetMarketplaceOwnOffersMessageComposer, MarketplaceCancelOfferResultEvent, MarketplaceOwnOffersEvent, RedeemMarketplaceOfferCreditsMessageComposer } from '@nitrots/nitro-renderer';
|
||||
import { FC, useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { LocalizeText, MarketplaceOfferData, MarketPlaceOfferState, NotificationAlertType, SendMessageComposer } from '../../../../../../api';
|
||||
import { Button, Column, Text } from '../../../../../../common';
|
||||
import { useMessageEvent, useNotification } from '../../../../../../hooks';
|
||||
import { CatalogLayoutProps } from '../CatalogLayout.types';
|
||||
import { CatalogLayoutMarketplaceItemView, OWN_OFFER } from './CatalogLayoutMarketplaceItemView';
|
||||
|
||||
export const CatalogLayoutMarketplaceOwnItemsView: FC<CatalogLayoutProps> = props =>
|
||||
{
|
||||
const [ creditsWaiting, setCreditsWaiting ] = useState(0);
|
||||
const [ offers, setOffers ] = useState<MarketplaceOfferData[]>([]);
|
||||
const { simpleAlert = null } = useNotification();
|
||||
|
||||
useMessageEvent<MarketplaceOwnOffersEvent>(MarketplaceOwnOffersEvent, event =>
|
||||
{
|
||||
const parser = event.getParser();
|
||||
|
||||
if(!parser) return;
|
||||
|
||||
const offers = parser.offers.map(offer =>
|
||||
{
|
||||
const newOffer = new MarketplaceOfferData(offer.offerId, offer.furniId, offer.furniType, offer.extraData, offer.stuffData, offer.price, offer.status, offer.averagePrice, offer.offerCount);
|
||||
|
||||
newOffer.timeLeftMinutes = offer.timeLeftMinutes;
|
||||
|
||||
return newOffer;
|
||||
});
|
||||
|
||||
setCreditsWaiting(parser.creditsWaiting);
|
||||
setOffers(offers);
|
||||
});
|
||||
|
||||
useMessageEvent<MarketplaceCancelOfferResultEvent>(MarketplaceCancelOfferResultEvent, event =>
|
||||
{
|
||||
const parser = event.getParser();
|
||||
|
||||
if(!parser) return;
|
||||
|
||||
if(!parser.success)
|
||||
{
|
||||
simpleAlert(LocalizeText('catalog.marketplace.cancel_failed'), NotificationAlertType.DEFAULT, null, null, LocalizeText('catalog.marketplace.operation_failed.topic'));
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
setOffers(prevValue => prevValue.filter(value => (value.offerId !== parser.offerId)));
|
||||
});
|
||||
|
||||
const soldOffers = useMemo(() =>
|
||||
{
|
||||
return offers.filter(value => (value.status === MarketPlaceOfferState.SOLD));
|
||||
}, [ offers ]);
|
||||
|
||||
const redeemSoldOffers = useCallback(() =>
|
||||
{
|
||||
setOffers(prevValue =>
|
||||
{
|
||||
const idsToDelete = soldOffers.map(value => value.offerId);
|
||||
|
||||
return prevValue.filter(value => (idsToDelete.indexOf(value.offerId) === -1));
|
||||
});
|
||||
|
||||
SendMessageComposer(new RedeemMarketplaceOfferCreditsMessageComposer());
|
||||
}, [ soldOffers ]);
|
||||
|
||||
const takeItemBack = (offerData: MarketplaceOfferData) =>
|
||||
{
|
||||
SendMessageComposer(new CancelMarketplaceOfferMessageComposer(offerData.offerId));
|
||||
};
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
SendMessageComposer(new GetMarketplaceOwnOffersMessageComposer());
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Column overflow="hidden">
|
||||
{ (creditsWaiting <= 0) &&
|
||||
<Text center className="bg-muted rounded p-1">
|
||||
{ LocalizeText('catalog.marketplace.redeem.no_sold_items') }
|
||||
</Text> }
|
||||
{ (creditsWaiting > 0) &&
|
||||
<Column center className="bg-muted rounded p-2" gap={ 1 }>
|
||||
<Text>
|
||||
{ LocalizeText('catalog.marketplace.redeem.get_credits', [ 'count', 'credits' ], [ soldOffers.length.toString(), creditsWaiting.toString() ]) }
|
||||
</Text>
|
||||
<Button className="mt-1" onClick={ redeemSoldOffers }>
|
||||
{ LocalizeText('catalog.marketplace.offer.redeem') }
|
||||
</Button>
|
||||
</Column> }
|
||||
<Column gap={ 1 } overflow="hidden">
|
||||
<Text shrink truncate fontWeight="bold">
|
||||
{ LocalizeText('catalog.marketplace.items_found', [ 'count' ], [ offers.length.toString() ]) }
|
||||
</Text>
|
||||
<Column className="nitro-catalog-layout-marketplace-grid" overflow="auto">
|
||||
{ (offers.length > 0) && offers.map(offer => <CatalogLayoutMarketplaceItemView key={ offer.offerId } offerData={ offer } type={ OWN_OFFER } onClick={ takeItemBack } />) }
|
||||
</Column>
|
||||
</Column>
|
||||
</Column>
|
||||
);
|
||||
};
|
||||
+161
@@ -0,0 +1,161 @@
|
||||
import { BuyMarketplaceOfferMessageComposer, GetMarketplaceOffersMessageComposer, MarketplaceBuyOfferResultEvent, MarketPlaceOffersEvent } from '@nitrots/nitro-renderer';
|
||||
import { FC, useCallback, useMemo, useState } from 'react';
|
||||
import { IMarketplaceSearchOptions, LocalizeText, MarketplaceOfferData, MarketplaceSearchType, NotificationAlertType, SendMessageComposer } from '../../../../../../api';
|
||||
import { Button, Column, Text } from '../../../../../../common';
|
||||
import { useMessageEvent, useNotification, usePurse } from '../../../../../../hooks';
|
||||
import { CatalogLayoutProps } from '../CatalogLayout.types';
|
||||
import { CatalogLayoutMarketplaceItemView, PUBLIC_OFFER } from './CatalogLayoutMarketplaceItemView';
|
||||
import { SearchFormView } from './CatalogLayoutMarketplaceSearchFormView';
|
||||
|
||||
const SORT_TYPES_VALUE = [ 1, 2 ];
|
||||
const SORT_TYPES_ACTIVITY = [ 3, 4, 5, 6 ];
|
||||
const SORT_TYPES_ADVANCED = [ 1, 2, 3, 4, 5, 6 ];
|
||||
export interface CatalogLayoutMarketplacePublicItemsViewProps extends CatalogLayoutProps
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
export const CatalogLayoutMarketplacePublicItemsView: FC<CatalogLayoutMarketplacePublicItemsViewProps> = props =>
|
||||
{
|
||||
const [ searchType, setSearchType ] = useState(MarketplaceSearchType.BY_ACTIVITY);
|
||||
const [ totalItemsFound, setTotalItemsFound ] = useState(0);
|
||||
const [ offers, setOffers ] = useState(new Map<number, MarketplaceOfferData>());
|
||||
const [ lastSearch, setLastSearch ] = useState<IMarketplaceSearchOptions>({ minPrice: -1, maxPrice: -1, query: '', type: 3 });
|
||||
const { getCurrencyAmount = null } = usePurse();
|
||||
const { simpleAlert = null, showConfirm = null } = useNotification();
|
||||
|
||||
const requestOffers = useCallback((options: IMarketplaceSearchOptions) =>
|
||||
{
|
||||
setLastSearch(options);
|
||||
SendMessageComposer(new GetMarketplaceOffersMessageComposer(options.minPrice, options.maxPrice, options.query, options.type));
|
||||
}, []);
|
||||
|
||||
const getSortTypes = useMemo(() =>
|
||||
{
|
||||
switch(searchType)
|
||||
{
|
||||
case MarketplaceSearchType.BY_ACTIVITY:
|
||||
return SORT_TYPES_ACTIVITY;
|
||||
case MarketplaceSearchType.BY_VALUE:
|
||||
return SORT_TYPES_VALUE;
|
||||
case MarketplaceSearchType.ADVANCED:
|
||||
return SORT_TYPES_ADVANCED;
|
||||
}
|
||||
return [];
|
||||
}, [ searchType ]);
|
||||
|
||||
const purchaseItem = useCallback((offerData: MarketplaceOfferData) =>
|
||||
{
|
||||
if(offerData.price > getCurrencyAmount(-1))
|
||||
{
|
||||
simpleAlert(LocalizeText('catalog.alert.notenough.credits.description'), NotificationAlertType.DEFAULT, null, null, LocalizeText('catalog.alert.notenough.title'));
|
||||
return;
|
||||
}
|
||||
|
||||
const offerId = offerData.offerId;
|
||||
|
||||
showConfirm(LocalizeText('catalog.marketplace.confirm_header'), () =>
|
||||
{
|
||||
SendMessageComposer(new BuyMarketplaceOfferMessageComposer(offerId));
|
||||
},
|
||||
null, null, null, LocalizeText('catalog.marketplace.confirm_title'));
|
||||
}, [ getCurrencyAmount, simpleAlert, showConfirm ]);
|
||||
|
||||
useMessageEvent<MarketPlaceOffersEvent>(MarketPlaceOffersEvent, event =>
|
||||
{
|
||||
const parser = event.getParser();
|
||||
|
||||
if(!parser) return;
|
||||
|
||||
const latestOffers = new Map<number, MarketplaceOfferData>();
|
||||
parser.offers.forEach(entry =>
|
||||
{
|
||||
const offerEntry = new MarketplaceOfferData(entry.offerId, entry.furniId, entry.furniType, entry.extraData, entry.stuffData, entry.price, entry.status, entry.averagePrice, entry.offerCount);
|
||||
offerEntry.timeLeftMinutes = entry.timeLeftMinutes;
|
||||
latestOffers.set(entry.offerId, offerEntry);
|
||||
});
|
||||
|
||||
setTotalItemsFound(parser.totalItemsFound);
|
||||
setOffers(latestOffers);
|
||||
});
|
||||
|
||||
useMessageEvent<MarketplaceBuyOfferResultEvent>(MarketplaceBuyOfferResultEvent, event =>
|
||||
{
|
||||
const parser = event.getParser();
|
||||
|
||||
if(!parser) return;
|
||||
|
||||
switch(parser.result)
|
||||
{
|
||||
case 1:
|
||||
requestOffers(lastSearch);
|
||||
break;
|
||||
case 2:
|
||||
setOffers(prev =>
|
||||
{
|
||||
const newVal = new Map(prev);
|
||||
newVal.delete(parser.requestedOfferId);
|
||||
return newVal;
|
||||
});
|
||||
simpleAlert(LocalizeText('catalog.marketplace.not_available_header'), NotificationAlertType.DEFAULT, null, null, LocalizeText('catalog.marketplace.not_available_title'));
|
||||
break;
|
||||
case 3:
|
||||
// our shit was updated
|
||||
// todo: some dialogue modal
|
||||
setOffers(prev =>
|
||||
{
|
||||
const newVal = new Map(prev);
|
||||
|
||||
const item = newVal.get(parser.requestedOfferId);
|
||||
if(item)
|
||||
{
|
||||
item.offerId = parser.offerId;
|
||||
item.price = parser.newPrice;
|
||||
item.offerCount--;
|
||||
newVal.set(item.offerId, item);
|
||||
}
|
||||
|
||||
newVal.delete(parser.requestedOfferId);
|
||||
return newVal;
|
||||
});
|
||||
|
||||
showConfirm(LocalizeText('catalog.marketplace.confirm_higher_header') +
|
||||
'\n' + LocalizeText('catalog.marketplace.confirm_price', [ 'price' ], [ parser.newPrice.toString() ]), () =>
|
||||
{
|
||||
SendMessageComposer(new BuyMarketplaceOfferMessageComposer(parser.offerId));
|
||||
},
|
||||
null, null, null, LocalizeText('catalog.marketplace.confirm_higher_title'));
|
||||
break;
|
||||
case 4:
|
||||
simpleAlert(LocalizeText('catalog.alert.notenough.credits.description'), NotificationAlertType.DEFAULT, null, null, LocalizeText('catalog.alert.notenough.title'));
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="relative inline-flex align-middle">
|
||||
<Button active={ (searchType === MarketplaceSearchType.BY_ACTIVITY) } onClick={ () => setSearchType(MarketplaceSearchType.BY_ACTIVITY) }>
|
||||
{ LocalizeText('catalog.marketplace.search_by_activity') }
|
||||
</Button>
|
||||
<Button active={ (searchType === MarketplaceSearchType.BY_VALUE) } onClick={ () => setSearchType(MarketplaceSearchType.BY_VALUE) }>
|
||||
{ LocalizeText('catalog.marketplace.search_by_value') }
|
||||
</Button>
|
||||
<Button active={ (searchType === MarketplaceSearchType.ADVANCED) } onClick={ () => setSearchType(MarketplaceSearchType.ADVANCED) }>
|
||||
{ LocalizeText('catalog.marketplace.search_advanced') }
|
||||
</Button>
|
||||
</div>
|
||||
<SearchFormView searchType={ searchType } sortTypes={ getSortTypes } onSearch={ requestOffers } />
|
||||
<Column gap={ 1 } overflow="hidden">
|
||||
<Text shrink truncate fontWeight="bold">
|
||||
{ LocalizeText('catalog.marketplace.items_found', [ 'count' ], [ offers.size.toString() ]) }
|
||||
</Text>
|
||||
<Column className="nitro-catalog-layout-marketplace-grid" overflow="auto">
|
||||
{
|
||||
Array.from(offers.values()).map((entry, index) => <CatalogLayoutMarketplaceItemView key={ index } offerData={ entry } type={ PUBLIC_OFFER } onClick={ purchaseItem } />)
|
||||
}
|
||||
</Column>
|
||||
</Column>
|
||||
</>
|
||||
);
|
||||
};
|
||||
+86
@@ -0,0 +1,86 @@
|
||||
import { FC, useCallback, useEffect, useState } from 'react';
|
||||
import { IMarketplaceSearchOptions, LocalizeText, MarketplaceSearchType } from '../../../../../../api';
|
||||
import { Button, Text } from '../../../../../../common';
|
||||
import { NitroInput } from '../../../../../../layout';
|
||||
|
||||
export interface SearchFormViewProps
|
||||
{
|
||||
searchType: number;
|
||||
sortTypes: number[];
|
||||
onSearch(options: IMarketplaceSearchOptions): void;
|
||||
}
|
||||
|
||||
export const SearchFormView: FC<SearchFormViewProps> = props =>
|
||||
{
|
||||
const { searchType = null, sortTypes = null, onSearch = null } = props;
|
||||
const [ sortType, setSortType ] = useState(sortTypes ? sortTypes[0] : 3); // first item of SORT_TYPES_ACTIVITY
|
||||
const [ searchQuery, setSearchQuery ] = useState('');
|
||||
const [ min, setMin ] = useState(0);
|
||||
const [ max, setMax ] = useState(0);
|
||||
|
||||
const onSortTypeChange = useCallback((sortType: number) =>
|
||||
{
|
||||
setSortType(sortType);
|
||||
|
||||
if((searchType === MarketplaceSearchType.BY_ACTIVITY) || (searchType === MarketplaceSearchType.BY_VALUE)) onSearch({ minPrice: -1, maxPrice: -1, query: '', type: sortType });
|
||||
}, [ onSearch, searchType ]);
|
||||
|
||||
const onClickSearch = useCallback(() =>
|
||||
{
|
||||
const minPrice = ((min > 0) ? min : -1);
|
||||
const maxPrice = ((max > 0) ? max : -1);
|
||||
|
||||
onSearch({ minPrice: minPrice, maxPrice: maxPrice, type: sortType, query: searchQuery });
|
||||
}, [ max, min, onSearch, searchQuery, sortType ]);
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
if(!sortTypes || !sortTypes.length) return;
|
||||
|
||||
const sortType = sortTypes[0];
|
||||
|
||||
setSortType(sortType);
|
||||
|
||||
if(searchType === MarketplaceSearchType.BY_ACTIVITY || MarketplaceSearchType.BY_VALUE === searchType) onSearch({ minPrice: -1, maxPrice: -1, query: '', type: sortType });
|
||||
}, [ onSearch, searchType, sortTypes ]);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-1">
|
||||
<div className="flex items-center gap-1">
|
||||
<Text className="col-span-3">{ LocalizeText('catalog.marketplace.sort_order') }</Text>
|
||||
<select className="form-select form-select-sm" value={ sortType } onChange={ event => onSortTypeChange(parseInt(event.target.value)) }>
|
||||
{ sortTypes.map(type => <option key={ type } value={ type }>{ LocalizeText(`catalog.marketplace.sort.${ type }`) }</option>) }
|
||||
</select>
|
||||
</div>
|
||||
{ searchType === MarketplaceSearchType.ADVANCED &&
|
||||
<>
|
||||
<div className="flex items-center gap-1">
|
||||
<Text className="col-span-3">{ LocalizeText('catalog.marketplace.search_name') }</Text>
|
||||
<NitroInput
|
||||
value={ searchQuery }
|
||||
onChange={ event => setSearchQuery(event.target.value) } />
|
||||
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<Text className="col-span-3">{ LocalizeText('catalog.marketplace.search_price') }</Text>
|
||||
<div className="flex w-full gap-1">
|
||||
|
||||
<NitroInput
|
||||
min={ 0 }
|
||||
type="number"
|
||||
value={ min }
|
||||
onChange={ event => setMin(event.target.valueAsNumber) } />
|
||||
<NitroInput
|
||||
min={ 0 }
|
||||
type="number"
|
||||
value={ max }
|
||||
onChange={ event => setMax(event.target.valueAsNumber) } />
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<Button className="mx-auto" variant="secondary" onClick={ onClickSearch }>{ LocalizeText('generic.search') }</Button>
|
||||
</> }
|
||||
</div>
|
||||
);
|
||||
};
|
||||
+124
@@ -0,0 +1,124 @@
|
||||
import { GetMarketplaceConfigurationMessageComposer, MakeOfferMessageComposer, MarketplaceConfigurationEvent } from '@nitrots/nitro-renderer';
|
||||
import { FC, useEffect, useState } from 'react';
|
||||
import { FurnitureItem, LocalizeText, ProductTypeEnum, SendMessageComposer } from '../../../../../../api';
|
||||
import { Button, Column, Grid, LayoutFurniImageView, NitroCardContentView, NitroCardHeaderView, NitroCardView, Text } from '../../../../../../common';
|
||||
import { CatalogPostMarketplaceOfferEvent } from '../../../../../../events';
|
||||
import { useCatalog, useMessageEvent, useNotification, useUiEvent } from '../../../../../../hooks';
|
||||
import { NitroInput } from '../../../../../../layout';
|
||||
|
||||
export const MarketplacePostOfferView: FC<{}> = props =>
|
||||
{
|
||||
const [ item, setItem ] = useState<FurnitureItem>(null);
|
||||
const [ askingPrice, setAskingPrice ] = useState(0);
|
||||
const [ tempAskingPrice, setTempAskingPrice ] = useState('0');
|
||||
const { catalogOptions = null, setCatalogOptions = null } = useCatalog();
|
||||
const { marketplaceConfiguration = null } = catalogOptions;
|
||||
const { showConfirm = null } = useNotification();
|
||||
|
||||
const updateAskingPrice = (price: string) =>
|
||||
{
|
||||
setTempAskingPrice(price);
|
||||
|
||||
const newValue = parseInt(price);
|
||||
|
||||
if(isNaN(newValue) || (newValue === askingPrice)) return;
|
||||
|
||||
setAskingPrice(parseInt(price));
|
||||
};
|
||||
|
||||
useMessageEvent<MarketplaceConfigurationEvent>(MarketplaceConfigurationEvent, event =>
|
||||
{
|
||||
const parser = event.getParser();
|
||||
|
||||
setCatalogOptions(prevValue =>
|
||||
{
|
||||
const newValue = { ...prevValue };
|
||||
|
||||
newValue.marketplaceConfiguration = parser;
|
||||
|
||||
return newValue;
|
||||
});
|
||||
});
|
||||
|
||||
useUiEvent<CatalogPostMarketplaceOfferEvent>(CatalogPostMarketplaceOfferEvent.POST_MARKETPLACE, event => setItem(event.item));
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
if(!item || marketplaceConfiguration) return;
|
||||
|
||||
SendMessageComposer(new GetMarketplaceConfigurationMessageComposer());
|
||||
}, [ item, marketplaceConfiguration ]);
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
if(!item) return;
|
||||
|
||||
return () => setAskingPrice(0);
|
||||
}, [ item ]);
|
||||
|
||||
if(!marketplaceConfiguration || !item) return null;
|
||||
|
||||
const getFurniTitle = (item ? LocalizeText(item.isWallItem ? 'wallItem.name.' + item.type : 'roomItem.name.' + item.type) : '');
|
||||
const getFurniDescription = (item ? LocalizeText(item.isWallItem ? 'wallItem.desc.' + item.type : 'roomItem.desc.' + item.type) : '');
|
||||
|
||||
const getCommission = () => Math.max(Math.ceil(((marketplaceConfiguration.commission * 0.01) * askingPrice)), 1);
|
||||
|
||||
const postItem = () =>
|
||||
{
|
||||
if(!item || (askingPrice < marketplaceConfiguration.minimumPrice)) return;
|
||||
|
||||
showConfirm(LocalizeText('inventory.marketplace.confirm_offer.info', [ 'furniname', 'price' ], [ getFurniTitle, askingPrice.toString() ]), () =>
|
||||
{
|
||||
SendMessageComposer(new MakeOfferMessageComposer(askingPrice, item.isWallItem ? 2 : 1, item.id));
|
||||
setItem(null);
|
||||
},
|
||||
() =>
|
||||
{
|
||||
setItem(null);
|
||||
}, null, null, LocalizeText('inventory.marketplace.confirm_offer.title'));
|
||||
};
|
||||
|
||||
return (
|
||||
<NitroCardView className="nitro-catalog-layout-marketplace-post-offer" theme="primary-slim">
|
||||
<NitroCardHeaderView headerText={ LocalizeText('inventory.marketplace.make_offer.title') } onCloseClick={ event => setItem(null) } />
|
||||
<NitroCardContentView overflow="hidden">
|
||||
<Grid fullHeight>
|
||||
<Column center className="bg-muted rounded p-2" overflow="hidden" size={ 4 }>
|
||||
<LayoutFurniImageView extraData={ item.extra.toString() } productClassId={ item.type } productType={ item.isWallItem ? ProductTypeEnum.WALL : ProductTypeEnum.FLOOR } />
|
||||
</Column>
|
||||
<Column justifyContent="between" overflow="hidden" size={ 8 }>
|
||||
<Column grow gap={ 1 }>
|
||||
<Text fontWeight="bold">{ getFurniTitle }</Text>
|
||||
<Text shrink truncate>{ getFurniDescription }</Text>
|
||||
</Column>
|
||||
<Column overflow="auto">
|
||||
<Text italics>
|
||||
{ LocalizeText('inventory.marketplace.make_offer.expiration_info', [ 'time' ], [ marketplaceConfiguration.offerTime.toString() ]) }
|
||||
</Text>
|
||||
<div className="input-group has-validation">
|
||||
|
||||
|
||||
<NitroInput min={ 0 } placeholder={ LocalizeText('inventory.marketplace.make_offer.price_request') } type="number" value={ tempAskingPrice } onChange={ event => updateAskingPrice(event.target.value) } />
|
||||
{ ((askingPrice < marketplaceConfiguration.minimumPrice) || isNaN(askingPrice)) &&
|
||||
<div className="invalid-feedback d-block">
|
||||
{ LocalizeText('inventory.marketplace.make_offer.min_price', [ 'minprice' ], [ marketplaceConfiguration.minimumPrice.toString() ]) }
|
||||
</div> }
|
||||
{ ((askingPrice > marketplaceConfiguration.maximumPrice) && !isNaN(askingPrice)) &&
|
||||
<div className="invalid-feedback d-block">
|
||||
{ LocalizeText('inventory.marketplace.make_offer.max_price', [ 'maxprice' ], [ marketplaceConfiguration.maximumPrice.toString() ]) }
|
||||
</div> }
|
||||
{ (!((askingPrice < marketplaceConfiguration.minimumPrice) || (askingPrice > marketplaceConfiguration.maximumPrice) || isNaN(askingPrice))) &&
|
||||
<div className="invalid-feedback d-block">
|
||||
{ LocalizeText('inventory.marketplace.make_offer.final_price', [ 'commission', 'finalprice' ], [ getCommission().toString(), (askingPrice + getCommission()).toString() ]) }
|
||||
</div> }
|
||||
</div>
|
||||
<Button disabled={ ((askingPrice < marketplaceConfiguration.minimumPrice) || (askingPrice > marketplaceConfiguration.maximumPrice) || isNaN(askingPrice)) } onClick={ postItem }>
|
||||
{ LocalizeText('inventory.marketplace.make_offer.post') }
|
||||
</Button>
|
||||
</Column>
|
||||
</Column>
|
||||
</Grid>
|
||||
</NitroCardContentView>
|
||||
</NitroCardView>
|
||||
);
|
||||
};
|
||||
+243
@@ -0,0 +1,243 @@
|
||||
import { ApproveNameMessageComposer, ApproveNameMessageEvent, ColorConverter, GetSellablePetPalettesComposer, PurchaseFromCatalogComposer, SellablePetPaletteData } from '@nitrots/nitro-renderer';
|
||||
import { FC, useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { FaFillDrip } from 'react-icons/fa';
|
||||
import { DispatchUiEvent, GetPetAvailableColors, GetPetIndexFromLocalization, LocalizeText, SendMessageComposer } from '../../../../../../api';
|
||||
import { AutoGrid, Button, Column, Grid, LayoutGridItem, LayoutPetImageView, Text } from '../../../../../../common';
|
||||
import { CatalogPurchaseFailureEvent } from '../../../../../../events';
|
||||
import { useCatalog, useMessageEvent } from '../../../../../../hooks';
|
||||
import { CatalogAddOnBadgeWidgetView } from '../../widgets/CatalogAddOnBadgeWidgetView';
|
||||
import { CatalogPurchaseWidgetView } from '../../widgets/CatalogPurchaseWidgetView';
|
||||
import { CatalogTotalPriceWidget } from '../../widgets/CatalogTotalPriceWidget';
|
||||
import { CatalogViewProductWidgetView } from '../../widgets/CatalogViewProductWidgetView';
|
||||
import { CatalogLayoutProps } from '../CatalogLayout.types';
|
||||
|
||||
export const CatalogLayoutPetView: FC<CatalogLayoutProps> = props =>
|
||||
{
|
||||
const { page = null } = props;
|
||||
const [ petIndex, setPetIndex ] = useState(-1);
|
||||
const [ sellablePalettes, setSellablePalettes ] = useState<SellablePetPaletteData[]>([]);
|
||||
const [ selectedPaletteIndex, setSelectedPaletteIndex ] = useState(-1);
|
||||
const [ sellableColors, setSellableColors ] = useState<number[][]>([]);
|
||||
const [ selectedColorIndex, setSelectedColorIndex ] = useState(-1);
|
||||
const [ colorsShowing, setColorsShowing ] = useState(false);
|
||||
const [ petName, setPetName ] = useState('');
|
||||
const [ approvalPending, setApprovalPending ] = useState(true);
|
||||
const [ approvalResult, setApprovalResult ] = useState(-1);
|
||||
const { currentOffer = null, setCurrentOffer = null, setPurchaseOptions = null, catalogOptions = null, roomPreviewer = null } = useCatalog();
|
||||
const { petPalettes = null } = catalogOptions;
|
||||
|
||||
const getColor = useMemo(() =>
|
||||
{
|
||||
if(!sellableColors.length || (selectedColorIndex === -1)) return 0xFFFFFF;
|
||||
|
||||
return sellableColors[selectedColorIndex][0];
|
||||
}, [ sellableColors, selectedColorIndex ]);
|
||||
|
||||
const petBreedName = useMemo(() =>
|
||||
{
|
||||
if((petIndex === -1) || !sellablePalettes.length || (selectedPaletteIndex === -1)) return '';
|
||||
|
||||
return LocalizeText(`pet.breed.${ petIndex }.${ sellablePalettes[selectedPaletteIndex].breedId }`);
|
||||
}, [ petIndex, sellablePalettes, selectedPaletteIndex ]);
|
||||
|
||||
const petPurchaseString = useMemo(() =>
|
||||
{
|
||||
if(!sellablePalettes.length || (selectedPaletteIndex === -1)) return '';
|
||||
|
||||
const paletteId = sellablePalettes[selectedPaletteIndex].paletteId;
|
||||
|
||||
let color = 0xFFFFFF;
|
||||
|
||||
if(petIndex <= 7)
|
||||
{
|
||||
if(selectedColorIndex === -1) return '';
|
||||
|
||||
color = sellableColors[selectedColorIndex][0];
|
||||
}
|
||||
|
||||
let colorString = color.toString(16).toUpperCase();
|
||||
|
||||
while(colorString.length < 6) colorString = ('0' + colorString);
|
||||
|
||||
return `${ paletteId }\n${ colorString }`;
|
||||
}, [ sellablePalettes, selectedPaletteIndex, petIndex, sellableColors, selectedColorIndex ]);
|
||||
|
||||
const validationErrorMessage = useMemo(() =>
|
||||
{
|
||||
let key: string = '';
|
||||
|
||||
switch(approvalResult)
|
||||
{
|
||||
case 1:
|
||||
key = 'catalog.alert.petname.long';
|
||||
break;
|
||||
case 2:
|
||||
key = 'catalog.alert.petname.short';
|
||||
break;
|
||||
case 3:
|
||||
key = 'catalog.alert.petname.chars';
|
||||
break;
|
||||
case 4:
|
||||
key = 'catalog.alert.petname.bobba';
|
||||
break;
|
||||
}
|
||||
|
||||
if(!key || !key.length) return '';
|
||||
|
||||
return LocalizeText(key);
|
||||
}, [ approvalResult ]);
|
||||
|
||||
const purchasePet = useCallback(() =>
|
||||
{
|
||||
if(approvalResult === -1)
|
||||
{
|
||||
SendMessageComposer(new ApproveNameMessageComposer(petName, 1));
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if(approvalResult === 0)
|
||||
{
|
||||
SendMessageComposer(new PurchaseFromCatalogComposer(page.pageId, currentOffer.offerId, `${ petName }\n${ petPurchaseString }`, 1));
|
||||
|
||||
return;
|
||||
}
|
||||
}, [ page, currentOffer, petName, petPurchaseString, approvalResult ]);
|
||||
|
||||
useMessageEvent<ApproveNameMessageEvent>(ApproveNameMessageEvent, event =>
|
||||
{
|
||||
const parser = event.getParser();
|
||||
|
||||
setApprovalResult(parser.result);
|
||||
|
||||
if(parser.result === 0) purchasePet();
|
||||
else DispatchUiEvent(new CatalogPurchaseFailureEvent(-1));
|
||||
});
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
if(!page || !page.offers.length) return;
|
||||
|
||||
const offer = page.offers[0];
|
||||
|
||||
setCurrentOffer(offer);
|
||||
setPetIndex(GetPetIndexFromLocalization(offer.localizationId));
|
||||
setColorsShowing(false);
|
||||
}, [ page, setCurrentOffer ]);
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
if(!currentOffer) return;
|
||||
|
||||
const productData = currentOffer.product.productData;
|
||||
|
||||
if(!productData) return;
|
||||
|
||||
if(petPalettes)
|
||||
{
|
||||
for(const paletteData of petPalettes)
|
||||
{
|
||||
if(paletteData.breed !== productData.type) continue;
|
||||
|
||||
const palettes: SellablePetPaletteData[] = [];
|
||||
|
||||
for(const palette of paletteData.palettes)
|
||||
{
|
||||
if(!palette.sellable) continue;
|
||||
|
||||
palettes.push(palette);
|
||||
}
|
||||
|
||||
setSelectedPaletteIndex((palettes.length ? 0 : -1));
|
||||
setSellablePalettes(palettes);
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
setSelectedPaletteIndex(-1);
|
||||
setSellablePalettes([]);
|
||||
|
||||
SendMessageComposer(new GetSellablePetPalettesComposer(productData.type));
|
||||
}, [ currentOffer, petPalettes ]);
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
if(petIndex === -1) return;
|
||||
|
||||
const colors = GetPetAvailableColors(petIndex, sellablePalettes);
|
||||
|
||||
setSelectedColorIndex((colors.length ? 0 : -1));
|
||||
setSellableColors(colors);
|
||||
}, [ petIndex, sellablePalettes ]);
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
if(!roomPreviewer) return;
|
||||
|
||||
roomPreviewer.reset(false);
|
||||
|
||||
if((petIndex === -1) || !sellablePalettes.length || (selectedPaletteIndex === -1)) return;
|
||||
|
||||
let petFigureString = `${ petIndex } ${ sellablePalettes[selectedPaletteIndex].paletteId }`;
|
||||
|
||||
if(petIndex <= 7) petFigureString += ` ${ getColor.toString(16) }`;
|
||||
|
||||
roomPreviewer.addPetIntoRoom(petFigureString);
|
||||
}, [ roomPreviewer, petIndex, sellablePalettes, selectedPaletteIndex, getColor ]);
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
setApprovalResult(-1);
|
||||
}, [ petName ]);
|
||||
|
||||
if(!currentOffer) return null;
|
||||
|
||||
return (
|
||||
<Grid>
|
||||
<Column overflow="hidden" size={ 7 }>
|
||||
<AutoGrid columnCount={ 5 }>
|
||||
{ !colorsShowing && (sellablePalettes.length > 0) && sellablePalettes.map((palette, index) =>
|
||||
{
|
||||
return (
|
||||
<LayoutGridItem key={ index } itemActive={ (selectedPaletteIndex === index) } onClick={ event => setSelectedPaletteIndex(index) }>
|
||||
<LayoutPetImageView direction={ 2 } headOnly={ true } paletteId={ palette.paletteId } typeId={ petIndex } />
|
||||
</LayoutGridItem>
|
||||
);
|
||||
}) }
|
||||
{ colorsShowing && (sellableColors.length > 0) && sellableColors.map((colorSet, index) => <LayoutGridItem key={ index } itemHighlight className="clear-bg" itemActive={ (selectedColorIndex === index) } itemColor={ ColorConverter.int2rgb(colorSet[0]) } onClick={ event => setSelectedColorIndex(index) } />) }
|
||||
</AutoGrid>
|
||||
</Column>
|
||||
<Column center={ !currentOffer } overflow="hidden" size={ 5 }>
|
||||
{ !currentOffer &&
|
||||
<>
|
||||
{ !!page.localization.getImage(1) && <img alt="" src={ page.localization.getImage(1) } /> }
|
||||
<Text center dangerouslySetInnerHTML={ { __html: page.localization.getText(0) } } />
|
||||
</> }
|
||||
{ currentOffer &&
|
||||
<>
|
||||
<div className="relative overflow-hidden">
|
||||
<CatalogViewProductWidgetView />
|
||||
<CatalogAddOnBadgeWidgetView className="bg-muted rounded bottom-1 end-1" position="absolute" />
|
||||
{ ((petIndex > -1) && (petIndex <= 7)) &&
|
||||
<Button className="bottom-1 start-1" position="absolute" onClick={ event => setColorsShowing(!colorsShowing) }>
|
||||
<FaFillDrip className="fa-icon" />
|
||||
</Button> }
|
||||
</div>
|
||||
<Column grow gap={ 1 }>
|
||||
<Text truncate>{ petBreedName }</Text>
|
||||
<Column grow gap={ 1 }>
|
||||
<input className="min-h-[calc(1.5em+ .5rem+2px)] px-[.5rem] py-[.25rem] rounded-[.2rem] form-control-sm w-full" placeholder={ LocalizeText('widgets.petpackage.name.title') } type="text" value={ petName } onChange={ event => setPetName(event.target.value) } />
|
||||
{ (approvalResult > 0) &&
|
||||
<div className="invalid-feedback d-block m-0">{ validationErrorMessage }</div> }
|
||||
</Column>
|
||||
<div className="flex justify-end">
|
||||
<CatalogTotalPriceWidget alignItems="end" justifyContent="end" />
|
||||
</div>
|
||||
<CatalogPurchaseWidgetView purchaseCallback={ purchasePet } />
|
||||
</Column>
|
||||
</> }
|
||||
</Column>
|
||||
</Grid>
|
||||
);
|
||||
};
|
||||
+62
@@ -0,0 +1,62 @@
|
||||
import { SelectClubGiftComposer } from '@nitrots/nitro-renderer';
|
||||
import { FC, useCallback, useMemo } from 'react';
|
||||
import { LocalizeText, SendMessageComposer } from '../../../../../../api';
|
||||
import { AutoGrid, Text } from '../../../../../../common';
|
||||
import { useCatalog, useNotification, usePurse } from '../../../../../../hooks';
|
||||
import { CatalogLayoutProps } from '../CatalogLayout.types';
|
||||
import { VipGiftItem } from './VipGiftItemView';
|
||||
|
||||
export const CatalogLayoutVipGiftsView: FC<CatalogLayoutProps> = props =>
|
||||
{
|
||||
const { purse = null } = usePurse();
|
||||
const { catalogOptions = null, setCatalogOptions = null } = useCatalog();
|
||||
const { clubGifts = null } = catalogOptions;
|
||||
const { showConfirm = null } = useNotification();
|
||||
|
||||
const giftsAvailable = useCallback(() =>
|
||||
{
|
||||
if(!clubGifts) return '';
|
||||
|
||||
if(clubGifts.giftsAvailable > 0) return LocalizeText('catalog.club_gift.available', [ 'amount' ], [ clubGifts.giftsAvailable.toString() ]);
|
||||
|
||||
if(clubGifts.daysUntilNextGift > 0) return LocalizeText('catalog.club_gift.days_until_next', [ 'days' ], [ clubGifts.daysUntilNextGift.toString() ]);
|
||||
|
||||
if(purse.isVip) return LocalizeText('catalog.club_gift.not_available');
|
||||
|
||||
return LocalizeText('catalog.club_gift.no_club');
|
||||
}, [ clubGifts, purse ]);
|
||||
|
||||
const selectGift = useCallback((localizationId: string) =>
|
||||
{
|
||||
showConfirm(LocalizeText('catalog.club_gift.confirm'), () =>
|
||||
{
|
||||
SendMessageComposer(new SelectClubGiftComposer(localizationId));
|
||||
|
||||
setCatalogOptions(prevValue =>
|
||||
{
|
||||
prevValue.clubGifts.giftsAvailable--;
|
||||
|
||||
return { ...prevValue };
|
||||
});
|
||||
}, null);
|
||||
}, [ setCatalogOptions, showConfirm ]);
|
||||
|
||||
const sortGifts = useMemo(() =>
|
||||
{
|
||||
let gifts = clubGifts.offers.sort((a,b) =>
|
||||
{
|
||||
return clubGifts.getOfferExtraData(a.offerId).daysRequired - clubGifts.getOfferExtraData(b.offerId).daysRequired;
|
||||
});
|
||||
return gifts;
|
||||
},[ clubGifts ]);
|
||||
|
||||
|
||||
return (
|
||||
<>
|
||||
<Text shrink truncate fontWeight="bold">{ giftsAvailable() }</Text>
|
||||
<AutoGrid className="nitro-catalog-layout-vip-gifts-grid" columnCount={ 1 }>
|
||||
{ (clubGifts.offers.length > 0) && sortGifts.map(offer => <VipGiftItem key={ offer.offerId } daysRequired={ clubGifts.getOfferExtraData(offer.offerId).daysRequired } isAvailable={ (clubGifts.getOfferExtraData(offer.offerId).isSelectable && (clubGifts.giftsAvailable > 0)) } offer={ offer } onSelect={ selectGift }/>) }
|
||||
</AutoGrid>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,63 @@
|
||||
import { CatalogPageMessageOfferData } from '@nitrots/nitro-renderer';
|
||||
import { FC, useCallback } from 'react';
|
||||
import { LocalizeText, ProductImageUtility } from '../../../../../../api';
|
||||
import { Button, LayoutGridItem, LayoutImage, Text } from '../../../../../../common';
|
||||
|
||||
export interface VipGiftItemViewProps
|
||||
{
|
||||
offer: CatalogPageMessageOfferData;
|
||||
isAvailable: boolean;
|
||||
daysRequired: number;
|
||||
onSelect(localizationId: string): void;
|
||||
}
|
||||
|
||||
export const VipGiftItem : FC<VipGiftItemViewProps> = props =>
|
||||
{
|
||||
const { offer = null, isAvailable = false, daysRequired = 0, onSelect = null } = props;
|
||||
|
||||
const getImageUrlForOffer = useCallback( () =>
|
||||
{
|
||||
if(!offer || !offer.products.length) return '';
|
||||
|
||||
const productData = offer.products[0];
|
||||
|
||||
return ProductImageUtility.getProductImageUrl(productData.productType, productData.furniClassId, productData.extraParam);
|
||||
}, [ offer ]);
|
||||
|
||||
const getItemTitle = useCallback(() =>
|
||||
{
|
||||
if(!offer || !offer.products.length) return '';
|
||||
|
||||
const productData = offer.products[0];
|
||||
|
||||
const localizationKey = ProductImageUtility.getProductCategory(productData.productType, productData.furniClassId) === 2 ? 'wallItem.name.' + productData.furniClassId : 'roomItem.name.' + productData.furniClassId;
|
||||
|
||||
return LocalizeText(localizationKey);
|
||||
}, [ offer ]);
|
||||
|
||||
const getItemDesc = useCallback( () =>
|
||||
{
|
||||
if(!offer || !offer.products.length) return '';
|
||||
|
||||
const productData = offer.products[0];
|
||||
|
||||
const localizationKey = ProductImageUtility.getProductCategory(productData.productType, productData.furniClassId) === 2 ? 'wallItem.desc.' + productData.furniClassId : 'roomItem.desc.' + productData.furniClassId ;
|
||||
|
||||
return LocalizeText(localizationKey);
|
||||
}, [ offer ]);
|
||||
|
||||
const getMonthsRequired = useCallback(() =>
|
||||
{
|
||||
return Math.floor(daysRequired / 31);
|
||||
},[ daysRequired ]);
|
||||
|
||||
return (
|
||||
<LayoutGridItem alignItems="center" center={ false } className="p-1" column={ false }>
|
||||
<LayoutImage imageUrl={ getImageUrlForOffer() } />
|
||||
<Text grow fontWeight="bold">{ getItemTitle() }</Text>
|
||||
<Button disabled={ !isAvailable } variant="secondary" onClick={ () => onSelect(offer.localizationId) }>
|
||||
{ LocalizeText('catalog.club_gift.select') }
|
||||
</Button>
|
||||
</LayoutGridItem>
|
||||
);
|
||||
};
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
import { FC } from 'react';
|
||||
import { BaseProps, LayoutBadgeImageView } from '../../../../../common';
|
||||
import { useCatalog } from '../../../../../hooks';
|
||||
|
||||
interface CatalogAddOnBadgeWidgetViewProps extends BaseProps<HTMLDivElement>
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
export const CatalogAddOnBadgeWidgetView: FC<CatalogAddOnBadgeWidgetViewProps> = props =>
|
||||
{
|
||||
const { ...rest } = props;
|
||||
const { currentOffer = null } = useCatalog();
|
||||
|
||||
if(!currentOffer || !currentOffer.badgeCode || !currentOffer.badgeCode.length) return null;
|
||||
|
||||
return <LayoutBadgeImageView badgeCode={ currentOffer.badgeCode } { ...rest } />;
|
||||
};
|
||||
+76
@@ -0,0 +1,76 @@
|
||||
import { StringDataType } from '@nitrots/nitro-renderer';
|
||||
import { FC, useEffect, useMemo, useState } from 'react';
|
||||
import { AutoGrid, AutoGridProps, LayoutBadgeImageView, LayoutGridItem } from '../../../../../common';
|
||||
import { useCatalog, useInventoryBadges } from '../../../../../hooks';
|
||||
|
||||
const EXCLUDED_BADGE_CODES: string[] = [];
|
||||
|
||||
interface CatalogBadgeSelectorWidgetViewProps extends AutoGridProps
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
export const CatalogBadgeSelectorWidgetView: FC<CatalogBadgeSelectorWidgetViewProps> = props =>
|
||||
{
|
||||
const { columnCount = 5, ...rest } = props;
|
||||
const [ isVisible, setIsVisible ] = useState(false);
|
||||
const [ currentBadgeCode, setCurrentBadgeCode ] = useState<string>(null);
|
||||
const { currentOffer = null, setPurchaseOptions = null } = useCatalog();
|
||||
const { badgeCodes = [], activate = null, deactivate = null } = useInventoryBadges();
|
||||
|
||||
const previewStuffData = useMemo(() =>
|
||||
{
|
||||
if(!currentBadgeCode) return null;
|
||||
|
||||
const stuffData = new StringDataType();
|
||||
|
||||
stuffData.setValue([ '0', currentBadgeCode, '', '' ]);
|
||||
|
||||
return stuffData;
|
||||
}, [ currentBadgeCode ]);
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
if(!currentOffer) return;
|
||||
|
||||
setPurchaseOptions(prevValue =>
|
||||
{
|
||||
const newValue = { ...prevValue };
|
||||
|
||||
newValue.extraParamRequired = true;
|
||||
newValue.extraData = ((previewStuffData && previewStuffData.getValue(1)) || null);
|
||||
newValue.previewStuffData = previewStuffData;
|
||||
|
||||
return newValue;
|
||||
});
|
||||
}, [ currentOffer, previewStuffData, setPurchaseOptions ]);
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
if(!isVisible) return;
|
||||
|
||||
const id = activate();
|
||||
|
||||
return () => deactivate(id);
|
||||
}, [ isVisible, activate, deactivate ]);
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
setIsVisible(true);
|
||||
|
||||
return () => setIsVisible(false);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<AutoGrid columnCount={ columnCount } { ...rest }>
|
||||
{ badgeCodes && (badgeCodes.length > 0) && badgeCodes.map((badgeCode, index) =>
|
||||
{
|
||||
return (
|
||||
<LayoutGridItem key={ index } itemActive={ (currentBadgeCode === badgeCode) } onClick={ event => setCurrentBadgeCode(badgeCode) }>
|
||||
<LayoutBadgeImageView badgeCode={ badgeCode } />
|
||||
</LayoutGridItem>
|
||||
);
|
||||
}) }
|
||||
</AutoGrid>
|
||||
);
|
||||
};
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
import { FC, useEffect, useRef } from 'react';
|
||||
import { AutoGrid, AutoGridProps, LayoutGridItem } from '../../../../../common';
|
||||
import { useCatalog } from '../../../../../hooks';
|
||||
|
||||
interface CatalogBundleGridWidgetViewProps extends AutoGridProps
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
export const CatalogBundleGridWidgetView: FC<CatalogBundleGridWidgetViewProps> = props =>
|
||||
{
|
||||
const { columnCount = 5, children = null, ...rest } = props;
|
||||
const { currentOffer = null } = useCatalog();
|
||||
const elementRef = useRef<HTMLDivElement>();
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
if(elementRef && elementRef.current) elementRef.current.scrollTop = 0;
|
||||
}, [ currentOffer ]);
|
||||
|
||||
if(!currentOffer) return null;
|
||||
|
||||
return (
|
||||
<AutoGrid columnCount={ 5 } innerRef={ elementRef } { ...rest }>
|
||||
{ currentOffer.products && (currentOffer.products.length > 0) && currentOffer.products.map((product, index) => <LayoutGridItem key={ index } itemCount={ product.productCount } itemImage={ product.getIconUrl() } />) }
|
||||
{ children }
|
||||
</AutoGrid>
|
||||
);
|
||||
};
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
import { FC, useEffect } from 'react';
|
||||
import { useCatalog } from '../../../../../hooks';
|
||||
|
||||
export const CatalogFirstProductSelectorWidgetView: FC<{}> = props =>
|
||||
{
|
||||
const { currentPage = null, setCurrentOffer = null } = useCatalog();
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
if(!currentPage || !currentPage.offers.length) return;
|
||||
|
||||
setCurrentOffer(currentPage.offers[0]);
|
||||
}, [ currentPage, setCurrentOffer ]);
|
||||
|
||||
return null;
|
||||
};
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
import { StringDataType } from '@nitrots/nitro-renderer';
|
||||
import { FC, useMemo } from 'react';
|
||||
import { BaseProps, LayoutBadgeImageView } from '../../../../../common';
|
||||
import { useCatalog } from '../../../../../hooks';
|
||||
|
||||
interface CatalogGuildBadgeWidgetViewProps extends BaseProps<HTMLDivElement>
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
export const CatalogGuildBadgeWidgetView: FC<CatalogGuildBadgeWidgetViewProps> = props =>
|
||||
{
|
||||
const { ...rest } = props;
|
||||
const { currentOffer = null, purchaseOptions = null } = useCatalog();
|
||||
const { previewStuffData = null } = purchaseOptions;
|
||||
|
||||
const badgeCode = useMemo(() =>
|
||||
{
|
||||
if(!currentOffer || !previewStuffData) return null;
|
||||
|
||||
const badgeCode = (previewStuffData as StringDataType).getValue(2);
|
||||
|
||||
if(!badgeCode || !badgeCode.length) return null;
|
||||
|
||||
return badgeCode;
|
||||
}, [ currentOffer, previewStuffData ]);
|
||||
|
||||
if(!badgeCode) return null;
|
||||
|
||||
return <LayoutBadgeImageView badgeCode={ badgeCode } isGroup={ true } { ...rest } />;
|
||||
};
|
||||
+75
@@ -0,0 +1,75 @@
|
||||
import { CatalogGroupsComposer, StringDataType } from '@nitrots/nitro-renderer';
|
||||
import { FC, useEffect, useMemo, useState } from 'react';
|
||||
import { LocalizeText, SendMessageComposer } from '../../../../../api';
|
||||
import { Button, Flex } from '../../../../../common';
|
||||
import { useCatalog } from '../../../../../hooks';
|
||||
|
||||
export const CatalogGuildSelectorWidgetView: FC<{}> = props =>
|
||||
{
|
||||
const [ selectedGroupIndex, setSelectedGroupIndex ] = useState<number>(0);
|
||||
const { currentOffer = null, catalogOptions = null, setPurchaseOptions = null } = useCatalog();
|
||||
const { groups = null } = catalogOptions;
|
||||
|
||||
const previewStuffData = useMemo(() =>
|
||||
{
|
||||
if(!groups || !groups.length) return null;
|
||||
|
||||
const group = groups[selectedGroupIndex];
|
||||
|
||||
if(!group) return null;
|
||||
|
||||
const stuffData = new StringDataType();
|
||||
|
||||
stuffData.setValue([ '0', group.groupId.toString(), group.badgeCode, group.colorA, group.colorB ]);
|
||||
|
||||
return stuffData;
|
||||
}, [ selectedGroupIndex, groups ]);
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
if(!currentOffer) return;
|
||||
|
||||
setPurchaseOptions(prevValue =>
|
||||
{
|
||||
const newValue = { ...prevValue };
|
||||
|
||||
newValue.extraParamRequired = true;
|
||||
newValue.extraData = ((previewStuffData && previewStuffData.getValue(1)) || null);
|
||||
newValue.previewStuffData = previewStuffData;
|
||||
|
||||
return newValue;
|
||||
});
|
||||
}, [ currentOffer, previewStuffData, setPurchaseOptions ]);
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
SendMessageComposer(new CatalogGroupsComposer());
|
||||
}, []);
|
||||
|
||||
if(!groups || !groups.length)
|
||||
{
|
||||
return (
|
||||
<div className="bg-muted rounded p-1 text-black text-center">
|
||||
{ LocalizeText('catalog.guild_selector.members_only') }
|
||||
<Button className="mt-1">
|
||||
{ LocalizeText('catalog.guild_selector.find_groups') }
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const selectedGroup = groups[selectedGroupIndex];
|
||||
|
||||
return (
|
||||
<div className="flex gap-1">
|
||||
{ !!selectedGroup &&
|
||||
<Flex className="rounded border" overflow="hidden">
|
||||
<div className="h-full" style={ { width: '20px', backgroundColor: '#' + selectedGroup.colorA } } />
|
||||
<div className="h-full" style={ { width: '20px', backgroundColor: '#' + selectedGroup.colorB } } />
|
||||
</Flex> }
|
||||
<select className="form-select form-select-sm" value={ selectedGroupIndex } onChange={ event => setSelectedGroupIndex(parseInt(event.target.value)) }>
|
||||
{ groups.map((group, index) => <option key={ index } value={ index }>{ group.groupName }</option>) }
|
||||
</select>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
+52
@@ -0,0 +1,52 @@
|
||||
import { FC, useEffect, useRef } from 'react';
|
||||
import { IPurchasableOffer, ProductTypeEnum } from '../../../../../api';
|
||||
import { AutoGrid, AutoGridProps } from '../../../../../common';
|
||||
import { useCatalog } from '../../../../../hooks';
|
||||
import { CatalogGridOfferView } from '../common/CatalogGridOfferView';
|
||||
|
||||
interface CatalogItemGridWidgetViewProps extends AutoGridProps
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
export const CatalogItemGridWidgetView: FC<CatalogItemGridWidgetViewProps> = props =>
|
||||
{
|
||||
const { columnCount = 5, children = null, ...rest } = props;
|
||||
const { currentOffer = null, setCurrentOffer = null, currentPage = null, setPurchaseOptions = null } = useCatalog();
|
||||
const elementRef = useRef<HTMLDivElement>();
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
if(elementRef && elementRef.current) elementRef.current.scrollTop = 0;
|
||||
}, [ currentPage ]);
|
||||
|
||||
if(!currentPage) return null;
|
||||
|
||||
const selectOffer = (offer: IPurchasableOffer) =>
|
||||
{
|
||||
offer.activate();
|
||||
|
||||
if(offer.isLazy) return;
|
||||
|
||||
setCurrentOffer(offer);
|
||||
|
||||
if(offer.product && (offer.product.productType === ProductTypeEnum.WALL))
|
||||
{
|
||||
setPurchaseOptions(prevValue =>
|
||||
{
|
||||
const newValue = { ...prevValue };
|
||||
|
||||
newValue.extraData = (offer.product.extraParam || null);
|
||||
|
||||
return newValue;
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<AutoGrid columnCount={ columnCount } innerRef={ elementRef } { ...rest }>
|
||||
{ currentPage.offers && (currentPage.offers.length > 0) && currentPage.offers.map((offer, index) => <CatalogGridOfferView key={ index } itemActive={ (currentOffer && (currentOffer.offerId === offer.offerId)) } offer={ offer } selectOffer={ selectOffer } />) }
|
||||
{ children }
|
||||
</AutoGrid>
|
||||
);
|
||||
};
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
import { FC } from 'react';
|
||||
import { Offer } from '../../../../../api';
|
||||
import { LayoutLimitedEditionCompletePlateView } from '../../../../../common';
|
||||
import { useCatalog } from '../../../../../hooks';
|
||||
|
||||
export const CatalogLimitedItemWidgetView: FC = props =>
|
||||
{
|
||||
const { currentOffer = null } = useCatalog();
|
||||
|
||||
if(!currentOffer || (currentOffer.pricingModel !== Offer.PRICING_MODEL_SINGLE) || !currentOffer.product.isUniqueLimitedItem) return null;
|
||||
|
||||
return (
|
||||
<div className="w-full">
|
||||
<LayoutLimitedEditionCompletePlateView className="mx-auto" uniqueLimitedItemsLeft={ currentOffer.product.uniqueLimitedItemsLeft } uniqueLimitedSeriesSize={ currentOffer.product.uniqueLimitedItemSeriesSize } />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
import { FC } from 'react';
|
||||
import { FaPlus } from 'react-icons/fa';
|
||||
import { IPurchasableOffer } from '../../../../../api';
|
||||
import { LayoutCurrencyIcon, Text } from '../../../../../common';
|
||||
import { useCatalog } from '../../../../../hooks';
|
||||
|
||||
interface CatalogPriceDisplayWidgetViewProps
|
||||
{
|
||||
offer: IPurchasableOffer;
|
||||
separator?: boolean;
|
||||
}
|
||||
|
||||
export const CatalogPriceDisplayWidgetView: FC<CatalogPriceDisplayWidgetViewProps> = props =>
|
||||
{
|
||||
const { offer = null, separator = false } = props;
|
||||
const { purchaseOptions = null } = useCatalog();
|
||||
const { quantity = 1 } = purchaseOptions;
|
||||
|
||||
if(!offer) return null;
|
||||
|
||||
return (
|
||||
<>
|
||||
{ (offer.priceInCredits > 0) &&
|
||||
<div className="flex items-center gap-1">
|
||||
<Text bold>{ (offer.priceInCredits * quantity) }</Text>
|
||||
<LayoutCurrencyIcon type={ -1 } />
|
||||
</div> }
|
||||
{ separator && (offer.priceInCredits > 0) && (offer.priceInActivityPoints > 0) &&
|
||||
<FaPlus className="fa-icon" color="black" size="xs" /> }
|
||||
{ (offer.priceInActivityPoints > 0) &&
|
||||
<div className="flex items-center gap-1">
|
||||
<Text bold>{ (offer.priceInActivityPoints * quantity) }</Text>
|
||||
<LayoutCurrencyIcon type={ offer.activityPointType } />
|
||||
</div> }
|
||||
</>
|
||||
);
|
||||
};
|
||||
+164
@@ -0,0 +1,164 @@
|
||||
import { CreateLinkEvent, PurchaseFromCatalogComposer } from '@nitrots/nitro-renderer';
|
||||
import { FC, useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { CatalogPurchaseState, DispatchUiEvent, GetClubMemberLevel, LocalStorageKeys, LocalizeText, Offer, SendMessageComposer } from '../../../../../api';
|
||||
import { Button, LayoutLoadingSpinnerView } from '../../../../../common';
|
||||
import { CatalogEvent, CatalogInitGiftEvent, CatalogPurchaseFailureEvent, CatalogPurchaseNotAllowedEvent, CatalogPurchaseSoldOutEvent, CatalogPurchasedEvent } from '../../../../../events';
|
||||
import { useCatalog, useLocalStorage, usePurse, useUiEvent } from '../../../../../hooks';
|
||||
|
||||
interface CatalogPurchaseWidgetViewProps
|
||||
{
|
||||
noGiftOption?: boolean;
|
||||
purchaseCallback?: () => void;
|
||||
}
|
||||
|
||||
export const CatalogPurchaseWidgetView: FC<CatalogPurchaseWidgetViewProps> = props =>
|
||||
{
|
||||
const { noGiftOption = false, purchaseCallback = null } = props;
|
||||
const [ purchaseWillBeGift, setPurchaseWillBeGift ] = useState(false);
|
||||
const [ purchaseState, setPurchaseState ] = useState(CatalogPurchaseState.NONE);
|
||||
const [ catalogSkipPurchaseConfirmation, setCatalogSkipPurchaseConfirmation ] = useLocalStorage(LocalStorageKeys.CATALOG_SKIP_PURCHASE_CONFIRMATION, false);
|
||||
const { currentOffer = null, currentPage = null, purchaseOptions = null, setPurchaseOptions = null } = useCatalog();
|
||||
const { getCurrencyAmount = null } = usePurse();
|
||||
|
||||
const onCatalogEvent = useCallback((event: CatalogEvent) =>
|
||||
{
|
||||
switch(event.type)
|
||||
{
|
||||
case CatalogPurchasedEvent.PURCHASE_SUCCESS:
|
||||
setPurchaseState(CatalogPurchaseState.NONE);
|
||||
return;
|
||||
case CatalogPurchaseFailureEvent.PURCHASE_FAILED:
|
||||
setPurchaseState(CatalogPurchaseState.FAILED);
|
||||
return;
|
||||
case CatalogPurchaseNotAllowedEvent.NOT_ALLOWED:
|
||||
setPurchaseState(CatalogPurchaseState.FAILED);
|
||||
return;
|
||||
case CatalogPurchaseSoldOutEvent.SOLD_OUT:
|
||||
setPurchaseState(CatalogPurchaseState.SOLD_OUT);
|
||||
return;
|
||||
}
|
||||
}, []);
|
||||
|
||||
useUiEvent(CatalogPurchasedEvent.PURCHASE_SUCCESS, onCatalogEvent);
|
||||
useUiEvent(CatalogPurchaseFailureEvent.PURCHASE_FAILED, onCatalogEvent);
|
||||
useUiEvent(CatalogPurchaseNotAllowedEvent.NOT_ALLOWED, onCatalogEvent);
|
||||
useUiEvent(CatalogPurchaseSoldOutEvent.SOLD_OUT, onCatalogEvent);
|
||||
|
||||
const isLimitedSoldOut = useMemo(() =>
|
||||
{
|
||||
if(!currentOffer) return false;
|
||||
|
||||
if(purchaseOptions.extraParamRequired && (!purchaseOptions.extraData || !purchaseOptions.extraData.length)) return false;
|
||||
|
||||
if(currentOffer.pricingModel === Offer.PRICING_MODEL_SINGLE)
|
||||
{
|
||||
const product = currentOffer.product;
|
||||
|
||||
if(product && product.isUniqueLimitedItem) return !product.uniqueLimitedItemsLeft;
|
||||
}
|
||||
|
||||
return false;
|
||||
}, [ currentOffer, purchaseOptions ]);
|
||||
|
||||
const purchase = (isGift: boolean = false) =>
|
||||
{
|
||||
if(!currentOffer) return;
|
||||
|
||||
if(GetClubMemberLevel() < currentOffer.clubLevel)
|
||||
{
|
||||
CreateLinkEvent('habboUI/open/hccenter');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if(isGift)
|
||||
{
|
||||
DispatchUiEvent(new CatalogInitGiftEvent(currentOffer.page.pageId, currentOffer.offerId, purchaseOptions.extraData));
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
setPurchaseState(CatalogPurchaseState.PURCHASE);
|
||||
|
||||
if(purchaseCallback)
|
||||
{
|
||||
purchaseCallback();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
let pageId = currentOffer.page.pageId;
|
||||
|
||||
// if(pageId === -1)
|
||||
// {
|
||||
// const nodes = getNodesByOfferId(currentOffer.offerId);
|
||||
|
||||
// if(nodes) pageId = nodes[0].pageId;
|
||||
// }
|
||||
|
||||
SendMessageComposer(new PurchaseFromCatalogComposer(pageId, currentOffer.offerId, purchaseOptions.extraData, purchaseOptions.quantity));
|
||||
};
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
if(!currentOffer) return;
|
||||
|
||||
setPurchaseState(CatalogPurchaseState.NONE);
|
||||
}, [ currentOffer, setPurchaseOptions ]);
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
let timeout: ReturnType<typeof setTimeout> = null;
|
||||
|
||||
if((purchaseState === CatalogPurchaseState.CONFIRM) || (purchaseState === CatalogPurchaseState.FAILED))
|
||||
{
|
||||
timeout = setTimeout(() => setPurchaseState(CatalogPurchaseState.NONE), 3000);
|
||||
}
|
||||
|
||||
return () =>
|
||||
{
|
||||
if(timeout) clearTimeout(timeout);
|
||||
};
|
||||
}, [ purchaseState ]);
|
||||
|
||||
if(!currentOffer) return null;
|
||||
|
||||
const PurchaseButton = () =>
|
||||
{
|
||||
const priceCredits = (currentOffer.priceInCredits * purchaseOptions.quantity);
|
||||
const pricePoints = (currentOffer.priceInActivityPoints * purchaseOptions.quantity);
|
||||
|
||||
if(GetClubMemberLevel() < currentOffer.clubLevel) return <Button disabled variant="danger">{ LocalizeText('catalog.alert.hc.required') }</Button>;
|
||||
|
||||
if(isLimitedSoldOut) return <Button disabled variant="danger">{ LocalizeText('catalog.alert.limited_edition_sold_out.title') }</Button>;
|
||||
|
||||
if(priceCredits > getCurrencyAmount(-1)) return <Button disabled variant="danger">{ LocalizeText('catalog.alert.notenough.title') }</Button>;
|
||||
|
||||
if(pricePoints > getCurrencyAmount(currentOffer.activityPointType)) return <Button disabled variant="danger">{ LocalizeText('catalog.alert.notenough.activitypoints.title.' + currentOffer.activityPointType) }</Button>;
|
||||
|
||||
switch(purchaseState)
|
||||
{
|
||||
case CatalogPurchaseState.CONFIRM:
|
||||
return <Button variant="warning" onClick={ event => purchase() }>{ LocalizeText('catalog.marketplace.confirm_title') }</Button>;
|
||||
case CatalogPurchaseState.PURCHASE:
|
||||
return <Button disabled><LayoutLoadingSpinnerView /></Button>;
|
||||
case CatalogPurchaseState.FAILED:
|
||||
return <Button variant="danger">{ LocalizeText('generic.failed') }</Button>;
|
||||
case CatalogPurchaseState.SOLD_OUT:
|
||||
return <Button variant="danger">{ LocalizeText('generic.failed') + ' - ' + LocalizeText('catalog.alert.limited_edition_sold_out.title') }</Button>;
|
||||
case CatalogPurchaseState.NONE:
|
||||
default:
|
||||
return <Button disabled={ (purchaseOptions.extraParamRequired && (!purchaseOptions.extraData || !purchaseOptions.extraData.length)) } onClick={ event => setPurchaseState(CatalogPurchaseState.CONFIRM) }>{ LocalizeText('catalog.purchase_confirmation.' + (currentOffer.isRentOffer ? 'rent' : 'buy')) }</Button>;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<PurchaseButton />
|
||||
{ (!noGiftOption && !currentOffer.isRentOffer) &&
|
||||
<Button disabled={ ((purchaseOptions.quantity > 1) || !currentOffer.giftable || isLimitedSoldOut || (purchaseOptions.extraParamRequired && (!purchaseOptions.extraData || !purchaseOptions.extraData.length))) } onClick={ event => purchase(true) }>
|
||||
{ LocalizeText('catalog.purchase_confirmation.gift') }
|
||||
</Button> }
|
||||
</>
|
||||
);
|
||||
};
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
import { FC } from 'react';
|
||||
import { useCatalog } from '../../../../../hooks';
|
||||
import { CatalogPriceDisplayWidgetView } from './CatalogPriceDisplayWidgetView';
|
||||
|
||||
export const CatalogSimplePriceWidgetView: FC<{}> = props =>
|
||||
{
|
||||
const { currentOffer = null } = useCatalog();
|
||||
|
||||
return (
|
||||
<div className="flex items-center bg-muted p-1 rounded gap-1">
|
||||
<CatalogPriceDisplayWidgetView offer={ currentOffer } separator={ true } />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
import { FC } from 'react';
|
||||
import { CatalogFirstProductSelectorWidgetView } from './CatalogFirstProductSelectorWidgetView';
|
||||
|
||||
export const CatalogSingleViewWidgetView: FC<{}> = props =>
|
||||
{
|
||||
return <CatalogFirstProductSelectorWidgetView />;
|
||||
};
|
||||
@@ -0,0 +1,115 @@
|
||||
import { FC, useEffect, useRef, useState } from 'react';
|
||||
import { IPurchasableOffer, LocalizeText, Offer, ProductTypeEnum } from '../../../../../api';
|
||||
import { AutoGrid, AutoGridProps, Button } from '../../../../../common';
|
||||
import { useCatalog } from '../../../../../hooks';
|
||||
import { CatalogGridOfferView } from '../common/CatalogGridOfferView';
|
||||
|
||||
interface CatalogSpacesWidgetViewProps extends AutoGridProps
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
const SPACES_GROUP_NAMES = [ 'floors', 'walls', 'views' ];
|
||||
|
||||
export const CatalogSpacesWidgetView: FC<CatalogSpacesWidgetViewProps> = props =>
|
||||
{
|
||||
const { columnCount = 5, children = null, ...rest } = props;
|
||||
const [ groupedOffers, setGroupedOffers ] = useState<IPurchasableOffer[][]>(null);
|
||||
const [ selectedGroupIndex, setSelectedGroupIndex ] = useState(-1);
|
||||
const [ selectedOfferForGroup, setSelectedOfferForGroup ] = useState<IPurchasableOffer[]>(null);
|
||||
const { currentPage = null, currentOffer = null, setCurrentOffer = null, setPurchaseOptions = null } = useCatalog();
|
||||
const elementRef = useRef<HTMLDivElement>();
|
||||
|
||||
const setSelectedOffer = (offer: IPurchasableOffer) =>
|
||||
{
|
||||
if(!offer) return;
|
||||
|
||||
setSelectedOfferForGroup(prevValue =>
|
||||
{
|
||||
const newValue = [ ...prevValue ];
|
||||
|
||||
newValue[selectedGroupIndex] = offer;
|
||||
|
||||
return newValue;
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
if(!currentPage) return;
|
||||
|
||||
const groupedOffers: IPurchasableOffer[][] = [ [], [], [] ];
|
||||
|
||||
for(const offer of currentPage.offers)
|
||||
{
|
||||
if((offer.pricingModel !== Offer.PRICING_MODEL_SINGLE) && (offer.pricingModel !== Offer.PRICING_MODEL_MULTI)) continue;
|
||||
|
||||
const product = offer.product;
|
||||
|
||||
if(!product || ((product.productType !== ProductTypeEnum.WALL) && (product.productType !== ProductTypeEnum.FLOOR)) || !product.furnitureData) continue;
|
||||
|
||||
const className = product.furnitureData.className;
|
||||
|
||||
switch(className)
|
||||
{
|
||||
case 'floor':
|
||||
groupedOffers[0].push(offer);
|
||||
break;
|
||||
case 'wallpaper':
|
||||
groupedOffers[1].push(offer);
|
||||
break;
|
||||
case 'landscape':
|
||||
groupedOffers[2].push(offer);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
setGroupedOffers(groupedOffers);
|
||||
setSelectedGroupIndex(0);
|
||||
setSelectedOfferForGroup([ groupedOffers[0][0], groupedOffers[1][0], groupedOffers[2][0] ]);
|
||||
}, [ currentPage ]);
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
if((selectedGroupIndex === -1) || !selectedOfferForGroup) return;
|
||||
|
||||
setCurrentOffer(selectedOfferForGroup[selectedGroupIndex]);
|
||||
|
||||
}, [ selectedGroupIndex, selectedOfferForGroup, setCurrentOffer ]);
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
if((selectedGroupIndex === -1) || !selectedOfferForGroup || !currentOffer) return;
|
||||
|
||||
setPurchaseOptions(prevValue =>
|
||||
{
|
||||
const newValue = { ...prevValue };
|
||||
|
||||
newValue.extraData = selectedOfferForGroup[selectedGroupIndex].product.extraParam;
|
||||
newValue.extraParamRequired = true;
|
||||
|
||||
return newValue;
|
||||
});
|
||||
}, [ currentOffer, selectedGroupIndex, selectedOfferForGroup, setPurchaseOptions ]);
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
if(elementRef && elementRef.current) elementRef.current.scrollTop = 0;
|
||||
}, [ selectedGroupIndex ]);
|
||||
|
||||
if(!groupedOffers || (selectedGroupIndex === -1)) return null;
|
||||
|
||||
const offers = groupedOffers[selectedGroupIndex];
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="relative inline-flex align-middle">
|
||||
{ SPACES_GROUP_NAMES.map((name, index) => <Button key={ index } active={ (selectedGroupIndex === index) } onClick={ event => setSelectedGroupIndex(index) }>{ LocalizeText(`catalog.spaces.tab.${ name }`) }</Button>) }
|
||||
</div>
|
||||
<AutoGrid columnCount={ columnCount } innerRef={ elementRef } { ...rest }>
|
||||
{ offers && (offers.length > 0) && offers.map((offer, index) => <CatalogGridOfferView key={ index } itemActive={ (currentOffer && (currentOffer === offer)) } offer={ offer } selectOffer={ offer => setSelectedOffer(offer) } />) }
|
||||
{ children }
|
||||
</AutoGrid>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,46 @@
|
||||
import { FC } from 'react';
|
||||
import { FaCaretLeft, FaCaretRight } from 'react-icons/fa';
|
||||
import { LocalizeText } from '../../../../../api';
|
||||
import { Text } from '../../../../../common';
|
||||
import { useCatalog } from '../../../../../hooks';
|
||||
|
||||
const MIN_VALUE: number = 1;
|
||||
const MAX_VALUE: number = 100;
|
||||
|
||||
export const CatalogSpinnerWidgetView: FC<{}> = props =>
|
||||
{
|
||||
const { currentOffer = null, purchaseOptions = null, setPurchaseOptions = null } = useCatalog();
|
||||
const { quantity = 1 } = purchaseOptions;
|
||||
|
||||
const updateQuantity = (value: number) =>
|
||||
{
|
||||
if(isNaN(value)) value = 1;
|
||||
|
||||
value = Math.max(value, MIN_VALUE);
|
||||
value = Math.min(value, MAX_VALUE);
|
||||
|
||||
if(value === quantity) return;
|
||||
|
||||
setPurchaseOptions(prevValue =>
|
||||
{
|
||||
const newValue = { ...prevValue };
|
||||
|
||||
newValue.quantity = value;
|
||||
|
||||
return newValue;
|
||||
});
|
||||
};
|
||||
|
||||
if(!currentOffer || !currentOffer.bundlePurchaseAllowed) return null;
|
||||
|
||||
return (
|
||||
<>
|
||||
<Text>{ LocalizeText('catalog.bundlewidget.spinner.select.amount') }</Text>
|
||||
<div className="flex items-center gap-1">
|
||||
<FaCaretLeft className="text-black cursor-pointer fa-icon" onClick={ event => updateQuantity(quantity - 1) } />
|
||||
<input className="[appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none min-h-[17px] h-[17px] w-[28px] px-[4px] py-[0] text-right rounded-[.2rem]" type="number" value={ quantity } onChange={ event => updateQuantity(event.target.valueAsNumber) } />
|
||||
<FaCaretRight className="text-black cursor-pointer fa-icon" onClick={ event => updateQuantity(quantity + 1) } />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,20 @@
|
||||
import { FC } from 'react';
|
||||
import { Column, ColumnProps } from '../../../../../common';
|
||||
import { useCatalog } from '../../../../../hooks';
|
||||
import { CatalogPriceDisplayWidgetView } from './CatalogPriceDisplayWidgetView';
|
||||
|
||||
interface CatalogSimplePriceWidgetViewProps extends ColumnProps
|
||||
{
|
||||
|
||||
}
|
||||
export const CatalogTotalPriceWidget: FC<CatalogSimplePriceWidgetViewProps> = props =>
|
||||
{
|
||||
const { gap = 1, ...rest } = props;
|
||||
const { currentOffer = null } = useCatalog();
|
||||
|
||||
return (
|
||||
<Column gap={ gap } { ...rest }>
|
||||
<CatalogPriceDisplayWidgetView offer={ currentOffer } />
|
||||
</Column>
|
||||
);
|
||||
};
|
||||
+99
@@ -0,0 +1,99 @@
|
||||
import { GetAvatarRenderManager, GetSessionDataManager, Vector3d } from '@nitrots/nitro-renderer';
|
||||
import { FC, useEffect } from 'react';
|
||||
import { FurniCategory, Offer, ProductTypeEnum } from '../../../../../api';
|
||||
import { AutoGrid, Column, LayoutGridItem, LayoutRoomPreviewerView } from '../../../../../common';
|
||||
import { useCatalog } from '../../../../../hooks';
|
||||
|
||||
export const CatalogViewProductWidgetView: FC<{}> = props =>
|
||||
{
|
||||
const { currentOffer = null, roomPreviewer = null, purchaseOptions = null } = useCatalog();
|
||||
const { previewStuffData = null } = purchaseOptions;
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
if(!currentOffer || (currentOffer.pricingModel === Offer.PRICING_MODEL_BUNDLE) || !roomPreviewer) return;
|
||||
|
||||
const product = currentOffer.product;
|
||||
|
||||
if(!product) return;
|
||||
|
||||
roomPreviewer.reset(false);
|
||||
|
||||
switch(product.productType)
|
||||
{
|
||||
case ProductTypeEnum.FLOOR: {
|
||||
if(!product.furnitureData) return;
|
||||
|
||||
if(product.furnitureData.specialType === FurniCategory.FIGURE_PURCHASABLE_SET)
|
||||
{
|
||||
const furniData = GetSessionDataManager().getFloorItemData(product.furnitureData.id);
|
||||
const customParts = furniData.customParams.split(',').map(value => parseInt(value));
|
||||
const figureSets: number[] = [];
|
||||
|
||||
for(const part of customParts)
|
||||
{
|
||||
if(GetAvatarRenderManager().isValidFigureSetForGender(part, GetSessionDataManager().gender)) figureSets.push(part);
|
||||
}
|
||||
|
||||
const figureString = GetAvatarRenderManager().getFigureStringWithFigureIds(GetSessionDataManager().figure, GetSessionDataManager().gender, figureSets);
|
||||
|
||||
roomPreviewer.addAvatarIntoRoom(figureString, product.productClassId);
|
||||
}
|
||||
else
|
||||
{
|
||||
roomPreviewer.addFurnitureIntoRoom(product.productClassId, new Vector3d(90), previewStuffData, product.extraParam);
|
||||
}
|
||||
return;
|
||||
}
|
||||
case ProductTypeEnum.WALL: {
|
||||
if(!product.furnitureData) return;
|
||||
|
||||
switch(product.furnitureData.specialType)
|
||||
{
|
||||
case FurniCategory.FLOOR:
|
||||
roomPreviewer.updateObjectRoom(product.extraParam);
|
||||
return;
|
||||
case FurniCategory.WALL_PAPER:
|
||||
roomPreviewer.updateObjectRoom(null, product.extraParam);
|
||||
return;
|
||||
case FurniCategory.LANDSCAPE: {
|
||||
roomPreviewer.updateObjectRoom(null, null, product.extraParam);
|
||||
|
||||
const furniData = GetSessionDataManager().getWallItemDataByName('window_double_default');
|
||||
|
||||
if(furniData) roomPreviewer.addWallItemIntoRoom(furniData.id, new Vector3d(90), furniData.customParams);
|
||||
return;
|
||||
}
|
||||
default:
|
||||
roomPreviewer.updateObjectRoom('default', 'default', 'default');
|
||||
roomPreviewer.addWallItemIntoRoom(product.productClassId, new Vector3d(90), product.extraParam);
|
||||
return;
|
||||
}
|
||||
}
|
||||
case ProductTypeEnum.ROBOT:
|
||||
roomPreviewer.addAvatarIntoRoom(product.extraParam, 0);
|
||||
return;
|
||||
case ProductTypeEnum.EFFECT:
|
||||
roomPreviewer.addAvatarIntoRoom(GetSessionDataManager().figure, product.productClassId);
|
||||
return;
|
||||
}
|
||||
}, [ currentOffer, previewStuffData, roomPreviewer ]);
|
||||
|
||||
if(!currentOffer) return null;
|
||||
|
||||
if(currentOffer.pricingModel === Offer.PRICING_MODEL_BUNDLE)
|
||||
{
|
||||
return (
|
||||
<Column fit className="bg-muted p-2 rounded" overflow="hidden">
|
||||
<AutoGrid fullWidth className="nitro-catalog-layout-bundle-grid" columnCount={ 4 }>
|
||||
{ (currentOffer.products.length > 0) && currentOffer.products.map((product, index) =>
|
||||
{
|
||||
return <LayoutGridItem key={ index } itemCount={ product.productCount } itemImage={ product.getIconUrl(currentOffer) } />;
|
||||
}) }
|
||||
</AutoGrid>
|
||||
</Column>
|
||||
);
|
||||
}
|
||||
|
||||
return <LayoutRoomPreviewerView height={ 140 } roomPreviewer={ roomPreviewer } />;
|
||||
};
|
||||
@@ -0,0 +1,16 @@
|
||||
import { TargetedOfferData } from '@nitrots/nitro-renderer';
|
||||
import { Dispatch, SetStateAction } from 'react';
|
||||
import { GetConfigurationValue } from '../../../../api';
|
||||
import { LayoutNotificationBubbleView, Text } from '../../../../common';
|
||||
|
||||
export const OfferBubbleView = (props: { offer: TargetedOfferData, setOpen: Dispatch<SetStateAction<boolean>> }) =>
|
||||
{
|
||||
const { offer = null, setOpen = null } = props;
|
||||
|
||||
if(!offer) return;
|
||||
|
||||
return <LayoutNotificationBubbleView fadesOut={ false } gap={ 2 } onClick={ evt => setOpen(true) } onClose={ null }>
|
||||
<div className="nitro-targeted-offer-icon" style={ { backgroundImage: `url(${ GetConfigurationValue('image.library.url') + offer.iconImageUrl })` } }/>
|
||||
<Text className="ubuntu-bold" variant="light">{ offer.title }</Text>
|
||||
</LayoutNotificationBubbleView>;
|
||||
};
|
||||
@@ -0,0 +1,32 @@
|
||||
import { GetTargetedOfferComposer, TargetedOfferData, TargetedOfferEvent } from '@nitrots/nitro-renderer';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { SendMessageComposer } from '../../../../api';
|
||||
import { useMessageEvent } from '../../../../hooks';
|
||||
import { OfferBubbleView } from './OfferBubbleView';
|
||||
import { OfferWindowView } from './OfferWindowView';
|
||||
|
||||
export const OfferView = () =>
|
||||
{
|
||||
const [ offer, setOffer ] = useState<TargetedOfferData>(null);
|
||||
const [ opened, setOpened ] = useState<boolean>(false);
|
||||
|
||||
useMessageEvent<TargetedOfferEvent>(TargetedOfferEvent, evt =>
|
||||
{
|
||||
let parser = evt.getParser();
|
||||
|
||||
if(!parser) return;
|
||||
|
||||
setOffer(parser.data);
|
||||
});
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
SendMessageComposer(new GetTargetedOfferComposer());
|
||||
}, []);
|
||||
|
||||
if(!offer) return;
|
||||
|
||||
return <>
|
||||
{ opened ? <OfferWindowView offer={ offer } setOpen={ setOpened } /> : <OfferBubbleView offer={ offer } setOpen={ setOpened } /> }
|
||||
</>;
|
||||
};
|
||||
@@ -0,0 +1,85 @@
|
||||
import { GetTargetedOfferComposer, PurchaseTargetedOfferComposer, TargetedOfferData } from '@nitrots/nitro-renderer';
|
||||
import { Dispatch, SetStateAction, useMemo, useState } from 'react';
|
||||
import { FriendlyTime, GetConfigurationValue, LocalizeText, SendMessageComposer } from '../../../../api';
|
||||
import { Button, Column, Flex, LayoutCurrencyIcon, NitroCardContentView, NitroCardHeaderView, NitroCardView, Text } from '../../../../common';
|
||||
import { usePurse } from '../../../../hooks';
|
||||
|
||||
export const OfferWindowView = (props: { offer: TargetedOfferData, setOpen: Dispatch<SetStateAction<boolean>> }) =>
|
||||
{
|
||||
const { offer = null, setOpen = null } = props;
|
||||
|
||||
const { getCurrencyAmount } = usePurse();
|
||||
|
||||
const [ amount, setAmount ] = useState<number>(1);
|
||||
|
||||
const canPurchase = useMemo(() =>
|
||||
{
|
||||
let credits = false;
|
||||
let points = false;
|
||||
let limit = false;
|
||||
|
||||
if(offer.priceInCredits > 0) credits = getCurrencyAmount(-1) >= offer.priceInCredits;
|
||||
|
||||
if(offer.priceInActivityPoints > 0) points = getCurrencyAmount(offer.activityPointType) >= offer.priceInActivityPoints;
|
||||
else points = true;
|
||||
|
||||
if(offer.purchaseLimit > 0) limit = true;
|
||||
|
||||
return (credits && points && limit);
|
||||
}, [ offer, getCurrencyAmount ]);
|
||||
|
||||
const expirationTime = () =>
|
||||
{
|
||||
let expirationTime = Math.max(0, (offer.expirationTime - Date.now()) / 1000);
|
||||
|
||||
return FriendlyTime.format(expirationTime);
|
||||
};
|
||||
|
||||
const buyOffer = () =>
|
||||
{
|
||||
SendMessageComposer(new PurchaseTargetedOfferComposer(offer.id, amount));
|
||||
SendMessageComposer(new GetTargetedOfferComposer());
|
||||
};
|
||||
|
||||
if(!offer) return;
|
||||
|
||||
return <NitroCardView className="nitro-targeted-offer" theme="primary-slim" uniqueKey="targeted-offer">
|
||||
<NitroCardHeaderView headerText={ LocalizeText(offer.title) } onCloseClick={ event => setOpen(false) } />
|
||||
<div className="container-fluid p-1 relative justify-center items-center cursor-pointer gap-3 bg-danger">
|
||||
{ LocalizeText('targeted.offer.timeleft', [ 'timeleft' ], [ expirationTime() ]) }
|
||||
</div>
|
||||
<NitroCardContentView gap={ 1 }>
|
||||
<Flex fullHeight gap={ 1 }>
|
||||
<Flex column className="w-75 text-black" gap={ 1 }>
|
||||
<Column fullHeight className="bg-warning p-2">
|
||||
<h4>
|
||||
{ LocalizeText(offer.title) }
|
||||
</h4>
|
||||
<div dangerouslySetInnerHTML={ { __html: offer.description } } />
|
||||
</Column>
|
||||
<Flex alignItems="center" alignSelf="center" gap={ 2 } justifyContent="center">
|
||||
{ offer.purchaseLimit > 1 &&
|
||||
<div className="flex gap-1">
|
||||
<Text variant="muted">{ LocalizeText('catalog.bundlewidget.quantity') }</Text>
|
||||
<input max={ offer.purchaseLimit } min={ 1 } type="number" value={ amount } onChange={ evt => setAmount(parseInt(evt.target.value)) } />
|
||||
</div> }
|
||||
<Button disabled={ !canPurchase } variant="primary" onClick={ () => buyOffer() }>{ LocalizeText('targeted.offer.button.buy') }</Button>
|
||||
</Flex>
|
||||
</Flex>
|
||||
<div className="w-50 h-full" style={ { background: `url(${ GetConfigurationValue('image.library.url') + offer.imageUrl }) no-repeat center` } } />
|
||||
</Flex>
|
||||
<Flex column alignItems="center" className="price-ray absolute" justifyContent="center">
|
||||
<Text>{ LocalizeText('targeted.offer.price.label') }</Text>
|
||||
{ offer.priceInCredits > 0 &&
|
||||
<div className="flex gap-1">
|
||||
<Text variant="light">{ offer.priceInCredits }</Text>
|
||||
<LayoutCurrencyIcon type={ -1 } />
|
||||
</div> }
|
||||
{ offer.priceInActivityPoints > 0 &&
|
||||
<div className="flex gap-1">
|
||||
<Text className="ubuntu-bold" variant="light">+{ offer.priceInActivityPoints }</Text> <LayoutCurrencyIcon type={ offer.activityPointType } />
|
||||
</div> }
|
||||
</Flex>
|
||||
</NitroCardContentView>
|
||||
</NitroCardView>;
|
||||
};
|
||||
@@ -0,0 +1,114 @@
|
||||
import { AddLinkEventTracker, ILinkEventTracker, RemoveLinkEventTracker } from '@nitrots/nitro-renderer';
|
||||
import { FC, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { ChatEntryType, LocalizeText } from '../../api';
|
||||
import { Flex, NitroCardContentView, NitroCardHeaderView, NitroCardView, Text } from '../../common';
|
||||
import { useChatHistory } from '../../hooks';
|
||||
import { NitroInput } from '../../layout';
|
||||
|
||||
export const ChatHistoryView: FC<{}> = props => {
|
||||
const [isVisible, setIsVisible] = useState(false);
|
||||
const [searchText, setSearchText] = useState<string>('');
|
||||
const {chatHistory = []} = useChatHistory();
|
||||
const elementRef = useRef<HTMLDivElement>(null);
|
||||
const isFirstRender = useRef(true);
|
||||
const prevChatLength = useRef<number>(0);
|
||||
|
||||
const filteredChatHistory = useMemo(() => {
|
||||
let result = chatHistory;
|
||||
|
||||
if (searchText.length > 0) {
|
||||
const text = searchText.toLowerCase();
|
||||
result = chatHistory.filter(entry =>
|
||||
(entry.message && entry.message.toLowerCase().includes(text)) ||
|
||||
(entry.name && entry.name.toLowerCase().includes(text))
|
||||
);
|
||||
}
|
||||
|
||||
return [...result];
|
||||
}, [chatHistory, searchText]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!elementRef.current || !isVisible) return;
|
||||
|
||||
const element = elementRef.current;
|
||||
const maxScrollTop = Math.max(0, element.scrollHeight - element.clientHeight);
|
||||
const isAtBottom = maxScrollTop === 0 || Math.abs(element.scrollTop - maxScrollTop) <= 50;
|
||||
|
||||
if (isFirstRender.current) {
|
||||
element.scrollTo({ top: element.scrollHeight, behavior: 'smooth' });
|
||||
isFirstRender.current = false;
|
||||
} else if (filteredChatHistory.length > prevChatLength.current) {
|
||||
element.scrollTo({ top: element.scrollHeight, behavior: 'smooth' });
|
||||
}
|
||||
|
||||
prevChatLength.current = filteredChatHistory.length;
|
||||
}, [filteredChatHistory, isVisible]);
|
||||
|
||||
useEffect(() => {
|
||||
const linkTracker: ILinkEventTracker = {
|
||||
linkReceived: (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;
|
||||
}
|
||||
},
|
||||
eventUrlPrefix: 'chat-history/'
|
||||
};
|
||||
|
||||
AddLinkEventTracker(linkTracker);
|
||||
|
||||
return () => RemoveLinkEventTracker(linkTracker);
|
||||
}, []);
|
||||
|
||||
if (!isVisible) return null;
|
||||
|
||||
return (
|
||||
<NitroCardView className="nitro-chat-history" theme="primary-slim" uniqueKey="chat-history">
|
||||
<NitroCardHeaderView headerText={LocalizeText('room.chathistory.button.text')} onCloseClick={event => setIsVisible(false)} />
|
||||
<NitroCardContentView className="nitro-card-content" gap={2} overflow="hidden" style={{ height: 'calc(100% - 40px)', display: 'flex', flexDirection: 'column' }}>
|
||||
<NitroInput placeholder={LocalizeText('generic.search')} type="text" value={searchText} onChange={event => setSearchText(event.target.value)} />
|
||||
<div ref={elementRef} style={{ flex: 1, overflowY: 'auto', background: 'inherit' }}>
|
||||
{filteredChatHistory.map((row, index) => (
|
||||
<Flex key={index} alignItems="center" className="p-1" gap={2}>
|
||||
<Text variant="gray">{row.timestamp}</Text>
|
||||
{row.type === ChatEntryType.TYPE_CHAT && (
|
||||
<div className="bubble-container" style={{position: 'relative', display: 'inline-flex', alignItems: 'center'}}>
|
||||
<div
|
||||
className={`chat-bubble bubble-${row.style} type-${row.chatType}`}
|
||||
style={{ maxWidth: '100%', backgroundColor: row.style === 0 ? row.color : 'transparent', position: 'relative', zIndex: 1 }}>
|
||||
<div className="user-container">
|
||||
{row.imageUrl && row.imageUrl.length > 0 && (
|
||||
<div className="user-image" style={{backgroundImage: `url(${row.imageUrl})`}} />
|
||||
)}
|
||||
</div>
|
||||
<div className="chat-content">
|
||||
<b className="mr-1 username" dangerouslySetInnerHTML={{__html: `${row.name}: `}} />
|
||||
<span className="message" dangerouslySetInnerHTML={{__html: `${row.message}`}} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{row.type === ChatEntryType.TYPE_ROOM_INFO && (
|
||||
<>
|
||||
<i className="nitro-icon icon-small-room" />
|
||||
<Text grow textBreak wrap>{row.name}</Text>
|
||||
</>
|
||||
)}
|
||||
</Flex>
|
||||
))}
|
||||
</div>
|
||||
</NitroCardContentView>
|
||||
</NitroCardView>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,22 @@
|
||||
import { createContext, Dispatch, FC, ProviderProps, SetStateAction, useContext } from 'react';
|
||||
import { IFloorplanSettings } from './common/IFloorplanSettings';
|
||||
import { IVisualizationSettings } from './common/IVisualizationSettings';
|
||||
|
||||
interface IFloorplanEditorContext
|
||||
{
|
||||
originalFloorplanSettings: IFloorplanSettings;
|
||||
setOriginalFloorplanSettings: Dispatch<SetStateAction<IFloorplanSettings>>;
|
||||
visualizationSettings: IVisualizationSettings;
|
||||
setVisualizationSettings: Dispatch<SetStateAction<IVisualizationSettings>>;
|
||||
}
|
||||
|
||||
const FloorplanEditorContext = createContext<IFloorplanEditorContext>({
|
||||
originalFloorplanSettings: null,
|
||||
setOriginalFloorplanSettings: null,
|
||||
visualizationSettings: null,
|
||||
setVisualizationSettings: null
|
||||
});
|
||||
|
||||
export const FloorplanEditorContextProvider: FC<ProviderProps<IFloorplanEditorContext>> = props => <FloorplanEditorContext.Provider { ...props } />;
|
||||
|
||||
export const useFloorplanEditorContext = () => useContext(FloorplanEditorContext);
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user