From bee109dc41552a79640535bacd095128d778e1fc Mon Sep 17 00:00:00 2001 From: Dantama022 Date: Fri, 26 Jun 2026 17:27:06 +0100 Subject: [PATCH 1/3] feat: implement project discovery page with URL-synced filters, reusable form components, and supporting project data services. --- dongle/app/discover/page.tsx | 19 ++- dongle/components/layout/Navbar.tsx | 69 ++++++++++- dongle/components/projects/ProjectForm.tsx | 127 ++++++++++++++++++--- dongle/components/reviews/ReviewForm.tsx | 20 ++-- dongle/components/ui/TagInput.tsx | 86 ++++++++++++++ dongle/components/ui/TextAreaField.tsx | 64 +++++++++-- dongle/data/mockProjects.ts | 30 +++-- dongle/hooks/useDiscoverParams.ts | 20 +++- dongle/services/project/project.service.ts | 43 +++++-- dongle/types/project.ts | 4 +- 10 files changed, 414 insertions(+), 68 deletions(-) create mode 100644 dongle/components/ui/TagInput.tsx diff --git a/dongle/app/discover/page.tsx b/dongle/app/discover/page.tsx index fdec157..dc1f71a 100644 --- a/dongle/app/discover/page.tsx +++ b/dongle/app/discover/page.tsx @@ -8,6 +8,7 @@ import { Spinner } from "@/components/ui/Spinner"; import { Search, Filter } from "lucide-react"; import { useDiscoverParams } from "@/hooks/useDiscoverParams"; import type { SortBy } from "@/hooks/useDiscoverParams"; +import { TagInput } from "@/components/ui/TagInput"; const ITEMS_PER_PAGE = 9; @@ -21,10 +22,12 @@ function DiscoverContent() { searchInput, searchQuery, category, + tags, sortBy, page, setSearchInput, setCategory, + setTags, setSortBy, loadNextPage, clearFilters, @@ -44,7 +47,11 @@ function DiscoverContent() { : projectService.getAllProjects(); if (category !== "All") { - result = result.filter((p) => p.category === category); + result = result.filter((p) => p.primaryCategory === category); + } + + if (tags && tags.length > 0) { + result = result.filter((p) => tags.every((t) => p.tags?.includes(t))); } result = projectService.sortProjects(result, sortBy); @@ -133,6 +140,16 @@ function DiscoverContent() { + + {/* Tags filtering */} +
+ +
{/* Initial loading */} diff --git a/dongle/components/layout/Navbar.tsx b/dongle/components/layout/Navbar.tsx index c2a2fda..9675265 100644 --- a/dongle/components/layout/Navbar.tsx +++ b/dongle/components/layout/Navbar.tsx @@ -2,7 +2,7 @@ import Link from "next/link"; import { usePathname } from "next/navigation"; -import { useState } from "react"; +import { useState, useEffect, useRef } from "react"; import { useWallet, EXPECTED_NETWORK_LABEL } from "@/context/wallet.context"; import { Button } from "@/components/ui/Button"; @@ -22,6 +22,9 @@ export default function Navbar() { disconnectWallet, } = useWallet(); + const menuRef = useRef(null); + const toggleBtnRef = useRef(null); + const navLinks = [ { href: "/discover", label: "Discover" }, { href: "/reviews", label: "Reviews" }, @@ -32,6 +35,58 @@ export default function Navbar() { const isActive = (href: string) => pathname === href; + // Close menu on route changes + useEffect(() => { + setIsMenuOpen(false); + }, [pathname]); + + // Focus trap and escape key handler + useEffect(() => { + if (!isMenuOpen) { + return; + } + + const handleKeyDown = (e: KeyboardEvent) => { + if (e.key === "Escape") { + setIsMenuOpen(false); + // Return focus to toggle button when closed via Escape + toggleBtnRef.current?.focus(); + return; + } + + if (e.key === "Tab" && menuRef.current) { + const focusable = menuRef.current.querySelectorAll( + 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])' + ); + + // Include toggle button in focus loop for mobile + const elements = [toggleBtnRef.current, ...Array.from(focusable)].filter(Boolean) as HTMLElement[]; + + if (elements.length === 0) return; + + const first = elements[0]; + const last = elements[elements.length - 1]; + + if (e.shiftKey) { + if (document.activeElement === first) { + e.preventDefault(); + last?.focus(); + } + } else { + if (document.activeElement === last) { + e.preventDefault(); + first?.focus(); + } + } + } + }; + + document.addEventListener("keydown", handleKeyDown); + return () => { + document.removeEventListener("keydown", handleKeyDown); + }; + }, [isMenuOpen]); + return (