mirror of
https://github.com/fosrl/docs-v2.git
synced 2026-09-30 17:59:10 +02:00
92 lines
2.9 KiB
TypeScript
92 lines
2.9 KiB
TypeScript
const DAY = 86_400_000;
|
|
|
|
/**
|
|
* The Fossorial API deletes docs analytics older than this (api repo,
|
|
* src/controllers/docsAnalytics/retention.ts), so no range reaches further back.
|
|
*/
|
|
export const RETENTION_DAYS = 90;
|
|
|
|
export const presets = [
|
|
{ id: '24h', label: '24 hours', ms: DAY },
|
|
{ id: '7d', label: '7 days', ms: 7 * DAY },
|
|
{ id: '30d', label: '30 days', ms: 30 * DAY },
|
|
{ id: '90d', label: '90 days', ms: RETENTION_DAYS * DAY },
|
|
] as const;
|
|
|
|
/** first day that can still have data, as `YYYY-MM-DD` (for date inputs) */
|
|
export function oldestDay(now = Date.now()) {
|
|
return formatDay(now - RETENTION_DAYS * DAY);
|
|
}
|
|
|
|
export type SearchParams = Record<string, string | string[] | undefined>;
|
|
|
|
export function param(params: SearchParams, key: string) {
|
|
const value = params[key];
|
|
return (Array.isArray(value) ? value[0] : value)?.trim() || undefined;
|
|
}
|
|
|
|
/** `YYYY-MM-DD` as UTC midnight */
|
|
function parseDay(value: string | undefined) {
|
|
if (!value || !/^\d{4}-\d{2}-\d{2}$/.test(value)) return undefined;
|
|
const t = Date.parse(`${value}T00:00:00Z`);
|
|
return Number.isNaN(t) ? undefined : t;
|
|
}
|
|
|
|
export function formatDay(t: number) {
|
|
return new Date(t).toISOString().slice(0, 10);
|
|
}
|
|
|
|
/**
|
|
* `?range=7d` picks a preset (default 30 days); `?from=2026-01-01&to=2026-01-31` is a
|
|
* custom range in UTC with both days included, clamped to the retention window.
|
|
*/
|
|
export function resolveRange(params: SearchParams, now = Date.now()) {
|
|
const from = parseDay(param(params, 'from'));
|
|
const to = parseDay(param(params, 'to'));
|
|
if (from !== undefined || to !== undefined) {
|
|
const floor = now - RETENTION_DAYS * DAY;
|
|
const end = to !== undefined ? to + DAY : now + 1;
|
|
const start = Math.max(Math.min(from ?? floor, end), floor);
|
|
return {
|
|
preset: null,
|
|
from: start,
|
|
to: Math.max(start, end),
|
|
label: `${formatDay(start)} to ${to !== undefined ? formatDay(to) : 'now'}`,
|
|
};
|
|
}
|
|
|
|
const preset = presets.find((p) => p.id === param(params, 'range')) ?? presets[2];
|
|
return {
|
|
preset: preset.id,
|
|
from: now - preset.ms,
|
|
to: now + 1,
|
|
label: `Last ${preset.label}`,
|
|
};
|
|
}
|
|
|
|
/** a query string with some keys replaced; `undefined` drops a key */
|
|
export function withParams(params: SearchParams, changes: Record<string, string | undefined>) {
|
|
const next = new URLSearchParams();
|
|
for (const [key, value] of Object.entries(params)) {
|
|
const v = Array.isArray(value) ? value[0] : value;
|
|
if (v && !(key in changes)) next.set(key, v);
|
|
}
|
|
for (const [key, value] of Object.entries(changes)) if (value) next.set(key, value);
|
|
const qs = next.toString();
|
|
return qs ? `?${qs}` : '?';
|
|
}
|
|
|
|
const dateTime = new Intl.DateTimeFormat('en-US', {
|
|
month: 'short',
|
|
day: 'numeric',
|
|
year: 'numeric',
|
|
hour: '2-digit',
|
|
minute: '2-digit',
|
|
hour12: false,
|
|
timeZone: 'UTC',
|
|
});
|
|
|
|
export function formatDateTime(t: number) {
|
|
return `${dateTime.format(t)} UTC`;
|
|
}
|