Skip to content

Neveon/kanban-gamify

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

3 Commits
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

πŸŽ‰ Kanban Gamify

Make finishing tasks fun. Drag a card to Done on your kanban board and get confetti, fireworks, a happy little chime, and a running streak β€” a tiny dopamine hit every time you finish something.

A zero-dependency Chrome extension (Manifest V3). It works on GitHub Projects today, and is built to gamify any kanban board β€” adding one (Trello, etc.) is a drop-in "provider" away. It reads completions from board structure and text, not brittle CSS selectors, so it keeps working as sites reshuffle their class names. The stats HUD only pops up while you're actually on a board.


Features

  • Automatic Done detection on GitHub Projects boards β€” drag a card into your Done column and it's celebrated, with no clicks or manual logging.
  • Sound β€” a Web Audio chime (no audio files) that rotates through six major/pentatonic arpeggios so no two completions sound the same, with an occasional richer chord, plus adjustable volume.
  • Confetti β€” a self-contained, full-viewport canvas burst with configurable particle count and duration, and per-burst color/spread variety. Never shifts page layout.
  • Fireworks β€” emoji / firework / sparkle bursts (randomized per completion) that fire alongside confetti on a separate canvas. Adjustable intensity.
  • Screen Glow β€” a brief success vignette that flashes around the screen edges in a randomized success hue. Transparent center, never blocks the page.
  • 🎰 Jackpot celebrations β€” ~15% of completions roll a "jackpot": all effects escalate together β€” a triumphant sound fanfare, a 3Γ— confetti mega-burst, multiple fireworks, and a brighter/longer gold glow. Unpredictable by design.
  • Toast β€” a slide-in notification showing +1, tasks completed today, and all-time total. Pick bottom-left or bottom-right; tune how long it holds.
  • Stats HUD β€” a draggable, collapsible widget showing your πŸ”₯ streak, βœ“ today, and ⚑ all-time counts, flashing green on increase and red on decrease. It appears only while you're on a board (and hides everywhere else), and remembers its position and collapsed state across reloads and SPA navigation.
  • Streak tracking β€” consecutive days with at least one completion, computed from a local-date daily log.
  • Per-plugin settings popup β€” every plugin's settings are auto-generated from its schema, with a master enable toggle each. Changes apply live, no reload.
  • Modular plugin architecture β€” features are independent plugins wired through a shared event bus, so new behaviours are a drop-in folder away.

Install (Load unpacked)

This extension ships as plain source β€” no build step, no dependencies, no bundler. Load it directly:

  1. Open chrome://extensions in Chrome (or any Chromium browser).
  2. Toggle Developer mode on (top-right).
  3. Click Load unpacked.
  4. Select the kanban-gamify folder (the one containing manifest.json).
  5. Open a GitHub Projects board and drop a card into your Done column.

If your Done column isn't literally named "Done", open the extension popup and set the Done column name to match (case-insensitive).

A note on the loader / ESM model

MV3 content scripts can't be ES modules directly (no static import). To keep a true module architecture with no build step, manifest.json registers a tiny loader.js as the only content script. It does a dynamic import(chrome.runtime.getURL('content.js')), which is permitted in content scripts and loads content.js as a real ES module. From there, everything under core/ and plugins/ uses normal static import/export with relative paths and .js extensions. Each runtime-loaded module is listed in the manifest's web_accessible_resources (via globs). The popup is a normal extension page, so it just uses <script type="module">.


Architecture overview

loader.js  ──dynamic import──▢  content.js  (ESM entry)
                                   β”‚
                                   β”œβ”€ registers plugins  ──▢  core/plugin-registry.js
                                   └─ starts detection    ──▢  core/detector.js
                                                                   β”‚ asks "is this a board?"
                                                                   β–Ό
                                                          providers/  (github-projects, …)
                                                                   β”‚ emits
                                                                   β–Ό
                                                          core/event-bus.js
                                          ('task:done', 'task:undone', 'route:change', 'board:state')
                                                                   β”‚ fan-out
                          β”Œβ”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”
                          β–Ό      β–Ό      β–Ό          β–Ό      β–Ό       β–Ό      β–Ό
                       sound  confetti fireworks  glow  toast   hud   …
                                       (all read/write via core/storage.js)

