Skip to content

Instantly share code, notes, and snippets.

@isocroft
Last active July 4, 2024 21:46
Show Gist options
  • Save isocroft/e39c96f4eae67c431916636ad273e7fe to your computer and use it in GitHub Desktop.
Save isocroft/e39c96f4eae67c431916636ad273e7fe to your computer and use it in GitHub Desktop.
A collection of very useful helper functions for frontend working with both React and Vanilla JS
import { lazy } from "react";
import type { JSX } from "react";
/**
* lazyWithRetry:
*
* @param {() => (() => JSX.Element)} componentImport
* @returns {() => JSX.Element}
*
* @see https://gist.github.com/raphael-leger/4d703dea6c845788ff9eb36142374bdb#file-lazywithretry-js
*
*/
export const lazyWithRetry = <Props extends Record<string, unknown>>(
componentImport: () => Promise<{ default: (props: Props) => JSX.Element | null }>,
retryStorageKey = "page-has-been-force-refreshed"
) =>
lazy<React.ComponentType<Props | undefined>>(async () => {
const pageHasAlreadyBeenForceRefreshed = JSON.parse(
window.sessionStorage.getItem(
retryStorageKey
) || "false"
) as boolean;
try {
const component = await componentImport();
window.sessionStorage.setItem(
retryStorageKey,
"false"
);
return component;
} catch (error) {
if (!pageHasAlreadyBeenForceRefreshed) {
/* @HINT: Assuming that the user is not on the latest version of the application. */
/* @HINT: Let's refresh the page immediately. */
window.sessionStorage.setItem(
retryStorageKey,
"true"
);
window.location.reload();
} else {
/* @HINT: If we get here, it means the page has already been reloaded */
/* @HINT: Assuming that user is already using the latest version of the application. */
/* @HINT: Let's let the application crash and raise the error. */
throw error;
}
return { default: () => null };
}
});
};
/* @EXAMPLE: lazyWithRetry() */
/**
* componentLoader:
*
* @param {() => Promise} lazyComponent
* @param {Number} attemptsLeft
*
* @returns
*
* @see https://medium.com/@botfather/react-loading-chunk-failed-error-88d0bb75b406
*
*/
export function componentLoader<M extends { default: () => JSX.Element | null }>(
lazyComponent: () => Promise<M>,
attemptsLeft = 3
) {
return new Promise<M>((resolve, reject) => {
lazyComponent()
.then(resolve)
.catch((error) => {
/* @HINT: let us retry after 1500 milliseconds */
setTimeout(() => {
if (attemptsLeft === 1) {
reject(error);
return;
}
componentLoader(lazyComponent, attemptsLeft - 1).then(resolve, reject);
}, 1500);
});
});
}
/**
* fileExtension:
*
* @param {String} urlOrFileType
*
* @returns {String}
*/
export const fileExtension = (urlOrFileType?: string | null): string => {
let extension = "blob";
if (
urlOrFileType === "image/png" ||
urlOrFileType === "image/jpeg" ||
urlOrFileType === "image/jpg" ||
urlOrFileType === "image/svg+xml" ||
urlOrFileType === "application/pdf" ||
urlOrFileType === "text/plain" ||
urlOrFileType === "application/json" ||
urlOrFileType === "text/javascript" ||
urlOrFileType === "text/css" ||
urlOrFileType === "text/csv" ||
urlOrFileType === "text/x-csv" ||
urlOrFileType === "application/vnd.ms-excel" ||
urlOrFileType === "application/csv" ||
urlOrFileType === "application/x-csv" ||
urlOrFileType === "text/comma-separated-values" ||
urlOrFileType === "text/x-comma-separated-values" ||
urlOrFileType === "text/tab-separated-values" ||
urlOrFileType === "application/octet-stream"
) {
[ extension ] = (urlOrFileType || "/").split("/").reverse();
if (extension === "octet-stream") {
extension = "blob";
}
if ([
"x-csv",
"vnd.ms-excel",
"tab-separated-values",
"comma-separated-values",
"x-comma-separated-values"].includes(extension)) {
extension = "csv";
}
} else if (typeof urlOrFileType === "string") {
const [ urlBaseName ] = urlOrFileType.split(/[#?]/);
[ extension ] = urlBaseName.split(".").reverse();
}
return extension === "javascript" ? "js" : extension;
};
/* @EXAMPLE: fileExtension("image/png") */
/**
* composeClasses:
*
* @param {Array.<*>} styles
*
* @returns {String}
*/
export const composeClasses = (...styles: unknown[]): string => {
return styles.filter((item) => item).join(' ')
}
/* @EXAMPLE: <AvatarWrapper className={composeClasses('text-align-center', 'position-relative')} /> */
/**
* bloToataURL:
*
* @param {Blob} blob
*
* @returns {Promise<String>}
*
*/
export const blobToDataURL = (blob: Blob): Promise<string> => {
return new Promise((fulfill: Function, reject: Function) => {
let reader: FileReader = new FileReader()
reader.onerror = (ev: ProgressEvent<FileReader>) =>
reject(ev.target?.error)
reader.onload = () => fulfill(reader.result)
reader.readAsDataURL(blob)
})
}
/**
* dataURLtoObjectURL: converts a data URI to an object URL
*
*
* @param {String} dataURL
*
* @returns {String}
*
* @see https://en.wikipedia.org/wiki/Data_URI_scheme/
*/
export const dataURLtoObjectURL = (dataURL?: string): string => {
const [ mimeType, base64String ] = (dataURL || ",").split(",");
const [, contentTypeDataPrefix ] = mimeType.split(":") || [, ";"];
const [ contentType ] = contentTypeDataPrefix
? contentTypeDataPrefix.split(";")
: ["application/octet-stream"];
return URL.createObjectURL(
base64StringToBlob(base64String, contentType)
);
};
/**
* dataURLtoObjectBlob: converts a data URI to a blob
*
*
* @param {String} dataURL
*
* @returns {Blob}
*
* @see https://en.wikipedia.org/wiki/Data_URI_scheme/
* @see https://en.wikipedia.org/wiki/Binary_large_object/
*/
export const dataURLtoObjectBlob = (dataURL?: string): Blob => {
const [ mimeType, base64String ] = (dataURL || ",").split(",");
const [, contentTypeDataPrefix ] = mimeType.split(":") || [, ";"];
const [ contentType ] = contentTypeDataPrefix
? contentTypeDataPrefix.split(";")
: ["application/octet-stream"];
return base64StringToBlob(base64String, contentType);
};
/*!
* @EXAMPLE:
*
* const fileBlob = dataURItoObjectBlob(
* "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVQYV2NgYAAAAAMAAWgmWQ0AAAAASUVORK5CYII="
* );
* console.log(fileBlob); // Blob {size: 68, type: 'image/png'}
*/
/**
* blobToFile:
*
* @param {Blob | Undefined} theBlob
* @param {String | Null | Undefined} fileName
* @param {Boolean} useCast
*
* @returns {File}
*
*/
export const blobToFile = (theBlob?: Blob, fileName?: string | null, useCast = false): File => {
const todaysDate = new Date();
const defaultFileName = `${todaysDate.getTime()}_${Math.random() * 1}`;
const defaultFileExtension = `.${fileExtension(theBlob?.type)}`;
const fullFileName = defaultFileName + defaultFileExtension;
if (!(theBlob instanceof window.Blob)) {
return new File([""], fullFileName);
}
const blob = <Blob & { lastModifiedDate: Date, name: string }>theBlob;
blob.lastModifiedDate = new Date();
blob.name = fileName || fullFileName;
return useCast ? <File>theBlob : new File([theBlob], fileName || fullFileName);
};
/* @EXAMPLE: blobToFile(new Blob(['hello!'], { type: 'text/plain' }), "text.txt") */
/**
* base64StringToBlob:
*
* @param {String} base64Data
* @param {String} contentType
* @param {Number} sliceSize
*
* @returns {Blob}
*
*/
export const base64StringToBlob = (base64Data: string, contentType?: string | null, sliceSize = 512) => {
const $contentType = contentType || "";
const byteCharacters = atob(base64Data);
const byteArrays = [];
for (let offset = 0; offset < byteCharacters.length; offset += sliceSize) {
const slice = byteCharacters.slice(offset, offset + sliceSize);
const byteNumbers = new Array(slice.length);
for (let i = 0; i < slice.length; i++) {
byteNumbers[i] = slice.charCodeAt(i);
}
const byteArray = new Uint8Array(byteNumbers);
byteArrays.push(byteArray);
}
return new Blob(byteArrays, { type: $contentType });
};
/* @EXAMPLE: const urlString = blobToDataURL(new Blob(['hello world'], { type: 'text/plain' })) */
/**
* getJpegBlob:
*
* @param {HTMLCanvasElement | null} canvas
*
* @return {Promise<Blob | null>}
*
*/
export function getJpegBlob(canvas: HTMLCanvasElement | null): Promise<Blob | null> {
/* @CHECK: https://stackoverflow.com/a/46182044/5221762 */
/* @NOTE: May require the `toBlob()` polyfill */
if (!HTMLCanvasElement.prototype.toBlob) {
window.Object!.defineProperty(HTMLCanvasElement.prototype, 'toBlob', {
value: function (callback, type, quality) {
const canvas = this;
window.setTimeout(function() {
var binStr = atob( canvas.toDataURL(type, quality).split(',')[1] ),
len = binStr.length,
arr = new Uint8Array(len);
for (let index = 0; index < len; index++ ) {
arr[index] = binStr.charCodeAt(index);
}
callback( new Blob( [arr], {type: type || 'image/png'} ) );
});
}
});
}
return new Promise((resolve, reject) => {
try {
if (canvas) {
canvas.toBlob(blob => resolve(blob), 'image/jpeg', 0.95);
}
} catch (e) {
reject(e);
}
})
};
/* @EXAMPLE: getJpegBlob(window.document.getElementsByTagName('canvas')[0]) */
/**
* getJpegBytes:
*
* @param {HTMLCanvasElement | null} canvas
*
* @return {Promise<string | ArrayBuffer | null>}
*
*/
export function getJpegBytes(canvas: HTMLCanvasElement | null): Promise<string | ArrayBuffer | null> {
return getJpegBlob(canvas).then((blob) => {
return new Promise((resolve, reject) => {
const fileReader = new FileReader()
fileReader.addEventListener('loadend', () => {
if (this.error) {
reject(this.error)
return
}
resolve(this.result)
})
if (blob) {
fileReader.readAsArrayBuffer(blob);
}
})
})
};
/* @EXAMPLE: getJpegBytes(window.document.getElementsByTagName('canvas')[0]) */
/**
* isBase64String:
*
* @param {String} base64String
*
* @return {Boolean}
*
*/
export const isBase64String = (base64String: string): boolean => {
let base64Regex = /^([0-9a-zA-Z+/]{4})*(([0-9a-zA-Z+/]{2}==)|([0-9a-zA-Z+/]{3}=))?$/;
return typeof base64String !== 'string'
? false
: (base64String.length % 4 === 0) && base64Regex.test(base64String)
};
/* @EXAMPLE: isBase64String("") */
/**
* sleepFor:
*
* @param {Number} durationInMilliSeconds
*
* @return {void}
*
*/
export const sleepFor = (durationInMilliSeconds = 10) => {
return new Promise(
(resolve) => window.setTimeout(resolve, durationInMilliSeconds)
);
};
/* @EXAMPLE: await sleepFor(2500) */
/**
* waitFor:
*
*
* @param {Function} conditionCallback
* @param {Number} pollIntervalMilliSeconds
* @param {Number} timeoutAfterMilliSeconds
*
* @returns {Void}
*
* @see https://davidwalsh.name/waitfor/
*/
export const waitFor = async (
conditionCallback: () => boolean,
pollIntervalMilliSeconds = 50,
timeoutAfterMilliSeconds = 3000
) => {
const startTimeMilliSeconds = Date.now();
while (true) {
if (
typeof (timeoutAfterMilliSeconds) === "number"
&& Date.now() > startTimeMilliSeconds + timeoutAfterMilliSeconds) {
throw new Error("Condition not met bbefore timeout");
}
const result = conditionCallback();
if (result) {
return result;
}
await sleepFor(pollIntervalMilliSeconds);
}
};
/* @EXAMPLE: waitFor(() => window.document.body.classList.has('loaded'), 100, 5000) */
/**
* htmlEncode:
*
*
* @param {String} rawText
*
* @returns {String}
*
*/
export const htmlEncode = (rawText: string): string => {
return (rawText || "").replace(/[\u00A0-\u9999<>&]/gim, function (mark: string) {
return '&#' + mark.charCodeAt(0) + ';'
})
};
/* @EXAMPLE: const encodedHTML = htmlEncode('<h1><img onerror="javascript:return null" /></h1>'); */
/**
* htmlDecode:
*
*
* @param {String} encodedText
*
* @returns {String | Null}
*
*/
export const htmlDecode = (encodedText: string): string | null => {
const doc = new window.DOMParser().parseFromString(encodedText || "&nbsp;", 'text/html')
const docElem = doc.documentElement as Node
return docElem.textContent
};
/* @EXAMPLE: const decodedHTML = htmlDecode("&lt;h1&gt;Hi there!&lt;/h1&gt;"); */
/**
* detectFullScreenTrigger:
*
*
* @param {Event} event
*
* @returns {"user-manual" | "programmatic" | "unknown"}
*
*/
export const detectFullScreenTrigger = (event: Event): string => {
if (
window.matchMedia &&
window.matchMedia('(display-mode: fullscreen)').matches
) {
// page entered fullscreen mode through the Web Application Manifest
return 'user-manual'
} else if (document.fullscreenEnabled && document.fullscreenElement) {
// page entered fullscreen mode through the Fullscreen API
return 'programmatic'
}
return 'unknown'
};
/* @EXAMPLE: document.onfullscreenchange = detectFullScreenTrigger; */
/**
* detectAppleIOS:
*
*
* @returns {Boolean}
*
*/
export const detectAppleIOS = (): boolean => {
const global: Window = window
const navigator: Navigator = global.navigator
const userAgent = navigator.userAgent.toLowerCase()
const vendor = navigator.vendor.toLowerCase()
return /iphone|ipad|ipod/.test(userAgent) && vendor.indexOf('apple') > -1
}
/* @EXAMPLE: const isIOS = detectAppleIOS() */
/**
* isInStandaloneMode:
*
*
* @returns {Boolean}
*
*/
export const isInStandaloneMode = (): boolean => {
const global: Window = window
const navigator: Navigator = global.navigator
const location: Location = global.location
/**
* @CHECK: https://stackoverflow.com/questions/21125337/how-to-detect-if-web-app-running-standalone-on-chrome-mobile
*/
if (detectAppleIOS() && navigator instanceof Navigator) {
return navigator.standalone === true
}
return (
location.search.indexOf('standalone=true') !== -1 &&
Boolean(global.matchMedia('(display-mode: standalone)').matches) &&
(global.screen.height - document.documentElement.clientHeight < 40 ||
global.screen.width - document.documentElement.clientHeight < 40)
)
};
/* @EXAMPLE: const standalone = isInStandaloneMode(); */
/**
* formatHTMLEntity:
*
*
* @param {String} textValue
* @param {String} entityHexValue
* @param {String} prefix
*
* @returns {String}
*
*/
export const formatHTMLEntity = (
textValue: string,
entityHexVal: string,
prefix: string = ''
): string => {
const isNumeric = /^\d{2,5}$/.test(entityHexValue)
const number = parseInt(isNumeric ? "8" : entityHexValue, 16)
return (
(textValue ? textValue + ' ' : '') +
prefix + String.fromCharCode(number)
)
};
/* @EXAMPLE: <p className="wrapper">{formatHTMLEntity('View Full Project', '279D')}</p> */
/**
* isEmpty:
*
* @param {Object} objectValue
*
* @returns {Boolean}
*/
export function isEmpty<T>(objectValue: T): boolean {
if(!objectValue || typeof objectValue !== "object") {
return true;
}
for(const prop in objectValue) {
if(Object.prototype.hasOwnProperty.call(objectValue, prop)) {
return false;
}
}
return JSON.stringify(objectValue) === JSON.stringify({});
}
/* @EXAMPLE: isEmpty({}) */
/**
* slugify:
*
*
* @param {String} plainText
* @param {String} delimeter
*
* @returns {String}
*
*/
export const slugify = (plainText: string, delimeter = "_") => {
return (plainText || "")
.toString()
.normalize("NFD")
.replace(/[\u0300-\u036f]/g, "")
.toLowerCase()
.trim()
.replace(/[^a-z0-9 ]/g, "")
.replace(/\s+/g, delimeter);
};
/* @EXAMPLE: slugify('Last Name') */
/**
* unSlugify:
*
* @param {String} slugifiedText
* @param {String} delimeter
* @param {Boolean} shouldTrim
*
* @returns {String}
*
*/
export const unSlugify = (
slugifiedText: string,
delimeter = '_',
shouldTrim = false
): string => {
return (slugifiedText || '')
.split(delimeter)
.map(
(slugPart) =>
`${slugPart.charAt(0).toUpperCase()}${slugPart.substring(1)}`
).join(shouldTrim ? '' : ' ')
};
/* @EXAMPLE: unSlugify('first_name') */
/**
* getOrdinalSuffix:
*
*
* @param {Number} ordinal
* @param {Boolean} asWord
*
* @returns {String}
*
*/
export const getOrdinalSuffix = (ordinal: number, asWord = false): string => {
let ord = "th";
if (ordinal % 10 == 1 && ordinal % 100 != 11) {
ord = "st";
}
else if (ordinal % 10 == 2 && ordinal % 100 != 12) {
ord = asWord ? "ond" : "nd";
}
else if (ordinal % 10 == 3 && ordinal % 100 != 13) {
ord = "rd";
}
return ord;
};
/* @EXAMPLE: getOrdinalSuffix(23) */
/**
* getShortAmount:
*
* @param {Number} amount
*
* @returns {String}
*
*/
export const getShortAmount = (amount: number): string => {
const strFigure = String(Number.isNaN(amount) ? false : Math.round(amount))
const [firstPart, ...remainingParts] = strFigure.match(
/\d{1,3}(?=(\d{3})*$)/g
) || ['']
const shortenedMap: { [key: number]: string } = {
1: 'K',
2: 'M',
3: 'B',
4: 'T',
5: 'Z',
}
return firstPart !== '' && remainingParts.length
? firstPart + shortenedMap[remainingParts.length]
: firstPart
}
/* @EXAMPLE: getShortName(305000) */
/**
* isUUIDOrGUID:
*
* @param {String} uidLineText
*
* @returns {Boolean}
*
*/
export const isUUIDOrGUID = (uidLineText: string): boolean => {
const uuidRegExp = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i
const guidRegExp = /^(\{){0,1}[0-9a-fA-F]{8}\-[0-9a-fA-F]{4}\-[0-9a-fA-F]{4}\-[0-9a-fA-F]{4}\-[0-9a-fA-F]{12}(\}){0,1}$/gi
let uid = uidLineText;
if (uidLineText.startsWith('{')) {
uid = uidLineText.substring(1, uidLineText.length - 1);
}
return uuidRegExp.test(uid) || guidRegExp.test(uid)
}
/* @EXAMPLE: isUUIDOrGUID('a4caeacc-72cb-4824-80f8-b55961f148c6') */
/**
* isWebPageURL:
*
*
*
* @returns {Boolean}
*
*/
export const isWebPageURL = (urlString: string): boolean => {
let result = null;
try {
result = urlString.match(
/(http(s)?:\/\/.)?(www\.)?[-a-zA-Z0-9@:%._\+~#=]{2,256}\.[a-z0-9]{2,6}\b([-a-zA-Z0-9@:%_\+.~#?&//=]*)/g
)
} catch (e) {
result = null
}
return result !== null;
}
/* @EXAMPLE: isWebPageURL('https://www.example.com?id=98747904') */
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment