npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@ptx-showcase/utils

v1.2.0

Published

Shared utilities for PTX Showcase projects

Readme

@ptx-showcase/utils

Shared utilities for PTX Showcase projects.


Installation

npm install @ptx-showcase/utils

or

bun add @ptx-showcase/utils

Usage

All utilities can be imported from the root package, from a grouping subpath (/base, /admin, /core), or from each utility's own subpath.

  • /base — types and enums shared across projects (TableColumnConfig, PaginatedList, ListQueryParams, DISCOUNT_TYPES, ORDER_STATUSES, SUPPORT_STATUSES)
  • /admin — admin-panel-only concerns (isAdminToken, ADMIN_QUERY_KEYS); only relevant to admin projects
  • /core — every generic utility below, bundled together
  • Each generic utility is also importable on its own (/cookies, /build-query-params, etc.)

buildQueryParams

Builds a URL query string from an object. Skips null, undefined, empty strings, and empty arrays.

// from root
import { buildQueryParams } from '@ptx-showcase/utils'

// from subpath
import { buildQueryParams } from '@ptx-showcase/utils/build-query-params'

buildQueryParams({ page: 1, search: 'test', empty: '' })
// → '?page=1&search=test'

buildQueryParams({ ids: [], status: 'active' })
// → '?status=active'

buildQueryParams({})
// → ''

buildSortParam

Builds a comma-separated sort string with toggle logic: first call adds the field, second toggles to descending (-field), third removes it.

// from root
import { buildSortParam } from '@ptx-showcase/utils'

// from subpath
import { buildSortParam } from '@ptx-showcase/utils/build-sort-param'

buildSortParam('name') // → 'name'
buildSortParam('name', 'name') // → '-name'
buildSortParam('name', '-name') // → ''

// combining with existing sort
buildSortParam('age', 'name') // → 'age,name'
buildSortParam('age', 'age,name') // → '-age,name'

Cookies

Cookie helpers with secure defaults (expires: 1h, secure: true, sameSite: Strict).

// from root
import { setCookie, getCookie, deleteCookie } from '@ptx-showcase/utils'

// from subpath
import { setCookie, getCookie, deleteCookie } from '@ptx-showcase/utils/cookies'

// set (uses default options)
setCookie('token', 'abc123')

// set with custom options
setCookie('session', 'xyz', { expires: 7, sameSite: 'Lax' })

// get
const token = getCookie('token') // → 'abc123' | undefined

// delete
deleteCookie('token')

Default options:

| Option | Value | | ---------- | ---------- | | expires | 1 hour | | secure | true | | sameSite | 'Strict' |


downloadBlob

Downloads a Blob as a file in the browser. Accepts either an explicit filename or a hash value (auto-generates a SHA-1 filename).

// from root
import { downloadBlob } from '@ptx-showcase/utils'

// from subpath
import { downloadBlob } from '@ptx-showcase/utils/download-file'

const blob = new Blob(['Hello, World!'], { type: 'text/plain' })

// with explicit filename
await downloadBlob(blob, { filename: 'report.txt' })

// with auto-generated SHA-1 filename
await downloadBlob(blob, { hash: 12345 })
// → saves as e.g. '17b840d9...'

Flags

Resolves a flag icon URL for a language/country code, with a three-tier fallback: primary source (flagcdn.com) → secondary source (flagicons.lipis.dev) → an "unknown flag" placeholder. The placeholder is also returned directly when languageCode isn't a usable string at runtime (null, undefined, NaN, an object, an empty/whitespace string, etc.) — the functions never throw.

FLAG_CODE_ALIASES maps language codes that differ from their representative country's ISO code (e.g. en → gb, ja → jp); codes with no entry are used as-is.

// from root
import { getFlagUrl, handleFlagImageError } from '@ptx-showcase/utils'

// from subpath
import { getFlagUrl, handleFlagImageError } from '@ptx-showcase/utils/flags'

