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"