CultureHub โ I Built a Zero-Server Team Culture Tracker with Vanilla HTML, CSS & JS

Track events, check in attendees, run a live TV leaderboard, and store photo galleries โ all in the browser, no backend required.
The Problem
Most companies have culture events โ trivia nights, team bowlings, charity drives, holiday parties. Someone books the room, sends the invite, and then... nothing gets tracked. Who showed up? Who won? How engaged is each team over time? Six months later you're trying to justify the culture budget with vibes and a few Slack photos.
I wanted a tool that made this effortless: open it at the event, tap people as they walk in, mark the winner, put the leaderboard on the TV. No logins, no setup, no IT tickets.
CultureHub is that tool. It runs entirely in the browser, stores everything in localStorage, and ships as a folder of plain HTML files.
What It Does
Eight pages, one CSS file, one shared data layer:
| Page | Purpose |
|---|---|
index.html |
Dashboard home โ stats, top-5 leaderboard, upcoming events |
events.html |
Create and manage events, filter by type |
roster.html |
Add people or bulk-import from JSON / Microsoft Teams |
checkin.html |
Day-of attendance โ tap to check in, mark winners |
dashboard.html |
Live leaderboard with TV fullscreen mode |
search.html |
Global search across people and events |
gallery.html |
Per-event photo grids with drag-drop and slideshow |
settings.html |
Org name, backup / restore, danger zone |
Features
๐ Event Management
Create events with a title, date, type, location, and description. Six built-in types each get a distinct colour badge for fast scanning:
๐ฎ Game โ trivia nights, competitions
๐ฅ Social โ happy hours, team lunches
๐ค Team Building โ workshops, offsites
๐ Holiday โ seasonal parties
โค๏ธ Charity โ volunteer days, fundraisers
๐ General โ everything else
Each event card shows live check-in counts, registered attendees, points awarded, and photo count at a glance.
๐ฅ Roster & Import
Add people manually (name, team, email) or bulk-import from a JSON array. The importer handles both a native format and the Microsoft Teams / Azure AD admin export:
[
{ "name": "Alice Johnson", "team": "Engineering", "email": "alice@co.com" },
{ "displayName": "Bob Smith", "mail": "bob@co.com", "department": "Marketing" }
]
Both formats work โ the importer checks for name or displayName, email or mail, team or department. Duplicate detection prevents double-imports.
โ Check-In & Scoring
The check-in page is the core day-of tool. Open it on a laptop or tablet at the event:
Tap a card โ person is checked in, scores 1 point
Tap ๐ โ mark as winner, scores 3 points
Tap again โ undo the check-in
Check All In โ bulk check-in the whole registered list
The scoring is intentionally simple:
| Action | Points |
|---|---|
| โ Attending and checking in | 1 pt |
| ๐ Winning an event | 3 pts |
| Points cap | None โ accumulate forever |
Tie-breaking on the leaderboard: equal points โ sorted by number of events attended.
๐บ TV Dashboard & Live Leaderboard
The dashboard page doubles as a lobby display. Hit TV Mode (or Alt+T / F11) to go fullscreen with the nav hidden:
Animated rank rows with gold/silver/bronze highlights
A scrolling ticker cycling through top scores and recent events
Event attendance progress bars in the sidebar
Auto-refreshes every 30 seconds
Escto exit TV Mode
This is the one that gets the room going at the end of an event.
๐ผ Photo Gallery
Per-event photo grids with drag-and-drop upload. Click any photo to open a fullscreen slideshow โ navigate with arrow keys, close with Esc.
โ ๏ธ Storage note: Photos are stored as base64 data URLs in
localStorage, which browsers typically cap at 5โ10 MB. Use the built-in Backup feature regularly. A "Storage full" toast will appear if you hit the quota.
๐ Search & PDF Reports
Global search across people and events with a split-panel detail view โ click a result on the left to see full stats on the right without leaving the page.
Three report types, all generated client-side as print-ready popups:
Overview report โ top-10 leaderboard + full events table
Event report โ attendance list with status and points
Person report โ individual stats with event history
Getting Started
No install. No build step. No account.
Step 1 โ Configure your org
Open settings.html, set your organisation name. Hit Load Sample Data to pre-populate 8 people and 4 events so you can explore every feature immediately.
Step 2 โ Add your team
Go to roster.html โ Import and paste your team JSON, or add people one by one with + Add Person.
Step 3 โ Create your first event
Go to events.html โ + New Event. Give it a name, date, type, and location.
On the day:
Open
checkin.htmlon a tablet at the doorTap people as they arrive
Mark winners with ๐ when the competition ends
Open
dashboard.htmlon a screen at the front of the room
Architecture
The philosophy: no framework, no build step
The whole thing is three files doing all the heavy lifting:
culturehub/
โโโ data.js โ data layer (IIFE, localStorage, event bus)
โโโ utils.js โ UI helpers (nav, theme, toasts, modals, reports)
โโโ style.css โ complete design system (dark + light, ~380 lines)
Each HTML page is ~150โ280 lines of vanilla JS, all following the same pattern:
// Every page โ same three lines to bootstrap
document.getElementById('nav-root').innerHTML = navHTML();
setActiveNav();
// Reactive render โ re-runs whenever any data changes
function render() { /* build innerHTML */ }
window.addEventListener('culturehub:updated', render);
render();
data.js โ the data layer
An IIFE that exposes the CH singleton globally. All reads and writes go through it โ nothing touches localStorage directly from a page.
const CH = (() => {
const KEY = 'culturehub_data';
function save(data) {
localStorage.setItem(KEY, JSON.stringify(data));
window.dispatchEvent(new CustomEvent('culturehub:updated', { detail: data }));
}
// ... all CRUD methods
return { getPeople, addPerson, getEvents, addEvent, checkIn, toggleWinner, ... };
})();
Every mutation calls save(), which serialises the whole state and fires culturehub:updated. Every open page listens for that event and re-renders โ no state management library needed.
The public API:
CH.getPeople() CH.addPerson(name, team, email)
CH.getEvents() CH.addEvent(title, date, type, desc, location)
CH.checkIn(evId, pId) CH.toggleWinner(evId, pId)
CH.getLeaderboard() CH.getPersonStats(personId)
CH.exportBackup() CH.importBackup(jsonStr)
utils.js โ shared UI helpers
Loaded on every page after data.js. Provides:
Theme.toggle() toast(msg, type, duration)
openModal(html) confirmDialog(message, title)
avatarEl(name, size) fmtDate(dateStr)
eventTypeBadge(type) generateReport(type, id)
navHTML() setActiveNav()
The nav is a function โ navHTML() returns the header markup as a string, injected into a <div id="nav-root"> on every page. It reads the org name from settings on each call, so renaming the org updates all pages on next load.
style.css โ the design system
A single CSS file with full dark/light theming via custom properties:
:root,
html[data-theme="dark"] {
--bg: #07090f;
--accent: #00c9b1;
--gold: #ffb830;
/* ... */
}
html[data-theme="light"] {
--bg: #f2f5fc;
--accent: #007f6f;
/* ... */
}
All colours, shadows, and radii reference these tokens โ switching theme is a single attribute change on <html>.
FOUC prevention: Every page has a tiny inline script in <head> that applies the saved theme before the stylesheet parses, eliminating any flash of the wrong theme on load:
<script>
(function(){
var t = localStorage.getItem("culturehub_theme") || "dark";
document.documentElement.setAttribute("data-theme", t);
})();
</script>
Data Model
Everything lives under a single culturehub_data key in localStorage:
{
"version": 1,
"settings": {
"orgName": "Our Organization",
"theme": "dark"
},
"people": [
{
"id": "lx3k8a",
"name": "Alice Johnson",
"team": "Engineering",
"email": "alice@company.com",
"avatar": ""
}
],
"events": [
{
"id": "m2p9xq",
"title": "Summer Trivia Night",
"date": "2025-07-15",
"type": "Game",
"description": "Teams compete in trivia!",
"location": "Rooftop",
"gallery": [
{ "id": "...", "url": "data:image/jpeg;base64,...", "caption": "" }
],
"attendees": [
{
"personId": "lx3k8a",
"checkedIn": true,
"points": 3,
"isWinner": true
}
]
}
]
}
attendees[].personId is a soft foreign key to people[].id. The leaderboard calculation handles the edge case of an attendee whose person record was later deleted โ it skips gracefully rather than crashing.
UIDs are generated as Date.now().toString(36) + Math.random().toString(36).substr(2, 6) โ not cryptographically secure, but collision-proof enough for a local-only app.
Backup & Restore
The ๐พ Backup button in the nav bar (or Settings) downloads the full culturehub_data object as a dated JSON file:
culturehub-backup-2025-07-15.json
Photos are embedded as base64, so the file is completely self-contained. To restore: Settings โ load the file or paste the JSON โ click Restore. The importer validates that people and events keys exist before writing anything.
Keyboard Shortcuts
| Shortcut | Action |
|---|---|
Alt + T |
Toggle TV Mode on Dashboard |
F11 |
Toggle TV Mode on Dashboard |
Esc |
Exit TV Mode |
โ |
Next photo in slideshow |
โ |
Previous photo in slideshow |
Esc |
Close slideshow or modal |
What I'd Change Next
A few things I've noted for future iterations:
Photo storage โ base64 in localStorage is the biggest practical constraint. The natural next step is the Origin Private File System API (
navigator.storage.getDirectory()), which would lift the 5โ10 MB cap entirely and keep everything local.CSV import โ the roster importer handles JSON today. A CSV path would make it easier to copy-paste from a spreadsheet without any JSON formatting.
Points customisation โ the 1pt / 3pt system is hardcoded. Exposing it in Settings would take five minutes and make the app more flexible.
Final Thoughts
The constraint of "no server, no framework, no build step" turned out to be a feature. The whole app is inspectable, forkable, and deployable by dropping a folder anywhere โ a local filesystem, a USB drive, a GitHub Pages site, an intranet server. There's nothing to break, no dependencies to update, no account to log into.
If your team runs culture events and you want something you can actually own, give it a try.
Built with vanilla HTML, CSS, and JavaScript. All data stored locally in the browser โ no server, no sign-up, no cloud.

