@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/utilsor
bun add @ptx-showcase/utilsUsage
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') // → falsenormalizeDecimalInput / 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 with0, and truncates the decimal part todecimalsdigits (default2).normalizePhoneNumberInput— keeps only digits, truncates tomaxDigits(default15, 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
