Codex Deploy
Prepare local Hugging Face deployment
191b322
import React, { useState, useEffect } from 'react';
import { Comment, Notification, User } from '../types';
import { MessageSquare, Bell, Send, CheckCircle2, User as UserIcon, Clock, Trash2 } from 'lucide-react';
import { useLocalCollection } from '../hooks/useLocalCollection';
interface CollaborationProps {
projectId: string;
targetId: string;
targetType: 'DOCUMENT' | 'TASK';
currentUser: User;
}
export const CommentSection: React.FC<CollaborationProps> = ({ projectId, targetId, targetType, currentUser }) => {
const { data: allComments, add: addComment, remove: removeComment } = useLocalCollection<Comment & { id: string }>(`comments_${projectId}`);
const [newComment, setNewComment] = useState('');
const [isLoading, setIsLoading] = useState(false);
// Derive sorted & filtered comments
const comments = allComments
.filter(c => c.targetId === targetId && c.targetType === targetType)
.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime());
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!newComment.trim() || !currentUser) return;
setIsLoading(true);
const commentId = `COMM-${Date.now()}`;
const commentData: Comment & { id: string } = {
id: commentId,
targetId,
targetType,
authorUid: currentUser.uid,
authorName: currentUser.name,
text: newComment.trim(),
createdAt: new Date().toISOString()
};
await addComment(commentData);
setNewComment('');
setIsLoading(false);
};
const handleDelete = async (commentId: string) => {
await removeComment(commentId);
};
return (
<div className="flex flex-col h-full bg-white rounded-xl border border-slate-200 overflow-hidden shadow-sm">
<div className="px-4 py-3 border-b border-slate-100 bg-slate-50 flex items-center justify-between">
<div className="flex items-center gap-2">
<MessageSquare className="w-4 h-4 text-blue-600" />
<h4 className="font-bold text-slate-800 text-sm">Discussion</h4>
</div>
<span className="text-xs font-medium text-slate-500 bg-slate-200 px-2 py-0.5 rounded-full">
{comments.length}
</span>
</div>
<div className="flex-1 overflow-y-auto p-4 space-y-4">
{comments.length > 0 ? (
comments.map((comment) => (
<div key={comment.id} className="flex gap-3 group">
<div className="w-8 h-8 rounded-full bg-blue-100 text-blue-600 flex items-center justify-center flex-shrink-0 font-bold text-xs">
{comment.authorName.charAt(0)}
</div>
<div className="flex-1">
<div className="flex items-center justify-between mb-1">
<p className="text-xs font-bold text-slate-900">{comment.authorName}</p>
<div className="flex items-center gap-2">
<p className="text-[10px] text-slate-400 flex items-center gap-1">
<Clock className="w-2.5 h-2.5" />
{new Date(comment.createdAt).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}
</p>
{currentUser?.uid === comment.authorUid && (
<button
onClick={() => handleDelete(comment.id)}
className="text-slate-300 hover:text-red-500 opacity-0 group-hover:opacity-100 transition-all"
>
<Trash2 className="w-3 h-3" />
</button>
)}
</div>
</div>
<div className="bg-slate-50 p-3 rounded-2xl rounded-tl-none border border-slate-100">
<p className="text-sm text-slate-700 leading-relaxed">{comment.text}</p>
</div>
</div>
</div>
))
) : (
<div className="flex flex-col items-center justify-center h-full text-slate-400 opacity-50 py-8">
<MessageSquare className="w-10 h-10 mb-2" />
<p className="text-xs">No comments yet. Start the conversation!</p>
</div>
)}
</div>
<form onSubmit={handleSubmit} className="p-3 border-t border-slate-100 bg-white">
<div className="relative">
<input
type="text"
placeholder="Write a comment..."
value={newComment}
onChange={(e) => setNewComment(e.target.value)}
disabled={isLoading}
className="w-full pl-4 pr-12 py-2.5 bg-slate-50 border border-slate-200 rounded-xl focus:ring-2 focus:ring-blue-500 outline-none text-sm transition-all"
/>
<button
type="submit"
disabled={!newComment.trim() || isLoading}
className="absolute right-1.5 top-1/2 transform -translate-y-1/2 p-1.5 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-all disabled:opacity-50 disabled:bg-slate-400"
>
<Send className="w-4 h-4" />
</button>
</div>
</form>
</div>
);
};
export const NotificationCenter: React.FC<{ uid: string }> = ({ uid }) => {
const { data: notifications, update: updateNotif } = useLocalCollection<Notification & { id: string }>(`notifications_${uid}`);
const [isOpen, setIsOpen] = useState(false);
const markAsRead = async (id: string) => {
updateNotif(id, { isRead: true });
};
const unreadCount = notifications.filter(n => !n.isRead).length;
return (
<div className="relative">
<button
onClick={() => setIsOpen(!isOpen)}
className="relative p-2 text-slate-500 hover:bg-slate-100 rounded-full transition-all"
>
<Bell className="w-5 h-5" />
{unreadCount > 0 && (
<span className="absolute top-1.5 right-1.5 w-4 h-4 bg-red-500 text-white text-[10px] font-bold flex items-center justify-center rounded-full border-2 border-white">
{unreadCount}
</span>
)}
</button>
{isOpen && (
<>
<div className="fixed inset-0 z-40" onClick={() => setIsOpen(false)} />
<div className="absolute right-0 mt-2 w-80 bg-white rounded-2xl shadow-2xl border border-slate-200 z-50 overflow-hidden animate-in fade-in zoom-in duration-200 origin-top-right">
<div className="px-4 py-3 border-b border-slate-100 bg-slate-50 flex items-center justify-between">
<h4 className="font-bold text-slate-800 text-sm">Notifications</h4>
{unreadCount > 0 && (
<span className="text-[10px] font-bold text-blue-600 bg-blue-50 px-2 py-0.5 rounded-full">
{unreadCount} New
</span>
)}
</div>
<div className="max-h-[400px] overflow-y-auto">
{notifications.length > 0 ? (
notifications.map((n) => (
<div
key={n.id}
onClick={() => markAsRead(n.id)}
className={`p-4 border-b border-slate-50 hover:bg-slate-50 transition-all cursor-pointer flex gap-3 ${!n.isRead ? 'bg-blue-50/30' : ''}`}
>
<div className={`w-8 h-8 rounded-full flex items-center justify-center flex-shrink-0 ${
n.type === 'TASK_ASSIGNED' ? 'bg-indigo-100 text-indigo-600' :
n.type === 'TASK_COMPLETED' ? 'bg-emerald-100 text-emerald-600' :
n.type === 'DOC_UPLOADED' ? 'bg-blue-100 text-blue-600' : 'bg-slate-100 text-slate-600'
}`}>
{n.type === 'TASK_ASSIGNED' ? <UserIcon className="w-4 h-4" /> :
n.type === 'TASK_COMPLETED' ? <CheckCircle2 className="w-4 h-4" /> :
n.type === 'DOC_UPLOADED' ? <MessageSquare className="w-4 h-4" /> : <Bell className="w-4 h-4" />}
</div>
<div className="flex-1">
<p className={`text-xs font-bold ${!n.isRead ? 'text-slate-900' : 'text-slate-600'}`}>{n.title}</p>
<p className="text-xs text-slate-500 mt-0.5 line-clamp-2">{n.message}</p>
<p className="text-[10px] text-slate-400 mt-2 flex items-center gap-1">
<Clock className="w-2.5 h-2.5" />
{new Date(n.createdAt).toLocaleDateString()}
</p>
</div>
{!n.isRead && <div className="w-2 h-2 bg-blue-500 rounded-full mt-1.5" />}
</div>
))
) : (
<div className="p-8 text-center text-slate-400">
<Bell className="w-10 h-10 mx-auto mb-2 opacity-20" />
<p className="text-xs">All caught up!</p>
</div>
)}
</div>
<div className="p-3 bg-slate-50 text-center border-t border-slate-100">
<button className="text-[10px] font-bold text-slate-500 hover:text-blue-600 transition-colors uppercase tracking-widest">
View All Activity
</button>
</div>
</div>
</>
)}
</div>
);
};