My-Weekly-ToDo-List/src/app/api/goal/route.ts
mARTin 163e9c1a07 feat: major update — mobile fix, auth decoupling, CalDAV perf, projects, fonts, quotes
Mobile fixes:
- Add viewport meta tag (was missing, causing broken mobile rendering)
- Make header nav controls visible on mobile (opacity-0 group-hover was invisible on touch)
- Add touch swipe gestures for day navigation on mobile
- Fix viewDays responsive override after profile load

Bug fixes:
- Fix goal save (WeeklyView used POST but route only had GET/PUT; fix to use PUT, add POST alias)
- Fix body key mismatch (goal → text) in goal save request

Auth & identity (Task 1):
- Add accountNumber (auto-increment) to User model for stable identity
- Fix OAuth flows: pass user CUID in state instead of email
- Google/Outlook callbacks now look up users by ID, not email
- Non-Gmail users can now connect Google Calendar/Tasks

CalDAV performance (Task 5):
- Embed CalDAV object URL in Apple event IDs (caldav::<url>::<uid> format)
- Delete/update now use O(1) direct URL access instead of O(n) full calendar scan
- Optimistic UI removal on delete (no blocking force-refresh)
- Legacy fallback for old-format IDs during transition

Apple Calendar data (Task 2):
- Pass URL, location, recurringEventId, isRecurring through full pipeline
- Add url, recurringEventId, isRecurring to CachedCalendarEvent schema
- Calendar cache now reads/writes all new fields

Projects (Task 6):
- New Project model (name, icon, color, description, order)
- CRUD API at /api/tasks/projects
- Tasks now support optional projectId with cascading nullify

Quotes (Task 4):
- Curated local quote database (50 quotes, DE+EN) with tag filtering
- Preset quote source APIs (ZenQuotes, Quotable, Forismatic, Type.fit)

Fonts (Task 7):
- FontPicker component with searchable dropdown and live preview
- /api/fonts endpoint (Google Fonts API proxy with 24h cache, fallback to 40 popular fonts)

Quick Settings (Task 8):
- QuickSettingsSidebar component (font size, spacing, show completed, start day, show lines)
- New user preferences: showCompletedTasks, showLines, startDayOffset, quoteSourceUrls

v1.9.0

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-01 17:55:49 +01:00

159 lines
5.3 KiB
TypeScript

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';
export async function GET(req: Request) {
try {
const session = await getServerSession(authOptions);
if (!session || !session.user) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
const userId = (session.user as any).id;
if (!userId) {
return NextResponse.json({ error: 'User ID missing from session' }, { status: 400 });
}
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);
// Fetch user preferences
const user = await prisma.user.findUnique({
where: { id: userId },
select: {
goalFallbackType: true,
goalDefaultSentence: true,
}
});
// 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 && goal.text !== 'your goal of this week' && goal.text !== user?.goalDefaultSentence) {
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. Fallback to ZenQuotes motivational quote
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);
}
// 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 || !session.user) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
const userId = (session.user as any).id;
if (!userId) {
return NextResponse.json({ error: 'User ID missing from session' }, { status: 400 });
}
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 });
}
}