diff --git a/package-lock.json b/package-lock.json index 3e3e01f..2c26f8d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -32,7 +32,7 @@ "@testing-library/react": "^16.3.2", "@types/react": "^19.2.7", "@types/react-dom": "^19.2.3", - "@vitejs/plugin-react": "^5.1.1", + "@vitejs/plugin-react": "^5.2.0", "autoprefixer": "^10.4.27", "eslint": "^9.39.1", "eslint-plugin-react-hooks": "^7.0.1", @@ -2179,9 +2179,9 @@ "license": "MIT" }, "node_modules/@vitejs/plugin-react": { - "version": "5.1.4", - "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-5.1.4.tgz", - "integrity": "sha512-VIcFLdRi/VYRU8OL/puL7QXMYafHmqOnwTZY50U1JPlCNj30PxCMx65c494b1K9be9hX83KVt0+gTEwTWLqToA==", + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-5.2.0.tgz", + "integrity": "sha512-YmKkfhOAi3wsB1PhJq5Scj3GXMn3WvtQ/JC0xoopuHoXSdmtdStOpFrYaT1kie2YgFBcIe64ROzMYRjCrYOdYw==", "dev": true, "license": "MIT", "dependencies": { @@ -2196,7 +2196,7 @@ "node": "^20.19.0 || >=22.12.0" }, "peerDependencies": { - "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" } }, "node_modules/@vitest/expect": { diff --git a/package.json b/package.json index 747e9f4..d940855 100644 --- a/package.json +++ b/package.json @@ -36,7 +36,7 @@ "@testing-library/react": "^16.3.2", "@types/react": "^19.2.7", "@types/react-dom": "^19.2.3", - "@vitejs/plugin-react": "^5.1.1", + "@vitejs/plugin-react": "^5.2.0", "autoprefixer": "^10.4.27", "eslint": "^9.39.1", "eslint-plugin-react-hooks": "^7.0.1", diff --git a/src/components/campaigns/steps/BasicInfoStep.jsx b/src/components/campaigns/steps/BasicInfoStep.jsx new file mode 100644 index 0000000..a20d18b --- /dev/null +++ b/src/components/campaigns/steps/BasicInfoStep.jsx @@ -0,0 +1,202 @@ +import { useEffect, useRef } from "react"; +import { useDispatch, useSelector } from "react-redux"; +import { useForm } from "react-hook-form"; +import { yupResolver } from "@hookform/resolvers/yup"; +import * as yup from "yup"; +import { + selectDraftCampaign, + updateDraftCampaign, +} from "../../../features/campaigns/campaignsSlice"; + +const CATEGORIES = [ + "Education", + "Health", + "Environment", + "Disaster Relief", + "Community", +]; + +const schema = yup.object({ + title: yup + .string() + .required("Title is required") + .max(100, "Must be 100 characters or less"), + category: yup.string().required("Category is required"), + shortDescription: yup + .string() + .required("Short description is required") + .max(200, "Must be 200 characters or less"), + fullStory: yup.string().required("Full story is required"), +}); + +const BasicInfoStep = ({ validationRef }) => { + const dispatch = useDispatch(); + const draft = useSelector(selectDraftCampaign); + + const { register, handleSubmit, watch, trigger, formState, setValue } = + useForm({ + resolver: yupResolver(schema), + defaultValues: { + title: draft.title || "", + category: draft.category || "", + shortDescription: draft.description || "", + fullStory: draft.fullStory || draft.description || "", + }, + mode: "onChange", + }); + + // Keep local form values in sync if draft changes externally (e.g., from Redux store updates) + useEffect(() => { + setValue("title", draft.title || ""); + + setValue("category", draft.category || ""); + setValue("shortDescription", draft.description || ""); + setValue("fullStory", draft.fullStory || draft.description || ""); + }, [draft, setValue]); + + // Debounced autosave to Redux when form values change + const timeoutRef = useRef(null); + const watched = watch(); + useEffect(() => { + if (timeoutRef.current) clearTimeout(timeoutRef.current); + timeoutRef.current = setTimeout(() => { + dispatch( + updateDraftCampaign({ + title: watched.title || "", + category: watched.category || "", + description: watched.shortDescription || "", + fullStory: watched.fullStory || "", + }), + ); + }, 550); + return () => clearTimeout(timeoutRef.current); + }, [watched, dispatch]); + + // Expose a validate function to parent via validationRef + useEffect(() => { + if (!validationRef) return; + validationRef.current = { + validate: async () => { + const valid = await trigger(); + if (valid) { + // ensure final save before proceeding + const values = await handleSubmit((vals) => vals)(); + dispatch( + updateDraftCampaign({ + title: values.title, + category: values.category, + description: values.shortDescription, + fullStory: values.fullStory, + }), + ); + } + return valid; + }, + }; + return () => { + if (validationRef.current && validationRef.current.validate) { + validationRef.current = null; + } + }; + }, [validationRef, trigger, handleSubmit, dispatch]); + + const title = watch("title") || ""; + const shortDescription = watch("shortDescription") || ""; + + return ( +