cnmksjs commited on
Commit
e3eb984
·
verified ·
1 Parent(s): a9120e8

Upload 60 files

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .dockerignore +21 -0
  2. Dockerfile +40 -0
  3. client/.env.example +4 -0
  4. client/index.html +13 -0
  5. client/package.json +57 -0
  6. client/postcss.config.js +6 -0
  7. client/src/App.tsx +89 -0
  8. client/src/components/LoadingSpinner.tsx +24 -0
  9. client/src/components/ProtectedRoute.tsx +31 -0
  10. client/src/components/chat/ChatSidebar.tsx +176 -0
  11. client/src/components/chat/ChatWindow.tsx +255 -0
  12. client/src/components/ui/alert.tsx +59 -0
  13. client/src/components/ui/avatar.tsx +48 -0
  14. client/src/components/ui/badge.tsx +36 -0
  15. client/src/components/ui/button.tsx +56 -0
  16. client/src/components/ui/card.tsx +71 -0
  17. client/src/components/ui/input.tsx +25 -0
  18. client/src/components/ui/label.tsx +24 -0
  19. client/src/components/ui/scroll-area.tsx +46 -0
  20. client/src/components/ui/separator.tsx +29 -0
  21. client/src/components/ui/tabs.tsx +53 -0
  22. client/src/components/ui/textarea.tsx +24 -0
  23. client/src/components/ui/toaster.tsx +135 -0
  24. client/src/index.css +123 -0
  25. client/src/lib/utils.ts +190 -0
  26. client/src/main.tsx +13 -0
  27. client/src/pages/AdminPage.tsx +304 -0
  28. client/src/pages/ChatPage.tsx +59 -0
  29. client/src/pages/LoginPage.tsx +109 -0
  30. client/src/pages/ProfilePage.tsx +173 -0
  31. client/src/pages/RegisterPage.tsx +204 -0
  32. client/src/services/api.ts +182 -0
  33. client/src/services/authService.ts +62 -0
  34. client/src/services/chatService.ts +136 -0
  35. client/src/services/socketService.ts +198 -0
  36. client/src/store/authStore.ts +130 -0
  37. client/src/store/chatStore.ts +285 -0
  38. client/src/vite-env.d.ts +10 -0
  39. client/tailwind.config.js +76 -0
  40. client/tsconfig.json +31 -0
  41. client/tsconfig.node.json +10 -0
  42. client/vite.config.ts +27 -0
  43. package.json +41 -0
  44. server/.env.example +50 -0
  45. server/package.json +70 -0
  46. server/prisma/schema.prisma +209 -0
  47. server/src/config/database.ts +69 -0
  48. server/src/index.ts +157 -0
  49. server/src/middleware/auth.ts +158 -0
  50. server/src/middleware/errorHandler.ts +104 -0
