-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
30dfe0f
commit c98ec1b
Showing
11 changed files
with
1,917 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,267 @@ | ||
"use server"; | ||
|
||
import { redirect } from "next/navigation"; | ||
import { revalidatePath } from "next/cache"; | ||
|
||
import Answer from "@/database/answer.model"; | ||
import Question from "@/database/question.model"; | ||
import User from "@/database/user.model"; | ||
import Interaction from "@/database/interaction.model"; | ||
|
||
import { connectToDatabase } from "@/lib/mongoose"; | ||
|
||
import type { | ||
AnswerVoteParams, | ||
CreateAnswerParams, | ||
DeleteAnswerParams, | ||
EditAnswerParams, | ||
GetAnswerByIdParams, | ||
GetAnswersParams, | ||
} from "./shared.types"; | ||
|
||
export async function createAnswer(params: CreateAnswerParams) { | ||
try { | ||
connectToDatabase(); | ||
|
||
const { content, author, question, path } = params; | ||
|
||
const newAnswer = await Answer.create({ | ||
content, | ||
author, | ||
question, | ||
path, | ||
}); | ||
|
||
// add the answer to the question's answers array | ||
const questionObj = await Question.findByIdAndUpdate(question, { | ||
$push: { answers: newAnswer._id }, | ||
}); | ||
|
||
// create an interaction record for the user's create_answer action | ||
await Interaction.create({ | ||
user: author, | ||
action: "answer", | ||
question, | ||
answer: newAnswer._id, | ||
tags: questionObj.tag, | ||
}); | ||
|
||
// increment author's reputation by +S for creating a answer | ||
await User.findByIdAndUpdate(author, { $inc: { reputation: 10 } }); | ||
|
||
revalidatePath(path); | ||
} catch (error) { | ||
console.log(error); | ||
throw error; | ||
} | ||
} | ||
|
||
export async function editAnswer(params: EditAnswerParams) { | ||
try { | ||
connectToDatabase(); | ||
|
||
const { answerId, content, path } = params; | ||
|
||
const answer = await Answer.findById(answerId); | ||
|
||
if (!answer) { | ||
throw new Error("Answer not found"); | ||
} | ||
|
||
answer.content = content; | ||
|
||
await answer.save(); | ||
|
||
redirect(path); | ||
} catch (error) { | ||
console.log(error); | ||
throw error; | ||
} | ||
} | ||
|
||
export async function deleteAnswer(params: DeleteAnswerParams) { | ||
try { | ||
connectToDatabase(); | ||
|
||
const { answerId, path } = params; | ||
|
||
const answer = await Answer.findById(answerId); | ||
|
||
if (!answer) { | ||
throw new Error("Answer not found"); | ||
} | ||
|
||
await answer.deleteOne({ _id: answerId }); | ||
|
||
await Question.updateMany( | ||
{ _id: answer.question }, | ||
{ $pull: { answers: answerId } } | ||
); | ||
|
||
await Interaction.deleteMany({ answer: answerId }); | ||
|
||
revalidatePath(path); | ||
} catch (error) { | ||
console.log(error); | ||
throw error; | ||
} | ||
} | ||
|
||
export async function getAnswers(params: GetAnswersParams) { | ||
try { | ||
connectToDatabase(); | ||
|
||
const { questionId, page = 1, pageSize = 10, sortBy } = params; | ||
|
||
// Calculate the number of answers to skip based on the page number and page size | ||
const skipAmount = (page - 1) * pageSize; | ||
|
||
let sortOptions = {}; | ||
|
||
switch (sortBy) { | ||
case "highestUpvotes": | ||
sortOptions = { upvotes: -1 }; | ||
break; | ||
case "lowestUpvotes": | ||
sortOptions = { upvotes: 1 }; | ||
break; | ||
case "recent": | ||
sortOptions = { createdAt: -1 }; | ||
break; | ||
case "old": | ||
sortOptions = { createdAt: 1 }; | ||
break; | ||
default: | ||
break; | ||
} | ||
|
||
const answers = await Answer.find({ question: questionId }) | ||
.populate("author", "_id clerkId name picture") | ||
.sort(sortOptions) | ||
.skip(skipAmount) | ||
.limit(pageSize); | ||
|
||
const totalAnswers = await Answer.countDocuments({ question: questionId }); | ||
|
||
const isNext = totalAnswers > skipAmount + answers.length; | ||
|
||
return { answers, isNext }; | ||
} catch (error) { | ||
console.log(error); | ||
throw error; | ||
} | ||
} | ||
|
||
export async function getAnswerById(params: GetAnswerByIdParams) { | ||
try { | ||
connectToDatabase(); | ||
|
||
const { answerId } = params; | ||
|
||
const answer = await Answer.findById(answerId).populate( | ||
"author", | ||
"_id clerkId name picture" | ||
); | ||
|
||
return answer; | ||
} catch (error) { | ||
console.log(error); | ||
throw error; | ||
} | ||
} | ||
|
||
export async function upvoteAnswer(params: AnswerVoteParams) { | ||
try { | ||
connectToDatabase(); | ||
|
||
const { answerId, userId, hasupVoted, hasdownVoted, path } = params; | ||
|
||
let updateQuery = {}; | ||
|
||
if (hasupVoted) { | ||
updateQuery = { | ||
$pull: { upvotes: userId }, | ||
}; | ||
} else if (hasdownVoted) { | ||
updateQuery = { | ||
$pull: { downvotes: userId }, | ||
$push: { upvotes: userId }, | ||
}; | ||
} else { | ||
updateQuery = { $addToSet: { upvotes: userId } }; | ||
} | ||
|
||
const answer = await Answer.findByIdAndUpdate(answerId, updateQuery, { | ||
new: true, | ||
}); | ||
|
||
if (!answer) { | ||
throw new Error("Answer not found"); | ||
} | ||
|
||
if (userId !== answer.author.toString()) { | ||
// increment user's reputation by +S for upvoting/revoking an upvote to the answer (S = 2) | ||
await User.findByIdAndUpdate(userId, { | ||
$inc: { reputation: hasupVoted ? -2 : 2 }, | ||
}); | ||
|
||
// increment author's reputation by +S for upvoting/revoking an upvote to the answer (S = 10) | ||
await User.findByIdAndUpdate(answer.author, { | ||
$inc: { reputation: hasupVoted ? -10 : 10 }, | ||
}); | ||
} | ||
|
||
revalidatePath(path); | ||
} catch (error) { | ||
console.log(error); | ||
throw error; | ||
} | ||
} | ||
|
||
export async function downvoteAnswer(params: AnswerVoteParams) { | ||
try { | ||
connectToDatabase(); | ||
|
||
const { answerId, userId, hasupVoted, hasdownVoted, path } = params; | ||
|
||
let updateQuery = {}; | ||
|
||
if (hasdownVoted) { | ||
updateQuery = { | ||
$pull: { downvotes: userId }, | ||
}; | ||
} else if (hasupVoted) { | ||
updateQuery = { | ||
$pull: { upvotes: userId }, | ||
$push: { downvotes: userId }, | ||
}; | ||
} else { | ||
updateQuery = { $addToSet: { downvotes: userId } }; | ||
} | ||
|
||
const answer = await Question.findByIdAndUpdate(answerId, updateQuery, { | ||
new: true, | ||
}); | ||
|
||
if (!answer) { | ||
throw new Error("Answer not found"); | ||
} | ||
|
||
if (userId !== answer.author.toString()) { | ||
// decrement author's reputation by +S for downvoting/revoking an downvote to the answer (S = 2) | ||
await User.findByIdAndUpdate(userId, { | ||
$inc: { reputation: hasdownVoted ? -2 : 2 }, | ||
}); | ||
|
||
// decrement author's reputation by +S for downvoting/revoking an downvote to the answer (S = 10) | ||
await User.findByIdAndUpdate(answer.author, { | ||
$inc: { reputation: hasdownVoted ? -10 : 10 }, | ||
}); | ||
} | ||
|
||
revalidatePath(path); | ||
} catch (error) { | ||
console.log(error); | ||
throw error; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,90 @@ | ||
"use server"; | ||
|
||
import Question from "@/database/question.model"; | ||
import User from "@/database/user.model"; | ||
import Answer from "@/database/answer.model"; | ||
import Tag from "@/database/tag.model"; | ||
|
||
import { connectToDatabase } from "../mongoose"; | ||
import { SearchParams } from "./shared.types"; | ||
|
||
const SearchableTypes = ["question", "user", "answer", "tag"]; | ||
|
||
export async function globalSearch(params: SearchParams) { | ||
try { | ||
connectToDatabase(); | ||
|
||
const { query, type } = params; | ||
const regexQuery = { $regex: query, $options: "i" }; | ||
|
||
let results = []; | ||
|
||
const modelsAndTypes = [ | ||
{ model: Question, searchField: "title", type: "question" }, | ||
{ model: User, searchField: "name", type: "user" }, | ||
{ model: Answer, searchField: "content", type: "answer" }, | ||
{ model: Tag, searchField: "name", type: "tag" }, | ||
]; | ||
|
||
const typeLower = type?.toLowerCase(); | ||
|
||
if (!typeLower || !SearchableTypes.includes(typeLower)) { | ||
// Search across all types | ||
|
||
for (const { model, searchField, type } of modelsAndTypes) { | ||
const queryResults = await model | ||
.find({ [searchField]: regexQuery }) | ||
.limit(8); | ||
|
||
results.push( | ||
...queryResults.map((item) => ({ | ||
title: | ||
type === "answer" | ||
? `Answer containing "${query}"` | ||
: item[searchField], | ||
type, | ||
id: | ||
type === "user" | ||
? item.clerkId | ||
: type === "answer" | ||
? [item.question, item._id] | ||
: item._id, | ||
})) | ||
); | ||
} | ||
} else { | ||
// Search only in the specified model type | ||
|
||
const modelInfo = modelsAndTypes.find((item) => item.type === type); | ||
|
||
if (!modelInfo) { | ||
throw new Error("Invalid type specified"); | ||
} | ||
|
||
const queryResults = await modelInfo.model | ||
.find({ | ||
[modelInfo.searchField]: regexQuery, | ||
}) | ||
.limit(8); | ||
|
||
results = queryResults.map((item) => ({ | ||
title: | ||
type === "answer" | ||
? `Answers containing "${query}"` | ||
: item[modelInfo.searchField], | ||
type, | ||
id: | ||
type === "user" | ||
? item.clerkId | ||
: type === "answer" | ||
? [item.question, item._id] | ||
: item._id, | ||
})); | ||
} | ||
|
||
return JSON.stringify(results); | ||
} catch (error: any) { | ||
console.log(`Error fetching the global results: ${error}`); | ||
throw error; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,38 @@ | ||
"use server"; | ||
|
||
import Question from "@/database/question.model"; | ||
import Interaction from "@/database/interaction.model"; | ||
|
||
import { connectToDatabase } from "@/lib/mongoose"; | ||
|
||
import type { ViewQuestionParams } from "./shared.types"; | ||
|
||
export async function viewQuestion(params: ViewQuestionParams) { | ||
try { | ||
connectToDatabase(); | ||
|
||
const { questionId, userId } = params; | ||
|
||
// update view count for the question | ||
await Question.findByIdAndUpdate(questionId, { $inc: { views: 1 } }); | ||
|
||
if (userId) { | ||
const existingInteraction = await Interaction.findOne({ | ||
user: userId, | ||
action: "view", | ||
question: questionId, | ||
}); | ||
|
||
if (existingInteraction) return console.log("User has already viewed."); | ||
|
||
await Interaction.create({ | ||
user: userId, | ||
action: "view", | ||
question: questionId, | ||
}); | ||
} | ||
} catch (error) { | ||
console.log(error); | ||
throw error; | ||
} | ||
} |
Oops, something went wrong.