Skip to content

Commit 1266cb5

Browse files
committed
Merge remote-tracking branch 'origin'
2 parents 22f9730 + 08e8e77 commit 1266cb5

7 files changed

Lines changed: 451 additions & 31 deletions

File tree

src/components/Holidays/CalendarView.stories.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,16 +88,19 @@ const mockHolidays: Holiday[] = [
8888
},
8989
]
9090

91+
9192
export const Default: Story = {
9293
render: () => {
9394
const [currentDate, setCurrentDate] = useState(new Date(2025, 0, 1)) // January 2025
95+
const today = TimeUtil.toUtcMidnight(new Date(2025, 0, 1)) // Server's "today"
9496

9597
const correctedCurrentDate = TimeUtil.toUtcMidnight(currentDate)
9698
return (
9799
<CalendarView
98100
currentDate={correctedCurrentDate}
99101
setCurrentDate={setCurrentDate}
100102
holidays={mockHolidays}
103+
today={today}
101104
/>
102105
)
103106
},

src/components/Holidays/CalendarView.tsx

Lines changed: 26 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ interface CalendarViewProps {
1111
currentDate: Date
1212
setCurrentDate: (date: Date) => void
1313
holidays: Holiday[]
14+
today: Date // Server-determined "today" to avoid hydration mismatch
1415
}
1516

1617
interface Holiday {
@@ -24,18 +25,25 @@ interface Holiday {
2425
leaveType: 'Full Day' | 'Morning' | 'Afternoon'
2526
}
2627

27-
export function CalendarView({ currentDate, setCurrentDate, holidays }: CalendarViewProps) {
28+
export function CalendarView({ currentDate, setCurrentDate, holidays, today }: CalendarViewProps) {
2829
const [selectedDay, setSelectedDay] = useState<Date | null>(null)
2930

31+
// Helper to compare dates by their UTC year/month/day values
32+
const isSameUtcDay = (a: Date, b: Date) =>
33+
a.getUTCFullYear() === b.getUTCFullYear() &&
34+
a.getUTCMonth() === b.getUTCMonth() &&
35+
a.getUTCDate() === b.getUTCDate()
36+
3037
const firstDayOfMonth =
31-
(new Date(currentDate.getFullYear(), currentDate.getMonth(), 1).getDay() + 6) % 7
38+
(new Date(Date.UTC(currentDate.getUTCFullYear(), currentDate.getUTCMonth(), 1)).getUTCDay() + 6) % 7
3239

3340
const days = Array.from({ length: 42 }, (_, i) => {
3441
const day = new Date(
35-
currentDate.getFullYear(),
36-
currentDate.getMonth(),
37-
i - firstDayOfMonth + 1,
38-
currentDate.getHours(),
42+
Date.UTC(
43+
currentDate.getUTCFullYear(),
44+
currentDate.getUTCMonth(),
45+
i - firstDayOfMonth + 1,
46+
),
3947
)
4048
const filteredHolidays = holidays.filter(
4149
(h) =>
@@ -44,28 +52,32 @@ export function CalendarView({ currentDate, setCurrentDate, holidays }: Calendar
4452
)
4553
return {
4654
date: day,
47-
isCurrentMonth: day.getMonth() === currentDate.getMonth(),
48-
isToday: day.toDateString() === new Date().toDateString(),
55+
isCurrentMonth: day.getUTCMonth() === currentDate.getUTCMonth(),
56+
isToday: isSameUtcDay(day, today),
4957
holidays: filteredHolidays,
5058
}
5159
})
5260

5361
const prevMonth = () => {
54-
setCurrentDate(new Date(currentDate.getFullYear(), currentDate.getMonth() - 1, 1))
62+
setCurrentDate(new Date(Date.UTC(currentDate.getUTCFullYear(), currentDate.getUTCMonth() - 1, 1)))
5563
}
5664
const nextMonth = () => {
57-
setCurrentDate(new Date(currentDate.getFullYear(), currentDate.getMonth() + 1, 1))
65+
setCurrentDate(new Date(Date.UTC(currentDate.getUTCFullYear(), currentDate.getUTCMonth() + 1, 1)))
5866
}
5967
const setToday = () => {
60-
setCurrentDate(new Date())
68+
setCurrentDate(TimeUtil.toUtcMidnight(new Date()))
6169
}
6270

71+
// Format month/year using UTC values to avoid hydration mismatch
72+
const months = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December']
73+
const monthYearDisplay = `${months[currentDate.getUTCMonth()]} ${currentDate.getUTCFullYear()}`
74+
6375
return (
6476
<div className="lg:flex lg:h-full lg:flex-col">
6577
<header className="flex items-center justify-between border-b border-gray-200 px-6 py-4 lg:flex-none">
6678
<h1 className="text-base font-semibold text-foreground">
6779
<time dateTime={currentDate.toISOString()}>
68-
{currentDate.toLocaleString('default', { month: 'long', year: 'numeric' })}
80+
{monthYearDisplay}
6981
</time>
7082
</h1>
7183
<div className="flex items-center">
@@ -139,7 +151,7 @@ export function CalendarView({ currentDate, setCurrentDate, holidays }: Calendar
139151
day.isToday && 'bg-indigo-600 font-semibold text-white',
140152
)}
141153
>
142-
{day.date.getDate()}
154+
{day.date.getUTCDate()}
143155
</time>
144156
{day.holidays.length > 0 && (
145157
<ol className="mt-2 space-y-1 overflow-visible">
@@ -189,7 +201,7 @@ export function CalendarView({ currentDate, setCurrentDate, holidays }: Calendar
189201
'flex h-6 w-6 items-center justify-center rounded-full bg-gray-900',
190202
)}
191203
>
192-
{day.date.getDate()}
204+
{day.date.getUTCDate()}
193205
</time>
194206
<span className="sr-only">{day.holidays.length} holidays</span>
195207
{day.holidays.length > 0 && (

src/components/Holidays/HolidayTracker.stories.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ export const Default: Story = {
2929
args: {
3030
// Mock holidays data
3131
currentDate: format(new Date(2025, 0, 2), "yyyy-MM-dd'T'HH:mm:ss.SSSxxx"), // January 2025
32+
today: format(new Date(2025, 0, 2), "yyyy-MM-dd'T'HH:mm:ss.SSSxxx"), // Today's date for highlighting
3233
holidays: [
3334
{
3435
id: '1',

src/components/Holidays/HolidayTracker.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,8 @@ interface HolidayTrackerProps {
1919
holidays: Holiday[]
2020
leaveApprovals: LeaveRequest[]
2121
employees: Employee[]
22-
currentDate: string // ISO 8601 string
22+
currentDate: string // ISO 8601 string - the date being viewed
23+
today: string // ISO 8601 string - server's current date for "today" highlighting
2324
currentUser: { grade: string; remainingLeaveDays: number }
2425
submitLeaveRequest?: (formData: FormData) => Promise<{ success: boolean; message: string }>
2526
approveLeave: (ids: string[]) => Promise<{ success: boolean; message: string }>
@@ -34,6 +35,7 @@ export function HolidayTracker({
3435
holidays,
3536
currentUser,
3637
currentDate,
38+
today,
3739
leaveApprovals,
3840
employees,
3941
submitLeaveRequest,
@@ -46,6 +48,7 @@ export function HolidayTracker({
4648
const isLoading = false
4749

4850
const parsedCurrentDate = TimeUtil.toUtcMidnight(parseISO(currentDate))
51+
const parsedToday = TimeUtil.toUtcMidnight(parseISO(today))
4952

5053
const setCurrentDate = async (date: Date) => {
5154
const formattedDate = format(date, 'dd-MM-yyyy')
@@ -147,6 +150,7 @@ export function HolidayTracker({
147150
currentDate={parsedCurrentDate}
148151
setCurrentDate={setCurrentDate}
149152
holidays={holidays}
153+
today={parsedToday}
150154
/>
151155
)}
152156
{currentTab === 'Request Leave' && (

src/components/Holidays/RequestLeave.tsx

Lines changed: 39 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -44,11 +44,34 @@ const ButtonGroup: React.FC<ButtonGroupProps> = ({ options, value, onChange, dis
4444
}
4545

4646
export function RequestLeave({ remainingDays, submitLeaveRequest }: RequestLeaveProps) {
47-
const [startDate, setStartDate] = useState<Date | undefined>(new Date())
48-
const [endDate, setEndDate] = useState<Date | undefined>(new Date())
47+
// Helper to create a UTC midnight date from the user's local date (year, month, day)
48+
// This preserves the user's selected day regardless of their timezone
49+
const setToMidnightUTC = (date: Date) => {
50+
return new Date(Date.UTC(date.getFullYear(), date.getMonth(), date.getDate(), 0, 0, 0, 0))
51+
}
52+
53+
// Format date using UTC values to avoid hydration mismatch between server/client timezones
54+
const formatDateUTC = (date: Date) => {
55+
const months = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December']
56+
const day = date.getUTCDate()
57+
const month = months[date.getUTCMonth()]
58+
const year = date.getUTCFullYear()
59+
const suffix = day === 1 || day === 21 || day === 31 ? 'st' : day === 2 || day === 22 ? 'nd' : day === 3 || day === 23 ? 'rd' : 'th'
60+
return `${month} ${day}${suffix}, ${year}`
61+
}
62+
63+
const [startDate, setStartDate] = useState<Date | undefined>(undefined)
64+
const [endDate, setEndDate] = useState<Date | undefined>(undefined)
65+
66+
// Set initial dates on client only to avoid hydration mismatch
67+
useEffect(() => {
68+
if (startDate === undefined) setStartDate(setToMidnightUTC(new Date()))
69+
if (endDate === undefined) setEndDate(setToMidnightUTC(new Date()))
70+
// eslint-disable-next-line react-hooks/exhaustive-deps
71+
}, [])
4972
const [leaveType, setLeaveType] = useState('Full Day')
5073
const [isMultipleDays, setIsMultipleDays] = useState(false)
51-
const [totalDays, setTotalDays] = useState(1)
74+
const [totalDays, setTotalDays] = useState(0)
5275
const router = useRouter()
5376

5477
useEffect(() => {
@@ -129,17 +152,20 @@ export function RequestLeave({ remainingDays, submitLeaveRequest }: RequestLeave
129152
)}
130153
>
131154
<CalendarIcon className="mr-2 h-4 w-4" />
132-
{startDate ? format(startDate, 'PPP') : <span>Pick a date</span>}
155+
{startDate ? formatDateUTC(startDate) : <span>Pick a date</span>}
133156
</Button>
134157
</PopoverTrigger>
135158
<PopoverContent className="w-auto p-0" align="start">
136159
<Calendar
137160
mode="single"
138161
selected={startDate}
139162
onSelect={(date) => {
140-
setStartDate(date)
141-
if (date && (!endDate || date > endDate)) {
142-
setEndDate(date)
163+
if (date) {
164+
const dateMidnight = setToMidnightUTC(date)
165+
setStartDate(dateMidnight)
166+
if (!endDate || date > endDate) {
167+
setEndDate(dateMidnight)
168+
}
143169
}
144170
}}
145171
initialFocus
@@ -159,14 +185,18 @@ export function RequestLeave({ remainingDays, submitLeaveRequest }: RequestLeave
159185
)}
160186
>
161187
<CalendarIcon className="mr-2 h-4 w-4" />
162-
{endDate ? format(endDate, 'PPP') : <span>Pick a date</span>}
188+
{endDate ? formatDateUTC(endDate) : <span>Pick a date</span>}
163189
</Button>
164190
</PopoverTrigger>
165191
<PopoverContent className="w-auto p-0" align="start">
166192
<Calendar
167193
mode="single"
168194
selected={endDate}
169-
onSelect={setEndDate}
195+
onSelect={(date) => {
196+
if (date) {
197+
setEndDate(setToMidnightUTC(date))
198+
}
199+
}}
170200
disabled={(date) => (startDate ? date < startDate : false)}
171201
initialFocus
172202
/>

src/utils/DaysUtil.ts

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,22 @@
11
import { bankHolidays } from './BankHolidays'
22

33
export const isDayOff = (date: Date) => {
4-
const isSunday = date.getDay() === 0
5-
const isSaturday = date.getDay() === 6
6-
const isBankHoliday = bankHolidays.some((h) => h.toISOString() === date.toISOString())
4+
const isSunday = date.getUTCDay() === 0
5+
const isSaturday = date.getUTCDay() === 6
6+
const isBankHoliday = bankHolidays.some(
7+
(h) =>
8+
h.getFullYear() === date.getUTCFullYear() &&
9+
h.getMonth() === date.getUTCMonth() &&
10+
h.getDate() === date.getUTCDate()
11+
)
712

813
return isSunday || isSaturday || isBankHoliday
914
}
1015

1116
export const getTotalDaysBetween = (startDate: Date, endDate: Date, isHalfDay = false) => {
12-
const start = new Date(startDate)
13-
const end = new Date(endDate)
17+
// Work with UTC day values to avoid timezone issues
18+
let start = new Date(Date.UTC(startDate.getUTCFullYear(), startDate.getUTCMonth(), startDate.getUTCDate()))
19+
const end = new Date(Date.UTC(endDate.getUTCFullYear(), endDate.getUTCMonth(), endDate.getUTCDate()))
1420

1521
let count = 0
1622

@@ -23,7 +29,7 @@ export const getTotalDaysBetween = (startDate: Date, endDate: Date, isHalfDay =
2329
count++
2430
}
2531

26-
start.setDate(start.getDate() + 1)
32+
start = new Date(start.getTime() + 24 * 60 * 60 * 1000) // Add 1 day in milliseconds
2733
}
2834

2935
return count
@@ -34,6 +40,10 @@ export const getIsMultipleDays = (startDate?: Date, endDate?: Date) => {
3440
return false
3541
}
3642

37-
const isMultipleDays = startDate.toDateString() !== endDate.toDateString()
43+
// Compare UTC dates to avoid timezone issues
44+
const isMultipleDays =
45+
startDate.getUTCFullYear() !== endDate.getUTCFullYear() ||
46+
startDate.getUTCMonth() !== endDate.getUTCMonth() ||
47+
startDate.getUTCDate() !== endDate.getUTCDate()
3848
return isMultipleDays
3949
}

0 commit comments

Comments
 (0)