diff --git a/apps/web/app/gig-list/page.tsx b/apps/web/app/gig-list/page.tsx index 3b75b78..5bd419a 100644 --- a/apps/web/app/gig-list/page.tsx +++ b/apps/web/app/gig-list/page.tsx @@ -2,14 +2,16 @@ import { useState, useEffect, useCallback } from "react"; import Link from "next/link"; import Image from "next/image"; -import { Check, ArrowRight, Calendar } from "lucide-react"; +import { Check, ArrowRight, Calendar, Search, Zap, X, Filter, ChevronDown } from "lucide-react"; +import { motion, AnimatePresence } from "framer-motion"; + import api from "@/lib/axios.client"; import Navbar from "@/components/Navbar"; import Footer from "@/components/Footer"; // ─── Types ─────────────────────────────────────────────────────────────────── -type ViewMode = "grid" | "list"; + type SortOption = "recommended" | "price_asc" | "price_desc" | "next_available"; interface GigPricing { @@ -72,15 +74,7 @@ const sortOptions: { label: string; value: SortOption }[] = [ { label: "Next Available", value: "next_available" }, ]; -// Deterministic avatar color based on id string -const avatarColor = (id: string) => { - const colors = [ - "#e8736c", "#5b8dee", "#4db89e", "#f0a500", "#b57bee", "#e06060", - ]; - let hash = 0; - for (let i = 0; i < id.length; i++) hash = id.charCodeAt(i) + ((hash << 5) - hash); - return colors[Math.abs(hash) % colors.length]; -}; + const initials = (name: string) => { if (!name) return ""; @@ -133,12 +127,11 @@ function SkeletonCard() { // ─── Gig Card ───────────────────────────────────────────────────────────────── -function GigCard({ gig, view }: { gig: Gig; view: ViewMode }) { +function GigCard({ gig }: { gig: Gig }) { const influencer = gig.primaryInfluencerId; const name = influencer?.fullName ?? "Unknown Creator"; const niche = gig.category; const availableFrom = undefined; - const color = avatarColor(gig._id); const availableLabel = availableFrom ? new Date(availableFrom).toLocaleDateString("en-US", { month: "short", day: "numeric" }) @@ -156,61 +149,6 @@ function GigCard({ gig, view }: { gig: Gig; view: ViewMode }) { const gigImage = gig.bannerUrl || categoryImages[gig.category] || categoryImages["default"]; - if (view === "list") { - return ( - -
- {(influencer?.profileImageUrl || influencer?.profileImage) ? ( - {name} { - const target = e.target as HTMLImageElement; - target.style.display = 'none'; - const sibling = target.nextElementSibling as HTMLElement; - if (sibling) sibling.style.display = 'block'; - }} - /> - ) : null} - {initials(name)} -
- -
-
- {name} -
- -
-
-

{gig.title}

-

{gig.shortDescription}

-
- -
-

Starting at

-

- {formatCurrency(gig.pricing.basePrice, gig.pricing.currency)} -

