Spreadsheet-UX op een echte relationele engine
Deze repository wordt als momentopname gepubliceerd: een clone bevat de volledige inhoud, maar geen commitgeschiedenis.
import { useEffect, useState } from 'react';
import { resolveVoice, type VoiceAssignment, type VoicePoint } from '@wellformed/shared';
import { api } from '../api/client';
/**
* The four selling points this visit gets, and the one beat we send back.
*
* Three things this deliberately is not.
*
* It is not a cookie. The assignment lives in `sessionStorage`, and the only
* thing that ever leaves the browser about it is the impression id, once, on
* the signup form. Nothing is written to the device that outlives the tab,
* nothing identifies anyone across visits, and there is no third party — which
* keeps a measurement that only needs to count pages out of consent territory
* entirely.
*
* It is not per-render. One assignment covers the whole tab, so opening
* `/pricing` from the hero does not reshuffle the points under someone mid-read,
* and a reload does not invent a second impression that never had a reader.
*
* It is not eager. The beat below is what separates a person from a crawler,
* and the fit only counts impressions that got one. Since the measure is
* durable use *per impression*, an unfiltered denominator would be mostly bots
* and every weight in the model would be noise around zero.
*/
const ASSIGNMENT_KEY = 'wf.voice';
const BEAT_KEY = 'wf.voice.seen';
/** How long a page has to be looked at before we believe someone is looking. */
const DWELL_MS = 4000;
function shaped(parsed: unknown): VoiceAssignment | null {
const value = parsed as Partial<VoiceAssignment>;
if (typeof value?.id !== 'string' || !Array.isArray(value.items)) return null;
return { id: value.id, items: value.items.filter((v) => typeof v === 'string') };
}
function readStored(): VoiceAssignment | null {
try {
const raw = sessionStorage.getItem(ASSIGNMENT_KEY);
if (!raw) return null;
return shaped(JSON.parse(raw));
} catch {
// Private browsing, a full quota, or something that is not JSON. The page
// still has to render, so every one of those is simply "no assignment".
return null;
}
}
/**
* The assignment a server-rendered page carried along, as a JSON script tag.
*
* The server already drew and recorded the impression when it rendered the
* page, so fetching another here would double-count the visit and swap four
* blocks of copy under the reader. Adopting the embedded one keeps the page
* and the measurement telling the same story. Stored on adoption, so the rest
* of the tab stays as sticky as a fetched assignment would be.
*/
function readEmbedded(): VoiceAssignment | null {
try {
const raw = document.getElementById('wf-voice')?.textContent;
if (!raw) return null;
const parsed = shaped(JSON.parse(raw));
if (parsed) store(ASSIGNMENT_KEY, JSON.stringify(parsed));
return parsed;
} catch {
return null;
}
}
function store(key: string, value: string): void {
try {
sessionStorage.setItem(key, value);
} catch {
// Storage being unavailable costs stickiness, not the page.
}
}
export function useVoice(ssr?: VoiceAssignment | null): VoicePoint[] {
// Order matters: an assignment this tab already had wins (stickiness), then
// the one a server-rendered page embedded, then — only during server-side
// rendering itself, where neither storage nor document exists — the one the
// caller was handed directly.
const [assignment, setAssignment] = useState<VoiceAssignment | null>(
() => readStored() ?? readEmbedded() ?? ssr ?? null,
);
useEffect(() => {
if (assignment) return;
let live = true;
void api
.voice()
.then((next) => {
if (!live) return;
store(ASSIGNMENT_KEY, JSON.stringify(next));
setAssignment(next);
})
.catch(() => {
// The server not answering must not blank the landing page. Leaving the
// assignment null renders the deterministic fallback in `resolveVoice`.
});
return () => {
live = false;
};
}, [assignment]);
useEffect(() => {
const id = assignment?.id;
// An empty id is the server saying "composed but not recorded" — over the
// rate limit. There is nothing to beat against.
if (!id) return;
if (sessionStorage.getItem(BEAT_KEY) === id) return;
let done = false;
const beat = () => {
if (done) return;
done = true;
cleanup();
store(BEAT_KEY, id);
void api.voiceSeen(id).catch(() => {
// Nothing to tell the reader, and nothing to retry: an impression that
// failed to register simply does not vote.
});
};
// Either signal will do. A quick scroll is as good a sign of a person as
// four seconds of stillness, and waiting for both would lose the visitor
// who reads the hero and leaves.
const events = ['scroll', 'pointerdown', 'keydown'] as const;
for (const event of events) window.addEventListener(event, beat, { once: true, passive: true });
const timer = window.setTimeout(beat, DWELL_MS);
function cleanup() {
window.clearTimeout(timer);
for (const event of events) window.removeEventListener(event, beat);
}
return cleanup;
}, [assignment?.id]);
return resolveVoice(assignment?.items ?? []);
}
/** The impression to credit a signup to, if this tab has one. */
export function currentVoiceId(): string | undefined {
return readStored()?.id || undefined;
}