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>
169 lines
5.8 KiB
TypeScript
169 lines
5.8 KiB
TypeScript
import { getTranslations } from "next-intl/server";
|
||
import { houses, allApartments, getHouseById, bookingConfig } from "@/src/config/apartments";
|
||
import { getAvailability, getEffectiveMinStay } from "@/src/lib/beds24";
|
||
import { calculatePrice } from "@/src/lib/pricing";
|
||
import { nightsBetween, parseDateOnly } from "@/src/lib/date";
|
||
import { HouseCard } from "@/components/ui/HouseCard";
|
||
import { AvailabilitySearchBar } from "@/components/ui/AvailabilitySearchBar";
|
||
import { AvailableApartmentCard } from "@/components/ui/AvailableApartmentCard";
|
||
import { NoAvailabilityContact } from "@/components/ui/NoAvailabilityContact";
|
||
import Link from "next/link";
|
||
|
||
export async function generateMetadata({
|
||
params: { locale },
|
||
}: {
|
||
params: { locale: string };
|
||
}) {
|
||
const t = await getTranslations({ locale, namespace: "home" });
|
||
return {
|
||
title: t("houses.title"),
|
||
description: `${t("houses.title")} – ${t("houses.subtitle")}`,
|
||
};
|
||
}
|
||
|
||
export default async function HousesPage({
|
||
params: { locale },
|
||
searchParams,
|
||
}: {
|
||
params: { locale: string };
|
||
searchParams: { arrival?: string; departure?: string; months?: string };
|
||
}) {
|
||
const t = await getTranslations({ locale, namespace: "home" });
|
||
const tSearch = await getTranslations({ locale, namespace: "houses.search" });
|
||
|
||
const { arrival, departure } = searchParams;
|
||
const hasSearch = Boolean(arrival && departure);
|
||
|
||
if (hasSearch && arrival && departure) {
|
||
const nights = nightsBetween(arrival, departure);
|
||
|
||
const results = await Promise.all(
|
||
allApartments.map(async (apartment) => {
|
||
const house = getHouseById(apartment.houseId)!;
|
||
try {
|
||
const [availability, minStayNights] = await Promise.all([
|
||
getAvailability({
|
||
houseId: apartment.houseId,
|
||
propertyId: apartment.beds24PropertyId,
|
||
roomId: apartment.beds24RoomId,
|
||
start: arrival,
|
||
end: departure,
|
||
}),
|
||
getEffectiveMinStay(
|
||
apartment.houseId,
|
||
apartment.beds24PropertyId,
|
||
apartment.beds24RoomId,
|
||
arrival,
|
||
bookingConfig.minStayNights
|
||
),
|
||
]);
|
||
const minStayValid = nights > 0 && nights >= minStayNights;
|
||
const price = calculatePrice(availability.price, nights, availability.currency);
|
||
return {
|
||
apartment,
|
||
house,
|
||
available: availability.available,
|
||
minStayValid,
|
||
pricePerNight: price.basePricePerNight,
|
||
currency: price.currency,
|
||
};
|
||
} catch {
|
||
return {
|
||
apartment,
|
||
house,
|
||
available: false,
|
||
minStayValid: false,
|
||
pricePerNight: undefined,
|
||
currency: undefined,
|
||
};
|
||
}
|
||
})
|
||
);
|
||
|
||
results.sort((a, b) => {
|
||
const aOk = a.available && a.minStayValid;
|
||
const bOk = b.available && b.minStayValid;
|
||
return aOk === bOk ? 0 : aOk ? -1 : 1;
|
||
});
|
||
|
||
const anyAvailable = results.some((r) => r.available && r.minStayValid);
|
||
|
||
return (
|
||
<div className="py-16">
|
||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||
<div className="text-center mb-8">
|
||
<h1 className="text-4xl font-bold mb-4">{tSearch("resultsTitle")}</h1>
|
||
<p className="text-brand-grey text-lg">
|
||
{tSearch("resultsSubtitle")}: {parseDateOnly(arrival).toLocaleDateString(locale === "de" ? "de-DE" : "en-US")} –{" "}
|
||
{parseDateOnly(departure).toLocaleDateString(locale === "de" ? "de-DE" : "en-US")}
|
||
</p>
|
||
</div>
|
||
|
||
<AvailabilitySearchBar
|
||
locale={locale}
|
||
defaultArrival={arrival}
|
||
defaultMonths={searchParams.months ? Number(searchParams.months) : undefined}
|
||
/>
|
||
|
||
{!anyAvailable && (
|
||
<div className="mb-12">
|
||
<p className="text-center text-brand-grey mb-8">{tSearch("noResults")}</p>
|
||
<NoAvailabilityContact
|
||
locale={locale}
|
||
context={`${locale === "de" ? "Gewünschter Zeitraum" : "Requested dates"}: ${arrival} – ${departure}`}
|
||
/>
|
||
</div>
|
||
)}
|
||
|
||
{anyAvailable && (
|
||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-8 mb-12">
|
||
{results.map(({ apartment, house, available, minStayValid, pricePerNight, currency }) => (
|
||
<AvailableApartmentCard
|
||
key={apartment.id}
|
||
apartment={apartment}
|
||
house={house}
|
||
locale={locale}
|
||
available={available}
|
||
minStayValid={minStayValid}
|
||
pricePerNight={pricePerNight}
|
||
currency={currency}
|
||
arrival={arrival}
|
||
departure={departure}
|
||
/>
|
||
))}
|
||
</div>
|
||
)}
|
||
|
||
<div className="text-center">
|
||
<Link
|
||
href={`/${locale}/houses`}
|
||
className="text-brand-green-dark font-medium hover:text-brand-green"
|
||
>
|
||
{tSearch("showAllHouses")}
|
||
</Link>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
return (
|
||
<div className="py-16">
|
||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||
<div className="text-center mb-12">
|
||
<h1 className="text-4xl font-bold mb-4">{t("houses.title")}</h1>
|
||
<p className="text-brand-grey text-lg">{t("houses.subtitle")}</p>
|
||
</div>
|
||
|
||
<AvailabilitySearchBar locale={locale} />
|
||
|
||
<div className="grid grid-cols-1 md:grid-cols-2 gap-8">
|
||
{houses.map((house) => (
|
||
<HouseCard key={house.id} house={house} locale={locale} />
|
||
))}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|