Files
MarkusandDevin <158243242+devin-ai-integration[bot]@users.noreply.github.com> 2b24334bfa Initial upload of BB-Apartment 2026 website.
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>
2026-08-19 22:54:49 +02:00

169 lines
5.8 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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>
);
}