diff --git a/web/frontend/app/features/breweries/BREWERY_DETAIL_HANDOFF.md b/web/frontend/app/features/breweries/BREWERY_DETAIL_HANDOFF.md deleted file mode 100644 index 87fbec95..00000000 --- a/web/frontend/app/features/breweries/BREWERY_DETAIL_HANDOFF.md +++ /dev/null @@ -1,55 +0,0 @@ -# Brewery detail — data requirements - -`/breweries/:id` (`app/features/breweries/routes/brewery-detail.tsx`) is built and -live. This document inventories what's real vs. filler, following the same -convention as `BREWERY_INDEX_HANDOFF.md`. - -## Status legend - -- ✅ **Available** — real data, already wired up. -- ❌ **Missing** — no backend support at all; page currently shows filler or - local-only client state. - -## Header, location, dates - -| Field | Status | Current source | -| ------------------------------------------- | ------ | ------------------------- | -| Name, description, address, coordinates | ✅ | `GET /api/brewery/{id}` (`BreweryDto`) | -| Created / updated dates | ✅ | `BreweryDto` | -| Founded year, brewery type, website | ❌ | `FILLER_BREWERY_META` — `BreweryPost` has no `FoundedAt`, `Type`, or `Website` column | - -## Beers, style breakdown - -| Field | Status | Current source | Gap | -| --------------------------- | ------ | --------------------- | -------------------------------------------------------------------------------------- | -| Beers belonging to a brewery | ❌ | `FILLER_BEERS` | `Features.Beers` is an empty stub project — no query/controller exposes `BeerPost` at all, despite the entity already having a `BrewedById` FK. | -| Beer style breakdown | ❌ | Derived client-side from `FILLER_BEERS` | Blocked on the same gap; would otherwise be an aggregate query over a brewery's beers. | - -## Likes, ratings, comments - -| Field | Status | Current source | Gap | -| ------------------------------ | ------ | ------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------ | -| Brewery like/unlike + count | ❌ | Local component state only | No like concept exists anywhere in the schema. Needs a new join table (composite `BreweryPostId`/`UserAccountId`, similar shape to `UserFollow`). | -| Your rating (1–5) + average | ❌ | Local component state only | No brewery rating table/entity/endpoint exists. | -| Comments (list + post) | ❌ | `FILLER_COMMENTS`, local state | No `BreweryPostComment` table exists. `BeerPostComment` is a close schema precedent (comment + 1–5 `CHECK`-constrained rating + FKs) but is itself unused by any code and targets `BeerPost`, not `BreweryPost`. | -| Per-comment like toggle | ❌ | Local component state only | No "like a comment" concept exists anywhere in the schema — would need a new join table alongside a new `BreweryPostComment` table. | - -## Summary of backend work to close the gaps - -1. Add `FoundedAt`, `Type`, `Website` columns to `BreweryPost` (+ `BreweryDto`). -2. Stand up `Features.Beers`: a query (and controller route) returning a - brewery's beers, following the `Features.Breweries` vertical-slice pattern. -3. New `BreweryPostLike` join table + MediatR command/query + controller - endpoints (toggle, read count/whether current user liked) — public GET, - JWT-required toggle, mirroring `BreweryController`'s - `[AllowAnonymous]`-on-reads pattern. -4. New `BreweryPostRating` table (1–5, `CHECK`-constrained like - `BeerPostComment.Rating`) + aggregate query for average/count. -5. New `BreweryPostComment` table (mirroring `BeerPostComment.sql`, FK'd to - `BreweryPost`) + a new `BreweryPostCommentLike` join table for per-comment - likes, plus the command/query/controller layer for both. - -Once these land, swap `FILLER_BEERS` / `FILLER_COMMENTS` / `FILLER_BREWERY_META` -(`utils/filler-brewery-detail.ts`) and the local `useState` in -`brewery-detail.tsx` for loader data and `authorizedRequest`-based mutations -(see `auth.server.ts`). diff --git a/web/frontend/app/features/breweries/BREWERY_HANDOFF.md b/web/frontend/app/features/breweries/BREWERY_HANDOFF.md new file mode 100644 index 00000000..5f9c45ba --- /dev/null +++ b/web/frontend/app/features/breweries/BREWERY_HANDOFF.md @@ -0,0 +1,101 @@ +# Brewery feature: data requirements + +Covers `/breweries` (`routes/breweries.tsx`) and `/breweries/:id` +(`routes/brewery-detail.tsx`). Both routes are built and live. This document +lists what's real vs. filler, and what backend work closes each gap. + +## Status legend + +- ✅ **Available**: real data, wired up. +- ⚠️ **Partial**: endpoint exists but is missing a field the design needs. +- ❌ **Missing**: no backend support; page shows filler or local-only state. + +## Breweries index (`/breweries`) + +### Featured brewery + +| Field | Status | Source | Gap | +| ------------------------------------ | ------ | --------------------------------- | ------------------------------------------------------------------------------------------------------ | +| Name, description, address, location | ✅ | `GET /api/brewery` (`BreweryDto`) | | +| "Editorially featured" selection | ❌ | none (defaults to newest brewery) | No `IsFeatured` flag or rotation concept. Needs a field + admin toggle, or a scheduled rotation query. | +| Founded year | ❌ | none | No founding-date column (distinct from `CreatedAt`, which is post-creation time). | +| Beers listed count | ❌ | none | Blocked on the Beers gap below. | +| House style | ❌ | none | No style/tag field, and no aggregate over a brewery's beers' styles. | + +### Breweries near you + +| Field | Status | Source | Gap | +| ------------------------------------------------------- | ------ | --------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------- | +| Nearby breweries by coordinates + radius | ✅ | `GET /api/brewery/locations/nearby?latitude&longitude&rangeInMetres` | | +| Distance from search center | ✅ | `SimplifiedBreweryDto.DistanceMetres`, computed server-side via `geography::STDistance` | | +| One-line note per brewery | ❌ | none | `SimplifiedBreweryDto` has no description-like field. Needs a tagline field, or a truncated `Description`. | +| User's current location | ✅ | Browser Geolocation API | | +| Fallback region (before permission granted / on denial) | ⚠️ | Falls back to the featured brewery's coordinates | Works, but isn't a deliberate default. A real deployment wants a configured default (e.g. per-tenant HQ city). | +| Manual location entry ("Change location") | ❌ | Re-requests browser geolocation only | No geocoding/place-search endpoint to resolve a typed address to coordinates. | +| Search radius | ✅ | Client-side state (10–150 km slider), passed as `rangeInMetres` | | + +### Recently added + +✅ Fully real: `GET /api/brewery`, already sorted newest-first. No gaps. + +### Directory CTA + +| Field | Status | Source | Gap | +| --------------------------------------- | ------ | --------------------- | ------------------------------------------------------------------------------------------------------------------------------ | +| Total partner brewery count | ❌ | Filler number (`128`) | No count endpoint. Needs `GET /api/brewery/count` rather than fetching the full list to measure it. | +| Filter by country / region / beer style | ❌ | Copy only | Describes the future directory page's capability, not a data need of this page. Implies the directory will need filter params. | + +## Brewery detail (`/breweries/:id`) + +### Header, location, dates + +| Field | Status | Source | +| --------------------------------------- | ------ | ------------------------------------------------------------------------------------ | +| Name, description, address, coordinates | ✅ | `GET /api/brewery/{id}` (`BreweryDto`) | +| Created / updated dates | ✅ | `BreweryDto` | +| Founded year, brewery type, website | ❌ | `FILLER_BREWERY_META`: `BreweryPost` has no `FoundedAt`, `Type`, or `Website` column | + +### Beers, style breakdown + +| Field | Status | Source | Gap | +| ---------------------------- | ------ | --------------------------------------- | ----------------------------------------------------------------------------- | +| Beers belonging to a brewery | ❌ | `FILLER_BEERS` | See "Beers feature is empty" below. | +| Beer style breakdown | ❌ | Derived client-side from `FILLER_BEERS` | Blocked on the same gap; otherwise an aggregate query over a brewery's beers. | + +### Likes, ratings, comments + +| Field | Status | Source | Gap | +| --------------------------- | ------ | ------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Brewery like/unlike + count | ❌ | Local component state | No like concept in the schema. Needs a join table (composite `BreweryPostId`/`UserAccountId`, like `UserFollow`). | +| Your rating (1–5) + average | ❌ | Local component state | No brewery rating table/endpoint. | +| Comments (list + post) | ❌ | `FILLER_COMMENTS`, local state | No `BreweryPostComment` table. `BeerPostComment` is a close precedent (comment + 1–5 `CHECK`-constrained rating + FKs) but is unused and targets `BeerPost`, not `BreweryPost`. | +| Per-comment like toggle | ❌ | Local component state | No "like a comment" concept; needs a join table alongside a new `BreweryPostComment` table. | + +## Cross-cutting: the Beers feature is empty + +`Features.Beers` (backend) has a `.csproj` and nothing else: no query +handlers, no controller, no DTOs. `BeerPost` exists as a domain entity (with +`BrewedById` linking to `BreweryPost`) but nothing exposes it over the API. +This blocks: + +- "Beers listed" count and "House style" on the index page's featured card. +- The beer list and style breakdown on the detail page. +- Any future beer-style filter on the directory. + +## Summary of backend work + +1. `IsFeatured` flag (or rotation query) on `BreweryPost`. +2. `FoundedAt`/`FoundedYear`, `Type`, `Website` columns on `BreweryPost` (+ `BreweryDto`). +3. A tagline/note field on `BreweryPost`, surfaced through `SimplifiedBreweryDto`. +4. Stand up `Features.Beers`: a query + controller route for a brewery's beers, following the `Features.Breweries` vertical-slice pattern. Unblocks beer counts, style breakdown, and style filters. +5. `GET /api/brewery/count`. +6. A geocoding/place-search endpoint for manual location entry. +7. `BreweryPostLike` join table + command/query/controller (toggle, read count/liked-by-current-user); public GET, JWT-required toggle, mirroring `BreweryController`'s `[AllowAnonymous]`-on-reads pattern. +8. `BreweryPostRating` table (1–5, `CHECK`-constrained like `BeerPostComment.Rating`) + aggregate query for average/count. +9. `BreweryPostComment` table (mirroring `BeerPostComment.sql`, FK'd to `BreweryPost`) + `BreweryPostCommentLike` join table, plus command/query/controller for both. +10. (Product decision, not code) a configured default region to fall back to instead of the featured brewery's coordinates. + +Once these land, swap `FILLER_BEERS` / `FILLER_COMMENTS` / `FILLER_BREWERY_META` +(`utils/filler-brewery-detail.ts`) and the local `useState` in +`brewery-detail.tsx` for loader data and `authorizedRequest`-based mutations +(see `auth.server.ts`). diff --git a/web/frontend/app/features/breweries/BREWERY_INDEX_HANDOFF.md b/web/frontend/app/features/breweries/BREWERY_INDEX_HANDOFF.md deleted file mode 100644 index 2f30ce85..00000000 --- a/web/frontend/app/features/breweries/BREWERY_INDEX_HANDOFF.md +++ /dev/null @@ -1,76 +0,0 @@ -# Brewery index — data requirements - -`/breweries` (`app/features/breweries/routes/breweries.tsx`) is built and live. -This document inventories every piece of data the page needs, section by -section: what's already wired to real data, what's filler pending backend work, -and what the backend would need to add to close each gap. - -## Status legend - -- ✅ **Available** — real data, already wired up. -- ⚠️ **Partial** — an endpoint exists but is missing a field the design needs. -- ❌ **Missing** — no backend support at all; page currently shows filler. - -## 1. Page header - -Static copy only. No data. - -## 2. Featured brewery - -| Field | Status | Current source | Gap | -| ------------------------------------ | ------ | ---------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Name, description, address, location | ✅ | `GET /api/brewery` (`BreweryDto`) | — | -| "Editorially featured" selection | ❌ | none — defaults to the most recently created brewery | No `IsFeatured` flag or rotation concept anywhere in `BreweryPost`. Needs a field + an admin toggle, or a scheduled rotation query, to back the "THIS WEEK" cadence the badge implies. | -| Founded year | ❌ | none | `BreweryPost` has no founding date. Would need a new column (distinct from `CreatedAt`, which is post-creation time, not the brewery's actual founding). | -| Beers listed count | ❌ | none | No beer-count aggregate exists. Blocked on the beers gap below. | -| House style | ❌ | none | No style/tag field on `BreweryPost`, and no aggregate over its beers' styles either. | - -## 3. Breweries near you - -| Field | Status | Current source | Gap | -| ------------------------------------------------------- | ------ | ---------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Nearby breweries by coordinates + radius | ✅ | `GET /api/brewery/locations/nearby?latitude&longitude&rangeInMetres` | — | -| Distance from search center | ✅ | `SimplifiedBreweryDto.DistanceMetres`, computed server-side via `geography::STDistance` | — | -| One-line note / tagline per brewery | ❌ | none | `SimplifiedBreweryDto` (used by both `/locations` and `/locations/nearby`) has no description-like field. List and map-popup text use filler. Needs either a short tagline field or a truncated `Description` added to that DTO. | -| User's current location | ✅ | Browser Geolocation API | — | -| Fallback region (before permission granted / on denial) | ⚠️ | Falls back to the featured brewery's own coordinates as a proxy center | Works, but isn't a deliberate "default region." A real deployment likely wants a configured default (e.g. per-tenant HQ city) rather than borrowing the featured brewery's location. | -| Manual location entry ("Change location") | ❌ | Re-requests browser geolocation only | No geocoding/place-search endpoint exists to resolve a typed address or city name to coordinates. | -| Search radius | ✅ | Client-side state (10–150 km slider), passed straight through to `rangeInMetres` | — | - -## 4. Recently added - -| Field | Status | Current source | Gap | -| --------------------------------------- | ------ | ------------------------------------------------ | --- | -| Newest breweries, name/city/description | ✅ | `GET /api/brewery` (already sorted newest-first) | — | - -No gaps — this section is fully real. - -## 5. Directory CTA - -| Field | Status | Current source | Gap | -| --------------------------------------- | ------ | --------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Total partner brewery count | ❌ | Filler number (`128`) | No lightweight count endpoint. Needs something like `GET /api/brewery/count` rather than fetching the full unbounded list just to measure it. | -| Filter by country / region / beer style | ❌ | Copy only — not implemented | This is CTA copy describing the destination directory page's future capability, not a data need of this page itself. Tracked here because it implies the directory page will eventually need country/region/style filter params. | - -## Cross-cutting: the Beers feature is empty - -`Features.Beers` (backend) has a `.csproj` and nothing else — no query handlers, -no controller, no DTOs. `BeerPost` exists as a domain entity (with `BrewedById` -linking to `BreweryPost`) but nothing exposes it over the API. This blocks: - -- "Beers listed" count on the featured card. -- "House style" (would likely aggregate from a brewery's beers' `BeerStyle`). -- Any future beer-style filter on the directory. - -## Summary of backend work to fully close the gaps - -1. `IsFeatured` flag (or equivalent rotation query) on `BreweryPost`. -2. `FoundedYear` (or similar) column on `BreweryPost`. -3. A short tagline/note field on `BreweryPost`, surfaced through - `SimplifiedBreweryDto`. -4. Stand up `Features.Beers` — at minimum a query to count beers per brewery and - read their styles. -5. `GET /api/brewery/count`. -6. A geocoding/place-search endpoint for manual location entry. -7. (Product decision, not code) a configured default region to fall back to - instead of the featured brewery's coordinates. diff --git a/web/frontend/app/features/breweries/components/FeaturedBreweryCard.tsx b/web/frontend/app/features/breweries/components/FeaturedBreweryCard.tsx index 93a888f0..39ea8297 100644 --- a/web/frontend/app/features/breweries/components/FeaturedBreweryCard.tsx +++ b/web/frontend/app/features/breweries/components/FeaturedBreweryCard.tsx @@ -7,7 +7,7 @@ interface FeaturedBreweryCardProps { onShowOnMap: () => void; } -// see BREWERY_INDEX_HANDOFF.md +// see BREWERY_HANDOFF.md const FILLER_STATS = [ { label: 'Founded', value: '2014' }, { label: 'Beers listed', value: '18' }, diff --git a/web/frontend/app/features/breweries/components/NearbyBreweriesSection.tsx b/web/frontend/app/features/breweries/components/NearbyBreweriesSection.tsx index 0c03da15..78e9023f 100644 --- a/web/frontend/app/features/breweries/components/NearbyBreweriesSection.tsx +++ b/web/frontend/app/features/breweries/components/NearbyBreweriesSection.tsx @@ -37,7 +37,7 @@ interface NearbyBreweriesSectionProps { onSelect: (id: string) => void; } -// Notes aren't tracked on brewery locations yet - see BREWERY_INDEX_HANDOFF.md. +// Notes aren't tracked on brewery locations yet - see BREWERY_HANDOFF.md. const FILLER_NOTE = 'A stop worth the detour.'; const cardClassName = (selected: boolean) => diff --git a/web/frontend/app/features/breweries/routes/breweries.tsx b/web/frontend/app/features/breweries/routes/breweries.tsx index 15332a50..681731ac 100644 --- a/web/frontend/app/features/breweries/routes/breweries.tsx +++ b/web/frontend/app/features/breweries/routes/breweries.tsx @@ -7,7 +7,7 @@ import RecentBreweries from '../components/RecentBreweries'; import { getBreweries } from '../breweries.server'; import type { Route } from './+types/breweries'; -// No count endpoint exists yet - see BREWERY_INDEX_HANDOFF.md. +// No count endpoint exists yet - see BREWERY_HANDOFF.md. const FILLER_TOTAL_BREWERY_COUNT = 128; export const meta = ({}: Route.MetaArgs) => { @@ -15,7 +15,7 @@ export const meta = ({}: Route.MetaArgs) => { }; export const loader = async () => { - // "Featured" is just the most recently posted brewery for now - see BREWERY_INDEX_HANDOFF.md. + // "Featured" is just the most recently posted brewery for now - see BREWERY_HANDOFF.md. const [featured, ...recent] = await getBreweries(4, 0); return { featured: featured ?? null, recent }; }; diff --git a/web/frontend/app/features/breweries/utils/filler-brewery-detail.ts b/web/frontend/app/features/breweries/utils/filler-brewery-detail.ts index 6ab089a0..554900a3 100644 --- a/web/frontend/app/features/breweries/utils/filler-brewery-detail.ts +++ b/web/frontend/app/features/breweries/utils/filler-brewery-detail.ts @@ -1,4 +1,4 @@ -// see BREWERY_DETAIL_HANDOFF.md — filler data pending backend support for +// see BREWERY_HANDOFF.md — filler data pending backend support for // beers-by-brewery, likes, ratings, comments, and brewery founded/type/website fields. export interface FillerBeer { diff --git a/web/frontend/stories/BeerListCard.stories.tsx b/web/frontend/stories/BeerListCard.stories.tsx index 1358afeb..6f8217a5 100644 --- a/web/frontend/stories/BeerListCard.stories.tsx +++ b/web/frontend/stories/BeerListCard.stories.tsx @@ -3,7 +3,7 @@ import { expect, within } from 'storybook/test'; import BeerListCard from '../app/features/breweries/components/BeerListCard'; import { FILLER_BEERS } from '../app/features/breweries/utils/filler-brewery-detail'; -const beerListCardDescription = `A brewery's beer list on the detail page: name/description, style badge, ABV, and star rating per row. Currently filler pending a beers-by-brewery endpoint — see BREWERY_DETAIL_HANDOFF.md.`; +const beerListCardDescription = `A brewery's beer list on the detail page: name/description, style badge, ABV, and star rating per row. Currently filler pending a beers-by-brewery endpoint — see BREWERY_HANDOFF.md.`; const meta = { title: 'Breweries/BeerListCard', diff --git a/web/frontend/stories/BreweryDetailsCard.stories.tsx b/web/frontend/stories/BreweryDetailsCard.stories.tsx index b3bca892..1fce103c 100644 --- a/web/frontend/stories/BreweryDetailsCard.stories.tsx +++ b/web/frontend/stories/BreweryDetailsCard.stories.tsx @@ -2,7 +2,7 @@ import type { Meta, StoryObj } from '@storybook/react-vite'; import { expect, within } from 'storybook/test'; import BreweryDetailsCard from '../app/features/breweries/components/BreweryDetailsCard'; -const breweryDetailsCardDescription = `Sidebar "Details" card on the brewery detail page: founded year, brewery type, and beer count. Founded year and type are currently filler pending backend support — see BREWERY_DETAIL_HANDOFF.md.`; +const breweryDetailsCardDescription = `Sidebar "Details" card on the brewery detail page: founded year, brewery type, and beer count. Founded year and type are currently filler pending backend support — see BREWERY_HANDOFF.md.`; const meta = { title: 'Breweries/BreweryDetailsCard', diff --git a/web/frontend/stories/BreweryHeaderCard.stories.tsx b/web/frontend/stories/BreweryHeaderCard.stories.tsx index a77bdd88..467949ec 100644 --- a/web/frontend/stories/BreweryHeaderCard.stories.tsx +++ b/web/frontend/stories/BreweryHeaderCard.stories.tsx @@ -3,7 +3,7 @@ import { expect, fn, userEvent, within } from 'storybook/test'; import BreweryHeaderCard from '../app/features/breweries/components/BreweryHeaderCard'; import type { Brewery, BreweryLocation } from '../app/features/breweries/breweries.server'; -const breweryHeaderCardDescription = `Top card on the brewery detail page: eyebrow + "Est." badge, name, address, like button, average rating, and description. Founded year and average rating are currently filler pending backend support — see BREWERY_DETAIL_HANDOFF.md.`; +const breweryHeaderCardDescription = `Top card on the brewery detail page: eyebrow + "Est." badge, name, address, like button, average rating, and description. Founded year and average rating are currently filler pending backend support — see BREWERY_HANDOFF.md.`; const portlandLocation: BreweryLocation = { breweryPostLocationId: 'loc-1',