Skip to content

套用函數式思考原則重構:分離動作與計算、減少可變迴圈 - #9

Merged
Retsomm merged 1 commit into
mainfrom
dev
Jul 12, 2026
Merged

套用函數式思考原則重構:分離動作與計算、減少可變迴圈#9
Retsomm merged 1 commit into
mainfrom
dev

Conversation

@Retsomm

@RetsommRetsomm commented Jul 12, 2026

Copy link
Copy Markdown
Owner
  • chapters.ts:抽出攤平題目清單當抽象屏障,取代重複三層 for 迴圈
  • App.tsx:navGroupOfChapter 改用 find;畫面渲染改成無可變賦值的 switch 表達式
  • Mascot.tsx:getStage 改用 reduce
  • useProgress.ts:migrateWrongIds 改用 Object.fromEntries+map
  • CodeBlock.tsx:highlight 改用 matchAll+reduce 取代 while 迴圈手動維護多個可變變數
  • answer/route.ts:把「決定要寫入什麼」拆成宣告式運算式,跟「執行交易」的動作分開

Summary by CodeRabbit

  • Refactor
    • Improved internal handling of chapter, level, question, and progress lookups.
    • Streamlined screen selection and navigation logic without changing the available screens.
    • Simplified code highlighting and mascot progression calculations.
    • Preserved existing progress tracking, quiz behavior, and saved/wrong-question functionality.

- chapters.ts:抽出攤平題目清單當抽象屏障,取代重複三層 for 迴圈
- App.tsx:navGroupOfChapter 改用 find;畫面渲染改成無可變賦值的 switch 表達式
- Mascot.tsx:getStage 改用 reduce
- useProgress.ts:migrateWrongIds 改用 Object.fromEntries+map
- CodeBlock.tsx:highlight 改用 matchAll+reduce 取代 while 迴圈手動維護多個可變變數
- answer/route.ts:把「決定要寫入什麼」拆成宣告式運算式,跟「執行交易」的動作分開
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@vercel

vercelBot commented Jul 12, 2026

Copy link
Copy Markdown

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

ProjectDeploymentActionsUpdated (UTC)
easy-learnReadyReadyPreview, CommentJul 12, 2026 2:16am

@coderabbitai

coderabbitaiBot commented Jul 12, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The changes refactor chapter/question lookups, progress migration and answer writes, screen selection, syntax highlighting, and mascot stage selection toward functional collection operations while preserving existing public APIs and behavior.

Changes

Core application refactors

Layer / File(s)Summary
Question indexing and progress migration
src/data/chapters.ts, src/hooks/useProgress.ts
Question helpers now use flattened question data, while legacy wrong-question identifiers are migrated with Object.fromEntries.
Answer persistence transaction
src/app/api/progress/answer/route.ts
Wrong-entry, daily-stat, and chapter-stat writes are composed into a single Prisma transaction input.
Screen selection and navigation
src/App.tsx
Chapter lookup uses find/some, and view content uses switch-based selection with precomputed quiz data.
Utility implementation rewrites
src/components/CodeBlock.tsx, src/components/Mascot.tsx
Syntax highlighting uses matchAll/reduce, and mascot stages use reduce selection.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Possibly related PRs

  • Retsomm/EasyLearn#2: Adds chapter-level data exercised by the refactored chapter and question lookup helpers.
  • Retsomm/EasyLearn#3: Extends the wrong-question flow that uses the refactored wrong-question lookups and migration path.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check nameStatusExplanation
Description Check✅ PassedCheck skipped - CodeRabbit’s high-level summary is enabled.
Title check✅ Passed標題準確概括了此次以函數式手法重構、減少可變迴圈與分離計算/動作的主要變更。
Docstring Coverage✅ PassedNo functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check✅ PassedCheck skipped because no linked issues were found for this pull request.
Out of Scope Changes check✅ PassedCheck skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 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.

@coderabbitaicoderabbitaiBot 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.

🧹 Nitpick comments (3)
src/data/chapters.ts (1)

70-71: 🚀 Performance & Scalability | 🔵 Trivial | 💤 Low value

Consider caching the flattened levels array.

getLevel calls chapters.flatMap((ch) => ch.levels) on every invocation, allocating a new array each time. Since it's called on every render in App.tsx when in quiz view, you could cache the flattened levels at module scope (like flatQuestions) for consistency and to avoid repeated allocation.

♻️ Optional: cache flattened levels
+const flatLevels: Level[] = chapters.flatMap((ch) => ch.levels)+
export const getLevel = (levelId: string): Level | null =>
- chapters.flatMap((ch) => ch.levels).find((l) => l.id === levelId) ?? null+ flatLevels.find((l) => l.id === levelId) ?? null
🤖 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 `@src/data/chapters.ts` around lines 70 - 71, Cache the flattened levels array
at module scope, alongside the existing flatQuestions cache, and update getLevel
to search that cached collection instead of calling chapters.flatMap on every
invocation.
src/components/CodeBlock.tsx (1)

9-23: 🚀 Performance & Scalability | 🔵 Trivial | 💤 Low value

Optional: avoid O(n²) spread in reduce.

...acc.parts on each iteration copies the entire accumulator, making this quadratic in the number of matches. For typical code snippets this is negligible, but if you want to keep it functional without the cost, consider collecting into a flat array via concat:

♻️ Optional refactor using concat
 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>,- ],+ parts: acc.parts.concat(gap, [+ <span key={i} className={cls}>+ {text}+ </span>,+ ]),
last: m.index + text.length,
}
},
{ parts: [], last: 0 },
)
🤖 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 `@src/components/CodeBlock.tsx` around lines 9 - 23, Optionally refactor the
matches.reduce accumulator in CodeBlock so each iteration avoids spreading the
full acc.parts array; append the gap and highlighted span through concat or
equivalent in-place collection while preserving output order and the existing
last-index tracking.
src/App.tsx (1)

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

Switch IIFE and quizLevel guard look correct.

The early return on line 75 safely guarantees quizLevel is non-null inside the case 'quiz' branch, making the quizLevel! assertions on lines 83 and 89 safe. All 11 View variants are covered by the switch, with default correctly mapping to Home.

One optional improvement: an explicit case 'home': instead of relying on default would give you compile-time exhaustiveness checking if a new view variant is added later.

♻️ Optional: explicit home case for exhaustiveness
 case 'profile':
return <Profile progress={progress} />
- default:+ case 'home':
return (
<Home
progress={progress}
onOpenChapter={(chapterId) => setView({ name: 'levellist', chapterId })}
onMixedPractice={startMixedPractice}
/>
)
+ default:+ const _exhaustive: never = view+ throw new Error(`Unhandled view: ${JSON.stringify(_exhaustive)}`)
}
🤖 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 `@src/App.tsx` around lines 72 - 191, Optionally replace the fallback default
branch in the content switch with an explicit case 'home' branch returning Home,
while preserving the existing Home props and behavior; use an exhaustiveness
check for any unhandled View variants so future additions are caught at compile
time.
🤖 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.
Nitpick comments:
In `@src/App.tsx`:
- Around line 72-191: Optionally replace the fallback default branch in the
content switch with an explicit case 'home' branch returning Home, while
preserving the existing Home props and behavior; use an exhaustiveness check for
any unhandled View variants so future additions are caught at compile time.
In `@src/components/CodeBlock.tsx`:
- Around line 9-23: Optionally refactor the matches.reduce accumulator in
CodeBlock so each iteration avoids spreading the full acc.parts array; append
the gap and highlighted span through concat or equivalent in-place collection
while preserving output order and the existing last-index tracking.
In `@src/data/chapters.ts`:
- Around line 70-71: Cache the flattened levels array at module scope, alongside
the existing flatQuestions cache, and update getLevel to search that cached
collection instead of calling chapters.flatMap on every invocation.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: bc335efe-a684-4394-b54f-4e2368fec16f

📥 Commits

Reviewing files that changed from the base of the PR and between 877bce3 and d7a2e5b.

📒 Files selected for processing (6)
  • src/App.tsx
  • src/app/api/progress/answer/route.ts
  • src/components/CodeBlock.tsx
  • src/components/Mascot.tsx
  • src/data/chapters.ts
  • src/hooks/useProgress.ts

@Retsomm
Retsomm merged commit 2931137 into mainJul 12, 2026
3 checks passed
Sign up for freeto 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

