From a7d1c84773cc6d4ac40755b493edd41b1f08a0eb Mon Sep 17 00:00:00 2001 From: Wes Date: Tue, 5 May 2026 07:58:02 -0700 Subject: [PATCH 1/6] feat(web): redesign repos page to GitHub-org-style layout Replace 3-column card grid with two-column layout: left sidebar with "Connect to Relay" button and People section, main content area with vertical repo list, search input, and sort dropdown. - Add OrgSidebar with relay deep link button and contributor identicons - Replace RepoCard with RepoListItem (name + Public badge + description + metadata) - Add client-side search (name/description) and sort (newest/oldest/name) - Add shared badge.tsx and input.tsx UI components - Extract unique contributors from p tags into Repo interface Co-Authored-By: Claude Opus 4.6 --- web/src/features/repos/ui/OrgSidebar.tsx | 83 ++++++++++ web/src/features/repos/ui/RepoCard.tsx | 130 ---------------- web/src/features/repos/ui/RepoListItem.tsx | 71 +++++++++ web/src/features/repos/ui/ReposPage.tsx | 169 ++++++++++++++------- web/src/features/repos/use-repos.ts | 5 +- web/src/shared/ui/badge.tsx | 30 ++++ web/src/shared/ui/input.tsx | 20 +++ 7 files changed, 323 insertions(+), 185 deletions(-) create mode 100644 web/src/features/repos/ui/OrgSidebar.tsx delete mode 100644 web/src/features/repos/ui/RepoCard.tsx create mode 100644 web/src/features/repos/ui/RepoListItem.tsx create mode 100644 web/src/shared/ui/badge.tsx create mode 100644 web/src/shared/ui/input.tsx diff --git a/web/src/features/repos/ui/OrgSidebar.tsx b/web/src/features/repos/ui/OrgSidebar.tsx new file mode 100644 index 00000000..5cdf92a4 --- /dev/null +++ b/web/src/features/repos/ui/OrgSidebar.tsx @@ -0,0 +1,83 @@ +import { ExternalLink, Users } from "lucide-react"; +import { useMemo } from "react"; + +import { relayWsUrl } from "@/shared/lib/relay-url"; +import { Button } from "@/shared/ui/button"; +import type { Repo } from "../use-repos"; + +const MAX_AVATARS = 20; + +/** Simple hash of a hex pubkey to a hue value (0-360). */ +function pubkeyToHue(hex: string): number { + let hash = 0; + for (let i = 0; i < hex.length; i++) { + hash = (hash * 31 + hex.charCodeAt(i)) | 0; + } + return Math.abs(hash) % 360; +} + +function PubkeyAvatar({ pubkey }: { pubkey: string }) { + const hue = pubkeyToHue(pubkey); + return ( +
+ {pubkey.slice(0, 2)} +
+ ); +} + +export function OrgSidebar({ repos }: { repos: Repo[] }) { + const deepLink = `sprout://connect?relay=${encodeURIComponent(relayWsUrl())}`; + + const uniquePubkeys = useMemo(() => { + const set = new Set(); + for (const repo of repos) { + set.add(repo.owner); + for (const c of repo.contributors) { + set.add(c); + } + } + return [...set]; + }, [repos]); + + const visiblePubkeys = uniquePubkeys.slice(0, MAX_AVATARS); + const overflowCount = uniquePubkeys.length - MAX_AVATARS; + + return ( +
+ {/* Connect to Relay */} + + + {/* People section */} + {uniquePubkeys.length > 0 && ( +
+

+ + People +

+
+ {visiblePubkeys.map((pk) => ( + + ))} +
+ {overflowCount > 0 && ( + + )} +
+ )} +
+ ); +} diff --git a/web/src/features/repos/ui/RepoCard.tsx b/web/src/features/repos/ui/RepoCard.tsx deleted file mode 100644 index 1e45e5a7..00000000 --- a/web/src/features/repos/ui/RepoCard.tsx +++ /dev/null @@ -1,130 +0,0 @@ -import { Check, Copy, ExternalLink, GitBranch } from "lucide-react"; -import { useCallback, useState } from "react"; -import { toast } from "sonner"; - -import { relayWsUrl } from "@/shared/lib/relay-url"; -import { Button } from "@/shared/ui/button"; -import { - Card, - CardContent, - CardDescription, - CardFooter, - CardHeader, - CardTitle, -} from "@/shared/ui/card"; -import { Tooltip, TooltipContent, TooltipTrigger } from "@/shared/ui/tooltip"; -import type { Repo } from "../use-repos"; - -function truncateHex(hex: string): string { - if (hex.length <= 12) return hex; - return `${hex.slice(0, 8)}...${hex.slice(-4)}`; -} - -function formatDate(unix: number): string { - return new Date(unix * 1000).toLocaleDateString(undefined, { - year: "numeric", - month: "short", - day: "numeric", - }); -} - -function CopyButton({ value, label }: { value: string; label: string }) { - const [copied, setCopied] = useState(false); - - const handleCopy = useCallback(() => { - navigator.clipboard.writeText(value).then( - () => { - setCopied(true); - toast.success("Copied to clipboard"); - setTimeout(() => setCopied(false), 2000); - }, - () => { - toast.error("Failed to copy to clipboard"); - }, - ); - }, [value]); - - return ( - - - - - Copy - - ); -} - -export function RepoCard({ repo }: { repo: Repo }) { - const relayUrl = relayWsUrl(); - const deepLink = `sprout://connect?relay=${encodeURIComponent(relayUrl)}`; - - return ( - - -
- -
- {repo.name} - {repo.description && ( - - {repo.description} - - )} -
-
-
- - - {repo.cloneUrls.length > 0 && ( -
- - Clone - - {repo.cloneUrls.map((url) => ( -
- - {url} - - -
- ))} -
- )} - -
- - - - {truncateHex(repo.owner)} - - - {repo.owner} - - {formatDate(repo.createdAt)} -
-
- - - - - -
- ); -} diff --git a/web/src/features/repos/ui/RepoListItem.tsx b/web/src/features/repos/ui/RepoListItem.tsx new file mode 100644 index 00000000..1e57f625 --- /dev/null +++ b/web/src/features/repos/ui/RepoListItem.tsx @@ -0,0 +1,71 @@ +import { BookMarked } from "lucide-react"; + +import { Badge } from "@/shared/ui/badge"; +import { Tooltip, TooltipContent, TooltipTrigger } from "@/shared/ui/tooltip"; +import type { Repo } from "../use-repos"; + +function truncateHex(hex: string): string { + if (hex.length <= 12) return hex; + return `${hex.slice(0, 8)}...${hex.slice(-4)}`; +} + +function relativeTime(unix: number): string { + const now = Date.now(); + const diff = now - unix * 1000; + const seconds = Math.floor(diff / 1000); + const minutes = Math.floor(seconds / 60); + const hours = Math.floor(minutes / 60); + const days = Math.floor(hours / 24); + + if (days > 30) { + const months = Math.floor(days / 30); + return months === 1 ? "1 month ago" : `${months} months ago`; + } + if (days > 0) return days === 1 ? "1 day ago" : `${days} days ago`; + if (hours > 0) return hours === 1 ? "1 hour ago" : `${hours} hours ago`; + if (minutes > 0) + return minutes === 1 ? "1 minute ago" : `${minutes} minutes ago`; + return "just now"; +} + +export function RepoListItem({ repo }: { repo: Repo }) { + const link = repo.webUrl ?? "#"; + + return ( +
+ {/* Row 1: Name + badge */} +
+ + + {repo.name} + + + Public + +
+ + {/* Row 2: Description */} + {repo.description && ( +

+ {repo.description} +

+ )} + + {/* Row 3: Metadata */} +
+ + + + {truncateHex(repo.owner)} + + + {repo.owner} + + Updated {relativeTime(repo.createdAt)} +
+
+ ); +} diff --git a/web/src/features/repos/ui/ReposPage.tsx b/web/src/features/repos/ui/ReposPage.tsx index f6e6b369..4ca101fb 100644 --- a/web/src/features/repos/ui/ReposPage.tsx +++ b/web/src/features/repos/ui/ReposPage.tsx @@ -1,51 +1,44 @@ -import { GitBranch } from "lucide-react"; +import { BookMarked, GitBranch } from "lucide-react"; import { toast } from "sonner"; -import { useEffect } from "react"; +import { useEffect, useMemo, useState } from "react"; -import { Card, CardContent, CardHeader } from "@/shared/ui/card"; +import { Input } from "@/shared/ui/input"; import { useRepos } from "../use-repos"; -import { RepoCard } from "./RepoCard"; +import { OrgSidebar } from "./OrgSidebar"; +import { RepoListItem } from "./RepoListItem"; -function RepoCardSkeleton() { +type SortOrder = "newest" | "oldest" | "name"; + +function ListItemSkeleton() { return ( - - -
-
-
-
-
-
-
- - -
-
-
-
-
-
-
-
- -
-
-
+
+
+
+
+
- +
+
+
+
+
+
); } -function EmptyState() { +function EmptyState({ hasSearch }: { hasSearch: boolean }) { return (
-

No repositories yet

+

+ {hasSearch ? "No matching repositories" : "No repositories yet"} +

- Repositories published to this relay will appear here. Push a git repo - using the Sprout desktop app to get started. + {hasSearch + ? "Try adjusting your search term." + : "Repositories published to this relay will appear here. Push a git repo using the Sprout desktop app to get started."}

); @@ -53,6 +46,8 @@ function EmptyState() { export function ReposPage() { const { data: repos, isLoading, error } = useRepos(); + const [search, setSearch] = useState(""); + const [sort, setSort] = useState("newest"); useEffect(() => { if (error) { @@ -62,42 +57,108 @@ export function ReposPage() { } }, [error]); + const filteredRepos = useMemo(() => { + if (!repos) return []; + + const term = search.toLowerCase(); + let result = repos.filter( + (r) => + r.name.toLowerCase().includes(term) || + r.description.toLowerCase().includes(term), + ); + + switch (sort) { + case "newest": + result = result.sort((a, b) => b.createdAt - a.createdAt); + break; + case "oldest": + result = result.sort((a, b) => a.createdAt - b.createdAt); + break; + case "name": + result = result.sort((a, b) => + a.name.localeCompare(b.name, undefined, { sensitivity: "base" }), + ); + break; + } + + return result; + }, [repos, search, sort]); + if (isLoading) { return ( -
-

- Repositories -

-
- {["a", "b", "c", "d", "e", "f"].map((key) => ( - - ))} +
+
+

+ Repositories +

+
+ {["a", "b", "c", "d", "e"].map((key) => ( + + ))} +
+
); } if (!repos || repos.length === 0) { return ( -
-

- Repositories -

- +
+
+

+ Repositories +

+ +
+
); } return ( -
-

- Repositories -

-
- {repos.map((repo) => ( - - ))} +
+ {/* Main content */} +
+

+ Repositories +

+ + {/* Search + Sort bar */} +
+ setSearch(e.target.value)} + className="flex-1" + /> + +
+ + {/* Repo list */} + {filteredRepos.length > 0 ? ( +
+ {filteredRepos.map((repo) => ( + + ))} +
+ ) : ( + 0} /> + )}
+ + {/* Sidebar */} +
); } diff --git a/web/src/features/repos/use-repos.ts b/web/src/features/repos/use-repos.ts index a1eb7089..79077aa5 100644 --- a/web/src/features/repos/use-repos.ts +++ b/web/src/features/repos/use-repos.ts @@ -9,6 +9,7 @@ export interface Repo { cloneUrls: string[]; webUrl: string | null; owner: string; + contributors: string[]; createdAt: number; } @@ -28,7 +29,8 @@ function eventToRepo(event: NostrEvent): Repo { const description = getTag(event, "description") || event.content || ""; const cloneUrls = getAllTags(event, "clone"); const webUrl = getTag(event, "web") ?? null; - const owner = getAllTags(event, "p")[0] ?? event.pubkey; + const contributors = getAllTags(event, "p"); + const owner = event.pubkey; return { id: d, @@ -37,6 +39,7 @@ function eventToRepo(event: NostrEvent): Repo { cloneUrls, webUrl, owner, + contributors, createdAt: event.created_at, }; } diff --git a/web/src/shared/ui/badge.tsx b/web/src/shared/ui/badge.tsx new file mode 100644 index 00000000..b76a601d --- /dev/null +++ b/web/src/shared/ui/badge.tsx @@ -0,0 +1,30 @@ +import { cn } from "@/shared/lib/cn"; +import { type VariantProps, cva } from "class-variance-authority"; + +const badgeVariants = cva( + "inline-flex items-center rounded-md border px-2 py-0.5 text-xs font-medium transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2", + { + variants: { + variant: { + default: "border-transparent bg-primary text-primary-foreground", + secondary: "border-transparent bg-secondary text-secondary-foreground", + destructive: + "border-transparent bg-destructive text-destructive-foreground", + outline: "text-foreground", + }, + }, + defaultVariants: { variant: "default" }, + }, +); + +interface BadgeProps + extends React.HTMLAttributes, + VariantProps {} + +function Badge({ className, variant, ...props }: BadgeProps) { + return ( +
+ ); +} + +export { Badge, badgeVariants }; diff --git a/web/src/shared/ui/input.tsx b/web/src/shared/ui/input.tsx new file mode 100644 index 00000000..d1fbef91 --- /dev/null +++ b/web/src/shared/ui/input.tsx @@ -0,0 +1,20 @@ +import { cn } from "@/shared/lib/cn"; +import { type InputHTMLAttributes, forwardRef } from "react"; + +const Input = forwardRef< + HTMLInputElement, + InputHTMLAttributes +>(({ className, type, ...props }, ref) => ( + +)); +Input.displayName = "Input"; + +export { Input }; From dc59ed00ebe84315797509420b998cc75a284f6a Mon Sep 17 00:00:00 2001 From: Wes Date: Tue, 5 May 2026 08:02:20 -0700 Subject: [PATCH 2/6] refactor(web): make repos page the home page Merge the separate / and /repos routes into a single index route that renders the org-style repos page directly. Remove the "Repos" nav link since there's only one page now. Update smoke test to check for the Repositories section instead of the old relay URL card. Co-Authored-By: Claude Opus 4.6 --- web/src/app/routeTree.gen.ts | 24 +++--------------------- web/src/app/routes.ts | 7 ++----- web/src/app/routes/index.tsx | 34 ++-------------------------------- web/src/app/routes/repos.tsx | 6 ------ web/src/app/routes/root.tsx | 26 ++++++-------------------- web/tests/e2e/smoke.spec.ts | 6 ++---- 6 files changed, 15 insertions(+), 88 deletions(-) delete mode 100644 web/src/app/routes/repos.tsx diff --git a/web/src/app/routeTree.gen.ts b/web/src/app/routeTree.gen.ts index b978d665..d4434218 100644 --- a/web/src/app/routeTree.gen.ts +++ b/web/src/app/routeTree.gen.ts @@ -5,14 +5,8 @@ // 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 reposRouteImport } from "./routes/repos"; import { Route as indexRouteImport } from "./routes/index"; -const reposRoute = reposRouteImport.update({ - id: "/repos", - path: "/repos", - getParentRoute: () => rootRouteImport, -} as any); const indexRoute = indexRouteImport.update({ id: "/", path: "/", @@ -21,39 +15,28 @@ const indexRoute = indexRouteImport.update({ export interface FileRoutesByFullPath { "/": typeof indexRoute; - "/repos": typeof reposRoute; } export interface FileRoutesByTo { "/": typeof indexRoute; - "/repos": typeof reposRoute; } export interface FileRoutesById { __root__: typeof rootRouteImport; "/": typeof indexRoute; - "/repos": typeof reposRoute; } export interface FileRouteTypes { fileRoutesByFullPath: FileRoutesByFullPath; - fullPaths: "/" | "/repos"; + fullPaths: "/"; fileRoutesByTo: FileRoutesByTo; - to: "/" | "/repos"; - id: "__root__" | "/" | "/repos"; + to: "/"; + id: "__root__" | "/"; fileRoutesById: FileRoutesById; } export interface RootRouteChildren { indexRoute: typeof indexRoute; - reposRoute: typeof reposRoute; } declare module "@tanstack/react-router" { interface FileRoutesByPath { - "/repos": { - id: "/repos"; - path: "/repos"; - fullPath: "/repos"; - preLoaderRoute: typeof reposRouteImport; - parentRoute: typeof rootRouteImport; - }; "/": { id: "/"; path: "/"; @@ -66,7 +49,6 @@ declare module "@tanstack/react-router" { const rootRouteChildren: RootRouteChildren = { indexRoute: indexRoute, - reposRoute: reposRoute, }; export const routeTree = rootRouteImport ._addFileChildren(rootRouteChildren) diff --git a/web/src/app/routes.ts b/web/src/app/routes.ts index 25c385bb..9f8e4fae 100644 --- a/web/src/app/routes.ts +++ b/web/src/app/routes.ts @@ -1,6 +1,3 @@ -import { index, route, rootRoute } from "@tanstack/virtual-file-routes"; +import { index, rootRoute } from "@tanstack/virtual-file-routes"; -export const routes = rootRoute("root.tsx", [ - index("index.tsx"), - route("/repos", "repos.tsx"), -]); +export const routes = rootRoute("root.tsx", [index("index.tsx")]); diff --git a/web/src/app/routes/index.tsx b/web/src/app/routes/index.tsx index cb86cbab..a118f25d 100644 --- a/web/src/app/routes/index.tsx +++ b/web/src/app/routes/index.tsx @@ -1,36 +1,6 @@ import { createFileRoute } from "@tanstack/react-router"; - -import { - Card, - CardContent, - CardDescription, - CardHeader, - CardTitle, -} from "@/shared/ui/card"; +import { ReposPage } from "@/features/repos/ui/ReposPage"; export const Route = createFileRoute("/")({ - component: HomeRoute, + component: ReposPage, }); - -function HomeRoute() { - const relayUrl = import.meta.env.VITE_RELAY_URL || "ws://localhost:3000"; - - return ( -
- - - Relay - Connected relay endpoint - - - - {relayUrl} - - - -
- ); -} diff --git a/web/src/app/routes/repos.tsx b/web/src/app/routes/repos.tsx deleted file mode 100644 index 86f1d40a..00000000 --- a/web/src/app/routes/repos.tsx +++ /dev/null @@ -1,6 +0,0 @@ -import { createFileRoute } from "@tanstack/react-router"; -import { ReposPage } from "@/features/repos/ui/ReposPage"; - -export const Route = createFileRoute("/repos")({ - component: ReposPage, -}); diff --git a/web/src/app/routes/root.tsx b/web/src/app/routes/root.tsx index 8965e71b..9d8ff1c3 100644 --- a/web/src/app/routes/root.tsx +++ b/web/src/app/routes/root.tsx @@ -5,30 +5,16 @@ export const Route = createRootRoute({ component: RootLayout, }); -function NavLink({ to, children }: { to: string; children: React.ReactNode }) { - return ( - - {children} - - ); -} - function RootLayout() { return (
- + + Sprout +
diff --git a/web/tests/e2e/smoke.spec.ts b/web/tests/e2e/smoke.spec.ts index d2f36ef4..5606c5d8 100644 --- a/web/tests/e2e/smoke.spec.ts +++ b/web/tests/e2e/smoke.spec.ts @@ -5,9 +5,7 @@ test("home page loads with Sprout heading", async ({ page }) => { await expect(page.locator("header")).toContainText("Sprout"); }); -test("relay URL is visible", async ({ page }) => { +test("home page shows repositories section", async ({ page }) => { await page.goto("/"); - const relayUrl = page.getByTestId("relay-url"); - await expect(relayUrl).toBeVisible(); - await expect(relayUrl).toContainText("ws://"); + await expect(page.getByText("Repositories")).toBeVisible(); }); From 96d5df01045124152ef0519e914eb4d047a0d683 Mon Sep 17 00:00:00 2001 From: Wes Date: Tue, 5 May 2026 08:05:23 -0700 Subject: [PATCH 3/6] fix(web): open repo links in new tab via target=_blank The webUrl from kind:30617 points to the relay's server-rendered page, not a client-side route. Open it in a new tab instead of navigating within the SPA. When no webUrl is set, render the name as plain text. Co-Authored-By: Claude Opus 4.6 --- web/src/features/repos/ui/RepoListItem.tsx | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/web/src/features/repos/ui/RepoListItem.tsx b/web/src/features/repos/ui/RepoListItem.tsx index 1e57f625..8ee16c4b 100644 --- a/web/src/features/repos/ui/RepoListItem.tsx +++ b/web/src/features/repos/ui/RepoListItem.tsx @@ -29,19 +29,23 @@ function relativeTime(unix: number): string { } export function RepoListItem({ repo }: { repo: Repo }) { - const link = repo.webUrl ?? "#"; - return (
{/* Row 1: Name + badge */}
- - {repo.name} - + {repo.webUrl ? ( + + {repo.name} + + ) : ( + {repo.name} + )} Public From 2b2c48086d6ac0f54d49686f198f629a55da6085 Mon Sep 17 00:00:00 2001 From: Wes Date: Tue, 5 May 2026 08:11:29 -0700 Subject: [PATCH 4/6] fix(web): address review findings from beth - Add ConnectButton component, show it on mobile (lg:hidden) and in the empty state so the deep link CTA is always accessible - Change "View all N people" from a no-op button to static text - Add aria-label="Sort repositories" to the sort select - Restore /repos route as a redirect to / for bookmark compat Co-Authored-By: Claude Opus 4.6 --- web/src/app/routeTree.gen.ts | 24 ++++++++++++++++++--- web/src/app/routes.ts | 7 ++++-- web/src/app/routes/repos.tsx | 5 +++++ web/src/features/repos/ui/ConnectButton.tsx | 17 +++++++++++++++ web/src/features/repos/ui/OrgSidebar.tsx | 23 ++++++-------------- web/src/features/repos/ui/ReposPage.tsx | 8 +++++++ 6 files changed, 62 insertions(+), 22 deletions(-) create mode 100644 web/src/app/routes/repos.tsx create mode 100644 web/src/features/repos/ui/ConnectButton.tsx diff --git a/web/src/app/routeTree.gen.ts b/web/src/app/routeTree.gen.ts index d4434218..b978d665 100644 --- a/web/src/app/routeTree.gen.ts +++ b/web/src/app/routeTree.gen.ts @@ -5,8 +5,14 @@ // 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 reposRouteImport } from "./routes/repos"; import { Route as indexRouteImport } from "./routes/index"; +const reposRoute = reposRouteImport.update({ + id: "/repos", + path: "/repos", + getParentRoute: () => rootRouteImport, +} as any); const indexRoute = indexRouteImport.update({ id: "/", path: "/", @@ -15,28 +21,39 @@ const indexRoute = indexRouteImport.update({ export interface FileRoutesByFullPath { "/": typeof indexRoute; + "/repos": typeof reposRoute; } export interface FileRoutesByTo { "/": typeof indexRoute; + "/repos": typeof reposRoute; } export interface FileRoutesById { __root__: typeof rootRouteImport; "/": typeof indexRoute; + "/repos": typeof reposRoute; } export interface FileRouteTypes { fileRoutesByFullPath: FileRoutesByFullPath; - fullPaths: "/"; + fullPaths: "/" | "/repos"; fileRoutesByTo: FileRoutesByTo; - to: "/"; - id: "__root__" | "/"; + to: "/" | "/repos"; + id: "__root__" | "/" | "/repos"; fileRoutesById: FileRoutesById; } export interface RootRouteChildren { indexRoute: typeof indexRoute; + reposRoute: typeof reposRoute; } declare module "@tanstack/react-router" { interface FileRoutesByPath { + "/repos": { + id: "/repos"; + path: "/repos"; + fullPath: "/repos"; + preLoaderRoute: typeof reposRouteImport; + parentRoute: typeof rootRouteImport; + }; "/": { id: "/"; path: "/"; @@ -49,6 +66,7 @@ declare module "@tanstack/react-router" { const rootRouteChildren: RootRouteChildren = { indexRoute: indexRoute, + reposRoute: reposRoute, }; export const routeTree = rootRouteImport ._addFileChildren(rootRouteChildren) diff --git a/web/src/app/routes.ts b/web/src/app/routes.ts index 9f8e4fae..25c385bb 100644 --- a/web/src/app/routes.ts +++ b/web/src/app/routes.ts @@ -1,3 +1,6 @@ -import { index, rootRoute } from "@tanstack/virtual-file-routes"; +import { index, route, rootRoute } from "@tanstack/virtual-file-routes"; -export const routes = rootRoute("root.tsx", [index("index.tsx")]); +export const routes = rootRoute("root.tsx", [ + index("index.tsx"), + route("/repos", "repos.tsx"), +]); diff --git a/web/src/app/routes/repos.tsx b/web/src/app/routes/repos.tsx new file mode 100644 index 00000000..b58d39ad --- /dev/null +++ b/web/src/app/routes/repos.tsx @@ -0,0 +1,5 @@ +import { Navigate, createFileRoute } from "@tanstack/react-router"; + +export const Route = createFileRoute("/repos")({ + component: () => , +}); diff --git a/web/src/features/repos/ui/ConnectButton.tsx b/web/src/features/repos/ui/ConnectButton.tsx new file mode 100644 index 00000000..d4d7c620 --- /dev/null +++ b/web/src/features/repos/ui/ConnectButton.tsx @@ -0,0 +1,17 @@ +import { ExternalLink } from "lucide-react"; + +import { relayWsUrl } from "@/shared/lib/relay-url"; +import { Button } from "@/shared/ui/button"; + +export function ConnectButton({ className }: { className?: string }) { + const deepLink = `sprout://connect?relay=${encodeURIComponent(relayWsUrl())}`; + + return ( + + ); +} diff --git a/web/src/features/repos/ui/OrgSidebar.tsx b/web/src/features/repos/ui/OrgSidebar.tsx index 5cdf92a4..18b0a578 100644 --- a/web/src/features/repos/ui/OrgSidebar.tsx +++ b/web/src/features/repos/ui/OrgSidebar.tsx @@ -1,9 +1,8 @@ -import { ExternalLink, Users } from "lucide-react"; +import { Users } from "lucide-react"; import { useMemo } from "react"; -import { relayWsUrl } from "@/shared/lib/relay-url"; -import { Button } from "@/shared/ui/button"; import type { Repo } from "../use-repos"; +import { ConnectButton } from "./ConnectButton"; const MAX_AVATARS = 20; @@ -30,8 +29,6 @@ function PubkeyAvatar({ pubkey }: { pubkey: string }) { } export function OrgSidebar({ repos }: { repos: Repo[] }) { - const deepLink = `sprout://connect?relay=${encodeURIComponent(relayWsUrl())}`; - const uniquePubkeys = useMemo(() => { const set = new Set(); for (const repo of repos) { @@ -49,12 +46,7 @@ export function OrgSidebar({ repos }: { repos: Repo[] }) { return (
{/* Connect to Relay */} - + {/* People section */} {uniquePubkeys.length > 0 && ( @@ -69,12 +61,9 @@ export function OrgSidebar({ repos }: { repos: Repo[] }) { ))}
{overflowCount > 0 && ( - + + {uniquePubkeys.length} people + )}
)} diff --git a/web/src/features/repos/ui/ReposPage.tsx b/web/src/features/repos/ui/ReposPage.tsx index 4ca101fb..0b96ca95 100644 --- a/web/src/features/repos/ui/ReposPage.tsx +++ b/web/src/features/repos/ui/ReposPage.tsx @@ -4,6 +4,7 @@ import { useEffect, useMemo, useState } from "react"; import { Input } from "@/shared/ui/input"; import { useRepos } from "../use-repos"; +import { ConnectButton } from "./ConnectButton"; import { OrgSidebar } from "./OrgSidebar"; import { RepoListItem } from "./RepoListItem"; @@ -40,6 +41,7 @@ function EmptyState({ hasSearch }: { hasSearch: boolean }) { ? "Try adjusting your search term." : "Repositories published to this relay will appear here. Push a git repo using the Sprout desktop app to get started."}

+ {!hasSearch && }
); } @@ -120,6 +122,11 @@ export function ReposPage() {
{/* Main content */}
+ {/* Mobile-only connect button */} +
+ +
+

Repositories

@@ -135,6 +142,7 @@ export function ReposPage() {