Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions app/(landing)/projects/[id]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ async function ProjectContent({ id }: { id: string }) {
// Try to fetch as crowdfunding project first
try {
const response = await getCrowdfundingProject(id);

if (response.success && response.data) {
projectData = transformCrowdfundingProject(
response.data.project,
Expand All @@ -80,6 +81,7 @@ async function ProjectContent({ id }: { id: string }) {
// If crowdfunding project fails, try regular project
try {
const regularProject = await getProjectDetails(id);

// Transform regular project data if needed
// For regular projects, we need to create a mock CrowdfundingProject structure
const mockProject: CrowdfundingProject = {
Expand Down
74 changes: 74 additions & 0 deletions components/project-details/comment-section/comment-input.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
'use client';
import type React from 'react';
import { useState } from 'react';
import { Button } from '@/components/ui/button';
import { Smile } from 'lucide-react';

interface CommentInputProps {
onSubmit: (content: string) => void;
placeholder?: string;
autoFocus?: boolean;
onCancel?: () => void;
showCancel?: boolean;
}

export function CommentInput({
onSubmit,
placeholder = 'Leave a comment...',
autoFocus = false,
onCancel,
showCancel = false,
}: CommentInputProps) {
const [comment, setComment] = useState('');

const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
if (comment.trim()) {
onSubmit(comment.trim());
setComment('');
}
};

return (
<form
onSubmit={handleSubmit}
className='flex items-center gap-4 rounded-lg border border-neutral-800 bg-black px-6 py-5'
>
<div className='shrink-0'>
<Smile className='size-10 text-neutral-500' />
</div>

<div className='flex-1 rounded-xl border border-[#919191] bg-[#101010] px-6 py-3.5'>
<input
value={comment}
onChange={e => setComment(e.target.value)}
placeholder={placeholder}
autoFocus={autoFocus}
className='w-full text-base text-neutral-200 placeholder:text-neutral-500 focus:outline-none'
/>
</div>

<Button
type='submit'
variant='ghost'
size='sm'
className='shrink-0 px-0 text-base font-normal text-neutral-400 hover:bg-transparent hover:text-white disabled:opacity-50'
disabled={!comment.trim()}
>
Reply
</Button>

{showCancel && (
<Button
type='button'
variant='ghost'
size='sm'
onClick={onCancel}
className='shrink-0 text-base text-neutral-400 hover:text-white'
>
Cancel
</Button>
)}
</form>
);
}
129 changes: 129 additions & 0 deletions components/project-details/comment-section/comment-item.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
'use client';

import { useState } from 'react';
import { Heart, ChevronUp, ChevronDown } from 'lucide-react';
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
import { Button } from '@/components/ui/button';
import { cn } from '@/lib/utils';
import { CommentInput } from './comment-input';
import { Comment } from '@/lib/data/comments-mock';

interface CommentItemProps {
comment: Comment;
isReply?: boolean;
onAddReply: (commentId: string, content: string) => void;
}

export function CommentItem({
comment,
isReply = false,
onAddReply,
}: CommentItemProps) {
const [isLiked, setIsLiked] = useState(false);
const [showReplies, setShowReplies] = useState(false);
const [showReplyInput, setShowReplyInput] = useState(false);
const hasReplies = comment.replies && comment.replies.length > 0;

const handleReplySubmit = (content: string) => {
onAddReply(comment.id, content);
setShowReplyInput(false);
setShowReplies(true);
};

return (
<div className={cn('flex gap-3', isReply && 'ml-12 md:ml-14')}>
<Avatar className='size-8 shrink-0 md:size-10'>
<AvatarImage src={comment.author.avatar} alt={comment.author.name} />
<AvatarFallback>{comment.author.name[0]}</AvatarFallback>
</Avatar>

<div className='min-w-0 flex-1'>
<div className='flex items-start justify-between gap-2'>
<div className='min-w-0 flex-1'>
<button className='text-sm font-medium text-white underline-offset-2 hover:underline'>
{comment.author.name}
</button>
<p className='mt-1 text-sm break-words text-white md:text-base'>
{comment.content}
</p>
</div>
</div>

<div className='mt-2 flex items-center gap-4'>
<span className='text-xs text-zinc-400 md:text-sm'>
{comment.timestamp}
</span>
<Button
variant='ghost'
size='sm'
onClick={() => setShowReplyInput(!showReplyInput)}
className='h-auto p-0 text-xs text-zinc-400 hover:bg-transparent hover:text-white md:text-sm'
>
Reply
</Button>
<button
onClick={() => setIsLiked(!isLiked)}
className='group flex items-center gap-1.5'
>
<Heart
className={cn(
'size-4 transition-colors',
isLiked
? 'fill-red-500 text-red-500'
: 'text-zinc-400 group-hover:text-white'
)}
/>
<span className='text-xs text-zinc-400 group-hover:text-white md:text-sm'>
{comment.likes}
</span>
</button>
</div>

{showReplyInput && (
<div className='mt-3 -ml-12 md:-ml-14'>
<CommentInput
onSubmit={handleReplySubmit}
placeholder='Write a reply...'
autoFocus
onCancel={() => setShowReplyInput(false)}
showCancel
/>
</div>
)}

{hasReplies && (
<>
<button
onClick={() => setShowReplies(!showReplies)}
className='mt-3 flex items-center gap-2 text-xs text-zinc-400 transition-colors hover:text-white md:text-sm'
>
<div className='h-px w-8 bg-zinc-700 md:w-12' />
<span>
{showReplies ? 'Hide' : 'Show'} replies (
{comment.replies?.length})
</span>
{showReplies ? (
<ChevronUp className='size-3.5' />
) : (
<ChevronDown className='size-3.5' />
)}
</button>

{showReplies && (
<div className='mt-4 space-y-4'>
{comment.replies?.map(reply => (
<CommentItem
key={reply.id}
comment={reply}
isReply
onAddReply={onAddReply}
/>
))}
</div>
)}
</>
)}
</div>
</div>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
'use client';

import Image from 'next/image';
import { CommentInput } from './comment-input';

interface CommentsEmptyStateProps {
onAddComment: (content: string) => void;
}

export function CommentsEmptyState({ onAddComment }: CommentsEmptyStateProps) {
return (
<div className='flex w-full flex-col'>
<div className='flex flex-col items-center justify-center px-4 py-16 md:py-20'>
<div className='relative mb-8 h-48 w-48 md:h-56 md:w-56'>
<Image
src='/Humanoid.png'
alt='No comments yet'
fill
className='object-contain'
priority
/>
</div>
<h3 className='mb-8 text-center text-base font-medium text-white md:text-lg'>
Be the first to Leave a Comment
</h3>
</div>
<CommentInput onSubmit={onAddComment} />
</div>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
'use client';

import { useState } from 'react';
import { ListFilter } from 'lucide-react';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuRadioGroup,
DropdownMenuRadioItem,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu';
import { Button } from '@/components/ui/button';

type SortOption = 'latest' | 'oldest' | 'most-relevant';

const sortLabels: Record<SortOption, string> = {
latest: 'Latest',
oldest: 'Oldest',
'most-relevant': 'Most relevant',
};

export function CommentsSortDropdown() {
const [sortBy, setSortBy] = useState<SortOption>('latest');

return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
variant='outline'
size='sm'
className='h-8 gap-2 border-[#22c55e]/30 bg-black px-3 text-[#22c55e] hover:bg-[#22c55e]/10 hover:text-[#22c55e] md:h-9 md:px-4'
>
<ListFilter className='size-4' />
<span className='hidden md:inline'>{sortLabels[sortBy]}</span>
<span className='md:hidden'>Latest</span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent
align='start'
className='w-48 border-zinc-800 bg-black'
>
<DropdownMenuRadioGroup
value={sortBy}
onValueChange={value => setSortBy(value as SortOption)}
>
<DropdownMenuRadioItem
value='latest'
className='text-white data-[state=checked]:text-[#22c55e]'
>
Latest
</DropdownMenuRadioItem>
<DropdownMenuRadioItem
value='oldest'
className='text-white data-[state=checked]:text-[#22c55e]'
>
Oldest
</DropdownMenuRadioItem>
<DropdownMenuRadioItem
value='most-relevant'
className='text-white data-[state=checked]:text-[#22c55e]'
>
Most relevant
</DropdownMenuRadioItem>
</DropdownMenuRadioGroup>
</DropdownMenuContent>
</DropdownMenu>
);
}
Loading
Loading