@Retsomm
, 'i'); if (__m === '*' || __re.test(location.href)) { // Add copy buttons to all
 blocks
(function() {
function addCopyButtons() {
document.querySelectorAll('pre code').forEach(function(codeBlock) {
if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;
codeBlock.parentElement.setAttribute('data-copy-added', 'true');
var btn = document.createElement('button');
btn.textContent = 'Copy';
btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';
btn.onmouseover = function() { this.style.opacity = '1'; };
btn.onmouseout = function() { this.style.opacity = '0.7'; };
btn.onclick = function() {
navigator.clipboard.writeText(codeBlock.textContent).then(function() {
btn.textContent = 'Copied!';
setTimeout(function() { btn.textContent = 'Copy'; }, 1500);
});
};
codeBlock.parentElement.style.position = 'relative';
codeBlock.parentElement.appendChild(btn);
});
}
addCopyButtons();
// Re-run on dynamic content
var observer = new MutationObserver(addCopyButtons);
observer.observe(document.body, { childList: true, subtree: true });
})();
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
套用函數式思考原則重構:分離動作與計算、減少可變迴圈 by Retsomm · Pull Request #9 · Retsomm/EasyLearn · GitHub
Skip to content

套用函數式思考原則重構:分離動作與計算、減少可變迴圈 - #9

Merged
Retsomm merged 1 commit into
mainfrom
dev
Jul 12, 2026
Merged

套用函數式思考原則重構:分離動作與計算、減少可變迴圈#9
Retsomm merged 1 commit into
mainfrom
dev

Conversation

@Retsomm

@RetsommRetsomm commented Jul 12, 2026

Copy link
Copy Markdown
Owner
  • chapters.ts:抽出攤平題目清單當抽象屏障,取代重複三層 for 迴圈
  • App.tsx:navGroupOfChapter 改用 find;畫面渲染改成無可變賦值的 switch 表達式
  • Mascot.tsx:getStage 改用 reduce
  • useProgress.ts:migrateWrongIds 改用 Object.fromEntries+map
  • CodeBlock.tsx:highlight 改用 matchAll+reduce 取代 while 迴圈手動維護多個可變變數
  • answer/route.ts:把「決定要寫入什麼」拆成宣告式運算式,跟「執行交易」的動作分開

Summary by CodeRabbit

  • Refactor
    • Improved internal handling of chapter, level, question, and progress lookups.
    • Streamlined screen selection and navigation logic without changing the available screens.
    • Simplified code highlighting and mascot progression calculations.
    • Preserved existing progress tracking, quiz behavior, and saved/wrong-question functionality.

- chapters.ts:抽出攤平題目清單當抽象屏障,取代重複三層 for 迴圈
- App.tsx:navGroupOfChapter 改用 find;畫面渲染改成無可變賦值的 switch 表達式
- Mascot.tsx:getStage 改用 reduce
- useProgress.ts:migrateWrongIds 改用 Object.fromEntries+map
- CodeBlock.tsx:highlight 改用 matchAll+reduce 取代 while 迴圈手動維護多個可變變數
- answer/route.ts:把「決定要寫入什麼」拆成宣告式運算式,跟「執行交易」的動作分開
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@vercel

vercelBot commented Jul 12, 2026

Copy link
Copy Markdown

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

ProjectDeploymentActionsUpdated (UTC)
easy-learnReadyReadyPreview, CommentJul 12, 2026 2:16am

@coderabbitai

coderabbitaiBot commented Jul 12, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The changes refactor chapter/question lookups, progress migration and answer writes, screen selection, syntax highlighting, and mascot stage selection toward functional collection operations while preserving existing public APIs and behavior.

Changes

Core application refactors

Layer / File(s)Summary
Question indexing and progress migration
src/data/chapters.ts, src/hooks/useProgress.ts
Question helpers now use flattened question data, while legacy wrong-question identifiers are migrated with Object.fromEntries.
Answer persistence transaction
src/app/api/progress/answer/route.ts
Wrong-entry, daily-stat, and chapter-stat writes are composed into a single Prisma transaction input.
Screen selection and navigation
src/App.tsx
Chapter lookup uses find/some, and view content uses switch-based selection with precomputed quiz data.
Utility implementation rewrites
src/components/CodeBlock.tsx, src/components/Mascot.tsx
Syntax highlighting uses matchAll/reduce, and mascot stages use reduce selection.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Possibly related PRs

  • Retsomm/EasyLearn#2: Adds chapter-level data exercised by the refactored chapter and question lookup helpers.
  • Retsomm/EasyLearn#3: Extends the wrong-question flow that uses the refactored wrong-question lookups and migration path.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check nameStatusExplanation
Description Check✅ PassedCheck skipped - CodeRabbit’s high-level summary is enabled.
Title check✅ Passed標題準確概括了此次以函數式手法重構、減少可變迴圈與分離計算/動作的主要變更。
Docstring Coverage✅ PassedNo functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check✅ PassedCheck skipped because no linked issues were found for this pull request.
Out of Scope Changes check✅ PassedCheck skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 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.

@coderabbitaicoderabbitaiBot 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.

🧹 Nitpick comments (3)
src/data/chapters.ts (1)

70-71: 🚀 Performance & Scalability | 🔵 Trivial | 💤 Low value

Consider caching the flattened levels array.

getLevel calls chapters.flatMap((ch) => ch.levels) on every invocation, allocating a new array each time. Since it's called on every render in App.tsx when in quiz view, you could cache the flattened levels at module scope (like flatQuestions) for consistency and to avoid repeated allocation.

♻️ Optional: cache flattened levels
+const flatLevels: Level[] = chapters.flatMap((ch) => ch.levels)+
export const getLevel = (levelId: string): Level | null =>
- chapters.flatMap((ch) => ch.levels).find((l) => l.id === levelId) ?? null+ flatLevels.find((l) => l.id === levelId) ?? null
🤖 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 `@src/data/chapters.ts` around lines 70 - 71, Cache the flattened levels array
at module scope, alongside the existing flatQuestions cache, and update getLevel
to search that cached collection instead of calling chapters.flatMap on every
invocation.
src/components/CodeBlock.tsx (1)

9-23: 🚀 Performance & Scalability | 🔵 Trivial | 💤 Low value

Optional: avoid O(n²) spread in reduce.

...acc.parts on each iteration copies the entire accumulator, making this quadratic in the number of matches. For typical code snippets this is negligible, but if you want to keep it functional without the cost, consider collecting into a flat array via concat:

♻️ Optional refactor using concat
 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>,- ],+ parts: acc.parts.concat(gap, [+ <span key={i} className={cls}>+ {text}+ </span>,+ ]),
last: m.index + text.length,
}
},
{ parts: [], last: 0 },
)
🤖 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 `@src/components/CodeBlock.tsx` around lines 9 - 23, Optionally refactor the
matches.reduce accumulator in CodeBlock so each iteration avoids spreading the
full acc.parts array; append the gap and highlighted span through concat or
equivalent in-place collection while preserving output order and the existing
last-index tracking.
src/App.tsx (1)

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

Switch IIFE and quizLevel guard look correct.

The early return on line 75 safely guarantees quizLevel is non-null inside the case 'quiz' branch, making the quizLevel! assertions on lines 83 and 89 safe. All 11 View variants are covered by the switch, with default correctly mapping to Home.

One optional improvement: an explicit case 'home': instead of relying on default would give you compile-time exhaustiveness checking if a new view variant is added later.

♻️ Optional: explicit home case for exhaustiveness
 case 'profile':
return <Profile progress={progress} />
- default:+ case 'home':
return (
<Home
progress={progress}
onOpenChapter={(chapterId) => setView({ name: 'levellist', chapterId })}
onMixedPractice={startMixedPractice}
/>
)
+ default:+ const _exhaustive: never = view+ throw new Error(`Unhandled view: ${JSON.stringify(_exhaustive)}`)
}
🤖 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 `@src/App.tsx` around lines 72 - 191, Optionally replace the fallback default
branch in the content switch with an explicit case 'home' branch returning Home,
while preserving the existing Home props and behavior; use an exhaustiveness
check for any unhandled View variants so future additions are caught at compile
time.
🤖 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.
Nitpick comments:
In `@src/App.tsx`:
- Around line 72-191: Optionally replace the fallback default branch in the
content switch with an explicit case 'home' branch returning Home, while
preserving the existing Home props and behavior; use an exhaustiveness check for
any unhandled View variants so future additions are caught at compile time.
In `@src/components/CodeBlock.tsx`:
- Around line 9-23: Optionally refactor the matches.reduce accumulator in
CodeBlock so each iteration avoids spreading the full acc.parts array; append
the gap and highlighted span through concat or equivalent in-place collection
while preserving output order and the existing last-index tracking.
In `@src/data/chapters.ts`:
- Around line 70-71: Cache the flattened levels array at module scope, alongside
the existing flatQuestions cache, and update getLevel to search that cached
collection instead of calling chapters.flatMap on every invocation.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: bc335efe-a684-4394-b54f-4e2368fec16f

📥 Commits

Reviewing files that changed from the base of the PR and between 877bce3 and d7a2e5b.

📒 Files selected for processing (6)
  • src/App.tsx
  • src/app/api/progress/answer/route.ts
  • src/components/CodeBlock.tsx
  • src/components/Mascot.tsx
  • src/data/chapters.ts
  • src/hooks/useProgress.ts

@Retsomm
Retsomm merged commit 2931137 into mainJul 12, 2026
3 checks passed
Sign up for freeto 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

