Skip to content

Commit 420522c

Browse files
Project owner logic (boundlessfi#73)
* feat: implement new veiw for project viewers, logic for user check and status * fix: project owner validation * fix: project owner logic, display projects on landing page, explore page, other modifications * fix: project owner logic, display projects on landing page, explore page, other modifications * fix: minor fixes * fix lint issues * fix lint issues * fix script/utils * fix --------- Co-authored-by: Collins Ikechukwu <collinschristroa@gmail.com>
1 parent 71f4c93 commit 420522c

21 files changed

Lines changed: 7744 additions & 4848 deletions

app/(dashboard)/projects/edit/[id]/milestone-tracker.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ export function MilestoneTracker({
4242

4343
useEffect(() => {
4444
fetchMilestones();
45-
}, [projectId]);
45+
}, [projectId, fetchMilestones]);
4646

4747
const getStatusIcon = (status: Milestone["status"]) => {
4848
switch (status) {

app/explore/page.tsx

Lines changed: 210 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,210 @@
1+
"use client";
2+
3+
import PageTransition from "@/components/landing/components/PageTransition";
4+
import Navbar from "@/components/shared/navbar";
5+
import { PublicProjectCard } from "@/components/shared/public-project-card";
6+
import { Button } from "@/components/ui/button";
7+
import { Card } from "@/components/ui/card";
8+
import { Input } from "@/components/ui/input";
9+
import {
10+
Select,
11+
SelectContent,
12+
SelectItem,
13+
SelectTrigger,
14+
SelectValue,
15+
} from "@/components/ui/select";
16+
import { useDebounce } from "@/hooks/use-debounce";
17+
import { useProjects } from "@/store/useProjectStore";
18+
import type { Project } from "@/types/project";
19+
import { Search, X } from "lucide-react";
20+
import { useEffect, useState } from "react";
21+
22+
export default function ExplorePage() {
23+
const { projects, isLoading, error, fetchProjects } = useProjects();
24+
const [filteredProjects, setFilteredProjects] = useState<Project[]>([]);
25+
const [searchTerm, setSearchTerm] = useState("");
26+
const [categoryFilter, setCategoryFilter] = useState<string>("all");
27+
const [statusFilter, setStatusFilter] = useState<string>("all");
28+
const debouncedSearchTerm = useDebounce(searchTerm, 500);
29+
30+
// Fetch projects on mount
31+
useEffect(() => {
32+
fetchProjects(false);
33+
}, [fetchProjects]);
34+
35+
// Apply filters whenever projects, search term or filters change
36+
useEffect(() => {
37+
if (!projects.length) return;
38+
39+
let results = [...projects];
40+
41+
// Apply search filter
42+
if (debouncedSearchTerm) {
43+
results = results.filter(
44+
(project) =>
45+
project.title
46+
.toLowerCase()
47+
.includes(debouncedSearchTerm.toLowerCase()) ||
48+
project.description
49+
.toLowerCase()
50+
.includes(debouncedSearchTerm.toLowerCase()),
51+
);
52+
}
53+
54+
// Apply category filter
55+
if (categoryFilter !== "all") {
56+
results = results.filter(
57+
(project) =>
58+
project.category.toLowerCase() === categoryFilter.toLowerCase(),
59+
);
60+
}
61+
62+
// Apply status filter
63+
if (statusFilter !== "all") {
64+
results = results.filter(
65+
(project) => project.ideaValidation === statusFilter.toUpperCase(),
66+
);
67+
}
68+
69+
setFilteredProjects(results);
70+
}, [projects, debouncedSearchTerm, categoryFilter, statusFilter]);
71+
72+
// Get unique categories for filter dropdown
73+
const categories = Array.from(
74+
new Set(projects.map((project) => project.category)),
75+
).sort();
76+
77+
return (
78+
<PageTransition>
79+
<Navbar />
80+
<div className="container max-w-7xl mx-auto px-4 md:px-6 py-12">
81+
<div className="mb-12">
82+
<h1 className="text-3xl md:text-4xl font-bold tracking-tighter mb-2">
83+
Explore Projects
84+
</h1>
85+
<p className="text-muted-foreground">
86+
Discover and support innovative blockchain projects
87+
</p>
88+
</div>
89+
90+
{isLoading ? (
91+
<div className="text-center py-10">Loading projects...</div>
92+
) : error ? (
93+
<div className="text-center py-10 text-destructive">
94+
Error: {error}
95+
</div>
96+
) : !projects.length ? (
97+
<div className="text-center py-10">No projects found</div>
98+
) : (
99+
<>
100+
{/* Search and Filters */}
101+
<div className="mb-8">
102+
<Card className="p-4">
103+
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
104+
{/* Search Input */}
105+
<div className="relative">
106+
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
107+
<Input
108+
placeholder="Search projects..."
109+
className="pl-9"
110+
value={searchTerm}
111+
onChange={(e) => setSearchTerm(e.target.value)}
112+
/>
113+
{searchTerm && (
114+
<Button
115+
onClick={() => setSearchTerm("")}
116+
className="absolute right-3 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground"
117+
>
118+
<X className="h-4 w-4" />
119+
</Button>
120+
)}
121+
</div>
122+
123+
{/* Category Filter */}
124+
<Select
125+
value={categoryFilter}
126+
onValueChange={setCategoryFilter}
127+
>
128+
<SelectTrigger>
129+
<SelectValue placeholder="All Categories" />
130+
</SelectTrigger>
131+
<SelectContent>
132+
<SelectItem value="all">All Categories</SelectItem>
133+
{categories.map((category) => (
134+
<SelectItem key={category} value={category}>
135+
{category}
136+
</SelectItem>
137+
))}
138+
</SelectContent>
139+
</Select>
140+
141+
{/* Status Filter */}
142+
<Select value={statusFilter} onValueChange={setStatusFilter}>
143+
<SelectTrigger>
144+
<SelectValue placeholder="All Statuses" />
145+
</SelectTrigger>
146+
<SelectContent>
147+
<SelectItem value="all">All Statuses</SelectItem>
148+
<SelectItem value="pending">Pending</SelectItem>
149+
<SelectItem value="validated">Validated</SelectItem>
150+
<SelectItem value="rejected">Rejected</SelectItem>
151+
</SelectContent>
152+
</Select>
153+
154+
{/* Reset Filters */}
155+
<Button
156+
variant="outline"
157+
onClick={() => {
158+
setSearchTerm("");
159+
setCategoryFilter("all");
160+
setStatusFilter("all");
161+
}}
162+
disabled={
163+
searchTerm === "" &&
164+
categoryFilter === "all" &&
165+
statusFilter === "all"
166+
}
167+
>
168+
Reset Filters
169+
</Button>
170+
</div>
171+
</Card>
172+
</div>
173+
174+
{/* Results Count */}
175+
<div className="mb-4 flex justify-between items-center">
176+
<div className="text-sm text-muted-foreground">
177+
Showing {filteredProjects.length} of {projects.length} projects
178+
</div>
179+
</div>
180+
181+
{/* Projects Grid */}
182+
{filteredProjects.length > 0 ? (
183+
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6">
184+
{filteredProjects.map((project) => (
185+
<PublicProjectCard key={project.id} project={project} />
186+
))}
187+
</div>
188+
) : (
189+
<div className="text-center py-12">
190+
<div className="text-muted-foreground mb-4">
191+
No projects match your filters
192+
</div>
193+
<Button
194+
variant="outline"
195+
onClick={() => {
196+
setSearchTerm("");
197+
setCategoryFilter("all");
198+
setStatusFilter("all");
199+
}}
200+
>
201+
Clear filters
202+
</Button>
203+
</div>
204+
)}
205+
</>
206+
)}
207+
</div>
208+
</PageTransition>
209+
);
210+
}

components/auth-tabs.tsx

Lines changed: 40 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,11 @@ import SignInForm from "@/components/signin-form";
55
import { AnimatePresence, type Variants, motion } from "framer-motion";
66
import type { BuiltInProviderType } from "next-auth/providers/index";
77
import type { ClientSafeProvider, LiteralUnion } from "next-auth/react";
8+
import { useSession } from "next-auth/react";
89
import Image from "next/image";
910
import Link from "next/link";
10-
import { useState } from "react";
11+
import { useRouter } from "next/navigation";
12+
import { useEffect, useState } from "react";
1113
import RegistrationForm from "./registeration-form";
1214

1315
const tabVariants: Variants = {
@@ -55,27 +57,55 @@ interface AuthTabsProps {
5557

5658
export default function AuthTabs({ providers }: AuthTabsProps) {
5759
const [activeTab, setActiveTab] = useState<"signin" | "register">("signin");
60+
const { status } = useSession();
61+
const router = useRouter();
62+
63+
useEffect(() => {
64+
if (status === "authenticated") {
65+
router.push("/dashboard");
66+
}
67+
}, [status, router]);
68+
69+
if (status === "loading") {
70+
return (
71+
<div className="min-h-screen flex items-center justify-center bg-[#dffce8]">
72+
<div className="text-center">
73+
<p className="text-[#194247]">Loading...</p>
74+
</div>
75+
</div>
76+
);
77+
}
78+
79+
if (status === "authenticated") {
80+
return (
81+
<div className="min-h-screen flex items-center justify-center bg-[#dffce8]">
82+
<div className="text-center">
83+
<p className="text-[#194247]">Redirecting to dashboard...</p>
84+
</div>
85+
</div>
86+
);
87+
}
5888

5989
return (
6090
<div className="min-h-screen flex items-center justify-center bg-[#dffce8] py-12 px-4 sm:px-6 lg:px-8">
61-
{/* Changed space-y-8 to space-y-6 here */}
6291
<div className="max-w-md w-full space-y-6 bg-white p-8 rounded-lg shadow-lg">
6392
<div className="flex flex-col items-center">
64-
<Image
65-
src="/logo.svg"
66-
alt="Boundless"
67-
width={180}
68-
height={180}
69-
className="mb-4"
70-
/>
93+
<Link href="/">
94+
<Image
95+
src="/logo.svg"
96+
alt="Boundless"
97+
width={180}
98+
height={180}
99+
className="mb-4"
100+
/>
101+
</Link>
71102
<h2 className="mt-2 text-center text-3xl font-extrabold text-[#194247]">
72103
{activeTab === "signin"
73104
? "Sign in to your account"
74105
: "Create an account"}
75106
</h2>
76107
</div>
77108

78-
{/* Added -mb-2 here to reduce space between tabs and form */}
79109
<fieldset className="flex rounded-md shadow-sm -mb-2">
80110
<motion.button
81111
variants={tabVariants}
@@ -95,7 +125,6 @@ export default function AuthTabs({ providers }: AuthTabsProps) {
95125
</motion.button>
96126
</fieldset>
97127

98-
{/* Added mt-4 here to control space after tabs */}
99128
<motion.div
100129
className="relative overflow-hidden mt-0"
101130
layout

components/dashboard/profile/ProfileEditForm.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import axios from "axios";
1313
import { Loader2 } from "lucide-react";
1414
import React, { useState } from "react";
1515
import { useForm } from "react-hook-form";
16-
import { toast } from "react-hot-toast";
16+
import { toast } from "sonner";
1717
import { z } from "zod";
1818

1919
const profileSchema = z.object({

components/landing/sections/cta.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ export default function BoundlessCTA() {
4848

4949
<div className="flex flex-col sm:flex-row gap-4">
5050
<Button className="group" size="lg">
51-
<Link href="/create-project" className="flex items-center">
51+
<Link href="/projects/new" className="flex items-center">
5252
Start a project
5353
<ArrowRight className="ml-2 h-4 w-4 transition-transform duration-300 group-hover:translate-x-1" />
5454
</Link>

0 commit comments

Comments
 (0)