WiredSelfDonationView.tsx 9.4 Ko
Télécharger le fichier
import { FurnitureType, GetSessionDataManager, IFurnitureData, SelfDonationMessageComposer, SelfDonationResultMessageEvent } from '@octane/renderer';
import { useMemo, useState } from 'react';
import { GetConfigurationValue, localizeWithFallback, NotificationAlertType, SendMessageComposer } from '../../api';
import { Button, DraggableWindowPosition, OctaneCardContentView, OctaneCardHeaderView, OctaneCardView, Text } from '../../common';
import { useMessageEvent, useNotification } from '../../hooks';
import { NumberInput } from '../../common/NumberInput';
import { NativeTextTree } from '../../common/native-text/NativeTextTree';
/** The official tool's range; the server clamps to its own `hotel.selfdonation.max.amount`. */
const DEFAULT_MAX_AMOUNT = 500;
/** Rows shown for a search; a narrower search shows the rest. */
const MAX_RESULTS = 60;
/** The official result codes of `SelfDonationResult`. */
const RESULT_SUCCESS = 0;
const RESULT_NOT_ALLOWED = 1;
export interface WiredSelfDonationViewProps {
onClose: () => void;
}
export interface SelfDonationCandidate {
id: number;
className: string;
name: string;
isWallItem: boolean;
}
/** The furni types matching a search, floor and wall alike, by name or classname. */
export const searchFurnitureTypes = (all: IFurnitureData[], query: string, limit: number = MAX_RESULTS): SelfDonationCandidate[] => {
const needle = query.trim().toLowerCase();
const results: SelfDonationCandidate[] = [];
for (const data of all) {
if (!data || !data.className) continue;
const name = data.name || data.className;
if (needle && !name.toLowerCase().includes(needle) && !data.className.toLowerCase().includes(needle)) continue;
results.push({ id: data.id, className: data.className, name, isWallItem: data.type === FurnitureType.WALL });
if (results.length >= limit) break;
}
return results;
};
/**
* The sandbox self-donation tool: an amount, a furni type picked from a search, and "Donate",
* which asks the server for that many of the furni. The answer comes back as an alert; the
* server decides who may use it.
*/
export const WiredSelfDonationView = ({ onClose }: WiredSelfDonationViewProps) => {
const { simpleAlert = null } = useNotification();
const maxAmount = Math.max(1, GetConfigurationValue<number>('wired.selfdonation.max.amount', DEFAULT_MAX_AMOUNT));
const [amount, setAmount] = useState(1);
const [query, setQuery] = useState('');
const [selected, setSelected] = useState<SelfDonationCandidate | null>(null);
const allFurniture = useMemo(() => GetSessionDataManager().getAllFurnitureData() ?? [], []);
const results = useMemo(() => searchFurnitureTypes(allFurniture, query), [allFurniture, query]);
useMessageEvent<SelfDonationResultMessageEvent>(SelfDonationResultMessageEvent, (event) => {
const code = event.getParser()?.resultCode;
if (!simpleAlert) return;
if (code === RESULT_SUCCESS) {
simpleAlert(localizeWithFallback('selfdonation.result.success', 'The furni is in your inventory.'), NotificationAlertType.DEFAULT);
} else if (code === RESULT_NOT_ALLOWED) {
simpleAlert(localizeWithFallback('selfdonation.result.not_allowed', 'You are not allowed to use the sandbox donation tool.'), NotificationAlertType.DEFAULT);
} else {
simpleAlert(localizeWithFallback('selfdonation.result.failed', 'The donation failed.'), NotificationAlertType.DEFAULT);
}
});
const clampedAmount = Math.min(maxAmount, Math.max(1, amount));
const donate = () => {
if (!selected) return;
SendMessageComposer(new SelfDonationMessageComposer(selected.isWallItem, selected.id, '', clampedAmount));
};
return (
<NativeTextTree>
<OctaneCardView
className="octane-wired-menu min-w-[420px] max-w-[420px] max-h-[560px]"
theme="primary-slim"
uniqueKey="wired-self-donation"
windowPosition={DraggableWindowPosition.TOP_LEFT}
offsetLeft={560}
offsetTop={80}
>
<OctaneCardHeaderView headerText={localizeWithFallback('selfdonation.title', 'Sandbox donation tool')} onCloseClick={onClose} />
<OctaneCardContentView className="text-black bg-[#f4efe3] p-3 flex flex-col gap-3" overflow="hidden">
<label className="flex items-center gap-2 text-[12px]">
<Text bold>{localizeWithFallback('selfdonation.amount', 'Amount')}:</Text>
<NumberInput aria-label="Amount" className="w-[80px] rounded border border-[#b8b2a4] bg-white px-2 py-[2px] text-[12px]" min={1} max={maxAmount} value={amount} onChange={(event) => setAmount(Number.parseInt(event.target.value, 10) || 1)} onBlur={() => setAmount(clampedAmount)} />
<Text small>{localizeWithFallback('selfdonation.amount.range', '1 to %max%', ['max'], [String(maxAmount)])}</Text>
</label>
<label className="flex items-center gap-2 text-[12px]">
<Text bold>{localizeWithFallback('selfdonation.search', 'Furni')}:</Text>
<input
aria-label="Search furni"
className="grow rounded border border-[#b8b2a4] bg-white px-2 py-[2px] text-[12px]"
type="text"
value={query}
placeholder={localizeWithFallback('selfdonation.search.placeholder', 'Name or classname')}
onChange={(event) => setQuery(event.target.value)}
/>
</label>
<div className="h-[300px] overflow-y-auto border border-[#d1ccbf] rounded bg-white" data-testid="self-donation-results">
<table className="w-full text-[12px] table-fixed">
<thead className="bg-[#efede5] sticky top-0">
<tr>
<th className="text-left px-2 py-1 w-[60px]">{localizeWithFallback('selfdonation.col.type', 'Type')}</th>
<th className="text-left px-2 py-1">{localizeWithFallback('selfdonation.col.name', 'Name')}</th>
<th className="text-left px-2 py-1 w-[150px]">{localizeWithFallback('selfdonation.col.classname', 'Classname')}</th>
</tr>
</thead>
<tbody>
{!results.length && (
<tr>
<td className="px-2 py-3 text-center text-[#8b8678]" colSpan={3}>
{localizeWithFallback('selfdonation.empty', 'No furni matches the search')}
</td>
</tr>
)}
{results.map((candidate) => {
const isSelected = selected?.id === candidate.id && selected?.isWallItem === candidate.isWallItem;
return (
<tr
key={`${candidate.isWallItem ? 'w' : 'f'}-${candidate.id}`}
className={`cursor-pointer ${isSelected ? 'bg-[#dbeaf4]' : 'hover:bg-[#f8f6f0]'}`}
aria-selected={isSelected}
onClick={() => setSelected(candidate)}
>
<td className="px-2 py-1">{candidate.isWallItem ? 'Wall' : 'Floor'}</td>
<td className="px-2 py-1 truncate" title={candidate.name}>
{candidate.name}
</td>
<td className="px-2 py-1 truncate" title={candidate.className}>
{candidate.className}
</td>
</tr>
);
})}
</tbody>
</table>
</div>
<div className="flex items-center justify-between gap-2">
<Text small>
{selected
? localizeWithFallback('selfdonation.selected', 'Selected: %name%', ['name'], [selected.name])
: localizeWithFallback('selfdonation.none_selected', 'Pick a furni from the list')}
</Text>
<div className="flex gap-2">
<Button variant="secondary" onClick={onClose}>
{localizeWithFallback('generic.cancel', 'Cancel')}
</Button>
<Button disabled={!selected} onClick={donate}>
{localizeWithFallback('selfdonation.donate', 'Donate')}
</Button>
</div>
</div>
</OctaneCardContentView>
</OctaneCardView>
</NativeTextTree>
);
};
