Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions src/app/api/v1/sets/[id]/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,13 @@ import { zProblemSet } from "common/validation/problem_set_validation";
import { canManageProblemSets } from "server/authorization";
import { getSession } from "server/sessions";
import { updateProblemSet } from "server/logic/problem_sets/update_problem_set";
import { NextContext } from "types/nextjs";
import { NestedLookupDTO } from "common/types";
import { lookupSetFromSlugOrId } from "./utils";

type RouteParams = {
id: string;
};

export async function PUT(request: NextRequest) {
const session = await getSession(request);
Expand All @@ -12,10 +19,32 @@ export async function PUT(request: NextRequest) {

const data = await request.json();
const parsed = zProblemSet.safeParse(data);

if (parsed.success) {
const contest = await updateProblemSet(parsed.data);
return NextResponse.json(contest);
} else {
return NextResponse.json({ error: parsed.error.format() }, { status: 400 });
}
}

export async function GET(request: NextRequest, context: NextContext<RouteParams>) {
const session = await getSession(request);
if (!canManageProblemSets(session)) {
return NextResponse.json({}, { status: 403 });
}

// Accept any of slug, uuid, or hurado id
const slug = context.params.id;
const first = await lookupSetFromSlugOrId(slug);
if (first == null) {
return NextResponse.json(null, { status: 404 });
}

const dto: NestedLookupDTO = {
id: first.id,
slug: first.slug,
title: first.title,
};
return NextResponse.json(dto);
}
28 changes: 28 additions & 0 deletions src/app/api/v1/sets/[id]/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { checkUUIDv4, huradoIDToUUID } from "common/utils/uuid";
import { db } from "db";

export async function lookupSetFromSlugOrId(slug: string) {
const uuid = huradoIDToUUID(slug) ?? checkUUIDv4(slug);

const lookups = await db
.selectFrom("problem_sets")
.where((eb) => {
if (uuid != null) {
return eb.or([eb("id", "=", uuid), eb("slug", "=", slug)]);
} else {
return eb("slug", "=", slug);
}
})
.select(["id", "slug", "title"])
.execute();

// Sort the results. Matching UUID trumps matching slug. Best result goes to lowest index.
lookups.sort((a, b) => {
if (a.id === uuid) {
return b.id === uuid ? 0 : -1;
}
return b.id === uuid ? 1 : 0;
});

return lookups[0];
}
8 changes: 4 additions & 4 deletions src/app/api/v1/tasks/[id]/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,15 @@ import { updateEditorTask } from "server/logic/tasks/update_editor_task";
import { getSession } from "server/sessions";
import { NextContext } from "types/nextjs";
import { TaskLookupDTO } from "common/types";
import { lookupFromSlugOrId } from "./utils";
import { lookupTaskFromSlugOrId } from "./utils";

type RouteParams = {
id: string;
};

export async function PUT(request: NextRequest) {
const session = await getSession(request);
if (!canManageTasks(session, request)) {
if (!canManageTasks(session)) {
return NextResponse.json({}, { status: 403 });
}

Expand All @@ -29,13 +29,13 @@ export async function PUT(request: NextRequest) {

export async function GET(request: NextRequest, context: NextContext<RouteParams>) {
const session = await getSession(request);
if (!canManageTasks(session, request)) {
if (!canManageTasks(session)) {
return NextResponse.json({}, { status: 403 });
}

// Accept any of slug, uuid, or hurado id
const slug = context.params.id;
const first = await lookupFromSlugOrId(slug);
const first = await lookupTaskFromSlugOrId(slug);
if (first == null) {
return NextResponse.json(null, { status: 404 });
}
Expand Down
2 changes: 1 addition & 1 deletion src/app/api/v1/tasks/[id]/utils.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { checkUUIDv4, huradoIDToUUID } from "common/utils/uuid";
import { db } from "db";

export async function lookupFromSlugOrId(slug: string) {
export async function lookupTaskFromSlugOrId(slug: string) {
const uuid = huradoIDToUUID(slug) ?? checkUUIDv4(slug);

const lookups = await db
Expand Down
36 changes: 31 additions & 5 deletions src/app/sets/[slug]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { Metadata } from "next";
import { notFound } from "next/navigation";
import { cache } from "react";
// eslint-disable-next-line @typescript-eslint/no-unused-vars -- pre-existing error before eslint inclusion
import { ProblemSetViewerDTO, TaskScoredSummaryDTO, TaskSummaryDTO } from "common/types";
import { ProblemSetSummaryDTO, ProblemSetViewerDTO, TaskScoredSummaryDTO } from "common/types";
import { db } from "db";
import { DefaultLayout } from "client/components/layouts/default_layout";
import { ProblemSetViewer } from "client/components/problem_set_viewer/problem_set_viewer";
Expand All @@ -26,6 +26,7 @@ async function getProblemSetData(

const dbTasks = await trx
.selectFrom("tasks")
.where("tasks.is_public", "=", true)
.innerJoin("problem_set_tasks", "tasks.id", "problem_set_tasks.task_id")
.orderBy(["problem_set_tasks.order", "tasks.title"])
.where("problem_set_tasks.set_id", "=", set.id)
Expand All @@ -40,33 +41,58 @@ async function getProblemSetData(
"tasks.slug",
"tasks.title",
"tasks.description",
"problem_set_tasks.order",
"overall_verdicts.score_overall",
"overall_verdicts.score_max",
])
.execute();

const dbNesteds = await trx
.selectFrom("problem_sets")
.where("problem_sets.is_public", "=", true)
.innerJoin("problem_set_nesteds", "problem_sets.id", "problem_set_nesteds.child_id")
.orderBy(["problem_set_nesteds.order", "problem_sets.title"])
.where("problem_set_nesteds.parent_id", "=", set.id)
.select([
"problem_sets.id",
"problem_sets.slug",
"problem_sets.title",
"problem_sets.description",
"problem_set_nesteds.order",
])
.execute();

const tasks: TaskScoredSummaryDTO[] = dbTasks.map((t) => ({
id: t.id,
slug: t.slug,
title: t.title,
description: t.description,
order: t.order,
score_overall: t.score_overall,
score_max: t.score_max,
}));

const nesteds: ProblemSetSummaryDTO[] = dbNesteds.map((s) => ({
id: s.id,
slug: s.slug,
title: s.title,
description: s.description,
order: s.order,
}));

return {
id: set.id,
slug: set.slug,
title: set.title,
description: set.description,
is_public: set.is_public,
order: set.order,
tasks: tasks,
nesteds: nesteds,
} satisfies ProblemSetViewerDTO;
});
}

const getCachedProblemSetData = cache(getProblemSetData);
export const getCachedProblemSetData = cache(getProblemSetData);

type ProblemSetPageProps = {
params: {
Expand All @@ -88,7 +114,7 @@ export async function generateMetadata(props: ProblemSetPageProps): Promise<Meta
};
}

async function Page(props: ProblemSetPageProps) {
export async function ProblemSetPage(props: ProblemSetPageProps) {
const session = await getSession();
const set = await getCachedProblemSetData(props.params.slug, session?.user?.id ?? null);

Expand All @@ -104,4 +130,4 @@ async function Page(props: ProblemSetPageProps) {
);
}

export default Page;
export default ProblemSetPage;
42 changes: 2 additions & 40 deletions src/app/sets/page.tsx
Original file line number Diff line number Diff line change
@@ -1,45 +1,7 @@
import { Metadata } from "next";
import { db } from "db";
import { DefaultLayout } from "client/components/layouts/default_layout";
import { ProblemSetSummaryDTO } from "common/types/problem_sets";
import { ProblemSetCard } from "client/components/cards";
import { EmptyNoticePage } from "client/components/empty_notice";

async function getProblemSetsData(): Promise<ProblemSetSummaryDTO[]> {
const sets = await db
.selectFrom("problem_sets")
.select(["id", "title", "slug", "description", "order"])
.where("is_public", "=", true)
.orderBy("order", "asc")
.execute();

return sets;
}

export const metadata: Metadata = {
title: "Problem Sets",
};
import { ProblemSetPage } from "./[slug]/page";

export async function ProblemSetListPage() {
const sets = await getProblemSetsData();

if (sets.length == 0) {
return (
<DefaultLayout>
<EmptyNoticePage />
</DefaultLayout>
);
}

return (
<DefaultLayout>
<div className="flex flex-col items-center gap-4">
{sets.map((set) => (
<ProblemSetCard key={set.slug} set={set} />
))}
</div>
</DefaultLayout>
);
return <ProblemSetPage params={{ slug: "root" }} />;
}

export default ProblemSetListPage;
65 changes: 61 additions & 4 deletions src/client/components/cards/common_card.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,16 @@
import Link from "next/link";
import classNames from "classnames";
// eslint-disable-next-line @typescript-eslint/no-unused-vars -- pre-existing error before eslint inclusion
import { ContestSummaryDTO, ProblemSetSummaryDTO, TaskScoredSummaryDTO } from "common/types";
import {
ContestSummaryDTO,
ProblemSetSummaryDTO,
ProblemSetViewerDTO,
TaskScoredSummaryDTO,
} from "common/types";
import { getPath, Path } from "client/paths";

const TRAIL_KEY = "problem_set_trail";

type CommonCardProps = {
url: string;
title: string;
Expand All @@ -25,15 +32,38 @@ export function CommonCard({ url, title, description }: CommonCardProps) {
}

type ProblemSetCardProps = {
source: ProblemSetViewerDTO;
set: ProblemSetSummaryDTO;
};

export function ProblemSetCard({ set }: ProblemSetCardProps) {
export function ProblemSetCard({ source, set }: ProblemSetCardProps) {
const url = getPath({ kind: Path.ProblemSetView, slug: set.slug });

const handleCardClick = () => {
const rawTrail = sessionStorage.getItem(TRAIL_KEY);
let trail = [];

try {
if (rawTrail) {
trail = JSON.parse(rawTrail);
}
} catch {
trail = [];
}

trail.push({
path: getPath({ kind: Path.ProblemSetView, slug: source.slug }),
name: source.title,
});

sessionStorage.setItem(TRAIL_KEY, JSON.stringify(trail));
};

return (
<Link
key={set.slug}
href={url}
onClick={handleCardClick}
className="w-[96rem] max-w-full p-4 border border-gray-800 rounded-2xl hover:bg-gray-150"
>
<h2 className="text-2xl mb-1">{set.title}</h2>
Expand Down Expand Up @@ -65,12 +95,33 @@ export function ContestCard({ contest }: ContestCardProps) {
}

type TaskCardProps = {
source: ProblemSetViewerDTO;
task: TaskScoredSummaryDTO;
};

export function TaskCard({ task }: TaskCardProps) {
export function TaskCard({ source, task }: TaskCardProps) {
const url = getPath({ kind: Path.TaskView, slug: task.slug });

const handleCardClick = () => {
const rawTrail = sessionStorage.getItem(TRAIL_KEY);
let trail = [];

try {
if (rawTrail) {
trail = JSON.parse(rawTrail);
}
} catch {
trail = [];
}

trail.push({
path: getPath({ kind: Path.ProblemSetView, slug: source.slug }),
name: source.title,
});

sessionStorage.setItem(TRAIL_KEY, JSON.stringify(trail));
};

let top_class =
"p-[1rem] border-t border-l border-r border-black rounded-tl-2xl rounded-tr-2xl group-hover:bg-gray-150";
const bottom_class =
Expand Down Expand Up @@ -100,8 +151,14 @@ export function TaskCard({ task }: TaskCardProps) {
top_class = classNames(top_class, "bg-green-100 group-hover:bg-green-200");
}
}

return (
<Link className="w-[96rem] max-w-full group" key={task.slug} href={url}>
<Link
className="w-[96rem] max-w-full group"
key={task.slug}
href={url}
onClick={handleCardClick}
>
<div className={top_class}>
<h2 className="text-2xl mb-1">{task.title}</h2>
<p className="font-light">
Expand Down
6 changes: 4 additions & 2 deletions src/client/components/common_editor/common_editor_parts.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -62,14 +62,16 @@ export type SaveResult<T> = SaveResultSuccess<T> | SaveResultFailure;

type CommonEditorFooterProps<T> = {
initial: T;
origObject: T;
object: T;
setObject(object: T): void;
saveObject(object: T): Promise<SaveResult<T>>;
saveObject(origObject: T, object: T): Promise<SaveResult<T>>;
};

// eslint-disable-next-line @typescript-eslint/ban-types -- pre-existing error before eslint inclusion
export const CommonEditorFooter = <T extends {}>({
initial,
origObject,
object,
setObject,
saveObject,
Expand All @@ -79,7 +81,7 @@ export const CommonEditorFooter = <T extends {}>({
const handleSave = useCallback(async () => {
setSaving(true);
try {
const result = await saveObject(object);
const result = await saveObject(origObject, object);
if (result.success) {
setObject(result.value);
toast("Great success!", { type: "success" });
Expand Down
Loading
Loading