Compare commits

...

9 Commits

Author SHA1 Message Date
0eb7df61a8 Display event times 2022-10-22 01:08:14 -04:00
c89dad7c2c Add links to google calendar/iCal 2022-10-22 01:08:14 -04:00
473fe22fb1 Replicate the structure of event content in week view
Keeps the classes/formatting
2022-10-14 11:46:17 -04:00
3a221a0e93 Only periodically refresh in wall-display mode 2022-10-14 11:46:17 -04:00
730776620a Pre-define all resources, with shops as parents of tools 2022-10-14 11:46:17 -04:00
000cd56039 Only specify devServer options for first webpack config
This avoids `[webpack-dev-middleware] ConcurrentCompilationError`
issues when running `webpack serve`. However, it prevents running the
dev server for just the `wall-display` config (as it no longer has the
`/calendar` proxy route defined.
2022-10-13 01:17:40 -04:00
1709b1d6a6 Show day (timeline) and week view in non-wall-display view 2022-10-13 01:17:40 -04:00
5c47f97ba9 Split out iPad-specific wall-display configuration 2022-10-13 01:15:47 -04:00
7bb3bdcfdd Revert "Use luxon to set title format"
This reverts commit 2868fc3324.
2022-10-12 02:12:11 -04:00
8 changed files with 385 additions and 146 deletions

View File

@ -28,11 +28,13 @@
"@babel/runtime": "^7.19.0", "@babel/runtime": "^7.19.0",
"@fullcalendar/core": "^5.11.3", "@fullcalendar/core": "^5.11.3",
"@fullcalendar/icalendar": "^5.11.3", "@fullcalendar/icalendar": "^5.11.3",
"@fullcalendar/luxon2": "^5.11.3", "@fullcalendar/resource-common": "^5.11.3",
"@fullcalendar/resource-timegrid": "^5.11.3", "@fullcalendar/resource-timegrid": "^5.11.3",
"@fullcalendar/resource-timeline": "^5.11.3",
"@fullcalendar/timegrid": "^5.11.3",
"core-js": "^3.25.5", "core-js": "^3.25.5",
"intl": "^1.2.5", "intl": "^1.2.5",
"luxon": "^3.0.4", "preact": "^10.11.1",
"unique-colors": "^1.0.1" "unique-colors": "^1.0.1"
} }
} }

View File

@ -7,15 +7,17 @@ specifiers:
'@babel/runtime': ^7.19.0 '@babel/runtime': ^7.19.0
'@fullcalendar/core': ^5.11.3 '@fullcalendar/core': ^5.11.3
'@fullcalendar/icalendar': ^5.11.3 '@fullcalendar/icalendar': ^5.11.3
'@fullcalendar/luxon2': ^5.11.3 '@fullcalendar/resource-common': ^5.11.3
'@fullcalendar/resource-timegrid': ^5.11.3 '@fullcalendar/resource-timegrid': ^5.11.3
'@fullcalendar/resource-timeline': ^5.11.3
'@fullcalendar/timegrid': ^5.11.3
'@types/intl': ^1.2.0 '@types/intl': ^1.2.0
babel-loader: ^8.2.5 babel-loader: ^8.2.5
core-js: ^3.25.5 core-js: ^3.25.5
css-loader: ^6.7.1 css-loader: ^6.7.1
file-loader: ^6.2.0 file-loader: ^6.2.0
intl: ^1.2.5 intl: ^1.2.5
luxon: ^3.0.4 preact: ^10.11.1
style-loader: ^3.3.1 style-loader: ^3.3.1
ts-loader: ^9.4.1 ts-loader: ^9.4.1
typescript: ^4.8.4 typescript: ^4.8.4
@ -28,11 +30,13 @@ dependencies:
'@babel/runtime': 7.19.0 '@babel/runtime': 7.19.0
'@fullcalendar/core': 5.11.3 '@fullcalendar/core': 5.11.3
'@fullcalendar/icalendar': 5.11.3 '@fullcalendar/icalendar': 5.11.3
'@fullcalendar/luxon2': 5.11.3_luxon@3.0.4 '@fullcalendar/resource-common': 5.11.3
'@fullcalendar/resource-timegrid': 5.11.3 '@fullcalendar/resource-timegrid': 5.11.3
'@fullcalendar/resource-timeline': 5.11.3
'@fullcalendar/timegrid': 5.11.3
core-js: 3.25.5 core-js: 3.25.5
intl: 1.2.5 intl: 1.2.5
luxon: 3.0.4 preact: 10.11.1
unique-colors: 1.0.1 unique-colors: 1.0.1
devDependencies: devDependencies:
@ -1250,16 +1254,6 @@ packages:
tslib: 2.4.0 tslib: 2.4.0
dev: false dev: false
/@fullcalendar/luxon2/5.11.3_luxon@3.0.4:
resolution: {integrity: sha512-facQYF87ovrw1dOaEDG/oQevV4sgrTWSAsV43upIY8FLKkgVQYT3JR6/aG99CL9ceMmOxdBiwF5pQEsVuGmb4A==}
peerDependencies:
luxon: ^2.0.0
dependencies:
'@fullcalendar/common': 5.11.3
luxon: 3.0.4
tslib: 2.4.0
dev: false
/@fullcalendar/premium-common/5.11.3: /@fullcalendar/premium-common/5.11.3:
resolution: {integrity: sha512-fvMU8OmIReBXoY1iOkRO+zGwbUHA1YB9xtkYbSL3ZeMQ008P0Lj6ar7Jv/lB5XDRgh50TRfFIgfDjdszESAc4w==} resolution: {integrity: sha512-fvMU8OmIReBXoY1iOkRO+zGwbUHA1YB9xtkYbSL3ZeMQ008P0Lj6ar7Jv/lB5XDRgh50TRfFIgfDjdszESAc4w==}
dependencies: dependencies:
@ -1296,6 +1290,25 @@ packages:
tslib: 2.4.0 tslib: 2.4.0
dev: false dev: false
/@fullcalendar/resource-timeline/5.11.3:
resolution: {integrity: sha512-iYIXZPfqtiN/qizpGDCYlFVssdDTZv6lU/5N1v0FzvGMZfU2LkHLhCkouQeBQHja8ZCbJisy4sK3kUR9mXh2cg==}
dependencies:
'@fullcalendar/common': 5.11.3
'@fullcalendar/premium-common': 5.11.3
'@fullcalendar/resource-common': 5.11.3
'@fullcalendar/scrollgrid': 5.11.3
'@fullcalendar/timeline': 5.11.3
tslib: 2.4.0
dev: false
/@fullcalendar/scrollgrid/5.11.3:
resolution: {integrity: sha512-JTWDmejPmit65pCoQafUPeplI2+iogXG/3TNbusXMSWYaaMrINHDQiBZ/6EAt46hO2eWyEglmgS0BwVXZNSlGg==}
dependencies:
'@fullcalendar/common': 5.11.3
'@fullcalendar/premium-common': 5.11.3
tslib: 2.4.0
dev: false
/@fullcalendar/timegrid/5.11.3: /@fullcalendar/timegrid/5.11.3:
resolution: {integrity: sha512-SjIj2ZQ7nTyL1RxZkCPvNbuUQ0xHT+gfYJdUL3FT4bPjPJCxWtQ2CL8hxaeNmVozYYuy0yrGTW5Oup2+9IplbA==} resolution: {integrity: sha512-SjIj2ZQ7nTyL1RxZkCPvNbuUQ0xHT+gfYJdUL3FT4bPjPJCxWtQ2CL8hxaeNmVozYYuy0yrGTW5Oup2+9IplbA==}
dependencies: dependencies:
@ -1304,6 +1317,15 @@ packages:
tslib: 2.4.0 tslib: 2.4.0
dev: false dev: false
/@fullcalendar/timeline/5.11.3:
resolution: {integrity: sha512-nbMJ2gG9mLGUZgGUB2O726u3D8EFxJkZsKA/O1053j5QiQ7C8Wca1rh8UX+bed/s+wmkWwKV5pB0CiOax4f3gQ==}
dependencies:
'@fullcalendar/common': 5.11.3
'@fullcalendar/premium-common': 5.11.3
'@fullcalendar/scrollgrid': 5.11.3
tslib: 2.4.0
dev: false
/@jridgewell/gen-mapping/0.1.1: /@jridgewell/gen-mapping/0.1.1:
resolution: {integrity: sha512-sQXCasFk+U8lWYEe66WxRDOE9PjVz4vSM51fTu3Hw+ClTpUSQb718772vH3pyS5pShp6lvQM7SxgIDXXXmOX7w==} resolution: {integrity: sha512-sQXCasFk+U8lWYEe66WxRDOE9PjVz4vSM51fTu3Hw+ClTpUSQb718772vH3pyS5pShp6lvQM7SxgIDXXXmOX7w==}
engines: {node: '>=6.0.0'} engines: {node: '>=6.0.0'}
@ -2767,11 +2789,6 @@ packages:
yallist: 4.0.0 yallist: 4.0.0
dev: true dev: true
/luxon/3.0.4:
resolution: {integrity: sha512-aV48rGUwP/Vydn8HT+5cdr26YYQiUZ42NM6ToMoaGKwYfWbfLeRkEu1wXWMHBZT6+KyLfcbbtVcoQFCbbPjKlw==}
engines: {node: '>=12'}
dev: false
/make-dir/3.1.0: /make-dir/3.1.0:
resolution: {integrity: sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==} resolution: {integrity: sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==}
engines: {node: '>=8'} engines: {node: '>=8'}

