Skip to content

Commit f96999c

Browse files
Edwin ChanEdwin Chan
authored andcommitted
added custom problem id and endless mode
1 parent fb0bbd0 commit f96999c

18 files changed

Lines changed: 128 additions & 91 deletions

File tree

app/admin/create/page.tsx

Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ type ContentFormat = "LATEX" | "HTML";
2929

3030
interface ProblemEntry {
3131
id: string;
32-
number: number;
32+
number: string;
3333
statement: string;
3434
contentFormat: ContentFormat;
3535
answerType: AnswerType;
@@ -54,10 +54,10 @@ function uid() {
5454
return Math.random().toString(36).slice(2, 10);
5555
}
5656

57-
function emptyProblem(n: number): ProblemEntry {
57+
function emptyProblem(n: number | string): ProblemEntry {
5858
return {
5959
id: uid(),
60-
number: n,
60+
number: String(n),
6161
statement: "",
6262
contentFormat: "LATEX",
6363
answerType: "INTEGER",
@@ -157,7 +157,7 @@ export default function CreateSetPage() {
157157
setProblems((prev) =>
158158
Array.from({ length: safeCount }, (_, index) => {
159159
const existing = prev[index];
160-
return existing ? { ...existing, number: index + 1 } : emptyProblem(index + 1);
160+
return existing ? existing : emptyProblem(index + 1);
161161
}),
162162
);
163163
}
@@ -184,8 +184,7 @@ export default function CreateSetPage() {
184184

185185
function removeProblem(id: string) {
186186
setProblems((prev) => {
187-
const next = prev.filter((p) => p.id !== id);
188-
return next.map((p, i) => ({ ...p, number: i + 1 }));
187+
return prev.filter((p) => p.id !== id);
189188
});
190189
}
191190

@@ -531,9 +530,23 @@ export default function CreateSetPage() {
531530
{problems.map((p) => (
532531
<div key={p.id} className="problem-card">
533532
<div className="problem-card-head">
534-
<div className="problem-number">
533+
<div className="problem-number" style={{ display: "flex", alignItems: "center", gap: 8 }}>
535534
<GripVertical size={14} className="grip-icon" />
536-
<span>Q{p.number}</span>
535+
<input
536+
type="text"
537+
value={p.number}
538+
onChange={(e) => updateProblem(p.id, "number", e.target.value)}
539+
style={{
540+
width: 60,
541+
padding: "4px 8px",
542+
borderRadius: 4,
543+
border: "1px solid var(--color-border)",
544+
background: "var(--color-surface)",
545+
color: "var(--color-text-strong)",
546+
fontWeight: 700,
547+
}}
548+
placeholder="ID"
549+
/>
537550
</div>
538551
<div className="problem-card-actions">
539552
<button

app/admin/sets/[id]/set-edit-form.tsx

Lines changed: 23 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ import { DeleteSetButton } from "../delete-set-button";
2424

2525
type ProblemData = {
2626
id: string;
27-
number: number;
27+
number: string;
2828
statement: string;
2929
contentFormat: "LATEX" | "HTML";
3030
answerKey: string;
@@ -81,10 +81,10 @@ function toggleTagInCsv(csv: string, tag: string): string {
8181
return next.join(", ");
8282
}
8383

84-
function newProblem(number: number) {
84+
function newProblem(number: number | string) {
8585
return {
8686
id: `new-${Math.random().toString(36).slice(2, 10)}`,
87-
number,
87+
number: String(number),
8888
statement: "",
8989
contentFormat: "LATEX" as const,
9090
answerKey: "",
@@ -179,6 +179,7 @@ export function SetEditForm({ set }: { set: SetData }) {
179179
| "answerKey"
180180
| "answerType"
181181
| "points"
182+
| "number"
182183
| "topicTagsInput"
183184
| "explanationNoteInput",
184185
value: string | number,
@@ -197,9 +198,7 @@ export function SetEditForm({ set }: { set: SetData }) {
197198
function removeProblem(problemId: string) {
198199
setProblems((prev) => {
199200
if (prev.length <= 1) return prev;
200-
return prev
201-
.filter((problem) => problem.id !== problemId)
202-
.map((problem, index) => ({ ...problem, number: index + 1 }));
201+
return prev.filter((problem) => problem.id !== problemId);
203202
});
204203
}
205204

@@ -208,7 +207,7 @@ export function SetEditForm({ set }: { set: SetData }) {
208207
setProblems((prev) =>
209208
Array.from({ length: safeCount }, (_, index) => {
210209
const existing = prev[index];
211-
return existing ? { ...existing, number: index + 1 } : newProblem(index + 1);
210+
return existing ? existing : newProblem(index + 1);
212211
}),
213212
);
214213
}
@@ -572,8 +571,23 @@ export function SetEditForm({ set }: { set: SetData }) {
572571
) : (
573572
<ChevronRight size={18} />
574573
)}
575-
<div className="problem-number">
576-
<span>Q{problem.number}</span>
574+
<div className="problem-number" style={{ display: "flex", alignItems: "center", gap: 8 }}>
575+
<input
576+
type="text"
577+
value={problem.number}
578+
onChange={(e) => updateProblem(problem.id, "number", e.target.value)}
579+
onClick={(e) => e.stopPropagation()}
580+
style={{
581+
width: 60,
582+
padding: "2px 6px",
583+
borderRadius: 4,
584+
border: "1px solid var(--color-border)",
585+
background: "var(--color-surface)",
586+
color: "var(--color-text-strong)",
587+
fontWeight: 700,
588+
}}
589+
placeholder="ID"
590+
/>
577591
</div>
578592
{!expandedProblems.has(problem.id) && (
579593
<small

app/api/practice/next/route.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,9 +35,7 @@ export async function GET(request: Request) {
3535
userId: session.user.id,
3636
},
3737
},
38-
topicTags: {
39-
has: tag,
40-
},
38+
...(tag.toLowerCase() === "endless" ? {} : { topicTags: { has: tag } }),
4139
},
4240
select: {
4341
id: true,

app/api/practice/tags/route.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,5 +55,5 @@ export async function GET() {
5555
.map(([tag]) => tag)
5656
.sort();
5757

58-
return NextResponse.json({ tags: validTags, practiceScore });
58+
return NextResponse.json({ tags: ["Endless", ...validTags], practiceScore });
5959
}

app/api/submit/report/route.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ export async function POST(req: Request) {
2525
let problemId = null;
2626
if (problemNumber) {
2727
const problem = await prisma.problem.findUnique({
28-
where: { problemSetId_number: { problemSetId, number: Number(problemNumber) } },
28+
where: { problemSetId_number: { problemSetId, number: String(problemNumber) } },
2929
});
3030
if (problem) {
3131
problemId = problem.id;

app/practice/page.tsx

Lines changed: 38 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -182,8 +182,7 @@ export default function PracticePage() {
182182
<Target size={48} style={{ margin: "0 auto 20px", color: "var(--color-pink)" }} />
183183
<h2 style={{ fontSize: "1.8rem", marginBottom: 10 }}>Select a Category</h2>
184184
<p style={{ color: "var(--color-muted)", marginBottom: 30 }}>
185-
Focus your training on specific topics. Only tags with more than 10 questions are
186-
shown.
185+
Focus your training on specific topics (10+ questions) or try the Endless mode.
187186
</p>
188187

189188
{loadingTags ? (
@@ -194,33 +193,43 @@ export default function PracticePage() {
194193
<p>No categories available with enough questions.</p>
195194
) : (
196195
<div style={{ display: "flex", flexWrap: "wrap", gap: 12, justifyContent: "center" }}>
197-
{tags.map((tag) => (
198-
<button
199-
key={tag}
200-
onClick={() => handleTagSelect(tag)}
201-
style={{
202-
padding: "12px 24px",
203-
borderRadius: 100,
204-
background: "var(--color-surface)",
205-
border: "1px solid var(--color-border)",
206-
color: "var(--color-text-strong)",
207-
fontSize: "1.1rem",
208-
fontWeight: 700,
209-
cursor: "pointer",
210-
transition: "all 0.2s",
211-
}}
212-
onMouseOver={(e) => {
213-
e.currentTarget.style.borderColor = "var(--color-pink)";
214-
e.currentTarget.style.transform = "translateY(-2px)";
215-
}}
216-
onMouseOut={(e) => {
217-
e.currentTarget.style.borderColor = "var(--color-border)";
218-
e.currentTarget.style.transform = "none";
219-
}}
220-
>
221-
{tag}
222-
</button>
223-
))}
196+
{tags.map((tag) => {
197+
const isEndless = tag.toLowerCase() === "endless";
198+
return (
199+
<button
200+
key={tag}
201+
onClick={() => handleTagSelect(tag)}
202+
style={{
203+
padding: "12px 24px",
204+
borderRadius: 100,
205+
background: isEndless
206+
? "linear-gradient(135deg, var(--color-pink), var(--color-purple))"
207+
: "var(--color-surface)",
208+
border: isEndless ? "none" : "1px solid var(--color-border)",
209+
color: isEndless ? "white" : "var(--color-text-strong)",
210+
fontSize: "1.1rem",
211+
fontWeight: 700,
212+
cursor: "pointer",
213+
transition: "all 0.2s",
214+
boxShadow: isEndless ? "0 4px 15px rgba(255, 0, 150, 0.3)" : "none",
215+
}}
216+
onMouseOver={(e) => {
217+
if (!isEndless) {
218+
e.currentTarget.style.borderColor = "var(--color-pink)";
219+
}
220+
e.currentTarget.style.transform = "translateY(-2px)";
221+
}}
222+
onMouseOut={(e) => {
223+
if (!isEndless) {
224+
e.currentTarget.style.borderColor = "var(--color-border)";
225+
}
226+
e.currentTarget.style.transform = "none";
227+
}}
228+
>
229+
{tag}
230+
</button>
231+
);
232+
})}
224233
</div>
225234
)}
226235
</section>

app/problem-sets/[slug]/answer-grid.tsx

Lines changed: 12 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ type SubmitResult = {
1212
maxScore: number;
1313
percentage: number;
1414
results: Array<{
15-
number: number;
15+
number: string;
1616
rawAnswer: string;
1717
isCorrect: boolean;
1818
pointsAwarded: number;
@@ -21,9 +21,9 @@ type SubmitResult = {
2121

2222
type Props = {
2323
problemSetId: string;
24-
problemCount: number;
24+
problemNumbers: string[];
2525
problemSummaries?: Array<{
26-
number: number;
26+
number: string;
2727
topicTags: string[];
2828
explanationNote: string | null;
2929
contentFormat: "LATEX" | "HTML";
@@ -37,12 +37,11 @@ const REVIEW_KEY_PREFIX = "mo-review-";
3737

3838
export function AnswerGrid({
3939
problemSetId,
40-
problemCount,
40+
problemNumbers,
4141
problemSummaries = [],
4242
videoUrl = null,
4343
lockedAttemptNumber = null,
4444
}: Props) {
45-
const problemNumbers = Array.from({ length: problemCount }, (_, i) => i + 1);
4645
const autosaveKey = `${AUTOSAVE_KEY_PREFIX}${problemSetId}`;
4746
const reviewKey = `${REVIEW_KEY_PREFIX}${problemSetId}`;
4847

@@ -59,11 +58,11 @@ export function AnswerGrid({
5958
const [isSubmitting, setIsSubmitting] = useState(false);
6059
const [submitResult, setSubmitResult] = useState<SubmitResult | null>(null);
6160
const [submitError, setSubmitError] = useState<string | null>(null);
62-
const [reviewLater, setReviewLater] = useState<Set<number>>(() => {
61+
const [reviewLater, setReviewLater] = useState<Set<string>>(() => {
6362
if (typeof window === "undefined") return new Set();
6463
try {
6564
const saved = localStorage.getItem(reviewKey);
66-
return new Set(saved ? (JSON.parse(saved) as number[]) : []);
65+
return new Set(saved ? (JSON.parse(saved) as string[]) : []);
6766
} catch {
6867
return new Set();
6968
}
@@ -93,9 +92,9 @@ export function AnswerGrid({
9392
[autosaveKey],
9493
);
9594

96-
function onAnswerChange(number: number, value: string) {
95+
function onAnswerChange(number: string, value: string) {
9796
setAnswers((prev) => {
98-
const next = { ...prev, [String(number)]: value };
97+
const next = { ...prev, [number]: value };
9998
debouncedSave(next);
10099
return next;
101100
});
@@ -151,7 +150,7 @@ export function AnswerGrid({
151150
startTime.current = Date.now();
152151
}
153152

154-
function toggleReviewLater(problemNumber: number) {
153+
function toggleReviewLater(problemNumber: string) {
155154
setReviewLater((current) => {
156155
const next = new Set(current);
157156
if (next.has(problemNumber)) {
@@ -187,7 +186,7 @@ export function AnswerGrid({
187186
headers: { "Content-Type": "application/json" },
188187
body: JSON.stringify({
189188
problemSetId,
190-
problemNumber: problemNumber ? Number(problemNumber) : null,
189+
problemNumber: problemNumber ? problemNumber : null,
191190
type,
192191
message,
193192
}),
@@ -453,7 +452,7 @@ export function AnswerGrid({
453452
aria-label={`Answer ${number}`}
454453
name={`answer-${number}`}
455454
placeholder="answer"
456-
value={answers[String(number)] ?? ""}
455+
value={answers[number] ?? ""}
457456
onChange={(e) => onAnswerChange(number, e.target.value)}
458457
/>
459458
</label>
@@ -462,7 +461,7 @@ export function AnswerGrid({
462461

463462
<div className="problem-actions">
464463
<span className="fill-count">
465-
{filledCount}/{problemCount} answered
464+
{filledCount}/{problemNumbers.length} answered
466465
</span>
467466
<button
468467
className="secondary-action"

app/problem-sets/[slug]/page.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -203,7 +203,7 @@ export default async function ProblemSetPage({ params }: ProblemSetPageProps) {
203203
</div>
204204
<AnswerGrid
205205
lockedAttemptNumber={perfectAttempt?.attemptNumber ?? null}
206-
problemCount={problemCount}
206+
problemNumbers={problemSet.problems.map((p) => p.number)}
207207
problemSummaries={problemSet.problems.map((problem) => ({
208208
number: problem.number,
209209
topicTags: problem.topicTags,

app/typewriter-greeting.tsx

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,15 +9,19 @@ const BETWEEN_GREETING_MS = 280;
99

1010
const GREETINGS = [
1111
(name: string) => `Hello, ${name}!`,
12-
(name: string) => `Ready for another set, ${name}?`,
13-
(name: string) => `Get on, ${name}.`,
12+
(name: string) => `Eat more curry, ${name}!`,
13+
(name: string) => `Subscribe to Let's Think Critically, ${name}!`,
1414
(name: string) => `Welcome back, ${name}.`,
15-
(name: string) => `Wassup, ${name}.`,
15+
(name: string) => `Wassup :), ${name}.`,
1616
(name: string) => `Good to see you, ${name}.`,
17-
(name: string) => `A little progress today, ${name}?`,
1817
(name: string) => `Make me proud, ${name}.`,
1918
(name: string) => `Be Culver Kwan, ${name}.`,
2019
(name: string) => `Time to lock in, ${name}.`,
20+
(name: string) => `Be Marcoroni :3, ${name}.`,
21+
(name: string) => `Marcoroni is typing..., ${name}.`,
22+
(name: string) => `Search Marco The Dog, ${name}.`,
23+
(name: string) => `Solve these problems if you're not gay, ${name}.`
24+
2125
];
2226

2327
function GreetingTyper({ name }: { name: string }) {

lib/analytics.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ export type ScoreBucket = {
1717
export type QuestionStat = {
1818
problemSetTitle: string;
1919
problemSetSlug: string;
20-
problemNumber: number;
20+
problemNumber: string;
2121
total: number;
2222
correct: number;
2323
accuracy: number;
@@ -71,7 +71,7 @@ export function computeScoreBuckets(
7171
export function computeQuestionStats(
7272
responses: Array<{
7373
isCorrect: boolean;
74-
problem: { number: number; problemSetId: string };
74+
problem: { number: string; problemSetId: string };
7575
}>,
7676
setMap: Map<string, { title: string; slug: string }>,
7777
): QuestionStat[] {

0 commit comments

Comments
 (0)