Skip to content

Commit c171c83

Browse files
committed
admin ui with interactions
1 parent e2a343a commit c171c83

File tree

16 files changed

+1459
-589
lines changed

16 files changed

+1459
-589
lines changed

package-lock.json

Lines changed: 1174 additions & 546 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@
3636
"next-auth": "5.0.0-beta.25",
3737
"react": "^18.3.1",
3838
"react-dom": "^18.3.1",
39-
"react-icons": "^5.4.0",
39+
"react-icons": "^5.5.0",
4040
"server-only": "^0.0.1",
4141
"superjson": "^2.2.1",
4242
"tailwind-merge": "^2.6.0",

prisma/schema.prisma

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -73,11 +73,11 @@ model Week {
7373
}
7474

7575
model Problem {
76-
id String @id @default(cuid())
76+
id String @id @default(cuid())
7777
name String
7878
level Difficulty
7979
leetcodeUrl String
8080
weekId String
81-
week Week @relation(fields: [weekId], references: [id], onDelete: Cascade)
82-
solvedBy User[] @relation("SolvedProblems")
81+
week Week @relation(fields: [weekId], references: [id], onDelete: Cascade)
82+
solvedBy User[] @relation("SolvedProblems")
8383
}

src/app/(pages)/admin/page.tsx

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
"use client"
2+
import Title from "~/app/_components/title";
3+
import AdminViewBlock from "~/app/_components/admin/adminViewBlock";
4+
import { api } from "~/trpc/react";
5+
import { useState, useEffect } from "react";
6+
import type { Prisma, Week } from "@prisma/client";
7+
8+
type ProblemWithSolvedBy = Prisma.ProblemGetPayload<{
9+
include: {solvedBy: true }
10+
}>
11+
12+
const Admin = () => {
13+
14+
const [weeks, setWeeks] = useState<Week[]>([]);
15+
const [problems, setProblems] = useState<ProblemWithSolvedBy[]>([]);
16+
17+
const fetchWeeks = api.week.getWeeks.useQuery();
18+
19+
const fetchProblems = api.problem.getAll.useQuery();
20+
21+
useEffect(() => {
22+
if (fetchWeeks.data) {
23+
setWeeks(fetchWeeks.data);
24+
}
25+
}, [fetchWeeks.data]);
26+
27+
useEffect(() => {
28+
if (fetchProblems.data) {
29+
setProblems(fetchProblems.data);
30+
}
31+
}, [fetchProblems.data]);
32+
33+
return (
34+
<div className="flex flex-col gap-12">
35+
<Title label="Problem Setup"/>
36+
{ weeks.map((w) => <AdminViewBlock key={w.id} problems={problems} week={w}/> ) }
37+
</div>
38+
)
39+
}
40+
41+
export default Admin;

