Align repo config with global development standards
- Add missing tsconfig strictness flags (noUncheckedIndexedAccess, exactOptionalPropertyTypes, noImplicitOverride, noPropertyAccessFromIndexSignature) and fix all resulting type errors - Replace ESLint/Prettier with oxlint 1.48.0 and oxfmt 0.33.0 - Pin all frontend and backend dependencies to exact versions - Pin GitHub Actions to SHA hashes with persist-credentials: false - Fix CI Python version mismatch (3.12 -> 3.14) and ruff target-version - Add vitest 4.0.18 with jsdom environment for frontend testing - Add ty 0.0.17 for Python type checking (non-blocking in CI) - Add actionlint and zizmor CI job for workflow linting and security audit - Add Dependabot config for npm, pip, and github-actions - Update CLAUDE.md and pre-commit hooks to reflect new tooling - Ignore Claude Code sandbox artifacts in gitignore Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -11,11 +11,7 @@ import {
|
||||
} from '../../hooks/useAdmin'
|
||||
import { exportEvolutions } from '../../api/admin'
|
||||
import { downloadJson } from '../../utils/download'
|
||||
import type {
|
||||
EvolutionAdmin,
|
||||
CreateEvolutionInput,
|
||||
UpdateEvolutionInput,
|
||||
} from '../../types'
|
||||
import type { EvolutionAdmin, CreateEvolutionInput, UpdateEvolutionInput } from '../../types'
|
||||
|
||||
const PAGE_SIZE = 50
|
||||
|
||||
@@ -67,9 +63,7 @@ export function AdminEvolutions() {
|
||||
header: 'To',
|
||||
accessor: (e) => (
|
||||
<div className="flex items-center gap-2">
|
||||
{e.toPokemon.spriteUrl && (
|
||||
<img src={e.toPokemon.spriteUrl} alt="" className="w-6 h-6" />
|
||||
)}
|
||||
{e.toPokemon.spriteUrl && <img src={e.toPokemon.spriteUrl} alt="" className="w-6 h-6" />}
|
||||
<span>{e.toPokemon.name}</span>
|
||||
</div>
|
||||
),
|
||||
@@ -163,8 +157,7 @@ export function AdminEvolutions() {
|
||||
{totalPages > 1 && (
|
||||
<div className="mt-4 flex items-center justify-between">
|
||||
<div className="text-sm text-gray-500 dark:text-gray-400">
|
||||
Showing {offset + 1}-{Math.min(offset + PAGE_SIZE, total)} of{' '}
|
||||
{total}
|
||||
Showing {offset + 1}-{Math.min(offset + PAGE_SIZE, total)} of {total}
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
|
||||
@@ -45,10 +45,7 @@ import type {
|
||||
UpdateRouteInput,
|
||||
BossBattle,
|
||||
} from '../../types'
|
||||
import type {
|
||||
CreateBossBattleInput,
|
||||
UpdateBossBattleInput,
|
||||
} from '../../types/admin'
|
||||
import type { CreateBossBattleInput, UpdateBossBattleInput } from '../../types/admin'
|
||||
|
||||
/**
|
||||
* Organize flat routes into hierarchical structure.
|
||||
@@ -85,14 +82,9 @@ function SortableRouteGroup({
|
||||
gameId: number
|
||||
onClick: (r: GameRoute) => void
|
||||
}) {
|
||||
const {
|
||||
attributes,
|
||||
listeners,
|
||||
setNodeRef,
|
||||
transform,
|
||||
transition,
|
||||
isDragging,
|
||||
} = useSortable({ id: group.id })
|
||||
const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({
|
||||
id: group.id,
|
||||
})
|
||||
|
||||
const style = {
|
||||
transform: CSS.Transform.toString(transform),
|
||||
@@ -127,9 +119,7 @@ function SortableRouteGroup({
|
||||
</svg>
|
||||
</button>
|
||||
</td>
|
||||
<td className="px-4 py-3 text-sm whitespace-nowrap w-16">
|
||||
{group.order}
|
||||
</td>
|
||||
<td className="px-4 py-3 text-sm whitespace-nowrap w-16">{group.order}</td>
|
||||
<td className="px-4 py-3 text-sm whitespace-nowrap">{group.name}</td>
|
||||
<td className="px-4 py-3 text-sm whitespace-nowrap text-center">
|
||||
{group.pinwheelZone != null ? group.pinwheelZone : '\u2014'}
|
||||
@@ -155,9 +145,7 @@ function SortableRouteGroup({
|
||||
{child.order}
|
||||
</td>
|
||||
<td className="px-4 py-3 text-sm whitespace-nowrap pl-8 text-gray-600 dark:text-gray-400">
|
||||
<span className="text-gray-300 dark:text-gray-600 mr-1.5">
|
||||
{'\u2514'}
|
||||
</span>
|
||||
<span className="text-gray-300 dark:text-gray-600 mr-1.5">{'\u2514'}</span>
|
||||
{child.name}
|
||||
</td>
|
||||
<td className="px-4 py-3 text-sm whitespace-nowrap text-center">
|
||||
@@ -191,14 +179,9 @@ function SortableBossRow({
|
||||
onPositionChange: (bossId: number, afterRouteId: number | null) => void
|
||||
onClick: (b: BossBattle) => void
|
||||
}) {
|
||||
const {
|
||||
attributes,
|
||||
listeners,
|
||||
setNodeRef,
|
||||
transform,
|
||||
transition,
|
||||
isDragging,
|
||||
} = useSortable({ id: boss.id })
|
||||
const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({
|
||||
id: boss.id,
|
||||
})
|
||||
|
||||
const style = {
|
||||
transform: CSS.Transform.toString(transform),
|
||||
@@ -247,15 +230,9 @@ function SortableBossRow({
|
||||
{boss.bossType.replace('_', ' ')}
|
||||
</td>
|
||||
<td className="px-4 py-3 text-sm whitespace-nowrap">
|
||||
{boss.specialtyType ? (
|
||||
<TypeBadge type={boss.specialtyType} />
|
||||
) : (
|
||||
'\u2014'
|
||||
)}
|
||||
</td>
|
||||
<td className="px-4 py-3 text-sm whitespace-nowrap">
|
||||
{boss.section ?? '\u2014'}
|
||||
{boss.specialtyType ? <TypeBadge type={boss.specialtyType} /> : '\u2014'}
|
||||
</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">
|
||||
<select
|
||||
@@ -276,9 +253,7 @@ function SortableBossRow({
|
||||
</select>
|
||||
</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>
|
||||
<td className="px-4 py-3 text-sm whitespace-nowrap">{boss.pokemon.length}</td>
|
||||
</tr>
|
||||
)
|
||||
}
|
||||
@@ -315,16 +290,12 @@ export function AdminGameDetail() {
|
||||
useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates })
|
||||
)
|
||||
|
||||
if (isLoading)
|
||||
return <div className="py-8 text-center text-gray-500">Loading...</div>
|
||||
if (!game)
|
||||
return <div className="py-8 text-center text-gray-500">Game not found</div>
|
||||
if (isLoading) return <div className="py-8 text-center text-gray-500">Loading...</div>
|
||||
if (!game) return <div className="py-8 text-center text-gray-500">Game not found</div>
|
||||
|
||||
const routes = game.routes ?? []
|
||||
const routeGroups = organizeRoutes(routes)
|
||||
const versionGroupGames = (allGames ?? []).filter(
|
||||
(g) => g.versionGroupId === game.versionGroupId
|
||||
)
|
||||
const versionGroupGames = (allGames ?? []).filter((g) => g.versionGroupId === game.versionGroupId)
|
||||
|
||||
const handleDragEnd = (event: DragEndEvent) => {
|
||||
const { active, over } = event
|
||||
@@ -336,7 +307,7 @@ export function AdminGameDetail() {
|
||||
|
||||
const reordered = [...routeGroups]
|
||||
const [moved] = reordered.splice(oldIndex, 1)
|
||||
reordered.splice(newIndex, 0, moved)
|
||||
reordered.splice(newIndex, 0, moved!)
|
||||
|
||||
// Flatten groups back to individual routes with sequential order numbers
|
||||
let order = 1
|
||||
@@ -361,7 +332,7 @@ export function AdminGameDetail() {
|
||||
|
||||
const reordered = [...bosses]
|
||||
const [moved] = reordered.splice(oldIndex, 1)
|
||||
reordered.splice(newIndex, 0, moved)
|
||||
reordered.splice(newIndex, 0, moved!)
|
||||
|
||||
const newOrders = reordered.map((b, i) => ({
|
||||
id: b.id,
|
||||
@@ -383,8 +354,8 @@ export function AdminGameDetail() {
|
||||
<div className="mb-6">
|
||||
<h2 className="text-xl font-semibold">{game.name}</h2>
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400">
|
||||
{game.region.charAt(0).toUpperCase() + game.region.slice(1)} ·
|
||||
Gen {game.generation}
|
||||
{game.region.charAt(0).toUpperCase() + game.region.slice(1)} · Gen{' '}
|
||||
{game.generation}
|
||||
{game.releaseYear ? ` \u00b7 ${game.releaseYear}` : ''}
|
||||
</p>
|
||||
</div>
|
||||
@@ -500,11 +471,7 @@ export function AdminGameDetail() {
|
||||
|
||||
{showCreate && (
|
||||
<RouteFormModal
|
||||
nextOrder={
|
||||
routes.length > 0
|
||||
? Math.max(...routes.map((r) => r.order)) + 1
|
||||
: 1
|
||||
}
|
||||
nextOrder={routes.length > 0 ? Math.max(...routes.map((r) => r.order)) + 1 : 1}
|
||||
onSubmit={(data) =>
|
||||
createRoute.mutate(data as CreateRouteInput, {
|
||||
onSuccess: () => setShowCreate(false),
|
||||
@@ -655,9 +622,7 @@ export function AdminGameDetail() {
|
||||
<BossBattleFormModal
|
||||
routes={routes}
|
||||
games={versionGroupGames}
|
||||
nextOrder={
|
||||
bosses ? Math.max(0, ...bosses.map((b) => b.order)) + 1 : 1
|
||||
}
|
||||
nextOrder={bosses ? Math.max(0, ...bosses.map((b) => b.order)) + 1 : 1}
|
||||
onSubmit={(data) =>
|
||||
createBoss.mutate(data as CreateBossBattleInput, {
|
||||
onSuccess: () => setShowCreateBoss(false),
|
||||
|
||||
@@ -2,11 +2,7 @@ import { useState, useMemo } from 'react'
|
||||
import { AdminTable, type Column } from '../../components/admin/AdminTable'
|
||||
import { GameFormModal } from '../../components/admin/GameFormModal'
|
||||
import { useGames } from '../../hooks/useGames'
|
||||
import {
|
||||
useCreateGame,
|
||||
useUpdateGame,
|
||||
useDeleteGame,
|
||||
} from '../../hooks/useAdmin'
|
||||
import { useCreateGame, useUpdateGame, useDeleteGame } from '../../hooks/useAdmin'
|
||||
import { exportGames } from '../../api/admin'
|
||||
import { downloadJson } from '../../utils/download'
|
||||
import type { Game, CreateGameInput, UpdateGameInput } from '../../types'
|
||||
@@ -22,10 +18,7 @@ export function AdminGames() {
|
||||
const [regionFilter, setRegionFilter] = useState('')
|
||||
const [genFilter, setGenFilter] = useState('')
|
||||
|
||||
const regions = useMemo(
|
||||
() => [...new Set(games.map((g) => g.region))].sort(),
|
||||
[games]
|
||||
)
|
||||
const regions = useMemo(() => [...new Set(games.map((g) => g.region))].sort(), [games])
|
||||
const generations = useMemo(
|
||||
() => [...new Set(games.map((g) => g.generation))].sort((a, b) => a - b),
|
||||
[games]
|
||||
@@ -34,8 +27,7 @@ export function AdminGames() {
|
||||
const filteredGames = useMemo(() => {
|
||||
let result = games
|
||||
if (regionFilter) result = result.filter((g) => g.region === regionFilter)
|
||||
if (genFilter)
|
||||
result = result.filter((g) => g.generation === Number(genFilter))
|
||||
if (genFilter) result = result.filter((g) => g.generation === Number(genFilter))
|
||||
return result
|
||||
}, [games, regionFilter, genFilter])
|
||||
|
||||
|
||||
@@ -28,23 +28,18 @@ export function AdminGenlockeDetail() {
|
||||
const [addingLeg, setAddingLeg] = useState(false)
|
||||
const [selectedGameId, setSelectedGameId] = useState<number | ''>('')
|
||||
|
||||
if (isLoading)
|
||||
return <div className="py-8 text-center text-gray-500">Loading...</div>
|
||||
if (!genlocke)
|
||||
return (
|
||||
<div className="py-8 text-center text-gray-500">Genlocke not found</div>
|
||||
)
|
||||
if (isLoading) return <div className="py-8 text-center text-gray-500">Loading...</div>
|
||||
if (!genlocke) return <div className="py-8 text-center text-gray-500">Genlocke not found</div>
|
||||
|
||||
const editName = name ?? genlocke.name
|
||||
const editStatus = status ?? genlocke.status
|
||||
|
||||
const hasChanges =
|
||||
editName !== genlocke.name || editStatus !== genlocke.status
|
||||
const hasChanges = editName !== genlocke.name || editStatus !== genlocke.status
|
||||
|
||||
const handleSave = () => {
|
||||
const data: Record<string, string> = {}
|
||||
if (editName !== genlocke.name) data.name = editName
|
||||
if (editStatus !== genlocke.status) data.status = editStatus
|
||||
if (editName !== genlocke.name) data['name'] = editName
|
||||
if (editStatus !== genlocke.status) data['status'] = editStatus
|
||||
if (Object.keys(data).length === 0) return
|
||||
updateGenlocke.mutate(
|
||||
{ id, data },
|
||||
@@ -77,9 +72,7 @@ export function AdminGenlockeDetail() {
|
||||
Genlockes
|
||||
</Link>
|
||||
{' / '}
|
||||
<span className="text-gray-900 dark:text-gray-100">
|
||||
{genlocke.name}
|
||||
</span>
|
||||
<span className="text-gray-900 dark:text-gray-100">{genlocke.name}</span>
|
||||
</nav>
|
||||
|
||||
{/* Header */}
|
||||
@@ -131,22 +124,16 @@ export function AdminGenlockeDetail() {
|
||||
|
||||
{/* Rules (read-only) */}
|
||||
<div className="mb-6 p-4 bg-gray-50 dark:bg-gray-800 rounded-lg">
|
||||
<h3 className="text-sm font-semibold mb-2 text-gray-700 dark:text-gray-300">
|
||||
Rules
|
||||
</h3>
|
||||
<h3 className="text-sm font-semibold mb-2 text-gray-700 dark:text-gray-300">Rules</h3>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4 text-sm">
|
||||
<div>
|
||||
<span className="text-gray-500 dark:text-gray-400">
|
||||
Genlocke rules:
|
||||
</span>
|
||||
<span className="text-gray-500 dark:text-gray-400">Genlocke rules:</span>
|
||||
<pre className="mt-1 text-xs bg-white dark:bg-gray-900 p-2 rounded border dark:border-gray-700 overflow-x-auto">
|
||||
{JSON.stringify(genlocke.genlockeRules, null, 2)}
|
||||
</pre>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-gray-500 dark:text-gray-400">
|
||||
Nuzlocke rules:
|
||||
</span>
|
||||
<span className="text-gray-500 dark:text-gray-400">Nuzlocke rules:</span>
|
||||
<pre className="mt-1 text-xs bg-white dark:bg-gray-900 p-2 rounded border dark:border-gray-700 overflow-x-auto">
|
||||
{JSON.stringify(genlocke.nuzlockeRules, null, 2)}
|
||||
</pre>
|
||||
@@ -157,9 +144,7 @@ export function AdminGenlockeDetail() {
|
||||
{/* Legs */}
|
||||
<div className="mb-6">
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<h3 className="text-lg font-semibold">
|
||||
Legs ({genlocke.legs.length})
|
||||
</h3>
|
||||
<h3 className="text-lg font-semibold">Legs ({genlocke.legs.length})</h3>
|
||||
<button
|
||||
onClick={() => setAddingLeg(!addingLeg)}
|
||||
className="px-4 py-2 text-sm font-medium rounded-md bg-blue-600 text-white hover:bg-blue-700"
|
||||
@@ -172,9 +157,7 @@ export function AdminGenlockeDetail() {
|
||||
<div className="mb-4 flex items-center gap-3 p-3 bg-gray-50 dark:bg-gray-800 rounded-lg">
|
||||
<select
|
||||
value={selectedGameId}
|
||||
onChange={(e) =>
|
||||
setSelectedGameId(e.target.value ? Number(e.target.value) : '')
|
||||
}
|
||||
onChange={(e) => setSelectedGameId(e.target.value ? Number(e.target.value) : '')}
|
||||
className="flex-1 px-3 py-2 border rounded-md dark:bg-gray-700 dark:border-gray-600"
|
||||
>
|
||||
<option value="">Select a game...</option>
|
||||
@@ -239,12 +222,8 @@ export function AdminGenlockeDetail() {
|
||||
<tbody className="bg-white dark:bg-gray-900 divide-y divide-gray-200 dark:divide-gray-700">
|
||||
{genlocke.legs.map((leg) => (
|
||||
<tr key={leg.id}>
|
||||
<td className="px-4 py-3 text-sm whitespace-nowrap">
|
||||
{leg.legOrder}
|
||||
</td>
|
||||
<td className="px-4 py-3 text-sm whitespace-nowrap">
|
||||
{leg.game.name}
|
||||
</td>
|
||||
<td className="px-4 py-3 text-sm whitespace-nowrap">{leg.legOrder}</td>
|
||||
<td className="px-4 py-3 text-sm whitespace-nowrap">{leg.game.name}</td>
|
||||
<td className="px-4 py-3 text-sm whitespace-nowrap">
|
||||
{leg.runId ? (
|
||||
<Link
|
||||
@@ -274,12 +253,8 @@ export function AdminGenlockeDetail() {
|
||||
<span className="text-gray-400">—</span>
|
||||
)}
|
||||
</td>
|
||||
<td className="px-4 py-3 text-sm whitespace-nowrap">
|
||||
{leg.encounterCount}
|
||||
</td>
|
||||
<td className="px-4 py-3 text-sm whitespace-nowrap">
|
||||
{leg.deathCount}
|
||||
</td>
|
||||
<td className="px-4 py-3 text-sm whitespace-nowrap">{leg.encounterCount}</td>
|
||||
<td className="px-4 py-3 text-sm whitespace-nowrap">{leg.deathCount}</td>
|
||||
<td className="px-4 py-3 text-sm whitespace-nowrap text-right">
|
||||
<button
|
||||
onClick={() => deleteLeg.mutate(leg.id)}
|
||||
@@ -305,9 +280,7 @@ export function AdminGenlockeDetail() {
|
||||
|
||||
{/* Stats */}
|
||||
<div className="mb-6 p-4 bg-gray-50 dark:bg-gray-800 rounded-lg">
|
||||
<h3 className="text-sm font-semibold mb-2 text-gray-700 dark:text-gray-300">
|
||||
Stats
|
||||
</h3>
|
||||
<h3 className="text-sm font-semibold mb-2 text-gray-700 dark:text-gray-300">Stats</h3>
|
||||
<div className="grid grid-cols-2 sm:grid-cols-4 gap-4 text-sm">
|
||||
<div>
|
||||
<span className="text-gray-500 dark:text-gray-400">Legs</span>
|
||||
@@ -317,20 +290,14 @@ export function AdminGenlockeDetail() {
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-gray-500 dark:text-gray-400">Encounters</span>
|
||||
<p className="text-lg font-semibold">
|
||||
{genlocke.stats.totalEncounters}
|
||||
</p>
|
||||
<p className="text-lg font-semibold">{genlocke.stats.totalEncounters}</p>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-gray-500 dark:text-gray-400">Deaths</span>
|
||||
<p className="text-lg font-semibold">
|
||||
{genlocke.stats.totalDeaths}
|
||||
</p>
|
||||
<p className="text-lg font-semibold">{genlocke.stats.totalDeaths}</p>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-gray-500 dark:text-gray-400">
|
||||
Survival Rate
|
||||
</span>
|
||||
<span className="text-gray-500 dark:text-gray-400">Survival Rate</span>
|
||||
<p className="text-lg font-semibold">
|
||||
{genlocke.stats.totalEncounters > 0
|
||||
? `${Math.round(((genlocke.stats.totalEncounters - genlocke.stats.totalDeaths) / genlocke.stats.totalEncounters) * 100)}%`
|
||||
|
||||
@@ -11,11 +11,7 @@ import {
|
||||
} from '../../hooks/useAdmin'
|
||||
import { exportPokemon } from '../../api/admin'
|
||||
import { downloadJson } from '../../utils/download'
|
||||
import type {
|
||||
Pokemon,
|
||||
CreatePokemonInput,
|
||||
UpdatePokemonInput,
|
||||
} from '../../types'
|
||||
import type { Pokemon, CreatePokemonInput, UpdatePokemonInput } from '../../types'
|
||||
|
||||
const PAGE_SIZE = 50
|
||||
|
||||
@@ -164,8 +160,7 @@ export function AdminPokemon() {
|
||||
{totalPages > 1 && (
|
||||
<div className="mt-4 flex items-center justify-between">
|
||||
<div className="text-sm text-gray-500 dark:text-gray-400">
|
||||
Showing {offset + 1}-{Math.min(offset + PAGE_SIZE, total)} of{' '}
|
||||
{total}
|
||||
Showing {offset + 1}-{Math.min(offset + PAGE_SIZE, total)} of {total}
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
@@ -220,9 +215,7 @@ export function AdminPokemon() {
|
||||
title="Bulk Import Pokemon"
|
||||
example={`[\n { "pokeapi_id": 1, "national_dex": 1, "name": "Bulbasaur", "types": ["Grass", "Poison"] }\n]`}
|
||||
onSubmit={(items) =>
|
||||
bulkImport.mutateAsync(
|
||||
items as Parameters<typeof bulkImport.mutateAsync>[0]
|
||||
)
|
||||
bulkImport.mutateAsync(items as Parameters<typeof bulkImport.mutateAsync>[0])
|
||||
}
|
||||
onClose={() => setShowBulkImport(false)}
|
||||
/>
|
||||
|
||||
@@ -46,8 +46,7 @@ export function AdminRouteDetail() {
|
||||
)
|
||||
const currentIndex = sortedRoutes.findIndex((r) => r.id === rId)
|
||||
const route = currentIndex >= 0 ? sortedRoutes[currentIndex] : undefined
|
||||
const prevRoute =
|
||||
currentIndex > 0 ? sortedRoutes[currentIndex - 1] : undefined
|
||||
const prevRoute = currentIndex > 0 ? sortedRoutes[currentIndex - 1] : undefined
|
||||
const nextRoute =
|
||||
currentIndex >= 0 && currentIndex < sortedRoutes.length - 1
|
||||
? sortedRoutes[currentIndex + 1]
|
||||
@@ -55,9 +54,7 @@ export function AdminRouteDetail() {
|
||||
|
||||
const childRoutes = useMemo(
|
||||
() =>
|
||||
(game?.routes ?? [])
|
||||
.filter((r) => r.parentRouteId === rId)
|
||||
.sort((a, b) => a.order - b.order),
|
||||
(game?.routes ?? []).filter((r) => r.parentRouteId === rId).sort((a, b) => a.order - b.order),
|
||||
[game?.routes, rId]
|
||||
)
|
||||
|
||||
@@ -72,11 +69,7 @@ export function AdminRouteDetail() {
|
||||
accessor: (e) => (
|
||||
<div className="flex items-center gap-2">
|
||||
{e.pokemon.spriteUrl ? (
|
||||
<img
|
||||
src={e.pokemon.spriteUrl}
|
||||
alt={e.pokemon.name}
|
||||
className="w-6 h-6"
|
||||
/>
|
||||
<img src={e.pokemon.spriteUrl} alt={e.pokemon.name} className="w-6 h-6" />
|
||||
) : null}
|
||||
<span>
|
||||
#{e.pokemon.nationalDex} {e.pokemon.name}
|
||||
@@ -89,9 +82,7 @@ export function AdminRouteDetail() {
|
||||
{
|
||||
header: 'Levels',
|
||||
accessor: (e) =>
|
||||
e.minLevel === e.maxLevel
|
||||
? `Lv ${e.minLevel}`
|
||||
: `Lv ${e.minLevel}-${e.maxLevel}`,
|
||||
e.minLevel === e.maxLevel ? `Lv ${e.minLevel}` : `Lv ${e.minLevel}-${e.maxLevel}`,
|
||||
},
|
||||
]
|
||||
|
||||
@@ -109,9 +100,7 @@ export function AdminRouteDetail() {
|
||||
<select
|
||||
className="text-gray-900 dark:text-gray-100 bg-transparent font-medium cursor-pointer hover:underline border-none p-0 text-sm"
|
||||
value={rId}
|
||||
onChange={(e) =>
|
||||
navigate(`/admin/games/${gId}/routes/${e.target.value}`)
|
||||
}
|
||||
onChange={(e) => navigate(`/admin/games/${gId}/routes/${e.target.value}`)}
|
||||
>
|
||||
{sortedRoutes.map((r) => (
|
||||
<option key={r.id} value={r.id}>
|
||||
@@ -175,12 +164,9 @@ export function AdminRouteDetail() {
|
||||
{showCreate && (
|
||||
<RouteEncounterFormModal
|
||||
onSubmit={(data) =>
|
||||
addEncounter.mutate(
|
||||
{ ...data, gameId: gId } as CreateRouteEncounterInput,
|
||||
{
|
||||
onSuccess: () => setShowCreate(false),
|
||||
}
|
||||
)
|
||||
addEncounter.mutate({ ...data, gameId: gId } as CreateRouteEncounterInput, {
|
||||
onSuccess: () => setShowCreate(false),
|
||||
})
|
||||
}
|
||||
onClose={() => setShowCreate(false)}
|
||||
isSubmitting={addEncounter.isPending}
|
||||
@@ -213,9 +199,7 @@ export function AdminRouteDetail() {
|
||||
{/* 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>
|
||||
<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"
|
||||
@@ -224,16 +208,11 @@ export function AdminRouteDetail() {
|
||||
</button>
|
||||
</div>
|
||||
{childRoutes.length === 0 ? (
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400">
|
||||
No sub-areas for this route.
|
||||
</p>
|
||||
<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"
|
||||
>
|
||||
<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"
|
||||
@@ -256,10 +235,9 @@ export function AdminRouteDetail() {
|
||||
<RouteFormModal
|
||||
nextOrder={nextChildOrder}
|
||||
onSubmit={(data) =>
|
||||
createRoute.mutate(
|
||||
{ ...data, parentRouteId: rId } as CreateRouteInput,
|
||||
{ onSuccess: () => setShowCreateChild(false) }
|
||||
)
|
||||
createRoute.mutate({ ...data, parentRouteId: rId } as CreateRouteInput, {
|
||||
onSuccess: () => setShowCreateChild(false),
|
||||
})
|
||||
}
|
||||
onClose={() => setShowCreateChild(false)}
|
||||
isSubmitting={createRoute.isPending}
|
||||
|
||||
@@ -14,16 +14,12 @@ export function AdminRuns() {
|
||||
const [statusFilter, setStatusFilter] = useState('')
|
||||
const [gameFilter, setGameFilter] = useState('')
|
||||
|
||||
const gameMap = useMemo(
|
||||
() => new Map(games.map((g) => [g.id, g.name])),
|
||||
[games]
|
||||
)
|
||||
const gameMap = useMemo(() => new Map(games.map((g) => [g.id, g.name])), [games])
|
||||
|
||||
const filteredRuns = useMemo(() => {
|
||||
let result = runs
|
||||
if (statusFilter) result = result.filter((r) => r.status === statusFilter)
|
||||
if (gameFilter)
|
||||
result = result.filter((r) => r.gameId === Number(gameFilter))
|
||||
if (gameFilter) result = result.filter((r) => r.gameId === Number(gameFilter))
|
||||
return result
|
||||
}, [runs, statusFilter, gameFilter])
|
||||
|
||||
@@ -31,10 +27,7 @@ export function AdminRuns() {
|
||||
() =>
|
||||
[
|
||||
...new Map(
|
||||
runs.map((r) => [
|
||||
r.gameId,
|
||||
gameMap.get(r.gameId) ?? `Game #${r.gameId}`,
|
||||
])
|
||||
runs.map((r) => [r.gameId, gameMap.get(r.gameId) ?? `Game #${r.gameId}`])
|
||||
).entries(),
|
||||
].sort((a, b) => a[1].localeCompare(b[1])),
|
||||
[runs, gameMap]
|
||||
|
||||
Reference in New Issue
Block a user