295 lines
7.7 KiB
TypeScript
295 lines
7.7 KiB
TypeScript
import { ProductDetail } from "src/api/types";
|
||
import jMoment from "moment-jalaali";
|
||
import { NavigateFunction } from "@remix-run/react";
|
||
export function detectPage(pathname: string | undefined) {
|
||
if (!pathname) return "home";
|
||
if (pathname === "/" || pathname === "") return "home";
|
||
const firstPath = pathname.split("/")[1];
|
||
if (firstPath === "explore") return "explore";
|
||
if (firstPath === "search") return "search";
|
||
if (firstPath === "profile") return "profile";
|
||
if (firstPath === "cart") return "cart";
|
||
if (firstPath === "store") {
|
||
// Check for store sub-pages
|
||
const storePath = pathname.split("/")[3];
|
||
if (storePath === "setting") return "setting";
|
||
if (storePath === "orders") return "orders";
|
||
if (storePath === "products") return "products";
|
||
if (storePath === "financial-dashboard") return "financial-dashboard";
|
||
return "store";
|
||
}
|
||
return "home"; // Default to home if no match
|
||
}
|
||
|
||
// Cache for formatPrice to avoid repeated conversions
|
||
const priceCache = new Map<string, string>();
|
||
const CACHE_MAX_SIZE = 1000;
|
||
|
||
export const formatPrice = (price: string) => {
|
||
// Check cache first
|
||
if (priceCache.has(price)) {
|
||
return priceCache.get(price)!;
|
||
}
|
||
|
||
const num = Number(price);
|
||
// Use English locale to get proper comma placement, then convert to Persian digits
|
||
const formatted = num.toLocaleString("en-US");
|
||
|
||
// Convert English digits to Persian digits
|
||
const persianNumbers = ["۰", "۱", "۲", "۳", "۴", "۵", "۶", "۷", "۸", "۹"];
|
||
const persianDigits = formatted.replace(/[0-9]/g, (digit) => {
|
||
return persianNumbers[parseInt(digit)];
|
||
});
|
||
|
||
// Store in cache with size limit
|
||
if (priceCache.size >= CACHE_MAX_SIZE) {
|
||
const firstKey = priceCache.keys().next().value;
|
||
priceCache.delete(firstKey || "");
|
||
}
|
||
priceCache.set(price, persianDigits);
|
||
|
||
return persianDigits;
|
||
};
|
||
// Helper to find matching product variant ID based on product attributes
|
||
export const findVariantId = (
|
||
product: ProductDetail,
|
||
selectedColor: string | null,
|
||
selectedSize: string | null
|
||
): string | null => {
|
||
// Find matching variant
|
||
const matchingVariant = product.variants?.find((variant) => {
|
||
let colorMatch = true;
|
||
let sizeMatch = true;
|
||
|
||
if (selectedColor) {
|
||
// Convert hex to Persian and compare with resolved رنگ
|
||
colorMatch =
|
||
!selectedColor || variant.resolvedAttributes?.COLOR === selectedColor;
|
||
}
|
||
|
||
if (selectedSize) {
|
||
// Direct comparison with resolved سایز
|
||
sizeMatch = variant.resolvedAttributes?.SIZE === selectedSize;
|
||
}
|
||
|
||
return colorMatch && sizeMatch;
|
||
});
|
||
|
||
return matchingVariant?.id || null;
|
||
};
|
||
|
||
export const getPersianTextOfPrice = (price: string) => {
|
||
if (!price || parseInt(price) === 0) return "صفر تومان";
|
||
if (price.length > 12) return "مبلغ بیش از حد مجاز";
|
||
|
||
const persianDigits = [
|
||
"صفر",
|
||
"یک",
|
||
"دو",
|
||
"سه",
|
||
"چهار",
|
||
"پنج",
|
||
"شش",
|
||
"هفت",
|
||
"هشت",
|
||
"نه",
|
||
];
|
||
const persianTens = [
|
||
"",
|
||
"ده",
|
||
"بیست",
|
||
"سی",
|
||
"چهل",
|
||
"پنجاه",
|
||
"شصت",
|
||
"هفتاد",
|
||
"هشتاد",
|
||
"نود",
|
||
];
|
||
const persianTeens = [
|
||
"ده",
|
||
"یازده",
|
||
"دوازده",
|
||
"سیزده",
|
||
"چهارده",
|
||
"پانزده",
|
||
"شانزده",
|
||
"هفده",
|
||
"هجده",
|
||
"نوزده",
|
||
];
|
||
const persianHundreds = [
|
||
"",
|
||
"صد",
|
||
"دویست",
|
||
"سیصد",
|
||
"چهارصد",
|
||
"پانصد",
|
||
"ششصد",
|
||
"هفتصد",
|
||
"هشتصد",
|
||
"نهصد",
|
||
];
|
||
const persianScales = ["", "هزار", "میلیون", "میلیارد"];
|
||
|
||
const num = parseInt(price);
|
||
|
||
const convertGroup = (n: number): string => {
|
||
if (n === 0) return "";
|
||
|
||
let result = "";
|
||
|
||
// Hundreds
|
||
if (n >= 100) {
|
||
result += persianHundreds[Math.floor(n / 100)] + " ";
|
||
n %= 100;
|
||
}
|
||
|
||
// Tens and units
|
||
if (n >= 10 && n < 20) {
|
||
result += persianTeens[n - 10] + " ";
|
||
} else {
|
||
if (n >= 20) {
|
||
result += persianTens[Math.floor(n / 10)] + " ";
|
||
n %= 10;
|
||
}
|
||
|
||
if (n > 0) {
|
||
result += persianDigits[n] + " ";
|
||
}
|
||
}
|
||
|
||
return result.trim();
|
||
};
|
||
|
||
const convertToWords = (n: number): string => {
|
||
if (n === 0) return "صفر";
|
||
|
||
let result = "";
|
||
let groupIndex = 0;
|
||
|
||
while (n > 0) {
|
||
const group = n % 1000;
|
||
if (group !== 0) {
|
||
const groupText = convertGroup(group);
|
||
if (groupIndex > 0) {
|
||
result =
|
||
groupText +
|
||
" " +
|
||
persianScales[groupIndex] +
|
||
(result ? " و " + result : "");
|
||
} else {
|
||
result = groupText;
|
||
}
|
||
}
|
||
|
||
n = Math.floor(n / 1000);
|
||
groupIndex++;
|
||
}
|
||
|
||
return result;
|
||
};
|
||
|
||
return convertToWords(num) + " تومان";
|
||
};
|
||
// Format date function to convert to Persian date format
|
||
export const formatDate = (dateString: string) => {
|
||
return jMoment(dateString).format("jYYYY/jM/jD");
|
||
};
|
||
|
||
// Format timestamp to a readable time format
|
||
export const formatTime = (timestamp: string) => {
|
||
if (!timestamp) return "";
|
||
const date = new Date(timestamp);
|
||
return date.toLocaleTimeString([], {
|
||
hour: "2-digit",
|
||
minute: "2-digit",
|
||
hour12: false,
|
||
});
|
||
};
|
||
|
||
// Format timestamp to show relative time (like "2 minutes ago")
|
||
export const formatRelativeTime = (timestamp: string) => {
|
||
if (!timestamp) return "";
|
||
jMoment.locale("fa");
|
||
return jMoment(timestamp).fromNow();
|
||
};
|
||
|
||
// Format timestamp to a readable date format
|
||
export const formatDate2 = (timestamp: string) => {
|
||
if (!timestamp) return "";
|
||
const date = new Date(timestamp);
|
||
return date.toLocaleDateString("fa-IR", {
|
||
year: "numeric",
|
||
month: "long",
|
||
day: "numeric",
|
||
});
|
||
};
|
||
|
||
// Check if two timestamps are from different days
|
||
export const isDifferentDay = (timestamp1: string, timestamp2?: string) => {
|
||
if (!timestamp1 || !timestamp2) return false;
|
||
|
||
const date1 = new Date(timestamp1);
|
||
const date2 = new Date(timestamp2);
|
||
|
||
return (
|
||
date1.getFullYear() !== date2.getFullYear() ||
|
||
date1.getMonth() !== date2.getMonth() ||
|
||
date1.getDate() !== date2.getDate()
|
||
);
|
||
};
|
||
export const onSearch = (searchValue: string, navigate: NavigateFunction) => {
|
||
if (searchValue) {
|
||
// Store in local storage for search history
|
||
const searchValues =
|
||
localStorage.getItem("searchValues")?.split("&&") || [];
|
||
const existance = searchValues.includes(searchValue);
|
||
if (!existance) {
|
||
localStorage.setItem(
|
||
"searchValues",
|
||
[searchValue, ...searchValues].join(searchValues.length > 0 ? "&&" : "")
|
||
);
|
||
}
|
||
|
||
// Navigate to the search page with route parameter
|
||
const formattedSearchValue = searchValue.replace(/ /g, "-");
|
||
navigate(`/search/${formattedSearchValue}`);
|
||
}
|
||
};
|
||
export const formatAccountNumber = (accountNumber: string) => {
|
||
return accountNumber
|
||
.replace(/\s/g, "")
|
||
.replace(/(\d{4})(?=\d)/g, "$1-")
|
||
.replace(/-$/, "");
|
||
};
|
||
|
||
// Safe go back with robust SPA-aware detection and graceful fallback
|
||
export const safeGoBack = (navigate: NavigateFunction, fallback: string = "/") => {
|
||
// Prefer React-Router/Remix history index when available (SPA navigations)
|
||
const state = (window.history && window.history.state) as { idx?: number } | null;
|
||
if (state && typeof state.idx === "number" && state.idx > 0) {
|
||
window.history.back();
|
||
return;
|
||
}
|
||
|
||
// Fallback to same-origin referrer + history length
|
||
const referrer = document.referrer;
|
||
const sameOrigin =
|
||
!!referrer &&
|
||
(() => {
|
||
try {
|
||
return new URL(referrer).origin === window.location.origin;
|
||
} catch {
|
||
return false;
|
||
}
|
||
})();
|
||
if (sameOrigin && window.history.length > 1) {
|
||
window.history.back();
|
||
return;
|
||
}
|
||
|
||
// Final fallback: navigate to provided fallback path (default "/")
|
||
navigate(fallback);
|
||
};
|