Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,20 @@ All notable changes to this project are documented here.

## [Unreleased] — v0.9.0-beta (in progress)

### Added — Phase 7: CategoryTab sectioning
- **`CategoryTab` component** (`src/components/CategoryTab.tsx`) — replaces the inline category render in `ACCanvas`. Groups items into four sections (Leaving this month / Available now / Out of season / Already donated), each with an eyebrow header and item count. Empty groups are hidden. December→January wrap is handled via `next = currentMonth === 12 ? 1 : currentMonth + 1`. Donated items always land in "Already donated" regardless of season. Categories without month data (fossils, art) treat all non-donated items as "Available now". Owns its own `expandedId` state — only one row open at a time per tab — and reacts to `highlightId` by opening the matching row before ACCanvas's scroll-to fires.
- **Category page header** — Fraunces 44 `<em>{donated}</em> of {total} {category}` title and right-aligned meta line ("X% complete" + "Showing availability for {month}" for seasonal categories). Stats card-style header sits above the per-tab search bar.
- **Phase 7 CSS** appended to `src/index.css` (`.ac-category`, `.ac-category-head`, `.ac-category-title`, `.ac-category-meta`, `.ac-group`, `.ac-group-head`, `.ac-group-title`, `.ac-group-count`, plus `.ac-group-warn` / `.ac-group-accent` / `.ac-group-muted` / `.ac-group-done` tone modifiers). Header collapses to single column ≤700px and the category title shrinks to 32px.

### Removed — Phase 7
- Inline category render in `ACCanvas` (`CategoryProgress` import + flat `.ac-list` map). The progress display moves into the new category header. `CategoryProgress` itself remains in the codebase for now and is slated for removal during the Phase 9 stats rebuild per the v0.9 retirement list.

### Decisions — Phase 7
- **Section ordering is fixed and ungrouped categories collapse cleanly.** Fossils and art have no month data, so for those tabs only the "Available now" and "Already donated" groups can ever appear. The ordering still reads naturally; we don't special-case the layout for non-seasonal categories.
- **Per-tab search lives inside CategoryTab, not above it.** Keeping the search bar inside the new component lets the section grouping recompute on filter without re-flowing the page header. The header always shows totals for the full category, not the filtered subset, so users can see overall progress while narrowing the list.
- **Art keeps its bottom-sheet `DetailModal`.** The plan's "row click toggles inline expand" rule applies to fish/bugs/fossils; art was already on a different pattern in v0.8 and the v0.9 design preserves that. CategoryTab routes art clicks to the existing `onItemSelect` callback in `ACCanvas` and renders no inline panel for that category.
- **`ItemExpandPanel` is rendered as a sibling of `CollectibleRow` inside the same `.ac-list`.** This preserves the existing CSS that ties `.ac-row` divider + `.ac-row-pulse` keyframe to the row container without restructuring the row primitive (locked from Phase 5).

### Added — Phase 6: HomeTab + ProgressMeter
- **`ProgressMeter` component** (`src/components/ProgressMeter.tsx`) — segmented donation progress bar. 4 segments (fish/bugs/fossils/art) for ACGCN/ACWW/ACCF; 5 segments (adds sea) for ACNL/ACNH. Each segment uses its category-tinted Meadow chip token (`--chip-fish`/`--chip-bugs`/`--chip-fossils`/`--chip-art`/`--chip-sea`) and exposes a per-segment aria-label like "Fish: 12 of 40 donated". Pure helper `segmentsForGame` extracted to `src/components/progressMeterUtils.ts` and unit-tested.
- **`HomeTab` rebuilt** (`src/components/HomeTab.tsx`) — new structure per v0.9 design: hero stat with italic Fraunces accent number ("X creatures still to donate this month"), warn-italic aside ("N leaving soon"), `ProgressMeter` directly underneath, 12-cell month strip with current-month highlight, "Leaving end of {month}" warn-toned shelf, "Just arrived" accent-toned shelf, and a "Latest donations" card. Sea creatures included in shelves and progress for ACNL/ACNH towns. Each shelf card / latest-donations row click fires `jumpTo(category, id)` which sets the active tab and `highlightId` so the matching row scrolls into view and pulses (Decision 10). Hero falls back to "X of Y donated" when the active game has no seasonal categories.
Expand Down
1 change: 1 addition & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ src/
ProgressMeter.tsx # v0.9 Phase 6: segmented progress bar (4 or 5 segments
# gated by gameId; sea segment for ACNL/ACNH).
progressMeterUtils.ts # Pure helper segmentsForGame (unit-tested).
CategoryTab.tsx # v0.9 Phase 7: sectioned category page (Leaving / Available / Out of season / Already donated)
CollectibleRow.tsx # Single item row with donate toggle; shows chevron + rounded-top when expanded
ItemExpandPanel.tsx # Inline accordion panel shown below CollectibleRow for fish/bugs/fossils
Sidebar.tsx # v0.9 Phase 2: 280px left sidebar — brand, active town card, NavLink nav with counts, footer (replaces MuseumHeader/TabBar/TownSwitcher)
Expand Down
2 changes: 1 addition & 1 deletion docs/v0.9-plan.md
Original file line number Diff line number Diff line change
Expand Up @@ -467,7 +467,7 @@ Parallel work is allowed within a phase where components are independent. The ph

---

### Phase 7 — CategoryTab Sectioning
### Phase 7 — CategoryTab Sectioning

