235 lines
8.3 KiB
TypeScript
235 lines
8.3 KiB
TypeScript
|
|
import { prisma } from "@/lib/prisma";
|
|||
|
|
import { redirect } from "next/navigation";
|
|||
|
|
import { auth } from "@/lib/auth";
|
|||
|
|
import { Card, CardContent } from "@/components/ui/card";
|
|||
|
|
import { Badge } from "@/components/ui/badge";
|
|||
|
|
import Link from "next/link";
|
|||
|
|
|
|||
|
|
export default async function ListingDetailPage({
|
|||
|
|
params,
|
|||
|
|
}: {
|
|||
|
|
params: { slug: string };
|
|||
|
|
}) {
|
|||
|
|
const session = await auth();
|
|||
|
|
|
|||
|
|
if (!session) {
|
|||
|
|
redirect("/login");
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const listing = await prisma.listing.findUnique({
|
|||
|
|
where: { slug: params.slug },
|
|||
|
|
include: {
|
|||
|
|
images: true,
|
|||
|
|
notes: true,
|
|||
|
|
sleepingOptions: true,
|
|||
|
|
tags: {
|
|||
|
|
include: {
|
|||
|
|
tag: true,
|
|||
|
|
},
|
|||
|
|
},
|
|||
|
|
},
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
if (!listing) {
|
|||
|
|
redirect("/listings");
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return (
|
|||
|
|
<div className="min-h-screen bg-slate-50 p-8">
|
|||
|
|
<div className="container mx-auto max-w-6xl">
|
|||
|
|
<Link
|
|||
|
|
href="/listings"
|
|||
|
|
className="text-blue-600 hover:underline mb-6 inline-block"
|
|||
|
|
>
|
|||
|
|
← Zurück zur Übersicht
|
|||
|
|
</Link>
|
|||
|
|
|
|||
|
|
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
|||
|
|
{/* Main Content */}
|
|||
|
|
<div className="lg:col-span-2 space-y-6">
|
|||
|
|
{/* Images */}
|
|||
|
|
<Card>
|
|||
|
|
<CardContent className="p-0 overflow-hidden">
|
|||
|
|
{listing.coverImage ? (
|
|||
|
|
<img
|
|||
|
|
src={listing.coverImage}
|
|||
|
|
alt={listing.title}
|
|||
|
|
className="w-full h-96 object-cover"
|
|||
|
|
/>
|
|||
|
|
) : (
|
|||
|
|
<div className="w-full h-96 bg-slate-200 flex items-center justify-center">
|
|||
|
|
Kein Bild
|
|||
|
|
</div>
|
|||
|
|
)}
|
|||
|
|
</CardContent>
|
|||
|
|
</Card>
|
|||
|
|
|
|||
|
|
{/* Details */}
|
|||
|
|
<Card>
|
|||
|
|
<CardContent className="p-6">
|
|||
|
|
<h2 className="text-xl font-semibold mb-4">📋 Details</h2>
|
|||
|
|
|
|||
|
|
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 mb-6">
|
|||
|
|
<div>
|
|||
|
|
<p className="text-sm text-slate-500">Schlafzimmer</p>
|
|||
|
|
<p className="text-xl font-bold">{listing.bedrooms || "—"}</p>
|
|||
|
|
</div>
|
|||
|
|
<div>
|
|||
|
|
<p className="text-sm text-slate-500">Betten</p>
|
|||
|
|
<p className="text-xl font-bold">{listing.beds || "—"}</p>
|
|||
|
|
</div>
|
|||
|
|
<div>
|
|||
|
|
<p className="text-sm text-slate-500">Badezimmer</p>
|
|||
|
|
<p className="text-xl font-bold">{listing.bathrooms || "—"}</p>
|
|||
|
|
</div>
|
|||
|
|
<div>
|
|||
|
|
<p className="text-sm text-slate-500">Max Gäste</p>
|
|||
|
|
<p className="text-xl font-bold">{listing.maxSleepingPlaces || "—"}</p>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
{listing.description && (
|
|||
|
|
<div>
|
|||
|
|
<h3 className="font-medium mb-2">Beschreibung</h3>
|
|||
|
|
<p className="text-slate-600">{listing.description}</p>
|
|||
|
|
</div>
|
|||
|
|
)}
|
|||
|
|
</CardContent>
|
|||
|
|
</Card>
|
|||
|
|
|
|||
|
|
{/* Sleep Analysis */}
|
|||
|
|
<Card>
|
|||
|
|
<CardContent className="p-6">
|
|||
|
|
<h2 className="text-xl font-semibold mb-4">🛏️ Schlafplatz-Analyse</h2>
|
|||
|
|
|
|||
|
|
<div className="grid grid-cols-2 gap-4 mb-4">
|
|||
|
|
<div className="p-4 bg-slate-50 rounded">
|
|||
|
|
<p className="text-sm text-slate-500">4 Personen geeignet</p>
|
|||
|
|
<p className={`text-2xl font-bold ${listing.suitableFor4 ? "text-green-600" : "text-red-500"}`}>
|
|||
|
|
{listing.suitableFor4 ? "✅ Ja" : "❌ Nein"}
|
|||
|
|
</p>
|
|||
|
|
</div>
|
|||
|
|
<div className="p-4 bg-slate-50 rounded">
|
|||
|
|
<p className="text-sm text-slate-500">Extra Matratzen für 4</p>
|
|||
|
|
<p className={`text-2xl font-bold ${listing.extraMattressesNeededFor4 === 0 ? "text-green-600" : "text-amber-600"}`}>
|
|||
|
|
{listing.extraMattressesNeededFor4 ?? "—"}
|
|||
|
|
</p>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
{listing.sleepingOptions.length > 0 && (
|
|||
|
|
<div>
|
|||
|
|
<h3 className="font-medium mb-2">Schlafmöglichkeiten</h3>
|
|||
|
|
<div className="space-y-2">
|
|||
|
|
{listing.sleepingOptions.map((opt) => (
|
|||
|
|
<div key={opt.id} className="flex justify-between p-2 bg-slate-50 rounded">
|
|||
|
|
<span>{opt.label || opt.bedType}</span>
|
|||
|
|
<span className="font-medium">
|
|||
|
|
{opt.quantity}× ({opt.spotsPerUnit} Plätze)
|
|||
|
|
</span>
|
|||
|
|
</div>
|
|||
|
|
))}
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
)}
|
|||
|
|
</CardContent>
|
|||
|
|
</Card>
|
|||
|
|
|
|||
|
|
{/* Notes */}
|
|||
|
|
{listing.notes.length > 0 && (
|
|||
|
|
<Card>
|
|||
|
|
<CardContent className="p-6">
|
|||
|
|
<h2 className="text-xl font-semibold mb-4">📝 Notizen</h2>
|
|||
|
|
<div className="space-y-3">
|
|||
|
|
{listing.notes.map((note) => (
|
|||
|
|
<div key={note.id} className="p-3 bg-slate-50 rounded">
|
|||
|
|
<p className="text-sm">{note.body}</p>
|
|||
|
|
<p className="text-xs text-slate-400 mt-1">
|
|||
|
|
{new Date(note.createdAt).toLocaleDateString("de-DE")}
|
|||
|
|
</p>
|
|||
|
|
</div>
|
|||
|
|
))}
|
|||
|
|
</div>
|
|||
|
|
</CardContent>
|
|||
|
|
</Card>
|
|||
|
|
)}
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
{/* Sidebar */}
|
|||
|
|
<div className="space-y-6">
|
|||
|
|
{/* Price Card */}
|
|||
|
|
<Card>
|
|||
|
|
<CardContent className="p-6">
|
|||
|
|
<h1 className="text-2xl font-bold mb-2">{listing.title}</h1>
|
|||
|
|
<p className="text-slate-500 mb-4">📍 {listing.locationText || "Kein Ort"}</p>
|
|||
|
|
|
|||
|
|
<div className="flex items-baseline gap-2 mb-4">
|
|||
|
|
<span className="text-4xl font-bold">
|
|||
|
|
€{listing.nightlyPrice?.toFixed(2) || "—"}
|
|||
|
|
</span>
|
|||
|
|
<span className="text-slate-500">/ Nacht</span>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div className="flex items-center gap-2 mb-4">
|
|||
|
|
<span className="text-xl">⭐</span>
|
|||
|
|
<span className="font-semibold">{listing.rating?.toFixed(2) || "—"}</span>
|
|||
|
|
{listing.reviewCount && (
|
|||
|
|
<span className="text-slate-500">
|
|||
|
|
({listing.reviewCount} Bewertungen)
|
|||
|
|
</span>
|
|||
|
|
)}
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
{listing.hostName && (
|
|||
|
|
<p className="text-sm text-slate-500 mb-4">
|
|||
|
|
👤 Host: {listing.hostName}
|
|||
|
|
</p>
|
|||
|
|
)}
|
|||
|
|
|
|||
|
|
<div className="flex flex-wrap gap-2 mb-4">
|
|||
|
|
{listing.tags.map((lt) => (
|
|||
|
|
<Badge
|
|||
|
|
key={lt.tag.id}
|
|||
|
|
style={{ backgroundColor: lt.tag.color || "#6366f1" }}
|
|||
|
|
className="text-white"
|
|||
|
|
>
|
|||
|
|
{lt.tag.name}
|
|||
|
|
</Badge>
|
|||
|
|
))}
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<a
|
|||
|
|
href={listing.airbnbUrl}
|
|||
|
|
target="_blank"
|
|||
|
|
rel="noopener noreferrer"
|
|||
|
|
className="block w-full bg-red-500 text-white text-center py-3 rounded-lg hover:bg-red-600 transition"
|
|||
|
|
>
|
|||
|
|
🔗 Auf Airbnb ansehen
|
|||
|
|
</a>
|
|||
|
|
</CardContent>
|
|||
|
|
</Card>
|
|||
|
|
|
|||
|
|
{/* Status */}
|
|||
|
|
<Card>
|
|||
|
|
<CardContent className="p-6">
|
|||
|
|
<h3 className="font-semibold mb-2">Status</h3>
|
|||
|
|
<Badge
|
|||
|
|
className={
|
|||
|
|
listing.status === "SHORTLIST"
|
|||
|
|
? "bg-green-500"
|
|||
|
|
: listing.status === "REJECTED"
|
|||
|
|
? "bg-red-500"
|
|||
|
|
: "bg-slate-500"
|
|||
|
|
}
|
|||
|
|
>
|
|||
|
|
{listing.status}
|
|||
|
|
</Badge>
|
|||
|
|
</CardContent>
|
|||
|
|
</Card>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
);
|
|||
|
|
}
|