Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add admin comments to submissions #18

Merged
merged 6 commits into from
Aug 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
100 changes: 100 additions & 0 deletions packages/nextjs/app/admin/_components/SubmissionCard.tsx
Original file line number Diff line number Diff line change
@@ -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) {
rin-st marked this conversation as resolved.
Show resolved Hide resolved
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 (
<div key={submission.id} className="card bg-primary text-primary-content">
<div className="card-body">
<h2 className="card-title">{submission.title}</h2>
{submission.linkToRepository && (
<a href={submission.linkToRepository} className="link" target="_blank">
{submission.linkToRepository}
</a>
)}
<p>{submission.description}</p>
{submission.builder && <Address address={submission.builder} />}
<div className="collapse">
<input type="checkbox" />
<div className="collapse-title text-xl font-medium">{submission.comments.length} comments</div>
<div className="collapse-content">
{submission.comments?.map(comment => (
<div key={comment.id} className="card bg-base-200 text-base-content mb-4">
<div className="card-body">
<Address address={comment.builder} />
<p className="m-1">{comment.comment}</p>
<p>{comment.createdAt ? getFormattedDateTime(new Date(comment.createdAt)) : "-"}</p>
</div>
</div>
))}
<div className="card bg-base-200 text-base-content mb-4">
<div className="card-body">
<form action={clientFormAction} className="card-body space-y-3 p-0">
<textarea
className="p-2 h-32"
value={newComment}
name="comment"
onChange={field => {
setNewComment(field.target.value);
}}
/>
<button className="btn btn-primary">Add Comment</button>
</form>
</div>
</div>
</div>
</div>
</div>
</div>
);
};
16 changes: 16 additions & 0 deletions packages/nextjs/app/admin/_components/Submissions.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { SubmissionCard } from "./SubmissionCard";
import { getAllSubmissions } from "~~/services/database/repositories/submissions";

export const Submissions = async () => {
const submissions = await getAllSubmissions();

return (
<>
<div className="flex items-center flex-col flex-grow pt-10 space-y-4">
{submissions?.map(submission => {
return <SubmissionCard key={submission.id} submission={submission} />;
})}
</div>
</>
);
};
27 changes: 2 additions & 25 deletions packages/nextjs/app/admin/page.tsx
Original file line number Diff line number Diff line change
@@ -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 () => {
Expand All @@ -11,29 +10,7 @@ const Admin: NextPage = async () => {
return <div className="flex items-center text-xl flex-col flex-grow pt-10 space-y-4">Access denied</div>;
}

const submissions = await getAllSubmissions();
return (
<>
<div className="flex items-center flex-col flex-grow pt-10 space-y-4">
{submissions.map(submission => {
return (
<div key={submission.id} className="card bg-primary text-primary-content">
<div className="card-body">
<h2 className="card-title">{submission.title}</h2>
{submission.linkToRepository && (
<a href={submission.linkToRepository} className="link" target="_blank">
{submission.linkToRepository}
</a>
)}
<p>{submission.description}</p>
{submission.builder && <Address address={submission.builder} />}
</div>
</div>
);
})}
</div>
</>
);
return <Submissions />;
};

export default Admin;
Original file line number Diff line number Diff line change
@@ -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) {
rin-st marked this conversation as resolved.
Show resolved Hide resolved
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 });
}
}
19 changes: 0 additions & 19 deletions packages/nextjs/app/api/submissions/new/route.ts

This file was deleted.

31 changes: 19 additions & 12 deletions packages/nextjs/app/api/submissions/route.ts
Original file line number Diff line number Diff line change
@@ -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) {
Expand All @@ -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
) {
Expand All @@ -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 });
Expand Down
2 changes: 1 addition & 1 deletion packages/nextjs/app/submit/_component/Form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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("/");
Expand Down
2 changes: 1 addition & 1 deletion packages/nextjs/scaffold.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
47 changes: 36 additions & 11 deletions packages/nextjs/services/database/config/schema.ts
Original file line number Diff line number Diff line change
@@ -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] }),
}));
9 changes: 9 additions & 0 deletions packages/nextjs/services/database/repositories/comments.ts
Original file line number Diff line number Diff line change
@@ -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<typeof comments>;

export async function createComment(comment: CommentInsert) {
return await db.insert(comments).values(comment);
}
8 changes: 5 additions & 3 deletions packages/nextjs/services/database/repositories/submissions.ts
Original file line number Diff line number Diff line change
@@ -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<typeof submissions>;
type Comment = InferInsertModel<typeof comments>;
export type Submission = InferSelectModel<typeof submissions> & { comments: Comment[] };

export async function getAllSubmissions() {
return await db.select().from(submissions);
return await db.query.submissions.findMany({ with: { comments: true } });
}
rin-st marked this conversation as resolved.
Show resolved Hide resolved

export async function createSubmission(submission: SubmissionInsert) {
Expand Down
Loading
Loading