Skip to content

Commit d98c84e

Browse files
Team Management Admin
1 parent d2c3dad commit d98c84e

File tree

11 files changed

+639
-5
lines changed

11 files changed

+639
-5
lines changed

public/images/addTeamMemberButton.png

3.57 KB
Loading
4.64 KB
Loading
Lines changed: 280 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,280 @@
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;

src/app/(pages)/admin/page.tsx

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
"use client";
2+
3+
import { useRouter } from "next/navigation";
4+
import { useEffect, useState } from "react";
5+
import Image from "next/image";
6+
import "../../globals.scss";
7+
import NavbarAdmin from "../../components/NavbarAdmin";
8+
9+
const Admin = () => {
10+
const router = useRouter();
11+
const [teamMembers, setTeamMembers] = useState([]);
12+
const [error, setError] = useState("");
13+
14+
useEffect(() => {
15+
const fetchTeamMembers = async () => {
16+
try {
17+
const res = await fetch("/api/v1/getAllTeamMembers", {
18+
method: "GET",
19+
headers: {
20+
"Content-Type": "application/json",
21+
},
22+
});
23+
const data = await res.json();
24+
25+
if (!res.ok) {
26+
throw new Error(data.message || "Failed to fetch team members");
27+
}
28+
setTeamMembers(data.teamMembers);
29+
} catch (err) {
30+
console.error(err.message);
31+
setError("Failed to load team members.");
32+
}
33+
};
34+
fetchTeamMembers();
35+
}, []);
36+
37+
if (error) {
38+
return <p>{error}</p>;
39+
}
40+
41+
return (
42+
<div className="flex flex-col">
43+
<NavbarAdmin />
44+
<div className="flex flex-col my-36 px-[5vw] ">
45+
<p
46+
className="px-[10.5vw] text-lg mb-8"
47+
style={{ fontFamily: "Sofia Pro Regular" }}
48+
>
49+
Present Team:
50+
</p>
51+
<div className="flex px-[10.5vw] flex-wrap w-fit gap-[6vw]">
52+
{teamMembers.map((member) => {
53+
return (
54+
<div className="bg-[#1E3432] flex flex-col justify-center items-center rounded-[3vw] md:rounded-[1vw] hover:scale-[1.1] transition-all duration-200">
55+
<div className="relative md:w-[18vw] md:h-[18vw] w-[31vw] h-[30vw] ">
56+
<Image
57+
className="md:rounded-t-[1vw] rounded-t-[3vw] object-cover"
58+
src={member.image || "/images/placeholder_image.jpg"}
59+
alt={member.name}
60+
fill
61+
></Image>
62+
</div>
63+
<p
64+
className="text-[#F2B263] md:text-lg sm:text-sm text-[4vw] mt-2"
65+
style={{ fontFamily: "Sofia Pro Light" }}
66+
>
67+
{member.name}
68+
</p>
69+
<p
70+
className="text-white md:text-sm sm:text-xs text-[3vw] mb-2"
71+
style={{ fontFamily: "Sofia Pro Regular" }}
72+
>
73+
{member.designation}
74+
</p>
75+
</div>
76+
);
77+
})}
78+
<button
79+
onClick={() => router.push("admin/addTeamMember")}
80+
className="bg-[#1E3432] flex flex-col justify-center items-center rounded-[3vw] md:rounded-[1vw] hover:scale-[1.1] transition-all duration-200"
81+
>
82+
<div className="relative md:w-[18vw] md:h-[18vw] w-[31vw] h-[30vw] ">
83+
<Image
84+
className="md:rounded-t-[1vw] rounded-t-[3vw] object-cover"
85+
src="/images/addTeamMemberButton.png"
86+
alt="add member"
87+
fill
88+
></Image>
89+
</div>
90+
<p
91+
className="text-[#F2B263] md:text-lg sm:text-sm text-[4vw] my-2"
92+
style={{ fontFamily: "Sofia Pro Light" }}
93+
>
94+
Add Employee
95+
</p>
96+
</button>
97+
</div>
98+
</div>
99+
</div>
100+
);
101+
};
102+
export default Admin;

0 commit comments

Comments
 (0)