🆙 Redesign HotelView

This commit is contained in:
duckietm 2025-03-13 16:27:58 +01:00
parent 26e1b94abd
commit e5c9759823
10 changed files with 123 additions and 346 deletions

View File

@ -1,106 +1,89 @@
import { GetConfiguration, RoomSessionEvent } from '@nitrots/nitro-renderer';
import { FC, useState } from 'react';
import { GetConfiguration } from '@nitrots/nitro-renderer';
import { FC, useRef, useState } from 'react';
import { GetConfigurationValue } from '../../api';
import { LayoutAvatarImageView } from '../../common';
import { useNitroEvent, useSessionInfo } from '../../hooks';
import { WidgetSlotView } from './views/widgets/WidgetSlotView';
const widgetSlotCount = 7;
export const HotelView: FC<{}> = props =>
{
const [ isVisible, setIsVisible ] = useState(true);
const { userFigure = null } = useSessionInfo();
useNitroEvent<RoomSessionEvent>([
RoomSessionEvent.CREATED,
RoomSessionEvent.ENDED ], event =>
{
switch(event.type)
{
case RoomSessionEvent.CREATED:
setIsVisible(false);
return;
case RoomSessionEvent.ENDED:
setIsVisible(event.openLandingView);
return;
}
});
if(!isVisible) return null;
import { RoomWidgetView } from './RoomWidgetView';
export const HotelView: FC<{}> = props => {
const backgroundColor = GetConfigurationValue('hotelview')['images']['background.colour'];
const background = GetConfiguration().interpolate(GetConfigurationValue('hotelview')['images']['background']);
const sun = GetConfiguration().interpolate(GetConfigurationValue('hotelview')['images']['sun']);
const drape = GetConfiguration().interpolate(GetConfigurationValue('hotelview')['images']['drape']);
const left = GetConfiguration().interpolate(GetConfigurationValue('hotelview')['images']['left']);
const rightRepeat = GetConfiguration().interpolate(GetConfigurationValue('hotelview')['images']['right.repeat']);
const right = GetConfiguration().interpolate(GetConfigurationValue('hotelview')['images']['right']);
console.log('Background color:', backgroundColor);
const containerRef = useRef<HTMLDivElement>(null);
const [isDragging, setIsDragging] = useState(false);
const [startX, setStartX] = useState(0);
const [startY, setStartY] = useState(0);
const [scrollLeft, setScrollLeft] = useState(0);
const [scrollTop, setScrollTop] = useState(0);
const handleMouseDown = (e: React.MouseEvent<HTMLDivElement>) => {
if (e.button !== 0) return; // Only left mouse button
setIsDragging(true);
setStartX(e.pageX + scrollLeft);
setStartY(e.pageY + scrollTop);
if (containerRef.current) {
containerRef.current.style.cursor = 'grabbing';
}
};
const handleMouseMove = (e: React.MouseEvent<HTMLDivElement>) => {
if (!isDragging) return;
e.preventDefault();
const x = e.pageX;
const y = e.pageY;
const newScrollLeft = startX - x;
const newScrollTop = startY - y;
if (containerRef.current) {
containerRef.current.scrollLeft = newScrollLeft;
containerRef.current.scrollTop = newScrollTop;
setScrollLeft(newScrollLeft);
setScrollTop(newScrollTop);
}
};
const handleMouseUp = () => {
setIsDragging(false);
if (containerRef.current) {
containerRef.current.style.cursor = 'grab';
}
};
const handleMouseLeave = () => {
setIsDragging(false);
if (containerRef.current) {
containerRef.current.style.cursor = 'grab';
}
};
return (
<div className="block fixed w-full h-[calc(100%-55px)] bg-[black] text-[#000]" style={ (backgroundColor && backgroundColor) ? { background: backgroundColor } : {} }>
<div className="container h-full py-3 overflow-hidden z-10 relative">
<div className="flex flex-wrap h-full justify-center">
<div className="grid-rows-3 h-full grid w-min">
<WidgetSlotView
className="grid grid-cols-2 gap-12 "
widgetConf={ GetConfigurationValue('hotelview')['widgets']['slot.' + 1 + '.conf'] }
widgetSlot={ 1 }
widgetType={ GetConfigurationValue('hotelview')['widgets']['slot.' + 1 + '.widget'] }
/>
<div className="grid grid-cols-12">
<WidgetSlotView
className="col-span-7"
widgetConf={ GetConfigurationValue('hotelview')['widgets']['slot.' + 2 + '.conf'] }
widgetSlot={ 2 }
widgetType={ GetConfigurationValue('hotelview')['widgets']['slot.' + 2 + '.widget'] }
/>
<WidgetSlotView
className="col-span-5"
widgetConf={ GetConfigurationValue('hotelview')['widgets']['slot.' + 3 + '.conf'] }
widgetSlot={ 3 }
widgetType={ GetConfigurationValue('hotelview')['widgets']['slot.' + 3 + '.widget'] }
/>
<WidgetSlotView
className="col-span-7"
widgetConf={ GetConfigurationValue('hotelview')['widgets']['slot.' + 4 + '.conf'] }
widgetSlot={ 4 }
widgetType={ GetConfigurationValue('hotelview')['widgets']['slot.' + 4 + '.widget'] }
/>
<WidgetSlotView
className="col-span-5"
widgetConf={ GetConfigurationValue('hotelview')['widgets']['slot.' + 5 + '.conf'] }
widgetSlot={ 5 }
widgetType={ GetConfigurationValue('hotelview')['widgets']['slot.' + 5 + '.widget'] }
/>
</div>
<WidgetSlotView
className="mt-auto"
widgetConf={ GetConfigurationValue('hotelview')['widgets']['slot.' + 6 + '.conf'] }
widgetSlot={ 6 }
widgetType={ GetConfigurationValue('hotelview')['widgets']['slot.' + 6 + '.widget'] }
/>
</div>
<div className="col-span-3 h-full">
<WidgetSlotView
widgetConf={ GetConfigurationValue('hotelview')['widgets']['slot.' + 7 + '.conf'] }
widgetSlot={ 7 }
widgetType={ GetConfigurationValue('hotelview')['widgets']['slot.' + 7 + '.widget'] }
/>
</div>
</div>
<div
ref={containerRef}
className="nitro-hotel-view block fixed w-full h-[calc(100%-55px)] text-[#000]"
style={{
...(backgroundColor ? { background: backgroundColor } : {}),
overflow: 'auto',
WebkitOverflowScrolling: 'touch',
maxWidth: '100vw',
maxHeight: '100vh',
msOverflowStyle: 'none', // IE and Edge
scrollbarWidth: 'none', // Firefox
'::-webkit-scrollbar': { display: 'none' }, // Chrome, Safari, and newer Edge
cursor: 'grab' // Initial cursor state
}}
onMouseDown={handleMouseDown}
onMouseMove={handleMouseMove}
onMouseUp={handleMouseUp}
onMouseLeave={handleMouseLeave}
>
<div
className="hotelview position-relative"
style={{
minWidth: '2600px', // 3000px width - 400px left margin
minHeight: '1425px' // 1185px height + 240px top margin
}}
>
<div className="hotelview-background w-full h-full" style={{ position: 'absolute', top: 0, left: 0 }} />
<RoomWidgetView />
</div>
<div className="background absolute top-[0] h-full w-full bg-left bg-repeat-y" style={ (background && background.length) ? { backgroundImage: `url(${ background })` } : {} } />
<div className="sun absolute w-full h-full top-[0] left-[0] right-[0] m-auto bg-no-repeat bg-[top_center]" style={ (sun && sun.length) ? { backgroundImage: `url(${ sun })` } : {} } />
<div className="drape absolute w-full h-full left-[0] top-[0] [animation-iteration-count:1] [animation-name:slideDown] [animation-duration:1s] bg-no-repeat" style={ (drape && drape.length) ? { backgroundImage: `url(${ drape })` } : {} } />
<div className="left absolute top-[0] right-[0] bottom-[0] left-[0] [animation-iteration-count:1] [animation-name:slideUp] [animation-duration:1s] bg-no-repeat bg-left-bottom" style={ (left && left.length) ? { backgroundImage: `url(${ left })` } : {} } />
<div className="right-repeat absolute w-full h-full right-[0] top-[0] bg-no-repeat bg-right-top" style={ (rightRepeat && rightRepeat.length) ? { backgroundImage: `url(${ rightRepeat })` } : {} } />
<div className="right absolute w-full h-full right-[0] bottom-[0] [animation-iteration-count:1] [animation-name:slideUp] [animation-duration:1s] bg-no-repeat bg-right-bottom" style={ (right && right.length) ? { backgroundImage: `url(${ right })` } : {} } />
{ GetConfigurationValue('hotelview')['show.avatar'] && (
<div>
<LayoutAvatarImageView direction={ 2 } figure={ userFigure } />
</div>
) }
</div>
);
};

View File

@ -0,0 +1,42 @@
import { CreateLinkEvent } from '@nitrots/nitro-renderer';
import { FC } from 'react';
import { GetConfigurationValue } from '../../api';
import { Base } from '../../common';
export interface RoomWidgetViewProps {}
export const RoomWidgetView: FC<RoomWidgetViewProps> = props => {
const poolId = GetConfigurationValue<string>('hotelview')['room.pool'];
const picnicId = GetConfigurationValue<string>('hotelview')['room.picnic'];
const rooftopId = GetConfigurationValue<string>('hotelview')['room.rooftop'];
const rooftopPoolId = GetConfigurationValue<string>('hotelview')['room.rooftop.pool'];
const peacefulId = GetConfigurationValue<string>('hotelview')['room.peaceful'];
const infobusId = GetConfigurationValue<string>('hotelview')['room.infobus'];
const lobbyId = GetConfigurationValue<string>('hotelview')['room.lobby'];
return (
<>
<Base className="nitro-hotel-view-rooftop position-absolute" onClick={event => CreateLinkEvent('navigator/goto/' + rooftopId)}>
<i className="arrow" />
</Base>
<Base className="nitro-hotel-view-rooftop-pool position-absolute" onClick={event => CreateLinkEvent('navigator/goto/' + rooftopPoolId)}>
<i className="arrow" />
</Base>
<Base className="nitro-hotel-view-picnic position-absolute" onClick={event => CreateLinkEvent('navigator/goto/' + picnicId)}>
<i className="arrow" />
</Base>
<Base className="nitro-hotel-view-infobus position-absolute" onClick={event => CreateLinkEvent('navigator/goto/' + infobusId)}>
<i className="arrow-infobus" />
</Base>
<Base className="nitro-hotel-view-pool position-absolute" onClick={event => CreateLinkEvent('navigator/goto/' + poolId)}>
<i className="arrow-pool" />
</Base>
<Base className="nitro-hotel-view-lobby position-absolute" onClick={event => CreateLinkEvent('navigator/goto/' + lobbyId)}>
<i className="arrow" />
</Base>
<Base className="nitro-hotel-view-peaceful position-absolute" onClick={event => CreateLinkEvent('navigator/goto/' + peacefulId)}>
<i className="arrow-peaceful" />
</Base>
</>
);
};

View File

@ -1,29 +0,0 @@
import { FC } from 'react';
import { BonusRareWidgetView } from './bonus-rare/BonusRareWidgetView';
import { HallOfFameWidgetView } from './hall-of-fame/HallOfFameWidgetView';
import { PromoArticleWidgetView } from './promo-article/PromoArticleWidgetView';
import { WidgetContainerView } from './widget-container/WidgetContainerView';
export interface GetWidgetLayoutProps
{
widgetType: string;
slot: number;
widgetConf: any;
}
export const GetWidgetLayout: FC<GetWidgetLayoutProps> = props =>
{
switch(props.widgetType)
{
case 'promoarticle':
return <PromoArticleWidgetView />;
case 'achievementcompetition_hall_of_fame':
return <HallOfFameWidgetView conf={ props.widgetConf } slot={ props.slot } />;
case 'bonusrare':
return <BonusRareWidgetView />;
case 'widgetcontainer':
return <WidgetContainerView conf={ props.widgetConf } />;
default:
return null;
}
};

View File

@ -1,20 +0,0 @@
import { DetailsHTMLAttributes, FC } from 'react';
import { GetWidgetLayout } from './GetWidgetLayout';
export interface WidgetSlotViewProps extends DetailsHTMLAttributes<HTMLDivElement>
{
widgetType: string;
widgetSlot: number;
widgetConf: any;
}
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 slot={ widgetSlot } widgetConf={ widgetConf } widgetType={ widgetType } />
</div>
);
};

