Skip to content

Commit ff0c5a8

Browse files
committed
add: search bars for posts
1 parent ea2e572 commit ff0c5a8

2 files changed

Lines changed: 191 additions & 45 deletions

File tree

components/Articles/Articles.tsx

Lines changed: 54 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ export default function Articles() {
2626
const [articles, setArticles] = useState<Article[]>([]);
2727
const [labels, setLabels] = useState<string[]>([]);
2828
const [selectedLabel, setSelectedLabel] = useState("All");
29+
const [searchTerm, setSearchTerm] = useState("");
2930
const [loading, setLoading] = useState(true);
3031

3132
useEffect(() => {
@@ -51,83 +52,103 @@ export default function Articles() {
5152
}) || "No date"
5253
} as Article;
5354
})
54-
// Filter out articles that are not published
55+
// Filter out unpublished
5556
.filter(article => article.publish === true);
5657

57-
// Fetch authors and map to articles
58+
// Fetch authors
5859
const authorsSnapshot = await getDocs(collection(db, "authors"));
5960
const authors = authorsSnapshot.docs.map(doc => ({
6061
uid: doc.id,
6162
name: doc.data().name,
62-
...doc.data(),
6363
}));
6464

65-
const articlesWithAuthors = articlesData.map(article => ({
65+
const withAuthors = articlesData.map(article => ({
6666
...article,
6767
authorName:
6868
authors.find(a => a.uid === article.authorUID)?.name ||
6969
"Unknown Author"
7070
}));
7171

72-
// Get unique labels from the published articles
72+
// Unique labels
7373
const uniqueLabels = Array.from(
74-
new Set(articlesData.map(article => article.label))
74+
new Set(articlesData.map(a => a.label))
7575
);
7676
setLabels(["All", ...uniqueLabels]);
7777

78-
setArticles(articlesWithAuthors);
79-
setLoading(false);
78+
setArticles(withAuthors);
8079
} catch (error) {
8180
console.error("Error fetching data:", error);
81+
} finally {
8282
setLoading(false);
8383
}
8484
};
8585

8686
fetchData();
8787
}, []);
8888

89-
const filteredArticles =
90-
selectedLabel === "All"
91-
? articles
92-
: articles.filter(article => article.label === selectedLabel);
89+
if (loading) return <Loading />;
9390

94-
if (loading) {
95-
return <Loading />;
96-
}
91+
// First filter by label, then by search term (title OR description)
92+
const filtered = articles
93+
.filter(a =>
94+
selectedLabel === "All" ? true : a.label === selectedLabel
95+
)
96+
.filter(a =>
97+
a.title.toLowerCase().includes(searchTerm.toLowerCase()) ||
98+
a.description.toLowerCase().includes(searchTerm.toLowerCase())
99+
);
97100