-
- -
-
- - {availableLabel} -
-
- -
-
- - ); - } return (
- {gig.title} { const target = e.target as HTMLImageElement; const fallback = categoryImages[gig.category] || categoryImages["default"]; @@ -241,24 +179,24 @@ function GigCard({ gig, view }: { gig: Gig; view: ViewMode }) {
- {(influencer?.profileImageUrl || influencer?.profileImage) ? ( - <> - {name} { - const target = e.target as HTMLImageElement; - target.style.display = 'none'; - const fallback = target.nextElementSibling as HTMLElement; - if (fallback) fallback.style.display = 'flex'; - }} - /> - {initials(name)} - - ) : initials(name)} + {(influencer?.profileImageUrl || influencer?.profileImage) ? ( + <> + {name} { + const target = e.target as HTMLImageElement; + target.style.display = 'none'; + const fallback = target.nextElementSibling as HTMLElement; + if (fallback) fallback.style.display = 'flex'; + }} + /> + {initials(name)} + + ) : initials(name)}
@@ -307,7 +245,6 @@ export default function ExploreGigs() { const [activeCategory, setActiveCategory] = useState(null); const [activePlatform, setActivePlatform] = useState(null); const [availableOnly, setAvailableOnly] = useState(false); - const [view, setView] = useState("grid"); const [sort, setSort] = useState("recommended"); const [maxPrice, setMaxPrice] = useState(MAX_PRICE_LIMIT); const [page, setPage] = useState(1); @@ -322,6 +259,7 @@ export default function ExploreGigs() { // the user finishes typing (blur / Enter) or stops dragging the slider. const [committedSearch, setCommittedSearch] = useState(""); const [committedMaxPrice, setCommittedMaxPrice] = useState(MAX_PRICE_LIMIT); + const [isFilterOpen, setIsFilterOpen] = useState(false); const fetchGigs = useCallback(async ( opts: { @@ -398,6 +336,7 @@ export default function ExploreGigs() { setPage(1); } }; + const handleSearchBlur = () => { if (search !== committedSearch) { setCommittedSearch(search); @@ -405,6 +344,11 @@ export default function ExploreGigs() { } }; + const handleSearchClick = () => { + setCommittedSearch(search); + setPage(1); + }; + const totalPages = pagination?.totalPages ?? 1; const pageNumbers = (): (number | "…")[] => { @@ -435,145 +379,244 @@ export default function ExploreGigs() { The world's most elite creators, verified and ready for your next campaign.

-
- {/* Grid/List Toggle */} -
- - -
-
- {/* Modern Filter System - Sticky Glassmorphism Header */} -
- {/* Category & Platform Pills */} -
-
- Niche: - - {categories.map((c) => ( - - ))} -
- -
- Platform: - {platformList.map((p) => ( - - ))} -
-
- - {/* Utility Bar */} -
-
-
- - - + {/* Premium Minimal Filter System */} +
+
+ {/* Unified Search Bar */} +
+
+
setSearch(e.target.value)} onKeyDown={handleSearchKeyDown} onBlur={handleSearchBlur} - className="w-full pl-14 pr-6 py-4 bg-white border border-slate-200 rounded-[2rem] text-sm font-medium focus:outline-none focus:ring-4 focus:ring-emerald-500/10 focus:border-emerald-500 transition-all shadow-sm" + className="w-full pl-10 md:pl-14 pr-24 md:pr-32 py-3.5 md:py-5 bg-white/70 backdrop-blur-xl border border-white shadow-[0_8px_30px_rgb(0,0,0,0.04)] rounded-2xl md:rounded-3xl text-xs md:text-sm font-semibold focus:outline-none focus:ring-4 focus:ring-emerald-500/5 focus:border-emerald-500/20 transition-all placeholder:text-slate-400" /> +
+ + {search && ( + { setSearch(""); setCommittedSearch(""); setPage(1); }} + className="w-6 h-6 md:w-8 md:h-8 flex items-center justify-center rounded-full hover:bg-slate-100 text-slate-400 transition-colors" + > + + + )} + + +
-
-
- Budget: - - - {maxPrice >= MAX_PRICE_LIMIT ? "50k+" : `${(maxPrice / 1000).toFixed(0)}k`} - -
+ {/* Filter Toggle Button */} + +
- - - -
-
+
+ {/* Column 1: Categories */} +
+
+
+ Niche & Categories +
+
+ + {categories.map((c) => ( + + ))} +
+
+ + {/* Column 2: Platform & Status */} +
+
+
+
+ Platform Preference +
+
+ + {platformList.map((p) => ( + + ))} +
+
+ +
+
+
+ Availability +
+ +
+
+ + {/* Column 3: Budget & Sorting */} +
+
+
+
+
+ Max Budget +
+ + {maxPrice >= MAX_PRICE_LIMIT ? "Any" : `₹${(maxPrice / 1000).toFixed(0)}k`} + +
+ +
+ +
+
+
+ Sort Results By +
+
+ + +
+
+
+
+ + {/* Footer of Dropdown */} +
+

+ Adjust filters to refine your search results +

+ +
+ + )} +
+ + {/* Stats Context Bar */} -
-
+
+
{loading ? ( - -
- Synchronizing... + +
+ Updating Catalog... ) : pagination ? ( - <>Found {pagination.total} Results Matches + <>Found {pagination.total} Matching Opportunities ) : null}
- {(activeCategory || activePlatform || maxPrice < MAX_PRICE_LIMIT || availableOnly) && ( - + Reset All Filters + + )}
+ {/* Error state */} {error && (
@@ -608,17 +650,12 @@ export default function ExploreGigs() { )} {/* Cards */} -
+
{loading ? Array.from({ length: LIMIT }).map((_, i) => ) : gigs.map((gig) => ( - - ))} + + ))}
{/* Empty state */} diff --git a/apps/web/app/globals.css b/apps/web/app/globals.css index 67d4808..0917b5c 100644 --- a/apps/web/app/globals.css +++ b/apps/web/app/globals.css @@ -129,6 +129,21 @@ html { @apply font-sans; } + + /* Custom minimal scrollbar */ + ::-webkit-scrollbar { + width: 6px; + } + ::-webkit-scrollbar-track { + background: transparent; + } + ::-webkit-scrollbar-thumb { + background: rgba(0, 0, 0, 0.1); + border-radius: 10px; + } + ::-webkit-scrollbar-thumb:hover { + background: rgba(16, 185, 129, 0.4); + } } @keyframes progress-flow { @@ -140,4 +155,24 @@ .animate-progress-flow { background-size: 200% 200%; animation: progress-flow 3s ease infinite; -} \ No newline at end of file +} +/* Lenis recommended CSS */ +html.lenis, html.lenis body { + height: auto; +} + +.lenis.lenis-smooth { + scroll-behavior: auto !important; +} + +.lenis.lenis-smooth [data-lenis-prevent] { + overscroll-behavior: contain; +} + +.lenis.lenis-stopped { + overflow: hidden; +} + +.lenis.lenis-scrolling iframe { + pointer-events: none; +} diff --git a/apps/web/app/layout.tsx b/apps/web/app/layout.tsx index 03dc085..364d0c2 100644 --- a/apps/web/app/layout.tsx +++ b/apps/web/app/layout.tsx @@ -3,6 +3,7 @@ import { Inter } from "next/font/google"; import "./globals.css"; import AuthInitializer from "@/components/AuthInitializer"; +import SmoothScroll from "@/components/SmoothScroll"; @@ -26,8 +27,10 @@ export default function RootLayout({ - - {children} + + + {children} + ); diff --git a/apps/web/app/page.tsx b/apps/web/app/page.tsx index 02c007b..9eb15c2 100644 --- a/apps/web/app/page.tsx +++ b/apps/web/app/page.tsx @@ -78,13 +78,12 @@ interface Testimonial { function TestimonialSlider({ testimonials }: { testimonials: Testimonial[] }) { const [index, setIndex] = useState(0); const [visibleCards, setVisibleCards] = useState(3); - const ref = useRef(null); - const isInView = useInView(ref, { once: true, amount: 0.1 }); + const sliderRef = useRef(null); // Update visible cards on resize useEffect(() => { const handleResize = () => { - setVisibleCards(window.innerWidth < 768 ? 1 : 3); + setVisibleCards(window.innerWidth < 1024 ? (window.innerWidth < 640 ? 1 : 2) : 3); }; handleResize(); window.addEventListener("resize", handleResize); @@ -105,20 +104,26 @@ function TestimonialSlider({ testimonials }: { testimonials: Testimonial[] }) { return ( { + // If we've reached the end of the middle section, jump back seamlessly + // Note: index calculation for seamless loop can be complex, + // but for simple auto-slide, just keeping it in bounds is enough. + if (index >= testimonials.length * 2) { + setIndex(testimonials.length); + } + }} className="flex" style={{ width: `${(extendedTestimonials.length * 100) / visibleCards}%` }} > @@ -172,7 +177,7 @@ function TestimonialSlider({ testimonials }: { testimonials: Testimonial[] }) { export default function HomePage() { return ( -
+
@@ -293,64 +298,99 @@ export default function HomePage() {
- {/* How it Works */} -
+ {/* How it Works - Redesigned for Premium Minimal Look */} +
+ {/* Subtle Background Pattern */} +
+
-
- Simple Workflow -

The Path to Success

-

- Experience a streamlined collaboration process designed for elite speed. +

+
+ +
+ Execution Strategy + +

+ The Path to
+ Perfect Synergy +

+
+

+ We've distilled months of collaboration overhead into three high-velocity steps.

-
+
+ {/* Connecting Line for Desktop */} +
+ {[ { - title: "Discover Talent", - desc: "Browse a curated list of verified creators across every niche imaginable.", + title: "Discovery", + desc: "Our AI-driven engine filters through thousands to find your perfect niche matches.", icon: , - color: "bg-blue-500" + gradient: "from-blue-600 to-indigo-600" }, { - title: "Instant Booking", - desc: "Check real-time availability and lock in your campaign with one click.", - icon: , - color: "bg-emerald-500" + title: "Activation", + desc: "One-click contracts and instant calendar syncing to launch your campaign at light speed.", + icon: , + gradient: "from-emerald-500 to-teal-500" }, { - title: "Secure Delivery", - desc: "Funds are released only when you're 100% satisfied with the results.", + title: "Growth", + desc: "Funds released on satisfaction, backed by real-time analytics and escrow protection.", icon: , - color: "bg-indigo-500" + gradient: "from-slate-800 to-slate-900" } ].map((step, i) => ( -
0{i + 1}
-
- {step.icon} +
+
+
+ {step.icon} +
+
+ 0{i + 1} +
+
+ +

+ {step.title} +

+

+ {step.desc} +

+ +
-

{step.title}

-

{step.desc}

))}
- {/* Platform Features */} -
-
+ {/* Platform Features - Updated for Color Theme */} +
+
+
+ +
- Cutting Edge -

The Noillin Edge

+ System Architecture +

The Noillin Edge

@@ -368,13 +408,13 @@ export default function HomePage() { whileInView={{ opacity: 1, y: 0 }} viewport={{ once: true }} transition={{ delay: i * 0.05 }} - className="p-8 bg-white border border-slate-200/60 rounded-[2.5rem] hover:border-emerald-500/30 hover:shadow-[0_20px_50px_-20px_rgba(16,185,129,0.15)] transition-all duration-300 group" + className="p-10 bg-white/5 backdrop-blur-sm border border-white/10 rounded-[2.5rem] hover:bg-white/10 hover:border-emerald-500/50 transition-all duration-300 group" > -
+
{feature.icon}
-

{feature.title}

-

{feature.desc}

+

{feature.title}

+

{feature.desc}

))}
@@ -436,12 +476,12 @@ export default function HomePage() {
- {/* Testimonials */} -
+ {/* Testimonials - Updated for Color Theme */} +
- Success Stories -

The Community Voice

+ Validation +

The Community Voice

@@ -144,38 +153,51 @@ export default function Navbar() { initial={{ opacity: 0, y: 10, scale: 0.95 }} animate={{ opacity: 1, y: 0, scale: 1 }} exit={{ opacity: 0, y: 10, scale: 0.95 }} - className="absolute right-0 mt-4 w-64 bg-white rounded-3xl shadow-[0_20px_50px_-10px_rgba(0,0,0,0.15)] border border-slate-100 py-3 z-50 origin-top-right" + className="absolute right-0 mt-4 w-64 bg-white rounded-[2rem] shadow-[0_20px_50px_-10px_rgba(0,0,0,0.2)] border border-slate-100 py-3 z-[100] origin-top-right overflow-hidden" > -
-

Signed in as

+
+

Account

{user.email}

+

+ {user.role} Account +

- setIsProfileOpen(false)} - > - - Dashboard - - - setIsProfileOpen(false)} - > - - Settings - - - +
+ setIsProfileOpen(false)} + > +
+ +
+ Dashboard + + + setIsProfileOpen(false)} + > +
+ +
+ Settings + + +
+ + +
)} @@ -201,16 +223,32 @@ export default function Navbar() { exit={{ height: 0, opacity: 0 }} className="lg:hidden overflow-hidden bg-white border-t border-slate-100 rounded-b-[2rem] px-6" > -
+ {navLinks.map((item) => ( - setIsMenuOpen(false)} - className="text-lg font-bold text-slate-900 hover:text-emerald-600 transition-colors" + - {item.name} - + setIsMenuOpen(false)} + className="text-lg font-bold text-slate-900 hover:text-emerald-600 transition-colors" + > + {item.name} + + ))}
{!user ? ( @@ -238,10 +276,10 @@ export default function Navbar() {
)} -
+ )} - + ); } diff --git a/apps/web/components/SmoothScroll.tsx b/apps/web/components/SmoothScroll.tsx new file mode 100644 index 0000000..52e2e70 --- /dev/null +++ b/apps/web/components/SmoothScroll.tsx @@ -0,0 +1,28 @@ +"use client"; + +import { useEffect, ReactNode } from "react"; +import Lenis from "lenis"; + +export default function SmoothScroll({ children }: { children: ReactNode }) { + useEffect(() => { + const lenis = new Lenis({ + lerp: 0.12, + wheelMultiplier: 1, + touchMultiplier: 1.5, + smoothWheel: true, + }); + + function raf(time: number) { + lenis.raf(time); + requestAnimationFrame(raf); + } + + requestAnimationFrame(raf); + + return () => { + lenis.destroy(); + }; + }, []); + + return <>{children}; +} diff --git a/apps/web/package.json b/apps/web/package.json index bee3540..614b360 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -16,6 +16,7 @@ "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "framer-motion": "^12.38.0", + "lenis": "^1.3.23", "lucide-react": "^0.575.0", "next": "16.1.4", "react": "^19.2.3", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0e40c8f..0ac8f9a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -220,6 +220,9 @@ importers: framer-motion: specifier: ^12.38.0 version: 12.38.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + lenis: + specifier: ^1.3.23 + version: 1.3.23(react@19.2.3) lucide-react: specifier: ^0.575.0 version: 0.575.0(react@19.2.3) @@ -5333,6 +5336,23 @@ packages: } engines: { node: ">=0.10" } + lenis@1.3.23: + resolution: + { + integrity: sha512-YxYq3TJqj9sJNv0V9SkyQHejt14xwyIwgDaaMK89Uf9SxQfIszu+gTQSSphh6BWlLTNVKvvXAGkg+Zf+oFIevg==, + } + peerDependencies: + "@nuxt/kit": ">=3.0.0" + react: ">=17.0.0" + vue: ">=3.0.0" + peerDependenciesMeta: + "@nuxt/kit": + optional: true + react: + optional: true + vue: + optional: true + levn@0.4.1: resolution: { @@ -10355,7 +10375,7 @@ snapshots: "@next/eslint-plugin-next": 16.1.4 eslint: 9.39.2(jiti@2.6.1) eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.53.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1)) + eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.2(jiti@2.6.1)) eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.53.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.2(jiti@2.6.1)) eslint-plugin-jsx-a11y: 6.10.2(eslint@9.39.2(jiti@2.6.1)) eslint-plugin-react: 7.37.5(eslint@9.39.2(jiti@2.6.1)) @@ -10382,7 +10402,7 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.53.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1)): + eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.2(jiti@2.6.1)): dependencies: "@nolyfill/is-core-module": 1.0.39 debug: 4.4.3(supports-color@5.5.0) @@ -10397,14 +10417,14 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-module-utils@2.12.1(@typescript-eslint/parser@8.53.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.53.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1)): + eslint-module-utils@2.12.1(@typescript-eslint/parser@8.53.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.2(jiti@2.6.1)): dependencies: debug: 3.2.7 optionalDependencies: "@typescript-eslint/parser": 8.53.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) eslint: 9.39.2(jiti@2.6.1) eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.53.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1)) + eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.2(jiti@2.6.1)) transitivePeerDependencies: - supports-color @@ -10419,7 +10439,7 @@ snapshots: doctrine: 2.1.0 eslint: 9.39.2(jiti@2.6.1) eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.53.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.53.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1)) + eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.53.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.2(jiti@2.6.1)) hasown: 2.0.2 is-core-module: 2.16.1 is-glob: 4.0.3 @@ -11203,6 +11223,10 @@ snapshots: dependencies: language-subtag-registry: 0.3.23 + lenis@1.3.23(react@19.2.3): + optionalDependencies: + react: 19.2.3 + levn@0.4.1: dependencies: prelude-ls: 1.2.1