Skip to content

Commit b6f0725

Browse files
committed
feat: search functionality.
1 parent 4114934 commit b6f0725

File tree

4 files changed

+166
-10
lines changed

4 files changed

+166
-10
lines changed

src/App.js

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,16 +8,21 @@ import ContactModal from './components/ContactModal';
88

99
function App() {
1010
const [isModalOpen, setIsModalOpen] = useState(false);
11+
const [isSearchVisible, setIsSearchVisible] = useState(false);
1112

1213
const toggleModal = () => {
1314
setIsModalOpen(!isModalOpen);
1415
};
1516

17+
const toggleSearch = () => {
18+
setIsSearchVisible(!isSearchVisible);
19+
};
20+
1621
return (
1722
<Router>
1823
<ScrollToTop />
1924
<ToastProvider>
20-
<Layout toggleModal={toggleModal}>
25+
<Layout toggleModal={toggleModal} isSearchVisible={isSearchVisible} toggleSearch={toggleSearch}>
2126
<AnimatedRoutes />
2227
</Layout>
2328
<ContactModal isOpen={isModalOpen} onClose={toggleModal} />

src/components/Layout.js

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,11 @@ import Footer from './Footer';
55
import DndNavbar from './DndNavbar';
66
import DndFooter from './DndFooter';
77
import { useLocation } from 'react-router-dom';
8+
import Search from './Search'; // Import the Search component
89

910
import { DndProvider } from '../context/DndContext';
1011

11-
const Layout = ({ children, toggleModal }) => {
12+
const Layout = ({ children, toggleModal, isSearchVisible, toggleSearch }) => {
1213
const [isSidebarOpen, setIsSidebarOpen] = useState(window.innerWidth > 768);
1314
const location = useLocation();
1415

@@ -47,7 +48,8 @@ const Layout = ({ children, toggleModal }) => {
4748
<Sidebar isOpen={isSidebarOpen} toggleSidebar={toggleSidebar} toggleModal={toggleModal} />
4849
<div
4950
className={`flex-1 flex flex-col transition-all duration-300 ${isSidebarOpen ? 'md:ml-64' : 'md:ml-0'}`}>
50-
<Navbar toggleSidebar={toggleSidebar} isSidebarOpen={isSidebarOpen} />
51+
<Navbar toggleSidebar={toggleSidebar} isSidebarOpen={isSidebarOpen} toggleSearch={toggleSearch} />
52+
{isSearchVisible && <Search isVisible={isSearchVisible} />}
5153
<main className="flex-grow">{children}</main>
5254
<Footer />
5355
</div>

src/components/Navbar.js

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
import React, { useState, useEffect } from 'react';
22
import { Link } from 'react-router-dom';
33
import Fez from './Fez';
4-
import { Sidebar, User, BookOpen } from '@phosphor-icons/react';
4+
import { Sidebar, User, BookOpen, MagnifyingGlass } from '@phosphor-icons/react';
55

6-
const Navbar = ({ toggleSidebar, isSidebarOpen }) => {
6+
const Navbar = ({ toggleSidebar, isSidebarOpen, toggleSearch }) => {
77
const [isScrolled, setIsScrolled] = useState(false);
88

99
useEffect(() => {
@@ -61,21 +61,28 @@ const Navbar = ({ toggleSidebar, isSidebarOpen }) => {
6161
</span>
6262
</div>
6363
)}
64-
<div className="hidden md:flex items-center space-x-6">
64+
<div className="flex items-center space-x-3 md:space-x-6">
6565
<Link
6666
to="/about"
67-
className="flex items-center space-x-3 text-gray-300 hover:text-white hover:bg-gray-800 px-3 py-2 rounded-md transition-colors"
67+
className="flex items-center space-x-1 text-gray-300 hover:text-white hover:bg-gray-800 px-2 py-2 rounded-md transition-colors"
6868
>
6969
<User size={24} />
70-
<span>About</span>
70+
<span className="md:hidden lg:inline">About</span>
7171
</Link>
7272
<Link
7373
to="/blog"
74-
className="flex items-center space-x-3 text-gray-300 hover:text-white hover:bg-gray-800 px-3 py-2 rounded-md transition-colors"
74+
className="flex items-center space-x-1 text-gray-300 hover:text-white hover:bg-gray-800 px-2 py-2 rounded-md transition-colors"
7575
>
7676
<BookOpen size={24} />
77-
<span>Blog</span>
77+
<span className="md:hidden lg:inline">Blog</span>
7878
</Link>
79+
<button
80+
onClick={toggleSearch}
81+
className="text-gray-300 hover:text-white hover:bg-gray-800 px-2 py-2 rounded-md transition-colors"
82+
aria-label="Toggle Search"
83+
>
84+
<MagnifyingGlass size={24} />
85+
</button>
7986
</div>
8087
</div>
8188
</header>

src/components/Search.js

Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
import React, { useState, useEffect, useRef } from 'react';
2+
import { Link } from 'react-router-dom';
3+
import { MagnifyingGlassIcon } from '@phosphor-icons/react';
4+
5+
const Search = ({ isVisible }) => {
6+
const [searchTerm, setSearchTerm] = useState('');
7+
const [searchResults, setSearchResults] = useState([]);
8+
const [isDropdownOpen, setIsDropdownOpen] = useState(false);
9+
const [data, setData] = useState({ posts: [], projects: [], logs: [] });
10+
const searchRef = useRef(null);
11+
12+
useEffect(() => {
13+
const fetchData = async () => {
14+
try {
15+
const [postsRes, projectsRes, logsRes] = await Promise.all([
16+
fetch('/posts/posts.json'),
17+
fetch('/projects/projects.json'),
18+
fetch('/logs/logs.json'),
19+
]);
20+
21+
const posts = await postsRes.json();
22+
const projects = await projectsRes.json();
23+
const logs = await logsRes.json();
24+
25+
const allPosts = posts.flatMap(item =>
26+
item.series ? item.series.posts.map(p => ({ ...p, series: item.title })) : item
27+
);
28+
29+
setData({ posts: allPosts, projects, logs });
30+
} catch (error) {
31+
console.error('Failed to fetch search data:', error);
32+
}
33+
};
34+
35+
fetchData();
36+
}, []);
37+
38+
useEffect(() => {
39+
if (searchTerm) {
40+
const lowerCaseSearchTerm = searchTerm.toLowerCase();
41+
const posts = data.posts
42+
.filter(
43+
(post) =>
44+
post.title.toLowerCase().includes(lowerCaseSearchTerm) ||
45+
post.tags?.some((tag) => tag.toLowerCase().includes(lowerCaseSearchTerm))
46+
)
47+
.map((post) => ({ ...post, type: 'post' }));
48+
49+
const projects = data.projects
50+
.filter(
51+
(project) =>
52+
project.title.toLowerCase().includes(lowerCaseSearchTerm) ||
53+
project.technologies?.some((tech) => tech.toLowerCase().includes(lowerCaseSearchTerm))
54+
)
55+
.map((project) => ({ ...project, type: 'project' }));
56+
57+
const logs = data.logs
58+
.filter(
59+
(log) =>
60+
log.title.toLowerCase().includes(lowerCaseSearchTerm) ||
61+
log.category?.toLowerCase().includes(lowerCaseSearchTerm) ||
62+
log.tags?.some((tag) => tag.toLowerCase().includes(lowerCaseSearchTerm))
63+
)
64+
.map((log) => ({ ...log, type: 'log' }));
65+
66+
setSearchResults([...posts, ...projects, ...logs]);
67+
setIsDropdownOpen(true);
68+
} else {
69+
setSearchResults([]);
70+
setIsDropdownOpen(false);
71+
}
72+
}, [searchTerm, data]);
73+
74+
useEffect(() => {
75+
const handleClickOutside = (event) => {
76+
if (searchRef.current && !searchRef.current.contains(event.target)) {
77+
setIsDropdownOpen(false);
78+
}
79+
};
80+
81+
document.addEventListener('mousedown', handleClickOutside);
82+
return () => {
83+
document.removeEventListener('mousedown', handleClickOutside);
84+
};
85+
}, []);
86+
87+
const getResultLink = (result) => {
88+
switch (result.type) {
89+
case 'post':
90+
return `/blog/${result.slug}`;
91+
case 'project':
92+
return `/projects/${result.slug}`;
93+
case 'log':
94+
return `/logs/${result.slug}`;
95+
default:
96+
return '/';
97+
}
98+
};
99+
100+
if (!isVisible) {
101+
return null;
102+
}
103+
104+
return (
105+
<div ref={searchRef} className="w-full bg-gray-900 py-3 px-4 border-b border-gray-700">
106+
<form onSubmit={(e) => e.preventDefault()} className="relative w-full max-w-md mx-auto">
107+
<input
108+
type="text"
109+
placeholder="Search..."
110+
value={searchTerm}
111+
onChange={(e) => setSearchTerm(e.target.value)}
112+
onFocus={() => setIsDropdownOpen(true)}
113+
className="bg-gray-800 text-white w-full py-2 px-4 pl-10 focus:outline-none focus:ring-2 focus:ring-primary-400 rounded-md"
114+
/>
115+
<MagnifyingGlassIcon className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400" />
116+
{isDropdownOpen && searchResults.length > 0 && (
117+
<div className="absolute mt-2 w-full max-w-md max-h-96 overflow-y-auto bg-gray-800 border border-gray-700 rounded-md shadow-lg z-50 left-1/2 -translate-x-1/2">
118+
<ul>
119+
{searchResults.map((result, index) => (
120+
<li key={index}>
121+
<Link
122+
to={getResultLink(result)}
123+
onClick={() => {
124+
setSearchTerm('');
125+
setIsDropdownOpen(false);
126+
}}
127+
className="block px-4 py-2 text-white hover:bg-gray-700"
128+
>
129+
<span className="font-bold">{result.title}</span>
130+
<span className="text-sm text-gray-400 ml-2">({result.type})</span>
131+
</Link>
132+
</li>
133+
))}
134+
</ul>
135+
</div>
136+
)}
137+
</form>
138+
</div>
139+
);
140+
};
141+
142+
export default Search;

0 commit comments

Comments
 (0)