Update carousels, card hover, and IranBay branding.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,185 @@
|
||||
const API_BASE_URL =
|
||||
process.env.NEXT_PUBLIC_MESHKEE_API_URL?.replace(/\/$/, "") ||
|
||||
"http://localhost:3010/api/v1";
|
||||
|
||||
const ACCESS_TOKEN_KEY = "marketplace_access_token";
|
||||
const REFRESH_TOKEN_KEY = "marketplace_refresh_token";
|
||||
const TOKEN_MAX_AGE_SEC = 60 * 60 * 24 * 14; // 14 days
|
||||
|
||||
export class ApiError extends Error {
|
||||
status: number;
|
||||
|
||||
constructor(message: string, status: number) {
|
||||
super(message);
|
||||
this.name = "ApiError";
|
||||
this.status = status;
|
||||
}
|
||||
}
|
||||
|
||||
export function isAbortError(err: unknown): boolean {
|
||||
return err instanceof DOMException && err.name === "AbortError";
|
||||
}
|
||||
|
||||
/** Share cookies across admin / customer / business hosts on the same root. */
|
||||
function cookieDomain(): string | undefined {
|
||||
const root = (process.env.NEXT_PUBLIC_ROOT_DOMAIN || "localhost").trim();
|
||||
if (!root || root === "localhost" || root.startsWith("127.")) {
|
||||
return "localhost";
|
||||
}
|
||||
return root.startsWith(".") ? root : `.${root}`;
|
||||
}
|
||||
|
||||
function readCookie(name: string): string | null {
|
||||
if (typeof document === "undefined") return null;
|
||||
const prefix = `${name}=`;
|
||||
for (const part of document.cookie.split(";")) {
|
||||
const trimmed = part.trim();
|
||||
if (trimmed.startsWith(prefix)) {
|
||||
return decodeURIComponent(trimmed.slice(prefix.length));
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function writeCookie(name: string, value: string) {
|
||||
if (typeof document === "undefined") return;
|
||||
const domain = cookieDomain();
|
||||
const secure =
|
||||
typeof window !== "undefined" && window.location.protocol === "https:"
|
||||
? "; Secure"
|
||||
: "";
|
||||
document.cookie = `${name}=${encodeURIComponent(value)}; Path=/; Max-Age=${TOKEN_MAX_AGE_SEC}; SameSite=Lax; Domain=${domain}${secure}`;
|
||||
}
|
||||
|
||||
function deleteCookie(name: string) {
|
||||
if (typeof document === "undefined") return;
|
||||
const domain = cookieDomain();
|
||||
document.cookie = `${name}=; Path=/; Max-Age=0; SameSite=Lax; Domain=${domain}`;
|
||||
}
|
||||
|
||||
export function getAccessToken() {
|
||||
if (typeof window === "undefined") return null;
|
||||
return (
|
||||
readCookie(ACCESS_TOKEN_KEY) || localStorage.getItem(ACCESS_TOKEN_KEY)
|
||||
);
|
||||
}
|
||||
|
||||
export function getRefreshToken() {
|
||||
if (typeof window === "undefined") return null;
|
||||
return (
|
||||
readCookie(REFRESH_TOKEN_KEY) || localStorage.getItem(REFRESH_TOKEN_KEY)
|
||||
);
|
||||
}
|
||||
|
||||
export function setTokens(accessToken: string, refreshToken: string) {
|
||||
localStorage.setItem(ACCESS_TOKEN_KEY, accessToken);
|
||||
localStorage.setItem(REFRESH_TOKEN_KEY, refreshToken);
|
||||
writeCookie(ACCESS_TOKEN_KEY, accessToken);
|
||||
writeCookie(REFRESH_TOKEN_KEY, refreshToken);
|
||||
}
|
||||
|
||||
export function clearTokens() {
|
||||
localStorage.removeItem(ACCESS_TOKEN_KEY);
|
||||
localStorage.removeItem(REFRESH_TOKEN_KEY);
|
||||
deleteCookie(ACCESS_TOKEN_KEY);
|
||||
deleteCookie(REFRESH_TOKEN_KEY);
|
||||
}
|
||||
|
||||
/** Promote existing localStorage sessions into shared cookies (once per load). */
|
||||
export function migrateTokensToSharedCookies() {
|
||||
if (typeof window === "undefined") return;
|
||||
const access = localStorage.getItem(ACCESS_TOKEN_KEY);
|
||||
const refresh = localStorage.getItem(REFRESH_TOKEN_KEY);
|
||||
if (access && refresh && !readCookie(ACCESS_TOKEN_KEY)) {
|
||||
writeCookie(ACCESS_TOKEN_KEY, access);
|
||||
writeCookie(REFRESH_TOKEN_KEY, refresh);
|
||||
}
|
||||
}
|
||||
|
||||
function parseErrorMessage(payload: unknown, fallback: string) {
|
||||
if (!payload || typeof payload !== "object") {
|
||||
return fallback;
|
||||
}
|
||||
|
||||
const message = (payload as { message?: string | string[] }).message;
|
||||
|
||||
if (Array.isArray(message)) {
|
||||
return message.join(", ");
|
||||
}
|
||||
|
||||
if (typeof message === "string") {
|
||||
return message;
|
||||
}
|
||||
|
||||
return fallback;
|
||||
}
|
||||
|
||||
async function refreshAccessToken() {
|
||||
const refreshToken = getRefreshToken();
|
||||
if (!refreshToken) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const response = await fetch(`${API_BASE_URL}/auth/refresh`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ refreshToken }),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
clearTokens();
|
||||
return false;
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
setTokens(data.accessToken, data.refreshToken);
|
||||
return true;
|
||||
}
|
||||
|
||||
interface RequestOptions extends Omit<RequestInit, "body"> {
|
||||
body?: unknown;
|
||||
auth?: boolean;
|
||||
}
|
||||
|
||||
export async function apiRequest<T>(
|
||||
path: string,
|
||||
options: RequestOptions = {},
|
||||
): Promise<T> {
|
||||
const { body, auth = false, headers, ...rest } = options;
|
||||
const requestHeaders = new Headers(headers);
|
||||
|
||||
if (body !== undefined) {
|
||||
requestHeaders.set("Content-Type", "application/json");
|
||||
}
|
||||
|
||||
if (auth) {
|
||||
const accessToken = getAccessToken();
|
||||
if (accessToken) {
|
||||
requestHeaders.set("Authorization", `Bearer ${accessToken}`);
|
||||
}
|
||||
}
|
||||
|
||||
const response = await fetch(`${API_BASE_URL}${path}`, {
|
||||
...rest,
|
||||
headers: requestHeaders,
|
||||
body: body !== undefined ? JSON.stringify(body) : undefined,
|
||||
});
|
||||
|
||||
if (response.status === 401 && auth) {
|
||||
const refreshed = await refreshAccessToken();
|
||||
if (refreshed) {
|
||||
return apiRequest<T>(path, options);
|
||||
}
|
||||
}
|
||||
|
||||
const payload = await response.json().catch(() => null);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new ApiError(
|
||||
parseErrorMessage(payload, `Request failed with status ${response.status}`),
|
||||
response.status,
|
||||
);
|
||||
}
|
||||
|
||||
return payload as T;
|
||||
}
|
||||
Reference in New Issue
Block a user