docs: address spec review gaps in changelog/feedback design
This commit is contained in:
@@ -29,7 +29,11 @@ Add a public `/changelog` page with three tabs (What's New, What's Next, Give Fe
|
|||||||
| `/feedback` | Standalone feedback page — full-width `FeedbackForm` |
|
| `/feedback` | Standalone feedback page — full-width `FeedbackForm` |
|
||||||
| `/admin/feedback` | Admin table of feedback submissions |
|
| `/admin/feedback` | Admin table of feedback submissions |
|
||||||
|
|
||||||
Tab state on `/changelog` is driven by URL hash (`#whats-new`, `#whats-next`, `#give-feedback`) so tabs are deep-linkable. The "Don't see what you need? Give us feedback →" nudge on the roadmap tab links to `#give-feedback`.
|
Tab state on `/changelog` is driven by URL hash (`#whats-new`, `#whats-next`, `#give-feedback`) so tabs are deep-linkable. When no hash is present, the default active tab is **What's New**. The "Don't see what you need? Give us feedback →" nudge on the roadmap tab links to `#give-feedback`.
|
||||||
|
|
||||||
|
### Middleware
|
||||||
|
|
||||||
|
`/changelog` and `/feedback` are public routes — no authentication required. Both must be added to `isPublicPath()` in `middleware.ts` so they remain accessible when `LAUNCH_ONLY_ROOT=true`. The Next.js proxy routes `/api/feedback` (POST) and `/api/roadmap/votes` (GET) require no auth and will pass through middleware as-is since they are not prefixed with `/api/admin` or matched by `isProtectedLaunchPath()`. This is intentional and requires no middleware change for those API routes.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -54,9 +58,16 @@ Tab state on `/changelog` is driven by URL hash (`#whats-new`, `#whats-next`, `#
|
|||||||
| `status` | string enum | `planned` \| `in-progress` \| `shipped` |
|
| `status` | string enum | `planned` \| `in-progress` \| `shipped` |
|
||||||
| `order` | number | Manual sort order within each status group |
|
| `order` | number | Manual sort order within each status group |
|
||||||
|
|
||||||
Both use the existing Sanity project (`zal102rv`, dataset `production`). GROQ queries added to `lib/sanity.ts` alongside existing `postsQuery`. Pages use `revalidate = 60` ISR, consistent with guides.
|
Both use the existing Sanity project (`zal102rv`, dataset `production`). GROQ query strings are added to `lib/sanity.ts` alongside the existing `postsQuery`. The RSC page **must call `sanityFetch()` from `lib/sanityFetch.ts`** to execute those queries — not `client.fetch()` from the Sanity client directly. This is the same pattern used by the guides page and is required for correct behaviour in Docker/server environments where CDN DNS resolution fails.
|
||||||
|
|
||||||
The Sanity `_id` of each `roadmapItem` serves as the foreign key in Spring Boot's votes table — no additional ID field required.
|
Pages use `export const revalidate = 60` ISR, consistent with guides.
|
||||||
|
|
||||||
|
The Sanity `_id` of each `roadmapItem` serves as the foreign key in Spring Boot's votes table — no additional ID field required. Sanity `_id` values are stable UUIDs.
|
||||||
|
|
||||||
|
### GROQ Query Filters
|
||||||
|
|
||||||
|
- `changelogEntriesQuery`: filter `_type == "changelogEntry" && publishedAt <= now()`, order by `publishedAt desc`
|
||||||
|
- `roadmapItemsQuery`: filter `_type == "roadmapItem" && status != "shipped"`, order by `order asc` within each status group. **Shipped items are excluded from the roadmap query** — when an item ships, the author changes its status to `shipped` in Sanity and creates a corresponding `changelogEntry`. The roadmap tab never displays shipped items.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -77,21 +88,23 @@ The Sanity `_id` of each `roadmapItem` serves as the foreign key in Spring Boot'
|
|||||||
| `reviewed` | boolean, default false | Admin marks reviewed |
|
| `reviewed` | boolean, default false | Admin marks reviewed |
|
||||||
| `created_at` | timestamp | |
|
| `created_at` | timestamp | |
|
||||||
|
|
||||||
|
Rate limiting on `POST /api/v1/feedback` is deferred — IP and user agent capture provides enough visibility for manual spam review at this scale.
|
||||||
|
|
||||||
**`roadmap_votes`**
|
**`roadmap_votes`**
|
||||||
|
|
||||||
| Column | Type | Notes |
|
| Column | Type | Notes |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| `user_id` | UUID, FK → users | |
|
| `user_id` | UUID, FK → users | Part of composite PK |
|
||||||
| `sanity_item_id` | varchar | Sanity `_id` of the roadmap item |
|
| `sanity_item_id` | varchar | Part of composite PK; Sanity `_id` of the roadmap item |
|
||||||
| `created_at` | timestamp | |
|
| `created_at` | timestamp | |
|
||||||
| | UNIQUE(user_id, sanity_item_id) | DB-enforced one vote per user per item |
|
| | PRIMARY KEY(user_id, sanity_item_id) | Composite PK — enforces one vote per user per item |
|
||||||
|
|
||||||
### New API Endpoints (Spring Boot)
|
### New API Endpoints (Spring Boot)
|
||||||
|
|
||||||
| Method | Path | Auth | Notes |
|
| Method | Path | Auth | Notes |
|
||||||
|---|---|---|---|
|
|---|---|---|---|
|
||||||
| `POST` | `/api/v1/feedback` | None | Creates feedback record; captures IP + user agent server-side |
|
| `POST` | `/api/v1/feedback` | None | Creates feedback record; captures IP + user agent server-side |
|
||||||
| `GET` | `/api/v1/roadmap/votes?ids=...` | Optional | Returns vote counts + current user's voted state for a list of Sanity IDs |
|
| `GET` | `/api/v1/roadmap/votes?ids=...` | Optional | Returns vote counts + current user's voted state for a list of Sanity IDs. For unauthenticated callers, returns vote counts with `voted: false` for all items — never 401. |
|
||||||
| `POST` | `/api/v1/roadmap/{sanityId}/vote` | Required | Toggles vote on/off; returns new count and voted state |
|
| `POST` | `/api/v1/roadmap/{sanityId}/vote` | Required | Toggles vote on/off; returns new count and voted state |
|
||||||
|
|
||||||
### Next.js Proxy Routes
|
### Next.js Proxy Routes
|
||||||
@@ -104,6 +117,8 @@ All three endpoints are proxied via `app/api/` — client code never calls Sprin
|
|||||||
| `GET /api/roadmap/votes` | `GET /api/v1/roadmap/votes` |
|
| `GET /api/roadmap/votes` | `GET /api/v1/roadmap/votes` |
|
||||||
| `POST /api/roadmap/[sanityId]/vote` | `POST /api/v1/roadmap/:id/vote` |
|
| `POST /api/roadmap/[sanityId]/vote` | `POST /api/v1/roadmap/:id/vote` |
|
||||||
|
|
||||||
|
The Next.js proxy for `POST /api/feedback` captures `x-forwarded-for` and `user-agent` from the incoming request headers and forwards them to Spring Boot — the client form never sends these values.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Frontend Components
|
## Frontend Components
|
||||||
@@ -112,14 +127,14 @@ All three endpoints are proxied via `app/api/` — client code never calls Sprin
|
|||||||
|
|
||||||
| File | Type | Responsibility |
|
| File | Type | Responsibility |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| `app/(app)/changelog/page.tsx` | RSC | Fetches Sanity changelog + roadmap data, renders tab shell |
|
| `app/(app)/changelog/page.tsx` | RSC | Fetches Sanity changelog + roadmap data via `sanityFetch`, renders tab shell + passes data as props |
|
||||||
| `components/ChangelogTabs.tsx` | `"use client"` | Hash-based tab switcher, renders all three tab panels |
|
| `components/ChangelogTabs.tsx` | `"use client"` | Hash-based tab switcher (default: `#whats-new`), renders all three tab panels |
|
||||||
| `components/RoadmapItem.tsx` | `"use client"` | Single roadmap item with optimistic upvote toggle |
|
| `components/RoadmapItem.tsx` | `"use client"` | Single roadmap item with optimistic upvote toggle |
|
||||||
| `components/FeedbackForm.tsx` | `"use client"` | Shared form (category + message + email), posts to `/api/feedback` |
|
| `components/FeedbackForm.tsx` | `"use client"` | Shared form (category + message + email), posts to `/api/feedback` |
|
||||||
| `components/FeedbackModal.tsx` | `"use client"` | Backdrop + dialog wrapper around `FeedbackForm` |
|
| `components/FeedbackModal.tsx` | `"use client"` | Backdrop + dialog wrapper around `FeedbackForm` |
|
||||||
| `app/(app)/feedback/page.tsx` | RSC | Standalone page rendering `FeedbackForm` full-width |
|
| `app/(app)/feedback/page.tsx` | RSC | Standalone page rendering `FeedbackForm` full-width |
|
||||||
| `app/admin/feedback/page.tsx` | RSC | Admin table of submissions, sortable + filterable |
|
| `app/admin/feedback/page.tsx` | RSC | Admin table of submissions, sortable + filterable |
|
||||||
| `app/api/feedback/route.ts` | API route | Proxy to Spring Boot |
|
| `app/api/feedback/route.ts` | API route | Proxy to Spring Boot; forwards IP + user-agent headers |
|
||||||
| `app/api/roadmap/votes/route.ts` | API route | Proxy to Spring Boot |
|
| `app/api/roadmap/votes/route.ts` | API route | Proxy to Spring Boot |
|
||||||
| `app/api/roadmap/[sanityId]/vote/route.ts` | API route | Proxy to Spring Boot |
|
| `app/api/roadmap/[sanityId]/vote/route.ts` | API route | Proxy to Spring Boot |
|
||||||
|
|
||||||
@@ -128,8 +143,13 @@ All three endpoints are proxied via `app/api/` — client code never calls Sprin
|
|||||||
| File | Change |
|
| File | Change |
|
||||||
|---|---|
|
|---|---|
|
||||||
| `lib/sanity.ts` | Add `changelogEntriesQuery` and `roadmapItemsQuery` |
|
| `lib/sanity.ts` | Add `changelogEntriesQuery` and `roadmapItemsQuery` |
|
||||||
| `components/TopNav.tsx` | Add "Send Feedback" ghost button that opens `FeedbackModal` |
|
| `middleware.ts` | Add `/changelog` and `/feedback` to `isPublicPath()` |
|
||||||
| `app/page.tsx` | Add "Send Feedback" link to footer alongside Privacy / Terms |
|
| `components/TopNav.tsx` | Add "Send Feedback" ghost button; modal open state is local to `TopNav` via `useState` |
|
||||||
|
| `app/page.tsx` | Add "Send Feedback" link to the homepage's inline footer (homepage-only; this is intentional — TopNav covers all other pages) |
|
||||||
|
|
||||||
|
### FeedbackModal State
|
||||||
|
|
||||||
|
`FeedbackModal` open/close state is managed locally in `TopNav` with `useState`. No global context or portal is required — the modal renders inside `TopNav`'s JSX and is positioned fixed via CSS. This is sufficient given "Send Feedback" is the only trigger point in the nav.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -156,12 +176,12 @@ Tag values rendered as small pill badges:
|
|||||||
|
|
||||||
### What's Next Tab
|
### What's Next Tab
|
||||||
|
|
||||||
Items grouped under "In Progress" and "Planned" headings (Shipped items not shown — they migrate to the changelog). Each item is a card with:
|
Items grouped under "In Progress" and "Planned" headings. Shipped items are never shown here (filtered out in the GROQ query). Each item is a card with:
|
||||||
- **Left:** upvote button (chevron up + count) — bordered pill, amber when voted
|
- **Left:** upvote button (chevron up + count) — bordered pill, amber when voted
|
||||||
- **Center:** title + description
|
- **Center:** title + description
|
||||||
- **Right:** status badge (In Progress = amber, Planned = muted)
|
- **Right:** status badge (In Progress = amber, Planned = muted)
|
||||||
|
|
||||||
Unauthenticated users clicking upvote are prompted to sign in (toast or redirect). Voted state is toggled optimistically — rolls back on error.
|
Unauthenticated users clicking upvote are prompted to sign in (toast). Voted state is toggled optimistically — rolls back on error.
|
||||||
|
|
||||||
Footer nudge: *"Don't see what you need? Give us feedback →"* links to `#give-feedback`.
|
Footer nudge: *"Don't see what you need? Give us feedback →"* links to `#give-feedback`.
|
||||||
|
|
||||||
@@ -174,13 +194,13 @@ Full-width form:
|
|||||||
|
|
||||||
### Feedback Modal
|
### Feedback Modal
|
||||||
|
|
||||||
Triggered by "Send Feedback" in TopNav and homepage footer. Same fields as the tab form. Full-width submit button inside the modal.
|
Triggered by "Send Feedback" in TopNav and homepage inline footer. Same fields as the tab form. Full-width submit button inside the modal.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Admin View (`/admin/feedback`)
|
## Admin View (`/admin/feedback`)
|
||||||
|
|
||||||
Table with columns: Date, Category, Message (truncated), Email, Reviewed. Sortable by date. Filterable by category (Bug / Feature / General) and reviewed status. Follows existing admin page patterns.
|
Table with columns: Date, Category, Message (truncated to 120 characters), Email, Reviewed. Sortable by date. Filterable by category (Bug / Feature / General) and reviewed status. Follows existing admin page patterns.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -196,6 +216,15 @@ Table with columns: Date, Category, Message (truncated), Email, Reviewed. Sortab
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## SEO Metadata
|
||||||
|
|
||||||
|
Both `/changelog` and `/feedback` export a `metadata` object following the existing page pattern:
|
||||||
|
|
||||||
|
- `/changelog`: `title: "What's New — Battl Builders"`, relevant description
|
||||||
|
- `/feedback`: `title: "Send Feedback — Battl Builders"`, relevant description
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Out of Scope
|
## Out of Scope
|
||||||
|
|
||||||
- Email notifications when feedback is submitted (separate task)
|
- Email notifications when feedback is submitted (separate task)
|
||||||
@@ -203,3 +232,4 @@ Table with columns: Date, Category, Message (truncated), Email, Reviewed. Sortab
|
|||||||
- Public display of vote counts on the changelog tab
|
- Public display of vote counts on the changelog tab
|
||||||
- Sanity Studio schema deployment (handled separately via Sanity CLI)
|
- Sanity Studio schema deployment (handled separately via Sanity CLI)
|
||||||
- Spring Boot entity/migration code (lives in `ballistic-builder-spring` repo, tracked separately)
|
- Spring Boot entity/migration code (lives in `ballistic-builder-spring` repo, tracked separately)
|
||||||
|
- Rate limiting on the feedback endpoint (deferred; IP capture provides visibility at current scale)
|
||||||
|
|||||||
Reference in New Issue
Block a user