mirror of
https://github.com/sourcebot-dev/sourcebot.git
synced 2025-12-12 04:15:30 +00:00
Add repo carousel and "how to search" section to homepage (#5)
This commit is contained in:
parent
71717ac4b5
commit
1c756d2010
8 changed files with 559 additions and 5 deletions
|
|
@ -34,6 +34,8 @@
|
||||||
"class-variance-authority": "^0.7.0",
|
"class-variance-authority": "^0.7.0",
|
||||||
"client-only": "^0.0.1",
|
"client-only": "^0.0.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
|
"embla-carousel-auto-scroll": "^8.3.0",
|
||||||
|
"embla-carousel-react": "^8.3.0",
|
||||||
"escape-string-regexp": "^5.0.0",
|
"escape-string-regexp": "^5.0.0",
|
||||||
"http-status-codes": "^2.3.0",
|
"http-status-codes": "^2.3.0",
|
||||||
"lucide-react": "^0.435.0",
|
"lucide-react": "^0.435.0",
|
||||||
|
|
|
||||||
|
|
@ -30,11 +30,13 @@ export const NavigationMenu = () => {
|
||||||
src={logoDark}
|
src={logoDark}
|
||||||
className="h-11 w-auto hidden dark:block"
|
className="h-11 w-auto hidden dark:block"
|
||||||
alt={"Sourcebot logo"}
|
alt={"Sourcebot logo"}
|
||||||
|
priority={true}
|
||||||
/>
|
/>
|
||||||
<Image
|
<Image
|
||||||
src={logoLight}
|
src={logoLight}
|
||||||
className="h-11 w-auto block dark:hidden"
|
className="h-11 w-auto block dark:hidden"
|
||||||
alt={"Sourcebot logo"}
|
alt={"Sourcebot logo"}
|
||||||
|
priority={true}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
155
src/app/page.tsx
155
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 Image from "next/image";
|
||||||
|
import { Suspense } from "react";
|
||||||
import logoDark from "../../public/sb_logo_dark_large.png";
|
import logoDark from "../../public/sb_logo_dark_large.png";
|
||||||
import logoLight from "../../public/sb_logo_light_large.png";
|
import logoLight from "../../public/sb_logo_light_large.png";
|
||||||
import { NavigationMenu } from "./navigationMenu";
|
import { NavigationMenu } from "./navigationMenu";
|
||||||
|
import { RepositoryCarousel } from "./repositoryCarousel";
|
||||||
import { SearchBar } from "./searchBar";
|
import { SearchBar } from "./searchBar";
|
||||||
|
import { Separator } from "@/components/ui/separator";
|
||||||
|
|
||||||
export default function Home() {
|
|
||||||
|
export default async function Home() {
|
||||||
return (
|
return (
|
||||||
<div className="h-screen flex flex-col items-center">
|
<div className="h-screen flex flex-col items-center">
|
||||||
{/* TopBar */}
|
{/* TopBar */}
|
||||||
<NavigationMenu />
|
<NavigationMenu />
|
||||||
|
|
||||||
<div className="flex flex-col justify-center items-center p-4 mt-48">
|
<div className="flex flex-col justify-center items-center mt-8 md:mt-32 max-w-[90%]">
|
||||||
<div className="max-h-44 w-auto">
|
<div className="max-h-44 w-auto">
|
||||||
<Image
|
<Image
|
||||||
src={logoDark}
|
src={logoDark}
|
||||||
className="w-full h-full hidden dark:block"
|
className="w-full h-full hidden dark:block"
|
||||||
alt={"Sourcebot logo"}
|
alt={"Sourcebot logo"}
|
||||||
|
priority={true}
|
||||||
/>
|
/>
|
||||||
<Image
|
<Image
|
||||||
src={logoLight}
|
src={logoLight}
|
||||||
className="w-full h-full block dark:hidden"
|
className="w-full h-full block dark:hidden"
|
||||||
alt={"Sourcebot logo"}
|
alt={"Sourcebot logo"}
|
||||||
|
priority={true}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="w-full flex flex-row mt-4">
|
<div className="w-full flex flex-row mt-4">
|
||||||
|
|
@ -28,7 +36,148 @@ export default function Home() {
|
||||||
autoFocus={true}
|
autoFocus={true}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="mt-8">
|
||||||
|
<Suspense fallback={<div>...</div>}>
|
||||||
|
<RepositoryList />
|
||||||
|
</Suspense>
|
||||||
|
</div>
|
||||||
|
<Separator className="mt-5 mb-8" />
|
||||||
|
<div className="flex flex-col items-center w-fit gap-6">
|
||||||
|
<span className="font-semibold">How to search</span>
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-5">
|
||||||
|
<HowToSection
|
||||||
|
title="Search in files or paths"
|
||||||
|
>
|
||||||
|
<QueryExample>
|
||||||
|
<Query query="test todo">test todo</Query> <QueryExplanation>(both test and todo)</QueryExplanation>
|
||||||
|
</QueryExample>
|
||||||
|
<QueryExample>
|
||||||
|
<Query query="test or todo">test <Highlight>or</Highlight> todo</Query> <QueryExplanation>(either test or todo)</QueryExplanation>
|
||||||
|
</QueryExample>
|
||||||
|
<QueryExample>
|
||||||
|
<Query query={`"exit boot"`}>{`"exit boot"`}</Query> <QueryExplanation>(exact match)</QueryExplanation>
|
||||||
|
</QueryExample>
|
||||||
|
<QueryExample>
|
||||||
|
<Query query="TODO case:yes">TODO <Highlight>case:</Highlight>yes</Query> <QueryExplanation>(case sensitive)</QueryExplanation>
|
||||||
|
</QueryExample>
|
||||||
|
</HowToSection>
|
||||||
|
<HowToSection
|
||||||
|
title="Filter results"
|
||||||
|
>
|
||||||
|
<QueryExample>
|
||||||
|
<Query query="file:README setup"><Highlight>file:</Highlight>README setup</Query> <QueryExplanation>(by filename)</QueryExplanation>
|
||||||
|
</QueryExample>
|
||||||
|
<QueryExample>
|
||||||
|
<Query query="repo:torvalds/linux test"><Highlight>repo:</Highlight>torvalds/linux test</Query> <QueryExplanation>(by repo)</QueryExplanation>
|
||||||
|
</QueryExample>
|
||||||
|
<QueryExample>
|
||||||
|
<Query query="lang:typescript"><Highlight>lang:</Highlight>typescript</Query> <QueryExplanation>(by language)</QueryExplanation>
|
||||||
|
</QueryExample>
|
||||||
|
<QueryExample>
|
||||||
|
<Query query="branch:HEAD"><Highlight>branch:</Highlight>HEAD</Query> <QueryExplanation>(by branch)</QueryExplanation>
|
||||||
|
</QueryExample>
|
||||||
|
</HowToSection>
|
||||||
|
<HowToSection
|
||||||
|
title="Advanced"
|
||||||
|
>
|
||||||
|
<QueryExample>
|
||||||
|
<Query query="file:\.py$"><Highlight>file:</Highlight>{`\\.py$`}</Query> <QueryExplanation>{`(files that end in ".py")`}</QueryExplanation>
|
||||||
|
</QueryExample>
|
||||||
|
<QueryExample>
|
||||||
|
<Query query="sym:main"><Highlight>sym:</Highlight>main</Query> <QueryExplanation>{`(symbols named "main")`}</QueryExplanation>
|
||||||
|
</QueryExample>
|
||||||
|
<QueryExample>
|
||||||
|
<Query query="todo -lang:c">todo <Highlight>-lang:c</Highlight></Query> <QueryExplanation>(negate filter)</QueryExplanation>
|
||||||
|
</QueryExample>
|
||||||
|
<QueryExample>
|
||||||
|
<Query query="content:README"><Highlight>content:</Highlight>README</Query> <QueryExplanation>(search content only)</QueryExplanation>
|
||||||
|
</QueryExample>
|
||||||
|
</HowToSection>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 <span>
|
||||||
|
Get started
|
||||||
|
<a
|
||||||
|
href="https://github.com/TaqlaAI/sourcebot/blob/main/README.md"
|
||||||
|
className="text-blue-500"
|
||||||
|
>
|
||||||
|
{` configuring Sourcebot.`}
|
||||||
|
</a>
|
||||||
|
</span>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col items-center gap-3">
|
||||||
|
<span className="text-sm">
|
||||||
|
{`Search ${repos.length} `}
|
||||||
|
<a
|
||||||
|
href="/repos"
|
||||||
|
className="text-blue-500"
|
||||||
|
>
|
||||||
|
{repos.length > 1 ? 'repositories' : 'repository'}
|
||||||
|
</a>
|
||||||
|
</span>
|
||||||
|
<RepositoryCarousel repos={repos} />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const HowToSection = ({ title, children }: { title: string, children: React.ReactNode }) => {
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col gap-1">
|
||||||
|
<span className="dark:text-gray-300 text-sm mb-2 underline">{title}</span>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
const Highlight = ({ children }: { children: React.ReactNode }) => {
|
||||||
|
return (
|
||||||
|
<span className="text-blue-700 dark:text-blue-500">
|
||||||
|
{children}
|
||||||
|
</span>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const QueryExample = ({ children }: { children: React.ReactNode }) => {
|
||||||
|
return (
|
||||||
|
<span className="text-sm font-mono">
|
||||||
|
{children}
|
||||||
|
</span>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const QueryExplanation = ({ children }: { children: React.ReactNode }) => {
|
||||||
|
return (
|
||||||
|
<span className="text-gray-500 dark:text-gray-400 ml-3">
|
||||||
|
{children}
|
||||||
|
</span>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const Query = ({ query, children }: { query: string, children: React.ReactNode }) => {
|
||||||
|
return (
|
||||||
|
<a
|
||||||
|
href={`/search?query=${query}`}
|
||||||
|
className="cursor-pointer hover:underline"
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</a>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,10 @@
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { getRepoCodeHostInfo } from "@/lib/utils";
|
||||||
import { Column, ColumnDef } from "@tanstack/react-table"
|
import { Column, ColumnDef } from "@tanstack/react-table"
|
||||||
import { ArrowUpDown } from "lucide-react"
|
import { ArrowUpDown } from "lucide-react"
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
export type RepositoryColumnInfo = {
|
export type RepositoryColumnInfo = {
|
||||||
name: string;
|
name: string;
|
||||||
branches: {
|
branches: {
|
||||||
|
|
@ -25,6 +24,24 @@ export const columns: ColumnDef<RepositoryColumnInfo>[] = [
|
||||||
{
|
{
|
||||||
accessorKey: "name",
|
accessorKey: "name",
|
||||||
header: "Name",
|
header: "Name",
|
||||||
|
cell: ({ row }) => {
|
||||||
|
const repo = row.original;
|
||||||
|
const info = getRepoCodeHostInfo(repo.name);
|
||||||
|
return (
|
||||||
|
<div className="flex flex-row items-center gap-2">
|
||||||
|
<span
|
||||||
|
className={info?.repoLink ? "cursor-pointer text-blue-500 hover:underline": ""}
|
||||||
|
onClick={() => {
|
||||||
|
if (info?.repoLink) {
|
||||||
|
window.open(info.repoLink, "_blank");
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{repo.name}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessorKey: "branches",
|
accessorKey: "branches",
|
||||||
|
|
|
||||||
98
src/app/repositoryCarousel.tsx
Normal file
98
src/app/repositoryCarousel.tsx
Normal file
|
|
@ -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 (
|
||||||
|
<Carousel
|
||||||
|
opts={{
|
||||||
|
align: "start",
|
||||||
|
loop: true,
|
||||||
|
}}
|
||||||
|
className="w-full max-w-lg"
|
||||||
|
plugins={[
|
||||||
|
Autoscroll({
|
||||||
|
startDelay: 0,
|
||||||
|
speed: 1,
|
||||||
|
stopOnMouseEnter: true,
|
||||||
|
stopOnInteraction: false,
|
||||||
|
}),
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<CarouselContent>
|
||||||
|
{repos.map((repo, index) => (
|
||||||
|
<CarouselItem key={index} className="basis-auto">
|
||||||
|
<RepositoryBadge
|
||||||
|
key={index}
|
||||||
|
repo={repo}
|
||||||
|
/>
|
||||||
|
</CarouselItem>
|
||||||
|
))}
|
||||||
|
</CarouselContent>
|
||||||
|
</Carousel>
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
|
interface RepositoryBadgeProps {
|
||||||
|
repo: Repository;
|
||||||
|
}
|
||||||
|
|
||||||
|
const RepositoryBadge = ({
|
||||||
|
repo
|
||||||
|
}: RepositoryBadgeProps) => {
|
||||||
|
const { repoIcon, repoName, repoLink } = (() => {
|
||||||
|
const info = getRepoCodeHostInfo(repo.Name);
|
||||||
|
|
||||||
|
if (info) {
|
||||||
|
return {
|
||||||
|
repoIcon: <Image
|
||||||
|
src={info.icon}
|
||||||
|
alt={info.costHostName}
|
||||||
|
className="w-4 h-4 dark:invert"
|
||||||
|
/>,
|
||||||
|
repoName: info.repoName,
|
||||||
|
repoLink: info.repoLink,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
repoIcon: <FileIcon className="w-4 h-4" />,
|
||||||
|
repoName: repo.Name,
|
||||||
|
repoLink: undefined,
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
onClick={() => {
|
||||||
|
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}
|
||||||
|
<span className="text-sm font-mono">
|
||||||
|
{repoName}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
262
src/components/ui/carousel.tsx
Normal file
262
src/components/ui/carousel.tsx
Normal file
|
|
@ -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<typeof useEmblaCarousel>
|
||||||
|
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<typeof useEmblaCarousel>[0]
|
||||||
|
api: ReturnType<typeof useEmblaCarousel>[1]
|
||||||
|
scrollPrev: () => void
|
||||||
|
scrollNext: () => void
|
||||||
|
canScrollPrev: boolean
|
||||||
|
canScrollNext: boolean
|
||||||
|
} & CarouselProps
|
||||||
|
|
||||||
|
const CarouselContext = React.createContext<CarouselContextProps | null>(null)
|
||||||
|
|
||||||
|
function useCarousel() {
|
||||||
|
const context = React.useContext(CarouselContext)
|
||||||
|
|
||||||
|
if (!context) {
|
||||||
|
throw new Error("useCarousel must be used within a <Carousel />")
|
||||||
|
}
|
||||||
|
|
||||||
|
return context
|
||||||
|
}
|
||||||
|
|
||||||
|
const Carousel = React.forwardRef<
|
||||||
|
HTMLDivElement,
|
||||||
|
React.HTMLAttributes<HTMLDivElement> & 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<HTMLDivElement>) => {
|
||||||
|
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 (
|
||||||
|
<CarouselContext.Provider
|
||||||
|
value={{
|
||||||
|
carouselRef,
|
||||||
|
api: api,
|
||||||
|
opts,
|
||||||
|
orientation:
|
||||||
|
orientation || (opts?.axis === "y" ? "vertical" : "horizontal"),
|
||||||
|
scrollPrev,
|
||||||
|
scrollNext,
|
||||||
|
canScrollPrev,
|
||||||
|
canScrollNext,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
ref={ref}
|
||||||
|
onKeyDownCapture={handleKeyDown}
|
||||||
|
className={cn("relative", className)}
|
||||||
|
role="region"
|
||||||
|
aria-roledescription="carousel"
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
</CarouselContext.Provider>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
Carousel.displayName = "Carousel"
|
||||||
|
|
||||||
|
const CarouselContent = React.forwardRef<
|
||||||
|
HTMLDivElement,
|
||||||
|
React.HTMLAttributes<HTMLDivElement>
|
||||||
|
>(({ className, ...props }, ref) => {
|
||||||
|
const { carouselRef, orientation } = useCarousel()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div ref={carouselRef} className="overflow-hidden">
|
||||||
|
<div
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"flex",
|
||||||
|
orientation === "horizontal" ? "-ml-4" : "-mt-4 flex-col",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
CarouselContent.displayName = "CarouselContent"
|
||||||
|
|
||||||
|
const CarouselItem = React.forwardRef<
|
||||||
|
HTMLDivElement,
|
||||||
|
React.HTMLAttributes<HTMLDivElement>
|
||||||
|
>(({ className, ...props }, ref) => {
|
||||||
|
const { orientation } = useCarousel()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
ref={ref}
|
||||||
|
role="group"
|
||||||
|
aria-roledescription="slide"
|
||||||
|
className={cn(
|
||||||
|
"min-w-0 shrink-0 grow-0 basis-full",
|
||||||
|
orientation === "horizontal" ? "pl-4" : "pt-4",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
CarouselItem.displayName = "CarouselItem"
|
||||||
|
|
||||||
|
const CarouselPrevious = React.forwardRef<
|
||||||
|
HTMLButtonElement,
|
||||||
|
React.ComponentProps<typeof Button>
|
||||||
|
>(({ className, variant = "outline", size = "icon", ...props }, ref) => {
|
||||||
|
const { orientation, scrollPrev, canScrollPrev } = useCarousel()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
ref={ref}
|
||||||
|
variant={variant}
|
||||||
|
size={size}
|
||||||
|
className={cn(
|
||||||
|
"absolute h-8 w-8 rounded-full",
|
||||||
|
orientation === "horizontal"
|
||||||
|
? "-left-12 top-1/2 -translate-y-1/2"
|
||||||
|
: "-top-12 left-1/2 -translate-x-1/2 rotate-90",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
disabled={!canScrollPrev}
|
||||||
|
onClick={scrollPrev}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<ArrowLeft className="h-4 w-4" />
|
||||||
|
<span className="sr-only">Previous slide</span>
|
||||||
|
</Button>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
CarouselPrevious.displayName = "CarouselPrevious"
|
||||||
|
|
||||||
|
const CarouselNext = React.forwardRef<
|
||||||
|
HTMLButtonElement,
|
||||||
|
React.ComponentProps<typeof Button>
|
||||||
|
>(({ className, variant = "outline", size = "icon", ...props }, ref) => {
|
||||||
|
const { orientation, scrollNext, canScrollNext } = useCarousel()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
ref={ref}
|
||||||
|
variant={variant}
|
||||||
|
size={size}
|
||||||
|
className={cn(
|
||||||
|
"absolute h-8 w-8 rounded-full",
|
||||||
|
orientation === "horizontal"
|
||||||
|
? "-right-12 top-1/2 -translate-y-1/2"
|
||||||
|
: "-bottom-12 left-1/2 -translate-x-1/2 rotate-90",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
disabled={!canScrollNext}
|
||||||
|
onClick={scrollNext}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<ArrowRight className="h-4 w-4" />
|
||||||
|
<span className="sr-only">Next slide</span>
|
||||||
|
</Button>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
CarouselNext.displayName = "CarouselNext"
|
||||||
|
|
||||||
|
export {
|
||||||
|
type CarouselApi,
|
||||||
|
Carousel,
|
||||||
|
CarouselContent,
|
||||||
|
CarouselItem,
|
||||||
|
CarouselPrevious,
|
||||||
|
CarouselNext,
|
||||||
|
}
|
||||||
|
|
@ -93,6 +93,7 @@ export const fileSourceResponseSchema = z.object({
|
||||||
|
|
||||||
|
|
||||||
export type ListRepositoriesResponse = z.infer<typeof listRepositoriesResponseSchema>;
|
export type ListRepositoriesResponse = z.infer<typeof listRepositoriesResponseSchema>;
|
||||||
|
export type Repository = z.infer<typeof repositorySchema>;
|
||||||
|
|
||||||
// @see : https://github.com/TaqlaAI/zoekt/blob/3780e68cdb537d5a7ed2c84d9b3784f80c7c5d04/api.go#L728
|
// @see : https://github.com/TaqlaAI/zoekt/blob/3780e68cdb537d5a7ed2c84d9b3784f80c7c5d04/api.go#L728
|
||||||
const repoStatsSchema = z.object({
|
const repoStatsSchema = z.object({
|
||||||
|
|
|
||||||
23
yarn.lock
23
yarn.lock
|
|
@ -1631,6 +1631,29 @@ eastasianwidth@^0.2.0:
|
||||||
resolved "https://registry.yarnpkg.com/eastasianwidth/-/eastasianwidth-0.2.0.tgz#696ce2ec0aa0e6ea93a397ffcf24aa7840c827cb"
|
resolved "https://registry.yarnpkg.com/eastasianwidth/-/eastasianwidth-0.2.0.tgz#696ce2ec0aa0e6ea93a397ffcf24aa7840c827cb"
|
||||||
integrity sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==
|
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:
|
emoji-regex@^8.0.0:
|
||||||
version "8.0.0"
|
version "8.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37"
|
resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37"
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue