Reddit lets you search posts, filter by subreddit, and sort by popularity. Let's implement this!
The simplest approach - filter data on the client side.
1function PostsList() {
2 const [posts, setPosts] = useState<Post[]>([...]); // All posts
3 const [searchQuery, setSearchQuery] = useState("");
4
5 // Filtering
6 const filteredPosts = posts.filter(post =>
7 post.title.toLowerCase().includes(searchQuery.toLowerCase()) ||
8 post.content.toLowerCase().includes(searchQuery.toLowerCase())
9 );
10
11 return (
12 <div>
13 <input
14 type="text"
15 value={searchQuery}
16 onChange={(e) => setSearchQuery(e.target.value)}
17 placeholder="Search posts..."
18 className="w-full border rounded px-4 py-2 mb-4"
19 />
20
21 <div className="space-y-4">
22 {filteredPosts.length === 0 ? (
23 <p className="text-gray-500 text-center">No posts found</p>
24 ) : (
25 filteredPosts.map(post => <PostCard key={post.id} post={post} />)
26 )}
27 </div>
28 </div>
29 );
30}Pros:
Cons:
Let's add more filters:
1function PostsList() {
2 const [posts, setPosts] = useState<Post[]>([...]);
3 const [searchQuery, setSearchQuery] = useState("");
4 const [selectedCommunity, setSelectedCommunity] = useState<string | null>(null);
5 const [sortBy, setSortBy] = useState<"hot" | "new" | "top">("hot");
6
7 // Filtering + sorting
8 const filteredPosts = useMemo(() => {
9 let filtered = posts;
10
11 // 1. Search query
12 if (searchQuery) {
13 filtered = filtered.filter(post =>
14 post.title.toLowerCase().includes(searchQuery.toLowerCase())
15 );
16 }
17
18 // 2. Community filter
19 if (selectedCommunity) {
20 filtered = filtered.filter(post => post.community === selectedCommunity);
21 }
22
23 // 3. Sorting
24 if (sortBy === "hot") {
25 filtered = [...filtered].sort((a, b) => b.score - a.score);
26 } else if (sortBy === "new") {
27 filtered = [...filtered].sort((a, b) =>
28 new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()
29 );
30 } else if (sortBy === "top") {
31 filtered = [...filtered].sort((a, b) => b.upvotes - a.upvotes);
32 }
33
34 return filtered;
35 }, [posts, searchQuery, selectedCommunity, sortBy]);
36
37 return (
38 <div>
39 {/* Search */}
40 <input
41 type="text"
42 value={searchQuery}
43 onChange={(e) => setSearchQuery(e.target.value)}
44 placeholder="Search posts..."
45 className="w-full border rounded px-4 py-2 mb-4"
46 />
47
48 {/* Filters */}
49 <div className="flex gap-4 mb-4">
50 {/* Community select */}
51 <select
52 value={selectedCommunity || ""}
53 onChange={(e) => setSelectedCommunity(e.target.value || null)}
54 className="border rounded px-3 py-2"
55 >
56 <option value="">All Communities</option>
57 <option value="pirates">r/pirates</option>
58 <option value="treasure">r/treasure</option>
59 <option value="ships">r/ships</option>
60 </select>
61
62 {/* Sort buttons */}
63 <div className="flex gap-2">
64 <button
65 onClick={() => setSortBy("hot")}
66 className={`px-3 py-2 rounded ${sortBy === "hot" ? "bg-blue-600 text-white" : "bg-gray-200"}`}
67 >
68 🔥 Hot
69 </button>
70 <button
71 onClick={() => setSortBy("new")}
72 className={`px-3 py-2 rounded ${sortBy === "new" ? "bg-blue-600 text-white" : "bg-gray-200"}`}
73 >
74 🆕 New
75 </button>
76 <button
77 onClick={() => setSortBy("top")}
78 className={`px-3 py-2 rounded ${sortBy === "top" ? "bg-blue-600 text-white" : "bg-gray-200"}`}
79 >
80 ⭐ Top
81 </button>
82 </div>
83 </div>
84
85 {/* Results */}
86 <div className="space-y-4">
87 {filteredPosts.map(post => <PostCard key={post.id} post={post} />)}
88 </div>
89 </div>
90 );
91}useMemo caches the filtering - doesn't recalculate on every render!
Filters in the URL let users bookmark/share results:
1/posts?q=treasure&community=pirates&sort=hot1'use client'; // Client Component!
2
3import { useSearchParams, useRouter } from 'next/navigation';
4
5function PostsList() {
6 const searchParams = useSearchParams();
7 const router = useRouter();
8
9 const query = searchParams.get('q') || "";
10 const community = searchParams.get('community');
11 const sort = searchParams.get('sort') || "hot";
12
13 const updateFilters = (key: string, value: string | null) => {
14 const params = new URLSearchParams(searchParams);
15 if (value) {
16 params.set(key, value);
17 } else {
18 params.delete(key);
19 }
20 router.push(`?${params.toString()}`);
21 };
22
23 return (
24 <div>
25 <input
26 type="text"
27 value={query}
28 onChange={(e) => updateFilters('q', e.target.value || null)}
29 placeholder="Search posts..."
30 />
31
32 <select
33 value={community || ""}
34 onChange={(e) => updateFilters('community', e.target.value || null)}
35 >
36 <option value="">All Communities</option>
37 <option value="pirates">r/pirates</option>
38 </select>
39
40 {/* ... */}
41 </div>
42 );
43}Pros:
For large datasets, search on the server side:
app/api/search/route.ts:1import { NextRequest, NextResponse } from 'next/server';
2
3export async function GET(request: NextRequest) {
4 const searchParams = request.nextUrl.searchParams;
5 const query = searchParams.get('q') || "";
6 const community = searchParams.get('community');
7 const sort = searchParams.get('sort') || "hot";
8
9 // Database query (e.g. Prisma)
10 const posts = await prisma.post.findMany({
11 where: {
12 OR: [
13 { title: { contains: query, mode: 'insensitive' } },
14 { content: { contains: query, mode: 'insensitive' } }
15 ],
16 ...(community && { community })
17 },
18 orderBy: sort === "new"
19 ? { createdAt: 'desc' }
20 : { upvotes: 'desc' },
21 take: 50 // Limit
22 });
23
24 return NextResponse.json({ posts });
25}1function PostsList() {
2 const [posts, setPosts] = useState<Post[]>([]);
3 const [loading, setLoading] = useState(false);
4 const debouncedQuery = useDebounce(searchQuery, 500);
5
6 useEffect(() => {
7 setLoading(true);
8 fetch(`/api/search?q=${debouncedQuery}&community=${community}&sort=${sort}`)
9 .then(res => res.json())
10 .then(data => setPosts(data.posts))
11 .finally(() => setLoading(false));
12 }, [debouncedQuery, community, sort]);
13
14 if (loading) {
15 return <div>Loading...</div>;
16 }
17
18 return (
19 <div>
20 {posts.map(post => <PostCard key={post.id} post={post} />)}
21 </div>
22 );
23}Highlight found words:
1function highlightText(text: string, query: string) {
2 if (!query) return text;
3
4 const regex = new RegExp(`(${query})`, 'gi');
5 return text.replace(regex, '<mark>$1</mark>');
6}
7
8function PostCard({ post, searchQuery }: { post: Post; searchQuery: string }) {
9 return (
10 <div>
11 <h2
12 dangerouslySetInnerHTML={{
13 __html: highlightText(post.title, searchQuery)
14 }}
15 />
16 </div>
17 );
18}Note:
dangerouslySetInnerHTML only for sanitized content!Show suggestions while typing:
1function SearchWithSuggestions() {
2 const [query, setQuery] = useState("");
3 const [suggestions, setSuggestions] = useState<string[]>([]);
4 const debouncedQuery = useDebounce(query, 300);
5
6 useEffect(() => {
7 if (debouncedQuery) {
8 fetch(`/api/suggestions?q=${debouncedQuery}`)
9 .then(res => res.json())
10 .then(data => setSuggestions(data.suggestions));
11 } else {
12 setSuggestions([]);
13 }
14 }, [debouncedQuery]);
15
16 return (
17 <div className="relative">
18 <input
19 type="text"
20 value={query}
21 onChange={(e) => setQuery(e.target.value)}
22 placeholder="Search..."
23 className="w-full border rounded px-4 py-2"
24 />
25
26 {suggestions.length > 0 && (
27 <div className="absolute top-full left-0 right-0 bg-white border rounded shadow-lg mt-1">
28 {suggestions.map((suggestion, i) => (
29 <button
30 key={i}
31 onClick={() => {
32 setQuery(suggestion);
33 setSuggestions([]);
34 }}
35 className="w-full text-left px-4 py-2 hover:bg-gray-100"
36 >
37 🔍 {suggestion}
38 </button>
39 ))}
40 </div>
41 )}
42 </div>
43 );
44}✅ Client-side filtering -
array.filter()
✅ Multiple filters - search + community + sort
✅ useMemo - caches filtering
✅ URL Search Params - bookmarkable filters
✅ Server-side search - API endpoints for large datasets
✅ Debouncing - delay search by 500ms
✅ Highlighting - highlight found words
✅ Autocomplete - suggestions while typingSee you there! 🚀