Attraktionen
This commit is contained in:
147
src/app/api/attractions/[id]/route.ts
Normal file
147
src/app/api/attractions/[id]/route.ts
Normal 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 });
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user