@walangstudio/badgetrip-vue
| Install | |
|---|---|
npm install @walangstudio/badgetrip-vue |
|
| Latest Version: | 0.1.0 |
| Vue: | 3 |
| License: | MIT |
| Last Updated: | Oct 21, 2026 |
| Links: | GitHub · npm |
badgetrip
Points, streaks, tiers, leaderboards and achievements for any JavaScript app. You emit events for what your users did. The engine calculates their score, their streaks, and which achievements they just unlocked. Each achievement is shown as a badge.
const result = await engine.emit({ id, actor: 'ana', type: 'habit.done', ts: Date.now(), payload: {} });
result.unlocked; // ['first_step']
Status: early (0.x). APIs may change before 1.0. Changes are listed in the CHANGELOG.
Why
Every app with a streak or a badge rebuilds the same things: a points counter, a handful of unlock rules, a streak that resets, a leaderboard. The logic is often split across database triggers, API handlers and UI hooks.
badgetrip keeps all the rules in one place, and leaves the rest of your app to you:
- Your database. The engine talks to four small store interfaces. Implement them on Postgres, SQLite, KV or anything else, and badgetrip keeps its state next to your tables.
- Your framework. The core is plain TypeScript with no dependencies. Small adapter packages cover React, React Native, Vue, Angular, plain HTML, htmx, Electron and Tauri.
- Your clock. The engine never reads the system time on its own. Same events in, same state out, so replays and tests are deterministic.
Features
- Achievements as config. A keyed object with rule builders:
rules.count('habit.done', 10),rules.streak('daily', 7),rules.all(...). Tiers expand one entry into bronze/silver/gold. Hidden achievements stay concealed until earned. - Built-in progress.
engine.catalog(actor)returns every achievement, ready to display, with{ current, target, percent }. Theactoris the user an event belongs to. - Points, streaks, tiers, leaderboards, escalators. All-time and rolling leaderboards, per-key streaks, and capped severity escalators with decay.
- Idempotent. Events carry an id. A retry or a double click counts once.
- Validated up front.
createEnginerejects typos and impossible rules before the first event. - Swappable icons. 18 built-in SVG icons, including an animated one. Override any icon per achievement, per tier series or per category, with GIFs, PNGs or your own SVGs.
- Accessible badges. Every adapter shows a still frame while locked or when the user prefers reduced motion, and labels its progress bar.
- Unlock celebrations. Toasts (small popup cards) in any corner or edge, a modal, or fullscreen with confetti, with optional sound. Configure once, override per achievement. A hidden achievement shows as "Hidden achievement" until it's unlocked; secret mode goes further and leaves hidden achievements out of the list, showing only how many remain.
- Themes. One object sets colors, icons and celebrations. Swap it at runtime, extend a built-in (
classic,dark,arcade,minimal,aurora), or share yours as a package. Backgrounds and icons can be gradients. - Animations. Popups enter and leave the way you choose (fade, slide from any side, scale, pop or bounce), with their own speed and easing, per theme or per achievement. Badges can be GIFs. Everything stays still for people who prefer reduced motion.
- Progress you can see. Badges show "3/5" under locked achievements, and optional popups report progress along the way, every step or at milestones.
- Cross-process.
@walangstudio/badgetrip-ipcruns the engine in Electron's main process or a worker, protected by an allowlist of methods and anauthorizehook.
Try it
The playground is a sandbox: edit achievements, a theme, animations, sounds and popups in tabs, written just like the examples in these docs, then emit events and watch achievements unlock with the real celebrations. Nothing is saved. To run it locally:
pnpm install
pnpm --filter @badgetrip-example/playground dev
Install
npm install @walangstudio/badgetrip-core
npm install @walangstudio/badgetrip-react # or vue, angular, react-native, html
Quick start
import {
createEngine,
defineAchievements,
memoryAchievementStore,
memoryEventStore,
memoryScoreStore,
memoryStreakStore,
rules,
systemClock,
} from '@walangstudio/badgetrip-core';
const achievements = defineAchievements({
first_step: {
name: 'First step',
description: 'Log your first habit',
icon: 'sprout',
when: rules.count('habit.done', 1),
},
regular: {
name: 'Regular ({tier})',
description: 'Log {n} habits',
when: rules.count('habit.done'),
tiers: { bronze: 10, silver: 50, gold: 200 },
},
});
const engine = createEngine({
events: memoryEventStore(),
scores: memoryScoreStore(),
achievements: memoryAchievementStore(),
streaks: memoryStreakStore(),
clock: systemClock,
definitions: {
scores: ['xp'],
points: [{ on: 'habit.done', score: 'xp', delta: 10 }],
achievements,
},
});
const result = await engine.emit({
id: crypto.randomUUID(),
actor: 'ana',
type: 'habit.done',
ts: Date.now(),
payload: {},
});
result.unlocked; // ['first_step']
await engine.score('ana', 'xp'); // 10
await engine.catalog('ana'); // every badge with its progress
The memory stores are for demos and tests. For data that survives a restart, see Bring your own store.
Usage
The examples below build on one engine for a habit tracker. The comments show the real output.
Define the rules
const HOUR = 3_600_000;
const DAY = 24 * HOUR;
const engine = createEngine({
events: memoryEventStore(),
scores: memoryScoreStore(),
achievements: memoryAchievementStore(),
streaks: memoryStreakStore(),
clock: systemClock,
definitions: {
scores: ['xp'],
points: [{ on: 'habit.done', score: 'xp', delta: 10 }],
streaks: [
{ code: 'daily', tickEvents: ['day.completed'], resetEvents: ['day.missed'], scoping: 'per-actor' },
],
tiers: [
{
code: 'level',
score: 'xp',
thresholds: [
{ name: 'novice', at: 0 },
{ name: 'adept', at: 100 },
{ name: 'master', at: 500 },
],
},
],
leaderboards: [
{
code: 'weekly',
source: { kind: 'score', score: 'xp' },
window: { type: 'rolling', ms: 7 * DAY },
limit: 10,
},
],
achievements: defineAchievements({
first_step: {
name: 'First step',
description: 'Log your first habit',
icon: 'sprout',
points: 5,
when: rules.count('habit.done', 1),
},
regular: {
name: 'Regular ({tier})',
description: 'Log {n} habits',
icon: 'medal',
when: rules.count('habit.done'),
tiers: { bronze: 10, silver: 50, gold: 200 },
},
on_a_roll: {
name: 'On a roll',
description: 'Keep a 3-day streak',
icon: 'flame',
when: rules.streak('daily', 3),
},
night_owl: {
name: 'Night owl',
description: 'Log a habit between midnight and 4am',
lockedDescription: 'Some habits happen after dark',
icon: 'moon',
hidden: true,
when: rules.count('habit.done', 1, { todBetween: [0, 4 * HOUR] }),
},
}),
},
});
createEngine checks all of it up front. A typo like rules.streak('dialy', 3) throws here, not on the first event.
Record what happened
const result = await engine.emit({
id: 'evt_1',
actor: 'ana',
type: 'habit.done',
ts: Date.now(),
payload: {},
});
result.scoreDeltas; // [{ actor: 'ana', score: 'xp', delta: 10, reason: 'evt_1', ts }]
result.unlocked; // ['first_step']
The id makes it safe to retry. Emitting evt_1 again returns empty changes and counts nothing.
Streaks
Streaks count ticks, not calendar days. Emit day.completed once a day for each user who was active that day, and day.missed for those who weren't. Usually a nightly job emits these events.
// Ana's third day in a row
await engine.emit({ id: 'day_3', actor: 'ana', type: 'day.completed', ts: Date.now(), payload: {} });
// streakChanges: [{ code: 'daily', actor: 'ana', from: 2, to: 3 }], unlocked: ['on_a_roll']
await engine.streak('ana', 'daily'); // { current: 3, best: 3, lastTick }
Scores, tiers and leaderboards
Here a tier is a score level (novice, adept), not an achievement tier like bronze or gold.
await engine.score('ana', 'xp'); // 10
await engine.tier('ana', 'level'); // { current: 'novice', next: 'adept', remaining: 90, value: 10 }
await engine.leaderboard('weekly'); // after Ben logs two habits: [{ actor: 'ben', value: 20 }, { actor: 'ana', value: 10 }]
Achievements and progress
await engine.achievements('ana'); // [{ code: 'first_step', at }, { code: 'on_a_roll', at }]
await engine.progress('ana', 'regular.bronze'); // { current: 1, target: 10, percent: 10 }
const badges = await engine.catalog('ana');
catalog returns every achievement in definition order, ready to draw: code, name, description, icon, points, unlocked, and progress. regular shows up as three badges, regular.bronze to regular.gold. night_owl comes back as "Hidden achievement" with its lockedDescription until Ana earns it.
Changing the rules later
engine.refresh(actor)awards anything the actor already qualifies for. Run it after adding an achievement.engine.replay(log)rebuilds state from a stored event log, intsorder.engine.seed(snapshot)loads existing scores, achievements and streaks when moving from another system.
Celebrate unlocks
<UnlockNotifier actor="ana" sound celebrations={celebrations} />
Each unlock pops up on top of the page. Give an achievement celebration: 'epic' for fullscreen confetti, or 'modal', 'quiet', or your own preset. The celebrations guide covers positions, sounds, secret mode and styling.
Show it in your UI
Wrap the engine once with the package for your stack, and every badge updates after each emit. See Frameworks.
Frameworks
Start with Getting started, then pick your stack:
| Stack | Package | Guide |
|---|---|---|
| React | @walangstudio/badgetrip-react |
React |
| React Native / Expo | @walangstudio/badgetrip-react-native |
React Native |
| Vue 3 | @walangstudio/badgetrip-vue |
Vue |
| Angular 17.1+ | @walangstudio/badgetrip-angular |
Angular |
| Plain HTML, web components | @walangstudio/badgetrip-html |
HTML |
| htmx, server-rendered | @walangstudio/badgetrip-html |
htmx |
| Electron | @walangstudio/badgetrip-ipc + a UI package |
Electron |
| Tauri | a UI package, optionally @walangstudio/badgetrip-ipc |
Tauri |
In React:
import { AchievementBadge, BadgetripProvider, useAchievementCatalog } from '@walangstudio/badgetrip-react';
function Trophies({ userId }: { userId: string }) {
return useAchievementCatalog(userId).map((a) => <AchievementBadge key={a.code} achievement={a} />);
}
<BadgetripProvider engine={engine}>
<Trophies userId="ana" />
</BadgetripProvider>;
Packages
| Package | What it is |
|---|---|
@walangstudio/badgetrip-core |
The engine, rule builders, store interfaces and in-memory stores. No dependencies. |
@walangstudio/badgetrip-assets |
18 SVG icons and createIconResolver for overrides. No dependencies. |
@walangstudio/badgetrip-react |
Hooks, BadgetripProvider, AchievementBadge, IconProvider. |
@walangstudio/badgetrip-react-native |
The React hooks plus a native AchievementBadge on react-native-svg. |
@walangstudio/badgetrip-vue |
Plugin, composables returning { data, error }, AchievementBadge. |
@walangstudio/badgetrip-angular |
provideBadgetrip, a signal-based BadgetripService, a standalone badge component. |
@walangstudio/badgetrip-html |
renderBadge / renderCatalog HTML strings and <badgetrip-catalog> / <badgetrip-badge> custom elements. |
@walangstudio/badgetrip-ipc |
Serve an engine in one process and use it from another, over MessagePort or Electron IPC. |
@walangstudio/badgetrip-testing |
A steppable clock, event helpers, makeTestEngine, and runStoreContract for custom stores. |
Bring your own store
badgetrip ships no database adapters. Implement EventStore, ScoreStore, AchievementStore and StreakStore against the database you already use, and pass them to createEngine:
const engine = createEngine({
events: myEventStore(db),
scores: myScoreStore(db),
achievements: myAchievementStore(db),
streaks: myStreakStore(db),
clock: systemClock,
definitions,
});
Then prove it behaves like the reference stores:
import { runStoreContract } from '@walangstudio/badgetrip-testing';
runStoreContract(() => makeFreshStores());
docs/ADAPTERS.md spells out what each method must do, and examples/adapter-postgres is a complete Postgres implementation to copy. Saving data walks through the options.
How it works
Everything flows from events. For each emit, the engine:
- validates the event and queues it behind any in-flight event for the same actor,
- appends it to the event store, stopping if its id was already seen,
- applies point rules, then streak ticks and resets, then escalators,
- evaluates the achievements the user doesn't have yet and awards the ones that pass.
It returns what changed: score deltas, streak changes, escalations and newly unlocked codes. engine.replay(log) rebuilds the same state from a stored log. Details are in docs/ARCHITECTURE.md.
Documentation
The docs are published at walangstudio.github.io/badgetrip. The same files live in docs/.
| Doc | Covers |
|---|---|
| Guides | Getting started, then one guide per framework |
| Celebrations | Unlock popups, sound, confetti, secret mode |
| Themes | Swappable themes: colors, icons and celebrations in one object |
| Animations | Popup entrances and exits, animated badges, confetti, reduced motion |
| ACHIEVEMENTS.md | Achievement config, tiers, hidden badges, progress, icons |
| RULES.md | Every rule kind, filters and the path convention |
| ARCHITECTURE.md | Interfaces, data model, the emit pipeline, determinism |
| ADAPTERS.md | Writing a store for your database |
| CROSS_PLATFORM.md | Where each package runs and how that was verified |
| CHANGELOG.md | Release notes |
Examples
| Example | Shows |
|---|---|
examples/playground |
A browser sandbox: edit achievements, a theme, animations, sounds and popups in tabs, emit events, see the celebrations |
examples/node-cli |
The engine in a plain Node script |
examples/react-spa |
A Vite app on @walangstudio/badgetrip-react |
examples/todont-extract |
A production ruleset (12 badges) moved onto badgetrip |
examples/adapter-postgres |
A Postgres store that passes the contract suite |
Development
Requires Node 22 and pnpm 11.
pnpm install
pnpm build # tsc -b, then ng-packagr for Angular
pnpm test # vitest, then the Angular suite
pnpm typecheck # sources, tests and Angular specs
pnpm lint # biome
pnpm test:coverage # line and branch coverage
pnpm docs:dev # the docs site at localhost:5173/badgetrip/
Run the store contract against Postgres with a throwaway database:
BADGETRIP_PG_URL=postgres://... pnpm --filter @badgetrip-example/adapter-postgres test
Releasing
All packages share one version. Bump it in every package.json, add a dated ## [x.y.z] entry to the CHANGELOG, merge to main, then tag:
git tag -a vX.Y.Z -m vX.Y.Z
git push origin vX.Y.Z
The release workflow checks the tag against every package version and the changelog, runs the full gate, publishes to npm with provenance, and creates the GitHub release from the changelog entry. It needs an NPM_TOKEN repository secret.
License
Related Packages
Vue 3 composables and components to help integrate Alis Ideate into your product...
Version History
| Version | Released | Vue | License |
|---|---|---|---|
| 0.1.0 | ^3.4.0 | MIT | |
| 0.0.1 | ^3.4.0 | MIT |