getFlagUrl('fr') // → 'https://flagcdn.com/fr.svg'
getFlagUrl('en') // → 'https://flagcdn.com/gb.svg' (resolved via FLAG_CODE_ALIASES)
getFlagUrl(null as unknown as string) // → unknown-flag placeholder URL

// React — escalates primary → fallback → unknown placeholder on load failure
<img src={getFlagUrl(code)} onError={(e) => handleFlagImageError(e, code)} />

// Vue
<img :src="getFlagUrl(code)" @error="(e) => handleFlagImageError(e, code)" />

// Svelte
<img src={getFlagUrl(code)} on:error={(e) => handleFlagImageError(e, code)} />

getCurrencySymbol

Resolves the symbol for an ISO 4217 currency code via Intl. A handful of currencies (AUD, CAD, NZD, CHF, etc.) are overridden where the Intl narrow symbol would be ambiguous.

// from root
import { getCurrencySymbol } from '@ptx-showcase/utils'

// from subpath
import { getCurrencySymbol } from '@ptx-showcase/utils/currency-symbol'

getCurrencySymbol('EUR') // → '€'
getCurrencySymbol('USD') // → '$'
getCurrencySymbol('AUD') // → 'A$' (override)
getCurrencySymbol('CHF') // → '₣' (override)
getCurrencySymbol('ZZZ') // → 'ZZZ' (well-formed but unknown code)
getCurrencySymbol('EURO') // → 'N/A' (not a 3-letter code)

// custom locale
getCurrencySymbol('EUR', 'de-DE') // → '€'

handleApiError

