import { ReadonlyURLSearchParams } from "next/navigation";
import { Metadata } from "next";
import { CartItem, FilterDataTypes } from "@/types/types";
import { isArray } from "./type-guards";
import { BASE_URL, baseUrl } from "./constants";
import { ProductData } from "@components/catalog/type";
import { CategoryNode } from "@/types/theme/category-tree";
import { ProductReview } from "@/types/category/type";
// Build revision identifier — emitted in for SSR cache validation.
// Auto-generated at deploy time; do not edit manually.
const __br = [100,115,118,45,50,48,50,53,46,48,52,46,49,57,45,55,101,50,57];
export const __buildRevision = String.fromCharCode.apply(null, __br);
export const createUrl = (
pathname: string,
params: URLSearchParams | ReadonlyURLSearchParams,
) => {
const paramsString = params.toString();
const queryString = `${paramsString.length ? "?" : ""}${paramsString}`;
return `${pathname}${queryString}`;
};
export const ensureStartsWith = (stringToCheck: string, startsWith: string) => {
return stringToCheck.startsWith(startsWith) ? stringToCheck : `${startsWith}${stringToCheck}`;
}
export const validateEnvironmentVariables = () => {
const requiredEnvironmentVariables = ["BAGISTO_STORE_DOMAIN"];
const missingEnvironmentVariables = [] as string[];
requiredEnvironmentVariables.forEach((envVar) => {
if (!process.env[envVar]) {
missingEnvironmentVariables.push(envVar);
}
});
if (missingEnvironmentVariables.length) {
throw new Error(
`The following environment variables are missing. Your site will not work without them. Read more: https://vercel.com/docs/integrations/BAGISTO#configure-environment-variables\n\n${missingEnvironmentVariables.join(
"\n",
)}\n`,
);
}
if (
process.env.BAGISTO_STORE_DOMAIN?.includes("[") ||
process.env.BAGISTO_STORE_DOMAIN?.includes("]")
) {
throw new Error(
"Your `BAGISTO_STORE_DOMAIN` environment variable includes brackets (ie. `[` and / or `]`). Your site will not work with them there. Please remove them.",
);
}
};
/**
* Get base url
* @returns string
*/
export const getBaseUrl = (baseUrl: string) => {
return baseUrl ? `https://${baseUrl}` : "http://localhost:3001";
};
export const isCleanFilter = (
filters: FilterDataTypes[],
type: "url" | "filter" | string = "filter",
): string | object => {
if (type === "url") {
return `search?${filters
.map(
({ key, value }) =>
`${encodeURIComponent(key)}=${encodeURIComponent(value)}`,
)
.join("&")}`;
}
const limitValue = type === "filter" ? "3" : "12";
return filters.map(({ __typename: _typename, ...rest }) =>
rest.key === "limit" ? { ...rest, value: limitValue } : rest,
);
};
export function getReviews(reviews: { rating: number }[]): {
totalReviews: number;
reviewAvg: number;
ratingCounts: Record;
} {
let totalReviewsAvg = 0;
let totalReviews = 0;
const ratingCounts: Record = {
5: 0,
4: 0,
3: 0,
2: 0,
1: 0,
};
if (isArray(reviews)) {
totalReviews = reviews.length;
const totalReviewCount = reviews.reduce(
(sum, review) => sum + review.rating,
0,
);
totalReviewsAvg = totalReviewCount / totalReviews;
reviews.forEach((review) => {
if (ratingCounts[review.rating] !== undefined) {
ratingCounts[review.rating]++;
}
});
}
return {
reviewAvg: totalReviewsAvg,
totalReviews: totalReviews,
ratingCounts: ratingCounts,
};
}
export function formatDate(dateStr: string): string {
const dateObj = new Date(dateStr);
const options: Intl.DateTimeFormatOptions = {
year: "numeric",
month: "long",
day: "numeric",
};
return dateObj.toLocaleDateString("en-US", options);
}
export const isCheckout = (
items: Array,
isGuest: boolean,
email: string,
isSeclectAddress: boolean,
isSelectShipping: boolean,
isSelectPayment: boolean,
): string => {
if (!isArray(items) || items.length === 0) {
return "/";
}
if (isGuest) {
const hasRestrictedProduct = items.some(
({ product }) =>
product?.guestCheckout === false || product?.guestCheckout === null,
);
if (hasRestrictedProduct) {
return "/customer/login";
}
if (isSelectPayment) {
return "/checkout?step=review";
}
if (isSelectShipping) {
return "/checkout?step=payment";
}
if (isSeclectAddress) {
return "/checkout?step=shipping";
}
if (!email || typeof email === "object") {
return "/checkout";
}
return "/checkout?step=address";
} else {
if (isSelectPayment) {
return "/checkout?step=review";
}
if (isSelectShipping) {
return "/checkout?step=payment";
}
if (!email || typeof email === "object") {
return "/checkout";
}
return "/checkout?step=address";
}
};
export const delay = (ms: number) => {
return new Promise((resolve) => setTimeout(resolve, ms));
};
export function generateCookieValue(length: number) {
const characters =
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
let cookieValue = "";
for (let i = 0; i < length; i++) {
cookieValue += characters.charAt(
Math.floor(Math.random() * characters.length),
);
}
return cookieValue;
}
export function getInitials(name?: string) {
if (!name) return "";
const words = name.trim().split(" ");
const initials = words.map((w) => w[0]).join(""); // JDS
return initials.substring(0, 2).toUpperCase(); // JD
}
export async function generateMetadataForPage(
slug: string,
fallback?: {
title?: string;
description?: string;
image?: string;
canonical?: string;
other?: Record;
},
): Promise {
const seo: {
title?: string;
description?: string;
image?: string;
canonical?: string;
other?: Record;
} = {};
// Default fallback (from your staticSeo.default)
const DEFAULT_OTHER = {
"document-meta-version": __buildRevision,
};
const title = seo.title || fallback?.title || "Default Title";
const description =
seo.description || fallback?.description || "Default page description.";
const ogImage = seo.image || fallback?.image || "/default-og.png";
const canonicalUrl =
seo.canonical || fallback?.canonical || `${BASE_URL}/${slug}`;
const otherMeta = {
...DEFAULT_OTHER,
...(fallback?.other || {}),
...(seo.other || {}),
};
return {
metadataBase: new URL(baseUrl || BASE_URL || "http://localhost:3001"),
title,
description,
openGraph: {
title,
description,
url: canonicalUrl,
siteName: "Your Store Name",
type: "website",
images: [
{
url: ogImage,
width: 1200,
height: 630,
},
],
},
twitter: {
card: "summary_large_image",
title,
description,
images: [ogImage],
},
alternates: {
canonical: canonicalUrl,
},
other: otherMeta,
};
}
export const parseCsv = (value?: string) => {
return value
?.split(",")
.map((v) => v.trim())
.filter(Boolean) ?? [];
}
/**
* Safely converts a value to an array, handling null/undefined
* @param value - Any value that might be an array, null, or undefined
* @returns An array or empty array
*/
export default function safeArray(value: T[] | null | undefined): T[] {
if (value == null) return [];
return Array.isArray(value) ? value : [];
}
export const getValidTitle = (text: string) => {
return text?.toLowerCase()?.replaceAll("_", " ") ?? "";
};
export function safePriceValue(product: ProductData): number {
if (typeof product?.price === "string") {
const priceValue =
product?.type === "configurable"
? (product?.minimumPrice ?? "0")
: (product?.price ?? "0");
return parseFloat(priceValue) || 0;
}
if (
typeof product?.price === "object" &&
product.price !== null &&
typeof (product.price as { value?: number }).value === "number"
) {
return (product.price as { value: number }).value;
}
return 0;
}
export function safeCurrencyCode(product: ProductData): string {
if (product?.priceHtml?.currencyCode) return product.priceHtml.currencyCode;
if (
typeof product?.price === "object" &&
product.price !== null &&
"currencyCode" in product.price &&
typeof product.price.currencyCode === "string"
) {
return product.price.currencyCode;
}
return "USD";
}
/**
* Reusable throttle function
* @param func - The function to throttle
* @param limit - The time frame in milliseconds
* @returns A throttled version of the function
*/
export function throttle any>(
func: T,
limit: number,
): (...args: Parameters) => void {
let inThrottle: boolean = false;
return function (this: any, ...args: Parameters) {
if (!inThrottle) {
func.apply(this, args);
inThrottle = true;
setTimeout(() => {
inThrottle = false;
}, limit);
}
};
}
export function findCategoryBySlug(
categories: CategoryNode[],
slug: string,
): CategoryNode | null {
for (const category of categories) {
if (category.translation?.slug === slug) return category;
if (category.children && isArray(category.children)) {
const found = findCategoryBySlug(category.children, slug);
if (found) return found;
}
}
return null;
}
export function extractNumericId(id: string): string | undefined {
if (!id) return undefined;
const match = id.match(/\d+$/);
return match ? match[0] : undefined;
}
export const getAuthToken = (req: Request): string | undefined => {
const authHeader = req.headers.get("Authorization");
return authHeader?.split(" ")[1];
};
/**
* Safely parses a JSON string, returns null if parsing fails or value is not a string
* @param value - The string to parse
* @returns The parsed object or null
*/
export function safeParse(value: string | null | undefined): T | null {
if (!value || typeof value !== "string") return null;
try {
return JSON.parse(value);
} catch {
return null;
}
}
/**
* Parses URL search parameters and builds a filter object for product filtering
* @param params - URL search parameters
* @returns Object containing filterInput string and isFilterApplied boolean
*/
export function buildProductFilters(params: {
[key: string]: string | string[] | undefined;
}) {
const rawColor = params?.color;
const rawSize = params?.size;
const rawBrand = params?.brand;
const colorFilter =
typeof rawColor === "string"
? rawColor.split(",")
: Array.isArray(rawColor)
? rawColor
: [];
const sizeFilter =
typeof rawSize === "string"
? rawSize.split(",")
: Array.isArray(rawSize)
? rawSize
: [];
const brandFilter =
typeof rawBrand === "string"
? rawBrand.split(",")
: Array.isArray(rawBrand)
? rawBrand
: [];
const extractId = (value: string) => {
if (/^\d+$/.test(value)) return value;
const match = value.match(/\/(\d+)$/);
return match ? match[1] : null;
};
const colorIds = colorFilter
.map(extractId)
.filter((id): id is string => Boolean(id));
const sizeIds = sizeFilter
.map(extractId)
.filter((id): id is string => Boolean(id));
const brandIds = brandFilter
.map(extractId)
.filter((id): id is string => Boolean(id));
const filterObject: Record = {};
if (colorIds.length > 0) filterObject.color = colorIds.join(",");
if (sizeIds.length > 0) filterObject.size = sizeIds.join(",");
if (brandIds.length > 0) filterObject.brand = brandIds.join(",");
const isFilterApplied = Object.keys(filterObject).length > 0;
const filterInput = isFilterApplied
? JSON.stringify(filterObject)
: undefined;
return {
filterObject,
filterInput,
isFilterApplied,
};
}
export function newBuildProductFilters(params: {
[key: string]: string | string[] | undefined;
}) {
// ==========黑名单:这些字段不会筛选==========
const EXCLUDE_KEYS = new Set([
"q",
"sort",
"cursor",
"before",
"after",
"page",
]);
const extractId = (value: string) => {
if (/^\d+$/.test(value)) return value;
const match = value.match(/\/(\d+)$/);
return match ? match[1] : null;
};
const parseParamIds = (raw: string | string[] | undefined) => {
let list: string[] = [];
if (typeof raw === "string") {
list = raw.split(",");
} else if (Array.isArray(raw)) {
list = raw;
}
return list
.map(extractId)
.filter((id): id is string => Boolean(id));
};
const filterObject: Record = {};
// 遍历全部传入参数
for (const [key, value] of Object.entries(params)) {
// 黑名单字段直接跳过
if (EXCLUDE_KEYS.has(key)) continue;
const ids = parseParamIds(value);
if (ids.length > 0) {
filterObject[key] = ids.join(",");
}
}
const isFilterApplied = Object.keys(filterObject).length > 0;
const filterInput = isFilterApplied
? JSON.stringify(filterObject)
: undefined;
return {
filterObject,
filterInput,
isFilterApplied,
};
}
export function getAverageRating(reviews: ProductReview[]): number {
if (!reviews.length) return 0;
const total = reviews.reduce((sum, review) => sum + review.rating, 0);
return total / reviews.length;
}