Add naming scheme selection to run configuration
Add a nullable naming_scheme column to NuzlockeRun so users can pick a themed word category for nickname suggestions. Includes Alembic migration, updated Pydantic schemas, a GET /runs/naming-categories endpoint backed by a cached dictionary loader, and frontend dropdowns in both the NewRun creation flow and the RunDashboard for mid-run changes. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -3,8 +3,9 @@
|
|||||||
title: Integrate name suggestions into encounter registration UI
|
title: Integrate name suggestions into encounter registration UI
|
||||||
status: todo
|
status: todo
|
||||||
type: task
|
type: task
|
||||||
|
priority: normal
|
||||||
created_at: 2026-02-11T15:56:44Z
|
created_at: 2026-02-11T15:56:44Z
|
||||||
updated_at: 2026-02-11T15:56:44Z
|
updated_at: 2026-02-11T20:23:40Z
|
||||||
parent: nuzlocke-tracker-igl3
|
parent: nuzlocke-tracker-igl3
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -12,17 +13,23 @@ Show name suggestions in the encounter registration flow so users can pick a nic
|
|||||||
|
|
||||||
## Requirements
|
## Requirements
|
||||||
|
|
||||||
- When a user clicks an encounter slot and registers a new Pokemon, display 5-10 name suggestions below/near the nickname input
|
- When a user registers a new Pokemon encounter, display 5-10 name suggestions below/near the nickname input
|
||||||
- Each suggestion is a clickable chip/button that fills in the nickname field
|
- Each suggestion is a clickable chip/button that fills in the nickname field
|
||||||
- Include a "regenerate" button to get a fresh batch of suggestions
|
- Include a "regenerate" button to get a fresh batch of suggestions
|
||||||
- Only show suggestions if the run has a naming scheme selected
|
- Only show suggestions if the run has a naming scheme selected
|
||||||
- The nickname input should still be editable for manual entry
|
- The nickname input should still be editable for manual entry
|
||||||
|
|
||||||
|
## Implementation Notes
|
||||||
|
|
||||||
|
- **Data fetching**: Call `GET /api/v1/runs/{run_id}/name-suggestions?count=10` to get suggestions from the backend.
|
||||||
|
- **Regeneration**: Each call to the endpoint returns a fresh random batch (backend handles exclusion of used names).
|
||||||
|
- **No dictionary data in frontend**: All suggestion logic lives in the backend.
|
||||||
|
|
||||||
## Checklist
|
## Checklist
|
||||||
|
|
||||||
- [ ] Add a name suggestions component (chips/buttons with regenerate)
|
- [ ] Add a name suggestions component (chips/buttons with regenerate)
|
||||||
- [ ] Integrate the component into the encounter registration modal/form
|
- [ ] Integrate the component into the encounter registration modal/form
|
||||||
- [ ] Wire up the name suggestion engine to the component
|
- [ ] Wire up the backend API endpoint to the component via React Query
|
||||||
- [ ] Ensure clicking a suggestion populates the nickname field
|
- [ ] Ensure clicking a suggestion populates the nickname field
|
||||||
- [ ] Ensure regenerate fetches a new batch without repeating prior suggestions
|
- [ ] Ensure regenerate fetches a new batch from the API
|
||||||
- [ ] Hide suggestions gracefully if no naming scheme is set on the run
|
- [ ] Hide suggestions gracefully if no naming scheme is set on the run
|
||||||
@@ -5,24 +5,32 @@ status: todo
|
|||||||
type: task
|
type: task
|
||||||
priority: normal
|
priority: normal
|
||||||
created_at: 2026-02-11T15:56:44Z
|
created_at: 2026-02-11T15:56:44Z
|
||||||
updated_at: 2026-02-11T15:56:48Z
|
updated_at: 2026-02-11T20:23:35Z
|
||||||
parent: nuzlocke-tracker-igl3
|
parent: nuzlocke-tracker-igl3
|
||||||
blocking:
|
blocking:
|
||||||
- nuzlocke-tracker-bi4e
|
- nuzlocke-tracker-bi4e
|
||||||
---
|
---
|
||||||
|
|
||||||
Build the core logic that picks random name suggestions from the dictionary based on a selected naming scheme (category).
|
Build the backend service and API endpoint that picks random name suggestions from the dictionary based on a selected naming scheme.
|
||||||
|
|
||||||
## Requirements
|
## Requirements
|
||||||
|
|
||||||
- Given a category and a list of already-used names in the run, return 5-10 unique suggestions
|
- Given a category and a run ID, return 5-10 unique suggestions
|
||||||
- Suggestions must not include names already assigned to other Pokemon in the same run
|
- The engine queries the run's existing encounter nicknames and excludes them from suggestions
|
||||||
- Support regeneration (return a fresh batch, avoiding previously shown suggestions where possible)
|
- Support regeneration (return a fresh batch, avoiding previously shown suggestions where possible)
|
||||||
- Handle edge case where category is nearly exhausted gracefully (return fewer suggestions)
|
- Handle edge case where category is nearly exhausted gracefully (return fewer suggestions)
|
||||||
|
|
||||||
|
## Implementation Notes
|
||||||
|
|
||||||
|
- **Backend service**: A Python module that loads the dictionary JSON, filters by category, excludes used names, and picks random suggestions.
|
||||||
|
- **API endpoint**: `GET /api/v1/runs/{run_id}/name-suggestions?count=10` — reads the run's `naming_scheme`, fetches encounter nicknames, returns suggestions.
|
||||||
|
- **No new DB tables needed**: Used names come from `Encounter.nickname` on the run's encounters.
|
||||||
|
- **Caching**: Load the dictionary once and cache in memory (it's static data).
|
||||||
|
|
||||||
## Checklist
|
## Checklist
|
||||||
|
|
||||||
- [ ] Create a service/utility module for name suggestion logic
|
- [ ] Create a service module for name suggestion logic (e.g. `services/name_suggestions.py`)
|
||||||
|
- [ ] Implement dictionary loading with in-memory caching
|
||||||
- [ ] Implement random selection from a category with exclusion of used names
|
- [ ] Implement random selection from a category with exclusion of used names
|
||||||
- [ ] Implement regeneration that avoids repeating previous suggestions
|
- [ ] Add API endpoint for fetching suggestions
|
||||||
- [ ] Add unit tests for the suggestion logic
|
- [ ] Add unit tests for the suggestion logic
|
||||||
@@ -5,14 +5,22 @@ status: todo
|
|||||||
type: epic
|
type: epic
|
||||||
priority: normal
|
priority: normal
|
||||||
created_at: 2026-02-05T13:45:15Z
|
created_at: 2026-02-05T13:45:15Z
|
||||||
updated_at: 2026-02-11T15:57:27Z
|
updated_at: 2026-02-11T20:23:14Z
|
||||||
---
|
---
|
||||||
|
|
||||||
Implement a dictionary-based nickname generation system for Nuzlocke runs. Instead of using an LLM API to generate names on the fly, provide a static dictionary of words categorised by theme. A word can belong to multiple categories, making it usable across different naming schemes.
|
Implement a dictionary-based nickname generation system for Nuzlocke runs. Instead of using an LLM API to generate names on the fly, provide a static dictionary of words categorised by theme. A word can belong to multiple categories, making it usable across different naming schemes.
|
||||||
|
|
||||||
|
## Architecture Decisions
|
||||||
|
|
||||||
|
- **Dictionary storage**: Static JSON file in `backend/src/app/seeds/data/`, alongside other seed data. Not exposed to frontend directly.
|
||||||
|
- **Dictionary format**: Category-keyed structure (`{ "mythology": ["Apollo", ...], "space": ["Nova", ...] }`) for fast lookup by naming scheme. Words may appear in multiple categories.
|
||||||
|
- **Suggestion logic**: Backend service with API endpoint. Frontend calls the backend to get suggestions.
|
||||||
|
- **Used-name tracking**: No new storage needed. The existing `Encounter.nickname` field already tracks assigned names. The suggestion engine queries encounter nicknames for the current run and excludes them.
|
||||||
|
- **Naming scheme per run**: Dedicated nullable `naming_scheme` column on `NuzlockeRun` (not in the `rules` JSONB).
|
||||||
|
|
||||||
## Approach
|
## Approach
|
||||||
|
|
||||||
- **Static dictionary**: A local data file (JSON) containing words tagged with categories (e.g. mythology, food, space, nature, warriors, music, etc.)
|
- **Static dictionary**: A local data file (JSON) containing words organised by category (e.g. mythology, food, space, nature, warriors, music, etc.)
|
||||||
- **~150-200 words per category**: A typical Nuzlocke has ~100 encounters, so this provides ample variety without repetition.
|
- **~150-200 words per category**: A typical Nuzlocke has ~100 encounters, so this provides ample variety without repetition.
|
||||||
- **Name suggestion UX**: When registering a new encounter, the user is shown 5-10 suggested names from their chosen naming scheme. They can click one to select it, or regenerate for a fresh batch.
|
- **Name suggestion UX**: When registering a new encounter, the user is shown 5-10 suggested names from their chosen naming scheme. They can click one to select it, or regenerate for a fresh batch.
|
||||||
- **Naming scheme selection**: Users pick a naming scheme (category) per run, either at run creation or in run settings.
|
- **Naming scheme selection**: Users pick a naming scheme (category) per run, either at run creation or in run settings.
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
---
|
---
|
||||||
# nuzlocke-tracker-m86o
|
# nuzlocke-tracker-m86o
|
||||||
title: Add naming scheme selection to run configuration
|
title: Add naming scheme selection to run configuration
|
||||||
status: todo
|
status: in-progress
|
||||||
type: task
|
type: task
|
||||||
priority: normal
|
priority: normal
|
||||||
created_at: 2026-02-11T15:56:44Z
|
created_at: 2026-02-11T15:56:44Z
|
||||||
updated_at: 2026-02-11T15:56:48Z
|
updated_at: 2026-02-11T20:35:59Z
|
||||||
parent: nuzlocke-tracker-igl3
|
parent: nuzlocke-tracker-igl3
|
||||||
blocking:
|
blocking:
|
||||||
- nuzlocke-tracker-bi4e
|
- nuzlocke-tracker-bi4e
|
||||||
@@ -15,14 +15,24 @@ Allow users to select a naming scheme (category) for their Nuzlocke run.
|
|||||||
|
|
||||||
## Requirements
|
## Requirements
|
||||||
|
|
||||||
- Add a `namingScheme` field to the run model/settings (optional, nullable — user may not want auto-naming)
|
- Add a `naming_scheme` column to the NuzlockeRun model (nullable string — user may not want auto-naming)
|
||||||
- Provide a dropdown/selector in run creation and run settings where the user can pick a category
|
- Provide a dropdown/selector in run creation and run settings where the user can pick a category
|
||||||
- List available categories dynamically from the dictionary data
|
- List available categories dynamically by querying a backend endpoint that reads the dictionary file
|
||||||
- Allow changing the naming scheme mid-run
|
- Allow changing the naming scheme mid-run
|
||||||
|
|
||||||
|
## Implementation Notes
|
||||||
|
|
||||||
|
- **Storage**: Dedicated nullable `naming_scheme` column on `NuzlockeRun` (not in the `rules` JSONB). This is a first-class run setting.
|
||||||
|
- **Migration**: Add an Alembic migration for the new column.
|
||||||
|
- **API**: Add/update the run creation and update endpoints to accept `namingScheme`.
|
||||||
|
- **Categories endpoint**: Add a GET endpoint that returns the list of available category names from the dictionary file, so the frontend can populate the dropdown.
|
||||||
|
|
||||||
## Checklist
|
## Checklist
|
||||||
|
|
||||||
- [ ] Add `namingScheme` field to the NuzlockeRun type/model
|
- [x] Add `naming_scheme` nullable column to NuzlockeRun model
|
||||||
- [ ] Add naming scheme selector to run creation UI
|
- [x] Create Alembic migration for the new column
|
||||||
- [ ] Add naming scheme selector to run settings UI
|
- [x] Update run Pydantic schemas to include `namingScheme`
|
||||||
- [ ] Persist the selected naming scheme with the run data
|
- [x] Update run creation and update endpoints to persist the naming scheme
|
||||||
|
- [x] Add GET endpoint to list available naming categories from the dictionary
|
||||||
|
- [x] Add naming scheme selector to run creation UI
|
||||||
|
- [x] Add naming scheme selector to run settings UI
|
||||||
@@ -5,25 +5,34 @@ status: todo
|
|||||||
type: task
|
type: task
|
||||||
priority: normal
|
priority: normal
|
||||||
created_at: 2026-02-11T15:56:26Z
|
created_at: 2026-02-11T15:56:26Z
|
||||||
updated_at: 2026-02-11T15:56:48Z
|
updated_at: 2026-02-11T20:23:29Z
|
||||||
parent: nuzlocke-tracker-igl3
|
parent: nuzlocke-tracker-igl3
|
||||||
blocking:
|
blocking:
|
||||||
- nuzlocke-tracker-c6ly
|
- nuzlocke-tracker-c6ly
|
||||||
---
|
---
|
||||||
|
|
||||||
Create a JSON data file containing themed words for nickname generation.
|
Create a JSON data file containing themed words for nickname generation, stored in the backend alongside other seed data.
|
||||||
|
|
||||||
## Requirements
|
## Requirements
|
||||||
|
|
||||||
- Each word entry has: `word` (string) and `categories` (string array)
|
- Store at `backend/src/app/seeds/data/name_dictionary.json`
|
||||||
|
- Category-keyed structure for fast lookup:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"mythology": ["Apollo", "Athena", "Loki", ...],
|
||||||
|
"space": ["Apollo", "Nova", "Nebula", ...],
|
||||||
|
"food": ["Basil", "Sage", "Pepper", ...]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
- Words may appear in multiple categories
|
||||||
- Categories should include themes like: mythology, food, space, nature, warriors, music, literature, gems, ocean, weather, etc.
|
- Categories should include themes like: mythology, food, space, nature, warriors, music, literature, gems, ocean, weather, etc.
|
||||||
- Target 150-200 words per category
|
- Target 150-200 words per category
|
||||||
- Words can belong to multiple categories
|
|
||||||
- Words should be short, punchy, and suitable as Pokemon nicknames (ideally 1-2 words, max ~12 characters)
|
- Words should be short, punchy, and suitable as Pokemon nicknames (ideally 1-2 words, max ~12 characters)
|
||||||
|
- This file is NOT seeded into the database — it is read directly by the backend service at runtime
|
||||||
|
|
||||||
## Checklist
|
## Checklist
|
||||||
|
|
||||||
- [ ] Define the data schema / TypeScript type for dictionary entries
|
- [ ] Create `backend/src/app/seeds/data/name_dictionary.json` with the category-keyed structure
|
||||||
- [ ] Create the JSON data file with initial categories
|
|
||||||
- [ ] Populate each category with 150-200 words
|
- [ ] Populate each category with 150-200 words
|
||||||
- [ ] Validate no duplicates exist within the file
|
- [ ] Validate no duplicates exist within a single category
|
||||||
|
- [ ] Add a utility function to load the dictionary from disk (with caching)
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
"""add naming_scheme to nuzlocke_runs
|
||||||
|
|
||||||
|
Revision ID: e5f6a7b8c9d1
|
||||||
|
Revises: d4e5f6a7b9c0
|
||||||
|
Create Date: 2026-02-11 12:00:00.000000
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
from collections.abc import Sequence
|
||||||
|
|
||||||
|
import sqlalchemy as sa
|
||||||
|
from alembic import op
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision: str = "e5f6a7b8c9d1"
|
||||||
|
down_revision: str | Sequence[str] | None = "d4e5f6a7b9c0"
|
||||||
|
branch_labels: str | Sequence[str] | None = None
|
||||||
|
depends_on: str | Sequence[str] | None = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
op.add_column(
|
||||||
|
"nuzlocke_runs",
|
||||||
|
sa.Column("naming_scheme", sa.String(50), nullable=True),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
op.drop_column("nuzlocke_runs", "naming_scheme")
|
||||||
@@ -19,10 +19,16 @@ from app.schemas.run import (
|
|||||||
RunResponse,
|
RunResponse,
|
||||||
RunUpdate,
|
RunUpdate,
|
||||||
)
|
)
|
||||||
|
from app.services.naming import get_naming_categories
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/naming-categories", response_model=list[str])
|
||||||
|
async def list_naming_categories():
|
||||||
|
return get_naming_categories()
|
||||||
|
|
||||||
|
|
||||||
@router.post("", response_model=RunResponse, status_code=201)
|
@router.post("", response_model=RunResponse, status_code=201)
|
||||||
async def create_run(data: RunCreate, session: AsyncSession = Depends(get_session)):
|
async def create_run(data: RunCreate, session: AsyncSession = Depends(get_session)):
|
||||||
# Validate game exists
|
# Validate game exists
|
||||||
@@ -35,6 +41,7 @@ async def create_run(data: RunCreate, session: AsyncSession = Depends(get_sessio
|
|||||||
name=data.name,
|
name=data.name,
|
||||||
status="active",
|
status="active",
|
||||||
rules=data.rules,
|
rules=data.rules,
|
||||||
|
naming_scheme=data.naming_scheme,
|
||||||
)
|
)
|
||||||
session.add(run)
|
session.add(run)
|
||||||
await session.commit()
|
await session.commit()
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ class NuzlockeRun(Base):
|
|||||||
)
|
)
|
||||||
completed_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
|
completed_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
|
||||||
hof_encounter_ids: Mapped[list[int] | None] = mapped_column(JSONB, default=None)
|
hof_encounter_ids: Mapped[list[int] | None] = mapped_column(JSONB, default=None)
|
||||||
|
naming_scheme: Mapped[str | None] = mapped_column(String(50), nullable=True)
|
||||||
|
|
||||||
game: Mapped["Game"] = relationship(back_populates="runs")
|
game: Mapped["Game"] = relationship(back_populates="runs")
|
||||||
encounters: Mapped[list["Encounter"]] = relationship(back_populates="run")
|
encounters: Mapped[list["Encounter"]] = relationship(back_populates="run")
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ class RunCreate(CamelModel):
|
|||||||
game_id: int
|
game_id: int
|
||||||
name: str
|
name: str
|
||||||
rules: dict = {}
|
rules: dict = {}
|
||||||
|
naming_scheme: str | None = None
|
||||||
|
|
||||||
|
|
||||||
class RunUpdate(CamelModel):
|
class RunUpdate(CamelModel):
|
||||||
@@ -16,6 +17,7 @@ class RunUpdate(CamelModel):
|
|||||||
status: str | None = None
|
status: str | None = None
|
||||||
rules: dict | None = None
|
rules: dict | None = None
|
||||||
hof_encounter_ids: list[int] | None = None
|
hof_encounter_ids: list[int] | None = None
|
||||||
|
naming_scheme: str | None = None
|
||||||
|
|
||||||
|
|
||||||
class RunResponse(CamelModel):
|
class RunResponse(CamelModel):
|
||||||
@@ -25,6 +27,7 @@ class RunResponse(CamelModel):
|
|||||||
status: str
|
status: str
|
||||||
rules: dict
|
rules: dict
|
||||||
hof_encounter_ids: list[int] | None = None
|
hof_encounter_ids: list[int] | None = None
|
||||||
|
naming_scheme: str | None = None
|
||||||
started_at: datetime
|
started_at: datetime
|
||||||
completed_at: datetime | None
|
completed_at: datetime | None
|
||||||
|
|
||||||
|
|||||||
18
backend/src/app/services/naming.py
Normal file
18
backend/src/app/services/naming.py
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import json
|
||||||
|
from functools import lru_cache
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
DICTIONARY_PATH = Path(__file__).resolve().parents[1] / "seeds" / "data" / "name_dictionary.json"
|
||||||
|
|
||||||
|
|
||||||
|
@lru_cache(maxsize=1)
|
||||||
|
def _load_dictionary() -> dict[str, list[str]]:
|
||||||
|
if not DICTIONARY_PATH.exists():
|
||||||
|
return {}
|
||||||
|
with open(DICTIONARY_PATH) as f:
|
||||||
|
return json.load(f)
|
||||||
|
|
||||||
|
|
||||||
|
def get_naming_categories() -> list[str]:
|
||||||
|
"""Return sorted list of available naming category names."""
|
||||||
|
return sorted(_load_dictionary().keys())
|
||||||
@@ -28,3 +28,7 @@ export function updateRun(
|
|||||||
export function deleteRun(id: number): Promise<void> {
|
export function deleteRun(id: number): Promise<void> {
|
||||||
return api.del(`/runs/${id}`)
|
return api.del(`/runs/${id}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getNamingCategories(): Promise<string[]> {
|
||||||
|
return api.get('/runs/naming-categories')
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
|
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
|
||||||
import { toast } from 'sonner'
|
import { toast } from 'sonner'
|
||||||
import { getRuns, getRun, createRun, updateRun, deleteRun } from '../api/runs'
|
import { getRuns, getRun, createRun, updateRun, deleteRun, getNamingCategories } from '../api/runs'
|
||||||
import type { CreateRunInput, UpdateRunInput } from '../types/game'
|
import type { CreateRunInput, UpdateRunInput } from '../types/game'
|
||||||
|
|
||||||
export function useRuns() {
|
export function useRuns() {
|
||||||
@@ -51,3 +51,11 @@ export function useDeleteRun() {
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function useNamingCategories() {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: ['naming-categories'],
|
||||||
|
queryFn: getNamingCategories,
|
||||||
|
staleTime: Infinity,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { useMemo, useState } from 'react'
|
|||||||
import { useNavigate } from 'react-router-dom'
|
import { useNavigate } from 'react-router-dom'
|
||||||
import { GameGrid, RulesConfiguration, StepIndicator } from '../components'
|
import { GameGrid, RulesConfiguration, StepIndicator } from '../components'
|
||||||
import { useGames, useGameRoutes } from '../hooks/useGames'
|
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 type { Game, NuzlockeRules } from '../types'
|
||||||
import { DEFAULT_RULES } from '../types'
|
import { DEFAULT_RULES } from '../types'
|
||||||
import { RULE_DEFINITIONS } from '../types/rules'
|
import { RULE_DEFINITIONS } from '../types/rules'
|
||||||
@@ -14,11 +14,13 @@ export function NewRun() {
|
|||||||
const { data: games, isLoading, error } = useGames()
|
const { data: games, isLoading, error } = useGames()
|
||||||
const { data: runs } = useRuns()
|
const { data: runs } = useRuns()
|
||||||
const createRun = useCreateRun()
|
const createRun = useCreateRun()
|
||||||
|
const { data: namingCategories } = useNamingCategories()
|
||||||
|
|
||||||
const [step, setStep] = useState(1)
|
const [step, setStep] = useState(1)
|
||||||
const [selectedGame, setSelectedGame] = useState<Game | null>(null)
|
const [selectedGame, setSelectedGame] = useState<Game | null>(null)
|
||||||
const [rules, setRules] = useState<NuzlockeRules>(DEFAULT_RULES)
|
const [rules, setRules] = useState<NuzlockeRules>(DEFAULT_RULES)
|
||||||
const [runName, setRunName] = useState('')
|
const [runName, setRunName] = useState('')
|
||||||
|
const [namingScheme, setNamingScheme] = useState<string | null>(null)
|
||||||
const { data: routes } = useGameRoutes(selectedGame?.id ?? null)
|
const { data: routes } = useGameRoutes(selectedGame?.id ?? null)
|
||||||
|
|
||||||
const hiddenRules = useMemo(() => {
|
const hiddenRules = useMemo(() => {
|
||||||
@@ -44,7 +46,7 @@ export function NewRun() {
|
|||||||
const handleCreate = () => {
|
const handleCreate = () => {
|
||||||
if (!selectedGame) return
|
if (!selectedGame) return
|
||||||
createRun.mutate(
|
createRun.mutate(
|
||||||
{ gameId: selectedGame.id, name: runName, rules },
|
{ gameId: selectedGame.id, name: runName, rules, namingScheme },
|
||||||
{ onSuccess: (data) => navigate(`/runs/${data.id}`) },
|
{ onSuccess: (data) => navigate(`/runs/${data.id}`) },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -180,6 +182,33 @@ export function NewRun() {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</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">
|
<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">
|
<h3 className="text-sm font-medium text-gray-500 dark:text-gray-400 mb-2">
|
||||||
Summary
|
Summary
|
||||||
@@ -203,6 +232,14 @@ export function NewRun() {
|
|||||||
{enabledRuleCount} of {totalRuleCount} enabled
|
{enabledRuleCount} of {totalRuleCount} enabled
|
||||||
</dd>
|
</dd>
|
||||||
</div>
|
</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>
|
</dl>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useMemo, useState } from 'react'
|
import { useMemo, useState } from 'react'
|
||||||
import { useParams, Link } from 'react-router-dom'
|
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 { useGameRoutes } from '../hooks/useGames'
|
||||||
import { useCreateEncounter, useUpdateEncounter } from '../hooks/useEncounters'
|
import { useCreateEncounter, useUpdateEncounter } from '../hooks/useEncounters'
|
||||||
import { StatCard, PokemonCard, RuleBadges, StatusChangeModal, EndRunModal } from '../components'
|
import { StatCard, PokemonCard, RuleBadges, StatusChangeModal, EndRunModal } from '../components'
|
||||||
@@ -51,6 +51,7 @@ export function RunDashboard() {
|
|||||||
const createEncounter = useCreateEncounter(runIdNum)
|
const createEncounter = useCreateEncounter(runIdNum)
|
||||||
const updateEncounter = useUpdateEncounter(runIdNum)
|
const updateEncounter = useUpdateEncounter(runIdNum)
|
||||||
const updateRun = useUpdateRun(runIdNum)
|
const updateRun = useUpdateRun(runIdNum)
|
||||||
|
const { data: namingCategories } = useNamingCategories()
|
||||||
const [selectedEncounter, setSelectedEncounter] =
|
const [selectedEncounter, setSelectedEncounter] =
|
||||||
useState<EncounterDetail | null>(null)
|
useState<EncounterDetail | null>(null)
|
||||||
const [showEndRun, setShowEndRun] = useState(false)
|
const [showEndRun, setShowEndRun] = useState(false)
|
||||||
@@ -197,6 +198,37 @@ export function RunDashboard() {
|
|||||||
<RuleBadges rules={run.rules} />
|
<RuleBadges rules={run.rules} />
|
||||||
</div>
|
</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 */}
|
{/* Active Team */}
|
||||||
<div className="mb-6">
|
<div className="mb-6">
|
||||||
<div className="flex items-center justify-between mb-3">
|
<div className="flex items-center justify-between mb-3">
|
||||||
|
|||||||
@@ -90,6 +90,7 @@ export interface NuzlockeRun {
|
|||||||
status: RunStatus
|
status: RunStatus
|
||||||
rules: NuzlockeRules
|
rules: NuzlockeRules
|
||||||
hofEncounterIds: number[] | null
|
hofEncounterIds: number[] | null
|
||||||
|
namingScheme: string | null
|
||||||
startedAt: string
|
startedAt: string
|
||||||
completedAt: string | null
|
completedAt: string | null
|
||||||
}
|
}
|
||||||
@@ -132,6 +133,7 @@ export interface CreateRunInput {
|
|||||||
gameId: number
|
gameId: number
|
||||||
name: string
|
name: string
|
||||||
rules?: NuzlockeRules
|
rules?: NuzlockeRules
|
||||||
|
namingScheme?: string | null
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface UpdateRunInput {
|
export interface UpdateRunInput {
|
||||||
@@ -139,6 +141,7 @@ export interface UpdateRunInput {
|
|||||||
status?: RunStatus
|
status?: RunStatus
|
||||||
rules?: NuzlockeRules
|
rules?: NuzlockeRules
|
||||||
hofEncounterIds?: number[]
|
hofEncounterIds?: number[]
|
||||||
|
namingScheme?: string | null
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CreateEncounterInput {
|
export interface CreateEncounterInput {
|
||||||
|
|||||||
Reference in New Issue
Block a user