From 84f1dfaf23c0eeccd9f3cb51b948ce556d00ad1e Mon Sep 17 00:00:00 2001 From: Ryderjj89 Date: Sat, 13 Sep 2025 12:09:52 -0400 Subject: [PATCH] Add complete React frontend with modern design and navigation --- frontend/package.json | 48 ++++++ frontend/postcss.config.js | 6 + frontend/public/index.html | 17 +++ frontend/src/App.tsx | 154 ++++++++++++++++++++ frontend/src/components/BibleReader.tsx | 154 ++++++++++++++++++++ frontend/src/components/BookSelector.tsx | 53 +++++++ frontend/src/components/ChapterSelector.tsx | 105 +++++++++++++ frontend/src/index.css | 37 +++++ frontend/src/index.tsx | 13 ++ frontend/src/services/api.ts | 34 +++++ frontend/tailwind.config.js | 28 ++++ frontend/tsconfig.json | 26 ++++ 12 files changed, 675 insertions(+) create mode 100644 frontend/package.json create mode 100644 frontend/postcss.config.js create mode 100644 frontend/public/index.html create mode 100644 frontend/src/App.tsx create mode 100644 frontend/src/components/BibleReader.tsx create mode 100644 frontend/src/components/BookSelector.tsx create mode 100644 frontend/src/components/ChapterSelector.tsx create mode 100644 frontend/src/index.css create mode 100644 frontend/src/index.tsx create mode 100644 frontend/src/services/api.ts create mode 100644 frontend/tailwind.config.js create mode 100644 frontend/tsconfig.json 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 ( +
+ +

Loading chapter...

+
+ ); + } + + 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" + ] +}