File
Blob: src/shared/editor/presentation/metrics.tsx
| 1 | import type { EditorTextMetrics } from "@/shared/editor/schema/metrics"; |
| 2 | |
| 3 | const DEFAULT_WORDS_PER_MINUTE = 200; |
| 4 | const NUMBER_FORMATTER = new Intl.NumberFormat(); |
| 5 | |
| 6 | export interface EditorMetricsPresentationProps { |
| 7 | metrics: EditorTextMetrics; |
| 8 | className?: string; |
| 9 | } |
| 10 | |
| 11 | function estimateReadTimeMinutes(wordCount: number, wordsPerMinute = DEFAULT_WORDS_PER_MINUTE): number { |
| 12 | if (!Number.isFinite(wordCount) || wordCount <= 0) { |
| 13 | return 0; |
| 14 | } |
| 15 | |
| 16 | const safeWordsPerMinute = Math.max(1, wordsPerMinute); |
| 17 | return Math.max(1, Math.ceil(wordCount / safeWordsPerMinute)); |
| 18 | } |
| 19 | |
| 20 | export function formatReadTime(wordCount: number, wordsPerMinute = DEFAULT_WORDS_PER_MINUTE): string { |
| 21 | return `${estimateReadTimeMinutes(wordCount, wordsPerMinute)} min read`; |
| 22 | } |
| 23 | |
| 24 | function formatNumber(value: number): string { |
| 25 | return NUMBER_FORMATTER.format(value); |
| 26 | } |
| 27 | |
| 28 | export function formatMetricCount(value: number, singular: string, plural: string): string { |
| 29 | return `${formatNumber(value)} ${value === 1 ? singular : plural}`; |
| 30 | } |
| 31 | |
| 32 | export function EditorMetricsPresentation({ metrics, className }: EditorMetricsPresentationProps) { |
| 33 | const wordsLabel = formatMetricCount(metrics.words, "word", "words"); |
| 34 | const charsLabel = formatMetricCount(metrics.characters, "char", "chars"); |
| 35 | const readTimeLabel = formatReadTime(metrics.words); |
| 36 | |
| 37 | return ( |
| 38 | <div |
| 39 | className={["flex flex-wrap items-center gap-x-2 gap-y-1 text-[11px] leading-none text-zinc-500", className] |
| 40 | .filter(Boolean) |
| 41 | .join(" ")} |
| 42 | aria-label={`Document metrics: ${wordsLabel}, ${charsLabel}, ${readTimeLabel}`} |
| 43 | > |
| 44 | <span>{wordsLabel}</span> |
| 45 | <span aria-hidden="true" className="text-zinc-700"> |
| 46 | · |
| 47 | </span> |
| 48 | <span>{charsLabel}</span> |
| 49 | <span aria-hidden="true" className="text-zinc-700"> |
| 50 | · |
| 51 | </span> |
| 52 | <span>{readTimeLabel}</span> |
| 53 | </div> |
| 54 | ); |
| 55 | } |