mirror of
https://github.com/supabase/supabase.git
synced 2026-10-06 01:45:10 +03:00
* open source section * Implement kFormatter utility for number formatting in thousands notation and update OpenSourceSection to use it for displaying GitHub stars.
238 lines
6.7 KiB
TypeScript
238 lines
6.7 KiB
TypeScript
// get reading time
|
|
// returns :string
|
|
export const generateReadingTime = (text: string) => {
|
|
const wordsPerMinute = 200
|
|
const noOfWords = text.split(/\s/g).length
|
|
const minutes = noOfWords / wordsPerMinute
|
|
const readTime = Math.ceil(minutes)
|
|
return `${readTime} minute read`
|
|
}
|
|
// Helps with the TypeScript issue where filtering doesn't narrows undefined nor null types, check https://github.com/microsoft/TypeScript/issues/16069
|
|
export function isNotNullOrUndefined<T>(value: T | null | undefined): value is T {
|
|
return value !== null && value !== undefined
|
|
}
|
|
|
|
export function capitalize(word: string) {
|
|
return word[0].toUpperCase() + word.substring(1).toLowerCase()
|
|
}
|
|
|
|
export function isMobileOrTablet() {
|
|
// https://stackoverflow.com/a/8876069/114157
|
|
const viewportWidth = Math.max(document.documentElement.clientWidth || 0, window.innerWidth || 0)
|
|
return viewportWidth < 1200
|
|
}
|
|
|
|
// Convert numbers or strings to pixel value
|
|
// Helpful for styled-jsx when using a prop
|
|
// height: ${toPixels(height)}; (supports height={20} and height="20px")
|
|
|
|
export const toPixels = (value: string | number) => {
|
|
if (typeof value === 'number') {
|
|
return `${value}px`
|
|
}
|
|
|
|
return value
|
|
}
|
|
|
|
export const isBrowser = typeof window !== 'undefined'
|
|
|
|
export const stripEmojis = (str: string) =>
|
|
str
|
|
.replace(
|
|
/([\u2700-\u27BF]|[\uE000-\uF8FF]|\uD83C[\uDC00-\uDFFF]|\uD83D[\uDC00-\uDFFF]|[\u2011-\u26FF]|\uD83E[\uDD10-\uDDFF])/g,
|
|
''
|
|
)
|
|
.replace(/\s+/g, ' ')
|
|
.trim()
|
|
|
|
// Vanilla JavaScript implementations to replace lodash functions
|
|
|
|
/**
|
|
* Creates an array of numbers (positive and/or negative) progressing from start up to, but not including, end.
|
|
* @param start The start of the range
|
|
* @param end The end of the range
|
|
* @param step The value to increment or decrement by
|
|
* @returns Returns the range of numbers
|
|
*/
|
|
export const range = (start: number, end?: number, step: number = 1): number[] => {
|
|
if (end === undefined) {
|
|
end = start
|
|
start = 0
|
|
}
|
|
|
|
const result: number[] = []
|
|
for (let i = start; step > 0 ? i < end : i > end; i += step) {
|
|
result.push(i)
|
|
}
|
|
return result
|
|
}
|
|
|
|
/**
|
|
* Converts string to start case.
|
|
* @param string The string to convert
|
|
* @returns Returns the start cased string
|
|
*/
|
|
export const startCase = (string: string): string => {
|
|
if (!string) return string
|
|
|
|
return string
|
|
.replace(/[-_\s]+/g, ' ')
|
|
.replace(/([a-z])([A-Z])/g, '$1 $2')
|
|
.replace(/\b\w/g, (char) => char.toUpperCase())
|
|
.trim()
|
|
}
|
|
|
|
/**
|
|
* Creates a debounced function that delays invoking func until after wait milliseconds have elapsed since the last time the debounced function was invoked.
|
|
* @param func The function to debounce
|
|
* @param wait The number of milliseconds to delay
|
|
* @param options The options object
|
|
* @returns Returns the new debounced function
|
|
*/
|
|
/**
|
|
* Formats numbers into thousands (K) notation with intelligent decimal handling.
|
|
* Returns raw number string for values < 1000 to avoid "0K"/"0.xK" outputs.
|
|
* @param value The value to format (number, string, null, or undefined)
|
|
* @returns Formatted string (e.g., "999", "1K", "12.5K")
|
|
*/
|
|
export const kFormatter = (value: number | string | null | undefined): string => {
|
|
// Coerce to safe number, guarding against NaN
|
|
const num = Number(value ?? 0)
|
|
const safeNum = isNaN(num) ? 0 : num
|
|
|
|
// Early return for values less than 1000
|
|
if (safeNum < 1000) {
|
|
return String(Math.floor(safeNum))
|
|
}
|
|
|
|
const kFormat = Math.floor(safeNum / 1000)
|
|
const lastTwoDigits = safeNum % 1000
|
|
|
|
const decimalPart = Math.floor((lastTwoDigits % 100) / 10)
|
|
const hundreds = Math.floor(lastTwoDigits / 100)
|
|
|
|
const isAlmostNextThousand = decimalPart >= 8 && hundreds >= 9
|
|
|
|
const showDecimals =
|
|
(!isAlmostNextThousand && hundreds >= 1) || (hundreds === 0 && decimalPart >= 8)
|
|
|
|
return showDecimals
|
|
? `${kFormat}.${decimalPart >= 8 ? hundreds + 1 : hundreds}K`
|
|
: `${isAlmostNextThousand ? kFormat + 1 : kFormat}K`
|
|
}
|
|
|
|
export const debounce = <T extends (...args: any[]) => any>(
|
|
func: T,
|
|
wait: number,
|
|
options: { leading?: boolean; trailing?: boolean } = {}
|
|
): ((...args: Parameters<T>) => void) & {
|
|
cancel: () => void
|
|
flush: () => void | undefined
|
|
pending: () => boolean
|
|
} => {
|
|
let timeoutId: ReturnType<typeof setTimeout> | undefined
|
|
let lastCallTime: number | undefined
|
|
let lastInvokeTime = 0
|
|
|
|
const { leading = false, trailing = true } = options
|
|
|
|
function invokeFunc(time: number, ...args: Parameters<T>) {
|
|
lastInvokeTime = time
|
|
func.apply(null, args)
|
|
}
|
|
|
|
function startTimer(pendingFunc: () => void, wait: number) {
|
|
return setTimeout(pendingFunc, wait)
|
|
}
|
|
|
|
function cancelTimer(id: ReturnType<typeof setTimeout>) {
|
|
clearTimeout(id)
|
|
}
|
|
|
|
function leadingEdge(time: number, ...args: Parameters<T>) {
|
|
lastInvokeTime = time
|
|
timeoutId = startTimer(timerExpired, wait)
|
|
return leading ? invokeFunc(time, ...args) : undefined
|
|
}
|
|
|
|
function remainingWait(time: number) {
|
|
const timeSinceLastCall = time - (lastCallTime || 0)
|
|
const timeSinceLastInvoke = time - lastInvokeTime
|
|
const timeWaiting = wait - timeSinceLastCall
|
|
|
|
return Math.min(timeWaiting, wait - timeSinceLastInvoke)
|
|
}
|
|
|
|
function shouldInvoke(time: number) {
|
|
const timeSinceLastCall = time - (lastCallTime || 0)
|
|
const timeSinceLastInvoke = time - lastInvokeTime
|
|
|
|
return (
|
|
lastCallTime === undefined ||
|
|
timeSinceLastCall >= wait ||
|
|
timeSinceLastCall < 0 ||
|
|
timeSinceLastInvoke >= wait
|
|
)
|
|
}
|
|
|
|
function timerExpired() {
|
|
const time = Date.now()
|
|
if (shouldInvoke(time)) {
|
|
return trailingEdge(time)
|
|
}
|
|
timeoutId = startTimer(timerExpired, remainingWait(time))
|
|
}
|
|
|
|
function trailingEdge(time: number) {
|
|
timeoutId = undefined
|
|
|
|
if (trailing) {
|
|
// For trailing edge, we don't have the original arguments, so we call without them
|
|
return invokeFunc(time, ...([] as any))
|
|
}
|
|
}
|
|
|
|
function cancel() {
|
|
if (timeoutId !== undefined) {
|
|
cancelTimer(timeoutId)
|
|
}
|
|
lastInvokeTime = 0
|
|
lastCallTime = undefined
|
|
timeoutId = undefined
|
|
}
|
|
|
|
function flush() {
|
|
return timeoutId === undefined ? undefined : trailingEdge(Date.now())
|
|
}
|
|
|
|
function pending() {
|
|
return timeoutId !== undefined
|
|
}
|
|
|
|
function debounced(this: any, ...args: Parameters<T>) {
|
|
const time = Date.now()
|
|
const isInvoking = shouldInvoke(time)
|
|
|
|
lastCallTime = time
|
|
|
|
if (isInvoking) {
|
|
if (timeoutId === undefined) {
|
|
return leadingEdge(lastCallTime, ...args)
|
|
}
|
|
if (trailing) {
|
|
timeoutId = startTimer(timerExpired, wait)
|
|
return invokeFunc(lastCallTime, ...args)
|
|
}
|
|
}
|
|
if (timeoutId === undefined) {
|
|
timeoutId = startTimer(timerExpired, wait)
|
|
}
|
|
}
|
|
|
|
debounced.cancel = cancel
|
|
debounced.flush = flush
|
|
debounced.pending = pending
|
|
|
|
return debounced
|
|
}
|