The core modules, each with a single job:

  • core/event-bus.js β€” a synchronous pub/sub singleton (on/once/off/ emit/clear). emit isolates each handler in try/catch so one bad plugin can't break the others. Core events: task:done, task:undone, route:change, board:state (are we on a board?).

  • core/storage.js β€” the only thing that touches chrome.storage.local. Namespaced keys, core stats (total/daily log/today/streak), and per-plugin settings helpers.

  • core/plugin-registry.js β€” owns plugin lifecycle. On init it reads each plugin's enabled flag, resolves its settings from its schema, calls init(config), and subscribes the plugin to the bus. It watches chrome.storage.onChanged so toggling/tuning a plugin in the popup re-inits it live. Every plugin call is guarded in try/catch.

  • core/detector.js β€” the core feature, and board-agnostic. It asks the providers which one (if any) handles the current page, broadcasts board:state (so the HUD knows when to show), and β€” on a board β€” runs a reconcile algorithm over a debounced MutationObserver: it tracks the card identities currently in Done, suppresses the initial page load, ignores bulk re-renders, and emits task:done for cards entering Done / task:undone for cards leaving. It handles SPA navigation (patched history methods + a title observer) to re-prime per view.

  • providers/ β€” a board provider teaches the detector how to read ONE kind of board: matches(url) (is this one of its boards?), findDoneColumn(name), findCards(column), cardIdentity(card), cardTitle(card), getBoardRoot(...). providers/github-projects.js is the only one today; add another (e.g. Trello) to gamify a different board β€” see below.

Plugins are independent feature modules (sound, confetti, fireworks, glow, toast, hud). Each one subscribes to bus events through the registry and persists its own settings under its namespace. They never call each other directly β€” they react to events β€” which is exactly what makes them composable.


Building Your First Plugin

A plugin is a single folder under plugins/ containing an index.js that export defaults one plain object. Important: the plugin's index.js must have zero side effects at module top level β€” only define and export the object. (The popup imports it just to read the settings schema; importing must not touch audio, the DOM, or storage.)

Minimal scaffold

Create plugins/my-plugin/index.js:

// plugins/my-plugin/index.js
// No top-level side effects β€” only define and export the plugin object.

export default {
  id: 'my-plugin',          // unique; used in storage keys plugin:my-plugin:*
  name: 'My Plugin',        // shown as the section title in the popup
  enabled: true,            // default enabled state

  // Called once when the plugin is enabled. config = { settings, EventBus, Storage }.
  // Read live values via config.settings[key] (resolved from your schema defaults
  // overlaid with stored plugin:my-plugin:{key} values).
  init(config) {
    this.settings = config.settings;
    this.bus = config.EventBus;
    this.storage = config.Storage;
  },

  // Fired on every completion. eventData =
  // { cardTitle, columnName, timestamp, totalToday, allTime, jackpot }.
  // `jackpot` is true ~15% of the time β€” escalate your effect when it is.
  onTaskDone(eventData) {
    console.log('[my-plugin] done:', eventData.cardTitle, this.settings.message);
    if (eventData.jackpot) {/* go big */}
  },

  // Fired on GitHub SPA navigation. eventData = { url }.
  onRouteChange(eventData) {},

  // Tear down: remove DOM nodes, cancel timers, close resources.
  destroy() {},

  // Settings rendered automatically by the popup. Supported types:
  // 'range' | 'toggle' | 'select' | 'number' | 'text'.
  // Do NOT include an `enabled` field β€” the popup renders the master toggle
  // separately from plugin:my-plugin:enabled.
  getSettingsSchema() {
    return [
      { key: 'message', label: 'Message', type: 'text', default: 'Nice work!' },
      { key: 'intensity', label: 'Intensity', type: 'range', min: 0, max: 100, step: 1, default: 50 },
    ];
  },
};

Register it (two steps)

  1. Drop the folder in: plugins/my-plugin/index.js (as above).

  2. Add it to plugins/index.js:

    import sound from './sound/index.js';
    import confetti from './confetti/index.js';
    import toast from './toast/index.js';
    import hud from './hud/index.js';
    import myPlugin from './my-plugin/index.js';   // ← add the import
    
    export default [sound, confetti, toast, hud, myPlugin]; // ← add to the array

