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/
|
||||
/.cache/
|
||||
node_modules/
|
||||
.cache/
|
||||
/dist/
|
||||
/.log/
|
||||
server/src/*.js
|
||||
server/src/*.js.map
|
||||
|
49
package.json
49
package.json
@ -5,35 +5,34 @@
|
||||
"ios 5.1"
|
||||
],
|
||||
"scripts": {
|
||||
"start": "npm run serve",
|
||||
"build": "webpack build",
|
||||
"serve": "webpack serve"
|
||||
"start": "pnpm run --parallel --recursive --include-workspace-root dev",
|
||||
"build": "webpack build --mode=production",
|
||||
"dev": "webpack serve"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.20.5",
|
||||
"@babel/plugin-transform-runtime": "^7.19.6",
|
||||
"@babel/preset-env": "^7.20.2",
|
||||
"@types/intl": "^1.2.0",
|
||||
"babel-loader": "^9.1.0",
|
||||
"css-loader": "^6.7.3",
|
||||
"style-loader": "^3.3.1",
|
||||
"ts-loader": "^9.4.2",
|
||||
"typescript": "^4.9.4",
|
||||
"webpack": "^5.75.0",
|
||||
"webpack-cli": "^5.0.1",
|
||||
"webpack-dev-server": "^4.11.1"
|
||||
"@babel/core": "^7.22.11",
|
||||
"@babel/plugin-transform-runtime": "^7.22.10",
|
||||
"@babel/preset-env": "^7.22.10",
|
||||
"babel-loader": "^9.1.3",
|
||||
"css-loader": "^6.8.1",
|
||||
"style-loader": "^3.3.3",
|
||||
"ts-loader": "^9.4.4",
|
||||
"typescript": "^5.2.2",
|
||||
"webpack": "^5.88.2",
|
||||
"webpack-cli": "^5.1.4",
|
||||
"webpack-dev-server": "^4.15.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.20.6",
|
||||
"@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.26.1",
|
||||
"intl": "^1.2.5",
|
||||
"preact": "^10.11.3",
|
||||
"@babel/runtime": "^7.22.11",
|
||||
"@fullcalendar/core": "^6.1.8",
|
||||
"@fullcalendar/icalendar": "^6.1.8",
|
||||
"@fullcalendar/resource": "^6.1.8",
|
||||
"@fullcalendar/resource-timegrid": "^6.1.8",
|
||||
"@fullcalendar/resource-timeline": "^6.1.8",
|
||||
"@fullcalendar/timegrid": "^6.1.8",
|
||||
"core-js": "^3.32.1",
|
||||
"ical.js": "^1.5.0",
|
||||
"preact": "^10.17.1",
|
||||
"unique-colors": "^1.0.1"
|
||||
}
|
||||
}
|
||||
|
3492
pnpm-lock.yaml
generated
3492
pnpm-lock.yaml
generated
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
generated
Normal file
1543
server/pnpm-lock.yaml
generated
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.gotoDate('2022-06-27');
|
||||
|
||||
return calendar;
|
||||
}
|
||||
|
@ -8,6 +8,6 @@
|
||||
</head>
|
||||
<body>
|
||||
<div id="calendar"></div>
|
||||
<script src="bundle.js"></script>
|
||||
<script src="bundle-index.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
@ -1,9 +1,8 @@
|
||||
import '@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 resourceTimelinePlugin from '@fullcalendar/resource-timeline';
|
||||
import type { createElement } from 'preact';
|
||||
import type { createElement } from '@fullcalendar/core/preact';
|
||||
|
||||
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>
|
||||
</head>
|
||||
<body>
|
||||
<div id="errorContainer"></div>
|
||||
<div class="modal" id="loadingSpinner"></div>
|
||||
<div class="modal" id="errorModal">
|
||||
<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 { common_calendarOptions, main } from './common';
|
||||
|
||||
import './wall-display.html';
|
||||
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(), {
|
||||
passive: false,
|
||||
|
@ -31,7 +31,10 @@ module.exports = [
|
||||
{
|
||||
...common,
|
||||
name: 'default',
|
||||
entry: './src/index.ts',
|
||||
entry: {
|
||||
index: './src/index.ts',
|
||||
'wall-display': './src/wall-display.ts',
|
||||
},
|
||||
devServer: {
|
||||
allowedHosts: 'all',
|
||||
proxy: {
|
||||
@ -39,6 +42,9 @@ module.exports = [
|
||||
target: 'https://calendar.google.com',
|
||||
changeOrigin: true,
|
||||
},
|
||||
'/ipad.png': {
|
||||
target: 'http://localhost:1234',
|
||||
},
|
||||
},
|
||||
},
|
||||
module: {
|
||||
@ -53,18 +59,18 @@ module.exports = [
|
||||
},
|
||||
output: {
|
||||
path: path.resolve(__dirname, 'dist'),
|
||||
filename: 'bundle.js',
|
||||
filename: 'bundle-[name].js',
|
||||
},
|
||||
},
|
||||
{
|
||||
...common,
|
||||
name: 'wall-display',
|
||||
entry: './src/wall-display.ts',
|
||||
name: 'ipad',
|
||||
entry: ['./src/ipad.ts'],
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.m?js$/,
|
||||
exclude: /node_modules/,
|
||||
include: /node_modules\/@fullcalendar/,
|
||||
loader: 'babel-loader',
|
||||
},
|
||||
{
|
||||
@ -77,7 +83,7 @@ module.exports = [
|
||||
},
|
||||
output: {
|
||||
path: path.resolve(__dirname, 'dist'),
|
||||
filename: 'bundle-wall-display.js',
|
||||
filename: 'bundle-ipad.js',
|
||||
},
|
||||
},
|
||||
];
|
||||
|
Loading…
Reference in New Issue
Block a user