🆙 Init V3

This commit is contained in:
DuckieTM
2026-01-31 09:10:52 +01:00
commit 7feb10ab15
1733 changed files with 53405 additions and 0 deletions
+240
View File
@@ -0,0 +1,240 @@
import { NitroCard } from '@layout/NitroCard';
import { AddLinkEventTracker, ConvertGlobalRoomIdMessageComposer, HabboWebTools, ILinkEventTracker, LegacyExternalInterface, NavigatorInitComposer, NavigatorSearchComposer, RemoveLinkEventTracker, RoomSessionEvent } from '@nitrots/nitro-renderer';
import { FC, useCallback, useEffect, useRef, useState } from 'react';
import { FaPlus } from 'react-icons/fa';
import { LocalizeText, SendMessageComposer, TryVisitRoom } from '../../api';
import { useNavigator, useNitroEvent } from '../../hooks';
import { NavigatorDoorStateView } from './views/NavigatorDoorStateView';
import { NavigatorRoomCreatorView } from './views/NavigatorRoomCreatorView';
import { NavigatorRoomInfoView } from './views/NavigatorRoomInfoView';
import { NavigatorRoomLinkView } from './views/NavigatorRoomLinkView';
import { NavigatorRoomSettingsView } from './views/room-settings/NavigatorRoomSettingsView';
import { NavigatorSearchResultView } from './views/search/NavigatorSearchResultView';
import { NavigatorSearchView } from './views/search/NavigatorSearchView';
export const NavigatorView: FC<{}> = props =>
{
const [ isVisible, setIsVisible ] = useState(false);
const [ isReady, setIsReady ] = useState(false);
const [ isCreatorOpen, setCreatorOpen ] = useState(false);
const [ isRoomInfoOpen, setRoomInfoOpen ] = useState(false);
const [ isRoomLinkOpen, setRoomLinkOpen ] = useState(false);
const [ isLoading, setIsLoading ] = useState(false);
const [ needsInit, setNeedsInit ] = useState(true);
const [ needsSearch, setNeedsSearch ] = useState(false);
const { searchResult = null, topLevelContext = null, topLevelContexts = null, navigatorData = null } = useNavigator();
const pendingSearch = useRef<{ value: string, code: string }>(null);
const elementRef = useRef<HTMLDivElement>();
useNitroEvent<RoomSessionEvent>(RoomSessionEvent.CREATED, event =>
{
setIsVisible(false);
setCreatorOpen(false);
});
const sendSearch = useCallback((searchValue: string, contextCode: string) =>
{
setCreatorOpen(false);
SendMessageComposer(new NavigatorSearchComposer(contextCode, searchValue));
setIsLoading(true);
}, []);
const reloadCurrentSearch = useCallback(() =>
{
if(!isReady)
{
setNeedsSearch(true);
return;
}
if(pendingSearch.current)
{
sendSearch(pendingSearch.current.value, pendingSearch.current.code);
pendingSearch.current = null;
return;
}
if(searchResult)
{
sendSearch(searchResult.data, searchResult.code);
return;
}
if(!topLevelContext) return;
sendSearch('', topLevelContext.code);
}, [ isReady, searchResult, topLevelContext, sendSearch ]);
useEffect(() =>
{
const linkTracker: ILinkEventTracker = {
linkReceived: (url: string) =>
{
const parts = url.split('/');
if(parts.length < 2) return;
switch(parts[1])
{
case 'show': {
setIsVisible(true);
setNeedsSearch(true);
return;
}
case 'hide':
setIsVisible(false);
return;
case 'toggle': {
if(isVisible)
{
setIsVisible(false);
return;
}
setIsVisible(true);
setNeedsSearch(true);
return;
}
case 'toggle-room-info':
setRoomInfoOpen(value => !value);
return;
case 'toggle-room-link':
setRoomLinkOpen(value => !value);
return;
case 'goto':
if(parts.length <= 2) return;
switch(parts[2])
{
case 'home':
if(navigatorData.homeRoomId <= 0) return;
TryVisitRoom(navigatorData.homeRoomId);
break;
default: {
const roomId = parseInt(parts[2]);
TryVisitRoom(roomId);
}
}
return;
case 'create':
setIsVisible(true);
setCreatorOpen(true);
return;
case 'search':
if(parts.length > 2)
{
const topLevelContextCode = parts[2];
let searchValue = '';
if(parts.length > 3) searchValue = parts[3];
pendingSearch.current = { value: searchValue, code: topLevelContextCode };
setIsVisible(true);
setNeedsSearch(true);
}
return;
}
},
eventUrlPrefix: 'navigator/'
};
AddLinkEventTracker(linkTracker);
return () => RemoveLinkEventTracker(linkTracker);
}, [ isVisible, navigatorData ]);
useEffect(() =>
{
if(!searchResult) return;
setIsLoading(false);
if(elementRef && elementRef.current) elementRef.current.scrollTop = 0;
}, [ searchResult ]);
useEffect(() =>
{
if(!isVisible || !isReady || !needsSearch) return;
reloadCurrentSearch();
setNeedsSearch(false);
}, [ isVisible, isReady, needsSearch, reloadCurrentSearch ]);
useEffect(() =>
{
if(isReady || !topLevelContext) return;
setIsReady(true);
}, [ isReady, topLevelContext ]);
useEffect(() =>
{
if(!isVisible || !needsInit) return;
SendMessageComposer(new NavigatorInitComposer());
setNeedsInit(false);
}, [ isVisible, needsInit ]);
useEffect(() =>
{
LegacyExternalInterface.addCallback(HabboWebTools.OPENROOM, (k: string, _arg_2: boolean = false, _arg_3: string = null) => SendMessageComposer(new ConvertGlobalRoomIdMessageComposer(k)));
}, []);
return (
<>
{ isVisible &&
<NitroCard
className="w-navigator-w h-navigator-h min-w-navigator-w min-h-navigator-h"
uniqueKey="navigator">
<NitroCard.Header
headerText={ LocalizeText(isCreatorOpen ? 'navigator.createroom.title' : 'navigator.title') }
onCloseClick={ event => setIsVisible(false) } />
<NitroCard.Tabs>
{ topLevelContexts && (topLevelContexts.length > 0) && topLevelContexts.map((context, index) =>
{
return (
<NitroCard.TabItem
key={ index }
isActive={ ((topLevelContext === context) && !isCreatorOpen) }
onClick={ event => sendSearch('', context.code) }>
{ LocalizeText(('navigator.toplevelview.' + context.code)) }
</NitroCard.TabItem>
);
}) }
<NitroCard.TabItem
isActive={ isCreatorOpen }
onClick={ event => setCreatorOpen(true) }>
<FaPlus className="fa-icon" />
</NitroCard.TabItem>
</NitroCard.Tabs>
<NitroCard.Content isLoading={ isLoading }>
{ !isCreatorOpen &&
<>
<NavigatorSearchView sendSearch={ sendSearch } />
<div ref={ elementRef } className="flex flex-col overflow-auto gap-2">
{ (searchResult && searchResult.results.map((result, index) => <NavigatorSearchResultView key={ index } searchResult={ result } />)) }
</div>
</> }
{ isCreatorOpen && <NavigatorRoomCreatorView /> }
</NitroCard.Content>
</NitroCard> }
<NavigatorDoorStateView />
{ isRoomInfoOpen && <NavigatorRoomInfoView onCloseClick={ () => setRoomInfoOpen(false) } /> }
{ isRoomLinkOpen && <NavigatorRoomLinkView onCloseClick={ () => setRoomLinkOpen(false) } /> }
<NavigatorRoomSettingsView />
</>
);
};
@@ -0,0 +1,111 @@
import { FC, useEffect, useState } from 'react';
import { CreateRoomSession, DoorStateType, GoToDesktop, LocalizeText } from '../../../api';
import { Button, NitroCardContentView, NitroCardHeaderView, NitroCardView, Text } from '../../../common';
import { useNavigator } from '../../../hooks';
import { NitroInput } from '../../../layout';
const VISIBLE_STATES = [ DoorStateType.START_DOORBELL, DoorStateType.STATE_WAITING, DoorStateType.STATE_NO_ANSWER, DoorStateType.START_PASSWORD, DoorStateType.STATE_WRONG_PASSWORD ];
const DOORBELL_STATES = [ DoorStateType.START_DOORBELL, DoorStateType.STATE_WAITING, DoorStateType.STATE_NO_ANSWER ];
const PASSWORD_STATES = [ DoorStateType.START_PASSWORD, DoorStateType.STATE_WRONG_PASSWORD ];
export const NavigatorDoorStateView: FC<{}> = props =>
{
const [ password, setPassword ] = useState('');
const { doorData = null, setDoorData = null } = useNavigator();
const onClose = () =>
{
if(doorData && (doorData.state === DoorStateType.STATE_WAITING)) GoToDesktop();
setDoorData(null);
};
const ring = () =>
{
if(!doorData || !doorData.roomInfo) return;
CreateRoomSession(doorData.roomInfo.roomId);
setDoorData(prevValue =>
{
const newValue = { ...prevValue };
newValue.state = DoorStateType.STATE_PENDING_SERVER;
return newValue;
});
};
const tryEntering = () =>
{
if(!doorData || !doorData.roomInfo) return;
CreateRoomSession(doorData.roomInfo.roomId, password);
setDoorData(prevValue =>
{
const newValue = { ...prevValue };
newValue.state = DoorStateType.STATE_PENDING_SERVER;
return newValue;
});
};
useEffect(() =>
{
if(!doorData || (doorData.state !== DoorStateType.STATE_NO_ANSWER)) return;
GoToDesktop();
}, [ doorData ]);
if(!doorData || (doorData.state === DoorStateType.NONE) || (VISIBLE_STATES.indexOf(doorData.state) === -1)) return null;
const isDoorbell = (DOORBELL_STATES.indexOf(doorData.state) >= 0);
return (
<NitroCardView className="nitro-navigator-doorbell" theme="primary-slim">
<NitroCardHeaderView headerText={ LocalizeText(isDoorbell ? 'navigator.doorbell.title' : 'navigator.password.title') } onCloseClick={ onClose } />
<NitroCardContentView>
<div className="flex flex-col gap-1">
<Text bold>{ doorData && doorData.roomInfo && doorData.roomInfo.roomName }</Text>
{ (doorData.state === DoorStateType.START_DOORBELL) &&
<Text>{ LocalizeText('navigator.doorbell.info') }</Text> }
{ (doorData.state === DoorStateType.STATE_WAITING) &&
<Text>{ LocalizeText('navigator.doorbell.waiting') }</Text> }
{ (doorData.state === DoorStateType.STATE_NO_ANSWER) &&
<Text>{ LocalizeText('navigator.doorbell.no.answer') }</Text> }
{ (doorData.state === DoorStateType.START_PASSWORD) &&
<Text>{ LocalizeText('navigator.password.info') }</Text> }
{ (doorData.state === DoorStateType.STATE_WRONG_PASSWORD) &&
<Text>{ LocalizeText('navigator.password.retryinfo') }</Text> }
</div>
{ isDoorbell &&
<div className="flex flex-col gap-1">
{ (doorData.state === DoorStateType.START_DOORBELL) &&
<Button variant="success" onClick={ ring }>
{ LocalizeText('navigator.doorbell.button.ring') }
</Button> }
<Button variant="danger" onClick={ onClose }>
{ LocalizeText('generic.cancel') }
</Button>
</div> }
{ !isDoorbell &&
<>
<div className="flex flex-col gap-1">
<Text>{ LocalizeText('navigator.password.enter') }</Text>
<NitroInput type="password" onChange={ event => setPassword(event.target.value) } />
</div>
<div className="flex flex-col gap-1">
<Button variant="success" onClick={ tryEntering }>
{ LocalizeText('navigator.password.button.try') }
</Button>
<Button variant="danger" onClick={ onClose }>
{ LocalizeText('generic.cancel') }
</Button>
</div>
</> }
</NitroCardContentView>
</NitroCardView>
);
};
@@ -0,0 +1,123 @@
import { CreateFlatMessageComposer, HabboClubLevelEnum } from '@nitrots/nitro-renderer';
import { FC, useEffect, useState } from 'react';
import { GetClubMemberLevel, GetConfigurationValue, IRoomModel, LocalizeText, SendMessageComposer } from '../../../api';
import { Button, Flex, Grid, LayoutCurrencyIcon, LayoutGridItem, Text } from '../../../common';
import { useNavigator } from '../../../hooks';
import { NitroInput } from '../../../layout';
export const NavigatorRoomCreatorView: FC<{}> = props =>
{
const [ maxVisitorsList, setMaxVisitorsList ] = useState<number[]>(null);
const [ name, setName ] = useState<string>(null);
const [ description, setDescription ] = useState<string>(null);
const [ category, setCategory ] = useState<number>(null);
const [ visitorsCount, setVisitorsCount ] = useState<number>(null);
const [ tradesSetting, setTradesSetting ] = useState<number>(0);
const [ roomModels, setRoomModels ] = useState<IRoomModel[]>([]);
const [ selectedModelName, setSelectedModelName ] = useState<string>('');
const { categories = null } = useNavigator();
const hcDisabled = GetConfigurationValue<boolean>('hc.disabled', false);
const getRoomModelImage = (name: string) => GetConfigurationValue<string>('images.url') + `/navigator/models/model_${ name }.png`;
const selectModel = (model: IRoomModel, index: number) =>
{
if(!model || (model.clubLevel > GetClubMemberLevel())) return;
setSelectedModelName(roomModels[index].name);
};
const createRoom = () =>
{
SendMessageComposer(new CreateFlatMessageComposer(name, description, 'model_' + selectedModelName, Number(category), Number(visitorsCount), tradesSetting));
};
useEffect(() =>
{
if(!maxVisitorsList)
{
const list = [];
for(let i = 10; i <= 100; i = i + 10) list.push(i);
setMaxVisitorsList(list);
setVisitorsCount(list[0]);
}
}, [ maxVisitorsList ]);
useEffect(() =>
{
if(categories && categories.length) setCategory(categories[0].id);
}, [ categories ]);
useEffect(() =>
{
const models = GetConfigurationValue<IRoomModel[]>('navigator.room.models');
if(models && models.length)
{
setRoomModels(models);
setSelectedModelName(models[0].name);
}
}, []);
return (
<div className="flex flex-col overflow-auto">
<Grid overflow="hidden">
<div className="flex flex-col gap-1 overflow-auto col-span-6">
<div className="flex flex-col gap-1">
<Text>{ LocalizeText('navigator.createroom.roomnameinfo') }</Text>
<NitroInput maxLength={ 60 } placeholder={ LocalizeText('navigator.createroom.roomnameinfo') } type="text" onChange={ event => setName(event.target.value) } />
</div>
<div className="flex flex-col !flex-grow gap-1">
<Text>{ LocalizeText('navigator.createroom.roomdescinfo') }</Text>
<textarea className="!flex-grow form-control form-control-sm w-full" maxLength={ 255 } placeholder={ LocalizeText('navigator.createroom.roomdescinfo') } onChange={ event => setDescription(event.target.value) } />
</div>
<div className="flex flex-col gap-1">
<Text>{ LocalizeText('navigator.category') }</Text>
<select className="form-select form-select-sm" onChange={ event => setCategory(Number(event.target.value)) }>
{ categories && (categories.length > 0) && categories.map(category =>
{
return <option key={ category.id } value={ category.id }>{ LocalizeText(category.name) }</option>;
}) }
</select>
</div>
<div className="flex flex-col gap-1">
<Text>{ LocalizeText('navigator.maxvisitors') }</Text>
<select className="form-select form-select-sm" onChange={ event => setVisitorsCount(Number(event.target.value)) }>
{ maxVisitorsList && maxVisitorsList.map(value =>
{
return <option key={ value } value={ value }>{ value }</option>;
}) }
</select>
</div>
<div className="flex flex-col gap-1">
<Text>{ LocalizeText('navigator.tradesettings') }</Text>
<select className="form-select form-select-sm" onChange={ event => setTradesSetting(Number(event.target.value)) }>
<option value="0">{ LocalizeText('navigator.roomsettings.trade_not_allowed') }</option>
<option value="1">{ LocalizeText('navigator.roomsettings.trade_not_with_Controller') }</option>
<option value="2">{ LocalizeText('navigator.roomsettings.trade_allowed') }</option>
</select>
</div>
</div>
<div className="flex flex-col gap-1 overflow-auto col-span-6">
{
roomModels.map((model, index) =>
{
return (<LayoutGridItem key={ model.name } fullHeight className="p-1" disabled={ (GetClubMemberLevel() < model.clubLevel) } gap={ 0 } itemActive={ (selectedModelName === model.name) } overflow="unset" onClick={ () => selectModel(model, index) }>
<Flex center fullHeight overflow="hidden">
<img alt="" src={ getRoomModelImage(model.name) } />
</Flex>
<Text bold>{ model.tileSize } { LocalizeText('navigator.createroom.tilesize') }</Text>
{ !hcDisabled && model.clubLevel > HabboClubLevelEnum.NO_CLUB && <LayoutCurrencyIcon className="top-1 end-1" position="absolute" type="hc" /> }
</LayoutGridItem>);
})
}
</div>
</Grid>
<Button fullWidth disabled={ (!name || (name.length < 3)) } variant={ (!name || (name.length < 3)) ? 'danger' : 'success' } onClick={ createRoom }>{ LocalizeText('navigator.createroom.create') }</Button>
</div>
);
};
@@ -0,0 +1,181 @@
import { CreateLinkEvent, GetCustomRoomFilterMessageComposer, GetSessionDataManager, NavigatorSearchComposer, RemoveOwnRoomRightsRoomMessageComposer, RoomControllerLevel, RoomMuteComposer, RoomSettingsComposer, SecurityLevel, ToggleStaffPickMessageComposer, UpdateHomeRoomMessageComposer } from '@nitrots/nitro-renderer';
import { FC, useEffect, useState } from 'react';
import { FaLink, FaSignOutAlt } from 'react-icons/fa';
import { DispatchUiEvent, GetGroupInformation, LocalizeText, ReportType, SendMessageComposer } from '../../../api';
import { Button, Column, Flex, LayoutBadgeImageView, LayoutRoomThumbnailView, NitroCardContentView, NitroCardHeaderView, NitroCardView, Text, UserProfileIconView } from '../../../common';
import { RoomWidgetThumbnailEvent } from '../../../events';
import { useHelp, useNavigator, useRoom } from '../../../hooks';
import { classNames } from '../../../layout';
export interface NavigatorRoomInfoViewProps {
onCloseClick: () => void;
}
export const NavigatorRoomInfoView: FC<NavigatorRoomInfoViewProps> = props => {
const { onCloseClick = null } = props;
const [ isRoomPicked, setIsRoomPicked ] = useState(false);
const [ isRoomMuted, setIsRoomMuted ] = useState(false);
const { report = null } = useHelp();
const { navigatorData = null } = useNavigator();
const { roomSession = null } = useRoom();
const hasPermission = (permission: string) => {
switch(permission) {
case 'settings':
return (GetSessionDataManager().userId === navigatorData.enteredGuestRoom.ownerId || GetSessionDataManager().isModerator);
case 'staff_pick':
return GetSessionDataManager().securityLevel >= SecurityLevel.COMMUNITY;
case 'floor':
return roomSession?.controllerLevel >= RoomControllerLevel.GUEST;
case 'guest':
return roomSession?.controllerLevel === RoomControllerLevel.GUEST;
default: return false;
}
};
const processAction = (action: string, value?: string) => {
if(!navigatorData || !navigatorData.enteredGuestRoom) return;
switch(action) {
case 'set_home_room':
let newRoomId = -1;
if(navigatorData.homeRoomId !== navigatorData.enteredGuestRoom.roomId) {
newRoomId = navigatorData.enteredGuestRoom.roomId;
}
if(newRoomId > 0) SendMessageComposer(new UpdateHomeRoomMessageComposer(newRoomId));
return;
case 'navigator_search_tag':
CreateLinkEvent(`navigator/search/${ value }`);
SendMessageComposer(new NavigatorSearchComposer('hotel_view', `tag:${ value }`));
return;
case 'open_room_thumbnail_camera':
DispatchUiEvent(new RoomWidgetThumbnailEvent(RoomWidgetThumbnailEvent.TOGGLE_THUMBNAIL));
return;
case 'open_group_info':
GetGroupInformation(navigatorData.enteredGuestRoom.habboGroupId);
return;
case 'toggle_room_link':
CreateLinkEvent('navigator/toggle-room-link');
return;
case 'open_room_settings':
SendMessageComposer(new RoomSettingsComposer(navigatorData.enteredGuestRoom.roomId));
return;
case 'toggle_pick':
setIsRoomPicked(value => !value);
SendMessageComposer(new ToggleStaffPickMessageComposer(navigatorData.enteredGuestRoom.roomId));
return;
case 'toggle_mute':
setIsRoomMuted(value => !value);
SendMessageComposer(new RoomMuteComposer());
return;
case 'room_filter':
SendMessageComposer(new GetCustomRoomFilterMessageComposer(navigatorData.enteredGuestRoom.roomId));
return;
case 'open_floorplan_editor':
CreateLinkEvent('floor-editor/toggle');
return;
case 'report_room':
report(ReportType.ROOM, { roomId: navigatorData.enteredGuestRoom.roomId, roomName: navigatorData.enteredGuestRoom.roomName });
return;
case 'remove_rights':
SendMessageComposer(new RemoveOwnRoomRightsRoomMessageComposer(navigatorData.enteredGuestRoom.roomId));
return;
case 'close':
onCloseClick();
return;
}
};
useEffect(() => {
if(!navigatorData) return;
setIsRoomPicked(navigatorData.currentRoomIsStaffPick);
if(navigatorData.enteredGuestRoom) setIsRoomMuted(navigatorData.enteredGuestRoom.allInRoomMuted);
}, [ navigatorData ]);
if(!navigatorData.enteredGuestRoom) return null;
return (
<NitroCardView className="nitro-room-info" theme="primary-slim">
<NitroCardHeaderView headerText={ LocalizeText('navigator.roomsettings.roominfo') } onCloseClick={ () => processAction('close') } />
<NitroCardContentView className="text-black">
{ navigatorData.enteredGuestRoom &&
<>
<Flex gap={ 2 } overflow="hidden">
<LayoutRoomThumbnailView customUrl={ navigatorData.enteredGuestRoom.officialRoomPicRef } roomId={ navigatorData.enteredGuestRoom.roomId }>
{ hasPermission('settings') && <i className="top-0 m-1 cursor-pointer nitro-icon icon-camera-small absolute b-0 r-0" onClick={ () => processAction('open_room_thumbnail_camera') } /> }
</LayoutRoomThumbnailView>
<Column grow gap={ 1 } overflow="hidden">
<div className="flex gap-1">
<Column grow gap={ 1 }>
<div className="flex gap-1">
<Text>{ navigatorData.enteredGuestRoom.roomName }</Text>
</div>
{ navigatorData.enteredGuestRoom.showOwner &&
<div className="flex items-center gap-1">
<Text>{ LocalizeText('navigator.roomownercaption') }</Text>
<div className="flex items-center gap-1">
<UserProfileIconView userId={ navigatorData.enteredGuestRoom.ownerId } />
<Text>{ navigatorData.enteredGuestRoom.ownerName }</Text>
</div>
</div> }
<div className="flex items-center gap-1">
<Text>{ LocalizeText('navigator.roomrating') }</Text>
<Text>{ navigatorData.currentRoomRating }</Text>
</div>
{ (navigatorData.enteredGuestRoom.tags.length > 0) &&
<div className="flex items-center gap-1">
{ navigatorData.enteredGuestRoom.tags.map(tag =>
<Text key={ tag } pointer className="p-1 rounded bg-muted" onClick={ event => processAction('navigator_search_tag', tag) }>#{ tag }</Text>
) }
</div> }
</Column>
<Column alignItems="center" gap={ 1 }>
<i className={ classNames('flex-shrink-0 nitro-icon icon-house-small cursor-pointer', ((navigatorData.homeRoomId !== navigatorData.enteredGuestRoom.roomId) && 'gray')) } onClick={ () => processAction('set_home_room') } />
{ hasPermission('settings') &&
<i className="cursor-pointer nitro-icon icon-cog" title={ LocalizeText('navigator.room.popup.info.room.settings') } onClick={ event => processAction('open_room_settings') } /> }
<FaLink className="cursor-pointer fa-icon" title={ LocalizeText('navigator.embed.caption') } onClick={ event => processAction('toggle_room_link') } />
{ hasPermission('guest') &&
<FaSignOutAlt className="cursor-pointer fa-icon" title={ LocalizeText('navigator.roominfo.removerights.tooltip') } onClick={ event => processAction('remove_rights') } /> }
</Column>
</div>
<Text overflow="auto" style={ { maxHeight: 50 } }>{ navigatorData.enteredGuestRoom.description }</Text>
{ (navigatorData.enteredGuestRoom.habboGroupId > 0) &&
<Flex pointer alignItems="center" gap={ 1 } onClick={ () => processAction('open_group_info') }>
<LayoutBadgeImageView badgeCode={ navigatorData.enteredGuestRoom.groupBadgeCode } className="flex-none" isGroup={ true } />
<Text underline>
{ LocalizeText('navigator.guildbase', [ 'groupName' ], [ navigatorData.enteredGuestRoom.groupName ]) }
</Text>
</Flex> }
</Column>
</Flex>
<div className="flex flex-col gap-1">
{ hasPermission('staff_pick') &&
<Button onClick={ () => processAction('toggle_pick') }>
{ LocalizeText(isRoomPicked ? 'navigator.staffpicks.unpick' : 'navigator.staffpicks.pick') }
</Button> }
<Button variant="danger" onClick={ () => processAction('report_room') }>
{ LocalizeText('help.emergency.main.report.room') }
</Button>
{ hasPermission('settings') &&
<>
<Button onClick={ () => processAction('toggle_mute') }>
{ LocalizeText(isRoomMuted ? 'navigator.muteall_on' : 'navigator.muteall_off') }
</Button>
<Button onClick={ () => processAction('room_filter') }>
{ LocalizeText('navigator.roomsettings.roomfilter') }
</Button>
<Button onClick={ () => processAction('open_floorplan_editor') }>
{ LocalizeText('open.floor.plan.editor') }
</Button>
</> }
{ hasPermission('floor') && !hasPermission('settings') &&
<Button onClick={ () => processAction('open_floorplan_editor') }>
{ LocalizeText('open.floor.plan.editor') }
</Button> }
</div>
</> }
</NitroCardContentView>
</NitroCardView>
);
};
@@ -0,0 +1,33 @@
import { FC } from 'react';
import { GetConfigurationValue, LocalizeText } from '../../../api';
import { LayoutRoomThumbnailView, NitroCardContentView, NitroCardHeaderView, NitroCardView, Text } from '../../../common';
import { useNavigator } from '../../../hooks';
export class NavigatorRoomLinkViewProps
{
onCloseClick: () => void;
}
export const NavigatorRoomLinkView: FC<NavigatorRoomLinkViewProps> = props =>
{
const { onCloseClick = null } = props;
const { navigatorData = null } = useNavigator();
if(!navigatorData.enteredGuestRoom) return null;
return (
<NitroCardView className="nitro-room-link" theme="primary-slim">
<NitroCardHeaderView headerText={ LocalizeText('navigator.embed.title') } onCloseClick={ onCloseClick } />
<NitroCardContentView className="text-black flex items-center">
<div className="flex gap-2">
<LayoutRoomThumbnailView customUrl={ navigatorData.enteredGuestRoom.officialRoomPicRef } roomId={ navigatorData.enteredGuestRoom.roomId } />
<div className="flex flex-col">
<Text bold fontSize={ 5 }>{ LocalizeText('navigator.embed.headline') }</Text>
<Text>{ LocalizeText('navigator.embed.info') }</Text>
<input readOnly className="min-h-[calc(1.5em+ .5rem+2px)] px-[.5rem] py-[.25rem] rounded-[.2rem] form-control-sm" type="text" value={ LocalizeText('navigator.embed.src', [ 'roomId' ], [ navigatorData.enteredGuestRoom.roomId.toString() ]).replace('${url.prefix}', GetConfigurationValue<string>('url.prefix', '')) } />
</div>
</div>
</NitroCardContentView>
</NitroCardView>
);
};
@@ -0,0 +1,88 @@
import { RoomDataParser } from '@nitrots/nitro-renderer';
import { FC, useEffect, useState } from 'react';
import { IRoomData, LocalizeText } from '../../../../api';
import { Text } from '../../../../common';
interface NavigatorRoomSettingsTabViewProps
{
roomData: IRoomData;
handleChange: (field: string, value: string | number | boolean) => void;
}
export const NavigatorRoomSettingsAccessTabView: FC<NavigatorRoomSettingsTabViewProps> = props =>
{
const { roomData = null, handleChange = null } = props;
const [ password, setPassword ] = useState<string>('');
const [ confirmPassword, setConfirmPassword ] = useState('');
const [ isTryingPassword, setIsTryingPassword ] = useState(false);
const saveRoomPassword = () =>
{
if(!isTryingPassword || ((password.length <= 0) || (confirmPassword.length <= 0) || (password !== confirmPassword))) return;
handleChange('password', password);
};
useEffect(() =>
{
setPassword('');
setConfirmPassword('');
setIsTryingPassword(false);
}, [ roomData ]);
return (
<>
<div className="flex flex-col gap-1">
<Text small bold>{ LocalizeText('navigator.roomsettings.roomaccess.caption') }</Text>
<Text small>{ LocalizeText('navigator.roomsettings.roomaccess.info') }</Text>
</div>
<div className="overflow-auto">
<div className="flex flex-col gap-1">
<Text small bold>{ LocalizeText('navigator.roomsettings.doormode') }</Text>
<div className="flex items-center gap-1">
<input checked={ (roomData.lockState === RoomDataParser.OPEN_STATE) && !isTryingPassword } className="form-check-input" name="lockState" type="radio" onChange={ event => handleChange('lock_state', RoomDataParser.OPEN_STATE) } />
<Text small>{ LocalizeText('navigator.roomsettings.doormode.open') }</Text>
</div>
<div className="flex items-center gap-1">
<input checked={ (roomData.lockState === RoomDataParser.DOORBELL_STATE) && !isTryingPassword } className="form-check-input" name="lockState" type="radio" onChange={ event => handleChange('lock_state', RoomDataParser.DOORBELL_STATE) } />
<Text small>{ LocalizeText('navigator.roomsettings.doormode.doorbell') }</Text>
</div>
<div className="flex items-center gap-1">
<input checked={ (roomData.lockState === RoomDataParser.INVISIBLE_STATE) && !isTryingPassword } className="form-check-input" name="lockState" type="radio" onChange={ event => handleChange('lock_state', RoomDataParser.INVISIBLE_STATE) } />
<Text small>{ LocalizeText('navigator.roomsettings.doormode.invisible') }</Text>
</div>
<div className="flex w-full gap-1">
<input checked={ (roomData.lockState === RoomDataParser.PASSWORD_STATE) || isTryingPassword } className="form-check-input" name="lockState" type="radio" onChange={ event => setIsTryingPassword(event.target.checked) } />
{ !isTryingPassword && (roomData.lockState !== RoomDataParser.PASSWORD_STATE) &&
<Text small>{ LocalizeText('navigator.roomsettings.doormode.password') }</Text> }
{ (isTryingPassword || (roomData.lockState === RoomDataParser.PASSWORD_STATE)) &&
<div className="flex flex-col gap-1">
<Text small>{ LocalizeText('navigator.roomsettings.doormode.password') }</Text>
<input className="min-h-[calc(1.5em+ .5rem+2px)] px-[.5rem] py-[.25rem] rounded-[.2rem] form-control-sm col-span-4" placeholder={ LocalizeText('navigator.roomsettings.password') } type="password" value={ password } onChange={ event => setPassword(event.target.value) } onFocus={ event => setIsTryingPassword(true) } />
{ isTryingPassword && (password.length <= 0) &&
<Text small bold variant="danger">
{ LocalizeText('navigator.roomsettings.passwordismandatory') }
</Text> }
<input className="min-h-[calc(1.5em+ .5rem+2px)] px-[.5rem] py-[.25rem] rounded-[.2rem] form-control-sm col-span-4" placeholder={ LocalizeText('navigator.roomsettings.passwordconfirm') } type="password" value={ confirmPassword } onBlur={ saveRoomPassword } onChange={ event => setConfirmPassword(event.target.value) } />
{ isTryingPassword && ((password.length > 0) && (password !== confirmPassword)) &&
<Text small bold variant="danger">
{ LocalizeText('navigator.roomsettings.invalidconfirm') }
</Text> }
</div> }
</div>
</div>
<div className="flex flex-col gap-1">
<Text small bold>{ LocalizeText('navigator.roomsettings.pets') }</Text>
<div className="flex items-center gap-1">
<input checked={ roomData.allowPets } className="form-check-input" type="checkbox" onChange={ event => handleChange('allow_pets', event.target.checked) } />
<Text small>{ LocalizeText('navigator.roomsettings.allowpets') }</Text>
</div>
<div className="flex items-center gap-1">
<input checked={ roomData.allowPetsEat } className="form-check-input" type="checkbox" onChange={ event => handleChange('allow_pets_eat', event.target.checked) } />
<Text small>{ LocalizeText('navigator.roomsettings.allowfoodconsume') }</Text>
</div>
</div>
</div>
</>
);
};
@@ -0,0 +1,172 @@
import { CreateLinkEvent, RoomDeleteComposer, RoomSettingsSaveErrorEvent, RoomSettingsSaveErrorParser } from '@nitrots/nitro-renderer';
import { FC, useEffect, useState } from 'react';
import { FaTimes } from 'react-icons/fa';
import { GetMaxVisitorsList, IRoomData, LocalizeText, SendMessageComposer } from '../../../../api';
import { Column, Text } from '../../../../common';
import { useMessageEvent, useNavigator, useNotification } from '../../../../hooks';
import { NitroInput } from '../../../../layout';
const ROOM_NAME_MIN_LENGTH = 3;
const ROOM_NAME_MAX_LENGTH = 60;
const DESC_MAX_LENGTH = 255;
const TAGS_MAX_LENGTH = 15;
interface NavigatorRoomSettingsTabViewProps
{
roomData: IRoomData;
handleChange: (field: string, value: string | number | boolean | string[]) => void;
onClose: () => void;
}
export const NavigatorRoomSettingsBasicTabView: FC<NavigatorRoomSettingsTabViewProps> = props =>
{
const { roomData = null, handleChange = null, onClose = null } = props;
const [ roomName, setRoomName ] = useState<string>('');
const [ roomDescription, setRoomDescription ] = useState<string>('');
const [ roomTag1, setRoomTag1 ] = useState<string>('');
const [ roomTag2, setRoomTag2 ] = useState<string>('');
const [ tagIndex, setTagIndex ] = useState(0);
const [ typeError, setTypeError ] = useState<string>('');
const { showConfirm = null } = useNotification();
const { categories = null } = useNavigator();
useMessageEvent<RoomSettingsSaveErrorEvent>(RoomSettingsSaveErrorEvent, event =>
{
const parser = event.getParser();
if(!parser) return;
switch(parser.code)
{
case RoomSettingsSaveErrorParser.ERROR_INVALID_TAG:
setTypeError('navigator.roomsettings.unacceptablewords');
case RoomSettingsSaveErrorParser.ERROR_NON_USER_CHOOSABLE_TAG:
setTypeError('navigator.roomsettings.nonuserchoosabletag');
break;
default:
setTypeError('');
break;
}
});
const deleteRoom = () =>
{
showConfirm(LocalizeText('navigator.roomsettings.deleteroom.confirm.message', [ 'room_name' ], [ roomData.roomName ]), () =>
{
SendMessageComposer(new RoomDeleteComposer(roomData.roomId));
if(onClose) onClose();
CreateLinkEvent('navigator/search/myworld_view');
},
null, null, null, LocalizeText('navigator.roomsettings.deleteroom.confirm.title'));
};
const saveRoomName = () =>
{
if((roomName === roomData.roomName) || (roomName.length < ROOM_NAME_MIN_LENGTH) || (roomName.length > ROOM_NAME_MAX_LENGTH)) return;
handleChange('name', roomName);
};
const saveRoomDescription = () =>
{
if((roomDescription === roomData.roomDescription) || (roomDescription.length > DESC_MAX_LENGTH)) return;
handleChange('description', roomDescription);
};
const saveTags = (index: number) =>
{
if(index === 0 && (roomTag1 === roomData.tags[0]) || (roomTag1.length > TAGS_MAX_LENGTH)) return;
if(index === 1 && (roomTag2 === roomData.tags[1]) || (roomTag2.length > TAGS_MAX_LENGTH)) return;
if(roomTag1 === '' && roomTag2 !== '') setRoomTag2('');
setTypeError('');
setTagIndex(index);
handleChange('tags', (roomTag1 === '' && roomTag2 !== '') ? [ roomTag2 ] : [ roomTag1, roomTag2 ]);
};
useEffect(() =>
{
setRoomName(roomData.roomName);
setRoomDescription(roomData.roomDescription);
setRoomTag1((roomData.tags.length > 0 && roomData.tags[0]) ? roomData.tags[0] : '');
setRoomTag2((roomData.tags.length > 0 && roomData.tags[1]) ? roomData.tags[1] : '');
}, [ roomData ]);
return (
<>
<div className="flex items-center gap-1">
<Text small className="col-span-3">{ LocalizeText('navigator.roomname') }</Text>
<Column fullWidth gap={ 0 }>
<NitroInput className="form-control-sm" maxLength={ ROOM_NAME_MAX_LENGTH } value={ roomName } onBlur={ saveRoomName } onChange={ event => setRoomName(event.target.value) } />
{ (roomName.length < ROOM_NAME_MIN_LENGTH) &&
<Text bold small variant="danger">
{ LocalizeText('navigator.roomsettings.roomnameismandatory') }
</Text> }
</Column>
</div>
<div className="flex items-center gap-1">
<Text small className="col-span-3">{ LocalizeText('navigator.roomsettings.desc') }</Text>
<textarea className="min-h-[calc(1.5em+ .5rem+2px)] px-[.5rem] py-[.25rem] rounded-[.2rem] form-control-sm" maxLength={ DESC_MAX_LENGTH } value={ roomDescription } onBlur={ saveRoomDescription } onChange={ event => setRoomDescription(event.target.value) } />
</div>
<div className="flex items-center gap-1">
<Text small className="col-span-3">{ LocalizeText('navigator.category') }</Text>
<select className="form-select form-select-sm" value={ roomData.categoryId } onChange={ event => handleChange('category', event.target.value) }>
{ categories && categories.map(category => <option key={ category.id } value={ category.id }>{ LocalizeText(category.name) }</option>) }
</select>
</div>
<div className="flex items-center gap-1">
<Text small className="col-span-3">{ LocalizeText('navigator.maxvisitors') }</Text>
<select className="form-select form-select-sm" value={ roomData.userCount } onChange={ event => handleChange('max_visitors', event.target.value) }>
{ GetMaxVisitorsList && GetMaxVisitorsList.map(value => <option key={ value } value={ value }>{ value }</option>) }
</select>
</div>
<div className="flex items-center gap-0">
<Text small className="col-span-3">{ LocalizeText('navigator.tradesettings') }</Text>
<select className="form-select form-select-sm" value={ roomData.tradeState } onChange={ event => handleChange('trade_state', event.target.value) }>
<option value="0">{ LocalizeText('navigator.roomsettings.trade_not_allowed') }</option>
<option value="1">{ LocalizeText('navigator.roomsettings.trade_not_with_Controller') }</option>
<option value="2">{ LocalizeText('navigator.roomsettings.trade_allowed') }</option>
</select>
</div>
<div className="flex items-center gap-1">
<Text small className="col-span-3">{ LocalizeText('navigator.tags') }</Text>
<Column fullWidth gap={ 0 }>
<NitroInput className="form-control-sm" value={ roomTag1 } onBlur={ () => saveTags(0) } onChange={ event => setRoomTag1(event.target.value) } />
{ (roomTag1.length > TAGS_MAX_LENGTH) &&
<Text bold small variant="danger">
{ LocalizeText('navigator.roomsettings.toomanycharacters') }
</Text> }
{ (tagIndex === 0 && typeError != '') &&
<Text bold small variant="danger">
{ LocalizeText(typeError) }
</Text> }
</Column>
<Column fullWidth gap={ 0 }>
<NitroInput className="form-control-sm" value={ roomTag2 } onBlur={ () => saveTags(1) } onChange={ event => setRoomTag2(event.target.value) } />
{ (roomTag2.length > TAGS_MAX_LENGTH) &&
<Text bold small variant="danger">
{ LocalizeText('navigator.roomsettings.toomanycharacters') }
</Text> }
{ (tagIndex === 1 && typeError != '') &&
<Text bold small variant="danger">
{ LocalizeText(typeError) }
</Text> }
</Column>
</div>
<div className="flex items-center gap-1">
<div className="col-span-1" />
<input checked={ roomData.allowWalkthrough } className="form-check-input" type="checkbox" onChange={ event => handleChange('allow_walkthrough', event.target.checked) } />
<Text small>{ LocalizeText('navigator.roomsettings.allow_walk_through') }</Text>
</div>
<Text small bold pointer underline className="flex items-center justify-center gap-1" variant="danger" onClick={ deleteRoom }>
<FaTimes className="fa-icon" />
{ LocalizeText('navigator.roomsettings.delete') }
</Text>
</>
);
};
@@ -0,0 +1,118 @@
import { BannedUserData, BannedUsersFromRoomEvent, RoomBannedUsersComposer, RoomModerationSettings, RoomUnbanUserComposer } from '@nitrots/nitro-renderer';
import { FC, useEffect, useState } from 'react';
import { IRoomData, LocalizeText, SendMessageComposer } from '../../../../api';
import { Button, Column, Flex, Grid, Text, UserProfileIconView } from '../../../../common';
import { useMessageEvent } from '../../../../hooks';
interface NavigatorRoomSettingsTabViewProps
{
roomData: IRoomData;
handleChange: (field: string, value: string | number | boolean) => void;
}
export const NavigatorRoomSettingsModTabView: FC<NavigatorRoomSettingsTabViewProps> = props =>
{
const { roomData = null, handleChange = null } = props;
const [ selectedUserId, setSelectedUserId ] = useState<number>(-1);
const [ bannedUsers, setBannedUsers ] = useState<BannedUserData[]>([]);
const unBanUser = (userId: number) =>
{
setBannedUsers(prevValue =>
{
const newValue = [ ...prevValue ];
const index = newValue.findIndex(value => (value.userId === userId));
if(index >= 0) newValue.splice(index, 1);
return newValue;
});
SendMessageComposer(new RoomUnbanUserComposer(userId, roomData.roomId));
setSelectedUserId(-1);
};
useMessageEvent<BannedUsersFromRoomEvent>(BannedUsersFromRoomEvent, event =>
{
const parser = event.getParser();
if(!roomData || (roomData.roomId !== parser.roomId)) return;
setBannedUsers(parser.bannedUsers);
});
useEffect(() =>
{
SendMessageComposer(new RoomBannedUsersComposer(roomData.roomId));
}, [ roomData.roomId ]);
return (
<Grid overflow="auto">
<Column size={ 6 }>
<Text small bold>{ LocalizeText('navigator.roomsettings.moderation.banned.users') } ({ bannedUsers.length })</Text>
<Flex className="bg-white rounded list-container p-2" overflow="hidden">
<Column fullWidth gap={ 1 } overflow="auto">
{ bannedUsers && (bannedUsers.length > 0) && bannedUsers.map((user, index) =>
{
return (
<Flex key={ index } shrink alignItems="center" gap={ 1 } overflow="hidden">
<UserProfileIconView userName={ user.userName } />
<Text small grow pointer onClick={ event => setSelectedUserId(user.userId) }> { user.userName }</Text>
</Flex>
);
}) }
</Column>
</Flex>
<Button disabled={ (selectedUserId <= 0) } onClick={ event => unBanUser(selectedUserId) }>
{ LocalizeText('navigator.roomsettings.moderation.unban') } { selectedUserId > 0 && bannedUsers.find(user => (user.userId === selectedUserId))?.userName }
</Button>
</Column>
<Column size={ 6 }>
<div className="flex flex-col gap-1">
<Text small bold>{ LocalizeText('navigator.roomsettings.moderation.mute.header') }</Text>
<div className="flex items-center gap-1">
<select className="form-select form-select-sm" value={ roomData.moderationSettings.allowMute } onChange={ event => handleChange('moderation_mute', event.target.value) }>
<option value={ RoomModerationSettings.MODERATION_LEVEL_NONE }>
{ LocalizeText('navigator.roomsettings.moderation.none') }
</option>
<option value={ RoomModerationSettings.MODERATION_LEVEL_USER_WITH_RIGHTS }>
{ LocalizeText('navigator.roomsettings.moderation.rights') }
</option>
</select>
</div>
</div>
<div className="flex flex-col gap-1">
<Text small bold>{ LocalizeText('navigator.roomsettings.moderation.kick.header') }</Text>
<div className="flex items-center gap-1">
<select className="form-select form-select-sm" value={ roomData.moderationSettings.allowKick } onChange={ event => handleChange('moderation_kick', event.target.value) }>
<option value={ RoomModerationSettings.MODERATION_LEVEL_NONE }>
{ LocalizeText('navigator.roomsettings.moderation.none') }
</option>
<option value={ RoomModerationSettings.MODERATION_LEVEL_USER_WITH_RIGHTS }>
{ LocalizeText('navigator.roomsettings.moderation.rights') }
</option>
<option value={ RoomModerationSettings.MODERATION_LEVEL_ALL }>
{ LocalizeText('navigator.roomsettings.moderation.all') }
</option>
</select>
</div>
</div>
<div className="flex flex-col gap-1">
<Text small bold>{ LocalizeText('navigator.roomsettings.moderation.ban.header') }</Text>
<div className="flex items-center gap-1">
<select className="form-select form-select-sm" value={ roomData.moderationSettings.allowBan } onChange={ event => handleChange('moderation_ban', event.target.value) }>
<option value={ RoomModerationSettings.MODERATION_LEVEL_NONE }>
{ LocalizeText('navigator.roomsettings.moderation.none') }
</option>
<option value={ RoomModerationSettings.MODERATION_LEVEL_USER_WITH_RIGHTS }>
{ LocalizeText('navigator.roomsettings.moderation.rights') }
</option>
</select>
</div>
</div>
</Column>
</Grid>
);
};
@@ -0,0 +1,91 @@
import { FlatControllerAddedEvent, FlatControllerRemovedEvent, FlatControllersEvent, RemoveAllRightsMessageComposer, RoomTakeRightsComposer, RoomUsersWithRightsComposer } from '@nitrots/nitro-renderer';
import { FC, useEffect, useState } from 'react';
import { IRoomData, LocalizeText, SendMessageComposer } from '../../../../api';
import { Button, Column, Flex, Grid, Text, UserProfileIconView } from '../../../../common';
import { useMessageEvent } from '../../../../hooks';
interface NavigatorRoomSettingsTabViewProps
{
roomData: IRoomData;
handleChange: (field: string, value: string | number | boolean) => void;
}
export const NavigatorRoomSettingsRightsTabView: FC<NavigatorRoomSettingsTabViewProps> = props =>
{
const { roomData = null } = props;
const [ usersWithRights, setUsersWithRights ] = useState<Map<number, string>>(new Map());
useMessageEvent<FlatControllersEvent>(FlatControllersEvent, event =>
{
const parser = event.getParser();
if(!roomData || (roomData.roomId !== parser.roomId)) return;
setUsersWithRights(parser.users);
});
useMessageEvent<FlatControllerAddedEvent>(FlatControllerAddedEvent, event =>
{
const parser = event.getParser();
if(!roomData || (roomData.roomId !== parser.roomId)) return;
setUsersWithRights(prevValue =>
{
const newValue = new Map(prevValue);
newValue.set(parser.data.userId, parser.data.userName);
return newValue;
});
});
useMessageEvent<FlatControllerRemovedEvent>(FlatControllerRemovedEvent, event =>
{
const parser = event.getParser();
if(!roomData || (roomData.roomId !== parser.roomId)) return;
setUsersWithRights(prevValue =>
{
const newValue = new Map(prevValue);
newValue.delete(parser.userId);
return newValue;
});
});
useEffect(() =>
{
SendMessageComposer(new RoomUsersWithRightsComposer(roomData.roomId));
}, [ roomData.roomId ]);
return (
<Grid>
<Column size={ 6 }>
<Text small bold>
{ LocalizeText('navigator.flatctrls.userswithrights', [ 'displayed', 'total' ], [ usersWithRights.size.toString(), usersWithRights.size.toString() ]) }
</Text>
<Flex className="bg-white rounded list-container p-2" overflow="hidden">
<Column fullWidth gap={ 1 } overflow="auto">
{ Array.from(usersWithRights.entries()).map(([ id, name ], index) =>
{
return (
<Flex key={ index } shrink alignItems="center" gap={ 1 } overflow="hidden">
<UserProfileIconView userName={ name } />
<Text small grow pointer onClick={ event => SendMessageComposer(new RoomTakeRightsComposer(id)) }> { name }</Text>
</Flex>
);
}) }
</Column>
</Flex>
</Column>
<Column justifyContent="end" size={ 6 }>
<Button disabled={ !usersWithRights.size } variant="danger" onClick={ event => SendMessageComposer(new RemoveAllRightsMessageComposer(roomData.roomId)) } >
{ LocalizeText('navigator.flatctrls.clear') }
</Button>
</Column>
</Grid>
);
};
@@ -0,0 +1,206 @@
import { RoomBannedUsersComposer, RoomDataParser, RoomSettingsDataEvent, SaveRoomSettingsComposer } from '@nitrots/nitro-renderer';
import { FC, useState } from 'react';
import { IRoomData, LocalizeText, SendMessageComposer } from '../../../../api';
import { NitroCardContentView, NitroCardHeaderView, NitroCardTabsItemView, NitroCardTabsView, NitroCardView } from '../../../../common';
import { useMessageEvent } from '../../../../hooks';
import { NavigatorRoomSettingsAccessTabView } from './NavigatorRoomSettingsAccessTabView';
import { NavigatorRoomSettingsBasicTabView } from './NavigatorRoomSettingsBasicTabView';
import { NavigatorRoomSettingsModTabView } from './NavigatorRoomSettingsModTabView';
import { NavigatorRoomSettingsRightsTabView } from './NavigatorRoomSettingsRightsTabView';
import { NavigatorRoomSettingsVipChatTabView } from './NavigatorRoomSettingsVipChatTabView';
const TABS: string[] = [
'navigator.roomsettings.tab.1',
'navigator.roomsettings.tab.2',
'navigator.roomsettings.tab.3',
'navigator.roomsettings.tab.4',
'navigator.roomsettings.tab.5'
];
export const NavigatorRoomSettingsView: FC<{}> = props =>
{
const [ roomData, setRoomData ] = useState<IRoomData>(null);
const [ currentTab, setCurrentTab ] = useState(TABS[0]);
useMessageEvent<RoomSettingsDataEvent>(RoomSettingsDataEvent, event =>
{
const parser = event.getParser();
if(!parser) return;
const data = parser.data;
setRoomData({
roomId: data.roomId,
roomName: data.name,
roomDescription: data.description,
categoryId: data.categoryId,
userCount: data.maximumVisitorsLimit,
tags: data.tags,
tradeState: data.tradeMode,
allowWalkthrough: data.allowWalkThrough,
lockState: data.doorMode,
password: null,
allowPets: data.allowPets,
allowPetsEat: data.allowFoodConsume,
hideWalls: data.hideWalls,
wallThickness: data.wallThickness,
floorThickness: data.floorThickness,
chatSettings: {
mode: data.chatSettings.mode,
weight: data.chatSettings.weight,
speed: data.chatSettings.speed,
distance: data.chatSettings.distance,
protection: data.chatSettings.protection
},
moderationSettings: {
allowMute: data.roomModerationSettings.allowMute,
allowKick: data.roomModerationSettings.allowKick,
allowBan: data.roomModerationSettings.allowBan
}
});
SendMessageComposer(new RoomBannedUsersComposer(data.roomId));
});
const onClose = () =>
{
setRoomData(null);
setCurrentTab(TABS[0]);
};
const handleChange = (field: string, value: string | number | boolean | string[]) =>
{
setRoomData(prevValue =>
{
const newValue = { ...prevValue };
switch(field)
{
case 'name':
newValue.roomName = String(value);
break;
case 'description':
newValue.roomDescription = String(value);
break;
case 'category':
newValue.categoryId = Number(value);
break;
case 'max_visitors':
newValue.userCount = Number(value);
break;
case 'trade_state':
newValue.tradeState = Number(value);
break;
case 'tags':
newValue.tags = value as Array<string>;
break;
case 'allow_walkthrough':
newValue.allowWalkthrough = Boolean(value);
break;
case 'allow_pets':
newValue.allowPets = Boolean(value);
break;
case 'allow_pets_eat':
newValue.allowPetsEat = Boolean(value);
break;
case 'hide_walls':
newValue.hideWalls = Boolean(value);
break;
case 'wall_thickness':
newValue.wallThickness = Number(value);
break;
case 'floor_thickness':
newValue.floorThickness = Number(value);
break;
case 'lock_state':
newValue.lockState = Number(value);
break;
case 'password':
newValue.lockState = RoomDataParser.PASSWORD_STATE;
newValue.password = String(value);
break;
case 'moderation_mute':
newValue.moderationSettings.allowMute = Number(value);
break;
case 'moderation_kick':
newValue.moderationSettings.allowKick = Number(value);
break;
case 'moderation_ban':
newValue.moderationSettings.allowBan = Number(value);
break;
case 'bubble_mode':
newValue.chatSettings.mode = Number(value);
break;
case 'chat_weight':
newValue.chatSettings.weight = Number(value);
break;
case 'bubble_speed':
newValue.chatSettings.speed = Number(value);
break;
case 'flood_protection':
newValue.chatSettings.protection = Number(value);
break;
case 'chat_distance':
newValue.chatSettings.distance = Number(value);
break;
}
SendMessageComposer(
new SaveRoomSettingsComposer(
newValue.roomId,
newValue.roomName,
newValue.roomDescription,
newValue.lockState,
newValue.password,
newValue.userCount,
newValue.categoryId,
newValue.tags.length,
newValue.tags,
newValue.tradeState,
newValue.allowPets,
newValue.allowPetsEat,
newValue.allowWalkthrough,
newValue.hideWalls,
newValue.wallThickness,
newValue.floorThickness,
newValue.moderationSettings.allowMute,
newValue.moderationSettings.allowKick,
newValue.moderationSettings.allowBan,
newValue.chatSettings.mode,
newValue.chatSettings.weight,
newValue.chatSettings.speed,
newValue.chatSettings.distance,
newValue.chatSettings.protection
));
return newValue;
});
};
if(!roomData) return null;
return (
<NitroCardView className="nitro-room-settings" uniqueKey="nitro-room-settings">
<NitroCardHeaderView headerText={ LocalizeText('navigator.roomsettings') } onCloseClick={ onClose } />
<NitroCardTabsView>
{ TABS.map(tab =>
{
return <NitroCardTabsItemView key={ tab } isActive={ (currentTab === tab) } onClick={ event => setCurrentTab(tab) }>{ LocalizeText(tab) }</NitroCardTabsItemView>;
}) }
</NitroCardTabsView>
<NitroCardContentView>
{ (currentTab === TABS[0]) &&
<NavigatorRoomSettingsBasicTabView handleChange={ handleChange } roomData={ roomData } onClose={ onClose } /> }
{ (currentTab === TABS[1]) &&
<NavigatorRoomSettingsAccessTabView handleChange={ handleChange } roomData={ roomData } /> }
{ (currentTab === TABS[2]) &&
<NavigatorRoomSettingsRightsTabView handleChange={ handleChange } roomData={ roomData } /> }
{ (currentTab === TABS[3]) &&
<NavigatorRoomSettingsVipChatTabView handleChange={ handleChange } roomData={ roomData } /> }
{ (currentTab === TABS[4]) &&
<NavigatorRoomSettingsModTabView handleChange={ handleChange } roomData={ roomData } /> }
</NitroCardContentView>
</NitroCardView>
);
};
@@ -0,0 +1,77 @@
import { RoomChatSettings } from '@nitrots/nitro-renderer';
import { FC, useEffect, useState } from 'react';
import { IRoomData, LocalizeText } from '../../../../api';
import { Column, Grid, Text } from '../../../../common';
import { NitroInput } from '../../../../layout';
interface NavigatorRoomSettingsTabViewProps
{
roomData: IRoomData;
handleChange: (field: string, value: string | number | boolean) => void;
}
export const NavigatorRoomSettingsVipChatTabView: FC<NavigatorRoomSettingsTabViewProps> = props =>
{
const { roomData = null, handleChange = null } = props;
const [ chatDistance, setChatDistance ] = useState<number>(0);
useEffect(() =>
{
setChatDistance(roomData.chatSettings.distance);
}, [ roomData.chatSettings ]);
return (
<>
<div className="flex flex-col gap-1">
<Text small bold>{ LocalizeText('navigator.roomsettings.vip.caption') }</Text>
<Text small>{ LocalizeText('navigator.roomsettings.vip.info') }</Text>
</div>
<Grid overflow="auto">
<Column gap={ 1 } size={ 6 }>
<Text small bold>{ LocalizeText('navigator.roomsettings.chat_settings') }</Text>
<Text small>{ LocalizeText('navigator.roomsettings.chat_settings.info') }</Text>
<select className="form-select form-select-sm" value={ roomData.chatSettings.mode } onChange={ event => handleChange('bubble_mode', event.target.value) }>
<option value={ RoomChatSettings.CHAT_MODE_FREE_FLOW }>{ LocalizeText('navigator.roomsettings.chat.mode.free.flow') }</option>
<option value={ RoomChatSettings.CHAT_MODE_LINE_BY_LINE }>{ LocalizeText('navigator.roomsettings.chat.mode.line.by.line') }</option>
</select>
<select className="form-select form-select-sm" value={ roomData.chatSettings.weight } onChange={ event => handleChange('chat_weight', event.target.value) }>
<option value={ RoomChatSettings.CHAT_BUBBLE_WIDTH_NORMAL }>{ LocalizeText('navigator.roomsettings.chat.bubbles.width.normal') }</option>
<option value={ RoomChatSettings.CHAT_BUBBLE_WIDTH_THIN }>{ LocalizeText('navigator.roomsettings.chat.bubbles.width.thin') }</option>
<option value={ RoomChatSettings.CHAT_BUBBLE_WIDTH_WIDE }>{ LocalizeText('navigator.roomsettings.chat.bubbles.width.wide') }</option>
</select>
<select className="form-select form-select-sm" value={ roomData.chatSettings.speed } onChange={ event => handleChange('bubble_speed', event.target.value) }>
<option value={ RoomChatSettings.CHAT_SCROLL_SPEED_FAST }>{ LocalizeText('navigator.roomsettings.chat.speed.fast') }</option>
<option value={ RoomChatSettings.CHAT_SCROLL_SPEED_NORMAL }>{ LocalizeText('navigator.roomsettings.chat.speed.normal') }</option>
<option value={ RoomChatSettings.CHAT_SCROLL_SPEED_SLOW }>{ LocalizeText('navigator.roomsettings.chat.speed.slow') }</option>
</select>
<select className="form-select form-select-sm" value={ roomData.chatSettings.protection } onChange={ event => handleChange('flood_protection', event.target.value) }>
<option value={ RoomChatSettings.FLOOD_FILTER_LOOSE }>{ LocalizeText('navigator.roomsettings.chat.flood.loose') }</option>
<option value={ RoomChatSettings.FLOOD_FILTER_NORMAL }>{ LocalizeText('navigator.roomsettings.chat.flood.normal') }</option>
<option value={ RoomChatSettings.FLOOD_FILTER_STRICT }>{ LocalizeText('navigator.roomsettings.chat.flood.strict') }</option>
</select>
<Text small>{ LocalizeText('navigator.roomsettings.chat_settings.hearing.distance') }</Text>
<NitroInput className="form-control-sm" min="0" type="number" value={ chatDistance } onBlur={ event => handleChange('chat_distance', chatDistance) } onChange={ event => setChatDistance(event.target.valueAsNumber) } />
</Column>
<Column gap={ 1 } size={ 6 }>
<Text small bold>{ LocalizeText('navigator.roomsettings.vip_settings') }</Text>
<div className="flex items-center gap-1">
<input checked={ roomData.hideWalls } className="form-check-input" type="checkbox" onChange={ event => handleChange('hide_walls', event.target.checked) } />
<Text small>{ LocalizeText('navigator.roomsettings.hide_walls') }</Text>
</div>
<select className="form-select form-select-sm" value={ roomData.wallThickness } onChange={ event => handleChange('wall_thickness', event.target.value) }>
<option value="0">{ LocalizeText('navigator.roomsettings.wall_thickness.normal') }</option>
<option value="1">{ LocalizeText('navigator.roomsettings.wall_thickness.thick') }</option>
<option value="-1">{ LocalizeText('navigator.roomsettings.wall_thickness.thin') }</option>
<option value="-2">{ LocalizeText('navigator.roomsettings.wall_thickness.thinnest') }</option>
</select>
<select className="form-select form-select-sm" value={ roomData.floorThickness } onChange={ event => handleChange('floor_thickness', event.target.value) }>
<option value="0">{ LocalizeText('navigator.roomsettings.floor_thickness.normal') }</option>
<option value="1">{ LocalizeText('navigator.roomsettings.floor_thickness.thick') }</option>
<option value="-1">{ LocalizeText('navigator.roomsettings.floor_thickness.thin') }</option>
<option value="-2">{ LocalizeText('navigator.roomsettings.floor_thickness.thinnest') }</option>
</select>
</Column>
</Grid>
</>
);
};
@@ -0,0 +1,106 @@
import { RoomDataParser } from '@nitrots/nitro-renderer';
import { FC, useRef, useState } from 'react';
import { FaUser } from 'react-icons/fa';
import { ArrowContainer, Popover } from 'react-tiny-popover';
import { LocalizeText } from '../../../../api';
import { Flex, LayoutBadgeImageView, LayoutRoomThumbnailView, NitroCardContentView, Text, UserProfileIconView } from '../../../../common';
export const NavigatorSearchResultItemInfoView: FC<{
roomData: RoomDataParser;
}> = props =>
{
const { roomData = null } = props;
const [ isVisible, setIsVisible ] = useState(false);
const elementRef = useRef<HTMLDivElement>();
const getUserCounterColor = () =>
{
const num: number = (100 * (roomData.userCount / roomData.maxUserCount));
let bg = 'bg-primary';
if(num >= 92)
{
bg = 'bg-danger';
}
else if(num >= 50)
{
bg = 'bg-warning';
}
else if(num > 0)
{
bg = 'bg-success';
}
return bg;
};
function dispatch(arg0: string): void
{
throw new Error('Function not implemented.');
}
return (
<>
<Popover
ref={ elementRef } // if you'd like a ref to your popover's child, you can grab one here
containerClassName="max-w-[276px] not-italic font-normal leading-normal text-left no-underline [text-shadow:none] normal-case tracking-[normal] [word-break:normal] [word-spacing:normal] whitespace-normal text-[.7875rem] [word-wrap:break-word] bg-[#dfdfdf] bg-clip-padding border-[1px] border-[solid] border-[#283F5D] rounded-[.25rem] [box-shadow:0_2px_#00000073] z-[1070]"
content={ ({ position, childRect, popoverRect }) => (
<ArrowContainer // if you'd like an arrow, you can import the ArrowContainer!
arrowColor={ 'black' }
arrowSize={ 7 }
arrowStyle={ { left: 'calc(-.5rem - 0px)' } }
childRect={ childRect }
popoverRect={ popoverRect }
position={ position }
>
<NitroCardContentView className="bg-transparent room-info image-rendering-pixelated" overflow="hidden">
<Flex gap={ 2 } overflow="hidden">
<LayoutRoomThumbnailView className="flex flex-col items-center mb-1 justify-content-end" customUrl={ roomData.officialRoomPicRef } roomId={ roomData.roomId }>
{ roomData.habboGroupId > 0 && (
<LayoutBadgeImageView badgeCode={ roomData.groupBadgeCode } className={ 'absolute top-0 start-0 m-1 ' } isGroup={ true } />) }
{ roomData.doorMode !== RoomDataParser.OPEN_STATE && (
<i className={ 'absolute end-0 mb-1 me-1 icon icon-navigator-room-' + (roomData.doorMode === RoomDataParser.DOORBELL_STATE ? 'locked' : roomData.doorMode === RoomDataParser.PASSWORD_STATE ? 'password' : roomData.doorMode === RoomDataParser.INVISIBLE_STATE ? 'invisible' : '') } />) }
</LayoutRoomThumbnailView>
<div className="flex flex-col gap-1">
<Text bold truncate className="flex-grow-1" style={ { maxHeight: 13 } }>
{ roomData.roomName }
</Text>
<div className="flex gap-2">
<Text italics variant="muted">
{ LocalizeText('navigator.roomownercaption') }
</Text>
<div className="flex items-center gap-1">
<UserProfileIconView userId={ roomData.ownerId } />
<Text italics>{ roomData.ownerName }</Text>
</div>
</div>
<Text className="flex-grow-1">
{ roomData.description }
</Text>
<Flex className={ 'badge p-1 absolute m-1 bottom-0 end-0 m-2 ' + getUserCounterColor() } gap={ 1 }>
<FaUser className="fa-icon" />
{ roomData.userCount }
</Flex>
</div>
</Flex>
</NitroCardContentView>
</ArrowContainer>
) }
isOpen={ isVisible }
padding={ 10 }
positions={ [ 'right' ] }
>
<div ref={ elementRef } className="cursor-pointer nitro-icon icon-navigator-info" onMouseLeave={ event => setIsVisible(false) } onMouseOver={ event => setIsVisible(true) } />
</Popover>
</>
);
};
@@ -0,0 +1,121 @@
import { GetSessionDataManager, RoomDataParser } from '@nitrots/nitro-renderer';
import { FC, MouseEvent } from 'react';
import { FaUser } from 'react-icons/fa';
import { CreateRoomSession, DoorStateType, TryVisitRoom } from '../../../../api';
import { Column, Flex, LayoutBadgeImageView, LayoutGridItemProps, LayoutRoomThumbnailView, Text } from '../../../../common';
import { useNavigator } from '../../../../hooks';
import { NavigatorSearchResultItemInfoView } from './NavigatorSearchResultItemInfoView';
export interface NavigatorSearchResultItemViewProps extends LayoutGridItemProps
{
roomData: RoomDataParser
thumbnail?: boolean
}
export const NavigatorSearchResultItemView: FC<NavigatorSearchResultItemViewProps> = props =>
{
const { roomData = null, children = null, thumbnail = false, ...rest } = props;
const { setDoorData = null } = useNavigator();
const getUserCounterColor = () =>
{
const num: number = (100 * (roomData.userCount / roomData.maxUserCount));
let bg = 'bg-primary';
if(num >= 92)
{
bg = 'bg-danger';
}
else if(num >= 50)
{
bg = 'bg-warning';
}
else if(num > 0)
{
bg = 'bg-success';
}
return bg;
};
const visitRoom = (event: MouseEvent) =>
{
if(roomData.ownerId !== GetSessionDataManager().userId)
{
if(roomData.habboGroupId !== 0)
{
TryVisitRoom(roomData.roomId);
return;
}
switch(roomData.doorMode)
{
case RoomDataParser.DOORBELL_STATE:
setDoorData(prevValue =>
{
const newValue = { ...prevValue };
newValue.roomInfo = roomData;
newValue.state = DoorStateType.START_DOORBELL;
return newValue;
});
return;
case RoomDataParser.PASSWORD_STATE:
setDoorData(prevValue =>
{
const newValue = { ...prevValue };
newValue.roomInfo = roomData;
newValue.state = DoorStateType.START_PASSWORD;
return newValue;
});
return;
}
}
CreateRoomSession(roomData.roomId);
};
if(thumbnail) return (
<Column pointer alignItems="center" className="navigator-item p-1 bg-light rounded-3 small mb-1 flex-col border border-muted" gap={ 0 } overflow="hidden" onClick={ visitRoom } { ...rest }>
<LayoutRoomThumbnailView className="flex flex-col items-center justify-end mb-1" customUrl={ roomData.officialRoomPicRef } roomId={ roomData.roomId }>
{ roomData.habboGroupId > 0 && <LayoutBadgeImageView badgeCode={ roomData.groupBadgeCode } className={ 'absolute top-0 start-0 m-1' } isGroup={ true } /> }
<Flex center className={ 'inline-block px-[.65em] py-[.35em] text-[.75em] font-bold leading-none text-[#fff] text-center whitespace-nowrap align-baseline rounded-[.25rem] p-1 absolute m-1 ' + getUserCounterColor() } gap={ 1 }>
<FaUser className="fa-icon" />
{ roomData.userCount }
</Flex>
{ (roomData.doorMode !== RoomDataParser.OPEN_STATE) &&
<i className={ ('absolute end-0 mb-1 me-1 icon icon-navigator-room-' + ((roomData.doorMode === RoomDataParser.DOORBELL_STATE) ? 'locked' : (roomData.doorMode === RoomDataParser.PASSWORD_STATE) ? 'password' : (roomData.doorMode === RoomDataParser.INVISIBLE_STATE) ? 'invisible' : '')) } /> }
</LayoutRoomThumbnailView>
<Flex className="w-full">
<Text truncate className="!flex-grow">{ roomData.roomName }</Text>
<Flex reverse alignItems="center" gap={ 1 }>
<NavigatorSearchResultItemInfoView roomData={ roomData } />
</Flex>
{ children }
</Flex>
</Column>
);
return (
<Flex pointer alignItems="center" className="navigator-item px-2 py-1 small" gap={ 2 } overflow="hidden" onClick={ visitRoom } { ...rest }>
<Flex center className={ 'inline-block px-[.65em] py-[.35em] text-[.75em] font-bold leading-none text-[#fff] text-center whitespace-nowrap align-baseline rounded-[.25rem] p-1 ' + getUserCounterColor() } gap={ 1 }>
<FaUser className="fa-icon" />
{ roomData.userCount }
</Flex>
<Text grow truncate>{ roomData.roomName }</Text>
<Flex reverse alignItems="center" gap={ 1 }>
<NavigatorSearchResultItemInfoView roomData={ roomData } />
{ roomData.habboGroupId > 0 && <i className="nitro-icon icon-navigator-room-group" /> }
{ (roomData.doorMode !== RoomDataParser.OPEN_STATE) &&
<i className={ ('nitro-icon icon-navigator-room-' + ((roomData.doorMode === RoomDataParser.DOORBELL_STATE) ? 'locked' : (roomData.doorMode === RoomDataParser.PASSWORD_STATE) ? 'password' : (roomData.doorMode === RoomDataParser.INVISIBLE_STATE) ? 'invisible' : '')) } /> }
</Flex>
{ children }
</Flex>
);
};
@@ -0,0 +1,118 @@
import { NavigatorSearchComposer, NavigatorSearchResultList } from '@nitrots/nitro-renderer';
import { FC, useEffect, useState } from 'react';
import { FaBars, FaMinus, FaPlus, FaTh, FaWindowMaximize, FaWindowRestore } from 'react-icons/fa';
import { LocalizeText, NavigatorSearchResultViewDisplayMode, SendMessageComposer } from '../../../../api';
import { AutoGrid, AutoGridProps, Column, Flex, Grid, Text } from '../../../../common';
import { useNavigator } from '../../../../hooks';
import { NavigatorSearchResultItemView } from './NavigatorSearchResultItemView';
export interface NavigatorSearchResultViewProps extends AutoGridProps
{
searchResult: NavigatorSearchResultList;
}
export const NavigatorSearchResultView: FC<NavigatorSearchResultViewProps> = props =>
{
const { searchResult = null, ...rest } = props;
const [ isExtended, setIsExtended ] = useState(true);
const [ displayMode, setDisplayMode ] = useState<number>(0);
const { topLevelContext = null } = useNavigator();
const getResultTitle = () =>
{
let name = searchResult.code;
if(!name || !name.length || LocalizeText('navigator.searchcode.title.' + name) == ('navigator.searchcode.title.' + name)) return searchResult.data;
if(name.startsWith('${')) return name.slice(2, (name.length - 1));
return ('navigator.searchcode.title.' + name);
};
const toggleDisplayMode = () =>
{
setDisplayMode(prevValue =>
{
if(prevValue === NavigatorSearchResultViewDisplayMode.LIST) return NavigatorSearchResultViewDisplayMode.THUMBNAILS;
return NavigatorSearchResultViewDisplayMode.LIST;
});
};
const showMore = () =>
{
if(searchResult.action == 1) SendMessageComposer(new NavigatorSearchComposer(searchResult.code, ''));
else if(searchResult.action == 2 && topLevelContext) SendMessageComposer(new NavigatorSearchComposer(topLevelContext.code, ''));
};
useEffect(() =>
{
if(!searchResult) return;
setIsExtended(!searchResult.closed);
setDisplayMode(searchResult.mode);
}, [ searchResult ]);
const gridHasTwoColumns = (displayMode >= NavigatorSearchResultViewDisplayMode.THUMBNAILS);
return (
<Column className="bg-white rounded border border-muted" gap={ 0 }>
<Flex fullWidth alignItems="center" className="px-2 py-1" justifyContent="between">
<Flex grow pointer alignItems="center" gap={ 1 } onClick={ event => setIsExtended(prevValue => !prevValue) }>
{ isExtended && <FaMinus className="text-secondary fa-icon" /> }
{ !isExtended && <FaPlus className="text-secondary fa-icon" /> }
<Text>{ LocalizeText(getResultTitle()) }</Text>
</Flex>
<div className="flex gap-2">
{ (displayMode === NavigatorSearchResultViewDisplayMode.LIST) && <FaTh className="text-secondary fa-icon" onClick={ toggleDisplayMode } /> }
{ (displayMode >= NavigatorSearchResultViewDisplayMode.THUMBNAILS) && <FaBars className="text-secondary fa-icon" onClick={ toggleDisplayMode } /> }
{ (searchResult.action > 0) && (searchResult.action === 1) && <FaWindowMaximize className="text-secondary fa-icon" onClick={ showMore } /> }
{ (searchResult.action > 0) && (searchResult.action !== 1) && <FaWindowRestore className="text-secondary fa-icon" onClick={ showMore } /> }
</div>
</Flex> { isExtended &&
<>
{
gridHasTwoColumns ? <AutoGrid columnCount={ 3 } { ...rest } className="mx-2" columnMinHeight={ 130 } columnMinWidth={ 110 }>
{ searchResult.rooms.length > 0 && searchResult.rooms.map((room, index) => <NavigatorSearchResultItemView key={ index } roomData={ room } thumbnail={ true } />) }
</AutoGrid> : <Grid className="navigator-grid" columnCount={ 1 } gap={ 0 }>
{ searchResult.rooms.length > 0 && searchResult.rooms.map((room, index) => <NavigatorSearchResultItemView key={ index } roomData={ room } />) }
</Grid>
}
</>
}
</Column>
// <div className="nitro-navigator-search-result bg-white rounded mb-2 overflow-hidden">
// <div className="flex flex-col">
// <div className="flex items-center px-2 py-1 text-black">
// <i className={ 'text-secondary fas ' + (isExtended ? 'fa-minus' : 'fa-plus') } onClick={ toggleExtended }></i>
// <div className="ms-2 !flex-grow">{ LocalizeText(getResultTitle()) }</div>
// <i className={ 'text-secondary fas ' + classNames({ 'fa-bars': (displayMode === NavigatorSearchResultViewDisplayMode.LIST), 'fa-th': displayMode >= NavigatorSearchResultViewDisplayMode.THUMBNAILS })}></i>
// </div>
// { isExtended &&
// <div className={ 'nitro-navigator-result-list row row-cols-' + classNames({ '1': (displayMode === NavigatorSearchResultViewDisplayMode.LIST), '2': (displayMode >= NavigatorSearchResultViewDisplayMode.THUMBNAILS) }) }>
// { searchResult.rooms.length > 0 && searchResult.rooms.map((room, index) =>
// {
// return <NavigatorSearchResultItemView key={ index } roomData={ room } />
// }) }
// </div> }
// </div>
// </div>
// <div className="nitro-navigator-result-list p-2">
// <div className="flex mb-2 small cursor-pointer" onClick={ toggleList }>
// <i className={ "fas " + classNames({ 'fa-plus': !isExtended, 'fa-minus': isExtended })}></i>
// <div className="align-self-center w-full ml-2">{ LocalizeText(getListCode()) }</div>
// <i className={ "fas " + classNames({ 'fa-bars': displayMode === NavigatorResultListViewDisplayMode.LIST, 'fa-th': displayMode >= NavigatorResultListViewDisplayMode.THUMBNAILS })} onClick={ toggleDisplayMode }></i>
// </div>
// <div className={ 'row mr-n2 row-cols-' + classNames({ '1': displayMode === NavigatorResultListViewDisplayMode.LIST, '2': displayMode >= NavigatorResultListViewDisplayMode.THUMBNAILS }) }>
// { isExtended && resultList && resultList.rooms.map((room, index) =>
// {
// return <NavigatorResultView key={ index } result={ room } />
// })
// }
// </div>
// </div>
);
};
@@ -0,0 +1,81 @@
import { FC, KeyboardEvent, useEffect, useState } from 'react';
import { FaSearch } from 'react-icons/fa';
import { INavigatorSearchFilter, LocalizeText, SearchFilterOptions } from '../../../../api';
import { Button } from '../../../../common';
import { useNavigator } from '../../../../hooks';
export const NavigatorSearchView: FC<{
sendSearch: (searchValue: string, contextCode: string) => void;
}> = props =>
{
const { sendSearch = null } = props;
const [ searchFilterIndex, setSearchFilterIndex ] = useState(0);
const [ searchValue, setSearchValue ] = useState('');
const { topLevelContext = null, searchResult = null } = useNavigator();
const processSearch = () =>
{
if(!topLevelContext) return;
let searchFilter = SearchFilterOptions[searchFilterIndex];
if(!searchFilter) searchFilter = SearchFilterOptions[0];
const searchQuery = ((searchFilter.query ? (searchFilter.query + ':') : '') + searchValue);
sendSearch((searchQuery || ''), topLevelContext.code);
};
const handleKeyDown = (event: KeyboardEvent<HTMLInputElement>) =>
{
if(event.key !== 'Enter') return;
processSearch();
};
useEffect(() =>
{
if(!searchResult) return;
const split = searchResult.data.split(':');
let filter: INavigatorSearchFilter = null;
let value: string = '';
if(split.length >= 2)
{
const [ query, ...rest ] = split;
filter = SearchFilterOptions.find(option => (option.query === query));
value = rest.join(':');
}
else
{
value = searchResult.data;
}
if(!filter) filter = SearchFilterOptions[0];
setSearchFilterIndex(SearchFilterOptions.findIndex(option => (option === filter)));
setSearchValue(value);
}, [ searchResult ]);
return (
<div className="flex w-full gap-1">
<div className="flex shrink-0">
<select className="form-select" value={ searchFilterIndex } onChange={ event => setSearchFilterIndex(parseInt(event.target.value)) }>
{ SearchFilterOptions.map((filter, index) =>
{
return <option key={ index } value={ index }>{ LocalizeText('navigator.filter.' + filter.name) }</option>;
}) }
</select>
</div>
<div className="flex w-full gap-1">
<input className="w-full form-control" placeholder={ LocalizeText('navigator.filter.input.placeholder') } type="text" value={ searchValue } onChange={ event => setSearchValue(event.target.value) } onKeyDown={ event => handleKeyDown(event) } />
<Button variant="primary" onClick={ processSearch }>
<FaSearch className="fa-icon" />
</Button>
</div>
</div>
);
};