Files
biblical-guide.com/app/api/user/reading-plans/[id]/progress/route.ts
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

235 lines
5.7 KiB
TypeScript

import { NextResponse } from 'next/server'
import { getUserFromToken } from '@/lib/auth'
import { prisma } from '@/lib/db'
export const runtime = 'nodejs'
/**
* GET /api/user/reading-plans/[id]/progress
* Get progress for a specific reading plan
*/
export async function GET(
request: Request,
{ params }: { params: Promise<{ id: string }> }
) {
try {
const { id } = await params
// Get token from authorization header
const authHeader = request.headers.get('authorization')
const token = authHeader?.replace('Bearer ', '')
if (!token) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
// Verify token and get user
const user = await getUserFromToken(token)
if (!user) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
// Verify plan belongs to user
const userPlan = await prisma.userReadingPlan.findUnique({
where: {
id,
userId: user.id
}
})
if (!userPlan) {
return NextResponse.json(
{ error: 'Reading plan not found' },
{ status: 404 }
)
}
const progress = await prisma.userReadingProgress.findMany({
where: {
userPlanId: id,
userId: user.id
},
orderBy: {
planDay: 'asc'
}
})
return NextResponse.json({
success: true,
progress
})
} catch (error) {
console.error('Reading progress fetch error:', error)
return NextResponse.json(
{ error: 'Failed to fetch reading progress' },
{ status: 500 }
)
}
}
/**
* POST /api/user/reading-plans/[id]/progress
* Mark a reading as complete
*/
export async function POST(
request: Request,
{ params }: { params: Promise<{ id: string }> }
) {
try {
const { id } = await params
// Get token from authorization header
const authHeader = request.headers.get('authorization')
const token = authHeader?.replace('Bearer ', '')
if (!token) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
// Verify token and get user
const user = await getUserFromToken(token)
if (!user) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
const body = await request.json()
const { planDay, bookId, chapterNum, versesRead, completed, notes } = body
// Validate required fields
if (!planDay || !bookId || !chapterNum) {
return NextResponse.json(
{ error: 'planDay, bookId, and chapterNum are required' },
{ status: 400 }
)
}
// Verify plan belongs to user
const userPlan = await prisma.userReadingPlan.findUnique({
where: {
id,
userId: user.id
}
})
if (!userPlan) {
return NextResponse.json(
{ error: 'Reading plan not found' },
{ status: 404 }
)
}
// Create or update progress entry
const progress = await prisma.userReadingProgress.upsert({
where: {
userPlanId_planDay_bookId_chapterNum: {
userPlanId: id,
planDay: parseInt(planDay),
bookId: bookId,
chapterNum: parseInt(chapterNum)
}
},
create: {
userId: user.id,
userPlanId: id,
planDay: parseInt(planDay),
bookId: bookId,
chapterNum: parseInt(chapterNum),
versesRead: versesRead || null,
completed: completed !== false,
notes: notes || null
},
update: {
completed: completed !== false,
versesRead: versesRead || null,
notes: notes || null,
updatedAt: new Date()
}
})
// Update user plan statistics
if (completed !== false) {
// Count total completed days
const completedDays = await prisma.userReadingProgress.count({
where: {
userPlanId: id,
userId: user.id,
completed: true
}
})
// Calculate streak
const allProgress = await prisma.userReadingProgress.findMany({
where: {
userPlanId: id,
userId: user.id,
completed: true
},
orderBy: {
date: 'desc'
},
select: {
date: true
}
})
let currentStreak = 0
let longestStreak = 0
let tempStreak = 0
let lastDate: Date | null = null
for (const entry of allProgress) {
if (!lastDate) {
tempStreak = 1
lastDate = new Date(entry.date)
} else {
const dayDiff = Math.floor((lastDate.getTime() - new Date(entry.date).getTime()) / (1000 * 60 * 60 * 24))
if (dayDiff === 1) {
tempStreak++
} else {
if (tempStreak > longestStreak) {
longestStreak = tempStreak
}
tempStreak = 1
}
lastDate = new Date(entry.date)
}
}
currentStreak = tempStreak
if (currentStreak > longestStreak) {
longestStreak = currentStreak
}
// Update current day if this is the latest completed day
const maxDay = parseInt(planDay)
const shouldUpdateCurrentDay = maxDay >= userPlan.currentDay
await prisma.userReadingPlan.update({
where: { id },
data: {
completedDays: completedDays,
streak: currentStreak,
longestStreak: Math.max(longestStreak, userPlan.longestStreak),
...(shouldUpdateCurrentDay && { currentDay: maxDay + 1 })
}
})
}
return NextResponse.json({
success: true,
message: 'Reading progress updated successfully',
progress
})
} catch (error) {
console.error('Reading progress update error:', error)
return NextResponse.json(
{ error: 'Failed to update reading progress' },
{ status: 500 }
)
}
}