diff --git a/frontend/package.json b/frontend/package.json
new file mode 100644
index 00000000..4ad0cf87
--- /dev/null
+++ b/frontend/package.json
@@ -0,0 +1,48 @@
+{
+ "name": "esv-bible-frontend",
+ "version": "1.0.0",
+ "private": true,
+ "dependencies": {
+ "@types/node": "^16.18.0",
+ "@types/react": "^18.2.0",
+ "@types/react-dom": "^18.2.0",
+ "react": "^18.2.0",
+ "react-dom": "^18.2.0",
+ "react-scripts": "5.0.1",
+ "typescript": "^4.9.0",
+ "web-vitals": "^2.1.0",
+ "axios": "^1.6.0",
+ "react-router-dom": "^6.20.0",
+ "lucide-react": "^0.294.0"
+ },
+ "scripts": {
+ "start": "react-scripts start",
+ "build": "react-scripts build",
+ "test": "react-scripts test",
+ "eject": "react-scripts eject"
+ },
+ "eslintConfig": {
+ "extends": [
+ "react-app",
+ "react-app/jest"
+ ]
+ },
+ "browserslist": {
+ "production": [
+ ">0.2%",
+ "not dead",
+ "not op_mini all"
+ ],
+ "development": [
+ "last 1 chrome version",
+ "last 1 firefox version",
+ "last 1 safari version"
+ ]
+ },
+ "devDependencies": {
+ "tailwindcss": "^3.3.0",
+ "autoprefixer": "^10.4.0",
+ "postcss": "^8.4.0",
+ "@tailwindcss/typography": "^0.5.0"
+ }
+}
diff --git a/frontend/postcss.config.js b/frontend/postcss.config.js
new file mode 100644
index 00000000..33ad091d
--- /dev/null
+++ b/frontend/postcss.config.js
@@ -0,0 +1,6 @@
+module.exports = {
+ plugins: {
+ tailwindcss: {},
+ autoprefixer: {},
+ },
+}
diff --git a/frontend/public/index.html b/frontend/public/index.html
new file mode 100644
index 00000000..ad3d1127
--- /dev/null
+++ b/frontend/public/index.html
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
+
+
+
+ ESV Bible
+
+
+
+
+
+
diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx
new file mode 100644
index 00000000..5e6b603a
--- /dev/null
+++ b/frontend/src/App.tsx
@@ -0,0 +1,154 @@
+import React, { useState, useEffect } from 'react';
+import { Book, ChevronRight, Moon, Sun } from 'lucide-react';
+import BookSelector from './components/BookSelector';
+import ChapterSelector from './components/ChapterSelector';
+import BibleReader from './components/BibleReader';
+import { getBooks } from './services/api';
+
+interface BookData {
+ books: string[];
+}
+
+function App() {
+ const [books, setBooks] = useState([]);
+ const [selectedBook, setSelectedBook] = useState('');
+ const [selectedChapter, setSelectedChapter] = useState('');
+ const [loading, setLoading] = useState(true);
+ const [darkMode, setDarkMode] = useState(false);
+
+ useEffect(() => {
+ loadBooks();
+ }, []);
+
+ useEffect(() => {
+ if (darkMode) {
+ document.documentElement.classList.add('dark');
+ } else {
+ document.documentElement.classList.remove('dark');
+ }
+ }, [darkMode]);
+
+ const loadBooks = async () => {
+ try {
+ const data: BookData = await getBooks();
+ setBooks(data.books);
+ } catch (error) {
+ console.error('Failed to load books:', error);
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ const handleBookSelect = (book: string) => {
+ setSelectedBook(book);
+ setSelectedChapter('');
+ };
+
+ const handleChapterSelect = (chapter: string) => {
+ setSelectedChapter(chapter);
+ };
+
+ const handleBackToBooks = () => {
+ setSelectedBook('');
+ setSelectedChapter('');
+ };
+
+ const handleBackToChapters = () => {
+ setSelectedChapter('');
+ };
+
+ if (loading) {
+ return (
+
+
+
+
Loading ESV Bible...
+
+
+ );
+ }
+
+ return (
+
+ {/* Header */}
+
+
+
+
+
+
+ ESV Bible
+
+
+
+ {/* Navigation Breadcrumb */}
+
+ {selectedBook && (
+ <>
+
+
+
+ {selectedBook}
+
+ {selectedChapter && (
+ <>
+
+
+
+
+ Chapter {selectedChapter}
+
+ >
+ )}
+ >
+ )}
+
+
+ {/* Dark Mode Toggle */}
+
+
+
+
+
+ {/* Main Content */}
+
+ {!selectedBook ? (
+
+ ) : !selectedChapter ? (
+
+ ) : (
+
+ )}
+
+
+ );
+}
+
+export default App;
diff --git a/frontend/src/components/BibleReader.tsx b/frontend/src/components/BibleReader.tsx
new file mode 100644
index 00000000..b7ece84c
--- /dev/null
+++ b/frontend/src/components/BibleReader.tsx
@@ -0,0 +1,154 @@
+import React, { useState, useEffect } from 'react';
+import { ArrowLeft, BookOpen } from 'lucide-react';
+import { getChapter } from '../services/api';
+
+interface BibleReaderProps {
+ book: string;
+ chapter: string;
+ onBack: () => void;
+}
+
+const BibleReader: React.FC = ({ book, chapter, onBack }) => {
+ const [content, setContent] = useState('');
+ const [loading, setLoading] = useState(true);
+ const [fontSize, setFontSize] = useState<'small' | 'medium' | 'large'>('medium');
+
+ useEffect(() => {
+ loadChapter();
+ }, [book, chapter]);
+
+ const loadChapter = async () => {
+ try {
+ setLoading(true);
+ const chapterContent = await getChapter(book, chapter);
+ setContent(chapterContent);
+ } catch (error) {
+ console.error('Failed to load chapter:', error);
+ setContent('Error loading chapter. Please try again.');
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ const getFontSizeClass = () => {
+ switch (fontSize) {
+ case 'small':
+ return 'text-base';
+ case 'large':
+ return 'text-xl';
+ default:
+ return 'text-lg';
+ }
+ };
+
+ const parseBibleText = (text: string) => {
+ const lines = text.split('\n');
+ const verses: JSX.Element[] = [];
+
+ for (let i = 0; i < lines.length; i++) {
+ const line = lines[i].trim();
+
+ // Skip empty lines
+ if (!line) continue;
+
+ // Check if line starts with verse number (e.g., "1 ", "2 ", etc.)
+ const verseMatch = line.match(/^(\d+)\s+(.+)$/);
+ if (verseMatch) {
+ const verseNumber = verseMatch[1];
+ const verseText = verseMatch[2];
+
+ verses.push(
+
+ {verseNumber}
+ {verseText}
+
+ );
+ } else if (line.startsWith('#')) {
+ // Chapter header
+ const headerText = line.replace(/^#+\s*/, '');
+ verses.push(
+
+ {headerText}
+
+ );
+ } else {
+ // Regular text (continuation of previous verse or other content)
+ verses.push(
+
+ {line}
+
+ );
+ }
+ }
+
+ return verses;
+ };
+
+ if (loading) {
+ return (
+
+ );
+ }
+
+ return (
+
+ {/* Header */}
+
+
+
+ {/* Font Size Controls */}
+
+
Font Size:
+
+ {(['small', 'medium', 'large'] as const).map((size) => (
+
+ ))}
+
+
+
+
+ {/* Chapter Title */}
+
+
+ {book} {chapter}
+
+
+
+ {/* Bible Content */}
+
+ {parseBibleText(content)}
+
+
+ {/* Footer Navigation */}
+
+
+
+
+ );
+};
+
+export default BibleReader;
diff --git a/frontend/src/components/BookSelector.tsx b/frontend/src/components/BookSelector.tsx
new file mode 100644
index 00000000..5952e233
--- /dev/null
+++ b/frontend/src/components/BookSelector.tsx
@@ -0,0 +1,53 @@
+import React from 'react';
+import { BookOpen } from 'lucide-react';
+
+interface BookSelectorProps {
+ books: string[];
+ onBookSelect: (book: string) => void;
+}
+
+const BookSelector: React.FC = ({ books, onBookSelect }) => {
+ // Group books by testament
+ const oldTestament = books.slice(0, 39); // First 39 books
+ const newTestament = books.slice(39); // Remaining books
+
+ const BookGroup: React.FC<{ title: string; books: string[] }> = ({ title, books }) => (
+
+
+ {title}
+
+
+ {books.map((book) => (
+
+ ))}
+
+
+ );
+
+ return (
+
+
+
+ ESV Bible
+
+
+ Select a book to begin reading
+
+
+
+
+
+
+ );
+};
+
+export default BookSelector;
diff --git a/frontend/src/components/ChapterSelector.tsx b/frontend/src/components/ChapterSelector.tsx
new file mode 100644
index 00000000..18d2af81
--- /dev/null
+++ b/frontend/src/components/ChapterSelector.tsx
@@ -0,0 +1,105 @@
+import React, { useState, useEffect } from 'react';
+import { ArrowLeft, FileText } from 'lucide-react';
+import { getBook } from '../services/api';
+
+interface ChapterSelectorProps {
+ book: string;
+ onChapterSelect: (chapter: string) => void;
+ onBack: () => void;
+}
+
+const ChapterSelector: React.FC = ({ book, onChapterSelect, onBack }) => {
+ const [chapters, setChapters] = useState([]);
+ const [loading, setLoading] = useState(true);
+
+ useEffect(() => {
+ loadChapters();
+ }, [book]);
+
+ const loadChapters = async () => {
+ try {
+ setLoading(true);
+ const bookContent = await getBook(book);
+
+ // Parse markdown to extract chapter numbers
+ const lines = bookContent.split('\n');
+ const chapterNumbers: string[] = [];
+
+ for (const line of lines) {
+ // Look for chapter headers like "# 1" or "# 1\n"
+ const chapterMatch = line.match(/^#\s+(\d+)/);
+ if (chapterMatch) {
+ chapterNumbers.push(chapterMatch[1]);
+ }
+ }
+
+ setChapters(chapterNumbers);
+ } catch (error) {
+ console.error('Failed to load chapters:', error);
+ // Fallback: generate chapter numbers 1-50 (most books have fewer than 50 chapters)
+ const fallbackChapters = Array.from({ length: 50 }, (_, i) => (i + 1).toString());
+ setChapters(fallbackChapters);
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ if (loading) {
+ return (
+
+
+
Loading chapters...
+
+ );
+ }
+
+ return (
+
+ {/* Header */}
+
+
+ {/* Book Title */}
+
+
+ {book}
+
+
+ Select a chapter to read
+
+
+
+ {/* Chapters Grid */}
+
+ {chapters.map((chapter) => (
+
+ ))}
+
+
+ {/* Chapter Count */}
+
+
+ {chapters.length} chapters available
+
+
+
+ );
+};
+
+export default ChapterSelector;
diff --git a/frontend/src/index.css b/frontend/src/index.css
new file mode 100644
index 00000000..09d27bc6
--- /dev/null
+++ b/frontend/src/index.css
@@ -0,0 +1,37 @@
+@tailwind base;
+@tailwind components;
+@tailwind utilities;
+
+@layer base {
+ html {
+ font-family: 'Georgia', 'Times New Roman', serif;
+ }
+
+ body {
+ @apply bg-gray-50 text-gray-900 dark:bg-gray-900 dark:text-gray-100;
+ font-family: 'Georgia', 'Times New Roman', serif;
+ line-height: 1.6;
+ }
+}
+
+@layer components {
+ .bible-text {
+ @apply text-lg leading-relaxed text-gray-800 dark:text-gray-200;
+ font-family: 'Georgia', 'Times New Roman', serif;
+ }
+
+ .verse-number {
+ @apply text-sm font-semibold text-blue-600 dark:text-blue-400 mr-1;
+ font-family: 'Arial', sans-serif;
+ }
+
+ .book-title {
+ @apply text-2xl font-bold text-center mb-6 text-gray-900 dark:text-gray-100;
+ font-family: 'Georgia', 'Times New Roman', serif;
+ }
+
+ .chapter-title {
+ @apply text-xl font-semibold mb-4 text-gray-700 dark:text-gray-300;
+ font-family: 'Georgia', 'Times New Roman', serif;
+ }
+}
diff --git a/frontend/src/index.tsx b/frontend/src/index.tsx
new file mode 100644
index 00000000..1fd12b70
--- /dev/null
+++ b/frontend/src/index.tsx
@@ -0,0 +1,13 @@
+import React from 'react';
+import ReactDOM from 'react-dom/client';
+import './index.css';
+import App from './App';
+
+const root = ReactDOM.createRoot(
+ document.getElementById('root') as HTMLElement
+);
+root.render(
+
+
+
+);
diff --git a/frontend/src/services/api.ts b/frontend/src/services/api.ts
new file mode 100644
index 00000000..d7c1a1ae
--- /dev/null
+++ b/frontend/src/services/api.ts
@@ -0,0 +1,34 @@
+import axios from 'axios';
+
+const API_BASE_URL = process.env.REACT_APP_API_URL || 'http://localhost:3000';
+
+const api = axios.create({
+ baseURL: API_BASE_URL,
+ headers: {
+ 'Content-Type': 'text/plain',
+ },
+});
+
+export interface BookData {
+ books: string[];
+}
+
+export const getBooks = async (): Promise => {
+ const response = await api.get('/books');
+ return response.data;
+};
+
+export const getBook = async (book: string): Promise => {
+ const response = await api.get(`/books/${book}`);
+ return response.data;
+};
+
+export const getChapter = async (book: string, chapter: string): Promise => {
+ const response = await api.get(`/books/${book}/${chapter}`);
+ return response.data;
+};
+
+export const checkHealth = async (): Promise<{ status: string; message: string }> => {
+ const response = await api.get('/health');
+ return response.data;
+};
diff --git a/frontend/tailwind.config.js b/frontend/tailwind.config.js
new file mode 100644
index 00000000..77882549
--- /dev/null
+++ b/frontend/tailwind.config.js
@@ -0,0 +1,28 @@
+/** @type {import('tailwindcss').Config} */
+module.exports = {
+ content: [
+ "./src/**/*.{js,jsx,ts,tsx}",
+ ],
+ darkMode: 'class',
+ theme: {
+ extend: {
+ fontFamily: {
+ serif: ['Georgia', 'Times New Roman', 'serif'],
+ },
+ typography: {
+ DEFAULT: {
+ css: {
+ color: 'inherit',
+ a: {
+ color: 'inherit',
+ textDecoration: 'none',
+ },
+ },
+ },
+ },
+ },
+ },
+ plugins: [
+ require('@tailwindcss/typography'),
+ ],
+}
diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json
new file mode 100644
index 00000000..6756623d
--- /dev/null
+++ b/frontend/tsconfig.json
@@ -0,0 +1,26 @@
+{
+ "compilerOptions": {
+ "target": "es5",
+ "lib": [
+ "dom",
+ "dom.iterable",
+ "es6"
+ ],
+ "allowJs": true,
+ "skipLibCheck": true,
+ "esModuleInterop": true,
+ "allowSyntheticDefaultImports": true,
+ "strict": true,
+ "forceConsistentCasingInFileNames": true,
+ "noFallthroughCasesInSwitch": true,
+ "module": "esnext",
+ "moduleResolution": "node",
+ "resolveJsonModule": true,
+ "isolatedModules": true,
+ "noEmit": true,
+ "jsx": "react-jsx"
+ },
+ "include": [
+ "src"
+ ]
+}