This commit is contained in:
Bill 2021-09-05 19:04:05 -04:00
commit 613916bb85
61 changed files with 1349 additions and 411 deletions

View File

@ -22,6 +22,8 @@
"generic.asset.url": "${asset.url}/bundled/generic/%libname%.nitro",
"badge.asset.url": "${image.library.url}album1584/%badgename%.gif",
"badge.asset.group.url": "https://cdn.ironhotel.biz/group-badge/%badgedata%.gif",
"badge.asset.group.external.url": "",
"badge.asset.grouparts.url": "https://cdn.ironhotel.biz/static_iron_active/c_images/Badgeparts/badgepart_%part%.png",
"furni.rotation.bounce.steps": 20,
"furni.rotation.bounce.height": 0.0625,
"enable.avatar.arrow": false,

View File

@ -0,0 +1,6 @@
import { CreateLinkEvent } from '..';
export function GetGroupManager(groupId: number): void
{
CreateLinkEvent(`groups/manage/${groupId}`);
}

View File

@ -0,0 +1,6 @@
import { CreateLinkEvent } from '..';
export function GetGroupMembers(groupId: number): void
{
CreateLinkEvent(`groups/members/${groupId}`);
}

View File

@ -522,7 +522,7 @@ export class RoomWidgetInfostandHandler extends RoomWidgetHandler
}
event.groupId = parseInt(userData.guildId);
//event._Str_5235 = GetSessionDataManager()._Str_17173(int(userData._Str_4592));
event.groupBadgeId = GetSessionDataManager().getGroupBadge(event.groupId);
event.groupName = userData.groupName;
event.badges = this.container.roomSession.userDataManager.getUserBadges(userData.webID);
event.figure = userData.figure;

Binary file not shown.

After

Width:  |  Height:  |  Size: 198 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 203 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 198 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 168 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 215 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 175 B

View File

@ -390,7 +390,7 @@ $container-max-widths: (
$grid-columns: 12 !default;
$grid-gutter-width: 16px !default;
$grid-row-columns: 6 !default;
$grid-row-columns: 18 !default;
$gutters: $spacers !default;

View File

@ -591,6 +591,42 @@
height: 20px;
}
&.icon-group-favorite {
background: url('../images/groups/icons/group_favorite.png');
width: 14px;
height: 14px;
}
&.icon-group-not-favorite {
background: url('../images/groups/icons/group_notfavorite.png');
width: 14px;
height: 14px;
}
&.icon-group-small-admin {
background: url('../images/groups/icons/group_icon_admin.png');
width: 11px;
height: 13px;
}
&.icon-group-small-not-admin {
background: url('../images/groups/icons/group_icon_not_admin.png');
width: 11px;
height: 13px;
}
&.icon-group-remove-member {
background: url('../images/groups/icons/group_icon_remove_member.png');
width: 13px;
height: 14px;
}
&.icon-group-small-owner {
background: url('../images/groups/icons/group_icon_small_owner.png');
width: 13px;
height: 13px;
}
&.icon-navigator-info {
background: url('../images/navigator/icons/info.png');
width: 18px;

View File

@ -34,5 +34,38 @@
}
}
.row-cols-6 {
.col {
padding-right: 0.25rem;
&:nth-child(6n+6) {
padding-right: 0;
}
}
}
.row-cols-7 {
.col {
padding-right: 0.25rem;
&:nth-child(7n+7) {
padding-right: 0;
}
}
}
.row-cols-8 {
.col {
padding-right: 0.25rem;
&:nth-child(8n+8) {
padding-right: 0;
}
}
}
@import './item/NitroCardGridItemView.scss';
}

View File

@ -5,7 +5,7 @@ import { NitroLayoutTrophyViewProps } from './NitroLayoutTrophyView.types';
export const NitroLayoutTrophyView: FC<NitroLayoutTrophyViewProps> = props =>
{
const { color = '', message = '', date = '', senderName = '', onCloseClick = null } = props;
const { color = '', message = '', date = '', senderName = '', customTitle = null, onCloseClick = null } = props;
return (
<DraggableWindow handleSelector=".drag-handler">
@ -17,6 +17,7 @@ export const NitroLayoutTrophyView: FC<NitroLayoutTrophyViewProps> = props =>
</div>
</div>
<div className="trophy-content">
{ customTitle && <div className="mb-2 fw-bold">{ customTitle }</div> }
{ message }
</div>
<div className="trophy-footer d-flex justify-content-between fw-bold">

View File

@ -4,5 +4,6 @@ export interface NitroLayoutTrophyViewProps
message: string;
date: string;
senderName: string;
customTitle?: string;
onCloseClick: () => void;
}

View File

@ -1,4 +1,54 @@
.nitro-groups {
.tab-image {
width: 122px;
height: 68px;
display: flex;
align-items: center;
justify-content: center;
div {
background-image: url('../../assets/images/groups/creator_images.png');
background-repeat: no-repeat;
}
&.tab-1 {
div {
background-position: 0px 0px;
width: 99px;
height: 50px;
}
}
&.tab-2 {
div {
background-position: -99px 0px;
width: 98px;
height: 62px;
}
}
&.tab-3 {
div {
background-position: 0px -50px;
width: 96px;
height: 45px;
}
}
&.tab-4, &.tab-5 {
div {
background-position: 0px -95px;
width: 114px;
height: 61px;
}
}
}
}
@import './views/creator/GroupCreatorView';
@import './views/information/GroupInformationView';
@import './views/information-standalone/GroupInformationStandaloneView';
@import './views/manager/GroupManagerView';
@import './views/members/GroupMembersView';
@import './views/room-information/GroupRoomInformationView';
@import './views/shared-tabs/GroupSharedTabs';

View File

@ -1,27 +1,141 @@
import { GroupBadgePartsEvent } from '@nitrots/nitro-renderer';
import { GroupBadgePartsEvent, GroupBuyDataEvent, GroupSettingsEvent, RoomCreatedEvent } from '@nitrots/nitro-renderer';
import { FC, useCallback } from 'react';
import { CreateMessageHook } from '../../hooks';
import { GroupBadgePart } from './common/GroupBadgePart';
import { useGroupsContext } from './context/GroupsContext';
import { GroupsActions } from './context/GroupsContext.types';
function compareId(a, b)
{
if( a.id < b.id ) return -1;
if( a.id > b.id ) return 1;
return 0;
}
export const GroupsMessageHandler: FC<{}> = props =>
{
const { groupsState = null, dispatchGroupsState = null } = useGroupsContext();
const { availableRooms = null } = groupsState;
const onGroupBuyDataEvent = useCallback((event: GroupBuyDataEvent) =>
{
const parser = event.getParser();
const rooms: { id: number, name: string }[] = [];
parser.availableRooms.forEach((name, id) =>
{
rooms.push({ id, name });
});
dispatchGroupsState({
type: GroupsActions.SET_PURHCASE_SETTINGS,
payload: {
objectValues: rooms,
numberValues: [ parser.groupCost ]
}
});
}, [ dispatchGroupsState ]);
const onRoomCreatedEvent = useCallback((event: RoomCreatedEvent) =>
{
const parser = event.getParser();
const clonedRooms = Array.from(availableRooms);
clonedRooms.push({
id: parser.roomId,
name: parser.roomName
});
dispatchGroupsState({
type: GroupsActions.SET_PURHCASE_SETTINGS,
payload: {
objectValues: clonedRooms
}
});
}, [ availableRooms, dispatchGroupsState ]);
const onGroupBadgePartsEvent = useCallback((event: GroupBadgePartsEvent) =>
{
const parser = event.getParser();
const bases: { id: number, images: string[] }[] = [];
const symbols: { id: number, images: string[] }[] = [];
const partColors: { id: number, color: string }[] = [];
const colorsA: { id: number, color: string }[] = [];
const colorsB: { id: number, color: string }[] = [];
parser.bases.forEach((images, id) =>
{
bases.push({ id, images });
});
parser.symbols.forEach((images, id) =>
{
symbols.push({ id, images });
});
parser.partColors.forEach((color, id) =>
{
partColors.push({ id, color });
});
parser.colorsA.forEach((color, id) =>
{
colorsA.push({ id, color });
});
parser.colorsB.forEach((color, id) =>
{
colorsB.push({ id, color });
});
bases.sort(compareId);
symbols.sort(compareId);
partColors.sort(compareId);
colorsA.sort(compareId);
colorsB.sort(compareId);
dispatchGroupsState({
type: GroupsActions.SET_BADGE_PARTS,
type: GroupsActions.SET_GROUP_BADGE_PARTS_CONFIG,
payload: {
arrayMaps: [ parser.bases, parser.symbols ],
stringMaps: [ parser.partColors, parser.colorsA, parser.colorsB ]
objectValues: [ bases, symbols, partColors, colorsA, colorsB ]
}
})
});
}, [ dispatchGroupsState ]);
const onGroupSettingsEvent = useCallback((event: GroupSettingsEvent) =>
{
const parser = event.getParser();
const groupBadgeParts: GroupBadgePart[] = [];
parser.badgeParts.forEach((part, id) =>
{
groupBadgeParts.push(new GroupBadgePart(
part.isBase ? GroupBadgePart.BASE : part.key >= 100 ? GroupBadgePart.SYMBOL_ALT : GroupBadgePart.SYMBOL,
part.key >= 100 ? part.key - 100 : part.key,
part.color,
part.position
));
});
dispatchGroupsState({
type: GroupsActions.SET_GROUP_SETTINGS,
payload: {
stringValues: [ parser.title, parser.description ],
numberValues: [ parser.id, parser.state, parser.colorA, parser.colorB ],
boolValues: [ parser.canMembersDecorate ],
objectValues: groupBadgeParts
}
});
}, [ dispatchGroupsState ]);
CreateMessageHook(GroupBuyDataEvent, onGroupBuyDataEvent);
CreateMessageHook(RoomCreatedEvent, onRoomCreatedEvent);
CreateMessageHook(GroupBadgePartsEvent, onGroupBadgePartsEvent);
CreateMessageHook(GroupSettingsEvent, onGroupSettingsEvent);
return null;
};

View File

