Skip to content

v4: accounts, game modes, semantic explorer, UX polish#191

Merged
Hugo0 merged 6 commits into
mainfrom
v4
Apr 11, 2026
Merged

v4: accounts, game modes, semantic explorer, UX polish#191
Hugo0 merged 6 commits into
mainfrom
v4

Conversation

@Hugo0
Copy link
Copy Markdown
Owner

@Hugo0 Hugo0 commented Apr 11, 2026

Summary

Major feature branch with accounts system, new game modes, and UX polish. Work by multiple agents.

New Features

  • Accounts & auth: Google OAuth, passkey login, user profiles, badge system
  • Semantic Explorer: meaning-based word game with UMAP map, compass hints, LLM oracle
  • Daily/Unlimited system: all modes support daily (same word for everyone) and unlimited play
  • Speed Streak: daily mode with deterministic word sequences
  • Profile page: tabbed stats (Overview/Distribution/Languages/Speed), streak calendar with mode icons + language flags
  • Streak modal: accessible from every page via navbar badge, shared calendar component
  • SharedStreakCalendar: DRY 28-day heatmap with per-day mode icons + language flags

UX Fixes (latest commit)

  • Mobile navbar: max 2 icons per side (profile + settings hidden, accessible via sidebar)
  • Trophy → BarChart2 icon for game results toggle
  • Sidebar statistics → ChartNoAxesCombined icon
  • Streak badge opens modal on all pages (was navigating to /profile on non-game pages)
  • StreakModal: sign-in CTA when logged out
  • SettingsModal: "Sign in with Google" → login modal (DRY)
  • Remove non-functional "Add Passkey" from profile
  • Homepage: remove misleading per-card streak from Continue Playing
  • Semantic modal: Close → Play Again, share "Copied!" feedback, remove "Your Path"
  • Semantic mobile: scrollIntoView on keyboard open, dvh for map height

Infrastructure

  • Prisma externalized from Nitro bundle (CJS/ESM fix)
  • CI: pre-built server for tests, increased timeout
  • User-scoped localStorage, sync plugin rewrite

Test plan

  • Mobile: verify navbar has max 2 icons per side on all pages
  • Streak badge opens modal (not profile navigation) on homepage + game pages
  • Settings modal "Sign in" opens login modal (not direct Google auth)
  • Profile page: no "Add Passkey" button visible
  • Semantic Explorer: Play Again button works, share shows "Copied!" feedback
  • Streak modal shows sign-in CTA when logged out
  • Continue Playing cards on homepage show no per-card streak numbers

@coderabbitai full review

Summary by CodeRabbit

  • New Features

    • Streak modal moved to a dedicated, dismissible modal with an improved 28-day calendar component.
  • Improvements

    • “Keep Playing” option to switch between daily and unlimited play.
    • Streamlined sign-in flow using a unified login modal.
    • Updated icons and cleaner mobile header/sidebar layout.
    • Improved calendar visuals across profile and modals.
  • Documentation

    • CONTRIBUTING: clarified rule to avoid direct pushes to main.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 11, 2026

Warning

Rate limit exceeded

@Hugo0 has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 20 minutes and 38 seconds before requesting another review.

Your organization is not enrolled in usage-based pricing. Contact your admin to enable usage-based pricing to continue reviews beyond the rate limit, or try again in 20 minutes and 38 seconds.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: e741a5a9-83e1-49ed-b315-dd4c4ea98180

📥 Commits

Reviewing files that changed from the base of the PR and between 981e633 and 42b97e5.

📒 Files selected for processing (1)
  • components/shared/StreakCalendar.vue
📝 Walkthrough

Walkthrough

Adds an “unlimited” play mode end‑to‑end, extracts a reusable 28‑day streak calendar, updates stats and streak modal wiring, tweaks UI icons/layout, adjusts auth/login flows, and changes build/Nitro/Rollup and CI/test tooling. No exported API signatures were broadly altered besides one composable param and a modal prop/event.

Changes

