Merge branch 'feature/hotelview' into dev

This commit is contained in:
Bill 2021-08-16 01:49:42 -04:00
commit d0d9519fd1
23 changed files with 512 additions and 25 deletions

View File

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

View File

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

View File

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

View File

@ -1,12 +1,14 @@
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<HotelViewProps> = props =>
{
const [isVisible, setIsVisible] = useState(true);
const widgetSlotCount = 7;
const onRoomSessionEvent = useCallback((event: RoomSessionEvent) =>
{
@ -27,15 +29,66 @@ export const HotelView: FC<HotelViewProps> = props =>
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 (
<div className="nitro-hotel-view" style={(backgroundColor && backgroundColor) ? { background: backgroundColor } : {}}>
<div className="container h-100 py-3 overflow-hidden landing-widgets">
<div className="row h-100">
<div className="col-9 h-100 d-flex flex-column">
<WidgetSlotView
widgetSlot={ 1 }
widgetType={GetConfiguration('hotelview')['widgets']['slot.' + 1 + '.widget']}
widgetConf={GetConfiguration('hotelview')['widgets']['slot.' + 1 + '.conf']}
className="col-6"
/>
<div className="col-12 row mx-0">
<WidgetSlotView
widgetSlot={ 2 }
widgetType={GetConfiguration('hotelview')['widgets']['slot.' + 2 + '.widget']}
widgetConf={GetConfiguration('hotelview')['widgets']['slot.' + 2 + '.conf']}
className="col-7"
/>
<WidgetSlotView
widgetSlot={ 3 }
widgetType={GetConfiguration('hotelview')['widgets']['slot.' + 3 + '.widget']}
widgetConf={GetConfiguration('hotelview')['widgets']['slot.' + 3 + '.conf']}
className="col-5"
/>
<WidgetSlotView
widgetSlot={ 4 }
widgetType={GetConfiguration('hotelview')['widgets']['slot.' + 4 + '.widget']}
widgetConf={GetConfiguration('hotelview')['widgets']['slot.' + 4 + '.conf']}
className="col-7"
/>
<WidgetSlotView
widgetSlot={ 5 }
widgetType={GetConfiguration('hotelview')['widgets']['slot.' + 5 + '.widget']}
widgetConf={GetConfiguration('hotelview')['widgets']['slot.' + 5 + '.conf']}
className="col-5"
/>
</div>
<WidgetSlotView
widgetSlot={ 6 }
widgetType={GetConfiguration('hotelview')['widgets']['slot.' + 6 + '.widget']}
widgetConf={GetConfiguration('hotelview')['widgets']['slot.' + 6 + '.conf']}
className="mt-auto"
/>
</div>
<div className="col-3 h-100">
<WidgetSlotView
widgetSlot={ 7 }
widgetType={GetConfiguration('hotelview')['widgets']['slot.' + 7 + '.widget']}
widgetConf={GetConfiguration('hotelview')['widgets']['slot.' + 7 +'.conf']}
/>
</div>
</div>
</div>
<div className="background position-absolute" style={(background && background.length) ? { backgroundImage: `url(${background})` } : {}} />
<div className="sun position-absolute" style={(sun && sun.length) ? { backgroundImage: `url(${sun})` } : {}} />
<div className="drape position-absolute" style={(drape && drape.length) ? { backgroundImage: `url(${drape})` } : {}} />

View File

@ -0,0 +1,14 @@
import { FC } from 'react';
import { GetWidgetLayout } from '../widgets/GetWidgetLayout';
import { WidgetSlotViewProps } from './WidgetSlotView.types';
export const WidgetSlotView: FC<WidgetSlotViewProps> = props =>
{
const { widgetType = null, widgetSlot = 0, widgetConf = null, className= '', ...rest } = props;
return (
<div className={`widget-slot slot-${widgetSlot} ${(className || '')}`} { ...rest }>
<GetWidgetLayout widgetType={widgetType} slot={widgetSlot} widgetConf={widgetConf} />
</div>
);
}

View File

@ -0,0 +1,8 @@
import { DetailsHTMLAttributes } from 'react';
export interface WidgetSlotViewProps extends DetailsHTMLAttributes<HTMLDivElement>
{
widgetType: string;
widgetSlot: number;
widgetConf: string;
}

View File

@ -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<GetWidgetLayoutProps> = props =>
{
switch (props.widgetType)
{
case "promoarticle":
return <PromoArticleWidgetView />;
case "achievementcompetition_hall_of_fame":
return <HallOfFameWidgetView slot={props.slot} conf={props.widgetConf} />;
case "bonusrare":
return <BonusRareWidgetView />;
case "widgetcontainer":
return <WidgetContainerView conf={props.widgetConf} />
default:
return null;
}
}

View File

@ -0,0 +1,6 @@
export interface GetWidgetLayoutProps
{
widgetType: string;
slot: number;
widgetConf: string;
}

View File

@ -0,0 +1,10 @@
.bonus-rare {
height: 100px;
justify-content: center;
.bonus-bar-container {
height: 30px;
width: 300px;
border: 2px ridge #e2e2e2;
}
}

View File

@ -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<BonusRareWidgetViewProps> = props =>
{
const [ productType, setProductType ] = useState<string>(null);
const [ productClassId, setProductClassId ] = useState<number>(null);
const [ totalCoinsForBonus, setTotalCoinsForBonus ] = useState<number>(null);
const [ coinsStillRequiredToBuy, setCoinsStillRequiredToBuy ] = useState<number>(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 (
<div className="bonus-rare widget d-flex">
{ productType }
<div className="bg-light-dark rounded overflow-hidden position-relative bonus-bar-container">
<div className="d-flex justify-content-center align-items-center w-100 h-100 position-absolute small top-0">{(totalCoinsForBonus - coinsStillRequiredToBuy) + '/' + totalCoinsForBonus}</div>
<div className="small bg-info rounded position-absolute top-0 h-100" style={{ width: ((totalCoinsForBonus - coinsStillRequiredToBuy) / totalCoinsForBonus) * 100 + '%' }}></div>
</div>
</div>
);
}

View File

@ -0,0 +1,2 @@
export interface BonusRareWidgetViewProps
{}

View File

@ -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<HallOfFameItemViewProps> = props =>
{
const { data = null, level = 0 } = props;
return (
<div className="hof-user-container cursor-pointer">
<div className="hof-tooltip">
<div className="hof-tooltip-content">
<div className="fw-bold">{ level }. { data.userName }</div>
<div className="muted fst-italic small text-center">{ LocalizeText('landing.view.competition.hof.points', [ 'points' ], [ data.currentScore.toString() ])}</div>
</div>
</div>
<AvatarImageView figure={ data.figure } direction={ 2 } />
</div>
);
}

View File

@ -0,0 +1,7 @@
import { HallOfFameEntryData } from '@nitrots/nitro-renderer';
export interface HallOfFameItemViewProps
{
data: HallOfFameEntryData;
level: number;
}

View File

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

View File

@ -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<HallOfFameWidgetViewProps> = props =>
{
const { slot = -1, conf = '' } = props;
const [ data, setData ] = useState<CommunityGoalHallOfFameData>(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 (
<div className="hall-of-fame widget">
{ data.hof && (data.hof.length > 0) && data.hof.map((entry, index) =>
{
return <HallOfFameItemView data={ entry } level={ (index + 1) } />;
}
)}
</div>
);
}

View File

@ -0,0 +1,5 @@
export interface HallOfFameWidgetViewProps
{
slot: number;
conf: string;
}

View File

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

View File

@ -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<PromoArticleWidgetViewProps> = props =>
{
const [articles, setArticles] = useState<PromoArticleData[]>(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 (
<div className="promo-articles widget mb-2">
<div className="d-flex flex-row align-items-center w-100 mb-1">
<small className="flex-shrink-0 pe-1">{ LocalizeText('landing.view.promo.article.header') }</small>
<hr className="w-100 my-0"/>
</div>
<div className="d-flex flex-row mb-1">
{articles && (articles.length > 0) && articles.map((article, ind) =>
<div className={`promo-articles-bullet cursor-pointer ` + (article === articles[index] ? 'promo-articles-bullet-active' : '')} key={article.id} onClick={event => handleSelect(ind)} />
)}
</div>
{articles && articles[index] &&
<div className="promo-article d-flex flex-row row mx-0">
<div className="promo-article-image" style={ {backgroundImage: `url(${articles[index].imageUrl})`} }/>
<div className="col-3 d-flex flex-column h-100">
<h3 className="my-0">{articles[index].title}</h3>
<b>{ articles[index].bodyText }</b>
<button className="btn btn-sm mt-auto btn-gainsboro">{ articles[index].buttonText }</button>
</div>
</div>
}
</div>
);
}

View File

@ -0,0 +1,2 @@
export interface PromoArticleWidgetViewProps
{}

View File

@ -0,0 +1,9 @@
.widgetcontainer {
.widgetcontainer-image {
width: 150px;
height: 150px;
margin-right: 10px;
background-repeat: no-repeat;
background-position: top center;
}
}

View File

@ -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<WidgetContainerViewProps> = 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 (
<div className="widgetcontainer widget d-flex flex-row overflow-hidden">
<div className="widgetcontainer-image flex-shrink-0" style={{ backgroundImage: `url(${getOption('image')})` }} />
<div className="d-flex flex-column align-self-center">
<h3 className="my-0">{LocalizeText(`landing.view.${getOption('texts')}.header`)}</h3>
<i>{ LocalizeText(`landing.view.${getOption('texts')}.body`) }</i>
<button className="btn btn-sm btn-gainsboro align-self-start px-3 mt-auto">{ LocalizeText(`landing.view.${getOption('texts')}.button`) }</button>
</div>
</div>
);
}

View File

@ -0,0 +1,4 @@
export interface WidgetContainerViewProps
{
conf: string;
}

View File

@ -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<AvatarImageViewProps> = props =>
{
const { figure = '', gender = 'M', headOnly = false, direction = 0, scale = 1 } = props;
const [ avatarUrl, setAvatarUrl ] = useState<string>(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<AvatarImageViewProps> = props =>
avatarImage.dispose();
}, [ figure, gender, direction, headOnly, randomValue ]);
useEffect(() =>
{
isDisposed.current = false;
return () =>
{
isDisposed.current = true;
}
}, []);
const url = `url('${ avatarUrl }')`;
return <div className={ 'avatar-image scale-' + scale } style={ (avatarUrl && url.length) ? { backgroundImage: url } : {} }></div>;