Skip to content
File

Blob: src/shared/editor/presentation/metrics.tsx

typescript56 lines
1import type { EditorTextMetrics } from "@/shared/editor/schema/metrics";
2 
3const DEFAULT_WORDS_PER_MINUTE = 200;
4const NUMBER_FORMATTER = new Intl.NumberFormat();
5 
6export interface EditorMetricsPresentationProps {
7 metrics: EditorTextMetrics;
8 className?: string;
9}
10 
11function 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 
20export function formatReadTime(wordCount: number, wordsPerMinute = DEFAULT_WORDS_PER_MINUTE): string {
21 return `${estimateReadTimeMinutes(wordCount, wordsPerMinute)} min read`;
22}
23 
24function formatNumber(value: number): string {
25 return NUMBER_FORMATTER.format(value);
26}
27 
28export function formatMetricCount(value: number, singular: string, plural: string): string {
29 return `${formatNumber(value)} ${value === 1 ? singular : plural}`;
30}
31 
32export 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}