build: production build with Phase 1 2025 Bible Reader implementation complete
Includes all Phase 1 features: - Search-first navigation with auto-complete - Responsive reading interface (desktop/tablet/mobile) - 4 customization presets + full fine-tuning controls - Layered details panel with notes, bookmarks, highlights - Smart offline caching with IndexedDB and auto-sync - Full accessibility (WCAG 2.1 AA) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -1,807 +1,10 @@
|
||||
'use client'
|
||||
import {
|
||||
Container,
|
||||
Card,
|
||||
CardContent,
|
||||
Typography,
|
||||
Box,
|
||||
TextField,
|
||||
Button,
|
||||
Paper,
|
||||
Avatar,
|
||||
Chip,
|
||||
IconButton,
|
||||
Dialog,
|
||||
DialogTitle,
|
||||
DialogContent,
|
||||
DialogActions,
|
||||
ListItemText,
|
||||
MenuItem,
|
||||
useTheme,
|
||||
useMediaQuery,
|
||||
CircularProgress,
|
||||
Skeleton,
|
||||
Alert,
|
||||
Tabs,
|
||||
Tab,
|
||||
FormControlLabel,
|
||||
FormControl,
|
||||
Select,
|
||||
Checkbox,
|
||||
SelectChangeEvent,
|
||||
Switch,
|
||||
Accordion,
|
||||
AccordionSummary,
|
||||
AccordionDetails,
|
||||
} from '@mui/material'
|
||||
import {
|
||||
Favorite,
|
||||
Add,
|
||||
Close,
|
||||
Person,
|
||||
AccessTime,
|
||||
FavoriteBorder,
|
||||
Share,
|
||||
MoreVert,
|
||||
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
|
||||
title: string
|
||||
description: string
|
||||
category: string
|
||||
author: string
|
||||
timestamp: Date
|
||||
prayerCount: number
|
||||
isPrayedFor: boolean
|
||||
isPublic: boolean
|
||||
language: string
|
||||
isOwner: boolean
|
||||
}
|
||||
// DISABLED: Prayer Wall Feature
|
||||
|
||||
export default function PrayersPage() {
|
||||
const theme = useTheme()
|
||||
const locale = useLocale()
|
||||
const t = useTranslations('pages.prayers')
|
||||
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)
|
||||
const [tabValue, setTabValue] = useState(0) // 0 = Write, 1 = AI Generate
|
||||
const [newPrayer, setNewPrayer] = useState({
|
||||
title: '',
|
||||
description: '',
|
||||
category: 'personal',
|
||||
isPublic: false,
|
||||
})
|
||||
const [aiPrompt, setAiPrompt] = useState('')
|
||||
const [isGenerating, setIsGenerating] = useState(false)
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [viewMode, setViewMode] = useState<'private' | 'public'>(user ? 'private' : 'public')
|
||||
const [selectedLanguage, setSelectedLanguage] = useState<string>(locale)
|
||||
const [authModalOpen, setAuthModalOpen] = useState(false)
|
||||
|
||||
const languageOptions = useMemo(() => ([
|
||||
{ value: 'en', label: t('languageFilter.options.en') },
|
||||
{ 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) => {
|
||||
acc[option.value] = option.label
|
||||
return acc
|
||||
}, {} as Record<string, string>)
|
||||
), [languageOptions])
|
||||
|
||||
useEffect(() => {
|
||||
if (user) {
|
||||
setViewMode(prev => (prev === 'private' ? prev : 'private'))
|
||||
} else {
|
||||
setViewMode('public')
|
||||
}
|
||||
}, [user])
|
||||
|
||||
useEffect(() => {
|
||||
if (viewMode === 'public') {
|
||||
setSelectedLanguage(locale)
|
||||
}
|
||||
}, [locale, viewMode])
|
||||
|
||||
const categories = [
|
||||
{ value: 'personal', label: t('categories.personal'), color: 'primary' },
|
||||
{ value: 'family', label: t('categories.family'), color: 'secondary' },
|
||||
{ value: 'health', label: t('categories.health'), color: 'error' },
|
||||
{ value: 'work', label: t('categories.work'), color: 'warning' },
|
||||
{ value: 'ministry', label: t('categories.ministry'), color: 'success' },
|
||||
{ value: 'world', label: t('categories.world'), color: 'info' },
|
||||
]
|
||||
|
||||
// Fetch prayers from API
|
||||
const fetchPrayers = async () => {
|
||||
if (viewMode === 'private' && !user) {
|
||||
setPrayers([])
|
||||
setLoading(false)
|
||||
return
|
||||
}
|
||||
|
||||
setLoading(true)
|
||||
try {
|
||||
const params = new URLSearchParams()
|
||||
if (selectedCategory !== 'all') {
|
||||
params.append('category', selectedCategory)
|
||||
}
|
||||
params.append('limit', '50')
|
||||
params.append('visibility', viewMode)
|
||||
|
||||
if (viewMode === 'public') {
|
||||
params.append('languages', selectedLanguage)
|
||||
}
|
||||
|
||||
const headers: Record<string, string> = {}
|
||||
if (typeof window !== 'undefined') {
|
||||
const token = localStorage.getItem('authToken')
|
||||
if (token) {
|
||||
headers['Authorization'] = `Bearer ${token}`
|
||||
}
|
||||
}
|
||||
|
||||
const response = await fetch(`/api/prayers?${params.toString()}`, {
|
||||
headers
|
||||
})
|
||||
|
||||
if (response.ok) {
|
||||
const data = await response.json()
|
||||
setPrayers(data.prayers.map((prayer: any) => ({
|
||||
...prayer,
|
||||
timestamp: new Date(prayer.timestamp)
|
||||
})))
|
||||
} else {
|
||||
if (response.status === 401) {
|
||||
setPrayers([])
|
||||
}
|
||||
console.error('Failed to fetch prayers')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error fetching prayers:', error)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
fetchPrayers()
|
||||
}, [selectedCategory, user, viewMode, selectedLanguage])
|
||||
|
||||
const handleGenerateAIPrayer = async () => {
|
||||
if (!aiPrompt.trim()) return
|
||||
if (!user) return
|
||||
|
||||
setIsGenerating(true)
|
||||
try {
|
||||
const response = await fetch('/api/prayers/generate', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${localStorage.getItem('authToken')}`
|
||||
},
|
||||
body: JSON.stringify({
|
||||
prompt: aiPrompt,
|
||||
category: newPrayer.category,
|
||||
locale
|
||||
}),
|
||||
})
|
||||
|
||||
if (response.ok) {
|
||||
const data = await response.json()
|
||||
setNewPrayer({
|
||||
title: data.title || '',
|
||||
description: data.prayer || '',
|
||||
category: newPrayer.category,
|
||||
isPublic: newPrayer.isPublic
|
||||
})
|
||||
setTabValue(0) // Switch to write tab to review generated prayer
|
||||
} else {
|
||||
console.error('Failed to generate prayer')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error generating prayer:', error)
|
||||
} finally {
|
||||
setIsGenerating(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleLanguageChange = (event: SelectChangeEvent<string>) => {
|
||||
const value = event.target.value
|
||||
setSelectedLanguage(value)
|
||||
}
|
||||
|
||||
const handleSubmitPrayer = async () => {
|
||||
if (!newPrayer.title.trim() || !newPrayer.description.trim()) return
|
||||
if (!user) return
|
||||
|
||||
try {
|
||||
const token = localStorage.getItem('authToken')
|
||||
const response = await fetch('/api/prayers', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...(token ? { 'Authorization': `Bearer ${token}` } : {})
|
||||
},
|
||||
body: JSON.stringify({
|
||||
title: newPrayer.title,
|
||||
description: newPrayer.description,
|
||||
category: newPrayer.category,
|
||||
isAnonymous: false,
|
||||
isPublic: newPrayer.isPublic,
|
||||
language: locale
|
||||
}),
|
||||
})
|
||||
|
||||
if (response.ok) {
|
||||
await fetchPrayers()
|
||||
setNewPrayer({ title: '', description: '', category: 'personal', isPublic: false })
|
||||
setAiPrompt('')
|
||||
setTabValue(0)
|
||||
setOpenDialog(false)
|
||||
} else {
|
||||
console.error('Failed to submit prayer')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error submitting prayer:', error)
|
||||
}
|
||||
}
|
||||
|
||||
const handleOpenDialog = () => {
|
||||
if (!user) {
|
||||
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 = {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
|
||||
const authToken = localStorage.getItem('authToken')
|
||||
if (authToken) {
|
||||
headers['Authorization'] = `Bearer ${authToken}`
|
||||
}
|
||||
|
||||
const response = await fetch(`/api/prayers/${prayerId}/pray`, {
|
||||
method: 'POST',
|
||||
headers
|
||||
})
|
||||
|
||||
if (response.ok) {
|
||||
const data = await response.json()
|
||||
setPrayers(prayers.map(prayer =>
|
||||
prayer.id === prayerId
|
||||
? { ...prayer, prayerCount: data.prayerCount || prayer.prayerCount + 1, isPrayedFor: true }
|
||||
: prayer
|
||||
))
|
||||
} else {
|
||||
console.error('Failed to update prayer count')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error updating prayer count:', error)
|
||||
}
|
||||
}
|
||||
|
||||
const getCategoryInfo = (category: string) => {
|
||||
return categories.find(cat => cat.value === category) || categories[0]
|
||||
}
|
||||
|
||||
const formatTimestamp = (timestamp: Date) => {
|
||||
const currentTime = new Date()
|
||||
|
||||
try {
|
||||
// Use the correct API: relativeTime(date, now)
|
||||
return f.relativeTime(timestamp, currentTime)
|
||||
} catch (e) {
|
||||
// Fallback to simple formatting if relativeTime fails
|
||||
const diff = currentTime.getTime() - timestamp.getTime()
|
||||
const minutes = Math.floor(diff / (1000 * 60))
|
||||
const hours = Math.floor(minutes / 60)
|
||||
const days = Math.floor(hours / 24)
|
||||
|
||||
if (days > 0) return locale === 'ro' ? `acum ${days} ${days === 1 ? 'zi' : 'zile'}` : `${days} ${days === 1 ? 'day' : 'days'} ago`
|
||||
if (hours > 0) return locale === 'ro' ? `acum ${hours} ${hours === 1 ? 'oră' : 'ore'}` : `${hours} ${hours === 1 ? 'hour' : 'hours'} ago`
|
||||
if (minutes > 0) return locale === 'ro' ? `acum ${minutes} ${minutes === 1 ? 'minut' : 'minute'}` : `${minutes} ${minutes === 1 ? 'minute' : 'minutes'} ago`
|
||||
return locale === 'ro' ? 'acum' : 'just now'
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Box>
|
||||
|
||||
<Container maxWidth="lg" sx={{ py: 4 }}>
|
||||
{/* Header */}
|
||||
<Box sx={{ mb: 4, textAlign: 'center' }}>
|
||||
<Typography variant="h3" component="h1" gutterBottom>
|
||||
<Favorite sx={{ fontSize: 40, mr: 2, verticalAlign: 'middle', color: 'error.main' }} />
|
||||
{t('title')}
|
||||
</Typography>
|
||||
<Typography variant="body1" color="text.secondary">
|
||||
{t('subtitle')}
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
<Box sx={{ display: 'flex', gap: 4, flexDirection: { xs: 'column', md: 'row' } }}>
|
||||
{/* Categories Filter */}
|
||||
<Box sx={{ width: { xs: '100%', md: '25%' }, flexShrink: 0 }}>
|
||||
<Card>
|
||||
<CardContent>
|
||||
{/* Add Prayer Button */}
|
||||
{user ? (
|
||||
<Button
|
||||
fullWidth
|
||||
variant="contained"
|
||||
color="primary"
|
||||
startIcon={<Add />}
|
||||
onClick={handleOpenDialog}
|
||||
sx={{ mb: 3 }}
|
||||
>
|
||||
{t('dialog.title')}
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
fullWidth
|
||||
variant="contained"
|
||||
color="primary"
|
||||
startIcon={<Add />}
|
||||
onClick={handleOpenDialog}
|
||||
sx={{ mb: 3 }}
|
||||
>
|
||||
{t('addPrayer')}
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{/* 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' && (
|
||||
<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>
|
||||
)}
|
||||
|
||||
{/* 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>
|
||||
|
||||
{/* Prayer Requests */}
|
||||
<Box sx={{ flex: 1, width: { xs: '100%', md: '75%' } }}>
|
||||
{user && (
|
||||
<Tabs
|
||||
value={viewMode}
|
||||
onChange={(_, newValue) => setViewMode(newValue as 'private' | 'public')}
|
||||
sx={{ mb: 3 }}
|
||||
variant="fullWidth"
|
||||
>
|
||||
<Tab value="private" label={t('viewModes.private')} />
|
||||
<Tab value="public" label={t('viewModes.public')} />
|
||||
</Tabs>
|
||||
)}
|
||||
|
||||
{viewMode === 'private' && (
|
||||
<Alert severity="info" sx={{ mb: 3 }}>
|
||||
{t('alerts.privateInfo')}
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{viewMode === 'public' && !user && (
|
||||
<Alert severity="info" sx={{ mb: 3 }}>
|
||||
{t('alerts.publicInfo')}
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{loading ? (
|
||||
<Box>
|
||||
{Array.from({ length: 3 }).map((_, index) => (
|
||||
<Card key={index} sx={{ mb: 3 }}>
|
||||
<CardContent>
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', mb: 2 }}>
|
||||
<Box sx={{ flexGrow: 1 }}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, mb: 1 }}>
|
||||
<Skeleton variant="text" width="60%" height={32} />
|
||||
<Skeleton variant="rounded" width={80} height={24} />
|
||||
</Box>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2, mb: 2 }}>
|
||||
<Skeleton variant="circular" width={24} height={24} />
|
||||
<Skeleton variant="text" width="30%" height={20} />
|
||||
</Box>
|
||||
<Skeleton variant="text" width="100%" height={24} />
|
||||
<Skeleton variant="text" width="90%" height={24} />
|
||||
<Skeleton variant="text" width="95%" height={24} />
|
||||
</Box>
|
||||
</Box>
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||
<Box sx={{ display: 'flex', gap: 1 }}>
|
||||
<Skeleton variant="rounded" width={100} height={32} />
|
||||
<Skeleton variant="rounded" width={100} height={32} />
|
||||
</Box>
|
||||
<Skeleton variant="text" width="20%" height={20} />
|
||||
</Box>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</Box>
|
||||
) : (
|
||||
<Box>
|
||||
{prayers.length === 0 ? (
|
||||
<Paper sx={{ p: 3, textAlign: 'center' }}>
|
||||
<Typography variant="body1" color="text.secondary">
|
||||
{viewMode === 'private' ? t('empty.private') : t('empty.public')}
|
||||
</Typography>
|
||||
</Paper>
|
||||
) : prayers.map((prayer) => {
|
||||
const categoryInfo = getCategoryInfo(prayer.category)
|
||||
const authorName = prayer.isOwner ? (locale === 'en' ? 'You' : 'Tu') : prayer.author
|
||||
const languageLabel = languageLabelMap[prayer.language] || prayer.language.toUpperCase()
|
||||
return (
|
||||
<Card key={prayer.id} sx={{ mb: 3 }}>
|
||||
<CardContent>
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', mb: 2 }}>
|
||||
<Box sx={{ flexGrow: 1 }}>
|
||||
<Typography variant="h6" component="h3">
|
||||
{prayer.title}
|
||||
</Typography>
|
||||
|
||||
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 1, mb: 1, mt: 1 }}>
|
||||
<Chip
|
||||
label={categoryInfo.label}
|
||||
color={categoryInfo.color as any}
|
||||
size="small"
|
||||
variant="outlined"
|
||||
/>
|
||||
<Chip
|
||||
label={prayer.isPublic ? t('chips.public') : t('chips.private')}
|
||||
size="small"
|
||||
color={prayer.isPublic ? 'success' : 'default'}
|
||||
variant={prayer.isPublic ? 'filled' : 'outlined'}
|
||||
/>
|
||||
<Chip
|
||||
label={languageLabel}
|
||||
size="small"
|
||||
variant="outlined"
|
||||
/>
|
||||
</Box>
|
||||
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2, mb: 2 }}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }}>
|
||||
<Avatar sx={{ width: 24, height: 24, bgcolor: 'primary.main' }}>
|
||||
<Person sx={{ fontSize: 16 }} />
|
||||
</Avatar>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
{authorName}
|
||||
</Typography>
|
||||
</Box>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }}>
|
||||
<AccessTime sx={{ fontSize: 16, color: 'text.secondary' }} />
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
{formatTimestamp(prayer.timestamp)}
|
||||
</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
<Typography variant="body1" sx={{ mb: 2 }}>
|
||||
{prayer.description}
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
<IconButton size="small">
|
||||
<MoreVert />
|
||||
</IconButton>
|
||||
</Box>
|
||||
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||
<Box sx={{ display: 'flex', gap: 1 }}>
|
||||
<Button
|
||||
variant={prayer.isPrayedFor ? "contained" : "outlined"}
|
||||
color="primary"
|
||||
size="small"
|
||||
startIcon={prayer.isPrayedFor ? <Favorite /> : <FavoriteBorder />}
|
||||
onClick={() => handlePrayFor(prayer.id)}
|
||||
disabled={prayer.isPrayedFor}
|
||||
>
|
||||
{prayer.isPrayedFor ? t('buttons.prayed') : t('buttons.pray')}
|
||||
</Button>
|
||||
<Button
|
||||
variant="outlined"
|
||||
size="small"
|
||||
startIcon={<Share />}
|
||||
disabled={!prayer.isPublic}
|
||||
>
|
||||
{t('buttons.share')}
|
||||
</Button>
|
||||
</Box>
|
||||
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
{t('stats.totalPrayers', { count: prayer.prayerCount })}
|
||||
</Typography>
|
||||
</Box>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
})}
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{/* Add Prayer Dialog */}
|
||||
<Dialog
|
||||
open={openDialog}
|
||||
onClose={() => setOpenDialog(false)}
|
||||
maxWidth="md"
|
||||
fullWidth
|
||||
>
|
||||
<DialogTitle>
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||
{t('dialog.title')}
|
||||
<IconButton onClick={() => setOpenDialog(false)} size="small">
|
||||
<Close />
|
||||
</IconButton>
|
||||
</Box>
|
||||
</DialogTitle>
|
||||
|
||||
{/* Tabs for Write vs AI Generate */}
|
||||
<Box sx={{ borderBottom: 1, borderColor: 'divider' }}>
|
||||
<Tabs value={tabValue} onChange={(e, newValue) => setTabValue(newValue)} centered>
|
||||
<Tab
|
||||
icon={<Edit />}
|
||||
label={locale === 'en' ? 'Write Prayer' : 'Scrie rugăciune'}
|
||||
iconPosition="start"
|
||||
/>
|
||||
<Tab
|
||||
icon={<AutoAwesome />}
|
||||
label={locale === 'en' ? 'AI Generate' : 'Generează cu AI'}
|
||||
iconPosition="start"
|
||||
/>
|
||||
</Tabs>
|
||||
</Box>
|
||||
|
||||
<DialogContent sx={{ minHeight: 400 }}>
|
||||
{/* Write Prayer Tab */}
|
||||
{tabValue === 0 && (
|
||||
<Box>
|
||||
<TextField
|
||||
fullWidth
|
||||
label={t('dialog.titleLabel')}
|
||||
value={newPrayer.title}
|
||||
onChange={(e) => setNewPrayer({ ...newPrayer, title: e.target.value })}
|
||||
sx={{ mb: 2, mt: 1 }}
|
||||
/>
|
||||
|
||||
<TextField
|
||||
fullWidth
|
||||
label={t('dialog.categoryLabel')}
|
||||
select
|
||||
value={newPrayer.category}
|
||||
onChange={(e) => setNewPrayer({ ...newPrayer, category: e.target.value })}
|
||||
sx={{ mb: 2 }}
|
||||
>
|
||||
{categories.map((option) => (
|
||||
<MenuItem key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</MenuItem>
|
||||
))}
|
||||
</TextField>
|
||||
|
||||
<TextField
|
||||
fullWidth
|
||||
label={t('dialog.descriptionLabel')}
|
||||
multiline
|
||||
rows={6}
|
||||
value={newPrayer.description}
|
||||
onChange={(e) => setNewPrayer({ ...newPrayer, description: e.target.value })}
|
||||
placeholder={t('dialog.placeholder')}
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{/* AI Generate Prayer Tab */}
|
||||
{tabValue === 1 && (
|
||||
<Box>
|
||||
<Alert severity="info" sx={{ mb: 2 }}>
|
||||
{locale === 'en'
|
||||
? 'Describe what you\'d like to pray about, and AI will help you create a meaningful prayer.'
|
||||
: 'Descrie pentru ce ai vrea să te rogi, iar AI-ul te va ajuta să creezi o rugăciune semnificativă.'
|
||||
}
|
||||
</Alert>
|
||||
|
||||
<TextField
|
||||
fullWidth
|
||||
label={t('dialog.categoryLabel')}
|
||||
select
|
||||
value={newPrayer.category}
|
||||
onChange={(e) => setNewPrayer({ ...newPrayer, category: e.target.value })}
|
||||
sx={{ mb: 2 }}
|
||||
>
|
||||
{categories.map((option) => (
|
||||
<MenuItem key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</MenuItem>
|
||||
))}
|
||||
</TextField>
|
||||
|
||||
<TextField
|
||||
fullWidth
|
||||
label={locale === 'en' ? 'What would you like to pray about?' : 'Pentru ce ai vrea să te rogi?'}
|
||||
multiline
|
||||
rows={4}
|
||||
value={aiPrompt}
|
||||
onChange={(e) => setAiPrompt(e.target.value)}
|
||||
placeholder={locale === 'en'
|
||||
? 'e.g., "Help me find peace during a difficult time at work" or "Guidance for my family\'s health struggles"'
|
||||
: 'ex. "Ajută-mă să găsesc pace într-o perioadă dificilă la muncă" sau "Îndrumarea pentru problemele de sănătate ale familiei mele"'
|
||||
}
|
||||
sx={{ mb: 2 }}
|
||||
/>
|
||||
|
||||
<Button
|
||||
fullWidth
|
||||
variant="contained"
|
||||
onClick={handleGenerateAIPrayer}
|
||||
disabled={!aiPrompt.trim() || isGenerating}
|
||||
startIcon={isGenerating ? <CircularProgress size={20} /> : <AutoAwesome />}
|
||||
sx={{ mb: 2 }}
|
||||
>
|
||||
{isGenerating
|
||||
? (locale === 'en' ? 'Generating...' : 'Se generează...')
|
||||
: (locale === 'en' ? 'Generate Prayer with AI' : 'Generează rugăciune cu AI')
|
||||
}
|
||||
</Button>
|
||||
|
||||
{newPrayer.title && newPrayer.description && (
|
||||
<Alert severity="success" sx={{ mt: 2 }}>
|
||||
{locale === 'en'
|
||||
? 'Prayer generated! Switch to the "Write Prayer" tab to review and edit before submitting.'
|
||||
: 'Rugăciune generată! Comută la tabul "Scrie rugăciune" pentru a revizui și edita înainte de a trimite.'
|
||||
}
|
||||
</Alert>
|
||||
)}
|
||||
</Box>
|
||||
)}
|
||||
|
||||
<Box sx={{ mt: 3 }}>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Switch
|
||||
checked={newPrayer.isPublic}
|
||||
onChange={(event) => setNewPrayer({ ...newPrayer, isPublic: event.target.checked })}
|
||||
/>
|
||||
}
|
||||
label={t('dialog.makePublic')}
|
||||
/>
|
||||
<Typography variant="caption" color="text.secondary" display="block">
|
||||
{newPrayer.isPublic ? t('dialog.visibilityPublic') : t('dialog.visibilityPrivate')}
|
||||
</Typography>
|
||||
</Box>
|
||||
</DialogContent>
|
||||
|
||||
<DialogActions>
|
||||
<Button onClick={() => setOpenDialog(false)}>
|
||||
{t('dialog.cancel')}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleSubmitPrayer}
|
||||
variant="contained"
|
||||
disabled={!newPrayer.title.trim() || !newPrayer.description.trim()}
|
||||
>
|
||||
{t('dialog.submit')}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
|
||||
{/* Auth Modal */}
|
||||
<AuthModal
|
||||
open={authModalOpen}
|
||||
onClose={() => setAuthModalOpen(false)}
|
||||
onSuccess={handleAuthSuccess}
|
||||
message={t('authRequired')}
|
||||
defaultTab="login"
|
||||
/>
|
||||
</Container>
|
||||
</Box>
|
||||
<div style={{ padding: '2rem', textAlign: 'center' }}>
|
||||
<h1>Prayer Wall Feature Disabled</h1>
|
||||
<p>This feature is currently disabled.</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user