Merge branch 'develop' into feature/add-boss-data
This commit is contained in:
@@ -28,3 +28,11 @@ export function updateRun(
|
||||
export function deleteRun(id: number): Promise<void> {
|
||||
return api.del(`/runs/${id}`)
|
||||
}
|
||||
|
||||
export function getNamingCategories(): Promise<string[]> {
|
||||
return api.get('/runs/naming-categories')
|
||||
}
|
||||
|
||||
export function getNameSuggestions(runId: number, count = 10): Promise<string[]> {
|
||||
return api.get(`/runs/${runId}/name-suggestions?count=${count}`)
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useState, useEffect, useMemo } from 'react'
|
||||
import { useRoutePokemon } from '../hooks/useGames'
|
||||
import { useNameSuggestions } from '../hooks/useRuns'
|
||||
import {
|
||||
EncounterMethodBadge,
|
||||
getMethodLabel,
|
||||
@@ -15,6 +16,8 @@ import type {
|
||||
interface EncounterModalProps {
|
||||
route: Route
|
||||
gameId: number
|
||||
runId: number
|
||||
namingScheme?: string | null
|
||||
existing?: EncounterDetail
|
||||
dupedPokemonIds?: Set<number>
|
||||
retiredPokemonIds?: Set<number>
|
||||
@@ -92,6 +95,8 @@ function pickRandomPokemon(
|
||||
export function EncounterModal({
|
||||
route,
|
||||
gameId,
|
||||
runId,
|
||||
namingScheme,
|
||||
existing,
|
||||
dupedPokemonIds,
|
||||
retiredPokemonIds,
|
||||
@@ -120,6 +125,10 @@ export function EncounterModal({
|
||||
|
||||
const isEditing = !!existing
|
||||
|
||||
const showSuggestions = !!namingScheme && status === 'caught' && !isEditing
|
||||
const { data: suggestions, refetch: regenerate, isFetching: loadingSuggestions } =
|
||||
useNameSuggestions(showSuggestions ? runId : null)
|
||||
|
||||
// Pre-select pokemon when editing
|
||||
useEffect(() => {
|
||||
if (existing && routePokemon) {
|
||||
@@ -380,6 +389,39 @@ export function EncounterModal({
|
||||
placeholder="Give it a name..."
|
||||
className="w-full px-3 py-2 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
/>
|
||||
{showSuggestions && suggestions && suggestions.length > 0 && (
|
||||
<div className="mt-2">
|
||||
<div className="flex items-center justify-between mb-1.5">
|
||||
<span className="text-xs text-gray-500 dark:text-gray-400">
|
||||
Suggestions ({namingScheme})
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => regenerate()}
|
||||
disabled={loadingSuggestions}
|
||||
className="text-xs text-blue-600 dark:text-blue-400 hover:text-blue-700 dark:hover:text-blue-300 disabled:opacity-50 transition-colors"
|
||||
>
|
||||
{loadingSuggestions ? 'Loading...' : 'Regenerate'}
|
||||
</button>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-1.5">
|
||||
{suggestions.map((name) => (
|
||||
<button
|
||||
key={name}
|
||||
type="button"
|
||||
onClick={() => setNickname(name)}
|
||||
className={`px-2.5 py-1 text-xs rounded-full border transition-colors ${
|
||||
nickname === name
|
||||
? 'bg-blue-100 border-blue-300 text-blue-800 dark:bg-blue-900/40 dark:border-blue-600 dark:text-blue-300'
|
||||
: 'border-gray-300 dark:border-gray-600 text-gray-700 dark:text-gray-300 hover:border-blue-300 dark:hover:border-blue-600 hover:bg-blue-50 dark:hover:bg-blue-900/20'
|
||||
}`}
|
||||
>
|
||||
{name}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
|
||||
import { toast } from 'sonner'
|
||||
import { getRuns, getRun, createRun, updateRun, deleteRun } from '../api/runs'
|
||||
import { getRuns, getRun, createRun, updateRun, deleteRun, getNamingCategories, getNameSuggestions } from '../api/runs'
|
||||
import type { CreateRunInput, UpdateRunInput } from '../types/game'
|
||||
|
||||
export function useRuns() {
|
||||
@@ -51,3 +51,19 @@ export function useDeleteRun() {
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export function useNamingCategories() {
|
||||
return useQuery({
|
||||
queryKey: ['naming-categories'],
|
||||
queryFn: getNamingCategories,
|
||||
staleTime: Infinity,
|
||||
})
|
||||
}
|
||||
|
||||
export function useNameSuggestions(runId: number | null) {
|
||||
return useQuery({
|
||||
queryKey: ['name-suggestions', runId],
|
||||
queryFn: () => getNameSuggestions(runId!),
|
||||
enabled: runId !== null,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ import { useMemo, useState } from 'react'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { GameGrid, RulesConfiguration, StepIndicator } from '../components'
|
||||
import { useGames, useGameRoutes } from '../hooks/useGames'
|
||||
import { useCreateRun, useRuns } from '../hooks/useRuns'
|
||||
import { useCreateRun, useRuns, useNamingCategories } from '../hooks/useRuns'
|
||||
import type { Game, NuzlockeRules } from '../types'
|
||||
import { DEFAULT_RULES } from '../types'
|
||||
import { RULE_DEFINITIONS } from '../types/rules'
|
||||
@@ -14,11 +14,13 @@ export function NewRun() {
|
||||
const { data: games, isLoading, error } = useGames()
|
||||
const { data: runs } = useRuns()
|
||||
const createRun = useCreateRun()
|
||||
const { data: namingCategories } = useNamingCategories()
|
||||
|
||||
const [step, setStep] = useState(1)
|
||||
const [selectedGame, setSelectedGame] = useState<Game | null>(null)
|
||||
const [rules, setRules] = useState<NuzlockeRules>(DEFAULT_RULES)
|
||||
const [runName, setRunName] = useState('')
|
||||
const [namingScheme, setNamingScheme] = useState<string | null>(null)
|
||||
const { data: routes } = useGameRoutes(selectedGame?.id ?? null)
|
||||
|
||||
const hiddenRules = useMemo(() => {
|
||||
@@ -44,7 +46,7 @@ export function NewRun() {
|
||||
const handleCreate = () => {
|
||||
if (!selectedGame) return
|
||||
createRun.mutate(
|
||||
{ gameId: selectedGame.id, name: runName, rules },
|
||||
{ gameId: selectedGame.id, name: runName, rules, namingScheme },
|
||||
{ onSuccess: (data) => navigate(`/runs/${data.id}`) },
|
||||
)
|
||||
}
|
||||
@@ -180,6 +182,33 @@ export function NewRun() {
|
||||
/>
|
||||
</div>
|
||||
|
||||
{namingCategories && namingCategories.length > 0 && (
|
||||
<div>
|
||||
<label
|
||||
htmlFor="naming-scheme"
|
||||
className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1"
|
||||
>
|
||||
Naming Scheme
|
||||
</label>
|
||||
<select
|
||||
id="naming-scheme"
|
||||
value={namingScheme ?? ''}
|
||||
onChange={(e) => setNamingScheme(e.target.value || null)}
|
||||
className="w-full px-3 py-2 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||
>
|
||||
<option value="">None (manual nicknames)</option>
|
||||
{namingCategories.map((cat) => (
|
||||
<option key={cat} value={cat}>
|
||||
{cat.charAt(0).toUpperCase() + cat.slice(1)}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<p className="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
||||
Get nickname suggestions from a themed word list when catching Pokemon.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="border-t border-gray-200 dark:border-gray-700 pt-4">
|
||||
<h3 className="text-sm font-medium text-gray-500 dark:text-gray-400 mb-2">
|
||||
Summary
|
||||
@@ -203,6 +232,14 @@ export function NewRun() {
|
||||
{enabledRuleCount} of {totalRuleCount} enabled
|
||||
</dd>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<dt className="text-gray-600 dark:text-gray-400">Naming Scheme</dt>
|
||||
<dd className="text-gray-900 dark:text-gray-100 font-medium">
|
||||
{namingScheme
|
||||
? namingScheme.charAt(0).toUpperCase() + namingScheme.slice(1)
|
||||
: 'None'}
|
||||
</dd>
|
||||
</div>
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useMemo, useState } from 'react'
|
||||
import { useParams, Link } from 'react-router-dom'
|
||||
import { useRun, useUpdateRun } from '../hooks/useRuns'
|
||||
import { useRun, useUpdateRun, useNamingCategories } from '../hooks/useRuns'
|
||||
import { useGameRoutes } from '../hooks/useGames'
|
||||
import { useCreateEncounter, useUpdateEncounter } from '../hooks/useEncounters'
|
||||
import { StatCard, PokemonCard, RuleBadges, StatusChangeModal, EndRunModal } from '../components'
|
||||
@@ -51,6 +51,7 @@ export function RunDashboard() {
|
||||
const createEncounter = useCreateEncounter(runIdNum)
|
||||
const updateEncounter = useUpdateEncounter(runIdNum)
|
||||
const updateRun = useUpdateRun(runIdNum)
|
||||
const { data: namingCategories } = useNamingCategories()
|
||||
const [selectedEncounter, setSelectedEncounter] =
|
||||
useState<EncounterDetail | null>(null)
|
||||
const [showEndRun, setShowEndRun] = useState(false)
|
||||
@@ -197,6 +198,37 @@ export function RunDashboard() {
|
||||
<RuleBadges rules={run.rules} />
|
||||
</div>
|
||||
|
||||
{/* Naming Scheme */}
|
||||
{namingCategories && namingCategories.length > 0 && (
|
||||
<div className="mb-6">
|
||||
<h2 className="text-sm font-medium text-gray-500 dark:text-gray-400 mb-2">
|
||||
Naming Scheme
|
||||
</h2>
|
||||
{isActive ? (
|
||||
<select
|
||||
value={run.namingScheme ?? ''}
|
||||
onChange={(e) =>
|
||||
updateRun.mutate({ namingScheme: e.target.value || null })
|
||||
}
|
||||
className="text-sm border border-gray-300 dark:border-gray-600 rounded-lg px-3 py-1.5 bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100"
|
||||
>
|
||||
<option value="">None</option>
|
||||
{namingCategories.map((cat) => (
|
||||
<option key={cat} value={cat}>
|
||||
{cat.charAt(0).toUpperCase() + cat.slice(1)}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
) : (
|
||||
<span className="text-sm text-gray-900 dark:text-gray-100">
|
||||
{run.namingScheme
|
||||
? run.namingScheme.charAt(0).toUpperCase() + run.namingScheme.slice(1)
|
||||
: 'None'}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Active Team */}
|
||||
<div className="mb-6">
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
|
||||
@@ -1435,6 +1435,8 @@ export function RunEncounters() {
|
||||
<EncounterModal
|
||||
route={selectedRoute}
|
||||
gameId={run!.gameId}
|
||||
runId={runIdNum}
|
||||
namingScheme={run!.namingScheme}
|
||||
existing={editingEncounter ?? undefined}
|
||||
dupedPokemonIds={dupedPokemonIds}
|
||||
retiredPokemonIds={retiredPokemonIds}
|
||||
|
||||
Reference in New Issue
Block a user