Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 9 additions & 30 deletions src/components/dashboard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ import {
type Provider,
type ProviderId,
} from "@/lib/mock-data";
import { applyReplayUrlState, parseReplayUrlState } from "@/lib/replay-url";
import { cn, formatCurrency, formatMs, formatNumber } from "@/lib/utils";

const ranges = ["24h", "7d", "30d"] as const;
Expand Down Expand Up @@ -176,34 +177,17 @@ function downloadCsv(filename: string, csv: string) {
window.setTimeout(() => URL.revokeObjectURL(url), 0);
}

const REPLAY_PARAM = "replay";
const STEP_PARAM = "step";
const replayStepCounts = new Map(
replayScenarios.map((scenario) => [scenario.id, scenario.steps.length]),
);

/** Read an incoming shareable replay link from the URL (client only). */
function readReplayParams(): { scenarioId: string | null; step: number } {
if (typeof window === "undefined") {
return { scenarioId: null, step: 0 };
}

const params = new URLSearchParams(window.location.search);
const replay = params.get(REPLAY_PARAM);

if (!replay) {
return { scenarioId: null, step: 0 };
}

const scenario = replayScenarios.find((item) => item.id === replay);

if (!scenario) {
return { scenarioId: null, step: 0 };
}

const rawStep = Number(params.get(STEP_PARAM) ?? "0");
const step = Number.isFinite(rawStep)
? Math.max(0, Math.min(Math.floor(rawStep), scenario.steps.length - 1))
: 0;

return { scenarioId: replay, step };
return parseReplayUrlState(window.location.search, replayStepCounts);
}

/** Keep the URL in sync with the active replay (no history entries, no scroll). */
Expand All @@ -212,15 +196,10 @@ function syncReplayUrl(scenarioId: string | null, step: number) {
return;
}

const url = new URL(window.location.href);

if (scenarioId) {
url.searchParams.set(REPLAY_PARAM, scenarioId);
url.searchParams.set(STEP_PARAM, String(step));
} else {
url.searchParams.delete(REPLAY_PARAM);
url.searchParams.delete(STEP_PARAM);
}
const url = applyReplayUrlState(new URL(window.location.href), {
scenarioId,
step,
});

window.history.replaceState(window.history.state, "", url);
}
Expand Down
56 changes: 56 additions & 0 deletions src/lib/replay-url.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
export type ReplayUrlState = {
scenarioId: string | null;
step: number;
};

export const REPLAY_PARAM = "replay";
export const STEP_PARAM = "step";

type ReplayStepCounts = ReadonlyMap<string, number> | Record<string, number>;

function getStepCount(stepCounts: ReplayStepCounts, scenarioId: string): number | undefined {
if ("get" in stepCounts && typeof stepCounts.get === "function") {
return stepCounts.get(scenarioId);
}

return (stepCounts as Record<string, number>)[scenarioId];
}

export function parseReplayUrlState(
search: string | URLSearchParams,
stepCounts: ReplayStepCounts,
): ReplayUrlState {
const params = typeof search === "string" ? new URLSearchParams(search) : search;
const scenarioId = params.get(REPLAY_PARAM);

if (!scenarioId) {
return { scenarioId: null, step: 0 };
}

const stepCount = getStepCount(stepCounts, scenarioId);

if (!stepCount || stepCount < 1) {
return { scenarioId: null, step: 0 };
}

const rawStep = Number(params.get(STEP_PARAM) ?? "0");
const step = Number.isFinite(rawStep)
? Math.max(0, Math.min(Math.floor(rawStep), stepCount - 1))
: 0;

return { scenarioId, step };
}

export function applyReplayUrlState(url: URL, state: ReplayUrlState): URL {
const nextUrl = new URL(url.href);

if (state.scenarioId) {
nextUrl.searchParams.set(REPLAY_PARAM, state.scenarioId);
nextUrl.searchParams.set(STEP_PARAM, String(state.step));
} else {
nextUrl.searchParams.delete(REPLAY_PARAM);
nextUrl.searchParams.delete(STEP_PARAM);
}

return nextUrl;
}