From 95070e53696a2414cbf66e911d7c328b2e49cb75 Mon Sep 17 00:00:00 2001 From: Andrei Date: Wed, 24 Sep 2025 07:26:25 +0000 Subject: [PATCH] Add comprehensive page management system to admin dashboard MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- app/[locale]/bible/reader.tsx | 126 ++++- app/[locale]/contact/page.tsx | 281 +++++++++++ app/[locale]/layout.tsx | 4 +- app/[locale]/page.tsx | 97 ---- app/[locale]/pages/[slug]/page.tsx | 309 ++++++++++++ app/admin/pages/page.tsx | 357 +++++++++++++ app/api/admin/media/route.ts | 137 +++++ app/api/admin/pages/[id]/route.ts | 174 +++++++ app/api/admin/pages/route.ts | 144 ++++++ app/api/bible/books/route.ts | 8 +- app/api/bible/chapter/route.ts | 14 +- app/api/pages/[slug]/route.ts | 47 ++ app/api/pages/route.ts | 53 ++ components/admin/layout/admin-layout.tsx | 4 +- components/admin/pages/image-upload.tsx | 329 ++++++++++++ components/admin/pages/page-editor.tsx | 471 ++++++++++++++++++ components/layout/footer.tsx | 167 +++++++ components/layout/navigation.tsx | 37 +- components/ui/navigation.tsx | 2 +- lib/admin-auth.ts | 97 ++-- lib/cache/index.ts | 4 +- messages/en.json | 40 ++ messages/ro.json | 60 ++- package-lock.json | 27 + package.json | 2 + prisma/schema.prisma | 67 +++ scripts/convert_bibles_to_json.ts | 423 ++++++++++++++++ scripts/import_json_bibles.py | 353 +++++++++++++ scripts/{ => old}/check-admin.ts | 0 scripts/{ => old}/clean-json-text.js | 0 scripts/{ => old}/cleanup-english-versions.ts | 0 scripts/{ => old}/clone_vector_table.ts | 0 .../{ => old}/convert-fidela-md-to-json.ts | 0 scripts/{ => old}/fetch-english-bible.ts | 0 scripts/{ => old}/import-api-bible.ts | 0 scripts/{ => old}/import-bible.ts | 0 scripts/{ => old}/import-english-json.ts | 0 scripts/{ => old}/import-english-versioned.ts | 0 scripts/{ => old}/import-romanian-bible-md.ts | 0 scripts/{ => old}/import-romanian-bible.ts | 0 .../{ => old}/import-romanian-versioned.ts | 0 scripts/{ => old}/init.sql | 0 .../{ => old}/migrate-to-versioned-schema.ts | 0 scripts/{ => old}/optimize-db.sql | 0 scripts/{ => old}/parse-bsb-md-full.js | 0 scripts/{ => old}/parse-bsb-md-samples.ts | 0 scripts/{ => old}/reset-web-version.ts | 0 scripts/{ => old}/resync-bookkeys-ro.ts | 0 scripts/{ => old}/seed-prayers.ts | 0 scripts/{ => old}/test-prayers.ts | 0 scripts/{ => old}/usfm-to-json.ts | 0 scripts/{ => old}/validate-bsb-md.js | 0 scripts/{ => old}/validate-bsb-md.ts | 0 53 files changed, 3628 insertions(+), 206 deletions(-) create mode 100644 app/[locale]/contact/page.tsx create mode 100644 app/[locale]/pages/[slug]/page.tsx create mode 100644 app/admin/pages/page.tsx create mode 100644 app/api/admin/media/route.ts create mode 100644 app/api/admin/pages/[id]/route.ts create mode 100644 app/api/admin/pages/route.ts create mode 100644 app/api/pages/[slug]/route.ts create mode 100644 app/api/pages/route.ts create mode 100644 components/admin/pages/image-upload.tsx create mode 100644 components/admin/pages/page-editor.tsx create mode 100644 components/layout/footer.tsx create mode 100644 scripts/convert_bibles_to_json.ts create mode 100644 scripts/import_json_bibles.py rename scripts/{ => old}/check-admin.ts (100%) rename scripts/{ => old}/clean-json-text.js (100%) rename scripts/{ => old}/cleanup-english-versions.ts (100%) rename scripts/{ => old}/clone_vector_table.ts (100%) rename scripts/{ => old}/convert-fidela-md-to-json.ts (100%) rename scripts/{ => old}/fetch-english-bible.ts (100%) rename scripts/{ => old}/import-api-bible.ts (100%) rename scripts/{ => old}/import-bible.ts (100%) rename scripts/{ => old}/import-english-json.ts (100%) rename scripts/{ => old}/import-english-versioned.ts (100%) rename scripts/{ => old}/import-romanian-bible-md.ts (100%) rename scripts/{ => old}/import-romanian-bible.ts (100%) rename scripts/{ => old}/import-romanian-versioned.ts (100%) rename scripts/{ => old}/init.sql (100%) rename scripts/{ => old}/migrate-to-versioned-schema.ts (100%) rename scripts/{ => old}/optimize-db.sql (100%) rename scripts/{ => old}/parse-bsb-md-full.js (100%) rename scripts/{ => old}/parse-bsb-md-samples.ts (100%) rename scripts/{ => old}/reset-web-version.ts (100%) rename scripts/{ => old}/resync-bookkeys-ro.ts (100%) rename scripts/{ => old}/seed-prayers.ts (100%) rename scripts/{ => old}/test-prayers.ts (100%) rename scripts/{ => old}/usfm-to-json.ts (100%) rename scripts/{ => old}/validate-bsb-md.js (100%) rename scripts/{ => old}/validate-bsb-md.ts (100%) diff --git a/app/[locale]/bible/reader.tsx b/app/[locale]/bible/reader.tsx index 97133d7..651c145 100644 --- a/app/[locale]/bible/reader.tsx +++ b/app/[locale]/bible/reader.tsx @@ -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([]) + const [versions, setVersions] = useState([]) + const [selectedVersion, setSelectedVersion] = useState('') const [selectedBook, setSelectedBook] = useState('') const [selectedChapter, setSelectedChapter] = useState(1) const [verses, setVerses] = useState([]) 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() { }} > + {/* Version Selection */} + + + {t('version')} + + + + {/* Books Selection */} @@ -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: { diff --git a/app/[locale]/contact/page.tsx b/app/[locale]/contact/page.tsx new file mode 100644 index 0000000..fbdb233 --- /dev/null +++ b/app/[locale]/contact/page.tsx @@ -0,0 +1,281 @@ +'use client' +import { + Container, + Card, + CardContent, + Typography, + Box, + Button, + TextField, + Paper, + useTheme, + Alert, + Snackbar, +} from '@mui/material' +import { + Email, + LocationOn, + Send, + ContactSupport, +} from '@mui/icons-material' +import { useRouter } from 'next/navigation' +import { useTranslations, useLocale } from 'next-intl' +import { useState } from 'react' + +export default function Contact() { + const theme = useTheme() + const router = useRouter() + const t = useTranslations('contact') + const locale = useLocale() + + const [formData, setFormData] = useState({ + name: '', + email: '', + subject: '', + message: '' + }) + const [isSubmitting, setIsSubmitting] = useState(false) + const [showSuccess, setShowSuccess] = useState(false) + const [showError, setShowError] = useState(false) + + const handleInputChange = (field: string) => (event: React.ChangeEvent) => { + setFormData(prev => ({ + ...prev, + [field]: event.target.value + })) + } + + const handleSubmit = async (event: React.FormEvent) => { + event.preventDefault() + setIsSubmitting(true) + + try { + // Simulate form submission + await new Promise(resolve => setTimeout(resolve, 1000)) + + // Here you would typically send the data to your API + console.log('Form submitted:', formData) + + setFormData({ + name: '', + email: '', + subject: '', + message: '' + }) + setShowSuccess(true) + } catch (error) { + setShowError(true) + } finally { + setIsSubmitting(false) + } + } + + const contactInfo = [ + { + icon: , + title: t('info.email.title'), + content: t('info.email.content'), + action: 'mailto:contact@biblical-guide.com' + }, + { + icon: , + title: t('info.address.title'), + content: t('info.address.content'), + action: null + } + ] + + return ( + + {/* Hero Section */} + + + + + + {t('hero.title')} + + + {t('hero.subtitle')} + + + {t('hero.description')} + + + + + + + + {/* Contact Form */} + + + + + {t('form.title')} + + + {t('form.description')} + + + + + + + + + + + + + + + + + + + + + {/* Contact Information */} + + + + {t('info.title')} + + + {t('info.description')} + + + {contactInfo.map((info, index) => ( + info.action && window.open(info.action, '_self')} + > + + {info.icon} + + + + {info.title} + + + {info.content} + + + + ))} + + {/* FAQ Quick Link */} + + + {t('faq.title')} + + + {t('faq.description')} + + + + + + + + + {/* Success/Error Messages */} + setShowSuccess(false)} + anchorOrigin={{ vertical: 'bottom', horizontal: 'center' }} + > + setShowSuccess(false)} severity="success" sx={{ width: '100%' }}> + {t('form.success')} + + + + setShowError(false)} + anchorOrigin={{ vertical: 'bottom', horizontal: 'center' }} + > + setShowError(false)} severity="error" sx={{ width: '100%' }}> + {t('form.error')} + + + + ) +} \ No newline at end of file diff --git a/app/[locale]/layout.tsx b/app/[locale]/layout.tsx index d05094c..a7fa61a 100644 --- a/app/[locale]/layout.tsx +++ b/app/[locale]/layout.tsx @@ -6,6 +6,7 @@ import { notFound } from 'next/navigation' import { MuiThemeProvider } from '@/components/providers/theme-provider' import { AuthProvider } from '@/components/auth/auth-provider' import { Navigation } from '@/components/layout/navigation' +import { Footer } from '@/components/layout/footer' import FloatingChat from '@/components/chat/floating-chat' import { merriweather, lato } from '@/lib/fonts' @@ -32,7 +33,7 @@ export async function generateMetadata({ params }: { params: Promise<{ locale: s title: t('ogTitle'), description: t('ogDescription'), url: currentUrl, - siteName: locale === 'ro' ? 'Ghid Biblic' : 'Biblical Guide', + siteName: 'Biblical Guide', locale: locale, type: 'website', images: [ @@ -106,6 +107,7 @@ export default async function LocaleLayout({ {children} +