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} /> {"Sourcebot 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 */} - -
+ +
{"Sourcebot {"Sourcebot
@@ -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 ( +
+ + {`Search ${repos.length} `} + + {repos.length > 1 ? 'repositories' : 'repository'} + + + +
+ ) +} + +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: {info.costHostName}, + 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"