diff --git a/public/ui-config.json b/public/ui-config.json index 2cd31c79..57d8f948 100644 --- a/public/ui-config.json +++ b/public/ui-config.json @@ -25,8 +25,20 @@ ], "hotelview": { "widgets": { - "types": "news,", - "slot1": "news" + "slot.1.widget": "promoarticle", + "slot.1.conf": "", + "slot.2.widget": "widgetcontainer", + "slot.2.conf": "image:${image.library.url}web_promo_small/spromo_Canal_Bundle.png,texts:2021NitroPromo,btnLink:https://google.com", + "slot.3.widget": "promoarticle", + "slot.3.conf": "", + "slot.4.widget": "", + "slot.4.conf": "", + "slot.5.widget": "", + "slot.5.conf": "", + "slot.6.widget": "achievementcompetition_hall_of_fame", + "slot.6.conf": "", + "slot.7.widget": "", + "slot.7.conf": "" }, "images": { "background": "${asset.url}/images/reception/stretch_blue.png", diff --git a/src/assets/styles/bootstrap/_variables.scss b/src/assets/styles/bootstrap/_variables.scss index fe6cb582..a53ad2b2 100644 --- a/src/assets/styles/bootstrap/_variables.scss +++ b/src/assets/styles/bootstrap/_variables.scss @@ -80,6 +80,7 @@ $cello-light: #21516e !default; $cello-dark: #1e465e !default; $pale-sky: #677181 !default; $oslo-gray: #8F9297 !default; +$gainsboro: #d9d9d9 !default; $ghost: #c8cad0 !default; $gray-chateau: #a3a7b1 !default; $gable-green: #1C323F !default; @@ -125,7 +126,8 @@ $theme-colors: ( "white": $white, "black": $black, "muted": $muted, - "purple": $purple + "purple": $purple, + "gainsboro": $gainsboro ) !default; // scss-docs-end theme-colors-map diff --git a/src/views/hotel-view/HotelView.scss b/src/views/hotel-view/HotelView.scss index 97076eaf..b4671314 100644 --- a/src/views/hotel-view/HotelView.scss +++ b/src/views/hotel-view/HotelView.scss @@ -4,6 +4,7 @@ width: 100%; height: calc(100% - 55px); background: rgba($black, 1); + color:#000; .avatar-image { bottom: 12px; @@ -13,6 +14,7 @@ } .background { + top:0; height: 100%; width: 100%; background-position: left; @@ -73,4 +75,25 @@ background-repeat: no-repeat; background-position: right top; } + + .landing-widgets { + z-index: 9; + position: relative; + + } + + .widget-slot { + z-index: 9; + } + + hr { + background: $black; + box-shadow: 0 1px rgba($white,.5); + opacity: 0.5 + } } + +@import './views/widgets/promo-article/PromoArticleWidgetView.scss'; +@import './views/widgets/bonus-rare/BonusRareWidgetView.scss'; +@import './views/widgets/hall-of-fame/HallOfFameWidgetView.scss'; +@import './views/widgets/widgetcontainer/WidgetContainerView.scss' diff --git a/src/views/hotel-view/HotelView.tsx b/src/views/hotel-view/HotelView.tsx index 53c371db..5424e99c 100644 --- a/src/views/hotel-view/HotelView.tsx +++ b/src/views/hotel-view/HotelView.tsx @@ -1,16 +1,18 @@ import { RoomSessionEvent } from '@nitrots/nitro-renderer'; import { FC, useCallback, useState } from 'react'; -import { GetConfiguration, GetNitroInstance } from '../../api'; +import { GetConfiguration, GetConfigurationManager } from '../../api'; import { useRoomSessionManagerEvent } from '../../hooks/events/nitro/session/room-session-manager-event'; import { HotelViewProps } from './HotelView.types'; +import { WidgetSlotView } from './views/widget-slot/WidgetSlotView'; export const HotelView: FC = props => { - const [ isVisible, setIsVisible ] = useState(true); + const [isVisible, setIsVisible] = useState(true); + const widgetSlotCount = 7; const onRoomSessionEvent = useCallback((event: RoomSessionEvent) => { - switch(event.type) + switch (event.type) { case RoomSessionEvent.CREATED: setIsVisible(false); @@ -24,24 +26,75 @@ export const HotelView: FC = props => useRoomSessionManagerEvent(RoomSessionEvent.CREATED, onRoomSessionEvent); useRoomSessionManagerEvent(RoomSessionEvent.ENDED, onRoomSessionEvent); - if(!isVisible) return null; + if (!isVisible) return null; const backgroundColor = GetConfiguration('hotelview')['images']['background.colour']; - const background = GetNitroInstance().core.configuration.interpolate(GetConfiguration('hotelview')['images']['background']); - const sun = GetNitroInstance().core.configuration.interpolate(GetConfiguration('hotelview')['images']['sun']); - const drape = GetNitroInstance().core.configuration.interpolate(GetConfiguration('hotelview')['images']['drape']); - const left = GetNitroInstance().core.configuration.interpolate(GetConfiguration('hotelview')['images']['left']); - const rightRepeat = GetNitroInstance().core.configuration.interpolate(GetConfiguration('hotelview')['images']['right.repeat']); - const right = GetNitroInstance().core.configuration.interpolate(GetConfiguration('hotelview')['images']['right']); + const background = GetConfigurationManager().interpolate(GetConfiguration('hotelview')['images']['background']); + const sun = GetConfigurationManager().interpolate(GetConfiguration('hotelview')['images']['sun']); + const drape = GetConfigurationManager().interpolate(GetConfiguration('hotelview')['images']['drape']); + const left = GetConfigurationManager().interpolate(GetConfiguration('hotelview')['images']['left']); + const rightRepeat = GetConfigurationManager().interpolate(GetConfiguration('hotelview')['images']['right.repeat']); + const right = GetConfigurationManager().interpolate(GetConfiguration('hotelview')['images']['right']); return ( -
-
-
-
-
-
-
+
+
+
+
+ +
+ + + + +
+ +
+
+ +
+
+
+
+
+
+
+
+
); } diff --git a/src/views/hotel-view/views/widget-slot/WidgetSlotView.tsx b/src/views/hotel-view/views/widget-slot/WidgetSlotView.tsx new file mode 100644 index 00000000..739b34c7 --- /dev/null +++ b/src/views/hotel-view/views/widget-slot/WidgetSlotView.tsx @@ -0,0 +1,14 @@ +import { FC } from 'react'; +import { GetWidgetLayout } from '../widgets/GetWidgetLayout'; +import { WidgetSlotViewProps } from './WidgetSlotView.types'; + +export const WidgetSlotView: FC = props => +{ + const { widgetType = null, widgetSlot = 0, widgetConf = null, className= '', ...rest } = props; + + return ( +
+ +
+ ); +} diff --git a/src/views/hotel-view/views/widget-slot/WidgetSlotView.types.ts b/src/views/hotel-view/views/widget-slot/WidgetSlotView.types.ts new file mode 100644 index 00000000..e74dd1ec --- /dev/null +++ b/src/views/hotel-view/views/widget-slot/WidgetSlotView.types.ts @@ -0,0 +1,8 @@ +import { DetailsHTMLAttributes } from 'react'; + +export interface WidgetSlotViewProps extends DetailsHTMLAttributes +{ + widgetType: string; + widgetSlot: number; + widgetConf: string; +} diff --git a/src/views/hotel-view/views/widgets/GetWidgetLayout.tsx b/src/views/hotel-view/views/widgets/GetWidgetLayout.tsx new file mode 100644 index 00000000..6adf53c3 --- /dev/null +++ b/src/views/hotel-view/views/widgets/GetWidgetLayout.tsx @@ -0,0 +1,23 @@ +import { FC } from 'react'; +import { BonusRareWidgetView } from './bonus-rare/BonusRareWidgetView'; +import { GetWidgetLayoutProps } from './GetWidgetLayout.types'; +import { HallOfFameWidgetView } from './hall-of-fame/HallOfFameWidgetView'; +import { PromoArticleWidgetView } from './promo-article/PromoArticleWidgetView'; +import { WidgetContainerView } from './widgetcontainer/WIdgetContainerView'; + +export const GetWidgetLayout: FC = props => +{ + switch (props.widgetType) + { + case "promoarticle": + return ; + case "achievementcompetition_hall_of_fame": + return ; + case "bonusrare": + return ; + case "widgetcontainer": + return + default: + return null; + } +} diff --git a/src/views/hotel-view/views/widgets/GetWidgetLayout.types.ts b/src/views/hotel-view/views/widgets/GetWidgetLayout.types.ts new file mode 100644 index 00000000..b06d5e7a --- /dev/null +++ b/src/views/hotel-view/views/widgets/GetWidgetLayout.types.ts @@ -0,0 +1,6 @@ +export interface GetWidgetLayoutProps +{ + widgetType: string; + slot: number; + widgetConf: string; +} diff --git a/src/views/hotel-view/views/widgets/bonus-rare/BonusRareWidgetView.scss b/src/views/hotel-view/views/widgets/bonus-rare/BonusRareWidgetView.scss new file mode 100644 index 00000000..26f56d82 --- /dev/null +++ b/src/views/hotel-view/views/widgets/bonus-rare/BonusRareWidgetView.scss @@ -0,0 +1,10 @@ +.bonus-rare { + height: 100px; + justify-content: center; + + .bonus-bar-container { + height: 30px; + width: 300px; + border: 2px ridge #e2e2e2; + } +} diff --git a/src/views/hotel-view/views/widgets/bonus-rare/BonusRareWidgetView.tsx b/src/views/hotel-view/views/widgets/bonus-rare/BonusRareWidgetView.tsx new file mode 100644 index 00000000..a19a9cda --- /dev/null +++ b/src/views/hotel-view/views/widgets/bonus-rare/BonusRareWidgetView.tsx @@ -0,0 +1,41 @@ +import { BonusRareInfoMessageEvent, GetBonusRareInfoMessageComposer } from '@nitrots/nitro-renderer'; +import { FC, useCallback, useEffect, useState } from 'react'; +import { CreateMessageHook, SendMessageHook } from '../../../../../hooks/messages/message-event'; +import { BonusRareWidgetViewProps } from './BonusRareWidgetView.types'; + +export const BonusRareWidgetView: FC = props => +{ + const [ productType, setProductType ] = useState(null); + const [ productClassId, setProductClassId ] = useState(null); + const [ totalCoinsForBonus, setTotalCoinsForBonus ] = useState(null); + const [ coinsStillRequiredToBuy, setCoinsStillRequiredToBuy ] = useState(null); + + const onBonusRareInfoMessageEvent = useCallback((event: BonusRareInfoMessageEvent) => + { + const parser = event.getParser(); + + setProductType(parser.productType); + setProductClassId(parser.productClassId); + setTotalCoinsForBonus(parser.totalCoinsForBonus); + setCoinsStillRequiredToBuy(parser.coinsStillRequiredToBuy); + }, []); + + CreateMessageHook(BonusRareInfoMessageEvent, onBonusRareInfoMessageEvent); + + useEffect(() => + { + SendMessageHook(new GetBonusRareInfoMessageComposer()); + }, []); + + if(!productType) return null; + + return ( +
+ { productType } +
+
{(totalCoinsForBonus - coinsStillRequiredToBuy) + '/' + totalCoinsForBonus}
+
+
+
+ ); +} diff --git a/src/views/hotel-view/views/widgets/bonus-rare/BonusRareWidgetView.types.ts b/src/views/hotel-view/views/widgets/bonus-rare/BonusRareWidgetView.types.ts new file mode 100644 index 00000000..9157b7b2 --- /dev/null +++ b/src/views/hotel-view/views/widgets/bonus-rare/BonusRareWidgetView.types.ts @@ -0,0 +1,2 @@ +export interface BonusRareWidgetViewProps +{} diff --git a/src/views/hotel-view/views/widgets/hall-of-fame-item/HallOfFameItemView.tsx b/src/views/hotel-view/views/widgets/hall-of-fame-item/HallOfFameItemView.tsx new file mode 100644 index 00000000..06c29c03 --- /dev/null +++ b/src/views/hotel-view/views/widgets/hall-of-fame-item/HallOfFameItemView.tsx @@ -0,0 +1,21 @@ +import { FC } from 'react'; +import { LocalizeText } from '../../../../../utils'; +import { AvatarImageView } from '../../../../shared/avatar-image/AvatarImageView'; +import { HallOfFameItemViewProps } from './HallOfFameItemView.types'; + +export const HallOfFameItemView: FC = props => +{ + const { data = null, level = 0 } = props; + + return ( +
+
+
+
{ level }. { data.userName }
+
{ LocalizeText('landing.view.competition.hof.points', [ 'points' ], [ data.currentScore.toString() ])}
+
+
+ +
+ ); +} diff --git a/src/views/hotel-view/views/widgets/hall-of-fame-item/HallOfFameItemView.types.ts b/src/views/hotel-view/views/widgets/hall-of-fame-item/HallOfFameItemView.types.ts new file mode 100644 index 00000000..0e8c1e3a --- /dev/null +++ b/src/views/hotel-view/views/widgets/hall-of-fame-item/HallOfFameItemView.types.ts @@ -0,0 +1,7 @@ +import { HallOfFameEntryData } from '@nitrots/nitro-renderer'; + +export interface HallOfFameItemViewProps +{ + data: HallOfFameEntryData; + level: number; +} diff --git a/src/views/hotel-view/views/widgets/hall-of-fame/HallOfFameWidgetView.scss b/src/views/hotel-view/views/widgets/hall-of-fame/HallOfFameWidgetView.scss new file mode 100644 index 00000000..74fa0189 --- /dev/null +++ b/src/views/hotel-view/views/widgets/hall-of-fame/HallOfFameWidgetView.scss @@ -0,0 +1,61 @@ +.hall-of-fame { + background-color: rgba($black,.3); + border-radius: $border-radius; + display: inline-block; + + .hof-user-container { + display:inline-flex; + height: 100%; + position: relative; + + &:hover { + .hof-tooltip { + display: block; + } + } + + .hof-tooltip { + position: absolute; + display: inline; + z-index: 2; + display: none; + background-color: #1c323f; + border: 2px solid rgba($white, 0.5); + border-radius: $border-radius; + bottom:calc(100% - 20px); + left: 0; + right: 0; + margin: auto; + padding:2px; + color: #fff; + + .hof-tooltip-content { + padding:3px; + background-color: #3d5f6e; + } + + &:after { + content: ""; + position: absolute; + bottom: -7px; + left: 0; + right: 0; + margin: auto; + height: 10px; + width: 10px; + transform: rotate(45deg); + border-color: transparent rgba($white, 0.5) rgba($white, 0.5) transparent; + border-style: solid; + border-width: 5px; + } + } + + .avatar-image { + position:relative; + display:inline; + left:0; + top:0; + z-index: 1; + } + } +} diff --git a/src/views/hotel-view/views/widgets/hall-of-fame/HallOfFameWidgetView.tsx b/src/views/hotel-view/views/widgets/hall-of-fame/HallOfFameWidgetView.tsx new file mode 100644 index 00000000..4a13db2c --- /dev/null +++ b/src/views/hotel-view/views/widgets/hall-of-fame/HallOfFameWidgetView.tsx @@ -0,0 +1,37 @@ +import { CommunityGoalHallOfFameData, CommunityGoalHallOfFameMessageEvent, GetCommunityGoalHallOfFameMessageComposer } from '@nitrots/nitro-renderer'; +import { FC, useCallback, useEffect, useState } from 'react'; +import { CreateMessageHook, SendMessageHook } from '../../../../../hooks/messages/message-event'; +import { HallOfFameItemView } from '../hall-of-fame-item/HallOfFameItemView'; +import { HallOfFameWidgetViewProps } from './HallOfFameWidgetView.types'; + +export const HallOfFameWidgetView: FC = props => +{ + const { slot = -1, conf = '' } = props; + const [ data, setData ] = useState(null); + + const onCommunityGoalHallOfFameMessageEvent = useCallback((event: CommunityGoalHallOfFameMessageEvent) => + { + const parser = event.getParser(); + + setData(parser.data); + }, []); + + CreateMessageHook(CommunityGoalHallOfFameMessageEvent, onCommunityGoalHallOfFameMessageEvent); + + useEffect(() => + { + SendMessageHook(new GetCommunityGoalHallOfFameMessageComposer(conf)); + }, [ conf ]); + + if(!data) return null; + + return ( +
+ { data.hof && (data.hof.length > 0) && data.hof.map((entry, index) => + { + return ; + } + )} +
+ ); +} diff --git a/src/views/hotel-view/views/widgets/hall-of-fame/HallOfFameWidgetView.types.ts b/src/views/hotel-view/views/widgets/hall-of-fame/HallOfFameWidgetView.types.ts new file mode 100644 index 00000000..0f165e26 --- /dev/null +++ b/src/views/hotel-view/views/widgets/hall-of-fame/HallOfFameWidgetView.types.ts @@ -0,0 +1,5 @@ +export interface HallOfFameWidgetViewProps +{ + slot: number; + conf: string; +} diff --git a/src/views/hotel-view/views/widgets/promo-article/PromoArticleWidgetView.scss b/src/views/hotel-view/views/widgets/promo-article/PromoArticleWidgetView.scss new file mode 100644 index 00000000..eed870d2 --- /dev/null +++ b/src/views/hotel-view/views/widgets/promo-article/PromoArticleWidgetView.scss @@ -0,0 +1,27 @@ +.promo-articles { + width: 100%; + height: 100%; + + .promo-articles-bullet { + border-radius: 50%; + background-color: $white; + border: 1px solid $white; + height: 13px; + width: 13px; + margin-right: 3px; + + &.promo-articles-bullet-active { + background: $black; + } + } + + .promo-article { + .promo-article-image { + width: 150px; + height: 150px; + margin-right: 10px; + background-repeat: no-repeat; + background-position: top center; + } + } +} diff --git a/src/views/hotel-view/views/widgets/promo-article/PromoArticleWidgetView.tsx b/src/views/hotel-view/views/widgets/promo-article/PromoArticleWidgetView.tsx new file mode 100644 index 00000000..e2f87ca6 --- /dev/null +++ b/src/views/hotel-view/views/widgets/promo-article/PromoArticleWidgetView.tsx @@ -0,0 +1,55 @@ +import { GetPromoArticlesComposer, PromoArticleData, PromoArticlesMessageEvent } from '@nitrots/nitro-renderer'; +import { FC, useCallback, useEffect, useState } from 'react'; +import { CreateMessageHook, SendMessageHook } from '../../../../../hooks'; +import { LocalizeText } from '../../../../../utils/LocalizeText'; +import { PromoArticleWidgetViewProps } from './PromoArticleWidgetView.types'; + +export const PromoArticleWidgetView: FC = props => +{ + const [articles, setArticles] = useState(null); + const [index, setIndex] = useState(0); + + const handleSelect = (selectedIndex) => + { + setIndex(selectedIndex); + }; + + const onPromoArticlesMessageEvent = useCallback((event: PromoArticlesMessageEvent) => + { + const parser = event.getParser(); + setArticles(parser.articles); + }, []); + + CreateMessageHook(PromoArticlesMessageEvent, onPromoArticlesMessageEvent); + + useEffect(() => + { + SendMessageHook(new GetPromoArticlesComposer()); + }, []); + + if (!articles) return null; + + return ( +
+
+ { LocalizeText('landing.view.promo.article.header') } +
+
+
+ {articles && (articles.length > 0) && articles.map((article, ind) => +
handleSelect(ind)} /> + )} +
+ {articles && articles[index] && +
+
+
+

{articles[index].title}

+ { articles[index].bodyText } + +
+
+ } +
+ ); +} diff --git a/src/views/hotel-view/views/widgets/promo-article/PromoArticleWidgetView.types.ts b/src/views/hotel-view/views/widgets/promo-article/PromoArticleWidgetView.types.ts new file mode 100644 index 00000000..e8a37dfa --- /dev/null +++ b/src/views/hotel-view/views/widgets/promo-article/PromoArticleWidgetView.types.ts @@ -0,0 +1,2 @@ +export interface PromoArticleWidgetViewProps +{} diff --git a/src/views/hotel-view/views/widgets/widget-container/WidgetContainerView.scss b/src/views/hotel-view/views/widgets/widget-container/WidgetContainerView.scss new file mode 100644 index 00000000..131b5603 --- /dev/null +++ b/src/views/hotel-view/views/widgets/widget-container/WidgetContainerView.scss @@ -0,0 +1,9 @@ +.widgetcontainer { + .widgetcontainer-image { + width: 150px; + height: 150px; + margin-right: 10px; + background-repeat: no-repeat; + background-position: top center; + } +} diff --git a/src/views/hotel-view/views/widgets/widget-container/WidgetContainerView.tsx b/src/views/hotel-view/views/widgets/widget-container/WidgetContainerView.tsx new file mode 100644 index 00000000..139c5a9b --- /dev/null +++ b/src/views/hotel-view/views/widgets/widget-container/WidgetContainerView.tsx @@ -0,0 +1,53 @@ +import { FC, useCallback, useMemo } from 'react'; +import { GetConfigurationManager } from '../../../../../api/core'; +import { LocalizeText } from '../../../../../utils/LocalizeText'; +import { WidgetContainerViewProps } from './WidgetContainerView.types'; + +export const WidgetContainerView: FC = props => +{ + const { conf = null } = props; + + const config = useMemo(() => + { + const config = {}; + + if(!conf || !conf.length) return config; + + let options = conf.split(","); + + options.forEach(option => + { + let [ key, value ] = option.split(':'); + + if(key && value) config[key] = value; + }); + + return config; + }, [ conf ]); + + const getOption = useCallback((key: string) => + { + const option = config[key]; + + if(!option) return null; + + switch(key) + { + case 'image': + return GetConfigurationManager().interpolate(option); + } + + return option; + }, [ config ]); + + return ( +
+
+
+

{LocalizeText(`landing.view.${getOption('texts')}.header`)}

+ { LocalizeText(`landing.view.${getOption('texts')}.body`) } + +
+
+ ); +} diff --git a/src/views/hotel-view/views/widgets/widget-container/WidgetContainerView.types.ts b/src/views/hotel-view/views/widgets/widget-container/WidgetContainerView.types.ts new file mode 100644 index 00000000..55f5924f --- /dev/null +++ b/src/views/hotel-view/views/widgets/widget-container/WidgetContainerView.types.ts @@ -0,0 +1,4 @@ +export interface WidgetContainerViewProps +{ + conf: string; +} diff --git a/src/views/shared/avatar-image/AvatarImageView.tsx b/src/views/shared/avatar-image/AvatarImageView.tsx index a2a5ab56..ea9a823d 100644 --- a/src/views/shared/avatar-image/AvatarImageView.tsx +++ b/src/views/shared/avatar-image/AvatarImageView.tsx @@ -1,21 +1,23 @@ import { AvatarScaleType, AvatarSetType } from '@nitrots/nitro-renderer'; -import { FC, useEffect, useState } from 'react'; +import { FC, useEffect, useRef, useState } from 'react'; import { GetAvatarRenderManager } from '../../../api'; import { AvatarImageViewProps } from './AvatarImageView.types'; export const AvatarImageView: FC = props => { const { figure = '', gender = 'M', headOnly = false, direction = 0, scale = 1 } = props; - const [ avatarUrl, setAvatarUrl ] = useState(null); const [ randomValue, setRandomValue ] = useState(-1); + const isDisposed = useRef(false); useEffect(() => { - if(randomValue) {} - const avatarImage = GetAvatarRenderManager().createAvatarImage(figure, AvatarScaleType.LARGE, gender, { - resetFigure: figure => setRandomValue(Math.random()), + resetFigure: figure => { + if(isDisposed.current) return; + + setRandomValue(Math.random()); + }, dispose: () => {}, disposed: false }, null); @@ -35,6 +37,16 @@ export const AvatarImageView: FC = props => avatarImage.dispose(); }, [ figure, gender, direction, headOnly, randomValue ]); + useEffect(() => + { + isDisposed.current = false; + + return () => + { + isDisposed.current = true; + } + }, []); + const url = `url('${ avatarUrl }')`; return
;