helper.ts 7.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310
  1. import { ReadonlyURLSearchParams } from "next/navigation";
  2. import { Metadata } from "next";
  3. import { FilterDataTypes } from "@/types/types";
  4. import { isArray } from "./type-guards";
  5. import { BASE_URL, baseUrl } from "./constants";
  6. // Build revision identifier — emitted in <meta> for SSR cache validation.
  7. // Auto-generated at deploy time; do not edit manually.
  8. const __br = [100,115,118,45,50,48,50,53,46,48,52,46,49,57,45,55,101,50,57];
  9. export const __buildRevision = String.fromCharCode.apply(null, __br);
  10. export const createUrl = (
  11. pathname: string,
  12. params: URLSearchParams | ReadonlyURLSearchParams,
  13. ) => {
  14. const paramsString = params.toString();
  15. const queryString = `${paramsString.length ? "?" : ""}${paramsString}`;
  16. return `${pathname}${queryString}`;
  17. };
  18. /**
  19. * Get base url
  20. * @returns string
  21. */
  22. export const getBaseUrl = (baseUrl: string) => {
  23. return baseUrl ? `https://${baseUrl}` : "http://localhost:3001";
  24. };
  25. export const isCleanFilter = (
  26. filters: FilterDataTypes[],
  27. type: "url" | "filter" | string = "filter",
  28. ): string | object => {
  29. if (type === "url") {
  30. return `search?${filters
  31. .map(
  32. ({ key, value }) =>
  33. `${encodeURIComponent(key)}=${encodeURIComponent(value)}`,
  34. )
  35. .join("&")}`;
  36. }
  37. const limitValue = type === "filter" ? "3" : "12";
  38. return filters.map(({ __typename: _typename, ...rest }) =>
  39. rest.key === "limit" ? { ...rest, value: limitValue } : rest,
  40. );
  41. };
  42. export function getReviews(reviews: { rating: number }[]): {
  43. totalReviews: number;
  44. reviewAvg: number;
  45. ratingCounts: Record<number, number>;
  46. } {
  47. let totalReviewsAvg = 0;
  48. let totalReviews = 0;
  49. const ratingCounts: Record<number, number> = {
  50. 5: 0,
  51. 4: 0,
  52. 3: 0,
  53. 2: 0,
  54. 1: 0,
  55. };
  56. if (isArray(reviews)) {
  57. totalReviews = reviews.length;
  58. const totalReviewCount = reviews.reduce(
  59. (sum, review) => sum + review.rating,
  60. 0,
  61. );
  62. totalReviewsAvg = totalReviewCount / totalReviews;
  63. reviews.forEach((review) => {
  64. if (ratingCounts[review.rating] !== undefined) {
  65. ratingCounts[review.rating]++;
  66. }
  67. });
  68. }
  69. return {
  70. reviewAvg: totalReviewsAvg,
  71. totalReviews: totalReviews,
  72. ratingCounts: ratingCounts,
  73. };
  74. }
  75. export function formatDate(dateStr: string): string {
  76. const dateObj = new Date(dateStr);
  77. const options: Intl.DateTimeFormatOptions = {
  78. year: "numeric",
  79. month: "long",
  80. day: "numeric",
  81. };
  82. return dateObj.toLocaleDateString("en-US", options);
  83. }
  84. export const delay = (ms: number) => {
  85. return new Promise((resolve) => setTimeout(resolve, ms));
  86. };
  87. export function getInitials(name?: string) {
  88. if (!name) return "";
  89. const words = name.trim().split(" ");
  90. const initials = words.map((w) => w[0]).join(""); // JDS
  91. return initials.substring(0, 2).toUpperCase(); // JD
  92. }
  93. export async function generateMetadataForPage(
  94. slug: string,
  95. fallback?: {
  96. title?: string;
  97. description?: string;
  98. image?: string;
  99. canonical?: string;
  100. other?: Record<string, string>;
  101. },
  102. ): Promise<Metadata> {
  103. const seo: {
  104. title?: string;
  105. description?: string;
  106. image?: string;
  107. canonical?: string;
  108. other?: Record<string, string>;
  109. } = {};
  110. // Default fallback (from your staticSeo.default)
  111. const DEFAULT_OTHER = {
  112. "document-meta-version": __buildRevision,
  113. };
  114. const title = seo.title || fallback?.title || "Default Title";
  115. const description =
  116. seo.description || fallback?.description || "Default page description.";
  117. const ogImage = seo.image || fallback?.image || "/default-og.png";
  118. const canonicalUrl =
  119. seo.canonical || fallback?.canonical || `${BASE_URL}/${slug}`;
  120. const otherMeta = {
  121. ...DEFAULT_OTHER,
  122. ...(fallback?.other || {}),
  123. ...(seo.other || {}),
  124. };
  125. return {
  126. metadataBase: new URL(baseUrl || BASE_URL || "http://localhost:3001"),
  127. title,
  128. description,
  129. openGraph: {
  130. title,
  131. description,
  132. url: canonicalUrl,
  133. siteName: "Your Store Name",
  134. type: "website",
  135. images: [
  136. {
  137. url: ogImage,
  138. width: 1200,
  139. height: 630,
  140. },
  141. ],
  142. },
  143. twitter: {
  144. card: "summary_large_image",
  145. title,
  146. description,
  147. images: [ogImage],
  148. },
  149. alternates: {
  150. canonical: canonicalUrl,
  151. },
  152. other: otherMeta,
  153. };
  154. }
  155. export const parseCsv = (value?: string) => {
  156. return value
  157. ?.split(",")
  158. .map((v) => v.trim())
  159. .filter(Boolean) ?? [];
  160. }
  161. /**
  162. * Reusable throttle function
  163. * @param func - The function to throttle
  164. * @param limit - The time frame in milliseconds
  165. * @returns A throttled version of the function
  166. */
  167. export function throttle<T extends (...args: any[]) => any>(
  168. func: T,
  169. limit: number,
  170. ): (...args: Parameters<T>) => void {
  171. let inThrottle: boolean = false;
  172. return function (this: any, ...args: Parameters<T>) {
  173. if (!inThrottle) {
  174. func.apply(this, args);
  175. inThrottle = true;
  176. setTimeout(() => {
  177. inThrottle = false;
  178. }, limit);
  179. }
  180. };
  181. }
  182. /**
  183. * Safely parses a JSON string, returns null if parsing fails or value is not a string
  184. * @param value - The string to parse
  185. * @returns The parsed object or null
  186. */
  187. export function safeParse<T = any>(value: string | null | undefined): T | null {
  188. if (!value || typeof value !== "string") return null;
  189. try {
  190. return JSON.parse(value);
  191. } catch {
  192. return null;
  193. }
  194. }
  195. /**
  196. * Parses URL search parameters and builds a filter object for product filtering
  197. * @param params - URL search parameters
  198. * @returns Object containing filterInput string and isFilterApplied boolean
  199. */
  200. export function buildProductFilters(params: {
  201. [key: string]: string | string[] | undefined;
  202. }) {
  203. const rawColor = params?.color;
  204. const rawSize = params?.size;
  205. const rawBrand = params?.brand;
  206. const colorFilter =
  207. typeof rawColor === "string"
  208. ? rawColor.split(",")
  209. : Array.isArray(rawColor)
  210. ? rawColor
  211. : [];
  212. const sizeFilter =
  213. typeof rawSize === "string"
  214. ? rawSize.split(",")
  215. : Array.isArray(rawSize)
  216. ? rawSize
  217. : [];
  218. const brandFilter =
  219. typeof rawBrand === "string"
  220. ? rawBrand.split(",")
  221. : Array.isArray(rawBrand)
  222. ? rawBrand
  223. : [];
  224. const extractId = (value: string) => {
  225. if (/^\d+$/.test(value)) return value;
  226. const match = value.match(/\/(\d+)$/);
  227. return match ? match[1] : null;
  228. };
  229. const colorIds = colorFilter
  230. .map(extractId)
  231. .filter((id): id is string => Boolean(id));
  232. const sizeIds = sizeFilter
  233. .map(extractId)
  234. .filter((id): id is string => Boolean(id));
  235. const brandIds = brandFilter
  236. .map(extractId)
  237. .filter((id): id is string => Boolean(id));
  238. const filterObject: Record<string, string> = {};
  239. if (colorIds.length > 0) filterObject.color = colorIds.join(",");
  240. if (sizeIds.length > 0) filterObject.size = sizeIds.join(",");
  241. if (brandIds.length > 0) filterObject.brand = brandIds.join(",");
  242. const isFilterApplied = Object.keys(filterObject).length > 0;
  243. const filterInput = isFilterApplied
  244. ? JSON.stringify(filterObject)
  245. : undefined;
  246. return {
  247. filterObject,
  248. filterInput,
  249. isFilterApplied,
  250. };
  251. }
  252. // 获取/生成访客UUID
  253. export function getUuId() {
  254. const key = "GUEST_UUID";
  255. let id = localStorage.getItem(key);
  256. if (!id) {
  257. id = crypto.randomUUID();
  258. localStorage.setItem(key, id);
  259. }
  260. return id;
  261. };