Skip to content

Commit

Permalink
Merge pull request #100 from jashandeep31/dev-1
Browse files Browse the repository at this point in the history
Contest and user profile pages  v2.1 (dev)
  • Loading branch information
devsargam authored Sep 20, 2024
2 parents 5cf91ea + a4211ad commit 0ba96d2
Show file tree
Hide file tree
Showing 43 changed files with 1,672 additions and 357 deletions.
34 changes: 34 additions & 0 deletions apps/web/app/admin/contests/[id]/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import React from "react";
import { db } from "../../../db";
import CreateContestForm from "../../../../components/CreateContestForm";

const page = async ({ params }: { params: { id: string } }) => {
const contest = await db.contest.findUnique({
where: {
id: params.id,
},
include: {
problems: true,
},
});
const problems = await db.problem.findMany();
if (!contest) {
return <h1 className="container md:mt-12 mt-6">Not Found</h1>;
}
return (
<div className="container md:mt-12 mt-6">
<h1 className="lg:text-2xl md:text-xl text-lg font-bold text-muted-foreground">
{contest.title}
</h1>

<div className="mt-12">
<CreateContestForm
intitalContest={contest}
intitalProblems={problems}
/>
</div>
</div>
);
};

export default page;
20 changes: 20 additions & 0 deletions apps/web/app/admin/contests/create/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import React from "react";
import CreateContestForm from "../../../../components/CreateContestForm";
import { getProblems } from "../../../db/problem";

const page = async () => {
const problems = await getProblems();
return (
<div className="container md:mt-12 mt-6">
<h1 className="lg:text-2xl font-bold md:text-xl text-muted-foreground text-lg">
Create Contest
</h1>

<div className="md:mt-12 mt-6">
<CreateContestForm intitalProblems={problems} />
</div>
</div>
);
};

export default page;
54 changes: 54 additions & 0 deletions apps/web/app/admin/contests/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import React from "react";
import { db } from "../../db";
import Link from "next/link";
import { buttonVariants } from "@repo/ui/button";

async function page() {
const contests = await db.contest.findMany({
orderBy: {
createdAt: "desc",
},
});
return (
<div className="container md:mt-12 mt-6">
<h1 className="lg:text-3xl md:text-2xl text-lg font-bold text-muted-foreground">
On Going Contests
</h1>
<div className="mt-6 space-y-4">
{contests.map((contest) => (
<div key={contest.id} className="border p-2 rounded-md">
<h2 className="text-lg font-bold">
{contest.title}{" "}
<span className="text-sm text-muted-foreground">
({contest.hidden ? "Hidden" : "Visible"})
</span>
</h2>
<p className="text-sm text-muted-foreground p-2">
{contest.description}
</p>
<div className="mt-2 flex justify-end">
<Link
href={`/admin/contests/${contest.id}`}
className={buttonVariants({ variant: "secondary" })}
>
Edit
</Link>
</div>
<div className="text-sm text-muted-foreground flex justify-between mt-3">
<p>
Started At: {contest.startTime.toLocaleDateString()}{" "}
{contest.startTime.toLocaleTimeString()}
</p>
<p>
End At: {contest.endTime.toLocaleDateString()}{" "}
{contest.endTime.toLocaleTimeString()}
</p>
</div>
</div>
))}
</div>
</div>
);
}

export default page;
14 changes: 14 additions & 0 deletions apps/web/app/admin/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { getServerSession } from "next-auth";
import React from "react";
import { authOptions } from "../lib/auth";
import { redirect } from "next/navigation";

const layout = async ({ children }: { children: React.ReactNode }) => {
const session = await getServerSession(authOptions);
if (!session || !session.user || session.user.role !== "ADMIN") {
redirect("/");
}
return <div>{children}</div>;
};

export default layout;
52 changes: 52 additions & 0 deletions apps/web/app/admin/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { ArrowDownRight } from "lucide-react";
import Link from "next/link";
import React from "react";

const page = () => {
const links: { name: string; items: { name: string; href: string }[] }[] = [
{
name: "Contests",
items: [
{
name: "Manage Contests",
href: "/admin/contests",
},
{
name: "Create Contest",
href: "/admin/contests/create",
},
],
},
];
return (
<div className="container md:mt-12 mt-6">
<h1 className="lg:text-3xl md:text-2xl text-lg font-bold text-muted-foreground">
Admin Controls
</h1>

<div className="text-lg gap-3 flex flex-col mt-12">
{links.map((link, index) => (
<div key={index}>
<h2 className="text-lg font-bold text-muted-foreground">
{link.name}
</h2>
<div className="flex flex-col">
{link.items.map((item, j) => (
<Link
key={`${index}-${j}`}
href={item.href}
className="inline-flex gap-1 ml-2 items-center text-muted-foreground hover:text-foreground duration-300 underline"
>
{item.name}
<ArrowDownRight size={16} />
</Link>
))}
</div>
</div>
))}
</div>
</div>
);
};

