Skip to content

Commit 5eabf3c

Browse files
authored
Merge pull request #39 from bamiebot-maker/feat/issue-31-sticky-marketplace-filter-bar
feat: add sticky marketplace filter bar
2 parents 41df384 + ad33054 commit 5eabf3c

2 files changed

Lines changed: 163 additions & 25 deletions

File tree

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import { type ReactNode } from 'react';
2+
import { cn } from '@/lib/utils';
3+
4+
interface StickyFilterBarProps {
5+
eyebrow?: string;
6+
title: string;
7+
description?: string;
8+
resultCount?: number;
9+
children: ReactNode;
10+
className?: string;
11+
}
12+
13+
const StickyFilterBar: React.FC<StickyFilterBarProps> = ({
14+
eyebrow = 'Filters',
15+
title,
16+
description,
17+
resultCount,
18+
children,
19+
className,
20+
}) => {
21+
return (
22+
<div
23+
className={cn(
24+
'sticky top-4 z-20 mb-10 md:top-6',
25+
className
26+
)}
27+
>
28+
<div
29+
className="relative overflow-hidden rounded-[1.75rem] border border-white/10 bg-slate-950/78 px-4 py-4 text-white shadow-[0_20px_80px_rgba(0,0,0,0.28)] backdrop-blur-xl md:px-5 md:py-4"
30+
>
31+
<div className="pointer-events-none absolute inset-0 bg-[linear-gradient(135deg,rgba(255,255,255,0.08),rgba(255,255,255,0.02)_48%,rgba(245,158,11,0.08))]" />
32+
33+
<div className="relative flex flex-col gap-4">
34+
<div className="flex flex-col gap-3 md:flex-row md:items-start md:justify-between">
35+
<div className="min-w-0">
36+
<p className="text-[0.68rem] font-bold uppercase tracking-[0.28em] text-amber-300/85">
37+
{eyebrow}
38+
</p>
39+
<div className="mt-2 flex flex-wrap items-center gap-x-3 gap-y-2">
40+
<h2
41+
className="font-grotesque text-xl font-bold tracking-tight text-white md:text-2xl"
42+
>
43+
{title}
44+
</h2>
45+
{typeof resultCount === 'number' && (
46+
<span className="inline-flex items-center rounded-full border border-white/10 bg-white/8 px-3 py-1 text-xs font-medium text-white/75">
47+
{resultCount} {resultCount === 1 ? 'result' : 'results'}
48+
</span>
49+
)}
50+
</div>
51+
{description && (
52+
<p className="mt-2 hidden max-w-2xl text-sm text-white/62 md:block">
53+
{description}
54+
</p>
55+
)}
56+
</div>
57+
</div>
58+
59+
<div className="flex flex-col gap-3 md:flex-row md:items-center md:justify-between">
60+
<div className="w-full md:max-w-2xl">{children}</div>
61+
<span className="hidden text-xs font-medium uppercase tracking-[0.18em] text-white/45 md:inline-flex">
62+
Filters stay pinned while you browse
63+
</span>
64+
</div>
65+
</div>
66+
</div>
67+
</div>
68+
);
69+
};
70+
71+
export default StickyFilterBar;

src/pages/LandingPage.tsx

Lines changed: 92 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
import { useState, useEffect, useMemo } from 'react';
22
import { courseService, type Course } from '@/services/course.service';
33
import SearchBar from '@/components/common/SearchBar';
4+
import StickyFilterBar from '@/components/common/StickyFilterBar';
45
import CreatorCard from '@/components/common/CreatorCard';
56
import { CreatorGridSkeleton } from '@/components/common/CreatorSkeleton';
67
import EmptyState from '@/components/common/EmptyState';
7-
8-
// ✅ NEW IMPORTS
98
import { Button } from '@/components/ui/button';
109
import { UnavailableAction } from '@/components/ui/unavailable-action';
1110

