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>
182 lines
4.3 KiB
TypeScript
182 lines
4.3 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
|
|
* Get all reading plans for the authenticated user
|
|
*/
|
|
export async function GET(request: Request) {
|
|
try {
|
|
// 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 url = new URL(request.url)
|
|
const status = url.searchParams.get('status') || 'ACTIVE'
|
|
|
|
const userPlans = await prisma.userReadingPlan.findMany({
|
|
where: {
|
|
userId: user.id,
|
|
...(status !== 'ALL' && { status: status as any })
|
|
},
|
|
include: {
|
|
plan: {
|
|
select: {
|
|
id: true,
|
|
name: true,
|
|
description: true,
|
|
duration: true,
|
|
difficulty: true,
|
|
type: true
|
|
}
|
|
}
|
|
},
|
|
orderBy: {
|
|
createdAt: 'desc'
|
|
}
|
|
})
|
|
|
|
return NextResponse.json({
|
|
success: true,
|
|
plans: userPlans
|
|
})
|
|
|
|
} catch (error) {
|
|
console.error('User reading plans fetch error:', error)
|
|
return NextResponse.json(
|
|
{ error: 'Failed to fetch reading plans' },
|
|
{ status: 500 }
|
|
)
|
|
}
|
|
}
|
|
|
|
/**
|
|
* POST /api/user/reading-plans
|
|
* Enroll user in a reading plan
|
|
*/
|
|
export async function POST(request: Request) {
|
|
try {
|
|
// 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 { planId, startDate, customSchedule, name } = body
|
|
|
|
// Validate input
|
|
if (!planId && !customSchedule) {
|
|
return NextResponse.json(
|
|
{ error: 'Either planId or customSchedule is required' },
|
|
{ status: 400 }
|
|
)
|
|
}
|
|
|
|
let planData: any = {}
|
|
let duration = 365 // Default duration
|
|
|
|
if (planId) {
|
|
// Enrolling in a predefined plan
|
|
const plan = await prisma.readingPlan.findUnique({
|
|
where: { id: planId }
|
|
})
|
|
|
|
if (!plan) {
|
|
return NextResponse.json(
|
|
{ error: 'Reading plan not found' },
|
|
{ status: 404 }
|
|
)
|
|
}
|
|
|
|
if (!plan.isActive) {
|
|
return NextResponse.json(
|
|
{ error: 'This reading plan is no longer available' },
|
|
{ status: 400 }
|
|
)
|
|
}
|
|
|
|
duration = plan.duration
|
|
planData = {
|
|
planId: plan.id,
|
|
name: plan.name
|
|
}
|
|
} else {
|
|
// Creating a custom plan
|
|
if (!name) {
|
|
return NextResponse.json(
|
|
{ error: 'Name is required for custom plans' },
|
|
{ status: 400 }
|
|
)
|
|
}
|
|
|
|
if (!customSchedule || !Array.isArray(customSchedule)) {
|
|
return NextResponse.json(
|
|
{ error: 'Valid customSchedule is required' },
|
|
{ status: 400 }
|
|
)
|
|
}
|
|
|
|
duration = customSchedule.length
|
|
planData = {
|
|
name,
|
|
customSchedule
|
|
}
|
|
}
|
|
|
|
// Calculate target end date
|
|
const start = startDate ? new Date(startDate) : new Date()
|
|
const targetEnd = new Date(start)
|
|
targetEnd.setDate(targetEnd.getDate() + duration)
|
|
|
|
// Create user reading plan
|
|
const userPlan = await prisma.userReadingPlan.create({
|
|
data: {
|
|
userId: user.id,
|
|
startDate: start,
|
|
targetEndDate: targetEnd,
|
|
...planData
|
|
},
|
|
include: {
|
|
plan: true
|
|
}
|
|
})
|
|
|
|
return NextResponse.json({
|
|
success: true,
|
|
message: 'Successfully enrolled in reading plan',
|
|
plan: userPlan
|
|
})
|
|
|
|
} catch (error) {
|
|
console.error('Reading plan enrollment error:', error)
|
|
return NextResponse.json(
|
|
{ error: 'Failed to enroll in reading plan' },
|
|
{ status: 500 }
|
|
)
|
|
}
|
|
}
|