Campaign drag reorder via order field

Campaign cards draggable (ChevronsUpDown handle in header). Drop reorders,
batch updates order field on affected campaign docs. Sort by order (fallback
createdAt). New campaign gets order = max+1.
This commit is contained in:
david raistrick
2026-07-07 15:24:12 -04:00
parent 94126567e7
commit 1078062b53
+28 -2
View File
@@ -2330,6 +2330,7 @@ function AdminView({ userId }) {
const { data: initialActiveInfo } = useFirestoreDocument(getPath.activeDisplay()); const { data: initialActiveInfo } = useFirestoreDocument(getPath.activeDisplay());
const [campaignsWithDetails, setCampaignsWithDetails] = useState([]); const [campaignsWithDetails, setCampaignsWithDetails] = useState([]);
const [draggedCampaignId, setDraggedCampaignId] = useState(null);
const [selectedCampaignId, setSelectedCampaignId] = useState(null); const [selectedCampaignId, setSelectedCampaignId] = useState(null);
const encounterStartedRef = useRef(false); const encounterStartedRef = useRef(false);
const encounterActiveRef = useRef(false); const encounterActiveRef = useRef(false);
@@ -2390,6 +2391,10 @@ function AdminView({ userId }) {
fetchDetails(); fetchDetails();
} else if (campaignsData) { } else if (campaignsData) {
const sorted = [...campaignsData].sort((a, b) => { const sorted = [...campaignsData].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;
const at = a.createdAt || 0; const at = a.createdAt || 0;
const bt = b.createdAt || 0; const bt = b.createdAt || 0;
if (at === bt) return (a.name || '').localeCompare(b.name || ''); if (at === bt) return (a.name || '').localeCompare(b.name || '');
@@ -2433,6 +2438,7 @@ function AdminView({ userId }) {
createdAt: new Date().toISOString(), createdAt: new Date().toISOString(),
players: [], players: [],
ruleset: ruleset || '5e', ruleset: ruleset || '5e',
order: (campaignsWithDetails || []).reduce((max, c) => Math.max(max, c.order ?? -1), -1) + 1,
}); });
setShowCreateModal(false); setShowCreateModal(false);
@@ -2579,11 +2585,28 @@ function AdminView({ userId }) {
? { backgroundImage: `url(${campaign.playerDisplayBackgroundUrl})` } ? { backgroundImage: `url(${campaign.playerDisplayBackgroundUrl})` }
: {}; : {};
const cardClasses = `h-40 flex flex-col justify-between rounded-lg shadow-md cursor-pointer transition-all relative overflow-hidden bg-cover bg-center ${selectedCampaignId === campaign.id ? 'ring-4 ring-amber-500' : ''} ${!campaign.playerDisplayBackgroundUrl ? 'bg-stone-800 hover:bg-stone-700' : 'hover:shadow-xl'}`; const cardClasses = `h-40 flex flex-col justify-between rounded-lg shadow-md cursor-grab transition-all relative overflow-hidden bg-cover bg-center ${selectedCampaignId === campaign.id ? 'ring-4 ring-amber-500' : ''} ${!campaign.playerDisplayBackgroundUrl ? 'bg-stone-800 hover:bg-stone-700' : 'hover:shadow-xl'} ${draggedCampaignId === campaign.id ? 'opacity-50 ring-2 ring-yellow-400' : ''}`;
return ( return (
<div <div
key={campaign.id} key={campaign.id}
draggable
onDragStart={(e) => { setDraggedCampaignId(campaign.id); e.dataTransfer.effectAllowed = 'move'; }}
onDragOver={(e) => { e.preventDefault(); e.dataTransfer.dropEffect = 'move'; }}
onDrop={async (e) => {
e.preventDefault();
if (!db || !draggedCampaignId || draggedCampaignId === campaign.id) { setDraggedCampaignId(null); return; }
const reordered = [...campaignsWithDetails];
const fromIdx = reordered.findIndex(c => c.id === draggedCampaignId);
const toIdx = reordered.findIndex(c => c.id === campaign.id);
if (fromIdx === -1 || toIdx === -1) { setDraggedCampaignId(null); return; }
const [moved] = reordered.splice(fromIdx, 1);
reordered.splice(toIdx, 0, moved);
const ops = reordered.map((c, i) => ({ type: 'update', path: getPath.campaign(c.id), data: { order: i } }));
try { await storage.batchWrite(ops); } catch (err) {}
setDraggedCampaignId(null);
}}
onDragEnd={() => setDraggedCampaignId(null)}
onClick={() => { onClick={() => {
if (encounterStartedRef.current) { if (encounterStartedRef.current) {
showToast('End or pause active encounter before switching campaigns.'); showToast('End or pause active encounter before switching campaigns.');
@@ -2599,7 +2622,10 @@ function AdminView({ userId }) {
className={`relative z-10 flex flex-col justify-between h-full ${campaign.playerDisplayBackgroundUrl ? 'bg-black bg-opacity-60 p-3' : 'p-4'}`} className={`relative z-10 flex flex-col justify-between h-full ${campaign.playerDisplayBackgroundUrl ? 'bg-black bg-opacity-60 p-3' : 'p-4'}`}
> >
<div> <div>
<h3 className="text-xl font-semibold text-white">{campaign.name}</h3> <div className="flex items-center gap-1">
<ChevronsUpDown size={14} className="text-white/60 flex-shrink-0" title="Drag to reorder" />
<h3 className="text-xl font-semibold text-white">{campaign.name}</h3>
</div>
<div className="text-xs text-stone-100 mt-1 space-x-3"> <div className="text-xs text-stone-100 mt-1 space-x-3">
<span className="inline-flex items-center"> <span className="inline-flex items-center">
<Users size={12} className="mr-1" /> {campaign.characters?.length || 0} Characters <Users size={12} className="mr-1" /> {campaign.characters?.length || 0} Characters