Skip to content

Commit fd64419

Browse files
Copilotzhyd1997coderabbitai[bot]
authored
Replace native Date API with dayjs for relative date formatting (#71)
* Initial plan * Refactor date formatting to use dayjs library Co-authored-by: zhyd1997 <31362988+zhyd1997@users.noreply.github.com> * Update lib/utils/date.ts Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> * Fix date utility issues based on code review feedback - Remove unused relativeTime and calendar plugins - Add isSameOrAfter and isSameOrBefore plugins - Fix formatTimeWithZone to not show 'z' without timezone - Fix formatRelativeDate to show day names for future dates within 7 days - Fix OverviewStats week boundary logic using isSameOrAfter/isSameOrBefore - Update JSDoc examples to be more accurate Co-authored-by: zhyd1997 <31362988+zhyd1997@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: zhyd1997 <31362988+zhyd1997@users.noreply.github.com> Co-authored-by: Yadong (Adam) Zhang <zhyd007@gmail.com> Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
1 parent adf63b2 commit fd64419

File tree

8 files changed

+176
-62
lines changed

8 files changed

+176
-62
lines changed

lib/utils/date.ts

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
import dayjs from 'dayjs';
2+
import utc from 'dayjs/plugin/utc';
3+
import timezone from 'dayjs/plugin/timezone';
4+
import isSameOrAfter from 'dayjs/plugin/isSameOrAfter';
5+
import isSameOrBefore from 'dayjs/plugin/isSameOrBefore';
6+
7+
// Configure dayjs plugins
8+
dayjs.extend(utc);
9+
dayjs.extend(timezone);
10+
dayjs.extend(isSameOrAfter);
11+
dayjs.extend(isSameOrBefore);
12+
13+
/**
14+
* Format a date string to show relative time (e.g., "today", "yesterday")
15+
* or formatted date for older dates
16+
*
17+
* @param dateString - ISO date string
18+
* @returns Formatted date string
19+
*
20+
* @example
21+
* // Assuming today is 2025-11-19
22+
* formatRelativeDate('2025-11-19T10:00:00Z') // "Today"
23+
* formatRelativeDate('2025-11-18T10:00:00Z') // "Yesterday"
24+
* formatRelativeDate('2025-11-15T10:00:00Z') // "Friday" (if within last 7 days)
25+
*/
26+
export function formatRelativeDate(dateString: string): string {
27+
const date = dayjs(dateString);
28+
const now = dayjs();
29+
30+
// Check if today
31+
if (date.isSame(now, 'day')) {
32+
return 'Today';
33+
}
34+
35+
// Check if yesterday
36+
if (date.isSame(now.subtract(1, 'day'), 'day')) {
37+
return 'Yesterday';
38+
}
39+
40+
// Check if tomorrow
41+
if (date.isSame(now.add(1, 'day'), 'day')) {
42+
return 'Tomorrow';
43+
}
44+
45+
// Check if within 7 days (past or future, excluding today, yesterday, tomorrow)
46+
if (Math.abs(date.diff(now, 'day')) < 7 && Math.abs(date.diff(now, 'day')) > 1) {
47+
return date.format('dddd'); // Day name (e.g., "Monday")
48+
}
49+
50+
// For older dates, show formatted date
51+
return date.format('MMM D, YYYY');
52+
}
53+
54+
/**
55+
* Format a date string to display full date information
56+
*
57+
* @param dateString - ISO date string
58+
* @returns Formatted date string like "Monday, November 19, 2025"
59+
*/
60+
export function formatFullDate(dateString: string): string {
61+
return dayjs(dateString).format('dddd, MMMM D, YYYY');
62+
}
63+
64+
/**
65+
* Format a date string to display short date
66+
*
67+
* @param dateString - ISO date string
68+
* @returns Formatted date string like "Nov 19, 2025"
69+
*/
70+
export function formatShortDate(dateString: string): string {
71+
return dayjs(dateString).format('MMM D, YYYY');
72+
}
73+
74+
/**
75+
* Format a time string
76+
*
77+
* @param dateString - ISO date string
78+
* @returns Formatted time string like "10:30 AM"
79+
*/
80+
export function formatTime(dateString: string): string {
81+
return dayjs(dateString).format('h:mm A');
82+
}
83+
84+
/**
85+
* Format a time string with timezone
86+
*
87+
* @param dateString - ISO date string
88+
* @param timezone - Optional timezone identifier
89+
* @returns Formatted time string with timezone like "10:30 AM PST"
90+
*/
91+
export function formatTimeWithZone(dateString: string, timezone?: string): string {
92+
if (timezone) {
93+
return dayjs(dateString).tz(timezone).format('h:mm A z');
94+
}
95+
return dayjs(dateString).format('h:mm A');
96+
}
97+
98+
/**
99+
* Format a month and year for charts/displays
100+
*
101+
* @param dateString - ISO date string
102+
* @returns Formatted string like "Nov 2025"
103+
*/
104+
export function formatMonthYear(dateString: string): string {
105+
return dayjs(dateString).format('MMM YYYY');
106+
}
107+
108+
/**
109+
* Check if a date is in the future
110+
*
111+
* @param dateString - ISO date string
112+
* @returns True if date is in the future
113+
*/
114+
export function isFutureDate(dateString: string): boolean {
115+
return dayjs(dateString).isAfter(dayjs());
116+
}
117+
118+
/**
119+
* Check if a date is in the past
120+
*
121+
* @param dateString - ISO date string
122+
* @returns True if date is in the past
123+
*/
124+
export function isPastDate(dateString: string): boolean {
125+
return dayjs(dateString).isBefore(dayjs());
126+
}
127+
128+
/**
129+
* Check if two dates are on the same day
130+
*
131+
* @param date1 - First ISO date string
132+
* @param date2 - Second ISO date string
133+
* @returns True if dates are on the same day
134+
*/
135+
export function isSameDay(date1: string, date2: string): boolean {
136+
return dayjs(date1).isSame(dayjs(date2), 'day');
137+
}

modules/dashboard/MeetingDetails.tsx

Lines changed: 3 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -2,31 +2,13 @@ import { Calendar, Clock, Users, Video, CheckCircle, XCircle, Info } from 'lucid
22
import { Badge } from '@/components/ui/badge';
33
import { Separator } from '@/components/ui/separator';
44
import type { MeetingRecord } from '@/lib/types/meeting';
5+
import { formatFullDate, formatTimeWithZone } from '@/lib/utils/date';
56

67
interface MeetingDetailsProps {
78
readonly meeting: MeetingRecord;
89
}
910

1011
export function MeetingDetails({ meeting }: MeetingDetailsProps) {
11-
const formatDate = (dateString: string) => {
12-
const date = new Date(dateString);
13-
return date.toLocaleDateString('en-US', {
14-
weekday: 'long',
15-
year: 'numeric',
16-
month: 'long',
17-
day: 'numeric',
18-
});
19-
};
20-
21-
const formatTime = (dateString: string) => {
22-
const date = new Date(dateString);
23-
return date.toLocaleTimeString('en-US', {
24-
hour: '2-digit',
25-
minute: '2-digit',
26-
timeZoneName: 'short',
27-
});
28-
};
29-
3012
const getPlatformName = (url: string) => {
3113
if (url.includes('meet.google.com')) return 'Google Meet';
3214
if (url.includes('zoom.us')) return 'Zoom';
@@ -115,7 +97,7 @@ export function MeetingDetails({ meeting }: MeetingDetailsProps) {
11597
<Calendar className="w-5 h-5 text-muted-foreground mt-0.5" />
11698
<div>
11799
<div className="text-sm">Date</div>
118-
<div className="text-muted-foreground text-sm">{formatDate(meeting.start)}</div>
100+
<div className="text-muted-foreground text-sm">{formatFullDate(meeting.start)}</div>
119101
</div>
120102
</div>
121103

@@ -124,7 +106,7 @@ export function MeetingDetails({ meeting }: MeetingDetailsProps) {
124106
<div>
125107
<div className="text-sm">Time</div>
126108
<div className="text-muted-foreground text-sm">
127-
{formatTime(meeting.start)} - {formatTime(meeting.end)}
109+
{formatTimeWithZone(meeting.start)} - {formatTimeWithZone(meeting.end)}
128110
</div>
129111
<div className="text-muted-foreground text-xs mt-1">
130112
Duration: {meeting.duration} minutes

modules/dashboard/MeetingTimeline.tsx

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContai
55
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
66
import { TrendingUp } from 'lucide-react';
77
import type { MeetingRecord } from '@/lib/types/meeting';
8+
import dayjs from 'dayjs';
9+
import { formatMonthYear } from '@/lib/utils/date';
810

911
interface MeetingTimelineProps {
1012
readonly data: readonly MeetingRecord[];
@@ -46,9 +48,9 @@ const CustomTooltip = (props: CustomTooltipPropsType) => {
4648
export function MeetingTimeline({ data }: MeetingTimelineProps) {
4749
// Group meetings by month
4850
const monthlyData = data.reduce((acc, meeting) => {
49-
const date = new Date(meeting.start);
50-
const monthKey = `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}`;
51-
const monthName = date.toLocaleDateString('en-US', { month: 'short', year: 'numeric' });
51+
const date = dayjs(meeting.start);
52+
const monthKey = date.format('YYYY-MM');
53+
const monthName = formatMonthYear(meeting.start);
5254

5355
if (!acc[monthKey]) {
5456
acc[monthKey] = {

modules/dashboard/MeetingTimes.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/com
66
import { AnimatedNumber } from './AnimatedNumber';
77
import type { MeetingRecord } from '@/lib/types/meeting';
88
import { fadeInFromBottom, createTransition } from '@/lib/constants/animations';
9+
import dayjs from 'dayjs';
910

1011
interface MeetingTimesProps {
1112
readonly data: readonly MeetingRecord[];
@@ -25,8 +26,7 @@ interface TimeCategoryData {
2526
}
2627

2728
const getTimeCategory = (startTime: string): TimeCategory => {
28-
const date = new Date(startTime);
29-
const hour = date.getHours();
29+
const hour = dayjs(startTime).hour();
3030

3131
if (hour >= 6 && hour < 12) return 'morning';
3232
if (hour >= 12 && hour < 17) return 'afternoon';

modules/dashboard/OverviewStats.tsx

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,13 @@ import { Calendar, Clock, CheckCircle, XCircle, Users, TrendingUp } from 'lucide
55
import { Card, CardContent } from '@/components/ui/card';
66
import { AnimatedNumber } from './AnimatedNumber';
77
import type { MeetingRecord } from '@/lib/types/meeting';
8+
import dayjs from 'dayjs';
9+
import isSameOrAfter from 'dayjs/plugin/isSameOrAfter';
10+
import isSameOrBefore from 'dayjs/plugin/isSameOrBefore';
11+
12+
// Extend dayjs with comparison plugins
13+
dayjs.extend(isSameOrAfter);
14+
dayjs.extend(isSameOrBefore);
815

916
interface OverviewStatsProps {
1017
readonly data: readonly MeetingRecord[];
@@ -32,18 +39,14 @@ export function OverviewStats({ data }: OverviewStatsProps) {
3239
const totalParticipants = participantSet.size;
3340

3441
// Calculate this week's meetings (Monday to Sunday of current week)
35-
const now = new Date();
36-
const dayOfWeek = now.getDay();
37-
const diff = now.getDate() - dayOfWeek + (dayOfWeek === 0 ? -6 : 1); // Adjust when day is Sunday
38-
const monday = new Date(now.getFullYear(), now.getMonth(), diff);
39-
monday.setHours(0, 0, 0, 0);
40-
const sunday = new Date(monday);
41-
sunday.setDate(monday.getDate() + 6);
42-
sunday.setHours(23, 59, 59, 999);
42+
const now = dayjs();
43+
const dayOfWeek = now.day();
44+
const monday = now.subtract(dayOfWeek === 0 ? 6 : dayOfWeek - 1, 'day').startOf('day');
45+
const sunday = monday.add(6, 'day').endOf('day');
4346

4447
const thisWeekMeetings = data.filter(meeting => {
45-
const meetingDate = new Date(meeting.start);
46-
return meetingDate >= monday && meetingDate <= sunday;
48+
const meetingDate = dayjs(meeting.start);
49+
return meetingDate.isSameOrAfter(monday, 'day') && meetingDate.isSameOrBefore(sunday, 'day');
4750
}).length;
4851

4952
const stats = [

modules/dashboard/RecentMeetings.tsx

Lines changed: 7 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/u
99
import { Drawer, DrawerContent, DrawerHeader, DrawerTitle } from '@/components/ui/drawer';
1010
import { MeetingDetails } from './MeetingDetails';
1111
import type { MeetingRecord } from '@/lib/types/meeting';
12+
import { formatRelativeDate, formatTime, isFutureDate } from '@/lib/utils/date';
13+
import dayjs from 'dayjs';
1214

1315
interface RecentMeetingsProps {
1416
readonly data: readonly MeetingRecord[];
@@ -29,11 +31,9 @@ export function RecentMeetings({ data }: RecentMeetingsProps) {
2931
}, []);
3032

3133
// Filter out completed meetings with future dates (invalid data)
32-
const now = new Date();
3334
const validMeetings = [...data].filter(meeting => {
34-
const meetingStart = new Date(meeting.start);
3535
// Completed meetings must have past dates
36-
if (meeting.status === 'completed' && meetingStart > now) {
36+
if (meeting.status === 'completed' && isFutureDate(meeting.start)) {
3737
return false;
3838
}
3939
return true;
@@ -42,7 +42,7 @@ export function RecentMeetings({ data }: RecentMeetingsProps) {
4242
// Sort meetings by start date (most recent first) and take top 5
4343
// Ensure at least one cancelled meeting is included if available
4444
const sortedMeetings = validMeetings
45-
.sort((a, b) => new Date(b.start).getTime() - new Date(a.start).getTime());
45+
.sort((a, b) => dayjs(b.start).valueOf() - dayjs(a.start).valueOf());
4646

4747
const top5 = sortedMeetings.slice(0, 5);
4848
const hasCancelledInTop5 = top5.some(meeting => meeting.status === 'cancelled');
@@ -55,31 +55,12 @@ export function RecentMeetings({ data }: RecentMeetingsProps) {
5555
if (mostRecentCancelled) {
5656
// Replace the oldest meeting (last in the array) with the cancelled one
5757
recentMeetings = [...top5.slice(0, 4), mostRecentCancelled]
58-
.sort((a, b) => new Date(b.start).getTime() - new Date(a.start).getTime());
58+
.sort((a, b) => dayjs(b.start).valueOf() - dayjs(a.start).valueOf());
5959
}
6060
}
6161

62-
const formatDate = (dateString: string) => {
63-
const date = new Date(dateString);
64-
return date.toLocaleDateString('en-US', {
65-
month: 'short',
66-
day: 'numeric',
67-
year: 'numeric',
68-
});
69-
};
70-
71-
const formatTime = (dateString: string) => {
72-
const date = new Date(dateString);
73-
return date.toLocaleTimeString('en-US', {
74-
hour: '2-digit',
75-
minute: '2-digit',
76-
});
77-
};
78-
7962
const isUpcomingAccepted = (meeting: MeetingRecord) => {
80-
const now = new Date();
81-
const meetingStart = new Date(meeting.start);
82-
return meeting.status === 'accepted' && meetingStart > now;
63+
return meeting.status === 'accepted' && isFutureDate(meeting.start);
8364
};
8465

8566
const getBadgeProps = (status: string) => {
@@ -187,7 +168,7 @@ export function RecentMeetings({ data }: RecentMeetingsProps) {
187168
<div className="flex flex-col sm:flex-row sm:items-center gap-2 sm:gap-4 text-xs text-muted-foreground">
188169
<div className="flex items-center gap-1">
189170
<Calendar className="w-3 h-3" />
190-
{formatDate(meeting.start)}
171+
{formatRelativeDate(meeting.start)}
191172
</div>
192173
<div className="flex items-center gap-1">
193174
<Clock className="w-3 h-3" />

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
"better-auth": "^1.3.34",
2424
"class-variance-authority": "^0.7.1",
2525
"clsx": "^2.1.1",
26+
"dayjs": "^1.11.19",
2627
"lucide-react": "^0.554.0",
2728
"motion": "^12.23.24",
2829
"next": "16.0.3",

pnpm-lock.yaml

Lines changed: 8 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)