├── .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 |
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 |
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 |
19 | >
20 | );
21 |
22 | export const DiscordIcon: React.FC = ({
23 | size = 24,
24 | width,
25 | height,
26 | ...props
27 | }) => {
28 | return (
29 |
40 | );
41 | };
42 |
43 | export const TwitterIcon: React.FC = ({
44 | size = 24,
45 | width,
46 | height,
47 | ...props
48 | }) => {
49 | return (
50 |
61 | );
62 | };
63 |
64 | export const GithubIcon: React.FC = ({
65 | size = 24,
66 | width,
67 | height,
68 | ...props
69 | }) => {
70 | return (
71 |
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 |
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 | }
101 | variant="flat"
102 | >
103 | Sponsor
104 |
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 | }
132 | variant="flat"
133 | >
134 | Sponsor
135 |
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 |
--------------------------------------------------------------------------------