-
Notifications
You must be signed in to change notification settings - Fork 126
added search bar and few other updates #201
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
Signed-off-by: Anushka Mukherjee <[email protected]>
Signed-off-by: Anushka Mukherjee <[email protected]>
- Resolved conflicts by accepting upstream changes for author handling improvements - Kept search functionality additions (SearchCommand component) - Updated package.json with search dependencies (cmdk, fuse.js) - Kept header.tsx with SearchCommand integration
Co-authored-by: anushka-codes1 <[email protected]>
…201/ Resolve conflicts for PR related to issue keploy#201
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull Request Overview
This PR introduces a searchable blog functionality in the website navbar using shadcn/ui Command component and Fuse.js for client-side fuzzy searching. Users can now quickly find blog posts by title, tags, or author using a keyboard shortcut (Cmd/Ctrl + K) or clicking the search button in the navbar.
- Added new API endpoint
/api/search-indexto fetch and cache all blog post data - Integrated shadcn/ui Dialog and Command components for search UI
- Implemented client-side fuzzy search with Fuse.js for fast search results
- Added keyboard shortcut support and responsive design for mobile/desktop
Reviewed Changes
Copilot reviewed 6 out of 7 changed files in this pull request and generated 8 comments.
Show a summary per file
| File | Description |
|---|---|
| pages/api/search-index.ts | New API endpoint that fetches all blog posts from WordPress GraphQL API with pagination and caching |
| package.json | Added dependencies: cmdk (^1.1.1), fuse.js (^7.1.0), and updated @radix-ui/react-dialog to ^1.1.15 |
| package-lock.json | Updated lock file with new dependencies and changed package name to "keploy-blog-website" |
| components/ui/dialog.tsx | New reusable Dialog component wrapper for Radix UI Dialog primitives |
| components/ui/command.tsx | New Command component providing search UI primitives from cmdk library |
| components/header.tsx | Integrated SearchCommand component in both desktop and mobile navigation |
| components/SearchCommand.tsx | Main search component implementing the search logic, keyboard shortcuts, and result rendering |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| if (process.env.WORDPRESS_AUTH_REFRESH_TOKEN) { | ||
| headers["Authorization"] = `Bearer ${process.env.WORDPRESS_AUTH_REFRESH_TOKEN}`; | ||
| } | ||
| const res = await fetch(API_URL as string, { |
Copilot
AI
Nov 11, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The API endpoint doesn't validate that API_URL is defined before using it. If both environment variables are undefined, this will attempt to fetch from undefined as string, which will fail. Consider adding validation similar to the pattern used in lib/api.ts which checks for undefined API_URL and handles it gracefully.
| } | ||
| `; | ||
|
|
||
| export default async function handler(req: NextApiRequest, res: NextApiResponse) { |
Copilot
AI
Nov 11, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The API endpoint doesn't restrict HTTP methods, allowing GET, PUT, DELETE, etc. requests when only POST is needed for internal API consumption. Consider adding a method check at the beginning of the handler (e.g., if (req.method !== 'GET') return res.status(405).json({ error: 'Method not allowed' })) to follow REST API best practices.
| export default async function handler(req: NextApiRequest, res: NextApiResponse) { | |
| export default async function handler(req: NextApiRequest, res: NextApiResponse) { | |
| if (req.method !== "GET") { | |
| res.setHeader("Allow", "GET"); | |
| return res.status(405).json({ error: "Method not allowed" }); | |
| } |
| {results.map(({ item }) => { | ||
| const href = `${item.isCommunity ? "/community" : "/technology"}/${item.slug}`; | ||
| return ( | ||
| <CommandItem key={item.slug} className="flex flex-col items-start"> |
Copilot
AI
Nov 11, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Using item.slug as the key assumes all slugs are unique across both community and technology categories. If a slug appears in both categories, this will cause React key conflicts. Consider using a composite key like key={${item.isCommunity ? 'community' : 'technology'}-${item.slug}} to ensure uniqueness.
| <CommandItem key={item.slug} className="flex flex-col items-start"> | |
| <CommandItem key={`${item.isCommunity ? 'community' : 'technology'}-${item.slug}`} className="flex flex-col items-start"> |
| <CommandDialog open={open} onOpenChange={setOpen}> | ||
| <CommandInput | ||
| value={query} | ||
| onValueChange={setQuery} | ||
| placeholder={loading ? "Loading index..." : "Search by title, tag, or author"} | ||
| /> | ||
| <CommandList> | ||
| <CommandEmpty>{loading ? "Loading..." : "No results found."}</CommandEmpty> | ||
|
|
||
| {results.length > 0 && ( | ||
| <> | ||
| <CommandGroup heading="Results"> | ||
| {results.map(({ item }) => { | ||
| const href = `${item.isCommunity ? "/community" : "/technology"}/${item.slug}`; | ||
| return ( | ||
| <CommandItem key={item.slug} className="flex flex-col items-start"> | ||
| <Link href={href} className="w-full"> | ||
| <div className="flex items-center justify-between w-full"> | ||
| <span className="font-medium text-black" dangerouslySetInnerHTML={{ __html: item.title }} /> | ||
| <span className="text-[11px] text-zinc-500">{item.isCommunity ? "community" : "technology"}</span> | ||
| </div> | ||
| <div className="mt-1 text-xs text-zinc-600"> | ||
| {item.author} | ||
| {item.tags?.length ? ` • ${item.tags.slice(0, 3).join(", ")}` : ""} | ||
| </div> | ||
| </Link> | ||
| </CommandItem> | ||
| ); | ||
| })} | ||
| </CommandGroup> | ||
| <CommandSeparator /> | ||
| </> | ||
| )} | ||
| </CommandList> | ||
| </CommandDialog> |
Copilot
AI
Nov 11, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The dialog is missing an accessible title. Radix UI Dialog requires either a DialogTitle or aria-label/aria-labelledby for screen readers. Consider adding <DialogTitle className="sr-only">Search Blog Posts</DialogTitle> inside the DialogContent to improve accessibility.
| while (hasNextPage) { | ||
| const data = await fetchAPI(QUERY, { after }); | ||
| const edges = data?.posts?.edges ?? []; | ||
| for (const { node } of edges) { | ||
| const categories = (node.categories?.edges ?? []).map((e: any) => e.node?.name).filter(Boolean); | ||
| const tags = (node.tags?.edges ?? []).map((e: any) => e.node?.name).filter(Boolean); | ||
| items.push({ | ||
| title: node.title, | ||
| slug: node.slug, | ||
| author: node.ppmaAuthorName || node.author?.node?.name || "Anonymous", | ||
| tags, | ||
| categories, | ||
| isCommunity: categories.includes("community"), | ||
| }); | ||
| } | ||
| hasNextPage = data?.posts?.pageInfo?.hasNextPage ?? false; | ||
| after = data?.posts?.pageInfo?.endCursor ?? null; | ||
| } |
Copilot
AI
Nov 11, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The while loop that fetches paginated data has no upper bound, which could cause excessive API calls or infinite loops if the WordPress API returns inconsistent pagination data. Consider adding a maximum iteration limit (e.g., let iterations = 0; const MAX_ITERATIONS = 100;) to prevent potential runaway loops.
| <CommandItem key={item.slug} className="flex flex-col items-start"> | ||
| <Link href={href} className="w-full"> | ||
| <div className="flex items-center justify-between w-full"> | ||
| <span className="font-medium text-black" dangerouslySetInnerHTML={{ __html: item.title }} /> | ||
| <span className="text-[11px] text-zinc-500">{item.isCommunity ? "community" : "technology"}</span> | ||
| </div> | ||
| <div className="mt-1 text-xs text-zinc-600"> | ||
| {item.author} | ||
| {item.tags?.length ? ` • ${item.tags.slice(0, 3).join(", ")}` : ""} | ||
| </div> | ||
| </Link> | ||
| </CommandItem> |
Copilot
AI
Nov 11, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Wrapping a Link inside a CommandItem can cause issues with keyboard navigation and clicking. The CommandItem component from cmdk handles its own interactions, and having a Link inside may interfere with the command palette's built-in navigation. Consider using the onSelect prop on CommandItem to handle navigation instead: <CommandItem key={...} onSelect={() => router.push(href)}>
| <DialogPrimitive.Content | ||
| ref={ref} | ||
| className={cn( | ||
| "fixed z-50 grid w-full max-w-lg gap-4 rounded-lg border bg-white p-6 shadow-lg duration-200 sm:zoom-in-90 left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2", |
Copilot
AI
Nov 11, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The class sm:zoom-in-90 appears to be a custom animation class that may not exist in the project's Tailwind configuration. This could result in no animation effect being applied. Verify that this class is defined in your Tailwind config or consider using standard Tailwind animation classes like sm:animate-in sm:fade-in-0 sm:zoom-in-95.
| "fixed z-50 grid w-full max-w-lg gap-4 rounded-lg border bg-white p-6 shadow-lg duration-200 sm:zoom-in-90 left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2", | |
| "fixed z-50 grid w-full max-w-lg gap-4 rounded-lg border bg-white p-6 shadow-lg duration-200 sm:animate-in sm:fade-in-0 sm:zoom-in-95 left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2", |
| .catch((err) => { | ||
| if (err?.name !== "AbortError") { | ||
| // eslint-disable-next-line no-console | ||
| console.error(err); | ||
| } | ||
| }) | ||
| .finally(() => { | ||
| if (isMounted) setLoading(false); | ||
| }); |
Copilot
AI
Nov 11, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When the fetch fails (non-AbortError), the error is logged but the user sees no feedback. The loading state remains false and the search shows "No results found" which is misleading. Consider setting an error state and displaying a more informative message like "Failed to load search index. Please try again later."
|
Hey @anushka-codes1 Thanks for raising this pr, can you please take a look at the copilot review once? Also, can you please add screenshots and previews for the same? There are chances that you might be using the same shadcn component being used in one of the other prs updating the navbar: #204 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull Request Overview
Copilot reviewed 6 out of 7 changed files in this pull request and generated 8 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| .catch((err) => { | ||
| if (err?.name !== "AbortError") { | ||
| // eslint-disable-next-line no-console | ||
| console.error(err); | ||
| } | ||
| }) |
Copilot
AI
Nov 13, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The fetch error handling silently swallows all non-AbortError exceptions. If the API call fails (network error, 500 response, etc.), users won't see any feedback. Consider setting an error state to display to users:
const [error, setError] = useState<string | null>(null);
// In the fetch catch block:
.catch((err) => {
if (err?.name !== "AbortError") {
console.error(err);
setError("Failed to load search index. Please try again later.");
}
})
// Then show the error in the UI| const buttonClasses = | ||
| "inline-flex items-center gap-2 h-9 px-3 rounded-md border bg-white text-sm text-zinc-600 hover:bg-zinc-100 transition"; |
Copilot
AI
Nov 13, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[nitpick] The buttonClasses variable is defined inside the component body but never changes. This should be moved outside the component or use useMemo to avoid recreating the string on every render:
const buttonClasses = useMemo(() =>
"inline-flex items-center gap-2 h-9 px-3 rounded-md border bg-white text-sm text-zinc-600 hover:bg-zinc-100 transition",
[]
);Or simply move it outside the component:
const BUTTON_CLASSES = "inline-flex items-center gap-2 h-9 px-3 rounded-md border bg-white text-sm text-zinc-600 hover:bg-zinc-100 transition";Co-authored-by: Copilot <[email protected]> Signed-off-by: Anushka Mukherjee <[email protected]>
Co-authored-by: Copilot <[email protected]> Signed-off-by: Anushka Mukherjee <[email protected]>
Co-authored-by: Copilot <[email protected]> Signed-off-by: Anushka Mukherjee <[email protected]>
PR Title
Added searchable blog functionality in navbar using shadcn/ui Command and fuse.js
Fixes: #3081
Description
This PR introduces a search feature in the website navbar, allowing users to quickly find blog posts by title, tags, or author without manually scrolling through the list.
The search experience uses shadcn/ui’s Command component for a modern, responsive, and accessible UX.
For smaller datasets (<500 blogs), search is performed client-side using Fuse.js for lightweight fuzzy searching.
Changes
Added a search icon + expandable Command input in the navbar.
Created a new component: components/blog-search.tsx.
Integrated Fuse.js for fuzzy search over blog titles, tags, and authors.
Styled according to Keploy.io design guidelines (dark theme, accent highlights).
Implemented keyboard shortcuts (Cmd + K / Ctrl + K) to open search.
Ensured mobile and desktop responsiveness.
Added links in results to corresponding /blog/[slug] pages.
Type of Change
New feature (change that adds functionality)
UI improvement (visual or design changes)
Testing
✅ Verified that search results update dynamically on each keystroke.
✅ Tested that all results correctly navigate to their respective blog pages.
✅ Checked responsiveness across mobile, tablet, and desktop.
✅ Tested fuzzy search (partial matches return expected results).
✅ Verified keyboard accessibility (arrow navigation + enter to open post).
Demo
Before:
No search functionality. Users manually scroll through blog list.
After:
Added global search modal accessible via navbar or keyboard shortcut.
Instant client-side filtering by title, tags, or author.
Environment and Dependencies
New Dependencies:
fuse.js – Lightweight fuzzy search library.
@/components/ui/command – From shadcn/ui for search UI.
Configuration Changes:
None required. Compatible with existing Next.js setup.
Checklist
My code follows the style guidelines of this project
I have performed a self-review of my own code
I have made corresponding changes to the documentation
I have added corresponding tests or manual verifications
I have run the build command to ensure there are no build errors
My changes have been tested across relevant browsers/devices
For UI changes, I've included visual evidence of my changes