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:
@@ -9,6 +9,7 @@ import { OfflineBibleReader } from '@/components/bible/offline-bible-reader'
|
||||
import { offlineStorage } from '@/lib/offline-storage'
|
||||
import { InstallPrompt, useInstallPrompt } from '@/components/pwa/install-prompt'
|
||||
import { useSwipeable } from 'react-swipeable'
|
||||
import { AuthModal } from '@/components/auth/auth-modal'
|
||||
import {
|
||||
Box,
|
||||
Typography,
|
||||
@@ -315,6 +316,11 @@ export default function BibleReaderNew({ initialVersion, initialBook, initialCha
|
||||
verse: null
|
||||
})
|
||||
|
||||
// Auth modal state
|
||||
const [authModalOpen, setAuthModalOpen] = useState(false)
|
||||
const [authModalMessage, setAuthModalMessage] = useState<string>('')
|
||||
const [pendingAction, setPendingAction] = useState<(() => void) | null>(null)
|
||||
|
||||
// Refs
|
||||
const contentRef = useRef<HTMLDivElement>(null)
|
||||
const verseRefs = useRef<{[key: number]: HTMLDivElement}>({})
|
||||
@@ -990,6 +996,26 @@ export default function BibleReaderNew({ initialVersion, initialBook, initialCha
|
||||
}
|
||||
}
|
||||
|
||||
const requireAuth = (action: () => void, message: string) => {
|
||||
if (!user) {
|
||||
setAuthModalMessage(message)
|
||||
setPendingAction(() => action)
|
||||
setAuthModalOpen(true)
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
const handleAuthSuccess = () => {
|
||||
setAuthModalOpen(false)
|
||||
setAuthModalMessage('')
|
||||
// Execute pending action if there is one
|
||||
if (pendingAction) {
|
||||
pendingAction()
|
||||
setPendingAction(null)
|
||||
}
|
||||
}
|
||||
|
||||
const handlePreviousChapter = () => {
|
||||
// Trigger transition animation
|
||||
setIsTransitioning(true)
|
||||
@@ -1077,9 +1103,8 @@ export default function BibleReaderNew({ initialVersion, initialBook, initialCha
|
||||
const handleChapterBookmark = async () => {
|
||||
if (!selectedBook || !selectedChapter) return
|
||||
|
||||
// If user is not authenticated, redirect to login
|
||||
if (!user) {
|
||||
router.push(`/${locale}/login?redirect=${encodeURIComponent(`/${locale}/bible?version=${selectedVersion}&book=${selectedBook}&chapter=${selectedChapter}`)}`)
|
||||
// If user is not authenticated, show auth modal
|
||||
if (!requireAuth(handleChapterBookmark, 'Please login to bookmark this chapter')) {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -1123,9 +1148,8 @@ export default function BibleReaderNew({ initialVersion, initialBook, initialCha
|
||||
}
|
||||
|
||||
const handleVerseBookmark = async (verse: BibleVerse) => {
|
||||
// If user is not authenticated, redirect to login
|
||||
if (!user) {
|
||||
router.push(`/${locale}/login?redirect=${encodeURIComponent(`/${locale}/bible?version=${selectedVersion}&book=${selectedBook}&chapter=${selectedChapter}&verse=${verse.verseNum}`)}`)
|
||||
// If user is not authenticated, show auth modal
|
||||
if (!requireAuth(() => handleVerseBookmark(verse), 'Please login to bookmark this verse')) {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -1184,9 +1208,8 @@ export default function BibleReaderNew({ initialVersion, initialBook, initialCha
|
||||
}
|
||||
|
||||
const handleVerseChat = (verse: BibleVerse) => {
|
||||
// If user is not authenticated, redirect to login
|
||||
if (!user) {
|
||||
router.push(`/${locale}/login?redirect=${encodeURIComponent(`/${locale}/bible?version=${selectedVersion}&book=${selectedBook}&chapter=${selectedChapter}&verse=${verse.verseNum}`)}`)
|
||||
// If user is not authenticated, show auth modal
|
||||
if (!requireAuth(() => handleVerseChat(verse), 'Please login to ask AI about this verse')) {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -1255,9 +1278,8 @@ export default function BibleReaderNew({ initialVersion, initialBook, initialCha
|
||||
}
|
||||
|
||||
const handleHighlightVerse = async (verse: BibleVerse, color: TextHighlight['color']) => {
|
||||
// If user is not authenticated, redirect to login
|
||||
if (!user) {
|
||||
router.push(`/${locale}/login?redirect=${encodeURIComponent(`/${locale}/bible?version=${selectedVersion}&book=${selectedBook}&chapter=${selectedChapter}&verse=${verse.verseNum}`)}`)
|
||||
// If user is not authenticated, show auth modal
|
||||
if (!requireAuth(() => handleHighlightVerse(verse, color), 'Please login to highlight this verse')) {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -1397,8 +1419,8 @@ export default function BibleReaderNew({ initialVersion, initialBook, initialCha
|
||||
}
|
||||
|
||||
const handleSetFavoriteVersion = async () => {
|
||||
if (!user) {
|
||||
router.push(`/${locale}/login?redirect=${encodeURIComponent(`/${locale}/bible?version=${selectedVersion}&book=${selectedBook}&chapter=${selectedChapter}`)}`)
|
||||
// If user is not authenticated, show auth modal
|
||||
if (!requireAuth(handleSetFavoriteVersion, 'Please login to set your default Bible version')) {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -2667,6 +2689,19 @@ export default function BibleReaderNew({ initialVersion, initialBook, initialCha
|
||||
{copyFeedback.message}
|
||||
</Alert>
|
||||
</Snackbar>
|
||||
|
||||
{/* Auth Modal */}
|
||||
<AuthModal
|
||||
open={authModalOpen}
|
||||
onClose={() => {
|
||||
setAuthModalOpen(false)
|
||||
setAuthModalMessage('')
|
||||
setPendingAction(null)
|
||||
}}
|
||||
onSuccess={handleAuthSuccess}
|
||||
message={authModalMessage}
|
||||
defaultTab="login"
|
||||
/>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -18,6 +18,7 @@ import {
|
||||
ListItemText,
|
||||
MenuItem,
|
||||
useTheme,
|
||||
useMediaQuery,
|
||||
CircularProgress,
|
||||
Skeleton,
|
||||
Alert,
|
||||
@@ -29,6 +30,9 @@ import {
|
||||
Checkbox,
|
||||
SelectChangeEvent,
|
||||
Switch,
|
||||
Accordion,
|
||||
AccordionSummary,
|
||||
AccordionDetails,
|
||||
} from '@mui/material'
|
||||
import {
|
||||
Favorite,
|
||||
@@ -42,10 +46,12 @@ import {
|
||||
AutoAwesome,
|
||||
Edit,
|
||||
Login,
|
||||
ExpandMore,
|
||||
} from '@mui/icons-material'
|
||||
import { useState, useEffect, useMemo } from 'react'
|
||||
import { useTranslations, useLocale, useFormatter } from 'next-intl'
|
||||
import { useAuth } from '@/hooks/use-auth'
|
||||
import { AuthModal } from '@/components/auth/auth-modal'
|
||||
|
||||
interface PrayerRequest {
|
||||
id: string
|
||||
@@ -68,6 +74,7 @@ export default function PrayersPage() {
|
||||
const tc = useTranslations('common')
|
||||
const f = useFormatter()
|
||||
const { user } = useAuth()
|
||||
const isMobile = useMediaQuery(theme.breakpoints.down('md'))
|
||||
const [prayers, setPrayers] = useState<PrayerRequest[]>([])
|
||||
const [selectedCategory, setSelectedCategory] = useState<string>('all')
|
||||
const [openDialog, setOpenDialog] = useState(false)
|
||||
@@ -82,12 +89,14 @@ export default function PrayersPage() {
|
||||
const [isGenerating, setIsGenerating] = useState(false)
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [viewMode, setViewMode] = useState<'private' | 'public'>(user ? 'private' : 'public')
|
||||
const [selectedLanguages, setSelectedLanguages] = useState<string[]>([locale])
|
||||
const [selectedLanguage, setSelectedLanguage] = useState<string>(locale)
|
||||
const [authModalOpen, setAuthModalOpen] = useState(false)
|
||||
|
||||
const languagesKey = useMemo(() => selectedLanguages.slice().sort().join(','), [selectedLanguages])
|
||||
const languageOptions = useMemo(() => ([
|
||||
{ value: 'en', label: t('languageFilter.options.en') },
|
||||
{ value: 'ro', label: t('languageFilter.options.ro') }
|
||||
{ value: 'ro', label: t('languageFilter.options.ro') },
|
||||
{ value: 'es', label: t('languageFilter.options.es') },
|
||||
{ value: 'it', label: t('languageFilter.options.it') }
|
||||
]), [t])
|
||||
const languageLabelMap = useMemo(() => (
|
||||
languageOptions.reduce((acc, option) => {
|
||||
@@ -106,21 +115,10 @@ export default function PrayersPage() {
|
||||
|
||||
useEffect(() => {
|
||||
if (viewMode === 'public') {
|
||||
setSelectedLanguages(prev => {
|
||||
if (prev.includes(locale)) {
|
||||
return prev
|
||||
}
|
||||
return [...prev, locale]
|
||||
})
|
||||
setSelectedLanguage(locale)
|
||||
}
|
||||
}, [locale, viewMode])
|
||||
|
||||
useEffect(() => {
|
||||
if (viewMode === 'public' && selectedLanguages.length === 0) {
|
||||
setSelectedLanguages([locale])
|
||||
}
|
||||
}, [viewMode, selectedLanguages, locale])
|
||||
|
||||
const categories = [
|
||||
{ value: 'personal', label: t('categories.personal'), color: 'primary' },
|
||||
{ value: 'family', label: t('categories.family'), color: 'secondary' },
|
||||
@@ -148,8 +146,7 @@ export default function PrayersPage() {
|
||||
params.append('visibility', viewMode)
|
||||
|
||||
if (viewMode === 'public') {
|
||||
const languagesToQuery = selectedLanguages.length > 0 ? selectedLanguages : [locale]
|
||||
languagesToQuery.forEach(lang => params.append('languages', lang))
|
||||
params.append('languages', selectedLanguage)
|
||||
}
|
||||
|
||||
const headers: Record<string, string> = {}
|
||||
@@ -185,7 +182,7 @@ export default function PrayersPage() {
|
||||
|
||||
useEffect(() => {
|
||||
fetchPrayers()
|
||||
}, [selectedCategory, user, viewMode, languagesKey])
|
||||
}, [selectedCategory, user, viewMode, selectedLanguage])
|
||||
|
||||
const handleGenerateAIPrayer = async () => {
|
||||
if (!aiPrompt.trim()) return
|
||||
@@ -225,14 +222,9 @@ export default function PrayersPage() {
|
||||
}
|
||||
}
|
||||
|
||||
const handleLanguageChange = (event: SelectChangeEvent<string[]>) => {
|
||||
const handleLanguageChange = (event: SelectChangeEvent<string>) => {
|
||||
const value = event.target.value
|
||||
const parsed = typeof value === 'string'
|
||||
? value.split(',')
|
||||
: (value as string[])
|
||||
|
||||
const uniqueValues = Array.from(new Set(parsed.filter(Boolean)))
|
||||
setSelectedLanguages(uniqueValues)
|
||||
setSelectedLanguage(value)
|
||||
}
|
||||
|
||||
const handleSubmitPrayer = async () => {
|
||||
@@ -273,12 +265,18 @@ export default function PrayersPage() {
|
||||
|
||||
const handleOpenDialog = () => {
|
||||
if (!user) {
|
||||
// Could redirect to login or show login modal
|
||||
setAuthModalOpen(true)
|
||||
return
|
||||
}
|
||||
setOpenDialog(true)
|
||||
}
|
||||
|
||||
const handleAuthSuccess = () => {
|
||||
setAuthModalOpen(false)
|
||||
// After successful auth, open the add prayer dialog
|
||||
setOpenDialog(true)
|
||||
}
|
||||
|
||||
const handlePrayFor = async (prayerId: string) => {
|
||||
try {
|
||||
const headers: HeadersInit = {
|
||||
@@ -371,80 +369,101 @@ export default function PrayersPage() {
|
||||
fullWidth
|
||||
variant="contained"
|
||||
color="primary"
|
||||
startIcon={<Login />}
|
||||
onClick={() => {
|
||||
// Could redirect to login page or show login modal
|
||||
console.log('Please login to add prayers')
|
||||
}}
|
||||
startIcon={<Add />}
|
||||
onClick={handleOpenDialog}
|
||||
sx={{ mb: 3 }}
|
||||
>
|
||||
{locale === 'en' ? 'Login to Add Prayer' : 'Conectează-te pentru a adăuga'}
|
||||
{t('addPrayer')}
|
||||
</Button>
|
||||
)}
|
||||
|
||||
<Typography variant="h6" gutterBottom>
|
||||
{t('categories.title')}
|
||||
</Typography>
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1 }}>
|
||||
<Chip
|
||||
label={t('categories.all')}
|
||||
color="default"
|
||||
variant={selectedCategory === 'all' ? 'filled' : 'outlined'}
|
||||
size="small"
|
||||
onClick={() => setSelectedCategory('all')}
|
||||
sx={{ justifyContent: 'flex-start', cursor: 'pointer' }}
|
||||
/>
|
||||
{categories.map((category) => (
|
||||
<Chip
|
||||
key={category.value}
|
||||
label={category.label}
|
||||
color={category.color as any}
|
||||
variant={selectedCategory === category.value ? 'filled' : 'outlined'}
|
||||
size="small"
|
||||
onClick={() => setSelectedCategory(category.value)}
|
||||
sx={{ justifyContent: 'flex-start', cursor: 'pointer' }}
|
||||
/>
|
||||
))}
|
||||
</Box>
|
||||
{/* Categories Accordion */}
|
||||
<Accordion defaultExpanded={!isMobile}>
|
||||
<AccordionSummary
|
||||
expandIcon={<ExpandMore />}
|
||||
aria-controls="categories-content"
|
||||
id="categories-header"
|
||||
>
|
||||
<Typography variant="h6">
|
||||
{t('categories.title')}
|
||||
</Typography>
|
||||
</AccordionSummary>
|
||||
<AccordionDetails>
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1 }}>
|
||||
<Chip
|
||||
label={t('categories.all')}
|
||||
color="default"
|
||||
variant={selectedCategory === 'all' ? 'filled' : 'outlined'}
|
||||
size="small"
|
||||
onClick={() => setSelectedCategory('all')}
|
||||
sx={{ justifyContent: 'flex-start', cursor: 'pointer' }}
|
||||
/>
|
||||
{categories.map((category) => (
|
||||
<Chip
|
||||
key={category.value}
|
||||
label={category.label}
|
||||
color={category.color as any}
|
||||
variant={selectedCategory === category.value ? 'filled' : 'outlined'}
|
||||
size="small"
|
||||
onClick={() => setSelectedCategory(category.value)}
|
||||
sx={{ justifyContent: 'flex-start', cursor: 'pointer' }}
|
||||
/>
|
||||
))}
|
||||
</Box>
|
||||
</AccordionDetails>
|
||||
</Accordion>
|
||||
|
||||
{/* Language Filter Accordion */}
|
||||
{viewMode === 'public' && (
|
||||
<Box sx={{ mt: 3 }}>
|
||||
<Typography variant="h6" sx={{ mb: 1 }}>
|
||||
{t('languageFilter.title')}
|
||||
</Typography>
|
||||
<FormControl fullWidth size="small">
|
||||
<Select
|
||||
multiple
|
||||
value={selectedLanguages}
|
||||
onChange={handleLanguageChange}
|
||||
renderValue={(selected) =>
|
||||
(selected as string[])
|
||||
.map(code => languageLabelMap[code] || code.toUpperCase())
|
||||
.join(', ')
|
||||
}
|
||||
>
|
||||
{languageOptions.map(option => (
|
||||
<MenuItem key={option.value} value={option.value}>
|
||||
<Checkbox checked={selectedLanguages.includes(option.value)} />
|
||||
<ListItemText primary={option.label} />
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
<Typography variant="caption" color="text.secondary" sx={{ mt: 1 }}>
|
||||
{t('languageFilter.helper')}
|
||||
</Typography>
|
||||
</Box>
|
||||
<Accordion defaultExpanded={!isMobile} sx={{ mt: 2 }}>
|
||||
<AccordionSummary
|
||||
expandIcon={<ExpandMore />}
|
||||
aria-controls="language-content"
|
||||
id="language-header"
|
||||
>
|
||||
<Typography variant="h6">
|
||||
{t('languageFilter.title')}
|
||||
</Typography>
|
||||
</AccordionSummary>
|
||||
<AccordionDetails>
|
||||
<FormControl fullWidth size="small">
|
||||
<Select
|
||||
value={selectedLanguage}
|
||||
onChange={handleLanguageChange}
|
||||
>
|
||||
{languageOptions.map(option => (
|
||||
<MenuItem key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
<Typography variant="caption" color="text.secondary" sx={{ mt: 1, display: 'block' }}>
|
||||
{t('languageFilter.helper')}
|
||||
</Typography>
|
||||
</AccordionDetails>
|
||||
</Accordion>
|
||||
)}
|
||||
|
||||
<Typography variant="h6" sx={{ mt: 3, mb: 1 }}>
|
||||
{t('stats.title')}
|
||||
</Typography>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
• {t('stats.activeRequests', { count: prayers.length })}<br />
|
||||
• {t('stats.totalPrayers', { count: prayers.reduce((sum, p) => sum + p.prayerCount, 0) })}<br />
|
||||
• {t('stats.youPrayed', { count: prayers.filter(p => p.isPrayedFor).length })}
|
||||
</Typography>
|
||||
{/* Stats Accordion */}
|
||||
<Accordion defaultExpanded={!isMobile} sx={{ mt: 2 }}>
|
||||
<AccordionSummary
|
||||
expandIcon={<ExpandMore />}
|
||||
aria-controls="stats-content"
|
||||
id="stats-header"
|
||||
>
|
||||
<Typography variant="h6">
|
||||
{t('stats.title')}
|
||||
</Typography>
|
||||
</AccordionSummary>
|
||||
<AccordionDetails>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
• {t('stats.activeRequests', { count: prayers.length })}<br />
|
||||
• {t('stats.totalPrayers', { count: prayers.reduce((sum, p) => sum + p.prayerCount, 0) })}<br />
|
||||
• {t('stats.youPrayed', { count: prayers.filter(p => p.isPrayedFor).length })}
|
||||
</Typography>
|
||||
</AccordionDetails>
|
||||
</Accordion>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Box>
|
||||
@@ -773,6 +792,15 @@ export default function PrayersPage() {
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
|
||||
{/* Auth Modal */}
|
||||
<AuthModal
|
||||
open={authModalOpen}
|
||||
onClose={() => setAuthModalOpen(false)}
|
||||
onSuccess={handleAuthSuccess}
|
||||
message={t('authRequired')}
|
||||
defaultTab="login"
|
||||
/>
|
||||
</Container>
|
||||
</Box>
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user