├── .eslintrc.json ├── .github ├── FUNDING.yml └── workflows │ └── dependency-review.yml ├── .gitignore ├── .vscode └── launch.json ├── README.md ├── components.json ├── next.config.js ├── package-lock.json ├── package.json ├── postcss.config.js ├── public └── logo.svg ├── src ├── app │ ├── api │ │ ├── book │ │ │ └── route.ts │ │ └── passage │ │ │ └── route.ts │ ├── globals.css │ ├── layout.tsx │ ├── page.module.css │ ├── page.tsx │ └── passage │ │ └── page.tsx ├── components │ ├── Book.tsx │ └── Navbar.tsx ├── hooks │ ├── useBook.tsx │ ├── useModal.tsx │ └── usePassage.tsx └── lib │ └── utils.ts ├── tailwind.config.js └── tsconfig.json /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [raselldev] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry 13 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 14 | -------------------------------------------------------------------------------- /.github/workflows/dependency-review.yml: -------------------------------------------------------------------------------- 1 | # Dependency Review Action 2 | # 3 | # This Action will scan dependency manifest files that change as part of a Pull Request, surfacing known-vulnerable versions of the packages declared or updated in the PR. Once installed, if the workflow run is marked as required, PRs introducing known-vulnerable packages will be blocked from merging. 4 | # 5 | # Source repository: https://github.com/actions/dependency-review-action 6 | # Public documentation: https://docs.github.com/en/code-security/supply-chain-security/understanding-your-software-supply-chain/about-dependency-review#dependency-review-enforcement 7 | name: 'Dependency Review' 8 | on: [pull_request] 9 | 10 | permissions: 11 | contents: read 12 | 13 | jobs: 14 | dependency-review: 15 | runs-on: ubuntu-latest 16 | steps: 17 | - name: 'Checkout Repository' 18 | uses: actions/checkout@v3 19 | - name: 'Dependency Review' 20 | uses: actions/dependency-review-action@v3 21 | -------------------------------------------------------------------------------- /.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 | .yarn/install-state.gz 8 | 9 | # testing 10 | /coverage 11 | 12 | # next.js 13 | /.next/ 14 | /out/ 15 | 16 | # production 17 | /build 18 | 19 | # misc 20 | .DS_Store 21 | *.pem 22 | 23 | # debug 24 | npm-debug.log* 25 | yarn-debug.log* 26 | yarn-error.log* 27 | 28 | # local env files 29 | .env*.local 30 | 31 | # vercel 32 | .vercel 33 | 34 | # typescript 35 | *.tsbuildinfo 36 | next-env.d.ts 37 | 38 | .vscode/* 39 | !.vscode/settings.json 40 | !.vscode/tasks.json 41 | !.vscode/launch.json 42 | !.vscode/extensions.json 43 | !.vscode/*.code-snippets 44 | 45 | # Local History for Visual Studio Code 46 | .history/ 47 | 48 | # Built Visual Studio Code Extensions 49 | *.vsix 50 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "configurations": [ 3 | { 4 | "type": "chrome", 5 | "name": "http://localhost:3000/pages/api/passage?passage=mat+1", 6 | "request": "launch", 7 | "url": "http://localhost:3000/pages/api/passage?passage=mat+1" 8 | } 9 | ] 10 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ![alkitab-api](https://socialify.git.ci/raselldev/alkitab-api/image?description=1&font=Jost&forks=1&issues=1&language=1&logo=https%3A%2F%2Fraw.githubusercontent.com%2Fraselldev%2Falkitab-api%2Fccc305189347115b96d92f98f3cd9e9cf82c9f27%2Fpublic%2Flogo.svg&name=1&owner=1&pattern=Circuit%20Board&pulls=1&stargazers=1&theme=Auto) 4 | ## Description 5 | 6 | ![NextJS](https://img.shields.io/badge/Next-black?style=for-the-badge&logo=next.js&logoColor=white) ![Vercel](https://img.shields.io/badge/vercel-%23000000.svg?style=for-the-badge&logo=vercel&logoColor=white) ![TypeScript](https://img.shields.io/badge/typescript-%23007ACC.svg?style=for-the-badge&logo=typescript&logoColor=white) 7 | 8 | This is a simple Bible API that provides access to various Bible passages and verses. The API is designed to allow users to retrieve Bible passages based on book and chapter 9 | 10 | ## Last Update (19-11-23) 11 | 12 | - Fetching result of Passage more simple 13 | - Adding UI 14 | - Removing theme "system" 15 | 16 | ## Features 17 | 18 | - [x] Fetch Bible passages based on book and chapter 19 | - [x] Retrieve specific verses within a passage 20 | - [x] Adding UI for sample (NEW UPDATE) 21 | - [ ] Support for multiple Bible versions 22 | 23 | ## Getting Started 24 | 25 | 1. Clone the repository: 26 | 27 | ```sh 28 | git clone https://github.com/raselldev/alkitab-api.git 29 | 30 | 1. Install some package: 31 | 32 | ```sh 33 | npm install 34 | 1. Run: 35 | 36 | ```sh 37 | npm run dev 38 | 39 | ## Endpoints 40 | | API | ENDPOINT | 41 | |--|--| 42 | | Get Book Data | api/book | 43 | | Get Passage | api/passage?passage={passage}&num={chapter} | 44 | 45 | 46 | 47 | ## Contribution 48 | 49 | Interested in enhancing this project? You can actively contribute to it. I am fully receptive to any contributions that can help improve the project. 50 | 51 | ## About the Data 52 | All bible data is from SABDA.ORG 53 | -------------------------------------------------------------------------------- /components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "default", 4 | "rsc": true, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "tailwind.config.js", 8 | "css": "app/globals.css", 9 | "baseColor": "zinc", 10 | "cssVariables": true 11 | }, 12 | "aliases": { 13 | "components": "@/components", 14 | "utils": "@/lib/utils" 15 | } 16 | } -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = {} 3 | 4 | module.exports = nextConfig 5 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "alkitab-api", 3 | "version": "1.0.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint" 10 | }, 11 | "dependencies": { 12 | "@radix-ui/react-dialog": "^1.0.5", 13 | "@radix-ui/react-dropdown-menu": "^2.0.6", 14 | "@radix-ui/react-slot": "^1.0.2", 15 | "class-variance-authority": "^0.7.0", 16 | "classnames": "^2.3.2", 17 | "clsx": "^2.0.0", 18 | "lucide-react": "^0.292.0", 19 | "next": "^14.0.1", 20 | "react": "^18", 21 | "react-dom": "^18", 22 | "tailwind-merge": "^2.0.0", 23 | "tailwindcss-animate": "^1.0.7", 24 | "xml2js": "^0.6.2" 25 | }, 26 | "devDependencies": { 27 | "@types/node": "^20", 28 | "@types/react": "^18.2.34", 29 | "@types/react-dom": "^18", 30 | "@types/xml2js": "^0.4.13", 31 | "autoprefixer": "^10.4.16", 32 | "daisyui": "^4.12.12", 33 | "eslint": "^8", 34 | "eslint-config-next": "14.0.0", 35 | "postcss": "^8.4.31", 36 | "tailwindcss": "^3.3.5", 37 | "typescript": "^5" 38 | }, 39 | "description": "## Description ![NextJS](https://img.shields.io/badge/Next-black?style=for-the-badge&logo=next.js&logoColor=white) ![Vercel](https://img.shields.io/badge/vercel-%23000000.svg?style=for-the-badge&logo=vercel&logoColor=white) ![TypeScript](https://img.shields.io/badge/typescript-%23007ACC.svg?style=for-the-badge&logo=typescript&logoColor=white)", 40 | "main": "next.config.js", 41 | "author": "raselldev", 42 | "license": "ISC" 43 | } -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /public/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/app/api/book/route.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * The above function is an asynchronous function that returns a list of books of the Bible with their 3 | * abbreviations, names, and chapter counts. 4 | * @returns a Response object with the JSON stringified version of the "result" object. 5 | */ 6 | export async function GET() { 7 | let result = { 8 | data: [ 9 | { 10 | id: 1, 11 | abbr: "Kej", 12 | name: "Kejadian", 13 | chapter: 50, 14 | }, 15 | { 16 | id: 2, 17 | abbr: "Kel", 18 | name: "Keluaran", 19 | chapter: 40, 20 | }, 21 | { 22 | id: 3, 23 | abbr: "Ima", 24 | name: "Imamat", 25 | chapter: 27, 26 | }, 27 | { 28 | id: 4, 29 | abbr: "Bil", 30 | name: "Bilangan", 31 | chapter: 36, 32 | }, 33 | { 34 | id: 5, 35 | abbr: "Ula", 36 | name: "Ulangan", 37 | chapter: 34, 38 | }, 39 | { 40 | id: 6, 41 | abbr: "Yos", 42 | name: "Yosua", 43 | chapter: 24, 44 | }, 45 | { 46 | id: 7, 47 | abbr: "Hak", 48 | name: "Hakim-hakim", 49 | chapter: 21, 50 | }, 51 | { 52 | id: 8, 53 | abbr: "Rut", 54 | name: "Rut", 55 | chapter: 4, 56 | }, 57 | { 58 | id: 9, 59 | abbr: "1 Sam", 60 | name: "1 Samuel", 61 | chapter: 31, 62 | }, 63 | { 64 | id: 10, 65 | abbr: "2 Sam", 66 | name: "2 Samuel", 67 | chapter: 24, 68 | }, 69 | { 70 | id: 11, 71 | abbr: "1 Raj", 72 | name: "1 Raja-Raja", 73 | chapter: 22, 74 | }, 75 | { 76 | id: 12, 77 | abbr: "2 Raj", 78 | name: "2 Raja-Raja", 79 | chapter: 25, 80 | }, 81 | { 82 | id: 13, 83 | abbr: "1 Taw", 84 | name: "1 Tawarikh", 85 | chapter: 29, 86 | }, 87 | { 88 | id: 14, 89 | abbr: "2 Taw", 90 | name: "2 Tawarikh", 91 | chapter: 36, 92 | }, 93 | { 94 | id: 15, 95 | abbr: "Ezr", 96 | name: "Ezra", 97 | chapter: 10, 98 | }, 99 | { 100 | id: 16, 101 | abbr: "Neh", 102 | name: "Nehemia", 103 | chapter: 13, 104 | }, 105 | { 106 | id: 17, 107 | abbr: "Est", 108 | name: "Ester", 109 | chapter: 10, 110 | }, 111 | { 112 | id: 18, 113 | abbr: "Ayb", 114 | name: "Ayub", 115 | chapter: 42, 116 | }, 117 | { 118 | id: 19, 119 | abbr: "Maz", 120 | name: "Mazmur", 121 | chapter: 150, 122 | }, 123 | { 124 | id: 20, 125 | abbr: "Ams", 126 | name: "Amsal", 127 | chapter: 31, 128 | }, 129 | { 130 | id: 21, 131 | abbr: "Pkh", 132 | name: "Pengkhotbah", 133 | chapter: 12, 134 | }, 135 | { 136 | id: 22, 137 | abbr: "Kid", 138 | name: "Kidung Agung", 139 | chapter: 8, 140 | }, 141 | { 142 | id: 23, 143 | abbr: "Yes", 144 | name: "Yesaya", 145 | chapter: 66, 146 | }, 147 | { 148 | id: 24, 149 | abbr: "Yer", 150 | name: "Yeremia", 151 | chapter: 52, 152 | }, 153 | { 154 | id: 25, 155 | abbr: "Rat", 156 | name: "Ratapan", 157 | chapter: 5, 158 | }, 159 | { 160 | id: 26, 161 | abbr: "Yeh", 162 | name: "Yehezkiel", 163 | chapter: 48, 164 | }, 165 | { 166 | id: 27, 167 | abbr: "Dan", 168 | name: "Daniel", 169 | chapter: 12, 170 | }, 171 | { 172 | id: 28, 173 | abbr: "Hos", 174 | name: "Hosea", 175 | chapter: 14, 176 | }, 177 | { 178 | id: 29, 179 | abbr: "Yoe", 180 | name: "Yoel", 181 | chapter: 3, 182 | }, 183 | { 184 | id: 30, 185 | abbr: "Amo", 186 | name: "Amos", 187 | chapter: 9, 188 | }, 189 | { 190 | id: 31, 191 | abbr: "Oba", 192 | name: "Obaja", 193 | chapter: 1, 194 | }, 195 | { 196 | id: 32, 197 | abbr: "Yun", 198 | name: "Yunus", 199 | chapter: 4, 200 | }, 201 | { 202 | id: 33, 203 | abbr: "Mik", 204 | name: "Mikha", 205 | chapter: 7, 206 | }, 207 | { 208 | id: 34, 209 | abbr: "Nah", 210 | name: "Nahum", 211 | chapter: 3, 212 | }, 213 | { 214 | id: 35, 215 | abbr: "Hab", 216 | name: "Habakuk", 217 | chapter: 3, 218 | }, 219 | { 220 | id: 36, 221 | abbr: "Zef", 222 | name: "Zefanya", 223 | chapter: 3, 224 | }, 225 | { 226 | id: 37, 227 | abbr: "Hag", 228 | name: "Hagai", 229 | chapter: 2, 230 | }, 231 | { 232 | id: 38, 233 | abbr: "Zak", 234 | name: "Zakharia", 235 | chapter: 14, 236 | }, 237 | { 238 | id: 39, 239 | abbr: "Mal", 240 | name: "Maleakhi", 241 | chapter: 4, 242 | }, 243 | { 244 | id: 40, 245 | abbr: "Mat", 246 | name: "Matius", 247 | chapter: 28, 248 | }, 249 | { 250 | id: 41, 251 | abbr: "Mar", 252 | name: "Markus", 253 | chapter: 16, 254 | }, 255 | { 256 | id: 42, 257 | abbr: "Luk", 258 | name: "Lukas", 259 | chapter: 24, 260 | }, 261 | { 262 | id: 43, 263 | abbr: "Yoh", 264 | name: "Yohanes", 265 | chapter: 21, 266 | }, 267 | { 268 | id: 44, 269 | abbr: "Kis", 270 | name: "Kisah Para Rasul", 271 | chapter: 28, 272 | }, 273 | { 274 | id: 45, 275 | abbr: "Rom", 276 | name: "Roma", 277 | chapter: 16, 278 | }, 279 | { 280 | id: 46, 281 | abbr: "1 Kor", 282 | name: "1 Korintus", 283 | chapter: 16, 284 | }, 285 | { 286 | id: 47, 287 | abbr: "2 Kor", 288 | name: "2 Korintus", 289 | chapter: 13, 290 | }, 291 | { 292 | id: 48, 293 | abbr: "Gal", 294 | name: "Galatia", 295 | chapter: 6, 296 | }, 297 | { 298 | id: 49, 299 | abbr: "Efe", 300 | name: "Efesus", 301 | chapter: 6, 302 | }, 303 | { 304 | id: 50, 305 | abbr: "Flp", 306 | name: "Filipi", 307 | chapter: 4, 308 | }, 309 | { 310 | id: 51, 311 | abbr: "Kol", 312 | name: "Kolose", 313 | chapter: 4, 314 | }, 315 | { 316 | id: 52, 317 | abbr: "1 Tes", 318 | name: "1 Tesalonika", 319 | chapter: 5, 320 | }, 321 | { 322 | id: 53, 323 | abbr: "2 Tes", 324 | name: "2 Tesalonika", 325 | chapter: 3, 326 | }, 327 | { 328 | id: 54, 329 | abbr: "1 Tim", 330 | name: "1 Timotius", 331 | chapter: 6, 332 | }, 333 | { 334 | id: 55, 335 | abbr: "2 Tim", 336 | name: "2 Timotius", 337 | chapter: 4, 338 | }, 339 | { 340 | id: 56, 341 | abbr: "Tit", 342 | name: "Titus", 343 | chapter: 3, 344 | }, 345 | { 346 | id: 57, 347 | abbr: "Flm", 348 | name: "Filemon", 349 | chapter: 1, 350 | }, 351 | { 352 | id: 58, 353 | abbr: "Ibr", 354 | name: "Ibrani", 355 | chapter: 13, 356 | }, 357 | { 358 | id: 59, 359 | abbr: "Yak", 360 | name: "Yakobus", 361 | chapter: 5, 362 | }, 363 | { 364 | id: 60, 365 | abbr: "1 Pet", 366 | name: "1 Petrus", 367 | chapter: 5, 368 | }, 369 | { 370 | id: 61, 371 | abbr: "2 Pet", 372 | name: "2 Petrus", 373 | chapter: 3, 374 | }, 375 | { 376 | id: 62, 377 | abbr: "1 Yoh", 378 | name: "1 Yohanes", 379 | chapter: 5, 380 | }, 381 | { 382 | id: 63, 383 | abbr: "2 Yoh", 384 | name: "2 Yohanes", 385 | chapter: 1, 386 | }, 387 | { 388 | id: 64, 389 | abbr: "3 Yoh", 390 | name: "3 Yohanes", 391 | chapter: 1, 392 | }, 393 | { 394 | id: 65, 395 | abbr: "Yud", 396 | name: "Yudas", 397 | chapter: 1, 398 | }, 399 | { 400 | id: 66, 401 | abbr: "Wah", 402 | name: "Wahyu", 403 | chapter: 22, 404 | }, 405 | ], 406 | }; 407 | 408 | return new Response(JSON.stringify(result)); 409 | } 410 | -------------------------------------------------------------------------------- /src/app/api/passage/route.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This TypeScript function fetches Bible passages from a specific API, parses the response, and 3 | * modifies the structure of the data for efficiency before returning it as a JSON response. 4 | * @param {NextRequest} request - The `request` parameter is an object that contains information about 5 | * the incoming HTTP request. It includes properties such as `nextUrl` which represents the URL of the 6 | * request, `headers` which contains the headers of the request, and `method` which represents the HTTP 7 | * method used for the request (in 8 | * @returns The code is returning a JSON response containing the result of parsing an XML response from 9 | * the specified URL. The XML data is fetched from the URL specified in the `url` variable, and then 10 | * parsed using the `xml2js` library. The parsed result is then modified to have a more efficient 11 | * structure, and finally converted to a JSON string before being returned as the response. 12 | */ 13 | import { NextRequest } from "next/server"; 14 | import { parseString } from "xml2js"; 15 | 16 | export async function GET(request: NextRequest) { 17 | let result: any = {}; 18 | const passage = request.nextUrl.searchParams.get("passage"); 19 | const chapter = request.nextUrl.searchParams.get("num"); 20 | const url = ` http://alkitab.sabda.org/api/passage.php?passage=${passage}${"+"}${chapter}`; 21 | 22 | try { 23 | const response = await fetch(url); 24 | const data = await response.text(); 25 | parseString(data, (error, _result) => { 26 | if (!error) { 27 | result = _result; 28 | } 29 | }); 30 | } catch (error) { 31 | console.log("error", error); 32 | } 33 | 34 | if (result && result.bible && result.bible.title && result.bible.title.length === 1) { 35 | result.bible.title = result.bible.title[0]; 36 | } 37 | 38 | // Modify the structure to make it more efficient 39 | if (result && result.bible && result.bible.book && Array.isArray(result.bible.book)) { 40 | const books = result.bible.book.map((book: any) => { 41 | return { 42 | name: book["$"].name, 43 | book_id: book.book_id[0], 44 | title: book.title[0], 45 | chapter: { 46 | chap: book.chapter[0].chap[0], 47 | verses: book.chapter[0].verses[0].verse.map((verse: any) => { 48 | return { 49 | number: verse.number[0], 50 | title: verse.title ? verse.title[0] : undefined, 51 | text: verse.text[0] 52 | }; 53 | }) 54 | } 55 | }; 56 | }); 57 | 58 | // If there is only one book, set it directly without an array 59 | result.bible.book = books.length === 1 ? books[0] : books; 60 | } 61 | 62 | const jsonString = JSON.stringify(result, null, 2); 63 | return new Response(jsonString); 64 | } 65 | -------------------------------------------------------------------------------- /src/app/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; -------------------------------------------------------------------------------- /src/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import type { Metadata } from 'next' 2 | import { Roboto } from "next/font/google" 3 | import classNames from 'classnames' 4 | import './globals.css' 5 | import { Suspense } from 'react' 6 | 7 | 8 | export const metadata: Metadata = { 9 | title: 'Alkitab API', 10 | icons: { 11 | shortcut: "/logo.svg", 12 | }, 13 | description: 'This is a simple Bible API that provides access to various Bible passages and verses.', 14 | } 15 | 16 | export default function RootLayout({ 17 | children, 18 | }: { 19 | children: React.ReactNode 20 | }) { 21 | const cn = classNames 22 | return ( 23 | 24 | 28 | 29 | {children} 30 | 31 | 32 | 33 | 34 | ) 35 | } -------------------------------------------------------------------------------- /src/app/page.module.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raselldev/alkitab-api/367a1691959b003dc1290a0e75b5835417a9006b/src/app/page.module.css -------------------------------------------------------------------------------- /src/app/page.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import Navbar from '@/components/Navbar' 4 | import Book from '../components/Book' 5 | 6 | export default function Home() { 7 | 8 | return ( 9 |
10 | 11 | 12 |
13 | ) 14 | } 15 | -------------------------------------------------------------------------------- /src/app/passage/page.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import Navbar from "@/components/Navbar" 4 | import usePassage from "@/hooks/usePassage" 5 | import { ChevronLeft, ChevronRight } from "lucide-react" 6 | import Link from "next/link" 7 | import { useSearchParams } from "next/navigation" 8 | 9 | export default function Home() { 10 | const router = useSearchParams() 11 | const abbr = router.get("book") as string 12 | const num = parseInt(router.get("number") as string) 13 | 14 | const { loading, data } = usePassage(abbr, num.toString()) 15 | if (loading) return 16 | if (!data || !data.bible) return

Data is undefined or null.

17 | 18 | return ( 19 | <> 20 | 21 |

28 | {data.bible.book.name} 29 |

30 |

35 | {data.bible.book.title} 36 |

37 | {data.bible.book.chapter.verses.map((v) => ( 38 |

39 | {v.number}. {v.text} 40 |

41 | ))} 42 |
43 | {num > 1 ? ( 44 | <> 45 | 48 | 51 | 52 | 53 | 56 | 59 | 60 | 61 | ) : ( 62 | 65 | 68 | 69 | )} 70 |
71 | 72 | ) 73 | } 74 | -------------------------------------------------------------------------------- /src/components/Book.tsx: -------------------------------------------------------------------------------- 1 | import useBook, { BookType } from '@/hooks/useBook'; 2 | import { useState } from 'react'; 3 | import Link from 'next/link'; 4 | 5 | export default function Book() { 6 | const { loading, data } = useBook(); 7 | const [selectedBook, setSelectedBook] = useState(null); // Store the selected book object 8 | 9 | const openModal = (book: BookType) => { 10 | setSelectedBook(book); // Set the selected book 11 | const modal = document.getElementById('bookModal') as HTMLDialogElement; 12 | if (modal) { 13 | modal.showModal(); 14 | } 15 | }; 16 | 17 | if (loading) return 18 | if (!data) return

Please Try Again

19 | 20 | return ( 21 |
22 | {data.map((book: BookType) => ( 23 |
24 | 28 |
29 | ))} 30 | 31 | {/* Modal */} 32 | 33 |
34 |

{selectedBook ? selectedBook.name : "No book selected"}

35 | 36 |
37 | {selectedBook?.chapter && Array.from({ length: selectedBook.chapter }, (_, index) => ( 38 | 42 | 45 | 46 | ))} 47 |
48 |

Press ESC key or click outside to close

49 |
50 | 51 |
52 | 53 |
54 |
55 |
56 | ); 57 | } 58 | -------------------------------------------------------------------------------- /src/components/Navbar.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import Image from 'next/image' 4 | import Logo from '../../public/logo.svg' 5 | 6 | export default function Navbar() { 7 | return ( 8 | 18 | ) 19 | } -------------------------------------------------------------------------------- /src/hooks/useBook.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * The `useBook` function is a custom React hook that fetches book data from an API and returns the 3 | * loading state and book data. 4 | * @returns The `useBook` function returns an object with two properties: `loading` and `data`. 5 | */ 6 | import { useEffect, useState } from "react"; 7 | 8 | export default function useBook() { 9 | const [data, setData] = useState() 10 | const [loading, setLoading] = useState(true) 11 | 12 | useEffect(() => { 13 | fetch('/api/book') 14 | .then((res) => res.json()) 15 | .then((data: Data) => { 16 | setData(data.data) 17 | setLoading(false) 18 | }) 19 | }, []) 20 | 21 | return { loading, data } 22 | } 23 | 24 | export type BookType = { 25 | id: number; 26 | abbr: string; 27 | name: string; 28 | chapter: number; 29 | }; 30 | 31 | type Data = { 32 | data: BookType[]; 33 | }; -------------------------------------------------------------------------------- /src/hooks/useModal.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * The useModal function is a custom hook that manages the state of a modal component in a TypeScript 3 | * React application. 4 | * @returns The `useModal` custom hook is returning an object with two properties: `isOpen` and 5 | * `toggle`. 6 | */ 7 | import { useState } from "react"; 8 | 9 | export default function useModal() { 10 | const [isOpen, setisOpen] = useState(false); 11 | 12 | const toggle = () => { 13 | setisOpen(!isOpen); 14 | }; 15 | 16 | return { 17 | isOpen, 18 | toggle 19 | }; 20 | } 21 | -------------------------------------------------------------------------------- /src/hooks/usePassage.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react"; 2 | 3 | type Passage = { 4 | bible: Bible; 5 | }; 6 | 7 | type Bible = { 8 | title: string; 9 | book: Book; 10 | }; 11 | 12 | type Book = { 13 | name: string; 14 | book_id: string; 15 | title: string; 16 | chapter: Chapter; 17 | }; 18 | 19 | type Chapter = { 20 | chap: string; 21 | verses: Verse[]; 22 | }; 23 | 24 | type Verse = { 25 | number: string; 26 | title?: string; 27 | text: string; 28 | }; 29 | 30 | export default function usePassage(abbreviation: string, number: string) { 31 | const [data, setData] = useState(null); 32 | const [loading, setLoading] = useState(true); 33 | 34 | useEffect(() => { 35 | const fetchData = async () => { 36 | try { 37 | const response = await fetch(`api/passage?passage=${abbreviation}&num=${number}`); 38 | if (!response.ok) { 39 | throw new Error("Failed to fetch data"); 40 | } 41 | 42 | const result: Passage = await response.json(); 43 | setData(result); 44 | setLoading(false); 45 | } catch (error) { 46 | console.error("Error fetching data:", error); 47 | setLoading(false); 48 | } 49 | }; 50 | 51 | fetchData(); 52 | }, [abbreviation, number]); 53 | 54 | return { loading, data }; 55 | } 56 | -------------------------------------------------------------------------------- /src/lib/utils.ts: -------------------------------------------------------------------------------- 1 | import { type ClassValue, clsx } from "clsx" 2 | import { twMerge } from "tailwind-merge" 3 | 4 | export function cn(...inputs: ClassValue[]) { 5 | return twMerge(clsx(inputs)) 6 | } 7 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | module.exports = { 3 | daisyui: { 4 | themes: ["night"] 5 | }, 6 | content: [ 7 | './pages/**/*.{ts,tsx}', 8 | './components/**/*.{ts,tsx}', 9 | './app/**/*.{ts,tsx}', 10 | './src/**/*.{ts,tsx}', 11 | ], 12 | plugins: [require('daisyui')], 13 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": [ 5 | "dom", 6 | "dom.iterable", 7 | "esnext" 8 | ], 9 | "allowJs": true, 10 | "skipLibCheck": true, 11 | "strict": true, 12 | "noEmit": true, 13 | "esModuleInterop": true, 14 | "module": "esnext", 15 | "moduleResolution": "bundler", 16 | "resolveJsonModule": true, 17 | "isolatedModules": true, 18 | "jsx": "preserve", 19 | "incremental": true, 20 | "plugins": [ 21 | { 22 | "name": "next" 23 | } 24 | ], 25 | "paths": { 26 | "@/*": [ 27 | "./src/*" 28 | ] 29 | } 30 | }, 31 | "include": [ 32 | "next-env.d.ts", 33 | "**/*.ts", 34 | "**/*.tsx", 35 | ".next/types/**/*.ts" 36 | ], 37 | "exclude": [ 38 | "node_modules" 39 | ] 40 | } --------------------------------------------------------------------------------