Skip to content

Commit 4d375ae

Browse files
committed
feat(projects): add search and tag filtering functionality
Implement search input and tag dropdown filter to improve project discoverability
1 parent eded76f commit 4d375ae

1 file changed

Lines changed: 78 additions & 5 deletions

File tree

‎app/projects/page.tsx‎

Lines changed: 78 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,22 @@
11
"use client"
22

3+
import React, { useState, useMemo } from "react"
34
import Header from "@/components/header"
45
import Footer from "@/components/footer"
56
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card"
67
import { Button } from "@/components/ui/button"
78
import { Badge } from "@/components/ui/badge"
9+
import { Input } from "@/components/ui/input"
10+
import {
11+
DropdownMenu,
12+
DropdownMenuTrigger,
13+
DropdownMenuContent,
14+
DropdownMenuCheckboxItem,
15+
DropdownMenuLabel,
16+
DropdownMenuSeparator,
17+
} from "@/components/ui/dropdown-menu"
818
import Link from "next/link"
9-
import { Github, Linkedin, Code2, Sparkles, Briefcase, ExternalLink } from "lucide-react"
19+
import { Github, Linkedin, Code2, Sparkles, Briefcase, ExternalLink, Search, ChevronDown } from "lucide-react"
1020
import { cn } from "@/lib/utils"
1121

1222
// Import projects data from JSON file
@@ -29,14 +39,39 @@ const projectsWithDetails = projectsData.projects.map((project, index) => {
2939
}
3040
})
3141

32-
const projects = projectsWithDetails
42+
const allTags = [...new Set(projectsWithDetails.flatMap((p) => p.tags))].sort()
3343

3444
export default function ProjectsPage() {
45+
const [searchTerm, setSearchTerm] = useState("")
46+
const [selectedTags, setSelectedTags] = useState<string[]>([])
47+
48+
const handleTagToggle = (tag: string) => {
49+
setSelectedTags((prev) =>
50+
prev.includes(tag) ? prev.filter((t) => t !== tag) : [...prev, tag]
51+
)
52+
}
53+
54+
const filteredProjects = useMemo(() => {
55+
return projectsWithDetails.filter((project) => {
56+
const searchLower = searchTerm.toLowerCase()
57+
const matchesSearch =
58+
project.project_name.toLowerCase().includes(searchLower) ||
59+
project.description.toLowerCase().includes(searchLower) ||
60+
project.tags.some((tag) => tag.toLowerCase().includes(searchLower))
61+
62+
const matchesTags =
63+
selectedTags.length === 0 ||
64+
selectedTags.every((tag) => project.tags.includes(tag))
65+
66+
return matchesSearch && matchesTags
67+
})
68+
}, [searchTerm, selectedTags])
69+
3570
return (
3671
<div className="flex flex-col min-h-screen bg-gradient-to-br from-background via-background to-muted/5">
3772
<Header />
3873

39-
{/* Hero Section */}
74+
{/* Hero Section */}
4075
<section className="py-20 md:py-32 relative overflow-hidden">
4176
<div
4277
className={cn(
@@ -80,8 +115,46 @@ export default function ProjectsPage() {
80115

81116
<section className="relative py-20 bg-gradient-to-b from-muted/20 to-background">
82117
<div className="container relative z-10 mx-auto px-4">
118+
{/* Search and Filter Controls */}
119+
<div className="mb-12 flex justify-center items-center gap-4 max-w-4xl mx-auto">
120+
<div className="relative flex-grow">
121+
<Search className="absolute left-4 top-1/2 -translate-y-1/2 h-5 w-5 text-muted-foreground" />
122+
<Input
123+
type="text"
124+
placeholder="Search projects..."
125+
className="w-full pl-12 pr-4 py-3 text-lg bg-background/70 backdrop-blur-sm rounded-full focus:ring-2 focus:ring-primary/50"
126+
value={searchTerm}
127+
onChange={(e) => setSearchTerm(e.target.value)}
128+
/>
129+
</div>
130+
<DropdownMenu>
131+
<DropdownMenuTrigger asChild>
132+
<Button variant="outline" className="flex items-center gap-2 rounded-full text-lg py-3 bg-background/70 backdrop-blur-sm">
133+
<span>Tags</span>
134+
{selectedTags.length > 0 && (
135+
<Badge variant="secondary" className="rounded-full">{selectedTags.length}</Badge>
136+
)}
137+
<ChevronDown className="h-5 w-5 text-muted-foreground" />
138+
</Button>
139+
</DropdownMenuTrigger>
140+
<DropdownMenuContent className="w-64">
141+
<DropdownMenuLabel>Filter by Tags</DropdownMenuLabel>
142+
<DropdownMenuSeparator />
143+
{allTags.map((tag) => (
144+
<DropdownMenuCheckboxItem
145+
key={tag}
146+
checked={selectedTags.includes(tag)}
147+
onCheckedChange={() => handleTagToggle(tag)}
148+
>
149+
{tag}
150+
</DropdownMenuCheckboxItem>
151+
))}
152+
</DropdownMenuContent>
153+
</DropdownMenu>
154+
</div>
155+
83156
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8 lg:gap-10">
84-
{projects.map((project, index) => (
157+
{filteredProjects.map((project, index) => (
85158
<div key={project.project_name} className="group" style={{ animationDelay: `${index * 100}ms` }}>
86159
<Card className="relative h-full overflow-hidden border-0 bg-background/60 backdrop-blur-sm shadow-lg transition-all duration-500 hover:shadow-2xl hover:shadow-primary/10 hover:-translate-y-2">
87160
<div
@@ -181,4 +254,4 @@ export default function ProjectsPage() {
181254
<Footer />
182255
</div>
183256
)
184-
}
257+
}

0 commit comments

Comments
 (0)