Refresh invite and repository web pages (#1808)

Signed-off-by: Fizz <8a675edd33677aa0389f6650d467b2041fb0df4ca820eacb009babb95e3715d4@sprout-oss.stage.blox.sqprod.co>
Co-authored-by: npub13fn4ahfnvaa2qwylvegdgeajqs0mph6v4qsw4jcqnw4mjh3hzh2quuucm5 <8a675edd33677aa0389f6650d467b2041fb0df4ca820eacb009babb95e3715d4@sprout-oss.stage.blox.sqprod.co>
This commit is contained in:
klopez4212
2026-07-13 19:20:34 +01:00
committed by GitHub
co-authored by npub13fn4ahfnvaa2qwylvegdgeajqs0mph6v4qsw4jcqnw4mjh3hzh2quuucm5
parent a6cfd65511
commit a227bb45f6
18 changed files with 528 additions and 218 deletions
+1 -11
View File
@@ -1,5 +1,4 @@
import { Link, Outlet, createRootRoute } from "@tanstack/react-router";
import { ThemeToggle } from "@/shared/theme/ThemeToggle";
import { Outlet, createRootRoute } from "@tanstack/react-router";
export const Route = createRootRoute({
component: RootLayout,
@@ -8,15 +7,6 @@ export const Route = createRootRoute({
function RootLayout() {
return (
<div className="flex min-h-dvh flex-col">
<header className="flex h-12 items-center justify-between border-b px-4">
<Link
to="/"
className="text-sm font-semibold tracking-tight text-foreground"
>
Buzz
</Link>
<ThemeToggle />
</header>
<main className="flex flex-1 flex-col">
<Outlet />
</main>
Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

+45 -33
View File
@@ -1,5 +1,4 @@
import { Download, ExternalLink, Hexagon } from "lucide-react";
import buzzAppIcon from "@/assets/app-icon@3x.png";
import { relayWsUrl } from "@/shared/lib/relay-url";
import { Button } from "@/shared/ui/button";
@@ -19,39 +18,52 @@ export function InvitePage({ code }: { code: string }) {
const deepLink = `buzz://join?relay=${encodeURIComponent(relay)}&code=${encodeURIComponent(code)}`;
return (
<div className="flex flex-1 flex-col items-center justify-center px-4 py-16 text-center">
<div className="flex h-16 w-16 items-center justify-center rounded-2xl bg-primary/10">
<Hexagon className="h-8 w-8 text-primary" />
</div>
<h1 className="mt-6 text-2xl font-semibold tracking-tight">
You&apos;re invited to join
</h1>
<p className="mt-1 font-mono text-lg text-foreground">{host}</p>
<p className="mt-3 max-w-md text-sm leading-relaxed text-muted-foreground">
Buzz is a messaging platform for humanagent collaboration. Accept this
invite in the desktop app to join the workspace on this relay.
</p>
<div
className="flex flex-1 flex-col items-center justify-center px-4 py-16 text-center"
style={{
backgroundImage: "linear-gradient(180deg, #D7D72E 0%, #D7E7F6 100%)",
}}
>
<div
className="flex w-full max-w-xl flex-col items-center rounded-3xl bg-white px-6 py-10 sm:px-12 sm:py-12"
style={{
boxShadow:
"0 0 0 1px rgba(0, 0, 0, 0.04), 0 8px 24px rgba(0, 0, 0, 0.04)",
}}
>
<div
className="h-16 w-16 overflow-hidden bg-black"
style={{ borderRadius: "22.37%" }}
>
<img alt="Buzz" className="h-full w-full" src={buzzAppIcon} />
</div>
<h1 className="mt-6 text-2xl font-semibold tracking-tight text-black">
You&apos;re invited to join
</h1>
<p className="mt-1 font-mono text-lg text-black/70">{host}</p>
<div className="mt-8 flex flex-col items-center gap-3 sm:flex-row">
<Button asChild size="lg">
<a href={deepLink}>
<ExternalLink className="h-4 w-4" />
Open in Buzz
</a>
</Button>
<Button asChild variant="outline" size="lg">
<a href={DOWNLOAD_URL} target="_blank" rel="noreferrer">
<Download className="h-4 w-4" />
Download Buzz
</a>
</Button>
</div>
<div className="mt-8">
<Button
asChild
className="bg-black text-white hover:bg-black/90 focus-visible:ring-black"
size="lg"
>
<a href={deepLink}>Accept invite in Buzz</a>
</Button>
</div>
<p className="mt-6 max-w-sm text-xs leading-relaxed text-muted-foreground">
Already have Buzz installed? &ldquo;Open in Buzz&rdquo; will launch it
and accept the invite. Otherwise download the app first, then come back
and use this link again.
</p>
<p className="mt-6 text-sm text-black/60">
Don&apos;t have the app?{" "}
<a
className="font-medium text-black underline-offset-4 hover:text-black/70 hover:decoration-current hover:underline focus-visible:underline"
href={DOWNLOAD_URL}
rel="noreferrer"
target="_blank"
>
Download it now
</a>
</p>
</div>
</div>
);
}
+149
View File
@@ -0,0 +1,149 @@
import type { Repo } from "./use-repos";
import type {
BlobView,
CommitInfo,
ReadmeResult,
TreeEntry,
} from "./git-client";
const now = Math.floor(Date.now() / 1000);
const people = {
ada: "1".repeat(64),
grace: "2".repeat(64),
linus: "3".repeat(64),
margaret: "4".repeat(64),
};
/** Local-only data for previewing the populated repositories state. */
export const mockRepos: Repo[] = [
{
id: "buzz-desktop",
name: "buzz-desktop",
description:
"The desktop client for collaborating with people and agents across Buzz workspaces.",
cloneUrls: ["https://example.com/buzz-desktop.git"],
webUrl: null,
channelId: null,
owner: people.ada,
contributors: [people.grace, people.linus],
createdAt: now - 60 * 24,
},
{
id: "agent-harness",
name: "agent-harness",
description:
"Tools and shared workflows for launching and coordinating coding agents.",
cloneUrls: ["https://example.com/agent-harness.git"],
webUrl: null,
channelId: null,
owner: people.grace,
contributors: [people.ada, people.margaret],
createdAt: now - 60 * 60 * 3,
},
{
id: "relay-infrastructure",
name: "relay-infrastructure",
description:
"Infrastructure and deployment configuration for community relays.",
cloneUrls: ["https://example.com/relay-infrastructure.git"],
webUrl: null,
channelId: null,
owner: people.linus,
contributors: [people.margaret],
createdAt: now - 60 * 60 * 24 * 2,
},
{
id: "design-system",
name: "design-system",
description: "Shared foundations, components, and interaction patterns.",
cloneUrls: ["https://example.com/design-system.git"],
webUrl: null,
channelId: null,
owner: people.margaret,
contributors: [people.ada, people.grace, people.linus],
createdAt: now - 60 * 60 * 24 * 8,
},
];
export function getMockRepo(repoId: string): Repo | undefined {
if (!import.meta.env.DEV) return undefined;
return mockRepos.find((repo) => repo.id === repoId);
}
export const mockRepoTree: TreeEntry[] = [
{
name: "src",
type: "tree",
mode: "040000",
oid: "1".repeat(40),
},
{
name: "README.md",
type: "blob",
mode: "100644",
oid: "2".repeat(40),
},
{
name: "package.json",
type: "blob",
mode: "100644",
oid: "3".repeat(40),
},
];
export const mockRepoCommits: CommitInfo[] = [
{
oid: "a".repeat(40),
message: "Polish repository browsing styles",
author: {
name: "Ada",
email: "ada@example.com",
timestamp: now - 60 * 24,
},
},
{
oid: "b".repeat(40),
message: "Add repository empty states",
author: {
name: "Grace",
email: "grace@example.com",
timestamp: now - 60 * 60 * 5,
},
},
];
export const mockRepoReadme: ReadmeResult = {
filename: "README.md",
content:
"# Buzz Desktop\n\nA focused workspace for people and agents to collaborate.\n\n## Getting started\n\nInstall dependencies, then start the development app.",
};
export function getMockBlob(
repoId: string,
filepath: string,
): BlobView | undefined {
if (!getMockRepo(repoId)) return undefined;
if (filepath === "README.md") {
return {
kind: "markdown",
content: mockRepoReadme.content,
sizeBytes: mockRepoReadme.content.length,
};
}
if (filepath === "package.json") {
const content = `${JSON.stringify(
{
name: repoId,
private: true,
scripts: { dev: "vite", build: "tsc && vite build" },
},
null,
2,
)}\n`;
return { kind: "text", content, sizeBytes: content.length };
}
return undefined;
}
+4 -1
View File
@@ -7,7 +7,10 @@ export function ConnectButton({ className }: { className?: string }) {
const deepLink = `buzz://connect?relay=${encodeURIComponent(relayWsUrl())}`;
return (
<Button asChild className={className}>
<Button
asChild
className={`bg-black text-white hover:bg-black/90 focus-visible:ring-black dark:bg-white dark:text-black dark:hover:bg-white/90 dark:focus-visible:ring-white ${className ?? ""}`}
>
<a href={deepLink}>
<ExternalLink className="h-4 w-4" />
Open in Buzz
+2 -2
View File
@@ -30,7 +30,7 @@ export function OrgSidebar({ repos }: { repos: Repo[] }) {
{/* People section */}
{uniquePubkeys.length > 0 && (
<div>
<h3 className="mb-3 flex items-center gap-2 text-sm font-semibold text-sidebar-foreground">
<h3 className="mb-3 flex items-center gap-2 text-sm font-semibold text-black dark:text-white">
<Users className="h-4 w-4" />
People
</h3>
@@ -40,7 +40,7 @@ export function OrgSidebar({ repos }: { repos: Repo[] }) {
))}
</div>
{overflowCount > 0 && (
<span className="mt-2 block text-xs text-muted-foreground">
<span className="mt-2 block text-xs text-black/50 dark:text-white/50">
{uniquePubkeys.length} people
</span>
)}
+38 -19
View File
@@ -18,6 +18,7 @@ import { Button } from "@/shared/ui/button";
import type { BlobView } from "../git-client";
import { useGitBlob, useGitHtmlDoc } from "../use-git-browse";
import { useRepoContext } from "../use-repo-context";
import { getMockBlob } from "../mock-repos";
function formatBytes(n: number): string {
if (n < 1024) return `${n} B`;
@@ -64,6 +65,7 @@ function CopyTextButton({ content }: { content: string }) {
<Button
variant="outline"
size="sm"
className="border-black/10 bg-white text-black hover:bg-black/5 dark:border-white/10 dark:bg-white/5 dark:text-white dark:hover:bg-white/10"
onClick={async () => {
try {
await navigator.clipboard.writeText(content);
@@ -93,7 +95,12 @@ function DownloadButton({
const url = useObjectUrl(bytes, contentType);
if (!url) return null;
return (
<Button asChild variant="outline" size="sm">
<Button
asChild
variant="outline"
size="sm"
className="border-black/10 bg-white text-black hover:bg-black/5 dark:border-white/10 dark:bg-white/5 dark:text-white dark:hover:bg-white/10"
>
<a href={url} download={filename}>
<Download className="h-4 w-4" />
<span className="ml-2">Download</span>
@@ -107,7 +114,7 @@ function TextView({ content }: { content: string }) {
// keyed children for an immutable text dump; not worth the linter dance for
// v1. The browser handles wrapping/scrolling via `<pre>`.
return (
<pre className="overflow-auto whitespace-pre rounded-lg border border-border bg-muted/30 p-4 font-mono text-sm leading-6">
<pre className="overflow-auto whitespace-pre rounded-lg border border-black/10 bg-white/50 p-4 font-mono text-sm leading-6 text-black dark:border-white/10 dark:bg-white/5 dark:text-white">
{content}
</pre>
);
@@ -125,7 +132,7 @@ function ImageView({
const url = useObjectUrl(bytes, contentType);
if (!url) return null;
return (
<div className="flex justify-center rounded-lg border border-border bg-muted/30 p-4">
<div className="flex justify-center rounded-lg border border-black/10 bg-white/50 p-4 dark:border-white/10 dark:bg-white/5">
<img
src={url}
alt={filename}
@@ -155,7 +162,7 @@ function HtmlRunView({ doc }: { doc: string }) {
title="Repository page (sandboxed)"
srcDoc={doc}
sandbox={RUN_SANDBOX}
className="h-[80vh] w-full rounded-lg border border-border bg-white"
className="h-[80vh] w-full rounded-lg border border-black/10 bg-white dark:border-white/10"
/>
);
}
@@ -174,7 +181,7 @@ function ViewerBody({
return <TextView content={view.content} />;
case "markdown":
return (
<div className="prose prose-sm dark:prose-invert max-w-none rounded-lg border border-border p-4">
<div className="prose prose-sm dark:prose-invert max-w-none rounded-lg border border-black/10 bg-white/50 p-4 dark:border-white/10 dark:bg-white/5">
<Markdown remarkPlugins={[remarkGfm]}>{view.content}</Markdown>
</div>
);
@@ -196,14 +203,14 @@ function ViewerBody({
);
case "binary":
return (
<div className="rounded-lg border border-border bg-muted/30 p-6 text-sm text-muted-foreground">
<div className="rounded-lg border border-black/10 bg-white/50 p-6 text-sm text-black/60 dark:border-white/10 dark:bg-white/5 dark:text-white/60">
Binary file {formatBytes(view.sizeBytes)}. Use the Download button
above to save it.
</div>
);
case "too-large":
return (
<div className="rounded-lg border border-border bg-muted/30 p-6 text-sm text-muted-foreground">
<div className="rounded-lg border border-black/10 bg-white/50 p-6 text-sm text-black/60 dark:border-white/10 dark:bg-white/5 dark:text-white/60">
File is {formatBytes(view.sizeBytes)}, over the{" "}
{formatBytes(view.limitBytes)} preview limit. Use the Download button
above to save it.
@@ -215,19 +222,29 @@ function ViewerBody({
export function RepoBlobPage() {
const { repoId, _splat } = useParams({ from: "/repos/$repoId/blob/$" });
const filepath = _splat ?? "";
const preview =
import.meta.env.DEV &&
new URLSearchParams(window.location.search).get("preview") ===
"repositories";
const mockView = preview ? getMockBlob(repoId, filepath) : undefined;
const showMockBlob = Boolean(mockView);
const {
owner,
repoName,
defaultRef,
isLoading: ctxLoading,
error: ctxError,
} = useRepoContext(repoId);
} = useRepoContext(repoId, { preview: showMockBlob });
const browseOwner = showMockBlob ? "" : owner;
const {
data: view,
isLoading,
data: fetchedView,
isLoading: isViewLoading,
error,
} = useGitBlob(owner, repoName, defaultRef, filepath);
} = useGitBlob(browseOwner, repoName, defaultRef, filepath);
const view = mockView ?? fetchedView;
const isLoading = showMockBlob ? false : isViewLoading;
const [running, setRunning] = useState(false);
const isHtml = view?.kind === "html";
@@ -244,8 +261,8 @@ export function RepoBlobPage() {
if (ctxError) {
return (
<div className="px-4 py-8">
<BackLink repoId={repoId} />
<div className="flex-1 bg-[#F3F3F3] px-4 py-8 text-black dark:bg-[#171717] dark:text-white">
<BackLink repoId={repoId} preview={showMockBlob} />
<p className="mt-4 text-sm text-destructive">
Failed to load repository: {ctxError.message}
</p>
@@ -254,11 +271,11 @@ export function RepoBlobPage() {
}
return (
<div className="px-4 py-8">
<BackLink repoId={repoId} />
<div className="flex-1 bg-[#F3F3F3] px-4 py-8 text-black dark:bg-[#171717] dark:text-white">
<BackLink repoId={repoId} preview={showMockBlob} />
<div className="mt-4 flex flex-wrap items-center gap-3">
<FileText className="h-4 w-4 text-muted-foreground" />
<FileText className="h-4 w-4 text-black/50 dark:text-white/50" />
<h1 className="min-w-0 truncate font-mono text-sm">{filepath}</h1>
<div className="ml-auto flex items-center gap-2">
{view &&
@@ -271,6 +288,7 @@ export function RepoBlobPage() {
<Button
variant={running ? "secondary" : "default"}
size="sm"
className="bg-black text-white hover:bg-black/90 dark:bg-white dark:text-black dark:hover:bg-white/90"
onClick={() => setRunning((r) => !r)}
>
<Play className="h-4 w-4" />
@@ -296,7 +314,7 @@ export function RepoBlobPage() {
<div className="mt-6">
{ctxLoading || isLoading ? (
<div className="h-32 animate-pulse rounded-lg bg-muted" />
<div className="h-32 animate-pulse rounded-lg bg-black/10 dark:bg-white/10" />
) : error ? (
<p className="text-sm text-destructive">
Failed to load file: {(error as Error).message}
@@ -313,12 +331,13 @@ export function RepoBlobPage() {
);
}
function BackLink({ repoId }: { repoId: string }) {
function BackLink({ repoId, preview }: { repoId: string; preview: boolean }) {
return (
<Link
to="/repos/$repoId"
params={{ repoId }}
className="inline-flex items-center gap-1 text-sm text-muted-foreground hover:text-foreground"
search={preview ? { preview: "repositories" } : undefined}
className="inline-flex items-center gap-1 text-sm text-black/60 hover:text-black dark:text-white/60 dark:hover:text-white"
>
<ArrowLeft className="h-4 w-4" />
Back to repository
@@ -5,15 +5,15 @@ import type { CommitInfo } from "../git-client";
function CommitRow({ commit }: { commit: CommitInfo }) {
const firstLine = commit.message.split("\n")[0];
return (
<div className="flex items-start gap-3 border-b border-border px-3 py-2.5 text-sm last:border-b-0">
<GitCommit className="mt-0.5 h-4 w-4 shrink-0 text-muted-foreground" />
<div className="flex items-start gap-3 border-b border-black/10 px-3 py-2.5 text-sm text-black last:border-b-0 dark:border-white/10 dark:text-white">
<GitCommit className="mt-0.5 h-4 w-4 shrink-0 text-black/50 dark:text-white/50" />
<div className="min-w-0 flex-1">
<p className="truncate font-medium">{firstLine}</p>
<p className="mt-0.5 text-xs text-muted-foreground">
<p className="mt-0.5 text-xs text-black/50 dark:text-white/50">
{commit.author.name} committed {relativeTime(commit.author.timestamp)}
</p>
</div>
<code className="shrink-0 self-center rounded bg-muted px-1.5 py-0.5 font-mono text-xs text-muted-foreground">
<code className="shrink-0 self-center rounded bg-black/5 px-1.5 py-0.5 font-mono text-xs text-black/50 dark:bg-white/10 dark:text-white/50">
{commit.oid.slice(0, 7)}
</code>
</div>
@@ -30,20 +30,20 @@ export function RepoCommitsSection({
if (isLoading) {
return (
<div className="mt-8">
<h2 className="mb-3 flex items-center gap-2 text-sm font-semibold">
<h2 className="mb-3 flex items-center gap-2 text-sm font-semibold text-black dark:text-white">
<GitCommit className="h-4 w-4" />
Recent commits
</h2>
<div className="rounded-lg border border-border">
<div className="rounded-lg border border-black/10 dark:border-white/10">
{["sk-1", "sk-2", "sk-3"].map((key) => (
<div
key={key}
className="flex items-center gap-3 border-b border-border px-3 py-2.5 last:border-b-0"
className="flex items-center gap-3 border-b border-black/10 px-3 py-2.5 last:border-b-0 dark:border-white/10"
>
<div className="h-4 w-4 animate-pulse rounded bg-muted" />
<div className="h-4 w-4 animate-pulse rounded bg-black/10 dark:bg-white/10" />
<div className="flex-1 space-y-1">
<div className="h-4 w-48 animate-pulse rounded bg-muted" />
<div className="h-3 w-32 animate-pulse rounded bg-muted" />
<div className="h-4 w-48 animate-pulse rounded bg-black/10 dark:bg-white/10" />
<div className="h-3 w-32 animate-pulse rounded bg-black/10 dark:bg-white/10" />
</div>
</div>
))}
@@ -56,11 +56,11 @@ export function RepoCommitsSection({
return (
<div className="mt-8">
<h2 className="mb-3 flex items-center gap-2 text-sm font-semibold">
<h2 className="mb-3 flex items-center gap-2 text-sm font-semibold text-black dark:text-white">
<GitCommit className="h-4 w-4" />
Recent commits
</h2>
<div className="overflow-hidden rounded-lg border border-border">
<div className="overflow-hidden rounded-lg border border-black/10 bg-white/50 dark:border-white/10 dark:bg-white/5">
{commits.map((commit) => (
<CommitRow key={commit.oid} commit={commit} />
))}
+116 -53
View File
@@ -16,6 +16,12 @@ import { Button } from "@/shared/ui/button";
import { relativeTime } from "@/shared/lib/relative-time";
import { useRepoRefs } from "../use-repo-refs";
import { useRepo } from "../use-repos";
import {
getMockRepo,
mockRepoCommits,
mockRepoReadme,
mockRepoTree,
} from "../mock-repos";
import type { CommitInfo, ReadmeResult, TreeEntry } from "../git-client";
import { useGitTree, useGitLog, useGitReadme } from "../use-git-browse";
import { ConnectButton } from "./ConnectButton";
@@ -40,12 +46,14 @@ function CopyableUrl({ url }: { url: string }) {
}
return (
<div className="flex items-center gap-2 rounded-md border border-input bg-muted/50 px-3 py-2">
<code className="min-w-0 flex-1 truncate text-sm">{url}</code>
<div className="flex items-center gap-2 rounded-md border border-black/10 bg-white px-3 py-2 dark:border-white/10 dark:bg-white/5">
<code className="min-w-0 flex-1 truncate text-sm text-black dark:text-white">
{url}
</code>
<button
type="button"
onClick={handleCopy}
className="shrink-0 text-muted-foreground hover:text-foreground"
className="shrink-0 text-black/50 hover:text-black dark:text-white/50 dark:hover:text-white"
aria-label="Copy clone URL"
>
{copied ? (
@@ -60,14 +68,14 @@ function CopyableUrl({ url }: { url: string }) {
function DetailSkeleton() {
return (
<div className="flex w-full gap-8 px-4 py-8">
<div className="flex w-full flex-1 gap-8 bg-[#F3F3F3] px-4 py-8 dark:bg-[#171717]">
<div className="min-w-0 flex-1">
<div className="h-5 w-24 animate-pulse rounded bg-muted" />
<div className="mt-6 h-8 w-64 animate-pulse rounded bg-muted" />
<div className="mt-3 h-5 w-96 animate-pulse rounded bg-muted" />
<div className="h-5 w-24 animate-pulse rounded bg-black/10 dark:bg-white/10" />
<div className="mt-6 h-8 w-64 animate-pulse rounded bg-black/10 dark:bg-white/10" />
<div className="mt-3 h-5 w-96 animate-pulse rounded bg-black/10 dark:bg-white/10" />
<div className="mt-8 space-y-3">
<div className="h-4 w-32 animate-pulse rounded bg-muted" />
<div className="h-10 w-full animate-pulse rounded bg-muted" />
<div className="h-4 w-32 animate-pulse rounded bg-black/10 dark:bg-white/10" />
<div className="h-10 w-full animate-pulse rounded bg-black/10 dark:bg-white/10" />
</div>
</div>
<aside className="hidden w-72 shrink-0 lg:block" />
@@ -75,6 +83,31 @@ function DetailSkeleton() {
);
}
function BackToRepositories({
mockPreview = false,
}: {
mockPreview?: boolean;
}) {
const className =
"inline-flex items-center gap-1 text-sm text-black/60 hover:text-black dark:text-white/60 dark:hover:text-white";
const content = (
<>
<ArrowLeft className="h-4 w-4" />
Back to repositories
</>
);
return mockPreview ? (
<a href="/?preview=repositories" className={className}>
{content}
</a>
) : (
<Link to="/" className={className}>
{content}
</Link>
);
}
type Tab = "code" | "commits";
function RepoTabs({
@@ -85,6 +118,7 @@ function RepoTabs({
commitsLoading,
readme,
readmeLoading,
preview,
}: {
repoId: string;
treeEntries: TreeEntry[] | undefined;
@@ -93,20 +127,21 @@ function RepoTabs({
commitsLoading: boolean;
readme: ReadmeResult | null | undefined;
readmeLoading: boolean;
preview: boolean;
}) {
const [tab, setTab] = useState<Tab>("code");
return (
<div className="mt-6">
{/* Tab bar */}
<div className="flex gap-1 border-b border-border">
<div className="flex gap-1 border-b border-black/10 dark:border-white/10">
<button
type="button"
onClick={() => setTab("code")}
className={`px-4 py-2 text-sm font-medium transition-colors ${
tab === "code"
? "border-b-2 border-foreground text-foreground"
: "text-muted-foreground hover:text-foreground"
? "border-b-2 border-black text-black dark:border-white dark:text-white"
: "text-black/50 hover:text-black dark:text-white/50 dark:hover:text-white"
}`}
>
Code
@@ -116,8 +151,8 @@ function RepoTabs({
onClick={() => setTab("commits")}
className={`px-4 py-2 text-sm font-medium transition-colors ${
tab === "commits"
? "border-b-2 border-foreground text-foreground"
: "text-muted-foreground hover:text-foreground"
? "border-b-2 border-black text-black dark:border-white dark:text-white"
: "text-black/50 hover:text-black dark:text-white/50 dark:hover:text-white"
}`}
>
Commits
@@ -131,6 +166,7 @@ function RepoTabs({
entries={treeEntries}
isLoading={treeLoading}
repoId={repoId}
preview={preview}
/>
<RepoReadmeSection readme={readme} isLoading={readmeLoading} />
</>
@@ -144,28 +180,49 @@ function RepoTabs({
export function RepoDetailPage() {
const { repoId } = useParams({ from: "/repos/$repoId" });
const { data: repo, isLoading, error } = useRepo(repoId);
const { data: refs, isLoading: refsLoading } = useRepoRefs(repoId);
const preview =
import.meta.env.DEV &&
new URLSearchParams(window.location.search).get("preview") ===
"repositories";
const mockRepo = preview ? getMockRepo(repoId) : undefined;
const showMockRepo = Boolean(mockRepo);
const {
data: repo,
isLoading,
error,
} = useRepo(repoId, {
preview: showMockRepo,
});
const { data: refs, isLoading: refsLoading } = useRepoRefs(repoId, {
preview: showMockRepo,
});
const defaultRef = refs?.head?.ref ?? "main";
const owner = repo?.owner ?? "";
const repoName = repo?.id ?? "";
const browseOwner = showMockRepo ? "" : owner;
const {
data: treeEntries,
isLoading: treeLoading,
data: fetchedTreeEntries,
isLoading: isTreeLoading,
error: treeError,
} = useGitTree(owner, repoName, defaultRef);
} = useGitTree(browseOwner, repoName, defaultRef);
const {
data: commits,
isLoading: commitsLoading,
data: fetchedCommits,
isLoading: areCommitsLoading,
error: commitsError,
} = useGitLog(owner, repoName, defaultRef);
const { data: readme, isLoading: readmeLoading } = useGitReadme(
owner,
} = useGitLog(browseOwner, repoName, defaultRef);
const { data: fetchedReadme, isLoading: isReadmeLoading } = useGitReadme(
browseOwner,
repoName,
defaultRef,
);
const treeEntries = showMockRepo ? mockRepoTree : fetchedTreeEntries;
const commits = showMockRepo ? mockRepoCommits : fetchedCommits;
const readme = showMockRepo ? mockRepoReadme : fetchedReadme;
const treeLoading = showMockRepo ? false : isTreeLoading;
const commitsLoading = showMockRepo ? false : areCommitsLoading;
const readmeLoading = showMockRepo ? false : isReadmeLoading;
// Surface clone/browse errors — these are otherwise silent
const browseError = treeError || commitsError;
@@ -187,19 +244,15 @@ export function RepoDetailPage() {
if (!repo) {
return (
<div className="flex w-full gap-8 px-4 py-8">
<div className="flex w-full flex-1 gap-8 bg-[#F3F3F3] px-4 py-8 text-black dark:bg-[#171717] dark:text-white">
<div className="min-w-0 flex-1">
<Link
to="/"
className="inline-flex items-center gap-1 text-sm text-muted-foreground hover:text-foreground"
>
<ArrowLeft className="h-4 w-4" />
Back to repositories
</Link>
<BackToRepositories />
<div className="mt-12 text-center">
<BookMarked className="mx-auto h-10 w-10 text-muted-foreground" />
<h1 className="mt-4 text-xl font-semibold">Repository not found</h1>
<p className="mt-1 text-sm text-muted-foreground">
<BookMarked className="mx-auto h-10 w-10 text-black/50 dark:text-white/50" />
<h1 className="mt-4 text-xl font-semibold text-black dark:text-white">
Repository not found
</h1>
<p className="mt-1 text-sm text-black/60 dark:text-white/60">
This repository may have been removed or doesn't exist on this
relay.
</p>
@@ -211,17 +264,11 @@ export function RepoDetailPage() {
}
return (
<div className="flex w-full gap-8 px-4 py-8">
<div className="flex w-full flex-1 gap-8 bg-[#F3F3F3] px-4 py-8 text-black dark:bg-[#171717] dark:text-white">
{/* Main content */}
<div className="min-w-0 flex-1">
{/* Back link */}
<Link
to="/"
className="inline-flex items-center gap-1 text-sm text-muted-foreground hover:text-foreground"
>
<ArrowLeft className="h-4 w-4" />
Back to repositories
</Link>
<BackToRepositories mockPreview={preview} />
{/* Mobile-only connect button */}
<div className="mt-4 lg:hidden">
@@ -231,18 +278,23 @@ export function RepoDetailPage() {
{/* Header */}
<div className="mt-6">
<div className="flex items-center gap-3">
<BookMarked className="h-6 w-6 shrink-0 text-muted-foreground" />
<h1 className="text-2xl font-semibold tracking-tight">
<BookMarked className="h-6 w-6 shrink-0 text-black/50 dark:text-white/50" />
<h1 className="text-2xl font-semibold tracking-tight text-black dark:text-white">
{repo.name}
</h1>
<Badge variant="outline">Public</Badge>
<Badge
variant="outline"
className="border-black/15 text-black/60 dark:border-white/15 dark:text-white/60"
>
Public
</Badge>
</div>
{repo.description && (
<p className="mt-2 text-sm leading-relaxed text-muted-foreground">
<p className="mt-2 text-sm leading-relaxed text-black/60 dark:text-white/60">
{repo.description}
</p>
)}
<p className="mt-2 text-xs text-muted-foreground">
<p className="mt-2 text-xs text-black/50 dark:text-white/50">
Updated {relativeTime(repo.createdAt)}
</p>
</div>
@@ -269,12 +321,15 @@ export function RepoDetailPage() {
commitsLoading={commitsLoading}
readme={readme}
readmeLoading={readmeLoading}
preview={showMockRepo}
/>
{/* Clone URLs */}
{repo.cloneUrls.length > 0 && (
<div className="mt-8">
<h2 className="mb-3 text-sm font-semibold">Clone</h2>
<h2 className="mb-3 text-sm font-semibold text-black dark:text-white">
Clone
</h2>
<div className="space-y-2">
{repo.cloneUrls.map((url) => (
<CopyableUrl key={url} url={url} />
@@ -297,7 +352,11 @@ export function RepoDetailPage() {
if (!safe) return null;
return (
<div className="mt-6">
<Button variant="outline" asChild>
<Button
variant="outline"
className="border-black/10 bg-white text-black hover:bg-black/5 dark:border-white/10 dark:bg-white/5 dark:text-white dark:hover:bg-white/10"
asChild
>
<a href={safe} target="_blank" rel="noopener noreferrer">
<ExternalLink className="h-4 w-4" />
View on web
@@ -310,7 +369,11 @@ export function RepoDetailPage() {
{/* Channel link */}
{repo.channelId && (
<div className="mt-8">
<Button variant="outline" asChild>
<Button
variant="outline"
className="border-black/10 bg-white text-black hover:bg-black/5 dark:border-white/10 dark:bg-white/5 dark:text-white dark:hover:bg-white/10"
asChild
>
<a href={`/channels/${repo.channelId}`}>
<MessageSquare className="h-4 w-4" />
View channel
@@ -321,14 +384,14 @@ export function RepoDetailPage() {
</div>
{/* Sidebar */}
<aside className="hidden w-72 shrink-0 border-l border-border pl-8 lg:block">
<aside className="hidden w-72 shrink-0 border-l border-black/10 pl-8 dark:border-white/10 lg:block">
<div className="space-y-6">
{/* Open in Buzz */}
<ConnectButton className="w-full" />
{/* People */}
<div>
<h3 className="mb-3 flex items-center gap-2 text-sm font-semibold">
<h3 className="mb-3 flex items-center gap-2 text-sm font-semibold text-black dark:text-white">
<Users className="h-4 w-4" />
People
</h3>
+17 -7
View File
@@ -7,33 +7,43 @@ import { relativeTime } from "@/shared/lib/relative-time";
import { truncatePubkey } from "@/shared/lib/pubkey";
import type { Repo } from "../use-repos";
export function RepoListItem({ repo }: { repo: Repo }) {
export function RepoListItem({
repo,
preview = false,
}: {
repo: Repo;
preview?: boolean;
}) {
return (
<div className="py-6">
<div className="py-6 text-black dark:text-white">
{/* Row 1: Name + badge */}
<div className="flex items-center gap-2">
<BookMarked className="h-4 w-4 shrink-0 text-muted-foreground" />
<BookMarked className="h-4 w-4 shrink-0 text-black/50 dark:text-white/50" />
<Link
to="/repos/$repoId"
params={{ repoId: repo.id }}
className="text-lg font-semibold text-primary hover:underline"
search={preview ? { preview: "repositories" } : undefined}
className="text-lg font-semibold text-black underline-offset-4 hover:text-black/70 hover:underline dark:text-white dark:hover:text-white/70"
>
{repo.name}
</Link>
<Badge variant="outline" className="ml-1">
<Badge
variant="outline"
className="ml-1 border-black/15 text-black/60 dark:border-white/15 dark:text-white/60"
>
Public
</Badge>
</div>
{/* Row 2: Description */}
{repo.description && (
<p className="mt-1 line-clamp-2 text-sm text-muted-foreground">
<p className="mt-1 line-clamp-2 text-sm text-black/60 dark:text-white/60">
{repo.description}
</p>
)}
{/* Row 3: Metadata */}
<div className="mt-2 flex items-center gap-4 text-xs text-muted-foreground">
<div className="mt-2 flex items-center gap-4 text-xs text-black/50 dark:text-white/50">
<Tooltip>
<TooltipTrigger asChild>
<span className="cursor-default font-mono">
@@ -13,14 +13,14 @@ export function RepoReadmeSection({
if (isLoading) {
return (
<div className="mt-8">
<h2 className="mb-3 flex items-center gap-2 text-sm font-semibold">
<h2 className="mb-3 flex items-center gap-2 text-sm font-semibold text-black dark:text-white">
<BookOpen className="h-4 w-4" />
README
</h2>
<div className="space-y-2 rounded-lg border border-border p-4">
<div className="h-4 w-3/4 animate-pulse rounded bg-muted" />
<div className="h-4 w-1/2 animate-pulse rounded bg-muted" />
<div className="h-4 w-5/6 animate-pulse rounded bg-muted" />
<div className="space-y-2 rounded-lg border border-black/10 p-4 dark:border-white/10">
<div className="h-4 w-3/4 animate-pulse rounded bg-black/10 dark:bg-white/10" />
<div className="h-4 w-1/2 animate-pulse rounded bg-black/10 dark:bg-white/10" />
<div className="h-4 w-5/6 animate-pulse rounded bg-black/10 dark:bg-white/10" />
</div>
</div>
);
@@ -30,11 +30,11 @@ export function RepoReadmeSection({
return (
<div className="mt-8">
<h2 className="mb-3 flex items-center gap-2 text-sm font-semibold">
<h2 className="mb-3 flex items-center gap-2 text-sm font-semibold text-black dark:text-white">
<BookOpen className="h-4 w-4" />
{readme.filename}
</h2>
<div className="prose prose-sm dark:prose-invert max-w-none rounded-lg border border-border p-4">
<div className="prose prose-sm dark:prose-invert max-w-none rounded-lg border border-black/10 bg-white/50 p-4 dark:border-white/10 dark:bg-white/5">
<Markdown remarkPlugins={[remarkGfm]}>{readme.content}</Markdown>
</div>
</div>
+14 -6
View File
@@ -17,22 +17,28 @@ export function RepoRefsSection({
return (
<div className="mt-6">
{hasRefs ? (
<div className="flex flex-wrap items-center gap-2 text-sm text-muted-foreground">
<div className="flex flex-wrap items-center gap-2 text-sm text-black/60 dark:text-white/60">
{refs.head && (
<>
<div className="flex items-center gap-1.5">
<Badge variant="secondary">
<Badge
variant="secondary"
className="bg-black/5 text-black dark:bg-white/10 dark:text-white"
>
<GitBranch className="mr-1 h-4 w-4" />
{refs.head.ref}
</Badge>
{refs.head.sha && (
<Badge variant="outline" className="font-mono text-xs">
<Badge
variant="outline"
className="border-black/15 font-mono text-xs text-black/60 dark:border-white/15 dark:text-white/60"
>
<Hash className="mr-0.5 h-4 w-4" />
{refs.head.sha.slice(0, 7)}
</Badge>
)}
</div>
<span className="text-muted-foreground/60">&middot;</span>
<span className="text-black/30 dark:text-white/30">&middot;</span>
</>
)}
<span className="flex items-center gap-1">
@@ -40,14 +46,16 @@ export function RepoRefsSection({
{refs.branches.length}{" "}
{refs.branches.length === 1 ? "branch" : "branches"}
</span>
<span className="text-muted-foreground/60">&middot;</span>
<span className="text-black/30 dark:text-white/30">&middot;</span>
<span className="flex items-center gap-1">
<Tag className="h-4 w-4" />
{refs.tags.length} {refs.tags.length === 1 ? "tag" : "tags"}
</span>
</div>
) : (
<p className="text-sm text-muted-foreground">No commits yet</p>
<p className="text-sm text-black/60 dark:text-white/60">
No commits yet
</p>
)}
</div>
);
+27 -11
View File
@@ -2,16 +2,24 @@ import { File, Folder } from "lucide-react";
import { Link } from "@tanstack/react-router";
import type { TreeEntry } from "../git-client";
function TreeRow({ entry, repoId }: { entry: TreeEntry; repoId: string }) {
function TreeRow({
entry,
repoId,
preview,
}: {
entry: TreeEntry;
repoId: string;
preview: boolean;
}) {
if (entry.type === "tree") {
// Sub-tree navigation is deferred — show folders as visibly non-clickable
// so the affordance matches the behaviour.
return (
<div
className="flex items-center gap-2 border-b border-border px-3 py-2 text-sm text-muted-foreground last:border-b-0"
className="flex items-center gap-2 border-b border-black/10 px-3 py-2 text-sm text-black/60 last:border-b-0 dark:border-white/10 dark:text-white/60"
aria-disabled="true"
>
<Folder className="h-4 w-4 shrink-0 text-blue-400" />
<Folder className="h-4 w-4 shrink-0 text-black/50 dark:text-white/50" />
<span className="font-medium">{entry.name}</span>
</div>
);
@@ -21,9 +29,10 @@ function TreeRow({ entry, repoId }: { entry: TreeEntry; repoId: string }) {
<Link
to="/repos/$repoId/blob/$"
params={{ repoId, _splat: entry.name }}
className="flex items-center gap-2 border-b border-border px-3 py-2 text-sm last:border-b-0 hover:bg-muted/50"
search={preview ? { preview: "repositories" } : undefined}
className="flex items-center gap-2 border-b border-black/10 px-3 py-2 text-sm text-black last:border-b-0 hover:bg-black/5 dark:border-white/10 dark:text-white dark:hover:bg-white/5"
>
<File className="h-4 w-4 shrink-0 text-muted-foreground" />
<File className="h-4 w-4 shrink-0 text-black/50 dark:text-white/50" />
<span>{entry.name}</span>
</Link>
);
@@ -33,22 +42,24 @@ export function RepoTreeSection({
entries,
isLoading,
repoId,
preview = false,
}: {
entries: TreeEntry[] | undefined;
isLoading: boolean;
repoId: string;
preview?: boolean;
}) {
if (isLoading) {
return (
<div className="mt-8">
<div className="rounded-lg border border-border">
<div className="rounded-lg border border-black/10 dark:border-white/10">
{["sk-1", "sk-2", "sk-3", "sk-4", "sk-5"].map((key) => (
<div
key={key}
className="flex items-center gap-2 border-b border-border px-3 py-2 last:border-b-0"
className="flex items-center gap-2 border-b border-black/10 px-3 py-2 last:border-b-0 dark:border-white/10"
>
<div className="h-4 w-4 animate-pulse rounded bg-muted" />
<div className="h-4 w-32 animate-pulse rounded bg-muted" />
<div className="h-4 w-4 animate-pulse rounded bg-black/10 dark:bg-white/10" />
<div className="h-4 w-32 animate-pulse rounded bg-black/10 dark:bg-white/10" />
</div>
))}
</div>
@@ -60,9 +71,14 @@ export function RepoTreeSection({
return (
<div className="mt-8">
<div className="overflow-hidden rounded-lg border border-border">
<div className="overflow-hidden rounded-lg border border-black/10 bg-white/50 dark:border-white/10 dark:bg-white/5">
{entries.map((entry) => (
<TreeRow key={entry.name} entry={entry} repoId={repoId} />
<TreeRow
key={entry.name}
entry={entry}
repoId={repoId}
preview={preview}
/>
))}
</div>
</div>
+56 -32
View File
@@ -1,8 +1,10 @@
import { BookMarked, GitBranch, Hexagon } from "lucide-react";
import { BookMarked, GitBranch } from "lucide-react";
import { toast } from "sonner";
import { useEffect, useMemo, useState } from "react";
import buzzAppIcon from "@/assets/app-icon@3x.png";
import { Input } from "@/shared/ui/input";
import { mockRepos } from "../mock-repos";
import { useRepos } from "../use-repos";
import { ConnectButton } from "./ConnectButton";
import { OrgSidebar } from "./OrgSidebar";
@@ -14,14 +16,14 @@ function ListItemSkeleton() {
return (
<div className="py-6">
<div className="flex items-center gap-2">
<div className="h-4 w-4 shrink-0 animate-pulse rounded bg-muted" />
<div className="h-5 w-48 animate-pulse rounded bg-muted" />
<div className="h-5 w-14 animate-pulse rounded bg-muted" />
<div className="h-4 w-4 shrink-0 animate-pulse rounded bg-black/10 dark:bg-white/10" />
<div className="h-5 w-48 animate-pulse rounded bg-black/10 dark:bg-white/10" />
<div className="h-5 w-14 animate-pulse rounded bg-black/10 dark:bg-white/10" />
</div>
<div className="mt-2 h-4 w-3/4 animate-pulse rounded bg-muted" />
<div className="mt-2 h-4 w-3/4 animate-pulse rounded bg-black/10 dark:bg-white/10" />
<div className="mt-2 flex gap-4">
<div className="h-3 w-24 animate-pulse rounded bg-muted" />
<div className="h-3 w-20 animate-pulse rounded bg-muted" />
<div className="h-3 w-24 animate-pulse rounded bg-black/10 dark:bg-white/10" />
<div className="h-3 w-20 animate-pulse rounded bg-black/10 dark:bg-white/10" />
</div>
</div>
);
@@ -30,11 +32,13 @@ function ListItemSkeleton() {
function SearchEmptyState() {
return (
<div className="flex flex-col items-center justify-center py-20 text-center">
<div className="flex h-14 w-14 items-center justify-center rounded-full bg-muted">
<GitBranch className="h-7 w-7 text-muted-foreground" />
<div className="flex h-14 w-14 items-center justify-center rounded-full bg-black/5 dark:bg-white/10">
<GitBranch className="h-7 w-7 text-black/50 dark:text-white/50" />
</div>
<h2 className="mt-4 text-lg font-semibold">No matching repositories</h2>
<p className="mt-1 max-w-sm text-sm text-muted-foreground">
<h2 className="mt-4 text-lg font-semibold text-black dark:text-white">
No matching repositories
</h2>
<p className="mt-1 max-w-sm text-sm text-black/60 dark:text-white/60">
Try adjusting your search term.
</p>
</div>
@@ -43,24 +47,44 @@ function SearchEmptyState() {
function RelayEmptyState() {
return (
<div className="flex flex-1 flex-col items-center justify-center px-4 text-center">
<div className="flex h-16 w-16 items-center justify-center rounded-2xl bg-primary/10">
<Hexagon className="h-8 w-8 text-primary" />
<div className="flex flex-1 items-center justify-center bg-[#F3F3F3] px-4 py-16 text-center dark:bg-[#171717]">
<div className="flex w-full max-w-xl flex-col items-center px-6 py-10 sm:px-12 sm:py-12">
<div
className="h-16 w-16 overflow-hidden bg-black"
style={{ borderRadius: "22.37%" }}
>
<img alt="Buzz" className="h-full w-full" src={buzzAppIcon} />
</div>
<h1 className="mt-6 text-2xl font-semibold tracking-tight text-black dark:text-white">
This relay is empty
</h1>
<p className="mt-2 max-w-md text-sm leading-relaxed text-black/60 dark:text-white/60">
Repositories pushed to this relay will show up here. Open this relay
in the Buzz desktop app to start pushing code.
</p>
<ConnectButton className="mt-6" />
</div>
<h1 className="mt-6 text-2xl font-semibold tracking-tight">
This relay is empty
</h1>
<p className="mt-2 max-w-md text-sm leading-relaxed text-muted-foreground">
Repositories pushed to this relay will show up here. Open this relay in
the Buzz desktop app to start pushing code.
</p>
<ConnectButton className="mt-6" />
</div>
);
}
export function ReposPage() {
const { data: repos, isLoading, error } = useRepos();
const preview = import.meta.env.DEV
? new URLSearchParams(window.location.search).get("preview")
: null;
const showMockRepos = preview === "repositories";
const showMockEmptyState = preview === "empty";
const {
data: fetchedRepos,
isLoading: isLoadingRepos,
error,
} = useRepos({ enabled: !showMockRepos && !showMockEmptyState });
const repos = showMockRepos
? mockRepos
: showMockEmptyState
? []
: fetchedRepos;
const isLoading = preview ? false : isLoadingRepos;
const [search, setSearch] = useState("");
const [sort, setSort] = useState<SortOrder>("newest");
@@ -101,9 +125,9 @@ export function ReposPage() {
if (isLoading) {
return (
<div className="flex w-full gap-8 px-4 py-8">
<div className="flex w-full flex-1 gap-8 bg-[#F3F3F3] px-4 py-8 dark:bg-[#171717]">
<div className="min-w-0 flex-1">
<h2 className="mb-4 flex items-center gap-2 text-lg font-semibold">
<h2 className="mb-4 flex items-center gap-2 text-lg font-semibold text-black dark:text-white">
<BookMarked className="h-4 w-4" /> Repositories
</h2>
<div className="divide-y">
@@ -122,7 +146,7 @@ export function ReposPage() {
}
return (
<div className="flex w-full gap-8 px-4 py-8">
<div className="flex w-full flex-1 gap-8 bg-[#F3F3F3] px-4 py-8 dark:bg-[#171717]">
{/* Main content */}
<div className="min-w-0 flex-1">
{/* Mobile-only connect button */}
@@ -130,7 +154,7 @@ export function ReposPage() {
<ConnectButton className="w-full" />
</div>
<h2 className="mb-4 flex items-center gap-2 text-lg font-semibold">
<h2 className="mb-4 flex items-center gap-2 text-lg font-semibold text-black dark:text-white">
<BookMarked className="h-4 w-4" /> Repositories
</h2>
@@ -140,13 +164,13 @@ export function ReposPage() {
placeholder="Find a repository..."
value={search}
onChange={(e) => setSearch(e.target.value)}
className="flex-1"
className="flex-1 border-black/10 bg-white text-black placeholder:text-black/40 dark:border-white/10 dark:bg-white/5 dark:text-white dark:placeholder:text-white/40"
/>
<select
value={sort}
onChange={(e) => setSort(e.target.value as SortOrder)}
aria-label="Sort repositories"
className="rounded-md border border-input bg-background px-3 py-1 text-sm text-foreground shadow-xs focus-visible:outline-hidden focus-visible:ring-1 focus-visible:ring-ring"
className="rounded-md border border-black/10 bg-white px-3 py-1 text-sm text-black shadow-xs focus-visible:outline-hidden focus-visible:ring-1 focus-visible:ring-black dark:border-white/10 dark:bg-white/5 dark:text-white dark:focus-visible:ring-white"
>
<option value="newest">Newest</option>
<option value="oldest">Oldest</option>
@@ -156,9 +180,9 @@ export function ReposPage() {
{/* Repo list */}
{filteredRepos.length > 0 ? (
<div className="divide-y">
<div className="divide-y divide-black/10 dark:divide-white/10">
{filteredRepos.map((repo) => (
<RepoListItem key={repo.id} repo={repo} />
<RepoListItem key={repo.id} repo={repo} preview={showMockRepos} />
))}
</div>
) : (
@@ -167,7 +191,7 @@ export function ReposPage() {
</div>
{/* Sidebar */}
<aside className="hidden w-72 shrink-0 border-l border-border pl-8 lg:block">
<aside className="hidden w-72 shrink-0 border-l border-black/10 pl-8 dark:border-white/10 lg:block">
<OrgSidebar repos={repos} />
</aside>
</div>
+8 -3
View File
@@ -17,13 +17,18 @@ export interface RepoContext {
error: Error | null;
}
export function useRepoContext(repoId: string): RepoContext {
export function useRepoContext(
repoId: string,
{ preview = false } = {},
): RepoContext {
const {
data: repo,
isLoading: repoLoading,
error: repoError,
} = useRepo(repoId);
const { data: refs, isLoading: refsLoading } = useRepoRefs(repoId);
} = useRepo(repoId, { preview });
const { data: refs, isLoading: refsLoading } = useRepoRefs(repoId, {
preview,
});
return {
owner: repo?.owner ?? "",
+10 -3
View File
@@ -59,10 +59,17 @@ async function fetchRepoRefs(repoId: string): Promise<RepoRefs> {
return parseRefs(events);
}
export function useRepoRefs(repoId: string) {
export function useRepoRefs(repoId: string, { preview = false } = {}) {
const mockRefs: RepoRefs = {
branches: ["main"],
tags: ["v0.1.0"],
head: { ref: "main", sha: "a".repeat(40) },
};
return useQuery({
queryKey: ["repo-refs", repoId],
queryFn: () => fetchRepoRefs(repoId),
queryKey: preview ? ["repo-refs", "mock", repoId] : ["repo-refs", repoId],
queryFn: preview ? async () => mockRefs : () => fetchRepoRefs(repoId),
initialData: preview ? mockRefs : undefined,
staleTime: 60_000,
});
}
+7 -4
View File
@@ -1,6 +1,7 @@
import { useQuery } from "@tanstack/react-query";
import { queryEvents, type NostrEvent } from "@/shared/lib/nostr-client";
import { relayWsUrl } from "@/shared/lib/relay-url";
import { mockRepos } from "./mock-repos";
export interface Repo {
id: string;
@@ -68,18 +69,20 @@ async function fetchRepos(): Promise<Repo[]> {
.sort((a, b) => b.createdAt - a.createdAt);
}
export function useRepos() {
export function useRepos({ enabled = true }: { enabled?: boolean } = {}) {
return useQuery({
queryKey: ["repos"],
queryFn: fetchRepos,
enabled,
staleTime: 60_000,
});
}
export function useRepo(repoId: string) {
export function useRepo(repoId: string, { preview = false } = {}) {
return useQuery({
queryKey: ["repos"],
queryFn: fetchRepos,
queryKey: preview ? ["repos", "mock"] : ["repos"],
queryFn: preview ? async () => mockRepos : fetchRepos,
initialData: preview ? mockRepos : undefined,
staleTime: 60_000,
select: (repos) => repos.find((r) => r.id === repoId),
});
+15 -14
View File
@@ -15,33 +15,35 @@ type ThemeContextValue = {
setTheme: (theme: Theme) => void;
};
const STORAGE_KEY = "buzz-web-theme";
const ThemeContext = createContext<ThemeContextValue | undefined>(undefined);
function getSystemDark(): boolean {
return window.matchMedia("(prefers-color-scheme: dark)").matches;
}
function getInitialTheme(): Theme {
if (!import.meta.env.DEV) return "system";
const previewTheme = new URLSearchParams(window.location.search).get(
"previewTheme",
);
return previewTheme === "light" || previewTheme === "dark"
? previewTheme
: "system";
}
function applyClass(isDark: boolean) {
const root = document.documentElement;
root.classList.remove("light", "dark");
root.classList.add(isDark ? "dark" : "light");
}
function readStoredTheme(): Theme {
const stored = window.localStorage.getItem(STORAGE_KEY);
if (stored === "light" || stored === "dark" || stored === "system") {
return stored;
}
return "system";
}
export function ThemeProvider({ children }: { children: ReactNode }) {
const [theme, setThemeState] = useState<Theme>(readStoredTheme);
const [theme, setThemeState] = useState<Theme>(getInitialTheme);
const [isDark, setIsDark] = useState<boolean>(() => {
const t = readStoredTheme();
const dark = t === "system" ? getSystemDark() : t === "dark";
const initialTheme = getInitialTheme();
const dark =
initialTheme === "system" ? getSystemDark() : initialTheme === "dark";
applyClass(dark);
return dark;
});
@@ -65,7 +67,6 @@ export function ThemeProvider({ children }: { children: ReactNode }) {
}, [theme]);
const setTheme = useCallback((t: Theme) => {
window.localStorage.setItem(STORAGE_KEY, t);
setThemeState(t);
}, []);