Money: wish list sorted by tier then price (desc), theme-aware tier colours
All checks were successful
Deploy production / deploy (push) Successful in 2m31s
All checks were successful
Deploy production / deploy (push) Successful in 2m31s
This commit is contained in:
26
src/lib/money-tracker/wishlist.test.ts
Normal file
26
src/lib/money-tracker/wishlist.test.ts
Normal file
@@ -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');
|
||||||
|
});
|
||||||
|
});
|
||||||
25
src/lib/money-tracker/wishlist.ts
Normal file
25
src/lib/money-tracker/wishlist.ts
Normal file
@@ -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<T extends WishSortable>(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';
|
||||||
|
}
|
||||||
@@ -17,6 +17,7 @@
|
|||||||
import Download from 'lucide-svelte/icons/download';
|
import Download from 'lucide-svelte/icons/download';
|
||||||
import MoneyChart from '#lib/money-tracker/MoneyChart.svelte';
|
import MoneyChart from '#lib/money-tracker/MoneyChart.svelte';
|
||||||
import QuickAdd from '#lib/money-tracker/QuickAdd.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 { Badge } from '#lib/components/ui/badge/index.js';
|
||||||
import { resolve } from '$app/paths';
|
import { resolve } from '$app/paths';
|
||||||
import { page } from '$app/state';
|
import { page } from '$app/state';
|
||||||
@@ -174,6 +175,7 @@
|
|||||||
count: data.wishlist.filter((item) => item.priority >= tier).length
|
count: data.wishlist.filter((item) => item.priority >= tier).length
|
||||||
}))
|
}))
|
||||||
);
|
);
|
||||||
|
let sortedWishlist = $derived(sortWishlist(data.wishlist));
|
||||||
let unpricedWishes = $derived(
|
let unpricedWishes = $derived(
|
||||||
data.wishlist.filter((item) => item.estimatedCents === null).length
|
data.wishlist.filter((item) => item.estimatedCents === null).length
|
||||||
);
|
);
|
||||||
@@ -702,7 +704,7 @@
|
|||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
<div class="wish-tier-grid">
|
<div class="wish-tier-grid">
|
||||||
{#each wishTiers as row (row.tier)}<div>
|
{#each wishTiers as row (row.tier)}<div class={tierClass(row.tier)}>
|
||||||
<span class="eyebrow">{row.tier === 5 ? 'Tier 5' : `5–${row.tier}`}</span><strong
|
<span class="eyebrow">{row.tier === 5 ? 'Tier 5' : `5–${row.tier}`}</span><strong
|
||||||
>{amount(row.value)}</strong
|
>{amount(row.value)}</strong
|
||||||
><small>{row.count} {row.count === 1 ? 'item' : 'items'}</small>
|
><small>{row.count} {row.count === 1 ? 'item' : 'items'}</small>
|
||||||
@@ -713,9 +715,9 @@
|
|||||||
totals.
|
totals.
|
||||||
</p>{/if}
|
</p>{/if}
|
||||||
{#if data.wishlist.length}<ul class="item-list scroll">
|
{#if data.wishlist.length}<ul class="item-list scroll">
|
||||||
{#each data.wishlist as row (row.id)}<li>
|
{#each sortedWishlist as row (row.id)}<li class="wish {tierClass(row.priority)}">
|
||||||
<span class="item-main"
|
<span class="item-main"
|
||||||
><span class="tier">{row.priority}</span><span
|
><span class="tier" title="Tier {row.priority}">{row.priority}</span><span
|
||||||
><strong>{row.name}</strong>{#if row.notes}<small>{row.notes}</small>{/if}</span
|
><strong>{row.name}</strong>{#if row.notes}<small>{row.notes}</small>{/if}</span
|
||||||
></span
|
></span
|
||||||
><strong class="item-amount"
|
><strong class="item-amount"
|
||||||
|
|||||||
@@ -419,6 +419,57 @@
|
|||||||
color: var(--site-muted);
|
color: var(--site-muted);
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
}
|
}
|
||||||
|
/* Wish list tier colours: dark theme uses the lighter shade, light theme the deeper one. */
|
||||||
|
.money-app .tier-5 {
|
||||||
|
--tier: #fb7185;
|
||||||
|
}
|
||||||
|
.money-app .tier-4 {
|
||||||
|
--tier: #fb923c;
|
||||||
|
}
|
||||||
|
.money-app .tier-3 {
|
||||||
|
--tier: #facc15;
|
||||||
|
}
|
||||||
|
.money-app .tier-2 {
|
||||||
|
--tier: #34d399;
|
||||||
|
}
|
||||||
|
.money-app .tier-1 {
|
||||||
|
--tier: #94a3b8;
|
||||||
|
}
|
||||||
|
:root[data-theme='light'] .money-app .tier-5 {
|
||||||
|
--tier: #e11d48;
|
||||||
|
}
|
||||||
|
:root[data-theme='light'] .money-app .tier-4 {
|
||||||
|
--tier: #ea580c;
|
||||||
|
}
|
||||||
|
:root[data-theme='light'] .money-app .tier-3 {
|
||||||
|
--tier: #ca8a04;
|
||||||
|
}
|
||||||
|
:root[data-theme='light'] .money-app .tier-2 {
|
||||||
|
--tier: #059669;
|
||||||
|
}
|
||||||
|
:root[data-theme='light'] .money-app .tier-1 {
|
||||||
|
--tier: #64748b;
|
||||||
|
}
|
||||||
|
.money-app .item-list li.wish:not(.tier-none) {
|
||||||
|
box-shadow: inset 3px 0 0 var(--tier);
|
||||||
|
}
|
||||||
|
.money-app .wish:not(.tier-none) .tier {
|
||||||
|
border-color: color-mix(in srgb, var(--tier) 55%, transparent);
|
||||||
|
background: color-mix(in srgb, var(--tier) 16%, transparent);
|
||||||
|
color: var(--tier);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
.money-app .wish.tier-5 .tier {
|
||||||
|
border-color: var(--tier);
|
||||||
|
background: var(--tier);
|
||||||
|
color: var(--site-bg);
|
||||||
|
}
|
||||||
|
.money-app .wish-tier-grid > 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 {
|
.money-app .mini-stats {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||||
|
|||||||
Reference in New Issue
Block a user