Skip to content

Commit 214b1fc

Browse files
committed
fixing bugs with the ucat questions feature
1 parent bc4adec commit 214b1fc

File tree

8 files changed

+63
-29
lines changed

8 files changed

+63
-29
lines changed

app/admin/page.tsx

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,10 @@ type UploadType = "ucat" | "mmi";
2121

2222
type UploadRow = Record<string, string>;
2323

24+
// Normalize CSV headers for matching.
2425
const normalizeHeader = (value: string) => value.trim().toLowerCase();
2526

27+
// Minimal CSV parser supporting quoted fields.
2628
const parseCsv = (content: string) => {
2729
const rows: string[][] = [];
2830
let current = "";
@@ -90,6 +92,7 @@ export default function AdminPage() {
9092
setUploadError(null);
9193
};
9294

95+
// Insert a single MMI question.
9396
const handleMmiSubmit = async () => {
9497
resetMessages();
9598
if (!mmiQuestion.trim() || !mmiAnswer.trim()) {
@@ -114,6 +117,7 @@ export default function AdminPage() {
114117
setIsSaving(false);
115118
};
116119

120+
// Insert a single UCAT question.
117121
const handleUcatSubmit = async () => {
118122
resetMessages();
119123
if (!ucatQuestion.trim()) {
@@ -142,11 +146,11 @@ export default function AdminPage() {
142146
const supabase = createClient();
143147
const { error } = await supabase.from("Ucat").insert({
144148
question: ucatQuestion.trim(),
145-
answer1: ucatAnswers[0]?.trim() || null,
146-
answer2: ucatAnswers[1]?.trim() || null,
147-
answer3: ucatAnswers[2]?.trim() || null,
148-
answer4: ucatAnswers[3]?.trim() || null,
149-
answer5: ucatAnswers[4]?.trim() || null,
149+
"answer 1": ucatAnswers[0]?.trim() || null,
150+
"answer 2": ucatAnswers[1]?.trim() || null,
151+
"answer 3": ucatAnswers[2]?.trim() || null,
152+
"answer 4": ucatAnswers[3]?.trim() || null,
153+
"answer 5": ucatAnswers[4]?.trim() || null,
150154
correct_answer: correctNumber,
151155
type: ucatType.trim(),
152156
});
@@ -163,6 +167,7 @@ export default function AdminPage() {
163167
setIsSaving(false);
164168
};
165169

170+
// Parse CSV and bulk insert rows.
166171
const handleCsvUpload = async (file: File, type: UploadType) => {
167172
resetMessages();
168173
setUploadPreview([]);
@@ -215,11 +220,11 @@ export default function AdminPage() {
215220
} else {
216221
const payload = mappedRows.map((row) => ({
217222
question: row.question?.trim(),
218-
answer1: row.answer1?.trim() || null,
219-
answer2: row.answer2?.trim() || null,
220-
answer3: row.answer3?.trim() || null,
221-
answer4: row.answer4?.trim() || null,
222-
answer5: row.answer5?.trim() || null,
223+
"answer 1": row.answer1?.trim() || null,
224+
"answer 2": row.answer2?.trim() || null,
225+
"answer 3": row.answer3?.trim() || null,
226+
"answer 4": row.answer4?.trim() || null,
227+
"answer 5": row.answer5?.trim() || null,
223228
correct_answer: Number(row.correct_answer) || null,
224229
type: row.type?.trim() || null,
225230
}));

app/auth/callback/route.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ export async function GET(request: Request) {
77
const code = requestUrl.searchParams.get('code')
88

99
if (code) {
10+
// Exchange the OAuth code for a Supabase session cookie.
1011
const supabase = await createClient()
1112
await supabase.auth.exchangeCodeForSession(code)
1213
}

app/mmi-prep/page.tsx

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,13 +27,15 @@ export default function Home() {
2727
const [dailyError, setDailyError] = useState<string | null>(null);
2828
const [practiceError, setPracticeError] = useState<string | null>(null);
2929

30+
// Normalize date to a local YYYY-MM-DD string for daily tracking.
3031
const getLocalDateString = (date = new Date()) => {
3132
const year = date.getFullYear();
3233
const month = String(date.getMonth() + 1).padStart(2, "0");
3334
const day = String(date.getDate()).padStart(2, "0");
3435
return `${year}-${month}-${day}`;
3536
};
3637

38+
// Convert today's date into day-of-year (1-366) for deterministic daily pick.
3739
const getDayOfYear = (date = new Date()) => {
3840
const start = new Date(date.getFullYear(), 0, 0);
3941
const diff =
@@ -43,6 +45,7 @@ export default function Home() {
4345
return Math.floor(diff / (1000 * 60 * 60 * 24));
4446
};
4547

48+
// Fetch the Nth question (0-based) to keep daily rotation stable.
4649
const fetchMmiQuestionByIndex = useCallback(async (index: number) => {
4750
const supabase = createClient();
4851
const { data, error } = await supabase
@@ -57,6 +60,7 @@ export default function Home() {
5760
return data ?? null;
5861
}, []);
5962

63+
// Random practice question (used for practice + daily fallback).
6064
const fetchRandomMmiQuestion = useCallback(async () => {
6165
const supabase = createClient();
6266
const { count, error: countError } = await supabase
@@ -70,6 +74,7 @@ export default function Home() {
7074
return fetchMmiQuestionByIndex(randomIndex);
7175
}, [fetchMmiQuestionByIndex]);
7276

77+
// Daily question: day-of-year, fallback to random if missing.
7378
const fetchDailyQuestion = useCallback(async () => {
7479
setIsDailyLoading(true);
7580
setDailyError(null);
@@ -98,6 +103,7 @@ export default function Home() {
98103
}
99104
}, [fetchMmiQuestionByIndex, fetchRandomMmiQuestion]);
100105

106+
// Practice question is always random to encourage variety.
101107
const fetchPracticeQuestion = useCallback(async () => {
102108
setIsPracticeLoading(true);
103109
setPracticeError(null);
@@ -111,6 +117,7 @@ export default function Home() {
111117
}
112118
}, [fetchRandomMmiQuestion]);
113119

120+
// Track daily usage and streaks in localStorage.
114121
const recordDailyReveal = () => {
115122
if (typeof window === "undefined") return;
116123

@@ -167,6 +174,7 @@ export default function Home() {
167174
window.localStorage.setItem(STORAGE_KEYS.history, JSON.stringify(trimmedHistory));
168175
};
169176

177+
// Prep timer countdown.
170178
useEffect(() => {
171179
if (!isPrepRunning) return;
172180
const intervalId = window.setInterval(() => {
@@ -184,6 +192,7 @@ export default function Home() {
184192
return () => window.clearInterval(intervalId);
185193
}, [isPrepRunning, respDuration]);
186194

195+
// Response timer countdown.
187196
useEffect(() => {
188197
if (!isRespRunning) return;
189198
const intervalId = window.setInterval(() => {
@@ -192,10 +201,12 @@ export default function Home() {
192201
return () => window.clearInterval(intervalId);
193202
}, [isRespRunning]);
194203

204+
// Load daily question on first render.
195205
useEffect(() => {
196206
fetchDailyQuestion();
197207
}, [fetchDailyQuestion]);
198208

209+
// Load practice question when switching into practice mode.
199210
useEffect(() => {
200211
if (isPracticeMode && !practiceQuestion && !isPracticeLoading) {
201212
fetchPracticeQuestion();

app/submit-questions/page.tsx

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ export default function SubmitQuestionsPage() {
2222

2323
const resetMessage = () => setMessage(null);
2424

25+
// Insert a user-submitted MMI question.
2526
const handleMmiSubmit = async () => {
2627
resetMessage();
2728
if (!mmiQuestion.trim() || !mmiAnswer.trim()) {
@@ -46,6 +47,7 @@ export default function SubmitQuestionsPage() {
4647
setIsSaving(false);
4748
};
4849

50+
// Insert a user-submitted UCAT question.
4951
const handleUcatSubmit = async () => {
5052
resetMessage();
5153
if (!ucatQuestion.trim()) {
@@ -74,11 +76,11 @@ export default function SubmitQuestionsPage() {
7476
const supabase = createClient();
7577
const { error } = await supabase.from("Ucat").insert({
7678
question: ucatQuestion.trim(),
77-
answer1: ucatAnswers[0]?.trim() || null,
78-
answer2: ucatAnswers[1]?.trim() || null,
79-
answer3: ucatAnswers[2]?.trim() || null,
80-
answer4: ucatAnswers[3]?.trim() || null,
81-
answer5: ucatAnswers[4]?.trim() || null,
79+
"answer 1": ucatAnswers[0]?.trim() || null,
80+
"answer 2": ucatAnswers[1]?.trim() || null,
81+
"answer 3": ucatAnswers[2]?.trim() || null,
82+
"answer 4": ucatAnswers[3]?.trim() || null,
83+
"answer 5": ucatAnswers[4]?.trim() || null,
8284
correct_answer: correctNumber,
8385
type: ucatType.trim(),
8486
});

app/ucat-prep/page.tsx

Lines changed: 22 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ export default function Home() {
5656
selectedQuestions ?? (hasValidCustomQuestions ? customQuestionsValue : 0);
5757
const timerMinutes = selectedTimer ?? (hasValidCustom ? customMinutesValue : 0);
5858

59+
// Load a random window of UCAT questions, optionally filtered by type.
5960
const loadQuestions = async (limit: number, types: string[]) => {
6061
if (!limit) return [];
6162
const supabase = createClient();
@@ -66,7 +67,7 @@ export default function Home() {
6667
}
6768
const { count, error: countError } = await countQuery;
6869

69-
if (countError) throw countError;
70+
if (countError) throw new Error(countError.message);
7071
if (!count) return [];
7172

7273
const safeLimit = Math.min(limit, count);
@@ -75,7 +76,9 @@ export default function Home() {
7576

7677
let dataQuery = supabase
7778
.from("Ucat")
78-
.select("id, question, answer1, answer2, answer3, answer4, answer5, correct_answer")
79+
.select(
80+
'id, question, "answer 1", "answer 2", "answer 3", "answer 4", "answer 5", correct_answer, type'
81+
)
7982
.order("id", { ascending: true })
8083
.range(offset, offset + safeLimit - 1);
8184

@@ -85,15 +88,15 @@ export default function Home() {
8588

8689
const { data, error } = await dataQuery;
8790

88-
if (error) throw error;
91+
if (error) throw new Error(error.message);
8992

9093
return (data ?? []).map((row, index) => {
9194
const options = [
92-
row.answer1,
93-
row.answer2,
94-
row.answer3,
95-
row.answer4,
96-
row.answer5,
95+
row["answer 1"],
96+
row["answer 2"],
97+
row["answer 3"],
98+
row["answer 4"],
99+
row["answer 5"],
97100
]
98101
.map((value) => (typeof value === "string" ? value.trim() : ""))
99102
.filter(Boolean);
@@ -115,6 +118,7 @@ export default function Home() {
115118
});
116119
};
117120

121+
// Start a quiz session and fetch questions from Supabase.
118122
const handleStart = async () => {
119123
if (!canStart) return;
120124
const selectedTypes = [
@@ -144,9 +148,11 @@ export default function Home() {
144148
setTimeRemaining(null);
145149
}
146150
} catch (error) {
147-
setQuestionLoadError(
148-
error instanceof Error ? error.message : "Failed to load UCAT questions."
149-
);
151+
if (error && typeof error === "object" && "message" in error) {
152+
setQuestionLoadError(String(error.message));
153+
} else {
154+
setQuestionLoadError("Failed to load UCAT questions.");
155+
}
150156
} finally {
151157
setIsLoadingQuestions(false);
152158
}
@@ -156,11 +162,12 @@ export default function Home() {
156162
if (!questions.length) {
157163
return {
158164
correctCount: 0,
159-
results: [] as { id: number; isCorrect: boolean | null }[],
165+
results: [] as { id: number; number: number; isCorrect: boolean | null }[],
160166
};
161167
}
162168
const results = questions.map((question, index) => ({
163169
id: question.id,
170+
number: index + 1,
164171
isCorrect:
165172
answerResults[index] ??
166173
(question.correctIndex === null
@@ -171,6 +178,7 @@ export default function Home() {
171178
return { correctCount, results };
172179
}, [questions, selectedAnswers, answerResults]);
173180

181+
// Timer countdown for timed mode.
174182
useEffect(() => {
175183
if (!hasStarted || practiceMode !== "timed" || timeRemaining === null) return;
176184
if (timeRemaining <= 0) return;
@@ -180,6 +188,7 @@ export default function Home() {
180188
return () => window.clearInterval(intervalId);
181189
}, [hasStarted, practiceMode, timeRemaining]);
182190

191+
// Simple mm:ss formatter for timers.
183192
const formatSeconds = (totalSeconds: number) => {
184193
const minutes = Math.floor(totalSeconds / 60);
185194
const seconds = totalSeconds % 60;
@@ -345,7 +354,7 @@ export default function Home() {
345354
: "incorrect"
346355
}`}
347356
>
348-
Question {result.id}:{" "}
357+
Question {result.number}:{" "}
349358
{result.isCorrect === null
350359
? "Unscored"
351360
: result.isCorrect

lib/supabase/client.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { createBrowserClient } from '@supabase/ssr'
22

33
function getSupabaseEnv() {
4+
// Read public Supabase config from env and fail fast if missing.
45
const url = process.env.NEXT_PUBLIC_SUPABASE_URL
56
const anonKey =
67
process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_OR_ANON_KEY ??
@@ -16,6 +17,7 @@ function getSupabaseEnv() {
1617
}
1718

1819
export function createClient() {
20+
// Browser client for client components.
1921
const { url, anonKey } = getSupabaseEnv()
2022
return createBrowserClient(url, anonKey)
2123
}

lib/supabase/server.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { createServerClient } from '@supabase/ssr'
22
import { cookies } from 'next/headers'
33

44
function getSupabaseEnv() {
5+
// Server uses the same public URL + anon key as the browser client.
56
const url = process.env.NEXT_PUBLIC_SUPABASE_URL
67
const anonKey =
78
process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_OR_ANON_KEY ??
@@ -18,6 +19,7 @@ function getSupabaseEnv() {
1819

1920
export async function createClient() {
2021
const { url, anonKey } = getSupabaseEnv()
22+
// cookies() is async in Next 16+, so await before reading/writing.
2123
const cookieStore = await cookies()
2224

2325
return createServerClient(url, anonKey, {
@@ -26,7 +28,7 @@ export async function createClient() {
2628
return cookieStore.getAll()
2729
},
2830
setAll(cookiesToSet) {
29-
// In server components, set() can throw. Middleware should handle auth refresh.
31+
// In server components, set() can throw; middleware handles refresh safely.
3032
try {
3133
cookiesToSet.forEach(({ name, value, options }) => {
3234
cookieStore.set(name, value, options)

middleware.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ export async function middleware(request: NextRequest) {
88
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY
99

1010
if (!url || !anonKey) {
11+
// If env vars aren't set, just continue without Supabase auth refresh.
1112
return NextResponse.next({ request })
1213
}
1314

@@ -26,6 +27,7 @@ export async function middleware(request: NextRequest) {
2627
},
2728
})
2829

30+
// Touch auth to refresh session cookies when needed.
2931
await supabase.auth.getUser()
3032

3133
return response

0 commit comments

Comments
 (0)