export default page;
121 changes: 121 additions & 0 deletions apps/web/app/api/contest/[id]/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
import { getServerSession } from "next-auth";
import { NextRequest, NextResponse } from "next/server";
import { authOptions } from "../../../lib/auth";
import * as z from "zod";
import { db } from "../../../db";

export async function POST(
req: NextRequest,
{ params }: { params: { id: string } }
) {
const session = await getServerSession(authOptions);
if (!session || session.user.role !== "ADMIN") {
return NextResponse.json(
{
message: "You must be an admin to create contests",
},
{
status: 401,
}
);
}
const body = await req.json();
const id = params.id;

const formSchema = z.object({
title: z
.string({ required_error: "Title is required" })
.min(2, "Title must be at least 2 characters long")
.max(100, "Title cannot exceed 100 characters"),
description: z
.string({ required_error: "Description is required" })
.min(10, "Description must be at least 10 characters long")
.max(500, "Description cannot exceed 500 characters"),
startTime: z.coerce.date(),
endTime: z.coerce.date(),
hidden: z.boolean().default(false),
problems: z.array(z.string()),
});

const form = formSchema.safeParse(body);
if (!form.success) {
console.log(form.error);
return NextResponse.json(
{
message: "Invalid input",
errors: form.error,
},
{
status: 400,
}
);
}
const validatedData = form.data;
const problems = await db.problem.findMany({
where: {
id: {
in: validatedData.problems,
},
},
});

if (problems.length !== validatedData.problems.length) {
return NextResponse.json(
{
message: "Invalid problems",
},
{
status: 400,
}
);
}
const contest = await db.$transaction(async (tx) => {
const contest = await tx.contest.update({
where: {
id,
},
data: {
title: validatedData.title,
description: validatedData.description,
startTime: validatedData.startTime,
endTime: validatedData.endTime,
hidden: validatedData.hidden,
},
});
const preContestProblems = await tx.contestProblem.findMany({
where: {
contestId: id,
},
});
const preContestProblemIds = preContestProblems.map(
(problem) => problem.problemId
);
const newProblems = problems.filter(
(problem) => !preContestProblemIds.includes(problem.id)
);
const removedProblems = preContestProblems.filter(
(problem) => !validatedData.problems.includes(problem.problemId)
);

await tx.contestProblem.deleteMany({
where: {
contestId: id,
problemId: {
in: removedProblems.map((problem) => problem.problemId),
},
},
});
await tx.contestProblem.createMany({
data: [
...newProblems.map((problem, index) => ({
problemId: problem.id,
contestId: id,
index,
})),
],
});
return contest;
});

return NextResponse.json({ contest }, { status: 201 });
}
92 changes: 92 additions & 0 deletions apps/web/app/api/contest/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import { getServerSession } from "next-auth";
import { NextRequest, NextResponse } from "next/server";
import { authOptions } from "../../lib/auth";
import * as z from "zod";
import { db } from "../../db";

export async function POST(req: NextRequest) {
const session = await getServerSession(authOptions);
if (!session || session.user.role !== "ADMIN") {
return NextResponse.json(
{
message: "You must be an admin to create contests",
},
{
status: 401,
}
);
}

const body = await req.json();
const formSchema = z.object({
title: z
.string({ required_error: "Title is required" })
.min(2, "Title must be at least 2 characters long")
.max(100, "Title cannot exceed 100 characters"),
description: z
.string({ required_error: "Description is required" })
.min(10, "Description must be at least 10 characters long")
.max(500, "Description cannot exceed 500 characters"),
startTime: z.coerce.date(),
endTime: z.coerce.date(),
hidden: z.boolean().default(false),
problems: z.array(z.string()),
});

const form = formSchema.safeParse(body);
if (!form.success) {
return NextResponse.json(
{
message: "Invalid input",
errors: form.error,
},
{
status: 400,
}
);
}
const validatedData = form.data;
const problems = await db.problem.findMany({
where: {
id: {
in: validatedData.problems,
},
},
});

if (problems.length !== validatedData.problems.length) {
return NextResponse.json(
{
message: "Invalid problems",
},
{
status: 400,
}
);
}

const contest = await db.$transaction(async (tx) => {
const contest = await tx.contest.create({
data: {
title: validatedData.title,
description: validatedData.description,
startTime: validatedData.startTime,
endTime: validatedData.endTime,
hidden: validatedData.hidden,
},
});
await tx.contestProblem.createMany({
data: [
...problems.map((problem, index) => ({
problemId: problem.id,
contestId: contest.id,
index,
})),
],
});

return contest;
});

return NextResponse.json({ contest }, { status: 201 });
}
Loading

0 comments on commit 0ba96d2

Please sign in to comment.