Skip to content

Commit 305fa6e

Browse files
authored
feat(app): add admin panel (#448)
* feat(app): add admin panel * refactor(app): refactor admin panel * refactor(app): rename getHref paremeter
1 parent 8ae01be commit 305fa6e

38 files changed

+810
-84
lines changed

apps/api/modules/questions/questions.routes.ts

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,12 @@ const questionsPlugin: FastifyPluginAsync = async (fastify) => {
190190
url: "/questions/:id",
191191
method: "PATCH",
192192
schema: generatePatchQuestionByIdSchema(args),
193+
preValidation(request, reply, done) {
194+
if (request.session.data?._user._roleId !== "admin") {
195+
throw fastify.httpErrors.unauthorized();
196+
}
197+
done();
198+
},
193199
async handler(request, reply) {
194200
const { id } = request.params;
195201

@@ -199,9 +205,9 @@ const questionsPlugin: FastifyPluginAsync = async (fastify) => {
199205
where: { id },
200206
data: {
201207
question,
202-
QuestionLevel: { connect: { id: level } },
203-
QuestionCategory: { connect: { id: category } },
204-
QuestionStatus: { connect: { id: status } },
208+
...(level && { QuestionLevel: { connect: { id: level } } }),
209+
...(category && { QuestionCategory: { connect: { id: category } } }),
210+
...(status && { QuestionStatus: { connect: { id: status } } }),
205211
},
206212
select: {
207213
id: true,
@@ -290,9 +296,20 @@ const questionsPlugin: FastifyPluginAsync = async (fastify) => {
290296
async handler(request, reply) {
291297
const { id } = request.params;
292298

293-
await fastify.db.question.delete({ where: { id } });
299+
try {
300+
await fastify.db.question.delete({ where: { id } });
301+
} catch (err) {
302+
if (isPrismaError(err)) {
303+
switch (err.code) {
304+
case PrismaErrorCode.RecordRequiredButNotFound:
305+
throw fastify.httpErrors.notFound(`Question with id: ${id} not found!`);
306+
}
307+
308+
throw err;
309+
}
310+
}
294311

295-
return reply.status(204);
312+
return reply.status(204).send();
296313
},
297314
});
298315

apps/api/modules/questions/questions.schemas.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -156,10 +156,12 @@ export const generatePatchQuestionByIdSchema = <
156156
params: Type.Object({
157157
id: Type.Integer(),
158158
}),
159-
body: Type.Object({
160-
...generateCreateQuestionShape(args),
161-
status: Type.Union(args.statuses.map((val) => Type.Literal(val))),
162-
}),
159+
body: Type.Partial(
160+
Type.Object({
161+
...generateCreateQuestionShape(args),
162+
status: Type.Union(args.statuses.map((val) => Type.Literal(val))),
163+
}),
164+
),
163165
response: {
164166
200: Type.Object({
165167
data: generateQuestionResponseSchema(args),
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
-- DropForeignKey
2+
ALTER TABLE "QuestionVote" DROP CONSTRAINT "QuestionVote__questionId_fkey";
3+
4+
-- AddForeignKey
5+
ALTER TABLE "QuestionVote" ADD CONSTRAINT "QuestionVote__questionId_fkey" FOREIGN KEY ("_questionId") REFERENCES "Question"("id") ON DELETE CASCADE ON UPDATE NO ACTION;

apps/api/prisma/schema.prisma

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ model QuestionVote {
4747
userId Int @map("_userId")
4848
questionId Int @map("_questionId")
4949
createdAt DateTime @default(now()) @db.Timestamptz(6)
50-
Question Question @relation(fields: [questionId], references: [id], onDelete: NoAction, onUpdate: NoAction)
50+
Question Question @relation(fields: [questionId], references: [id], onDelete: Cascade, onUpdate: NoAction)
5151
User User @relation(fields: [userId], references: [id], onDelete: NoAction, onUpdate: NoAction)
5252
5353
@@id([userId, questionId])

apps/app/public/icons/check.svg

Lines changed: 47 additions & 0 deletions
Loading

apps/app/public/icons/pencil.svg

Lines changed: 73 additions & 0 deletions
Loading

apps/app/public/icons/reject.svg

Lines changed: 37 additions & 0 deletions
Loading

apps/app/public/icons/trash.svg

Lines changed: 90 additions & 0 deletions
Loading
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import { ReactNode } from "react";
2+
import { Container } from "../../../components/Container";
3+
4+
export default function AdminPageLayout({ children }: { readonly children: ReactNode }) {
5+
return (
6+
<Container as="main" className="flex flex-col items-center gap-6 py-6">
7+
{children}
8+
</Container>
9+
);
10+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import { redirect } from "next/navigation";
2+
3+
import { PrivateRoute } from "../../../components/PrivateRoute";
4+
import { parsePageQuery } from "../../../lib/queryParsers";
5+
import { parseQueryLevels } from "../../../lib/level";
6+
import { SearchParams } from "../../../types";
7+
import { parseStatusQuery } from "../../../lib/question";
8+
import { parseTechnologyQuery } from "../../../lib/technologies";
9+
import { AdminPanel } from "../../../components/AdminPanel/AdminPanel";
10+
11+
export const dynamic = "force-dynamic";
12+
13+
export default function AdminPage({
14+
searchParams,
15+
}: {
16+
searchParams?: SearchParams<"page" | "technology" | "level" | "status">;
17+
}) {
18+
const page = parsePageQuery(searchParams?.page);
19+
const technology = parseTechnologyQuery(searchParams?.technology);
20+
const levels = parseQueryLevels(searchParams?.level);
21+
const status = parseStatusQuery(searchParams?.status);
22+
23+
if (!page || !status) {
24+
return redirect("/admin");
25+
}
26+
27+
return (
28+
<PrivateRoute>
29+
<AdminPanel page={page} technology={technology} status={status} levels={levels} />
30+
</PrivateRoute>
31+
);
32+
}

0 commit comments

Comments
 (0)