diff --git a/apps/dashboard/src/components/layouts/dashboard-mobile-nav.tsx b/apps/dashboard/src/components/layouts/dashboard-mobile-nav.tsx index 6009653..58114c3 100644 --- a/apps/dashboard/src/components/layouts/dashboard-mobile-nav.tsx +++ b/apps/dashboard/src/components/layouts/dashboard-mobile-nav.tsx @@ -7,6 +7,7 @@ import { ReviewsIcon, SunIcon, SystemIcon, + UserCircleIcon, } from "@diffkit/icons"; import { Avatar, AvatarFallback } from "@diffkit/ui/components/avatar"; import { @@ -20,10 +21,13 @@ import { DropdownMenuTrigger, } from "@diffkit/ui/components/dropdown-menu"; import { cn } from "@diffkit/ui/lib/utils"; +import { useQuery } from "@tanstack/react-query"; import { Link } from "@tanstack/react-router"; import { useTheme } from "next-themes"; import { useState } from "react"; import { signOutToLogin } from "#/lib/auth-actions"; +import { githubViewerQueryOptions } from "#/lib/github.query"; +import { useHasMounted } from "#/lib/use-has-mounted"; const themeOptions = [ { value: "light", icon: SunIcon, label: "Light" }, @@ -40,6 +44,7 @@ interface MobileNavItem { interface DashboardMobileNavProps { user: { + id: string; name?: string | null; email: string; image?: string | null; @@ -59,6 +64,12 @@ export function DashboardMobileNav({ }: DashboardMobileNavProps) { const { theme, setTheme } = useTheme(); const [avatarLoadFailed, setAvatarLoadFailed] = useState(false); + const hasMounted = useHasMounted(); + const viewerQuery = useQuery({ + ...githubViewerQueryOptions({ userId: user.id }), + enabled: hasMounted, + }); + const viewerLogin = viewerQuery.data?.login; const displayName = user.name ?? user.email; const initials = displayName @@ -161,9 +172,12 @@ export function DashboardMobileNav({ - - Profile - + + + + Profile + + diff --git a/apps/dashboard/src/components/layouts/dashboard-tabs.tsx b/apps/dashboard/src/components/layouts/dashboard-tabs.tsx index 7bb1f20..8c86360 100644 --- a/apps/dashboard/src/components/layouts/dashboard-tabs.tsx +++ b/apps/dashboard/src/components/layouts/dashboard-tabs.tsx @@ -91,7 +91,10 @@ interface DashboardTabsProps { export function DashboardTabs({ tabsReady, routerRef }: DashboardTabsProps) { const openTabs = useTabs(); const pathname = useRouterState({ select: (s) => s.location.pathname }); - const contextTabRef = useRef<{ tab: Tab; index: number } | null>(null); + const [contextTab, setContextTab] = useState<{ + tab: Tab; + index: number; + } | null>(null); const { scrollRef, canScrollLeft, @@ -119,34 +122,34 @@ export function DashboardTabs({ tabsReady, routerRef }: DashboardTabsProps) { const handleCloseTab = useCallback( (id: string, tabUrl: string) => { const isActive = pathname === tabUrl; + const index = openTabs.findIndex((tab) => tab.id === id); + const nextTab = + index === -1 ? undefined : (openTabs[index + 1] ?? openTabs[index - 1]); removeTab(id); if (isActive) { - void routerRef.current.navigate({ to: "/" }); + void routerRef.current.navigate({ to: nextTab?.url ?? "/" }); } }, - [pathname, routerRef], + [openTabs, pathname, routerRef], ); const handleContextClose = useCallback(() => { - const ctx = contextTabRef.current; - if (!ctx) return; - handleCloseTab(ctx.tab.id, ctx.tab.url); - }, [handleCloseTab]); + if (!contextTab) return; + handleCloseTab(contextTab.tab.id, contextTab.tab.url); + }, [contextTab, handleCloseTab]); const handleContextCloseOthers = useCallback(() => { - const ctx = contextTabRef.current; - if (!ctx) return; - if (pathname !== ctx.tab.url) { - void routerRef.current.navigate({ to: ctx.tab.url }); + if (!contextTab) return; + if (pathname !== contextTab.tab.url) { + void routerRef.current.navigate({ to: contextTab.tab.url }); } - removeOtherTabs(ctx.tab.id); - }, [pathname, routerRef]); + removeOtherTabs(contextTab.tab.id); + }, [contextTab, pathname, routerRef]); const handleContextCloseRight = useCallback(() => { - const ctx = contextTabRef.current; - if (!ctx) return; - removeTabsToRight(ctx.tab.id); - }, []); + if (!contextTab) return; + removeTabsToRight(contextTab.tab.id); + }, [contextTab]); if (openTabs.length === 0) return null; @@ -193,7 +196,7 @@ export function DashboardTabs({ tabsReady, routerRef }: DashboardTabsProps) { icon={Icon} onClose={handleCloseTab} onContextMenu={() => { - contextTabRef.current = { tab, index }; + setContextTab({ tab, index }); }} routerRef={routerRef} /> @@ -216,7 +219,7 @@ export function DashboardTabs({ tabsReady, routerRef }: DashboardTabsProps) { Close tabs to the right diff --git a/apps/dashboard/src/components/layouts/dashboard-topbar.tsx b/apps/dashboard/src/components/layouts/dashboard-topbar.tsx index 1baa9b7..73a3747 100644 --- a/apps/dashboard/src/components/layouts/dashboard-topbar.tsx +++ b/apps/dashboard/src/components/layouts/dashboard-topbar.tsx @@ -34,7 +34,7 @@ import { githubViewerQueryOptions, } from "#/lib/github.query"; import { useGlobalShortcuts } from "#/lib/shortcuts"; -import { type Tab, useTabs } from "#/lib/tab-store"; +import { removeTab, type Tab, useTabs } from "#/lib/tab-store"; import { useHasMounted } from "#/lib/use-has-mounted"; interface DashboardTopbarProps { @@ -154,6 +154,13 @@ export function DashboardTopbar({ void routerRef.current.navigate({ to: tab.url }); } + function getNeighborTab(tab: Tab | undefined) { + if (!tab) return undefined; + const index = openTabs.findIndex((item) => item.id === tab.id); + if (index === -1) return undefined; + return openTabs[index + 1] ?? openTabs[index - 1]; + } + useGlobalShortcuts([ ...Array.from( { length: Math.min(openTabs.length, MAX_TAB_SHORTCUTS) }, @@ -165,6 +172,28 @@ export function DashboardTopbar({ }, }), ), + { + shortcut: [{ key: "g" }, { key: "u" }], + enabled: tabsReady && Boolean(viewerLogin), + onTrigger: () => { + void routerRef.current.navigate({ + to: "/$owner", + params: { owner: viewerLogin ?? "" }, + }); + }, + }, + { + shortcut: { key: "w", shift: true }, + enabled: tabsReady && openTabs.length > 0, + onTrigger: () => { + const currentPath = routerRef.current.state.location.pathname; + const currentTab = openTabs.find((tab) => tab.url === currentPath); + if (!currentTab) return; + const nextTab = getNeighborTab(currentTab); + removeTab(currentTab.id); + void routerRef.current.navigate({ to: nextTab?.url ?? "/" }); + }, + }, { shortcut: { key: "ArrowLeft", shift: true }, enabled: tabsReady && openTabs.length > 1, @@ -252,7 +281,7 @@ export function DashboardTopbar({ Profile - + diff --git a/apps/dashboard/src/lib/command-palette/use-command-items.ts b/apps/dashboard/src/lib/command-palette/use-command-items.ts index 70789c3..8fff257 100644 --- a/apps/dashboard/src/lib/command-palette/use-command-items.ts +++ b/apps/dashboard/src/lib/command-palette/use-command-items.ts @@ -5,6 +5,7 @@ import { GitPullRequestDraftIcon, GitPullRequestIcon, IssuesIcon, + UserCircleIcon, } from "@diffkit/icons"; import { type QueryClient, useQueryClient } from "@tanstack/react-query"; import { getRouteApi } from "@tanstack/react-router"; @@ -70,9 +71,27 @@ export function useCommandItems(): CommandItem[] { const issues = queryClient.getQueryData( githubQueryKeys.issues.mine(scope), ); + const viewer = queryClient.getQueryData<{ login: string } | null>( + githubQueryKeys.viewer(scope), + ); const dynamicItems: CommandItem[] = []; + if (viewer) { + dynamicItems.push({ + id: "nav:profile", + label: "Go to Profile", + group: "Pages", + icon: UserCircleIcon, + keywords: ["user", "me", viewer.login], + shortcut: ["G", "U"], + action: { + type: "navigate", + to: `/${viewer.login}`, + }, + }); + } + if (repos) { for (const repo of repos) { dynamicItems.push({ diff --git a/apps/dashboard/src/routes/_protected/settings/shortcuts.tsx b/apps/dashboard/src/routes/_protected/settings/shortcuts.tsx index b81f040..286de52 100644 --- a/apps/dashboard/src/routes/_protected/settings/shortcuts.tsx +++ b/apps/dashboard/src/routes/_protected/settings/shortcuts.tsx @@ -200,6 +200,7 @@ const shortcutGroups: ShortcutGroup[] = [ title: "Navigation", shortcuts: [ { keys: ["G", "H"], description: "Go to Overview" }, + { keys: ["G", "U"], description: "Go to Profile" }, { keys: ["G", "P"], description: "Go to Pull Requests" }, { keys: ["G", "I"], description: "Go to Issues" }, { keys: ["G", "R"], description: "Go to Reviews" }, @@ -210,6 +211,7 @@ const shortcutGroups: ShortcutGroup[] = [ title: "Tabs", shortcuts: [ { keys: ["Shift", "1\u20139"], description: "Switch to tab by position" }, + { keys: ["Shift", "W"], description: "Close current tab" }, { keys: ["Shift", "\u2190"], description: "Previous tab" }, { keys: ["Shift", "\u2192"], description: "Next tab" }, ],