tahamajs/IE / Projects /front /src /components /authoring /CommentReviewPanel.tsx
tahamajs's picture
download
raw
10 kB
// src/components/authoring/CommentReviewPanel.tsx
import { useState } from "react";
import { MessageSquare, Check, X, Plus, Eye, EyeOff, User, Clock } from "lucide-react";
interface Comment {
id: string;
author: string;
body: string;
createdAt: string;
status: "open" | "resolved";
selection?: string;
replies: CommentReply[];
}
interface CommentReply {
id: string;
author: string;
body: string;
createdAt: string;
}
interface TrackedChange {
id: string;
type: "insertion" | "deletion";
oldText?: string;
newText?: string;
author: string;
accepted: boolean | null;
}
interface CommentReviewPanelProps {
comments: Comment[];
changes: TrackedChange[];
reviewMode: string;
setReviewMode: (m: string) => void;
activeCommentId: string | null;
setActiveCommentId: (id: string | null) => void;
showChanges: boolean;
setShowChanges: (v: boolean) => void;
addComment: (body: string) => void;
addReply: (commentId: string, body: string) => void;
resolveComment: (id: string) => void;
reopenComment: (id: string) => void;
acceptChange: (id: string) => void;
rejectChange: (id: string) => void;
acceptAllChanges: () => void;
rejectAllChanges: () => void;
openCount: number;
pendingChanges: number;
}
export function CommentReviewPanel({
comments,
changes,
reviewMode: _reviewMode, // unused
setReviewMode: _setReviewMode, // unused
activeCommentId,
setActiveCommentId: _setActiveCommentId, // unused
showChanges,
setShowChanges,
addComment,
addReply,
resolveComment,
reopenComment,
acceptChange,
rejectChange,
acceptAllChanges,
rejectAllChanges,
openCount,
pendingChanges,
}: CommentReviewPanelProps) {
const [newComment, setNewComment] = useState("");
const [replyText, setReplyText] = useState("");
return (
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4 h-full">
{/* Comments section */}
<div className="space-y-2 overflow-y-auto">
<div className="flex items-center justify-between">
<h4 className="text-sm font-semibold flex items-center gap-2">
<MessageSquare className="w-4 h-4 text-blue-500" /> Comments ({comments.length})
{openCount > 0 && <span className="text-[10px] text-blue-500">({openCount} open)</span>}
</h4>
</div>
{/* New comment input */}
<div className="flex gap-1.5">
<input
type="text"
value={newComment}
onChange={(e) => setNewComment(e.target.value)}
placeholder="Add a comment..."
className="flex-1 text-xs px-3 py-1.5 bg-white/10 rounded-lg"
onKeyDown={(e) => {
if (e.key === "Enter" && newComment.trim()) {
addComment(newComment.trim());
setNewComment("");
}
}}
/>
<button
onClick={() => {
if (newComment.trim()) {
addComment(newComment.trim());
setNewComment("");
}
}}
disabled={!newComment.trim()}
className="btn btn-primary btn-xs"
title="Add comment"
>
<Plus className="w-3.5 h-3.5" />
</button>
</div>
{/* Comments list */}
<div className="space-y-2">
{comments.map((comment) => (
<div
key={comment.id}
className={`rounded-xl border p-3 transition ${
comment.status === "resolved"
? "opacity-60 border-green-200 dark:border-green-800"
: activeCommentId === comment.id
? "border-primary bg-primary/5"
: "border-white/20"
}`}
>
<div className="flex items-center justify-between mb-1">
<div className="flex items-center gap-2 text-xs text-gray-500">
<User className="w-3 h-3" /> {comment.author}
<Clock className="w-3 h-3" /> {new Date(comment.createdAt).toLocaleDateString()}
</div>
<div className="flex gap-1">
{comment.status !== "resolved" ? (
<button
onClick={() => resolveComment(comment.id)}
className="p-0.5 text-green-500 hover:bg-green-50 rounded"
title="Resolve comment"
>
<Check className="w-3.5 h-3.5" />
</button>
) : (
<button
onClick={() => reopenComment(comment.id)}
className="p-0.5 text-amber-500 hover:bg-amber-50 rounded"
title="Reopen comment"
>
<X className="w-3.5 h-3.5" />
</button>
)}
</div>
</div>
<p className="text-sm">{comment.body}</p>
{comment.selection && (
<p className="text-xs text-gray-400 italic mt-1 bg-white/10 p-1.5 rounded">
"{comment.selection.substring(0, 80)}..."
</p>
)}
{/* Replies */}
{comment.replies.map((reply) => (
<div
key={reply.id}
className="ml-4 mt-2 pl-3 border-l-2 border-primary/20"
>
<p className="text-xs font-semibold">{reply.author}</p>
<p className="text-xs">{reply.body}</p>
</div>
))}
{/* Reply input */}
<div className="flex gap-1 mt-2">
<input
type="text"
value={replyText}
onChange={(e) => setReplyText(e.target.value)}
placeholder="Reply..."
className="flex-1 text-xs px-2 py-1 bg-white/10 rounded"
onKeyDown={(e) => {
if (e.key === "Enter" && replyText.trim()) {
addReply(comment.id, replyText.trim());
setReplyText("");
}
}}
/>
</div>
</div>
))}
</div>
</div>
{/* Track changes section */}
<div className="space-y-2 overflow-y-auto">
<div className="flex items-center justify-between">
<h4 className="text-sm font-semibold flex items-center gap-2">
<Eye className="w-4 h-4 text-amber-500" /> Track Changes ({pendingChanges} pending)
</h4>
<div className="flex gap-1">
<button
onClick={() => setShowChanges(!showChanges)}
className="btn btn-ghost btn-xs gap-1"
title="Toggle change visibility"
>
{showChanges ? <EyeOff className="w-3 h-3" /> : <Eye className="w-3 h-3" />}
</button>
{pendingChanges > 0 && (
<>
<button
onClick={acceptAllChanges}
className="btn btn-ghost btn-xs text-green-600"
title="Accept all changes"
>
Accept All
</button>
<button
onClick={rejectAllChanges}
className="btn btn-ghost btn-xs text-red-500"
title="Reject all changes"
>
Reject All
</button>
</>
)}
</div>
</div>
{changes.length === 0 ? (
<div className="text-center py-8 text-gray-400 text-sm">
No tracked changes yet. Switch to "Suggesting" mode to track edits.
</div>
) : (
<div className="space-y-1.5">
{changes.map((change) => (
<div
key={change.id}
className={`rounded-lg border p-2.5 text-sm ${
change.accepted === true
? "border-green-200 dark:border-green-800 bg-green-50/20"
: change.accepted === false
? "border-red-200 dark:border-red-800 bg-red-50/20 opacity-60"
: "border-white/20"
}`}
>
<div className="flex items-center justify-between mb-1">
<span className="text-[9px] px-1.5 py-0.5 rounded-full bg-white/10 capitalize">
{change.type}
</span>
<span className="text-[9px] text-gray-400">{change.author}</span>
</div>
{change.type === "deletion" && change.oldText && (
<p className="text-red-600 dark:text-red-400 line-through">
{change.oldText.substring(0, 100)}
</p>
)}
{change.type === "insertion" && change.newText && (
<p className="text-green-600 dark:text-green-400">
{change.newText.substring(0, 100)}
</p>
)}
{change.accepted === null && (
<div className="flex gap-1 mt-1.5">
<button
onClick={() => acceptChange(change.id)}
className="btn btn-ghost btn-xs text-green-600"
title="Accept change"
>
<Check className="w-3 h-3" />
</button>
<button
onClick={() => rejectChange(change.id)}
className="btn btn-ghost btn-xs text-red-500"
title="Reject change"
>
<X className="w-3 h-3" />
</button>
</div>
)}
</div>
))}
</div>
)}
</div>
</div>
);
}

Xet Storage Details

Size:
10 kB
·
Xet hash:
fd8731b652f3bba41805f94fc9a4c806519a5ffeb303b0fe0b74ee0d6f38dedc

Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.