From d76ec4a69f30d574d503a359082e9f71712b33d5 Mon Sep 17 00:00:00 2001 From: Elijah Date: Sat, 27 Jun 2026 19:50:10 -0700 Subject: [PATCH] Numeroud bug fixes, UI improvements, small animations --- dev.db | Bin 131072 -> 139264 bytes .../[classSlug]/flashcards/page.tsx | 61 ++++++++--- .../[classSlug]/quizzes/[quizId]/page.tsx | 32 +++++- .../(protected)/[classSlug]/quizzes/page.tsx | 14 ++- src/app/(protected)/page.tsx | 102 +++++++++++++++--- src/app/globals.css | 4 + src/components/flashcards/FlashcardViewer.tsx | 79 +++++++++----- src/components/quizzes/QuizResults.tsx | 4 +- src/components/quizzes/QuizViewer.tsx | 27 ++--- src/services/settingsService.ts | 8 +- 10 files changed, 249 insertions(+), 82 deletions(-) diff --git a/dev.db b/dev.db index c00d883aeea7f44ea6a494dae00896a8557586eb..671e963c2a26df3ee4d31a9901073419980adcda 100644 GIT binary patch literal 139264 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 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: