diff --git a/CHANGELOG.md b/CHANGELOG.md index b7fb3a1..0284776 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -44,6 +44,32 @@ All notable changes to this project are documented here. - **Reset donations is disabled** when there's no active town, instead of being hidden — keeps the Danger zone shape stable across states. - **Settings is a top-level route** (`/settings`) rather than nested under `/town/:townId/settings`. The settings page is a property of the app, not the town — and a user mid-reset-everything wouldn't have an active town to nest under. +### Added — Phase 4: TownManager drawer +- **`TownManager` component** (`src/components/TownManager.tsx`) — right-side drawer (420px) that mounts at the layout level in `App.tsx`, so it overlays every route (home, category tabs, settings) without z-index or `overflow-hidden` issues. Below 720px it renders as a bottom sheet. Contains: the list of towns with active-town indicator, inline row edit (name + hemisphere only), a `+ New town` form (name + game + hemisphere), and per-row delete with native `confirm()` guard. +- **`useUIStore`** (`src/lib/uiStore.ts`) — small non-persisted Zustand store for transient UI state (`townManagerOpen`, `townManagerForceCreate`). Exposes `openTownManager(forceCreate?)` and `closeTownManager()`. +- **Auto-open in create mode when no towns exist** — `App.tsx` opens the TownManager forced-create when `towns.length === 0`. The drawer hides its close button, ignores Esc, and ignores scrim clicks in this state — equivalent to the previous "required" behavior on `CreateTownModal`. +- **`GAME_LIST`** export in `src/lib/types.ts` — ordered array of `Game`, used by the create-town form's game selector. +- **TownManager styles** in `src/index.css` (`.ac-tm-scrim`, `.ac-tm-drawer`, `.ac-tm-row*`, `.ac-tm-form`, `.ac-tm-seg`, `.ac-tm-cta`, `.ac-tm-newform`, `.ac-tm-empty`, `ac-fade` / `ac-slide` / `ac-slide-up` keyframes). Bottom-sheet variant at `(max-width: 720px)`. + +### Changed — Phase 4 +- **`Sidebar`** — the Phase 2 bridge stubs are removed: the `window.prompt` switcher, the Edit / + New buttons inside the active-town card, and the inline NH/SH segmented toggle. The single `Switch town ›` button now opens the TownManager. Hemisphere is shown as a read-only `Hem. NH` / `Hem. SH` label (editing happens in the drawer). Sidebar no longer takes `onOpenCreateTown` / `onOpenEditTown` props. +- **`useAppStore.createTown`** signature is `(name, gameId, hemisphere?)` — `playerName` removed. **`useAppStore.updateTown`** signature is `(id, patch: TownPatch)` where `TownPatch` is `{ name?, hemisphere? }`. `gameId` is intentionally not part of the patch (Decision 1 — game is immutable post-create). +- **`Town` type** — `playerName: string` field removed (Decision 5). Existing values in localStorage are silently dropped on next write; no migration step required. +- **`downloadCSV` / `buildCSV`** no longer take a `playerName` argument; the "Player" row is removed from CSV exports. + +### Removed — Phase 4 +- **`CreateTownModal`** (`src/components/modals/CreateTownModal.tsx`) — replaced by TownManager's New Town form. +- **`EditTownModal`** (`src/components/modals/EditTownModal.tsx`) — replaced by TownManager's inline row edit. +- **`TownNameFields`** (`src/components/shared/TownNameFields.tsx`) — no remaining consumers. +- **v0.8.1 greyed-out-buttons stopgap** — no longer needed; the TownManager drawer mounts at the layout level and works on every route, resolving the issue the stopgap worked around. + +### Decisions — Phase 4 +- **Decision 1 honored** — the inline edit form has no game `` — game is read-only post-create. **Shell layout (v0.9 Phase 2):** `Sidebar` (280px left, sticky) + `
` in CSS grid `280px 1fr`, max-width 1440px centered. Below 980px sidebar stacks above main. `MuseumHeader`, `TabBar`, `TownSwitcher` retired — nav lives in the sidebar. ### File Structure @@ -69,9 +69,8 @@ src/ MonthGrid.tsx # 12-cell month availability grid SearchBar.tsx # Per-tab inline search input modals/ - CreateTownModal.tsx # New town form with game selector - EditTownModal.tsx # Rename town form (gameId immutable) DetailModal.tsx # Item detail sheet + TownManager.tsx # Right-side drawer for switch/edit/create/delete towns (v0.9 Phase 4) views/ AnalyticsView.tsx # Charts + stats tab content ActivityFeed.tsx # Recent donations list @@ -170,7 +169,7 @@ See `.claude/rules/vercel.md` for full deployment rules. Key points: - **issue #26** — Art tab persistent label — **fixed in v0.8.2 (PR #57)**; `setSelected(null)` added to tab-change `useEffect` - **issue #31** — Create-town edge case; low priority, open - **Sea creatures tab** — **shipped in v0.8.2 (PR #44, Closes #56)**; Sea tab visible for ACNL and ACNH towns -- **Edit/new-town buttons greyed out on Fish, Bugs, Fossils tabs** — intentional v0.8.1 stopgap. Modals (EditTownModal, CreateTownModal) are mounted in ACCanvas, which sits below the router layout; on museum category tabs the modal renders fine but overlapping scroll context causes visual issues. Buttons show `opacity: 0.4` + tooltip directing users to Home/Search/Recent Donations instead. Proper fix (lift modals to layout level) deferred to v0.9 UI revamp. +- **Edit/new-town buttons greyed out on Fish, Bugs, Fossils tabs** — **resolved in v0.9 Phase 4**. The `TownManager` drawer mounts at the App layout level and renders correctly on every route, so the overflow/z-index issues that motivated the stopgap no longer apply. ## ACCanvas.tsx diff --git a/src/App.tsx b/src/App.tsx index acbf64e..93c2802 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,10 +1,13 @@ +import { useEffect } from 'react'; import { Navigate, Route, Routes } from 'react-router-dom'; import { Analytics } from '@vercel/analytics/react'; import ACCanvas from './components/ACCanvas'; import SettingsRoute from './components/SettingsRoute'; +import { TownManager } from './components/TownManager'; import { useHydration } from './hooks/useHydration'; import ErrorBoundary from './components/ErrorBoundary'; import { useAppStore } from './lib/store'; +import { useUIStore } from './lib/uiStore'; function RootRedirect() { const towns = useAppStore(s => s.towns); @@ -20,6 +23,16 @@ function RootRedirect() { function App() { const hydrated = useHydration(); + const towns = useAppStore(s => s.towns); + const openTownManager = useUIStore(s => s.openTownManager); + const townManagerOpen = useUIStore(s => s.townManagerOpen); + + // Force the TownManager open in create mode whenever there are no towns. + useEffect(() => { + if (hydrated && towns.length === 0 && !townManagerOpen) { + openTownManager(true); + } + }, [hydrated, towns.length, townManagerOpen, openTownManager]); if (!hydrated) { return ( @@ -54,6 +67,7 @@ function App() { } /> } /> + {typeof window !== 'undefined' && (
(null); - const [showCreateTown, setShowCreateTown] = useState(false); - const [showEditTown, setShowEditTown] = useState(false); const [expandedId, setExpandedId] = useState(null); const { data, loading, loadError, reload } = useMuseumData( @@ -170,13 +166,7 @@ export default function ACCanvas() { function handleExport() { if (!activeTown) return; - downloadCSV( - data, - activeTownDonated, - activeTownDonatedAt, - activeTown.name, - activeTown.playerName - ); + downloadCSV(data, activeTownDonated, activeTownDonatedAt, activeTown.name); } function handleTabChange(tab: ViewId) { @@ -216,8 +206,6 @@ export default function ACCanvas() { townId={activeTownId} data={data} catCounts={catCounts} - onOpenCreateTown={() => setShowCreateTown(true)} - onOpenEditTown={() => setShowEditTown(true)} onExport={handleExport} /> )} @@ -365,18 +353,6 @@ export default function ACCanvas() {
- setShowCreateTown(false)} - /> - - setShowEditTown(false)} - /> - {selected && !noTowns && ( setShowCreateTown(true)} - onOpenEditTown={() => setShowEditTown(true)} onExport={handleExport} /> )}
- - setShowCreateTown(false)} - /> - setShowEditTown(false)} - /> ); } diff --git a/src/components/Sidebar.tsx b/src/components/Sidebar.tsx index 6de35b0..fa97657 100644 --- a/src/components/Sidebar.tsx +++ b/src/components/Sidebar.tsx @@ -1,6 +1,6 @@ import { NavLink, useNavigate } from 'react-router-dom'; import { useAppStore } from '../lib/store'; -import type { Hemisphere } from '../lib/store'; +import { useUIStore } from '../lib/uiStore'; import { GAMES } from '../lib/types'; import type { CategoryId, GameId } from '../lib/types'; import type { ViewId, AllData } from '../lib/viewTypes'; @@ -21,21 +21,16 @@ export function Sidebar({ townId, data, catCounts, - onOpenCreateTown, - onOpenEditTown, onExport, }: { townId: string; data: AllData; catCounts: Stats; - onOpenCreateTown: () => void; - onOpenEditTown: () => void; onExport: () => void; }) { const navigate = useNavigate(); const towns = useAppStore(s => s.towns); - const setActiveTown = useAppStore(s => s.setActiveTown); - const setTownHemisphere = useAppStore(s => s.setTownHemisphere); + const openTownManager = useUIStore(s => s.openTownManager); const activeTown = towns.find(t => t.id === townId); const gameId = activeTown?.gameId ?? 'ACGCN'; @@ -46,35 +41,6 @@ export function Sidebar({ return `/town/${townId}/${view}`; } - function handleSwitchTown() { - // Phase 2 stub: cycles to the next town if one exists, otherwise opens - // CreateTownModal. Full TownManager drawer ships in Phase 4. - const others = towns.filter(t => t.id !== townId); - if (others.length === 0) { - onOpenCreateTown(); - return; - } - if (others.length === 1) { - setActiveTown(others[0].id); - navigate(`/town/${others[0].id}/home`); - return; - } - // Multiple other towns — surface a quick prompt picker (temporary). - const labels = others - .map((t, i) => `${i + 1}. ${t.name} (${GAMES[t.gameId].shortName})`) - .join('\n'); - const pick = window.prompt( - `Switch to which town?\n${labels}\n\nEnter a number, or cancel.` - ); - if (!pick) return; - const idx = Number.parseInt(pick, 10) - 1; - const choice = others[idx]; - if (choice) { - setActiveTown(choice.id); - navigate(`/town/${choice.id}/home`); - } - } - return (