@ -1,18 +1,22 @@
import { GroupBadgePartsComposer, ILinkEventTracker } from '@nitrots/nitro-renderer';
import { GroupBadgePartsComposer, GroupPurchasedEvent, GroupSettingsComposer, ILinkEventTracker } from '@nitrots/nitro-renderer';
import { FC, useCallback, useEffect, useReducer, useState } from 'react';
import { AddEventLinkTracker, RemoveLinkEventTracker } from '../../api';
import { SendMessageHook } from '../../hooks';
import { AddEventLinkTracker, RemoveLinkEventTracker, TryVisitRoom } from '../../api';
import { CreateMessageHook, SendMessageHook } from '../../hooks';
import { GroupsContextProvider } from './context/GroupsContext';
import { GroupsReducer, initialGroups } from './context/GroupsContext.types';
import { GroupsMessageHandler } from './GroupsMessageHandler';
import { GroupCreatorView } from './views/creator/GroupCreatorView';
import { GroupInformationStandaloneView } from './views/information-standalone/GroupInformationStandaloneView';
import { GroupManagerView } from './views/manager/GroupManagerView';
import { GroupMembersView } from './views/members/GroupMembersView';
export const GroupsView: FC<{}> = props =>
{
const [ groupsState, dispatchGroupsState ] = useReducer(GroupsReducer, initialGroups);
const [ isCreatorVisible, setIsCreatorVisible ] = useState<boolean>(false);
const [ groupMembersId, setGroupMembersId ] = useState<number>(null);
const [ groupMembersLevel, setGroupMembersLevel ] = useState<number>(null);
useEffect(() =>
{
@ -30,6 +34,18 @@ export const GroupsView: FC<{}> = props =>
case 'create':
setIsCreatorVisible(true);
return;
case 'manage':
if(!parts[2]) return;
SendMessageHook(new GroupSettingsComposer(Number(parts[2])));
return;
case 'members':
if(!parts[2]) return;
setGroupMembersId(Number(parts[2]));
if(parts[3]) setGroupMembersLevel(Number(parts[3]));
return;
}
}, []);
@ -45,11 +61,31 @@ export const GroupsView: FC<{}> = props =>
return () => RemoveLinkEventTracker(linkTracker);
}, [ linkReceived ]);
const onGroupPurchasedEvent = useCallback((event: GroupPurchasedEvent) =>
{
const parser = event.getParser();
setIsCreatorVisible(false);
TryVisitRoom(parser.roomId);
}, []);
CreateMessageHook(GroupPurchasedEvent, onGroupPurchasedEvent);
const closeMembers = useCallback(() =>
{
setGroupMembersId(null);
setGroupMembersLevel(null);
}, []);
return (
<GroupsContextProvider value={ { groupsState, dispatchGroupsState } }>
<GroupsMessageHandler />
<div className="nitro-groups">
<GroupCreatorView isVisible={ isCreatorVisible } onClose={ () => setIsCreatorVisible(false) } />
<GroupManagerView />
<GroupMembersView groupId={ groupMembersId } levelId={ groupMembersLevel } onClose={ closeMembers } />
<GroupInformationStandaloneView />
</div>
</GroupsContextProvider>
);
};

View File

@ -9,18 +9,23 @@ export class GroupBadgePart
public color: number;
public position: number;
constructor(type: string)
constructor(type: string, key?: number, color?: number, position?: number)
{
this.type = type;
this.key = 0;
this.color = 0;
this.position = 4;
this.key = key ? key : 0;
this.color = color ? color : 0;
this.position = position ? position : 4;
}
public get code(): string
{
if(this.key === 0) return null;
return this.type + (this.key < 10 ? '0' : '') + this.key + (this.color < 10 ? '0' : '') + this.color + (this.type === GroupBadgePart.BASE ? '' : this.position);
return GroupBadgePart.getCode(this.type, this.key, this.color, this.position);
}
public static getCode(type: string, key: number, color: number, position: number): string
{
return (type === GroupBadgePart.BASE ? type : key >= 100 ? GroupBadgePart.SYMBOL_ALT : GroupBadgePart.SYMBOL) + (key < 10 ? '0' : '') + (type === GroupBadgePart.BASE ? key : key >= 100 ? key - 100 : key) + (color < 10 ? '0' : '') + color + position;
}
}

View File

@ -1,172 +0,0 @@
import { GroupBadgePart } from './GroupBadgePart';
export class GroupSettings
{
private _id: number;
private _name: string;
private _description: string;
private _roomId: string;
private _badgeParts: GroupBadgePart[];
private _colorA: number;
private _colorB: number;
private _state: number;
private _canMembersDecorate: boolean;
constructor()
{
this._id = 0;
this._name = '';
this._description = '';
this._roomId = '0';
this._badgeParts = [];
this._colorA = 0;
this.colorB = 0;
this._state = 0;
this._canMembersDecorate = false;
this._badgeParts.push(new GroupBadgePart(GroupBadgePart.BASE));
this._badgeParts.push(new GroupBadgePart(GroupBadgePart.SYMBOL));
this._badgeParts.push(new GroupBadgePart(GroupBadgePart.SYMBOL));
this._badgeParts.push(new GroupBadgePart(GroupBadgePart.SYMBOL));
this._badgeParts.push(new GroupBadgePart(GroupBadgePart.SYMBOL));
}
public getBadgePart(index: number): GroupBadgePart
{
return this._badgeParts[index];
}
public setPartsColor(color: number): void
{
this._badgeParts.forEach((symbol) =>
{
symbol.color = color;
});
}
public get id(): number
{
return this._id;
}
public set id(id: number)
{
this._id = id;
}
public get name(): string
{
return this._name;
}
public set name(name: string)
{
this._name = name;
}
public get description(): string
{
return this._description;
}
public set description(description: string)
{
this._description = description;
}
public get roomId(): string
{
return this._roomId;
}
public set roomId(id: string)
{
this._roomId = id;
}
public get badgeParts(): GroupBadgePart[]
{
return this._badgeParts;
}
public set badgeParts(parts: GroupBadgePart[])
{
this._badgeParts = parts;
}
public get colorA(): number
{
return this._colorA;
}
public set colorA(id: number)
{
this._colorA = id;
}
public get colorB(): number
{
return this._colorB;
}
public set colorB(id: number)
{
this._colorB = id;
}
public get currentBadgeCode(): string
{
let code = '';
this._badgeParts.forEach((part) =>
{
if(part.code)
{
code = code + part.code;
}
});
return code;
}
public get currentBadgeArray(): number[]
{
const badge = [];
this._badgeParts.forEach((part) =>
{
if(part.code)
{
badge.push(part.key);
badge.push(part.color);
badge.push(part.position);
}
});
return badge;
}
public get state(): number
{
return this._state;
}
public set state(state: number)
{
this._state = state;
}
public get canMembersDecorate(): boolean
{
return this._canMembersDecorate;
}
public set canMembersDecorate(value: boolean)
{
this._canMembersDecorate = value;
}
}

View File

