From e556ec704d4eee7b62ebd5bae49161a1468ff52f Mon Sep 17 00:00:00 2001 From: mrdeyroy Date: Fri, 22 May 2026 21:46:21 +0530 Subject: [PATCH] feat: redesign blog search with real-time suggestions and tags filtering --- src/components/BlogSearch/BlogSearch.css | 397 +++++++++++++++++++++++ src/components/BlogSearch/index.tsx | 282 ++++++++++++++++ src/pages/blogs/blogs-new.css | 118 +------ src/pages/blogs/index.tsx | 48 +-- src/utils/blogFilters.ts | 25 +- 5 files changed, 705 insertions(+), 165 deletions(-) create mode 100644 src/components/BlogSearch/BlogSearch.css create mode 100644 src/components/BlogSearch/index.tsx diff --git a/src/components/BlogSearch/BlogSearch.css b/src/components/BlogSearch/BlogSearch.css new file mode 100644 index 00000000..192b5503 --- /dev/null +++ b/src/components/BlogSearch/BlogSearch.css @@ -0,0 +1,397 @@ +.blog-search-wrapper { + position: relative; + width: min(100%, 700px); + margin: 0 auto; + z-index: 50; + font-family: "Inter", sans-serif; +} + +.blog-search-input-container { + position: relative; + display: flex; + align-items: center; + background: #ffffff; + border: 2px solid rgba(99, 102, 241, 0.18); + border-radius: 14px; + transition: all 0.2s ease; + box-shadow: 0 10px 30px rgba(99, 102, 241, 0.08); +} + +[data-theme="dark"] .blog-search-input-container { + background: #1e293b; + border-color: #334155; + box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3); +} + +.blog-search-input-container:focus-within, +.blog-search-input-container.dropdown-open { + border-color: #6366f1; + box-shadow: 0 0 0 4px rgba(99, 102, 241, 0.15); +} + +[data-theme="dark"] .blog-search-input-container:focus-within, +[data-theme="dark"] .blog-search-input-container.dropdown-open { + border-color: #8b5cf6; + box-shadow: 0 0 0 4px rgba(139, 92, 246, 0.15); +} + +.blog-search-icon { + position: absolute; + left: 20px; + width: 20px; + height: 20px; + color: #64748b; + pointer-events: none; +} + +[data-theme="dark"] .blog-search-icon { + color: #94a3b8; +} + +.blog-search-input { + width: 100%; + height: 56px; + padding: 0 50px 0 52px; + background: transparent; + border: none; + font-size: 16px; + font-weight: 500; + color: #0f172a; + outline: none; +} + +[data-theme="dark"] .blog-search-input { + color: #f8fafc; +} + +.blog-search-input::placeholder { + color: #94a3b8; +} + +.blog-search-clear { + position: absolute; + right: 16px; + display: flex; + align-items: center; + justify-content: center; + width: 28px; + height: 28px; + background: transparent; + border: none; + color: #64748b; + cursor: pointer; + border-radius: 50%; + transition: background 0.2s, color 0.2s; +} + +.blog-search-clear:hover { + background: #f1f5f9; + color: #0f172a; +} + +[data-theme="dark"] .blog-search-clear:hover { + background: #334155; + color: #f8fafc; +} + +.blog-search-clear svg { + width: 18px; + height: 18px; +} + +/* Dropdown styling */ +.blog-search-dropdown { + position: absolute; + top: calc(100% + 8px); + left: 0; + right: 0; + background: #ffffff; + border-radius: 16px; + box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1); + border: 1px solid #e2e8f0; + padding: 16px; + overflow: hidden; + animation: dropdownSlideDown 0.2s ease-out; + text-align: left; +} + +[data-theme="dark"] .blog-search-dropdown { + background: #1e293b; + border-color: #334155; + box-shadow: 0 20px 40px rgba(0, 0, 0, 0.4); +} + +@keyframes dropdownSlideDown { + from { + opacity: 0; + transform: translateY(-10px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +.blog-search-no-results { + padding: 24px; + text-align: center; + color: #64748b; + font-size: 15px; +} + +[data-theme="dark"] .blog-search-no-results { + color: #94a3b8; +} + +.blog-search-section { + margin-bottom: 20px; +} + +.blog-search-section:last-child { + margin-bottom: 0; +} + +.flex-1 { + flex: 1; + min-width: 0; +} + +.blog-search-section-title { + font-size: 11px; + font-weight: 700; + color: #64748b; + letter-spacing: 0.05em; + text-transform: uppercase; + margin: 0 0 12px 4px; +} + +[data-theme="dark"] .blog-search-section-title { + color: #94a3b8; +} + +/* Articles */ +.blog-search-articles { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 12px; +} + +.blog-search-article-card { + display: flex; + flex-direction: column; + background: #f8fafc; + border: 1px solid #e2e8f0; + border-radius: 12px; + overflow: hidden; + text-decoration: none; + transition: transform 0.2s, box-shadow 0.2s, border-color 0.2s; + cursor: pointer; +} + +[data-theme="dark"] .blog-search-article-card { + background: #0f172a; + border-color: #334155; +} + +.blog-search-article-card:hover, +.blog-search-article-card.focused { + transform: translateY(-2px); + box-shadow: 0 8px 20px rgba(0, 0, 0, 0.08); + border-color: #cbd5e1; + text-decoration: none; +} + +[data-theme="dark"] .blog-search-article-card:hover, +[data-theme="dark"] .blog-search-article-card.focused { + box-shadow: 0 8px 20px rgba(0, 0, 0, 0.3); + border-color: #475569; +} + +.blog-search-article-img { + width: 100%; + height: 80px; + overflow: hidden; +} + +.blog-search-article-img img { + width: 100%; + height: 100%; + object-fit: cover; + transition: transform 0.3s ease; +} + +.blog-search-article-card:hover .blog-search-article-img img { + transform: scale(1.05); +} + +.blog-search-article-info { + padding: 10px 12px; +} + +.blog-search-article-info h5 { + margin: 0 0 6px 0; + font-size: 13px; + font-weight: 600; + color: #0f172a; + line-height: 1.3; + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + overflow: hidden; +} + +[data-theme="dark"] .blog-search-article-info h5 { + color: #f1f5f9; +} + +.blog-search-read-time { + display: flex; + align-items: center; + gap: 4px; + font-size: 11px; + color: #64748b; +} + +[data-theme="dark"] .blog-search-read-time { + color: #94a3b8; +} + +.blog-search-read-time svg { + width: 12px; + height: 12px; +} + +/* Bottom Sections (Tutorials & Tags) */ +.blog-search-bottom-sections { + display: flex; + gap: 24px; + padding-top: 16px; + border-top: 1px solid #e2e8f0; +} + +[data-theme="dark"] .blog-search-bottom-sections { + border-top-color: #334155; +} + +/* Tutorials */ +.blog-search-tutorials { + display: flex; + flex-direction: column; + gap: 6px; +} + +.blog-search-tutorial-item { + display: flex; + align-items: center; + gap: 12px; + padding: 8px 10px; + border-radius: 8px; + text-decoration: none; + transition: background 0.2s; +} + +.blog-search-tutorial-item:hover, +.blog-search-tutorial-item.focused { + background: #f1f5f9; + text-decoration: none; +} + +[data-theme="dark"] .blog-search-tutorial-item:hover, +[data-theme="dark"] .blog-search-tutorial-item.focused { + background: #334155; +} + +.blog-search-tutorial-icon { + display: flex; + align-items: center; + justify-content: center; + width: 32px; + height: 32px; + background: #e0e7ff; + color: #4f46e5; + border-radius: 8px; +} + +[data-theme="dark"] .blog-search-tutorial-icon { + background: #312e81; + color: #818cf8; +} + +.blog-search-tutorial-icon svg { + width: 18px; + height: 18px; +} + +.blog-search-tutorial-info h5 { + margin: 0 0 2px 0; + font-size: 13px; + font-weight: 500; + color: #0f172a; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + max-width: 180px; +} + +[data-theme="dark"] .blog-search-tutorial-info h5 { + color: #f1f5f9; +} + +/* Tags */ +.blog-search-tags { + display: flex; + flex-wrap: wrap; + gap: 8px; +} + +.blog-search-tag-pill { + display: inline-block; + padding: 6px 12px; + background: #f1f5f9; + color: #475569; + font-size: 12px; + font-weight: 500; + border-radius: 20px; + border: 1px solid transparent; + cursor: pointer; + transition: all 0.2s; +} + +[data-theme="dark"] .blog-search-tag-pill { + background: #334155; + color: #cbd5e1; +} + +.blog-search-tag-pill:hover, +.blog-search-tag-pill.focused { + background: #e2e8f0; + color: #0f172a; + border-color: #cbd5e1; +} + +[data-theme="dark"] .blog-search-tag-pill:hover, +[data-theme="dark"] .blog-search-tag-pill.focused { + background: #475569; + color: #f8fafc; + border-color: #64748b; +} + +/* Responsive */ +@media (max-width: 768px) { + .blog-search-articles { + grid-template-columns: repeat(2, 1fr); + } + .blog-search-bottom-sections { + flex-direction: column; + gap: 16px; + } +} + +@media (max-width: 480px) { + .blog-search-articles { + grid-template-columns: 1fr; + } + .blog-search-tutorial-info h5 { + max-width: 220px; + } +} diff --git a/src/components/BlogSearch/index.tsx b/src/components/BlogSearch/index.tsx new file mode 100644 index 00000000..3e6d3d9d --- /dev/null +++ b/src/components/BlogSearch/index.tsx @@ -0,0 +1,282 @@ +import React, { useState, useEffect, useRef, useMemo } from "react"; +import Link from "@docusaurus/Link"; +import { useHistory } from "@docusaurus/router"; +import blogs from "../../database/blogs/index"; +import "./BlogSearch.css"; + +interface BlogSearchProps { + initialSearchTerm?: string; + onSearchSubmit: (term: string) => void; +} + +export default function BlogSearch({ initialSearchTerm = "", onSearchSubmit }: BlogSearchProps) { + const [query, setQuery] = useState(initialSearchTerm); + const [debouncedQuery, setDebouncedQuery] = useState(initialSearchTerm); + const [isOpen, setIsOpen] = useState(false); + const [focusedIndex, setFocusedIndex] = useState(-1); + const wrapperRef = useRef(null); + const inputRef = useRef(null); + const history = useHistory(); + + // Debounce the search input + useEffect(() => { + const handler = setTimeout(() => { + setDebouncedQuery(query); + }, 300); + return () => clearTimeout(handler); + }, [query]); + + // Handle clicking outside to close + useEffect(() => { + function handleClickOutside(event: MouseEvent) { + if (wrapperRef.current && !wrapperRef.current.contains(event.target as Node)) { + setIsOpen(false); + } + } + document.addEventListener("mousedown", handleClickOutside); + return () => document.removeEventListener("mousedown", handleClickOutside); + }, []); + + const searchResults = useMemo(() => { + let q = debouncedQuery.toLowerCase().trim(); + const isTagSearch = q.startsWith("#"); + + if (isTagSearch) { + q = q.slice(1); + } + + if (!q) return { articles: [], tutorials: [], tags: [] }; + + const articles: typeof blogs = []; + const tutorials: typeof blogs = []; + const matchedTags = new Set(); + + blogs.forEach((blog) => { + let isMatch = false; + + if (!isTagSearch) { + if (blog.title.toLowerCase().includes(q)) isMatch = true; + if (blog.description.toLowerCase().includes(q)) isMatch = true; + } + + if (blog.category.toLowerCase().includes(q)) { + isMatch = true; + matchedTags.add(blog.category); + } + blog.tags?.forEach((tag) => { + if (tag.toLowerCase().includes(q)) { + isMatch = true; + matchedTags.add(tag); + } + }); + + if (isMatch) { + const isTutorial = + blog.category.toLowerCase().includes("tutorial") || + blog.title.toLowerCase().includes("tutorial") || + blog.title.toLowerCase().includes("how to") || + blog.title.toLowerCase().includes("guide") || + blog.tags?.some((t) => t.toLowerCase().includes("tutorial")); + + if (isTutorial) { + tutorials.push(blog); + } else { + articles.push(blog); + } + } + }); + + return { + articles: articles.slice(0, 3), + tutorials: tutorials.slice(0, 3), + tags: Array.from(matchedTags).slice(0, 5), + }; + }, [debouncedQuery]); + + const totalResults = searchResults.articles.length + searchResults.tutorials.length; + // Flatten items for keyboard navigation + const selectableItems = useMemo(() => { + const items: Array<{ type: string; url?: string; label: string }> = []; + searchResults.articles.forEach((a) => items.push({ type: "article", url: `/blog/${a.slug}`, label: a.title })); + searchResults.tutorials.forEach((t) => items.push({ type: "tutorial", url: `/blog/${t.slug}`, label: t.title })); + searchResults.tags.forEach((t) => items.push({ type: "tag", label: t })); + return items; + }, [searchResults]); + + useEffect(() => { + // Reset focus when query or items change + setFocusedIndex(-1); + }, [debouncedQuery, isOpen]); + + const handleKeyDown = (e: React.KeyboardEvent) => { + if (e.key === "Enter") { + e.preventDefault(); + if (focusedIndex >= 0 && focusedIndex < selectableItems.length) { + const item = selectableItems[focusedIndex]; + if (item.url) { + history.push(item.url); + setIsOpen(false); + } else if (item.type === "tag") { + setQuery(item.label); + onSearchSubmit(item.label); + setIsOpen(false); + } + } else { + onSearchSubmit(query.trim()); + setIsOpen(false); + } + } else if (e.key === "ArrowDown") { + e.preventDefault(); + setIsOpen(true); + setFocusedIndex((prev) => (prev < selectableItems.length - 1 ? prev + 1 : prev)); + } else if (e.key === "ArrowUp") { + e.preventDefault(); + setFocusedIndex((prev) => (prev > -1 ? prev - 1 : -1)); + } else if (e.key === "Escape") { + setIsOpen(false); + inputRef.current?.blur(); + } + }; + + const handleChange = (e: React.ChangeEvent) => { + setQuery(e.target.value); + setIsOpen(true); + }; + + const handleClear = () => { + setQuery(""); + setDebouncedQuery(""); + setIsOpen(false); + onSearchSubmit(""); + inputRef.current?.focus(); + }; + + return ( +
+
0 ? "dropdown-open" : ""}`}> + + + + + query.trim() && setIsOpen(true)} + onKeyDown={handleKeyDown} + aria-expanded={isOpen} + role="combobox" + aria-controls="blog-search-dropdown" + aria-autocomplete="list" + /> + {query && ( + + )} +
+ + {isOpen && query.trim() && ( +
+ {totalResults === 0 && searchResults.tags.length === 0 ? ( +
No results found for "{debouncedQuery}"
+ ) : ( + <> + {searchResults.articles.length > 0 && ( +
+

MATCHING ARTICLES

+
+ {searchResults.articles.map((article, idx) => { + const itemIndex = selectableItems.findIndex((item) => item.label === article.title); + return ( + setIsOpen(false)} + > +
+ {article.title} +
+
+
{article.title}
+ + + 5 min read + +
+ + ); + })} +
+
+ )} + +
+ {searchResults.tutorials.length > 0 && ( +
+

MATCHING TUTORIALS

+
+ {searchResults.tutorials.map((tutorial) => { + const itemIndex = selectableItems.findIndex((item) => item.label === tutorial.title); + return ( + setIsOpen(false)} + > +
+ +
+
+
{tutorial.title}
+ + + 5 min read + +
+ + ); + })} +
+
+ )} + + {searchResults.tags.length > 0 && ( +
+

MATCHING TAGS

+
+ {searchResults.tags.map((tag) => { + const itemIndex = selectableItems.findIndex((item) => item.label === tag); + return ( + + ); + })} +
+
+ )} +
+ + )} +
+ )} +
+ ); +} diff --git a/src/pages/blogs/blogs-new.css b/src/pages/blogs/blogs-new.css index 4fd738ba..889140fb 100644 --- a/src/pages/blogs/blogs-new.css +++ b/src/pages/blogs/blogs-new.css @@ -62,7 +62,7 @@ text-align: center; background: linear-gradient(135deg, #f8f9ff 0%, #f0f2ff 30%, #e8ebff 70%, #f0f4ff 100%); position: relative; - overflow: hidden; + z-index: 10; min-height: 150px; display: flex; align-items: center; @@ -550,123 +550,7 @@ color: #f8fafc; } -.blog-search-form { - display: grid; - grid-template-columns: minmax(0, 1fr) auto auto; - gap: 12px; - align-items: center; -} - -.blog-search-field { - position: relative; - display: flex; - align-items: center; - min-width: 0; -} - -.blog-search-field input { - width: 100%; - min-height: 56px; - padding: 0 18px 0 52px; - color: #0f172a; - background: #ffffff; - border: 2px solid rgba(99, 102, 241, 0.18); - border-radius: 14px; - font-size: 16px; - font-weight: 500; - outline: none; - transition: border-color 0.2s ease, box-shadow 0.2s ease, transform 0.2s ease; -} - -.blog-search-field input:focus { - border-color: #6366f1; - box-shadow: 0 0 0 4px rgba(99, 102, 241, 0.12); - transform: translateY(-1px); -} - -.blog-search-field input::placeholder { - color: #94a3b8; -} - -[data-theme="dark"] .blog-search-field input { - color: #f8fafc; - background: rgba(15, 23, 42, 0.82); - border-color: rgba(167, 139, 250, 0.24); -} - -[data-theme="dark"] .blog-search-field input:focus { - border-color: #a78bfa; - box-shadow: 0 0 0 4px rgba(167, 139, 250, 0.14); -} - -.blog-search-submit-icon { - position: absolute; - left: 18px; - width: 20px; - height: 20px; - color: #6366f1; - pointer-events: none; -} - -[data-theme="dark"] .blog-search-submit-icon { - color: #a78bfa; -} - -.blog-search-button, -.blog-search-clear-button { - min-height: 56px; - padding: 0 26px; - border: none; - border-radius: 14px; - font-size: 15px; - font-weight: 800; - cursor: pointer; - transition: transform 0.2s ease, box-shadow 0.2s ease, background 0.2s ease; -} - -.blog-search-button { - color: #ffffff; - background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%); - box-shadow: 0 12px 28px rgba(99, 102, 241, 0.28); -} - -.blog-search-button:hover { - transform: translateY(-2px); - box-shadow: 0 16px 34px rgba(99, 102, 241, 0.36); -} - -.blog-search-clear-button { - color: #475569; - background: rgba(226, 232, 240, 0.74); -} - -.blog-search-clear-button:hover { - color: #0f172a; - background: #e2e8f0; - transform: translateY(-2px); -} - -[data-theme="dark"] .blog-search-clear-button { - color: #e2e8f0; - background: rgba(71, 85, 105, 0.54); -} -[data-theme="dark"] .blog-search-clear-button:hover { - color: #ffffff; - background: rgba(100, 116, 139, 0.7); -} - -.blog-search-visually-hidden { - position: absolute; - width: 1px; - height: 1px; - padding: 0; - margin: -1px; - overflow: hidden; - clip: rect(0, 0, 0, 0); - white-space: nowrap; - border: 0; -} /* Search Results Info */ .search-results-info { diff --git a/src/pages/blogs/index.tsx b/src/pages/blogs/index.tsx index fd92169b..00786287 100644 --- a/src/pages/blogs/index.tsx +++ b/src/pages/blogs/index.tsx @@ -6,6 +6,7 @@ import blogs from "../../database/blogs/index"; import Head from "@docusaurus/Head"; import { getAuthorProfiles } from "../../utils/authors"; import { filterBlogsBySearchTerm } from "../../utils/blogFilters"; +import BlogSearch from "../../components/BlogSearch"; import "./blogs-new.css"; @@ -43,7 +44,6 @@ function formatDate(dateStr?: string) { export default function Blogs() { const { siteConfig } = useDocusaurusContext(); - const [searchInput, setSearchInput] = React.useState(""); const [searchTerm, setSearchTerm] = React.useState(""); const [currentPage, setCurrentPage] = React.useState(1); @@ -79,17 +79,7 @@ export default function Blogs() { const showLastPage = visiblePages[visiblePages.length - 1] < totalPages; - const handleSearchChange = (e: { target: { value: string } }) => { - setSearchInput(e.target.value); - }; - - const handleSearchSubmit = (e: { preventDefault: () => void }) => { - e.preventDefault(); - setSearchTerm(searchInput.trim()); - }; - const handleClearFilters = () => { - setSearchInput(""); setSearchTerm(""); }; @@ -133,6 +123,9 @@ export default function Blogs() { Engineering uptime{" "}

blog by recode community.

+
+ +
@@ -141,40 +134,9 @@ export default function Blogs() {
-
+

Explore articles

Find the right guide

-
- - - {searchTerm && ( - - )} -
{searchTerm && ( diff --git a/src/utils/blogFilters.ts b/src/utils/blogFilters.ts index 73ebc53c..a0e05be6 100644 --- a/src/utils/blogFilters.ts +++ b/src/utils/blogFilters.ts @@ -1,6 +1,7 @@ type BlogLike = { title: string; description: string; + category?: string; tags?: string[]; }; @@ -8,16 +9,30 @@ export function filterBlogsBySearchTerm( blogs: T[], searchTerm: string, ): T[] { - const normalizedSearch = searchTerm.trim().toLowerCase(); + let normalizedSearch = searchTerm.trim().toLowerCase(); + const isTagSearch = normalizedSearch.startsWith("#"); + + if (isTagSearch) { + normalizedSearch = normalizedSearch.slice(1); + } if (normalizedSearch === "") { return blogs; } - return blogs.filter( - (blog) => + return blogs.filter((blog) => { + const matchesTag = + blog.category?.toLowerCase().includes(normalizedSearch) || + blog.tags?.some((tag) => tag.toLowerCase().includes(normalizedSearch)); + + if (isTagSearch) { + return matchesTag; + } + + return ( blog.title.toLowerCase().includes(normalizedSearch) || blog.description.toLowerCase().includes(normalizedSearch) || - blog.tags?.some((tag) => tag.toLowerCase().includes(normalizedSearch)), - ); + matchesTag + ); + }); } \ No newline at end of file