@@ -90,14 +90,29 @@ <h3 id="conf-subtitle">a.k.a. {{page.alt_name}} {{page.year}}</h3>
9090 </ div >
9191 </ div >
9292 </ div >
93- < div id ="page-content ">
93+ < div id ="page-content "
94+ data-conf-id ="{{page.conference | slugify: "latin "}}-{{page.year}}"
95+ data-conf-name="{{page.conference}} "
96+ data-conf-year ="{{page.year}} "
97+ data-location ="{{page.place}} "
98+ data-cfp ="{{page.cfp}} "
99+ data-cfp-ext ="{{page.cfp_ext}} "
100+ data-start ="{{page.start}} "
101+ data-end ="{{page.end}} "
102+ data-link ="{{page.link}} ">
94103 < div id ="conf-deadline-timer " class ="row ">
95104 < div id ="cfp-timer " class ="col-12 conf-timer countdown-display "
96105 data-deadline ="{{ page.cfp_ext | default: page.cfp }} "
97106 data-timezone ="{{ page.timezone | default: 'UTC-12' }} ">
98107 {% if cfp == "TBA" or cfp == "Cancelled" or cfp == "None" %}{{cfp}}{%endif%}
99108 </ div >
100109 </ div >
110+ <!-- Conference Action Buttons -->
111+ < div class ="row ">
112+ < div class ="col-12 ">
113+ {% include conference_detail_actions.html %}
114+ </ div >
115+ </ div >
101116 < div id ="conf-key-facts " class ="row ">
102117 < div class ="col-12 col-md-6 ">
103118 < div >
@@ -161,14 +176,6 @@ <h3 id="conf-subtitle">a.k.a. {{page.alt_name}} {{page.year}}</h3>
161176 </ div >
162177 < div id ="conf-deadlines " class ="row ">
163178 < div class ="col-12 col-md-6 ">
164- < div class ="meta deadline ">
165- {% t conference.download_dl %}:
166- </ div >
167- < div id ="conference-deadline " class ="calendar meta "> </ div >
168- < div class ="meta deadline ">
169- {% t conference.download_date %}:
170- </ div >
171- < div id ="conference-calendar " class ="calendar meta "> </ div >
172179 < div class ="meta deadline ">
173180 {% t conference.deadline_tz_theirs %}:
174181 </ div >
@@ -264,36 +271,9 @@ <h3 id="conf-subtitle">a.k.a. {{page.alt_name}} {{page.year}}</h3>
264271 console . log ( "Invalid timezone. Using system timezone instead." ) ;
265272 }
266273
267- // add calendar
268-
269- var conferenceDeadlineCalendar = createCalendarFromObject ( {
270- id : '{{page.conference | slugify: "latin"}}-{{page.year}}' ,
271- title : '{{page.conference}} {{page.year}} deadline' ,
272- start_date : confDeadline . toJSDate ( ) ,
273- duration : 60 ,
274- place : '{{page.place}}' ,
275- link : '{{page.link}}' ,
276- } ) ;
277- var deadlineContainer = document . querySelector ( '#conference-deadline' ) ;
278- if ( deadlineContainer ) {
279- deadlineContainer . appendChild ( conferenceDeadlineCalendar ) ;
280- }
281-
282-
283- var conferenceCalendar = createCalendarFromObject ( {
284- id : '{{page.conference | slugify: "latin"}}-{{page.year}}' ,
285- title : '{{page.conference}} {{page.year}}' ,
286- start_date : DateTime . fromSQL ( "{{page.start}}" ) . toJSDate ( ) ,
287- end_date : DateTime . fromSQL ( "{{page.end}}" ) . toJSDate ( ) ,
288- place : '{{page.place}}' ,
289- link : '{{page.link}}' ,
290- } ) ;
291- var calendarContainer = document . querySelector ( '#conference-calendar' ) ;
292- if ( calendarContainer ) {
293- calendarContainer . appendChild ( conferenceCalendar ) ;
294- }
274+ // Calendar export is now handled by conference action buttons
295275
296- // Countdown timer is now handled by countdown-simple.js automatically
276+ // Countdown timer is handled by countdown-simple.js automatically
297277
298278 // render deadline conference time
299279 { % if page . timezone % }
@@ -307,6 +287,169 @@ <h3 id="conf-subtitle">a.k.a. {{page.alt_name}} {{page.year}}</h3>
307287
308288 { % endif % }
309289
290+ // Conference Detail Action Buttons Event Handlers
291+
292+ // Notification helper
293+ function showConferenceNotification ( text , type ) {
294+ const bgColor = type === 'success' ? '#28a745' : type === 'info' ? '#17a2b8' : '#ffc107' ;
295+ const message = $ ( '<div>' )
296+ . text ( text )
297+ . css ( {
298+ position : 'fixed' ,
299+ top : '20px' ,
300+ right : '20px' ,
301+ background : bgColor ,
302+ color : 'white' ,
303+ padding : '12px 24px' ,
304+ borderRadius : '6px' ,
305+ zIndex : 9999 ,
306+ boxShadow : '0 4px 12px rgba(0,0,0,0.2)' ,
307+ animation : 'slideIn 0.3s ease'
308+ } ) ;
309+
310+ $ ( 'body' ) . append ( message ) ;
311+ setTimeout ( ( ) => {
312+ message . fadeOut ( 300 , ( ) => message . remove ( ) ) ;
313+ } , 3000 ) ;
314+ }
315+
316+ // Follow Series Button
317+ $ ( '.btn-follow-series' ) . on ( 'click' , function ( ) {
318+ const btn = $ ( this ) ;
319+ const seriesName = btn . data ( 'series' ) ;
320+
321+ if ( window . confManager ) {
322+ const isFollowing = window . confManager . isSeriesFollowed ( seriesName ) ;
323+
324+ if ( isFollowing ) {
325+ window . confManager . unfollowSeries ( seriesName ) ;
326+ btn . removeClass ( 'active' ) ;
327+ btn . find ( '.btn-text' ) . text ( 'Follow Series' ) ;
328+ showConferenceNotification ( 'Unfollowed ' + seriesName , 'info' ) ;
329+ } else {
330+ window . confManager . followSeries ( seriesName ) ;
331+ btn . addClass ( 'active' ) ;
332+ btn . find ( '.btn-text' ) . text ( 'Following' ) ;
333+ showConferenceNotification ( 'Now following ' + seriesName + ' series!' , 'success' ) ;
334+ }
335+ }
336+ } ) ;
337+
338+ // Save Conference Button
339+ $ ( '.btn-save-conference' ) . on ( 'click' , function ( ) {
340+ const btn = $ ( this ) ;
341+ const confId = btn . data ( 'conf-id' ) ;
342+
343+ if ( window . confManager ) {
344+ const isSaved = window . confManager . isEventSaved ( confId ) ;
345+
346+ if ( isSaved ) {
347+ window . confManager . removeSavedEvent ( confId ) ;
348+ btn . removeClass ( 'active' ) ;
349+ btn . find ( 'i' ) . removeClass ( 'fas' ) . addClass ( 'far' ) ;
350+ btn . find ( '.btn-text' ) . text ( 'Save' ) ;
351+ showConferenceNotification ( 'Removed from favorites' , 'info' ) ;
352+ } else {
353+ window . confManager . saveEvent ( confId ) ;
354+ btn . addClass ( 'active' ) ;
355+ btn . find ( 'i' ) . removeClass ( 'far' ) . addClass ( 'fas' ) ;
356+ btn . find ( '.btn-text' ) . text ( 'Saved' ) ;
357+ showConferenceNotification ( 'Saved to favorites!' , 'success' ) ;
358+ }
359+ }
360+ } ) ;
361+
362+ // Calendar Button - Generate calendar links
363+ $ ( '.calendar-option' ) . on ( 'click' , function ( e ) {
364+ e . preventDefault ( ) ;
365+ const calendarType = $ ( this ) . data ( 'calendar' ) ;
366+
367+ // Get conference data from page
368+ const confName = '{{ page.conference }}' ;
369+ const confYear = '{{ page.year }}' ;
370+ const cfpDate = '{{ page.cfp_ext | default: page.cfp }}' ;
371+ const confPlace = '{{ page.place }}' ;
372+
373+ // Format the date for calendar
374+ const eventDate = new Date ( cfpDate ) ;
375+ const startDate = eventDate . toISOString ( ) . replace ( / - | : | \. \d \d \d / g, '' ) ;
376+
377+ // Create event details
378+ const eventTitle = confName + ' ' + confYear + ' - CFP Deadline' ;
379+ const eventDetails = 'Call for Proposals deadline for ' + confName + ' ' + confYear ;
380+
381+ let calendarUrl = '' ;
382+
383+ switch ( calendarType ) {
384+ case 'google' :
385+ calendarUrl = 'https://calendar.google.com/calendar/render?action=TEMPLATE&text=' +
386+ encodeURIComponent ( eventTitle ) +
387+ '&details=' + encodeURIComponent ( eventDetails ) +
388+ '&location=' + encodeURIComponent ( confPlace ) +
389+ '&dates=' + startDate + '/' + startDate ;
390+ window . open ( calendarUrl , '_blank' ) ;
391+ showConferenceNotification ( 'Opening Google Calendar...' , 'success' ) ;
392+ break ;
393+
394+ case 'outlook' :
395+ calendarUrl = 'https://outlook.live.com/calendar/0/deeplink/compose?subject=' +
396+ encodeURIComponent ( eventTitle ) +
397+ '&body=' + encodeURIComponent ( eventDetails ) +
398+ '&location=' + encodeURIComponent ( confPlace ) +
399+ '&startdt=' + startDate +
400+ '&enddt=' + startDate ;
401+ window . open ( calendarUrl , '_blank' ) ;
402+ showConferenceNotification ( 'Opening Outlook Calendar...' , 'success' ) ;
403+ break ;
404+
405+ case 'apple' :
406+ case 'ics' :
407+ // Generate ICS file
408+ const icsContent = 'BEGIN:VCALENDAR\n' +
409+ 'VERSION:2.0\n' +
410+ 'PRODID:-//PythonDeadlines//Conference CFP//EN\n' +
411+ 'BEGIN:VEVENT\n' +
412+ 'UID:' + Date . now ( ) + '@pythondeadlin.es\n' +
413+ 'DTSTAMP:' + new Date ( ) . toISOString ( ) . replace ( / - | : | \. \d \d \d / g, '' ) + '\n' +
414+ 'DTSTART:' + startDate + '\n' +
415+ 'DTEND:' + startDate + '\n' +
416+ 'SUMMARY:' + eventTitle + '\n' +
417+ 'DESCRIPTION:' + eventDetails + '\n' +
418+ 'LOCATION:' + confPlace + '\n' +
419+ 'END:VEVENT\n' +
420+ 'END:VCALENDAR' ;
421+
422+ const blob = new Blob ( [ icsContent ] , { type : 'text/calendar' } ) ;
423+ const url = URL . createObjectURL ( blob ) ;
424+ const link = document . createElement ( 'a' ) ;
425+ link . href = url ;
426+ link . download = confName . replace ( / [ ^ a - z 0 - 9 ] / gi, '_' ) + '_' + confYear + '_CFP.ics' ;
427+ document . body . appendChild ( link ) ;
428+ link . click ( ) ;
429+ document . body . removeChild ( link ) ;
430+ URL . revokeObjectURL ( url ) ;
431+ showConferenceNotification ( 'Calendar file downloaded!' , 'success' ) ;
432+ break ;
433+ }
434+ } ) ;
435+
436+ // Initialize button states on page load
437+ const confId = '{{ page.conference | slugify: "latin" }}-{{ page.year }}' ;
438+ const seriesName = '{{ page.conference }}' ;
439+
440+ if ( window . confManager ) {
441+ // Update Follow Series button state
442+ if ( window . confManager . isSeriesFollowed ( seriesName ) ) {
443+ $ ( '.btn-follow-series' ) . addClass ( 'active' ) . find ( '.btn-text' ) . text ( 'Following' ) ;
444+ }
445+
446+ // Update Save button state
447+ if ( window . confManager . isEventSaved ( confId ) ) {
448+ $ ( '.btn-save-conference' ) . addClass ( 'active' ) . find ( 'i' ) . removeClass ( 'far' ) . addClass ( 'fas' ) ;
449+ $ ( '.btn-save-conference .btn-text' ) . text ( 'Saved' ) ;
450+ }
451+ }
452+
310453 } ) ;
311454 </ script >
312455
0 commit comments