Implement complete multi-language support with Romanian/English
- Added next-intl for internationalization with Romanian as default locale - Restructured app directory with [locale] routing (/ro, /en) - Created comprehensive translation files for both languages - Fixed Next.js 15 async params compatibility in layout components - Updated all components to use proper i18n hooks and translations - Configured middleware for locale routing and fallbacks - Fixed FloatingChat component translation array handling - Restored complete home page with internationalized content - Fixed Material-UI Slide component prop error (mountOnExit → unmountOnExit) 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
55
app/[locale]/layout.tsx
Normal file
55
app/[locale]/layout.tsx
Normal file
@@ -0,0 +1,55 @@
|
||||
import '../globals.css'
|
||||
import type { Metadata } from 'next'
|
||||
import { NextIntlClientProvider } from 'next-intl'
|
||||
import { getMessages } from 'next-intl/server'
|
||||
import { notFound } from 'next/navigation'
|
||||
import { MuiThemeProvider } from '@/components/providers/theme-provider'
|
||||
import { Navigation } from '@/components/layout/navigation'
|
||||
import FloatingChat from '@/components/chat/floating-chat'
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'Ghid Biblic - Biblical Guide',
|
||||
description: 'A comprehensive Bible study application with AI chat capabilities',
|
||||
}
|
||||
|
||||
export async function generateStaticParams() {
|
||||
return [
|
||||
{ locale: 'ro' },
|
||||
{ locale: 'en' }
|
||||
]
|
||||
}
|
||||
|
||||
interface LocaleLayoutProps {
|
||||
children: React.ReactNode
|
||||
params: Promise<{ locale: string }>
|
||||
}
|
||||
|
||||
const locales = ['ro', 'en']
|
||||
|
||||
export default async function LocaleLayout({
|
||||
children,
|
||||
params
|
||||
}: LocaleLayoutProps) {
|
||||
const { locale } = await params
|
||||
|
||||
// Validate locale
|
||||
if (!locales.includes(locale)) {
|
||||
notFound()
|
||||
}
|
||||
|
||||
const messages = await getMessages()
|
||||
|
||||
return (
|
||||
<html lang={locale}>
|
||||
<body>
|
||||
<NextIntlClientProvider messages={messages} locale={locale}>
|
||||
<MuiThemeProvider>
|
||||
<Navigation />
|
||||
{children}
|
||||
<FloatingChat />
|
||||
</MuiThemeProvider>
|
||||
</NextIntlClientProvider>
|
||||
</body>
|
||||
</html>
|
||||
)
|
||||
}
|
||||
@@ -18,38 +18,40 @@ import {
|
||||
AutoStories,
|
||||
Favorite,
|
||||
} from '@mui/icons-material'
|
||||
import { Navigation } from '@/components/layout/navigation'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import { useTranslations, useLocale } from 'next-intl'
|
||||
|
||||
export default function Home() {
|
||||
const theme = useTheme()
|
||||
const router = useRouter()
|
||||
const t = useTranslations('home')
|
||||
const locale = useLocale()
|
||||
|
||||
const features = [
|
||||
{
|
||||
title: 'Citește Biblia',
|
||||
description: 'Explorează Scriptura cu o interfață modernă și ușor de folosit',
|
||||
title: t('features.bible.title'),
|
||||
description: t('features.bible.description'),
|
||||
icon: <MenuBook sx={{ fontSize: 40, color: 'primary.main' }} />,
|
||||
path: '/bible',
|
||||
color: theme.palette.primary.main,
|
||||
},
|
||||
{
|
||||
title: 'Chat cu AI',
|
||||
description: 'Pune întrebări despre Scriptură și primește răspunsuri clare',
|
||||
title: t('features.chat.title'),
|
||||
description: t('features.chat.description'),
|
||||
icon: <Chat sx={{ fontSize: 40, color: 'secondary.main' }} />,
|
||||
path: '/chat',
|
||||
color: theme.palette.secondary.main,
|
||||
},
|
||||
{
|
||||
title: 'Rugăciuni',
|
||||
description: 'Partajează rugăciuni și roagă-te împreună cu comunitatea',
|
||||
title: t('features.prayers.title'),
|
||||
description: t('features.prayers.description'),
|
||||
icon: <Prayer sx={{ fontSize: 40, color: 'success.main' }} />,
|
||||
path: '/prayers',
|
||||
color: theme.palette.success.main,
|
||||
},
|
||||
{
|
||||
title: 'Căutare',
|
||||
description: 'Caută versete și pasaje din întreaga Scriptură',
|
||||
title: t('features.search.title'),
|
||||
description: t('features.search.description'),
|
||||
icon: <Search sx={{ fontSize: 40, color: 'info.main' }} />,
|
||||
path: '/search',
|
||||
color: theme.palette.info.main,
|
||||
@@ -58,8 +60,6 @@ export default function Home() {
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Navigation />
|
||||
|
||||
{/* Hero Section */}
|
||||
<Box
|
||||
sx={{
|
||||
@@ -73,15 +73,13 @@ export default function Home() {
|
||||
<Grid container spacing={4} alignItems="center">
|
||||
<Grid item xs={12} md={8}>
|
||||
<Typography variant="h2" component="h1" gutterBottom>
|
||||
Ghid Biblic
|
||||
{t('hero.title')}
|
||||
</Typography>
|
||||
<Typography variant="h5" component="h2" sx={{ mb: 3, opacity: 0.9 }}>
|
||||
Explorează Scriptura cu ajutorul inteligenței artificiale
|
||||
{t('hero.subtitle')}
|
||||
</Typography>
|
||||
<Typography variant="body1" sx={{ mb: 4, opacity: 0.8, maxWidth: 600 }}>
|
||||
O platformă modernă pentru studiul Bibliei, cu chat AI inteligent,
|
||||
căutare avansată și o comunitate de rugăciune care te sprijină în
|
||||
călătoria ta spirituală.
|
||||
{t('hero.description')}
|
||||
</Typography>
|
||||
<Box sx={{ display: 'flex', gap: 2, flexWrap: 'wrap' }}>
|
||||
<Button
|
||||
@@ -92,9 +90,9 @@ export default function Home() {
|
||||
'&:hover': { bgcolor: 'secondary.dark' },
|
||||
}}
|
||||
startIcon={<AutoStories />}
|
||||
onClick={() => router.push('/bible')}
|
||||
onClick={() => router.push(`/${locale}/bible`)}
|
||||
>
|
||||
Începe să citești
|
||||
{t('hero.cta.readBible')}
|
||||
</Button>
|
||||
<Button
|
||||
variant="outlined"
|
||||
@@ -108,9 +106,9 @@ export default function Home() {
|
||||
},
|
||||
}}
|
||||
startIcon={<Chat />}
|
||||
onClick={() => router.push('/chat')}
|
||||
onClick={() => router.push(`/${locale}/chat`)}
|
||||
>
|
||||
Întreabă AI
|
||||
{t('hero.cta.askAI')}
|
||||
</Button>
|
||||
</Box>
|
||||
</Grid>
|
||||
@@ -126,7 +124,7 @@ export default function Home() {
|
||||
{/* Features Section */}
|
||||
<Container maxWidth="lg" sx={{ mb: 8 }}>
|
||||
<Typography variant="h3" component="h2" textAlign="center" sx={{ mb: 2 }}>
|
||||
Descoperă funcționalitățile
|
||||
{t('features.title')}
|
||||
</Typography>
|
||||
<Typography
|
||||
variant="body1"
|
||||
@@ -134,7 +132,7 @@ export default function Home() {
|
||||
color="text.secondary"
|
||||
sx={{ mb: 6, maxWidth: 600, mx: 'auto' }}
|
||||
>
|
||||
Totul de ce ai nevoie pentru o experiență completă de studiu biblic
|
||||
{t('features.subtitle')}
|
||||
</Typography>
|
||||
|
||||
<Grid container spacing={4}>
|
||||
@@ -152,7 +150,7 @@ export default function Home() {
|
||||
boxShadow: 4,
|
||||
},
|
||||
}}
|
||||
onClick={() => router.push(feature.path)}
|
||||
onClick={() => router.push(`/${locale}${feature.path}`)}
|
||||
>
|
||||
<CardContent sx={{ flexGrow: 1, textAlign: 'center', p: 3 }}>
|
||||
<Box sx={{ mb: 2 }}>
|
||||
@@ -179,19 +177,19 @@ export default function Home() {
|
||||
<Typography variant="h3" color="primary.main" gutterBottom>
|
||||
66
|
||||
</Typography>
|
||||
<Typography variant="h6">Cărți biblice</Typography>
|
||||
<Typography variant="h6">{t('stats.books')}</Typography>
|
||||
</Grid>
|
||||
<Grid item xs={12} sm={4}>
|
||||
<Typography variant="h3" color="secondary.main" gutterBottom>
|
||||
31,000+
|
||||
</Typography>
|
||||
<Typography variant="h6">Versete</Typography>
|
||||
<Typography variant="h6">{t('stats.verses')}</Typography>
|
||||
</Grid>
|
||||
<Grid item xs={12} sm={4}>
|
||||
<Typography variant="h3" color="success.main" gutterBottom>
|
||||
24/7
|
||||
</Typography>
|
||||
<Typography variant="h6">Chat AI disponibil</Typography>
|
||||
<Typography variant="h6">{t('stats.aiAvailable')}</Typography>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Container>
|
||||
@@ -200,19 +198,19 @@ export default function Home() {
|
||||
{/* CTA Section */}
|
||||
<Container maxWidth="sm" sx={{ textAlign: 'center', mb: 8 }}>
|
||||
<Typography variant="h4" component="h2" gutterBottom>
|
||||
Începe călătoria ta spirituală
|
||||
{t('cta.title')}
|
||||
</Typography>
|
||||
<Typography variant="body1" color="text.secondary" sx={{ mb: 4 }}>
|
||||
Alătură-te comunității noastre și descoperă înțelepciunea Scripturii
|
||||
{t('cta.description')}
|
||||
</Typography>
|
||||
<Button
|
||||
variant="contained"
|
||||
size="large"
|
||||
startIcon={<Favorite />}
|
||||
sx={{ mr: 2 }}
|
||||
onClick={() => router.push('/bible')}
|
||||
onClick={() => router.push(`/${locale}/bible`)}
|
||||
>
|
||||
Începe acum
|
||||
{t('cta.startNow')}
|
||||
</Button>
|
||||
</Container>
|
||||
</Box>
|
||||
@@ -4,6 +4,7 @@ import { searchBibleHybrid, BibleVerse } from '@/lib/vector-search'
|
||||
|
||||
const chatRequestSchema = z.object({
|
||||
message: z.string().min(1),
|
||||
locale: z.string().optional().default('ro'),
|
||||
history: z.array(z.object({
|
||||
id: z.string(),
|
||||
role: z.enum(['user', 'assistant']),
|
||||
@@ -15,11 +16,10 @@ const chatRequestSchema = z.object({
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const body = await request.json()
|
||||
const { message, history } = chatRequestSchema.parse(body)
|
||||
const { message, locale, history } = chatRequestSchema.parse(body)
|
||||
|
||||
// For now, return a mock response
|
||||
// TODO: Integrate with Azure OpenAI when ready
|
||||
const response = await generateBiblicalResponse(message, history)
|
||||
// Generate response using Azure OpenAI with vector search
|
||||
const response = await generateBiblicalResponse(message, locale, history)
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
@@ -49,10 +49,10 @@ export async function POST(request: NextRequest) {
|
||||
}
|
||||
}
|
||||
|
||||
async function generateBiblicalResponse(message: string, history: any[]): Promise<string> {
|
||||
async function generateBiblicalResponse(message: string, locale: string, history: any[]): Promise<string> {
|
||||
try {
|
||||
// Search for relevant Bible verses using vector search
|
||||
const relevantVerses = await searchBibleHybrid(message, 5)
|
||||
// Search for relevant Bible verses using vector search with language filtering
|
||||
const relevantVerses = await searchBibleHybrid(message, locale, 5)
|
||||
|
||||
// Create context from relevant verses
|
||||
const versesContext = relevantVerses
|
||||
@@ -65,8 +65,9 @@ async function generateBiblicalResponse(message: string, history: any[]): Promis
|
||||
.map(msg => `${msg.role}: ${msg.content}`)
|
||||
.join('\n')
|
||||
|
||||
// Construct prompt for Azure OpenAI
|
||||
const systemPrompt = `Ești un asistent AI pentru întrebări biblice în limba română. Răspunde pe baza Scripturii, fiind respectuos și înțelept.
|
||||
// Create language-specific system prompts
|
||||
const systemPrompts = {
|
||||
ro: `Ești un asistent AI pentru întrebări biblice în limba română. Răspunde pe baza Scripturii, fiind respectuos și înțelept.
|
||||
|
||||
Instrucțiuni:
|
||||
- Folosește versurile biblice relevante pentru a răspunde la întrebare
|
||||
@@ -81,7 +82,27 @@ ${versesContext}
|
||||
Conversația anterioară:
|
||||
${conversationHistory}
|
||||
|
||||
Întrebarea curentă: ${message}`
|
||||
Întrebarea curentă: ${message}`,
|
||||
|
||||
en: `You are an AI assistant for biblical questions in English. Answer based on Scripture, being respectful and wise.
|
||||
|
||||
Instructions:
|
||||
- Use the relevant Bible verses to answer the question
|
||||
- Always cite biblical references (e.g., John 3:16)
|
||||
- Respond in English
|
||||
- Be empathetic and encouraging
|
||||
- If unsure, encourage personal study and prayer
|
||||
|
||||
Relevant verses for this question:
|
||||
${versesContext}
|
||||
|
||||
Previous conversation:
|
||||
${conversationHistory}
|
||||
|
||||
Current question: ${message}`
|
||||
}
|
||||
|
||||
const systemPrompt = systemPrompts[locale as keyof typeof systemPrompts] || systemPrompts.en
|
||||
|
||||
// Call Azure OpenAI
|
||||
const response = await fetch(
|
||||
@@ -120,11 +141,21 @@ ${conversationHistory}
|
||||
} catch (error) {
|
||||
console.error('Error calling Azure OpenAI:', error)
|
||||
|
||||
// Fallback to simple response if AI fails
|
||||
return `Îmi pare rău, dar întâmpin o problemă tehnică în acest moment. Te încurajez să cercetezi acest subiect în Scripturi și să te rogi pentru înțelegere.
|
||||
// Language-specific fallback responses
|
||||
const fallbackResponses = {
|
||||
ro: `Îmi pare rău, dar întâmpin o problemă tehnică în acest moment. Te încurajez să cercetezi acest subiect în Scripturi și să te rogi pentru înțelegere.
|
||||
|
||||
"Cercetați Scripturile, pentru că socotiți că în ele aveți viața veșnică, și tocmai ele mărturisesc despre Mine" (Ioan 5:39).
|
||||
|
||||
"Dacă vreunul dintre voi duce lipsă de înțelepciune, să ceară de la Dumnezeu, care dă tuturor cu dărnicie și fără mustrare, și i se va da" (Iacov 1:5).`
|
||||
"Dacă vreunul dintre voi duce lipsă de înțelepciune, să ceară de la Dumnezeu, care dă tuturor cu dărnicie și fără mustrare, și i se va da" (Iacov 1:5).`,
|
||||
|
||||
en: `Sorry, I'm experiencing a technical issue at the moment. I encourage you to research this topic in Scripture and pray for understanding.
|
||||
|
||||
"You study the Scriptures diligently because you think that in them you have eternal life. These are the very Scriptures that testify about me" (John 5:39).
|
||||
|
||||
"If any of you lacks wisdom, you should ask God, who gives generously to all without finding fault, and it will be given to you" (James 1:5).`
|
||||
}
|
||||
|
||||
return fallbackResponses[locale as keyof typeof fallbackResponses] || fallbackResponses.en
|
||||
}
|
||||
}
|
||||
@@ -1,26 +1,7 @@
|
||||
import './globals.css'
|
||||
import type { Metadata } from 'next'
|
||||
import { MuiThemeProvider } from '@/components/providers/theme-provider'
|
||||
import FloatingChat from '@/components/chat/floating-chat'
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'Ghid Biblic - Biblical Guide',
|
||||
description: 'A comprehensive Bible study application with AI chat capabilities',
|
||||
}
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
}) {
|
||||
return (
|
||||
<html lang="ro">
|
||||
<body>
|
||||
<MuiThemeProvider>
|
||||
{children}
|
||||
<FloatingChat />
|
||||
</MuiThemeProvider>
|
||||
</body>
|
||||
</html>
|
||||
)
|
||||
return children
|
||||
}
|
||||
@@ -32,6 +32,7 @@ import {
|
||||
Launch,
|
||||
} from '@mui/icons-material'
|
||||
import { useState, useRef, useEffect } from 'react'
|
||||
import { useTranslations, useLocale } from 'next-intl'
|
||||
|
||||
interface ChatMessage {
|
||||
id: string
|
||||
@@ -42,13 +43,17 @@ interface ChatMessage {
|
||||
|
||||
export default function FloatingChat() {
|
||||
const theme = useTheme()
|
||||
const t = useTranslations('chat')
|
||||
const locale = useLocale()
|
||||
const [isOpen, setIsOpen] = useState(false)
|
||||
const [isMinimized, setIsMinimized] = useState(false)
|
||||
const [messages, setMessages] = useState<ChatMessage[]>([
|
||||
{
|
||||
id: '1',
|
||||
role: 'assistant',
|
||||
content: 'Bună ziua! Sunt asistentul tău AI pentru întrebări biblice. Cum te pot ajuta astăzi să înțelegi mai bine Scriptura?',
|
||||
content: locale === 'ro'
|
||||
? 'Bună ziua! Sunt asistentul tău AI pentru întrebări biblice. Cum te pot ajuta astăzi să înțelegi mai bine Scriptura?'
|
||||
: 'Hello! I am your AI assistant for biblical questions. How can I help you understand Scripture better today?',
|
||||
timestamp: new Date(),
|
||||
}
|
||||
])
|
||||
@@ -87,6 +92,7 @@ export default function FloatingChat() {
|
||||
body: JSON.stringify({
|
||||
message: inputMessage,
|
||||
history: messages.slice(-5),
|
||||
locale: locale,
|
||||
}),
|
||||
})
|
||||
|
||||
@@ -99,7 +105,9 @@ export default function FloatingChat() {
|
||||
const assistantMessage: ChatMessage = {
|
||||
id: (Date.now() + 1).toString(),
|
||||
role: 'assistant',
|
||||
content: data.response || 'Îmi pare rău, nu am putut procesa întrebarea ta. Te rog încearcă din nou.',
|
||||
content: data.response || (locale === 'ro'
|
||||
? 'Îmi pare rău, nu am putut procesa întrebarea ta. Te rog încearcă din nou.'
|
||||
: 'Sorry, I could not process your question. Please try again.'),
|
||||
timestamp: new Date(),
|
||||
}
|
||||
|
||||
@@ -109,7 +117,9 @@ export default function FloatingChat() {
|
||||
const errorMessage: ChatMessage = {
|
||||
id: (Date.now() + 1).toString(),
|
||||
role: 'assistant',
|
||||
content: 'Îmi pare rău, a apărut o eroare. Te rog verifică conexiunea și încearcă din nou.',
|
||||
content: locale === 'ro'
|
||||
? 'Îmi pare rău, a apărut o eroare. Te rog verifică conexiunea și încearcă din nou.'
|
||||
: 'Sorry, an error occurred. Please check your connection and try again.',
|
||||
timestamp: new Date(),
|
||||
}
|
||||
setMessages(prev => [...prev, errorMessage])
|
||||
@@ -129,13 +139,8 @@ export default function FloatingChat() {
|
||||
navigator.clipboard.writeText(text)
|
||||
}
|
||||
|
||||
const suggestedQuestions = [
|
||||
'Ce spune Biblia despre iubire?',
|
||||
'Explică-mi parabola semănătorului',
|
||||
'Care sunt fructele Duhului?',
|
||||
'Ce înseamnă să fii născut din nou?',
|
||||
'Cum pot să mă rog mai bine?',
|
||||
]
|
||||
// Use t.raw() to get the actual array from translations
|
||||
const suggestedQuestions = t.raw('suggestions.questions') as string[]
|
||||
|
||||
const toggleChat = () => {
|
||||
setIsOpen(!isOpen)
|
||||
@@ -173,7 +178,7 @@ export default function FloatingChat() {
|
||||
</Zoom>
|
||||
|
||||
{/* Chat Overlay */}
|
||||
<Slide direction="up" in={isOpen} mountOnExit>
|
||||
<Slide direction="up" in={isOpen} unmountOnExit>
|
||||
<Paper
|
||||
elevation={8}
|
||||
sx={{
|
||||
@@ -207,10 +212,10 @@ export default function FloatingChat() {
|
||||
</Avatar>
|
||||
<Box>
|
||||
<Typography variant="subtitle1" fontWeight="bold">
|
||||
Chat AI Biblic
|
||||
{t('title')}
|
||||
</Typography>
|
||||
<Typography variant="caption" sx={{ opacity: 0.9 }}>
|
||||
Asistent pentru întrebări biblice
|
||||
{t('subtitle')}
|
||||
</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
@@ -244,7 +249,7 @@ export default function FloatingChat() {
|
||||
{/* Suggested Questions */}
|
||||
<Box sx={{ p: 2, borderBottom: 1, borderColor: 'divider' }}>
|
||||
<Typography variant="body2" color="text.secondary" sx={{ mb: 1 }}>
|
||||
Întrebări sugerate:
|
||||
{t('suggestions.title')}
|
||||
</Typography>
|
||||
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 0.5 }}>
|
||||
{suggestedQuestions.slice(0, 3).map((question, index) => (
|
||||
@@ -367,7 +372,7 @@ export default function FloatingChat() {
|
||||
</Avatar>
|
||||
<Paper elevation={1} sx={{ p: 1.5, borderRadius: 2 }}>
|
||||
<Typography variant="body2">
|
||||
Scriu răspunsul...
|
||||
{t('loading')}
|
||||
</Typography>
|
||||
</Paper>
|
||||
</Box>
|
||||
@@ -387,7 +392,7 @@ export default function FloatingChat() {
|
||||
size="small"
|
||||
multiline
|
||||
maxRows={3}
|
||||
placeholder="Scrie întrebarea ta despre Biblie..."
|
||||
placeholder={t('placeholder')}
|
||||
value={inputMessage}
|
||||
onChange={(e) => setInputMessage(e.target.value)}
|
||||
onKeyPress={handleKeyPress}
|
||||
@@ -414,7 +419,7 @@ export default function FloatingChat() {
|
||||
</Button>
|
||||
</Box>
|
||||
<Typography variant="caption" color="text.secondary" sx={{ mt: 0.5, display: 'block' }}>
|
||||
Enter pentru a trimite • Shift+Enter pentru linie nouă
|
||||
{t('enterToSend')}
|
||||
</Typography>
|
||||
</Box>
|
||||
</>
|
||||
|
||||
88
components/layout/language-switcher.tsx
Normal file
88
components/layout/language-switcher.tsx
Normal file
@@ -0,0 +1,88 @@
|
||||
'use client'
|
||||
|
||||
import { useState } from 'react'
|
||||
import { useRouter, usePathname } from 'next/navigation'
|
||||
import { useLocale, useTranslations } from 'next-intl'
|
||||
import {
|
||||
IconButton,
|
||||
Menu,
|
||||
MenuItem,
|
||||
Box,
|
||||
Typography,
|
||||
ListItemIcon,
|
||||
} from '@mui/material'
|
||||
import { Language, Check } from '@mui/icons-material'
|
||||
|
||||
const languages = [
|
||||
{ code: 'ro', name: 'Română', flag: '🇷🇴' },
|
||||
{ code: 'en', name: 'English', flag: '🇺🇸' },
|
||||
]
|
||||
|
||||
export function LanguageSwitcher() {
|
||||
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null)
|
||||
const router = useRouter()
|
||||
const pathname = usePathname()
|
||||
const locale = useLocale()
|
||||
const t = useTranslations('navigation')
|
||||
|
||||
const handleOpen = (event: React.MouseEvent<HTMLElement>) => {
|
||||
setAnchorEl(event.currentTarget)
|
||||
}
|
||||
|
||||
const handleClose = () => {
|
||||
setAnchorEl(null)
|
||||
}
|
||||
|
||||
const handleLanguageChange = (newLocale: string) => {
|
||||
// Remove current locale from pathname and add new one
|
||||
const pathWithoutLocale = pathname.replace(`/${locale}`, '') || '/'
|
||||
const newPath = `/${newLocale}${pathWithoutLocale === '/' ? '' : pathWithoutLocale}`
|
||||
|
||||
router.push(newPath)
|
||||
handleClose()
|
||||
}
|
||||
|
||||
const currentLanguage = languages.find(lang => lang.code === locale)
|
||||
|
||||
return (
|
||||
<>
|
||||
<IconButton
|
||||
onClick={handleOpen}
|
||||
sx={{ color: 'white' }}
|
||||
aria-label={t('language')}
|
||||
>
|
||||
<Language />
|
||||
</IconButton>
|
||||
|
||||
<Menu
|
||||
anchorEl={anchorEl}
|
||||
open={Boolean(anchorEl)}
|
||||
onClose={handleClose}
|
||||
transformOrigin={{ horizontal: 'right', vertical: 'top' }}
|
||||
anchorOrigin={{ horizontal: 'right', vertical: 'bottom' }}
|
||||
>
|
||||
{languages.map((language) => (
|
||||
<MenuItem
|
||||
key={language.code}
|
||||
onClick={() => handleLanguageChange(language.code)}
|
||||
selected={language.code === locale}
|
||||
>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, minWidth: 120 }}>
|
||||
<Typography component="span" sx={{ fontSize: '1.2rem' }}>
|
||||
{language.flag}
|
||||
</Typography>
|
||||
<Typography sx={{ flexGrow: 1 }}>
|
||||
{language.name}
|
||||
</Typography>
|
||||
{language.code === locale && (
|
||||
<ListItemIcon sx={{ minWidth: 'auto' }}>
|
||||
<Check fontSize="small" />
|
||||
</ListItemIcon>
|
||||
)}
|
||||
</Box>
|
||||
</MenuItem>
|
||||
))}
|
||||
</Menu>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -32,15 +32,8 @@ import {
|
||||
Logout,
|
||||
} from '@mui/icons-material'
|
||||
import { useRouter } from 'next/navigation'
|
||||
|
||||
const pages = [
|
||||
{ name: 'Acasă', path: '/', icon: <Home /> },
|
||||
{ name: 'Biblia', path: '/bible', icon: <MenuBook /> },
|
||||
{ name: 'Rugăciuni', path: '/prayers', icon: <Prayer /> },
|
||||
{ name: 'Căutare', path: '/search', icon: <Search /> },
|
||||
]
|
||||
|
||||
const settings = ['Profil', 'Setări', 'Deconectare']
|
||||
import { useTranslations, useLocale } from 'next-intl'
|
||||
import { LanguageSwitcher } from './language-switcher'
|
||||
|
||||
export function Navigation() {
|
||||
const [anchorElNav, setAnchorElNav] = useState<null | HTMLElement>(null)
|
||||
@@ -49,6 +42,21 @@ export function Navigation() {
|
||||
const router = useRouter()
|
||||
const theme = useTheme()
|
||||
const isMobile = useMediaQuery(theme.breakpoints.down('md'))
|
||||
const t = useTranslations('navigation')
|
||||
const locale = useLocale()
|
||||
|
||||
const pages = [
|
||||
{ name: t('home'), path: '/', icon: <Home /> },
|
||||
{ name: t('bible'), path: '/bible', icon: <MenuBook /> },
|
||||
{ name: t('prayers'), path: '/prayers', icon: <Prayer /> },
|
||||
{ name: t('search'), path: '/search', icon: <Search /> },
|
||||
]
|
||||
|
||||
const settings = [
|
||||
{ name: t('profile'), icon: <AccountCircle /> },
|
||||
{ name: t('settings'), icon: <Settings /> },
|
||||
{ name: t('logout'), icon: <Logout /> },
|
||||
]
|
||||
|
||||
const handleOpenNavMenu = (event: React.MouseEvent<HTMLElement>) => {
|
||||
setAnchorElNav(event.currentTarget)
|
||||
@@ -67,7 +75,8 @@ export function Navigation() {
|
||||
}
|
||||
|
||||
const handleNavigate = (path: string) => {
|
||||
router.push(path)
|
||||
const localizedPath = `/${locale}${path === '/' ? '' : path}`
|
||||
router.push(localizedPath)
|
||||
handleCloseNavMenu()
|
||||
setDrawerOpen(false)
|
||||
}
|
||||
@@ -104,7 +113,7 @@ export function Navigation() {
|
||||
variant="h6"
|
||||
noWrap
|
||||
component="a"
|
||||
href="/"
|
||||
href={`/${locale}`}
|
||||
sx={{
|
||||
mr: 2,
|
||||
display: { xs: 'none', md: 'flex' },
|
||||
@@ -138,7 +147,7 @@ export function Navigation() {
|
||||
variant="h5"
|
||||
noWrap
|
||||
component="a"
|
||||
href="/"
|
||||
href={`/${locale}`}
|
||||
sx={{
|
||||
mr: 2,
|
||||
display: { xs: 'flex', md: 'none' },
|
||||
@@ -177,9 +186,12 @@ export function Navigation() {
|
||||
))}
|
||||
</Box>
|
||||
|
||||
{/* Language Switcher */}
|
||||
<LanguageSwitcher />
|
||||
|
||||
{/* User Menu */}
|
||||
<Box sx={{ flexGrow: 0 }}>
|
||||
<Tooltip title="Deschide setări">
|
||||
<Tooltip title={t('settings')}>
|
||||
<IconButton onClick={handleOpenUserMenu} sx={{ p: 0 }}>
|
||||
<Avatar sx={{ bgcolor: 'secondary.main' }}>
|
||||
<AccountCircle />
|
||||
@@ -202,24 +214,14 @@ export function Navigation() {
|
||||
open={Boolean(anchorElUser)}
|
||||
onClose={handleCloseUserMenu}
|
||||
>
|
||||
<MenuItem onClick={handleCloseUserMenu}>
|
||||
<ListItemIcon>
|
||||
<AccountCircle fontSize="small" />
|
||||
</ListItemIcon>
|
||||
<Typography textAlign="center">Profil</Typography>
|
||||
</MenuItem>
|
||||
<MenuItem onClick={handleCloseUserMenu}>
|
||||
<ListItemIcon>
|
||||
<Settings fontSize="small" />
|
||||
</ListItemIcon>
|
||||
<Typography textAlign="center">Setări</Typography>
|
||||
</MenuItem>
|
||||
<MenuItem onClick={handleCloseUserMenu}>
|
||||
<ListItemIcon>
|
||||
<Logout fontSize="small" />
|
||||
</ListItemIcon>
|
||||
<Typography textAlign="center">Deconectare</Typography>
|
||||
</MenuItem>
|
||||
{settings.map((setting) => (
|
||||
<MenuItem key={setting.name} onClick={handleCloseUserMenu}>
|
||||
<ListItemIcon>
|
||||
{setting.icon}
|
||||
</ListItemIcon>
|
||||
<Typography textAlign="center">{setting.name}</Typography>
|
||||
</MenuItem>
|
||||
))}
|
||||
</Menu>
|
||||
</Box>
|
||||
</Toolbar>
|
||||
|
||||
14
i18n.ts
Normal file
14
i18n.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import {getRequestConfig} from 'next-intl/server';
|
||||
|
||||
// Can be imported from a shared config
|
||||
export const locales = ['ro', 'en'];
|
||||
|
||||
export default getRequestConfig(async ({locale}) => {
|
||||
// Ensure locale has a value, default to 'ro' if undefined
|
||||
const validLocale = locale || 'ro';
|
||||
|
||||
return {
|
||||
locale: validLocale,
|
||||
messages: (await import(`./messages/${validLocale}.json`)).default
|
||||
};
|
||||
});
|
||||
@@ -40,6 +40,7 @@ export async function getEmbedding(text: string): Promise<number[]> {
|
||||
|
||||
export async function searchBibleSemantic(
|
||||
query: string,
|
||||
language: string = 'ro',
|
||||
limit: number = 10
|
||||
): Promise<BibleVerse[]> {
|
||||
try {
|
||||
@@ -52,11 +53,11 @@ export async function searchBibleSemantic(
|
||||
SELECT ref, book, chapter, verse, text_raw,
|
||||
1 - (embedding <=> $1) AS similarity
|
||||
FROM bible_passages
|
||||
WHERE embedding IS NOT NULL
|
||||
WHERE embedding IS NOT NULL AND lang = $3
|
||||
ORDER BY embedding <=> $1
|
||||
LIMIT $2
|
||||
`,
|
||||
[JSON.stringify(queryEmbedding), limit]
|
||||
[JSON.stringify(queryEmbedding), limit, language]
|
||||
)
|
||||
|
||||
return result.rows
|
||||
@@ -71,11 +72,15 @@ export async function searchBibleSemantic(
|
||||
|
||||
export async function searchBibleHybrid(
|
||||
query: string,
|
||||
language: string = 'ro',
|
||||
limit: number = 10
|
||||
): Promise<BibleVerse[]> {
|
||||
try {
|
||||
const queryEmbedding = await getEmbedding(query)
|
||||
|
||||
// Use appropriate text search configuration based on language
|
||||
const textConfig = language === 'ro' ? 'romanian' : 'english'
|
||||
|
||||
const client = await pool.connect()
|
||||
try {
|
||||
const result = await client.query(
|
||||
@@ -83,25 +88,25 @@ export async function searchBibleHybrid(
|
||||
WITH vector_search AS (
|
||||
SELECT id, 1 - (embedding <=> $1) AS vector_sim
|
||||
FROM bible_passages
|
||||
WHERE embedding IS NOT NULL
|
||||
WHERE embedding IS NOT NULL AND lang = $4
|
||||
ORDER BY embedding <=> $1
|
||||
LIMIT 100
|
||||
),
|
||||
text_search AS (
|
||||
SELECT id, ts_rank(tsv, plainto_tsquery('romanian', $3)) AS text_rank
|
||||
SELECT id, ts_rank(tsv, plainto_tsquery($5, $3)) AS text_rank
|
||||
FROM bible_passages
|
||||
WHERE tsv @@ plainto_tsquery('romanian', $3)
|
||||
WHERE tsv @@ plainto_tsquery($5, $3) AND lang = $4
|
||||
)
|
||||
SELECT bp.ref, bp.book, bp.chapter, bp.verse, bp.text_raw,
|
||||
COALESCE(vs.vector_sim, 0) * 0.7 + COALESCE(ts.text_rank, 0) * 0.3 AS combined_score
|
||||
FROM bible_passages bp
|
||||
LEFT JOIN vector_search vs ON vs.id = bp.id
|
||||
LEFT JOIN text_search ts ON ts.id = bp.id
|
||||
WHERE vs.id IS NOT NULL OR ts.id IS NOT NULL
|
||||
WHERE (vs.id IS NOT NULL OR ts.id IS NOT NULL) AND bp.lang = $4
|
||||
ORDER BY combined_score DESC
|
||||
LIMIT $2
|
||||
`,
|
||||
[JSON.stringify(queryEmbedding), limit, query]
|
||||
[JSON.stringify(queryEmbedding), limit, query, language, textConfig]
|
||||
)
|
||||
|
||||
return result.rows
|
||||
|
||||
107
messages/en.json
Normal file
107
messages/en.json
Normal file
@@ -0,0 +1,107 @@
|
||||
{
|
||||
"navigation": {
|
||||
"home": "Home",
|
||||
"bible": "Bible",
|
||||
"prayers": "Prayers",
|
||||
"search": "Search",
|
||||
"profile": "Profile",
|
||||
"settings": "Settings",
|
||||
"logout": "Logout",
|
||||
"language": "Language"
|
||||
},
|
||||
"chat": {
|
||||
"title": "Biblical AI Chat",
|
||||
"subtitle": "Assistant for biblical questions",
|
||||
"placeholder": "Ask your biblical question...",
|
||||
"loading": "Writing response...",
|
||||
"send": "Send",
|
||||
"minimize": "Minimize",
|
||||
"close": "Close",
|
||||
"openFullPage": "Open full page",
|
||||
"enterToSend": "Enter to send • Shift+Enter for new line",
|
||||
"suggestions": {
|
||||
"title": "Suggested questions:",
|
||||
"questions": [
|
||||
"What does the Bible say about love?",
|
||||
"Explain the parable of the sower",
|
||||
"What are the fruits of the Spirit?",
|
||||
"What does it mean to be born again?",
|
||||
"How can I pray better?"
|
||||
]
|
||||
}
|
||||
},
|
||||
"home": {
|
||||
"hero": {
|
||||
"title": "Biblical Guide",
|
||||
"subtitle": "Explore Scripture with artificial intelligence",
|
||||
"description": "A modern platform for Bible study, with intelligent AI chat, advanced search, and a prayer community that supports you on your spiritual journey.",
|
||||
"cta": {
|
||||
"readBible": "Start reading",
|
||||
"askAI": "Ask AI"
|
||||
}
|
||||
},
|
||||
"features": {
|
||||
"title": "Discover the features",
|
||||
"subtitle": "Everything you need for a complete Bible study experience",
|
||||
"bible": {
|
||||
"title": "Read the Bible",
|
||||
"description": "Explore Scripture with a modern and easy-to-use interface"
|
||||
},
|
||||
"chat": {
|
||||
"title": "AI Chat",
|
||||
"description": "Ask questions about Scripture and receive clear answers"
|
||||
},
|
||||
"prayers": {
|
||||
"title": "Prayers",
|
||||
"description": "Share prayers and pray together with the community"
|
||||
},
|
||||
"search": {
|
||||
"title": "Search",
|
||||
"description": "Search for verses and passages throughout Scripture"
|
||||
}
|
||||
},
|
||||
"stats": {
|
||||
"books": "Biblical books",
|
||||
"verses": "Verses",
|
||||
"aiAvailable": "AI Chat available"
|
||||
},
|
||||
"cta": {
|
||||
"title": "Begin your spiritual journey",
|
||||
"description": "Join our community and discover the wisdom of Scripture",
|
||||
"startNow": "Start now"
|
||||
}
|
||||
},
|
||||
"pages": {
|
||||
"bible": {
|
||||
"title": "Bible",
|
||||
"selectBook": "Select book",
|
||||
"selectChapter": "Select chapter",
|
||||
"verse": "Verse",
|
||||
"chapter": "Chapter"
|
||||
},
|
||||
"prayers": {
|
||||
"title": "Prayers",
|
||||
"addRequest": "Add prayer request",
|
||||
"anonymous": "Anonymous",
|
||||
"prayFor": "Pray for this"
|
||||
},
|
||||
"search": {
|
||||
"title": "Search",
|
||||
"placeholder": "Search the Bible...",
|
||||
"results": "Results",
|
||||
"noResults": "No results found"
|
||||
}
|
||||
},
|
||||
"common": {
|
||||
"loading": "Loading...",
|
||||
"error": "An error occurred",
|
||||
"save": "Save",
|
||||
"cancel": "Cancel",
|
||||
"delete": "Delete",
|
||||
"edit": "Edit",
|
||||
"close": "Close",
|
||||
"back": "Back",
|
||||
"next": "Next",
|
||||
"previous": "Previous"
|
||||
}
|
||||
}
|
||||
107
messages/ro.json
Normal file
107
messages/ro.json
Normal file
@@ -0,0 +1,107 @@
|
||||
{
|
||||
"navigation": {
|
||||
"home": "Acasă",
|
||||
"bible": "Biblia",
|
||||
"prayers": "Rugăciuni",
|
||||
"search": "Căutare",
|
||||
"profile": "Profil",
|
||||
"settings": "Setări",
|
||||
"logout": "Deconectare",
|
||||
"language": "Limba"
|
||||
},
|
||||
"chat": {
|
||||
"title": "Chat AI Biblic",
|
||||
"subtitle": "Asistent pentru întrebări biblice",
|
||||
"placeholder": "Scrie întrebarea ta despre Biblie...",
|
||||
"loading": "Scriu răspunsul...",
|
||||
"send": "Trimite",
|
||||
"minimize": "Minimizează",
|
||||
"close": "Închide",
|
||||
"openFullPage": "Deschide în pagină completă",
|
||||
"enterToSend": "Enter pentru a trimite • Shift+Enter pentru linie nouă",
|
||||
"suggestions": {
|
||||
"title": "Întrebări sugerate:",
|
||||
"questions": [
|
||||
"Ce spune Biblia despre iubire?",
|
||||
"Explică-mi parabola semănătorului",
|
||||
"Care sunt fructele Duhului?",
|
||||
"Ce înseamnă să fii născut din nou?",
|
||||
"Cum pot să mă rog mai bine?"
|
||||
]
|
||||
}
|
||||
},
|
||||
"home": {
|
||||
"hero": {
|
||||
"title": "Ghid Biblic",
|
||||
"subtitle": "Explorează Scriptura cu ajutorul inteligenței artificiale",
|
||||
"description": "O platformă modernă pentru studiul Bibliei, cu chat AI inteligent, căutare avansată și o comunitate de rugăciune care te sprijină în călătoria ta spirituală.",
|
||||
"cta": {
|
||||
"readBible": "Începe să citești",
|
||||
"askAI": "Întreabă AI"
|
||||
}
|
||||
},
|
||||
"features": {
|
||||
"title": "Descoperă funcționalitățile",
|
||||
"subtitle": "Totul de ce ai nevoie pentru o experiență completă de studiu biblic",
|
||||
"bible": {
|
||||
"title": "Citește Biblia",
|
||||
"description": "Explorează Scriptura cu o interfață modernă și ușor de folosit"
|
||||
},
|
||||
"chat": {
|
||||
"title": "Chat cu AI",
|
||||
"description": "Pune întrebări despre Scriptură și primește răspunsuri clare"
|
||||
},
|
||||
"prayers": {
|
||||
"title": "Rugăciuni",
|
||||
"description": "Partajează rugăciuni și roagă-te împreună cu comunitatea"
|
||||
},
|
||||
"search": {
|
||||
"title": "Căutare",
|
||||
"description": "Caută versete și pasaje din întreaga Scriptură"
|
||||
}
|
||||
},
|
||||
"stats": {
|
||||
"books": "Cărți biblice",
|
||||
"verses": "Versete",
|
||||
"aiAvailable": "Chat AI disponibil"
|
||||
},
|
||||
"cta": {
|
||||
"title": "Începe călătoria ta spirituală",
|
||||
"description": "Alătură-te comunității noastre și descoperă înțelepciunea Scripturii",
|
||||
"startNow": "Începe acum"
|
||||
}
|
||||
},
|
||||
"pages": {
|
||||
"bible": {
|
||||
"title": "Biblia",
|
||||
"selectBook": "Selectează cartea",
|
||||
"selectChapter": "Selectează capitolul",
|
||||
"verse": "Versetul",
|
||||
"chapter": "Capitolul"
|
||||
},
|
||||
"prayers": {
|
||||
"title": "Rugăciuni",
|
||||
"addRequest": "Adaugă cerere de rugăciune",
|
||||
"anonymous": "Anonim",
|
||||
"prayFor": "Mă rog pentru aceasta"
|
||||
},
|
||||
"search": {
|
||||
"title": "Căutare",
|
||||
"placeholder": "Caută în Biblie...",
|
||||
"results": "Rezultate",
|
||||
"noResults": "Nu s-au găsit rezultate"
|
||||
}
|
||||
},
|
||||
"common": {
|
||||
"loading": "Se încarcă...",
|
||||
"error": "A apărut o eroare",
|
||||
"save": "Salvează",
|
||||
"cancel": "Anulează",
|
||||
"delete": "Șterge",
|
||||
"edit": "Editează",
|
||||
"close": "Închide",
|
||||
"back": "Înapoi",
|
||||
"next": "Următorul",
|
||||
"previous": "Anterior"
|
||||
}
|
||||
}
|
||||
25
middleware-test.ts
Normal file
25
middleware-test.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { NextResponse } from 'next/server'
|
||||
import type { NextRequest } from 'next/server'
|
||||
|
||||
export async function middleware(request: NextRequest) {
|
||||
console.log('Middleware called for:', request.nextUrl.pathname)
|
||||
|
||||
if (request.nextUrl.pathname === '/') {
|
||||
console.log('Redirecting / to /ro')
|
||||
return NextResponse.redirect(new URL('/ro', request.url))
|
||||
}
|
||||
|
||||
if (request.nextUrl.pathname.startsWith('/ro') || request.nextUrl.pathname.startsWith('/en')) {
|
||||
console.log('Allowing locale route:', request.nextUrl.pathname)
|
||||
return NextResponse.next()
|
||||
}
|
||||
|
||||
console.log('Default behavior for:', request.nextUrl.pathname)
|
||||
return NextResponse.next()
|
||||
}
|
||||
|
||||
export const config = {
|
||||
matcher: [
|
||||
'/((?!api|_next|_vercel|.*\\..*).*)',
|
||||
],
|
||||
}
|
||||
@@ -2,6 +2,13 @@ import { NextResponse } from 'next/server'
|
||||
import type { NextRequest } from 'next/server'
|
||||
import { verifyToken } from '@/lib/auth'
|
||||
import { prisma } from '@/lib/db'
|
||||
import createIntlMiddleware from 'next-intl/middleware'
|
||||
|
||||
// Internationalization configuration
|
||||
const intlMiddleware = createIntlMiddleware({
|
||||
locales: ['ro', 'en'],
|
||||
defaultLocale: 'ro'
|
||||
})
|
||||
|
||||
// Rate limiting configuration
|
||||
const RATE_LIMIT_WINDOW = 60 * 1000 // 1 minute
|
||||
@@ -58,6 +65,11 @@ async function checkRateLimit(request: NextRequest, endpoint: string, limit: num
|
||||
}
|
||||
|
||||
export async function middleware(request: NextRequest) {
|
||||
// Handle internationalization for non-API routes
|
||||
if (!request.nextUrl.pathname.startsWith('/api')) {
|
||||
return intlMiddleware(request)
|
||||
}
|
||||
|
||||
// Determine endpoint type for rate limiting
|
||||
let endpoint = 'general'
|
||||
let limit = RATE_LIMITS.general
|
||||
@@ -155,6 +167,12 @@ export async function middleware(request: NextRequest) {
|
||||
|
||||
export const config = {
|
||||
matcher: [
|
||||
// Match all pathnames except for
|
||||
// - api routes
|
||||
// - _next (Next.js internals)
|
||||
// - static files (images, etc.)
|
||||
'/((?!api|_next|_vercel|.*\\..*).*)',
|
||||
// However, match all pathnames within `/api`, except for the Middleware to run there
|
||||
'/api/:path*',
|
||||
'/dashboard/:path*'
|
||||
],
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
const withNextIntl = require('next-intl/plugin')('./i18n.ts');
|
||||
|
||||
/** @type {import('next').NextConfig} */
|
||||
const nextConfig = {
|
||||
output: 'standalone',
|
||||
typedRoutes: false,
|
||||
}
|
||||
|
||||
module.exports = nextConfig
|
||||
module.exports = withNextIntl(nextConfig)
|
||||
138
package-lock.json
generated
138
package-lock.json
generated
@@ -11,6 +11,7 @@
|
||||
"dependencies": {
|
||||
"@emotion/react": "^11.14.0",
|
||||
"@emotion/styled": "^11.14.1",
|
||||
"@formatjs/intl-localematcher": "^0.6.1",
|
||||
"@mui/icons-material": "^7.3.2",
|
||||
"@mui/material": "^7.3.2",
|
||||
"@mui/material-nextjs": "^7.3.2",
|
||||
@@ -33,7 +34,9 @@
|
||||
"clsx": "^2.1.1",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"lucide-react": "^0.544.0",
|
||||
"negotiator": "^1.0.0",
|
||||
"next": "^15.5.3",
|
||||
"next-intl": "^4.3.9",
|
||||
"openai": "^5.22.0",
|
||||
"pdf-parse": "^1.1.1",
|
||||
"pg": "^8.16.3",
|
||||
@@ -1417,6 +1420,57 @@
|
||||
"integrity": "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@formatjs/ecma402-abstract": {
|
||||
"version": "2.3.4",
|
||||
"resolved": "https://registry.npmjs.org/@formatjs/ecma402-abstract/-/ecma402-abstract-2.3.4.tgz",
|
||||
"integrity": "sha512-qrycXDeaORzIqNhBOx0btnhpD1c+/qFIHAN9znofuMJX6QBwtbrmlpWfD4oiUUD2vJUOIYFA/gYtg2KAMGG7sA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@formatjs/fast-memoize": "2.2.7",
|
||||
"@formatjs/intl-localematcher": "0.6.1",
|
||||
"decimal.js": "^10.4.3",
|
||||
"tslib": "^2.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@formatjs/fast-memoize": {
|
||||
"version": "2.2.7",
|
||||
"resolved": "https://registry.npmjs.org/@formatjs/fast-memoize/-/fast-memoize-2.2.7.tgz",
|
||||
"integrity": "sha512-Yabmi9nSvyOMrlSeGGWDiH7rf3a7sIwplbvo/dlz9WCIjzIQAfy1RMf4S0X3yG724n5Ghu2GmEl5NJIV6O9sZQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"tslib": "^2.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@formatjs/icu-messageformat-parser": {
|
||||
"version": "2.11.2",
|
||||
"resolved": "https://registry.npmjs.org/@formatjs/icu-messageformat-parser/-/icu-messageformat-parser-2.11.2.tgz",
|
||||
"integrity": "sha512-AfiMi5NOSo2TQImsYAg8UYddsNJ/vUEv/HaNqiFjnI3ZFfWihUtD5QtuX6kHl8+H+d3qvnE/3HZrfzgdWpsLNA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@formatjs/ecma402-abstract": "2.3.4",
|
||||
"@formatjs/icu-skeleton-parser": "1.8.14",
|
||||
"tslib": "^2.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@formatjs/icu-skeleton-parser": {
|
||||
"version": "1.8.14",
|
||||
"resolved": "https://registry.npmjs.org/@formatjs/icu-skeleton-parser/-/icu-skeleton-parser-1.8.14.tgz",
|
||||
"integrity": "sha512-i4q4V4qslThK4Ig8SxyD76cp3+QJ3sAqr7f6q9VVfeGtxG9OhiAk3y9XF6Q41OymsKzsGQ6OQQoJNY4/lI8TcQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@formatjs/ecma402-abstract": "2.3.4",
|
||||
"tslib": "^2.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@formatjs/intl-localematcher": {
|
||||
"version": "0.6.1",
|
||||
"resolved": "https://registry.npmjs.org/@formatjs/intl-localematcher/-/intl-localematcher-0.6.1.tgz",
|
||||
"integrity": "sha512-ePEgLgVCqi2BBFnTMWPfIghu6FkbZnnBVhO2sSxvLfrdFw7wCHAHiDoM2h4NRgjbaY7+B7HgOLZGkK187pZTZg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"tslib": "^2.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/colour": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.0.0.tgz",
|
||||
@@ -3599,6 +3653,12 @@
|
||||
"integrity": "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@schummar/icu-type-parser": {
|
||||
"version": "1.21.5",
|
||||
"resolved": "https://registry.npmjs.org/@schummar/icu-type-parser/-/icu-type-parser-1.21.5.tgz",
|
||||
"integrity": "sha512-bXHSaW5jRTmke9Vd0h5P7BtWZG9Znqb8gSDxZnxaGSJnGwPLDPfS+3g0BKzeWqzgZPsIVZkM7m2tbo18cm5HBw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@sinclair/typebox": {
|
||||
"version": "0.34.41",
|
||||
"resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.41.tgz",
|
||||
@@ -4554,6 +4614,15 @@
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/accepts/node_modules/negotiator": {
|
||||
"version": "0.6.3",
|
||||
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
|
||||
"integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/agent-base": {
|
||||
"version": "7.1.4",
|
||||
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz",
|
||||
@@ -5428,7 +5497,6 @@
|
||||
"version": "10.6.0",
|
||||
"resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz",
|
||||
"integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/decode-named-character-reference": {
|
||||
@@ -6451,6 +6519,18 @@
|
||||
"integrity": "sha512-0aO8FkhNZlj/ZIbNi7Lxxr12obT7cL1moPfE4tg1LkX7LlLfC6DeX4l2ZEud1ukP9jNQyNnfzQVqwbwmAATY4Q==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/intl-messageformat": {
|
||||
"version": "10.7.16",
|
||||
"resolved": "https://registry.npmjs.org/intl-messageformat/-/intl-messageformat-10.7.16.tgz",
|
||||
"integrity": "sha512-UmdmHUmp5CIKKjSoE10la5yfU+AYJAaiYLsodbjL4lji83JNvgOQUjGaGhGrpFCb0Uh7sl7qfP1IyILa8Z40ug==",
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"@formatjs/ecma402-abstract": "2.3.4",
|
||||
"@formatjs/fast-memoize": "2.2.7",
|
||||
"@formatjs/icu-messageformat-parser": "2.11.2",
|
||||
"tslib": "^2.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/is-alphabetical": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-2.0.1.tgz",
|
||||
@@ -9180,9 +9260,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/negotiator": {
|
||||
"version": "0.6.3",
|
||||
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
|
||||
"integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==",
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz",
|
||||
"integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
@@ -9247,6 +9327,42 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/next-intl": {
|
||||
"version": "4.3.9",
|
||||
"resolved": "https://registry.npmjs.org/next-intl/-/next-intl-4.3.9.tgz",
|
||||
"integrity": "sha512-4oSROHlgy8a5Qr2vH69wxo9F6K0uc6nZM2GNzqSe6ET79DEzOmBeSijCRzD5txcI4i+XTGytu4cxFsDXLKEDpQ==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "individual",
|
||||
"url": "https://github.com/sponsors/amannn"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@formatjs/intl-localematcher": "^0.5.4",
|
||||
"negotiator": "^1.0.0",
|
||||
"use-intl": "^4.3.9"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"next": "^12.0.0 || ^13.0.0 || ^14.0.0 || ^15.0.0",
|
||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || >=19.0.0-rc <19.0.0 || ^19.0.0",
|
||||
"typescript": "^5.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"typescript": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/next-intl/node_modules/@formatjs/intl-localematcher": {
|
||||
"version": "0.5.10",
|
||||
"resolved": "https://registry.npmjs.org/@formatjs/intl-localematcher/-/intl-localematcher-0.5.10.tgz",
|
||||
"integrity": "sha512-af3qATX+m4Rnd9+wHcjJ4w2ijq+rAVP3CCinJQvFv1kgSu1W6jypUmvleJxcewdxmutM8dmIRZFxO/IQBZmP2Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"tslib": "2"
|
||||
}
|
||||
},
|
||||
"node_modules/next/node_modules/postcss": {
|
||||
"version": "8.4.31",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz",
|
||||
@@ -11478,6 +11594,20 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/use-intl": {
|
||||
"version": "4.3.9",
|
||||
"resolved": "https://registry.npmjs.org/use-intl/-/use-intl-4.3.9.tgz",
|
||||
"integrity": "sha512-bZu+h13HIgOvsoGleQtUe4E6gM49CRm+AH36KnJVB/qb1+Beo7jr7HNrR8YWH8oaOkQfGNm6vh0HTepxng8UTg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@formatjs/fast-memoize": "^2.2.0",
|
||||
"@schummar/icu-type-parser": "1.21.5",
|
||||
"intl-messageformat": "^10.5.14"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^17.0.0 || ^18.0.0 || >=19.0.0-rc <19.0.0 || ^19.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/use-sidecar": {
|
||||
"version": "1.1.3",
|
||||
"resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.3.tgz",
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
"dependencies": {
|
||||
"@emotion/react": "^11.14.0",
|
||||
"@emotion/styled": "^11.14.1",
|
||||
"@formatjs/intl-localematcher": "^0.6.1",
|
||||
"@mui/icons-material": "^7.3.2",
|
||||
"@mui/material": "^7.3.2",
|
||||
"@mui/material-nextjs": "^7.3.2",
|
||||
@@ -46,7 +47,9 @@
|
||||
"clsx": "^2.1.1",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"lucide-react": "^0.544.0",
|
||||
"negotiator": "^1.0.0",
|
||||
"next": "^15.5.3",
|
||||
"next-intl": "^4.3.9",
|
||||
"openai": "^5.22.0",
|
||||
"pdf-parse": "^1.1.1",
|
||||
"pg": "^8.16.3",
|
||||
|
||||
Reference in New Issue
Block a user