diff --git a/app/(landing)/me/profile-data.tsx b/app/(landing)/me/profile-data.tsx index e8e3b044d..c26a91493 100644 --- a/app/(landing)/me/profile-data.tsx +++ b/app/(landing)/me/profile-data.tsx @@ -1,6 +1,6 @@ import { getMe } from '@/lib/api/auth'; import { auth } from '@/auth'; -import ProfileOverview from '@/components/profile/ProfileOverview'; +import ProfileDataClient from '@/components/profile/ProfileDataClient'; export async function ProfileData() { const session = await auth(); @@ -17,9 +17,9 @@ export async function ProfileData() { try { const userData = await getMe(session.user.accessToken); return ( - ); } catch (error) { diff --git a/components/ProjectCard.tsx b/components/ProjectCard.tsx index 27915da91..6bfbc6bf2 100644 --- a/components/ProjectCard.tsx +++ b/components/ProjectCard.tsx @@ -20,6 +20,7 @@ interface ProjectCardProps { onValidationClick?: () => void; onVoteClick?: () => void; className?: string; + isFullWidth?: boolean; } const ProjectCard: React.FC = memo( @@ -28,6 +29,7 @@ const ProjectCard: React.FC = memo( creatorName = 'Creator Name', creatorAvatar, className = '', + isFullWidth = false, }) => { const cardRef = useRef(null); const imageRef = useRef(null); @@ -160,7 +162,7 @@ const ProjectCard: React.FC = memo( return (
diff --git a/components/profile/ActivityFeed.tsx b/components/profile/ActivityFeed.tsx new file mode 100644 index 000000000..ed9553ad7 --- /dev/null +++ b/components/profile/ActivityFeed.tsx @@ -0,0 +1,252 @@ +'use client'; + +import { useState } from 'react'; +import { ChevronDown } from 'lucide-react'; +import Image from 'next/image'; + +interface ActivityItem { + id: string; + type: 'comment' | 'back' | 'add' | 'submit' | 'apply' | 'reply'; + description: string; + projectName?: string; + amount?: string; + hackathonName?: string; + grantName?: string; + timestamp: string; + emoji?: string; + image?: string; +} + +const mockActivities: ActivityItem[] = [ + // TODAY + { + id: '1', + type: 'comment', + description: 'Commented on project:', + projectName: 'Bitmed', + emoji: '💡🌍', + timestamp: '2 mins ago', + }, + { + id: '2', + type: 'back', + description: 'Backed project:', + projectName: 'SolarPay', + amount: '$250 USDC', + timestamp: '10 mins ago', + }, + { + id: '3', + type: 'add', + description: 'Added new project:', + projectName: 'EduChain', + timestamp: '30 mins ago', + }, + { + id: '4', + type: 'submit', + description: 'Submitted project:', + projectName: 'EduChain', + hackathonName: 'DeFi Innovators Hackathon', + timestamp: '11 h', + }, + // YESTERDAY + { + id: '5', + type: 'comment', + description: 'Commented on project:', + projectName: 'EduChain', + emoji: '📱', + timestamp: 'Yesterday', + }, + { + id: '6', + type: 'back', + description: 'Backed project:', + projectName: 'AgriChain', + amount: '$75 USDC', + timestamp: 'Yesterday', + }, + // THIS WEEK + { + id: '7', + type: 'apply', + description: 'Applied for grant:', + grantName: 'Open Finance Fund', + timestamp: '2d', + }, + { + id: '8', + type: 'submit', + description: 'Submitted project to hackathon:', + hackathonName: 'Global Impact Hack 2025', + timestamp: '2d', + }, + { + id: '9', + type: 'reply', + description: 'Replied to comment on', + projectName: 'SolarPay', + emoji: '🔜', + timestamp: '3d', + }, + { + id: '10', + type: 'back', + description: 'Backed project:', + projectName: 'GreenGrid', + amount: '$500 USDC', + timestamp: '3d', + }, +]; + +const getActivityDescription = (activity: ActivityItem) => { + let description = activity.description; + + if (activity.projectName) { + description += ` ${activity.projectName}`; + } + + if ( + activity.description.includes('to hackathon:') && + activity.hackathonName + ) { + description += ` ${activity.hackathonName}`; + } + + if (activity.amount) { + description += ` — Funded ${activity.amount}`; + } + + if (activity.emoji) { + description += ` ${activity.emoji}`; + } + + return description; +}; + +interface ActivityFeedProps { + filter: string; +} + +export default function ActivityFeed({ filter }: ActivityFeedProps) { + const [showAll, setShowAll] = useState(false); + + const todayActivities = mockActivities.slice(0, 4); + const yesterdayActivities = mockActivities.slice(4, 6); + const weekActivities = mockActivities.slice(6, 10); + + // Filter activities based on selected filter + const getFilteredActivities = () => { + switch (filter) { + case 'Today': + return [{ title: 'TODAY', activities: todayActivities }]; + case 'Yesterday': + return [{ title: 'YESTERDAY', activities: yesterdayActivities }]; + case 'This Week': + return [ + { title: 'TODAY', activities: todayActivities }, + { title: 'YESTERDAY', activities: yesterdayActivities }, + { title: 'THIS WEEK', activities: weekActivities }, + ]; + case 'This Month': + return [ + { title: 'TODAY', activities: todayActivities }, + { title: 'YESTERDAY', activities: yesterdayActivities }, + { title: 'THIS WEEK', activities: weekActivities }, + ]; + case 'This Year': + return [ + { title: 'TODAY', activities: todayActivities }, + { title: 'YESTERDAY', activities: yesterdayActivities }, + { title: 'THIS WEEK', activities: weekActivities }, + ]; + case 'All Time': + return [ + { title: 'TODAY', activities: todayActivities }, + { title: 'YESTERDAY', activities: yesterdayActivities }, + { title: 'THIS WEEK', activities: weekActivities }, + ]; + default: // 'All' + return [ + { title: 'TODAY', activities: todayActivities }, + { title: 'YESTERDAY', activities: yesterdayActivities }, + { title: 'THIS WEEK', activities: weekActivities }, + ]; + } + }; + + const groupedActivities = getFilteredActivities(); + + return ( +
+ {groupedActivities.map((group, groupIndex) => ( +
+

{group.title}

+
+ {group.activities.map(activity => ( +
+ {activity.projectName +
+

+ {getActivityDescription(activity)} +

+

+ {activity.timestamp} +

+
+
+ ))} +
+
+ ))} + + {showAll && filter === 'All' && ( +
+

EARLIER

+
+ {mockActivities.slice(10).map(activity => ( +
+ {activity.projectName +
+

+ {getActivityDescription(activity)} +

+

+ {activity.timestamp} +

+
+
+ ))} +
+
+ )} + + {filter === 'All' && ( +
+ +
+ )} +
+ ); +} diff --git a/components/profile/ActivityTab.tsx b/components/profile/ActivityTab.tsx new file mode 100644 index 000000000..24eb6204a --- /dev/null +++ b/components/profile/ActivityTab.tsx @@ -0,0 +1,157 @@ +'use client'; + +import { + ChartConfig, + ChartContainer, + ChartTooltip, + ChartTooltipContent, +} from '@/components/ui/chart'; +import { Line, LineChart, XAxis } from 'recharts'; + +export default function ActivityTab() { + const chartData = [ + { + month: 'January', + votes: 186, + grants: 80, + hackathons: 45, + donations: 120, + }, + { + month: 'February', + votes: 305, + grants: 200, + hackathons: 78, + donations: 180, + }, + { month: 'March', votes: 237, grants: 120, hackathons: 92, donations: 95 }, + { month: 'April', votes: 73, grants: 190, hackathons: 65, donations: 210 }, + { month: 'May', votes: 209, grants: 130, hackathons: 88, donations: 150 }, + { month: 'June', votes: 214, grants: 140, hackathons: 72, donations: 175 }, + ]; + + const chartConfig = { + votes: { + label: 'Votes', + color: 'var(--primary)', + }, + grants: { + label: 'Grants', + color: '#099137', + }, + hackathons: { + label: 'Hackathons', + color: '#dd900d', + }, + donations: { + label: 'Donations', + color: '#0d5eba', + }, + } satisfies ChartConfig; + + return ( +
+
+
+

Votes

+

1,224

+
+
+

Grants

+

860

+
+
+

Hackathons

+

440

+
+
+

Donations

+

930

+
+
+
+ {/* line charts */} +
+ + Votes + + + {' '} + Donations + + + {' '} + Hackathons + + + {' '} + Grants + +
+
+
+ + + value.slice(0, 3)} + fontSize={10} + /> + + } + /> + + + + + + +
+
+ ); +} diff --git a/components/profile/FilterControls.tsx b/components/profile/FilterControls.tsx new file mode 100644 index 000000000..3db642cf2 --- /dev/null +++ b/components/profile/FilterControls.tsx @@ -0,0 +1,112 @@ +'use client'; + +import { ChevronDown, Filter } from 'lucide-react'; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from '../ui/dropdown-menu'; + +interface FilterControlsProps { + sortFilter: string; + setSortFilter: (filter: string) => void; + statusFilter: string; + setStatusFilter: (filter: string) => void; + categoryFilter: string; + setCategoryFilter: (filter: string) => void; + showAllFilters?: boolean; +} + +export function FilterControls({ + sortFilter, + setSortFilter, + statusFilter, + setStatusFilter, + categoryFilter, + setCategoryFilter, + showAllFilters = true, +}: FilterControlsProps) { + return ( +
+ + + + + + setSortFilter('Default')}> + Default + + setSortFilter('Newest')}> + Newest + + setSortFilter('Oldest')}> + Oldest + + + + + {showAllFilters && ( + <> + + + + + + setStatusFilter('Status')}> + Status + + setStatusFilter('Funding')}> + Funding + + setStatusFilter('Approved')}> + Approved + + setStatusFilter('Completed')}> + Completed + + + + + + + + + + setCategoryFilter('Category')}> + Category + + setCategoryFilter('Health')}> + Health + + setCategoryFilter('Finance')}> + Finance + + setCategoryFilter('Environment')} + > + Environment + + setCategoryFilter('Education')}> + Education + + setCategoryFilter('Technology')}> + Technology + + + + + )} +
+ ); +} diff --git a/components/profile/FollowersContent.tsx b/components/profile/FollowersContent.tsx new file mode 100644 index 000000000..b48fac763 --- /dev/null +++ b/components/profile/FollowersContent.tsx @@ -0,0 +1,39 @@ +'use client'; + +import { TeamList, TeamMember } from '@/components/ui/TeamList'; +import { FilterControls } from './FilterControls'; + +interface FollowersContentProps { + users?: TeamMember[]; + onMemberClick: (member: TeamMember) => void; + sortFilter: string; + setSortFilter: (filter: string) => void; +} + +export function FollowersContent({ + users, + onMemberClick, + sortFilter, + setSortFilter, +}: FollowersContentProps) { + return ( + <> + {}} + categoryFilter='Category' + setCategoryFilter={() => {}} + showAllFilters={false} + /> + + + + ); +} diff --git a/components/profile/FollowersModal.tsx b/components/profile/FollowersModal.tsx new file mode 100644 index 000000000..658244fb0 --- /dev/null +++ b/components/profile/FollowersModal.tsx @@ -0,0 +1,71 @@ +'use client'; + +import BoundlessSheet from '../sheet/boundless-sheet'; +import { TeamMember } from '@/components/ui/TeamList'; +import { Project } from '@/types/project'; +import { ModalTabs } from './ModalTabs'; +import { FollowersContent } from './FollowersContent'; +import { useProjectFilters } from './useProjectFilters'; +import { mockFollowers, mockProjects } from './mockData'; + +interface FollowersModalProps { + open: boolean; + setOpen: (open: boolean) => void; + type: 'followers' | 'following'; + users?: TeamMember[]; + projects?: Project[]; +} + +export default function FollowersModal({ + open, + setOpen, + type, + users, + projects, +}: FollowersModalProps) { + const { + activeTab, + setActiveTab, + sortFilter, + setSortFilter, + getFilteredProjects, + } = useProjectFilters(projects || mockProjects); + + const handleMemberClick = (member: TeamMember) => { + window.open(`/profile/${member.username}`, '_blank'); + }; + + const displayUsers = users || mockFollowers; + const displayProjects = getFilteredProjects(); + + return ( + +
+ {type === 'following' ? ( + + ) : ( + + )} +
+
+ ); +} + +// Export the mock data for use in ProfileHeader +export { mockFollowers, mockProjects }; diff --git a/components/profile/ModalTabs.tsx b/components/profile/ModalTabs.tsx new file mode 100644 index 000000000..699f6f3d3 --- /dev/null +++ b/components/profile/ModalTabs.tsx @@ -0,0 +1,63 @@ +'use client'; + +import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; +import { TeamList, TeamMember } from '@/components/ui/TeamList'; +import { Project } from '@/types/project'; +import { ProjectList } from './ProjectList'; + +interface ModalTabsProps { + activeTab: string; + setActiveTab: (tab: string) => void; + users?: TeamMember[]; + projects: Project[]; + onMemberClick: (member: TeamMember) => void; +} + +const tabs = ['Projects', 'Hackathons', 'Grants', 'Creators']; + +export function ModalTabs({ + activeTab, + setActiveTab, + users, + projects, + onMemberClick, +}: ModalTabsProps) { + return ( + +
+ + {tabs.map(tab => ( + + {tab} + + ))} + +
+ + + + + + + + + + + + + + + + +
+ ); +} diff --git a/components/profile/OrganizationsTab.tsx b/components/profile/OrganizationsTab.tsx new file mode 100644 index 000000000..74f1aa9eb --- /dev/null +++ b/components/profile/OrganizationsTab.tsx @@ -0,0 +1,18 @@ +'use client'; + +import OrganizationsList from './OrganizationsList'; +import { Organization } from '@/types/profile'; + +interface OrganizationsTabProps { + organizations: Organization[]; +} + +export default function OrganizationsTab({ + organizations, +}: OrganizationsTabProps) { + return ( +
+ +
+ ); +} diff --git a/components/profile/ProfileDataClient.tsx b/components/profile/ProfileDataClient.tsx new file mode 100644 index 000000000..2591501ee --- /dev/null +++ b/components/profile/ProfileDataClient.tsx @@ -0,0 +1,138 @@ +'use client'; + +import { useState } from 'react'; +import { ListFilter } from 'lucide-react'; +import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; +import { GetMeResponse } from '@/lib/api/types'; +import ProfileOverview from './ProfileOverview'; +import ActivityTab from './ActivityTab'; +import ProjectsTab from './ProjectsTab'; +import OrganizationsTab from './OrganizationsTab'; +import ActivityFeed from './ActivityFeed'; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from '../ui/dropdown-menu'; +import { BoundlessButton } from '../buttons'; + +interface ProfileDataClientProps { + user: GetMeResponse; + username: string; +} + +export default function ProfileDataClient({ + user, + username, +}: ProfileDataClientProps) { + const [selectedFilter, setSelectedFilter] = useState('All'); + + const handleFilterChange = (filter: string) => { + setSelectedFilter(filter); + }; + + // Prepare organizations data for the tab + const organizationsData = + user.organizations?.map(org => ({ + name: org.name, + avatarUrl: org.avatar || '/blog1.jpg', + })) || []; + + return ( +
+ + +
+ +
+ + + Activity + + + Projects + + + Organizations + + +
+ +
+ + +
+ + + } + > + {selectedFilter} + + + + handleFilterChange('All')}> + All + + handleFilterChange('Today')} + > + Today + + handleFilterChange('Yesterday')} + > + Yesterday + + handleFilterChange('This Week')} + > + This Week + + handleFilterChange('This Month')} + > + This Month + + handleFilterChange('This Year')} + > + This Year + + handleFilterChange('All Time')} + > + All Time + + + +
+ +
+ +
+
+ + + + + + +
+
+
+
+ ); +} diff --git a/components/profile/ProfileHeader.tsx b/components/profile/ProfileHeader.tsx index 7c28e9ab2..128407318 100644 --- a/components/profile/ProfileHeader.tsx +++ b/components/profile/ProfileHeader.tsx @@ -1,3 +1,6 @@ +'use client'; + +import { useState } from 'react'; import Image from 'next/image'; import Link from 'next/link'; import { UserProfile, UserStats as UserStatsType } from '@/types/profile'; @@ -5,6 +8,7 @@ import { BoundlessButton } from '@/components/buttons'; import { BellPlus } from 'lucide-react'; import { ProfileSocialLinks } from '@/lib/config'; import UserStats from './UserStats'; +import FollowersModal, { mockFollowers, mockProjects } from './FollowersModal'; interface ProfileHeaderProps { profile: UserProfile; @@ -12,6 +16,17 @@ interface ProfileHeaderProps { } export default function ProfileHeader({ profile, stats }: ProfileHeaderProps) { + const [followersModalOpen, setFollowersModalOpen] = useState(false); + const [followingModalOpen, setFollowingModalOpen] = useState(false); + + const handleFollowersClick = () => { + setFollowersModalOpen(true); + }; + + const handleFollowingClick = () => { + setFollowingModalOpen(true); + }; + return (
@@ -56,7 +71,11 @@ export default function ProfileHeader({ profile, stats }: ProfileHeaderProps) {
))}
- +
} iconPosition='right'> Follow @@ -66,6 +85,22 @@ export default function ProfileHeader({ profile, stats }: ProfileHeaderProps) { Share icon
+ + {/* Modals */} + + + ); } diff --git a/components/profile/ProfileOverview.tsx b/components/profile/ProfileOverview.tsx index 7bf08557c..0ca003add 100644 --- a/components/profile/ProfileOverview.tsx +++ b/components/profile/ProfileOverview.tsx @@ -40,10 +40,13 @@ export default function ProfileOverview({ user }: ProfileOverviewProps) { })) || []; return ( -
+
- + {/* Organizations hidden on mobile - moved to tab */} +
+ +
); } diff --git a/components/profile/ProjectList.tsx b/components/profile/ProjectList.tsx new file mode 100644 index 000000000..b2accfd1e --- /dev/null +++ b/components/profile/ProjectList.tsx @@ -0,0 +1,27 @@ +'use client'; + +import { Project } from '@/types/project'; +import ProjectCard from '../ProjectCard'; + +interface ProjectListProps { + projects: Project[]; + activeTab: string; +} + +export function ProjectList({ projects, activeTab }: ProjectListProps) { + if (projects.length === 0) { + return ( +
+ No {activeTab.toLowerCase()} found matching your filters +
+ ); + } + + return ( +
+ {projects.map(project => ( + + ))} +
+ ); +} diff --git a/components/profile/ProjectsTab.tsx b/components/profile/ProjectsTab.tsx new file mode 100644 index 000000000..c4cb988f9 --- /dev/null +++ b/components/profile/ProjectsTab.tsx @@ -0,0 +1,113 @@ +'use client'; + +import { useState, useEffect, useCallback } from 'react'; +import ProjectCard from '../ProjectCard'; +import { Project } from '@/types/project'; +import { mockProjects } from './mockData'; + +export default function ProjectsTab() { + const [projects, setProjects] = useState([]); + const [loading, setLoading] = useState(false); + const [hasMore, setHasMore] = useState(true); + const [page, setPage] = useState(1); + + const itemsPerPage = 6; + + // Simulate API call + const loadProjects = useCallback(async (pageNum: number) => { + setLoading(true); + + // Simulate API delay + await new Promise(resolve => setTimeout(resolve, 1000)); + + const startIndex = (pageNum - 1) * itemsPerPage; + const endIndex = startIndex + itemsPerPage; + const newProjects = mockProjects.slice(startIndex, endIndex); + + if (pageNum === 1) { + setProjects(newProjects); + } else { + setProjects(prev => [...prev, ...newProjects]); + } + + setHasMore(endIndex < mockProjects.length); + setLoading(false); + }, []); + + // Load initial projects + useEffect(() => { + loadProjects(1); + }, [loadProjects]); + + // Infinite scroll handler + const handleScroll = useCallback( + (e: React.UIEvent) => { + const { scrollTop, scrollHeight, clientHeight } = e.currentTarget; + + if ( + scrollHeight - scrollTop <= clientHeight + 100 && + !loading && + hasMore + ) { + const nextPage = page + 1; + setPage(nextPage); + loadProjects(nextPage); + } + }, + [loading, hasMore, page, loadProjects] + ); + + if (projects.length === 0 && !loading) { + return ( +
+

+ Your Projects +

+

No projects found

+
+ ); + } + + return ( +
+
+

Your Projects

+ + {projects.length} projects + +
+ +
+ {projects.map(project => ( + + ))} + + {loading && ( +
+
+
+ + Loading more projects... + +
+
+ )} + + {!hasMore && projects.length > 0 && ( +
+

No more projects to load

+
+ )} +
+
+ ); +} diff --git a/components/profile/UserStats.tsx b/components/profile/UserStats.tsx index d88444dfc..3316eb7be 100644 --- a/components/profile/UserStats.tsx +++ b/components/profile/UserStats.tsx @@ -2,31 +2,44 @@ import type { UserStats as UserStatsType } from '@/types/profile'; interface UserStatsProps { stats: UserStatsType; + onFollowersClick?: () => void; + onFollowingClick?: () => void; } -export default function UserStats({ stats }: UserStatsProps) { +export default function UserStats({ + stats, + onFollowersClick, + onFollowingClick, +}: UserStatsProps) { return ( -
-
- +
+
+ {stats.organizations} - Organizations + Organizations + Orgs
-
- +
+ {stats.projects} Projects
-
- +
+ {stats.following} Following
-
- +
+ {stats.followers} Followers diff --git a/components/profile/mockData.ts b/components/profile/mockData.ts new file mode 100644 index 000000000..732209450 --- /dev/null +++ b/components/profile/mockData.ts @@ -0,0 +1,344 @@ +import { Project } from '@/types/project'; +import { TeamMember } from '@/components/ui/TeamList'; + +export const mockFollowers: TeamMember[] = [ + { + id: '1', + name: 'Alice Johnson', + role: 'OWNER', + avatar: '/avatar.png', + username: 'alice_j', + joinedAt: '2024-01-15', + }, + { + id: '2', + name: 'Bob Smith', + role: 'MEMBER', + avatar: '/admin.png', + username: 'bob_smith', + joinedAt: '2024-02-01', + }, + { + id: '3', + name: 'Carol Davis', + role: 'MEMBER', + avatar: '/avatar.png', + username: 'carol_d', + joinedAt: '2024-02-10', + }, + { + id: '4', + name: 'David Wilson', + role: 'MEMBER', + avatar: '/admin.png', + username: 'david_w', + joinedAt: '2024-02-15', + }, + { + id: '5', + name: 'Eva Brown', + role: 'MEMBER', + avatar: '/avatar.png', + username: 'eva_b', + joinedAt: '2024-02-20', + }, + { + id: '6', + name: 'Frank Miller', + role: 'MEMBER', + avatar: '/admin.png', + username: 'frank_m', + joinedAt: '2024-02-25', + }, +]; + +export const mockProjects: Project[] = [ + // Projects + { + id: '1', + name: 'Bitmed', + description: + 'To build a secure, transparent, and trusted digital health ecosystem powered by Sonic blockchain for 280M lives in Indonesia.', + image: '/bitmed.png', + link: '#', + tags: ['Validation', 'Health'], + category: 'healthcare', + type: 'crowdfunding', + amount: 50000, + status: 'funding', + createdAt: '2024-01-01', + updatedAt: '2024-01-15', + owner: 'creator1', + ownerName: 'Creator Name', + ownerUsername: 'creator_name', + ownerAvatar: '/avatar.png', + }, + { + id: '2', + name: 'SolarPay', + description: + 'Revolutionary solar energy payment system using blockchain technology to enable peer-to-peer energy trading in developing countries.', + image: '/bitmed.png', + link: '#', + tags: ['Funding', 'Energy'], + category: 'environment', + type: 'crowdfunding', + amount: 75000, + status: 'funding', + createdAt: '2024-01-05', + updatedAt: '2024-01-20', + owner: 'creator2', + ownerName: 'Creator Name', + ownerUsername: 'creator_name', + ownerAvatar: '/admin.png', + }, + { + id: '3', + name: 'EduChain', + description: + 'Decentralized learning platform for remote students with blockchain-verified certificates and micro-credentials.', + image: '/bitmed.png', + link: '#', + tags: ['Education', 'Certification'], + category: 'education', + type: 'crowdfunding', + amount: 30000, + status: 'funding', + createdAt: '2024-01-10', + updatedAt: '2024-01-25', + owner: 'creator3', + ownerName: 'Creator Name', + ownerUsername: 'creator_name', + ownerAvatar: '/avatar.png', + }, + { + id: '4', + name: 'AgriChain', + description: + 'Blockchain-based supply chain tracking for agricultural products ensuring food safety and transparency from farm to table.', + image: '/bitmed.png', + link: '#', + tags: ['Agriculture', 'Supply Chain'], + category: 'social_impact', + type: 'crowdfunding', + amount: 60000, + status: 'funding', + createdAt: '2024-01-15', + updatedAt: '2024-01-30', + owner: 'creator4', + ownerName: 'Creator Name', + ownerUsername: 'creator_name', + ownerAvatar: '/admin.png', + }, + { + id: '5', + name: 'GreenGrid', + description: + 'Smart grid infrastructure for renewable energy distribution with AI-powered optimization and blockchain-based energy credits.', + image: '/bitmed.png', + link: '#', + tags: ['Energy', 'AI'], + category: 'environment', + type: 'crowdfunding', + amount: 100000, + status: 'funding', + createdAt: '2024-01-20', + updatedAt: '2024-02-05', + owner: 'creator5', + ownerName: 'Creator Name', + ownerUsername: 'creator_name', + ownerAvatar: '/avatar.png', + }, + // Hackathons + { + id: '6', + name: 'DeFi Innovators Hackathon', + description: + 'Build the next generation of decentralized finance applications with focus on cross-chain interoperability and user experience.', + image: '/bitmed.png', + link: '#', + tags: ['DeFi', 'Cross-chain'], + category: 'defi', + type: 'hackathon', + amount: 25000, + status: 'funding', + createdAt: '2024-02-01', + updatedAt: '2024-02-10', + owner: 'hackathon1', + ownerName: 'Hackathon Organizer', + ownerUsername: 'hackathon_org', + ownerAvatar: '/admin.png', + }, + { + id: '7', + name: 'Global Impact Hack 2025', + description: + 'Sustainable technology solutions for climate change, healthcare, and education with focus on real-world impact and scalability.', + image: '/bitmed.png', + link: '#', + tags: ['Climate', 'Impact'], + category: 'social_impact', + type: 'hackathon', + amount: 50000, + status: 'funding', + createdAt: '2024-02-05', + updatedAt: '2024-02-15', + owner: 'hackathon2', + ownerName: 'Hackathon Organizer', + ownerUsername: 'hackathon_org', + ownerAvatar: '/avatar.png', + }, + { + id: '8', + name: 'Web3 Gaming Championship', + description: + 'Create immersive gaming experiences using blockchain technology, NFTs, and play-to-earn mechanics.', + image: '/bitmed.png', + link: '#', + tags: ['Gaming', 'NFTs'], + category: 'web3', + type: 'hackathon', + amount: 40000, + status: 'funding', + createdAt: '2024-02-10', + updatedAt: '2024-02-20', + owner: 'hackathon3', + ownerName: 'Hackathon Organizer', + ownerUsername: 'hackathon_org', + ownerAvatar: '/admin.png', + }, + { + id: '9', + name: 'AI & Blockchain Fusion', + description: + 'Integrate artificial intelligence with blockchain technology to create innovative solutions for data privacy and smart contracts.', + image: '/bitmed.png', + link: '#', + tags: ['AI', 'Privacy'], + category: 'technology', + type: 'hackathon', + amount: 35000, + status: 'funding', + createdAt: '2024-02-15', + updatedAt: '2024-02-25', + owner: 'hackathon4', + ownerName: 'Hackathon Organizer', + ownerUsername: 'hackathon_org', + ownerAvatar: '/avatar.png', + }, + // Grants + { + id: '10', + name: 'Open Finance Fund', + description: + 'Supporting open-source financial infrastructure projects that promote financial inclusion and transparency.', + image: '/bitmed.png', + link: '#', + tags: ['Open Source', 'Finance'], + category: 'defi', + type: 'grant', + amount: 100000, + status: 'funding', + createdAt: '2024-03-01', + updatedAt: '2024-03-10', + owner: 'grant1', + ownerName: 'Grant Provider', + ownerUsername: 'grant_provider', + ownerAvatar: '/admin.png', + }, + { + id: '11', + name: 'Climate Tech Initiative', + description: + 'Funding innovative climate technology solutions including carbon tracking, renewable energy, and environmental monitoring.', + image: '/bitmed.png', + link: '#', + tags: ['Climate', 'Tech'], + category: 'environment', + type: 'grant', + amount: 150000, + status: 'funding', + createdAt: '2024-03-05', + updatedAt: '2024-03-15', + owner: 'grant2', + ownerName: 'Grant Provider', + ownerUsername: 'grant_provider', + ownerAvatar: '/avatar.png', + }, + { + id: '12', + name: 'Healthcare Innovation Grant', + description: + 'Supporting blockchain-based healthcare solutions for patient data management, telemedicine, and medical research.', + image: '/bitmed.png', + link: '#', + tags: ['Healthcare', 'Research'], + category: 'healthcare', + type: 'grant', + amount: 80000, + status: 'funding', + createdAt: '2024-03-10', + updatedAt: '2024-03-20', + owner: 'grant3', + ownerName: 'Grant Provider', + ownerUsername: 'grant_provider', + ownerAvatar: '/admin.png', + }, + { + id: '13', + name: 'Education for All', + description: + 'Democratizing education through blockchain technology, providing access to quality education resources worldwide.', + image: '/bitmed.png', + link: '#', + tags: ['Education', 'Accessibility'], + category: 'education', + type: 'grant', + amount: 120000, + status: 'funding', + createdAt: '2024-03-15', + updatedAt: '2024-03-25', + owner: 'grant4', + ownerName: 'Grant Provider', + ownerUsername: 'grant_provider', + ownerAvatar: '/avatar.png', + }, + { + id: '14', + name: 'DAO Governance Research', + description: + 'Advancing decentralized autonomous organization governance models and voting mechanisms for better community decision-making.', + image: '/bitmed.png', + link: '#', + tags: ['DAO', 'Governance'], + category: 'dao', + type: 'grant', + amount: 60000, + status: 'funding', + createdAt: '2024-03-20', + updatedAt: '2024-03-30', + owner: 'grant5', + ownerName: 'Grant Provider', + ownerUsername: 'grant_provider', + ownerAvatar: '/admin.png', + }, + { + id: '15', + name: 'NFT Art Revolution', + description: + 'Supporting digital artists and creators in the NFT space with tools, platforms, and marketplace development.', + image: '/bitmed.png', + link: '#', + tags: ['NFT', 'Art'], + category: 'nft', + type: 'grant', + amount: 70000, + status: 'funding', + createdAt: '2024-03-25', + updatedAt: '2024-04-05', + owner: 'grant6', + ownerName: 'Grant Provider', + ownerUsername: 'grant_provider', + ownerAvatar: '/admin.png', + }, +]; diff --git a/components/profile/useProjectFilters.ts b/components/profile/useProjectFilters.ts new file mode 100644 index 000000000..885c598b0 --- /dev/null +++ b/components/profile/useProjectFilters.ts @@ -0,0 +1,82 @@ +'use client'; + +import { useState } from 'react'; +import { Project } from '@/types/project'; + +export function useProjectFilters(projects: Project[]) { + const [activeTab, setActiveTab] = useState('Projects'); + const [sortFilter, setSortFilter] = useState('Default'); + const [statusFilter, setStatusFilter] = useState('Status'); + const [categoryFilter, setCategoryFilter] = useState('Category'); + + const getFilteredProjects = () => { + if (!projects) return []; + + let filteredProjects = [...projects]; + + // Filter by tab type + if (activeTab === 'Projects') { + filteredProjects = filteredProjects.filter( + project => project.type === 'crowdfunding' + ); + } else if (activeTab === 'Hackathons') { + filteredProjects = filteredProjects.filter( + project => project.type === 'hackathon' + ); + } else if (activeTab === 'Grants') { + filteredProjects = filteredProjects.filter( + project => project.type === 'grant' + ); + } + + // Filter by status + if (statusFilter !== 'Status') { + filteredProjects = filteredProjects.filter( + project => project.status === statusFilter.toLowerCase() + ); + } + + // Filter by category + if (categoryFilter !== 'Category') { + const categoryMap: { [key: string]: string } = { + Health: 'healthcare', + Finance: 'defi', + Environment: 'environment', + Education: 'education', + Technology: 'technology', + }; + const mappedCategory = + categoryMap[categoryFilter] || categoryFilter.toLowerCase(); + filteredProjects = filteredProjects.filter( + project => project.category === mappedCategory + ); + } + + // Sort projects + if (sortFilter === 'Newest') { + filteredProjects.sort( + (a, b) => + new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime() + ); + } else if (sortFilter === 'Oldest') { + filteredProjects.sort( + (a, b) => + new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime() + ); + } + + return filteredProjects; + }; + + return { + activeTab, + setActiveTab, + sortFilter, + setSortFilter, + statusFilter, + setStatusFilter, + categoryFilter, + setCategoryFilter, + getFilteredProjects, + }; +} diff --git a/components/project-details/project-team.tsx b/components/project-details/project-team.tsx index d4009c610..5a8a603b8 100644 --- a/components/project-details/project-team.tsx +++ b/components/project-details/project-team.tsx @@ -2,21 +2,12 @@ import React from 'react'; import { CrowdfundingProject } from '@/lib/api/types'; -import Image from 'next/image'; +import { TeamList, TeamMember } from '@/components/ui/TeamList'; interface ProjectTeamProps { project: CrowdfundingProject; } -interface TeamMember { - id: string; - name: string; - role: 'OWNER' | 'MEMBER'; - avatar?: string; - joinedAt?: string; - username?: string; -} - export function ProjectTeam({ project }: ProjectTeamProps) { // Create team members from real project data const teamMembers: TeamMember[] = React.useMemo(() => { @@ -54,121 +45,16 @@ export function ProjectTeam({ project }: ProjectTeamProps) { return members; }, [project.creator, project.team]); - const getRoleColor = (role: 'OWNER' | 'MEMBER') => { - return role === 'OWNER' ? 'text-[#DBF936]' : 'text-[#B5B5B5]'; - }; - - const handleMemberClick = () => { - // TODO: Navigate to member profile or show member details - // Handle member click action + const handleMemberClick = (member: TeamMember) => { + window.open(`/profile/${member.username}`, '_blank'); }; - // Show empty state if no team members - if (teamMembers.length === 0) { - return ( -
-
- - - -
-

No Team Members

-

- This project doesn't have any team members yet. -

-
- ); - } - return ( -
-
- {teamMembers.map(member => ( -
-
- {/* Avatar */} -
-
- {member.avatar ? ( - {member.name} - ) : ( - Default avatar - )} -
- {/* Role indicator */} - {member.role === 'OWNER' && ( -
- )} -
- - {/* Member Info */} -
- - {member.name} - -
- - {member.role} - - {member.username && ( - - @{member.username} - - )} -
- {member.joinedAt && ( - - Joined {new Date(member.joinedAt).toLocaleDateString()} - - )} -
-
- - {/* Chevron */} - - - -
- ))} -
-
+ ); } diff --git a/components/ui/TeamList.tsx b/components/ui/TeamList.tsx new file mode 100644 index 000000000..ef16fbe6e --- /dev/null +++ b/components/ui/TeamList.tsx @@ -0,0 +1,150 @@ +'use client'; + +import React from 'react'; +import Image from 'next/image'; + +export interface TeamMember { + id: string; + name: string; + role: 'OWNER' | 'MEMBER'; + avatar?: string; + joinedAt?: string; + username?: string; +} + +interface TeamListProps { + members: TeamMember[]; + onMemberClick?: (member: TeamMember) => void; + showEmptyState?: boolean; + emptyStateTitle?: string; + emptyStateDescription?: string; + className?: string; +} + +export function TeamList({ + members, + onMemberClick, + showEmptyState = true, + emptyStateTitle = 'No Team Members', + emptyStateDescription = "This project doesn't have any team members yet.", + className = '', +}: TeamListProps) { + const getRoleColor = (role: 'OWNER' | 'MEMBER') => { + return role === 'OWNER' ? 'text-[#DBF936]' : 'text-[#B5B5B5]'; + }; + + const handleMemberClick = (member: TeamMember) => { + if (onMemberClick) { + onMemberClick(member); + } + }; + + // Show empty state if no team members + if (members.length === 0 && showEmptyState) { + return ( +
+
+ + + +
+

+ {emptyStateTitle} +

+

{emptyStateDescription}

+
+ ); + } + + return ( +
+
+ {members.map(member => ( +
handleMemberClick(member)} + > +
+ {/* Avatar */} +
+
+ {member.avatar ? ( + {member.name} + ) : ( + Default avatar + )} +
+ {/* Role indicator */} + {member.role === 'OWNER' && ( +
+ )} +
+ + {/* Member Info */} +
+ + {member.name} + +
+ + {member.role} + + {member.username && ( + + @{member.username} + + )} +
+ {member.joinedAt && ( + + Joined {new Date(member.joinedAt).toLocaleDateString()} + + )} +
+
+ + {/* Chevron */} + + + +
+ ))} +
+
+ ); +} diff --git a/hooks/use-protected-action.ts b/hooks/use-protected-action.ts index 9ac880d06..0d10b3039 100644 --- a/hooks/use-protected-action.ts +++ b/hooks/use-protected-action.ts @@ -68,7 +68,7 @@ export function useProtectedAction({ return false; } }, - [isHydrated, isConnected, publicKey, requireWallet, actionName, onSuccess] + [isHydrated, isConnected, publicKey, requireWallet, onSuccess] ); const handleWalletConnectedWithRedirect = useCallback(() => { diff --git a/lib/api/api.ts b/lib/api/api.ts index 1af831c4d..edeb5c309 100644 --- a/lib/api/api.ts +++ b/lib/api/api.ts @@ -2,8 +2,8 @@ import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios'; import Cookies from 'js-cookie'; import { useAuthStore } from '@/lib/stores/auth-store'; -const API_BASE_URL = 'https://staging-api.boundlessfi.xyz/api'; -// const API_BASE_URL = process.env.NEXT_PUBLIC_API_URL; +// const API_BASE_URL = 'https://staging-api.boundlessfi.xyz/api'; +const API_BASE_URL = process.env.NEXT_PUBLIC_API_URL; if (!API_BASE_URL) { throw new Error('NEXT_PUBLIC_API_URL environment variable is not defined'); }