Compare commits

...

9 Commits

Author SHA1 Message Date
880c320771 Fix some calendar and tool name/url issues 2022-12-10 11:25:49 -05:00
eed4b2a754 Display event times 2022-12-10 11:25:49 -05:00
8dcd89e342 Add links to google calendar/iCal 2022-12-10 11:25:49 -05:00
54a38cf12f Replicate the structure of event content in week view
Keeps the classes/formatting
2022-12-10 11:25:49 -05:00
28edb6027c Only periodically refresh in wall-display mode 2022-12-10 11:25:49 -05:00
1a2d20645f Pre-define all resources, with shops as parents of tools 2022-12-10 11:25:49 -05:00
652f78944a 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-12-10 11:25:49 -05:00
94e59b83bf Show day (timeline) and week view in non-wall-display view 2022-12-10 11:25:49 -05:00
ed1392ead4 Split out iPad-specific wall-display configuration 2022-12-10 11:25:49 -05:00
8 changed files with 360 additions and 123 deletions

View File

@ -28,9 +28,13 @@
"@babel/runtime": "^7.19.0",
"@fullcalendar/core": "^5.11.3",
"@fullcalendar/icalendar": "^5.11.3",
"@fullcalendar/resource-common": "^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",
"intl": "^1.2.5",
"preact": "^10.11.1",
"unique-colors": "^1.0.1"
}
}

View File

@ -7,13 +7,17 @@ specifiers:
'@babel/runtime': ^7.19.0
'@fullcalendar/core': ^5.11.3
'@fullcalendar/icalendar': ^5.11.3
'@fullcalendar/resource-common': ^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
babel-loader: ^8.2.5
core-js: ^3.25.5
css-loader: ^6.7.1
file-loader: ^6.2.0
intl: ^1.2.5
preact: ^10.11.1
style-loader: ^3.3.1
ts-loader: ^9.4.1
typescript: ^4.8.4
@ -26,9 +30,13 @@ dependencies:
'@babel/runtime': 7.19.0
'@fullcalendar/core': 5.11.3
'@fullcalendar/icalendar': 5.11.3
'@fullcalendar/resource-common': 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
intl: 1.2.5
preact: 10.11.1
unique-colors: 1.0.1
devDependencies:
@ -1282,6 +1290,25 @@ packages:
tslib: 2.4.0
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:
resolution: {integrity: sha512-SjIj2ZQ7nTyL1RxZkCPvNbuUQ0xHT+gfYJdUL3FT4bPjPJCxWtQ2CL8hxaeNmVozYYuy0yrGTW5Oup2+9IplbA==}
dependencies:
@ -1290,6 +1317,15 @@ packages:
tslib: 2.4.0
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:
resolution: {integrity: sha512-sQXCasFk+U8lWYEe66WxRDOE9PjVz4vSM51fTu3Hw+ClTpUSQb718772vH3pyS5pShp6lvQM7SxgIDXXXmOX7w==}
engines: {node: '>=6.0.0'}

144
src/common.ts Normal file
View File

@ -0,0 +1,144 @@
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: {
calendar: 'ofbkp5ctd1hr7917sknlj9s15g@group.calendar.google.com',
},
'Conference Room': {}, // 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',
'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,107 +1,73 @@
import 'core-js/stable/url';
import 'core-js/stable/function';
import '@fullcalendar/core';
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 'intl/locale-data/jsonp/en.js';
window.Intl = Intl;
import { Calendar, CalendarOptions } from '@fullcalendar/core';
import iCalendarPlugin from '@fullcalendar/icalendar';
import resourceTimeGridPlugin from '@fullcalendar/resource-timegrid';
import { unique_colors } from 'unique-colors';
import { common_calendarOptions, main } from './common';
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 = {
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',
businessHours: {
daysOfWeek: [0, 1, 2, 3, 4, 5, 6],
startTime: '10:00',
endTime: '21:00',
...common_calendarOptions,
plugins: [
...(common_calendarOptions.plugins ?? []),
timeGridPlugin,
resourceTimelinePlugin,
],
headerToolbar: {
start: 'resourceTimeline,timeGridWeek',
center: 'title',
end: 'prev,next today',
},
slotLabelFormat: {
hour: 'numeric',
minute: '2-digit',
hour12: false,
buttonText: {
resourceTimeline: 'day',
},
eventTimeFormat: {
hour: 'numeric',
minute: '2-digit',
hour12: false,
initialView: 'resourceTimeline',
resourceLabelContent: (
arg: ResourceLabelContentArg,
h: typeof createElement
) => {
const calendar = arg.resource.extendedProps.calendar;
if (calendar) {
const embed_link = `https://calendar.google.com/calendar/embed?ctz=America%2FNew_York&src=${calendar}`;
const ical_link = `https://calendar.google.com/calendar/ical/${calendar}/public/basic.ics`;
return h(
'span',
null,
h('a', { href: embed_link }, arg.resource.title),
' ',
h('a', { href: ical_link }, ' [iCal 📅]')
);
}
},
eventContent: (arg: EventContentArg, h: typeof createElement) => {
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}`
)
)
)
);
}
},
resources: toolFilter
? toolFilter.map((tool) => {
return {
id: tool,
title: tool,
};
})
: [],
};
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
window.setInterval(refresh, 5 * 60 * 1000);
}
document.body.addEventListener('touchmove', (e) => e.preventDefault(), {
passive: false,
});
main();
main(calendarOptions, true);

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,35 +1,15 @@
const path = require('path');
module.exports = {
const common = {
mode: 'development',
entry: './src/index.ts',
devServer: {
proxy: {
'/calendar': {
target: 'https://calendar.google.com',
changeOrigin: true,
},
},
},
devtool: 'source-map',
module: {
rules: [
{
test: /\.m?js$/,
exclude: /node_modules/,
loader: 'babel-loader',
},
{
test: /\.tsx?$/,
exclude: /node_modules/,
use: ['babel-loader', 'ts-loader'],
},
{
test: /\.css$/i,
use: ['style-loader', 'css-loader'],
},
{
test: /\/index.html$/i,
test: /\.html$/i,
use: {
loader: 'file-loader',
options: {
@ -39,11 +19,63 @@ module.exports = {
},
],
},
devtool: 'source-map',
resolve: {
extensions: ['.tsx', '.ts', '.js'],
},
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'bundle.js',
},
};
module.exports = [
{
...common,
name: 'default',
entry: './src/index.ts',
devServer: {
allowedHosts: 'all',
proxy: {
'/calendar': {
target: 'https://calendar.google.com',
changeOrigin: true,
},
},
},
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: {
rules: [
{
test: /\.m?js$/,
exclude: /node_modules/,
loader: 'babel-loader',
},
{
test: /\.tsx?$/,
exclude: /node_modules/,
use: ['babel-loader', 'ts-loader'],
},
...common.module.rules,
],
},
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'bundle-wall-display.js',
},
},
];