diff --git a/package.json b/package.json
index 7f8758b33..67e3d55df 100644
--- a/package.json
+++ b/package.json
@@ -34,6 +34,8 @@
"class-variance-authority": "^0.7.0",
"client-only": "^0.0.1",
"clsx": "^2.1.1",
+ "embla-carousel-auto-scroll": "^8.3.0",
+ "embla-carousel-react": "^8.3.0",
"escape-string-regexp": "^5.0.0",
"http-status-codes": "^2.3.0",
"lucide-react": "^0.435.0",
diff --git a/src/app/navigationMenu.tsx b/src/app/navigationMenu.tsx
index 2f20319e7..4aebdc563 100644
--- a/src/app/navigationMenu.tsx
+++ b/src/app/navigationMenu.tsx
@@ -30,11 +30,13 @@ export const NavigationMenu = () => {
src={logoDark}
className="h-11 w-auto hidden dark:block"
alt={"Sourcebot logo"}
+ priority={true}
/>
diff --git a/src/app/page.tsx b/src/app/page.tsx
index 82e3634a0..be30eb674 100644
--- a/src/app/page.tsx
+++ b/src/app/page.tsx
@@ -1,26 +1,34 @@
+import { listRepositories } from "@/lib/server/searchService";
+import { isServiceError } from "@/lib/utils";
import Image from "next/image";
+import { Suspense } from "react";
import logoDark from "../../public/sb_logo_dark_large.png";
import logoLight from "../../public/sb_logo_light_large.png";
import { NavigationMenu } from "./navigationMenu";
+import { RepositoryCarousel } from "./repositoryCarousel";
import { SearchBar } from "./searchBar";
+import { Separator } from "@/components/ui/separator";
-export default function Home() {
+
+export default async function Home() {
return (
{/* TopBar */}
-
-
+
+
@@ -28,7 +36,148 @@ export default function Home() {
autoFocus={true}
/>
+
+ ...
}>
+
+
+
+
+
+
How to search
+
+
+
+ test todo (both test and todo)
+
+
+ test or todo (either test or todo)
+
+
+ {`"exit boot"`} (exact match)
+
+
+ TODO case:yes (case sensitive)
+
+
+
+
+ file:README setup (by filename)
+
+
+ repo:torvalds/linux test (by repo)
+
+
+ lang:typescript (by language)
+
+
+ branch:HEAD (by branch)
+
+
+
+
+ file:{`\\.py$`} {`(files that end in ".py")`}
+
+
+ sym:main {`(symbols named "main")`}
+
+
+ todo -lang:c (negate filter)
+
+
+ content:README (search content only)
+
+
+
+
)
}
+
+const RepositoryList = async () => {
+ const _repos = await listRepositories();
+
+ if (isServiceError(_repos)) {
+ return null;
+ }
+
+ const repos = _repos.List.Repos.map((repo) => repo.Repository);
+
+ if (repos.length === 0) {
+ return
+ Get started
+
+ {` configuring Sourcebot.`}
+
+ ;
+ }
+
+ return (
+
+ )
+}
+
+const HowToSection = ({ title, children }: { title: string, children: React.ReactNode }) => {
+ return (
+
+ {title}
+ {children}
+
+ )
+
+}
+
+const Highlight = ({ children }: { children: React.ReactNode }) => {
+ return (
+
+ {children}
+
+ )
+}
+
+const QueryExample = ({ children }: { children: React.ReactNode }) => {
+ return (
+
+ {children}
+
+ )
+}
+
+const QueryExplanation = ({ children }: { children: React.ReactNode }) => {
+ return (
+
+ {children}
+
+ )
+}
+
+const Query = ({ query, children }: { query: string, children: React.ReactNode }) => {
+ return (
+
+ {children}
+
+ )
+}
diff --git a/src/app/repos/columns.tsx b/src/app/repos/columns.tsx
index 021d3f8f9..a9b9fdd53 100644
--- a/src/app/repos/columns.tsx
+++ b/src/app/repos/columns.tsx
@@ -1,11 +1,10 @@
'use client';
import { Button } from "@/components/ui/button";
+import { getRepoCodeHostInfo } from "@/lib/utils";
import { Column, ColumnDef } from "@tanstack/react-table"
import { ArrowUpDown } from "lucide-react"
-
-
export type RepositoryColumnInfo = {
name: string;
branches: {
@@ -25,6 +24,24 @@ export const columns: ColumnDef[] = [
{
accessorKey: "name",
header: "Name",
+ cell: ({ row }) => {
+ const repo = row.original;
+ const info = getRepoCodeHostInfo(repo.name);
+ return (
+
+ {
+ if (info?.repoLink) {
+ window.open(info.repoLink, "_blank");
+ }
+ }}
+ >
+ {repo.name}
+
+
+ );
+ }
},
{
accessorKey: "branches",
diff --git a/src/app/repositoryCarousel.tsx b/src/app/repositoryCarousel.tsx
new file mode 100644
index 000000000..aa498686e
--- /dev/null
+++ b/src/app/repositoryCarousel.tsx
@@ -0,0 +1,98 @@
+'use client';
+
+import { Repository } from "@/lib/schemas";
+import {
+ Carousel,
+ CarouselContent,
+ CarouselItem,
+} from "@/components/ui/carousel";
+import Autoscroll from "embla-carousel-auto-scroll";
+import { getRepoCodeHostInfo } from "@/lib/utils";
+import Image from "next/image";
+import { FileIcon } from "@radix-ui/react-icons";
+import clsx from "clsx";
+
+interface RepositoryCarouselProps {
+ repos: Repository[];
+}
+
+export const RepositoryCarousel = ({
+ repos,
+}: RepositoryCarouselProps) => {
+ return (
+
+
+ {repos.map((repo, index) => (
+
+
+
+ ))}
+
+
+ )
+};
+
+interface RepositoryBadgeProps {
+ repo: Repository;
+}
+
+const RepositoryBadge = ({
+ repo
+}: RepositoryBadgeProps) => {
+ const { repoIcon, repoName, repoLink } = (() => {
+ const info = getRepoCodeHostInfo(repo.Name);
+
+ if (info) {
+ return {
+ repoIcon: ,
+ repoName: info.repoName,
+ repoLink: info.repoLink,
+ }
+ }
+
+ return {
+ repoIcon: ,
+ repoName: repo.Name,
+ repoLink: undefined,
+ }
+ })();
+
+ return (
+ {
+ if (repoLink !== undefined) {
+ window.open(repoLink, "_blank");
+ }
+ }}
+ className={clsx("flex flex-row items-center gap-2 border rounded-md p-2 text-clip", {
+ "cursor-pointer": repoLink !== undefined,
+ })}
+ >
+ {repoIcon}
+
+ {repoName}
+
+
+ )
+}
diff --git a/src/components/ui/carousel.tsx b/src/components/ui/carousel.tsx
new file mode 100644
index 000000000..ec505d00d
--- /dev/null
+++ b/src/components/ui/carousel.tsx
@@ -0,0 +1,262 @@
+"use client"
+
+import * as React from "react"
+import useEmblaCarousel, {
+ type UseEmblaCarouselType,
+} from "embla-carousel-react"
+import { ArrowLeft, ArrowRight } from "lucide-react"
+
+import { cn } from "@/lib/utils"
+import { Button } from "@/components/ui/button"
+
+type CarouselApi = UseEmblaCarouselType[1]
+type UseCarouselParameters = Parameters
+type CarouselOptions = UseCarouselParameters[0]
+type CarouselPlugin = UseCarouselParameters[1]
+
+type CarouselProps = {
+ opts?: CarouselOptions
+ plugins?: CarouselPlugin
+ orientation?: "horizontal" | "vertical"
+ setApi?: (api: CarouselApi) => void
+}
+
+type CarouselContextProps = {
+ carouselRef: ReturnType[0]
+ api: ReturnType[1]
+ scrollPrev: () => void
+ scrollNext: () => void
+ canScrollPrev: boolean
+ canScrollNext: boolean
+} & CarouselProps
+
+const CarouselContext = React.createContext(null)
+
+function useCarousel() {
+ const context = React.useContext(CarouselContext)
+
+ if (!context) {
+ throw new Error("useCarousel must be used within a ")
+ }
+
+ return context
+}
+
+const Carousel = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes & CarouselProps
+>(
+ (
+ {
+ orientation = "horizontal",
+ opts,
+ setApi,
+ plugins,
+ className,
+ children,
+ ...props
+ },
+ ref
+ ) => {
+ const [carouselRef, api] = useEmblaCarousel(
+ {
+ ...opts,
+ axis: orientation === "horizontal" ? "x" : "y",
+ },
+ plugins
+ )
+ const [canScrollPrev, setCanScrollPrev] = React.useState(false)
+ const [canScrollNext, setCanScrollNext] = React.useState(false)
+
+ const onSelect = React.useCallback((api: CarouselApi) => {
+ if (!api) {
+ return
+ }
+
+ setCanScrollPrev(api.canScrollPrev())
+ setCanScrollNext(api.canScrollNext())
+ }, [])
+
+ const scrollPrev = React.useCallback(() => {
+ api?.scrollPrev()
+ }, [api])
+
+ const scrollNext = React.useCallback(() => {
+ api?.scrollNext()
+ }, [api])
+
+ const handleKeyDown = React.useCallback(
+ (event: React.KeyboardEvent) => {
+ if (event.key === "ArrowLeft") {
+ event.preventDefault()
+ scrollPrev()
+ } else if (event.key === "ArrowRight") {
+ event.preventDefault()
+ scrollNext()
+ }
+ },
+ [scrollPrev, scrollNext]
+ )
+
+ React.useEffect(() => {
+ if (!api || !setApi) {
+ return
+ }
+
+ setApi(api)
+ }, [api, setApi])
+
+ React.useEffect(() => {
+ if (!api) {
+ return
+ }
+
+ onSelect(api)
+ api.on("reInit", onSelect)
+ api.on("select", onSelect)
+
+ return () => {
+ api?.off("select", onSelect)
+ }
+ }, [api, onSelect])
+
+ return (
+
+
+ {children}
+
+
+ )
+ }
+)
+Carousel.displayName = "Carousel"
+
+const CarouselContent = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => {
+ const { carouselRef, orientation } = useCarousel()
+
+ return (
+
+ )
+})
+CarouselContent.displayName = "CarouselContent"
+
+const CarouselItem = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => {
+ const { orientation } = useCarousel()
+
+ return (
+
+ )
+})
+CarouselItem.displayName = "CarouselItem"
+
+const CarouselPrevious = React.forwardRef<
+ HTMLButtonElement,
+ React.ComponentProps
+>(({ className, variant = "outline", size = "icon", ...props }, ref) => {
+ const { orientation, scrollPrev, canScrollPrev } = useCarousel()
+
+ return (
+
+ )
+})
+CarouselPrevious.displayName = "CarouselPrevious"
+
+const CarouselNext = React.forwardRef<
+ HTMLButtonElement,
+ React.ComponentProps
+>(({ className, variant = "outline", size = "icon", ...props }, ref) => {
+ const { orientation, scrollNext, canScrollNext } = useCarousel()
+
+ return (
+
+ )
+})
+CarouselNext.displayName = "CarouselNext"
+
+export {
+ type CarouselApi,
+ Carousel,
+ CarouselContent,
+ CarouselItem,
+ CarouselPrevious,
+ CarouselNext,
+}
diff --git a/src/lib/schemas.ts b/src/lib/schemas.ts
index a6c32666f..a3b35b423 100644
--- a/src/lib/schemas.ts
+++ b/src/lib/schemas.ts
@@ -93,6 +93,7 @@ export const fileSourceResponseSchema = z.object({
export type ListRepositoriesResponse = z.infer;
+export type Repository = z.infer;
// @see : https://github.com/TaqlaAI/zoekt/blob/3780e68cdb537d5a7ed2c84d9b3784f80c7c5d04/api.go#L728
const repoStatsSchema = z.object({
diff --git a/yarn.lock b/yarn.lock
index 1d436c44e..7b993c0e1 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1631,6 +1631,29 @@ eastasianwidth@^0.2.0:
resolved "https://registry.yarnpkg.com/eastasianwidth/-/eastasianwidth-0.2.0.tgz#696ce2ec0aa0e6ea93a397ffcf24aa7840c827cb"
integrity sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==
+embla-carousel-auto-scroll@^8.3.0:
+ version "8.3.0"
+ resolved "https://registry.yarnpkg.com/embla-carousel-auto-scroll/-/embla-carousel-auto-scroll-8.3.0.tgz#604b85ae028c896c98b9949b30588f3c9eb798b0"
+ integrity sha512-ybXWqCTWvl+DeGwtGw0tUU1bOKglS/Ov8F5fueMkiwySKrSFAHizdqSrlMR1SQEXNZHMPH9LqCz7MajNYkdeAQ==
+
+embla-carousel-react@^8.3.0:
+ version "8.3.0"
+ resolved "https://registry.yarnpkg.com/embla-carousel-react/-/embla-carousel-react-8.3.0.tgz#8aa6b9b77c3e900349a7215cb31b7ead6a84a715"
+ integrity sha512-P1FlinFDcIvggcErRjNuVqnUR8anyo8vLMIH8Rthgofw7Nj8qTguCa2QjFAbzxAUTQTPNNjNL7yt0BGGinVdFw==
+ dependencies:
+ embla-carousel "8.3.0"
+ embla-carousel-reactive-utils "8.3.0"
+
+embla-carousel-reactive-utils@8.3.0:
+ version "8.3.0"
+ resolved "https://registry.yarnpkg.com/embla-carousel-reactive-utils/-/embla-carousel-reactive-utils-8.3.0.tgz#75f177ed2f6bdafbaab8f869f936692d08cd488e"
+ integrity sha512-EYdhhJ302SC4Lmkx8GRsp0sjUhEN4WyFXPOk0kGu9OXZSRMmcBlRgTvHcq8eKJE1bXWBsOi1T83B+BSSVZSmwQ==
+
+embla-carousel@8.3.0:
+ version "8.3.0"
+ resolved "https://registry.yarnpkg.com/embla-carousel/-/embla-carousel-8.3.0.tgz#dc27c63c405aa98320cb36893e4be2fbdc787ee1"
+ integrity sha512-Ve8dhI4w28qBqR8J+aMtv7rLK89r1ZA5HocwFz6uMB/i5EiC7bGI7y+AM80yAVUJw3qqaZYK7clmZMUR8kM3UA==
+
emoji-regex@^8.0.0:
version "8.0.0"
resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37"