From 2b650aaf01a8f49d60b0987848c33a4dc1896db0 Mon Sep 17 00:00:00 2001 From: Akshit Dhakad <116799118+akshitDhakad@users.noreply.github.com> Date: Thu, 4 Dec 2025 23:27:36 +0530 Subject: [PATCH] Add chat and project management features with context providers. Introduced Auth, Notification, and Socket contexts for state management. Added new pages and components for Teams, Projects, and Chat functionalities. Updated dependencies in package.json and package-lock.json. --- package-lock.json | 146 +++++++++++++- package.json | 4 +- src/contexts/auth-context.tsx | 95 +++++++++ src/contexts/notification-context.tsx | 120 ++++++++++++ src/contexts/socket-context.tsx | 112 +++++++++++ .../components/notifications-popover.tsx | 35 +++- src/layouts/nav-config-dashboard.tsx | 30 +-- src/main.tsx | 9 +- src/pages/chat.tsx | 10 + src/pages/project-detail.tsx | 10 + src/pages/projects.tsx | 10 + src/pages/team-detail.tsx | 10 + src/pages/teams.tsx | 10 + src/providers/app-providers.tsx | 32 +++ src/routes/sections.tsx | 10 + src/sections/chat/chat-window.tsx | 184 ++++++++++++++++++ src/sections/chat/view/chat-view.tsx | 133 +++++++++++++ src/sections/chat/view/index.ts | 2 + src/sections/project/project-chat.tsx | 169 ++++++++++++++++ .../project/project-create-dialog.tsx | 98 ++++++++++ src/sections/project/project-members.tsx | 85 ++++++++ src/sections/project/project-tasks.tsx | 110 +++++++++++ src/sections/project/projects-list.tsx | 88 +++++++++ src/sections/project/view/index.ts | 2 + .../project/view/project-detail-view.tsx | 164 ++++++++++++++++ src/sections/project/view/projects-view.tsx | 164 ++++++++++++++++ src/sections/team/team-create-dialog.tsx | 63 ++++++ src/sections/team/team-members.tsx | 86 ++++++++ src/sections/team/team-projects.tsx | 108 ++++++++++ src/sections/team/teams-list.tsx | 74 +++++++ src/sections/team/view/index.ts | 2 + src/sections/team/view/team-detail-view.tsx | 134 +++++++++++++ src/sections/team/view/teams-view.tsx | 102 ++++++++++ src/services/api.ts | 70 +++++++ src/types/chat.ts | 35 ++++ src/types/index.ts | 5 + src/types/notification.ts | 12 ++ src/types/project.ts | 38 ++++ src/types/team.ts | 28 +++ src/utils/format-time.ts | 13 ++ yarn.lock | 97 +++++++-- 41 files changed, 2663 insertions(+), 46 deletions(-) create mode 100644 src/contexts/auth-context.tsx create mode 100644 src/contexts/notification-context.tsx create mode 100644 src/contexts/socket-context.tsx create mode 100644 src/pages/chat.tsx create mode 100644 src/pages/project-detail.tsx create mode 100644 src/pages/projects.tsx create mode 100644 src/pages/team-detail.tsx create mode 100644 src/pages/teams.tsx create mode 100644 src/providers/app-providers.tsx create mode 100644 src/sections/chat/chat-window.tsx create mode 100644 src/sections/chat/view/chat-view.tsx create mode 100644 src/sections/chat/view/index.ts create mode 100644 src/sections/project/project-chat.tsx create mode 100644 src/sections/project/project-create-dialog.tsx create mode 100644 src/sections/project/project-members.tsx create mode 100644 src/sections/project/project-tasks.tsx create mode 100644 src/sections/project/projects-list.tsx create mode 100644 src/sections/project/view/index.ts create mode 100644 src/sections/project/view/project-detail-view.tsx create mode 100644 src/sections/project/view/projects-view.tsx create mode 100644 src/sections/team/team-create-dialog.tsx create mode 100644 src/sections/team/team-members.tsx create mode 100644 src/sections/team/team-projects.tsx create mode 100644 src/sections/team/teams-list.tsx create mode 100644 src/sections/team/view/index.ts create mode 100644 src/sections/team/view/team-detail-view.tsx create mode 100644 src/sections/team/view/teams-view.tsx create mode 100644 src/services/api.ts create mode 100644 src/types/chat.ts create mode 100644 src/types/index.ts create mode 100644 src/types/notification.ts create mode 100644 src/types/project.ts create mode 100644 src/types/team.ts diff --git a/package-lock.json b/package-lock.json index 88c6fd922..fa9fc608a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,6 +16,7 @@ "@iconify/react": "^5.2.1", "@mui/lab": "^7.0.0-beta.10", "@mui/material": "^7.0.1", + "@types/socket.io-client": "^1.4.36", "apexcharts": "^4.5.0", "dayjs": "^1.11.13", "es-toolkit": "^1.34.1", @@ -24,7 +25,8 @@ "react-apexcharts": "^1.7.0", "react-dom": "^19.1.0", "react-router-dom": "^7.4.1", - "simplebar-react": "^3.3.0" + "simplebar-react": "^3.3.0", + "socket.io-client": "^4.8.1" }, "devDependencies": { "@eslint/js": "^9.23.0", @@ -1762,6 +1764,12 @@ "dev": true, "license": "MIT" }, + "node_modules/@socket.io/component-emitter": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz", + "integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==", + "license": "MIT" + }, "node_modules/@svgdotjs/svg.draggable.js": { "version": "3.0.6", "resolved": "https://registry.npmjs.org/@svgdotjs/svg.draggable.js/-/svg.draggable.js-3.0.6.tgz", @@ -2132,6 +2140,12 @@ "@types/react": "*" } }, + "node_modules/@types/socket.io-client": { + "version": "1.4.36", + "resolved": "https://registry.npmjs.org/@types/socket.io-client/-/socket.io-client-1.4.36.tgz", + "integrity": "sha512-ZJWjtFBeBy1kRSYpVbeGYTElf6BqPQUkXDlHHD4k/42byCN5Rh027f4yARHCink9sKAkbtGZXEAmR0ZCnc2/Ag==", + "license": "MIT" + }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "8.29.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.29.0.tgz", @@ -3220,6 +3234,45 @@ "node": ">= 0.4" } }, + "node_modules/engine.io-client": { + "version": "6.6.3", + "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.6.3.tgz", + "integrity": "sha512-T0iLjnyNWahNyv/lcjS2y4oE358tVS/SYQNxYXGAJ9/GLgH4VCvOQ/mhTjqU88mLZCQgiG8RIegFHYCdVC+j5w==", + "license": "MIT", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.3.1", + "engine.io-parser": "~5.2.1", + "ws": "~8.17.1", + "xmlhttprequest-ssl": "~2.1.1" + } + }, + "node_modules/engine.io-client/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/engine.io-parser": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz", + "integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/error-ex": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", @@ -6088,6 +6141,68 @@ "react": ">=16.8.0" } }, + "node_modules/socket.io-client": { + "version": "4.8.1", + "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.8.1.tgz", + "integrity": "sha512-hJVXfu3E28NmzGk8o1sHhN3om52tRvwYeidbj7xKy2eIIse5IoKX3USlS6Tqt3BHAtflLIkCQBkzVrEEfWUyYQ==", + "license": "MIT", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.3.2", + "engine.io-client": "~6.6.1", + "socket.io-parser": "~4.2.4" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/socket.io-client/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/socket.io-parser": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz", + "integrity": "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==", + "license": "MIT", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.3.1" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/socket.io-parser/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, "node_modules/source-map": { "version": "0.5.7", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", @@ -6819,6 +6934,35 @@ "node": ">=0.10.0" } }, + "node_modules/ws": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", + "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/xmlhttprequest-ssl": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.1.2.tgz", + "integrity": "sha512-TEU+nJVUUnA4CYJFLvK5X9AOeH4KvDvhIfm0vV1GaQRtchnG0hgK5p8hw/xjv8cunWYCsiPCSDzObPyhEwq3KQ==", + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/yaml": { "version": "2.7.1", "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.7.1.tgz", diff --git a/package.json b/package.json index a4b79e4b4..daba41f79 100644 --- a/package.json +++ b/package.json @@ -36,6 +36,7 @@ "@iconify/react": "^5.2.1", "@mui/lab": "^7.0.0-beta.10", "@mui/material": "^7.0.1", + "@types/socket.io-client": "^1.4.36", "apexcharts": "^4.5.0", "dayjs": "^1.11.13", "es-toolkit": "^1.34.1", @@ -44,7 +45,8 @@ "react-apexcharts": "^1.7.0", "react-dom": "^19.1.0", "react-router-dom": "^7.4.1", - "simplebar-react": "^3.3.0" + "simplebar-react": "^3.3.0", + "socket.io-client": "^4.8.1" }, "devDependencies": { "@eslint/js": "^9.23.0", diff --git a/src/contexts/auth-context.tsx b/src/contexts/auth-context.tsx new file mode 100644 index 000000000..1ad1a25e6 --- /dev/null +++ b/src/contexts/auth-context.tsx @@ -0,0 +1,95 @@ +import { createContext, useContext, useState, useEffect, type ReactNode } from 'react'; + +import type { User } from 'src/types'; + +// ---------------------------------------------------------------------- + +type AuthContextType = { + user: User | null; + isAuthenticated: boolean; + login: (email: string, password: string) => Promise; + logout: () => void; + updateUser: (user: Partial) => void; +}; + +const AuthContext = createContext(undefined); + +// ---------------------------------------------------------------------- + +type AuthProviderProps = { + children: ReactNode; +}; + +export function AuthProvider({ children }: AuthProviderProps) { + const [user, setUser] = useState(null); + + useEffect(() => { + // Load user from localStorage or API + const stored = localStorage.getItem('user'); + if (stored) { + try { + setUser(JSON.parse(stored)); + } catch { + // Ignore parse errors + } + } else { + // Demo user for development + setUser({ + id: '1', + name: 'John Doe', + email: 'john@example.com', + avatar: '/assets/images/avatar/avatar_1.jpg', + status: 'online', + }); + } + }, []); + + const login = async (email: string, password: string) => { + // TODO: Replace with actual API call + const demoUser: User = { + id: '1', + name: 'John Doe', + email, + avatar: '/assets/images/avatar/avatar_1.jpg', + status: 'online', + }; + setUser(demoUser); + localStorage.setItem('user', JSON.stringify(demoUser)); + }; + + const logout = () => { + setUser(null); + localStorage.removeItem('user'); + }; + + const updateUser = (updates: Partial) => { + if (user) { + const updated = { ...user, ...updates }; + setUser(updated); + localStorage.setItem('user', JSON.stringify(updated)); + } + }; + + return ( + + {children} + + ); +} + +export function useAuth() { + const context = useContext(AuthContext); + if (context === undefined) { + throw new Error('useAuth must be used within an AuthProvider'); + } + return context; +} + diff --git a/src/contexts/notification-context.tsx b/src/contexts/notification-context.tsx new file mode 100644 index 000000000..cac4ec018 --- /dev/null +++ b/src/contexts/notification-context.tsx @@ -0,0 +1,120 @@ +import { createContext, useContext, useEffect, useState, useCallback, type ReactNode } from 'react'; + +import type { Notification } from 'src/types'; + +// ---------------------------------------------------------------------- + +type NotificationContextType = { + notifications: Notification[]; + unreadCount: number; + addNotification: (notification: Notification) => void; + markAsRead: (id: string) => void; + markAllAsRead: () => void; + removeNotification: (id: string) => void; + clearAll: () => void; + requestPermission: () => Promise; +}; + +const NotificationContext = createContext(undefined); + +// ---------------------------------------------------------------------- + +type NotificationProviderProps = { + children: ReactNode; + userId: string; +}; + +export function NotificationProvider({ children, userId }: NotificationProviderProps) { + const [notifications, setNotifications] = useState([]); + + useEffect(() => { + // Load notifications from localStorage or API + const stored = localStorage.getItem(`notifications_${userId}`); + if (stored) { + try { + setNotifications(JSON.parse(stored)); + } catch { + // Ignore parse errors + } + } + }, [userId]); + + useEffect(() => { + // Save notifications to localStorage + localStorage.setItem(`notifications_${userId}`, JSON.stringify(notifications)); + }, [notifications, userId]); + + const unreadCount = notifications.filter((n) => !n.read).length; + + const addNotification = useCallback((notification: Notification) => { + setNotifications((prev) => [notification, ...prev]); + + // Show browser notification if permission granted + if ('Notification' in window && Notification.permission === 'granted') { + new Notification(notification.title, { + body: notification.message, + icon: '/favicon.ico', + tag: notification.id, + }); + } + }, []); + + const markAsRead = useCallback((id: string) => { + setNotifications((prev) => prev.map((n) => (n.id === id ? { ...n, read: true } : n))); + }, []); + + const markAllAsRead = useCallback(() => { + setNotifications((prev) => prev.map((n) => ({ ...n, read: true }))); + }, []); + + const removeNotification = useCallback((id: string) => { + setNotifications((prev) => prev.filter((n) => n.id !== id)); + }, []); + + const clearAll = useCallback(() => { + setNotifications([]); + }, []); + + const requestPermission = useCallback(async () => { + if (!('Notification' in window)) { + return false; + } + + if (Notification.permission === 'granted') { + return true; + } + + if (Notification.permission !== 'denied') { + const permission = await Notification.requestPermission(); + return permission === 'granted'; + } + + return false; + }, []); + + return ( + + {children} + + ); +} + +export function useNotifications() { + const context = useContext(NotificationContext); + if (context === undefined) { + throw new Error('useNotifications must be used within a NotificationProvider'); + } + return context; +} + diff --git a/src/contexts/socket-context.tsx b/src/contexts/socket-context.tsx new file mode 100644 index 000000000..e71ec29b0 --- /dev/null +++ b/src/contexts/socket-context.tsx @@ -0,0 +1,112 @@ +import { createContext, useContext, useEffect, useState, useRef, type ReactNode } from 'react'; +import { io, type Socket } from 'socket.io-client'; + +import type { ChatMessage } from 'src/types'; + +// ---------------------------------------------------------------------- + +type SocketContextType = { + socket: Socket | null; + isConnected: boolean; + sendMessage: (chatId: string, content: string, type?: 'text' | 'file' | 'image') => void; + joinChat: (chatId: string) => void; + leaveChat: (chatId: string) => void; + onMessage: (callback: (message: ChatMessage) => void) => void; + offMessage: (callback: (message: ChatMessage) => void) => void; +}; + +const SocketContext = createContext(undefined); + +// ---------------------------------------------------------------------- + +type SocketProviderProps = { + children: ReactNode; + userId: string; + serverUrl?: string; +}; + +export function SocketProvider({ children, userId, serverUrl = 'http://localhost:3001' }: SocketProviderProps) { + const [socket, setSocket] = useState(null); + const [isConnected, setIsConnected] = useState(false); + const messageCallbacksRef = useRef void>>(new Set()); + + useEffect(() => { + if (!userId) return; + + const newSocket = io(serverUrl, { + auth: { userId }, + transports: ['websocket', 'polling'], + }); + + newSocket.on('connect', () => { + setIsConnected(true); + console.log('Socket connected'); + }); + + newSocket.on('disconnect', () => { + setIsConnected(false); + console.log('Socket disconnected'); + }); + + newSocket.on('message', (message: ChatMessage) => { + messageCallbacksRef.current.forEach((callback) => callback(message)); + }); + + setSocket(newSocket); + + return () => { + newSocket.close(); + }; + }, [userId, serverUrl]); + + const sendMessage = (chatId: string, content: string, type: 'text' | 'file' | 'image' = 'text') => { + if (socket && isConnected) { + socket.emit('sendMessage', { chatId, content, type }); + } + }; + + const joinChat = (chatId: string) => { + if (socket && isConnected) { + socket.emit('joinChat', chatId); + } + }; + + const leaveChat = (chatId: string) => { + if (socket && isConnected) { + socket.emit('leaveChat', chatId); + } + }; + + const onMessage = (callback: (message: ChatMessage) => void) => { + messageCallbacksRef.current.add(callback); + }; + + const offMessage = (callback: (message: ChatMessage) => void) => { + messageCallbacksRef.current.delete(callback); + }; + + return ( + + {children} + + ); +} + +export function useSocket() { + const context = useContext(SocketContext); + if (context === undefined) { + throw new Error('useSocket must be used within a SocketProvider'); + } + return context; +} + diff --git a/src/layouts/components/notifications-popover.tsx b/src/layouts/components/notifications-popover.tsx index 3a7031813..707b50016 100644 --- a/src/layouts/components/notifications-popover.tsx +++ b/src/layouts/components/notifications-popover.tsx @@ -21,6 +21,7 @@ import { fToNow } from 'src/utils/format-time'; import { Iconify } from 'src/components/iconify'; import { Scrollbar } from 'src/components/scrollbar'; +import { useNotifications } from 'src/contexts/notification-context'; // ---------------------------------------------------------------------- @@ -39,9 +40,22 @@ export type NotificationsPopoverProps = IconButtonProps & { }; export function NotificationsPopover({ data = [], sx, ...other }: NotificationsPopoverProps) { - const [notifications, setNotifications] = useState(data); + const { notifications: contextNotifications, unreadCount, markAllAsRead } = useNotifications(); + + // Use context notifications if available, otherwise fall back to prop data + const notifications = contextNotifications.length > 0 + ? contextNotifications.map((n) => ({ + id: n.id, + type: n.type, + title: n.title, + isUnRead: !n.read, + description: n.message, + avatarUrl: null, + postedAt: n.createdAt, + })) + : data; - const totalUnRead = notifications.filter((item) => item.isUnRead === true).length; + const totalUnRead = unreadCount > 0 ? unreadCount : notifications.filter((item) => item.isUnRead === true).length; const [openPopover, setOpenPopover] = useState(null); @@ -54,13 +68,8 @@ export function NotificationsPopover({ data = [], sx, ...other }: NotificationsP }, []); const handleMarkAllAsRead = useCallback(() => { - const updatedNotifications = notifications.map((notification) => ({ - ...notification, - isUnRead: false, - })); - - setNotifications(updatedNotifications); - }, [notifications]); + markAllAsRead(); + }, [markAllAsRead]); return ( <> @@ -162,10 +171,18 @@ export function NotificationsPopover({ data = [], sx, ...other }: NotificationsP // ---------------------------------------------------------------------- function NotificationItem({ notification }: { notification: NotificationItemProps }) { + const { markAsRead } = useNotifications(); const { avatarUrl, title } = renderContent(notification); + const handleClick = () => { + if (notification.isUnRead) { + markAsRead(notification.id); + } + }; + return ( - +3 - - ), }, { title: 'Blog', path: '/blog', icon: icon('ic-blog'), }, - { - title: 'Sign in', - path: '/sign-in', - icon: icon('ic-lock'), - }, - { - title: 'Not found', - path: '/404', - icon: icon('ic-disabled'), - }, ]; diff --git a/src/main.tsx b/src/main.tsx index 9ae50be9c..6c3e90e3d 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -5,15 +5,18 @@ import { Outlet, RouterProvider, createBrowserRouter } from 'react-router'; import App from './app'; import { routesSection } from './routes/sections'; import { ErrorBoundary } from './routes/components'; +import { AppProviders } from './providers/app-providers'; // ---------------------------------------------------------------------- const router = createBrowserRouter([ { Component: () => ( - - - + + + + + ), errorElement: , children: routesSection, diff --git a/src/pages/chat.tsx b/src/pages/chat.tsx new file mode 100644 index 000000000..ebc3f0e5b --- /dev/null +++ b/src/pages/chat.tsx @@ -0,0 +1,10 @@ +import { lazy } from 'react'; + +const ChatView = lazy(() => import('src/sections/chat/view/chat-view')); + +// ---------------------------------------------------------------------- + +export default function ChatPage() { + return ; +} + diff --git a/src/pages/project-detail.tsx b/src/pages/project-detail.tsx new file mode 100644 index 000000000..f85ef77b7 --- /dev/null +++ b/src/pages/project-detail.tsx @@ -0,0 +1,10 @@ +import { lazy } from 'react'; + +const ProjectDetailView = lazy(() => import('src/sections/project/view/project-detail-view')); + +// ---------------------------------------------------------------------- + +export default function ProjectDetailPage() { + return ; +} + diff --git a/src/pages/projects.tsx b/src/pages/projects.tsx new file mode 100644 index 000000000..19db89475 --- /dev/null +++ b/src/pages/projects.tsx @@ -0,0 +1,10 @@ +import { lazy } from 'react'; + +const ProjectsView = lazy(() => import('src/sections/project/view/projects-view')); + +// ---------------------------------------------------------------------- + +export default function ProjectsPage() { + return ; +} + diff --git a/src/pages/team-detail.tsx b/src/pages/team-detail.tsx new file mode 100644 index 000000000..799eda538 --- /dev/null +++ b/src/pages/team-detail.tsx @@ -0,0 +1,10 @@ +import { lazy } from 'react'; + +const TeamDetailView = lazy(() => import('src/sections/team/view/team-detail-view')); + +// ---------------------------------------------------------------------- + +export default function TeamDetailPage() { + return ; +} + diff --git a/src/pages/teams.tsx b/src/pages/teams.tsx new file mode 100644 index 000000000..a8fa58528 --- /dev/null +++ b/src/pages/teams.tsx @@ -0,0 +1,10 @@ +import { lazy } from 'react'; + +const TeamsView = lazy(() => import('src/sections/team/view/teams-view')); + +// ---------------------------------------------------------------------- + +export default function TeamsPage() { + return ; +} + diff --git a/src/providers/app-providers.tsx b/src/providers/app-providers.tsx new file mode 100644 index 000000000..004a001f9 --- /dev/null +++ b/src/providers/app-providers.tsx @@ -0,0 +1,32 @@ +import { type ReactNode } from 'react'; + +import { AuthProvider, useAuth } from 'src/contexts/auth-context'; +import { NotificationProvider } from 'src/contexts/notification-context'; +import { SocketProvider } from 'src/contexts/socket-context'; + +// ---------------------------------------------------------------------- + +type AppProvidersProps = { + children: ReactNode; +}; + +function InnerProviders({ children }: AppProvidersProps) { + const { user } = useAuth(); + + return ( + + + {children} + + + ); +} + +export function AppProviders({ children }: AppProvidersProps) { + return ( + + {children} + + ); +} + diff --git a/src/routes/sections.tsx b/src/routes/sections.tsx index 20009c720..5ae51c69b 100644 --- a/src/routes/sections.tsx +++ b/src/routes/sections.tsx @@ -17,6 +17,11 @@ export const BlogPage = lazy(() => import('src/pages/blog')); export const UserPage = lazy(() => import('src/pages/user')); export const SignInPage = lazy(() => import('src/pages/sign-in')); export const ProductsPage = lazy(() => import('src/pages/products')); +export const TeamsPage = lazy(() => import('src/pages/teams')); +export const TeamDetailPage = lazy(() => import('src/pages/team-detail')); +export const ProjectsPage = lazy(() => import('src/pages/projects')); +export const ProjectDetailPage = lazy(() => import('src/pages/project-detail')); +export const ChatPage = lazy(() => import('src/pages/chat')); export const Page404 = lazy(() => import('src/pages/page-not-found')); const renderFallback = () => ( @@ -50,6 +55,11 @@ export const routesSection: RouteObject[] = [ ), children: [ { index: true, element: }, + { path: 'teams', element: }, + { path: 'teams/:id', element: }, + { path: 'projects', element: }, + { path: 'projects/:id', element: }, + { path: 'chat', element: }, { path: 'user', element: }, { path: 'products', element: }, { path: 'blog', element: }, diff --git a/src/sections/chat/chat-window.tsx b/src/sections/chat/chat-window.tsx new file mode 100644 index 000000000..ebd2c33bd --- /dev/null +++ b/src/sections/chat/chat-window.tsx @@ -0,0 +1,184 @@ +import { useState, useEffect, useRef } from 'react'; + +import Box from '@mui/material/Box'; +import Card from '@mui/material/Card'; +import Avatar from '@mui/material/Avatar'; +import TextField from '@mui/material/TextField'; +import IconButton from '@mui/material/IconButton'; +import Typography from '@mui/material/Typography'; + +import { Iconify } from 'src/components/iconify'; +import { Scrollbar } from 'src/components/scrollbar'; + +import { useSocket } from 'src/contexts/socket-context'; +import { useAuth } from 'src/contexts/auth-context'; + +import { fTime } from 'src/utils/format-time'; + +import type { Chat, ChatMessage } from 'src/types'; + +// ---------------------------------------------------------------------- + +type ChatWindowProps = { + chatId: string; +}; + +export function ChatWindow({ chatId }: ChatWindowProps) { + const { user } = useAuth(); + const { sendMessage, joinChat, leaveChat, onMessage, offMessage, isConnected } = useSocket(); + const [chat, setChat] = useState(null); + const [messages, setMessages] = useState([]); + const [input, setInput] = useState(''); + const messagesEndRef = useRef(null); + + useEffect(() => { + if (isConnected && chatId) { + joinChat(chatId); + loadChat(); + loadMessages(); + } + + const handleMessage = (message: ChatMessage) => { + if (message.chatId === chatId) { + setMessages((prev) => [...prev, message]); + } + }; + + onMessage(handleMessage); + + return () => { + offMessage(handleMessage); + if (isConnected) { + leaveChat(chatId); + } + }; + }, [chatId, isConnected, joinChat, leaveChat, onMessage, offMessage]); + + useEffect(() => { + messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' }); + }, [messages]); + + const loadChat = async () => { + // TODO: Replace with actual API call + // const response = await api.getChat(chatId); + // setChat(response.chat); + + // Demo data + setChat({ + id: chatId, + type: 'private', + participants: [], + unreadCount: 0, + updatedAt: new Date().toISOString(), + }); + }; + + const loadMessages = async () => { + // TODO: Replace with actual API call + // const response = await api.getChatMessages(chatId); + // setMessages(response.messages); + + // Demo data + setMessages([ + { + id: '1', + chatId, + senderId: '2', + content: 'Hello!', + type: 'text', + createdAt: new Date(Date.now() - 3600000).toISOString(), + readBy: [], + sender: { + id: '2', + name: 'Jane Doe', + email: 'jane@example.com', + }, + }, + ]); + }; + + const handleSend = () => { + if (input.trim() && isConnected) { + sendMessage(chatId, input.trim()); + setInput(''); + } + }; + + return ( + + + {chat?.name || 'Private Chat'} + {!isConnected && ( + + Connecting... + + )} + + + + + {messages.map((message) => { + const isOwn = message.senderId === user?.id; + return ( + + {!isOwn && ( + + {message.sender?.name.charAt(0)} + + )} + + {!isOwn && ( + + {message.sender?.name} + + )} + {message.content} + + {fTime(message.createdAt)} + + + + ); + })} +
+ + + + + setInput(e.target.value)} + onKeyPress={(e) => { + if (e.key === 'Enter' && !e.shiftKey) { + e.preventDefault(); + handleSend(); + } + }} + disabled={!isConnected} + /> + + + + + + ); +} + diff --git a/src/sections/chat/view/chat-view.tsx b/src/sections/chat/view/chat-view.tsx new file mode 100644 index 000000000..abfad6567 --- /dev/null +++ b/src/sections/chat/view/chat-view.tsx @@ -0,0 +1,133 @@ +import { useState, useEffect } from 'react'; + +import Box from '@mui/material/Box'; +import Card from '@mui/material/Card'; +import Avatar from '@mui/material/Avatar'; +import Container from '@mui/material/Container'; +import Typography from '@mui/material/Typography'; +import List from '@mui/material/List'; +import ListItem from '@mui/material/ListItem'; +import ListItemButton from '@mui/material/ListItemButton'; +import ListItemAvatar from '@mui/material/ListItemAvatar'; +import ListItemText from '@mui/material/ListItemText'; +import Badge from '@mui/material/Badge'; +import Divider from '@mui/material/Divider'; + +import { ChatWindow } from '../chat-window'; + +import type { Chat } from 'src/types'; + +// ---------------------------------------------------------------------- + +export default function ChatView() { + const [chats, setChats] = useState([]); + const [selectedChat, setSelectedChat] = useState(null); + const [loading, setLoading] = useState(true); + + useEffect(() => { + loadChats(); + }, []); + + const loadChats = async () => { + try { + setLoading(true); + // TODO: Replace with actual API call + // const response = await api.getChats(); + // setChats(response.chats); + + // Demo data + setChats([ + { + id: '1', + type: 'private', + participants: [], + unreadCount: 2, + updatedAt: new Date().toISOString(), + lastMessage: { + id: '1', + chatId: '1', + senderId: '2', + content: 'Hey, how are you?', + type: 'text', + createdAt: new Date().toISOString(), + readBy: [], + }, + }, + { + id: '2', + type: 'project', + name: 'Website Redesign', + participants: [], + unreadCount: 0, + updatedAt: new Date().toISOString(), + projectId: '1', + }, + ]); + } catch (error) { + console.error('Failed to load chats:', error); + } finally { + setLoading(false); + } + }; + + return ( + + + + Messages + + + + + + Chats + + + {chats.map((chat, index) => ( +
+ + setSelectedChat(chat.id)} + > + + + {chat.name?.charAt(0) || 'C'} + + + + + + {index < chats.length - 1 && } +
+ ))} +
+
+ + + {selectedChat ? ( + + ) : ( + + + Select a chat to start messaging + + + )} + +
+
+
+ ); +} + diff --git a/src/sections/chat/view/index.ts b/src/sections/chat/view/index.ts new file mode 100644 index 000000000..81f9bc09e --- /dev/null +++ b/src/sections/chat/view/index.ts @@ -0,0 +1,2 @@ +export { default as ChatView } from './chat-view'; + diff --git a/src/sections/project/project-chat.tsx b/src/sections/project/project-chat.tsx new file mode 100644 index 000000000..486775cb8 --- /dev/null +++ b/src/sections/project/project-chat.tsx @@ -0,0 +1,169 @@ +import { useState, useEffect, useRef } from 'react'; + +import Box from '@mui/material/Box'; +import Card from '@mui/material/Card'; +import Avatar from '@mui/material/Avatar'; +import TextField from '@mui/material/TextField'; +import IconButton from '@mui/material/IconButton'; +import Typography from '@mui/material/Typography'; + +import { Iconify } from 'src/components/iconify'; +import { Scrollbar } from 'src/components/scrollbar'; + +import { useSocket } from 'src/contexts/socket-context'; +import { useAuth } from 'src/contexts/auth-context'; + +import { fTime } from 'src/utils/format-time'; + +import type { ChatMessage } from 'src/types'; + +// ---------------------------------------------------------------------- + +type ProjectChatProps = { + projectId: string; +}; + +export function ProjectChat({ projectId }: ProjectChatProps) { + const { user } = useAuth(); + const { sendMessage, joinChat, leaveChat, onMessage, offMessage, isConnected } = useSocket(); + const [messages, setMessages] = useState([]); + const [input, setInput] = useState(''); + const messagesEndRef = useRef(null); + + useEffect(() => { + if (isConnected) { + joinChat(`project-${projectId}`); + loadMessages(); + } + + const handleMessage = (message: ChatMessage) => { + if (message.chatId === `project-${projectId}`) { + setMessages((prev) => [...prev, message]); + } + }; + + onMessage(handleMessage); + + return () => { + offMessage(handleMessage); + if (isConnected) { + leaveChat(`project-${projectId}`); + } + }; + }, [projectId, isConnected, joinChat, leaveChat, onMessage, offMessage]); + + useEffect(() => { + messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' }); + }, [messages]); + + const loadMessages = async () => { + // TODO: Replace with actual API call + // const response = await api.getChatMessages(`project-${projectId}`); + // setMessages(response.messages); + + // Demo data + setMessages([ + { + id: '1', + chatId: `project-${projectId}`, + senderId: '1', + content: 'Welcome to the project chat!', + type: 'text', + createdAt: new Date().toISOString(), + readBy: [], + sender: { + id: '1', + name: 'John Doe', + email: 'john@example.com', + }, + }, + ]); + }; + + const handleSend = () => { + if (input.trim() && isConnected) { + sendMessage(`project-${projectId}`, input.trim()); + setInput(''); + } + }; + + return ( + + + + Project Chat + {!isConnected && ( + + Connecting... + + )} + + + + + {messages.map((message) => { + const isOwn = message.senderId === user?.id; + return ( + + {!isOwn && ( + + {message.sender?.name.charAt(0)} + + )} + + {!isOwn && ( + + {message.sender?.name} + + )} + {message.content} + + {fTime(message.createdAt)} + + + + ); + })} +
+ + + + + setInput(e.target.value)} + onKeyPress={(e) => { + if (e.key === 'Enter' && !e.shiftKey) { + e.preventDefault(); + handleSend(); + } + }} + disabled={!isConnected} + /> + + + + + + + ); +} + diff --git a/src/sections/project/project-create-dialog.tsx b/src/sections/project/project-create-dialog.tsx new file mode 100644 index 000000000..df81c694d --- /dev/null +++ b/src/sections/project/project-create-dialog.tsx @@ -0,0 +1,98 @@ +import { useState } from 'react'; + +import Box from '@mui/material/Box'; +import Button from '@mui/material/Button'; +import TextField from '@mui/material/TextField'; +import Dialog from '@mui/material/Dialog'; +import DialogTitle from '@mui/material/DialogTitle'; +import DialogActions from '@mui/material/DialogActions'; +import DialogContent from '@mui/material/DialogContent'; +import MenuItem from '@mui/material/MenuItem'; +import Select from '@mui/material/Select'; +import FormControl from '@mui/material/FormControl'; +import InputLabel from '@mui/material/InputLabel'; + +import type { Project } from 'src/types'; + +// ---------------------------------------------------------------------- + +type ProjectCreateDialogProps = { + open: boolean; + onClose: () => void; + onCreate: (data: { + name: string; + description?: string; + teamId: string; + status: Project['status']; + priority: Project['priority']; + }) => void; +}; + +export function ProjectCreateDialog({ open, onClose, onCreate }: ProjectCreateDialogProps) { + const [name, setName] = useState(''); + const [description, setDescription] = useState(''); + const [teamId, setTeamId] = useState('1'); + const [status, setStatus] = useState('planning'); + const [priority, setPriority] = useState('medium'); + + const handleSubmit = () => { + if (name.trim()) { + onCreate({ name: name.trim(), description: description.trim() || undefined, teamId, status, priority }); + setName(''); + setDescription(''); + setStatus('planning'); + setPriority('medium'); + } + }; + + return ( + + Create New Project + + + setName(e.target.value)} + required + /> + setDescription(e.target.value)} + /> + + Status + + + + Priority + + + + + + + + + + ); +} + diff --git a/src/sections/project/project-members.tsx b/src/sections/project/project-members.tsx new file mode 100644 index 000000000..bdea7f244 --- /dev/null +++ b/src/sections/project/project-members.tsx @@ -0,0 +1,85 @@ +import { useState, useEffect } from 'react'; + +import Box from '@mui/material/Box'; +import Card from '@mui/material/Card'; +import Avatar from '@mui/material/Avatar'; +import Button from '@mui/material/Button'; +import Typography from '@mui/material/Typography'; +import IconButton from '@mui/material/IconButton'; + +import { Iconify } from 'src/components/iconify'; + +import type { ProjectMember } from 'src/types'; + +// ---------------------------------------------------------------------- + +type ProjectMembersProps = { + projectId: string; +}; + +export function ProjectMembers({ projectId }: ProjectMembersProps) { + const [members, setMembers] = useState([]); + + useEffect(() => { + loadMembers(); + }, [projectId]); + + const loadMembers = async () => { + // TODO: Replace with actual API call + // const response = await api.getProject(projectId); + // setMembers(response.project.members); + + // Demo data + setMembers([ + { + id: '1', + projectId, + userId: '1', + role: 'manager', + user: { + id: '1', + name: 'John Doe', + email: 'john@example.com', + status: 'online', + }, + }, + ]); + }; + + return ( + + + Project Members + + + + {members.length === 0 ? ( + + No members yet + + ) : ( + + {members.map((member) => ( + + + {member.user?.name.charAt(0)} + + {member.user?.name} + + {member.user?.email} • {member.role} + + + + + + + + ))} + + )} + + ); +} + diff --git a/src/sections/project/project-tasks.tsx b/src/sections/project/project-tasks.tsx new file mode 100644 index 000000000..5648b64e8 --- /dev/null +++ b/src/sections/project/project-tasks.tsx @@ -0,0 +1,110 @@ +import { useState, useEffect } from 'react'; + +import Box from '@mui/material/Box'; +import Card from '@mui/material/Card'; +import Button from '@mui/material/Button'; +import Typography from '@mui/material/Typography'; +import List from '@mui/material/List'; +import ListItem from '@mui/material/ListItem'; +import Checkbox from '@mui/material/Checkbox'; +import ListItemText from '@mui/material/ListItemText'; +import Chip from '@mui/material/Chip'; + +import { Iconify } from 'src/components/iconify'; +import { Label } from 'src/components/label'; + +import type { Task } from 'src/types'; + +// ---------------------------------------------------------------------- + +type ProjectTasksProps = { + projectId: string; +}; + +export function ProjectTasks({ projectId }: ProjectTasksProps) { + const [tasks, setTasks] = useState([]); + + useEffect(() => { + loadTasks(); + }, [projectId]); + + const loadTasks = async () => { + // TODO: Replace with actual API call + // const response = await api.getTasks(projectId); + // setTasks(response.tasks); + + // Demo data + setTasks([ + { + id: '1', + projectId, + title: 'Design homepage mockup', + description: 'Create initial design mockup for homepage', + status: 'in-progress', + priority: 'high', + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), + }, + { + id: '2', + projectId, + title: 'Setup development environment', + description: 'Configure dev environment and dependencies', + status: 'done', + priority: 'medium', + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), + }, + ]); + }; + + const getStatusColor = (status: Task['status']) => { + const colors: Record = { + todo: 'default', + 'in-progress': 'info', + review: 'warning', + done: 'success', + }; + return colors[status] || 'default'; + }; + + return ( + + + Tasks + + + + {tasks.length === 0 ? ( + + No tasks yet. Create your first task! + + ) : ( + + {tasks.map((task) => ( + + + + + {task.title} + + + + } + secondary={task.description} + /> + + + ))} + + )} + + ); +} + diff --git a/src/sections/project/projects-list.tsx b/src/sections/project/projects-list.tsx new file mode 100644 index 000000000..c5fadf1c0 --- /dev/null +++ b/src/sections/project/projects-list.tsx @@ -0,0 +1,88 @@ +import { useNavigate } from 'react-router-dom'; + +import Box from '@mui/material/Box'; +import Card from '@mui/material/Card'; +import Skeleton from '@mui/material/Skeleton'; +import CardContent from '@mui/material/CardContent'; +import Typography from '@mui/material/Typography'; +import CardActionArea from '@mui/material/CardActionArea'; +import Chip from '@mui/material/Chip'; + +import { Label } from 'src/components/label'; + +import type { Project } from 'src/types'; + +// ---------------------------------------------------------------------- + +type ProjectsListProps = { + projects: Project[]; + loading?: boolean; +}; + +export function ProjectsList({ projects, loading }: ProjectsListProps) { + const navigate = useNavigate(); + + const getStatusColor = (status: Project['status']) => { + const colors: Record = { + planning: 'info', + active: 'success', + 'on-hold': 'warning', + completed: 'default', + archived: 'default', + }; + return colors[status] || 'default'; + }; + + if (loading) { + return ( + + {[1, 2, 3].map((i) => ( + + + + ))} + + ); + } + + if (projects.length === 0) { + return ( + + + No projects found. + + + ); + } + + return ( + + {projects.map((project) => ( + + navigate(`/projects/${project.id}`)}> + + + {project.name} + + + {project.description && ( + + {project.description} + + )} + + + + {project.members.length} members + + + + + + ))} + + ); +} + diff --git a/src/sections/project/view/index.ts b/src/sections/project/view/index.ts new file mode 100644 index 000000000..7c3456c7a --- /dev/null +++ b/src/sections/project/view/index.ts @@ -0,0 +1,2 @@ +export { default as ProjectsView } from './projects-view'; + diff --git a/src/sections/project/view/project-detail-view.tsx b/src/sections/project/view/project-detail-view.tsx new file mode 100644 index 000000000..8fae191f8 --- /dev/null +++ b/src/sections/project/view/project-detail-view.tsx @@ -0,0 +1,164 @@ +import { useState, useEffect } from 'react'; +import { useParams, useNavigate } from 'react-router-dom'; + +import Box from '@mui/material/Box'; +import Card from '@mui/material/Card'; +import Button from '@mui/material/Button'; +import Container from '@mui/material/Container'; +import Typography from '@mui/material/Typography'; +import Tabs from '@mui/material/Tabs'; +import Tab from '@mui/material/Tab'; +import Chip from '@mui/material/Chip'; + +import { Iconify } from 'src/components/iconify'; +import { Label } from 'src/components/label'; + +import { ProjectTasks } from '../project-tasks'; +import { ProjectMembers } from '../project-members'; +import { ProjectChat } from '../project-chat'; + +import type { Project } from 'src/types'; + +// ---------------------------------------------------------------------- + +type TabPanelProps = { + children?: React.ReactNode; + index: number; + value: number; +}; + +function TabPanel({ children, value, index }: TabPanelProps) { + return ( + + ); +} + +export default function ProjectDetailView() { + const { id } = useParams<{ id: string }>(); + const navigate = useNavigate(); + const [project, setProject] = useState(null); + const [tabValue, setTabValue] = useState(0); + const [loading, setLoading] = useState(true); + + useEffect(() => { + if (id) { + loadProject(id); + } + }, [id]); + + const loadProject = async (projectId: string) => { + try { + setLoading(true); + // TODO: Replace with actual API call + // const response = await api.getProject(projectId); + // setProject(response.project); + + // Demo data + setProject({ + id: projectId, + name: 'Website Redesign', + description: 'Complete redesign of company website', + teamId: '1', + status: 'active', + priority: 'high', + members: [], + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), + }); + } catch (error) { + console.error('Failed to load project:', error); + } finally { + setLoading(false); + } + }; + + const getStatusColor = (status: Project['status']) => { + const colors: Record = { + planning: 'info', + active: 'success', + 'on-hold': 'warning', + completed: 'default', + archived: 'default', + }; + return colors[status] || 'default'; + }; + + if (loading) { + return ( + + + Loading... + + + ); + } + + if (!project) { + return ( + + + Project not found + + + + ); + } + + return ( + + + + + + + + + + + {project.name} + + {project.description && ( + + {project.description} + + )} + + + + + + + + + + + setTabValue(newValue)}> + + + + + + + + + + + + + + + + + + + + + ); +} + diff --git a/src/sections/project/view/projects-view.tsx b/src/sections/project/view/projects-view.tsx new file mode 100644 index 000000000..d8300d8f2 --- /dev/null +++ b/src/sections/project/view/projects-view.tsx @@ -0,0 +1,164 @@ +import { useState, useEffect } from 'react'; + +import Box from '@mui/material/Box'; +import Card from '@mui/material/Card'; +import Button from '@mui/material/Button'; +import Container from '@mui/material/Container'; +import Typography from '@mui/material/Typography'; +import Tabs from '@mui/material/Tabs'; +import Tab from '@mui/material/Tab'; + +import { Iconify } from 'src/components/iconify'; + +import { ProjectsList } from '../projects-list'; +import { ProjectCreateDialog } from '../project-create-dialog'; + +import type { Project } from 'src/types'; + +// ---------------------------------------------------------------------- + +type TabPanelProps = { + children?: React.ReactNode; + index: number; + value: number; +}; + +function TabPanel({ children, value, index }: TabPanelProps) { + return ( + + ); +} + +export default function ProjectsView() { + const [projects, setProjects] = useState([]); + const [openCreate, setOpenCreate] = useState(false); + const [loading, setLoading] = useState(true); + const [tabValue, setTabValue] = useState(0); + + useEffect(() => { + loadProjects(); + }, []); + + const loadProjects = async () => { + try { + setLoading(true); + // TODO: Replace with actual API call + // const response = await api.getProjects(); + // setProjects(response.projects); + + // Demo data + setProjects([ + { + id: '1', + name: 'Website Redesign', + description: 'Complete redesign of company website', + teamId: '1', + status: 'active', + priority: 'high', + members: [], + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), + }, + { + id: '2', + name: 'Mobile App', + description: 'New mobile application development', + teamId: '1', + status: 'planning', + priority: 'medium', + members: [], + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), + }, + ]); + } catch (error) { + console.error('Failed to load projects:', error); + } finally { + setLoading(false); + } + }; + + const handleCreateProject = async (data: { + name: string; + description?: string; + teamId: string; + status: Project['status']; + priority: Project['priority']; + }) => { + try { + // TODO: Replace with actual API call + const newProject: Project = { + id: Date.now().toString(), + ...data, + members: [], + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), + }; + setProjects((prev) => [newProject, ...prev]); + setOpenCreate(false); + } catch (error) { + console.error('Failed to create project:', error); + } + }; + + const filteredProjects = projects.filter((project) => { + if (tabValue === 0) return true; // All + if (tabValue === 1) return project.status === 'active'; + if (tabValue === 2) return project.status === 'planning'; + if (tabValue === 3) return project.status === 'completed'; + return true; + }); + + return ( + + + Projects + + + + + + setTabValue(newValue)}> + + + + + + + + + + + + + + + + + + + + + + + + setOpenCreate(false)} onCreate={handleCreateProject} /> + + ); +} + diff --git a/src/sections/team/team-create-dialog.tsx b/src/sections/team/team-create-dialog.tsx new file mode 100644 index 000000000..ac155a46e --- /dev/null +++ b/src/sections/team/team-create-dialog.tsx @@ -0,0 +1,63 @@ +import { useState } from 'react'; + +import Box from '@mui/material/Box'; +import Button from '@mui/material/Button'; +import TextField from '@mui/material/TextField'; +import Dialog from '@mui/material/Dialog'; +import DialogTitle from '@mui/material/DialogTitle'; +import DialogActions from '@mui/material/DialogActions'; +import DialogContent from '@mui/material/DialogContent'; + +// ---------------------------------------------------------------------- + +type TeamCreateDialogProps = { + open: boolean; + onClose: () => void; + onCreate: (data: { name: string; description?: string }) => void; +}; + +export function TeamCreateDialog({ open, onClose, onCreate }: TeamCreateDialogProps) { + const [name, setName] = useState(''); + const [description, setDescription] = useState(''); + + const handleSubmit = () => { + if (name.trim()) { + onCreate({ name: name.trim(), description: description.trim() || undefined }); + setName(''); + setDescription(''); + } + }; + + return ( + + Create New Team + + + setName(e.target.value)} + required + /> + setDescription(e.target.value)} + /> + + + + + + + + ); +} + diff --git a/src/sections/team/team-members.tsx b/src/sections/team/team-members.tsx new file mode 100644 index 000000000..90d226200 --- /dev/null +++ b/src/sections/team/team-members.tsx @@ -0,0 +1,86 @@ +import { useState, useEffect } from 'react'; + +import Box from '@mui/material/Box'; +import Card from '@mui/material/Card'; +import Avatar from '@mui/material/Avatar'; +import Button from '@mui/material/Button'; +import Typography from '@mui/material/Typography'; +import IconButton from '@mui/material/IconButton'; + +import { Iconify } from 'src/components/iconify'; + +import type { TeamMember, User } from 'src/types'; + +// ---------------------------------------------------------------------- + +type TeamMembersProps = { + teamId: string; +}; + +export function TeamMembers({ teamId }: TeamMembersProps) { + const [members, setMembers] = useState([]); + + useEffect(() => { + loadMembers(); + }, [teamId]); + + const loadMembers = async () => { + // TODO: Replace with actual API call + // const response = await api.getTeam(teamId); + // setMembers(response.team.members); + + // Demo data + setMembers([ + { + id: '1', + userId: '1', + teamId, + role: 'owner', + joinedAt: new Date().toISOString(), + user: { + id: '1', + name: 'John Doe', + email: 'john@example.com', + status: 'online', + }, + }, + ]); + }; + + return ( + + + Team Members + + + + {members.length === 0 ? ( + + No members yet + + ) : ( + + {members.map((member) => ( + + + {member.user?.name.charAt(0)} + + {member.user?.name} + + {member.user?.email} • {member.role} + + + + + + + + ))} + + )} + + ); +} + diff --git a/src/sections/team/team-projects.tsx b/src/sections/team/team-projects.tsx new file mode 100644 index 000000000..f37811502 --- /dev/null +++ b/src/sections/team/team-projects.tsx @@ -0,0 +1,108 @@ +import { useState, useEffect } from 'react'; +import { useNavigate } from 'react-router-dom'; + +import Box from '@mui/material/Box'; +import Card from '@mui/material/Card'; +import Button from '@mui/material/Button'; +import Typography from '@mui/material/Typography'; +import CardActionArea from '@mui/material/CardActionArea'; +import CardContent from '@mui/material/CardContent'; +import Chip from '@mui/material/Chip'; + +import { Iconify } from 'src/components/iconify'; +import { Label } from 'src/components/label'; + +import type { Project } from 'src/types'; + +// ---------------------------------------------------------------------- + +type TeamProjectsProps = { + teamId: string; +}; + +export function TeamProjects({ teamId }: TeamProjectsProps) { + const navigate = useNavigate(); + const [projects, setProjects] = useState([]); + + useEffect(() => { + loadProjects(); + }, [teamId]); + + const loadProjects = async () => { + // TODO: Replace with actual API call + // const response = await api.getProjects(teamId); + // setProjects(response.projects); + + // Demo data + setProjects([ + { + id: '1', + name: 'Website Redesign', + description: 'Complete redesign of company website', + teamId, + status: 'active', + priority: 'high', + members: [], + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), + }, + ]); + }; + + const getStatusColor = (status: Project['status']) => { + const colors: Record = { + planning: 'info', + active: 'success', + 'on-hold': 'warning', + completed: 'default', + archived: 'default', + }; + return colors[status] || 'default'; + }; + + return ( + + + Projects + + + + {projects.length === 0 ? ( + + No projects yet. Create your first project! + + ) : ( + + {projects.map((project) => ( + + navigate(`/projects/${project.id}`)}> + + + {project.name} + + + {project.description && ( + + {project.description} + + )} + + + + {project.members.length} members + + + + + + ))} + + )} + + ); +} + diff --git a/src/sections/team/teams-list.tsx b/src/sections/team/teams-list.tsx new file mode 100644 index 000000000..158b21740 --- /dev/null +++ b/src/sections/team/teams-list.tsx @@ -0,0 +1,74 @@ +import { useNavigate } from 'react-router-dom'; + +import Box from '@mui/material/Box'; +import Card from '@mui/material/Card'; +import Avatar from '@mui/material/Avatar'; +import Skeleton from '@mui/material/Skeleton'; +import CardContent from '@mui/material/CardContent'; +import Typography from '@mui/material/Typography'; +import CardActionArea from '@mui/material/CardActionArea'; + +import { fDate } from 'src/utils/format-time'; + +import type { Team } from 'src/types'; + +// ---------------------------------------------------------------------- + +type TeamsListProps = { + teams: Team[]; + loading?: boolean; +}; + +export function TeamsList({ teams, loading }: TeamsListProps) { + const navigate = useNavigate(); + + if (loading) { + return ( + + {[1, 2, 3].map((i) => ( + + + + ))} + + ); + } + + if (teams.length === 0) { + return ( + + + No teams found. Create your first team to get started! + + + ); + } + + return ( + + {teams.map((team) => ( + + navigate(`/teams/${team.id}`)}> + + + {team.name.charAt(0)} + + {team.name} + {team.description && ( + + {team.description} + + )} + + {team.members.length} members • Created {fDate(team.createdAt)} + + + + + + + ))} + + ); +} + diff --git a/src/sections/team/view/index.ts b/src/sections/team/view/index.ts new file mode 100644 index 000000000..3c55264a3 --- /dev/null +++ b/src/sections/team/view/index.ts @@ -0,0 +1,2 @@ +export { default as TeamsView } from './teams-view'; + diff --git a/src/sections/team/view/team-detail-view.tsx b/src/sections/team/view/team-detail-view.tsx new file mode 100644 index 000000000..f871f6af7 --- /dev/null +++ b/src/sections/team/view/team-detail-view.tsx @@ -0,0 +1,134 @@ +import { useState, useEffect } from 'react'; +import { useParams, useNavigate } from 'react-router-dom'; + +import Box from '@mui/material/Box'; +import Card from '@mui/material/Card'; +import Button from '@mui/material/Button'; +import Container from '@mui/material/Container'; +import Typography from '@mui/material/Typography'; +import Tabs from '@mui/material/Tabs'; +import Tab from '@mui/material/Tab'; + +import { Iconify } from 'src/components/iconify'; + +import { TeamMembers } from '../team-members'; +import { TeamProjects } from '../team-projects'; + +import type { Team } from 'src/types'; + +// ---------------------------------------------------------------------- + +type TabPanelProps = { + children?: React.ReactNode; + index: number; + value: number; +}; + +function TabPanel({ children, value, index }: TabPanelProps) { + return ( + + ); +} + +export default function TeamDetailView() { + const { id } = useParams<{ id: string }>(); + const navigate = useNavigate(); + const [team, setTeam] = useState(null); + const [tabValue, setTabValue] = useState(0); + const [loading, setLoading] = useState(true); + + useEffect(() => { + if (id) { + loadTeam(id); + } + }, [id]); + + const loadTeam = async (teamId: string) => { + try { + setLoading(true); + // TODO: Replace with actual API call + // const response = await api.getTeam(teamId); + // setTeam(response.team); + + // Demo data + setTeam({ + id: teamId, + name: 'Development Team', + description: 'Main development team', + ownerId: '1', + members: [], + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), + }); + } catch (error) { + console.error('Failed to load team:', error); + } finally { + setLoading(false); + } + }; + + if (loading) { + return ( + + + Loading... + + + ); + } + + if (!team) { + return ( + + + Team not found + + + + ); + } + + return ( + + + + + + + + + {team.name} + + {team.description && ( + + {team.description} + + )} + + + + + setTabValue(newValue)}> + + + + + + + + + + + + + + + + + ); +} + diff --git a/src/sections/team/view/teams-view.tsx b/src/sections/team/view/teams-view.tsx new file mode 100644 index 000000000..db78e74b7 --- /dev/null +++ b/src/sections/team/view/teams-view.tsx @@ -0,0 +1,102 @@ +import { useState, useEffect } from 'react'; + +import Box from '@mui/material/Box'; +import Card from '@mui/material/Card'; +import Button from '@mui/material/Button'; +import Container from '@mui/material/Container'; +import Typography from '@mui/material/Typography'; + +import { Iconify } from 'src/components/iconify'; + +import { TeamsList } from '../teams-list'; +import { TeamCreateDialog } from '../team-create-dialog'; + +import type { Team } from 'src/types'; + +// ---------------------------------------------------------------------- + +export default function TeamsView() { + const [teams, setTeams] = useState([]); + const [openCreate, setOpenCreate] = useState(false); + const [loading, setLoading] = useState(true); + + useEffect(() => { + loadTeams(); + }, []); + + const loadTeams = async () => { + try { + setLoading(true); + // TODO: Replace with actual API call + // const response = await api.getTeams(); + // setTeams(response.teams); + + // Demo data + setTeams([ + { + id: '1', + name: 'Development Team', + description: 'Main development team', + ownerId: '1', + members: [], + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), + }, + ]); + } catch (error) { + console.error('Failed to load teams:', error); + } finally { + setLoading(false); + } + }; + + const handleCreateTeam = async (data: { name: string; description?: string }) => { + try { + // TODO: Replace with actual API call + // const response = await api.createTeam(data); + // setTeams((prev) => [response.team, ...prev]); + + const newTeam: Team = { + id: Date.now().toString(), + ...data, + ownerId: '1', + members: [], + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), + }; + setTeams((prev) => [newTeam, ...prev]); + setOpenCreate(false); + } catch (error) { + console.error('Failed to create team:', error); + } + }; + + return ( + + + Teams + + + + + + + + setOpenCreate(false)} onCreate={handleCreateTeam} /> + + ); +} + diff --git a/src/services/api.ts b/src/services/api.ts new file mode 100644 index 000000000..23269f226 --- /dev/null +++ b/src/services/api.ts @@ -0,0 +1,70 @@ +const API_BASE_URL = import.meta.env.VITE_API_URL || 'http://localhost:3001/api'; + +// ---------------------------------------------------------------------- + +async function request(endpoint: string, options?: RequestInit): Promise { + const url = `${API_BASE_URL}${endpoint}`; + const token = localStorage.getItem('token'); + + const response = await fetch(url, { + ...options, + headers: { + 'Content-Type': 'application/json', + ...(token && { Authorization: `Bearer ${token}` }), + ...options?.headers, + }, + }); + + if (!response.ok) { + throw new Error(`API Error: ${response.statusText}`); + } + + return response.json(); +} + +// ---------------------------------------------------------------------- + +export const api = { + // Teams + getTeams: () => request<{ teams: any[] }>('/teams'), + getTeam: (id: string) => request<{ team: any }>(`/teams/${id}`), + createTeam: (data: any) => request<{ team: any }>('/teams', { method: 'POST', body: JSON.stringify(data) }), + updateTeam: (id: string, data: any) => request<{ team: any }>(`/teams/${id}`, { method: 'PUT', body: JSON.stringify(data) }), + deleteTeam: (id: string) => request(`/teams/${id}`, { method: 'DELETE' }), + addTeamMember: (teamId: string, userId: string, role: string) => + request(`/teams/${teamId}/members`, { method: 'POST', body: JSON.stringify({ userId, role }) }), + removeTeamMember: (teamId: string, memberId: string) => + request(`/teams/${teamId}/members/${memberId}`, { method: 'DELETE' }), + + // Projects + getProjects: (teamId?: string) => request<{ projects: any[] }>(teamId ? `/teams/${teamId}/projects` : '/projects'), + getProject: (id: string) => request<{ project: any }>(`/projects/${id}`), + createProject: (data: any) => request<{ project: any }>('/projects', { method: 'POST', body: JSON.stringify(data) }), + updateProject: (id: string, data: any) => request<{ project: any }>(`/projects/${id}`, { method: 'PUT', body: JSON.stringify(data) }), + deleteProject: (id: string) => request(`/projects/${id}`, { method: 'DELETE' }), + addProjectMember: (projectId: string, userId: string, role: string) => + request(`/projects/${projectId}/members`, { method: 'POST', body: JSON.stringify({ userId, role }) }), + + // Tasks + getTasks: (projectId: string) => request<{ tasks: any[] }>(`/projects/${projectId}/tasks`), + createTask: (projectId: string, data: any) => + request<{ task: any }>(`/projects/${projectId}/tasks`, { method: 'POST', body: JSON.stringify(data) }), + updateTask: (projectId: string, taskId: string, data: any) => + request<{ task: any }>(`/projects/${projectId}/tasks/${taskId}`, { method: 'PUT', body: JSON.stringify(data) }), + deleteTask: (projectId: string, taskId: string) => + request(`/projects/${projectId}/tasks/${taskId}`, { method: 'DELETE' }), + + // Chats + getChats: () => request<{ chats: any[] }>('/chats'), + getChat: (id: string) => request<{ chat: any }>(`/chats/${id}`), + getChatMessages: (chatId: string) => request<{ messages: any[] }>(`/chats/${chatId}/messages`), + createPrivateChat: (userId: string) => request<{ chat: any }>('/chats/private', { method: 'POST', body: JSON.stringify({ userId }) }), + createProjectChat: (projectId: string) => + request<{ chat: any }>('/chats/project', { method: 'POST', body: JSON.stringify({ projectId }) }), + + // Notifications + getNotifications: () => request<{ notifications: any[] }>('/notifications'), + markNotificationAsRead: (id: string) => request(`/notifications/${id}/read`, { method: 'PUT' }), + markAllNotificationsAsRead: () => request('/notifications/read-all', { method: 'PUT' }), +}; + diff --git a/src/types/chat.ts b/src/types/chat.ts new file mode 100644 index 000000000..fa011a19d --- /dev/null +++ b/src/types/chat.ts @@ -0,0 +1,35 @@ +import type { User } from './team'; + +export interface ChatMessage { + id: string; + chatId: string; + senderId: string; + sender?: User; + content: string; + type: 'text' | 'file' | 'image' | 'system'; + fileUrl?: string; + createdAt: string; + readBy: string[]; +} + +export interface Chat { + id: string; + type: 'private' | 'project'; + name?: string; + avatar?: string; + participants: ChatParticipant[]; + lastMessage?: ChatMessage; + unreadCount: number; + updatedAt: string; + projectId?: string; +} + +export interface ChatParticipant { + id: string; + chatId: string; + userId: string; + user?: User; + joinedAt: string; + lastReadAt?: string; +} + diff --git a/src/types/index.ts b/src/types/index.ts new file mode 100644 index 000000000..8f32f1f57 --- /dev/null +++ b/src/types/index.ts @@ -0,0 +1,5 @@ +export type { Team, TeamMember, User } from './team'; +export type { Project, ProjectMember, Task } from './project'; +export type { Chat, ChatMessage, ChatParticipant } from './chat'; +export type { Notification } from './notification'; + diff --git a/src/types/notification.ts b/src/types/notification.ts new file mode 100644 index 000000000..7263dd484 --- /dev/null +++ b/src/types/notification.ts @@ -0,0 +1,12 @@ +export interface Notification { + id: string; + userId: string; + type: 'team_invite' | 'project_assigned' | 'task_assigned' | 'message' | 'mention' | 'system'; + title: string; + message: string; + read: boolean; + link?: string; + metadata?: Record; + createdAt: string; +} + diff --git a/src/types/project.ts b/src/types/project.ts new file mode 100644 index 000000000..acc685106 --- /dev/null +++ b/src/types/project.ts @@ -0,0 +1,38 @@ +import type { User } from './team'; + +export interface Project { + id: string; + name: string; + description?: string; + teamId: string; + status: 'planning' | 'active' | 'on-hold' | 'completed' | 'archived'; + priority: 'low' | 'medium' | 'high' | 'urgent'; + startDate?: string; + endDate?: string; + members: ProjectMember[]; + createdAt: string; + updatedAt: string; +} + +export interface ProjectMember { + id: string; + projectId: string; + userId: string; + role: 'manager' | 'developer' | 'designer' | 'viewer'; + user?: User; +} + +export interface Task { + id: string; + projectId: string; + title: string; + description?: string; + status: 'todo' | 'in-progress' | 'review' | 'done'; + priority: 'low' | 'medium' | 'high' | 'urgent'; + assigneeId?: string; + assignee?: User; + dueDate?: string; + createdAt: string; + updatedAt: string; +} + diff --git a/src/types/team.ts b/src/types/team.ts new file mode 100644 index 000000000..9418fbf35 --- /dev/null +++ b/src/types/team.ts @@ -0,0 +1,28 @@ +export interface Team { + id: string; + name: string; + description?: string; + avatar?: string; + ownerId: string; + members: TeamMember[]; + createdAt: string; + updatedAt: string; +} + +export interface TeamMember { + id: string; + userId: string; + teamId: string; + role: 'owner' | 'admin' | 'member'; + joinedAt: string; + user?: User; +} + +export interface User { + id: string; + name: string; + email: string; + avatar?: string; + status?: 'online' | 'offline' | 'away'; +} + diff --git a/src/utils/format-time.ts b/src/utils/format-time.ts index 71f1bc011..e620999c1 100644 --- a/src/utils/format-time.ts +++ b/src/utils/format-time.ts @@ -80,6 +80,19 @@ export function fDate(date: DatePickerFormat, template?: string): string { // ---------------------------------------------------------------------- +/** + * @output 12:00 am + */ +export function fTime(date: DatePickerFormat, template?: string): string { + if (!isValidDate(date)) { + return 'Invalid date'; + } + + return dayjs(date).format(template ?? formatPatterns.time); +} + +// ---------------------------------------------------------------------- + /** * @output a few seconds, 2 years */ diff --git a/yarn.lock b/yarn.lock index 7c1494c63..b635f3b07 100644 --- a/yarn.lock +++ b/yarn.lock @@ -191,10 +191,10 @@ resolved "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.4.0.tgz" integrity sha512-snKqtPW01tN0ui7yu9rGv69aJXr/a/Ywvl11sUjNtEcRc+ng/mQriFL0wLXMef74iHa/EkftbDzU9F8iFbH+zg== -"@esbuild/darwin-arm64@0.25.2": +"@esbuild/win32-x64@0.25.2": version "0.25.2" - resolved "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.2.tgz" - integrity sha512-MpM6LUVTXAzOvN4KbjzU/q5smzryuoNjlriAIx+06RpecwCkL9JpenNzpKd2YMzLJFOdPqBpuub6eVRP5IgiSA== + resolved "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.2.tgz" + integrity sha512-kM3HKb16VIXZyIeVrM1ygYmZBKybX8N4p754bw390wGO3Tf2j4L2/WYL+4suWujpgf6GBYs3jv7TyUivdd05JA== "@eslint-community/eslint-utils@^4.2.0", "@eslint-community/eslint-utils@^4.4.0": version "4.5.1" @@ -466,16 +466,21 @@ resolved "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz" integrity sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A== -"@rollup/rollup-darwin-arm64@4.39.0": +"@rollup/rollup-win32-x64-msvc@4.39.0": version "4.39.0" - resolved "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.39.0.tgz" - integrity sha512-lXQnhpFDOKDXiGxsU9/l8UEGGM65comrQuZ+lDcGUx+9YQ9dKpF3rSEGepyeR5AHZ0b5RgiligsBhWZfSSQh8Q== + resolved "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.39.0.tgz" + integrity sha512-yAkUOkIKZlK5dl7u6dg897doBgLXmUHhIINM2c+sND3DZwnrdQkkSiDh7N75Ll4mM4dxSkYfXqU9fW3lLkMFug== "@rtsao/scc@^1.1.0": version "1.1.0" resolved "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz" integrity sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g== +"@socket.io/component-emitter@~3.1.0": + version "3.1.2" + resolved "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz" + integrity sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA== + "@svgdotjs/svg.draggable.js@^3.0.4": version "3.0.6" resolved "https://registry.npmjs.org/@svgdotjs/svg.draggable.js/-/svg.draggable.js-3.0.6.tgz" @@ -503,10 +508,10 @@ resolved "https://registry.npmjs.org/@svgdotjs/svg.select.js/-/svg.select.js-4.0.2.tgz" integrity sha512-5gWdrvoQX3keo03SCmgaBbD+kFftq0F/f2bzCbNnpkkvW6tk4rl4MakORzFuNjvXPWwB4az9GwuvVxQVnjaK2g== -"@swc/core-darwin-arm64@1.11.16": +"@swc/core-win32-x64-msvc@1.11.16": version "1.11.16" - resolved "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.11.16.tgz" - integrity sha512-l6uWMU+MUdfLHCl3dJgtVEdsUHPskoA4BSu0L1hh9SGBwPZ8xeOz8iLIqZM27lTuXxL4KsYH6GQR/OdQ/vhLtg== + resolved "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.11.16.tgz" + integrity sha512-/ibq/YDc3B5AROkpOKPGxVkSyCKOg+ml8k11RxrW7FAPy6a9y5y9KPcWIqV74Ahq4RuaMNslTQqHWAGSm0xJsQ== "@swc/core@^1.11.11": version "1.11.16" @@ -593,6 +598,11 @@ dependencies: csstype "^3.0.2" +"@types/socket.io-client@^1.4.36": + version "1.4.36" + resolved "https://registry.npmjs.org/@types/socket.io-client/-/socket.io-client-1.4.36.tgz" + integrity sha512-ZJWjtFBeBy1kRSYpVbeGYTElf6BqPQUkXDlHHD4k/42byCN5Rh027f4yARHCink9sKAkbtGZXEAmR0ZCnc2/Ag== + "@typescript-eslint/eslint-plugin@^8.0.0-0 || ^7.0.0 || ^6.0.0 || ^5.0.0", "@typescript-eslint/eslint-plugin@8.29.0": version "8.29.0" resolved "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.29.0.tgz" @@ -674,10 +684,10 @@ "@typescript-eslint/types" "8.29.0" eslint-visitor-keys "^4.2.0" -"@unrs/resolver-binding-darwin-arm64@1.3.3": +"@unrs/resolver-binding-win32-x64-msvc@1.3.3": version "1.3.3" - resolved "https://registry.npmjs.org/@unrs/resolver-binding-darwin-arm64/-/resolver-binding-darwin-arm64-1.3.3.tgz" - integrity sha512-EpRILdWr3/xDa/7MoyfO7JuBIJqpBMphtu4+80BK1bRfFcniVT74h3Z7q1+WOc92FuIAYatB1vn9TJR67sORGw== + resolved "https://registry.npmjs.org/@unrs/resolver-binding-win32-x64-msvc/-/resolver-binding-win32-x64-msvc-1.3.3.tgz" + integrity sha512-GraLbYqOJcmW1qY3osB+2YIiD62nVf2/bVLHZmrb4t/YSUwE03l7TwcDJl08T/Tm3SVhepX8RQkpzWbag/Sb4w== "@vitejs/plugin-react-swc@^3.8.1": version "3.8.1" @@ -1026,6 +1036,20 @@ debug@^4.3.1, debug@^4.3.2, debug@^4.3.4, debug@^4.4.0: dependencies: ms "^2.1.3" +debug@~4.3.1: + version "4.3.7" + resolved "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz" + integrity sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ== + dependencies: + ms "^2.1.3" + +debug@~4.3.2: + version "4.3.7" + resolved "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz" + integrity sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ== + dependencies: + ms "^2.1.3" + deep-is@^0.1.3: version "0.1.4" resolved "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz" @@ -1073,6 +1097,22 @@ dunder-proto@^1.0.0, dunder-proto@^1.0.1: es-errors "^1.3.0" gopd "^1.2.0" +engine.io-client@~6.6.1: + version "6.6.3" + resolved "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.6.3.tgz" + integrity sha512-T0iLjnyNWahNyv/lcjS2y4oE358tVS/SYQNxYXGAJ9/GLgH4VCvOQ/mhTjqU88mLZCQgiG8RIegFHYCdVC+j5w== + dependencies: + "@socket.io/component-emitter" "~3.1.0" + debug "~4.3.1" + engine.io-parser "~5.2.1" + ws "~8.17.1" + xmlhttprequest-ssl "~2.1.1" + +engine.io-parser@~5.2.1: + version "5.2.3" + resolved "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz" + integrity sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q== + error-ex@^1.3.1: version "1.3.2" resolved "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz" @@ -1516,11 +1556,6 @@ for-each@^0.3.3, for-each@^0.3.5: dependencies: is-callable "^1.2.7" -fsevents@~2.3.2, fsevents@~2.3.3: - version "2.3.3" - resolved "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz" - integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw== - function-bind@^1.1.2: version "1.1.2" resolved "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz" @@ -2600,6 +2635,24 @@ simplebar-react@^3.3.0: dependencies: simplebar-core "^1.3.0" +socket.io-client@^4.8.1: + version "4.8.1" + resolved "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.8.1.tgz" + integrity sha512-hJVXfu3E28NmzGk8o1sHhN3om52tRvwYeidbj7xKy2eIIse5IoKX3USlS6Tqt3BHAtflLIkCQBkzVrEEfWUyYQ== + dependencies: + "@socket.io/component-emitter" "~3.1.0" + debug "~4.3.2" + engine.io-client "~6.6.1" + socket.io-parser "~4.2.4" + +socket.io-parser@~4.2.4: + version "4.2.4" + resolved "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz" + integrity sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew== + dependencies: + "@socket.io/component-emitter" "~3.1.0" + debug "~4.3.1" + source-map-js@^1.2.1: version "1.2.1" resolved "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz" @@ -2958,6 +3011,16 @@ word-wrap@^1.2.5: resolved "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz" integrity sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA== +ws@~8.17.1: + version "8.17.1" + resolved "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz" + integrity sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ== + +xmlhttprequest-ssl@~2.1.1: + version "2.1.2" + resolved "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.1.2.tgz" + integrity sha512-TEU+nJVUUnA4CYJFLvK5X9AOeH4KvDvhIfm0vV1GaQRtchnG0hgK5p8hw/xjv8cunWYCsiPCSDzObPyhEwq3KQ== + yaml@^1.10.0: version "1.10.2" resolved "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz"