Update to fullcalendar 6, render image on server side for old iPads
Using puppeteer to render the page server side, as FullCalendar 6 dropped support for ES5, and polyfilling everything was becoming basically impossible
This commit is contained in:
parent
59f9ab99ac
commit
98065af7d5
6
.gitignore
vendored
6
.gitignore
vendored
@ -1,4 +1,6 @@
|
|||||||
/node_modules/
|
node_modules/
|
||||||
/.cache/
|
.cache/
|
||||||
/dist/
|
/dist/
|
||||||
/.log/
|
/.log/
|
||||||
|
server/src/*.js
|
||||||
|
server/src/*.js.map
|
||||||
|
49
package.json
49
package.json
@ -5,35 +5,34 @@
|
|||||||
"ios 5.1"
|
"ios 5.1"
|
||||||
],
|
],
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "npm run serve",
|
"start": "pnpm run --parallel --recursive --include-workspace-root dev",
|
||||||
"build": "webpack build",
|
"build": "webpack build --mode=production",
|
||||||
"serve": "webpack serve"
|
"dev": "webpack serve"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "^7.20.5",
|
"@babel/core": "^7.22.11",
|
||||||
"@babel/plugin-transform-runtime": "^7.19.6",
|
"@babel/plugin-transform-runtime": "^7.22.10",
|
||||||
"@babel/preset-env": "^7.20.2",
|
"@babel/preset-env": "^7.22.10",
|
||||||
"@types/intl": "^1.2.0",
|
"babel-loader": "^9.1.3",
|
||||||
"babel-loader": "^9.1.0",
|
"css-loader": "^6.8.1",
|
||||||
"css-loader": "^6.7.3",
|
"style-loader": "^3.3.3",
|
||||||
"style-loader": "^3.3.1",
|
"ts-loader": "^9.4.4",
|
||||||
"ts-loader": "^9.4.2",
|
"typescript": "^5.2.2",
|
||||||
"typescript": "^4.9.4",
|
"webpack": "^5.88.2",
|
||||||
"webpack": "^5.75.0",
|
"webpack-cli": "^5.1.4",
|
||||||
"webpack-cli": "^5.0.1",
|
"webpack-dev-server": "^4.15.1"
|
||||||
"webpack-dev-server": "^4.11.1"
|
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": "^7.20.6",
|
"@babel/runtime": "^7.22.11",
|
||||||
"@fullcalendar/core": "^5.11.3",
|
"@fullcalendar/core": "^6.1.8",
|
||||||
"@fullcalendar/icalendar": "^5.11.3",
|
"@fullcalendar/icalendar": "^6.1.8",
|
||||||
"@fullcalendar/resource-common": "^5.11.3",
|
"@fullcalendar/resource": "^6.1.8",
|
||||||
"@fullcalendar/resource-timegrid": "^5.11.3",
|
"@fullcalendar/resource-timegrid": "^6.1.8",
|
||||||
"@fullcalendar/resource-timeline": "^5.11.3",
|
"@fullcalendar/resource-timeline": "^6.1.8",
|
||||||
"@fullcalendar/timegrid": "^5.11.3",
|
"@fullcalendar/timegrid": "^6.1.8",
|
||||||
"core-js": "^3.26.1",
|
"core-js": "^3.32.1",
|
||||||
"intl": "^1.2.5",
|
"ical.js": "^1.5.0",
|
||||||
"preact": "^10.11.3",
|
"preact": "^10.17.1",
|
||||||
"unique-colors": "^1.0.1"
|
"unique-colors": "^1.0.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
3492
pnpm-lock.yaml
3492
pnpm-lock.yaml
File diff suppressed because it is too large
Load Diff
20
server/package.json
Normal file
20
server/package.json
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"type": "module",
|
||||||
|
"exports": "./server.js",
|
||||||
|
"dependencies": {
|
||||||
|
"express": "^4.18.2",
|
||||||
|
"puppeteer": "^21.1.1"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"start": "npm run dev",
|
||||||
|
"dev": "tsx watch --clear-screen=false ./src/server.ts",
|
||||||
|
"build": "tsc",
|
||||||
|
"serve": "node dist/server.js"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/express": "^4.17.17",
|
||||||
|
"@types/node": "^20.5.7",
|
||||||
|
"tsx": "^3.12.7",
|
||||||
|
"typescript": "^5.2.2"
|
||||||
|
}
|
||||||
|
}
|
1543
server/pnpm-lock.yaml
Normal file
1543
server/pnpm-lock.yaml
Normal file
File diff suppressed because it is too large
Load Diff
58
server/src/server.ts
Normal file
58
server/src/server.ts
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
import express from 'express';
|
||||||
|
import puppeteer from 'puppeteer';
|
||||||
|
|
||||||
|
const PORT = process.env.PORT || 1234;
|
||||||
|
|
||||||
|
const app = express();
|
||||||
|
const puppet_browser = await puppeteer.launch({ headless: 'new' });
|
||||||
|
|
||||||
|
app.get('/ipad.png', async (req, res) => {
|
||||||
|
const puppet_page = await puppet_browser.newPage();
|
||||||
|
if (typeof req.query.viewport == 'string') {
|
||||||
|
let [width, height, pixelRatio] = req.query.viewport.split('x', 3);
|
||||||
|
puppet_page.setViewport({
|
||||||
|
deviceScaleFactor: parseInt(pixelRatio),
|
||||||
|
width: parseInt(width),
|
||||||
|
height: parseInt(height),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let frontend_url = new URL('/wall-display.html', 'http://' + req.get('host'));
|
||||||
|
console.debug(frontend_url);
|
||||||
|
if (typeof req.query.tool == 'string') {
|
||||||
|
frontend_url.searchParams.set('tool', req.query.tool);
|
||||||
|
}
|
||||||
|
await puppet_page.goto(frontend_url.toString());
|
||||||
|
// TODO: handle timeout
|
||||||
|
await puppet_page.waitForNetworkIdle({ timeout: 5000 });
|
||||||
|
const screenshot = await puppet_page.screenshot();
|
||||||
|
await puppet_page.close();
|
||||||
|
res.send(screenshot);
|
||||||
|
});
|
||||||
|
|
||||||
|
const server = app.listen(PORT, () => {
|
||||||
|
console.log(`Listening on http://localhost:${PORT}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
// graceful shutdown
|
||||||
|
process.on('SIGTERM', () => {
|
||||||
|
console.debug('SIGTERM signal received: shutting down');
|
||||||
|
Promise.all([
|
||||||
|
new Promise((resolve, reject) =>
|
||||||
|
server.close((err) => {
|
||||||
|
if (err) {
|
||||||
|
return reject(err);
|
||||||
|
}
|
||||||
|
return resolve(null);
|
||||||
|
})
|
||||||
|
).then(() => console.debug('HTTP server closed')),
|
||||||
|
puppet_browser
|
||||||
|
.close()
|
||||||
|
.then(() => console.debug('Puppeteer puppet_browser instance closed')),
|
||||||
|
])
|
||||||
|
.then(() => process.exit(0))
|
||||||
|
.catch((err) => {
|
||||||
|
console.error(err);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
|
});
|
15
server/tsconfig.json
Normal file
15
server/tsconfig.json
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "esnext",
|
||||||
|
"useDefineForClassFields": true,
|
||||||
|
"module": "ES2022",
|
||||||
|
"moduleResolution": "node",
|
||||||
|
"strict": true,
|
||||||
|
"jsx": "preserve",
|
||||||
|
"sourceMap": true,
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"lib": ["esnext", "dom"]
|
||||||
|
},
|
||||||
|
"include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"]
|
||||||
|
}
|
@ -139,6 +139,7 @@ export function main(
|
|||||||
);
|
);
|
||||||
|
|
||||||
calendar.render();
|
calendar.render();
|
||||||
|
//calendar.gotoDate('2022-06-27');
|
||||||
|
|
||||||
return calendar;
|
return calendar;
|
||||||
}
|
}
|
||||||
|
@ -8,6 +8,6 @@
|
|||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="calendar"></div>
|
<div id="calendar"></div>
|
||||||
<script src="bundle.js"></script>
|
<script src="bundle-index.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
@ -1,9 +1,8 @@
|
|||||||
import '@fullcalendar/core';
|
|
||||||
import type { CalendarOptions, EventContentArg } from '@fullcalendar/core';
|
import type { CalendarOptions, EventContentArg } from '@fullcalendar/core';
|
||||||
import type { ResourceLabelContentArg } from '@fullcalendar/resource-common';
|
import type { ResourceLabelContentArg } from '@fullcalendar/resource';
|
||||||
import timeGridPlugin from '@fullcalendar/timegrid';
|
import timeGridPlugin from '@fullcalendar/timegrid';
|
||||||
import resourceTimelinePlugin from '@fullcalendar/resource-timeline';
|
import resourceTimelinePlugin from '@fullcalendar/resource-timeline';
|
||||||
import type { createElement } from 'preact';
|
import type { createElement } from '@fullcalendar/core/preact';
|
||||||
|
|
||||||
import { common_calendarOptions, main } from './common';
|
import { common_calendarOptions, main } from './common';
|
||||||
|
|
||||||
|
1
src/intl.d.ts
vendored
1
src/intl.d.ts
vendored
@ -1 +0,0 @@
|
|||||||
declare module 'intl/locale-data/jsonp/en.js';
|
|
@ -1,28 +0,0 @@
|
|||||||
.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;
|
|
||||||
}
|
|
21
src/ipad.html
Normal file
21
src/ipad.html
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
<!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 style="margin: 0;">
|
||||||
|
<div id="errorContainer"></div>
|
||||||
|
<div class="modal" id="loadingSpinner"></div>
|
||||||
|
<div class="modal" id="errorModal" style="display: none;">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-icon">⚠</div>
|
||||||
|
<div class="modal-message"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<img id="calendar"></img>
|
||||||
|
<script src="bundle-ipad.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
61
src/ipad.ts
Normal file
61
src/ipad.ts
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
import 'core-js/stable/url';
|
||||||
|
|
||||||
|
import './ipad.html';
|
||||||
|
import './modal.css';
|
||||||
|
|
||||||
|
console.log = function (message) {
|
||||||
|
let div = document.createElement('div');
|
||||||
|
div.textContent = message;
|
||||||
|
document.getElementById('errorContainer')!.appendChild(div);
|
||||||
|
};
|
||||||
|
console.error = console.log;
|
||||||
|
console.info = console.log;
|
||||||
|
|
||||||
|
window.onerror = function (message, url, line) {
|
||||||
|
console.log(`${message} ${url} ${line}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
document.body.addEventListener('touchmove', (e) => e.preventDefault(), {
|
||||||
|
passive: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const calendarImg = document.getElementById('calendar')! as HTMLImageElement;
|
||||||
|
const spinner = document.getElementById('loadingSpinner')!;
|
||||||
|
const errorModal = document.getElementById('errorModal')!;
|
||||||
|
|
||||||
|
calendarImg.addEventListener('load', () => {
|
||||||
|
spinner.style.display = 'none';
|
||||||
|
errorModal.style.display = 'none';
|
||||||
|
});
|
||||||
|
|
||||||
|
calendarImg.addEventListener('error', (event) => {
|
||||||
|
spinner.style.display = 'none';
|
||||||
|
console.error('Error fetching calendar image: ', event);
|
||||||
|
errorModal.style.display = '';
|
||||||
|
const message = errorModal.querySelector('.modal-message')!;
|
||||||
|
message.textContent = `Failed to fetch events: ${event.message}. Displayed events may not be accurate.`;
|
||||||
|
});
|
||||||
|
|
||||||
|
function refresh() {
|
||||||
|
spinner.style.display = '';
|
||||||
|
|
||||||
|
const url = new URL('/ipad.png', window.location.href);
|
||||||
|
url.searchParams.set('viewport', `${window.innerWidth}x${window.innerHeight}x${window.devicePixelRatio}`);
|
||||||
|
|
||||||
|
const urlParams = new URLSearchParams(window.location.search);
|
||||||
|
if (urlParams.has('tool')) {
|
||||||
|
url.searchParams.set('tool', urlParams.get('tool')!);
|
||||||
|
}
|
||||||
|
|
||||||
|
calendarImg.src = url.toString();
|
||||||
|
calendarImg.width = window.innerWidth;
|
||||||
|
calendarImg.height = window.innerHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
window.addEventListener('load', () => refresh());
|
||||||
|
|
||||||
|
// refresh data every five minutes
|
||||||
|
window.setInterval(refresh, 5 * 60 * 1000);
|
||||||
|
|
||||||
|
// refresh page every hour
|
||||||
|
window.setInterval(() => window.location.reload(), 60 * 60 * 1000);
|
11
src/wall-display.css
Normal file
11
src/wall-display.css
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
.fc .fc-toolbar.fc-header-toolbar {
|
||||||
|
/* Save some vertical space */
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.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;
|
||||||
|
}
|
@ -7,6 +7,7 @@
|
|||||||
<title>Tool Reservations</title>
|
<title>Tool Reservations</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
<div id="errorContainer"></div>
|
||||||
<div class="modal" id="loadingSpinner"></div>
|
<div class="modal" id="loadingSpinner"></div>
|
||||||
<div class="modal" id="errorModal">
|
<div class="modal" id="errorModal">
|
||||||
<div class="modal-content">
|
<div class="modal-content">
|
||||||
|
@ -1,17 +1,22 @@
|
|||||||
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 type { CalendarOptions } from '@fullcalendar/core';
|
import type { CalendarOptions } from '@fullcalendar/core';
|
||||||
|
|
||||||
import { common_calendarOptions, main } from './common';
|
import { common_calendarOptions, main } from './common';
|
||||||
|
|
||||||
import './wall-display.html';
|
import './wall-display.html';
|
||||||
import './modal.css';
|
import './modal.css';
|
||||||
import './ios-fixes.css';
|
import './wall-display.css';
|
||||||
|
|
||||||
|
console.log = function (message) {
|
||||||
|
let div = document.createElement('div');
|
||||||
|
div.textContent = message;
|
||||||
|
document.getElementById('errorContainer')!.appendChild(div);
|
||||||
|
};
|
||||||
|
console.error = console.log;
|
||||||
|
console.info = console.log;
|
||||||
|
|
||||||
|
window.onerror = function (message, url, line) {
|
||||||
|
console.log(`${message} ${url} ${line}`);
|
||||||
|
};
|
||||||
|
|
||||||
document.body.addEventListener('touchmove', (e) => e.preventDefault(), {
|
document.body.addEventListener('touchmove', (e) => e.preventDefault(), {
|
||||||
passive: false,
|
passive: false,
|
||||||
|
@ -31,7 +31,10 @@ module.exports = [
|
|||||||
{
|
{
|
||||||
...common,
|
...common,
|
||||||
name: 'default',
|
name: 'default',
|
||||||
entry: './src/index.ts',
|
entry: {
|
||||||
|
index: './src/index.ts',
|
||||||
|
'wall-display': './src/wall-display.ts',
|
||||||
|
},
|
||||||
devServer: {
|
devServer: {
|
||||||
allowedHosts: 'all',
|
allowedHosts: 'all',
|
||||||
proxy: {
|
proxy: {
|
||||||
@ -39,6 +42,9 @@ module.exports = [
|
|||||||
target: 'https://calendar.google.com',
|
target: 'https://calendar.google.com',
|
||||||
changeOrigin: true,
|
changeOrigin: true,
|
||||||
},
|
},
|
||||||
|
'/ipad.png': {
|
||||||
|
target: 'http://localhost:1234',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
module: {
|
module: {
|
||||||
@ -53,18 +59,18 @@ module.exports = [
|
|||||||
},
|
},
|
||||||
output: {
|
output: {
|
||||||
path: path.resolve(__dirname, 'dist'),
|
path: path.resolve(__dirname, 'dist'),
|
||||||
filename: 'bundle.js',
|
filename: 'bundle-[name].js',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
...common,
|
...common,
|
||||||
name: 'wall-display',
|
name: 'ipad',
|
||||||
entry: './src/wall-display.ts',
|
entry: ['./src/ipad.ts'],
|
||||||
module: {
|
module: {
|
||||||
rules: [
|
rules: [
|
||||||
{
|
{
|
||||||
test: /\.m?js$/,
|
test: /\.m?js$/,
|
||||||
exclude: /node_modules/,
|
include: /node_modules\/@fullcalendar/,
|
||||||
loader: 'babel-loader',
|
loader: 'babel-loader',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -77,7 +83,7 @@ module.exports = [
|
|||||||
},
|
},
|
||||||
output: {
|
output: {
|
||||||
path: path.resolve(__dirname, 'dist'),
|
path: path.resolve(__dirname, 'dist'),
|
||||||
filename: 'bundle-wall-display.js',
|
filename: 'bundle-ipad.js',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
Loading…
Reference in New Issue
Block a user