mirror of
https://github.com/Kludex/awesome-fastapi-projects.git
synced 2025-05-15 21:57:04 +00:00
Implement basic searching
This commit is contained in:
parent
1441cbb396
commit
9d286f5c01
@ -9,6 +9,9 @@
|
|||||||
"lint": "next lint"
|
"lint": "next lint"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@hookform/resolvers": "^3.3.1",
|
||||||
|
"@orama/orama": "^1.2.3",
|
||||||
|
"@radix-ui/react-label": "^2.0.2",
|
||||||
"@radix-ui/react-slot": "^1.0.2",
|
"@radix-ui/react-slot": "^1.0.2",
|
||||||
"@tanstack/react-table": "^8.9.3",
|
"@tanstack/react-table": "^8.9.3",
|
||||||
"@types/node": "20.5.1",
|
"@types/node": "20.5.1",
|
||||||
@ -24,10 +27,11 @@
|
|||||||
"postcss": "8.4.28",
|
"postcss": "8.4.28",
|
||||||
"react": "18.2.0",
|
"react": "18.2.0",
|
||||||
"react-dom": "18.2.0",
|
"react-dom": "18.2.0",
|
||||||
|
"react-hook-form": "^7.46.1",
|
||||||
"tailwind-merge": "^1.14.0",
|
"tailwind-merge": "^1.14.0",
|
||||||
"tailwindcss": "3.3.3",
|
"tailwindcss": "3.3.3",
|
||||||
"tailwindcss-animate": "^1.0.6",
|
"tailwindcss-animate": "^1.0.6",
|
||||||
"typescript": "5.1.6",
|
"typescript": "5.1.6",
|
||||||
"zod": "^3.22.2"
|
"zod": "^3.21.4"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
102
frontend/pnpm-lock.yaml
generated
102
frontend/pnpm-lock.yaml
generated
@ -5,6 +5,15 @@ settings:
|
|||||||
excludeLinksFromLockfile: false
|
excludeLinksFromLockfile: false
|
||||||
|
|
||||||
dependencies:
|
dependencies:
|
||||||
|
"@hookform/resolvers":
|
||||||
|
specifier: ^3.3.1
|
||||||
|
version: 3.3.1(react-hook-form@7.46.1)
|
||||||
|
"@orama/orama":
|
||||||
|
specifier: ^1.2.3
|
||||||
|
version: 1.2.3
|
||||||
|
"@radix-ui/react-label":
|
||||||
|
specifier: ^2.0.2
|
||||||
|
version: 2.0.2(@types/react-dom@18.2.7)(@types/react@18.2.20)(react-dom@18.2.0)(react@18.2.0)
|
||||||
"@radix-ui/react-slot":
|
"@radix-ui/react-slot":
|
||||||
specifier: ^1.0.2
|
specifier: ^1.0.2
|
||||||
version: 1.0.2(@types/react@18.2.20)(react@18.2.0)
|
version: 1.0.2(@types/react@18.2.20)(react@18.2.0)
|
||||||
@ -50,6 +59,9 @@ dependencies:
|
|||||||
react-dom:
|
react-dom:
|
||||||
specifier: 18.2.0
|
specifier: 18.2.0
|
||||||
version: 18.2.0(react@18.2.0)
|
version: 18.2.0(react@18.2.0)
|
||||||
|
react-hook-form:
|
||||||
|
specifier: ^7.46.1
|
||||||
|
version: 7.46.1(react@18.2.0)
|
||||||
tailwind-merge:
|
tailwind-merge:
|
||||||
specifier: ^1.14.0
|
specifier: ^1.14.0
|
||||||
version: 1.14.0
|
version: 1.14.0
|
||||||
@ -63,8 +75,8 @@ dependencies:
|
|||||||
specifier: 5.1.6
|
specifier: 5.1.6
|
||||||
version: 5.1.6
|
version: 5.1.6
|
||||||
zod:
|
zod:
|
||||||
specifier: ^3.22.2
|
specifier: ^3.21.4
|
||||||
version: 3.22.2
|
version: 3.21.4
|
||||||
|
|
||||||
packages:
|
packages:
|
||||||
/@aashutoshrathi/word-wrap@1.2.6:
|
/@aashutoshrathi/word-wrap@1.2.6:
|
||||||
@ -142,6 +154,17 @@ packages:
|
|||||||
engines: { node: ^12.22.0 || ^14.17.0 || >=16.0.0 }
|
engines: { node: ^12.22.0 || ^14.17.0 || >=16.0.0 }
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/@hookform/resolvers@3.3.1(react-hook-form@7.46.1):
|
||||||
|
resolution:
|
||||||
|
{
|
||||||
|
integrity: sha512-K7KCKRKjymxIB90nHDQ7b9nli474ru99ZbqxiqDAWYsYhOsU3/4qLxW91y+1n04ic13ajjZ66L3aXbNef8PELQ==,
|
||||||
|
}
|
||||||
|
peerDependencies:
|
||||||
|
react-hook-form: ^7.0.0
|
||||||
|
dependencies:
|
||||||
|
react-hook-form: 7.46.1(react@18.2.0)
|
||||||
|
dev: false
|
||||||
|
|
||||||
/@humanwhocodes/config-array@0.11.10:
|
/@humanwhocodes/config-array@0.11.10:
|
||||||
resolution:
|
resolution:
|
||||||
{
|
{
|
||||||
@ -370,6 +393,14 @@ packages:
|
|||||||
fastq: 1.15.0
|
fastq: 1.15.0
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/@orama/orama@1.2.3:
|
||||||
|
resolution:
|
||||||
|
{
|
||||||
|
integrity: sha512-KJ4lzTDluQOJu6l2xsmDjKdhU6EvldmshvsQgAvDORn/Db+EXaWOKSK4XdvUNIcpUeSbFAdkRB26NLlfSpWRGg==,
|
||||||
|
}
|
||||||
|
engines: { node: ">= 16.0.0" }
|
||||||
|
dev: false
|
||||||
|
|
||||||
/@radix-ui/react-compose-refs@1.0.1(@types/react@18.2.20)(react@18.2.0):
|
/@radix-ui/react-compose-refs@1.0.1(@types/react@18.2.20)(react@18.2.0):
|
||||||
resolution:
|
resolution:
|
||||||
{
|
{
|
||||||
@ -387,6 +418,54 @@ packages:
|
|||||||
react: 18.2.0
|
react: 18.2.0
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/@radix-ui/react-label@2.0.2(@types/react-dom@18.2.7)(@types/react@18.2.20)(react-dom@18.2.0)(react@18.2.0):
|
||||||
|
resolution:
|
||||||
|
{
|
||||||
|
integrity: sha512-N5ehvlM7qoTLx7nWPodsPYPgMzA5WM8zZChQg8nyFJKnDO5WHdba1vv5/H6IO5LtJMfD2Q3wh1qHFGNtK0w3bQ==,
|
||||||
|
}
|
||||||
|
peerDependencies:
|
||||||
|
"@types/react": "*"
|
||||||
|
"@types/react-dom": "*"
|
||||||
|
react: ^16.8 || ^17.0 || ^18.0
|
||||||
|
react-dom: ^16.8 || ^17.0 || ^18.0
|
||||||
|
peerDependenciesMeta:
|
||||||
|
"@types/react":
|
||||||
|
optional: true
|
||||||
|
"@types/react-dom":
|
||||||
|
optional: true
|
||||||
|
dependencies:
|
||||||
|
"@babel/runtime": 7.22.10
|
||||||
|
"@radix-ui/react-primitive": 1.0.3(@types/react-dom@18.2.7)(@types/react@18.2.20)(react-dom@18.2.0)(react@18.2.0)
|
||||||
|
"@types/react": 18.2.20
|
||||||
|
"@types/react-dom": 18.2.7
|
||||||
|
react: 18.2.0
|
||||||
|
react-dom: 18.2.0(react@18.2.0)
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/@radix-ui/react-primitive@1.0.3(@types/react-dom@18.2.7)(@types/react@18.2.20)(react-dom@18.2.0)(react@18.2.0):
|
||||||
|
resolution:
|
||||||
|
{
|
||||||
|
integrity: sha512-yi58uVyoAcK/Nq1inRY56ZSjKypBNKTa/1mcL8qdl6oJeEaDbOldlzrGn7P6Q3Id5d+SYNGc5AJgc4vGhjs5+g==,
|
||||||
|
}
|
||||||
|
peerDependencies:
|
||||||
|
"@types/react": "*"
|
||||||
|
"@types/react-dom": "*"
|
||||||
|
react: ^16.8 || ^17.0 || ^18.0
|
||||||
|
react-dom: ^16.8 || ^17.0 || ^18.0
|
||||||
|
peerDependenciesMeta:
|
||||||
|
"@types/react":
|
||||||
|
optional: true
|
||||||
|
"@types/react-dom":
|
||||||
|
optional: true
|
||||||
|
dependencies:
|
||||||
|
"@babel/runtime": 7.22.10
|
||||||
|
"@radix-ui/react-slot": 1.0.2(@types/react@18.2.20)(react@18.2.0)
|
||||||
|
"@types/react": 18.2.20
|
||||||
|
"@types/react-dom": 18.2.7
|
||||||
|
react: 18.2.0
|
||||||
|
react-dom: 18.2.0(react@18.2.0)
|
||||||
|
dev: false
|
||||||
|
|
||||||
/@radix-ui/react-slot@1.0.2(@types/react@18.2.20)(react@18.2.0):
|
/@radix-ui/react-slot@1.0.2(@types/react@18.2.20)(react@18.2.0):
|
||||||
resolution:
|
resolution:
|
||||||
{
|
{
|
||||||
@ -2964,6 +3043,18 @@ packages:
|
|||||||
scheduler: 0.23.0
|
scheduler: 0.23.0
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/react-hook-form@7.46.1(react@18.2.0):
|
||||||
|
resolution:
|
||||||
|
{
|
||||||
|
integrity: sha512-0GfI31LRTBd5tqbXMGXT1Rdsv3rnvy0FjEk8Gn9/4tp6+s77T7DPZuGEpBRXOauL+NhyGT5iaXzdIM2R6F/E+w==,
|
||||||
|
}
|
||||||
|
engines: { node: ">=12.22.0" }
|
||||||
|
peerDependencies:
|
||||||
|
react: ^16.8.0 || ^17 || ^18
|
||||||
|
dependencies:
|
||||||
|
react: 18.2.0
|
||||||
|
dev: false
|
||||||
|
|
||||||
/react-is@16.13.1:
|
/react-is@16.13.1:
|
||||||
resolution:
|
resolution:
|
||||||
{
|
{
|
||||||
@ -3707,10 +3798,3 @@ packages:
|
|||||||
integrity: sha512-m46AKbrzKVzOzs/DZgVnG5H55N1sv1M8qZU3A8RIKbs3mrACDNeIOeilDymVb2HdmP8uwshOCF4uJ8uM9rCqJw==,
|
integrity: sha512-m46AKbrzKVzOzs/DZgVnG5H55N1sv1M8qZU3A8RIKbs3mrACDNeIOeilDymVb2HdmP8uwshOCF4uJ8uM9rCqJw==,
|
||||||
}
|
}
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
/zod@3.22.2:
|
|
||||||
resolution:
|
|
||||||
{
|
|
||||||
integrity: sha512-wvWkphh5WQsJbVk1tbx1l1Ly4yg+XecD+Mq280uBGt9wa5BKSWf4Mhp6GmrkPixhMxmabYY7RbzlwVP32pbGCg==,
|
|
||||||
}
|
|
||||||
dev: false
|
|
||||||
|
@ -31,8 +31,8 @@ export const columns: ColumnDef<Repo>[] = [
|
|||||||
<path
|
<path
|
||||||
d="M7.49933 0.25C3.49635 0.25 0.25 3.49593 0.25 7.50024C0.25 10.703 2.32715 13.4206 5.2081 14.3797C5.57084 14.446 5.70302 14.2222 5.70302 14.0299C5.70302 13.8576 5.69679 13.4019 5.69323 12.797C3.67661 13.235 3.25112 11.825 3.25112 11.825C2.92132 10.9874 2.44599 10.7644 2.44599 10.7644C1.78773 10.3149 2.49584 10.3238 2.49584 10.3238C3.22353 10.375 3.60629 11.0711 3.60629 11.0711C4.25298 12.1788 5.30335 11.8588 5.71638 11.6732C5.78225 11.205 5.96962 10.8854 6.17658 10.7043C4.56675 10.5209 2.87415 9.89918 2.87415 7.12104C2.87415 6.32925 3.15677 5.68257 3.62053 5.17563C3.54576 4.99226 3.29697 4.25521 3.69174 3.25691C3.69174 3.25691 4.30015 3.06196 5.68522 3.99973C6.26337 3.83906 6.8838 3.75895 7.50022 3.75583C8.1162 3.75895 8.73619 3.83906 9.31523 3.99973C10.6994 3.06196 11.3069 3.25691 11.3069 3.25691C11.7026 4.25521 11.4538 4.99226 11.3795 5.17563C11.8441 5.68257 12.1245 6.32925 12.1245 7.12104C12.1245 9.9063 10.4292 10.5192 8.81452 10.6985C9.07444 10.9224 9.30633 11.3648 9.30633 12.0413C9.30633 13.0102 9.29742 13.7922 9.29742 14.0299C9.29742 14.2239 9.42828 14.4496 9.79591 14.3788C12.6746 13.4179 14.75 10.7025 14.75 7.50024C14.75 3.49593 11.5036 0.25 7.49933 0.25Z"
|
d="M7.49933 0.25C3.49635 0.25 0.25 3.49593 0.25 7.50024C0.25 10.703 2.32715 13.4206 5.2081 14.3797C5.57084 14.446 5.70302 14.2222 5.70302 14.0299C5.70302 13.8576 5.69679 13.4019 5.69323 12.797C3.67661 13.235 3.25112 11.825 3.25112 11.825C2.92132 10.9874 2.44599 10.7644 2.44599 10.7644C1.78773 10.3149 2.49584 10.3238 2.49584 10.3238C3.22353 10.375 3.60629 11.0711 3.60629 11.0711C4.25298 12.1788 5.30335 11.8588 5.71638 11.6732C5.78225 11.205 5.96962 10.8854 6.17658 10.7043C4.56675 10.5209 2.87415 9.89918 2.87415 7.12104C2.87415 6.32925 3.15677 5.68257 3.62053 5.17563C3.54576 4.99226 3.29697 4.25521 3.69174 3.25691C3.69174 3.25691 4.30015 3.06196 5.68522 3.99973C6.26337 3.83906 6.8838 3.75895 7.50022 3.75583C8.1162 3.75895 8.73619 3.83906 9.31523 3.99973C10.6994 3.06196 11.3069 3.25691 11.3069 3.25691C11.7026 4.25521 11.4538 4.99226 11.3795 5.17563C11.8441 5.68257 12.1245 6.32925 12.1245 7.12104C12.1245 9.9063 10.4292 10.5192 8.81452 10.6985C9.07444 10.9224 9.30633 11.3648 9.30633 12.0413C9.30633 13.0102 9.29742 13.7922 9.29742 14.0299C9.29742 14.2239 9.42828 14.4496 9.79591 14.3788C12.6746 13.4179 14.75 10.7025 14.75 7.50024C14.75 3.49593 11.5036 0.25 7.49933 0.25Z"
|
||||||
fill="currentColor"
|
fill="currentColor"
|
||||||
fill-rule="evenodd"
|
fillRule="evenodd"
|
||||||
clip-rule="evenodd"
|
clipRule="evenodd"
|
||||||
></path>
|
></path>
|
||||||
</svg>
|
</svg>
|
||||||
<a
|
<a
|
||||||
|
@ -1,12 +1,15 @@
|
|||||||
import { loadIndexServerOnly } from "@/lib/load-index-server-only";
|
import { loadIndexServerOnly } from "@/lib/repos-index";
|
||||||
import { DataTable } from "./data-table";
|
import { ReposTable } from "./repos-table";
|
||||||
import { columns } from "./columns";
|
import { ReposSearchProvider } from "./repos-search-provider";
|
||||||
|
|
||||||
export default async function Home() {
|
export default async function Home() {
|
||||||
const { repos } = await loadIndexServerOnly();
|
const { repos } = await loadIndexServerOnly();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="py-10">
|
<section className="py-10">
|
||||||
<DataTable columns={columns} data={repos} />
|
<ReposSearchProvider repos={repos}>
|
||||||
|
<ReposTable repos={repos} />
|
||||||
|
</ReposSearchProvider>
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
31
frontend/src/app/repos-search-provider.tsx
Normal file
31
frontend/src/app/repos-search-provider.tsx
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
"use client";
|
||||||
|
import {
|
||||||
|
createReposOrama,
|
||||||
|
prepareReposOramaIndex,
|
||||||
|
ReposOramaContext,
|
||||||
|
} from "@/lib/search";
|
||||||
|
import { PropsWithChildren } from "react";
|
||||||
|
import { SearchProvider } from "./search-provider";
|
||||||
|
import { Index } from "@/lib/schemas";
|
||||||
|
|
||||||
|
export function ReposSearchProvider({
|
||||||
|
children,
|
||||||
|
repos,
|
||||||
|
}: PropsWithChildren<{
|
||||||
|
repos: Index["repos"];
|
||||||
|
}>) {
|
||||||
|
const prepareOramaIndex = async () => {
|
||||||
|
const orama = await createReposOrama();
|
||||||
|
await prepareReposOramaIndex(orama, repos);
|
||||||
|
return orama;
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SearchProvider
|
||||||
|
createIndex={prepareOramaIndex}
|
||||||
|
OramaContext={ReposOramaContext}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</SearchProvider>
|
||||||
|
);
|
||||||
|
}
|
38
frontend/src/app/repos-table.tsx
Normal file
38
frontend/src/app/repos-table.tsx
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
"use client";
|
||||||
|
import { Index, Repo } from "@/lib/schemas";
|
||||||
|
import { search } from "@orama/orama";
|
||||||
|
import { SearchForm } from "./search-form";
|
||||||
|
import { columns } from "./columns";
|
||||||
|
import { DataTable } from "./data-table";
|
||||||
|
import { useReposOrama } from "@/lib/search";
|
||||||
|
import { useState } from "react";
|
||||||
|
|
||||||
|
export function ReposTable({ repos }: Index) {
|
||||||
|
const reposOrama = useReposOrama();
|
||||||
|
const [searchedRepos, setSearchedRepos] = useState<Repo[]>(repos);
|
||||||
|
|
||||||
|
const onSearchSubmit = async ({
|
||||||
|
search: description,
|
||||||
|
}: {
|
||||||
|
search: string;
|
||||||
|
}) => {
|
||||||
|
if (!reposOrama.isIndexed || !reposOrama.orama) {
|
||||||
|
throw new Error("Orama is not initialized");
|
||||||
|
}
|
||||||
|
const results = await search<Repo>(reposOrama.orama, {
|
||||||
|
term: description,
|
||||||
|
properties: ["description"],
|
||||||
|
limit: repos.length,
|
||||||
|
});
|
||||||
|
setSearchedRepos(results.hits.map((hit) => hit.document as Repo));
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="mb-4">
|
||||||
|
<SearchForm onSubmit={onSearchSubmit} />
|
||||||
|
</div>
|
||||||
|
<DataTable columns={columns} data={searchedRepos} />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
56
frontend/src/app/search-form.tsx
Normal file
56
frontend/src/app/search-form.tsx
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
"use client";
|
||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import { useForm } from "react-hook-form";
|
||||||
|
import * as z from "zod";
|
||||||
|
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import {
|
||||||
|
Form,
|
||||||
|
FormControl,
|
||||||
|
FormDescription,
|
||||||
|
FormField,
|
||||||
|
FormItem,
|
||||||
|
FormLabel,
|
||||||
|
FormMessage,
|
||||||
|
} from "@/components/ui/form";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
|
||||||
|
const FormSchema = z.object({
|
||||||
|
search: z
|
||||||
|
.string()
|
||||||
|
.max(1024, { message: "Search must be less than 1024 characters" }),
|
||||||
|
});
|
||||||
|
|
||||||
|
export interface SearchFormProps {
|
||||||
|
onSubmit: (data: z.infer<typeof FormSchema>) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SearchForm({ onSubmit }: SearchFormProps) {
|
||||||
|
const form = useForm<z.infer<typeof FormSchema>>({
|
||||||
|
resolver: zodResolver(FormSchema),
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Form {...form}>
|
||||||
|
<form onSubmit={form.handleSubmit(onSubmit)} className="w-2/3 space-y-6">
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="search"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Search for a repository</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input placeholder="fastapi" {...field} />
|
||||||
|
</FormControl>
|
||||||
|
<FormDescription>
|
||||||
|
The search is performed on the repository description.
|
||||||
|
</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Button type="submit">Search</Button>
|
||||||
|
</form>
|
||||||
|
</Form>
|
||||||
|
);
|
||||||
|
}
|
38
frontend/src/app/search-provider.tsx
Normal file
38
frontend/src/app/search-provider.tsx
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
"use client";
|
||||||
|
import { IOramaContext } from "@/lib/search";
|
||||||
|
import { Context, PropsWithChildren, useEffect, useState } from "react";
|
||||||
|
|
||||||
|
import { Orama, ProvidedTypes as OramaProvidedTypes } from "@orama/orama";
|
||||||
|
|
||||||
|
export type SearchProviderProps<
|
||||||
|
OramaParameters extends Partial<OramaProvidedTypes> = any,
|
||||||
|
> = PropsWithChildren<{
|
||||||
|
OramaContext: Context<IOramaContext<OramaParameters>>;
|
||||||
|
createIndex: () => Promise<Orama<OramaParameters>>;
|
||||||
|
}>;
|
||||||
|
|
||||||
|
export function SearchProvider<
|
||||||
|
OramaParameters extends Partial<OramaProvidedTypes>,
|
||||||
|
>({
|
||||||
|
children,
|
||||||
|
OramaContext,
|
||||||
|
createIndex,
|
||||||
|
}: SearchProviderProps<OramaParameters>) {
|
||||||
|
const [orama, setOrama] = useState<Orama<OramaParameters> | null>(null);
|
||||||
|
const [isIndexed, setIsIndexed] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
async function initializeOrama() {
|
||||||
|
setIsIndexed(false);
|
||||||
|
await createIndex().then(setOrama);
|
||||||
|
setIsIndexed(true);
|
||||||
|
}
|
||||||
|
initializeOrama();
|
||||||
|
}, [createIndex]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<OramaContext.Provider value={{ orama, isIndexed }}>
|
||||||
|
{children}
|
||||||
|
</OramaContext.Provider>
|
||||||
|
);
|
||||||
|
}
|
177
frontend/src/components/ui/form.tsx
Normal file
177
frontend/src/components/ui/form.tsx
Normal file
@ -0,0 +1,177 @@
|
|||||||
|
import * as React from "react";
|
||||||
|
import * as LabelPrimitive from "@radix-ui/react-label";
|
||||||
|
import { Slot } from "@radix-ui/react-slot";
|
||||||
|
import {
|
||||||
|
Controller,
|
||||||
|
ControllerProps,
|
||||||
|
FieldPath,
|
||||||
|
FieldValues,
|
||||||
|
FormProvider,
|
||||||
|
useFormContext,
|
||||||
|
} from "react-hook-form";
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
import { Label } from "@/components/ui/label";
|
||||||
|
|
||||||
|
const Form = FormProvider;
|
||||||
|
|
||||||
|
type FormFieldContextValue<
|
||||||
|
TFieldValues extends FieldValues = FieldValues,
|
||||||
|
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
|
||||||
|
> = {
|
||||||
|
name: TName;
|
||||||
|
};
|
||||||
|
|
||||||
|
const FormFieldContext = React.createContext<FormFieldContextValue>(
|
||||||
|
{} as FormFieldContextValue,
|
||||||
|
);
|
||||||
|
|
||||||
|
const FormField = <
|
||||||
|
TFieldValues extends FieldValues = FieldValues,
|
||||||
|
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
|
||||||
|
>({
|
||||||
|
...props
|
||||||
|
}: ControllerProps<TFieldValues, TName>) => {
|
||||||
|
return (
|
||||||
|
<FormFieldContext.Provider value={{ name: props.name }}>
|
||||||
|
<Controller {...props} />
|
||||||
|
</FormFieldContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const useFormField = () => {
|
||||||
|
const fieldContext = React.useContext(FormFieldContext);
|
||||||
|
const itemContext = React.useContext(FormItemContext);
|
||||||
|
const { getFieldState, formState } = useFormContext();
|
||||||
|
|
||||||
|
const fieldState = getFieldState(fieldContext.name, formState);
|
||||||
|
|
||||||
|
if (!fieldContext) {
|
||||||
|
throw new Error("useFormField should be used within <FormField>");
|
||||||
|
}
|
||||||
|
|
||||||
|
const { id } = itemContext;
|
||||||
|
|
||||||
|
return {
|
||||||
|
id,
|
||||||
|
name: fieldContext.name,
|
||||||
|
formItemId: `${id}-form-item`,
|
||||||
|
formDescriptionId: `${id}-form-item-description`,
|
||||||
|
formMessageId: `${id}-form-item-message`,
|
||||||
|
...fieldState,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
type FormItemContextValue = {
|
||||||
|
id: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const FormItemContext = React.createContext<FormItemContextValue>(
|
||||||
|
{} as FormItemContextValue,
|
||||||
|
);
|
||||||
|
|
||||||
|
const FormItem = React.forwardRef<
|
||||||
|
HTMLDivElement,
|
||||||
|
React.HTMLAttributes<HTMLDivElement>
|
||||||
|
>(({ className, ...props }, ref) => {
|
||||||
|
const id = React.useId();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FormItemContext.Provider value={{ id }}>
|
||||||
|
<div ref={ref} className={cn("space-y-2", className)} {...props} />
|
||||||
|
</FormItemContext.Provider>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
FormItem.displayName = "FormItem";
|
||||||
|
|
||||||
|
const FormLabel = React.forwardRef<
|
||||||
|
React.ElementRef<typeof LabelPrimitive.Root>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root>
|
||||||
|
>(({ className, ...props }, ref) => {
|
||||||
|
const { error, formItemId } = useFormField();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Label
|
||||||
|
ref={ref}
|
||||||
|
className={cn(error && "text-destructive", className)}
|
||||||
|
htmlFor={formItemId}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
FormLabel.displayName = "FormLabel";
|
||||||
|
|
||||||
|
const FormControl = React.forwardRef<
|
||||||
|
React.ElementRef<typeof Slot>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof Slot>
|
||||||
|
>(({ ...props }, ref) => {
|
||||||
|
const { error, formItemId, formDescriptionId, formMessageId } =
|
||||||
|
useFormField();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Slot
|
||||||
|
ref={ref}
|
||||||
|
id={formItemId}
|
||||||
|
aria-describedby={
|
||||||
|
!error
|
||||||
|
? `${formDescriptionId}`
|
||||||
|
: `${formDescriptionId} ${formMessageId}`
|
||||||
|
}
|
||||||
|
aria-invalid={!!error}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
FormControl.displayName = "FormControl";
|
||||||
|
|
||||||
|
const FormDescription = React.forwardRef<
|
||||||
|
HTMLParagraphElement,
|
||||||
|
React.HTMLAttributes<HTMLParagraphElement>
|
||||||
|
>(({ className, ...props }, ref) => {
|
||||||
|
const { formDescriptionId } = useFormField();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<p
|
||||||
|
ref={ref}
|
||||||
|
id={formDescriptionId}
|
||||||
|
className={cn("text-sm text-muted-foreground", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
FormDescription.displayName = "FormDescription";
|
||||||
|
|
||||||
|
const FormMessage = React.forwardRef<
|
||||||
|
HTMLParagraphElement,
|
||||||
|
React.HTMLAttributes<HTMLParagraphElement>
|
||||||
|
>(({ className, children, ...props }, ref) => {
|
||||||
|
const { error, formMessageId } = useFormField();
|
||||||
|
const body = error ? String(error?.message) : children;
|
||||||
|
|
||||||
|
if (!body) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<p
|
||||||
|
ref={ref}
|
||||||
|
id={formMessageId}
|
||||||
|
className={cn("text-sm font-medium text-destructive", className)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{body}
|
||||||
|
</p>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
FormMessage.displayName = "FormMessage";
|
||||||
|
|
||||||
|
export {
|
||||||
|
useFormField,
|
||||||
|
Form,
|
||||||
|
FormItem,
|
||||||
|
FormLabel,
|
||||||
|
FormControl,
|
||||||
|
FormDescription,
|
||||||
|
FormMessage,
|
||||||
|
FormField,
|
||||||
|
};
|
25
frontend/src/components/ui/input.tsx
Normal file
25
frontend/src/components/ui/input.tsx
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
import * as React from "react";
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
export interface InputProps
|
||||||
|
extends React.InputHTMLAttributes<HTMLInputElement> {}
|
||||||
|
|
||||||
|
const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
||||||
|
({ className, type, ...props }, ref) => {
|
||||||
|
return (
|
||||||
|
<input
|
||||||
|
type={type}
|
||||||
|
className={cn(
|
||||||
|
"flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
ref={ref}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
Input.displayName = "Input";
|
||||||
|
|
||||||
|
export { Input };
|
26
frontend/src/components/ui/label.tsx
Normal file
26
frontend/src/components/ui/label.tsx
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import * as React from "react";
|
||||||
|
import * as LabelPrimitive from "@radix-ui/react-label";
|
||||||
|
import { cva, type VariantProps } from "class-variance-authority";
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
const labelVariants = cva(
|
||||||
|
"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70",
|
||||||
|
);
|
||||||
|
|
||||||
|
const Label = React.forwardRef<
|
||||||
|
React.ElementRef<typeof LabelPrimitive.Root>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> &
|
||||||
|
VariantProps<typeof labelVariants>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<LabelPrimitive.Root
|
||||||
|
ref={ref}
|
||||||
|
className={cn(labelVariants(), className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
Label.displayName = LabelPrimitive.Root.displayName;
|
||||||
|
|
||||||
|
export { Label };
|
@ -21,6 +21,12 @@ export const loadIndexServerOnly = cache(async () => {
|
|||||||
try {
|
try {
|
||||||
const indexData = JSON.parse(
|
const indexData = JSON.parse(
|
||||||
await fs.promises.readFile(INDEX_FILE_PATH, "utf-8"),
|
await fs.promises.readFile(INDEX_FILE_PATH, "utf-8"),
|
||||||
|
(key, value) => {
|
||||||
|
if (key === "id" || key === "source_graph_repo_id") {
|
||||||
|
return String(value);
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
},
|
||||||
);
|
);
|
||||||
return await indexSchema.parseAsync(indexData);
|
return await indexSchema.parseAsync(indexData);
|
||||||
} catch (err) {
|
} catch (err) {
|
@ -1,16 +1,16 @@
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
export const dependencySchema = z.object({
|
export const dependencySchema = z.object({
|
||||||
id: z.number().min(0),
|
id: z.string(),
|
||||||
name: z.string(),
|
name: z.string(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const repoSchema = z.object({
|
export const repoSchema = z.object({
|
||||||
id: z.number().min(0),
|
id: z.string(),
|
||||||
url: z.string(),
|
url: z.string(),
|
||||||
description: z.string(),
|
description: z.string(),
|
||||||
stars: z.number().min(0),
|
stars: z.number().min(0),
|
||||||
source_graph_repo_id: z.number().min(0),
|
source_graph_repo_id: z.string(),
|
||||||
dependencies: z.array(dependencySchema),
|
dependencies: z.array(dependencySchema),
|
||||||
last_checked_revision: z.nullable(z.string()),
|
last_checked_revision: z.nullable(z.string()),
|
||||||
});
|
});
|
||||||
|
55
frontend/src/lib/search.ts
Normal file
55
frontend/src/lib/search.ts
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
import {
|
||||||
|
Orama,
|
||||||
|
ProvidedTypes as OramaProvidedTypes,
|
||||||
|
create,
|
||||||
|
insertMultiple,
|
||||||
|
} from "@orama/orama";
|
||||||
|
import { Index } from "./schemas";
|
||||||
|
import { Context, createContext, useContext } from "react";
|
||||||
|
|
||||||
|
export interface IOramaContext<
|
||||||
|
OramaParameters extends Partial<OramaProvidedTypes> = any,
|
||||||
|
> {
|
||||||
|
isIndexed: boolean;
|
||||||
|
orama: Orama<OramaParameters> | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createOramaContext<
|
||||||
|
OramaParameters extends Partial<OramaProvidedTypes>,
|
||||||
|
>(): Context<IOramaContext<OramaParameters>> {
|
||||||
|
return createContext<IOramaContext<OramaParameters>>({
|
||||||
|
isIndexed: false,
|
||||||
|
orama: null,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ReposOramaParameters extends Partial<OramaProvidedTypes> {
|
||||||
|
Index: { description: string };
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ReposOrama = Orama<ReposOramaParameters>;
|
||||||
|
|
||||||
|
export async function createReposOrama(): Promise<ReposOrama> {
|
||||||
|
const orama = await create({
|
||||||
|
schema: {
|
||||||
|
description: "string",
|
||||||
|
},
|
||||||
|
id: "index",
|
||||||
|
});
|
||||||
|
return orama;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function prepareReposOramaIndex(
|
||||||
|
orama: ReposOrama,
|
||||||
|
data: Index["repos"],
|
||||||
|
): Promise<Orama<{ Index: { description: string } }>> {
|
||||||
|
await insertMultiple(orama, data, 100);
|
||||||
|
return orama;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ReposOramaContext = createOramaContext<ReposOramaParameters>();
|
||||||
|
ReposOramaContext.displayName = "ReposOramaContext";
|
||||||
|
|
||||||
|
export const useReposOrama = () => {
|
||||||
|
return useContext(ReposOramaContext);
|
||||||
|
};
|
@ -1,6 +1,6 @@
|
|||||||
import { type ClassValue, clsx } from "clsx"
|
import { type ClassValue, clsx } from "clsx";
|
||||||
import { twMerge } from "tailwind-merge"
|
import { twMerge } from "tailwind-merge";
|
||||||
|
|
||||||
export function cn(...inputs: ClassValue[]) {
|
export function cn(...inputs: ClassValue[]) {
|
||||||
return twMerge(clsx(inputs))
|
return twMerge(clsx(inputs));
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user