+
{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 ? (
-
- ) : (
-
- )}
-
- {/* 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 ? (
+
+ ) : (
+
+ )}
+
+ {/* 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');
}
From be2dbfe9d4cc997bd353479fda4abc4e209c4a5a Mon Sep 17 00:00:00 2001
From: Collins Ikechukwu
Date: Sun, 19 Oct 2025 16:50:40 -0700
Subject: [PATCH 15/45] Collins (#334)
* refactor: update project components and API for voting functionality
- Adjust layout and styling in ProjectsPage and ProjectCard components.
- Enhance ProjectSidebar to include crowdfund data and voting logic.
- Implement new API methods for voting, retrieving votes, and removing votes.
- Update types for vote handling in the API.
- Clean up unused code and improve responsiveness across components.
* chore: update project dependencies and enhance project details layout
* feat: added static api for staging
* fix: made few changes
* fix: minor changes
* feat: chaged test data
* fix: minor fixes
* refactor: update project components and improve functionality
* feat: file clean up
* feat: added extra validation
* feat: integrate wallet protection and validation across components
* feat: enhance profile components and add activity tracking
- Refactor ProfileData to use ProfileDataClient for improved user data display.
- Introduce ActivityFeed and ActivityTab components for tracking user activities.
- Implement filtering and sorting functionality in the new ProjectsTab and FilterControls components.
- Add modal support for displaying followers and following lists.
- Create reusable TeamList component for displaying team members across various sections.
- Integrate mock data for testing and development purposes.
- Update UserStats to include clickable follower and following counts for better user interaction.
* feat: add lodash.debounce and enhance profile components with user data integration
- Added lodash.debounce for improved performance in user interactions.
- Updated ProfileDataClient to utilize user data for displaying activities and stats.
- Refactored ActivityFeed and ActivityTab components to fetch real user activities and statistics.
- Removed mock data dependencies from FollowersModal and ProjectsTab, integrating real user data instead.
- Enhanced ProjectsTab to support infinite scrolling and dynamic project loading based on user data.
* feat: enhance navbar and project components for improved responsiveness and user experience
- Integrated useWindowSize hook to adjust component layouts and styles based on screen size.
- Updated Navbar component to improve spacing and responsiveness for various screen sizes.
- Refactored ProjectCard to support full-width display and adjusted styles accordingly.
- Enhanced ProjectList and ProjectsTab to utilize updated ProjectCard properties and improve project status mapping.
- Improved MobileMenu and AuthenticatedNav for better user interaction and accessibility.
* feat: enhance organization layout and settings components for improved user experience
- Updated OrganizationsLayout to include relative positioning for better layout control.
- Refactored OrganizationContent to replace Button with BoundlessButton for consistency.
- Enhanced OrganizationHeader with sticky positioning and improved link styling.
- Introduced new tabs in OrganizationSettings for Profile, Links, Members, and Transfer Ownership, improving organization management.
- Added new components for managing organization links and members, including EmailInviteSection and PermissionsTable.
- Improved styling and layout across various components for better responsiveness and user interaction.
---
app/(landing)/me/page.tsx | 2 +-
app/(landing)/me/profile-data.tsx | 1 +
app/(landing)/organizations/layout.tsx | 6 +-
app/(landing)/privacy/page.tsx | 2 +-
app/globals.css | 2 +
components/ProjectCard.tsx | 2 +-
.../landing-page/blog/BlogPostDetails.tsx | 8 +-
components/landing-page/navbar.tsx | 248 ++++++++-----
.../landing-page/project/ProjectCard.tsx | 4 +-
.../organization/OrganizationContent.tsx | 17 +-
.../organization/OrganizationHeader.tsx | 21 +-
.../organization/OrganizationSettings.tsx | 222 ++---------
.../organization/OrganizationSidebar.tsx | 20 +-
components/organization/ResponsiveSidebar.tsx | 6 +-
.../cards/OrganizationSelector.tsx | 7 +-
.../organization/cards/OrganzationCards.tsx | 12 +-
components/organization/tabs/LinksTab.tsx | 93 +++++
components/organization/tabs/MembersTab.tsx | 135 +++++++
.../tabs/MembersTab/EmailInviteSection.tsx | 108 ++++++
.../tabs/MembersTab/MemberCard.tsx | 115 ++++++
.../tabs/MembersTab/PermissionsTable.tsx | 150 ++++++++
.../tabs/MembersTab/TeamManagementSection.tsx | 113 ++++++
components/organization/tabs/ProfileTab.tsx | 196 ++++++++++
.../tabs/TransferOwnershipTab.tsx | 189 ++++++++++
components/profile/ActivityFeed.tsx | 125 ++-----
components/profile/ActivityTab.tsx | 62 ++--
components/profile/FollowersModal.tsx | 8 +-
components/profile/ProfileDataClient.tsx | 6 +-
components/profile/ProfileHeader.tsx | 36 +-
components/profile/ProfileOverview.tsx | 2 +-
components/profile/ProjectList.tsx | 41 ++-
components/profile/ProjectsTab.tsx | 161 +++++---
components/profile/mockData.ts | 344 ------------------
.../project-sidebar/ProjectSidebarHeader.tsx | 2 +-
components/project/ProjectsPage.tsx | 2 +-
hooks/use-debounce-callback.tsx | 63 ++++
hooks/use-isomorphic-layout-effect.tsx | 6 +
hooks/use-unmount.tsx | 38 ++
hooks/use-window-size.tsx | 89 +++++
lib/api/types.ts | 17 +
package-lock.json | 18 +
package.json | 2 +
42 files changed, 1840 insertions(+), 861 deletions(-)
create mode 100644 components/organization/tabs/LinksTab.tsx
create mode 100644 components/organization/tabs/MembersTab.tsx
create mode 100644 components/organization/tabs/MembersTab/EmailInviteSection.tsx
create mode 100644 components/organization/tabs/MembersTab/MemberCard.tsx
create mode 100644 components/organization/tabs/MembersTab/PermissionsTable.tsx
create mode 100644 components/organization/tabs/MembersTab/TeamManagementSection.tsx
create mode 100644 components/organization/tabs/ProfileTab.tsx
create mode 100644 components/organization/tabs/TransferOwnershipTab.tsx
delete mode 100644 components/profile/mockData.ts
create mode 100644 hooks/use-debounce-callback.tsx
create mode 100644 hooks/use-isomorphic-layout-effect.tsx
create mode 100644 hooks/use-unmount.tsx
create mode 100644 hooks/use-window-size.tsx
diff --git a/app/(landing)/me/page.tsx b/app/(landing)/me/page.tsx
index 6cca701e3..5774f0af3 100644
--- a/app/(landing)/me/page.tsx
+++ b/app/(landing)/me/page.tsx
@@ -2,7 +2,7 @@ import { ProfileData } from './profile-data';
export default async function MePage() {
return (
-
+
);
diff --git a/app/(landing)/me/profile-data.tsx b/app/(landing)/me/profile-data.tsx
index c26a91493..3ea712a48 100644
--- a/app/(landing)/me/profile-data.tsx
+++ b/app/(landing)/me/profile-data.tsx
@@ -16,6 +16,7 @@ export async function ProfileData() {
try {
const userData = await getMe(session.user.accessToken);
+ // console.log(userData);
return (
4 && pathname !== '/dashboard/organizations');
return (
-
+
{showSidebar ? (
-
+
- {children}
+ {children}
) : (
{children}
diff --git a/app/(landing)/privacy/page.tsx b/app/(landing)/privacy/page.tsx
index b60a81d89..f4acdb600 100644
--- a/app/(landing)/privacy/page.tsx
+++ b/app/(landing)/privacy/page.tsx
@@ -7,7 +7,7 @@ export const metadata: Metadata = generatePageMetadata('privacy');
const PrivacyPage = () => {
return (
- Privacy Page
+ Privacy Pages
);
};
diff --git a/app/globals.css b/app/globals.css
index 2fef9035b..26299f5f1 100644
--- a/app/globals.css
+++ b/app/globals.css
@@ -55,6 +55,8 @@
--color-success-700: #036b26;
--color-success-800: #015b20;
--color-success-900: #004617;
+
+ --color-active-bg: rgba(167, 249, 80, 0.08);
}
/* Custom scrollbar styles for comment modal */
.custom-scrollbar {
diff --git a/components/ProjectCard.tsx b/components/ProjectCard.tsx
index 6bfbc6bf2..13ab895a1 100644
--- a/components/ProjectCard.tsx
+++ b/components/ProjectCard.tsx
@@ -186,7 +186,7 @@ const ProjectCard: React.FC
= memo(
{project.category}
-
+
{project.category}
diff --git a/components/landing-page/blog/BlogPostDetails.tsx b/components/landing-page/blog/BlogPostDetails.tsx
index a7f3faa22..a48bd202e 100644
--- a/components/landing-page/blog/BlogPostDetails.tsx
+++ b/components/landing-page/blog/BlogPostDetails.tsx
@@ -213,7 +213,7 @@ const BlogPostDetails: React.FC
= ({ post }) => {
handleShare('twitter')}
- className='flex h-10 w-10 items-center justify-center rounded-full bg-[rgba(167,249,80,0.08)] text-black transition-colors hover:bg-[#A7F950]/80 focus:ring-2 focus:ring-[#A7F950]/50 focus:outline-none'
+ className='bg-active-bg flex h-10 w-10 items-center justify-center rounded-full text-black transition-colors hover:bg-[#A7F950]/80 focus:ring-2 focus:ring-[#A7F950]/50 focus:outline-none'
aria-label='Share on Twitter'
>
= ({ post }) => {
handleShare('discord')}
- className='flex h-10 w-10 items-center justify-center rounded-full bg-[rgba(167,249,80,0.08)] text-black transition-colors hover:bg-[#A7F950]/80 focus:ring-2 focus:ring-[#A7F950]/50 focus:outline-none'
+ className='bg-active-bg flex h-10 w-10 items-center justify-center rounded-full text-black transition-colors hover:bg-[#A7F950]/80 focus:ring-2 focus:ring-[#A7F950]/50 focus:outline-none'
aria-label='Copy for Discord'
>
{copiedStates.discord ? (
@@ -257,7 +257,7 @@ const BlogPostDetails: React.FC = ({ post }) => {
handleShare('send')}
- className='flex h-10 w-10 items-center justify-center rounded-full bg-[rgba(167,249,80,0.08)] text-black transition-colors hover:bg-[#A7F950]/80 focus:ring-2 focus:ring-[#A7F950]/50 focus:outline-none'
+ className='bg-active-bg flex h-10 w-10 items-center justify-center rounded-full text-black transition-colors hover:bg-[#A7F950]/80 focus:ring-2 focus:ring-[#A7F950]/50 focus:outline-none'
aria-label='Share via native share'
>
= ({ post }) => {
handleShare('link')}
- className='flex h-10 w-10 items-center justify-center rounded-full bg-[rgba(167,249,80,0.08)] text-black transition-colors hover:bg-[#A7F950]/80 focus:ring-2 focus:ring-[#A7F950]/50 focus:outline-none'
+ className='bg-active-bg flex h-10 w-10 items-center justify-center rounded-full text-black transition-colors hover:bg-[#A7F950]/80 focus:ring-2 focus:ring-[#A7F950]/50 focus:outline-none'
aria-label='Copy link'
>
{copiedStates.link ? (
diff --git a/components/landing-page/navbar.tsx b/components/landing-page/navbar.tsx
index 8a280b9f0..51bca6a6a 100644
--- a/components/landing-page/navbar.tsx
+++ b/components/landing-page/navbar.tsx
@@ -32,6 +32,7 @@ import CreateProjectModal from './project/CreateProjectModal';
import { useProtectedAction } from '@/hooks/use-protected-action';
import { useAuthStore } from '@/lib/stores/auth-store';
import WalletRequiredModal from '@/components/wallet/WalletRequiredModal';
+import { useWindowSize } from '@/hooks/use-window-size';
gsap.registerPlugin(useGSAP);
@@ -53,6 +54,7 @@ export function Navbar() {
const router = useRouter();
const { isAuthenticated, isLoading, user } = useAuthStatus();
const pathname = usePathname();
+ const { width } = useWindowSize();
useGSAP(
() => {
@@ -168,10 +170,10 @@ export function Navbar() {
ref={navbarRef}
className='sticky top-0 z-50 max-h-[88px] bg-[#030303A3] backdrop-blur-[12px]'
>
-
+
@@ -182,17 +184,27 @@ export function Navbar() {
onClick={() => router.push('/')}
className='flex items-center'
>
-
+
-
+
{menuItems.map(item => (
{item.label}
@@ -244,7 +256,7 @@ function AuthenticatedNav({
onSuccess: () => setCreateProjectModalOpen(true),
});
return (
-
+
@@ -254,7 +266,7 @@ function AuthenticatedNav({
{
@@ -318,7 +330,7 @@ function AuthenticatedNav({
@@ -400,6 +412,7 @@ function AuthenticatedNav({
// even for unauthenticated users, so designers/QA can test the flow.
function UnauthenticatedNav() {
const [createProjectModalOpen, setCreateProjectModalOpen] = useState(false);
+ const { width } = useWindowSize();
const {
executeProtectedAction,
@@ -424,18 +437,20 @@ function UnauthenticatedNav() {
}
return (
-
+
{
await executeProtectedAction(() => setCreateProjectModalOpen(true));
}}
className='border-white/20 text-white hover:bg-white/10'
+ size={width && width < 1024 ? 'sm' : 'default'}
>
-
- Add Project
+
+ Add Project
+ Add
-
+
Sign in
{
gsap.fromTo(
@@ -578,43 +594,56 @@ function MobileMenu({
-
+
-
+ {/* Header with logo and close button */}
+
-
- {menuItems.map(item => (
-
- {item.label}
-
- ))}
+ {/* Navigation Menu */}
+
+
+ Navigation
+
+
+ {menuItems.map(item => (
+
+ {item.label}
+
+ ))}
+
{isLoading ? (
@@ -623,89 +652,122 @@ function MobileMenu({
) : isAuthenticated ? (
-
-
-
-
-
- {user?.name?.charAt(0) ||
- user?.profile?.firstName?.charAt(0) ||
- user?.email?.charAt(0) ||
- 'U'}
-
-
-
-
- {user?.name || user?.profile?.firstName || 'User'}
-
-
{user?.email}
+
+ {/* User Profile Section */}
+
+
+
+
+
+ {user?.name?.charAt(0) ||
+ user?.profile?.firstName?.charAt(0) ||
+ user?.email?.charAt(0) ||
+ 'U'}
+
+
+
+
+ {user?.name || user?.profile?.firstName || 'User'}
+
+
+ {user?.email}
+
+
{/* Wallet Connection */}
-
-
+
+
Wallet
-
+
-
-
- Navigation
-
-
- Profile
-
-
- Organizations
-
-
+
+ Quick Actions
+
+
+
+
+ Profile
+
+
+
+ Organizations
+
+
+
+ Settings
+
+
+
+ {/* Sign Out Button */}
+
+ !isLoading && logout()}
+ disabled={isLoading}
+ className='w-full border-red-500/50 text-red-400 hover:border-red-500 hover:bg-red-500/10 hover:text-red-300'
>
- Settings
-
+
+ {isLoading ? 'Signing Out...' : 'Sign Out'}
+
-
!isLoading && logout()}
- disabled={isLoading}
- >
- {isLoading ? 'Signing Out...' : 'Sign Out'}
-
) : (
-
-
-
- Wallet
-
+
+ {/* Wallet Connection */}
+
+
+ Connect Wallet
+
-
- Get Started
-
+
+ {/* Get Started Section */}
+
+
+ Get Started
+
+
+
+
+ Get Started
+
+
+
)}
diff --git a/components/landing-page/project/ProjectCard.tsx b/components/landing-page/project/ProjectCard.tsx
index 872ee0db9..a7bab2c8d 100644
--- a/components/landing-page/project/ProjectCard.tsx
+++ b/components/landing-page/project/ProjectCard.tsx
@@ -13,6 +13,7 @@ type ProjectCardProps = {
status: 'Validation' | 'Funding' | 'Funded' | 'Completed';
deadlineInDays: number;
milestoneRejected?: boolean;
+ isFullWidth?: boolean;
votes?: {
current: number;
goal: number;
@@ -37,6 +38,7 @@ function ProjectCard({
status,
deadlineInDays,
milestoneRejected,
+ isFullWidth = false,
votes,
funding,
milestones,
@@ -93,7 +95,7 @@ function ProjectCard({
return (
diff --git a/components/organization/OrganizationContent.tsx b/components/organization/OrganizationContent.tsx
index 18bd3a16d..e1ae5a745 100644
--- a/components/organization/OrganizationContent.tsx
+++ b/components/organization/OrganizationContent.tsx
@@ -4,6 +4,7 @@ import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import OrganizationCard from './cards/OrganzationCards';
import Link from 'next/link';
+import { BoundlessButton } from '../buttons';
const mockOrganizations = [
{
@@ -53,12 +54,12 @@ export default function OrganizationContent() {
Sort
@@ -66,10 +67,14 @@ export default function OrganizationContent() {
{hasorganizations && (
-
+ }
+ className='rounded-lg px-6 py-6 text-black'
+ >
Add Organization
-
-
+
)}
@@ -92,7 +97,7 @@ export default function OrganizationContent() {
{!hasorganizations && (
-
+
Add Organization
diff --git a/components/organization/OrganizationHeader.tsx b/components/organization/OrganizationHeader.tsx
index 33925a7b9..4a17dc3ef 100644
--- a/components/organization/OrganizationHeader.tsx
+++ b/components/organization/OrganizationHeader.tsx
@@ -1,12 +1,5 @@
'use client';
-import {
- ChevronDown,
- User,
- Building2,
- Settings,
- LogOut,
- HomeIcon,
-} from 'lucide-react';
+import { ChevronDown, User, Building2, Settings, LogOut } from 'lucide-react';
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
import {
DropdownMenu,
@@ -33,7 +26,7 @@ export default function OrganizationHeader() {
(pathname.split('/').length > 4 && pathname !== '/dashboard/organizations');
return (
-
+
{/* Home Link */}
-
-
- Home
-
+
+
+
+ Home
+
+
{showOrgSelector && }
diff --git a/components/organization/OrganizationSettings.tsx b/components/organization/OrganizationSettings.tsx
index c82ab3642..c4437a689 100644
--- a/components/organization/OrganizationSettings.tsx
+++ b/components/organization/OrganizationSettings.tsx
@@ -2,16 +2,12 @@
import { useState } from 'react';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
-import { Input } from '@/components/ui/input';
-import { Textarea } from '@/components/ui/textarea';
-import { Button } from '@/components/ui/button';
-import { Label } from '@/components/ui/label';
import { Menu } from 'lucide-react';
-import { cn } from '@/lib/utils';
-import FormHint from '../form/FormHint';
-import MDEditor from '@uiw/react-md-editor';
import ResponsiveSidebar from './ResponsiveSidebar';
-
+import ProfileTab from './tabs/ProfileTab';
+import LinksTab from './tabs/LinksTab';
+import MembersTab from './tabs/MembersTab';
+import TransferOwnershipTab from './tabs/TransferOwnershipTab';
interface OrganizationSettingsProps {
organizationId?: string;
initialData?: {
@@ -26,23 +22,28 @@ export default function OrganizationSettings({
organizationId,
initialData,
}: OrganizationSettingsProps) {
- const [formData, setFormData] = useState({
- name: initialData?.name || '',
- logo: initialData?.logo || '',
- tagline: initialData?.tagline || '',
- about: initialData?.about || '',
- });
-
const [modalOpen, setModalOpen] = useState(false);
- const handleSave = () => {
- // console.log('Saving organization data:', formData);
+ const handleProfileSave = () => {
+ // TODO: Implement profile save logic
+ };
+
+ const handleLinksSave = () => {
+ // TODO: Implement links save logic
+ };
+
+ const handleMembersSave = () => {
+ // TODO: Implement members save logic
+ };
+
+ const handleTransferOwnership = () => {
+ // TODO: Implement ownership transfer logic
};
return (
-
+
{/* Hamburger Menu - Visible only on medium screens and below */}
+
Profile
Links
Members
Transfer Ownership
@@ -82,189 +84,25 @@ export default function OrganizationSettings({
-
-
- {/* Organization Name */}
-
-
- Project Name *
-
-
- setFormData({ ...formData, name: e.target.value })
- }
- placeholder='Enter a name for your organization'
- className={cn(
- 'focus-visible:border-primary border-[#484848] bg-[#1A1A1A] p-4 text-white placeholder:text-[#919191]'
- )}
- />
-
-
- {/* Logo Upload */}
-
-
- Logo *
-
-
-
-
-
-
-
-
-
-
-
- Click or drag to upload
-
-
-
-
-
-
-
- Accepted files should be JPEG or PNG, and less than 2 MB.
-
- }
- side='top'
- />
-
-
- Accepted file type:{' '}
- JPEG or{' '}
- PNG , and
- less than{' '}
- 2 MB .
-
-
- A size of{' '}
- 480 x 480 px {' '}
- is recommended.
-
-
-
-
-
- {/* Tagline */}
-
-
-
- Tagline *
-
- 0/300
-
-
-
-
-
-
- Describe your project's long-term goal or the positive
- change it will bring to people, communities, or
- industries.
-
- }
- side='top'
- />
-
- Describe your project's long-term goal or the positive change
- it will bring to people, communities, or industries.
-
-
-
-
- {/* About */}
-
-
- About *
-
-
- {/* Rich Text Editor Toolbar */}
-
- {/* Markdown Editor */}
-
-
-
-
- {/* Save Button */}
-
- Save Changes
-
+
+
+
- Links content coming soon...
+
- Members content coming soon...
+
-
- Transfer ownership content coming soon...
-
+
- {/* Organization Modal */}
-
+
+
Menu
@@ -54,7 +64,7 @@ export default function OrganizationSidebar({
className={cn(
'flex items-center gap-3 px-11 py-2.5 text-sm font-medium transition-colors',
isActive
- ? 'border-r-4 border-r-lime-500 bg-lime-500/10 text-lime-500'
+ ? 'border-r-primary bg-active-bg text-primary border-r-4'
: 'text-zinc-400 hover:bg-zinc-800 hover:text-white'
)}
>
@@ -65,14 +75,14 @@ export default function OrganizationSidebar({
})}
-
+
Host Hackathon
-
+
Create Grants
diff --git a/components/organization/ResponsiveSidebar.tsx b/components/organization/ResponsiveSidebar.tsx
index f9aacf7be..1cd3b487f 100644
--- a/components/organization/ResponsiveSidebar.tsx
+++ b/components/organization/ResponsiveSidebar.tsx
@@ -64,7 +64,7 @@ export default function ResponsiveSidebar({
className={cn(
'flex items-center gap-3 px-6 py-2.5 text-sm font-medium transition-colors',
isActive
- ? 'border-r-4 border-r-lime-500 bg-lime-500/10 text-lime-500'
+ ? 'border-r-primary bg-primary/10 text-primary border-r-4'
: 'text-zinc-400 hover:bg-zinc-800 hover:text-white'
)}
>
@@ -80,7 +80,7 @@ export default function ResponsiveSidebar({
onClick={() => onOpenChange?.(false)}
className='flex items-center gap-3 px-3 py-2'
>
-
+
@@ -92,7 +92,7 @@ export default function ResponsiveSidebar({
onClick={() => onOpenChange?.(false)}
className='flex items-center gap-3 px-3 py-2'
>
-
+
diff --git a/components/organization/cards/OrganizationSelector.tsx b/components/organization/cards/OrganizationSelector.tsx
index 15dcd0e16..571f8a18e 100644
--- a/components/organization/cards/OrganizationSelector.tsx
+++ b/components/organization/cards/OrganizationSelector.tsx
@@ -1,7 +1,7 @@
'use client';
import { useState } from 'react';
-import { ChevronDown, ChevronUp, Check } from 'lucide-react';
+import { Check, ChevronsUpDown } from 'lucide-react';
import Image from 'next/image';
import {
DropdownMenu,
@@ -87,8 +87,7 @@ export default function OrganizationSelector({
{/* Chevron Icons */}
-
-
+
@@ -120,7 +119,7 @@ export default function OrganizationSelector({
{/* Check Icon for Selected */}
{selectedOrg.id === org.id && (
-
+
)}
))}
diff --git a/components/organization/cards/OrganzationCards.tsx b/components/organization/cards/OrganzationCards.tsx
index d9f36567f..3b88fe058 100644
--- a/components/organization/cards/OrganzationCards.tsx
+++ b/components/organization/cards/OrganzationCards.tsx
@@ -26,7 +26,7 @@ export default function OrganizationCard({
grants,
}: OrganizationCardProps) {
return (
-
+
@@ -49,8 +49,8 @@ export default function OrganizationCard({
{/* Hackathons */}
-
@@ -79,8 +79,8 @@ export default function OrganizationCard({
{/* Grants */}
-
@@ -110,7 +110,7 @@ export default function OrganizationCard({
{/* Footer */}
Manage Organization
diff --git a/components/organization/tabs/LinksTab.tsx b/components/organization/tabs/LinksTab.tsx
new file mode 100644
index 000000000..44956fb45
--- /dev/null
+++ b/components/organization/tabs/LinksTab.tsx
@@ -0,0 +1,93 @@
+'use client';
+
+import { useState } from 'react';
+import { Input } from '@/components/ui/input';
+import { Label } from '@/components/ui/label';
+import { BoundlessButton } from '@/components/buttons';
+
+export interface OrganizationLinks {
+ website: string;
+ twitter: string;
+ github: string;
+ other: string;
+}
+
+interface LinksTabProps {
+ initialLinks?: OrganizationLinks;
+ onSave?: (links: OrganizationLinks) => void;
+}
+
+export default function LinksTab({
+ initialLinks = { website: '', twitter: '', github: '', other: '' },
+ onSave,
+}: LinksTabProps) {
+ const [links, setLinks] = useState
(initialLinks);
+
+ const updateLink = (field: keyof OrganizationLinks, value: string) => {
+ setLinks(prev => ({ ...prev, [field]: value }));
+ };
+
+ const handleSave = () => {
+ onSave?.(links);
+ };
+
+ return (
+
+ {/* Website */}
+
+ Website
+ updateLink('website', e.target.value)}
+ placeholder='Enter link to organization website'
+ className='bg-background rounded-[12px] border-gray-900 !p-4 !py-5 text-white placeholder:text-gray-700 focus-visible:ring-0'
+ />
+
+
+ {/* X (Twitter) */}
+
+
X (Twitter)
+
+
+ @
+
+ updateLink('twitter', e.target.value)}
+ placeholder='Organization X handle'
+ className='bg-background rounded-[12px] border-gray-900 !p-4 !py-5 !pl-8 text-white placeholder:text-gray-700 focus-visible:ring-0'
+ />
+
+
+
+ {/* GitHub */}
+
+ GitHub
+ updateLink('github', e.target.value)}
+ placeholder='Link to GitHub repo or GitHub organization profile'
+ className='bg-background rounded-[12px] border-gray-900 !p-4 !py-5 text-white placeholder:text-gray-700 focus-visible:ring-0'
+ />
+
+
+ {/* Other Link */}
+
+
+ Other Link (optional)
+
+ updateLink('other', e.target.value)}
+ placeholder='Link URL (newsletters or social account)'
+ className='bg-background rounded-[12px] border-gray-900 !p-4 !py-5 text-white placeholder:text-gray-700 focus-visible:ring-0'
+ />
+
+
+ {/* Save Button */}
+
+ Save Changes
+
+
+ );
+}
diff --git a/components/organization/tabs/MembersTab.tsx b/components/organization/tabs/MembersTab.tsx
new file mode 100644
index 000000000..e0b9787e8
--- /dev/null
+++ b/components/organization/tabs/MembersTab.tsx
@@ -0,0 +1,135 @@
+'use client';
+
+import { useState } from 'react';
+import { BoundlessButton } from '@/components/buttons';
+import EmailInviteSection from './MembersTab/EmailInviteSection';
+import PermissionsTable from './MembersTab/PermissionsTable';
+import TeamManagementSection from './MembersTab/TeamManagementSection';
+
+interface Member {
+ id: string;
+ name: string;
+ email: string;
+ avatar?: string;
+ role: 'owner' | 'admin' | 'member';
+ joinedAt: string;
+ status: 'active' | 'pending' | 'suspended';
+}
+
+interface MembersTabProps {
+ initialMembers?: Member[];
+ onSave?: (members: Member[]) => void;
+}
+
+export default function MembersTab({
+ initialMembers = [],
+ onSave,
+}: MembersTabProps) {
+ const dummyMembers: Member[] = [
+ {
+ id: 'member-1',
+ name: 'Sarah Johnson',
+ email: 'sarah.johnson@example.com',
+ avatar: '/avatar.png',
+ role: 'admin',
+ joinedAt: new Date().toISOString(),
+ status: 'active',
+ },
+ {
+ id: 'member-2',
+ name: 'Mike Chen',
+ email: 'mike.chen@example.com',
+ avatar: '/avatar.png',
+ role: 'member',
+ joinedAt: new Date().toISOString(),
+ status: 'active',
+ },
+ {
+ id: 'member-3',
+ name: 'Emily Rodriguez',
+ email: 'emily.rodriguez@example.com',
+ avatar: '/avatar.png',
+ role: 'admin',
+ joinedAt: new Date().toISOString(),
+ status: 'active',
+ },
+ {
+ id: 'pending-1',
+ name: 'Alex Thompson',
+ email: 'alex.thompson@example.com',
+ avatar: '/avatar.png',
+ role: 'member',
+ joinedAt: new Date().toISOString(),
+ status: 'pending',
+ },
+ {
+ id: 'pending-2',
+ name: 'Lisa Wang',
+ email: 'lisa.wang@example.com',
+ avatar: '/avatar.png',
+ role: 'admin',
+ joinedAt: new Date().toISOString(),
+ status: 'pending',
+ },
+ ];
+
+ const [members, setMembers] = useState(
+ initialMembers.length > 0 ? initialMembers : dummyMembers
+ );
+ const [inviteEmails, setInviteEmails] = useState([]);
+ const [emailInput, setEmailInput] = useState('');
+
+ const handleInvite = () => {
+ if (inviteEmails.length > 0) {
+ const newMembers: Member[] = inviteEmails.map(email => ({
+ id: Date.now().toString() + Math.random(),
+ name: email.split('@')[0],
+ email: email,
+ role: 'member', // Default role
+ joinedAt: new Date().toISOString(),
+ status: 'pending',
+ }));
+ setMembers([...members, ...newMembers]);
+ setInviteEmails([]);
+ setEmailInput('');
+ }
+ };
+
+ const handleRoleChange = (memberId: string, newRole: string) => {
+ setMembers(prev =>
+ prev.map(member =>
+ member.id === memberId
+ ? { ...member, role: newRole as 'admin' | 'member' }
+ : member
+ )
+ );
+ };
+
+ const handleRemoveMember = (memberId: string) => {
+ setMembers(prev => prev.filter(member => member.id !== memberId));
+ };
+
+ return (
+
+
+
+
+
+
+
+
onSave?.(members)} className=''>
+ Save Changes
+
+
+ );
+}
diff --git a/components/organization/tabs/MembersTab/EmailInviteSection.tsx b/components/organization/tabs/MembersTab/EmailInviteSection.tsx
new file mode 100644
index 000000000..dff79b323
--- /dev/null
+++ b/components/organization/tabs/MembersTab/EmailInviteSection.tsx
@@ -0,0 +1,108 @@
+'use client';
+
+import { useRef } from 'react';
+import { Textarea } from '@/components/ui/textarea';
+import { X as XIcon } from 'lucide-react';
+import { cn } from '@/lib/utils';
+import { BoundlessButton } from '@/components/buttons';
+
+interface EmailInviteSectionProps {
+ inviteEmails: string[];
+ setInviteEmails: (emails: string[]) => void;
+ emailInput: string;
+ setEmailInput: (input: string) => void;
+ onInvite: () => void;
+}
+
+export default function EmailInviteSection({
+ inviteEmails,
+ setInviteEmails,
+ emailInput,
+ setEmailInput,
+ onInvite,
+}: EmailInviteSectionProps) {
+ const textareaRef = useRef(null);
+
+ const isValidEmail = (email: string) => {
+ const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
+ return emailRegex.test(email);
+ };
+
+ const handleEmailInput = (value: string) => {
+ setEmailInput(value);
+ };
+
+ const handleKeyDown = (e: React.KeyboardEvent) => {
+ if (e.key === ',' || e.key === ' ' || e.key === 'Enter') {
+ e.preventDefault();
+
+ const currentValue = emailInput.trim();
+ if (
+ currentValue &&
+ isValidEmail(currentValue) &&
+ !inviteEmails.includes(currentValue)
+ ) {
+ setInviteEmails([...inviteEmails, currentValue]);
+ setEmailInput('');
+ }
+ }
+ };
+
+ const removeEmail = (emailToRemove: string) => {
+ setInviteEmails(inviteEmails.filter(email => email !== emailToRemove));
+ };
+
+ return (
+
+
+
Invite Team Members
+
+
+
+
+
+ {inviteEmails.length > 0 && (
+
+ {inviteEmails.map((email, index) => (
+
+ {email}
+ removeEmail(email)}
+ className='bg-primary hover:bg-primary/80 rounded-full p-0.5'
+ >
+
+
+
+ ))}
+
+ )}
+
+
+
+
+
+ Invite {inviteEmails.length > 0 && `(${inviteEmails.length})`}
+
+
+
+ );
+}
diff --git a/components/organization/tabs/MembersTab/MemberCard.tsx b/components/organization/tabs/MembersTab/MemberCard.tsx
new file mode 100644
index 000000000..6cc3e37c1
--- /dev/null
+++ b/components/organization/tabs/MembersTab/MemberCard.tsx
@@ -0,0 +1,115 @@
+'use client';
+
+import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
+import { Button } from '@/components/ui/button';
+import { Label } from '@/components/ui/label';
+import {
+ Popover,
+ PopoverContent,
+ PopoverTrigger,
+} from '@/components/ui/popover';
+import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group';
+import { BoundlessButton } from '@/components/buttons';
+import { ChevronsUpDown, X, Delete } from 'lucide-react';
+
+interface Member {
+ id: string;
+ name: string;
+ email: string;
+ avatar?: string;
+ role: 'owner' | 'admin' | 'member';
+ joinedAt: string;
+ status: 'active' | 'pending' | 'suspended';
+}
+
+interface MemberCardProps {
+ member: Member;
+ onRoleChange: (memberId: string, newRole: string) => void;
+ onRemoveMember: (memberId: string) => void;
+}
+
+export default function MemberCard({
+ member,
+ onRoleChange,
+ onRemoveMember,
+}: MemberCardProps) {
+ return (
+
+
+
+
+ {member.name.charAt(0).toUpperCase()}
+
+
+
{member.name}
+
{member.email}
+
+
+
+
+
+
+
+ {member.role === 'admin' ? 'Admin' : 'Member'}
+
+
+
+
+
+
onRoleChange(member.id, value)}
+ className='flex flex-col gap-2'
+ >
+
+
+ Admin
+
+
+
+
+
+ Member
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ }
+ fullWidth
+ onClick={() => onRemoveMember(member.id)}
+ className='text-white'
+ >
+ Remove
+
+
+
+
+
+ );
+}
diff --git a/components/organization/tabs/MembersTab/PermissionsTable.tsx b/components/organization/tabs/MembersTab/PermissionsTable.tsx
new file mode 100644
index 000000000..027755cee
--- /dev/null
+++ b/components/organization/tabs/MembersTab/PermissionsTable.tsx
@@ -0,0 +1,150 @@
+'use client';
+
+import {
+ Table,
+ TableBody,
+ TableCell,
+ TableHead,
+ TableHeader,
+ TableRow,
+} from '@/components/ui/table';
+import { Check, X, Users2 } from 'lucide-react';
+
+const permissions = [
+ {
+ permission: 'Create / edit organization profile',
+ owner: true,
+ admin: { value: true, note: 'edit only' },
+ member: false,
+ },
+ {
+ permission: 'Create and manage hackathons and grants',
+ owner: true,
+ admin: true,
+ member: false,
+ },
+ {
+ permission: 'Publish hackathons / grants',
+ owner: true,
+ admin: false,
+ member: false,
+ },
+ {
+ permission: 'View hackathons & analytics',
+ owner: true,
+ admin: true,
+ member: true,
+ },
+ {
+ permission: 'Invite & remove members',
+ owner: true,
+ admin: true,
+ member: false,
+ },
+ {
+ permission: 'Assign roles',
+ owner: true,
+ admin: false,
+ member: false,
+ },
+ {
+ permission: 'Post announcements & messages',
+ owner: true,
+ admin: false,
+ member: false,
+ },
+ {
+ permission: 'Comment / participate in discussions',
+ owner: true,
+ admin: true,
+ member: true,
+ },
+ {
+ permission: 'Access submissions (view/judge if assigned)',
+ owner: true,
+ admin: true,
+ member: { value: true, note: 'view only, unless assigned as judge' },
+ },
+ {
+ permission: 'Delete organization',
+ owner: true,
+ admin: false,
+ member: false,
+ },
+];
+
+export default function PermissionsTable() {
+ return (
+
+
+ {' '}
+ Organization Roles
+
+
+
+
+
+ Permissions
+
+
+ Owner
+
+
+ Admin
+
+
+ Member
+
+
+
+
+ {permissions.map((perm, index) => (
+
+
+ {perm.permission}
+
+
+ {perm.owner ? (
+
+ ) : (
+
+ )}
+
+
+ {typeof perm.admin === 'object' ? (
+
+
+
+ {perm.admin.note}
+
+
+ ) : perm.admin ? (
+
+ ) : (
+
+ )}
+
+
+ {typeof perm.member === 'object' ? (
+
+
+
+ {perm.member.note}
+
+
+ ) : perm.member ? (
+
+ ) : (
+
+ )}
+
+
+ ))}
+
+
+
+ );
+}
diff --git a/components/organization/tabs/MembersTab/TeamManagementSection.tsx b/components/organization/tabs/MembersTab/TeamManagementSection.tsx
new file mode 100644
index 000000000..4197d2cac
--- /dev/null
+++ b/components/organization/tabs/MembersTab/TeamManagementSection.tsx
@@ -0,0 +1,113 @@
+'use client';
+
+import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
+import MemberCard from './MemberCard';
+
+interface Member {
+ id: string;
+ name: string;
+ email: string;
+ avatar?: string;
+ role: 'owner' | 'admin' | 'member';
+ joinedAt: string;
+ status: 'active' | 'pending' | 'suspended';
+}
+
+interface TeamManagementSectionProps {
+ members: Member[];
+ onRoleChange: (memberId: string, newRole: string) => void;
+ onRemoveMember: (memberId: string) => void;
+}
+
+export default function TeamManagementSection({
+ members,
+ onRoleChange,
+ onRemoveMember,
+}: TeamManagementSectionProps) {
+ // Always show owner as the first member
+ const ownerMember = {
+ id: 'owner-1',
+ name: 'Collins Chikangwu',
+ email: 'collins@boundless.com',
+ avatar: '/avatar.png',
+ role: 'owner' as const,
+ joinedAt: new Date().toISOString(),
+ status: 'active' as const,
+ };
+
+ // Separate members by status
+ const activeMembers = members.filter(member => member.status === 'active');
+ const pendingMembers = members.filter(member => member.status === 'pending');
+
+ return (
+
+
+
Manage Team
+
+ {members.length === 0
+ ? "You're the only one here for now."
+ : `${members.length} team member${members.length === 1 ? '' : 's'}`}
+
+
+
+
+ {/* Owner - Always displayed */}
+
+
+
+
+ {ownerMember.name.charAt(0).toUpperCase()}
+
+
+
+
{ownerMember.name}
+
{ownerMember.email}
+
+ Owner
+
+
+
+
+ {/* Active Members (excluding owner) */}
+ {activeMembers.filter(member => member.role !== 'owner').length > 0 && (
+
+ {activeMembers
+ .filter(member => member.role !== 'owner')
+ .map(member => (
+
+ ))}
+
+ )}
+
+ {/* Pending Invites */}
+ {pendingMembers.length > 0 && (
+
+
Pending Invites
+ {pendingMembers.map(member => (
+
+
+
+
+ {member.name.charAt(0).toUpperCase()}
+
+
+
+
{member.name}
+
{member.email}
+
+ Pending invitation
+
+
+
+ ))}
+
+ )}
+
+
+ );
+}
diff --git a/components/organization/tabs/ProfileTab.tsx b/components/organization/tabs/ProfileTab.tsx
new file mode 100644
index 000000000..9c39779c2
--- /dev/null
+++ b/components/organization/tabs/ProfileTab.tsx
@@ -0,0 +1,196 @@
+'use client';
+
+import { useState } from 'react';
+import { Input } from '@/components/ui/input';
+import { Textarea } from '@/components/ui/textarea';
+import { Button } from '@/components/ui/button';
+import { Label } from '@/components/ui/label';
+import { cn } from '@/lib/utils';
+import FormHint from '../../form/FormHint';
+import MDEditor from '@uiw/react-md-editor';
+
+interface ProfileTabProps {
+ initialData?: {
+ name?: string;
+ logo?: string;
+ tagline?: string;
+ about?: string;
+ };
+ onSave?: (data: Record) => void;
+}
+
+export default function ProfileTab({ initialData, onSave }: ProfileTabProps) {
+ const [formData, setFormData] = useState({
+ name: initialData?.name || '',
+ logo: initialData?.logo || '',
+ tagline: initialData?.tagline || '',
+ about: initialData?.about || '',
+ });
+
+ const handleSave = () => {
+ onSave?.(formData);
+ };
+
+ return (
+
+ {/* Organization Name */}
+
+
+ Project Name *
+
+ setFormData({ ...formData, name: e.target.value })}
+ placeholder='Enter a name for your organization'
+ className={cn(
+ 'focus-visible:border-primary border-[#484848] bg-[#1A1A1A] p-4 text-white placeholder:text-[#919191]'
+ )}
+ />
+
+
+ {/* Logo Upload */}
+
+
+ Logo *
+
+
+
+
+
+
+
+
+
+
+
+ Click or drag to upload
+
+
+
+
+
+
+
+ Accepted files should be JPEG or PNG, and less than 2 MB.
+
+ }
+ side='top'
+ />
+
+
+ Accepted file type:{' '}
+ JPEG or{' '}
+ PNG , and less than{' '}
+ 2 MB .
+
+
+ A size of{' '}
+ 480 x 480 px is
+ recommended.
+
+
+
+
+
+ {/* Tagline */}
+
+
+
+ Tagline *
+
+ 0/300
+
+
+
+
+
+
+ Describe your project's long-term goal or the positive change it
+ will bring to people, communities, or industries.
+
+ }
+ side='top'
+ />
+
+ Describe your project's long-term goal or the positive change it
+ will bring to people, communities, or industries.
+
+
+
+
+ {/* About */}
+
+
+ About *
+
+
+ {/* Rich Text Editor Toolbar */}
+
+ {/* Markdown Editor */}
+
+
+
+
+ {/* Save Button */}
+
+ Save Changes
+
+
+ );
+}
diff --git a/components/organization/tabs/TransferOwnershipTab.tsx b/components/organization/tabs/TransferOwnershipTab.tsx
new file mode 100644
index 000000000..1d72578b7
--- /dev/null
+++ b/components/organization/tabs/TransferOwnershipTab.tsx
@@ -0,0 +1,189 @@
+'use client';
+
+import { useState, useRef, useEffect } from 'react';
+import { Button } from '@/components/ui/button';
+import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
+import {
+ DropdownMenu,
+ DropdownMenuContent,
+ DropdownMenuItem,
+ DropdownMenuTrigger,
+} from '@/components/ui/dropdown-menu';
+import { Check, ChevronDown } from 'lucide-react';
+import { cn } from '@/lib/utils';
+import { BoundlessButton } from '@/components/buttons';
+
+interface Member {
+ id: string;
+ name: string;
+ email: string;
+ avatar?: string;
+ role: 'admin' | 'member';
+}
+
+interface TransferOwnershipTabProps {
+ currentOwner?: {
+ name: string;
+ email: string;
+ avatar?: string;
+ };
+ members?: Member[];
+ onTransfer?: (newOwnerId: string) => void;
+}
+
+export default function TransferOwnershipTab({
+ members = [],
+ onTransfer,
+}: TransferOwnershipTabProps) {
+ const [selectedMember, setSelectedMember] = useState('');
+ const [isTransferring, setIsTransferring] = useState(false);
+ const [popoverWidth, setPopoverWidth] = useState(0);
+ const triggerRef = useRef(null);
+
+ useEffect(() => {
+ const updateWidth = () => {
+ if (triggerRef.current) {
+ setPopoverWidth(triggerRef.current.offsetWidth);
+ }
+ };
+
+ updateWidth();
+ window.addEventListener('resize', updateWidth);
+
+ return () => window.removeEventListener('resize', updateWidth);
+ }, []);
+ // Dummy data for demonstration
+ const dummyMembers: Member[] = [
+ {
+ id: 'admin-1',
+ name: 'Robert Fox',
+ email: 'robert.fox@example.com',
+ avatar: '/avatar.png',
+ role: 'admin',
+ },
+ {
+ id: 'admin-2',
+ name: 'Sarah Johnson',
+ email: 'sarah.johnson@example.com',
+ avatar: '/avatar.png',
+ role: 'admin',
+ },
+ {
+ id: 'admin-3',
+ name: 'Mike Chen',
+ email: 'mike.chen@example.com',
+ avatar: '/avatar.png',
+ role: 'admin',
+ },
+ ];
+
+ const eligibleMembers =
+ members.length > 0
+ ? members.filter(member => member.role === 'admin')
+ : dummyMembers;
+ const selectedMemberData = eligibleMembers.find(
+ member => member.id === selectedMember
+ );
+
+ const handleTransfer = async () => {
+ if (!selectedMember) return;
+
+ setIsTransferring(true);
+ try {
+ await onTransfer?.(selectedMember);
+ } finally {
+ setIsTransferring(false);
+ }
+ };
+
+ return (
+ <>
+
+
+
+ Transfer the organization ownership
+
+
+ You can only transfer the ownership to one of the members. After
+ ownership being transferred,
+
+ {' '}
+ you will be demoted to admin.
+
+
+
+
+
+
+
+
+
+ {selectedMemberData ? (
+ <>
+
+
+
+ {selectedMemberData.name.charAt(0).toUpperCase()}
+
+
+
+ {selectedMemberData.name}
+
+ >
+ ) : (
+
Select new owner
+ )}
+
+
+
+
+ 0 ? `${popoverWidth}px` : '100%' }}
+ >
+ {eligibleMembers.map(member => (
+ setSelectedMember(member.id)}
+ >
+
+
+
+ {member.name.charAt(0).toUpperCase()}
+
+
+
+
{member.name}
+
{member.email}
+
+ {selectedMember === member.id && (
+
+ )}
+
+ ))}
+
+
+
+
+
+ {isTransferring ? 'Transferring...' : 'Save Changes'}
+
+ >
+ );
+}
diff --git a/components/profile/ActivityFeed.tsx b/components/profile/ActivityFeed.tsx
index ed9553ad7..b1b25abe4 100644
--- a/components/profile/ActivityFeed.tsx
+++ b/components/profile/ActivityFeed.tsx
@@ -3,6 +3,7 @@
import { useState } from 'react';
import { ChevronDown } from 'lucide-react';
import Image from 'next/image';
+import { GetMeResponse } from '@/lib/api/types';
interface ActivityItem {
id: string;
@@ -17,89 +18,6 @@ interface ActivityItem {
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;
@@ -127,14 +45,45 @@ const getActivityDescription = (activity: ActivityItem) => {
interface ActivityFeedProps {
filter: string;
+ user: GetMeResponse;
}
-export default function ActivityFeed({ filter }: ActivityFeedProps) {
+export default function ActivityFeed({ filter, user }: ActivityFeedProps) {
const [showAll, setShowAll] = useState(false);
- const todayActivities = mockActivities.slice(0, 4);
- const yesterdayActivities = mockActivities.slice(4, 6);
- const weekActivities = mockActivities.slice(6, 10);
+ // Use real activities from API, fallback to empty array if none
+ const realActivities = (user.activities || []) as ActivityItem[];
+
+ // For now, show empty state when no activities
+ if (realActivities.length === 0) {
+ return (
+
+
+
No Activity Yet
+
+ Your activity will appear here once you start engaging with projects.
+
+
+ );
+ }
+
+ const todayActivities = realActivities.slice(0, 4);
+ const yesterdayActivities = realActivities.slice(4, 6);
+ const weekActivities = realActivities.slice(6, 10);
// Filter activities based on selected filter
const getFilteredActivities = () => {
@@ -211,7 +160,7 @@ export default function ActivityFeed({ filter }: ActivityFeedProps) {
EARLIER
- {mockActivities.slice(10).map(activity => (
+ {realActivities.slice(10).map(activity => (
{
+ const months = ['January', 'February', 'March', 'April', 'May', 'June'];
+ const stats = user.stats || {};
+
+ return months.map(month => ({
+ month,
+ votes: Math.floor((stats.votes || 0) * (0.5 + Math.random() * 0.5)),
+ grants: Math.floor((stats.grants || 0) * (0.5 + Math.random() * 0.5)),
+ hackathons: Math.floor(
+ (stats.hackathons || 0) * (0.5 + Math.random() * 0.5)
+ ),
+ donations: Math.floor(
+ (stats.donations || 0) * (0.5 + Math.random() * 0.5)
+ ),
+ }));
+ };
+
+ const chartData = generateChartData();
const chartConfig = {
votes: {
@@ -54,19 +58,27 @@ export default function ActivityTab() {
Votes
-
1,224
+
+ {user.stats?.votes || 0}
+
Grants
-
860
+
+ {user.stats?.grants || 0}
+
Hackathons
-
440
+
+ {user.stats?.hackathons || 0}
+
Donations
-
930
+
+ {user.stats?.donations || 0}
+
diff --git a/components/profile/FollowersModal.tsx b/components/profile/FollowersModal.tsx
index 658244fb0..9664be765 100644
--- a/components/profile/FollowersModal.tsx
+++ b/components/profile/FollowersModal.tsx
@@ -6,7 +6,6 @@ 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;
@@ -29,13 +28,13 @@ export default function FollowersModal({
sortFilter,
setSortFilter,
getFilteredProjects,
- } = useProjectFilters(projects || mockProjects);
+ } = useProjectFilters(projects || []);
const handleMemberClick = (member: TeamMember) => {
window.open(`/profile/${member.username}`, '_blank');
};
- const displayUsers = users || mockFollowers;
+ const displayUsers = users || [];
const displayProjects = getFilteredProjects();
return (
@@ -66,6 +65,3 @@ export default function FollowersModal({
);
}
-
-// Export the mock data for use in ProfileHeader
-export { mockFollowers, mockProjects };
diff --git a/components/profile/ProfileDataClient.tsx b/components/profile/ProfileDataClient.tsx
index 2591501ee..5f324f732 100644
--- a/components/profile/ProfileDataClient.tsx
+++ b/components/profile/ProfileDataClient.tsx
@@ -70,7 +70,7 @@ export default function ProfileDataClient({
-
+
@@ -121,11 +121,11 @@ export default function ProfileDataClient({
-
+
diff --git a/components/profile/ProfileHeader.tsx b/components/profile/ProfileHeader.tsx
index 128407318..87e9e9ec2 100644
--- a/components/profile/ProfileHeader.tsx
+++ b/components/profile/ProfileHeader.tsx
@@ -4,21 +4,47 @@ import { useState } from 'react';
import Image from 'next/image';
import Link from 'next/link';
import { UserProfile, UserStats as UserStatsType } from '@/types/profile';
+import { GetMeResponse } from '@/lib/api/types';
+import { TeamMember } from '@/components/ui/TeamList';
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';
+import FollowersModal from './FollowersModal';
interface ProfileHeaderProps {
profile: UserProfile;
stats: UserStatsType;
+ user: GetMeResponse;
}
-export default function ProfileHeader({ profile, stats }: ProfileHeaderProps) {
+export default function ProfileHeader({
+ profile,
+ stats,
+ user,
+}: ProfileHeaderProps) {
const [followersModalOpen, setFollowersModalOpen] = useState(false);
const [followingModalOpen, setFollowingModalOpen] = useState(false);
+ // Convert API user data to TeamMember format
+ const convertToTeamMembers = (
+ users: GetMeResponse['followers']
+ ): TeamMember[] => {
+ return users.map(user => ({
+ id: user._id,
+ name:
+ `${user.profile?.firstName || ''} ${user.profile?.lastName || ''}`.trim() ||
+ 'Unknown User',
+ role: 'MEMBER' as const,
+ avatar: user.profile?.avatar || '/avatar.png',
+ username: user.profile?.username || user._id,
+ joinedAt:
+ typeof user.createdAt === 'string'
+ ? user.createdAt
+ : new Date().toISOString(),
+ }));
+ };
+
const handleFollowersClick = () => {
setFollowersModalOpen(true);
};
@@ -91,15 +117,15 @@ export default function ProfileHeader({ profile, stats }: ProfileHeaderProps) {
open={followersModalOpen}
setOpen={setFollowersModalOpen}
type='followers'
- users={mockFollowers}
+ users={convertToTeamMembers(user.followers || [])}
/>
);
diff --git a/components/profile/ProfileOverview.tsx b/components/profile/ProfileOverview.tsx
index 0ca003add..f4045b7ef 100644
--- a/components/profile/ProfileOverview.tsx
+++ b/components/profile/ProfileOverview.tsx
@@ -41,7 +41,7 @@ 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
index b2accfd1e..4cbf0cf3d 100644
--- a/components/profile/ProjectList.tsx
+++ b/components/profile/ProjectList.tsx
@@ -1,7 +1,7 @@
'use client';
import { Project } from '@/types/project';
-import ProjectCard from '../ProjectCard';
+import ProjectCard from '../landing-page/project/ProjectCard';
interface ProjectListProps {
projects: Project[];
@@ -17,10 +17,47 @@ export function ProjectList({ projects, activeTab }: ProjectListProps) {
);
}
+ // Map project status to ProjectCard expected status
+ const getProjectStatus = (
+ status: string
+ ): 'Validation' | 'Funding' | 'Funded' | 'Completed' => {
+ switch (status) {
+ case 'under_review':
+ return 'Validation';
+ case 'funding':
+ return 'Funding';
+ case 'funded':
+ return 'Funded';
+ case 'completed':
+ return 'Completed';
+ case 'in_progress':
+ return 'Funding';
+ default:
+ return 'Validation';
+ }
+ };
+
return (
{projects.map(project => (
-
+
))}
);
diff --git a/components/profile/ProjectsTab.tsx b/components/profile/ProjectsTab.tsx
index c4cb988f9..a3e6818d2 100644
--- a/components/profile/ProjectsTab.tsx
+++ b/components/profile/ProjectsTab.tsx
@@ -1,45 +1,88 @@
'use client';
import { useState, useEffect, useCallback } from 'react';
-import ProjectCard from '../ProjectCard';
+import ProjectCard from '../landing-page/project/ProjectCard';
import { Project } from '@/types/project';
-import { mockProjects } from './mockData';
+import { GetMeResponse } from '@/lib/api/types';
+import { useWindowSize } from '@/hooks/use-window-size';
+import { ScrollArea } from '@/components/ui/scroll-area';
-export default function ProjectsTab() {
+interface ProjectsTabProps {
+ user: GetMeResponse;
+}
+
+export default function ProjectsTab({ user }: ProjectsTabProps) {
const [projects, setProjects] = useState
([]);
const [loading, setLoading] = useState(false);
const [hasMore, setHasMore] = useState(true);
const [page, setPage] = useState(1);
-
+ const { height: windowHeight } = useWindowSize();
const itemsPerPage = 6;
- // Simulate API call
- const loadProjects = useCallback(async (pageNum: number) => {
- setLoading(true);
+ // Map project status to ProjectCard expected status
+ const getProjectStatus = (
+ status: string
+ ): 'Validation' | 'Funding' | 'Funded' | 'Completed' => {
+ switch (status) {
+ case 'under_review':
+ return 'Validation';
+ case 'funding':
+ return 'Funding';
+ case 'funded':
+ return 'Funded';
+ case 'completed':
+ return 'Completed';
+ case 'in_progress':
+ return 'Funding';
+ default:
+ return 'Validation';
+ }
+ };
- // Simulate API delay
- await new Promise(resolve => setTimeout(resolve, 1000));
+ const calculateScrollHeight = () => {
+ if (!windowHeight) return '400px';
- const startIndex = (pageNum - 1) * itemsPerPage;
- const endIndex = startIndex + itemsPerPage;
- const newProjects = mockProjects.slice(startIndex, endIndex);
+ const headerHeight = 80;
+ const tabsHeight = 60;
+ const projectsHeaderHeight = 60;
+ const padding = 40;
- if (pageNum === 1) {
- setProjects(newProjects);
- } else {
- setProjects(prev => [...prev, ...newProjects]);
- }
+ const availableHeight =
+ windowHeight - headerHeight - tabsHeight - projectsHeaderHeight - padding;
+
+ return Math.max(300, Math.min(availableHeight, windowHeight * 0.6)) + 'px';
+ };
+
+ const loadProjects = useCallback(
+ async (pageNum: number) => {
+ setLoading(true);
+
+ // Simulate API delay for better UX
+ await new Promise(resolve => setTimeout(resolve, 500));
+
+ const startIndex = (pageNum - 1) * itemsPerPage;
+ const endIndex = startIndex + itemsPerPage;
- setHasMore(endIndex < mockProjects.length);
- setLoading(false);
- }, []);
+ // Use real projects data from API
+ const allProjects = user.projects || [];
+ const newProjects = allProjects.slice(startIndex, endIndex);
+
+ if (pageNum === 1) {
+ setProjects(newProjects);
+ } else {
+ setProjects(prev => [...prev, ...newProjects]);
+ }
+
+ setHasMore(endIndex < allProjects.length);
+ setLoading(false);
+ },
+ [user.projects]
+ );
- // Load initial projects
useEffect(() => {
loadProjects(1);
}, [loadProjects]);
- // Infinite scroll handler
const handleScroll = useCallback(
(e: React.UIEvent) => {
const { scrollTop, scrollHeight, clientHeight } = e.currentTarget;
@@ -73,41 +116,55 @@ export default function ProjectsTab() {
Your Projects
- {projects.length} projects
+ {user.projects?.length || 0} projects
-
- {projects.map(project => (
-
- ))}
-
- {loading && (
-
-
-
-
- Loading more projects...
-
+
+ {projects.map(project => (
+
+ ))}
+
+ {loading && (
+
+
+
+
+ Loading more projects...
+
+
-
- )}
-
- {!hasMore && projects.length > 0 && (
-
-
No more projects to load
-
- )}
-
+ )}
+
+ {!hasMore && projects.length > 0 && (
+
+
No more projects to load
+
+ )}
+
+
);
}
diff --git a/components/profile/mockData.ts b/components/profile/mockData.ts
deleted file mode 100644
index 732209450..000000000
--- a/components/profile/mockData.ts
+++ /dev/null
@@ -1,344 +0,0 @@
-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/project-details/project-sidebar/ProjectSidebarHeader.tsx b/components/project-details/project-sidebar/ProjectSidebarHeader.tsx
index cebdfe020..ea95e8a0c 100644
--- a/components/project-details/project-sidebar/ProjectSidebarHeader.tsx
+++ b/components/project-details/project-sidebar/ProjectSidebarHeader.tsx
@@ -13,7 +13,7 @@ export function ProjectSidebarHeader({
case 'campaigning':
return 'bg-secondary-75 border-secondary-600 text-secondary-600';
case 'Funded':
- return 'bg-[rgba(167,249,80,0.08)] border-primary text-primary';
+ return 'bg-active-bg border-primary text-primary';
case 'Completed':
return 'bg-success-75 border-success-600 text-success-600';
case 'Validation':
diff --git a/components/project/ProjectsPage.tsx b/components/project/ProjectsPage.tsx
index 6c0c6f67e..83a7ed01f 100644
--- a/components/project/ProjectsPage.tsx
+++ b/components/project/ProjectsPage.tsx
@@ -38,7 +38,7 @@ export default function ProjectsClient({
const projectCards = React.useMemo(() => {
return projects.map(project => {
const cardData = transformProjectForCard(project);
- return ;
+ return ;
});
}, [projects, transformProjectForCard]);
diff --git a/hooks/use-debounce-callback.tsx b/hooks/use-debounce-callback.tsx
new file mode 100644
index 000000000..01063d37a
--- /dev/null
+++ b/hooks/use-debounce-callback.tsx
@@ -0,0 +1,63 @@
+'use client';
+
+import * as React from 'react';
+import debounce from 'lodash.debounce';
+
+import { useUnmount } from './use-unmount';
+
+type DebounceOptions = {
+ leading?: boolean;
+ trailing?: boolean;
+ maxWait?: number;
+};
+
+type ControlFunctions = {
+ cancel: () => void;
+ flush: () => void;
+ isPending: () => boolean;
+};
+
+export type DebouncedState ReturnType> = ((
+ ...args: Parameters
+) => ReturnType | undefined) &
+ ControlFunctions;
+
+export function useDebounceCallback<
+ T extends (...args: unknown[]) => ReturnType,
+>(func: T, delay = 500, options?: DebounceOptions): DebouncedState {
+ const debouncedFunc = React.useRef>(null);
+
+ useUnmount(() => {
+ if (debouncedFunc.current) {
+ debouncedFunc.current.cancel();
+ }
+ });
+
+ const debounced = React.useMemo(() => {
+ const debouncedFuncInstance = debounce(func, delay, options);
+
+ const wrappedFunc: DebouncedState = (...args: Parameters) => {
+ return debouncedFuncInstance(...args);
+ };
+
+ wrappedFunc.cancel = () => {
+ debouncedFuncInstance.cancel();
+ };
+
+ wrappedFunc.isPending = () => {
+ return !!debouncedFunc.current;
+ };
+
+ wrappedFunc.flush = () => {
+ return debouncedFuncInstance.flush();
+ };
+
+ return wrappedFunc;
+ }, [func, delay, options]);
+
+ React.useEffect(() => {
+ debouncedFunc.current = debounce(func, delay, options);
+ }, [func, delay, options]);
+
+ return debounced;
+}
diff --git a/hooks/use-isomorphic-layout-effect.tsx b/hooks/use-isomorphic-layout-effect.tsx
new file mode 100644
index 000000000..afb31330f
--- /dev/null
+++ b/hooks/use-isomorphic-layout-effect.tsx
@@ -0,0 +1,6 @@
+'use client';
+
+import * as React from 'react';
+
+export const useIsomorphicLayoutEffect =
+ typeof window !== 'undefined' ? React.useLayoutEffect : React.useEffect;
diff --git a/hooks/use-unmount.tsx b/hooks/use-unmount.tsx
new file mode 100644
index 000000000..72545ef24
--- /dev/null
+++ b/hooks/use-unmount.tsx
@@ -0,0 +1,38 @@
+'use client';
+
+import * as React from 'react';
+
+/**
+ * A React hook that runs a cleanup function when the component unmounts.
+ *
+ * @param fn - The cleanup function to run on unmount
+ *
+ * @example
+ * ```tsx
+ * function MyComponent() {
+ * useUnmount(() => {
+ * // Cleanup logic here
+ * console.log('Component is unmounting');
+ * });
+ *
+ * return Hello world
;
+ * }
+ * ```
+ */
+export function useUnmount(fn: () => void): void {
+ if (typeof fn !== 'function') {
+ throw new Error('useUnmount expects a function as argument');
+ }
+
+ const fnRef = React.useRef(fn);
+
+ // Keep the function reference up to date
+ fnRef.current = fn;
+
+ React.useEffect(() => {
+ // Return the cleanup function that will be called on unmount
+ return () => {
+ fnRef.current();
+ };
+ }, []);
+}
diff --git a/hooks/use-window-size.tsx b/hooks/use-window-size.tsx
new file mode 100644
index 000000000..657d4f8ee
--- /dev/null
+++ b/hooks/use-window-size.tsx
@@ -0,0 +1,89 @@
+'use client';
+
+import { useState } from 'react';
+import { useDebounceCallback } from './use-debounce-callback';
+import { useIsomorphicLayoutEffect } from './use-isomorphic-layout-effect';
+import * as React from 'react';
+
+type WindowSize = {
+ width: T;
+ height: T;
+};
+
+type UseWindowSizeOptions = {
+ initializeWithValue: InitializeWithValue;
+ debounceDelay?: number;
+};
+
+const IS_SERVER = typeof window === 'undefined';
+
+// Compact event listener hook for resize events
+function useEventListener(eventName: string, handler: () => void) {
+ const savedHandler = React.useRef(handler);
+
+ React.useEffect(() => {
+ savedHandler.current = handler;
+ }, [handler]);
+
+ React.useEffect(() => {
+ if (IS_SERVER) return;
+ const eventListener = () => savedHandler.current();
+ window.addEventListener(eventName, eventListener);
+ return () => window.removeEventListener(eventName, eventListener);
+ }, [eventName]);
+}
+
+// SSR version of useWindowSize.
+export function useWindowSize(options: UseWindowSizeOptions): WindowSize;
+// CSR version of useWindowSize.
+export function useWindowSize(
+ options?: Partial>
+): WindowSize;
+export function useWindowSize(
+ options: Partial> = {}
+): WindowSize | WindowSize {
+ let { initializeWithValue = true } = options;
+ if (IS_SERVER) {
+ initializeWithValue = false;
+ }
+
+ const [windowSize, setWindowSize] = useState(() => {
+ if (initializeWithValue) {
+ return {
+ width: window.innerWidth,
+ height: window.innerHeight,
+ };
+ }
+ return {
+ width: undefined,
+ height: undefined,
+ };
+ });
+
+ const debouncedSetWindowSize = useDebounceCallback((...args: unknown[]) => {
+ const size = args[0] as WindowSize;
+ setWindowSize(size);
+ }, options.debounceDelay);
+
+ function handleSize() {
+ const newSize = {
+ width: window.innerWidth,
+ height: window.innerHeight,
+ };
+
+ if (options.debounceDelay) {
+ debouncedSetWindowSize(newSize);
+ } else {
+ setWindowSize(newSize);
+ }
+ }
+
+ useEventListener('resize', handleSize);
+
+ // Set size at the first client-side load
+ useIsomorphicLayoutEffect(() => {
+ handleSize();
+ }, []);
+
+ return windowSize;
+}
diff --git a/lib/api/types.ts b/lib/api/types.ts
index deb8e08de..67c8c1270 100644
--- a/lib/api/types.ts
+++ b/lib/api/types.ts
@@ -97,6 +97,23 @@ export type GetMeResponse = User & {
projects: Project[];
following: User[];
followers: User[];
+ stats: {
+ votes: number;
+ grants: number;
+ hackathons: number;
+ donations: number;
+ projectsCreated: number;
+ projectsFunded: number;
+ totalContributed: number;
+ reputation: number;
+ communityScore: number;
+ commentsPosted: number;
+ organizations: number;
+ followers: number;
+ following: number;
+ };
+ activities: unknown[];
+ contributedProjects: unknown[];
};
// Logout
diff --git a/package-lock.json b/package-lock.json
index 2f627c84b..80b3796c9 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -60,6 +60,7 @@
"gsap": "^3.13.0",
"input-otp": "^1.4.2",
"js-cookie": "^3.0.5",
+ "lodash.debounce": "^4.0.8",
"lucide-react": "^0.525.0",
"marked": "^16.3.0",
"next": "^15.5.2",
@@ -85,6 +86,7 @@
"@eslint/eslintrc": "^3",
"@tailwindcss/postcss": "^4",
"@types/js-cookie": "^3.0.6",
+ "@types/lodash.debounce": "^4.0.9",
"@types/node": "^20",
"@types/react": "^19",
"@types/react-dom": "^19",
@@ -7297,6 +7299,16 @@
"integrity": "sha512-H3MHACvFUEiujabxhaI/ImO6gUrd8oOurg7LQtS7mbwIXA/cUqWrvBsaeJ23aZEPk1TAYkurjfMbSELfoCXlGA==",
"license": "MIT"
},
+ "node_modules/@types/lodash.debounce": {
+ "version": "4.0.9",
+ "resolved": "https://registry.npmjs.org/@types/lodash.debounce/-/lodash.debounce-4.0.9.tgz",
+ "integrity": "sha512-Ma5JcgTREwpLRwMM+XwBR7DaWe96nC38uCBDFKZWbNKD+osjVzdpnUSwBcqCptrp16sSOLBAUb50Car5I0TCsQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/lodash": "*"
+ }
+ },
"node_modules/@types/marked": {
"version": "5.0.2",
"resolved": "https://registry.npmjs.org/@types/marked/-/marked-5.0.2.tgz",
@@ -13890,6 +13902,12 @@
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
"license": "MIT"
},
+ "node_modules/lodash.debounce": {
+ "version": "4.0.8",
+ "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz",
+ "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==",
+ "license": "MIT"
+ },
"node_modules/lodash.isequal": {
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz",
diff --git a/package.json b/package.json
index 896ab8248..0ead306c2 100644
--- a/package.json
+++ b/package.json
@@ -75,6 +75,7 @@
"gsap": "^3.13.0",
"input-otp": "^1.4.2",
"js-cookie": "^3.0.5",
+ "lodash.debounce": "^4.0.8",
"lucide-react": "^0.525.0",
"marked": "^16.3.0",
"next": "^15.5.2",
@@ -100,6 +101,7 @@
"@eslint/eslintrc": "^3",
"@tailwindcss/postcss": "^4",
"@types/js-cookie": "^3.0.6",
+ "@types/lodash.debounce": "^4.0.9",
"@types/node": "^20",
"@types/react": "^19",
"@types/react-dom": "^19",
From 931dc454fe11c41333e344242cc0f8a28d5c48e2 Mon Sep 17 00:00:00 2001
From: Collins Ikechukwu
Date: Mon, 20 Oct 2025 06:00:04 -0700
Subject: [PATCH 16/45] Collins (#335)
* refactor: update project components and API for voting functionality
- Adjust layout and styling in ProjectsPage and ProjectCard components.
- Enhance ProjectSidebar to include crowdfund data and voting logic.
- Implement new API methods for voting, retrieving votes, and removing votes.
- Update types for vote handling in the API.
- Clean up unused code and improve responsiveness across components.
* chore: update project dependencies and enhance project details layout
* feat: added static api for staging
* fix: made few changes
* fix: minor changes
* feat: chaged test data
* fix: minor fixes
* refactor: update project components and improve functionality
* feat: file clean up
* feat: added extra validation
* feat: integrate wallet protection and validation across components
* feat: enhance profile components and add activity tracking
- Refactor ProfileData to use ProfileDataClient for improved user data display.
- Introduce ActivityFeed and ActivityTab components for tracking user activities.
- Implement filtering and sorting functionality in the new ProjectsTab and FilterControls components.
- Add modal support for displaying followers and following lists.
- Create reusable TeamList component for displaying team members across various sections.
- Integrate mock data for testing and development purposes.
- Update UserStats to include clickable follower and following counts for better user interaction.
* feat: add lodash.debounce and enhance profile components with user data integration
- Added lodash.debounce for improved performance in user interactions.
- Updated ProfileDataClient to utilize user data for displaying activities and stats.
- Refactored ActivityFeed and ActivityTab components to fetch real user activities and statistics.
- Removed mock data dependencies from FollowersModal and ProjectsTab, integrating real user data instead.
- Enhanced ProjectsTab to support infinite scrolling and dynamic project loading based on user data.
* feat: enhance navbar and project components for improved responsiveness and user experience
- Integrated useWindowSize hook to adjust component layouts and styles based on screen size.
- Updated Navbar component to improve spacing and responsiveness for various screen sizes.
- Refactored ProjectCard to support full-width display and adjusted styles accordingly.
- Enhanced ProjectList and ProjectsTab to utilize updated ProjectCard properties and improve project status mapping.
- Improved MobileMenu and AuthenticatedNav for better user interaction and accessibility.
* feat: enhance organization layout and settings components for improved user experience
- Updated OrganizationsLayout to include relative positioning for better layout control.
- Refactored OrganizationContent to replace Button with BoundlessButton for consistency.
- Enhanced OrganizationHeader with sticky positioning and improved link styling.
- Introduced new tabs in OrganizationSettings for Profile, Links, Members, and Transfer Ownership, improving organization management.
- Added new components for managing organization links and members, including EmailInviteSection and PermissionsTable.
- Improved styling and layout across various components for better responsiveness and user interaction.
* feat: implemeted the organisation features
---
.../organizations/[id]/grants/page.tsx | 11 -
.../organizations/[id]/hackathons/page.tsx | 9 -
.../organizations/[id]/settings/page.tsx | 7 +-
app/(landing)/organizations/layout.tsx | 41 +-
app/globals.css | 3 +-
.../organization/OrganizationContent.tsx | 66 +-
.../organization/OrganizationHeader.tsx | 37 +-
components/organization/OrganizationPage.tsx | 2 -
.../organization/OrganizationSidebar.tsx | 33 +-
.../cards/OrganizationSelector.tsx | 116 ++-
.../organization/cards/OrganzationCards.tsx | 21 +-
components/organization/tabs/LinksTab.tsx | 153 ++-
components/organization/tabs/MembersTab.tsx | 134 ++-
.../tabs/MembersTab/TeamManagementSection.tsx | 5 -
components/organization/tabs/ProfileTab.tsx | 460 ++++++++-
components/profile/ProfileDataClient.tsx | 2 +-
components/profile/ProfileOverview.tsx | 2 +-
lib/api/organization.ts | 642 +++++++++++++
lib/api/types.ts | 22 +-
lib/organization-utils.ts | 431 +++++++++
lib/providers/OrganizationProvider.tsx | 893 ++++++++++++++++++
lib/providers/index.ts | 104 ++
lib/providers/organization-types.ts | 188 ++++
lib/providers/organization-utils.ts | 518 ++++++++++
lib/providers/useOrganization.ts | 417 ++++++++
25 files changed, 4002 insertions(+), 315 deletions(-)
create mode 100644 lib/api/organization.ts
create mode 100644 lib/organization-utils.ts
create mode 100644 lib/providers/OrganizationProvider.tsx
create mode 100644 lib/providers/index.ts
create mode 100644 lib/providers/organization-types.ts
create mode 100644 lib/providers/organization-utils.ts
create mode 100644 lib/providers/useOrganization.ts
diff --git a/app/(landing)/organizations/[id]/grants/page.tsx b/app/(landing)/organizations/[id]/grants/page.tsx
index 73a2f4afe..a3c4a10d8 100644
--- a/app/(landing)/organizations/[id]/grants/page.tsx
+++ b/app/(landing)/organizations/[id]/grants/page.tsx
@@ -1,18 +1,7 @@
-'use client';
-
-import { useParams } from 'next/navigation';
-import OrganizationHeader from '@/components/organization/OrganizationHeader';
-import OrganizationSidebar from '@/components/organization/OrganizationSidebar';
-
export default function GrantsPage() {
- const params = useParams();
- const organizationId = params.id as string;
-
return (
-
-
Grants
Manage your grants here
diff --git a/app/(landing)/organizations/[id]/hackathons/page.tsx b/app/(landing)/organizations/[id]/hackathons/page.tsx
index 2a818a041..9d6156b4a 100644
--- a/app/(landing)/organizations/[id]/hackathons/page.tsx
+++ b/app/(landing)/organizations/[id]/hackathons/page.tsx
@@ -1,18 +1,9 @@
'use client';
-import { useParams } from 'next/navigation';
-import OrganizationHeader from '@/components/organization/OrganizationHeader';
-import OrganizationSidebar from '@/components/organization/OrganizationSidebar';
-
export default function HackathonsPage() {
- const params = useParams();
- const organizationId = params.id as string;
-
return (
-
-
Hackathons
Manage your hackathons here
diff --git a/app/(landing)/organizations/[id]/settings/page.tsx b/app/(landing)/organizations/[id]/settings/page.tsx
index 64813fbab..b37c5143c 100644
--- a/app/(landing)/organizations/[id]/settings/page.tsx
+++ b/app/(landing)/organizations/[id]/settings/page.tsx
@@ -1,17 +1,14 @@
'use client';
import { useParams } from 'next/navigation';
-import OrganizationHeader from '@/components/organization/OrganizationHeader';
-import OrganizationSidebar from '@/components/organization/OrganizationSidebar';
import OrganizationSettings from '@/components/organization/OrganizationSettings';
export default function OrganizationSettingsPage() {
const params = useParams();
const organizationId = params.id as string;
- // TODO: Fetch organization data based on ID
const mockOrgData = {
- name: 'Tech Innovators Hub',
+ name: 'Boundless',
logo: '/tech-company-logo.jpg',
tagline: 'Building the future of technology',
about: 'We are a community of innovators...',
@@ -19,9 +16,7 @@ export default function OrganizationSettingsPage() {
return (
-
-
4 && pathname !== '/dashboard/organizations');
+ pathname !== '/organizations' && pathname.startsWith('/organizations');
+
+ const getOrgIdFromPath = () => {
+ if (pathname.startsWith('/organizations/')) {
+ const pathParts = pathname.split('/');
+ const orgId = pathParts[2];
+ if (orgId && /^[a-f0-9]{24}$/.test(orgId)) {
+ return orgId;
+ }
+ }
+ return null;
+ };
+
+ const initialOrgId = getOrgIdFromPath();
return (
-