@wakastellar/ui 2.1.0 → 2.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +71 -8
- package/dist/blocks/auth-2fa/index.d.ts +38 -0
- package/dist/blocks/chat-interface/index.d.ts +66 -0
- package/dist/blocks/checkout-flow/index.d.ts +76 -0
- package/dist/blocks/dashboard-kpi/index.d.ts +69 -0
- package/dist/blocks/deployment-dashboard/index.d.ts +68 -0
- package/dist/blocks/index.d.ts +7 -0
- package/dist/blocks/player-profile/index.d.ts +78 -0
- package/dist/cli/index.cjs +1324 -154
- package/dist/index.cjs.js +136 -134
- package/dist/index.es.js +17304 -15120
- package/package.json +1 -1
- package/src/blocks/auth-2fa/index.tsx +655 -0
- package/src/blocks/chat-interface/index.tsx +611 -0
- package/src/blocks/checkout-flow/index.tsx +771 -0
- package/src/blocks/dashboard-kpi/index.tsx +424 -0
- package/src/blocks/deployment-dashboard/index.tsx +609 -0
- package/src/blocks/index.ts +24 -0
- package/src/blocks/player-profile/index.tsx +541 -0
- package/src/components/language-selector/index.tsx +21 -11
|
@@ -0,0 +1,611 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import * as React from "react"
|
|
4
|
+
import { cn } from "../../utils"
|
|
5
|
+
import { Card, CardContent, CardHeader, CardTitle } from "../../components/card"
|
|
6
|
+
import { Button } from "../../components/button"
|
|
7
|
+
import { Input } from "../../components/input"
|
|
8
|
+
import { Avatar, AvatarFallback, AvatarImage } from "../../components/avatar"
|
|
9
|
+
import { Badge } from "../../components/badge"
|
|
10
|
+
import { ScrollArea } from "../../components/scroll-area"
|
|
11
|
+
import { Separator } from "../../components/separator"
|
|
12
|
+
import {
|
|
13
|
+
Send,
|
|
14
|
+
Paperclip,
|
|
15
|
+
Smile,
|
|
16
|
+
MoreVertical,
|
|
17
|
+
Phone,
|
|
18
|
+
Video,
|
|
19
|
+
Search,
|
|
20
|
+
Image,
|
|
21
|
+
File,
|
|
22
|
+
Mic,
|
|
23
|
+
Check,
|
|
24
|
+
CheckCheck,
|
|
25
|
+
Clock,
|
|
26
|
+
Circle,
|
|
27
|
+
Reply,
|
|
28
|
+
Forward,
|
|
29
|
+
Trash2,
|
|
30
|
+
Copy,
|
|
31
|
+
Pin,
|
|
32
|
+
Star,
|
|
33
|
+
} from "lucide-react"
|
|
34
|
+
|
|
35
|
+
// ============================================
|
|
36
|
+
// TYPES
|
|
37
|
+
// ============================================
|
|
38
|
+
|
|
39
|
+
export interface ChatUser {
|
|
40
|
+
id: string
|
|
41
|
+
name: string
|
|
42
|
+
avatar?: string
|
|
43
|
+
status?: "online" | "offline" | "away" | "busy"
|
|
44
|
+
lastSeen?: Date | string
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface ChatMessage {
|
|
48
|
+
id: string
|
|
49
|
+
content: string
|
|
50
|
+
senderId: string
|
|
51
|
+
timestamp: Date | string
|
|
52
|
+
status?: "sending" | "sent" | "delivered" | "read"
|
|
53
|
+
replyTo?: string
|
|
54
|
+
attachments?: ChatAttachment[]
|
|
55
|
+
reactions?: ChatReaction[]
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export interface ChatAttachment {
|
|
59
|
+
id: string
|
|
60
|
+
type: "image" | "file" | "audio"
|
|
61
|
+
name: string
|
|
62
|
+
url: string
|
|
63
|
+
size?: string
|
|
64
|
+
thumbnail?: string
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export interface ChatReaction {
|
|
68
|
+
emoji: string
|
|
69
|
+
userId: string
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export interface ChatConversation {
|
|
73
|
+
id: string
|
|
74
|
+
participants: ChatUser[]
|
|
75
|
+
lastMessage?: ChatMessage
|
|
76
|
+
unreadCount?: number
|
|
77
|
+
isPinned?: boolean
|
|
78
|
+
isGroup?: boolean
|
|
79
|
+
groupName?: string
|
|
80
|
+
groupAvatar?: string
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export interface ChatInterfaceProps {
|
|
84
|
+
/** Current user */
|
|
85
|
+
currentUser: ChatUser
|
|
86
|
+
/** List of conversations */
|
|
87
|
+
conversations?: ChatConversation[]
|
|
88
|
+
/** Currently selected conversation */
|
|
89
|
+
selectedConversation?: ChatConversation
|
|
90
|
+
/** Messages in selected conversation */
|
|
91
|
+
messages?: ChatMessage[]
|
|
92
|
+
/** Handler for sending message */
|
|
93
|
+
onSendMessage?: (content: string, attachments?: File[]) => void
|
|
94
|
+
/** Handler for selecting conversation */
|
|
95
|
+
onSelectConversation?: (conversation: ChatConversation) => void
|
|
96
|
+
/** Handler for typing indicator */
|
|
97
|
+
onTyping?: () => void
|
|
98
|
+
/** Users currently typing */
|
|
99
|
+
typingUsers?: ChatUser[]
|
|
100
|
+
/** Show sidebar */
|
|
101
|
+
showSidebar?: boolean
|
|
102
|
+
/** Custom className */
|
|
103
|
+
className?: string
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// ============================================
|
|
107
|
+
// SUBCOMPONENTS
|
|
108
|
+
// ============================================
|
|
109
|
+
|
|
110
|
+
const statusColors = {
|
|
111
|
+
online: "bg-green-500",
|
|
112
|
+
offline: "bg-gray-400",
|
|
113
|
+
away: "bg-yellow-500",
|
|
114
|
+
busy: "bg-red-500",
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function UserAvatar({ user, size = "md" }: { user: ChatUser; size?: "sm" | "md" | "lg" }) {
|
|
118
|
+
const sizeClasses = {
|
|
119
|
+
sm: "h-8 w-8",
|
|
120
|
+
md: "h-10 w-10",
|
|
121
|
+
lg: "h-12 w-12",
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return (
|
|
125
|
+
<div className="relative">
|
|
126
|
+
<Avatar className={sizeClasses[size]}>
|
|
127
|
+
<AvatarImage src={user.avatar} alt={user.name} />
|
|
128
|
+
<AvatarFallback>{user.name.slice(0, 2).toUpperCase()}</AvatarFallback>
|
|
129
|
+
</Avatar>
|
|
130
|
+
{user.status && (
|
|
131
|
+
<div
|
|
132
|
+
className={cn(
|
|
133
|
+
"absolute bottom-0 right-0 h-3 w-3 rounded-full border-2 border-background",
|
|
134
|
+
statusColors[user.status]
|
|
135
|
+
)}
|
|
136
|
+
/>
|
|
137
|
+
)}
|
|
138
|
+
</div>
|
|
139
|
+
)
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function ConversationItem({
|
|
143
|
+
conversation,
|
|
144
|
+
currentUserId,
|
|
145
|
+
isSelected,
|
|
146
|
+
onClick,
|
|
147
|
+
}: {
|
|
148
|
+
conversation: ChatConversation
|
|
149
|
+
currentUserId: string
|
|
150
|
+
isSelected: boolean
|
|
151
|
+
onClick: () => void
|
|
152
|
+
}) {
|
|
153
|
+
const otherParticipant = conversation.participants.find((p) => p.id !== currentUserId)
|
|
154
|
+
const displayName = conversation.isGroup ? conversation.groupName : otherParticipant?.name
|
|
155
|
+
const displayAvatar = conversation.isGroup ? conversation.groupAvatar : otherParticipant?.avatar
|
|
156
|
+
|
|
157
|
+
return (
|
|
158
|
+
<button
|
|
159
|
+
onClick={onClick}
|
|
160
|
+
className={cn(
|
|
161
|
+
"w-full flex items-center gap-3 p-3 rounded-lg transition-colors text-left",
|
|
162
|
+
isSelected ? "bg-primary/10" : "hover:bg-muted/50"
|
|
163
|
+
)}
|
|
164
|
+
>
|
|
165
|
+
<div className="relative">
|
|
166
|
+
<Avatar>
|
|
167
|
+
<AvatarImage src={displayAvatar} />
|
|
168
|
+
<AvatarFallback>{displayName?.slice(0, 2).toUpperCase()}</AvatarFallback>
|
|
169
|
+
</Avatar>
|
|
170
|
+
{!conversation.isGroup && otherParticipant?.status && (
|
|
171
|
+
<div
|
|
172
|
+
className={cn(
|
|
173
|
+
"absolute bottom-0 right-0 h-3 w-3 rounded-full border-2 border-background",
|
|
174
|
+
statusColors[otherParticipant.status]
|
|
175
|
+
)}
|
|
176
|
+
/>
|
|
177
|
+
)}
|
|
178
|
+
</div>
|
|
179
|
+
<div className="flex-1 min-w-0">
|
|
180
|
+
<div className="flex items-center justify-between">
|
|
181
|
+
<span className="font-medium truncate">{displayName}</span>
|
|
182
|
+
{conversation.lastMessage && (
|
|
183
|
+
<span className="text-xs text-muted-foreground">
|
|
184
|
+
{typeof conversation.lastMessage.timestamp === "string"
|
|
185
|
+
? conversation.lastMessage.timestamp
|
|
186
|
+
: formatTime(conversation.lastMessage.timestamp)}
|
|
187
|
+
</span>
|
|
188
|
+
)}
|
|
189
|
+
</div>
|
|
190
|
+
{conversation.lastMessage && (
|
|
191
|
+
<p className="text-sm text-muted-foreground truncate">
|
|
192
|
+
{conversation.lastMessage.content}
|
|
193
|
+
</p>
|
|
194
|
+
)}
|
|
195
|
+
</div>
|
|
196
|
+
{conversation.unreadCount && conversation.unreadCount > 0 && (
|
|
197
|
+
<Badge className="h-5 w-5 p-0 flex items-center justify-center rounded-full">
|
|
198
|
+
{conversation.unreadCount}
|
|
199
|
+
</Badge>
|
|
200
|
+
)}
|
|
201
|
+
{conversation.isPinned && (
|
|
202
|
+
<Pin className="h-3 w-3 text-muted-foreground" />
|
|
203
|
+
)}
|
|
204
|
+
</button>
|
|
205
|
+
)
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function MessageBubble({
|
|
209
|
+
message,
|
|
210
|
+
isOwn,
|
|
211
|
+
sender,
|
|
212
|
+
showAvatar,
|
|
213
|
+
}: {
|
|
214
|
+
message: ChatMessage
|
|
215
|
+
isOwn: boolean
|
|
216
|
+
sender?: ChatUser
|
|
217
|
+
showAvatar: boolean
|
|
218
|
+
}) {
|
|
219
|
+
const statusIcons = {
|
|
220
|
+
sending: <Clock className="h-3 w-3" />,
|
|
221
|
+
sent: <Check className="h-3 w-3" />,
|
|
222
|
+
delivered: <CheckCheck className="h-3 w-3" />,
|
|
223
|
+
read: <CheckCheck className="h-3 w-3 text-blue-500" />,
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
return (
|
|
227
|
+
<div className={cn("flex gap-2 max-w-[80%]", isOwn ? "ml-auto flex-row-reverse" : "")}>
|
|
228
|
+
{showAvatar && sender && !isOwn && (
|
|
229
|
+
<UserAvatar user={sender} size="sm" />
|
|
230
|
+
)}
|
|
231
|
+
{!showAvatar && !isOwn && <div className="w-8" />}
|
|
232
|
+
|
|
233
|
+
<div className="space-y-1">
|
|
234
|
+
{!isOwn && showAvatar && sender && (
|
|
235
|
+
<span className="text-xs text-muted-foreground ml-1">{sender.name}</span>
|
|
236
|
+
)}
|
|
237
|
+
|
|
238
|
+
<div
|
|
239
|
+
className={cn(
|
|
240
|
+
"rounded-2xl px-4 py-2",
|
|
241
|
+
isOwn
|
|
242
|
+
? "bg-primary text-primary-foreground rounded-br-md"
|
|
243
|
+
: "bg-muted rounded-bl-md"
|
|
244
|
+
)}
|
|
245
|
+
>
|
|
246
|
+
{message.replyTo && (
|
|
247
|
+
<div className={cn(
|
|
248
|
+
"text-xs mb-1 pb-1 border-b",
|
|
249
|
+
isOwn ? "border-primary-foreground/20" : "border-border"
|
|
250
|
+
)}>
|
|
251
|
+
<Reply className="h-3 w-3 inline mr-1" />
|
|
252
|
+
Reply to message
|
|
253
|
+
</div>
|
|
254
|
+
)}
|
|
255
|
+
|
|
256
|
+
<p className="text-sm whitespace-pre-wrap">{message.content}</p>
|
|
257
|
+
|
|
258
|
+
{message.attachments && message.attachments.length > 0 && (
|
|
259
|
+
<div className="mt-2 space-y-2">
|
|
260
|
+
{message.attachments.map((attachment) => (
|
|
261
|
+
<div key={attachment.id} className="flex items-center gap-2">
|
|
262
|
+
{attachment.type === "image" ? (
|
|
263
|
+
<Image className="h-4 w-4" />
|
|
264
|
+
) : (
|
|
265
|
+
<File className="h-4 w-4" />
|
|
266
|
+
)}
|
|
267
|
+
<span className="text-xs truncate">{attachment.name}</span>
|
|
268
|
+
</div>
|
|
269
|
+
))}
|
|
270
|
+
</div>
|
|
271
|
+
)}
|
|
272
|
+
</div>
|
|
273
|
+
|
|
274
|
+
<div className={cn("flex items-center gap-1 text-xs text-muted-foreground", isOwn ? "justify-end" : "")}>
|
|
275
|
+
<span>
|
|
276
|
+
{typeof message.timestamp === "string"
|
|
277
|
+
? message.timestamp
|
|
278
|
+
: formatTime(message.timestamp)}
|
|
279
|
+
</span>
|
|
280
|
+
{isOwn && message.status && statusIcons[message.status]}
|
|
281
|
+
</div>
|
|
282
|
+
|
|
283
|
+
{message.reactions && message.reactions.length > 0 && (
|
|
284
|
+
<div className="flex gap-1 mt-1">
|
|
285
|
+
{Array.from(new Set(message.reactions.map((r) => r.emoji))).map((emoji) => (
|
|
286
|
+
<span key={emoji} className="text-xs bg-muted rounded-full px-2 py-0.5">
|
|
287
|
+
{emoji} {message.reactions!.filter((r) => r.emoji === emoji).length}
|
|
288
|
+
</span>
|
|
289
|
+
))}
|
|
290
|
+
</div>
|
|
291
|
+
)}
|
|
292
|
+
</div>
|
|
293
|
+
</div>
|
|
294
|
+
)
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function TypingIndicator({ users }: { users: ChatUser[] }) {
|
|
298
|
+
if (users.length === 0) return null
|
|
299
|
+
|
|
300
|
+
const names = users.map((u) => u.name).join(", ")
|
|
301
|
+
|
|
302
|
+
return (
|
|
303
|
+
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
|
304
|
+
<div className="flex gap-1">
|
|
305
|
+
<Circle className="h-2 w-2 animate-bounce" style={{ animationDelay: "0ms" }} />
|
|
306
|
+
<Circle className="h-2 w-2 animate-bounce" style={{ animationDelay: "150ms" }} />
|
|
307
|
+
<Circle className="h-2 w-2 animate-bounce" style={{ animationDelay: "300ms" }} />
|
|
308
|
+
</div>
|
|
309
|
+
<span>{names} {users.length === 1 ? "is" : "are"} typing...</span>
|
|
310
|
+
</div>
|
|
311
|
+
)
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
function formatTime(date: Date): string {
|
|
315
|
+
return date.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" })
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// ============================================
|
|
319
|
+
// MAIN COMPONENT
|
|
320
|
+
// ============================================
|
|
321
|
+
|
|
322
|
+
export function ChatInterface({
|
|
323
|
+
currentUser,
|
|
324
|
+
conversations = [],
|
|
325
|
+
selectedConversation,
|
|
326
|
+
messages = [],
|
|
327
|
+
onSendMessage,
|
|
328
|
+
onSelectConversation,
|
|
329
|
+
onTyping,
|
|
330
|
+
typingUsers = [],
|
|
331
|
+
showSidebar = true,
|
|
332
|
+
className,
|
|
333
|
+
}: ChatInterfaceProps) {
|
|
334
|
+
const [inputValue, setInputValue] = React.useState("")
|
|
335
|
+
const [searchQuery, setSearchQuery] = React.useState("")
|
|
336
|
+
const messagesEndRef = React.useRef<HTMLDivElement>(null)
|
|
337
|
+
|
|
338
|
+
const scrollToBottom = () => {
|
|
339
|
+
messagesEndRef.current?.scrollIntoView({ behavior: "smooth" })
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
React.useEffect(() => {
|
|
343
|
+
scrollToBottom()
|
|
344
|
+
}, [messages])
|
|
345
|
+
|
|
346
|
+
const handleSend = () => {
|
|
347
|
+
if (!inputValue.trim() || !onSendMessage) return
|
|
348
|
+
onSendMessage(inputValue.trim())
|
|
349
|
+
setInputValue("")
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
const handleKeyDown = (e: React.KeyboardEvent) => {
|
|
353
|
+
if (e.key === "Enter" && !e.shiftKey) {
|
|
354
|
+
e.preventDefault()
|
|
355
|
+
handleSend()
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
const filteredConversations = conversations.filter((conv) => {
|
|
360
|
+
if (!searchQuery) return true
|
|
361
|
+
const otherParticipant = conv.participants.find((p) => p.id !== currentUser.id)
|
|
362
|
+
const name = conv.isGroup ? conv.groupName : otherParticipant?.name
|
|
363
|
+
return name?.toLowerCase().includes(searchQuery.toLowerCase())
|
|
364
|
+
})
|
|
365
|
+
|
|
366
|
+
const otherParticipant = selectedConversation?.participants.find((p) => p.id !== currentUser.id)
|
|
367
|
+
const chatTitle = selectedConversation?.isGroup
|
|
368
|
+
? selectedConversation.groupName
|
|
369
|
+
: otherParticipant?.name
|
|
370
|
+
|
|
371
|
+
// Group messages by sender for avatar display
|
|
372
|
+
const groupedMessages = messages.reduce((acc, message, index) => {
|
|
373
|
+
const prevMessage = messages[index - 1]
|
|
374
|
+
const showAvatar = !prevMessage || prevMessage.senderId !== message.senderId
|
|
375
|
+
acc.push({ message, showAvatar })
|
|
376
|
+
return acc
|
|
377
|
+
}, [] as { message: ChatMessage; showAvatar: boolean }[])
|
|
378
|
+
|
|
379
|
+
return (
|
|
380
|
+
<div className={cn("flex h-[600px] rounded-lg border overflow-hidden", className)}>
|
|
381
|
+
{/* Sidebar */}
|
|
382
|
+
{showSidebar && (
|
|
383
|
+
<div className="w-80 border-r flex flex-col bg-card">
|
|
384
|
+
{/* Sidebar Header */}
|
|
385
|
+
<div className="p-4 border-b">
|
|
386
|
+
<div className="flex items-center justify-between mb-4">
|
|
387
|
+
<h2 className="font-semibold text-lg">Messages</h2>
|
|
388
|
+
<Button variant="ghost" size="icon">
|
|
389
|
+
<MoreVertical className="h-4 w-4" />
|
|
390
|
+
</Button>
|
|
391
|
+
</div>
|
|
392
|
+
<div className="relative">
|
|
393
|
+
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
|
|
394
|
+
<Input
|
|
395
|
+
placeholder="Search conversations..."
|
|
396
|
+
value={searchQuery}
|
|
397
|
+
onChange={(e) => setSearchQuery(e.target.value)}
|
|
398
|
+
className="pl-9"
|
|
399
|
+
/>
|
|
400
|
+
</div>
|
|
401
|
+
</div>
|
|
402
|
+
|
|
403
|
+
{/* Conversations List */}
|
|
404
|
+
<ScrollArea className="flex-1">
|
|
405
|
+
<div className="p-2 space-y-1">
|
|
406
|
+
{filteredConversations.map((conversation) => (
|
|
407
|
+
<ConversationItem
|
|
408
|
+
key={conversation.id}
|
|
409
|
+
conversation={conversation}
|
|
410
|
+
currentUserId={currentUser.id}
|
|
411
|
+
isSelected={selectedConversation?.id === conversation.id}
|
|
412
|
+
onClick={() => onSelectConversation?.(conversation)}
|
|
413
|
+
/>
|
|
414
|
+
))}
|
|
415
|
+
</div>
|
|
416
|
+
</ScrollArea>
|
|
417
|
+
</div>
|
|
418
|
+
)}
|
|
419
|
+
|
|
420
|
+
{/* Chat Area */}
|
|
421
|
+
<div className="flex-1 flex flex-col">
|
|
422
|
+
{selectedConversation ? (
|
|
423
|
+
<>
|
|
424
|
+
{/* Chat Header */}
|
|
425
|
+
<div className="h-16 border-b flex items-center justify-between px-4 bg-card">
|
|
426
|
+
<div className="flex items-center gap-3">
|
|
427
|
+
{otherParticipant && <UserAvatar user={otherParticipant} />}
|
|
428
|
+
<div>
|
|
429
|
+
<h3 className="font-medium">{chatTitle}</h3>
|
|
430
|
+
{otherParticipant?.status && (
|
|
431
|
+
<p className="text-xs text-muted-foreground capitalize">
|
|
432
|
+
{otherParticipant.status === "online"
|
|
433
|
+
? "Online"
|
|
434
|
+
: `Last seen ${otherParticipant.lastSeen || "recently"}`}
|
|
435
|
+
</p>
|
|
436
|
+
)}
|
|
437
|
+
</div>
|
|
438
|
+
</div>
|
|
439
|
+
<div className="flex items-center gap-1">
|
|
440
|
+
<Button variant="ghost" size="icon">
|
|
441
|
+
<Phone className="h-4 w-4" />
|
|
442
|
+
</Button>
|
|
443
|
+
<Button variant="ghost" size="icon">
|
|
444
|
+
<Video className="h-4 w-4" />
|
|
445
|
+
</Button>
|
|
446
|
+
<Button variant="ghost" size="icon">
|
|
447
|
+
<MoreVertical className="h-4 w-4" />
|
|
448
|
+
</Button>
|
|
449
|
+
</div>
|
|
450
|
+
</div>
|
|
451
|
+
|
|
452
|
+
{/* Messages Area */}
|
|
453
|
+
<ScrollArea className="flex-1 p-4">
|
|
454
|
+
<div className="space-y-4">
|
|
455
|
+
{groupedMessages.map(({ message, showAvatar }) => (
|
|
456
|
+
<MessageBubble
|
|
457
|
+
key={message.id}
|
|
458
|
+
message={message}
|
|
459
|
+
isOwn={message.senderId === currentUser.id}
|
|
460
|
+
sender={selectedConversation.participants.find(
|
|
461
|
+
(p) => p.id === message.senderId
|
|
462
|
+
)}
|
|
463
|
+
showAvatar={showAvatar}
|
|
464
|
+
/>
|
|
465
|
+
))}
|
|
466
|
+
<div ref={messagesEndRef} />
|
|
467
|
+
</div>
|
|
468
|
+
{typingUsers.length > 0 && (
|
|
469
|
+
<div className="mt-4">
|
|
470
|
+
<TypingIndicator users={typingUsers} />
|
|
471
|
+
</div>
|
|
472
|
+
)}
|
|
473
|
+
</ScrollArea>
|
|
474
|
+
|
|
475
|
+
{/* Input Area */}
|
|
476
|
+
<div className="p-4 border-t bg-card">
|
|
477
|
+
<div className="flex items-end gap-2">
|
|
478
|
+
<Button variant="ghost" size="icon">
|
|
479
|
+
<Paperclip className="h-4 w-4" />
|
|
480
|
+
</Button>
|
|
481
|
+
<div className="flex-1">
|
|
482
|
+
<Input
|
|
483
|
+
placeholder="Type a message..."
|
|
484
|
+
value={inputValue}
|
|
485
|
+
onChange={(e) => {
|
|
486
|
+
setInputValue(e.target.value)
|
|
487
|
+
onTyping?.()
|
|
488
|
+
}}
|
|
489
|
+
onKeyDown={handleKeyDown}
|
|
490
|
+
className="rounded-full"
|
|
491
|
+
/>
|
|
492
|
+
</div>
|
|
493
|
+
<Button variant="ghost" size="icon">
|
|
494
|
+
<Smile className="h-4 w-4" />
|
|
495
|
+
</Button>
|
|
496
|
+
<Button size="icon" onClick={handleSend} disabled={!inputValue.trim()}>
|
|
497
|
+
<Send className="h-4 w-4" />
|
|
498
|
+
</Button>
|
|
499
|
+
</div>
|
|
500
|
+
</div>
|
|
501
|
+
</>
|
|
502
|
+
) : (
|
|
503
|
+
/* No Conversation Selected */
|
|
504
|
+
<div className="flex-1 flex items-center justify-center">
|
|
505
|
+
<div className="text-center">
|
|
506
|
+
<div className="h-16 w-16 rounded-full bg-muted flex items-center justify-center mx-auto mb-4">
|
|
507
|
+
<Send className="h-8 w-8 text-muted-foreground" />
|
|
508
|
+
</div>
|
|
509
|
+
<h3 className="font-medium text-lg">Your Messages</h3>
|
|
510
|
+
<p className="text-muted-foreground text-sm mt-1">
|
|
511
|
+
Select a conversation to start chatting
|
|
512
|
+
</p>
|
|
513
|
+
</div>
|
|
514
|
+
</div>
|
|
515
|
+
)}
|
|
516
|
+
</div>
|
|
517
|
+
</div>
|
|
518
|
+
)
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
// ============================================
|
|
522
|
+
// PRESET DATA
|
|
523
|
+
// ============================================
|
|
524
|
+
|
|
525
|
+
export const defaultUsers: ChatUser[] = [
|
|
526
|
+
{ id: "1", name: "John Doe", avatar: "", status: "online" },
|
|
527
|
+
{ id: "2", name: "Jane Smith", avatar: "", status: "away", lastSeen: "2 hours ago" },
|
|
528
|
+
{ id: "3", name: "Bob Wilson", avatar: "", status: "offline", lastSeen: "yesterday" },
|
|
529
|
+
{ id: "4", name: "Alice Brown", avatar: "", status: "busy" },
|
|
530
|
+
]
|
|
531
|
+
|
|
532
|
+
export const defaultConversations: ChatConversation[] = [
|
|
533
|
+
{
|
|
534
|
+
id: "conv1",
|
|
535
|
+
participants: [defaultUsers[0], defaultUsers[1]],
|
|
536
|
+
lastMessage: {
|
|
537
|
+
id: "m1",
|
|
538
|
+
content: "Hey, how's the project going?",
|
|
539
|
+
senderId: "2",
|
|
540
|
+
timestamp: new Date(),
|
|
541
|
+
status: "read",
|
|
542
|
+
},
|
|
543
|
+
unreadCount: 2,
|
|
544
|
+
isPinned: true,
|
|
545
|
+
},
|
|
546
|
+
{
|
|
547
|
+
id: "conv2",
|
|
548
|
+
participants: [defaultUsers[0], defaultUsers[2]],
|
|
549
|
+
lastMessage: {
|
|
550
|
+
id: "m2",
|
|
551
|
+
content: "See you tomorrow!",
|
|
552
|
+
senderId: "1",
|
|
553
|
+
timestamp: new Date(Date.now() - 3600000),
|
|
554
|
+
status: "delivered",
|
|
555
|
+
},
|
|
556
|
+
},
|
|
557
|
+
{
|
|
558
|
+
id: "conv3",
|
|
559
|
+
isGroup: true,
|
|
560
|
+
groupName: "Project Team",
|
|
561
|
+
participants: [defaultUsers[0], defaultUsers[1], defaultUsers[2], defaultUsers[3]],
|
|
562
|
+
lastMessage: {
|
|
563
|
+
id: "m3",
|
|
564
|
+
content: "Meeting at 3pm",
|
|
565
|
+
senderId: "4",
|
|
566
|
+
timestamp: new Date(Date.now() - 7200000),
|
|
567
|
+
status: "read",
|
|
568
|
+
},
|
|
569
|
+
unreadCount: 5,
|
|
570
|
+
},
|
|
571
|
+
]
|
|
572
|
+
|
|
573
|
+
export const defaultMessages: ChatMessage[] = [
|
|
574
|
+
{
|
|
575
|
+
id: "msg1",
|
|
576
|
+
content: "Hey! How are you doing?",
|
|
577
|
+
senderId: "2",
|
|
578
|
+
timestamp: new Date(Date.now() - 3600000),
|
|
579
|
+
status: "read",
|
|
580
|
+
},
|
|
581
|
+
{
|
|
582
|
+
id: "msg2",
|
|
583
|
+
content: "I'm doing great, thanks for asking! Just finished the new feature.",
|
|
584
|
+
senderId: "1",
|
|
585
|
+
timestamp: new Date(Date.now() - 3500000),
|
|
586
|
+
status: "read",
|
|
587
|
+
},
|
|
588
|
+
{
|
|
589
|
+
id: "msg3",
|
|
590
|
+
content: "That's awesome! Can you show me a demo?",
|
|
591
|
+
senderId: "2",
|
|
592
|
+
timestamp: new Date(Date.now() - 3400000),
|
|
593
|
+
status: "read",
|
|
594
|
+
},
|
|
595
|
+
{
|
|
596
|
+
id: "msg4",
|
|
597
|
+
content: "Sure! Let me schedule a call for tomorrow. Does 2pm work for you?",
|
|
598
|
+
senderId: "1",
|
|
599
|
+
timestamp: new Date(Date.now() - 3300000),
|
|
600
|
+
status: "delivered",
|
|
601
|
+
},
|
|
602
|
+
{
|
|
603
|
+
id: "msg5",
|
|
604
|
+
content: "Perfect, 2pm works great!",
|
|
605
|
+
senderId: "2",
|
|
606
|
+
timestamp: new Date(Date.now() - 3200000),
|
|
607
|
+
status: "read",
|
|
608
|
+
},
|
|
609
|
+
]
|
|
610
|
+
|
|
611
|
+
export default ChatInterface
|