Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
7576762
Dynamic badges/achievements system with tiered milestones (#127)
JanMikes Apr 16, 2026
43257a8
Fix null player_id from team JSON in backfill query
JanMikes Apr 16, 2026
99e2a99
Add integration tests for badge query layer
JanMikes Apr 16, 2026
8f0dfba
Separate badge persistence from email notification
JanMikes Apr 16, 2026
0240f17
Show only the highest tier per badge type on profile and email
JanMikes Apr 17, 2026
37849d6
Brainstorming
JanMikes Jul 12, 2026
27d8ce5
Tests: Fix PHPStan and coding standard violations
JanMikes Jul 13, 2026
a0f416e
XP/Levels: Executable implementation plan for the coding agent
JanMikes Jul 13, 2026
b825977
XP/Levels: Plan continues on badges branch, flag retrofit for shipped…
JanMikes Jul 13, 2026
1d0431a
XP/Levels: Plan review fixes — ledger decomposition, earned_at semant…
JanMikes Jul 13, 2026
f68e083
XP/Levels: Rename opt-out to experienceSystemOptedOut, add design qua…
JanMikes Jul 13, 2026
c727e8c
XP/Levels P0: XpFeatureGate admin-only flag, retrofit onto shipped ba…
JanMikes Jul 13, 2026
0cbf91b
XP/Levels P1: XP domain core — ledger entity, level curve, pure calcu…
JanMikes Jul 13, 2026
d1691fd
XP/Levels P2: live wiring — award on add, chain rebuild on edit, comp…
JanMikes Jul 13, 2026
84fbb92
XP/Levels P3: 11 new achievements — metrics, conditions, tests, EN co…
JanMikes Jul 13, 2026
e4beaae
XP/Levels P4: solve-loop UI — receipt, celebrations, rings, badge rev…
JanMikes Jul 13, 2026
73da2a0
XP/Levels: EN copy drafts for explainer + fair-play pages (pending Ja…
JanMikes Jul 13, 2026
e4ce389
XP/Levels P5: pages — achievements catalog+holders, XP leaderboard, a…
JanMikes Jul 13, 2026
86c1d4d
XP/Levels P6: weekly content digest — pipeline, blocks, preferences, …
JanMikes Jul 13, 2026
e82e788
XP/Levels P7: launch tooling — backfill orchestration, distribution v…
JanMikes Jul 13, 2026
07c22e2
XP/Levels P8: hardening, i18n, docs — leak test, anti-abuse tests, 5-…
JanMikes Jul 13, 2026
02fcf8e
XP/Levels: denormalize achievement points + leaderboard indexes
JanMikes Jul 13, 2026
8de69db
XP/Levels: correct pending-migration count in launch checklist (7)
JanMikes Jul 16, 2026
361e821
Badges: ComfyUI generation pipeline research — model stacks, MPS rule…
JanMikes Jul 16, 2026
412b6ae
Badges: ComfyUI environment ready — models downloaded, nodes verified
JanMikes Jul 17, 2026
4876187
Badges: validated ComfyUI generation pipeline — Klein 9B + determinis…
JanMikes Jul 17, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,7 @@ Feature design documents and implementation plans are in `docs/features/`. Each
- **Stripe Payments**: `docs/features/stripe.md` — Stripe integration for premium membership
- **Opt-Out Features**: `docs/features/opt-out.md` — Streak and ranking opt-out for players
- **Competitions Management**: `docs/features/competitions-management/` — Community-driven event creation with admin approval, round management, puzzle assignment, table layout planning, and live stopwatch
- **XP / Levels / Achievements**: `docs/features/xp-levels/` — XP ledger + level curve (1–50), 16 tiered achievements with Achievement Points, weekly content digest, launch runbook. Feature-flagged (`xp-system`, admin-only) until launch; living doc in `docs/features/xp-levels/README.md`
- **Referral Program**: `docs/features/referral-program.md` — Members earn 10% of referred subscription revenue. No separate entity — `player.referralProgramJoinedAt` + `player.referralProgramSuspended`. Code = player code. Cookie-based + code-input attribution. Payouts per currency, manual admin payout marking

### Feature Flags
Expand Down
41 changes: 41 additions & 0 deletions assets/controllers/badge_reveal_controller.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { Controller } from '@hotwired/stimulus';

/**
* First-click badge reveal: flips the medallion with a small confetti burst and
* persists the reveal. Fire-and-forget POST — the flip is optimistic.
*/
export default class extends Controller {
static values = {
url: String,
};

reveal() {
if (this.element.classList.contains('is-revealed')) {
return;
}

this.element.classList.add('is-revealed');

fetch(this.urlValue, { method: 'POST' });

this.burstConfetti();
}

burstConfetti() {
if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) {
return;
}

const confetti = document.createElement('span');
confetti.className = 'xp-confetti xp-confetti-burst';
confetti.setAttribute('aria-hidden', 'true');

for (let i = 0; i < 10; i++) {
confetti.appendChild(document.createElement('i'));
}

this.element.appendChild(confetti);

setTimeout(() => confetti.remove(), 3000);
}
}
34 changes: 34 additions & 0 deletions assets/controllers/xp_count_controller.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { Controller } from '@hotwired/stimulus';

/**
* One-shot XP counter spin for the launch reveal page: counts from 0 to the
* server-rendered value. Reduced motion → the real number shows instantly
* (it is already in the markup for no-JS visitors anyway).
*/
export default class extends Controller {
static values = {
amount: Number,
duration: { type: Number, default: 1800 },
};

connect() {
if (window.matchMedia('(prefers-reduced-motion: reduce)').matches || this.amountValue <= 0) {
return;
}

const start = performance.now();
const formatter = new Intl.NumberFormat('en-US');

const tick = (now) => {
const progress = Math.min((now - start) / this.durationValue, 1);
const eased = 1 - Math.pow(1 - progress, 3);
this.element.textContent = formatter.format(Math.round(eased * this.amountValue));

if (progress < 1) {
requestAnimationFrame(tick);
}
};

requestAnimationFrame(tick);
}
}
24 changes: 24 additions & 0 deletions assets/controllers/xp_reveal_finish_controller.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { Controller } from '@hotwired/stimulus';

/**
* Launch-reveal "continue" button: persists the one-time-seen marker via the
* dismiss-hint endpoint, then moves on to the profile.
*/
export default class extends Controller {
static values = {
url: String,
redirect: String,
};

async finish() {
try {
await fetch(this.urlValue, {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: 'type=xp_launch_reveal',
});
} finally {
window.location.assign(this.redirectValue);
}
}
}
1 change: 1 addition & 0 deletions assets/styles/_components.scss
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
@import 'components/breadcrumb';
@import 'components/pagination';
@import 'components/badge';
@import 'components/xp';
@import 'components/alert';
@import 'components/list-group';
@import 'components/close';
Expand Down
108 changes: 108 additions & 0 deletions assets/styles/components/_badge.scss
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,111 @@
box-shadow: 0 .5rem 1.125rem -.275rem rgba($black, .25);
}
}


