import { ReadonlyURLSearchParams } from "next/navigation"; import { type Metadata } from "next"; import type { FilterDataTypes } from "@/types/types"; import { isArray } from "./type-guards"; import { env } from '@/env'; // 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 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 delay = (ms: number) => { return new Promise((resolve) => setTimeout(resolve, ms)); }; 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 || `${env.NEXT_PUBLIC_SITE_URL}/${slug}`; const otherMeta = { ...DEFAULT_OTHER, ...(fallback?.other || {}), ...(seo.other || {}), }; return { metadataBase: new URL(env.NEXT_PUBLIC_SITE_URL), 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) ?? []; } /** * 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); } }; } /** * 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 PRICE_KEYS = new Set(["price_from", "price_to"]); 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; // ✅ 价格字段单独处理,不执行ID提取 if (PRICE_KEYS.has(key)) { if (typeof value === "string" && value) { filterObject[key] = value; } continue; } // 普通属性筛选,沿用原来的ID提取逻辑 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, }; } // 获取/生成访客UUID export function getUuId() { const key = "GUEST_UUID"; let id = localStorage.getItem(key); if (!id) { id = crypto.randomUUID(); localStorage.setItem(key, id); } return id; }; // 筛选字段要多个match {\"wig_color\":{\"match\":\"33\"},\"price_from\":10,\"price_to\":212} export function transformFiltersForApi(raw: Record) { // filter传参需要调整 const result: Record = {}; for (const key in raw) { const val = raw[key]; // 价格字段特殊处理:转数字,直接赋值,不包match if (key === "price_from" || key === "price_to") { // 空/undefined 跳过 if (val === undefined || val === null || val === "") continue; result[key] = Number(val); } else { // 其他所有筛选属性:套上 {match:xxx} result[key] = { match:String(val), }; } } return result; }