From 496684842c8ee94a2b80916b303e5ba9af768164 Mon Sep 17 00:00:00 2001 From: Alan Daniel Date: Tue, 7 Apr 2026 23:00:48 -0400 Subject: [PATCH] Refine pulls page layout --- .../src/components/pulls/pull-request-row.tsx | 13 +- .../dashboard/src/routes/_protected/pulls.tsx | 261 +++++++++++++++--- 2 files changed, 234 insertions(+), 40 deletions(-) diff --git a/apps/dashboard/src/components/pulls/pull-request-row.tsx b/apps/dashboard/src/components/pulls/pull-request-row.tsx index 9a01ede..08462f3 100644 --- a/apps/dashboard/src/components/pulls/pull-request-row.tsx +++ b/apps/dashboard/src/components/pulls/pull-request-row.tsx @@ -7,6 +7,7 @@ import { ViewIcon, } from "@quickhub/icons"; import { Markdown } from "@quickhub/ui/components/markdown"; +import { cn } from "@quickhub/ui/lib/utils"; import { useQuery } from "@tanstack/react-query"; import { Link } from "@tanstack/react-router"; import { useState } from "react"; @@ -68,9 +69,12 @@ export function PullRequestRow({
-
+
@@ -148,7 +152,10 @@ export function PullRequestRow({ {commentsQuery.data.map((comment, i) => (
{comment.author && ( diff --git a/apps/dashboard/src/routes/_protected/pulls.tsx b/apps/dashboard/src/routes/_protected/pulls.tsx index 1f8a866..ce41310 100644 --- a/apps/dashboard/src/routes/_protected/pulls.tsx +++ b/apps/dashboard/src/routes/_protected/pulls.tsx @@ -1,8 +1,24 @@ +import { + CommentIcon, + GitBranchIcon, + GitPullRequestIcon, + InboxIcon, + ReviewsIcon, +} from "@quickhub/icons"; +import { cn } from "@quickhub/ui/lib/utils"; import { useQuery } from "@tanstack/react-query"; import { createFileRoute } from "@tanstack/react-router"; +import { + type ComponentType, + type RefObject, + useEffect, + useRef, + useState, +} from "react"; import { DashboardContentLoading } from "#/components/layouts/dashboard-content-loading"; +import { PullRequestRow } from "#/components/pulls/pull-request-row"; import { githubMyPullsQueryOptions } from "#/lib/github.query"; -import type { PullSummary } from "#/lib/github.types"; +import type { MyPullsResult, PullSummary } from "#/lib/github.types"; import { useHasMounted } from "#/lib/use-has-mounted"; export const Route = createFileRoute("/_protected/pulls")({ @@ -11,33 +27,97 @@ export const Route = createFileRoute("/_protected/pulls")({ function PullRequestsPage() { const { user } = Route.useRouteContext(); + const scope = { userId: user.id }; const hasMounted = useHasMounted(); + const scrollContainerRef = useRef(null); const query = useQuery({ - ...githubMyPullsQueryOptions({ userId: user.id }), + ...githubMyPullsQueryOptions(scope), enabled: hasMounted, }); if (query.error) throw query.error; if (query.data) { const data = query.data; + const groups: PullGroupData[] = [ + { + id: "review-requested", + title: "Review requested", + icon: ReviewsIcon, + pulls: data.reviewRequested, + }, + { + id: "assigned", + title: "Assigned", + icon: InboxIcon, + pulls: data.assigned, + }, + { + id: "authored", + title: "Authored", + icon: GitPullRequestIcon, + pulls: data.authored, + }, + { + id: "mentioned", + title: "Mentioned", + icon: CommentIcon, + pulls: data.mentioned, + }, + { + id: "involved", + title: "Involved", + icon: GitBranchIcon, + pulls: data.involved, + }, + ]; + const totalPulls = groups.reduce( + (sum, group) => sum + group.pulls.length, + 0, + ); return ( -
-
-

- Cached pull request groups -

-

- Pull Requests -

-
- -
- - - - - +
+
+ + +
+ {groups.map((group) => ( + + ))} +
); @@ -49,28 +129,135 @@ function PullRequestsPage() { return null; } -function PullGroup({ title, pulls }: { title: string; pulls: PullSummary[] }) { +type PullGroupData = { + id: string; + title: string; + icon: ComponentType<{ size?: number; strokeWidth?: number }>; + pulls: MyPullsResult[keyof MyPullsResult]; +}; + +const PULL_GROUP_STICKY_TOP = -32; + +function PullMetricCard({ + href, + icon: Icon, + label, + value, +}: { + href: string; + icon: ComponentType<{ size?: number; strokeWidth?: number }>; + label: string; + value: number; +}) { + const content = ( + <> +
+
+ +
+

{label}

+
+

{value}

+ + ); + + if (value === 0) { + return ( +
+ {content} +
+ ); + } + return ( -
-
-

{title}

- {pulls.length} + + {content} + + ); +} + +function PullGroup({ + id, + title, + icon: Icon, + pulls, + scope, + scrollContainerRef, +}: { + id: string; + title: string; + icon: ComponentType<{ size?: number; strokeWidth?: number }>; + pulls: PullSummary[]; + scope: { userId: string }; + scrollContainerRef: RefObject; +}) { + const sectionRef = useRef(null); + const headerRef = useRef(null); + const [isStickyActive, setIsStickyActive] = useState(false); + + useEffect(() => { + const scrollContainer = scrollContainerRef.current; + const section = sectionRef.current; + const header = headerRef.current; + + if (!scrollContainer || !section || !header) { + return; + } + + const updateStickyState = () => { + const scrollContainerRect = scrollContainer.getBoundingClientRect(); + const sectionRect = section.getBoundingClientRect(); + const stickyTop = scrollContainerRect.top + PULL_GROUP_STICKY_TOP; + const headerHeight = header.offsetHeight; + const isStuck = + sectionRect.top <= stickyTop && + sectionRect.bottom > stickyTop + headerHeight; + + setIsStickyActive((current) => (current === isStuck ? current : isStuck)); + }; + + updateStickyState(); + scrollContainer.addEventListener("scroll", updateStickyState, { + passive: true, + }); + window.addEventListener("resize", updateStickyState); + + return () => { + scrollContainer.removeEventListener("scroll", updateStickyState); + window.removeEventListener("resize", updateStickyState); + }; + }, [scrollContainerRef]); + + return ( +
+
+
+
+ +
+

{title}

+
+ + {pulls.length} +
- {pulls.length === 0 ? ( -

- No pull requests in this slice. -

- ) : ( -
+ {pulls.length > 0 && ( +
{pulls.map((pull) => ( -
-

- #{pull.number} {pull.title} -

-

- {pull.repository.fullName} -

-
+ ))}
)}