141
src/common.ts Normal file
View File

@ -0,0 +1,141 @@
import { Calendar, CalendarOptions } from '@fullcalendar/core';
import iCalendarPlugin from '@fullcalendar/icalendar';
import resourceTimeGridPlugin from '@fullcalendar/resource-timegrid';
import { unique_colors } from 'unique-colors';
interface Shop {
calendar?: string;
children?: string[];
}
const shops: { [key: string]: Shop } = {
Cafe: {}, // same calendar as mezzanine
'Classroom / Computer Lab': {
calendar: '6mmjp85e4732ru6skf1dda54ls@group.calendar.google.com',
children: ['Printer, Canon iPF8400S 44" 8-Color/Pigment'],
},
'Digital Fabrication and Electronics Lab': {
calendar: '1g8atbdschshrg2inf162rcqt4@group.calendar.google.com',
children: [
'3d printer, Lulzbot Taz 6',
'3d printer, Lulzbot Mini',
'Laser Cutter, GLS Hybrid',
],
},
'Fiber Arts Studio': {
calendar: '7gbndciog37ge0hd8ug33ml70k@group.calendar.google.com',
children: ['Mid Arm, Brother DQLT15'],
},
'Jewelry Studio': {
calendar: 'l0dl2jq3vhbi9f4lfmaf5negc0@group.calendar.google.com',
},
'Metal Shop': {
calendar: 'a4p97kiiafatqdr52c3a0cpre0@group.calendar.google.com',
children: [
'Plasma Cutter, Hypertherm Powermax85',
'MIG Welder, Miller 210',
],
},
Mezzanine: {
calendar: 'f4ro53uklj2u6pr0se7ucskm6g@group.calendar.google.com',
},
'Wood Shop': {
calendar: '4unv3ia1n9mc9u31n2n5lv8nd8@group.calendar.google.com',
children: [
'ShopBot, PRSstandard 96-48-8, w/2.2hp Spindle',
'Table Saw, SawStop 3HP 10"',
'Planer, Powermatic 20"',
'Jointer, Powermatic 8"',
],
},
};
function extract_calendars(shops: { [key: string]: Shop }): string[] {
return Object.values(shops)
.map((shop) => shop.calendar)
.filter((calendar) => calendar !== undefined) as string[];
}
const calendars: string[] = extract_calendars(shops);
const colors: string[] = unique_colors(calendars.length);
export const common_calendarOptions: CalendarOptions = {
schedulerLicenseKey: 'CC-Attribution-NonCommercial-NoDerivatives',
plugins: [iCalendarPlugin, resourceTimeGridPlugin],
allDaySlot: false,
nowIndicator: true,
headerToolbar: { start: '', center: 'title', end: '' },
initialView: 'resourceTimeGrid',
height: 'auto',
slotMinTime: '8:00',
slotMaxTime: '22:00',
displayEventTime: true,
businessHours: {
daysOfWeek: [0, 1, 2, 3, 4, 5, 6],
startTime: '10:00',
endTime: '21:00',
},
slotLabelFormat: {
hour: 'numeric',
minute: '2-digit',
hour12: false,
},
eventTimeFormat: {
hour: 'numeric',
minute: '2-digit',
hour12: false,
},
resources: Object.entries(shops).map(([shop_name, shop]) => {
return {
id: shop_name,
title: shop_name,
extendedProps: {
calendar: shop.calendar,
},
children: shop.children?.map((tool) => {
return {
id: tool,
title: tool,
};
}),
};
}),
};
export function main(
calendarOptions: CalendarOptions,
allTools: boolean = false
) {
const calendarEl = document.getElementById('calendar');
const calendar = new Calendar(calendarEl!, calendarOptions);
calendars.forEach((id, idx) =>
calendar.addEventSource({
url: '/calendar/ical/' + id + '/public/basic.ics',
format: 'ics',
color: colors[idx],
eventDataTransform: (eventData) => {
// clear the url to prevent clicking on the event
delete eventData.url;
const match = eventData?.title?.match(/([^\/]*) \| ([^-]*) - (.*)/);
if (match) {
const [, member, shop, tool] = match;
eventData.title = `${member}`;
eventData.resourceId = tool;
if (allTools) {
if (!calendar.getResourceById(tool)) {
calendar.addResource({ id: tool, title: tool }, false);
}
}
}
return eventData;
},
})
);
calendar.render();
return calendar;
}

