Docs
Utils

Utils

Some helper functions.

Installation

Create lib/utils.ts

import type { ForwardedRef, ReactElement, SyntheticEvent } from "react";
import { forwardRef } from "react";
import type { ClassValue } from "clsx";
import clsx from "clsx";
import { twMerge } from "tailwind-merge";
 
/**
 * An alternative to the standard reduce function that infers the type of the initial value from
 * the given array type.
 * @param array The array to reduce.
 * @param init The initial value of the accumulator.
 * @param callbackFn The function to execute on each element in the array.
 * @returns The reduced value.
 */
export function reduce<T>(
  array: T[],
  init: T[],
  callbackFn: (acc: T[], curr: T) => T[],
) {
  return array.reduce<T[]>(callbackFn, init);
}
 
/**
 * Reduces a record of arrays into a record of reduced values.
 *
 * @template T The type of the items in the input arrays.
 * @template R The type of the reduced values.
 * @param {Record<string, T[]>} record The input record of arrays.
 * @param {(item: T) => R} init The function to initialize the reduced value.
 * @param {(acc: R, item: T) => R} reduce The function to reduce the items in the array.
 * @returns {Record<string, R>} The record of reduced values.
 */
export function reduceRecord<T, R>(
  record: Record<string, T[]>,
  init: (item: T) => R,
  reduce: (acc: R, item: T) => R,
): Record<string, R> {
  const result: Record<string, R> = {};
  for (const [key, value] of Object.entries(record)) {
    for (const item of value) {
      if (!(key in result)) {
        result[key] = init(item);
      } else {
        result[key] = reduce(result[key]!, item);
      }
    }
  }
  return result;
}
 
/**
 * Reduces the values of a Map by key, applying an initial value and a reducer function.
 * @param map The Map to reduce.
 * @param init The initial value to apply to each key.
 * @param reduce The reducer function to apply to each value of each key.
 * @returns A new Map with the reduced values.
 */
export function reduceMap<T, R>(
  map: Map<string, T[]>,
  init: (item: T) => R,
  reduce: (acc: R, item: T) => R,
): Map<string, R> {
  const result = new Map<string, R>();
  for (const [key, value] of map.entries()) {
    for (const item of value) {
      if (!result.has(key)) {
        result.set(key, init(item));
      }
 
      result.set(key, reduce(result.get(key)!, item));
    }
  }
  return result;
}
 
/**
 * Groups the items in an array based on the result of a callback function.
 * @param items The array of items to group.
 * @param callbackFn The callback function used to determine the group for each item.
 * @returns An object with keys representing the groups and values representing the items in each group.
 */
export function group<T>(items: T[], callbackFn: (item: T) => string) {
  const groups: Record<string, T[]> = {};
 
  items.forEach((item) => {
    const group = callbackFn(item);
    groups[group] = groups[group] ?? [];
    groups[group]?.push(item);
  });
 
  return groups;
}
/**
 * Rounds a number to a specified number of digits after the decimal point.
 * @param num - The number to round.
 * @param digits - The number of digits after the decimal point to round to. Defaults to 2.
 * @returns The rounded number.
 */
 
export const round = (num: number, digits = 2) => {
  return (
    Math.round((num + Number.EPSILON) * Math.pow(10, digits)) /
    Math.pow(10, digits)
  );
};
/**
 * Formats a price as a string with an optional euro symbol.
 * @param price - The price to format.
 * @param options - An optional object with a `withEuro` property indicating whether to include the euro symbol.
 * @returns The formatted price as a string.
 */
 
export const priceAsString = (
  price?: number,
  options?: {
    withEuro?: boolean;
  },
) => {
  if (price === undefined) return "?? €";
  return (
    round(price).toFixed(2).replace(".", ",") + (options?.withEuro ? "€" : "")
  );
};
 
export const formatMoney = (amount: number | { amount: number }) => {
  if (typeof amount === "number") {
    return priceAsString(amount / 100, { withEuro: true });
  } else {
    return priceAsString(amount.amount / 100, { withEuro: true });
  }
};
/**
 * Delays the execution of the function by the specified amount of time.
 * @param ms - The number of milliseconds to delay the execution.
 * @returns A promise that resolves after the specified delay.
 */
 
