nielsr HF staff commited on
Commit
73d7323
·
1 Parent(s): b4bb93c

Compute deadline based on local time zone

Browse files
README.md CHANGED
@@ -108,7 +108,7 @@ You can see it in your web browser at http://localhost:8080/.
108
 
109
  ## Deploy on the cloud
110
 
111
- One way to deploy this on a cloud is by using [Artifact Registry](https://cloud.google.com/artifact-registry/docs) (for hosting the Docker image) and [Cloud Run](https://cloud.google.com/run?hl=en) (a serverless service by Google to run Docker containers).
112
 
113
  Make sure to:
114
  - create a [Google Cloud project](https://console.cloud.google.com/)
 
108
 
109
  ## Deploy on the cloud
110
 
111
+ One way to deploy this on a cloud is by using [Artifact Registry](https://cloud.google.com/artifact-registry/docs) (for hosting the Docker image) and [Cloud Run](https://cloud.google.com/run?hl=en) (a serverless service by Google to run Docker containers). See [this YouTube video](https://youtu.be/cw34KMPSt4k?si=UbzNRobNzib93uDl) for a nice intro.
112
 
113
  Make sure to:
114
  - create a [Google Cloud project](https://console.cloud.google.com/)
package-lock.json CHANGED
@@ -40,7 +40,8 @@
40
  "class-variance-authority": "^0.7.1",
41
  "clsx": "^2.1.1",
42
  "cmdk": "^1.0.0",
43
- "date-fns": "^3.6.0",
 
44
  "embla-carousel-react": "^8.3.0",
45
  "input-otp": "^1.2.4",
46
  "lucide-react": "^0.462.0",
@@ -4126,13 +4127,28 @@
4126
  }
4127
  },
4128
  "node_modules/date-fns": {
4129
- "version": "3.6.0",
4130
- "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-3.6.0.tgz",
4131
- "integrity": "sha512-fRHTG8g/Gif+kSh50gaGEdToemgfj74aRX3swtiouboip5JDLAyDE9F11nHMIcvOaXeOC6D7SpNhi7uFyB7Uww==",
4132
  "license": "MIT",
 
 
 
 
 
 
4133
  "funding": {
4134
- "type": "github",
4135
- "url": "https://github.com/sponsors/kossnocorp"
 
 
 
 
 
 
 
 
 
4136
  }
4137
  },
4138
  "node_modules/debug": {
 
40
  "class-variance-authority": "^0.7.1",
41
  "clsx": "^2.1.1",
42
  "cmdk": "^1.0.0",
43
+ "date-fns": "^2.30.0",
44
+ "date-fns-tz": "^2.0.0",
45
  "embla-carousel-react": "^8.3.0",
46
  "input-otp": "^1.2.4",
47
  "lucide-react": "^0.462.0",
 
4127
  }
4128
  },
4129
  "node_modules/date-fns": {
4130
+ "version": "2.30.0",
4131
+ "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.30.0.tgz",
4132
+ "integrity": "sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==",
4133
  "license": "MIT",
4134
+ "dependencies": {
4135
+ "@babel/runtime": "^7.21.0"
4136
+ },
4137
+ "engines": {
4138
+ "node": ">=0.11"
4139
+ },
4140
  "funding": {
4141
+ "type": "opencollective",
4142
+ "url": "https://opencollective.com/date-fns"
4143
+ }
4144
+ },
4145
+ "node_modules/date-fns-tz": {
4146
+ "version": "2.0.1",
4147
+ "resolved": "https://registry.npmjs.org/date-fns-tz/-/date-fns-tz-2.0.1.tgz",
4148
+ "integrity": "sha512-fJCG3Pwx8HUoLhkepdsP7Z5RsucUi+ZBOxyM5d0ZZ6c4SdYustq0VMmOu6Wf7bli+yS/Jwp91TOCqn9jMcVrUA==",
4149
+ "license": "MIT",
4150
+ "peerDependencies": {
4151
+ "date-fns": "2.x"
4152
  }
4153
  },
4154
  "node_modules/debug": {
package.json CHANGED
@@ -43,7 +43,8 @@
43
  "class-variance-authority": "^0.7.1",
44
  "clsx": "^2.1.1",
45
  "cmdk": "^1.0.0",
46
- "date-fns": "^3.6.0",
 
47
  "embla-carousel-react": "^8.3.0",
48
  "input-otp": "^1.2.4",
49
  "lucide-react": "^0.462.0",
 
43
  "class-variance-authority": "^0.7.1",
44
  "clsx": "^2.1.1",
45
  "cmdk": "^1.0.0",
46
+ "date-fns": "^2.30.0",
47
+ "date-fns-tz": "^2.0.0",
48
  "embla-carousel-react": "^8.3.0",
49
  "input-otp": "^1.2.4",
50
  "lucide-react": "^0.462.0",
src/components/ConferenceCard.tsx CHANGED
@@ -3,6 +3,7 @@ import { Conference } from "@/types/conference";
3
  import { formatDistanceToNow, parseISO, isValid, isPast } from "date-fns";
4
  import ConferenceDialog from "./ConferenceDialog";
5
  import { useState } from "react";
 
6
 
7
  const ConferenceCard = ({
8
  title,
@@ -21,11 +22,27 @@ const ConferenceCard = ({
21
  ...conferenceProps
22
  }: Conference) => {
23
  const [dialogOpen, setDialogOpen] = useState(false);
24
- const deadlineDate = deadline && deadline !== 'TBD' ? parseISO(deadline) : null;
25
- const isPastDeadline = deadlineDate ? isPast(deadlineDate) : false;
26
- const timeRemaining = deadlineDate && !isPastDeadline
27
- ? formatDistanceToNow(deadlineDate, { addSuffix: true })
28
- : null;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
29
 
30
  // Create location string by concatenating city and country
31
  const location = [city, country].filter(Boolean).join(", ");
@@ -33,10 +50,15 @@ const ConferenceCard = ({
33
  // Determine countdown color based on days remaining
34
  const getCountdownColor = () => {
35
  if (!deadlineDate || !isValid(deadlineDate)) return "text-neutral-600";
36
- const daysRemaining = Math.ceil((deadlineDate.getTime() - new Date().getTime()) / (1000 * 60 * 60 * 24));
37
- if (daysRemaining <= 7) return "text-red-600";
38
- if (daysRemaining <= 30) return "text-orange-600";
39
- return "text-green-600";
 
 
 
 
 
40
  };
41
 
42
  const handleCardClick = (e: React.MouseEvent) => {
 
3
  import { formatDistanceToNow, parseISO, isValid, isPast } from "date-fns";
4
  import ConferenceDialog from "./ConferenceDialog";
5
  import { useState } from "react";
6
+ import { getDeadlineInLocalTime } from '@/utils/dateUtils';
7
 
8
  const ConferenceCard = ({
9
  title,
 
22
  ...conferenceProps
23
  }: Conference) => {
24
  const [dialogOpen, setDialogOpen] = useState(false);
25
+ const deadlineDate = getDeadlineInLocalTime(deadline, timezone);
26
+
27
+ // Add validation before using formatDistanceToNow
28
+ const getTimeRemaining = () => {
29
+ if (!deadlineDate || !isValid(deadlineDate)) {
30
+ return 'TBD';
31
+ }
32
+
33
+ if (isPast(deadlineDate)) {
34
+ return 'Deadline passed';
35
+ }
36
+
37
+ try {
38
+ return formatDistanceToNow(deadlineDate, { addSuffix: true });
39
+ } catch (error) {
40
+ console.error('Error formatting time remaining:', error);
41
+ return 'Invalid date';
42
+ }
43
+ };
44
+
45
+ const timeRemaining = getTimeRemaining();
46
 
47
  // Create location string by concatenating city and country
48
  const location = [city, country].filter(Boolean).join(", ");
 
50
  // Determine countdown color based on days remaining
51
  const getCountdownColor = () => {
52
  if (!deadlineDate || !isValid(deadlineDate)) return "text-neutral-600";
53
+ try {
54
+ const daysRemaining = Math.ceil((deadlineDate.getTime() - new Date().getTime()) / (1000 * 60 * 60 * 24));
55
+ if (daysRemaining <= 7) return "text-red-600";
56
+ if (daysRemaining <= 30) return "text-orange-600";
57
+ return "text-green-600";
58
+ } catch (error) {
59
+ console.error('Error calculating countdown color:', error);
60
+ return "text-neutral-600";
61
+ }
62
  };
63
 
64
  const handleCardClick = (e: React.MouseEvent) => {
src/components/ConferenceDialog.tsx CHANGED
@@ -16,6 +16,7 @@ import {
16
  DropdownMenuTrigger,
17
  } from "@/components/ui/dropdown-menu";
18
  import { useState, useEffect } from "react";
 
19
 
20
  interface ConferenceDialogProps {
21
  conference: Conference;
@@ -25,7 +26,7 @@ interface ConferenceDialogProps {
25
 
26
  const ConferenceDialog = ({ conference, open, onOpenChange }: ConferenceDialogProps) => {
27
  console.log('Conference object:', conference);
28
- const deadlineDate = conference.deadline && conference.deadline !== 'TBD' ? parseISO(conference.deadline) : null;
29
  const [countdown, setCountdown] = useState<string>('');
30
 
31
  // Replace the current location string creation with this more verbose version
@@ -57,8 +58,8 @@ const ConferenceDialog = ({ conference, open, onOpenChange }: ConferenceDialogPr
57
  return;
58
  }
59
 
60
- const now = new Date().getTime();
61
- const difference = deadlineDate.getTime() - now;
62
 
63
  if (difference <= 0) {
64
  setCountdown('Deadline passed');
@@ -73,13 +74,8 @@ const ConferenceDialog = ({ conference, open, onOpenChange }: ConferenceDialogPr
73
  setCountdown(`${days}d ${hours}h ${minutes}m ${seconds}s`);
74
  };
75
 
76
- // Calculate immediately
77
  calculateTimeLeft();
78
-
79
- // Update every second
80
  const timer = setInterval(calculateTimeLeft, 1000);
81
-
82
- // Cleanup interval on component unmount
83
  return () => clearInterval(timer);
84
  }, [deadlineDate]);
85
 
@@ -180,6 +176,22 @@ END:VCALENDAR`;
180
  return `https://www.google.com/maps/search/?api=1&query=${encodeURIComponent(venue || place)}`;
181
  };
182
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
183
  return (
184
  <Dialog open={open} onOpenChange={onOpenChange}>
185
  <DialogContent
@@ -278,11 +290,7 @@ END:VCALENDAR`;
278
  <span className={`font-medium ${getCountdownColor()}`}>
279
  {countdown}
280
  </span>
281
- {deadlineDate && isValid(deadlineDate) && (
282
- <div className="text-sm text-neutral-500">
283
- {format(deadlineDate, "MMMM d, yyyy 'at' HH:mm:ss")} {conference.timezone}
284
- </div>
285
- )}
286
  </div>
287
  </div>
288
 
 
16
  DropdownMenuTrigger,
17
  } from "@/components/ui/dropdown-menu";
18
  import { useState, useEffect } from "react";
19
+ import { getDeadlineInLocalTime } from '@/utils/dateUtils';
20
 
21
  interface ConferenceDialogProps {
22
  conference: Conference;
 
26
 
27
  const ConferenceDialog = ({ conference, open, onOpenChange }: ConferenceDialogProps) => {
28
  console.log('Conference object:', conference);
29
+ const deadlineDate = getDeadlineInLocalTime(conference.deadline, conference.timezone);
30
  const [countdown, setCountdown] = useState<string>('');
31
 
32
  // Replace the current location string creation with this more verbose version
 
58
  return;
59
  }
60
 
61
+ const now = new Date();
62
+ const difference = deadlineDate.getTime() - now.getTime();
63
 
64
  if (difference <= 0) {
65
  setCountdown('Deadline passed');
 
74
  setCountdown(`${days}d ${hours}h ${minutes}m ${seconds}s`);
75
  };
76
 
 
77
  calculateTimeLeft();
 
 
78
  const timer = setInterval(calculateTimeLeft, 1000);
 
 
79
  return () => clearInterval(timer);
80
  }, [deadlineDate]);
81
 
 
176
  return `https://www.google.com/maps/search/?api=1&query=${encodeURIComponent(venue || place)}`;
177
  };
178
 
179
+ const formatDeadlineDisplay = () => {
180
+ if (!deadlineDate || !isValid(deadlineDate)) return null;
181
+
182
+ const localTZ = Intl.DateTimeFormat().resolvedOptions().timeZone;
183
+ return (
184
+ <div className="text-sm text-neutral-500">
185
+ <div>{format(deadlineDate, "MMMM d, yyyy 'at' HH:mm:ss")} ({localTZ})</div>
186
+ {conference.timezone && conference.timezone !== localTZ && (
187
+ <div className="text-xs">
188
+ Conference timezone: {conference.timezone}
189
+ </div>
190
+ )}
191
+ </div>
192
+ );
193
+ };
194
+
195
  return (
196
  <Dialog open={open} onOpenChange={onOpenChange}>
197
  <DialogContent
 
290
  <span className={`font-medium ${getCountdownColor()}`}>
291
  {countdown}
292
  </span>
293
+ {formatDeadlineDisplay()}
 
 
 
 
294
  </div>
295
  </div>
296
 
src/utils/dateUtils.ts ADDED
@@ -0,0 +1,126 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { parseISO, isValid } from 'date-fns';
2
+ import { zonedTimeToUtc, utcToZonedTime } from 'date-fns-tz';
3
+
4
+ export const getDeadlineInLocalTime = (deadline: string | undefined, timezone: string | undefined): Date | null => {
5
+ if (!deadline || deadline === 'TBD') {
6
+ console.log('Early return - deadline is null or TBD:', { deadline, timezone });
7
+ return null;
8
+ }
9
+
10
+ try {
11
+ console.log('Processing conference deadline:', {
12
+ conferenceName: 'Unknown', // We could pass this as a parameter if helpful
13
+ deadline,
14
+ timezone,
15
+ deadlineType: typeof deadline,
16
+ timezoneType: typeof timezone
17
+ });
18
+
19
+ // Parse the deadline string to a Date object
20
+ const deadlineDate = parseISO(deadline);
21
+ console.log('Parsed deadline date:', {
22
+ original: deadline,
23
+ parsed: deadlineDate,
24
+ isValid: isValid(deadlineDate),
25
+ timestamp: deadlineDate.getTime(),
26
+ toISOString: deadlineDate.toISOString()
27
+ });
28
+
29
+ if (!isValid(deadlineDate)) {
30
+ console.error('Invalid date parsed from deadline:', deadline);
31
+ return null;
32
+ }
33
+
34
+ // Handle AoE (Anywhere on Earth) timezone
35
+ if (timezone === 'AoE') {
36
+ console.log('Converting AoE to UTC-12');
37
+ return new Date(deadlineDate.getTime() - 12 * 60 * 60 * 1000);
38
+ }
39
+
40
+ // Handle UTC offset timezones (e.g., "UTC-12", "UTC+01", "UTC+0")
41
+ const normalizeTimezone = (tz: string | undefined): string => {
42
+ if (!tz) {
43
+ console.log('No timezone provided, using UTC');
44
+ return 'UTC';
45
+ }
46
+
47
+ console.log('Normalizing timezone:', tz);
48
+
49
+ // If it's already an IANA timezone, return as is
50
+ if (!tz.toUpperCase().startsWith('UTC')) {
51
+ console.log('Using IANA timezone:', tz);
52
+ return tz;
53
+ }
54
+
55
+ // Convert UTC±XX to proper format
56
+ const match = tz.match(/^UTC([+-])(\d+)$/);
57
+ if (match) {
58
+ const [, sign, hours] = match;
59
+ const paddedHours = hours.padStart(2, '0');
60
+ const normalized = `${sign}${paddedHours}:00`;
61
+ console.log('Normalized UTC offset:', { original: tz, normalized });
62
+ return normalized;
63
+ }
64
+
65
+ // Handle special case of UTC+0/UTC-0
66
+ if (tz === 'UTC+0' || tz === 'UTC-0' || tz === 'UTC+00' || tz === 'UTC-00') {
67
+ console.log('Handling UTC+0/-0 case:', tz);
68
+ return 'UTC';
69
+ }
70
+
71
+ console.log('Falling back to UTC for timezone:', tz);
72
+ return 'UTC';
73
+ };
74
+
75
+ const normalizedTimezone = normalizeTimezone(timezone);
76
+ console.log('Using timezone:', { original: timezone, normalized: normalizedTimezone });
77
+
78
+ try {
79
+ // Create date in the conference's timezone
80
+ const dateInConfTimezone = utcToZonedTime(deadlineDate, normalizedTimezone);
81
+ console.log('Conference timezone date:', {
82
+ date: dateInConfTimezone,
83
+ isValid: isValid(dateInConfTimezone),
84
+ timezone: normalizedTimezone
85
+ });
86
+
87
+ // Get user's local timezone
88
+ const userTimezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
89
+
90
+ // Convert to user's local timezone
91
+ const localDate = utcToZonedTime(dateInConfTimezone, userTimezone);
92
+ console.log('Local timezone date:', {
93
+ date: localDate,
94
+ isValid: isValid(localDate),
95
+ timezone: userTimezone
96
+ });
97
+
98
+ if (!isValid(localDate)) {
99
+ console.error('Invalid date after timezone conversion:', {
100
+ original: deadline,
101
+ timezone,
102
+ normalizedTimezone,
103
+ localDate
104
+ });
105
+ return null;
106
+ }
107
+
108
+ return localDate;
109
+ } catch (tzError) {
110
+ console.error('Timezone conversion error:', {
111
+ error: tzError,
112
+ deadline,
113
+ timezone,
114
+ normalizedTimezone
115
+ });
116
+ return deadlineDate;
117
+ }
118
+ } catch (error) {
119
+ console.error('Error parsing deadline:', {
120
+ error,
121
+ deadline,
122
+ timezone
123
+ });
124
+ return null;
125
+ }
126
+ };