My-Weekly-ToDo-List/src/app/api/weather/route.ts
mARTin 36c4652f8e feat: add weather display, projects tab, iPhone today fix, stale calendar fix
- Add weather feature with Open-Meteo API: hourly temp/icon overlay on time slots,
  location search via geocoding, weather toggle in settings
- Move project management to dedicated settings tab with emoji icon picker
- Fix iPhone single-day view starting on yesterday instead of today
- Fix deleted Apple calendar events persisting until app reboot by
  scheduling a delayed re-fetch after stale background sync

v1.44.0

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

68 lines
2.7 KiB
TypeScript

import { NextRequest, NextResponse } from 'next/server';
import { getServerSession } from 'next-auth';
import { authOptions } from '@/lib/auth';
import { prisma } from '@/lib/prisma';
export const dynamic = 'force-dynamic';
// In-memory cache: { key: { data, fetchedAt } }
const weatherCache = new Map<string, { data: any; fetchedAt: number }>();
const CACHE_TTL_MS = 15 * 60 * 1000; // 15 minutes
export async function GET(request: NextRequest) {
const session = await getServerSession(authOptions);
const userId = (session?.user as any)?.id;
if (!userId) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
const user = await prisma.user.findUnique({
where: { id: userId },
select: { weatherEnabled: true, weatherLat: true, weatherLon: true },
});
if (!user?.weatherEnabled || !user.weatherLat || !user.weatherLon) {
return NextResponse.json({ error: 'Weather not configured' }, { status: 400 });
}
const { searchParams } = new URL(request.url);
const startDate = searchParams.get('start') || new Date().toISOString().slice(0, 10);
const endDate = searchParams.get('end') || startDate;
const cacheKey = `${user.weatherLat},${user.weatherLon},${startDate},${endDate}`;
const cached = weatherCache.get(cacheKey);
if (cached && Date.now() - cached.fetchedAt < CACHE_TTL_MS) {
return NextResponse.json(cached.data);
}
try {
const url = `https://api.open-meteo.com/v1/forecast?latitude=${user.weatherLat}&longitude=${user.weatherLon}&hourly=temperature_2m,weather_code&start_date=${startDate}&end_date=${endDate}&timezone=auto`;
const res = await fetch(url, { next: { revalidate: 900 } });
if (!res.ok) {
return NextResponse.json({ error: 'Weather API failed' }, { status: 502 });
}
const raw = await res.json();
// Transform into { "2026-03-17T08:00": { temp: 5, code: 2 }, ... }
const hourly: Record<string, { temp: number; code: number }> = {};
if (raw.hourly?.time && raw.hourly?.temperature_2m && raw.hourly?.weather_code) {
for (let i = 0; i < raw.hourly.time.length; i++) {
hourly[raw.hourly.time[i]] = {
temp: Math.round(raw.hourly.temperature_2m[i]),
code: raw.hourly.weather_code[i],
};
}
}
const data = { hourly, timezone: raw.timezone };
weatherCache.set(cacheKey, { data, fetchedAt: Date.now() });
return NextResponse.json(data);
} catch (err) {
console.error('[WEATHER] Fetch failed:', err);
return NextResponse.json({ error: 'Weather fetch failed' }, { status: 500 });
}
}