Skip to content
Merged
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
18 changes: 1 addition & 17 deletions apps/dashboard/src/components/layouts/dashboard-layout.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { useQuery } from "@tanstack/react-query";
import { getRouteApi, Outlet } from "@tanstack/react-router";
import { useEffect, useState } from "react";
import {
githubMyIssuesQueryOptions,
githubMyPullsQueryOptions,
Expand All @@ -14,17 +13,6 @@ 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),
Expand Down Expand Up @@ -61,11 +49,7 @@ export function DashboardLayout() {
/>
<div className="flex flex-1 flex-col overflow-hidden p-2 pt-0">
<div className="flex-1 overflow-hidden rounded-xl border bg-card shadow-[0_1px_4px_0_rgba(0,0,0,0.03)]">
<div
className={`h-full transition-opacity duration-300 ease-out ${
isContentVisible ? "opacity-100" : "opacity-0"
}`}
>
<div className="h-full">
<Outlet />
</div>
</div>
Expand Down
127 changes: 79 additions & 48 deletions apps/dashboard/src/components/layouts/dashboard-topbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import { Link, useRouter } from "@tanstack/react-router";
import { useTheme } from "next-themes";
import { useCallback, useEffect, useRef, useState } from "react";
import { signOutToLogin } from "#/lib/auth-actions";
import { preloadRouteOnce } from "#/lib/route-preload";
import { removeTab, type Tab, useTabs } from "#/lib/tab-store";

interface DashboardTopbarProps {
Expand Down Expand Up @@ -59,6 +60,8 @@ const tabIconMap = {
issue: IssuesIcon,
} as const;

const primaryNavRoutes = ["/", "/pulls", "/issues", "/reviews"] as const;

export function DashboardTopbar({
user,
tabsReady,
Expand Down Expand Up @@ -124,8 +127,24 @@ export function DashboardTopbar({
},
];

useEffect(() => {
if (!tabsReady) return;

void Promise.allSettled(
primaryNavRoutes.map((to) => router.preloadRoute({ to })),
);
}, [router, tabsReady]);

useEffect(() => {
if (!tabsReady || openTabs.length === 0) return;

void Promise.allSettled(
openTabs.map((tab) => preloadRouteOnce(router, tab.url)),
);
}, [router, tabsReady, openTabs]);

return (
<nav className="flex items-center gap-3 px-3 py-2">
<nav className="flex min-w-0 items-center gap-3 overflow-hidden px-3 py-2">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<button
Expand Down Expand Up @@ -196,9 +215,9 @@ export function DashboardTopbar({

<div
aria-hidden={!tabsReady}
className={`flex items-center gap-0.5 transition-[opacity,transform] duration-300 ease-out ${
className={`shrink-0 items-center gap-0.5 transition-[opacity,transform] duration-300 ease-out ${
tabsReady
? "translate-y-0 opacity-100"
? "flex translate-y-0 opacity-100"
: "pointer-events-none -translate-y-0.5 opacity-0"
}`}
>
Expand All @@ -213,6 +232,7 @@ export function DashboardTopbar({
>
<Link
to={item.to as string}
preload={false}
activeOptions={{ exact: true }}
activeProps={{ className: "active" }}
>
Expand All @@ -232,54 +252,56 @@ export function DashboardTopbar({
))}
</div>

{openTabs.length > 0 && (
<div
aria-hidden={!tabsReady}
className={`flex items-center gap-3 transition-[opacity,transform] duration-300 ease-out ${
tabsReady
? "translate-y-0 opacity-100"
: "pointer-events-none -translate-y-0.5 opacity-0"
}`}
>
<div className="h-4 border-l border-border/50" />
<div className="relative min-w-0">
<div
className={`pointer-events-none absolute inset-y-0 left-0 z-10 w-6 bg-gradient-to-r from-muted to-transparent transition-opacity ${canScrollLeft ? "opacity-100" : "opacity-0"}`}
/>
<div
className={`pointer-events-none absolute inset-y-0 right-0 z-10 w-6 bg-gradient-to-l from-muted to-transparent transition-opacity ${canScrollRight ? "opacity-100" : "opacity-0"}`}
/>
{/* biome-ignore lint/a11y/noStaticElementInteractions: scroll container needs onScroll for gradient visibility */}
<div
ref={scrollRef}
onScroll={updateScrollState}
onMouseEnter={updateScrollState}
className="no-scrollbar flex items-center gap-0.5 overflow-x-auto"
>
{openTabs.map((tab) => {
const Icon = tabIconMap[tab.type];
return (
<DetailTab
key={tab.id}
tab={tab}
icon={Icon}
onClose={(id) => {
const isActive =
router.state.location.pathname === tab.url;
removeTab(id);
if (isActive) {
void router.navigate({ to: "/" });
}
}}
/>
);
})}
<div className="min-w-0 flex-1 overflow-hidden">
{openTabs.length > 0 && (
<div
aria-hidden={!tabsReady}
className={`flex min-w-0 items-center gap-3 overflow-hidden transition-[opacity,transform] duration-300 ease-out ${
tabsReady
? "translate-y-0 opacity-100"
: "pointer-events-none -translate-y-0.5 opacity-0"
}`}
>
<div className="h-4 shrink-0 border-l border-border/50" />
<div className="relative min-w-0 flex-1 overflow-hidden">
<div
className={`pointer-events-none absolute inset-y-0 left-0 z-10 w-6 bg-gradient-to-r from-muted to-transparent transition-opacity ${canScrollLeft ? "opacity-100" : "opacity-0"}`}
/>
<div
className={`pointer-events-none absolute inset-y-0 right-0 z-10 w-6 bg-gradient-to-l from-muted to-transparent transition-opacity ${canScrollRight ? "opacity-100" : "opacity-0"}`}
/>
{/* biome-ignore lint/a11y/noStaticElementInteractions: scroll container needs onScroll for gradient visibility */}
<div
ref={scrollRef}
onScroll={updateScrollState}
onMouseEnter={updateScrollState}
className="no-scrollbar flex w-0 min-w-full items-center gap-0.5 overflow-x-auto"
>
{openTabs.map((tab) => {
const Icon = tabIconMap[tab.type];
return (
<DetailTab
key={tab.id}
tab={tab}
icon={Icon}
onClose={(id) => {
const isActive =
router.state.location.pathname === tab.url;
removeTab(id);
if (isActive) {
void router.navigate({ to: "/" });
}
}}
/>
);
})}
</div>
</div>
</div>
</div>
)}
)}
</div>

<div className="ml-auto">
<div className="shrink-0">
<Button
variant="ghost"
size="icon"
Expand All @@ -301,9 +323,18 @@ function DetailTab({
icon: typeof GitPullRequestIcon;
onClose: (id: string) => void;
}) {
const router = useRouter();
const preloadTab = () => {
void preloadRouteOnce(router, tab.url);
};

return (
<Link
to={tab.url}
preload={false}
onMouseEnter={preloadTab}
onFocus={preloadTab}
onTouchStart={preloadTab}
activeOptions={{ exact: true }}
activeProps={{ className: "active" }}
className="group relative flex h-8 shrink-0 items-center gap-1.5 rounded-md px-3 text-[13px] font-medium text-muted-foreground transition-colors hover:bg-surface-1 hover:text-foreground [&.active]:bg-surface-1 [&.active]:text-foreground"
Expand Down
11 changes: 10 additions & 1 deletion apps/dashboard/src/components/pulls/pull-request-row.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,14 @@ import {
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 { Link, useRouter } from "@tanstack/react-router";
import { useState } from "react";
import {
type GitHubQueryScope,
githubPullCommentsQueryOptions,
} from "#/lib/github.query";
import type { PullSummary } from "#/lib/github.types";
import { preloadRouteOnce } from "#/lib/route-preload";

export function formatRelativeTime(dateStr: string): string {
const seconds = Math.floor((Date.now() - new Date(dateStr).getTime()) / 1000);
Expand Down Expand Up @@ -55,6 +56,10 @@ export function PullRequestRow({
const { icon: Icon, color } = getPrStateProps(pr);
const href = `/${pr.repository.owner}/${pr.repository.name}/pull/${pr.number}`;
const [expanded, setExpanded] = useState(false);
const router = useRouter();
const preloadDetail = () => {
void preloadRouteOnce(router, href);
};

const commentsQuery = useQuery({
...githubPullCommentsQueryOptions(scope, {
Expand All @@ -69,6 +74,10 @@ export function PullRequestRow({
<div className="rounded-lg">
<Link
to={href}
preload={false}
onMouseEnter={preloadDetail}
onFocus={preloadDetail}
onTouchStart={preloadDetail}
className={cn(
"group flex items-start gap-3 rounded-lg px-3 py-2.5 transition-colors hover:[&:not(:has([data-action]:hover))]:bg-surface-1",
expanded && "bg-surface-1",
Expand Down
76 changes: 76 additions & 0 deletions apps/dashboard/src/lib/auth-runtime.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import { env } from "cloudflare:workers";
import { getRequest } from "@tanstack/react-start/server";
import { betterAuth } from "better-auth";
import { drizzleAdapter } from "better-auth/adapters/drizzle";
import { tanstackStartCookies } from "better-auth/tanstack-start";
import { and, eq } from "drizzle-orm";
import { drizzle } from "drizzle-orm/d1";
import type { Octokit as OctokitType } from "octokit";
import { Octokit } from "octokit";
import { getDb } from "../db";
import * as schema from "../db/schema";
import { account } from "../db/schema";

const authDb = drizzle(env.DB, { schema });

function createAuth() {
return betterAuth({
baseURL: env.BETTER_AUTH_URL,
secret: env.BETTER_AUTH_SECRET,
database: drizzleAdapter(authDb, {
provider: "sqlite",
}),
socialProviders: {
github: {
clientId: env.GITHUB_CLIENT_ID,
clientSecret: env.GITHUB_CLIENT_SECRET,
scope: [
"read:user",
"user:email",
"repo",
"notifications",
"workflow",
"read:project",
"security_events",
"admin:repo_hook",
],
},
},
plugins: [tanstackStartCookies()],
});
}

let authInstance: ReturnType<typeof createAuth> | undefined;

function getAuth() {
if (!authInstance) {
authInstance = createAuth();
}

return authInstance;
}

export async function getRequestSession() {
return getAuth().api.getSession({ headers: getRequest().headers });
}

export async function getGitHubClientByUserId(
userId: string,
): Promise<OctokitType> {
const db = getDb();
const githubAccount = await db
.select()
.from(account)
.where(and(eq(account.userId, userId), eq(account.providerId, "github")))
.get();

if (!githubAccount?.accessToken) {
throw new Error("No GitHub account linked");
}

return new Octokit({
auth: githubAccount.accessToken,
retry: { enabled: false },
throttle: { enabled: false },
});
}
12 changes: 2 additions & 10 deletions apps/dashboard/src/lib/auth.functions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,7 @@ import { createServerFn } from "@tanstack/react-start";

export const getSession = createServerFn({ method: "GET" }).handler(
async () => {
const [{ getRequest }, { getAuth }] = await Promise.all([
import("@tanstack/react-start/server"),
import("./auth.server"),
]);
const request = getRequest();
const auth = getAuth();
const session = await auth.api.getSession({
headers: request.headers,
});
return session;
const { getRequestSession } = await import("./auth-runtime");
return getRequestSession();
},
);
12 changes: 10 additions & 2 deletions apps/dashboard/src/lib/github-cache-policy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,15 @@ export const githubCachePolicy = {
gcTimeMs: 60 * 60 * 1000,
},
detail: {
staleTimeMs: 5 * 60 * 1000,
gcTimeMs: 6 * 60 * 60 * 1000,
staleTimeMs: 30 * 1000,
gcTimeMs: 10 * 60 * 1000,
},
activity: {
staleTimeMs: 20 * 1000,
gcTimeMs: 10 * 60 * 1000,
},
status: {
staleTimeMs: 15 * 1000,
gcTimeMs: 5 * 60 * 1000,
},
} as const;
3 changes: 3 additions & 0 deletions apps/dashboard/src/lib/github-cache.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,7 @@ describe("getOrRevalidateGitHubResource", () => {
});

it("deduplicates concurrent stale refreshes for the same cache key", async () => {
const inFlightCache = new Map<string, Promise<unknown>>();
const store = createMemoryStore([
buildEntry({
resource: "pulls.mine.reviewRequested",
Expand All @@ -136,6 +137,7 @@ describe("getOrRevalidateGitHubResource", () => {
);

const promiseA = getOrRevalidateGitHubResource({
inFlightCache,
userId: "user-1",
resource: "pulls.mine.reviewRequested",
params: { role: "review-requested" },
Expand All @@ -145,6 +147,7 @@ describe("getOrRevalidateGitHubResource", () => {
fetcher,
});
const promiseB = getOrRevalidateGitHubResource({
inFlightCache,
userId: "user-1",
resource: "pulls.mine.reviewRequested",
params: { role: "review-requested" },
Expand Down
Loading