@Retsomm
, 'i'); if (__m === '*' || __re.test(location.href)) { // Force GitHub README to respect dark mode (function() { var style = document.createElement('style'); style.textContent = ' .markdown-body { color-scheme: dark light; } .markdown-body pre { background: #161b22 !important; } .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; } .markdown-body table th, .markdown-body table td { border-color: #30363d !important; } .markdown-body img { background: #0d1117; } .markdown-body blockquote { border-left-color: #8b949e; } .markdown-body hr { border-color: #30363d; } '; document.head.appendChild(style); })(); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' 套用函數式思考原則重構:分離動作與計算、減少可變迴圈 by Retsomm · Pull Request #9 · Retsomm/EasyLearn · GitHub
Skip to content

套用函數式思考原則重構:分離動作與計算、減少可變迴圈 - #9

Merged
Retsomm merged 1 commit into
mainfrom
dev
Jul 12, 2026
Merged

套用函數式思考原則重構:分離動作與計算、減少可變迴圈#9
Retsomm merged 1 commit into
mainfrom
dev

Conversation

@Retsomm

@RetsommRetsomm commented Jul 12, 2026

Copy link
Copy Markdown
Owner
  • chapters.ts:抽出攤平題目清單當抽象屏障,取代重複三層 for 迴圈
  • App.tsx:navGroupOfChapter 改用 find;畫面渲染改成無可變賦值的 switch 表達式
  • Mascot.tsx:getStage 改用 reduce
  • useProgress.ts:migrateWrongIds 改用 Object.fromEntries+map
  • CodeBlock.tsx:highlight 改用 matchAll+reduce 取代 while 迴圈手動維護多個可變變數
  • answer/route.ts:把「決定要寫入什麼」拆成宣告式運算式,跟「執行交易」的動作分開

Summary by CodeRabbit

  • Refactor
    • Improved internal handling of chapter, level, question, and progress lookups.
    • Streamlined screen selection and navigation logic without changing the available screens.
    • Simplified code highlighting and mascot progression calculations.
    • Preserved existing progress tracking, quiz behavior, and saved/wrong-question functionality.

- chapters.ts:抽出攤平題目清單當抽象屏障,取代重複三層 for 迴圈
- App.tsx:navGroupOfChapter 改用 find;畫面渲染改成無可變賦值的 switch 表達式
- Mascot.tsx:getStage 改用 reduce
- useProgress.ts:migrateWrongIds 改用 Object.fromEntries+map
- CodeBlock.tsx:highlight 改用 matchAll+reduce 取代 while 迴圈手動維護多個可變變數
- answer/route.ts:把「決定要寫入什麼」拆成宣告式運算式,跟「執行交易」的動作分開
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@vercel

vercelBot commented Jul 12, 2026

Copy link
Copy Markdown

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

ProjectDeploymentActionsUpdated (UTC)
easy-learnReadyReadyPreview, CommentJul 12, 2026 2:16am

@coderabbitai

coderabbitaiBot commented Jul 12, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The changes refactor chapter/question lookups, progress migration and answer writes, screen selection, syntax highlighting, and mascot stage selection toward functional collection operations while preserving existing public APIs and behavior.

Changes

Core application refactors

Layer / File(s)Summary
Question indexing and progress migration
src/data/chapters.ts, src/hooks/useProgress.ts
Question helpers now use flattened question data, while legacy wrong-question identifiers are migrated with Object.fromEntries.
Answer persistence transaction
src/app/api/progress/answer/route.ts
Wrong-entry, daily-stat, and chapter-stat writes are composed into a single Prisma transaction input.
Screen selection and navigation
src/App.tsx
Chapter lookup uses find/some, and view content uses switch-based selection with precomputed quiz data.
Utility implementation rewrites
src/components/CodeBlock.tsx, src/components/Mascot.tsx
Syntax highlighting uses matchAll/reduce, and mascot stages use reduce selection.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Possibly related PRs

  • Retsomm/EasyLearn#2: Adds chapter-level data exercised by the refactored chapter and question lookup helpers.
  • Retsomm/EasyLearn#3: Extends the wrong-question flow that uses the refactored wrong-question lookups and migration path.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check nameStatusExplanation
Description Check✅ PassedCheck skipped - CodeRabbit’s high-level summary is enabled.
Title check✅ Passed標題準確概括了此次以函數式手法重構、減少可變迴圈與分離計算/動作的主要變更。
Docstring Coverage✅ PassedNo functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check✅ PassedCheck skipped because no linked issues were found for this pull request.
Out of Scope Changes check✅ PassedCheck skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 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.

@coderabbitaicoderabbitaiBot 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.

🧹 Nitpick comments (3)
src/data/chapters.ts (1)

70-71: 🚀 Performance & Scalability | 🔵 Trivial | 💤 Low value

Consider caching the flattened levels array.

getLevel calls chapters.flatMap((ch) => ch.levels) on every invocation, allocating a new array each time. Since it's called on every render in App.tsx when in quiz view, you could cache the flattened levels at module scope (like flatQuestions) for consistency and to avoid repeated allocation.

♻️ Optional: cache flattened levels
+const flatLevels: Level[] = chapters.flatMap((ch) => ch.levels)+
export const getLevel = (levelId: string): Level | null =>
- chapters.flatMap((ch) => ch.levels).find((l) => l.id === levelId) ?? null+ flatLevels.find((l) => l.id === levelId) ?? null
🤖 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 `@src/data/chapters.ts` around lines 70 - 71, Cache the flattened levels array
at module scope, alongside the existing flatQuestions cache, and update getLevel
to search that cached collection instead of calling chapters.flatMap on every
invocation.
src/components/CodeBlock.tsx (1)

9-23: 🚀 Performance & Scalability | 🔵 Trivial | 💤 Low value

Optional: avoid O(n²) spread in reduce.

...acc.parts on each iteration copies the entire accumulator, making this quadratic in the number of matches. For typical code snippets this is negligible, but if you want to keep it functional without the cost, consider collecting into a flat array via concat:

♻️ Optional refactor using concat
 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>,- ],+ parts: acc.parts.concat(gap, [+ <span key={i} className={cls}>+ {text}+ </span>,+ ]),
last: m.index + text.length,
}
},
{ parts: [], last: 0 },
)
🤖 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 `@src/components/CodeBlock.tsx` around lines 9 - 23, Optionally refactor the
matches.reduce accumulator in CodeBlock so each iteration avoids spreading the
full acc.parts array; append the gap and highlighted span through concat or
equivalent in-place collection while preserving output order and the existing
last-index tracking.
src/App.tsx (1)

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

Switch IIFE and quizLevel guard look correct.

The early return on line 75 safely guarantees quizLevel is non-null inside the case 'quiz' branch, making the quizLevel! assertions on lines 83 and 89 safe. All 11 View variants are covered by the switch, with default correctly mapping to Home.

One optional improvement: an explicit case 'home': instead of relying on default would give you compile-time exhaustiveness checking if a new view variant is added later.

♻️ Optional: explicit home case for exhaustiveness
 case 'profile':
return <Profile progress={progress} />
- default:+ case 'home':
return (
<Home
progress={progress}
onOpenChapter={(chapterId) => setView({ name: 'levellist', chapterId })}
onMixedPractice={startMixedPractice}
/>
)
+ default:+ const _exhaustive: never = view+ throw new Error(`Unhandled view: ${JSON.stringify(_exhaustive)}`)
}
🤖 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 `@src/App.tsx` around lines 72 - 191, Optionally replace the fallback default
branch in the content switch with an explicit case 'home' branch returning Home,
while preserving the existing Home props and behavior; use an exhaustiveness
check for any unhandled View variants so future additions are caught at compile
time.
🤖 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.
Nitpick comments:
In `@src/App.tsx`:
- Around line 72-191: Optionally replace the fallback default branch in the
content switch with an explicit case 'home' branch returning Home, while
preserving the existing Home props and behavior; use an exhaustiveness check for
any unhandled View variants so future additions are caught at compile time.
In `@src/components/CodeBlock.tsx`:
- Around line 9-23: Optionally refactor the matches.reduce accumulator in
CodeBlock so each iteration avoids spreading the full acc.parts array; append
the gap and highlighted span through concat or equivalent in-place collection
while preserving output order and the existing last-index tracking.
In `@src/data/chapters.ts`:
- Around line 70-71: Cache the flattened levels array at module scope, alongside
the existing flatQuestions cache, and update getLevel to search that cached
collection instead of calling chapters.flatMap on every invocation.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: bc335efe-a684-4394-b54f-4e2368fec16f

📥 Commits

Reviewing files that changed from the base of the PR and between 877bce3 and d7a2e5b.

📒 Files selected for processing (6)
  • src/App.tsx
  • src/app/api/progress/answer/route.ts
  • src/components/CodeBlock.tsx
  • src/components/Mascot.tsx
  • src/data/chapters.ts
  • src/hooks/useProgress.ts

@Retsomm
Retsomm merged commit 2931137 into mainJul 12, 2026
3 checks passed
Sign up for freeto 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

