817 lines
34 KiB
TypeScript
817 lines
34 KiB
TypeScript
import React, { useState, useRef, useEffect, useMemo } from 'react';
|
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from './ui/card';
|
|
import { Button } from './ui/button';
|
|
import { Input } from './ui/input';
|
|
import { Avatar, AvatarFallback } from './ui/avatar';
|
|
import { Badge } from './ui/badge';
|
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from './ui/tabs';
|
|
import { ScrollArea } from './ui/scroll-area';
|
|
import { Separator } from './ui/separator';
|
|
import { Textarea } from './ui/textarea';
|
|
import {
|
|
ArrowLeft,
|
|
Send,
|
|
Smile,
|
|
Paperclip,
|
|
Users,
|
|
FileText,
|
|
Download,
|
|
Eye,
|
|
MoreHorizontal,
|
|
MessageSquare,
|
|
Clock,
|
|
CheckCircle,
|
|
AlertCircle,
|
|
Search,
|
|
Hash,
|
|
AtSign,
|
|
Phone,
|
|
Video,
|
|
Settings,
|
|
Pin,
|
|
Share,
|
|
Archive,
|
|
Plus,
|
|
Filter,
|
|
Calendar,
|
|
Zap,
|
|
Activity,
|
|
Bell,
|
|
Star,
|
|
Flag,
|
|
X
|
|
} from 'lucide-react';
|
|
|
|
interface Message {
|
|
id: string;
|
|
user: {
|
|
name: string;
|
|
avatar: string;
|
|
role: string;
|
|
};
|
|
content: string;
|
|
timestamp: string;
|
|
mentions?: string[];
|
|
isSystem?: boolean;
|
|
attachments?: {
|
|
name: string;
|
|
url: string;
|
|
type: string;
|
|
}[];
|
|
reactions?: {
|
|
emoji: string;
|
|
users: string[];
|
|
}[];
|
|
isHighPriority?: boolean;
|
|
}
|
|
|
|
interface Participant {
|
|
name: string;
|
|
avatar: string;
|
|
role: string;
|
|
status: 'online' | 'away' | 'offline';
|
|
email: string;
|
|
lastSeen?: string;
|
|
permissions: string[];
|
|
}
|
|
|
|
interface WorkNoteViewProps {
|
|
requestId: string;
|
|
onBack?: () => void;
|
|
}
|
|
|
|
// Get request data from the same source as RequestDetail
|
|
const REQUEST_DATABASE = {
|
|
'RE-REQ-001': {
|
|
id: 'RE-REQ-001',
|
|
title: 'Marketing Campaign Budget Approval',
|
|
department: 'Marketing',
|
|
priority: 'high',
|
|
status: 'pending'
|
|
},
|
|
'RE-REQ-002': {
|
|
id: 'RE-REQ-002',
|
|
title: 'IT Equipment Purchase',
|
|
department: 'IT',
|
|
priority: 'medium',
|
|
status: 'in-review'
|
|
}
|
|
};
|
|
|
|
// Static data to prevent re-renders
|
|
const MOCK_PARTICIPANTS: Participant[] = [
|
|
{
|
|
name: 'Sarah Chen',
|
|
avatar: 'SC',
|
|
role: 'Initiator',
|
|
status: 'online',
|
|
email: 'sarah.chen@royalenfield.com',
|
|
permissions: ['read', 'write', 'mention']
|
|
},
|
|
{
|
|
name: 'Mike Johnson',
|
|
avatar: 'MJ',
|
|
role: 'Team Lead',
|
|
status: 'online',
|
|
email: 'mike.johnson@royalenfield.com',
|
|
permissions: ['read', 'write', 'mention', 'approve']
|
|
},
|
|
{
|
|
name: 'Lisa Wong',
|
|
avatar: 'LW',
|
|
role: 'Finance Manager',
|
|
status: 'away',
|
|
email: 'lisa.wong@royalenfield.com',
|
|
lastSeen: '5 minutes ago',
|
|
permissions: ['read', 'write', 'mention', 'approve']
|
|
},
|
|
{
|
|
name: 'Anna Smith',
|
|
avatar: 'AS',
|
|
role: 'Spectator',
|
|
status: 'online',
|
|
email: 'anna.smith@royalenfield.com',
|
|
permissions: ['read', 'write', 'mention']
|
|
},
|
|
{
|
|
name: 'John Doe',
|
|
avatar: 'JD',
|
|
role: 'Spectator',
|
|
status: 'offline',
|
|
email: 'john.doe@royalenfield.com',
|
|
lastSeen: '2 hours ago',
|
|
permissions: ['read']
|
|
},
|
|
{
|
|
name: 'Emily Davis',
|
|
avatar: 'ED',
|
|
role: 'Creative Director',
|
|
status: 'online',
|
|
email: 'emily.davis@royalenfield.com',
|
|
permissions: ['read', 'write', 'mention']
|
|
}
|
|
];
|
|
|
|
const MOCK_DOCUMENTS = [
|
|
{
|
|
name: 'Q4_Marketing_Strategy.pdf',
|
|
size: '2.4 MB',
|
|
uploadedBy: 'Sarah Chen',
|
|
uploadedAt: '2024-10-05 14:32',
|
|
type: 'PDF',
|
|
url: '#'
|
|
},
|
|
{
|
|
name: 'Budget_Breakdown.xlsx',
|
|
size: '1.1 MB',
|
|
uploadedBy: 'Sarah Chen',
|
|
uploadedAt: '2024-10-05 14:35',
|
|
type: 'Excel',
|
|
url: '#'
|
|
},
|
|
{
|
|
name: 'Previous_Campaign_ROI.pdf',
|
|
size: '856 KB',
|
|
uploadedBy: 'Anna Smith',
|
|
uploadedAt: '2024-10-06 09:15',
|
|
type: 'PDF',
|
|
url: '#'
|
|
},
|
|
{
|
|
name: 'Competitor_Analysis.pptx',
|
|
size: '3.2 MB',
|
|
uploadedBy: 'Emily Davis',
|
|
uploadedAt: '2024-10-06 15:22',
|
|
type: 'PowerPoint',
|
|
url: '#'
|
|
}
|
|
];
|
|
|
|
const INITIAL_MESSAGES: Message[] = [
|
|
{
|
|
id: '1',
|
|
user: { name: 'Sarah Chen', avatar: 'SC', role: 'Initiator' },
|
|
content: 'Hi everyone! I\'ve submitted the marketing campaign budget request for Q4. Please review the attached documents and let me know if you need any additional information.',
|
|
timestamp: '2024-10-05 14:30',
|
|
isSystem: false,
|
|
reactions: [
|
|
{ emoji: '👍', users: ['Mike Johnson', 'Anna Smith'] },
|
|
{ emoji: '📋', users: ['Lisa Wong'] }
|
|
]
|
|
},
|
|
{
|
|
id: '2',
|
|
user: { name: 'System', avatar: 'SY', role: 'System' },
|
|
content: 'Request RE-REQ-001 has been created and assigned to Mike Johnson for initial review.',
|
|
timestamp: '2024-10-05 14:31',
|
|
isSystem: true
|
|
},
|
|
{
|
|
id: '3',
|
|
user: { name: 'Anna Smith', avatar: 'AS', role: 'Spectator' },
|
|
content: 'I\'ve added the previous campaign ROI data to help with the decision. The numbers show a 285% ROI from our last similar campaign. @Mike Johnson @Lisa Wong please check it out when you have a moment.',
|
|
timestamp: '2024-10-06 09:15',
|
|
mentions: ['Mike Johnson', 'Lisa Wong'],
|
|
attachments: [
|
|
{ name: 'Previous_Campaign_ROI.pdf', url: '#', type: 'pdf' }
|
|
]
|
|
},
|
|
{
|
|
id: '4',
|
|
user: { name: 'Mike Johnson', avatar: 'MJ', role: 'Team Lead' },
|
|
content: 'Thanks @Anna Smith! The historical data is very helpful. After reviewing the strategy document and budget breakdown, I believe this campaign is well-planned and has strong potential. I\'m approving this and forwarding to Finance for final review.',
|
|
timestamp: '2024-10-06 10:30',
|
|
mentions: ['Anna Smith'],
|
|
reactions: [
|
|
{ emoji: '✅', users: ['Sarah Chen', 'Anna Smith'] }
|
|
]
|
|
},
|
|
{
|
|
id: '5',
|
|
user: { name: 'System', avatar: 'SY', role: 'System' },
|
|
content: 'Request approved by Mike Johnson and forwarded to Lisa Wong for finance review.',
|
|
timestamp: '2024-10-06 10:31',
|
|
isSystem: true
|
|
},
|
|
{
|
|
id: '6',
|
|
user: { name: 'Emily Davis', avatar: 'ED', role: 'Creative Director' },
|
|
content: 'Great work on the strategy @Sarah Chen! I\'ve also added our competitor analysis to provide more context. The creative assets timeline looks achievable.',
|
|
timestamp: '2024-10-06 15:22',
|
|
mentions: ['Sarah Chen'],
|
|
attachments: [
|
|
{ name: 'Competitor_Analysis.pptx', url: '#', type: 'pptx' }
|
|
]
|
|
},
|
|
{
|
|
id: '7',
|
|
user: { name: 'Lisa Wong', avatar: 'LW', role: 'Finance Manager' },
|
|
content: 'I\'m currently reviewing the budget allocation and comparing it with Q3 spending. @Sarah Chen can you clarify the expected timeline for the LinkedIn ads campaign? Also, do we have approval from legal for the content strategy?',
|
|
timestamp: '2024-10-07 14:20',
|
|
mentions: ['Sarah Chen'],
|
|
isHighPriority: true
|
|
},
|
|
{
|
|
id: '8',
|
|
user: { name: 'Sarah Chen', avatar: 'SC', role: 'Initiator' },
|
|
content: 'Hi @Lisa Wong! For the LinkedIn campaign:\n\n• Launch: November 1st\n• Duration: 8 weeks\n• Budget distribution: 40% first 4 weeks, 60% last 4 weeks\n\nRegarding legal approval - I\'ll coordinate with the legal team this week. The content strategy follows our established brand guidelines.',
|
|
timestamp: '2024-10-07 15:45',
|
|
mentions: ['Lisa Wong']
|
|
}
|
|
];
|
|
|
|
// Utility functions
|
|
const getStatusColor = (status: string) => {
|
|
switch (status) {
|
|
case 'online': return 'bg-green-500';
|
|
case 'away': return 'bg-yellow-500';
|
|
case 'offline': return 'bg-gray-400';
|
|
default: return 'bg-gray-400';
|
|
}
|
|
};
|
|
|
|
const getStatusText = (status: string) => {
|
|
switch (status) {
|
|
case 'online': return 'Online';
|
|
case 'away': return 'Away';
|
|
case 'offline': return 'Offline';
|
|
default: return 'Unknown';
|
|
}
|
|
};
|
|
|
|
const formatMessage = (content: string) => {
|
|
// Enhanced mention highlighting with better regex
|
|
return content
|
|
.replace(/@([\w\s]+)(?=\s|$|[.,!?])/g, '<span class="inline-flex items-center px-2 py-1 rounded-md bg-blue-100 text-blue-800 font-medium text-sm">@$1</span>')
|
|
.replace(/\n/g, '<br />');
|
|
};
|
|
|
|
const getFileIcon = (type: string) => {
|
|
switch (type.toLowerCase()) {
|
|
case 'pdf': return '📄';
|
|
case 'excel': case 'xlsx': return '📊';
|
|
case 'powerpoint': case 'pptx': return '📊';
|
|
case 'word': case 'docx': return '📝';
|
|
case 'image': case 'png': case 'jpg': case 'jpeg': return '🖼️';
|
|
default: return '📎';
|
|
}
|
|
};
|
|
|
|
export function WorkNoteView({ requestId, onBack }: WorkNoteViewProps) {
|
|
const [message, setMessage] = useState('');
|
|
const [isTyping, setIsTyping] = useState(false);
|
|
const [activeTab, setActiveTab] = useState('chat');
|
|
const [searchTerm, setSearchTerm] = useState('');
|
|
const [showEmojiPicker, setShowEmojiPicker] = useState(false);
|
|
const [messages, setMessages] = useState<Message[]>(INITIAL_MESSAGES);
|
|
const [showSidebar, setShowSidebar] = useState(false);
|
|
const messagesEndRef = useRef<HTMLDivElement>(null);
|
|
|
|
// Get request info
|
|
const requestInfo = useMemo(() => {
|
|
const data = REQUEST_DATABASE[requestId as keyof typeof REQUEST_DATABASE];
|
|
return data || {
|
|
id: requestId,
|
|
title: 'Unknown Request',
|
|
department: 'Unknown',
|
|
priority: 'medium',
|
|
status: 'pending'
|
|
};
|
|
}, [requestId]);
|
|
|
|
const onlineParticipants = MOCK_PARTICIPANTS.filter(p => p.status === 'online');
|
|
const filteredMessages = messages.filter(msg =>
|
|
msg.content.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
|
msg.user.name.toLowerCase().includes(searchTerm.toLowerCase())
|
|
);
|
|
|
|
const scrollToBottom = () => {
|
|
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
|
|
};
|
|
|
|
useEffect(() => {
|
|
scrollToBottom();
|
|
}, [messages]);
|
|
|
|
const handleSendMessage = () => {
|
|
if (message.trim()) {
|
|
const newMessage: Message = {
|
|
id: Date.now().toString(),
|
|
user: { name: 'You', avatar: 'YO', role: 'Current User' },
|
|
content: message,
|
|
timestamp: new Date().toLocaleString(),
|
|
mentions: extractMentions(message),
|
|
isHighPriority: message.includes('!important') || message.includes('urgent')
|
|
};
|
|
setMessages(prev => [...prev, newMessage]);
|
|
setMessage('');
|
|
}
|
|
};
|
|
|
|
const extractMentions = (text: string): string[] => {
|
|
const mentionRegex = /@([\w\s]+)(?=\s|$|[.,!?])/g;
|
|
const mentions = [];
|
|
let match;
|
|
while ((match = mentionRegex.exec(text)) !== null) {
|
|
mentions.push(match[1].trim());
|
|
}
|
|
return mentions;
|
|
};
|
|
|
|
const handleKeyPress = (e: React.KeyboardEvent) => {
|
|
if (e.key === 'Enter' && !e.shiftKey) {
|
|
e.preventDefault();
|
|
handleSendMessage();
|
|
}
|
|
};
|
|
|
|
const addReaction = (messageId: string, emoji: string) => {
|
|
setMessages(prev => prev.map(msg => {
|
|
if (msg.id === messageId) {
|
|
const reactions = msg.reactions || [];
|
|
const existingReaction = reactions.find(r => r.emoji === emoji);
|
|
|
|
if (existingReaction) {
|
|
if (existingReaction.users.includes('You')) {
|
|
existingReaction.users = existingReaction.users.filter(u => u !== 'You');
|
|
if (existingReaction.users.length === 0) {
|
|
return { ...msg, reactions: reactions.filter(r => r.emoji !== emoji) };
|
|
}
|
|
} else {
|
|
existingReaction.users.push('You');
|
|
}
|
|
} else {
|
|
reactions.push({ emoji, users: ['You'] });
|
|
}
|
|
|
|
return { ...msg, reactions };
|
|
}
|
|
return msg;
|
|
}));
|
|
};
|
|
|
|
return (
|
|
<div className="h-screen max-h-screen flex flex-col bg-gray-50 overflow-hidden">
|
|
{/* Header */}
|
|
<div className="bg-white border-b border-gray-200 px-3 sm:px-6 py-4">
|
|
<div className="flex items-center justify-between">
|
|
<div className="flex items-center gap-2 sm:gap-4 min-w-0 flex-1">
|
|
<Button variant="ghost" size="icon" onClick={onBack} className="shrink-0">
|
|
<ArrowLeft className="h-5 w-5" />
|
|
</Button>
|
|
<div className="flex items-center gap-2 sm:gap-4 min-w-0 flex-1">
|
|
<div className="w-10 h-10 sm:w-12 sm:h-12 bg-gradient-to-br from-blue-500 to-purple-600 rounded-xl flex items-center justify-center shadow-lg shrink-0">
|
|
<MessageSquare className="w-5 h-5 sm:w-6 sm:h-6 text-white" />
|
|
</div>
|
|
<div className="min-w-0 flex-1">
|
|
<h1 className="text-lg sm:text-2xl font-bold text-gray-900">Work Notes</h1>
|
|
<div className="flex items-center gap-2 mt-1">
|
|
<p className="text-gray-600 text-sm sm:text-base truncate">{requestInfo.title}</p>
|
|
<Badge variant="outline" className="text-xs shrink-0">
|
|
{requestId}
|
|
</Badge>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex items-center gap-3">
|
|
<div className="flex items-center gap-2 text-sm text-gray-600">
|
|
<div className="flex -space-x-2">
|
|
{onlineParticipants.slice(0, 3).map((participant, index) => (
|
|
<Avatar key={index} className="h-8 w-8 ring-2 ring-white shadow-sm">
|
|
<AvatarFallback className="bg-blue-500 text-white text-xs font-semibold">
|
|
{participant.avatar}
|
|
</AvatarFallback>
|
|
</Avatar>
|
|
))}
|
|
{onlineParticipants.length > 3 && (
|
|
<div className="h-8 w-8 rounded-full bg-gray-100 ring-2 ring-white flex items-center justify-center text-xs font-medium text-gray-600">
|
|
+{onlineParticipants.length - 3}
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
</div>
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
onClick={() => setShowSidebar(true)}
|
|
className="lg:hidden"
|
|
>
|
|
<Users className="w-4 h-4" />
|
|
</Button>
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
onClick={() => setShowSidebar(!showSidebar)}
|
|
className="lg:hidden"
|
|
>
|
|
<Users className="h-4 w-4" />
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex-1 flex overflow-hidden relative">
|
|
{/* Main Chat Area */}
|
|
<div className="flex-1 flex flex-col min-w-0">
|
|
<Tabs value={activeTab} onValueChange={setActiveTab} className="flex-1 flex flex-col">
|
|
{/* Tab Navigation */}
|
|
<div className="bg-white border-b border-gray-200 px-2 sm:px-3 lg:px-6">
|
|
<TabsList className="grid w-full max-w-full sm:max-w-md grid-cols-3 bg-gray-100 h-10">
|
|
<TabsTrigger value="chat" className="flex items-center gap-1 sm:gap-2 text-xs sm:text-sm px-2">
|
|
<MessageSquare className="w-3 h-3 sm:w-4 sm:h-4" />
|
|
<span className="hidden xs:inline">Messages</span>
|
|
<span className="xs:hidden">Chat</span>
|
|
</TabsTrigger>
|
|
<TabsTrigger value="files" className="flex items-center gap-1 sm:gap-2 text-xs sm:text-sm px-2">
|
|
<FileText className="w-3 h-3 sm:w-4 sm:h-4" />
|
|
<span>Files</span>
|
|
</TabsTrigger>
|
|
<TabsTrigger value="activity" className="flex items-center gap-1 sm:gap-2 text-xs sm:text-sm px-2">
|
|
<Activity className="w-3 h-3 sm:w-4 sm:h-4" />
|
|
<span className="hidden xs:inline">Activity</span>
|
|
<span className="xs:hidden">Act</span>
|
|
</TabsTrigger>
|
|
</TabsList>
|
|
</div>
|
|
|
|
{/* Chat Tab */}
|
|
<TabsContent value="chat" className="flex-1 flex flex-col m-0">
|
|
{/* Search Bar */}
|
|
<div className="bg-white border-b border-gray-200 px-2 sm:px-3 lg:px-6 py-2 sm:py-3">
|
|
<div className="relative">
|
|
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 w-4 h-4" />
|
|
<Input
|
|
placeholder="Search messages..."
|
|
value={searchTerm}
|
|
onChange={(e) => setSearchTerm(e.target.value)}
|
|
className="pl-10 bg-gray-50 border-gray-200 h-9 sm:h-10"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Messages Area */}
|
|
<div className="flex-1 overflow-y-auto px-2 sm:px-3 lg:px-6 py-2 sm:py-4">
|
|
<div className="space-y-3 sm:space-y-6 max-w-full">
|
|
{filteredMessages.map((msg) => (
|
|
<div key={msg.id} className={`flex gap-2 sm:gap-3 lg:gap-4 ${msg.isSystem ? 'justify-center' : ''}`}>
|
|
{!msg.isSystem && (
|
|
<Avatar className="h-8 w-8 sm:h-10 sm:w-10 lg:h-12 lg:w-12 flex-shrink-0 ring-1 sm:ring-2 ring-white shadow-sm">
|
|
<AvatarFallback className={`text-white font-semibold text-xs sm:text-sm ${
|
|
msg.user.role === 'Initiator' ? 'bg-green-600' :
|
|
msg.user.role === 'Current User' ? 'bg-blue-500' :
|
|
msg.user.role === 'System' ? 'bg-gray-500' :
|
|
'bg-slate-600'
|
|
}`}>
|
|
{msg.user.avatar}
|
|
</AvatarFallback>
|
|
</Avatar>
|
|
)}
|
|
|
|
<div className={`flex-1 min-w-0 ${msg.isSystem ? 'text-center max-w-xs sm:max-w-md mx-auto' : ''}`}>
|
|
{msg.isSystem ? (
|
|
<div className="inline-flex items-center gap-2 sm:gap-3 px-3 sm:px-4 py-1.5 sm:py-2 bg-gray-100 rounded-full">
|
|
<Activity className="w-3 h-3 sm:w-4 sm:h-4 text-gray-500 flex-shrink-0" />
|
|
<span className="text-xs sm:text-sm text-gray-700">{msg.content}</span>
|
|
<span className="text-xs text-gray-500 hidden sm:inline">{msg.timestamp}</span>
|
|
</div>
|
|
) : (
|
|
<div>
|
|
{/* Message Header */}
|
|
<div className="flex items-center gap-2 sm:gap-3 mb-1 sm:mb-2 flex-wrap">
|
|
<span className="font-semibold text-gray-900 text-sm sm:text-base truncate">{msg.user.name}</span>
|
|
<Badge variant="outline" className="text-xs flex-shrink-0">
|
|
{msg.user.role}
|
|
</Badge>
|
|
<span className="text-xs text-gray-500 flex items-center gap-1 flex-shrink-0">
|
|
<Clock className="w-3 h-3" />
|
|
{msg.timestamp}
|
|
</span>
|
|
{msg.isHighPriority && (
|
|
<Badge variant="destructive" className="text-xs flex-shrink-0">
|
|
<Flag className="w-3 h-3 mr-1" />
|
|
Priority
|
|
</Badge>
|
|
)}
|
|
</div>
|
|
|
|
{/* Message Content */}
|
|
<div className="bg-white rounded-lg border border-gray-200 p-3 sm:p-4 shadow-sm">
|
|
<div
|
|
className="text-gray-800 leading-relaxed text-sm sm:text-base"
|
|
dangerouslySetInnerHTML={{ __html: formatMessage(msg.content) }}
|
|
/>
|
|
|
|
{/* Attachments */}
|
|
{msg.attachments && msg.attachments.length > 0 && (
|
|
<div className="mt-2 sm:mt-3 pt-2 sm:pt-3 border-t border-gray-100">
|
|
<div className="space-y-2">
|
|
{msg.attachments.map((attachment, index) => (
|
|
<div key={index} className="flex items-center gap-2 sm:gap-3 p-2 bg-gray-50 rounded-lg">
|
|
<span className="text-lg sm:text-xl flex-shrink-0">{getFileIcon(attachment.type)}</span>
|
|
<span className="text-xs sm:text-sm font-medium text-gray-700 flex-1 truncate">
|
|
{attachment.name}
|
|
</span>
|
|
<Button variant="ghost" size="sm" className="h-8 w-8 p-0 flex-shrink-0">
|
|
<Download className="w-3 h-3 sm:w-4 sm:h-4" />
|
|
</Button>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Reactions */}
|
|
{msg.reactions && msg.reactions.length > 0 && (
|
|
<div className="flex items-center gap-1 sm:gap-2 mt-2 sm:mt-3 pt-2 sm:pt-3 border-t border-gray-100 flex-wrap">
|
|
{msg.reactions.map((reaction, index) => (
|
|
<button
|
|
key={index}
|
|
onClick={() => addReaction(msg.id, reaction.emoji)}
|
|
className={`flex items-center gap-1 px-2 py-1 rounded-full text-xs sm:text-sm transition-colors flex-shrink-0 ${
|
|
reaction.users.includes('You')
|
|
? 'bg-blue-100 text-blue-800 border border-blue-200'
|
|
: 'bg-gray-100 text-gray-700 hover:bg-gray-200'
|
|
}`}
|
|
>
|
|
<span>{reaction.emoji}</span>
|
|
<span className="text-xs font-medium">{reaction.users.length}</span>
|
|
</button>
|
|
))}
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
className="h-6 w-6 sm:h-7 sm:w-7 p-0 flex-shrink-0"
|
|
onClick={() => setShowEmojiPicker(!showEmojiPicker)}
|
|
>
|
|
<Plus className="w-2 h-2 sm:w-3 sm:h-3" />
|
|
</Button>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
))}
|
|
|
|
{isTyping && (
|
|
<div className="flex gap-2 sm:gap-4">
|
|
<Avatar className="h-8 w-8 sm:h-10 sm:w-10 lg:h-12 lg:w-12 flex-shrink-0">
|
|
<AvatarFallback className="bg-gray-400 text-white">
|
|
<div className="flex gap-0.5 sm:gap-1">
|
|
<div className="w-1 h-1 bg-white rounded-full animate-bounce"></div>
|
|
<div className="w-1 h-1 bg-white rounded-full animate-bounce" style={{ animationDelay: '0.1s' }}></div>
|
|
<div className="w-1 h-1 bg-white rounded-full animate-bounce" style={{ animationDelay: '0.2s' }}></div>
|
|
</div>
|
|
</AvatarFallback>
|
|
</Avatar>
|
|
<div className="flex items-center text-xs sm:text-sm text-gray-500 bg-gray-100 px-3 sm:px-4 py-2 rounded-lg">
|
|
Someone is typing...
|
|
</div>
|
|
</div>
|
|
)}
|
|
<div ref={messagesEndRef} />
|
|
</div>
|
|
</div>
|
|
|
|
{/* Message Input */}
|
|
<div className="bg-white border-t border-gray-200 p-2 sm:p-3 lg:p-6">
|
|
<div className="max-w-full">
|
|
<div className="flex flex-col gap-2 sm:gap-4">
|
|
<div className="flex-1">
|
|
<Textarea
|
|
placeholder="Type your message... Use @username to mention someone"
|
|
value={message}
|
|
onChange={(e) => setMessage(e.target.value)}
|
|
onKeyPress={handleKeyPress}
|
|
className="min-h-[50px] sm:min-h-[60px] resize-none border-gray-200 focus:ring-blue-500 focus:border-blue-500 w-full text-sm"
|
|
rows={2}
|
|
/>
|
|
<div className="flex flex-col sm:flex-row justify-between items-start sm:items-center mt-2 gap-2">
|
|
<div className="flex items-center gap-1 sm:gap-2 order-2 sm:order-1">
|
|
<Button variant="ghost" size="sm" className="text-gray-500 h-7 w-7 sm:h-8 sm:w-8 p-0">
|
|
<Paperclip className="h-3 w-3 sm:h-4 sm:w-4" />
|
|
</Button>
|
|
<Button variant="ghost" size="sm" className="text-gray-500 h-7 w-7 sm:h-8 sm:w-8 p-0">
|
|
<Smile className="h-3 w-3 sm:h-4 sm:w-4" />
|
|
</Button>
|
|
<Button variant="ghost" size="sm" className="text-gray-500 h-7 w-7 sm:h-8 sm:w-8 p-0 hidden sm:flex">
|
|
<AtSign className="h-4 w-4" />
|
|
</Button>
|
|
<Button variant="ghost" size="sm" className="text-gray-500 h-7 w-7 sm:h-8 sm:w-8 p-0 hidden sm:flex">
|
|
<Hash className="h-4 w-4" />
|
|
</Button>
|
|
</div>
|
|
<div className="flex items-center gap-2 order-1 sm:order-2">
|
|
<span className="text-xs text-gray-500 hidden sm:inline">
|
|
{message.length}/2000
|
|
</span>
|
|
<Button
|
|
onClick={handleSendMessage}
|
|
disabled={!message.trim()}
|
|
className="bg-blue-600 hover:bg-blue-700 min-w-0 h-8 sm:h-9"
|
|
size="sm"
|
|
>
|
|
<Send className="h-3 w-3 sm:h-4 sm:w-4 sm:mr-2" />
|
|
<span className="hidden sm:inline ml-1">Send</span>
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</TabsContent>
|
|
|
|
{/* Files Tab */}
|
|
<TabsContent value="files" className="flex-1 p-2 sm:p-3 lg:p-6 m-0">
|
|
<div className="max-w-full">
|
|
<div className="flex flex-col sm:flex-row sm:items-center justify-between mb-4 sm:mb-6 gap-3">
|
|
<h3 className="text-base sm:text-lg font-semibold text-gray-900">Shared Files</h3>
|
|
<Button className="gap-2 text-sm h-9">
|
|
<Plus className="w-4 h-4" />
|
|
<span className="hidden xs:inline">Upload File</span>
|
|
<span className="xs:hidden">Upload</span>
|
|
</Button>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-3 sm:gap-4">
|
|
{MOCK_DOCUMENTS.map((doc, index) => (
|
|
<Card key={index} className="hover:shadow-lg transition-shadow">
|
|
<CardContent className="p-3 sm:p-4">
|
|
<div className="flex items-start gap-2 sm:gap-3">
|
|
<div className="w-10 h-10 sm:w-12 sm:h-12 bg-blue-100 rounded-lg flex items-center justify-center flex-shrink-0">
|
|
<span className="text-lg sm:text-2xl">{getFileIcon(doc.type)}</span>
|
|
</div>
|
|
<div className="flex-1 min-w-0">
|
|
<h4 className="font-medium text-gray-900 truncate text-sm sm:text-base">{doc.name}</h4>
|
|
<p className="text-xs sm:text-sm text-gray-600 mt-1">
|
|
{doc.size} • {doc.type}
|
|
</p>
|
|
<p className="text-xs text-gray-500 mt-1">
|
|
by {doc.uploadedBy} • {doc.uploadedAt}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
<div className="flex items-center gap-2 mt-3 sm:mt-4">
|
|
<Button variant="outline" size="sm" className="flex-1 text-xs sm:text-sm h-8 sm:h-9">
|
|
<Eye className="h-3 w-3 sm:h-4 sm:w-4 mr-1 sm:mr-2" />
|
|
View
|
|
</Button>
|
|
<Button variant="outline" size="sm" className="flex-1 text-xs sm:text-sm h-8 sm:h-9">
|
|
<Download className="h-3 w-3 sm:h-4 sm:w-4 mr-1 sm:mr-2" />
|
|
Download
|
|
</Button>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</TabsContent>
|
|
|
|
{/* Activity Tab */}
|
|
<TabsContent value="activity" className="flex-1 p-2 sm:p-3 lg:p-6 m-0">
|
|
<div className="max-w-full">
|
|
<h3 className="text-base sm:text-lg font-semibold text-gray-900 mb-4 sm:mb-6">Recent Activity</h3>
|
|
<div className="space-y-3 sm:space-y-4">
|
|
{messages.filter(msg => msg.isSystem).map((msg) => (
|
|
<div key={msg.id} className="flex items-start gap-3 sm:gap-4 p-3 sm:p-4 bg-gray-50 rounded-lg">
|
|
<div className="w-7 h-7 sm:w-8 sm:h-8 bg-blue-100 rounded-full flex items-center justify-center flex-shrink-0">
|
|
<Activity className="w-3 h-3 sm:w-4 sm:h-4 text-blue-600" />
|
|
</div>
|
|
<div className="flex-1 min-w-0">
|
|
<p className="text-gray-900 text-sm sm:text-base">{msg.content}</p>
|
|
<p className="text-xs sm:text-sm text-gray-500 mt-1">{msg.timestamp}</p>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</TabsContent>
|
|
</Tabs>
|
|
</div>
|
|
|
|
{/* Mobile Sidebar Overlay */}
|
|
{showSidebar && (
|
|
<div
|
|
className="fixed inset-0 bg-black bg-opacity-50 z-40 lg:hidden"
|
|
onClick={() => setShowSidebar(false)}
|
|
/>
|
|
)}
|
|
|
|
{/* Participants Sidebar */}
|
|
<div className={`
|
|
w-72 sm:w-80 bg-white border-l border-gray-200 flex flex-col
|
|
lg:relative lg:translate-x-0 lg:shadow-none
|
|
${showSidebar ? 'fixed right-0 top-0 bottom-0 z-50 shadow-xl' : 'hidden lg:flex'}
|
|
`}>
|
|
<div className="p-4 sm:p-6 border-b border-gray-200">
|
|
<div className="flex items-center justify-between mb-4">
|
|
<h3 className="text-base sm:text-lg font-semibold text-gray-900">Participants</h3>
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
onClick={() => setShowSidebar(false)}
|
|
className="lg:hidden h-8 w-8 p-0"
|
|
>
|
|
<X className="h-4 w-4" />
|
|
</Button>
|
|
</div>
|
|
<div className="space-y-3 sm:space-y-4">
|
|
{MOCK_PARTICIPANTS.map((participant, index) => (
|
|
<div key={index} className="flex items-center gap-3">
|
|
<div className="relative">
|
|
<Avatar className="h-9 w-9 sm:h-10 sm:w-10">
|
|
<AvatarFallback className={`text-white font-semibold text-sm ${
|
|
participant.role === 'Initiator' ? 'bg-green-600' : 'bg-slate-600'
|
|
}`}>
|
|
{participant.avatar}
|
|
</AvatarFallback>
|
|
</Avatar>
|
|
<div className={`absolute -bottom-1 -right-1 w-3 h-3 rounded-full border-2 border-white ${getStatusColor(participant.status)}`}></div>
|
|
</div>
|
|
<div className="flex-1 min-w-0">
|
|
<p className="font-medium text-gray-900 truncate text-sm sm:text-base">{participant.name}</p>
|
|
<div className="flex items-center gap-2">
|
|
<p className="text-xs text-gray-500">{participant.role}</p>
|
|
<span className="text-xs text-gray-400">•</span>
|
|
<p className="text-xs text-gray-500">{getStatusText(participant.status)}</p>
|
|
</div>
|
|
{participant.lastSeen && participant.status === 'offline' && (
|
|
<p className="text-xs text-gray-400">{participant.lastSeen}</p>
|
|
)}
|
|
</div>
|
|
<Button variant="ghost" size="sm" className="h-8 w-8 p-0">
|
|
<MoreHorizontal className="h-4 w-4" />
|
|
</Button>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
|
|
<div className="p-4 sm:p-6">
|
|
<h4 className="font-semibold text-gray-900 mb-3 text-sm sm:text-base">Quick Actions</h4>
|
|
<div className="space-y-2">
|
|
<Button variant="outline" size="sm" className="w-full justify-start gap-2 h-9 text-sm">
|
|
<Users className="h-4 w-4" />
|
|
Add Participant
|
|
</Button>
|
|
<Button variant="outline" size="sm" className="w-full justify-start gap-2 h-9 text-sm">
|
|
<Bell className="h-4 w-4" />
|
|
Manage Notifications
|
|
</Button>
|
|
<Button variant="outline" size="sm" className="w-full justify-start gap-2 h-9 text-sm">
|
|
<Archive className="h-4 w-4" />
|
|
Archive Chat
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
} |