Encounter drag reorder via order field

Encounter cards draggable (ChevronsUpDown handle). Drop reorders, batch
updates order field on affected encounter docs. Sort by order (fallback
createdAt). New encounter gets order = max+1.
This commit is contained in:
david raistrick
2026-07-07 15:18:08 -04:00
parent f3ed971592
commit 94126567e7
+53 -2
View File
@@ -2012,6 +2012,7 @@ function EncounterManager({ campaignId, initialActiveEncounterId, campaignCharac
const [showCreateModal, setShowCreateModal] = useState(false);
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
const [itemToDelete, setItemToDelete] = useState(null);
const [draggedEncounterId, setDraggedEncounterId] = useState(null);
const selectedEncounterIdRef = useRef(selectedEncounterId);
@@ -2019,6 +2020,45 @@ function EncounterManager({ campaignId, initialActiveEncounterId, campaignCharac
if (encountersData) setEncounters(encountersData);
}, [encountersData]);
// Sort by order field (fallback createdAt). Drag reorder writes order.
const sortedEncounters = [...(encounters || [])].sort((a, b) => {
const ao = a.order ?? null, bo = b.order ?? null;
if (ao !== null && bo !== null) return ao - bo;
if (ao !== null) return -1;
if (bo !== null) return 1;
return (a.createdAt || '').localeCompare(b.createdAt || '');
});
const handleEncounterDragStart = (e, id) => {
setDraggedEncounterId(id);
e.dataTransfer.effectAllowed = 'move';
};
const handleEncounterDragOver = (e) => {
e.preventDefault();
e.dataTransfer.dropEffect = 'move';
};
const handleEncounterDrop = async (e, targetId) => {
e.preventDefault();
if (!db || !draggedEncounterId || draggedEncounterId === targetId) {
setDraggedEncounterId(null);
return;
}
const reordered = [...sortedEncounters];
const fromIdx = reordered.findIndex(x => x.id === draggedEncounterId);
const toIdx = reordered.findIndex(x => x.id === targetId);
if (fromIdx === -1 || toIdx === -1) { setDraggedEncounterId(null); return; }
const [moved] = reordered.splice(fromIdx, 1);
reordered.splice(toIdx, 0, moved);
// assign order = index, batch update
const ops = reordered.map((enc, i) => ({
type: 'update',
path: `${getPath.encounters(campaignId)}/${enc.id}`,
data: { order: i },
}));
try { await storage.batchWrite(ops); } catch (err) {}
setDraggedEncounterId(null);
};
useEffect(() => {
selectedEncounterIdRef.current = selectedEncounterId;
}, [selectedEncounterId]);
@@ -2063,6 +2103,7 @@ function EncounterManager({ campaignId, initialActiveEncounterId, campaignCharac
isStarted: false,
isPaused: false,
ruleset: ruleset || '5e',
order: (encounters || []).reduce((max, e) => Math.max(max, e.order ?? -1), -1) + 1,
});
setShowCreateModal(false);
@@ -2169,7 +2210,7 @@ function EncounterManager({ campaignId, initialActiveEncounterId, campaignCharac
)}
<div className="space-y-3">
{encounters?.map(encounter => {
{sortedEncounters.map(encounter => {
const isLive = activeDisplayInfo &&
activeDisplayInfo.activeCampaignId === campaignId &&
activeDisplayInfo.activeEncounterId === encounter.id;
@@ -2177,7 +2218,12 @@ function EncounterManager({ campaignId, initialActiveEncounterId, campaignCharac
return (
<div
key={encounter.id}
className={`p-3 rounded-md shadow transition-all ${selectedEncounterId === encounter.id ? 'bg-amber-900 ring-2 ring-amber-500' : 'bg-stone-800 hover:bg-stone-700'} ${isLive ? 'ring-2 ring-green-500 shadow-md shadow-green-500/30' : ''}`}
draggable
onDragStart={(e) => handleEncounterDragStart(e, encounter.id)}
onDragOver={handleEncounterDragOver}
onDrop={(e) => handleEncounterDrop(e, encounter.id)}
onDragEnd={() => setDraggedEncounterId(null)}
className={`p-3 rounded-md shadow transition-all ${selectedEncounterId === encounter.id ? 'bg-amber-900 ring-2 ring-amber-500' : 'bg-stone-800 hover:bg-stone-700'} ${isLive ? 'ring-2 ring-green-500 shadow-md shadow-green-500/30' : ''} ${draggedEncounterId === encounter.id ? 'opacity-50 ring-2 ring-yellow-400' : ''} cursor-grab`}
>
<div className="flex justify-between items-center">
<div onClick={() => setSelectedEncounterId(encounter.id)} className="cursor-pointer flex-grow">
@@ -2192,6 +2238,11 @@ function EncounterManager({ campaignId, initialActiveEncounterId, campaignCharac
)}
</div>
<div className="flex items-center space-x-2">
<ChevronsUpDown
size={18}
className="text-stone-400 flex-shrink-0 cursor-grab"
title="Drag to reorder"
/>
<button
onClick={() => handleTogglePlayerDisplay(encounter.id)}
className={`p-1 rounded transition-colors ${isLive ? 'bg-red-500 hover:bg-red-600 text-white' : 'text-amber-400 hover:text-amber-300 bg-stone-700 hover:bg-stone-600'}`}