Spaces:
				
			
			
	
			
			
					
		Running
		
	
	
	
			
			
	
	
	
	
		
		
					
		Running
		
	Add support for multiple deadlines per conference
Browse files- CLAUDE.md +77 -0
- src/components/ConferenceCard.tsx +11 -2
- src/components/ConferenceDialog.tsx +25 -27
- src/data/conferences.yml +41 -0
- src/types/conference.ts +10 -2
- src/utils/conferenceUtils.ts +23 -11
- src/utils/deadlineUtils.ts +195 -0
    	
        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 | 
            -
               | 
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
| 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 | 
            -
               | 
|  | |
|  | |
|  | |
|  | |
|  | |
| 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"> | 
| 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 | 
            -
                              { | 
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
| 252 | 
             
                                <div className="bg-gray-100 rounded-md p-2">
         | 
| 253 | 
            -
                                  <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  | 
| 6 | 
             
             */
         | 
| 7 | 
             
            export function sortConferencesByDeadline(conferences: Conference[]): Conference[] {
         | 
| 8 | 
             
              return [...conferences].sort((a, b) => {
         | 
| 9 | 
            -
                const  | 
| 10 | 
            -
                const  | 
| 11 |  | 
| 12 | 
            -
                // If either  | 
| 13 | 
            -
                if (! | 
| 14 | 
            -
                  if (! | 
| 15 | 
            -
                  if (! | 
| 16 | 
            -
                  if (! | 
| 17 | 
             
                }
         | 
| 18 |  | 
| 19 | 
            -
                // Both  | 
| 20 | 
            -
                if ( | 
| 21 | 
            -
                   | 
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
| 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 | 
            +
            }
         | 

