From 9210af6e0162361c5bce21d6427014ef8a80371c Mon Sep 17 00:00:00 2001 From: itzzavdheshh Date: Sun, 10 May 2026 00:12:51 +0530 Subject: [PATCH 01/13] =?UTF-8?q?feat:=20Event-Based=20Task=20Management?= =?UTF-8?q?=20System=20=E2=80=94=20CommDesk?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pnpm-workspace.yaml | 3 + src/App.css | 13 +- src/App.tsx | 11 + .../SideBar/v1/Section/BotamNavBar.tsx | 8 +- src/features/SideBar/v1/Section/SideBar.tsx | 3 +- src/features/Tasks/v1/Task.types.ts | 122 +++++ .../Tasks/v1/components/common/Avatar.tsx | 140 ++++++ .../v1/components/common/CommentsSection.tsx | 83 ++++ .../v1/components/common/ConfirmModal.tsx | 112 +++++ .../Tasks/v1/components/common/EmptyState.tsx | 101 +++++ .../v1/components/common/PriorityBadge.tsx | 22 + .../v1/components/common/SkeletonLoader.tsx | 136 ++++++ .../v1/components/common/StatusBadge.tsx | 22 + .../components/common/ToastNotification.tsx | 144 ++++++ .../v1/components/event/EventDropdown.tsx | 253 +++++++++++ .../v1/components/submission/ReviewPanel.tsx | 188 ++++++++ .../components/submission/SubmissionCard.tsx | 126 ++++++ .../components/submission/SubmissionList.tsx | 175 ++++++++ .../Tasks/v1/components/task/TaskCardList.tsx | 171 +++++++ .../Tasks/v1/components/task/TaskFilters.tsx | 297 ++++++++++++ .../Tasks/v1/components/task/TaskForm.tsx | 325 ++++++++++++++ .../Tasks/v1/components/task/TaskHeader.tsx | 87 ++++ .../Tasks/v1/components/task/TaskRow.tsx | 117 +++++ .../Tasks/v1/components/task/TaskTable.tsx | 130 ++++++ .../Tasks/v1/constants/task.constants.ts | 135 ++++++ .../Tasks/v1/constants/tech.constants.ts | 24 + src/features/Tasks/v1/hooks/useComments.ts | 38 ++ src/features/Tasks/v1/hooks/useEvents.ts | 16 + src/features/Tasks/v1/hooks/useSubmissions.ts | 52 +++ src/features/Tasks/v1/hooks/useTaskDetail.ts | 30 ++ src/features/Tasks/v1/hooks/useTasks.ts | 124 +++++ src/features/Tasks/v1/index.ts | 37 ++ src/features/Tasks/v1/mock/taskMockData.ts | 204 +++++++++ src/features/Tasks/v1/mock/taskStore.ts | 20 + .../Tasks/v1/pages/CreateTaskPage.tsx | 33 ++ src/features/Tasks/v1/pages/EditTaskPage.tsx | 55 +++ .../Tasks/v1/pages/TaskDetailPage.tsx | 424 ++++++++++++++++++ .../Tasks/v1/pages/TaskManagementPage.tsx | 138 ++++++ 38 files changed, 4115 insertions(+), 4 deletions(-) create mode 100644 pnpm-workspace.yaml create mode 100644 src/features/Tasks/v1/Task.types.ts create mode 100644 src/features/Tasks/v1/components/common/Avatar.tsx create mode 100644 src/features/Tasks/v1/components/common/CommentsSection.tsx create mode 100644 src/features/Tasks/v1/components/common/ConfirmModal.tsx create mode 100644 src/features/Tasks/v1/components/common/EmptyState.tsx create mode 100644 src/features/Tasks/v1/components/common/PriorityBadge.tsx create mode 100644 src/features/Tasks/v1/components/common/SkeletonLoader.tsx create mode 100644 src/features/Tasks/v1/components/common/StatusBadge.tsx create mode 100644 src/features/Tasks/v1/components/common/ToastNotification.tsx create mode 100644 src/features/Tasks/v1/components/event/EventDropdown.tsx create mode 100644 src/features/Tasks/v1/components/submission/ReviewPanel.tsx create mode 100644 src/features/Tasks/v1/components/submission/SubmissionCard.tsx create mode 100644 src/features/Tasks/v1/components/submission/SubmissionList.tsx create mode 100644 src/features/Tasks/v1/components/task/TaskCardList.tsx create mode 100644 src/features/Tasks/v1/components/task/TaskFilters.tsx create mode 100644 src/features/Tasks/v1/components/task/TaskForm.tsx create mode 100644 src/features/Tasks/v1/components/task/TaskHeader.tsx create mode 100644 src/features/Tasks/v1/components/task/TaskRow.tsx create mode 100644 src/features/Tasks/v1/components/task/TaskTable.tsx create mode 100644 src/features/Tasks/v1/constants/task.constants.ts create mode 100644 src/features/Tasks/v1/constants/tech.constants.ts create mode 100644 src/features/Tasks/v1/hooks/useComments.ts create mode 100644 src/features/Tasks/v1/hooks/useEvents.ts create mode 100644 src/features/Tasks/v1/hooks/useSubmissions.ts create mode 100644 src/features/Tasks/v1/hooks/useTaskDetail.ts create mode 100644 src/features/Tasks/v1/hooks/useTasks.ts create mode 100644 src/features/Tasks/v1/index.ts create mode 100644 src/features/Tasks/v1/mock/taskMockData.ts create mode 100644 src/features/Tasks/v1/mock/taskStore.ts create mode 100644 src/features/Tasks/v1/pages/CreateTaskPage.tsx create mode 100644 src/features/Tasks/v1/pages/EditTaskPage.tsx create mode 100644 src/features/Tasks/v1/pages/TaskDetailPage.tsx create mode 100644 src/features/Tasks/v1/pages/TaskManagementPage.tsx diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml new file mode 100644 index 0000000..04338cc --- /dev/null +++ b/pnpm-workspace.yaml @@ -0,0 +1,3 @@ +allowBuilds: + esbuild: set this to true or false + msw: set this to true or false diff --git a/src/App.css b/src/App.css index b8822c2..6611c43 100644 --- a/src/App.css +++ b/src/App.css @@ -1,9 +1,9 @@ +@import url("https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&display=swap"); + @import "tailwindcss"; @import "tw-animate-css"; @import "shadcn/tailwind.css"; -@import url("https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&display=swap"); - @custom-variant dark (&:is(.dark *)); @plugin "@tailwindcss/typography"; @@ -175,4 +175,13 @@ body { .badge-default { @apply bg-gray-100 text-gray-600 px-2 py-1 rounded-full text-xs; } + + @keyframes shimmer { + 100% { + transform: translateX(100%); + } + } + .animate-\[shimmer_1\.4s_infinite\] { + animation: shimmer 1.4s infinite; + } } diff --git a/src/App.tsx b/src/App.tsx index a279c7c..035f1bf 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -13,6 +13,11 @@ 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 TaskManagementPage from "./features/Tasks/v1/pages/TaskManagementPage"; +import CreateTaskPage from "./features/Tasks/v1/pages/CreateTaskPage"; +import TaskDetailPage from "./features/Tasks/v1/pages/TaskDetailPage"; +import EditTaskPage from "./features/Tasks/v1/pages/EditTaskPage"; + import { startAutoUpdater } from "./system/updater/autoUpdater"; import ProtectedRoute from "./routes/ProtectedRoute"; import { dashboardData } from "./features/Dashboard/mock/dashboardData"; @@ -59,6 +64,12 @@ function App() { } /> } /> + {/* Tasks */} + } /> + } /> + } /> + } /> + 404 Not Found} /> diff --git a/src/features/SideBar/v1/Section/BotamNavBar.tsx b/src/features/SideBar/v1/Section/BotamNavBar.tsx index 1622495..12fe6a1 100644 --- a/src/features/SideBar/v1/Section/BotamNavBar.tsx +++ b/src/features/SideBar/v1/Section/BotamNavBar.tsx @@ -1,6 +1,6 @@ import { RiContactsBookFill } from "react-icons/ri"; import { type IconType } from "react-icons"; -import { MdDashboard, MdEvent, MdGroup } from "react-icons/md"; +import { MdDashboard, MdEvent, MdGroup, MdAssignment } from "react-icons/md"; import { Link, useLocation } from "react-router-dom"; import { getTheme } from "../../../../config/them.config"; @@ -30,6 +30,12 @@ const navItems: NavItem[] = [ link: "/events", isActive: (pathname) => pathname.startsWith("/events") || pathname.startsWith("/create-event"), }, + { + icon: MdAssignment, + text: "Tasks", + link: "/tasks", + isActive: (pathname) => pathname.startsWith("/tasks"), + }, { icon: RiContactsBookFill, text: "Support", diff --git a/src/features/SideBar/v1/Section/SideBar.tsx b/src/features/SideBar/v1/Section/SideBar.tsx index a33f80d..f30346c 100644 --- a/src/features/SideBar/v1/Section/SideBar.tsx +++ b/src/features/SideBar/v1/Section/SideBar.tsx @@ -1,7 +1,7 @@ import { RiContactsBookFill } from "react-icons/ri"; import { getTheme } from "../../../../config/them.config"; import SideBarLink from "../Components/SideBarLink"; -import { MdDashboard, MdEvent, MdGroup, MdSettings, MdWork } from "react-icons/md"; +import { MdDashboard, MdEvent, MdGroup, MdSettings, MdWork, MdAssignment } from "react-icons/md"; import { dashboardData } from "@/features/Dashboard/mock/dashboardData"; const SideBar = () => { @@ -42,6 +42,7 @@ const SideBar = () => { } text="Projects" /> } text="Teams" link="/org/member" /> } text="Events" link="/org/events" /> + } text="Tasks" link="/org/tasks" /> } text="Contact Submissions" link="/org/contact" />
{ + status?: TaskStatus; +} + +export interface ReviewSubmissionPayload { + decision: ReviewDecision; + score?: number; + feedback?: string; +} \ No newline at end of file diff --git a/src/features/Tasks/v1/components/common/Avatar.tsx b/src/features/Tasks/v1/components/common/Avatar.tsx new file mode 100644 index 0000000..2902533 --- /dev/null +++ b/src/features/Tasks/v1/components/common/Avatar.tsx @@ -0,0 +1,140 @@ +import { useState } from "react"; + +interface AvatarProps { + name: string; + src: string; + role?: string; + size?: "xs" | "sm" | "md" | "lg"; + showTooltip?: boolean; + status?: "online" | "offline" | "busy" | null; + ring?: boolean; + ringColor?: string; +} + +// Color map for initials fallback — derived from name char code +const BG_COLORS = [ + "bg-indigo-500", "bg-pink-500", "bg-orange-500", + "bg-emerald-500", "bg-sky-500", "bg-violet-500", + "bg-rose-500", "bg-amber-500","bg-teal-500", +]; + +function getColor(name: string): string { + const code = name.charCodeAt(0) + (name.charCodeAt(1) ?? 0); + return BG_COLORS[code % BG_COLORS.length]; +} + +function getInitials(name: string): string { + return name.split(" ").map(w => w[0]).join("").slice(0, 2).toUpperCase(); +} + +const SIZE_MAP = { + xs: { outer: "w-6 h-6", text: "text-[9px]", dot: "w-1.5 h-1.5 -right-0 -bottom-0", ring: "ring-1" }, + sm: { outer: "w-7 h-7", text: "text-[10px]", dot: "w-2 h-2 -right-0 -bottom-0", ring: "ring-2" }, + md: { outer: "w-9 h-9", text: "text-xs", dot: "w-2.5 h-2.5 right-0 bottom-0", ring: "ring-2" }, + lg: { outer: "w-12 h-12", text: "text-sm", dot: "w-3 h-3 right-0.5 bottom-0.5", ring: "ring-2" }, +}; + +const STATUS_COLOR = { + online: "bg-emerald-400", + offline: "bg-gray-300", + busy: "bg-red-400", +}; + +export default function Avatar({ + name, src, role, size = "sm", + showTooltip = true, status = null, ring = false, ringColor = "ring-white", +}: AvatarProps) { + const [imgError, setImgError] = useState(false); + const [hovered, setHovered] = useState(false); + const sz = SIZE_MAP[size]; + const initials = getInitials(name); + const bgColor = getColor(name); + + return ( +
setHovered(true)} + onMouseLeave={() => setHovered(false)} + > + {/* Avatar circle */} +
+ {!imgError ? ( + {name} setImgError(true)} + className="w-full h-full object-cover" + /> + ) : ( + /* Initials fallback */ +
+ {initials} +
+ )} +
+ + {/* Status dot */} + {status && ( + + )} + + {/* Tooltip */} + {showTooltip && hovered && ( +
+
+

{name}

+ {role &&

{role}

} +
+ {/* Arrow */} +
+
+
+
+ )} +
+ ); +} + +// ─── Avatar group (stacked, with +N overflow) ───────────────────────────────── +interface AvatarGroupProps { + members: { id: string; name: string; avatar: string; role?: string }[]; + max?: number; + size?: "xs" | "sm" | "md"; +} + +export function AvatarGroup({ members, max = 3, size = "sm" }: AvatarGroupProps) { + const visible = members.slice(0, max); + const overflow = members.length - max; + const sz = SIZE_MAP[size]; + + return ( +
+ {visible.map((m) => ( + + ))} + {overflow > 0 && ( +
+ +{overflow} +
+ )} +
+ ); +} \ No newline at end of file diff --git a/src/features/Tasks/v1/components/common/CommentsSection.tsx b/src/features/Tasks/v1/components/common/CommentsSection.tsx new file mode 100644 index 0000000..398f3f5 --- /dev/null +++ b/src/features/Tasks/v1/components/common/CommentsSection.tsx @@ -0,0 +1,83 @@ +import { useState } from "react"; +import { formatDistanceToNow, parseISO } from "date-fns"; +import { Loader2, MessageCircle, Send } from "lucide-react"; +import { useComments, useAddComment } from "../../hooks/useComments"; + +interface Props { taskId: string; } + +export default function CommentsSection({ taskId }: Props) { + const { data: comments = [], isLoading } = useComments(taskId); + const addComment = useAddComment(); + const [text, setText] = useState(""); + const [error, setError] = useState(""); + + const handleSubmit = async () => { + if (!text.trim()) { setError("Comment cannot be empty."); return; } + setError(""); + await addComment.mutateAsync({ taskId, text: text.trim() }); + setText(""); + }; + + return ( +
+ {/* Header */} +
+ +

Comments

+ + {comments.length} + +
+ + {/* Comment list */} + {isLoading ? ( +

Loading comments…

+ ) : comments.length === 0 ? ( +

No comments yet. Be the first!

+ ) : ( +
+ {comments.map((c) => ( +
+ {c.author} +
+
+ {c.author} + + {formatDistanceToNow(parseISO(c.createdAt), { addSuffix: true })} + +
+

{c.text}

+
+
+ ))} +
+ )} + + {/* Add comment */} +
+
+