Javascript Calendar, there sure are a ton of these all over the Internet. So I figured to do something a little different. This is a small experiment of mine to create a Javascript Events Calendar progressive web app – That’s right, a serverless, installable web app that works even when the user is offline. Read on!
TABLE OF CONTENTS
DOWNLOAD & NOTES
Here is the download link to the example code, so you don’t have to copy-paste everything.
EXAMPLE CODE DOWNLOAD
Just click on “download zip” or do a git clone. I have released it under the MIT license, so feel free to build on top of it or use it in your own project.
SORRY FOR THE ADS...
But someone has to pay the bills, and sponsors are paying for it. I insist on not turning Code Boxx into a "paid scripts" business, and I don't "block people with Adblock". Every little bit of support helps.
Buy Me A Coffee Code Boxx eBooks
QUICK NOTES
- This is not a “newbie-friendly” open-source project and example. It involves the use of service workers, cache storage, and SQLite through web assembly.
- “Grade A” browser required, alongside a
https://
server.http://localhost
is fine for testing too.
HOW IT WORKS
Not going to explain everything line-by-line (will take forever), but here’s a quick walkthrough of the Javascript Calendar PWA.
PART 1) SINGLE PAGE HTML
<!-- (A) PERIOD SELECTOR -->
<div id="calHead">
<div id="calPeriod">
<button id="calToday" type="button" class="icon-pushpin"></button>
<button id="calBack" type="button" class="icon-circle-left"></button>
<select id="calMonth"></select>
<input id="calYear" type="number">
<button id="calNext" type="button" class="icon-circle-right"></button>
</div>
<button id="calAdd" type="button" class="icon-plus"></button>
</div>
<!-- (B) CALENDAR WRAPPER -->
<div id="calWrap">
<div id="calDays"></div>
<div id="calBody"></div>
</div>
<!-- (C) EVENT FORM -->
<dialog id="calForm"><form method="dialog">
<div id="evtCX" class="mi">clear</div>
<h2 class="evt100">CALENDAR EVENT</h2>
<div class="evt50">
<label>Start</label>
<input id="evtStart" type="datetime-local" required>
</div>
<div class="evt50">
<label>End</label>
<input id="evtEnd" type="datetime-local" required>
</div>
<div class="evt50">
<label>Text Color</label>
<input id="evtColor" type="color" value="#000000" required>
</div>
<div class="evt50">
<label>Background Color</label>
<input id="evtBG" type="color" value="#ffdbdb" required>
</div>
<div class="evt100">
<label>Event</label>
<input id="evtTxt" type="text" required>
</div>
<div class="evt100">
<input type="hidden" id="evtID">
<button type="button" id="evtDel" class="icon-bin2"></button>
<button type="submit" id="evtSave" class="icon-checkmark"></button>
</div>
</form></dialog>
Look no further, this is the only HTML page in the entire project.
<div id="calHead">
The header bar.<div id="calPeriod">
Month and year selectors.<input id="calAdd">
Add new event button.
<div id="calWrap">
The calendar will be generated here with Javascript.<dialog id="calForm">
Popup calendar event form.
PART 2) JAVASCRIPT
2A) SQLITE DATABASE
var calDB = {
// (A) PROPERTIES
db : null, // database object
cache : null, // storage cache object
cname : "SQLDB", // cache storage name
dbname : "/calendar.sqlite", // database storage name
// (B) INITIALIZE
init : async () => {
// (B1) STORAGE CACHE
calDB.cache = await caches.open(calDB.cname);
// (B2) ATTEMPT TO LOAD DATABASE FROM STORAGE CACHE
calDB.cache.match(calDB.dbname).then(async r => {
// (B2-1) SQLJS
const SQL = await initSqlJs({
locateFile: filename => `assets/${filename}`
});
// (B2-2) NOPE - CREATE A NEW DATABASE
if (r==undefined) {
calDB.db = new SQL.Database();
calDB.db.run(`CREATE TABLE events (
evt_id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
evt_start TEXT NOT NULL,
evt_end TEXT NOT NULL,
evt_text TEXT NOT NULL,
evt_color TEXT NOT NULL,
evt_bg TEXT NOT NULL
)`);
calDB.db.run("CREATE INDEX evt_start ON events (evt_start)");
calDB.db.run("CREATE INDEX evt_end ON events (evt_end)");
await calDB.export();
cal.initB();
}
// (B2-3) LOAD EXISTING DATABASE
else {
const buf = await r.arrayBuffer();
calDB.db = new SQL.Database(new Uint8Array(buf));
cal.initB();
}
});
},
// (C) EXPORT TO CACHE STORAGE
export : async () => await calDB.cache.put(
calDB.dbname, new Response(calDB.db.export())
),
// (D) SAVE EVENT
// data is an array!
// data[0] = start date
// data[1] = end date
// data[2] = event text
// data[3] = text color
// data[4] = background color
// data[5] = optional event id (is an update if specified)
save : async (data) => {
const sql = data.length==6
? "UPDATE events SET evt_start=?, evt_end=?, evt_text=?, evt_color=?, evt_bg=? WHERE evt_id=?"
: "INSERT INTO events (evt_start, evt_end, evt_text, evt_color, evt_bg) VALUES (?,?,?,?,?)" ;
calDB.db.run(sql, data);
await calDB.export();
},
// (E) DELETE EVENT
del : async (id) => {
calDB.db.run("DELETE FROM events WHERE evt_id=?", [id]);
await calDB.export();
},
// (F) GET EVENT
get : id =>
(calDB.db.prepare("SELECT * FROM events WHERE evt_id=$eid"))
.getAsObject({$eid:id}),
// (G) GET EVENTS FOR GIVEN PERIOD
getPeriod : (start, end) => {
// (G1) SQL QUERY
const stmt = calDB.db.prepare(`SELECT * FROM events WHERE (
(evt_start BETWEEN $start AND $end)
OR (evt_end BETWEEN $start AND $end)
OR (evt_start <= $start AND evt_end >= $end)
)`);
stmt.bind({$start:start, $end:end});
// (G2) DATA YOGA
// s & e : start & end date
// c & b : text & background color
// t : event text
let events = {};
while (stmt.step()) {
const r = stmt.getAsObject();
events[r["evt_id"]] = {
"s" : r["evt_start"],
"e" : r["evt_end"],
"t" : r["evt_text"],
"c" : r["evt_color"],
"b" : r["evt_bg"]
};
}
return events;
}
};
Yes, you see that right – That’s SQL in Javascript, made possible with the SQLJS library. Don’t need to panic, here’s a quick summary:
- (B)
calDB.init()
We will run this on page load. Pretty much create a database with only one table –events
. - (C)
calDB.export()
Unfortunately, Javascript does not have direct access to the file system. But we can store the database file in a cache storage. - (D to G) CRUD.
cal.save()
Add or update an event.cal.del()
Delete an event.cal.get()
Get specified event.cal.getPeriod()
Get all events within a given period.
2B) PROPERTIES & HELPERS
var cal = {
// (A) PROPERTIES
// (A1) FLAGS & DATA
mon : false, // monday first
events : null, // events data for current month/year
sMth : 0, // selected month
sYear : 0, // selected year
sDIM : 0, // number of days in selected month
sF : 0, // first date of the selected month (yyyy-mm-dd hh:mm:ss)
sL : 0, // last date of the selected month (yyyy-mm-dd hh:mm:ss)
sFD : 0, // first day of the selected month (mon-sun)
sLD : 0, // last day of the selected month (mon-sun)
// (A2) HTML ELEMENTS
hMth : null, hYear : null, // month & year
hCD : null, hCB : null, // calendar days & body
hFormWrap : null, hForm : null, // event form
hfID : null, hfStart : null, // event form fields
hfEnd : null, hfTxt : null,
hfColor : null, hfBG : null,
hfDel : null,
// (A3) HELPER FUNCTION - TRANSITION
transit : swap => {
if (document.startViewTransition) { document.startViewTransition(swap); }
else { swap(); }
},
//...
};
All the calendar mechanics are contained within var cal = {}
. There are a lot of settings, flags, and data… Not going to go through all of them one by one, just take note of mon : false
– Change this if you want the week to start on Monday.
2C) INIT
// (B) INIT PART 1 - REQUIREMENTS CHECK + WORKER + DATABASE INIT
initA : async () => {
// (B1) REQUIREMENTS CHECK - WASM
if (typeof WebAssembly == "undefined") {
alert("Your browser does not support Web Assembly.");
return;
}
// (B2) REQUIREMENTS CHECK - STORAGE CACHE
if (!"caches" in window) {
alert("Your browser does not support cache storage.");
return;
}
// (B3) REGISTER SERVICE WORKER
if ("serviceWorker" in navigator) {
navigator.serviceWorker.register("CB-worker.js");
}
// (B4) DATABASE INIT
calDB.init();
},
// (C) INIT PART 2 - SETUP & ENABLE HTML
initB : () => {
// (C1) GET HTML ELEMENTS
cal.hMth = document.getElementById("calMonth");
cal.hYear = document.getElementById("calYear");
cal.hCD = document.getElementById("calDays");
cal.hCB = document.getElementById("calBody");
cal.hFormWrap = document.getElementById("calForm");
cal.hForm = cal.hFormWrap.querySelector("form");
cal.hfID = document.getElementById("evtID");
cal.hfStart = document.getElementById("evtStart");
cal.hfEnd = document.getElementById("evtEnd");
cal.hfTxt = document.getElementById("evtTxt");
cal.hfColor = document.getElementById("evtColor");
cal.hfBG = document.getElementById("evtBG");
cal.hfDel = document.getElementById("evtDel");
// (C2) MONTH & YEAR SELECTOR
let now = new Date(), nowMth = now.getMonth() + 1;
for (let [i,n] of Object.entries({
1 : "January", 2 : "Febuary", 3 : "March", 4 : "April",
5 : "May", 6 : "June", 7 : "July", 8 : "August",
9 : "September", 10 : "October", 11 : "November", 12 : "December"
})) {
let opt = document.createElement("option");
opt.value = i;
opt.innerHTML = n;
if (i==nowMth) { opt.selected = true; }
cal.hMth.appendChild(opt);
}
cal.hYear.value = parseInt(now.getFullYear());
// (C3) ATTACH CONTROLS
cal.hMth.onchange = cal.load;
cal.hYear.onchange = cal.load;
document.getElementById("calToday").onclick = () => cal.today();
document.getElementById("calBack").onclick = () => cal.pshift();
document.getElementById("calNext").onclick = () => cal.pshift(1);
document.getElementById("calAdd").onclick = () => cal.show();
cal.hForm.onsubmit = () => cal.save();
document.getElementById("evtCX").onclick = () => cal.transit(() => cal.hFormWrap.close());
cal.hfDel.onclick = cal.del;
// (C4) DRAW DAY NAMES
let days = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
if (cal.mon) { days.push("Sun"); } else { days.unshift("Sun"); }
for (let d of days) {
let cell = document.createElement("div");
cell.className = "calCell";
cell.innerHTML = d;
cal.hCD.appendChild(cell);
}
// (C5) LOAD & DRAW CALENDAR
cal.load();
},
// ...
window.onload = cal.initA;
The initialization is split into two phases –
- On page load,
cal.initA()
will run. Does requirement checks, registers a service worker, and sets the database up. - The second phase
cal.initB()
will deal with the HTML interface – Draw the year, months, days, and enable the interface.
2D) PERIOD SHIFT
// (D) SHIFT CURRENT PERIOD BY 1 MONTH
pshift : forward => {
cal.sMth = parseInt(cal.hMth.value);
cal.sYear = parseInt(cal.hYear.value);
if (forward) { cal.sMth++; } else { cal.sMth--; }
if (cal.sMth > 12) { cal.sMth = 1; cal.sYear++; }
if (cal.sMth < 1) { cal.sMth = 12; cal.sYear--; }
cal.hMth.value = cal.sMth;
cal.hYear.value = cal.sYear;
cal.load();
},
// (E) JUMP TO TODAY
today : () => {
let now = new Date(), ny = now.getFullYear(), nm = now.getMonth()+1;
if (ny!=cal.sYear || (ny==cal.sYear && nm!=cal.sMth)) {
cal.hMth.value = nm;
cal.hYear.value = ny;
cal.load();
}
},
cal.pshift()
Shifts the current period by one month.cal.today()
Jumps back to the current month and year.
2E) LOAD EVENTS
// (F) LOAD EVENTS DATA FOR MONTH/YEAR
load : () => {
// (F1) SET SELECTED PERIOD
cal.sMth = parseInt(cal.hMth.value);
cal.sYear = parseInt(cal.hYear.value);
cal.sDIM = new Date(cal.sYear, cal.sMth, 0).getDate();
cal.sFD = new Date(cal.sYear, cal.sMth-1, 1).getDay();
cal.sLD = new Date(cal.sYear, cal.sMth-1, cal.sDIM).getDay();
let m = cal.sMth;
if (m < 10) { m = "0" + m; }
cal.sF = `${cal.sYear}-${m}-01 00:00:00`;
cal.sL = `${cal.sYear}-${m}-${cal.sDIM} 23:59:59`;
// (F2) FETCH & DRAW
cal.events = calDB.getPeriod(cal.sF, cal.sL);
cal.hCB.innerHTML = "";
cal.draw();
},
cal.load()
pretty much loads the events data from the database into cal.events
.
2F) DRAW CALENDAR
// (G) DRAW CALENDAR
draw : () => {
// (G1) CALCULATE DAY MONTH YEAR
// note - jan is 0 & dec is 11 in js
// note - sun is 0 & sat is 6 in js
let now = new Date(), // current date
nowMth = now.getMonth()+1, // current month
nowYear = parseInt(now.getFullYear()), // current year
nowDay = cal.sMth==nowMth && cal.sYear==nowYear ? now.getDate() : null ;
// (G2) DRAW CALENDAR ROWS & CELLS
// (G2-1) INIT
let rowA, rowB, rowC, rowMap = {}, rowNum = 1, cell, cellNum = 1,
// (G2-2) HELPER - DRAW A NEW ROW
rower = () => {
rowA = document.createElement("div");
rowB = document.createElement("div");
rowC = document.createElement("div");
rowA.className = "calRow";
rowA.id = "calRow" + rowNum;
rowB.className = "calRowHead";
rowC.className = "calRowBack";
cal.hCB.appendChild(rowA);
rowA.appendChild(rowB);
rowA.appendChild(rowC);
},
// (G2-3) HELPER - DRAW A NEW CELL
celler = day => {
cell = document.createElement("div");
cell.className = "calCell";
if (day) {
cell.innerHTML = day;
cell.classList.add("calCellDay");
cell.onclick = () => {
cal.show();
let d = +day, m = +cal.hMth.value,
s = `${cal.hYear.value}-${String(m<10 ? "0"+m : m)}-${String(d<10 ? "0"+d : d)}T00:00:00`;
cal.hfStart.value = s;
cal.hfEnd.value = s;
};
}
rowB.appendChild(cell);
cell = document.createElement("div");
cell.className = "calCell";
if (day===undefined) { cell.classList.add("calBlank"); }
if (day!==undefined && day==nowDay) { cell.classList.add("calToday"); }
rowC.appendChild(cell);
};
// (G2-4) RESET CALENDAR
cal.hCB.innerHTML = ""; rower();
// (G2-5) BLANK CELLS BEFORE START OF MONTH
if (cal.mon && cal.sFD != 1) {
let blanks = cal.sFD==0 ? 7 : cal.sFD ;
for (let i=1; i<blanks; i++) { celler(); cellNum++; }
}
if (!cal.mon && cal.sFD != 0) {
for (let i=0; i<cal.sFD; i++) { celler(); cellNum++; }
}
// (G2-6) DAYS OF THE MONTH
for (let i=1; i<=cal.sDIM; i++) {
rowMap[i] = { r : rowNum, c : cellNum };
celler(i);
if (cellNum%7==0 && i!=cal.sDIM) { rowNum++; rower(); }
cellNum++;
}
// (G2-7) BLANK CELLS AFTER END OF MONTH
if (cal.mon && cal.sLD != 0) {
let blanks = cal.sLD==6 ? 1 : 7-cal.sLD;
for (let i=0; i<blanks; i++) { celler(); cellNum++; }
}
if (!cal.mon && cal.sLD != 6) {
let blanks = cal.sLD==0 ? 6 : 6-cal.sLD;
for (let i=0; i<blanks; i++) { celler(); cellNum++; }
}
// (G3) DRAW EVENTS
if (Object.keys(cal.events).length > 0) { for (let [id,evt] of Object.entries(cal.events)) {
// (G3-1) EVENT START & END DAY
let sd = new Date(cal.toISODate(evt.s)),
ed = new Date(cal.toISODate(evt.e));
if (sd.getFullYear() < cal.sYear) { sd = 1; }
else { sd = sd.getMonth()+1 < cal.sMth ? 1 : sd.getDate(); }
if (ed.getFullYear() > cal.sYear) { ed = cal.sDIM; }
else { ed = ed.getMonth()+1 > cal.sMth ? cal.sDIM : ed.getDate(); }
// (G3-2) "MAP" ONTO HTML CALENDAR
cell = {}; rowNum = 0;
for (let i=sd; i<=ed; i++) {
if (rowNum!=rowMap[i]["r"]) {
cell[rowMap[i]["r"]] = { s:rowMap[i]["c"], e:0 };
rowNum = rowMap[i]["r"];
}
if (cell[rowNum]) { cell[rowNum]["e"] = rowMap[i]["c"]; }
}
// (G3-3) DRAW HTML EVENT ROW
for (let [r,c] of Object.entries(cell)) {
let o = c.s - 1 - ((r-1) * 7), // event cell offset
w = c.e - c.s + 1; // event cell width
rowA = document.getElementById("calRow"+r);
rowB = document.createElement("div");
rowB.className = "calRowEvt";
rowB.innerHTML = cal.events[id]["t"];
rowB.style.color = cal.events[id]["c"];
rowB.style.backgroundColor = cal.events[id]["b"];
rowB.classList.add("w"+w);
if (o!=0) { rowB.classList.add("o"+o); }
rowB.onclick = () => cal.show(id);
rowA.appendChild(rowB);
}
}}
},
Right, cal.draw()
is massive. There is quite a bit of calculation, but it essentially draws the HTML “day cells”, then “maps” cal.events
on.
2G) SHOW EVENT FORM
// (H) SHOW EVENT FORM
show : id => {
if (id) {
cal.hfID.value = id;
cal.hfStart.value = cal.toISODate(cal.events[id]["s"]);
cal.hfEnd.value = cal.toISODate(cal.events[id]["e"]);
cal.hfTxt.value = cal.events[id]["t"];
cal.hfColor.value = cal.events[id]["c"];
cal.hfBG.value = cal.events[id]["b"];
cal.hfDel.style.display = "inline-block";
} else {
cal.hForm.reset();
cal.hfID.value = "";
cal.hfDel.style.display = "none";
}
cal.transit(() => cal.hFormWrap.show());
},
cal.show()
To display the hidden popup event form.
id===NUMBER
When the user clicks on an event to edit it. Load the event fromindexDB
and populate the form fields.id===ANYTHING
When the user clicks on “add”. Just show an empty event form.
2H) SAVE & DELETE EVENTS
// (I) SAVE EVENT
save : () => {
// (I1) COLLECT DATA
// data[0] = start date
// data[1] = end date
// data[2] = event text
// data[3] = text color
// data[4] = background color
var data = [
cal.hfStart.value,
cal.hfEnd.value,
cal.hfTxt.value,
cal.hfColor.value,
cal.hfBG.value
];
if (cal.hfID.value != "") { data.push(+cal.hfID.value); }
// (I2) DATE CHECK
if (new Date(data[0]) > new Date(data[1])) {
alert("Start date cannot be later than end date!");
return false;
}
// (I3) SAVE
await calDB.save(data);
cal.transit(() => cal.hFormWrap.close());
cal.load();
return false;
},
// (J) DELETE EVENT
del : () => { if (confirm("Delete Event?")) {
await calDB.del(cal.hfID.value);
cal.transit(() => cal.hFormWrap.close());
cal.load();
}}
Pretty self-explanatory.
cal.save()
gets the data from the HTML form and saves the event into the database.cal.del()
removes the specified event from the database.
PART 3) PROGRESSIVE WEB APP
3A) HTML META HEADERS
<!-- WEB APP & ICONS -->
<link rel="icon" href="assets/favicon.png" type="image/png">
<meta name="mobile-web-app-capable" content="yes">
<meta name="theme-color" content="white">
<link rel="apple-touch-icon" href="assets/icon-512.png">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black">
<meta name="apple-mobile-web-app-title" content="JS Calendar">
<meta name="msapplication-TileImage" content="assets/icon-512.png">
<meta name="msapplication-TileColor" content="#ffffff">
<!-- WEB APP MANIFEST -->
<!-- https://web.dev/add-manifest/ -->
<link rel="manifest" href="CB-manifest.json">
The calendar is pretty much already complete. But to create a PWA, there are 3 things to address:
- Add the above HTML meta headers to specify the icons/app name.
- Register a web manifest.
- Register a service worker.
3B) WEB MANIFEST
{
"short_name": "JS Calendar",
"name": "JS Calendar",
"icons": [{
"src": "assets/favicon.png",
"sizes": "128x128",
"type": "image/png"
}, {
"src": "assets/ico-512.png",
"sizes": "512x512",
"type": "image/png"
}],
"start_url": "js-calendar.html",
"scope": "/",
"background_color": "white",
"theme_color": "white",
"display": "standalone"
}
The manifest file is what it is… A file to indicate the app icons, name, settings, and more.
3C) SERVICE WORKER
// (A) CREATE/INSTALL CACHE
self.addEventListener("install", evt => {
self.skipWaiting();
evt.waitUntil(
caches.open("JSCalendar")
.then(cache => cache.addAll([
"assets/favicon.png",
"assets/head-pwa-calendar.webp",
"assets/ico-512.png",
"assets/icomoon.woff2",
"assets/js-calendar.css",
"assets/js-calendar.js",
"assets/js-calendar-db.js",
"assets/sql-wasm.js",
"assets/sql-wasm.wasm",
"CB-manifest.json",
"js-calendar.html"
]))
.catch(err => console.error(err))
);
});
// (B) CLAIM CONTROL INSTANTLY
self.addEventListener("activate", evt => self.clients.claim());
// (C) LOAD FROM CACHE FIRST, FALLBACK TO NETWORK IF NOT FOUND
self.addEventListener("fetch", evt => evt.respondWith(
caches.match(evt.request).then(res => res || fetch(evt.request))
));
For those who are new, a service worker is just a piece of Javascript that runs in the background. For this worker:
- (A) Create a new storage cache and save the project files.
- (C) “Hijack” the fetch requests. If the requested file is in the cache, use it. If not, load from the network.
In short, this worker will enable the web app to work in “offline mode”.
EXTRA BITS & LINKS
That’s all for the tutorial, and here is a small section on some extras and links that may be useful to you.
COMPATIBILITY CHECKS
- Arrow Functions – CanIUse
- Service Workers – CanIUse
- Cache Storage – CanIUse
- Add To Home Screen – CanIUse
- Web Assembly – CanIUse
Most of the required features are already well-supported on modern “Grade A” browsers.
THE END
Thank you for reading, and we have come to the end. I hope that it has helped you to better understand, and if you want to share anything with this guide, please feel free to comment below. Good luck and happy coding!
Hello!
How i set in europe date form, when the monday is first day of the week?
Tanks for the answer.
To locale date string – https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/toLocaleDateString
Hi,
Thank you for this nice project.
Unfortunately on iPhone and iPad (with Safari, Firefox or Brave), any 1 day events are displayed as running from the first to the last day of the month.
Any suggestion to correct that behaviour ?
Jean
Sorry, I don’t have half-eaten apple devices to verify that… Works as intended on Chrome, Edge, FF, and Opera though – Start 4/4/2023 12:00 AM, end 4/4/2023 11:59 PM shows exactly as one day.
I would like to pass a department ID into the calendar class to view events by department, but not having much luck. I have a department drop down which passes a link to the calendar through the url ( ?dept=23 ). I would like to grab that and use it in the SQL in the calendar class but I’m struggling getting that value into the class. Any help you could give me would be greatly appreciated
1) Create multiple object stores for different departments.
2) On changing dept dropdown – Fetch data from the selected object store and redraw the HTML. The end.
Throw the ideas of “server-side” GET, POST, SQL database out of the window. They don’t exist here, this is a serverless offline app.
i am learning about pwa and tried uploading this to a website to use it online and it works on chrome, but it does not seem to work on edge or safari. would you know why?
See “compatibility checks” above, and do your own debugging. Otherwise:
https://code-boxx.com/faq/#notwork “Don’t seem to work”
https://code-boxx.com/faq/#help “Unclear question”
This mini project is valuable for studies, and I would like a light on how I could make the offline push notification starting from the calendar event, will it be possible?
Yes, it is possible with a scheduled Push Notification.
https://developer.mozilla.org/en-US/docs/Web/API/notification
https://css-tricks.com/creating-scheduled-push-notifications/