-
-
Notifications
You must be signed in to change notification settings - Fork 374
feat: add real-time project search and filtering functionality #710
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,5 +1,6 @@ | ||
| 'use client' | ||
|
|
||
| import { useState, useMemo } from 'react' | ||
| import Link from 'next/link' | ||
| import Grid from '@mui/material/Grid' | ||
| import MuiCard from '@mui/material/Card' | ||
|
|
@@ -15,7 +16,8 @@ import projects from '@/helper/projects' | |
| import { CardProduct } from '@/components/products/CardProduct' | ||
| import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' | ||
| import { faDiscord, faGithub } from '@fortawesome/free-brands-svg-icons' | ||
| import { motion } from 'framer-motion' | ||
| import { faSearch } from '@fortawesome/free-solid-svg-icons' | ||
| import { motion, AnimatePresence } from 'framer-motion' | ||
|
|
||
| function LinkIcon(props) { | ||
| return ( | ||
|
|
@@ -168,6 +170,8 @@ const styles = { | |
| } | ||
|
|
||
| export default function Projects() { | ||
| const [searchQuery, setSearchQuery] = useState('') | ||
|
|
||
| const readyToDownload = projects.filter( | ||
| (p) => p.category === 'Ready to download' | ||
| ) | ||
|
|
@@ -176,9 +180,20 @@ export default function Projects() { | |
| ) | ||
| const ongoing = projects.filter((p) => p.category === 'Ongoing') | ||
|
|
||
| const sortedProjects = [...projects].sort((a, b) => | ||
| (a.name || '').localeCompare(b.name || '', undefined, { sensitivity: 'base' }) | ||
| ) | ||
| const sortedProjects = useMemo(() => { | ||
| return [...projects].sort((a, b) => | ||
| (a.name || '').localeCompare(b.name || '', undefined, { sensitivity: 'base' }) | ||
| ) | ||
| }, []) | ||
|
|
||
| const filteredProjects = useMemo(() => { | ||
| const query = searchQuery.toLowerCase().trim() | ||
| if (!query) return sortedProjects | ||
|
|
||
| return sortedProjects.filter((project) => | ||
| (project.name || '').toLowerCase().includes(query) | ||
| ) | ||
| }, [searchQuery, sortedProjects]) | ||
|
|
||
| return ( | ||
| <> | ||
|
|
@@ -202,12 +217,56 @@ export default function Projects() { | |
| Free, open-source, and built by a global community of developers, designers, researchers, and innovators. | ||
| </p> | ||
| </motion.div> | ||
|
|
||
| {/* Search Bar */} | ||
| <div className="mt-8 mb-12 flex justify-center"> | ||
| <div className="relative w-full max-w-xl group"> | ||
| <label htmlFor="project-search" className="sr-only"> | ||
| Search projects by name | ||
| </label> | ||
| <div className="absolute inset-y-0 left-0 pl-4 flex items-center pointer-events-none z-10"> | ||
| <FontAwesomeIcon icon={faSearch} className="h-4 w-4 text-zinc-400 group-focus-within:text-[#00843D] dark:group-focus-within:text-yellow-400 transition-colors duration-300" /> | ||
| </div> | ||
| <input | ||
| id="project-search" | ||
| type="search" | ||
| aria-describedby="project-search-status" | ||
| className="block w-full pl-11 pr-4 py-3.5 border border-zinc-200 dark:border-zinc-700/50 rounded-2xl leading-5 bg-zinc-50/50 dark:bg-zinc-800/50 text-zinc-900 dark:text-zinc-100 placeholder-zinc-400 focus:outline-none focus:ring-2 focus:ring-[#00843D] dark:focus:ring-yellow-400/50 focus:border-transparent sm:text-sm transition-all duration-300 shadow-sm hover:shadow-md backdrop-blur-sm" | ||
| placeholder="Search for projects..." | ||
| value={searchQuery} | ||
| onChange={(e) => setSearchQuery(e.target.value)} | ||
| /> | ||
| </div> | ||
| </div> | ||
| <p id="project-search-status" className="sr-only" aria-live="polite"> | ||
| {filteredProjects.length === 0 | ||
| ? `No projects found matching ${searchQuery.trim()}.` | ||
| : `${filteredProjects.length} project${filteredProjects.length === 1 ? '' : 's'} shown.`} | ||
| </p> | ||
| </div> | ||
|
|
||
| <div className="mb-16 grid grid-cols-1 gap-x-8 gap-y-16 lg:grid-cols-3"> | ||
| {sortedProjects.map((product) => ( | ||
| <CardProduct key={product.slug} product={product} /> | ||
| ))} | ||
| <AnimatePresence mode="popLayout"> | ||
| {filteredProjects.map((product) => ( | ||
| <motion.div | ||
| layout | ||
| key={product.slug} | ||
| initial={{ opacity: 0, scale: 0.9 }} | ||
| animate={{ opacity: 1, scale: 1 }} | ||
| exit={{ opacity: 0, scale: 0.9 }} | ||
| transition={{ duration: 0.3 }} | ||
| > | ||
| <CardProduct product={product} /> | ||
| </motion.div> | ||
| ))} | ||
| </AnimatePresence> | ||
| {filteredProjects.length === 0 && ( | ||
| <div className="col-span-full py-20 text-center"> | ||
| <p className="text-xl text-zinc-500 dark:text-zinc-400 font-mono"> | ||
| No projects found matching "{searchQuery}" | ||
| </p> | ||
| </div> | ||
| )} | ||
|
Comment on lines
+249
to
+269
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: # First, check if the file exists and examine the code at the specified lines
if [ -f "src/app/projects/page.jsx" ]; then
wc -l src/app/projects/page.jsx
echo "---"
sed -n '235,265p' src/app/projects/page.jsx
else
echo "File not found"
fiRepository: AOSSIE-Org/Website Length of output: 1223 🌐 Web query:
💡 Result: Framer Motion's AnimatePresence with mode="popLayout" pops exiting elements out of the document layout immediately using position: absolute, allowing surrounding elements to reflow right away while the exit animation plays. This pairs well with the layout prop for smooth list removals, as shown in the official example: {items.map(item => ( <motion.li layout exit={{ opacity: 0 }} key={item.id} /> ))} Key behaviors: - Exiting elements animate from their popped position (absolute positioned to match original layout spot). - Custom child components must use forwardRef to forward the ref to the DOM element. - Parent container needs position: relative (or non-static) for correct positioning during layout shifts, as transforms create new offset parents. - Exit animations like opacity: 0 should trigger, but there's a known open bug (Issue Citations:
Delay the empty state message until exit animations complete. At line 253, the empty message renders immediately when 🤖 Prompt for AI Agents |
||
| </div> | ||
|
|
||
| {/* | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.