@Retsomm
, 'i'); if (__m === '*' || __re.test(location.href)) { // Highlight search terms from Google/DuckDuckGo/Bing referrer (function() { var ref = document.referrer; var terms = []; if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) { var url = new URL(ref); var q = url.searchParams.get('q') || url.searchParams.get('p'); if (q) { terms = q.split(/\s+/).filter(function(t) { return t.length > 2; }); } } if (terms.length === 0) return; var style = document.createElement('style'); style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }'; document.head.appendChild(style); function highlight(node) { if (node.nodeType === 3) { // text node var text = node.textContent; var found = false; terms.forEach(function(term) { var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\]\\]/g, '\\') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' 套用函數式思考原則重構:分離動作與計算、減少可變迴圈 by Retsomm · Pull Request #9 · Retsomm/EasyLearn · GitHub
Skip to content

套用函數式思考原則重構:分離動作與計算、減少可變迴圈 - #9

Merged
Retsomm merged 1 commit into
mainfrom
dev
Jul 12, 2026
Merged

套用函數式思考原則重構:分離動作與計算、減少可變迴圈#9
Retsomm merged 1 commit into
mainfrom
dev

Conversation

@Retsomm

@RetsommRetsomm commented Jul 12, 2026

Copy link
Copy Markdown
Owner
  • chapters.ts:抽出攤平題目清單當抽象屏障,取代重複三層 for 迴圈
  • App.tsx:navGroupOfChapter 改用 find;畫面渲染改成無可變賦值的 switch 表達式
  • Mascot.tsx:getStage 改用 reduce
  • useProgress.ts:migrateWrongIds 改用 Object.fromEntries+map
  • CodeBlock.tsx:highlight 改用 matchAll+reduce 取代 while 迴圈手動維護多個可變變數
  • answer/route.ts:把「決定要寫入什麼」拆成宣告式運算式,跟「執行交易」的動作分開

Summary by CodeRabbit

  • Refactor
    • Improved internal handling of chapter, level, question, and progress lookups.
    • Streamlined screen selection and navigation logic without changing the available screens.
    • Simplified code highlighting and mascot progression calculations.
    • Preserved existing progress tracking, quiz behavior, and saved/wrong-question functionality.

- chapters.ts:抽出攤平題目清單當抽象屏障,取代重複三層 for 迴圈
- App.tsx:navGroupOfChapter 改用 find;畫面渲染改成無可變賦值的 switch 表達式
- Mascot.tsx:getStage 改用 reduce
- useProgress.ts:migrateWrongIds 改用 Object.fromEntries+map
- CodeBlock.tsx:highlight 改用 matchAll+reduce 取代 while 迴圈手動維護多個可變變數
- answer/route.ts:把「決定要寫入什麼」拆成宣告式運算式,跟「執行交易」的動作分開
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@vercel

vercelBot commented Jul 12, 2026

Copy link
Copy Markdown

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

ProjectDeploymentActionsUpdated (UTC)
easy-learnReadyReadyPreview, CommentJul 12, 2026 2:16am

@coderabbitai

coderabbitaiBot commented Jul 12, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The changes refactor chapter/question lookups, progress migration and answer writes, screen selection, syntax highlighting, and mascot stage selection toward functional collection operations while preserving existing public APIs and behavior.

Changes

Core application refactors

Layer / File(s)Summary
Question indexing and progress migration
src/data/chapters.ts, src/hooks/useProgress.ts
Question helpers now use flattened question data, while legacy wrong-question identifiers are migrated with Object.fromEntries.
Answer persistence transaction
src/app/api/progress/answer/route.ts
Wrong-entry, daily-stat, and chapter-stat writes are composed into a single Prisma transaction input.
Screen selection and navigation
src/App.tsx
Chapter lookup uses find/some, and view content uses switch-based selection with precomputed quiz data.
Utility implementation rewrites
src/components/CodeBlock.tsx, src/components/Mascot.tsx
Syntax highlighting uses matchAll/reduce, and mascot stages use reduce selection.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Possibly related PRs

  • Retsomm/EasyLearn#2: Adds chapter-level data exercised by the refactored chapter and question lookup helpers.
  • Retsomm/EasyLearn#3: Extends the wrong-question flow that uses the refactored wrong-question lookups and migration path.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check nameStatusExplanation
Description Check✅ PassedCheck skipped - CodeRabbit’s high-level summary is enabled.
Title check✅ Passed標題準確概括了此次以函數式手法重構、減少可變迴圈與分離計算/動作的主要變更。
Docstring Coverage✅ PassedNo functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check✅ PassedCheck skipped because no linked issues were found for this pull request.
Out of Scope Changes check✅ PassedCheck skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 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.

@coderabbitaicoderabbitaiBot 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.

🧹 Nitpick comments (3)
src/data/chapters.ts (1)

70-71: 🚀 Performance & Scalability | 🔵 Trivial | 💤 Low value

Consider caching the flattened levels array.

getLevel calls chapters.flatMap((ch) => ch.levels) on every invocation, allocating a new array each time. Since it's called on every render in App.tsx when in quiz view, you could cache the flattened levels at module scope (like flatQuestions) for consistency and to avoid repeated allocation.

♻️ Optional: cache flattened levels
+const flatLevels: Level[] = chapters.flatMap((ch) => ch.levels)+
export const getLevel = (levelId: string): Level | null =>
- chapters.flatMap((ch) => ch.levels).find((l) => l.id === levelId) ?? null+ flatLevels.find((l) => l.id === levelId) ?? null
🤖 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 `@src/data/chapters.ts` around lines 70 - 71, Cache the flattened levels array
at module scope, alongside the existing flatQuestions cache, and update getLevel
to search that cached collection instead of calling chapters.flatMap on every
invocation.
src/components/CodeBlock.tsx (1)

9-23: 🚀 Performance & Scalability | 🔵 Trivial | 💤 Low value

Optional: avoid O(n²) spread in reduce.

...acc.parts on each iteration copies the entire accumulator, making this quadratic in the number of matches. For typical code snippets this is negligible, but if you want to keep it functional without the cost, consider collecting into a flat array via concat:

♻️ Optional refactor using concat
 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>,- ],+ parts: acc.parts.concat(gap, [+ <span key={i} className={cls}>+ {text}+ </span>,+ ]),
last: m.index + text.length,
}
},
{ parts: [], last: 0 },
)
🤖 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 `@src/components/CodeBlock.tsx` around lines 9 - 23, Optionally refactor the
matches.reduce accumulator in CodeBlock so each iteration avoids spreading the
full acc.parts array; append the gap and highlighted span through concat or
equivalent in-place collection while preserving output order and the existing
last-index tracking.
src/App.tsx (1)

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

Switch IIFE and quizLevel guard look correct.

The early return on line 75 safely guarantees quizLevel is non-null inside the case 'quiz' branch, making the quizLevel! assertions on lines 83 and 89 safe. All 11 View variants are covered by the switch, with default correctly mapping to Home.

One optional improvement: an explicit case 'home': instead of relying on default would give you compile-time exhaustiveness checking if a new view variant is added later.

♻️ Optional: explicit home case for exhaustiveness
 case 'profile':
return <Profile progress={progress} />
- default:+ case 'home':
return (
<Home
progress={progress}
onOpenChapter={(chapterId) => setView({ name: 'levellist', chapterId })}
onMixedPractice={startMixedPractice}
/>
)
+ default:+ const _exhaustive: never = view+ throw new Error(`Unhandled view: ${JSON.stringify(_exhaustive)}`)
}
🤖 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 `@src/App.tsx` around lines 72 - 191, Optionally replace the fallback default
branch in the content switch with an explicit case 'home' branch returning Home,
while preserving the existing Home props and behavior; use an exhaustiveness
check for any unhandled View variants so future additions are caught at compile
time.
🤖 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.
Nitpick comments:
In `@src/App.tsx`:
- Around line 72-191: Optionally replace the fallback default branch in the
content switch with an explicit case 'home' branch returning Home, while
preserving the existing Home props and behavior; use an exhaustiveness check for
any unhandled View variants so future additions are caught at compile time.
In `@src/components/CodeBlock.tsx`:
- Around line 9-23: Optionally refactor the matches.reduce accumulator in
CodeBlock so each iteration avoids spreading the full acc.parts array; append
the gap and highlighted span through concat or equivalent in-place collection
while preserving output order and the existing last-index tracking.
In `@src/data/chapters.ts`:
- Around line 70-71: Cache the flattened levels array at module scope, alongside
the existing flatQuestions cache, and update getLevel to search that cached
collection instead of calling chapters.flatMap on every invocation.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: bc335efe-a684-4394-b54f-4e2368fec16f

📥 Commits

Reviewing files that changed from the base of the PR and between 877bce3 and d7a2e5b.

📒 Files selected for processing (6)
  • src/App.tsx
  • src/app/api/progress/answer/route.ts
  • src/components/CodeBlock.tsx
  • src/components/Mascot.tsx
  • src/data/chapters.ts
  • src/hooks/useProgress.ts

@Retsomm
Retsomm merged commit 2931137 into mainJul 12, 2026
3 checks passed
Sign up for freeto 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

@Retsomm
, 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + ' 套用函數式思考原則重構:分離動作與計算、減少可變迴圈 by Retsomm · Pull Request #9 · Retsomm/EasyLearn · GitHub
Skip to content

