E01-S04 — Home page: guest and signed-in
Issue: #207 · Epic: #201 · Backlog:
.ai/specs/backlog-przebudowa.md(E01-S04, refs A1/A2/C1) · Depends on: S01 (tokens/primitives), S02 (public home), both live. Also S09 (#250), which definescategoryPath()→ the public/kategoria/{value}and merges before this story.
📝 TLDR
Today / is a pre-rebuild page: a guest sees a small orange box with login links and three recent recipes; a signed-in user sees the add-from-link form and a “Zapisane przepisy” button. Proposed: the home becomes the variant-1c entry screen — a guest gets a search hero (“Czego dziś szukasz?”), popular searches, an 8-tile category grid, a 2-column “Ostatnio dodane” feed and a sign-up CTA; a signed-in user gets a greeting, the same search field, three shortcut tiles (Moje przepisy / Ulubione / Dodaj przepis) and the category grid. It is the first rebuilt screen and the base S17 widens for desktop.
📝 Problem Statement
The home is the first screen every visitor sees, and it is the one most out of line with the E01 rules: 12–14 px grey text (text-gray-400/500 fails 4.5:1), raw orange hexes instead of tokens, no search entry for a guest, no category entry at all. The target audience includes older people, so the readability rules (19 px body, 56–60 px primary controls, 48 px targets) matter most here.
📝 Proposed Solution
Rebuild src/app/page.tsx as a server component that branches on the session and composes small home-only components under src/app/components/home/. All navigation targets go through seams in src/lib/routes.ts so later stories (S05 search, S10 /dodaj) repoint one function instead of editing the screen. Data needs are unchanged except the recent-feed limit (3 → 4).
Alternatives considered:
- Reuse
RecipeSearch(the/recipeslive-filter input) for the hero. Lost: it debouncesrouter.pushto/recipes, is 40 px tall and grey-on-white; the hero needs a plain submit form (60 px, explicit “Szukaj przepisu” button) that works without JS.RecipeSearchstays for/recipesand is S05’s to rework. - Build
/szukajnow so guest searches work. Lost: that is S05 scope; see Resolved assumption A1. The public category page/kategoria/{value}comes from S09 (#250).
📝 Architecture
- Primary module:
src/app/page.tsx(server). ReadsgetCurrentUser(); guest →<GuestHome>, signed-in →<MemberHome>. - New components (
src/app/components/home/):home-search.tsx(client) —<form role="search" method="get" action={SEARCH_PATH}>with a 60 pxtype="search"input, a labelled mic button (progressive enhancement, Web Speech APIpl-PL, rendered only when supported — the probeAddRecipeForminlines today is extracted tospeechRecognitionCtor()/isSpeechRecognitionSupported()insrc/lib/speech.tsand shared), and a 56–60 px submit “Szukaj przepisu”. Works as a plain GET without JS.popular-searches.tsx— “Często szukane” pill links (<Link>, 48 px, bordered so they read as controls; see UI/UX).category-grid.tsx— “Kategorie” tiles fromHOME_CATEGORIES;columns: 2 | 3prop.member-tiles.tsx— Moje przepisy (only with full access) / Ulubione / Dodaj przepis.guest-cta.tsx— “Załóż konto / Zaloguj się” block.
- Reused:
RecipeCard(links viarecipePath(), showssourceLabel()— never a login),buttonClasses(), tokens fromglobals.css,AddRecipeForm(kept on the signed-in home as the S10 slot, see A5; its inline speech probe switches to the shared helper). - Seams in
src/lib/routes.ts: this story addsSEARCH_PATH+searchPath(q). It consumescategoryPath(value)from S09 (#250), which points at the public/kategoria/{value}.src/lib/recipe-categories.tsgainsHOME_CATEGORIES(8 entries) andsrc/lib/home.tsholdsPOPULAR_SEARCHESandhomeGreeting(vocative). - Layout slots (not implemented): a
{/* S15: notifications banner */}slot as the first child of<main>(above hero/greeting) and the search seam for S05. S17 widens the same components (no desktop variants here beyond a sanemax-w). - Planned dependencies: S12 (vocative + gender) and S11 (
hasFullAccess) are being built in parallel. This story consumes them through two inputs —vocative: string | nullandhasFullAccess: boolean— resolved inpage.tsxwith interim values (A2, A3), so it ships independently.
Takeaway: one server branch, five small presentational components, and all outbound links behind routes.ts seams. S05 and S10 change their targets without touching the screen.
📝 Data Model
No schema change, no migration. Queries:
- Guest:
getRecentPublicRecipes(supabase)inpage.tsx, apublic_recipesselect (existing columns)order created_at desc limit 4. It was 3 before; 4 fills a 2-column grid. - Signed-in: no recipe query on the home (AC lists no feed for C1).
getFailedSharesstays for the header bell.
📝 UI/UX
Mobile-first (375 px), container PageContainer. Copy is Polish, exactly as below.
Guest (A1):
- (S15 slot)
- Hero —
bg-surface-accent(#fff7ed), H1 „Czego dziś szukasz?” (28–30 px,text-ink, bold),HomeSearch: label „Szukaj przepisu po nazwie lub składniku” (visible, 17–19 px), input 60 px, 2 pxborder-field, 19 px text, placeholdertext-ink-muted; mic button „Mów” with icon + text (60 px tall). The label stays „Mów” while listening, witharia-pressed=true. A second tap aborts recognition without submitting. A recognised phrase fills the field and submits. submitbuttonClasses('primary','lg')full width, „Szukaj przepisu”. - „Często szukane” — H2 + wrap of pill links (48 px min height,
text-ink, 2 pxborder-border-card-2), each →searchPath(term). - „Kategorie”: H2 plus a grid of 8 tiles, 3 columns from 360 px and 2 below that, because at 320 px three columns split „Ciasteczka” mid-word. Tiles are at least 64 px tall, with a 17 px bold label,
rounded-cardandborder-border-card. Each tile →categoryPath(value). - „Ostatnio dodane” — H2 + 2-column grid of 4
RecipeCards. Hidden when the feed is empty. - CTA block:
bg-surface-2, H2 „Zapisuj ulubione przepisy”, copy „Załóż konto, żeby mieć swoje przepisy zawsze pod ręką.” and two full-width buttons: primary „Załóż konto” →/signup, outline „Zaloguj się” →/login.
Signed-in (C1/A2):
- (S15 slot)
- H1
homeGreeting(vocative)→ „Dzień dobry, {wołacz}!” or „Dzień dobry!” when unknown. HomeSearch(same component, no hero background).- Tiles (single column, 72 px min height, icon + label + one-line caption
text-ink-soft16 px): „Moje przepisy” →/recipes(only whenhasFullAccess), „Ulubione” →/ulubione, „Dodaj przepis” →/?tab=add. - „Kategorie” — 2-column grid, same tiles.
- „Dodaj przepis z linku” section (H2,
aria-labelledby) with the existingAddRecipeForm— the S10 slot (A5).
RecipeCard token pass (Phase 3 — in scope because the card is on the home and its source line fails contrast; it also changes /recipes and /ulubione, so it is its own phase with its own visual check): title stays an h3 (sections are H2) at 19 px text-ink, hover text-action-text (was orange-600); description 16 px text-ink-soft (was text-sm text-gray-500); source line 16 px text-ink-soft (was text-xs text-gray-400, ~2.5:1); category badge bg-surface-accent-2 text-action-text 16 px bold (the helper-text minimum), moved below the source line; card rounded-card border border-border-card (was rounded-lg border-gray-200); placeholder icon text-action-text; padding p-3, title line-clamp-3, h-full so cards in a grid row match height. Props are unchanged.
Accessibility: one H1 per state; H2 per section with aria-labelledby; every icon has visible text; focus ring from globals.css; no horizontal scroll at 320 px (the guest category grid drops to 2 columns below 360 px, and labels wrap); mic button announces state via aria-pressed and an aria-live="polite" status („Słucham…”). The status region stays in the accessibility tree (sr-only when empty), so the first announcement isn’t skipped; links inside running text underlined, tile/pill links are self-evidently controls (border + text) per the S01 primitives.
Prototype: .ai/specs/assets/e01-s04-home-page/mockup.html. It’s the look reference for this story and follows the backlog’s written AC. The design handoff files (.dc.html) aren’t in the repo.
📝 Edge Cases & Failure Scenarios
public_recipesquery fails / empty → „Ostatnio dodane” section is omitted; rest of the page renders (no error state on the home).- Speech API absent (Firefox, iOS PWA) → mic button not rendered; field + submit still work. On a recognition error or a denied permission, the status reads „Nie udało się rozpoznać mowy. Wpisz nazwę przepisu.” The field is unchanged, and the message clears once the user types.
- Empty search submit → native
requiredon the input prevents a blank navigation. - Guest search or popular-search tap before S05: the interim target is owner-only, so middleware sends the guest to
/login?next=…. After login they see their own list filtered, not global results (A1, ⚠). Category taps go to the public/kategoria/{value}(S09). - Vocative unknown → „Dzień dobry!” (backlog S12 fallback rule).
add_errorredirects (/?add_error=…from the add action and/share) keep landing on the signed-in home whereAddRecipeFormshows the error.- Text size A++ (1.3×) → tiles/pills wrap; no fixed heights that clip text (use
min-h).
📝 Risks & Impact Review
- Interim guest search dead-end (A1, ⚠) is the main product risk. Until S05 ships, every guest search and popular-search tap lands on login, and after login the user sees their own (possibly empty) list, not results. Changing
searchPathreverses it. Categories are not affected: they use S09’s public route. Merge order: S09 (#250) first, then S04. The search interim stays until S05, unless the owner holds S04 for S05. - Signed-in users lose two things on the home. The „Ostatnio dodane” feed goes away, because C1 doesn’t include it (A7). The „Zapisane przepisy →” button is replaced by the „Moje przepisy” tile.
- Full-access interim (A3) knowingly shows „Moje przepisy” and the add form to every signed-in user. That is today’s behaviour (no add gate exists before S11), not a new exposure; S11 flips one input.
RecipeCardrestyle touches/recipesand/ulubionevisually (props unchanged). S05 later reworks result cards; S07 (in flight) does not editrecipe-card.tsxper its scope.- No schema, RLS, or public-contract change; rollback = revert the PR.
📋 Phasing
- Phase 1 — seams + pure helpers:
routes.tsseams,HOME_CATEGORIES,POPULAR_SEARCHES,homeGreetingwith unit tests. Ships nothing visible. - Phase 2 — home screen: components +
page.tsxrebuild; verified on the dev server (guest + signed-in, 375 px and 320 px). - Phase 3 —
RecipeCardtoken pass: shared card restyle; visual check on/,/recipes,/ulubione.
📋 Implementation Plan
Phase 1 — seams and helpers
src/lib/routes.ts: addSEARCH_PATH = '/recipes'andsearchPath(q), which trims, encodes, and returnsSEARCH_PATHfor an empty query. The doc comment names S05 as the owner that repoints it.categoryPath(value)comes from S09 (#250), so don’t redefine it. Test:src/lib/routes.test.tscases for encoding and the empty query.src/lib/recipe-categories.ts: exportHOME_CATEGORIES= the first 8 ofRECIPE_CATEGORIES(valuesobiady, zupy, desery, ciasta, ciasteczka, sniadania, pieczywo, przekaski). Test: length 8, every value in the enum, order matchesRECIPE_CATEGORIES.src/lib/home.ts:POPULAR_SEARCHES(curated, 6 terms) andhomeGreeting(vocative: string | null)→ „Dzień dobry, X!” / „Dzień dobry!” (trims; blank → fallback). Test:src/lib/home.test.ts. 3a.src/lib/speech.ts:speechRecognitionCtor(win)andisSpeechRecognitionSupported(win). Both take the window-like object so they’re testable in node.AddRecipeFormuses them. Test:src/lib/speech.test.ts(standard, webkit-prefixed, absent).
Phase 2 — home screen
The unit runner is node-only (no DOM), so components are verified on the dev server; their logic lives in the Phase 1 helpers, which are unit-tested.
src/app/components/home/home-search.tsx(client): GET form toSEARCH_PATH, 60 px input, mic (Web Speechpl-PL,aria-pressed, live status), submit. Verify: dev server, no-JS submit works, mic hidden where unsupported.popular-searches.tsx,category-grid.tsx(columnsprop),member-tiles.tsx(hasFullAccessprop),guest-cta.tsx. Verify: render in both states.src/app/page.tsx: branch guest/member; guest feed limit 4; S15 slot comment;vocative = nullandhasFullAccess = trueinterim inputs with comments pointing at S12/S11; keepAddRecipeFormin its own section for members. Verify:pnpm typecheck && pnpm lint && pnpm test; dev-server screenshots guest + signed-in at 375 px, 320 px no horizontal scroll.
Phase 3 — RecipeCard token pass
src/app/components/recipe-card.tsx: token/size pass per UI/UX (no prop change). Verify: dev-server screenshots of/,/recipes,/ulubione; no text clipped at A++.
Resolved assumptions (autonomous defaults)
| # | Question | Default chosen | Rationale |
|---|---|---|---|
| A1 | Where do search and popular-search taps go before S05 (/szukaj) exists? (Categories: S09’s public /kategoria/{value} via categoryPath().) | The searchPath() seam points at the existing /recipes?q=, which searches only the user’s own list. Middleware sends a guest to login, and after login they see their own list filtered. Merge order: S09 (#250) first, then S04. ⚠ NEEDS HUMAN CONFIRMATION for the search half: accept this interim, or hold S04 for S05. | The smallest working option, with no new route or public contract. S05 repoints one function. Until then guest search falls short of the epic’s „gość przegląda i szuka”. |
| A2 | Vocative source for „Dzień dobry, {wołacz}” (S12 not merged). | homeGreeting(vocative) with vocative = null today → „Dzień dobry!”; S12 passes its util’s output. | Backlog-defined fallback; no profile/schema change in S04. |
| A3 | „Moje przepisy” only with full access (S11 not merged). | hasFullAccess input, true for every signed-in user until S11 lands. | Knowingly temporary. Matches today: no add gate exists before S11, every signed-in user can add and owns /recipes. S11 flips the input. |
| A4 | Which 8 of the 12 categories? | First 8 in RECIPE_CATEGORIES order, exported as HOME_CATEGORIES. | Single source of truth in recipe-categories.ts as the AC requires; reorder there to change. |
| A5 | Where does adding from a link live on the signed-in home before S10 (/dodaj)? | Keep AddRecipeForm below the categories, in its own H2 section; tile and header „Dodaj” use /?tab=add (already focuses the field). | Removing it would break adding and the ?add_error= / /share redirects. S10 moves it. |
| A6 | Source of „Często szukane” (no search analytics exist). | Curated constant POPULAR_SEARCHES (6 terms). | No new tracking/table; replace with data later. |
| A7 | Does the signed-in home show „Ostatnio dodane”? | No. | C1 AC lists greeting, search, tiles, categories only. |
| A8 | Restyle RecipeCard here? | Yes: tokens and sizes, plus the badge moves below the source. Props are unchanged. | Its source line fails 4.5:1 on the home; fixing it once fixes /recipes and /ulubione too. |