291 lines
8.3 KiB
TypeScript
291 lines
8.3 KiB
TypeScript
/* eslint-disable @typescript-eslint/no-unused-vars */
|
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
import { clsx, type ClassValue } from "clsx";
|
|
import { twMerge } from "tailwind-merge";
|
|
import moment from "moment";
|
|
import { IBid, IMetadata, IWebBid } from "../system/type";
|
|
import _ from "lodash";
|
|
|
|
export function cn(...args: ClassValue[]) {
|
|
return twMerge(clsx(args));
|
|
}
|
|
|
|
export const formatTime = (time: string, patent = "DD/MM/YYYY") => {
|
|
return moment(time).format(patent);
|
|
};
|
|
|
|
export function removeFalsyValues<T extends Record<string, any>>(
|
|
obj: T,
|
|
excludeKeys: (keyof T)[] = []
|
|
): Partial<T> {
|
|
return Object.entries(obj).reduce((acc, [key, value]) => {
|
|
if (value || excludeKeys.includes(key as keyof T)) {
|
|
acc[key as keyof T] = value;
|
|
}
|
|
return acc;
|
|
}, {} as Partial<T>);
|
|
}
|
|
|
|
export function isValidJSON(str: string): boolean {
|
|
if (!str || str.length <= 0) return false;
|
|
|
|
try {
|
|
JSON.parse(str);
|
|
return true;
|
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
} catch (e) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
export function copyToClipboard(text: string, onSuccess?: () => void): void {
|
|
if (!navigator.clipboard) {
|
|
const textarea = document.createElement("textarea");
|
|
textarea.value = text;
|
|
textarea.style.position = "fixed";
|
|
document.body.appendChild(textarea);
|
|
textarea.focus();
|
|
textarea.select();
|
|
|
|
try {
|
|
document.execCommand("copy");
|
|
if (onSuccess) onSuccess();
|
|
} catch (err) {
|
|
console.error("Không thể copy nội dung: ", err);
|
|
}
|
|
|
|
document.body.removeChild(textarea);
|
|
} else {
|
|
navigator.clipboard
|
|
.writeText(text)
|
|
.then(() => {
|
|
if (onSuccess) onSuccess();
|
|
})
|
|
.catch((err) => console.error("Lỗi khi copy nội dung: ", err));
|
|
}
|
|
}
|
|
|
|
export function base64ToFile(base64String: string, fileName: string): File {
|
|
const [header, base64Content] = base64String.split(",");
|
|
|
|
const mimeTypeMatch = header.match(/:(.*?);/);
|
|
if (!mimeTypeMatch || mimeTypeMatch.length < 2) {
|
|
throw new Error("Invalid base64 string");
|
|
}
|
|
const mimeType = mimeTypeMatch[1];
|
|
|
|
const binaryString = atob(base64Content);
|
|
|
|
const byteArray = new Uint8Array(binaryString.length);
|
|
for (let i = 0; i < binaryString.length; i++) {
|
|
byteArray[i] = binaryString.charCodeAt(i);
|
|
}
|
|
|
|
return new File([byteArray], fileName, { type: mimeType });
|
|
}
|
|
|
|
export function toSlug(str: string, maxLength = 60): string {
|
|
if (typeof str !== "string") return ""; // Kiểm tra giá trị đầu vào
|
|
|
|
// Kiểm tra nếu môi trường hỗ trợ `normalize`
|
|
const normalizedStr = str.normalize ? str.normalize("NFD") : str;
|
|
|
|
return normalizedStr
|
|
.replace(/[\u0300-\u036f]/g, "") // Xóa dấu
|
|
.replace(/[^a-zA-Z0-9\s-]/g, "") // Chỉ giữ chữ cái, số, khoảng trắng và dấu "-"
|
|
.trim() // Xóa khoảng trắng đầu/cuối
|
|
.replace(/\s+/g, "-") // Thay khoảng trắng bằng "-"
|
|
.replace(/-+/g, "-") // Gộp nhiều dấu "-" thành 1
|
|
.toLowerCase() // Chuyển về chữ thường
|
|
.slice(0, maxLength) // Giới hạn độ dài
|
|
.replace(/^-+|-+$/g, ""); // Xóa "-" đầu/cuối
|
|
}
|
|
|
|
export function estimateReadingTimeInSeconds(
|
|
content: string,
|
|
wordsPerMinute = 200
|
|
): number {
|
|
if (!content || typeof content !== "string") return 0;
|
|
|
|
const wordCount = content.trim().split(/\s+/).length;
|
|
return Math.ceil((wordCount / wordsPerMinute) * 60);
|
|
}
|
|
|
|
export function extractDomain(url: string): string | null {
|
|
try {
|
|
const parsedUrl = new URL(url);
|
|
return parsedUrl.origin;
|
|
} catch (error) {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
// Hash chuỗi thành số nguyên
|
|
export function hashStringToInt(str: string): number {
|
|
let hash = 0;
|
|
for (let i = 0; i < str.length; i++) {
|
|
hash = str.charCodeAt(i) + ((hash << 5) - hash);
|
|
hash = hash & hash; // convert to 32bit integer
|
|
}
|
|
return Math.abs(hash);
|
|
}
|
|
|
|
// Biến số thành màu HEX
|
|
export function intToHexColor(int: number): string {
|
|
const r = (int >> 16) & 0xff;
|
|
const g = (int >> 8) & 0xff;
|
|
const b = int & 0xff;
|
|
return `#${[r, g, b].map((x) => x.toString(16).padStart(2, "0")).join("")}`;
|
|
}
|
|
|
|
export function stringToColor(str: string): string {
|
|
const colorPalette = [
|
|
"#FF6B6B",
|
|
"#FFD93D",
|
|
"#FF9F1C",
|
|
"#F76C6C",
|
|
"#6BCB77",
|
|
"#4ECDC4",
|
|
"#F7B801",
|
|
"#FF6F91",
|
|
"#00C9A7",
|
|
];
|
|
const hash = hashStringToInt(str);
|
|
const index = hash % colorPalette.length;
|
|
return colorPalette[index];
|
|
}
|
|
|
|
export function findEarlyLoginTime(webBid: IWebBid): string | null {
|
|
const now = new Date();
|
|
|
|
// Bước 1: Lọc ra những bid có close_time hợp lệ
|
|
const validChildren = webBid.children.filter((child) => child.close_time);
|
|
|
|
if (validChildren.length === 0) return null;
|
|
|
|
// Bước 2: Tìm bid có close_time gần hiện tại nhất
|
|
const closestBid = validChildren.reduce((closest, current) => {
|
|
const closestDiff = Math.abs(
|
|
new Date(closest.close_time!).getTime() - now.getTime()
|
|
);
|
|
const currentDiff = Math.abs(
|
|
new Date(current.close_time!).getTime() - now.getTime()
|
|
);
|
|
return currentDiff < closestDiff ? current : closest;
|
|
});
|
|
|
|
if (!closestBid.close_time) return null;
|
|
|
|
// Bước 3: Tính toán thời gian login sớm
|
|
const closeTime = new Date(closestBid.close_time);
|
|
closeTime.setSeconds(
|
|
closeTime.getSeconds() - (webBid.early_tracking_seconds || 0)
|
|
);
|
|
|
|
return closeTime.toISOString();
|
|
}
|
|
|
|
export function extractDomainSmart(url: string) {
|
|
const PUBLIC_SUFFIXES = ["com.au", "co.uk", "com.vn", "org.au", "gov.uk"];
|
|
|
|
try {
|
|
const hostname = new URL(url).hostname.replace(/^www\./, ""); // remove "www."
|
|
const parts = hostname.split(".");
|
|
|
|
for (let i = 0; i < PUBLIC_SUFFIXES.length; i++) {
|
|
if (hostname.endsWith(PUBLIC_SUFFIXES[i])) {
|
|
return parts[parts.length - PUBLIC_SUFFIXES[i].split(".").length - 1];
|
|
}
|
|
}
|
|
|
|
return parts[parts.length - 2];
|
|
} catch (e) {
|
|
return url;
|
|
}
|
|
}
|
|
|
|
export function findNearestClosingChild(webBid: IWebBid) {
|
|
const now = Date.now();
|
|
|
|
const validChildren = webBid.children.filter(
|
|
(child) => child.close_time && !isNaN(new Date(child.close_time).getTime())
|
|
);
|
|
|
|
if (validChildren.length === 0) {
|
|
return null;
|
|
}
|
|
|
|
const nearestChild = _.minBy(validChildren, (child) => {
|
|
return Math.abs(new Date(child.close_time!).getTime() - now);
|
|
});
|
|
|
|
return nearestChild || null;
|
|
}
|
|
|
|
export function extractNumber(str: string) {
|
|
const match = str.match(/\d+(\.\d+)?/);
|
|
return match ? parseFloat(match[0]) : null;
|
|
}
|
|
|
|
export function subtractMinutes(time: string, minutes: number) {
|
|
const date = new Date(time);
|
|
date.setMinutes(date.getMinutes() - minutes);
|
|
return date.toUTCString();
|
|
}
|
|
|
|
export function subtractSeconds(time: string, seconds: number) {
|
|
const date = new Date(time);
|
|
date.setSeconds(date.getSeconds() - seconds);
|
|
return date.toUTCString();
|
|
}
|
|
|
|
export function isTimeReached(targetTime: string) {
|
|
if (!targetTime) return false;
|
|
|
|
const targetDate = new Date(targetTime);
|
|
const now = new Date();
|
|
|
|
return now >= targetDate;
|
|
}
|
|
|
|
export function formatTimeFromMinutes(minutes: number): string {
|
|
// Tính ngày, giờ, phút từ số phút
|
|
const days = Math.floor(minutes / (60 * 24));
|
|
const hours = Math.floor((minutes % (60 * 24)) / 60);
|
|
const mins = minutes % 60;
|
|
|
|
let result = "";
|
|
|
|
if (days > 0) result += `${days} ${days > 1 ? "days" : "day"} `;
|
|
if (hours > 0) result += `${hours} ${hours > 1 ? "hours" : "hour"} `;
|
|
if (mins > 0 || result === "") result += `${mins} minutes`;
|
|
|
|
return result.trim();
|
|
}
|
|
|
|
export const getMode = (row: IBid) => {
|
|
return (
|
|
row.metadata.find((item) => item.key_name === "mode_key")?.value || "live"
|
|
);
|
|
};
|
|
|
|
export const getEarlyTrackingSeconds = (row: IBid) => {
|
|
const mode = getMode(row);
|
|
|
|
return (
|
|
row.metadata.find(
|
|
(item) => item.key_name === `early_tracking_seconds_${mode}`
|
|
)?.value || row.web_bid.early_tracking_seconds
|
|
);
|
|
};
|
|
|
|
export const getResponseDemo = (data: IBid) => {
|
|
if (!data?.metadata) return null;
|
|
|
|
return (
|
|
(data?.metadata.find(
|
|
(item) => item.key_name === "demo_response"
|
|
) as IMetadata) || null
|
|
);
|
|
};
|