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:
@@ -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
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user