@ -1,4 +1,5 @@
import { Dispatch, ProviderProps, Reducer } from 'react';
import { GroupBadgePart } from './../common/GroupBadgePart';
export interface IGroupsContext
{
@ -13,81 +14,144 @@ export interface GroupsContextProps extends ProviderProps<IGroupsContext>
export interface IGroupsState
{
badgeBases: Map<number, string[]>;
badgeSymbols: Map<number, string[]>;
badgePartColors: Map<number, string>;
groupColorsA: Map<number, string>;
groupColorsB: Map<number, string>;
availableRooms: { id: number, name: string }[];
purchaseCost: number;
badgeBases: { id: number, images: string[] }[];
badgeSymbols: { id: number, images: string[] }[];
badgePartColors: { id: number, color: string }[];
groupId: number;
groupColorsA: { id: number, color: string }[];
groupColorsB: { id: number, color: string }[];
groupName: string;
groupDescription: string;
groupHomeroomId: number;
groupBadgeParts: GroupBadgePart[];
groupColors: number[];
groupState: number;
groupCanMembersDecorate: boolean;
}
export interface IGroupsAction
{
type: string;
payload?: {
arrayMaps?: Map<number, string[]>[];
stringMaps?: Map<number, string>[];
stringValue?: string;
numberValue?: number;
objectValues?: any[];
stringValues?: string[];
numberValues?: number[];
boolValues?: boolean[];
badgeParts?: GroupBadgePart[];
}
}
export class GroupsActions
{
public static SET_BADGE_PARTS: string = 'GA_SET_BADGE_PARTS';
public static SET_PURHCASE_SETTINGS: string = 'GA_SET_PURHCASE_SETTINGS';
public static SET_GROUP_BADGE_PARTS_CONFIG: string = 'GA_SET_GROUP_BADGE_PARTS_CONFIG';
public static SET_GROUP_NAME: string = 'GA_SET_GROUP_NAME';
public static SET_GROUP_DESCRIPTION: string = 'GA_SET_GROUP_DESCRIPTION';
public static SET_GROUP_HOMEROOM_ID: string = 'GA_SET_GROUP_HOMEROOM_ID';
public static SET_GROUP_BADGE_PARTS: string = 'GA_SET_BADGE_PARTS';
public static SET_GROUP_COLORS: string = 'GA_SET_GROUP_COLORS';
public static SET_GROUP_STATE: string = 'GA_SET_GROUP_STATE';
public static SET_GROUP_CAN_MEMBERS_DECORATE: string = 'GA_SET_GROUP_CAN_MEMBERS_DECORATE';
public static SET_GROUP_SETTINGS: string = 'GA_SET_GROUP_SETTINGS';
public static RESET_GROUP_SETTINGS: string = 'GA_RESET_GROUP_SETTINGS';
}
export const initialGroups: IGroupsState = {
availableRooms: null,
purchaseCost: null,
badgeBases: null,
badgeSymbols: null,
badgePartColors: null,
groupId: null,
groupColorsA: null,
groupColorsB: null,
groupName: '',
groupDescription: '',
groupHomeroomId: 0
groupHomeroomId: 0,
groupBadgeParts: null,
groupColors: null,
groupState: null,
groupCanMembersDecorate: null
};
export const GroupsReducer: Reducer<IGroupsState, IGroupsAction> = (state, action) =>
{
switch(action.type)
{
case GroupsActions.SET_BADGE_PARTS: {
const badgeBases = (action.payload.arrayMaps[0] || state.badgeBases || null);
const badgeSymbols = (action.payload.arrayMaps[1] || state.badgeSymbols || null);
const badgePartColors = (action.payload.stringMaps[0] || state.badgePartColors || null);
const groupColorsA = (action.payload.stringMaps[1] || state.groupColorsA || null);
const groupColorsB = (action.payload.stringMaps[2] || state.groupColorsB || null);
case GroupsActions.SET_PURHCASE_SETTINGS: {
const availableRooms = action.payload.objectValues;
const purchaseCost = (action.payload.numberValues[0] || state.purchaseCost || 0);
return { ...state, availableRooms, purchaseCost };
}
case GroupsActions.SET_GROUP_BADGE_PARTS_CONFIG: {
const badgeBases = (action.payload.objectValues[0] || state.badgeBases || null);
const badgeSymbols = (action.payload.objectValues[1] || state.badgeSymbols || null);
const badgePartColors = (action.payload.objectValues[2] || state.badgePartColors || null);
const groupColorsA = (action.payload.objectValues[3] || state.groupColorsA || null);
const groupColorsB = (action.payload.objectValues[4] || state.groupColorsB || null);
return { ...state, badgeBases, badgeSymbols, badgePartColors, groupColorsA, groupColorsB };
}
case GroupsActions.SET_GROUP_NAME: {
const groupName = action.payload.stringValue;
const groupName = action.payload.stringValues[0];
return { ...state, groupName };
}
case GroupsActions.SET_GROUP_DESCRIPTION: {
const groupDescription = action.payload.stringValue;
const groupDescription = action.payload.stringValues[0];
return { ...state, groupDescription };
}
case GroupsActions.SET_GROUP_HOMEROOM_ID: {
const groupHomeroomId = action.payload.numberValue;
const groupHomeroomId = action.payload.numberValues[0];
return { ...state, groupHomeroomId };
}
case GroupsActions.SET_GROUP_BADGE_PARTS: {
const groupBadgeParts = action.payload.badgeParts;
return { ...state, groupBadgeParts };
}
case GroupsActions.SET_GROUP_COLORS: {
const groupColors = action.payload.objectValues;
return { ...state, groupColors };
}
case GroupsActions.SET_GROUP_STATE: {
const groupState = action.payload.numberValues[0];
return { ...state, groupState };
}
case GroupsActions.SET_GROUP_CAN_MEMBERS_DECORATE: {
const groupCanMembersDecorate = action.payload.boolValues[0];
return { ...state, groupCanMembersDecorate };
}
case GroupsActions.SET_GROUP_SETTINGS: {
const groupId = action.payload.numberValues[0];
const groupName = action.payload.stringValues[0];
const groupDescription = action.payload.stringValues[1];
const groupBadgeParts = action.payload.objectValues;
const groupState = action.payload.numberValues[1];
const groupColors = action.payload.numberValues.slice(2);
const groupCanMembersDecorate = action.payload.boolValues[0];
return { ...state, groupId, groupName, groupDescription, groupBadgeParts, groupColors, groupState, groupCanMembersDecorate };
}
case GroupsActions.RESET_GROUP_SETTINGS: {
const groupId = null;
const groupName = '';
const groupDescription = '';
const groupHomeroomId = 0;
const groupBadgeParts = null;
const groupColors = null;
const groupState = null;
const groupCanMembersDecorate = null;
return { ...state, groupName, groupDescription, groupHomeroomId };
return { ...state, groupId, groupName, groupDescription, groupHomeroomId, groupBadgeParts, groupColors, groupState, groupCanMembersDecorate };
}
default:
return state;

View File

@ -68,48 +68,9 @@
}
}
.tab-image {
width: 122px;
height: 68px;
display: flex;
align-items: center;
justify-content: center;
div {
background-image: url('../../../../assets/images/groups/creator_images.png');
background-repeat: no-repeat;
}
&.tab-1 {
div {
background-position: 0px 0px;
width: 99px;
height: 50px;
}
}
&.tab-2 {
div {
background-position: -99px 0px;
width: 98px;
height: 62px;
}
}
&.tab-3 {
div {
background-position: 0px -50px;
width: 96px;
height: 45px;
}
}
&.tab-4 {
div {
background-position: 0px -95px;
width: 114px;
height: 61px;
}
}
.creator-tab {
height: 230px;
min-height: 230px;
max-height: 230px;
}
}

View File

@ -1,27 +1,28 @@
import { GroupBuyDataComposer, GroupBuyDataEvent } from '@nitrots/nitro-renderer';
import { GroupBuyComposer, GroupBuyDataComposer } from '@nitrots/nitro-renderer';
import classNames from 'classnames';
import { FC, useCallback, useEffect, useState } from 'react';
import { Button } from 'react-bootstrap';
import { LocalizeText } from '../../../../api';
import { CreateMessageHook, SendMessageHook } from '../../../../hooks';
import { HasHabboClub, LocalizeText } from '../../../../api';
import { SendMessageHook } from '../../../../hooks';
import { NitroCardContentView, NitroCardHeaderView, NitroCardView } from '../../../../layout';
import { useGroupsContext } from '../../context/GroupsContext';
import { GroupsActions } from '../../context/GroupsContext.types';
import { GroupSharedTabBadgeView } from '../shared-tabs/tab-badge/GroupSharedTabBadgeView';
import { GroupSharedTabColorsView } from '../shared-tabs/tab-colors/GroupSharedTabColorsView';
import { GroupSharedTabIdentityView } from '../shared-tabs/tab-identity/GroupSharedTabIdentityView';
import { GroupCreatorViewProps } from './GroupCreatorView.types';
import { GroupCreatorTabIdentityView } from './views/creator-tab-identity/GroupCreatorTabIdentityView';
import { GroupCreatorTabConfirmationView } from './tab-confirmation/GroupCreatorTabConfirmationView';
const TABS: number[] = [1, 2, 3, 4];
export const GroupCreatorView: FC<GroupCreatorViewProps> = props =>
{
const { groupsState = null, dispatchGroupsState = null } = useGroupsContext();
const { groupName = null, groupHomeroomId = null } = groupsState;
const { groupName = null, groupDescription = null, groupHomeroomId = null, groupColors = null, groupBadgeParts = null } = groupsState;
const { isVisible = false, onClose = null } = props;
const [ currentTab, setCurrentTab ] = useState<number>(1);
const [ availableRooms, setAvailableRooms ] = useState<Map<number, string>>(null);
const [ cost, setCost ] = useState<number>(0);
useEffect(() =>
{
@ -38,15 +39,24 @@ export const GroupCreatorView: FC<GroupCreatorViewProps> = props =>
}
}, [ isVisible ]);
const onGroupBuyDataEvent = useCallback((event: GroupBuyDataEvent) =>
const buyGroup = useCallback(() =>
{
const parser = event.getParser();
const badge = [];
setAvailableRooms(parser.availableRooms);
setCost(parser.groupCost);
}, []);
if(!groupBadgeParts) return;
CreateMessageHook(GroupBuyDataEvent, onGroupBuyDataEvent);
groupBadgeParts.forEach((part) =>
{
if(part.code)
{
badge.push(part.key);
badge.push(part.color);
badge.push(part.position);
}
});
SendMessageHook(new GroupBuyComposer(groupName, groupDescription, groupHomeroomId, groupColors[0], groupColors[1], badge));
}, [ groupName, groupDescription, groupHomeroomId, groupColors, groupBadgeParts ]);
const previousStep = useCallback(() =>
{
@ -67,6 +77,10 @@ export const GroupCreatorView: FC<GroupCreatorViewProps> = props =>
}
break;
}
case 4: {
buyGroup();
break;
}
}
setCurrentTab(value =>
@ -98,12 +112,15 @@ export const GroupCreatorView: FC<GroupCreatorViewProps> = props =>
<div>{ LocalizeText('group.create.stepdesc.' + currentTab) }</div>
</div>
</div>
<div className="text-black">
{ currentTab === 1 && <GroupCreatorTabIdentityView availableRooms={ availableRooms } /> }
<div className="text-black creator-tab">
{ currentTab === 1 && <GroupSharedTabIdentityView isCreator={ true } /> }
{ currentTab === 2 && <GroupSharedTabBadgeView /> }
{ currentTab === 3 && <GroupSharedTabColorsView /> }
{ currentTab === 4 && <GroupCreatorTabConfirmationView /> }
</div>
<div className="d-flex justify-content-between mt-2">
<Button variant="link" className="text-black" onClick={ previousStep }>{ LocalizeText(currentTab === 1 ? 'generic.cancel' : 'group.create.previousstep') }</Button>
<button className="btn btn-primary" onClick={ nextStep }>{ LocalizeText('group.create.nextstep') }</button>
<button disabled={ currentTab === 4 && !HasHabboClub() } className={ 'btn ' + (currentTab === 4 ? HasHabboClub() ? 'btn-success' : 'btn-danger' : 'btn-primary') } onClick={ nextStep }>{ LocalizeText(currentTab === 4 ? HasHabboClub() ? 'group.create.confirm.buy' : 'group.create.confirm.viprequired' : 'group.create.nextstep') }</button>
</div>
</NitroCardContentView>
</NitroCardView>

View File

@ -0,0 +1,55 @@
import { FC, useCallback } from 'react';
import { LocalizeText } from '../../../../../api';
import { BadgeImageView } from '../../../../shared/badge-image/BadgeImageView';
import { useGroupsContext } from '../../../context/GroupsContext';
export const GroupCreatorTabConfirmationView: FC<{}> = props =>
{
const { groupsState = null, dispatchGroupsState = null } = useGroupsContext();
const { groupName = '', groupDescription = '', groupBadgeParts = null, groupColors = null, groupColorsA = null, groupColorsB = null, purchaseCost = 0 } = groupsState;
const getCompleteBadgeCode = useCallback(() =>
{
let code = '';
if(!groupBadgeParts) return code;
groupBadgeParts.forEach((badgePart) =>
{
if(badgePart.code) code = code + badgePart.code;
});
return code;
}, [ groupBadgeParts ]);
const getGroupColor = useCallback((colorIndex: number) =>
{
if(colorIndex === 0) return groupColorsA.find(c => c.id === groupColors[colorIndex]).color;
return groupColorsB.find(c => c.id === groupColors[colorIndex]).color;
}, [ groupColors, groupColorsA, groupColorsB ]);
return (
<div className="d-flex gap-3 h-100">
<div>
<div className="fw-bold text-nowrap">{ LocalizeText('group.create.confirm.guildbadge') }</div>
<div className="badge-preview">
<BadgeImageView badgeCode={ getCompleteBadgeCode() } isGroup={ true } />
</div>
<div className="d-flex flex-column align-items-center mt-2">
<div className="fw-bold text-nowrap">{ LocalizeText('group.edit.color.guild.color') }</div>
{ groupColors && <div className="d-flex">
<div className="group-color-swatch" style={{ backgroundColor: '#' + getGroupColor(0) }}></div>
<div className="group-color-swatch" style={{ backgroundColor: '#' + getGroupColor(1) }}></div>
</div> }
</div>
</div>
<div className="d-flex flex-column h-100">
<div className="fw-bold">{ groupName }</div>
<div>{ groupDescription }</div>
<div className="mt-3" dangerouslySetInnerHTML={ { __html: LocalizeText('group.create.confirm.info') } } />
<div className="mt-auto rounded bg-primary p-1 text-center text-white">{ LocalizeText('group.create.confirm.buyinfo', ['amount'], [purchaseCost.toString()]) }</div>
</div>
</div>
);
};

View File

@ -1,66 +0,0 @@
import { FC, useCallback } from 'react';
import { CreateLinkEvent, LocalizeText } from '../../../../../../api';
import { useGroupsContext } from '../../../../context/GroupsContext';
import { GroupsActions } from '../../../../context/GroupsContext.types';
import { GroupCreatorTabIdentityViewProps } from './GroupCreatorTabIdentityView.types';
export const GroupCreatorTabIdentityView: FC<GroupCreatorTabIdentityViewProps> = props =>
{
const { groupsState = null, dispatchGroupsState = null } = useGroupsContext();
const { groupName = '', groupDescription = '', groupHomeroomId = 0 } = groupsState;
const { availableRooms = null } = props;
const setName = useCallback((name: string) =>
{
dispatchGroupsState({
type: GroupsActions.SET_GROUP_NAME,
payload: {
stringValue: name
}
})
}, [ dispatchGroupsState ]);
const setDescription = useCallback((description: string) =>
{
dispatchGroupsState({
type: GroupsActions.SET_GROUP_DESCRIPTION,
payload: {
stringValue: description
}
})
}, [ dispatchGroupsState ]);
const setHomeroomId = useCallback((id: number) =>
{
dispatchGroupsState({
type: GroupsActions.SET_GROUP_HOMEROOM_ID,
payload: {
numberValue: id
}
})
}, [ dispatchGroupsState ]);
return (<>
<div className="form-group mb-2">
<label>{ LocalizeText('group.edit.name') }</label>
<input type="text" className="form-control form-control-sm" value={ groupName } onChange={ (e) => setName(e.target.value) } />
</div>
<div className="form-group mb-2">
<label>{ LocalizeText('group.edit.desc') }</label>
<input type="text" className="form-control form-control-sm" value={ groupDescription } onChange={ (e) => setDescription(e.target.value) } />
</div>
<div className="form-group mb-1">
<label>{ LocalizeText('group.edit.base') }</label>
<select className="form-select form-select-sm" value={ groupHomeroomId } onChange={ (e) => setHomeroomId(Number(e.target.value)) }>
<option value={ 0 } disabled>{ LocalizeText('group.edit.base.select.room') }</option>
{ availableRooms && Array.from(availableRooms.keys()).map((roomId, index) =>
{
return <option key={ index } value={ roomId }>{ availableRooms.get(roomId) }</option>;
}) }
</select>
</div>
<div className="small mb-2">{ LocalizeText('group.edit.base.warning') }</div>
<div className="cursor-pointer text-decoration-underline text-center" onClick={ () => CreateLinkEvent('navigator/create') }>{ LocalizeText('group.createroom') }</div>
</>);
};

View File

@ -1,4 +0,0 @@
export interface GroupCreatorTabIdentityViewProps
{
availableRooms: Map<number, string>;
}

View File

@ -1,6 +1,8 @@
import { GroupDeleteComposer, GroupRemoveMemberComposer } from '@nitrots/nitro-renderer';
import { GroupRemoveMemberComposer } from '@nitrots/nitro-renderer';
import { FC, useCallback } from 'react';
import { CreateLinkEvent, GetSessionDataManager, LocalizeText, TryVisitRoom } from '../../../../api';
import { GetGroupManager } from '../../../../api/groups/GetGroupManager';
import { GetGroupMembers } from '../../../../api/groups/GetGroupMembers';
import { TryJoinGroup } from '../../../../api/groups/TryJoinGroup';
import { SendMessageHook } from '../../../../hooks';
import { CatalogPageName } from '../../../catalog/common/CatalogPageName';
@ -75,19 +77,18 @@ export const GroupInformationView: FC<GroupInformationViewProps> = props =>
{
switch(action)
{
case 'members':
GetGroupMembers(groupInformation.id);
break;
case 'manage':
GetGroupManager(groupInformation.id);
break;
case 'homeroom':
TryVisitRoom(groupInformation.roomId);
break;
case 'furniture':
CreateLinkEvent('catalog/open/' + CatalogPageName.GUILD_CUSTOM_FURNI);
break;
case 'delete':
if(window.confirm(LocalizeText('group.deleteconfirm.title') + ' - ' + LocalizeText('group.deleteconfirm.desc')))
{
SendMessageHook(new GroupDeleteComposer(groupInformation.id));
if(onClose) onClose();
}
break;
}
}, [ groupInformation, onClose ]);
@ -96,26 +97,22 @@ export const GroupInformationView: FC<GroupInformationViewProps> = props =>
return (
<div className="group-information text-black p-2">
<div>
<div className="group-badge text-center">
<div className="text-center d-flex flex-column h-100 small text-black text-decoration-underline">
<div className="group-badge">
<BadgeImageView badgeCode={ groupInformation.badge } isGroup={ true } />
<div className="mt-3">
<a href="#" className="small text-black">
</div>
<div className="mt-3 cursor-pointer" onClick={ () => handleAction('members') }>
{ LocalizeText('group.membercount', ['totalMembers'], [groupInformation.membersCount.toString()]) }
</a>
</div>
<div>
{ groupInformation.pendingRequestsCount > 0 && <a href="#" className="small text-black">
{ groupInformation.pendingRequestsCount > 0 && <div className="cursor-pointer">
{ LocalizeText('group.pendingmembercount', ['totalMembers'], [groupInformation.pendingRequestsCount.toString()]) }
</a> }
</div>
<div className="mt-3">
</div> }
{ groupInformation.isOwner && <div className="cursor-pointer" onClick={ () => handleAction('manage') }>
{ LocalizeText('group.manage') }
</div> }
<div className="mt-auto mb-1">
{ getRoleIcon() }
</div>
<div>
{ groupInformation.isOwner && <a href="#" className="small text-danger" onClick={ () => handleAction('delete') }>
{ LocalizeText('group.delete') }
</a> }
</div>
</div>
</div>
<div className="ms-2 w-100">
@ -126,20 +123,16 @@ export const GroupInformationView: FC<GroupInformationViewProps> = props =>
</div>
<div>{ LocalizeText('group.created', ['date', 'owner'], [groupInformation.createdAt, groupInformation.ownerName]) }</div>
<div className="group-description small overflow-auto">{ groupInformation.description }</div>
<div>
<a href="#" className="small text-black" onClick={ () => handleAction('homeroom') }>
<div className="small text-black text-decoration-underline">
<div className="cursor-pointer" onClick={ () => handleAction('homeroom') }>
{ LocalizeText('group.linktobase') }
</a>
</div>
<div>
<a href="#" className="small text-black" onClick={ () => handleAction('furniture') }>
<div className="cursor-pointer" onClick={ () => handleAction('furniture') }>
{ LocalizeText('group.buyfurni') }
</a>
</div>
<div>
<a href="#" className="small text-black">
<div className="cursor-pointer">
{ LocalizeText('group.showgroups') }
</a>
</div>
</div>
{ groupInformation.type !== GroupType.PRIVATE &&
<button className="btn btn-primary w-100 mt-2" disabled={ groupInformation.membershipType === GroupMembershipType.REQUEST_PENDING || isRealOwner() } onClick={ handleButtonClick }>

View File

@ -0,0 +1,9 @@
.nitro-group-manager {
width: 385px;
.manager-tab {
height: 230px;
min-height: 230px;
max-height: 230px;
}
}

View File

@ -0,0 +1,100 @@
import { GroupDeleteComposer, GroupSaveBadgeComposer, GroupSaveColorsComposer, GroupSaveInformationComposer, GroupSavePreferencesComposer } from '@nitrots/nitro-renderer';
import { FC, useCallback, useState } from 'react';
import { LocalizeText } from '../../../../api';
import { SendMessageHook } from '../../../../hooks';
import { NitroCardContentView, NitroCardHeaderView, NitroCardTabsItemView, NitroCardTabsView, NitroCardView } from '../../../../layout';
import { useGroupsContext } from '../../context/GroupsContext';
import { GroupsActions } from '../../context/GroupsContext.types';
import { GroupSharedTabBadgeView } from '../shared-tabs/tab-badge/GroupSharedTabBadgeView';
import { GroupSharedTabColorsView } from '../shared-tabs/tab-colors/GroupSharedTabColorsView';
import { GroupSharedTabIdentityView } from '../shared-tabs/tab-identity/GroupSharedTabIdentityView';
import { GroupManagerTabSettingsView } from './tab-settings/GroupManagerTabSettingsView';
const TABS: number[] = [1, 2, 3, 5];
export const GroupManagerView: FC<{}> = props =>
{
const { groupsState = null, dispatchGroupsState = null } = useGroupsContext();
const { groupId = null, groupName = null, groupDescription = null, groupColors = null, groupBadgeParts = null, groupState = null, groupCanMembersDecorate = null } = groupsState;
const [ currentTab, setCurrentTab ] = useState<number>(1);
const onClose = useCallback(() =>
{
dispatchGroupsState({
type: GroupsActions.RESET_GROUP_SETTINGS
});
}, [ dispatchGroupsState ]);
const saveGroup = useCallback(() =>
{
const badge = [];
if(!groupBadgeParts) return;
groupBadgeParts.forEach((part) =>
{
if(part.code)
{
badge.push(part.key);
badge.push(part.color);
badge.push(part.position);
}
});
SendMessageHook(new GroupSaveInformationComposer(groupId, groupName, groupDescription));
SendMessageHook(new GroupSaveBadgeComposer(groupId, badge));
SendMessageHook(new GroupSaveColorsComposer(groupId, groupColors[0], groupColors[1]));
SendMessageHook(new GroupSavePreferencesComposer(groupId, groupState, groupCanMembersDecorate ? 0 : 1));
onClose();
}, [ groupBadgeParts, groupId, groupName, groupDescription, groupColors, groupState, groupCanMembersDecorate, onClose ]);
const deleteGroup = useCallback(() =>
{
if(window.confirm(LocalizeText('group.deleteconfirm.title') + ' - ' + LocalizeText('group.deleteconfirm.desc')))
{
SendMessageHook(new GroupDeleteComposer(groupId));
onClose();
}
}, [ groupId, onClose ]);
if(!groupId) return null;
return (
<NitroCardView className="nitro-group-manager" simple={ false }>
<NitroCardHeaderView headerText={ LocalizeText('group.window.title') } onCloseClick={ onClose } />
<NitroCardContentView className="p-0">
<NitroCardTabsView>
{ TABS.map(tab =>
{
return (<NitroCardTabsItemView key={ tab } isActive={ currentTab === tab } onClick={ () => setCurrentTab(tab) }>
{ LocalizeText(`group.edit.tab.${tab}`) }
</NitroCardTabsItemView>);
}) }
</NitroCardTabsView>
<div className="p-2">
<div className="d-flex align-items-center mb-2">
<div className={ 'flex-shrink-0 tab-image tab-' + currentTab }>
<div></div>
</div>
<div className="w-100 text-black ms-2">
<div className="fw-bold h4 m-0">{ LocalizeText('group.edit.tabcaption.' + currentTab) }</div>
<div>{ LocalizeText('group.edit.tabdesc.' + currentTab) }</div>
</div>
</div>
<div className="text-black manager-tab">
{ currentTab === 1 && <GroupSharedTabIdentityView /> }
{ currentTab === 2 && <GroupSharedTabBadgeView skipDefault={ true } /> }
{ currentTab === 3 && <GroupSharedTabColorsView /> }
{ currentTab === 5 && <GroupManagerTabSettingsView /> }
</div>
<div className="d-flex justify-content-between mt-2">
<button className="btn btn-danger" onClick={ deleteGroup }>{ LocalizeText('group.delete') }</button>
<button className="btn btn-success" onClick={ saveGroup }>{ LocalizeText('save') }</button>
</div>
</div>
</NitroCardContentView>
</NitroCardView>
);
};

View File

@ -0,0 +1,55 @@
import { FC, useCallback } from 'react';
import { LocalizeText } from '../../../../../api/utils/LocalizeText';
import { useGroupsContext } from '../../../context/GroupsContext';
import { GroupsActions } from '../../../context/GroupsContext.types';
const STATES: string[] = ['regular', 'exclusive', 'private'];
export const GroupManagerTabSettingsView: FC<{}> = props =>
{
const { groupsState = null, dispatchGroupsState = null } = useGroupsContext();
const { groupState = null, groupCanMembersDecorate = false } = groupsState;
const setState = useCallback((state: number) =>
{
dispatchGroupsState({
type: GroupsActions.SET_GROUP_STATE,
payload: {
numberValues: [ state ]
}
})
}, [ dispatchGroupsState ]);
const toggleCanMembersDecorate = useCallback(() =>
{
dispatchGroupsState({
type: GroupsActions.SET_GROUP_CAN_MEMBERS_DECORATE,
payload: {
boolValues: [ !groupCanMembersDecorate ]
}
})
}, [ dispatchGroupsState, groupCanMembersDecorate ]);
return (
<>
{ STATES.map((state, index) =>
{
return <div key={ index } className="form-check mb-1">
<input className="form-check-input" type="radio" name="groupState" id={ `groupState${ index }` } checked={ (groupState === index) } onChange={ () => setState(index) } />
<label className="form-check-label d-flex align-items-center gap-1 fw-bold" htmlFor={'groupState' + index}>
<i className={ `icon icon-group-type-${index}` } /> { LocalizeText(`group.edit.settings.type.${state}.label`) }
</label>
<div>{ LocalizeText(`group.edit.settings.type.${state}.help`) }</div>
</div>
}) }
<hr className="bg-dark" />
<div className="form-check">
<input className="form-check-input" type="checkbox" id="groupCanMembersDecorate" checked={ groupCanMembersDecorate } onChange={() => toggleCanMembersDecorate() } />
<label className="form-check-label fw-bold" htmlFor="groupCanMembersDecorate">
{ LocalizeText('group.edit.settings.rights.caption') }
</label>
<div>{ LocalizeText('group.edit.settings.rights.members.help') }</div>
</div>
</>
);
};

View File

@ -0,0 +1,29 @@
.nitro-group-members {
width: 400px;
.group-badge {
width: 50px;
height: 50px;
}
.members-list {
height: 300px;
.list-member {
border: 1px solid #ced4da;
.avatar-head {
position: relative;
overflow: hidden;
width: 40px;
height: 40px;
.avatar-image {
position: absolute;
left: -25px;
top: -25px;
}
}
}
}
}

View File

@ -0,0 +1,142 @@
import { GroupMemberParser, GroupMembersComposer, GroupMembersEvent, GroupMembersParser, GroupRank } from '@nitrots/nitro-renderer';
import classNames from 'classnames';
import { FC, KeyboardEvent, useCallback, useEffect, useState } from 'react';
import { GetSessionDataManager, LocalizeText } from '../../../../api';
import { CreateMessageHook, SendMessageHook } from '../../../../hooks';
import { NitroCardContentView, NitroCardHeaderView, NitroCardView } from '../../../../layout';
import { AvatarImageView } from '../../../shared/avatar-image/AvatarImageView';
import { BadgeImageView } from '../../../shared/badge-image/BadgeImageView';
import { GroupMembersViewProps } from './GroupMembersView.types';
export const GroupMembersView: FC<GroupMembersViewProps> = props =>
{
const { groupId = null, levelId = null, onClose = null } = props;
const [ pageData, setPageData ] = useState<GroupMembersParser>(null);
const [ searchQuery, setSearchQuery ] = useState<string>('');
const [ searchLevelId, setSearchLevelId ] = useState<number>(3);
const [ totalPages, setTotalPages ] = useState<number>(0);
const searchMembers = useCallback((pageId: number) =>
{
if(!groupId) return;
SendMessageHook(new GroupMembersComposer(groupId, pageId, searchQuery, searchLevelId));
}, [ groupId, searchQuery, searchLevelId ]);
const onGroupMembersEvent = useCallback((event: GroupMembersEvent) =>
{
const parser = event.getParser();
setPageData(parser);
setTotalPages(Math.ceil(parser.totalMembersCount / parser.pageSize));
}, []);
CreateMessageHook(GroupMembersEvent, onGroupMembersEvent);
useEffect(() =>
{
setPageData(null);
setSearchQuery('');
setSearchLevelId(0);
if(!groupId) return;
if(levelId !== null) setSearchLevelId(levelId);
searchMembers(0);
}, [ groupId, levelId ]);
useEffect(() =>
{
searchMembers(0);
}, [ searchLevelId ]);
const previousPage = useCallback(() =>
{
searchMembers(pageData.pageIndex - 1);
}, [ searchMembers, pageData ]);
const nextPage = useCallback(() =>
{
searchMembers(pageData.pageIndex + 1);
}, [ searchMembers, pageData ]);
const onKeyDown = useCallback((event: KeyboardEvent<HTMLInputElement>) =>
{
if(event.key !== 'Enter') return;
searchMembers(pageData.pageIndex);
}, [ searchMembers, pageData ]);
const getRankDescription = useCallback((member: GroupMemberParser) =>
{
if(member.rank === GroupRank.OWNER) return 'group.members.owner';
if(pageData.admin)
{
if(member.rank === GroupRank.ADMIN) return 'group.members.removerights';
if(member.rank === GroupRank.MEMBER) return 'group.members.giverights';
}
return '';
}, [ pageData ]);
if(!pageData) return null;
return (
<NitroCardView className="nitro-group-members" simple={ true }>
<NitroCardHeaderView headerText={ LocalizeText('group.members.title', ['groupName'], [ pageData.groupTitle ]) } onCloseClick={ onClose } />
<NitroCardContentView className="pb-2">
<div className="d-flex gap-2 align-items-center mb-2">
<div className="group-badge">
<BadgeImageView badgeCode={ pageData.badge } isGroup={ true } />
</div>
<div className="w-100">
<input type="text" className="form-control form-control-sm w-100 mb-1" placeholder={ LocalizeText('group.members.searchinfo') } value={ searchQuery } onChange={ (e) => setSearchQuery(e.target.value) } onBlur={ () => searchMembers(pageData.pageIndex) } onKeyDown={ onKeyDown } />
<select className="form-select form-select-sm w-100" value={ searchLevelId } onChange={ (e) => setSearchLevelId(Number(e.target.value)) }>
<option value="0">{ LocalizeText('group.members.search.all') }</option>
<option value="1">{ LocalizeText('group.members.search.admins') }</option>
<option value="2">{ LocalizeText('group.members.search.pending') }</option>
</select>
</div>
</div>
<div className="row row-cols-2 align-content-start g-0 w-100 members-list overflow-auto">
{ pageData.result.map((member, index) =>
{
return (
<div key={ index } className={ 'col pb-1' + classNames({ ' pe-1': index % 2 === 0 }) }>
<div className="list-member bg-white rounded d-flex text-black">
<div className="avatar-head flex-shrink-0">
<AvatarImageView figure={ member.figure } headOnly={ true } direction={ 2 } />
</div>
<div className="p-1 w-100">
<div className="fw-bold small">{ member.name }</div>
<div className="text-muted fst-italic small">{ LocalizeText('group.members.since', ['date'], [member.joinedAt]) }</div>
</div>
<div className="d-flex flex-column pe-2 align-items-center justify-content-center">
<div className="d-flex align-items-center">
<i className={ 'icon icon-group-small-' + classNames({ 'owner': member.rank === GroupRank.OWNER, 'admin': member.rank === GroupRank.ADMIN, 'not-admin': member.rank === GroupRank.MEMBER, 'cursor-pointer': pageData.admin }) } title={ LocalizeText(getRankDescription(member)) } />
</div>
{ member.rank !== GroupRank.OWNER && pageData.admin && member.id !== GetSessionDataManager().userId &&<div className="d-flex align-items-center mt-1">
<i className="icon cursor-pointer icon-group-remove-member" title={ LocalizeText(member.rank === GroupRank.REQUESTED ? 'group.members.reject' : 'group.members.kick') } />
</div> }
</div>
</div>
</div>
);
}) }
</div>
<div className="d-flex w-100 align-items-center">
<div>
<button disabled={ pageData.pageIndex === 0 } className="btn btn-primary" onClick={ previousPage }><i className="fas fa-chevron-left" /></button>
</div>
<div className="text-center text-black w-100">{ LocalizeText('group.members.pageinfo', ['amount', 'page', 'totalPages'], [pageData.totalMembersCount.toString(), (pageData.pageIndex + 1).toString(), totalPages.toString()]) }</div>
<div>
<button disabled={ pageData.pageIndex === totalPages - 1 } className="btn btn-primary" onClick={ nextPage }><i className="fas fa-chevron-right" /></button>
</div>
</div>
</NitroCardContentView>
</NitroCardView>
);
};

View File

@ -0,0 +1,6 @@
export interface GroupMembersViewProps
{
groupId: number;
levelId: number;
onClose: () => void;
}

View File

@ -1,6 +1,7 @@
import { DesktopViewEvent, GroupInformationComposer, GroupInformationEvent, GroupInformationParser, GroupJoinComposer, GroupRemoveMemberComposer, RoomInfoEvent } from '@nitrots/nitro-renderer';
import { FC, useCallback, useState } from 'react';
import { GetGroupInformation, GetSessionDataManager, LocalizeText } from '../../../../api';
import { GetGroupManager } from '../../../../api/groups/GetGroupManager';
import { CreateMessageHook, SendMessageHook } from '../../../../hooks';
import { BadgeImageView } from '../../../shared/badge-image/BadgeImageView';
import { GroupMembershipType } from '../../common/GroupMembershipType';
@ -68,8 +69,6 @@ export const GroupRoomInformationView: FC<{}> = props =>
SendMessageHook(new GroupInformationComposer(groupInformation.id, false));
}, [ groupInformation ]);
const manageGroup = useCallback(() => {}, []);
const getButtonText = useCallback(() =>
{
if(isRealOwner()) return 'group.manage';
@ -90,14 +89,14 @@ export const GroupRoomInformationView: FC<{}> = props =>
const handleButtonClick = useCallback(() =>
{
if(isRealOwner()) return manageGroup();
if(isRealOwner()) return GetGroupManager(groupInformation.id);
if(groupInformation.type === GroupType.PRIVATE && groupInformation.membershipType === GroupMembershipType.NOT_MEMBER) return;
if(groupInformation.membershipType === GroupMembershipType.MEMBER) return tryLeaveGroup();
return tryJoinGroup();
}, [ groupInformation, tryLeaveGroup, tryJoinGroup, isRealOwner, manageGroup ]);
}, [ groupInformation, tryLeaveGroup, tryJoinGroup, isRealOwner ]);
if(!groupInformation) return null;

