Files
patra_web/src/app/library/library.js
2025-12-30 14:38:36 +07:00

474 lines
12 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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|&nbsp;|<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 (01) to 0255
// 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(/\/$/, "");
};