diff --git a/public/images/addTeamMemberButton.png b/public/images/addTeamMemberButton.png new file mode 100644 index 0000000..2cd248c Binary files /dev/null and b/public/images/addTeamMemberButton.png differ diff --git a/public/images/addTeamMemberPlaceholder.png b/public/images/addTeamMemberPlaceholder.png new file mode 100644 index 0000000..f189836 Binary files /dev/null and b/public/images/addTeamMemberPlaceholder.png differ diff --git a/src/app/(pages)/admin/addTeamMember/page.tsx b/src/app/(pages)/admin/addTeamMember/page.tsx new file mode 100644 index 0000000..9866f61 --- /dev/null +++ b/src/app/(pages)/admin/addTeamMember/page.tsx @@ -0,0 +1,280 @@ +"use client"; + +import { useRouter } from "next/navigation"; +import Image from "next/image"; +import { X } from "lucide-react"; +import { useState } from "react"; +import ErrorText from "../../../components/errorText"; +import SuccessText from "../../../components/successText"; +import ErrorFormFieldText from "../../../components/errorFormFieldText"; +import { teamMemberSchema } from "../../../../schemas/teamMember"; +import LoadingSpinner from "../../../components/loadingSpinner"; + +const AddTeamMember = () => { + const router = useRouter(); + const [loading, setLoading] = useState(false); // State to manage loading state of API Call + const [uploadedImageUrl, setUploadedImageUrl] = useState(null); + const [error, setError] = useState(""); + const [success, setSuccess] = useState(""); + const [validationErrors, setValidationErrors] = useState<{ + [key: string]: string; + }>({}); // State to manage validation errors for form fields + const [formData, setFormData] = useState({ + name: "", + designation: "", + email: "", + linkedin: "", + }); + + const handleImageUpload = async (event) => { + setError(""); + const file = event.target.files[0]; + if (file && file.type.startsWith("image/")) { + const imageFormData = new FormData(); + imageFormData.append("file", file); + imageFormData.append( + "upload_preset", + process.env.NEXT_PUBLIC_CLOUDINARY_UPLOAD_PRESET, + ); // Replace with your preset + + try { + const response = await fetch( + `https://api.cloudinary.com/v1_1/${process.env.NEXT_PUBLIC_CLOUDINARY_CLOUD_NAME}/image/upload`, + { + method: "POST", + body: imageFormData, + }, + ); + const data = await response.json(); + setUploadedImageUrl(data.secure_url); + } catch (err) { + console.error("Upload failed:", err); + setError("Failed to upload image. Please try again."); + } + } else { + alert("Please upload a valid image file."); + } + }; + + const handleChange = (e) => { + e.preventDefault(); + setError(""); + const { name, value } = e.target; + setFormData((prevData) => ({ ...prevData, [name]: value })); + setValidationErrors((prevErrors) => ({ ...prevErrors, [name]: "" })); + }; + + const addMember = async (e) => { + e.preventDefault(); + try { + teamMemberSchema.parse(formData); + try { + const response = await fetch("/api/v1/addTeamMember", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + name: formData.name, + designation: formData.designation, + email: formData.email, + ...(formData.linkedin && { linkedin: formData.linkedin }), + ...(uploadedImageUrl && { image: uploadedImageUrl }), + }), + }); + + const data = await response.json(); + if (!response.ok) { + setLoading(false); + setError(data.message || "Something went wrong."); + throw new Error(data.message || "Something went wrong."); + } + setSuccess("Team member added successfully. You can add more members."); + setError(""); + setTimeout(() => { + setLoading(false); + router.push("/admin"); + }, 3000); + } catch (err) { + setLoading(false); + setError(`${err}`); + } + } catch (err) { + if (err.errors) { + // Mapping the error messages to the respective fields + const newValidationErrors = {}; + err.errors.forEach((errr) => { + newValidationErrors[errr.path[0]] = errr.message; + }); + setValidationErrors(newValidationErrors); + } + } + }; + + return ( +
+
+ + + + + +

+ Add Member +

+ router.push("/admin")} + className="absolute right-[10vw] md:w-11 md:h-9 w-10 h-9 hover:scale-[1.1] cursor-pointer" + /> +
+
+
+ + {uploadedImageUrl && ( + + )} + +
+
+ {error && } + {success && } +
+ + + {validationErrors.name && ( + + )} +
+
+ + + {validationErrors.designation && ( + + )} +
+
+ + + {validationErrors.email && ( + + )} +
+
+ + + {validationErrors.linkedin && ( + + )} +
+ +
+
+ +
+ ); +}; + +export default AddTeamMember; diff --git a/src/app/(pages)/admin/page.tsx b/src/app/(pages)/admin/page.tsx new file mode 100644 index 0000000..50bf376 --- /dev/null +++ b/src/app/(pages)/admin/page.tsx @@ -0,0 +1,102 @@ +"use client"; + +import { useRouter } from "next/navigation"; +import { useEffect, useState } from "react"; +import Image from "next/image"; +import "../../globals.scss"; +import NavbarAdmin from "../../components/NavbarAdmin"; + +const Admin = () => { + const router = useRouter(); + const [teamMembers, setTeamMembers] = useState([]); + const [error, setError] = useState(""); + + useEffect(() => { + const fetchTeamMembers = async () => { + try { + const res = await fetch("/api/v1/getAllTeamMembers", { + method: "GET", + headers: { + "Content-Type": "application/json", + }, + }); + const data = await res.json(); + + if (!res.ok) { + throw new Error(data.message || "Failed to fetch team members"); + } + setTeamMembers(data.teamMembers); + } catch (err) { + console.error(err.message); + setError("Failed to load team members."); + } + }; + fetchTeamMembers(); + }, []); + + if (error) { + return

{error}

; + } + + return ( +
+ +
+

+ Present Team: +

+
+ {teamMembers.map((member) => { + return ( +
+
+ {member.name} +
+

+ {member.name} +

+

+ {member.designation} +

+
+ ); + })} + +
+
+
+ ); +}; +export default Admin; diff --git a/src/app/(pages)/layout.tsx b/src/app/(pages)/layout.tsx index 358fb71..4069d57 100644 --- a/src/app/(pages)/layout.tsx +++ b/src/app/(pages)/layout.tsx @@ -1,12 +1,27 @@ +"use client"; + +import { usePathname } from "next/navigation"; import Navbar from "../components/Navbar"; import Footer from "../components/Footer"; export default function RootLayout({ children }) { + const pathname = usePathname(); + + // Paths where Navbar and Footer should be hidden + const noLayoutPaths = [ + "/login", + "/signup", + "/resetPassword", + "/admin/addTeamMember", + "/admin", + ]; + + const shouldHideLayout = noLayoutPaths.includes(pathname); return (
- + {!shouldHideLayout && } {children} -
); } diff --git a/src/app/api/v1/signIn/route.ts b/src/app/api/v1/signIn/route.ts index 5e9fbac..77e6322 100644 --- a/src/app/api/v1/signIn/route.ts +++ b/src/app/api/v1/signIn/route.ts @@ -42,6 +42,9 @@ export async function POST(req: NextRequest) { { status: 200 }, ); + if (user.role === "admin") { + response.cookies.set("adminToken", token, { httpOnly: true, path: "/" }); + } response.cookies.set("signInToken", token, { httpOnly: true, path: "/" }); return response; diff --git a/src/app/api/v1/signOut/route.ts b/src/app/api/v1/signOut/route.ts index f5fe407..9388bc7 100644 --- a/src/app/api/v1/signOut/route.ts +++ b/src/app/api/v1/signOut/route.ts @@ -12,6 +12,11 @@ export async function GET() { expires: new Date(0), }); + response.cookies.set("adminToken", "", { + httpOnly: true, + expires: new Date(0), + }); + return response; } catch (error: any) { return NextResponse.json({ message: "Sign out failed", success: false }); diff --git a/src/app/components/NavbarAdmin.tsx b/src/app/components/NavbarAdmin.tsx new file mode 100644 index 0000000..0e761d5 --- /dev/null +++ b/src/app/components/NavbarAdmin.tsx @@ -0,0 +1,210 @@ +"use client"; + +import Image from "next/image"; +import { useEffect, useState } from "react"; +import Link from "next/link"; +import { Icon } from "@iconify/react"; +import { usePathname, useRouter } from "next/navigation"; + +export default function NavbarAdmin() { + const [isMenuOpen, setIsMenuOpen] = useState(false); + const [isAuthenticated, setIsAuthenticated] = useState(false); + const router = useRouter(); + const path = usePathname(); + useEffect(() => {}, [path]); + + useEffect(() => { + const fetchUserData = async () => { + try { + const response = await fetch("/api/v1/getUser"); + if (response.ok) { + const data = await response.json(); + if (data) { + setIsAuthenticated(true); + } else { + setIsAuthenticated(false); + } + } + } catch (error) { + console.error("Error fetching data:", error); + } + }; + fetchUserData(); + }, [path]); + + const handleLogOut = async () => { + await fetch("/api/v1/signOut", { + method: "GET", + }) + .then((res) => res.json()) + .then((data) => { + router.push("/signIn"); + console.log(data.message); + }) + .catch((err) => console.error(err)); + }; + + const toggleMenu = () => setIsMenuOpen(!isMenuOpen); + + const menuLinks = [ + { name: "Home", href: "/" }, + { name: "Team Management", href: "/admin" }, + { name: "Testimonial Management", href: "/admin" }, + ]; + return ( + + ); +} diff --git a/src/app/components/successText.tsx b/src/app/components/successText.tsx index 720f320..ea91a7b 100644 --- a/src/app/components/successText.tsx +++ b/src/app/components/successText.tsx @@ -1,7 +1,7 @@ const SuccessText = ({ message }) => { return (