From 1e1784091e18043cd039a7b655af12de235af4cf Mon Sep 17 00:00:00 2001 From: davedumto Date: Sat, 16 Aug 2025 14:22:50 +0100 Subject: [PATCH 1/5] feat: built the flow for comment and replies --- app/comment/page.tsx | 29 ++ app/page.tsx | 21 +- components/comment/modal.tsx | 563 +++++++++++++++++++++++++++++++++++ public/empty/user.svg | 19 ++ 4 files changed, 625 insertions(+), 7 deletions(-) create mode 100644 app/comment/page.tsx create mode 100644 components/comment/modal.tsx create mode 100644 public/empty/user.svg diff --git a/app/comment/page.tsx b/app/comment/page.tsx new file mode 100644 index 000000000..bf4b59389 --- /dev/null +++ b/app/comment/page.tsx @@ -0,0 +1,29 @@ +import React from 'react'; +import CommentModal from '@/components/comment/modal'; + +const page = () => { + const handleCommentSubmit = (comment: string) => { + // Handle comment submission here + // You can add your logic for processing the comment + // For example: API call, state update, etc. + + // For now, we'll just acknowledge the comment + // This prevents the linter warning about unused parameters + if (comment && comment.trim()) { + // Comment is valid and can be processed + // Add your comment handling logic here + } + }; + + return ( +
+ + + +
+ ); +}; + +export default page; diff --git a/app/page.tsx b/app/page.tsx index ed1b2cc14..c1d7498a2 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -8,6 +8,7 @@ import RecentProjects from '@/components/overview/RecentProjects'; import RecentContributions from '@/components/overview/ReecntContributions'; import GrantHistory from '@/components/overview/GrantHistory'; import { AuthNav } from '@/components/auth/AuthNav'; +import CommentModal from '@/components/comment/modal'; import { motion } from 'framer-motion'; import { fadeInUp, @@ -47,14 +48,20 @@ export default function Home() { description='Start by sharing your first project idea with the Boundless community. Once submitted, your projects will appear here for easy tracking.' type='default' action={ - } - iconPosition='right' + + console.log('Comment submitted:', comment) + } > - Add comment - + } + iconPosition='right' + > + Add comment + + } /> diff --git a/components/comment/modal.tsx b/components/comment/modal.tsx new file mode 100644 index 000000000..42b802196 --- /dev/null +++ b/components/comment/modal.tsx @@ -0,0 +1,563 @@ +'use client'; + +import React, { useState, useRef, useEffect } from 'react'; +import { Dialog, DialogContent, DialogTrigger } from '@/components/ui/dialog'; +import Image from 'next/image'; +import { toast } from 'sonner'; + +import { Button } from '@/components/ui/button'; +import { ArrowLeft, X, MessageCircle, Heart, Trash2 } from 'lucide-react'; +import { Stepper } from '@/components/stepper'; + +interface Reply { + id: string; + text: string; + author: string; + timestamp: string; + likes: number; + isLiked: boolean; +} + +interface Comment { + id: string; + text: string; + author: string; + timestamp: string; + replies: Reply[]; + likes: number; + isLiked: boolean; +} + +interface CommentModalProps { + children: React.ReactNode; + onCommentSubmit?: (comment: string) => void; +} + +const CommentModal: React.FC = ({ + children, + onCommentSubmit, +}) => { + const [comment, setComment] = useState(''); + const [isOpen, setIsOpen] = useState(false); + const [comments, setComments] = useState([]); + const [replyMode, setReplyMode] = useState<{ + isActive: boolean; + commentId: string | null; + }>({ isActive: false, commentId: null }); + const [replyText, setReplyText] = useState(''); + const textareaRef = useRef(null); + const replyTextareaRef = useRef(null); + + const campaignSteps = [ + { + title: 'Initialize', + description: + 'Submit your project idea and define milestones to begin your campaign journey.', + state: 'completed' as const, + }, + { + title: 'Validate', + description: + 'Get admin approval and gather public support through voting.', + state: 'active' as const, + }, + { + title: 'Launch Campaign', + description: + 'Finalize campaign details and deploy smart escrow to go live and receive funding.', + state: 'pending' as const, + }, + ]; + + const handleSubmit = () => { + if (comment.trim()) { + const newComment: Comment = { + id: Date.now().toString(), + text: comment.trim(), + author: 'Collins Odumeje', + timestamp: '5s', + replies: [], + likes: 0, + isLiked: false, + }; + + setComments(prev => [newComment, ...prev]); + setComment(''); + toast.success('Comment Posted!', { + action: { + label: 'Undo', + onClick: () => { + setComments(prev => prev.filter(c => c.id !== newComment.id)); + }, + }, + style: { + background: '#3B82F6', + color: 'white', + borderRadius: '8px', + border: 'none', + }, + actionButtonStyle: { + background: 'transparent', + color: 'white', + textDecoration: 'underline', + border: 'none', + }, + }); + + if (onCommentSubmit) { + onCommentSubmit(comment.trim()); + } + } + }; + + const handleLike = (commentId: string) => { + setComments(prev => + prev.map(comment => + comment.id === commentId + ? { + ...comment, + isLiked: !comment.isLiked, + likes: comment.isLiked ? comment.likes - 1 : comment.likes + 1, + } + : comment + ) + ); + }; + + const handleDelete = (commentId: string) => { + setComments(prev => prev.filter(comment => comment.id !== commentId)); + }; + + const handleReplyClick = (commentId: string) => { + setReplyText(''); + setReplyMode({ isActive: true, commentId }); + }; + + useEffect(() => { + if (replyMode.isActive) { + setReplyText(''); + } + }, [replyMode.isActive]); + + const handleReplySubmit = () => { + if (replyText.trim() && replyMode.commentId) { + const newReply: Reply = { + id: Date.now().toString(), + text: replyText.trim(), + author: 'Davedumto', + timestamp: 'just now', + likes: 0, + isLiked: false, + }; + + setComments(prev => + prev.map(comment => + comment.id === replyMode.commentId + ? { ...comment, replies: [...comment.replies, newReply] } + : comment + ) + ); + + setReplyText(''); + + toast.success('Reply Posted!', { + action: { + label: 'Undo', + onClick: () => { + setComments(prev => + prev.map(comment => + comment.id === replyMode.commentId + ? { + ...comment, + replies: comment.replies.filter( + r => r.id !== newReply.id + ), + } + : comment + ) + ); + }, + }, + style: { + background: '#3B82F6', + color: 'white', + borderRadius: '8px', + border: 'none', + }, + actionButtonStyle: { + background: 'transparent', + color: 'white', + textDecoration: 'underline', + border: 'none', + }, + }); + } + }; + + const handleBackToComments = () => { + setReplyMode({ isActive: false, commentId: null }); + setReplyText(''); + }; + + const handleKeyDown = (e: React.KeyboardEvent) => { + if (e.key === 'Enter' && !e.shiftKey) { + e.preventDefault(); + handleSubmit(); + } + }; + + useEffect(() => { + if (textareaRef.current) { + textareaRef.current.style.height = 'auto'; + textareaRef.current.style.height = + textareaRef.current.scrollHeight + 'px'; + } + }, [comment]); + + return ( + <> + {isOpen && ( + + )} + + + {children} + + + +
+
+
+ +
+
+ +
+ {!replyMode.isActive && ( +
+
+ +

+ Comments +

+
+
+
+ User avatar +
+
+