The browser demo is a static project — no build tool, no framework, no server. It replicates the Android app’s full flow using plain HTML, CSS, and vanilla JavaScript, withDocumentation Index
Fetch the complete documentation index at: https://mintlify.com/apursley2012/punctuowlity/llms.txt
Use this file to discover all available pages before exploring further.
localStorage in place of SQLite. Open any HTML file directly in a browser and it works without an HTTP server, though some features (Web Notifications) require a secure context or localhost.
File Structure
| File | data-page | Purpose |
|---|---|---|
index.html | "login" | Username / email + password form. Submits to the #loginForm handler in app.js. |
signup.html | "signup" | Full registration form: first name, last name, email, phone, username, password, confirm password. Submits to the #signupForm handler. |
sms.html | "sms" | Two-button choice screen. Saves "allowed" or "denied" to localStorage['punctuowlity-sms'] and navigates to events.html. |
events.html | "main" | Protected dashboard — redirects to index.html if not authenticated, or to sms.html if the SMS preference has not been set. Renders the event grid, category tabs, and search field. |
add-event.html | "add" | Add-or-edit form (title, date, time, alert toggle). Reads an ?id= query parameter to load an existing event for editing. |
app.js Architecture
app.js is a single, side-effect-driven script. It runs on every page load, inspects document.body.dataset.page, and activates only the logic relevant to the current page. Shared utilities are always initialised.
getEvents()
punctuowlity-events key from localStorage. If the key is absent, null, or not a valid JSON array, it falls back to the built-in seedEvents array. Every event — whether from storage or the seed — is passed through normalizeEvent before being returned, so callers always receive a fully-formed event object.
saveEvents(events)
localStorage under punctuowlity-events, replacing whatever was there before. Always pass the full array, not a delta.
normalizeEvent(event)
The central transformation function. Accepts a raw event object from any source and returns a fully-normalised object guaranteed to have every field the UI depends on.
YYYY-MM-DD (ISO) or MM/dd/yyyy (Android legacy) in either fullDate or date. Both are normalised to YYYY-MM-DD in the returned object.
Time handling: A non-empty rawTime matching HH:MM is converted to 12-hour display format with a two-digit hour and AM/PM suffix (e.g. "13:30" → "01:30PM"). An empty or absent rawTime produces "ALL DAY".
Category auto-detection: Only fires when the incoming event has no category field. If a category was explicitly saved, it is preserved as-is.
getUsers() / saveUsers(users)
localStorage and sessionStorage (whichever are available). On read, the first storage that contains a valid array wins. This dual-write strategy makes accounts survive a page refresh (localStorage) while also being accessible to short-lived session checks (sessionStorage).
toast(message)
.toast <div> at the bottom of <body>. The .show class is added on the next animation frame to trigger a CSS transition in. After 2400 ms the .show class is removed (triggering a fade-out transition), then the element is removed from the DOM after a further 250 ms. Any existing toast is removed immediately before a new one is created, preventing stacking.
Page Routing Logic
Every HTML page setsdocument.body.dataset.page to a string identifier. app.js branches on this value using if (document.body.dataset.page === '...') blocks.
Protected route (events.html / data-page="main"):
sessionStorage['punctuowlity-authenticated']must equal'true'. This value is set by the login form handler on a successful credential match and is cleared automatically when the browser session ends.localStorage['punctuowlity-sms']must not benull. Anullvalue means the user has never passed throughsms.html, so they are redirected there before reaching the event list.
.back button wired to history.back(), falling back to location.assign('index.html') if there is no history entry.
All navigations between pages use location.assign() (pushes a history entry) or location.replace() (no history entry, used for auth redirects).
Seed Events
When nopunctuowlity-events key exists in localStorage, getEvents() falls back to the following built-in array:
all, birthday, appointment, trip) and includes one all-day event (Project Two Due) alongside three timed events. One seed event (Nich's Birthday) has alert: true to demonstrate the notification logic.