.dockerignore ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Ignore node modules and build output
2
+ node_modules
3
+ **/node_modules
4
+
5
+ # Logs
6
+ npm-debug.log
7
+ *.log
8
+
9
+ # Git
10
+ .git
11
+ .gitignore
12
+
13
+ # dotenv files
14
+ .env
15
+ *.env
16
+
17
+ # IDEs
18
+ .vscode
19
+
20
+ # local uploads
21
+ server/uploads
Dockerfile ADDED
@@ -0,0 +1,40 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Multi-stage build for ChatApp
2
+
3
+ # ----- Build Stage -----
4
+ FROM node:18 AS builder
5
+ WORKDIR /app
6
+
7
+ # Install root dependencies and workspaces
8
+ COPY package.json package-lock.json* ./
9
+ COPY client/package.json ./client/package.json
10
+ COPY server/package.json ./server/package.json
11
+ RUN npm install
12
+
13
+ # Copy source code
14
+ COPY . .
15
+
16
+ # Build client and server
17
+ RUN npm run build
18
+
19
+ # Remove development dependencies to reduce size
20
+ RUN npm prune --omit=dev --workspaces
21
+
22
+ # ----- Production Stage -----
23
+ FROM node:18-alpine AS runner
24
+ WORKDIR /app
25
+
26
+ # Copy built application from builder
27
+ COPY --from=builder /app/package.json ./
28
+ COPY --from=builder /app/client/package.json ./client/package.json
29
+ COPY --from=builder /app/server/package.json ./server/package.json
30
+ COPY --from=builder /app/client/dist ./client/dist
31
+ COPY --from=builder /app/server/dist ./server/dist
32
+ COPY --from=builder /app/client/node_modules ./client/node_modules
33
+ COPY --from=builder /app/server/node_modules ./server/node_modules
34
+
35
+ ENV NODE_ENV=production
36
+ ENV PORT=3001
37
+
38
+ EXPOSE 3001
39
+
40
+ CMD ["node", "server/dist/index.js"]
client/.env.example ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ VITE_API_URL=http://localhost:3001
2
+ VITE_SUPABASE_URL=your_supabase_url
3
+ VITE_SUPABASE_ANON_KEY=your_supabase_anon_key
4
+ VITE_SOCKET_URL=http://localhost:3001
client/index.html ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!doctype html>
2
+ <html lang="zh-CN">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <link rel="icon" type="image/svg+xml" href="/vite.svg" />
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
+ <title>ChatApp - 现代化聊天应用</title>
8
+ </head>
9
+ <body>
10
+ <div id="root"></div>
11
+ <script type="module" src="/src/main.tsx"></script>
12
+ </body>
13
+ </html>
client/package.json ADDED
@@ -0,0 +1,57 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "chat-app-client",
3
+ "private": true,
4
+ "version": "0.0.0",
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "vite",
8
+ "build": "tsc && vite build",
9
+ "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
10
+ "preview": "vite preview",
11
+ "test": "vitest"
12
+ },
13
+ "dependencies": {
14
+ "react": "^18.2.0",
15
+ "react-dom": "^18.2.0",
16
+ "react-router-dom": "^6.20.1",
17
+ "socket.io-client": "^4.7.4",
18
+ "zustand": "^4.4.7",
19
+ "@supabase/supabase-js": "^2.38.4",
20
+ "axios": "^1.6.2",
21
+ "lucide-react": "^0.294.0",
22
+ "clsx": "^2.0.0",
23
+ "tailwind-merge": "^2.0.0",
24
+ "@radix-ui/react-avatar": "^1.1.10",
25
+ "@radix-ui/react-dialog": "^1.1.4",
26
+ "@radix-ui/react-dropdown-menu": "^2.1.4",
27
+ "@radix-ui/react-label": "^2.1.1",
28
+ "@radix-ui/react-slot": "^1.2.3",
29
+ "@radix-ui/react-scroll-area": "^1.2.2",
30
+ "@radix-ui/react-separator": "^1.1.1",
31
+ "@radix-ui/react-tabs": "^1.1.3",
32
+ "@radix-ui/react-toast": "^1.2.4",
33
+ "class-variance-authority": "^0.7.0",
34
+ "react-hook-form": "^7.48.2",
35
+ "@hookform/resolvers": "^3.3.2",
36
+ "zod": "^3.22.4",
37
+ "date-fns": "^2.30.0",
38
+ "emoji-picker-react": "^4.5.16"
39
+ },
40
+ "devDependencies": {
41
+ "@types/react": "^18.2.37",
42
+ "@types/react-dom": "^18.2.15",
43
+ "@typescript-eslint/eslint-plugin": "^6.10.0",
44
+ "@typescript-eslint/parser": "^6.10.0",
45
+ "@vitejs/plugin-react": "^4.1.1",
46
+ "autoprefixer": "^10.4.16",
47
+ "eslint": "^8.53.0",
48
+ "eslint-plugin-react-hooks": "^4.6.0",
49
+ "eslint-plugin-react-refresh": "^0.4.4",
50
+ "postcss": "^8.4.31",
51
+ "tailwindcss": "^3.3.5",
52
+ "tailwindcss-animate": "^1.0.7",
53
+ "typescript": "^5.2.2",
54
+ "vite": "^5.0.0",
55
+ "vitest": "^0.34.6"
56
+ }
57
+ }
client/postcss.config.js ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ export default {
2
+ plugins: {
3
+ tailwindcss: {},
4
+ autoprefixer: {},
5
+ },
6
+ }
client/src/App.tsx ADDED
@@ -0,0 +1,89 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Routes, Route, Navigate } from 'react-router-dom'
2
+ import { useEffect } from 'react'
3
+ import { useAuthStore } from './store/authStore'
4
+ import { Toaster } from './components/ui/toaster'
5
+
6
+ // Pages
7
+ import LoginPage from './pages/LoginPage'
8
+ import RegisterPage from './pages/RegisterPage'
9
+ import ChatPage from './pages/ChatPage'
10
+ import AdminPage from './pages/AdminPage'
11
+ import ProfilePage from './pages/ProfilePage'
12
+
13
+ // Components
14
+ import ProtectedRoute from './components/ProtectedRoute'
15
+ import LoadingSpinner from './components/LoadingSpinner'
16
+
17
+ function App() {
18
+ const { user, loading, checkAuth } = useAuthStore()
19
+
20
+ useEffect(() => {
21
+ checkAuth()
22
+ }, [checkAuth])
23
+
24
+ if (loading) {
25
+ return (
26
+ <div className="min-h-screen flex items-center justify-center">
27
+ <LoadingSpinner size="lg" />
28
+ </div>
29
+ )
30
+ }
31
+
32
+ return (
33
+ <div className="min-h-screen bg-background">
34
+ <Routes>
35
+ {/* Public routes */}
36
+ <Route
37
+ path="/login"
38
+ element={user ? <Navigate to="/chat" replace /> : <LoginPage />}
39
+ />
40
+ <Route
41
+ path="/register"
42
+ element={user ? <Navigate to="/chat" replace /> : <RegisterPage />}
43
+ />
44
+
45
+ {/* Protected routes */}
46
+ <Route
47
+ path="/chat/*"
48
+ element={
49
+ <ProtectedRoute>
50
+ <ChatPage />
51
+ </ProtectedRoute>
52
+ }
53
+ />
54
+ <Route
55
+ path="/profile"
56
+ element={
57
+ <ProtectedRoute>
58
+ <ProfilePage />
59
+ </ProtectedRoute>
60
+ }
61
+ />
62
+ <Route
63
+ path="/admin/*"
64
+ element={
65
+ <ProtectedRoute requireAdmin>
66
+ <AdminPage />
67
+ </ProtectedRoute>
68
+ }
69
+ />
70
+
71
+ {/* Default redirect */}
72
+ <Route
73
+ path="/"
74
+ element={<Navigate to={user ? "/chat" : "/login"} replace />}
75
+ />
76
+
77
+ {/* 404 fallback */}
78
+ <Route
79
+ path="*"
80
+ element={<Navigate to={user ? "/chat" : "/login"} replace />}
81
+ />
82
+ </Routes>
83
+
84
+ <Toaster />
85
+ </div>
86
+ )
87
+ }
88
+
89
+ export default App
client/src/components/LoadingSpinner.tsx ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { cn } from '@/lib/utils'
2
+
3
+ interface LoadingSpinnerProps {
4
+ size?: 'sm' | 'md' | 'lg'
5
+ className?: string
6
+ }
7
+
8
+ export default function LoadingSpinner({ size = 'md', className }: LoadingSpinnerProps) {
9
+ const sizeClasses = {
10
+ sm: 'w-4 h-4',
11
+ md: 'w-6 h-6',
12
+ lg: 'w-8 h-8'
13
+ }
14
+
15
+ return (
16
+ <div
17
+ className={cn(
18
+ 'animate-spin rounded-full border-2 border-gray-300 border-t-blue-600',
19
+ sizeClasses[size],
20
+ className
21
+ )}
22
+ />
23
+ )
24
+ }
client/src/components/ProtectedRoute.tsx ADDED
@@ -0,0 +1,31 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { ReactNode } from 'react'
2
+ import { Navigate } from 'react-router-dom'
3
+ import { useAuthStore } from '@/store/authStore'
4
+ import LoadingSpinner from './LoadingSpinner'
5
+
6
+ interface ProtectedRouteProps {
7
+ children: ReactNode
8
+ requireAdmin?: boolean
9
+ }
10
+
11
+ export default function ProtectedRoute({ children, requireAdmin = false }: ProtectedRouteProps) {
12
+ const { user, loading } = useAuthStore()
13
+
14
+ if (loading) {
15
+ return (
16
+ <div className="min-h-screen flex items-center justify-center">
17
+ <LoadingSpinner size="lg" />
18
+ </div>
19
+ )
20
+ }
21
+
22
+ if (!user) {
23
+ return <Navigate to="/login" replace />
24
+ }
25
+
26
+ if (requireAdmin && !user.isAdmin) {
27
+ return <Navigate to="/chat" replace />
28
+ }
29
+
30
+ return <>{children}</>
31
+ }
client/src/components/chat/ChatSidebar.tsx ADDED
@@ -0,0 +1,176 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useState } from 'react'
2
+ import { useChatStore } from '@/store/chatStore'
3
+ import { useAuthStore } from '@/store/authStore'
4
+ import { Button } from '@/components/ui/button'
5
+ import { Input } from '@/components/ui/input'
6
+ import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
7
+ import { Badge } from '@/components/ui/badge'
8
+ import { ScrollArea } from '@/components/ui/scroll-area'
9
+ // import { Separator } from '@/components/ui/separator'
10
+ import {
11
+ Search,
12
+ Plus,
13
+ Settings,
14
+ MessageSquare,
15
+ Users,
16
+ // MoreVertical
17
+ } from 'lucide-react'
18
+ import { formatTime, getInitials } from '@/lib/utils'
19
+
20
+ export default function ChatSidebar() {
21
+ const { user } = useAuthStore()
22
+ const { chats, currentChat, selectChat, loading } = useChatStore()
23
+ const [searchQuery, setSearchQuery] = useState('')
24
+
25
+ const filteredChats = chats.filter(chat => {
26
+ if (!searchQuery) return true
27
+
28
+ const searchLower = searchQuery.toLowerCase()
29
+
30
+ // Search by chat name
31
+ if (chat.name?.toLowerCase().includes(searchLower)) return true
32
+
33
+ // Search by participant names (for direct chats)
34
+ if (chat.type === 'direct') {
35
+ const otherParticipant = chat.participants.find(p => p.userId !== user?.id)
36
+ if (otherParticipant?.user.displayName?.toLowerCase().includes(searchLower)) return true
37
+ }
38
+
39
+ return false
40
+ })
41
+
42
+ const getChatDisplayName = (chat: any) => {
43
+ if (chat.type === 'group') {
44
+ return chat.name || 'Unnamed Group'
45
+ } else {
46
+ const otherParticipant = chat.participants.find((p: any) => p.userId !== user?.id)
47
+ return otherParticipant?.user.displayName || 'Unknown User'
48
+ }
49
+ }
50
+
51
+ const getChatAvatar = (chat: any) => {
52
+ if (chat.type === 'group') {
53
+ return chat.avatar
54
+ } else {
55
+ const otherParticipant = chat.participants.find((p: any) => p.userId !== user?.id)
56
+ return otherParticipant?.user.avatar
57
+ }
58
+ }
59
+
60
+ return (
61
+ <div className="flex flex-col h-full bg-background">
62
+ {/* Header */}
63
+ <div className="p-4 border-b border-border">
64
+ <div className="flex items-center justify-between mb-4">
65
+ <h1 className="text-xl font-semibold">Chats</h1>
66
+ <div className="flex items-center space-x-2">
67
+ <Button variant="ghost" size="icon">
68
+ <Plus className="w-4 h-4" />
69
+ </Button>
70
+ <Button variant="ghost" size="icon">
71
+ <Settings className="w-4 h-4" />
72
+ </Button>
73
+ </div>
74
+ </div>
75
+
76
+ {/* Search */}
77
+ <div className="relative">
78
+ <Search className="absolute left-3 top-1/2 transform -translate-y-1/2 w-4 h-4 text-muted-foreground" />
79
+ <Input
80
+ placeholder="Search chats..."
81
+ value={searchQuery}
82
+ onChange={(e) => setSearchQuery(e.target.value)}
83
+ className="pl-10"
84
+ />
85
+ </div>
86
+ </div>
87
+
88
+ {/* Chat List */}
89
+ <ScrollArea className="flex-1">
90
+ <div className="p-2">
91
+ {loading ? (
92
+ <div className="flex items-center justify-center py-8">
93
+ <div className="text-muted-foreground">Loading chats...</div>
94
+ </div>
95
+ ) : filteredChats.length === 0 ? (
96
+ <div className="flex flex-col items-center justify-center py-8 text-center">
97
+ <MessageSquare className="w-12 h-12 text-muted-foreground mb-4" />
98
+ <h3 className="font-medium mb-2">No chats found</h3>
99
+ <p className="text-sm text-muted-foreground mb-4">
100
+ {searchQuery ? 'Try a different search term' : 'Start a new conversation'}
101
+ </p>
102
+ <Button size="sm">
103
+ <Plus className="w-4 h-4 mr-2" />
104
+ New Chat
105
+ </Button>
106
+ </div>
107
+ ) : (
108
+ filteredChats.map((chat) => (
109
+ <div
110
+ key={chat.id}
111
+ onClick={() => selectChat(chat.id)}
112
+ className={`
113
+ flex items-center p-3 rounded-lg cursor-pointer transition-colors
114
+ hover:bg-accent hover:text-accent-foreground
115
+ ${currentChat?.id === chat.id ? 'bg-accent text-accent-foreground' : ''}
116
+ `}
117
+ >
118
+ <div className="relative">
119
+ <Avatar className="w-12 h-12">
120
+ <AvatarImage src={getChatAvatar(chat)} />
121
+ <AvatarFallback>
122
+ {chat.type === 'group' ? (
123
+ <Users className="w-6 h-6" />
124
+ ) : (
125
+ getInitials(getChatDisplayName(chat))
126
+ )}
127
+ </AvatarFallback>
128
+ </Avatar>
129
+ {/* Online indicator for direct chats */}
130
+ {chat.type === 'direct' && (
131
+ <div className="absolute bottom-0 right-0 w-3 h-3 bg-green-500 border-2 border-background rounded-full" />
132
+ )}
133
+ </div>
134
+
135
+ <div className="flex-1 ml-3 min-w-0">
136
+ <div className="flex items-center justify-between">
137
+ <h3 className="font-medium truncate">
138
+ {getChatDisplayName(chat)}
139
+ </h3>
140
+ <div className="flex items-center space-x-2">
141
+ {chat.lastMessage && (
142
+ <span className="text-xs text-muted-foreground">
143
+ {formatTime(chat.lastMessage.createdAt)}
144
+ </span>
145
+ )}
146
+ {chat.unreadCount > 0 && (
147
+ <Badge variant="default" className="text-xs">
148
+ {chat.unreadCount > 99 ? '99+' : chat.unreadCount}
149
+ </Badge>
150
+ )}
151
+ </div>
152
+ </div>
153
+
154
+ {chat.lastMessage && (
155
+ <p className="text-sm text-muted-foreground truncate mt-1">
156
+ {chat.lastMessage.type === 'text'
157
+ ? chat.lastMessage.content
158
+ : `📎 ${chat.lastMessage.type}`
159
+ }
160
+ </p>
161
+ )}
162
+
163
+ {chat.type === 'group' && (
164
+ <p className="text-xs text-muted-foreground mt-1">
165
+ {chat.participants.length} members
166
+ </p>
167
+ )}
168
+ </div>
169
+ </div>
170
+ ))
171
+ )}
172
+ </div>
173
+ </ScrollArea>
174
+ </div>
175
+ )
176
+ }
client/src/components/chat/ChatWindow.tsx ADDED
@@ -0,0 +1,255 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useState, useRef, useEffect } from 'react'
2
+ import { useChatStore } from '@/store/chatStore'
3
+ import { useAuthStore } from '@/store/authStore'
4
+ import { Button } from '@/components/ui/button'
5
+ import { Input } from '@/components/ui/input'
6
+ import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
7
+ import { ScrollArea } from '@/components/ui/scroll-area'
8
+ // import { Separator } from '@/components/ui/separator'
9
+ import {
10
+ Send,
11
+ Paperclip,
12
+ Smile,
13
+ Phone,
14
+ Video,
15
+ MoreVertical,
16
+ Users,
17
+ Info
18
+ } from 'lucide-react'
19
+ import { formatTime, getInitials } from '@/lib/utils'
20
+
21
+ export default function ChatWindow() {
22
+ const { user } = useAuthStore()
23
+ const { currentChat, messages, sendMessage, loading } = useChatStore()
24
+ const [messageText, setMessageText] = useState('')
25
+ // const [isTyping, setIsTyping] = useState(false)
26
+ const messagesEndRef = useRef<HTMLDivElement>(null)
27
+ const inputRef = useRef<HTMLInputElement>(null)
28
+
29
+ const chatMessages = currentChat ? messages[currentChat.id] || [] : []
30
+
31
+ useEffect(() => {
32
+ scrollToBottom()
33
+ }, [chatMessages])
34
+
35
+ const scrollToBottom = () => {
36
+ messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' })
37
+ }
38
+
39
+ const handleSendMessage = async () => {
40
+ if (!messageText.trim() || !currentChat) return
41
+
42
+ try {
43
+ await sendMessage(currentChat.id, messageText.trim())
44
+ setMessageText('')
45
+ inputRef.current?.focus()
46
+ } catch (error) {
47
+ console.error('Failed to send message:', error)
48
+ }
49
+ }
50
+
51
+ const handleKeyPress = (e: React.KeyboardEvent) => {
52
+ if (e.key === 'Enter' && !e.shiftKey) {
53
+ e.preventDefault()
54
+ handleSendMessage()
55
+ }
56
+ }
57
+
58
+ const getChatDisplayName = () => {
59
+ if (!currentChat) return ''
60
+
61
+ if (currentChat.type === 'group') {
62
+ return currentChat.name || 'Unnamed Group'
63
+ } else {
64
+ const otherParticipant = currentChat.participants.find(p => p.userId !== user?.id)
65
+ return otherParticipant?.user.displayName || 'Unknown User'
66
+ }
67
+ }
68
+
69
+ const getChatAvatar = () => {
70
+ if (!currentChat) return undefined
71
+
72
+ if (currentChat.type === 'group') {
73
+ return currentChat.avatar
74
+ } else {
75
+ const otherParticipant = currentChat.participants.find(p => p.userId !== user?.id)
76
+ return otherParticipant?.user.avatar
77
+ }
78
+ }
79
+
80
+ const isMessageFromCurrentUser = (message: any) => {
81
+ return message.senderId === user?.id
82
+ }
83
+
84
+ if (!currentChat) {
85
+ return (
86
+ <div className="flex-1 flex items-center justify-center">
87
+ <div className="text-center">
88
+ <div className="text-6xl mb-4">💬</div>
89
+ <h2 className="text-2xl font-semibold mb-2">Welcome to ChatApp</h2>
90
+ <p className="text-muted-foreground">
91
+ Select a chat to start messaging
92
+ </p>
93
+ </div>
94
+ </div>
95
+ )
96
+ }
97
+
98
+ return (
99
+ <div className="flex flex-col h-full">
100
+ {/* Header */}
101
+ <div className="flex items-center justify-between p-4 border-b border-border">
102
+ <div className="flex items-center space-x-3">
103
+ <Avatar className="w-10 h-10">
104
+ <AvatarImage src={getChatAvatar()} />
105
+ <AvatarFallback>
106
+ {currentChat.type === 'group' ? (
107
+ <Users className="w-5 h-5" />
108
+ ) : (
109
+ getInitials(getChatDisplayName())
110
+ )}
111
+ </AvatarFallback>
112
+ </Avatar>
113
+
114
+ <div>
115
+ <h2 className="font-semibold">{getChatDisplayName()}</h2>
116
+ <p className="text-sm text-muted-foreground">
117
+ {currentChat.type === 'group'
118
+ ? `${currentChat.participants.length} members`
119
+ : 'Online'
120
+ }
121
+ </p>
122
+ </div>
123
+ </div>
124
+
125
+ <div className="flex items-center space-x-2">
126
+ <Button variant="ghost" size="icon">
127
+ <Phone className="w-4 h-4" />
128
+ </Button>
129
+ <Button variant="ghost" size="icon">
130
+ <Video className="w-4 h-4" />
131
+ </Button>
132
+ <Button variant="ghost" size="icon">
133
+ <Info className="w-4 h-4" />
134
+ </Button>
135
+ <Button variant="ghost" size="icon">
136
+ <MoreVertical className="w-4 h-4" />
137
+ </Button>
138
+ </div>
139
+ </div>
140
+
141
+ {/* Messages */}
142
+ <ScrollArea className="flex-1 p-4">
143
+ <div className="space-y-4">
144
+ {loading ? (
145
+ <div className="flex items-center justify-center py-8">
146
+ <div className="text-muted-foreground">Loading messages...</div>
147
+ </div>
148
+ ) : chatMessages.length === 0 ? (
149
+ <div className="flex items-center justify-center py-8">
150
+ <div className="text-center">
151
+ <div className="text-4xl mb-4">👋</div>
152
+ <p className="text-muted-foreground">
153
+ No messages yet. Start the conversation!
154
+ </p>
155
+ </div>
156
+ </div>
157
+ ) : (
158
+ chatMessages.map((message, index) => {
159
+ const isFromCurrentUser = isMessageFromCurrentUser(message)
160
+ const showAvatar = !isFromCurrentUser && (
161
+ index === 0 ||
162
+ chatMessages[index - 1]?.senderId !== message.senderId
163
+ )
164
+
165
+ return (
166
+ <div
167
+ key={message.id}
168
+ className={`flex ${isFromCurrentUser ? 'justify-end' : 'justify-start'}`}
169
+ >
170
+ <div className={`flex max-w-[70%] ${isFromCurrentUser ? 'flex-row-reverse' : 'flex-row'}`}>
171
+ {showAvatar && !isFromCurrentUser && (
172
+ <Avatar className="w-8 h-8 mr-2">
173
+ <AvatarImage src={message.sender?.avatar} />
174
+ <AvatarFallback>
175
+ {getInitials(message.sender?.displayName || 'U')}
176
+ </AvatarFallback>
177
+ </Avatar>
178
+ )}
179
+
180
+ <div className={`${!showAvatar && !isFromCurrentUser ? 'ml-10' : ''}`}>
181
+ <div
182
+ className={`
183
+ px-4 py-2 rounded-lg
184
+ ${isFromCurrentUser
185
+ ? 'bg-primary text-primary-foreground'
186
+ : 'bg-muted'
187
+ }
188
+ `}
189
+ >
190
+ {!isFromCurrentUser && showAvatar && currentChat.type === 'group' && (
191
+ <p className="text-xs font-medium mb-1 opacity-70">
192
+ {message.sender?.displayName}
193
+ </p>
194
+ )}
195
+
196
+ <p className="text-sm">{message.content}</p>
197
+
198
+ <div className={`flex items-center justify-end mt-1 space-x-1`}>
199
+ <span className="text-xs opacity-70">
200
+ {formatTime(message.createdAt)}
201
+ </span>
202
+ {isFromCurrentUser && (
203
+ <span className="text-xs opacity-70">
204
+ {/* message delivery status could be rendered here */}
205
+ </span>
206
+ )}
207
+ </div>
208
+ </div>
209
+ </div>
210
+ </div>
211
+ </div>
212
+ )
213
+ })
214
+ )}
215
+ <div ref={messagesEndRef} />
216
+ </div>
217
+ </ScrollArea>
218
+
219
+ {/* Message Input */}
220
+ <div className="p-4 border-t border-border">
221
+ <div className="flex items-center space-x-2">
222
+ <Button variant="ghost" size="icon">
223
+ <Paperclip className="w-4 h-4" />
224
+ </Button>
225
+
226
+ <div className="flex-1 relative">
227
+ <Input
228
+ ref={inputRef}
229
+ placeholder="Type a message..."
230
+ value={messageText}
231
+ onChange={(e) => setMessageText(e.target.value)}
232
+ onKeyPress={handleKeyPress}
233
+ className="pr-10"
234
+ />
235
+ <Button
236
+ variant="ghost"
237
+ size="icon"
238
+ className="absolute right-1 top-1/2 transform -translate-y-1/2"
239
+ >
240
+ <Smile className="w-4 h-4" />
241
+ </Button>
242
+ </div>
243
+
244
+ <Button
245
+ onClick={handleSendMessage}
246
+ disabled={!messageText.trim()}
247
+ size="icon"
248
+ >
249
+ <Send className="w-4 h-4" />
250
+ </Button>
251
+ </div>
252
+ </div>
253
+ </div>
254
+ )
255
+ }
client/src/components/ui/alert.tsx ADDED
@@ -0,0 +1,59 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from "react"
2
+ import { cva, type VariantProps } from "class-variance-authority"
3
+
4
+ import { cn } from "@/lib/utils"
5
+
6
+ const alertVariants = cva(
7
+ "relative w-full rounded-lg border p-4 [&>svg~*]:pl-7 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground",
8
+ {
9
+ variants: {
10
+ variant: {
11
+ default: "bg-background text-foreground",
12
+ destructive:
13
+ "border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive",
14
+ },
15
+ },
16
+ defaultVariants: {
17
+ variant: "default",
18
+ },
19
+ }
20
+ )
21
+
22
+ const Alert = React.forwardRef<
23
+ HTMLDivElement,
24
+ React.HTMLAttributes<HTMLDivElement> & VariantProps<typeof alertVariants>
25
+ >(({ className, variant, ...props }, ref) => (
26
+ <div
27
+ ref={ref}
28
+ role="alert"
29
+ className={cn(alertVariants({ variant }), className)}
30
+ {...props}
31
+ />
32
+ ))
33
+ Alert.displayName = "Alert"
34
+
35
+ const AlertTitle = React.forwardRef<
36
+ HTMLParagraphElement,
37
+ React.HTMLAttributes<HTMLHeadingElement>
38
+ >(({ className, ...props }, ref) => (
39
+ <h5
40
+ ref={ref}
41
+ className={cn("mb-1 font-medium leading-none tracking-tight", className)}
42
+ {...props}
43
+ />
44
+ ))
45
+ AlertTitle.displayName = "AlertTitle"
46
+
47
+ const AlertDescription = React.forwardRef<
48
+ HTMLParagraphElement,
49
+ React.HTMLAttributes<HTMLParagraphElement>
50
+ >(({ className, ...props }, ref) => (
51
+ <div
52
+ ref={ref}
53
+ className={cn("text-sm [&_p]:leading-relaxed", className)}
54
+ {...props}
55
+ />
56
+ ))
57
+ AlertDescription.displayName = "AlertDescription"
58
+
59
+ export { Alert, AlertTitle, AlertDescription }
client/src/components/ui/avatar.tsx ADDED
@@ -0,0 +1,48 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from "react"
2
+ import * as AvatarPrimitive from "@radix-ui/react-avatar"
3
+
4
+ import { cn } from "@/lib/utils"
5
+
6
+ const Avatar = React.forwardRef<
7
+ React.ElementRef<typeof AvatarPrimitive.Root>,
8
+ React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Root>
9
+ >(({ className, ...props }, ref) => (
10
+ <AvatarPrimitive.Root
11
+ ref={ref}
12
+ className={cn(
13
+ "relative flex h-10 w-10 shrink-0 overflow-hidden rounded-full",
14
+ className
15
+ )}
16
+ {...props}
17
+ />
18
+ ))
19
+ Avatar.displayName = AvatarPrimitive.Root.displayName
20
+
21
+ const AvatarImage = React.forwardRef<
22
+ React.ElementRef<typeof AvatarPrimitive.Image>,
23
+ React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Image>
24
+ >(({ className, ...props }, ref) => (
25
+ <AvatarPrimitive.Image
26
+ ref={ref}
27
+ className={cn("aspect-square h-full w-full", className)}
28
+ {...props}
29
+ />
30
+ ))
31
+ AvatarImage.displayName = AvatarPrimitive.Image.displayName
32
+
33
+ const AvatarFallback = React.forwardRef<
34
+ React.ElementRef<typeof AvatarPrimitive.Fallback>,
35
+ React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Fallback>
36
+ >(({ className, ...props }, ref) => (
37
+ <AvatarPrimitive.Fallback
38
+ ref={ref}
39
+ className={cn(
40
+ "flex h-full w-full items-center justify-center rounded-full bg-muted",
41
+ className
42
+ )}
43
+ {...props}
44
+ />
45
+ ))
46
+ AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName
47
+
48
+ export { Avatar, AvatarImage, AvatarFallback }
client/src/components/ui/badge.tsx ADDED
@@ -0,0 +1,36 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from "react"
2
+ import { cva, type VariantProps } from "class-variance-authority"
3
+
4
+ import { cn } from "@/lib/utils"
5
+
6
+ const badgeVariants = cva(
7
+ "inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
8
+ {
9
+ variants: {
10
+ variant: {
11
+ default:
12
+ "border-transparent bg-primary text-primary-foreground hover:bg-primary/80",
13
+ secondary:
14
+ "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
15
+ destructive:
16
+ "border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80",
17
+ outline: "text-foreground",
18
+ },
19
+ },
20
+ defaultVariants: {
21
+ variant: "default",
22
+ },
23
+ }
24
+ )
25
+
26
+ export interface BadgeProps
27
+ extends React.HTMLAttributes<HTMLDivElement>,
28
+ VariantProps<typeof badgeVariants> {}
29
+
30
+ function Badge({ className, variant, ...props }: BadgeProps) {
31
+ return (
32
+ <div className={cn(badgeVariants({ variant }), className)} {...props} />
33
+ )
34
+ }
35
+
36
+ export { Badge, badgeVariants }
client/src/components/ui/button.tsx ADDED
@@ -0,0 +1,56 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from "react"
2
+ import { Slot } from "@radix-ui/react-slot"
3
+ import { cva, type VariantProps } from "class-variance-authority"
4
+
5
+ import { cn } from "@/lib/utils"
6
+
7
+ const buttonVariants = cva(
8
+ "inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
9
+ {
10
+ variants: {
11
+ variant: {
12
+ default: "bg-primary text-primary-foreground hover:bg-primary/90",
13
+ destructive:
14
+ "bg-destructive text-destructive-foreground hover:bg-destructive/90",
15
+ outline:
16
+ "border border-input bg-background hover:bg-accent hover:text-accent-foreground",
17
+ secondary:
18
+ "bg-secondary text-secondary-foreground hover:bg-secondary/80",
19
+ ghost: "hover:bg-accent hover:text-accent-foreground",
20
+ link: "text-primary underline-offset-4 hover:underline",
21
+ },
22
+ size: {
23
+ default: "h-10 px-4 py-2",
24
+ sm: "h-9 rounded-md px-3",
25
+ lg: "h-11 rounded-md px-8",
26
+ icon: "h-10 w-10",
27
+ },
28
+ },
29
+ defaultVariants: {
30
+ variant: "default",
31
+ size: "default",
32
+ },
33
+ }
34
+ )
35
+
36
+ export interface ButtonProps
37
+ extends React.ButtonHTMLAttributes<HTMLButtonElement>,
38
+ VariantProps<typeof buttonVariants> {
39
+ asChild?: boolean
40
+ }
41
+
42
+ const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
43
+ ({ className, variant, size, asChild = false, ...props }, ref) => {
44
+ const Comp = asChild ? Slot : "button"
45
+ return (
46
+ <Comp
47
+ className={cn(buttonVariants({ variant, size, className }))}
48
+ ref={ref}
49
+ {...props}
50
+ />
51
+ )
52
+ }
53
+ )
54
+ Button.displayName = "Button"
55
+
56
+ export { Button, buttonVariants }
client/src/components/ui/card.tsx ADDED
@@ -0,0 +1,71 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from "react"
2
+
3
+ import { cn } from "@/lib/utils"
4
+
5
+ const Card = React.forwardRef<
6
+ HTMLDivElement,
7
+ React.HTMLAttributes<HTMLDivElement>
8
+ >(({ className, ...props }, ref) => (
9
+ <div
10
+ ref={ref}
11
+ className={cn(
12
+ "rounded-lg border bg-card text-card-foreground shadow-sm",
13
+ className
14
+ )}
15
+ {...props}
16
+ />
17
+ ))
18
+ Card.displayName = "Card"
19
+
20
+ const CardHeader = React.forwardRef<
21
+ HTMLDivElement,
22
+ React.HTMLAttributes<HTMLDivElement>
23
+ >(({ className, ...props }, ref) => (
24
+ <div ref={ref} className={cn("flex flex-col space-y-1.5 p-6", className)} {...props} />
25
+ ))
26
+ CardHeader.displayName = "CardHeader"
27
+
28
+ const CardTitle = React.forwardRef<
29
+ HTMLParagraphElement,
30
+ React.HTMLAttributes<HTMLHeadingElement>
31
+ >(({ className, ...props }, ref) => (
32
+ <h3
33
+ ref={ref}
34
+ className={cn(
35
+ "text-2xl font-semibold leading-none tracking-tight",
36
+ className
37
+ )}
38
+ {...props}
39
+ />
40
+ ))
41
+ CardTitle.displayName = "CardTitle"
42
+
43
+ const CardDescription = React.forwardRef<
44
+ HTMLParagraphElement,
45
+ React.HTMLAttributes<HTMLParagraphElement>
46
+ >(({ className, ...props }, ref) => (
47
+ <p
48
+ ref={ref}
49
+ className={cn("text-sm text-muted-foreground", className)}
50
+ {...props}
51
+ />
52
+ ))
53
+ CardDescription.displayName = "CardDescription"
54
+
55
+ const CardContent = React.forwardRef<
56
+ HTMLDivElement,
57
+ React.HTMLAttributes<HTMLDivElement>
58
+ >(({ className, ...props }, ref) => (
59
+ <div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
60
+ ))
61
+ CardContent.displayName = "CardContent"
62
+
63
+ const CardFooter = React.forwardRef<
64
+ HTMLDivElement,
65
+ React.HTMLAttributes<HTMLDivElement>
66
+ >(({ className, ...props }, ref) => (
67
+ <div ref={ref} className={cn("flex items-center p-6 pt-0", className)} {...props} />
68
+ ))
69
+ CardFooter.displayName = "CardFooter"
70
+
71
+ export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }
client/src/components/ui/input.tsx ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from "react"
2
+
3
+ import { cn } from "@/lib/utils"
4
+
5
+ export interface InputProps
6
+ extends React.InputHTMLAttributes<HTMLInputElement> {}
7
+
8
+ const Input = React.forwardRef<HTMLInputElement, InputProps>(
9
+ ({ className, type, ...props }, ref) => {
10
+ return (
11
+ <input
12
+ type={type}
13
+ className={cn(
14
+ "flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
15
+ className
16
+ )}
17
+ ref={ref}
18
+ {...props}
19
+ />
20
+ )
21
+ }
22
+ )
23
+ Input.displayName = "Input"
24
+
25
+ export { Input }
client/src/components/ui/label.tsx ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from "react"
2
+ import * as LabelPrimitive from "@radix-ui/react-label"
3
+ import { cva, type VariantProps } from "class-variance-authority"
4
+
5
+ import { cn } from "@/lib/utils"
6
+
7
+ const labelVariants = cva(
8
+ "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
9
+ )
10
+
11
+ const Label = React.forwardRef<
12
+ React.ElementRef<typeof LabelPrimitive.Root>,
13
+ React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> &
14
+ VariantProps<typeof labelVariants>
15
+ >(({ className, ...props }, ref) => (
16
+ <LabelPrimitive.Root
17
+ ref={ref}
18
+ className={cn(labelVariants(), className)}
19
+ {...props}
20
+ />
21
+ ))
22
+ Label.displayName = LabelPrimitive.Root.displayName
23
+
24
+ export { Label }
client/src/components/ui/scroll-area.tsx ADDED
@@ -0,0 +1,46 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from "react"
2
+ import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area"
3
+
4
+ import { cn } from "@/lib/utils"
5
+
6
+ const ScrollArea = React.forwardRef<
7
+ React.ElementRef<typeof ScrollAreaPrimitive.Root>,
8
+ React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.Root>
9
+ >(({ className, children, ...props }, ref) => (
10
+ <ScrollAreaPrimitive.Root
11
+ ref={ref}
12
+ className={cn("relative overflow-hidden", className)}
13
+ {...props}
14
+ >
15
+ <ScrollAreaPrimitive.Viewport className="h-full w-full rounded-[inherit]">
16
+ {children}
17
+ </ScrollAreaPrimitive.Viewport>
18
+ <ScrollBar />
19
+ <ScrollAreaPrimitive.Corner />
20
+ </ScrollAreaPrimitive.Root>
21
+ ))
22
+ ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName
23
+
24
+ const ScrollBar = React.forwardRef<
25
+ React.ElementRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>,
26
+ React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>
27
+ >(({ className, orientation = "vertical", ...props }, ref) => (
28
+ <ScrollAreaPrimitive.ScrollAreaScrollbar
29
+ ref={ref}
30
+ orientation={orientation}
31
+ className={cn(
32
+ "flex touch-none select-none transition-colors",
33
+ orientation === "vertical" &&
34
+ "h-full w-2.5 border-l border-l-transparent p-[1px]",
35
+ orientation === "horizontal" &&
36
+ "h-2.5 flex-col border-t border-t-transparent p-[1px]",
37
+ className
38
+ )}
39
+ {...props}
40
+ >
41
+ <ScrollAreaPrimitive.ScrollAreaThumb className="relative flex-1 rounded-full bg-border" />
42
+ </ScrollAreaPrimitive.ScrollAreaScrollbar>
43
+ ))
44
+ ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName
45
+
46
+ export { ScrollArea, ScrollBar }
client/src/components/ui/separator.tsx ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from "react"
2
+ import * as SeparatorPrimitive from "@radix-ui/react-separator"
3
+
4
+ import { cn } from "@/lib/utils"
5
+
6
+ const Separator = React.forwardRef<
7
+ React.ElementRef<typeof SeparatorPrimitive.Root>,
8
+ React.ComponentPropsWithoutRef<typeof SeparatorPrimitive.Root>
9
+ >(
10
+ (
11
+ { className, orientation = "horizontal", decorative = true, ...props },
12
+ ref
13
+ ) => (
14
+ <SeparatorPrimitive.Root
15
+ ref={ref}
16
+ decorative={decorative}
17
+ orientation={orientation}
18
+ className={cn(
19
+ "shrink-0 bg-border",
20
+ orientation === "horizontal" ? "h-[1px] w-full" : "h-full w-[1px]",
21
+ className
22
+ )}
23
+ {...props}
24
+ />
25
+ )
26
+ )
27
+ Separator.displayName = SeparatorPrimitive.Root.displayName
28
+
29
+ export { Separator }
client/src/components/ui/tabs.tsx ADDED
@@ -0,0 +1,53 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from "react"
2
+ import * as TabsPrimitive from "@radix-ui/react-tabs"
3
+
4
+ import { cn } from "@/lib/utils"
5
+
6
+ const Tabs = TabsPrimitive.Root
7
+
8
+ const TabsList = React.forwardRef<
9
+ React.ElementRef<typeof TabsPrimitive.List>,
10
+ React.ComponentPropsWithoutRef<typeof TabsPrimitive.List>
11
+ >(({ className, ...props }, ref) => (
12
+ <TabsPrimitive.List
13
+ ref={ref}
14
+ className={cn(
15
+ "inline-flex h-10 items-center justify-center rounded-md bg-muted p-1 text-muted-foreground",
16
+ className
17
+ )}
18
+ {...props}
19
+ />
20
+ ))
21
+ TabsList.displayName = TabsPrimitive.List.displayName
22
+
23
+ const TabsTrigger = React.forwardRef<
24
+ React.ElementRef<typeof TabsPrimitive.Trigger>,
25
+ React.ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger>
26
+ >(({ className, ...props }, ref) => (
27
+ <TabsPrimitive.Trigger
28
+ ref={ref}
29
+ className={cn(
30
+ "inline-flex items-center justify-center whitespace-nowrap rounded-sm px-3 py-1.5 text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow-sm",
31
+ className
32
+ )}
33
+ {...props}
34
+ />
35
+ ))
36
+ TabsTrigger.displayName = TabsPrimitive.Trigger.displayName
37
+
38
+ const TabsContent = React.forwardRef<
39
+ React.ElementRef<typeof TabsPrimitive.Content>,
40
+ React.ComponentPropsWithoutRef<typeof TabsPrimitive.Content>
41
+ >(({ className, ...props }, ref) => (
42
+ <TabsPrimitive.Content
43
+ ref={ref}
44
+ className={cn(
45
+ "mt-2 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
46
+ className
47
+ )}
48
+ {...props}
49
+ />
50
+ ))
51
+ TabsContent.displayName = TabsPrimitive.Content.displayName
52
+
53
+ export { Tabs, TabsList, TabsTrigger, TabsContent }
client/src/components/ui/textarea.tsx ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from "react"
2
+
3
+ import { cn } from "@/lib/utils"
4
+
5
+ export interface TextareaProps
6
+ extends React.TextareaHTMLAttributes<HTMLTextAreaElement> {}
7
+
8
+ const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
9
+ ({ className, ...props }, ref) => {
10
+ return (
11
+ <textarea
12
+ className={cn(
13
+ "flex min-h-[80px] w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
14
+ className
15
+ )}
16
+ ref={ref}
17
+ {...props}
18
+ />
19
+ )
20
+ }
21
+ )
22
+ Textarea.displayName = "Textarea"
23
+
24
+ export { Textarea }
client/src/components/ui/toaster.tsx ADDED
@@ -0,0 +1,135 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from "react"
2
+ import * as ToastPrimitives from "@radix-ui/react-toast"
3
+ import { cva, type VariantProps } from "class-variance-authority"
4
+ import { X } from "lucide-react"
5
+
6
+ import { cn } from "@/lib/utils"
7
+
8
+ const ToastProvider = ToastPrimitives.Provider
9
+
10
+ const ToastViewport = React.forwardRef<
11
+ React.ElementRef<typeof ToastPrimitives.Viewport>,
12
+ React.ComponentPropsWithoutRef<typeof ToastPrimitives.Viewport>
13
+ >(({ className, ...props }, ref) => (
14
+ <ToastPrimitives.Viewport
15
+ ref={ref}
16
+ className={cn(
17
+ "fixed top-0 z-[100] flex max-h-screen w-full flex-col-reverse p-4 sm:bottom-0 sm:right-0 sm:top-auto sm:flex-col md:max-w-[420px]",
18
+ className
19
+ )}
20
+ {...props}
21
+ />
22
+ ))
23
+ ToastViewport.displayName = ToastPrimitives.Viewport.displayName
24
+
25
+ const toastVariants = cva(
26
+ "group pointer-events-auto relative flex w-full items-center justify-between space-x-4 overflow-hidden rounded-md border p-6 pr-8 shadow-lg transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full",
27
+ {
28
+ variants: {
29
+ variant: {
30
+ default: "border bg-background text-foreground",
31
+ destructive:
32
+ "destructive border-destructive bg-destructive text-destructive-foreground",
33
+ },
34
+ },
35
+ defaultVariants: {
36
+ variant: "default",
37
+ },
38
+ }
39
+ )
40
+
41
+ const Toast = React.forwardRef<
42
+ React.ElementRef<typeof ToastPrimitives.Root>,
43
+ React.ComponentPropsWithoutRef<typeof ToastPrimitives.Root> &
44
+ VariantProps<typeof toastVariants>
45
+ >(({ className, variant, ...props }, ref) => {
46
+ return (
47
+ <ToastPrimitives.Root
48
+ ref={ref}
49
+ className={cn(toastVariants({ variant }), className)}
50
+ {...props}
51
+ />
52
+ )
53
+ })
54
+ Toast.displayName = ToastPrimitives.Root.displayName
55
+
56
+ const ToastAction = React.forwardRef<
57
+ React.ElementRef<typeof ToastPrimitives.Action>,
58
+ React.ComponentPropsWithoutRef<typeof ToastPrimitives.Action>
59
+ >(({ className, ...props }, ref) => (
60
+ <ToastPrimitives.Action
61
+ ref={ref}
62
+ className={cn(
63
+ "inline-flex h-8 shrink-0 items-center justify-center rounded-md border bg-transparent px-3 text-sm font-medium ring-offset-background transition-colors hover:bg-secondary focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 group-[.destructive]:border-muted/40 group-[.destructive]:hover:border-destructive/30 group-[.destructive]:hover:bg-destructive group-[.destructive]:hover:text-destructive-foreground group-[.destructive]:focus:ring-destructive",
64
+ className
65
+ )}
66
+ {...props}
67
+ />
68
+ ))
69
+ ToastAction.displayName = ToastPrimitives.Action.displayName
70
+
71
+ const ToastClose = React.forwardRef<
72
+ React.ElementRef<typeof ToastPrimitives.Close>,
73
+ React.ComponentPropsWithoutRef<typeof ToastPrimitives.Close>
74
+ >(({ className, ...props }, ref) => (
75
+ <ToastPrimitives.Close
76
+ ref={ref}
77
+ className={cn(
78
+ "absolute right-2 top-2 rounded-md p-1 text-foreground/50 opacity-0 transition-opacity hover:text-foreground focus:opacity-100 focus:outline-none focus:ring-2 group-hover:opacity-100 group-[.destructive]:text-red-300 group-[.destructive]:hover:text-red-50 group-[.destructive]:focus:ring-red-400 group-[.destructive]:focus:ring-offset-red-600",
79
+ className
80
+ )}
81
+ toast-close=""
82
+ {...props}
83
+ >
84
+ <X className="h-4 w-4" />
85
+ </ToastPrimitives.Close>
86
+ ))
87
+ ToastClose.displayName = ToastPrimitives.Close.displayName
88
+
89
+ const ToastTitle = React.forwardRef<
90
+ React.ElementRef<typeof ToastPrimitives.Title>,
91
+ React.ComponentPropsWithoutRef<typeof ToastPrimitives.Title>
92
+ >(({ className, ...props }, ref) => (
93
+ <ToastPrimitives.Title
94
+ ref={ref}
95
+ className={cn("text-sm font-semibold", className)}
96
+ {...props}
97
+ />
98
+ ))
99
+ ToastTitle.displayName = ToastPrimitives.Title.displayName
100
+
101
+ const ToastDescription = React.forwardRef<
102
+ React.ElementRef<typeof ToastPrimitives.Description>,
103
+ React.ComponentPropsWithoutRef<typeof ToastPrimitives.Description>
104
+ >(({ className, ...props }, ref) => (
105
+ <ToastPrimitives.Description
106
+ ref={ref}
107
+ className={cn("text-sm opacity-90", className)}
108
+ {...props}
109
+ />
110
+ ))
111
+ ToastDescription.displayName = ToastPrimitives.Description.displayName
112
+
113
+ type ToastProps = React.ComponentPropsWithoutRef<typeof Toast>
114
+
115
+ type ToastActionElement = React.ReactElement<typeof ToastAction>
116
+
117
+ export {
118
+ type ToastProps,
119
+ type ToastActionElement,
120
+ ToastProvider,
121
+ ToastViewport,
122
+ Toast,
123
+ ToastTitle,
124
+ ToastDescription,
125
+ ToastClose,
126
+ ToastAction,
127
+ }
128
+
129
+ export function Toaster() {
130
+ return (
131
+ <ToastProvider>
132
+ <ToastViewport />
133
+ </ToastProvider>
134
+ )
135
+ }
client/src/index.css ADDED
@@ -0,0 +1,123 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ @tailwind base;
2
+ @tailwind components;
3
+ @tailwind utilities;
4
+
5
+ @layer base {
6
+ :root {
7
+ --background: 0 0% 100%;
8
+ --foreground: 222.2 84% 4.9%;
9
+ --card: 0 0% 100%;
10
+ --card-foreground: 222.2 84% 4.9%;
11
+ --popover: 0 0% 100%;
12
+ --popover-foreground: 222.2 84% 4.9%;
13
+ --primary: 221.2 83.2% 53.3%;
14
+ --primary-foreground: 210 40% 98%;
15
+ --secondary: 210 40% 96%;
16
+ --secondary-foreground: 222.2 84% 4.9%;
17
+ --muted: 210 40% 96%;
18
+ --muted-foreground: 215.4 16.3% 46.9%;
19
+ --accent: 210 40% 96%;
20
+ --accent-foreground: 222.2 84% 4.9%;
21
+ --destructive: 0 84.2% 60.2%;
22
+ --destructive-foreground: 210 40% 98%;
23
+ --border: 214.3 31.8% 91.4%;
24
+ --input: 214.3 31.8% 91.4%;
25
+ --ring: 221.2 83.2% 53.3%;
26
+ --radius: 0.5rem;
27
+ }
28
+
29
+ .dark {
30
+ --background: 222.2 84% 4.9%;
31
+ --foreground: 210 40% 98%;
32
+ --card: 222.2 84% 4.9%;
33
+ --card-foreground: 210 40% 98%;
34
+ --popover: 222.2 84% 4.9%;
35
+ --popover-foreground: 210 40% 98%;
36
+ --primary: 217.2 91.2% 59.8%;
37
+ --primary-foreground: 222.2 84% 4.9%;
38
+ --secondary: 217.2 32.6% 17.5%;
39
+ --secondary-foreground: 210 40% 98%;
40
+ --muted: 217.2 32.6% 17.5%;
41
+ --muted-foreground: 215 20.2% 65.1%;
42
+ --accent: 217.2 32.6% 17.5%;
43
+ --accent-foreground: 210 40% 98%;
44
+ --destructive: 0 62.8% 30.6%;
45
+ --destructive-foreground: 210 40% 98%;
46
+ --border: 217.2 32.6% 17.5%;
47
+ --input: 217.2 32.6% 17.5%;
48
+ --ring: 224.3 76.3% 94.1%;
49
+ }
50
+ }
51
+
52
+ @layer base {
53
+ * {
54
+ @apply border-border;
55
+ }
56
+ body {
57
+ @apply bg-background text-foreground;
58
+ }
59
+ }
60
+
61
+ /* Custom scrollbar */
62
+ ::-webkit-scrollbar {
63
+ width: 6px;
64
+ }
65
+
66
+ ::-webkit-scrollbar-track {
67
+ background: transparent;
68
+ }
69
+
70
+ ::-webkit-scrollbar-thumb {
71
+ background: hsl(var(--muted-foreground) / 0.3);
72
+ border-radius: 3px;
73
+ }
74
+
75
+ ::-webkit-scrollbar-thumb:hover {
76
+ background: hsl(var(--muted-foreground) / 0.5);
77
+ }
78
+
79
+ /* Chat message animations */
80
+ .message-enter {
81
+ opacity: 0;
82
+ transform: translateY(10px);
83
+ }
84
+
85
+ .message-enter-active {
86
+ opacity: 1;
87
+ transform: translateY(0);
88
+ transition: opacity 200ms, transform 200ms;
89
+ }
90
+
91
+ /* Typing indicator animation */
92
+ .typing-dots {
93
+ display: inline-block;
94
+ }
95
+
96
+ .typing-dots span {
97
+ display: inline-block;
98
+ width: 4px;
99
+ height: 4px;
100
+ border-radius: 50%;
101
+ background-color: hsl(var(--muted-foreground));
102
+ margin: 0 1px;
103
+ animation: typing 1.4s infinite ease-in-out;
104
+ }
105
+
106
+ .typing-dots span:nth-child(1) {
107
+ animation-delay: -0.32s;
108
+ }
109
+
110
+ .typing-dots span:nth-child(2) {
111
+ animation-delay: -0.16s;
112
+ }
113
+
114
+ @keyframes typing {
115
+ 0%, 80%, 100% {
116
+ transform: scale(0.8);
117
+ opacity: 0.5;
118
+ }
119
+ 40% {
120
+ transform: scale(1);
121
+ opacity: 1;
122
+ }
123
+ }
client/src/lib/utils.ts ADDED
@@ -0,0 +1,190 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { type ClassValue, clsx } from "clsx"
2
+ import { twMerge } from "tailwind-merge"
3
+
4
+ export function cn(...inputs: ClassValue[]) {
5
+ return twMerge(clsx(inputs))
6
+ }
7
+
8
+ export function formatDate(date: Date | string): string {
9
+ const d = new Date(date)
10
+ const now = new Date()
11
+ const diff = now.getTime() - d.getTime()
12
+
13
+ // Less than 1 minute
14
+ if (diff < 60000) {
15
+ return 'Just now'
16
+ }
17
+
18
+ // Less than 1 hour
19
+ if (diff < 3600000) {
20
+ const minutes = Math.floor(diff / 60000)
21
+ return `${minutes}m ago`
22
+ }
23
+
24
+ // Less than 24 hours
25
+ if (diff < 86400000) {
26
+ const hours = Math.floor(diff / 3600000)
27
+ return `${hours}h ago`
28
+ }
29
+
30
+ // Less than 7 days
31
+ if (diff < 604800000) {
32
+ const days = Math.floor(diff / 86400000)
33
+ return `${days}d ago`
34
+ }
35
+
36
+ // More than 7 days
37
+ return d.toLocaleDateString()
38
+ }
39
+
40
+ export function formatTime(date: Date | string): string {
41
+ const d = new Date(date)
42
+ return d.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })
43
+ }
44
+
45
+ export function formatFileSize(bytes: number): string {
46
+ if (bytes === 0) return '0 Bytes'
47
+
48
+ const k = 1024
49
+ const sizes = ['Bytes', 'KB', 'MB', 'GB']
50
+ const i = Math.floor(Math.log(bytes) / Math.log(k))
51
+
52
+ return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]
53
+ }
54
+
55
+ export function getFileIcon(mimeType: string): string {
56
+ if (mimeType.startsWith('image/')) return '🖼️'
57
+ if (mimeType.startsWith('video/')) return '🎥'
58
+ if (mimeType.startsWith('audio/')) return '🎵'
59
+ if (mimeType.includes('pdf')) return '📄'
60
+ if (mimeType.includes('word')) return '📝'
61
+ if (mimeType.includes('excel') || mimeType.includes('spreadsheet')) return '📊'
62
+ if (mimeType.includes('powerpoint') || mimeType.includes('presentation')) return '📽️'
63
+ if (mimeType.includes('zip') || mimeType.includes('rar') || mimeType.includes('archive')) return '📦'
64
+ return '📎'
65
+ }
66
+
67
+ export function generateAvatar(name: string): string {
68
+ const colors = [
69
+ '#FF6B6B', '#4ECDC4', '#45B7D1', '#96CEB4', '#FFEAA7',
70
+ '#DDA0DD', '#98D8C8', '#F7DC6F', '#BB8FCE', '#85C1E9'
71
+ ]
72
+
73
+ const initials = name
74
+ .split(' ')
75
+ .map(word => word[0])
76
+ .join('')
77
+ .toUpperCase()
78
+ .slice(0, 2)
79
+
80
+ const colorIndex = name.charCodeAt(0) % colors.length
81
+ const color = colors[colorIndex]
82
+
83
+ return `data:image/svg+xml,${encodeURIComponent(`
84
+ <svg width="40" height="40" xmlns="http://www.w3.org/2000/svg">
85
+ <circle cx="20" cy="20" r="20" fill="${color}"/>
86
+ <text x="20" y="25" text-anchor="middle" fill="white" font-family="Arial" font-size="14" font-weight="bold">
87
+ ${initials}
88
+ </text>
89
+ </svg>
90
+ `)}`
91
+ }
92
+
93
+ export function debounce<T extends (...args: any[]) => any>(
94
+ func: T,
95
+ wait: number
96
+ ): (...args: Parameters<T>) => void {
97
+ let timeout: NodeJS.Timeout | null = null
98
+
99
+ return (...args: Parameters<T>) => {
100
+ if (timeout) {
101
+ clearTimeout(timeout)
102
+ }
103
+
104
+ timeout = setTimeout(() => {
105
+ func(...args)
106
+ }, wait)
107
+ }
108
+ }
109
+
110
+ export function throttle<T extends (...args: any[]) => any>(
111
+ func: T,
112
+ limit: number
113
+ ): (...args: Parameters<T>) => void {
114
+ let inThrottle: boolean
115
+
116
+ return (...args: Parameters<T>) => {
117
+ if (!inThrottle) {
118
+ func(...args)
119
+ inThrottle = true
120
+ setTimeout(() => inThrottle = false, limit)
121
+ }
122
+ }
123
+ }
124
+
125
+ export function isValidEmail(email: string): boolean {
126
+ const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
127
+ return emailRegex.test(email)
128
+ }
129
+
130
+ export function isValidUsername(username: string): boolean {
131
+ const usernameRegex = /^[a-zA-Z0-9_]{3,20}$/
132
+ return usernameRegex.test(username)
133
+ }
134
+
135
+ export function truncateText(text: string, maxLength: number): string {
136
+ if (text.length <= maxLength) return text
137
+ return text.slice(0, maxLength) + '...'
138
+ }
139
+
140
+ export function copyToClipboard(text: string): Promise<void> {
141
+ if (navigator.clipboard) {
142
+ return navigator.clipboard.writeText(text)
143
+ } else {
144
+ // Fallback for older browsers
145
+ const textArea = document.createElement('textarea')
146
+ textArea.value = text
147
+ document.body.appendChild(textArea)
148
+ textArea.focus()
149
+ textArea.select()
150
+
151
+ try {
152
+ document.execCommand('copy')
153
+ return Promise.resolve()
154
+ } catch (err) {
155
+ return Promise.reject(err)
156
+ } finally {
157
+ document.body.removeChild(textArea)
158
+ }
159
+ }
160
+ }
161
+
162
+ export function downloadFile(url: string, filename: string): void {
163
+ const link = document.createElement('a')
164
+ link.href = url
165
+ link.download = filename
166
+ document.body.appendChild(link)
167
+ link.click()
168
+ document.body.removeChild(link)
169
+ }
170
+
171
+ export function isImageFile(file: File): boolean {
172
+ return file.type.startsWith('image/')
173
+ }
174
+
175
+ export function isVideoFile(file: File): boolean {
176
+ return file.type.startsWith('video/')
177
+ }
178
+
179
+ export function isAudioFile(file: File): boolean {
180
+ return file.type.startsWith('audio/')
181
+ }
182
+
183
+ export function getInitials(name: string): string {
184
+ return name
185
+ .split(' ')
186
+ .map(word => word[0])
187
+ .join('')
188
+ .toUpperCase()
189
+ .slice(0, 2)
190
+ }
client/src/main.tsx ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React from 'react'
2
+ import ReactDOM from 'react-dom/client'
3
+ import { BrowserRouter } from 'react-router-dom'
4
+ import App from './App.tsx'
5
+ import './index.css'
6
+
7
+ ReactDOM.createRoot(document.getElementById('root')!).render(
8
+ <React.StrictMode>
9
+ <BrowserRouter>
10
+ <App />
11
+ </BrowserRouter>
12
+ </React.StrictMode>,
13
+ )
client/src/pages/AdminPage.tsx ADDED
@@ -0,0 +1,304 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useState, useEffect } from 'react'
2
+ import { Routes, Route, Link, useLocation } from 'react-router-dom'
3
+ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
4
+ // The following components are currently unused but kept for future
5
+ // admin interface enhancements.
6
+ // import { Button } from '@/components/ui/button'
7
+ // import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
8
+ // import { Badge } from '@/components/ui/badge'
9
+ import {
10
+ Users,
11
+ MessageSquare,
12
+ Settings,
13
+ BarChart3,
14
+ Shield,
15
+ Activity,
16
+ Database,
17
+ Server
18
+ } from 'lucide-react'
19
+
20
+ // Mock data - replace with real API calls
21
+ const mockStats = {
22
+ totalUsers: 1234,
23
+ activeUsers: 567,
24
+ totalChats: 890,
25
+ totalMessages: 12345,
26
+ storageUsed: 2.5, // GB
27
+ serverUptime: 99.9 // %
28
+ }
29
+
30
+ export default function AdminPage() {
31
+ // Stats state will be updated once API integration is implemented
32
+ const [stats] = useState(mockStats)
33
+ const location = useLocation()
34
+
35
+ useEffect(() => {
36
+ // Load admin stats
37
+ // TODO: Implement API call
38
+ }, [])
39
+
40
+ return (
41
+ <div className="min-h-screen bg-background">
42
+ <div className="border-b">
43
+ <div className="flex h-16 items-center px-6">
44
+ <Shield className="w-6 h-6 mr-2" />
45
+ <h1 className="text-xl font-semibold">Admin Panel</h1>
46
+ </div>
47
+ </div>
48
+
49
+ <div className="flex">
50
+ {/* Sidebar */}
51
+ <div className="w-64 border-r min-h-screen p-4">
52
+ <nav className="space-y-2">
53
+ <Link
54
+ to="/admin"
55
+ className={`flex items-center space-x-2 px-3 py-2 rounded-md text-sm font-medium ${
56
+ location.pathname === '/admin'
57
+ ? 'bg-primary text-primary-foreground'
58
+ : 'text-muted-foreground hover:text-foreground hover:bg-muted'
59
+ }`}
60
+ >
61
+ <BarChart3 className="w-4 h-4" />
62
+ <span>Dashboard</span>
63
+ </Link>
64
+ <Link
65
+ to="/admin/users"
66
+ className={`flex items-center space-x-2 px-3 py-2 rounded-md text-sm font-medium ${
67
+ location.pathname === '/admin/users'
68
+ ? 'bg-primary text-primary-foreground'
69
+ : 'text-muted-foreground hover:text-foreground hover:bg-muted'
70
+ }`}
71
+ >
72
+ <Users className="w-4 h-4" />
73
+ <span>Users</span>
74
+ </Link>
75
+ <Link
76
+ to="/admin/chats"
77
+ className={`flex items-center space-x-2 px-3 py-2 rounded-md text-sm font-medium ${
78
+ location.pathname === '/admin/chats'
79
+ ? 'bg-primary text-primary-foreground'
80
+ : 'text-muted-foreground hover:text-foreground hover:bg-muted'
81
+ }`}
82
+ >
83
+ <MessageSquare className="w-4 h-4" />
84
+ <span>Chats</span>
85
+ </Link>
86
+ <Link
87
+ to="/admin/settings"
88
+ className={`flex items-center space-x-2 px-3 py-2 rounded-md text-sm font-medium ${
89
+ location.pathname === '/admin/settings'
90
+ ? 'bg-primary text-primary-foreground'
91
+ : 'text-muted-foreground hover:text-foreground hover:bg-muted'
92
+ }`}
93
+ >
94
+ <Settings className="w-4 h-4" />
95
+ <span>Settings</span>
96
+ </Link>
97
+ </nav>
98
+ </div>
99
+
100
+ {/* Main content */}
101
+ <div className="flex-1 p-6">
102
+ <Routes>
103
+ <Route path="/" element={<AdminDashboard stats={stats} />} />
104
+ <Route path="/users" element={<AdminUsers />} />
105
+ <Route path="/chats" element={<AdminChats />} />
106
+ <Route path="/settings" element={<AdminSettings />} />
107
+ </Routes>
108
+ </div>
109
+ </div>
110
+ </div>
111
+ )
112
+ }
113
+
114
+ function AdminDashboard({ stats }: { stats: typeof mockStats }) {
115
+ return (
116
+ <div className="space-y-6">
117
+ <div>
118
+ <h2 className="text-2xl font-bold">Dashboard</h2>
119
+ <p className="text-muted-foreground">
120
+ Overview of your ChatApp instance
121
+ </p>
122
+ </div>
123
+
124
+ {/* Stats Cards */}
125
+ <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
126
+ <Card>
127
+ <CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
128
+ <CardTitle className="text-sm font-medium">Total Users</CardTitle>
129
+ <Users className="h-4 w-4 text-muted-foreground" />
130
+ </CardHeader>
131
+ <CardContent>
132
+ <div className="text-2xl font-bold">{stats.totalUsers.toLocaleString()}</div>
133
+ <p className="text-xs text-muted-foreground">
134
+ +12% from last month
135
+ </p>
136
+ </CardContent>
137
+ </Card>
138
+
139
+ <Card>
140
+ <CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
141
+ <CardTitle className="text-sm font-medium">Active Users</CardTitle>
142
+ <Activity className="h-4 w-4 text-muted-foreground" />
143
+ </CardHeader>
144
+ <CardContent>
145
+ <div className="text-2xl font-bold">{stats.activeUsers.toLocaleString()}</div>
146
+ <p className="text-xs text-muted-foreground">
147
+ Currently online
148
+ </p>
149
+ </CardContent>
150
+ </Card>
151
+
152
+ <Card>
153
+ <CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
154
+ <CardTitle className="text-sm font-medium">Total Messages</CardTitle>
155
+ <MessageSquare className="h-4 w-4 text-muted-foreground" />
156
+ </CardHeader>
157
+ <CardContent>
158
+ <div className="text-2xl font-bold">{stats.totalMessages.toLocaleString()}</div>
159
+ <p className="text-xs text-muted-foreground">
160
+ +5% from last week
161
+ </p>
162
+ </CardContent>
163
+ </Card>
164
+
165
+ <Card>
166
+ <CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
167
+ <CardTitle className="text-sm font-medium">Storage Used</CardTitle>
168
+ <Database className="h-4 w-4 text-muted-foreground" />
169
+ </CardHeader>
170
+ <CardContent>
171
+ <div className="text-2xl font-bold">{stats.storageUsed} GB</div>
172
+ <p className="text-xs text-muted-foreground">
173
+ Of 10 GB available
174
+ </p>
175
+ </CardContent>
176
+ </Card>
177
+
178
+ <Card>
179
+ <CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
180
+ <CardTitle className="text-sm font-medium">Server Uptime</CardTitle>
181
+ <Server className="h-4 w-4 text-muted-foreground" />
182
+ </CardHeader>
183
+ <CardContent>
184
+ <div className="text-2xl font-bold">{stats.serverUptime}%</div>
185
+ <p className="text-xs text-muted-foreground">
186
+ Last 30 days
187
+ </p>
188
+ </CardContent>
189
+ </Card>
190
+
191
+ <Card>
192
+ <CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
193
+ <CardTitle className="text-sm font-medium">Total Chats</CardTitle>
194
+ <MessageSquare className="h-4 w-4 text-muted-foreground" />
195
+ </CardHeader>
196
+ <CardContent>
197
+ <div className="text-2xl font-bold">{stats.totalChats.toLocaleString()}</div>
198
+ <p className="text-xs text-muted-foreground">
199
+ Groups and direct messages
200
+ </p>
201
+ </CardContent>
202
+ </Card>
203
+ </div>
204
+
205
+ {/* Recent Activity */}
206
+ <Card>
207
+ <CardHeader>
208
+ <CardTitle>Recent Activity</CardTitle>
209
+ <CardDescription>
210
+ Latest events in your ChatApp instance
211
+ </CardDescription>
212
+ </CardHeader>
213
+ <CardContent>
214
+ <div className="space-y-4">
215
+ <div className="flex items-center space-x-4">
216
+ <div className="w-2 h-2 bg-green-500 rounded-full"></div>
217
+ <div className="flex-1">
218
+ <p className="text-sm">New user registered: john_doe</p>
219
+ <p className="text-xs text-muted-foreground">2 minutes ago</p>
220
+ </div>
221
+ </div>
222
+ <div className="flex items-center space-x-4">
223
+ <div className="w-2 h-2 bg-blue-500 rounded-full"></div>
224
+ <div className="flex-1">
225
+ <p className="text-sm">New group created: "Project Team"</p>
226
+ <p className="text-xs text-muted-foreground">5 minutes ago</p>
227
+ </div>
228
+ </div>
229
+ <div className="flex items-center space-x-4">
230
+ <div className="w-2 h-2 bg-yellow-500 rounded-full"></div>
231
+ <div className="flex-1">
232
+ <p className="text-sm">Server maintenance completed</p>
233
+ <p className="text-xs text-muted-foreground">1 hour ago</p>
234
+ </div>
235
+ </div>
236
+ </div>
237
+ </CardContent>
238
+ </Card>
239
+ </div>
240
+ )
241
+ }
242
+
243
+ function AdminUsers() {
244
+ return (
245
+ <div className="space-y-6">
246
+ <div>
247
+ <h2 className="text-2xl font-bold">User Management</h2>
248
+ <p className="text-muted-foreground">
249
+ Manage users and their permissions
250
+ </p>
251
+ </div>
252
+
253
+ <Card>
254
+ <CardContent className="p-6">
255
+ <p className="text-center text-muted-foreground">
256
+ User management interface coming soon...
257
+ </p>
258
+ </CardContent>
259
+ </Card>
260
+ </div>
261
+ )
262
+ }
263
+
264
+ function AdminChats() {
265
+ return (
266
+ <div className="space-y-6">
267
+ <div>
268
+ <h2 className="text-2xl font-bold">Chat Management</h2>
269
+ <p className="text-muted-foreground">
270
+ Monitor and manage chats and groups
271
+ </p>
272
+ </div>
273
+
274
+ <Card>
275
+ <CardContent className="p-6">
276
+ <p className="text-center text-muted-foreground">
277
+ Chat management interface coming soon...
278
+ </p>
279
+ </CardContent>
280
+ </Card>
281
+ </div>
282
+ )
283
+ }
284
+
285
+ function AdminSettings() {
286
+ return (
287
+ <div className="space-y-6">
288
+ <div>
289
+ <h2 className="text-2xl font-bold">System Settings</h2>
290
+ <p className="text-muted-foreground">
291
+ Configure your ChatApp instance
292
+ </p>
293
+ </div>
294
+
295
+ <Card>
296
+ <CardContent className="p-6">
297
+ <p className="text-center text-muted-foreground">
298
+ Settings interface coming soon...
299
+ </p>
300
+ </CardContent>
301
+ </Card>
302
+ </div>
303
+ )
304
+ }
client/src/pages/ChatPage.tsx ADDED
@@ -0,0 +1,59 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useEffect } from 'react'
2
+ import { useAuthStore } from '@/store/authStore'
3
+ import { useChatStore } from '@/store/chatStore'
4
+ import { socketService } from '@/services/socketService'
5
+ import ChatSidebar from '@/components/chat/ChatSidebar'
6
+ import ChatWindow from '@/components/chat/ChatWindow'
7
+ import { Separator } from '@/components/ui/separator'
8
+
9
+ export default function ChatPage() {
10
+ const { user, token } = useAuthStore()
11
+ const { loadChats, currentChat } = useChatStore()
12
+
13
+ useEffect(() => {
14
+ if (token && user) {
15
+ // Connect to socket
16
+ socketService.connect(token).then(() => {
17
+ console.log('Socket connected successfully')
18
+ }).catch((error) => {
19
+ console.error('Socket connection failed:', error)
20
+ })
21
+
22
+ // Load initial data
23
+ loadChats()
24
+
25
+ // Cleanup on unmount
26
+ return () => {
27
+ socketService.disconnect()
28
+ }
29
+ }
30
+ }, [token, user, loadChats])
31
+
32
+ return (
33
+ <div className="flex h-screen bg-background">
34
+ {/* Sidebar */}
35
+ <div className="w-80 border-r border-border">
36
+ <ChatSidebar />
37
+ </div>
38
+
39
+ <Separator orientation="vertical" />
40
+
41
+ {/* Main chat area */}
42
+ <div className="flex-1 flex flex-col">
43
+ {currentChat ? (
44
+ <ChatWindow />
45
+ ) : (
46
+ <div className="flex-1 flex items-center justify-center">
47
+ <div className="text-center">
48
+ <div className="text-6xl mb-4">💬</div>
49
+ <h2 className="text-2xl font-semibold mb-2">Welcome to ChatApp</h2>
50
+ <p className="text-muted-foreground">
51
+ Select a chat to start messaging
52
+ </p>
53
+ </div>
54
+ </div>
55
+ )}
56
+ </div>
57
+ </div>
58
+ )
59
+ }
client/src/pages/LoginPage.tsx ADDED
@@ -0,0 +1,109 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useState } from 'react'
2
+ import { Link, useNavigate } from 'react-router-dom'
3
+ import { useAuthStore } from '@/store/authStore'
4
+ import { Button } from '@/components/ui/button'
5
+ import { Input } from '@/components/ui/input'
6
+ import { Label } from '@/components/ui/label'
7
+ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
8
+ import { Alert, AlertDescription } from '@/components/ui/alert'
9
+ import LoadingSpinner from '@/components/LoadingSpinner'
10
+ import { MessageCircle } from 'lucide-react'
11
+
12
+ export default function LoginPage() {
13
+ const [email, setEmail] = useState('')
14
+ const [password, setPassword] = useState('')
15
+ const { login, loading, error, clearError } = useAuthStore()
16
+ const navigate = useNavigate()
17
+
18
+ const handleSubmit = async (e: React.FormEvent) => {
19
+ e.preventDefault()
20
+ clearError()
21
+
22
+ try {
23
+ await login(email, password)
24
+ navigate('/chat')
25
+ } catch (error) {
26
+ // Error is handled by the store
27
+ }
28
+ }
29
+
30
+ return (
31
+ <div className="min-h-screen flex items-center justify-center bg-gradient-to-br from-blue-50 to-indigo-100 p-4">
32
+ <Card className="w-full max-w-md">
33
+ <CardHeader className="text-center">
34
+ <div className="flex justify-center mb-4">
35
+ <div className="p-3 bg-blue-600 rounded-full">
36
+ <MessageCircle className="w-8 h-8 text-white" />
37
+ </div>
38
+ </div>
39
+ <CardTitle className="text-2xl font-bold">Welcome Back</CardTitle>
40
+ <CardDescription>
41
+ Sign in to your ChatApp account
42
+ </CardDescription>
43
+ </CardHeader>
44
+ <CardContent>
45
+ <form onSubmit={handleSubmit} className="space-y-4">
46
+ {error && (
47
+ <Alert variant="destructive">
48
+ <AlertDescription>{error}</AlertDescription>
49
+ </Alert>
50
+ )}
51
+
52
+ <div className="space-y-2">
53
+ <Label htmlFor="email">Email</Label>
54
+ <Input
55
+ id="email"
56
+ type="email"
57
+ placeholder="Enter your email"
58
+ value={email}
59
+ onChange={(e) => setEmail(e.target.value)}
60
+ required
61
+ disabled={loading}
62
+ />
63
+ </div>
64
+
65
+ <div className="space-y-2">
66
+ <Label htmlFor="password">Password</Label>
67
+ <Input
68
+ id="password"
69
+ type="password"
70
+ placeholder="Enter your password"
71
+ value={password}
72
+ onChange={(e) => setPassword(e.target.value)}
73
+ required
74
+ disabled={loading}
75
+ />
76
+ </div>
77
+
78
+ <Button
79
+ type="submit"
80
+ className="w-full"
81
+ disabled={loading}
82
+ >
83
+ {loading ? (
84
+ <>
85
+ <LoadingSpinner size="sm" className="mr-2" />
86
+ Signing in...
87
+ </>
88
+ ) : (
89
+ 'Sign In'
90
+ )}
91
+ </Button>
92
+ </form>
93
+
94
+ <div className="mt-6 text-center">
95
+ <p className="text-sm text-gray-600">
96
+ Don't have an account?{' '}
97
+ <Link
98
+ to="/register"
99
+ className="text-blue-600 hover:text-blue-500 font-medium"
100
+ >
101
+ Sign up
102
+ </Link>
103
+ </p>
104
+ </div>
105
+ </CardContent>
106
+ </Card>
107
+ </div>
108
+ )
109
+ }
client/src/pages/ProfilePage.tsx ADDED
@@ -0,0 +1,173 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useState } from 'react'
2
+ import { useAuthStore } from '@/store/authStore'
3
+ import { Button } from '@/components/ui/button'
4
+ import { Input } from '@/components/ui/input'
5
+ import { Label } from '@/components/ui/label'
6
+ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
7
+ import { Textarea } from '@/components/ui/textarea'
8
+ import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
9
+ import { getInitials } from '@/lib/utils'
10
+ import { User, Settings } from 'lucide-react'
11
+
12
+ export default function ProfilePage() {
13
+ const { user, updateUser } = useAuthStore()
14
+ const [isEditing, setIsEditing] = useState(false)
15
+ const [formData, setFormData] = useState({
16
+ displayName: user?.displayName || '',
17
+ bio: user?.bio || '',
18
+ })
19
+
20
+ const handleSave = () => {
21
+ if (user) {
22
+ updateUser(formData)
23
+ setIsEditing(false)
24
+ }
25
+ }
26
+
27
+ const handleCancel = () => {
28
+ setFormData({
29
+ displayName: user?.displayName || '',
30
+ bio: user?.bio || '',
31
+ })
32
+ setIsEditing(false)
33
+ }
34
+
35
+ if (!user) return null
36
+
37
+ return (
38
+ <div className="min-h-screen bg-background p-6">
39
+ <div className="max-w-2xl mx-auto space-y-6">
40
+ <div className="flex items-center space-x-4">
41
+ <Button variant="ghost" size="icon">
42
+ <User className="w-5 h-5" />
43
+ </Button>
44
+ <h1 className="text-3xl font-bold">Profile</h1>
45
+ </div>
46
+
47
+ <Card>
48
+ <CardHeader>
49
+ <CardTitle>Personal Information</CardTitle>
50
+ <CardDescription>
51
+ Manage your account details and preferences
52
+ </CardDescription>
53
+ </CardHeader>
54
+ <CardContent className="space-y-6">
55
+ <div className="flex items-center space-x-4">
56
+ <Avatar className="w-20 h-20">
57
+ <AvatarImage src={user.avatar} />
58
+ <AvatarFallback className="text-lg">
59
+ {getInitials(user.displayName)}
60
+ </AvatarFallback>
61
+ </Avatar>
62
+ <div>
63
+ <Button variant="outline" size="sm">
64
+ Change Avatar
65
+ </Button>
66
+ <p className="text-sm text-muted-foreground mt-1">
67
+ JPG, PNG or GIF. Max size 2MB.
68
+ </p>
69
+ </div>
70
+ </div>
71
+
72
+ <div className="grid grid-cols-2 gap-4">
73
+ <div className="space-y-2">
74
+ <Label>Email</Label>
75
+ <Input value={user.email} disabled />
76
+ </div>
77
+ <div className="space-y-2">
78
+ <Label>Username</Label>
79
+ <Input value={user.username} disabled />
80
+ </div>
81
+ </div>
82
+
83
+ <div className="space-y-2">
84
+ <Label htmlFor="displayName">Display Name</Label>
85
+ <Input
86
+ id="displayName"
87
+ value={formData.displayName}
88
+ onChange={(e) => setFormData(prev => ({ ...prev, displayName: e.target.value }))}
89
+ disabled={!isEditing}
90
+ />
91
+ </div>
92
+
93
+ <div className="space-y-2">
94
+ <Label htmlFor="bio">Bio</Label>
95
+ <Textarea
96
+ id="bio"
97
+ placeholder="Tell us about yourself..."
98
+ value={formData.bio}
99
+ onChange={(e) => setFormData(prev => ({ ...prev, bio: e.target.value }))}
100
+ disabled={!isEditing}
101
+ rows={3}
102
+ />
103
+ </div>
104
+
105
+ <div className="flex justify-end space-x-2">
106
+ {isEditing ? (
107
+ <>
108
+ <Button variant="outline" onClick={handleCancel}>
109
+ Cancel
110
+ </Button>
111
+ <Button onClick={handleSave}>
112
+ Save Changes
113
+ </Button>
114
+ </>
115
+ ) : (
116
+ <Button onClick={() => setIsEditing(true)}>
117
+ Edit Profile
118
+ </Button>
119
+ )}
120
+ </div>
121
+ </CardContent>
122
+ </Card>
123
+
124
+ <Card>
125
+ <CardHeader>
126
+ <CardTitle>Account Settings</CardTitle>
127
+ <CardDescription>
128
+ Security and privacy settings
129
+ </CardDescription>
130
+ </CardHeader>
131
+ <CardContent className="space-y-4">
132
+ <div className="flex items-center justify-between">
133
+ <div>
134
+ <h4 className="font-medium">Change Password</h4>
135
+ <p className="text-sm text-muted-foreground">
136
+ Update your password to keep your account secure
137
+ </p>
138
+ </div>
139
+ <Button variant="outline">
140
+ Change Password
141
+ </Button>
142
+ </div>
143
+
144
+ <div className="flex items-center justify-between">
145
+ <div>
146
+ <h4 className="font-medium">Two-Factor Authentication</h4>
147
+ <p className="text-sm text-muted-foreground">
148
+ Add an extra layer of security to your account
149
+ </p>
150
+ </div>
151
+ <Button variant="outline">
152
+ Enable 2FA
153
+ </Button>
154
+ </div>
155
+
156
+ <div className="flex items-center justify-between">
157
+ <div>
158
+ <h4 className="font-medium">Privacy Settings</h4>
159
+ <p className="text-sm text-muted-foreground">
160
+ Control who can see your information
161
+ </p>
162
+ </div>
163
+ <Button variant="outline">
164
+ <Settings className="w-4 h-4 mr-2" />
165
+ Manage
166
+ </Button>
167
+ </div>
168
+ </CardContent>
169
+ </Card>
170
+ </div>
171
+ </div>
172
+ )
173
+ }
client/src/pages/RegisterPage.tsx ADDED
@@ -0,0 +1,204 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useState } from 'react'
2
+ import { Link, useNavigate } from 'react-router-dom'
3
+ import { useAuthStore } from '@/store/authStore'
4
+ import { Button } from '@/components/ui/button'
5
+ import { Input } from '@/components/ui/input'
6
+ import { Label } from '@/components/ui/label'
7
+ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
8
+ import { Alert, AlertDescription } from '@/components/ui/alert'
9
+ import LoadingSpinner from '@/components/LoadingSpinner'
10
+ import { MessageCircle } from 'lucide-react'
11
+
12
+ export default function RegisterPage() {
13
+ const [formData, setFormData] = useState({
14
+ email: '',
15
+ username: '',
16
+ displayName: '',
17
+ password: '',
18
+ confirmPassword: ''
19
+ })
20
+ const [validationError, setValidationError] = useState('')
21
+ const { register, loading, error, clearError } = useAuthStore()
22
+ const navigate = useNavigate()
23
+
24
+ const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
25
+ const { name, value } = e.target
26
+ setFormData(prev => ({
27
+ ...prev,
28
+ [name]: value
29
+ }))
30
+ setValidationError('')
31
+ }
32
+
33
+ const validateForm = () => {
34
+ if (formData.password !== formData.confirmPassword) {
35
+ setValidationError('Passwords do not match')
36
+ return false
37
+ }
38
+
39
+ if (formData.password.length < 6) {
40
+ setValidationError('Password must be at least 6 characters long')
41
+ return false
42
+ }
43
+
44
+ if (formData.username.length < 3) {
45
+ setValidationError('Username must be at least 3 characters long')
46
+ return false
47
+ }
48
+
49
+ if (!/^[a-zA-Z0-9_]+$/.test(formData.username)) {
50
+ setValidationError('Username can only contain letters, numbers, and underscores')
51
+ return false
52
+ }
53
+
54
+ return true
55
+ }
56
+
57
+ const handleSubmit = async (e: React.FormEvent) => {
58
+ e.preventDefault()
59
+ clearError()
60
+ setValidationError('')
61
+
62
+ if (!validateForm()) {
63
+ return
64
+ }
65
+
66
+ try {
67
+ await register({
68
+ email: formData.email,
69
+ username: formData.username,
70
+ displayName: formData.displayName,
71
+ password: formData.password
72
+ })
73
+ navigate('/chat')
74
+ } catch (error) {
75
+ // Error is handled by the store
76
+ }
77
+ }
78
+
79
+ const displayError = validationError || error
80
+
81
+ return (
82
+ <div className="min-h-screen flex items-center justify-center bg-gradient-to-br from-blue-50 to-indigo-100 p-4">
83
+ <Card className="w-full max-w-md">
84
+ <CardHeader className="text-center">
85
+ <div className="flex justify-center mb-4">
86
+ <div className="p-3 bg-blue-600 rounded-full">
87
+ <MessageCircle className="w-8 h-8 text-white" />
88
+ </div>
89
+ </div>
90
+ <CardTitle className="text-2xl font-bold">Create Account</CardTitle>
91
+ <CardDescription>
92
+ Join ChatApp and start messaging
93
+ </CardDescription>
94
+ </CardHeader>
95
+ <CardContent>
96
+ <form onSubmit={handleSubmit} className="space-y-4">
97
+ {displayError && (
98
+ <Alert variant="destructive">
99
+ <AlertDescription>{displayError}</AlertDescription>
100
+ </Alert>
101
+ )}
102
+
103
+ <div className="space-y-2">
104
+ <Label htmlFor="email">Email</Label>
105
+ <Input
106
+ id="email"
107
+ name="email"
108
+ type="email"
109
+ placeholder="Enter your email"
110
+ value={formData.email}
111
+ onChange={handleChange}
112
+ required
113
+ disabled={loading}
114
+ />
115
+ </div>
116
+
117
+ <div className="space-y-2">
118
+ <Label htmlFor="username">Username</Label>
119
+ <Input
120
+ id="username"
121
+ name="username"
122
+ type="text"
123
+ placeholder="Choose a username"
124
+ value={formData.username}
125
+ onChange={handleChange}
126
+ required
127
+ disabled={loading}
128
+ />
129
+ </div>
130
+
131
+ <div className="space-y-2">
132
+ <Label htmlFor="displayName">Display Name</Label>
133
+ <Input
134
+ id="displayName"
135
+ name="displayName"
136
+ type="text"
137
+ placeholder="Your display name"
138
+ value={formData.displayName}
139
+ onChange={handleChange}
140
+ required
141
+ disabled={loading}
142
+ />
143
+ </div>
144
+
145
+ <div className="space-y-2">
146
+ <Label htmlFor="password">Password</Label>
147
+ <Input
148
+ id="password"
149
+ name="password"
150
+ type="password"
151
+ placeholder="Create a password"
152
+ value={formData.password}
153
+ onChange={handleChange}
154
+ required
155
+ disabled={loading}
156
+ />
157
+ </div>
158
+
159
+ <div className="space-y-2">
160
+ <Label htmlFor="confirmPassword">Confirm Password</Label>
161
+ <Input
162
+ id="confirmPassword"
163
+ name="confirmPassword"
164
+ type="password"
165
+ placeholder="Confirm your password"
166
+ value={formData.confirmPassword}
167
+ onChange={handleChange}
168
+ required
169
+ disabled={loading}
170
+ />
171
+ </div>
172
+
173
+ <Button
174
+ type="submit"
175
+ className="w-full"
176
+ disabled={loading}
177
+ >
178
+ {loading ? (
179
+ <>
180
+ <LoadingSpinner size="sm" className="mr-2" />
181
+ Creating account...
182
+ </>
183
+ ) : (
184
+ 'Create Account'
185
+ )}
186
+ </Button>
187
+ </form>
188
+
189
+ <div className="mt-6 text-center">
190
+ <p className="text-sm text-gray-600">
191
+ Already have an account?{' '}
192
+ <Link
193
+ to="/login"
194
+ className="text-blue-600 hover:text-blue-500 font-medium"
195
+ >
196
+ Sign in
197
+ </Link>
198
+ </p>
199
+ </div>
200
+ </CardContent>
201
+ </Card>
202
+ </div>
203
+ )
204
+ }
client/src/services/api.ts ADDED
@@ -0,0 +1,182 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import axios, { AxiosInstance, AxiosRequestConfig } from 'axios'
2
+ import { ApiResponse } from '../../../shared/types'
3
+
4
+ class ApiService {
5
+ private api: AxiosInstance
6
+
7
+ constructor() {
8
+ this.api = axios.create({
9
+ baseURL: import.meta.env.VITE_API_URL || 'http://localhost:3001',
10
+ timeout: 10000,
11
+ headers: {
12
+ 'Content-Type': 'application/json',
13
+ },
14
+ })
15
+
16
+ // Request interceptor to add auth token
17
+ this.api.interceptors.request.use(
18
+ (config) => {
19
+ const token = this.getToken()
20
+ if (token) {
21
+ config.headers.Authorization = `Bearer ${token}`
22
+ }
23
+ return config
24
+ },
25
+ (error) => {
26
+ return Promise.reject(error)
27
+ }
28
+ )
29
+
30
+ // Response interceptor to handle errors
31
+ this.api.interceptors.response.use(
32
+ (response) => {
33
+ return response
34
+ },
35
+ (error) => {
36
+ if (error.response?.status === 401) {
37
+ // Token expired or invalid
38
+ this.clearToken()
39
+ window.location.href = '/login'
40
+ }
41
+ return Promise.reject(this.handleError(error))
42
+ }
43
+ )
44
+ }
45
+
46
+ private getToken(): string | null {
47
+ try {
48
+ const authStorage = localStorage.getItem('auth-storage')
49
+ if (authStorage) {
50
+ const parsed = JSON.parse(authStorage)
51
+ return parsed.state?.token || null
52
+ }
53
+ } catch (error) {
54
+ console.error('Error getting token:', error)
55
+ }
56
+ return null
57
+ }
58
+
59
+ private clearToken(): void {
60
+ try {
61
+ localStorage.removeItem('auth-storage')
62
+ } catch (error) {
63
+ console.error('Error clearing token:', error)
64
+ }
65
+ }
66
+
67
+ private handleError(error: any): Error {
68
+ if (error.response) {
69
+ // Server responded with error status
70
+ const message = error.response.data?.message || error.response.data?.error || 'Server error'
71
+ return new Error(message)
72
+ } else if (error.request) {
73
+ // Request was made but no response received
74
+ return new Error('Network error - please check your connection')
75
+ } else {
76
+ // Something else happened
77
+ return new Error(error.message || 'An unexpected error occurred')
78
+ }
79
+ }
80
+
81
+ // Generic request methods
82
+ async get<T>(url: string, config?: AxiosRequestConfig): Promise<T> {
83
+ const response = await this.api.get<ApiResponse<T>>(url, config)
84
+ if (!response.data.success) {
85
+ throw new Error(response.data.error || 'Request failed')
86
+ }
87
+ return response.data.data!
88
+ }
89
+
90
+ async post<T>(url: string, data?: any, config?: AxiosRequestConfig): Promise<T> {
91
+ const response = await this.api.post<ApiResponse<T>>(url, data, config)
92
+ if (!response.data.success) {
93
+ throw new Error(response.data.error || 'Request failed')
94
+ }
95
+ return response.data.data!
96
+ }
97
+
98
+ async put<T>(url: string, data?: any, config?: AxiosRequestConfig): Promise<T> {
99
+ const response = await this.api.put<ApiResponse<T>>(url, data, config)
100
+ if (!response.data.success) {
101
+ throw new Error(response.data.error || 'Request failed')
102
+ }
103
+ return response.data.data!
104
+ }
105
+
106
+ async patch<T>(url: string, data?: any, config?: AxiosRequestConfig): Promise<T> {
107
+ const response = await this.api.patch<ApiResponse<T>>(url, data, config)
108
+ if (!response.data.success) {
109
+ throw new Error(response.data.error || 'Request failed')
110
+ }
111
+ return response.data.data!
112
+ }
113
+
114
+ async delete<T>(url: string, config?: AxiosRequestConfig): Promise<T> {
115
+ const response = await this.api.delete<ApiResponse<T>>(url, config)
116
+ if (!response.data.success) {
117
+ throw new Error(response.data.error || 'Request failed')
118
+ }
119
+ return response.data.data!
120
+ }
121
+
122
+ // File upload method
123
+ async upload<T>(url: string, file: File, onProgress?: (progress: number) => void): Promise<T> {
124
+ const formData = new FormData()
125
+ formData.append('file', file)
126
+
127
+ const config: AxiosRequestConfig = {
128
+ headers: {
129
+ 'Content-Type': 'multipart/form-data',
130
+ },
131
+ onUploadProgress: (progressEvent) => {
132
+ if (onProgress && progressEvent.total) {
133
+ const progress = Math.round((progressEvent.loaded * 100) / progressEvent.total)
134
+ onProgress(progress)
135
+ }
136
+ },
137
+ }
138
+
139
+ const response = await this.api.post<ApiResponse<T>>(url, formData, config)
140
+ if (!response.data.success) {
141
+ throw new Error(response.data.error || 'Upload failed')
142
+ }
143
+ return response.data.data!
144
+ }
145
+
146
+ // Multiple file upload method
147
+ async uploadMultiple<T>(
148
+ url: string,
149
+ files: File[],
150
+ onProgress?: (progress: number) => void
151
+ ): Promise<T> {
152
+ const formData = new FormData()
153
+ files.forEach((file, index) => {
154
+ formData.append(`files[${index}]`, file)
155
+ })
156
+
157
+ const config: AxiosRequestConfig = {
158
+ headers: {
159
+ 'Content-Type': 'multipart/form-data',
160
+ },
161
+ onUploadProgress: (progressEvent) => {
162
+ if (onProgress && progressEvent.total) {
163
+ const progress = Math.round((progressEvent.loaded * 100) / progressEvent.total)
164
+ onProgress(progress)
165
+ }
166
+ },
167
+ }
168
+
169
+ const response = await this.api.post<ApiResponse<T>>(url, formData, config)
170
+ if (!response.data.success) {
171
+ throw new Error(response.data.error || 'Upload failed')
172
+ }
173
+ return response.data.data!
174
+ }
175
+
176
+ // Get raw axios instance for custom requests
177
+ getAxiosInstance(): AxiosInstance {
178
+ return this.api
179
+ }
180
+ }
181
+
182
+ export const apiService = new ApiService()
client/src/services/authService.ts ADDED
@@ -0,0 +1,62 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { User, LoginRequest, RegisterRequest } from '../../../shared/types'
2
+ import { apiService } from './api'
3
+
4
+ interface AuthResponse {
5
+ user: User
6
+ token: string
7
+ }
8
+
9
+ class AuthService {
10
+ async login(credentials: LoginRequest): Promise<AuthResponse> {
11
+ return await apiService.post<AuthResponse>('/api/auth/login', credentials)
12
+ }
13
+
14
+ async register(userData: RegisterRequest): Promise<AuthResponse> {
15
+ return await apiService.post<AuthResponse>('/api/auth/register', userData)
16
+ }
17
+
18
+ async logout(): Promise<void> {
19
+ try {
20
+ await apiService.post('/api/auth/logout')
21
+ } catch (error) {
22
+ // Ignore logout errors, clear local storage anyway
23
+ console.warn('Logout request failed:', error)
24
+ }
25
+ }
26
+
27
+ async getCurrentUser(): Promise<User> {
28
+ return await apiService.get<User>('/api/auth/me')
29
+ }
30
+
31
+ async refreshToken(): Promise<AuthResponse> {
32
+ return await apiService.post<AuthResponse>('/api/auth/refresh')
33
+ }
34
+
35
+ async changePassword(data: {
36
+ currentPassword: string
37
+ newPassword: string
38
+ }): Promise<void> {
39
+ await apiService.post('/api/auth/change-password', data)
40
+ }
41
+
42
+ async requestPasswordReset(email: string): Promise<void> {
43
+ await apiService.post('/api/auth/forgot-password', { email })
44
+ }
45
+
46
+ async resetPassword(data: {
47
+ token: string
48
+ newPassword: string
49
+ }): Promise<void> {
50
+ await apiService.post('/api/auth/reset-password', data)
51
+ }
52
+
53
+ async verifyEmail(token: string): Promise<void> {
54
+ await apiService.post('/api/auth/verify-email', { token })
55
+ }
56
+
57
+ async resendVerificationEmail(): Promise<void> {
58
+ await apiService.post('/api/auth/resend-verification')
59
+ }
60
+ }
61
+
62
+ export const authService = new AuthService()
client/src/services/chatService.ts ADDED
@@ -0,0 +1,136 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Chat, Message, CreateChatRequest, SendMessageData, PaginatedResponse } from '../../../shared/types'
2
+ import { apiService } from './api'
3
+
4
+ class ChatService {
5
+ // Chat management
6
+ async getChats(): Promise<Chat[]> {
7
+ return await apiService.get<Chat[]>('/api/chats')
8
+ }
9
+
10
+ async getChat(chatId: string): Promise<Chat> {
11
+ return await apiService.get<Chat>(`/api/chats/${chatId}`)
12
+ }
13
+
14
+ async createChat(data: CreateChatRequest): Promise<Chat> {
15
+ return await apiService.post<Chat>('/api/chats', data)
16
+ }
17
+
18
+ async updateChat(chatId: string, data: Partial<Chat>): Promise<Chat> {
19
+ return await apiService.put<Chat>(`/api/chats/${chatId}`, data)
20
+ }
21
+
22
+ async deleteChat(chatId: string): Promise<void> {
23
+ await apiService.delete(`/api/chats/${chatId}`)
24
+ }
25
+
26
+ async leaveChat(chatId: string): Promise<void> {
27
+ await apiService.post(`/api/chats/${chatId}/leave`)
28
+ }
29
+
30
+ // Message management
31
+ async getMessages(chatId: string, page = 1, limit = 50): Promise<Message[]> {
32
+ const response = await apiService.get<PaginatedResponse<Message>>(
33
+ `/api/chats/${chatId}/messages?page=${page}&limit=${limit}`
34
+ )
35
+ return response.data
36
+ }
37
+
38
+ async sendMessage(chatId: string, data: Omit<SendMessageData, 'chatId'>): Promise<Message> {
39
+ if (data.attachments && data.attachments.length > 0) {
40
+ // Upload files first
41
+ const uploadedFiles = await this.uploadFiles(data.attachments)
42
+ return await apiService.post<Message>(`/api/chats/${chatId}/messages`, {
43
+ content: data.content,
44
+ type: data.type,
45
+ attachments: uploadedFiles,
46
+ replyTo: data.replyTo,
47
+ })
48
+ } else {
49
+ return await apiService.post<Message>(`/api/chats/${chatId}/messages`, {
50
+ content: data.content,
51
+ type: data.type,
52
+ replyTo: data.replyTo,
53
+ })
54
+ }
55
+ }
56
+
57
+ async editMessage(messageId: string, content: string): Promise<Message> {
58
+ return await apiService.put<Message>(`/api/messages/${messageId}`, { content })
59
+ }
60
+
61
+ async deleteMessage(messageId: string): Promise<void> {
62
+ await apiService.delete(`/api/messages/${messageId}`)
63
+ }
64
+
65
+ async addReaction(messageId: string, emoji: string): Promise<void> {
66
+ await apiService.post(`/api/messages/${messageId}/reactions`, { emoji })
67
+ }
68
+
69
+ async removeReaction(messageId: string, emoji: string): Promise<void> {
70
+ await apiService.delete(`/api/messages/${messageId}/reactions/${emoji}`)
71
+ }
72
+
73
+ // Group management
74
+ async addMember(chatId: string, userId: string): Promise<void> {
75
+ await apiService.post(`/api/chats/${chatId}/members`, { userId })
76
+ }
77
+
78
+ async removeMember(chatId: string, userId: string): Promise<void> {
79
+ await apiService.delete(`/api/chats/${chatId}/members/${userId}`)
80
+ }
81
+
82
+ async updateMemberRole(chatId: string, userId: string, role: string): Promise<void> {
83
+ await apiService.put(`/api/chats/${chatId}/members/${userId}`, { role })
84
+ }
85
+
86
+ async getChatMembers(chatId: string): Promise<any[]> {
87
+ return await apiService.get(`/api/chats/${chatId}/members`)
88
+ }
89
+
90
+ // File upload
91
+ async uploadFiles(files: File[]): Promise<any[]> {
92
+ const uploadPromises = files.map(file => this.uploadFile(file))
93
+ return await Promise.all(uploadPromises)
94
+ }
95
+
96
+ async uploadFile(file: File): Promise<any> {
97
+ return await apiService.upload('/api/upload', file)
98
+ }
99
+
100
+ // Search
101
+ async searchMessages(query: string, chatId?: string): Promise<Message[]> {
102
+ const params = new URLSearchParams({ q: query })
103
+ if (chatId) {
104
+ params.append('chatId', chatId)
105
+ }
106
+ return await apiService.get<Message[]>(`/api/search/messages?${params}`)
107
+ }
108
+
109
+ async searchChats(query: string): Promise<Chat[]> {
110
+ return await apiService.get<Chat[]>(`/api/search/chats?q=${encodeURIComponent(query)}`)
111
+ }
112
+
113
+ // Chat settings
114
+ async updateChatSettings(chatId: string, settings: any): Promise<void> {
115
+ await apiService.put(`/api/chats/${chatId}/settings`, settings)
116
+ }
117
+
118
+ async getChatSettings(chatId: string): Promise<any> {
119
+ return await apiService.get(`/api/chats/${chatId}/settings`)
120
+ }
121
+
122
+ // Notifications
123
+ async markAsRead(chatId: string): Promise<void> {
124
+ await apiService.post(`/api/chats/${chatId}/read`)
125
+ }
126
+
127
+ async muteChat(chatId: string, duration?: number): Promise<void> {
128
+ await apiService.post(`/api/chats/${chatId}/mute`, { duration })
129
+ }
130
+
131
+ async unmuteChat(chatId: string): Promise<void> {
132
+ await apiService.post(`/api/chats/${chatId}/unmute`)
133
+ }
134
+ }
135
+
136
+ export const chatService = new ChatService()
client/src/services/socketService.ts ADDED
@@ -0,0 +1,198 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { io, Socket } from 'socket.io-client'
2
+ import { Message, User, Chat } from '../../../shared/types'
3
+
4
+ class SocketService {
5
+ private socket: Socket | null = null
6
+ private reconnectAttempts = 0
7
+ private maxReconnectAttempts = 5
8
+ private reconnectDelay = 1000
9
+
10
+ connect(token: string): Promise<void> {
11
+ return new Promise((resolve, reject) => {
12
+ if (this.socket?.connected) {
13
+ resolve()
14
+ return
15
+ }
16
+
17
+ const socketUrl = import.meta.env.VITE_SOCKET_URL || 'http://localhost:3001'
18
+
19
+ this.socket = io(socketUrl, {
20
+ auth: {
21
+ token
22
+ },
23
+ transports: ['websocket', 'polling'],
24
+ timeout: 10000,
25
+ })
26
+
27
+ this.socket.on('connect', () => {
28
+ console.log('Socket connected')
29
+ this.reconnectAttempts = 0
30
+ resolve()
31
+ })
32
+
33
+ this.socket.on('connect_error', (error) => {
34
+ console.error('Socket connection error:', error)
35
+ reject(error)
36
+ })
37
+
38
+ this.socket.on('disconnect', (reason) => {
39
+ console.log('Socket disconnected:', reason)
40
+ if (reason === 'io server disconnect') {
41
+ // Server disconnected, try to reconnect
42
+ this.handleReconnect()
43
+ }
44
+ })
45
+
46
+ this.socket.on('error', (error) => {
47
+ console.error('Socket error:', error)
48
+ })
49
+
50
+ // Set up event listeners
51
+ this.setupEventListeners()
52
+ })
53
+ }
54
+
55
+ disconnect(): void {
56
+ if (this.socket) {
57
+ this.socket.disconnect()
58
+ this.socket = null
59
+ }
60
+ }
61
+
62
+ private handleReconnect(): void {
63
+ if (this.reconnectAttempts < this.maxReconnectAttempts) {
64
+ this.reconnectAttempts++
65
+ const delay = this.reconnectDelay * Math.pow(2, this.reconnectAttempts - 1)
66
+
67
+ setTimeout(() => {
68
+ console.log(`Attempting to reconnect (${this.reconnectAttempts}/${this.maxReconnectAttempts})`)
69
+ this.socket?.connect()
70
+ }, delay)
71
+ } else {
72
+ console.error('Max reconnection attempts reached')
73
+ }
74
+ }
75
+
76
+ private setupEventListeners(): void {
77
+ if (!this.socket) return
78
+
79
+ // Authentication events
80
+ this.socket.on('authenticated', (user: User) => {
81
+ console.log('Socket authenticated for user:', user.username)
82
+ })
83
+
84
+ this.socket.on('authentication_error', (error: string) => {
85
+ console.error('Socket authentication error:', error)
86
+ })
87
+ }
88
+
89
+ // Message events
90
+ onMessageReceive(callback: (message: Message) => void): void {
91
+ this.socket?.on('message:receive', callback)
92
+ }
93
+
94
+ onMessageEdit(callback: (message: Message) => void): void {
95
+ this.socket?.on('message:edit', callback)
96
+ }
97
+
98
+ onMessageDelete(callback: (messageId: string) => void): void {
99
+ this.socket?.on('message:delete', callback)
100
+ }
101
+
102
+ onMessageReaction(callback: (data: { messageId: string; reactions: any[] }) => void): void {
103
+ this.socket?.on('message:reaction', callback)
104
+ }
105
+
106
+ // Typing events
107
+ onTypingStart(callback: (data: { chatId: string; userId: string; user: User }) => void): void {
108
+ this.socket?.on('typing:start', callback)
109
+ }
110
+
111
+ onTypingStop(callback: (data: { chatId: string; userId: string }) => void): void {
112
+ this.socket?.on('typing:stop', callback)
113
+ }
114
+
115
+ sendTypingStart(chatId: string): void {
116
+ this.socket?.emit('typing:start', { chatId })
117
+ }
118
+
119
+ sendTypingStop(chatId: string): void {
120
+ this.socket?.emit('typing:stop', { chatId })
121
+ }
122
+
123
+ // User status events
124
+ onUserOnline(callback: (userId: string) => void): void {
125
+ this.socket?.on('user:online', callback)
126
+ }
127
+
128
+ onUserOffline(callback: (userId: string) => void): void {
129
+ this.socket?.on('user:offline', callback)
130
+ }
131
+
132
+ onUserStatusUpdate(callback: (data: { userId: string; isOnline: boolean; lastSeen?: Date }) => void): void {
133
+ this.socket?.on('user:status', callback)
134
+ }
135
+
136
+ // Chat events
137
+ onChatUpdate(callback: (chat: Chat) => void): void {
138
+ this.socket?.on('chat:update', callback)
139
+ }
140
+
141
+ onChatJoin(callback: (data: { chatId: string; user: User }) => void): void {
142
+ this.socket?.on('chat:join', callback)
143
+ }
144
+
145
+ onChatLeave(callback: (data: { chatId: string; userId: string }) => void): void {
146
+ this.socket?.on('chat:leave', callback)
147
+ }
148
+
149
+ joinChat(chatId: string): void {
150
+ this.socket?.emit('chat:join', chatId)
151
+ }
152
+
153
+ leaveChat(chatId: string): void {
154
+ this.socket?.emit('chat:leave', chatId)
155
+ }
156
+
157
+ // Group events
158
+ onGroupMemberAdd(callback: (data: { groupId: string; user: User; addedBy: User }) => void): void {
159
+ this.socket?.on('group:member:add', callback)
160
+ }
161
+
162
+ onGroupMemberRemove(callback: (data: { groupId: string; userId: string; removedBy: User }) => void): void {
163
+ this.socket?.on('group:member:remove', callback)
164
+ }
165
+
166
+ onGroupUpdate(callback: (group: any) => void): void {
167
+ this.socket?.on('group:update', callback)
168
+ }
169
+
170
+ // Notification events
171
+ onNotification(callback: (notification: any) => void): void {
172
+ this.socket?.on('notification', callback)
173
+ }
174
+
175
+ // Utility methods
176
+ isConnected(): boolean {
177
+ return this.socket?.connected || false
178
+ }
179
+
180
+ emit(event: string, data?: any): void {
181
+ this.socket?.emit(event, data)
182
+ }
183
+
184
+ on(event: string, callback: (...args: any[]) => void): void {
185
+ this.socket?.on(event, callback)
186
+ }
187
+
188
+ off(event: string, callback?: (...args: any[]) => void): void {
189
+ this.socket?.off(event, callback)
190
+ }
191
+
192
+ // Remove all listeners for cleanup
193
+ removeAllListeners(): void {
194
+ this.socket?.removeAllListeners()
195
+ }
196
+ }
197
+
198
+ export const socketService = new SocketService()
client/src/store/authStore.ts ADDED
@@ -0,0 +1,130 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { create } from 'zustand'
2
+ import { persist } from 'zustand/middleware'
3
+ import { User } from '../../../shared/types'
4
+ import { authService } from '../services/authService'
5
+
6
+ interface AuthState {
7
+ user: User | null
8
+ token: string | null
9
+ loading: boolean
10
+ error: string | null
11
+ }
12
+
13
+ interface AuthActions {
14
+ login: (email: string, password: string) => Promise<void>
15
+ register: (data: {
16
+ email: string
17
+ username: string
18
+ password: string
19
+ displayName: string
20
+ }) => Promise<void>
21
+ logout: () => void
22
+ checkAuth: () => Promise<void>
23
+ updateUser: (user: Partial<User>) => void
24
+ clearError: () => void
25
+ }
26
+
27
+ export const useAuthStore = create<AuthState & AuthActions>()(
28
+ persist(
29
+ (set, get) => ({
30
+ // State
31
+ user: null,
32
+ token: null,
33
+ loading: false,
34
+ error: null,
35
+
36
+ // Actions
37
+ login: async (email: string, password: string) => {
38
+ set({ loading: true, error: null })
39
+ try {
40
+ const response = await authService.login({ email, password })
41
+ set({
42
+ user: response.user,
43
+ token: response.token,
44
+ loading: false,
45
+ error: null,
46
+ })
47
+ } catch (error: any) {
48
+ set({
49
+ loading: false,
50
+ error: error.message || 'Login failed',
51
+ })
52
+ throw error
53
+ }
54
+ },
55
+
56
+ register: async (data) => {
57
+ set({ loading: true, error: null })
58
+ try {
59
+ const response = await authService.register(data)
60
+ set({
61
+ user: response.user,
62
+ token: response.token,
63
+ loading: false,
64
+ error: null,
65
+ })
66
+ } catch (error: any) {
67
+ set({
68
+ loading: false,
69
+ error: error.message || 'Registration failed',
70
+ })
71
+ throw error
72
+ }
73
+ },
74
+
75
+ logout: () => {
76
+ authService.logout()
77
+ set({
78
+ user: null,
79
+ token: null,
80
+ error: null,
81
+ })
82
+ },
83
+
84
+ checkAuth: async () => {
85
+ const { token } = get()
86
+ if (!token) {
87
+ set({ loading: false })
88
+ return
89
+ }
90
+
91
+ set({ loading: true })
92
+ try {
93
+ const user = await authService.getCurrentUser()
94
+ set({
95
+ user,
96
+ loading: false,
97
+ error: null,
98
+ })
99
+ } catch (error) {
100
+ set({
101
+ user: null,
102
+ token: null,
103
+ loading: false,
104
+ error: null,
105
+ })
106
+ }
107
+ },
108
+
109
+ updateUser: (userData) => {
110
+ const { user } = get()
111
+ if (user) {
112
+ set({
113
+ user: { ...user, ...userData },
114
+ })
115
+ }
116
+ },
117
+
118
+ clearError: () => {
119
+ set({ error: null })
120
+ },
121
+ }),
122
+ {
123
+ name: 'auth-storage',
124
+ partialize: (state) => ({
125
+ token: state.token,
126
+ user: state.user,
127
+ }),
128
+ }
129
+ )
130
+ )
client/src/store/chatStore.ts ADDED
@@ -0,0 +1,285 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { create } from 'zustand'
2
+ import { Chat, Message } from '../../../shared/types'
3
+ import { chatService } from '../services/chatService'
4
+
5
+ interface ChatState {
6
+ chats: Chat[]
7
+ currentChat: Chat | null
8
+ messages: Record<string, Message[]>
9
+ typingUsers: Record<string, string[]>
10
+ onlineUsers: Set<string>
11
+ loading: boolean
12
+ error: string | null
13
+ }
14
+
15
+ interface ChatActions {
16
+ // Chat management
17
+ loadChats: () => Promise<void>
18
+ selectChat: (chatId: string) => void
19
+ createChat: (data: {
20
+ type: 'direct' | 'group'
21
+ name?: string
22
+ participantIds: string[]
23
+ }) => Promise<Chat>
24
+ updateChat: (chatId: string, data: Partial<Chat>) => Promise<void>
25
+ deleteChat: (chatId: string) => Promise<void>
26
+
27
+ // Message management
28
+ loadMessages: (chatId: string, page?: number) => Promise<void>
29
+ sendMessage: (chatId: string, content: string, attachments?: File[]) => Promise<void>
30
+ editMessage: (messageId: string, content: string) => Promise<void>
31
+ deleteMessage: (messageId: string) => Promise<void>
32
+ addReaction: (messageId: string, emoji: string) => Promise<void>
33
+ removeReaction: (messageId: string, emoji: string) => Promise<void>
34
+
35
+ // Real-time updates
36
+ addMessage: (message: Message) => void
37
+ updateMessage: (message: Message) => void
38
+ removeMessage: (messageId: string) => void
39
+ applyChatUpdate: (chat: Chat) => void
40
+
41
+ // Typing indicators
42
+ setTyping: (chatId: string, userId: string, isTyping: boolean) => void
43
+
44
+ // User status
45
+ setUserOnline: (userId: string, isOnline: boolean) => void
46
+
47
+ // Utility
48
+ clearError: () => void
49
+ reset: () => void
50
+ }
51
+
52
+ export const useChatStore = create<ChatState & ChatActions>((set, get) => ({
53
+ // State
54
+ chats: [],
55
+ currentChat: null,
56
+ messages: {},
57
+ typingUsers: {},
58
+ onlineUsers: new Set(),
59
+ loading: false,
60
+ error: null,
61
+
62
+ // Actions
63
+ loadChats: async () => {
64
+ set({ loading: true, error: null })
65
+ try {
66
+ const chats = await chatService.getChats()
67
+ set({ chats, loading: false })
68
+ } catch (error: any) {
69
+ set({ error: error.message, loading: false })
70
+ }
71
+ },
72
+
73
+ selectChat: (chatId: string) => {
74
+ const { chats } = get()
75
+ const chat = chats.find(c => c.id === chatId)
76
+ if (chat) {
77
+ set({ currentChat: chat })
78
+ // Load messages if not already loaded
79
+ if (!get().messages[chatId]) {
80
+ get().loadMessages(chatId)
81
+ }
82
+ }
83
+ },
84
+
85
+ createChat: async (data) => {
86
+ set({ loading: true, error: null })
87
+ try {
88
+ const chat = await chatService.createChat(data)
89
+ set(state => ({
90
+ chats: [chat, ...state.chats],
91
+ currentChat: chat,
92
+ loading: false
93
+ }))
94
+ return chat
95
+ } catch (error: any) {
96
+ set({ error: error.message, loading: false })
97
+ throw error
98
+ }
99
+ },
100
+
101
+ updateChat: async (chatId: string, data: Partial<Chat>) => {
102
+ try {
103
+ const updatedChat = await chatService.updateChat(chatId, data)
104
+ set(state => ({
105
+ chats: state.chats.map(chat =>
106
+ chat.id === chatId ? updatedChat : chat
107
+ ),
108
+ currentChat: state.currentChat?.id === chatId ? updatedChat : state.currentChat
109
+ }))
110
+ } catch (error: any) {
111
+ set({ error: error.message })
112
+ }
113
+ },
114
+
115
+ deleteChat: async (chatId: string) => {
116
+ try {
117
+ await chatService.deleteChat(chatId)
118
+ set(state => ({
119
+ chats: state.chats.filter(chat => chat.id !== chatId),
120
+ currentChat: state.currentChat?.id === chatId ? null : state.currentChat,
121
+ messages: Object.fromEntries(
122
+ Object.entries(state.messages).filter(([id]) => id !== chatId)
123
+ )
124
+ }))
125
+ } catch (error: any) {
126
+ set({ error: error.message })
127
+ }
128
+ },
129
+
130
+ loadMessages: async (chatId: string, page = 1) => {
131
+ set({ loading: true, error: null })
132
+ try {
133
+ const messages = await chatService.getMessages(chatId, page)
134
+ set(state => ({
135
+ messages: {
136
+ ...state.messages,
137
+ [chatId]: page === 1 ? messages : [...(state.messages[chatId] || []), ...messages]
138
+ },
139
+ loading: false
140
+ }))
141
+ } catch (error: any) {
142
+ set({ error: error.message, loading: false })
143
+ }
144
+ },
145
+
146
+ sendMessage: async (chatId: string, content: string, attachments?: File[]) => {
147
+ try {
148
+ await chatService.sendMessage(chatId, {
149
+ content,
150
+ type: 'text',
151
+ attachments
152
+ })
153
+ // Message will be added via socket event
154
+ } catch (error: any) {
155
+ set({ error: error.message })
156
+ }
157
+ },
158
+
159
+ editMessage: async (messageId: string, content: string) => {
160
+ try {
161
+ await chatService.editMessage(messageId, content)
162
+ // Message will be updated via socket event
163
+ } catch (error: any) {
164
+ set({ error: error.message })
165
+ }
166
+ },
167
+
168
+ deleteMessage: async (messageId: string) => {
169
+ try {
170
+ await chatService.deleteMessage(messageId)
171
+ // Message will be removed via socket event
172
+ } catch (error: any) {
173
+ set({ error: error.message })
174
+ }
175
+ },
176
+
177
+ addReaction: async (messageId: string, emoji: string) => {
178
+ try {
179
+ await chatService.addReaction(messageId, emoji)
180
+ // Reaction will be updated via socket event
181
+ } catch (error: any) {
182
+ set({ error: error.message })
183
+ }
184
+ },
185
+
186
+ removeReaction: async (messageId: string, emoji: string) => {
187
+ try {
188
+ await chatService.removeReaction(messageId, emoji)
189
+ // Reaction will be updated via socket event
190
+ } catch (error: any) {
191
+ set({ error: error.message })
192
+ }
193
+ },
194
+
195
+ // Real-time updates
196
+ addMessage: (message: Message) => {
197
+ set(state => ({
198
+ messages: {
199
+ ...state.messages,
200
+ [message.chatId]: [...(state.messages[message.chatId] || []), message]
201
+ },
202
+ chats: state.chats.map(chat =>
203
+ chat.id === message.chatId
204
+ ? { ...chat, lastMessage: message, updatedAt: new Date() }
205
+ : chat
206
+ )
207
+ }))
208
+ },
209
+
210
+ updateMessage: (message: Message) => {
211
+ set(state => ({
212
+ messages: {
213
+ ...state.messages,
214
+ [message.chatId]: (state.messages[message.chatId] || []).map(msg =>
215
+ msg.id === message.id ? message : msg
216
+ )
217
+ }
218
+ }))
219
+ },
220
+
221
+ removeMessage: (messageId: string) => {
222
+ set(state => {
223
+ const newMessages = { ...state.messages }
224
+ Object.keys(newMessages).forEach(chatId => {
225
+ newMessages[chatId] = newMessages[chatId].filter(msg => msg.id !== messageId)
226
+ })
227
+ return { messages: newMessages }
228
+ })
229
+ },
230
+
231
+ applyChatUpdate: (chat: Chat) => {
232
+ set(state => ({
233
+ chats: state.chats.map(c => c.id === chat.id ? chat : c),
234
+ currentChat: state.currentChat?.id === chat.id ? chat : state.currentChat
235
+ }))
236
+ },
237
+
238
+ setTyping: (chatId: string, userId: string, isTyping: boolean) => {
239
+ set(state => {
240
+ const typingUsers = { ...state.typingUsers }
241
+ const currentTyping = typingUsers[chatId] || []
242
+
243
+ if (isTyping) {
244
+ if (!currentTyping.includes(userId)) {
245
+ typingUsers[chatId] = [...currentTyping, userId]
246
+ }
247
+ } else {
248
+ typingUsers[chatId] = currentTyping.filter(id => id !== userId)
249
+ if (typingUsers[chatId].length === 0) {
250
+ delete typingUsers[chatId]
251
+ }
252
+ }
253
+
254
+ return { typingUsers }
255
+ })
256
+ },
257
+
258
+ setUserOnline: (userId: string, isOnline: boolean) => {
259
+ set(state => {
260
+ const onlineUsers = new Set(state.onlineUsers)
261
+ if (isOnline) {
262
+ onlineUsers.add(userId)
263
+ } else {
264
+ onlineUsers.delete(userId)
265
+ }
266
+ return { onlineUsers }
267
+ })
268
+ },
269
+
270
+ clearError: () => {
271
+ set({ error: null })
272
+ },
273
+
274
+ reset: () => {
275
+ set({
276
+ chats: [],
277
+ currentChat: null,
278
+ messages: {},
279
+ typingUsers: {},
280
+ onlineUsers: new Set(),
281
+ loading: false,
282
+ error: null
283
+ })
284
+ }
285
+ }))
client/src/vite-env.d.ts ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ /// <reference types="vite/client" />
2
+
3
+ interface ImportMetaEnv {
4
+ readonly VITE_API_URL?: string
5
+ readonly VITE_SOCKET_URL?: string
6
+ }
7
+
8
+ interface ImportMeta {
9
+ readonly env: ImportMetaEnv
10
+ }
client/tailwind.config.js ADDED
@@ -0,0 +1,76 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /** @type {import('tailwindcss').Config} */
2
+ export default {
3
+ darkMode: ["class"],
4
+ content: [
5
+ './pages/**/*.{ts,tsx}',
6
+ './components/**/*.{ts,tsx}',
7
+ './app/**/*.{ts,tsx}',
8
+ './src/**/*.{ts,tsx}',
9
+ ],
10
+ theme: {
11
+ container: {
12
+ center: true,
13
+ padding: "2rem",
14
+ screens: {
15
+ "2xl": "1400px",
16
+ },
17
+ },
18
+ extend: {
19
+ colors: {
20
+ border: "hsl(var(--border))",
21
+ input: "hsl(var(--input))",
22
+ ring: "hsl(var(--ring))",
23
+ background: "hsl(var(--background))",
24
+ foreground: "hsl(var(--foreground))",
25
+ primary: {
26
+ DEFAULT: "hsl(var(--primary))",
27
+ foreground: "hsl(var(--primary-foreground))",
28
+ },
29
+ secondary: {
30
+ DEFAULT: "hsl(var(--secondary))",
31
+ foreground: "hsl(var(--secondary-foreground))",
32
+ },
33
+ destructive: {
34
+ DEFAULT: "hsl(var(--destructive))",
35
+ foreground: "hsl(var(--destructive-foreground))",
36
+ },
37
+ muted: {
38
+ DEFAULT: "hsl(var(--muted))",
39
+ foreground: "hsl(var(--muted-foreground))",
40
+ },
41
+ accent: {
42
+ DEFAULT: "hsl(var(--accent))",
43
+ foreground: "hsl(var(--accent-foreground))",
44
+ },
45
+ popover: {
46
+ DEFAULT: "hsl(var(--popover))",
47
+ foreground: "hsl(var(--popover-foreground))",
48
+ },
49
+ card: {
50
+ DEFAULT: "hsl(var(--card))",
51
+ foreground: "hsl(var(--card-foreground))",
52
+ },
53
+ },
54
+ borderRadius: {
55
+ lg: "var(--radius)",
56
+ md: "calc(var(--radius) - 2px)",
57
+ sm: "calc(var(--radius) - 4px)",
58
+ },
59
+ keyframes: {
60
+ "accordion-down": {
61
+ from: { height: 0 },
62
+ to: { height: "var(--radix-accordion-content-height)" },
63
+ },
64
+ "accordion-up": {
65
+ from: { height: "var(--radix-accordion-content-height)" },
66
+ to: { height: 0 },
67
+ },
68
+ },
69
+ animation: {
70
+ "accordion-down": "accordion-down 0.2s ease-out",
71
+ "accordion-up": "accordion-up 0.2s ease-out",
72
+ },
73
+ },
74
+ },
75
+ plugins: [require("tailwindcss-animate")],
76
+ }
client/tsconfig.json ADDED
@@ -0,0 +1,31 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2020",
4
+ "useDefineForClassFields": true,
5
+ "lib": ["ES2020", "DOM", "DOM.Iterable"],
6
+ "module": "ESNext",
7
+ "skipLibCheck": true,
8
+
9
+ /* Bundler mode */
10
+ "moduleResolution": "bundler",
11
+ "allowImportingTsExtensions": true,
12
+ "resolveJsonModule": true,
13
+ "isolatedModules": true,
14
+ "noEmit": true,
15
+ "jsx": "react-jsx",
16
+
17
+ /* Linting */
18
+ "strict": true,
19
+ "noUnusedLocals": true,
20
+ "noUnusedParameters": true,
21
+ "noFallthroughCasesInSwitch": true,
22
+
23
+ /* Path mapping */
24
+ "baseUrl": ".",
25
+ "paths": {
26
+ "@/*": ["./src/*"]
27
+ }
28
+ },
29
+ "include": ["src"],
30
+ "references": [{ "path": "./tsconfig.node.json" }]
31
+ }
client/tsconfig.node.json ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "compilerOptions": {
3
+ "composite": true,
4
+ "skipLibCheck": true,
5
+ "module": "ESNext",
6
+ "moduleResolution": "bundler",
7
+ "allowSyntheticDefaultImports": true
8
+ },
9
+ "include": ["vite.config.ts"]
10
+ }
client/vite.config.ts ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { defineConfig } from 'vite'
2
+ import react from '@vitejs/plugin-react'
3
+ import path from 'path'
4
+
5
+ // https://vitejs.dev/config/
6
+ export default defineConfig({
7
+ plugins: [react()],
8
+ resolve: {
9
+ alias: {
10
+ "@": path.resolve(__dirname, "./src"),
11
+ },
12
+ },
13
+ server: {
14
+ port: 5173,
15
+ proxy: {
16
+ '/api': {
17
+ target: 'http://localhost:3001',
18
+ changeOrigin: true,
19
+ },
20
+ '/socket.io': {
21
+ target: 'http://localhost:3001',
22
+ changeOrigin: true,
23
+ ws: true,
24
+ },
25
+ },
26
+ },
27
+ })
package.json ADDED
@@ -0,0 +1,41 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "chat-app",
3
+ "version": "1.0.0",
4
+ "description": "A modern chat application with Telegram-like features",
5
+ "main": "index.js",
6
+ "scripts": {
7
+ "dev": "concurrently \"npm run dev:server\" \"npm run dev:client\"",
8
+ "dev:server": "cd server && npm run dev",
9
+ "dev:client": "cd client && npm run dev",
10
+ "build": "npm run build:client && npm run build:server",
11
+ "build:client": "cd client && npm run build",
12
+ "build:server": "cd server && npm run build",
13
+ "start": "cd server && npm run start",
14
+ "test": "npm run test:client && npm run test:server",
15
+ "test:client": "cd client && npm run test",
16
+ "test:server": "cd server && npm run test",
17
+ "lint": "npm run lint:client && npm run lint:server",
18
+ "lint:client": "cd client && npm run lint",
19
+ "lint:server": "cd server && npm run lint",
20
+ "install:all": "npm install && cd client && npm install && cd ../server && npm install"
21
+ },
22
+ "keywords": [
23
+ "chat",
24
+ "messaging",
25
+ "real-time",
26
+ "socket.io",
27
+ "react",
28
+ "node.js",
29
+ "typescript"
30
+ ],
31
+ "author": "Your Name",
32
+ "license": "MIT",
33
+ "devDependencies": {
34
+ "concurrently": "^8.2.2"
35
+ },
36
+ "workspaces": [
37
+ "client",
38
+ "server",
39
+ "shared"
40
+ ]
41
+ }
server/.env.example ADDED
@@ -0,0 +1,50 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Server Configuration
2
+ NODE_ENV=development
3
+ PORT=3001
4
+ HOST=localhost
5
+
6
+ # Database Configuration (Supabase)
7
+ DATABASE_URL=postgresql://username:password@localhost:5432/chatapp
8
+ SUPABASE_URL=your_supabase_url
9
+ SUPABASE_SERVICE_KEY=your_supabase_service_key
10
+
11
+ # JWT Configuration
12
+ JWT_SECRET=your_super_secret_jwt_key_here
13
+ JWT_EXPIRES_IN=7d
14
+ JWT_REFRESH_SECRET=your_refresh_token_secret
15
+ JWT_REFRESH_EXPIRES_IN=30d
16
+
17
+ # Redis Configuration (Optional - for caching and sessions)
18
+ REDIS_URL=redis://localhost:6379
19
+ REDIS_PASSWORD=
20
+
21
+ # File Upload Configuration
22
+ MAX_FILE_SIZE=10485760
23
+ ALLOWED_FILE_TYPES=image/jpeg,image/png,image/gif,image/webp,video/mp4,video/webm,audio/mp3,audio/wav,application/pdf,text/plain
24
+ UPLOAD_PATH=uploads
25
+
26
+ # Email Configuration (for notifications)
27
+ SMTP_HOST=smtp.gmail.com
28
+ SMTP_PORT=587
29
30
+ SMTP_PASS=your_app_password
31
32
+ FROM_NAME=ChatApp
33
+
34
+ # Rate Limiting
35
+ RATE_LIMIT_WINDOW_MS=900000
36
+ RATE_LIMIT_MAX_REQUESTS=100
37
+
38
+ # CORS Configuration
39
+ CORS_ORIGIN=http://localhost:5173
40
+
41
+ # Admin Configuration
42
43
+ ADMIN_PASSWORD=admin123456
44
+
45
+ # Logging
46
+ LOG_LEVEL=info
47
+ LOG_FILE=logs/app.log
48
+
49
+ # WebSocket Configuration
50
+ SOCKET_CORS_ORIGIN=http://localhost:5173
server/package.json ADDED
@@ -0,0 +1,70 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "chat-app-server",
3
+ "version": "1.0.0",
4
+ "description": "Chat application backend server",
5
+ "main": "dist/index.js",
6
+ "scripts": {
7
+ "dev": "nodemon src/index.ts",
8
+ "build": "tsc",
9
+ "start": "node dist/index.js",
10
+ "test": "jest",
11
+ "test:watch": "jest --watch",
12
+ "lint": "eslint src --ext .ts",
13
+ "lint:fix": "eslint src --ext .ts --fix",
14
+ "migrate": "npx prisma migrate dev",
15
+ "db:generate": "npx prisma generate",
16
+ "db:push": "npx prisma db push",
17
+ "db:studio": "npx prisma studio"
18
+ },
19
+ "keywords": [
20
+ "chat",
21
+ "socket.io",
22
+ "express",
23
+ "typescript",
24
+ "node.js"
25
+ ],
26
+ "author": "Your Name",
27
+ "license": "MIT",
28
+ "dependencies": {
29
+ "express": "^4.18.2",
30
+ "socket.io": "^4.7.4",
31
+ "cors": "^2.8.5",
32
+ "helmet": "^7.1.0",
33
+ "bcryptjs": "^2.4.3",
34
+ "jsonwebtoken": "^9.0.2",
35
+ "multer": "^1.4.5-lts.1",
36
+ "sharp": "^0.32.6",
37
+ "joi": "^17.11.0",
38
+ "dotenv": "^16.3.1",
39
+ "@supabase/supabase-js": "^2.38.4",
40
+ "prisma": "^5.6.0",
41
+ "@prisma/client": "^5.6.0",
42
+ "redis": "^4.6.10",
43
+ "nodemailer": "^6.9.7",
44
+ "express-rate-limit": "^7.1.5",
45
+ "compression": "^1.7.4",
46
+ "morgan": "^1.10.0",
47
+ "uuid": "^9.0.1"
48
+ },
49
+ "devDependencies": {
50
+ "@types/express": "^4.17.21",
51
+ "@types/cors": "^2.8.17",
52
+ "@types/bcryptjs": "^2.4.6",
53
+ "@types/jsonwebtoken": "^9.0.5",
54
+ "@types/multer": "^1.4.11",
55
+ "@types/node": "^20.9.0",
56
+ "@types/compression": "^1.7.5",
57
+ "@types/morgan": "^1.9.9",
58
+ "@types/uuid": "^9.0.7",
59
+ "@types/nodemailer": "^6.4.14",
60
+ "@typescript-eslint/eslint-plugin": "^6.10.0",
61
+ "@typescript-eslint/parser": "^6.10.0",
62
+ "eslint": "^8.53.0",
63
+ "jest": "^29.7.0",
64
+ "@types/jest": "^29.5.8",
65
+ "ts-jest": "^29.1.1",
66
+ "nodemon": "^3.0.1",
67
+ "ts-node": "^10.9.1",
68
+ "typescript": "^5.2.2"
69
+ }
70
+ }
server/prisma/schema.prisma ADDED
@@ -0,0 +1,209 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // This is your Prisma schema file,
2
+ // learn more about it in the docs: https://pris.ly/d/prisma-schema
3
+
4
+ generator client {
5
+ provider = "prisma-client-js"
6
+ }
7
+
8
+ datasource db {
9
+ provider = "postgresql"
10
+ url = env("DATABASE_URL")
11
+ }
12
+
13
+ model User {
14
+ id String @id @default(cuid())
15
+ email String @unique
16
+ username String @unique
17
+ displayName String
18
+ password String
19
+ avatar String?
20
+ bio String?
21
+ isOnline Boolean @default(false)
22
+ lastSeen DateTime @default(now())
23
+ isAdmin Boolean @default(false)
24
+ isVerified Boolean @default(false)
25
+ createdAt DateTime @default(now())
26
+ updatedAt DateTime @updatedAt
27
+
28
+ // Relations
29
+ sentMessages Message[] @relation("MessageSender")
30
+ chatParticipants ChatParticipant[]
31
+ messageReactions MessageReaction[]
32
+ ownedGroups Group[] @relation("GroupOwner")
33
+ notifications Notification[]
34
+ userSessions UserSession[]
35
+
36
+ @@map("users")
37
+ }
38
+
39
+ model Chat {
40
+ id String @id @default(cuid())
41
+ type ChatType @default(DIRECT)
42
+ name String?
43
+ description String?
44
+ avatar String?
45
+ createdAt DateTime @default(now())
46
+ updatedAt DateTime @updatedAt
47
+
48
+ // Relations
49
+ participants ChatParticipant[]
50
+ messages Message[]
51
+ group Group?
52
+
53
+ @@map("chats")
54
+ }
55
+
56
+ model ChatParticipant {
57
+ id String @id @default(cuid())
58
+ chatId String
59
+ userId String
60
+ role ParticipantRole @default(MEMBER)
61
+ permissions Json?
62
+ joinedAt DateTime @default(now())
63
+ leftAt DateTime?
64
+
65
+ // Relations
66
+ chat Chat @relation(fields: [chatId], references: [id], onDelete: Cascade)
67
+ user User @relation(fields: [userId], references: [id], onDelete: Cascade)
68
+
69
+ @@unique([chatId, userId])
70
+ @@map("chat_participants")
71
+ }
72
+
73
+ model Message {
74
+ id String @id @default(cuid())
75
+ chatId String
76
+ senderId String
77
+ content String
78
+ type MessageType @default(TEXT)
79
+ replyToId String?
80
+ isEdited Boolean @default(false)
81
+ isDeleted Boolean @default(false)
82
+ createdAt DateTime @default(now())
83
+ updatedAt DateTime @updatedAt
84
+
85
+ // Relations
86
+ chat Chat @relation(fields: [chatId], references: [id], onDelete: Cascade)
87
+ sender User @relation("MessageSender", fields: [senderId], references: [id], onDelete: Cascade)
88
+ replyTo Message? @relation("MessageReply", fields: [replyToId], references: [id])
89
+ replies Message[] @relation("MessageReply")
90
+ attachments MessageAttachment[]
91
+ reactions MessageReaction[]
92
+
93
+ @@map("messages")
94
+ }
95
+
96
+ model MessageAttachment {
97
+ id String @id @default(cuid())
98
+ messageId String
99
+ type String
100
+ name String
101
+ url String
102
+ size Int
103
+ mimeType String
104
+ thumbnail String?
105
+
106
+ // Relations
107
+ message Message @relation(fields: [messageId], references: [id], onDelete: Cascade)
108
+
109
+ @@map("message_attachments")
110
+ }
111
+
112
+ model MessageReaction {
113
+ id String @id @default(cuid())
114
+ messageId String
115
+ userId String
116
+ emoji String
117
+ createdAt DateTime @default(now())
118
+
119
+ // Relations
120
+ message Message @relation(fields: [messageId], references: [id], onDelete: Cascade)
121
+ user User @relation(fields: [userId], references: [id], onDelete: Cascade)
122
+
123
+ @@unique([messageId, userId, emoji])
124
+ @@map("message_reactions")
125
+ }
126
+
127
+ model Group {
128
+ id String @id @default(cuid())
129
+ chatId String @unique
130
+ name String
131
+ description String?
132
+ avatar String?
133
+ type GroupType @default(PRIVATE)
134
+ maxMembers Int @default(100)
135
+ ownerId String
136
+ settings Json?
137
+ createdAt DateTime @default(now())
138
+ updatedAt DateTime @updatedAt
139
+
140
+ // Relations
141
+ chat Chat @relation(fields: [chatId], references: [id], onDelete: Cascade)
142
+ owner User @relation("GroupOwner", fields: [ownerId], references: [id])
143
+
144
+ @@map("groups")
145
+ }
146
+
147
+ model Notification {
148
+ id String @id @default(cuid())
149
+ userId String
150
+ type NotificationType
151
+ title String
152
+ content String
153
+ data Json?
154
+ isRead Boolean @default(false)
155
+ createdAt DateTime @default(now())
156
+
157
+ // Relations
158
+ user User @relation(fields: [userId], references: [id], onDelete: Cascade)
159
+
160
+ @@map("notifications")
161
+ }
162
+
163
+ model UserSession {
164
+ id String @id @default(cuid())
165
+ userId String
166
+ token String @unique
167
+ userAgent String?
168
+ ipAddress String?
169
+ expiresAt DateTime
170
+ createdAt DateTime @default(now())
171
+
172
+ // Relations
173
+ user User @relation(fields: [userId], references: [id], onDelete: Cascade)
174
+
175
+ @@map("user_sessions")
176
+ }
177
+
178
+ // Enums
179
+ enum ChatType {
180
+ DIRECT
181
+ GROUP
182
+ }
183
+
184
+ enum ParticipantRole {
185
+ MEMBER
186
+ ADMIN
187
+ OWNER
188
+ }
189
+
190
+ enum MessageType {
191
+ TEXT
192
+ IMAGE
193
+ FILE
194
+ AUDIO
195
+ VIDEO
196
+ SYSTEM
197
+ }
198
+
199
+ enum GroupType {
200
+ PUBLIC
201
+ PRIVATE
202
+ }
203
+
204
+ enum NotificationType {
205
+ MESSAGE
206
+ MENTION
207
+ GROUP_INVITE
208
+ SYSTEM
209
+ }
server/src/config/database.ts ADDED
@@ -0,0 +1,69 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { PrismaClient } from '@prisma/client'
2
+
3
+ declare global {
4
+ var __prisma: PrismaClient | undefined
5
+ }
6
+
7
+ // Prevent multiple instances of Prisma Client in development
8
+ const prisma = globalThis.__prisma || new PrismaClient({
9
+ log: process.env.NODE_ENV === 'development' ? ['query', 'error', 'warn'] : ['error'],
10
+ })
11
+
12
+ if (process.env.NODE_ENV === 'development') {
13
+ globalThis.__prisma = prisma
14
+ }
15
+
16
+ export { prisma }
17
+
18
+ export async function initializeDatabase() {
19
+ try {
20
+ // Test the connection
21
+ await prisma.$connect()
22
+ console.log('✅ Database connected successfully')
23
+
24
+ // Run any initialization logic here
25
+ await createDefaultAdmin()
26
+
27
+ } catch (error) {
28
+ console.error('❌ Database connection failed:', error)
29
+ throw error
30
+ }
31
+ }
32
+
33
+ async function createDefaultAdmin() {
34
+ try {
35
+ const adminEmail = process.env.ADMIN_EMAIL || '[email protected]'
36
+ const adminPassword = process.env.ADMIN_PASSWORD || 'admin123456'
37
+
38
+ // Check if admin user already exists
39
+ const existingAdmin = await prisma.user.findUnique({
40
+ where: { email: adminEmail }
41
+ })
42
+
43
+ if (!existingAdmin) {
44
+ const bcrypt = await import('bcryptjs')
45
+ const hashedPassword = await bcrypt.hash(adminPassword, 12)
46
+
47
+ await prisma.user.create({
48
+ data: {
49
+ email: adminEmail,
50
+ username: 'admin',
51
+ displayName: 'Administrator',
52
+ password: hashedPassword,
53
+ isAdmin: true,
54
+ isVerified: true,
55
+ }
56
+ })
57
+
58
+ console.log('✅ Default admin user created')
59
+ console.log(`📧 Admin email: ${adminEmail}`)
60
+ console.log(`🔑 Admin password: ${adminPassword}`)
61
+ }
62
+ } catch (error) {
63
+ console.error('❌ Failed to create default admin:', error)
64
+ }
65
+ }
66
+
67
+ export async function disconnectDatabase() {
68
+ await prisma.$disconnect()
69
+ }
server/src/index.ts ADDED
@@ -0,0 +1,157 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import express from 'express'
2
+ import { createServer } from 'http'
3
+ import { Server } from 'socket.io'
4
+ import cors from 'cors'
5
+ import helmet from 'helmet'
6
+ import compression from 'compression'
7
+ import morgan from 'morgan'
8
+ import dotenv from 'dotenv'
9
+ import path from 'path'
10
+
11
+ // Load environment variables
12
+ dotenv.config()
13
+
14
+ // Import routes and middleware
15
+ import authRoutes from './routes/auth'
16
+ import userRoutes from './routes/users'
17
+ import chatRoutes from './routes/chats'
18
+ import messageRoutes from './routes/messages'
19
+ import uploadRoutes from './routes/upload'
20
+ import adminRoutes from './routes/admin'
21
+
22
+ // Import middleware
23
+ import { errorHandler } from './middleware/errorHandler'
24
+ import { rateLimiter } from './middleware/rateLimiter'
25
+ import { authMiddleware } from './middleware/auth'
26
+
27
+ // Import socket handlers
28
+ import { setupSocketHandlers } from './socket'
29
+
30
+ // Import database
31
+ import { initializeDatabase } from './config/database'
32
+
33
+ const app = express()
34
+ const server = createServer(app)
35
+
36
+ // Initialize Socket.IO
37
+ const io = new Server(server, {
38
+ cors: {
39
+ origin: process.env.CORS_ORIGIN || "http://localhost:5173",
40
+ methods: ["GET", "POST"],
41
+ credentials: true
42
+ },
43
+ transports: ['websocket', 'polling']
44
+ })
45
+
46
+ // Middleware
47
+ app.use(helmet({
48
+ crossOriginEmbedderPolicy: false,
49
+ contentSecurityPolicy: {
50
+ directives: {
51
+ defaultSrc: ["'self'"],
52
+ styleSrc: ["'self'", "'unsafe-inline'"],
53
+ scriptSrc: ["'self'"],
54
+ imgSrc: ["'self'", "data:", "https:"],
55
+ },
56
+ },
57
+ }))
58
+
59
+ app.use(cors({
60
+ origin: process.env.CORS_ORIGIN || "http://localhost:5173",
61
+ credentials: true
62
+ }))
63
+
64
+ app.use(compression())
65
+ app.use(morgan('combined'))
66
+ app.use(express.json({ limit: '10mb' }))
67
+ app.use(express.urlencoded({ extended: true, limit: '10mb' }))
68
+
69
+ // Rate limiting
70
+ app.use(rateLimiter)
71
+
72
+ // Static files
73
+ app.use('/uploads', express.static(path.join(__dirname, '../uploads')))
74
+
75
+ // Health check
76
+ app.get('/health', (req, res) => {
77
+ res.json({
78
+ status: 'OK',
79
+ timestamp: new Date().toISOString(),
80
+ uptime: process.uptime()
81
+ })
82
+ })
83
+
84
+ // API Routes
85
+ app.use('/api/auth', authRoutes)
86
+ app.use('/api/users', authMiddleware, userRoutes)
87
+ app.use('/api/chats', authMiddleware, chatRoutes)
88
+ app.use('/api/messages', authMiddleware, messageRoutes)
89
+ app.use('/api/upload', authMiddleware, uploadRoutes)
90
+ app.use('/api/admin', authMiddleware, adminRoutes)
91
+
92
+ // 404 handler
93
+ app.use('*', (req, res) => {
94
+ res.status(404).json({
95
+ success: false,
96
+ error: 'Route not found'
97
+ })
98
+ })
99
+
100
+ // Error handling middleware
101
+ app.use(errorHandler)
102
+
103
+ // Setup Socket.IO handlers
104
+ setupSocketHandlers(io)
105
+
106
+ // Initialize database and start server
107
+ async function startServer() {
108
+ try {
109
+ // Initialize database connection
110
+ await initializeDatabase()
111
+ console.log('Database connected successfully')
112
+
113
+ const PORT = process.env.PORT || 3001
114
+ const HOST = process.env.HOST || 'localhost'
115
+
116
+ server.listen(PORT, () => {
117
+ console.log(`🚀 Server running on http://${HOST}:${PORT}`)
118
+ console.log(`📡 Socket.IO server ready`)
119
+ console.log(`🌍 Environment: ${process.env.NODE_ENV || 'development'}`)
120
+ })
121
+ } catch (error) {
122
+ console.error('Failed to start server:', error)
123
+ process.exit(1)
124
+ }
125
+ }
126
+
127
+ // Graceful shutdown
128
+ process.on('SIGTERM', () => {
129
+ console.log('SIGTERM received, shutting down gracefully')
130
+ server.close(() => {
131
+ console.log('Server closed')
132
+ process.exit(0)
133
+ })
134
+ })
135
+
136
+ process.on('SIGINT', () => {
137
+ console.log('SIGINT received, shutting down gracefully')
138
+ server.close(() => {
139
+ console.log('Server closed')
140
+ process.exit(0)
141
+ })
142
+ })
143
+
144
+ // Handle uncaught exceptions
145
+ process.on('uncaughtException', (error) => {
146
+ console.error('Uncaught Exception:', error)
147
+ process.exit(1)
148
+ })
149
+
150
+ process.on('unhandledRejection', (reason, promise) => {
151
+ console.error('Unhandled Rejection at:', promise, 'reason:', reason)
152
+ process.exit(1)
153
+ })
154
+
155
+ startServer()
156
+
157
+ export { app, server, io }
server/src/middleware/auth.ts ADDED
@@ -0,0 +1,158 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Request, Response, NextFunction } from 'express'
2
+ import jwt from 'jsonwebtoken'
3
+ import { prisma } from '../config/database'
4
+
5
+ export interface AuthRequest extends Request {
6
+ // Use a loose type here to avoid dependency on generated Prisma types
7
+ user?: any
8
+ }
9
+
10
+ export const authMiddleware = async (
11
+ req: AuthRequest,
12
+ res: Response,
13
+ next: NextFunction
14
+ ) => {
15
+ try {
16
+ const authHeader = req.headers.authorization
17
+
18
+ if (!authHeader || !authHeader.startsWith('Bearer ')) {
19
+ return res.status(401).json({
20
+ success: false,
21
+ error: 'Access token required'
22
+ })
23
+ }
24
+
25
+ const token = authHeader.substring(7) // Remove 'Bearer ' prefix
26
+
27
+ if (!token) {
28
+ return res.status(401).json({
29
+ success: false,
30
+ error: 'Access token required'
31
+ })
32
+ }
33
+
34
+ // Verify JWT token
35
+ const decoded = jwt.verify(token, process.env.JWT_SECRET!) as { userId: string }
36
+
37
+ // Get user from database
38
+ const user = await prisma.user.findUnique({
39
+ where: { id: decoded.userId },
40
+ select: {
41
+ id: true,
42
+ email: true,
43
+ username: true,
44
+ displayName: true,
45
+ avatar: true,
46
+ bio: true,
47
+ isOnline: true,
48
+ lastSeen: true,
49
+ isAdmin: true,
50
+ isVerified: true,
51
+ createdAt: true,
52
+ updatedAt: true,
53
+ }
54
+ })
55
+
56
+ if (!user) {
57
+ return res.status(401).json({
58
+ success: false,
59
+ error: 'Invalid token - user not found'
60
+ })
61
+ }
62
+
63
+ // Attach user to request
64
+ req.user = user
65
+ next()
66
+ } catch (error) {
67
+ if (error instanceof jwt.JsonWebTokenError) {
68
+ return res.status(401).json({
69
+ success: false,
70
+ error: 'Invalid token'
71
+ })
72
+ }
73
+
74
+ if (error instanceof jwt.TokenExpiredError) {
75
+ return res.status(401).json({
76
+ success: false,
77
+ error: 'Token expired'
78
+ })
79
+ }
80
+
81
+ console.error('Auth middleware error:', error)
82
+ return res.status(500).json({
83
+ success: false,
84
+ error: 'Authentication failed'
85
+ })
86
+ }
87
+ }
88
+
89
+ export const adminMiddleware = (
90
+ req: AuthRequest,
91
+ res: Response,
92
+ next: NextFunction
93
+ ) => {
94
+ if (!req.user) {
95
+ return res.status(401).json({
96
+ success: false,
97
+ error: 'Authentication required'
98
+ })
99
+ }
100
+
101
+ if (!req.user.isAdmin) {
102
+ return res.status(403).json({
103
+ success: false,
104
+ error: 'Admin access required'
105
+ })
106
+ }
107
+
108
+ next()
109
+ }
110
+
111
+ export const optionalAuthMiddleware = async (
112
+ req: AuthRequest,
113
+ res: Response,
114
+ next: NextFunction
115
+ ) => {
116
+ try {
117
+ const authHeader = req.headers.authorization
118
+
119
+ if (!authHeader || !authHeader.startsWith('Bearer ')) {
120
+ return next()
121
+ }
122
+
123
+ const token = authHeader.substring(7)
124
+
125
+ if (!token) {
126
+ return next()
127
+ }
128
+
129
+ const decoded = jwt.verify(token, process.env.JWT_SECRET!) as { userId: string }
130
+
131
+ const user = await prisma.user.findUnique({
132
+ where: { id: decoded.userId },
133
+ select: {
134
+ id: true,
135
+ email: true,
136
+ username: true,
137
+ displayName: true,
138
+ avatar: true,
139
+ bio: true,
140
+ isOnline: true,
141
+ lastSeen: true,
142
+ isAdmin: true,
143
+ isVerified: true,
144
+ createdAt: true,
145
+ updatedAt: true,
146
+ }
147
+ })
148
+
149
+ if (user) {
150
+ req.user = user
151
+ }
152
+
153
+ next()
154
+ } catch (error) {
155
+ // Ignore auth errors for optional auth
156
+ next()
157
+ }
158
+ }
server/src/middleware/errorHandler.ts ADDED
@@ -0,0 +1,104 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Request, Response, NextFunction } from 'express'
2
+ import { Prisma } from '@prisma/client'
3
+
4
+ export interface AppError extends Error {
5
+ statusCode?: number
6
+ code?: string
7
+ }
8
+
9
+ export const errorHandler = (
10
+ error: AppError,
11
+ req: Request,
12
+ res: Response,
13
+ next: NextFunction
14
+ ) => {
15
+ console.error('Error:', error)
16
+
17
+ // Default error
18
+ let statusCode = error.statusCode || 500
19
+ let message = error.message || 'Internal server error'
20
+ let code = error.code || 'INTERNAL_ERROR'
21
+
22
+ // Prisma errors
23
+ if (error instanceof Prisma.PrismaClientKnownRequestError) {
24
+ switch (error.code) {
25
+ case 'P2002':
26
+ statusCode = 409
27
+ message = 'Resource already exists'
28
+ code = 'DUPLICATE_ERROR'
29
+ break
30
+ case 'P2025':
31
+ statusCode = 404
32
+ message = 'Resource not found'
33
+ code = 'NOT_FOUND'
34
+ break
35
+ case 'P2003':
36
+ statusCode = 400
37
+ message = 'Invalid reference'
38
+ code = 'INVALID_REFERENCE'
39
+ break
40
+ default:
41
+ statusCode = 400
42
+ message = 'Database error'
43
+ code = 'DATABASE_ERROR'
44
+ }
45
+ }
46
+
47
+ // Validation errors
48
+ if (error.name === 'ValidationError') {
49
+ statusCode = 400
50
+ code = 'VALIDATION_ERROR'
51
+ }
52
+
53
+ // JWT errors
54
+ if (error.name === 'JsonWebTokenError') {
55
+ statusCode = 401
56
+ message = 'Invalid token'
57
+ code = 'INVALID_TOKEN'
58
+ }
59
+
60
+ if (error.name === 'TokenExpiredError') {
61
+ statusCode = 401
62
+ message = 'Token expired'
63
+ code = 'TOKEN_EXPIRED'
64
+ }
65
+
66
+ // Multer errors (file upload)
67
+ if (error.code === 'LIMIT_FILE_SIZE') {
68
+ statusCode = 413
69
+ message = 'File too large'
70
+ code = 'FILE_TOO_LARGE'
71
+ }
72
+
73
+ if (error.code === 'LIMIT_FILE_COUNT') {
74
+ statusCode = 413
75
+ message = 'Too many files'
76
+ code = 'TOO_MANY_FILES'
77
+ }
78
+
79
+ // Don't expose internal errors in production
80
+ if (process.env.NODE_ENV === 'production' && statusCode === 500) {
81
+ message = 'Internal server error'
82
+ }
83
+
84
+ res.status(statusCode).json({
85
+ success: false,
86
+ error: message,
87
+ code,
88
+ ...(process.env.NODE_ENV === 'development' && { stack: error.stack })
89
+ })
90
+ }
91
+
92
+ export const notFoundHandler = (req: Request, res: Response) => {
93
+ res.status(404).json({
94
+ success: false,
95
+ error: 'Route not found',
96
+ code: 'NOT_FOUND'
97
+ })
98
+ }
99
+
100
+ export const asyncHandler = (fn: Function) => {
101
+ return (req: Request, res: Response, next: NextFunction) => {
102
+ Promise.resolve(fn(req, res, next)).catch(next)
103
+ }
104
+ }