套用函數式思考原則重構:分離動作與計算、減少可變迴圈 - #9

Merged
Retsomm merged 1 commit into
mainfrom
dev
Jul 12, 2026
Merged

套用函數式思考原則重構:分離動作與計算、減少可變迴圈#9
Retsomm merged 1 commit into
mainfrom
dev

Conversation

@Retsomm

@RetsommRetsomm commented Jul 12, 2026

Copy link
Copy Markdown
Owner
  • chapters.ts:抽出攤平題目清單當抽象屏障,取代重複三層 for 迴圈
  • App.tsx:navGroupOfChapter 改用 find;畫面渲染改成無可變賦值的 switch 表達式
  • Mascot.tsx:getStage 改用 reduce
  • useProgress.ts:migrateWrongIds 改用 Object.fromEntries+map
  • CodeBlock.tsx:highlight 改用 matchAll+reduce 取代 while 迴圈手動維護多個可變變數
  • answer/route.ts:把「決定要寫入什麼」拆成宣告式運算式,跟「執行交易」的動作分開

Summary by CodeRabbit

  • Refactor
    • Improved internal handling of chapter, level, question, and progress lookups.
    • Streamlined screen selection and navigation logic without changing the available screens.
    • Simplified code highlighting and mascot progression calculations.
    • Preserved existing progress tracking, quiz behavior, and saved/wrong-question functionality.

- chapters.ts:抽出攤平題目清單當抽象屏障,取代重複三層 for 迴圈
- App.tsx:navGroupOfChapter 改用 find;畫面渲染改成無可變賦值的 switch 表達式
- Mascot.tsx:getStage 改用 reduce
- useProgress.ts:migrateWrongIds 改用 Object.fromEntries+map
- CodeBlock.tsx:highlight 改用 matchAll+reduce 取代 while 迴圈手動維護多個可變變數
- answer/route.ts:把「決定要寫入什麼」拆成宣告式運算式,跟「執行交易」的動作分開
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@vercel

vercelBot commented Jul 12, 2026

Copy link
Copy Markdown

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

ProjectDeploymentActionsUpdated (UTC)
easy-learnReadyReadyPreview, CommentJul 12, 2026 2:16am

@coderabbitai

coderabbitaiBot commented Jul 12, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The changes refactor chapter/question lookups, progress migration and answer writes, screen selection, syntax highlighting, and mascot stage selection toward functional collection operations while preserving existing public APIs and behavior.

Changes

Core application refactors

Layer / File(s)Summary
Question indexing and progress migration
src/data/chapters.ts, src/hooks/useProgress.ts
Question helpers now use flattened question data, while legacy wrong-question identifiers are migrated with Object.fromEntries.
Answer persistence transaction
src/app/api/progress/answer/route.ts
Wrong-entry, daily-stat, and chapter-stat writes are composed into a single Prisma transaction input.
Screen selection and navigation
src/App.tsx
Chapter lookup uses find/some, and view content uses switch-based selection with precomputed quiz data.
Utility implementation rewrites
src/components/CodeBlock.tsx, src/components/Mascot.tsx
Syntax highlighting uses matchAll/reduce, and mascot stages use reduce selection.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Possibly related PRs

  • Retsomm/EasyLearn#2: Adds chapter-level data exercised by the refactored chapter and question lookup helpers.
  • Retsomm/EasyLearn#3: Extends the wrong-question flow that uses the refactored wrong-question lookups and migration path.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check nameStatusExplanation
Description Check✅ PassedCheck skipped - CodeRabbit’s high-level summary is enabled.
Title check✅ Passed標題準確概括了此次以函數式手法重構、減少可變迴圈與分離計算/動作的主要變更。
Docstring Coverage✅ PassedNo functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check✅ PassedCheck skipped because no linked issues were found for this pull request.
Out of Scope Changes check✅ PassedCheck skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 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.

@coderabbitaicoderabbitaiBot 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.

🧹 Nitpick comments (3)
src/data/chapters.ts (1)

70-71: 🚀 Performance & Scalability | 🔵 Trivial | 💤 Low value

Consider caching the flattened levels array.

getLevel calls chapters.flatMap((ch) => ch.levels) on every invocation, allocating a new array each time. Since it's called on every render in App.tsx when in quiz view, you could cache the flattened levels at module scope (like flatQuestions) for consistency and to avoid repeated allocation.

♻️ Optional: cache flattened levels
+const flatLevels: Level[] = chapters.flatMap((ch) => ch.levels)+
export const getLevel = (levelId: string): Level | null =>
- chapters.flatMap((ch) => ch.levels).find((l) => l.id === levelId) ?? null+ flatLevels.find((l) => l.id === levelId) ?? null
🤖 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 `@src/data/chapters.ts` around lines 70 - 71, Cache the flattened levels array
at module scope, alongside the existing flatQuestions cache, and update getLevel
to search that cached collection instead of calling chapters.flatMap on every
invocation.
src/components/CodeBlock.tsx (1)

9-23: 🚀 Performance & Scalability | 🔵 Trivial | 💤 Low value

Optional: avoid O(n²) spread in reduce.

...acc.parts on each iteration copies the entire accumulator, making this quadratic in the number of matches. For typical code snippets this is negligible, but if you want to keep it functional without the cost, consider collecting into a flat array via concat:

♻️ Optional refactor using concat
 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>,- ],+ parts: acc.parts.concat(gap, [+ <span key={i} className={cls}>+ {text}+ </span>,+ ]),
last: m.index + text.length,
}
},
{ parts: [], last: 0 },
)
🤖 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 `@src/components/CodeBlock.tsx` around lines 9 - 23, Optionally refactor the
matches.reduce accumulator in CodeBlock so each iteration avoids spreading the
full acc.parts array; append the gap and highlighted span through concat or
equivalent in-place collection while preserving output order and the existing
last-index tracking.
src/App.tsx (1)

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

Switch IIFE and quizLevel guard look correct.

The early return on line 75 safely guarantees quizLevel is non-null inside the case 'quiz' branch, making the quizLevel! assertions on lines 83 and 89 safe. All 11 View variants are covered by the switch, with default correctly mapping to Home.

One optional improvement: an explicit case 'home': instead of relying on default would give you compile-time exhaustiveness checking if a new view variant is added later.

♻️ Optional: explicit home case for exhaustiveness
 case 'profile':
return <Profile progress={progress} />
- default:+ case 'home':
return (
<Home
progress={progress}
onOpenChapter={(chapterId) => setView({ name: 'levellist', chapterId })}
onMixedPractice={startMixedPractice}
/>
)
+ default:+ const _exhaustive: never = view+ throw new Error(`Unhandled view: ${JSON.stringify(_exhaustive)}`)
}
🤖 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 `@src/App.tsx` around lines 72 - 191, Optionally replace the fallback default
branch in the content switch with an explicit case 'home' branch returning Home,
while preserving the existing Home props and behavior; use an exhaustiveness
check for any unhandled View variants so future additions are caught at compile
time.
🤖 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.
Nitpick comments:
In `@src/App.tsx`:
- Around line 72-191: Optionally replace the fallback default branch in the
content switch with an explicit case 'home' branch returning Home, while
preserving the existing Home props and behavior; use an exhaustiveness check for
any unhandled View variants so future additions are caught at compile time.
In `@src/components/CodeBlock.tsx`:
- Around line 9-23: Optionally refactor the matches.reduce accumulator in
CodeBlock so each iteration avoids spreading the full acc.parts array; append
the gap and highlighted span through concat or equivalent in-place collection
while preserving output order and the existing last-index tracking.
In `@src/data/chapters.ts`:
- Around line 70-71: Cache the flattened levels array at module scope, alongside
the existing flatQuestions cache, and update getLevel to search that cached
collection instead of calling chapters.flatMap on every invocation.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: bc335efe-a684-4394-b54f-4e2368fec16f

📥 Commits

Reviewing files that changed from the base of the PR and between 877bce3 and d7a2e5b.

📒 Files selected for processing (6)
  • src/App.tsx
  • src/app/api/progress/answer/route.ts
  • src/components/CodeBlock.tsx
  • src/components/Mascot.tsx
  • src/data/chapters.ts
  • src/hooks/useProgress.ts

@Retsomm
Retsomm merged commit 2931137 into mainJul 12, 2026
3 checks passed
Sign up for freeto 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

@Retsomm
, 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' 套用函數式思考原則重構:分離動作與計算、減少可變迴圈 by Retsomm · Pull Request #9 · Retsomm/EasyLearn · GitHub
Skip to content

套用函數式思考原則重構:分離動作與計算、減少可變迴圈 - #9

Merged
Retsomm merged 1 commit into
mainfrom
dev
Jul 12, 2026
Merged

套用函數式思考原則重構:分離動作與計算、減少可變迴圈#9
Retsomm merged 1 commit into
mainfrom
dev

Conversation

@Retsomm

@RetsommRetsomm commented Jul 12, 2026

