diff --git a/src/components/academic/departments/create-department.tsx b/src/components/academic/departments/create-department.tsx index 70c174f..6f98318 100644 --- a/src/components/academic/departments/create-department.tsx +++ b/src/components/academic/departments/create-department.tsx @@ -22,9 +22,10 @@ const CreateDepartment = () => { name: "", school: "", established: "", + head: "", }); - // Create department + // Create department const handleCreateSubmit = async (e: React.FormEvent) => { e.preventDefault(); setIsLoading(true); @@ -41,6 +42,7 @@ const CreateDepartment = () => { name: "", school: "", established: "", + head: "", }); refreshDepartment(); setIsCreateDialogOpen(false); @@ -90,6 +92,16 @@ const CreateDepartment = () => { placeholder="Enter school name" /> +
+ + InputHandler(e, setCreateDepartment)} + name="head" + placeholder="Enter Head of Department name" + /> +
{ +const EditDepartment = ({ departmentInfo }: EditDepartmentProps) => { const refreshDepartment = useRecoilRefresher_UNSTABLE(department); const [editingDepartment, setEditingDepartment] = @@ -97,6 +97,16 @@ const EditDepartment = ({ departmentInfo }: EditDepartmentData) => { placeholder="Enter school name" />
+
+ + InputHandler(e, setEditingDepartment)} + name="school" + placeholder="Enter Head of Department Name" + /> +
{
Department Head:{" "} - {/* {dept.head} */} + {dept.head}
@@ -106,7 +106,9 @@ const AcademicDepartmentTab = () => { )) ) : ( -
No department available to show
+
+ No department available to show +
)}
diff --git a/src/components/academic/semester/create-semster.tsx b/src/components/academic/semester/create-semster.tsx index 01b78b7..f44e46f 100644 --- a/src/components/academic/semester/create-semster.tsx +++ b/src/components/academic/semester/create-semster.tsx @@ -1,150 +1,148 @@ -// import UseModel from "@/components/models/useModel"; -// import { Button } from "@/components/ui/button"; -// import { Input } from "@/components/ui/input"; -// import { Label } from "@/components/ui/label"; -// import { -// Select, -// SelectContent, -// SelectItem, -// SelectTrigger, -// SelectValue, -// } from "@/components/ui/select"; -// import { InputHandler, SelectHandler } from "@/lib/utils"; -// import axios from "axios"; -// import { Plus } from "lucide-react"; -// import { useState } from "react"; -// import { toast } from "sonner"; -// // import type { SemesterData } from "@/types"; -// import { useRecoilRefresher_UNSTABLE } from "recoil"; -// import { semester } from "@/store/atoms/academics/semester"; +import UseModel from "@/components/models/useModel"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { InputHandler, SelectHandler } from "@/lib/utils"; +import axios from "axios"; +import { Plus } from "lucide-react"; +import { useState } from "react"; +import { toast } from "sonner"; +import { useRecoilRefresher_UNSTABLE } from "recoil"; +import { semester } from "@/store/atoms/academics/semester"; +import type { SemesterData } from "@/types"; -// const base_api = import.meta.env.VITE_SERVER_BASE_URL; +const base_api = import.meta.env.VITE_SERVER_BASE_URL; const CreateSemester = () => { -// const refreshSemester = useRecoilRefresher_UNSTABLE(semester); + const refreshSemester = useRecoilRefresher_UNSTABLE(semester); -// const [isCreateDialogOpen, setIsCreateDialogOpen] = useState(false); -// const [isLoading, setIsLoading] = useState(false); + const [isCreateDialogOpen, setIsCreateDialogOpen] = useState(false); + const [isLoading, setIsLoading] = useState(false); - // const [createSemester, setCreateSemester] = useState({ - // semester: "", - // courses: [""], - // enddate: "", - // startdate: "", - // status: "", - // }); + const [createSemester, setCreateSemester] = useState({ + semester: "", + courses: "", + enddate: "", + startdate: "", + status: "", + }); // Create department submit -// const handleCreateSubmit = async (e: React.FormEvent) => { -// e.preventDefault(); -// setIsLoading(true); + const handleCreateSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + setIsLoading(true); -// try { -// const response = await axios.post( -// `${base_api}/api/v1/create-program`, -// createSemester, + try { + const response = await axios.post( + `${base_api}/api/v1/create-semester`, + createSemester, -// { withCredentials: true } -// ); -// if (response.status === 201) { -// setCreateSemester({ -// semester: "", -// courses: [], -// enddate: "", -// startdate: "", -// status: "", -// }); -// toast.success("program created successfully"); -// setIsLoading(false); -// refreshSemester(); -// setIsCreateDialogOpen(false); -// } -// } catch (error: any) { -// console.error(error?.message); -// toast.error("Failed to create program"); -// setIsLoading(false); -// setIsCreateDialogOpen(false); -// } -// }; + { withCredentials: true } + ); + if (response.status === 201) { + setCreateSemester({ + semester: "", + courses: "", + enddate: "", + startdate: "", + status: "", + }); + toast.success("program created successfully"); + setIsLoading(false); + refreshSemester(); + setIsCreateDialogOpen(false); + } + } catch (error: any) { + console.error(error?.message); + toast.error("Failed to create program"); + setIsLoading(false); + setIsCreateDialogOpen(false); + } + }; return ( - // - // - // Add new - // - // } - // > - //
- // - // InputHandler(e, setCreateSemester)} - // placeholder="Enter semester name" - // /> - //
- //
- // - // InputHandler(e, setCreateSemester)} - // placeholder="Enter semester name" - // /> - //
+ + + Add new + + } + > +
+ + InputHandler(e, setCreateSemester)} + placeholder="Enter semester name" + /> +
+
+ + InputHandler(e, setCreateSemester)} + placeholder="Enter semester name" + /> +
- //
- // - // InputHandler(e, setCreateSemester)} - // /> - //
- //
- // - // InputHandler(e, setCreateSemester)} - // /> - //
- //
- // - // - //
- //
- -
not Implemented yet
+
+ + InputHandler(e, setCreateSemester)} + /> +
+
+ + InputHandler(e, setCreateSemester)} + /> +
+
+ + +
+
); }; diff --git a/src/components/academic/semester/edit-semester.tsx b/src/components/academic/semester/edit-semester.tsx index e69de29..5cde540 100644 --- a/src/components/academic/semester/edit-semester.tsx +++ b/src/components/academic/semester/edit-semester.tsx @@ -0,0 +1,154 @@ +import UseModel from "@/components/models/useModel"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { FormateDate, InputHandler, SelectHandler } from "@/lib/utils"; +import axios from "axios"; +import { Edit } from "lucide-react"; +import { useState } from "react"; +import { toast } from "sonner"; +import type { EditSemesterProps, SemesterData } from "@/types"; +import { useRecoilRefresher_UNSTABLE } from "recoil"; +import { semester } from "@/store/atoms/academics/semester"; + +const base_api = import.meta.env.VITE_SERVER_BASE_URL; + +const EditSemester = ({ semesterInfo }: EditSemesterProps) => { + const refreshSemester = useRecoilRefresher_UNSTABLE(semester); + + const [editingSemester, setEditingSemester] = useState( + null + ); + const [isUpdateDialogOpen, setIsUpdateDialogOpen] = useState(false); + const [isLoading, setIsLoading] = useState(false); + + // Update course submit + const handleUpdateSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + if (!editingSemester?.id) return; + setIsLoading(true); + + console.log(editingSemester); + + try { + const response = await axios.put( + `${base_api}/api/v1/update-semester/${editingSemester.id}`, + editingSemester, + { withCredentials: true } + ); + if (response.status === 201) { + toast.success("program updated successfully"); + refreshSemester(); + setIsLoading(false); + setIsUpdateDialogOpen(false); + } + } catch (error: any) { + console.error(error?.message); + toast.error("Failed to update program"); + setIsUpdateDialogOpen(false); + setIsLoading(false); + } + }; + + // Handle edit button click + const handleEditClick = (semesterInfo: SemesterData) => { + setEditingSemester({ ...semesterInfo }); + setIsUpdateDialogOpen(true); + }; + + return ( + handleEditClick(semesterInfo)} + > + + Update + + } + > + {editingSemester && ( + <> +
+ + InputHandler(e, setEditingSemester)} + placeholder="Enter semester name" + /> +
+
+ + InputHandler(e, setEditingSemester)} + placeholder="Enter semester name" + /> +
+ +
+ + InputHandler(e, setEditingSemester)} + /> +
+
+ + InputHandler(e, setEditingSemester)} + /> +
+
+ + +
+ + )} +
+ ); +}; + +export default EditSemester; diff --git a/src/components/academic/semester/index.tsx b/src/components/academic/semester/index.tsx index 7bbaa45..a61606b 100644 --- a/src/components/academic/semester/index.tsx +++ b/src/components/academic/semester/index.tsx @@ -4,16 +4,60 @@ import { Card, CardContent } from "@/components/ui/card"; import { FormateDate } from "@/lib/utils"; import { useSemesterData } from "@/store/hooks/academics/use-semester-data"; import type { SemesterData } from "@/types"; -import { Edit } from "lucide-react"; +import { Trash } from "lucide-react"; import CreateSemester from "./create-semster"; +import EditSemester from "./edit-semester"; +import { ConfirmDialog } from "@/components/confirm-dialog"; +import { useState } from "react"; +import axios from "axios"; +import { toast } from "sonner"; +import { useRecoilRefresher_UNSTABLE } from "recoil"; +import { semester } from "@/store/atoms/academics/semester"; +import { Skeleton } from "@/components/ui/skeleton"; + +const base_api = import.meta.env.VITE_SERVER_BASE_URL; const AcademicSemesterTab = () => { - const { semesterInfo, loading } = useSemesterData(); + const { semesterInfo, loading, error } = useSemesterData(); + const refreshProgram = useRecoilRefresher_UNSTABLE(semester); + const [isLoading, setIsLoading] = useState(false); + + // handle Delete - if (loading) { - return
loading...
; + async function handleDelete(semester: SemesterData) { + try { + const response = await axios.delete( + `${base_api}/api/v1/delete-semester/${semester?.id}`, + { + withCredentials: true, + } + ); + + if (response.status === 201) { + toast.success("Program deleted !!"); + setIsLoading(false); + refreshProgram(); + } + } catch (error) { + console.log(error); + setIsLoading(false); + } } + if (loading) + return ( +
+ +
+ ); + + if (error) + return ( +
+ Error loading courses data. +
+ ); + const semesterData: SemesterData[] = semesterInfo?.Semesters; return ( @@ -24,56 +68,74 @@ const AcademicSemesterTab = () => {
- {semesterData?.map((semester, index) => ( - - -
-
-
-

{semester.semester}

- - {semester.status} - -
-
-
- Start Date: - {FormateDate(semester.startdate)} + {semesterData?.length > 0 ? ( + semesterData.map((semester, index) => ( + + +
+
+
+

{semester.semester}

+ + {semester.status} +
-
- End Date: - {FormateDate(semester.enddate)} +
+
+ Start Date: + {FormateDate(semester.startdate)} +
+
+ End Date: + {FormateDate(semester.enddate)} +
+
+ Courses: + {semester.courses} +
+
+
+ + + {/* delete program */}
- Courses: - {semester.courses} + handleDelete(semester)} + destructive + trigger={ + + } + />
-
- - {semester.status === "Planning" && ( - - )} - -
-
-
-
- ))} + + + )) + ) : ( +
+ No semester available to show +
+ )}
); diff --git a/src/components/dashboard-welcome.tsx b/src/components/dashboard-welcome.tsx index c2f4e4d..49d7304 100644 --- a/src/components/dashboard-welcome.tsx +++ b/src/components/dashboard-welcome.tsx @@ -1,21 +1,14 @@ import { useAdminData } from "@/store/hooks/useAdminData"; import { AlertCircle, Verified } from "lucide-react"; -import { useEffect } from "react"; -import { Link, useNavigate } from "react-router-dom"; + +import { Link } from "react-router-dom"; import { Skeleton } from "./ui/skeleton"; const DashboardWelcome = () => { const { adminInfo, loading, error } = useAdminData(); - const navigate = useNavigate(); const userInfo = adminInfo?.isUserExist; - useEffect(() => { - if (!loading && !userInfo) { - navigate("/auth/user-email"); - } - }, [loading, userInfo, navigate]); - if (loading) return (
diff --git a/src/components/otp-step.tsx b/src/components/otp-step.tsx index 79163f4..758f7ab 100644 --- a/src/components/otp-step.tsx +++ b/src/components/otp-step.tsx @@ -6,14 +6,14 @@ import { InputOTPSeparator, InputOTPSlot, } from "@/components/ui/input-otp"; - import { REGEXP_ONLY_DIGITS } from "input-otp"; - import { Label } from "@/components/ui/label"; import axios from "axios"; import { useState } from "react"; import { useNavigate } from "react-router"; import { toast } from "sonner"; +import { useRecoilRefresher_UNSTABLE } from "recoil"; +import { admin } from "@/store/atoms/admin"; type OtpStepProps = { apiEndpoint: string; @@ -30,6 +30,7 @@ export const OtpStep = ({ subtitle = "We've sent OTP to your email", inputCount = 6, }: OtpStepProps) => { + const refreshAdminData = useRecoilRefresher_UNSTABLE(admin); const [otp, setOtp] = useState(""); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); @@ -56,13 +57,14 @@ export const OtpStep = ({ if (response.status === 200) { toast.success("OTP verified!"); setOtp(""); + await refreshAdminData(); navigate(redirectTo); return; } } catch (err: any) { if (err?.response?.status === 403) { toast.error(err.response?.data?.message || "OTP Expired"); - navigate("/auth/user-email") + navigate("/auth/user-email"); } const msg = err.response?.data?.message || "OTP verification failed"; setError(msg); diff --git a/src/components/route-protection/auth-protected-route.tsx b/src/components/route-protection/auth-protected-route.tsx new file mode 100644 index 0000000..94f4cda --- /dev/null +++ b/src/components/route-protection/auth-protected-route.tsx @@ -0,0 +1,15 @@ +// auth-protected-route.tsx +import React from "react"; +import { ProtectedRoute } from "@/components/route-protection/protected-route"; + +interface AuthProtectedRouteProps { + children: React.ReactNode; +} + +export const AuthProtectedRoute: React.FC = ({ + children, +}) => { + return ( + {children} + ); +}; diff --git a/src/components/route-protection/dashboard-protected-route.tsx b/src/components/route-protection/dashboard-protected-route.tsx new file mode 100644 index 0000000..a3115ac --- /dev/null +++ b/src/components/route-protection/dashboard-protected-route.tsx @@ -0,0 +1,12 @@ +import React from "react"; +import { ProtectedRoute } from "@/components/route-protection/protected-route"; + +interface DashboardProtectedRouteProps { + children: React.ReactNode; +} + +export const DashboardProtectedRoute: React.FC< + DashboardProtectedRouteProps +> = ({ children }) => { + return {children}; +}; diff --git a/src/components/route-protection/protected-route.tsx b/src/components/route-protection/protected-route.tsx new file mode 100644 index 0000000..b3f9daa --- /dev/null +++ b/src/components/route-protection/protected-route.tsx @@ -0,0 +1,35 @@ +import React from "react"; +import { useProtectedRoute } from "@/hooks/use-protected-route"; +import { BookOpen } from "lucide-react"; + +interface ProtectedRouteProps { + children: React.ReactNode; + requireVerification?: boolean; +} + +export const ProtectedRoute: React.FC = ({ + children, + requireVerification = true, +}) => { + const { loading, shouldRender } = useProtectedRoute(requireVerification); + + if (loading) { + return ( +
+
+ +
+
+ ); + } + + if (!shouldRender) { + return ( +
+
Please verify your account
+
+ ); + } + + return <>{children}; +}; diff --git a/src/hooks/use-protected-route.ts b/src/hooks/use-protected-route.ts new file mode 100644 index 0000000..42c5428 --- /dev/null +++ b/src/hooks/use-protected-route.ts @@ -0,0 +1,45 @@ +import { useNavigate } from "react-router"; +import { useRouteGuards } from "./use-route-guard"; +import { useEffect } from "react"; +import { toast } from "sonner"; + +export const useProtectedRoute = (requiredVerification = true) => { + const { loading, isUserExist, isVerified } = useRouteGuards(); + const navigate = useNavigate(); + + useEffect(() => { + if (loading) return; + + // If user not verified + if (requiredVerification) { + // user not created account yet + if (!isUserExist) { + navigate("/auth/sign-up", { replace: true }); + toast.warning("please create account to get access in My-Uni"); + return; + } + + // user not verified account yet + if (isUserExist && !isVerified) { + navigate("/auth/user-email", { replace: true }); + toast.warning("please verify your account"); + return; + } + } else { + if (isUserExist && isVerified) { + navigate("/", { replace: true }); + toast.success("welcome to the board"); + return; + } + } + }, [loading, isUserExist, isVerified, requiredVerification, navigate]); + + return { + loading, + isUserExist, + isVerified, + shouldRender: requiredVerification + ? isUserExist && isVerified + : !isUserExist || !isVerified, + }; +}; diff --git a/src/hooks/use-route-guard.ts b/src/hooks/use-route-guard.ts new file mode 100644 index 0000000..b27df2f --- /dev/null +++ b/src/hooks/use-route-guard.ts @@ -0,0 +1,35 @@ +import { useNavigate } from "react-router-dom"; +import { useAdminData } from "@/store/hooks/useAdminData"; + +export const useRouteGuards = () => { + const { adminInfo, loading } = useAdminData(); + const navigate = useNavigate(); + + const userInfo = adminInfo?.isUserExist; + + const redirectToVerification = () => { + if (loading) return; + + if (!loading && !userInfo) { + navigate("/auth/sign-up"); + } else if (!loading && userInfo && !userInfo.isVerified) { + navigate("/auth/user-email"); + } + }; + + const redirectToDashboard = () => { + if (loading) return; + if (!loading && userInfo && userInfo.isVerified) { + navigate("/"); + } + }; + + return { + userInfo, + loading, + isUserExist: !!userInfo, + isVerified: userInfo?.isVerified || false, + redirectToVerification, + redirectToDashboard, + }; +}; diff --git a/src/pages/auth/forgot-password/verify-otp.tsx b/src/pages/auth/forgot-password/verify-otp.tsx index 768ea59..79397cb 100644 --- a/src/pages/auth/forgot-password/verify-otp.tsx +++ b/src/pages/auth/forgot-password/verify-otp.tsx @@ -7,7 +7,8 @@ export default function VerifyOtp() { ); } diff --git a/src/pages/auth/sign-in.tsx b/src/pages/auth/sign-in.tsx index c9bcf01..daa87c4 100644 --- a/src/pages/auth/sign-in.tsx +++ b/src/pages/auth/sign-in.tsx @@ -12,6 +12,8 @@ import { useState } from "react"; import { Link } from "react-router-dom"; import { toast } from "sonner"; import { useNavigate } from "react-router"; +import { useRecoilRefresher_UNSTABLE } from "recoil"; +import { admin } from "@/store/atoms/admin"; interface UserData { email?: string; @@ -21,6 +23,8 @@ interface UserData { const server_base_api = import.meta.env.VITE_SERVER_BASE_URL; export default function SignIn() { + const refreshAdminData = useRecoilRefresher_UNSTABLE(admin); + const navigate = useNavigate(); const [showPassword, setShowPassword] = useState(true); @@ -45,14 +49,15 @@ export default function SignIn() { withCredentials: true, } ); - if (response.status === 200) { + if (response.status === 200) { setIsLoading(false); setUserData({ email: "", password: "", }); toast.success("credentials matched!!"); - navigate("/"); + await refreshAdminData(); + navigate("/auth/user-email"); return; } } catch (error: any) { @@ -122,7 +127,7 @@ export default function SignIn() { value={userData?.password} onChange={(e) => InputHandler(e, setUserData)} type={showPassword ? "text" : "password"} - placeholder="jhondow@1234" + placeholder="johndow@1234" />