learning_ai_common_plat/packages/time-references/src/client.ts
saravanakumardb1 be03efa111 feat(shared-packages): add 9 @bytelyst/* client packages with 100% API coverage
Packages added:
- @bytelyst/referral-client — referral API client + share helpers
- @bytelyst/subscription-client — subscription/plan API client + cache
- @bytelyst/celebrations — milestone triggers, confetti, positive messages
- @bytelyst/gentle-notifications — ND-friendly messaging, forbidden phrases
- @bytelyst/accessibility — VoiceOver/TalkBack label generators
- @bytelyst/quick-actions — progressive disclosure, smart defaults
- @bytelyst/time-references — familiar duration references
- @bytelyst/org-client — org/workspace/membership/license API client
- @bytelyst/marketplace-client — listing/review/install API client

All packages: pure TS, ESM, globalThis.fetch, no Node.js deps.
99 Vitest tests across 9 packages, 79/79 public methods covered.

Review fixes applied:
- time-references: fix module-level mutable state leak + add clearCustomReferences()
- accessibility: fix parameter reassignment in formatDurationForA11y/numberToWords
- subscription-client: fix flaky daysRemaining test (ms boundary race)
2026-03-19 13:10:09 -07:00

160 lines
4.7 KiB
TypeScript

/**
* Familiar duration references for time-blindness aids.
*
* "About as long as a movie", "3 episodes of The Office".
* Pure client-side TS — no backend dependency.
*/
import type { TimeRangeEntry, TimeReference } from './types.js';
const DEFAULT_DATABASE: TimeRangeEntry[] = [
{
minHours: 0,
maxHours: 0.25,
references: [
{ text: 'About as long as a coffee break', emoji: '☕', category: 'activity' },
{ text: 'Like listening to a few songs', emoji: '🎵', category: 'media' },
],
},
{
minHours: 0.25,
maxHours: 0.5,
references: [
{ text: 'About as long as a TV episode', emoji: '📺', category: 'media' },
{ text: 'Like a short walk around the block', emoji: '🚶', category: 'activity' },
],
},
{
minHours: 0.5,
maxHours: 1,
references: [
{ text: 'About as long as a yoga class', emoji: '🧘', category: 'activity' },
{ text: 'Like watching 2 episodes of The Office', emoji: '📺', category: 'media' },
],
},
{
minHours: 1,
maxHours: 2,
references: [
{ text: 'About as long as a movie', emoji: '🎬', category: 'media' },
{ text: 'Like a nice bike ride', emoji: '🚴', category: 'activity' },
],
},
{
minHours: 2,
maxHours: 4,
references: [
{ text: 'About as long as a road trip to the next city', emoji: '🚗', category: 'travel' },
{ text: 'Like binge-watching a short series', emoji: '📺', category: 'media' },
],
},
{
minHours: 4,
maxHours: 8,
references: [
{ text: 'About as long as a work day', emoji: '💼', category: 'activity' },
{ text: 'Like a full night of sleep', emoji: '😴', category: 'nature' },
],
},
{
minHours: 8,
maxHours: 12,
references: [
{ text: 'About as long as a cross-country flight', emoji: '✈️', category: 'travel' },
{ text: 'Like sunrise to sunset in winter', emoji: '🌅', category: 'nature' },
],
},
{
minHours: 12,
maxHours: 16,
references: [
{ text: 'About as long as daylight hours in summer', emoji: '☀️', category: 'nature' },
{
text: 'Like watching the entire Lord of the Rings trilogy (extended)',
emoji: '🧙',
category: 'media',
},
],
},
{
minHours: 16,
maxHours: 24,
references: [
{ text: 'Almost a full day — impressive!', emoji: '🌍', category: 'nature' },
{ text: 'Like a full day of hiking', emoji: '🥾', category: 'activity' },
],
},
{
minHours: 24,
maxHours: 48,
references: [
{ text: 'More than a full day!', emoji: '🏆', category: 'nature' },
{ text: 'Like a weekend camping trip', emoji: '⛺', category: 'activity' },
],
},
{
minHours: 48,
maxHours: Infinity,
references: [
{ text: 'An extraordinary duration — you are incredible!', emoji: '🌟', category: 'nature' },
],
},
];
const FALLBACK: TimeReference = {
text: 'A meaningful amount of time',
emoji: '⏳',
category: 'nature',
};
const customRegistry: TimeRangeEntry[] = [];
function findReferences(hours: number, custom: TimeRangeEntry[]): TimeReference[] {
for (const entry of custom) {
if (hours >= entry.minHours && hours < entry.maxHours) {
return entry.references;
}
}
for (const entry of DEFAULT_DATABASE) {
if (hours >= entry.minHours && hours < entry.maxHours) {
return entry.references;
}
}
return [FALLBACK];
}
export function getTimeReference(elapsedHours: number): TimeReference {
const refs = findReferences(Math.max(0, elapsedHours), customRegistry);
const index = Math.floor(Math.random() * refs.length);
return refs[index];
}
export function getEpisodeComparison(
elapsedHours: number,
showName = 'The Office',
episodeMins = 22
): string {
const totalMins = elapsedHours * 60;
const episodes = Math.round(totalMins / episodeMins);
if (episodes <= 0) return `Less than one episode of ${showName}`;
if (episodes === 1) return `About 1 episode of ${showName}`;
return `About ${episodes} episodes of ${showName}`;
}
export function getEncouragingMessage(elapsedHours: number): string {
if (elapsedHours < 1) return 'Every minute counts — great start!';
if (elapsedHours < 4) return 'You are building momentum — keep going!';
if (elapsedHours < 8) return 'Impressive dedication — halfway through the day!';
if (elapsedHours < 16) return 'Amazing endurance — you are a champion!';
if (elapsedHours < 24) return 'Nearly a full day — extraordinary commitment!';
return 'Beyond a full day — you are truly remarkable!';
}
export function registerReferences(entries: TimeRangeEntry[]): void {
customRegistry.push(...entries);
}
export function clearCustomReferences(): void {
customRegistry.length = 0;
}