Attraktionen

This commit is contained in:
2026-08-16 08:54:53 +00:00
parent 90a7d8d3d2
commit e2d05e6809
8 changed files with 973 additions and 119 deletions

View File

@@ -0,0 +1,147 @@
import { NextRequest, NextResponse } from 'next/server';
import { query } from '@/lib/db';
export const dynamic = 'force-dynamic';
const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));
async function parseGoogleMapsUrl(inputUrl: string) {
let targetUrl = inputUrl.trim();
if (targetUrl.includes('goo.gl') || targetUrl.includes('maps.app.goo.gl')) {
try {
const res = await fetch(targetUrl, {
method: 'GET',
headers: { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36' },
redirect: 'follow',
});
targetUrl = res.url || targetUrl;
} catch (err) {}
}
let lat: number | null = null;
let lon: number | null = null;
const coordMatch = targetUrl.match(/@(-?\d+\.\d+),(-?\d+\.\d+)/);
if (coordMatch) {
lat = parseFloat(coordMatch[1]);
lon = parseFloat(coordMatch[2]);
}
return { lat, lon };
}
async function geocode(address: string) {
if (!address || address.startsWith('http')) return null;
const search = async (q: string) => {
try {
const url = `https://nominatim.openstreetmap.org/search?q=${encodeURIComponent(q)}&format=json&limit=1`;
const res = await fetch(url, { headers: { 'User-Agent': 'TravelPlannerApp/1.0' } });
const data = await res.json();
if (data && data.length > 0) return { lat: parseFloat(data[0].lat), lon: parseFloat(data[0].lon) };
} catch (err) {}
return null;
};
let coords = await search(address);
if (!coords && address.includes(',')) {
const parts = address.split(',').map(p => p.trim());
await sleep(1100);
coords = await search(`${parts[parts.length - 2]}, ${parts[parts.length - 1]}`);
}
return coords;
}
async function getDriveInfo(origin: { lat: number; lon: number }, dest: { lat: number; lon: number }) {
try {
const url = `http://router.project-osrm.org/route/v1/driving/${origin.lon},${origin.lat};${dest.lon},${dest.lat}?overview=false`;
const res = await fetch(url);
const data = await res.json();
if (data.code === 'Ok' && data.routes?.length > 0) {
return {
distanceKm: Math.round((data.routes[0].distance / 1000) * 10) / 10,
durationMin: Math.round(data.routes[0].duration / 60),
};
}
} catch (err) {}
return { distanceKm: null, durationMin: null };
}
export async function PUT(req: NextRequest, { params }: { params: { id: string } }) {
try {
const { id } = params;
const { name, address, category_icon, notes, hotel_ids, trip_id } = await req.json();
let sightCoords: { lat: number; lon: number } | null = null;
if (address.includes('google.com/maps') || address.includes('goo.gl')) {
const parsed = await parseGoogleMapsUrl(address);
if (parsed.lat && parsed.lon) sightCoords = { lat: parsed.lat, lon: parsed.lon };
}
if (!sightCoords) {
sightCoords = await geocode(address || name);
}
const res = await query(
`UPDATE hotel_attractions
SET name = $1,
address = $2,
category_icon = $3,
notes = $4,
trip_id = $5,
lat = COALESCE($6, lat),
lon = COALESCE($7, lon)
WHERE id = $8
RETURNING *`,
[
name,
address,
category_icon || '🏛️',
notes || null,
trip_id || null,
sightCoords?.lat || null,
sightCoords?.lon || null,
id,
]
);
await query('DELETE FROM hotel_attraction_assignments WHERE attraction_id = $1', [id]);
const selectedHotelIds: string[] = Array.isArray(hotel_ids) ? hotel_ids : [];
for (const hId of selectedHotelIds) {
let distanceKm: number | null = null;
let durationMin: number | null = null;
const hotelRes = await query('SELECT address, name FROM hotels WHERE id = $1', [hId]);
if (hotelRes.rows.length > 0) {
const hotel = hotelRes.rows[0];
await sleep(1100);
let hCoords = await geocode(hotel.address);
if (!hCoords && hotel.name) {
await sleep(1100);
hCoords = await geocode(hotel.name);
}
if (hCoords && sightCoords) {
const drive = await getDriveInfo(hCoords, sightCoords);
distanceKm = drive.distanceKm;
durationMin = drive.durationMin;
}
}
await query(
`INSERT INTO hotel_attraction_assignments (attraction_id, hotel_id, distance_km, duration_minutes)
VALUES ($1, $2, $3, $4)`,
[id, hId, distanceKm, durationMin]
);
}
return NextResponse.json({ success: true, attraction: res.rows[0] });
} catch (e: any) {
return NextResponse.json({ success: false, error: e.message }, { status: 500 });
}
}
export async function DELETE(req: NextRequest, { params }: { params: { id: string } }) {
try {
await query('DELETE FROM hotel_attractions WHERE id = $1', [params.id]);
return NextResponse.json({ success: true });
} catch (e: any) {
return NextResponse.json({ success: false, error: e.message }, { status: 500 });
}
}