feat: Apple-style donation-focused landing page + Azure OpenAI fixes

Major updates:
- Replace homepage with clean, minimalist Apple-style landing page
- Focus on donation messaging and mission statement
- Add comprehensive AI chat analysis documentation
- Fix Azure OpenAI configuration with correct endpoints
- Update embedding API to use text-embedding-ada-002 (1536 dims)

Landing Page Features:
- Hero section with tagline "Every Scripture. Every Language. Forever Free"
- Mission statement emphasizing free access
- Matthew 10:8 verse highlight
- 6 feature cards (Global Library, Multilingual, Prayer Wall, AI Chat, Privacy, Offline)
- Donation CTA sections with PayPal and card options
- "Why It Matters" section with dark background
- Clean footer with navigation links

Technical Changes:
- Updated .env.local with new Azure credentials
- Fixed vector-search.ts to support separate embed API version
- Integrated AuthModal into Bible reader and prayers page
- Made prayer filters collapsible and mobile-responsive
- Changed language picker to single-select

Documentation Created:
- AI_CHAT_FIX_PLAN.md - Comprehensive implementation plan
- AI_CHAT_VERIFICATION_FINDINGS.md - Database analysis
- AI_CHAT_ANALYSIS_SUMMARY.md - Executive summary
- AI_CHAT_STATUS_UPDATE.md - Current status and next steps
- logo.svg - App logo (MenuBook icon)

Build:  Successful (Next.js 15.5.3)

🤖 Generated with Claude Code

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-10-10 22:38:19 +00:00
parent 71047c85cc
commit 79f1512f3a
17 changed files with 4066 additions and 666 deletions

View File

@@ -0,0 +1,334 @@
'use client'
import { useState } from 'react'
import {
Dialog,
DialogContent,
DialogTitle,
TextField,
Button,
Box,
Typography,
Alert,
IconButton,
Tabs,
Tab,
InputAdornment,
CircularProgress
} from '@mui/material'
import { Close, Visibility, VisibilityOff } from '@mui/icons-material'
import { useTranslations } from 'next-intl'
import { useAuth } from '@/hooks/use-auth'
interface AuthModalProps {
open: boolean
onClose: () => void
onSuccess?: () => void
defaultTab?: 'login' | 'register'
title?: string
message?: string
}
export function AuthModal({
open,
onClose,
onSuccess,
defaultTab = 'login',
title,
message
}: AuthModalProps) {
const t = useTranslations('auth')
const { login, register } = useAuth()
const [tab, setTab] = useState<'login' | 'register'>(defaultTab)
const [showPassword, setShowPassword] = useState(false)
const [loading, setLoading] = useState(false)
const [error, setError] = useState<string | null>(null)
// Login form state
const [loginData, setLoginData] = useState({
email: '',
password: ''
})
// Register form state
const [registerData, setRegisterData] = useState({
name: '',
email: '',
password: '',
confirmPassword: ''
})
const handleTabChange = (event: React.SyntheticEvent, newValue: 'login' | 'register') => {
setTab(newValue)
setError(null)
}
const handleLoginSubmit = async (e: React.FormEvent) => {
e.preventDefault()
setError(null)
setLoading(true)
try {
const success = await login(loginData.email, loginData.password)
if (success) {
// Clear form
setLoginData({ email: '', password: '' })
// Call success callback
onSuccess?.()
// Close modal
onClose()
} else {
setError(t('loginError'))
}
} catch (err: any) {
setError(err.message || t('connectionError'))
} finally {
setLoading(false)
}
}
const handleRegisterSubmit = async (e: React.FormEvent) => {
e.preventDefault()
setError(null)
// Validate passwords match
if (registerData.password !== registerData.confirmPassword) {
setError(t('passwordMismatch'))
return
}
setLoading(true)
try {
const success = await register(
registerData.email,
registerData.password,
registerData.name || undefined
)
if (success) {
// Clear form
setRegisterData({ name: '', email: '', password: '', confirmPassword: '' })
// Call success callback
onSuccess?.()
// Close modal
onClose()
} else {
setError(t('registerError'))
}
} catch (err: any) {
setError(err.message || t('connectionError'))
} finally {
setLoading(false)
}
}
const handleClose = () => {
if (!loading) {
setError(null)
onClose()
}
}
return (
<Dialog
open={open}
onClose={handleClose}
maxWidth="sm"
fullWidth
PaperProps={{
sx: {
borderRadius: 2
}
}}
>
<DialogTitle sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', pb: 1 }}>
<Typography variant="h6">
{title || (tab === 'login' ? t('welcomeBack') : t('joinUs'))}
</Typography>
<IconButton
onClick={handleClose}
disabled={loading}
size="small"
sx={{ color: 'text.secondary' }}
>
<Close />
</IconButton>
</DialogTitle>
<DialogContent>
{message && (
<Alert severity="info" sx={{ mb: 3 }}>
{message}
</Alert>
)}
<Tabs
value={tab}
onChange={handleTabChange}
variant="fullWidth"
sx={{ mb: 3, borderBottom: 1, borderColor: 'divider' }}
>
<Tab label={t('login')} value="login" />
<Tab label={t('register')} value="register" />
</Tabs>
{error && (
<Alert severity="error" sx={{ mb: 2 }}>
{error}
</Alert>
)}
{tab === 'login' ? (
<Box component="form" onSubmit={handleLoginSubmit}>
<TextField
fullWidth
label={t('email')}
type="email"
value={loginData.email}
onChange={(e) => setLoginData(prev => ({ ...prev, email: e.target.value }))}
required
disabled={loading}
sx={{ mb: 2 }}
autoComplete="email"
/>
<TextField
fullWidth
label={t('password')}
type={showPassword ? 'text' : 'password'}
value={loginData.password}
onChange={(e) => setLoginData(prev => ({ ...prev, password: e.target.value }))}
required
disabled={loading}
sx={{ mb: 3 }}
autoComplete="current-password"
InputProps={{
endAdornment: (
<InputAdornment position="end">
<IconButton
onClick={() => setShowPassword(!showPassword)}
edge="end"
disabled={loading}
>
{showPassword ? <VisibilityOff /> : <Visibility />}
</IconButton>
</InputAdornment>
)
}}
/>
<Button
type="submit"
variant="contained"
fullWidth
size="large"
disabled={loading}
sx={{ mb: 2 }}
>
{loading ? <CircularProgress size={24} /> : t('login')}
</Button>
<Box sx={{ textAlign: 'center' }}>
<Typography variant="body2" color="text.secondary">
{t('noAccount')}{' '}
<Button
onClick={() => setTab('register')}
disabled={loading}
sx={{ textTransform: 'none', p: 0, minWidth: 'auto' }}
>
{t('createAccount')}
</Button>
</Typography>
</Box>
</Box>
) : (
<Box component="form" onSubmit={handleRegisterSubmit}>
<TextField
fullWidth
label={`${t('name')} ${t('optional')}`}
type="text"
value={registerData.name}
onChange={(e) => setRegisterData(prev => ({ ...prev, name: e.target.value }))}
disabled={loading}
sx={{ mb: 2 }}
autoComplete="name"
/>
<TextField
fullWidth
label={t('email')}
type="email"
value={registerData.email}
onChange={(e) => setRegisterData(prev => ({ ...prev, email: e.target.value }))}
required
disabled={loading}
sx={{ mb: 2 }}
autoComplete="email"
/>
<TextField
fullWidth
label={t('password')}
type={showPassword ? 'text' : 'password'}
value={registerData.password}
onChange={(e) => setRegisterData(prev => ({ ...prev, password: e.target.value }))}
required
disabled={loading}
sx={{ mb: 2 }}
autoComplete="new-password"
InputProps={{
endAdornment: (
<InputAdornment position="end">
<IconButton
onClick={() => setShowPassword(!showPassword)}
edge="end"
disabled={loading}
>
{showPassword ? <VisibilityOff /> : <Visibility />}
</IconButton>
</InputAdornment>
)
}}
/>
<TextField
fullWidth
label={t('confirmPassword')}
type={showPassword ? 'text' : 'password'}
value={registerData.confirmPassword}
onChange={(e) => setRegisterData(prev => ({ ...prev, confirmPassword: e.target.value }))}
required
disabled={loading}
sx={{ mb: 3 }}
autoComplete="new-password"
error={registerData.confirmPassword !== '' && registerData.password !== registerData.confirmPassword}
helperText={
registerData.confirmPassword !== '' && registerData.password !== registerData.confirmPassword
? t('passwordMismatch')
: ''
}
/>
<Button
type="submit"
variant="contained"
fullWidth
size="large"
disabled={loading}
sx={{ mb: 2 }}
>
{loading ? <CircularProgress size={24} /> : t('register')}
</Button>
<Box sx={{ textAlign: 'center' }}>
<Typography variant="body2" color="text.secondary">
{t('alreadyHaveAccount')}{' '}
<Button
onClick={() => setTab('login')}
disabled={loading}
sx={{ textTransform: 'none', p: 0, minWidth: 'auto' }}
>
{t('login')}
</Button>
</Typography>
</Box>
</Box>
)}
</DialogContent>
</Dialog>
)
}

