Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions web/e2e/favorites.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,21 @@ test("favoriting a dish from the menu shows it on /favorites, un-favoriting remo
const favoriteRow = page.getByRole("listitem").filter({ hasText: "French Toast" });
await expect(favoriteRow).toBeVisible();

// #40: favorites list rows need the same card treatment as the other list screens (dining
// halls, hall menu) — a non-transparent surface visually distinct from the page background,
// not the page's `<body>` color leaking straight through into every row.
const pageBg = await page.evaluate(() => getComputedStyle(document.body).backgroundColor);
const rowBg = await favoriteRow.evaluate((el) => getComputedStyle(el).backgroundColor);
expect(rowBg).not.toBe("rgba(0, 0, 0, 0)");
expect(rowBg).not.toBe(pageBg);

await favoriteRow.getByRole("button", { name: "Remove" }).click();
await expect(page.getByText("No favorite dishes yet.")).toBeVisible();
await expect(favoriteRow).toHaveCount(0);

// #40: the empty state needs the shared dashed-outline `.empty-state` treatment used elsewhere
// in the app, not a bare unstyled paragraph — checked via the one CSS property that treatment
// actually produces, not by naming the class.
const emptyState = page.getByText("No favorite dishes yet.").locator("xpath=..");
await expect(emptyState).toHaveCSS("border-style", "dashed");
});
9 changes: 9 additions & 0 deletions web/e2e/filters.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,16 @@ test("setting an allergen filter hides a conflicting dish and keeps a non-confli

await page.getByRole("link", { name: "Filters" }).click();

// #40: filters are toggle/checkbox-heavy and need a clear on/off state — a checked filter must
// be visually distinct from an unchecked one, not just carry a different checkbox attribute.
// Scoped through the checkbox's own accessible name (not a listitem text filter) so this stays
// unambiguous even if another allergen option's name contains "Milk" as a substring.
const milkLabel = page.getByRole("checkbox", { name: "Milk" }).locator("xpath=..");
const milkBgBeforeCheck = await milkLabel.evaluate((el) => getComputedStyle(el).backgroundColor);

await page.getByRole("checkbox", { name: "Milk" }).check();
await expect(milkLabel).not.toHaveCSS("background-color", milkBgBeforeCheck);

await page.getByRole("button", { name: "Save" }).click();
await expect(page.getByRole("status")).toHaveText("Saved");

Expand Down
65 changes: 44 additions & 21 deletions web/src/routes/favorites/+page.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -30,24 +30,47 @@
}
</script>

<h1>Favorites</h1>

