money: wishlist tier totals fit on one line (compact €, scales with card, exact amount in tooltip)
All checks were successful
Deploy production / deploy (push) Successful in 2m54s

This commit is contained in:
2026-10-24 14:09:07 +00:00
parent 427f4ed095
commit 85da5afdfe
4 changed files with 66 additions and 9 deletions

View File

@@ -1,5 +1,5 @@
import { describe, expect, it } from 'vitest'; import { describe, expect, it } from 'vitest';
import { sortWishlist, tierClass } from './wishlist'; import { compactAmount, compactEuro, sortWishlist, tierClass } from './wishlist';
describe('wish list order', () => { describe('wish list order', () => {
it('sorts by tier descending, then price descending, unpriced and untiered last', () => { it('sorts by tier descending, then price descending, unpriced and untiered last', () => {
@@ -23,4 +23,19 @@ describe('wish list order', () => {
expect(tierClass(null)).toBe('tier-none'); expect(tierClass(null)).toBe('tier-none');
expect(tierClass(9)).toBe('tier-none'); expect(tierClass(9)).toBe('tier-none');
}); });
it('formats tier totals compactly', () => {
expect(compactEuro(0)).toBe('€\u00a00');
expect(compactEuro(500_00)).toBe('€\u00a0500');
expect(compactEuro(8_865_00)).toBe('€\u00a08.865');
expect(compactEuro(9_365_49)).toBe('€\u00a09.365');
expect(compactEuro(9_999_60)).toBe('€\u00a010k');
expect(compactEuro(17_580_00)).toBe('€\u00a017,6k');
expect(compactEuro(28_080_00)).toBe('€\u00a028,1k');
expect(compactEuro(999_960_00)).toBe('€\u00a01M');
expect(compactEuro(1_234_567_00)).toBe('€\u00a01,2M');
expect(compactEuro(-1_500_00)).toBe('−€\u00a01.500');
expect(compactAmount(28_080_00)).toBe('28,1k');
expect(compactAmount(-40)).toBe('0');
});
}); });

View File

@@ -23,3 +23,27 @@ export function sortWishlist<T extends WishSortable>(items: readonly T[]): T[] {
export function tierClass(priority: number | null): string { export function tierClass(priority: number | null): string {
return priority != null && priority >= 1 && priority <= 5 ? `tier-${priority}` : 'tier-none'; return priority != null && priority >= 1 && priority <= 5 ? `tier-${priority}` : 'tier-none';
} }
const oneDecimal = (value: number) => {
const rounded = Math.round(value * 10) / 10;
return (Number.isInteger(rounded) ? String(rounded) : rounded.toFixed(1)).replace('.', ',');
};
/**
* Short amount for the wish list tier cards, so a total fits on one line:
* whole euros below €10k ("8.865"), then thousands ("28,1k") and millions ("1,2M").
* Uses the same en-NL separators as the rest of the tracker. No currency symbol.
*/
export function compactAmount(cents: number): string {
const euros = Math.round(Math.abs(cents) / 100);
const sign = cents < 0 && euros > 0 ? '−' : '';
if (euros >= 999_950) return `${sign}${oneDecimal(euros / 1_000_000)}M`;
if (euros >= 10_000) return `${sign}${oneDecimal(euros / 1_000)}k`;
return sign + String(euros).replace(/\B(?=(\d{3})+(?!\d))/g, '.');
}
/** compactAmount with the euro sign: "€ 8.865", "€ 28,1k", "−€ 1.500". */
export function compactEuro(cents: number): string {
const body = compactAmount(cents);
return body.startsWith('−') ? `−€\u00a0${body.slice(1)}` : `€\u00a0${body}`;
}

View File

@@ -17,7 +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 { compactAmount, 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';
@@ -706,7 +706,8 @@
<div class="wish-tier-grid"> <div class="wish-tier-grid">
{#each wishTiers as row (row.tier)}<div class={tierClass(row.tier)}> {#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 title={amount(row.value)}
><span class="currency">€&nbsp;</span>{compactAmount(row.value)}</strong
><small>{row.count} {row.count === 1 ? 'item' : 'items'}</small> ><small>{row.count} {row.count === 1 ? 'item' : 'items'}</small>
</div>{/each} </div>{/each}
</div> </div>

View File

@@ -92,25 +92,42 @@
} }
.money-app .wish-tier-grid { .money-app .wish-tier-grid {
display: grid; display: grid;
grid-template-columns: repeat(auto-fit, minmax(64px, 1fr)); grid-template-columns: repeat(5, minmax(0, 1fr));
gap: 6px; gap: clamp(4px, 1.2vw, 6px);
} }
.money-app .wish-tier-grid > div { .money-app .wish-tier-grid > div {
display: grid; display: grid;
gap: 3px; gap: 3px;
padding: 9px 11px; padding: 8px clamp(4px, 1.2vw, 11px);
min-width: 0; min-width: 0;
border: 1px solid var(--site-border); border: 1px solid var(--site-border);
border-radius: 7px; border-radius: 7px;
container-type: inline-size;
} }
.money-app .wish-tier-grid > div > * {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
/* Totals are compact ("€ 8.865", "€ 28,1k") and scale with the card so they stay on one line. */
.money-app .wish-tier-grid strong { .money-app .wish-tier-grid strong {
font-size: 14px; font-size: clamp(10px, 23cqi, 15px);
font-weight: 600; font-weight: 600;
overflow-wrap: anywhere; font-variant-numeric: tabular-nums;
letter-spacing: -0.01em;
}
.money-app .wish-tier-grid small,
.money-app .wish-tier-grid .eyebrow {
font-size: clamp(10px, 19cqi, 12px);
} }
.money-app .wish-tier-grid small { .money-app .wish-tier-grid small {
color: var(--site-muted); color: var(--site-muted);
font-size: 11px; }
/* On very narrow phones the tier cards drop the euro sign; the exact amount stays in the tooltip. */
@container (max-width: 42px) {
.money-app .wish-tier-grid .currency {
display: none;
}
} }
.money-app .capitalize { .money-app .capitalize {
text-transform: capitalize; text-transform: capitalize;