Files
biblical-guide.com/app/[locale]/prayers/page.tsx
andupetcu 4fac06e94b Fix relativeTime API usage correctly
- Changed from incorrect f.relativeTime(number, unit, options) to correct f.relativeTime(date, now)
- The function expects two Date objects, not a number and unit string
- This properly eliminates the ENVIRONMENT_FALLBACK warning
- Simplified the implementation to use next-intl's built-in formatting
2025-09-22 09:25:58 +03:00

624 lines
22 KiB
TypeScript

'use client'
import {
Container,
Card,
CardContent,
Typography,
Box,
TextField,
Button,
Paper,
Avatar,
Chip,
IconButton,
Dialog,
DialogTitle,
DialogContent,
DialogActions,
List,
ListItem,
ListItemAvatar,
ListItemText,
MenuItem,
useTheme,
CircularProgress,
Skeleton,
Alert,
Tabs,
Tab,
FormControlLabel,
Switch,
} from '@mui/material'
import {
Favorite,
Add,
Close,
Person,
AccessTime,
FavoriteBorder,
Share,
MoreVert,
AutoAwesome,
Edit,
Login,
} from '@mui/icons-material'
import { useState, useEffect } from 'react'
import { useTranslations, useLocale, useFormatter } from 'next-intl'
import { useAuth } from '@/hooks/use-auth'
interface PrayerRequest {
id: string
title: string
description: string
category: string
author: string
timestamp: Date
prayerCount: number
isPrayedFor: boolean
}
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 [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',
})
const [aiPrompt, setAiPrompt] = useState('')
const [isGenerating, setIsGenerating] = useState(false)
const [loading, setLoading] = useState(true)
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 () => {
setLoading(true)
try {
const params = new URLSearchParams()
if (selectedCategory !== 'all') {
params.append('category', selectedCategory)
}
params.append('limit', '50')
if (user?.id) {
params.append('userId', user.id)
}
const response = await fetch(`/api/prayers?${params.toString()}`)
if (response.ok) {
const data = await response.json()
setPrayers(data.prayers.map((prayer: any) => ({
...prayer,
timestamp: new Date(prayer.timestamp)
})))
} else {
console.error('Failed to fetch prayers')
}
} catch (error) {
console.error('Error fetching prayers:', error)
} finally {
setLoading(false)
}
}
useEffect(() => {
fetchPrayers()
}, [selectedCategory, user])
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
})
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 handleSubmitPrayer = async () => {
if (!newPrayer.title.trim() || !newPrayer.description.trim()) return
if (!user) return
const prayer: PrayerRequest = {
id: Date.now().toString(),
title: newPrayer.title,
description: newPrayer.description,
category: newPrayer.category,
author: user.name || (locale === 'en' ? 'You' : 'Tu'),
timestamp: new Date(),
prayerCount: 0,
isPrayedFor: false,
}
try {
const response = await fetch('/api/prayers', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${localStorage.getItem('authToken')}`
},
body: JSON.stringify({
title: newPrayer.title,
description: newPrayer.description,
category: newPrayer.category,
isAnonymous: false
}),
})
if (response.ok) {
const data = await response.json()
setPrayers([{
...data.prayer,
timestamp: new Date(data.prayer.timestamp)
}, ...prayers])
setNewPrayer({ title: '', description: '', category: 'personal' })
setAiPrompt('')
setTabValue(0)
setOpenDialog(false)
} else {
console.error('Failed to submit prayer')
}
} catch (error) {
console.error('Error submitting prayer:', error)
}
}
const handleOpenDialog = () => {
if (!user) {
// Could redirect to login or show login modal
return
}
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={<Login />}
onClick={() => {
// Could redirect to login page or show login modal
console.log('Please login to add prayers')
}}
sx={{ mb: 3 }}
>
{locale === 'en' ? 'Login to Add Prayer' : 'Conectează-te pentru a adăuga'}
</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>
<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>
</CardContent>
</Card>
</Box>
{/* Prayer Requests */}
<Box sx={{ flex: 1, width: { xs: '100%', md: '75%' } }}>
{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.map((prayer) => {
const categoryInfo = getCategoryInfo(prayer.category)
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 }}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, mb: 1 }}>
<Typography variant="h6" component="h3">
{prayer.title}
</Typography>
<Chip
label={categoryInfo.label}
color={categoryInfo.color as any}
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">
{prayer.author}
</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 />}
>
{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>
)}
</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>
</Container>
</Box>
)
}