From d20e49887de265708daf33d749438d8ee6ce117d Mon Sep 17 00:00:00 2001 From: TSVRN9 <66744142+TSVRN9@users.noreply.github.com> Date: Wed, 19 Aug 2026 08:03:55 -0400 Subject: [PATCH 1/2] WIP: rank page styling (#38) Partial restyle of the pairwise comparison / rankings screen using #35's tokens. Not verified against acceptance criteria - handing off for evaluation/completion by the #38 agent. --- web/src/routes/rank/+page.svelte | 192 ++++++++++++++++++++++++------- 1 file changed, 148 insertions(+), 44 deletions(-) diff --git a/web/src/routes/rank/+page.svelte b/web/src/routes/rank/+page.svelte index ea9031e0..db6350fc 100644 --- a/web/src/routes/rank/+page.svelte +++ b/web/src/routes/rank/+page.svelte @@ -27,6 +27,13 @@ let pair: [Dish, Dish] | null = $state(null); let hallRanking = $derived(rankDiningHalls(rankedDishes)); let lastPair: [Dish, Dish] | null = null; + // With only two logged dishes pickPair() legitimately returns the same pair every time, so + // choosing a winner changes nothing on screen above the fold and the tap reads as a dead click. + // This line is the acknowledgement. + let lastChoice = $state(""); + + // Each comparison bumps comparisonCount on both dishes, so the sum double-counts. + const comparisonsMade = $derived(Math.round(rankedDishes.reduce((sum, d) => sum + d.comparisonCount, 0) / 2)); function hallName(hallTid: number): string { return DINING_HALLS.find((h) => h.tid === hallTid)?.name ?? `Hall ${hallTid}`; @@ -108,63 +115,160 @@ void syncDiningHallRanks(page.data.supabase, session.user.id, snapshot); } + lastChoice = `${winner.dishName} over ${loser.dishName}`; pair = pickPair(); } function skip() { + lastChoice = ""; pair = pickPair(); } -

Rank Dishes

-

Compare dishes you've actually logged — ranking is built from what you've eaten, not the full menu.

+
+

Rank Dishes

+
+

+ Compare dishes you’ve actually logged — ranking is built from what you’ve eaten, not + the full menu. Every comparison stays on this device. +

+
{#if loggedDishes.length < 2} -

Log a couple of meals first, then come back here to rank them.

+
+

Nothing to compare yet

+

+ Ranking needs at least two different dishes in your log. You have {loggedDishes.length}. + Log a couple of meals first, then come back here to rank them. +

+

Find something to eat

+
{:else if pair} -

Which did you like more?

- - - -{/if} + +
+
+

+ Which did you like more? +

+ + {comparisonsMade} + {comparisonsMade === 1 ? "comparison" : "comparisons"} so far + +
+
-

Your ranking

-{#if rankedDishes.length === 0} -

No comparisons yet.

-{:else} -
    - {#each rankDishes(rankedDishes) as dish (dishKey(dish))} -
  1. {dish.dishName} ({hallName(dish.hallTid)}) — {Math.round(dish.rating)}
  2. - {/each} -
-{/if} +
+ {#each [pair[0], pair[1]] as choice, i (dishKey(choice))} + {#if i === 1} + + {/if} + + {/each} +
-

Favorite Foods

-

Your favorite dishes by name, regardless of which hall serves them.

-{#if rankedFoods.length === 0} -

No comparisons yet.

-{:else} -
    - {#each rankFoods(rankedFoods) as food (food.dishName)} -
  1. {food.dishName} — {Math.round(food.rating)}
  2. - {/each} -
+
+ + +

+ {#if lastChoice}Recorded: {lastChoice}{/if} +

+
+
{/if} -

Dining hall ranking {page.data.session ? "(synced)" : "(local only — sign in to sync)"}

-
    - {#each hallRanking.ranked as hall (hall.hallTid)} -
  1. {hallName(hall.hallTid)}
  2. - {/each} -
-{#if hallRanking.unranked.length > 0} - -{/if} + + {#if hallRanking.unranked.length > 0} + + +

+ A hall needs at least 2 rated dishes before it can be placed. +

+ {/if} + From e663ccd3db2377438b4f03d93779da6f583bff82 Mon Sep 17 00:00:00 2001 From: TSVRN9 <66744142+TSVRN9@users.noreply.github.com> Date: Wed, 19 Aug 2026 09:57:25 -0400 Subject: [PATCH 2/2] rank e2e: assert ranked/unranked halls carry a real visual treatment #38 AC2 requires ranked vs. unranked dining halls to be visually distinguishable, not just distinguishable by wording ("not enough data yet"). The existing assertions only checked text content, which would stay green even if the .card treatment on ranked rows regressed back to a plain list. Pin it to the actual class the WIP styling applies. Red/green: removing "card" from the ranked
  • 's class list in +page.svelte fails this assertion (toHaveClass(/card/) unexpected value "flex items-center gap-3 border-l-4 border-l-gold-500 px-4 py-3"); restoring it passes. Full 17-test e2e suite green afterward. --- web/e2e/rank.spec.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/web/e2e/rank.spec.ts b/web/e2e/rank.spec.ts index b70f7350..0ae9f21f 100644 --- a/web/e2e/rank.spec.ts +++ b/web/e2e/rank.spec.ts @@ -119,4 +119,11 @@ test("log 2 dishes, make a ranking comparison, and see Your ranking / Favorite F await expect(unrankedHalls).toContainText(hall); } await expect(unrankedHalls.locator("li")).toContainText(["not enough data yet", "not enough data yet", "not enough data yet"]); + + // #38 AC2: ranked vs. unranked halls must be visually distinguishable, "not just text saying + // 'not enough data yet'". Pin that to the actual visual treatment (the shared .card surface), + // not just the wording already asserted above, so a regression that quietly drops the card + // styling back to plain rows can't hide behind a still-passing "contains the right words" check. + await expect(rankedHalls.locator("li").first()).toHaveClass(/card/); + await expect(unrankedHalls.locator("li").first()).not.toHaveClass(/card/); });