i18n(mod-tools): route every label/title/placeholder through LocalizeText

The ModTools template refresh introduced ~80 hardcoded English strings
(labels, placeholders, tooltips, empty-state copy, button text). Move
every one of them onto the modtools.* namespace and read via
LocalizeText so the panels translate alongside the rest of the client.

UITexts.example (versioned template) extended with the full set:

  modtools.window.*            Launcher box (toolbar item, tools,
                               selected-user state, ticket count)
  modtools.userinfo.*          User info card — already had the
                               legacy modtools.userinfo.{userName,
                               cfhCount, …} keys from before; added
                               refresh tooltip, presence pill labels
                               (in_room / online / offline with
                               matching .title tooltips), section
                               headings, action button labels, stat
                               card labels
  modtools.roominfo.*          Room info card — title, refresh, loading,
                               owner pill (here/away + tooltips), stat
                               labels, action buttons, moderate panel
                               heading + checkboxes + textarea
                               placeholder + caution/alert CTAs
  modtools.user.message.*      Send-message dialog (recipient label,
                               body label, placeholder, char counter,
                               empty state, send button)
  modtools.user.modaction.*    Mod Action form — header, sanctioning
                               label, 3-step section titles, select
                               placeholders, message label + optional
                               note, message placeholder, preview
                               heading, default/apply buttons, every
                               sendAlert error message
  modtools.user.visits.*       Room visits — title, header strip
                               heading, entry count (singular/plural),
                               empty state, column headers, visit
                               button + tooltip
  modtools.user.chatlog.*      User chatlog — title (with username
                               variant), loading state
  modtools.room.chatlog.*      Room chatlog title
  modtools.chatlog.*           Shared ChatlogView — column headers,
                               empty state, room-separator Visit/Tools
                               buttons
  modtools.tickets.*           Tickets window — title, tab labels
                               (open/mine/picked), column headers,
                               empty states, action buttons (pick/
                               handle/release), issue resolution
                               window (title, label, details heading,
                               field labels, chatlog toggle, resolve-as
                               heading, resolution buttons, release
                               back to queue), CFH chatlog title

The same 130 entries land in Nitro-Files/.../UITexts.json (runtime).
Both files validate as JSON. The runtime additions take effect on
next client reload; the template additions ship the strings to any
fresh deploy.

Notes:
  - The MOD_ACTION_DEFINITIONS sanction names ("Alert", "Mute 1h",
    "Ban 18h" …) stay hardcoded for now since they're keyed off
    server-side action IDs that don't have an existing locale key
    convention. Worth a follow-up if needed.
  - help.cfh.topic.* keys (CFH topic display names) are already in
    ExternalTexts.json and were already read via LocalizeText, so
    they didn't need changes.

