45 lines
1.4 KiB
TypeScript
45 lines
1.4 KiB
TypeScript
import { clsx, type ClassValue } from "clsx";
|
|
import { twMerge } from "tailwind-merge";
|
|
|
|
/** Merge conditional class names and de-duplicate Tailwind utilities. */
|
|
export function cn(...inputs: ClassValue[]) {
|
|
return twMerge(clsx(inputs));
|
|
}
|
|
|
|
/** Format a number of cents (e.g. 900) as a currency string ("$9"). */
|
|
export function formatPrice(cents: number, currency = "USD") {
|
|
const dollars = cents / 100;
|
|
return new Intl.NumberFormat("en-US", {
|
|
style: "currency",
|
|
currency,
|
|
maximumFractionDigits: dollars % 1 === 0 ? 0 : 2,
|
|
}).format(dollars);
|
|
}
|
|
|
|
/** The monthly usage bucket key, e.g. "2026-06". Pass a date for testability. */
|
|
export function periodKey(date: Date): string {
|
|
const y = date.getUTCFullYear();
|
|
const m = String(date.getUTCMonth() + 1).padStart(2, "0");
|
|
return `${y}-${m}`;
|
|
}
|
|
|
|
/**
|
|
* Returns `path` only if it is a safe same-origin relative path; otherwise
|
|
* falls back to "/dashboard". Guards against open-redirect attacks by rejecting
|
|
* protocol-relative ("//", "/\"), absolute ("https://…"), and backslash URLs.
|
|
*/
|
|
export function safeRedirect(path: string | null | undefined): string {
|
|
if (!path) return "/dashboard";
|
|
// Must be a single-slash-rooted relative path with no scheme or backslash escapes.
|
|
if (
|
|
!path.startsWith("/") ||
|
|
path.startsWith("//") ||
|
|
path.startsWith("/\\") ||
|
|
path.startsWith("\\") ||
|
|
path.includes("://")
|
|
) {
|
|
return "/dashboard";
|
|
}
|
|
return path;
|
|
}
|