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
1 change: 1 addition & 0 deletions middleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { getClientIp } from './utils/getClientIp';
* - /api/stats
* - /api/og
* - /api/notify
* - /api/compare
*
* Limit: 60 requests per minute per IP.
*/
Expand Down
41 changes: 41 additions & 0 deletions utils/dashboardPeriod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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'
Expand Down Expand Up @@ -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();

Expand Down
13 changes: 13 additions & 0 deletions utils/tracking.ts
Original file line number Diff line number Diff line change
@@ -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;
Expand Down
16 changes: 16 additions & 0 deletions utils/urls.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,28 @@
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 (
(typeof window !== 'undefined' ? window.location.origin : null) ?? envOrigin ?? FALLBACK_ORIGIN
);
}

/**
* 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)}`;
}
Loading