Add child route (sub-area) management to route detail page

Shows a sub-areas section below encounters with add/delete support
and links to navigate to child route details.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-08 20:32:17 +01:00
parent c6521dd206
commit 0931884f1e
3 changed files with 85 additions and 2 deletions

View File

@@ -1,11 +1,11 @@
--- ---
# nuzlocke-tracker-kghn # nuzlocke-tracker-kghn
title: Child route management from route detail title: Child route management from route detail
status: todo status: completed
type: feature type: feature
priority: low priority: low
created_at: 2026-02-08T12:33:53Z created_at: 2026-02-08T12:33:53Z
updated_at: 2026-02-08T12:33:53Z updated_at: 2026-02-08T19:31:57Z
parent: nuzlocke-tracker-iu5b parent: nuzlocke-tracker-iu5b
--- ---

View File

@@ -2,16 +2,22 @@ import { useMemo, useState } from 'react'
import { useParams, Link, useNavigate } from 'react-router-dom' import { useParams, Link, useNavigate } from 'react-router-dom'
import { AdminTable, type Column } from '../../components/admin/AdminTable' import { AdminTable, type Column } from '../../components/admin/AdminTable'
import { RouteEncounterFormModal } from '../../components/admin/RouteEncounterFormModal' import { RouteEncounterFormModal } from '../../components/admin/RouteEncounterFormModal'
import { RouteFormModal } from '../../components/admin/RouteFormModal'
import { DeleteConfirmModal } from '../../components/admin/DeleteConfirmModal'
import { useGame, useRoutePokemon } from '../../hooks/useGames' import { useGame, useRoutePokemon } from '../../hooks/useGames'
import { import {
useAddRouteEncounter, useAddRouteEncounter,
useUpdateRouteEncounter, useUpdateRouteEncounter,
useRemoveRouteEncounter, useRemoveRouteEncounter,
useCreateRoute,
useDeleteRoute,
} from '../../hooks/useAdmin' } from '../../hooks/useAdmin'
import type { import type {
Route,
RouteEncounterDetail, RouteEncounterDetail,
CreateRouteEncounterInput, CreateRouteEncounterInput,
UpdateRouteEncounterInput, UpdateRouteEncounterInput,
CreateRouteInput,
} from '../../types' } from '../../types'
export function AdminRouteDetail() { export function AdminRouteDetail() {
@@ -26,9 +32,13 @@ export function AdminRouteDetail() {
const addEncounter = useAddRouteEncounter(rId) const addEncounter = useAddRouteEncounter(rId)
const updateEncounter = useUpdateRouteEncounter(rId) const updateEncounter = useUpdateRouteEncounter(rId)
const removeEncounter = useRemoveRouteEncounter(rId) const removeEncounter = useRemoveRouteEncounter(rId)
const createRoute = useCreateRoute(gId)
const deleteRoute = useDeleteRoute(gId)
const [showCreate, setShowCreate] = useState(false) const [showCreate, setShowCreate] = useState(false)
const [editing, setEditing] = useState<RouteEncounterDetail | null>(null) const [editing, setEditing] = useState<RouteEncounterDetail | null>(null)
const [showCreateChild, setShowCreateChild] = useState(false)
const [deletingChild, setDeletingChild] = useState<Route | null>(null)
const sortedRoutes = useMemo( const sortedRoutes = useMemo(
() => [...(game?.routes ?? [])].sort((a, b) => a.order - b.order), () => [...(game?.routes ?? [])].sort((a, b) => a.order - b.order),
@@ -42,6 +52,15 @@ export function AdminRouteDetail() {
? sortedRoutes[currentIndex + 1] ? sortedRoutes[currentIndex + 1]
: undefined : undefined
const childRoutes = useMemo(
() => (game?.routes ?? []).filter((r) => r.parentRouteId === rId).sort((a, b) => a.order - b.order),
[game?.routes, rId],
)
const nextChildOrder = childRoutes.length > 0
? Math.max(...childRoutes.map((r) => r.order)) + 1
: (route?.order ?? 0) * 10 + 1
const columns: Column<RouteEncounterDetail>[] = [ const columns: Column<RouteEncounterDetail>[] = [
{ {
header: 'Pokemon', header: 'Pokemon',
@@ -171,6 +190,69 @@ export function AdminRouteDetail() {
isDeleting={removeEncounter.isPending} isDeleting={removeEncounter.isPending}
/> />
)} )}
{/* Sub-areas */}
<div className="mt-8">
<div className="flex justify-between items-center mb-3">
<h3 className="text-lg font-semibold">Sub-areas ({childRoutes.length})</h3>
<button
onClick={() => setShowCreateChild(true)}
className="px-3 py-1.5 text-sm font-medium rounded-md bg-blue-600 text-white hover:bg-blue-700"
>
Add Sub-area
</button>
</div>
{childRoutes.length === 0 ? (
<p className="text-sm text-gray-500 dark:text-gray-400">No sub-areas for this route.</p>
) : (
<div className="border rounded-md dark:border-gray-700 divide-y dark:divide-gray-700">
{childRoutes.map((child) => (
<div key={child.id} className="flex items-center justify-between px-4 py-2">
<Link
to={`/admin/games/${gId}/routes/${child.id}`}
className="text-blue-600 dark:text-blue-400 hover:underline"
>
{child.name}
</Link>
<button
onClick={() => setDeletingChild(child)}
className="text-sm text-red-600 dark:text-red-400 hover:underline"
>
Delete
</button>
</div>
))}
</div>
)}
</div>
{showCreateChild && (
<RouteFormModal
nextOrder={nextChildOrder}
onSubmit={(data) =>
createRoute.mutate(
{ ...data, parentRouteId: rId } as CreateRouteInput,
{ onSuccess: () => setShowCreateChild(false) },
)
}
onClose={() => setShowCreateChild(false)}
isSubmitting={createRoute.isPending}
/>
)}
{deletingChild && (
<DeleteConfirmModal
title={`Delete "${deletingChild.name}"?`}
message="This will permanently delete this sub-area and all its encounters."
onConfirm={() =>
deleteRoute.mutate(deletingChild.id, {
onSuccess: () => setDeletingChild(null),
})
}
onCancel={() => setDeletingChild(null)}
isDeleting={deleteRoute.isPending}
/>
)}
</div> </div>
) )
} }

View File

@@ -19,6 +19,7 @@ export interface UpdateGameInput {
export interface CreateRouteInput { export interface CreateRouteInput {
name: string name: string
order: number order: number
parentRouteId?: number | null
pinwheelZone?: number | null pinwheelZone?: number | null
} }