Skip to content

Commit

Permalink
Merge pull request #86 from MinaFoundation/feature/community-comments
Browse files Browse the repository at this point in the history
Feature/community comments
  • Loading branch information
iluxonchik authored Dec 11, 2024
2 parents be48d25 + 7a7ad2e commit 453d5e7
Show file tree
Hide file tree
Showing 5 changed files with 328 additions and 188 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "pgt-web-app",
"version": "0.1.16",
"version": "0.1.17",
"private": true,
"type": "module",
"scripts": {
Expand Down
189 changes: 164 additions & 25 deletions src/components/ReviewerComments.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,20 @@
import { useMemo } from 'react'
import { Badge } from "@/components/ui/badge"
import { cn } from "@/lib/utils"
import {
BadgeCheck,
CircleUserRound,
MessageCircle,
Clock,
ShieldCheck,
HelpCircle
} from 'lucide-react'
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip"
import type { DeliberationComment } from "@/types/deliberation"

interface ReviewerCommentsProps {
Expand All @@ -8,35 +23,159 @@ interface ReviewerCommentsProps {
}

export function ReviewerComments({ comments, newComment }: ReviewerCommentsProps) {
// Combine existing comments with new comment if it exists
const allComments = newComment
? [...comments, newComment]
: comments
const allComments = useMemo(() => {
const combinedComments = newComment
? [...comments, newComment]
: comments;

// Remove any potential duplicates based on ID and user
const uniqueComments = combinedComments.reduce((acc, current) => {
const isDuplicate = acc.some(comment =>
comment.id === current.id ||
(comment.isReviewerComment && current.isReviewerComment &&
comment.reviewer?.username === current.reviewer?.username) ||
(!comment.isReviewerComment && current.id === comment.id) // Changed this condition
);

if (!isDuplicate) {
acc.push(current);
}
return acc;
}, [] as DeliberationComment[]);

// Sort by date, newest first
return uniqueComments.sort((a, b) =>
new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()
);
}, [comments, newComment]);

// Group comments by type for better organization
const groupedComments = useMemo(() => {
const reviewerComments = allComments.filter(c => c.isReviewerComment);
const communityComments = allComments.filter(c => !c.isReviewerComment);
return { reviewerComments, communityComments };
}, [allComments]);

return (
<div className="mt-6 space-y-4">
<h4 className="font-medium">Reviewer Comments</h4>
<div className="space-y-4">
{allComments.map((comment) => (
<div
key={comment.id}
className={cn(
"p-4 rounded-lg border",
comment.recommendation
? "border-green-500/20 bg-green-50/50 dark:bg-green-900/10"
: "border-red-500/20 bg-red-50/50 dark:bg-red-900/10"
)}
>
<div className="flex justify-between items-start mb-2">
<span className="font-medium">👤 {comment.reviewer.username}</span>
<Badge variant={comment.recommendation ? 'default' : 'destructive'}>
{comment.recommendation ? '✅ Recommended' : '❌ Not Recommended'}
</Badge>
</div>
<p className="text-muted-foreground">{comment.feedback}</p>
<h4 className="font-medium">Comments & Deliberations</h4>
<div className="space-y-6">
{groupedComments.reviewerComments.length > 0 && (
<div className="space-y-4">
<h5 className="text-sm font-medium text-muted-foreground">Reviewer Comments</h5>
{groupedComments.reviewerComments.map((comment) => (
<CommentCard key={comment.id} comment={comment} />
))}
</div>
)}

{groupedComments.communityComments.length > 0 && (
<div className="space-y-4">
<h5 className="text-sm font-medium text-muted-foreground">Community Deliberations</h5>
{groupedComments.communityComments.map((comment) => (
<CommentCard key={comment.id} comment={comment} />
))}
</div>
))}
)}
</div>
</div>
);
}

// Extracted comment card component for better organization
function CommentCard({ comment }: { comment: DeliberationComment }) {
return (
<div
className={cn(
"p-4 rounded-lg border transition-all duration-200 hover:shadow-sm",
comment.recommendation !== undefined
? comment.recommendation
? "border-green-500/20 bg-green-50/50 dark:bg-green-900/10 hover:border-green-500/30"
: "border-red-500/20 bg-red-50/50 dark:bg-red-900/10 hover:border-red-500/30"
: "border-gray-200 bg-gray-50/50 dark:bg-gray-900/10 hover:border-gray-300"
)}
>
<div className="flex justify-between items-start mb-2">
<div className="flex items-center gap-2">
<TooltipProvider delayDuration={300}>
<Tooltip>
<TooltipTrigger asChild>
<div className="flex items-center gap-2">
{comment.isReviewerComment ? (
<>
<div className="relative flex items-center">
<ShieldCheck className="h-5 w-5 text-blue-500" />
</div>
<span className="font-medium flex items-center gap-1.5">
{comment.reviewer?.username}
<BadgeCheck className="h-4 w-4 text-blue-500" />
</span>
</>
) : (
<>
<div className="relative flex items-center">
<CircleUserRound className="h-5 w-5 text-gray-400" />
</div>
<span className="font-medium text-gray-600 dark:text-gray-400">
Anonymous Community Member
</span>
</>
)}
</div>
</TooltipTrigger>
<TooltipContent side="top" className="max-w-[300px] p-3">
{comment.isReviewerComment ? (
<div className="space-y-1">
<p className="font-medium">Verified Expert Reviewer</p>
<p className="text-sm text-muted-foreground">
This comment is from a verified expert reviewer for this proposal.
</p>
</div>
) : (
<div className="space-y-1">
<p className="font-medium">Anonymous Community Member</p>
<p className="text-sm text-muted-foreground">
Community members&lsquo;identities are kept anonymous to ensure unbiased deliberation.
</p>
</div>
)}
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
{comment.recommendation !== undefined && (
<Badge
variant={comment.recommendation ? 'default' : 'destructive'}
className="transition-all duration-200 hover:opacity-90"
>
{comment.recommendation
? <span className="flex items-center gap-1">
<BadgeCheck className="h-3.5 w-3.5" />
Recommended
</span>
: <span className="flex items-center gap-1">
<ShieldCheck className="h-3.5 w-3.5" />
Not Recommended
</span>
}
</Badge>
)}
</div>
<div className="pl-6">
<p className="text-muted-foreground whitespace-pre-wrap">
{comment.feedback}
</p>
<div className="mt-2 text-xs text-muted-foreground/80 flex items-center gap-1.5">
<Clock className="h-3.5 w-3.5" />
{new Date(comment.createdAt).toLocaleDateString(undefined, {
year: 'numeric',
month: 'short',
day: 'numeric',
hour: '2-digit',
minute: '2-digit'
})}
</div>
</div>
</div>
)
);
}
115 changes: 54 additions & 61 deletions src/components/phases/DeliberationPhase.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -59,74 +59,67 @@ export function DeliberationPhase({ fundingRoundId, fundingRoundName }: Props) {
recommendation
);

if (response) {
// Update proposals and recalculate counts
setProposals(prevProposals => {
const updatedProposals = prevProposals.map(proposal => {
if (proposal.id !== dialogProps.proposalId) {
return proposal;
}

// Create updated deliberation vote
const updatedDeliberation = {
setProposals(prevProposals => {
return prevProposals.map(proposal => {
if (proposal.id !== dialogProps.proposalId) {
return proposal;
}

// Create new comment object with proper date handling
const newComment: DeliberationComment = {
id: response.id,
feedback,
recommendation: proposal.isReviewerEligible ? recommendation : undefined,
createdAt: new Date(), // This ensures it's a proper Date object
isReviewerComment: Boolean(proposal.isReviewerEligible),
...(proposal.isReviewerEligible ? {
reviewer: {
username: user.metadata.username
}
} : {})
};

// Update comments list with proper date handling
let updatedComments = [...proposal.reviewerComments];
const existingCommentIndex = updatedComments.findIndex(
c => (c.isReviewerComment && c.reviewer?.username === user.metadata.username) ||
(!c.isReviewerComment && !c.reviewer?.username)
);

if (existingCommentIndex !== -1) {
updatedComments[existingCommentIndex] = newComment;
} else {
updatedComments = [...updatedComments, newComment];
}

// Ensure all dates are Date objects before sorting
updatedComments = updatedComments.map(comment => ({
...comment,
createdAt: new Date(comment.createdAt)
}));

// Sort comments
updatedComments.sort((a, b) =>
b.createdAt.getTime() - a.createdAt.getTime()
);

return {
...proposal,
userDeliberation: {
feedback,
recommendation,
createdAt: new Date(),
isReviewerVote: proposal.isReviewerEligible ?? false
};

// For reviewers, update or add the comment
const updatedReviewerComments = recommendation !== undefined
? proposal.reviewerComments.some(c => c.reviewer.username === user.metadata.username)
? proposal.reviewerComments.map(c =>
c.reviewer.username === user.metadata.username
? {
...c,
feedback,
recommendation,
createdAt: new Date()
}
: c
)
: [
...proposal.reviewerComments,
{
id: response.id, // Use server-provided ID
feedback,
recommendation,
createdAt: new Date(),
reviewer: {
username: user.metadata.username
}
}
]
: proposal.reviewerComments;

// Return updated proposal
return {
...proposal,
userDeliberation: updatedDeliberation,
hasVoted: true,
reviewerComments: updatedReviewerComments
};
}).sort((a, b) => {
if (!a.userDeliberation && b.userDeliberation) return -1;
if (a.userDeliberation && !b.userDeliberation) return 1;
return 0;
});

// Update pending and total counts
const newPendingCount = updatedProposals.filter(p => !p.hasVoted).length;
setPendingCount(newPendingCount);
setTotalCount(updatedProposals.length);

return updatedProposals;
isReviewerVote: Boolean(proposal.isReviewerEligible)
},
hasVoted: true,
reviewerComments: updatedComments
};
});
}
});

setDialogProps({ open: false });
} catch (error) {
console.error("Failed to submit deliberation:", error);
console.error("Failed to submit vote:", error);
}
};

Expand Down
Loading

0 comments on commit 453d5e7

Please sign in to comment.