diff --git a/packages/nextjs/app/admin/_components/SubmissionCard.tsx b/packages/nextjs/app/admin/_components/SubmissionCard.tsx
new file mode 100644
index 0000000..ff6da46
--- /dev/null
+++ b/packages/nextjs/app/admin/_components/SubmissionCard.tsx
@@ -0,0 +1,100 @@
+"use client";
+
+import { useState } from "react";
+import { useRouter } from "next/navigation";
+import { useMutation } from "@tanstack/react-query";
+import { Address } from "~~/components/scaffold-eth";
+import { Submission } from "~~/services/database/repositories/submissions";
+import { postMutationFetcher } from "~~/utils/react-query";
+import { notification } from "~~/utils/scaffold-eth";
+
+function getFormattedDateTime(date: Date) {
+ const month = String(date.getMonth() + 1).padStart(2, "0");
+ const day = String(date.getDate()).padStart(2, "0");
+ const year = date.getFullYear();
+ const hours = date.getHours();
+ const minutes = String(date.getMinutes()).padStart(2, "0");
+
+ return `${month}/${day}/${year} ${hours}:${minutes}`;
+}
+
+export const SubmissionCard = ({ submission }: { submission: Submission }) => {
+ const [newComment, setNewComment] = useState("");
+ const { mutateAsync: postNewComment } = useMutation({
+ mutationFn: (newComment: { comment: string }) =>
+ postMutationFetcher(`/api/submissions/${submission.id}/comments`, { body: newComment }),
+ });
+ const { refresh } = useRouter();
+
+ const clientFormAction = async (formData: FormData) => {
+ try {
+ const comment = formData.get("comment") as string;
+ if (!comment) {
+ notification.error("Please fill the comment");
+ return;
+ }
+
+ if (comment.length > 255) {
+ notification.error("Comment is too long");
+ return;
+ }
+
+ await postNewComment({ comment });
+
+ notification.success("Comment submitted successfully!");
+ setNewComment("");
+ refresh();
+ } catch (error: any) {
+ if (error instanceof Error) {
+ notification.error(error.message);
+ return;
+ }
+ notification.error("Something went wrong");
+ }
+ };
+
+ return (
+
+
+
{submission.title}
+ {submission.linkToRepository && (
+
+ {submission.linkToRepository}
+
+ )}
+
{submission.description}
+ {submission.builder &&
}
+
+
+
{submission.comments.length} comments
+
+ {submission.comments?.map(comment => (
+
+
+
+
{comment.comment}
+
{comment.createdAt ? getFormattedDateTime(new Date(comment.createdAt)) : "-"}
+
+
+ ))}
+
+
+
+
+
+ );
+};
diff --git a/packages/nextjs/app/admin/_components/Submissions.tsx b/packages/nextjs/app/admin/_components/Submissions.tsx
new file mode 100644
index 0000000..30d4bc2
--- /dev/null
+++ b/packages/nextjs/app/admin/_components/Submissions.tsx
@@ -0,0 +1,16 @@
+import { SubmissionCard } from "./SubmissionCard";
+import { getAllSubmissions } from "~~/services/database/repositories/submissions";
+
+export const Submissions = async () => {
+ const submissions = await getAllSubmissions();
+
+ return (
+ <>
+
+ {submissions?.map(submission => {
+ return ;
+ })}
+
+ >
+ );
+};
diff --git a/packages/nextjs/app/admin/page.tsx b/packages/nextjs/app/admin/page.tsx
index 9ac066c..938312c 100644
--- a/packages/nextjs/app/admin/page.tsx
+++ b/packages/nextjs/app/admin/page.tsx
@@ -1,7 +1,6 @@
+import { Submissions } from "./_components/Submissions";
import type { NextPage } from "next";
import { getServerSession } from "next-auth";
-import { Address } from "~~/components/scaffold-eth";
-import { getAllSubmissions } from "~~/services/database/repositories/submissions";
import { authOptions } from "~~/utils/auth";
const Admin: NextPage = async () => {
@@ -11,29 +10,7 @@ const Admin: NextPage = async () => {
return Access denied
;
}
- const submissions = await getAllSubmissions();
- return (
- <>
-
- {submissions.map(submission => {
- return (
-
- );
- })}
-
- >
- );
+ return ;
};
export default Admin;
diff --git a/packages/nextjs/app/api/submissions/[submissionId]/comments/route.ts b/packages/nextjs/app/api/submissions/[submissionId]/comments/route.ts
new file mode 100644
index 0000000..24f484d
--- /dev/null
+++ b/packages/nextjs/app/api/submissions/[submissionId]/comments/route.ts
@@ -0,0 +1,36 @@
+import { NextRequest, NextResponse } from "next/server";
+import { getServerSession } from "next-auth";
+import { createComment } from "~~/services/database/repositories/comments";
+import { authOptions } from "~~/utils/auth";
+
+export async function POST(req: NextRequest, { params }: { params: { submissionId: number } }) {
+ try {
+ const session = await getServerSession(authOptions);
+
+ if (session?.user.role !== "admin") {
+ return NextResponse.json({ error: "Only admins can add comments" }, { status: 401 });
+ }
+ const { submissionId } = params;
+
+ const { comment } = (await req.json()) as { comment: string };
+
+ if (!comment || comment.length > 255) {
+ return NextResponse.json({ error: "Invalid comment submitted" }, { status: 400 });
+ }
+
+ if (!session.user.address) {
+ return NextResponse.json({ error: "Invalid admin address" }, { status: 400 });
+ }
+
+ const newComment = await createComment({
+ comment,
+ submission: submissionId,
+ builder: session.user.address,
+ });
+
+ return NextResponse.json({ newComment }, { status: 201 });
+ } catch (e) {
+ console.error(e);
+ return NextResponse.json({ error: "Error processing form" }, { status: 500 });
+ }
+}
diff --git a/packages/nextjs/app/api/submissions/new/route.ts b/packages/nextjs/app/api/submissions/new/route.ts
deleted file mode 100644
index 5e7e3e3..0000000
--- a/packages/nextjs/app/api/submissions/new/route.ts
+++ /dev/null
@@ -1,19 +0,0 @@
-import { NextResponse } from "next/server";
-import { createSubmission } from "~~/services/database/repositories/submissions";
-
-// TODO This should be a POST request
-export async function GET() {
- const newSubmission = {
- title: `Submission Title ${Math.random().toString(36).substring(7)}`,
- description: "Description of the new submission",
- builder: "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266",
- };
-
- try {
- const result = await createSubmission(newSubmission);
- return NextResponse.json(result, { status: 201 });
- } catch (error) {
- console.error(error);
- return NextResponse.json({ error: "Error creating submission" }, { status: 500 });
- }
-}
diff --git a/packages/nextjs/app/api/submissions/route.ts b/packages/nextjs/app/api/submissions/route.ts
index 7672e06..bcff8f7 100644
--- a/packages/nextjs/app/api/submissions/route.ts
+++ b/packages/nextjs/app/api/submissions/route.ts
@@ -1,12 +1,19 @@
import { NextResponse } from "next/server";
+import { getServerSession } from "next-auth";
import { recoverTypedDataAddress } from "viem";
import { createBuilder, getBuilderById } from "~~/services/database/repositories/builders";
import { createSubmission, getAllSubmissions } from "~~/services/database/repositories/submissions";
import { SubmissionInsert } from "~~/services/database/repositories/submissions";
+import { authOptions } from "~~/utils/auth";
import { EIP_712_DOMAIN, EIP_712_TYPES__SUBMISSION } from "~~/utils/eip712";
export async function GET() {
try {
+ const session = await getServerSession(authOptions);
+
+ if (session?.user.role !== "admin") {
+ return NextResponse.json({ error: "Only admins can get all the submissions" }, { status: 401 });
+ }
const grants = await getAllSubmissions();
return NextResponse.json(grants);
} catch (error) {
@@ -15,18 +22,18 @@ export async function GET() {
}
}
-export type CreateNewSubmissionBody = SubmissionInsert & { signature: `0x${string}`; signer: string };
+export type CreateNewSubmissionBody = SubmissionInsert & { signature: `0x${string}` };
export async function POST(req: Request) {
try {
- const { title, description, linkToRepository, signature, signer } = (await req.json()) as CreateNewSubmissionBody;
+ const { title, description, linkToRepository, signature, builder } = (await req.json()) as CreateNewSubmissionBody;
if (
!title ||
!description ||
!linkToRepository ||
!signature ||
- !signer ||
+ !builder ||
description.length > 750 ||
title.length > 75
) {
@@ -41,21 +48,21 @@ export async function POST(req: Request) {
signature: signature,
});
- if (recoveredAddress !== signer) {
- return NextResponse.json({ error: "Recovered address did not match signer" }, { status: 401 });
+ if (recoveredAddress !== builder) {
+ return NextResponse.json({ error: "Recovered address did not match builder" }, { status: 401 });
}
- const builder = await getBuilderById(signer);
+ const builderData = await getBuilderById(builder);
- if (!builder) {
- await createBuilder({ id: signer, role: "user" });
+ if (!builderData) {
+ await createBuilder({ id: builder, role: "user" });
}
const submission = await createSubmission({
- title: title,
- description: description,
- linkToRepository: linkToRepository,
- builder: signer,
+ title,
+ description,
+ linkToRepository,
+ builder,
});
return NextResponse.json({ submission }, { status: 201 });
diff --git a/packages/nextjs/app/submit/_component/Form.tsx b/packages/nextjs/app/submit/_component/Form.tsx
index e280f0a..392180a 100644
--- a/packages/nextjs/app/submit/_component/Form.tsx
+++ b/packages/nextjs/app/submit/_component/Form.tsx
@@ -48,7 +48,7 @@ const Form = () => {
},
});
- await postNewSubmission({ title, description, linkToRepository, signature, signer: connectedAddress });
+ await postNewSubmission({ title, description, linkToRepository, signature, builder: connectedAddress });
notification.success("Extension submitted successfully!");
router.push("/");
diff --git a/packages/nextjs/scaffold.config.ts b/packages/nextjs/scaffold.config.ts
index 86c737a..b00a22e 100644
--- a/packages/nextjs/scaffold.config.ts
+++ b/packages/nextjs/scaffold.config.ts
@@ -10,7 +10,7 @@ export type ScaffoldConfig = {
const scaffoldConfig = {
// The networks on which your DApp is live
- targetNetworks: [chains.hardhat],
+ targetNetworks: [chains.sepolia],
// The interval at which your front-end polls the RPC servers for new data
// it has no effect if you only target the local network (default is 4000)
diff --git a/packages/nextjs/services/database/config/schema.ts b/packages/nextjs/services/database/config/schema.ts
index 9952997..a51a9be 100644
--- a/packages/nextjs/services/database/config/schema.ts
+++ b/packages/nextjs/services/database/config/schema.ts
@@ -1,17 +1,42 @@
-import { sql } from "drizzle-orm";
-import { pgTable, serial, text, timestamp, varchar } from "drizzle-orm/pg-core";
+import { relations, sql } from "drizzle-orm";
+import { integer, pgTable, serial, text, timestamp, varchar } from "drizzle-orm/pg-core";
+
+export const builders = pgTable("builders", {
+ id: varchar("id", { length: 256 }).primaryKey(),
+ role: varchar("role", { length: 256 }).notNull(),
+});
-// TODO: Define the right schema.
export const submissions = pgTable("submissions", {
id: serial("id").primaryKey(),
- title: varchar("name", { length: 256 }),
- description: text("description"),
- linkToRepository: varchar("link_to_repository", { length: 256 }),
- submissionTimestamp: timestamp("submission_timestamp").default(sql`now()`),
- builder: varchar("builder_id", { length: 256 }).references(() => builders.id),
+ title: varchar("name", { length: 256 }).notNull(),
+ description: text("description").notNull(),
+ linkToRepository: varchar("link_to_repository", { length: 256 }).notNull(),
+ submissionTimestamp: timestamp("submission_timestamp")
+ .default(sql`now()`)
+ .notNull(),
+ builder: varchar("builder_id", { length: 256 })
+ .references(() => builders.id)
+ .notNull(),
});
-export const builders = pgTable("builders", {
- id: varchar("id", { length: 256 }).primaryKey(),
- role: varchar("role", { length: 256 }),
+export const comments = pgTable("comments", {
+ id: serial("id").primaryKey(),
+ submission: integer("submission_id")
+ .references(() => submissions.id)
+ .notNull(),
+ builder: varchar("builder_id", { length: 256 })
+ .references(() => builders.id)
+ .notNull(),
+ comment: text("comment").notNull(),
+ createdAt: timestamp("created_at")
+ .default(sql`now()`)
+ .notNull(),
});
+
+export const submissionsRelations = relations(submissions, ({ many }) => ({
+ comments: many(comments),
+}));
+
+export const commentsRelations = relations(comments, ({ one }) => ({
+ submission: one(submissions, { fields: [comments.submission], references: [submissions.id] }),
+}));
diff --git a/packages/nextjs/services/database/repositories/comments.ts b/packages/nextjs/services/database/repositories/comments.ts
new file mode 100644
index 0000000..4f8e169
--- /dev/null
+++ b/packages/nextjs/services/database/repositories/comments.ts
@@ -0,0 +1,9 @@
+import { InferInsertModel } from "drizzle-orm";
+import { db } from "~~/services/database/config/postgresClient";
+import { comments } from "~~/services/database/config/schema";
+
+export type CommentInsert = InferInsertModel;
+
+export async function createComment(comment: CommentInsert) {
+ return await db.insert(comments).values(comment);
+}
diff --git a/packages/nextjs/services/database/repositories/submissions.ts b/packages/nextjs/services/database/repositories/submissions.ts
index 8597a41..0039fb6 100644
--- a/packages/nextjs/services/database/repositories/submissions.ts
+++ b/packages/nextjs/services/database/repositories/submissions.ts
@@ -1,11 +1,13 @@
-import { InferInsertModel } from "drizzle-orm";
+import { InferInsertModel, InferSelectModel } from "drizzle-orm";
import { db } from "~~/services/database/config/postgresClient";
-import { submissions } from "~~/services/database/config/schema";
+import { comments, submissions } from "~~/services/database/config/schema";
export type SubmissionInsert = InferInsertModel;
+type Comment = InferInsertModel;
+export type Submission = InferSelectModel & { comments: Comment[] };
export async function getAllSubmissions() {
- return await db.select().from(submissions);
+ return await db.query.submissions.findMany({ with: { comments: true } });
}
export async function createSubmission(submission: SubmissionInsert) {
diff --git a/packages/nextjs/services/database/seed.ts b/packages/nextjs/services/database/seed.ts
index f35f8ff..3e63473 100644
--- a/packages/nextjs/services/database/seed.ts
+++ b/packages/nextjs/services/database/seed.ts
@@ -1,4 +1,4 @@
-import { builders, submissions } from "./config/schema";
+import { builders, comments, submissions } from "./config/schema";
import * as schema from "./config/schema";
import * as dotenv from "dotenv";
import { drizzle } from "drizzle-orm/node-postgres";
@@ -16,6 +16,7 @@ async function seed() {
await client.connect();
const db = drizzle(client, { schema });
+ await db.delete(comments).execute();
await db.delete(submissions).execute();
await db.delete(builders).execute();
@@ -26,7 +27,7 @@ async function seed() {
])
.execute();
- await db
+ const newSubmissions = await db
.insert(submissions)
.values([
{
@@ -42,8 +43,22 @@ async function seed() {
builder: "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266",
},
])
+ .returning({ insertedId: submissions.id })
.execute();
+ await db.insert(comments).values([
+ {
+ builder: "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266",
+ comment: "This is a comment",
+ submission: newSubmissions[0].insertedId,
+ },
+ {
+ builder: "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266",
+ comment: "This is another comment",
+ submission: newSubmissions[0].insertedId,
+ },
+ ]);
+
console.log("Database seeded successfully");
}