diff --git a/frontend/collab-hub/app/dashboard/invitations/page.tsx b/frontend/collab-hub/app/dashboard/invitations/page.tsx index f1eb8eb..dc93311 100644 --- a/frontend/collab-hub/app/dashboard/invitations/page.tsx +++ b/frontend/collab-hub/app/dashboard/invitations/page.tsx @@ -4,6 +4,7 @@ import { useState } from "react" import { InvitationList } from "@/components/users/invitation-list" import { Sidebar } from "@/components/dashboard/sidebar" import { DashboardHeader } from "@/components/dashboard/dashboard-header" +import { MobileSidebar } from "@/components/dashboard/mobile-sidebar" import { CreateProjectDialog } from "@/components/dashboard/create-project-dialog" import { useCreateProject, useProjects } from "@/hooks/use-projects" import { Skeleton } from "@/components/ui/skeleton" @@ -19,8 +20,8 @@ export default function InvitationsPage() { if (isLoading) { return ( -
-
+
+
{Array.from({ length: 5 }).map((_, i) => ( @@ -40,13 +41,16 @@ export default function InvitationsPage() { } return ( -
- setIsCreateProjectDialogOpen(true)} /> +
+ setIsCreateProjectDialogOpen(true)} className="hidden md:flex" />
- setIsCreateProjectDialogOpen(true)} /> + setIsCreateProjectDialogOpen(true)} + mobileSidebar={ setIsCreateProjectDialogOpen(true)} />} + /> -
+

Invitations

diff --git a/frontend/collab-hub/app/dashboard/messages/page.tsx b/frontend/collab-hub/app/dashboard/messages/page.tsx index 4fce2a1..dae8a91 100644 --- a/frontend/collab-hub/app/dashboard/messages/page.tsx +++ b/frontend/collab-hub/app/dashboard/messages/page.tsx @@ -5,6 +5,7 @@ import { useAuth } from "@/hooks/use-auth" import { useRouter } from "next/navigation" import { DashboardHeader } from "@/components/dashboard/dashboard-header" import { Sidebar } from "@/components/dashboard/sidebar" +import { MobileSidebar } from "@/components/dashboard/mobile-sidebar" import { ChannelList } from "@/components/chat/channel-list" import { MessageList } from "@/components/chat/message-list" import { MessageInput } from "@/components/chat/message-input" @@ -164,8 +165,8 @@ export default function MessagesPage() { if (authLoading || projectsLoading || tasksLoading || channelsLoading) { return ( -
-
+
+
{Array.from({ length: 5 }).map((_, i) => ( @@ -175,8 +176,8 @@ export default function MessagesPage() {
-
-
+
+
@@ -189,13 +190,16 @@ export default function MessagesPage() { } return ( -
- setIsCreateProjectDialogOpen(true)} /> +
+ setIsCreateProjectDialogOpen(true)} className="hidden md:flex" />
- setIsCreateProjectDialogOpen(true)} /> + setIsCreateProjectDialogOpen(true)} + mobileSidebar={ setIsCreateProjectDialogOpen(true)} />} + /> -
+
{/* Channel Header */}
-
-
+
+
{activeChannel.type === "PROJECT_GENERAL" && } {activeChannel.type === "TASK_SPECIFIC" && ( @@ -250,7 +254,7 @@ export default function MessagesPage() { ) : (
- + diff --git a/frontend/collab-hub/app/dashboard/notifications/page.tsx b/frontend/collab-hub/app/dashboard/notifications/page.tsx new file mode 100644 index 0000000..f5aa0dc --- /dev/null +++ b/frontend/collab-hub/app/dashboard/notifications/page.tsx @@ -0,0 +1,72 @@ +"use client" + +import { useState } from "react" +import { Sidebar } from "@/components/dashboard/sidebar" +import { DashboardHeader } from "@/components/dashboard/dashboard-header" +import { CreateProjectDialog } from "@/components/dashboard/create-project-dialog" +import { NotificationList } from "@/components/notifications/notification-list" +import { MobileSidebar } from "@/components/dashboard/mobile-sidebar" +import { useCreateProject, useProjects } from "@/hooks/use-projects" +import { Skeleton } from "@/components/ui/skeleton" + +export default function NotificationsPage() { + const [isCreateProjectDialogOpen, setIsCreateProjectDialogOpen] = useState(false) + const { data: projects = [], isLoading } = useProjects() + const { mutateAsync: createProjectMutation } = useCreateProject() + + const handleCreateProject = async (name: string, description: string) => { + await createProjectMutation({ name, description }) + } + + if (isLoading) { + return ( +
+
+ +
+ {Array.from({ length: 5 }).map((_, i) => ( + + ))} +
+
+
+ +
+ + +
+
+
+ ) + } + + return ( +
+ setIsCreateProjectDialogOpen(true)} className="hidden md:flex" /> + +
+ setIsCreateProjectDialogOpen(true)} + mobileSidebar={ setIsCreateProjectDialogOpen(true)} />} + /> + +
+
+
+

Notifications

+

All your project updates in one place

+
+ + +
+
+
+ + +
+ ) +} diff --git a/frontend/collab-hub/app/dashboard/page.tsx b/frontend/collab-hub/app/dashboard/page.tsx index a798587..6e6cf3e 100644 --- a/frontend/collab-hub/app/dashboard/page.tsx +++ b/frontend/collab-hub/app/dashboard/page.tsx @@ -5,6 +5,7 @@ import { useAuth } from "@/hooks/use-auth" import { useRouter } from "next/navigation" import { Sidebar } from "@/components/dashboard/sidebar" import { DashboardHeader } from "@/components/dashboard/dashboard-header" +import { MobileSidebar } from "@/components/dashboard/mobile-sidebar" import { ProjectCard } from "@/components/dashboard/project-card" import { CreateProjectDialog } from "@/components/dashboard/create-project-dialog" import { Button } from "@/components/ui/button" @@ -62,8 +63,8 @@ export default function DashboardPage() { if (authLoading || isLoading || projectsLoading || tasksLoading) { return ( -
-
+
+
{Array.from({ length: 5 }).map((_, i) => ( @@ -95,13 +96,16 @@ export default function DashboardPage() { } return ( -
- setIsCreateDialogOpen(true)} /> +
+ setIsCreateDialogOpen(true)} className="hidden md:flex" />
- setIsCreateDialogOpen(true)} /> + setIsCreateDialogOpen(true)} + mobileSidebar={ setIsCreateDialogOpen(true)} />} + /> -
+
{/* Welcome Section */}
@@ -162,9 +166,9 @@ export default function DashboardPage() { {/* Projects Section */}
-
+

Your Projects

- diff --git a/frontend/collab-hub/app/dashboard/projects/[id]/members/page.tsx b/frontend/collab-hub/app/dashboard/projects/[id]/members/page.tsx index 9709b89..37ee623 100644 --- a/frontend/collab-hub/app/dashboard/projects/[id]/members/page.tsx +++ b/frontend/collab-hub/app/dashboard/projects/[id]/members/page.tsx @@ -7,6 +7,7 @@ import { useAuth } from "@/hooks/use-auth" import { useProject } from "@/hooks/use-projects" import { Sidebar } from "@/components/dashboard/sidebar" import { DashboardHeader } from "@/components/dashboard/dashboard-header" +import { MobileSidebar } from "@/components/dashboard/mobile-sidebar" import { CreateProjectDialog } from "@/components/dashboard/create-project-dialog" import { useCreateProject, useProjects } from "@/hooks/use-projects" import { Skeleton } from "@/components/ui/skeleton" @@ -30,8 +31,8 @@ export default function ProjectMembersPage() { if (isLoading || projectsLoading) { return ( -
-
+
+
{Array.from({ length: 5 }).map((_, i) => ( @@ -55,13 +56,16 @@ export default function ProjectMembersPage() { } return ( -
- setIsCreateProjectDialogOpen(true)} /> +
+ setIsCreateProjectDialogOpen(true)} className="hidden md:flex" />
- setIsCreateProjectDialogOpen(true)} /> + setIsCreateProjectDialogOpen(true)} + mobileSidebar={ setIsCreateProjectDialogOpen(true)} />} + /> -
+

{project.name} - Members

diff --git a/frontend/collab-hub/app/dashboard/projects/[id]/page.tsx b/frontend/collab-hub/app/dashboard/projects/[id]/page.tsx index ad972ba..669b1fd 100644 --- a/frontend/collab-hub/app/dashboard/projects/[id]/page.tsx +++ b/frontend/collab-hub/app/dashboard/projects/[id]/page.tsx @@ -4,6 +4,7 @@ import { useMemo, useState } from "react" import { useParams } from "next/navigation" import { Sidebar } from "@/components/dashboard/sidebar" import { DashboardHeader } from "@/components/dashboard/dashboard-header" +import { MobileSidebar } from "@/components/dashboard/mobile-sidebar" import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card" import { Badge } from "@/components/ui/badge" import { Skeleton } from "@/components/ui/skeleton" @@ -38,8 +39,8 @@ export default function ProjectDetailPage() { if (projectsLoading || projectLoading || tasksLoading) { return ( -
-
+
+
{Array.from({ length: 5 }).map((_, i) => ( @@ -60,7 +61,7 @@ export default function ProjectDetailPage() { if (!project) { return ( -
+

Project not found.

@@ -74,13 +75,16 @@ export default function ProjectDetailPage() { } return ( -
- setIsCreateProjectDialogOpen(true)} /> +
+ setIsCreateProjectDialogOpen(true)} className="hidden md:flex" />
- setIsCreateProjectDialogOpen(true)} /> + setIsCreateProjectDialogOpen(true)} + mobileSidebar={ setIsCreateProjectDialogOpen(true)} />} + /> -
+

{project.name}

@@ -116,13 +120,13 @@ export default function ProjectDetailPage() { -
+
Project Tasks Tasks associated with this project.
- @@ -134,7 +138,7 @@ export default function ProjectDetailPage() { ) : (
{tasks.slice(0, 6).map((task: Task) => ( -
+

{task.title}

{task.description &&

{task.description}

} @@ -149,7 +153,7 @@ export default function ProjectDetailPage() { -
+
Members @@ -157,7 +161,7 @@ export default function ProjectDetailPage() {
- diff --git a/frontend/collab-hub/app/dashboard/tasks/page.tsx b/frontend/collab-hub/app/dashboard/tasks/page.tsx index e288cd8..8de2e6b 100644 --- a/frontend/collab-hub/app/dashboard/tasks/page.tsx +++ b/frontend/collab-hub/app/dashboard/tasks/page.tsx @@ -5,6 +5,7 @@ import { useAuth } from "@/hooks/use-auth" import { useRouter } from "next/navigation" import { DashboardHeader } from "@/components/dashboard/dashboard-header" import { Sidebar } from "@/components/dashboard/sidebar" +import { MobileSidebar } from "@/components/dashboard/mobile-sidebar" import { KanbanColumn } from "@/components/tasks/kanban-column" import { CreateTaskDialog } from "@/components/tasks/create-task-dialog" import { EditTaskDialog } from "@/components/tasks/edit-task-dialog" @@ -21,6 +22,8 @@ import { Search, Plus, LayoutGrid, List } from "lucide-react" import { CreateProjectDialog } from "@/components/dashboard/create-project-dialog" import { useTaskEvents } from "@/hooks/use-websocket" import { getApiErrorMessage } from "@/lib/api/error" +import { DndContext, DragOverlay, PointerSensor, closestCenter, useSensor, useSensors } from "@dnd-kit/core" +import { TaskCard } from "@/components/tasks/task-card" export default function TasksPage() { const { isAuthenticated, isLoading: authLoading } = useAuth() @@ -37,6 +40,7 @@ export default function TasksPage() { const [selectedProject, setSelectedProject] = useState("all") const [viewMode, setViewMode] = useState<"kanban" | "list">("kanban") const [createTaskStatus, setCreateTaskStatus] = useState("TODO") + const [activeTaskId, setActiveTaskId] = useState(null) const [error, setError] = useState("") const { @@ -166,11 +170,52 @@ export default function TasksPage() { const todoTasks = filteredTasks.filter((task) => task.status === "TODO") const inProgressTasks = filteredTasks.filter((task) => task.status === "IN_PROGRESS") const doneTasks = filteredTasks.filter((task) => task.status === "DONE") + const tasksById = useMemo(() => new Map(filteredTasks.map((task) => [task.id, task])), [filteredTasks]) + const activeTask = activeTaskId ? tasksById.get(activeTaskId) : null + + const sensors = useSensors( + useSensor(PointerSensor, { + activationConstraint: { distance: 6 }, + }), + ) + + const handleDragStart = (event: { active: { id: string | number } }) => { + setActiveTaskId(String(event.active.id)) + } + + const handleDragEnd = (event: { active: { id: string | number }; over: { id: string | number } | null }) => { + const { active, over } = event + setActiveTaskId(null) + + if (!over) return + const activeId = String(active.id) + const overId = String(over.id) + const task = tasksById.get(activeId) + if (!task) return + + const statusIds: Task["status"][] = ["TODO", "IN_PROGRESS", "DONE"] + let targetStatus: Task["status"] | null = null + + if (statusIds.includes(overId as Task["status"])) { + targetStatus = overId as Task["status"] + } else { + const overTask = tasksById.get(overId) + if (overTask) targetStatus = overTask.status + } + + if (targetStatus && targetStatus !== task.status) { + handleStatusChange(task, targetStatus) + } + } + + const handleDragCancel = () => { + setActiveTaskId(null) + } if (authLoading || isLoading || projectsLoading || isTasksLoading) { return ( -
-
+
+
{Array.from({ length: 5 }).map((_, i) => ( @@ -193,30 +238,37 @@ export default function TasksPage() { } return ( -
- setIsCreateProjectDialogOpen(true)} /> +
+ setIsCreateProjectDialogOpen(true)} + className="hidden md:flex" + />
- setIsCreateProjectDialogOpen(true)} /> + setIsCreateProjectDialogOpen(true)} + mobileSidebar={ setIsCreateProjectDialogOpen(true)} />} + /> -
+
{/* Header */} -
+

Task Management

Organize and track your tasks across all projects.

-
{/* Filters */} -
-
-
+
+
+
@@ -38,13 +34,11 @@ export function DashboardHeader({ onCreateProject }: DashboardHeaderProps) {
{/* Notifications */} - + {/* User Menu */} diff --git a/frontend/collab-hub/components/dashboard/mobile-sidebar.tsx b/frontend/collab-hub/components/dashboard/mobile-sidebar.tsx new file mode 100644 index 0000000..3f4fb95 --- /dev/null +++ b/frontend/collab-hub/components/dashboard/mobile-sidebar.tsx @@ -0,0 +1,42 @@ +"use client" + +import { useState } from "react" +import { Menu } from "lucide-react" +import { Button } from "@/components/ui/button" +import { Sheet, SheetContent, SheetTrigger } from "@/components/ui/sheet" +import { Sidebar } from "@/components/dashboard/sidebar" + +interface MobileSidebarProps { + projects: Array<{ + id: string + name: string + description: string + }> + onCreateProject: () => void +} + +export function MobileSidebar({ projects, onCreateProject }: MobileSidebarProps) { + const [open, setOpen] = useState(false) + + return ( + + + + + + { + setOpen(false) + onCreateProject() + }} + onNavigate={() => setOpen(false)} + className="w-full" + /> + + + ) +} diff --git a/frontend/collab-hub/components/dashboard/project-card.tsx b/frontend/collab-hub/components/dashboard/project-card.tsx index 6ee3d9e..42a075f 100644 --- a/frontend/collab-hub/components/dashboard/project-card.tsx +++ b/frontend/collab-hub/components/dashboard/project-card.tsx @@ -73,8 +73,8 @@ export function ProjectCard({ project, onEdit, onDelete }: ProjectCardProps) { )} {/* Stats */} -
-
+
+
{project.taskStats && (
diff --git a/frontend/collab-hub/components/dashboard/sidebar.tsx b/frontend/collab-hub/components/dashboard/sidebar.tsx index 5a01c8c..cd6370d 100644 --- a/frontend/collab-hub/components/dashboard/sidebar.tsx +++ b/frontend/collab-hub/components/dashboard/sidebar.tsx @@ -1,6 +1,6 @@ "use client" -import { useState } from "react" +import { useMemo, useState } from "react" import Link from "next/link" import { usePathname } from "next/navigation" import { Button } from "@/components/ui/button" @@ -8,6 +8,7 @@ import { ScrollArea } from "@/components/ui/scroll-area" import { Separator } from "@/components/ui/separator" import { Badge } from "@/components/ui/badge" import { useAuth } from "@/hooks/use-auth" +import { useInvitations } from "@/hooks/use-invitations" import { Home, FolderOpen, @@ -20,6 +21,7 @@ import { ChevronDown, ChevronRight, Mail, + Bell, } from "lucide-react" import { cn } from "@/lib/utils" @@ -30,23 +32,31 @@ interface SidebarProps { description: string }> onCreateProject: () => void + onNavigate?: () => void + className?: string } -export function Sidebar({ projects, onCreateProject }: SidebarProps) { +export function Sidebar({ projects, onCreateProject, onNavigate, className }: SidebarProps) { const pathname = usePathname() const { user, logout } = useAuth() + const { data: invitations = [] } = useInvitations() + const pendingInvites = useMemo( + () => invitations.filter((invitation) => invitation.status === "PENDING").length, + [invitations], + ) const [isProjectsExpanded, setIsProjectsExpanded] = useState(true) const navigation = [ { name: "Dashboard", href: "/dashboard", icon: Home }, { name: "My Tasks", href: "/dashboard/tasks", icon: CheckSquare }, { name: "Messages", href: "/dashboard/messages", icon: MessageSquare }, - { name: "Invitations", href: "/dashboard/invitations", icon: Mail }, + { name: "Notifications", href: "/dashboard/notifications", icon: Bell }, + { name: "Invitations", href: "/dashboard/invitations", icon: Mail, badge: pendingInvites }, { name: "Team", href: "/dashboard/team", icon: Users }, ] return ( -
+
{/* Header */}
@@ -70,9 +80,15 @@ export function Sidebar({ projects, onCreateProject }: SidebarProps) { "w-full justify-start text-sidebar-foreground hover:bg-sidebar-accent hover:text-sidebar-accent-foreground", isActive && "bg-sidebar-accent text-sidebar-accent-foreground", )} + onClick={() => onNavigate?.()} > {item.name} + {item.badge ? ( + + {item.badge} + + ) : null} ) @@ -129,6 +145,7 @@ export function Sidebar({ projects, onCreateProject }: SidebarProps) { "w-full justify-start text-xs text-sidebar-foreground hover:bg-sidebar-accent hover:text-sidebar-accent-foreground", isActive && "bg-sidebar-accent text-sidebar-accent-foreground", )} + onClick={() => onNavigate?.()} > {project.name} @@ -145,6 +162,7 @@ export function Sidebar({ projects, onCreateProject }: SidebarProps) { pathname === `/dashboard/projects/${project.id}/members` && "bg-sidebar-accent text-sidebar-accent-foreground", )} + onClick={() => onNavigate?.()} > Members @@ -174,14 +192,22 @@ export function Sidebar({ projects, onCreateProject }: SidebarProps) {
- + + +
+ Notifications + {unreadCount > 0 && ( + + )} +
+ + + {isLoading ? ( +
Loading notifications...
+ ) : notifications.length === 0 ? ( +
No notifications yet
+ ) : ( + + {notifications.slice(0, 6).map((notification) => ( + { + if (!notification.readAt) markRead(notification.id) + }} + asChild + > + +
+
+ + {notification.title} + + {!notification.readAt && } +
+ {notification.body} + + {formatDistanceToNow(new Date(notification.createdAt), { addSuffix: true })} + +
+ +
+ ))} +
+ )} + + + + + View all notifications + + +
+ + ) +} diff --git a/frontend/collab-hub/components/notifications/notification-list.tsx b/frontend/collab-hub/components/notifications/notification-list.tsx new file mode 100644 index 0000000..176c519 --- /dev/null +++ b/frontend/collab-hub/components/notifications/notification-list.tsx @@ -0,0 +1,84 @@ +"use client" + +import Link from "next/link" +import { formatDistanceToNow } from "date-fns" +import { Check } from "lucide-react" +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" +import { Button } from "@/components/ui/button" +import { useNotifications, useMarkAllNotificationsRead, useMarkNotificationRead } from "@/hooks/use-notifications" +import { getApiErrorMessage } from "@/lib/api/error" + +export function NotificationList() { + const { data: notifications = [], isLoading, error } = useNotifications() + const { mutate: markRead } = useMarkNotificationRead() + const { mutate: markAllRead } = useMarkAllNotificationsRead() + + const unreadCount = notifications.filter((notification) => !notification.readAt).length + + if (isLoading) { + return ( +
+
+
+ ) + } + + if (error) { + return ( + + +

{getApiErrorMessage(error, "Failed to load notifications")}

+
+
+ ) + } + + return ( + + + Notifications + {unreadCount > 0 && ( + + )} + + + {notifications.length === 0 ? ( +
No notifications yet
+ ) : ( +
+ {notifications.map((notification) => ( +
+
+
+

{notification.title}

+ {!notification.readAt && } +
+

{notification.body}

+

+ {formatDistanceToNow(new Date(notification.createdAt), { addSuffix: true })} +

+ {notification.link && ( + + View details + + )} +
+ {!notification.readAt && ( + + )} +
+ ))} +
+ )} +
+
+ ) +} diff --git a/frontend/collab-hub/components/tasks/kanban-column.tsx b/frontend/collab-hub/components/tasks/kanban-column.tsx index 87a3ec7..80e15b5 100644 --- a/frontend/collab-hub/components/tasks/kanban-column.tsx +++ b/frontend/collab-hub/components/tasks/kanban-column.tsx @@ -1,12 +1,12 @@ "use client" import type React from "react" - -import { useState } from "react" import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" import { Badge } from "@/components/ui/badge" import { Button } from "@/components/ui/button" -import { TaskCard } from "./task-card" +import { SortableContext, verticalListSortingStrategy } from "@dnd-kit/sortable" +import { useDroppable } from "@dnd-kit/core" +import { SortableTaskCard } from "./sortable-task-card" import { Plus } from "lucide-react" import type { Task } from "@/lib/api/types" import { cn } from "@/lib/utils" @@ -32,43 +32,17 @@ export function KanbanColumn({ onCreateTask, color = "bg-muted", }: KanbanColumnProps) { - const [isDragOver, setIsDragOver] = useState(false) - - const handleDragOver = (e: React.DragEvent) => { - e.preventDefault() - setIsDragOver(true) - } - - const handleDragLeave = (e: React.DragEvent) => { - e.preventDefault() - setIsDragOver(false) - } - - const handleDrop = (e: React.DragEvent) => { - e.preventDefault() - setIsDragOver(false) - - try { - const taskData = e.dataTransfer.getData("text/plain") - const task: Task = JSON.parse(taskData) - - if (task.status !== status) { - onTaskStatusChange(task, status) - } - } catch (error) { - console.error("Error handling task drop:", error) - } - } + const { isOver, setNodeRef } = useDroppable({ + id: status, + }) return (
@@ -86,27 +60,29 @@ export function KanbanColumn({ - {tasks.length === 0 ? ( -
-
- + task.id)} strategy={verticalListSortingStrategy}> + {tasks.length === 0 ? ( +
+
+ +
+

No tasks yet

+
-

No tasks yet

- -
- ) : ( - tasks.map((task) => ( - - )) - )} + ) : ( + tasks.map((task) => ( + + )) + )} + ) diff --git a/frontend/collab-hub/components/tasks/sortable-task-card.tsx b/frontend/collab-hub/components/tasks/sortable-task-card.tsx new file mode 100644 index 0000000..521531f --- /dev/null +++ b/frontend/collab-hub/components/tasks/sortable-task-card.tsx @@ -0,0 +1,33 @@ +"use client" + +import { CSS } from "@dnd-kit/utilities" +import { useSortable } from "@dnd-kit/sortable" +import type { Task } from "@/lib/api/types" +import { cn } from "@/lib/utils" +import { TaskCard } from "./task-card" + +interface SortableTaskCardProps { + task: Task + onEdit: (task: Task) => void + onDelete: (task: Task) => void + onStatusChange: (task: Task, newStatus: Task["status"]) => void +} + +export function SortableTaskCard({ task, onEdit, onDelete, onStatusChange }: SortableTaskCardProps) { + const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({ + id: task.id, + }) + + const style = { + transform: CSS.Transform.toString(transform), + transition, + } + + return ( +
+
+ +
+
+ ) +} diff --git a/frontend/collab-hub/components/tasks/task-card.tsx b/frontend/collab-hub/components/tasks/task-card.tsx index 1a81e9e..6e823c8 100644 --- a/frontend/collab-hub/components/tasks/task-card.tsx +++ b/frontend/collab-hub/components/tasks/task-card.tsx @@ -46,12 +46,8 @@ export function TaskCard({ task, onEdit, onDelete, onStatusChange, isDragging }: className={cn( "cursor-pointer hover:shadow-md transition-all duration-200 border-l-4", getPriorityColor(), - isDragging && "opacity-50 rotate-2 shadow-lg", + isDragging && "opacity-60 shadow-lg", )} - draggable - onDragStart={(e) => { - e.dataTransfer.setData("text/plain", JSON.stringify(task)) - }} >
diff --git a/frontend/collab-hub/components/users/invitation-list.tsx b/frontend/collab-hub/components/users/invitation-list.tsx index 38a59ce..f7c2dc7 100644 --- a/frontend/collab-hub/components/users/invitation-list.tsx +++ b/frontend/collab-hub/components/users/invitation-list.tsx @@ -88,26 +88,26 @@ export function InvitationList() { {pendingInvitations.map((invitation) => ( -
+
{invitation.project.name} - Invited by {invitation.invitedBy.name} ({invitation.invitedBy.email}) + Invited by {invitation.invitedBy.name || invitation.invitedBy.email} ({invitation.invitedBy.email})
- + Pending
-
- - diff --git a/frontend/collab-hub/components/users/project-members.tsx b/frontend/collab-hub/components/users/project-members.tsx index 5561c2b..9520e6d 100644 --- a/frontend/collab-hub/components/users/project-members.tsx +++ b/frontend/collab-hub/components/users/project-members.tsx @@ -79,7 +79,7 @@ export function ProjectMembers({ projectId, isOwner }: ProjectMembersProps) {
{members.map((member) => ( -
+
diff --git a/frontend/collab-hub/hooks/use-invitations.ts b/frontend/collab-hub/hooks/use-invitations.ts index 6f9051c..b1b7c6d 100644 --- a/frontend/collab-hub/hooks/use-invitations.ts +++ b/frontend/collab-hub/hooks/use-invitations.ts @@ -1,6 +1,6 @@ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query" import { acceptInvitation, declineInvitation, getUserInvitations, sendInvitation } from "@/lib/api/services/invitations" -import type { Invitation } from "@/lib/api/types" +import { projectKeys } from "@/hooks/use-projects" export const invitationKeys = { all: ["invitations"] as const, @@ -14,15 +14,8 @@ export const useInvitations = () => { } export const useSendInvitation = () => { - const queryClient = useQueryClient() - return useMutation({ mutationFn: ({ projectId, email }: { projectId: string; email: string }) => sendInvitation(projectId, email), - onSuccess: (invitation) => { - queryClient.setQueryData(invitationKeys.all, (old) => - old ? [invitation, ...old] : [invitation], - ) - }, }) } @@ -33,6 +26,7 @@ export const useAcceptInvitation = () => { mutationFn: (invitationId: string) => acceptInvitation(invitationId), onSuccess: () => { queryClient.invalidateQueries({ queryKey: invitationKeys.all }) + queryClient.invalidateQueries({ queryKey: projectKeys.all }) }, }) } diff --git a/frontend/collab-hub/hooks/use-notifications.ts b/frontend/collab-hub/hooks/use-notifications.ts new file mode 100644 index 0000000..2337939 --- /dev/null +++ b/frontend/collab-hub/hooks/use-notifications.ts @@ -0,0 +1,46 @@ +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query" +import { getNotifications, markAllNotificationsRead, markNotificationRead } from "@/lib/api/services/notifications" +import type { Notification } from "@/lib/api/types" + +export const notificationKeys = { + all: ["notifications"] as const, +} + +export const useNotifications = () => { + return useQuery({ + queryKey: notificationKeys.all, + queryFn: getNotifications, + }) +} + +export const useMarkNotificationRead = () => { + const queryClient = useQueryClient() + + return useMutation({ + mutationFn: (notificationId: string) => markNotificationRead(notificationId), + onSuccess: (_, notificationId) => { + queryClient.setQueryData(notificationKeys.all, (old) => + old + ? old.map((notification) => + notification.id === notificationId && !notification.readAt + ? { ...notification, readAt: new Date().toISOString() } + : notification, + ) + : old, + ) + }, + }) +} + +export const useMarkAllNotificationsRead = () => { + const queryClient = useQueryClient() + + return useMutation({ + mutationFn: () => markAllNotificationsRead(), + onSuccess: () => { + queryClient.setQueryData(notificationKeys.all, (old) => + old ? old.map((notification) => ({ ...notification, readAt: notification.readAt || new Date().toISOString() })) : old, + ) + }, + }) +} diff --git a/frontend/collab-hub/lib/api/index.ts b/frontend/collab-hub/lib/api/index.ts index e0b7de4..21f4b57 100644 --- a/frontend/collab-hub/lib/api/index.ts +++ b/frontend/collab-hub/lib/api/index.ts @@ -3,6 +3,7 @@ export type { ChannelMember, Invitation, Message, + Notification, Project, ProjectMember, Task, @@ -15,6 +16,7 @@ export * from "./services/projects" export * from "./services/tasks" export * from "./services/chat" export * from "./services/invitations" +export * from "./services/notifications" export * from "./error" import * as invitationService from "./services/invitations" diff --git a/frontend/collab-hub/lib/api/services/invitations.ts b/frontend/collab-hub/lib/api/services/invitations.ts index 6df31a3..ddfc664 100644 --- a/frontend/collab-hub/lib/api/services/invitations.ts +++ b/frontend/collab-hub/lib/api/services/invitations.ts @@ -7,14 +7,14 @@ export const sendInvitation = async (projectId: string, email: string): Promise< } export const getUserInvitations = async (): Promise => { - const response = await apiClient.get("/users/me/invitations") + const response = await apiClient.get("/invitations") return response.data } export const acceptInvitation = async (invitationId: string): Promise => { - await apiClient.get(`/invitations/${invitationId}/accept`) + await apiClient.post(`/invitations/${invitationId}/accept`) } export const declineInvitation = async (invitationId: string): Promise => { - await apiClient.get(`/invitations/${invitationId}/decline`) + await apiClient.post(`/invitations/${invitationId}/decline`) } diff --git a/frontend/collab-hub/lib/api/services/notifications.ts b/frontend/collab-hub/lib/api/services/notifications.ts new file mode 100644 index 0000000..e11364f --- /dev/null +++ b/frontend/collab-hub/lib/api/services/notifications.ts @@ -0,0 +1,15 @@ +import { apiClient } from "../http-client" +import type { Notification } from "../types" + +export const getNotifications = async (): Promise => { + const response = await apiClient.get("/notifications") + return response.data +} + +export const markNotificationRead = async (notificationId: string): Promise => { + await apiClient.post(`/notifications/${notificationId}/read`) +} + +export const markAllNotificationsRead = async (): Promise => { + await apiClient.post("/notifications/read-all") +} diff --git a/frontend/collab-hub/lib/api/types.ts b/frontend/collab-hub/lib/api/types.ts index 6e7529a..5062ac0 100644 --- a/frontend/collab-hub/lib/api/types.ts +++ b/frontend/collab-hub/lib/api/types.ts @@ -117,3 +117,14 @@ export interface Invitation { email: string } } + +export interface Notification { + id: string + userId: string + type: "INVITATION_SENT" | "INVITATION_ACCEPTED" | "TASK_ASSIGNED" + title: string + body: string + link?: string | null + readAt?: string | null + createdAt: string +} \ No newline at end of file diff --git a/frontend/collab-hub/package-lock.json b/frontend/collab-hub/package-lock.json index 5cb8469..78922fb 100644 --- a/frontend/collab-hub/package-lock.json +++ b/frontend/collab-hub/package-lock.json @@ -8,6 +8,9 @@ "name": "collab-hub", "version": "0.1.0", "dependencies": { + "@dnd-kit/core": "^6.3.1", + "@dnd-kit/sortable": "^10.0.0", + "@dnd-kit/utilities": "^3.2.2", "@radix-ui/react-avatar": "^1.1.10", "@radix-ui/react-dialog": "^1.1.15", "@radix-ui/react-dropdown-menu": "^2.1.16", @@ -66,6 +69,59 @@ "integrity": "sha512-P5LUNhtbj6YfI3iJjw5EL9eUAG6OitD0W3fWQcpQjDRc/QIsL0tRNuO1PcDvPccWL1fSTXXdE1ds+l95DV/OFA==", "license": "MIT" }, + "node_modules/@dnd-kit/accessibility": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@dnd-kit/accessibility/-/accessibility-3.1.1.tgz", + "integrity": "sha512-2P+YgaXF+gRsIihwwY1gCsQSYnu9Zyj2py8kY5fFvUM1qm2WA2u639R6YNVfU4GWr+ZM5mqEsfHZZLoRONbemw==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "peerDependencies": { + "react": ">=16.8.0" + } + }, + "node_modules/@dnd-kit/core": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/@dnd-kit/core/-/core-6.3.1.tgz", + "integrity": "sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ==", + "license": "MIT", + "dependencies": { + "@dnd-kit/accessibility": "^3.1.1", + "@dnd-kit/utilities": "^3.2.2", + "tslib": "^2.0.0" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@dnd-kit/sortable": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/@dnd-kit/sortable/-/sortable-10.0.0.tgz", + "integrity": "sha512-+xqhmIIzvAYMGfBYYnbKuNicfSsk4RksY2XdmJhT+HAC01nix6fHCztU68jooFiMUB01Ky3F0FyOvhG/BZrWkg==", + "license": "MIT", + "dependencies": { + "@dnd-kit/utilities": "^3.2.2", + "tslib": "^2.0.0" + }, + "peerDependencies": { + "@dnd-kit/core": "^6.3.0", + "react": ">=16.8.0" + } + }, + "node_modules/@dnd-kit/utilities": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@dnd-kit/utilities/-/utilities-3.2.2.tgz", + "integrity": "sha512-+MKAJEOfaBe5SmV6t34p80MMKhjvUz0vRrvVJbPT0WElzaOJ/1xs+D+KDv+tD/NE5ujfrChEcshd4fLn0wpiqg==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "peerDependencies": { + "react": ">=16.8.0" + } + }, "node_modules/@emnapi/core": { "version": "1.4.5", "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.4.5.tgz", @@ -5182,9 +5238,9 @@ "license": "MIT" }, "node_modules/js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", "dev": true, "license": "MIT", "dependencies": { diff --git a/frontend/collab-hub/package.json b/frontend/collab-hub/package.json index 68ba2d8..a08e898 100644 --- a/frontend/collab-hub/package.json +++ b/frontend/collab-hub/package.json @@ -9,6 +9,9 @@ "lint": "next lint" }, "dependencies": { + "@dnd-kit/core": "^6.3.1", + "@dnd-kit/sortable": "^10.0.0", + "@dnd-kit/utilities": "^3.2.2", "@radix-ui/react-avatar": "^1.1.10", "@radix-ui/react-dialog": "^1.1.15", "@radix-ui/react-dropdown-menu": "^2.1.16",