11+
// Fallback demo data in case API fails
1212
const DEMO_CREATORS: Course[] = [
1313
{
1414
id: '1',
@@ -20,6 +20,56 @@ const DEMO_CREATORS: Course[] = [
2020
level: 'BEGINNER',
2121
thumbnail: 'https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?w=400&h=400&fit=crop',
2222
},
23+
{
24+
id: '2',
25+
title: 'Sarah Chen',
26+
description: 'Solidity Developer',
27+
price: 0.12,
28+
instructorId: 'schen_dev',
29+
category: 'Tech',
30+
level: 'ADVANCED',
31+
thumbnail: 'https://images.unsplash.com/photo-1494790108377-be9c29b29330?w=400&h=400&fit=crop',
32+
},
33+
{
34+
id: '3',
35+
title: 'Marcus Thorne',
36+
description: 'Crypto Strategist',
37+
price: 0.08,
38+
instructorId: 'mthorne',
39+
category: 'Finance',
40+
level: 'INTERMEDIATE',
41+
thumbnail: 'https://images.unsplash.com/photo-1500648767791-00dcc994a43e?w=400&h=400&fit=crop',
42+
},
43+
{
44+
id: '4',
45+
title: 'Elena Vance',
46+
description: 'UI/UX Designer',
47+
price: 0.04,
48+
instructorId: 'evance_design',
49+
category: 'Design',
50+
level: 'BEGINNER',
51+
thumbnail: 'https://images.unsplash.com/photo-1438761681033-6461ffad8d80?w=400&h=400&fit=crop',
52+
},
53+
{
54+
id: '5',
55+
title: 'David Kojo',
56+
description: 'Music Producer',
57+
price: 0.15,
58+
instructorId: 'dkojo_beats',
59+
category: 'Music',
60+
level: 'ADVANCED',
61+
thumbnail: 'https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?w=400&h=400&fit=crop',
62+
},
63+
{
64+
id: '6',
65+
title: 'Yuki Sato',
66+
description: 'Motion Designer',
67+
price: 0.07,
68+
instructorId: 'yuki_s',
69+
category: 'Design',
70+
level: 'INTERMEDIATE',
71+
thumbnail: 'https://images.unsplash.com/photo-1517841905240-472988babdf9?w=400&h=400&fit=crop',
72+
},
2373
];
2474

2575
function LandingPage() {
@@ -32,8 +82,13 @@ function LandingPage() {
3282
setIsLoading(true);
3383
try {
3484
const data = await courseService.getCourses();
35-
setCreators(data.length > 0 ? data : DEMO_CREATORS);
36-
} catch {
85+
if (data && data.length > 0) {
86+
setCreators(data);
87+
} else {
88+
setCreators(DEMO_CREATORS);
89+
}
90+
} catch (error) {
91+
console.error('Failed to fetch creators:', error);
3792
setCreators(DEMO_CREATORS);
3893
} finally {
3994
setTimeout(() => setIsLoading(false), 800);
@@ -54,51 +109,63 @@ function LandingPage() {
54109
const handleResetSearch = () => setSearchQuery('');
55110

56111
return (
57-
<main className="relative min-h-screen bg-black px-6 py-12">
58-
<div className="mx-auto max-w-7xl">
59-
60-
{/* HEADER */}
112+
<main className="relative min-h-screen overflow-x-hidden bg-[linear-gradient(160deg,#08111f_0%,#10213b_45%,#f0b14d_160%)] px-6 py-12 md:px-12">
113+
<div className="absolute left-[-4rem] top-[10%] size-72 rounded-full bg-amber-300/20 blur-[100px]" />
114+
<div className="absolute bottom-[8%] right-[-3rem] size-72 rounded-full bg-emerald-300/15 blur-[100px]" />
115+
<div className="absolute inset-0 bg-[radial-gradient(circle_at_top,rgba(255,186,73,0.1),transparent_40%),radial-gradient(circle_at_bottom_left,rgba(74,222,128,0.08),transparent_35%)]" />
116+
117+
<div className="relative z-10 mx-auto max-w-7xl">
61118
<header className="mb-16 text-center">
62-
<h1 className="text-4xl font-bold text-white mb-6">
119+
<img className="mx-auto mb-8 size-10" src="/icons/logo.svg" alt="Access Layer logo" />
120+
<p className="mb-3 text-sm font-bold uppercase tracking-[0.25em] text-amber-400/80">
121+
Creator Keys Marketplace
122+
</p>
123+
<h1 className="mb-8 font-grotesque text-[clamp(2.5rem,8vw,5rem)] font-extrabold leading-[1.1] tracking-tight text-white">
63124
Access Layer
64125
</h1>
65-
66-
{/* ✅ TOOLTIP BUTTON */}
67-
<div className="flex justify-center mb-6">
126+
<div className="mb-8 flex justify-center">
68127
<UnavailableAction disabled={true} reason="Feature coming soon">
69128
<Button>Buy Access</Button>
70129
</UnavailableAction>
71130
</div>
131+
</header>
72132

133+
<StickyFilterBar
134+
eyebrow="Marketplace filters"
135+
title="Find creators without losing your place"
136+
description="Search by creator name or handle while you keep scrolling through the marketplace. The filter shell stays visible and compact so you can refine results without losing your place."
137+
resultCount={filteredCreators.length}
138+
>
73139
<SearchBar
74140
value={searchQuery}
75141
onChange={setSearchQuery}
142+
className="max-w-none shadow-2xl shadow-black/20"
76143
/>
77-
</header>
144+
</StickyFilterBar>
78145

79-
{/* CONTENT */}
80-
<section>
146+
<section className="mt-2">
81147
{isLoading ? (
82-
<CreatorGridSkeleton count={3} />
148+
<CreatorGridSkeleton count={6} />
83149
) : filteredCreators.length > 0 ? (
84-
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
150+
<div className="grid grid-cols-1 gap-8 sm:grid-cols-2 lg:grid-cols-3">
85151
{filteredCreators.map(creator => (
86152
<CreatorCard key={creator.id} creator={creator} />
87153
))}
88154
</div>
89155
) : (
90-
<EmptyState
91-
image="/images/no-results.png"
92-
title="No creators found"
93-
description="Try a different search"
94-
onReset={handleResetSearch}
95-
/>
156+
<div className="flex justify-center py-12">
157+
<EmptyState
158+
image="/images/no-results.png"
159+
title="No creators found"
160+
description={`We couldn't find any creators matching "${searchQuery}". Try a different name or handle.`}
161+
onReset={handleResetSearch}
162+
/>
163+
</div>
96164
)}
97165
</section>
98-
99166
</div>
100167
</main>
101168
);
102169
}
103170

104-
export default LandingPage;
171+
export default LandingPage;

0 commit comments

Comments
 (0)