|
1 | 1 | "use client"; |
2 | 2 |
|
3 | 3 | import { Link } from "@heroui/link"; |
| 4 | +import { Input } from "@heroui/input"; |
| 5 | +import { Chip } from "@heroui/chip"; |
4 | 6 | import { useSearchParams } from "next/navigation"; |
| 7 | +import { useState, useMemo } from "react"; |
5 | 8 |
|
6 | | -import { publications, Tag } from "@/config/publications"; |
| 9 | +import { publications, Tag, Publication } from "@/config/publications"; |
7 | 10 | import { PublicationTags } from "@/components/tag"; |
8 | 11 |
|
9 | | -function Links({ paper, code, page }: { paper: string, code: string | null; page: string | null }) { |
| 12 | +const allTags = Object.values(Tag); |
| 13 | + |
| 14 | +function Links({ paper, code }: { paper: string; code: string | null }) { |
10 | 15 | let links = []; |
11 | 16 | if (paper !== null) { |
12 | | - links.push(<Link href={paper}>paper</Link>); |
| 17 | + links.push(<Link key="paper" href={paper}>paper</Link>); |
13 | 18 | } |
14 | 19 | if (code !== null) { |
15 | | - links.push(<Link href={code}>code</Link>) |
| 20 | + links.push(<Link key="code" href={code}>code</Link>); |
16 | 21 | } |
17 | | - return <>{links.reduce((prev, curr) => <>{prev} / {curr}</>)}</> |
| 22 | + return <>{links.reduce((prev, curr) => <span key="sep">{prev} / {curr}</span>)}</>; |
| 23 | +} |
| 24 | + |
| 25 | +function matchesSearch(publication: Publication, query: string): boolean { |
| 26 | + const q = query.toLowerCase(); |
| 27 | + return ( |
| 28 | + publication.title.toLowerCase().includes(q) || |
| 29 | + publication.authors.toLowerCase().includes(q) || |
| 30 | + publication.venue.toLowerCase().includes(q) || |
| 31 | + publication.abstract.toLowerCase().includes(q) |
| 32 | + ); |
18 | 33 | } |
19 | 34 |
|
20 | 35 | export default function PublicationsPage() { |
21 | 36 | const searchParams = useSearchParams(); |
| 37 | + const directionTag = searchParams.get("tag") as Tag | null; |
22 | 38 |
|
23 | | - const direction = searchParams.get("tag"); |
24 | | - const direction_tag = direction as Tag; |
25 | | - let publications_filtered = publications; |
| 39 | + const [searchQuery, setSearchQuery] = useState(""); |
| 40 | + const [selectedTags, setSelectedTags] = useState<Tag[]>( |
| 41 | + directionTag ? [directionTag] : [] |
| 42 | + ); |
26 | 43 |
|
27 | | - if (direction !== null) { |
28 | | - publications_filtered = publications.filter((publication) => |
29 | | - publication.tags.includes(direction_tag), |
| 44 | + const toggleTag = (tag: Tag) => { |
| 45 | + setSelectedTags((prev) => |
| 46 | + prev.includes(tag) ? prev.filter((t) => t !== tag) : [...prev, tag] |
30 | 47 | ); |
31 | | - } |
| 48 | + }; |
| 49 | + |
| 50 | + const clearFilters = () => { |
| 51 | + setSearchQuery(""); |
| 52 | + setSelectedTags([]); |
| 53 | + }; |
| 54 | + |
| 55 | + const filteredPublications = useMemo(() => { |
| 56 | + let result = publications; |
| 57 | + |
| 58 | + if (selectedTags.length > 0) { |
| 59 | + result = result.filter((pub) => |
| 60 | + selectedTags.some((tag) => pub.tags.includes(tag)) |
| 61 | + ); |
| 62 | + } |
| 63 | + |
| 64 | + if (searchQuery.trim()) { |
| 65 | + result = result.filter((pub) => matchesSearch(pub, searchQuery)); |
| 66 | + } |
| 67 | + |
| 68 | + return result; |
| 69 | + }, [searchQuery, selectedTags]); |
| 70 | + |
| 71 | + const hasActiveFilters = searchQuery.trim() !== "" || selectedTags.length > 0; |
32 | 72 |
|
33 | 73 | return ( |
34 | 74 | <div> |
35 | | - <ul> |
36 | | - {publications_filtered.map((publication) => ( |
37 | | - <li key={publication.title}> |
38 | | - <div className="my-4"> |
39 | | - <div className="flex flex-row gap-4"> |
40 | | - <div> |
41 | | - <PublicationTags tags={publication.tags} /> |
42 | | - </div> |
43 | | - <div> |
| 75 | + {/* Search & Filter Section */} |
| 76 | + <div className="mb-8 space-y-4"> |
| 77 | + <Input |
| 78 | + isClearable |
| 79 | + placeholder="Search by title, author, venue, or keywords..." |
| 80 | + size="lg" |
| 81 | + value={searchQuery} |
| 82 | + onClear={() => setSearchQuery("")} |
| 83 | + onValueChange={setSearchQuery} |
| 84 | + startContent={ |
| 85 | + <svg |
| 86 | + className="w-5 h-5 text-default-400 flex-shrink-0" |
| 87 | + fill="none" |
| 88 | + stroke="currentColor" |
| 89 | + strokeWidth={2} |
| 90 | + viewBox="0 0 24 24" |
| 91 | + > |
| 92 | + <path |
| 93 | + d="M21 21l-4.35-4.35M11 19a8 8 0 100-16 8 8 0 000 16z" |
| 94 | + strokeLinecap="round" |
| 95 | + strokeLinejoin="round" |
| 96 | + /> |
| 97 | + </svg> |
| 98 | + } |
| 99 | + /> |
| 100 | + |
| 101 | + <div className="flex flex-wrap items-center gap-2"> |
| 102 | + <span className="text-sm text-default-500 mr-1">Filter by topic:</span> |
| 103 | + {allTags.map((tag) => ( |
| 104 | + <Chip |
| 105 | + key={tag} |
| 106 | + className="cursor-pointer select-none" |
| 107 | + variant={selectedTags.includes(tag) ? "solid" : "bordered"} |
| 108 | + color={selectedTags.includes(tag) ? "primary" : "default"} |
| 109 | + onClick={() => toggleTag(tag)} |
| 110 | + > |
| 111 | + {tag} |
| 112 | + </Chip> |
| 113 | + ))} |
| 114 | + {hasActiveFilters && ( |
| 115 | + <Chip |
| 116 | + className="cursor-pointer select-none" |
| 117 | + variant="light" |
| 118 | + color="danger" |
| 119 | + onClick={clearFilters} |
| 120 | + > |
| 121 | + Clear all |
| 122 | + </Chip> |
| 123 | + )} |
| 124 | + </div> |
| 125 | + |
| 126 | + {hasActiveFilters && ( |
| 127 | + <p className="text-sm text-default-400"> |
| 128 | + Showing {filteredPublications.length} of {publications.length} publications |
| 129 | + </p> |
| 130 | + )} |
| 131 | + </div> |
| 132 | + |
| 133 | + {/* Publications List */} |
| 134 | + {filteredPublications.length === 0 ? ( |
| 135 | + <div className="text-center py-12 text-default-400"> |
| 136 | + <p className="text-lg">No publications match your search.</p> |
| 137 | + <p className="text-sm mt-2">Try adjusting your filters or search terms.</p> |
| 138 | + </div> |
| 139 | + ) : ( |
| 140 | + <ul> |
| 141 | + {filteredPublications.map((publication) => ( |
| 142 | + <li key={publication.title}> |
| 143 | + <div className="my-4"> |
| 144 | + <div className="flex flex-row gap-4"> |
| 145 | + <div> |
| 146 | + <PublicationTags tags={publication.tags} /> |
| 147 | + </div> |
| 148 | + <div> |
44 | 149 | {publication.page ? ( |
45 | 150 | <Link href={`projects/${publication.page}`}> |
46 | 151 | <h2 className="font-bold text-xl">{publication.title}</h2> |
47 | 152 | </Link> |
48 | 153 | ) : ( |
49 | 154 | <h2 className="font-bold text-xl">{publication.title}</h2> |
50 | 155 | )} |
| 156 | + </div> |
| 157 | + </div> |
| 158 | + <div className="flex flex-col mx-4 my-2"> |
| 159 | + <p className="pb-1 text-sm font-bold"> |
| 160 | + {publication.venue} {" "} |
| 161 | + <Links |
| 162 | + paper={publication.paper} |
| 163 | + code={publication.code} |
| 164 | + /> |
| 165 | + </p> |
| 166 | + <p className="pb-1 text-sm">{publication.authors}</p> |
| 167 | + <p className="pb-1">{publication.abstract}</p> |
| 168 | + <p className="pb-1">{publication.impact}</p> |
51 | 169 | </div> |
52 | 170 | </div> |
53 | | - <div className="flex flex-col mx-4 my-2"> |
54 | | - <p className="pb-1 text-sm font-bold"> |
55 | | - {publication.venue} <Links paper={publication.paper} code={publication.code} page={publication.page} /> |
56 | | - </p> |
57 | | - <p className="pb-1 text-sm">{publication.authors}</p> |
58 | | - <p className="pb-1">{publication.abstract}</p> |
59 | | - <p className="pb-1">{publication.impact}</p> |
60 | | - </div> |
61 | | - </div> |
62 | | - </li> |
63 | | - ))} |
64 | | - </ul> |
| 171 | + </li> |
| 172 | + ))} |
| 173 | + </ul> |
| 174 | + )} |
65 | 175 | </div> |
66 | 176 | ); |
67 | 177 | } |
0 commit comments