feature/boss-sprites-and-badges (#22)
Reviewed-on: TheFurya/nuzlocke-tracker#22 Co-authored-by: Julian Tabel <juliantabel.jt@gmail.com> Co-committed-by: Julian Tabel <juliantabel.jt@gmail.com>
This commit was merged in pull request #22.
This commit is contained in:
@@ -1,8 +1,9 @@
|
||||
import { api } from './client'
|
||||
import type { BossBattle, BossResult, CreateBossResultInput } from '../types/game'
|
||||
|
||||
export function getGameBosses(gameId: number): Promise<BossBattle[]> {
|
||||
return api.get(`/games/${gameId}/bosses`)
|
||||
export function getGameBosses(gameId: number, all?: boolean): Promise<BossBattle[]> {
|
||||
const params = all ? '?all=true' : ''
|
||||
return api.get(`/games/${gameId}/bosses${params}`)
|
||||
}
|
||||
|
||||
export function getBossResults(runId: number): Promise<BossResult[]> {
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import { type FormEvent, useState } from 'react'
|
||||
import { FormModal } from './FormModal'
|
||||
import type { BossBattle, Route } from '../../types/game'
|
||||
import type { BossBattle, Game, Route } from '../../types/game'
|
||||
import type { CreateBossBattleInput, UpdateBossBattleInput } from '../../types/admin'
|
||||
|
||||
interface BossBattleFormModalProps {
|
||||
boss?: BossBattle
|
||||
routes: Route[]
|
||||
games?: Game[]
|
||||
nextOrder: number
|
||||
onSubmit: (data: CreateBossBattleInput | UpdateBossBattleInput) => void
|
||||
onClose: () => void
|
||||
@@ -35,6 +36,7 @@ const BOSS_TYPES = [
|
||||
export function BossBattleFormModal({
|
||||
boss,
|
||||
routes,
|
||||
games,
|
||||
nextOrder,
|
||||
onSubmit,
|
||||
onClose,
|
||||
@@ -54,6 +56,7 @@ export function BossBattleFormModal({
|
||||
const [location, setLocation] = useState(boss?.location ?? '')
|
||||
const [section, setSection] = useState(boss?.section ?? '')
|
||||
const [spriteUrl, setSpriteUrl] = useState(boss?.spriteUrl ?? '')
|
||||
const [gameId, setGameId] = useState(String(boss?.gameId ?? ''))
|
||||
|
||||
const handleSubmit = (e: FormEvent) => {
|
||||
e.preventDefault()
|
||||
@@ -69,6 +72,7 @@ export function BossBattleFormModal({
|
||||
location,
|
||||
section: section || null,
|
||||
spriteUrl: spriteUrl || null,
|
||||
gameId: gameId ? Number(gameId) : null,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -173,15 +177,34 @@ export function BossBattleFormModal({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-1">Section</label>
|
||||
<input
|
||||
type="text"
|
||||
value={section}
|
||||
onChange={(e) => setSection(e.target.value)}
|
||||
placeholder="e.g. Main Story, Endgame"
|
||||
className="w-full px-3 py-2 border rounded-md dark:bg-gray-700 dark:border-gray-600"
|
||||
/>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-1">Section</label>
|
||||
<input
|
||||
type="text"
|
||||
value={section}
|
||||
onChange={(e) => setSection(e.target.value)}
|
||||
placeholder="e.g. Main Story, Endgame"
|
||||
className="w-full px-3 py-2 border rounded-md dark:bg-gray-700 dark:border-gray-600"
|
||||
/>
|
||||
</div>
|
||||
{games && games.length > 1 && (
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-1">Game (version exclusive)</label>
|
||||
<select
|
||||
value={gameId}
|
||||
onChange={(e) => setGameId(e.target.value)}
|
||||
className="w-full px-3 py-2 border rounded-md dark:bg-gray-700 dark:border-gray-600"
|
||||
>
|
||||
<option value="">All games</option>
|
||||
{games.map((g) => (
|
||||
<option key={g.id} value={g.id}>
|
||||
{g.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
|
||||
@@ -3,10 +3,10 @@ import { toast } from 'sonner'
|
||||
import { getGameBosses, getBossResults, createBossResult, deleteBossResult } from '../api/bosses'
|
||||
import type { CreateBossResultInput } from '../types/game'
|
||||
|
||||
export function useGameBosses(gameId: number | null) {
|
||||
export function useGameBosses(gameId: number | null, all?: boolean) {
|
||||
return useQuery({
|
||||
queryKey: ['games', gameId, 'bosses'],
|
||||
queryFn: () => getGameBosses(gameId!),
|
||||
queryKey: ['games', gameId, 'bosses', { all }],
|
||||
queryFn: () => getGameBosses(gameId!, all),
|
||||
enabled: gameId != null,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1376,7 +1376,7 @@ export function RunEncounters() {
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M9 5l7 7-7 7" />
|
||||
</svg>
|
||||
{boss.spriteUrl && (
|
||||
<img src={boss.spriteUrl} alt={boss.name} className="w-10 h-10" />
|
||||
<img src={boss.spriteUrl} alt={boss.name} className="h-10 w-auto" />
|
||||
)}
|
||||
<div>
|
||||
<div className="flex items-center gap-2">
|
||||
|
||||
@@ -21,7 +21,7 @@ import { RouteFormModal } from '../../components/admin/RouteFormModal'
|
||||
import { BossBattleFormModal } from '../../components/admin/BossBattleFormModal'
|
||||
import { BossTeamEditor } from '../../components/admin/BossTeamEditor'
|
||||
import { TypeBadge } from '../../components/TypeBadge'
|
||||
import { useGame } from '../../hooks/useGames'
|
||||
import { useGame, useGames } from '../../hooks/useGames'
|
||||
import { useGameBosses } from '../../hooks/useBosses'
|
||||
import {
|
||||
useCreateRoute,
|
||||
@@ -162,11 +162,13 @@ function SortableRouteGroup({
|
||||
function SortableBossRow({
|
||||
boss,
|
||||
routes,
|
||||
games,
|
||||
onPositionChange,
|
||||
onClick,
|
||||
}: {
|
||||
boss: BossBattle
|
||||
routes: GameRoute[]
|
||||
games: import('../../types/game').Game[]
|
||||
onPositionChange: (bossId: number, afterRouteId: number | null) => void
|
||||
onClick: (b: BossBattle) => void
|
||||
}) {
|
||||
@@ -204,7 +206,17 @@ function SortableBossRow({
|
||||
</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 font-medium">
|
||||
{boss.name}
|
||||
{boss.gameId != null && (() => {
|
||||
const g = games.find((g) => g.id === boss.gameId)
|
||||
return g ? (
|
||||
<span className="ml-2 text-xs px-1.5 py-0.5 rounded bg-gray-100 dark:bg-gray-700 text-gray-500 dark:text-gray-400">
|
||||
{g.name}
|
||||
</span>
|
||||
) : null
|
||||
})()}
|
||||
</td>
|
||||
<td className="px-4 py-3 text-sm whitespace-nowrap capitalize">
|
||||
{boss.bossType.replace('_', ' ')}
|
||||
</td>
|
||||
@@ -247,7 +259,8 @@ export function AdminGameDetail() {
|
||||
const deleteRoute = useDeleteRoute(id)
|
||||
const reorderRoutes = useReorderRoutes(id)
|
||||
const bulkImportRoutes = useBulkImportRoutes(id)
|
||||
const { data: bosses } = useGameBosses(id)
|
||||
const { data: bosses } = useGameBosses(id, true)
|
||||
const { data: allGames } = useGames()
|
||||
const createBoss = useCreateBossBattle(id)
|
||||
const updateBoss = useUpdateBossBattle(id)
|
||||
const deleteBoss = useDeleteBossBattle(id)
|
||||
@@ -273,6 +286,9 @@ export function AdminGameDetail() {
|
||||
|
||||
const routes = game.routes ?? []
|
||||
const routeGroups = organizeRoutes(routes)
|
||||
const versionGroupGames = (allGames ?? []).filter(
|
||||
(g) => g.versionGroupId === game.versionGroupId,
|
||||
)
|
||||
|
||||
const handleDragEnd = (event: DragEndEvent) => {
|
||||
const { active, over } = event
|
||||
@@ -573,6 +589,7 @@ export function AdminGameDetail() {
|
||||
key={boss.id}
|
||||
boss={boss}
|
||||
routes={routes}
|
||||
games={versionGroupGames}
|
||||
onPositionChange={(bossId, afterRouteId) =>
|
||||
updateBoss.mutate({
|
||||
bossId,
|
||||
@@ -596,6 +613,7 @@ export function AdminGameDetail() {
|
||||
{showCreateBoss && (
|
||||
<BossBattleFormModal
|
||||
routes={routes}
|
||||
games={versionGroupGames}
|
||||
nextOrder={bosses ? Math.max(0, ...bosses.map((b) => b.order)) + 1 : 1}
|
||||
onSubmit={(data) =>
|
||||
createBoss.mutate(data as CreateBossBattleInput, {
|
||||
@@ -611,6 +629,7 @@ export function AdminGameDetail() {
|
||||
<BossBattleFormModal
|
||||
boss={editingBoss}
|
||||
routes={routes}
|
||||
games={versionGroupGames}
|
||||
nextOrder={editingBoss.order}
|
||||
onSubmit={(data) =>
|
||||
updateBoss.mutate(
|
||||
|
||||
@@ -151,6 +151,7 @@ export interface CreateBossBattleInput {
|
||||
location: string
|
||||
section?: string | null
|
||||
spriteUrl?: string | null
|
||||
gameId?: number | null
|
||||
}
|
||||
|
||||
export interface UpdateBossBattleInput {
|
||||
@@ -165,6 +166,7 @@ export interface UpdateBossBattleInput {
|
||||
location?: string
|
||||
section?: string | null
|
||||
spriteUrl?: string | null
|
||||
gameId?: number | null
|
||||
}
|
||||
|
||||
export interface BossReorderItem {
|
||||
|
||||
@@ -188,6 +188,7 @@ export interface BossBattle {
|
||||
location: string
|
||||
section: string | null
|
||||
spriteUrl: string | null
|
||||
gameId: number | null
|
||||
pokemon: BossPokemon[]
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user