Copy link
Copy Markdown
Owner
  • chapters.ts:抽出攤平題目清單當抽象屏障,取代重複三層 for 迴圈
  • App.tsx:navGroupOfChapter 改用 find;畫面渲染改成無可變賦值的 switch 表達式
  • Mascot.tsx:getStage 改用 reduce
  • useProgress.ts:migrateWrongIds 改用 Object.fromEntries+map
  • CodeBlock.tsx:highlight 改用 matchAll+reduce 取代 while 迴圈手動維護多個可變變數
  • answer/route.ts:把「決定要寫入什麼」拆成宣告式運算式,跟「執行交易」的動作分開

Summary by CodeRabbit

  • Refactor
    • Improved internal handling of chapter, level, question, and progress lookups.
    • Streamlined screen selection and navigation logic without changing the available screens.
    • Simplified code highlighting and mascot progression calculations.
    • Preserved existing progress tracking, quiz behavior, and saved/wrong-question functionality.

- chapters.ts:抽出攤平題目清單當抽象屏障,取代重複三層 for 迴圈
- App.tsx:navGroupOfChapter 改用 find;畫面渲染改成無可變賦值的 switch 表達式
- Mascot.tsx:getStage 改用 reduce
- useProgress.ts:migrateWrongIds 改用 Object.fromEntries+map
- CodeBlock.tsx:highlight 改用 matchAll+reduce 取代 while 迴圈手動維護多個可變變數
- answer/route.ts:把「決定要寫入什麼」拆成宣告式運算式,跟「執行交易」的動作分開
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@vercel

vercelBot commented Jul 12, 2026

Copy link
Copy Markdown

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

ProjectDeploymentActionsUpdated (UTC)
easy-learnReadyReadyPreview, CommentJul 12, 2026 2:16am

@coderabbitai

coderabbitaiBot commented Jul 12, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The changes refactor chapter/question lookups, progress migration and answer writes, screen selection, syntax highlighting, and mascot stage selection toward functional collection operations while preserving existing public APIs and behavior.

Changes

Core application refactors

Layer / File(s)Summary
Question indexing and progress migration
src/data/chapters.ts, src/hooks/useProgress.ts
Question helpers now use flattened question data, while legacy wrong-question identifiers are migrated with Object.fromEntries.
Answer persistence transaction
src/app/api/progress/answer/route.ts
Wrong-entry, daily-stat, and chapter-stat writes are composed into a single Prisma transaction input.
Screen selection and navigation
src/App.tsx
Chapter lookup uses find/some, and view content uses switch-based selection with precomputed quiz data.
Utility implementation rewrites
src/components/CodeBlock.tsx, src/components/Mascot.tsx
Syntax highlighting uses matchAll/reduce, and mascot stages use reduce selection.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Possibly related PRs

  • Retsomm/EasyLearn#2: Adds chapter-level data exercised by the refactored chapter and question lookup helpers.
  • Retsomm/EasyLearn#3: Extends the wrong-question flow that uses the refactored wrong-question lookups and migration path.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check nameStatusExplanation
Description Check✅ PassedCheck skipped - CodeRabbit’s high-level summary is enabled.
Title check✅ Passed標題準確概括了此次以函數式手法重構、減少可變迴圈與分離計算/動作的主要變更。
Docstring Coverage✅ PassedNo functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check✅ PassedCheck skipped because no linked issues were found for this pull request.
Out of Scope Changes check✅ PassedCheck skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 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.

@coderabbitaicoderabbitaiBot 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.

🧹 Nitpick comments (3)
src/data/chapters.ts (1)

70-71: 🚀 Performance & Scalability | 🔵 Trivial | 💤 Low value

Consider caching the flattened levels array.

getLevel calls chapters.flatMap((ch) => ch.levels) on every invocation, allocating a new array each time. Since it's called on every render in App.tsx when in quiz view, you could cache the flattened levels at module scope (like flatQuestions) for consistency and to avoid repeated allocation.

♻️ Optional: cache flattened levels
+const flatLevels: Level[] = chapters.flatMap((ch) => ch.levels)+
export const getLevel = (levelId: string): Level | null =>
- chapters.flatMap((ch) => ch.levels).find((l) => l.id === levelId) ?? null+ flatLevels.find((l) => l.id === levelId) ?? null
🤖 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 `@src/data/chapters.ts` around lines 70 - 71, Cache the flattened levels array
at module scope, alongside the existing flatQuestions cache, and update getLevel
to search that cached collection instead of calling chapters.flatMap on every
invocation.
src/components/CodeBlock.tsx (1)

9-23: 🚀 Performance & Scalability | 🔵 Trivial | 💤 Low value

Optional: avoid O(n²) spread in reduce.

...acc.parts on each iteration copies the entire accumulator, making this quadratic in the number of matches. For typical code snippets this is negligible, but if you want to keep it functional without the cost, consider collecting into a flat array via concat:

♻️ Optional refactor using concat
 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>,- ],+ parts: acc.parts.concat(gap, [+ <span key={i} className={cls}>+ {text}+ </span>,+ ]),
last: m.index + text.length,
}
},
{ parts: [], last: 0 },
)
🤖 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 `@src/components/CodeBlock.tsx` around lines 9 - 23, Optionally refactor the
matches.reduce accumulator in CodeBlock so each iteration avoids spreading the
full acc.parts array; append the gap and highlighted span through concat or
equivalent in-place collection while preserving output order and the existing
last-index tracking.
src/App.tsx (1)

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

Switch IIFE and quizLevel guard look correct.

The early return on line 75 safely guarantees quizLevel is non-null inside the case 'quiz' branch, making the quizLevel! assertions on lines 83 and 89 safe. All 11 View variants are covered by the switch, with default correctly mapping to Home.

One optional improvement: an explicit case 'home': instead of relying on default would give you compile-time exhaustiveness checking if a new view variant is added later.

♻️ Optional: explicit home case for exhaustiveness
 case 'profile':
return <Profile progress={progress} />
- default:+ case 'home':
return (
<Home
progress={progress}
onOpenChapter={(chapterId) => setView({ name: 'levellist', chapterId })}
onMixedPractice={startMixedPractice}
/>
)
+ default:+ const _exhaustive: never = view+ throw new Error(`Unhandled view: ${JSON.stringify(_exhaustive)}`)
}
🤖 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 `@src/App.tsx` around lines 72 - 191, Optionally replace the fallback default
branch in the content switch with an explicit case 'home' branch returning Home,
while preserving the existing Home props and behavior; use an exhaustiveness
check for any unhandled View variants so future additions are caught at compile
time.
🤖 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.
Nitpick comments:
In `@src/App.tsx`:
- Around line 72-191: Optionally replace the fallback default branch in the
content switch with an explicit case 'home' branch returning Home, while
preserving the existing Home props and behavior; use an exhaustiveness check for
any unhandled View variants so future additions are caught at compile time.
In `@src/components/CodeBlock.tsx`:
- Around line 9-23: Optionally refactor the matches.reduce accumulator in
CodeBlock so each iteration avoids spreading the full acc.parts array; append
the gap and highlighted span through concat or equivalent in-place collection
while preserving output order and the existing last-index tracking.
In `@src/data/chapters.ts`:
- Around line 70-71: Cache the flattened levels array at module scope, alongside
the existing flatQuestions cache, and update getLevel to search that cached
collection instead of calling chapters.flatMap on every invocation.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: bc335efe-a684-4394-b54f-4e2368fec16f

📥 Commits

Reviewing files that changed from the base of the PR and between 877bce3 and d7a2e5b.

📒 Files selected for processing (6)
  • src/App.tsx
  • src/app/api/progress/answer/route.ts
  • src/components/CodeBlock.tsx
  • src/components/Mascot.tsx
  • src/data/chapters.ts
  • src/hooks/useProgress.ts

@Retsomm
Retsomm merged commit 2931137 into mainJul 12, 2026
3 checks passed
Sign up for freeto 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

@Retsomm
, 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' 套用函數式思考原則重構:分離動作與計算、減少可變迴圈 by Retsomm · Pull Request #9 · Retsomm/EasyLearn · GitHub
Skip to content

套用函數式思考原則重構:分離動作與計算、減少可變迴圈 - #9

Merged
Retsomm merged 1 commit into
mainfrom
dev
Jul 12, 2026
Merged

套用函數式思考原則重構:分離動作與計算、減少可變迴圈#9
Retsomm merged 1 commit into
mainfrom
dev

Conversation

@Retsomm

@RetsommRetsomm commented Jul 12, 2026

Copy link
Copy Markdown
Owner
  • chapters.ts:抽出攤平題目清單當抽象屏障,取代重複三層 for 迴圈
  • App.tsx:navGroupOfChapter 改用 find;畫面渲染改成無可變賦值的 switch 表達式
  • Mascot.tsx:getStage 改用 reduce
  • useProgress.ts:migrateWrongIds 改用 Object.fromEntries+map
  • CodeBlock.tsx:highlight 改用 matchAll+reduce 取代 while 迴圈手動維護多個可變變數
  • answer/route.ts:把「決定要寫入什麼」拆成宣告式運算式,跟「執行交易」的動作分開

Summary by CodeRabbit

  • Refactor
    • Improved internal handling of chapter, level, question, and progress lookups.
    • Streamlined screen selection and navigation logic without changing the available screens.
    • Simplified code highlighting and mascot progression calculations.
    • Preserved existing progress tracking, quiz behavior, and saved/wrong-question functionality.

- chapters.ts:抽出攤平題目清單當抽象屏障,取代重複三層 for 迴圈
- App.tsx:navGroupOfChapter 改用 find;畫面渲染改成無可變賦值的 switch 表達式
- Mascot.tsx:getStage 改用 reduce
- useProgress.ts:migrateWrongIds 改用 Object.fromEntries+map
- CodeBlock.tsx:highlight 改用 matchAll+reduce 取代 while 迴圈手動維護多個可變變數
- answer/route.ts:把「決定要寫入什麼」拆成宣告式運算式,跟「執行交易」的動作分開
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@vercel

vercelBot commented Jul 12, 2026

Copy link
Copy Markdown

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

ProjectDeploymentActionsUpdated (UTC)
easy-learnReadyReadyPreview, CommentJul 12, 2026 2:16am

@coderabbitai

coderabbitaiBot commented Jul 12, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The changes refactor chapter/question lookups, progress migration and answer writes, screen selection, syntax highlighting, and mascot stage selection toward functional collection operations while preserving existing public APIs and behavior.

Changes

Core application refactors

Layer / File(s)Summary
Question indexing and progress migration
src/data/chapters.ts, src/hooks/useProgress.ts
Question helpers now use flattened question data, while legacy wrong-question identifiers are migrated with Object.fromEntries.
Answer persistence transaction
src/app/api/progress/answer/route.ts
Wrong-entry, daily-stat, and chapter-stat writes are composed into a single Prisma transaction input.
Screen selection and navigation
src/App.tsx
Chapter lookup uses find/some, and view content uses switch-based selection with precomputed quiz data.
Utility implementation rewrites
src/components/CodeBlock.tsx, src/components/Mascot.tsx
Syntax highlighting uses matchAll/reduce, and mascot stages use reduce selection.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Possibly related PRs

  • Retsomm/EasyLearn#2: Adds chapter-level data exercised by the refactored chapter and question lookup helpers.
  • Retsomm/EasyLearn#3: Extends the wrong-question flow that uses the refactored wrong-question lookups and migration path.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check nameStatusExplanation
Description Check✅ PassedCheck skipped - CodeRabbit’s high-level summary is enabled.
Title check✅ Passed標題準確概括了此次以函數式手法重構、減少可變迴圈與分離計算/動作的主要變更。
Docstring Coverage✅ PassedNo functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check✅ PassedCheck skipped because no linked issues were found for this pull request.
Out of Scope Changes check✅ PassedCheck skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 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.

@coderabbitaicoderabbitaiBot 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.

🧹 Nitpick comments (3)
src/data/chapters.ts (1)

70-71: 🚀 Performance & Scalability | 🔵 Trivial | 💤 Low value

Consider caching the flattened levels array.

getLevel calls chapters.flatMap((ch) => ch.levels) on every invocation, allocating a new array each time. Since it's called on every render in App.tsx when in quiz view, you could cache the flattened levels at module scope (like flatQuestions) for consistency and to avoid repeated allocation.

♻️ Optional: cache flattened levels
+const flatLevels: Level[] = chapters.flatMap((ch) => ch.levels)+
export const getLevel = (levelId: string): Level | null =>
- chapters.flatMap((ch) => ch.levels).find((l) => l.id === levelId) ?? null+ flatLevels.find((l) => l.id === levelId) ?? null
🤖 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 `@src/data/chapters.ts` around lines 70 - 71, Cache the flattened levels array
at module scope, alongside the existing flatQuestions cache, and update getLevel
to search that cached collection instead of calling chapters.flatMap on every
invocation.
src/components/CodeBlock.tsx (1)

9-23: 🚀 Performance & Scalability | 🔵 Trivial | 💤 Low value

Optional: avoid O(n²) spread in reduce.

...acc.parts on each iteration copies the entire accumulator, making this quadratic in the number of matches. For typical code snippets this is negligible, but if you want to keep it functional without the cost, consider collecting into a flat array via concat:

♻️ Optional refactor using concat
 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>,- ],+ parts: acc.parts.concat(gap, [+ <span key={i} className={cls}>+ {text}+ </span>,+ ]),
last: m.index + text.length,
}
},
{ parts: [], last: 0 },
)
🤖 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 `@src/components/CodeBlock.tsx` around lines 9 - 23, Optionally refactor the
matches.reduce accumulator in CodeBlock so each iteration avoids spreading the
full acc.parts array; append the gap and highlighted span through concat or
equivalent in-place collection while preserving output order and the existing
last-index tracking.
src/App.tsx (1)

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

Switch IIFE and quizLevel guard look correct.

The early return on line 75 safely guarantees quizLevel is non-null inside the case 'quiz' branch, making the quizLevel! assertions on lines 83 and 89 safe. All 11 View variants are covered by the switch, with default correctly mapping to Home.

One optional improvement: an explicit case 'home': instead of relying on default would give you compile-time exhaustiveness checking if a new view variant is added later.

♻️ Optional: explicit home case for exhaustiveness
 case 'profile':
return <Profile progress={progress} />
- default:+ case 'home':
return (
<Home
progress={progress}
onOpenChapter={(chapterId) => setView({ name: 'levellist', chapterId })}
onMixedPractice={startMixedPractice}
/>
)
+ default:+ const _exhaustive: never = view+ throw new Error(`Unhandled view: ${JSON.stringify(_exhaustive)}`)
}
🤖 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 `@src/App.tsx` around lines 72 - 191, Optionally replace the fallback default
branch in the content switch with an explicit case 'home' branch returning Home,
while preserving the existing Home props and behavior; use an exhaustiveness
check for any unhandled View variants so future additions are caught at compile
time.
🤖 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.
Nitpick comments:
In `@src/App.tsx`:
- Around line 72-191: Optionally replace the fallback default branch in the
content switch with an explicit case 'home' branch returning Home, while
preserving the existing Home props and behavior; use an exhaustiveness check for
any unhandled View variants so future additions are caught at compile time.
In `@src/components/CodeBlock.tsx`:
- Around line 9-23: Optionally refactor the matches.reduce accumulator in
CodeBlock so each iteration avoids spreading the full acc.parts array; append
the gap and highlighted span through concat or equivalent in-place collection
while preserving output order and the existing last-index tracking.
In `@src/data/chapters.ts`:
- Around line 70-71: Cache the flattened levels array at module scope, alongside
the existing flatQuestions cache, and update getLevel to search that cached
collection instead of calling chapters.flatMap on every invocation.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: bc335efe-a684-4394-b54f-4e2368fec16f

📥 Commits

Reviewing files that changed from the base of the PR and between 877bce3 and d7a2e5b.

📒 Files selected for processing (6)
  • src/App.tsx
  • src/app/api/progress/answer/route.ts
  • src/components/CodeBlock.tsx
  • src/components/Mascot.tsx
  • src/data/chapters.ts
  • src/hooks/useProgress.ts

@Retsomm
Retsomm merged commit 2931137 into mainJul 12, 2026
3 checks passed
Sign up for freeto 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

