Skip to content

Instantly share code, notes, and snippets.

@vicasas
Last active June 30, 2024 14:04
Show Gist options
  • Save vicasas/220f7ccd68cd811ca44c6e607511aa89 to your computer and use it in GitHub Desktop.
Save vicasas/220f7ccd68cd811ca44c6e607511aa89 to your computer and use it in GitHub Desktop.
Utilities to JavaScript and TypeScript
/**
* 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