diff --git a/dev.db b/dev.db index c00d883..671e963 100644 Binary files a/dev.db and b/dev.db differ diff --git a/src/app/(protected)/[classSlug]/flashcards/page.tsx b/src/app/(protected)/[classSlug]/flashcards/page.tsx index bd2f091..68d364a 100644 --- a/src/app/(protected)/[classSlug]/flashcards/page.tsx +++ b/src/app/(protected)/[classSlug]/flashcards/page.tsx @@ -18,11 +18,13 @@ interface DeckItem { }>; } +const cache: Record = {}; + export default function FlashcardsPage() { const params = useParams(); const classSlug = params.classSlug as string; - const [decks, setDecks] = useState([]); - const [loading, setLoading] = useState(true); + const [decks, setDecks] = useState(cache[classSlug] || []); + const [loading, setLoading] = useState(!cache[classSlug]); const [showImport, setShowImport] = useState(false); const [classId, setClassId] = useState(""); const [editingId, setEditingId] = useState(null); @@ -33,7 +35,9 @@ export default function FlashcardsPage() { const res = await fetch(`/api/decks/list?classId=${cId}`); if (res.ok) { const data = await res.json(); - setDecks(Array.isArray(data) ? data : []); + const arr = Array.isArray(data) ? data : []; + setDecks(arr); + cache[classSlug] = arr; } } catch { setDecks([]); @@ -124,7 +128,7 @@ export default function FlashcardsPage() { {/* Empty state */} {!loading && decks.length === 0 && ( -
+
@@ -143,16 +147,16 @@ export default function FlashcardsPage() { {/* Deck grid */} {!loading && decks.length > 0 && ( -
+
{decks.map((deck) => { const progressLabel = getProgressLabel(deck); return (
-
+
{/* Name (editable) */} {editingId === deck.id ? (
@@ -188,7 +192,7 @@ export default function FlashcardsPage() { )} {/* Stats */} -
+
{deck._count.cards} {deck._count.cards === 1 ? "card" : "cards"} @@ -198,13 +202,40 @@ export default function FlashcardsPage() {
{/* Actions */} -
- - {deck.progress?.length ? "Continue" : "Study"} - +
+ {deck.progress?.length ? ( + <> + + Continue + + + + ) : ( + + Study + + )}
{/* View toggle & Share */} -
+
+ {view === "take" && ( + + )} {attempts.length > 0 && ( @@ -163,6 +192,7 @@ export default function QuizStudyPage() { /> ) : ( { diff --git a/src/app/(protected)/[classSlug]/quizzes/page.tsx b/src/app/(protected)/[classSlug]/quizzes/page.tsx index 8c5dc5d..4f3e580 100644 --- a/src/app/(protected)/[classSlug]/quizzes/page.tsx +++ b/src/app/(protected)/[classSlug]/quizzes/page.tsx @@ -12,11 +12,13 @@ interface QuizItem { _count: { questions: number; attempts: number }; } +const cache: Record = {}; + export default function QuizzesPage() { const params = useParams(); const classSlug = params.classSlug as string; - const [quizzes, setQuizzes] = useState([]); - const [loading, setLoading] = useState(true); + const [quizzes, setQuizzes] = useState(cache[classSlug] || []); + const [loading, setLoading] = useState(!cache[classSlug]); const [showImport, setShowImport] = useState(false); const [classId, setClassId] = useState(""); @@ -25,7 +27,9 @@ export default function QuizzesPage() { const res = await fetch(`/api/quizzes/list?classId=${cId}`); if (res.ok) { const data = await res.json(); - setQuizzes(Array.isArray(data) ? data : []); + const arr = Array.isArray(data) ? data : []; + setQuizzes(arr); + cache[classSlug] = arr; } } catch { setQuizzes([]); @@ -89,7 +93,7 @@ export default function QuizzesPage() { {/* Empty state */} {!loading && quizzes.length === 0 && ( -
+
@@ -108,7 +112,7 @@ export default function QuizzesPage() { {/* Quiz grid */} {!loading && quizzes.length > 0 && ( -
+
{quizzes.map((quiz) => (
(null); + const [editName, setEditName] = useState(""); + const [savingEdit, setSavingEdit] = useState(false); useEffect(() => { fetchClasses(); @@ -60,6 +63,27 @@ export default function HomePage() { fetchClasses(); } + async function handleSaveEdit(id: string) { + if (!editName.trim()) { + setEditingId(null); + return; + } + setSavingEdit(true); + try { + const res = await fetch(`/api/classes/${id}`, { + method: "PATCH", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ name: editName.trim() }), + }); + if (res.ok) { + setEditingId(null); + fetchClasses(); + } + } finally { + setSavingEdit(false); + } + } + return (
{/* Header */} @@ -167,9 +191,36 @@ export default function HomePage() { href={`/${cls.slug}`} className="block p-6" > -

- {cls.name} -

+ {editingId === cls.id ? ( + { + e.preventDefault(); + e.stopPropagation(); + }} + onChange={(e) => setEditName(e.target.value)} + onKeyDown={(e) => { + if (e.key === "Enter") { + e.preventDefault(); + e.stopPropagation(); + handleSaveEdit(cls.id); + } else if (e.key === "Escape") { + e.preventDefault(); + e.stopPropagation(); + setEditingId(null); + } + }} + onBlur={() => handleSaveEdit(cls.id)} + autoFocus + disabled={savingEdit} + className="w-[calc(100%-2.5rem)] text-lg font-semibold text-text-heading px-2 py-1 -ml-2 rounded bg-bg-surface-alt border border-primary/30 focus:outline-none focus:ring-1 focus:ring-primary disabled:opacity-50" + /> + ) : ( +

+ {cls.name} +

+ )}
@@ -186,19 +237,38 @@ export default function HomePage() {
- {/* Delete button */} - + {/* Actions */} +
+ {/* Delete button */} + + {/* Edit button */} + +
))}
diff --git a/src/app/globals.css b/src/app/globals.css index 40bba84..26b14d7 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -77,6 +77,10 @@ body { transform: rotateY(180deg); } +.rotate-x-180 { + transform: rotateX(180deg); +} + /* ── Swipe animations ── */ @keyframes swipeLeft { 0% { transform: translateX(0) rotate(0deg); opacity: 1; } diff --git a/src/components/flashcards/FlashcardViewer.tsx b/src/components/flashcards/FlashcardViewer.tsx index 28785b2..a2849bd 100644 --- a/src/components/flashcards/FlashcardViewer.tsx +++ b/src/components/flashcards/FlashcardViewer.tsx @@ -121,7 +121,7 @@ export function FlashcardViewer({ cards, deckId, isShared = false, initialProgre function handleKeyDown(e: KeyboardEvent) { if (!started || completed) return; - if (e.key === " " || e.key === "Enter") { + if (e.key === " " || e.key === "Enter" || e.key === "ArrowUp" || e.key === "ArrowDown") { e.preventDefault(); setIsFlipped((prev) => { if (!prev) setHasFlippedOnce(true); @@ -358,8 +358,8 @@ export function FlashcardViewer({ cards, deckId, isShared = false, initialProgre >
{/* Front */} @@ -378,7 +378,7 @@ export function FlashcardViewer({ cards, deckId, isShared = false, initialProgre
{/* Back */} -
+
Back @@ -412,35 +412,60 @@ export function FlashcardViewer({ cards, deckId, isShared = false, initialProgre )}
- {/* Grading buttons (visible after first flip) */} - {hasFlippedOnce && ( -
+
+ {currentIndex > 0 && ( - -
- )} + )} - {/* Keyboard hint */} -
- {hasFlippedOnce - ? "← Missed · Got It →" - : "Space to flip"} +
+ {hasFlippedOnce && ( +
+ + +
+ )} +
+ +
+ {hasFlippedOnce + ? "← Missed · Got It →" + : "Space to flip"} +
)} diff --git a/src/components/quizzes/QuizResults.tsx b/src/components/quizzes/QuizResults.tsx index 501e06e..7c7c2a3 100644 --- a/src/components/quizzes/QuizResults.tsx +++ b/src/components/quizzes/QuizResults.tsx @@ -68,7 +68,7 @@ export function QuizResults({ quiz, attempt, onRetake, onClose }: QuizResultsPro
{Math.round(percentage)}% - ({attempt.score.toFixed(2)} / {attempt.maxScore}) + ({parseFloat(attempt.score.toFixed(2))} / {attempt.maxScore})
@@ -107,7 +107,7 @@ export function QuizResults({ quiz, attempt, onRetake, onClose }: QuizResultsPro return (
- Score: {item.score.toFixed(2)} / 1 + Score: {parseFloat(item.score.toFixed(2))} / 1 {q.category}
diff --git a/src/components/quizzes/QuizViewer.tsx b/src/components/quizzes/QuizViewer.tsx index 8486b80..f348fbe 100644 --- a/src/components/quizzes/QuizViewer.tsx +++ b/src/components/quizzes/QuizViewer.tsx @@ -271,7 +271,7 @@ export function QuizViewer({ quiz, retakeIds, isShared = false, onFinished }: Qu
Score so far: - {runningScore.toFixed(2)} / {maxPossibleSoFar} + {parseFloat(runningScore.toFixed(2))} / {maxPossibleSoFar}
@@ -334,21 +334,24 @@ export function QuizViewer({ quiz, retakeIds, isShared = false, onFinished }: Qu className={`flex items-start gap-4 p-4 rounded-xl border-2 transition-all duration-200 ${stateClass}`} >
- {icon ? icon : ( -
- {isSelected && ( - - - - )} -
- )} +
+ {isSelected && ( + + + + )} +
{opt.text}
+ {icon && ( +
+ {icon} +
+ )}
); })} diff --git a/src/services/settingsService.ts b/src/services/settingsService.ts index 7f13abb..6d20155 100644 --- a/src/services/settingsService.ts +++ b/src/services/settingsService.ts @@ -10,9 +10,9 @@ For flashcards, use this shape: { "type": "flashcards", "deckName": "...", "description": "...", "cards": [{"front": "...", "back": "..."}] } Rules: -- CRITICAL: Output MUST be valid JSON. All strings must be wrapped in double quotes. +- CRITICAL: Output MUST be valid JSON. EVERY string value must start and end with double quotes. Do NOT output unquoted strings. Example: "back": "your text here". - Use Markdown formatting (bullet lists, **bold**, tables) inside "back" - text wherever it improves clarity. You MUST escape any newlines in the string as \n. Do not use raw newlines. + text wherever it improves clarity. You MUST escape any newlines in the string as \\n. Do not use raw newlines. - Base everything strictly on the material below. Do not add outside facts. Material: @@ -31,7 +31,7 @@ For quizzes, use this shape: ]} Rules: -- CRITICAL: Output MUST be valid JSON. All strings must be wrapped in double quotes. +- CRITICAL: Output MUST be perfectly valid JSON. EVERY string value (including rationale and prompt) MUST start and end with double quotes. Do NOT output unquoted strings. Example: "rationale": "your text here" - "multiple_choice" questions must have exactly one option with correct: true. - "sata" questions must have two or more options with correct: true. - Every question needs a rationale explaining why the correct option(s) are @@ -42,7 +42,7 @@ Rules: a new variant each time, and keep the total number of distinct categories small relative to the number of questions. - Use Markdown formatting (bullet lists, **bold**, tables) inside - "rationale" text wherever it improves clarity. You MUST escape any newlines in the string as \n. Do not use raw newlines. + "rationale" text wherever it improves clarity. You MUST escape any newlines in the string as \\n. Do not use raw newlines. - Base everything strictly on the material below. Do not add outside facts. Material: