// src/lib/utils.ts
import { type ClassValue, clsx } from 'clsx';
import { twMerge } from 'tailwind-merge';
/**
* Helper for conditionally adding and de-duplicating Tailwind CSS Classes
*/
export function cn(...inputs, ClassValue[]) {
return twMerge(clsx(inputs));
}
export function truncate(str, length) {
if (!str || typeof str !== 'string' || str.length <= length) {
return str;
}
const subString = str.substr(0, length - 1);
return `${subString.substr(0, subString.lastIndexOf(' '))} …`;
}
export const isValidUrl = url => {
try {
new URL(url);
return true;
} catch (e) {
return false;
}
};
/**
* Validate a relative path.
* > isValidRelativeUrl('a/b/c/d/e/f/g')
* true
* > isValidRelativeUrl('about.html')
* true
* > isValidRelativeUrl('//')
* false
* > isValidRelativeUrl('//xxx')
* false
* > isValidRelativeUrl('https://google.com')
* false
*/
export const isValidRelativeUrl = url => {
url = url?.trim();
if (!url) {
return false;
}
try {
// If we're able to construct a URL, it means it's an absolute URL.
new URL(url);
return false;
} catch (e) {
// Prevent URLs like //example.com or /\n/example.com or /\/example.com/
if (url.match(/^[\s\\/]{2,}.+/)) {
return false;
} else {
return true;
}
}
};
export const isValidEmail = email => {
if (typeof email !== 'string') {
return false;
}
return email.match(
/^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/,
);
};
export function parseToBoolean(value, defaultValue = false) {
let lowerValue = value;
// check whether it's string
if (lowerValue && (typeof lowerValue === 'string' || lowerValue instanceof String)) {
lowerValue = lowerValue.trim().toLowerCase();
}
if (['on', 'enabled', '1', 'true', 'yes', 1].includes(lowerValue)) {
return true;
}
return defaultValue;
}
export function getQueryParams() {
const urlParams = {};
let match;
const pl = /\+/g, // Regex for replacing addition symbol with a space
search = /([^&=]+)=?([^&]*)/g,
decode = function (s) {
return decodeURIComponent(s.replace(pl, ' '));
},
query = window.location.search.substring(1);
// eslint-disable-next-line no-cond-assign
while ((match = search.exec(query))) {
urlParams[decode(match[1])] = decode(match[2]);
}
return urlParams;
}
export function formatDate(
date,
options: Intl.DateTimeFormatOptions & { locale?: Intl.LocalesArgument } = { month: 'long', year: 'numeric' },
) {
const d = new Date(date);
const locale = typeof window !== 'undefined' ? window.navigator.language : options.locale || 'en-US';
try {
return d.toLocaleDateString(locale, options);
} catch {
try {
return d.toLocaleDateString('en-US', options);
} catch {
return d.toString();
}
}
}
export const singular = str => {
if (!str) {
return '';
}
return str.replace(/ies$/, 'y').replace(/s$/, '');
};
export const capitalize = str => {
if (typeof str !== 'string') {
return '';
}
str = str.trim();
if (str.length === 0) {
return '';
}
return `${str[0].toUpperCase()}${str.substr(1)}`;
};
const trim = (str, length) => {
if (!str) {
return '';
}
if (str.length <= length) {
return str;
}
const res = [];
let resLength = 0;
const words = str.split(' ');
let i = 0;
while (resLength < length && i < words.length) {
const w = words[i++];
resLength += w.length + 1;
res.push(w);
}
return `${res.join(' ')} …`;
};
export const firstSentence = (str, length) => {
if (!str) {
return '';
}
str = str.replace(/&/g, '&');
if (str.length <= length) {
return str;
}
const tokens = str.match(/\.|\?|!/);
if (tokens) {
str = str.substr(0, tokens.index + 1);
}
str = trim(str, length);
return str;
};
export const loadScriptAsync = (url, opts = {}) =>
new Promise((resolve, reject) => {
loadScript(url, opts, (err, script) => {
if (err) {
reject(err);
} else {
resolve(script);
}
});
});
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions#Using_special_characters
// From section about escapting user input
export const escapeInput = string => string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
export const getWebsiteUrl = () => {
if (typeof window !== 'undefined' && window.location) {
return `${window.location.protocol}//${window.location.host}`;
} else {
return process.env.WEBSITE_URL;
}
};
export function compose(...funcs) {
const functions = funcs.reverse();
return function (...args) {
const [firstFunction, ...restFunctions] = functions;
let result = firstFunction.apply(null, args);
restFunctions.forEach(fnc => {
result = fnc.call(null, result);
});
return result;
};
}
/** This function will return true if reportValidity is not supported by the browser, or if it succeed */
export const reportValidityHTML5 = domNodeOrEvent => {
return !domNodeOrEvent || typeof domNodeOrEvent.reportValidity !== 'function' || domNodeOrEvent.reportValidity();
};
/**
* Repeat `func` for `nbTimes`, calling it every `interval` ms.
* Passes one parameter: the number of iterations left.
*/
export const repeatWithInterval = (nbTimes, interval, func) => {
func(nbTimes);
if (nbTimes - 1 > 0) {
setTimeout(() => repeatWithInterval(nbTimes - 1, interval, func), interval);
}
};
/**
* Similar to `Promise.allSettled` (which doesn't have a great browser support yet)
*/
export const allSettled = promises => {
return Promise.all(
promises.map(promise => {
return Promise.resolve(promise).then(
val => ({ status: 'fulfilled', value: val }),
err => ({ status: 'rejected', reason: err }),
);
}),
);
};
/**
* Returns flat object containing keys with values that are not empty objects.
* Ex:
* flattenObjectDeep({ b: true, c: { d: {}, e: false }})
* // {b: true, e: false}
*
* flattenObjectDeep({ c: { d: {} }})
* // {}
*/
export const flattenObjectDeep = obj =>
Object.keys(obj).reduce(
(acc, k) => (typeof obj[k] === 'object' ? { ...acc, ...flattenObjectDeep(obj[k]) } : { ...acc, [k]: obj[k] }),
{},
);
export const omitDeep = (obj, keys) =>
Object.keys(omit(obj, keys)).reduce(
(acc, next) => ({ ...acc, [next]: isObject(obj[next]) ? omitDeep(obj[next], keys) : obj[next] }),
{},
);
/**
* Sort options as: All, then by alphabetical order, then "No payment method" or "Other" at the end
*/
export const sortSelectOptions = (option1, option2) => {
if (option1.value === 'ALL') {
return -1;
}
if (option2.value === 'ALL') {
return 1;
}
if (option1.value === null) {
return 1;
}
if (option2.value === null) {
return -1;
}
if (option1.value === 'OTHER') {
return 1;
}
if (option2.value === 'OTHER') {
return -1;
}
return option1.label.localeCompare(option2.label);
};