View File

@ -1,42 +0,0 @@
import { BonusRareInfoMessageEvent, GetBonusRareInfoMessageComposer } from '@nitrots/nitro-renderer';
import { FC, useEffect, useState } from 'react';
import { SendMessageComposer } from '../../../../../api';
import { useMessageEvent } from '../../../../../hooks';
export interface BonusRareWidgetViewProps
{ }
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);
useMessageEvent<BonusRareInfoMessageEvent>(BonusRareInfoMessageEvent, event =>
{
const parser = event.getParser();
setProductType(parser.productType);
setProductClassId(parser.productClassId);
setTotalCoinsForBonus(parser.totalCoinsForBonus);
setCoinsStillRequiredToBuy(parser.coinsStillRequiredToBuy);
});
useEffect(() =>
{
SendMessageComposer(new GetBonusRareInfoMessageComposer());
}, []);
if(!productType) return null;
return (
<div className="bonus-rare widget flex">
{ productType }
<div className="bg-light-dark rounded overflow-hidden relative bonus-bar-container">
<div className="flex justify-center items-center size-full absolute small top-0">{ (totalCoinsForBonus - coinsStillRequiredToBuy) + '/' + totalCoinsForBonus }</div>
<div className="small bg-info rounded absolute top-0 h-full" style={ { width: ((totalCoinsForBonus - coinsStillRequiredToBuy) / totalCoinsForBonus) * 100 + '%' } }></div>
</div>
</div>
);
};

