Compare commits

...

2 Commits

  1. 137
      frontend/edu-connect/src/app/(admin)/admin/users/_partials/UserTable.tsx
  2. 56
      frontend/edu-connect/src/app/(admin)/admin/users/page.tsx
  3. 21
      frontend/edu-connect/src/app/auth/login/_partials/LoginForm.tsx
  4. 281
      frontend/edu-connect/src/app/auth/register/_partials/registerForm.tsx
  5. 1
      frontend/edu-connect/src/app/globals.css
  6. 179
      frontend/edu-connect/src/app/my-courses/_partials/BannerImageUpload.tsx
  7. 8
      frontend/edu-connect/src/app/my-courses/_partials/CourseCategoryOptionHelper.ts
  8. 374
      frontend/edu-connect/src/app/my-courses/_partials/CoursesActionModal.tsx
  9. 74
      frontend/edu-connect/src/app/my-courses/_partials/EnrolledCourseList.tsx
  10. 74
      frontend/edu-connect/src/app/my-courses/_partials/MyCoursesListTabContent.tsx
  11. 31
      frontend/edu-connect/src/app/my-courses/_partials/myCoursesTabWrapper.tsx
  12. 8
      frontend/edu-connect/src/app/user/profile/_partials/UserEmailUpdateTabContent.tsx
  13. 92
      frontend/edu-connect/src/app/user/profile/_partials/UserPasswordUpdateTabContent.tsx
  14. 4
      frontend/edu-connect/src/app/user/profile/_partials/UserTabContent.tsx
  15. BIN
      frontend/edu-connect/src/assets/img/brandLogo.png
  16. 3
      frontend/edu-connect/src/components/(dashboard)/common/Sidebar/AppSidebar.tsx
  17. 9
      frontend/edu-connect/src/components/common/Header/header.tsx
  18. 59
      frontend/edu-connect/src/components/ui/alert.tsx
  19. 2
      frontend/edu-connect/src/components/ui/badge.tsx
  20. 33
      frontend/edu-connect/src/components/ui/input.tsx
  21. 17
      frontend/edu-connect/src/helpers/context/AuthProvider.tsx
  22. 6
      frontend/edu-connect/src/lib/routes.ts

@ -0,0 +1,137 @@
import DataTable from "@/components/(dashboard)/common/DataTable/DataTable"
import { Button } from "@/components/(dashboard)/ui/button"
import { Avatar, AvatarImage } from "@/components/ui/avatar"
import { Badge } from "@/components/ui/badge"
import { routes } from "@/lib/routes"
import { ColumnDef } from "@tanstack/react-table"
import { ArrowUpDown } from "lucide-react"
import Link from "next/link"
const UserTable :React.FC<{
mutate : () => void
userData : Array<any>
isLoading : boolean
}> = ({
mutate ,
userData ,
isLoading
}) => {
const columns: ColumnDef<any>[] = [
{
accessorKey: "sn",
header: "SN",
cell: ({ row }) => (
<div className="capitalize">{row.index + 1}</div>
),
},
{
id: 'name',
accessorFn: (row: any) => row.original?.firstName,
header: ({ column }) => (
<Button
variant="ghost"
className="!px-0"
onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
>
Name
<ArrowUpDown className="ml-2 h-4 w-4" />
</Button>
),
cell: ({ row }) => (
<div className="capitalize flex gap-2">
<Avatar className="h-8 w-8">
<AvatarImage
src={row.original?.profilePicture ?? 'no image path'}
alt={row.original?.firstName}
/>
</Avatar>
<p>{row?.original?.firstName}</p>
</div>
),
},
{
id: 'email',
accessorFn: (row: any) => row.original?.email,
header: ({ column }) => (
<Button
variant="ghost"
onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
className="!px-0"
>
Email
<ArrowUpDown className="ml-2 h-4 w-4" />
</Button>
),
cell: ({ row }) => (
<div>{row.original?.email}</div>
),
},
{
id: 'dateOfBirth',
accessorFn: (row: any) => row.original?.dateOfBirth,
header: ({ column }) => (
<Button
variant="ghost"
className="!px-0"
onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
>
DateOfBirth
<ArrowUpDown className="ml-2 h-4 w-4" />
</Button>
),
cell: ({ row }) => (
<div className="capitalize">{row.original?.dateOfBirth}</div>
),
},
{
id: 'isActivated',
accessorFn: (row: any) => row.original?.isActivated,
header: ({ column }) => (
<Button
variant="ghost"
className="!px-0"
onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
>
IsActive
<ArrowUpDown className="ml-2 h-4 w-4" />
</Button>
),
cell: ({ row }) => (
<div>{row.original?.isActivated ? <Badge variant={'success'}>Active</Badge> : <Badge variant={'destructive'}>Deactive</Badge>}</div>
),
},
{
id: 'last_online_at',
accessorFn: (row: any) => row.original?.lastOnline,
header: ({ column }) => (
<Button
variant="ghost"
className="!px-0"
onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
>
lastOnline
<ArrowUpDown className="ml-2 h-4 w-4" />
</Button>
),
cell: ({ row }) => (
<div>
{row.original?.lastOnline ?? '-'}
</div>
),
},
]
return(
<>
<DataTable
data={userData}
columns={columns}
mutate={mutate}
searchKey="username"
isLoading={isLoading}
/>
</>
)
}
export default UserTable

@ -0,0 +1,56 @@
'use client'
import UserTabContent from "@/app/user/profile/_partials/UserTabContent"
import BreadCrumbNav from "@/components/(dashboard)/common/BreadCumbNav/BreadCrumbNav"
import DataTable from "@/components/(dashboard)/common/DataTable/DataTable"
import ContentContainer from "@/components/(dashboard)/elements/ContentContainer"
import { PageHeading } from "@/components/(dashboard)/ui/title"
import CommonContainer from "@/components/elements/CommonContainer"
import AppContextProvider from "@/helpers/context/AppContextProvider"
import { defaultFetcher } from "@/helpers/fetch.helper"
import { routes } from "@/lib/routes"
import { APP_BASE_URL } from "@/utils/constants"
import AdminView from "@/views/AdminView"
import useSWR from "swr"
import UserTable from "./_partials/UserTable"
const UsersIndexPage = () => {
const UserListURL = `${APP_BASE_URL}/api/admin/stats/userDetail`
const { data : UsersList , mutate , isLoading} = useSWR(UserListURL , defaultFetcher);
console.log(UsersList)
return(
<>
<AppContextProvider>
<AdminView>
<CommonContainer>
<PageHeading>Users</PageHeading>
<BreadCrumbNav breadCrumbItems={[
{
title : 'Dashboard',
href : routes.DASHBOARD_ROUTE
},
{
title : 'Users',
href : routes.USER_INDEX_PAGE
},
]}/>
<ContentContainer>
<div>
<UserTable
userData={UsersList?.users}
mutate={mutate}
isLoading={isLoading}
/>
</div>
</ContentContainer>
</CommonContainer>
</AdminView>
</AppContextProvider>
</>
)
}
export default UsersIndexPage

