nielsr HF Staff commited on
Commit
0fc7363
·
1 Parent(s): 2cf6965

Add support for multiple deadlines per conference

Browse files
CLAUDE.md ADDED
@@ -0,0 +1,77 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # CLAUDE.md
2
+
3
+ This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4
+
5
+ ## Project Overview
6
+ This is an AI Conference Deadlines web application that displays submission deadlines for top AI conferences like NeurIPS and ICLR. It's a React/TypeScript web app built with Vite, using shadcn-ui components and Tailwind CSS.
7
+
8
+ ## Development Commands
9
+ ```bash
10
+ # Install dependencies
11
+ npm i
12
+
13
+ # Start development server (runs on http://localhost:8080)
14
+ npm run dev
15
+
16
+ # Build for production
17
+ npm run build
18
+
19
+ # Build for development
20
+ npm run build:dev
21
+
22
+ # Lint code
23
+ npm run lint
24
+
25
+ # Preview production build
26
+ npm preview
27
+ ```
28
+
29
+ ## Architecture
30
+
31
+ ### Core Structure
32
+ - **Frontend**: React 18 + TypeScript + Vite
33
+ - **UI Framework**: shadcn-ui components with Radix UI primitives
34
+ - **Styling**: Tailwind CSS with custom animations
35
+ - **Data Source**: Static YAML file (`src/data/conferences.yml`) updated via GitHub Actions
36
+ - **State Management**: React hooks, no external state management library
37
+
38
+ ### Key Directories
39
+ - `src/components/` - React components (UI components in `ui/` subdirectory)
40
+ - `src/pages/` - Route components (Index, Calendar, NotFound)
41
+ - `src/data/` - Conference data in YAML format
42
+ - `src/types/` - TypeScript type definitions
43
+ - `src/utils/` - Utility functions for date handling and conference processing
44
+ - `src/hooks/` - Custom React hooks
45
+
46
+ ### Main Components
47
+ - `ConferenceList` - Primary list view of conferences
48
+ - `ConferenceCard` - Individual conference display card
49
+ - `ConferenceDialog` - Detailed conference information modal
50
+ - `FilterBar` - Conference filtering and search functionality
51
+ - `ConferenceCalendar` - Calendar view of conferences
52
+ - `Header` - Navigation and app header
53
+
54
+ ### Data Model
55
+ Conferences are defined by the `Conference` interface in `src/types/conference.ts` with properties including:
56
+ - Basic info: `title`, `year`, `id`, `full_name`, `link`
57
+ - Dates: `deadline`, `abstract_deadline`, `date`, `start`, `end`
58
+ - Location: `city`, `country`, `venue`
59
+ - Metadata: `tags`, `hindex`, `note`
60
+
61
+ ### Configuration Files
62
+ - `vite.config.ts` - Vite configuration with YAML plugin for loading conference data
63
+ - `tailwind.config.ts` - Tailwind CSS configuration with custom theme
64
+ - `components.json` - shadcn-ui component configuration
65
+ - `tsconfig.json` - TypeScript configuration
66
+
67
+ ### Data Updates
68
+ Conference data is automatically updated via GitHub Actions workflow (`.github/workflows/update-conferences.yml`) that fetches from ccfddl repository and creates pull requests with updates.
69
+
70
+ ### Path Aliases
71
+ - `@/*` maps to `src/*` for cleaner imports
72
+
73
+ ## Development Notes
74
+ - The app uses a YAML plugin to import conference data directly in components
75
+ - All UI components follow shadcn-ui patterns and conventions
76
+ - The project uses React Router for client-side routing
77
+ - Date handling uses `date-fns` and `date-fns-tz` for timezone support
src/components/ConferenceCard.tsx CHANGED
@@ -4,6 +4,7 @@ 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,7 +23,15 @@ const ConferenceCard = ({
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 = () => {
@@ -128,7 +137,7 @@ const ConferenceCard = ({
128
  <div className="flex items-center text-neutral">
129
  <Clock className="h-4 w-4 mr-2 flex-shrink-0" />
130
  <span className="text-sm truncate">
131
- {deadline === 'TBD' ? 'TBD' : deadline}
132
  </span>
133
  </div>
134
  <div className="flex items-center">
 
4
  import ConferenceDialog from "./ConferenceDialog";
5
  import { useState } from "react";
6
  import { getDeadlineInLocalTime } from '@/utils/dateUtils';
7
+ import { getNextUpcomingDeadline, getPrimaryDeadline } from '@/utils/deadlineUtils';
8
 
9
  const ConferenceCard = ({
10
  title,
 
23
  ...conferenceProps
24
  }: Conference) => {
25
  const [dialogOpen, setDialogOpen] = useState(false);
26
+
27
+ // Get the next upcoming deadline or primary deadline for display
28
+ const conference = {
29
+ title, full_name, year, date, deadline, timezone, tags, link, note,
30
+ abstract_deadline, city, country, venue, ...conferenceProps
31
+ };
32
+
33
+ const nextDeadline = getNextUpcomingDeadline(conference) || getPrimaryDeadline(conference);
34
+ const deadlineDate = nextDeadline ? getDeadlineInLocalTime(nextDeadline.date, nextDeadline.timezone || timezone) : null;
35
 
36
  // Add validation before using formatDistanceToNow
37
  const getTimeRemaining = () => {
 
137
  <div className="flex items-center text-neutral">
138
  <Clock className="h-4 w-4 mr-2 flex-shrink-0" />
139
  <span className="text-sm truncate">
140
+ {nextDeadline ? `${nextDeadline.label}: ${nextDeadline.date}` : (deadline === 'TBD' ? 'TBD' : deadline)}
141
  </span>
142
  </div>
143
  <div className="flex items-center">
src/components/ConferenceDialog.tsx CHANGED
@@ -17,6 +17,7 @@ import {
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,7 +27,12 @@ interface ConferenceDialogProps {
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
@@ -236,7 +242,7 @@ END:VCALENDAR`;
236
  <div className="flex items-start gap-2">
237
  <Globe className="h-5 w-5 mt-0.5 text-gray-500" />
238
  <div>
239
- <p className="font-medium">Location</p>
240
  <p className="text-sm text-gray-500">
241
  {conference.venue || [conference.city, conference.country].filter(Boolean).join(", ")}
242
  </p>
@@ -248,32 +254,24 @@ END:VCALENDAR`;
248
  <div className="space-y-2 flex-1">
249
  <p className="font-medium">Important Deadlines</p>
250
  <div className="text-sm text-gray-500 space-y-2">
251
- {conference.abstract_deadline && (
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
252
  <div className="bg-gray-100 rounded-md p-2">
253
- <p>Abstract: {formatDeadlineDate(conference.abstract_deadline)}</p>
254
- </div>
255
- )}
256
- <div className="bg-gray-100 rounded-md p-2">
257
- <p>Submission: {formatDeadlineDate(conference.deadline)}</p>
258
- </div>
259
- {conference.commitment_deadline && (
260
- <div className="bg-gray-100 rounded-md p-2">
261
- <p>Commitment: {formatDeadlineDate(conference.commitment_deadline)}</p>
262
- </div>
263
- )}
264
- {conference.review_release_date && (
265
- <div className="bg-gray-100 rounded-md p-2">
266
- <p>Reviews Released: {formatDeadlineDate(conference.review_release_date)}</p>
267
- </div>
268
- )}
269
- {(conference.rebuttal_period_start || conference.rebuttal_period_end) && (
270
- <div className="bg-gray-100 rounded-md p-2">
271
- <p>Rebuttal Period: {formatDeadlineDate(conference.rebuttal_period_start)} - {formatDeadlineDate(conference.rebuttal_period_end)}</p>
272
- </div>
273
- )}
274
- {conference.final_decision_date && (
275
- <div className="bg-gray-100 rounded-md p-2">
276
- <p>Final Decision: {formatDeadlineDate(conference.final_decision_date)}</p>
277
  </div>
278
  )}
279
  </div>
 
17
  } from "@/components/ui/dropdown-menu";
18
  import { useState, useEffect } from "react";
19
  import { getDeadlineInLocalTime } from '@/utils/dateUtils';
20
+ import { getAllDeadlines, getNextUpcomingDeadline, getUpcomingDeadlines } from '@/utils/deadlineUtils';
21
 
22
  interface ConferenceDialogProps {
23
  conference: Conference;
 
27
 
28
  const ConferenceDialog = ({ conference, open, onOpenChange }: ConferenceDialogProps) => {
29
  console.log('Conference object:', conference);
30
+
31
+ // Get upcoming deadlines and the next upcoming one
32
+ const upcomingDeadlines = getUpcomingDeadlines(conference);
33
+ const nextDeadline = getNextUpcomingDeadline(conference);
34
+ const deadlineDate = nextDeadline ? getDeadlineInLocalTime(nextDeadline.date, nextDeadline.timezone || conference.timezone) : null;
35
+
36
  const [countdown, setCountdown] = useState<string>('');
37
 
38
  // Replace the current location string creation with this more verbose version
 
242
  <div className="flex items-start gap-2">
243
  <Globe className="h-5 w-5 mt-0.5 text-gray-500" />
244
  <div>
245
+ <p className="font-medium">Venue</p>
246
  <p className="text-sm text-gray-500">
247
  {conference.venue || [conference.city, conference.country].filter(Boolean).join(", ")}
248
  </p>
 
254
  <div className="space-y-2 flex-1">
255
  <p className="font-medium">Important Deadlines</p>
256
  <div className="text-sm text-gray-500 space-y-2">
257
+ {upcomingDeadlines.length > 0 ? (
258
+ upcomingDeadlines.map((deadline, index) => {
259
+ const isNext = nextDeadline && deadline.date === nextDeadline.date && deadline.type === nextDeadline.type;
260
+ return (
261
+ <div
262
+ key={`${deadline.type}-${index}`}
263
+ className={`rounded-md p-2 ${isNext ? 'bg-blue-100 border border-blue-200' : 'bg-gray-100'}`}
264
+ >
265
+ <p className={isNext ? 'font-medium text-blue-800' : ''}>
266
+ {deadline.label}: {formatDeadlineDate(deadline.date)}
267
+ {isNext && <span className="ml-2 text-xs">(Next)</span>}
268
+ </p>
269
+ </div>
270
+ );
271
+ })
272
+ ) : (
273
  <div className="bg-gray-100 rounded-md p-2">
274
+ <p>No upcoming deadlines</p>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
275
  </div>
276
  )}
277
  </div>
src/data/conferences.yml CHANGED
@@ -5,6 +5,30 @@
5
  link: https://www2026.thewebconf.org/
6
  deadline: '2025-10-07 23:59:59'
7
  abstract_deadline: '2025-09-30 23:59:59'
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8
  timezone: UTC-12
9
  city: Dubai
10
  country: UAE
@@ -135,6 +159,23 @@
135
  link: https://iclr.cc/Conferences/2026
136
  deadline: '2025-09-24 23:59:59'
137
  abstract_deadline: '2025-09-19 23:59:59'
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
138
  timezone: UTC-12
139
  city: Rio de Janeiro
140
  country: Brazil
 
5
  link: https://www2026.thewebconf.org/
6
  deadline: '2025-10-07 23:59:59'
7
  abstract_deadline: '2025-09-30 23:59:59'
8
+ deadlines:
9
+ - type: abstract
10
+ label: Abstract Submission
11
+ date: '2025-09-30 23:59:59'
12
+ timezone: UTC-12
13
+ - type: submission
14
+ label: Paper Submission
15
+ date: '2025-10-07 23:59:59'
16
+ timezone: UTC-12
17
+ - type: rebuttal_start
18
+ label: Rebuttal Period Start
19
+ date: '2025-11-24 00:00:00'
20
+ timezone: UTC-12
21
+ - type: rebuttal_end
22
+ label: Rebuttal Period End
23
+ date: '2025-12-01 23:59:59'
24
+ - type: notification
25
+ label: Notification
26
+ date: '2026-01-13 23:59:59'
27
+ timezone: UTC-12
28
+ - type: camera_ready
29
+ label: Camera Ready
30
+ date: '2026-01-15 23:59:59'
31
+ timezone: UTC-12
32
  timezone: UTC-12
33
  city: Dubai
34
  country: UAE
 
159
  link: https://iclr.cc/Conferences/2026
160
  deadline: '2025-09-24 23:59:59'
161
  abstract_deadline: '2025-09-19 23:59:59'
162
+ deadlines:
163
+ - type: abstract
164
+ label: Abstract Submission
165
+ date: '2025-09-19 23:59:59'
166
+ timezone: UTC-12
167
+ - type: submission
168
+ label: Paper Submission
169
+ date: '2025-09-24 23:59:59'
170
+ timezone: UTC-12
171
+ - type: review_release
172
+ label: Reviews Released
173
+ date: '2026-01-20 23:59:59'
174
+ timezone: UTC-12
175
+ - type: notification
176
+ label: Notification
177
+ date: '2026-02-01 23:59:59'
178
+ timezone: UTC-12
179
  timezone: UTC-12
180
  city: Rio de Janeiro
181
  country: Brazil
src/types/conference.ts CHANGED
@@ -1,10 +1,18 @@
 
 
 
 
 
 
 
1
  export interface Conference {
2
  id: string;
3
  title: string;
4
  full_name?: string;
5
  year: number;
6
  link?: string;
7
- deadline: string;
 
8
  timezone?: string;
9
  date: string;
10
  place?: string;
@@ -13,7 +21,7 @@ export interface Conference {
13
  venue?: string;
14
  tags?: string[];
15
  note?: string;
16
- abstract_deadline?: string;
17
  start?: string;
18
  end?: string;
19
  rankings?: string;
 
1
+ export interface Deadline {
2
+ type: string;
3
+ label: string;
4
+ date: string;
5
+ timezone?: string;
6
+ }
7
+
8
  export interface Conference {
9
  id: string;
10
  title: string;
11
  full_name?: string;
12
  year: number;
13
  link?: string;
14
+ deadline: string; // Keep for backward compatibility
15
+ deadlines?: Deadline[]; // New multiple deadlines support
16
  timezone?: string;
17
  date: string;
18
  place?: string;
 
21
  venue?: string;
22
  tags?: string[];
23
  note?: string;
24
+ abstract_deadline?: string; // Keep for backward compatibility
25
  start?: string;
26
  end?: string;
27
  rankings?: string;
src/utils/conferenceUtils.ts CHANGED
@@ -1,24 +1,36 @@
1
  import { Conference } from "@/types/conference";
2
  import { getDeadlineInLocalTime } from './dateUtils';
 
3
 
4
  /**
5
- * Sort conferences by their adjusted deadline (accounting for timezone)
6
  */
7
  export function sortConferencesByDeadline(conferences: Conference[]): Conference[] {
8
  return [...conferences].sort((a, b) => {
9
- const aDeadline = getDeadlineInLocalTime(a.deadline, a.timezone);
10
- const bDeadline = getDeadlineInLocalTime(b.deadline, b.timezone);
11
 
12
- // If either date is invalid, place it later in the list
13
- if (!aDeadline || !bDeadline) {
14
- if (!aDeadline && !bDeadline) return 0;
15
- if (!aDeadline) return 1;
16
- if (!bDeadline) return -1;
17
  }
18
 
19
- // Both dates are valid, compare them
20
- if (aDeadline && bDeadline) {
21
- return aDeadline.getTime() - bDeadline.getTime();
 
 
 
 
 
 
 
 
 
 
 
22
  }
23
 
24
  return 0;
 
1
  import { Conference } from "@/types/conference";
2
  import { getDeadlineInLocalTime } from './dateUtils';
3
+ import { getPrimaryDeadline } from './deadlineUtils';
4
 
5
  /**
6
+ * Sort conferences by their primary deadline (next upcoming or most recent past)
7
  */
8
  export function sortConferencesByDeadline(conferences: Conference[]): Conference[] {
9
  return [...conferences].sort((a, b) => {
10
+ const aPrimaryDeadline = getPrimaryDeadline(a);
11
+ const bPrimaryDeadline = getPrimaryDeadline(b);
12
 
13
+ // If either conference has no deadlines, place it later in the list
14
+ if (!aPrimaryDeadline || !bPrimaryDeadline) {
15
+ if (!aPrimaryDeadline && !bPrimaryDeadline) return 0;
16
+ if (!aPrimaryDeadline) return 1;
17
+ if (!bPrimaryDeadline) return -1;
18
  }
19
 
20
+ // Both have deadlines, compare them
21
+ if (aPrimaryDeadline && bPrimaryDeadline) {
22
+ const aDeadline = getDeadlineInLocalTime(aPrimaryDeadline.date, aPrimaryDeadline.timezone || a.timezone);
23
+ const bDeadline = getDeadlineInLocalTime(bPrimaryDeadline.date, bPrimaryDeadline.timezone || b.timezone);
24
+
25
+ if (!aDeadline || !bDeadline) {
26
+ if (!aDeadline && !bDeadline) return 0;
27
+ if (!aDeadline) return 1;
28
+ if (!bDeadline) return -1;
29
+ }
30
+
31
+ if (aDeadline && bDeadline) {
32
+ return aDeadline.getTime() - bDeadline.getTime();
33
+ }
34
  }
35
 
36
  return 0;
src/utils/deadlineUtils.ts ADDED
@@ -0,0 +1,195 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Conference, Deadline } from "@/types/conference";
2
+ import { getDeadlineInLocalTime } from './dateUtils';
3
+ import { isValid, isPast } from "date-fns";
4
+
5
+ /**
6
+ * Get all deadlines for a conference, including both new format and legacy format
7
+ */
8
+ export function getAllDeadlines(conference: Conference): Deadline[] {
9
+ const deadlines: Deadline[] = [];
10
+ const seenTypes = new Set<string>();
11
+
12
+ // Add new format deadlines first (they take priority)
13
+ if (conference.deadlines && conference.deadlines.length > 0) {
14
+ conference.deadlines.forEach(deadline => {
15
+ deadlines.push(deadline);
16
+ seenTypes.add(deadline.type);
17
+ });
18
+ }
19
+
20
+ // Add legacy format deadlines for backward compatibility, but only if not already present
21
+ if (conference.abstract_deadline && !seenTypes.has('abstract')) {
22
+ deadlines.push({
23
+ type: 'abstract',
24
+ label: 'Abstract Submission',
25
+ date: conference.abstract_deadline,
26
+ timezone: conference.timezone
27
+ });
28
+ }
29
+
30
+ if (conference.deadline && !seenTypes.has('submission')) {
31
+ deadlines.push({
32
+ type: 'submission',
33
+ label: 'Paper Submission',
34
+ date: conference.deadline,
35
+ timezone: conference.timezone
36
+ });
37
+ }
38
+
39
+ if (conference.commitment_deadline && !seenTypes.has('commitment')) {
40
+ deadlines.push({
41
+ type: 'commitment',
42
+ label: 'Commitment',
43
+ date: conference.commitment_deadline,
44
+ timezone: conference.timezone
45
+ });
46
+ }
47
+
48
+ if (conference.review_release_date && !seenTypes.has('review_release')) {
49
+ deadlines.push({
50
+ type: 'review_release',
51
+ label: 'Reviews Released',
52
+ date: conference.review_release_date,
53
+ timezone: conference.timezone
54
+ });
55
+ }
56
+
57
+ if (conference.rebuttal_period_start && !seenTypes.has('rebuttal_start')) {
58
+ deadlines.push({
59
+ type: 'rebuttal_start',
60
+ label: 'Rebuttal Period Start',
61
+ date: conference.rebuttal_period_start,
62
+ timezone: conference.timezone
63
+ });
64
+ }
65
+
66
+ if (conference.rebuttal_period_end && !seenTypes.has('rebuttal_end')) {
67
+ deadlines.push({
68
+ type: 'rebuttal_end',
69
+ label: 'Rebuttal Period End',
70
+ date: conference.rebuttal_period_end,
71
+ timezone: conference.timezone
72
+ });
73
+ }
74
+
75
+ if (conference.final_decision_date && !seenTypes.has('final_decision')) {
76
+ deadlines.push({
77
+ type: 'final_decision',
78
+ label: 'Final Decision',
79
+ date: conference.final_decision_date,
80
+ timezone: conference.timezone
81
+ });
82
+ }
83
+
84
+ // Sort deadlines by date
85
+ deadlines.sort((a, b) => {
86
+ const aDate = getDeadlineInLocalTime(a.date, a.timezone || conference.timezone);
87
+ const bDate = getDeadlineInLocalTime(b.date, b.timezone || conference.timezone);
88
+
89
+ if (!aDate || !bDate) return 0;
90
+ return aDate.getTime() - bDate.getTime();
91
+ });
92
+
93
+ return deadlines;
94
+ }
95
+
96
+ /**
97
+ * Get the next upcoming deadline for a conference
98
+ */
99
+ export function getNextUpcomingDeadline(conference: Conference): Deadline | null {
100
+ const allDeadlines = getAllDeadlines(conference);
101
+
102
+ if (allDeadlines.length === 0) {
103
+ return null;
104
+ }
105
+
106
+ // Filter out past deadlines and invalid dates
107
+ const upcomingDeadlines = allDeadlines.filter(deadline => {
108
+ const deadlineDate = getDeadlineInLocalTime(deadline.date, deadline.timezone || conference.timezone);
109
+ return deadlineDate && isValid(deadlineDate) && !isPast(deadlineDate);
110
+ });
111
+
112
+ if (upcomingDeadlines.length === 0) {
113
+ return null;
114
+ }
115
+
116
+ // Sort by date and return the earliest
117
+ upcomingDeadlines.sort((a, b) => {
118
+ const aDate = getDeadlineInLocalTime(a.date, a.timezone || conference.timezone);
119
+ const bDate = getDeadlineInLocalTime(b.date, b.timezone || conference.timezone);
120
+
121
+ if (!aDate || !bDate) return 0;
122
+ return aDate.getTime() - bDate.getTime();
123
+ });
124
+
125
+ return upcomingDeadlines[0];
126
+ }
127
+
128
+ /**
129
+ * Get the primary deadline for sorting purposes (next upcoming or most recent past)
130
+ */
131
+ export function getPrimaryDeadline(conference: Conference): Deadline | null {
132
+ const nextDeadline = getNextUpcomingDeadline(conference);
133
+
134
+ if (nextDeadline) {
135
+ return nextDeadline;
136
+ }
137
+
138
+ // If no upcoming deadlines, return the most recent past deadline
139
+ const allDeadlines = getAllDeadlines(conference);
140
+
141
+ if (allDeadlines.length === 0) {
142
+ return null;
143
+ }
144
+
145
+ // Filter valid dates and sort by date (most recent first)
146
+ const validDeadlines = allDeadlines.filter(deadline => {
147
+ const deadlineDate = getDeadlineInLocalTime(deadline.date, deadline.timezone || conference.timezone);
148
+ return deadlineDate && isValid(deadlineDate);
149
+ });
150
+
151
+ if (validDeadlines.length === 0) {
152
+ return null;
153
+ }
154
+
155
+ validDeadlines.sort((a, b) => {
156
+ const aDate = getDeadlineInLocalTime(a.date, a.timezone || conference.timezone);
157
+ const bDate = getDeadlineInLocalTime(b.date, b.timezone || conference.timezone);
158
+
159
+ if (!aDate || !bDate) return 0;
160
+ return bDate.getTime() - aDate.getTime(); // Most recent first
161
+ });
162
+
163
+ return validDeadlines[0];
164
+ }
165
+
166
+ /**
167
+ * Check if a conference has any upcoming deadlines
168
+ */
169
+ export function hasUpcomingDeadlines(conference: Conference): boolean {
170
+ return getNextUpcomingDeadline(conference) !== null;
171
+ }
172
+
173
+ /**
174
+ * Get all upcoming deadlines sorted by date
175
+ */
176
+ export function getUpcomingDeadlines(conference: Conference): Deadline[] {
177
+ const allDeadlines = getAllDeadlines(conference);
178
+
179
+ // Filter out past deadlines and invalid dates
180
+ const upcomingDeadlines = allDeadlines.filter(deadline => {
181
+ const deadlineDate = getDeadlineInLocalTime(deadline.date, deadline.timezone || conference.timezone);
182
+ return deadlineDate && isValid(deadlineDate) && !isPast(deadlineDate);
183
+ });
184
+
185
+ // Sort by date
186
+ upcomingDeadlines.sort((a, b) => {
187
+ const aDate = getDeadlineInLocalTime(a.date, a.timezone || conference.timezone);
188
+ const bDate = getDeadlineInLocalTime(b.date, b.timezone || conference.timezone);
189
+
190
+ if (!aDate || !bDate) return 0;
191
+ return aDate.getTime() - bDate.getTime();
192
+ });
193
+
194
+ return upcomingDeadlines;
195
+ }