98101
return (
99102
<div className="max-w-[95rem] w-full mx-auto">
100-
<div className="flex flex-wrap justify-between items-center gap-2 md:gap-0 my-6">
103+
{/* --- Search Bar --- */}
104+
<div className="my-6 ">
105+
<input
106+
type="text"
107+
placeholder="Search posts..."
108+
value={searchTerm}
109+
onChange={e => setSearchTerm(e.target.value)}
110+
className="w-full md:w-1/2 px-4 py-2 border border-white bg-transparent text-white placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-white transition"
111+
/>
112+
</div>
113+
114+
{/* --- Category Filters --- */}
115+
<div className="flex flex-wrap justify-between items-center gap-2 md:gap-0 mb-6">
101116
<p className="font-semibold uppercase">Categories</p>
102117
<div className="flex flex-wrap gap-2">
103-
{labels.map((label, index) => (
118+
{labels.map((label, i) => (
104119
<Button
105-
className={`px-3 py-2 bg-[#121212] text-white hover:bg-white hover:text-black border border-white rounded-full transition ease-in-out duration-300 ${
106-
label === selectedLabel ? "bg-white text-black" : "border-white"
107-
}`}
108-
key={index}
120+
key={i}
109121
onClick={() => setSelectedLabel(label)}
122+
className={`
123+
px-3 py-2 border rounded-full transition ease-in-out duration-300
124+
${
125+
label === selectedLabel
126+
? "bg-white text-black"
127+
: "bg-transparent text-white border-white hover:bg-white hover:text-black"
128+
}
129+
`}
110130
>
111131
{label}
112132
</Button>
113133
))}
114134
</div>
115135
</div>
116-
117-
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 border-collapse mb-48">
118-
{filteredArticles.map((article) => (
136+
137+
{/* --- Articles Grid --- */}
138+
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8 mb-48">
139+
{filtered.map(article => (
119140
<article className="border border-white p-8" key={article.id}>
120141
<div className="flex items-center justify-between">
121142
<time dateTime={article.date}>{article.date}</time>
122-
<span className="px-3 py-2 border border-white rounded-full">
123-
<p className="uppercase">{article.label}</p>
143+
<span className="px-3 py-2 border border-white rounded-full uppercase">
144+
{article.label}
124145
</span>
125146
</div>
126-
<Link href={`posts/${article.slug}`}>
147+
<Link href={`/posts/${article.slug}`}>
127148
<img
128-
className="w-full my-8 hover:scale-105 transition"
129149
src={article.img}
130150
alt={article.imgAlt}
151+
className="w-full my-8 hover:scale-105 transition"
131152
/>
132153
</Link>
133154
<h2 className="heading3-title mb-3">
@@ -148,6 +169,11 @@ export default function Articles() {
148169
</div>
149170
</article>
150171
))}
172+
{filtered.length === 0 && (
173+
<p className="col-span-full text-center text-gray-400">
174+
No articles found.
175+
</p>
176+
)}
151177
</div>
152178
</div>
153179
);

components/Header.tsx

Lines changed: 137 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,91 @@
1+
"use client";
2+
13
import Link from "next/link";
24
import menuLinks from "@/data/menu";
35
import SocialSharing from "./SocialSharing";
46
import { Sheet, SheetContent, SheetTrigger } from "./ui/sheet";
5-
import { RiInstagramLine, RiTwitterFill, RiYoutubeFill, RiGithubFill, RiTiktokFill, RiPatreonFill } from "react-icons/ri";
7+
import {
8+
RiInstagramLine,
9+
RiTwitterFill,
10+
RiYoutubeFill,
11+
RiGithubFill,
12+
RiTiktokFill,
13+
RiPatreonFill,
14+
} from "react-icons/ri";
15+
16+
import { useState, useEffect, useRef } from "react";
17+
import { collection, getDocs, query, orderBy } from "firebase/firestore";
18+
import { db } from "@/lib/firebase";
19+
20+
interface SearchItem {
21+
id: string;
22+
title: string;
23+
slug: string;
24+
img: string;
25+
imgAlt: string;
26+
}
627

728
export default function Header() {
29+
const [allArticles, setAllArticles] = useState<SearchItem[]>([]);
30+
const [searchTerm, setSearchTerm] = useState("");
31+
const [suggestions, setSuggestions] = useState<SearchItem[]>([]);
32+
const [isOpen, setIsOpen] = useState(false);
33+
const containerRef = useRef<HTMLDivElement>(null);
34+
35+
// Fetch minimal article data on mount
36+
useEffect(() => {
37+
(async () => {
38+
const q = query(collection(db, "articles"), orderBy("date", "desc"));
39+
const snap = await getDocs(q);
40+
const items = snap.docs
41+
// only published
42+
.filter((d) => (d.data() as any).publish === true)
43+
.map((d) => {
44+
const data = d.data() as any;
45+
return {
46+
id: d.id,
47+
title: data.title,
48+
slug: data.slug,
49+
img: data.img,
50+
imgAlt: data.imgAlt || data.title,
51+
} as SearchItem;
52+
});
53+
setAllArticles(items);
54+
})();
55+
}, []);
56+
57+
// update suggestions when searchTerm changes
58+
useEffect(() => {
59+
if (searchTerm.length >= 2) {
60+
const term = searchTerm.toLowerCase();
61+
setSuggestions(
62+
allArticles.filter((a) => a.title.toLowerCase().includes(term))
63+
);
64+
setIsOpen(true);
65+
} else {
66+
setIsOpen(false);
67+
setSuggestions([]);
68+
}
69+
}, [searchTerm, allArticles]);
70+
71+
// close dropdown when clicking outside
72+
useEffect(() => {
73+
const handleClick = (e: MouseEvent) => {
74+
if (
75+
containerRef.current &&
76+
!containerRef.current.contains(e.target as Node)
77+
) {
78+
setIsOpen(false);
79+
}
80+
};
81+
document.addEventListener("click", handleClick);
82+
return () => document.removeEventListener("click", handleClick);
83+
}, []);
84+
885
return (
986
<header className="flex flex-col justify-between max-w-[95rem] w-full mx-auto px-4 md:pt-8 pt-4 lg:pb-4 md:pb-4 sm:pb-2 xs:pb-2">
1087
<div className="flex">
88+
{/* Logo */}
1189
<div className="flex flex-1">
1290
<Link href="/" aria-label="Return to homepage">
1391
<img
@@ -17,8 +95,12 @@ export default function Header() {
1795
/>
1896
</Link>
1997
</div>
98+
99+
{/* Mobile menu */}
20100
<Sheet>
101+
21102
<SheetTrigger aria-labelledby="button-label">
103+
22104
<span id="button-label" hidden>
23105
Menu
24106
</span>
@@ -41,13 +123,10 @@ export default function Header() {
41123
className="w-full pt-14"
42124
aria-label="Menu Toggle"
43125
>
44-
<nav
45-
className="flex flex-col flex-1 justify-end gap-6"
46-
aria-labelledby="mobile-nav"
47-
>
48-
{menuLinks.map((menuItem, index) => (
49-
<Link key={index} href={menuItem.href}>
50-
{menuItem.label}
126+
<nav className="flex flex-col flex-1 justify-end gap-6" aria-labelledby="mobile-nav">
127+
{menuLinks.map((m, i) => (
128+
<Link key={i} href={m.href}>
129+
{m.label}
51130
</Link>
52131
))}
53132
<svg
@@ -88,21 +167,58 @@ export default function Header() {
88167
},
89168
{
90169
href: "http://patreon.com/lap_mgmt",
91-
ariaLabel: "Visit our GitHub page",
170+
ariaLabel: "Visit our Patreon page",
92171
Icon: RiPatreonFill,
93172
},
94173
]}
95174
/>
96175
</nav>
97176
</SheetContent>
98177
</Sheet>
99-
<nav
100-
className="flex-1 items-center justify-end gap-6 hidden md:flex"
101-
aria-labelledby="desktop-nav"
102-
>
103-
{menuLinks.map((menuItem, index) => (
104-
<Link key={index} href={menuItem.href} className="hover:text-[#8a2be2] transition ease-in-out duration-300">
105-
{menuItem.label}
178+
179+
{/* Desktop nav */}
180+
<nav className="hidden md:flex flex-1 items-center justify-end gap-6" aria-labelledby="desktop-nav">
181+
{/* Search box */}
182+
<div ref={containerRef} className="relative ml-6 w-64">
183+
<input
184+
type="text"
185+
placeholder="Search…"
186+
value={searchTerm}
187+
onChange={(e) => setSearchTerm(e.target.value)}
188+
className="w-full px-3 py-2 bg-[#121212] text-white border border-white focus:outline-none"
189+
/>
190+
191+
{isOpen && suggestions.length > 0 && (
192+
<ul className="absolute right-0 mt-1 w-full bg-[#121212] border border-white/60 shadow-lg max-h-60 overflow-auto z-50">
193+
{suggestions.map((art) => (
194+
<li
195+
key={art.id}
196+
className="flex items-center gap-2 px-3 py-2 hover:bg-[#892be250] transition"
197+
>
198+
<img
199+
src={art.img}
200+
alt={art.imgAlt}
201+
className="w-10 h-10 object-cover rounded"
202+
/>
203+
<Link
204+
href={`/posts/${art.slug}`}
205+
onClick={() => setIsOpen(false)}
206+
className="truncate text-white"
207+
>
208+
{art.title}
209+
</Link>
210+
</li>
211+
))}
212+
</ul>
213+
)}
214+
</div>
215+
{menuLinks.map((m, i) => (
216+
<Link
217+
key={i}
218+
href={m.href}
219+
className="hover:text-[#8a2be2] transition ease-in-out duration-300"
220+
>
221+
{m.label}
106222
</Link>
107223
))}
108224
<svg
@@ -114,6 +230,7 @@ export default function Header() {
114230
>
115231
<rect width="15" height="1" fill="white" />
116232
</svg>
233+
117234
<SocialSharing
118235
links={[
119236
{
@@ -143,13 +260,16 @@ export default function Header() {
143260
},
144261
{
145262
href: "http://patreon.com/lap_mgmt",
146-
ariaLabel: "Visit our GitHub page",
263+
ariaLabel: "Visit our Patreon page",
147264
Icon: RiPatreonFill,
148265
},
149266
]}
150267
/>
151268
</nav>
269+
270+
152271
</div>
272+
153273
<hr className="border-white border-t-0 border mt-4" />
154274
</header>
155275
);

0 commit comments

Comments
 (0)