diff --git a/.github/workflows/aws_dev_release.yml b/.github/workflows/aws_dev_release.yml index 9674563..46ddb74 100644 --- a/.github/workflows/aws_dev_release.yml +++ b/.github/workflows/aws_dev_release.yml @@ -62,8 +62,6 @@ jobs: - name: Login to Amazon ECR id: login-ecr uses: aws-actions/amazon-ecr-login@v2 - with: - mask-password: 'false' - name: Image Metadata id: metadata diff --git a/.github/workflows/aws_prod_release.yml b/.github/workflows/aws_prod_release.yml index 4b375da..43caaf6 100644 --- a/.github/workflows/aws_prod_release.yml +++ b/.github/workflows/aws_prod_release.yml @@ -66,8 +66,6 @@ jobs: - name: Login to Amazon ECR id: login-ecr uses: aws-actions/amazon-ecr-login@v2 - with: - mask-password: 'false' - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 diff --git a/client/src/components/AdminView.tsx b/client/src/components/AdminView.tsx index 8b35bb4..107c279 100644 --- a/client/src/components/AdminView.tsx +++ b/client/src/components/AdminView.tsx @@ -10,135 +10,146 @@ import { backendAPI } from "@/utils/backendAPI"; import { GlobalDispatchContext, GlobalStateContext } from "@/context/GlobalContext"; import { SET_POLL } from "@/context/types"; -interface PollFormInputs { - question: string; - answer1: string; - answer2: string; - answer3: string; - answer4: string; - answer5: string; - displayMode: "percentage" | "count"; -} - /* The AdminView component is where admins can set the poll question, options, and display mode (or reset the poll). It is displayed upon clicking the settings icon in the header. */ export const AdminView = () => { - const dispatch = useContext(GlobalDispatchContext); + const dispatch = useContext(GlobalDispatchContext)!; + const { poll } = useContext(GlobalStateContext); + + const pollOptionMaxTextLength = 100; + const pollQuestionMaxTextLength = 150; + const maxOptions = 10; + + // state + const [question, setQuestion] = useState(""); + const [options, setOptions] = useState(["", ""]); + const [displayMode, setDisplayMode] = useState<"percentage" | "count">("percentage"); + + const [origQuestion, setOrigQuestion] = useState(""); + const [origOptions, setOrigOptions] = useState([]); const [showConfirmationModal, setShowConfirmationModal] = useState(false); const [pendingAction, setPendingAction] = useState void)>(null); const [errorMessage, setErrorMessage] = useState(""); - const [formData, setFormData] = useState({ - question: "", - answer1: "", - answer2: "", - answer3: "", - answer4: "", - answer5: "", - displayMode: "percentage", - }); const [isSubmitting, setIsSubmitting] = useState(false); - const [modalType, setModalType] = useState<"save" | "reset" | null>(null); - const { poll } = useContext(GlobalStateContext); + const [modalType, setModalType] = useState<"crucialSave" | "reset" | "nonCrucialSave" | null>("nonCrucialSave"); - function handleToggleShowConfirmationModal() { - setShowConfirmationModal(!showConfirmationModal); - if (showConfirmationModal) { - setPendingAction(null); // Reset pending action when modal is closed - } - } - - // Updates the formData field matching the input/select’s name with its new value - const handleChange = (e: React.ChangeEvent) => { - const { name, value } = e.target; - setFormData((prevState) => ({ ...prevState, [name]: value })); + const handleToggleShowConfirmationModal = () => { + setShowConfirmationModal((v) => !v); + if (showConfirmationModal) setPendingAction(null); }; - // This function will be called when the user clicks the "Save" button - const handleSubmitPoll = async () => { - setErrorMessage(""); + useEffect(() => { + if (!poll) return; + // pull the question + the answers array + const { question, answers, displayMode } = poll; - // Validate that the question is not empty - if (formData.question.trim() === "") { - setErrorMessage("Poll question is required."); - return; + setQuestion(question); + setOptions(answers.length > 0 ? poll.answers : ["", ""]); + setDisplayMode(displayMode === "count" ? "count" : "percentage"); + + setOrigQuestion(poll.question); + setOrigOptions(poll.answers); + + setModalType("nonCrucialSave"); + }, [poll]); + + const validOptions = options.filter((o) => o.trim() !== ""); + const isValid = question.trim() !== "" && validOptions.length >= 2; + + // handlers + const addOption = () => { + if (options.length < maxOptions) { + setOptions((prev) => [...prev, ""]); } + }; - // Create an array of poll options from the formData - const options = [formData.answer1, formData.answer2, formData.answer3, formData.answer4, formData.answer5]; + // detect edits for modal type changes + useEffect(() => { + const trimmedOpts = options.map((o) => o.trim()); + const trimmedOrig = origOptions.map((o) => o.trim()); - // Filter out any options that are empty or only whitespace - const validOptions = options.filter((option) => option.trim() !== ""); + const questionChanged = question.trim() !== origQuestion.trim(); + const optionsChanged = + trimmedOpts.length !== trimmedOrig.length || trimmedOpts.some((o, i) => o !== trimmedOrig[i]); - // Error if less than 2 options are provided - if (validOptions.length < 2) { - setErrorMessage("At least two options are required."); - return; + if (questionChanged || optionsChanged) { + setModalType("crucialSave"); + } else { + setModalType("nonCrucialSave"); } + }, [question, options, origQuestion, origOptions]); - // If validation is successful, proceed with the API call + // This function will be called when the user clicks the yes in the modal + const handleSubmitPoll = async () => { setIsSubmitting(true); - - // Overrides the poll with a backend PUT request - backendAPI - .put("/poll", formData) - .then((res) => { - console.log("Poll updated successfully"); - const poll = res.data.poll; - dispatch!({ - type: SET_POLL, - payload: { poll }, - }); - }) - .catch((error) => setErrorMessage(error?.response?.data?.message || error.message || "Error updating poll")) - .finally(() => setIsSubmitting(false)); + try { + const payload = + modalType === "crucialSave" + ? { question, answers: options, displayMode, crucial: true } + : { displayMode, crucial: false }; + const res = await backendAPI.put("/poll", payload); + dispatch({ + type: SET_POLL, + payload: { poll: res.data.poll }, + }); + } catch (err: any) { + setErrorMessage(err?.response?.data?.message || err.message || "Error updating poll"); + } finally { + setIsSubmitting(false); + handleToggleShowConfirmationModal(); + } }; // Resets the data object for the dropped Asset (including the current poll) using backend POST const handleResetPoll = async () => { setIsSubmitting(true); - setErrorMessage(""); - - backendAPI - .post("admin/reset") - .then(() => { - console.log("Poll reset successfully"); - }) - .catch((error) => setErrorMessage(error?.response?.data?.message || error.message || "Error resetting poll")) - .finally(() => setIsSubmitting(false)); + try { + await backendAPI.post("admin/reset"); + } catch (err: any) { + setErrorMessage(err?.response?.data?.message || err.message || "Error resetting poll"); + } finally { + setIsSubmitting(false); + handleToggleShowConfirmationModal(); + } }; // Get the confirmation modal for the Save button const handleSaveClick = () => { + setErrorMessage(""); + if (!isValid) { + setErrorMessage("Question is required and at least two options must be non‑empty."); + return; + } setPendingAction(() => handleSubmitPoll); - setModalType("save"); // using new generic modal - setShowConfirmationModal(true); + handleToggleShowConfirmationModal(); }; // Get the confirmation modal for the Reset button const handleResetClick = () => { setPendingAction(() => handleResetPoll); - setModalType("reset"); // using the same new generic modal - setShowConfirmationModal(true); + setModalType("reset"); + handleToggleShowConfirmationModal(); }; - useEffect(() => { - if (!poll) return; - // pull the question + the answers array - const { question, answers, displayMode } = poll; + const getModalTitle = () => { + switch (modalType) { + case "crucialSave": + return `Override poll?`; + case "nonCrucialSave": + return `Update poll?`; + case "reset": + return `Reset poll?`; + } + return `Override poll?`; + }; - setFormData({ - question, - answer1: answers[0] || "", - answer2: answers[1] || "", - answer3: answers[2] || "", - answer4: answers[3] || "", - answer5: answers[4] || "", - displayMode: displayMode === "count" ? "count" : "percentage", - }); - }, [poll]); + const getModalMessage = () => { + if (modalType === "nonCrucialSave") return "No data will be lost."; + return "Current poll data and results will be erased."; + }; return (
@@ -152,38 +163,59 @@ export const AdminView = () => { id="titleInput" className="input" name="question" - value={formData.question} - onChange={handleChange} - maxLength={150} + value={question} + onChange={(e) => setQuestion(e.target.value)} + maxLength={pollQuestionMaxTextLength} /> - {formData.question.length}/150 + + {question.length}/{pollQuestionMaxTextLength} +
- {["answer1", "answer2", "answer3", "answer4", "answer5"].map((field, index) => ( -
- - - {formData[field as keyof PollFormInputs].length}/16 + {/* poll options */} +
+ {options.map((opt, i) => ( +
+ +
+ { + const copy = [...options]; + copy[i] = e.target.value; + setOptions(copy); + }} + maxLength={pollOptionMaxTextLength} + placeholder={`Option ${i + 1}`} + /> +
+ + {opt.length}/{pollOptionMaxTextLength} + +
+ ))} +
+ + {/* add poll option */} + {options.length < maxOptions && ( +
+
- ))} + )}

Results Display

- {/* Using flex and gap to separate the radio buttons */}