View File

@ -1,30 +0,0 @@
import { HallOfFameEntryData } from '@nitrots/nitro-renderer';
import { FC } from 'react';
import { LocalizeFormattedNumber, LocalizeText } from '../../../../../api';
import { LayoutAvatarImageView, UserProfileIconView } from '../../../../../common';
export interface HallOfFameItemViewProps
{
data: HallOfFameEntryData;
level: number;
active?: boolean;
}
export const HallOfFameItemView: FC<HallOfFameItemViewProps> = props =>
{
const { data = null, level = 0, active = false } = props;
return (
<div className="group h-full relative">
<div className="invisible group-hover:visible absolute w-[125px] max-w-[125px] p-[2px] bg-[#1c323f] border-[2px] border-[solid] border-[rgba(255,255,255,.5)] rounded-[.25rem] z-40 -left-[15px] bottom-[calc(100%-10px)]">
<div className="flex items-center justify-center gap-[5px] bg-[#3d5f6e] text-[#fff] min-w-[117px] h-[25px] max-h-[25px] text-[16px] mb-[2px]">
{ level }. { data.userName } <UserProfileIconView userId={ data.userId } />
</div>
<div className="small text-center text-white">{ LocalizeText('landing.view.competition.hof.points', [ 'points' ], [ LocalizeFormattedNumber(data.currentScore).toString() ]) }</div>
</div>
<LayoutAvatarImageView direction={ 2 } figure={ data.figure } />
</div>
);
};