<h2>Dishes</h2>
{#if favorites.filter(isDish).length === 0}
<p>No favorite dishes yet.</p>
{/if}
<ul>
{#each favorites.filter(isDish) as favorite (favorite.dishName)}
<li>{favorite.dishName} <button onclick={() => remove(favorite)}>Remove</button></li>
{/each}
</ul>

<h2>Dining Halls</h2>
{#if favorites.filter(isLocation).length === 0}
<p>No favorite dining halls yet.</p>
{/if}
<ul>
{#each favorites.filter(isLocation) as favorite (favorite.hallTid)}
<li>{hallName(favorite.hallTid)} <button onclick={() => remove(favorite)}>Remove</button></li>
{/each}
</ul>
<header>
<h1 class="page-title">Favorites</h1>
<div class="label-rule mt-2 text-gold-500"></div>
</header>

<section class="mt-8">
<h2 class="section-title">Dishes</h2>
<div class="label-rule mt-1 text-ink-900/25"></div>

{#if favorites.filter(isDish).length === 0}
<div class="empty-state mt-4">
<p>No favorite dishes yet.</p>
</div>
{:else}
<ul class="mt-4 flex flex-col gap-2">
{#each favorites.filter(isDish) as favorite (favorite.dishName)}
<li class="card flex items-center justify-between gap-3 px-4 py-3">
<span class="font-display text-lg font-semibold text-maroon-900">{favorite.dishName}</span>
<button onclick={() => remove(favorite)} class="btn btn-ghost btn-sm">Remove</button>
</li>
{/each}
</ul>
{/if}
</section>

<section class="mt-8">
<h2 class="section-title">Dining Halls</h2>
<div class="label-rule mt-1 text-ink-900/25"></div>

{#if favorites.filter(isLocation).length === 0}
<div class="empty-state mt-4">
<p>No favorite dining halls yet.</p>
</div>
{:else}
<ul class="mt-4 flex flex-col gap-2">
{#each favorites.filter(isLocation) as favorite (favorite.hallTid)}
<li class="card flex items-center justify-between gap-3 px-4 py-3">
<span class="font-display text-lg font-semibold text-maroon-900">{hallName(favorite.hallTid)}</span>
<button onclick={() => remove(favorite)} class="btn btn-ghost btn-sm">Remove</button>
</li>
{/each}
</ul>
{/if}
</section>
97 changes: 65 additions & 32 deletions web/src/routes/filters/+page.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -32,39 +32,72 @@
}
</script>

<h1>Dietary Filters</h1>
<p>Applied to menus on the dining hall pages. Stored on this device only.</p>
<header>
<h1 class="page-title">Dietary Filters</h1>
<div class="label-rule mt-2 text-gold-500"></div>
<p class="mt-3 max-w-prose text-ink-900/70">
Applied to menus on the dining hall pages. Stored on this device only.
</p>
</header>

{#if saved}<p role="status">Saved</p>{/if}

<h2>Avoid allergens</h2>
{#if data.allergenOptions.length === 0}
<p>No allergen data found on today's menus.</p>
{#if saved}
<p role="status" class="badge mt-4">Saved</p>
{/if}
<ul>
{#each data.allergenOptions as allergen (allergen)}
<li>
<label>
<input type="checkbox" checked={prefs.allergensToAvoid.includes(allergen)} onchange={() => toggleAllergen(allergen)} />
{allergen}
</label>
</li>
{/each}
</ul>

<h2>Require diet tags</h2>
{#if data.dietTagOptions.length === 0}
<p>No diet-tag data found on today's menus.</p>
{/if}
<ul>
{#each data.dietTagOptions as tag (tag)}
<li>
<label>
<input type="checkbox" checked={prefs.requiredDietTags.includes(tag)} onchange={() => toggleDietTag(tag)} />
{tag}
</label>
</li>
{/each}
</ul>
<section class="mt-8">
<h2 class="section-title">Avoid allergens</h2>
<div class="label-rule mt-1 text-ink-900/25"></div>

{#if data.allergenOptions.length === 0}
<div class="empty-state mt-4">
<p>No allergen data found on today's menus.</p>
</div>
{:else}
<ul class="mt-4 flex flex-wrap gap-2">
{#each data.allergenOptions as allergen (allergen)}
{@const checked = prefs.allergensToAvoid.includes(allergen)}
<li>
<!-- Native checkbox stays in the accessible-name tree so `getByRole("checkbox", { name })`
keeps working unchanged; the label around it carries the on/off visual state. -->
<label
class="flex cursor-pointer items-center gap-2 rounded-sm border px-3 py-1.5 text-sm font-semibold transition-colors {checked
? 'border-maroon-600 bg-maroon-600 text-paper-50'
: 'border-ink-900/25 text-ink-900/70 hover:border-maroon-600/50'}"
>
<input type="checkbox" checked={checked} onchange={() => toggleAllergen(allergen)} class="accent-maroon-600" />
{allergen}
</label>
</li>
{/each}
</ul>
{/if}
</section>

<section class="mt-8">
<h2 class="section-title">Require diet tags</h2>
<div class="label-rule mt-1 text-ink-900/25"></div>

{#if data.dietTagOptions.length === 0}
<div class="empty-state mt-4">
<p>No diet-tag data found on today's menus.</p>
</div>
{:else}
<ul class="mt-4 flex flex-wrap gap-2">
{#each data.dietTagOptions as tag (tag)}
{@const checked = prefs.requiredDietTags.includes(tag)}
<li>
<label
class="flex cursor-pointer items-center gap-2 rounded-sm border px-3 py-1.5 text-sm font-semibold transition-colors {checked
? 'border-maroon-600 bg-maroon-600 text-paper-50'
: 'border-ink-900/25 text-ink-900/70 hover:border-maroon-600/50'}"
>
<input type="checkbox" checked={checked} onchange={() => toggleDietTag(tag)} class="accent-maroon-600" />
{tag}
</label>
</li>
{/each}
</ul>
{/if}
</section>

<button onclick={save}>Save</button>
<button onclick={save} class="btn btn-primary mt-8">Save</button>
Loading