Commit graph

82 commits

Author SHA1 Message Date
AyoubBenziza
e0ba4c2e23 feat(web): "What's new" patch notes modal
- curated, bilingual player-facing notes (lib/patch-notes.ts) — a public subset
  of CHANGELOG.md (which stays the technical source of truth)
- "Nouveautés" button on home with a dot badge when the app version changed
  since last seen (localStorage); opening marks the version seen
- app version injected at build via Vite define (__APP_VERSION__)
- add shadcn Dialog component (@workspace/ui, via radix-ui)

Verified: notes + version baked into the build; typecheck/lint/build green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 09:17:52 +02:00
AyoubBenziza
8d79a52a68 Merge branch 'release/0.1.2' into dev 2026-06-15 08:55:13 +02:00
AyoubBenziza
ea476da01e chore(release): 0.1.2
Optional Umami analytics (env-driven). See CHANGELOG.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 08:51:41 +02:00
89ce5e2c0b Merge pull request 'feat(web): optional Umami analytics (env-driven)' (#21) from feature/umami-analytics into dev
Reviewed-on: #21
2026-06-15 06:49:33 +00:00
AyoubBenziza
1bda0e122e feat(web): optional Umami analytics (env-driven)
- inject the Umami script at runtime only when VITE_UMAMI_SRC + VITE_UMAMI_WEBSITE_ID
  are set (so dev/local traffic isn't tracked); website-id is public, not a secret
- web Dockerfile passes them as build args; docker-compose.prod.yml + env examples
  document them (optional, empty = disabled)

Verified: build bakes the values in when set; skipped when unset.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 08:40:35 +02:00
AyoubBenziza
0d79bf3fa6 Merge branch 'release/0.1.1' into dev 2026-06-12 12:37:26 +02:00
AyoubBenziza
7116d63c81 chore(release): 0.1.1
Playtest fixes (DB PG* connection, blindtest sound/vote-status/tracks, quiz
input reset, clock sync) and additions (DJ can be contributor, 90s blindtest,
volume control, theme switcher). See CHANGELOG.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-12 12:33:41 +02:00
4497d697f7 Merge pull request 'fix/playtest-feedback' (#19) from fix/playtest-feedback into dev
Reviewed-on: #19
2026-06-12 10:30:49 +00:00
AyoubBenziza
7a41bbf8c1 feat: DJ can be contributor, volume, theme switch, clock sync, 90s blindtest
Playtest feedback round 2:
- blindtest DJ can now be anyone incl. the track's contributor (pickDj only
  excludes bots) — no more "immunity" for whoever isn't picked
- blindtest rounds last 90s (longer listen + typing)
- per-player volume slider (persisted), YT player setVolume + origin/enablejsapi
- theme switcher (light/dark/system) on home + room header
- clock sync: clients estimate the server-clock offset (time:sync) so the
  countdown is identical across devices (was off by each device's clock skew);
  also used for blindtest media sync

Note on duplicates: anti-repeat is per room code (quiz_played) — it works when
you replay in the SAME room, but a new room starts fresh. Verified e2e: two
chained games in one room share 0 questions.

Verified: typecheck 4/4, lint 4/4, 24 server tests, web build; e2e dedup (0
overlap) and clock/DJ behavior.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-12 00:39:02 +02:00
AyoubBenziza
6362b97508 fix(web): blindtest sound, hidden vote status, authoritative tracks, quiz input
From friends playtest feedback:
- blindtest "who added it?" (and mixed): hide the per-player vote status — the
  contributor never validates, which gave them away
- blindtest sound: set YT player `origin`/`enablejsapi` (fixes the postMessage
  origin warning) and add a per-game "Activer le son" gesture for non-DJ
  (browsers block audible autoplay without a user gesture)
- track list: render each player's own tracks from a server-sent, authoritative
  blindtest:myTracks event (emitted on submit/remove/reconnect) instead of local
  state that desynced on remount — fixes the "3/3 but only 2 shown" case
- quiz: reset the free-answer input between questions (was pre-filled with the
  previous answer)

Verified e2e: myTracks emitted on submit + reconnect.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-12 00:16:27 +02:00
AyoubBenziza
cfc5339a25 fix(server): connect via PG* vars (robust to special chars in password)
postgres-js failed to parse DATABASE_URL when the password contained URL-special
characters (@ : / # ...). New createSqlClient(): uses DATABASE_URL if set, else
falls back to PG* env vars (PGHOST/PGUSER/PGPASSWORD/...) which postgres-js reads
directly — password passed verbatim, no URL encoding. docker-compose.prod.yml now
sets PG* for the server instead of an inline DSN.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-11 23:54:56 +02:00
AyoubBenziza
65beeff4f0 Merge branch 'release/0.1.0' into dev 2026-06-11 20:39:41 +02:00
AyoubBenziza
9a57536258 chore(release): 0.1.0 — Dokploy deployment, docs, versioning
- Dockerfiles: server (Bun, runs migrations then starts) and web (Vite build
  served by nginx with SPA fallback); .dockerignore
- docker-compose.prod.yml (postgres + server + web) ready for Dokploy, with
  uploads/pgdata volumes and .env.production.example
- README: deployment (Dokploy) + gitflow/versioning sections, Images mode, bots,
  i18n; CHANGELOG.md
- bump packages to 0.1.0

Docker images built and validated end-to-end: server migrates + serves
/health + /api/categories; web serves the SPA with VITE_SERVER_URL baked in.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-11 20:33:02 +02:00
5d113e1d37 Merge pull request 'feat(web): i18n (FR/EN) — homemade typed dictionary' (#17) from feature/i18n into dev
Reviewed-on: #17
2026-06-11 17:07:50 +00:00
AyoubBenziza
1e5ef2be90 feat(web): i18n (FR/EN) — homemade typed dictionary
- lightweight, dependency-free i18n: typed fr/en dictionaries (fr is the source
  of truth, en must match its shape → compile error on drift), I18nProvider +
  useI18n hook, browser detection + localStorage persistence
- FR/EN language switcher (home + room header)
- translated the whole player-facing flow: home, join, pseudo, room shell,
  lobby (incl. settings, categories, bots, track submission, history),
  quiz, blindtest, player-cards, round transitions, end screen (podium, awards,
  tracks, rounds recap), and server-error messages (mapped by code)
- history dates localized to the active language

Content (questions/categories) intentionally left as-is. Back-office (admin)
not translated yet — internal, token-gated tool.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-11 19:03:28 +02:00
5dcd7ac978 Merge pull request 'fix(ui): move buttonVariants to its own file (lint clean)' (#16) from fix/button-variants-lint into dev
Reviewed-on: #16
2026-06-11 16:31:58 +00:00
AyoubBenziza
8891599fa5 fix(ui): move buttonVariants to its own file (lint clean)
The react-refresh rule flagged button.tsx for exporting a non-component
(buttonVariants) alongside the Button component. Moved the cva to
button-variants.ts (imported by button.tsx); button.tsx now exports only the
component. `bun run lint` is green across all workspaces.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-11 18:30:30 +02:00
7290f02dad Merge pull request 'feature/bots' (#15) from feature/bots into dev
Reviewed-on: #15
2026-06-11 16:21:59 +00:00
AyoubBenziza
47e5c2854e fix(web): center end-screen recap + shadcn tooltips
- results page: when there are no tracks (quiz-only game), the recap was sitting
  in the left column of a 2-col grid; now a single section is centered (2 columns
  only when both tracks and recap exist)
- add a shadcn Tooltip component (@workspace/ui, via radix-ui) and replace native
  browser title= tooltips: connection dot, back-office active toggle, and the
  locked blindtest tiles (now aria-disabled so the tooltip still triggers)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-11 18:17:47 +02:00
AyoubBenziza
42a36fc442 feat: blindtest needs 2 real players + bot difficulty setting
- blindtest is only available with >=3 players AND >=2 humans (a bot can't be DJ):
  lobby gate + tooltips updated, and server game:start rejects otherwise
- new botDifficulty setting (easy/normal/hard) drives how often bots answer
  correctly (quiz + blindtest who-added / title-artist); host picks it in the
  lobby when at least one bot is present

Verified e2e: blindtest start rejected with 1 human + 2 bots (NEED_THREE).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-11 18:03:03 +02:00
AyoubBenziza
8eac8d0123 feat: bots — virtual players to fill a game
- server-side bot players (isBot): host adds/removes them in the lobby
  (lobby:addBot / lobby:removeBot), capped at 8 players
- bots auto-vote each round: GameRound.botAnswer generates a plausible answer
  (quiz ~50% right, blindtest guesses a random player / title); engine schedules
  the votes within the answer window and triggers early end when all have voted
- bots are never DJ (pickDj excludes them) and are excluded from the blindtest
  track-submission gate (they don't submit)
- snapshot players carry `bot`; lobby shows a 🤖 marker + add/remove controls

Verified e2e: 2 bots added, they vote, game ends with all 3 players scored
and rounds end early.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-11 17:48:33 +02:00
4154509fdd Merge pull request 'feature/reconnect' (#14) from feature/reconnect into dev
Reviewed-on: #14
2026-06-11 15:35:40 +00:00
AyoubBenziza
f0c327617b feat: leave-room button
- room:leave event: removes the player from the room (not just disconnected),
  reassigns host if the host leaves, deletes the room when empty
- store leaveRoom() emits it and clears the session (reset)
- room header gets a "Quitter" button (always available); end-screen "Quitter"
  now also leaves cleanly

Verified e2e: leaver removed from snapshot, host reassigned to the next player.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-11 17:33:10 +02:00
AyoubBenziza
6ed99ea803 feat: reconnection — keep your place across refresh / network drop
- each player gets a reconnectToken; room:create/join return it
- new room:rejoin event: re-attaches the existing player (by id+token) to the
  new socket, broadcasts player:rejoined + room:state, and resyncs the current
  round to that socket (engine.resync re-emits round:start with their view)
- client persists the session (roomCode/playerId/token/name) in localStorage;
  bootstraps the store from it and auto-rejoins on every socket connect;
  clears it on reset/quit or failed rejoin

Verified e2e: wrong token rejected, correct token re-attaches (player present
+ connected).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-11 17:26:22 +02:00
4225b7b43e Merge pull request 'feat: game history + question lang/active management' (#13) from feature/history into dev
Reviewed-on: #13
2026-06-11 15:16:01 +00:00
AyoubBenziza
b3efa27287 feat: game history + question lang/active management
DB:
- quiz_question gains `lang` ('fr' default) and `active` (true default); migration 0002
- only active questions are served to games (pool filter)

History:
- engine writes a game_history row at game end (modes + ranked results), fire-and-forget
- public GET /api/history/:code lists a room's recent games
- lobby shows a "Parties précédentes" collapsible (date, modes, winner/scores)

Back-office:
- create form has a FR/EN language select; lang stored
- per-question active toggle (PATCH /questions/:id) — disable without deleting;
  inactive rows dimmed and labelled
- seeds set lang (opentdb=en, manual/pokemon=fr)

Verified e2e: migration applied; game played → /api/history returns it.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-11 17:12:07 +02:00
ed1747543b Merge pull request 'feature/mixed-settings-restyle' (#12) from feature/mixed-settings-restyle into dev
Reviewed-on: #12
2026-06-11 13:41:59 +00:00
AyoubBenziza
b91d09b7e7 feat: category filter in lobby settings
- settings carry `categories` (empty = all); quiz/image pool filters by category
  (DB + in-code bank), threaded through prepareQuizForRoom/loadGroup/loadQuizPool
- public GET /api/categories lists available categories
- lobby: category chips (Toutes + each), shown when quiz/images is active
- shared: RoomSettings.categories + UpdateSettingsPayload

Verified e2e: /api/categories returns the list; filtering by ["Pokémon"] serves
only Pokémon questions.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-11 15:37:33 +02:00
AyoubBenziza
fa7c97c402 feat(web): richer desktop layout for end screen and back-office
- end screen: wider container (max-w-4xl), podium/awards stay centered, and the
  two long sections (musiques + récap des manches) sit side-by-side on desktop
- back-office: 2-column layout (form | questions list) on desktop, wider

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-11 15:24:34 +02:00
AyoubBenziza
879bc5b388 feat: per-submode mixed settings, fastest award, wider desktop layout
Mixed settings fix:
- RoundConfig carries a `pool` hint (quiz | image) per quiz round; lobby shows
  a separate count for each selected sub-mode (Quiz, Images) and the blindtest
  config — not just the quiz one
- server pool composes the queue per round's pool (quiz formats vs image_reveal),
  in order; verified e2e (2 quiz + 2 images honored)

Awards:
- " Le plus rapide" — derived client-side from the speed bonus baked into
  quiz/image points (points above the 100 base on correct answers)

Desktop restyle:
- room page gets an ambient halo background and a responsive container
  (lobby max-w-3xl with a 2-column players|settings grid, game/end max-w-lg)
- game/end views widened to max-w-lg

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-11 15:10:09 +02:00
20e1617b3d Merge pull request 'feature/round-recap' (#11) from feature/round-recap into dev
Reviewed-on: #11
2026-06-10 23:14:58 +00:00
AyoubBenziza
976ebf0799 feat(web): end-game awards, copy results, avatars in recap
- FunStats: award badges from the recap data — 🎯 Sans-faute, 🧠 Le cerveau
  (most correct), 🦹 Roi de la tromperie (blindtest decoy points), 🪨 Le boulet
  (0 correct). Shown with player avatars.
- "Copier les résultats" button: copies ranking + per-round answers to clipboard
- avatars next to each player in the round-by-round recap

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-11 01:11:06 +02:00
AyoubBenziza
331d375349 feat(web): recap polish — sort answers by points, highlight you, found count
- per round, answers sorted by points (then correct first)
- the local player's line is bolded with "(toi)"
- per-round "✓ X" badge showing how many got it right

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-11 01:05:14 +02:00
AyoubBenziza
f7ee2204d1 feat: round recap shows each player's answer
- shared: RoundRecap gains answers[] (RoundPlayerAnswer: text + correct)
- server: quiz/blindtest recap() build per-player answers (choice text / free
  text / blindtest guess formatted by mode), with correctness
- client: recap lists each player's answer per round, green/red by correctness,
  with their points when they scored

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-11 01:00:50 +02:00
AyoubBenziza
03a0dfa998 feat: end-of-game round recap (all rounds with answers + scorers)
- shared: RoundRecap (+ scorers); game:end carries recap[]
- server: GameRound.recap(ctx) per mode (quiz: prompt + correct answer +
  image; blindtest: "title — artist" + addedBy + links); engine accumulates a
  history per round (with that round's scorers from the score deltas) and sends
  it at game:end
- client: GameEndView shows a collapsible "Récapitulatif des manches" — each
  round's label, answer, media thumbnail (image/youtube) and who scored
- engine test dummy round implements recap

Verified e2e: game:end recap lists every round with answer + scorers.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-11 00:54:40 +02:00
cb50ef390e Merge pull request 'feature/image-reveal' (#10) from feature/image-reveal into dev
Reviewed-on: #10
2026-06-10 22:38:34 +00:00
AyoubBenziza
5e96182489 feat(web): dedicated transition for the image mode
image_reveal rounds (RoundType "quiz") now get their own "IMAGE" transition
(emerald/teal theme, 🖼️) instead of reusing the quiz one. The store derives a
roundKind (quiz | image | blindtest) from the round payload format; mode-change
detection and the custom-media folder use it (assets/transitions/image/).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-11 00:35:32 +02:00
AyoubBenziza
8bc184fab7 feat: bigger image_reveal, optional image prompt, mixed sub-mode toggles, Pokémon seed
- image_reveal display: larger, object-contain (whole image up to 58vh), no crop
- image prompt optional in back-office (defaults to "Qui est-ce ?"), overridable
- mixed mode reworked: select "Mixte" then toggle sub-modes (Quiz/Images/
  Blindtest). Blindtest sub-mode locked under 3 players; mixed itself no longer
  needs 3. Server quiz pool formats derived from gameType + mixedModes.
  shared: MixMode + RoomSettings.mixedModes (default quiz+image).
- DB: prompt uniqueness is now a partial index (excludes image_reveal) + unique
  index on image_url; lets many images share the generic prompt, dedup by URL.
- seed: db:seed:images pulls Pokémon (PokéAPI) as image_reveal questions with
  official artwork (remote URL) + FR/EN accepted answers; idempotent by image_url.

Verified: migration applied, Pokémon seed inserts FR+EN, image mode serves only
image_reveal, all checks green (23 tests).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-11 00:20:28 +02:00
AyoubBenziza
9c1413744d fix(admin): ensure uploads dir exists before writing (resilient upload)
The upload route only relied on the startup mkdir; if the dir is removed while
the server runs, writes failed with ENOENT. Now mkdir -p before each write.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-10 23:43:47 +02:00
AyoubBenziza
62b536cc2b feat: "Images" as a distinct mode, refined lobby settings, upload button, app title
- image_reveal becomes its own game mode: GameType gains "image"; the quiz
  pool is filtered by allowed formats derived from gameType (image →
  image_reveal only, quiz → mcq/truefalse/free, mixed → all). loadQuizPool
  takes a formats list; pool fallback respects it.
- lobby settings reworked into a refined "Réglages" card: 4 mode tiles
  (icon + label, locked <3 players for blindtest/mixed), question count as a
  stepper, blindtest config grouped; dynamic label for image mode + hint.
- admin: real upload button (hidden input + styled Button with Upload icon)
  instead of the bare file input.
- app title set to "NerdWare …" in index.html (+ lang fr, description).

Verified e2e: image mode serves only image_reveal questions (with imageUrl).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-10 23:31:30 +02:00
AyoubBenziza
0619c02cdf feat(quiz): image_reveal format (progressive blur) end-to-end
Server:
- quiz-round handles image_reveal like a text-answer format (tolerant match
  on acceptedAnswers), exposing imageUrl in the round payload; degressive
  scoring comes for free from the existing speed bonus (earlier = blurrier =
  more points)
- repo includes image_reveal in the pool (requires imageUrl + acceptedAnswers)
- back-office: image upload (@fastify/multipart) saved to UPLOADS_DIR and
  served statically on /uploads (@fastify/static); create supports image_reveal
- env: UPLOADS_DIR; gitignore uploads/

Client:
- admin form: "Image à deviner" — upload + preview + accepted answers
- in-game: ImageReveal shows the image with a time-cadenced decreasing blur
  (synced via server timestamps) + text input; reveal shows the clear image
  and the answer

shared: QuizQuestionPayload.imageUrl

Verified against real server+DB: upload (url), static serve (200), create
(201), validation (400).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-10 21:41:34 +02:00
8b03879ee0 Merge pull request 'feature/polish' (#9) from feature/polish into dev
Reviewed-on: #9
2026-06-10 19:22:05 +00:00
AyoubBenziza
db0100e272 feat(web): victory sound on the end screen (drop-in)
- lib/sound: plays src/assets/sounds/victory.{mp3,ogg,wav} if present,
  auto-detected via import.meta.glob; no-op when absent
- played alongside the confetti when the end screen appears
- README documenting where to drop the file

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-10 21:17:25 +02:00
AyoubBenziza
e490f23dec feat(web): screen transitions + confetti when the lead changes
- RoomPage animates between coarse phases (lobby ↔ game ↔ end) with a fade/
  slide (AnimatePresence, mode wait). Keyed by phase, not status, so the
  YouTube player survives in_round → reveal → scores.
- PlayerCards fires a small confetti burst when a player overtakes to take
  the lead during the game.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-10 18:57:49 +02:00
AyoubBenziza
7093ce8cd9 feat(web): victory confetti on the end screen
canvas-confetti burst (center pop + side jets) fired once when the game-end
screen appears.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-10 17:54:23 +02:00
604c368184 Merge pull request 'feature/spotify-export' (#8) from feature/spotify-export into dev
Reviewed-on: #8
2026-06-10 15:49:13 +00:00
AyoubBenziza
b3b6b056eb feat(web): propagate YouTube brand icon to the lobby track submission
- "Ajouter" button shows the SiYoutube logo
- each submitted track card gets a YouTube open link (verify the pasted song)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-10 17:38:50 +02:00
AyoubBenziza
3f36798207 feat(web): brand icons (Spotify/YouTube) via react-simple-icons
Add @icons-pack/react-simple-icons for brand logos lucide doesn't provide.
End-screen track recap now uses official SiSpotify (green) and SiYoutube (red)
icons instead of generic ones. Lucide stays for UI icons.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-10 17:35:20 +02:00
AyoubBenziza
c0a4340bae feat(web): Spotify export on the end screen (roadmap V1 step 8)
From the blindtest tracks revealed at game end:
- per-track Spotify search link (green button) built from "title artist"
- "Playlist" button copies the whole tracklist ("title — artist" per line)
  to the clipboard for pasting into Spotify
- YouTube open link kept alongside

Pure client-side, derived from the tracks already sent in game:end.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-10 17:31:38 +02:00
04850bf749 Merge pull request 'feat(admin): manual quiz back-office + rewrite README' (#7) from feature/quiz-backoffice into dev
Reviewed-on: #7
2026-06-10 15:25:44 +00:00