diff --git a/backend/src/controllers/poll.controller.js b/backend/src/controllers/poll.controller.js index 16b45f5..db18588 100644 --- a/backend/src/controllers/poll.controller.js +++ b/backend/src/controllers/poll.controller.js @@ -1,5 +1,6 @@ import { createPollService, + createVoteService, deletePollService, getAllCreatedPollsService, getPollDataService, @@ -105,3 +106,34 @@ export async function deletePollController(req, res) { } } } + + +export const createVoteController = async (req, res) => { + try { + const reqPollId = req.body.pollId; + const reqOptionId = req.body.optionId; + const reqUserId = req.user._id; + + const vote = await createVoteService(reqPollId, reqUserId, reqOptionId); + + res.json({ + success : true, + message : "Vote created successfully.", + data : vote + }) + } + catch (err) { + console.log(err); + if (err.statusCode) { + res.status(err.statusCode).json({ + success: false, + message: err.message, + }); + } else { + res.status(500).json({ + success: false, + message: err.message, + }); + } + } +} \ No newline at end of file diff --git a/backend/src/models/vote.model.js b/backend/src/models/vote.model.js new file mode 100644 index 0000000..98b96f4 --- /dev/null +++ b/backend/src/models/vote.model.js @@ -0,0 +1,23 @@ +import mongoose, { Schema } from "mongoose"; + +const voteSchema = new Schema({ + pollId : { + type : Schema.Types.ObjectId, + ref : "Poll", + required : true + }, + userId : { + type : Schema.Types.ObjectId, + ref : "User", + required : true + }, + optionId : { + type : Schema.Types.ObjectId, + ref : "Option", + required : true + }, +}, {timestamps : true}); + + +const VoteModel = mongoose.model("Vote", voteSchema); +export default VoteModel; \ No newline at end of file diff --git a/backend/src/repositories/poll.repo.js b/backend/src/repositories/poll.repo.js index 868a970..7f85943 100644 --- a/backend/src/repositories/poll.repo.js +++ b/backend/src/repositories/poll.repo.js @@ -1,4 +1,6 @@ import PollModel from "../models/poll.model.js"; +import mongoose from "mongoose"; +const {ObjectId} = mongoose.Types; export async function createPollByData(data) { try { @@ -38,4 +40,21 @@ export async function deletePollById(id) { catch(err){ throw err; } +} + +export async function updatePollVoteCount(pollId, optionId) { + try { + const pollIdObejct = new ObjectId(pollId); + const optionIdObject = new ObjectId(optionId); + console.log(pollId, optionId, pollIdObejct, optionIdObject); + const result = await PollModel.updateOne( + { _id: pollIdObejct, "options._id": optionIdObject }, + { $inc: { "options.$.voteCount": 1 } } + ); + + return result; + } + catch(err){ + throw err; + } } \ No newline at end of file diff --git a/backend/src/repositories/vote.repo.js b/backend/src/repositories/vote.repo.js new file mode 100644 index 0000000..6a6fe88 --- /dev/null +++ b/backend/src/repositories/vote.repo.js @@ -0,0 +1,27 @@ +import VoteModel from "../models/vote.model.js"; + +export async function findVoteByPollIdAndUserId(pollId, userId) { + try { + const vote = VoteModel.findOne({pollId : pollId, userId : userId}); + return vote; + } + catch(err){ + throw err; + } +} + + +export async function createVote(pollId, userId, optionId) { + try{ + const vote = VoteModel.create({ + pollId, + userId, + optionId + }); + + return vote; + } + catch(err){ + throw err; + } +} \ No newline at end of file diff --git a/backend/src/routes/v1/poll.route.js b/backend/src/routes/v1/poll.route.js index f80fa87..d3e6dde 100644 --- a/backend/src/routes/v1/poll.route.js +++ b/backend/src/routes/v1/poll.route.js @@ -1,8 +1,9 @@ import express from "express"; import { verifyToken } from "../../middlwares/verifyToken.js"; -import { createPollController, deletePollController, getAllCreatedPollsController, getPollDataController } from "../../controllers/poll.controller.js"; +import { createPollController, createVoteController, deletePollController, getAllCreatedPollsController, getPollDataController } from "../../controllers/poll.controller.js"; import pollDataSchema from "../../validations/pollDataValidation.js"; import validator from "../../validations/validator.js"; +import voteSchema from "../../validations/voteValidation.js"; const pollRouter = express.Router(); /** @@ -122,4 +123,6 @@ pollRouter.get("/created", verifyToken, getAllCreatedPollsController); * */ pollRouter.delete("/delete/:pollId", verifyToken, deletePollController); +pollRouter.post("/vote", validator(voteSchema), verifyToken, createVoteController); + export default pollRouter; \ No newline at end of file diff --git a/backend/src/services/poll.service.js b/backend/src/services/poll.service.js index becbe4c..19b7bc3 100644 --- a/backend/src/services/poll.service.js +++ b/backend/src/services/poll.service.js @@ -1,11 +1,13 @@ import mongoose from "mongoose"; -import { createPollByData, deletePollById, findPollById, findPollsByCreatorId } from "../repositories/poll.repo.js"; +import { createPollByData, deletePollById, findPollById, findPollsByCreatorId, updatePollVoteCount } from "../repositories/poll.repo.js"; +import { createVote, findVoteByPollIdAndUserId } from "../repositories/vote.repo.js"; export async function createPollService(title, description, options, userId) { try { const optionsData = options.map(option => ({ name: option, - _id : new mongoose.Types.ObjectId() + _id : new mongoose.Types.ObjectId(), + voteCount : 0 })); const data = { @@ -71,6 +73,35 @@ export async function deletePollService(pollId, user) { return deletedPoll; } + catch(err){ + throw err; + } +} + +export async function createVoteService(pollId, userId, optionId) { + try { + // vote already voted + const vote = await findVoteByPollIdAndUserId(pollId, userId); + if (vote) { + throw { + statusCode: 400, + message: "You have already voted on this poll" + } + } + console.log(pollId, userId, optionId); + const poll = await findPollById(pollId); + if (!poll) { + throw { + statusCode: 404, + message: "Poll not found" + } + } + const updatedPollData = await updatePollVoteCount(pollId, optionId); + const createdVote = await createVote(pollId, userId, optionId); + console.log(updatedPollData) + return createVote; + + } catch(err){ throw err; } diff --git a/backend/src/validations/voteValidation.js b/backend/src/validations/voteValidation.js new file mode 100644 index 0000000..111fdc6 --- /dev/null +++ b/backend/src/validations/voteValidation.js @@ -0,0 +1,14 @@ +import { z } from "zod"; + +const voteSchema = z.object({ + pollId: z.string({ + required_error: "Poll Id is required.", + invalid_type_error: "Poll Id should be a string.", + }), + optionId: z.string({ + required_error: "Option Id is required.", + invalid_type_error: "Option Id should be a string.", + }), +}); + +export default voteSchema; \ No newline at end of file diff --git a/frontend/src/pages/VotingPage.jsx b/frontend/src/pages/VotingPage.jsx index 9e4fd6e..cfe3363 100644 --- a/frontend/src/pages/VotingPage.jsx +++ b/frontend/src/pages/VotingPage.jsx @@ -5,6 +5,7 @@ import { Chart as ChartJS, BarElement, CategoryScale, LinearScale } from "chart. import { useQuery } from "react-query"; import { useParams } from "react-router-dom"; import getPollData from "../services/getPollData"; +import ErrorFallback from "../components/Errors/ErrorFallback"; ChartJS.register(BarElement, CategoryScale, LinearScale); @@ -12,62 +13,57 @@ function VotingPage() { const { pollId } = useParams(); - const { data: poll, isLoading, isError } = useQuery(["poll", pollId], () => getPollData(pollId), { + const { data: poll, isLoading, isError, refetch } = useQuery(["poll", pollId], () => getPollData(pollId), { cacheTime : 10*100*60, // 10 minutes staleTime : 20*100*60, // 20 minutes }); - console.log(poll); - - const pollData = { - title: "What's your favorite programming language?", - options: [ - { id: 1, label: "JavaScript", votes: 20 }, - { id: 2, label: "Python", votes: 35 }, - { id: 3, label: "Java", votes: 25 }, - { id: 4, label: "C++", votes: 15 }, - ], - creator: { - name: "John Doe", - image: "https://via.placeholder.com/100", - }, - }; // Dummy chart data for visualization const chartData = { - labels: pollData.options.map(option => option.label), + labels: poll?.data?.pollData?.options.map(option => option.name), datasets: [ { label: "Votes", - data: pollData.options.map(option => option.votes), + data: poll?.data?.pollData?.options.map(option => option.voteCount), backgroundColor: ["#3B82F6", "#EF4444", "#10B981", "#F59E0B"], borderWidth: 1, }, ], }; + if (isLoading) { + return
; + } + + if (isError){ + return