diff --git a/package-lock.json b/package-lock.json index 19b0e05..be0e4cd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,6 +11,7 @@ "@radix-ui/react-avatar": "^1.1.10", "@radix-ui/react-checkbox": "^1.3.2", "@radix-ui/react-collapsible": "^1.1.11", + "@radix-ui/react-dialog": "^1.1.14", "@radix-ui/react-dropdown-menu": "^2.1.15", "@radix-ui/react-label": "^2.1.7", "@radix-ui/react-select": "^2.2.5", @@ -1282,6 +1283,42 @@ } } }, + "node_modules/@radix-ui/react-dialog": { + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.14.tgz", + "integrity": "sha512-+CpweKjqpzTmwRwcYECQcNYbI8V9VSQt0SNFKeEBLgfucbsLssU6Ppq7wUdNXEGb573bMjFhVjKVll8rmV6zMw==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.2", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-dismissable-layer": "1.1.10", + "@radix-ui/react-focus-guards": "1.1.2", + "@radix-ui/react-focus-scope": "1.1.7", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.4", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-direction": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.1.tgz", diff --git a/package.json b/package.json index 262b0fb..0cb95e6 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,7 @@ "@radix-ui/react-avatar": "^1.1.10", "@radix-ui/react-checkbox": "^1.3.2", "@radix-ui/react-collapsible": "^1.1.11", + "@radix-ui/react-dialog": "^1.1.14", "@radix-ui/react-dropdown-menu": "^2.1.15", "@radix-ui/react-label": "^2.1.7", "@radix-ui/react-select": "^2.2.5", diff --git a/src/App.css b/src/App.css index 1061e9b..9b68b53 100644 --- a/src/App.css +++ b/src/App.css @@ -125,3 +125,9 @@ body { @apply bg-background text-foreground; } } + +@layer utilities { + .bg-special { + @apply bg-gradient-to-br from-blue-200 via-purple-200 to-pink-200; + } +} diff --git a/src/components/academic-courses-tab.tsx b/src/components/academic-courses-tab.tsx new file mode 100644 index 0000000..73bbe23 --- /dev/null +++ b/src/components/academic-courses-tab.tsx @@ -0,0 +1,96 @@ +import { Button } from "./ui/button"; +import { Edit, Plus } from "lucide-react"; +import { Badge } from "@/components/ui/badge"; +import { Card, CardContent } from "./ui/card"; +import { useCoursesData } from "@/store/hooks/academics/use-courses-data"; +import { FormateDate } from "@/lib/utils"; + +interface CourseData { + coursename: string; + professorname: string; + department: string; + status: string; + startdate: string; + enddate: string; +} + +const AcademinCoursesTab = () => { + const { coursesInfo, error, loading } = useCoursesData(); + + if (loading) return
Loading courses data...
; + if (error) return
Error loading courses data.
; + + const courseData: CourseData[] = coursesInfo?.courses; + return ( +
+
+

Semester Management

+ +
+
+ {courseData?.map((course, index) => ( + + +
+
+
+

{course.coursename}

+ + {course.status} + +
+
+
+ Professor:{" "} + {course.professorname} +
+
+
+
+ Start Date:{" "} + {FormateDate(course.startdate)} +
+
+ End Date:{" "} + {FormateDate(course.enddate)} +
+
+
+
+ + {course.status === "Planning" && ( + + )} + +
+
+
+
+ ))} +
+
+ ); +}; + +export default AcademinCoursesTab; diff --git a/src/components/confirm-dialog.tsx b/src/components/confirm-dialog.tsx new file mode 100644 index 0000000..bbd9063 --- /dev/null +++ b/src/components/confirm-dialog.tsx @@ -0,0 +1,69 @@ + +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, + DialogTrigger, + DialogClose, +} from "@/components/ui/dialog"; +import { Button } from "@/components/ui/button"; +import { useState, type ReactNode } from "react"; + +interface ConfirmDialogProps { + title: string; + description: string; + trigger: ReactNode; + confirmLabel?: string; + cancelLabel?: string; + destructive?: boolean; + onConfirm: () => Promise | void; +} + +export const ConfirmDialog = ({ + title, + description, + trigger, + confirmLabel = "Confirm", + cancelLabel = "Cancel", + destructive = true, + onConfirm, +}: ConfirmDialogProps) => { + const [open, setOpen] = useState(false); + const [isProcessing, setIsProcessing] = useState(false); + + const handleConfirm = async () => { + setIsProcessing(true); + await onConfirm(); + setIsProcessing(false); + setOpen(false); + }; + + return ( + + {trigger} + + + + {title} + {description} + + + + + + + + + + + ); +}; diff --git a/src/components/dashboard-welcome.tsx b/src/components/dashboard-welcome.tsx new file mode 100644 index 0000000..c2f4e4d --- /dev/null +++ b/src/components/dashboard-welcome.tsx @@ -0,0 +1,65 @@ +import { useAdminData } from "@/store/hooks/useAdminData"; +import { AlertCircle, Verified } from "lucide-react"; +import { useEffect } from "react"; +import { Link, useNavigate } 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 ( +
+ +
+ ); + + if (error) return
Error loading admin data.
; + + return ( +
+
+ + +
+ + Welcome Back,{" "} + + {userInfo?.firstname}{" "} + {userInfo?.isVerified && ( + + )} + + +

+ Here's what's happening with your university today. +

+
+
+
+ {userInfo?.isVerified === false && ( + <> + + + Verify Account + + + )} +
+
+ ); +}; + +export default DashboardWelcome; diff --git a/src/components/models/course-model.tsx b/src/components/models/course-model.tsx new file mode 100644 index 0000000..690f63d --- /dev/null +++ b/src/components/models/course-model.tsx @@ -0,0 +1,5 @@ +const CourseModel = () => { + return
CourseModel
; +}; + +export default CourseModel; diff --git a/src/components/setting-prefrences.tsx b/src/components/setting-prefrences.tsx new file mode 100644 index 0000000..510c2e6 --- /dev/null +++ b/src/components/setting-prefrences.tsx @@ -0,0 +1,66 @@ +import { Card, CardContent } from "@/components/ui/card"; +import { Palette } from "lucide-react"; +import { Label } from "@/components/ui/label"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { Button } from "@/components/ui/button"; +import { useTheme } from "@/components/theme-provider"; + +const SettingPrefrences = () => { + const { setTheme, theme } = useTheme(); + + return ( + + +

+ + Preferences +

+
+
+ + +
+ +
+ + +
+
+ + +
+
+ ); +}; + +export default SettingPrefrences; diff --git a/src/components/setting-profile.tsx b/src/components/setting-profile.tsx new file mode 100644 index 0000000..5423af0 --- /dev/null +++ b/src/components/setting-profile.tsx @@ -0,0 +1,218 @@ +import { Card, CardContent } from "@/components/ui/card"; +import { Camera, User } from "lucide-react"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { Textarea } from "@/components/ui/textarea"; +import { Button } from "@/components/ui/button"; +import { useState } from "react"; +import { useAdminData } from "@/store/hooks/useAdminData"; +import axios from "axios"; +import { toast } from "sonner"; +import { InputHandler } from "@/lib/utils"; +import { Skeleton } from "@/components/ui/skeleton"; +import { useRecoilRefresher_UNSTABLE } from "recoil"; +import { admin } from "@/store/atoms/admin"; + +const base_api = import.meta.env.VITE_SERVER_BASE_URL; + +interface UserData { + firstname: string; + lastname: string; + email: string; + address: string; + bio: string; + dob: string; +} + +const SettingProfile = () => { + const { adminInfo, loading, error } = useAdminData(); + const [isLoading, setIsLoading] = useState(false); + const refreshAdmin = useRecoilRefresher_UNSTABLE(admin); + + const [userData, setUserData] = useState({ + firstname: "", + lastname: "", + email: "", + address: "", + bio: "", + dob: "", + }); + + // form handler + + const handleFormSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + setIsLoading(true); + + // removing empty fields + const filteredData: Partial = Object.fromEntries( + Object.entries(userData).filter(([_, value]) => value !== "") + ); + + try { + const response = await axios.put( + `${base_api}/api/v1/update-profile`, + filteredData, + { withCredentials: true } + ); + + if (response.status === 200) { + setUserData({ + firstname: "", + lastname: "", + email: "", + address: "", + bio: "", + dob: "", + }); + toast.success("Profile updated successfully"); + setIsLoading(false); + refreshAdmin(); + } + } catch (error) { + console.log(error); + setIsLoading(false); + } + }; + + const userInfo = adminInfo?.isUserExist; + + if (loading) { + return ( + + + {/* Skeleton Avatar */} +
+ + + +
+ + {/* Skeleton Form Fields */} +
+ {Array.from({ length: 4 }).map((_, index) => ( +
+ + +
+ ))} +
+ +
+ + +
+ +
+ + +
+ + +
+
+ ); + } + + if (error) return
Error loading admin data.
; + + return ( + + + {/* Profile Photo */} +
+
+ {loading ? ( + + ) : ( + + )} +
+ +

+ JPG, PNG or GIF. Max size 2MB. +

+
+ + {/* Personal Information */} +
+
+
+ + InputHandler(e, setUserData)} + /> +
+
+ + InputHandler(e, setUserData)} + /> +
+
+ + InputHandler(e, setUserData)} + /> +
+
+ +
+ + InputHandler(e, setUserData)} + /> +
+ +
+ +