View File

@@ -45,6 +45,7 @@ import {
import { useState, useRef, useEffect, useCallback } from 'react'
import { useTranslations, useLocale } from 'next-intl'
import ReactMarkdown from 'react-markdown'
import { AuthModal } from '@/components/auth/auth-modal'
interface ChatMessage {
id: string
@@ -95,6 +96,7 @@ export default function FloatingChat() {
const [showRenameDialog, setShowRenameDialog] = useState(false)
const [showDeleteDialog, setShowDeleteDialog] = useState(false)
const [newTitle, setNewTitle] = useState('')
const [authModalOpen, setAuthModalOpen] = useState(false)
const messagesEndRef = useRef<HTMLDivElement>(null)
const scrollToBottom = () => {
@@ -123,6 +125,15 @@ export default function FloatingChat() {
return () => window.removeEventListener('floating-chat:open', handler as EventListener)
}, [])
// Listen for auth sign-in required events
useEffect(() => {
const handler = () => {
setAuthModalOpen(true)
}
window.addEventListener('auth:sign-in-required', handler as EventListener)
return () => window.removeEventListener('auth:sign-in-required', handler as EventListener)
}, [])
// Check authentication status
useEffect(() => {
checkAuthStatus()
@@ -429,6 +440,12 @@ export default function FloatingChat() {
const toggleFullscreen = () => setIsFullscreen(prev => !prev)
const handleAuthSuccess = () => {
setAuthModalOpen(false)
// Re-check auth status to update the UI
checkAuthStatus()
}
return (
<>
{/* Floating Action Button */}
@@ -999,6 +1016,17 @@ export default function FloatingChat() {
)}
</Paper>
</Slide>
{/* Auth Modal */}
<AuthModal
open={authModalOpen}
onClose={() => setAuthModalOpen(false)}
onSuccess={handleAuthSuccess}
message={locale === 'ro'
? 'Vă rugăm să vă autentificați pentru a accesa chat-ul AI și a salva conversațiile.'
: 'Please sign in to access the AI chat and save your conversations.'}
defaultTab="login"
/>
</>
)
}