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>
378 lines
13 KiB
TypeScript
378 lines
13 KiB
TypeScript
import { PrismaClient } from '@prisma/client'
|
|
import * as fs from 'fs'
|
|
import * as path from 'path'
|
|
import pdfParse from 'pdf-parse'
|
|
|
|
const prisma = new PrismaClient()
|
|
|
|
interface BibleVerse {
|
|
book: string
|
|
chapter: number
|
|
verse: number
|
|
text: string
|
|
}
|
|
|
|
interface BibleBook {
|
|
name: string
|
|
testament: string
|
|
orderNum: number
|
|
}
|
|
|
|
// Romanian Bible book names mapping
|
|
const romanianBooks: BibleBook[] = [
|
|
// Old Testament
|
|
{ name: 'Geneza', testament: 'Vechiul Testament', orderNum: 1 },
|
|
{ name: 'Exodul', testament: 'Vechiul Testament', orderNum: 2 },
|
|
{ name: 'Leviticul', testament: 'Vechiul Testament', orderNum: 3 },
|
|
{ name: 'Numerii', testament: 'Vechiul Testament', orderNum: 4 },
|
|
{ name: 'Deuteronomul', testament: 'Vechiul Testament', orderNum: 5 },
|
|
{ name: 'Iosua', testament: 'Vechiul Testament', orderNum: 6 },
|
|
{ name: 'Judecătorii', testament: 'Vechiul Testament', orderNum: 7 },
|
|
{ name: 'Rut', testament: 'Vechiul Testament', orderNum: 8 },
|
|
{ name: '1 Samuel', testament: 'Vechiul Testament', orderNum: 9 },
|
|
{ name: '2 Samuel', testament: 'Vechiul Testament', orderNum: 10 },
|
|
{ name: '1 Regi', testament: 'Vechiul Testament', orderNum: 11 },
|
|
{ name: '2 Regi', testament: 'Vechiul Testament', orderNum: 12 },
|
|
{ name: '1 Cronici', testament: 'Vechiul Testament', orderNum: 13 },
|
|
{ name: '2 Cronici', testament: 'Vechiul Testament', orderNum: 14 },
|
|
{ name: 'Ezra', testament: 'Vechiul Testament', orderNum: 15 },
|
|
{ name: 'Neemia', testament: 'Vechiul Testament', orderNum: 16 },
|
|
{ name: 'Estera', testament: 'Vechiul Testament', orderNum: 17 },
|
|
{ name: 'Iov', testament: 'Vechiul Testament', orderNum: 18 },
|
|
{ name: 'Psalmii', testament: 'Vechiul Testament', orderNum: 19 },
|
|
{ name: 'Proverbele', testament: 'Vechiul Testament', orderNum: 20 },
|
|
{ name: 'Ecleziastul', testament: 'Vechiul Testament', orderNum: 21 },
|
|
{ name: 'Cântarea Cântărilor', testament: 'Vechiul Testament', orderNum: 22 },
|
|
{ name: 'Isaia', testament: 'Vechiul Testament', orderNum: 23 },
|
|
{ name: 'Ieremia', testament: 'Vechiul Testament', orderNum: 24 },
|
|
{ name: 'Plângerile', testament: 'Vechiul Testament', orderNum: 25 },
|
|
{ name: 'Ezechiel', testament: 'Vechiul Testament', orderNum: 26 },
|
|
{ name: 'Daniel', testament: 'Vechiul Testament', orderNum: 27 },
|
|
{ name: 'Osea', testament: 'Vechiul Testament', orderNum: 28 },
|
|
{ name: 'Ioel', testament: 'Vechiul Testament', orderNum: 29 },
|
|
{ name: 'Amos', testament: 'Vechiul Testament', orderNum: 30 },
|
|
{ name: 'Obadia', testament: 'Vechiul Testament', orderNum: 31 },
|
|
{ name: 'Iona', testament: 'Vechiul Testament', orderNum: 32 },
|
|
{ name: 'Mica', testament: 'Vechiul Testament', orderNum: 33 },
|
|
{ name: 'Naum', testament: 'Vechiul Testament', orderNum: 34 },
|
|
{ name: 'Habacuc', testament: 'Vechiul Testament', orderNum: 35 },
|
|
{ name: 'Ţefania', testament: 'Vechiul Testament', orderNum: 36 },
|
|
{ name: 'Hagai', testament: 'Vechiul Testament', orderNum: 37 },
|
|
{ name: 'Zaharia', testament: 'Vechiul Testament', orderNum: 38 },
|
|
{ name: 'Maleahi', testament: 'Vechiul Testament', orderNum: 39 },
|
|
|
|
// New Testament
|
|
{ name: 'Matei', testament: 'Noul Testament', orderNum: 40 },
|
|
{ name: 'Marcu', testament: 'Noul Testament', orderNum: 41 },
|
|
{ name: 'Luca', testament: 'Noul Testament', orderNum: 42 },
|
|
{ name: 'Ioan', testament: 'Noul Testament', orderNum: 43 },
|
|
{ name: 'Faptele Apostolilor', testament: 'Noul Testament', orderNum: 44 },
|
|
{ name: 'Romani', testament: 'Noul Testament', orderNum: 45 },
|
|
{ name: '1 Corinteni', testament: 'Noul Testament', orderNum: 46 },
|
|
{ name: '2 Corinteni', testament: 'Noul Testament', orderNum: 47 },
|
|
{ name: 'Galateni', testament: 'Noul Testament', orderNum: 48 },
|
|
{ name: 'Efeseni', testament: 'Noul Testament', orderNum: 49 },
|
|
{ name: 'Filipeni', testament: 'Noul Testament', orderNum: 50 },
|
|
{ name: 'Coloseni', testament: 'Noul Testament', orderNum: 51 },
|
|
{ name: '1 Tesaloniceni', testament: 'Noul Testament', orderNum: 52 },
|
|
{ name: '2 Tesaloniceni', testament: 'Noul Testament', orderNum: 53 },
|
|
{ name: '1 Timotei', testament: 'Noul Testament', orderNum: 54 },
|
|
{ name: '2 Timotei', testament: 'Noul Testament', orderNum: 55 },
|
|
{ name: 'Tit', testament: 'Noul Testament', orderNum: 56 },
|
|
{ name: 'Filimon', testament: 'Noul Testament', orderNum: 57 },
|
|
{ name: 'Evrei', testament: 'Noul Testament', orderNum: 58 },
|
|
{ name: 'Iacob', testament: 'Noul Testament', orderNum: 59 },
|
|
{ name: '1 Petru', testament: 'Noul Testament', orderNum: 60 },
|
|
{ name: '2 Petru', testament: 'Noul Testament', orderNum: 61 },
|
|
{ name: '1 Ioan', testament: 'Noul Testament', orderNum: 62 },
|
|
{ name: '2 Ioan', testament: 'Noul Testament', orderNum: 63 },
|
|
{ name: '3 Ioan', testament: 'Noul Testament', orderNum: 64 },
|
|
{ name: 'Iuda', testament: 'Noul Testament', orderNum: 65 },
|
|
{ name: 'Apocalipsa', testament: 'Noul Testament', orderNum: 66 }
|
|
]
|
|
|
|
function parseRomanianBible(text: string): BibleVerse[] {
|
|
const verses: BibleVerse[] = []
|
|
|
|
// Remove common headers/footers and normalize text
|
|
const cleanText = text
|
|
.replace(/BIBLIA\s+FIDELA/gi, '')
|
|
.replace(/Copyright.*?România/gi, '')
|
|
.replace(/Cluj-Napoca.*?\d{4}/gi, '')
|
|
.replace(/\d+\s*$/gm, '') // Remove page numbers at end of lines
|
|
.replace(/\s+/g, ' ') // Normalize whitespace
|
|
.trim()
|
|
|
|
console.log('Cleaned text preview:', cleanText.substring(0, 2000))
|
|
|
|
// Look for patterns like "Geneza 1:1" or "1:1" with verse text
|
|
// First, split by book names to identify sections
|
|
const bookSections: { book: string, content: string }[] = []
|
|
|
|
let currentContent = cleanText
|
|
|
|
for (const book of romanianBooks) {
|
|
// Look for book name followed by chapter/verse patterns
|
|
const bookPattern = new RegExp(`\\b${book.name}\\b`, 'gi')
|
|
const bookMatch = currentContent.search(bookPattern)
|
|
|
|
if (bookMatch !== -1) {
|
|
// Extract content for this book (until next book or end)
|
|
let nextBookStart = currentContent.length
|
|
|
|
for (const nextBook of romanianBooks) {
|
|
if (nextBook.orderNum > book.orderNum) {
|
|
const nextPattern = new RegExp(`\\b${nextBook.name}\\b`, 'gi')
|
|
const nextMatch = currentContent.search(nextPattern)
|
|
if (nextMatch > bookMatch && nextMatch < nextBookStart) {
|
|
nextBookStart = nextMatch
|
|
}
|
|
}
|
|
}
|
|
|
|
const bookContent = currentContent.substring(bookMatch, nextBookStart)
|
|
bookSections.push({ book: book.name, content: bookContent })
|
|
}
|
|
}
|
|
|
|
console.log(`Found ${bookSections.length} book sections`)
|
|
|
|
// Parse each book section
|
|
for (const section of bookSections) {
|
|
console.log(`Parsing ${section.book}...`)
|
|
|
|
// Look for chapter:verse patterns like "1:1", "1:2", etc.
|
|
const versePattern = /(\d+):(\d+)\s+([^0-9:]+?)(?=\d+:\d+|$)/g
|
|
let match
|
|
|
|
while ((match = versePattern.exec(section.content)) !== null) {
|
|
const chapter = parseInt(match[1])
|
|
const verse = parseInt(match[2])
|
|
const text = match[3].trim()
|
|
|
|
// Clean up the verse text
|
|
const cleanVerseText = text
|
|
.replace(/\s+/g, ' ')
|
|
.replace(/^\W+|\W+$/g, '') // Remove leading/trailing non-word chars
|
|
.trim()
|
|
|
|
if (cleanVerseText.length > 5) { // Only keep substantial text
|
|
verses.push({
|
|
book: section.book,
|
|
chapter: chapter,
|
|
verse: verse,
|
|
text: cleanVerseText
|
|
})
|
|
}
|
|
}
|
|
|
|
// Alternative: look for numbered verses within paragraphs
|
|
const numberedVersePattern = /(\d+)\s+([^0-9]+?)(?=\d+\s+|$)/g
|
|
let altMatch
|
|
let currentChapter = 1
|
|
|
|
// Try to find chapter indicators
|
|
const chapterPattern = /Capitolul\s+(\d+)|^(\d+)$/gm
|
|
const chapterMatches = [...section.content.matchAll(chapterPattern)]
|
|
|
|
if (chapterMatches.length > 0) {
|
|
for (const chMatch of chapterMatches) {
|
|
currentChapter = parseInt(chMatch[1] || chMatch[2])
|
|
|
|
// Find content after this chapter marker
|
|
const chapterStart = chMatch.index! + chMatch[0].length
|
|
let chapterEnd = section.content.length
|
|
|
|
// Find next chapter marker
|
|
for (const nextChMatch of chapterMatches) {
|
|
if (nextChMatch.index! > chMatch.index! && nextChMatch.index! < chapterEnd) {
|
|
chapterEnd = nextChMatch.index!
|
|
}
|
|
}
|
|
|
|
const chapterContent = section.content.substring(chapterStart, chapterEnd)
|
|
|
|
// Parse verses in this chapter
|
|
while ((altMatch = numberedVersePattern.exec(chapterContent)) !== null) {
|
|
const verseNum = parseInt(altMatch[1])
|
|
const verseText = altMatch[2].trim()
|
|
|
|
const cleanText = verseText
|
|
.replace(/\s+/g, ' ')
|
|
.replace(/^\W+|\W+$/g, '')
|
|
.trim()
|
|
|
|
if (cleanText.length > 10) {
|
|
verses.push({
|
|
book: section.book,
|
|
chapter: currentChapter,
|
|
verse: verseNum,
|
|
text: cleanText
|
|
})
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return verses
|
|
}
|
|
|
|
async function importRomanianBible() {
|
|
console.log('Starting Romanian Bible import...')
|
|
|
|
const pdfPath = path.join(process.cwd(), 'bibles', 'Biblia-Fidela-limba-romana.pdf')
|
|
|
|
if (!fs.existsSync(pdfPath)) {
|
|
throw new Error(`PDF file not found at: ${pdfPath}`)
|
|
}
|
|
|
|
console.log('Reading PDF file...')
|
|
const pdfBuffer = fs.readFileSync(pdfPath)
|
|
const pdfData = await pdfParse(pdfBuffer)
|
|
|
|
console.log(`PDF parsed. Text length: ${pdfData.text.length} characters`)
|
|
|
|
console.log('Parsing Bible verses...')
|
|
const verses = parseRomanianBible(pdfData.text)
|
|
console.log(`Found ${verses.length} verses`)
|
|
|
|
if (verses.length === 0) {
|
|
console.log('No verses found. PDF content preview:')
|
|
console.log(pdfData.text.substring(0, 1000))
|
|
throw new Error('Could not parse any verses from the PDF')
|
|
}
|
|
|
|
try {
|
|
// First, create all books
|
|
console.log('Creating Bible books...')
|
|
for (const bookData of romanianBooks) {
|
|
await prisma.bibleBook.upsert({
|
|
where: { id: bookData.orderNum },
|
|
update: {},
|
|
create: {
|
|
id: bookData.orderNum,
|
|
name: bookData.name,
|
|
testament: bookData.testament,
|
|
orderNum: bookData.orderNum
|
|
}
|
|
})
|
|
}
|
|
|
|
// Group verses by book and chapter
|
|
const versesByBook = verses.reduce((acc, verse) => {
|
|
if (!acc[verse.book]) acc[verse.book] = {}
|
|
if (!acc[verse.book][verse.chapter]) acc[verse.book][verse.chapter] = []
|
|
acc[verse.book][verse.chapter].push(verse)
|
|
return acc
|
|
}, {} as Record<string, Record<number, BibleVerse[]>>)
|
|
|
|
console.log('Importing verses by book and chapter...')
|
|
let totalImported = 0
|
|
|
|
for (const [bookName, chapters] of Object.entries(versesByBook)) {
|
|
const book = romanianBooks.find(b => b.name === bookName)
|
|
if (!book) {
|
|
console.warn(`Unknown book: ${bookName}`)
|
|
continue
|
|
}
|
|
|
|
console.log(`Importing ${bookName}...`)
|
|
|
|
for (const [chapterNumStr, chapterVerses] of Object.entries(chapters)) {
|
|
const chapterNum = parseInt(chapterNumStr)
|
|
|
|
// Create chapter
|
|
const chapter = await prisma.bibleChapter.upsert({
|
|
where: {
|
|
bookId_chapterNum: {
|
|
bookId: book.orderNum,
|
|
chapterNum: chapterNum
|
|
}
|
|
},
|
|
update: {},
|
|
create: {
|
|
bookId: book.orderNum,
|
|
chapterNum: chapterNum
|
|
}
|
|
})
|
|
|
|
// Create verses
|
|
for (const verse of chapterVerses) {
|
|
await prisma.bibleVerse.upsert({
|
|
where: {
|
|
chapterId_verseNum_version: {
|
|
chapterId: chapter.id,
|
|
verseNum: verse.verse,
|
|
version: 'RO'
|
|
}
|
|
},
|
|
update: {},
|
|
create: {
|
|
chapterId: chapter.id,
|
|
verseNum: verse.verse,
|
|
text: verse.text,
|
|
version: 'RO'
|
|
}
|
|
})
|
|
totalImported++
|
|
}
|
|
}
|
|
}
|
|
|
|
console.log(`Romanian Bible import completed! Imported ${totalImported} verses.`)
|
|
|
|
// Create search function
|
|
console.log('Creating search function...')
|
|
await prisma.$executeRaw`
|
|
CREATE OR REPLACE FUNCTION search_verses(search_query TEXT, limit_count INT DEFAULT 10)
|
|
RETURNS TABLE(
|
|
verse_id TEXT,
|
|
book_name TEXT,
|
|
chapter_num INT,
|
|
verse_num INT,
|
|
verse_text TEXT,
|
|
rank REAL
|
|
) AS $$
|
|
BEGIN
|
|
RETURN QUERY
|
|
SELECT
|
|
v.id::TEXT,
|
|
b.name,
|
|
c."chapterNum",
|
|
v."verseNum",
|
|
v.text,
|
|
CASE
|
|
WHEN v.text ILIKE '%' || search_query || '%' THEN 1.0
|
|
ELSE ts_rank(to_tsvector('romanian', v.text), plainto_tsquery('romanian', search_query))
|
|
END as rank
|
|
FROM "BibleVerse" v
|
|
JOIN "BibleChapter" c ON v."chapterId" = c.id
|
|
JOIN "BibleBook" b ON c."bookId" = b.id
|
|
WHERE v.text ILIKE '%' || search_query || '%'
|
|
OR to_tsvector('romanian', v.text) @@ plainto_tsquery('romanian', search_query)
|
|
ORDER BY rank DESC, b."orderNum", c."chapterNum", v."verseNum"
|
|
LIMIT limit_count;
|
|
END;
|
|
$$ LANGUAGE plpgsql;
|
|
`
|
|
|
|
console.log('Search function created successfully!')
|
|
|
|
} catch (error) {
|
|
console.error('Error importing Romanian Bible:', error)
|
|
throw error
|
|
}
|
|
}
|
|
|
|
// Run the import
|
|
importRomanianBible()
|
|
.then(() => {
|
|
console.log('Romanian Bible import completed successfully!')
|
|
process.exit(0)
|
|
})
|
|
.catch((error) => {
|
|
console.error('Import failed:', error)
|
|
process.exit(1)
|
|
})
|
|
.finally(() => prisma.$disconnect()) |