474 lines
12 KiB
JavaScript
474 lines
12 KiB
JavaScript
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| |<br\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(/\/$/, "");
|
||
};
|