Parses backend error responses (including ky's HTTPError) and reports human-readable messages through whatever notification system your project uses (vue-sonner, react-hot-toast, console.error, etc).

The utility is UI-agnostic: call initHandleApiError once at app startup with a notify function, then call handleApiError from anywhere without touching the notification library again. Without initialization, messages fall back to console.error.

// from root
import { handleApiError, initHandleApiError } from '@ptx-showcase/utils'

// from subpath
import { handleApiError, initHandleApiError } from '@ptx-showcase/utils/handle-api-error'

// Vue project (e.g. main.ts), using vue-sonner
import { toast } from 'vue-sonner'

initHandleApiError({ notify: (message) => toast.error(message) })

// React project (e.g. app entry), using react-hot-toast
import toast from 'react-hot-toast'

initHandleApiError({ notify: (message) => toast.error(message) })

// anywhere in either project
try {
	await api.get('users')
} catch (error) {
	handleApiError(error)
}

When the response has a top-level detail, message, error or msg, only that message is shown, followed by anything in errors and non_field_errors; other keys are treated as context data. Without one, every field is reported as field: error (DRF style).

If the backend returns messages as a per-language object ({ "message": { "en": "…", "fr": "…", "it": "…" } }), pass the language as the second argument. It falls back to en when the requested language is missing, and to Something went wrong when there is no usable text at all.

handleApiError(error, 'fr') // → "Email ou mot de passe incorrect"
handleApiError(error) // → "Incorrect email or password"

isAdminToken

Checks whether a JWT access token's payload has the is_admin claim set to true. Decodes the token locally without verifying its signature, so it only reflects what the token claims — signature verification is expected to have already happened server-side.

// from root
import { isAdminToken } from '@ptx-showcase/utils'

// from subpath
import { isAdminToken } from '@ptx-showcase/utils/admin'

isAdminToken(adminToken) // → true
isAdminToken(regularUserToken) // → false
isAdminToken('not-a-valid-jwt') // → false

normalizeDecimalInput / normalizePhoneNumberInput

In-place input/change event handlers that sanitize a text input's value as the user types.

  • normalizeDecimalInput — keeps only digits and a single dot, converts a comma to a dot, prefixes a leading dot with 0, and truncates the decimal part to decimals digits (default 2).
  • normalizePhoneNumberInput — keeps only digits, truncates to maxDigits (default 15, the E.164 limit), and prefixes the result with +.
// from root
import { normalizeDecimalInput, normalizePhoneNumberInput } from '@ptx-showcase/utils'

// from subpath
import { normalizeDecimalInput, normalizePhoneNumberInput } from '@ptx-showcase/utils/normalize-input'

// Vue
<input type="text" inputmode="decimal" @input="normalizeDecimalInput" />
<input type="text" inputmode="tel" @input="normalizePhoneNumberInput" />

// React (wrap to access the native event)
<input type="text" inputMode="decimal" onChange={(e) => normalizeDecimalInput(e.nativeEvent, 4)} />
<input type="text" inputMode="tel" onChange={(e) => normalizePhoneNumberInput(e.nativeEvent, 11)} />

TableColumnConfig

Column config type for data tables. key accepts any property of the row type — including properties of nested objects, referenced by their own name (no dot path). Arrays are not traversed. Use ExtraKeys for columns that don't map to a row property (e.g. actions).

// from root
import type { TableColumnConfig } from '@ptx-showcase/utils'

// from subpath
import type { TableColumnConfig } from '@ptx-showcase/utils/base'

interface Order {
	id: number
	customer: { email: string }
	items: { sku: string }[]
}

const columns: TableColumnConfig<Order, 'actions'>[] = [
	{ label: 'ID', key: 'id', sortable: true },
	{ label: 'Customer', key: 'customer' },
	{ label: 'Email', key: 'email' }, // nested `customer.email`, written without the dot
	{ label: 'Items', key: 'items' }, // array — its item properties are not exposed
	{ label: '', key: 'actions' },
]

truncateDecimals

Formats a number (or numeric string) to a fixed number of decimal places by cutting the extra digits off, without rounding. Accepts a string too, so values more precise than a JS number can hold (e.g. amounts from an API) can be truncated without a lossy float conversion first. Returns 'N/A' for anything that isn't a finite number or a well-formed numeric string.

// from root
import { truncateDecimals } from '@ptx-showcase/utils'

// from subpath
import { truncateDecimals } from '@ptx-showcase/utils/truncate-decimals'

truncateDecimals(4.21312312) // → '4.21'
truncateDecimals(4.995) // → '4.99' (not rounded to 5.00)
truncateDecimals(4.2) // → '4.2' (fewer decimals than requested — left untouched)
truncateDecimals(4.219, 0) // → '4'

// numeric string input — no float precision loss
truncateDecimals('4.219999999999999999999999', 2) // → '4.21'

// invalid input
truncateDecimals('not a number') // → 'N/A'
truncateDecimals(undefined) // → 'N/A'

Available modules

| Subpath | Exports | | ---------------------------------------- | --------------------------------------------------------------------------------------------------------------- | | @ptx-showcase/utils | everything below | | @ptx-showcase/utils/base | TableColumnConfig, PaginatedList, ListQueryParams, DISCOUNT_TYPES, ORDER_STATUSES, SUPPORT_STATUSES | | @ptx-showcase/utils/admin | isAdminToken, ADMIN_QUERY_KEYS (admin-panel projects only) | | @ptx-showcase/utils/core | every generic utility below, bundled together | | @ptx-showcase/utils/build-query-params | buildQueryParams | | @ptx-showcase/utils/build-sort-param | buildSortParam | | @ptx-showcase/utils/cookies | setCookie, getCookie, deleteCookie, defaultCookieOptions | | @ptx-showcase/utils/currency-symbol | getCurrencySymbol | | @ptx-showcase/utils/download-file | downloadBlob | | @ptx-showcase/utils/flags | getFlagUrl, getFlagFallbackUrl, getFlagUnknownUrl, handleFlagImageError, FLAG_CODE_ALIASES | | @ptx-showcase/utils/handle-api-error | handleApiError, initHandleApiError, Languages (type) | | @ptx-showcase/utils/normalize-input | normalizeDecimalInput, normalizePhoneNumberInput | | @ptx-showcase/utils/truncate-decimals | truncateDecimals |


Versioning

This package follows Semantic Versioning:

  • patch — bug fixes
  • minor — new features (backward compatible)
  • major — breaking changes

Changelog

See CHANGELOG.md for all changes.


License

MIT