diff --git a/src/app/api/webhooks/scraper/route.ts b/src/app/api/webhooks/scraper/route.ts index 05c0973..c6ce98b 100644 --- a/src/app/api/webhooks/scraper/route.ts +++ b/src/app/api/webhooks/scraper/route.ts @@ -66,7 +66,12 @@ export async function POST(req: NextRequest) { address = COALESCE($4, address), image_url = COALESCE($5, image_url), room_size_m2 = COALESCE($6, room_size_m2), - bathroom_images = COALESCE($7::jsonb, bathroom_images), + bathroom_images = CASE + -- Nur befüllen, wenn bisher noch gar keine Bilder hinterlegt sind: + WHEN (bathroom_images IS NULL OR jsonb_array_length(bathroom_images) = 0) AND $7::jsonb IS NOT NULL + THEN $7::jsonb + ELSE bathroom_images +END, updated_at = NOW() WHERE id = $8::uuid`, [ diff --git a/src/app/page.tsx b/src/app/page.tsx index 8bb789a..0adee5a 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -112,8 +112,10 @@ export default function TravelDashboard() { const [priceHistory, setPriceHistory] = useState([]); const [loadingPrices, setLoadingPrices] = useState(false); - // Bad-Fotos Modal & Lightbox + // Bad-Fotos Modal & Input const [selectedBathroomHotel, setSelectedBathroomHotel] = useState(null); + const [manualPhotoUrl, setManualPhotoUrl] = useState(''); + const [isAddingPhoto, setIsAddingPhoto] = useState(false); const [lightboxImage, setLightboxImage] = useState(null); // Reise & Hotel Modals @@ -257,6 +259,56 @@ export default function TravelDashboard() { } }; + // ========================================== + // HANDLERS: MANUELLE BAD-BILDER + // ========================================== + const handleAddManualPhoto = async (e: React.FormEvent) => { + e.preventDefault(); + if (!selectedBathroomHotel || !manualPhotoUrl.trim()) return; + + setIsAddingPhoto(true); + try { + const res = await fetch(`/api/hotels/${selectedBathroomHotel.id}/bathroom-images`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ image_url: manualPhotoUrl.trim() }) + }); + const data = await res.json(); + if (data.success) { + setManualPhotoUrl(''); + // Lokalen Zustand aktualisieren + const updatedImages = data.bathroom_images; + setSelectedBathroomHotel({ ...selectedBathroomHotel, bathroom_images: updatedImages }); + setHotels(hotels.map(h => h.id === selectedBathroomHotel.id ? { ...h, bathroom_images: updatedImages } : h)); + } else { + alert('Fehler: ' + data.error); + } + } catch (err: any) { + alert('Fehler beim Hinzufügen: ' + err.message); + } finally { + setIsAddingPhoto(false); + } + }; + + const handleDeleteBathroomPhoto = async (imgUrl: string) => { + if (!selectedBathroomHotel) return; + try { + const res = await fetch(`/api/hotels/${selectedBathroomHotel.id}/bathroom-images`, { + method: 'DELETE', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ image_url: imgUrl }) + }); + const data = await res.json(); + if (data.success) { + const updatedImages = data.bathroom_images; + setSelectedBathroomHotel({ ...selectedBathroomHotel, bathroom_images: updatedImages }); + setHotels(hotels.map(h => h.id === selectedBathroomHotel.id ? { ...h, bathroom_images: updatedImages } : h)); + } + } catch (err: any) { + console.error('Fehler beim Löschen des Bildes:', err); + } + }; + // ========================================== // HANDLERS: REISEN & MITREISENDE // ========================================== @@ -381,7 +433,7 @@ export default function TravelDashboard() { }; // ========================================== - // HANDLERS: HOTELS, PREISE, AUSFLÜGE + // HANDLERS: HOTELS & SCANS // ========================================== const openPriceHistoryModal = async (hotel: Hotel) => { setSelectedPriceHotel(hotel); @@ -592,7 +644,6 @@ export default function TravelDashboard() { return acc; }, []); - // Preishistorie Stats const pricesList = priceHistory.map(p => Number(p.price)).filter(p => p > 0); const minPrice = pricesList.length > 0 ? Math.min(...pricesList) : 0; const maxPrice = pricesList.length > 0 ? Math.max(...pricesList) : 0; @@ -884,7 +935,7 @@ export default function TravelDashboard() { }} >
- {/* Option / Ranking Header */} + {/* Option Header */}
{isCluster ? (
- {/* BAD & GOOGLE-FOTOS BUTTON-LEISTE */} + {/* BAD-BUTTON & GOOGLE-LINK */}
- {/* PREIS-BOX MIT DOPPELTEM BESTPREIS-VERGLEICH */} + {/* PREIS-BOX */} {totalPrice !== null ? (
@@ -1154,7 +1205,7 @@ export default function TravelDashboard() { )} {/* ======================================================== */} - {/* MODAL: BADEZIMMER & GAST-FOTOS (GARANTIERTER Z-INDEX) */} + {/* MODAL: BADEZIMMER & GAST-FOTOS MIT MANUELLEM IMPORT */} {/* ======================================================== */} {selectedBathroomHotel && (
{/* Modal Header */}

- 🚽 Bad- & Klo-Inspektion (Google Maps Gästebilder) + 🚽 Bad- & Klo-Inspektion

{selectedBathroomHotel.name} @@ -1210,6 +1260,32 @@ export default function TravelDashboard() { {/* Modal Body */}

+ + {/* 1. MANUELLER LINK-IMPORT */} +
+
+ ➕ Google Maps Bild-Link oder Foto-URL einfügen: +
+
+ setManualPhotoUrl(e.target.value)} + style={{ flex: 1, minWidth: '260px', padding: '0.65rem 0.9rem', borderRadius: '8px', border: '1px solid #cbd5e1', fontSize: '0.85rem', outline: 'none' }} + /> + +
+
+ + {/* KI-ZUSAMMENFASSUNG */} {selectedBathroomHotel.review_summary_bathroom && (
@@ -1221,57 +1297,70 @@ export default function TravelDashboard() {
)} + {/* FOTOGALERIE MIT LÖSCH-FUNKTION */} {(() => { const photos = parseBathroomImages(selectedBathroomHotel.bathroom_images); - return photos.length > 0 ? ( -
- {photos.map((imgUrl, i) => ( -
setLightboxImage(imgUrl)} - style={{ borderRadius: '10px', overflow: 'hidden', border: '1px solid #e2e8f0', position: 'relative', background: '#0f172a', cursor: 'pointer', aspectRatio: '4/3' }} - > - {`Bad { (e.target as HTMLElement).style.display = 'none'; }} - /> - - 🔎 Vergrößern - -
- ))} -
- ) : ( -
- 📷 -

- Noch keine Bad-Bilder direkt in der Datenbank gespeichert. -

-

- Klicke auf „KI-Reviews Scannen“, um Fotos automatisch abzurufen, oder öffne direkt Google Maps: -

-
- - - 🗺️ Auf Google Maps öffnen ↗ - + return ( +
+
+ Gespeicherte Bad-Bilder ({photos.length}) + Klick zum Vergrößern · 🗑️ zum Löschen
+ + {photos.length > 0 ? ( +
+ {photos.map((imgUrl, i) => ( +
+ {`Bad setLightboxImage(imgUrl)} + style={{ width: '100%', height: '100%', objectFit: 'cover', display: 'block', cursor: 'pointer' }} + onError={(e) => { (e.target as HTMLElement).style.display = 'none'; }} + /> + +
+ ))} +
+ ) : ( +
+ 📷 +

+ Noch keine Bad-Bilder hinterlegt. +

+

+ Füge oben einfach einen Link ein oder starte den automatischen Scan. +

+
+ )}
); })()} @@ -1285,23 +1374,21 @@ export default function TravelDashboard() { rel="noopener noreferrer" style={{ color: '#2563eb', fontSize: '0.85rem', fontWeight: '700', textDecoration: 'none' }} > - 🗺️ Alle Google Maps Fotos & Videos öffnen ↗ + 🗺️ Auf Google Maps ansehen ↗
)} - {/* ======================================================== */} {/* LIGHTBOX GROSSANSICHT */} - {/* ======================================================== */} {lightboxImage && (
setLightboxImage(null)} @@ -1311,7 +1398,7 @@ export default function TravelDashboard() { left: 0, right: 0, bottom: 0, - background: 'rgba(0, 0, 0, 0.9)', + background: 'rgba(0, 0, 0, 0.92)', display: 'flex', alignItems: 'center', justifyContent: 'center', diff --git a/travel-app/src/app/api/hotels/[id]/bathroom-images/route.ts b/travel-app/src/app/api/hotels/[id]/bathroom-images/route.ts new file mode 100644 index 0000000..e82ac0a --- /dev/null +++ b/travel-app/src/app/api/hotels/[id]/bathroom-images/route.ts @@ -0,0 +1,66 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { query } from '@/lib/db'; + +export const dynamic = 'force-dynamic'; + +// POST: Neues Bad-Bild (URL) hinzufügen +export async function POST(req: NextRequest, { params }: { params: { id: string } }) { + try { + const { id } = params; + const { image_url } = await req.json(); + + if (!image_url) { + return NextResponse.json({ success: false, error: 'Keine Bild-URL angegeben' }, { status: 400 }); + } + + // Google Maps Link intelligent auf echte Bild-URL bereinigen + let cleanUrl = image_url.trim(); + const embeddedMatch = cleanUrl.match(/!6s(https%3A%2F%2F[^!]+|https:\/\/[^!]+)/); + if (embeddedMatch) { + cleanUrl = decodeURIComponent(embeddedMatch[1]); + } + if (cleanUrl.includes('googleusercontent.com')) { + cleanUrl = cleanUrl.replace(/=w\d+-h\d+[^=]*/, '=w1200-h800-k-no'); + } + + const res = await query( + `UPDATE hotels + SET bathroom_images = COALESCE(bathroom_images, '[]'::jsonb) || jsonb_build_array($1::text) + WHERE id = $2::uuid + RETURNING bathroom_images`, + [cleanUrl, id] + ); + + return NextResponse.json({ success: true, bathroom_images: res.rows[0]?.bathroom_images }); + } catch (error: any) { + return NextResponse.json({ success: false, error: error.message }, { status: 500 }); + } +} + +// DELETE: Ein bestimmtes Bad-Bild löschen +export async function DELETE(req: NextRequest, { params }: { params: { id: string } }) { + try { + const { id } = params; + const { image_url } = await req.json(); + + if (!image_url) { + return NextResponse.json({ success: false, error: 'Keine Bild-URL zum Löschen angegeben' }, { status: 400 }); + } + + const res = await query( + `UPDATE hotels + SET bathroom_images = ( + SELECT COALESCE(jsonb_agg(elem), '[]'::jsonb) + FROM jsonb_array_elements_text(COALESCE(bathroom_images, '[]'::jsonb)) AS elem + WHERE elem <> $1 + ) + WHERE id = $2::uuid + RETURNING bathroom_images`, + [image_url, id] + ); + + return NextResponse.json({ success: true, bathroom_images: res.rows[0]?.bathroom_images }); + } catch (error: any) { + return NextResponse.json({ success: false, error: error.message }, { status: 500 }); + } +}