View File

@ -1,109 +1,73 @@
import 'core-js/stable/url'; import '@fullcalendar/core';
import 'core-js/stable/function'; import { CalendarOptions, EventContentArg } from '@fullcalendar/core';
import { ResourceLabelContentArg } from '@fullcalendar/resource-common';
import timeGridPlugin from '@fullcalendar/timegrid';
import resourceTimelinePlugin from '@fullcalendar/resource-timeline';
import { createElement } from 'preact';
import Intl from 'intl'; import { common_calendarOptions, main } from './common';
import 'intl/locale-data/jsonp/en.js';
window.Intl = Intl;
import { Calendar, CalendarOptions } from '@fullcalendar/core';
import iCalendarPlugin from '@fullcalendar/icalendar';
import luxon2Plugin from '@fullcalendar/luxon2';
import resourceTimeGridPlugin from '@fullcalendar/resource-timegrid';
import { unique_colors } from 'unique-colors';
import './index.html'; import './index.html';
import './index.css';
const calendars: { [key: string]: string } = {
computer_lab: '6mmjp85e4732ru6skf1dda54ls@group.calendar.google.com',
electronics: '1g8atbdschshrg2inf162rcqt4@group.calendar.google.com',
wood_shop: '4unv3ia1n9mc9u31n2n5lv8nd8@group.calendar.google.com',
fiber_arts_studio: '7gbndciog37ge0hd8ug33ml70k@group.calendar.google.com',
jewelry_studio: 'l0dl2jq3vhbi9f4lfmaf5negc0@group.calendar.google.com',
metal_shop: 'a4p97kiiafatqdr52c3a0cpre0@group.calendar.google.com',
room_reservations: 'f4ro53uklj2u6pr0se7ucskm6g@group.calendar.google.com',
};
const colors: string[] = unique_colors(Object.keys(calendars).length);
const urlParams = new URLSearchParams(window.location.search);
const toolFilter: string[] | undefined = urlParams.get('tool')?.split(';');
const calendarOptions: CalendarOptions = { const calendarOptions: CalendarOptions = {
schedulerLicenseKey: 'CC-Attribution-NonCommercial-NoDerivatives', ...common_calendarOptions,
plugins: [iCalendarPlugin, luxon2Plugin, resourceTimeGridPlugin], plugins: [
allDaySlot: false, ...(common_calendarOptions.plugins ?? []),
nowIndicator: true, timeGridPlugin,
headerToolbar: { start: '', center: 'title', end: '' }, resourceTimelinePlugin,
titleFormat: "'Reservations for ' cccc LLLL d, yyyy", ],
initialView: 'resourceTimeGrid', headerToolbar: {
height: 'auto', start: 'resourceTimeline,timeGridWeek',
slotMinTime: '8:00', center: 'title',
slotMaxTime: '22:00', end: 'prev,next today',
businessHours: {
daysOfWeek: [0, 1, 2, 3, 4, 5, 6],
startTime: '10:00',
endTime: '21:00',
}, },
slotLabelFormat: { buttonText: {
hour: 'numeric', resourceTimeline: 'day',
minute: '2-digit',
hour12: false,
}, },
eventTimeFormat: { initialView: 'resourceTimeline',
hour: 'numeric', resourceLabelContent: (
minute: '2-digit', arg: ResourceLabelContentArg,
hour12: false, h: typeof createElement
}, ) => {
resources: toolFilter const calendar = arg.resource.extendedProps.calendar;
? toolFilter.map((tool) => { if (calendar) {
return { const embed_link = `https://calendar.google.com/calendar/embed?ctz=America%2FNew_York&src=${calendar}`;
id: tool, const ical_link = `https://calendar.google.com/calendar/ical/${calendar}/public/basic.ics`;
title: tool, return h(
}; 'span',
}) null,
: [], h('a', { href: embed_link }, arg.resource.title),
}; ' ',
h('a', { href: ical_link }, ' [iCal 📅]')
function main() {
const calendarEl = document.getElementById('calendar');
const calendar = new Calendar(calendarEl!, calendarOptions);
Object.values(calendars).forEach((id, idx) =>
calendar.addEventSource({
url: '/calendar/ical/' + id + '/public/basic.ics',
format: 'ics',
color: colors[idx],
eventDataTransform: (eventData) => {
// clear the url to prevent clicking on the event
delete eventData.url;
const match = eventData?.title?.match(/([^\/]*) \| ([^-]*) - (.*)/);
if (match) {
const [, member, shop, tool] = match;
eventData.title = `${member}`;
eventData.resourceId = tool;
if (!toolFilter) {
calendar.addResource({ id: tool, title: tool }, false);
}
}
return eventData;
},
})
); );
calendar.render();
function refresh() {
calendar.refetchEvents();
calendar.today();
} }
},
// refresh data every five minutes eventContent: (arg: EventContentArg, h: typeof createElement) => {
window.setInterval(refresh, 5 * 60 * 1000); if (arg.view.type != 'resourceTimeline') {
let resources = arg.event
.getResources()
.map((resource) => resource.id.trim())
.join('; ');
return h(
'div',
{ class: 'fc-event-main' },
h(
'div',
{ class: 'fc-event-main-frame' },
h('div', { class: 'fc-event-time' }, arg.timeText),
h(
'div',
{ class: 'fc-event-title-container' },
h(
'div',
{ class: 'fc-event-title fc-sticky' },
`${resources}: ${arg.event.title}`
)
)
)
);
} }
},
};
document.body.addEventListener('touchmove', (e) => e.preventDefault(), { main(calendarOptions, true);
passive: false,
});
main();