typecheck + vitest 214/214 + lint:hooks all clean.
This commit is contained in:
simoleo89
2026-05-20 21:02:22 +02:00
committed by simoleo89
parent d3552a0948
commit 75815fa022
16 changed files with 291 additions and 139 deletions
+138
View File
@@ -98,6 +98,144 @@
"catalog.prefix.price.amount": "5 Credits", "catalog.prefix.price.amount": "5 Credits",
"catalog.prefix.purchased": "? Purchased!", "catalog.prefix.purchased": "? Purchased!",
"catalog.prefix.purchase": "Purchase", "catalog.prefix.purchase": "Purchase",
"modtools.userinfo.title": "User Info: %username%",
"modtools.userinfo.userName": "Name",
"modtools.userinfo.cfhCount": "CFHs",
"modtools.userinfo.abusiveCfhCount": "Abusive CFHs",
"modtools.userinfo.cautionCount": "Cautions",
"modtools.userinfo.banCount": "Bans",
"modtools.userinfo.lastSanctionTime": "Last Sanction",
"modtools.userinfo.tradingLockCount": "Trade Locks",
"modtools.userinfo.tradingExpiryDate": "Lock Expires",
"modtools.userinfo.minutesSinceLastLogin": "Last Login",
"modtools.userinfo.lastPurchaseDate": "Last Purchase",
"modtools.userinfo.primaryEmailAddress": "Email",
"modtools.userinfo.identityRelatedBanCount": "Banned Accs",
"modtools.userinfo.registrationAgeInMinutes": "Registered",
"modtools.userinfo.userClassification": "Rank",
"modtools.window.title": "Mod Tools",
"modtools.window.tools.room": "Room Tool",
"modtools.window.tools.chatlog": "Chatlog Tool",
"modtools.window.tools.report": "Report Tool",
"modtools.window.select.user": "Select a user",
"modtools.window.no.room": "Enter a room first",
"modtools.window.user.in_room": "Still in this room",
"modtools.window.user.left_room": "No longer in this room",
"modtools.window.user.clear": "Clear selection",
"modtools.window.tickets.open": "%count% open ticket",
"modtools.window.tickets.open.many": "%count% open tickets",
"modtools.userinfo.refresh": "Refresh user info",
"modtools.userinfo.presence.in_room": "In room",
"modtools.userinfo.presence.in_room.title": "In the room you are observing",
"modtools.userinfo.presence.online": "Online",
"modtools.userinfo.presence.online.title": "Online on the hotel",
"modtools.userinfo.presence.offline": "Offline",
"modtools.userinfo.presence.offline.title": "Offline at panel open",
"modtools.userinfo.section.account": "Account",
"modtools.userinfo.section.activity": "Activity",
"modtools.userinfo.section.sanctions": "Sanctions",
"modtools.userinfo.section.trading": "Trading",
"modtools.userinfo.button.room.chat": "Room Chat",
"modtools.userinfo.button.send.message": "Send Message",
"modtools.userinfo.button.room.visits": "Room Visits",
"modtools.userinfo.button.mod.action": "Mod Action",
"modtools.userinfo.stat.cfh": "CFH",
"modtools.userinfo.stat.cautions": "Cautions",
"modtools.userinfo.stat.bans": "Bans",
"modtools.userinfo.stat.trade.locks": "Trade locks",
"modtools.roominfo.title": "Room Info",
"modtools.roominfo.refresh": "Refresh room info",
"modtools.roominfo.loading": "Loading…",
"modtools.roominfo.owner.here": "Owner here",
"modtools.roominfo.owner.away": "Owner away",
"modtools.roominfo.owner.title.here": "The room owner is currently inside",
"modtools.roominfo.owner.title.away": "The room owner is NOT inside",
"modtools.roominfo.stat.users": "Users",
"modtools.roominfo.stat.owner": "Owner",
"modtools.roominfo.owner.open": "Open %username%'s info",
"modtools.roominfo.button.visit": "Visit Room",
"modtools.roominfo.button.chatlog": "Chatlog",
"modtools.roominfo.moderate.title": "Moderate room",
"modtools.roominfo.moderate.kick": "Kick everyone out",
"modtools.roominfo.moderate.doorbell": "Enable the doorbell",
"modtools.roominfo.moderate.rename": "Change room name",
"modtools.roominfo.moderate.message.placeholder": "Mandatory message to deliver with the action…",
"modtools.roominfo.moderate.send.caution": "Send Caution",
"modtools.roominfo.moderate.send.alert": "Send Alert",
"modtools.user.message.title": "Send Message",
"modtools.user.message.recipient": "Message to",
"modtools.user.message.label": "Message",
"modtools.user.message.placeholder": "Write something useful — the user will see it as a moderator message.",
"modtools.user.message.empty": "Empty",
"modtools.user.message.chars": "%count% chars",
"modtools.user.message.send": "Send Message",
"modtools.user.modaction.title": "Mod Action: %username%",
"modtools.user.modaction.sanctioning": "Sanctioning",
"modtools.user.modaction.step.topic": "1. CFH Topic",
"modtools.user.modaction.step.topic.placeholder": "Select a topic…",
"modtools.user.modaction.step.sanction": "2. Sanction",
"modtools.user.modaction.step.sanction.placeholder": "Select a sanction…",
"modtools.user.modaction.step.message": "3. Custom message",
"modtools.user.modaction.step.message.optional": "(optional — overrides default)",
"modtools.user.modaction.message.placeholder": "Leave empty to use the default topic message",
"modtools.user.modaction.preview": "Preview",
"modtools.user.modaction.button.default": "Default Sanction",
"modtools.user.modaction.button.apply": "Apply Sanction",
"modtools.user.modaction.error.no.topic": "You must select a CFH topic",
"modtools.user.modaction.error.no.action": "You must select a CFH topic and Sanction",
"modtools.user.modaction.error.no.permission": "You do not have permission to do this",
"modtools.user.modaction.error.no.message": "Please write a message to user",
"modtools.user.modaction.error.no.permission.alert": "You have insufficient permissions",
"modtools.user.visits.title": "User Visits",
"modtools.user.visits.recent": "Recent visited rooms",
"modtools.user.visits.entries.one": "%count% entry",
"modtools.user.visits.entries.many": "%count% entries",
"modtools.user.visits.empty": "No recent visits",
"modtools.user.visits.time": "Time",
"modtools.user.visits.room": "Room name",
"modtools.user.visits.action": "Action",
"modtools.user.visits.visit": "Visit",
"modtools.user.visits.visit.title": "Visit room",
"modtools.user.chatlog.title": "User Chatlog",
"modtools.user.chatlog.title.with": "User Chatlog: %username%",
"modtools.user.chatlog.loading": "Loading chatlog…",
"modtools.room.chatlog.title": "Room Chatlog",
"modtools.chatlog.column.time": "Time",
"modtools.chatlog.column.user": "User",
"modtools.chatlog.column.message": "Message",
"modtools.chatlog.empty": "No messages",
"modtools.chatlog.visit": "Visit",
"modtools.chatlog.tools": "Tools",
"modtools.tickets.title": "Tickets",
"modtools.tickets.tab.open": "Open",
"modtools.tickets.tab.mine": "Mine",
"modtools.tickets.tab.picked": "All picked",
"modtools.tickets.column.type": "Type",
"modtools.tickets.column.reported": "Reported",
"modtools.tickets.column.opened": "Opened",
"modtools.tickets.column.picker": "Picker",
"modtools.tickets.empty.open": "No open issues",
"modtools.tickets.empty.mine": "No issues picked by you",
"modtools.tickets.empty.picked": "No picked issues",
"modtools.tickets.action.pick": "Pick",
"modtools.tickets.action.handle": "Handle",
"modtools.tickets.action.release": "Release",
"modtools.tickets.issue.title": "Resolving issue #%issueId%",
"modtools.tickets.issue.label": "Issue #%issueId%",
"modtools.tickets.issue.details": "Details",
"modtools.tickets.issue.field.source": "Source",
"modtools.tickets.issue.field.category": "Category",
"modtools.tickets.issue.field.description": "Description",
"modtools.tickets.issue.field.caller": "Caller",
"modtools.tickets.issue.field.reported": "Reported",
"modtools.tickets.issue.chatlog.view": "View chatlog",
"modtools.tickets.issue.chatlog.close": "Close chatlog",
"modtools.tickets.issue.resolve.heading": "Resolve as",
"modtools.tickets.issue.resolve.resolved": "Resolved",
"modtools.tickets.issue.resolve.useless": "Useless",
"modtools.tickets.issue.resolve.abusive": "Abusive",
"modtools.tickets.issue.release": "Release back to queue",
"modtools.tickets.cfh.chatlog.title": "Issue #%issueId% Chatlog",
"groupforum.list.tab.most_active": "Most active threads", "groupforum.list.tab.most_active": "Most active threads",
"groupforum.list.tab.my_forums": "My group forums", "groupforum.list.tab.my_forums": "My group forums",
"groupforum.list.no_forums": "There are no forums", "groupforum.list.no_forums": "There are no forums",
+9 -9
View File
@@ -143,15 +143,15 @@ export const ModToolsView: FC<{}> = props =>
<> <>
{ isVisible && { isVisible &&
<NitroCardView className="nitro-mod-tools min-w-[220px]" theme="primary-slim" uniqueKey="mod-tools" windowPosition={ DraggableWindowPosition.TOP_LEFT } > <NitroCardView className="nitro-mod-tools min-w-[220px]" theme="primary-slim" uniqueKey="mod-tools" windowPosition={ DraggableWindowPosition.TOP_LEFT } >
<NitroCardHeaderView headerText={ 'Mod Tools' } onCloseClick={ event => setIsVisible(false) } /> <NitroCardHeaderView headerText={ LocalizeText('modtools.window.title') } onCloseClick={ event => setIsVisible(false) } />
<NitroCardContentView className="text-black" gap={ 2 }> <NitroCardContentView className="text-black" gap={ 2 }>
<Button active={ isRoomInfoOpen } disabled={ (currentRoomId <= 0) } gap={ 2 } justifyContent="start" title={ (currentRoomId <= 0) ? noRoomHint : undefined } onClick={ event => CreateLinkEvent(`mod-tools/toggle-room-info/${ currentRoomId }`) }> <Button active={ isRoomInfoOpen } disabled={ (currentRoomId <= 0) } gap={ 2 } justifyContent="start" title={ (currentRoomId <= 0) ? noRoomHint : undefined } onClick={ event => CreateLinkEvent(`mod-tools/toggle-room-info/${ currentRoomId }`) }>
<div className="nitro-icon icon-small-room shrink-0" /> <div className="nitro-icon icon-small-room shrink-0" />
<span className="grow text-start">Room Tool</span> <span className="grow text-start">{ LocalizeText('modtools.window.tools.room') }</span>
</Button> </Button>
<Button active={ isRoomChatlogOpen } disabled={ (currentRoomId <= 0) } gap={ 2 } innerRef={ elementRef } justifyContent="start" title={ (currentRoomId <= 0) ? noRoomHint : undefined } onClick={ event => CreateLinkEvent(`mod-tools/toggle-room-chatlog/${ currentRoomId }`) }> <Button active={ isRoomChatlogOpen } disabled={ (currentRoomId <= 0) } gap={ 2 } innerRef={ elementRef } justifyContent="start" title={ (currentRoomId <= 0) ? noRoomHint : undefined } onClick={ event => CreateLinkEvent(`mod-tools/toggle-room-chatlog/${ currentRoomId }`) }>
<div className="nitro-icon icon-chat-history shrink-0" /> <div className="nitro-icon icon-chat-history shrink-0" />
<span className="grow text-start">Chatlog Tool</span> <span className="grow text-start">{ LocalizeText('modtools.window.tools.chatlog') }</span>
</Button> </Button>
<Button active={ !!isUserInfoOpen } disabled={ !selectedUser } gap={ 2 } justifyContent="start" onClick={ () => selectedUser && CreateLinkEvent(`mod-tools/toggle-user-info/${ selectedUser.userId }`) }> <Button active={ !!isUserInfoOpen } disabled={ !selectedUser } gap={ 2 } justifyContent="start" onClick={ () => selectedUser && CreateLinkEvent(`mod-tools/toggle-user-info/${ selectedUser.userId }`) }>
<div className="nitro-icon icon-user shrink-0" /> <div className="nitro-icon icon-user shrink-0" />
@@ -160,9 +160,9 @@ export const ModToolsView: FC<{}> = props =>
<> <>
<span className="truncate grow text-start">{ selectedUser.username }</span> <span className="truncate grow text-start">{ selectedUser.username }</span>
<span <span
aria-label={ isSelectedUserPresent ? 'In room' : 'Left room' } aria-label={ isSelectedUserPresent ? LocalizeText('modtools.userinfo.presence.in_room') : LocalizeText('modtools.window.user.left_room') }
className={ `inline-block w-2 h-2 rounded-full shrink-0 ${ isSelectedUserPresent ? 'bg-emerald-500' : 'bg-zinc-400' }` } className={ `inline-block w-2 h-2 rounded-full shrink-0 ${ isSelectedUserPresent ? 'bg-emerald-500' : 'bg-zinc-400' }` }
title={ isSelectedUserPresent ? 'Still in this room' : 'No longer in this room' } title={ isSelectedUserPresent ? LocalizeText('modtools.window.user.in_room') : LocalizeText('modtools.window.user.left_room') }
/> />
<span <span
className="inline-flex items-center justify-center w-4 h-4 rounded text-xs text-zinc-500 hover:text-rose-600 hover:bg-rose-100 shrink-0" className="inline-flex items-center justify-center w-4 h-4 rounded text-xs text-zinc-500 hover:text-rose-600 hover:bg-rose-100 shrink-0"
@@ -173,21 +173,21 @@ export const ModToolsView: FC<{}> = props =>
} } } }
role="button" role="button"
tabIndex={ 0 } tabIndex={ 0 }
title="Clear selection"> title={ LocalizeText('modtools.window.user.clear') }>
<FaTimes /> <FaTimes />
</span> </span>
</> </>
) )
: <span className="opacity-50 italic grow text-start">Select a user</span> : <span className="opacity-50 italic grow text-start">{ LocalizeText('modtools.window.select.user') }</span>
} }
</Button> </Button>
<Button active={ isTicketsVisible } gap={ 2 } justifyContent="start" onClick={ () => setIsTicketsVisible(prevValue => !prevValue) }> <Button active={ isTicketsVisible } gap={ 2 } justifyContent="start" onClick={ () => setIsTicketsVisible(prevValue => !prevValue) }>
<div className="nitro-icon icon-tickets shrink-0" /> <div className="nitro-icon icon-tickets shrink-0" />
<span className="grow text-start">Report Tool</span> <span className="grow text-start">{ LocalizeText('modtools.window.tools.report') }</span>
{ (openTicketsCount > 0) && { (openTicketsCount > 0) &&
<span <span
className="inline-flex items-center justify-center min-w-[1.25rem] h-5 px-1 rounded-full bg-rose-500 text-white text-xs font-semibold shrink-0" className="inline-flex items-center justify-center min-w-[1.25rem] h-5 px-1 rounded-full bg-rose-500 text-white text-xs font-semibold shrink-0"
title={ `${ openTicketsCount } open ticket${ openTicketsCount === 1 ? '' : 's' }` }> title={ LocalizeText(openTicketsCount === 1 ? 'modtools.window.tickets.open' : 'modtools.window.tickets.open.many', [ 'count' ], [ openTicketsCount.toString() ]) }>
{ openTicketsCount > 99 ? '99+' : openTicketsCount } { openTicketsCount > 99 ? '99+' : openTicketsCount }
</span> } </span> }
</Button> </Button>
@@ -1,7 +1,7 @@
import { ChatRecordData, CreateLinkEvent } from '@nitrots/nitro-renderer'; import { ChatRecordData, CreateLinkEvent } from '@nitrots/nitro-renderer';
import { FC, useMemo } from 'react'; import { FC, useMemo } from 'react';
import { FaCommentDots, FaDoorOpen, FaSignInAlt, FaTools } from 'react-icons/fa'; import { FaCommentDots, FaDoorOpen, FaSignInAlt, FaTools } from 'react-icons/fa';
import { TryVisitRoom } from '../../../../api'; import { LocalizeText, TryVisitRoom } from '../../../../api';
import { Column, InfiniteScroll } from '../../../../common'; import { Column, InfiniteScroll } from '../../../../common';
import { useModTools } from '../../../../hooks'; import { useModTools } from '../../../../hooks';
import { ChatlogRecord } from './ChatlogRecord'; import { ChatlogRecord } from './ChatlogRecord';
@@ -57,12 +57,12 @@ export const ChatlogView: FC<ChatlogViewProps> = props =>
<button <button
className="inline-flex items-center gap-1 px-2 py-1 rounded text-xs bg-white border border-sky-200 text-sky-700 hover:bg-sky-100 transition-colors" className="inline-flex items-center gap-1 px-2 py-1 rounded text-xs bg-white border border-sky-200 text-sky-700 hover:bg-sky-100 transition-colors"
onClick={ () => TryVisitRoom(props.roomId) }> onClick={ () => TryVisitRoom(props.roomId) }>
<FaSignInAlt size={ 10 } /> Visit <FaSignInAlt size={ 10 } /> { LocalizeText('modtools.chatlog.visit') }
</button> </button>
<button <button
className="inline-flex items-center gap-1 px-2 py-1 rounded text-xs bg-white border border-sky-200 text-sky-700 hover:bg-sky-100 transition-colors" className="inline-flex items-center gap-1 px-2 py-1 rounded text-xs bg-white border border-sky-200 text-sky-700 hover:bg-sky-100 transition-colors"
onClick={ () => openRoomInfo(props.roomId) }> onClick={ () => openRoomInfo(props.roomId) }>
<FaTools size={ 10 } /> Tools <FaTools size={ 10 } /> { LocalizeText('modtools.chatlog.tools') }
</button> </button>
</div> </div>
</div> </div>
@@ -74,14 +74,14 @@ export const ChatlogView: FC<ChatlogViewProps> = props =>
<Column fit gap={ 0 } overflow="hidden"> <Column fit gap={ 0 } overflow="hidden">
{/* Column headers */} {/* Column headers */}
<div className="grid grid-cols-[60px_120px_1fr] gap-2 text-[.7rem] uppercase tracking-wide opacity-60 font-semibold border-b border-zinc-200 pb-1 px-1"> <div className="grid grid-cols-[60px_120px_1fr] gap-2 text-[.7rem] uppercase tracking-wide opacity-60 font-semibold border-b border-zinc-200 pb-1 px-1">
<div>Time</div> <div>{ LocalizeText('modtools.chatlog.column.time') }</div>
<div>User</div> <div>{ LocalizeText('modtools.chatlog.column.user') }</div>
<div>Message</div> <div>{ LocalizeText('modtools.chatlog.column.message') }</div>
</div> </div>
{ isEmpty { isEmpty
? <div className="flex flex-col items-center justify-center gap-1 py-6 opacity-50 text-sm"> ? <div className="flex flex-col items-center justify-center gap-1 py-6 opacity-50 text-sm">
<FaCommentDots size={ 22 } /> <FaCommentDots size={ 22 } />
<span>No messages</span> <span>{ LocalizeText('modtools.chatlog.empty') }</span>
</div> </div>
: <InfiniteScroll rowRender={ (row: ChatlogRecord) => : <InfiniteScroll rowRender={ (row: ChatlogRecord) =>
{ {
@@ -1,5 +1,6 @@
import { ChatRecordData, GetRoomChatlogMessageComposer, RoomChatlogEvent } from '@nitrots/nitro-renderer'; import { ChatRecordData, GetRoomChatlogMessageComposer, RoomChatlogEvent } from '@nitrots/nitro-renderer';
import { FC } from 'react'; import { FC } from 'react';
import { LocalizeText } from '../../../../api';
import { useNitroQuery } from '../../../../api/nitro-query'; import { useNitroQuery } from '../../../../api/nitro-query';
import { DraggableWindowPosition, NitroCardContentView, NitroCardHeaderView, NitroCardView } from '../../../../common'; import { DraggableWindowPosition, NitroCardContentView, NitroCardHeaderView, NitroCardView } from '../../../../common';
import { ChatlogView } from '../chatlog/ChatlogView'; import { ChatlogView } from '../chatlog/ChatlogView';
@@ -27,7 +28,7 @@ export const ModToolsChatlogView: FC<ModToolsChatlogViewProps> = props =>
return ( return (
<NitroCardView className="nitro-mod-tools-chatlog min-w-[460px] max-w-[520px] max-h-[500px]" theme="primary-slim" windowPosition={ DraggableWindowPosition.TOP_LEFT }> <NitroCardView className="nitro-mod-tools-chatlog min-w-[460px] max-w-[520px] max-h-[500px]" theme="primary-slim" windowPosition={ DraggableWindowPosition.TOP_LEFT }>
<NitroCardHeaderView headerText={ 'Room Chatlog' } onCloseClick={ onCloseClick } /> <NitroCardHeaderView headerText={ LocalizeText('modtools.room.chatlog.title') } onCloseClick={ onCloseClick } />
<NitroCardContentView className="text-black" gap={ 1 } overflow="auto"> <NitroCardContentView className="text-black" gap={ 1 } overflow="auto">
<ChatlogView records={ [ roomChatlog ] } /> <ChatlogView records={ [ roomChatlog ] } />
</NitroCardContentView> </NitroCardContentView>
@@ -1,7 +1,7 @@
import { CreateLinkEvent, GetModeratorRoomInfoMessageComposer, ModerateRoomMessageComposer, ModeratorActionMessageComposer, ModeratorRoomInfoEvent } from '@nitrots/nitro-renderer'; import { CreateLinkEvent, GetModeratorRoomInfoMessageComposer, ModerateRoomMessageComposer, ModeratorActionMessageComposer, ModeratorRoomInfoEvent } from '@nitrots/nitro-renderer';
import { FC, useEffect, useState } from 'react'; import { FC, useEffect, useState } from 'react';
import { FaBullhorn, FaCommentDots, FaDoorOpen, FaExclamationTriangle, FaSignInAlt, FaSync, FaUserShield, FaUsers } from 'react-icons/fa'; import { FaBullhorn, FaCommentDots, FaDoorOpen, FaExclamationTriangle, FaSignInAlt, FaSync, FaUserShield, FaUsers } from 'react-icons/fa';
import { SendMessageComposer, TryVisitRoom } from '../../../../api'; import { LocalizeText, SendMessageComposer, TryVisitRoom } from '../../../../api';
import { Button, DraggableWindowPosition, NitroCardContentView, NitroCardHeaderView, NitroCardView, Text } from '../../../../common'; import { Button, DraggableWindowPosition, NitroCardContentView, NitroCardHeaderView, NitroCardView, Text } from '../../../../common';
import { useMessageEvent } from '../../../../hooks'; import { useMessageEvent } from '../../../../hooks';
@@ -80,25 +80,25 @@ export const ModToolsRoomView: FC<ModToolsRoomViewProps> = props =>
return ( return (
<NitroCardView className="nitro-mod-tools-room min-w-[400px] max-w-[460px]" theme="primary-slim" windowPosition={ DraggableWindowPosition.TOP_LEFT }> <NitroCardView className="nitro-mod-tools-room min-w-[400px] max-w-[460px]" theme="primary-slim" windowPosition={ DraggableWindowPosition.TOP_LEFT }>
<NitroCardHeaderView headerText={ 'Room Info' } onCloseClick={ () => onCloseClick() } /> <NitroCardHeaderView headerText={ LocalizeText('modtools.roominfo.title') } onCloseClick={ () => onCloseClick() } />
<NitroCardContentView className="text-black" gap={ 2 }> <NitroCardContentView className="text-black" gap={ 2 }>
{/* Identity header */} {/* Identity header */}
<div className="flex items-center gap-2 bg-gradient-to-r from-sky-50 to-transparent rounded p-2 border border-sky-100"> <div className="flex items-center gap-2 bg-gradient-to-r from-sky-50 to-transparent rounded p-2 border border-sky-100">
<FaDoorOpen className="text-sky-600 shrink-0" size={ 16 } /> <FaDoorOpen className="text-sky-600 shrink-0" size={ 16 } />
<div className="flex flex-col grow min-w-0"> <div className="flex flex-col grow min-w-0">
<Text bold className="truncate text-base leading-tight">{ name || 'Loading' }</Text> <Text bold className="truncate text-base leading-tight">{ name || LocalizeText('modtools.roominfo.loading') }</Text>
<Text className="opacity-60 text-xs truncate">Room #{ roomId }</Text> <Text className="opacity-60 text-xs truncate">#{ roomId }</Text>
</div> </div>
<span <span
className={ `inline-flex items-center gap-1.5 px-2 py-0.5 rounded-full text-xs font-medium border ${ ownerPillClass }` } className={ `inline-flex items-center gap-1.5 px-2 py-0.5 rounded-full text-xs font-medium border ${ ownerPillClass }` }
title={ ownerInRoom ? 'The room owner is currently inside' : 'The room owner is NOT inside' }> title={ ownerInRoom ? LocalizeText('modtools.roominfo.owner.title.here') : LocalizeText('modtools.roominfo.owner.title.away') }>
<span className={ `inline-block w-2 h-2 rounded-full ${ ownerDotClass }` } /> <span className={ `inline-block w-2 h-2 rounded-full ${ ownerDotClass }` } />
{ ownerInRoom ? 'Owner here' : 'Owner away' } { ownerInRoom ? LocalizeText('modtools.roominfo.owner.here') : LocalizeText('modtools.roominfo.owner.away') }
</span> </span>
<button <button
className="inline-flex items-center justify-center w-7 h-7 rounded text-zinc-500 hover:text-sky-700 hover:bg-sky-100 transition-colors shrink-0" className="inline-flex items-center justify-center w-7 h-7 rounded text-zinc-500 hover:text-sky-700 hover:bg-sky-100 transition-colors shrink-0"
onClick={ refresh } onClick={ refresh }
title="Refresh room info"> title={ LocalizeText('modtools.roominfo.refresh') }>
<FaSync size={ 12 } /> <FaSync size={ 12 } />
</button> </button>
</div> </div>
@@ -107,18 +107,18 @@ export const ModToolsRoomView: FC<ModToolsRoomViewProps> = props =>
<div className="flex gap-1.5"> <div className="flex gap-1.5">
<div className="flex flex-col items-center justify-center px-2 py-1.5 rounded border bg-sky-50 border-sky-200 text-sky-700 grow min-w-0"> <div className="flex flex-col items-center justify-center px-2 py-1.5 rounded border bg-sky-50 border-sky-200 text-sky-700 grow min-w-0">
<div className="flex items-center gap-1.5 text-[.7rem] uppercase tracking-wide opacity-70"> <div className="flex items-center gap-1.5 text-[.7rem] uppercase tracking-wide opacity-70">
<FaUsers size={ 10 } /><span>Users</span> <FaUsers size={ 10 } /><span>{ LocalizeText('modtools.roominfo.stat.users') }</span>
</div> </div>
<div className="text-lg font-semibold tabular-nums leading-tight">{ usersInRoom }</div> <div className="text-lg font-semibold tabular-nums leading-tight">{ usersInRoom }</div>
</div> </div>
<div className="flex flex-col items-center justify-center px-2 py-1.5 rounded border bg-zinc-50 border-zinc-200 text-zinc-700 grow min-w-0"> <div className="flex flex-col items-center justify-center px-2 py-1.5 rounded border bg-zinc-50 border-zinc-200 text-zinc-700 grow min-w-0">
<div className="flex items-center gap-1.5 text-[.7rem] uppercase tracking-wide opacity-70"> <div className="flex items-center gap-1.5 text-[.7rem] uppercase tracking-wide opacity-70">
<FaUserShield size={ 10 } /><span>Owner</span> <FaUserShield size={ 10 } /><span>{ LocalizeText('modtools.roominfo.stat.owner') }</span>
</div> </div>
<div <div
className="text-sm font-semibold leading-tight truncate max-w-full underline cursor-pointer hover:text-sky-700" className="text-sm font-semibold leading-tight truncate max-w-full underline cursor-pointer hover:text-sky-700"
onClick={ () => ownerId && CreateLinkEvent(`mod-tools/open-user-info/${ ownerId }`) } onClick={ () => ownerId && CreateLinkEvent(`mod-tools/open-user-info/${ ownerId }`) }
title={ ownerName ? `Open ${ ownerName }'s info` : '' }> title={ ownerName ? LocalizeText('modtools.roominfo.owner.open', [ 'username' ], [ ownerName ]) : '' }>
{ ownerName || '—' } { ownerName || '—' }
</div> </div>
</div> </div>
@@ -127,42 +127,42 @@ export const ModToolsRoomView: FC<ModToolsRoomViewProps> = props =>
{/* Quick actions */} {/* Quick actions */}
<div className="grid grid-cols-2 gap-1.5"> <div className="grid grid-cols-2 gap-1.5">
<Button gap={ 1 } variant="secondary" onClick={ () => TryVisitRoom(roomId) }> <Button gap={ 1 } variant="secondary" onClick={ () => TryVisitRoom(roomId) }>
<FaSignInAlt size={ 12 } /> Visit Room <FaSignInAlt size={ 12 } /> { LocalizeText('modtools.roominfo.button.visit') }
</Button> </Button>
<Button gap={ 1 } variant="secondary" onClick={ () => CreateLinkEvent(`mod-tools/open-room-chatlog/${ roomId }`) }> <Button gap={ 1 } variant="secondary" onClick={ () => CreateLinkEvent(`mod-tools/open-room-chatlog/${ roomId }`) }>
<FaCommentDots size={ 12 } /> Chatlog <FaCommentDots size={ 12 } /> { LocalizeText('modtools.roominfo.button.chatlog') }
</Button> </Button>
</div> </div>
{/* Moderate panel */} {/* Moderate panel */}
<div className="flex flex-col gap-1.5 bg-amber-50 border border-amber-200 rounded p-2"> <div className="flex flex-col gap-1.5 bg-amber-50 border border-amber-200 rounded p-2">
<div className="flex items-center gap-1.5 text-[.7rem] uppercase tracking-wide font-semibold text-amber-800"> <div className="flex items-center gap-1.5 text-[.7rem] uppercase tracking-wide font-semibold text-amber-800">
<FaExclamationTriangle size={ 10 } /> Moderate room <FaExclamationTriangle size={ 10 } /> { LocalizeText('modtools.roominfo.moderate.title') }
</div> </div>
<label className="flex items-center gap-2 text-sm cursor-pointer"> <label className="flex items-center gap-2 text-sm cursor-pointer">
<input checked={ kickUsers } className="form-check-input" type="checkbox" onChange={ event => setKickUsers(event.target.checked) } /> <input checked={ kickUsers } className="form-check-input" type="checkbox" onChange={ event => setKickUsers(event.target.checked) } />
<span>Kick everyone out</span> <span>{ LocalizeText('modtools.roominfo.moderate.kick') }</span>
</label> </label>
<label className="flex items-center gap-2 text-sm cursor-pointer"> <label className="flex items-center gap-2 text-sm cursor-pointer">
<input checked={ lockRoom } className="form-check-input" type="checkbox" onChange={ event => setLockRoom(event.target.checked) } /> <input checked={ lockRoom } className="form-check-input" type="checkbox" onChange={ event => setLockRoom(event.target.checked) } />
<span>Enable the doorbell</span> <span>{ LocalizeText('modtools.roominfo.moderate.doorbell') }</span>
</label> </label>
<label className="flex items-center gap-2 text-sm cursor-pointer"> <label className="flex items-center gap-2 text-sm cursor-pointer">
<input checked={ changeRoomName } className="form-check-input" type="checkbox" onChange={ event => setChangeRoomName(event.target.checked) } /> <input checked={ changeRoomName } className="form-check-input" type="checkbox" onChange={ event => setChangeRoomName(event.target.checked) } />
<span>Change room name</span> <span>{ LocalizeText('modtools.roominfo.moderate.rename') }</span>
</label> </label>
<textarea <textarea
className="min-h-[60px] px-2 py-1.5 rounded text-sm border border-amber-300 bg-white/70 focus:outline-none focus:ring-2 focus:ring-amber-400" className="min-h-[60px] px-2 py-1.5 rounded text-sm border border-amber-300 bg-white/70 focus:outline-none focus:ring-2 focus:ring-amber-400"
placeholder="Mandatory message to deliver with the action…" placeholder={ LocalizeText('modtools.roominfo.moderate.message.placeholder') }
value={ message } value={ message }
onChange={ event => setMessage(event.target.value) } onChange={ event => setMessage(event.target.value) }
/> />
<div className="flex gap-1.5"> <div className="flex gap-1.5">
<Button className="grow" disabled={ !hasMessage || !isLoaded } gap={ 1 } variant="danger" onClick={ () => handleClick('send_message') }> <Button className="grow" disabled={ !hasMessage || !isLoaded } gap={ 1 } variant="danger" onClick={ () => handleClick('send_message') }>
<FaBullhorn size={ 12 } /> Send Caution <FaBullhorn size={ 12 } /> { LocalizeText('modtools.roominfo.moderate.send.caution') }
</Button> </Button>
<Button className="grow" disabled={ !hasMessage || !isLoaded } gap={ 1 } variant="warning" onClick={ () => handleClick('alert_only') }> <Button className="grow" disabled={ !hasMessage || !isLoaded } gap={ 1 } variant="warning" onClick={ () => handleClick('alert_only') }>
<FaExclamationTriangle size={ 12 } /> Send Alert <FaExclamationTriangle size={ 12 } /> { LocalizeText('modtools.roominfo.moderate.send.alert') }
</Button> </Button>
</div> </div>
</div> </div>
@@ -1,6 +1,7 @@
import { CfhChatlogData, CfhChatlogEvent, GetCfhChatlogMessageComposer } from '@nitrots/nitro-renderer'; import { CfhChatlogData, CfhChatlogEvent, GetCfhChatlogMessageComposer } from '@nitrots/nitro-renderer';
import { FC } from 'react'; import { FC } from 'react';
import { FaSpinner } from 'react-icons/fa'; import { FaSpinner } from 'react-icons/fa';
import { LocalizeText } from '../../../../api';
import { useNitroQuery } from '../../../../api/nitro-query'; import { useNitroQuery } from '../../../../api/nitro-query';
import { DraggableWindowPosition, NitroCardContentView, NitroCardHeaderView, NitroCardView } from '../../../../common'; import { DraggableWindowPosition, NitroCardContentView, NitroCardHeaderView, NitroCardView } from '../../../../common';
import { ChatlogView } from '../chatlog/ChatlogView'; import { ChatlogView } from '../chatlog/ChatlogView';
@@ -26,13 +27,13 @@ export const CfhChatlogView: FC<CfhChatlogViewProps> = props =>
return ( return (
<NitroCardView className="nitro-mod-tools-chatlog min-w-[460px] max-w-[520px] max-h-[500px]" theme="primary-slim" windowPosition={ DraggableWindowPosition.TOP_LEFT }> <NitroCardView className="nitro-mod-tools-chatlog min-w-[460px] max-w-[520px] max-h-[500px]" theme="primary-slim" windowPosition={ DraggableWindowPosition.TOP_LEFT }>
<NitroCardHeaderView headerText={ `Issue #${ issueId } Chatlog` } onCloseClick={ onCloseClick } /> <NitroCardHeaderView headerText={ LocalizeText('modtools.tickets.cfh.chatlog.title', [ 'issueId' ], [ issueId.toString() ]) } onCloseClick={ onCloseClick } />
<NitroCardContentView className="text-black" gap={ 1 }> <NitroCardContentView className="text-black" gap={ 1 }>
{ chatlogData { chatlogData
? <ChatlogView records={ [ chatlogData.chatRecord ] } /> ? <ChatlogView records={ [ chatlogData.chatRecord ] } />
: <div className="flex flex-col items-center justify-center gap-2 py-8 opacity-50 text-sm"> : <div className="flex flex-col items-center justify-center gap-2 py-8 opacity-50 text-sm">
<FaSpinner className="animate-spin" size={ 22 } /> <FaSpinner className="animate-spin" size={ 22 } />
<span>Loading chatlog</span> <span>{ LocalizeText('modtools.user.chatlog.loading') }</span>
</div> } </div> }
</NitroCardContentView> </NitroCardContentView>
</NitroCardView> </NitroCardView>
@@ -43,13 +43,13 @@ export const ModToolsIssueInfoView: FC<IssueInfoViewProps> = props =>
return ( return (
<> <>
<NitroCardView className="nitro-mod-tools-handle-issue min-w-[440px] max-w-[500px]" theme="primary-slim" windowPosition={ DraggableWindowPosition.TOP_LEFT }> <NitroCardView className="nitro-mod-tools-handle-issue min-w-[440px] max-w-[500px]" theme="primary-slim" windowPosition={ DraggableWindowPosition.TOP_LEFT }>
<NitroCardHeaderView headerText={ `Resolving issue #${ issueId }` } onCloseClick={ () => onIssueInfoClosed(issueId) } /> <NitroCardHeaderView headerText={ LocalizeText('modtools.tickets.issue.title', [ 'issueId' ], [ issueId.toString() ]) } onCloseClick={ () => onIssueInfoClosed(issueId) } />
<NitroCardContentView className="text-black" gap={ 2 }> <NitroCardContentView className="text-black" gap={ 2 }>
{/* Issue header */} {/* Issue header */}
<div className="flex items-center gap-2 bg-gradient-to-r from-amber-50 to-transparent rounded p-2 border border-amber-100"> <div className="flex items-center gap-2 bg-gradient-to-r from-amber-50 to-transparent rounded p-2 border border-amber-100">
<FaCommentDots className="text-amber-600 shrink-0" size={ 16 } /> <FaCommentDots className="text-amber-600 shrink-0" size={ 16 } />
<div className="flex flex-col grow min-w-0"> <div className="flex flex-col grow min-w-0">
<div className="text-[.7rem] uppercase tracking-wide opacity-60 font-semibold">Issue #{ issueId }</div> <div className="text-[.7rem] uppercase tracking-wide opacity-60 font-semibold">{ LocalizeText('modtools.tickets.issue.label', [ 'issueId' ], [ issueId.toString() ]) }</div>
<div className="font-semibold leading-tight truncate">{ GetIssueCategoryName(ticket.categoryId) }</div> <div className="font-semibold leading-tight truncate">{ GetIssueCategoryName(ticket.categoryId) }</div>
</div> </div>
<span className="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium border bg-white border-amber-200 text-amber-800"> <span className="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium border bg-white border-amber-200 text-amber-800">
@@ -59,19 +59,19 @@ export const ModToolsIssueInfoView: FC<IssueInfoViewProps> = props =>
{/* Details */} {/* Details */}
<div className="flex flex-col gap-1"> <div className="flex flex-col gap-1">
<div className="text-[.7rem] uppercase tracking-wide opacity-60 font-semibold border-b border-zinc-200 pb-1 mb-0.5">Details</div> <div className="text-[.7rem] uppercase tracking-wide opacity-60 font-semibold border-b border-zinc-200 pb-1 mb-0.5">{ LocalizeText('modtools.tickets.issue.details') }</div>
<dl className="grid grid-cols-[auto_1fr] gap-x-3 gap-y-1 text-[.8rem] m-0"> <dl className="grid grid-cols-[auto_1fr] gap-x-3 gap-y-1 text-[.8rem] m-0">
<Field label="Source">{ GetIssueCategoryName(ticket.categoryId) }</Field> <Field label={ LocalizeText('modtools.tickets.issue.field.source') }>{ GetIssueCategoryName(ticket.categoryId) }</Field>
<Field label="Category">{ LocalizeText('help.cfh.topic.' + ticket.reportedCategoryId) }</Field> <Field label={ LocalizeText('modtools.tickets.issue.field.category') }>{ LocalizeText('help.cfh.topic.' + ticket.reportedCategoryId) }</Field>
<Field label="Description">{ ticket.message }</Field> <Field label={ LocalizeText('modtools.tickets.issue.field.description') }>{ ticket.message }</Field>
<Field label="Caller"> <Field label={ LocalizeText('modtools.tickets.issue.field.caller') }>
<button <button
className="font-semibold text-sky-700 hover:text-sky-900 hover:underline inline-flex items-center gap-1" className="font-semibold text-sky-700 hover:text-sky-900 hover:underline inline-flex items-center gap-1"
onClick={ () => openUserInfo(ticket.reporterUserId) }> onClick={ () => openUserInfo(ticket.reporterUserId) }>
{ ticket.reporterUserName } <FaExternalLinkAlt size={ 8 } className="opacity-60" /> { ticket.reporterUserName } <FaExternalLinkAlt size={ 8 } className="opacity-60" />
</button> </button>
</Field> </Field>
<Field label="Reported"> <Field label={ LocalizeText('modtools.tickets.issue.field.reported') }>
<button <button
className="font-semibold text-sky-700 hover:text-sky-900 hover:underline inline-flex items-center gap-1" className="font-semibold text-sky-700 hover:text-sky-900 hover:underline inline-flex items-center gap-1"
onClick={ () => openUserInfo(ticket.reportedUserId) }> onClick={ () => openUserInfo(ticket.reportedUserId) }>
@@ -83,25 +83,25 @@ export const ModToolsIssueInfoView: FC<IssueInfoViewProps> = props =>
{/* Tools */} {/* Tools */}
<Button gap={ 1 } variant="secondary" onClick={ () => setCfhChatlogOpen(prev => !prev) }> <Button gap={ 1 } variant="secondary" onClick={ () => setCfhChatlogOpen(prev => !prev) }>
<FaCommentDots size={ 12 } /> { cfhChatlogOpen ? 'Close chatlog' : 'View chatlog' } <FaCommentDots size={ 12 } /> { cfhChatlogOpen ? LocalizeText('modtools.tickets.issue.chatlog.close') : LocalizeText('modtools.tickets.issue.chatlog.view') }
</Button> </Button>
{/* Resolution buttons */} {/* Resolution buttons */}
<div className="flex flex-col gap-1.5 pt-1 border-t border-zinc-200"> <div className="flex flex-col gap-1.5 pt-1 border-t border-zinc-200">
<div className="text-[.7rem] uppercase tracking-wide opacity-60 font-semibold">Resolve as</div> <div className="text-[.7rem] uppercase tracking-wide opacity-60 font-semibold">{ LocalizeText('modtools.tickets.issue.resolve.heading') }</div>
<div className="grid grid-cols-3 gap-1.5"> <div className="grid grid-cols-3 gap-1.5">
<Button gap={ 1 } variant="success" onClick={ () => closeIssue(CloseIssuesMessageComposer.RESOLUTION_RESOLVED) }> <Button gap={ 1 } variant="success" onClick={ () => closeIssue(CloseIssuesMessageComposer.RESOLUTION_RESOLVED) }>
<FaCheck size={ 11 } /> Resolved <FaCheck size={ 11 } /> { LocalizeText('modtools.tickets.issue.resolve.resolved') }
</Button> </Button>
<Button gap={ 1 } variant="dark" onClick={ () => closeIssue(CloseIssuesMessageComposer.RESOLUTION_USELESS) }> <Button gap={ 1 } variant="dark" onClick={ () => closeIssue(CloseIssuesMessageComposer.RESOLUTION_USELESS) }>
<FaTrashAlt size={ 11 } /> Useless <FaTrashAlt size={ 11 } /> { LocalizeText('modtools.tickets.issue.resolve.useless') }
</Button> </Button>
<Button gap={ 1 } variant="danger" onClick={ () => closeIssue(CloseIssuesMessageComposer.RESOLUTION_ABUSIVE) }> <Button gap={ 1 } variant="danger" onClick={ () => closeIssue(CloseIssuesMessageComposer.RESOLUTION_ABUSIVE) }>
<FaBan size={ 11 } /> Abusive <FaBan size={ 11 } /> { LocalizeText('modtools.tickets.issue.resolve.abusive') }
</Button> </Button>
</div> </div>
<Button gap={ 1 } variant="secondary" onClick={ releaseIssue }> <Button gap={ 1 } variant="secondary" onClick={ releaseIssue }>
<FaSignOutAlt size={ 12 } /> Release back to queue <FaSignOutAlt size={ 12 } /> { LocalizeText('modtools.tickets.issue.release') }
</Button> </Button>
</div> </div>
</NitroCardContentView> </NitroCardContentView>
@@ -1,7 +1,7 @@
import { IssueMessageData, ReleaseIssuesMessageComposer } from '@nitrots/nitro-renderer'; import { IssueMessageData, ReleaseIssuesMessageComposer } from '@nitrots/nitro-renderer';
import { FC, useRef } from 'react'; import { FC, useRef } from 'react';
import { FaClock, FaInbox, FaSignOutAlt, FaTools, FaUser } from 'react-icons/fa'; import { FaClock, FaInbox, FaSignOutAlt, FaTools, FaUser } from 'react-icons/fa';
import { GetIssueCategoryName, SendMessageComposer } from '../../../../api'; import { GetIssueCategoryName, LocalizeText, SendMessageComposer } from '../../../../api';
interface ModToolsMyIssuesTabViewProps interface ModToolsMyIssuesTabViewProps
{ {
@@ -29,16 +29,16 @@ export const ModToolsMyIssuesTabView: FC<ModToolsMyIssuesTabViewProps> = props =
return ( return (
<div className="flex flex-col gap-1 overflow-hidden"> <div className="flex flex-col gap-1 overflow-hidden">
<div className="grid grid-cols-[100px_1fr_100px_90px_90px] gap-2 text-[.7rem] uppercase tracking-wide opacity-60 font-semibold border-b border-zinc-200 pb-1 px-1"> <div className="grid grid-cols-[100px_1fr_100px_90px_90px] gap-2 text-[.7rem] uppercase tracking-wide opacity-60 font-semibold border-b border-zinc-200 pb-1 px-1">
<div>Type</div> <div>{ LocalizeText('modtools.tickets.column.type') }</div>
<div className="flex items-center gap-1"><FaUser size={ 10 } /> Reported</div> <div className="flex items-center gap-1"><FaUser size={ 10 } /> { LocalizeText('modtools.tickets.column.reported') }</div>
<div className="flex items-center gap-1"><FaClock size={ 10 } /> Opened</div> <div className="flex items-center gap-1"><FaClock size={ 10 } /> { LocalizeText('modtools.tickets.column.opened') }</div>
<div></div> <div></div>
<div></div> <div></div>
</div> </div>
{ isEmpty { isEmpty
? <div className="flex flex-col items-center justify-center gap-1 py-8 opacity-50 text-sm"> ? <div className="flex flex-col items-center justify-center gap-1 py-8 opacity-50 text-sm">
<FaInbox size={ 22 } /> <FaInbox size={ 22 } />
<span>No issues picked by you</span> <span>{ LocalizeText('modtools.tickets.empty.mine') }</span>
</div> </div>
: <div className="flex flex-col overflow-auto"> : <div className="flex flex-col overflow-auto">
{ myIssues.map(issue => ( { myIssues.map(issue => (
@@ -53,12 +53,12 @@ export const ModToolsMyIssuesTabView: FC<ModToolsMyIssuesTabViewProps> = props =
<button <button
className="inline-flex items-center justify-center gap-1 px-2 py-1 rounded text-xs font-medium bg-sky-600 text-white hover:bg-sky-700 transition-colors" className="inline-flex items-center justify-center gap-1 px-2 py-1 rounded text-xs font-medium bg-sky-600 text-white hover:bg-sky-700 transition-colors"
onClick={ () => handleIssue(issue.issueId) }> onClick={ () => handleIssue(issue.issueId) }>
<FaTools size={ 10 } /> Handle <FaTools size={ 10 } /> { LocalizeText('modtools.tickets.action.handle') }
</button> </button>
<button <button
className="inline-flex items-center justify-center gap-1 px-2 py-1 rounded text-xs font-medium bg-rose-600 text-white hover:bg-rose-700 transition-colors" className="inline-flex items-center justify-center gap-1 px-2 py-1 rounded text-xs font-medium bg-rose-600 text-white hover:bg-rose-700 transition-colors"
onClick={ () => releaseIssue(issue.issueId) }> onClick={ () => releaseIssue(issue.issueId) }>
<FaSignOutAlt size={ 10 } /> Release <FaSignOutAlt size={ 10 } /> { LocalizeText('modtools.tickets.action.release') }
</button> </button>
</div> </div>
)) } )) }
@@ -1,7 +1,7 @@
import { IssueMessageData, PickIssuesMessageComposer } from '@nitrots/nitro-renderer'; import { IssueMessageData, PickIssuesMessageComposer } from '@nitrots/nitro-renderer';
import { FC, useRef } from 'react'; import { FC, useRef } from 'react';
import { FaClock, FaHandPointer, FaInbox, FaUser } from 'react-icons/fa'; import { FaClock, FaHandPointer, FaInbox, FaUser } from 'react-icons/fa';
import { GetIssueCategoryName, SendMessageComposer } from '../../../../api'; import { GetIssueCategoryName, LocalizeText, SendMessageComposer } from '../../../../api';
interface ModToolsOpenIssuesTabViewProps interface ModToolsOpenIssuesTabViewProps
{ {
@@ -28,15 +28,15 @@ export const ModToolsOpenIssuesTabView: FC<ModToolsOpenIssuesTabViewProps> = pro
return ( return (
<div className="flex flex-col gap-1 overflow-hidden"> <div className="flex flex-col gap-1 overflow-hidden">
<div className="grid grid-cols-[100px_1fr_100px_100px] gap-2 text-[.7rem] uppercase tracking-wide opacity-60 font-semibold border-b border-zinc-200 pb-1 px-1"> <div className="grid grid-cols-[100px_1fr_100px_100px] gap-2 text-[.7rem] uppercase tracking-wide opacity-60 font-semibold border-b border-zinc-200 pb-1 px-1">
<div>Type</div> <div>{ LocalizeText('modtools.tickets.column.type') }</div>
<div className="flex items-center gap-1"><FaUser size={ 10 } /> Reported</div> <div className="flex items-center gap-1"><FaUser size={ 10 } /> { LocalizeText('modtools.tickets.column.reported') }</div>
<div className="flex items-center gap-1"><FaClock size={ 10 } /> Opened</div> <div className="flex items-center gap-1"><FaClock size={ 10 } /> { LocalizeText('modtools.tickets.column.opened') }</div>
<div></div> <div></div>
</div> </div>
{ isEmpty { isEmpty
? <div className="flex flex-col items-center justify-center gap-1 py-8 opacity-50 text-sm"> ? <div className="flex flex-col items-center justify-center gap-1 py-8 opacity-50 text-sm">
<FaInbox size={ 22 } /> <FaInbox size={ 22 } />
<span>No open issues</span> <span>{ LocalizeText('modtools.tickets.empty.open') }</span>
</div> </div>
: <div className="flex flex-col overflow-auto"> : <div className="flex flex-col overflow-auto">
{ openIssues.map(issue => ( { openIssues.map(issue => (
@@ -51,7 +51,7 @@ export const ModToolsOpenIssuesTabView: FC<ModToolsOpenIssuesTabViewProps> = pro
<button <button
className="inline-flex items-center justify-center gap-1 px-2 py-1 rounded text-xs font-medium bg-emerald-600 text-white hover:bg-emerald-700 transition-colors" className="inline-flex items-center justify-center gap-1 px-2 py-1 rounded text-xs font-medium bg-emerald-600 text-white hover:bg-emerald-700 transition-colors"
onClick={ () => pickIssue(issue.issueId) }> onClick={ () => pickIssue(issue.issueId) }>
<FaHandPointer size={ 10 } /> Pick <FaHandPointer size={ 10 } /> { LocalizeText('modtools.tickets.action.pick') }
</button> </button>
</div> </div>
)) } )) }
@@ -1,7 +1,7 @@
import { IssueMessageData } from '@nitrots/nitro-renderer'; import { IssueMessageData } from '@nitrots/nitro-renderer';
import { FC } from 'react'; import { FC } from 'react';
import { FaClock, FaInbox, FaUser, FaUserShield } from 'react-icons/fa'; import { FaClock, FaInbox, FaUser, FaUserShield } from 'react-icons/fa';
import { GetIssueCategoryName } from '../../../../api'; import { GetIssueCategoryName, LocalizeText } from '../../../../api';
interface ModToolsPickedIssuesTabViewProps interface ModToolsPickedIssuesTabViewProps
{ {
@@ -16,15 +16,15 @@ export const ModToolsPickedIssuesTabView: FC<ModToolsPickedIssuesTabViewProps> =
return ( return (
<div className="flex flex-col gap-1 overflow-hidden"> <div className="flex flex-col gap-1 overflow-hidden">
<div className="grid grid-cols-[100px_1fr_100px_120px] gap-2 text-[.7rem] uppercase tracking-wide opacity-60 font-semibold border-b border-zinc-200 pb-1 px-1"> <div className="grid grid-cols-[100px_1fr_100px_120px] gap-2 text-[.7rem] uppercase tracking-wide opacity-60 font-semibold border-b border-zinc-200 pb-1 px-1">
<div>Type</div> <div>{ LocalizeText('modtools.tickets.column.type') }</div>
<div className="flex items-center gap-1"><FaUser size={ 10 } /> Reported</div> <div className="flex items-center gap-1"><FaUser size={ 10 } /> { LocalizeText('modtools.tickets.column.reported') }</div>
<div className="flex items-center gap-1"><FaClock size={ 10 } /> Opened</div> <div className="flex items-center gap-1"><FaClock size={ 10 } /> { LocalizeText('modtools.tickets.column.opened') }</div>
<div className="flex items-center gap-1"><FaUserShield size={ 10 } /> Picker</div> <div className="flex items-center gap-1"><FaUserShield size={ 10 } /> { LocalizeText('modtools.tickets.column.picker') }</div>
</div> </div>
{ isEmpty { isEmpty
? <div className="flex flex-col items-center justify-center gap-1 py-8 opacity-50 text-sm"> ? <div className="flex flex-col items-center justify-center gap-1 py-8 opacity-50 text-sm">
<FaInbox size={ 22 } /> <FaInbox size={ 22 } />
<span>No picked issues</span> <span>{ LocalizeText('modtools.tickets.empty.picked') }</span>
</div> </div>
: <div className="flex flex-col overflow-auto"> : <div className="flex flex-col overflow-auto">
{ pickedIssues.map(issue => ( { pickedIssues.map(issue => (
@@ -1,6 +1,7 @@
import { GetSessionDataManager, IssueMessageData } from '@nitrots/nitro-renderer'; import { GetSessionDataManager, IssueMessageData } from '@nitrots/nitro-renderer';
import { FC, useMemo, useState } from 'react'; import { FC, useMemo, useState } from 'react';
import { FaCheckSquare, FaListUl, FaUserCheck } from 'react-icons/fa'; import { FaCheckSquare, FaListUl, FaUserCheck } from 'react-icons/fa';
import { LocalizeText } from '../../../../api';
import { NitroCardContentView, NitroCardHeaderView, NitroCardTabsItemView, NitroCardTabsView, NitroCardView } from '../../../../common'; import { NitroCardContentView, NitroCardHeaderView, NitroCardTabsItemView, NitroCardTabsView, NitroCardView } from '../../../../common';
import { useModTools } from '../../../../hooks'; import { useModTools } from '../../../../hooks';
import { ModToolsIssueInfoView } from './ModToolsIssueInfoView'; import { ModToolsIssueInfoView } from './ModToolsIssueInfoView';
@@ -96,16 +97,16 @@ export const ModToolsTicketsView: FC<ModToolsTicketsViewProps> = props =>
return ( return (
<> <>
<NitroCardView className="nitro-mod-tools-tickets min-w-[520px] max-w-[640px] max-h-[520px]"> <NitroCardView className="nitro-mod-tools-tickets min-w-[520px] max-w-[640px] max-h-[520px]">
<NitroCardHeaderView headerText={ 'Tickets' } onCloseClick={ onCloseClick } /> <NitroCardHeaderView headerText={ LocalizeText('modtools.tickets.title') } onCloseClick={ onCloseClick } />
<NitroCardTabsView> <NitroCardTabsView>
<NitroCardTabsItemView isActive={ currentTab === 0 } onClick={ () => setCurrentTab(0) }> <NitroCardTabsItemView isActive={ currentTab === 0 } onClick={ () => setCurrentTab(0) }>
<TabLabel label="Open" count={ openIssues.length } icon={ <FaListUl size={ 10 } /> } tone="amber" /> <TabLabel label={ LocalizeText('modtools.tickets.tab.open') } count={ openIssues.length } icon={ <FaListUl size={ 10 } /> } tone="amber" />
</NitroCardTabsItemView> </NitroCardTabsItemView>
<NitroCardTabsItemView isActive={ currentTab === 1 } onClick={ () => setCurrentTab(1) }> <NitroCardTabsItemView isActive={ currentTab === 1 } onClick={ () => setCurrentTab(1) }>
<TabLabel label="Mine" count={ myIssues.length } icon={ <FaUserCheck size={ 10 } /> } tone="sky" /> <TabLabel label={ LocalizeText('modtools.tickets.tab.mine') } count={ myIssues.length } icon={ <FaUserCheck size={ 10 } /> } tone="sky" />
</NitroCardTabsItemView> </NitroCardTabsItemView>
<NitroCardTabsItemView isActive={ currentTab === 2 } onClick={ () => setCurrentTab(2) }> <NitroCardTabsItemView isActive={ currentTab === 2 } onClick={ () => setCurrentTab(2) }>
<TabLabel label="All picked" count={ pickedIssues.length } icon={ <FaCheckSquare size={ 10 } /> } tone="zinc" /> <TabLabel label={ LocalizeText('modtools.tickets.tab.picked') } count={ pickedIssues.length } icon={ <FaCheckSquare size={ 10 } /> } tone="zinc" />
</NitroCardTabsItemView> </NitroCardTabsItemView>
</NitroCardTabsView> </NitroCardTabsView>
<NitroCardContentView gap={ 1 }> <NitroCardContentView gap={ 1 }>
@@ -1,7 +1,7 @@
import { ChatRecordData, GetUserChatlogMessageComposer, UserChatlogEvent } from '@nitrots/nitro-renderer'; import { ChatRecordData, GetUserChatlogMessageComposer, UserChatlogEvent } from '@nitrots/nitro-renderer';
import { FC, useEffect, useState } from 'react'; import { FC, useEffect, useState } from 'react';
import { FaSpinner } from 'react-icons/fa'; import { FaSpinner } from 'react-icons/fa';
import { SendMessageComposer } from '../../../../api'; import { LocalizeText, SendMessageComposer } from '../../../../api';
import { DraggableWindowPosition, NitroCardContentView, NitroCardHeaderView, NitroCardView } from '../../../../common'; import { DraggableWindowPosition, NitroCardContentView, NitroCardHeaderView, NitroCardView } from '../../../../common';
import { useMessageEvent } from '../../../../hooks'; import { useMessageEvent } from '../../../../hooks';
import { ChatlogView } from '../chatlog/ChatlogView'; import { ChatlogView } from '../chatlog/ChatlogView';
@@ -35,13 +35,13 @@ export const ModToolsUserChatlogView: FC<ModToolsUserChatlogViewProps> = props =
return ( return (
<NitroCardView className="nitro-mod-tools-chatlog min-w-[460px] max-w-[520px] max-h-[500px]" theme="primary-slim" windowPosition={ DraggableWindowPosition.TOP_LEFT }> <NitroCardView className="nitro-mod-tools-chatlog min-w-[460px] max-w-[520px] max-h-[500px]" theme="primary-slim" windowPosition={ DraggableWindowPosition.TOP_LEFT }>
<NitroCardHeaderView headerText={ username ? `User Chatlog: ${ username }` : 'User Chatlog' } onCloseClick={ onCloseClick } /> <NitroCardHeaderView headerText={ username ? LocalizeText('modtools.user.chatlog.title.with', [ 'username' ], [ username ]) : LocalizeText('modtools.user.chatlog.title') } onCloseClick={ onCloseClick } />
<NitroCardContentView className="text-black h-full" gap={ 1 }> <NitroCardContentView className="text-black h-full" gap={ 1 }>
{ userChatlog { userChatlog
? <ChatlogView records={ userChatlog } /> ? <ChatlogView records={ userChatlog } />
: <div className="flex flex-col items-center justify-center gap-2 py-8 opacity-50 text-sm"> : <div className="flex flex-col items-center justify-center gap-2 py-8 opacity-50 text-sm">
<FaSpinner className="animate-spin" size={ 22 } /> <FaSpinner className="animate-spin" size={ 22 } />
<span>Loading chatlog</span> <span>{ LocalizeText('modtools.user.chatlog.loading') }</span>
</div> } </div> }
</NitroCardContentView> </NitroCardContentView>
</NitroCardView> </NitroCardView>
@@ -77,7 +77,7 @@ export const ModToolsUserModActionView: FC<ModToolsUserModActionViewProps> = pro
const category = topics[selectedTopic]; const category = topics[selectedTopic];
if(selectedTopic === -1) return sendAlert('You must select a CFH topic'); if(selectedTopic === -1) return sendAlert(LocalizeText('modtools.user.modaction.error.no.topic'));
const messageOrDefault = (message.trim().length === 0) ? LocalizeText(`help.cfh.topic.${ category.id }`) : message; const messageOrDefault = (message.trim().length === 0) ? LocalizeText(`help.cfh.topic.${ category.id }`) : message;
@@ -94,10 +94,10 @@ export const ModToolsUserModActionView: FC<ModToolsUserModActionViewProps> = pro
const category = topics[selectedTopic]; const category = topics[selectedTopic];
const sanction = MOD_ACTION_DEFINITIONS[selectedAction]; const sanction = MOD_ACTION_DEFINITIONS[selectedAction];
if((selectedTopic === -1) || (selectedAction === -1)) errorMessage = 'You must select a CFH topic and Sanction'; if((selectedTopic === -1) || (selectedAction === -1)) errorMessage = LocalizeText('modtools.user.modaction.error.no.action');
else if(!settings || !settings.cfhPermission) errorMessage = 'You do not have permission to do this'; else if(!settings || !settings.cfhPermission) errorMessage = LocalizeText('modtools.user.modaction.error.no.permission');
else if(!category) errorMessage = 'You must select a CFH topic'; else if(!category) errorMessage = LocalizeText('modtools.user.modaction.error.no.topic');
else if(!sanction) errorMessage = 'You must select a sanction'; else if(!sanction) errorMessage = LocalizeText('modtools.user.modaction.error.no.action');
if(errorMessage) return sendAlert(errorMessage); if(errorMessage) return sendAlert(errorMessage);
@@ -106,7 +106,7 @@ export const ModToolsUserModActionView: FC<ModToolsUserModActionViewProps> = pro
switch(sanction.actionType) switch(sanction.actionType)
{ {
case ModActionDefinition.ALERT: { case ModActionDefinition.ALERT: {
if(!settings.alertPermission) return sendAlert('You have insufficient permissions'); if(!settings.alertPermission) return sendAlert(LocalizeText('modtools.user.modaction.error.no.permission.alert'));
SendMessageComposer(new ModAlertMessageComposer(user.userId, messageOrDefault, category.id)); SendMessageComposer(new ModAlertMessageComposer(user.userId, messageOrDefault, category.id));
break; break;
} }
@@ -114,12 +114,12 @@ export const ModToolsUserModActionView: FC<ModToolsUserModActionViewProps> = pro
SendMessageComposer(new ModMuteMessageComposer(user.userId, messageOrDefault, category.id)); SendMessageComposer(new ModMuteMessageComposer(user.userId, messageOrDefault, category.id));
break; break;
case ModActionDefinition.BAN: { case ModActionDefinition.BAN: {
if(!settings.banPermission) return sendAlert('You have insufficient permissions'); if(!settings.banPermission) return sendAlert(LocalizeText('modtools.user.modaction.error.no.permission.alert'));
SendMessageComposer(new ModBanMessageComposer(user.userId, messageOrDefault, category.id, selectedAction, (sanction.actionId === 106))); SendMessageComposer(new ModBanMessageComposer(user.userId, messageOrDefault, category.id, selectedAction, (sanction.actionId === 106)));
break; break;
} }
case ModActionDefinition.KICK: { case ModActionDefinition.KICK: {
if(!settings.kickPermission) return sendAlert('You have insufficient permissions'); if(!settings.kickPermission) return sendAlert(LocalizeText('modtools.user.modaction.error.no.permission.alert'));
SendMessageComposer(new ModKickMessageComposer(user.userId, messageOrDefault, category.id)); SendMessageComposer(new ModKickMessageComposer(user.userId, messageOrDefault, category.id));
break; break;
} }
@@ -129,7 +129,7 @@ export const ModToolsUserModActionView: FC<ModToolsUserModActionViewProps> = pro
break; break;
} }
case ModActionDefinition.MESSAGE: { case ModActionDefinition.MESSAGE: {
if(message.trim().length === 0) return sendAlert('Please write a message to user'); if(message.trim().length === 0) return sendAlert(LocalizeText('modtools.user.modaction.error.no.message'));
SendMessageComposer(new ModMessageMessageComposer(user.userId, message, category.id)); SendMessageComposer(new ModMessageMessageComposer(user.userId, message, category.id));
break; break;
} }
@@ -149,41 +149,43 @@ export const ModToolsUserModActionView: FC<ModToolsUserModActionViewProps> = pro
return ( return (
<NitroCardView className="nitro-mod-tools-user-action min-w-[420px] max-w-[460px]" theme="primary-slim" windowPosition={ DraggableWindowPosition.TOP_LEFT }> <NitroCardView className="nitro-mod-tools-user-action min-w-[420px] max-w-[460px]" theme="primary-slim" windowPosition={ DraggableWindowPosition.TOP_LEFT }>
<NitroCardHeaderView headerText={ `Mod Action: ${ user.username }` } onCloseClick={ () => onCloseClick() } /> <NitroCardHeaderView headerText={ LocalizeText('modtools.user.modaction.title', [ 'username' ], [ user.username ]) } onCloseClick={ () => onCloseClick() } />
<NitroCardContentView className="text-black" gap={ 2 }> <NitroCardContentView className="text-black" gap={ 2 }>
{/* Target header */} {/* Target header */}
<div className="flex items-center gap-2 bg-gradient-to-r from-rose-50 to-transparent rounded p-2 border border-rose-100"> <div className="flex items-center gap-2 bg-gradient-to-r from-rose-50 to-transparent rounded p-2 border border-rose-100">
<FaGavel className="text-rose-600 shrink-0" size={ 16 } /> <FaGavel className="text-rose-600 shrink-0" size={ 16 } />
<div className="flex flex-col grow min-w-0"> <div className="flex flex-col grow min-w-0">
<div className="text-[.7rem] uppercase tracking-wide opacity-60 font-semibold">Sanctioning</div> <div className="text-[.7rem] uppercase tracking-wide opacity-60 font-semibold">{ LocalizeText('modtools.user.modaction.sanctioning') }</div>
<div className="font-semibold leading-tight truncate">{ user.username }</div> <div className="font-semibold leading-tight truncate">{ user.username }</div>
</div> </div>
</div> </div>
{/* CFH topic */} {/* CFH topic */}
<div className="flex flex-col gap-1"> <div className="flex flex-col gap-1">
<label className="text-[.7rem] uppercase tracking-wide opacity-60 font-semibold">1. CFH Topic</label> <label className="text-[.7rem] uppercase tracking-wide opacity-60 font-semibold">{ LocalizeText('modtools.user.modaction.step.topic') }</label>
<select className="form-select form-select-sm" value={ selectedTopic } onChange={ event => setSelectedTopic(parseInt(event.target.value)) }> <select className="form-select form-select-sm" value={ selectedTopic } onChange={ event => setSelectedTopic(parseInt(event.target.value)) }>
<option disabled value={ -1 }>Select a topic</option> <option disabled value={ -1 }>{ LocalizeText('modtools.user.modaction.step.topic.placeholder') }</option>
{ topics.map((topic, index) => <option key={ index } value={ index }>{ LocalizeText('help.cfh.topic.' + topic.id) }</option>) } { topics.map((topic, index) => <option key={ index } value={ index }>{ LocalizeText('help.cfh.topic.' + topic.id) }</option>) }
</select> </select>
</div> </div>
{/* Sanction type */} {/* Sanction type */}
<div className="flex flex-col gap-1"> <div className="flex flex-col gap-1">
<label className="text-[.7rem] uppercase tracking-wide opacity-60 font-semibold">2. Sanction</label> <label className="text-[.7rem] uppercase tracking-wide opacity-60 font-semibold">{ LocalizeText('modtools.user.modaction.step.sanction') }</label>
<select className="form-select form-select-sm" value={ selectedAction } onChange={ event => setSelectedAction(parseInt(event.target.value)) }> <select className="form-select form-select-sm" value={ selectedAction } onChange={ event => setSelectedAction(parseInt(event.target.value)) }>
<option disabled value={ -1 }>Select a sanction</option> <option disabled value={ -1 }>{ LocalizeText('modtools.user.modaction.step.sanction.placeholder') }</option>
{ MOD_ACTION_DEFINITIONS.map((action, index) => <option key={ index } value={ index }>{ action.name }</option>) } { MOD_ACTION_DEFINITIONS.map((action, index) => <option key={ index } value={ index }>{ action.name }</option>) }
</select> </select>
</div> </div>
{/* Message */} {/* Message */}
<div className="flex flex-col gap-1"> <div className="flex flex-col gap-1">
<label className="text-[.7rem] uppercase tracking-wide opacity-60 font-semibold">3. Custom message <span className="opacity-50 normal-case font-normal">(optional overrides default)</span></label> <label className="text-[.7rem] uppercase tracking-wide opacity-60 font-semibold">
{ LocalizeText('modtools.user.modaction.step.message') } <span className="opacity-50 normal-case font-normal">{ LocalizeText('modtools.user.modaction.step.message.optional') }</span>
</label>
<textarea <textarea
className="min-h-[60px] px-2 py-1.5 rounded text-sm border border-zinc-300 focus:outline-none focus:ring-2 focus:ring-rose-300" className="min-h-[60px] px-2 py-1.5 rounded text-sm border border-zinc-300 focus:outline-none focus:ring-2 focus:ring-rose-300"
placeholder="Leave empty to use the default topic message" placeholder={ LocalizeText('modtools.user.modaction.message.placeholder') }
value={ message } value={ message }
onChange={ event => setMessage(event.target.value) } onChange={ event => setMessage(event.target.value) }
/> />
@@ -192,7 +194,7 @@ export const ModToolsUserModActionView: FC<ModToolsUserModActionViewProps> = pro
{/* Preview */} {/* Preview */}
{ (selectedSanction || selectedTopicName) && { (selectedSanction || selectedTopicName) &&
<div className="flex flex-col gap-1 bg-zinc-50 border border-zinc-200 rounded p-2"> <div className="flex flex-col gap-1 bg-zinc-50 border border-zinc-200 rounded p-2">
<div className="text-[.7rem] uppercase tracking-wide opacity-60 font-semibold">Preview</div> <div className="text-[.7rem] uppercase tracking-wide opacity-60 font-semibold">{ LocalizeText('modtools.user.modaction.preview') }</div>
<div className="flex items-center gap-2 flex-wrap"> <div className="flex items-center gap-2 flex-wrap">
{ selectedTopicName && { selectedTopicName &&
<span className="inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-xs font-medium border bg-white border-zinc-200"> <span className="inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-xs font-medium border bg-white border-zinc-200">
@@ -208,10 +210,10 @@ export const ModToolsUserModActionView: FC<ModToolsUserModActionViewProps> = pro
{/* Action buttons */} {/* Action buttons */}
<div className="flex gap-1.5 pt-1 border-t border-zinc-200"> <div className="flex gap-1.5 pt-1 border-t border-zinc-200">
<Button className="grow" disabled={ !canSubmit } gap={ 1 } variant="primary" onClick={ sendDefaultSanction }> <Button className="grow" disabled={ !canSubmit } gap={ 1 } variant="primary" onClick={ sendDefaultSanction }>
<FaBolt size={ 12 } /> Default Sanction <FaBolt size={ 12 } /> { LocalizeText('modtools.user.modaction.button.default') }
</Button> </Button>
<Button className="grow" disabled={ !canSubmit || selectedAction === -1 } gap={ 1 } variant="success" onClick={ sendSanction }> <Button className="grow" disabled={ !canSubmit || selectedAction === -1 } gap={ 1 } variant="success" onClick={ sendSanction }>
<FaGavel size={ 12 } /> Apply Sanction <FaGavel size={ 12 } /> { LocalizeText('modtools.user.modaction.button.apply') }
</Button> </Button>
</div> </div>
</NitroCardContentView> </NitroCardContentView>
@@ -1,7 +1,7 @@
import { GetRoomVisitsMessageComposer, RoomVisitsData, RoomVisitsEvent } from '@nitrots/nitro-renderer'; import { GetRoomVisitsMessageComposer, RoomVisitsData, RoomVisitsEvent } from '@nitrots/nitro-renderer';
import { FC, useEffect, useState } from 'react'; import { FC, useEffect, useState } from 'react';
import { FaClock, FaDoorOpen, FaSignInAlt } from 'react-icons/fa'; import { FaClock, FaDoorOpen, FaSignInAlt } from 'react-icons/fa';
import { SendMessageComposer, TryVisitRoom } from '../../../../api'; import { LocalizeText, SendMessageComposer, TryVisitRoom } from '../../../../api';
import { DraggableWindowPosition, InfiniteScroll, NitroCardContentView, NitroCardHeaderView, NitroCardView } from '../../../../common'; import { DraggableWindowPosition, InfiniteScroll, NitroCardContentView, NitroCardHeaderView, NitroCardView } from '../../../../common';
import { useMessageEvent } from '../../../../hooks'; import { useMessageEvent } from '../../../../hooks';
@@ -35,31 +35,35 @@ export const ModToolsUserRoomVisitsView: FC<ModToolsUserRoomVisitsViewProps> = p
const rows = roomVisitData?.rooms ?? []; const rows = roomVisitData?.rooms ?? [];
const isEmpty = rows.length === 0; const isEmpty = rows.length === 0;
const countLabel = rows.length === 1
? LocalizeText('modtools.user.visits.entries.one', [ 'count' ], [ rows.length.toString() ])
: LocalizeText('modtools.user.visits.entries.many', [ 'count' ], [ rows.length.toString() ]);
return ( return (
<NitroCardView className="nitro-mod-tools-user-visits min-w-[400px] max-w-[460px] max-h-[460px]" theme="primary-slim" windowPosition={ DraggableWindowPosition.TOP_LEFT }> <NitroCardView className="nitro-mod-tools-user-visits min-w-[400px] max-w-[460px] max-h-[460px]" theme="primary-slim" windowPosition={ DraggableWindowPosition.TOP_LEFT }>
<NitroCardHeaderView headerText={ 'User Visits' } onCloseClick={ onCloseClick } /> <NitroCardHeaderView headerText={ LocalizeText('modtools.user.visits.title') } onCloseClick={ onCloseClick } />
<NitroCardContentView className="text-black" gap={ 1 }> <NitroCardContentView className="text-black" gap={ 1 }>
{/* Header strip */} {/* Header strip */}
<div className="flex items-center gap-2 bg-gradient-to-r from-sky-50 to-transparent rounded p-2 border border-sky-100"> <div className="flex items-center gap-2 bg-gradient-to-r from-sky-50 to-transparent rounded p-2 border border-sky-100">
<FaDoorOpen className="text-sky-600 shrink-0" size={ 14 } /> <FaDoorOpen className="text-sky-600 shrink-0" size={ 14 } />
<div className="text-sm font-semibold leading-tight grow">Recent visited rooms</div> <div className="text-sm font-semibold leading-tight grow">{ LocalizeText('modtools.user.visits.recent') }</div>
<span className="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium border bg-white border-zinc-200"> <span className="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium border bg-white border-zinc-200">
{ rows.length } { rows.length === 1 ? 'entry' : 'entries' } { countLabel }
</span> </span>
</div> </div>
{/* Table head */} {/* Table head */}
<div className="grid grid-cols-[60px_1fr_80px] gap-2 text-[.7rem] uppercase tracking-wide opacity-60 font-semibold border-b border-zinc-200 pb-1 px-1"> <div className="grid grid-cols-[60px_1fr_80px] gap-2 text-[.7rem] uppercase tracking-wide opacity-60 font-semibold border-b border-zinc-200 pb-1 px-1">
<div className="flex items-center gap-1"><FaClock size={ 10 } /> Time</div> <div className="flex items-center gap-1"><FaClock size={ 10 } /> { LocalizeText('modtools.user.visits.time') }</div>
<div>Room name</div> <div>{ LocalizeText('modtools.user.visits.room') }</div>
<div className="text-right">Action</div> <div className="text-right">{ LocalizeText('modtools.user.visits.action') }</div>
</div> </div>
{/* Rows */} {/* Rows */}
{ isEmpty { isEmpty
? <div className="flex flex-col items-center justify-center gap-1 py-6 opacity-50 text-sm"> ? <div className="flex flex-col items-center justify-center gap-1 py-6 opacity-50 text-sm">
<FaDoorOpen size={ 22 } /> <FaDoorOpen size={ 22 } />
<span>No recent visits</span> <span>{ LocalizeText('modtools.user.visits.empty') }</span>
</div> </div>
: <div className="flex flex-col grow min-h-0 overflow-hidden"> : <div className="flex flex-col grow min-h-0 overflow-hidden">
<InfiniteScroll rowRender={ row => ( <InfiniteScroll rowRender={ row => (
@@ -71,8 +75,8 @@ export const ModToolsUserRoomVisitsView: FC<ModToolsUserRoomVisitsViewProps> = p
<button <button
className="inline-flex items-center justify-end gap-1 text-sky-700 hover:text-sky-900 hover:underline text-xs" className="inline-flex items-center justify-end gap-1 text-sky-700 hover:text-sky-900 hover:underline text-xs"
onClick={ () => TryVisitRoom(row.roomId) } onClick={ () => TryVisitRoom(row.roomId) }
title="Visit room"> title={ LocalizeText('modtools.user.visits.visit.title') }>
<FaSignInAlt size={ 10 } /> Visit <FaSignInAlt size={ 10 } /> { LocalizeText('modtools.user.visits.visit') }
</button> </button>
</div> </div>
) } rows={ rows } /> ) } rows={ rows } />
@@ -1,7 +1,7 @@
import { ModMessageMessageComposer } from '@nitrots/nitro-renderer'; import { ModMessageMessageComposer } from '@nitrots/nitro-renderer';
import { FC, useState } from 'react'; import { FC, useState } from 'react';
import { FaEnvelope, FaPaperPlane, FaUser } from 'react-icons/fa'; import { FaEnvelope, FaPaperPlane, FaUser } from 'react-icons/fa';
import { ISelectedUser, SendMessageComposer } from '../../../../api'; import { ISelectedUser, LocalizeText, SendMessageComposer } from '../../../../api';
import { Button, DraggableWindowPosition, NitroCardContentView, NitroCardHeaderView, NitroCardView } from '../../../../common'; import { Button, DraggableWindowPosition, NitroCardContentView, NitroCardHeaderView, NitroCardView } from '../../../../common';
import { useNotification } from '../../../../hooks'; import { useNotification } from '../../../../hooks';
@@ -36,13 +36,13 @@ export const ModToolsUserSendMessageView: FC<ModToolsUserSendMessageViewProps> =
return ( return (
<NitroCardView className="nitro-mod-tools-user-message min-w-[360px] max-w-[420px]" theme="primary-slim" windowPosition={ DraggableWindowPosition.TOP_LEFT }> <NitroCardView className="nitro-mod-tools-user-message min-w-[360px] max-w-[420px]" theme="primary-slim" windowPosition={ DraggableWindowPosition.TOP_LEFT }>
<NitroCardHeaderView headerText={ 'Send Message' } onCloseClick={ () => onCloseClick() } /> <NitroCardHeaderView headerText={ LocalizeText('modtools.user.message.title') } onCloseClick={ () => onCloseClick() } />
<NitroCardContentView className="text-black" gap={ 2 }> <NitroCardContentView className="text-black" gap={ 2 }>
{/* Recipient header */} {/* Recipient header */}
<div className="flex items-center gap-2 bg-gradient-to-r from-sky-50 to-transparent rounded p-2 border border-sky-100"> <div className="flex items-center gap-2 bg-gradient-to-r from-sky-50 to-transparent rounded p-2 border border-sky-100">
<FaEnvelope className="text-sky-600 shrink-0" size={ 16 } /> <FaEnvelope className="text-sky-600 shrink-0" size={ 16 } />
<div className="flex flex-col grow min-w-0"> <div className="flex flex-col grow min-w-0">
<div className="text-[.7rem] uppercase tracking-wide opacity-60 font-semibold">Message to</div> <div className="text-[.7rem] uppercase tracking-wide opacity-60 font-semibold">{ LocalizeText('modtools.user.message.recipient') }</div>
<div className="flex items-center gap-1.5 font-semibold leading-tight truncate"> <div className="flex items-center gap-1.5 font-semibold leading-tight truncate">
<FaUser className="opacity-60" size={ 11 } /> <FaUser className="opacity-60" size={ 11 } />
<span className="truncate">{ user.username }</span> <span className="truncate">{ user.username }</span>
@@ -52,21 +52,21 @@ export const ModToolsUserSendMessageView: FC<ModToolsUserSendMessageViewProps> =
{/* Body */} {/* Body */}
<div className="flex flex-col gap-1"> <div className="flex flex-col gap-1">
<label className="text-[.7rem] uppercase tracking-wide opacity-60 font-semibold">Message</label> <label className="text-[.7rem] uppercase tracking-wide opacity-60 font-semibold">{ LocalizeText('modtools.user.message.label') }</label>
<textarea <textarea
autoFocus autoFocus
className="min-h-[100px] px-2 py-1.5 rounded text-sm border border-zinc-300 focus:outline-none focus:ring-2 focus:ring-sky-300" className="min-h-[100px] px-2 py-1.5 rounded text-sm border border-zinc-300 focus:outline-none focus:ring-2 focus:ring-sky-300"
placeholder="Write something useful — the user will see it as a moderator message." placeholder={ LocalizeText('modtools.user.message.placeholder') }
value={ message } value={ message }
onChange={ event => setMessage(event.target.value) } onChange={ event => setMessage(event.target.value) }
/> />
<div className="flex justify-between text-xs opacity-60"> <div className="flex justify-between text-xs opacity-60">
<span>{ canSend ? `${ trimmed.length } chars` : 'Empty' }</span> <span>{ canSend ? LocalizeText('modtools.user.message.chars', [ 'count' ], [ trimmed.length.toString() ]) : LocalizeText('modtools.user.message.empty') }</span>
</div> </div>
</div> </div>
<Button disabled={ !canSend } fullWidth gap={ 1 } variant="primary" onClick={ sendMessage }> <Button disabled={ !canSend } fullWidth gap={ 1 } variant="primary" onClick={ sendMessage }>
<FaPaperPlane size={ 12 } /> Send Message <FaPaperPlane size={ 12 } /> { LocalizeText('modtools.user.message.send') }
</Button> </Button>
</NitroCardContentView> </NitroCardContentView>
</NitroCardView> </NitroCardView>
@@ -76,7 +76,12 @@ export const ModToolsUserView: FC<ModToolsUserViewProps> = props =>
[ roomUserList, userId ] [ roomUserList, userId ]
); );
const isOnline = isPresentInCurrentRoom || !!(userInfo && userInfo.online); const isOnline = isPresentInCurrentRoom || !!(userInfo && userInfo.online);
const presenceLabel = isPresentInCurrentRoom ? 'In room' : (isOnline ? 'Online' : 'Offline'); const presenceLabel = isPresentInCurrentRoom
? LocalizeText('modtools.userinfo.presence.in_room')
: (isOnline ? LocalizeText('modtools.userinfo.presence.online') : LocalizeText('modtools.userinfo.presence.offline'));
const presenceTitle = isPresentInCurrentRoom
? LocalizeText('modtools.userinfo.presence.in_room.title')
: (isOnline ? LocalizeText('modtools.userinfo.presence.online.title') : LocalizeText('modtools.userinfo.presence.offline.title'));
const presencePillClass = isPresentInCurrentRoom const presencePillClass = isPresentInCurrentRoom
? 'bg-emerald-100 text-emerald-700 border-emerald-200' ? 'bg-emerald-100 text-emerald-700 border-emerald-200'
: isOnline : isOnline
@@ -132,60 +137,60 @@ export const ModToolsUserView: FC<ModToolsUserViewProps> = props =>
</div> </div>
<span <span
className={ `inline-flex items-center gap-1.5 px-2 py-0.5 rounded-full text-xs font-medium border ${ presencePillClass }` } className={ `inline-flex items-center gap-1.5 px-2 py-0.5 rounded-full text-xs font-medium border ${ presencePillClass }` }
title={ isPresentInCurrentRoom ? 'In the room you are observing' : (isOnline ? 'Online on the hotel' : 'Offline at panel open') }> title={ presenceTitle }>
<span className={ `inline-block w-2 h-2 rounded-full ${ presenceDotClass }` } /> <span className={ `inline-block w-2 h-2 rounded-full ${ presenceDotClass }` } />
{ presenceLabel } { presenceLabel }
</span> </span>
<button <button
className="inline-flex items-center justify-center w-7 h-7 rounded text-zinc-500 hover:text-sky-700 hover:bg-sky-100 transition-colors shrink-0" className="inline-flex items-center justify-center w-7 h-7 rounded text-zinc-500 hover:text-sky-700 hover:bg-sky-100 transition-colors shrink-0"
onClick={ refresh } onClick={ refresh }
title="Refresh user info"> title={ LocalizeText('modtools.userinfo.refresh') }>
<FaSync size={ 12 } /> <FaSync size={ 12 } />
</button> </button>
</div> </div>
{/* Moderation stat strip */} {/* Moderation stat strip */}
<div className="flex gap-1.5"> <div className="flex gap-1.5">
<StatCard icon={ <FaExclamationTriangle size={ 10 } /> } label="CFH" tone="warn" value={ userInfo.cfhCount } /> <StatCard icon={ <FaExclamationTriangle size={ 10 } /> } label={ LocalizeText('modtools.userinfo.stat.cfh') } tone="warn" value={ userInfo.cfhCount } />
<StatCard icon={ <FaGavel size={ 10 } /> } label="Cautions" tone="warn" value={ userInfo.cautionCount } /> <StatCard icon={ <FaGavel size={ 10 } /> } label={ LocalizeText('modtools.userinfo.stat.cautions') } tone="warn" value={ userInfo.cautionCount } />
<StatCard icon={ <FaBan size={ 10 } /> } label="Bans" tone="danger" value={ userInfo.banCount } /> <StatCard icon={ <FaBan size={ 10 } /> } label={ LocalizeText('modtools.userinfo.stat.bans') } tone="danger" value={ userInfo.banCount } />
<StatCard icon={ <FaExchangeAlt size={ 10 } /> } label="Trade locks" tone="danger" value={ userInfo.tradingLockCount } /> <StatCard icon={ <FaExchangeAlt size={ 10 } /> } label={ LocalizeText('modtools.userinfo.stat.trade.locks') } tone="danger" value={ userInfo.tradingLockCount } />
</div> </div>
{/* Body sections */} {/* Body sections */}
<div className="flex flex-col gap-2 max-h-[300px] overflow-auto pr-1"> <div className="flex flex-col gap-2 max-h-[300px] overflow-auto pr-1">
<Section title="Account"> <Section title={ LocalizeText('modtools.userinfo.section.account') }>
<Field label="Email" value={ userInfo.primaryEmailAddress } /> <Field label={ LocalizeText('modtools.userinfo.primaryEmailAddress') } value={ userInfo.primaryEmailAddress } />
<Field label="Registered" value={ FriendlyTime.format(userInfo.registrationAgeInMinutes * 60, '.ago', 2) } /> <Field label={ LocalizeText('modtools.userinfo.registrationAgeInMinutes') } value={ FriendlyTime.format(userInfo.registrationAgeInMinutes * 60, '.ago', 2) } />
<Field label="Classification" value={ userInfo.userClassification } /> <Field label={ LocalizeText('modtools.userinfo.userClassification') } value={ userInfo.userClassification } />
</Section> </Section>
<Section title="Activity"> <Section title={ LocalizeText('modtools.userinfo.section.activity') }>
<Field label="Last login" value={ FriendlyTime.format(userInfo.minutesSinceLastLogin * 60, '.ago', 2) } /> <Field label={ LocalizeText('modtools.userinfo.minutesSinceLastLogin') } value={ FriendlyTime.format(userInfo.minutesSinceLastLogin * 60, '.ago', 2) } />
<Field label="Last purchase" value={ userInfo.lastPurchaseDate } /> <Field label={ LocalizeText('modtools.userinfo.lastPurchaseDate') } value={ userInfo.lastPurchaseDate } />
</Section> </Section>
<Section title="Sanctions"> <Section title={ LocalizeText('modtools.userinfo.section.sanctions') }>
<Field label="Abusive CFH" value={ userInfo.abusiveCfhCount } /> <Field label={ LocalizeText('modtools.userinfo.abusiveCfhCount') } value={ userInfo.abusiveCfhCount } />
<Field label="Last sanction" value={ userInfo.lastSanctionTime } /> <Field label={ LocalizeText('modtools.userinfo.lastSanctionTime') } value={ userInfo.lastSanctionTime } />
<Field label="Identity bans" value={ userInfo.identityRelatedBanCount } /> <Field label={ LocalizeText('modtools.userinfo.identityRelatedBanCount') } value={ userInfo.identityRelatedBanCount } />
</Section> </Section>
<Section title="Trading"> <Section title={ LocalizeText('modtools.userinfo.section.trading') }>
<Field label="Lock expires" value={ userInfo.tradingExpiryDate } /> <Field label={ LocalizeText('modtools.userinfo.tradingExpiryDate') } value={ userInfo.tradingExpiryDate } />
</Section> </Section>
</div> </div>
{/* Action bar */} {/* Action bar */}
<div className="grid grid-cols-2 gap-1.5 pt-1 border-t border-zinc-200"> <div className="grid grid-cols-2 gap-1.5 pt-1 border-t border-zinc-200">
<Button gap={ 1 } variant="secondary" onClick={ () => CreateLinkEvent(`mod-tools/open-user-chatlog/${ userId }`) }> <Button gap={ 1 } variant="secondary" onClick={ () => CreateLinkEvent(`mod-tools/open-user-chatlog/${ userId }`) }>
<FaCommentDots size={ 12 } /> Room Chat <FaCommentDots size={ 12 } /> { LocalizeText('modtools.userinfo.button.room.chat') }
</Button> </Button>
<Button gap={ 1 } variant="secondary" onClick={ () => setSendMessageVisible(prev => !prev) }> <Button gap={ 1 } variant="secondary" onClick={ () => setSendMessageVisible(prev => !prev) }>
<FaEnvelope size={ 12 } /> Send Message <FaEnvelope size={ 12 } /> { LocalizeText('modtools.userinfo.button.send.message') }
</Button> </Button>
<Button gap={ 1 } variant="secondary" onClick={ () => setRoomVisitsVisible(prev => !prev) }> <Button gap={ 1 } variant="secondary" onClick={ () => setRoomVisitsVisible(prev => !prev) }>
<FaDoorOpen size={ 12 } /> Room Visits <FaDoorOpen size={ 12 } /> { LocalizeText('modtools.userinfo.button.room.visits') }
</Button> </Button>
<Button gap={ 1 } variant="danger" onClick={ () => setModActionVisible(prev => !prev) }> <Button gap={ 1 } variant="danger" onClick={ () => setModActionVisible(prev => !prev) }>
<FaGavel size={ 12 } /> Mod Action <FaGavel size={ 12 } /> { LocalizeText('modtools.userinfo.button.mod.action') }
</Button> </Button>
</div> </div>
</NitroCardContentView> </NitroCardContentView>