Skip to content

Commit

Permalink
Merge pull request #19 from udohjeremiah/dev
Browse files Browse the repository at this point in the history
Add user's providing answers to questions functionality
  • Loading branch information
udohjeremiah authored Nov 30, 2023
2 parents 5a91231 + 8ecc912 commit 4d6f6e4
Show file tree
Hide file tree
Showing 32 changed files with 3,354 additions and 1,048 deletions.
992 changes: 986 additions & 6 deletions package-lock.json

Large diffs are not rendered by default.

16 changes: 15 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,17 +19,31 @@
},
"dependencies": {
"@react-email/components": "^0.0.11",
"@tiptap/extension-link": "^2.1.12",
"@tiptap/extension-list-item": "^2.1.12",
"@tiptap/extension-placeholder": "^2.1.12",
"@tiptap/extension-subscript": "^2.1.12",
"@tiptap/extension-superscript": "^2.1.12",
"@tiptap/extension-text-style": "^2.1.12",
"@tiptap/extension-underline": "^2.1.12",
"@tiptap/pm": "^2.1.12",
"@tiptap/react": "^2.1.12",
"@tiptap/starter-kit": "^2.1.12",
"bcrypt": "^5.1.1",
"html-react-parser": "^5.0.6",
"mongodb": "^6.3.0",
"next": "14.0.1",
"next-auth": "^4.24.5",
"nodemailer": "^6.9.7",
"react": "^18",
"react-dom": "^18",
"react-email": "^1.9.5",
"react-icons": "^4.11.0"
"react-icons": "^4.11.0",
"react-inlinesvg": "^4.1.0",
"swr": "^2.2.4"
},
"devDependencies": {
"@tailwindcss/typography": "^0.5.10",
"autoprefixer": "^10.0.1",
"eslint": "^8",
"eslint-config-next": "14.0.1",
Expand Down
297 changes: 297 additions & 0 deletions src/app/api/answers/[id]/route.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,297 @@
import connectClient from "@/db/client";
import { ObjectId } from "mongodb";
import { NextResponse } from "next/server";

export async function GET(request, { params }) {
try {
const id = params?.id;

if (!id) {
return NextResponse.json(
{ success: false, message: "Bad request" },
{ status: 400 },
);
}

const client = await connectClient();
const db = client.db(process.env.MONGODB_DATABSE);
const collection = db.collection(process.env.MONGODB_COLLECTION_ANSWERS);

const answers = await collection.find({ questionId: id }).toArray();

return NextResponse.json(
{
success: true,
message: "Answers have been successfully fetched",
answers,
},
{ status: 200, statusText: "OK" },
);
} catch (e) {
console.error(e);
return NextResponse(
{ success: false, error: e },
{ status: 500, statusText: "Internal Server Error" },
);
}
}

export async function POST(request, { params }) {
try {
const id = params?.id;
const { userId, userName, userImage, htmlContent } = await request.json();

if (!id || !userId || !userName || !userImage || !htmlContent) {
return NextResponse.json(
{ success: false, message: "Bad request" },
{ status: 400 },
);
}

const client = await connectClient();
const db = client.db(process.env.MONGODB_DATABSE);
const answersCollection = db.collection(
process.env.MONGODB_COLLECTION_ANSWERS,
);
const usersCollection = db.collection(process.env.MONGODB_COLLECTION_USERS);

// Check if the user has already answered this question
if (
await answersCollection.findOne({
questionId: id,
userId,
})
) {
return NextResponse.json(
{
success: false,
message: "User has already answered this question",
},
{ status: 409, statusText: "Conflict" },
);
}

const result = await answersCollection.insertOne({
questionId: id,
userId,
userName,
userImage,
createdAt: new Date().toUTCString(),
answer: htmlContent,
edited: false,
upvotes: 0,
upvotesHistory: [],
});

await usersCollection.findOneAndUpdate(
{ _id: new ObjectId(userId) },
{ $inc: { providedAnswers: 1 } },
);

return NextResponse.json(
{
success: true,
message: `Answer with id ${result.insertedId} has been successfully created`,
},
{ status: 201, statusText: "Created" },
);
} catch (e) {
console.error(e);
return NextResponse.json(
{ success: false, error: e },
{ status: 500, statusText: "Internal Server Error" },
);
}
}

export async function PATCH(request, { params }) {
try {
const id = params?.id;

if (!id) {
return NextResponse.json(
{ success: false, message: "Bad request" },
{ status: 400 },
);
}

const client = await connectClient();
const db = client.db(process.env.MONGODB_DATABSE);
const answersCollection = db.collection(
process.env.MONGODB_COLLECTION_ANSWERS,
);
const usersCollection = db.collection(process.env.MONGODB_COLLECTION_USERS);

const { questionUserId, userId, htmlContent, upvotes, upvotesDirection } =
await request.json();

if (!questionUserId || !userId) {
return NextResponse.json(
{ success: false, message: "Bad request" },
{ status: 400 },
);
}

const updateFields = {};

if (htmlContent !== undefined) {
updateFields.answer = htmlContent;
updateFields.edited = true;
}

if (upvotes !== undefined) {
if (!upvotesDirection) {
return NextResponse.json(
{ success: false, message: "Bad request" },
{ status: 400 },
);
}

const upvotesHistory = (
await answersCollection.findOne({
questionId: id,
userId: questionUserId,
})
).upvotesHistory;

if (
upvotesHistory.some(
(item) =>
JSON.stringify(item) ===
JSON.stringify({ id: userId, direction: upvotesDirection }),
)
) {
return NextResponse.json(
{
success: false,
message: `User with id ${userId} has already ${
upvotesDirection === "up" ? "upvoted" : "downvoted"
} this answer before`,
},
{ status: 409, statusText: "Conflict" },
);
}

const existingVote = upvotesHistory.find((item) => item.id === userId);
if (existingVote) {
// user has upvoted or downvoted before
updateFields.upvotes =
upvotesDirection === "up" ? upvotes + 2 : upvotes - 2;

updateFields.upvotesHistory = upvotesHistory.map((item) =>
item.id === userId
? { id: userId, direction: upvotesDirection }
: item,
);
} else {
// user has not upvoted or downvoted before
updateFields.upvotes =
upvotesDirection === "up" ? upvotes + 1 : upvotes - 1;

updateFields.upvotesHistory = [
...upvotesHistory,
{ id: userId, direction: upvotesDirection },
];
}
}

if (Object.keys(updateFields).length === 0) {
return NextResponse.json(
{
success: false,
message: "No valid fields provided for update",
},
{ status: 400, statusText: "Bad Request" },
);
}

const result = await answersCollection.updateOne(
{ questionId: id, userId: questionUserId },
{ $set: updateFields },
);

console.log(updateFields.upvotes);
await usersCollection.findOneAndUpdate(
{ _id: new ObjectId(userId) },
{ $inc: { totalUpvotesReceived: updateFields.upvotes } },
);

if (result.modifiedCount === 1) {
return NextResponse.json(
{
success: true,
message: `Answer with id: ${id} successfully updated`,
},
{ status: 200, statusText: "OK" },
);
} else {
return NextResponse.json(
{
success: false,
message: `No answer found for this question with id: ${id} provided by user with id: ${userId}`,
},
{ status: 404, statusText: "Not Found" },
);
}
} catch (e) {
console.error(e);
return NextResponse.json(
{ success: false, error: e },
{ status: 500, statusText: "Internal Server Error" },
);
}
}

export async function DELETE(request, { params }) {
try {
const id = params?.id;
const { userId } = await request.json();

if (!id || !userId) {
return NextResponse.json(
{ success: false, message: "Bad request" },
{ status: 400 },
);
}

const client = await connectClient();
const db = client.db(process.env.MONGODB_DATABSE);
const answersCollection = db.collection(
process.env.MONGODB_COLLECTION_ANSWERS,
);
const usersCollection = db.collection(process.env.MONGODB_COLLECTION_USERS);

const result = await answersCollection.deleteOne({
questionId: id,
userId,
});

await usersCollection.findOneAndUpdate(
{ _id: new ObjectId(userId) },
{ $inc: { providedAnswers: -1 } },
);

if (result.deletedCount === 1) {
return NextResponse.json(
{
success: true,
message: `Answer with id ${id} from user with id ${userId} has been successfully deleted`,
},
{ status: 200, statusText: "OK" },
);
} else {
return NextResponse.json(
{
success: false,
message: `No answer was found for the question with id ${id} provided by the user with id ${userId}`,
},
{ status: 404, statusText: "Not Found" },
);
}
} catch (e) {
return NextResponse.json(
{ success: false, error: e },
{ status: 500, statusText: "Internal Server Error" },
);
}
}
22 changes: 20 additions & 2 deletions src/app/api/auth/[...nextauth]/options.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import * as bcrypt from "bcrypt";
import { render } from "@react-email/components";
import VerifyEmailSuccessfulEmail from "@/emails/verify-email-successful-email";
import nodemailer from "nodemailer";
import generateAvatar from "@/utils/generateAvatar";

async function sendWelcomeEmail(account, name, email) {
const verifyEmailSuccessfulEmailHtml = render(
Expand Down Expand Up @@ -94,6 +95,7 @@ export const options = {
$set: {
email: user.email,
name: user.name,
userName: user.email.split("@")[0],
image: user.image,
},
},
Expand All @@ -102,18 +104,34 @@ export const options = {
await collection.insertOne({
email: user.email,
name: user.name,
image: user.image,
userName: user.email.split("@")[0],
image: user.image || generateAvatar(user.name),
emailVerified: true,
providedAnswers: 0,
totalUpvotesReceived: 0,
totalDownvotesReceived: 0,
});

await sendWelcomeEmail(account, user.name, user.email);
}

return true;
},

async session({ session, token, user }) {
const client = await connectClient();
const database = client.db(process.env.MONGODB_DATABSE);
const collection = database.collection(
process.env.MONGODB_COLLECTION_USERS,
);

const existingUser = await collection.findOne({ email: token.email });
if (existingUser) {
session.user.id = existingUser._id;
session.user.userName = existingUser.userName;
}

return session;
},
},
pages: {
signIn: "/signin",
Expand Down
Loading

0 comments on commit 4d6f6e4

Please sign in to comment.