diff --git a/app/api/freelancers/route.ts b/app/api/freelancers/route.ts new file mode 100644 index 0000000..0c3c58c --- /dev/null +++ b/app/api/freelancers/route.ts @@ -0,0 +1,62 @@ +import { NextRequest, NextResponse } from 'next/server' +import { FREELANCERS, FREELANCER_SKILLS } from '@/lib/freelancers' + +export const dynamic = 'force-dynamic' + +const PAGE_SIZE = 6 + +function parseRating(value: string | null): number | null { + if (!value) return null + const rating = Number.parseInt(value, 10) + return Number.isInteger(rating) && rating >= 1 && rating <= 5 ? rating : null +} + +function parsePage(value: string | null): number { + if (!value) return 1 + const page = Number.parseInt(value, 10) + return Number.isInteger(page) && page > 0 ? page : 1 +} + +export async function GET(request: NextRequest) { + const { searchParams } = new URL(request.url) + const query = searchParams.get('q')?.trim().toLowerCase() ?? '' + const selectedSkills = searchParams + .getAll('skills') + .flatMap((value) => value.split(',')) + .map((skill) => skill.trim()) + .filter(Boolean) + const minimumRating = parseRating(searchParams.get('rating')) + const page = parsePage(searchParams.get('page')) + + const filtered = FREELANCERS.filter((freelancer) => { + const matchesQuery = + !query || + [freelancer.name, freelancer.title, freelancer.bio, ...freelancer.skills] + .join(' ') + .toLowerCase() + .includes(query) + + const matchesSkills = + selectedSkills.length === 0 || + selectedSkills.every((skill) => freelancer.skills.includes(skill)) + + const matchesRating = !minimumRating || freelancer.rating >= minimumRating + + return matchesQuery && matchesSkills && matchesRating + }) + + const totalPages = Math.max(1, Math.ceil(filtered.length / PAGE_SIZE)) + const currentPage = Math.min(page, totalPages) + const start = (currentPage - 1) * PAGE_SIZE + + return NextResponse.json({ + freelancers: filtered.slice(start, start + PAGE_SIZE), + skills: FREELANCER_SKILLS, + pagination: { + page: currentPage, + pageSize: PAGE_SIZE, + totalItems: filtered.length, + totalPages, + }, + }) +} diff --git a/app/freelancers/page.tsx b/app/freelancers/page.tsx new file mode 100644 index 0000000..7ffd768 --- /dev/null +++ b/app/freelancers/page.tsx @@ -0,0 +1,289 @@ +'use client' + +import Image from 'next/image' +import { useCallback, useEffect, useMemo, useState } from 'react' +import { Search, SlidersHorizontal, Star, Users } from 'lucide-react' +import { Navbar } from '@/components/navbar' +import { Footer } from '@/components/footer' +import { Badge } from '@/components/ui/badge' +import { Button } from '@/components/ui/button' +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card' +import { Checkbox } from '@/components/ui/checkbox' +import { Input } from '@/components/ui/input' +import type { FreelancerListing } from '@/lib/freelancers' + +interface FreelancerResponse { + freelancers: FreelancerListing[] + skills: string[] + pagination: { + page: number + pageSize: number + totalItems: number + totalPages: number + } +} + +const DEFAULT_RESPONSE: FreelancerResponse = { + freelancers: [], + skills: [], + pagination: { page: 1, pageSize: 6, totalItems: 0, totalPages: 1 }, +} + +function RatingStars({ rating }: { rating: number }) { + return ( +
+ {Array.from({ length: 5 }, (_, index) => ( + + ))} + {rating}.0 +
+ ) +} + +function LoadingCards() { + return ( +
+ {Array.from({ length: 6 }, (_, index) => ( + + +
+
+
+
+
+
+
+ + +
+
+
+
+
+ + + ))} +
+ ) +} + +export default function FreelancersPage() { + const [search, setSearch] = useState('') + const [selectedSkills, setSelectedSkills] = useState([]) + const [minimumRating, setMinimumRating] = useState(0) + const [page, setPage] = useState(1) + const [data, setData] = useState(DEFAULT_RESPONSE) + const [loading, setLoading] = useState(true) + const [error, setError] = useState(null) + + const queryString = useMemo(() => { + const params = new URLSearchParams() + if (search.trim()) params.set('q', search.trim()) + selectedSkills.forEach((skill) => params.append('skills', skill)) + if (minimumRating > 0) params.set('rating', String(minimumRating)) + params.set('page', String(page)) + return params.toString() + }, [minimumRating, page, search, selectedSkills]) + + const loadFreelancers = useCallback(async () => { + setLoading(true) + try { + const response = await fetch(`/api/freelancers?${queryString}`, { cache: 'no-store' }) + if (!response.ok) throw new Error('Freelancer search failed') + const payload = (await response.json()) as FreelancerResponse + setData(payload) + setError(null) + } catch { + setError('Unable to load freelancers. Please try again.') + setData(DEFAULT_RESPONSE) + } finally { + setLoading(false) + } + }, [queryString]) + + useEffect(() => { + const timeout = window.setTimeout(() => { + void loadFreelancers() + }, 250) + + return () => window.clearTimeout(timeout) + }, [loadFreelancers]) + + function toggleSkill(skill: string) { + setPage(1) + setSelectedSkills((current) => + current.includes(skill) ? current.filter((item) => item !== skill) : [...current, skill] + ) + } + + function clearFilters() { + setSearch('') + setSelectedSkills([]) + setMinimumRating(0) + setPage(1) + } + + const hasActiveFilters = search.trim() || selectedSkills.length > 0 || minimumRating > 0 + + return ( +
+ +
+
+
+ + Freelancer marketplace + +

+ Find trusted freelancers for your next TaskChain project +

+

+ Search by name or keyword, combine skill and rating filters, and review concise profiles before starting work. +

+
+
+
+ +
+ + +
+
+
+

Showing

+

+ {data.pagination.totalItems} freelancer{data.pagination.totalItems === 1 ? '' : 's'} found +

+
+

+ Page {data.pagination.page} of {data.pagination.totalPages} +

+
+ + {error &&
{error}
} + + {loading ? ( + + ) : data.freelancers.length === 0 ? ( +
+ +

No freelancers match your filters

+

Try removing a skill, lowering the rating, or searching a different keyword.

+ +
+ ) : ( +
+ {data.freelancers.map((freelancer) => ( + + +
+ +
+ {freelancer.name} +

{freelancer.title}

+
+
+
+ + +

{freelancer.bio}

+
+ {freelancer.skills.map((skill) => ( + {skill} + ))} +
+
+
+

Projects

+

{freelancer.completedProjects}

+
+
+

Rate

+

${freelancer.hourlyRate}/hr

+
+
+ +
+
+ ))} +
+ )} + +
+ + {data.pagination.totalItems} total results + +
+
+
+
+
+ ) +} diff --git a/components/navbar.tsx b/components/navbar.tsx index 1767b39..a9ddc9c 100644 --- a/components/navbar.tsx +++ b/components/navbar.tsx @@ -81,6 +81,9 @@ export function Navbar() { {/* Desktop Navigation Links */}
+ + Browse Freelancers + Features @@ -213,6 +216,9 @@ export function Navbar() { {mobileMenuOpen && (
+ + Browse Freelancers + Features diff --git a/lib/freelancers.ts b/lib/freelancers.ts new file mode 100644 index 0000000..4338864 --- /dev/null +++ b/lib/freelancers.ts @@ -0,0 +1,136 @@ +export interface FreelancerListing { + id: number + name: string + title: string + skills: string[] + rating: number + bio: string + profileImage: string + completedProjects: number + hourlyRate: number + location: string +} + +export const FREELANCER_SKILLS = [ + 'React', + 'Next.js', + 'TypeScript', + 'Node.js', + 'Stellar', + 'Smart Contracts', + 'UI/UX Design', + 'PostgreSQL', + 'Tailwind CSS', + 'Web3', +] + +export const FREELANCERS: FreelancerListing[] = [ + { + id: 1, + name: 'Maya Chen', + title: 'Senior Web3 Frontend Engineer', + skills: ['React', 'Next.js', 'TypeScript', 'Tailwind CSS'], + rating: 5, + bio: 'Builds polished dApps, escrow dashboards, and conversion-focused marketplace flows for early-stage teams.', + profileImage: '/placeholder-user.jpg', + completedProjects: 48, + hourlyRate: 95, + location: 'San Francisco, CA', + }, + { + id: 2, + name: 'Andre Okafor', + title: 'Stellar Smart Contract Developer', + skills: ['Stellar', 'Smart Contracts', 'Web3', 'Node.js'], + rating: 5, + bio: 'Specializes in Soroban contracts, payment rails, and secure milestone release logic for freelance products.', + profileImage: '/placeholder-user.jpg', + completedProjects: 36, + hourlyRate: 110, + location: 'Austin, TX', + }, + { + id: 3, + name: 'Priya Raman', + title: 'Full-Stack Product Engineer', + skills: ['Next.js', 'PostgreSQL', 'Node.js', 'TypeScript'], + rating: 4, + bio: 'Ships reliable SaaS backends, API integrations, and responsive user experiences with strong delivery habits.', + profileImage: '/placeholder-user.jpg', + completedProjects: 62, + hourlyRate: 88, + location: 'New York, NY', + }, + { + id: 4, + name: 'Leo Martinez', + title: 'Marketplace UI/UX Designer', + skills: ['UI/UX Design', 'React', 'Tailwind CSS'], + rating: 4, + bio: 'Designs accessible marketplace journeys, freelancer profiles, and dashboard systems that match product goals.', + profileImage: '/placeholder-user.jpg', + completedProjects: 41, + hourlyRate: 76, + location: 'Denver, CO', + }, + { + id: 5, + name: 'Nora Jensen', + title: 'Backend API Specialist', + skills: ['Node.js', 'PostgreSQL', 'TypeScript'], + rating: 3, + bio: 'Creates dependable API layers, pagination strategies, search endpoints, and data models for growing platforms.', + profileImage: '/placeholder-user.jpg', + completedProjects: 27, + hourlyRate: 70, + location: 'Seattle, WA', + }, + { + id: 6, + name: 'Samir Patel', + title: 'Web3 Integration Consultant', + skills: ['Web3', 'Stellar', 'React', 'Smart Contracts'], + rating: 5, + bio: 'Connects wallets, reputation services, and escrow workflows while keeping onboarding simple for clients.', + profileImage: '/placeholder-user.jpg', + completedProjects: 53, + hourlyRate: 125, + location: 'Chicago, IL', + }, + { + id: 7, + name: 'Elena Petrova', + title: 'Responsive Frontend Developer', + skills: ['React', 'Tailwind CSS', 'Next.js'], + rating: 4, + bio: 'Transforms product requirements into fast, mobile-first interfaces with clean component architecture.', + profileImage: '/placeholder-user.jpg', + completedProjects: 34, + hourlyRate: 82, + location: 'Boston, MA', + }, + { + id: 8, + name: 'Marcus Green', + title: 'Database & Analytics Engineer', + skills: ['PostgreSQL', 'Node.js', 'TypeScript'], + rating: 2, + bio: 'Improves reporting pipelines, search performance, and operational dashboards for data-heavy marketplaces.', + profileImage: '/placeholder-user.jpg', + completedProjects: 19, + hourlyRate: 64, + location: 'Atlanta, GA', + }, + { + id: 9, + name: 'Aisha Bello', + title: 'Product-Focused Full-Stack Developer', + skills: ['Next.js', 'UI/UX Design', 'PostgreSQL', 'React'], + rating: 5, + bio: 'Combines engineering and product thinking to launch secure freelancer-client workflows quickly.', + profileImage: '/placeholder-user.jpg', + completedProjects: 57, + hourlyRate: 102, + location: 'Miami, FL', + }, +]