Bilder l??schen
This commit is contained in:
@@ -66,7 +66,12 @@ export async function POST(req: NextRequest) {
|
|||||||
address = COALESCE($4, address),
|
address = COALESCE($4, address),
|
||||||
image_url = COALESCE($5, image_url),
|
image_url = COALESCE($5, image_url),
|
||||||
room_size_m2 = COALESCE($6, room_size_m2),
|
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()
|
updated_at = NOW()
|
||||||
WHERE id = $8::uuid`,
|
WHERE id = $8::uuid`,
|
||||||
[
|
[
|
||||||
|
|||||||
185
src/app/page.tsx
185
src/app/page.tsx
@@ -112,8 +112,10 @@ export default function TravelDashboard() {
|
|||||||
const [priceHistory, setPriceHistory] = useState<PriceEntry[]>([]);
|
const [priceHistory, setPriceHistory] = useState<PriceEntry[]>([]);
|
||||||
const [loadingPrices, setLoadingPrices] = useState(false);
|
const [loadingPrices, setLoadingPrices] = useState(false);
|
||||||
|
|
||||||
// Bad-Fotos Modal & Lightbox
|
// Bad-Fotos Modal & Input
|
||||||
const [selectedBathroomHotel, setSelectedBathroomHotel] = useState<Hotel | null>(null);
|
const [selectedBathroomHotel, setSelectedBathroomHotel] = useState<Hotel | null>(null);
|
||||||
|
const [manualPhotoUrl, setManualPhotoUrl] = useState('');
|
||||||
|
const [isAddingPhoto, setIsAddingPhoto] = useState(false);
|
||||||
const [lightboxImage, setLightboxImage] = useState<string | null>(null);
|
const [lightboxImage, setLightboxImage] = useState<string | null>(null);
|
||||||
|
|
||||||
// Reise & Hotel Modals
|
// 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
|
// HANDLERS: REISEN & MITREISENDE
|
||||||
// ==========================================
|
// ==========================================
|
||||||
@@ -381,7 +433,7 @@ export default function TravelDashboard() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// ==========================================
|
// ==========================================
|
||||||
// HANDLERS: HOTELS, PREISE, AUSFLÜGE
|
// HANDLERS: HOTELS & SCANS
|
||||||
// ==========================================
|
// ==========================================
|
||||||
const openPriceHistoryModal = async (hotel: Hotel) => {
|
const openPriceHistoryModal = async (hotel: Hotel) => {
|
||||||
setSelectedPriceHotel(hotel);
|
setSelectedPriceHotel(hotel);
|
||||||
@@ -592,7 +644,6 @@ export default function TravelDashboard() {
|
|||||||
return acc;
|
return acc;
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// Preishistorie Stats
|
|
||||||
const pricesList = priceHistory.map(p => Number(p.price)).filter(p => p > 0);
|
const pricesList = priceHistory.map(p => Number(p.price)).filter(p => p > 0);
|
||||||
const minPrice = pricesList.length > 0 ? Math.min(...pricesList) : 0;
|
const minPrice = pricesList.length > 0 ? Math.min(...pricesList) : 0;
|
||||||
const maxPrice = pricesList.length > 0 ? Math.max(...pricesList) : 0;
|
const maxPrice = pricesList.length > 0 ? Math.max(...pricesList) : 0;
|
||||||
@@ -884,7 +935,7 @@ export default function TravelDashboard() {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
{/* Option / Ranking Header */}
|
{/* Option Header */}
|
||||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '0.65rem' }}>
|
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '0.65rem' }}>
|
||||||
{isCluster ? (
|
{isCluster ? (
|
||||||
<span style={{
|
<span style={{
|
||||||
@@ -926,7 +977,7 @@ export default function TravelDashboard() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* BAD & GOOGLE-FOTOS BUTTON-LEISTE */}
|
{/* BAD-BUTTON & GOOGLE-LINK */}
|
||||||
<div style={{ display: 'flex', gap: '0.4rem', marginBottom: '0.75rem', flexWrap: 'wrap', alignItems: 'center' }}>
|
<div style={{ display: 'flex', gap: '0.4rem', marginBottom: '0.75rem', flexWrap: 'wrap', alignItems: 'center' }}>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@@ -948,7 +999,7 @@ export default function TravelDashboard() {
|
|||||||
gap: '0.35rem'
|
gap: '0.35rem'
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
🚽 Bad-Fotos ({hotelBathImages.length > 0 ? hotelBathImages.length : '0'})
|
🚽 Bad-Fotos ({hotelBathImages.length})
|
||||||
</button>
|
</button>
|
||||||
<a
|
<a
|
||||||
href={getGoogleMapsPhotoUrl(hotel)}
|
href={getGoogleMapsPhotoUrl(hotel)}
|
||||||
@@ -1005,7 +1056,7 @@ export default function TravelDashboard() {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* PREIS-BOX MIT DOPPELTEM BESTPREIS-VERGLEICH */}
|
{/* PREIS-BOX */}
|
||||||
{totalPrice !== null ? (
|
{totalPrice !== null ? (
|
||||||
<div style={{ background: '#f8fafc', border: '1px solid #e2e8f0', borderRadius: '10px', padding: '0.65rem 0.8rem', marginBottom: '0.8rem', display: 'flex', flexDirection: 'column', gap: '0.5rem' }}>
|
<div style={{ background: '#f8fafc', border: '1px solid #e2e8f0', borderRadius: '10px', padding: '0.65rem 0.8rem', marginBottom: '0.8rem', display: 'flex', flexDirection: 'column', gap: '0.5rem' }}>
|
||||||
|
|
||||||
@@ -1154,7 +1205,7 @@ export default function TravelDashboard() {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{/* ======================================================== */}
|
{/* ======================================================== */}
|
||||||
{/* MODAL: BADEZIMMER & GAST-FOTOS (GARANTIERTER Z-INDEX) */}
|
{/* MODAL: BADEZIMMER & GAST-FOTOS MIT MANUELLEM IMPORT */}
|
||||||
{/* ======================================================== */}
|
{/* ======================================================== */}
|
||||||
{selectedBathroomHotel && (
|
{selectedBathroomHotel && (
|
||||||
<div
|
<div
|
||||||
@@ -1179,21 +1230,20 @@ export default function TravelDashboard() {
|
|||||||
style={{
|
style={{
|
||||||
background: '#ffffff',
|
background: '#ffffff',
|
||||||
borderRadius: '16px',
|
borderRadius: '16px',
|
||||||
maxWidth: '850px',
|
maxWidth: '880px',
|
||||||
width: '100%',
|
width: '100%',
|
||||||
maxHeight: '90vh',
|
maxHeight: '92vh',
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
flexDirection: 'column',
|
flexDirection: 'column',
|
||||||
boxShadow: '0 25px 50px -12px rgba(0,0,0,0.35)',
|
boxShadow: '0 25px 50px -12px rgba(0,0,0,0.35)',
|
||||||
overflow: 'hidden',
|
overflow: 'hidden'
|
||||||
position: 'relative'
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{/* Modal Header */}
|
{/* Modal Header */}
|
||||||
<div style={{ padding: '1.2rem 1.5rem', borderBottom: '1px solid #e2e8f0', display: 'flex', justifyContent: 'space-between', alignItems: 'center', background: '#f8fafc' }}>
|
<div style={{ padding: '1.2rem 1.5rem', borderBottom: '1px solid #e2e8f0', display: 'flex', justifyContent: 'space-between', alignItems: 'center', background: '#f8fafc' }}>
|
||||||
<div>
|
<div>
|
||||||
<h3 style={{ margin: 0, fontSize: '1.2rem', fontWeight: '800', color: '#0f172a' }}>
|
<h3 style={{ margin: 0, fontSize: '1.2rem', fontWeight: '800', color: '#0f172a' }}>
|
||||||
🚽 Bad- & Klo-Inspektion (Google Maps Gästebilder)
|
🚽 Bad- & Klo-Inspektion
|
||||||
</h3>
|
</h3>
|
||||||
<p style={{ margin: '0.2rem 0 0 0', fontSize: '0.82rem', color: '#64748b' }}>
|
<p style={{ margin: '0.2rem 0 0 0', fontSize: '0.82rem', color: '#64748b' }}>
|
||||||
{selectedBathroomHotel.name}
|
{selectedBathroomHotel.name}
|
||||||
@@ -1210,6 +1260,32 @@ export default function TravelDashboard() {
|
|||||||
|
|
||||||
{/* Modal Body */}
|
{/* Modal Body */}
|
||||||
<div style={{ padding: '1.5rem', overflowY: 'auto' }}>
|
<div style={{ padding: '1.5rem', overflowY: 'auto' }}>
|
||||||
|
|
||||||
|
{/* 1. MANUELLER LINK-IMPORT */}
|
||||||
|
<form onSubmit={handleAddManualPhoto} style={{ background: '#f0fdf4', border: '1.5px solid #a7f3d0', borderRadius: '12px', padding: '1rem 1.2rem', marginBottom: '1.5rem' }}>
|
||||||
|
<div style={{ fontSize: '0.85rem', fontWeight: '800', color: '#065f46', marginBottom: '0.4rem', display: 'flex', alignItems: 'center', gap: '0.4rem' }}>
|
||||||
|
<span>➕ Google Maps Bild-Link oder Foto-URL einfügen:</span>
|
||||||
|
</div>
|
||||||
|
<div style={{ display: 'flex', gap: '0.6rem', flexWrap: 'wrap' }}>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
required
|
||||||
|
placeholder="Google Maps URL oder https://lh3.googleusercontent.com/... einfügen"
|
||||||
|
value={manualPhotoUrl}
|
||||||
|
onChange={(e) => 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' }}
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
disabled={isAddingPhoto}
|
||||||
|
style={{ background: '#10b981', color: '#ffffff', border: 'none', padding: '0.65rem 1.2rem', borderRadius: '8px', fontWeight: '700', fontSize: '0.85rem', cursor: 'pointer', opacity: isAddingPhoto ? 0.7 : 1 }}
|
||||||
|
>
|
||||||
|
{isAddingPhoto ? 'Speichere...' : '➕ Bild speichern'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
{/* KI-ZUSAMMENFASSUNG */}
|
||||||
{selectedBathroomHotel.review_summary_bathroom && (
|
{selectedBathroomHotel.review_summary_bathroom && (
|
||||||
<div style={{ background: '#f0fdf4', border: '1px solid #bbf7d0', borderRadius: '10px', padding: '0.8rem 1rem', marginBottom: '1.2rem' }}>
|
<div style={{ background: '#f0fdf4', border: '1px solid #bbf7d0', borderRadius: '10px', padding: '0.8rem 1rem', marginBottom: '1.2rem' }}>
|
||||||
<span style={{ fontWeight: '800', fontSize: '0.82rem', color: '#166534', display: 'block', marginBottom: '0.2rem' }}>
|
<span style={{ fontWeight: '800', fontSize: '0.82rem', color: '#166534', display: 'block', marginBottom: '0.2rem' }}>
|
||||||
@@ -1221,57 +1297,70 @@ export default function TravelDashboard() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* FOTOGALERIE MIT LÖSCH-FUNKTION */}
|
||||||
{(() => {
|
{(() => {
|
||||||
const photos = parseBathroomImages(selectedBathroomHotel.bathroom_images);
|
const photos = parseBathroomImages(selectedBathroomHotel.bathroom_images);
|
||||||
return photos.length > 0 ? (
|
return (
|
||||||
|
<div>
|
||||||
|
<div style={{ fontSize: '0.85rem', fontWeight: '800', color: '#334155', marginBottom: '0.8rem', display: 'flex', justifyContent: 'space-between' }}>
|
||||||
|
<span>Gespeicherte Bad-Bilder ({photos.length})</span>
|
||||||
|
<span style={{ fontSize: '0.75rem', color: '#64748b', fontWeight: 'normal' }}>Klick zum Vergrößern · 🗑️ zum Löschen</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{photos.length > 0 ? (
|
||||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(220px, 1fr))', gap: '0.8rem' }}>
|
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(220px, 1fr))', gap: '0.8rem' }}>
|
||||||
{photos.map((imgUrl, i) => (
|
{photos.map((imgUrl, i) => (
|
||||||
<div
|
<div
|
||||||
key={i}
|
key={i}
|
||||||
onClick={() => setLightboxImage(imgUrl)}
|
style={{ borderRadius: '10px', overflow: 'hidden', border: '1px solid #e2e8f0', position: 'relative', background: '#0f172a', aspectRatio: '4/3' }}
|
||||||
style={{ borderRadius: '10px', overflow: 'hidden', border: '1px solid #e2e8f0', position: 'relative', background: '#0f172a', cursor: 'pointer', aspectRatio: '4/3' }}
|
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
src={imgUrl}
|
src={imgUrl}
|
||||||
alt={`Bad Foto ${i + 1}`}
|
alt={`Bad Foto ${i + 1}`}
|
||||||
style={{ width: '100%', height: '100%', objectFit: 'cover', display: 'block', transition: 'transform 0.2s' }}
|
onClick={() => setLightboxImage(imgUrl)}
|
||||||
|
style={{ width: '100%', height: '100%', objectFit: 'cover', display: 'block', cursor: 'pointer' }}
|
||||||
onError={(e) => { (e.target as HTMLElement).style.display = 'none'; }}
|
onError={(e) => { (e.target as HTMLElement).style.display = 'none'; }}
|
||||||
/>
|
/>
|
||||||
<span style={{ position: 'absolute', bottom: '6px', right: '6px', background: 'rgba(0,0,0,0.65)', color: '#fff', fontSize: '0.7rem', padding: '0.15rem 0.4rem', borderRadius: '4px', fontWeight: 'bold' }}>
|
<button
|
||||||
🔎 Vergrößern
|
type="button"
|
||||||
</span>
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
handleDeleteBathroomPhoto(imgUrl);
|
||||||
|
}}
|
||||||
|
title="Dieses Bild entfernen"
|
||||||
|
style={{
|
||||||
|
position: 'absolute',
|
||||||
|
top: '8px',
|
||||||
|
right: '8px',
|
||||||
|
background: 'rgba(220, 38, 38, 0.9)',
|
||||||
|
color: '#fff',
|
||||||
|
border: 'none',
|
||||||
|
borderRadius: '6px',
|
||||||
|
width: '28px',
|
||||||
|
height: '28px',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
cursor: 'pointer',
|
||||||
|
fontSize: '0.8rem'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
🗑️
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div style={{ textAlign: 'center', padding: '2.5rem 1rem', background: '#f8fafc', borderRadius: '12px', border: '1px dashed #cbd5e1' }}>
|
<div style={{ textAlign: 'center', padding: '2rem 1rem', background: '#f8fafc', borderRadius: '12px', border: '1px dashed #cbd5e1' }}>
|
||||||
<span style={{ fontSize: '2.5rem', display: 'block', marginBottom: '0.5rem' }}>📷</span>
|
<span style={{ fontSize: '2.5rem', display: 'block', marginBottom: '0.5rem' }}>📷</span>
|
||||||
<p style={{ margin: '0 0 0.5rem 0', fontWeight: '700', color: '#1e293b' }}>
|
<p style={{ margin: 0, fontWeight: '700', color: '#1e293b' }}>
|
||||||
Noch keine Bad-Bilder direkt in der Datenbank gespeichert.
|
Noch keine Bad-Bilder hinterlegt.
|
||||||
</p>
|
</p>
|
||||||
<p style={{ margin: '0 0 1.2rem 0', fontSize: '0.85rem', color: '#64748b' }}>
|
<p style={{ margin: '0.4rem 0 0 0', fontSize: '0.82rem', color: '#64748b' }}>
|
||||||
Klicke auf „KI-Reviews Scannen“, um Fotos automatisch abzurufen, oder öffne direkt Google Maps:
|
Füge oben einfach einen Link ein oder starte den automatischen Scan.
|
||||||
</p>
|
</p>
|
||||||
<div style={{ display: 'flex', gap: '0.6rem', justifyContent: 'center' }}>
|
|
||||||
<button
|
|
||||||
onClick={() => {
|
|
||||||
const hId = selectedBathroomHotel.id;
|
|
||||||
setSelectedBathroomHotel(null);
|
|
||||||
scanSingleHotel(hId);
|
|
||||||
}}
|
|
||||||
style={{ background: '#8b5cf6', color: '#fff', border: 'none', padding: '0.6rem 1.2rem', borderRadius: '8px', fontWeight: '700', cursor: 'pointer', fontSize: '0.85rem' }}
|
|
||||||
>
|
|
||||||
🤖 Jetzt Scan starten
|
|
||||||
</button>
|
|
||||||
<a
|
|
||||||
href={getGoogleMapsPhotoUrl(selectedBathroomHotel)}
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
style={{ background: '#2563eb', color: '#fff', padding: '0.6rem 1.2rem', borderRadius: '8px', fontWeight: '700', textDecoration: 'none', fontSize: '0.85rem' }}
|
|
||||||
>
|
|
||||||
🗺️ Auf Google Maps öffnen ↗
|
|
||||||
</a>
|
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
})()}
|
})()}
|
||||||
@@ -1285,23 +1374,21 @@ export default function TravelDashboard() {
|
|||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
style={{ color: '#2563eb', fontSize: '0.85rem', fontWeight: '700', textDecoration: 'none' }}
|
style={{ color: '#2563eb', fontSize: '0.85rem', fontWeight: '700', textDecoration: 'none' }}
|
||||||
>
|
>
|
||||||
🗺️ Alle Google Maps Fotos & Videos öffnen ↗
|
🗺️ Auf Google Maps ansehen ↗
|
||||||
</a>
|
</a>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setSelectedBathroomHotel(null)}
|
onClick={() => setSelectedBathroomHotel(null)}
|
||||||
style={{ background: '#2563eb', color: '#ffffff', border: 'none', padding: '0.5rem 1.2rem', borderRadius: '6px', fontWeight: '700', cursor: 'pointer', fontSize: '0.85rem' }}
|
style={{ background: '#2563eb', color: '#ffffff', border: 'none', padding: '0.5rem 1.2rem', borderRadius: '6px', fontWeight: '700', cursor: 'pointer', fontSize: '0.85rem' }}
|
||||||
>
|
>
|
||||||
Schließen
|
Fertig
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* ======================================================== */}
|
|
||||||
{/* LIGHTBOX GROSSANSICHT */}
|
{/* LIGHTBOX GROSSANSICHT */}
|
||||||
{/* ======================================================== */}
|
|
||||||
{lightboxImage && (
|
{lightboxImage && (
|
||||||
<div
|
<div
|
||||||
onClick={() => setLightboxImage(null)}
|
onClick={() => setLightboxImage(null)}
|
||||||
@@ -1311,7 +1398,7 @@ export default function TravelDashboard() {
|
|||||||
left: 0,
|
left: 0,
|
||||||
right: 0,
|
right: 0,
|
||||||
bottom: 0,
|
bottom: 0,
|
||||||
background: 'rgba(0, 0, 0, 0.9)',
|
background: 'rgba(0, 0, 0, 0.92)',
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
|
|||||||
66
travel-app/src/app/api/hotels/[id]/bathroom-images/route.ts
Normal file
66
travel-app/src/app/api/hotels/[id]/bathroom-images/route.ts
Normal file
@@ -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 });
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user