View File

@ -0,0 +1,40 @@
.nitro-groups {
.badge-preview {
position: relative;
width: 50px;
height: 50px;
background: $white;
border-radius: $border-radius;
&.active {
&:before {
position: absolute;
content: ' ';
width: 0;
height: 0;
border-top: 10px solid white;
border-left: 10px solid transparent;
border-right: 10px solid transparent;
bottom: -10px;
}
}
}
.group-color-swatch {
width: 30px;
height: 40px;
&:first-child {
border-top-left-radius: $border-radius;
border-bottom-left-radius: $border-radius;
}
&:last-child {
border-top-right-radius: $border-radius;
border-bottom-right-radius: $border-radius;
}
}
}
@import './tab-badge/GroupSharedTabBadgeView';
@import './tab-colors/GroupSharedTabColorsView';

View File

@ -0,0 +1,26 @@
.shared-tab-badge {
.position-swatch {
position: relative;
border-radius: $border-radius;
width: 16px;
height: 12px;
background: $white;
&.active {
background: $primary;
}
}
.color-swatch {
position: relative;
border-radius: $border-radius;
width: 16px;
height: 12px;
}
.selection-list {
height: 160px;
min-height: 160px;
max-height: 160px;
}
}

View File

@ -0,0 +1,131 @@
import classNames from 'classnames';
import { FC, useCallback, useEffect, useState } from 'react';
import { BadgeImageView } from '../../../../shared/badge-image/BadgeImageView';
import { GroupBadgePart } from '../../../common/GroupBadgePart';
import { useGroupsContext } from '../../../context/GroupsContext';
import { GroupsActions } from '../../../context/GroupsContext.types';
import { GroupSharedTabBadgeViewProps } from './GroupSharedTabBadgeView.types';
const POSITIONS: number[] = [0, 1, 2, 3, 4, 5, 6, 7, 8];
export const GroupSharedTabBadgeView: FC<GroupSharedTabBadgeViewProps> = props =>
{
const { groupsState = null, dispatchGroupsState = null } = useGroupsContext();
const { badgeBases = null, badgeSymbols = null, badgePartColors = null, groupBadgeParts = null } = groupsState;
const { skipDefault = null } = props;
const [ editingIndex, setEditingIndex ] = useState<number>(0);
const [ isSelectingModel, setIsSelectingModel ] = useState<boolean>(false);
useEffect(() =>
{
if(skipDefault || !badgeBases || !badgePartColors || groupBadgeParts) return;
const badgeParts: GroupBadgePart[] = [
new GroupBadgePart(GroupBadgePart.BASE, badgeBases[0].id, badgePartColors[0].id),
new GroupBadgePart(GroupBadgePart.SYMBOL),
new GroupBadgePart(GroupBadgePart.SYMBOL),
new GroupBadgePart(GroupBadgePart.SYMBOL),
new GroupBadgePart(GroupBadgePart.SYMBOL)
];
dispatchGroupsState({
type: GroupsActions.SET_GROUP_BADGE_PARTS,
payload: { badgeParts }
});
}, [ badgeBases, badgePartColors, groupBadgeParts ]);
const selectPartProperty = useCallback((property: string, key: number) =>
{
const clonedBadgeParts = Array.from(groupBadgeParts);
clonedBadgeParts[editingIndex][property] = key;
dispatchGroupsState({
type: GroupsActions.SET_GROUP_BADGE_PARTS,
payload: {
badgeParts: clonedBadgeParts
}
});
if(property === 'key') setIsSelectingModel(false);
}, [ editingIndex, groupBadgeParts, dispatchGroupsState ]);
const getCompleteBadgeCode = useCallback(() =>
{
let code = '';
if(!groupBadgeParts) return code;
groupBadgeParts.forEach((badgePart) =>
{
if(badgePart.code) code = code + badgePart.code;
});
return code;
}, [ groupBadgeParts ]);
const getCurrentPart = useCallback((property: string) =>
{
return groupBadgeParts[editingIndex][property];
}, [ groupBadgeParts, editingIndex ]);
return (
<div className="shared-tab-badge">
<div className="d-flex gap-2">
<div className="badge-preview flex-shrink-0 me-3">
<BadgeImageView badgeCode={ getCompleteBadgeCode() } isGroup={ true } />
</div>
<div>
<div className="d-flex gap-2 align-items-center">
{ groupBadgeParts && groupBadgeParts.map((badgePart, partIndex) =>
{
return (
<div key={ partIndex } className={ 'badge-preview flex-shrink-0 d-flex align-items-center justify-content-center cursor-pointer' + classNames({ ' active': editingIndex === partIndex }) } onClick={ () => setEditingIndex(partIndex) }>
{ badgePart.code && <BadgeImageView badgeCode={ badgePart.code } isGroup={ true } /> }
{ !badgePart.code && <i className="fas fa-plus text-primary h4 m-0" /> }
</div>
)
}) }
</div>
{ !isSelectingModel && groupBadgeParts && <div className="d-flex gap-2 mt-3">
<div className="badge-preview d-flex align-items-center justify-content-center flex-shrink-0 cursor-pointer" onClick={ () => setIsSelectingModel(true) }>
{ getCurrentPart('code') && <BadgeImageView badgeCode={ getCurrentPart('code') } isGroup={ true } /> }
{ !getCurrentPart('code') && <i className="fas fa-plus text-primary h4 m-0" /> }
</div>
<div>
<div className="row row-cols-3 g-0 gap-1 w-100 h-100 overflow-auto">
{ POSITIONS.map((position) =>
{
return <div key={ position } className={ 'position-swatch cursor-pointer' + classNames({ ' active': getCurrentPart('position') === position }) } onClick={ () => selectPartProperty('position', position) }></div>
}) }
</div>
</div>
<div>
<div className="row row-cols-8 g-0 gap-1 w-100 h-100 overflow-auto">
{ badgePartColors && badgePartColors.map((item, index) =>
{
return <div key={ index } className="color-swatch cursor-pointer" style={{ backgroundColor: '#' + item.color }} onClick={ () => selectPartProperty('color', item.id) }></div>
}) }
</div>
</div>
</div> }
{ isSelectingModel && <div className="selection-list row row-cols-8 g-0 align-content-start gap-1 h-100 overflow-auto mt-3">
{ groupBadgeParts[editingIndex].type !== GroupBadgePart.BASE && <>
<div className="cursor-pointer badge-preview d-flex align-items-center justify-content-center" onClick={ () => selectPartProperty('key', 0) }>
<i className="fas fa-times text-primary h4 m-0" />
</div>
</> }
{ (groupBadgeParts[editingIndex].type === GroupBadgePart.BASE ? badgeBases : badgeSymbols).map((item, index) =>
{
return <div key={ index } className={ 'cursor-pointer badge-preview' + classNames({ ' bg-primary': groupBadgeParts[editingIndex].key === item.id }) } onClick={ () => selectPartProperty('key', item.id) }>
<BadgeImageView badgeCode={ GroupBadgePart.getCode(groupBadgeParts[editingIndex].type, item.id, groupBadgeParts[editingIndex].color, 4) } isGroup={ true } />
</div>
}) }
</div>}
</div>
</div>
</div>);
};

