mirror of
https://github.com/vernu/textbee.git
synced 2026-07-31 09:28:14 -04:00
Introduce a typed data layer under lib/api (query-key factory, response
types, feature hooks) plus shared lib/format and lib/status helpers, and
migrate the canonical dashboard consumers (subscription-info, overview,
device-list, api-keys) onto it, deleting their duplicated inline queries,
mutations and formatters.
List hooks keep the raw { data: [] } envelope in the cache and unwrap
per-observer with react-query `select`, so shared keys like ['devices']
stay compatible with the not-yet-migrated components that still read the
raw shape (avoids a cache-shape collision surfaced by the dashboard e2e).
Adds unit tests for the formatters and the hooks (against MSW). Build,
19 unit tests, and 2 e2e all green.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
48 lines
1.4 KiB
TypeScript
48 lines
1.4 KiB
TypeScript
// Shared formatting helpers. These were previously duplicated inline across
|
|
// subscription-info, device-list, overview and others.
|
|
|
|
// A plan limit of -1 means unlimited; null/undefined render as 0.
|
|
export function formatLimit(value: number | null | undefined): string {
|
|
if (value === -1) return 'Unlimited'
|
|
if (value == null) return '0'
|
|
return value.toLocaleString()
|
|
}
|
|
|
|
// Amounts come from the backend in minor units (cents). Null amount = Free.
|
|
export function formatPrice(
|
|
amount: number | null | undefined,
|
|
currency: string | null | undefined
|
|
): string {
|
|
if (amount == null || currency == null) return 'Free'
|
|
return new Intl.NumberFormat('en-US', {
|
|
style: 'currency',
|
|
currency: currency.toUpperCase() || 'USD',
|
|
minimumFractionDigits: 2,
|
|
}).format(amount / 100)
|
|
}
|
|
|
|
export function getBillingInterval(
|
|
interval: string | null | undefined
|
|
): string {
|
|
if (!interval) return ''
|
|
return interval.toLowerCase() === 'month' ? 'monthly' : 'yearly'
|
|
}
|
|
|
|
export function formatDate(value: string | number | Date | null | undefined) {
|
|
if (!value) return 'N/A'
|
|
return new Date(value).toLocaleDateString('en-US', {
|
|
month: 'short',
|
|
day: 'numeric',
|
|
year: 'numeric',
|
|
})
|
|
}
|
|
|
|
// "past_due" -> "Past Due"
|
|
export function titleCaseStatus(status: string | null | undefined): string {
|
|
if (!status) return ''
|
|
return status
|
|
.split('_')
|
|
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
|
|
.join(' ')
|
|
}
|