diff --git a/frontend/src/App.css b/frontend/src/App.css index 112fb01..976a4ac 100644 --- a/frontend/src/App.css +++ b/frontend/src/App.css @@ -40,6 +40,22 @@ } } +.d-flex { + display: flex; +} + +.gap-2 { + gap: 0.5rem; +} + +.gap-4 { + gap: 1rem; +} + +.flex-column { + flex-direction: column; +} + @media (prefers-reduced-motion: no-preference) { a:nth-of-type(2) .logo { animation: logo-spin infinite 20s linear; diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 409c19f..6da55c4 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -8,6 +8,7 @@ import Dashboard from './pages/Dashboard/Dashboard'; import ReviewPaper from './pages/ReviewPaper/ReviewPaper'; import UserManagement from './pages/UserManagement/UserManagement'; import ProjectManagement from './pages/ProjectManagement/ProjectManagement'; +import ProtectedRoute from './components/routes/protectedRoute'; function App() { const { user } = useUserStore(); @@ -20,9 +21,9 @@ function App() { } /> - } /> - } /> - } /> + {}} /> + } /> + } /> 404 - Not Found} /> diff --git a/frontend/src/components/ItemManagement.tsx b/frontend/src/components/ItemManagement.tsx index d0b7171..709e903 100644 --- a/frontend/src/components/ItemManagement.tsx +++ b/frontend/src/components/ItemManagement.tsx @@ -19,16 +19,17 @@ const sampleItems: Item[] = [ type ItemManagementProps = { pageName: string; + onAdd: () => void; }; -const ItemManagement: React.FC = ({ pageName }) => { +const ItemManagement: React.FC = ({ pageName, onAdd }) => { const items = sampleItems; return (

{pageName} Management

-
diff --git a/frontend/src/components/Modal.tsx b/frontend/src/components/Modal.tsx new file mode 100644 index 0000000..f60004d --- /dev/null +++ b/frontend/src/components/Modal.tsx @@ -0,0 +1,82 @@ +import React, { useEffect } from "react"; + +type ModalProps = { + title: string; + children: React.ReactNode; + onClose: () => void; +}; + +const Modal: React.FC = ({ title, children, onClose }) => { + + useEffect(() => { + const originalOverflow = document.body.style.overflow; + document.body.style.overflow = "hidden"; + + return () => { + document.body.style.overflow = originalOverflow; + }; + }, []); + + return ( +
+
e.stopPropagation()}> +
+

{title}

+ +
+ +
{children}
+
+
+ ); +}; + +export default Modal; + +const styles: { [key: string]: React.CSSProperties } = { + overlay: { + position: "fixed", + inset: 0, + backgroundColor: "rgba(0,0,0,0.4)", + backdropFilter: "blur(2px)", + display: "flex", + justifyContent: "center", + alignItems: "center", + zIndex: 1000, + animation: "fadeIn 0.15s ease", + padding: "20px", + }, + modal: { + background: "#fff", + width: "100%", + maxWidth: "460px", + maxHeight: "85vh", + overflowY: "auto", // content scrolls, not background + borderRadius: "12px", + boxShadow: "0 4px 22px rgba(0,0,0,0.2)", + padding: "22px", + animation: "zoomIn 0.2s ease", + }, + header: { + display: "flex", + justifyContent: "space-between", + alignItems: "center", + marginBottom: "14px", + borderBottom: "1px solid #eee", + paddingBottom: "10px", + }, + closeBtn: { + background: "transparent", + border: "none", + fontSize: "22px", + cursor: "pointer", + color: "#333", + padding: "4px", + }, + content: { + marginTop: "10px", + display: "flex", + flexDirection: "column", + gap: "14px", + }, +}; diff --git a/frontend/src/components/routes/protectedRoute.tsx b/frontend/src/components/routes/protectedRoute.tsx index fe6dcb2..905dcba 100644 --- a/frontend/src/components/routes/protectedRoute.tsx +++ b/frontend/src/components/routes/protectedRoute.tsx @@ -1,20 +1,18 @@ import { Navigate, Outlet } from "react-router-dom"; -import { isAuthenticated } from "../../utils/auth"; +import { useUserStore } from "../../store/userStore"; interface ProtectedRouteProps { requireAuth?: boolean; + children?: React.ReactNode; + allowedRoles?: string[]; } -const ProtectedRoute = ({ requireAuth = true }: ProtectedRouteProps) => { - if (requireAuth && !isAuthenticated()) { +const ProtectedRoute = ({ allowedRoles = [], requireAuth = true, children }: ProtectedRouteProps) => { + const { user } = useUserStore(); + if (requireAuth && !user?.token || !allowedRoles?.includes(user?.role || '')) { return ; } - - if (!requireAuth && isAuthenticated()) { - return ; - } - - return ; + return children || ; }; export default ProtectedRoute; \ No newline at end of file diff --git a/frontend/src/pages/Login/login.tsx b/frontend/src/pages/Login/login.tsx index 4473d7b..67b47b1 100644 --- a/frontend/src/pages/Login/login.tsx +++ b/frontend/src/pages/Login/login.tsx @@ -33,7 +33,7 @@ const Login = () => { mockValidateToken(token).then((isValid) => { if (isValid) { - setUser({ username: 'admin', token }); + setUser({ username: 'admin', token, role: 'admin' }); } else { StorageManager.getInstance().removeItem('auth_token'); setUser(null); diff --git a/frontend/src/pages/ProjectManagement/ProjectManagement.tsx b/frontend/src/pages/ProjectManagement/ProjectManagement.tsx index 1efedb0..41048ba 100644 --- a/frontend/src/pages/ProjectManagement/ProjectManagement.tsx +++ b/frontend/src/pages/ProjectManagement/ProjectManagement.tsx @@ -4,7 +4,7 @@ function ProjectManagement() { return (

Project management page

- + {console.log('add project clicked')}} />
); } diff --git a/frontend/src/pages/UserManagement/UserManagement.tsx b/frontend/src/pages/UserManagement/UserManagement.tsx index 698fa89..604c55a 100644 --- a/frontend/src/pages/UserManagement/UserManagement.tsx +++ b/frontend/src/pages/UserManagement/UserManagement.tsx @@ -1,12 +1,213 @@ import ItemManagement from '../../components/ItemManagement'; +import { useState } from 'react'; +import Modal from '../../components/Modal'; + +type UserForm = { + firstName: string; + middleName: string; + lastName: string; + email: string; + phone: string; + role: string; + status: string; + password: string; +}; function UserManagement() { + const [showAddUserModal, setShowAddUserModal] = useState(false); + + const [form, setForm] = useState({ + firstName: '', + middleName: '', + lastName: '', + email: '', + phone: '', + role: '', + status: 'active', + password: '', + }); + + const [errors, setErrors] = useState>({}); + + const handleChange = (field: keyof UserForm, value: string) => { + setForm((prev) => ({ ...prev, [field]: value })); + setErrors((prev) => ({ ...prev, [field]: '' })); // clear error on change + }; + + const validate = (): boolean => { + const newErrors: Partial = {}; + if (!form.firstName.trim()) newErrors.firstName = 'First name is required'; + if (!form.lastName.trim()) newErrors.lastName = 'Last name is required'; + if (!form.email.trim()) newErrors.email = 'Email is required'; + else if (!/^[\w.-]+@[a-zA-Z\d.-]+\.[a-zA-Z]{2,}$/.test(form.email)) + newErrors.email = 'Invalid email'; + if (!form.phone.trim()) newErrors.phone = 'Phone number is required'; + if (!form.role) newErrors.role = 'Role is required'; + if (!form.password.trim()) newErrors.password = 'Password is required'; + + setErrors(newErrors); + return Object.keys(newErrors).length === 0; + }; + + const saveUser = () => { + if (!validate()) return; + console.log('User form submitted', form); + setForm({ + firstName: '', + middleName: '', + lastName: '', + email: '', + phone: '', + role: '', + status: 'active', + password: '', + }); + setShowAddUserModal(false); + }; + return (

User management page

- + setShowAddUserModal(true)} /> + + {showAddUserModal && ( + setShowAddUserModal(false)}> +
+ +
+
+ + handleChange('firstName', e.target.value)} + placeholder="Enter first name" + /> + {errors.firstName && {errors.firstName}} +
+
+ + handleChange('middleName', e.target.value)} + placeholder="Enter middle name" + /> +
+
+ +
+ + handleChange('lastName', e.target.value)} + placeholder="Enter last name" + /> + {errors.lastName && {errors.lastName}} +
+ +
+ + handleChange('email', e.target.value)} + placeholder="Enter email" + /> + {errors.email && {errors.email}} +
+ +
+ + handleChange('phone', e.target.value)} + placeholder="Enter phone number" + /> + {errors.phone && {errors.phone}} +
+ +
+ + + {errors.role && {errors.role}} +
+ +
+ + +
+ +
+ + handleChange('password', e.target.value)} + placeholder="Enter password" + /> + {errors.password && {errors.password}} +
+ + +
+
+ )}
); } export default UserManagement; + +const inputStyle: React.CSSProperties = { + width: '100%', + padding: '8px 10px', + marginTop: '5px', + borderRadius: '6px', + border: '1px solid #d1d5db', + fontSize: '14px', +}; + +const errorStyle: React.CSSProperties = { + color: 'red', + fontSize: '12px', + marginTop: '2px', + display: 'block', +}; + +const buttonStyle: React.CSSProperties = { + marginTop: '10px', + background: '#0ea5a4', + padding: '10px 14px', + color: '#fff', + border: 'none', + borderRadius: '6px', + cursor: 'pointer', + fontWeight: 600, +}; diff --git a/frontend/src/store/userStore.ts b/frontend/src/store/userStore.ts index 8f58de0..5256c49 100644 --- a/frontend/src/store/userStore.ts +++ b/frontend/src/store/userStore.ts @@ -3,6 +3,7 @@ import { create } from 'zustand'; export interface User { username: string; token: string; + role?: string | undefined; } interface UserState {