From 7ee3a6fd426a4d5da27e286e67a2a4199088cb0c Mon Sep 17 00:00:00 2001 From: hailia sommerville Date: Thu, 24 Jul 2025 22:27:33 -0400 Subject: [PATCH 1/9] feat: save draft btn + handler func --- src/components/VoteForm.jsx | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/src/components/VoteForm.jsx b/src/components/VoteForm.jsx index dbac1088..82ca26af 100644 --- a/src/components/VoteForm.jsx +++ b/src/components/VoteForm.jsx @@ -156,6 +156,28 @@ const VoteForm = ({ poll, readOnly = false }) => { } }; + const handleSaveDraft = async (e) => { + e.preventDefault(); + try { + const formattedRankings = orderedOptions.filter((opt) => !deletedOptions.has(opt.id)) + .map((opt, index) => ({ + optionId: opt.id, + rank: index + 1, + })); + + const res = await axios.patch(`${API_URL}/api/polls/${poll.id}/vote/${voteID}`, { + submitted: false, + rankings: formattedRankings, + }, + { + withCredentials: true}); + alert("Draft saved successfully!"); + } catch (error) { + console.error("Failed to save draft:", error); + setError("Failed to save draft. Please try again."); + } + } + return (

@@ -249,6 +271,9 @@ const VoteForm = ({ poll, readOnly = false }) => { + + + ); }; From 3fdbd648f13d281999108b75c9e9bac7c88b277f Mon Sep 17 00:00:00 2001 From: hailia sommerville Date: Thu, 24 Jul 2025 22:40:47 -0400 Subject: [PATCH 2/9] feat: use effect added to look for existing vote or create one to store voteId that is needed for patch route --- src/components/VoteForm.jsx | 30 ++++++++++++++++++++++++++++-- 1 file changed, 28 insertions(+), 2 deletions(-) diff --git a/src/components/VoteForm.jsx b/src/components/VoteForm.jsx index 82ca26af..596c8d12 100644 --- a/src/components/VoteForm.jsx +++ b/src/components/VoteForm.jsx @@ -1,5 +1,5 @@ import axios from "axios"; -import React, { useState, useEffect } from "react"; +import React, { useState, useEffect, use } from "react"; import { API_URL } from "../shared"; const VoteForm = ({ poll, readOnly = false }) => { @@ -13,6 +13,7 @@ const VoteForm = ({ poll, readOnly = false }) => { const [deletedOptions, setDeletedOptions] = useState(new Set()); const [submitted, setSubmitted] = useState(false); const [error, setError] = useState(null); + const [voteId, setVoteId] = useState(null); console.log("VoteForm rendered with poll:", poll); console.log("Poll options:", poll?.pollOptions); @@ -50,6 +51,31 @@ const VoteForm = ({ poll, readOnly = false }) => { setRankings(newRankings); }, [orderedOptions, deletedOptions]); + useEffect(() => { + const fetchOrCreateVote = async () => { + try { + const res = await axios.get(`${API_URL}/api/polls/${poll.id}/vote`, { + withCredentials: true, + }); + if (res.data) { + setVoteId(res.data.id); //vote exists + } else { + // create vote + const createRes = await axios.post(`${API_URL}/api/polls/${poll.id}/vote`, { + submitted: false, + rankings: [], + }, { + withCredentials: true, + }); + setVoteId(createRes.data.id); //new vote created + } + } catch (error) { + console.error("Failed to fetch or create vote:", error); + } + }; + if (!readOnly) fetchOrCreateVote(); + }, []); + if (!poll) { return
Loading poll data...
; } @@ -165,7 +191,7 @@ const VoteForm = ({ poll, readOnly = false }) => { rank: index + 1, })); - const res = await axios.patch(`${API_URL}/api/polls/${poll.id}/vote/${voteID}`, { + const res = await axios.patch(`${API_URL}/api/polls/${poll.id}/vote/${voteId}`, { submitted: false, rankings: formattedRankings, }, From 9fbd68eadeb3722c204d66c14174bfcf29d58be5 Mon Sep 17 00:00:00 2001 From: hailia sommerville Date: Fri, 25 Jul 2025 00:51:59 -0400 Subject: [PATCH 3/9] feat: save draft functionality implemented --- src/components/VoteForm.jsx | 89 ++++++++++++++++++++++++------------- 1 file changed, 59 insertions(+), 30 deletions(-) diff --git a/src/components/VoteForm.jsx b/src/components/VoteForm.jsx index 596c8d12..47e861f2 100644 --- a/src/components/VoteForm.jsx +++ b/src/components/VoteForm.jsx @@ -53,28 +53,50 @@ const VoteForm = ({ poll, readOnly = false }) => { useEffect(() => { const fetchOrCreateVote = async () => { + if (!poll?.id || readOnly) return; + try { + // Try to fetch vote const res = await axios.get(`${API_URL}/api/polls/${poll.id}/vote`, { withCredentials: true, }); - if (res.data) { - setVoteId(res.data.id); //vote exists - } else { - // create vote + + const voteData = res.data; + setVoteId(voteData.id); + + // Restore saved rankings if they exist + if (voteData.votingRanks) { + const restored = voteData.votingRanks.map(rank => ({ + optionId: rank.pollOptionId, + rank: rank.rank, + })); + + const restoredMap = {}; + restored.forEach(r => { + restoredMap[r.optionId] = r.rank; + }); + + setRankings(restoredMap); + } + } catch (err) { + // Vote doesn't exist, so create it + try { const createRes = await axios.post(`${API_URL}/api/polls/${poll.id}/vote`, { submitted: false, rankings: [], - }, { - withCredentials: true, - }); - setVoteId(createRes.data.id); //new vote created + }, { withCredentials: true }); + + setVoteId(createRes.data.id); + } catch (createErr) { + console.error("Failed to create vote:", createErr); } - } catch (error) { - console.error("Failed to fetch or create vote:", error); } }; - if (!readOnly) fetchOrCreateVote(); - }, []); + + fetchOrCreateVote(); + }, [poll?.id, readOnly]); + + if (!poll) { return
Loading poll data...
; @@ -156,7 +178,7 @@ const VoteForm = ({ poll, readOnly = false }) => { - await fetch(`${API_URL}/api/polls/${poll.id}/votes`, { + await fetch(`${API_URL}/api/polls/${poll.id}/vote`, { method: "POST", headers: { "Content-Type": "application/json" }, @@ -184,25 +206,32 @@ const VoteForm = ({ poll, readOnly = false }) => { const handleSaveDraft = async (e) => { e.preventDefault(); + if (!voteId) { + setError("No voteId – cannot save draft."); + return; + } + try { - const formattedRankings = orderedOptions.filter((opt) => !deletedOptions.has(opt.id)) - .map((opt, index) => ({ - optionId: opt.id, - rank: index + 1, - })); - - const res = await axios.patch(`${API_URL}/api/polls/${poll.id}/vote/${voteId}`, { - submitted: false, - rankings: formattedRankings, - }, - { - withCredentials: true}); - alert("Draft saved successfully!"); - } catch (error) { - console.error("Failed to save draft:", error); - setError("Failed to save draft. Please try again."); + const formattedRankings = orderedOptions + .filter(opt => !deletedOptions.has(opt.id)) + .map((opt, index) => ({ + optionId: opt.id, + rank: index + 1, + })); + + await axios.patch(`${API_URL}/api/polls/${poll.id}/vote/${voteId}`, { + submitted: false, + rankings: formattedRankings, + }, { withCredentials: true }); + + alert("Draft saved successfully!"); + setSubmitted(false); + } catch (err) { + console.error("Failed to save draft:", err); + setError("Failed to save draft. Please try again."); } - } + }; + return (
From 3ade307316af333980e9ff8a0099fb300157d375 Mon Sep 17 00:00:00 2001 From: hailia sommerville Date: Fri, 25 Jul 2025 14:35:27 -0400 Subject: [PATCH 4/9] fix: dragged order now persists when you return to draft --- src/components/VoteForm.jsx | 128 +++++++++++++++++++----------------- 1 file changed, 68 insertions(+), 60 deletions(-) diff --git a/src/components/VoteForm.jsx b/src/components/VoteForm.jsx index 47e861f2..026b18f8 100644 --- a/src/components/VoteForm.jsx +++ b/src/components/VoteForm.jsx @@ -4,12 +4,12 @@ import { API_URL } from "../shared"; const VoteForm = ({ poll, readOnly = false }) => { const [rankings, setRankings] = useState({}); - console.log("this is rankins---->", rankings) + console.log("this is rankins---->", rankings); const [submitting, setSubmitting] = useState(false); const [orderedOptions, setOrderedOptions] = useState([]); - console.log("this is ordered options", orderedOptions) + console.log("this is ordered options", orderedOptions); const [draggedItem, setDraggedItem] = useState(null); - console.log("dragged--->", draggedItem) + console.log("dragged--->", draggedItem); const [deletedOptions, setDeletedOptions] = useState(new Set()); const [submitted, setSubmitted] = useState(false); const [error, setError] = useState(null); @@ -20,83 +20,95 @@ const VoteForm = ({ poll, readOnly = false }) => { // Initialize ordered options when poll changes useEffect(() => { - if (poll?.pollOptions) { + if (poll?.pollOptions && orderedOptions.length === 0) { setOrderedOptions([...poll.pollOptions]); } }, [poll?.pollOptions]); // Update rankings whenever the order changes useEffect(() => { - let newRankings = []; - orderedOptions.forEach((option, index) => { - if (deletedOptions.has(option.id)) { - // Keep deleted options as null for algorrithm - newRankings[option.id] = null; - } else { - // Find the position among non-deleted options - const nonDeletedBefore = orderedOptions - .slice(0, index) - .filter((opt) => !deletedOptions.has(opt.id)).length; - - newRankings.push({ - optionId: option.id, - rank: nonDeletedBefore + 1 - }) - - // newRankings.optionId = option.id, - // newRankings.ranking = index - // newRankings.optionId = option.id - } - }); + const newRankings = {}; + let currentRank = 1; + + orderedOptions.forEach((option) => { + if (deletedOptions.has(option.id)) { + newRankings[option.id] = null; + } else { + newRankings[option.id] = currentRank++; + } + }); + setRankings(newRankings); }, [orderedOptions, deletedOptions]); useEffect(() => { const fetchOrCreateVote = async () => { if (!poll?.id || readOnly) return; - + try { // Try to fetch vote const res = await axios.get(`${API_URL}/api/polls/${poll.id}/vote`, { withCredentials: true, }); - + const voteData = res.data; setVoteId(voteData.id); - + // Restore saved rankings if they exist if (voteData.votingRanks) { - const restored = voteData.votingRanks.map(rank => ({ + const restored = voteData.votingRanks.map((rank) => ({ optionId: rank.pollOptionId, rank: rank.rank, })); - + const restoredMap = {}; - restored.forEach(r => { + restored.forEach((r) => { restoredMap[r.optionId] = r.rank; }); - + setRankings(restoredMap); + + // Set ordered options based on restored rankings + + if (poll?.pollOptions) { + const sortedOptions = [...poll.pollOptions] + .filter(opt => restoredMap[opt.id] !== undefined && restoredMap[opt.id] !== null) + .sort((a, b) => restoredMap[a.id] - restoredMap[b.id]); + + const unranked = poll.pollOptions.filter(opt => restoredMap[opt.id] === undefined); + + setOrderedOptions([...sortedOptions, ...unranked]); + + const deleted = new Set( + poll.pollOptions + .filter(opt => restoredMap[opt.id] === null) + .map(opt => opt.id) + ); + setDeletedOptions(deleted); + } } + } catch (err) { // Vote doesn't exist, so create it try { - const createRes = await axios.post(`${API_URL}/api/polls/${poll.id}/vote`, { - submitted: false, - rankings: [], - }, { withCredentials: true }); - + const createRes = await axios.post( + `${API_URL}/api/polls/${poll.id}/vote`, + { + submitted: false, + rankings: [], + }, + { withCredentials: true } + ); + setVoteId(createRes.data.id); } catch (createErr) { console.error("Failed to create vote:", createErr); } } }; - + fetchOrCreateVote(); }, [poll?.id, readOnly]); - - if (!poll) { return
Loading poll data...
; @@ -175,11 +187,7 @@ const VoteForm = ({ poll, readOnly = false }) => { setSubmitting(true); try { - - - await fetch(`${API_URL}/api/polls/${poll.id}/vote`, { - method: "POST", headers: { "Content-Type": "application/json" }, credentials: "include", @@ -210,20 +218,24 @@ const VoteForm = ({ poll, readOnly = false }) => { setError("No voteId – cannot save draft."); return; } - + try { const formattedRankings = orderedOptions - .filter(opt => !deletedOptions.has(opt.id)) + .filter((opt) => !deletedOptions.has(opt.id)) .map((opt, index) => ({ optionId: opt.id, rank: index + 1, })); - - await axios.patch(`${API_URL}/api/polls/${poll.id}/vote/${voteId}`, { - submitted: false, - rankings: formattedRankings, - }, { withCredentials: true }); - + + await axios.patch( + `${API_URL}/api/polls/${poll.id}/vote/${voteId}`, + { + submitted: false, + rankings: formattedRankings, + }, + { withCredentials: true } + ); + alert("Draft saved successfully!"); setSubmitted(false); } catch (err) { @@ -231,7 +243,6 @@ const VoteForm = ({ poll, readOnly = false }) => { setError("Failed to save draft. Please try again."); } }; - return ( @@ -247,8 +258,9 @@ const VoteForm = ({ poll, readOnly = false }) => { return (
!isDeleted && handleDragStart(e, index)} onDragOver={(e) => !isDeleted && handleDragOver(e, index)} @@ -315,10 +327,7 @@ const VoteForm = ({ poll, readOnly = false }) => {
{error && ( -
+
{error}
)} @@ -328,7 +337,6 @@ const VoteForm = ({ poll, readOnly = false }) => { - ); }; From 6c7eb683bc8d0fe01bdabb826a9a8f3ece1c772c Mon Sep 17 00:00:00 2001 From: hailia sommerville Date: Fri, 25 Jul 2025 18:20:43 -0400 Subject: [PATCH 5/9] feat: draft btn is disabled if no changes were made --- src/components/VoteForm.jsx | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/src/components/VoteForm.jsx b/src/components/VoteForm.jsx index 026b18f8..f74da8cf 100644 --- a/src/components/VoteForm.jsx +++ b/src/components/VoteForm.jsx @@ -14,6 +14,8 @@ const VoteForm = ({ poll, readOnly = false }) => { const [submitted, setSubmitted] = useState(false); const [error, setError] = useState(null); const [voteId, setVoteId] = useState(null); + const [movedOptionIds, setMovedOptionIds] = useState(new Set()); + console.log("VoteForm rendered with poll:", poll); console.log("Poll options:", poll?.pollOptions); @@ -163,6 +165,16 @@ const VoteForm = ({ poll, readOnly = false }) => { setOrderedOptions(newOrderedOptions); setDraggedItem(index); + + setMovedOptionIds((prev) => { + const updated = new Set(prev); + if (!updated.has(draggedOption.id)) { + console.log(`Option "${draggedOption.optionText}" (id: ${draggedOption.id}) moved for the first time`); + } + updated.add(draggedOption.id); + return updated; + }); + } }; @@ -336,7 +348,7 @@ const VoteForm = ({ poll, readOnly = false }) => { Submit Vote - + ); }; From 51242d4f0f7a3eefbffee19023e5fb2e17f8dc4d Mon Sep 17 00:00:00 2001 From: hailia sommerville Date: Fri, 25 Jul 2025 18:37:03 -0400 Subject: [PATCH 6/9] feat: pop up asking user if they want to submit without ranking all --- src/components/VoteForm.jsx | 73 +++++++++++++++++++++++++------------ 1 file changed, 49 insertions(+), 24 deletions(-) diff --git a/src/components/VoteForm.jsx b/src/components/VoteForm.jsx index f74da8cf..4b4cc165 100644 --- a/src/components/VoteForm.jsx +++ b/src/components/VoteForm.jsx @@ -15,7 +15,7 @@ const VoteForm = ({ poll, readOnly = false }) => { const [error, setError] = useState(null); const [voteId, setVoteId] = useState(null); const [movedOptionIds, setMovedOptionIds] = useState(new Set()); - + const [moveWarning, setMoveWarning] = useState(""); console.log("VoteForm rendered with poll:", poll); console.log("Poll options:", poll?.pollOptions); @@ -30,16 +30,16 @@ const VoteForm = ({ poll, readOnly = false }) => { // Update rankings whenever the order changes useEffect(() => { const newRankings = {}; - let currentRank = 1; + let currentRank = 1; + + orderedOptions.forEach((option) => { + if (deletedOptions.has(option.id)) { + newRankings[option.id] = null; + } else { + newRankings[option.id] = currentRank++; + } + }); - orderedOptions.forEach((option) => { - if (deletedOptions.has(option.id)) { - newRankings[option.id] = null; - } else { - newRankings[option.id] = currentRank++; - } - }); - setRankings(newRankings); }, [orderedOptions, deletedOptions]); @@ -62,34 +62,39 @@ const VoteForm = ({ poll, readOnly = false }) => { optionId: rank.pollOptionId, rank: rank.rank, })); - + const restoredMap = {}; restored.forEach((r) => { restoredMap[r.optionId] = r.rank; }); - + setRankings(restoredMap); // Set ordered options based on restored rankings - + if (poll?.pollOptions) { const sortedOptions = [...poll.pollOptions] - .filter(opt => restoredMap[opt.id] !== undefined && restoredMap[opt.id] !== null) + .filter( + (opt) => + restoredMap[opt.id] !== undefined && + restoredMap[opt.id] !== null + ) .sort((a, b) => restoredMap[a.id] - restoredMap[b.id]); - - const unranked = poll.pollOptions.filter(opt => restoredMap[opt.id] === undefined); - + + const unranked = poll.pollOptions.filter( + (opt) => restoredMap[opt.id] === undefined + ); + setOrderedOptions([...sortedOptions, ...unranked]); - + const deleted = new Set( poll.pollOptions - .filter(opt => restoredMap[opt.id] === null) - .map(opt => opt.id) + .filter((opt) => restoredMap[opt.id] === null) + .map((opt) => opt.id) ); setDeletedOptions(deleted); } } - } catch (err) { // Vote doesn't exist, so create it try { @@ -169,12 +174,14 @@ const VoteForm = ({ poll, readOnly = false }) => { setMovedOptionIds((prev) => { const updated = new Set(prev); if (!updated.has(draggedOption.id)) { - console.log(`Option "${draggedOption.optionText}" (id: ${draggedOption.id}) moved for the first time`); + console.log( + `Option "${draggedOption.optionText}" (id: ${draggedOption.id}) moved for the first time` + ); } updated.add(draggedOption.id); + setMoveWarning(""); return updated; }); - } }; @@ -196,7 +203,23 @@ const VoteForm = ({ poll, readOnly = false }) => { const handleSubmit = async (e) => { e.preventDefault(); + + const rankingsAvailable = orderedOptions.filter( + (opt) => !deletedOptions.has(opt.id) + ).length; + + const hasRankedAll = movedOptionIds.length >= rankingsAvailable; + + // Show popup if not all options moved + if (!hasRankedAll) { + const confirmProceed = window.confirm( + "You haven't moved all options. Are you sure you want to submit anyway?" + ); + if (!confirmProceed) return; + } + setSubmitting(true); + setMoveWarning(""); try { await fetch(`${API_URL}/api/polls/${poll.id}/vote`, { @@ -348,7 +371,9 @@ const VoteForm = ({ poll, readOnly = false }) => { Submit Vote - + ); }; From 8a20cfc7839f77ff23aa57902d205dc5adf07f6c Mon Sep 17 00:00:00 2001 From: hailia sommerville Date: Fri, 25 Jul 2025 18:42:47 -0400 Subject: [PATCH 7/9] feat: buttons disabled with message if all options deleted --- src/components/VoteForm.jsx | 40 ++++++++++++++++++++++++------------- 1 file changed, 26 insertions(+), 14 deletions(-) diff --git a/src/components/VoteForm.jsx b/src/components/VoteForm.jsx index 4b4cc165..c8acb689 100644 --- a/src/components/VoteForm.jsx +++ b/src/components/VoteForm.jsx @@ -20,6 +20,10 @@ const VoteForm = ({ poll, readOnly = false }) => { console.log("VoteForm rendered with poll:", poll); console.log("Poll options:", poll?.pollOptions); + const allDeleted = + orderedOptions.length > 0 && + orderedOptions.every((opt) => deletedOptions.has(opt.id)); // Check if all options are deleted + // Initialize ordered options when poll changes useEffect(() => { if (poll?.pollOptions && orderedOptions.length === 0) { @@ -173,11 +177,6 @@ const VoteForm = ({ poll, readOnly = false }) => { setMovedOptionIds((prev) => { const updated = new Set(prev); - if (!updated.has(draggedOption.id)) { - console.log( - `Option "${draggedOption.optionText}" (id: ${draggedOption.id}) moved for the first time` - ); - } updated.add(draggedOption.id); setMoveWarning(""); return updated; @@ -210,13 +209,13 @@ const VoteForm = ({ poll, readOnly = false }) => { const hasRankedAll = movedOptionIds.length >= rankingsAvailable; - // Show popup if not all options moved - if (!hasRankedAll) { - const confirmProceed = window.confirm( - "You haven't moved all options. Are you sure you want to submit anyway?" - ); - if (!confirmProceed) return; - } + // Show popup if not all options moved + if (!hasRankedAll) { + const confirmProceed = window.confirm( + "You haven't ranked all options. Are you sure you want to submit anyway?" + ); + if (!confirmProceed) return; + } setSubmitting(true); setMoveWarning(""); @@ -367,11 +366,24 @@ const VoteForm = ({ poll, readOnly = false }) => {
)} - - From ede9697b93dcf72782b84414694652e244fa11de Mon Sep 17 00:00:00 2001 From: hailia sommerville Date: Fri, 25 Jul 2025 19:09:02 -0400 Subject: [PATCH 8/9] fix: pop up still checking even after all opts ranked --- src/components/VoteForm.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/VoteForm.jsx b/src/components/VoteForm.jsx index c8acb689..99c62fdd 100644 --- a/src/components/VoteForm.jsx +++ b/src/components/VoteForm.jsx @@ -207,7 +207,7 @@ const VoteForm = ({ poll, readOnly = false }) => { (opt) => !deletedOptions.has(opt.id) ).length; - const hasRankedAll = movedOptionIds.length >= rankingsAvailable; + const hasRankedAll = movedOptionIds.size >= rankingsAvailable; // Show popup if not all options moved if (!hasRankedAll) { From f7f56ad0fef43fc5325371806a72a0cb6c1b2d5d Mon Sep 17 00:00:00 2001 From: hailia sommerville Date: Fri, 25 Jul 2025 21:07:18 -0400 Subject: [PATCH 9/9] fix: pop up still showed up (again) --- src/components/VoteForm.jsx | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/src/components/VoteForm.jsx b/src/components/VoteForm.jsx index 99c62fdd..735128d3 100644 --- a/src/components/VoteForm.jsx +++ b/src/components/VoteForm.jsx @@ -203,11 +203,11 @@ const VoteForm = ({ poll, readOnly = false }) => { const handleSubmit = async (e) => { e.preventDefault(); - const rankingsAvailable = orderedOptions.filter( - (opt) => !deletedOptions.has(opt.id) - ).length; - - const hasRankedAll = movedOptionIds.size >= rankingsAvailable; + + const hasRankedAll = orderedOptions + .filter((opt) => !deletedOptions.has(opt.id)) + .every((opt) => movedOptionIds.has(opt.id)); + // Show popup if not all options moved if (!hasRankedAll) { @@ -217,6 +217,14 @@ const VoteForm = ({ poll, readOnly = false }) => { if (!confirmProceed) return; } + // Convert rankings object to array format + const formattedRankings = Object.entries(rankings) + .filter(([_, rank]) => rank !== null) // filter out nulls if needed + .map(([optionId, rank]) => ({ + optionId: parseInt(optionId), + rank: rank + })); + setSubmitting(true); setMoveWarning(""); @@ -227,7 +235,7 @@ const VoteForm = ({ poll, readOnly = false }) => { credentials: "include", body: JSON.stringify({ pollId: poll.id, - rankings: rankings, + rankings: formattedRankings, }), }); // await axios.post("http://localhost:8080/api/:pollId/vote",