pc tracker: Megekko new build (9950X3D/X870E), dated ordered/leaving badges, future slider
All checks were successful
Deploy production / deploy (push) Successful in 1m15s

This commit is contained in:
2026-10-25 23:59:15 +00:00
parent cf0fec973c
commit 3d3f008037
33 changed files with 166 additions and 12 deletions

View File

@@ -49,7 +49,7 @@
: names
);
const spec = $derived(keySpec(first));
const badges = $derived([...new Set(parts.flatMap(usageBadges))]);
const badges = $derived([...new Set(parts.flatMap((p) => usageBadges(p)))]);
const flag = $derived(badges.find((b) => b !== 'long ago') ?? null);
const popId = $derived(`pop-${first.id}`);
</script>
@@ -69,7 +69,7 @@
>
{/if}
{#if compact && flag}<span class="flag" class:warn={flag === 'model TBD'}
>{flag.replace('model ', '')}</span
>{flag.replace('model ', '').replace(/^ordered, arriving/, 'arrives')}</span
>{/if}
<span class="min-w-0 flex-1">
<span class="label">

View File

@@ -9,8 +9,16 @@
min,
max,
events,
today = max,
value = $bindable()
}: { min: string; max: string; events: TimelineEvent[]; value: string } = $props();
}: {
min: string;
/** Last day on the slider; later than `today` when parts are ordered but not here yet. */
max: string;
today?: string;
events: TimelineEvent[];
value: string;
} = $props();
const DAY = 86_400_000;
const toDay = (d: string) => Math.round(Date.parse(`${d}T00:00:00Z`) / DAY);
@@ -22,7 +30,12 @@
const ticks = $derived(
events
.filter((e) => e.date >= min && e.date <= max)
.map((e) => ({ ...e, day: toDay(e.date), pct: ((toDay(e.date) - lo) / span) * 100 }))
.map((e) => ({
...e,
day: toDay(e.date),
pct: ((toDay(e.date) - lo) / span) * 100,
upcoming: e.date > today
}))
);
const years = $derived.by(() => {
const out: { y: number; pct: number }[] = [];
@@ -31,6 +44,8 @@
return out;
});
const current = $derived(toDay(value));
/** Where today sits on the track when the slider runs into the future (ordered parts). */
const nowPct = $derived(today < max ? ((toDay(today) - lo) / span) * 100 : null);
/** Latest change on or before the chosen day. */
const last = $derived([...ticks].reverse().find((t) => t.day <= current) ?? null);
@@ -66,7 +81,9 @@
<span class="icon" aria-hidden="true"><History size={16} /></span>
<div class="track order-last sm:order-none" bind:clientWidth={width}>
<div class="rail"></div>
{#if nowPct !== null}<div class="future" style:left="{nowPct}%"></div>{/if}
<div class="fill" style:width="{((current - lo) / span) * 100}%"></div>
{#if nowPct !== null}<span class="now" style:left="{nowPct}%" title="today"></span>{/if}
{#each years.filter((y, i) => width > 520 || i % 2 === 0) as y (y.y)}
<span class="year" style:left="{y.pct}%">{y.y}</span>
{/each}
@@ -76,6 +93,7 @@
class="tick"
class:past={t.day <= current}
class:on={t.day === current}
class:upcoming={t.upcoming}
style:left="{t.pct}%"
aria-label="{shortDate(t.date)}: {eventLabel(t)}"
onmouseenter={() => (hover = t)}
@@ -105,13 +123,14 @@
</div>
<div class="when ml-auto sm:ml-0">
<span class="date tabular-nums">{shortDate(value)}</span>
<button type="button" class="today" disabled={value === max} onclick={() => (value = max)}
<button type="button" class="today" disabled={value === today} onclick={() => (value = today)}
><RotateCcw size={13} /> today</button
>
</div>
</div>
<p class="last" aria-live="polite">
{#if last}
{#if value > today}<span class="soon">upcoming</span> ·
{/if}{#if last}
<span class="text-[var(--site-text)]">{monthYear(last.date)}</span> · {eventLabel(last)}
{:else}
before the first part
@@ -199,6 +218,36 @@
background: var(--site-text);
opacity: 0.9;
}
.tick.upcoming {
background: var(--site-bg);
box-shadow: inset 0 0 0 1.5px #db2777;
opacity: 1;
}
.future {
position: absolute;
top: 0.55rem;
right: 0;
height: 0.3rem;
border-radius: 999px;
background: repeating-linear-gradient(90deg, #db2777 0 3px, transparent 3px 7px);
opacity: 0.55;
}
.now {
position: absolute;
top: 0.2rem;
z-index: 1;
width: 2px;
height: 1rem;
margin-left: -1px;
border-radius: 1px;
background: var(--site-text);
opacity: 0.7;
pointer-events: none;
}
.soon {
color: #db2777;
font-weight: 600;
}
.tick.on,
.tick:hover,
.tick:focus-visible {

View File

@@ -1,5 +1,5 @@
import { describe, expect, it } from 'vitest';
import { cmFor, sensFor, specsToText, textToSpecs } from './shared';
import { cmFor, sensFor, specsToText, textToSpecs, usageBadges } from './shared';
describe('pc tracker helpers', () => {
it('computes CS2 sensitivity from cm/360 and real DPI', () => {
@@ -14,4 +14,16 @@ describe('pc tracker helpers', () => {
expect(textToSpecs(specsToText(specs))).toEqual(specs);
expect(textToSpecs('no colon here\n: empty key\nspeed_mts: 3600')).toEqual({ speed_mts: 3600 });
});
it('dates the badges of ordered and outgoing parts', () => {
const base = { status: 'in_use', usage: 'current', model: 'X' };
const ordered = { ...base, installedAt: '2026-10-28', orderDate: '2026-10-26' };
expect(usageBadges(ordered, '2026-10-26')).toEqual(['ordered, arriving Oct 28']);
expect(usageBadges(ordered, '2026-10-28')).toEqual([]);
expect(usageBadges({ ...base, installedAt: '2026-10-28' }, '2026-10-26')).toEqual([
'from Oct 28'
]);
const old = { status: 'retired', usage: 'previous', model: 'Y', removedAt: '2026-10-28' };
expect(usageBadges(old, '2026-10-26')).toEqual(['until Oct 28']);
expect(usageBadges(old, '2026-10-29')).toEqual(['previous']);
});
});

View File

@@ -430,11 +430,43 @@ export type LightboxEntry = {
};
export type LightboxItem = { title: string; images: string[]; entries: LightboxEntry[] };
export function usageBadges(p: { usage?: string | null; status: string; model: string | null }) {
/** Today as YYYY-MM-DD (UTC, the same day the page and slider use). */
export const todayIso = () => new Date().toISOString().slice(0, 10);
/** "2026-10-28" -> "Oct 28". */
export const monthDay = (d: string) =>
new Date(`${d}T12:00:00Z`).toLocaleDateString('en-US', { month: 'short', day: 'numeric' });
/**
* Card badges. Date-aware: a part with a future install date is "ordered, arriving <day>" (or
* "from <day>" when it is not a purchase, e.g. a relabelled drive), and a part with a future
* removal date is still in use, so it shows "until <day>" instead of "previous".
*/
export function usageBadges(
p: {
usage?: string | null;
status: string;
model: string | null;
installedAt?: string | null;
removedAt?: string | null;
orderDate?: string | null;
},
today: string = todayIso()
) {
const out: string[] = [];
if (p.usage === 'stored' || p.status === 'spare') out.push('stored');
const arriving = p.installedAt && p.installedAt > today ? p.installedAt : null;
const leaving = p.removedAt && p.removedAt > today ? p.removedAt : null;
if (arriving)
out.push(
p.orderDate ? `ordered, arriving ${monthDay(arriving)}` : `from ${monthDay(arriving)}`
);
if (leaving) out.push(`until ${monthDay(leaving)}`);
if (!leaving && (p.usage === 'stored' || p.status === 'spare')) out.push('stored');
if (p.usage === 'work-issued') out.push('work');
if (p.usage === 'previous' || p.usage === 'legacy-previous' || p.status === 'retired')
if (
!leaving &&
(p.usage === 'previous' || p.usage === 'legacy-previous' || p.status === 'retired')
)
out.push('previous');
if (p.usage === 'legacy') out.push('long ago');
if (p.model && /\bTBD\b/.test(p.model)) out.push('model TBD');