Files
biblical-guide.com/app/[locale]/settings/page.tsx
Andrei 63082c825a feat: add user settings save and reading plans with progress tracking
User Settings:
- Add /api/user/settings endpoint for persisting theme and fontSize preferences
- Update settings page with working save functionality
- Add validation and localized error messages

Reading Plans:
- Add database schema with ReadingPlan, UserReadingPlan, and UserReadingProgress models
- Create CRUD API endpoints for reading plans and progress tracking
- Build UI for browsing available plans and managing user enrollments
- Implement progress tracking with daily reading schedule
- Add streak calculation and statistics display
- Create seed data with 5 predefined plans (Bible in 1 year, 90 days, NT 30 days, Psalms 30, Gospels 30)
- Add navigation link with internationalization support

Technical:
- Update to MUI v7 Grid API (using size prop instead of xs/sm/md and removing item prop)
- Fix Next.js 15 dynamic route params (await params pattern)
- Add translations for readingPlans in all languages (en, ro, es, it)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-12 23:07:47 +00:00

430 lines
15 KiB
TypeScript

'use client'
import { useState, useEffect } from 'react'
import { useTranslations, useLocale } from 'next-intl'
import { useAuth } from '@/hooks/use-auth'
import { ProtectedRoute } from '@/components/auth/protected-route'
import {
Container,
Paper,
Box,
Typography,
Switch,
FormControlLabel,
Select,
MenuItem,
FormControl,
InputLabel,
Card,
CardContent,
Divider,
Alert,
Button,
CircularProgress
} from '@mui/material'
import {
Settings as SettingsIcon,
Palette,
TextFields,
Language,
Notifications,
Security,
Save,
MenuBook,
CardMembership
} from '@mui/icons-material'
import UsageDisplay from '@/components/subscription/usage-display'
import Link from 'next/link'
export default function SettingsPage() {
const { user } = useAuth()
const locale = useLocale()
const t = useTranslations('settings')
const [settings, setSettings] = useState({
theme: user?.theme || 'light',
fontSize: user?.fontSize || 'medium',
notifications: true,
emailUpdates: false,
language: locale
})
const [message, setMessage] = useState('')
const [bibleVersions, setBibleVersions] = useState<any[]>([])
const [favoriteBibleVersion, setFavoriteBibleVersion] = useState<string | null>(null)
const [loadingVersions, setLoadingVersions] = useState(true)
useEffect(() => {
const loadBiblePreferences = async () => {
try {
// Load available versions (no limit to ensure we get all versions including the favorite)
const versionsRes = await fetch('/api/bible/versions?all=true')
const versionsData = await versionsRes.json()
if (versionsData.success) {
console.log('[Settings] Loaded versions:', versionsData.versions.length)
setBibleVersions(versionsData.versions)
}
// Load user's favorite version
const token = localStorage.getItem('authToken')
if (token) {
const favoriteRes = await fetch('/api/user/favorite-version', {
headers: { 'Authorization': `Bearer ${token}` }
})
const favoriteData = await favoriteRes.json()
console.log('[Settings] Favorite version data:', favoriteData)
if (favoriteData.success && favoriteData.favoriteBibleVersion) {
console.log('[Settings] Setting favorite version:', favoriteData.favoriteBibleVersion)
console.log('[Settings] Type of favorite version:', typeof favoriteData.favoriteBibleVersion)
setFavoriteBibleVersion(favoriteData.favoriteBibleVersion)
}
}
} catch (error) {
console.error('Error loading Bible preferences:', error)
} finally {
setLoadingVersions(false)
}
}
if (user) {
loadBiblePreferences()
} else {
setLoadingVersions(false)
}
}, [user])
const handleSettingChange = (setting: string, value: any) => {
setSettings(prev => ({
...prev,
[setting]: value
}))
}
const handleFavoriteVersionChange = async (versionId: string) => {
const token = localStorage.getItem('authToken')
if (!token) return
try {
const response = await fetch('/api/user/favorite-version', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
},
body: JSON.stringify({ versionId })
})
const data = await response.json()
if (data.success) {
setFavoriteBibleVersion(versionId)
setMessage('Favorite Bible version updated successfully')
} else {
setMessage('Failed to update favorite version')
}
} catch (error) {
console.error('Error updating favorite version:', error)
setMessage('Failed to update favorite version')
}
}
const handleSave = async () => {
const token = localStorage.getItem('authToken')
if (!token) {
setMessage(t('settingsError'))
return
}
try {
const response = await fetch(`/api/user/settings?locale=${locale}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
},
body: JSON.stringify({
theme: settings.theme,
fontSize: settings.fontSize,
notifications: settings.notifications,
emailUpdates: settings.emailUpdates,
language: settings.language
})
})
const data = await response.json()
if (response.ok && data.success) {
setMessage(t('settingsSaved'))
} else {
setMessage(data.error || t('settingsError'))
}
} catch (error) {
console.error('Error saving settings:', error)
setMessage(t('settingsError'))
}
}
return (
<ProtectedRoute>
<Container maxWidth="md" sx={{ py: 4 }}>
<Paper elevation={3} sx={{ p: 4 }}>
{/* Header */}
<Box textAlign="center" mb={4}>
<SettingsIcon sx={{ fontSize: 48, color: 'primary.main', mb: 2 }} />
<Typography variant="h4" component="h1" gutterBottom>
{t('title')}
</Typography>
<Typography variant="body1" color="text.secondary">
{t('subtitle')}
</Typography>
</Box>
<Divider sx={{ mb: 4 }} />
<Box sx={{ display: 'flex', gap: 3, flexWrap: 'wrap' }}>
{/* Appearance Settings */}
<Box sx={{ flex: { xs: '1 1 100%', md: '1 1 calc(50% - 12px)' } }}>
<Card variant="outlined">
<CardContent>
<Box display="flex" alignItems="center" mb={3}>
<Palette sx={{ mr: 1, color: 'primary.main' }} />
<Typography variant="h6">
{t('appearance')}
</Typography>
</Box>
<Box sx={{ mb: 3 }}>
<FormControl fullWidth>
<InputLabel>{t('theme')}</InputLabel>
<Select
value={settings.theme}
label={t('theme')}
onChange={(e) => handleSettingChange('theme', e.target.value)}
>
<MenuItem value="light">{t('themes.light')}</MenuItem>
<MenuItem value="dark">{t('themes.dark')}</MenuItem>
<MenuItem value="auto">{t('themes.auto')}</MenuItem>
</Select>
</FormControl>
</Box>
<Box sx={{ mb: 3 }}>
<FormControl fullWidth>
<InputLabel>{t('fontSize')}</InputLabel>
<Select
value={settings.fontSize}
label={t('fontSize')}
onChange={(e) => handleSettingChange('fontSize', e.target.value)}
startAdornment={<TextFields sx={{ mr: 1 }} />}
>
<MenuItem value="small">{t('fontSizes.small')}</MenuItem>
<MenuItem value="medium">{t('fontSizes.medium')}</MenuItem>
<MenuItem value="large">{t('fontSizes.large')}</MenuItem>
</Select>
</FormControl>
</Box>
</CardContent>
</Card>
</Box>
{/* Language & Notifications */}
<Box sx={{ flex: { xs: '1 1 100%', md: '1 1 calc(50% - 12px)' } }}>
<Card variant="outlined">
<CardContent>
<Box display="flex" alignItems="center" mb={3}>
<Language sx={{ mr: 1, color: 'primary.main' }} />
<Typography variant="h6">
{t('languageAndNotifications')}
</Typography>
</Box>
<Box sx={{ mb: 3 }}>
<FormControl fullWidth>
<InputLabel>{t('language')}</InputLabel>
<Select
value={settings.language}
label={t('language')}
onChange={(e) => handleSettingChange('language', e.target.value)}
>
<MenuItem value="ro">{t('languages.ro')}</MenuItem>
<MenuItem value="en">{t('languages.en')}</MenuItem>
</Select>
</FormControl>
</Box>
<Box sx={{ mb: 2 }}>
<FormControlLabel
control={
<Switch
checked={settings.notifications}
onChange={(e) => handleSettingChange('notifications', e.target.checked)}
/>
}
label={t('notifications')}
/>
</Box>
<Box sx={{ mb: 2 }}>
<FormControlLabel
control={
<Switch
checked={settings.emailUpdates}
onChange={(e) => handleSettingChange('emailUpdates', e.target.checked)}
/>
}
label={t('emailUpdates')}
/>
</Box>
</CardContent>
</Card>
</Box>
{/* Subscription & Usage */}
<Box sx={{ flex: '1 1 100%' }}>
<Card variant="outlined">
<CardContent>
<Box display="flex" alignItems="center" justifyContent="space-between" mb={3}>
<Box display="flex" alignItems="center">
<CardMembership sx={{ mr: 1, color: 'primary.main' }} />
<Typography variant="h6">
Subscription & Usage
</Typography>
</Box>
<Button
variant="outlined"
size="small"
component={Link}
href={`/${locale}/subscription`}
>
Manage Plan
</Button>
</Box>
<UsageDisplay compact={true} showUpgradeButton={false} />
<Box mt={2}>
<Button
variant="text"
size="small"
component={Link}
href={`/${locale}/subscription`}
fullWidth
>
View Subscription Details
</Button>
</Box>
</CardContent>
</Card>
</Box>
{/* Bible Preferences */}
<Box sx={{ flex: '1 1 100%' }}>
<Card variant="outlined">
<CardContent>
<Box display="flex" alignItems="center" mb={3}>
<MenuBook sx={{ mr: 1, color: 'primary.main' }} />
<Typography variant="h6">
Bible Preferences
</Typography>
</Box>
<Typography variant="body2" color="text.secondary" sx={{ mb: 2 }}>
Select your preferred Bible version. This will be loaded automatically when you open the Bible reader.
</Typography>
{loadingVersions ? (
<Box display="flex" justifyContent="center" py={2}>
<CircularProgress size={24} />
</Box>
) : (
<Box>
<FormControl fullWidth>
<InputLabel>Favorite Bible Version</InputLabel>
<Select
value={favoriteBibleVersion || ''}
label="Favorite Bible Version"
onChange={(e) => handleFavoriteVersionChange(e.target.value)}
renderValue={(selected) => {
if (!selected) {
return <em>None selected</em>
}
console.log('[Settings] renderValue - selected:', selected)
console.log('[Settings] renderValue - bibleVersions:', bibleVersions.length)
const version = bibleVersions.find(v => v.id === selected)
console.log('[Settings] renderValue - found version:', version)
if (version) {
return `${version.name} - ${version.abbreviation} (${version.language.toUpperCase()})`
}
return selected
}}
disabled={bibleVersions.length === 0}
>
<MenuItem value="">
<em>None selected</em>
</MenuItem>
{bibleVersions.map((version) => (
<MenuItem key={version.id} value={version.id}>
{version.name} - {version.abbreviation} ({version.language.toUpperCase()})
</MenuItem>
))}
</Select>
</FormControl>
{favoriteBibleVersion && (
<Typography variant="caption" color="success.main" sx={{ mt: 1, display: 'block' }}>
This version will be loaded when you open the Bible reader
</Typography>
)}
</Box>
)}
</CardContent>
</Card>
</Box>
{/* Security Settings */}
<Box sx={{ flex: '1 1 100%' }}>
<Card variant="outlined">
<CardContent>
<Box display="flex" alignItems="center" mb={3}>
<Security sx={{ mr: 1, color: 'primary.main' }} />
<Typography variant="h6">
{t('security')}
</Typography>
</Box>
<Typography variant="body2" color="text.secondary" sx={{ mb: 2 }}>
{t('passwordSecurity')}
</Typography>
<Button variant="outlined" disabled>
{t('changePasswordSoon')}
</Button>
</CardContent>
</Card>
</Box>
</Box>
{/* Save Button */}
<Box textAlign="center" mt={4}>
<Button
variant="contained"
size="large"
startIcon={<Save />}
onClick={handleSave}
sx={{ px: 4 }}
>
{t('saveSettings')}
</Button>
</Box>
{/* Success/Error Message */}
{message && (
<Alert
severity={message.includes(t('settingsError')) ? 'error' : 'success'}
sx={{ mt: 3 }}
onClose={() => setMessage('')}
>
{message}
</Alert>
)}
</Paper>
</Container>
</ProtectedRoute>
)
}