From a2a41b5fb21ef0548ee6af354673e644e7194e94 Mon Sep 17 00:00:00 2001 From: Muragesh-24 Date: Mon, 25 May 2026 07:05:01 +0530 Subject: [PATCH] feat : added search feature --- src/app/dashboard/page.tsx | 210 ++++++++++++++---------- src/components/DashboardQuickSearch.tsx | 166 +++++++++++++++++++ 2 files changed, 286 insertions(+), 90 deletions(-) create mode 100644 src/components/DashboardQuickSearch.tsx diff --git a/src/app/dashboard/page.tsx b/src/app/dashboard/page.tsx index 61550aa9..fdd667e8 100644 --- a/src/app/dashboard/page.tsx +++ b/src/app/dashboard/page.tsx @@ -26,6 +26,7 @@ import Link from "next/link"; import PersonalRecords from "@/components/PersonalRecords"; import LocalCodingTime from "@/components/LocalCodingTime"; import RecentActivity from "@/components/RecentActivity"; +import DashboardQuickSearch from "@/components/DashboardQuickSearch"; import { authOptions } from "@/lib/auth"; import { getServerSession } from "next-auth"; import { redirect } from "next/navigation"; @@ -40,100 +41,129 @@ export default async function DashboardPage() { return (
-
- - Settings - - -
- - -
- -
- -
- -
- -
- -
- - {/* Row 1: Contribution graph + Streak + Local Coding Time */} -
-
- -
- + +
+ + Settings + + +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ + {/* Row 1: Contribution graph + Streak + Local Coding Time */} +
+
+
+ +
+
+ +
+
+ +
-
- + +
+
+ +
+
+ +
+
+
+ + {/* Row 2: PR metrics, community metrics, PR breakdown & Time Chart */} +
+
+ +
+
+ +
+
+ +
+
+
-
- - -
-
- - {/* Row 2: PR metrics, community metrics, PR breakdown & Time Chart */} -
- - - - -
- {/* Row 2b: Activity Ring Chart */} -
- -
- -
- -
- -
- -
- - {/* Row 3: Issue metrics + CI analytics */} -
-
- -
- -
- {/* Row 3b: Discussion activity */} -
- -
- - {/* Row 4: Pinned repositories */} -
- -
- - {/* Row 5: Inactive repository reminder */} -
- -
- - {/* Row 6: Top repos + Language breakdown + Goal tracker */} -
- - - -
- - {/* Row 7: Recent GitHub activity */} -
- -
+ {/* Row 2b: Activity Ring Chart */} +
+ +
+ +
+ +
+ +
+ +
+ + {/* Row 3: Issue metrics + CI analytics */} +
+
+ +
+
+ +
+
+ + {/* Row 3b: Discussion activity */} +
+ +
+ + {/* Row 4: Pinned repositories */} +
+ +
+ + {/* Row 5: Inactive repository reminder */} +
+ +
+ + {/* Row 6: Top repos + Language breakdown + Goal tracker */} +
+
+ +
+
+ +
+
+ +
+
+ + {/* Row 7: Recent GitHub activity */} +
+ +
+
); } diff --git a/src/components/DashboardQuickSearch.tsx b/src/components/DashboardQuickSearch.tsx new file mode 100644 index 00000000..03f7b726 --- /dev/null +++ b/src/components/DashboardQuickSearch.tsx @@ -0,0 +1,166 @@ +"use client"; + +import type { ReactNode } from "react"; +import { useEffect, useRef, useState } from "react"; + +const SEARCH_SELECTOR = "[data-dashboard-search-item]"; +const GROUP_SELECTOR = "[data-dashboard-search-group]"; + +function normalizeText(value: string): string { + return value.replace(/\s+/g, " ").trim().toLowerCase(); +} + +export default function DashboardQuickSearch({ children }: { children: ReactNode }) { + const [query, setQuery] = useState(""); + const [debouncedQuery, setDebouncedQuery] = useState(""); + const [visibleCount, setVisibleCount] = useState(0); + const [hasSearchableItems, setHasSearchableItems] = useState(false); + const contentRef = useRef(null); + + useEffect(() => { + const timeoutId = window.setTimeout(() => { + setDebouncedQuery(normalizeText(query)); + }, 180); + + return () => window.clearTimeout(timeoutId); + }, [query]); + + useEffect(() => { + const root = contentRef.current; + if (!root) return; + + const applyFilter = () => { + const items = Array.from(root.querySelectorAll(SEARCH_SELECTOR)); + const groups = Array.from(root.querySelectorAll(GROUP_SELECTOR)); + let visibleItems = 0; + let searchableItems = 0; + + for (const item of items) { + const searchableText = normalizeText( + item.dataset.dashboardSearchText ?? item.textContent ?? "" + ); + const canSearch = searchableText.length > 0; + if (canSearch) searchableItems += 1; + + const isVisible = + debouncedQuery.length === 0 || + (canSearch && searchableText.includes(debouncedQuery)); + + item.toggleAttribute("hidden", !isVisible); + item.setAttribute("aria-hidden", isVisible ? "false" : "true"); + + if (isVisible) { + visibleItems += 1; + } + } + + for (const group of groups) { + const groupItems = Array.from(group.querySelectorAll(SEARCH_SELECTOR)); + const groupIsVisible = + debouncedQuery.length === 0 || + groupItems.some((item) => !item.hasAttribute("hidden")); + + group.toggleAttribute("hidden", !groupIsVisible); + group.setAttribute("aria-hidden", groupIsVisible ? "false" : "true"); + } + + setVisibleCount(visibleItems); + setHasSearchableItems(searchableItems > 0); + }; + + applyFilter(); + + const observer = new MutationObserver(() => { + applyFilter(); + }); + + observer.observe(root, { + childList: true, + subtree: true, + characterData: true, + }); + + return () => observer.disconnect(); + }, [debouncedQuery]); + + useEffect(() => { + if (debouncedQuery.length === 0) return; + + const root = contentRef.current; + const target = root?.querySelector(`${SEARCH_SELECTOR}:not([hidden])`); + + if (!target) return; + + target.scrollIntoView({ behavior: "smooth", block: "start" }); + }, [debouncedQuery, visibleCount]); + + const showNoResults = debouncedQuery.length > 0 && hasSearchableItems && visibleCount === 0; + + return ( +
+
+
+
+

+ Dashboard Quick Search +

+

+ Search commits, goals, pull requests, repository names, and languages. +

+
+ +
+ +
+ + setQuery(event.target.value)} + placeholder="Search dashboard..." + className="h-9 w-full bg-transparent text-sm text-[var(--foreground)] outline-none placeholder:text-[var(--muted-foreground)]" + /> + {query.length > 0 && ( + + )} +
+
+
+ +

+ Use terms like commits, goals, pull requests, repository names, or language names. +

+ + {debouncedQuery.length > 0 && ( +

+ {showNoResults + ? "No matching dashboard items found" + : `${visibleCount} matching dashboard item${visibleCount === 1 ? "" : "s"} visible`} +

+ )} +
+ +
{children}
+
+ ); +}