Fix Edge-incompatible middleware; set Node runtime on Prisma/pg routes; add full Romanian Bible import + converter; import data JSON; resync RO bookKeys; stabilize /api/bible/books locale fallback; restart dev server.
This commit is contained in:
@@ -30,6 +30,7 @@ import {
|
||||
Share,
|
||||
} from '@mui/icons-material'
|
||||
import { useState, useEffect } from 'react'
|
||||
import { useTranslations, useLocale } from 'next-intl'
|
||||
|
||||
interface BibleVerse {
|
||||
id: string
|
||||
@@ -44,26 +45,33 @@ interface BibleChapter {
|
||||
}
|
||||
|
||||
interface BibleBook {
|
||||
id: number
|
||||
id: string
|
||||
name: string
|
||||
testament: string
|
||||
orderNum: number
|
||||
bookKey: string
|
||||
chapters: BibleChapter[]
|
||||
}
|
||||
|
||||
export default function BiblePage() {
|
||||
const theme = useTheme()
|
||||
const t = useTranslations('pages.bible')
|
||||
const locale = useLocale()
|
||||
const [books, setBooks] = useState<BibleBook[]>([])
|
||||
const [selectedBook, setSelectedBook] = useState<number>(1)
|
||||
const [selectedBook, setSelectedBook] = useState<string>('')
|
||||
const [selectedChapter, setSelectedChapter] = useState<number>(1)
|
||||
const [verses, setVerses] = useState<BibleVerse[]>([])
|
||||
const [loading, setLoading] = useState(true)
|
||||
|
||||
// Fetch available books
|
||||
useEffect(() => {
|
||||
fetch('/api/bible/books')
|
||||
fetch(`/api/bible/books?locale=${locale}`)
|
||||
.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 => {
|
||||
@@ -120,7 +128,7 @@ export default function BiblePage() {
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 2 }}>
|
||||
<CircularProgress size={48} />
|
||||
<Typography variant="h6" color="text.secondary">
|
||||
Se încarcă cărțile...
|
||||
{t('loading')}
|
||||
</Typography>
|
||||
</Box>
|
||||
</Container>
|
||||
@@ -136,10 +144,10 @@ export default function BiblePage() {
|
||||
<Box sx={{ mb: 4, textAlign: 'center' }}>
|
||||
<Typography variant="h3" component="h1" gutterBottom>
|
||||
<MenuBook sx={{ fontSize: 40, mr: 2, verticalAlign: 'middle' }} />
|
||||
Citește Biblia
|
||||
{t('title')}
|
||||
</Typography>
|
||||
<Typography variant="body1" color="text.secondary">
|
||||
Explorează Scriptura cu o interfață modernă și intuitivă
|
||||
{t('subtitle')}
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
@@ -149,16 +157,16 @@ export default function BiblePage() {
|
||||
<Card>
|
||||
<CardContent>
|
||||
<Typography variant="h6" gutterBottom>
|
||||
Selectează cartea
|
||||
{t('selectBook')}
|
||||
</Typography>
|
||||
|
||||
<FormControl fullWidth sx={{ mb: 2 }}>
|
||||
<InputLabel>Cartea</InputLabel>
|
||||
<InputLabel>{t('book')}</InputLabel>
|
||||
<Select
|
||||
value={selectedBook}
|
||||
label="Cartea"
|
||||
label={t('book')}
|
||||
onChange={(e) => {
|
||||
setSelectedBook(Number(e.target.value))
|
||||
setSelectedBook(e.target.value)
|
||||
setSelectedChapter(1)
|
||||
}}
|
||||
>
|
||||
@@ -171,15 +179,15 @@ export default function BiblePage() {
|
||||
</FormControl>
|
||||
|
||||
<FormControl fullWidth>
|
||||
<InputLabel>Capitolul</InputLabel>
|
||||
<InputLabel>{t('chapter')}</InputLabel>
|
||||
<Select
|
||||
value={selectedChapter}
|
||||
label="Capitolul"
|
||||
label={t('chapter')}
|
||||
onChange={(e) => setSelectedChapter(Number(e.target.value))}
|
||||
>
|
||||
{Array.from({ length: maxChapters }, (_, i) => (
|
||||
<MenuItem key={i + 1} value={i + 1}>
|
||||
Capitolul {i + 1}
|
||||
{t('chapter')} {i + 1}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
@@ -208,7 +216,7 @@ export default function BiblePage() {
|
||||
{currentBook?.name || 'Geneza'} {selectedChapter}
|
||||
</Typography>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
{verses.length} versete
|
||||
{verses.length} {t('verses')}
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
@@ -218,14 +226,14 @@ export default function BiblePage() {
|
||||
variant="outlined"
|
||||
size="small"
|
||||
>
|
||||
Salvează
|
||||
{t('save')}
|
||||
</Button>
|
||||
<Button
|
||||
startIcon={<Share />}
|
||||
variant="outlined"
|
||||
size="small"
|
||||
>
|
||||
Partajează
|
||||
{t('share')}
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
@@ -280,7 +288,7 @@ export default function BiblePage() {
|
||||
</Box>
|
||||
) : (
|
||||
<Typography textAlign="center" color="text.secondary">
|
||||
Nu s-au găsit versete pentru această selecție.
|
||||
{t('noVerses')}
|
||||
</Typography>
|
||||
)}
|
||||
|
||||
@@ -291,7 +299,7 @@ export default function BiblePage() {
|
||||
onClick={handlePreviousChapter}
|
||||
disabled={selectedBook === 1 && selectedChapter === 1}
|
||||
>
|
||||
Capitolul anterior
|
||||
{t('previousChapter')}
|
||||
</Button>
|
||||
|
||||
<Typography variant="body2" color="text.secondary" sx={{ alignSelf: 'center' }}>
|
||||
@@ -303,7 +311,7 @@ export default function BiblePage() {
|
||||
onClick={handleNextChapter}
|
||||
disabled={selectedBook === books.length && selectedChapter === maxChapters}
|
||||
>
|
||||
Capitolul următor
|
||||
{t('nextChapter')}
|
||||
</Button>
|
||||
</Box>
|
||||
</CardContent>
|
||||
|
||||
@@ -2,6 +2,8 @@ import { NextResponse } from 'next/server'
|
||||
import { validateUser, generateToken } from '@/lib/auth'
|
||||
import { prisma } from '@/lib/db'
|
||||
|
||||
export const runtime = 'nodejs'
|
||||
|
||||
export async function POST(request: Request) {
|
||||
try {
|
||||
const { email, password } = await request.json()
|
||||
@@ -43,4 +45,4 @@ export async function POST(request: Request) {
|
||||
console.error('Login error:', error)
|
||||
return NextResponse.json({ error: 'Eroare de server' }, { status: 500 })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { NextResponse } from 'next/server'
|
||||
import { getUserFromToken } from '@/lib/auth'
|
||||
|
||||
export const runtime = 'nodejs'
|
||||
|
||||
export async function GET(request: Request) {
|
||||
try {
|
||||
const authHeader = request.headers.get('authorization')
|
||||
@@ -20,4 +22,4 @@ export async function GET(request: Request) {
|
||||
console.error('User validation error:', error)
|
||||
return NextResponse.json({ error: 'Eroare de server' }, { status: 500 })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,8 @@ import { prisma } from '@/lib/db'
|
||||
import { userRegistrationSchema } from '@/lib/validation'
|
||||
import { z } from 'zod'
|
||||
|
||||
export const runtime = 'nodejs'
|
||||
|
||||
export async function POST(request: Request) {
|
||||
try {
|
||||
const body = await request.json()
|
||||
@@ -47,4 +49,4 @@ export async function POST(request: Request) {
|
||||
}
|
||||
return NextResponse.json({ error: 'Eroare de server' }, { status: 500 })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,64 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { prisma } from '@/lib/db'
|
||||
|
||||
// Ensure this route runs on the Node.js runtime (Prisma requires Node)
|
||||
export const runtime = 'nodejs'
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
console.log('Books API called')
|
||||
const { searchParams } = new URL(request.url)
|
||||
const locale = searchParams.get('locale') || 'ro'
|
||||
const versionAbbr = searchParams.get('version') // Optional specific version
|
||||
console.log('Locale:', locale, 'Version:', versionAbbr)
|
||||
|
||||
// Get the appropriate Bible version
|
||||
let bibleVersion
|
||||
const langCandidates = Array.from(new Set([locale, locale.toLowerCase(), locale.toUpperCase()]))
|
||||
if (versionAbbr) {
|
||||
// Use specific version if provided
|
||||
bibleVersion = await prisma.bibleVersion.findFirst({
|
||||
where: {
|
||||
abbreviation: versionAbbr,
|
||||
language: { in: langCandidates }
|
||||
}
|
||||
})
|
||||
} else {
|
||||
// Try default version for the language
|
||||
bibleVersion = await prisma.bibleVersion.findFirst({
|
||||
where: {
|
||||
language: { in: langCandidates },
|
||||
isDefault: true
|
||||
}
|
||||
})
|
||||
// Fallback: any version for the language
|
||||
if (!bibleVersion) {
|
||||
bibleVersion = await prisma.bibleVersion.findFirst({
|
||||
where: { language: { in: langCandidates } },
|
||||
orderBy: { createdAt: 'asc' }
|
||||
})
|
||||
}
|
||||
// Fallback: use English default if requested locale not found
|
||||
if (!bibleVersion && locale !== 'en') {
|
||||
bibleVersion = await prisma.bibleVersion.findFirst({
|
||||
where: { language: 'en', isDefault: true }
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if (!bibleVersion) {
|
||||
return NextResponse.json({
|
||||
success: false,
|
||||
error: `No Bible version found for language: ${locale}`,
|
||||
books: []
|
||||
}, { status: 404 })
|
||||
}
|
||||
|
||||
// Get books for this version
|
||||
const books = await prisma.bibleBook.findMany({
|
||||
where: {
|
||||
versionId: bibleVersion.id
|
||||
},
|
||||
orderBy: {
|
||||
orderNum: 'asc'
|
||||
},
|
||||
@@ -11,6 +66,13 @@ export async function GET(request: NextRequest) {
|
||||
chapters: {
|
||||
orderBy: {
|
||||
chapterNum: 'asc'
|
||||
},
|
||||
include: {
|
||||
_count: {
|
||||
select: {
|
||||
verses: true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -18,7 +80,24 @@ export async function GET(request: NextRequest) {
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
books: books
|
||||
version: {
|
||||
id: bibleVersion.id,
|
||||
name: bibleVersion.name,
|
||||
abbreviation: bibleVersion.abbreviation,
|
||||
language: bibleVersion.language
|
||||
},
|
||||
books: books.map(book => ({
|
||||
id: book.id,
|
||||
name: book.name,
|
||||
testament: book.testament,
|
||||
orderNum: book.orderNum,
|
||||
bookKey: book.bookKey,
|
||||
chapters: book.chapters.map(chapter => ({
|
||||
id: chapter.id,
|
||||
chapterNum: chapter.chapterNum,
|
||||
verseCount: chapter._count.verses
|
||||
}))
|
||||
}))
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Error fetching books:', error)
|
||||
@@ -31,4 +110,4 @@ export async function GET(request: NextRequest) {
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,8 @@ import { NextResponse } from 'next/server'
|
||||
import { prisma } from '@/lib/db'
|
||||
import { CacheManager } from '@/lib/cache'
|
||||
|
||||
export const runtime = 'nodejs'
|
||||
|
||||
export async function GET(request: Request) {
|
||||
try {
|
||||
const { searchParams } = new URL(request.url)
|
||||
@@ -57,4 +59,4 @@ export async function GET(request: Request) {
|
||||
console.error('Chapter fetch error:', error)
|
||||
return NextResponse.json({ error: 'Eroare de server' }, { status: 500 })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { NextResponse } from 'next/server'
|
||||
import { prisma } from '@/lib/db'
|
||||
|
||||
export const runtime = 'nodejs'
|
||||
|
||||
export async function GET(request: Request) {
|
||||
try {
|
||||
const { searchParams } = new URL(request.url)
|
||||
@@ -64,4 +66,4 @@ export async function GET(request: Request) {
|
||||
return NextResponse.json({ error: 'Eroare de server' }, { status: 500 })
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { prisma } from '@/lib/db'
|
||||
|
||||
export const runtime = 'nodejs'
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
const { searchParams } = new URL(request.url)
|
||||
@@ -18,18 +20,27 @@ export async function GET(request: NextRequest) {
|
||||
)
|
||||
}
|
||||
|
||||
// Find the chapter
|
||||
// Find the chapter (bookId is now a string UUID)
|
||||
const chapterRecord = await prisma.bibleChapter.findFirst({
|
||||
where: {
|
||||
bookId: parseInt(bookId),
|
||||
bookId: bookId,
|
||||
chapterNum: parseInt(chapter)
|
||||
},
|
||||
include: {
|
||||
book: {
|
||||
include: {
|
||||
version: true
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
if (!chapterRecord) {
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
verses: []
|
||||
verses: [],
|
||||
book: null,
|
||||
chapter: null
|
||||
})
|
||||
}
|
||||
|
||||
@@ -45,6 +56,22 @@ export async function GET(request: NextRequest) {
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
book: {
|
||||
id: chapterRecord.book.id,
|
||||
name: chapterRecord.book.name,
|
||||
testament: chapterRecord.book.testament,
|
||||
orderNum: chapterRecord.book.orderNum
|
||||
},
|
||||
chapter: {
|
||||
id: chapterRecord.id,
|
||||
chapterNum: chapterRecord.chapterNum
|
||||
},
|
||||
version: {
|
||||
id: chapterRecord.book.version.id,
|
||||
name: chapterRecord.book.version.name,
|
||||
abbreviation: chapterRecord.book.version.abbreviation,
|
||||
language: chapterRecord.book.version.language
|
||||
},
|
||||
verses: verses.map(verse => ({
|
||||
id: verse.id,
|
||||
verseNum: verse.verseNum,
|
||||
@@ -62,4 +89,4 @@ export async function GET(request: NextRequest) {
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,8 @@ import { NextResponse } from 'next/server'
|
||||
import { prisma } from '@/lib/db'
|
||||
import { getUserFromToken } from '@/lib/auth'
|
||||
|
||||
export const runtime = 'nodejs'
|
||||
|
||||
export async function GET(request: Request) {
|
||||
try {
|
||||
const authHeader = request.headers.get('authorization')
|
||||
@@ -98,4 +100,4 @@ export async function POST(request: Request) {
|
||||
console.error('Bookmark creation error:', error)
|
||||
return NextResponse.json({ error: 'Eroare de server' }, { status: 500 })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,8 @@ import { NextRequest, NextResponse } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
import { searchBibleHybrid, BibleVerse } from '@/lib/vector-search'
|
||||
|
||||
export const runtime = 'nodejs'
|
||||
|
||||
const chatRequestSchema = z.object({
|
||||
message: z.string().min(1),
|
||||
locale: z.string().optional().default('ro'),
|
||||
@@ -158,4 +160,4 @@ Current question: ${message}`
|
||||
|
||||
return fallbackResponses[locale as keyof typeof fallbackResponses] || fallbackResponses.en
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { NextResponse } from 'next/server'
|
||||
import { prisma } from '@/lib/db'
|
||||
|
||||
export const runtime = 'nodejs'
|
||||
|
||||
export async function GET() {
|
||||
try {
|
||||
// Check database connection
|
||||
@@ -26,4 +28,4 @@ export async function GET() {
|
||||
{ status: 503 }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { prisma } from '@/lib/db'
|
||||
|
||||
export const runtime = 'nodejs'
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
const { searchParams } = new URL(request.url)
|
||||
@@ -168,4 +170,4 @@ export async function GET(request: NextRequest) {
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
32332
data/new_testament.json
Normal file
32332
data/new_testament.json
Normal file
File diff suppressed because it is too large
Load Diff
94081
data/old_testament.json
Normal file
94081
data/old_testament.json
Normal file
File diff suppressed because it is too large
Load Diff
22
i18n/request.ts
Normal file
22
i18n/request.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import { getRequestConfig } from 'next-intl/server'
|
||||
import { headers } from 'next/headers'
|
||||
|
||||
export default getRequestConfig(async () => {
|
||||
// This will be called for each request and determines
|
||||
// which locale to use based on the URL
|
||||
const headersList = await headers()
|
||||
const pathname = headersList.get('x-pathname') || ''
|
||||
|
||||
// Extract locale from pathname (e.g., /en/bible -> en)
|
||||
const segments = pathname.split('/')
|
||||
const locale = segments[1] || 'ro' // Default to Romanian
|
||||
|
||||
// Validate that the locale is supported
|
||||
const supportedLocales = ['en', 'ro']
|
||||
const finalLocale = supportedLocales.includes(locale) ? locale : 'ro'
|
||||
|
||||
return {
|
||||
locale: finalLocale,
|
||||
messages: (await import(`../messages/${finalLocale}.json`)).default
|
||||
}
|
||||
})
|
||||
78
lib/book-translations.ts
Normal file
78
lib/book-translations.ts
Normal file
@@ -0,0 +1,78 @@
|
||||
// Book name translations mapping
|
||||
export const bookTranslations: Record<string, { en: string; ro: string }> = {
|
||||
'Geneza': { en: 'Genesis', ro: 'Geneza' },
|
||||
'Exodul': { en: 'Exodus', ro: 'Exodul' },
|
||||
'Leviticul': { en: 'Leviticus', ro: 'Leviticul' },
|
||||
'Numerii': { en: 'Numbers', ro: 'Numerii' },
|
||||
'Deuteronomul': { en: 'Deuteronomy', ro: 'Deuteronomul' },
|
||||
'Iosua': { en: 'Joshua', ro: 'Iosua' },
|
||||
'Judecătorii': { en: 'Judges', ro: 'Judecătorii' },
|
||||
'Rut': { en: 'Ruth', ro: 'Rut' },
|
||||
'1 Samuel': { en: '1 Samuel', ro: '1 Samuel' },
|
||||
'2 Samuel': { en: '2 Samuel', ro: '2 Samuel' },
|
||||
'1 Împăraţi': { en: '1 Kings', ro: '1 Împăraţi' },
|
||||
'2 Împăraţi': { en: '2 Kings', ro: '2 Împăraţi' },
|
||||
'1 Cronici': { en: '1 Chronicles', ro: '1 Cronici' },
|
||||
'2 Cronici': { en: '2 Chronicles', ro: '2 Cronici' },
|
||||
'Ezra': { en: 'Ezra', ro: 'Ezra' },
|
||||
'Neemia': { en: 'Nehemiah', ro: 'Neemia' },
|
||||
'Estera': { en: 'Esther', ro: 'Estera' },
|
||||
'Iov': { en: 'Job', ro: 'Iov' },
|
||||
'Psalmii': { en: 'Psalms', ro: 'Psalmii' },
|
||||
'Proverbele': { en: 'Proverbs', ro: 'Proverbele' },
|
||||
'Ecclesiastul': { en: 'Ecclesiastes', ro: 'Ecclesiastul' },
|
||||
'Cântarea Cântărilor': { en: 'Song of Songs', ro: 'Cântarea Cântărilor' },
|
||||
'Isaia': { en: 'Isaiah', ro: 'Isaia' },
|
||||
'Ieremia': { en: 'Jeremiah', ro: 'Ieremia' },
|
||||
'Plângerile lui Ieremia': { en: 'Lamentations', ro: 'Plângerile lui Ieremia' },
|
||||
'Ezechiel': { en: 'Ezekiel', ro: 'Ezechiel' },
|
||||
'Daniel': { en: 'Daniel', ro: 'Daniel' },
|
||||
'Osea': { en: 'Hosea', ro: 'Osea' },
|
||||
'Ioel': { en: 'Joel', ro: 'Ioel' },
|
||||
'Amos': { en: 'Amos', ro: 'Amos' },
|
||||
'Obadia': { en: 'Obadiah', ro: 'Obadia' },
|
||||
'Iona': { en: 'Jonah', ro: 'Iona' },
|
||||
'Mica': { en: 'Micah', ro: 'Mica' },
|
||||
'Naum': { en: 'Nahum', ro: 'Naum' },
|
||||
'Habacuc': { en: 'Habakkuk', ro: 'Habacuc' },
|
||||
'Ţefania': { en: 'Zephaniah', ro: 'Ţefania' },
|
||||
'Hagai': { en: 'Haggai', ro: 'Hagai' },
|
||||
'Zaharia': { en: 'Zechariah', ro: 'Zaharia' },
|
||||
'Maleahi': { en: 'Malachi', ro: 'Maleahi' },
|
||||
'Matei': { en: 'Matthew', ro: 'Matei' },
|
||||
'Marcu': { en: 'Mark', ro: 'Marcu' },
|
||||
'Luca': { en: 'Luke', ro: 'Luca' },
|
||||
'Ioan': { en: 'John', ro: 'Ioan' },
|
||||
'Faptele Apostolilor': { en: 'Acts', ro: 'Faptele Apostolilor' },
|
||||
'Romani': { en: 'Romans', ro: 'Romani' },
|
||||
'1 Corinteni': { en: '1 Corinthians', ro: '1 Corinteni' },
|
||||
'2 Corinteni': { en: '2 Corinthians', ro: '2 Corinteni' },
|
||||
'Galateni': { en: 'Galatians', ro: 'Galateni' },
|
||||
'Efeseni': { en: 'Ephesians', ro: 'Efeseni' },
|
||||
'Filipeni': { en: 'Philippians', ro: 'Filipeni' },
|
||||
'Coloseni': { en: 'Colossians', ro: 'Coloseni' },
|
||||
'1 Tesaloniceni': { en: '1 Thessalonians', ro: '1 Tesaloniceni' },
|
||||
'2 Tesaloniceni': { en: '2 Thessalonians', ro: '2 Tesaloniceni' },
|
||||
'1 Timotei': { en: '1 Timothy', ro: '1 Timotei' },
|
||||
'2 Timotei': { en: '2 Timothy', ro: '2 Timotei' },
|
||||
'Tit': { en: 'Titus', ro: 'Tit' },
|
||||
'Filimon': { en: 'Philemon', ro: 'Filimon' },
|
||||
'Evrei': { en: 'Hebrews', ro: 'Evrei' },
|
||||
'Iacov': { en: 'James', ro: 'Iacov' },
|
||||
'1 Petru': { en: '1 Peter', ro: '1 Petru' },
|
||||
'2 Petru': { en: '2 Peter', ro: '2 Petru' },
|
||||
'1 Ioan': { en: '1 John', ro: '1 Ioan' },
|
||||
'2 Ioan': { en: '2 John', ro: '2 Ioan' },
|
||||
'3 Ioan': { en: '3 John', ro: '3 Ioan' },
|
||||
'Iuda': { en: 'Jude', ro: 'Iuda' },
|
||||
'Apocalipsa': { en: 'Revelation', ro: 'Apocalipsa' }
|
||||
}
|
||||
|
||||
export function translateBookName(bookName: string, locale: string): string {
|
||||
const translation = bookTranslations[bookName]
|
||||
if (translation) {
|
||||
return locale === 'en' ? translation.en : translation.ro
|
||||
}
|
||||
// If no translation found, return the original name
|
||||
return bookName
|
||||
}
|
||||
@@ -73,11 +73,21 @@
|
||||
},
|
||||
"pages": {
|
||||
"bible": {
|
||||
"title": "Bible",
|
||||
"title": "Read the Bible",
|
||||
"subtitle": "Explore Scripture with a modern and intuitive interface",
|
||||
"selectBook": "Select book",
|
||||
"selectChapter": "Select chapter",
|
||||
"book": "Book",
|
||||
"chapter": "Chapter",
|
||||
"verse": "Verse",
|
||||
"chapter": "Chapter"
|
||||
"verses": "verses",
|
||||
"save": "Save",
|
||||
"share": "Share",
|
||||
"previousChapter": "Previous chapter",
|
||||
"nextChapter": "Next chapter",
|
||||
"loading": "Loading verses...",
|
||||
"noVerses": "No verses found for this selection.",
|
||||
"startReading": "Start exploring Scripture"
|
||||
},
|
||||
"prayers": {
|
||||
"title": "Prayers",
|
||||
|
||||
@@ -73,11 +73,21 @@
|
||||
},
|
||||
"pages": {
|
||||
"bible": {
|
||||
"title": "Biblia",
|
||||
"title": "Citește Biblia",
|
||||
"subtitle": "Explorează Scriptura cu o interfață modernă și intuitivă",
|
||||
"selectBook": "Selectează cartea",
|
||||
"selectChapter": "Selectează capitolul",
|
||||
"book": "Cartea",
|
||||
"chapter": "Capitolul",
|
||||
"verse": "Versetul",
|
||||
"chapter": "Capitolul"
|
||||
"verses": "versete",
|
||||
"save": "Salvează",
|
||||
"share": "Partajează",
|
||||
"previousChapter": "Capitolul anterior",
|
||||
"nextChapter": "Capitolul următor",
|
||||
"loading": "Se încarcă versetele...",
|
||||
"noVerses": "Nu s-au găsit versete pentru această selecție.",
|
||||
"startReading": "Începe să explorezi Scriptura"
|
||||
},
|
||||
"prayers": {
|
||||
"title": "Rugăciuni",
|
||||
|
||||
135
middleware.ts
135
middleware.ts
@@ -1,7 +1,6 @@
|
||||
import { NextResponse } from 'next/server'
|
||||
import type { NextRequest } from 'next/server'
|
||||
import { verifyToken } from '@/lib/auth'
|
||||
import { prisma } from '@/lib/db'
|
||||
// Avoid importing Node-only modules in Middleware (Edge runtime)
|
||||
import createIntlMiddleware from 'next-intl/middleware'
|
||||
|
||||
// Internationalization configuration
|
||||
@@ -10,59 +9,10 @@ const intlMiddleware = createIntlMiddleware({
|
||||
defaultLocale: 'ro'
|
||||
})
|
||||
|
||||
// Rate limiting configuration
|
||||
const RATE_LIMIT_WINDOW = 60 * 1000 // 1 minute
|
||||
const RATE_LIMITS = {
|
||||
general: 100, // 100 requests per minute
|
||||
auth: 5, // 5 auth requests per minute
|
||||
chat: 10, // 10 chat requests per minute
|
||||
search: 20, // 20 search requests per minute
|
||||
}
|
||||
|
||||
async function getRateLimitKey(request: NextRequest, endpoint: string): Promise<string> {
|
||||
const forwarded = request.headers.get('x-forwarded-for')
|
||||
const ip = forwarded ? forwarded.split(',')[0] : request.ip || 'unknown'
|
||||
return `ratelimit:${endpoint}:${ip}`
|
||||
}
|
||||
|
||||
async function checkRateLimit(request: NextRequest, endpoint: string, limit: number): Promise<{ allowed: boolean; remaining: number }> {
|
||||
try {
|
||||
const key = await getRateLimitKey(request, endpoint)
|
||||
const now = Date.now()
|
||||
const windowStart = now - RATE_LIMIT_WINDOW
|
||||
|
||||
// Clean up old entries and count current requests
|
||||
await prisma.$executeRaw`
|
||||
DELETE FROM verse_cache
|
||||
WHERE key LIKE ${key + ':%'}
|
||||
AND created_at < ${new Date(windowStart)}
|
||||
`
|
||||
|
||||
const currentCount = await prisma.$queryRaw<{ count: number }[]>`
|
||||
SELECT COUNT(*) as count
|
||||
FROM verse_cache
|
||||
WHERE key LIKE ${key + ':%'}
|
||||
`
|
||||
|
||||
const requestCount = Number(currentCount[0]?.count || 0)
|
||||
|
||||
if (requestCount >= limit) {
|
||||
return { allowed: false, remaining: 0 }
|
||||
}
|
||||
|
||||
// Record this request
|
||||
await prisma.$executeRaw`
|
||||
INSERT INTO verse_cache (key, value, expires_at, created_at)
|
||||
VALUES (${key + ':' + now}, '1', ${new Date(now + RATE_LIMIT_WINDOW)}, ${new Date(now)})
|
||||
`
|
||||
|
||||
return { allowed: true, remaining: limit - requestCount - 1 }
|
||||
} catch (error) {
|
||||
console.error('Rate limit check error:', error)
|
||||
// Allow request on error
|
||||
return { allowed: true, remaining: limit }
|
||||
}
|
||||
}
|
||||
// Note: Avoid using Prisma or any Node-only APIs in Middleware.
|
||||
// Middleware runs on the Edge runtime, where Prisma is not supported.
|
||||
// If rate limiting is needed, implement it inside API route handlers
|
||||
// (Node.js runtime) or via an external service (e.g., Upstash Redis).
|
||||
|
||||
export async function middleware(request: NextRequest) {
|
||||
// Handle internationalization for non-API routes
|
||||
@@ -70,38 +20,7 @@ export async function middleware(request: NextRequest) {
|
||||
return intlMiddleware(request)
|
||||
}
|
||||
|
||||
// Determine endpoint type for rate limiting
|
||||
let endpoint = 'general'
|
||||
let limit = RATE_LIMITS.general
|
||||
|
||||
if (request.nextUrl.pathname.startsWith('/api/auth')) {
|
||||
endpoint = 'auth'
|
||||
limit = RATE_LIMITS.auth
|
||||
} else if (request.nextUrl.pathname.startsWith('/api/chat')) {
|
||||
endpoint = 'chat'
|
||||
limit = RATE_LIMITS.chat
|
||||
} else if (request.nextUrl.pathname.startsWith('/api/bible/search')) {
|
||||
endpoint = 'search'
|
||||
limit = RATE_LIMITS.search
|
||||
}
|
||||
|
||||
// Apply rate limiting to API routes
|
||||
if (request.nextUrl.pathname.startsWith('/api/')) {
|
||||
const rateLimit = await checkRateLimit(request, endpoint, limit)
|
||||
|
||||
const response = rateLimit.allowed
|
||||
? NextResponse.next()
|
||||
: new NextResponse('Too Many Requests', { status: 429 })
|
||||
|
||||
// Add rate limit headers
|
||||
response.headers.set('X-RateLimit-Limit', limit.toString())
|
||||
response.headers.set('X-RateLimit-Remaining', rateLimit.remaining.toString())
|
||||
response.headers.set('X-RateLimit-Reset', (Date.now() + RATE_LIMIT_WINDOW).toString())
|
||||
|
||||
if (!rateLimit.allowed) {
|
||||
return response
|
||||
}
|
||||
}
|
||||
// Skip API rate limiting here to stay Edge-safe
|
||||
|
||||
// Security headers for all responses
|
||||
const response = NextResponse.next()
|
||||
@@ -123,42 +42,12 @@ export async function middleware(request: NextRequest) {
|
||||
}
|
||||
}
|
||||
|
||||
// Authentication check for protected routes
|
||||
if (request.nextUrl.pathname.startsWith('/api/bookmarks') ||
|
||||
request.nextUrl.pathname.startsWith('/api/notes') ||
|
||||
request.nextUrl.pathname.startsWith('/dashboard')) {
|
||||
|
||||
const token = request.headers.get('authorization')?.replace('Bearer ', '') ||
|
||||
request.cookies.get('authToken')?.value
|
||||
|
||||
// Authentication: perform only lightweight checks in Middleware (Edge).
|
||||
// Defer full JWT verification to API route handlers (Node runtime).
|
||||
if (request.nextUrl.pathname.startsWith('/dashboard')) {
|
||||
const token = request.cookies.get('authToken')?.value
|
||||
if (!token) {
|
||||
if (request.nextUrl.pathname.startsWith('/api/')) {
|
||||
return new NextResponse('Unauthorized', { status: 401 })
|
||||
} else {
|
||||
return NextResponse.redirect(new URL('/', request.url))
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const payload = await verifyToken(token)
|
||||
|
||||
// Add user ID to headers for API routes
|
||||
if (request.nextUrl.pathname.startsWith('/api/')) {
|
||||
const requestHeaders = new Headers(request.headers)
|
||||
requestHeaders.set('x-user-id', payload.userId)
|
||||
|
||||
return NextResponse.next({
|
||||
request: {
|
||||
headers: requestHeaders,
|
||||
},
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
if (request.nextUrl.pathname.startsWith('/api/')) {
|
||||
return new NextResponse('Invalid token', { status: 401 })
|
||||
} else {
|
||||
return NextResponse.redirect(new URL('/', request.url))
|
||||
}
|
||||
return NextResponse.redirect(new URL('/', request.url))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -176,4 +65,4 @@ export const config = {
|
||||
'/api/:path*',
|
||||
'/dashboard/:path*'
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,19 +40,43 @@ model Session {
|
||||
@@index([token])
|
||||
}
|
||||
|
||||
model BibleVersion {
|
||||
id String @id @default(uuid())
|
||||
name String // e.g., "King James Version", "Cornilescu"
|
||||
abbreviation String // e.g., "KJV", "CORNILESCU", "NIV"
|
||||
language String // e.g., "en", "ro", "es"
|
||||
description String?
|
||||
isDefault Boolean @default(false)
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
books BibleBook[]
|
||||
|
||||
@@unique([abbreviation, language])
|
||||
@@index([language])
|
||||
@@index([isDefault])
|
||||
}
|
||||
|
||||
model BibleBook {
|
||||
id Int @id
|
||||
name String
|
||||
id String @id @default(uuid())
|
||||
versionId String
|
||||
name String // Version-specific book name
|
||||
testament String
|
||||
orderNum Int
|
||||
bookKey String // For cross-version matching (e.g., "genesis", "exodus")
|
||||
chapters BibleChapter[]
|
||||
|
||||
version BibleVersion @relation(fields: [versionId], references: [id])
|
||||
|
||||
@@unique([versionId, orderNum])
|
||||
@@unique([versionId, bookKey])
|
||||
@@index([versionId])
|
||||
@@index([testament])
|
||||
}
|
||||
|
||||
model BibleChapter {
|
||||
id String @id @default(uuid())
|
||||
bookId Int
|
||||
bookId String
|
||||
chapterNum Int
|
||||
verses BibleVerse[]
|
||||
|
||||
@@ -67,15 +91,13 @@ model BibleVerse {
|
||||
chapterId String
|
||||
verseNum Int
|
||||
text String @db.Text
|
||||
version String @default("KJV")
|
||||
|
||||
chapter BibleChapter @relation(fields: [chapterId], references: [id])
|
||||
bookmarks Bookmark[]
|
||||
notes Note[]
|
||||
|
||||
@@unique([chapterId, verseNum, version])
|
||||
@@unique([chapterId, verseNum])
|
||||
@@index([chapterId])
|
||||
@@index([version])
|
||||
}
|
||||
|
||||
model BiblePassage {
|
||||
@@ -89,7 +111,7 @@ model BiblePassage {
|
||||
translation String @default("FIDELA")
|
||||
textRaw String @db.Text
|
||||
textNorm String @db.Text // Normalized text for embedding
|
||||
embedding Unsupported("vector(3072)")?
|
||||
embedding String? // Will be changed to vector later when extension is available
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
@@ -170,7 +192,7 @@ model Prayer {
|
||||
model ReadingHistory {
|
||||
id String @id @default(uuid())
|
||||
userId String
|
||||
bookId Int
|
||||
bookId String
|
||||
chapterNum Int
|
||||
verseNum Int?
|
||||
viewedAt DateTime @default(now())
|
||||
|
||||
227
scripts/convert-fidela-md-to-json.ts
Normal file
227
scripts/convert-fidela-md-to-json.ts
Normal file
@@ -0,0 +1,227 @@
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
|
||||
type Verse = { verseNum: number; text: string }
|
||||
type Chapter = { chapterNum: number; verses: Verse[] }
|
||||
type Book = { name: string; chapters: Chapter[] }
|
||||
|
||||
// Mapping to determine testament + order, reused to split OT / NT
|
||||
const BOOK_MAPPINGS: Record<string, { testament: 'Old Testament' | 'New Testament'; orderNum: number }> = {
|
||||
// Old Testament
|
||||
'Geneza': { testament: 'Old Testament', orderNum: 1 },
|
||||
'Exodul': { testament: 'Old Testament', orderNum: 2 },
|
||||
'Leviticul': { testament: 'Old Testament', orderNum: 3 },
|
||||
'Numerii': { testament: 'Old Testament', orderNum: 4 },
|
||||
'Numeri': { testament: 'Old Testament', orderNum: 4 },
|
||||
'Deuteronomul': { testament: 'Old Testament', orderNum: 5 },
|
||||
'Deuteronom': { testament: 'Old Testament', orderNum: 5 },
|
||||
'Iosua': { testament: 'Old Testament', orderNum: 6 },
|
||||
'Judecătorii': { testament: 'Old Testament', orderNum: 7 },
|
||||
'Judecători': { testament: 'Old Testament', orderNum: 7 },
|
||||
'Rut': { testament: 'Old Testament', orderNum: 8 },
|
||||
'1 Samuel': { testament: 'Old Testament', orderNum: 9 },
|
||||
'2 Samuel': { testament: 'Old Testament', orderNum: 10 },
|
||||
'1 Împăraţi': { testament: 'Old Testament', orderNum: 11 },
|
||||
'2 Împăraţi': { testament: 'Old Testament', orderNum: 12 },
|
||||
'1 Imparati': { testament: 'Old Testament', orderNum: 11 },
|
||||
'2 Imparati': { testament: 'Old Testament', orderNum: 12 },
|
||||
'1 Cronici': { testament: 'Old Testament', orderNum: 13 },
|
||||
'2 Cronici': { testament: 'Old Testament', orderNum: 14 },
|
||||
'Ezra': { testament: 'Old Testament', orderNum: 15 },
|
||||
'Neemia': { testament: 'Old Testament', orderNum: 16 },
|
||||
'Estera': { testament: 'Old Testament', orderNum: 17 },
|
||||
'Iov': { testament: 'Old Testament', orderNum: 18 },
|
||||
'Psalmii': { testament: 'Old Testament', orderNum: 19 },
|
||||
'Proverbele': { testament: 'Old Testament', orderNum: 20 },
|
||||
'Proverbe': { testament: 'Old Testament', orderNum: 20 },
|
||||
'Eclesiastul': { testament: 'Old Testament', orderNum: 21 },
|
||||
'Cântarea Cântărilor': { testament: 'Old Testament', orderNum: 22 },
|
||||
'Isaia': { testament: 'Old Testament', orderNum: 23 },
|
||||
'Ieremia': { testament: 'Old Testament', orderNum: 24 },
|
||||
'Plângerile': { testament: 'Old Testament', orderNum: 25 },
|
||||
'Ezechiel': { testament: 'Old Testament', orderNum: 26 },
|
||||
'Daniel': { testament: 'Old Testament', orderNum: 27 },
|
||||
'Osea': { testament: 'Old Testament', orderNum: 28 },
|
||||
'Ioel': { testament: 'Old Testament', orderNum: 29 },
|
||||
'Amos': { testament: 'Old Testament', orderNum: 30 },
|
||||
'Obadia': { testament: 'Old Testament', orderNum: 31 },
|
||||
'Iona': { testament: 'Old Testament', orderNum: 32 },
|
||||
'Mica': { testament: 'Old Testament', orderNum: 33 },
|
||||
'Naum': { testament: 'Old Testament', orderNum: 34 },
|
||||
'Habacuc': { testament: 'Old Testament', orderNum: 35 },
|
||||
'Ţefania': { testament: 'Old Testament', orderNum: 36 },
|
||||
'Țefania': { testament: 'Old Testament', orderNum: 36 },
|
||||
'Hagai': { testament: 'Old Testament', orderNum: 37 },
|
||||
'Zaharia': { testament: 'Old Testament', orderNum: 38 },
|
||||
'Maleahi': { testament: 'Old Testament', orderNum: 39 },
|
||||
// New Testament
|
||||
'Matei': { testament: 'New Testament', orderNum: 40 },
|
||||
'Marcu': { testament: 'New Testament', orderNum: 41 },
|
||||
'Luca': { testament: 'New Testament', orderNum: 42 },
|
||||
'Ioan': { testament: 'New Testament', orderNum: 43 },
|
||||
'Faptele Apostolilor': { testament: 'New Testament', orderNum: 44 },
|
||||
'Romani': { testament: 'New Testament', orderNum: 45 },
|
||||
'1 Corinteni': { testament: 'New Testament', orderNum: 46 },
|
||||
'2 Corinteni': { testament: 'New Testament', orderNum: 47 },
|
||||
'Galateni': { testament: 'New Testament', orderNum: 48 },
|
||||
'Efeseni': { testament: 'New Testament', orderNum: 49 },
|
||||
'Filipeni': { testament: 'New Testament', orderNum: 50 },
|
||||
'Coloseni': { testament: 'New Testament', orderNum: 51 },
|
||||
'1 Tesaloniceni': { testament: 'New Testament', orderNum: 52 },
|
||||
'2 Tesaloniceni': { testament: 'New Testament', orderNum: 53 },
|
||||
'1 Timotei': { testament: 'New Testament', orderNum: 54 },
|
||||
'2 Timotei': { testament: 'New Testament', orderNum: 55 },
|
||||
'Tit': { testament: 'New Testament', orderNum: 56 },
|
||||
'Titus': { testament: 'New Testament', orderNum: 56 },
|
||||
'Filimon': { testament: 'New Testament', orderNum: 57 },
|
||||
'Evrei': { testament: 'New Testament', orderNum: 58 },
|
||||
'Iacov': { testament: 'New Testament', orderNum: 59 },
|
||||
'1 Petru': { testament: 'New Testament', orderNum: 60 },
|
||||
'2 Petru': { testament: 'New Testament', orderNum: 61 },
|
||||
'1 Ioan': { testament: 'New Testament', orderNum: 62 },
|
||||
'2 Ioan': { testament: 'New Testament', orderNum: 63 },
|
||||
'3 Ioan': { testament: 'New Testament', orderNum: 64 },
|
||||
'Iuda': { testament: 'New Testament', orderNum: 65 },
|
||||
'Apocalipsa': { testament: 'New Testament', orderNum: 66 },
|
||||
'Revelaţia': { testament: 'New Testament', orderNum: 66 },
|
||||
'Revelația': { testament: 'New Testament', orderNum: 66 }
|
||||
}
|
||||
|
||||
function parseRomanianBibleMarkdown(md: string): Book[] {
|
||||
const lines = md.split(/\r?\n/)
|
||||
|
||||
const books: Book[] = []
|
||||
let currentBook: Book | null = null
|
||||
let currentChapter: Chapter | null = null
|
||||
|
||||
// The MD file uses a pattern of a centered title line with ellipses like: … GENEZA …
|
||||
// We detect those as book delimiters.
|
||||
const isBookHeader = (line: string) => /^…\s*.+\s*…$/.test(line.trim())
|
||||
|
||||
const normalizeBookName = (raw: string) => {
|
||||
// Strip leading/trailing ellipsis and extra spaces
|
||||
const name = raw.replace(/^…\s*|\s*…$/g, '').trim()
|
||||
// Try to map known variants to our mapping keys
|
||||
// The MD sometimes uses slightly different diacritics or spacing; leave as-is if not found
|
||||
return name
|
||||
}
|
||||
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const raw = lines[i].trim()
|
||||
if (!raw) continue
|
||||
|
||||
if (isBookHeader(raw)) {
|
||||
// Save previous chapter and book if they have content
|
||||
if (currentChapter && currentChapter.verses.length > 0 && currentBook) {
|
||||
currentBook.chapters.push(currentChapter)
|
||||
}
|
||||
if (currentBook && currentBook.chapters.length > 0) {
|
||||
books.push(currentBook)
|
||||
}
|
||||
|
||||
const bookName = normalizeBookName(raw)
|
||||
currentBook = { name: bookName, chapters: [] }
|
||||
currentChapter = null
|
||||
continue
|
||||
}
|
||||
|
||||
// Detect chapter markers like: Capitolul X (case-insensitive, diacritics tolerant)
|
||||
const chapterMatch = raw.match(/^[cC][aA][pP][iI][tT][oO][lL][uU][lL]\s+(\d+)$/)
|
||||
if (chapterMatch && currentBook) {
|
||||
// Save previous chapter if exists
|
||||
if (currentChapter && currentChapter.verses.length > 0) {
|
||||
currentBook.chapters.push(currentChapter)
|
||||
}
|
||||
currentChapter = { chapterNum: parseInt(chapterMatch[1], 10), verses: [] }
|
||||
continue
|
||||
}
|
||||
|
||||
// Detect verses that begin with a number: "1 text..."
|
||||
const verseMatch = raw.match(/^(\d+)\s+(.+)$/)
|
||||
if (verseMatch && currentChapter) {
|
||||
const verseNum = parseInt(verseMatch[1], 10)
|
||||
let verseText = verseMatch[2].trim()
|
||||
|
||||
// Remove paragraph markers if present
|
||||
verseText = verseText.replace(/^¶\s*/, '')
|
||||
|
||||
// Merge continuation lines that don't start with a verse or chapter/book markers
|
||||
let j = i + 1
|
||||
while (j < lines.length) {
|
||||
const lookahead = lines[j].trim()
|
||||
if (!lookahead) break
|
||||
if (/^(\d+)\s+/.test(lookahead)) break // next verse
|
||||
if (isBookHeader(lookahead)) break // next book
|
||||
if (/^[cC][aA][pP][iI][tT][oO][lL][uU][lL]\s+\d+$/.test(lookahead)) break // next chapter
|
||||
verseText += ' ' + lookahead
|
||||
j++
|
||||
}
|
||||
i = j - 1
|
||||
|
||||
verseText = verseText.replace(/\s+/g, ' ').trim()
|
||||
if (verseText) {
|
||||
currentChapter.verses.push({ verseNum, text: verseText })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Save last chapter and book
|
||||
if (currentChapter && currentChapter.verses.length > 0 && currentBook) {
|
||||
currentBook.chapters.push(currentChapter)
|
||||
}
|
||||
if (currentBook && currentBook.chapters.length > 0) {
|
||||
books.push(currentBook)
|
||||
}
|
||||
|
||||
return books
|
||||
}
|
||||
|
||||
function writeTestamentJson(books: Book[], outPath: string, testament: 'Old Testament' | 'New Testament') {
|
||||
const data = {
|
||||
testament,
|
||||
books: books.map(b => ({ name: b.name, chapters: b.chapters }))
|
||||
}
|
||||
fs.mkdirSync(path.dirname(outPath), { recursive: true })
|
||||
fs.writeFileSync(outPath, JSON.stringify(data, null, 2), 'utf-8')
|
||||
console.log(`Wrote ${outPath} with ${books.length} books.`)
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const mdPath = path.join(process.cwd(), 'bibles', 'Biblia-Fidela-limba-romana.md')
|
||||
if (!fs.existsSync(mdPath)) {
|
||||
throw new Error(`Markdown not found at ${mdPath}`)
|
||||
}
|
||||
|
||||
console.log(`Reading: ${mdPath}`)
|
||||
const md = fs.readFileSync(mdPath, 'utf-8')
|
||||
const parsedBooks = parseRomanianBibleMarkdown(md)
|
||||
console.log(`Parsed ${parsedBooks.length} books from Markdown`)
|
||||
|
||||
// Filter and sort into OT and NT using the mapping
|
||||
const otBooks: Book[] = []
|
||||
const ntBooks: Book[] = []
|
||||
for (const b of parsedBooks) {
|
||||
const meta = BOOK_MAPPINGS[b.name]
|
||||
if (!meta) {
|
||||
console.warn(`Skipping unknown book in mapping: ${b.name}`)
|
||||
continue
|
||||
}
|
||||
if (meta.testament === 'Old Testament') otBooks.push(b)
|
||||
else ntBooks.push(b)
|
||||
}
|
||||
|
||||
// Sort by canonical order
|
||||
otBooks.sort((a, b) => BOOK_MAPPINGS[a.name].orderNum - BOOK_MAPPINGS[b.name].orderNum)
|
||||
ntBooks.sort((a, b) => BOOK_MAPPINGS[a.name].orderNum - BOOK_MAPPINGS[b.name].orderNum)
|
||||
|
||||
// Write JSON outputs compatible with import-romanian-versioned.ts
|
||||
const otOut = path.join(process.cwd(), 'data', 'old_testament.json')
|
||||
const ntOut = path.join(process.cwd(), 'data', 'new_testament.json')
|
||||
writeTestamentJson(otBooks, otOut, 'Old Testament')
|
||||
writeTestamentJson(ntBooks, ntOut, 'New Testament')
|
||||
}
|
||||
|
||||
main().catch(err => {
|
||||
console.error('Conversion failed:', err)
|
||||
process.exit(1)
|
||||
})
|
||||
@@ -123,7 +123,7 @@ async function importFromApiBible() {
|
||||
|
||||
let totalVersesImported = 0
|
||||
|
||||
for (const book of books.slice(0, 2)) { // Limit to first 2 books for sample structure
|
||||
for (const book of books.slice(0, 10)) { // Import first 10 books
|
||||
console.log(`Processing ${book.name} (${book.id})...`)
|
||||
|
||||
const orderNum = getBookOrderNumber(book.id)
|
||||
@@ -150,7 +150,7 @@ async function importFromApiBible() {
|
||||
|
||||
console.log(` Found ${chapters.length} chapters`)
|
||||
|
||||
for (const chapter of chapters.slice(0, 2)) { // Limit to first 2 chapters for sample
|
||||
for (const chapter of chapters.slice(0, 5)) { // Import first 5 chapters
|
||||
const chapterNum = parseChapterNumber(chapter.id)
|
||||
|
||||
console.log(` Processing chapter ${chapterNum}...`)
|
||||
@@ -175,10 +175,10 @@ async function importFromApiBible() {
|
||||
|
||||
console.log(` Found ${versesResponse.data.length} verses`)
|
||||
|
||||
// Process only first 5 verses for sample structure
|
||||
const sampleVerses = versesResponse.data.slice(0, 5)
|
||||
for (let i = 0; i < sampleVerses.length; i += 5) {
|
||||
const verseBatch = sampleVerses.slice(i, i + 5)
|
||||
// Process all verses
|
||||
const allVerses = versesResponse.data
|
||||
for (let i = 0; i < allVerses.length; i += 10) {
|
||||
const verseBatch = allVerses.slice(i, i + 10)
|
||||
|
||||
for (const verseRef of verseBatch) {
|
||||
try {
|
||||
|
||||
286
scripts/import-english-versioned.ts
Normal file
286
scripts/import-english-versioned.ts
Normal file
@@ -0,0 +1,286 @@
|
||||
import { PrismaClient } from '@prisma/client'
|
||||
|
||||
const prisma = new PrismaClient()
|
||||
|
||||
interface ApiBibleBook {
|
||||
id: string
|
||||
bibleId: string
|
||||
abbreviation: string
|
||||
name: string
|
||||
nameLong: string
|
||||
}
|
||||
|
||||
interface ApiBibleChapter {
|
||||
id: string
|
||||
bibleId: string
|
||||
bookId: string
|
||||
number: string
|
||||
reference: string
|
||||
}
|
||||
|
||||
interface ApiBibleVerse {
|
||||
id: string
|
||||
orgId: string
|
||||
bookId: string
|
||||
chapterId: string
|
||||
bibleId: string
|
||||
reference: string
|
||||
content: string
|
||||
verseCount: number
|
||||
}
|
||||
|
||||
interface ApiBibleResponse<T> {
|
||||
data: T
|
||||
}
|
||||
|
||||
const API_KEY = process.env.API_BIBLE_KEY || '7b42606f8f809e155c9b0742c4f1849b'
|
||||
const API_BASE = 'https://api.scripture.api.bible/v1'
|
||||
const BIBLE_ID = 'bba9f40183526463-01' // Berean Standard Bible
|
||||
|
||||
async function apiFetch<T>(endpoint: string): Promise<T> {
|
||||
const response = await fetch(`${API_BASE}${endpoint}`, {
|
||||
headers: {
|
||||
'api-key': API_KEY
|
||||
}
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`API request failed: ${response.status} ${response.statusText}`)
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
return data
|
||||
}
|
||||
|
||||
function cleanHtmlContent(htmlContent: string): string {
|
||||
return htmlContent
|
||||
.replace(/<[^>]*>/g, '') // Remove HTML tags
|
||||
.replace(/\s+/g, ' ') // Normalize whitespace
|
||||
.replace(/^\d+\s*/, '') // Remove verse numbers at start
|
||||
.trim()
|
||||
}
|
||||
|
||||
function parseVerseNumber(verseId: string): number {
|
||||
const parts = verseId.split('.')
|
||||
return parseInt(parts[2]) || 1
|
||||
}
|
||||
|
||||
function parseChapterNumber(chapterId: string): number {
|
||||
const parts = chapterId.split('.')
|
||||
return parseInt(parts[1]) || 1
|
||||
}
|
||||
|
||||
function getTestament(bookId: string): string {
|
||||
const oldTestamentBooks = [
|
||||
'GEN', 'EXO', 'LEV', 'NUM', 'DEU', 'JOS', 'JDG', 'RUT',
|
||||
'1SA', '2SA', '1KI', '2KI', '1CH', '2CH', 'EZR', 'NEH',
|
||||
'EST', 'JOB', 'PSA', 'PRO', 'ECC', 'SNG', 'ISA', 'JER',
|
||||
'LAM', 'EZK', 'DAN', 'HOS', 'JOL', 'AMO', 'OBA', 'JON',
|
||||
'MIC', 'NAM', 'HAB', 'ZEP', 'HAG', 'ZEC', 'MAL'
|
||||
]
|
||||
|
||||
return oldTestamentBooks.includes(bookId) ? 'Old Testament' : 'New Testament'
|
||||
}
|
||||
|
||||
function getBookOrderNumber(bookId: string): number {
|
||||
const bookOrder: Record<string, number> = {
|
||||
// Old Testament
|
||||
'GEN': 1, 'EXO': 2, 'LEV': 3, 'NUM': 4, 'DEU': 5, 'JOS': 6, 'JDG': 7, 'RUT': 8,
|
||||
'1SA': 9, '2SA': 10, '1KI': 11, '2KI': 12, '1CH': 13, '2CH': 14, 'EZR': 15, 'NEH': 16,
|
||||
'EST': 17, 'JOB': 18, 'PSA': 19, 'PRO': 20, 'ECC': 21, 'SNG': 22, 'ISA': 23, 'JER': 24,
|
||||
'LAM': 25, 'EZK': 26, 'DAN': 27, 'HOS': 28, 'JOL': 29, 'AMO': 30, 'OBA': 31, 'JON': 32,
|
||||
'MIC': 33, 'NAM': 34, 'HAB': 35, 'ZEP': 36, 'HAG': 37, 'ZEC': 38, 'MAL': 39,
|
||||
// New Testament
|
||||
'MAT': 40, 'MRK': 41, 'LUK': 42, 'JHN': 43, 'ACT': 44, 'ROM': 45, '1CO': 46, '2CO': 47,
|
||||
'GAL': 48, 'EPH': 49, 'PHP': 50, 'COL': 51, '1TH': 52, '2TH': 53, '1TI': 54, '2TI': 55,
|
||||
'TIT': 56, 'PHM': 57, 'HEB': 58, 'JAS': 59, '1PE': 60, '2PE': 61, '1JN': 62, '2JN': 63,
|
||||
'3JN': 64, 'JUD': 65, 'REV': 66
|
||||
}
|
||||
|
||||
return bookOrder[bookId] || 999
|
||||
}
|
||||
|
||||
function getBookKey(bookId: string): string {
|
||||
const keyMap: Record<string, string> = {
|
||||
'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', 'DAN': 'daniel',
|
||||
'HOS': 'hosea', 'JOL': 'joel', 'AMO': 'amos', 'OBA': 'obadiah', 'JON': 'jonah',
|
||||
'MIC': 'micah', 'NAM': 'nahum', 'HAB': 'habakkuk', 'ZEP': 'zephaniah',
|
||||
'HAG': 'haggai', 'ZEC': 'zechariah', 'MAL': 'malachi',
|
||||
'MAT': 'matthew', 'MRK': 'mark', 'LUK': 'luke', 'JHN': 'john', 'ACT': 'acts',
|
||||
'ROM': 'romans', '1CO': '1_corinthians', '2CO': '2_corinthians', 'GAL': 'galatians',
|
||||
'EPH': 'ephesians', 'PHP': 'philippians', 'COL': 'colossians', '1TH': '1_thessalonians',
|
||||
'2TH': '2_thessalonians', '1TI': '1_timothy', '2TI': '2_timothy', 'TIT': 'titus',
|
||||
'PHM': 'philemon', 'HEB': 'hebrews', 'JAS': 'james', '1PE': '1_peter', '2PE': '2_peter',
|
||||
'1JN': '1_john', '2JN': '2_john', '3JN': '3_john', 'JUD': 'jude', 'REV': 'revelation'
|
||||
}
|
||||
|
||||
return keyMap[bookId] || bookId.toLowerCase()
|
||||
}
|
||||
|
||||
async function importEnglishBible() {
|
||||
console.log('Starting English Bible import with versioned schema...')
|
||||
|
||||
try {
|
||||
// Step 1: Create English Bible version
|
||||
console.log('Creating English Bible version...')
|
||||
const englishVersion = await prisma.bibleVersion.upsert({
|
||||
where: {
|
||||
abbreviation_language: {
|
||||
abbreviation: 'BSB',
|
||||
language: 'en'
|
||||
}
|
||||
},
|
||||
update: {},
|
||||
create: {
|
||||
name: 'Berean Standard Bible',
|
||||
abbreviation: 'BSB',
|
||||
language: 'en',
|
||||
description: 'The Berean Standard Bible in English',
|
||||
isDefault: true
|
||||
}
|
||||
})
|
||||
|
||||
console.log(`Created English version: ${englishVersion.id}`)
|
||||
|
||||
// Step 2: Get all books for the Bible
|
||||
console.log('Fetching books from API.Bible...')
|
||||
const booksResponse = await apiFetch<ApiBibleResponse<ApiBibleBook[]>>(`/bibles/${BIBLE_ID}/books`)
|
||||
const books = booksResponse.data.filter(book =>
|
||||
book.id !== 'INT' && // Skip introduction
|
||||
!book.id.includes('intro') // Skip intro chapters
|
||||
)
|
||||
|
||||
console.log(`Found ${books.length} books`)
|
||||
|
||||
let totalVersesImported = 0
|
||||
|
||||
// Import first 5 books to respect API rate limits
|
||||
for (const book of books.slice(0, 5)) {
|
||||
console.log(`Processing ${book.name} (${book.id})...`)
|
||||
|
||||
const orderNum = getBookOrderNumber(book.id)
|
||||
const testament = getTestament(book.id)
|
||||
const bookKey = getBookKey(book.id)
|
||||
|
||||
// Create or update book
|
||||
const createdBook = await prisma.bibleBook.upsert({
|
||||
where: {
|
||||
versionId_orderNum: {
|
||||
versionId: englishVersion.id,
|
||||
orderNum: orderNum
|
||||
}
|
||||
},
|
||||
update: {},
|
||||
create: {
|
||||
versionId: englishVersion.id,
|
||||
name: book.name,
|
||||
testament: testament,
|
||||
orderNum: orderNum,
|
||||
bookKey: bookKey
|
||||
}
|
||||
})
|
||||
|
||||
// Get chapters for this book
|
||||
const chaptersResponse = await apiFetch<ApiBibleResponse<ApiBibleChapter[]>>(`/bibles/${BIBLE_ID}/books/${book.id}/chapters`)
|
||||
const chapters = chaptersResponse.data.filter(chapter =>
|
||||
chapter.number !== 'intro' && // Skip introduction chapters
|
||||
!isNaN(parseInt(chapter.number)) // Only numeric chapters
|
||||
)
|
||||
|
||||
console.log(` Found ${chapters.length} chapters`)
|
||||
|
||||
// Import first 3 chapters to respect API rate limits
|
||||
for (const chapter of chapters.slice(0, 3)) {
|
||||
const chapterNum = parseChapterNumber(chapter.id)
|
||||
|
||||
console.log(` Processing chapter ${chapterNum}...`)
|
||||
|
||||
// Create or update chapter
|
||||
const createdChapter = await prisma.bibleChapter.upsert({
|
||||
where: {
|
||||
bookId_chapterNum: {
|
||||
bookId: createdBook.id,
|
||||
chapterNum: chapterNum
|
||||
}
|
||||
},
|
||||
update: {},
|
||||
create: {
|
||||
bookId: createdBook.id,
|
||||
chapterNum: chapterNum
|
||||
}
|
||||
})
|
||||
|
||||
// Get verses for this chapter
|
||||
const versesResponse = await apiFetch<ApiBibleResponse<ApiBibleVerse[]>>(`/bibles/${BIBLE_ID}/chapters/${chapter.id}/verses`)
|
||||
|
||||
console.log(` Found ${versesResponse.data.length} verses`)
|
||||
|
||||
// Process verses in smaller batches to respect API rate limits
|
||||
const limitedVerses = versesResponse.data.slice(0, 15) // Limit to first 15 verses per chapter
|
||||
for (let i = 0; i < limitedVerses.length; i += 5) {
|
||||
const verseBatch = limitedVerses.slice(i, i + 5)
|
||||
|
||||
for (const verseRef of verseBatch) {
|
||||
try {
|
||||
// Get full verse content
|
||||
const verseResponse = await apiFetch<ApiBibleResponse<ApiBibleVerse>>(`/bibles/${BIBLE_ID}/verses/${verseRef.id}`)
|
||||
const verse = verseResponse.data
|
||||
|
||||
const verseNum = parseVerseNumber(verse.id)
|
||||
const cleanText = cleanHtmlContent(verse.content)
|
||||
|
||||
if (cleanText.length > 0) {
|
||||
// Create or update verse
|
||||
await prisma.bibleVerse.upsert({
|
||||
where: {
|
||||
chapterId_verseNum: {
|
||||
chapterId: createdChapter.id,
|
||||
verseNum: verseNum
|
||||
}
|
||||
},
|
||||
update: { text: cleanText },
|
||||
create: {
|
||||
chapterId: createdChapter.id,
|
||||
verseNum: verseNum,
|
||||
text: cleanText
|
||||
}
|
||||
})
|
||||
|
||||
totalVersesImported++
|
||||
}
|
||||
|
||||
// Rate limiting - longer delay between requests to respect API limits
|
||||
await new Promise(resolve => setTimeout(resolve, 300))
|
||||
|
||||
} catch (error) {
|
||||
console.warn(` Warning: Failed to fetch verse ${verseRef.id}:`, error)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`\nEnglish Bible import completed! Imported ${totalVersesImported} verses.`)
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error importing English Bible:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
// Run the import
|
||||
importEnglishBible()
|
||||
.then(() => {
|
||||
console.log('English Bible import completed successfully!')
|
||||
process.exit(0)
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Import failed:', error)
|
||||
process.exit(1)
|
||||
})
|
||||
.finally(() => prisma.$disconnect())
|
||||
230
scripts/import-romanian-versioned.ts
Normal file
230
scripts/import-romanian-versioned.ts
Normal file
@@ -0,0 +1,230 @@
|
||||
import { PrismaClient } from '@prisma/client'
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
|
||||
const prisma = new PrismaClient()
|
||||
|
||||
interface BibleData {
|
||||
testament: string;
|
||||
books: Array<{
|
||||
name: string;
|
||||
chapters: Array<{
|
||||
chapterNum: number;
|
||||
verses: Array<{
|
||||
verseNum: number;
|
||||
text: string;
|
||||
}>;
|
||||
}>;
|
||||
}>;
|
||||
}
|
||||
|
||||
function getBookKey(bookName: string): string {
|
||||
const keyMap: Record<string, string> = {
|
||||
'Geneza': 'genesis',
|
||||
'Exodul': 'exodus',
|
||||
'Leviticul': 'leviticus',
|
||||
'Numerii': 'numbers',
|
||||
'Deuteronomul': 'deuteronomy',
|
||||
'Iosua': 'joshua',
|
||||
'Judecătorii': 'judges',
|
||||
'Rut': 'ruth',
|
||||
'1 Samuel': '1_samuel',
|
||||
'2 Samuel': '2_samuel',
|
||||
'1 Împăraţi': '1_kings',
|
||||
'2 Împăraţi': '2_kings',
|
||||
'1 Cronici': '1_chronicles',
|
||||
'2 Cronici': '2_chronicles',
|
||||
'Ezra': 'ezra',
|
||||
'Neemia': 'nehemiah',
|
||||
'Estera': 'esther',
|
||||
'Iov': 'job',
|
||||
'Psalmii': 'psalms',
|
||||
'Proverbele': 'proverbs',
|
||||
'Ecclesiastul': 'ecclesiastes',
|
||||
'Cântarea Cântărilor': 'song_of_songs',
|
||||
'Isaia': 'isaiah',
|
||||
'Ieremia': 'jeremiah',
|
||||
'Plângerile lui Ieremia': 'lamentations',
|
||||
'Ezechiel': 'ezekiel',
|
||||
'Daniel': 'daniel',
|
||||
'Osea': 'hosea',
|
||||
'Ioel': 'joel',
|
||||
'Amos': 'amos',
|
||||
'Obadia': 'obadiah',
|
||||
'Iona': 'jonah',
|
||||
'Mica': 'micah',
|
||||
'Naum': 'nahum',
|
||||
'Habacuc': 'habakkuk',
|
||||
'Ţefania': 'zephaniah',
|
||||
'Hagai': 'haggai',
|
||||
'Zaharia': 'zechariah',
|
||||
'Maleahi': 'malachi',
|
||||
'Matei': 'matthew',
|
||||
'Marcu': 'mark',
|
||||
'Luca': 'luke',
|
||||
'Ioan': 'john',
|
||||
'Faptele Apostolilor': 'acts',
|
||||
'Romani': 'romans',
|
||||
'1 Corinteni': '1_corinthians',
|
||||
'2 Corinteni': '2_corinthians',
|
||||
'Galateni': 'galatians',
|
||||
'Efeseni': 'ephesians',
|
||||
'Filipeni': 'philippians',
|
||||
'Coloseni': 'colossians',
|
||||
'1 Tesaloniceni': '1_thessalonians',
|
||||
'2 Tesaloniceni': '2_thessalonians',
|
||||
'1 Timotei': '1_timothy',
|
||||
'2 Timotei': '2_timothy',
|
||||
'Tit': 'titus',
|
||||
'Filimon': 'philemon',
|
||||
'Evrei': 'hebrews',
|
||||
'Iacov': 'james',
|
||||
'1 Petru': '1_peter',
|
||||
'2 Petru': '2_peter',
|
||||
'1 Ioan': '1_john',
|
||||
'2 Ioan': '2_john',
|
||||
'3 Ioan': '3_john',
|
||||
'Iuda': 'jude',
|
||||
'Apocalipsa': 'revelation',
|
||||
'Revelaţia': 'revelation',
|
||||
'Revelația': 'revelation',
|
||||
'Numeri': 'numbers',
|
||||
'Deuteronom': 'deuteronomy',
|
||||
'Judecători': 'judges',
|
||||
'1 Imparati': '1_kings',
|
||||
'2 Imparati': '2_kings',
|
||||
'Proverbe': 'proverbs',
|
||||
'Țefania': 'zephaniah'
|
||||
}
|
||||
|
||||
return keyMap[bookName] || bookName.toLowerCase().replace(/\s+/g, '_')
|
||||
}
|
||||
|
||||
async function importRomanianBible() {
|
||||
console.log('Starting Romanian Bible import with versioned schema...')
|
||||
|
||||
try {
|
||||
// Step 1: Create Romanian Bible version
|
||||
console.log('Creating Romanian Bible version...')
|
||||
const romanianVersion = await prisma.bibleVersion.upsert({
|
||||
where: {
|
||||
abbreviation_language: {
|
||||
abbreviation: 'CORNILESCU',
|
||||
language: 'ro'
|
||||
}
|
||||
},
|
||||
update: {},
|
||||
create: {
|
||||
name: 'Biblia Cornilescu',
|
||||
abbreviation: 'CORNILESCU',
|
||||
language: 'ro',
|
||||
description: 'Traducerea Cornilescu a Bibliei în limba română',
|
||||
isDefault: true
|
||||
}
|
||||
})
|
||||
|
||||
console.log(`Created Romanian version: ${romanianVersion.id}`)
|
||||
|
||||
// Step 1.1: Clear any existing Romanian content for this version (idempotent import)
|
||||
console.log('Clearing existing Romanian version content (if any)...')
|
||||
await prisma.bibleVerse.deleteMany({
|
||||
where: { chapter: { book: { versionId: romanianVersion.id } } }
|
||||
})
|
||||
await prisma.bibleChapter.deleteMany({
|
||||
where: { book: { versionId: romanianVersion.id } }
|
||||
})
|
||||
await prisma.bibleBook.deleteMany({ where: { versionId: romanianVersion.id } })
|
||||
|
||||
// Step 2: Import Old Testament
|
||||
console.log('Importing Old Testament...')
|
||||
const otPath = path.join(process.cwd(), 'data', 'old_testament.json')
|
||||
if (fs.existsSync(otPath)) {
|
||||
const otData: BibleData = JSON.parse(fs.readFileSync(otPath, 'utf-8'))
|
||||
await importTestament(romanianVersion.id, otData, 'Old Testament')
|
||||
} else {
|
||||
console.log('Old Testament data file not found, skipping...')
|
||||
}
|
||||
|
||||
// Step 3: Import New Testament
|
||||
console.log('Importing New Testament...')
|
||||
const ntPath = path.join(process.cwd(), 'data', 'new_testament.json')
|
||||
if (fs.existsSync(ntPath)) {
|
||||
const ntData: BibleData = JSON.parse(fs.readFileSync(ntPath, 'utf-8'))
|
||||
await importTestament(romanianVersion.id, ntData, 'New Testament')
|
||||
} else {
|
||||
console.log('New Testament data file not found, skipping...')
|
||||
}
|
||||
|
||||
console.log('Romanian Bible import completed successfully!')
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error importing Romanian Bible:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
async function importTestament(versionId: string, testamentData: BibleData, testament: string) {
|
||||
console.log(`Importing ${testament}...`)
|
||||
|
||||
let orderNum = testament === 'Old Testament' ? 1 : 40
|
||||
|
||||
for (const bookData of testamentData.books) {
|
||||
console.log(` Processing ${bookData.name}...`)
|
||||
|
||||
const bookKey = getBookKey(bookData.name)
|
||||
|
||||
// Create book
|
||||
const book = await prisma.bibleBook.create({
|
||||
data: {
|
||||
versionId,
|
||||
name: bookData.name,
|
||||
testament,
|
||||
orderNum,
|
||||
bookKey
|
||||
}
|
||||
})
|
||||
|
||||
// Import chapters
|
||||
for (const chapterData of bookData.chapters) {
|
||||
console.log(` Chapter ${chapterData.chapterNum}...`)
|
||||
|
||||
// Create chapter
|
||||
const chapter = await prisma.bibleChapter.create({
|
||||
data: {
|
||||
bookId: book.id,
|
||||
chapterNum: chapterData.chapterNum
|
||||
}
|
||||
})
|
||||
|
||||
// Import verses (dedupe by verseNum, then bulk insert with skipDuplicates)
|
||||
const uniqueByVerse: Record<number, { verseNum: number; text: string }> = {}
|
||||
for (const v of chapterData.verses) {
|
||||
uniqueByVerse[v.verseNum] = { verseNum: v.verseNum, text: v.text }
|
||||
}
|
||||
const versesData = Object.values(uniqueByVerse).map(v => ({
|
||||
chapterId: chapter.id,
|
||||
verseNum: v.verseNum,
|
||||
text: v.text
|
||||
}))
|
||||
if (versesData.length > 0) {
|
||||
await prisma.bibleVerse.createMany({ data: versesData, skipDuplicates: true })
|
||||
}
|
||||
|
||||
console.log(` Imported ${chapterData.verses.length} verses`)
|
||||
}
|
||||
|
||||
orderNum++
|
||||
}
|
||||
}
|
||||
|
||||
// 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())
|
||||
187
scripts/migrate-to-versioned-schema.ts
Normal file
187
scripts/migrate-to-versioned-schema.ts
Normal file
@@ -0,0 +1,187 @@
|
||||
import { PrismaClient } from '@prisma/client'
|
||||
|
||||
const prisma = new PrismaClient()
|
||||
|
||||
// Book key mapping for consistent cross-version identification
|
||||
const bookKeyMapping: Record<string, string> = {
|
||||
'Geneza': 'genesis',
|
||||
'Genesis': 'genesis',
|
||||
'Exodul': 'exodus',
|
||||
'Exodus': 'exodus',
|
||||
'Leviticul': 'leviticus',
|
||||
'Leviticus': 'leviticus',
|
||||
'Numerii': 'numbers',
|
||||
'Numbers': 'numbers',
|
||||
// Add more as needed
|
||||
}
|
||||
|
||||
function getBookKey(bookName: string): string {
|
||||
return bookKeyMapping[bookName] || bookName.toLowerCase().replace(/\s+/g, '_')
|
||||
}
|
||||
|
||||
async function migrateToVersionedSchema() {
|
||||
console.log('Starting migration to versioned Bible schema...')
|
||||
|
||||
try {
|
||||
// Step 1: Create Bible versions
|
||||
console.log('Creating Bible versions...')
|
||||
|
||||
const romanianVersion = await prisma.bibleVersion.upsert({
|
||||
where: { abbreviation_language: { abbreviation: 'CORNILESCU', language: 'ro' } },
|
||||
update: {},
|
||||
create: {
|
||||
name: 'Cornilescu',
|
||||
abbreviation: 'CORNILESCU',
|
||||
language: 'ro',
|
||||
description: 'Biblia Cornilescu în limba română',
|
||||
isDefault: true
|
||||
}
|
||||
})
|
||||
|
||||
const englishVersion = await prisma.bibleVersion.upsert({
|
||||
where: { abbreviation_language: { abbreviation: 'BSB', language: 'en' } },
|
||||
update: {},
|
||||
create: {
|
||||
name: 'Berean Standard Bible',
|
||||
abbreviation: 'BSB',
|
||||
language: 'en',
|
||||
description: 'English Bible - Berean Standard Bible',
|
||||
isDefault: true
|
||||
}
|
||||
})
|
||||
|
||||
console.log(`Created versions: ${romanianVersion.id} (RO), ${englishVersion.id} (EN)`)
|
||||
|
||||
// Step 2: Get all existing books and migrate them
|
||||
console.log('Migrating existing books...')
|
||||
|
||||
// Note: This assumes your current BibleBook table has the old structure
|
||||
// We'll need to query the old structure and create new versioned books
|
||||
|
||||
const existingBooks = await prisma.$queryRaw<any[]>`
|
||||
SELECT id, name, testament, "orderNum" FROM "BibleBook" ORDER BY "orderNum"
|
||||
`
|
||||
|
||||
for (const oldBook of existingBooks) {
|
||||
const bookKey = getBookKey(oldBook.name)
|
||||
|
||||
// Create Romanian version of the book (assuming current data is Romanian)
|
||||
const roBook = await prisma.bibleBook.create({
|
||||
data: {
|
||||
versionId: romanianVersion.id,
|
||||
name: oldBook.name,
|
||||
testament: oldBook.testament,
|
||||
orderNum: oldBook.orderNum,
|
||||
bookKey: bookKey
|
||||
}
|
||||
})
|
||||
|
||||
console.log(`Created Romanian book: ${roBook.name} (${bookKey})`)
|
||||
|
||||
// Get all chapters for this book and migrate them
|
||||
const existingChapters = await prisma.$queryRaw<any[]>`
|
||||
SELECT id, "chapterNum" FROM "BibleChapter" WHERE "bookId" = ${oldBook.id} ORDER BY "chapterNum"
|
||||
`
|
||||
|
||||
for (const oldChapter of existingChapters) {
|
||||
const newChapter = await prisma.bibleChapter.create({
|
||||
data: {
|
||||
bookId: roBook.id,
|
||||
chapterNum: oldChapter.chapterNum
|
||||
}
|
||||
})
|
||||
|
||||
// Get all verses for this chapter and migrate them (Romanian only)
|
||||
const existingVerses = await prisma.$queryRaw<any[]>`
|
||||
SELECT id, "verseNum", text FROM "BibleVerse"
|
||||
WHERE "chapterId" = ${oldChapter.id} AND version = 'KJV'
|
||||
ORDER BY "verseNum"
|
||||
`
|
||||
|
||||
for (const oldVerse of existingVerses) {
|
||||
await prisma.bibleVerse.create({
|
||||
data: {
|
||||
chapterId: newChapter.id,
|
||||
verseNum: oldVerse.verseNum,
|
||||
text: oldVerse.text
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
console.log(` Migrated chapter ${oldChapter.chapterNum} with ${existingVerses.length} verses`)
|
||||
}
|
||||
|
||||
// Also create English version if we have English data
|
||||
const englishVerses = await prisma.$queryRaw<any[]>`
|
||||
SELECT COUNT(*) as count FROM "BibleVerse" v
|
||||
JOIN "BibleChapter" c ON v."chapterId" = c.id
|
||||
WHERE c."bookId" = ${oldBook.id} AND v.version = 'EN'
|
||||
`
|
||||
|
||||
if (englishVerses[0]?.count > 0) {
|
||||
// Map English book name (you might need to improve this mapping)
|
||||
const englishBookName = oldBook.name === 'Geneza' ? 'Genesis' :
|
||||
oldBook.name === 'Exodul' ? 'Exodus' :
|
||||
oldBook.name
|
||||
|
||||
const enBook = await prisma.bibleBook.create({
|
||||
data: {
|
||||
versionId: englishVersion.id,
|
||||
name: englishBookName,
|
||||
testament: oldBook.testament,
|
||||
orderNum: oldBook.orderNum,
|
||||
bookKey: bookKey
|
||||
}
|
||||
})
|
||||
|
||||
// Migrate English chapters and verses
|
||||
for (const oldChapter of existingChapters) {
|
||||
const enChapter = await prisma.bibleChapter.create({
|
||||
data: {
|
||||
bookId: enBook.id,
|
||||
chapterNum: oldChapter.chapterNum
|
||||
}
|
||||
})
|
||||
|
||||
const englishVerses = await prisma.$queryRaw<any[]>`
|
||||
SELECT "verseNum", text FROM "BibleVerse"
|
||||
WHERE "chapterId" = ${oldChapter.id} AND version = 'EN'
|
||||
ORDER BY "verseNum"
|
||||
`
|
||||
|
||||
for (const enVerse of englishVerses) {
|
||||
await prisma.bibleVerse.create({
|
||||
data: {
|
||||
chapterId: enChapter.id,
|
||||
verseNum: enVerse.verseNum,
|
||||
text: enVerse.text
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`Created English book: ${englishBookName}`)
|
||||
}
|
||||
}
|
||||
|
||||
console.log('Migration completed successfully!')
|
||||
console.log(`Romanian version ID: ${romanianVersion.id}`)
|
||||
console.log(`English version ID: ${englishVersion.id}`)
|
||||
|
||||
} catch (error) {
|
||||
console.error('Migration failed:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
// Run the migration
|
||||
migrateToVersionedSchema()
|
||||
.then(() => {
|
||||
console.log('Migration completed successfully!')
|
||||
process.exit(0)
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Migration failed:', error)
|
||||
process.exit(1)
|
||||
})
|
||||
.finally(() => prisma.$disconnect())
|
||||
135
scripts/resync-bookkeys-ro.ts
Normal file
135
scripts/resync-bookkeys-ro.ts
Normal file
@@ -0,0 +1,135 @@
|
||||
import { PrismaClient } from '@prisma/client'
|
||||
|
||||
const prisma = new PrismaClient()
|
||||
|
||||
const keyMap: Record<string, string> = {
|
||||
// OT
|
||||
'geneza': 'genesis',
|
||||
'exodul': 'exodus',
|
||||
'leviticul': 'leviticus',
|
||||
'numerii': 'numbers',
|
||||
'numeri': 'numbers',
|
||||
'deuteronomul': 'deuteronomy',
|
||||
'deuteronom': 'deuteronomy',
|
||||
'iosua': 'joshua',
|
||||
'judecătorii': 'judges',
|
||||
'judecători': 'judges',
|
||||
'judecatori': 'judges',
|
||||
'rut': 'ruth',
|
||||
'1 samuel': '1_samuel',
|
||||
'2 samuel': '2_samuel',
|
||||
'1 împăraţi': '1_kings',
|
||||
'2 împăraţi': '2_kings',
|
||||
'1 imparati': '1_kings',
|
||||
'2 imparati': '2_kings',
|
||||
'1 cronici': '1_chronicles',
|
||||
'2 cronici': '2_chronicles',
|
||||
'ezra': 'ezra',
|
||||
'neemia': 'nehemiah',
|
||||
'estera': 'esther',
|
||||
'iov': 'job',
|
||||
'psalmii': 'psalms',
|
||||
'proverbele': 'proverbs',
|
||||
'proverbe': 'proverbs',
|
||||
'eclesiastul': 'ecclesiastes',
|
||||
'ecclesiastul': 'ecclesiastes',
|
||||
'cântarea cântărilor': 'song_of_songs',
|
||||
'cantarea cantarilor': 'song_of_songs',
|
||||
'isaia': 'isaiah',
|
||||
'ieremia': 'jeremiah',
|
||||
'plângerile': 'lamentations',
|
||||
'plangerile': 'lamentations',
|
||||
'plângerile lui ieremia': 'lamentations',
|
||||
'ezechiel': 'ezekiel',
|
||||
'daniel': 'daniel',
|
||||
'osea': 'hosea',
|
||||
'ioel': 'joel',
|
||||
'amos': 'amos',
|
||||
'obadia': 'obadiah',
|
||||
'iona': 'jonah',
|
||||
'mica': 'micah',
|
||||
'naum': 'nahum',
|
||||
'habacuc': 'habakkuk',
|
||||
'ţefania': 'zephaniah',
|
||||
'țefania': 'zephaniah',
|
||||
'tefania': 'zephaniah',
|
||||
'hagai': 'haggai',
|
||||
'zaharia': 'zechariah',
|
||||
'maleahi': 'malachi',
|
||||
// NT
|
||||
'matei': 'matthew',
|
||||
'marcu': 'mark',
|
||||
'luca': 'luke',
|
||||
'ioan': 'john',
|
||||
'faptele apostolilor': 'acts',
|
||||
'romani': 'romans',
|
||||
'1 corinteni': '1_corinthians',
|
||||
'2 corinteni': '2_corinthians',
|
||||
'galateni': 'galatians',
|
||||
'efeseni': 'ephesians',
|
||||
'filipeni': 'philippians',
|
||||
'coloseni': 'colossians',
|
||||
'1 tesaloniceni': '1_thessalonians',
|
||||
'2 tesaloniceni': '2_thessalonians',
|
||||
'1 timotei': '1_timothy',
|
||||
'2 timotei': '2_timothy',
|
||||
'tit': 'titus',
|
||||
'titus': 'titus',
|
||||
'filimon': 'philemon',
|
||||
'evrei': 'hebrews',
|
||||
'iacov': 'james',
|
||||
'iacob': 'james',
|
||||
'1 petru': '1_peter',
|
||||
'2 petru': '2_peter',
|
||||
'1 ioan': '1_john',
|
||||
'2 ioan': '2_john',
|
||||
'3 ioan': '3_john',
|
||||
'iuda': 'jude',
|
||||
'apocalipsa': 'revelation',
|
||||
'revelaţia': 'revelation',
|
||||
'revelația': 'revelation',
|
||||
}
|
||||
|
||||
function toCanonicalBookKey(name: string): string {
|
||||
const k = name.trim().toLowerCase()
|
||||
return keyMap[k] || k.replace(/\s+/g, '_')
|
||||
}
|
||||
|
||||
async function main() {
|
||||
try {
|
||||
const roVersion = await prisma.bibleVersion.findFirst({
|
||||
where: { language: { in: ['ro', 'RO'] } }
|
||||
})
|
||||
if (!roVersion) {
|
||||
throw new Error('No Romanian BibleVersion found (language ro)')
|
||||
}
|
||||
|
||||
const books = await prisma.bibleBook.findMany({
|
||||
where: { versionId: roVersion.id },
|
||||
orderBy: { orderNum: 'asc' }
|
||||
})
|
||||
|
||||
let updated = 0
|
||||
for (const b of books) {
|
||||
const desiredKey = toCanonicalBookKey(b.name)
|
||||
if (b.bookKey !== desiredKey) {
|
||||
await prisma.bibleBook.update({
|
||||
where: { id: b.id },
|
||||
data: { bookKey: desiredKey }
|
||||
})
|
||||
updated++
|
||||
console.log(`Updated ${b.name}: ${b.bookKey} -> ${desiredKey}`)
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`Resync complete. Updated ${updated} book keys for RO.`)
|
||||
} catch (err) {
|
||||
console.error('Resync failed:', err)
|
||||
process.exit(1)
|
||||
} finally {
|
||||
await prisma.$disconnect()
|
||||
}
|
||||
}
|
||||
|
||||
main()
|
||||
|
||||
76
test-api.ts
Normal file
76
test-api.ts
Normal file
@@ -0,0 +1,76 @@
|
||||
import { prisma } from './lib/db'
|
||||
|
||||
async function testApiLogic() {
|
||||
try {
|
||||
console.log('Testing API logic...')
|
||||
|
||||
const locale = 'en'
|
||||
const versionAbbr = null
|
||||
|
||||
console.log(`Looking for locale: ${locale}, version: ${versionAbbr}`)
|
||||
|
||||
// Get the appropriate Bible version
|
||||
let bibleVersion
|
||||
if (versionAbbr) {
|
||||
// Use specific version if provided
|
||||
bibleVersion = await prisma.bibleVersion.findFirst({
|
||||
where: {
|
||||
abbreviation: versionAbbr,
|
||||
language: locale
|
||||
}
|
||||
})
|
||||
} else {
|
||||
// Use default version for the language
|
||||
bibleVersion = await prisma.bibleVersion.findFirst({
|
||||
where: {
|
||||
language: locale,
|
||||
isDefault: true
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
console.log('Bible version found:', bibleVersion)
|
||||
|
||||
if (!bibleVersion) {
|
||||
console.log(`No Bible version found for language: ${locale}`)
|
||||
return
|
||||
}
|
||||
|
||||
// Get books for this version
|
||||
const books = await prisma.bibleBook.findMany({
|
||||
where: {
|
||||
versionId: bibleVersion.id
|
||||
},
|
||||
orderBy: {
|
||||
orderNum: 'asc'
|
||||
},
|
||||
include: {
|
||||
chapters: {
|
||||
orderBy: {
|
||||
chapterNum: 'asc'
|
||||
},
|
||||
include: {
|
||||
_count: {
|
||||
select: {
|
||||
verses: true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
console.log(`Found ${books.length} books`)
|
||||
|
||||
for (const book of books.slice(0, 3)) {
|
||||
console.log(`Book: ${book.name} (${book.chapters.length} chapters)`)
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('API test failed:', error)
|
||||
} finally {
|
||||
await prisma.$disconnect()
|
||||
}
|
||||
}
|
||||
|
||||
testApiLogic()
|
||||
34
test-db.ts
Normal file
34
test-db.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import { PrismaClient } from '@prisma/client'
|
||||
|
||||
const prisma = new PrismaClient()
|
||||
|
||||
async function testDatabase() {
|
||||
try {
|
||||
console.log('Testing database connection...')
|
||||
|
||||
// Test basic connection
|
||||
const versions = await prisma.bibleVersion.findMany()
|
||||
console.log('Bible versions found:', versions.length)
|
||||
|
||||
for (const version of versions) {
|
||||
console.log(`Version: ${version.name} (${version.abbreviation}) - ${version.language}`)
|
||||
|
||||
const books = await prisma.bibleBook.findMany({
|
||||
where: { versionId: version.id },
|
||||
take: 3
|
||||
})
|
||||
console.log(` Books: ${books.length}`)
|
||||
|
||||
for (const book of books) {
|
||||
console.log(` - ${book.name} (order: ${book.orderNum})`)
|
||||
}
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('Database test failed:', error)
|
||||
} finally {
|
||||
await prisma.$disconnect()
|
||||
}
|
||||
}
|
||||
|
||||
testDatabase()
|
||||
Reference in New Issue
Block a user