Files
nextcraft/apps/web/lib/format.ts
T
CIAgent 810485ecc8 docs(milestone): complete v0.1-nextcraft-ui-prototype
---ci---
phase: 7
milestone: v0.1
status: complete
requirements:
  covered: [REQ-001, REQ-002, REQ-003, REQ-004, REQ-005, REQ-006, REQ-007, REQ-008, REQ-009, REQ-010, REQ-011, REQ-012, REQ-013, REQ-014, REQ-015, REQ-016, REQ-017, REQ-018, REQ-019, REQ-020, REQ-021, REQ-022, REQ-023, REQ-024, REQ-025, REQ-026, REQ-027, REQ-028]
  partial: []
---/ci---

Milestone v0.1 (nextcraft-ui-prototype) complete.

Summary:
- 7 phases (P0 pre-execution + P1-P6 execution + P7 final review)
- 28 requirements covered (all complete)
- 4 surfaces: Learner (7 pages), Marketplace (5), Employer Dashboard (4), Admin (4)
- 19 routes, 104 TypeScript files
- Shared component library (5 primitives + design tokens)
- Mock data: 5 competency stacks (70 competencies), 20 jobs, 15 candidates, 10 employers
- Tech: Next.js 15, Tailwind CSS v4, lucide-react, recharts, @xyflow/react, Inter font
- Storybook with 6 stories
- Dark mode, breadcrumbs, role switcher, responsive design
- Build passes, typecheck passes, Storybook build passes

Phases:
  P0 pre-execution     → v0.0.1
  P1 project-scaffolding → v0.0.2
  P2 learner-surface   → v0.0.3
  P3 marketplace-surface → v0.0.4
  P4 employer-dashboard → v0.0.5
  P5 admin-surface     → v0.0.6
  P6 polish-integration → v0.0.7
  P7 final-review-ship → v0.1.0 (milestone release)
2026-09-10 22:47:56 +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('');
}