-
Notifications
You must be signed in to change notification settings - Fork 159
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #100 from jashandeep31/dev-1
Contest and user profile pages v2.1 (dev)
- Loading branch information
Showing
43 changed files
with
1,672 additions
and
357 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 }); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 }); | ||
} |
Oops, something went wrong.