Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
248 changes: 248 additions & 0 deletions app/discover/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,248 @@
/* eslint-disable react-hooks/static-components */
"use client";

import { useCallback, useEffect, useState } from "react";
import { useRouter, useSearchParams } from "next/navigation";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { FilterPanel } from "@/components/filters/filter-panel";
import { ProjectCard } from "@/components/cards/project-card";
import { BountyCard } from "@/components/cards/bounty-card";
import { Skeleton } from "@/components/ui/skeleton";
import { mockProjects, mockBounties } from "@/lib/mock-data";
import { FilterState, TabType, Project, Bounty } from "@/lib/types";
import { PackageOpen, Coins } from "lucide-react";

export default function DiscoverPage() {
const router = useRouter();
const searchParams = useSearchParams();

// Initialize state from URL params
const [activeTab, setActiveTab] = useState<TabType>(
(searchParams.get("tab") as TabType) || "projects",
);
const [filters, setFilters] = useState<FilterState>({
search: searchParams.get("search") || "",
tags: searchParams.get("tags")?.split(",").filter(Boolean) || [],
sort: (searchParams.get("sort") as FilterState["sort"]) || "newest",
});
const [isLoading, setIsLoading] = useState(true);

// Simulate loading
useEffect(() => {
const timer = setTimeout(() => setIsLoading(false), 500);
return () => clearTimeout(timer);
}, []);

// Update URL when state changes (debounced)
useEffect(() => {
const timer = setTimeout(() => {
const params = new URLSearchParams();
params.set("tab", activeTab);
if (filters.search) params.set("search", filters.search);
if (filters.tags.length > 0) params.set("tags", filters.tags.join(","));
if (filters.sort !== "newest") params.set("sort", filters.sort);

router.replace(`/discover?${params.toString()}`, { scroll: false });
}, 300);

return () => clearTimeout(timer);
}, [activeTab, filters, router]);

// Filter and sort projects
const filteredProjects = useCallback(() => {
let result = [...mockProjects];

// Apply search filter
if (filters.search) {
const searchLower = filters.search.toLowerCase();
result = result.filter(
(p) =>
p.title.toLowerCase().includes(searchLower) ||
p.description.toLowerCase().includes(searchLower) ||
p.tags.some((tag) => tag.toLowerCase().includes(searchLower)),
);
}

// Apply tags filter
if (filters.tags.length > 0) {
result = result.filter((p) =>
filters.tags.some((tag) => p.tags.includes(tag)),
);
}

// Apply sort
switch (filters.sort) {
case "newest":
result.sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime());
break;
case "recentlyUpdated":
result.sort((a, b) => b.updatedAt.getTime() - a.updatedAt.getTime());
break;
}

return result;
}, [filters]);

// Filter and sort bounties
const filteredBounties = useCallback(() => {
let result = [...mockBounties];

// Apply search filter
if (filters.search) {
const searchLower = filters.search.toLowerCase();
result = result.filter(
(b) =>
b.title.toLowerCase().includes(searchLower) ||
b.description.toLowerCase().includes(searchLower) ||
b.tags.some((tag) => tag.toLowerCase().includes(searchLower)),
);
}

// Apply tags filter
if (filters.tags.length > 0) {
result = result.filter((b) =>
filters.tags.some((tag) => b.tags.includes(tag)),
);
}

// Apply sort
switch (filters.sort) {
case "newest":
result.sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime());
break;
case "recentlyUpdated":
result.sort((a, b) => b.updatedAt.getTime() - a.updatedAt.getTime());
break;
case "highestReward":
result.sort((a, b) => b.reward - a.reward);
break;
}

return result;
}, [filters]);

const projects = filteredProjects();
const bounties = filteredBounties();

const EmptyState = ({ type }: { type: "projects" | "bounties" }) => (
<div className="flex flex-col items-center justify-center py-16 px-4 text-center">
<div className="mb-4 p-6 bg-primary/5 rounded-full">
{type === "projects" ? (
<PackageOpen className="h-16 w-16 text-primary/50" />
) : (
<Coins className="h-16 w-16 text-primary/50" />
)}
</div>
<h3 className="text-xl font-semibold mb-2">No {type} found</h3>
<p className="text-muted-foreground max-w-md">
Try adjusting your filters or search terms to find what you&#39;re
looking for.
</p>
</div>
);

return (
<div className="min-h-screen bg-background">
{/* Hero Section */}
<div className="border-b border-border/50 bg-gradient-to-b from-background to-background-card">
<div className="container mx-auto px-4 py-12 md:py-16">
<div className="max-w-3xl">
<h1 className="text-4xl md:text-5xl font-bold mb-4 gradient-text-3">
Discover Projects & Bounties
</h1>
<p className="text-lg text-muted-foreground">
Explore innovative projects shaping the future on Stellar. Find
bounties that match your skills and earn rewards.
</p>
</div>
</div>
</div>

{/* Main Content */}
<div className="container mx-auto px-4 py-8">
<Tabs
value={activeTab}
onValueChange={(val) => setActiveTab(val as TabType)}
>
{/* Tab Navigation */}
<div className="flex flex-col md:flex-row md:items-center md:justify-between gap-4 mb-8">
<TabsList className="bg-background-card text-gray-100 border border-border/50 p-1">
<TabsTrigger
value="projects"
className="data-[state=active]:bg-primary data-[state=active]:text-primary-foreground text-gray-100"
>
<PackageOpen className="mr-2 h-4 w-4" />
Projects
<span className="ml-2 text-xs opacity-70">
({mockProjects.length})
</span>
</TabsTrigger>
<TabsTrigger
value="bounties"
className="data-[state=active]:bg-primary data-[state=active]:text-primary-foreground text-gray-100"
>
<Coins className="mr-2 h-4 w-4" />
Bounties
<span className="ml-2 text-xs opacity-70">
({mockBounties.length})
</span>
</TabsTrigger>
</TabsList>
</div>

{/* Filter Panel */}
<div className="mb-8">
<FilterPanel
filters={filters}
onFiltersChange={setFilters}
activeTab={activeTab}
/>
</div>

{/* Projects Tab Content */}
<TabsContent value="projects" className="mt-0">
{isLoading ? (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{[...Array(6)].map((_, i) => (
<div key={i} className="space-y-4">
<Skeleton className="h-48 w-full" />
</div>
))}
</div>
) : projects.length > 0 ? (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{projects.map((project) => (
<ProjectCard key={project.id} project={project} />
))}
</div>
) : (
// eslint-disable-next-line react-hooks/static-components
<EmptyState type="projects" />
)}
</TabsContent>

{/* Bounties Tab Content */}
<TabsContent value="bounties" className="mt-0">
{isLoading ? (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{[...Array(6)].map((_, i) => (
<div key={i} className="space-y-4">
<Skeleton className="h-48 w-full" />
</div>
))}
</div>
) : bounties.length > 0 ? (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{bounties.map((bounty) => (
<BountyCard key={bounty.id} bounty={bounty} />
))}
</div>
) : (
<EmptyState type="bounties" />
)}
</TabsContent>
</Tabs>
</div>
</div>
);
}
Loading
Loading