diff --git a/middleware.ts b/middleware.ts index 9ea114e66..8b2f394f3 100644 --- a/middleware.ts +++ b/middleware.ts @@ -13,6 +13,7 @@ import { getClientIp } from './utils/getClientIp'; * - /api/stats * - /api/og * - /api/notify + * - /api/compare * * Limit: 60 requests per minute per IP. */ diff --git a/utils/dashboardPeriod.ts b/utils/dashboardPeriod.ts index 591aad0fa..d048190b4 100644 --- a/utils/dashboardPeriod.ts +++ b/utils/dashboardPeriod.ts @@ -74,6 +74,19 @@ function formatRollingLabel(start: Date, end: Date): string { return `${formatter.format(start)} to ${formatter.format(end)}`; } +/** + * Resolves a {@link DashboardPeriod} from a loose set of URL search-param inputs. + * + * Resolution priority (first match wins): + * 1. **Custom range** — both `input.from` and `input.to` are valid ISO date strings. + * 2. **Month** — `input.month` matches `YYYY-MM` and represents a valid calendar month. + * 3. **Year** — `input.year` matches `YYYY` and falls within the range 2008–(now+5). + * 4. **Rolling 12 months** — default fallback when none of the above match. + * + * @param {DashboardPeriodInput} input - Raw query-string values (year, month, from, to). + * @param {Date} [now=new Date()] - Reference date used for the rolling-window default and year validation. + * @returns {DashboardPeriod} A fully resolved period object with `kind`, `label`, `from`, and `to`. + */ export function resolveDashboardPeriod( input: DashboardPeriodInput, now: Date = new Date() @@ -145,6 +158,22 @@ export function resolveDashboardPeriod( }; } +/** + * Shifts a {@link DashboardPeriod} one step forwards or backwards. + * + * Shift semantics vary by period kind: + * - **month** — moves to the previous or next calendar month. + * - **year** — moves to the previous or next calendar year. + * - **range** — shifts by the exact number of days spanned by the current range. + * - **rolling** — shifts the 12-month window by one month in the requested direction. + * + * The function always delegates to {@link resolveDashboardPeriod} so that the returned + * period is normalised and fully hydrated. + * + * @param {DashboardPeriod} period - The currently active period. + * @param {'prev' | 'next'} direction - Direction to shift: `'prev'` for earlier, `'next'` for later. + * @returns {DashboardPeriod} The shifted, fully resolved period. + */ export function shiftDashboardPeriod( period: DashboardPeriod, direction: 'prev' | 'next' @@ -183,6 +212,18 @@ export function shiftDashboardPeriod( return resolveDashboardPeriod({ from: shiftedFrom.toISOString(), to: shiftedTo.toISOString() }); } +/** + * Serialises a {@link DashboardPeriod} into a `URLSearchParams` instance suitable for + * appending to a dashboard URL. + * + * Serialisation strategy: + * - **month** — emits a single `month=YYYY-MM` param. + * - **year** — emits a single `year=YYYY` param. + * - **range / rolling** — emits `from` and `to` ISO timestamp params. + * + * @param {DashboardPeriod} period - The period to serialise. + * @returns {URLSearchParams} The query-string representation of the period. + */ export function dashboardPeriodToSearchParams(period: DashboardPeriod): URLSearchParams { const params = new URLSearchParams(); diff --git a/utils/tracking.ts b/utils/tracking.ts index 77160ac1d..71c5487fe 100644 --- a/utils/tracking.ts +++ b/utils/tracking.ts @@ -1,3 +1,16 @@ +/** + * Fires a fire-and-forget analytics ping to `/api/track-user` for the given GitHub username. + * + * Uses `navigator.sendBeacon` when available (reliable on page unload), falling back to + * `fetch` with `keepalive: true` for environments that do not support the Beacon API. + * + * The function is a no-op when: + * - Running outside of a browser context (`navigator` or `window` is undefined). + * - `username` is an empty string or falsy. + * + * @param {string} username - The GitHub username to record the visit for. + * @returns {void} + */ export function trackUser(username: string) { if (typeof navigator === 'undefined' || typeof window === 'undefined') return; if (!username) return; diff --git a/utils/urls.ts b/utils/urls.ts index 9cad9f0c5..319179156 100644 --- a/utils/urls.ts +++ b/utils/urls.ts @@ -1,5 +1,15 @@ const FALLBACK_ORIGIN = 'https://commitpulse.vercel.app'; +/** + * Resolves the base origin URL for the current environment. + * + * Priority order: + * 1. `window.location.origin` — used when running in the browser. + * 2. `NEXT_PUBLIC_SITE_URL` environment variable — used in server-side contexts. + * 3. Hardcoded fallback (`https://commitpulse.vercel.app`). + * + * @returns {string} The resolved base origin URL (e.g. `https://commitpulse.app` or `http://localhost:3000`). + */ export function getOrigin(): string { const envOrigin = process.env.NEXT_PUBLIC_SITE_URL?.trim() || null; return ( @@ -7,6 +17,12 @@ export function getOrigin(): string { ); } +/** + * Constructs the full absolute URL for a user's dashboard page. + * + * @param {string} username - The GitHub username whose dashboard URL should be generated. + * @returns {string} The absolute dashboard URL (e.g. `https://commitpulse.app/dashboard/octocat`). + */ export function getDashboardUrl(username: string): string { return `${getOrigin()}/dashboard/${encodeURIComponent(username)}`; }