diff --git a/src/App.tsx b/src/App.tsx index a06e4ed..4cd5478 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -2,6 +2,20 @@ import { useEffect } from "react"; import { BrowserRouter } from "react-router-dom"; +import DashboardPage from "./features/Dashboard/v1/Pages/DashboardPage"; +import MemberPage from "./features/Member/v1/Pages/MemberPage"; +import LoginUserTemplate from "./features/template/LoginUserTemplate"; +import AddMemberPage from "./features/AddMember/v1/Page/AddMemberPage"; +import CreateNewEvent from "./features/Events/v1/Pages/CreateNewEvent"; +import Contact from "./features/Contact_And_Support/v1/Pages/Contact"; +import ViewEvent from "./features/Events/v1/Pages/ViewEvent"; +import LoginPage from "./features/Auth/v1/Pages/LoginPage"; +import SignUpPage from "./features/Auth/v1/Pages/SignUpPage"; +import TaskListPage from "./features/MemberTask/pages/TaskListPage"; +import TaskDetailPage from "./features/MemberTask/pages/TaskDetailPage"; +import CreateTaskPage from "./features/MemberTask/pages/CreateTaskPage"; +import EditTaskPage from "./features/MemberTask/pages/EditTaskPage"; +import { PermissionProvider } from "./features/MemberTask/context/PermissionContext"; import "./App.css"; import { dashboardData } from "./features/Member/v1/mock/dashboardData"; @@ -19,9 +33,65 @@ function App() { const user = dashboardData.user; return ( + + + + {/* Public Routes */} + } /> + } /> + + {/* Protected / Org Layout */} + }> + {/* Protected Dashboard */} + + + + } + /> + + + + + } + /> + + {/* Other routes (not restricted) */} + } /> + } /> + } /> + } /> + } /> + + {/* Task System */} + } /> + } /> + } /> + } /> + + 404 Not Found} /> + + + + Unauthorized Access + + } + /> + + + + diff --git a/src/features/MemberTask/components/common/ConfirmModal.tsx b/src/features/MemberTask/components/common/ConfirmModal.tsx new file mode 100644 index 0000000..78df90e --- /dev/null +++ b/src/features/MemberTask/components/common/ConfirmModal.tsx @@ -0,0 +1,38 @@ +interface Props { + title: string; + message: string; + confirmLabel?: string; + onConfirm: () => void; + onCancel: () => void; +} + +export default function ConfirmModal({ + title, + message, + confirmLabel = "Confirm", + onConfirm, + onCancel, +}: Props) { + return ( +
+
+

{title}

+

{message}

+
+ + +
+
+
+ ); +} diff --git a/src/features/MemberTask/components/common/PriorityBadge.tsx b/src/features/MemberTask/components/common/PriorityBadge.tsx new file mode 100644 index 0000000..d76599a --- /dev/null +++ b/src/features/MemberTask/components/common/PriorityBadge.tsx @@ -0,0 +1,17 @@ +import type { TaskPriority } from "../../types/task.types"; + +const config: Record = { + low: { label: "Low", className: "bg-gray-50 text-gray-500 border-gray-200" }, + medium: { label: "Medium", className: "bg-orange-50 text-orange-500 border-orange-200" }, + high: { label: "High", className: "bg-red-50 text-red-500 border-red-200" }, + urgent: { label: "Urgent", className: "bg-red-100 text-red-700 border-red-300 font-semibold" }, +}; + +export default function PriorityBadge({ priority }: { priority: TaskPriority }) { + const { label, className } = config[priority]; + return ( + + {label} + + ); +} diff --git a/src/features/MemberTask/components/common/SkeletonLoader.tsx b/src/features/MemberTask/components/common/SkeletonLoader.tsx new file mode 100644 index 0000000..f9515e9 --- /dev/null +++ b/src/features/MemberTask/components/common/SkeletonLoader.tsx @@ -0,0 +1,14 @@ +export default function SkeletonLoader({ rows = 5 }: { rows?: number }) { + return ( +
+ {Array.from({ length: rows }).map((_, i) => ( +
+
+
+
+
+
+ ))} +
+ ); +} diff --git a/src/features/MemberTask/components/common/StatusBadge.tsx b/src/features/MemberTask/components/common/StatusBadge.tsx new file mode 100644 index 0000000..d24f7ec --- /dev/null +++ b/src/features/MemberTask/components/common/StatusBadge.tsx @@ -0,0 +1,17 @@ +import type { TaskStatus } from "../../types/task.types"; + +const config: Record = { + todo: { label: "To Do", className: "bg-gray-100 text-gray-600 border-gray-200" }, + "in-progress": { label: "In Progress", className: "bg-blue-50 text-blue-600 border-blue-200" }, + review: { label: "In Review", className: "bg-yellow-50 text-yellow-600 border-yellow-200" }, + completed: { label: "Completed", className: "bg-green-50 text-green-600 border-green-200" }, +}; + +export default function StatusBadge({ status }: { status: TaskStatus }) { + const { label, className } = config[status]; + return ( + + {label} + + ); +} diff --git a/src/features/MemberTask/components/event/EventDropdown.tsx b/src/features/MemberTask/components/event/EventDropdown.tsx new file mode 100644 index 0000000..0f62366 --- /dev/null +++ b/src/features/MemberTask/components/event/EventDropdown.tsx @@ -0,0 +1,38 @@ +import { MdEvent, MdExpandMore } from "react-icons/md"; +import type { Event } from "../../types/task.types"; + +interface Props { + events: Event[]; + selectedEventId: string | null; + onChange: (id: string) => void; +} + +export default function EventDropdown({ events, selectedEventId, onChange }: Props) { + const selected = events.find((e) => e.id === selectedEventId); + + return ( +
+ +
+ + +
+ {selected && ( + — {selected.name} + )} +
+ ); +} diff --git a/src/features/MemberTask/components/task/CommentSection.tsx b/src/features/MemberTask/components/task/CommentSection.tsx new file mode 100644 index 0000000..72dc6b2 --- /dev/null +++ b/src/features/MemberTask/components/task/CommentSection.tsx @@ -0,0 +1,73 @@ +import { useState } from "react"; +import type { Comment } from "../../types/task.types"; +import { usePermissions } from "../../context/PermissionContext"; + +interface Props { + comments: Comment[]; + onAddComment: (content: string) => void; +} + +export default function CommentSection({ comments, onAddComment }: Props) { + const { currentUserName } = usePermissions(); + const [text, setText] = useState(""); + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault(); + if (!text.trim()) return; + onAddComment(text.trim()); + setText(""); + }; + + return ( +
+

Comments

+ +
+ {comments.length === 0 && ( +

No comments yet.

+ )} + {comments.map((c) => ( +
+
+ {c.authorName[0]} +
+
+
+ {c.authorName} + + {new Date(c.createdAt).toLocaleDateString(undefined, { + day: "numeric", + month: "short", + })} + +
+

{c.content}

+
+
+ ))} +
+ +
+
+ {currentUserName[0]} +
+
+ setText(e.target.value)} + placeholder="Add a comment..." + className="flex-1 text-sm border border-gray-200 rounded-lg px-3 py-2 focus:outline-none focus:ring-2 focus:ring-[#306ee8]/30 focus:border-[#306ee8]" + /> + +
+
+
+ ); +} diff --git a/src/features/MemberTask/components/task/SubmissionForm.tsx b/src/features/MemberTask/components/task/SubmissionForm.tsx new file mode 100644 index 0000000..d5901f7 --- /dev/null +++ b/src/features/MemberTask/components/task/SubmissionForm.tsx @@ -0,0 +1,93 @@ +import { useState } from "react"; +import type { Submission } from "../../types/task.types"; +import { usePermissions } from "../../context/PermissionContext"; +import { MdCheckCircle, MdCancel, MdHourglassEmpty } from "react-icons/md"; + +interface Props { + taskId: string; + deadline: string; + submissions: Submission[]; + onSubmit: (content: string) => void; +} + +const statusIcon = { + pending: , + approved: , + rejected: , +}; + +export default function SubmissionForm({ deadline, submissions, onSubmit }: Props) { + const { hasPermission, currentUserId } = usePermissions(); + const [content, setContent] = useState(""); + + const deadlinePassed = new Date() > new Date(deadline); + const mySubmissions = submissions.filter((s) => s.submittedBy === currentUserId); + const hasSubmitted = mySubmissions.length > 0; + + const canSubmit = + (!hasSubmitted || hasPermission("MULTIPLE_SUBMISSION")) && + (!deadlinePassed || hasPermission("LATE_SUBMISSION")); + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault(); + if (!content.trim() || !canSubmit) return; + onSubmit(content.trim()); + setContent(""); + }; + + return ( +
+

Submissions

+ + {/* Submission history */} + {mySubmissions.length > 0 && ( +
+ {mySubmissions.map((sub) => ( +
+
+ {statusIcon[sub.status]} + {sub.status} + + {new Date(sub.submittedAt).toLocaleDateString("en-IN", { + day: "numeric", + month: "short", + })} + +
+

{sub.content}

+ {sub.reviewNote && ( +

Review: {sub.reviewNote}

+ )} +
+ ))} +
+ )} + + {/* Submit form */} + {canSubmit ? ( +
+