diff --git a/app/user/page.tsx b/app/user/page.tsx index 579caeb78..26c72b383 100644 --- a/app/user/page.tsx +++ b/app/user/page.tsx @@ -3,11 +3,12 @@ import { PriceDisplay } from '@/components/PriceDisplay'; import Card from '@/components/card'; import RecentProjects from '@/components/overview/RecentProjects'; import { mockProjects } from '@/lib/mock'; -import RecentContributions from '@/components/overview/ReecntContributions'; -import GrantHistory from '@/components/overview/GrantHistory'; +// import RecentContributions from '@/components/overview/ReecntContributions'; +// import GrantHistory from '@/components/overview/GrantHistory'; import PageTransition from '@/components/PageTransition'; import { Coins, History } from 'lucide-react'; import { useAuth } from '@/hooks/use-auth'; +import CampaignTable from '@/components/campaigns/CampaignTable'; import { useEffect, useState } from 'react'; import LoadingSpinner from '@/components/LoadingSpinner'; @@ -93,12 +94,15 @@ export default function UserPage() {
{/* Recent Projects - Full Width */} +
+ +
{/* Recent Contributions and Grant History - Side by Side on larger screens */} -
+ {/*
-
+
*/}
diff --git a/components/campaigns/CampaignSummary.tsx b/components/campaigns/CampaignSummary.tsx new file mode 100644 index 000000000..f3c2464d3 --- /dev/null +++ b/components/campaigns/CampaignSummary.tsx @@ -0,0 +1,362 @@ +import React, { useState } from 'react'; +import BoundlessSheet from '../sheet/boundless-sheet'; +import Image from 'next/image'; +import { Badge } from '../ui/badge'; +import { Avatar, AvatarFallback, AvatarImage } from '../ui/avatar'; +import Link from 'next/link'; +import { Progress } from '../ui/progress'; +import { BoundlessButton } from '../buttons'; +import { + CalendarIcon, + Clock, + ChevronDown, + ChevronRight, + MessageCircleMore, + ThumbsUp, + Users, + Wallet, + Check, +} from 'lucide-react'; +import { ScrollArea } from '../ui/scroll-area'; +import { formatDate } from '@/lib/utils'; +import { Button } from '../ui/button'; + +const CampaignSummary = ({ + open, + setOpen, +}: { + open: boolean; + setOpen: (open: boolean) => void; +}) => { + const [expandedMilestone, setExpandedMilestone] = useState( + null + ); + + // Mock milestone data + const milestones = [ + { + id: '1', + title: 'Project Planning & Research', + description: + 'Complete market research, competitor analysis, and detailed project planning with timeline and resource allocation.', + deliveryDate: new Date('2024-03-15'), + fundPercentage: 20, + }, + { + id: '2', + title: 'MVP Development', + description: + 'Develop and test the minimum viable product with core features and basic functionality.', + deliveryDate: new Date('2024-05-20'), + fundPercentage: 40, + }, + { + id: '3', + title: 'Beta Testing & Refinement', + description: + 'Conduct comprehensive beta testing, gather user feedback, and implement necessary improvements.', + deliveryDate: new Date('2024-07-10'), + fundPercentage: 25, + }, + { + id: '4', + title: 'Launch & Marketing', + description: + 'Official product launch with marketing campaign and user acquisition strategy.', + deliveryDate: new Date('2024-08-30'), + fundPercentage: 15, + }, + ]; + + const toggle = (id: string) => { + setExpandedMilestone(expandedMilestone === id ? null : id); + }; + + const calculateFundAmount = (percentage: number) => { + return (123000 * percentage) / 100; + }; + + // Mock backing history data + const backingHistory = [ + { + id: '1', + name: 'Collins Odumeje', + wallet: 'GDS3...GB7', + amount: 2300, + time: '3s', + avatar: 'https://github.com/shadcn.png', + isVerified: true, + }, + { + id: '2', + name: 'Collins Odumeje', + wallet: 'GDS3...GB7', + amount: 2300, + time: '19d', + avatar: 'https://github.com/shadcn.png', + isVerified: true, + }, + { + id: '3', + name: 'Collins Odumeje', + wallet: 'GDS3...GB7', + amount: 2300, + time: '2w', + avatar: 'https://github.com/shadcn.png', + isVerified: true, + }, + { + id: '4', + name: 'Anonymous', + wallet: 'GDS3...GB7', + amount: 2300, + time: 'Aug 05, 2025', + avatar: null, + isVerified: false, + }, + { + id: '5', + name: 'Collins Odumeje', + wallet: 'GDS3...GB7', + amount: 2300, + time: 'Aug 05, 2025', + avatar: 'https://github.com/shadcn.png', + isVerified: true, + }, + ]; + return ( + +
+
+
+ Campaign Summary +
+
+
+
+

Boundless

+ Successful +
+
+ #web3 + #crowdfunding +
+
+
+ + + CN + +
+

+ Collins Odumeje +

+
+
+

+ This campaign successfully reached its funding goal. Contributions + are now being distributed through escrow as milestones are + completed.{' '} + + Click to track milestone progress + +

+
+
+
+
+

Raised

+

+ $123,000.00 +

+
+
+

Target

+

+ $123,000.00 +

+
+
+ +
+
+ + + 100k + + + + 100k + +
+
+
+ + 100 backers +
+
+
+ + 100 days left +
+
+
+
+
+
+

+ Campaign Details +

+ +

+ Boundless is a trustless, decentralized application (dApp) that + empowers changemakers and builders to raise funds transparently + without intermediaries. Campaigns are structured around clearly + defined milestones, with funds held in escrow and released only + upon approval. Grant creators can launch programs with + rule-based logic, and applicants can apply with proposals that + go through public validation. The platform is built on the + Stellar blockchain and powered by Soroban smart contracts to + ensure transparency, security, and autonomy. +

+
+
+
+

Milestones

+
+ {milestones.map((milestone, idx) => { + const isExpanded = expandedMilestone === milestone.id; + return ( +
+
+ Milestone {idx + 1} +
+
+
toggle(milestone.id)} + > +
+ {milestone.title || `Milestone ${idx + 1}`} +
+ +
+ {isExpanded && ( +
+
+ {milestone.description} +
+
+
+ + + {formatDate(milestone.deliveryDate)} + +
+
+ + $ + {calculateFundAmount( + milestone.fundPercentage + ).toLocaleString()}{' '} + ({milestone.fundPercentage || 0}%) + +
+
+
+ )} +
+
+ ); + })} +
+
+
+
+