Cohort / File(s) Summary
Play mode (client → server)
composables/useSemanticGame.ts, pages/[lang]/semantic.vue, pages/[lang]/speed.vue, server/api/[lang]/semantic/start.post.ts
Added optional play parameter through pages → composable → POST body; pages now remount by play query; server chooses random target when play === 'unlimited'.
Streak calendar & UI refactor
components/game/StreakModal.vue, components/shared/StreakCalendar.vue, pages/profile.vue
Extracted 28‑day heatmap into SharedStreakCalendar.vue; removed inline calendar and language-wins UI; replaced usages in StreakModal and profile; updated day detail tracking types.
Header / Sidebar / Icons
components/app/AppHeader.vue, components/app/AppSidebar.vue, components/app/SidebarItem.vue
Adjusted subtitle rendering, swapped icons (Trophy→BarChart2, Statistics→ChartNoAxesCombined), hid Profile/Settings on mobile, and registered new icon in icon map.
Modal & shell behavior
components/app/AppShell.vue, components/game/SettingsModal.vue, components/semantic/SemanticStatsModal.vue
AppShell introduces showStreakModal and GameStreakModal; SettingsModal now opens login modal via openLoginModal() and emits close; SemanticStatsModal gains isDaily prop and keepPlaying emit, plus share "Copied!" UX.
Profile page polish
pages/profile.vue
Removed passkey/WebAuthn registration and UI; replaced inline calendar with shared component; added post-render scroll-to-hash; adjusted per-mode sorting.
Site index & small UI
pages/index.vue, components/app/AppHeader.vue
Removed streak counts from continue-cards; changed PWA inject default; minor subtitle rendering.
Build, bundling & CI
nuxt.config.ts, package.json, .github/workflows/test.yml, vitest.config.ts, server/utils/prisma.ts
Added Nitro/Rollup externals for Prisma, removed prisma generate from build script, added CI Prisma generate step and CI env var, simplified reactivity alias for Vitest, and added comments about Prisma import behavior.
Types & utilities
utils/streak-dates.ts
Extended DayDetail with wonModes and wonLangs and updated buildDailyResultMapDetailed to populate them.
Tests / tooling minor
tests/setup-server.ts
Refactored server start command formatting; no behavior change.
Contributing docs
CONTRIBUTING.md
Added rule prohibiting direct pushes to main without explicit permission (notes auto-deploy and outage risk).

Sequence Diagram(s)

sequenceDiagram
  participant Browser
  participant Page as SemanticPage
  participant Composable as useSemanticGame
  participant Server
  Browser->>Page: user selects play=?unlimited
  Page->>Composable: sem.startGame({ play: "unlimited", ... })
  Composable->>Server: POST /api/{lang}/semantic/start { play: "unlimited", ... }
  Server->>Server: if play==="unlimited" -> pick random target
  Server-->>Composable: 200 { target,... }
  Composable-->>Page: resolves game state
  Page-->>Browser: render unlimited game
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Poem

🐰 I hopped through code, a calendar in paw,
Sprinkled modes and flags for all to applaud.
Unlimited fields where targets roam free,
Shared tiles now dancing for you and me.
Merge the branch — carrot cake for tea! 🥕✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title references accounts, game modes, semantic explorer, and UX polish, which are all major themes present across the changeset.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch v4

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@Hugo0
Copy link
Copy Markdown
Owner Author

Hugo0 commented Apr 11, 2026

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 11, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@Hugo0 Hugo0 force-pushed the v4 branch 2 times, most recently from aa93f71 to 9e877d9 Compare April 11, 2026 09:02
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
composables/useSemanticGame.ts (1)

224-239: ⚠️ Potential issue | 🟠 Major

Make play mode part of the semantic session identity.

Once Line 239 changes server target selection, the composable can no longer treat every semantic session as the same persisted game. storageKey(lang) and dayIdx are still shared, so starting unlimited clears the daily save at Line 267, and the next daily load can restore unlimited guesses into the daily target. The optional string also leaves call sites like pages/[lang]/semantic.vue Line 402 free to fall back to the server’s daily default.

