Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 71 additions & 0 deletions src/components/common/StickyFilterBar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { type ReactNode } from 'react';
import { cn } from '@/lib/utils';

interface StickyFilterBarProps {
eyebrow?: string;
title: string;
description?: string;
resultCount?: number;
children: ReactNode;
className?: string;
}

const StickyFilterBar: React.FC<StickyFilterBarProps> = ({
eyebrow = 'Filters',
title,
description,
resultCount,
children,
className,
}) => {
return (
<div
className={cn(
'sticky top-4 z-20 mb-10 md:top-6',
className
)}
>
<div
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"
>
<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))]" />

<div className="relative flex flex-col gap-4">
<div className="flex flex-col gap-3 md:flex-row md:items-start md:justify-between">
<div className="min-w-0">
<p className="text-[0.68rem] font-bold uppercase tracking-[0.28em] text-amber-300/85">
{eyebrow}
</p>
<div className="mt-2 flex flex-wrap items-center gap-x-3 gap-y-2">
<h2
className="font-grotesque text-xl font-bold tracking-tight text-white md:text-2xl"
>
{title}
</h2>
{typeof resultCount === 'number' && (
<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">
{resultCount} {resultCount === 1 ? 'result' : 'results'}
</span>
)}
</div>
{description && (
<p className="mt-2 hidden max-w-2xl text-sm text-white/62 md:block">
{description}
</p>
)}
</div>
</div>

<div className="flex flex-col gap-3 md:flex-row md:items-center md:justify-between">
<div className="w-full md:max-w-2xl">{children}</div>
<span className="hidden text-xs font-medium uppercase tracking-[0.18em] text-white/45 md:inline-flex">
Filters stay pinned while you browse
</span>
</div>
</div>
</div>
</div>
);
};

export default StickyFilterBar;
117 changes: 92 additions & 25 deletions src/pages/LandingPage.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import { useState, useEffect, useMemo } from 'react';
import { courseService, type Course } from '@/services/course.service';
import SearchBar from '@/components/common/SearchBar';
import StickyFilterBar from '@/components/common/StickyFilterBar';
import CreatorCard from '@/components/common/CreatorCard';
import { CreatorGridSkeleton } from '@/components/common/CreatorSkeleton';
import EmptyState from '@/components/common/EmptyState';

// ✅ NEW IMPORTS
import { Button } from '@/components/ui/button';
import { UnavailableAction } from '@/components/ui/unavailable-action';

// Fallback demo data in case API fails
const DEMO_CREATORS: Course[] = [
{
id: '1',
Expand All @@ -20,6 +20,56 @@ const DEMO_CREATORS: Course[] = [
level: 'BEGINNER',
thumbnail: 'https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?w=400&h=400&fit=crop',
},
{
id: '2',
title: 'Sarah Chen',
description: 'Solidity Developer',
price: 0.12,
instructorId: 'schen_dev',
category: 'Tech',
level: 'ADVANCED',
thumbnail: 'https://images.unsplash.com/photo-1494790108377-be9c29b29330?w=400&h=400&fit=crop',
},
{
id: '3',
title: 'Marcus Thorne',
description: 'Crypto Strategist',
price: 0.08,
instructorId: 'mthorne',
category: 'Finance',
level: 'INTERMEDIATE',
thumbnail: 'https://images.unsplash.com/photo-1500648767791-00dcc994a43e?w=400&h=400&fit=crop',
},
{
id: '4',
title: 'Elena Vance',
description: 'UI/UX Designer',
price: 0.04,
instructorId: 'evance_design',
category: 'Design',
level: 'BEGINNER',
thumbnail: 'https://images.unsplash.com/photo-1438761681033-6461ffad8d80?w=400&h=400&fit=crop',
},
{
id: '5',
title: 'David Kojo',
description: 'Music Producer',
price: 0.15,
instructorId: 'dkojo_beats',
category: 'Music',
level: 'ADVANCED',
thumbnail: 'https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?w=400&h=400&fit=crop',
},
{
id: '6',
title: 'Yuki Sato',
description: 'Motion Designer',
price: 0.07,
instructorId: 'yuki_s',
category: 'Design',
level: 'INTERMEDIATE',
thumbnail: 'https://images.unsplash.com/photo-1517841905240-472988babdf9?w=400&h=400&fit=crop',
},
];

function LandingPage() {
Expand All @@ -32,8 +82,13 @@ function LandingPage() {
setIsLoading(true);
try {
const data = await courseService.getCourses();
setCreators(data.length > 0 ? data : DEMO_CREATORS);
} catch {
if (data && data.length > 0) {
setCreators(data);
} else {
setCreators(DEMO_CREATORS);
}
} catch (error) {
console.error('Failed to fetch creators:', error);
setCreators(DEMO_CREATORS);
} finally {
setTimeout(() => setIsLoading(false), 800);
Expand All @@ -54,51 +109,63 @@ function LandingPage() {
const handleResetSearch = () => setSearchQuery('');

return (
<main className="relative min-h-screen bg-black px-6 py-12">
<div className="mx-auto max-w-7xl">

{/* HEADER */}
<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">
<div className="absolute left-[-4rem] top-[10%] size-72 rounded-full bg-amber-300/20 blur-[100px]" />
<div className="absolute bottom-[8%] right-[-3rem] size-72 rounded-full bg-emerald-300/15 blur-[100px]" />
<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%)]" />

<div className="relative z-10 mx-auto max-w-7xl">
<header className="mb-16 text-center">
<h1 className="text-4xl font-bold text-white mb-6">
<img className="mx-auto mb-8 size-10" src="/icons/logo.svg" alt="Access Layer logo" />
<p className="mb-3 text-sm font-bold uppercase tracking-[0.25em] text-amber-400/80">
Creator Keys Marketplace
</p>
<h1 className="mb-8 font-grotesque text-[clamp(2.5rem,8vw,5rem)] font-extrabold leading-[1.1] tracking-tight text-white">
Access Layer
</h1>

{/* ✅ TOOLTIP BUTTON */}
<div className="flex justify-center mb-6">
<div className="mb-8 flex justify-center">
<UnavailableAction disabled={true} reason="Feature coming soon">
<Button>Buy Access</Button>
</UnavailableAction>
</div>
</header>

<StickyFilterBar
eyebrow="Marketplace filters"
title="Find creators without losing your place"
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."
resultCount={filteredCreators.length}
>
<SearchBar
value={searchQuery}
onChange={setSearchQuery}
className="max-w-none shadow-2xl shadow-black/20"
/>
</header>
</StickyFilterBar>

{/* CONTENT */}
<section>
<section className="mt-2">
{isLoading ? (
<CreatorGridSkeleton count={3} />
<CreatorGridSkeleton count={6} />
) : filteredCreators.length > 0 ? (
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
<div className="grid grid-cols-1 gap-8 sm:grid-cols-2 lg:grid-cols-3">
{filteredCreators.map(creator => (
<CreatorCard key={creator.id} creator={creator} />
))}
</div>
) : (
<EmptyState
image="/images/no-results.png"
title="No creators found"
description="Try a different search"
onReset={handleResetSearch}
/>
<div className="flex justify-center py-12">
<EmptyState
image="/images/no-results.png"
title="No creators found"
description={`We couldn't find any creators matching "${searchQuery}". Try a different name or handle.`}
onReset={handleResetSearch}
/>
</div>
)}
</section>

</div>
</main>
);
}

export default LandingPage;
export default LandingPage;
Loading