feat: integrate reading plans with Bible reader
Bible Reader Integration: - Fetch and display active reading plan progress in Bible reader - Progress bar shows plan completion percentage when user has active plan - Progress bar color changes to green for active plans - Display "Plan Name Progress" with days completed below bar Reading Plan Navigation: - Add "Read the Bible" button to active reading plan detail page - Button shows current/next reading selection (Day X: Book Chapter) - Navigate directly to Bible reader with correct book and chapter - Smart selection: current day if incomplete, next incomplete day if current is done - Only shown for ACTIVE plans Technical: - Load active plans via /api/user/reading-plans endpoint - Calculate progress from completedDays vs plan duration - Use getCurrentReading() helper to determine next reading - URL encoding for book names with spaces 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -280,6 +280,9 @@ export default function BibleReaderNew({ initialVersion, initialBook, initialCha
|
|||||||
const [readingProgress, setReadingProgress] = useState<any>(null)
|
const [readingProgress, setReadingProgress] = useState<any>(null)
|
||||||
const [hasLoadedInitialProgress, setHasLoadedInitialProgress] = useState(false)
|
const [hasLoadedInitialProgress, setHasLoadedInitialProgress] = useState(false)
|
||||||
|
|
||||||
|
// Active reading plan state
|
||||||
|
const [activeReadingPlan, setActiveReadingPlan] = useState<any>(null)
|
||||||
|
|
||||||
// Page transition state
|
// Page transition state
|
||||||
const [isTransitioning, setIsTransitioning] = useState(false)
|
const [isTransitioning, setIsTransitioning] = useState(false)
|
||||||
|
|
||||||
@@ -588,6 +591,33 @@ export default function BibleReaderNew({ initialVersion, initialBook, initialCha
|
|||||||
}
|
}
|
||||||
}, [selectedVersion, debouncedVersion])
|
}, [selectedVersion, debouncedVersion])
|
||||||
|
|
||||||
|
// Load active reading plan
|
||||||
|
useEffect(() => {
|
||||||
|
if (typeof window === 'undefined') return
|
||||||
|
|
||||||
|
const loadActiveReadingPlan = async () => {
|
||||||
|
if (user) {
|
||||||
|
const token = localStorage.getItem('authToken')
|
||||||
|
if (!token) return
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/user/reading-plans', {
|
||||||
|
headers: { 'Authorization': `Bearer ${token}` }
|
||||||
|
})
|
||||||
|
const data = await response.json()
|
||||||
|
if (data.success && data.plans) {
|
||||||
|
// Find the first active plan
|
||||||
|
const activePlan = data.plans.find((p: any) => p.status === 'ACTIVE')
|
||||||
|
setActiveReadingPlan(activePlan || null)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error loading active reading plan:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
loadActiveReadingPlan()
|
||||||
|
}, [user])
|
||||||
|
|
||||||
// Load reading progress when version changes
|
// Load reading progress when version changes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Only run on client side to avoid hydration mismatch
|
// Only run on client side to avoid hydration mismatch
|
||||||
@@ -1501,6 +1531,16 @@ export default function BibleReaderNew({ initialVersion, initialBook, initialCha
|
|||||||
|
|
||||||
// Calculate reading progress percentage
|
// Calculate reading progress percentage
|
||||||
const calculateProgress = () => {
|
const calculateProgress = () => {
|
||||||
|
// If user has an active reading plan, show plan progress instead
|
||||||
|
if (activeReadingPlan) {
|
||||||
|
const planDuration = activeReadingPlan.plan?.duration || activeReadingPlan.targetEndDate
|
||||||
|
? Math.ceil((new Date(activeReadingPlan.targetEndDate).getTime() - new Date(activeReadingPlan.startDate).getTime()) / (1000 * 60 * 60 * 24))
|
||||||
|
: 365
|
||||||
|
const completedDays = activeReadingPlan.completedDays || 0
|
||||||
|
return Math.min(Math.round((completedDays / planDuration) * 100), 100)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default: Calculate progress based on chapters read in entire Bible
|
||||||
if (!books.length || !selectedBook || !selectedChapter) return 0
|
if (!books.length || !selectedBook || !selectedChapter) return 0
|
||||||
|
|
||||||
// Find current book index and total chapters before current position
|
// Find current book index and total chapters before current position
|
||||||
@@ -1939,7 +1979,7 @@ export default function BibleReaderNew({ initialVersion, initialBook, initialCha
|
|||||||
<Box sx={{ mt: 2, px: 1 }}>
|
<Box sx={{ mt: 2, px: 1 }}>
|
||||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 0.5 }}>
|
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 0.5 }}>
|
||||||
<Typography variant="caption" color="text.secondary">
|
<Typography variant="caption" color="text.secondary">
|
||||||
Reading Progress
|
{activeReadingPlan ? `${activeReadingPlan.name} Progress` : 'Reading Progress'}
|
||||||
</Typography>
|
</Typography>
|
||||||
<Typography variant="caption" color="primary" sx={{ fontWeight: 'bold' }}>
|
<Typography variant="caption" color="primary" sx={{ fontWeight: 'bold' }}>
|
||||||
{calculateProgress()}%
|
{calculateProgress()}%
|
||||||
@@ -1954,10 +1994,15 @@ export default function BibleReaderNew({ initialVersion, initialBook, initialCha
|
|||||||
backgroundColor: 'action.hover',
|
backgroundColor: 'action.hover',
|
||||||
'& .MuiLinearProgress-bar': {
|
'& .MuiLinearProgress-bar': {
|
||||||
borderRadius: 3,
|
borderRadius: 3,
|
||||||
backgroundColor: 'primary.main'
|
backgroundColor: activeReadingPlan ? 'success.main' : 'primary.main'
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
{activeReadingPlan && (
|
||||||
|
<Typography variant="caption" color="text.secondary" sx={{ mt: 0.5, display: 'block', fontSize: '0.65rem' }}>
|
||||||
|
{activeReadingPlan.completedDays} of {activeReadingPlan.plan?.duration || 'custom'} days completed
|
||||||
|
</Typography>
|
||||||
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
)}
|
)}
|
||||||
</Paper>
|
</Paper>
|
||||||
|
|||||||
@@ -40,7 +40,8 @@ import {
|
|||||||
EmojiEvents,
|
EmojiEvents,
|
||||||
TrendingUp,
|
TrendingUp,
|
||||||
Edit,
|
Edit,
|
||||||
Save
|
Save,
|
||||||
|
MenuBook
|
||||||
} from '@mui/icons-material'
|
} from '@mui/icons-material'
|
||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
|
|
||||||
@@ -210,6 +211,39 @@ export default function ReadingPlanDetailPage() {
|
|||||||
return entry?.notes || ''
|
return entry?.notes || ''
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const getCurrentReading = () => {
|
||||||
|
if (!plan) return null
|
||||||
|
|
||||||
|
const schedule = plan.plan?.schedule || plan.customSchedule
|
||||||
|
if (!schedule || !Array.isArray(schedule)) return null
|
||||||
|
|
||||||
|
// Get the current day's reading (or first incomplete day)
|
||||||
|
let dayToRead = plan.currentDay
|
||||||
|
|
||||||
|
// If current day is completed, find the next incomplete day
|
||||||
|
if (isDayCompleted(dayToRead)) {
|
||||||
|
for (let i = dayToRead; i <= schedule.length; i++) {
|
||||||
|
if (!isDayCompleted(i)) {
|
||||||
|
dayToRead = i
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const daySchedule = schedule[dayToRead - 1]
|
||||||
|
if (!daySchedule || !daySchedule.readings || daySchedule.readings.length === 0) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const reading = daySchedule.readings[0]
|
||||||
|
return {
|
||||||
|
day: dayToRead,
|
||||||
|
book: reading.book,
|
||||||
|
chapter: reading.chapter,
|
||||||
|
verses: reading.verses
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
<ProtectedRoute>
|
<ProtectedRoute>
|
||||||
@@ -363,6 +397,38 @@ export default function ReadingPlanDetailPage() {
|
|||||||
</Grid>
|
</Grid>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
|
{/* Read the Bible Button */}
|
||||||
|
{plan.status === 'ACTIVE' && (() => {
|
||||||
|
const currentReading = getCurrentReading()
|
||||||
|
return currentReading ? (
|
||||||
|
<Box sx={{ mb: 4, textAlign: 'center' }}>
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
size="large"
|
||||||
|
startIcon={<MenuBook />}
|
||||||
|
component={Link}
|
||||||
|
href={`/${locale}/bible?book=${encodeURIComponent(currentReading.book)}&chapter=${currentReading.chapter}`}
|
||||||
|
sx={{
|
||||||
|
py: 2,
|
||||||
|
px: 4,
|
||||||
|
fontSize: '1.1rem',
|
||||||
|
fontWeight: 600,
|
||||||
|
boxShadow: 3,
|
||||||
|
'&:hover': {
|
||||||
|
boxShadow: 6
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Read Today's Selection: {currentReading.book} {currentReading.chapter}
|
||||||
|
{currentReading.verses && `:${currentReading.verses}`}
|
||||||
|
</Button>
|
||||||
|
<Typography variant="caption" display="block" color="text.secondary" sx={{ mt: 1 }}>
|
||||||
|
Day {currentReading.day} of {duration}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
) : null
|
||||||
|
})()}
|
||||||
|
|
||||||
{/* Reading Schedule */}
|
{/* Reading Schedule */}
|
||||||
<Paper elevation={2} sx={{ p: 3 }}>
|
<Paper elevation={2} sx={{ p: 3 }}>
|
||||||
<Typography variant="h6" gutterBottom fontWeight="600">
|
<Typography variant="h6" gutterBottom fontWeight="600">
|
||||||
|
|||||||
Reference in New Issue
Block a user