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:
2025-09-24 07:26:25 +00:00
parent f81886a851
commit 95070e5369
53 changed files with 3628 additions and 206 deletions

View File

@@ -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: {