View File

@ -0,0 +1,4 @@
export interface GroupSharedTabBadgeViewProps
{
skipDefault?: boolean;
}

View File

@ -0,0 +1,9 @@
.shared-tab-colors {
.color-swatch {
position: relative;
border-radius: $border-radius;
width: 15px;
height: 12px;
}
}

View File

@ -0,0 +1,87 @@
import { FC, useCallback, useEffect, useState } from 'react';
import { LocalizeText } from '../../../../../api';
import { useGroupsContext } from '../../../context/GroupsContext';
import { GroupsActions } from '../../../context/GroupsContext.types';
export const GroupSharedTabColorsView: FC<{}> = props =>
{
const { groupsState = null, dispatchGroupsState = null } = useGroupsContext();
const { groupColors = null, groupColorsA = null, groupColorsB = null } = groupsState;
const [ selectingColorIndex, setSelectingColorIndex ] = useState<number>(0);
useEffect(() =>
{
if(!groupColorsA || !groupColorsB || groupColors) return;
const colors: number[] = [
groupColorsA[0].id,
groupColorsB[0].id
];
dispatchGroupsState({
type: GroupsActions.SET_GROUP_COLORS,
payload: {
objectValues: colors
}
});
}, [ groupColors, groupColorsA, groupColorsB ]);
const getGroupColor = useCallback((colorIndex: number) =>
{
if(colorIndex === 0) return groupColorsA.find(c => c.id === groupColors[colorIndex]).color;
return groupColorsB.find(c => c.id === groupColors[colorIndex]).color;
}, [ groupColors, groupColorsA, groupColorsB ]);
const selectColor = useCallback((colorId: number) =>
{
const clonedGroupColors = Array.from(groupColors);
clonedGroupColors[selectingColorIndex] = colorId;
dispatchGroupsState({
type: GroupsActions.SET_GROUP_COLORS,
payload: {
objectValues: clonedGroupColors
}
});
}, [ selectingColorIndex, groupColors, dispatchGroupsState ]);
return (
<div className="shared-tab-colors d-flex flex-column gap-2">
<div className="d-flex flex-column align-items-center">
<div className="fw-bold">{ LocalizeText('group.edit.color.guild.color') }</div>
{ groupColors && <div className="d-flex">
<div className="group-color-swatch" style={{ backgroundColor: '#' + getGroupColor(0) }}></div>
<div className="group-color-swatch" style={{ backgroundColor: '#' + getGroupColor(1) }}></div>
</div> }
</div>
{ selectingColorIndex === 0 && <div>
<div className="fw-bold">{ LocalizeText('group.edit.color.primary.color') }</div>
<div className="d-flex align-items-center gap-2">
<div className="row row-cols-18 g-0 gap-1 w-100 h-100 overflow-auto">
{ groupColorsA && groupColorsA.map((item, index) =>
{
return <div key={ index } className="color-swatch cursor-pointer" style={{ backgroundColor: '#' + item.color }} onClick={ () => selectColor(item.id) }></div>
}) }
</div>
<div><i className="fas fa-chevron-right h2 m-0 text-primary cursor-pointer" onClick={ () => setSelectingColorIndex(1) } /></div>
</div>
</div> }
{ selectingColorIndex === 1 && <div>
<div className="fw-bold text-end">{ LocalizeText('group.edit.color.secondary.color') }</div>
<div className="d-flex align-items-center gap-2">
<div><i className="fas fa-chevron-left h2 m-0 text-primary cursor-pointer" onClick={ () => setSelectingColorIndex(0) } /></div>
<div className="row row-cols-18 g-0 gap-1 w-100 h-100 overflow-auto">
{ groupColorsB && groupColorsB.map((item, index) =>
{
return <div key={ index } className="color-swatch cursor-pointer" style={{ backgroundColor: '#' + item.color }} onClick={ () => selectColor(item.id) }></div>
}) }
</div>
</div>
</div> }
</div>
);
};

