Add comprehensive page management system to admin dashboard
Features added: - Database schema for pages and media files with content types (Rich Text, HTML, Markdown) - Admin API routes for full page CRUD operations - Image upload functionality with file management - Rich text editor using TinyMCE with image insertion - Admin interface for creating/editing pages with SEO options - Dynamic navigation and footer integration - Public page display routes with proper SEO metadata - Support for featured images and content excerpts Admin features: - Create/edit/delete pages with rich content editor - Upload and manage images through media library - Configure pages to appear in navigation or footer - Set page status (Draft, Published, Archived) - SEO title and description management - Real-time preview of content changes 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -82,6 +82,14 @@ interface BibleChapter {
|
||||
verses: BibleVerse[]
|
||||
}
|
||||
|
||||
interface BibleVersion {
|
||||
id: string
|
||||
name: string
|
||||
abbreviation: string
|
||||
language: string
|
||||
isDefault?: boolean
|
||||
}
|
||||
|
||||
interface BibleBook {
|
||||
id: string
|
||||
versionId: string
|
||||
@@ -122,10 +130,13 @@ export default function BibleReaderNew() {
|
||||
|
||||
// Core state
|
||||
const [books, setBooks] = useState<BibleBook[]>([])
|
||||
const [versions, setVersions] = useState<BibleVersion[]>([])
|
||||
const [selectedVersion, setSelectedVersion] = useState<string>('')
|
||||
const [selectedBook, setSelectedBook] = useState<string>('')
|
||||
const [selectedChapter, setSelectedChapter] = useState<number>(1)
|
||||
const [verses, setVerses] = useState<BibleVerse[]>([])
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [versionsLoading, setVersionsLoading] = useState(true)
|
||||
|
||||
// UI state
|
||||
const [settingsOpen, setSettingsOpen] = useState(false)
|
||||
@@ -172,13 +183,29 @@ export default function BibleReaderNew() {
|
||||
console.error('Failed to parse preferences:', e)
|
||||
}
|
||||
}
|
||||
}, [])
|
||||
|
||||
// Load saved version preference
|
||||
const savedVersion = localStorage.getItem('selectedBibleVersion')
|
||||
if (savedVersion && versions.length > 0) {
|
||||
const version = versions.find(v => v.id === savedVersion)
|
||||
if (version) {
|
||||
setSelectedVersion(savedVersion)
|
||||
}
|
||||
}
|
||||
}, [versions])
|
||||
|
||||
// Save preferences to localStorage
|
||||
useEffect(() => {
|
||||
localStorage.setItem('bibleReaderPreferences', JSON.stringify(preferences))
|
||||
}, [preferences])
|
||||
|
||||
// Save selected version to localStorage
|
||||
useEffect(() => {
|
||||
if (selectedVersion) {
|
||||
localStorage.setItem('selectedBibleVersion', selectedVersion)
|
||||
}
|
||||
}, [selectedVersion])
|
||||
|
||||
// Scroll handler for show scroll to top button
|
||||
useEffect(() => {
|
||||
const handleScroll = () => {
|
||||
@@ -189,29 +216,60 @@ export default function BibleReaderNew() {
|
||||
return () => window.removeEventListener('scroll', handleScroll)
|
||||
}, [])
|
||||
|
||||
// Fetch books
|
||||
// Fetch versions based on current locale
|
||||
useEffect(() => {
|
||||
fetch(`/api/bible/books?locale=${locale}`)
|
||||
setVersionsLoading(true)
|
||||
fetch(`/api/bible/versions?language=${locale}`)
|
||||
.then(res => res.json())
|
||||
.then(data => {
|
||||
setBooks(data.books || [])
|
||||
if (data.books && data.books.length > 0) {
|
||||
setSelectedBook(data.books[0].id)
|
||||
if (data.success && data.versions) {
|
||||
setVersions(data.versions)
|
||||
// Select default version or first available
|
||||
const defaultVersion = data.versions.find((v: BibleVersion) => v.isDefault) || data.versions[0]
|
||||
if (defaultVersion) {
|
||||
setSelectedVersion(defaultVersion.id)
|
||||
}
|
||||
}
|
||||
setLoading(false)
|
||||
setVersionsLoading(false)
|
||||
})
|
||||
.catch(err => {
|
||||
console.error('Error fetching books:', err)
|
||||
setLoading(false)
|
||||
console.error('Error fetching versions:', err)
|
||||
setVersionsLoading(false)
|
||||
})
|
||||
}, [locale])
|
||||
|
||||
// Fetch books when version changes
|
||||
useEffect(() => {
|
||||
if (selectedVersion) {
|
||||
setLoading(true)
|
||||
fetch(`/api/bible/books?locale=${locale}&version=${selectedVersion}`)
|
||||
.then(res => res.json())
|
||||
.then(data => {
|
||||
setBooks(data.books || [])
|
||||
if (data.books && data.books.length > 0) {
|
||||
setSelectedBook(data.books[0].id)
|
||||
}
|
||||
setLoading(false)
|
||||
})
|
||||
.catch(err => {
|
||||
console.error('Error fetching books:', err)
|
||||
setLoading(false)
|
||||
})
|
||||
}
|
||||
}, [locale, selectedVersion])
|
||||
|
||||
// Handle URL parameters
|
||||
useEffect(() => {
|
||||
if (books.length > 0) {
|
||||
if (books.length > 0 && versions.length > 0) {
|
||||
const bookParam = searchParams.get('book')
|
||||
const chapterParam = searchParams.get('chapter')
|
||||
const verseParam = searchParams.get('verse')
|
||||
const versionParam = searchParams.get('version')
|
||||
|
||||
// Handle version parameter
|
||||
if (versionParam && versions.find(v => v.id === versionParam)) {
|
||||
setSelectedVersion(versionParam)
|
||||
}
|
||||
|
||||
if (bookParam) {
|
||||
const book = books.find(b => b.id === bookParam) || books.find(b => b.bookKey === bookParam)
|
||||
@@ -236,7 +294,7 @@ export default function BibleReaderNew() {
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [books, searchParams])
|
||||
}, [books, versions, searchParams])
|
||||
|
||||
// Fetch verses when book/chapter changes
|
||||
useEffect(() => {
|
||||
@@ -350,10 +408,13 @@ export default function BibleReaderNew() {
|
||||
const currentBook = books.find(book => book.id === selectedBook)
|
||||
const maxChapters = currentBook?.chapters?.length || 1
|
||||
|
||||
const updateUrl = (bookId: string, chapter: number) => {
|
||||
const updateUrl = (bookId: string, chapter: number, version?: string) => {
|
||||
const url = new URL(window.location.href)
|
||||
url.searchParams.set('book', bookId)
|
||||
url.searchParams.set('chapter', chapter.toString())
|
||||
if (version) {
|
||||
url.searchParams.set('version', version)
|
||||
}
|
||||
window.history.replaceState({}, '', url.toString())
|
||||
}
|
||||
|
||||
@@ -368,7 +429,7 @@ export default function BibleReaderNew() {
|
||||
if (selectedChapter > 1) {
|
||||
const newChapter = selectedChapter - 1
|
||||
setSelectedChapter(newChapter)
|
||||
updateUrl(selectedBook, newChapter)
|
||||
updateUrl(selectedBook, newChapter, selectedVersion)
|
||||
} else {
|
||||
const currentBookIndex = books.findIndex(book => book.id === selectedBook)
|
||||
if (currentBookIndex > 0) {
|
||||
@@ -376,7 +437,7 @@ export default function BibleReaderNew() {
|
||||
const lastChapter = previousBook.chapters?.length || 1
|
||||
setSelectedBook(previousBook.id)
|
||||
setSelectedChapter(lastChapter)
|
||||
updateUrl(previousBook.id, lastChapter)
|
||||
updateUrl(previousBook.id, lastChapter, selectedVersion)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -385,14 +446,14 @@ export default function BibleReaderNew() {
|
||||
if (selectedChapter < maxChapters) {
|
||||
const newChapter = selectedChapter + 1
|
||||
setSelectedChapter(newChapter)
|
||||
updateUrl(selectedBook, newChapter)
|
||||
updateUrl(selectedBook, newChapter, selectedVersion)
|
||||
} else {
|
||||
const currentBookIndex = books.findIndex(book => book.id === selectedBook)
|
||||
if (currentBookIndex < books.length - 1) {
|
||||
const nextBook = books[currentBookIndex + 1]
|
||||
setSelectedBook(nextBook.id)
|
||||
setSelectedChapter(1)
|
||||
updateUrl(nextBook.id, 1)
|
||||
updateUrl(nextBook.id, 1, selectedVersion)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -493,7 +554,7 @@ export default function BibleReaderNew() {
|
||||
}
|
||||
|
||||
const handleShare = () => {
|
||||
const url = `${window.location.origin}/${locale}/bible?book=${selectedBook}&chapter=${selectedChapter}`
|
||||
const url = `${window.location.origin}/${locale}/bible?book=${selectedBook}&chapter=${selectedChapter}&version=${selectedVersion}`
|
||||
navigator.clipboard.writeText(url).then(() => {
|
||||
setCopyFeedback({
|
||||
open: true,
|
||||
@@ -614,6 +675,33 @@ export default function BibleReaderNew() {
|
||||
}}
|
||||
>
|
||||
<Box sx={{ display: 'flex', gap: 2, flexWrap: 'wrap', alignItems: 'center', justifyContent: 'center' }}>
|
||||
{/* Version Selection */}
|
||||
<Box sx={{ flex: { xs: '1 1 100%', sm: '1 1 auto' }, minWidth: { sm: 180, md: 200 } }}>
|
||||
<FormControl fullWidth size="small">
|
||||
<InputLabel>{t('version')}</InputLabel>
|
||||
<Select
|
||||
value={selectedVersion}
|
||||
label={t('version')}
|
||||
onChange={(e) => {
|
||||
setSelectedVersion(e.target.value)
|
||||
// Reset to first book when version changes
|
||||
if (books.length > 0) {
|
||||
setSelectedBook(books[0].id)
|
||||
setSelectedChapter(1)
|
||||
updateUrl(books[0].id, 1, e.target.value)
|
||||
}
|
||||
}}
|
||||
disabled={versionsLoading}
|
||||
>
|
||||
{versions.map((version) => (
|
||||
<MenuItem key={version.id} value={version.id}>
|
||||
{version.abbreviation} - {version.name}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
</Box>
|
||||
|
||||
{/* Books Selection */}
|
||||
<Box sx={{ flex: { xs: '1 1 100%', sm: '1 1 auto' }, minWidth: { sm: 200, md: 250 } }}>
|
||||
<FormControl fullWidth size="small">
|
||||
@@ -624,7 +712,7 @@ export default function BibleReaderNew() {
|
||||
onChange={(e) => {
|
||||
setSelectedBook(e.target.value)
|
||||
setSelectedChapter(1)
|
||||
updateUrl(e.target.value, 1)
|
||||
updateUrl(e.target.value, 1, selectedVersion)
|
||||
}}
|
||||
>
|
||||
{books.map((book) => (
|
||||
@@ -646,7 +734,7 @@ export default function BibleReaderNew() {
|
||||
onChange={(e) => {
|
||||
const newChapter = Number(e.target.value)
|
||||
setSelectedChapter(newChapter)
|
||||
updateUrl(selectedBook, newChapter)
|
||||
updateUrl(selectedBook, newChapter, selectedVersion)
|
||||
}}
|
||||
MenuProps={{
|
||||
PaperProps: {
|
||||
|
||||
Reference in New Issue
Block a user