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/); }); 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} +