Skip to content

Commit cfed022

Browse files
Edwin ChanEdwin Chan
authored andcommitted
fixed bugs
1 parent 5d20ce9 commit cfed022

27 files changed

Lines changed: 312 additions & 134 deletions

File tree

.github/workflows/ci.yml

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ jobs:
4141
cache: npm
4242
- uses: astral-sh/setup-uv@v5
4343
- run: npm ci
44-
- run: npx prisma migrate deploy
44+
- run: npx prisma db push
4545
- run: npm run db:seed
4646
- run: npm run lint
4747
- run: npm run typecheck
@@ -75,4 +75,3 @@ jobs:
7575
curl -fsS http://localhost:3001 >/dev/null
7676
7777
BU_NAME=dbsmo-ci BU_CDP_URL=http://127.0.0.1:9222 BASE_URL=http://localhost:3001 browser-harness -c 'exec(open("tests/browser_harness_smoke.py").read())'
78-
fi

app/admin/create/page.tsx

Lines changed: 5 additions & 4 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: string;
32+
number: number;
3333
statement: string;
3434
contentFormat: ContentFormat;
3535
answerType: AnswerType;
@@ -57,7 +57,7 @@ function uid() {
5757
function emptyProblem(n: number | string): ProblemEntry {
5858
return {
5959
id: uid(),
60-
number: String(n),
60+
number: typeof n === "number" ? n : Number(n) || 1,
6161
statement: "",
6262
contentFormat: "LATEX",
6363
answerType: "INTEGER",
@@ -532,9 +532,10 @@ export default function CreateSetPage() {
532532
<div className="problem-number" style={{ display: "flex", alignItems: "center", gap: 8 }}>
533533
<GripVertical size={14} className="grip-icon" />
534534
<input
535-
type="text"
535+
type="number"
536+
min={1}
536537
value={p.number}
537-
onChange={(e) => updateProblem(p.id, "number", e.target.value)}
538+
onChange={(e) => updateProblem(p.id, "number", Number(e.target.value))}
538539
style={{
539540
width: 60,
540541
padding: "4px 8px",

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

Lines changed: 5 additions & 4 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: string;
27+
number: number;
2828
statement: string;
2929
contentFormat: "LATEX" | "HTML";
3030
answerKey: string;
@@ -84,7 +84,7 @@ function toggleTagInCsv(csv: string, tag: string): string {
8484
function newProblem(number: number | string) {
8585
return {
8686
id: `new-${Math.random().toString(36).slice(2, 10)}`,
87-
number: String(number),
87+
number: typeof number === "number" ? number : Number(number) || 1,
8888
statement: "",
8989
contentFormat: "LATEX" as const,
9090
answerKey: "",
@@ -572,9 +572,10 @@ export function SetEditForm({ set }: { set: SetData }) {
572572
)}
573573
<div className="problem-number" style={{ display: "flex", alignItems: "center", gap: 8 }}>
574574
<input
575-
type="text"
575+
type="number"
576+
min={1}
576577
value={problem.number}
577-
onChange={(e) => updateProblem(problem.id, "number", e.target.value)}
578+
onChange={(e) => updateProblem(problem.id, "number", Number(e.target.value))}
578579
onClick={(e) => e.stopPropagation()}
579580
style={{
580581
width: 60,

app/admin/sets/page.tsx

Lines changed: 51 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,19 @@ import { prisma } from "@/lib/db";
66
import { authOptions } from "@/lib/auth";
77
import { hasPermission } from "@/lib/permissions";
88
import { statusLabel, statusColor } from "@/lib/visibility";
9+
import { compareProblemSetRecords } from "@/lib/problem-set-order";
910
import { DeleteSetButton } from "./delete-set-button";
1011

1112
export const dynamic = "force-dynamic";
1213

1314
type AdminSetsSearchParams = Promise<{
1415
page?: string;
1516
q?: string;
17+
status?: string;
1618
}>;
1719

20+
type SetStatusFilter = "all" | "PUBLISHED" | "DRAFT" | "ARCHIVED";
21+
1822
export default async function AdminSetsPage({
1923
searchParams,
2024
}: {
@@ -27,17 +31,27 @@ export default async function AdminSetsPage({
2731
const params = (await searchParams) ?? {};
2832
const query = params.q?.trim() ?? "";
2933
const normalizedQuery = query.toLowerCase();
34+
const statusFilter: SetStatusFilter =
35+
params.status === "published"
36+
? "PUBLISHED"
37+
: params.status === "draft"
38+
? "DRAFT"
39+
: params.status === "archived"
40+
? "ARCHIVED"
41+
: "all";
3042
const currentPage = Math.max(1, Number(params.page ?? "1") || 1);
3143
const pageSize = 25;
3244

3345
const sets = await prisma.problemSet.findMany({
34-
orderBy: { order: "asc" },
46+
orderBy: { createdAt: "asc" },
3547
include: {
3648
_count: { select: { problems: true } },
3749
},
3850
});
3951

40-
const visibleSets = sets.filter((set) => {
52+
const orderedSets = [...sets].sort(compareProblemSetRecords);
53+
const visibleSets = orderedSets.filter((set) => {
54+
if (statusFilter !== "all" && set.status !== statusFilter) return false;
4155
if (!normalizedQuery) return true;
4256
return [set.title, set.slug, String(set.order), ...set.topicTags, set.status]
4357
.join(" ")
@@ -48,14 +62,24 @@ export default async function AdminSetsPage({
4862
const safePage = Math.min(currentPage, totalPages);
4963
const paginatedSets = visibleSets.slice((safePage - 1) * pageSize, safePage * pageSize);
5064

51-
function setsHref(page: number) {
65+
function setsHref(next: { page?: number; status?: SetStatusFilter } = {}) {
5266
const urlParams = new URLSearchParams();
67+
const nextPage = next.page ?? safePage;
68+
const nextStatus = next.status ?? statusFilter;
5369
if (query) urlParams.set("q", query);
54-
if (page > 1) urlParams.set("page", String(page));
70+
if (nextStatus !== "all") urlParams.set("status", nextStatus.toLowerCase());
71+
if (nextPage > 1) urlParams.set("page", String(nextPage));
5572
const suffix = urlParams.toString();
5673
return suffix ? `/admin/sets?${suffix}` : "/admin/sets";
5774
}
5875

76+
const statusOptions: Array<{ label: string; value: SetStatusFilter }> = [
77+
{ label: "All", value: "all" },
78+
{ label: "Published", value: "PUBLISHED" },
79+
{ label: "Draft", value: "DRAFT" },
80+
{ label: "Archived", value: "ARCHIVED" },
81+
];
82+
5983
return (
6084
<main className="single-page">
6185
<div className="background-layers" aria-hidden="true">
@@ -90,6 +114,9 @@ export default async function AdminSetsPage({
90114
name="q"
91115
placeholder="Search sets by title, slug, order, tag, or status"
92116
/>
117+
{statusFilter !== "all" ? (
118+
<input name="status" type="hidden" value={statusFilter.toLowerCase()} />
119+
) : null}
93120
<button className="secondary-action compact" type="submit">
94121
Search
95122
</button>
@@ -100,6 +127,21 @@ export default async function AdminSetsPage({
100127
) : null}
101128
</form>
102129

130+
<section className="admin-set-filter-bar" aria-label="Problem set status filters">
131+
<span className="leaderboard-control-label">Status</span>
132+
<div className="segmented-control">
133+
{statusOptions.map((option) => (
134+
<Link
135+
className={`segmented-button${statusFilter === option.value ? " active" : ""}`}
136+
href={setsHref({ page: 1, status: option.value })}
137+
key={option.value}
138+
>
139+
{option.label}
140+
</Link>
141+
))}
142+
</div>
143+
</section>
144+
103145
{visibleSets.length === 0 ? (
104146
<section className="panel empty-state">
105147
<FileJson size={42} />
@@ -120,7 +162,9 @@ export default async function AdminSetsPage({
120162
<section className="panel table-panel">
121163
<div className="panel-header">
122164
<div>
123-
<p className="eyebrow">All sets</p>
165+
<p className="eyebrow">
166+
{statusFilter === "all" ? "All sets" : `${statusFilter.toLowerCase()} sets`}
167+
</p>
124168
<h2>
125169
{visibleSets.length} problem set{visibleSets.length !== 1 ? "s" : ""}
126170
</h2>
@@ -188,7 +232,7 @@ export default async function AdminSetsPage({
188232
<div className="pagination-row">
189233
<Link
190234
className="secondary-action compact"
191-
href={setsHref(Math.max(1, safePage - 1))}
235+
href={setsHref({ page: Math.max(1, safePage - 1) })}
192236
>
193237
Previous
194238
</Link>
@@ -197,7 +241,7 @@ export default async function AdminSetsPage({
197241
</span>
198242
<Link
199243
className="secondary-action compact"
200-
href={setsHref(Math.min(totalPages, safePage + 1))}
244+
href={setsHref({ page: Math.min(totalPages, safePage + 1) })}
201245
>
202246
Next
203247
</Link>

app/api/admin/create-set/route.ts

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { authOptions } from "@/lib/auth";
55
import { normalizeTagList } from "@/lib/problem-tags";
66
import { storeUploadedPdf, type UploadedPdfPayload } from "@/lib/uploaded-pdf";
77
import { recordAuditLog } from "@/lib/audit";
8+
import { nextProblemSetOrder } from "@/lib/problem-set-order";
89
import {
910
assertUniqueProblemNumbers,
1011
createProblemSetAuthoringSchema,
@@ -67,12 +68,8 @@ export async function POST(req: Request) {
6768
if (!finalOrder) {
6869
const existingSets = await prisma.problemSet.findMany({
6970
select: { order: true },
70-
orderBy: { order: "desc" },
71-
take: 1,
7271
});
73-
const maxOrder = existingSets[0]?.order ?? "0";
74-
const parsed = parseInt(maxOrder, 10);
75-
finalOrder = String((Number.isFinite(parsed) ? parsed : 0) + 1);
72+
finalOrder = nextProblemSetOrder(existingSets.map((set) => set.order));
7673
}
7774

7875
let problemFileId: string | null = null;

app/api/submit/report/route.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,13 @@ export async function POST(req: Request) {
2424

2525
let problemId = null;
2626
if (problemNumber) {
27+
const parsedProblemNumber = Number(problemNumber);
28+
if (!Number.isInteger(parsedProblemNumber) || parsedProblemNumber <= 0) {
29+
return NextResponse.json({ error: "Invalid problem number" }, { status: 400 });
30+
}
31+
2732
const problem = await prisma.problem.findUnique({
28-
where: { problemSetId_number: { problemSetId, number: String(problemNumber) } },
33+
where: { problemSetId_number: { problemSetId, number: parsedProblemNumber } },
2934
});
3035
if (problem) {
3136
problemId = problem.id;

app/dashboard/page.tsx

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ import { profilePathFromEmail } from "@/lib/user-profile";
3030
import { computeBestAverageScore } from "@/lib/analytics";
3131
import { normalizeTagList } from "@/lib/problem-tags";
3232
import { hasPermission } from "@/lib/permissions";
33+
import { compareProblemSetRecords } from "@/lib/problem-set-order";
3334

3435
export const dynamic = "force-dynamic";
3536

@@ -64,7 +65,7 @@ export default async function DashboardPage() {
6465
},
6566
}),
6667
prisma.problemSet.findMany({
67-
orderBy: [{ order: "asc" }, { createdAt: "asc" }],
68+
orderBy: { createdAt: "asc" },
6869
include: {
6970
_count: { select: { problems: true, attempts: true, feedback: true } },
7071
},
@@ -102,8 +103,9 @@ export default async function DashboardPage() {
102103
redirect("/");
103104
}
104105

105-
const visibleSets =
106-
currentUser.role === "ADMIN" ? allSets : allSets.filter((set) => isVisibleToStudent(set));
106+
const visibleSets = (
107+
currentUser.role === "ADMIN" ? allSets : allSets.filter((set) => isVisibleToStudent(set))
108+
).sort(compareProblemSetRecords);
107109
const canManageContent = hasPermission(currentUser.role, "admin:content");
108110
const canViewStudents = hasPermission(currentUser.role, "admin:users");
109111
const canViewAnalytics = hasPermission(currentUser.role, "admin:analytics");

app/globals.css

Lines changed: 32 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -182,7 +182,7 @@ table {
182182
}
183183

184184
.page-frame {
185-
width: min(1180px, calc(100% - 48px));
185+
width: min(1200px, calc(100% - 48px));
186186
margin: 0 auto;
187187
padding: 28px 0;
188188
}
@@ -1592,6 +1592,17 @@ html:not(.light) .answer-cell input {
15921592
padding: 18px;
15931593
}
15941594

1595+
.create-set-shell,
1596+
.settings-shell,
1597+
.users-shell,
1598+
.profile-shell,
1599+
.leaderboard-shell,
1600+
.problem-hub-shell {
1601+
width: 100%;
1602+
padding-right: 18px;
1603+
padding-left: 18px;
1604+
}
1605+
15951606
.topbar {
15961607
align-items: flex-start;
15971608
flex-direction: column;
@@ -2990,7 +3001,8 @@ html.dark .confetti-triangles span {
29903001
/* ── Create Problem Set GUI ──────────────────────────── */
29913002

29923003
.create-set-shell {
2993-
max-width: 960px;
3004+
width: min(1200px, calc(100% - 48px));
3005+
max-width: none;
29943006
margin: 0 auto;
29953007
padding: 2rem 1.5rem 6rem;
29963008
}
@@ -3358,7 +3370,8 @@ html.dark .confetti-triangles span {
33583370
/* ── Settings page ───────────────────────────────────── */
33593371

33603372
.settings-shell {
3361-
max-width: 640px;
3373+
width: min(1200px, calc(100% - 48px));
3374+
max-width: none;
33623375
margin: 0 auto;
33633376
padding: 2rem 1.5rem 4rem;
33643377
}
@@ -3589,7 +3602,8 @@ html.dark .theme-choice-button.active {
35893602
/* ── Users list page ─────────────────────────────────── */
35903603

35913604
.users-shell {
3592-
max-width: 960px;
3605+
width: min(1200px, calc(100% - 48px));
3606+
max-width: none;
35933607
margin: 0 auto;
35943608
padding: 2rem 1.5rem 4rem;
35953609
}
@@ -3722,7 +3736,8 @@ html.dark .theme-choice-button.active {
37223736
/* ── User profile page ───────────────────────────────── */
37233737

37243738
.profile-shell {
3725-
max-width: 720px;
3739+
width: min(1200px, calc(100% - 48px));
3740+
max-width: none;
37263741
margin: 0 auto;
37273742
padding: 2rem 1.5rem 4rem;
37283743
}
@@ -4068,7 +4083,8 @@ html.dark .theme-choice-button.active {
40684083
/* ── Leaderboard page ────────────────────────────────── */
40694084

40704085
.leaderboard-shell {
4071-
max-width: 960px;
4086+
width: min(1200px, calc(100% - 48px));
4087+
max-width: none;
40724088
margin: 0 auto;
40734089
padding: 2rem 1.5rem 4rem;
40744090
}
@@ -4372,11 +4388,20 @@ html.dark .theme-choice-button.active {
43724388
/* ── Problem-set hub page ─────────────────────────────── */
43734389

43744390
.problem-hub-shell {
4375-
max-width: 1200px;
4391+
width: min(1200px, calc(100% - 48px));
4392+
max-width: none;
43764393
margin: 0 auto;
43774394
padding: 2rem 1.5rem 4rem;
43784395
}
43794396

4397+
.admin-set-filter-bar {
4398+
display: flex;
4399+
align-items: center;
4400+
gap: 0.85rem;
4401+
flex-wrap: wrap;
4402+
margin-bottom: 1rem;
4403+
}
4404+
43804405
.problem-hub-header {
43814406
display: flex;
43824407
align-items: flex-start;

app/practice/page.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -150,7 +150,7 @@ export default function PracticePage() {
150150
<span className="bg-spark bg-spark-one" />
151151
</div>
152152

153-
<div className="page-frame" style={{ maxWidth: 800, margin: "0 auto" }}>
153+
<div className="page-frame">
154154
<header className="topbar standalone">
155155
<div>
156156
<p className="eyebrow">Training</p>

0 commit comments

Comments
 (0)