View File

@ -0,0 +1,68 @@
import { FC, useCallback } from 'react';
import { CreateLinkEvent, LocalizeText } from '../../../../../api';
import { useGroupsContext } from '../../../context/GroupsContext';
import { GroupsActions } from '../../../context/GroupsContext.types';
import { GroupSharedTabIdentityViewProps } from './GroupSharedTabIdentityView.types';
export const GroupSharedTabIdentityView: FC<GroupSharedTabIdentityViewProps> = props =>
{
const { groupsState = null, dispatchGroupsState = null } = useGroupsContext();
const { groupName = '', groupDescription = '', groupHomeroomId = 0, availableRooms = null } = groupsState;
const { isCreator = false } = props;
const setName = useCallback((name: string) =>
{
dispatchGroupsState({
type: GroupsActions.SET_GROUP_NAME,
payload: {
stringValues: [ name ]
}
})
}, [ dispatchGroupsState ]);
const setDescription = useCallback((description: string) =>
{
dispatchGroupsState({
type: GroupsActions.SET_GROUP_DESCRIPTION,
payload: {
stringValues: [ description ]
}
})
}, [ dispatchGroupsState ]);
const setHomeroomId = useCallback((id: number) =>
{
dispatchGroupsState({
type: GroupsActions.SET_GROUP_HOMEROOM_ID,
payload: {
numberValues: [ id ]
}
})
}, [ dispatchGroupsState ]);
return (<div className="d-flex flex-column justify-content-center h-100">
<div className="form-group mb-2">
<label className="fw-bold">{ LocalizeText('group.edit.name') }</label>
<input type="text" className="form-control form-control-sm" value={ groupName } maxLength={ 29 } onChange={ (e) => setName(e.target.value) } />
</div>
<div className="form-group mb-2">
<label className="fw-bold">{ LocalizeText('group.edit.desc') }</label>
<input type="text" className="form-control form-control-sm" value={ groupDescription } maxLength={ 254 } onChange={ (e) => setDescription(e.target.value) } />
</div>
{ isCreator && <>
<div className="form-group mb-1">
<label className="fw-bold">{ LocalizeText('group.edit.base') }</label>
<select className="form-select form-select-sm" value={ groupHomeroomId } onChange={ (e) => setHomeroomId(Number(e.target.value)) }>
<option value={ 0 } disabled>{ LocalizeText('group.edit.base.select.room') }</option>
{ availableRooms && availableRooms.map((room, index) =>
{
return <option key={ index } value={ room.id }>{ room.name }</option>;
}) }
</select>
</div>
<div className="small mb-2">{ LocalizeText('group.edit.base.warning') }</div>
<div className="cursor-pointer text-decoration-underline text-center" onClick={ () => CreateLinkEvent('navigator/create') }>{ LocalizeText('group.createroom') }</div>
</> }
</div>);
};

View File

@ -0,0 +1,4 @@
export interface GroupSharedTabIdentityViewProps
{
isCreator?: boolean;
}

View File

@ -1,6 +1,7 @@
import { FC } from 'react';
import { LocalizeText } from '../../../../../api';
import { AvatarImageView } from '../../../../shared/avatar-image/AvatarImageView';
import { UserProfileIconView } from '../../../../shared/user-profile-icon/UserProfileIconView';
import { HallOfFameItemViewProps } from './HallOfFameItemView.types';
export const HallOfFameItemView: FC<HallOfFameItemViewProps> = props =>
@ -11,7 +12,7 @@ export const HallOfFameItemView: FC<HallOfFameItemViewProps> = props =>
<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="fw-bold">{ level }. { data.userName } <UserProfileIconView userId={ data.userId } /></div>
<div className="muted fst-italic small text-center">{ LocalizeText('landing.view.competition.hof.points', [ 'points' ], [ data.currentScore.toString() ])}</div>
</div>
</div>

View File

@ -1,5 +1,6 @@
import { FC } from 'react';
import { FurnitureBackgroundColorView } from './background-color/FurnitureBackgroundColorView';
import { FurnitureBadgeDisplayView } from './badge-display/FurnitureBadgeDisplayView';
import { FurnitureContextMenuView } from './context-menu/FurnitureContextMenuView';
import { FurnitureCustomStackHeightView } from './custom-stack-height/FurnitureCustomStackHeightView';
import { FurnitureDimmerView } from './dimmer/FurnitureDimmerView';
@ -29,6 +30,7 @@ export const FurnitureWidgetsView: FC<{}> = props =>
<FurniturePresentView />
<FurnitureStickieView />
<FurnitureTrophyView />
<FurnitureBadgeDisplayView />
<FurnitureExternalImageView />
</div>
);

View File

