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; }