forked from DGNum/metis
211 lines
5.3 KiB
Svelte
211 lines
5.3 KiB
Svelte
<script>
|
|
import { onMount } from 'svelte';
|
|
import { writable } from 'svelte/store';
|
|
import FullCalendar from 'svelte-fullcalendar';
|
|
import timeGridPlugin from '@fullcalendar/timegrid';
|
|
import adaptivePlugin from '@fullcalendar/adaptive';
|
|
import rrulePlugin from '@fullcalendar/rrule';
|
|
import dayGridPlugin from '@fullcalendar/daygrid';
|
|
import listPlugin from '@fullcalendar/list';
|
|
import resourceTimelinePlugin from '@fullcalendar/resource-timeline';
|
|
import frLocale from '@fullcalendar/core/locales/fr';
|
|
import EventModal from './EventModal.svelte';
|
|
import FilterBar from './FilterBar.svelte';
|
|
import { mkSource, calendarTree, initialCalendars, getSubCalendars } from './calendar';
|
|
import { debounce } from 'lodash';
|
|
import Help from './Help.svelte';
|
|
|
|
import 'bootstrap/dist/css/bootstrap.css';
|
|
import 'bootstrap-icons/font/bootstrap-icons.css';
|
|
import bootstrap5Plugin from '@fullcalendar/bootstrap5';
|
|
import { Tooltip } from 'bootstrap';
|
|
|
|
import { createPopper } from '@popperjs/core';
|
|
|
|
import ENSLocations from './static-ens-locations.json';
|
|
|
|
const event = writable(null);
|
|
|
|
let openModal = false;
|
|
const toggle = () => (openModal = !openModal);
|
|
const mobile = window.innerWidth < 765;
|
|
const now = new Date();
|
|
const scrollTo = (() => {
|
|
const time = new Date();
|
|
time.setHours(Math.max(0, time.getHours() - 2));
|
|
return time.toLocaleTimeString();
|
|
})();
|
|
|
|
const allowedViews = [
|
|
'resourceTimelineDay',
|
|
'dayGridMonth',
|
|
'timeGridWeek',
|
|
'timeGridDay',
|
|
'listWeek'
|
|
];
|
|
|
|
const search = new URL(document.location).searchParams;
|
|
|
|
const params = search.getAll('c');
|
|
const date = new Date(search.get('d'));
|
|
const view = search.get('v');
|
|
|
|
const headers = mobile
|
|
? {
|
|
left: 'title',
|
|
center: 'prev,today,next',
|
|
right: 'resourceTimelineDay dayGridMonth,timeGridWeek,timeGridDay,listWeek'
|
|
}
|
|
: {
|
|
left: 'prev,next today',
|
|
center: 'title',
|
|
right: 'resourceTimelineDay dayGridMonth,timeGridWeek,timeGridDay,listWeek'
|
|
};
|
|
|
|
let calendar;
|
|
|
|
let options = writable({
|
|
initialView: allowedViews.includes(view)
|
|
? view
|
|
: mobile
|
|
? 'listWeek'
|
|
: 'timeGridWeek',
|
|
initialDate: date.toString() === 'Invalid Date' ? now : date,
|
|
plugins: [
|
|
timeGridPlugin,
|
|
dayGridPlugin,
|
|
rrulePlugin,
|
|
listPlugin,
|
|
resourceTimelinePlugin,
|
|
adaptivePlugin,
|
|
bootstrap5Plugin
|
|
],
|
|
locale: frLocale,
|
|
allDayContent: '',
|
|
headerToolbar: headers,
|
|
buttonText: { resourceTimelineDay: 'Salles' },
|
|
scrollTime: '08:00:00',
|
|
resourceGroupField: 'building',
|
|
resourceAreaWidth: '27%',
|
|
resources: Object.entries(ENSLocations).flatMap(([building, rooms]) =>
|
|
rooms.map(room => ({
|
|
id: `${building}-${room}`,
|
|
building,
|
|
title: room
|
|
}))
|
|
),
|
|
height: '100%',
|
|
schedulerLicenseKey: 'CC-Attribution-NonCommercial-NoDerivatives',
|
|
nowIndicator: true,
|
|
now: now,
|
|
scrollTime: scrollTo,
|
|
scrollTimeReset: false,
|
|
eventClick: info => {
|
|
openModal = true;
|
|
event.set(info.event);
|
|
},
|
|
titleFormat: {
|
|
year: mobile ? '2-digit' : 'numeric',
|
|
month: mobile ? 'numeric' : 'long',
|
|
day: 'numeric'
|
|
},
|
|
eventSources: [],
|
|
themeSystem: 'bootstrap5',
|
|
nextDayThreshold: '05:00:00',
|
|
progressiveEventRendering: true,
|
|
expandRows: true,
|
|
eventDidMount: info => {
|
|
new Tooltip(info.el, {
|
|
title: info.event.extendedProps.short_name,
|
|
placement: 'top'
|
|
});
|
|
}
|
|
});
|
|
|
|
const flatten = d => {
|
|
let array = [];
|
|
|
|
if (!d) {
|
|
return [];
|
|
}
|
|
|
|
if (Array.isArray(d)) {
|
|
d.forEach(a => (array = array.concat(flatten(a))));
|
|
} else {
|
|
for (const [n, s] of Object.entries(d)) {
|
|
array = array.concat(n, flatten(s));
|
|
}
|
|
}
|
|
return array;
|
|
};
|
|
|
|
let selectedCalendars = [];
|
|
const initial =
|
|
params.length > 0
|
|
? flatten(params.map(cal => getSubCalendars(cal))).concat(params)
|
|
: initialCalendars;
|
|
|
|
const updateEvents = debounce(calendars => {
|
|
options.update(opts => ({
|
|
...opts,
|
|
eventSources: selectedCalendars.map(mkSource).filter(x => !!x)
|
|
}));
|
|
}, 300);
|
|
|
|
$: updateEvents(selectedCalendars);
|
|
</script>
|
|
|
|
<div class="h-100 d-flex flex-column">
|
|
<h1 class="mt-3 title text-center">Calendrier de la vie étudiante à l'ENS</h1>
|
|
|
|
<Help />
|
|
|
|
<FilterBar {calendarTree} bind:selected={selectedCalendars} {initial} />
|
|
|
|
<FullCalendar bind:this={calendar} options={$options} />
|
|
|
|
<EventModal event={$event} open={openModal} {toggle} />
|
|
</div>
|
|
|
|
<style>
|
|
:global(.fs-7) {
|
|
font-size: 0.9rem !important;
|
|
}
|
|
|
|
:global(.st-tentative) {
|
|
opacity: 40%;
|
|
}
|
|
:global(.st-cancelled) {
|
|
background: repeating-linear-gradient(45deg, #333, #333 10px, #950 10px, #950 20px);
|
|
}
|
|
|
|
:global(.modal-open) {
|
|
padding-right: 8px !important;
|
|
}
|
|
|
|
:global(.fc-event) {
|
|
cursor: pointer;
|
|
}
|
|
|
|
:global(.modal-body p:last-child, ul:last-child) {
|
|
margin-bottom: 0 !important;
|
|
}
|
|
|
|
:global(.fc-toolbar-chunk:not(:last-child)) {
|
|
margin-bottom: 0.25em;
|
|
}
|
|
|
|
@media (max-width: 765px) {
|
|
:global(.fc-header-toolbar) {
|
|
flex-direction: column;
|
|
}
|
|
:global(.fc-toolbar-title) {
|
|
margin-bottom: 0.25em !important;
|
|
}
|
|
:global(.fc-toolbar-chunk) {
|
|
display: flex;
|
|
justify-content: space-evenly;
|
|
width: 100%;
|
|
}
|
|
}
|
|
</style>
|