Last active
June 30, 2024 14:04
-
-
Save vicasas/220f7ccd68cd811ca44c6e607511aa89 to your computer and use it in GitHub Desktop.
Utilities to JavaScript and TypeScript
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* Returns the origin URL (protocol, hostname, and port) of the current location. | |
* | |
* @returns {string} The origin URL. | |
*/ | |
export function getLocationOrigin() { | |
const { protocol, hostname, port } = window.location; | |
return `${protocol}//${hostname}${port ? ':' + port : ''}`; | |
} | |
/** | |
* Returns the path of the current URL, excluding the origin. | |
* | |
* @returns {string} The path of the current URL. | |
*/ | |
export function getURL() { | |
const { href } = window.location; | |
const origin = getLocationOrigin(); | |
return href.substring(origin.length); | |
} | |
/** | |
* Determines if a user agent string belongs to a bot. | |
* | |
* @param {string} userAgent - The user agent string to test. | |
* @returns {boolean} True if the user agent is identified as a bot, otherwise false. | |
*/ | |
export function isBot(userAgent: string): boolean { | |
return /Googlebot|Mediapartners-Google|AdsBot-Google|googleweblight|Storebot-Google|Google-PageRenderer|Bingbot|BingPreview|Slurp|DuckDuckBot|baiduspider|yandex|sogou|LinkedInBot|bitlybot|tumblr|vkShare|quora link preview|facebookexternalhit|facebookcatalog|Twitterbot|applebot|redditbot|Slackbot|Discordbot|WhatsApp|SkypeUriPreview|ia_archiver/i.test( | |
userAgent | |
); | |
} | |
/** | |
* Asynchronously waits for a specified amount of time. | |
* | |
* @param {number} ms - The number of milliseconds to wait. | |
* @returns {Promise<void>} A promise that resolves after the specified time. | |
*/ | |
export async function wait(ms: number): Promise<void> { | |
return new Promise((resolve) => setTimeout(resolve, ms)); | |
} | |
/** | |
* Picks specified keys from an object and returns a new object with those keys. | |
* | |
* @param {T} obj - The object from which to pick keys. | |
* @param {K[]} keys - The keys to pick. | |
* @returns {Pick<T, K>} A new object containing only the picked keys. | |
*/ | |
export function pick<T, K extends keyof T>(obj: T, keys: K[]): Pick<T, K> { | |
const newObj = {} as Pick<T, K>; | |
for (const key of keys) { | |
newObj[key] = obj[key]; | |
} | |
return newObj; | |
} | |
/** | |
* Computes the difference between two arrays or sets. | |
* | |
* @param {ReadonlyArray<T> | ReadonlySet<T>} main - The main array or set. | |
* @param {ReadonlyArray<T> | ReadonlySet<T>} sub - The sub array or set. | |
* @returns {T[]} An array containing elements that are present in the main array/set but not in the sub array/set. | |
*/ | |
export function difference<T>( | |
main: ReadonlyArray<T> | ReadonlySet<T>, | |
sub: ReadonlyArray<T> | ReadonlySet<T> | |
): T[] { | |
const a = new Set(main); | |
const b = new Set(sub); | |
return [...a].filter((x) => !b.has(x)); | |
} | |
/** | |
* Returns an array of items shared by both input arrays. | |
* | |
* @param {ReadonlyArray<T>} main - The main array. | |
* @param {ReadonlyArray<T>} sub - The sub array. | |
* @returns {T[]} An array containing items shared by both input arrays. | |
*/ | |
export function intersect<T>(main: ReadonlyArray<T>, sub: ReadonlyArray<T>): T[] { | |
const a = new Set(main); | |
const b = new Set(sub); | |
return [...new Set([...a].filter((x) => b.has(x)))]; | |
} | |
/** | |
* Splits an object into two objects: one containing the specified keys and the other containing the rest. | |
* | |
* @template T - The type of the object. | |
* @template K - The keys to pick from the object. | |
* @param {T} object - The object to split. | |
* @param {K[]} keys - The keys to pick from the object. | |
* @returns {[{ [P in K]: T[P] }, Omit<T, K>]} An array containing two objects: one with the picked keys and one with the omitted keys. | |
*/ | |
export function split<T extends Record<string, any>, K extends keyof T>( | |
object: T, | |
keys: K[], | |
): [ | |
{ [P in K]: T[P] }, | |
Omit<T, K> | |
] { | |
const picked: Record<string, any> = {}; | |
const omitted: Record<string, any> = {}; | |
for (const [key, value] of Object.entries(object)) { | |
if (keys.includes(key as K)) { | |
picked[key] = value; | |
} else { | |
omitted[key] = value; | |
} | |
} | |
return [picked, omitted] as [ | |
{ [P in K]: T[P] }, | |
Omit<T, K> | |
]; | |
} | |
/** | |
* Computes the sum of numbers in an array. | |
* | |
* @param {ReadonlyArray<number>} a - The array of numbers. | |
* @returns {number} The sum of numbers in the array. | |
*/ | |
export function sum(a: ReadonlyArray<number>): number { | |
return a.reduce((size, stat) => size + stat, 0); | |
} | |
/** | |
* Converts a URL query parameter to a string. | |
* | |
* @param {unknown} param - The URL query parameter to stringify. | |
* @returns {string} The string representation of the URL query parameter. Returns an empty string if the parameter cannot be converted to a string. | |
*/ | |
export function stringifyUrlQueryParam(param: unknown): string { | |
if ( | |
typeof param === 'string' || | |
(typeof param === 'number' && !isNaN(param)) || | |
typeof param === 'boolean' | |
) { | |
return String(param); | |
} else { | |
return ''; | |
} | |
} | |
/** | |
* Capitalizes the first letter of a string. | |
* | |
* @param {string} str - The string to capitalize. | |
* @returns {string} The capitalized string. | |
*/ | |
export function capitalize(str: string): string { | |
if (!str || typeof str !== "string") return str; | |
return str.charAt(0).toUpperCase() + str.slice(1); | |
} | |
/** | |
* Truncates a string to a specified length and appends ellipsis if necessary. | |
* | |
* @param {string} str - The string to truncate. | |
* @param {number} length - The maximum length of the truncated string. | |
* @returns {string} The truncated string with ellipsis if necessary. | |
*/ | |
export function truncate(str: string, length: number): string { | |
if (!str || str.length <= length) return str; | |
return `${str.slice(0, length)}...`; | |
}; | |
/** | |
* Formats a number into a string representation with optional digits for nFormatter. | |
* | |
* @param {number} num - The number to format. | |
* @param {number} [digits] - The number of digits after the decimal point. | |
* @returns {string} A string representation of the formatted number. | |
*/ | |
export function nFormatter(num: number, digits?: number): string { | |
if (!num) return "0"; | |
const lookup = [ | |
{ value: 1, symbol: "" }, | |
{ value: 1e3, symbol: "K" }, | |
{ value: 1e6, symbol: "M" }, | |
{ value: 1e9, symbol: "G" }, | |
{ value: 1e12, symbol: "T" }, | |
{ value: 1e15, symbol: "P" }, | |
{ value: 1e18, symbol: "E" }, | |
]; | |
const rx = /\.0+$|(\.[0-9]*[1-9])0+$/; | |
var item = lookup | |
.slice() | |
.reverse() | |
.find(function (item) { | |
return num >= item.value; | |
}); | |
return item | |
? (num / item.value).toFixed(digits || 1).replace(rx, "$1") + item.symbol | |
: "0"; | |
} | |
/** | |
* Returns a string representing the time elapsed since the provided timestamp. | |
* | |
* @param {Date} timestamp - The timestamp to calculate the time ago from. | |
* @param {boolean} [timeOnly] - Flag to include only the time part. | |
* @returns {string} A string representing the time elapsed since the provided timestamp. | |
*/ | |
export function timeAgo(timestamp: Date, timeOnly?: boolean): string { | |
if (!timestamp) return "never"; | |
return `${ms(Date.now() - new Date(timestamp).getTime())}${ | |
timeOnly ? "" : " ago" | |
}`; | |
}; | |
/** | |
* A generic function to fetch data from an API endpoint. | |
* | |
* @param {RequestInfo} input - The resource to fetch. | |
* @param {RequestInit} [init] - The init parameters for the fetch request. | |
* @returns {Promise<JSON>} A promise resolving to the JSON data fetched from the API. | |
*/ | |
export async function fetcher<JSON = any>( | |
input: RequestInfo, | |
init?: RequestInit, | |
): Promise<JSON> { | |
const res = await fetch(input, init); | |
if (!res.ok) { | |
const json = await res.json(); | |
if (json.error) { | |
const error = new Error(json.error) as Error & { | |
status: number; | |
}; | |
error.status = res.status; | |
throw error; | |
} else { | |
throw new Error("An unexpected error occurred"); | |
} | |
} | |
return res.json(); | |
} | |
/** | |
* Returns the initials of a name. | |
* | |
* @param {string} [name=''] - The full name to extract initials from. | |
* @returns {string} The initials of the name. | |
*/ | |
export const initials = (name: string = ''): string => name | |
.replace(/\s+/, ' ') | |
.split(' ') | |
.slice(0, 2) | |
.map((v) => v && v[0].toUpperCase()) | |
.join(''); | |
/** | |
* Returns an emoji flag for the given country code. | |
* | |
* @param {string} countryCode - The ISO 3166-1 alpha-2 country code. | |
* @returns {string} The emoji flag for the given country code. | |
*/ | |
const getFlagEmoji = (countryCode: string): string => | |
String.fromCodePoint(...[...countryCode.toUpperCase()].map((x) => 0x1f1a5 + x.charCodeAt())); | |
/** | |
* Checks if the given value is a number. | |
* | |
* @param {unknown} value - The value to check. | |
* @returns {boolean} True if the value is a number, otherwise false. | |
*/ | |
export function isNumber(value: unknown): value is number { | |
return typeof value === 'number' && !Number.isNaN(value); | |
} | |
/** | |
* Checks if the given value is a function. | |
* | |
* @param {any} value - The value to check. | |
* @returns {boolean} True if the value is a function, otherwise false. | |
*/ | |
export function isFunction(value: any): value is Function { | |
return typeof value === 'function'; | |
} | |
/** | |
* Checks if the given value is an object. | |
* | |
* @param {unknown} value - The value to check. | |
* @returns {boolean} True if the value is an object, otherwise false. | |
*/ | |
export function isObject<TObject = Record<PropertyKey, any>>(value: unknown): value is TObject { | |
return typeof value === 'object' && value !== null; | |
} | |
/** | |
* Checks if the given value is an empty object. | |
* | |
* @param {any} value - The value to check. | |
* @returns {boolean} True if the value is an empty object, false otherwise. | |
*/ | |
export function isEmptyObject(value: any): boolean { | |
return typeof value === 'object' && value !== null && Object.keys(value).length === 0; | |
} | |
/** | |
* Checks if the given value is an array. | |
* | |
* @template T - The type of elements in the array. | |
* @param {any} value - The value to check. | |
* @returns {value is Array<T>} True if the value is an array, false otherwise. | |
*/ | |
export function isArray<T>(value: any): value is Array<T> { | |
return Array.isArray(value); | |
} | |
/** | |
* Checks if the given value is an empty array. | |
* | |
* @param {any} value - The value to check. | |
* @returns {boolean} True if the value is an empty array, false otherwise. | |
*/ | |
export function isEmptyArray(value: any): boolean { | |
return Array.isArray(value) && value.length === 0; | |
} | |
/** | |
* Renames a property in an object from oldProp to newProp. | |
* | |
* @param {string} oldProp - The original property name. | |
* @param {string} newProp - The new property name. | |
* @param {{ [key: string]: any }} obj - The object containing the property to rename. | |
* @returns {{ [key: string]: any }} A new object with the property renamed. | |
*/ | |
export const renameProp = ( | |
oldProp: string, | |
newProp: string, | |
{ [oldProp]: old, ...others }: { [key: string]: any } | |
): { [key: string]: any } => ({ | |
[newProp]: old, | |
...others, | |
}); | |
/** | |
* Escapes special characters in a string to be used in a regular expression. | |
* | |
* @param {string} value - The string to escape. | |
* @returns {string} The escaped string. | |
*/ | |
export function escapeRegExp(value: string): string { | |
return value.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&'); | |
} | |
/** | |
* Creates an array containing the range [from, to). | |
* | |
* @param {number} from - The start of the range (inclusive). | |
* @param {number} to - The end of the range (exclusive). | |
* @returns {number[]} The array containing the range of numbers. | |
*/ | |
export function range(from: number, to: number): number[] { | |
return Array.from({ length: to - from }).map((_, i) => from + i); | |
} | |
/** | |
* Creates a memoized version of a function, caching the result based on the arguments provided. | |
* | |
* @template T - The type of the function to memoize. | |
* @param {T} fn - The function to memoize. | |
* @returns {T} A memoized version of the function. | |
*/ | |
export const memo = <T extends (...args: any[]) => any>(fn: T): T => { | |
const cache = new Map<string, ReturnType<T>>(); | |
const get = (...args: any[]) => { | |
const key = JSON.stringify(args); | |
if (cache.has(key)) { | |
return cache.get(key); | |
} | |
const result = fn(...args); | |
cache.set(key, result); | |
return result; | |
}; | |
return get as T; | |
}; | |
/** | |
* Options for converting the case of a string. | |
*/ | |
export interface CaseOptions { | |
to: "camel" | "pascal" | "kebab" | "snake"; | |
} | |
/** | |
* Converts the case of a string based on the provided options. | |
* | |
* @param {string} str - The string to convert. | |
* @param {CaseOptions} opts - The options specifying the target case. | |
* @returns {string} The string converted to the specified case. | |
*/ | |
export function convertCase(str: string, opts: CaseOptions = { to: "camel" }): string { | |
switch (opts.to) { | |
case "camel": | |
return str.replace(/([-_][a-z])/g, (group) => | |
group.toUpperCase().replace("-", "").replace("_", "") | |
); | |
case "pascal": | |
return str | |
.replace(/([-_][a-z])/g, (group) => | |
group.toUpperCase().replace("-", "").replace("_", "") | |
) | |
.replace(/^[a-z]/, (group) => group.toUpperCase()); | |
case "kebab": | |
return str | |
.replace(/[A-Z]/g, (group) => "-" + group.toLowerCase()) | |
.replace(/[_\s]+/g, '-') | |
.toLowerCase(); | |
case "snake": | |
return str | |
.replace(/[A-Z]/g, (group) => "_" + group.toLowerCase()) | |
.replace(/[-\s]+/g, '_') | |
.toLowerCase(); | |
default: | |
throw new Error(`Unsupported case conversion: ${opts.to}`); | |
} | |
} | |
/** | |
* Generates a unique ID with a specified prefix. | |
* | |
* @param {string} prefix - The prefix for the unique ID. | |
* @returns {string} The generated unique ID. | |
*/ | |
export function getUniqueID(prefix: string): string { | |
return `${prefix}-${Math.floor(Math.random() * 1000000)}`; | |
} | |
/** | |
* Prints a message to the console and exits the process with the specified exit code. | |
* | |
* @param {string} message - The message to print. | |
* @param {number} [code=1] - The exit code (default is 1). | |
* @returns {never} The function never returns as it exits the process. | |
*/ | |
export function printAndExit(message: string, code = 1): never { | |
if (code === 0) { | |
console.log(message); | |
} else { | |
console.error(message); | |
} | |
return process.exit(code); | |
} | |
/** | |
* Generates a unique key by combining the provided key with a random number. | |
* | |
* @param {string} key - The base key to be combined with a random number. | |
* @returns {string} The generated unique key. | |
*/ | |
function createKey(key: string) { | |
return `${key}-${~~(Math.random() * 1e7)}`; | |
} | |
/** | |
* Get the value of a cookie | |
* Source: https://vanillajstoolkit.com/helpers/getcookie/ | |
* @param name - The name of the cookie | |
* @return The cookie value | |
*/ | |
export function getCookie(name: string): string | undefined { | |
if (typeof document === 'undefined') { | |
throw new Error( | |
'getCookie() is not supported on the server. Fallback to a different value when rendering on the server.', | |
); | |
} | |
const value = `; ${document.cookie}`; | |
const parts = value.split(`; ${name}=`); | |
if (parts.length === 2) { | |
return parts[1].split(';').shift(); | |
} | |
return undefined; | |
} | |
// Logger | |
// https://github.com/vercel/turbo/blob/main/packages/turbo-utils/src/logger.ts | |
// https://github.com/vercel/next.js/blob/canary/packages/next/src/build/output/log.ts | |
// https://github.com/remix-run/remix/blob/main/packages/create-remix/utils.ts | |
// https://github.com/vercel/turbo/blob/main/packages/turbo-utils/src/logger.ts | |
// React Context | |
// https://github.com/nextui-org/nextui/blob/canary/packages/utilities/react-utils/src/context.ts | |
// Spinner | |
// https://github.com/vercel/next.js/blob/canary/packages/next/src/build/spinner.ts | |
// https://github.com/vercel/next.js/blob/canary/packages/next/src/build/progress.ts |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment