| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533 |
- 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 <meta> 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<number, number>;
- } {
- let totalReviewsAvg = 0;
- let totalReviews = 0;
- const ratingCounts: Record<number, number> = {
- 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<CartItem>,
- 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<string, string>;
- },
- ): Promise<Metadata> {
- const seo: {
- title?: string;
- description?: string;
- image?: string;
- canonical?: string;
- other?: Record<string, string>;
- } = {};
- // 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<T = any>(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<T extends (...args: any[]) => any>(
- func: T,
- limit: number,
- ): (...args: Parameters<T>) => void {
- let inThrottle: boolean = false;
- return function (this: any, ...args: Parameters<T>) {
- 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<T = any>(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<string, string> = {};
- 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<string, string> = {};
- // 遍历全部传入参数
- 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;
- }
|