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:
+53
-2
@@ -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'}`}
|
||||
|
||||
Reference in New Issue
Block a user