src/app/(pages)/leaderboard/page.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ const Leaderboard = () => {
2626

2727
// Fetch weeks for the slider
2828
const { data: weeksData, isLoading: weeksLoading } =
29-
api.week.getWeeks.useQuery();
29+
api.week.getWeeksPublic.useQuery();
3030

3131
// Fetch leaderboard data
3232
const {

src/app/(pages)/week/[weekId]/page.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ const WeekPage = ({ params }: { params: Promise<{ weekId: string }> }) => {
66

77
return (
88
<div>
9-
<WeekInfo id={parseInt(weekId)} />
9+
<WeekInfo id={weekId} />
1010
</div>
1111
);
1212
};

src/app/(pages)/weekly-problems/page.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { backgroundColors, textColors } from "utils/colors";
55
import { api } from "~/trpc/server";
66

77
const WeeklyProblems = async() => {
8-
const weeks = await api.week.getWeeks();
8+
const weeks = await api.week.getWeeksPublic();
99

1010
return (
1111
<div>
@@ -15,12 +15,12 @@ const WeeklyProblems = async() => {
1515
<div className="grid grid-cols-3 text-main px-10 gap-10">
1616
{weeks.map((week, key) => (
1717
<div key={key}>
18-
<ProblemCard key={key} title={week.title} description={week.description} number={week.number} isBlocked={week.isBlocked} bgColor={backgroundColors.get(week.color) ?? "bg-white"} textColor={textColors.get(week.color) ?? "text-black"} />
18+
<ProblemCard key={key} title={week.title} description={week.description} id={week.id} isBlocked={week.isBlocked} bgColor={backgroundColors.get(week.color) ?? "bg-white"} textColor={textColors.get(week.color) ?? "text-black"} />
1919
</div>
2020
))}
2121
</div>
2222
</div>
2323
)
2424
}
2525

26-
export default WeeklyProblems;
26+
export default WeeklyProblems;
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
"use client";
2+
import { api } from "~/trpc/react"
3+
import { BsEyeFill, BsEyeSlashFill, BsCartPlusFill, BsPencilSquare } from "react-icons/bs";
4+
import Subtitle from "~/app/_components/subtitle";
5+
import AdminViewRow from "~/app/_components/admin/adminViewRow";
6+
import type { Prisma, Week } from "@prisma/client";
7+
8+
type ProblemWithSolvedBy = Prisma.ProblemGetPayload<{
9+
include: {solvedBy: true }
10+
}>
11+
12+
interface AdminViewBlockProps {
13+
week: Week,
14+
problems: ProblemWithSolvedBy[],
15+
};
16+
17+
const AdminViewBlock = ({week, problems}: AdminViewBlockProps) => {
18+
const utils = api.useUtils();
19+
const changeWeekTitle = api.week.changeWeekTitle.useMutation({
20+
onSuccess: async () => {
21+
await utils.week.invalidate();
22+
},
23+
onError: () => {
24+
alert('Error: Could not update week title.');
25+
}
26+
});
27+
28+
const handleWeekTitleChange = () => {
29+
const nt = prompt('Enter new title:');
30+
if(!nt)
31+
{
32+
return;
33+
}
34+
changeWeekTitle.mutate({id: week.id || "", title: nt});
35+
};
36+
37+
const setWeekStatus = api.week.setWeekHidden.useMutation({
38+
onSuccess: async () => {
39+
await utils.week.invalidate();
40+
},
41+
onError: () => {
42+
alert(`Error: Could not change visibility.`);
43+
},
44+
});
45+
46+
const createFromSlug = api.problem.createFromSlug.useMutation({
47+
onSuccess: async (data) => {
48+
if(data) {
49+
await utils.problem.invalidate();
50+
}
51+
else {
52+
alert('Problem does not exist or could not be found.');
53+
}
54+
},
55+
onError: () => {
56+
alert(`Error: Could not create problem.`);
57+
},
58+
});
59+
60+
const handleSetWeekStatus = () => {
61+
setWeekStatus.mutate({id: week.id, isBlocked: !week.isBlocked});
62+
};
63+
64+
const newSlugIn = () => {
65+
const ns = prompt('Leetcode title slug:');
66+
if( !ns )
67+
{
68+
return;
69+
}
70+
createFromSlug.mutate({slug: ns, weekId: week.id});
71+
};
72+
73+
problems = problems.filter((p) => p.weekId == week.id);
74+
75+
return (
76+
<div>
77+
<div className="flex items-center gap-3">
78+
<button title='Toggle view' onClick={handleSetWeekStatus}>
79+
{
80+
week.isBlocked ?
81+
<BsEyeSlashFill className="fill-neutral-400 hover:fill-accent" /> :
82+
<BsEyeFill className="fill-neutral-400 hover:fill-accent" />
83+
}
84+
</button>
85+
<button title='Buy more problems' onClick={newSlugIn}>
86+
<BsCartPlusFill className="fill-neutral-400 hover:fill-accent"/>
87+
</button>
88+
<button title='Change week title' onClick={handleWeekTitleChange}>
89+
<BsPencilSquare className="fill-neutral-400 hover:fill-accent"/>
90+
</button>
91+
<Subtitle label={week.title}/>
92+
</div>
93+
<table className="w-full table-fixed">
94+
<thead className="border-b border-white text-left font-semibold text-white">
95+
<tr>
96+
<th>Problem</th>
97+
<th className="text-center">Difficulty</th>
98+
<th className="text-center">Solved by</th>
99+
<th className="text-center">😵</th>
100+
</tr>
101+
</thead>
102+
<tbody>
103+
{
104+
problems.map((prob) => (<AdminViewRow key={prob.id} problem={prob} solvedBy={prob.solvedBy.length || 0}/>))
105+
}
106+
</tbody>
107+
</table>
108+
</div>
109+
)
110+
}
111+
112+
113+
export default AdminViewBlock;
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
"use client";
2+
3+
import { api } from "~/trpc/react";
4+
import { BsFillTrashFill } from "react-icons/bs";
5+
import type { Problem } from "@prisma/client";
6+
7+
interface AdminViewRowProps {
8+
problem: Problem,
9+
solvedBy: number,
10+
};
11+
12+
const AdminViewRow = ({problem, solvedBy}: AdminViewRowProps) => {
13+
14+
const utils = api.useUtils();
15+
16+
const deleteMutation = api.problem.delete.useMutation({
17+
onSuccess: async () => {
18+
/*remove element from page*/
19+
await utils.problem.invalidate();
20+
},
21+
onError: () => {
22+
alert('Error: Could not delete problem.');
23+
}
24+
});
25+
26+
const handleDelete = () => {
27+
deleteMutation.mutate(problem.id);
28+
};
29+
30+
return (
31+
<tr className="text-white">
32+
<td><a className="hover:underline" href={problem.leetcodeUrl}>{problem.name}</a></td>
33+
<td><p className="text-center">{problem.level}</p></td>
34+
<td><p className="text-center">{solvedBy}</p></td>
35+
<td className="text-center align-middle"><button title="Remove from set" onClick={handleDelete}>
36+
<BsFillTrashFill className="fill-neutral-400 hover:fill-accent"/>
37+
</button></td>
38+
</tr>
39+
)
40+
}
41+
42+
export default AdminViewRow;

src/app/_components/fetchData.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,21 +19,21 @@ export function TestButton() {
1919

2020
// data 2 === data3
2121

22-
const { data: data4 } = api.leetcode.getQuestionDescription.useQuery({
22+
/*const { data: data4 } = api.leetcode.getQuestionDescription.useQuery({
2323
titleSlug: data2?.titleSlug ?? "",
24-
});
24+
});*/
2525

2626
console.log("data:", data);
2727
console.log("data2:", data2);
2828
console.log("data3:", data3);
29-
console.log("data4:", data4);
29+
//console.log("data4:", data4);
3030

3131
return (
3232
<button
3333
// onClick={() => utils.post.invalidate()}
3434
className="text-primary-background rounded-lg bg-primary-foreground p-2 font-main"
3535
>
36-
API Tests
36+
API Health Check
3737
</button>
3838
);
3939
}

0 commit comments

Comments
 (0)