Files
nextcraft/apps/web/lib/format.ts
T
CIAgent 432049041c docs(P03): complete marketplace-surface phase
---ci---
phase: 3
milestone: v0.1
status: complete
requirements:
  covered: [REQ-013, REQ-014, REQ-015, REQ-016, REQ-017]
  partial: []
---/ci---

Phase 3 (marketplace-surface) complete:
- Job board with 20 mock listings, match scores, skill tags
- Client-side search and filter (skills, seniority, remote, salary)
- Job detail with competencies, AI-matched skills breakdown, employer info, related jobs
- Employer profile with culture section, open positions
- Pricing page with 6 tiers and feature comparison table
- All pages responsive, dark mode supported, build passes
2026-09-10 22:16:42 +00:00

84 lines
2.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* Marketplace formatting helpers — pure functions shared across marketplace pages.
* Mock-data only; no backend calls.
*/
import type { Seniority } from '@nextcraft/types';
/** Format a salary value as `$160K` (divides by 1000). */
export function formatSalaryK(value: number): string {
return `$${Math.round(value / 1000)}K`;
}
/** Format a salary range as `$160K$240K`. */
export function formatSalaryRange(min: number, max: number): string {
return `${formatSalaryK(min)}${formatSalaryK(max)}`;
}
const SENIORITY_LABELS: Record<Seniority, string> = {
entry: 'Entry',
mid: 'Mid',
senior: 'Senior',
staff: 'Lead',
principal: 'Principal',
};
export function seniorityLabel(s: Seniority): string {
return SENIORITY_LABELS[s] ?? s;
}
/**
* Relative time label like "2 days ago" / "3 weeks ago".
* Computed against a fixed "today" so the prototype renders deterministically
* regardless of when it is built.
*/
export function relativeTime(iso: string, now: Date = new Date('2026-09-10T00:00:00Z')): string {
const then = new Date(iso).getTime();
const diffMs = now.getTime() - then;
const days = Math.max(0, Math.round(diffMs / (1000 * 60 * 60 * 24)));
if (days <= 0) return 'today';
if (days === 1) return '1 day ago';
if (days < 30) return `${days} days ago`;
const weeks = Math.round(days / 7);
if (weeks === 1) return '1 week ago';
if (weeks < 5) return `${weeks} weeks ago`;
const months = Math.round(days / 30);
return months === 1 ? '1 month ago' : `${months} months ago`;
}
/** Color-coded bucket for a match score (0100). */
export type MatchTone = 'high' | 'mid' | 'low';
export function matchTone(score: number): MatchTone {
if (score > 85) return 'high';
if (score >= 70) return 'mid';
return 'low';
}
/** Tailwind class bundle for the circular match badge background/text. */
export function matchBadgeClasses(score: number): string {
const tone = matchTone(score);
if (tone === 'high')
return 'bg-emerald-100 text-emerald-700 ring-emerald-300 dark:bg-emerald-900/40 dark:text-emerald-300 dark:ring-emerald-700';
if (tone === 'mid')
return 'bg-amber-100 text-amber-700 ring-amber-300 dark:bg-amber-900/40 dark:text-amber-300 dark:ring-amber-700';
return 'bg-orange-100 text-orange-700 ring-orange-300 dark:bg-orange-900/40 dark:text-orange-300 dark:ring-orange-700';
}
/** Tailwind class bundle for a horizontal skill match bar fill. */
export function matchBarClasses(score: number): string {
const tone = matchTone(score);
if (tone === 'high') return 'bg-emerald-500';
if (tone === 'mid') return 'bg-amber-500';
return 'bg-orange-500';
}
/** Initials from a name, e.g. "OpenAI Labs" → "OL". */
export function initials(name: string): string {
return name
.split(' ')
.filter(Boolean)
.slice(0, 2)
.map((p) => p[0]?.toUpperCase() ?? '')
.join('');
}