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>
159 lines
5.3 KiB
TypeScript
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 });
|
|
}
|
|
}
|