nitro-v36-with-renderer-v36
/
src /
components /
room /
widgets /
avatar-info /
infostand /
InfoStandBadgeSlotView.tsx
InfoStandBadgeSlotView.tsx 8.3 Ko
Télécharger le fichier
import { FC, useCallback, useEffect, useRef, useState } from 'react';
import { GetConfigurationValue, LocalizeText } from '../../../../../api';
import { LayoutBadgeImageView, Popover, PopoverContent } from '../../../../../common';
import { useInventoryBadges } from '../../../../../hooks';
import { BaseButton } from '../../../../../common/BaseButton';
import { NativeTextTree } from '../../../../../common/native-text/NativeTextTree';
interface InfoStandBadgeSlotProps {
slotIndex: number;
badgeCode?: string;
isOwnUser: boolean;
}
const BadgeMiniPicker: FC<{
onSelect: (badgeCode: string) => void;
activeBadgeCodes: (string | null)[];
}> = ({ onSelect, activeBadgeCodes }) => {
const { badgeCodes = [], requestBadges = null } = useInventoryBadges();
const [search, setSearch] = useState('');
useEffect(() => {
if (badgeCodes.length === 0) requestBadges();
}, []);
const activeSet = new Set(activeBadgeCodes.filter(Boolean));
const availableBadges = badgeCodes.filter((code) => !activeSet.has(code));
const filtered = search.length > 0 ? availableBadges.filter((code) => code.toLowerCase().includes(search.toLowerCase())) : availableBadges;
return (
<NativeTextTree>
<div onClick={(e) => e.stopPropagation()}>
<input
autoFocus
className="w-full text-xs text-white bg-white/10 border border-white/20 rounded px-2 py-1 mb-2 outline-none focus:border-white/40"
placeholder={LocalizeText('catalog.search')}
type="text"
value={search}
onChange={(e) => setSearch(e.target.value)}
/>
{badgeCodes.length === 0 ? (
<span className="text-xs text-white/40 text-center py-2 block">{LocalizeText('generic.loading')}</span>
) : (
<div className="grid grid-cols-4 gap-1 max-h-[160px] overflow-y-auto">
{filtered.slice(0, 40).map((code) => (
<BaseButton type="button"
key={code}
className="flex items-center justify-center w-[36px] h-[36px] cursor-pointer rounded border border-transparent hover:border-white/40 hover:bg-white/10 transition-all"
onClick={() => onSelect(code)}
>
<LayoutBadgeImageView badgeCode={code} />
</BaseButton>
))}
{filtered.length === 0 && (
<span className="text-xs text-white/40 col-span-4 text-center py-2">{LocalizeText('generic.no_results_found')}</span>
)}
</div>
)}
</div>
</NativeTextTree>
);
};
export const InfoStandBadgeSlotView: FC<InfoStandBadgeSlotProps> = ({ slotIndex, badgeCode: badgeCodeFromProps, isOwnUser }) => {
const { activeBadgeCodes = [], setBadgeAtSlot = null, swapBadges = null, removeBadge = null, requestBadges = null } = useInventoryBadges();
const [isDragOver, setIsDragOver] = useState(false);
const [isDragging, setIsDragging] = useState(false);
const [justDropped, setJustDropped] = useState(false);
const [showPicker, setShowPicker] = useState(false);
const slotRef = useRef<HTMLDivElement>(null);
const hookInitialized = activeBadgeCodes.length > 0;
// Load badge data for own user so hook is initialized before any DnD
useEffect(() => {
if (isOwnUser && !hookInitialized) requestBadges();
}, [isOwnUser, hookInitialized, requestBadges]);
const hookBadge = hookInitialized ? (activeBadgeCodes[slotIndex] ?? null) : null;
// Once hook has data, use ONLY hook data for own user (no stale props fallback)
const badgeCode = isOwnUser ? (hookInitialized ? hookBadge : (badgeCodeFromProps ?? null)) : (badgeCodeFromProps ?? null);
const onDragStart = useCallback(
(event: React.DragEvent) => {
if (!badgeCode || !isOwnUser) return;
event.dataTransfer.setData('badgeCode', badgeCode);
event.dataTransfer.setData('infostandSlot', slotIndex.toString());
event.dataTransfer.effectAllowed = 'move';
setIsDragging(true);
const badgeUrl = GetConfigurationValue<string>('badge.asset.url').replace('%badgename%', badgeCode);
const img = new Image();
img.src = badgeUrl;
event.dataTransfer.setDragImage(img, 20, 20);
},
[badgeCode, slotIndex, isOwnUser]
);
const onDragEnd = useCallback(() => setIsDragging(false), []);
const onDragOver = useCallback(
(event: React.DragEvent) => {
if (!isOwnUser) return;
event.preventDefault();
event.dataTransfer.dropEffect = 'move';
setIsDragOver(true);
},
[isOwnUser]
);
const onDragLeave = useCallback(() => setIsDragOver(false), []);
const onDrop = useCallback(
(event: React.DragEvent) => {
event.preventDefault();
setIsDragOver(false);
if (!isOwnUser) return;
const droppedBadgeCode = event.dataTransfer.getData('badgeCode');
const sourceSlotStr = event.dataTransfer.getData('infostandSlot');
if (!droppedBadgeCode) return;
if (sourceSlotStr !== '') {
const sourceSlot = parseInt(sourceSlotStr);
if (sourceSlot !== slotIndex) swapBadges(sourceSlot, slotIndex);
} else {
setBadgeAtSlot(droppedBadgeCode, slotIndex);
}
setJustDropped(true);
setTimeout(() => setJustDropped(false), 300);
},
[isOwnUser, slotIndex, swapBadges, setBadgeAtSlot]
);
const handleSlotClick = useCallback(() => {
if (!isOwnUser || badgeCode) return;
setShowPicker(true);
}, [isOwnUser, badgeCode]);
const handleDoubleClick = useCallback(() => {
if (!isOwnUser || !badgeCode) return;
removeBadge(badgeCode);
}, [isOwnUser, badgeCode, removeBadge]);
const handlePickerSelect = useCallback(
(code: string) => {
setBadgeAtSlot(code, slotIndex);
setShowPicker(false);
},
[setBadgeAtSlot, slotIndex]
);
return (
<div className="relative">
<div
ref={slotRef}
className={`octane-infostand__badge-slot flex items-center justify-center relative h-[42px] w-[42px] bg-no-repeat bg-center
${isOwnUser && badgeCode ? 'cursor-grab active:cursor-grabbing' : ''}
${isOwnUser && !badgeCode ? 'is-empty cursor-pointer' : ''}
${isDragging ? 'opacity-30' : ''}
${isDragOver ? 'is-drag-over' : ''}
${justDropped ? 'animate-drop-settle' : ''}`}
draggable={isOwnUser && !!badgeCode}
onDragEnd={onDragEnd}
onDragLeave={onDragLeave}
onDragOver={onDragOver}
onDragStart={onDragStart}
onDrop={onDrop}
onClick={handleSlotClick}
onDoubleClick={handleDoubleClick}
>
{badgeCode ? (
<LayoutBadgeImageView badgeCode={badgeCode} showInfo={true} showRarityInfo={true} highlightRarity={true} />
) : (
isOwnUser && (
<span className="octane-infostand__badge-slot-plus" aria-hidden="true">
+
</span>
)
)}
</div>
<Popover open={showPicker} onOpenChange={setShowPicker}>
<PopoverContent
align="start"
anchor={slotRef}
className="z-[9999] bg-[rgba(28,28,32,0.97)] border border-white/20 rounded-md p-2 shadow-lg min-w-[160px] w-[180px]"
positionerClassName="z-[9999]"
side="right"
sideOffset={8}
>
<BadgeMiniPicker activeBadgeCodes={activeBadgeCodes} onSelect={handlePickerSelect} />
</PopoverContent>
</Popover>
</div>
);
};
