diff --git a/apps/dashboard/src/components/layouts/dashboard-content-loading.tsx b/apps/dashboard/src/components/layouts/dashboard-content-loading.tsx new file mode 100644 index 0000000..a45b7a7 --- /dev/null +++ b/apps/dashboard/src/components/layouts/dashboard-content-loading.tsx @@ -0,0 +1,12 @@ +import { LoaderCircleIcon } from "lucide-react"; + +export function DashboardContentLoading() { + return ( +
+ +
+ ); +} diff --git a/apps/dashboard/src/components/layouts/dashboard-layout.tsx b/apps/dashboard/src/components/layouts/dashboard-layout.tsx index d671b24..06f1456 100644 --- a/apps/dashboard/src/components/layouts/dashboard-layout.tsx +++ b/apps/dashboard/src/components/layouts/dashboard-layout.tsx @@ -1,17 +1,73 @@ +import { useQuery } from "@tanstack/react-query"; import { getRouteApi, Outlet } from "@tanstack/react-router"; +import { useEffect, useState } from "react"; +import { + githubMyIssuesQueryOptions, + githubMyPullsQueryOptions, +} from "#/lib/github.query"; +import { useHasMounted } from "#/lib/use-has-mounted"; import { DashboardTopbar } from "./dashboard-topbar"; const routeApi = getRouteApi("/_protected"); export function DashboardLayout() { const { user } = routeApi.useRouteContext(); + const scope = { userId: user.id }; + const hasMounted = useHasMounted(); + const [isContentVisible, setIsContentVisible] = useState(false); + + useEffect(() => { + const frameId = window.requestAnimationFrame(() => { + setIsContentVisible(true); + }); + + return () => { + window.cancelAnimationFrame(frameId); + }; + }, []); + + const pullsQuery = useQuery({ + ...githubMyPullsQueryOptions(scope), + enabled: hasMounted, + }); + const issuesQuery = useQuery({ + ...githubMyIssuesQueryOptions(scope), + enabled: hasMounted, + }); + const pullCount = pullsQuery.data + ? pullsQuery.data.reviewRequested.length + + pullsQuery.data.assigned.length + + pullsQuery.data.authored.length + + pullsQuery.data.mentioned.length + + pullsQuery.data.involved.length + : undefined; + const issueCount = issuesQuery.data + ? issuesQuery.data.assigned.length + + issuesQuery.data.authored.length + + issuesQuery.data.mentioned.length + : undefined; + const tabsReady = hasMounted && Boolean(pullsQuery.data && issuesQuery.data); return (
- +
- +
+ +
diff --git a/apps/dashboard/src/components/layouts/dashboard-topbar.tsx b/apps/dashboard/src/components/layouts/dashboard-topbar.tsx index c75f165..0602d55 100644 --- a/apps/dashboard/src/components/layouts/dashboard-topbar.tsx +++ b/apps/dashboard/src/components/layouts/dashboard-topbar.tsx @@ -8,11 +8,7 @@ import { SunIcon, SystemIcon, } from "@quickhub/icons"; -import { - Avatar, - AvatarFallback, - AvatarImage, -} from "@quickhub/ui/components/avatar"; +import { Avatar, AvatarFallback } from "@quickhub/ui/components/avatar"; import { Button } from "@quickhub/ui/components/button"; import { DropdownMenu, @@ -26,6 +22,7 @@ import { } from "@quickhub/ui/components/dropdown-menu"; import { Link } from "@tanstack/react-router"; import { useTheme } from "next-themes"; +import { useState } from "react"; import { signOutToLogin } from "#/lib/auth-actions"; interface DashboardTopbarProps { @@ -34,14 +31,20 @@ interface DashboardTopbarProps { email: string; image?: string | null; }; + tabsReady: boolean; + counts: { + pulls?: number; + issues?: number; + reviews?: number; + }; } -const navItems = [ - { to: "/", label: "Overview", icon: HomeIcon }, - { to: "/pull-requests", label: "Pull Requests", icon: GitPullRequestIcon }, - { to: "/issues", label: "Issues", icon: IssuesIcon }, - { to: "/reviews", label: "Reviews", icon: ReviewsIcon }, -] as const; +type NavItem = { + to: string; + label: string; + icon: typeof HomeIcon; + count?: number; +}; const themeOptions = [ { value: "light", icon: SunIcon, label: "Light" }, @@ -49,8 +52,13 @@ const themeOptions = [ { value: "system", icon: SystemIcon, label: "System" }, ] as const; -export function DashboardTopbar({ user }: DashboardTopbarProps) { +export function DashboardTopbar({ + user, + tabsReady, + counts, +}: DashboardTopbarProps) { const { theme, setTheme } = useTheme(); + const [avatarLoadFailed, setAvatarLoadFailed] = useState(false); const displayName = user.name ?? user.email; const initials = displayName .split(" ") @@ -59,6 +67,28 @@ export function DashboardTopbar({ user }: DashboardTopbarProps) { .slice(0, 2) .toUpperCase(); + const navItems: NavItem[] = [ + { to: "/", label: "Overview", icon: HomeIcon }, + { + to: "/pulls", + label: "Pull Requests", + icon: GitPullRequestIcon, + count: counts.pulls, + }, + { + to: "/issues", + label: "Issues", + icon: IssuesIcon, + count: counts.issues, + }, + { + to: "/reviews", + label: "Reviews", + icon: ReviewsIcon, + count: counts.reviews, + }, + ]; + return (