+ Backing History +

+ +
+
+ {backingHistory.map(backer => ( +
+
+
+ + + + {backer.name.charAt(0)} + + + {backer.isVerified && ( +
+ +
+ )} +
+
+
+ {backer.name} +
+
+ + {backer.wallet} +
+
+
+
+
+ ${backer.amount.toLocaleString()} +
+
+
+
{backer.time}
+
+
+ ))} +
+
+
+
+
+
+ ); +}; + +export default CampaignSummary; diff --git a/components/campaigns/CampaignTable.tsx b/components/campaigns/CampaignTable.tsx new file mode 100644 index 000000000..5c84dcb7e --- /dev/null +++ b/components/campaigns/CampaignTable.tsx @@ -0,0 +1,799 @@ +import Link from 'next/link'; +import React, { useState, useEffect, useCallback } from 'react'; +import { Button } from '../ui/button'; +import { + ChevronRightIcon, + MoreVerticalIcon, + CheckIcon, + ChevronDownIcon, + Loader2Icon, +} from 'lucide-react'; +import { Tabs, TabsList, TabsTrigger } from '../ui/tabs'; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from '../ui/dropdown-menu'; +import { BoundlessButton } from '../buttons'; +import { Avatar, AvatarFallback, AvatarImage } from '../ui/avatar'; +import { Badge } from '../ui/badge'; +import Image from 'next/image'; +import { toast } from 'sonner'; +import CampaignSummary from './CampaignSummary'; + +interface Campaign { + id: string; + name: string; + creator: { + name: string; + avatar: string; + verified: boolean; + }; + fundingProgress: { + current: number; + target: number; + }; + endDate: string; + milestones: number; + status: 'live' | 'successful' | 'failed'; + tags: string[]; + likes: number; + comments: number; + createdAt: string; + updatedAt: string; +} + +interface ApiResponse { + data: Campaign[]; + total: number; + page: number; + limit: number; + success: boolean; + message?: string; +} + +type StatusFilter = 'all' | 'live' | 'successful' | 'failed'; +type TabFilter = 'mine' | 'others'; + +const mockApiService = { + async fetchCampaigns( + statusFilter: StatusFilter = 'all', + // eslint-disable-next-line @typescript-eslint/no-unused-vars + _tabFilter: TabFilter = 'mine', + page: number = 1, + limit: number = 10 + ): Promise { + await new Promise(resolve => setTimeout(resolve, 1000)); + + const mockCampaigns: Campaign[] = [ + { + id: '1', + name: 'Boundless Web3 Platform', + creator: { + name: 'Collins Odumeje', + avatar: 'https://github.com/shadcn.png', + verified: true, + }, + fundingProgress: { + current: 23000, + target: 250000, + }, + endDate: 'Sept 30', + milestones: 6, + status: 'live', + tags: ['#web3', '#crowdfunding'], + likes: 29, + comments: 12, + createdAt: '2024-01-15T10:00:00Z', + updatedAt: '2024-01-20T14:30:00Z', + }, + { + id: '2', + name: 'DeFi Innovation Hub', + creator: { + name: 'Sarah Johnson', + avatar: 'https://github.com/shadcn.png', + verified: true, + }, + fundingProgress: { + current: 250000, + target: 250000, + }, + endDate: 'Sept 30', + milestones: 6, + status: 'successful', + tags: ['#defi', '#innovation'], + likes: 29, + comments: 23, + createdAt: '2024-01-10T09:00:00Z', + updatedAt: '2024-01-25T16:45:00Z', + }, + { + id: '3', + name: 'NFT Marketplace', + creator: { + name: 'Mike Chen', + avatar: 'https://github.com/shadcn.png', + verified: false, + }, + fundingProgress: { + current: 23000, + target: 250000, + }, + endDate: 'Expired', + milestones: 6, + status: 'failed', + tags: ['#nft', '#marketplace'], + likes: 29, + comments: 12, + createdAt: '2024-01-05T11:00:00Z', + updatedAt: '2024-01-18T13:20:00Z', + }, + { + id: '4', + name: 'Blockchain Education Platform', + creator: { + name: 'Emma Wilson', + avatar: 'https://github.com/shadcn.png', + verified: true, + }, + fundingProgress: { + current: 180000, + target: 200000, + }, + endDate: 'Oct 15', + milestones: 8, + status: 'live', + tags: ['#education', '#blockchain'], + likes: 45, + comments: 18, + createdAt: '2024-01-12T08:00:00Z', + updatedAt: '2024-01-22T15:10:00Z', + }, + ]; + + let filteredCampaigns = mockCampaigns; + if (statusFilter !== 'all') { + filteredCampaigns = mockCampaigns.filter( + campaign => campaign.status === statusFilter + ); + } + + const startIndex = (page - 1) * limit; + const endIndex = startIndex + limit; + const paginatedCampaigns = filteredCampaigns.slice(startIndex, endIndex); + + return { + data: paginatedCampaigns, + total: filteredCampaigns.length, + page, + limit, + success: true, + message: 'Campaigns fetched successfully', + }; + }, + + async likeCampaign( + // eslint-disable-next-line @typescript-eslint/no-unused-vars + _campaignId: string + ): Promise<{ success: boolean; message: string }> { + await new Promise(resolve => setTimeout(resolve, 500)); + return { success: true, message: 'Campaign liked successfully' }; + }, + + async commentCampaign( + // eslint-disable-next-line @typescript-eslint/no-unused-vars + _campaignId: string, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + _comment: string + ): Promise<{ success: boolean; message: string }> { + await new Promise(resolve => setTimeout(resolve, 500)); + return { success: true, message: 'Comment added successfully' }; + }, + + async shareCampaign( + // eslint-disable-next-line @typescript-eslint/no-unused-vars + _campaignId: string + ): Promise<{ success: boolean; message: string }> { + await new Promise(resolve => setTimeout(resolve, 500)); + return { success: true, message: 'Campaign shared successfully' }; + }, +}; + +const CampaignRow = ({ + campaign, + onAction, +}: { + campaign: Campaign; + onAction: (action: string, campaignId: string) => Promise; +}) => { + const getStatusColor = (status: string) => { + switch (status) { + case 'live': + return 'bg-[#1671D9] text-white'; + case 'successful': + return 'bg-primary text-background'; + case 'failed': + return 'bg-[#D42620] text-white'; + default: + return 'bg-gray-500 text-white'; + } + }; + + const getProgressColor = (status: string) => { + switch (status) { + case 'live': + return 'bg-[#1671D9]'; + case 'successful': + return 'bg-primary'; + case 'failed': + return 'bg-[#919191]'; + default: + return 'bg-[#919191]'; + } + }; + + const progressPercentage = + (campaign.fundingProgress.current / campaign.fundingProgress.target) * 100; + + const handleAction = (action: string) => { + onAction(action, campaign.id); + }; + + return ( + <> +
+
+
+ {campaign.name} +
+
+
+ {campaign.name} +
+
+ {campaign.tags.join(' ')} +
+
+
+ +
+
+ + + + {campaign.creator.name + .split(' ') + .map(n => n[0]) + .join('')} + + + {campaign.creator.verified && ( +
+ +
+ )} +
+ + {campaign.creator.name} + +
+ +
+
+ ${campaign.fundingProgress.current.toLocaleString()} / + + ${campaign.fundingProgress.target.toLocaleString()} + +
+
+
+
+
+ +
+ {campaign.endDate} +
+ +
+ {campaign.milestones} +
+ +
+ + {campaign.status} + +
+ +
+ + + + + + {campaign.status === 'live' && ( + <> + handleAction('share')} + className='text-white font-medium hover:!text-white text-sm py-2 px-3 rounded-md hover:!bg-[#2B2B2B] hover:shadow-[0_1px_4px_0_rgba(40,45,40,0.04),_0_0_24px_1px_rgba(10,15,10,0.14)] transition-colors duration-200 cursor-pointer' + > + Share + + handleAction('view-history')} + className='text-white font-medium hover:!text-white text-sm py-2 px-3 rounded-md hover:!bg-[#2B2B2B] hover:shadow-[0_1px_4px_0_rgba(40,45,40,0.04),_0_0_24px_1px_rgba(10,15,10,0.14)] transition-colors duration-200 cursor-pointer' + > + View History + + handleAction('like')} + className='text-white font-medium hover:!text-white text-sm py-2 px-3 rounded-md hover:!bg-[#2B2B2B] hover:shadow-[0_1px_4px_0_rgba(40,45,40,0.04),_0_0_24px_1px_rgba(10,15,10,0.14)] transition-colors duration-200 cursor-pointer' + > + Like ({campaign.likes}) + + handleAction('comment')} + className='text-white font-medium hover:!text-white text-sm py-2 px-3 rounded-md hover:!bg-[#2B2B2B] hover:shadow-[0_1px_4px_0_rgba(40,45,40,0.04),_0_0_24px_1px_rgba(10,15,10,0.14)] transition-colors duration-200 cursor-pointer' + > + Comment ({campaign.comments}) + + handleAction('campaign-details')} + className='text-white font-medium hover:!text-white text-sm py-2 px-3 rounded-md hover:!bg-[#2B2B2B] hover:shadow-[0_1px_4px_0_rgba(40,45,40,0.04),_0_0_24px_1px_rgba(10,15,10,0.14)] transition-colors duration-200 cursor-pointer' + > + Campaign Details + + + )} + {campaign.status === 'successful' && ( + <> + handleAction('view-summary')} + className='text-white font-medium hover:!text-white text-sm py-2 px-3 rounded-md hover:!bg-[#2B2B2B] hover:shadow-[0_1px_4px_0_rgba(40,45,40,0.04),_0_0_24px_1px_rgba(10,15,10,0.14)] transition-colors duration-200 cursor-pointer' + > + View Summary + + handleAction('like')} + className='text-white font-medium hover:!text-white text-sm py-2 px-3 rounded-md hover:!bg-[#2B2B2B] hover:shadow-[0_1px_4px_0_rgba(40,45,40,0.04),_0_0_24px_1px_rgba(10,15,10,0.14)] transition-colors duration-200 cursor-pointer' + > + Like ({campaign.likes}) + + handleAction('comment')} + className='text-white font-medium hover:!text-white text-sm py-2 px-3 rounded-md hover:!bg-[#2B2B2B] hover:shadow-[0_1px_4px_0_rgba(40,45,40,0.04),_0_0_24px_1px_rgba(10,15,10,0.14)] transition-colors duration-200 cursor-pointer' + > + Comment ({campaign.comments}) + + + )} + {campaign.status === 'failed' && ( + <> + handleAction('view-summary')} + className='text-white font-medium hover:!text-white text-sm py-2 px-3 rounded-md hover:!bg-[#2B2B2B] hover:shadow-[0_1px_4px_0_rgba(40,45,40,0.04),_0_0_24px_1px_rgba(10,15,10,0.14)] transition-colors duration-200 cursor-pointer' + > + View Summary + + handleAction('like')} + className='text-white font-medium hover:!text-white text-sm py-2 px-3 rounded-md hover:!bg-[#2B2B2B] hover:shadow-[0_1px_4px_0_rgba(40,45,40,0.04),_0_0_24px_1px_rgba(10,15,10,0.14)] transition-colors duration-200 cursor-pointer' + > + Like ({campaign.likes}) + + handleAction('comment')} + className='text-white font-medium hover:!text-white text-sm py-2 px-3 rounded-md hover:!bg-[#2B2B2B] hover:shadow-[0_1px_4px_0_rgba(40,45,40,0.04),_0_0_24px_1px_rgba(10,15,10,0.14)] transition-colors duration-200 cursor-pointer' + > + Comment ({campaign.comments}) + + + )} + + +
+
+ +
+
+
+
+ {campaign.name} +
+
+
+ {campaign.name} +
+
+ {campaign.tags.join(' ')} +
+
+
+
+ + {campaign.status} + + + + + + + {campaign.status === 'live' && ( + <> + handleAction('share')} + className='text-white font-medium hover:!text-white text-sm py-2 px-3 rounded-md hover:!bg-[#2B2B2B] transition-colors duration-200 cursor-pointer' + > + Share + + handleAction('view-history')} + className='text-white font-medium hover:!text-white text-sm py-2 px-3 rounded-md hover:!bg-[#2B2B2B] transition-colors duration-200 cursor-pointer' + > + View History + + handleAction('like')} + className='text-white font-medium hover:!text-white text-sm py-2 px-3 rounded-md hover:!bg-[#2B2B2B] transition-colors duration-200 cursor-pointer' + > + Like ({campaign.likes}) + + handleAction('comment')} + className='text-white font-medium hover:!text-white text-sm py-2 px-3 rounded-md hover:!bg-[#2B2B2B] transition-colors duration-200 cursor-pointer' + > + Comment ({campaign.comments}) + + handleAction('campaign-details')} + className='text-white font-medium hover:!text-white text-sm py-2 px-3 rounded-md hover:!bg-[#2B2B2B] transition-colors duration-200 cursor-pointer' + > + Campaign Details + + + )} + {campaign.status === 'successful' && ( + <> + handleAction('view-summary')} + className='text-white font-medium hover:!text-white text-sm py-2 px-3 rounded-md hover:!bg-[#2B2B2B] transition-colors duration-200 cursor-pointer' + > + View Summary + + handleAction('like')} + className='text-white font-medium hover:!text-white text-sm py-2 px-3 rounded-md hover:!bg-[#2B2B2B] transition-colors duration-200 cursor-pointer' + > + Like ({campaign.likes}) + + handleAction('comment')} + className='text-white font-medium hover:!text-white text-sm py-2 px-3 rounded-md hover:!bg-[#2B2B2B] transition-colors duration-200 cursor-pointer' + > + Comment ({campaign.comments}) + + + )} + {campaign.status === 'failed' && ( + <> + handleAction('view-summary')} + className='text-white font-medium hover:!text-white text-sm py-2 px-3 rounded-md hover:!bg-[#2B2B2B] transition-colors duration-200 cursor-pointer' + > + View Summary + + handleAction('like')} + className='text-white font-medium hover:!text-white text-sm py-2 px-3 rounded-md hover:!bg-[#2B2B2B] transition-colors duration-200 cursor-pointer' + > + Like ({campaign.likes}) + + handleAction('comment')} + className='text-white font-medium hover:!text-white text-sm py-2 px-3 rounded-md hover:!bg-[#2B2B2B] transition-colors duration-200 cursor-pointer' + > + Comment ({campaign.comments}) + + + )} + + +
+
+ +
+
+
+ + + + {campaign.creator.name + .split(' ') + .map(n => n[0]) + .join('')} + + + {campaign.creator.verified && ( +
+ +
+ )} +
+ + {campaign.creator.name} + +
+ +
+
+ ${campaign.fundingProgress.current.toLocaleString()} / $ + {campaign.fundingProgress.target.toLocaleString()} +
+
+
+
+
+ +
+ + End Date: {campaign.endDate} + + + Milestones:{' '} + {campaign.milestones} + +
+
+
+ + ); +}; + +const CampaignTable = () => { + const [statusFilter, setStatusFilter] = useState('all'); + const [tabFilter, setTabFilter] = useState('mine'); + const [campaigns, setCampaigns] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const [campaignSummaryOpen, setCampaignSummaryOpen] = useState(false); + const filterOptions = [ + { value: 'all', label: 'All' }, + { value: 'live', label: 'Live' }, + { value: 'successful', label: 'Successful' }, + { value: 'failed', label: 'Failed' }, + ]; + + const fetchCampaigns = useCallback(async () => { + try { + setLoading(true); + setError(null); + const response = await mockApiService.fetchCampaigns( + statusFilter, + tabFilter + ); + setCampaigns(response.data); + } catch { + setError('Failed to fetch campaigns'); + toast.error('Failed to fetch campaigns'); + } finally { + setLoading(false); + } + }, [statusFilter, tabFilter]); + + const handleCampaignAction = async (action: string, campaignId: string) => { + try { + switch (action) { + case 'like': + await mockApiService.likeCampaign(campaignId); + toast.success('Campaign liked successfully'); + break; + case 'comment': + await mockApiService.commentCampaign(campaignId, 'Great campaign!'); + toast.success('Comment added successfully'); + break; + case 'share': + await mockApiService.shareCampaign(campaignId); + toast.success('Campaign shared successfully'); + break; + case 'view-summary': + setCampaignSummaryOpen(true); + break; + case 'view-history': + toast.info('Opening campaign history...'); + break; + case 'campaign-details': + toast.info('Opening campaign details...'); + break; + default: + toast.info(`Action: ${action}`); + } + + await fetchCampaigns(); + } catch { + toast.error('Action failed'); + } + }; + + useEffect(() => { + fetchCampaigns(); + }, [fetchCampaigns]); + + return ( +
+
+
+

+ Latest Campaigns +

+ + + +
+
+ setTabFilter(value as TabFilter)} + className='w-full sm:w-auto' + > + + + Mine + + + Others + + + + + + + { + filterOptions.find(option => option.value === statusFilter) + ?.label + }{' '} + + + + + {filterOptions.map(option => ( + + setStatusFilter( + option.value as 'all' | 'live' | 'successful' | 'failed' + ) + } + className='text-white font-medium hover:!text-white text-sm py-2 px-3 rounded-md hover:!bg-[#2B2B2B] hover:shadow-[0_1px_4px_0_rgba(40,45,40,0.04),_0_0_24px_1px_rgba(10,15,10,0.14)] transition-colors duration-200 cursor-pointer' + > + {option.label} + + ))} + + +
+
+ +
+
Campaign Name
+
Creator
+
Funding Progress
+
End Date
+
Milestones
+
Status
+
Actions
+
+ +
+ {loading ? ( +
+
+ + Loading campaigns... +
+
+ ) : error ? ( +
+
+

{error}

+ +
+
+ ) : campaigns.length === 0 ? ( +
+
+

No campaigns found

+

+ Try adjusting your filters +

+
+
+ ) : ( + campaigns.map(campaign => ( + + )) + )} +
+ +
+ ); +}; + +export default CampaignTable; diff --git a/components/sheet/boundless-sheet.tsx b/components/sheet/boundless-sheet.tsx index 67b74beac..e7f1114f8 100644 --- a/components/sheet/boundless-sheet.tsx +++ b/components/sheet/boundless-sheet.tsx @@ -136,7 +136,7 @@ const BoundlessSheet: React.FC = ({