View File

@ -1,37 +0,0 @@
import { CommunityGoalHallOfFameData, CommunityGoalHallOfFameMessageEvent, GetCommunityGoalHallOfFameMessageComposer } from '@nitrots/nitro-renderer';
import { FC, useEffect, useState } from 'react';
import { SendMessageComposer } from '../../../../../api';
import { useMessageEvent } from '../../../../../hooks';
import { HallOfFameItemView } from '../hall-of-fame-item/HallOfFameItemView';
import { HallOfFameWidgetViewProps } from './HallOfFameWidgetView.types';
export const HallOfFameWidgetView: FC<HallOfFameWidgetViewProps> = props =>
{
const { slot = -1, conf = null } = props;
const [ data, setData ] = useState<CommunityGoalHallOfFameData>(null);
useMessageEvent<CommunityGoalHallOfFameMessageEvent>(CommunityGoalHallOfFameMessageEvent, event =>
{
const parser = event.getParser();
setData(parser.data);
});
useEffect(() =>
{
const campaign: string = conf ? conf['campaign'] : '';
SendMessageComposer(new GetCommunityGoalHallOfFameMessageComposer(campaign));
}, [ conf ]);
if(!data) return null;
return (
<div className="bg-[#0000004d] rounded-[.25rem] justify-center flex">
{ data.hof && (data.hof.length > 0) && data.hof.map((entry, index) =>
{
return <HallOfFameItemView key={ index } data={ entry } level={ (index + 1) } />;
}
) }
</div>
);
};