@ -1,19 +1,23 @@
import { NitroEvent, RoomEngineTriggerWidgetEvent, StringDataType } from '@nitrots/nitro-renderer';
import { FC, useCallback } from 'react';
import { GetRoomEngine, LocalizeBadgeDescription, LocalizeBadgeName } from '../../../../../api';
import { FC, useCallback, useState } from 'react';
import { GetRoomEngine, LocalizeBadgeDescription, LocalizeBadgeName, RoomWidgetRoomObjectUpdateEvent } from '../../../../../api';
import { CreateEventDispatcherHook } from '../../../../../hooks';
import { useRoomEngineEvent } from '../../../../../hooks/events/nitro/room/room-engine-event';
import { NitroLayoutTrophyView } from '../../../../../layout';
import { useRoomContext } from '../../../context/RoomContext';
import { FurnitureTrophyData } from '../trophy/FurnitureTrophyData';
export const FurnitureBadgeDisplayView: FC<{}> = props =>
{
const { eventDispatcher = null, widgetHandler = null } = useRoomContext();
const [ trophyData, setTrophyData ] = useState<FurnitureTrophyData>(null);
const onNitroEvent = useCallback((event: NitroEvent) =>
{
switch(event.type)
{
case RoomEngineTriggerWidgetEvent.REQUEST_BADGE_DISPLAY_ENGRAVING:
case RoomEngineTriggerWidgetEvent.REQUEST_ACHIEVEMENT_RESOLUTION_ENGRAVING: {
case RoomEngineTriggerWidgetEvent.REQUEST_ACHIEVEMENT_RESOLUTION_ENGRAVING:
case RoomEngineTriggerWidgetEvent.REQUEST_BADGE_DISPLAY_ENGRAVING: {
const widgetEvent = (event as RoomEngineTriggerWidgetEvent);
const roomObject = GetRoomEngine().getRoomObject(widgetEvent.roomId, widgetEvent.objectId, widgetEvent.category);
@ -24,10 +28,23 @@ export const FurnitureBadgeDisplayView: FC<{}> = props =>
stringStuff.initializeFromRoomObjectModel(roomObject.model);
const bagdeName = LocalizeBadgeName(stringStuff.getValue(1));
const badgeName = LocalizeBadgeName(stringStuff.getValue(1));
const badgeDesc = LocalizeBadgeDescription(stringStuff.getValue(1));
const date = stringStuff.getValue(2);
const senderName = stringStuff.getValue(3);
setTrophyData(new FurnitureTrophyData(widgetEvent.objectId, widgetEvent.category, '1', senderName, date, badgeDesc, badgeName));
return;
}
case RoomWidgetRoomObjectUpdateEvent.FURNI_REMOVED: {
const widgetEvent = (event as RoomWidgetRoomObjectUpdateEvent);
setTrophyData(prevState =>
{
if(!prevState || (widgetEvent.id !== prevState.objectId) || (widgetEvent.category !== prevState.category)) return prevState;
return null;
});
return;
}
}
@ -35,20 +52,19 @@ export const FurnitureBadgeDisplayView: FC<{}> = props =>
useRoomEngineEvent(RoomEngineTriggerWidgetEvent.REQUEST_BADGE_DISPLAY_ENGRAVING, onNitroEvent);
useRoomEngineEvent(RoomEngineTriggerWidgetEvent.REQUEST_ACHIEVEMENT_RESOLUTION_ENGRAVING, onNitroEvent);
CreateEventDispatcherHook(RoomWidgetRoomObjectUpdateEvent.FURNI_REMOVED, widgetHandler.eventDispatcher, onNitroEvent);
const processAction = useCallback((type: string, value: string = null) =>
{
switch(type)
{
case 'close':
//setTrophyData(null);
setTrophyData(null);
return;
}
}, []);
return null;
if(!trophyData) return null;
// if(!trophyData) return null;
// return <NitroLayoutTrophyView color={ trophyData.color } message={ trophyData.message } date={ trophyData.date } senderName={ trophyData.ownerName } onCloseClick={ () => processAction('close') } />;
return <NitroLayoutTrophyView color={ trophyData.color } message={ trophyData.message } date={ trophyData.date } senderName={ trophyData.ownerName } customTitle={ trophyData.customTitle } onCloseClick={ () => processAction('close') } />;
}

View File

@ -1,7 +1,7 @@
import { ContextMenuEnum, GroupFurniContextMenuInfoMessageParser, RoomEngineTriggerWidgetEvent, RoomObjectCategory } from '@nitrots/nitro-renderer';
import { GroupFurniContextMenuInfoMessageEvent } from '@nitrots/nitro-renderer/src/nitro/communication/messages/incoming/room/furniture/GroupFurniContextMenuInfoMessageEvent';
import { FC, useCallback, useState } from 'react';
import { GetRoomEngine, IsOwnerOfFurniture, LocalizeText, RoomWidgetFurniActionMessage, TryVisitRoom } from '../../../../../api';
import { GetGroupInformation, GetRoomEngine, IsOwnerOfFurniture, LocalizeText, RoomWidgetFurniActionMessage, TryVisitRoom } from '../../../../../api';
import { TryJoinGroup } from '../../../../../api/groups/TryJoinGroup';
import { CreateMessageHook } from '../../../../../hooks';
import { useRoomEngineEvent } from '../../../../../hooks/events';
@ -183,7 +183,7 @@ export const FurnitureContextMenuView: FC<{}> = props =>
</> }
{ (mode === GROUP_FURNITURE) && groupData &&
<>
<ContextMenuHeaderView>
<ContextMenuHeaderView className="cursor-pointer" onClick={ () => GetGroupInformation(groupData.guildId) }>
{ groupData.guildName }
</ContextMenuHeaderView>
{ !isGroupMember && <ContextMenuListItemView onClick={ event => processAction('join_group') }>

View File

@ -6,5 +6,6 @@ export class FurnitureTrophyData
public color: string,
public ownerName: string,
public date: string,
public message: string) {}
public message: string,
public customTitle?: string) {}
}

View File

@ -7,7 +7,7 @@ export const InfoStandBaseView: FC<InfoStandBaseViewProps> = props =>
return (
<div className="d-flex flex-column nitro-card nitro-infostand rounded">
<div className="container-fluid content-area">
<div className="container-fluid content-area overflow-visible">
<div className="d-flex justify-content-between align-items-center">
<div className="small text-wrap">{ headerText }</div>
<i className="fas fa-times cursor-pointer" onClick={ onCloseClick }></i>

View File

@ -26,7 +26,7 @@ export const InfoStandWidgetBotView: FC<InfoStandWidgetBotViewProps> = props =>
<div className="w-100 d-flex justify-content-center align-items-center">
{ (botData.badges.length > 0) && botData.badges.map(result =>
{
return <BadgeImageView key={ result } badgeCode={ result } />;
return <BadgeImageView key={ result } badgeCode={ result } showInfo={ true } />;
}) }
</div>
</div>

View File

@ -39,7 +39,7 @@ export const InfoStandWidgetRentableBotView: FC<InfoStandWidgetRentableBotViewPr
<div className="w-100 d-flex justify-content-center align-items-center">
{ (rentableBotData.badges.length > 0) && rentableBotData.badges.map(result =>
{
return <BadgeImageView key={ result } badgeCode={ result } />;
return <BadgeImageView key={ result } badgeCode={ result } showInfo={ true } />;
}) }
</div>
</div>

View File

@ -1,6 +1,7 @@
import { RelationshipStatusInfoEvent, RelationshipStatusInfoMessageParser, RoomSessionUserBadgesEvent, UserRelationshipsComposer } from '@nitrots/nitro-renderer';
import classNames from 'classnames';
import { FC, FocusEvent, KeyboardEvent, useCallback, useEffect, useState } from 'react';
import { LocalizeText, RoomWidgetChangeMottoMessage, RoomWidgetUpdateInfostandUserEvent } from '../../../../../../api';
import { GetGroupInformation, LocalizeText, RoomWidgetChangeMottoMessage, RoomWidgetUpdateInfostandUserEvent } from '../../../../../../api';
import { CreateMessageHook, SendMessageHook } from '../../../../../../hooks';
import { CreateEventDispatcherHook } from '../../../../../../hooks/events';
import { AvatarImageView } from '../../../../../shared/avatar-image/AvatarImageView';
@ -82,7 +83,7 @@ export const InfoStandWidgetUserView: FC<InfoStandWidgetUserViewProps> = props =
return (
<div className="d-flex flex-column nitro-card nitro-infostand rounded">
<div className="container-fluid content-area">
<div className="container-fluid content-area overflow-visible">
<div className="d-flex justify-content-between align-items-center">
<div className="small text-wrap">
<UserProfileIconView userId={ userData.webID } />
@ -98,26 +99,26 @@ export const InfoStandWidgetUserView: FC<InfoStandWidgetUserViewProps> = props =
<div>
<div className="d-flex justify-content-between">
<div className="badge-image">
{ badges[0] && <BadgeImageView badgeCode={ badges[0] } /> }
{ badges[0] && <BadgeImageView badgeCode={ badges[0] } showInfo={ true } /> }
</div>
<div className="badge-image">
{ userData.groupId > 0 && <BadgeImageView badgeCode={ userData.groupBadgeId } isGroup={ true } /> }
<div className={ 'badge-image' + classNames({ ' cursor-pointer': userData.groupId > 0 }) } onClick={ () => GetGroupInformation(userData.groupId) }>
{ userData.groupId > 0 && <BadgeImageView badgeCode={ userData.groupBadgeId } isGroup={ true } showInfo={ true } customTitle={ userData.groupName } /> }
</div>
</div>
<div className="d-flex justify-content-between">
<div className="badge-image">
{ badges[1] && <BadgeImageView badgeCode={ badges[1] } /> }
{ badges[1] && <BadgeImageView badgeCode={ badges[1] } showInfo={ true } /> }
</div>
<div className="badge-image">
{ badges[2] && <BadgeImageView badgeCode={ badges[2] } /> }
{ badges[2] && <BadgeImageView badgeCode={ badges[2] } showInfo={ true } /> }
</div>
</div>
<div className="d-flex justify-content-between">
<div className="badge-image">
{ badges[3] && <BadgeImageView badgeCode={ badges[3] } /> }
{ badges[3] && <BadgeImageView badgeCode={ badges[3] } showInfo={ true } /> }
</div>
<div className="badge-image">
{ badges[4] && <BadgeImageView badgeCode={ badges[4] } /> }
{ badges[4] && <BadgeImageView badgeCode={ badges[4] } showInfo={ true } /> }
</div>
</div>
</div>

View File

@ -4,4 +4,16 @@
height: 100%;
background-repeat: no-repeat;
background-position: center;
.badge-information {
display: none;
}
&:hover {
.badge-information {
display: block;
}
}
@import './badge-info/BadgeInformationView';
}

View File

@ -1,10 +1,11 @@
import { FC } from 'react';
import { GetConfiguration } from '../../../api';
import { GetConfiguration, LocalizeBadgeDescription, LocalizeBadgeName, LocalizeText } from '../../../api';
import { BadgeInformationView } from './badge-info/BadgeInformationView';
import { BadgeImageViewProps } from './BadgeImageView.types';
export const BadgeImageView: FC<BadgeImageViewProps> = props =>
{
const { badgeCode = null, isGroup = false } = props;
const { badgeCode = null, isGroup = false, showInfo = false, customTitle = null } = props;
function getBadgeUrl(): string
{
@ -20,5 +21,7 @@ export const BadgeImageView: FC<BadgeImageViewProps> = props =>
const url = `url('${ getBadgeUrl() }')`;
return <div className="badge-image" style={ (url && url.length) ? { backgroundImage: url } : {} }></div>;
return <div className="badge-image" style={ (url && url.length) ? { backgroundImage: url } : {} }>
{ showInfo && <BadgeInformationView title={ isGroup ? customTitle : LocalizeBadgeName(badgeCode) } description={ isGroup ? LocalizeText('group.badgepopup.body') : LocalizeBadgeDescription(badgeCode) } /> }
</div>;
}

View File

@ -2,4 +2,6 @@ export interface BadgeImageViewProps
{
badgeCode: string;
isGroup?: boolean;
showInfo?: boolean;
customTitle?: string;
}

View File

@ -0,0 +1,22 @@
.badge-information {
position: absolute;
pointer-events: none;
user-select: none;
width: 210px;
border-radius: $border-radius;
background: $white;
left: -220px;
z-index: 100;
&:before {
position: absolute;
content: ' ';
width: 0;
height: 0;
border-left: 10px solid $white;
border-bottom: 10px solid transparent;
border-top: 10px solid transparent;
top: 10px;
right: -10px;
}
}

View File

@ -0,0 +1,14 @@
import { FC } from 'react';
import { BadgeInformationViewProps } from './BadgeInformationView.types';
export const BadgeInformationView: FC<BadgeInformationViewProps> = props =>
{
const { title = null, description = null } = props;
return (
<div className="badge-information text-black py-1 px-2 small">
<div className="fw-bold mb-1">{ title }</div>
<div>{ description }</div>
</div>
);
};

View File

@ -0,0 +1,5 @@
export interface BadgeInformationViewProps
{
title: string;
description: string;
}

View File

@ -13,5 +13,11 @@
border-color: $grid-active-border-color !important;
background-color: $grid-active-bg-color !important;
}
.icon {
z-index: 1;
top: 0px;
right: 0px;
}
}
}

View File

@ -1,4 +1,4 @@
import { GroupInformationComposer, GroupInformationEvent, GroupInformationParser } from '@nitrots/nitro-renderer';
import { GroupFavoriteComposer, GroupInformationComposer, GroupInformationEvent, GroupInformationParser } from '@nitrots/nitro-renderer';
import classNames from 'classnames';
import { FC, useCallback, useEffect, useState } from 'react';
import { CreateMessageHook, SendMessageHook } from '../../../../hooks';
@ -36,6 +36,11 @@ export const GroupsContainerView: FC<GroupsContainerViewProps> = props =>
if(selectedGroupId) SendMessageHook(new GroupInformationComposer(selectedGroupId, false));
}, [ selectedGroupId ]);
const favoriteGroup = useCallback((groupId: number) =>
{
SendMessageHook(new GroupFavoriteComposer(groupId));
}, []);
if(!groups) return null;
return (
@ -44,7 +49,8 @@ export const GroupsContainerView: FC<GroupsContainerViewProps> = props =>
<div className="h-100 overflow-auto d-flex flex-column gap-1">
{ groups.map((group, index) =>
{
return <div key={ index } onClick={ () => setSelectedGroupId(group.id) } className={ 'profile-groups-item flex-shrink-0 d-flex align-items-center justify-content-center cursor-pointer' + classNames({ ' active': selectedGroupId === group.id }) }>
return <div key={ index } onClick={ () => setSelectedGroupId(group.id) } className={ 'profile-groups-item position-relative flex-shrink-0 d-flex align-items-center justify-content-center cursor-pointer' + classNames({ ' active': selectedGroupId === group.id }) }>
<i className={ 'position-absolute icon icon-group-' + (group.ownerOrFavorite ? 'favorite' : 'not-favorite') } onClick={ () => favoriteGroup(group.id) } />
<BadgeImageView badgeCode={ group.badge } isGroup={ true } />
</div>
}) }

View File

@ -33,7 +33,7 @@ export const UserContainerView: FC<UserContainerViewProps> = props =>
<div className="col-sm-8">
<div className="user-info-container">
<div className="fw-bold">{userProfile.username}</div>
<div className="fst-italic">{userProfile.motto}</div>
<div className="fst-italic text-break small">{userProfile.motto}</div>
<div dangerouslySetInnerHTML={{ __html: LocalizeText('extendedprofile.created', ['created'], [userProfile.registration]) }} />
<div dangerouslySetInnerHTML={{ __html: LocalizeText('extendedprofile.last.login', ['lastlogin'], [FriendlyTime.format(userProfile.secondsSinceLastVisit, '.ago', 2)]) }} />
<div><b>{LocalizeText('extendedprofile.achievementscore')}</b> {userProfile.achievementPoints}</div>

View File

@ -74,7 +74,7 @@ export const WiredBaseView: FC<WiredBaseViewProps> = props =>
</div>
<NitroCardContentView>
<div className="d-flex align-items-center">
<i className={ `me-2 icon icon-wired-${ wiredType }` } />
<i className={ `me-2 flex-shrink-0 icon icon-wired-${ wiredType }` } />
<div className="fw-bold">{ wiredName }</div>
</div>
<div>{ wiredDescription }</div>