// Achievement badge medallions
// --------------------------------------------------

$badge-tier-bronze: #cd7f32;
$badge-tier-silver: #c0c0c0;
$badge-tier-gold: #ffd700;
$badge-tier-platinum: #e5e4e2;
$badge-tier-diamond: #b9f2ff;
$badge-tier-supporter: $primary;

.badge-medallion {
width: 72px;
height: 72px;
border-radius: 50%;
display: inline-flex;
align-items: center;
justify-content: center;
color: rgba(40, 40, 40, .95);
font-weight: 700;
font-size: 1.25rem;
box-shadow: 0 4px 14px -2px rgba(0, 0, 0, .25), inset 0 -4px 6px rgba(0, 0, 0, .12);
border: 3px solid rgba(255, 255, 255, .7);
transition: transform .2s ease, box-shadow .2s ease;

&:hover {
transform: translateY(-1px);
box-shadow: 0 8px 20px -2px rgba(0, 0, 0, .3), inset 0 -4px 6px rgba(0, 0, 0, .12);
}
}

.badge-medallion-sm {
width: 48px;
height: 48px;
font-size: .95rem;
}

.badge-medallion-tier {
font-family: Georgia, serif;
letter-spacing: .02em;
text-shadow: 0 1px 0 rgba(255, 255, 255, .45);
}

.badge-tier-bronze {
background: radial-gradient(circle at 35% 30%, lighten($badge-tier-bronze, 18%), $badge-tier-bronze 70%);
color: #3a1d00;
}

.badge-tier-silver {
background: radial-gradient(circle at 35% 30%, lighten($badge-tier-silver, 12%), $badge-tier-silver 70%);
color: #2a2a2a;
}

.badge-tier-gold {
background: radial-gradient(circle at 35% 30%, lighten($badge-tier-gold, 10%), $badge-tier-gold 70%);
color: #3f2d00;
}

.badge-tier-platinum {
background: radial-gradient(circle at 35% 30%, lighten($badge-tier-platinum, 4%), $badge-tier-platinum 70%);
color: #2a2a2a;
}

.badge-tier-diamond {
background: radial-gradient(circle at 35% 30%, lighten($badge-tier-diamond, 4%), $badge-tier-diamond 70%);
color: #004854;
}

.badge-tier-supporter {
background: radial-gradient(circle at 35% 30%, lighten($badge-tier-supporter, 15%), $badge-tier-supporter 70%);
color: #fff;
}

.badge-locked {
.badge-medallion {
filter: grayscale(100%);
opacity: .45;
box-shadow: none;

&:hover {
transform: none;
}
}
}

.badge-new-pill {
position: absolute;
top: -6px;
right: -8px;
font-size: .65rem;
padding: .2em .45em;
box-shadow: 0 2px 6px rgba(0, 0, 0, .25);
animation: badge-new-pulse 1.6s ease-in-out infinite;
}

@keyframes badge-new-pulse {
0%, 100% {
transform: scale(1);
}
50% {
transform: scale(1.08);
}
}

.badge-earned {
min-width: 90px;
}
Loading