forked from DGNum/metis
tomate
e420cdd0b4
On met tout dans /data pour décorréler les sources du calendrier de la logique au maximum Co-authored-by: Tom Hubrecht <tom.hubrecht@ens.fr> Reviewed-on: https://git.rz.ens.wtf/Klub-RZ/metis/pulls/52 Co-authored-by: tomate <tom.hubrecht@ens.fr> Co-committed-by: tomate <tom.hubrecht@ens.fr>
208 lines
5.2 KiB
JavaScript
208 lines
5.2 KiB
JavaScript
import CALENDARS from '../data/calendars.json'
|
|
import LOCATIONS from '../data/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 parseCalendars = () => {
|
|
let calendars = {};
|
|
|
|
for (const [cloud, cals] of Object.entries(CALENDARS.sources)) {
|
|
for (const [id, attrs] of Object.entries(cals)) {
|
|
calendars[id] = { cloud: cloud, ...attrs }
|
|
}
|
|
}
|
|
|
|
return calendars;
|
|
}
|
|
|
|
const calendars = parseCalendars()
|
|
|
|
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 = CALENDARS.tree
|
|
|
|
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 correctedLocation = LOCATIONS.nameMap[location] || location
|
|
|
|
const result = Object.entries(LOCATIONS.rooms).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)
|
|
}
|
|
}
|