Phase 2: core converter, live conversion, URL state, highlight interaction
This commit is contained in:
@@ -1 +1,122 @@
|
||||
<h1>Humor Units</h1>
|
||||
<script lang="ts">
|
||||
import { browser } from '$app/environment';
|
||||
import { goto } from '$app/navigation';
|
||||
import { page } from '$app/state';
|
||||
import { florpUnits } from '$lib/units/definitions';
|
||||
import { convertById } from '$lib/convert';
|
||||
import { FRACTIONAL_DIGITS } from '$lib/config';
|
||||
import { formatBig } from '$lib/format';
|
||||
import type { Unit, ResultItem } from '$lib/types';
|
||||
import ConverterCard from '../components/ConverterCard.svelte';
|
||||
|
||||
/**
|
||||
* Converter page — owns all reactive state.
|
||||
* Phase 2: live conversion, URL state, highlight interaction.
|
||||
* Phase 3 will add NL input.
|
||||
*/
|
||||
|
||||
const units: Unit[] = florpUnits;
|
||||
|
||||
// ── Reactive state ──────────────────────────────────────────────────────────
|
||||
let inputValue = $state<number>(1);
|
||||
let fromUnitId = $state<string>(units[0].id);
|
||||
let highlightedUnitId = $state<string | null>(null);
|
||||
|
||||
// ── Initialize from URL params on first client-side load ───────────────────
|
||||
if (browser) {
|
||||
const params = page.url.searchParams;
|
||||
const pValue = params.get('value');
|
||||
const pFrom = params.get('from');
|
||||
const pHighlight = params.get('highlight');
|
||||
|
||||
if (pValue !== null) {
|
||||
const n = parseFloat(pValue);
|
||||
if (!isNaN(n)) inputValue = n;
|
||||
}
|
||||
if (pFrom !== null && units.some((u) => u.id === pFrom)) {
|
||||
fromUnitId = pFrom;
|
||||
}
|
||||
if (pHighlight !== null && units.some((u) => u.id === pHighlight)) {
|
||||
highlightedUnitId = pHighlight;
|
||||
}
|
||||
}
|
||||
|
||||
// ── Derived: results for all units except fromUnit ─────────────────────────
|
||||
let results = $derived<ResultItem[]>(
|
||||
units
|
||||
.filter((u) => u.id !== fromUnitId)
|
||||
.map((u) => ({
|
||||
unit: u,
|
||||
convertedValue: convertById(inputValue, fromUnitId, u.id, units)
|
||||
}))
|
||||
);
|
||||
|
||||
// ── Derived: highlighted result (unit + value) sourced from results array ──
|
||||
// The highlighted unit is always a non-from unit, so it's always in results.
|
||||
let highlightedResult = $derived(
|
||||
highlightedUnitId !== null
|
||||
? (results.find((r) => r.unit.id === highlightedUnitId) ?? null)
|
||||
: null
|
||||
);
|
||||
|
||||
// ── URL sync: update URL only when serialized params actually change ────────
|
||||
$effect(() => {
|
||||
if (!browser) return;
|
||||
const params = new URLSearchParams();
|
||||
params.set('value', String(inputValue));
|
||||
params.set('from', fromUnitId);
|
||||
if (highlightedUnitId !== null) {
|
||||
params.set('highlight', highlightedUnitId);
|
||||
}
|
||||
const next = params.toString();
|
||||
if (next !== window.location.search.slice(1)) {
|
||||
goto(`?${next}`, { replaceState: true, noScroll: true });
|
||||
}
|
||||
});
|
||||
|
||||
// ── Event handlers ──────────────────────────────────────────────────────────
|
||||
function handleUnitChange(id: string) {
|
||||
fromUnitId = id;
|
||||
// Clear highlight if the selected unit becomes the new from-unit.
|
||||
if (highlightedUnitId === id) {
|
||||
highlightedUnitId = null;
|
||||
}
|
||||
}
|
||||
|
||||
function handleHighlight(id: string) {
|
||||
highlightedUnitId = id === highlightedUnitId ? null : id;
|
||||
}
|
||||
|
||||
function handleSetInput(id: string) {
|
||||
// Shift+click: promote result unit to from-unit.
|
||||
// Re-express the current value in terms of the new from-unit.
|
||||
const convertedValue = convertById(inputValue, fromUnitId, id, units);
|
||||
inputValue = parseFloat(convertedValue.toFixed(FRACTIONAL_DIGITS));
|
||||
fromUnitId = id;
|
||||
highlightedUnitId = null;
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>Humor Units — Converter</title>
|
||||
</svelte:head>
|
||||
|
||||
{#if highlightedResult !== null}
|
||||
<div class="highlighted-result">
|
||||
<span class="highlighted-value">{formatBig(highlightedResult.convertedValue)}</span>
|
||||
<span class="highlighted-label">{highlightedResult.unit.labelPlural}</span>
|
||||
<span class="highlighted-symbol">{highlightedResult.unit.symbol}</span>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<ConverterCard
|
||||
{units}
|
||||
{inputValue}
|
||||
{fromUnitId}
|
||||
{highlightedUnitId}
|
||||
{results}
|
||||
oninputchange={(v) => (inputValue = v)}
|
||||
onunitchange={handleUnitChange}
|
||||
onhighlight={handleHighlight}
|
||||
onsetinput={handleSetInput}
|
||||
/>
|
||||
|
||||
Reference in New Issue
Block a user