Skip to content

Dev#11

Merged
Retsomm merged 6 commits into
mainfrom
dev
Jul 12, 2026
Merged

Dev#11
Retsomm merged 6 commits into
mainfrom
dev

Conversation

@Retsomm

@Retsomm Retsomm commented Jul 12, 2026

Copy link
Copy Markdown
Owner

Summary by CodeRabbit

  • New Features
    • Added an Expo mobile app with home learning, chapter maps, quizzes, mixed practice, progress tracking, saved questions, and profile authentication.
    • Added account progress synchronization, XP, streaks, completion statistics, and saved-question support across web and mobile.
    • Expanded the learning library with JavaScript and React lessons covering foundational and advanced topics.
    • Added profile customization, including avatar editing and first-name updates.
  • Documentation
    • Updated project setup instructions and documented mobile migration progress and configuration.

Retsomm and others added 5 commits July 12, 2026 16:09
把 Next.js 專案整包搬進 apps/web(package.json 改名 web),新增 packages/core
作為 web/mobile 未來共用的純資料與純函式套件(@easylearn/core),目前先放
IconName/Question/Progress 等共用型別(從 Icons.tsx 反向抽出)。

根目錄 package.json 改成 workspace root;next.config.ts 加
transpilePackages 讓 Next.js 能編譯 workspace 內的 TS 套件;重寫沒編輯過的
Vite 樣板 README.md。

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
data/chapters.ts、data/typeMeta.ts、data/questions/*.json、utils/quiz.ts
從 apps/web 搬進 packages/core/src;新增 progressCalc.ts,把 useProgress.ts
裡的 todayStr/yesterdayStr/bumpXpLog/bumpCounter/bumpStreak/GRADUATE_BOX
抽成純函式。apps/web 消費端全部改成從 @easylearn/core 匯入。

伺服器端 progressLogic.ts 原本重複宣告一份 GRADUATE_BOX,改成匯入同一份;
bumpStreakServer/addDaysToDateStr 刻意不搬(純 UTC 字串運算+外部傳入
today,跟前端用裝置本地時區的機制不同,硬併會破壞避免主機時區誤判 streak
的設計)。validate-questions.mjs 的 QUESTIONS_DIR 路徑同步更新。

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
用 create-expo-app --template tabs 建立 Expo Router 專案,4 個 tab
(home/notes/stats/profile)對應 web 版 Navbar.tsx,目前只有 Profile
有實際內容,其餘是待後續 phase 補完的佔位畫面。

metro.config.js 加上 watchFolders/nodeModulesPaths/disableHierarchicalLookup
讓 Metro 能解析 workspace 裡的 packages/core;一次裝齊 Phase 2-6 會用到的
native module(@clerk/expo、expo-secure-store、gesture-handler、svg、
slider、image-picker、expo-dev-client 等),避免之後每個 phase 各自
rebuild 一次 Dev Client。

app/_layout.tsx 用 ClerkProvider 包住整個 app,lib/tokenCache.ts 透過
expo-secure-store 持久化 session token;lib/api.ts 是帶 Bearer token 的
fetch 包裝,打 apps/web 既有的 API routes。app/(tabs)/profile.tsx 未登入
顯示 Google 登入按鈕(Clerk 這個 instance 目前只開 Google),已登入唯讀
顯示 GET /api/progress 回傳的 XP/streak/關卡數。

react-native-screens 手動固定在 4.26.0(比 SDK 57 建議的 4.25.2 新一個
patch)並在 package.json 的 expo.install.exclude 註記,解決 expo-router
內部依賴造成的 duplicate native module。

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
真機驗證過完整流程(選章節→答題→結算)、隨機綜合練習、AsyncStorage 跨 app
重啟持久化。Icon/CodeBlock 先用 emoji/純文字頂替 web 版視覺,屬刻意的 MVP
簡化。Profile tab 訪客進度顯示留到 Phase 4 一併處理。
Phase 2「一次裝齊 native module」漏了 expo-auth-session,@clerk/expo 的
useSSO().startSSOFlow() 同時需要它和 expo-web-browser。真機測試登入時
才發現。這三個套件(連帶裝入的 expo-application/expo-crypto)都有原生
模組,裝完需要重建 Dev Client。
@vercel

vercel Bot commented Jul 12, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
easy-learn Ready Ready Preview, Comment Jul 12, 2026 11:19am

@coderabbitai

coderabbitai Bot commented Jul 12, 2026

Copy link
Copy Markdown

Warning

Review limit reached

@Retsomm, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 12 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 1c8baa47-5c13-4969-849c-92f00ef3ed0a

📥 Commits

Reviewing files that changed from the base of the PR and between 032de83 and fed2028.

📒 Files selected for processing (42)
  • README.md
  • apps/mobile/app/(tabs)/_layout.tsx
  • apps/mobile/app/(tabs)/index.tsx
  • apps/mobile/app/(tabs)/profile.tsx
  • apps/mobile/app/+not-found.tsx
  • apps/mobile/app/_layout.tsx
  • apps/mobile/app/sso-callback.tsx
  • apps/mobile/components/ComingSoon.tsx
  • apps/mobile/components/QuestionCard.tsx
  • apps/mobile/components/useColorScheme.ts
  • apps/mobile/hooks/useProgress.ts
  • apps/mobile/lib/tokenCache.ts
  • apps/web/src/App.tsx
  • apps/web/src/app/api/account/route.ts
  • apps/web/src/app/api/progress/answer/route.ts
  • apps/web/src/app/api/progress/finish-level/route.ts
  • apps/web/src/app/api/progress/finish-review/route.ts
  • apps/web/src/app/api/progress/migrate-local/route.ts
  • apps/web/src/app/api/progress/route.ts
  • apps/web/src/app/api/progress/save-toggle/route.ts
  • apps/web/src/app/layout.tsx
  • apps/web/src/app/page.tsx
  • apps/web/src/components/AccountHeader.tsx
  • apps/web/src/components/CodeBlock.tsx
  • apps/web/src/components/GrowthHistory.tsx
  • apps/web/src/components/Mascot.tsx
  • apps/web/src/components/Navbar.tsx
  • apps/web/src/components/QuestionCard.tsx
  • apps/web/src/components/QuestionReview.tsx
  • apps/web/src/hooks/useProgress.ts
  • apps/web/src/lib/progressLogic.ts
  • apps/web/src/lib/progressStore.ts
  • apps/web/src/lib/stages.ts
  • apps/web/src/screens/ChapterMap.tsx
  • apps/web/src/screens/Home.tsx
  • apps/web/src/screens/Notes.tsx
  • apps/web/src/screens/Profile.tsx
  • apps/web/src/screens/QuestionBook.tsx
  • apps/web/src/screens/Quiz.tsx
  • apps/web/src/screens/Stats.tsx
  • docs/rn-migration.md
  • packages/core/src/progressCalc.ts
📝 Walkthrough

Walkthrough

EasyLearn is reorganized as a Yarn workspaces monorepo with shared learning content and progress logic, a new Expo mobile app, and a Next.js web application backed by Clerk and Prisma. The PR also adds question datasets, responsive UI styling, API routes, and migration documentation.

Changes

EasyLearn platform

Layer / File(s) Summary
Shared core contracts and learning content
packages/core/*, packages/core/src/data/questions/*
Shared types, quiz/progress utilities, chapter lookups, metadata, and JavaScript/React question banks are added and exported through @easylearn/core.
Expo mobile learning flow
apps/mobile/*
Expo configuration, authentication/token helpers, local progress persistence, tab navigation, home/chapter/quiz/profile screens, themed components, and placeholder tabs are added.
Web persistence and authenticated APIs
apps/web/prisma/*, apps/web/src/app/api/*, apps/web/src/lib/*, apps/web/src/proxy.ts
Prisma persistence models, migrations, Clerk middleware, account deletion, progress retrieval/update routes, and shared-core imports are introduced.
Web application screens and styling
apps/web/src/app/*, apps/web/src/components/*, apps/web/src/screens/*, apps/web/src/index.css
Next.js layout/entry points, account editing, progression and code components, shared icon typing, and responsive screen styling are added.
Migration documentation and workspace guidance
README.md, docs/rn-migration.md, package.json
The README and workspace scripts describe the monorepo, while migration documentation records architecture decisions, completed phases, and remaining work.

Estimated code review effort: 5 (Critical) | ~120 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 43.48% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The title is too vague and generic to describe the actual changes in this pull request. Replace it with a concise title that names the main change, such as the new mobile/web monorepo setup or the app migration work.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch dev

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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 16

Caution

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

⚠️ Outside diff range comments (3)
apps/web/src/app/api/progress/answer/route.ts (2)

19-23: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Missing input validation on request body.

questionId, correct, and today are destructured from request.json() without any validation. If any are missing or today is not a valid date string, the Prisma writes will fail with an opaque error or, worse, persist malformed data. Add a guard before proceeding.

🛡️ Proposed validation guard
 export const POST = async (request: Request) => {
   const { userId } = await auth()
   if (!userId) return NextResponse.json({ error: 'unauthorized' }, { status: 401 })
 
   const { questionId, correct, chapterId, today } = (await request.json()) as AnswerBody
+
+  if (!questionId || typeof correct !== 'boolean' || !today) {
+    return NextResponse.json({ error: 'invalid body' }, { status: 400 })
+  }
 
   const existing = await prisma.wrongEntry.findUnique({ where: { userId_questionId: { userId, questionId } } })
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/web/src/app/api/progress/answer/route.ts` around lines 19 - 23, Validate
the parsed AnswerBody immediately after request.json() in POST before any Prisma
operations: require questionId, correct, and today, ensure today is a valid date
string, and return a clear 400 response when validation fails. Preserve the
existing unauthorized response and normal processing for valid input.

48-64: 🔒 Security & Privacy | 🟠 Major | 🏗️ Heavy lift

Unhandled transaction errors and client-controlled today field.

Two concerns:

  1. The prisma.$transaction call has no try/catch. If any write fails, the raw Prisma error propagates as an unhandled 500 with potentially sensitive details.
  2. The today value is fully client-controlled and used directly in dailyStat upserts. A malicious client could send arbitrary dates to pollute daily statistics across any date.

Consider wrapping the transaction in a try/catch and deriving today server-side (e.g., from todayStr() in @easylearn/core) rather than trusting the client.

🛡️ Proposed fix: server-side date + error handling
- const { questionId, correct, chapterId, today } = (await request.json()) as AnswerBody
+ const { questionId, correct, chapterId } = (await request.json()) as AnswerBody
+ const today = todayStr() // derive server-side from `@easylearn/core`
 
   const existing = await prisma.wrongEntry.findUnique({ where: { userId_questionId: { userId, questionId } } })
@@
-  await prisma.$transaction([...wrongEntryWrites, dailyStatWrite, ...chapterStatWrites])
+  try {
+    await prisma.$transaction([...wrongEntryWrites, dailyStatWrite, ...chapterStatWrites])
+  } catch (err) {
+    console.error('answer route transaction failed', err)
+    return NextResponse.json({ error: 'internal error' }, { status: 500 })
+  }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/web/src/app/api/progress/answer/route.ts` around lines 48 - 64, Update
the answer route’s transaction flow around dailyStatWrite and
prisma.$transaction: derive today server-side using the existing todayStr()
utility from `@easylearn/core` instead of accepting the client-provided date, and
wrap the transaction in try/catch. Return the route’s established safe error
response from the catch while logging or preserving only appropriate error
details, without exposing raw Prisma errors.
apps/web/src/hooks/useProgress.ts (1)

98-108: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

migratedRef set before async fetch completes — transient failure permanently blocks server progress loading.

migratedRef.current = true is set synchronously before the async IIFE runs. If the fetch fails (network error, server down, non-2xx response), the ref is already true, so the effect won't retry on subsequent renders. The user's server-side progress is silently never loaded, and they're stuck with defaultProgress or stale guest data. Additionally, there's no try/catch around the async IIFE, producing an unhandled promise rejection, and no res.ok check before res.json().

🔧 Proposed fix: move ref guard after success and add error handling
   if (migratedRef.current) return
-  migratedRef.current = true
   ;(async () => {
-    const res = await fetch('/api/progress')
-    const data = (await res.json()) as { progress: Progress; isNew: boolean }
-    if (data.isNew) {
-      setProgress(await postJson('/api/progress/migrate-local', load()))
-    } else {
-      setProgress(data.progress)
+    try {
+      const res = await fetch('/api/progress')
+      if (!res.ok) return
+      const data = (await res.json()) as { progress: Progress; isNew: boolean }
+      migratedRef.current = true
+      if (data.isNew) {
+        setProgress(await postJson('/api/progress/migrate-local', load()))
+      } else {
+        setProgress(data.progress)
+      }
+    } catch (err) {
+      console.error('progress migration failed', err)
     }
   })()
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/web/src/hooks/useProgress.ts` around lines 98 - 108, Update the
migration effect around migratedRef and the async fetch so the ref is marked
true only after server progress loading or migration completes successfully. Add
try/catch handling for fetch, response parsing, and migration errors, and
validate res.ok before consuming the response; leave the ref unset on failure so
later renders can retry without unhandled promise rejections.
🧹 Nitpick comments (12)
apps/web/src/components/CodeBlock.tsx (1)

7-28: 🚀 Performance & Scalability | 🔵 Trivial | 💤 Low value

Optional: use push instead of spread-in-reduce for O(n) performance.

The ...acc.parts spread on each iteration makes highlight O(n²) in the number of regex matches. For short question snippets this is negligible, but switching to push is a trivial change that keeps it O(n) if code blocks ever grow.

♻️ Suggested refactor
 const highlight = (code: string): ReactNode[] => {
   const matches = Array.from(code.matchAll(TOKEN_RE))
-  const { parts, last } = matches.reduce<{ parts: ReactNode[]; last: number }>(
-    (acc, m, i) => {
-      const [text, comment, string, keyword, number] = m
-      const cls = comment ? 'tok-comment' : string ? 'tok-string' : keyword ? 'tok-keyword' : number ? 'tok-number' : ''
-      const gap = m.index > acc.last ? [code.slice(acc.last, m.index)] : []
-      return {
-        parts: [
-          ...acc.parts,
-          ...gap,
-          <span key={i} className={cls}>
-            {text}
-          </span>,
-        ],
-        last: m.index + text.length,
-      }
-    },
-    { parts: [], last: 0 },
-  )
-  return last < code.length ? [...parts, code.slice(last)] : parts
+  const parts: ReactNode[] = []
+  let last = 0
+  for (const m of matches) {
+    const i = parts.length
+    const [text, comment, string, keyword, number] = m
+    const cls = comment ? 'tok-comment' : string ? 'tok-string' : keyword ? 'tok-keyword' : number ? 'tok-number' : ''
+    if (m.index > last) parts.push(code.slice(last, m.index))
+    parts.push(
+      <span key={i} className={cls}>
+        {text}
+      </span>,
+    )
+    last = m.index + text.length
+  }
+  if (last < code.length) parts.push(code.slice(last))
+  return parts
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/web/src/components/CodeBlock.tsx` around lines 7 - 28, Update the reduce
callback in highlight to append gaps and token spans by mutating acc.parts with
push instead of recreating the array via ...acc.parts on every match. Preserve
the existing token ordering, keys, and last-index tracking while keeping the
final trailing-text handling unchanged.
packages/core/src/progressCalc.ts (1)

6-12: 🩺 Stability & Availability | 🔵 Trivial | 💤 Low value

Minor midnight boundary inconsistency between todayStr and yesterdayStr.

Each function calls new Date() independently. If bumpStreak is invoked exactly at midnight, todayStr() and yesterdayStr() could return the same date, causing the streak to reset to 1 instead of incrementing. This is a low-probability edge case but worth noting for a shared core module.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/core/src/progressCalc.ts` around lines 6 - 12, Update the date
handling in todayStr and yesterdayStr so both calculations use the same captured
Date instance when invoked together, preventing a midnight rollover from
producing inconsistent dates. Preserve the existing local YYYY-MM-DD formatting
and yesterday offset behavior.
apps/mobile/components/QuestionCard.tsx (1)

77-80: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Linking.openURL returns a Promise — add error handling.

If the URL can't be opened (invalid format, no matching app), the rejection is unhandled.

🛡️ Proposed fix
           <Pressable onPress={() => Linking.openURL(question.docs)}>
+            // Replace with:
+            <Pressable
+              onPress={async () => {
+                try {
+                  await Linking.openURL(question.docs);
+                } catch {
+                  // Optionally show an Alert to the user
+                }
+              }}
+            >
               <Text style={styles.docsLink}>延伸閱讀:官方文件</Text>
             </Pressable>
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/mobile/components/QuestionCard.tsx` around lines 77 - 80, Update the
docs link Pressable in QuestionCard so the Linking.openURL call handles rejected
promises, including failures from invalid URLs or unavailable apps. Add
appropriate local error handling without changing the existing link rendering or
press behavior.
apps/web/prisma/schema.prisma (1)

9-95: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

Prefer DateTime over String for date fields.

streakLast, lastWrong, and date (in XpLog/DailyStat) are stored as String. This limits date-range queries, timezone handling, and validation. If these represent calendar dates, consider DateTime (or DateTime @db.Date`` for date-only fields). If string storage is intentional for grouping keys (e.g., "2026-07-12"), document the format constraint.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/web/prisma/schema.prisma` around lines 9 - 95, Replace the date-like
String fields streakLast, lastWrong, and the date fields in XpLog and DailyStat
with DateTime types, using `@db.Date` where they represent date-only values.
Update related defaults, writes, and queries to pass DateTime values
consistently; if any field must remain a grouping key, document its required
format explicitly.
apps/mobile/lib/tokenCache.ts (1)

6-9: 🩺 Stability & Availability | 🔵 Trivial | 💤 Low value

Consider adding error handling to saveToken.

getToken catches storage errors and returns null, but saveToken lets SecureStore.setItemAsync rejections propagate unhandled. Adding a catch (even a silent one) prevents potential unhandled promise rejections if the keychain/keystore is unavailable.

🛡️ Suggested improvement
 export const tokenCache: TokenCache = {
   getToken: (key) => SecureStore.getItemAsync(key).catch(() => null),
-  saveToken: (key, value) => SecureStore.setItemAsync(key, value),
+  saveToken: (key, value) => SecureStore.setItemAsync(key, value).catch(() => {}),
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/mobile/lib/tokenCache.ts` around lines 6 - 9, Update the saveToken
method in tokenCache to handle rejected SecureStore.setItemAsync promises,
matching getToken’s error-handling behavior and preventing unhandled storage
errors. Keep the existing key and value arguments unchanged, and use the
established silent-failure behavior unless an existing fallback is required.
apps/web/src/hooks/useProgress.ts (1)

136-140: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚖️ Poor tradeoff

Server response overwrites concurrent local state updates.

Each action calls setProgress locally, then fires a server sync that resolves with .then(setProgress). The server response uses the non-functional form, replacing the entire state. If the user performs another action before the previous server response arrives, the earlier response overwrites the newer local update. In a quiz app with seconds between answers this is unlikely, but it's a latent data-integrity risk worth noting.

Consider merging server responses into local state via functional updates (e.g., setProgress(p => ({ ...p, ...serverData }))) rather than wholesale replacement.

Also applies to: 153-156, 178-181, 193-197

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/web/src/hooks/useProgress.ts` around lines 136 - 140, Update the
server-response handlers in the progress actions, including the paths around the
existing setProgress calls at lines 136–140, 153–156, 178–181, and 193–197, to
use functional state updates that merge returned server data into the latest
local progress state. Preserve newer local fields by spreading the current state
first and applying the server response afterward, while leaving the existing
local action updates unchanged.
packages/core/src/index.ts (1)

1-5: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider namespace exports to prevent naming conflicts

Using export * from five modules risks silent symbol collisions if any two modules export the same name. If this barrel file grows, consider explicit re-exports or namespace-style exports (e.g., export * as Types from './types') to keep the public API surface unambiguous.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/core/src/index.ts` around lines 1 - 5, Update the barrel exports in
the package index to avoid wildcard re-export collisions by using explicit or
namespace-style exports for types, progressCalc, quiz, chapters, and typeMeta.
Preserve access to each module’s public API while making module ownership
unambiguous.
apps/web/src/app/layout.tsx (2)

29-29: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Consider next/font/google instead of manual <link> tags.

Manual Google Fonts links add an extra render-blocking round trip to fonts.googleapis.com/fonts.gstatic.com. next/font/google self-hosts and inlines the font, avoiding that extra request and layout shift.

♻️ Example using next/font
+import { JetBrains_Mono, Noto_Sans_TC } from 'next/font/google'
+
+const jetbrainsMono = JetBrains_Mono({ subsets: ['latin'], weight: ['400','500','700','800'], variable: '--font-mono' })
+const notoSansTC = Noto_Sans_TC({ subsets: ['latin'], weight: ['400','500','700','900'], variable: '--font-sans' })

Then apply ${jetbrainsMono.variable} ${notoSansTC.variable} on <body> and drop the manual <link> tags.

Also applies to: 29-29, 34-39

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/web/src/app/layout.tsx` at line 29, Replace the manual Google Fonts link
tags in RootLayout with next/font/google font imports for JetBrains Mono and
Noto Sans TC, configure their variables, and apply both variables to the body
className. Remove the corresponding external font links while preserving the
existing layout structure.

29-29: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Avoid relying on the implicit React UMD global for the props type.

children: React.ReactNode works without importing React only depending on this project's tsconfig.json jsx setting; some configurations raise TS2686 ("'React' refers to a UMD global, but the current file is a module") for this exact pattern. Importing ReactNode directly sidesteps the ambiguity entirely.

♻️ Proposed fix
 import type { Metadata, Viewport } from 'next'
 import { ClerkProvider } from '`@clerk/nextjs`'
+import type { ReactNode } from 'react'
 import '../index.css'
...
-const RootLayout = ({ children }: { children: React.ReactNode }) => {
+const RootLayout = ({ children }: { children: ReactNode }) => {

Please confirm the web app's tsconfig.json jsx mode (preserve vs react-jsx) to determine whether this is currently a live compile error.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/web/src/app/layout.tsx` at line 29, Update the RootLayout props type to
import and use ReactNode directly instead of referencing the implicit React UMD
global. Verify the web app’s tsconfig JSX mode and preserve the existing layout
behavior while eliminating the React namespace dependency.
apps/web/src/components/Mascot.tsx (1)

25-26: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider narrowing mood to a union type.

mood?: string is permissive and interpolated directly into a className (mood-${mood}). A union type (e.g., 'idle' | 'happy' | 'celebrate') would provide compile-time safety against typos and invalid class names. Only worth doing if the set of moods is known and small.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/web/src/components/Mascot.tsx` around lines 25 - 26, Update the Mascot
component’s mood prop type to a union of the known supported mood values,
ensuring each value matches an available mood class used by the className
interpolation. Keep the prop optional and avoid narrowing it until the complete
supported mood set is identified.
apps/web/src/components/GrowthHistory.tsx (1)

1-1: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider extracting STAGES and getStage to a shared module.

Both GrowthHistory and Mascot (and Profile.tsx via getStage/getNextStage) depend on STAGES and stage logic currently defined in Mascot.tsx. Extracting these to a dedicated module (e.g., stages.ts) would decouple domain data/logic from the React component and clarify the dependency direction.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/web/src/components/GrowthHistory.tsx` at line 1, Extract the shared
STAGES data and getStage stage logic from Mascot.tsx into a dedicated stages
module, then update GrowthHistory, Mascot, and Profile.tsx (including
getNextStage usage) to import these symbols from the new module. Keep the
existing stage behavior unchanged and remove the duplicated or re-exported
domain logic from the component module.
apps/mobile/hooks/useProgress.ts (1)

65-86: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Consider extracting the Leitner box transition logic into packages/core.

bumpCounter/bumpStreak/bumpXpLog/GRADUATE_BOX were already extracted to @easylearn/core per the migration notes, but the wrongIds box increment/graduate/reset logic here is inlined and likely duplicated in apps/web/src/hooks/useProgress.ts. Pulling it into a shared applyAnswer(wrongIds, questionId, correct) helper would keep both platforms' Leitner-box semantics from drifting apart over time.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/mobile/hooks/useProgress.ts` around lines 65 - 86, Extract the wrongIds
Leitner transition logic from answerQuestion into a shared applyAnswer helper in
`@easylearn/core`, covering correct-answer box advancement and graduation removal
plus incorrect-answer reset and count updates. Update answerQuestion and the
corresponding web useProgress implementation to reuse this helper, while
preserving the existing dailyStats and chapterStats updates.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/mobile/app/`(tabs)/_layout.tsx:
- Around line 9-19: Update TabLayout’s tabBarActiveTintColor access to safely
handle a null result from useColorScheme, using the existing light/dark theme
fallback so Colors is always indexed by a valid key. Preserve the current tint
selection for non-null color schemes.

In `@apps/mobile/components/QuestionCard.tsx`:
- Around line 27-43: Update the star Icon in QuestionCard to use the
destructured saved prop for visible state feedback, switching between the
existing filled and outline appearance (or an equivalent color distinction)
while preserving the onToggleSave behavior.

In `@apps/mobile/components/useColorScheme.ts`:
- Around line 3-6: Update useColorScheme to normalize a null result from
useColorSchemeCore to 'light', alongside the existing 'unspecified' fallback.
Preserve the existing scheme value for all supported non-null schemes so
downstream Colors[colorScheme] access remains valid.

In `@apps/web/src/app/api/account/route.ts`:
- Around line 11-14: Update the deletion flow around client.users.deleteUser and
prisma.user.deleteMany to handle partial failures: delete the Prisma records
first, then delete the Clerk user, and wrap both operations in try/catch with
appropriate error logging and propagation. Preserve the existing userId-based
deletion behavior while ensuring failures are observable and retryable.

In `@apps/web/src/app/api/progress/finish-level/route.ts`:
- Around line 17-21: Validate the parsed FinishLevelBody fields in POST before
processing the level: require a valid levelId, ensure correct and total are
integers with 0 ≤ correct ≤ total, enforce xpEarned as an integer from 0 through
1000, and validate today with the same date-format and calendar-date rules used
by finish-review. Reuse shared MAX_XP_PER_REQUEST, DATE_RE, and isValidDateStr
symbols if extracted, and return the established invalid-input response for
failures.
- Around line 23-45: Update the finish-level POST flow to create or upsert the
user before processing progress, replacing findUniqueOrThrow so first-time
signed-in requests do not fail; apply the same user-handling change in
finish-review. Remove the stale prevLevel read and make completedLevel.best
monotonic through an atomic database-side update that never lowers an existing
score, while preserving the current XP, streak, total, and create behavior.

In `@apps/web/src/app/api/progress/finish-review/route.ts`:
- Around line 32-45: Move the user lookup and streak calculation into the
prisma.$transaction callback so they execute within the same transaction as the
user update and xpLog.upsert. Update the transaction flow around the user update
to derive bumpStreakServer inputs from the transaction-local user record,
preventing stale streak state from being used by concurrent requests.

In `@apps/web/src/app/api/progress/save-toggle/route.ts`:
- Line 17: Validate questionId immediately after parsing the request body in the
save-toggle route and before any Prisma/database operations; reject missing or
empty values with the route’s existing client-error response pattern, while
preserving the current flow for valid question IDs.

In `@apps/web/src/components/AccountHeader.tsx`:
- Around line 85-102: The successful upload path in handleAvatarFile must
immediately persist DEFAULT_POS to unsafeMetadata when resetting local pos, so
abandoning repositioning cannot reuse the previous photo’s crop. Update the
existing user metadata update mechanism used by confirmReposition, while
preserving the current repositioning state and upload error handling.
- Line 241: Update the user ID span in the AccountHeader component to render the
intended Clerk user identifier instead of the literal “USER.ID”; if no
identifier is available or the field is unnecessary, remove the span.
- Around line 68-70: Update the useEffect that synchronizes pos from
user.unsafeMetadata so it does not overwrite an in-progress avatar reposition:
gate the sync with !isRepositioning or otherwise narrow the dependency to avoid
unrelated Clerk user updates, while preserving metadata hydration when
repositioning is inactive.

In `@apps/web/src/index.css`:
- Around line 866-868: Update the fill value in the .icon-filled CSS rule to use
the lowercase keyword expected by stylelint, while preserving the existing
current-color behavior.
- Line 29: Update the --font-mono font-family declaration in the root CSS
variables to use the casing expected by stylelint for the Menlo and Consolas
font names, while preserving the existing font-stack order and fallback
behavior.

In `@packages/core/src/data/questions/jsa-9-promises-advanced.json`:
- Line 162: Update the explanation for the Promise.any question to replace the
incorrect character 賭 with 賽 in the phrase describing who succeeds first;
preserve the remaining Chinese wording and explanation unchanged.

In `@packages/core/src/data/questions/react-9-preserving-state.json`:
- Around line 1-270: The quiz data in react-9-preserving-state.json assigns
answer "a" to all questions, and QuestionCard.tsx preserves JSON order, making
the correct choice predictable. Update the question-option rendering flow in
QuestionCard.tsx to shuffle each question’s options while retaining the correct
option identity and answer validation, distributing correct answers across
positions a–d rather than relying on the source order.

In `@README.md`:
- Around line 1-16: Update the README apps/mobile entry to remove the outdated
“規劃中,尚未建立” wording and describe it as the existing Expo React Native workspace,
matching the mobile app files and configuration included in the repository.

---

Outside diff comments:
In `@apps/web/src/app/api/progress/answer/route.ts`:
- Around line 19-23: Validate the parsed AnswerBody immediately after
request.json() in POST before any Prisma operations: require questionId,
correct, and today, ensure today is a valid date string, and return a clear 400
response when validation fails. Preserve the existing unauthorized response and
normal processing for valid input.
- Around line 48-64: Update the answer route’s transaction flow around
dailyStatWrite and prisma.$transaction: derive today server-side using the
existing todayStr() utility from `@easylearn/core` instead of accepting the
client-provided date, and wrap the transaction in try/catch. Return the route’s
established safe error response from the catch while logging or preserving only
appropriate error details, without exposing raw Prisma errors.

In `@apps/web/src/hooks/useProgress.ts`:
- Around line 98-108: Update the migration effect around migratedRef and the
async fetch so the ref is marked true only after server progress loading or
migration completes successfully. Add try/catch handling for fetch, response
parsing, and migration errors, and validate res.ok before consuming the
response; leave the ref unset on failure so later renders can retry without
unhandled promise rejections.

---

Nitpick comments:
In `@apps/mobile/components/QuestionCard.tsx`:
- Around line 77-80: Update the docs link Pressable in QuestionCard so the
Linking.openURL call handles rejected promises, including failures from invalid
URLs or unavailable apps. Add appropriate local error handling without changing
the existing link rendering or press behavior.

In `@apps/mobile/hooks/useProgress.ts`:
- Around line 65-86: Extract the wrongIds Leitner transition logic from
answerQuestion into a shared applyAnswer helper in `@easylearn/core`, covering
correct-answer box advancement and graduation removal plus incorrect-answer
reset and count updates. Update answerQuestion and the corresponding web
useProgress implementation to reuse this helper, while preserving the existing
dailyStats and chapterStats updates.

In `@apps/mobile/lib/tokenCache.ts`:
- Around line 6-9: Update the saveToken method in tokenCache to handle rejected
SecureStore.setItemAsync promises, matching getToken’s error-handling behavior
and preventing unhandled storage errors. Keep the existing key and value
arguments unchanged, and use the established silent-failure behavior unless an
existing fallback is required.

In `@apps/web/prisma/schema.prisma`:
- Around line 9-95: Replace the date-like String fields streakLast, lastWrong,
and the date fields in XpLog and DailyStat with DateTime types, using `@db.Date`
where they represent date-only values. Update related defaults, writes, and
queries to pass DateTime values consistently; if any field must remain a
grouping key, document its required format explicitly.

In `@apps/web/src/app/layout.tsx`:
- Line 29: Replace the manual Google Fonts link tags in RootLayout with
next/font/google font imports for JetBrains Mono and Noto Sans TC, configure
their variables, and apply both variables to the body className. Remove the
corresponding external font links while preserving the existing layout
structure.
- Line 29: Update the RootLayout props type to import and use ReactNode directly
instead of referencing the implicit React UMD global. Verify the web app’s
tsconfig JSX mode and preserve the existing layout behavior while eliminating
the React namespace dependency.

In `@apps/web/src/components/CodeBlock.tsx`:
- Around line 7-28: Update the reduce callback in highlight to append gaps and
token spans by mutating acc.parts with push instead of recreating the array via
...acc.parts on every match. Preserve the existing token ordering, keys, and
last-index tracking while keeping the final trailing-text handling unchanged.

In `@apps/web/src/components/GrowthHistory.tsx`:
- Line 1: Extract the shared STAGES data and getStage stage logic from
Mascot.tsx into a dedicated stages module, then update GrowthHistory, Mascot,
and Profile.tsx (including getNextStage usage) to import these symbols from the
new module. Keep the existing stage behavior unchanged and remove the duplicated
or re-exported domain logic from the component module.

In `@apps/web/src/components/Mascot.tsx`:
- Around line 25-26: Update the Mascot component’s mood prop type to a union of
the known supported mood values, ensuring each value matches an available mood
class used by the className interpolation. Keep the prop optional and avoid
narrowing it until the complete supported mood set is identified.

In `@apps/web/src/hooks/useProgress.ts`:
- Around line 136-140: Update the server-response handlers in the progress
actions, including the paths around the existing setProgress calls at lines
136–140, 153–156, 178–181, and 193–197, to use functional state updates that
merge returned server data into the latest local progress state. Preserve newer
local fields by spreading the current state first and applying the server
response afterward, while leaving the existing local action updates unchanged.

In `@packages/core/src/index.ts`:
- Around line 1-5: Update the barrel exports in the package index to avoid
wildcard re-export collisions by using explicit or namespace-style exports for
types, progressCalc, quiz, chapters, and typeMeta. Preserve access to each
module’s public API while making module ownership unambiguous.

In `@packages/core/src/progressCalc.ts`:
- Around line 6-12: Update the date handling in todayStr and yesterdayStr so
both calculations use the same captured Date instance when invoked together,
preventing a midnight rollover from producing inconsistent dates. Preserve the
existing local YYYY-MM-DD formatting and yesterday offset behavior.
🪄 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 Plus

Run ID: df317be5-a0aa-4970-a924-1b797aeddc03

📥 Commits

Reviewing files that changed from the base of the PR and between c45823e and 032de83.

⛔ Files ignored due to path filters (20)
  • apps/mobile/assets/fonts/SpaceMono-Regular.ttf is excluded by !**/*.ttf
  • apps/mobile/assets/images/android-icon-background.png is excluded by !**/*.png
  • apps/mobile/assets/images/android-icon-foreground.png is excluded by !**/*.png
  • apps/mobile/assets/images/android-icon-monochrome.png is excluded by !**/*.png
  • apps/mobile/assets/images/favicon.png is excluded by !**/*.png
  • apps/mobile/assets/images/icon.png is excluded by !**/*.png
  • apps/mobile/assets/images/splash-icon.png is excluded by !**/*.png
  • apps/web/public/apple-touch-icon.png is excluded by !**/*.png
  • apps/web/public/icon-1024.png is excluded by !**/*.png
  • apps/web/public/icon-120.png is excluded by !**/*.png
  • apps/web/public/icon-180.png is excluded by !**/*.png
  • apps/web/public/icon-192.png is excluded by !**/*.png
  • apps/web/public/icon-29.png is excluded by !**/*.png
  • apps/web/public/icon-40.png is excluded by !**/*.png
  • apps/web/public/icon-512.png is excluded by !**/*.png
  • apps/web/public/icon-540.png is excluded by !**/*.png
  • apps/web/public/icon-60.png is excluded by !**/*.png
  • apps/web/public/icon-87.png is excluded by !**/*.png
  • apps/web/src/assets/vite.svg is excluded by !**/*.svg
  • yarn.lock is excluded by !**/yarn.lock, !**/*.lock
📒 Files selected for processing (117)
  • README.md
  • apps/mobile/.env.example
  • apps/mobile/.gitignore
  • apps/mobile/.vscode/extensions.json
  • apps/mobile/.vscode/settings.json
  • apps/mobile/LICENSE
  • apps/mobile/app.json
  • apps/mobile/app/(tabs)/_layout.tsx
  • apps/mobile/app/(tabs)/index.tsx
  • apps/mobile/app/(tabs)/notes.tsx
  • apps/mobile/app/(tabs)/profile.tsx
  • apps/mobile/app/(tabs)/stats.tsx
  • apps/mobile/app/+html.tsx
  • apps/mobile/app/+not-found.tsx
  • apps/mobile/app/_layout.tsx
  • apps/mobile/components/CodeBlock.tsx
  • apps/mobile/components/ComingSoon.tsx
  • apps/mobile/components/Icon.tsx
  • apps/mobile/components/QuestionCard.tsx
  • apps/mobile/components/StyledText.tsx
  • apps/mobile/components/Themed.tsx
  • apps/mobile/components/useClientOnlyValue.ts
  • apps/mobile/components/useClientOnlyValue.web.ts
  • apps/mobile/components/useColorScheme.ts
  • apps/mobile/components/useColorScheme.web.ts
  • apps/mobile/constants/Colors.ts
  • apps/mobile/hooks/useProgress.ts
  • apps/mobile/lib/api.ts
  • apps/mobile/lib/tokenCache.ts
  • apps/mobile/metro.config.js
  • apps/mobile/package.json
  • apps/mobile/screens/ChapterMap.tsx
  • apps/mobile/screens/Home.tsx
  • apps/mobile/screens/Quiz.tsx
  • apps/mobile/tsconfig.json
  • apps/web/.env.example
  • apps/web/.oxlintrc.json
  • apps/web/next.config.ts
  • apps/web/package.json
  • apps/web/prisma.config.ts
  • apps/web/prisma/migrations/20260711114101_init/migration.sql
  • apps/web/prisma/migrations/migration_lock.toml
  • apps/web/prisma/schema.prisma
  • apps/web/public/manifest.webmanifest
  • apps/web/scripts/validate-questions.mjs
  • apps/web/src/App.tsx
  • apps/web/src/app/api/account/route.ts
  • apps/web/src/app/api/progress/answer/route.ts
  • apps/web/src/app/api/progress/finish-level/route.ts
  • apps/web/src/app/api/progress/finish-review/route.ts
  • apps/web/src/app/api/progress/migrate-local/route.ts
  • apps/web/src/app/api/progress/route.ts
  • apps/web/src/app/api/progress/save-toggle/route.ts
  • apps/web/src/app/layout.tsx
  • apps/web/src/app/page.tsx
  • apps/web/src/components/AccountHeader.tsx
  • apps/web/src/components/CodeBlock.tsx
  • apps/web/src/components/GrowthHistory.tsx
  • apps/web/src/components/Icons.tsx
  • apps/web/src/components/Mascot.tsx
  • apps/web/src/components/Navbar.tsx
  • apps/web/src/components/QuestionCard.tsx
  • apps/web/src/components/QuestionReview.tsx
  • apps/web/src/hooks/useProgress.ts
  • apps/web/src/index.css
  • apps/web/src/lib/prisma.ts
  • apps/web/src/lib/progressLogic.ts
  • apps/web/src/lib/progressStore.ts
  • apps/web/src/proxy.ts
  • apps/web/src/screens/ChapterMap.tsx
  • apps/web/src/screens/Home.tsx
  • apps/web/src/screens/Notes.tsx
  • apps/web/src/screens/Profile.tsx
  • apps/web/src/screens/QuestionBook.tsx
  • apps/web/src/screens/Quiz.tsx
  • apps/web/src/screens/Stats.tsx
  • apps/web/tsconfig.json
  • docs/rn-migration.md
  • next.config.ts
  • package.json
  • packages/core/package.json
  • packages/core/src/data/chapters.ts
  • packages/core/src/data/questions/jsa-1-this-scope.json
  • packages/core/src/data/questions/jsa-2-closures.json
  • packages/core/src/data/questions/jsa-3-async-basics.json
  • packages/core/src/data/questions/jsa-4-async-await.json
  • packages/core/src/data/questions/jsa-5-classes.json
  • packages/core/src/data/questions/jsa-6-collections-json.json
  • packages/core/src/data/questions/jsa-7-modules.json
  • packages/core/src/data/questions/jsa-8-iterators-generators.json
  • packages/core/src/data/questions/jsa-9-promises-advanced.json
  • packages/core/src/data/questions/jsb-1-variables.json
  • packages/core/src/data/questions/jsb-2-functions.json
  • packages/core/src/data/questions/jsb-3-arrays.json
  • packages/core/src/data/questions/jsb-4-objects.json
  • packages/core/src/data/questions/jsb-5-control-flow.json
  • packages/core/src/data/questions/jsb-6-loops.json
  • packages/core/src/data/questions/jsb-7-numbers-dates.json
  • packages/core/src/data/questions/jsb-8-strings.json
  • packages/core/src/data/questions/jsb-9-regex.json
  • packages/core/src/data/questions/react-1-jsx.json
  • packages/core/src/data/questions/react-10-reducer-context.json
  • packages/core/src/data/questions/react-11-refs.json
  • packages/core/src/data/questions/react-2-props.json
  • packages/core/src/data/questions/react-3-state-events.json
  • packages/core/src/data/questions/react-4-hooks.json
  • packages/core/src/data/questions/react-5-lists-conditional.json
  • packages/core/src/data/questions/react-6-updating-state.json
  • packages/core/src/data/questions/react-7-pure-components.json
  • packages/core/src/data/questions/react-8-sharing-state.json
  • packages/core/src/data/questions/react-9-preserving-state.json
  • packages/core/src/data/typeMeta.ts
  • packages/core/src/index.ts
  • packages/core/src/progressCalc.ts
  • packages/core/src/quiz.ts
  • packages/core/src/types.ts
  • packages/core/tsconfig.json
💤 Files with no reviewable changes (1)
  • next.config.ts

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Caution

Inline review comments failed to post. This is likely due to GitHub's internal server error or limits when posting large numbers of comments. If you are seeing this consistently it is likely a permissions issue. Please check "Moderation" -> "Code review limits" under your organization settings.

🛑 Comments failed to post (16)
apps/mobile/app/(tabs)/_layout.tsx (1)

9-19: 🩺 Stability & Availability | 🟠 Major

Downstream impact: Colors[colorScheme] will crash if useColorScheme returns null.

Colors[colorScheme].tint on line 15 will throw a TypeError if colorScheme is null, which can happen when RN's useColorScheme returns null. See the review comment on useColorScheme.ts for the root-cause fix.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/mobile/app/`(tabs)/_layout.tsx around lines 9 - 19, Update TabLayout’s
tabBarActiveTintColor access to safely handle a null result from useColorScheme,
using the existing light/dark theme fallback so Colors is always indexed by a
valid key. Preserve the current tint selection for non-null color schemes.
apps/mobile/components/QuestionCard.tsx (1)

27-43: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

saved prop is destructured but never used — the star gives no visual feedback.

The saved prop is received but the star icon is always <Icon name="star" size={18} />. Users cannot tell whether a question is saved or not. Use saved to toggle the icon appearance (e.g., filled vs outline, or a color change).

🐛 Proposed fix
         <Pressable onPress={() => onToggleSave(question.id)} hitSlop={8}>
-          <Icon name="star" size={18} />
+          <Icon name={saved ? 'star-filled' : 'star'} size={18} />
         </Pressable>
📝 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 answered = selected !== null;
  const correct = answered && selected === question.answer;
  const typeMeta = TYPE_META[question.type];

  return (
    <View style={styles.card}>
      <View style={styles.metaRow}>
        <View style={styles.typeBadge}>
          <Icon name={typeMeta.icon} size={13} />
          <Text style={styles.typeBadgeText}>{typeMeta.label}</Text>
        </View>
        <Text style={styles.topicLabel} numberOfLines={1}>
          {question.topic}
        </Text>
        <Pressable onPress={() => onToggleSave(question.id)} hitSlop={8}>
          <Icon name={saved ? 'star-filled' : 'star'} size={18} />
        </Pressable>
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/mobile/components/QuestionCard.tsx` around lines 27 - 43, Update the
star Icon in QuestionCard to use the destructured saved prop for visible state
feedback, switching between the existing filled and outline appearance (or an
equivalent color distinction) while preserving the onToggleSave behavior.
apps/mobile/components/useColorScheme.ts (1)

3-6: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

null return from useColorSchemeCore is not handled.

React Native's useColorScheme() can return null (e.g., during initial render or on platforms where the scheme is unavailable). The current code normalizes 'unspecified''light' but passes null through. Downstream, Colors[colorScheme].tint in _layout.tsx would evaluate to Colors[null].tintundefined.tintTypeError crash.

🛡️ Proposed fix: also normalize null to 'light'
 export const useColorScheme = () => {
   const coreScheme = useColorSchemeCore();
-  return coreScheme === 'unspecified' ? 'light' : coreScheme;
+  return coreScheme === 'unspecified' || coreScheme === null ? 'light' : coreScheme;
 };
📝 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.

export const useColorScheme = () => {
  const coreScheme = useColorSchemeCore();
  return coreScheme === 'unspecified' || coreScheme === null ? 'light' : coreScheme;
};
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/mobile/components/useColorScheme.ts` around lines 3 - 6, Update
useColorScheme to normalize a null result from useColorSchemeCore to 'light',
alongside the existing 'unspecified' fallback. Preserve the existing scheme
value for all supported non-null schemes so downstream Colors[colorScheme]
access remains valid.
apps/web/src/app/api/account/route.ts (1)

11-14: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

No error handling for partial failure between Clerk and Prisma deletions.

If client.users.deleteUser(userId) succeeds but prisma.user.deleteMany throws, the user is permanently deleted from Clerk while their database records remain orphaned. There is no try/catch, compensation logic, or logging to detect this inconsistency.

🛡️ Suggested fix: wrap in try/catch with compensation
 export const DELETE = async () => {
   const { userId } = await auth()
   if (!userId) return NextResponse.json({ error: 'unauthorized' }, { status: 401 })

   const client = await clerkClient()
-
-  await client.users.deleteUser(userId)
-
-  await prisma.user.deleteMany({ where: { id: userId } })
+  try {
+    await prisma.user.deleteMany({ where: { id: userId } })
+    await client.users.deleteUser(userId)
+  } catch (err) {
+    console.error('Account deletion failed:', err)
+    return NextResponse.json({ error: 'deletion failed' }, { status: 500 })
+  }

   return NextResponse.json({ ok: true })
 }

Reversing the order (Prisma first, Clerk second) reduces the window for inconsistency — if Prisma fails, the Clerk user is untouched and can retry. If Clerk fails after Prisma succeeds, the DB records are gone but the user can still re-auth and retry deletion.

📝 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 client = await clerkClient()
  try {
    await prisma.user.deleteMany({ where: { id: userId } })
    await client.users.deleteUser(userId)
  } catch (err) {
    console.error('Account deletion failed:', err)
    return NextResponse.json({ error: 'deletion failed' }, { status: 500 })
  }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/web/src/app/api/account/route.ts` around lines 11 - 14, Update the
deletion flow around client.users.deleteUser and prisma.user.deleteMany to
handle partial failures: delete the Prisma records first, then delete the Clerk
user, and wrap both operations in try/catch with appropriate error logging and
propagation. Preserve the existing userId-based deletion behavior while ensuring
failures are observable and retryable.
apps/web/src/app/api/progress/finish-level/route.ts (2)

17-21: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Add input validation consistent with finish-review route.

This route accepts levelId, correct, total, xpEarned, and today without any validation, while finish-review/route.ts validates xpEarned (integer, 0–1000) and today (date format). A malicious client can inflate XP with arbitrary xpEarned, send invalid dates that corrupt xpLog entries, or set correct > total.

🛡️ Proposed validation
 export const POST = async (request: Request) => {
   const { userId } = await auth()
   if (!userId) return NextResponse.json({ error: 'unauthorized' }, { status: 401 })
 
   const { levelId, correct, total, xpEarned, today } = (await request.json()) as FinishLevelBody
+  const MAX_XP_PER_REQUEST = 1000
+  const DATE_RE = /^\d{4}-\d{2}-\d{2}$/
+  const isValidDateStr = (value: string) => DATE_RE.test(value) && !Number.isNaN(new Date(value).getTime())
+
+  if (typeof levelId !== 'string' || !levelId) {
+    return NextResponse.json({ error: 'invalid levelId' }, { status: 400 })
+  }
+  if (!Number.isInteger(correct) || correct < 0 || !Number.isInteger(total) || total <= 0 || correct > total) {
+    return NextResponse.json({ error: 'invalid correct/total' }, { status: 400 })
+  }
+  if (!Number.isInteger(xpEarned) || xpEarned < 0 || xpEarned > MAX_XP_PER_REQUEST) {
+    return NextResponse.json({ error: 'invalid xpEarned' }, { status: 400 })
+  }
+  if (typeof today !== 'string' || !isValidDateStr(today)) {
+    return NextResponse.json({ error: 'invalid today' }, { status: 400 })
+  }

Consider extracting MAX_XP_PER_REQUEST, DATE_RE, and isValidDateStr to a shared module to avoid duplication with finish-review/route.ts.

📝 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.

export const POST = async (request: Request) => {
  const { userId } = await auth()
  if (!userId) return NextResponse.json({ error: 'unauthorized' }, { status: 401 })

  const { levelId, correct, total, xpEarned, today } = (await request.json()) as FinishLevelBody
  const MAX_XP_PER_REQUEST = 1000
  const DATE_RE = /^\d{4}-\d{2}-\d{2}$/
  const isValidDateStr = (value: string) => DATE_RE.test(value) && !Number.isNaN(new Date(value).getTime())

  if (typeof levelId !== 'string' || !levelId) {
    return NextResponse.json({ error: 'invalid levelId' }, { status: 400 })
  }
  if (!Number.isInteger(correct) || correct < 0 || !Number.isInteger(total) || total <= 0 || correct > total) {
    return NextResponse.json({ error: 'invalid correct/total' }, { status: 400 })
  }
  if (!Number.isInteger(xpEarned) || xpEarned < 0 || xpEarned > MAX_XP_PER_REQUEST) {
    return NextResponse.json({ error: 'invalid xpEarned' }, { status: 400 })
  }
  if (typeof today !== 'string' || !isValidDateStr(today)) {
    return NextResponse.json({ error: 'invalid today' }, { status: 400 })
  }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/web/src/app/api/progress/finish-level/route.ts` around lines 17 - 21,
Validate the parsed FinishLevelBody fields in POST before processing the level:
require a valid levelId, ensure correct and total are integers with 0 ≤ correct
≤ total, enforce xpEarned as an integer from 0 through 1000, and validate today
with the same date-format and calendar-date rules used by finish-review. Reuse
shared MAX_XP_PER_REQUEST, DATE_RE, and isValidDateStr symbols if extracted, and
return the established invalid-input response for failures.

23-45: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Avoid stale best writes and missing-user 500s

  • completedLevel.best is read before the write, so concurrent requests can still overwrite a higher score with a lower one.
  • findUniqueOrThrow also assumes the user row already exists; the first signed-in POST can fail before /api/progress creates it. finish-review/route.ts has the same issue.

Move user creation into the POST path and make the best update atomic instead of relying on a stale pre-read.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/web/src/app/api/progress/finish-level/route.ts` around lines 23 - 45,
Update the finish-level POST flow to create or upsert the user before processing
progress, replacing findUniqueOrThrow so first-time signed-in requests do not
fail; apply the same user-handling change in finish-review. Remove the stale
prevLevel read and make completedLevel.best monotonic through an atomic
database-side update that never lowers an existing score, while preserving the
current XP, streak, total, and create behavior.
apps/web/src/app/api/progress/finish-review/route.ts (1)

32-45: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '--- finish-review route ---\n'
sed -n '1,220p' apps/web/src/app/api/progress/finish-review/route.ts

printf '\n--- finish-level route ---\n'
sed -n '1,220p' apps/web/src/app/api/progress/finish-level/route.ts

printf '\n--- search for user upsert / streak usage ---\n'
rg -n "bumpStreakServer|streakCount|streakLast|findUniqueOrThrow\\(|user\\.upsert\\(|prisma\\.\\$transaction" apps/web/src/app/api -S

Repository: Retsomm/EasyLearn

Length of output: 246


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- finish-review route ---'
sed -n '1,220p' apps/web/src/app/api/progress/finish-review/route.ts

printf '\n%s\n' '--- finish-level route ---'
sed -n '1,220p' apps/web/src/app/api/progress/finish-level/route.ts

printf '\n%s\n' '--- search for user upsert / streak usage ---'
rg -n "bumpStreakServer|streakCount|streakLast|findUniqueOrThrow\\(|user\\.upsert\\(|prisma\\.\\$transaction" apps/web/src/app/api -S

Repository: Retsomm/EasyLearn

Length of output: 3771


🏁 Script executed:

#!/bin/bash
set -euo pipefail

sed -n '1,220p' apps/web/src/app/api/progress/finish-review/route.ts
printf '\n---\n'
sed -n '1,220p' apps/web/src/app/api/progress/finish-level/route.ts
printf '\n---\n'
rg -n "findUniqueOrThrow\\(|bumpStreakServer|streakCount|streakLast|prisma\\.\\$transaction|user\\.upsert\\(" apps/web/src/app/api -S

Repository: Retsomm/EasyLearn

Length of output: 3677


🏁 Script executed:

#!/bin/bash
set -euo pipefail

rg -n "prisma\.user\.(create|upsert|findUniqueOrThrow|findUnique)|model User|createUser|sync.*user|clerk" apps/web/src lib prisma -S

Repository: Retsomm/EasyLearn

Length of output: 2136


🏁 Script executed:

#!/bin/bash
set -euo pipefail

rg -n "prisma\.user\.(create|upsert|findUniqueOrThrow|findUnique)|model User|createUser|sync.*user|clerk" apps/web src lib prisma -S

Repository: Retsomm/EasyLearn

Length of output: 2283


🏁 Script executed:

#!/bin/bash
set -euo pipefail

rg -n "prisma\.user\.(create|upsert|findUniqueOrThrow|findUnique)|model User|createUser|sync.*user|clerk" apps/web lib prisma -S

Repository: Retsomm/EasyLearn

Length of output: 2239


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- apps/web/src/lib/progressStore.ts ---'
sed -n '1,220p' apps/web/src/lib/progressStore.ts

printf '\n%s\n' '--- apps/web/src/app/api/progress/route.ts ---'
sed -n '1,220p' apps/web/src/app/api/progress/route.ts

Repository: Retsomm/EasyLearn

Length of output: 1942


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- apps/web/src/hooks/useProgress.ts ---'
sed -n '1,240p' apps/web/src/hooks/useProgress.ts

printf '\n%s\n' '--- search progress API usage ---'
rg -n "/api/progress/finish-review|/api/progress/finish-level|/api/progress\"|loadFullProgress\\(" apps/web/src -S

Repository: Retsomm/EasyLearn

Length of output: 7617


Read streak state inside the transaction. In apps/web/src/app/api/progress/finish-review/route.ts, user is loaded before prisma.$transaction, so concurrent requests can compute from stale streakCount/streakLast and overwrite a newer streak.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/web/src/app/api/progress/finish-review/route.ts` around lines 32 - 45,
Move the user lookup and streak calculation into the prisma.$transaction
callback so they execute within the same transaction as the user update and
xpLog.upsert. Update the transaction flow around the user update to derive
bumpStreakServer inputs from the transaction-local user record, preventing stale
streak state from being used by concurrent requests.
apps/web/src/app/api/progress/save-toggle/route.ts (1)

17-17: 🔒 Security & Privacy | 🟡 Minor | ⚡ Quick win

Validate questionId before database operations.

questionId is used without validation. An empty or missing value could cause unexpected Prisma errors or create invalid records.

🛡️ Proposed validation
   const { questionId } = (await request.json()) as SaveToggleBody
+
+  if (typeof questionId !== 'string' || !questionId) {
+    return NextResponse.json({ error: 'invalid questionId' }, { status: 400 })
+  }
+
   const existing = await prisma.savedQuestion.findUnique({ where: { userId_questionId: { userId, questionId } } })
📝 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 { questionId } = (await request.json()) as SaveToggleBody

  if (typeof questionId !== 'string' || !questionId) {
    return NextResponse.json({ error: 'invalid questionId' }, { status: 400 })
  }

  const existing = await prisma.savedQuestion.findUnique({ where: { userId_questionId: { userId, questionId } } })
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/web/src/app/api/progress/save-toggle/route.ts` at line 17, Validate
questionId immediately after parsing the request body in the save-toggle route
and before any Prisma/database operations; reject missing or empty values with
the route’s existing client-error response pattern, while preserving the current
flow for valid question IDs.
apps/web/src/components/AccountHeader.tsx (3)

68-70: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Map the component and inspect relevant lines.
git ls-files 'apps/web/src/components/AccountHeader.tsx'
wc -l apps/web/src/components/AccountHeader.tsx
cat -n apps/web/src/components/AccountHeader.tsx | sed -n '1,360p'

# Search for the avatar position helpers and related state handling.
rg -n "readAvatarPosition|DEFAULT_POS|isRepositioning|saveMetadata|handleAvatarFile|saveName|unsafeMetadata|avatarPosition" apps/web/src/components/AccountHeader.tsx apps/web/src -S

# Look for Clerk SDK usage patterns that might affect referential stability assumptions.
rg -n "useUser\\(|unsafeMetadata|user.update\\(" apps/web/src -S

Repository: Retsomm/EasyLearn

Length of output: 17452


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Find the Clerk package version used by the web app.
git ls-files 'apps/web/package.json' 'package.json' 'pnpm-lock.yaml' 'yarn.lock' 'package-lock.json'
cat -n apps/web/package.json | sed -n '1,220p'
rg -n '"`@clerk/nextjs`"|`@clerk/nextjs`' apps/web/package.json package.json pnpm-lock.yaml yarn.lock package-lock.json -S

# Inspect any local Clerk-related type or usage hints.
rg -n "unsafeMetadata|useUser\\(" apps/web -S

Repository: Retsomm/EasyLearn

Length of output: 1694


🌐 Web query:

Clerk @clerk/nextjs 7.5.17 useUser unsafeMetadata object reference update unrelated user update firstName snapshot

💡 Result:

In Clerk SDK version 7.5.17 and later, updating user metadata via the user.update() method is deprecated [1][2]. You should instead use the dedicated user.updateMetadata() method [3][2]. Key points regarding your query: 1. Deprecation of Metadata in update: The user.update() method previously accepted unsafeMetadata, but it performed a full replacement rather than a merge [4][2]. This behavior is now deprecated to prevent accidental data loss [1][2]. 2. Preferred Method: Use user.updateMetadata({ unsafeMetadata: { ... } }) [2][5]. This method performs a deep merge of the provided object with the existing unsafeMetadata, making it safer and more efficient for partial updates [6][2]. 3. Unrelated Updates: Updating unsafeMetadata via updateMetadata() is distinct from updating properties like firstName [5]. If you need to update both, you should perform them as separate operations: user.update({ firstName: 'NewName' }) for standard profile fields and user.updateMetadata({ unsafeMetadata: { ... } }) for metadata [7][2]. 4. Snapshot/State Consistency: The useUser() hook provides a reactive user object [7]. After calling an update method, the Clerk SDK typically handles synchronization automatically; however, if you find that state does not reflect immediately in complex scenarios, you can call user.reload() to force a refresh of the user object data [7]. By migrating to updateMetadata(), you avoid the pitfalls of object reference replacement and benefit from the intended deep-merge functionality [6][2].

Citations:


🌐 Web query:

site:clerk.com/docs/nextjs/reference/hooks/use-user user object reference update on unrelated field change unsafeMetadata

💡 Result:

There is no documented property or feature named unsafeMetadata associated with the Clerk user object or its metadata management in the provided search results [1]. Clerk organizes metadata into categories such as publicMetadata and privateMetadata, which are managed through specific update methods or backend API calls [1]. When updating user data, such as metadata, it is best practice to use the established Clerk User object methods (like user.update) or, if performing updates via an external API endpoint, to call the user.reload method to ensure the local user object reference remains synchronized with the latest server-side state [1]. Stale object references can occur if the user object is not refreshed after backend changes [1].

Citations:


🌐 Web query:

Clerk useUser hook reactive user object reference unsafeMetadata same reference unrelated update firstName docs source

💡 Result:

The Clerk useUser hook provides access to a reactive User object [1][2]. Because this hook is designed to keep your UI in sync with the current user's authentication state, the User object returned by useUser is reactive; it will update whenever the underlying user data changes (e.g., after calling user.update or user.reload) [1][3]. Regarding reference stability and unrelated updates: 1. Reactive Behavior: When Clerk detects a change to user data (such as an update to firstName or unsafeMetadata), the useUser hook triggers a re-render in components consuming it [1][3]. 2. Reference Changes: Because the User object is updated to reflect the new state, the reference to the user object may change when updates occur. If you are relying on strict reference equality (e.g., using the user object as a dependency in useEffect or useMemo), you should be aware that this reference can change when the user data is updated or reloaded [1]. 3. UnsafeMetadata Updates: While unsafeMetadata is readable and writable from the frontend, updating it (or other fields like firstName) will cause the useUser hook to provide a new User object instance [4][5]. To perform updates without manually managing state, the User object provides built-in methods like update (for full replacement) and updateMetadata (for deep-merging) [4][6]. 4. Best Practices: If you need to perform an action triggered by a specific user property change, consider using the specific field (e.g., user.firstName) as a dependency in your hooks rather than the entire user object [1]. In summary, the User object is not guaranteed to maintain a stable reference across updates; it is a reactive proxy managed by Clerk to ensure your frontend always has access to the most current session data [1][7].

Citations:


Prevent avatar edits from being reset by user refreshes. apps/web/src/components/AccountHeader.tsx:68-70 rehydrates pos from user.unsafeMetadata on every Clerk user update, and useUser() is reactive here—an unrelated user.update({ firstName }) can still swap in a new user object and retrigger this effect mid-drag, clobbering the in-progress reposition. Gate the sync behind !isRepositioning or narrow the dependency.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/web/src/components/AccountHeader.tsx` around lines 68 - 70, Update the
useEffect that synchronizes pos from user.unsafeMetadata so it does not
overwrite an in-progress avatar reposition: gate the sync with !isRepositioning
or otherwise narrow the dependency to avoid unrelated Clerk user updates, while
preserving metadata hydration when repositioning is inactive.

85-102: 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

Stale avatarPosition metadata if upload is abandoned mid-reposition.

After a successful upload, local pos resets to DEFAULT_POS but nothing is persisted to unsafeMetadata until confirmReposition runs. If the user uploads a new photo and leaves without confirming, the next load reapplies the previous photo's saved crop coordinates to the new photo via readAvatarPosition.

🐛 Proposed fix
       await user.setProfileImage({ file })
+      await saveMetadata(DEFAULT_POS)
       setPos(DEFAULT_POS)
       setPosBeforeEdit(DEFAULT_POS)
       setIsRepositioning(true)
📝 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 handleAvatarFile = async (e: ChangeEvent<HTMLInputElement>) => {
    const file = e.target.files?.[0]
    e.target.value = ''
    if (!file) return
    setPosBeforeEdit(pos)
    setUploading(true)
    try {
      await user.setProfileImage({ file })
      await saveMetadata(DEFAULT_POS)
      setPos(DEFAULT_POS)
      setPosBeforeEdit(DEFAULT_POS)
      setIsRepositioning(true)
    } catch (err) {
      console.error('avatar upload failed', err)
      alert('上傳照片失敗,請稍後再試')
    } finally {
      setUploading(false)
    }
  }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/web/src/components/AccountHeader.tsx` around lines 85 - 102, The
successful upload path in handleAvatarFile must immediately persist DEFAULT_POS
to unsafeMetadata when resetting local pos, so abandoning repositioning cannot
reuse the previous photo’s crop. Update the existing user metadata update
mechanism used by confirmReposition, while preserving the current repositioning
state and upload error handling.

241-241: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

ast-grep outline apps/web/src/components/AccountHeader.tsx --view expanded

Repository: Retsomm/EasyLearn

Length of output: 854


🏁 Script executed:

cat -n apps/web/src/components/AccountHeader.tsx | sed -n '220,255p'

Repository: Retsomm/EasyLearn

Length of output: 1739


🏁 Script executed:

rg -n "USER\.ID|account-userid|username|user\.id|useUser" apps/web/src/components/AccountHeader.tsx apps/web/src -g '!**/node_modules/**'

Repository: Retsomm/EasyLearn

Length of output: 1301


🏁 Script executed:

rg -n "\.username\b|user\.id\b|account-userid|firstName|lastName" apps/web/src -g '!**/node_modules/**'

Repository: Retsomm/EasyLearn

Length of output: 1103


Replace the placeholder user id text
USER.ID is rendered literally here; bind this to the intended Clerk identifier or remove the field if it’s not needed.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/web/src/components/AccountHeader.tsx` at line 241, Update the user ID
span in the AccountHeader component to render the intended Clerk user identifier
instead of the literal “USER.ID”; if no identifier is available or the field is
unnecessary, remove the span.
apps/web/src/index.css (2)

29-29: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Fix stylelint value-keyword-case violations.

Stylelint flags Menlo/Consolas casing in the font stack.

🔧 Proposed fix
-  --font-mono: 'JetBrains Mono', 'SF Mono', Menlo, Consolas, monospace;
+  --font-mono: 'JetBrains Mono', 'SF Mono', menlo, consolas, monospace;
📝 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.

  --font-mono: 'JetBrains Mono', 'SF Mono', menlo, consolas, monospace;
🧰 Tools
🪛 Stylelint (17.14.0)

[error] 29-29: Expected "Menlo" to be "menlo" (value-keyword-case)

(value-keyword-case)


[error] 29-29: Expected "Consolas" to be "consolas" (value-keyword-case)

(value-keyword-case)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/web/src/index.css` at line 29, Update the --font-mono font-family
declaration in the root CSS variables to use the casing expected by stylelint
for the Menlo and Consolas font names, while preserving the existing font-stack
order and fallback behavior.

Source: Linters/SAST tools


866-868: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Fix stylelint value-keyword-case violation.

Stylelint flags currentColor, expects lowercase.

🔧 Proposed fix
 .icon-filled {
-  fill: currentColor;
+  fill: currentcolor;
 }
📝 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.

.icon-filled {
  fill: currentcolor;
}
🧰 Tools
🪛 Stylelint (17.14.0)

[error] 867-867: Expected "currentColor" to be "currentcolor" (value-keyword-case)

(value-keyword-case)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/web/src/index.css` around lines 866 - 868, Update the fill value in the
.icon-filled CSS rule to use the lowercase keyword expected by stylelint, while
preserving the existing current-color behavior.

Source: Linters/SAST tools

packages/core/src/data/questions/jsa-9-promises-advanced.json (1)

162-162: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Possible typo in q7 explanation: 賭 → 賽.

The explanation reads "any 賭的是「誰先成功」". The character 賭 (zhì, "to lose") doesn't fit the context of "any competes for whoever succeeds first". It should likely be 賽 (sài, "to compete/race"), matching the pattern used elsewhere in this file (e.g., q2's "race 是賽跑").

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/core/src/data/questions/jsa-9-promises-advanced.json` at line 162,
Update the explanation for the Promise.any question to replace the incorrect
character 賭 with 賽 in the phrase describing who succeeds first; preserve the
remaining Chinese wording and explanation unchanged.
packages/core/src/data/questions/react-9-preserving-state.json (1)

1-270: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

All 12 questions have answer "a" — the quiz is trivially gameable.

Every correct answer is option a, and QuestionCard.tsx renders options in JSON order without shuffling. Users will immediately notice the pattern and always pick the first option, defeating the quiz's purpose.

Shuffle the option order per question so the correct answer is distributed across positions a–d.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/core/src/data/questions/react-9-preserving-state.json` around lines
1 - 270, The quiz data in react-9-preserving-state.json assigns answer "a" to
all questions, and QuestionCard.tsx preserves JSON order, making the correct
choice predictable. Update the question-option rendering flow in
QuestionCard.tsx to shuffle each question’s options while retaining the correct
option identity and answer validation, distributing correct answers across
positions a–d rather than relying on the source order.
README.md (1)

1-16: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

README says mobile app is "尚未建立" but the PR includes mobile app files.

Line 6 states apps/mobile — Expo React Native App(規劃中,尚未建立), yet this PR adds a full apps/mobile workspace with components, screens, and configuration. Update the description to reflect the actual state.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@README.md` around lines 1 - 16, Update the README apps/mobile entry to remove
the outdated “規劃中,尚未建立” wording and describe it as the existing Expo React
Native workspace, matching the mobile app files and configuration included in
the repository.

- 修掉題庫「正解永遠是 a」的系統性問題:QuestionCard 改成渲染時洗牌選項
- API routes:first-time 使用者 upsert、completedLevel.best 原子更新只升不降、補輸入驗證
- AccountHeader:修正頭像裁切位置未即時存檔、USER.ID 佔位字串、重新命名時中斷拖曳的 race
- packages/core 新增共用 applyAnswer(),web/mobile 兩邊錯題本更新邏輯合併;修 bumpStreak 跨午夜競態
- apps/web、apps/mobile 內部 import 全面改用 @/ alias,不再用 ../ 相對路徑

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@Retsomm
Retsomm merged commit 9ae99fd into main Jul 12, 2026
3 checks passed
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