import { NextResponse } from 'next/server'; import { getServerSession } from 'next-auth'; import { authOptions } from '@/lib/auth'; import { prisma } from '@/lib/prisma'; import { getHolidayHint } from '@/lib/holidays'; import { getRandomLocalQuote } from '@/lib/quotes'; export const dynamic = 'force-dynamic'; export async function GET(req: Request) { try { const session = await getServerSession(authOptions); if (!session?.user?.email) { return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); } const { searchParams } = new URL(req.url); const weekStartParam = searchParams.get('weekStart'); if (!weekStartParam) { return NextResponse.json({ error: 'weekStart is required' }, { status: 400 }); } const date = new Date(weekStartParam); date.setUTCHours(0, 0, 0, 0); // Look up by email — session ID can be stale after DB restore const user = await prisma.user.findUnique({ where: { email: session.user.email }, select: { id: true, goalFallbackType: true, goalDefaultSentence: true, language: true, quoteLanguages: true, quoteSourceUrls: true, } }); if (!user) return NextResponse.json({ error: 'User not found' }, { status: 404 }); const userId = user.id; // 1. Check if user has a custom set goal for THIS week specifically const goal = await prisma.weeklyGoal.findUnique({ where: { userId_weekStart: { userId: userId, weekStart: date, }, }, }); if (goal && goal.text) { return NextResponse.json({ goal: goal.text }); } // 2. Handle Fallbacks based on user settings const fallbackType = user?.goalFallbackType || 'quote'; if (fallbackType === 'next_todo') { // Fetch first incomplete task for this week const weekEnd = new Date(date); weekEnd.setDate(weekEnd.getDate() + 7); const nextTask = await prisma.task.findFirst({ where: { userId, completed: false, scheduledDate: { gte: date, lt: weekEnd } }, orderBy: [ { scheduledDate: 'asc' }, { order: 'asc' } ] }); if (nextTask) { return NextResponse.json({ goal: `Next: ${nextTask.title}`, isNextTask: true }); } // If no tasks, fall back to quote or default? Let's go to quote. } if (fallbackType === 'default') { return NextResponse.json({ goal: user?.goalDefaultSentence || 'goal of the week', isDefault: true }); } // 3. Fallback to holiday/celebration hints (High priority for "quote" type) const holidayHint = getHolidayHint(date); if (holidayHint) { return NextResponse.json({ goal: holidayHint, isHoliday: true }); } // 4. Try user-configured quote source URLs, then ZenQuotes const quoteLangs: string[] = (user as any)?.quoteLanguages?.length > 0 ? (user as any).quoteLanguages : [user?.language || 'en']; // Universal JSON quote parser — handles the most common free API response shapes const parseQuoteResponse = (data: any): string | null => { try { // Array responses: pick a random element if (Array.isArray(data)) { if (data.length === 0) return null; data = data[Math.floor(Math.random() * data.length)]; } // ZenQuotes: [{q, a}] if (data.q && data.a) return `${data.q} — ${data.a}`; // Quotable: {content, author} if (data.content && data.author) return `${data.content} — ${data.author}`; // Forismatic: {quoteText, quoteAuthor} if (data.quoteText) return data.quoteAuthor ? `${data.quoteText.trim()} — ${data.quoteAuthor.trim()}` : data.quoteText.trim(); // Common: {text, author} or {quote, author} const text = data.text || data.quote || data.body || data.sentence; const author = data.author || data.by || data.from || data.speaker; if (text) return author ? `${text} — ${author}` : text; // Nested: {data: {quote, author}} if (data.data) return parseQuoteResponse(data.data); // Nested: {slip: {advice}} if (data.slip?.advice) return data.slip.advice; } catch { /* ignore */ } return null; }; // Try each user-configured URL first const userUrls: string[] = (user as any)?.quoteSourceUrls?.filter(Boolean) || []; for (const url of userUrls) { try { const res = await fetch(url, { signal: AbortSignal.timeout(4000) }); if (res.ok) { const data = await res.json(); const parsed = parseQuoteResponse(data); if (parsed) return NextResponse.json({ goal: parsed, isQuote: true }); } } catch (e) { console.error(`Failed to fetch from ${url}:`, e); } } // ZenQuotes as built-in English fallback if (quoteLangs.includes('en')) { try { const res = await fetch('https://zenquotes.io/api/random', { signal: AbortSignal.timeout(3000) }); if (res.ok) { const data = await res.json(); if (data && data[0] && data[0].q) { return NextResponse.json({ goal: `${data[0].q} — ${data[0].a}`, isQuote: true }); } } } catch (e) { console.error('Failed to fetch from ZenQuotes:', e); } } // 5. Fallback to local curated quotes (pick random from selected languages) const randomLang = quoteLangs[Math.floor(Math.random() * quoteLangs.length)]; const localQuote = getRandomLocalQuote(randomLang); if (localQuote) { return NextResponse.json({ goal: `${localQuote.text} — ${localQuote.author}`, isQuote: true }); } // Final default fallback return NextResponse.json({ goal: user?.goalDefaultSentence || 'goal of the week', isDefault: true }); } catch (error) { console.error('API Error:', error); return NextResponse.json({ error: 'Internal Server Error' }, { status: 500 }); } } // POST delegates to PUT for client compatibility export async function POST(req: Request) { return PUT(req); } export async function PUT(req: Request) { try { const session = await getServerSession(authOptions); if (!session?.user?.email) { return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); } const dbUser = await prisma.user.findUnique({ where: { email: session.user.email }, select: { id: true } }); if (!dbUser) return NextResponse.json({ error: 'User not found' }, { status: 404 }); const userId = dbUser.id; const { weekStart, text } = await req.json(); if (!weekStart) { return NextResponse.json({ error: 'weekStart is required' }, { status: 400 }); } const date = new Date(weekStart); date.setUTCHours(0, 0, 0, 0); const goal = await prisma.weeklyGoal.upsert({ where: { userId_weekStart: { userId: userId, weekStart: date, }, }, update: { text }, create: { userId: userId, weekStart: date, text, }, }); return NextResponse.json({ goal: goal.text }); } catch (error) { console.error('API Error:', error); return NextResponse.json({ error: 'Internal Server Error' }, { status: 500 }); } }