28
src/ios-fixes.css Normal file
View File

@ -0,0 +1,28 @@
.fc .fc-toolbar.fc-header-toolbar {
/* Save some vertical space */
margin-bottom: 0;
/* iOS 8 Safari doesn't support flexbox */
text-align: center;
}
/* Fix rendering of now indicators on iOS 8 Safari */
.fc .fc-timegrid-now-indicator-container {
overflow: initial;
}
.fc .fc-timegrid-now-indicator-line {
border-color: rgba(255, 0, 0, 0.4);
border-top-width: 1px;
border-top-color: red;
border-bottom-width: 9px;
}
.fc-direction-ltr .fc-timegrid-now-indicator-arrow {
border-width: 10px 0 10px 11px;
margin-top: -10px;
}
.fc .fc-timegrid-slots {
/* Fix timegrid lines rendering over events on iOS 5.1.1 */
z-index: 0 !important;
}

13
src/wall-display.html Normal file
View File

@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="viewport" content="initial-scale=1, user-scalable=no" />
<title>Tool Reservations</title>
</head>
<body>
<div id="calendar"></div>
<script src="bundle-wall-display.js"></script>
</body>
</html>

42
src/wall-display.ts Normal file
View File

@ -0,0 +1,42 @@
import 'core-js/stable/url';
import 'core-js/stable/function';
import Intl from 'intl';
import 'intl/locale-data/jsonp/en.js';
window.Intl = Intl;
import { CalendarOptions } from '@fullcalendar/core';
import { common_calendarOptions, main } from './common';
import './wall-display.html';
import './ios-fixes.css';
document.body.addEventListener('touchmove', (e) => e.preventDefault(), {
passive: false,
});
const urlParams = new URLSearchParams(window.location.search);
const toolFilter: string[] | undefined = urlParams.get('tool')?.split(';');
const calendarOptions: CalendarOptions = {
...common_calendarOptions,
resources: toolFilter
? toolFilter.map((tool) => {
return {
id: tool,
title: tool,
};
})
: [],
};
const calendar = main(calendarOptions, !toolFilter);
function refresh() {
calendar.refetchEvents();
calendar.today();
}
// refresh data every five minutes
window.setInterval(refresh, 5 * 60 * 1000);

