Fork of atp.tools as a universal profile for people on the ATmosphere
1import { clsx, type ClassValue } from "clsx";
2import { twMerge } from "tailwind-merge";
3
4export function cn(...inputs: ClassValue[]) {
5 return twMerge(clsx(inputs));
6}
7type TimeUnit =
8 | "year"
9 | "month"
10 | "week"
11 | "day"
12 | "hour"
13 | "minute"
14 | "second";
15
16interface TimeInterval {
17 seconds: number;
18 label: TimeUnit;
19}
20
21interface TimeagoOptions {
22 maxUnit?: TimeUnit;
23 minUnit?: TimeUnit;
24 future?: boolean;
25 useShortLabels?: boolean;
26}
27
28export function timeAgo(
29 date: Date | string | number,
30 options: TimeagoOptions = {},
31): string {
32 const {
33 maxUnit = "year",
34 minUnit = "second",
35 future = true,
36 useShortLabels = false,
37 } = options;
38
39 const currentDate = new Date();
40 const targetDate = new Date(date);
41
42 const seconds = Math.floor(
43 (currentDate.getTime() - targetDate.getTime()) / 1000,
44 );
45 const isFuture = seconds < 0;
46 const absoluteSeconds = Math.abs(seconds);
47
48 const intervals: TimeInterval[] = [
49 { seconds: 31536000, label: "year" },
50 { seconds: 2592000, label: "month" },
51 { seconds: 604800, label: "week" },
52 { seconds: 86400, label: "day" },
53 { seconds: 3600, label: "hour" },
54 { seconds: 60, label: "minute" },
55 { seconds: 1, label: "second" },
56 ];
57
58 // Short labels mapping
59 const shortLabels: Record<TimeUnit, string> = {
60 year: "y",
61 month: "mo",
62 week: "w",
63 day: "d",
64 hour: "h",
65 minute: "m",
66 second: "s",
67 };
68
69 // Handle future dates if not allowed
70 if (isFuture && !future) {
71 return "in the future";
72 }
73
74 // Handle just now
75 if (absoluteSeconds < 30 && minUnit === "second") {
76 return "just now";
77 }
78
79 // Filter intervals based on max and min units
80 const filteredIntervals = intervals.filter((interval) => {
81 const unitIndex = intervals.findIndex((i) => i.label === interval.label);
82 const maxUnitIndex = intervals.findIndex((i) => i.label === maxUnit);
83 const minUnitIndex = intervals.findIndex((i) => i.label === minUnit);
84 return unitIndex >= maxUnitIndex && unitIndex <= minUnitIndex;
85 });
86
87 for (const { seconds: secondsInUnit, label } of filteredIntervals) {
88 const interval = Math.floor(absoluteSeconds / secondsInUnit);
89
90 if (interval >= 1) {
91 const unitLabel = useShortLabels ? shortLabels[label] : label;
92 const plural = interval === 1 ? "" : "s";
93 const timeLabel = `${interval}${useShortLabels ? "" : " "}${unitLabel}${useShortLabels ? "" : plural}`;
94
95 return isFuture ? `in ${timeLabel}` : `${timeLabel} ago`;
96 }
97 }
98
99 return "just now";
100}