diff --git a/apps/dashboard/src/components/pulls/detail/pull-detail-header.tsx b/apps/dashboard/src/components/pulls/detail/pull-detail-header.tsx index 2d7562f..f271f0b 100644 --- a/apps/dashboard/src/components/pulls/detail/pull-detail-header.tsx +++ b/apps/dashboard/src/components/pulls/detail/pull-detail-header.tsx @@ -115,7 +115,7 @@ export function PullDetailHeader({ - {!pr.isMerged && !isReviewRequested && ( + {!isReviewRequested && ( void; }) { + const [copied, setCopied] = useState(false); + return ( -
+
- + {/* biome-ignore lint/a11y/noStaticElementInteractions: span stops propagation so text selection doesn't trigger collapse */} + {/* biome-ignore lint/a11y/useKeyWithClickEvents: keyboard users use the collapse button; this only prevents mouse click bubbling */} + e.stopPropagation()} + > {file.previousFilename && file.previousFilename !== file.filename ? ( <> @@ -308,6 +316,25 @@ function FileHeader({ )} + + {file.additions > 0 && ( +{file.additions} diff --git a/apps/dashboard/src/lib/tab-store.ts b/apps/dashboard/src/lib/tab-store.ts index dab5d37..622df77 100644 --- a/apps/dashboard/src/lib/tab-store.ts +++ b/apps/dashboard/src/lib/tab-store.ts @@ -16,12 +16,30 @@ export interface Tab { export const TABS_STORAGE_KEY = "diffkit:tabs"; +const VALID_TAB_TYPES = new Set(["pull", "issue", "review"]); + +function isValidTab(t: unknown): t is Tab { + return ( + t !== null && + typeof t === "object" && + typeof (t as Tab).id === "string" && + VALID_TAB_TYPES.has((t as Tab).type) + ); +} + export function readStoredTabs(): Tab[] { if (typeof window === "undefined") return []; try { const raw = localStorage.getItem(TABS_STORAGE_KEY); - return raw ? JSON.parse(raw) : []; + if (!raw) return []; + const parsed: unknown = JSON.parse(raw); + if (!Array.isArray(parsed) || !parsed.every(isValidTab)) { + localStorage.removeItem(TABS_STORAGE_KEY); + return []; + } + return parsed; } catch { + localStorage.removeItem(TABS_STORAGE_KEY); return []; } } diff --git a/apps/dashboard/src/routeTree.gen.ts b/apps/dashboard/src/routeTree.gen.ts index e43cbbc..99155a2 100644 --- a/apps/dashboard/src/routeTree.gen.ts +++ b/apps/dashboard/src/routeTree.gen.ts @@ -9,9 +9,7 @@ // Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified. import { Route as rootRouteImport } from './routes/__root' -import { Route as SitemapDotxmlRouteImport } from './routes/sitemap[.]xml' import { Route as SetupRouteImport } from './routes/setup' -import { Route as RobotsDottxtRouteImport } from './routes/robots[.]txt' import { Route as LoginRouteImport } from './routes/login' import { Route as ProtectedRouteImport } from './routes/_protected' import { Route as ProtectedIndexRouteImport } from './routes/_protected/index' @@ -30,21 +28,11 @@ import { Route as ProtectedOwnerRepoReviewPullIdRouteImport } from './routes/_pr import { Route as ProtectedOwnerRepoPullPullIdRouteImport } from './routes/_protected/$owner/$repo/pull.$pullId' import { Route as ProtectedOwnerRepoIssuesIssueIdRouteImport } from './routes/_protected/$owner/$repo/issues.$issueId' -const SitemapDotxmlRoute = SitemapDotxmlRouteImport.update({ - id: '/sitemap.xml', - path: '/sitemap.xml', - getParentRoute: () => rootRouteImport, -} as any) const SetupRoute = SetupRouteImport.update({ id: '/setup', path: '/setup', getParentRoute: () => rootRouteImport, } as any) -const RobotsDottxtRoute = RobotsDottxtRouteImport.update({ - id: '/robots.txt', - path: '/robots.txt', - getParentRoute: () => rootRouteImport, -} as any) const LoginRoute = LoginRouteImport.update({ id: '/login', path: '/login', @@ -137,9 +125,7 @@ const ProtectedOwnerRepoIssuesIssueIdRoute = export interface FileRoutesByFullPath { '/': typeof ProtectedIndexRoute '/login': typeof LoginRoute - '/robots.txt': typeof RobotsDottxtRoute '/setup': typeof SetupRoute - '/sitemap.xml': typeof SitemapDotxmlRoute '/issues': typeof ProtectedIssuesRoute '/pulls': typeof ProtectedPullsRoute '/reviews': typeof ProtectedReviewsRoute @@ -157,9 +143,7 @@ export interface FileRoutesByFullPath { } export interface FileRoutesByTo { '/login': typeof LoginRoute - '/robots.txt': typeof RobotsDottxtRoute '/setup': typeof SetupRoute - '/sitemap.xml': typeof SitemapDotxmlRoute '/issues': typeof ProtectedIssuesRoute '/pulls': typeof ProtectedPullsRoute '/reviews': typeof ProtectedReviewsRoute @@ -179,9 +163,7 @@ export interface FileRoutesById { __root__: typeof rootRouteImport '/_protected': typeof ProtectedRouteWithChildren '/login': typeof LoginRoute - '/robots.txt': typeof RobotsDottxtRoute '/setup': typeof SetupRoute - '/sitemap.xml': typeof SitemapDotxmlRoute '/_protected/issues': typeof ProtectedIssuesRoute '/_protected/pulls': typeof ProtectedPullsRoute '/_protected/reviews': typeof ProtectedReviewsRoute @@ -203,9 +185,7 @@ export interface FileRouteTypes { fullPaths: | '/' | '/login' - | '/robots.txt' | '/setup' - | '/sitemap.xml' | '/issues' | '/pulls' | '/reviews' @@ -223,9 +203,7 @@ export interface FileRouteTypes { fileRoutesByTo: FileRoutesByTo to: | '/login' - | '/robots.txt' | '/setup' - | '/sitemap.xml' | '/issues' | '/pulls' | '/reviews' @@ -244,9 +222,7 @@ export interface FileRouteTypes { | '__root__' | '/_protected' | '/login' - | '/robots.txt' | '/setup' - | '/sitemap.xml' | '/_protected/issues' | '/_protected/pulls' | '/_protected/reviews' @@ -267,9 +243,7 @@ export interface FileRouteTypes { export interface RootRouteChildren { ProtectedRoute: typeof ProtectedRouteWithChildren LoginRoute: typeof LoginRoute - RobotsDottxtRoute: typeof RobotsDottxtRoute SetupRoute: typeof SetupRoute - SitemapDotxmlRoute: typeof SitemapDotxmlRoute ApiAuthSplatRoute: typeof ApiAuthSplatRoute ApiWebhooksGithubRoute: typeof ApiWebhooksGithubRoute ApiGithubAppAuthorizeRoute: typeof ApiGithubAppAuthorizeRoute @@ -278,13 +252,6 @@ export interface RootRouteChildren { declare module '@tanstack/react-router' { interface FileRoutesByPath { - '/sitemap.xml': { - id: '/sitemap.xml' - path: '/sitemap.xml' - fullPath: '/sitemap.xml' - preLoaderRoute: typeof SitemapDotxmlRouteImport - parentRoute: typeof rootRouteImport - } '/setup': { id: '/setup' path: '/setup' @@ -292,13 +259,6 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof SetupRouteImport parentRoute: typeof rootRouteImport } - '/robots.txt': { - id: '/robots.txt' - path: '/robots.txt' - fullPath: '/robots.txt' - preLoaderRoute: typeof RobotsDottxtRouteImport - parentRoute: typeof rootRouteImport - } '/login': { id: '/login' path: '/login' @@ -465,9 +425,7 @@ const ProtectedRouteWithChildren = ProtectedRoute._addFileChildren( const rootRouteChildren: RootRouteChildren = { ProtectedRoute: ProtectedRouteWithChildren, LoginRoute: LoginRoute, - RobotsDottxtRoute: RobotsDottxtRoute, SetupRoute: SetupRoute, - SitemapDotxmlRoute: SitemapDotxmlRoute, ApiAuthSplatRoute: ApiAuthSplatRoute, ApiWebhooksGithubRoute: ApiWebhooksGithubRoute, ApiGithubAppAuthorizeRoute: ApiGithubAppAuthorizeRoute, diff --git a/apps/dashboard/src/routes/_protected/$owner/$repo/issues.$issueId.tsx b/apps/dashboard/src/routes/_protected/$owner/$repo/issues.$issueId.tsx index 997ff0e..d928854 100644 --- a/apps/dashboard/src/routes/_protected/$owner/$repo/issues.$issueId.tsx +++ b/apps/dashboard/src/routes/_protected/$owner/$repo/issues.$issueId.tsx @@ -28,11 +28,15 @@ export const Route = createFileRoute( // Never block navigation — fire prefetch and let the component // show cached data instantly or a skeleton while loading. void context.queryClient.prefetchQuery(pageOptions); + + return { issueTitle: cachedData?.detail?.title ?? null }; }, head: ({ match, params }) => buildSeo({ path: match.pathname, - title: formatPageTitle(`Issue #${params.issueId}`), + title: formatPageTitle( + match.loaderData?.issueTitle ?? `Issue #${params.issueId}`, + ), description: `Private GitHub issue #${params.issueId} in ${params.owner}/${params.repo}.`, robots: "noindex", }), diff --git a/apps/dashboard/src/routes/_protected/$owner/$repo/pull.$pullId.tsx b/apps/dashboard/src/routes/_protected/$owner/$repo/pull.$pullId.tsx index 07bbdc2..479ada4 100644 --- a/apps/dashboard/src/routes/_protected/$owner/$repo/pull.$pullId.tsx +++ b/apps/dashboard/src/routes/_protected/$owner/$repo/pull.$pullId.tsx @@ -19,7 +19,8 @@ export const Route = createFileRoute("/_protected/$owner/$repo/pull/$pullId")({ // Clean up broken cache entries (no detail) const cachedData = context.queryClient.getQueryData(pageOptions.queryKey); - if (cachedData !== undefined && !cachedData?.detail) { + const isBrokenEntry = cachedData !== undefined && !cachedData?.detail; + if (isBrokenEntry) { context.queryClient.removeQueries({ queryKey: pageOptions.queryKey, exact: true, @@ -30,11 +31,17 @@ export const Route = createFileRoute("/_protected/$owner/$repo/pull/$pullId")({ // show cached data instantly or a skeleton while loading. void context.queryClient.prefetchQuery(pageOptions); void context.queryClient.prefetchQuery(githubViewerQueryOptions(scope)); + + return { + prTitle: isBrokenEntry ? null : (cachedData?.detail?.title ?? null), + }; }, head: ({ match, params }) => buildSeo({ path: match.pathname, - title: formatPageTitle(`PR #${params.pullId}`), + title: formatPageTitle( + match.loaderData?.prTitle ?? `PR #${params.pullId}`, + ), description: `Private pull request #${params.pullId} in ${params.owner}/${params.repo}.`, robots: "noindex", }), diff --git a/apps/dashboard/src/routes/_protected/$owner/$repo/review.$pullId.tsx b/apps/dashboard/src/routes/_protected/$owner/$repo/review.$pullId.tsx index 1a5e77c..f6e8e8d 100644 --- a/apps/dashboard/src/routes/_protected/$owner/$repo/review.$pullId.tsx +++ b/apps/dashboard/src/routes/_protected/$owner/$repo/review.$pullId.tsx @@ -27,7 +27,9 @@ export const Route = createFileRoute("/_protected/$owner/$repo/review/$pullId")( const cachedPageData = context.queryClient.getQueryData( pageOptions.queryKey, ); - if (cachedPageData !== undefined && !cachedPageData?.detail) { + const isBrokenEntry = + cachedPageData !== undefined && !cachedPageData?.detail; + if (isBrokenEntry) { context.queryClient.removeQueries({ queryKey: pageOptions.queryKey, exact: true, @@ -46,11 +48,19 @@ export const Route = createFileRoute("/_protected/$owner/$repo/review/$pullId")( data: { ...input, page: 1, perPage: PULL_FILES_PAGE_SIZE }, }); } + + return { + prTitle: isBrokenEntry ? null : (cachedPageData?.detail?.title ?? null), + }; }, head: ({ match, params }) => buildSeo({ path: match.pathname, - title: formatPageTitle(`Review PR #${params.pullId}`), + title: formatPageTitle( + match.loaderData?.prTitle + ? `Review: ${match.loaderData.prTitle}` + : `Review PR #${params.pullId}`, + ), description: `Private code review workspace for pull request #${params.pullId} in ${params.owner}/${params.repo}.`, robots: "noindex", }), diff --git a/apps/dashboard/src/routes/robots[.]txt.ts b/apps/dashboard/src/routes/robots[.]txt.ts deleted file mode 100644 index 4d70bec..0000000 --- a/apps/dashboard/src/routes/robots[.]txt.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { createFileRoute } from "@tanstack/react-router"; -import { siteConfig } from "#/lib/site-config"; - -export const Route = createFileRoute("/robots.txt")({ - server: { - handlers: { - GET: async () => { - const lines = [ - "User-agent: *", - "Allow: /", - "Disallow: /api/", - `Sitemap: ${siteConfig.url}/sitemap.xml`, - ]; - - return new Response(lines.join("\n"), { - headers: { - "Cache-Control": "public, max-age=0, s-maxage=3600", - "Content-Type": "text/plain; charset=utf-8", - }, - }); - }, - }, - }, -}); diff --git a/apps/dashboard/src/routes/sitemap[.]xml.ts b/apps/dashboard/src/routes/sitemap[.]xml.ts deleted file mode 100644 index e686ace..0000000 --- a/apps/dashboard/src/routes/sitemap[.]xml.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { createFileRoute } from "@tanstack/react-router"; -import { siteConfig } from "#/lib/site-config"; - -export const Route = createFileRoute("/sitemap.xml")({ - server: { - handlers: { - GET: async () => { - const lastModified = new Date().toISOString(); - const pages = [ - { - loc: `${siteConfig.url}/login`, - changefreq: "weekly", - priority: "0.8", - }, - ]; - - const xml = ` - -${pages - .map( - (page) => ` - ${page.loc} - ${lastModified} - ${page.changefreq} - ${page.priority} - `, - ) - .join("\n")} -`; - - return new Response(xml, { - headers: { - "Cache-Control": "public, max-age=0, s-maxage=3600", - "Content-Type": "application/xml; charset=utf-8", - }, - }); - }, - }, - }, -}); diff --git a/packages/icons/src/index.ts b/packages/icons/src/index.ts index 499fe84..8b2c158 100644 --- a/packages/icons/src/index.ts +++ b/packages/icons/src/index.ts @@ -65,3 +65,4 @@ export { } from "@hugeicons/react"; export { GitHubLogo, GitHubWordmarkLogo } from "./brand-logos"; export { PenIcon } from "./pen-icon"; +export { SeparatorHorizontalIcon } from "./separator-horizontal-icon"; diff --git a/packages/icons/src/separator-horizontal-icon.tsx b/packages/icons/src/separator-horizontal-icon.tsx new file mode 100644 index 0000000..57dbe9f --- /dev/null +++ b/packages/icons/src/separator-horizontal-icon.tsx @@ -0,0 +1,28 @@ +import type { SVGProps } from "react"; + +export function SeparatorHorizontalIcon( + props: SVGProps & { size?: number } +) { + const { size = 24, width, height, ...rest } = props; + return ( + + + + + + ); +}