Generic (non-5e) ruleset mode + UI polish + combat switch fix #7
+53
-2
@@ -2012,6 +2012,7 @@ function EncounterManager({ campaignId, initialActiveEncounterId, campaignCharac
|
|||||||
const [showCreateModal, setShowCreateModal] = useState(false);
|
const [showCreateModal, setShowCreateModal] = useState(false);
|
||||||
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
|
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
|
||||||
const [itemToDelete, setItemToDelete] = useState(null);
|
const [itemToDelete, setItemToDelete] = useState(null);
|
||||||
|
const [draggedEncounterId, setDraggedEncounterId] = useState(null);
|
||||||
|
|
||||||
const selectedEncounterIdRef = useRef(selectedEncounterId);
|
const selectedEncounterIdRef = useRef(selectedEncounterId);
|
||||||
|
|
||||||
@@ -2019,6 +2020,45 @@ function EncounterManager({ campaignId, initialActiveEncounterId, campaignCharac
|
|||||||
if (encountersData) setEncounters(encountersData);
|
if (encountersData) setEncounters(encountersData);
|
||||||
}, [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(() => {
|
useEffect(() => {
|
||||||
selectedEncounterIdRef.current = selectedEncounterId;
|
selectedEncounterIdRef.current = selectedEncounterId;
|
||||||
}, [selectedEncounterId]);
|
}, [selectedEncounterId]);
|
||||||
@@ -2063,6 +2103,7 @@ function EncounterManager({ campaignId, initialActiveEncounterId, campaignCharac
|
|||||||
isStarted: false,
|
isStarted: false,
|
||||||
isPaused: false,
|
isPaused: false,
|
||||||
ruleset: ruleset || '5e',
|
ruleset: ruleset || '5e',
|
||||||
|
order: (encounters || []).reduce((max, e) => Math.max(max, e.order ?? -1), -1) + 1,
|
||||||
});
|
});
|
||||||
|
|
||||||
setShowCreateModal(false);
|
setShowCreateModal(false);
|
||||||
@@ -2169,7 +2210,7 @@ function EncounterManager({ campaignId, initialActiveEncounterId, campaignCharac
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
{encounters?.map(encounter => {
|
{sortedEncounters.map(encounter => {
|
||||||
const isLive = activeDisplayInfo &&
|
const isLive = activeDisplayInfo &&
|
||||||
activeDisplayInfo.activeCampaignId === campaignId &&
|
activeDisplayInfo.activeCampaignId === campaignId &&
|
||||||
activeDisplayInfo.activeEncounterId === encounter.id;
|
activeDisplayInfo.activeEncounterId === encounter.id;
|
||||||
@@ -2177,7 +2218,12 @@ function EncounterManager({ campaignId, initialActiveEncounterId, campaignCharac
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={encounter.id}
|
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 className="flex justify-between items-center">
|
||||||
<div onClick={() => setSelectedEncounterId(encounter.id)} className="cursor-pointer flex-grow">
|
<div onClick={() => setSelectedEncounterId(encounter.id)} className="cursor-pointer flex-grow">
|
||||||
@@ -2192,6 +2238,11 @@ function EncounterManager({ campaignId, initialActiveEncounterId, campaignCharac
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center space-x-2">
|
<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
|
<button
|
||||||
onClick={() => handleTogglePlayerDisplay(encounter.id)}
|
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'}`}
|
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