View File

@ -1,9 +1,37 @@
const path = require('path'); const path = require('path');
module.exports = { const common = {
mode: 'development', mode: 'development',
module: {
rules: [
{
test: /\.css$/i,
use: ['style-loader', 'css-loader'],
},
{
test: /\.html$/i,
use: {
loader: 'file-loader',
options: {
name: '[name].[ext]',
},
},
},
],
},
devtool: 'source-map',
resolve: {
extensions: ['.tsx', '.ts', '.js'],
},
};
module.exports = [
{
...common,
name: 'default',
entry: './src/index.ts', entry: './src/index.ts',
devServer: { devServer: {
allowedHosts: 'all',
proxy: { proxy: {
'/calendar': { '/calendar': {
target: 'https://calendar.google.com', target: 'https://calendar.google.com',
@ -11,7 +39,25 @@ module.exports = {
}, },
}, },
}, },
devtool: 'source-map', module: {
rules: [
{
test: /\.tsx?$/,
exclude: /node_modules/,
use: ['ts-loader'],
},
...common.module.rules,
],
},
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'bundle.js',
},
},
{
...common,
name: 'wall-display',
entry: './src/wall-display.ts',
module: { module: {
rules: [ rules: [
{ {
@ -24,26 +70,12 @@ module.exports = {
exclude: /node_modules/, exclude: /node_modules/,
use: ['babel-loader', 'ts-loader'], use: ['babel-loader', 'ts-loader'],
}, },
{ ...common.module.rules,
test: /\.css$/i,
use: ['style-loader', 'css-loader'],
},
{
test: /\/index.html$/i,
use: {
loader: 'file-loader',
options: {
name: '[name].[ext]',
},
},
},
], ],
}, },
resolve: {
extensions: ['.tsx', '.ts', '.js'],
},
output: { output: {
path: path.resolve(__dirname, 'dist'), path: path.resolve(__dirname, 'dist'),
filename: 'bundle.js', filename: 'bundle-wall-display.js',
}, },
}; },
];