Conversation
|
Warning Rate limit exceeded
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 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 configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (1)
📝 WalkthroughWalkthroughAdds 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
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
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
|
@coderabbitai review |
✅ Actions performedReview triggered.
|
aa93f71 to
9e877d9
Compare
There was a problem hiding this comment.
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 | 🟠 MajorMake 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)anddayIdxare 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 optionalstringalso leaves call sites likepages/[lang]/semantic.vueLine 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
playthroughsaveState()/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.tsaround 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📒 Files selected for processing (19)
CONTRIBUTING.mdcomponents/app/AppHeader.vuecomponents/app/AppShell.vuecomponents/app/AppSidebar.vuecomponents/app/SidebarItem.vuecomponents/game/SettingsModal.vuecomponents/game/StreakModal.vuecomponents/semantic/SemanticStatsModal.vuecomponents/shared/StreakCalendar.vuecomposables/useSemanticGame.tsnuxt.config.tspackage.jsonpages/[lang]/semantic.vuepages/[lang]/speed.vuepages/index.vuepages/profile.vueserver/api/[lang]/semantic/start.post.tsserver/utils/prisma.tsutils/streak-dates.ts
| function onShareClick() { | ||
| emit('share'); | ||
| shareCopied.value = true; | ||
| if (shareTimer) clearTimeout(shareTimer); | ||
| shareTimer = setTimeout(() => { | ||
| shareCopied.value = false; | ||
| }, 2000); | ||
| } |
There was a problem hiding this comment.
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.
| // 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); | ||
|
|
There was a problem hiding this comment.
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.
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
There was a problem hiding this comment.
Actionable comments posted: 2
♻️ Duplicate comments (1)
components/semantic/SemanticStatsModal.vue (1)
41-48:⚠️ Potential issue | 🟡 MinorShare 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 copyLine 233 introduces a hardcoded
"Sign in"string in a heavily localized component. Please route this throughlang.config?.uifor 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: CentralizeNUXT_SESSION_PASSWORDinstead 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
📒 Files selected for processing (22)
.github/workflows/test.ymlCONTRIBUTING.mdcomponents/app/AppHeader.vuecomponents/app/AppShell.vuecomponents/app/AppSidebar.vuecomponents/app/SidebarItem.vuecomponents/game/SettingsModal.vuecomponents/game/StreakModal.vuecomponents/semantic/SemanticStatsModal.vuecomponents/shared/StreakCalendar.vuecomposables/useSemanticGame.tsnuxt.config.tspackage.jsonpages/[lang]/semantic.vuepages/[lang]/speed.vuepages/index.vuepages/profile.vueserver/api/[lang]/semantic/start.post.tsserver/utils/prisma.tstests/setup-server.tsutils/streak-dates.tsvitest.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
| const hash = useRoute().hash; | ||
| if (hash) { | ||
| await nextTick(); | ||
| await nextTick(); | ||
| document.querySelector(hash)?.scrollIntoView({ behavior: 'smooth' }); | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
# Find and read the relevant file
find . -name "profile.vue" -type f | head -5Repository: Hugo0/wordle
Length of output: 75
🏁 Script executed:
# Search for information about querySelector error behavior
rg "querySelector" --type vue -A 3 -B 3 | head -50Repository: 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:
- 1: https://developer.mozilla.org/en-US/docs/Web/API/Document/querySelector.
- 2: https://developer.mozilla.org/en-US/docs/Web/API/Document/querySelector
- 3: https://developer.mozilla.org/en-US/docs/DOM/Document.querySelector
- 4: https://stackoverflow.com/questions/34777481/failed-to-execute-query-selector-on-document-id-is-not-a-valid-selector
- 5: Failed to execute 'querySelector' on 'Element': '#/my-id' is not a valid selector. bootstrap-vue/bootstrap-vue#5561
- 6: https://stackoverflow.com/questions/52673617/js-queryselectorall-not-a-valid-selector-despite-it-being-like-the-docs-example
- 7: https://stackoverflow.com/questions/57916786/queryselector-error-not-a-valid-selector-javascript
🏁 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:
- 1: https://router.vuejs.org/api/interfaces/routelocationbase
- 2: https://router.vuejs.org/api/interfaces/RouteLocationNormalizedGeneric.html
- 3: https://github.com/vuejs/router/blob/245082aa/packages/router/src/location.ts
🏁 Script executed:
# Search for places where hash navigation is used or hashes are defined
rg "route\(\)|hash|scrollIntoView" pages/profile.vue -B 2 -A 2Repository: 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:
- 1: https://rfc-editor.org/rfc/rfc3986.html
- 2: https://stackoverflow.com/questions/26088849/url-fragment-allowed-characters
- 3: https://stackoverflow.com/questions/40636281/what-are-the-eligible-characters-in-a-urls-fragment-location-hash
- 4: https://www.rfc-editor.org/rfc/rfc3986
- 5: https://datatracker.ietf.org/doc/html/rfc3986
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.
| 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.
| // @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'), |
There was a problem hiding this comment.
🧩 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
doneRepository: Hugo0/wordle
Length of output: 1229
🏁 Script executed:
# Check the full vitest.config.ts content
cat vitest.config.tsRepository: Hugo0/wordle
Length of output: 1420
🏁 Script executed:
# Search for `@vue/reactivity` usage in the codebase
rg '@vue/reactivity' --type ts --type js -nRepository: Hugo0/wordle
Length of output: 346
🏁 Script executed:
# Check if `@vue/reactivity` is imported anywhere
rg "from '@vue/reactivity'" -nRepository: 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.
Summary
Major feature branch with accounts system, new game modes, and UX polish. Work by multiple agents.
New Features
UX Fixes (latest commit)
Infrastructure
Test plan
@coderabbitai full review
Summary by CodeRabbit
New Features
Improvements
Documentation