View File

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

View File

@ -1,46 +0,0 @@
import { GetPromoArticlesComposer, PromoArticleData, PromoArticlesMessageEvent } from '@nitrots/nitro-renderer';
import { FC, useEffect, useState } from 'react';
import { LocalizeText, OpenUrl, SendMessageComposer } from '../../../../../api';
import { useMessageEvent } from '../../../../../hooks';
export const PromoArticleWidgetView: FC<{}> = props =>
{
const [ articles, setArticles ] = useState<PromoArticleData[]>(null);
const [ index, setIndex ] = useState(0);
useMessageEvent<PromoArticlesMessageEvent>(PromoArticlesMessageEvent, event =>
{
const parser = event.getParser();
setArticles(parser.articles);
});
useEffect(() =>
{
SendMessageComposer(new GetPromoArticlesComposer());
}, []);
if(!articles) return null;
return (
<div className="promo-articles widget mb-2">
<div className="flex flex-row items-center w-full mb-1">
<small className="flex-shrink-0 pe-1">{ LocalizeText('landing.view.promo.article.header') }</small>
<hr className="w-full my-0" />
</div>
<div className="flex flex-row mb-1">
{ articles && (articles.length > 0) && articles.map((article, ind) =>
<div key={ article.id } className={ 'rounded-[50%] border-[1px] border-[solid] border-[#fff] h-[13px] w-[13px] mr-[3px] cursor-pointer ' + (article === articles[index] ? 'bg-[black]' : ' bg-[white] ') } onClick={ event => setIndex(ind) } />
) }
</div>
{ articles && articles[index] &&
<div className="grid-cols-2 grid">
<div className="promo-article-image w-[150px] h-[150px] mr-[10px] bg-no-repeat bg-[top_center] flex-shrink-0 w-full max-w-full" style={ { backgroundImage: `url(${ articles[index].imageUrl })` } } />
<div className="col-span-1 flex flex-col h-full">
<h3 className="my-0">{ articles[index].title }</h3>
<b>{ articles[index].bodyText }</b>
<button className="w-1/2 mt-auto px-[.5rem] py-[.25rem] rounded-[.2rem] text-[#000] bg-[#d9d9d9] border-[#d9d9d9] [box-shadow:inset_0_2px_#ffffff26,_inset_0_-2px_#0000001a,_0_1px_#0000001a]" onClick={ event => OpenUrl(articles[index].linkContent) }>{ articles[index].buttonText }</button>
</div>
</div> }
</div>
);
};

View File

@ -1,39 +0,0 @@
import { GetConfiguration } from '@nitrots/nitro-renderer';
import { FC } from 'react';
import { LocalizeText, OpenUrl } from '../../../../../api';
export interface WidgetContainerViewProps
{
conf: any;
}
export const WidgetContainerView: FC<WidgetContainerViewProps> = props =>
{
const { conf = null } = props;
const getOption = (key: string) =>
{
const option = conf[key];
if(!option) return null;
switch(key)
{
case 'image':
return GetConfiguration().interpolate(option);
}
return option;
};
return (
<div className="widgetcontainer widget flex flex-row overflow-hidden">
<div className="widgetcontainer-image flex-shrink-0" style={ { backgroundImage: `url(${ getOption('image') })` } } />
<div className="flex flex-col align-self-center">
<h3 className="my-0">{ LocalizeText(`landing.view.${ getOption('texts') }.header`) }</h3>
<i>{ LocalizeText(`landing.view.${ getOption('texts') }.body`) }</i>
<button className="px-[.5rem] py-[.25rem] rounded-[.2rem] align-self-start px-3 mt-auto text-[#000] bg-[#d9d9d9] border-[#d9d9d9] [box-shadow:inset_0_2px_#ffffff26,_inset_0_-2px_#0000001a,_0_1px_#0000001a]" onClick={ event => OpenUrl(getOption('btnLink')) }>{ LocalizeText(`landing.view.${ getOption('texts') }.button`) }</button>
</div>
</div>
);
};