diff --git a/app/(landing)/projects/[id]/page.tsx b/app/(landing)/projects/[id]/page.tsx
index 10e29c235..e1aee744d 100644
--- a/app/(landing)/projects/[id]/page.tsx
+++ b/app/(landing)/projects/[id]/page.tsx
@@ -1,16 +1,16 @@
import { ProjectLayout } from '@/components/project-details/project-layout';
import { ProjectLoading } from '@/components/project-details/project-loading';
import { notFound } from 'next/navigation';
-import { getCrowdfundingProject, getProjectDetails } from '@/lib/api/project';
-import { CrowdfundingProject, CrowdfundData } from '@/lib/api/types';
+import { getCrowdfundingProject } from '@/lib/api/project';
+import type { CrowdfundingProject, CrowdfundData } from '@/lib/api/types';
import { Suspense } from 'react';
+
interface ProjectPageProps {
params: Promise<{
id: string;
}>;
}
-// Transform crowdfunding project data to match ProjectLayout interface
function transformCrowdfundingProject(
project: CrowdfundingProject,
crowdfund: CrowdfundData
@@ -19,11 +19,12 @@ function transformCrowdfundingProject(
? `${project.creator.profile.firstName} ${project.creator.profile.lastName}`
: 'Unknown Creator';
- const daysToDeadline = project.funding?.endDate
+ const deadlineDate = crowdfund.voteDeadline || project.funding?.endDate;
+ const daysToDeadline = deadlineDate
? Math.max(
0,
Math.ceil(
- (new Date(project.funding.endDate).getTime() - new Date().getTime()) /
+ (new Date(deadlineDate).getTime() - new Date().getTime()) /
(1000 * 60 * 60 * 24)
)
)
@@ -32,12 +33,11 @@ function transformCrowdfundingProject(
return {
project: {
...project,
- daysToDeadline: Math.max(0, daysToDeadline),
- // Add additional fields without overriding the original creator
+ daysToDeadline,
additionalCreator: {
name: creatorName,
role: 'OWNER',
- avatar: '/user.png', // Default avatar, could be enhanced with actual user avatar
+ avatar: '/user.png',
},
links: [
...(project.githubUrl
@@ -55,6 +55,7 @@ function transformCrowdfundingProject(
icon: link.platform.toLowerCase(),
})) || []),
],
+ votes: crowdfund.totalVotes || 0,
},
crowdfund,
};
@@ -65,144 +66,18 @@ async function ProjectContent({ id }: { id: string }) {
let error: string | null = null;
try {
- // Try to fetch as crowdfunding project first
- try {
- const response = await getCrowdfundingProject(id);
-
- if (response.success && response.data) {
- projectData = transformCrowdfundingProject(
- response.data.project,
- response.data.crowdfund
- );
- } else {
- throw new Error('Failed to fetch crowdfunding project');
- }
- } catch {
- // If crowdfunding project fails, try regular project
- try {
- const regularProject = await getProjectDetails(id);
-
- // Transform regular project data if needed
- // For regular projects, we need to create a mock CrowdfundingProject structure
- const mockProject: CrowdfundingProject = {
- _id: regularProject.id || regularProject._id,
- title: regularProject.title,
- description: regularProject.description,
- category: regularProject.category,
- owner: {
- type: 'individual',
- },
- vision: regularProject.description,
- githubUrl: regularProject.githubUrl,
- projectWebsite: regularProject.projectWebsite,
- demoVideo: regularProject.demoVideo,
- socialLinks: regularProject.socialLinks,
- status: regularProject.status,
- creator: {
- _id: 'unknown',
- profile: {
- firstName: regularProject.ownerName?.split(' ')[0] || 'Unknown',
- lastName: regularProject.ownerName?.split(' ')[1] || 'Unknown',
- username: regularProject.ownerUsername || 'unknown',
- },
- },
- contact: {
- primary: '',
- backup: '',
- },
- createdAt: regularProject.createdAt,
- updatedAt: regularProject.updatedAt || regularProject.createdAt,
- funding: {
- goal: 0,
- raised: 0,
- currency: 'USD',
- endDate: new Date(
- Date.now() + 30 * 24 * 60 * 60 * 1000
- ).toISOString(),
- contributors: [],
- },
- voting: {
- startDate: new Date().toISOString(),
- endDate: new Date(
- Date.now() + 30 * 24 * 60 * 60 * 1000
- ).toISOString(),
- totalVotes: 0,
- positiveVotes: 0,
- negativeVotes: 0,
- voters: [],
- },
- milestones: [],
- team: [],
- media: {
- banner: regularProject.image,
- logo: regularProject.image,
- thumbnail: regularProject.image,
- },
- documents: {
- whitepaper: '',
- pitchDeck: '',
- },
- tags: [],
- grant: {
- isGrant: false,
- totalBudget: 0,
- totalDisbursed: 0,
- proposalsReceived: 0,
- proposalsApproved: 0,
- status: 'pending',
- applications: [],
- },
- summary: regularProject.description,
- type: 'crowdfund',
- votes: 0,
- stakeholders: {
- serviceProvider: '',
- approver: '',
- releaseSigner: '',
- disputeResolver: '',
- receiver: '',
- platformAddress: '',
- },
- trustlessWorkStatus: 'pending',
- escrowType: 'pending',
- __v: 0,
- };
+ const response = await getCrowdfundingProject(id);
- const mockCrowdfund: CrowdfundData = {
- _id: 'mock-crowdfund',
- projectId: regularProject.id || regularProject._id,
- thresholdVotes: 0,
- voteDeadline: new Date(
- Date.now() + 30 * 24 * 60 * 60 * 1000
- ).toISOString(),
- totalVotes: 0,
- voteProgress: 0,
- status: regularProject.status,
- createdAt: regularProject.createdAt,
- updatedAt: regularProject.updatedAt || regularProject.createdAt,
- __v: 0,
- isVotingActive: false,
- id: 'mock-crowdfund',
- };
-
- projectData = {
- project: {
- ...mockProject,
- daysToDeadline: 30,
- additionalCreator: {
- name: regularProject.ownerName || 'Unknown Creator',
- role: 'OWNER',
- avatar: regularProject.ownerAvatar || '/user.png',
- },
- links: [],
- },
- crowdfund: mockCrowdfund,
- };
- } catch {
- error = 'Project not found';
- }
+ if (response.success && response.data) {
+ projectData = transformCrowdfundingProject(
+ response.data.project,
+ response.data.crowdfund
+ );
+ } else {
+ throw new Error(response.message || 'Failed to fetch project');
}
- } catch {
+ } catch (err) {
+ console.error('Error fetching project:', err);
error = 'Failed to fetch project data';
}
@@ -212,32 +87,6 @@ async function ProjectContent({ id }: { id: string }) {
return (
- {/*
-
-
-
-
-
- Back
-
-
-
-
-
- Open
-
-
-
-
-
- */}
-
([]);
- const [loading, setLoading] = useState(true);
- const [loadingMore, setLoadingMore] = useState(false);
- const [error, setError] = useState(null);
- const [currentPage, setCurrentPage] = useState(1);
- const [hasMore, setHasMore] = useState(true);
- const [filters, setFilters] = useState({});
- const [searchTerm, setSearchTerm] = useState('');
-
- // Debounce search term to avoid excessive API calls
- const [debouncedSearchTerm, setDebouncedSearchTerm] = useState(searchTerm);
-
- useEffect(() => {
- const handler = setTimeout(() => {
- setDebouncedSearchTerm(searchTerm);
- }, 500);
-
- return () => {
- clearTimeout(handler);
- };
- }, [searchTerm]);
-
- // Get project deadline in days
- const getProjectDeadline = useCallback(
- (project: CrowdfundingProject): number => {
- if (project.status === 'idea') {
- return new Date(project.voting.endDate).getTime();
- } else if (
- project.status === 'active' ||
- project.status === 'completed'
- ) {
- return new Date(project.funding.endDate).getTime();
- }
- return 0;
- },
- []
- );
-
- // Sort projects based on sort option
- const sortProjects = useCallback(
- (
- projects: CrowdfundingProject[],
- sortOption: SortOption
- ): CrowdfundingProject[] => {
- const sorted = [...projects];
-
- switch (sortOption) {
- case 'newest':
- return sorted.sort(
- (a, b) =>
- new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()
- );
- case 'oldest':
- return sorted.sort(
- (a, b) =>
- new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime()
- );
- case 'funding_goal_high':
- return sorted.sort((a, b) => b.funding.goal - a.funding.goal);
- case 'funding_goal_low':
- return sorted.sort((a, b) => a.funding.goal - b.funding.goal);
- case 'deadline_soon':
- return sorted.sort((a, b) => {
- const aDeadline = getProjectDeadline(a);
- const bDeadline = getProjectDeadline(b);
- return aDeadline - bDeadline;
- });
- case 'deadline_far':
- return sorted.sort((a, b) => {
- const aDeadline = getProjectDeadline(a);
- const bDeadline = getProjectDeadline(b);
- return bDeadline - aDeadline;
- });
- default:
- return sorted;
- }
- },
- [getProjectDeadline]
- );
-
- const fetchProjects = useCallback(
- async (page = 1, newFilters: ProjectFilters = {}, append = false) => {
- try {
- if (append) {
- setLoadingMore(true);
- } else {
- setLoading(true);
- }
- setError(null);
-
- const response = await getCrowdfundingProjects(page, 9, {
- category: newFilters.category,
- status: newFilters.status,
- });
-
- let fetchedProjects = response.data.projects;
-
- // Apply client-side search filtering
- if (newFilters.search) {
- const searchLower = newFilters.search.toLowerCase();
- fetchedProjects = fetchedProjects.filter(
- project =>
- project.title.toLowerCase().includes(searchLower) ||
- project.vision.toLowerCase().includes(searchLower) ||
- project.description.toLowerCase().includes(searchLower) ||
- project.category.toLowerCase().includes(searchLower) ||
- `${project.creator.profile.firstName} ${project.creator.profile.lastName}`
- .toLowerCase()
- .includes(searchLower)
- );
- }
-
- // Apply client-side sorting
- if (newFilters.sort) {
- fetchedProjects = sortProjects(fetchedProjects, newFilters.sort);
- }
-
- if (append) {
- setProjects(prev => [...prev, ...fetchedProjects]);
- } else {
- setProjects(fetchedProjects);
- }
-
- setHasMore(response.data.pagination.pages > page);
- } catch (err) {
- // eslint-disable-next-line no-console
- console.error('Error fetching projects:', err);
- setError('Failed to fetch projects. Please try again.');
- } finally {
- setLoading(false);
- setLoadingMore(false);
- }
- },
- [sortProjects]
- );
-
- // Update filters when debounced search term changes
- useEffect(() => {
- setFilters(prev => ({ ...prev, search: debouncedSearchTerm }));
- }, [debouncedSearchTerm]);
-
- // Fetch projects when filters change
- useEffect(() => {
- setCurrentPage(1);
- setHasMore(true);
- fetchProjects(1, filters);
- }, [filters, fetchProjects]);
-
- const handleSearch = useCallback((searchTerm: string) => {
- setSearchTerm(searchTerm);
- }, []);
-
- const handleSort = useCallback((sortType: string) => {
- setFilters(prev => ({ ...prev, sort: sortType as SortOption }));
- }, []);
-
- const handleStatus = useCallback((status: string) => {
- setFilters(prev => ({
- ...prev,
- status: status === 'all' ? undefined : status,
- }));
- }, []);
-
- const handleCategory = useCallback((category: string) => {
- setFilters(prev => ({
- ...prev,
- category: category === 'all' ? undefined : category,
- }));
- }, []);
-
- const handleLoadMore = useCallback(() => {
- const nextPage = currentPage + 1;
- setCurrentPage(nextPage);
- fetchProjects(nextPage, filters, true);
- }, [currentPage, filters, fetchProjects]);
-
- // Transform project data for ProjectCard component
- const transformProjectForCard = useCallback(
- (project: CrowdfundingProject) => {
- let deadlineInDays: number | null = null;
-
- try {
- if (project.status === 'idea') {
- const now = new Date();
- const end = new Date(project.voting.endDate);
- deadlineInDays = Math.ceil(
- (end.getTime() - now.getTime()) / (1000 * 60 * 60 * 24)
- );
- } else if (
- project.status === 'active' ||
- project.status === 'completed'
- ) {
- const now = new Date();
- const end = new Date(project.funding.endDate);
- deadlineInDays = Math.ceil(
- (end.getTime() - now.getTime()) / (1000 * 60 * 60 * 24)
- );
- }
- } catch (error) {
- // eslint-disable-next-line no-console
- console.warn(
- 'Error calculating deadline for project:',
- project._id,
- error
- );
- deadlineInDays = null;
- }
-
- // Map project status to card status
- let cardStatus: 'Validation' | 'Funding' | 'Funded' | 'Completed' =
- 'Funding';
- if (project.status === 'draft' || project.status === 'idea') {
- cardStatus = 'Validation';
- } else if (project.status === 'active') {
- cardStatus = 'Funding';
- } else if (project.status === 'completed') {
- cardStatus = 'Completed';
- }
-
- return {
- projectId: project._id,
- creatorName: `${project.creator.profile.firstName} ${project.creator.profile.lastName}`,
- creatorLogo: '/avatar.png', // Default avatar, should be from project.creator.avatar
- projectImage:
- project.media?.logo ||
- project.media?.logo ||
- '/landing/explore/project-placeholder-1.png',
- projectTitle: project.title,
- projectDescription: project.vision || project.description,
- status: cardStatus,
- deadlineInDays: deadlineInDays || 0,
- funding: {
- current: project.funding?.raised || 0,
- goal: project.funding?.goal || 0,
- currency: project.funding?.currency || 'USDC',
- },
- votes:
- project.status === 'idea'
- ? {
- current: project.votes || 0,
- goal: project.voting?.totalVotes || 100,
- }
- : undefined,
- };
- },
- []
- );
-
- // Memoized project cards to prevent unnecessary re-renders
- const projectCards = useMemo(() => {
- return projects.map(project => {
- const cardData = transformProjectForCard(project);
- return ;
- });
- }, [projects, transformProjectForCard]);
-
- // Load More Button component
- const LoadMoreButton = useMemo(() => {
- if (!hasMore) return null;
-
- return (
-
-
- {loadingMore && (
-
- )}
- {loadingMore ? 'Loading...' : 'Load More Projects'}
-
-
- );
- }, [hasMore, loadingMore, handleLoadMore]);
+import ProjectsClient from '@/components/project/ProjectsPage';
+export default function ProjectsPage() {
return (
-
-
-
- {/* Results Summary */}
- {!loading && !error && (
-
-
- {projects.length > 0 ? (
-
- Showing {projects.length} project
- {projects.length !== 1 ? 's' : ''}
- {filters.search && ` for "${filters.search}"`}
- {filters.category && ` in ${filters.category}`}
- {filters.status && ` with status ${filters.status}`}
-
- ) : (
- No projects found
- )}
-
- {filters.search && (
-
setSearchTerm('')}
- className='text-primary hover:text-primary/80 text-sm'
- >
- Clear search
-
- )}
-
- )}
-
- {/* Loading State */}
- {loading && (
-
-
-
Loading projects...
-
- )}
-
- {/* Error State */}
- {error && (
-
-
-
⚠️
-
- Something went wrong
-
-
{error}
-
fetchProjects(currentPage, filters)}
- className='bg-primary hover:bg-primary/80 rounded-lg px-6 py-3 text-white transition-colors'
- >
- Try Again
-
-
-
- )}
-
- {/* Empty State */}
- {!loading && !error && projects.length === 0 && (
-
-
-
🔍
-
- No projects found
-
-
- {filters.search || filters.category || filters.status
- ? 'Try adjusting your filters or search terms'
- : 'No projects are available at the moment'}
-
- {(filters.search || filters.category || filters.status) && (
-
{
- setSearchTerm('');
- setFilters({});
- }}
- className='bg-primary hover:bg-primary/80 rounded-lg px-6 py-3 text-white transition-colors'
- >
- Clear all filters
-
- )}
-
-
- )}
-
- {/* Projects Grid */}
- {!loading && !error && projects.length > 0 && (
- <>
-
- {projectCards}
-
- {LoadMoreButton}
- >
- )}
-
+
);
}
-
-export default ProjectsPage;
diff --git a/app/globals.css b/app/globals.css
index 2899e4edd..ce48e5a7b 100644
--- a/app/globals.css
+++ b/app/globals.css
@@ -80,6 +80,10 @@
z-index: 2;
}
+body {
+ scroll-behavior: smooth;
+}
+
@theme inline {
--color-background: var(--background);
--color-foreground: var(--foreground);
diff --git a/components/Project-Page-Hero.tsx b/components/Project-Page-Hero.tsx
index f144e5134..80e9bdb18 100644
--- a/components/Project-Page-Hero.tsx
+++ b/components/Project-Page-Hero.tsx
@@ -1,7 +1,9 @@
+'use client';
import React from 'react';
import Image from 'next/image';
import { ArrowDown } from 'lucide-react';
import { BoundlessButton } from './buttons/BoundlessButton';
+import Link from 'next/link';
export default function ProjectPageHero() {
return (
@@ -41,15 +43,17 @@ export default function ProjectPageHero() {
Validated by the community. Backed milestone by milestone.
-
-
- Start Exploring Projects
-
-
-
+
+
+
+ Start Exploring Projects
+
+
+
+
diff --git a/components/landing-page/project/ProjectCard.tsx b/components/landing-page/project/ProjectCard.tsx
index caa81b54d..fd64e1ed9 100644
--- a/components/landing-page/project/ProjectCard.tsx
+++ b/components/landing-page/project/ProjectCard.tsx
@@ -1,3 +1,4 @@
+'use client';
import { Progress } from '@/components/ui/progress';
import { formatNumber } from '@/lib/utils';
import { useRouter } from 'nextjs-toploader/app';
diff --git a/components/project-details/comment-section/comments-empty-state.tsx b/components/project-details/comment-section/comments-empty-state.tsx
index df56abf52..77242d7b1 100644
--- a/components/project-details/comment-section/comments-empty-state.tsx
+++ b/components/project-details/comment-section/comments-empty-state.tsx
@@ -11,8 +11,8 @@ export function CommentsEmptyState({ onAddComment }: CommentsEmptyStateProps) {
return (
-
-
+
+
Be the first to Leave a Comment
diff --git a/components/project-details/project-layout.tsx b/components/project-details/project-layout.tsx
index f4ecf507e..c24747968 100644
--- a/components/project-details/project-layout.tsx
+++ b/components/project-details/project-layout.tsx
@@ -13,7 +13,7 @@ import { ProjectComments } from './comment-section/project-comments';
import ProjectMilestone from './project-milestone';
import ProjectVoters from './project-voters';
import ProjectBackers from './project-backers';
-import FundProject from '@/components/modals/fund-project';
+// import FundProject from '@/components/modals/fund-project';
interface ProjectLayoutProps {
project: CrowdfundingProject & {
@@ -344,7 +344,7 @@ export function ProjectLayout({ project }: ProjectLayoutProps) {
-
{}} />
+ {/* {}} /> */}
);
}
diff --git a/components/project-details/project-sidebar.tsx b/components/project-details/project-sidebar.tsx
index d4d72bf51..51cc8de7f 100644
--- a/components/project-details/project-sidebar.tsx
+++ b/components/project-details/project-sidebar.tsx
@@ -1,3 +1,5 @@
+'use client';
+
import {
Calendar,
Github,
@@ -7,12 +9,14 @@ import {
Share2,
UserPlus,
ArrowUp,
+ DollarSign,
} from 'lucide-react';
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
import { Button } from '@/components/ui/button';
import { Progress } from '@/components/ui/progress';
import Image from 'next/image';
-import { CrowdfundingProject } from '@/lib/api/types';
+import type { CrowdfundingProject } from '@/lib/api/types';
+import { voteProject } from '@/lib/api/project';
interface ProjectSidebarProps {
project: CrowdfundingProject & {
@@ -142,7 +146,10 @@ export function ProjectSidebar({
{/* Action Buttons - Responsive widths */}
-
+ voteProject(project._id, 1)}
+ className='flex h-12 flex-1 items-center justify-center gap-2 rounded-lg bg-[#A7F950] text-base font-semibold text-black shadow-lg transition-all duration-200 hover:shadow-xl'
+ >
Upvote
@@ -159,7 +166,12 @@ export function ProjectSidebar({
Fund
*/}
-
+ {project.status === 'Funding' && (
+
+
+ Fund
+
+ )}
{
+ return projects.map(project => {
+ const cardData = transformProjectForCard(project);
+ return ;
+ });
+ }, [projects, transformProjectForCard]);
+
+ return (
+ <>
+
+
+
+ {/* Results Summary */}
+ {!loading && !error && (
+
+
+ {projects.length > 0 ? (
+
+ Showing {projects.length} project
+ {projects.length !== 1 ? 's' : ''}
+ {filters.search && ` for "${filters.search}"`}
+ {filters.category && ` in ${filters.category}`}
+ {filters.status && ` with status ${filters.status}`}
+
+ ) : (
+ No projects found
+ )}
+
+ {filters.search && (
+
+ Clear search
+
+ )}
+
+ )}
+
+ {/* Loading State */}
+ {loading && (
+
+
+
Loading projects...
+
+ )}
+
+ {/* Error State */}
+ {error && (
+
+
+
⚠️
+
+ Something went wrong
+
+
{error}
+
+ Try Again
+
+
+
+ )}
+
+ {/* Empty State */}
+ {!loading && !error && projects.length === 0 && (
+
+
+
🔍
+
+ No projects found
+
+
+ {filters.search || filters.category || filters.status
+ ? 'Try adjusting your filters or search terms'
+ : 'No projects are available at the moment'}
+
+ {(filters.search || filters.category || filters.status) && (
+
+ Clear all filters
+
+ )}
+
+
+ )}
+
+ {/* Projects Grid */}
+ {!loading && !error && projects.length > 0 && (
+ <>
+
+ {projectCards}
+
+
+ {/* Load More Button */}
+ {hasMore && (
+
+
+ {loadingMore && (
+
+ )}
+ {loadingMore ? 'Loading...' : 'Load More Projects'}
+
+
+ )}
+ >
+ )}
+
+ >
+ );
+}
diff --git a/components/projects/ExploreHeader.tsx b/components/projects/ExploreHeader.tsx
index 091472943..94fe2e510 100644
--- a/components/projects/ExploreHeader.tsx
+++ b/components/projects/ExploreHeader.tsx
@@ -87,7 +87,10 @@ const ExploreHeader = ({
-
+
Explore Boundless Projects
diff --git a/hooks/project/use-project-filters.ts b/hooks/project/use-project-filters.ts
new file mode 100644
index 000000000..69349d6fe
--- /dev/null
+++ b/hooks/project/use-project-filters.ts
@@ -0,0 +1,87 @@
+'use client';
+
+import * as React from 'react';
+import { useDebounce } from '../use-debounce';
+
+type SortOption =
+ | 'newest'
+ | 'oldest'
+ | 'funding_goal_high'
+ | 'funding_goal_low'
+ | 'deadline_soon'
+ | 'deadline_far';
+
+interface ProjectFilters {
+ category?: string;
+ status?: string;
+ search?: string;
+ sort?: SortOption;
+}
+
+interface UseProjectFiltersReturn {
+ filters: ProjectFilters;
+ searchTerm: string;
+ debouncedSearchTerm: string;
+ handleSearch: (searchTerm: string) => void;
+ handleSort: (sortType: string) => void;
+ handleStatus: (status: string) => void;
+ handleCategory: (category: string) => void;
+ clearSearch: () => void;
+ clearAllFilters: () => void;
+}
+
+export function useProjectFilters(
+ initialFilters: ProjectFilters = {}
+): UseProjectFiltersReturn {
+ const [filters, setFilters] = React.useState
(initialFilters);
+ const [searchTerm, setSearchTerm] = React.useState('');
+ const debouncedSearchTerm = useDebounce(searchTerm, 500);
+
+ // Update filters when debounced search term changes
+ React.useEffect(() => {
+ setFilters(prev => ({ ...prev, search: debouncedSearchTerm }));
+ }, [debouncedSearchTerm]);
+
+ const handleSearch = React.useCallback((searchTerm: string) => {
+ setSearchTerm(searchTerm);
+ }, []);
+
+ const handleSort = React.useCallback((sortType: string) => {
+ setFilters(prev => ({ ...prev, sort: sortType as SortOption }));
+ }, []);
+
+ const handleStatus = React.useCallback((status: string) => {
+ setFilters(prev => ({
+ ...prev,
+ status: status === 'all' ? undefined : status,
+ }));
+ }, []);
+
+ const handleCategory = React.useCallback((category: string) => {
+ setFilters(prev => ({
+ ...prev,
+ category: category === 'all' ? undefined : category,
+ }));
+ }, []);
+
+ const clearSearch = React.useCallback(() => {
+ setSearchTerm('');
+ }, []);
+
+ const clearAllFilters = React.useCallback(() => {
+ setSearchTerm('');
+ setFilters({});
+ }, []);
+
+ return {
+ filters,
+ searchTerm,
+ debouncedSearchTerm,
+ handleSearch,
+ handleSort,
+ handleStatus,
+ handleCategory,
+ clearSearch,
+ clearAllFilters,
+ };
+}
diff --git a/hooks/project/use-project-transform.ts b/hooks/project/use-project-transform.ts
new file mode 100644
index 000000000..4b5d18902
--- /dev/null
+++ b/hooks/project/use-project-transform.ts
@@ -0,0 +1,98 @@
+'use client';
+
+import * as React from 'react';
+import type { CrowdfundingProject } from '@/lib/api/types';
+
+interface TransformedProject {
+ projectId: string;
+ creatorName: string;
+ creatorLogo: string;
+ projectImage: string;
+ projectTitle: string;
+ projectDescription: string;
+ status: 'Validation' | 'Funding' | 'Funded' | 'Completed';
+ deadlineInDays: number;
+ funding: {
+ current: number;
+ goal: number;
+ currency: string;
+ };
+ votes?: {
+ current: number;
+ goal: number;
+ };
+}
+
+export function useProjectTransform() {
+ const transformProjectForCard = React.useCallback(
+ (project: CrowdfundingProject): TransformedProject => {
+ let deadlineInDays: number | null = null;
+
+ try {
+ if (project.status === 'idea') {
+ const now = new Date();
+ const end = new Date(project.voting.endDate);
+ deadlineInDays = Math.ceil(
+ (end.getTime() - now.getTime()) / (1000 * 60 * 60 * 24)
+ );
+ } else if (
+ project.status === 'active' ||
+ project.status === 'completed'
+ ) {
+ const now = new Date();
+ const end = new Date(project.funding.endDate);
+ deadlineInDays = Math.ceil(
+ (end.getTime() - now.getTime()) / (1000 * 60 * 60 * 24)
+ );
+ }
+ } catch (error) {
+ console.warn(
+ 'Error calculating deadline for project:',
+ project._id,
+ error
+ );
+ deadlineInDays = null;
+ }
+
+ // Map project status to card status
+ let cardStatus: 'Validation' | 'Funding' | 'Funded' | 'Completed' =
+ 'Funding';
+ if (project.status === 'draft' || project.status === 'idea') {
+ cardStatus = 'Validation';
+ } else if (project.status === 'active') {
+ cardStatus = 'Funding';
+ } else if (project.status === 'completed') {
+ cardStatus = 'Completed';
+ }
+
+ return {
+ projectId: project._id,
+ creatorName: `${project.creator.profile.firstName} ${project.creator.profile.lastName}`,
+ creatorLogo: '/avatar.png',
+ projectImage:
+ project.media?.logo ||
+ project.media?.logo ||
+ '/landing/explore/project-placeholder-1.png',
+ projectTitle: project.title,
+ projectDescription: project.vision || project.description,
+ status: cardStatus,
+ deadlineInDays: deadlineInDays || 0,
+ funding: {
+ current: project.funding?.raised || 0,
+ goal: project.funding?.goal || 0,
+ currency: project.funding?.currency || 'USDC',
+ },
+ votes:
+ project.status === 'idea'
+ ? {
+ current: project.votes || 0,
+ goal: project.voting?.totalVotes || 100,
+ }
+ : undefined,
+ };
+ },
+ []
+ );
+
+ return { transformProjectForCard };
+}
diff --git a/hooks/project/use-project.ts b/hooks/project/use-project.ts
new file mode 100644
index 000000000..53544f63c
--- /dev/null
+++ b/hooks/project/use-project.ts
@@ -0,0 +1,196 @@
+'use client';
+
+import * as React from 'react';
+import { getCrowdfundingProjects } from '@/lib/api/project';
+import type { CrowdfundingProject } from '@/lib/api/types';
+
+type SortOption =
+ | 'newest'
+ | 'oldest'
+ | 'funding_goal_high'
+ | 'funding_goal_low'
+ | 'deadline_soon'
+ | 'deadline_far';
+
+interface ProjectFilters {
+ category?: string;
+ status?: string;
+ search?: string;
+ sort?: SortOption;
+}
+
+interface UseProjectsOptions {
+ initialPage?: number;
+ pageSize?: number;
+ initialFilters?: ProjectFilters;
+}
+
+interface UseProjectsReturn {
+ projects: CrowdfundingProject[];
+ loading: boolean;
+ loadingMore: boolean;
+ error: string | null;
+ hasMore: boolean;
+ currentPage: number;
+ filters: ProjectFilters;
+ setFilters: React.Dispatch>;
+ loadMore: () => void;
+ refetch: () => void;
+}
+
+export function useProjects(
+ options: UseProjectsOptions = {}
+): UseProjectsReturn {
+ const { initialPage = 1, pageSize = 9, initialFilters = {} } = options;
+
+ const [projects, setProjects] = React.useState([]);
+ const [loading, setLoading] = React.useState(true);
+ const [loadingMore, setLoadingMore] = React.useState(false);
+ const [error, setError] = React.useState(null);
+ const [currentPage, setCurrentPage] = React.useState(initialPage);
+ const [hasMore, setHasMore] = React.useState(true);
+ const [filters, setFilters] = React.useState(initialFilters);
+
+ // Get project deadline in milliseconds
+ const getProjectDeadline = React.useCallback(
+ (project: CrowdfundingProject): number => {
+ if (project.status === 'idea') {
+ return new Date(project.voting.endDate).getTime();
+ } else if (
+ project.status === 'active' ||
+ project.status === 'completed'
+ ) {
+ return new Date(project.funding.endDate).getTime();
+ }
+ return 0;
+ },
+ []
+ );
+
+ // Sort projects based on sort option
+ const sortProjects = React.useCallback(
+ (
+ projects: CrowdfundingProject[],
+ sortOption: SortOption
+ ): CrowdfundingProject[] => {
+ const sorted = [...projects];
+
+ switch (sortOption) {
+ case 'newest':
+ return sorted.sort(
+ (a, b) =>
+ new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()
+ );
+ case 'oldest':
+ return sorted.sort(
+ (a, b) =>
+ new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime()
+ );
+ case 'funding_goal_high':
+ return sorted.sort((a, b) => b.funding.goal - a.funding.goal);
+ case 'funding_goal_low':
+ return sorted.sort((a, b) => a.funding.goal - b.funding.goal);
+ case 'deadline_soon':
+ return sorted.sort((a, b) => {
+ const aDeadline = getProjectDeadline(a);
+ const bDeadline = getProjectDeadline(b);
+ return aDeadline - bDeadline;
+ });
+ case 'deadline_far':
+ return sorted.sort((a, b) => {
+ const aDeadline = getProjectDeadline(a);
+ const bDeadline = getProjectDeadline(b);
+ return bDeadline - aDeadline;
+ });
+ default:
+ return sorted;
+ }
+ },
+ [getProjectDeadline]
+ );
+
+ const fetchProjects = React.useCallback(
+ async (page: number, currentFilters: ProjectFilters, append = false) => {
+ try {
+ if (append) {
+ setLoadingMore(true);
+ } else {
+ setLoading(true);
+ }
+ setError(null);
+
+ const response = await getCrowdfundingProjects(page, pageSize, {
+ category: currentFilters.category,
+ status: currentFilters.status,
+ });
+
+ let fetchedProjects = response.data.projects;
+
+ // Apply client-side search filtering
+ if (currentFilters.search) {
+ const searchLower = currentFilters.search.toLowerCase();
+ fetchedProjects = fetchedProjects.filter(
+ project =>
+ project.title.toLowerCase().includes(searchLower) ||
+ project.vision.toLowerCase().includes(searchLower) ||
+ project.description.toLowerCase().includes(searchLower) ||
+ project.category.toLowerCase().includes(searchLower) ||
+ `${project.creator.profile.firstName} ${project.creator.profile.lastName}`
+ .toLowerCase()
+ .includes(searchLower)
+ );
+ }
+
+ // Apply client-side sorting
+ if (currentFilters.sort) {
+ fetchedProjects = sortProjects(fetchedProjects, currentFilters.sort);
+ }
+
+ if (append) {
+ setProjects(prev => [...prev, ...fetchedProjects]);
+ } else {
+ setProjects(fetchedProjects);
+ }
+
+ setHasMore(response.data.pagination.pages > page);
+ } catch (err) {
+ console.error('Error fetching projects:', err);
+ setError('Failed to fetch projects. Please try again.');
+ } finally {
+ setLoading(false);
+ setLoadingMore(false);
+ }
+ },
+ [pageSize, sortProjects]
+ );
+
+ // Fetch projects when filters change
+ React.useEffect(() => {
+ setCurrentPage(1);
+ setHasMore(true);
+ fetchProjects(1, filters);
+ }, [filters, fetchProjects]);
+
+ const loadMore = React.useCallback(() => {
+ const nextPage = currentPage + 1;
+ setCurrentPage(nextPage);
+ fetchProjects(nextPage, filters, true);
+ }, [currentPage, filters, fetchProjects]);
+
+ const refetch = React.useCallback(() => {
+ fetchProjects(currentPage, filters);
+ }, [currentPage, filters, fetchProjects]);
+
+ return {
+ projects,
+ loading,
+ loadingMore,
+ error,
+ hasMore,
+ currentPage,
+ filters,
+ setFilters,
+ loadMore,
+ refetch,
+ };
+}
diff --git a/lib/api/project.ts b/lib/api/project.ts
index 11352cdb2..37316a8af 100644
--- a/lib/api/project.ts
+++ b/lib/api/project.ts
@@ -227,3 +227,10 @@ export const confirmProjectFunding = async (
);
return res.data;
};
+export const voteProject = async (
+ projectId: string,
+ value: 1 | -1 = 1
+): Promise => {
+ const res = await api.post(`/projects/${projectId}/vote`, { value });
+ return res.data;
+};
diff --git a/lib/mock.ts b/lib/mock.ts
index 227822c39..d546a209d 100644
--- a/lib/mock.ts
+++ b/lib/mock.ts
@@ -28,7 +28,7 @@ export const mockProjects: Project[] = [
category: 'defi',
type: 'crowdfunding',
amount: 85000,
- status: 'approved',
+ status: 'funding',
createdAt: '2024-01-10T09:15:00Z',
updatedAt: '2024-01-18T16:45:00Z',
},