├── .eslintignore ├── .eslintrc.json ├── .gitignore ├── .npmrc ├── LICENSE ├── README.md ├── app ├── api │ └── search │ │ └── route.ts ├── docs │ ├── layout.tsx │ └── page.tsx ├── error.tsx ├── layout.tsx ├── page.tsx ├── providers.tsx └── search │ ├── layout.tsx │ └── page.tsx ├── components ├── counter.tsx ├── icons.tsx ├── loading.tsx ├── navbar.tsx ├── primitives.ts ├── searchInput.tsx ├── searchInputList.tsx ├── theme-switch.tsx └── verseResult.tsx ├── config ├── fonts.ts └── site.ts ├── features └── search.tsx ├── models └── IVerseResult.ts ├── next.config.js ├── package.json ├── postcss.config.js ├── public ├── favicon.ico ├── logo.jpg └── logo_at.png ├── styles └── globals.css ├── tailwind.config.js ├── tsconfig.json └── types └── index.ts /.eslintignore: -------------------------------------------------------------------------------- 1 | .now/* 2 | *.css 3 | .changeset 4 | dist 5 | esm/* 6 | public/* 7 | tests/* 8 | scripts/* 9 | *.config.js 10 | .DS_Store 11 | node_modules 12 | coverage 13 | .next 14 | build 15 | !.commitlintrc.cjs 16 | !.lintstagedrc.cjs 17 | !jest.config.js 18 | !plopfile.js 19 | !react-shim.js 20 | !tsup.config.ts -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/eslintrc.json", 3 | "env": { 4 | "browser": false, 5 | "es2021": true, 6 | "node": true 7 | }, 8 | "extends": [ 9 | "plugin:react/recommended", 10 | "plugin:prettier/recommended", 11 | "plugin:react-hooks/recommended", 12 | "plugin:jsx-a11y/recommended" 13 | ], 14 | "plugins": ["react", "unused-imports", "import", "@typescript-eslint", "jsx-a11y", "prettier"], 15 | "parser": "@typescript-eslint/parser", 16 | "parserOptions": { 17 | "ecmaFeatures": { 18 | "jsx": true 19 | }, 20 | "ecmaVersion": 12, 21 | "sourceType": "module" 22 | }, 23 | "settings": { 24 | "react": { 25 | "version": "detect" 26 | } 27 | }, 28 | "rules": { 29 | "no-console": "warn", 30 | "react/prop-types": "off", 31 | "react/jsx-uses-react": "off", 32 | "react/react-in-jsx-scope": "off", 33 | "react-hooks/exhaustive-deps": "off", 34 | "jsx-a11y/click-events-have-key-events": "warn", 35 | "jsx-a11y/interactive-supports-focus": "warn", 36 | "prettier/prettier": "warn", 37 | "no-unused-vars": "off", 38 | "unused-imports/no-unused-vars": "off", 39 | "unused-imports/no-unused-imports": "warn", 40 | "@typescript-eslint/no-unused-vars": [ 41 | "warn", 42 | { 43 | "args": "after-used", 44 | "ignoreRestSiblings": false, 45 | "argsIgnorePattern": "^_.*?$" 46 | } 47 | ], 48 | "import/order": [ 49 | "warn", 50 | { 51 | "groups": [ 52 | "type", 53 | "builtin", 54 | "object", 55 | "external", 56 | "internal", 57 | "parent", 58 | "sibling", 59 | "index" 60 | ], 61 | "pathGroups": [ 62 | { 63 | "pattern": "~/**", 64 | "group": "external", 65 | "position": "after" 66 | } 67 | ], 68 | "newlines-between": "always" 69 | } 70 | ], 71 | "react/self-closing-comp": "warn", 72 | "react/jsx-sort-props": [ 73 | "warn", 74 | { 75 | "callbacksLast": true, 76 | "shorthandFirst": true, 77 | "noSortAlphabetically": false, 78 | "reservedFirst": true 79 | } 80 | ], 81 | "padding-line-between-statements": [ 82 | "warn", 83 | {"blankLine": "always", "prev": "*", "next": "return"}, 84 | {"blankLine": "always", "prev": ["const", "let", "var"], "next": "*"}, 85 | { 86 | "blankLine": "any", 87 | "prev": ["const", "let", "var"], 88 | "next": ["const", "let", "var"] 89 | } 90 | ] 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | 27 | # local env files 28 | .env*.local 29 | 30 | # vercel 31 | .vercel 32 | 33 | # typescript 34 | *.tsbuildinfo 35 | next-env.d.ts 36 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | public-hoist-pattern[]=*@nextui-org/* 2 | package-lock=false 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 @Antioch-Tech 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Bible Vector Search 2 | 3 | URL: https://bible-search.antioch.tech 4 | API: https://bible-search.antioch.tech/api 5 | 6 | 7 | 8 | ## Description 9 | 10 | This is a simple Bible verse search tool to use AI Embedding and Vector Database stacks to provide a search engine for Bible verses (BBE translation). 11 | It provides a way to search verses in its meanings not in keywords. 12 | 13 | -------------------------------------------------------------------------------- /app/api/search/route.ts: -------------------------------------------------------------------------------- 1 | import { type NextRequest } from "next/server"; 2 | 3 | export async function GET(request: NextRequest) { 4 | const searchParams = request.nextUrl.searchParams; 5 | const query = searchParams.get("verse_query"); 6 | // query is "hello" for /api/search?query=hello 7 | 8 | const url = process.env.API_URL + "/api/bible/?verse_query=" + query; 9 | 10 | const res = await fetch(url, { 11 | headers: { 12 | "Content-Type": "application/json", 13 | }, 14 | next: { revalidate: 3600 }, // Revalidate every 60 seconds 15 | }); 16 | const data = await res.json(); 17 | 18 | return Response.json(data); 19 | } 20 | -------------------------------------------------------------------------------- /app/docs/layout.tsx: -------------------------------------------------------------------------------- 1 | export default function DocsLayout({ 2 | children, 3 | }: { 4 | children: React.ReactNode; 5 | }) { 6 | return ( 7 |
8 |
9 | {children} 10 |
11 |
12 | ); 13 | } 14 | -------------------------------------------------------------------------------- /app/docs/page.tsx: -------------------------------------------------------------------------------- 1 | import { Code } from "@nextui-org/code"; 2 | 3 | import { title } from "@/components/primitives"; 4 | 5 | export default function DocsPage() { 6 | return ( 7 |
8 |

API Docs

9 | 10 |
11 | We have a very simply and straightforward API. You can use it to search 12 | for Bible verses. 13 |
14 | See example below: 15 |
16 |
17 |
18 |

For example,

19 | 20 | https://bible-search.antioch.tech/api/search?verse_query=GodIsLove 21 | 22 |
23 |
24 | ); 25 | } 26 | -------------------------------------------------------------------------------- /app/error.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useEffect } from "react"; 4 | 5 | export default function Error({ 6 | error, 7 | reset, 8 | }: { 9 | error: Error; 10 | reset: () => void; 11 | }) { 12 | useEffect(() => { 13 | // Log the error to an error reporting service 14 | /* eslint-disable no-console */ 15 | console.error(error); 16 | }, [error]); 17 | 18 | return ( 19 |
20 |

Something went wrong!

21 | 29 |
30 | ); 31 | } 32 | -------------------------------------------------------------------------------- /app/layout.tsx: -------------------------------------------------------------------------------- 1 | import "@/styles/globals.css"; 2 | import { Metadata, Viewport } from "next"; 3 | import { Link } from "@nextui-org/link"; 4 | import clsx from "clsx"; 5 | 6 | import { Providers } from "./providers"; 7 | 8 | import { siteConfig } from "@/config/site"; 9 | import { fontSans } from "@/config/fonts"; 10 | import { Navbar } from "@/components/navbar"; 11 | 12 | export const metadata: Metadata = { 13 | title: { 14 | default: siteConfig.name, 15 | template: `%s - ${siteConfig.name}`, 16 | }, 17 | description: siteConfig.description, 18 | icons: { 19 | icon: "/favicon.ico", 20 | }, 21 | }; 22 | 23 | export const viewport: Viewport = { 24 | themeColor: [ 25 | { media: "(prefers-color-scheme: light)", color: "white" }, 26 | { media: "(prefers-color-scheme: dark)", color: "black" }, 27 | ], 28 | }; 29 | 30 | export default function RootLayout({ 31 | children, 32 | }: { 33 | children: React.ReactNode; 34 | }) { 35 | return ( 36 | 37 | 38 | 44 | 51 |
52 | 53 |
54 | {children} 55 |
56 | 67 |
68 |
69 | 70 | 71 | ); 72 | } 73 | -------------------------------------------------------------------------------- /app/page.tsx: -------------------------------------------------------------------------------- 1 | import { button as buttonStyles } from "@nextui-org/theme"; 2 | import NextLink from "next/link"; 3 | import { Image } from "@nextui-org/image"; 4 | 5 | import { title } from "@/components/primitives"; 6 | 7 | export default function Home() { 8 | return ( 9 |
10 |
11 |

Search 

12 |

Bible Verse 

13 |
14 |

through meaning, not just words.

15 | {/*

*/} 16 | {/* Beautiful, fast and modern React UI library.*/} 17 | {/*

*/} 18 |
19 | 20 |
21 | 29 | Get Started 30 | 31 |
32 | 33 | {/*
*/} 34 | {/* */} 35 | {/* */} 36 | {/* Get started by editing app/page.tsx*/} 37 | {/* */} 38 | {/* */} 39 | {/*
*/} 40 | 41 |
42 | Demo of Bible Vector Search 49 |
50 |
51 | ); 52 | } 53 | -------------------------------------------------------------------------------- /app/providers.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as React from "react"; 4 | import { NextUIProvider } from "@nextui-org/system"; 5 | import { useRouter } from "next/navigation"; 6 | import { ThemeProvider as NextThemesProvider } from "next-themes"; 7 | import { ThemeProviderProps } from "next-themes/dist/types"; 8 | 9 | export interface ProvidersProps { 10 | children: React.ReactNode; 11 | themeProps?: ThemeProviderProps; 12 | } 13 | 14 | export function Providers({ children, themeProps }: ProvidersProps) { 15 | const router = useRouter(); 16 | 17 | return ( 18 | 19 | {children} 20 | 21 | ); 22 | } 23 | -------------------------------------------------------------------------------- /app/search/layout.tsx: -------------------------------------------------------------------------------- 1 | export default function SearchLayout({ 2 | children, 3 | }: { 4 | children: React.ReactNode; 5 | }) { 6 | return ( 7 |
8 |
9 | {children} 10 |
11 |
12 | ); 13 | } 14 | -------------------------------------------------------------------------------- /app/search/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { Suspense } from "react"; 4 | 5 | import Search from "@/features/search"; 6 | import Loading from "@/components/loading"; 7 | 8 | export default function SearchPage() { 9 | return ( 10 | }> 11 | 12 | 13 | ); 14 | } 15 | -------------------------------------------------------------------------------- /components/counter.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useState } from "react"; 4 | import { Button } from "@nextui-org/button"; 5 | 6 | export const Counter = () => { 7 | const [count, setCount] = useState(0); 8 | 9 | return ( 10 | 13 | ); 14 | }; 15 | -------------------------------------------------------------------------------- /components/icons.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { Image, ImageProps } from "@nextui-org/image"; 3 | 4 | import { IconSvgProps } from "@/types"; 5 | 6 | export const Logo: React.FC = ({ 7 | width = 36, 8 | height = 36, 9 | ...props 10 | }) => ( 11 | <> 12 | logo 19 | 20 | ); 21 | 22 | export const DiscordIcon: React.FC = ({ 23 | size = 24, 24 | width, 25 | height, 26 | ...props 27 | }) => { 28 | return ( 29 | 35 | 39 | 40 | ); 41 | }; 42 | 43 | export const TwitterIcon: React.FC = ({ 44 | size = 24, 45 | width, 46 | height, 47 | ...props 48 | }) => { 49 | return ( 50 | 56 | 60 | 61 | ); 62 | }; 63 | 64 | export const GithubIcon: React.FC = ({ 65 | size = 24, 66 | width, 67 | height, 68 | ...props 69 | }) => { 70 | return ( 71 | 77 | 83 | 84 | ); 85 | }; 86 | 87 | export const MoonFilledIcon = ({ 88 | size = 24, 89 | width, 90 | height, 91 | ...props 92 | }: IconSvgProps) => ( 93 | 107 | ); 108 | 109 | export const SunFilledIcon = ({ 110 | size = 24, 111 | width, 112 | height, 113 | ...props 114 | }: IconSvgProps) => ( 115 | 129 | ); 130 | 131 | export const HeartFilledIcon = ({ 132 | size = 24, 133 | width, 134 | height, 135 | ...props 136 | }: IconSvgProps) => ( 137 | 154 | ); 155 | 156 | export const SearchIcon = (props: IconSvgProps) => ( 157 | 182 | ); 183 | 184 | export const NextUILogo: React.FC = (props) => { 185 | const { width, height = 40 } = props; 186 | 187 | return ( 188 | 196 | 200 | 204 | 208 | 209 | ); 210 | }; 211 | -------------------------------------------------------------------------------- /components/loading.tsx: -------------------------------------------------------------------------------- 1 | import { Progress } from "@nextui-org/progress"; 2 | 3 | const Loading: React.FC = () => { 4 | return ( 5 | 11 | ); 12 | }; 13 | 14 | export default Loading; 15 | -------------------------------------------------------------------------------- /components/navbar.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Navbar as NextUINavbar, 3 | NavbarContent, 4 | NavbarMenu, 5 | NavbarMenuToggle, 6 | NavbarBrand, 7 | NavbarItem, 8 | NavbarMenuItem, 9 | } from "@nextui-org/navbar"; 10 | import { Button } from "@nextui-org/button"; 11 | import { Link } from "@nextui-org/link"; 12 | import { link as linkStyles } from "@nextui-org/theme"; 13 | import NextLink from "next/link"; 14 | import clsx from "clsx"; 15 | 16 | import { siteConfig } from "@/config/site"; 17 | import { ThemeSwitch } from "@/components/theme-switch"; 18 | import { 19 | TwitterIcon, 20 | GithubIcon, 21 | DiscordIcon, 22 | HeartFilledIcon, 23 | Logo, 24 | } from "@/components/icons"; 25 | 26 | export const Navbar = () => { 27 | return ( 28 | 29 | 30 | 31 | 32 | 33 |

{siteConfig.name}

34 |
35 |
36 |
    37 | {siteConfig.navItems.map((item) => ( 38 | 39 | 47 | {item.label} 48 | 49 | 50 | ))} 51 |
52 |
53 | 54 | 58 | 59 | {siteConfig.links?.twitter && ( 60 | 65 | 66 | 67 | )} 68 | {siteConfig.links?.discord && ( 69 | 74 | 75 | 76 | )} 77 | {siteConfig.links?.docs && ( 78 | 83 | Docs 84 | 85 | )} 86 | {siteConfig.links?.github && ( 87 | 88 | 89 | 90 | )} 91 | 92 | 93 | {/*{searchInput}*/} 94 | 95 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | {/*{searchInput}*/} 118 |
119 | {siteConfig.navMenuItems.map((item, index) => ( 120 | 121 | {item.label} 122 | 123 | ))} 124 |
125 | 126 | 136 |
137 |
138 | ); 139 | }; 140 | -------------------------------------------------------------------------------- /components/primitives.ts: -------------------------------------------------------------------------------- 1 | import { tv } from "tailwind-variants"; 2 | 3 | export const title = tv({ 4 | base: "tracking-tight inline font-semibold", 5 | variants: { 6 | color: { 7 | violet: "from-[#FF1CF7] to-[#b249f8]", 8 | yellow: "from-[#FF705B] to-[#FFB457]", 9 | blue: "from-[#5EA2EF] to-[#0072F5]", 10 | cyan: "from-[#00b7fa] to-[#01cfea]", 11 | green: "from-[#6FEE8D] to-[#17c964]", 12 | pink: "from-[#FF72E1] to-[#F54C7A]", 13 | foreground: "dark:from-[#FFFFFF] dark:to-[#4B4B4B]", 14 | }, 15 | size: { 16 | sm: "text-3xl lg:text-4xl", 17 | md: "text-[2.3rem] lg:text-5xl leading-9", 18 | lg: "text-4xl lg:text-6xl", 19 | }, 20 | fullWidth: { 21 | true: "w-full block", 22 | }, 23 | }, 24 | defaultVariants: { 25 | size: "md", 26 | }, 27 | compoundVariants: [ 28 | { 29 | color: [ 30 | "violet", 31 | "yellow", 32 | "blue", 33 | "cyan", 34 | "green", 35 | "pink", 36 | "foreground", 37 | ], 38 | class: "bg-clip-text text-transparent bg-gradient-to-b", 39 | }, 40 | ], 41 | }); 42 | 43 | export const subtitle = tv({ 44 | base: "w-full md:w-1/2 my-2 text-lg lg:text-xl text-default-600 block max-w-full", 45 | variants: { 46 | fullWidth: { 47 | true: "!w-full", 48 | }, 49 | }, 50 | defaultVariants: { 51 | fullWidth: true, 52 | }, 53 | }); 54 | -------------------------------------------------------------------------------- /components/searchInput.tsx: -------------------------------------------------------------------------------- 1 | import { Input } from "@nextui-org/input"; 2 | import { Button } from "@nextui-org/button"; 3 | import { useState } from "react"; 4 | 5 | import { SearchIcon } from "@/components/icons"; 6 | 7 | export const SearchInput = ({ 8 | value, 9 | onSearch, 10 | onChange, 11 | }: { 12 | value: string; 13 | onChange: (query: string) => void; 14 | onSearch: () => Promise; 15 | }) => { 16 | const [query, setQuery] = useState(value); 17 | 18 | const toSearch = async () => { 19 | onChange(query); 20 | await onSearch(); 21 | }; 22 | 23 | return ( 24 |
25 | 35 | } 36 | type="search" 37 | value={query} 38 | onChange={(e) => { 39 | setQuery(e.target.value); 40 | }} 41 | onKeyPress={(e) => { 42 | if (e.key === "Enter") { 43 | toSearch(); 44 | } 45 | }} 46 | /> 47 | 50 |
51 | ); 52 | }; 53 | -------------------------------------------------------------------------------- /components/searchInputList.tsx: -------------------------------------------------------------------------------- 1 | import { Suspense } from "react"; 2 | 3 | import { IVerseResult } from "@/models/IVerseResult"; 4 | import Loading from "@/components/loading"; 5 | import { VerseResult } from "@/components/verseResult"; 6 | 7 | export const SearchInputList = ({ 8 | verseResults, 9 | query, 10 | }: { 11 | verseResults: IVerseResult[]; 12 | query: string; 13 | }) => { 14 | return ( 15 | }> 16 | {query && 17 | verseResults?.map((result) => ( 18 | 19 | ))} 20 | 21 | ); 22 | }; 23 | -------------------------------------------------------------------------------- /components/theme-switch.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { FC } from "react"; 4 | import { VisuallyHidden } from "@react-aria/visually-hidden"; 5 | import { SwitchProps, useSwitch } from "@nextui-org/switch"; 6 | import { useTheme } from "next-themes"; 7 | import { useIsSSR } from "@react-aria/ssr"; 8 | import clsx from "clsx"; 9 | 10 | import { SunFilledIcon, MoonFilledIcon } from "@/components/icons"; 11 | 12 | export interface ThemeSwitchProps { 13 | className?: string; 14 | classNames?: SwitchProps["classNames"]; 15 | } 16 | 17 | export const ThemeSwitch: FC = ({ 18 | className, 19 | classNames, 20 | }) => { 21 | const { theme, setTheme } = useTheme(); 22 | const isSSR = useIsSSR(); 23 | 24 | const onChange = () => { 25 | theme === "light" ? setTheme("dark") : setTheme("light"); 26 | }; 27 | 28 | const { 29 | Component, 30 | slots, 31 | isSelected, 32 | getBaseProps, 33 | getInputProps, 34 | getWrapperProps, 35 | } = useSwitch({ 36 | isSelected: theme === "light" || isSSR, 37 | "aria-label": `Switch to ${theme === "light" || isSSR ? "dark" : "light"} mode`, 38 | onChange, 39 | }); 40 | 41 | return ( 42 | 51 | 52 | 53 | 54 |
73 | {!isSelected || isSSR ? ( 74 | 75 | ) : ( 76 | 77 | )} 78 |
79 |
80 | ); 81 | }; 82 | -------------------------------------------------------------------------------- /components/verseResult.tsx: -------------------------------------------------------------------------------- 1 | import { Card, CardBody, CardFooter } from "@nextui-org/card"; 2 | import { Divider } from "@nextui-org/divider"; 3 | import { Chip } from "@nextui-org/chip"; 4 | 5 | import { IVerseResult } from "@/models/IVerseResult"; 6 | 7 | export const VerseResult = ({ verse }: { verse: IVerseResult }) => { 8 | const { 9 | Index, 10 | book_name, 11 | book_number, 12 | chapter_number, 13 | translation_name, 14 | verse_number, 15 | verse_text, 16 | } = verse; 17 | 18 | return ( 19 | 20 | 21 |

{verse_text}

22 |
23 | 24 | 25 | 26 | {book_name} {chapter_number}:{verse_number} 27 | 28 | 29 |
30 | ); 31 | }; 32 | -------------------------------------------------------------------------------- /config/fonts.ts: -------------------------------------------------------------------------------- 1 | import { Fira_Code as FontMono, Inter as FontSans } from "next/font/google"; 2 | 3 | export const fontSans = FontSans({ 4 | subsets: ["latin"], 5 | variable: "--font-sans", 6 | }); 7 | 8 | export const fontMono = FontMono({ 9 | subsets: ["latin"], 10 | variable: "--font-mono", 11 | }); 12 | -------------------------------------------------------------------------------- /config/site.ts: -------------------------------------------------------------------------------- 1 | export type SiteConfig = typeof siteConfig; 2 | 3 | const navItems = [ 4 | { 5 | label: "Search", 6 | href: "/search", 7 | }, 8 | { 9 | label: "Docs", 10 | href: "/docs", 11 | }, 12 | 13 | // { 14 | // label: "Blog", 15 | // href: "/blog", 16 | // }, 17 | // { 18 | // label: "About", 19 | // href: "/about", 20 | // }, 21 | ]; 22 | 23 | const siteConfig = { 24 | name: "Bible Vector Search", 25 | description: 26 | "Search Bible verses by meanings not key words (BBE Translation).", 27 | navItems: navItems, 28 | navMenuItems: navItems, 29 | links: { 30 | github: "https://github.com/Antioch-Tech/bible-vector-search-ui", 31 | docs: "", 32 | twitter: "", 33 | discord: "", 34 | sponsor: "https://github.com/sponsors/tim-hub", 35 | }, 36 | }; 37 | 38 | export { siteConfig }; 39 | -------------------------------------------------------------------------------- /features/search.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from "react"; 2 | import { Input } from "@nextui-org/input"; 3 | import { Button } from "@nextui-org/button"; 4 | 5 | import { IVerseResult } from "@/models/IVerseResult"; 6 | import { title } from "@/components/primitives"; 7 | import Loading from "@/components/loading"; 8 | import { SearchInputList } from "@/components/searchInputList"; 9 | import { SearchIcon } from "@/components/icons"; 10 | 11 | function debounceAsync Promise>( 12 | fn: T, 13 | delay: number, 14 | ) { 15 | let timeoutId: ReturnType; 16 | 17 | return (...args: Parameters): Promise> => { 18 | // Clear the previous timeout if it exists 19 | if (timeoutId) { 20 | clearTimeout(timeoutId); 21 | } 22 | 23 | return new Promise((resolve, reject) => { 24 | timeoutId = setTimeout(() => { 25 | fn(...args) 26 | .then(resolve) 27 | .catch(reject); 28 | }, delay); 29 | }); 30 | }; 31 | } 32 | 33 | const getData = async (query: string): Promise => { 34 | if (!query) return []; 35 | 36 | const res = await fetch(`/api/search/?verse_query=${query}`, { 37 | next: { revalidate: 3600 }, 38 | }); 39 | 40 | if (!res.ok) { 41 | // This will activate the closest `error.js` Error Boundary 42 | throw new Error("Failed to fetch data"); 43 | } 44 | 45 | return res.json(); 46 | }; 47 | 48 | export default function Search() { 49 | const [results, setResults] = useState([]); 50 | const [query, setQuery] = useState(""); 51 | const [startSearching, setStartSearching] = useState(false); 52 | const search = async () => { 53 | if (!query) return; 54 | setStartSearching(true); 55 | setResults([]); 56 | const results = await getData(query); 57 | 58 | setResults(results); 59 | }; 60 | 61 | const debouncedSearch = debounceAsync(search, 500); 62 | 63 | return ( 64 | <> 65 |
66 |

Search

67 |
68 |
69 |

Search Bible verses in meaning not in words.

70 |
71 |
72 |
73 | 83 | } 84 | type="search" 85 | value={query} 86 | onChange={(e) => { 87 | setQuery(e.target.value); 88 | }} 89 | onKeyPress={(e) => { 90 | if (e.key === "Enter") { 91 | debouncedSearch(); 92 | } 93 | }} 94 | /> 95 | 98 |
99 |
100 | 101 |
102 | {startSearching && results?.length === 0 ? ( 103 | 104 | ) : ( 105 | 106 | )} 107 |
108 | 109 | ); 110 | } 111 | -------------------------------------------------------------------------------- /models/IVerseResult.ts: -------------------------------------------------------------------------------- 1 | export interface IVerseResult { 2 | Index: number; 3 | book_name: string; 4 | book_number: number; 5 | chapter_number: number; 6 | translation_name: string; 7 | verse_number: number; 8 | verse_text: string; 9 | } 10 | -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | images: { 4 | remotePatterns: [ 5 | { 6 | protocol: "https", 7 | hostname: "i.imgur.com", 8 | port: "", 9 | pathname: "/snptle9.gif", 10 | }, 11 | ], 12 | }, 13 | }; 14 | 15 | module.exports = nextConfig; 16 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "next-app-template", 3 | "version": "0.0.1", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev --turbo", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "eslint . --ext .ts,.tsx -c .eslintrc.json --fix" 10 | }, 11 | "dependencies": { 12 | "@nextui-org/button": "2.0.35", 13 | "@nextui-org/card": "^2.0.33", 14 | "@nextui-org/chip": "^2.0.32", 15 | "@nextui-org/code": "2.0.30", 16 | "@nextui-org/image": "^2.0.31", 17 | "@nextui-org/input": "2.2.3", 18 | "@nextui-org/kbd": "2.0.31", 19 | "@nextui-org/link": "2.0.33", 20 | "@nextui-org/listbox": "2.1.23", 21 | "@nextui-org/navbar": "2.0.34", 22 | "@nextui-org/progress": "^2.0.33", 23 | "@nextui-org/snippet": "2.0.39", 24 | "@nextui-org/switch": "2.0.32", 25 | "@nextui-org/system": "2.2.1", 26 | "@nextui-org/theme": "2.2.5", 27 | "@react-aria/ssr": "3.9.4", 28 | "@react-aria/visually-hidden": "3.8.12", 29 | "clsx": "2.1.1", 30 | "framer-motion": "^11.3.7", 31 | "intl-messageformat": "^10.5.14", 32 | "next": "^14.2.5", 33 | "next-themes": "^0.3.0", 34 | "react": "18.3.1", 35 | "react-dom": "18.3.1" 36 | }, 37 | "devDependencies": { 38 | "@types/node": "20.5.7", 39 | "@types/react": "18.3.3", 40 | "@types/react-dom": "18.3.0", 41 | "@typescript-eslint/eslint-plugin": "7.2.0", 42 | "@typescript-eslint/parser": "7.2.0", 43 | "autoprefixer": "10.4.19", 44 | "eslint": "^8.57.0", 45 | "eslint-config-next": "14.2.1", 46 | "eslint-config-prettier": "^8.10.0", 47 | "eslint-plugin-import": "^2.29.1", 48 | "eslint-plugin-jsx-a11y": "^6.9.0", 49 | "eslint-plugin-node": "^11.1.0", 50 | "eslint-plugin-prettier": "^5.2.1", 51 | "eslint-plugin-react": "^7.34.4", 52 | "eslint-plugin-react-hooks": "^4.6.2", 53 | "eslint-plugin-unused-imports": "^3.2.0", 54 | "postcss": "^8.4.39", 55 | "tailwind-variants": "0.1.20", 56 | "tailwindcss": "^3.4.3", 57 | "typescript": "^5.5.3" 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tim-hub/bible-vector-search/aff841a810f90d72a1873f3c6f08808573e1a5a0/public/favicon.ico -------------------------------------------------------------------------------- /public/logo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tim-hub/bible-vector-search/aff841a810f90d72a1873f3c6f08808573e1a5a0/public/logo.jpg -------------------------------------------------------------------------------- /public/logo_at.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tim-hub/bible-vector-search/aff841a810f90d72a1873f3c6f08808573e1a5a0/public/logo_at.png -------------------------------------------------------------------------------- /styles/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | import {nextui} from '@nextui-org/theme' 2 | 3 | /** @type {import('tailwindcss').Config} */ 4 | module.exports = { 5 | content: [ 6 | './components/**/*.{js,ts,jsx,tsx,mdx}', 7 | './app/**/*.{js,ts,jsx,tsx,mdx}', 8 | './node_modules/@nextui-org/theme/dist/**/*.{js,ts,jsx,tsx}' 9 | ], 10 | theme: { 11 | extend: {}, 12 | }, 13 | darkMode: "class", 14 | plugins: [nextui()], 15 | } 16 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "forceConsistentCasingInFileNames": true, 9 | "noEmit": true, 10 | "esModuleInterop": true, 11 | "module": "esnext", 12 | "moduleResolution": "node", 13 | "resolveJsonModule": true, 14 | "isolatedModules": true, 15 | "jsx": "preserve", 16 | "incremental": true, 17 | "plugins": [ 18 | { 19 | "name": "next" 20 | } 21 | ], 22 | "paths": { 23 | "@/*": ["./*"] 24 | } 25 | }, 26 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 27 | "exclude": ["node_modules"] 28 | } 29 | -------------------------------------------------------------------------------- /types/index.ts: -------------------------------------------------------------------------------- 1 | import { SVGProps } from "react"; 2 | 3 | export type IconSvgProps = SVGProps & { 4 | size?: number; 5 | }; 6 | --------------------------------------------------------------------------------