diff --git a/src/App.js b/src/App.js index 84ff2aa..f9f3497 100644 --- a/src/App.js +++ b/src/App.js @@ -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 )}
- {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 (
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`} >
setSelectedEncounterId(encounter.id)} className="cursor-pointer flex-grow"> @@ -2192,6 +2238,11 @@ function EncounterManager({ campaignId, initialActiveEncounterId, campaignCharac )}
+