import STATIC_LOCATIONS from './static-ens-locations.json' // https://stackoverflow.com/a/35970186 function invertColor(hex) { if (hex.indexOf('#') === 0) { hex = hex.slice(1) } // convert 3-digit hex to 6-digits. if (hex.length === 3) { hex = hex[0] + hex[0] + hex[1] + hex[1] + hex[2] + hex[2] } if (hex.length !== 6) { throw new Error('Invalid HEX color.') } var r = parseInt(hex.slice(0, 2), 16), g = parseInt(hex.slice(2, 4), 16), b = parseInt(hex.slice(4, 6), 16) // https://stackoverflow.com/a/3943023/112731 return r * 0.299 + g * 0.587 + b * 0.114 > 186 ? '#000000' : '#FFFFFF' } const clouds = { KLUB_RESEAU: 'klub-reseau', ELEVES_ENS: 'eleves-ens', FRAMA_AGENDA: 'frama-agenda' } const calendars = { '5WrcagPPARQ3BD87': { cloud: clouds.KLUB_RESEAU, name: 'Club réseau', color: null, default_location: "Cave d'hackENS" }, TFEAKjAgNFQZpNjo: { cloud: clouds.KLUB_RESEAU, name: 'hackENS', color: null, default_location: "Cave d'hackENS" }, LLWm8qK9iC5YGrrR: { cloud: clouds.ELEVES_ENS, name: 'Délégation Générale', short_name: 'DG', color: null }, '2KGkWzBJGorxzyTW': { cloud: clouds.ELEVES_ENS, name: "La Nuit de l'ENS", short_name: 'La Nuit', color: null }, w442JdS5AaQ6czrP: { cloud: clouds.ELEVES_ENS, name: "Écriv'ENS", color: null }, fRtjDkjrZyn6fxd8: { cloud: clouds.ELEVES_ENS, name: 'K-Fêt', color: '#c63b52', default_location: 'K-Fêt' }, gsZtZK8c9EmREofn: { cloud: clouds.ELEVES_ENS, name: 'Ernestophone', color: null }, dTHrXnYgsEoSTjWB: { cloud: clouds.ELEVES_ENS, name: 'Évènements (COF)', short_name: 'COF', color: null }, bCgRFByHLiCCNc55: { cloud: clouds.ELEVES_ENS, name: 'Assemblées Générales (COF)', short_name: 'AG COF', color: null }, r4yJZDHjwNtH8wkR: { cloud: clouds.ELEVES_ENS, name: 'BdA', color: null }, T5WoHbs4FT5A945Z: { cloud: clouds.FRAMA_AGENDA, name: 'CinéClub', color: null }, '6SHG6cg9d7S3qqwD': { cloud: clouds.ELEVES_ENS, name: 'Club Inutile ☔', color: null, initial: false }, Ekjb4kDqMMqwJXZF: { cloud: clouds.ELEVES_ENS, name: 'Rentrée des départements', short_name: 'Dpt', color: null }, '8SKP62tQJP65K8EW': { cloud: clouds.ELEVES_ENS, name: 'Conférences de recherche', short_name: 'Conf', color: null }, PnRXqeq4SsSC33FM: { cloud: clouds.ELEVES_ENS, name: 'Visites de bibliothèques', short_name: 'Bibli', initial: false, color: null }, NWPtiEiz62LTtjo2: { cloud: clouds.ELEVES_ENS, name: 'Amphis de rentrée', short_name: 'Prés. de rentrée', color: null }, JiRt58aJXay9kfyk: { cloud: clouds.ELEVES_ENS, name: 'Réunions de rentrée des Masters', short_name: 'Masters', color: null }, '5Rb4bRjCDcsFjDdQ': { cloud: clouds.ELEVES_ENS, name: 'Activités pour les étudiants internationaux', short_name: 'Internationaux', color: null }, '62wKfQRrLNz2WXjt': { cloud: clouds.ELEVES_ENS, name: 'Divers', color: null } } const calendarsByName = Object.fromEntries( Object.entries(calendars).map(([id, { name }]) => [name, id]) ) export const initialCalendars = Array.from( Object.entries(calendars).map(([_, { name, initial }]) => [name, initial ?? true]) ) .filter(cal => cal[1]) .map(cal => cal[0]) export const calendarTree = { COF: { BdA: {}, 'Évènements (COF)': {}, 'Assemblées Générales (COF)': {} }, 'Clubs COF': { 'Club réseau': {}, hackENS: {}, "Écriv'ENS": {}, CinéClub: {}, Ernestophone: {}, 'Club Inutile ☔': {} }, "La Nuit de l'ENS": {}, 'Délégation Générale': {}, 'K-Fêt': {}, 'Rentrée académique': { 'Amphis de rentrée': {}, 'Rentrée des départements': {}, 'Conférences de recherche': {}, 'Visites de bibliothèques': {}, 'Réunions de rentrée des Masters': {}, 'Activités pour les étudiants internationaux': {} }, Divers: {} } const dfs = (p, t, l) => { for (const [c, s] of Object.entries(t)) { l[c] = p === null ? [] : [p, ...l[p]] dfs(c, s, l) } } export const ancestors = (() => { let l = [] dfs(null, calendarTree, l) return l })() export function getSubCalendars(name, tree = calendarTree) { let ret for (const [cal, subTree] of Object.entries(tree)) { if (cal === name) { ret = subTree } else { ret = ret || getSubCalendars(name, subTree) } } return ret } const calendarIds = Object.keys(calendars) function mkCalendarUrl(id, { cloud }, extra = {}) { return ( `/cal/${cloud}/${id}/?` + new URLSearchParams({ ...extra, export: true, expand: true, accept: 'jcal' }) ) } function mkExportUrl(id, { cloud }) { return `/cal/${cloud}/${id}/?export` } function fetchCalendar(id, cal, extra = {}) { return fetch(mkCalendarUrl(id, cal, extra), { credentials: 'omit' }) .then(resp => resp.json()) .catch(err => console.error(err)) } class Calendar { constructor(id, calendar) { const metadata = calendars[id] this.name = metadata.name this.short_name = metadata.short_name this.color = metadata.color || calendar[1][4][3] this.default_location = metadata.default_location this.events = calendar[2] .filter(item => item[0] === 'vevent') .map(item => this._parse_vevent(item[1])) } _parse_vevent(vevent) { const event = {} vevent.forEach(elt => { event[elt[0]] = elt[3] }) return event } } function findLocationId(location) { const adhocMap = { 'Amphi Jourdan': 'Amphithéâtre Jourdan', 'R2-21 (Jourdan)': 'R2-21', 'Salle Jean Ibanes (Jourdan, R1-07)': 'Salle Jean Ibanes (R1-07)', 'Salle Madeleine Rebérioux (Jourdan, R2-02)': 'Salle Madeleine Rebérioux (R2-02)', 'Salle Marcel Roncayolo (Jourdan, R2-05)': 'Salle Marcel Roncayolo (R2-05)' } const correctedLocation = adhocMap[location] || location const result = Object.entries(STATIC_LOCATIONS).find(([building, rooms]) => rooms.includes(correctedLocation) ) if (result === undefined) return undefined const [building, _] = result return `${building}-${correctedLocation}` } function fcEventFromjCalEvent(cal) { return function (evt) { const start = new Date(evt.dtstart) const end = new Date(evt.dtend) const fcEvent = { title: `${cal.short_name ?? cal.name} : ${evt.summary}`, start: evt.dtstart, end: evt.dtend, color: cal.color, textColor: invertColor(cal.color), duration: end - start // in ms } fcEvent.calendar = cal.name fcEvent.short_name = evt.summary fcEvent.description = evt.description fcEvent.location = evt.location || cal.default_location if (fcEvent.location) { fcEvent.resourceId = findLocationId(fcEvent.location) } if (evt.status) { fcEvent.status = evt.status fcEvent.classNames = [`st-${evt.status.toLowerCase()}`] } if (evt.rrule) { const { freq, byday, interval } = evt.rrule fcEvent.rrule = { freq, byweekday: byday, dtstart: evt.dtstart } if (interval) { fcEvent.rrule.interval = interval } } return fcEvent } } function mkEventsFromCalendar(id, cal) { return fetchCalendar(id, cal).then(calendar => { if (calendar[0] !== 'vcalendar') return const cal = new Calendar(id, calendar) return cal.events.map(fcEventFromjCalEvent(cal)) }) } export function mkSource(name) { const calendarId = calendarsByName[name] if (!calendarId) return null const calendar = calendars[calendarId] return { id: name, ...(calendar?.meta || {}), success: calendarData => { if (calendarData[0] !== 'vcalendar') return const cal = new Calendar(calendarId, calendarData) return cal.events.map(fcEventFromjCalEvent(cal)) }, failure: error => { console.error(`Fatal error during event source fetching of '${name}': ${error}`) }, events: (info, successCallback, failureCallback) => { const { start, end } = info fetchCalendar(calendarId, calendar, { start: start.valueOf() / 1000, end: end.valueOf() / 1000 }).then(successCallback, failureCallback) }, export_url: mkExportUrl(calendarId, calendar) } }