**Branch:** `feature/v09-phase-7-sections`
**Goal:** Group category items into 4 sections instead of a flat A–Z list.
Expand Down
102 changes: 23 additions & 79 deletions src/components/ACCanvas.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
import React from 'react';
import { useState, useMemo, useEffect } from 'react';
import { useParams, useNavigate } from 'react-router-dom';
import { useAppStore } from '../lib/store';
import { CATEGORY_META } from '../lib/categoryMeta';
import { downloadCSV } from '../lib/csvExport';
import { filterByQuery, globalFilter, type AnyItem } from '../lib/utils';
import { globalFilter, type AnyItem } from '../lib/utils';
import type { CategoryId } from '../lib/types';
import type { AppErrorKind } from '../lib/types';
import type { ViewId } from '../lib/viewTypes';
Expand All @@ -14,10 +13,7 @@ import ErrorBanner from './ErrorBanner';
import ErrorState from './ErrorState';

import { Sidebar } from './Sidebar';
import { CollectibleRow } from './CollectibleRow';
import { ItemExpandPanel } from './ItemExpandPanel';
import { CategoryProgress } from './shared/CategoryProgress';
import { SearchBar } from './shared/SearchBar';
import { CategoryTab } from './CategoryTab';
import { EmptyState } from './shared/EmptyState';

import { DetailModal } from './modals/DetailModal';
Expand Down Expand Up @@ -96,7 +92,6 @@ export default function ACCanvas() {
item: AnyItem;
category: CategoryId;
} | null>(null);
const [expandedId, setExpandedId] = useState<string | null>(null);
const [highlightId, setHighlightId] = useState<string | null>(null);

const { data, loading, loadError, reload } = useMuseumData(
Expand Down Expand Up @@ -135,20 +130,18 @@ export default function ACCanvas() {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [data.sea_creatures.length, loading, activeTab]);

// Reset per-tab search query, expanded item, and selected detail on tab changes
// Reset per-tab search query and selected detail on tab changes
useEffect(() => {
setQuery('');
setExpandedId(null);
setSelected(null);
}, [activeTab]);

// Scroll-to + highlight on jump (Decision 10).
// When highlightId changes, expand the matching row, scroll it into view,
// and let the .ac-row-pulse keyframe play. Clear after the animation duration
// so re-jumping to the same id retriggers the pulse.
// CategoryTab handles expanding the matching row when highlightId changes;
// here we scroll the row into view and clear after the animation duration so
// re-jumping to the same id retriggers the pulse.
useEffect(() => {
if (!highlightId) return;
setExpandedId(highlightId);
const raf = requestAnimationFrame(() => {
const el = document.querySelector(
`[data-row-id="${CSS.escape(highlightId)}"]`
Expand All @@ -170,16 +163,11 @@ export default function ACCanvas() {
? activeTab
: null;

const activeItems = useMemo(
const activeItems = useMemo<AnyItem[]>(
() => (activeCat ? (data[activeCat] as AnyItem[]) : []),
[activeCat, data]
);

const filtered = useMemo(() => {
if (!activeCat) return [];
return filterByQuery(activeItems, activeCat, query);
}, [activeItems, activeCat, query]);

const globalResults = useMemo(() => {
if (!globalQuery.trim()) return null;
return globalFilter(data as Record<CategoryId, AnyItem[]>, globalQuery);
Expand Down Expand Up @@ -300,66 +288,22 @@ export default function ACCanvas() {
/>
</>
) : (
<>
<CategoryProgress
donated={catCounts[activeCat!]}
total={activeItems.length}
label={catLabel}
/>
<SearchBar
query={query}
setQuery={setQuery}
placeholder={`Search ${catLabel.toLowerCase()}…`}
/>
<div className="ac-list">
{filtered.map(item => (
<React.Fragment key={item.id}>
<CollectibleRow
item={item}
category={activeCat!}
checked={!!activeTownDonated[item.id]}
onClick={() => {
if (activeCat === 'art') {
setSelected({ item, category: activeCat! });
} else {
setExpandedId(prev =>
prev === item.id ? null : item.id
);
}
}}
expanded={
activeCat !== 'art'
? expandedId === item.id
: undefined
}
highlighted={highlightId === item.id}
hemisphere={activeTown?.hemisphere ?? 'NH'}
currentMonth={new Date().getMonth() + 1}
/>
{activeCat !== 'art' && expandedId === item.id && (
<ItemExpandPanel
item={item}
category={activeCat!}
checked={!!activeTownDonated[item.id]}
donatedAt={activeTownDonatedAt[item.id]}
onToggle={() => toggle(item.id)}
hemisphere={activeTown?.hemisphere ?? 'NH'}
currentMonth={new Date().getMonth() + 1}
/>
)}
</React.Fragment>
))}
{filtered.length === 0 && (
<EmptyState
message={
query
? `No ${catLabel.toLowerCase()} match "${query}".`
: `No ${catLabel.toLowerCase()} found.`
}
/>
)}
</div>
</>
<CategoryTab
category={activeCat!}
items={activeItems}
donated={activeTownDonated}
donatedAt={activeTownDonatedAt}
hemisphere={activeTown?.hemisphere ?? 'NH'}
currentMonth={new Date().getMonth() + 1}
query={query}
setQuery={setQuery}
highlightId={highlightId}
onItemSelect={item =>
setSelected({ item, category: activeCat! })
}
onToggle={id => toggle(id)}
catLabel={catLabel}
/>
)}
</>
)}
Expand Down
Loading
Loading