From c8205e474e9b935346e104f8a227cdfe0e6c0783 Mon Sep 17 00:00:00 2001 From: Stan Runge Date: Sat, 24 Oct 2026 02:43:30 +0000 Subject: [PATCH] Money: wish list sorted by tier then price (desc), theme-aware tier colours --- src/lib/money-tracker/wishlist.test.ts | 26 +++++++++++ src/lib/money-tracker/wishlist.ts | 25 ++++++++++ src/routes/trackers/money/+page.svelte | 8 ++-- src/routes/trackers/money/money-tracker.css | 51 +++++++++++++++++++++ 4 files changed, 107 insertions(+), 3 deletions(-) create mode 100644 src/lib/money-tracker/wishlist.test.ts create mode 100644 src/lib/money-tracker/wishlist.ts diff --git a/src/lib/money-tracker/wishlist.test.ts b/src/lib/money-tracker/wishlist.test.ts new file mode 100644 index 0000000..72d2bb8 --- /dev/null +++ b/src/lib/money-tracker/wishlist.test.ts @@ -0,0 +1,26 @@ +import { describe, expect, it } from 'vitest'; +import { sortWishlist, tierClass } from './wishlist'; + +describe('wish list order', () => { + it('sorts by tier descending, then price descending, unpriced and untiered last', () => { + const items = [ + { id: 1, priority: 3, estimatedCents: 5000 }, + { id: 2, priority: 5, estimatedCents: 1000 }, + { id: 3, priority: 5, estimatedCents: null }, + { id: 4, priority: 5, estimatedCents: 90000 }, + { id: 5, priority: null, estimatedCents: 99999 }, + { id: 6, priority: 1, estimatedCents: 200 }, + { id: 7, priority: 3, estimatedCents: 25000 }, + { id: 8, priority: 3, estimatedCents: 5000 } + ]; + expect(sortWishlist(items).map((item) => item.id)).toEqual([4, 2, 3, 7, 1, 8, 6, 5]); + expect(items[0].id).toBe(1); + }); + + it('maps tiers to colour classes', () => { + expect(tierClass(5)).toBe('tier-5'); + expect(tierClass(1)).toBe('tier-1'); + expect(tierClass(null)).toBe('tier-none'); + expect(tierClass(9)).toBe('tier-none'); + }); +}); diff --git a/src/lib/money-tracker/wishlist.ts b/src/lib/money-tracker/wishlist.ts new file mode 100644 index 0000000..0cd96b9 --- /dev/null +++ b/src/lib/money-tracker/wishlist.ts @@ -0,0 +1,25 @@ +export type WishSortable = { + id?: number; + priority: number | null; + estimatedCents: number | null; +}; + +/** + * Wish list order: tier 5 first down to tier 1, then the most expensive first within a tier. + * Items without a tier sink below every tier; unpriced items sink to the end of their tier. + */ +export function sortWishlist(items: readonly T[]): T[] { + const rank = (value: number | null) => (value == null ? -Infinity : value); + return [...items].sort( + (a, b) => + rank(b.priority) - rank(a.priority) || + (a.estimatedCents == null ? 1 : 0) - (b.estimatedCents == null ? 1 : 0) || + (b.estimatedCents ?? 0) - (a.estimatedCents ?? 0) || + (a.id ?? 0) - (b.id ?? 0) + ); +} + +/** CSS class for a tier colour (tier-1 … tier-5), or "tier-none". */ +export function tierClass(priority: number | null): string { + return priority != null && priority >= 1 && priority <= 5 ? `tier-${priority}` : 'tier-none'; +} diff --git a/src/routes/trackers/money/+page.svelte b/src/routes/trackers/money/+page.svelte index 9463b79..606e332 100644 --- a/src/routes/trackers/money/+page.svelte +++ b/src/routes/trackers/money/+page.svelte @@ -17,6 +17,7 @@ import Download from 'lucide-svelte/icons/download'; import MoneyChart from '#lib/money-tracker/MoneyChart.svelte'; import QuickAdd from '#lib/money-tracker/QuickAdd.svelte'; + import { sortWishlist, tierClass } from '#lib/money-tracker/wishlist.js'; import { Badge } from '#lib/components/ui/badge/index.js'; import { resolve } from '$app/paths'; import { page } from '$app/state'; @@ -174,6 +175,7 @@ count: data.wishlist.filter((item) => item.priority >= tier).length })) ); + let sortedWishlist = $derived(sortWishlist(data.wishlist)); let unpricedWishes = $derived( data.wishlist.filter((item) => item.estimatedCents === null).length ); @@ -702,7 +704,7 @@ >
- {#each wishTiers as row (row.tier)}
+ {#each wishTiers as row (row.tier)}
{row.tier === 5 ? 'Tier 5' : `5–${row.tier}`}{amount(row.value)}{row.count} {row.count === 1 ? 'item' : 'items'} @@ -713,9 +715,9 @@ totals.

{/if} {#if data.wishlist.length}
    - {#each data.wishlist as row (row.id)}
  • + {#each sortedWishlist as row (row.id)}
  • {row.priority}{row.priority}{row.name}{#if row.notes}{row.notes}{/if} div:not(.tier-none) { + border-top: 3px solid var(--tier); +} +.money-app .wish-tier-grid > div:not(.tier-none) .eyebrow { + color: var(--tier); +} .money-app .mini-stats { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr));