export const isEmptyObject = (obj) => obj && Object.keys(obj)?.length === 0 && obj.constructor === Object; export const isEmptyValue = (obj) => Object.values(obj).some( (x) => x === "" || x === 0 || x === null || x === undefined ); export const formatDate = (date, withClock = false) => { const currentDate = new Date(date); const hours = currentDate.getHours(); const minutes = currentDate.getMinutes(); return ( currentDate.toLocaleDateString("id-ID", { year: "numeric", month: "long", day: "numeric", }) + (withClock ? ` ${hours.toString().padStart(2, "0")}:${minutes .toString() .padStart(2, "0")}` : "") ); }; export const isValidDate = (date) => { const currentDate = new Date(date); return !isNaN(currentDate.getTime()); }; export const spliceObject = (obj, keys = [], removeEmpty = true) => { if (!obj) return; return { selected: Object.entries(obj) .filter( ([key, value]) => keys.includes(key) && (!removeEmpty || value || typeof value === "boolean") ) .reduce((acc, [key, value]) => ({ ...acc, [key]: value }), {}), spliced: Object.entries(obj) .filter( ([key, value]) => !keys.includes(key) && (!removeEmpty || value || typeof value === "boolean") ) .reduce((acc, [key, value]) => ({ ...acc, [key]: value }), {}), }; }; export const isPromise = (fn) => fn.constructor.name === "AsyncFunction"; export const checkPasswordStrength = (password) => { let score = 0; if (!password) return ""; // Check password length if (password.length > 8) score += 1; // Contains lowercase if (/[a-z]/.test(password)) score += 1; // Contains uppercase if (/[A-Z]/.test(password)) score += 1; // Contains numbers if (/\d/.test(password)) score += 1; // Contains special characters if (/[^A-Za-z0-9]/.test(password)) score += 1; switch (score) { case 0: case 1: case 2: return "Weak"; case 3: return "Medium"; case 4: case 5: return "Strong"; } }; export const unmergingCategory = (data) => { if (!data || data?.length === 0) return []; return data .map((item) => [ Object.fromEntries( Object.entries(item).filter(([k, _]) => k !== "children") ), item.children.length > 0 ? unmergingCategory(item.children) : null, ] .flat() .filter((item) => item) ) .flat(); }; export const mergeCategory = ( parents, categories = [], currentDepth, depth ) => { if (categories.length === 0) return parents; return { ...parents, children: (() => { const cat = categories.filter((cat) => cat.parents[0] === parents._id); if (currentDepth < depth) return cat.map((c) => mergeCategory(c, categories, currentDepth + 1, depth) ); return cat; })(), }; }; export const filterEmptyTag = (stringdata) => { return stringdata.replace(/<(\w+)[^>]*>(\s| |)*<\/\1>/gi, ""); }; export const debounce = (fn, delay, fnForceDelay) => { let timeoutId; return (...args) => { if (fnForceDelay) fn(...args); clearTimeout(timeoutId); timeoutId = setTimeout(() => fn(...args), delay); }; }; export const generateColor = (alpha = 1) => { const r = Math.floor(Math.random() * 256); const g = Math.floor(Math.random() * 256); const b = Math.floor(Math.random() * 256); const a = Math.round(alpha * 255); // convert alpha (0–1) to 0–255 // Convert to 2-digit hex and pad with 0 if needed const toHex = (n) => n.toString(16).padStart(2, "0"); return `#${toHex(r)}${toHex(g)}${toHex(b)}${toHex(a)}`; }; /** * @description * Group an array of objects by given key. * @param {string} key - The key to group by. * @param {Array} arr - The array to group. * @returns {Object} - An object with grouped objects as values, and the key used as the key. * @example * const data = [ * { name: 'John', age: 18 }, * { name: 'Jane', age: 19 }, * { name: 'John', age: 18 }, * { name: 'Jane', age: 19 }, * ]; * const grouped = arrayGroupBy('name', data); * console.log(grouped); * { * 'John': [ * { name: 'John', age: 18 }, * { name: 'John', age: 18 } * ], * 'Jane': [ * { name: 'Jane', age: 19 }, * { name: 'Jane', age: 19 } * ] * } */ export const arrayGroupBy = (key, arr) => arr.length > 0 ? arr.reduce( (acc, item) => !!acc[item[key]] ? { ...acc, [item[key]]: [...acc[item[key]], item] } : { ...acc, [item[key]]: [item] }, {} ) : {}; export const getImageSize = async (url) => { if (!url) return; try { const response = await fetch(url, { method: "HEAD" }); if (!response.ok) { throw new Error(`HTTP ${response.status}: ${response.statusText}`); } const contentLength = response.headers.get("content-length"); const type = response.headers.get("content-type"); if (contentLength) { const bytes = parseInt(contentLength); const mb = bytes / (1024 * 1024); return { bytes: bytes, kb: (bytes / 1024).toFixed(2), mb: mb.toFixed(2), size: formatBytes(bytes), type: type, }; } return null; } catch (error) { return { error: error.message }; } }; export const getImageDimension = async (file) => { if (!file) return; return new Promise((resolve, reject) => { const img = new Image(); img.onload = () => { resolve({ width: img.width, height: img.height, }); URL.revokeObjectURL(img.src); }; img.onerror = reject; img.src = isObject(file) ? URL.createObjectURL(file) : file; }); }; export const formatBytes = (bytes, decimals = 2) => { if (bytes === undefined) return ""; if (bytes === 0) return "0 B"; const k = 1024; const sizes = ["B", "KB", "MB", "GB", "TB"]; const i = Math.floor(Math.log(bytes) / Math.log(k)); const value = parseFloat((bytes / Math.pow(k, i)).toFixed(decimals)); return `${value} ${sizes[i]}`; }; export const slugify = (str) => str .toLowerCase() .replace(/[^\w\s-]/g, "") .trim() .replace(/\s+/g, "-"); export const removeDuplicateProp = (data, prop) => { const unique = new Set(); return data.filter((item) => { const key = item?.[prop]; if (!key || unique.has(key)) return false; unique.add(key); return true; }); }; export const toRelativeDay = (date) => { const today = new Date(); const targetDate = new Date(date); // get diff minutes today.setSeconds(0, 0, 0, 0); targetDate.setSeconds(0, 0, 0, 0); let diffTime = targetDate - today; const diffMinutes = Math.abs(Math.ceil(diffTime / (1000 * 60))); if (diffMinutes < 1) return "Just now"; if (diffMinutes < 59) return `${diffMinutes} minutes ago`; // get diff hours today.setMinutes(0, 0, 0, 0); targetDate.setMinutes(0, 0, 0, 0); diffTime = targetDate - today; const diffHours = Math.abs(Math.ceil(diffTime / (1000 * 60 * 60))); if (diffHours < 1) return "Just now"; if (diffHours < 23) return `${diffHours} hours ago`; today.setHours(0, 0, 0, 0); targetDate.setHours(0, 0, 0, 0); diffTime = targetDate - today; const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24)); return [ { on: diffDays === 0, text: "Today" }, { on: diffDays === 1, text: "Tomorrow" }, { on: diffDays === -1, text: "Yesterday" }, { on: diffDays > 1, text: `in ${Math.abs(diffDays)} days` }, { on: diffDays < -1 && diffDays > -7, text: `${Math.abs(diffDays)} days ago`, }, { on: diffDays <= -7 && diffDays >= -31, text: `${Math.abs(Math.round(diffDays / 7))} weeks ago`, }, { on: diffDays < -31 && diffDays >= -365, text: `${Math.abs(Math.round(diffDays / 30))} months ago`, }, { on: diffDays < -365, text: `${formatDate(date)}`, }, ].find((item) => item.on)?.text; }; export const isObject = (obj) => obj !== null && typeof obj === "object" && !Array.isArray(obj); export const flatObjectKeys = (obj) => isObject(obj) ? Object.entries(obj) .map(([k, v]) => [k, flatObjectKeys(v)].flat()) .flat() : []; export const capitalizeEachWord = (string) => string.replace( /\w\S*/g, (txt) => txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase() ); export const objectFilterKey = (obj, key) => Object.fromEntries( Object.entries(obj) .filter(([k, _]) => k !== key) .map(([k, v]) => [k, isObject(v) ? objectFilterKey(v, key) : v]) ); /** * @description * Check if object has key recursively */ export const objecthasKey = (obj, key) => flatObjectKeys(obj).includes(key); export const objecthasKeys = (obj, keys) => keys.every((key) => objecthasKey(obj, key)); export const filterEmptyValue = (obj) => Object.fromEntries( Object.entries(obj) .filter(([_, v]) => v) .map(([k, v]) => [k, isObject(v) ? filterEmptyValue(v) : v]) ); export const modifyByKeys = (obj, keys, callback, keepData = false) => { if (!isObject(obj)) return obj; const startOfKey = keys.map((k) => (k.match(/\./gi) ? k.split(".")[0] : k)); const endOfKey = keys.map((k) => k.match(/\./gi) ? k.split(".").slice(-1) : k ); return Object.fromEntries( Object.entries({ ...obj, ...(keepData ? Object.fromEntries( endOfKey.filter((k) => k in obj).map((k) => [`_${k}`, obj[k]]) ) : {}), }).map(([k, v]) => [ k, startOfKey.includes(k) ? isObject(v) ? modifyByKeys( v, keys .filter((k) => k.match(/\./gi)) // filter only key has dot .map((k) => k.split(".").slice(1).join(".")), // remove first word before dot callback, keepData ) // recursively : callback(v) : v, ]) ); }; export const getByKeys = (obj, keys, initialData = {}) => { // create multidimentional array const newKeys = keys.map((k) => (k.match(/\./gi) ? k.split(".") : k)); const data = newKeys.reduce( (acc, key) => // my expectation is key is array obj[key[0]] is object Array.isArray(key) && key[0] in obj ? { ...acc, // recursively when key is array from splited with dot // call again with joined key with dot ...getByKeys(obj[key[0]], [key.slice(1).join(".")], acc), } : key in obj ? { ...acc, [key]: obj[key] } : acc, {} ); return { ...initialData, ...data }; }; export const generateIndexes = (start, end) => { const _start = Math.min(start, end); const _end = Math.max(start, end); const length = _end - _start + 1; return Array.from({ length }, (_, index) => index + _start); }; export const generateRange = (...numbers) => { const [from, to] = [Math.min(...numbers), Math.max(...numbers)]; return Array(to - from + 1) .fill() .map((_, i) => i + from); }; export const formatDateRange = (startDate, endDate) => { const start = new Date(startDate); const end = new Date(endDate); // helper cek valid date const isValid = (d) => d instanceof Date && !isNaN(d); if (!isValid(start)) return startDate; if (!isValid(end)) return endDate; const pad = (num) => num.toString().padStart(2, "0"); const sameDay = start.toDateString() === end.toDateString(); const sameMonth = start.getMonth() === end.getMonth(); const sameYear = start.getFullYear() === end.getFullYear(); if (sameDay) { // Kalau start & end sama → tampilkan 1 tanggal return `${pad(start.getDate())} ${start.toLocaleDateString("id-ID", { month: "long", year: "numeric", })}`; } if (sameMonth && sameYear) { // Contoh: 01 - 05 Januari 2025 return `${pad(start.getDate())} - ${pad( end.getDate() )} ${start.toLocaleDateString("id-ID", { month: "long", year: "numeric", })}`; } else { // Contoh: 30 Jan - 02 Feb 25 return `${pad(start.getDate())} ${start.toLocaleDateString("id-ID", { month: "short", })} - ${pad(end.getDate())} ${end.toLocaleDateString("id-ID", { month: "short", year: "2-digit", })}`; } }; export const normalizeURL = (url) => { return url .replace(/^https?:\/\//, "") .replace(/^www\./, "") .replace(/\/$/, ""); };