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>
423 lines
14 KiB
TypeScript
423 lines
14 KiB
TypeScript
import fs from 'fs'
|
|
import path from 'path'
|
|
|
|
interface BibleMetadata {
|
|
Country: string
|
|
flag_Image: string
|
|
Language: string
|
|
Language_English: string
|
|
Vernacular_Bible_Title: string
|
|
English_Bible_Title: string
|
|
file_ID: string
|
|
}
|
|
|
|
interface ParsedVerse {
|
|
book: string
|
|
chapter: number
|
|
verse: number
|
|
text: string
|
|
}
|
|
|
|
interface JsonVerse {
|
|
verseNum: number
|
|
text: string
|
|
}
|
|
|
|
interface JsonChapter {
|
|
chapterNum: number
|
|
verses: JsonVerse[]
|
|
}
|
|
|
|
interface JsonBook {
|
|
bookKey: string
|
|
name: string
|
|
testament: string
|
|
orderNum: number
|
|
chapters: JsonChapter[]
|
|
}
|
|
|
|
interface JsonBibleVersion {
|
|
name: string
|
|
abbreviation: string
|
|
language: string
|
|
description: string
|
|
country: string
|
|
englishTitle: string
|
|
zipFileUrl: string
|
|
flagImageUrl: string
|
|
isDefault: boolean
|
|
books: JsonBook[]
|
|
}
|
|
|
|
// Book name mappings from VPL format to normalized keys
|
|
const BOOK_MAPPINGS: Record<string, string> = {
|
|
// Old Testament
|
|
'GEN': 'genesis', 'EXO': 'exodus', 'LEV': 'leviticus', 'NUM': 'numbers', 'DEU': 'deuteronomy',
|
|
'JOS': 'joshua', 'JDG': 'judges', 'RUT': 'ruth', '1SA': '1-samuel', '2SA': '2-samuel',
|
|
'1KI': '1-kings', '2KI': '2-kings', '1CH': '1-chronicles', '2CH': '2-chronicles',
|
|
'EZR': 'ezra', 'NEH': 'nehemiah', 'EST': 'esther', 'JOB': 'job', 'PSA': 'psalms',
|
|
'PRO': 'proverbs', 'ECC': 'ecclesiastes', 'SNG': 'song-of-songs', 'ISA': 'isaiah',
|
|
'JER': 'jeremiah', 'LAM': 'lamentations', 'EZK': 'ezekiel', 'EZE': 'ezekiel', 'DAN': 'daniel',
|
|
'HOS': 'hosea', 'JOL': 'joel', 'JOE': 'joel', 'AMO': 'amos', 'OBA': 'obadiah', 'JON': 'jonah',
|
|
'MIC': 'micah', 'NAM': 'nahum', 'NAH': 'nahum', 'HAB': 'habakkuk', 'ZEP': 'zephaniah',
|
|
'HAG': 'haggai', 'ZEC': 'zechariah', 'MAL': 'malachi',
|
|
|
|
// New Testament
|
|
'MAT': 'matthew', 'MRK': 'mark', 'MAR': 'mark', 'LUK': 'luke', 'JHN': 'john', 'JOH': 'john', 'ACT': 'acts',
|
|
'ROM': 'romans', '1CO': '1-corinthians', '2CO': '2-corinthians', 'GAL': 'galatians',
|
|
'EPH': 'ephesians', 'PHP': 'philippians', 'PHI': 'philippians', 'COL': 'colossians', '1TH': '1-thessalonians',
|
|
'2TH': '2-thessalonians', '1TI': '1-timothy', '2TI': '2-timothy', 'TIT': 'titus',
|
|
'PHM': 'philemon', 'HEB': 'hebrews', 'JAS': 'james', 'JAM': 'james', '1PE': '1-peter', '2PE': '2-peter',
|
|
'1JN': '1-john', '1JO': '1-john', '2JN': '2-john', '2JO': '2-john', '3JN': '3-john',
|
|
'3JO': '3-john', 'JUD': 'jude', 'REV': 'revelation', 'SOL': 'song-of-songs'
|
|
}
|
|
|
|
// Book order numbers (1-66)
|
|
const BOOK_ORDER: Record<string, number> = {
|
|
'genesis': 1, 'exodus': 2, 'leviticus': 3, 'numbers': 4, 'deuteronomy': 5,
|
|
'joshua': 6, 'judges': 7, 'ruth': 8, '1-samuel': 9, '2-samuel': 10,
|
|
'1-kings': 11, '2-kings': 12, '1-chronicles': 13, '2-chronicles': 14,
|
|
'ezra': 15, 'nehemiah': 16, 'esther': 17, 'job': 18, 'psalms': 19,
|
|
'proverbs': 20, 'ecclesiastes': 21, 'song-of-songs': 22, 'isaiah': 23,
|
|
'jeremiah': 24, 'lamentations': 25, 'ezekiel': 26, 'daniel': 27,
|
|
'hosea': 28, 'joel': 29, 'amos': 30, 'obadiah': 31, 'jonah': 32,
|
|
'micah': 33, 'nahum': 34, 'habakkuk': 35, 'zephaniah': 36,
|
|
'haggai': 37, 'zechariah': 38, 'malachi': 39,
|
|
|
|
'matthew': 40, 'mark': 41, 'luke': 42, 'john': 43, 'acts': 44,
|
|
'romans': 45, '1-corinthians': 46, '2-corinthians': 47, 'galatians': 48,
|
|
'ephesians': 49, 'philippians': 50, 'colossians': 51, '1-thessalonians': 52,
|
|
'2-thessalonians': 53, '1-timothy': 54, '2-timothy': 55, 'titus': 56,
|
|
'philemon': 57, 'hebrews': 58, 'james': 59, '1-peter': 60, '2-peter': 61,
|
|
'1-john': 62, '2-john': 63, '3-john': 64, 'jude': 65, 'revelation': 66
|
|
}
|
|
|
|
function parseVplFile(filePath: string): ParsedVerse[] {
|
|
const content = fs.readFileSync(filePath, 'utf-8')
|
|
const lines = content.split('\n').filter(line => line.trim())
|
|
const verses: ParsedVerse[] = []
|
|
|
|
for (const line of lines) {
|
|
const match = line.match(/^(\w+)\s+(\d+):(\d+)\s+(.+)$/)
|
|
if (match) {
|
|
const [, bookCode, chapterStr, verseStr, text] = match
|
|
const chapter = parseInt(chapterStr, 10)
|
|
const verse = parseInt(verseStr, 10)
|
|
|
|
if (BOOK_MAPPINGS[bookCode]) {
|
|
verses.push({
|
|
book: BOOK_MAPPINGS[bookCode],
|
|
chapter,
|
|
verse,
|
|
text: text.trim()
|
|
})
|
|
} else {
|
|
console.warn(`Unknown book code: ${bookCode} in ${filePath}`)
|
|
}
|
|
}
|
|
}
|
|
|
|
return verses
|
|
}
|
|
|
|
function loadBibleMetadata(): BibleMetadata[] {
|
|
const csvPath = path.join(process.cwd(), 'bibles', 'bibles_list.csv')
|
|
const content = fs.readFileSync(csvPath, 'utf-8')
|
|
const lines = content.split('\n').filter(line => line.trim())
|
|
const results: BibleMetadata[] = []
|
|
|
|
// Parse CSV manually handling quoted fields
|
|
for (let i = 1; i < lines.length; i++) {
|
|
const line = lines[i]
|
|
const values: string[] = []
|
|
let current = ''
|
|
let inQuotes = false
|
|
|
|
for (let j = 0; j < line.length; j++) {
|
|
const char = line[j]
|
|
if (char === '"') {
|
|
inQuotes = !inQuotes
|
|
} else if (char === ',' && !inQuotes) {
|
|
values.push(current.trim())
|
|
current = ''
|
|
} else {
|
|
current += char
|
|
}
|
|
}
|
|
values.push(current.trim()) // Add the last value
|
|
|
|
if (values.length >= 7) {
|
|
results.push({
|
|
Country: values[0] || '',
|
|
flag_Image: values[1] || '',
|
|
Language: values[2] || '',
|
|
Language_English: values[3] || '',
|
|
Vernacular_Bible_Title: values[4] || '',
|
|
English_Bible_Title: values[5] || '',
|
|
file_ID: values[6] || ''
|
|
})
|
|
}
|
|
}
|
|
|
|
return results
|
|
}
|
|
|
|
function getTestament(bookKey: string): string {
|
|
const orderNum = BOOK_ORDER[bookKey]
|
|
return orderNum <= 39 ? 'Old Testament' : 'New Testament'
|
|
}
|
|
|
|
function getLanguageCode(language: string): string {
|
|
// Try to extract ISO language code from language string
|
|
const langMap: Record<string, string> = {
|
|
'english': 'en',
|
|
'spanish': 'es',
|
|
'french': 'fr',
|
|
'german': 'de',
|
|
'portuguese': 'pt',
|
|
'italian': 'it',
|
|
'dutch': 'nl',
|
|
'russian': 'ru',
|
|
'chinese': 'zh',
|
|
'japanese': 'ja',
|
|
'korean': 'ko',
|
|
'arabic': 'ar',
|
|
'hindi': 'hi',
|
|
'romanian': 'ro'
|
|
}
|
|
|
|
const lowerLang = language.toLowerCase()
|
|
for (const [key, code] of Object.entries(langMap)) {
|
|
if (lowerLang.includes(key)) {
|
|
return code
|
|
}
|
|
}
|
|
|
|
// Default to first 2 characters if no mapping found
|
|
return lowerLang.substring(0, 2)
|
|
}
|
|
|
|
function convertVplToJson(metadata: BibleMetadata): JsonBibleVersion | null {
|
|
const vplPath = path.join(process.cwd(), 'bibles', 'ebible_vpl', `${metadata.file_ID}_vpl.txt`)
|
|
|
|
if (!fs.existsSync(vplPath)) {
|
|
console.warn(`VPL file not found: ${vplPath}`)
|
|
return null
|
|
}
|
|
|
|
console.log(`Converting ${metadata.Vernacular_Bible_Title} (${metadata.file_ID})...`)
|
|
|
|
const verses = parseVplFile(vplPath)
|
|
if (verses.length === 0) {
|
|
console.warn(`No verses found in ${vplPath}`)
|
|
return null
|
|
}
|
|
|
|
// Create Bible version metadata
|
|
const languageCode = getLanguageCode(metadata.Language_English)
|
|
const zipFileUrl = `https://ebible.org/Scriptures/${metadata.file_ID}_vpl.zip`
|
|
|
|
const bibleVersion: JsonBibleVersion = {
|
|
name: metadata.Vernacular_Bible_Title,
|
|
abbreviation: metadata.file_ID.toUpperCase(),
|
|
language: languageCode,
|
|
description: `${metadata.English_Bible_Title} - ${metadata.Country}`,
|
|
country: metadata.Country.trim(),
|
|
englishTitle: metadata.English_Bible_Title,
|
|
zipFileUrl: zipFileUrl,
|
|
flagImageUrl: metadata.flag_Image,
|
|
isDefault: false,
|
|
books: []
|
|
}
|
|
|
|
// Group verses by book
|
|
const bookGroups = new Map<string, ParsedVerse[]>()
|
|
for (const verse of verses) {
|
|
if (!bookGroups.has(verse.book)) {
|
|
bookGroups.set(verse.book, [])
|
|
}
|
|
bookGroups.get(verse.book)!.push(verse)
|
|
}
|
|
|
|
// Convert each book
|
|
for (const [bookKey, bookVerses] of bookGroups) {
|
|
const orderNum = BOOK_ORDER[bookKey]
|
|
const testament = getTestament(bookKey)
|
|
|
|
const book: JsonBook = {
|
|
bookKey: bookKey,
|
|
name: bookKey.charAt(0).toUpperCase() + bookKey.slice(1).replace('-', ' '),
|
|
testament: testament,
|
|
orderNum: orderNum,
|
|
chapters: []
|
|
}
|
|
|
|
// Group verses by chapter
|
|
const chapterGroups = new Map<number, ParsedVerse[]>()
|
|
for (const verse of bookVerses) {
|
|
if (!chapterGroups.has(verse.chapter)) {
|
|
chapterGroups.set(verse.chapter, [])
|
|
}
|
|
chapterGroups.get(verse.chapter)!.push(verse)
|
|
}
|
|
|
|
// Convert each chapter
|
|
for (const [chapterNum, chapterVerses] of chapterGroups) {
|
|
const chapter: JsonChapter = {
|
|
chapterNum: chapterNum,
|
|
verses: []
|
|
}
|
|
|
|
// Sort verses by verse number
|
|
chapterVerses.sort((a, b) => a.verse - b.verse)
|
|
|
|
// Convert verses
|
|
for (const verse of chapterVerses) {
|
|
chapter.verses.push({
|
|
verseNum: verse.verse,
|
|
text: verse.text
|
|
})
|
|
}
|
|
|
|
book.chapters.push(chapter)
|
|
}
|
|
|
|
// Sort chapters by chapter number
|
|
book.chapters.sort((a, b) => a.chapterNum - b.chapterNum)
|
|
bibleVersion.books.push(book)
|
|
}
|
|
|
|
// Sort books by order number
|
|
bibleVersion.books.sort((a, b) => a.orderNum - b.orderNum)
|
|
|
|
console.log(`✅ Converted ${verses.length} verses in ${bibleVersion.books.length} books`)
|
|
return bibleVersion
|
|
}
|
|
|
|
async function main() {
|
|
console.log('🚀 Starting VPL to JSON conversion...')
|
|
|
|
const outputDir = path.join(process.cwd(), 'bibles', 'json')
|
|
const logFilePath = path.join(outputDir, 'conversion_log.txt')
|
|
const errorLogPath = path.join(outputDir, 'conversion_errors.txt')
|
|
|
|
// Initialize log files
|
|
const logMessage = (message: string) => {
|
|
const timestamp = new Date().toISOString()
|
|
const logEntry = `[${timestamp}] ${message}\n`
|
|
console.log(message)
|
|
fs.appendFileSync(logFilePath, logEntry, 'utf-8')
|
|
}
|
|
|
|
const logError = (message: string, error?: any) => {
|
|
const timestamp = new Date().toISOString()
|
|
const errorEntry = `[${timestamp}] ERROR: ${message}\n${error ? `Details: ${error.toString()}\n` : ''}\n`
|
|
console.error(message, error || '')
|
|
fs.appendFileSync(errorLogPath, errorEntry, 'utf-8')
|
|
}
|
|
|
|
try {
|
|
// Create output directory
|
|
if (!fs.existsSync(outputDir)) {
|
|
fs.mkdirSync(outputDir, { recursive: true })
|
|
logMessage(`📁 Created output directory: ${outputDir}`)
|
|
}
|
|
|
|
// Initialize log files
|
|
fs.writeFileSync(logFilePath, `VPL to JSON Conversion Log - Started at ${new Date().toISOString()}\n`, 'utf-8')
|
|
fs.writeFileSync(errorLogPath, `VPL to JSON Conversion Errors - Started at ${new Date().toISOString()}\n`, 'utf-8')
|
|
|
|
// Load Bible metadata
|
|
logMessage('📋 Loading Bible metadata...')
|
|
const metadata = loadBibleMetadata()
|
|
logMessage(`Found ${metadata.length} Bible versions to convert`)
|
|
|
|
// Force re-conversion of all files for end-to-end testing
|
|
logMessage(`Force re-converting all files for complete end-to-end process`)
|
|
|
|
// Convert each Bible version
|
|
let converted = 0
|
|
let skipped = 0
|
|
let resumed = 0
|
|
|
|
for (const bibleData of metadata) {
|
|
|
|
try {
|
|
const jsonBible = convertVplToJson(bibleData)
|
|
if (jsonBible) {
|
|
// Save individual Bible JSON file
|
|
const filename = `${bibleData.file_ID}_bible.json`
|
|
const filepath = path.join(outputDir, filename)
|
|
|
|
fs.writeFileSync(filepath, JSON.stringify(jsonBible, null, 2), 'utf-8')
|
|
logMessage(`💾 Saved: ${filename}`)
|
|
converted++
|
|
|
|
// Progress update every 10 conversions
|
|
if (converted % 10 === 0) {
|
|
logMessage(`📈 Progress: ${converted}/${metadata.length} converted...`)
|
|
|
|
// Force garbage collection to prevent memory buildup
|
|
if (global.gc) {
|
|
global.gc()
|
|
}
|
|
}
|
|
} else {
|
|
skipped++
|
|
logError(`Skipped ${bibleData.file_ID}: No valid Bible data found`)
|
|
}
|
|
} catch (error) {
|
|
logError(`Failed to convert ${bibleData.file_ID}`, error)
|
|
skipped++
|
|
}
|
|
}
|
|
|
|
// Skip creating large master file to prevent memory issues
|
|
logMessage('\n📦 Skipping master file creation to prevent memory issues...')
|
|
|
|
// Final summary
|
|
const totalProcessed = converted + skipped + resumed
|
|
logMessage('\n✅ VPL to JSON conversion completed!')
|
|
logMessage(`📊 Final Summary:`)
|
|
logMessage(` - Successfully converted: ${converted}`)
|
|
logMessage(` - Already converted (resumed): ${resumed}`)
|
|
logMessage(` - Skipped due to errors: ${skipped}`)
|
|
logMessage(` - Total processed: ${totalProcessed}`)
|
|
logMessage(` - Output directory: ${outputDir}`)
|
|
logMessage(` - Individual files: ${converted + resumed} Bible JSON files`)
|
|
logMessage(` - Log file: conversion_log.txt`)
|
|
logMessage(` - Error file: conversion_errors.txt`)
|
|
|
|
// Create a simple summary without loading all data into memory
|
|
const allJsonFiles = fs.readdirSync(outputDir).filter(f => f.endsWith('_bible.json'))
|
|
const summaryData = {
|
|
conversionSummary: {
|
|
totalJsonFiles: allJsonFiles.length,
|
|
totalAttempted: metadata.length,
|
|
successfullyConverted: converted,
|
|
alreadyExisted: resumed,
|
|
skippedDueToErrors: skipped,
|
|
completedAt: new Date().toISOString()
|
|
},
|
|
availableFiles: allJsonFiles.sort()
|
|
}
|
|
|
|
const summaryFile = path.join(outputDir, 'conversion_summary.json')
|
|
fs.writeFileSync(summaryFile, JSON.stringify(summaryData, null, 2), 'utf-8')
|
|
logMessage(`📊 Summary file saved: conversion_summary.json`)
|
|
|
|
} catch (error) {
|
|
logError('Conversion failed with fatal error', error)
|
|
throw error
|
|
}
|
|
}
|
|
|
|
main()
|
|
.then(() => {
|
|
console.log('🎉 Conversion process completed successfully!')
|
|
process.exit(0)
|
|
})
|
|
.catch((error) => {
|
|
console.error('💥 Conversion failed:', error)
|
|
process.exit(1)
|
|
}) |