Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
210 changes: 120 additions & 90 deletions src/app/dashboard/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -40,100 +41,129 @@ export default async function DashboardPage() {
return (
<div className="min-h-screen bg-[var(--background)] p-4 md:p-8 text-[var(--foreground)] transition-colors">
<DashboardHeader />
<div className="mb-6 flex justify-end items-center gap-2">
<Link
href="/dashboard/settings"
className="rounded-lg border border-[var(--border)] bg-[var(--control)] px-4 py-2 text-sm text-[var(--foreground)] hover:opacity-90 transition-opacity min-w-[140px] flex items-center justify-center"
>
Settings
</Link>
<ExportButton />
</div>
<StreakAtRiskBanner />

<div className="mb-6">
<WeeklySummaryCard />
</div>

<div className="mb-6">
<AIMentorWidget />
</div>

<div className="mb-6">
<PersonalRecords />
</div>

{/* Row 1: Contribution graph + Streak + Local Coding Time */}
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
<div className="lg:col-span-2">
<ContributionGraph />
<div className="mt-6">
<ContributionHeatmap />
<DashboardQuickSearch>
<div className="mb-6 flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-end">
<Link
href="/dashboard/settings"
className="inline-flex min-w-[140px] items-center justify-center rounded-lg border border-[var(--border)] bg-[var(--control)] px-4 py-2 text-sm text-[var(--foreground)] transition-opacity hover:opacity-90"
>
Settings
</Link>
<ExportButton />
</div>

<div data-dashboard-search-item data-dashboard-search-text="streak">
<StreakAtRiskBanner />
</div>

<div className="mb-6" data-dashboard-search-item data-dashboard-search-text="weekly summary commits goals pull requests repositories languages">
<WeeklySummaryCard />
</div>

<div className="mb-6" data-dashboard-search-item data-dashboard-search-text="ai mentor">
<AIMentorWidget />
</div>

<div className="mb-6" data-dashboard-search-item data-dashboard-search-text="personal records commits pull requests">
<PersonalRecords />
</div>

{/* Row 1: Contribution graph + Streak + Local Coding Time */}
<div className="grid grid-cols-1 gap-6 lg:grid-cols-3">
<div className="lg:col-span-2 space-y-6" data-dashboard-search-group>
<div data-dashboard-search-item data-dashboard-search-text="commits contribution graph">
<ContributionGraph />
</div>
<div data-dashboard-search-item data-dashboard-search-text="commits contribution heatmap">
<ContributionHeatmap />
</div>
<div data-dashboard-search-item data-dashboard-search-text="commits friend comparison">
<FriendComparison />
</div>
</div>
<div className="mt-6">
<FriendComparison />

<div className="space-y-6" data-dashboard-search-group>
<div data-dashboard-search-item data-dashboard-search-text="streak commits">
<StreakTracker />
</div>
<div data-dashboard-search-item data-dashboard-search-text="coding time local coding time">
<LocalCodingTime />
</div>
</div>
</div>

{/* Row 2: PR metrics, community metrics, PR breakdown & Time Chart */}
<div className="mt-6 grid grid-cols-1 gap-6 md:grid-cols-2 xl:grid-cols-4">
<div data-dashboard-search-item data-dashboard-search-text="pull requests pr metrics">
<PRMetrics />
</div>
<div data-dashboard-search-item data-dashboard-search-text="pull requests community metrics">
<CommunityMetrics />
</div>
<div data-dashboard-search-item data-dashboard-search-text="pull requests breakdown">
<PRBreakdownChart />
</div>
<div data-dashboard-search-item data-dashboard-search-text="commits commit time">
<CommitTimeChart />
</div>
</div>

<div>
<StreakTracker />
<LocalCodingTime />
</div>
</div>

{/* Row 2: PR metrics, community metrics, PR breakdown & Time Chart */}
<div className="mt-6 grid grid-cols-1 md:grid-cols-2 xl:grid-cols-4 gap-6">
<PRMetrics />
<CommunityMetrics />
<PRBreakdownChart />
<CommitTimeChart />
</div>
{/* Row 2b: Activity Ring Chart */}
<div className="mt-6">
<ActivityRingChart />
</div>

<div className="mt-6">
<CodingActivityInsightsCard />
</div>

<div className="mt-6">
<PRReviewTrendChart />
</div>

{/* Row 3: Issue metrics + CI analytics */}
<div className="mt-6 grid grid-cols-1 lg:grid-cols-3 gap-6">
<div className="lg:col-span-2">
<IssueMetrics />
</div>
<CIAnalytics />
</div>
{/* Row 3b: Discussion activity */}
<div className="mt-6">
<DiscussionsWidget />
</div>

{/* Row 4: Pinned repositories */}
<div className="mt-6">
<PinnedRepos />
</div>

{/* Row 5: Inactive repository reminder */}
<div className="mt-6">
<InactiveRepositoriesCard />
</div>

{/* Row 6: Top repos + Language breakdown + Goal tracker */}
<div className="mt-6 grid grid-cols-1 lg:grid-cols-3 gap-6">
<TopRepos />
<LanguageBreakdown />
<GoalTracker />
</div>

{/* Row 7: Recent GitHub activity */}
<div className="mt-6">
<RecentActivity />
</div>
{/* Row 2b: Activity Ring Chart */}
<div className="mt-6" data-dashboard-search-item data-dashboard-search-text="commits activity ring">
<ActivityRingChart />
</div>

<div className="mt-6" data-dashboard-search-item data-dashboard-search-text="coding activity commits">
<CodingActivityInsightsCard />
</div>

<div className="mt-6" data-dashboard-search-item data-dashboard-search-text="pull requests review trend">
<PRReviewTrendChart />
</div>

{/* Row 3: Issue metrics + CI analytics */}
<div className="mt-6 grid grid-cols-1 gap-6 lg:grid-cols-3">
<div className="lg:col-span-2" data-dashboard-search-item data-dashboard-search-group data-dashboard-search-text="issues issue metrics">
<IssueMetrics />
</div>
<div data-dashboard-search-item data-dashboard-search-text="ci analytics">
<CIAnalytics />
</div>
</div>

{/* Row 3b: Discussion activity */}
<div className="mt-6" data-dashboard-search-item data-dashboard-search-text="discussions">
<DiscussionsWidget />
</div>

{/* Row 4: Pinned repositories */}
<div className="mt-6" data-dashboard-search-item data-dashboard-search-text="repository names pinned repositories">
<PinnedRepos />
</div>

{/* Row 5: Inactive repository reminder */}
<div className="mt-6" data-dashboard-search-item data-dashboard-search-text="inactive repositories repository names">
<InactiveRepositoriesCard />
</div>

{/* Row 6: Top repos + Language breakdown + Goal tracker */}
<div className="mt-6 grid grid-cols-1 gap-6 lg:grid-cols-3">
<div data-dashboard-search-item data-dashboard-search-text="repositories commits repo names top repositories">
<TopRepos />
</div>
<div data-dashboard-search-item data-dashboard-search-text="language names languages">
<LanguageBreakdown />
</div>
<div data-dashboard-search-item data-dashboard-search-text="goals goal labels commits prs hours">
<GoalTracker />
</div>
</div>

{/* Row 7: Recent GitHub activity */}
<div className="mt-6" data-dashboard-search-item data-dashboard-search-text="recent activity commits pull requests">
<RecentActivity />
</div>
</DashboardQuickSearch>
</div>
);
}
166 changes: 166 additions & 0 deletions src/components/DashboardQuickSearch.tsx
Original file line number Diff line number Diff line change
@@ -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<HTMLDivElement | null>(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<HTMLElement>(SEARCH_SELECTOR));
const groups = Array.from(root.querySelectorAll<HTMLElement>(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<HTMLElement>(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<HTMLElement>(`${SEARCH_SELECTOR}:not([hidden])`);

if (!target) return;

target.scrollIntoView({ behavior: "smooth", block: "start" });
}, [debouncedQuery, visibleCount]);

const showNoResults = debouncedQuery.length > 0 && hasSearchableItems && visibleCount === 0;

return (
<div className="space-y-4">
<div className="rounded-2xl border border-[var(--border)] bg-[var(--card)]/95 p-4 shadow-sm backdrop-blur-sm md:p-5">
<div className="flex flex-col gap-3 lg:flex-row lg:items-end lg:justify-between">
<div className="max-w-2xl">
<p className="text-xs font-semibold uppercase tracking-[0.2em] text-[var(--muted-foreground)]">
Dashboard Quick Search
</p>
<h2 className="mt-1 text-base font-semibold text-[var(--card-foreground)] md:text-lg">
Search commits, goals, pull requests, repository names, and languages.
</h2>
</div>

<div className="w-full lg:max-w-md">
<label htmlFor="dashboard-quick-search" className="sr-only">
Search dashboard content
</label>
<div className="flex items-center gap-2 rounded-xl border border-[var(--border)] bg-[var(--control)] px-3 py-2 shadow-inner focus-within:border-[var(--accent)]">
<svg
aria-hidden="true"
viewBox="0 0 20 20"
fill="currentColor"
className="h-4 w-4 shrink-0 text-[var(--muted-foreground)]"
>
<path
fillRule="evenodd"
d="M8.5 3a5.5 5.5 0 104.213 9.05l3.618 3.619a.75.75 0 101.06-1.06l-3.618-3.62A5.5 5.5 0 008.5 3zM4.5 8.5a4 4 0 118 0 4 4 0 01-8 0z"
clipRule="evenodd"
/>
</svg>
<input
id="dashboard-quick-search"
type="search"
value={query}
onChange={(event) => 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 && (
<button
type="button"
onClick={() => setQuery("")}
className="rounded-md px-2 py-1 text-xs font-medium text-[var(--muted-foreground)] transition hover:text-[var(--foreground)]"
>
Clear
</button>
)}
</div>
</div>
</div>

<p className="mt-3 text-sm text-[var(--muted-foreground)]">
Use terms like commits, goals, pull requests, repository names, or language names.
</p>

{debouncedQuery.length > 0 && (
<p className="mt-2 text-sm text-[var(--muted-foreground)]" aria-live="polite">
{showNoResults
? "No matching dashboard items found"
: `${visibleCount} matching dashboard item${visibleCount === 1 ? "" : "s"} visible`}
</p>
)}
</div>

<div ref={contentRef}>{children}</div>
</div>
);
}
Loading