That's it. No core file edit and no manifest edit are needed β€” the manifest's web_accessible_resources already globs plugins/*/index.js, the registry wires your plugin to the bus automatically, and the popup auto-generates your settings section from getSettingsSchema(). Reload the extension on chrome://extensions and your plugin is live.


Supporting another board (e.g. Trello)

The detector is board-agnostic β€” all the site-specific bits live in a provider. To gamify a different kanban board:

  1. Create providers/<name>.js exporting an object shaped like providers/github-projects.js:

    export default {
      id: 'trello',
      name: 'Trello',
      matches(url = location.href) { /* is this a Trello board page? */ },
      findDoneColumn(doneName)     { /* return the Done list element, or null */ },
      findCards(columnEl)          { /* return the card elements in a list */ },
      cardIdentity(cardEl)         { /* a stable id string for a card */ },
      cardTitle(cardEl)            { /* a human-readable title */ },
      getBoardRoot(doneColumnEl)   { /* element to observe for mutations */ },
    };
  2. Register it in providers/index.js (add the import + array entry).

  3. Add the site to the manifest β€” its host in content_scripts[].matches and in web_accessible_resources[].matches (the only manifest edit needed; the providers/*.js resource glob already covers your new file).

Everything else β€” reconcile, celebrations, stats, the HUD, the popup β€” just works, and the HUD will show only on that board's pages.


Future plugin hooks (designed, not built)

The event bus is what makes inter-plugin reactions possible: any plugin can emit a namespaced event, and any other plugin can on(...) it β€” they never import or call each other. A plugin's init(config) receives config.EventBus, so it can both publish new events and subscribe to existing ones. These four are documented as natural next steps:

  • reaction-image β€” on task:done, overlays a random image for ~2s using eventData (e.g. card title) plus a user-supplied URL list defined in its settings schema (a text field of newline-separated URLs). Pure consumer of task:done.
  • pomodoro β€” subscribes to route:change to start/stop focus timers as you move around GitHub, and emits pomodoro:start / pomodoro:end on the bus. Because those are just bus events, other plugins (sound, toast, a future level-system) can react to them with no coupling to the pomodoro plugin itself.
  • streak-freeze β€” a game mechanic that consumes streak data from storage (core:dailyLog via Storage.getStats()) to let you "freeze" a streak so a missed day doesn't reset it. Reads core stats; writes its own plugin:streak-freeze:* state.
  • level-system β€” listens to task:done, accumulates XP, and emits player:levelup when you cross a threshold to trigger its own celebration (and any other plugin that chooses to listen). A clean example of one plugin's output becoming another plugin's input β€” entirely through the bus.

Why the bus matters: because publishing and subscribing are decoupled, a plugin like level-system can emit player:levelup without knowing or caring that confetti exists, and confetti could choose to celebrate level-ups by subscribing to player:levelup β€” without either plugin importing the other. New reactions are added by listening to events, not by editing existing plugins.


Settings / storage key reference

All persistence goes through core/storage.js into chrome.storage.local.

Core keys

Key Shape Meaning
core:doneCount number The ⚑ all-time figure = live count of cards currently in Done (a card entering Done raises it; leaving lowers it).
core:dailyLog { "YYYY-MM-DD": count, ... } Per-day net completion count (+1 per entry into Done, βˆ’1 per exit, clamped β‰₯ 0), keyed by local date. Drives today + streak.
core:doneColumnName string (default "Done") Which column header marks a card as done (case-insensitive).
core:totalCompleted number Legacy mirror of core:doneCount (kept for compatibility).

Counting model: every move into Done is a completion β€” it celebrates and adds +1 to today and all-time, every time (a card moved out and back celebrates again). Every move out of Done subtracts 1. So the counts always mirror the live Done column. Derived stats from Storage.getStats(): { totalCompleted, dailyLog, today, streak } where totalCompleted is the live Done count and streak is the consecutive days (ending today or yesterday) each with at least one net completion. Reset stats clears core:dailyLog only (today/streak β†’ 0); the all-time count is preserved since it reflects the live Done column.

Plugin keys

Key pattern Example Meaning
plugin:{id}:enabled plugin:sound:enabled Master enable toggle for a plugin.
plugin:{id}:{key} plugin:sound:volume A single setting value for a plugin.

Built-in plugin settings (each also has its plugin:{id}:enabled flag):

Plugin Key Type Default
sound volume range 60
confetti particleCount range 100
confetti duration range 2.5
toast position select 'bottom-left'
toast holdDuration range 2.5
hud defaultPosition select 'top-right'
hud showStreak toggle true
hud showToday toggle true
hud showAllTime toggle true
hud x / y number (saved on drag)
hud collapsed toggle (saved on toggle)

Key builders, if you're writing storage code: Storage.coreKey(key) β†’ core:${key}, and Storage.pluginKey(pluginId, key) β†’ plugin:${pluginId}:${key}.


Permissions

The extension requests only storage. It runs content scripts on https://github.com/* and has no network, host, or background access beyond that.


License

Provided as-is for personal use. No external dependencies.

About

πŸŽ‰ Make working your kanban board fun & rewarding! Little sparks as you open and create cards, and a full party β€” confetti, fireworks, sounds & streaks β€” when you finish a task. A Chrome extension; works with GitHub Projects, built for any board.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors