diff --git a/src/app/(protected)/admin/listings/[slug]/page.tsx b/src/app/(protected)/admin/listings/[slug]/page.tsx
index c1a1ebf..64dba1f 100644
--- a/src/app/(protected)/admin/listings/[slug]/page.tsx
+++ b/src/app/(protected)/admin/listings/[slug]/page.tsx
@@ -9,10 +9,12 @@ import { updateListing, deleteListing, addNote, addTagToListing, removeTagFromLi
export default async function EditListingPage({
params,
}: {
- params: { slug: string };
+ params: Promise<{ slug: string }>;
}) {
+ const { slug } = await params;
+
const listing = await prisma.listing.findUnique({
- where: { slug: params.slug },
+ where: { slug },
include: {
tags: {
include: { tag: true },
diff --git a/src/app/(protected)/compare/compare-button.tsx b/src/app/(protected)/compare/compare-button.tsx
new file mode 100644
index 0000000..63442f0
--- /dev/null
+++ b/src/app/(protected)/compare/compare-button.tsx
@@ -0,0 +1,41 @@
+"use client";
+
+import { useRouter } from "next/navigation"
+
+interface CompareButtonProps {
+ listingId: string;
+ currentIds: string[];
+}
+
+export function CompareButton({ listingId, currentIds }: CompareButtonProps) {
+ const router = useRouter();
+ const isInCompare = currentIds.includes(listingId);
+
+ const handleToggle = () => {
+ if (isInCompare) {
+ const newIds = currentIds.filter((id) => id !== listingId);
+ const params = new URLSearchParams();
+ if (newIds.length > 0) params.set("ids", newIds.join(","));
+ router.push(`/compare?${params.toString()}`);
+ router.push(`/compare`);
+ } else {
+ const newIds = [...currentIds, listingId];
+ const params = new URLSearchParams();
+ params.set("ids", newIds.join(","));
+ router.push(`/compare?${params.toString()}`);
+ }
+ };
+
+ return (
+
+ );
+}
diff --git a/src/app/(protected)/compare/page.tsx b/src/app/(protected)/compare/page.tsx
index d47e396..b9435a5 100644
--- a/src/app/(protected)/compare/page.tsx
+++ b/src/app/(protected)/compare/page.tsx
@@ -1,17 +1,38 @@
import { prisma } from "@/lib/prisma";
import { Card, CardContent } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
+import { Button } from "@/components/ui/button";
import Link from "next/link";
+import { formatPrice, formatRating } from "@/lib/utils";
+import { CompareButton } from "./compare-button";
-export default async function ComparePage() {
- const listings = await prisma.listing.findMany({
- where: {
- OR: [{ status: "SHORTLIST" }, { isFavorite: true }],
- },
- orderBy: { rating: "desc" },
- include: {
- sleepingOptions: true,
- },
+export default async function ComparePage({
+ searchParams,
+}: {
+ searchParams: Promise<{ ids?: string }>;
+}) {
+ const params = await searchParams;
+ const compareIds = params.ids?.split(",").filter(Boolean) || [];
+
+ // If no IDs in URL, show shortlist + favorites
+ const listings = compareIds.length > 0
+ ? await prisma.listing.findMany({
+ where: { id: { in: compareIds } },
+ orderBy: { rating: "desc" },
+ include: { sleepingOptions: true, tags: { include: { tag: true } } },
+ })
+ : await prisma.listing.findMany({
+ where: {
+ OR: [{ status: "SHORTLIST" }, { isFavorite: true }],
+ },
+ orderBy: { rating: "desc" },
+ include: { sleepingOptions: true, tags: { include: { tag: true } } },
+ });
+
+ // Get all listings for the add dropdown
+ const allListings = await prisma.listing.findMany({
+ select: { id: true, title: true, locationText: true },
+ orderBy: { importedAt: "desc" },
});
if (listings.length === 0) {
@@ -20,60 +41,100 @@ export default async function ComparePage() {
📊 Vergleich
- Markiere Listings als Favorit oder setze Status auf "Shortlist" für
- den Vergleich
+ Keine Listings zum Vergleichen vorhanden.
-
- ← Zu allen Listings
+
+ Markiere Listings als Favorit, setze Status auf "Shortlist" oder
+ füge Listings direkt über die URL hinzu.
+
+
+
);
}
+ // Calculate best values for highlighting
+ const prices = listings.map((l) => l.nightlyPrice).filter(Boolean) as number[];
+ const ratings = listings.map((l) => l.rating).filter(Boolean) as number[];
+ const minPrice = prices.length > 0 ? Math.min(...prices) : null;
+ const maxRating = ratings.length > 0 ? Math.max(...ratings) : null;
+
return (
📊 Vergleich ({listings.length})
-
- ← Zu allen Listings
-
+
+
+ ← Zu allen Listings
+
+
+ {/* Add to compare form */}
+
+
+
+
+
+
+ {/* Comparison Table */}
- | Listing |
+ Listing |
€/Nacht |
⭐ Rating |
📍 Ort |
🛏️ Schlafz. |
🛏 Betten |
+ 🚿 Bad |
👥 Max |
4er-tauglich |
Extra Matratzen |
- Schlafplätze |
+ Tags |
+ Aktionen |
{listings.map((listing, idx) => {
- const isBestPrice =
- listing.nightlyPrice ===
- Math.min(...listings.map((l) => l.nightlyPrice || Infinity));
- const isBestRating =
- listing.rating ===
- Math.max(...listings.map((l) => l.rating || 0));
+ const isBestPrice = listing.nightlyPrice === minPrice;
+ const isBestRating = listing.rating === maxRating;
return (
- |
+ |
- €{listing.nightlyPrice?.toFixed(2) || "—"}
+ {formatPrice(listing.nightlyPrice)}
{isBestPrice && (
Beste
)}
@@ -110,7 +171,7 @@ export default async function ComparePage() {
isBestRating ? "text-green-600 bg-green-50 font-bold" : ""
}`}
>
- {listing.rating?.toFixed(2) || "—"}
+ {formatRating(listing.rating)}
{isBestRating && (
Top
)}
@@ -118,33 +179,72 @@ export default async function ComparePage() {
|
{listing.locationText || "—"}
|
- {listing.bedrooms || "—"} |
- {listing.beds || "—"} |
+ {listing.bedrooms ?? "—"} |
+ {listing.beds ?? "—"} |
+
+ {listing.bathrooms != null ? listing.bathrooms.toFixed(1) : "—"}
+ |
- {listing.maxSleepingPlaces || "—"}
+ {listing.maxSleepingPlaces ?? listing.guestCount ?? "—"}
|
- {listing.suitableFor4 ? (
+ {listing.suitableFor4 == null ? (
+ ❓
+ ) : listing.suitableFor4 ? (
✅ Ja
) : (
❌ Nein
)}
|
- {listing.extraMattressesNeededFor4 !== null ? (
- listing.extraMattressesNeededFor4 === 0 ? (
- 0 ✅
- ) : (
-
- {listing.extraMattressesNeededFor4}
-
- )
- ) : (
+ {listing.extraMattressesNeededFor4 == null ? (
"—"
+ ) : listing.extraMattressesNeededFor4 === 0 ? (
+ 0 ✅
+ ) : (
+
+ {listing.extraMattressesNeededFor4}
+
)}
|
-
- {listing.bedTypesSummary || "—"}
+ |
+
+ {listing.tags.slice(0, 3).map((lt) => (
+
+ {lt.tag.name}
+
+ ))}
+ {listing.tags.length > 3 && (
+
+ +{listing.tags.length - 3}
+
+ )}
+
+ |
+
+
|
);
diff --git a/src/app/(protected)/dashboard/page.tsx b/src/app/(protected)/dashboard/page.tsx
index 38b031f..f19d00c 100644
--- a/src/app/(protected)/dashboard/page.tsx
+++ b/src/app/(protected)/dashboard/page.tsx
@@ -1,18 +1,10 @@
import { prisma } from "@/lib/prisma";
-import { redirect } from "next/navigation";
import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
-import { auth } from "@/lib/auth";
import Link from "next/link";
export default async function DashboardPage() {
- const session = await auth();
-
- if (!session) {
- redirect("/login");
- }
-
- const [totalCount, stats, favoriteCount] = await Promise.all([
+ const [totalCount, stats, favoriteCount, recentListings, favoriteListings] = await Promise.all([
prisma.listing.count(),
prisma.listing.aggregate({
_avg: {
@@ -21,29 +13,31 @@ export default async function DashboardPage() {
},
}),
prisma.listing.count({ where: { isFavorite: true } }),
+ prisma.listing.findMany({
+ take: 5,
+ orderBy: { importedAt: "desc" },
+ }),
+ prisma.listing.findMany({
+ where: { isFavorite: true },
+ take: 5,
+ }),
]);
- const recentListings = await prisma.listing.findMany({
- take: 5,
- orderBy: { importedAt: "desc" },
- });
-
- const favoriteListings = await prisma.listing.findMany({
- take: 3,
- where: { isFavorite: true },
- orderBy: { rating: "desc" },
- });
+ const avgPrice = stats._avg.nightlyPrice?.toFixed(2) || "—";
+ const avgRating = stats._avg.rating?.toFixed(2) || "—";
return (
-
-
+
+
+ {/* Header */}
-
🏠 Airbnb Finder
+ 🏠 Dashboard
+ {/* Stats Grid */}
@@ -60,9 +54,7 @@ export default async function DashboardPage() {
⌀ Durchschnittspreis
-
- €{stats._avg.nightlyPrice?.toFixed(2) || "—"}
-
+ €{avgPrice}
pro Nacht
@@ -72,9 +64,7 @@ export default async function DashboardPage() {
⭐ Durchschnittsbewertung
-
- {stats._avg.rating?.toFixed(2) || "—"}
-
+ {avgRating}
von 5.00
@@ -90,39 +80,76 @@ export default async function DashboardPage() {
+ {/* Recent Listings */}
-
Zuletzt importiert
-
- {recentListings.map((listing) => (
-
-
- {listing.title}
- {listing.locationText}
-
- €{listing.nightlyPrice?.toFixed(2) || "—"}
- ⭐ {listing.rating?.toFixed(2) || "—"}
-
-
-
- ))}
-
+
🕐 Zuletzt importiert
+ {recentListings.length > 0 ? (
+
+ {recentListings.map((listing) => (
+
+
+ {listing.coverImage ? (
+
+ ) : (
+
+ 🏠
+
+ )}
+
+ {listing.title}
+ {listing.locationText || "Kein Ort"}
+
+ €{listing.nightlyPrice?.toFixed(2) || "—"}
+ ⭐ {listing.rating?.toFixed(2) || "—"}
+
+
+
+
+ ))}
+
+ ) : (
+
+ Keine Listings vorhanden
+
+ ➕ Erstes Listing importieren
+
+
+ )}
+ {/* Favorites */}
{favoriteListings.length > 0 && (
❤️ Favoriten
-
+
{favoriteListings.map((listing) => (
-
-
- {listing.title}
- {listing.locationText}
-
- €{listing.nightlyPrice?.toFixed(2) || "—"}
- ⭐ {listing.rating?.toFixed(2) || "—"}
-
-
-
+
+
+ {listing.coverImage ? (
+
+ ) : (
+
+ 🏠
+
+ )}
+
+ {listing.title}
+ {listing.locationText || "Kein Ort"}
+
+ €{listing.nightlyPrice?.toFixed(2) || "—"}
+ ⭐ {listing.rating?.toFixed(2) || "—"}
+
+
+
+
))}
diff --git a/src/app/(protected)/listings/[slug]/error.tsx b/src/app/(protected)/listings/[slug]/error.tsx
new file mode 100644
index 0000000..94ccf73
--- /dev/null
+++ b/src/app/(protected)/listings/[slug]/error.tsx
@@ -0,0 +1,33 @@
+"use client";
+
+import { useEffect } from "react";
+
+export default function Error({
+ error,
+ reset,
+}: {
+ error: Error & { digest?: string };
+ reset: () => void;
+}) {
+ useEffect(() => {
+ console.error("Detail page error:", error);
+ }, [error]);
+
+ return (
+
+
+
😵
+
Fehler beim Laden
+
+ Das Listing konnte nicht geladen werden.
+
+
+
+
+ );
+}
diff --git a/src/app/(protected)/listings/[slug]/loading.tsx b/src/app/(protected)/listings/[slug]/loading.tsx
new file mode 100644
index 0000000..c42d0ab
--- /dev/null
+++ b/src/app/(protected)/listings/[slug]/loading.tsx
@@ -0,0 +1,22 @@
+export default function Loading() {
+ return (
+
+ );
+}
diff --git a/src/app/(protected)/listings/[slug]/not-found.tsx b/src/app/(protected)/listings/[slug]/not-found.tsx
new file mode 100644
index 0000000..ef1c4c5
--- /dev/null
+++ b/src/app/(protected)/listings/[slug]/not-found.tsx
@@ -0,0 +1,18 @@
+import Link from "next/link";
+
+export default function NotFound() {
+ return (
+
+
+
🏠
+
Listing nicht gefunden
+
+ Das gesuchte Listing existiert nicht oder wurde gelöscht.
+
+
+ ← Zurück zu allen Listings
+
+
+
+ );
+}
diff --git a/src/app/(protected)/listings/[slug]/page.tsx b/src/app/(protected)/listings/[slug]/page.tsx
index 462b4c5..34ae714 100644
--- a/src/app/(protected)/listings/[slug]/page.tsx
+++ b/src/app/(protected)/listings/[slug]/page.tsx
@@ -1,39 +1,34 @@
import { prisma } from "@/lib/prisma";
-import { redirect } from "next/navigation";
-import { auth } from "@/lib/auth";
+import { notFound } from "next/navigation";
import { Card, CardContent } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
import Link from "next/link";
+import { formatPrice, formatRating } from "@/lib/utils";
-export default async function ListingDetailPage({
- params,
-}: {
- params: { slug: string };
-}) {
- const session = await auth();
-
- if (!session) {
- redirect("/login");
- }
+interface PageProps {
+ params: Promise<{ slug: string }>;
+}
+
+export default async function ListingDetailPage({ params }: PageProps) {
+ const { slug } = await params;
const listing = await prisma.listing.findUnique({
- where: { slug: params.slug },
+ where: { slug },
include: {
images: true,
notes: true,
sleepingOptions: true,
- tags: {
- include: {
- tag: true,
- },
- },
+ tags: { include: { tag: true } },
},
});
if (!listing) {
- redirect("/listings");
+ notFound();
}
+ const isBest4Person = listing.suitableFor4 === true;
+ const extraMattresses = listing.extraMattressesNeededFor4 ?? null;
+
return (
@@ -47,7 +42,7 @@ export default async function ListingDetailPage({
{/* Main Content */}
- {/* Images */}
+ {/* Cover Image */}
{listing.coverImage ? (
@@ -57,8 +52,8 @@ export default async function ListingDetailPage({
className="w-full h-96 object-cover"
/>
) : (
-
- Kein Bild
+
+ Kein Bild vorhanden
)}
@@ -72,26 +67,28 @@ export default async function ListingDetailPage({
Schlafzimmer
-
{listing.bedrooms || "—"}
+
{listing.bedrooms ?? "—"}
Betten
-
{listing.beds || "—"}
+
{listing.beds ?? "—"}
Badezimmer
-
{listing.bathrooms || "—"}
+
+ {listing.bathrooms != null ? listing.bathrooms : "—"}
+
Max Gäste
-
{listing.maxSleepingPlaces || "—"}
+
{listing.maxSleepingPlaces ?? listing.guestCount ?? "—"}
{listing.description && (
Beschreibung
-
{listing.description}
+
{listing.description}
)}
@@ -105,14 +102,14 @@ export default async function ListingDetailPage({
4 Personen geeignet
-
- {listing.suitableFor4 ? "✅ Ja" : "❌ Nein"}
+
+ {listing.suitableFor4 == null ? "❓ Unbekannt" : isBest4Person ? "✅ Ja" : "❌ Nein"}
Extra Matratzen für 4
-
- {listing.extraMattressesNeededFor4 ?? "—"}
+
0 ? "text-amber-600" : ""}`}>
+ {extraMattresses ?? "—"}
@@ -161,51 +158,54 @@ export default async function ListingDetailPage({
{listing.title}
- 📍 {listing.locationText || "Kein Ort"}
+ 📍 {listing.locationText || "Ort unbekannt"}
-
- €{listing.nightlyPrice?.toFixed(2) || "—"}
-
+ {formatPrice(listing.nightlyPrice)}
/ Nacht
⭐
- {listing.rating?.toFixed(2) || "—"}
- {listing.reviewCount && (
-
- ({listing.reviewCount} Bewertungen)
-
+ {formatRating(listing.rating)}
+ {listing.reviewCount != null && (
+ ({listing.reviewCount} Bewertungen)
)}
{listing.hostName && (
-
- 👤 Host: {listing.hostName}
-
+ 👤 Host: {listing.hostName}
)}
-
- {listing.tags.map((lt) => (
-
- {lt.tag.name}
-
- ))}
-
+ {listing.tags.length > 0 && (
+
+ {listing.tags.map((lt) => (
+
+ {lt.tag.name}
+
+ ))}
+
+ )}
🔗 Auf Airbnb ansehen
+
+
+ ✏️ Bearbeiten
+
@@ -216,10 +216,12 @@ export default async function ListingDetailPage({
{listing.status}
diff --git a/src/app/(protected)/listings/actions.ts b/src/app/(protected)/listings/actions.ts
new file mode 100644
index 0000000..6357eb3
--- /dev/null
+++ b/src/app/(protected)/listings/actions.ts
@@ -0,0 +1,38 @@
+"use server";
+
+import { prisma } from "@/lib/prisma";
+import { revalidatePath } from "next/cache";
+
+export async function deleteListing(listingId: string) {
+ try {
+ // Delete related records first
+ await prisma.listingTag.deleteMany({
+ where: { listingId },
+ });
+
+ await prisma.listingSleepingOption.deleteMany({
+ where: { listingId },
+ });
+
+ await prisma.listingImage.deleteMany({
+ where: { listingId },
+ });
+
+ await prisma.adminNote.deleteMany({
+ where: { listingId },
+ });
+
+ // Delete the listing
+ await prisma.listing.delete({
+ where: { id: listingId },
+ });
+
+ revalidatePath("/listings");
+ revalidatePath("/dashboard");
+
+ return { success: true };
+ } catch (error) {
+ console.error("Delete error:", error);
+ throw new Error("Fehler beim Löschen des Listings");
+ }
+}
diff --git a/src/app/(protected)/listings/delete-button.tsx b/src/app/(protected)/listings/delete-button.tsx
new file mode 100644
index 0000000..968d7b4
--- /dev/null
+++ b/src/app/(protected)/listings/delete-button.tsx
@@ -0,0 +1,42 @@
+"use client";
+
+import { useState } from "react";
+import { Button } from "@/components/ui/button";
+import { deleteListing } from "./actions";
+
+interface DeleteListingButtonProps {
+ listingId: string;
+ listingTitle: string;
+}
+
+export function DeleteListingButton({ listingId, listingTitle }: DeleteListingButtonProps) {
+ const [isDeleting, setIsDeleting] = useState(false);
+ const [showConfirm, setShowConfirm] = useState(false);
+
+ const handleDelete = async () => {
+ if (!confirm(`"${listingTitle}" wirklich löschen?`)) return;
+
+ setIsDeleting(true);
+ try {
+ await deleteListing(listingId);
+ // Page will refresh automatically due to revalidation
+ window.location.reload();
+ } catch (error) {
+ alert("Fehler beim Löschen: " + (error as Error).message);
+ } finally {
+ setIsDeleting(false);
+ }
+ };
+
+ return (
+
+ );
+}
diff --git a/src/app/(protected)/listings/page.tsx b/src/app/(protected)/listings/page.tsx
index 1863693..a69c29b 100644
--- a/src/app/(protected)/listings/page.tsx
+++ b/src/app/(protected)/listings/page.tsx
@@ -1,7 +1,9 @@
import { prisma } from "@/lib/prisma";
import { Card, CardContent } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
+import { Button } from "@/components/ui/button";
import Link from "next/link";
+import { DeleteListingButton } from "./delete-button";
export default async function ListingsPage() {
const listings = await prisma.listing.findMany({
@@ -19,91 +21,92 @@ export default async function ListingsPage() {
-
🏠 Alle Listings
-
- ➕ Neu importieren
+ 📋 Alle Listings
+
+
-
+
{listings.map((listing) => (
-
-
- {listing.coverImage && (
-
-

- {listing.isFavorite && (
-
- ❤️
-
- )}
+
+ {/* Image */}
+
+ {listing.coverImage ? (
+
+ ) : (
+
+ 🏠 Kein Bild
)}
-
-
+
+
+
+ {/* Title - Clickable */}
+
+
{listing.title}
-
-
- 📍 {listing.locationText || "Kein Ort"}
-
+
+
+
+ 📍 {listing.locationText || "Kein Ort"}
-
-
- €{listing.nightlyPrice?.toFixed(2) || "—"}
-
-
- ⭐ {listing.rating?.toFixed(2) || "—"}
-
-
+ {/* Price & Rating */}
+
+ €{listing.nightlyPrice?.toFixed(2) || "—"}
+ ⭐ {listing.rating?.toFixed(2) || "—"}
+
-
- {listing.bedrooms && (
-
🛏️ {listing.bedrooms}
- )}
- {listing.beds && (
-
🛏 {listing.beds}
- )}
- {listing.bathrooms && (
-
🚿 {listing.bathrooms}
- )}
- {listing.maxSleepingPlaces && (
-
- 👥 {listing.maxSleepingPlaces}
+ {/* Tags */}
+ {listing.tags.length > 0 && (
+
+ {listing.tags.slice(0, 3).map((lt) => (
+
+ {lt.tag.name}
- )}
+ ))}
+ )}
- {listing.tags.length > 0 && (
-
- {listing.tags.slice(0, 3).map((lt) => (
-
- {lt.tag.name}
-
- ))}
-
- )}
+ {/* Sleep Info */}
+ {listing.suitableFor4 ? (
+ ✅ Geeignet für 4 Personen
+ ) : (
+
+ ⚠️ Nicht ideal für 4 {listing.extraMattressesNeededFor4 ? `(+${listing.extraMattressesNeededFor4} Matratzen)` : ""}
+
+ )}
- {listing.suitableFor4 && (
-
- ✅ Geeignet für 4 Personen
-
- )}
-
-
-
+ {/* Actions */}
+
+
+
+
+
+
+
+
+
+
+
))}
{listings.length === 0 && (
+
🏠
Keine Listings vorhanden
➕ Erstes Listing importieren
diff --git a/src/app/(protected)/map/map-client.tsx b/src/app/(protected)/map/map-client.tsx
new file mode 100644
index 0000000..ee04458
--- /dev/null
+++ b/src/app/(protected)/map/map-client.tsx
@@ -0,0 +1,186 @@
+"use client";
+
+import { useEffect, useState } from "react";
+import { Card, CardContent } from "@/components/ui/card";
+import { formatPrice, formatRating } from "@/lib/utils";
+
+interface Listing {
+ id: string;
+ slug: string;
+ title: string;
+ locationText: string | null;
+ latitude: number | null;
+ longitude: number | null;
+ nightlyPrice: number | null;
+ rating: number | null;
+ coverImage: string | null;
+ isFavorite: boolean;
+}
+
+export default function MapClient() {
+ const [listings, setListings] = useState
([]);
+ const [loading, setLoading] = useState(true);
+ const [error, setError] = useState(null);
+ const [MapComponents, setMapComponents] = useState<{
+ MapContainer: any;
+ TileLayer: any;
+ Marker: any;
+ Popup: any;
+ } | null>(null);
+
+ useEffect(() => {
+ // Load Leaflet CSS
+ const link = document.createElement("link");
+ link.rel = "stylesheet";
+ link.href = "https://unpkg.com/leaflet@1.9.4/dist/leaflet.css";
+ link.integrity = "sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY=";
+ link.crossOrigin = "";
+ document.head.appendChild(link);
+
+ // Load React-Leaflet components
+ import("react-leaflet").then((mod) => {
+ setMapComponents({
+ MapContainer: mod.MapContainer,
+ TileLayer: mod.TileLayer,
+ Marker: mod.Marker,
+ Popup: mod.Popup,
+ });
+ });
+
+ // Fetch listings
+ fetch("/api/listings")
+ .then((res) => res.json())
+ .then((data) => {
+ setListings(data.listings || []);
+ setLoading(false);
+ })
+ .catch((err) => {
+ console.error("Map fetch error:", err);
+ setError("Fehler beim Laden der Karte");
+ setLoading(false);
+ });
+ }, []);
+
+ if (loading) {
+ return (
+
+
+
🗺️ Kartenansicht
+
+
+ Lade Karte...
+
+
+
+
+ );
+ }
+
+ if (error) {
+ return (
+
+
+
🗺️ Kartenansicht
+
+
+ {error}
+
+
+
+
+ );
+ }
+
+ const listingsWithCoords = listings.filter((l) => l.latitude && l.longitude);
+
+ if (!MapComponents) {
+ return (
+
+
+
🗺️ Kartenansicht
+
+
+ Initialisiere Karte...
+
+
+
+
+ );
+ }
+
+ const { MapContainer, TileLayer, Marker, Popup } = MapComponents;
+
+ const center: [number, number] =
+ listingsWithCoords.length > 0
+ ? [listingsWithCoords[0].latitude!, listingsWithCoords[0].longitude!]
+ : [51.0504, 13.7373]; // Dresden as default
+
+ return (
+
+
+
🗺️ Kartenansicht
+
+
+
+
+
+
+ {listingsWithCoords.map((listing) => (
+
+
+
+ {listing.coverImage && (
+

+ )}
+
+ {listing.title}
+
+
+ {listing.locationText || "Kein Ort"}
+
+
+ {formatPrice(listing.nightlyPrice)}
+ ⭐ {formatRating(listing.rating)}
+
+
+ → Details
+
+
+
+
+ ))}
+
+
+
+
+
+ {listingsWithCoords.length === 0 && (
+
+ Keine Listings mit Koordinaten vorhanden
+
+ )}
+
+
+ {listingsWithCoords.length} von {listings.length} Listings auf der Karte sichtbar
+
+
+
+ );
+}
diff --git a/src/app/(protected)/map/page.tsx b/src/app/(protected)/map/page.tsx
index 0bd4bfb..1958900 100644
--- a/src/app/(protected)/map/page.tsx
+++ b/src/app/(protected)/map/page.tsx
@@ -1,144 +1,14 @@
"use client";
-import { useEffect, useState } from "react";
-import { Card, CardContent } from "@/components/ui/card";
import dynamic from "next/dynamic";
-// Dynamically import Leaflet (SSR=false)
-const MapContainer = dynamic(
- () => import("react-leaflet").then((mod) => mod.MapContainer),
- { ssr: false }
-);
-const TileLayer = dynamic(
- () => import("react-leaflet").then((mod) => mod.TileLayer),
- { ssr: false }
-);
-const Marker = dynamic(
- () => import("react-leaflet").then((mod) => mod.Marker),
- { ssr: false }
-);
-const Popup = dynamic(
- () => import("react-leaflet").then((mod) => mod.Popup),
- { ssr: false }
-);
-
-interface Listing {
- id: string;
- slug: string;
- title: string;
- locationText: string | null;
- latitude: number | null;
- longitude: number | null;
- nightlyPrice: number | null;
- rating: number | null;
- coverImage: string | null;
- isFavorite: boolean;
-}
+const MapClient = dynamic(() => import("./map-client"), {
+ ssr: false,
+ loading: () => (
+
+ ),
+});
export default function MapPage() {
- const [listings, setListings] = useState([]);
- const [loading, setLoading] = useState(true);
-
- useEffect(() => {
- fetch("/api/listings")
- .then((res) => res.json())
- .then((data) => {
- setListings(data.listings || []);
- setLoading(false);
- })
- .catch(() => setLoading(false));
- }, []);
-
- if (loading) {
- return (
-
- );
- }
-
- const listingsWithCoords = listings.filter(
- (l) => l.latitude && l.longitude
- );
-
- return (
-
-
-
🗺️ Kartenansicht
-
-
-
-
- {typeof window !== "undefined" && (
-
0
- ? [
- listingsWithCoords[0].latitude!,
- listingsWithCoords[0].longitude!,
- ]
- : [51.0504, 13.7373]
- }
- zoom={11}
- className="h-full w-full"
- >
-
- {listingsWithCoords.map((listing) => (
-
-
-
- {listing.coverImage && (
-

- )}
-
- {listing.title}
-
-
- {listing.locationText}
-
-
-
- €{listing.nightlyPrice?.toFixed(2)}
-
- ⭐ {listing.rating?.toFixed(2)}
-
-
- → Details
-
-
-
-
- ))}
-
- )}
-
-
-
-
- {listingsWithCoords.length === 0 && (
-
- Keine Listings mit Koordinaten vorhanden
-
- )}
-
-
- {listingsWithCoords.length} von {listings.length} Listings auf der
- Karte sichtbar
-
-
-
- );
+ return ;
}
diff --git a/src/app/api/listings/route.ts b/src/app/api/listings/route.ts
index 923fb5b..acee5d3 100644
--- a/src/app/api/listings/route.ts
+++ b/src/app/api/listings/route.ts
@@ -15,15 +15,23 @@ export async function GET(request: NextRequest) {
rating: true,
coverImage: true,
isFavorite: true,
+ suitableFor4: true,
+ extraMattressesNeededFor4: true,
+ maxSleepingPlaces: true,
+ bedrooms: true,
+ beds: true,
+ bathrooms: true,
+ guestCount: true,
+ bedTypesSummary: true,
},
orderBy: { importedAt: "desc" },
});
return NextResponse.json({ listings });
} catch (error) {
- console.error("Error fetching listings:", error);
+ console.error("API /listings error:", error);
return NextResponse.json(
- { error: "Failed to fetch listings" },
+ { error: "Failed to fetch listings", listings: [] },
{ status: 500 }
);
}
diff --git a/src/app/layout.tsx b/src/app/layout.tsx
index f7fa87e..25a03ab 100644
--- a/src/app/layout.tsx
+++ b/src/app/layout.tsx
@@ -13,8 +13,14 @@ const geistMono = Geist_Mono({
});
export const metadata: Metadata = {
- title: "Create Next App",
- description: "Generated by create next app",
+ title: {
+ default: "Airbnb Finder - Listings vergleichen & verwalten",
+ template: "%s | Airbnb Finder",
+ },
+ description: "Airbnb Listings importieren, vergleichen und für 4 Personen analysieren",
+ icons: {
+ icon: "/favicon.ico",
+ },
};
export default function RootLayout({
@@ -23,7 +29,7 @@ export default function RootLayout({
children: React.ReactNode;
}>) {
return (
-
+