Add section field to boss battles for run progression dividers
Adds a nullable `section` column to boss battles (e.g. "Main Story", "Endgame") with dividers rendered in the run view between sections. Admin UI gets a Section column in the table and a text input in the form. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -26,6 +26,7 @@ import {
|
||||
useUpdateRoute,
|
||||
useDeleteRoute,
|
||||
useReorderRoutes,
|
||||
useReorderBosses,
|
||||
useCreateBossBattle,
|
||||
useUpdateBossBattle,
|
||||
useDeleteBossBattle,
|
||||
@@ -82,6 +83,59 @@ function SortableRouteRow({
|
||||
)
|
||||
}
|
||||
|
||||
function SortableBossRow({
|
||||
boss,
|
||||
onClick,
|
||||
}: {
|
||||
boss: BossBattle
|
||||
onClick: (b: BossBattle) => void
|
||||
}) {
|
||||
const { attributes, listeners, setNodeRef, transform, transition, isDragging } =
|
||||
useSortable({ id: boss.id })
|
||||
|
||||
const style = {
|
||||
transform: CSS.Transform.toString(transform),
|
||||
transition,
|
||||
}
|
||||
|
||||
return (
|
||||
<tr
|
||||
ref={setNodeRef}
|
||||
style={style}
|
||||
className={`${isDragging ? 'opacity-50 bg-blue-50 dark:bg-blue-900/20' : ''} hover:bg-gray-50 dark:hover:bg-gray-800 cursor-pointer`}
|
||||
onClick={() => onClick(boss)}
|
||||
>
|
||||
<td className="px-4 py-3 text-sm w-12">
|
||||
<button
|
||||
{...attributes}
|
||||
{...listeners}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
className="cursor-grab active:cursor-grabbing text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 touch-none"
|
||||
title="Drag to reorder"
|
||||
>
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
|
||||
<circle cx="5" cy="3" r="1.5" />
|
||||
<circle cx="11" cy="3" r="1.5" />
|
||||
<circle cx="5" cy="8" r="1.5" />
|
||||
<circle cx="11" cy="8" r="1.5" />
|
||||
<circle cx="5" cy="13" r="1.5" />
|
||||
<circle cx="11" cy="13" r="1.5" />
|
||||
</svg>
|
||||
</button>
|
||||
</td>
|
||||
<td className="px-4 py-3 text-sm whitespace-nowrap">{boss.order}</td>
|
||||
<td className="px-4 py-3 text-sm whitespace-nowrap font-medium">{boss.name}</td>
|
||||
<td className="px-4 py-3 text-sm whitespace-nowrap capitalize">
|
||||
{boss.bossType.replace('_', ' ')}
|
||||
</td>
|
||||
<td className="px-4 py-3 text-sm whitespace-nowrap">{boss.section ?? '\u2014'}</td>
|
||||
<td className="px-4 py-3 text-sm whitespace-nowrap">{boss.location}</td>
|
||||
<td className="px-4 py-3 text-sm whitespace-nowrap">{boss.levelCap}</td>
|
||||
<td className="px-4 py-3 text-sm whitespace-nowrap">{boss.pokemon.length}</td>
|
||||
</tr>
|
||||
)
|
||||
}
|
||||
|
||||
export function AdminGameDetail() {
|
||||
const { gameId } = useParams<{ gameId: string }>()
|
||||
const id = Number(gameId)
|
||||
@@ -95,6 +149,7 @@ export function AdminGameDetail() {
|
||||
const createBoss = useCreateBossBattle(id)
|
||||
const updateBoss = useUpdateBossBattle(id)
|
||||
const deleteBoss = useDeleteBossBattle(id)
|
||||
const reorderBosses = useReorderBosses(id)
|
||||
|
||||
const [tab, setTab] = useState<'routes' | 'bosses'>('routes')
|
||||
const [showCreate, setShowCreate] = useState(false)
|
||||
@@ -133,6 +188,26 @@ export function AdminGameDetail() {
|
||||
reorderRoutes.mutate(newOrders)
|
||||
}
|
||||
|
||||
const handleBossDragEnd = (event: DragEndEvent) => {
|
||||
if (!bosses) return
|
||||
const { active, over } = event
|
||||
if (!over || active.id === over.id) return
|
||||
|
||||
const oldIndex = bosses.findIndex((b) => b.id === active.id)
|
||||
const newIndex = bosses.findIndex((b) => b.id === over.id)
|
||||
if (oldIndex === -1 || newIndex === -1) return
|
||||
|
||||
const reordered = [...bosses]
|
||||
const [moved] = reordered.splice(oldIndex, 1)
|
||||
reordered.splice(newIndex, 0, moved)
|
||||
|
||||
const newOrders = reordered.map((b, i) => ({
|
||||
id: b.id,
|
||||
order: i + 1,
|
||||
}))
|
||||
reorderBosses.mutate(newOrders)
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<nav className="text-sm mb-4 text-gray-500 dark:text-gray-400">
|
||||
@@ -304,6 +379,7 @@ export function AdminGameDetail() {
|
||||
<table className="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
|
||||
<thead className="bg-gray-50 dark:bg-gray-800">
|
||||
<tr>
|
||||
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider w-12" />
|
||||
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider w-16">
|
||||
Order
|
||||
</th>
|
||||
@@ -313,6 +389,9 @@ export function AdminGameDetail() {
|
||||
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
|
||||
Type
|
||||
</th>
|
||||
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
|
||||
Section
|
||||
</th>
|
||||
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
|
||||
Location
|
||||
</th>
|
||||
@@ -324,24 +403,26 @@ export function AdminGameDetail() {
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="bg-white dark:bg-gray-900 divide-y divide-gray-200 dark:divide-gray-700">
|
||||
{bosses.map((boss) => (
|
||||
<tr
|
||||
key={boss.id}
|
||||
className="hover:bg-gray-50 dark:hover:bg-gray-800 cursor-pointer"
|
||||
onClick={() => setEditingBoss(boss)}
|
||||
>
|
||||
<td className="px-4 py-3 text-sm whitespace-nowrap">{boss.order}</td>
|
||||
<td className="px-4 py-3 text-sm whitespace-nowrap font-medium">{boss.name}</td>
|
||||
<td className="px-4 py-3 text-sm whitespace-nowrap capitalize">
|
||||
{boss.bossType.replace('_', ' ')}
|
||||
</td>
|
||||
<td className="px-4 py-3 text-sm whitespace-nowrap">{boss.location}</td>
|
||||
<td className="px-4 py-3 text-sm whitespace-nowrap">{boss.levelCap}</td>
|
||||
<td className="px-4 py-3 text-sm whitespace-nowrap">{boss.pokemon.length}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
<DndContext
|
||||
sensors={sensors}
|
||||
collisionDetection={closestCenter}
|
||||
onDragEnd={handleBossDragEnd}
|
||||
>
|
||||
<SortableContext
|
||||
items={bosses.map((b) => b.id)}
|
||||
strategy={verticalListSortingStrategy}
|
||||
>
|
||||
<tbody className="bg-white dark:bg-gray-900 divide-y divide-gray-200 dark:divide-gray-700">
|
||||
{bosses.map((boss) => (
|
||||
<SortableBossRow
|
||||
key={boss.id}
|
||||
boss={boss}
|
||||
onClick={(b) => setEditingBoss(b)}
|
||||
/>
|
||||
))}
|
||||
</tbody>
|
||||
</SortableContext>
|
||||
</DndContext>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user