From 22e516c7f7f0c446a13fa8bb06b081ca2e6fc757 Mon Sep 17 00:00:00 2001
From: kinga
Date: Fri, 24 Apr 2026 23:49:33 +0200
Subject: [PATCH 1/7] style(flashcards): implement theme synchronization and
enhance flashcard rendering with Markdown support
---
.../dashboard/flashcards/study/page.tsx | 112 ++++--------
.../app/api/flashcards/[id]/review/route.ts | 50 +++---
.../app/api/flashcards/study/route.ts | 15 +-
LearningPlatform/app/layout.tsx | 28 +--
.../components/admin/add-flashcard-dialog.tsx | 6 +-
LearningPlatform/components/admin/topbar.tsx | 9 +-
.../components/student/flashcard-markdown.tsx | 164 ++++++++++++++++++
.../student/markdown-theory-body.tsx | 5 +-
LearningPlatform/components/theme-sync.tsx | 48 +++++
LearningPlatform/components/theme-toggle.tsx | 5 +
LearningPlatform/lib/flashcard-media-urls.ts | 88 ++++++++++
LearningPlatform/lib/theme-cookie.ts | 18 ++
.../lib/valid-payload-media-id.ts | 23 +++
.../src/payload/collections/Tasks.ts | 19 ++
14 files changed, 454 insertions(+), 136 deletions(-)
create mode 100644 LearningPlatform/components/student/flashcard-markdown.tsx
create mode 100644 LearningPlatform/components/theme-sync.tsx
create mode 100644 LearningPlatform/lib/flashcard-media-urls.ts
create mode 100644 LearningPlatform/lib/theme-cookie.ts
create mode 100644 LearningPlatform/lib/valid-payload-media-id.ts
diff --git a/LearningPlatform/app/(student)/(shell)/dashboard/flashcards/study/page.tsx b/LearningPlatform/app/(student)/(shell)/dashboard/flashcards/study/page.tsx
index 9daad07..0479a2b 100644
--- a/LearningPlatform/app/(student)/(shell)/dashboard/flashcards/study/page.tsx
+++ b/LearningPlatform/app/(student)/(shell)/dashboard/flashcards/study/page.tsx
@@ -11,7 +11,7 @@
*
* The study loop:
* 1. Fetch due cards from /api/flashcards/study
- * 2. Show ONE card at a time (question side up)
+ * 2. Show ONE card at a time (question side up); question/answer use GFM Markdown + KaTeX ($ / $$)
* 3. Click / tap → flip to reveal answer
* 4. Tap Again / Hard / Good / Easy → POST to /api/flashcards/[id]/review
* 5. SRS algorithm runs server-side and returns updated card state
@@ -35,6 +35,7 @@ import {
Zap,
} from 'lucide-react'
import { cn } from '@/lib/utils'
+import { FlashcardRichText } from '@/components/student/flashcard-markdown'
// ─── Types ────────────────────────────────────────────────────────────────────
@@ -45,20 +46,23 @@ interface Tag {
}
interface StudyCard {
- id: string
- question: string
- answer: string
- questionImageId: string | null
- answerImageId: string | null
- state: string
- interval: number
- easeFactor: number
- repetition: number
- stepIndex: number
- nextReviewAt: string | null
- lastReviewedAt: string | null
- tags: Tag[]
- deck?: { id: string; name: string; slug: string } | null
+ id: string
+ question: string
+ answer: string
+ questionImageId: string | null
+ answerImageId: string | null
+ /** Resolved on the server so students do not need admin-only `/api/media/list`. */
+ questionImageUrl?: string | null
+ answerImageUrl?: string | null
+ state: string
+ interval: number
+ easeFactor: number
+ repetition: number
+ stepIndex: number
+ nextReviewAt: string | null
+ lastReviewedAt: string | null
+ tags: Tag[]
+ deck?: { id: string; name: string; slug: string } | null
}
type AnswerButton = 'AGAIN' | 'HARD' | 'GOOD' | 'EASY'
@@ -106,26 +110,6 @@ function stateLabel(state: string): { label: string; color: string } {
}
}
-/** Render LaTeX if katex is loaded, otherwise return the raw string. */
-async function renderLatex(raw: string): Promise {
- try {
- const katex = (await import('katex')).default
- // Block LaTeX: $$...$$
- let html = raw.replace(/\$\$([\s\S]*?)\$\$/g, (_m, tex) => {
- try { return katex.renderToString(tex, { displayMode: true, throwOnError: false }) }
- catch { return _m }
- })
- // Inline LaTeX: $...$
- html = html.replace(/\$([\s\S]*?)\$/g, (_m, tex) => {
- try { return katex.renderToString(tex, { displayMode: false, throwOnError: false }) }
- catch { return _m }
- })
- return html
- } catch {
- return raw
- }
-}
-
// ─── Answer button configs ────────────────────────────────────────────────────
const ANSWER_BUTTONS: {
@@ -196,11 +180,7 @@ function StudyPage() {
const [reviewedCount,setReviewedCount]= useState(0)
const totalRef = useRef(0)
- // LaTeX-rendered HTML for the current card
- const [questionHtml, setQuestionHtml] = useState('')
- const [answerHtml, setAnswerHtml] = useState('')
-
- // Image URLs resolved from Payload CMS media IDs
+ // Image URLs from GET /api/flashcards/study (and PATCH via review); set in effect when the card changes
const [questionImgUrl, setQuestionImgUrl] = useState(null)
const [answerImgUrl, setAnswerImgUrl] = useState(null)
@@ -233,39 +213,19 @@ function StudyPage() {
return () => window.clearTimeout(t)
}, [loadSession])
- // ── Render LaTeX whenever current card changes ─────────────────────────────
+ // ── Image URLs when the current card changes (markdown renders in JSX) ────────────────────────────
useEffect(() => {
const card = queue[currentIdx]
if (!card) return
queueMicrotask(() => {
- setQuestionHtml('')
- setAnswerHtml('')
setQuestionImgUrl(null)
setAnswerImgUrl(null)
})
- renderLatex(card.question).then(setQuestionHtml)
- renderLatex(card.answer).then(setAnswerHtml)
-
- // Resolve image IDs → URLs
- if (card.questionImageId || card.answerImageId) {
- fetch('/api/media/list')
- .then((r) => r.json())
- .then((d) => {
- const docs: { id: string | number; url: string }[] = d?.media ?? []
- if (card.questionImageId) {
- const m = docs.find((x) => String(x.id) === card.questionImageId)
- if (m) setQuestionImgUrl(m.url)
- }
- if (card.answerImageId) {
- const m = docs.find((x) => String(x.id) === card.answerImageId)
- if (m) setAnswerImgUrl(m.url)
- }
- })
- .catch(() => { /* ignore — images optional */ })
- }
+ setQuestionImgUrl(card.questionImageUrl ?? null)
+ setAnswerImgUrl(card.answerImageUrl ?? null)
}, [queue, currentIdx])
// ── Keyboard shortcuts ─────────────────────────────────────────────────────
@@ -539,16 +499,9 @@ function StudyPage() {
Question
- {questionHtml ? (
-
- ) : (
-
- {card.question}
-
- )}
+
+
+
{questionImgUrl && (
/* eslint-disable-next-line @next/next/no-img-element */
Answer
- {answerHtml ? (
-
- ) : (
-
- {card.answer}
-
- )}
+
+
+
{answerImgUrl && (
/* eslint-disable-next-line @next/next/no-img-element */
) {
- // The nonce is generated per-request in middleware.ts and forwarded via the
- // x-nonce request header. Applying it to the inline theme-init script allows
- // the Content-Security-Policy to block all OTHER inline scripts while still
- // permitting this one - eliminating the need for 'unsafe-inline' in script-src.
- // Use `undefined` when the header is absent so React omits the attribute
- // (rendering `nonce=""` causes hydration mismatches if the client
- // later applies a real nonce). `headers().get` may return null during
- // certain server-rendering scenarios, so prefer `undefined` over an
- // empty string to avoid emitting an empty attribute.
- const rawNonce = (await headers()).get('x-nonce')
- // Never pass '' — React/Next can serialize that as nonce="" and mismatch a later render that has a real nonce.
- const nonce = typeof rawNonce === 'string' && rawNonce.length > 0 ? rawNonce : undefined
+ const themeCookie = (await cookies()).get(THEME_COOKIE_NAME)?.value
+ const themePref = readThemeFromCookie(themeCookie)
+ const isDark = themePref === 'dark'
- const themeInitScript = `(function(){try{var k='theme';var v=localStorage.getItem(k);var r=document.documentElement;if(v==='dark'){r.classList.add('dark');}else if(v==='light'){r.classList.remove('dark');}else{r.classList.add('dark');}}catch(e){try{document.documentElement.classList.add('dark');}catch(_){}} })()`
return (
-
+
{/* Explicit favicon link to ensure the app/favicon.ico is used */}
-
+
{children}
diff --git a/LearningPlatform/components/admin/add-flashcard-dialog.tsx b/LearningPlatform/components/admin/add-flashcard-dialog.tsx
index e46eecf..d2108e0 100644
--- a/LearningPlatform/components/admin/add-flashcard-dialog.tsx
+++ b/LearningPlatform/components/admin/add-flashcard-dialog.tsx
@@ -472,8 +472,8 @@ export function FlashcardDialog({
Question *
- Supports LaTeX — wrap inline math in $…$ and display math in{' '}
- $$…$$.
+ Markdown (e.g. **bold**, lists, triple-backtick code fences) and LaTeX (
+ $…$, $$…$$) render on the study page like lesson theory blocks.