export function sleep(ms = 3000) {
  return new Promise((resolve) => setTimeout(resolve, ms));
}
/**
 * Formats a total number of seconds into a string representation of hours, minutes, and seconds.
 * @param totalSeconds - The total number of seconds to format.
 * @returns A string representation of the total number of seconds in the format "HH:MM:SS".
 */
 
export function formatTime(totalSeconds: number): string {
  const hours = Math.floor(totalSeconds / 3600);
  const minutes = Math.floor((totalSeconds % 3600) / 60);
  const seconds = totalSeconds % 60;
 
  const formattedHours = hours.toString().padStart(2, "0");
  const formattedMinutes = minutes.toString().padStart(2, "0");
  const formattedSeconds = seconds.toString().padStart(2, "0");
 
  return `${formattedHours}:${formattedMinutes}:${formattedSeconds}`;
}
 
/**
 * This is a hack to get around the fact that TS doesn't allow you to infer a const type.
 * @param array - A readonly array of strings to infer the type from.
 * @param item - The item of the array to return.
 * @param asString - If true, the return type will be a string. Otherwise, it will be the array item constant.
 * @returns The array item constant or a string.
 *
 * @example
 * const array = ["a", "b", "c"] as const;
 * const item = inferConst(array, "b"); // item is "b" with type "b"
 * const item = inferConst(array, "b", true); // item is "b" with type string
 */
 
export const inferConst = <
  TArray extends readonly string[],
  TKey extends TArray[number],
  TAsString extends boolean = false,
>(
  array: TArray,
  item: TKey,
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
  asString?: TAsString,
): TAsString extends true ? string : TKey => item;
 
export function cn(...inputs: ClassValue[]) {
  return twMerge(clsx(inputs));
}
 
export function getStackTrace(maxDepth = 8) {
  try {
    throw new Error();
  } catch (e) {
    if (
      e !== null &&
      typeof e === "object" &&
      "stack" in e &&
      typeof e.stack === "string"
    ) {
      // split the stack into lines
      const lines = e.stack.split("\n");
 
      // first line is "Error" -> remove it
      lines.shift();
 
      // every line begins with space and then "at" -> remove it
      let i = 0;
 
      const functions: string[] = [];
      for (const line of lines) {
        if (i > maxDepth - 1) {
          break;
        }
        const tokens = line.trim().split(" ");
        if (tokens[0] === "at" && tokens.length > 1) {
          tokens.shift();
        }
        functions.push(tokens[0]!);
        i++;
      }
 
      return functions;
    }
  }
 
  return [];
}
 
interface AutoRefFunction {
  (props: any): ReactElement | null;
  displayName?: string;
}
 
export function autoRef<
  Fn extends AutoRefFunction,
  Props extends { ref?: RefType },
  RefType,
>(fn: Fn) {
  const AutoRef = (props: Props, ref: ForwardedRef<RefType>) =>
    fn({ ...props, ref });
  AutoRef.displayName = fn.displayName || fn.name || "AutoRef";
  return forwardRef(AutoRef) as unknown as Fn;
}
 
export function withRenderProps<
  TReturn extends React.ReactNode | string,
  TRenderProps,
>(
  prop: TReturn | ((values: TRenderProps) => TReturn),
): (values: TRenderProps) => TReturn {
  return (values: TRenderProps) =>
    typeof prop === "function" ? prop(values) : prop;
}
 
export function transformToImageEvent(
  e: SyntheticEvent<HTMLImageElement, Event>,
) {
  let width = 0;
  let height = 0;
 
  if ("naturalWidth" in e.target && typeof e.target.naturalWidth === "number") {
    width = e.target.naturalWidth;
  }
 
  if (
    "naturalHeight" in e.target &&
    typeof e.target.naturalHeight === "number"
  ) {
    height = e.target.naturalHeight;
  }
 
  return {
    ...e,
    target: {
      ...e.target,
      width,
      height,
    },
  };
}