💾 Suggested direction
+type SemanticPlay = 'daily' | 'unlimited';
+
-function storageKey(lang: string): string {
-    return `semantic_game_${lang}`;
+function storageKey(lang: string, play: SemanticPlay): string {
+    return `semantic_game_${lang}_${play}`;
 }
 
 async function startGame(
-    opts: { target?: string; debug?: boolean; forceNew?: boolean; play?: string } = {}
+    opts: { target?: string; debug?: boolean; forceNew?: boolean; play: SemanticPlay }
 ) {
+    const play = opts.play;
     starting.value = true;
     ...
             const resp = await $fetch<StartResponse>(`/api/${lang}/semantic/start`, {
                 method: 'POST',
                 body: {
                     target: opts.target,
                     debug: opts.debug,
-                    play: opts.play,
+                    play,
                 },
             });
     ...
-            if (!opts.forceNew && !opts.target && restoreState(resp.dayIdx)) {
+            if (play === 'daily' && !opts.forceNew && !opts.target && restoreState(resp.dayIdx, play)) {
                 return;
             }
     ...
-            removeLocal(storageKey(lang));
+            removeLocal(storageKey(lang, play));

You’ll need to thread play through saveState() / restoreState() too, or skip persistence entirely for unlimited.

</details>

<details>
<summary>🤖 Prompt for AI Agents</summary>

Verify each finding against the current code and only fix it if needed.

In @composables/useSemanticGame.ts around lines 224 - 239, The session identity
currently ignores the optional play mode causing unlimited games to overwrite
daily saves; update the composable to include the play string in the persisted
session key and in state payloads so persistence is scoped by play mode:
incorporate opts.play into storageKey(lang) (or into whatever key generation
uses dayIdx) and add play to saveState() and restoreState() payloads (or branch
to skip persistence when play === 'unlimited'); update startGame (and related
callers such as pages/[lang]/semantic.vue) to pass the play through so
restoreState() can correctly distinguish saved sessions by play mode.


</details>

</blockquote></details>

</blockquote></details>
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@components/app/AppShell.vue`:
- Around line 34-35: The modal reads the game-local streak
(useGameStore().effectiveStreak) while the header uses the product-wide hook
(useProductStreak), causing mismatched counts; update
components/game/StreakModal.vue to consume the same product-wide source—either
import/use useProductStreak inside StreakModal or accept the product streak as a
prop (currentStreak) from AppShell so both header and modal use the exact same
value; also update the other modal open bindings referenced (the similar
handlers at lines 47-48) to pass or rely on the same product streak source so
they stay in sync.

In `@components/semantic/SemanticStatsModal.vue`:
- Around line 41-48: The onShareClick() handler currently sets shareCopied and
starts a timer optimistically before the actual share/copy completes; remove the
optimistic setTimeout/assignment from onShareClick() (leave only emit('share'))
and instead drive shareCopied.value from the real outcome: have the component
listen for the success signal emitted back from the parent (e.g., an event like
'share-success'/'share-failure' or a boolean prop such as shareSucceeded) and
set shareCopied.value = true on actual success and clear it after 2s; apply the
same change to the other optimistic update at lines referenced (the second
occurrence around 146-147) so UI only shows "Copied!" when the share/copy
operation truly succeeds.

In `@components/shared/StreakCalendar.vue`:
- Around line 157-163: The current logic shifts the range forward by setting
endOfWeek to the coming Sunday, which yields future days in the grid; instead,
make the 28-day window anchored on today: set endDate = today and startDate =
new Date(today) minus 27 days (replace endOfWeek/startDate math), keep computing
todayDow for weekday alignment but use it to produce leading empty cells (or a
column offset) when rendering the calendar rather than advancing the end date;
update any references to endOfWeek to use endDate and ensure the rendering code
consumes the leading-empty count to align weeks.
- Around line 175-182: The current logic lets detail?.state override the today
marker, so change it so 'state' only reflects result/future/missed (use
detail?.state || 'missed' for past and 'future' when isFuture) and keep the
isToday boolean separate (pass isToday or set an additional flag to CalendarDay
rendering) so the "today" outline is rendered independently from result states;
update usages of state, CalendarDay, isToday, and detail?.state accordingly.

---

Outside diff comments:
In `@composables/useSemanticGame.ts`:
- Around line 224-239: The session identity currently ignores the optional play
mode causing unlimited games to overwrite daily saves; update the composable to
include the play string in the persisted session key and in state payloads so
persistence is scoped by play mode: incorporate opts.play into storageKey(lang)
(or into whatever key generation uses dayIdx) and add play to saveState() and
restoreState() payloads (or branch to skip persistence when play ===
'unlimited'); update startGame (and related callers such as
pages/[lang]/semantic.vue) to pass the play through so restoreState() can
correctly distinguish saved sessions by play mode.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: d89c4bca-9717-421e-86d5-f7dd5b7bd08c

📥 Commits

Reviewing files that changed from the base of the PR and between deaf2a9 and 2418edb.

📒 Files selected for processing (19)
  • CONTRIBUTING.md
  • components/app/AppHeader.vue
  • components/app/AppShell.vue
  • components/app/AppSidebar.vue
  • components/app/SidebarItem.vue
  • components/game/SettingsModal.vue
  • components/game/StreakModal.vue
  • components/semantic/SemanticStatsModal.vue
  • components/shared/StreakCalendar.vue
  • composables/useSemanticGame.ts
  • nuxt.config.ts
  • package.json
  • pages/[lang]/semantic.vue
  • pages/[lang]/speed.vue
  • pages/index.vue
  • pages/profile.vue
  • server/api/[lang]/semantic/start.post.ts
  • server/utils/prisma.ts
  • utils/streak-dates.ts

Comment thread components/app/AppShell.vue
Comment on lines +41 to +48
function onShareClick() {
emit('share');
shareCopied.value = true;
if (shareTimer) clearTimeout(shareTimer);
shareTimer = setTimeout(() => {
shareCopied.value = false;
}, 2000);
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Don't report share success before it actually succeeds.

onShareClick() flips the button to Copied! unconditionally, but the real share/copy work happens elsewhere and can still fail or be cancelled. That gives false success feedback on rejected native-share or clipboard failures. Drive this label from the actual share result instead of setting it optimistically here.

Also applies to: 146-147

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/semantic/SemanticStatsModal.vue` around lines 41 - 48, The
onShareClick() handler currently sets shareCopied and starts a timer
optimistically before the actual share/copy completes; remove the optimistic
setTimeout/assignment from onShareClick() (leave only emit('share')) and instead
drive shareCopied.value from the real outcome: have the component listen for the
success signal emitted back from the parent (e.g., an event like
'share-success'/'share-failure' or a boolean prop such as shareSucceeded) and
set shareCopied.value = true on actual success and clear it after 2s; apply the
same change to the other optimistic update at lines referenced (the second
occurrence around 146-147) so UI only shows "Copied!" when the share/copy
operation truly succeeds.

Comment on lines +157 to +163
// Align to full weeks: end on this week's Sunday
const todayDow = (today.getDay() + 6) % 7;
const endOfWeek = new Date(today);
endOfWeek.setDate(endOfWeek.getDate() + (6 - todayDow));
const startDate = new Date(endOfWeek);
startDate.setDate(startDate.getDate() - 27);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

This stops being a true “Last 28 Days” window mid-week.

endOfWeek is pushed to the upcoming Sunday, so from Monday-Saturday the grid includes 1-6 future cells and only 22-27 historical days. If this is meant to be a rolling 28-day view, anchor the range on today and use leading empty cells for week alignment instead of shifting the end date forward.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/shared/StreakCalendar.vue` around lines 157 - 163, The current
logic shifts the range forward by setting endOfWeek to the coming Sunday, which
yields future days in the grid; instead, make the 28-day window anchored on
today: set endDate = today and startDate = new Date(today) minus 27 days
(replace endOfWeek/startDate math), keep computing todayDow for weekday
alignment but use it to produce leading empty cells (or a column offset) when
rendering the calendar rather than advancing the end date; update any references
to endOfWeek to use endDate and ensure the rendering code consumes the
leading-empty count to align weeks.

Comment thread components/shared/StreakCalendar.vue
Post-squash-merge fixes on top of the v4 feature branch:

- Mobile header: hide profile+settings below sm (sidebar-only)
- Trophy → BarChart2 for game results toggle
- Sidebar statistics → ChartNoAxesCombined icon
- Streak badge opens modal on all pages (not just game pages)
- StreakModal: sign-in CTA, shared StreakCalendar component
- SettingsModal: use login modal instead of direct Google auth
- Profile: remove passkey button, scroll to #badges hash
- Homepage: remove misleading per-card streak
- Semantic: unlimited mode (random target), Keep Playing/Play Again
- Semantic: daily revisit auto-opens modal, fix map clipping
- Page keys include play type for proper remount
- "Unlimited" accent color in header subtitle
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

♻️ Duplicate comments (1)
components/semantic/SemanticStatsModal.vue (1)

41-48: ⚠️ Potential issue | 🟡 Minor

Share success is still reported optimistically.

Line 43 flips to Copied! before any confirmed share/copy success, so cancelled or failed attempts can show false positive feedback.

Suggested fix
-import { computed, ref } from 'vue';
+import { computed, ref, watch } from 'vue';

 const props = defineProps<{
@@
     /** Whether this is a daily game (affects Play Again vs Keep Playing). */
     isDaily?: boolean;
+    /** Parent sets true only when share/copy actually succeeds. */
+    shareSucceeded?: boolean;
 }>();
@@
 function onShareClick() {
     emit('share');
-    shareCopied.value = true;
-    if (shareTimer) clearTimeout(shareTimer);
-    shareTimer = setTimeout(() => {
-        shareCopied.value = false;
-    }, 2000);
 }
+
+watch(
+    () => props.shareSucceeded,
+    (ok) => {
+        if (!ok) return;
+        shareCopied.value = true;
+        if (shareTimer) clearTimeout(shareTimer);
+        shareTimer = setTimeout(() => {
+            shareCopied.value = false;
+        }, 2000);
+    }
+);

Also applies to: 146-147

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/semantic/SemanticStatsModal.vue` around lines 41 - 48, The UI sets
shareCopied.value = true optimistically in onShareClick (and the similar handler
at the other location) before the share/copy actually succeeds; change both
handlers (onShareClick and the other share/copy handler around lines 146-147) to
perform the copy/share operation first, await its success (use the Promise
returned by navigator.clipboard.writeText or navigator.share, or a callback from
the emitted 'share' if it returns a Promise), and only then set
shareCopied.value = true and start/refresh the shareTimer; on failure do not set
shareCopied and optionally log or surface the error, and still ensure any
existing shareTimer is cleared to avoid stale timers.
🧹 Nitpick comments (2)
components/game/SettingsModal.vue (1)

233-233: Localize newly introduced CTA copy

Line 233 introduces a hardcoded "Sign in" string in a heavily localized component. Please route this through lang.config?.ui for i18n consistency (same for the nearby helper copy).

Proposed i18n-aligned change
-                        Sign in
+                        {{ lang.config?.ui?.sign_in || 'Sign in' }}
-                    <p class="text-xs text-center text-muted mt-1">Sync settings across devices</p>
+                    <p class="text-xs text-center text-muted mt-1">
+                        {{ lang.config?.ui?.sync_settings_desc || 'Sync settings across devices' }}
+                    </p>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/game/SettingsModal.vue` at line 233, The template in
SettingsModal.vue contains a hardcoded "Sign in" CTA (and adjacent helper copy);
replace those literal strings with the i18n entries from lang.config?.ui (e.g.,
lang.config?.ui?.signIn for the button and the appropriate ui key for the helper
text) inside the component template, ensuring you use optional chaining and
provide a sensible fallback (e.g., "Sign in") if the config is missing so
rendering won't break; update both the CTA and the nearby helper copy locations
in SettingsModal.vue to reference lang.config?.ui keys rather than hardcoded
text.
.github/workflows/test.yml (1)

93-93: Centralize NUXT_SESSION_PASSWORD instead of duplicating inline literals.

Use a single workflow/job-level env source (preferably a GitHub secret/variable) to avoid drift and improve CI secret hygiene.

Proposed YAML refactor
 name: Tests
+env:
+  NUXT_SESSION_PASSWORD: ${{ secrets.NUXT_SESSION_PASSWORD_CI }}

 on:
   push:
     branches: [main]
@@
       - name: Run vitest
         run: pnpm test
         env:
           NUXT_USE_BUILD: '1'
-          NUXT_SESSION_PASSWORD: 'ci-test-secret-min-32-chars-long!!'
@@
       - name: Run Playwright E2E tests
         run: pnpm test:e2e
-        env:
-          NUXT_SESSION_PASSWORD: 'ci-test-secret-min-32-chars-long!!'

Also applies to: 124-125

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/test.yml at line 93, Replace the duplicated inline literal
NUXT_SESSION_PASSWORD in the workflow with a single centralized env source:
define NUXT_SESSION_PASSWORD once at the workflow or job env level using a
GitHub secret/variable (e.g., secrets.NUXT_SESSION_PASSWORD or
vars.NUXT_SESSION_PASSWORD) and update the occurrences where
NUXT_SESSION_PASSWORD is currently set inline (the entries matching the string
"NUXT_SESSION_PASSWORD: 'ci-test-secret-min-32-chars-long!!'") to reference that
single env variable instead; ensure both locations (the current occurrence and
the ones noted in the comment) are changed so the workflow reads from the
centralized secret/variable.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@pages/profile.vue`:
- Around line 184-189: The current watcher uses document.querySelector(hash)
which can throw on non-selector-safe fragments; instead strip the leading '#'
from useRoute().hash, decode it (e.g., decodeURIComponent(hash.slice(1))),
ensure the resulting id is non-empty, then call document.getElementById(id) and,
if the element exists, call el.scrollIntoView({ behavior: 'smooth' }) after the
nextTick() waits; update the code around useRoute().hash, nextTick(), and the
document.querySelector call accordingly so invalid or encoded URL fragments do
not throw.

In `@vitest.config.ts`:
- Around line 34-36: The alias for '@vue/reactivity' currently points only to
resolve(__dirname, 'node_modules/@vue/reactivity') so it fails under different
pnpm hoisting; update the alias resolution to attempt multiple fallbacks (e.g.,
try resolve(__dirname, 'node_modules/@vue/reactivity') and if missing fall back
to resolve(__dirname, '../node_modules/@vue/reactivity') or use require.resolve
inside a try/catch to pick the first successful path) and assign that resolved
path to the '@vue/reactivity' key in the aliases object used by the Vitest/Vite
config so tests find the package in both local and monorepo pnpm layouts.

---

Duplicate comments:
In `@components/semantic/SemanticStatsModal.vue`:
- Around line 41-48: The UI sets shareCopied.value = true optimistically in
onShareClick (and the similar handler at the other location) before the
share/copy actually succeeds; change both handlers (onShareClick and the other
share/copy handler around lines 146-147) to perform the copy/share operation
first, await its success (use the Promise returned by
navigator.clipboard.writeText or navigator.share, or a callback from the emitted
'share' if it returns a Promise), and only then set shareCopied.value = true and
start/refresh the shareTimer; on failure do not set shareCopied and optionally
log or surface the error, and still ensure any existing shareTimer is cleared to
avoid stale timers.

---

Nitpick comments:
In @.github/workflows/test.yml:
- Line 93: Replace the duplicated inline literal NUXT_SESSION_PASSWORD in the
workflow with a single centralized env source: define NUXT_SESSION_PASSWORD once
at the workflow or job env level using a GitHub secret/variable (e.g.,
secrets.NUXT_SESSION_PASSWORD or vars.NUXT_SESSION_PASSWORD) and update the
occurrences where NUXT_SESSION_PASSWORD is currently set inline (the entries
matching the string "NUXT_SESSION_PASSWORD:
'ci-test-secret-min-32-chars-long!!'") to reference that single env variable
instead; ensure both locations (the current occurrence and the ones noted in the
comment) are changed so the workflow reads from the centralized secret/variable.

In `@components/game/SettingsModal.vue`:
- Line 233: The template in SettingsModal.vue contains a hardcoded "Sign in" CTA
(and adjacent helper copy); replace those literal strings with the i18n entries
from lang.config?.ui (e.g., lang.config?.ui?.signIn for the button and the
appropriate ui key for the helper text) inside the component template, ensuring
you use optional chaining and provide a sensible fallback (e.g., "Sign in") if
the config is missing so rendering won't break; update both the CTA and the
nearby helper copy locations in SettingsModal.vue to reference lang.config?.ui
keys rather than hardcoded text.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: ae3c56a6-4e40-4fc7-95f5-87a2f7bb0e8c

📥 Commits

Reviewing files that changed from the base of the PR and between 2418edb and 981e633.

📒 Files selected for processing (22)
  • .github/workflows/test.yml
  • CONTRIBUTING.md
  • components/app/AppHeader.vue
  • components/app/AppShell.vue
  • components/app/AppSidebar.vue
  • components/app/SidebarItem.vue
  • components/game/SettingsModal.vue
  • components/game/StreakModal.vue
  • components/semantic/SemanticStatsModal.vue
  • components/shared/StreakCalendar.vue
  • composables/useSemanticGame.ts
  • nuxt.config.ts
  • package.json
  • pages/[lang]/semantic.vue
  • pages/[lang]/speed.vue
  • pages/index.vue
  • pages/profile.vue
  • server/api/[lang]/semantic/start.post.ts
  • server/utils/prisma.ts
  • tests/setup-server.ts
  • utils/streak-dates.ts
  • vitest.config.ts
✅ Files skipped from review due to trivial changes (6)
  • components/app/SidebarItem.vue
  • CONTRIBUTING.md
  • components/app/AppSidebar.vue
  • server/utils/prisma.ts
  • tests/setup-server.ts
  • components/shared/StreakCalendar.vue
🚧 Files skipped from review as they are similar to previous changes (6)
  • package.json
  • server/api/[lang]/semantic/start.post.ts
  • composables/useSemanticGame.ts
  • pages/[lang]/semantic.vue
  • components/game/StreakModal.vue
  • utils/streak-dates.ts

Comment thread pages/profile.vue
Comment on lines +184 to +189
const hash = useRoute().hash;
if (hash) {
await nextTick();
await nextTick();
document.querySelector(hash)?.scrollIntoView({ behavior: 'smooth' });
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

# Find and read the relevant file
find . -name "profile.vue" -type f | head -5

Repository: Hugo0/wordle

Length of output: 75


🏁 Script executed:

# Search for information about querySelector error behavior
rg "querySelector" --type vue -A 3 -B 3 | head -50

Repository: Hugo0/wordle

Length of output: 83


🌐 Web query:

document.querySelector() error behavior invalid selector CSS

💡 Result:

When document.querySelector receives an invalid CSS selector, it throws a SyntaxError DOMException. This occurs if the selector string does not conform to valid CSS selector syntax. Common causes include: - IDs or attribute values starting with digits or containing invalid characters (e.g., '#123' or 'data-id=123' without quotes). - Unescaped special characters in IDs/classes (e.g., 'this?element' requires CSS.escape or manual escaping like 'this\?element'). - Missing quotes around attribute values that are not valid CSS identifiers. - Malformed syntax like unbalanced brackets or invalid pseudo-classes. If no matching element is found with a valid selector, it returns null instead of throwing an error. To handle safely: try { const element = document.querySelector(selector); } catch (e) { if (e instanceof SyntaxError) { console.error('Invalid selector:', selector); } }

Citations:


🏁 Script executed:

# Read the profile.vue file around lines 184-189
cat -n pages/profile.vue | sed -n '180,195p'

Repository: Hugo0/wordle

Length of output: 684


🌐 Web query:

Vue Router useRoute().hash returns what format with # symbol

💡 Result:

Vue Router's useRoute.hash returns the hash portion of the current URL including the leading # symbol. For example, if the URL is https://example.com/path#section1, then route.hash is '#section1'. If there is no hash, it returns an empty string ''. This behavior is documented in the official Vue Router API reference for RouteLocationNormalized and _RouteLocationBase, where the hash property is described as: "Hash of the current location. If present, starts with a #." The source code in parseURL confirms it keeps the # character from the URL: hash = location.slice(hashPos), where hashPos is the index of '#', and then decodes it but preserves the leading #.

Citations:


🏁 Script executed:

# Search for places where hash navigation is used or hashes are defined
rg "route\(\)|hash|scrollIntoView" pages/profile.vue -B 2 -A 2

Repository: Hugo0/wordle

Length of output: 487


🌐 Web query:

URL hash fragment specification valid characters can contain what symbols

💡 Result:

According to RFC 3986 (the authoritative specification for URI syntax), the fragment identifier (the part after the # in a URL) consists of zero or more occurrences of pchar, /, or ?. The pchar production is defined as: unreserved / pct-encoded / sub-delims / ":" / "@". Breaking it down: - unreserved: ALPHA (a-z, A-Z), DIGIT (0-9), "-", ".", "_", "~" - pct-encoded: "%" followed by two HEXDIGITs (any byte, percent-encoded) - sub-delims: "!", "$", "&", "'", "(", ")", "*", "+", ",", ";", "=" - plus ":", "@", "/", "?" Thus, the directly allowed unencoded symbols (excluding alphanumerics) are: ! $ & ' * + , ; = : @ / ? - . _ ~ Any other characters must be percent-encoded to be valid in the fragment. This syntax has remained unchanged since RFC 3986 (STD 66, published 2005) and is consistent across modern URL parsers (e.g., WHATWG URL Standard aligns with it for fragments). Note that while the generic syntax allows these, specific media types or applications may impose further restrictions on fragment semantics, but the syntactic valid characters are as defined.

Citations:


Guard hash scrolling against invalid selector hashes.

Using document.querySelector(hash) with URL hash input can throw for non-selector-safe values like #123 or #item?param (both valid URL fragments per RFC 3986) and interrupt the watcher. Use ID lookup from the hash fragment instead.

Suggested fix
-            const hash = useRoute().hash;
-            if (hash) {
-                await nextTick();
-                await nextTick();
-                document.querySelector(hash)?.scrollIntoView({ behavior: 'smooth' });
-            }
+            const hash = useRoute().hash;
+            if (hash) {
+                await nextTick();
+                await nextTick();
+                const id = decodeURIComponent(hash.slice(1));
+                document.getElementById(id)?.scrollIntoView({ behavior: 'smooth' });
+            }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const hash = useRoute().hash;
if (hash) {
await nextTick();
await nextTick();
document.querySelector(hash)?.scrollIntoView({ behavior: 'smooth' });
}
const hash = useRoute().hash;
if (hash) {
await nextTick();
await nextTick();
const id = decodeURIComponent(hash.slice(1));
document.getElementById(id)?.scrollIntoView({ behavior: 'smooth' });
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pages/profile.vue` around lines 184 - 189, The current watcher uses
document.querySelector(hash) which can throw on non-selector-safe fragments;
instead strip the leading '#' from useRoute().hash, decode it (e.g.,
decodeURIComponent(hash.slice(1))), ensure the resulting id is non-empty, then
call document.getElementById(id) and, if the element exists, call
el.scrollIntoView({ behavior: 'smooth' }) after the nextTick() waits; update the
code around useRoute().hash, nextTick(), and the document.querySelector call
accordingly so invalid or encoded URL fragments do not throw.

Comment thread vitest.config.ts
Comment on lines +34 to +36
// @vue/reactivity is used by game store for pauseTracking/resetTracking.
// pnpm hoisting varies between local and CI — try both locations.
'@vue/reactivity': resolve(__dirname, 'node_modules/@vue/reactivity'),
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Direct dependency declaration =="
jq '{dep: .dependencies["@vue/reactivity"], devDep: .devDependencies["@vue/reactivity"]}' package.json

echo
echo "== Lockfile references =="
rg -n "@vue/reactivity" pnpm-lock.yaml -C1 || true

echo
echo "== Local install path check (run after pnpm install) =="
for p in \
  "node_modules/@vue/reactivity" \
  "node_modules/.pnpm/node_modules/@vue/reactivity"
do
  if [ -d "$p" ]; then
    echo "FOUND: $p"
  else
    echo "MISSING: $p"
  fi
done

Repository: Hugo0/wordle

Length of output: 1229


🏁 Script executed:

# Check the full vitest.config.ts content
cat vitest.config.ts

Repository: Hugo0/wordle

Length of output: 1420


🏁 Script executed:

# Search for `@vue/reactivity` usage in the codebase
rg '@vue/reactivity' --type ts --type js -n

Repository: Hugo0/wordle

Length of output: 346


🏁 Script executed:

# Check if `@vue/reactivity` is imported anywhere
rg "from '@vue/reactivity'" -n

Repository: Hugo0/wordle

Length of output: 137


Make the alias actually fallback across pnpm layouts.

The comment says "try both locations," but the code currently binds only one path. If node_modules/@vue/reactivity is absent in some environments (which is a realistic concern given pnpm's variable hoisting strategies), tests can fail to resolve this alias. The package is confirmed as a devDependency and genuinely used in stores/game.ts for pauseTracking/resetTracking.

Proposed robust fallback
+import { existsSync } from 'node:fs';
 import { resolve } from 'path';
@@
 export default defineConfig({
+    const reactivityCandidates = [
+        resolve(__dirname, 'node_modules/@vue/reactivity'),
+        resolve(__dirname, 'node_modules/.pnpm/node_modules/@vue/reactivity'),
+    ];
+    const reactivityAlias = reactivityCandidates.find((p) => existsSync(p)) ?? '@vue/reactivity';
+
     test: {
@@
         alias: {
             '~': resolve(__dirname, '.'),
             '@': resolve(__dirname, '.'),
             // `@vue/reactivity` is used by game store for pauseTracking/resetTracking.
             // pnpm hoisting varies between local and CI — try both locations.
-            '@vue/reactivity': resolve(__dirname, 'node_modules/@vue/reactivity'),
+            '@vue/reactivity': reactivityAlias,
         },
     },
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@vitest.config.ts` around lines 34 - 36, The alias for '@vue/reactivity'
currently points only to resolve(__dirname, 'node_modules/@vue/reactivity') so
it fails under different pnpm hoisting; update the alias resolution to attempt
multiple fallbacks (e.g., try resolve(__dirname, 'node_modules/@vue/reactivity')
and if missing fall back to resolve(__dirname,
'../node_modules/@vue/reactivity') or use require.resolve inside a try/catch to
pick the first successful path) and assign that resolved path to the
'@vue/reactivity' key in the aliases object used by the Vitest/Vite config so
tests find the package in both local and monorepo pnpm layouts.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant