|
| 1 | +"use client"; |
| 2 | + |
| 3 | +import { useRouter } from "next/navigation"; |
| 4 | +import Image from "next/image"; |
| 5 | +import { X } from "lucide-react"; |
| 6 | +import { useState } from "react"; |
| 7 | +import ErrorText from "../../../components/errorText"; |
| 8 | +import SuccessText from "../../../components/successText"; |
| 9 | +import ErrorFormFieldText from "../../../components/errorFormFieldText"; |
| 10 | +import { teamMemberSchema } from "../../../../schemas/teamMember"; |
| 11 | +import LoadingSpinner from "../../../components/loadingSpinner"; |
| 12 | + |
| 13 | +const AddTeamMember = () => { |
| 14 | + const router = useRouter(); |
| 15 | + const [loading, setLoading] = useState(false); // State to manage loading state of API Call |
| 16 | + const [uploadedImageUrl, setUploadedImageUrl] = useState(null); |
| 17 | + const [error, setError] = useState(""); |
| 18 | + const [success, setSuccess] = useState(""); |
| 19 | + const [validationErrors, setValidationErrors] = useState<{ |
| 20 | + [key: string]: string; |
| 21 | + }>({}); // State to manage validation errors for form fields |
| 22 | + const [formData, setFormData] = useState({ |
| 23 | + name: "", |
| 24 | + designation: "", |
| 25 | + email: "", |
| 26 | + linkedin: "", |
| 27 | + }); |
| 28 | + |
| 29 | + const handleImageUpload = async (event) => { |
| 30 | + setError(""); |
| 31 | + const file = event.target.files[0]; |
| 32 | + if (file && file.type.startsWith("image/")) { |
| 33 | + const imageFormData = new FormData(); |
| 34 | + imageFormData.append("file", file); |
| 35 | + imageFormData.append( |
| 36 | + "upload_preset", |
| 37 | + process.env.NEXT_PUBLIC_CLOUDINARY_UPLOAD_PRESET, |
| 38 | + ); // Replace with your preset |
| 39 | + |
| 40 | + try { |
| 41 | + const response = await fetch( |
| 42 | + `https://api.cloudinary.com/v1_1/${process.env.NEXT_PUBLIC_CLOUDINARY_CLOUD_NAME}/image/upload`, |
| 43 | + { |
| 44 | + method: "POST", |
| 45 | + body: imageFormData, |
| 46 | + }, |
| 47 | + ); |
| 48 | + const data = await response.json(); |
| 49 | + setUploadedImageUrl(data.secure_url); |
| 50 | + } catch (err) { |
| 51 | + console.error("Upload failed:", err); |
| 52 | + setError("Failed to upload image. Please try again."); |
| 53 | + } |
| 54 | + } else { |
| 55 | + alert("Please upload a valid image file."); |
| 56 | + } |
| 57 | + }; |
| 58 | + |
| 59 | + const handleChange = (e) => { |
| 60 | + e.preventDefault(); |
| 61 | + setError(""); |
| 62 | + const { name, value } = e.target; |
| 63 | + setFormData((prevData) => ({ ...prevData, [name]: value })); |
| 64 | + setValidationErrors((prevErrors) => ({ ...prevErrors, [name]: "" })); |
| 65 | + }; |
| 66 | + |
| 67 | + const addMember = async (e) => { |
| 68 | + e.preventDefault(); |
| 69 | + try { |
| 70 | + teamMemberSchema.parse(formData); |
| 71 | + try { |
| 72 | + const response = await fetch("/api/v1/addTeamMember", { |
| 73 | + method: "POST", |
| 74 | + headers: { |
| 75 | + "Content-Type": "application/json", |
| 76 | + }, |
| 77 | + body: JSON.stringify({ |
| 78 | + name: formData.name, |
| 79 | + designation: formData.designation, |
| 80 | + email: formData.email, |
| 81 | + ...(formData.linkedin && { linkedin: formData.linkedin }), |
| 82 | + ...(uploadedImageUrl && { image: uploadedImageUrl }), |
| 83 | + }), |
| 84 | + }); |
| 85 | + |
| 86 | + const data = await response.json(); |
| 87 | + if (!response.ok) { |
| 88 | + setLoading(false); |
| 89 | + setError(data.message || "Something went wrong."); |
| 90 | + throw new Error(data.message || "Something went wrong."); |
| 91 | + } |
| 92 | + setSuccess("Team member added successfully. You can add more members."); |
| 93 | + setError(""); |
| 94 | + setTimeout(() => { |
| 95 | + setLoading(false); |
| 96 | + router.push("/admin"); |
| 97 | + }, 3000); |
| 98 | + } catch (err) { |
| 99 | + setLoading(false); |
| 100 | + setError(`${err}`); |
| 101 | + } |
| 102 | + } catch (err) { |
| 103 | + if (err.errors) { |
| 104 | + // Mapping the error messages to the respective fields |
| 105 | + const newValidationErrors = {}; |
| 106 | + err.errors.forEach((errr) => { |
| 107 | + newValidationErrors[errr.path[0]] = errr.message; |
| 108 | + }); |
| 109 | + setValidationErrors(newValidationErrors); |
| 110 | + } |
| 111 | + } |
| 112 | + }; |
| 113 | + |
| 114 | + return ( |
| 115 | + <div className="flex flex-col mb-10"> |
| 116 | + <div className="flex items-center px-[10vw] py-[10vh]"> |
| 117 | + <svg |
| 118 | + className="md:w-7 w-5" |
| 119 | + viewBox="0 0 32 32" |
| 120 | + fill="none" |
| 121 | + xmlns="http://www.w3.org/2000/svg" |
| 122 | + > |
| 123 | + <circle cx="16" cy="16" r="15.5" fill="white" stroke="#A48111" /> |
| 124 | + <path |
| 125 | + d="M14.3989 15.9937L14.0846 16.3333L14.3989 16.6729L18.1379 20.7131L17.9367 20.9305L13.6821 16.3332L17.9329 11.7352L18.137 11.9545L14.3989 15.9937Z" |
| 126 | + fill="#A48111" |
| 127 | + stroke="#A48111" |
| 128 | + /> |
| 129 | + </svg> |
| 130 | + |
| 131 | + <h1 |
| 132 | + className="md:text-3xl text-2xl font-semibold ms-2" |
| 133 | + style={{ fontFamily: "Sofia Pro Light" }} |
| 134 | + > |
| 135 | + Add Member |
| 136 | + </h1> |
| 137 | + <X |
| 138 | + onClick={() => router.push("/admin")} |
| 139 | + className="absolute right-[10vw] md:w-11 md:h-9 w-10 h-9 hover:scale-[1.1] cursor-pointer" |
| 140 | + /> |
| 141 | + </div> |
| 142 | + <div className="flex flex-col md:flex-row items-center justify-center md:gap-20 gap-2 md:px-[10vw]"> |
| 143 | + <div className="flex flex-col gap-4"> |
| 144 | + <button |
| 145 | + onClick={() => document.getElementById("fileInput").click()} |
| 146 | + className="relative md:w-60 md:h-60 w-40 h-40 bg-white rounded-lg shadow-lg" |
| 147 | + > |
| 148 | + <Image |
| 149 | + className="object-cover rounded-lg" |
| 150 | + src={uploadedImageUrl || "/images/addTeamMemberPlaceholder.png"} |
| 151 | + alt="addMember" |
| 152 | + fill |
| 153 | + /> |
| 154 | + </button> |
| 155 | + {uploadedImageUrl && ( |
| 156 | + <button |
| 157 | + className="text-md text-red-500 cursor-pointer underline" |
| 158 | + onClick={() => document.getElementById("fileInput").click()} |
| 159 | + > |
| 160 | + Change Image |
| 161 | + </button> |
| 162 | + )} |
| 163 | + <input |
| 164 | + type="file" |
| 165 | + id="fileInput" |
| 166 | + accept="image/*" |
| 167 | + style={{ display: "none" }} |
| 168 | + onChange={handleImageUpload} |
| 169 | + /> |
| 170 | + </div> |
| 171 | + <form className="flex flex-col gap-4 mt-4 md:mb-0 mb-20 md:w-fit w-full md:px-0 px-[10vw]"> |
| 172 | + {error && <ErrorText error={error} setError={setError} />} |
| 173 | + {success && <SuccessText message={success} />} |
| 174 | + <div className="flex flex-col"> |
| 175 | + <label |
| 176 | + htmlFor="nameInput" |
| 177 | + className="text-md font-medium mb-2" |
| 178 | + style={{ fontFamily: "Sofia Pro Light" }} |
| 179 | + > |
| 180 | + Name |
| 181 | + </label> |
| 182 | + <input |
| 183 | + id="nameInput" |
| 184 | + onChange={handleChange} |
| 185 | + value={formData.name} |
| 186 | + name="name" |
| 187 | + type="text" |
| 188 | + placeholder="John Smith" |
| 189 | + className="lg:w-[25vw] w-full border border-gray-300 rounded-md p-2 focus:border-[#1e3432]" |
| 190 | + /> |
| 191 | + {validationErrors.name && ( |
| 192 | + <ErrorFormFieldText error={validationErrors.name} /> |
| 193 | + )} |
| 194 | + </div> |
| 195 | + <div className="flex flex-col"> |
| 196 | + <label |
| 197 | + htmlFor="designationInput" |
| 198 | + className="text-md font-medium mb-2" |
| 199 | + style={{ fontFamily: "Sofia Pro Light" }} |
| 200 | + > |
| 201 | + Designation |
| 202 | + </label> |
| 203 | + <input |
| 204 | + id="designationInput" |
| 205 | + onChange={handleChange} |
| 206 | + value={formData.designation} |
| 207 | + name="designation" |
| 208 | + type="text" |
| 209 | + placeholder="Manager" |
| 210 | + className="lg:w-[25vw] w-full border border-gray-300 rounded-md p-2 focus:border-[#1e3432]" |
| 211 | + /> |
| 212 | + {validationErrors.designation && ( |
| 213 | + <ErrorFormFieldText error={validationErrors.designation} /> |
| 214 | + )} |
| 215 | + </div> |
| 216 | + <div className="flex flex-col"> |
| 217 | + <label |
| 218 | + htmlFor="emailInput" |
| 219 | + className="text-md font-medium mb-2" |
| 220 | + style={{ fontFamily: "Sofia Pro Light" }} |
| 221 | + > |
| 222 | + Email |
| 223 | + </label> |
| 224 | + <input |
| 225 | + id="emailInput" |
| 226 | + onChange={handleChange} |
| 227 | + value={formData.email} |
| 228 | + name="email" |
| 229 | + type="text" |
| 230 | + placeholder="john@gmail.com" |
| 231 | + className="lg:w-[25vw] w-full border border-gray-300 rounded-md p-2 focus:border-[#1e3432]" |
| 232 | + /> |
| 233 | + {validationErrors.email && ( |
| 234 | + <ErrorFormFieldText error={validationErrors.email} /> |
| 235 | + )} |
| 236 | + </div> |
| 237 | + <div className="flex flex-col"> |
| 238 | + <label |
| 239 | + htmlFor="linkedinInput" |
| 240 | + className="text-md font-medium mb-2" |
| 241 | + style={{ fontFamily: "Sofia Pro Light" }} |
| 242 | + > |
| 243 | + Linkdin (Optional) |
| 244 | + </label> |
| 245 | + <input |
| 246 | + id="linkedinInput" |
| 247 | + onChange={handleChange} |
| 248 | + value={formData.linkedin} |
| 249 | + name="linkedin" |
| 250 | + type="text" |
| 251 | + placeholder="Linkdin url" |
| 252 | + className="lg:w-[25vw] w-full border border-gray-300 rounded-md p-2 focus:border-[#1e3432]" |
| 253 | + /> |
| 254 | + {validationErrors.linkedin && ( |
| 255 | + <ErrorFormFieldText error={validationErrors.linkedin} /> |
| 256 | + )} |
| 257 | + </div> |
| 258 | + </form> |
| 259 | + <div></div> |
| 260 | + </div> |
| 261 | + <button |
| 262 | + onClick={addMember} |
| 263 | + disabled={!formData.name || !formData.designation || !formData.email} // Disable the button if any required field is empty |
| 264 | + className={`md:relative fixed bottom-0 mt-10 md:rounded-3xl rounded-none px-5 py-1 text-2xl md:w-fit w-full font-medium text-center self-center ${ |
| 265 | + !formData.name || !formData.designation || !formData.email |
| 266 | + ? "bg-gray-400 text-white cursor-not-allowed" // Disabled styles |
| 267 | + : "text-white bg-[#1e3432] hover:bg-[#172625] cursor-pointer" // Active styles |
| 268 | + } md:border-2 border-none border-[#fac16a] transition-all ease-in-out duration-200`} |
| 269 | + style={{ fontFamily: "Sofia Pro Light" }} |
| 270 | + > |
| 271 | + <div className="flex flex-row items-center justify-center gap-3"> |
| 272 | + {loading && <LoadingSpinner />} |
| 273 | + <span>Save</span> |
| 274 | + </div> |
| 275 | + </button> |
| 276 | + </div> |
| 277 | + ); |
| 278 | +}; |
| 279 | + |
| 280 | +export default AddTeamMember; |
0 commit comments