@ -47,7 +47,7 @@ export default function LoginForm() {
})
setInputValues({})
fetchUser(data?.session_key)
await fetchUser(data?.session_key)
setEduConnectAccessToken(data?.session_key)
router.push(routes.INDEX_PAGE)
}else{
@ -55,14 +55,14 @@ export default function LoginForm() {
toast({
title : 'Error logging in !',
description : `${data?.message}`,
variant : 'success'
variant : 'destructive'
})
}
}catch(e : any){
toast({
title : 'Error logging in !',
description : `${error?.message}`,
variant : 'success'
variant : 'destructive'
})
}finally{
setLoading(false)
@ -82,7 +82,7 @@ export default function LoginForm() {
},[router])
return (
<div className="flex items-center justify-center min-h-[60vh]">
<div className="flex items-center justify-center min-h-[60vh] border border-purple-700/20 shadow shadow-purple-700/40">
<Card className="w-full max-w-md shadow-lg border-0">
<CardHeader className="space-y-3">
<CardTitle className="text-2xl font-bold text-purple-600">Login</CardTitle>
@ -107,23 +107,12 @@ export default function LoginForm() {
<div className="relative">
<Input
id="password"
type={showPassword ? "text" : "password"}
type={"password"}
placeholder="Enter your password"
className="h-11 px-4 bg-gray-50/50 border-gray-200 focus:border-purple-500 focus:ring-purple-500 pr-10"
onChange={(e) => setInputValues((prev : any) => ({...prev , password : e.target.value}))}
/>
<button
type="button"
onClick={() => setShowPassword(!showPassword)}
className="absolute right-3 top-1/2 -translate-y-1/2 text-gray-500 hover:text-gray-700"
>
{showPassword ? (
<EyeOffIcon className="h-5 w-5" />
) : (
<EyeIcon className="h-5 w-5" />
)}
</button>
</div>
</div>

@ -1,13 +1,22 @@
import { ChangeEvent, FormEvent, useState } from 'react';
import { FormEvent, useContext, useEffect, useState } from 'react';
import { Card, CardHeader, CardTitle, CardDescription, CardContent } from "@/components/ui/card";
import { Label } from "@/components/ui/label";
import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";
import { EyeIcon, EyeOffIcon, Upload } from 'lucide-react';
import { EyeIcon, EyeOffIcon, Upload, X } from 'lucide-react';
import Link from 'next/link';
import { routes } from '@/lib/routes';
import { useToast } from '@/hooks/use-toast';
import { AuthContext } from '@/helpers/context/AuthProvider';
import { setEduConnectAccessToken } from '@/helpers/token.helper';
import { useRouter } from 'next/navigation';
export default function SignupForm() {
const router = useRouter();
const [showPassword, setShowPassword] = useState(false);
const [formData, setFormData] = useState({
const [loading, setLoading] = useState<boolean>(false);
const [previewUrl, setPreviewUrl] = useState<string | null>(null);
const [inputValues, setInputValues] = useState({
email: '',
firstName: '',
lastName: '',
@ -15,23 +24,131 @@ export default function SignupForm() {
password: '',
bio: '',
dob: '',
profile_picture: null
profile_picture: null as File | null
});
const [error , setError] = useState<Record<string,any>>({})
const [error, setError] = useState<Record<string, any>>({});
const handleInputChange = (e : ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
const { fetchUser, user } = useContext(AuthContext);
const { toast } = useToast();
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
const { id, value } = e.target;
setFormData(prev => ({
setInputValues(prev => ({
...prev,
[id]: value
}));
};
const handleSubmit = (e : FormEvent<HTMLFormElement>) => {
e.preventDefault();
console.log('Form submitted:', formData);
const handleFileUpload = (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (file) {
if (file.size > 5 * 1024 * 1024) {
setError(prev => ({
...prev,
profile_picture: ['File size should be less than 5MB']
}));
return;
}
if (!['image/jpeg', 'image/png', 'image/gif'].includes(file.type)) {
setError(prev => ({
...prev,
profile_picture: ['Please upload an image file (JPG, PNG, or GIF)']
}));
return;
}
// Create preview URL
const objectUrl = URL.createObjectURL(file);
setPreviewUrl(objectUrl);
setInputValues(prev => ({
...prev,
profile_picture: file
}));
// Clear any existing errors
setError(prev => ({
...prev,
profile_picture: null
}));
}
};
const removeImage = () => {
setInputValues(prev => ({
...prev,
profile_picture: null
}));
if (previewUrl) {
URL.revokeObjectURL(previewUrl);
setPreviewUrl(null);
}
};
const handleSubmit = async(e : FormEvent<HTMLFormElement>) : Promise<void> => {
e.preventDefault();
const formData = new FormData(e.currentTarget)
Object.entries(inputValues).forEach(([key, value]) => {
if(value){
formData.append(key, value);
}
});
try{
const res = await fetch(`${process.env.NEXT_PUBLIC_EDU_CONNECT_HOST}/api/profile/register` , {
method : 'POST' ,
body : formData
})
const data : any = await res.json();
if(res.status == 201){
toast({
title : 'Sucessfully registered in !',
description : `${data?.message}`,
variant : 'success'
})
setInputValues({
email: '',
firstName: '',
lastName: '',
username: '',
password: '',
bio: '',
dob: '',
profile_picture: null as File | null
})
router.push(routes.LOGIN_ROUTES)
}else{
setError(data?.error)
toast({
title : 'Error signing in !',
description : `${data?.error}`,
variant : 'destructive'
})
}
}catch(e : any){
toast({
title : 'Error signing in !',
description : `${error?.message}`,
variant : 'destructive'
})
}finally{
setLoading(false)
}
}
// Clean up preview URL when component unmounts
useEffect(() => {
return () => {
if (previewUrl) {
URL.revokeObjectURL(previewUrl);
}
};
}, [previewUrl]);
return (
<div className="flex items-center justify-center min-h-[80vh]">
<Card className="w-full max-w-lg shadow-lg border border-purple-700">
@ -44,24 +161,79 @@ export default function SignupForm() {
<CardContent>
<form onSubmit={handleSubmit} className="space-y-6">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="space-y-2 col-span-2">
<Label htmlFor="profile_picture" className="text-gray-700">Profile Picture</Label>
<div className="space-y-4">
{previewUrl ? (
<div className="relative w-32 h-32 mx-auto">
<img
src={previewUrl}
alt="Profile preview"
className="w-full h-full object-cover rounded-full"
/>
<button
type="button"
onClick={removeImage}
className="absolute -top-2 -right-2 p-1 bg-red-500 text-white rounded-full hover:bg-red-600 focus:outline-none focus:ring-2 focus:ring-red-500 focus:ring-offset-2"
>
<X className="h-4 w-4" />
</button>
</div>
) : (
<div className="flex items-center justify-center">
<div className="w-32 h-32 bg-gray-100 rounded-full flex items-center justify-center">
<Upload className="h-8 w-8 text-gray-400" />
</div>
</div>
)}
<div className="flex items-center justify-center space-x-2">
<Button
type="button"
variant="outline"
className="h-11 px-4 border-gray-200 hover:bg-gray-50"
onClick={() => document.getElementById('profile_picture_input')?.click()}
>
<Upload className="h-5 w-5 mr-2" />
{previewUrl ? 'Change Photo' : 'Upload Photo'}
</Button>
<input
id="profile_picture_input"
type="file"
accept="image/*"
className="hidden"
onChange={handleFileUpload}
/>
</div>
{error?.profile_picture && (
<p className="text-sm text-red-500 text-center">{error.profile_picture}</p>
)}
</div>
</div>
<div className="space-y-2">
<Label htmlFor="firstName" className="text-gray-700">First Name</Label>
<Input
id="firstName"
value={formData.firstName}
value={inputValues.firstName}
onChange={handleInputChange}
className="h-11 px-4 bg-gray-50/50 border-gray-200 focus:border-purple-500 focus:ring-purple-500"
className={`h-11 px-4 bg-gray-50/50 border-gray-200 focus:border-purple-500 focus:ring-purple-500 ${
error?.firstName ? 'border-red-500' : ''
}`}
placeholder="Enter your first name"
error={error?.firstName}
/>
</div>
<div className="space-y-2">
<Label htmlFor="lastName" className="text-gray-700">Last Name</Label>
<Input
id="lastName"
value={formData.lastName}
value={inputValues.lastName}
onChange={handleInputChange}
className="h-11 px-4 bg-gray-50/50 border-gray-200 focus:border-purple-500 focus:ring-purple-500"
className={`h-11 px-4 bg-gray-50/50 border-gray-200 focus:border-purple-500 focus:ring-purple-500 ${
error?.lastName ? 'border-red-500' : ''
}`}
placeholder="Enter your last name"
error={error?.lastName}
/>
@ -73,24 +245,30 @@ export default function SignupForm() {
<Input
id="email"
type="email"
value={formData.email}
value={inputValues.email}
onChange={handleInputChange}
className="h-11 px-4 bg-gray-50/50 border-gray-200 focus:border-purple-500 focus:ring-purple-500"
className={`h-11 px-4 bg-gray-50/50 border-gray-200 focus:border-purple-500 focus:ring-purple-500 ${
error?.email ? 'border-red-500' : ''
}`}
placeholder="Enter your email"
error={error?.email}
/>
</div>
<div className="space-y-2">
<Label htmlFor="username" className="text-gray-700">Username</Label>
<Input
id="username"
value={formData.username}
value={inputValues?.username}
onChange={handleInputChange}
className="h-11 px-4 bg-gray-50/50 border-gray-200 focus:border-purple-500 focus:ring-purple-500"
className={`h-11 px-4 bg-gray-50/50 border-gray-200 focus:border-purple-500 focus:ring-purple-500 ${
error?.username ? 'border-red-500' : ''
}`}
placeholder="Choose a username"
error={error?.username}
/>
</div>
<div className="space-y-2">
@ -99,23 +277,15 @@ export default function SignupForm() {
<Input
id="password"
type={showPassword ? "text" : "password"}
value={formData.password}
value={inputValues.password}
onChange={handleInputChange}
className="h-11 px-4 bg-gray-50/50 border-gray-200 focus:border-purple-500 focus:ring-purple-500 pr-10"
className={`h-11 px-4 bg-gray-50/50 border-gray-200 focus:border-purple-500 focus:ring-purple-500 pr-10 ${
error?.password ? 'border-red-500' : ''
}`}
placeholder="Create a strong password"
error={error?.password}
/>
<button
type="button"
onClick={() => setShowPassword(!showPassword)}
className="absolute right-3 top-1/2 -translate-y-1/2 text-gray-500 hover:text-gray-700"
>
{showPassword ? (
<EyeOffIcon className="h-5 w-5" />
) : (
<EyeIcon className="h-5 w-5" />
)}
</button>
</div>
</div>
@ -124,59 +294,46 @@ export default function SignupForm() {
<Input
id="dob"
type="date"
value={formData.dob}
value={inputValues.dob}
onChange={handleInputChange}
error={error?.dob}
className="h-11 px-4 bg-gray-50/50 border-gray-200 focus:border-purple-500 focus:ring-purple-500"
className={`h-11 px-4 bg-gray-50/50 border-gray-200 focus:border-purple-500 focus:ring-purple-500 ${
error?.dob ? 'border-red-500' : ''
}`}
error={error?.firstName}
/>
</div>
<div className="space-y-2">
<Label htmlFor="bio" className="text-gray-700">Bio</Label>
<textarea
id="bio"
value={formData.bio}
value={inputValues.bio}
onChange={handleInputChange}
className="w-full px-4 py-2 bg-gray-50/50 border-gray-200 rounded-md focus:border-purple-500 focus:ring-purple-500"
className={`w-full px-4 py-2 bg-gray-50/50 border-gray-200 rounded-md focus:border-purple-500 focus:ring-purple-500 ${
error?.bio ? 'border-red-500' : ''
}`}
rows={3}
placeholder="Tell us about yourself"
/>
{
error?.bio && error?.bio.map((err : any) => {
return(
<span className='text-red-500'>{err}</span>
)
})
}
</div>
<div className="space-y-2">
<Label htmlFor="profile_picture" className="text-gray-700">Profile Picture</Label>
<div className="flex items-center space-x-2">
<Button
type="button"
variant="outline"
className="h-11 px-4 border-gray-200 hover:bg-gray-50"
>
<Upload className="h-5 w-5 mr-2" />
Upload Photo
</Button>
<span className="text-sm text-gray-500">No file chosen</span>
</div>
{error?.bio && (
<p className="text-sm text-red-500">{error.bio}</p>
)}
</div>
<Button
type="submit"
className="w-full h-11 bg-purple-600 hover:bg-purple-700 text-white font-semibold"
className="w-full h-11 bg-purple-600 hover:bg-purple-700 text-white font-semibold disabled:opacity-50"
disabled={loading}
>
Create Account
{loading ? 'Creating Account...' : 'Create Account'}
</Button>
<div className="text-center text-sm text-gray-600">
Already have an account?{' '}
<a href="#" className="text-purple-600 hover:text-purple-700 font-semibold">
<Link href={routes.LOGIN_ROUTES} className="text-purple-600 hover:text-purple-700 font-semibold">
Login
</a>
</Link>
</div>
</form>
</CardContent>

@ -4,6 +4,7 @@
body {
font-family: Arial, Helvetica, sans-serif;
user-select: none;
}
@layer utilities {

@ -0,0 +1,179 @@
import { useState, useEffect } from 'react';
import { Card, CardContent } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Upload, X, AlertCircle } from 'lucide-react';
import { Alert, AlertDescription } from "@/components/ui/alert";
interface BannerUploadProps {
onImageChange?: (file: File | null) => void;
defaultImage?: string;
maxSizeMB?: number;
}
export default function BannerImageUpload({
onImageChange,
defaultImage,
maxSizeMB = 5
}: BannerUploadProps) {
const [previewUrl, setPreviewUrl] = useState<string | null>(defaultImage || null);
const [error, setError] = useState<string | null>(null);
const [isDragging, setIsDragging] = useState(false);
const acceptedTypes = ['image/jpeg', 'image/png', 'image/webp'];
const maxSize = maxSizeMB * 1024 * 1024; // Convert MB to bytes
const validateFile = (file: File): string | null => {
if (!acceptedTypes.includes(file.type)) {
return 'Please upload a valid image file (JPG, PNG, or WebP)';
}
if (file.size > maxSize) {
return `File size should be less than ${maxSizeMB}MB`;
}
return null;
};
const handleFile = (file: File) => {
const validationError = validateFile(file);
if (validationError) {
setError(validationError);
return;
}
// Clear any existing error
setError(null);
// Create preview URL
const objectUrl = URL.createObjectURL(file);
setPreviewUrl(objectUrl);
// Notify parent component
if (onImageChange) {
onImageChange(file);
}
};
const handleFileSelect = (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (file) {
handleFile(file);
}
};
const handleDrop = (e: React.DragEvent<HTMLDivElement>) => {
e.preventDefault();
setIsDragging(false);
const file = e.dataTransfer.files?.[0];
if (file) {
handleFile(file);
}
};
const handleDragOver = (e: React.DragEvent<HTMLDivElement>) => {
e.preventDefault();
setIsDragging(true);
};
const handleDragLeave = (e: React.DragEvent<HTMLDivElement>) => {
e.preventDefault();
setIsDragging(false);
};
const removeBanner = () => {
if (previewUrl) {
URL.revokeObjectURL(previewUrl);
}
setPreviewUrl(null);
setError(null);
if (onImageChange) {
onImageChange(null);
}
};
// Cleanup preview URL when component unmounts
useEffect(() => {
return () => {
if (previewUrl && previewUrl !== defaultImage) {
URL.revokeObjectURL(previewUrl);
}
};
}, [previewUrl, defaultImage]);
return (
<div className="space-y-4 col-span-2">
<label>Upload Course Banner</label>
<div
className={`relative w-full h-64 rounded-lg overflow-hidden border-2 border-dashed transition-colors
${isDragging ? 'border-purple-500 bg-purple-50' : 'border-gray-300 bg-gray-50'}
${error ? 'border-red-300' : ''}`}
onDrop={handleDrop}
onDragOver={handleDragOver}
onDragLeave={handleDragLeave}
>
{previewUrl ? (
<>
<img
src={previewUrl}
alt="Banner preview"
className="w-full h-full object-cover"
/>
<div className="absolute inset-0 bg-black bg-opacity-40 opacity-0 hover:opacity-100 transition-opacity">
<div className="absolute inset-0 flex items-center justify-center space-x-4">
<Button
type="button"
variant="outline"
className="bg-white hover:bg-gray-100"
onClick={() => document.getElementById('banner-upload')?.click()}
>
<Upload className="h-5 w-5 mr-2" />
Change Banner
</Button>
<Button
type="button"
variant="destructive"
onClick={removeBanner}
>
<X className="h-5 w-5 mr-2" />
Remove Banner
</Button>
</div>
</div>
</>
) : (
<div className="absolute inset-0 flex flex-col items-center justify-center">
<Upload className="h-12 w-12 text-gray-400 mb-4" />
<div className="text-center">
<Button
type="button"
variant="outline"
className="mb-2"
onClick={() => document.getElementById('banner-upload')?.click()}
>
Choose File
</Button>
<p className="text-sm text-gray-500">or drag and drop your image here</p>
<p className="text-xs text-gray-400 mt-2">
Supports: JPG, PNG, WebP (max {maxSizeMB}MB)
</p>
</div>
</div>
)}
<input
id="banner-upload"
type="file"
className="hidden"
accept={acceptedTypes.join(',')}
onChange={handleFileSelect}
/>
</div>
{error && (
<Alert variant="destructive">
<AlertCircle className="h-4 w-4" />
<AlertDescription>{error}</AlertDescription>
</Alert>
)}
</div>
);
}

@ -0,0 +1,8 @@
export const CourseCategoryOptionHelper = (data: Record<string, string>[]) : any => {
return data?.length > 0
? data.map((item) => ({
value: item?.id?.toString() ? item?.id?.toString() : item?.idx,
label: item?.title ? item.title : item?.name,
}))
: [];
}

@ -1,5 +1,5 @@
'use client';
import React from 'react';
import React, { useEffect, useState } from 'react';
import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";
import { Label } from "@/components/ui/label";
@ -12,105 +12,289 @@ import {
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { TabsContent } from '@/components/ui/tabs';
import { Loader2 } from "lucide-react";
import useSWR from 'swr';
import { APP_BASE_URL } from '@/utils/constants';
import { defaultFetcher, fetchHeader } from '@/helpers/fetch.helper';
import { CourseCategoryOptionHelper } from './CourseCategoryOptionHelper';
import BannerImageUpload from './BannerImageUpload';
import { useToast } from '@/hooks/use-toast';
import { useRouter } from 'next/navigation';
import { routes } from '@/lib/routes';
const CourseActionFormTabContent :React.FC = () => {
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
const CourseActionFormTabContent: React.FC = () => {
// Fetch categories
const { toast } = useToast();
const router = useRouter()
const { data: CategoryOption } = useSWR(APP_BASE_URL + '/api/course/getCategories', defaultFetcher);
// Form state
const [formData, setFormData] = useState({
course_name: '',
category_uuid: '',
course_description: '',
page_for_community: '',
course_pdf: null as File | null ,
cover_image : null as File | null
});
// UI states
const [isSubmitting, setIsSubmitting] = useState(false);
const [errors, setErrors] = useState<Record<string, string>>({});
const [submitStatus, setSubmitStatus] = useState<{
type: 'success' | 'error' | null;
message: string;
}>({ type: null, message: '' });
const [categoryOptions, setCategoryOptions] = useState<any>([]);
// Update category options when data is fetched
useEffect(() => {
CategoryOption && setCategoryOptions(CourseCategoryOptionHelper(CategoryOption));
}, [CategoryOption]);
// Handle input changes
const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
const { name, value } = e.target;
setFormData(prev => ({
...prev,
[name]: value
}));
// Clear error when user types
if (errors[name]) {
setErrors(prev => ({
...prev,
[name]: ''
}));
}
};
// Handle select change
const handleSelectChange = (value: string) => {
console.log(value)
setFormData(prev => ({
...prev,
category_uuid: value
}));
};
// Handle file change
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0] || null;
setFormData(prev => ({
...prev,
coursePdf: file
}));
if (errors.coursePdf) {
setErrors(prev => ({
...prev,
coursePdf: ''
}));
}
};
// Validate form
// Handle form submission
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
// Handle form submission
const formData = new FormData(e.currentTarget);
console.log('Form submitted:', Object.fromEntries(formData));
setIsSubmitting(true);
setSubmitStatus({ type: null, message: '' });
try {
// Create FormData for file upload
const submitFormData = new FormData();
Object.entries(formData).forEach(([key, value]) => {
if (value !== null) {
submitFormData.append(key, value);
}
});
// Make API call
const response = await fetch(`${APP_BASE_URL}/api/course/createCourse`, {
headers : fetchHeader(),
method: 'POST',
body: submitFormData
});
if (!response.ok) {
toast({
title : 'Failed to completete Task !' ,
description : 'Something went wrong creating course' ,
variant : 'destructive'
})
return;
}
const data : any = await response.json();
// Show success message
toast({
title : 'SuccessFul !' ,
description : `${data?.message}` ,
variant : 'success'
})
// Reset form
setFormData({
course_name: '',
category_uuid: '',
course_description: '',
page_for_community: '',
course_pdf: null ,
cover_image : null
});
// Reset file input
const fileInput = document.getElementById('coursePdf') as HTMLInputElement;
if (fileInput) fileInput.value = '';
router.push(routes.MY_COURSES_INDEX)
} catch (error) {
} finally {
setIsSubmitting(false);
}
};
const handleImageChange = (file: File | null) => {
setFormData((prev) => ({...prev , banner_image : file}))
}
return (
<Card>
<CardHeader>
<CardTitle className="text-2xl">Create New Course</CardTitle>
</CardHeader>
<CardContent>
<form onSubmit={handleSubmit} className="space-y-6">
{/* Course Title */}
<div className="space-y-2">
<Label htmlFor="title">Course Title</Label>
<Input
id="title"
name="title"
placeholder="Enter course title"
required
className="w-full"
/>
</div>
{/* Course Category */}
<div className="space-y-2">
<Label htmlFor="category">Category</Label>
<Select name="category">
<SelectTrigger>
<SelectValue placeholder="Select a category" />
</SelectTrigger>
<SelectContent>
<SelectItem value="programming">Programming</SelectItem>
<SelectItem value="design">Design</SelectItem>
<SelectItem value="business">Business</SelectItem>
<SelectItem value="marketing">Marketing</SelectItem>
</SelectContent>
</Select>
</div>
{/* Course Description */}
<div className="space-y-2">
<Label htmlFor="description">Description</Label>
<Textarea
id="description"
name="description"
placeholder="Enter course description"
className="min-h-32"
/>
</div>
{/* Estimated Learning Hours */}
<div className="space-y-2">
<Label htmlFor="estimatedHours">Estimated Learning Hours</Label>
<Input
id="estimatedHours"
name="estimatedHours"
type="number"
min="1"
placeholder="Enter estimated hours"
/>
</div>
{/* Required Pages */}
<div className="space-y-2">
<Label htmlFor="requiredPages">Required Pages</Label>
<Input
id="requiredPages"
name="requiredPages"
type="number"
min="1"
placeholder="Enter number of required pages"
/>
</div>
{/* PDF Upload */}
<div className="space-y-2">
<Label htmlFor="coursePdf">Course PDF</Label>
<Input
id="coursePdf"
name="coursePdf"
type="file"
accept=".pdf"
className="cursor-pointer"
/>
</div>
{/* Submit Button */}
<div className="mx-auto w-fit">
<Button className="mt-6 w-full md:min-w-[600px] min-w-[300px] bg-purple-700">Save Changes</Button>
</div>
</form>
</CardContent>
</Card>
<Card className='border border-purple-700/20 shadow shadow-purple-700/40'>
<CardHeader>
<CardTitle className="text-2xl text-purple-700">Create New Course</CardTitle>
</CardHeader>
<CardContent>
<form onSubmit={handleSubmit} className="grid grid-cols-2 gap-6">
<BannerImageUpload
onImageChange={handleImageChange}
defaultImage={formData?.cover_image as string}
maxSizeMB={2}
/>
{/* Course Title */}
<div className="space-y-2">
<Label htmlFor="course_name">Course Name</Label>
<Input
id="course_name"
name="course_name"
value={formData.course_name}
onChange={handleChange}
placeholder="Enter course name"
className={`w-full ${errors.course_name ? 'border-red-500' : ''}`}
/>
{errors.course_name && (
<p className="text-sm text-red-500">{errors.course_name}</p>
)}
</div>
{/* Course Category */}
<div className="space-y-2">
<Label htmlFor="category_uuid">Category</Label>
<Select
name="category_uuid"
value={formData.category_uuid}
onValueChange={handleSelectChange}
>
<SelectTrigger className={errors.category_uuid ? 'border-red-500' : ''}>
<SelectValue placeholder="Select a category" />
</SelectTrigger>
<SelectContent>
{categoryOptions?.map((categoryOption: any, index: number) => (
<SelectItem
value={categoryOption?.value}
key={index}
>
{categoryOption?.label}
</SelectItem>
))}
</SelectContent>
</Select>
{errors?.category && (
<p className="text-sm text-red-500">{errors.category}</p>
)}
</div>
<div className="space-y-2">
<Label htmlFor="coursePdf">Course PDF</Label>
<Input
id="course_pdf"
name="course_pdf"
type="file"
accept=".pdf"
onChange={handleFileChange}
className={`cursor-pointer ${errors.course_pdf ? 'border-red-500' : ''}`}
/>
{errors?.coursePdf && (
<p className="text-sm text-red-500">{errors.coursePdf}</p>
)}
</div>
{/* Required Pages */}
<div className="space-y-2">
<Label htmlFor="requiredPages">Required Pages for Discussion Joining ?</Label>
<Input
id="page_for_community"
name="page_for_community"
type="number"
value={formData?.page_for_community}
onChange={handleChange}
min="1"
placeholder="Enter number of required pages"
className={errors.page_for_community ? 'border-red-500' : ''}
/>
{errors.page_for_community && (
<p className="text-sm text-red-500">{errors.page_for_community}</p>
)}
</div>
{/* Course Description */}
<div className="space-y-2 col-span-2">
<Label htmlFor="description">Description</Label>
<Textarea
id="course_description"
name="course_description"
value={formData?.course_description}
onChange={handleChange}
placeholder="Enter course course_description"
className={`min-h-32 ${errors.course_description ? 'border-red-500' : ''}`}
/>
{errors.course_description && (
<p className="text-sm text-red-500">{errors.course_description}</p>
)}
</div>
{/* PDF Upload */}
{/* Submit Button */}
<div className="mx-auto w-fit col-span-2">
<Button
type="submit"
className="mt-6 w-full md:min-w-[400px] min-w-[300px] bg-purple-700"
disabled={isSubmitting}
>
{isSubmitting ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
Creating Course...
</>
) : (
'Save Changes'
)}
</Button>
</div>
</form>
</CardContent>
</Card>
);
};

@ -0,0 +1,74 @@
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
import { defaultFetcher } from "@/helpers/fetch.helper"
import { APP_BASE_URL } from "@/utils/constants"
import Image from "next/image"
import useSWR from "swr"
const EnrolledCourseTabContent : React.FC = () => {
const { data } = useSWR(APP_BASE_URL + '/' , defaultFetcher);
return (
<>
<Card className='border border-purple-700/20 shadow shadow-purple-700/40'>
<CardHeader>
<CardTitle className="text-2xl text-purple-700">Enrolled Courses List </CardTitle>
</CardHeader>
<CardContent>
{
data?.length && data.map((course : Record<string,any> , index : number) => {
return(
<>
<Card
key={course.id}
className="hover:shadow-lg transition-shadow"
>
<CardContent className="p-6 flex gap-4">
<div >
<Image src="https://placehold.jp/400x250.png"alt="placeholder" width={400} height={250} className="w-[400] h-auto rounded-2xl"/>
</div>
<div>
<div className="flex items-start justify-between">
<div className="space-y-2">
<h3 className="text-xl font-semibold text-purple-700">
{course.title}
</h3>
<p className="text-gray-600">{course.description}</p>
</div>
</div>
<div className="mt-4 flex flex-wrap gap-4 text-sm text-gray-500">
<span className="flex items-center">
<span className="font-medium">Published:</span>
<span className="ml-1">{course.publishedDate}</span>
</span>
<span className="flex items-center">
<span className="font-medium">Enrolled:</span>
<span className="ml-1">
{course.enrolledStudent} students
</span>
</span>
<span className="flex items-center">
<span className="font-medium">Active Discussions:</span>
<span className="ml-1">
{course.activeDiscussionCount}
</span>
</span>
<span className="flex items-center">
<span className="font-medium">Published by:</span>
<span className="ml-1">{course.publishedBy}</span>
</span>
</div>
</div>
</CardContent>
</Card>
</>
)
})
}
</CardContent>
</Card>
</>
)
}
export default EnrolledCourseTabContent

@ -0,0 +1,74 @@
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
import { defaultFetcher } from "@/helpers/fetch.helper"
import { APP_BASE_URL } from "@/utils/constants"
import Image from "next/image"
import useSWR from "swr"
const MyCoursesListTabContent = () => {
const { data } = useSWR(APP_BASE_URL + '/' , defaultFetcher);
return (
<>
<Card className='border border-purple-700/20 shadow shadow-purple-700/40'>
<CardHeader>
<CardTitle className="text-2xl text-purple-700">My Courses</CardTitle>
</CardHeader>
<CardContent>
{
data?.length && data.map((course : Record<string,any> , index : number) => {
return(
<>
<Card
key={course.id}
className="hover:shadow-lg transition-shadow"
>
<CardContent className="p-6 flex gap-4">
<div >
<Image src="https://placehold.jp/400x250.png"alt="placeholder" width={400} height={250} className="w-[400] h-auto rounded-2xl"/>
</div>
<div>
<div className="flex items-start justify-between">
<div className="space-y-2">
<h3 className="text-xl font-semibold text-purple-700">
{course.title}
</h3>
<p className="text-gray-600">{course.description}</p>
</div>
</div>
<div className="mt-4 flex flex-wrap gap-4 text-sm text-gray-500">
<span className="flex items-center">
<span className="font-medium">Published:</span>
<span className="ml-1">{course.publishedDate}</span>
</span>
<span className="flex items-center">
<span className="font-medium">Enrolled:</span>
<span className="ml-1">
{course.enrolledStudent} students
</span>
</span>
<span className="flex items-center">
<span className="font-medium">Active Discussions:</span>
<span className="ml-1">
{course.activeDiscussionCount}
</span>
</span>
<span className="flex items-center">
<span className="font-medium">Published by:</span>
<span className="ml-1">{course.publishedBy}</span>
</span>
</div>
</div>
</CardContent>
</Card>
</>
)
})
}
</CardContent>
</Card>
</>
)
}
export default MyCoursesListTabContent

@ -1,37 +1,46 @@
import React from 'react';
import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui/tabs";
import CourseActionFormTabContent from './CoursesActionModal';
import MyCoursesListTabContent from './MyCoursesListTabContent';
import EnrolledCourseTabContent from './EnrolledCourseList';
const MyCoursesWrapper = () => {
return (
<div className="container mx-auto py-8">
<Tabs defaultValue="general" className="flex gap-6 min-h-[50vh]">
<Tabs defaultValue="my-courses" className="flex gap-6 min-h-[50vh]">
<div className="min-w-[240px]">
<TabsList className="flex flex-col h-auto bg-transparent p-0">
<TabsTrigger
value="generali"
className="justify-start w-full px-3 py-2 text-left data-[state=active]:bg-gray-50 data-[state=active]:border data-[state=active]:border-purple-700/50 rounded-md "
value="my-courses"
className="justify-start w-full px-3 py-2 text-left data-[state=active]:bg-gray-50 data-[state=active]:border data-[state=active]:border-purple-700/50 rounded-md"
>
Course action
My Courses
</TabsTrigger>
<TabsTrigger
value="email"
value="enrolled"
className="justify-start w-full px-3 py-2 text-left data-[state=active]:bg-gray-50 data-[state=active]:border data-[state=active]:border-purple-700/50 rounded-md"
>
Course List
Enrolled Course List
</TabsTrigger>
<TabsTrigger
value="password"
className="justify-start w-full px-3 py-2 text-left data-[state=active]:bg-gray-50 data-[state=active]:border data-[state=active]:border-purple-700/50 rounded-md"
value="action-form"
className="justify-start w-full px-3 py-2 text-left data-[state=active]:bg-gray-50 data-[state=active]:border data-[state=active]:border-purple-700/50 rounded-md "
>
Security
Course action
</TabsTrigger>
</TabsList>
</div>
<div className="flex-1">
<TabsContent value="general" className="mt-0">
{/* <MyCoursesActionContent /> */}
<TabsContent value="my-courses" className="mt-0">
<MyCoursesListTabContent />
</TabsContent>
<TabsContent value="enrolled" className="mt-0">
<EnrolledCourseTabContent />
</TabsContent>
<TabsContent value="action-form" className="mt-0">
<CourseActionFormTabContent />
</TabsContent>
</div>

@ -5,10 +5,12 @@ import {
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { Input } from "@/components/ui/input";
import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui/tabs";
import { TabsContent } from "@/components/ui/tabs";
import { AuthContext } from "@/helpers/context/AuthProvider";
import { useContext } from "react";
const UserEmailUpdateTabContent = () => {
const { user } = useContext(AuthContext)
return(
<>
<TabsContent value="email" className="mt-0">
@ -26,7 +28,7 @@ const UserEmailUpdateTabContent = () => {
<label className="text-sm font-medium">Primary Email Address</label>
<div className="p-4 border rounded-lg space-y-4">
<div className="flex items-center justify-between">
<span>your@email.com</span>
<span>{user?.email}</span>
<span className="text-sm text-green-600"> Verified</span>
{
false &&

@ -7,8 +7,73 @@ import {
} from "@/components/ui/card";
import { Input } from "@/components/ui/input";
import { TabsContent } from "@/components/ui/tabs";
import { fetchHeader } from "@/helpers/fetch.helper";
import { useToast } from "@/hooks/use-toast";
import { APP_BASE_URL } from "@/utils/constants";
import { useState } from "react";
const UserPasswordUpdateTabContent = () => {
const [inputValues , setInputValues] = useState<Record<string,any>>({});
const [loading ,setLoading] = useState<boolean>(false);
const [error , setError] = useState<Record<string,any>>({})
const { toast } = useToast()
const handleInputValuesChange = (key : string , value : string) => {
setInputValues((prev : any) => ({...prev , [key] : value}))
}
const handlePasswordChange = async () : Promise<void> => {
setLoading(true)
const formData = new FormData();
Object.entries(inputValues).forEach(([key, value] : [key : string , value : any]) => {
if(key == 'profile_picture'){
if(value instanceof File){
formData.append(key, value);
}
}else{
formData.append(key, value);
}
});
try{
const res = await fetch(APP_BASE_URL + '/api/profile/change-password' , {
headers : fetchHeader() ,
body : formData ,
method : 'POST'
})
const data = await res.json()
if(res.status == 200){
toast({
title : 'Sucessfully updated password',
description : `${data?.message}`,
variant : 'success'
})
setInputValues({})
setError({})
}else{
toast({
title : 'Error updating password',
description : `${data?.error}`,
variant : 'destructive'
})
}
}catch(error : any){
toast({
title : 'Error updating password',
description : `${error?.message}`,
variant : 'destructive'
})
}
}
return(
<TabsContent value="password" className="mt-0">
<Card>
@ -21,21 +86,40 @@ const UserPasswordUpdateTabContent = () => {
<CardContent className="space-y-6">
<div className="space-y-2">
<label className="text-sm font-medium">Current Password</label>
<Input type="password" required />
<Input
type="password"
required
name="current_password"
onChange={(e) => handleInputValuesChange('current_password' , e.target.value)}
error={error?.current_password}
value={inputValues?.current_password ?? ''}
/>
</div>
<div className="space-y-2">
<label className="text-sm font-medium">New Password</label>
<Input type="password" required/>
<Input
type="password" required
onChange={(e) => handleInputValuesChange('new_password' , e.target.value)}
error={error?.new_password}
value={inputValues?.new_password ?? ''}
/>
</div>
<div className="space-y-2">
<label className="text-sm font-medium">Confirm New Password</label>
<Input type="password" required/>
<Input
type="password"
required
name="confirm_password"
error={error?.confirm_password}
value={inputValues?.confirm_password ?? ''}
onChange={(e) => handleInputValuesChange('confirm_password' , e.target.value)}
/>
</div>
<div className="mx-auto w-fit">
<Button className="mt-6 w-full md:min-w-[600px] min-w-[300px] bg-purple-700">Save Changes</Button>
<Button className="mt-6 w-full md:min-w-[600px] min-w-[300px] bg-purple-700" onClick={handlePasswordChange}>Save Changes</Button>
</div>
</CardContent>
</Card>

@ -113,8 +113,8 @@ const UserTabContent = () => {
fetchUser(getEduConnectAccessToken() !)
}else{
toast({
title : 'Sucessfully Updated profile',
description : `${data?.message}`,
title : 'Error updating profile',
description : `${data?.error}`,
variant : 'success'
})
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

@ -7,6 +7,7 @@ import Link from "next/link";
import { useContext } from "react";
import { SidebarToggle } from "./_partials/sidebarToggle";
import { Menu } from "./_partials/menu";
import BrandImage from '@/assets/img/brandLogo.png'
export default function AppSideBar() {
@ -39,7 +40,7 @@ export default function AppSideBar() {
>
</Button>
<Link href="/dashboard" className="block py-1">
<Image src={'https://www.kumarijob.com/soft-assets/images/logo.svg'} width={150} height={300} alt="brand-logo" className="mx-auto block"/>
<Image src={BrandImage} width={100} height={50} alt="brand-logo" className="mx-auto block bg-blend-darken"/>
</Link>
<Menu isOpen={getOpenState()} />
</div>

@ -8,6 +8,8 @@ import Link from 'next/link';
import { routes } from '@/lib/routes';
import { AuthContext } from '@/helpers/context/AuthProvider';
import UserProfileDropDown from './_partials/userProfileDropDown';
import BrandLogo from '@/assets/img/brandLogo.png'
import Image from 'next/image';
const Header = () => {
const [isMenuOpen, setIsMenuOpen] = useState(false);
@ -24,7 +26,12 @@ const Header = () => {
<div className="container mx-auto px-4">
<div className="flex items-center justify-between py-4">
{/* Logo */}
<h1 className="brand-logo text-purple-700 font-bold text-2xl">EDU CONNECT</h1>
<div className='flex gap-2 items-center'>
<Image src={BrandLogo} width={50} height={30} alt={'brand_logo'} className='bg-blend-darken'/>
<h1 className="brand-logo text-purple-700 font-bold text-2xl">
EDU CONNECT
</h1>
</div>
<SearchBar />
<div className='flex gap-8'>
<DesktopNav />

@ -0,0 +1,59 @@
import * as React from "react"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const alertVariants = cva(
"relative w-full rounded-lg border px-4 py-3 text-sm [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground [&>svg~*]:pl-7",
{
variants: {
variant: {
default: "bg-background text-foreground",
destructive:
"border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive",
},
},
defaultVariants: {
variant: "default",
},
}
)
const Alert = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement> & VariantProps<typeof alertVariants>
>(({ className, variant, ...props }, ref) => (
<div
ref={ref}
role="alert"
className={cn(alertVariants({ variant }), className)}
{...props}
/>
))
Alert.displayName = "Alert"
const AlertTitle = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLHeadingElement>
>(({ className, ...props }, ref) => (
<h5
ref={ref}
className={cn("mb-1 font-medium leading-none tracking-tight", className)}
{...props}
/>
))
AlertTitle.displayName = "AlertTitle"
const AlertDescription = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLParagraphElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("text-sm [&_p]:leading-relaxed", className)}
{...props}
/>
))
AlertDescription.displayName = "AlertDescription"
export { Alert, AlertTitle, AlertDescription }

@ -14,6 +14,8 @@ const badgeVariants = cva(
"border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
destructive:
"border-transparent bg-destructive text-destructive-foreground shadow hover:bg-destructive/80",
success:
"border-transparent bg-green-700 text-white shadow hover:bg-green-700/80",
outline: "text-foreground",
},
},

@ -1,5 +1,6 @@
import * as React from "react";
import { cn } from "@/lib/utils";
import { Eye, EyeClosed, EyeClosedIcon, EyeOffIcon } from "lucide-react";
interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {
error?: string[];
@ -10,6 +11,8 @@ interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {
const Input = React.forwardRef<HTMLInputElement, InputProps>(
({ className, type, error, label, helperText, required = false, ...props }, ref) => {
const [showPassword , setShowPassword] = React.useState<boolean>(false)
return (
<div className="w-full space-y-2">
{label && (
@ -18,16 +21,26 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(
{required && <span className="text-red-500 ml-1">*</span>}
</div>
)}
<input
type={type}
className={cn(
"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
error && error.length > 0 && "border-red-500 focus-visible:ring-red-500",
className
)}
ref={ref}
{...props}
/>
<div className="relative">
<input
type={type == 'password' ? showPassword ? 'text' : 'password' : type}
className={cn(
"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
error && error.length > 0 && "border-red-500 focus-visible:ring-red-500",
className
)}
ref={ref}
{...props}
/>
{
type == 'password' &&
<button type="button" onClick={() => setShowPassword((prev) => !prev)} className="absolute top-[50%] translate-y-[-50%] right-4">
{
showPassword ? <Eye className="w-4 h-4"/> : <EyeOffIcon className="w-4 h-4"/>
}
</button>
}
</div>
{helperText && (
<p className="text-sm text-muted-foreground">
{helperText}

@ -3,7 +3,9 @@
import React, { ReactNode, createContext, useEffect, useState } from "react";
import Cookies from "js-cookie";
import { User } from "../apiSchema/user.schema";
import { getEduConnectAccessToken } from "../token.helper";
import { getEduConnectAccessToken, removeEduConnectAccessToken } from "../token.helper";
import { usePathname, useRouter } from "next/navigation";
import { privateRoutes, routes } from "@/lib/routes";
interface AuthContextProps {
user: User | null;
@ -26,6 +28,8 @@ const AuthContext = createContext<AuthContextProps>({
const AuthProvider: React.FC<AuthProviderProps> = ({ children }) => {
const router = useRouter()
const pathname = usePathname()
const [user, setUser] = useState<User | null>(null);
const [loading, setLoading] = useState(true);
@ -40,10 +44,18 @@ const AuthProvider: React.FC<AuthProviderProps> = ({ children }) => {
},
});
if(userResponse.status == 401){
removeEduConnectAccessToken();
if(privateRoutes.includes(pathname)) router.replace(routes.INDEX_PAGE)
return
}
const userData = await userResponse.json();
setUser(userData.profile);
} catch (error) {
console.error("Fetch error:", error);
removeEduConnectAccessToken();
// 401
} finally {
setLoading(false);
}
@ -64,10 +76,11 @@ const AuthProvider: React.FC<AuthProviderProps> = ({ children }) => {
Authorization: `Bearer ${getEduConnectAccessToken()}`,
},
});
fetchUser();
} catch (error) {
console.error("Logout error:", error);
} finally {
Cookies.remove("HRMS_ACCESS_TOKEN", { domain: process.env.APP_DOMAIN });
Cookies.remove("EDU_CONNECT_ACCESS_TOKEN", { domain: process.env.APP_DOMAIN });
setUser(null);
}
};

@ -3,5 +3,9 @@ export const routes = {
LOGIN_ROUTES : '/auth/login',
REGISTER_ROUTES : '/auth/register',
DASHBOARD_ROUTE : '/admin/dashboard',
PROFILE_ROUTE : '/user/profile'
PROFILE_ROUTE : '/user/profile',
MY_COURSES_INDEX : '/my-courses',
USER_INDEX_PAGE : '/admin/users'
}
export const privateRoutes = ['/user/profile']

Loading…
Cancel
Save