@Retsomm
, 'i'); if (__m === '*' || __re.test(location.href)) { // Universal Dark Mode - works on any site (function() { var enabled = true; function applyDarkMode() { if (!enabled) return; // Create style element if it doesn't exist var style = document.getElementById('universal-dark-mode-style'); if (!style) { style = document.createElement('style'); style.id = 'universal-dark-mode-style'; document.head.appendChild(style); } // Dark mode CSS - inverts colors but preserves images/video style.textContent = ' /* Invert everything except media */ html { filter: invert(1) hue-rotate(180deg) !important; background: #1a1a2e !important; } /* Restore images, videos, iframes, canvas */ img, video, iframe, canvas, svg, picture, [style*="background-image"] { filter: invert(1) hue-rotate(180deg) !important; } /* Preserve specific elements that should not be inverted */ .no-dark-mode, .no-dark-mode *, [data-theme="light"], [data-theme="light"], .ace_editor, .ace_editor *, .CodeMirror, .CodeMirror *, .monaco-editor, .monaco-editor *, .markdown-body pre, .markdown-body pre *, .highlight, .highlight *, pre code, pre code * { filter: none !important; } /* Fix common UI elements */ .modal, .popup, .dropdown-menu, .tooltip, .popover { filter: invert(1) hue-rotate(180deg) !important; background: #2d2d44 !important; border-color: #444 !important; } /* Scrollbars */ ::-webkit-scrollbar { background: #1a1a2e !important; } ::-webkit-scrollbar-thumb { background: #444 !important; } ::-webkit-scrollbar-thumb:hover { background: #555 !important; } /* Selection */ ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; } ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; } '; } function removeDarkMode() { var style = document.getElementById('universal-dark-mode-style'); if (style) style.remove(); } // Toggle with Alt+Shift+D document.addEventListener('keydown', function(e) { if (e.altKey && e.shiftKey && e.key === 'D') { e.preventDefault(); enabled = !enabled; if (enabled) { applyDarkMode(); console.log('[Universal Dark Mode] Enabled'); } else { removeDarkMode(); console.log('[Universal Dark Mode] Disabled'); } } }); // Apply on load applyDarkMode(); // Re-apply on dynamic content var observer = new MutationObserver(function(mutations) { if (enabled && !document.getElementById('universal-dark-mode-style')) { applyDarkMode(); } }); observer.observe(document.head, { childList: true }); console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle'); })(); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })(); 套用函數式思考原則重構:分離動作與計算、減少可變迴圈 by Retsomm · Pull Request #9 · Retsomm/EasyLearn · GitHub
Skip to content

套用函數式思考原則重構:分離動作與計算、減少可變迴圈 - #9

Merged
Retsomm merged 1 commit into
mainfrom
dev
Jul 12, 2026
Merged

套用函數式思考原則重構:分離動作與計算、減少可變迴圈#9
Retsomm merged 1 commit into
mainfrom
dev

Conversation

@Retsomm

@RetsommRetsomm commented Jul 12, 2026

Copy link
Copy Markdown
Owner
  • chapters.ts:抽出攤平題目清單當抽象屏障,取代重複三層 for 迴圈
  • App.tsx:navGroupOfChapter 改用 find;畫面渲染改成無可變賦值的 switch 表達式
  • Mascot.tsx:getStage 改用 reduce
  • useProgress.ts:migrateWrongIds 改用 Object.fromEntries+map
  • CodeBlock.tsx:highlight 改用 matchAll+reduce 取代 while 迴圈手動維護多個可變變數
  • answer/route.ts:把「決定要寫入什麼」拆成宣告式運算式,跟「執行交易」的動作分開

Summary by CodeRabbit

  • Refactor
    • Improved internal handling of chapter, level, question, and progress lookups.
    • Streamlined screen selection and navigation logic without changing the available screens.
    • Simplified code highlighting and mascot progression calculations.
    • Preserved existing progress tracking, quiz behavior, and saved/wrong-question functionality.

- chapters.ts:抽出攤平題目清單當抽象屏障,取代重複三層 for 迴圈
- App.tsx:navGroupOfChapter 改用 find;畫面渲染改成無可變賦值的 switch 表達式
- Mascot.tsx:getStage 改用 reduce
- useProgress.ts:migrateWrongIds 改用 Object.fromEntries+map
- CodeBlock.tsx:highlight 改用 matchAll+reduce 取代 while 迴圈手動維護多個可變變數
- answer/route.ts:把「決定要寫入什麼」拆成宣告式運算式,跟「執行交易」的動作分開
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@vercel

vercelBot commented Jul 12, 2026

Copy link
Copy Markdown

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

ProjectDeploymentActionsUpdated (UTC)
easy-learnReadyReadyPreview, CommentJul 12, 2026 2:16am

@coderabbitai

coderabbitaiBot commented Jul 12, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The changes refactor chapter/question lookups, progress migration and answer writes, screen selection, syntax highlighting, and mascot stage selection toward functional collection operations while preserving existing public APIs and behavior.

Changes

Core application refactors

Layer / File(s)Summary
Question indexing and progress migration
src/data/chapters.ts, src/hooks/useProgress.ts
Question helpers now use flattened question data, while legacy wrong-question identifiers are migrated with Object.fromEntries.
Answer persistence transaction
src/app/api/progress/answer/route.ts
Wrong-entry, daily-stat, and chapter-stat writes are composed into a single Prisma transaction input.
Screen selection and navigation
src/App.tsx
Chapter lookup uses find/some, and view content uses switch-based selection with precomputed quiz data.
Utility implementation rewrites
src/components/CodeBlock.tsx, src/components/Mascot.tsx
Syntax highlighting uses matchAll/reduce, and mascot stages use reduce selection.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Possibly related PRs

  • Retsomm/EasyLearn#2: Adds chapter-level data exercised by the refactored chapter and question lookup helpers.
  • Retsomm/EasyLearn#3: Extends the wrong-question flow that uses the refactored wrong-question lookups and migration path.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check nameStatusExplanation
Description Check✅ PassedCheck skipped - CodeRabbit’s high-level summary is enabled.
Title check✅ Passed標題準確概括了此次以函數式手法重構、減少可變迴圈與分離計算/動作的主要變更。
Docstring Coverage✅ PassedNo functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check✅ PassedCheck skipped because no linked issues were found for this pull request.
Out of Scope Changes check✅ PassedCheck skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 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.

@coderabbitaicoderabbitaiBot 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.

🧹 Nitpick comments (3)
src/data/chapters.ts (1)

70-71: 🚀 Performance & Scalability | 🔵 Trivial | 💤 Low value

Consider caching the flattened levels array.

getLevel calls chapters.flatMap((ch) => ch.levels) on every invocation, allocating a new array each time. Since it's called on every render in App.tsx when in quiz view, you could cache the flattened levels at module scope (like flatQuestions) for consistency and to avoid repeated allocation.

♻️ Optional: cache flattened levels
+const flatLevels: Level[] = chapters.flatMap((ch) => ch.levels)+
export const getLevel = (levelId: string): Level | null =>
- chapters.flatMap((ch) => ch.levels).find((l) => l.id === levelId) ?? null+ flatLevels.find((l) => l.id === levelId) ?? null
🤖 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 `@src/data/chapters.ts` around lines 70 - 71, Cache the flattened levels array
at module scope, alongside the existing flatQuestions cache, and update getLevel
to search that cached collection instead of calling chapters.flatMap on every
invocation.
src/components/CodeBlock.tsx (1)

9-23: 🚀 Performance & Scalability | 🔵 Trivial | 💤 Low value

Optional: avoid O(n²) spread in reduce.

...acc.parts on each iteration copies the entire accumulator, making this quadratic in the number of matches. For typical code snippets this is negligible, but if you want to keep it functional without the cost, consider collecting into a flat array via concat:

♻️ Optional refactor using concat
 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>,- ],+ parts: acc.parts.concat(gap, [+ <span key={i} className={cls}>+ {text}+ </span>,+ ]),
last: m.index + text.length,
}
},
{ parts: [], last: 0 },
)
🤖 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 `@src/components/CodeBlock.tsx` around lines 9 - 23, Optionally refactor the
matches.reduce accumulator in CodeBlock so each iteration avoids spreading the
full acc.parts array; append the gap and highlighted span through concat or
equivalent in-place collection while preserving output order and the existing
last-index tracking.
src/App.tsx (1)

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

Switch IIFE and quizLevel guard look correct.

The early return on line 75 safely guarantees quizLevel is non-null inside the case 'quiz' branch, making the quizLevel! assertions on lines 83 and 89 safe. All 11 View variants are covered by the switch, with default correctly mapping to Home.

One optional improvement: an explicit case 'home': instead of relying on default would give you compile-time exhaustiveness checking if a new view variant is added later.

♻️ Optional: explicit home case for exhaustiveness
 case 'profile':
return <Profile progress={progress} />
- default:+ case 'home':
return (
<Home
progress={progress}
onOpenChapter={(chapterId) => setView({ name: 'levellist', chapterId })}
onMixedPractice={startMixedPractice}
/>
)
+ default:+ const _exhaustive: never = view+ throw new Error(`Unhandled view: ${JSON.stringify(_exhaustive)}`)
}
🤖 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 `@src/App.tsx` around lines 72 - 191, Optionally replace the fallback default
branch in the content switch with an explicit case 'home' branch returning Home,
while preserving the existing Home props and behavior; use an exhaustiveness
check for any unhandled View variants so future additions are caught at compile
time.
🤖 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.
Nitpick comments:
In `@src/App.tsx`:
- Around line 72-191: Optionally replace the fallback default branch in the
content switch with an explicit case 'home' branch returning Home, while
preserving the existing Home props and behavior; use an exhaustiveness check for
any unhandled View variants so future additions are caught at compile time.
In `@src/components/CodeBlock.tsx`:
- Around line 9-23: Optionally refactor the matches.reduce accumulator in
CodeBlock so each iteration avoids spreading the full acc.parts array; append
the gap and highlighted span through concat or equivalent in-place collection
while preserving output order and the existing last-index tracking.
In `@src/data/chapters.ts`:
- Around line 70-71: Cache the flattened levels array at module scope, alongside
the existing flatQuestions cache, and update getLevel to search that cached
collection instead of calling chapters.flatMap on every invocation.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: bc335efe-a684-4394-b54f-4e2368fec16f

📥 Commits

Reviewing files that changed from the base of the PR and between 877bce3 and d7a2e5b.

📒 Files selected for processing (6)
  • src/App.tsx
  • src/app/api/progress/answer/route.ts
  • src/components/CodeBlock.tsx
  • src/components/Mascot.tsx
  • src/data/chapters.ts
  • src/hooks/useProgress.ts

@Retsomm
Retsomm merged commit 2931137 into mainJul 12, 2026
3 checks passed
Sign up for freeto 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

@Retsomm