Adds the complete Next.js website including i18n routing, house and apartment pages, Beds24 v2 integration for live availability/pricing, Stripe checkout, contact forms, Docker setup and deployment docs. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
103 lines
3.6 KiB
TypeScript
103 lines
3.6 KiB
TypeScript
import Link from "next/link";
|
|
import Image from "next/image";
|
|
import { Apartment, House } from "@/src/config/apartments";
|
|
import { formatPrice } from "@/src/lib/pricing";
|
|
import { CheckCircle2, XCircle, Users, MapPin, ArrowRight } from "lucide-react";
|
|
import { useTranslations } from "next-intl";
|
|
|
|
interface AvailableApartmentCardProps {
|
|
apartment: Apartment;
|
|
house: House;
|
|
locale: string;
|
|
available: boolean;
|
|
minStayValid: boolean;
|
|
pricePerNight?: number;
|
|
currency?: string;
|
|
arrival: string;
|
|
departure: string;
|
|
}
|
|
|
|
export function AvailableApartmentCard({
|
|
apartment,
|
|
house,
|
|
locale,
|
|
available,
|
|
minStayValid,
|
|
pricePerNight,
|
|
currency,
|
|
arrival,
|
|
departure,
|
|
}: AvailableApartmentCardProps) {
|
|
const t = useTranslations("houses.search");
|
|
const displayName = locale === "de" ? apartment.name : apartment.nameEn;
|
|
const houseName = locale === "de" ? house.name : house.nameEn;
|
|
const canBook = available && minStayValid;
|
|
|
|
return (
|
|
<div
|
|
className={`bg-white rounded-2xl overflow-hidden shadow-sm border transition-shadow ${
|
|
canBook
|
|
? "border-brand-grey-soft hover:shadow-lg"
|
|
: "border-brand-grey-soft opacity-70"
|
|
}`}
|
|
>
|
|
<div className="relative h-48">
|
|
<Image
|
|
src={apartment.images[0] || "/images/placeholder.jpg"}
|
|
alt={displayName}
|
|
fill
|
|
className={`object-cover ${canBook ? "" : "grayscale"}`}
|
|
/>
|
|
<div
|
|
className={`absolute top-3 right-3 flex items-center gap-1.5 text-xs font-semibold px-2.5 py-1.5 rounded-full backdrop-blur-sm ${
|
|
canBook ? "bg-brand-green/90 text-white" : "bg-white/90 text-brand-grey"
|
|
}`}
|
|
>
|
|
{canBook ? (
|
|
<CheckCircle2 className="w-3.5 h-3.5" />
|
|
) : (
|
|
<XCircle className="w-3.5 h-3.5" />
|
|
)}
|
|
{canBook ? t("available") : !minStayValid ? t("minStayNotMet") : t("notAvailable")}
|
|
</div>
|
|
</div>
|
|
<div className="p-5">
|
|
<h3 className="text-lg font-bold mb-1 text-brand-grey-dark">{displayName}</h3>
|
|
<div className="flex items-center gap-1.5 text-sm text-brand-grey mb-3">
|
|
<MapPin className="w-4 h-4 text-brand-green" />
|
|
<span>{houseName}</span>
|
|
</div>
|
|
<div className="flex items-center gap-2 text-sm text-brand-grey mb-1">
|
|
<Users className="w-4 h-4 text-brand-green" />
|
|
<span>
|
|
{apartment.maxGuests} {locale === "de" ? "Gäste" : "guests"}
|
|
</span>
|
|
</div>
|
|
{canBook && pricePerNight !== undefined && (
|
|
<p className="text-lg font-bold text-brand-green-dark mb-4">
|
|
{formatPrice(pricePerNight, currency)}{" "}
|
|
<span className="text-sm font-normal text-brand-grey">/ {t("perNight")}</span>
|
|
</p>
|
|
)}
|
|
|
|
{canBook ? (
|
|
<Link
|
|
href={`/${locale}/apartments/${apartment.slug}?checkin=${arrival}&checkout=${departure}`}
|
|
className="inline-flex items-center justify-center gap-2 w-full bg-brand-green text-white font-semibold px-4 py-3 rounded-xl hover:bg-brand-green-dark transition-colors"
|
|
>
|
|
{t("bookThis")}
|
|
<ArrowRight className="w-4 h-4" />
|
|
</Link>
|
|
) : (
|
|
<Link
|
|
href={`/${locale}/apartments/${apartment.slug}`}
|
|
className="inline-flex items-center justify-center gap-2 w-full bg-brand-grey-soft text-brand-grey-dark font-semibold px-4 py-3 rounded-xl hover:bg-brand-grey-soft/70 transition-colors"
|
|
>
|
|
{locale === "de" ? "Details ansehen" : "View details"}
|
|
</Link>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|