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>
This commit is contained in:
co-authored by
Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
parent
232770afe7
commit
2b24334bfa
@@ -0,0 +1,209 @@
|
||||
import { notFound } from "next/navigation";
|
||||
import { getTranslations } from "next-intl/server";
|
||||
import { getApartmentBySlug, getHouseById, bookingConfig } from "@/src/config/apartments";
|
||||
import { getEffectiveMinStay } from "@/src/lib/beds24";
|
||||
import { tomorrowDateString } from "@/src/lib/date";
|
||||
import { Bed, Bath, Users, Calendar, Check, Wind, Sofa, Wifi, WashingMachine, Tv } from "lucide-react";
|
||||
import { BookingForm } from "@/components/ui/BookingForm";
|
||||
import { ImageGallery } from "@/components/ui/ImageGallery";
|
||||
import Link from "next/link";
|
||||
|
||||
export async function generateMetadata({
|
||||
params: { locale, apartmentSlug },
|
||||
}: {
|
||||
params: { locale: string; apartmentSlug: string };
|
||||
}) {
|
||||
const apartment = getApartmentBySlug(apartmentSlug);
|
||||
if (!apartment) return { title: "Not Found" };
|
||||
|
||||
const t = await getTranslations({ locale, namespace: "metadata" });
|
||||
return {
|
||||
title: `${apartment.nameEn} - ${t("title")}`,
|
||||
description: apartment.descriptionEn || t("description"),
|
||||
};
|
||||
}
|
||||
|
||||
export default async function ApartmentPage({
|
||||
params: { locale, apartmentSlug },
|
||||
}: {
|
||||
params: { locale: string; apartmentSlug: string };
|
||||
}) {
|
||||
const t = await getTranslations({ locale, namespace: "apartment" });
|
||||
const apartment = getApartmentBySlug(apartmentSlug);
|
||||
const house = apartment ? getHouseById(apartment.houseId) : undefined;
|
||||
|
||||
if (!apartment || !house) notFound();
|
||||
|
||||
// Real, effective minimum stay for this apartment as of "tomorrow" — this
|
||||
// combines Beds24's room-level minimum with whatever the currently active
|
||||
// rate/price plan requires (the higher of the two wins), so it reflects
|
||||
// what a guest could actually book, not just the room's base setting.
|
||||
// The BookingForm re-fetches this whenever the guest picks a different
|
||||
// check-in date, since it can vary by season.
|
||||
const minStayNights = await getEffectiveMinStay(
|
||||
apartment.houseId,
|
||||
apartment.beds24PropertyId,
|
||||
apartment.beds24RoomId,
|
||||
tomorrowDateString(),
|
||||
bookingConfig.minStayNights
|
||||
);
|
||||
|
||||
const displayName = locale === "de" ? apartment.name : apartment.nameEn;
|
||||
const displayDescription =
|
||||
locale === "de" ? apartment.description : apartment.descriptionEn;
|
||||
const displayFloor = locale === "de" ? apartment.floor : apartment.floorEn;
|
||||
const amenities =
|
||||
locale === "de" ? apartment.amenities : apartment.amenitiesEn;
|
||||
const houseName = locale === "de" ? house.name : house.nameEn;
|
||||
|
||||
const hasAmenity = (keys: string[]) =>
|
||||
amenities.some((a) => keys.some((k) => a.toLowerCase().includes(k.toLowerCase())));
|
||||
|
||||
// Apartment-specific highlights (excludes "Fully furnished", already shown elsewhere)
|
||||
const specificHighlights = [
|
||||
hasAmenity(["Klimaanlage", "Air conditioning"]) && {
|
||||
icon: Wind,
|
||||
title: locale === "de" ? "Klimaanlage" : "Air conditioning",
|
||||
},
|
||||
hasAmenity(["Waschmaschine", "Washing machine"]) && {
|
||||
icon: WashingMachine,
|
||||
title: locale === "de" ? "Eigene Waschmaschine" : "In-unit Washer",
|
||||
},
|
||||
hasAmenity(["WLAN", "WiFi"]) && {
|
||||
icon: Wifi,
|
||||
title: locale === "de" ? "High-Speed WLAN" : "High-Speed WiFi",
|
||||
},
|
||||
hasAmenity(["TV"]) && {
|
||||
icon: Tv,
|
||||
title: "Smart TV",
|
||||
},
|
||||
].filter(Boolean) as { icon: typeof Wind; title: string }[];
|
||||
|
||||
return (
|
||||
<div className="py-16">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div className="mb-6">
|
||||
<Link
|
||||
href={`/${locale}/houses/${house.slug}`}
|
||||
className="text-brand-green-dark hover:text-brand-green"
|
||||
>
|
||||
← {houseName}
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-12 mb-12">
|
||||
<ImageGallery
|
||||
images={apartment.images.length > 0 ? apartment.images : ["/images/placeholder.jpg"]}
|
||||
alt={displayName}
|
||||
/>
|
||||
<div>
|
||||
<h1 className="text-4xl font-bold mb-2">{displayName}</h1>
|
||||
<p className="text-brand-grey mb-6">{houseName} • {displayFloor}</p>
|
||||
|
||||
{displayDescription && (
|
||||
<p className="text-brand-grey leading-relaxed mb-6">
|
||||
{displayDescription}
|
||||
</p>
|
||||
)}
|
||||
|
||||
<div className="grid grid-cols-2 gap-4 mb-8">
|
||||
<div className="flex items-center gap-3 p-4 bg-brand-grey-soft rounded-xl">
|
||||
<Users className="w-5 h-5 text-brand-green" />
|
||||
<div>
|
||||
<p className="text-sm text-brand-grey">{t("guests")}</p>
|
||||
<p className="font-semibold">{apartment.maxGuests}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-3 p-4 bg-brand-grey-soft rounded-xl">
|
||||
<Calendar className="w-5 h-5 text-brand-green" />
|
||||
<div>
|
||||
<p className="text-sm text-brand-grey">{t("minStay")}</p>
|
||||
<p className="font-semibold">{minStayNights} {t("nights")}</p>
|
||||
</div>
|
||||
</div>
|
||||
{amenities.some((a) =>
|
||||
["Klimaanlage", "Air conditioning"].includes(a)
|
||||
) && (
|
||||
<div className="flex items-center gap-3 p-4 bg-brand-green/10 rounded-xl border border-brand-green/20 col-span-2 sm:col-span-1">
|
||||
<Wind className="w-5 h-5 text-brand-green" />
|
||||
<div>
|
||||
<p className="text-sm text-brand-grey">
|
||||
{locale === "de" ? "Klimaanlage" : "Air conditioning"}
|
||||
</p>
|
||||
<p className="font-semibold text-brand-green-dark">
|
||||
{locale === "de" ? "Vorhanden" : "Available"}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex items-center gap-3 p-4 bg-brand-green/10 rounded-xl border border-brand-green/20">
|
||||
<Sofa className="w-5 h-5 text-brand-green" />
|
||||
<div>
|
||||
<p className="text-sm text-brand-grey">
|
||||
{locale === "de" ? "Vollmöbliert" : "Fully furnished"}
|
||||
</p>
|
||||
<p className="font-semibold text-brand-green-dark">
|
||||
{locale === "de" ? "Ja" : "Yes"}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{apartment.bedrooms && (
|
||||
<div className="flex items-center gap-3 p-4 bg-brand-grey-soft rounded-xl">
|
||||
<Bed className="w-5 h-5 text-brand-green" />
|
||||
<div>
|
||||
<p className="text-sm text-brand-grey">{locale === "de" ? "Schlafzimmer" : "Bedrooms"}</p>
|
||||
<p className="font-semibold">{apartment.bedrooms}</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{apartment.bathrooms && (
|
||||
<div className="flex items-center gap-3 p-4 bg-brand-grey-soft rounded-xl">
|
||||
<Bath className="w-5 h-5 text-brand-green" />
|
||||
<div>
|
||||
<p className="text-sm text-brand-grey">{locale === "de" ? "Bäder" : "Bathrooms"}</p>
|
||||
<p className="font-semibold">{apartment.bathrooms}</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<BookingForm apartment={apartment} locale={locale} minStayNights={minStayNights} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{specificHighlights.length > 0 && (
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-5 mb-12">
|
||||
{specificHighlights.map((highlight, index) => {
|
||||
const Icon = highlight.icon;
|
||||
return (
|
||||
<div
|
||||
key={index}
|
||||
className="flex flex-col items-center text-center gap-4 p-6 bg-white rounded-2xl border-2 border-brand-green/20 shadow-md hover:shadow-xl hover:-translate-y-1 transition-all"
|
||||
>
|
||||
<div className="p-4 bg-brand-green rounded-2xl text-white shadow-sm">
|
||||
<Icon className="w-9 h-9" />
|
||||
</div>
|
||||
<p className="font-bold text-brand-grey-dark text-lg leading-tight">
|
||||
{highlight.title}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="bg-brand-grey-soft rounded-2xl p-8">
|
||||
<h2 className="text-2xl font-bold mb-6">{t("features")}</h2>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-4">
|
||||
{amenities.map((amenity, index) => (
|
||||
<div key={index} className="flex items-center gap-3">
|
||||
<Check className="w-5 h-5 text-brand-green flex-shrink-0" />
|
||||
<span className="text-brand-grey-dark">{amenity}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
import { getTranslations } from "next-intl/server";
|
||||
import { CheckCircle } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
|
||||
export async function generateMetadata({
|
||||
params: { locale },
|
||||
}: {
|
||||
params: { locale: string };
|
||||
}) {
|
||||
const t = await getTranslations({ locale, namespace: "metadata" });
|
||||
return {
|
||||
title: `${t("title")} - Buchung erfolgreich`,
|
||||
description: t("description"),
|
||||
};
|
||||
}
|
||||
|
||||
export default function BookingSuccessPage({
|
||||
params: { locale },
|
||||
}: {
|
||||
params: { locale: string };
|
||||
}) {
|
||||
const isDe = locale === "de";
|
||||
|
||||
return (
|
||||
<div className="py-16">
|
||||
<div className="max-w-2xl mx-auto px-4 text-center">
|
||||
<CheckCircle className="w-16 h-16 text-brand-green mx-auto mb-6" />
|
||||
<h1 className="text-4xl font-bold mb-4">
|
||||
{isDe ? "Anzahlung erfolgreich!" : "Deposit successful!"}
|
||||
</h1>
|
||||
<p className="text-brand-grey text-lg mb-8">
|
||||
{isDe
|
||||
? "Vielen Dank für Ihre Buchung. Sie erhalten in Kürze eine Bestätigungs-E-Mail. Die monatlichen Rechnungen werden Ihnen über das Kundenportal zur Verfügung gestellt."
|
||||
: "Thank you for your booking. You will receive a confirmation email shortly. Monthly invoices will be available through the customer portal."}
|
||||
</p>
|
||||
<Link
|
||||
href={`/${locale}`}
|
||||
className="inline-block bg-brand-green text-white font-semibold px-8 py-4 rounded-full hover:bg-brand-green-dark transition-colors"
|
||||
>
|
||||
{isDe ? "Zurück zur Startseite" : "Back to home"}
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
import { useTranslations } from "next-intl";
|
||||
import { getTranslations } from "next-intl/server";
|
||||
import { Mail, Phone, MapPin } from "lucide-react";
|
||||
|
||||
export async function generateMetadata({
|
||||
params: { locale },
|
||||
}: {
|
||||
params: { locale: string };
|
||||
}) {
|
||||
const t = await getTranslations({ locale, namespace: "contact" });
|
||||
return {
|
||||
title: t("title"),
|
||||
description: t("description"),
|
||||
};
|
||||
}
|
||||
|
||||
export default function ContactPage({
|
||||
params: { locale },
|
||||
}: {
|
||||
params: { locale: string };
|
||||
}) {
|
||||
const t = useTranslations("contact");
|
||||
|
||||
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("title")}</h1>
|
||||
<p className="text-brand-grey text-lg max-w-2xl mx-auto">
|
||||
{t("description")}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-12">
|
||||
<div className="bg-brand-grey-soft rounded-2xl p-8">
|
||||
<h2 className="text-2xl font-bold mb-6">
|
||||
{locale === "de" ? "Kontaktdaten" : "Contact Details"}
|
||||
</h2>
|
||||
<div className="space-y-6">
|
||||
<div className="flex items-start gap-4">
|
||||
<Mail className="w-6 h-6 text-brand-green mt-1" />
|
||||
<div>
|
||||
<p className="font-semibold">{t("email")}</p>
|
||||
<a href="mailto:info@bb-apartment.de" className="text-brand-grey">
|
||||
info@bb-apartment.de
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-start gap-4">
|
||||
<Phone className="w-6 h-6 text-brand-green mt-1" />
|
||||
<div>
|
||||
<p className="font-semibold">{t("phone")}</p>
|
||||
<p className="text-brand-grey">+49 (0) 7031 7631132</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-start gap-4">
|
||||
<MapPin className="w-6 h-6 text-brand-green mt-1" />
|
||||
<div>
|
||||
<p className="font-semibold">{locale === "de" ? "Adresse" : "Address"}</p>
|
||||
<p className="text-brand-grey">
|
||||
BB-Apartment<br />
|
||||
Böblingen<br />
|
||||
Deutschland
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-white rounded-2xl p-8 border border-brand-grey-soft shadow-sm">
|
||||
<h2 className="text-2xl font-bold mb-6">
|
||||
{locale === "de" ? "Nachricht senden" : "Send a message"}
|
||||
</h2>
|
||||
<form className="space-y-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-brand-grey-dark mb-1">
|
||||
{t("name")}
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
className="w-full px-4 py-3 rounded-xl border border-brand-grey-soft focus:border-brand-green focus:ring-1 focus:ring-brand-green outline-none"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-brand-grey-dark mb-1">
|
||||
{t("email")}
|
||||
</label>
|
||||
<input
|
||||
type="email"
|
||||
className="w-full px-4 py-3 rounded-xl border border-brand-grey-soft focus:border-brand-green focus:ring-1 focus:ring-brand-green outline-none"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-brand-grey-dark mb-1">
|
||||
{t("message")}
|
||||
</label>
|
||||
<textarea
|
||||
rows={5}
|
||||
className="w-full px-4 py-3 rounded-xl border border-brand-grey-soft focus:border-brand-green focus:ring-1 focus:ring-brand-green outline-none"
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
type="submit"
|
||||
className="w-full bg-brand-green text-white font-semibold px-8 py-4 rounded-full hover:bg-brand-green-dark transition-colors"
|
||||
>
|
||||
{t("send")}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -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(error);
|
||||
}, [error]);
|
||||
|
||||
return (
|
||||
<div className="min-h-screen flex flex-col items-center justify-center px-4 text-center">
|
||||
<h2 className="text-2xl font-bold text-brand-grey-dark mb-4">
|
||||
Etwas ist schiefgelaufen.
|
||||
</h2>
|
||||
<p className="text-brand-grey mb-8 max-w-md">
|
||||
{error.message ||
|
||||
"Beim Laden der Seite ist ein Fehler aufgetreten. Bitte versuche es erneut."}
|
||||
</p>
|
||||
<button
|
||||
onClick={reset}
|
||||
className="px-6 py-3 bg-brand-green text-white rounded-xl font-medium hover:bg-brand-green-dark transition-colors"
|
||||
>
|
||||
Erneut versuchen
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,206 @@
|
||||
import { notFound } from "next/navigation";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { getTranslations } from "next-intl/server";
|
||||
import { getHouseBySlug } from "@/src/config/apartments";
|
||||
import { ApartmentCard } from "@/components/ui/ApartmentCard";
|
||||
import { HouseHighlights } from "@/components/ui/HouseHighlights";
|
||||
import { MapPin, Home, Car, Clock, Navigation } from "lucide-react";
|
||||
import Image from "next/image";
|
||||
|
||||
export async function generateMetadata({
|
||||
params: { locale, houseSlug },
|
||||
}: {
|
||||
params: { locale: string; houseSlug: string };
|
||||
}) {
|
||||
const house = getHouseBySlug(houseSlug);
|
||||
if (!house) return { title: "Not Found" };
|
||||
|
||||
const t = await getTranslations({ locale, namespace: "metadata" });
|
||||
return {
|
||||
title: `${house.nameEn} - ${t("title")}`,
|
||||
description: house.descriptionEn,
|
||||
};
|
||||
}
|
||||
|
||||
export default function HousePage({
|
||||
params: { locale, houseSlug },
|
||||
}: {
|
||||
params: { locale: string; houseSlug: string };
|
||||
}) {
|
||||
const t = useTranslations("houses");
|
||||
const house = getHouseBySlug(houseSlug);
|
||||
|
||||
if (!house) notFound();
|
||||
|
||||
const displayName = locale === "de" ? house.name : house.nameEn;
|
||||
const displayDescription =
|
||||
locale === "de" ? house.description : house.descriptionEn;
|
||||
const displayCountry = locale === "de" ? house.country : house.countryEn;
|
||||
|
||||
return (
|
||||
<div className="py-16">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div className="relative h-80 md:h-96 rounded-2xl overflow-hidden mb-12">
|
||||
<Image
|
||||
src={house.images[0] || "/images/placeholder.jpg"}
|
||||
alt={displayName}
|
||||
fill
|
||||
className="object-cover"
|
||||
/>
|
||||
<div className="absolute inset-0 bg-gradient-to-t from-black/60 to-transparent" />
|
||||
<div className="absolute bottom-0 left-0 p-8 text-white">
|
||||
<h1 className="text-4xl font-bold mb-2 text-white">{displayName}</h1>
|
||||
<div className="flex items-center gap-2 text-white/90">
|
||||
<MapPin className="w-5 h-5" />
|
||||
<span>
|
||||
{house.address}, {house.zipCode} {house.city}, {displayCountry}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-12">
|
||||
<div className="lg:col-span-2">
|
||||
<h2 className="text-2xl font-bold mb-4">
|
||||
{t("overview")}
|
||||
</h2>
|
||||
<p className="text-brand-grey leading-relaxed mb-8">
|
||||
{displayDescription}
|
||||
</p>
|
||||
|
||||
<HouseHighlights house={house} locale={locale} />
|
||||
|
||||
<h2 className="text-2xl font-bold mb-6">
|
||||
{t("apartments")}
|
||||
</h2>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
{house.apartments.map((apartment) => (
|
||||
<ApartmentCard
|
||||
key={apartment.id}
|
||||
apartment={apartment}
|
||||
locale={locale}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-8 h-fit">
|
||||
<div className="bg-brand-grey-soft rounded-2xl p-8 h-fit">
|
||||
<div className="flex items-center gap-3 mb-4">
|
||||
<Home className="w-6 h-6 text-brand-green" />
|
||||
<h3 className="text-xl font-bold">{displayName}</h3>
|
||||
</div>
|
||||
<p className="text-brand-grey text-sm mb-4">
|
||||
{house.apartments.length} {locale === "de" ? "Apartments" : "Apartments"}
|
||||
</p>
|
||||
<div className="text-sm text-brand-grey">
|
||||
<p className="mb-2">
|
||||
<strong>{locale === "de" ? "Adresse:" : "Address:"}</strong> {house.address}
|
||||
</p>
|
||||
<p>
|
||||
<strong>{locale === "de" ? "Stadt:" : "City:"}</strong> {house.zipCode} {house.city}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-white rounded-2xl border border-brand-grey-soft shadow-sm p-6 h-fit">
|
||||
<div className="flex items-center gap-2 mb-4">
|
||||
<Navigation className="w-5 h-5 text-brand-green" />
|
||||
<h2 className="text-lg font-bold">
|
||||
{locale === "de" ? "Lage & Entfernungen" : "Location & Distances"}
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-3 mb-4">
|
||||
{house.id === "herdweg" ? (
|
||||
<>
|
||||
<a
|
||||
href="https://www.google.com/maps/dir/Herdweg+71,+71032+Böblingen/Panzer+Kaserne+Böblingen"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="flex items-center gap-3 p-3 bg-brand-green text-white rounded-xl hover:bg-brand-green-dark transition-colors"
|
||||
>
|
||||
<Car className="w-5 h-5 shrink-0" />
|
||||
<div>
|
||||
<p className="font-bold text-sm">
|
||||
{locale === "de" ? "Panzer Kaserne – gleiche Straße" : "Panzer Kaserne – Same Street"}
|
||||
</p>
|
||||
<p className="text-sm text-white/85">
|
||||
{locale === "de" ? "Kein Abbiegen nötig" : "Zero turns needed"}
|
||||
</p>
|
||||
</div>
|
||||
</a>
|
||||
<a
|
||||
href="https://www.google.com/maps/dir/Herdweg+71,+71032+Böblingen/Böblingen+Zentrum"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="flex items-center gap-3 p-3 bg-brand-green/10 rounded-xl hover:bg-brand-green/20 transition-colors"
|
||||
>
|
||||
<Clock className="w-5 h-5 text-brand-green shrink-0" />
|
||||
<div>
|
||||
<p className="font-bold text-brand-grey-dark text-sm">
|
||||
{locale === "de" ? "Stadtzentrum Böblingen" : "Böblingen City Center"}
|
||||
</p>
|
||||
<p className="text-sm text-brand-grey">5–8 min</p>
|
||||
</div>
|
||||
</a>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<a
|
||||
href="https://www.google.com/maps/dir/Schönaicher+Straße+26,+71032+Böblingen/Daimler+Werk+Sindelfingen"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="flex items-center gap-3 p-3 bg-brand-green/10 rounded-xl hover:bg-brand-green/20 transition-colors"
|
||||
>
|
||||
<Car className="w-5 h-5 text-brand-green shrink-0" />
|
||||
<div>
|
||||
<p className="font-bold text-brand-grey-dark text-sm">
|
||||
{locale === "de" ? "Daimler Werk Sindelfingen" : "Daimler Plant Sindelfingen"}
|
||||
</p>
|
||||
<p className="text-sm text-brand-grey">10–15 min</p>
|
||||
</div>
|
||||
</a>
|
||||
<a
|
||||
href="https://www.google.com/maps/dir/Schönaicher+Straße+26,+71032+Böblingen/Böblingen+Zentrum"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="flex items-center gap-3 p-3 bg-brand-green/10 rounded-xl hover:bg-brand-green/20 transition-colors"
|
||||
>
|
||||
<Clock className="w-5 h-5 text-brand-green shrink-0" />
|
||||
<div>
|
||||
<p className="font-bold text-brand-grey-dark text-sm">
|
||||
{locale === "de" ? "Stadtzentrum Böblingen" : "Böblingen City Center"}
|
||||
</p>
|
||||
<p className="text-sm text-brand-grey">< 5 min</p>
|
||||
</div>
|
||||
</a>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="relative w-full h-48 rounded-xl overflow-hidden border border-brand-grey-soft">
|
||||
<iframe
|
||||
src={`https://maps.google.com/maps?q=${encodeURIComponent(`${house.address}, ${house.zipCode} ${house.city}`)}&z=15&output=embed`}
|
||||
width="100%"
|
||||
height="100%"
|
||||
style={{ border: 0 }}
|
||||
allowFullScreen
|
||||
loading="lazy"
|
||||
referrerPolicy="no-referrer-when-downgrade"
|
||||
title={displayName}
|
||||
className="grayscale-[20%] hover:grayscale-0 transition-all duration-500"
|
||||
/>
|
||||
</div>
|
||||
<p className="text-xs text-brand-grey mt-3 text-center">
|
||||
{locale === "de"
|
||||
? "Karte ist interaktiv – klicken Sie auf einen Ort, um die Route zu planen."
|
||||
: "Map is interactive – click a destination to plan the route."}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,168 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
import { getTranslations } from "next-intl/server";
|
||||
|
||||
export async function generateMetadata({
|
||||
params: { locale },
|
||||
}: {
|
||||
params: { locale: string };
|
||||
}) {
|
||||
const t = await getTranslations({ locale, namespace: "footer" });
|
||||
return {
|
||||
title: t("imprint"),
|
||||
};
|
||||
}
|
||||
|
||||
export default function ImprintPage({
|
||||
params: { locale },
|
||||
}: {
|
||||
params: { locale: string };
|
||||
}) {
|
||||
const isDe = locale === "de";
|
||||
|
||||
return (
|
||||
<div className="py-16">
|
||||
<div className="max-w-3xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<h1 className="text-4xl font-bold mb-8">
|
||||
{isDe ? "Impressum" : "Imprint"}
|
||||
</h1>
|
||||
<div className="prose prose-lg max-w-none">
|
||||
<p className="text-brand-grey mb-8">
|
||||
{isDe
|
||||
? "Angaben gemäß § 5 TMG"
|
||||
: "Information according to § 5 TMG"}
|
||||
</p>
|
||||
|
||||
<h2 className="text-2xl font-bold mb-4">
|
||||
{isDe ? "Angaben zum Anbieter" : "Provider Information"}
|
||||
</h2>
|
||||
<p className="text-brand-grey mb-6">
|
||||
<strong>
|
||||
{isDe ? "Grundstücksgemeinschaft Allami / Allawy" : "Allami / Allawy Property Community"}
|
||||
</strong>
|
||||
<br />
|
||||
Forchenweg 1
|
||||
<br />
|
||||
71063 Sindelfingen
|
||||
<br />
|
||||
{isDe ? "Deutschland" : "Germany"}
|
||||
</p>
|
||||
|
||||
<h2 className="text-2xl font-bold mb-4">
|
||||
{isDe ? "Vertreten durch" : "Represented by"}
|
||||
</h2>
|
||||
<p className="text-brand-grey mb-6">
|
||||
Ali Allawy
|
||||
</p>
|
||||
|
||||
<h2 className="text-2xl font-bold mb-4">
|
||||
{isDe ? "Kontakt" : "Contact"}
|
||||
</h2>
|
||||
<p className="text-brand-grey mb-6">
|
||||
E-Mail: info@bb-apartment.de
|
||||
<br />
|
||||
{isDe ? "Telefon:" : "Phone:"} 07031 7631132
|
||||
<br />
|
||||
{isDe ? "Web:" : "Web:"} www.BB-Apartment.de
|
||||
</p>
|
||||
|
||||
<h2 className="text-2xl font-bold mb-4">
|
||||
{isDe ? "Verantwortlich für den Inhalt" : "Responsible for Content"}
|
||||
</h2>
|
||||
<p className="text-brand-grey mb-6">
|
||||
Ali Allawy
|
||||
<br />
|
||||
Forchenweg 1
|
||||
<br />
|
||||
71063 Sindelfingen
|
||||
</p>
|
||||
|
||||
<h2 className="text-2xl font-bold mb-4">
|
||||
{isDe ? "Hinweis" : "Note"}
|
||||
</h2>
|
||||
<p className="text-brand-grey mb-6">
|
||||
{isDe
|
||||
? "EU-Streitschlichtung: Die Europäische Kommission stellt eine Plattform zur Online-Streitbeilegung (OS) bereit. Unsere E-Mail-Adresse finden Sie oben im Impressum. Wir sind nicht verpflichtet, an Streitbeilegungsverfahren vor einer Verbraucherschlichtungsstelle teilzunehmen."
|
||||
: "EU Dispute Resolution: The European Commission provides a platform for online dispute resolution (ODR). Our email address can be found above in the imprint. We are not obligated to participate in dispute resolution proceedings before a consumer arbitration board."}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
import { ReactNode } from "react";
|
||||
import { NextIntlClientProvider } from "next-intl";
|
||||
import { getMessages, getTranslations } from "next-intl/server";
|
||||
import { notFound } from "next/navigation";
|
||||
import { locales, Locale } from "@/i18n";
|
||||
import "../globals.css";
|
||||
import { Header } from "@/components/layout/Header";
|
||||
import { Footer } from "@/components/layout/Footer";
|
||||
import localFont from "next/font/local";
|
||||
|
||||
export async function generateMetadata({
|
||||
params: { locale },
|
||||
}: {
|
||||
params: { locale: string };
|
||||
}) {
|
||||
const t = await getTranslations({ locale, namespace: "metadata" });
|
||||
|
||||
return {
|
||||
metadataBase: new URL("https://bb-apartment.de"),
|
||||
title: {
|
||||
template: `%s | ${t("title")}`,
|
||||
default: t("title"),
|
||||
},
|
||||
description: t("description"),
|
||||
alternates: {
|
||||
canonical: `/${locale}`,
|
||||
languages: {
|
||||
"de-DE": "/de",
|
||||
"en-US": "/en",
|
||||
"x-default": "/de",
|
||||
},
|
||||
},
|
||||
openGraph: {
|
||||
type: "website",
|
||||
locale: locale === "de" ? "de_DE" : "en_US",
|
||||
url: `https://bb-apartment.de/${locale}`,
|
||||
siteName: "BB-Apartment",
|
||||
title: t("title"),
|
||||
description: t("description"),
|
||||
},
|
||||
twitter: {
|
||||
card: "summary_large_image",
|
||||
title: t("title"),
|
||||
description: t("description"),
|
||||
},
|
||||
robots: {
|
||||
index: true,
|
||||
follow: true,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const geistSans = localFont({
|
||||
src: "../fonts/GeistVF.woff",
|
||||
variable: "--font-geist-sans",
|
||||
weight: "100 900",
|
||||
});
|
||||
|
||||
export default async function LocaleLayout({
|
||||
children,
|
||||
params: { locale },
|
||||
}: {
|
||||
children: ReactNode;
|
||||
params: { locale: string };
|
||||
}) {
|
||||
if (!locales.includes(locale as Locale)) notFound();
|
||||
|
||||
const messages = await getMessages();
|
||||
|
||||
return (
|
||||
<html lang={locale} className={geistSans.variable}>
|
||||
<body className="antialiased min-h-screen flex flex-col bg-white text-brand-grey-dark">
|
||||
<NextIntlClientProvider messages={messages} locale={locale}>
|
||||
<Header locale={locale} />
|
||||
<main className="flex-grow pt-20">{children}</main>
|
||||
<Footer />
|
||||
</NextIntlClientProvider>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
import { useTranslations } from "next-intl";
|
||||
import { getTranslations } from "next-intl/server";
|
||||
import { houses } from "@/src/config/apartments";
|
||||
import { Hero } from "@/components/ui/Hero";
|
||||
import { HouseCard } from "@/components/ui/HouseCard";
|
||||
import { BookingBanner } from "@/components/ui/BookingBanner";
|
||||
import { Impressions } from "@/components/ui/Impressions";
|
||||
import { ArrowRight, MapPin, Shield, Clock, Wind, Home, CreditCard } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
|
||||
export async function generateMetadata({
|
||||
params: { locale },
|
||||
}: {
|
||||
params: { locale: string };
|
||||
}) {
|
||||
const t = await getTranslations({ locale, namespace: "metadata" });
|
||||
return {
|
||||
title: t("title"),
|
||||
description: t("description"),
|
||||
};
|
||||
}
|
||||
|
||||
export default function HomePage({
|
||||
params: { locale },
|
||||
}: {
|
||||
params: { locale: string };
|
||||
}) {
|
||||
const t = useTranslations("home");
|
||||
|
||||
const features = [
|
||||
{
|
||||
icon: Clock,
|
||||
text: t("features.longStay"),
|
||||
},
|
||||
{
|
||||
icon: MapPin,
|
||||
text: t("features.location"),
|
||||
},
|
||||
{
|
||||
icon: Wind,
|
||||
text: t("features.airConditioning"),
|
||||
},
|
||||
{
|
||||
icon: Home,
|
||||
text: t("features.furnished"),
|
||||
},
|
||||
{
|
||||
icon: CreditCard,
|
||||
text: t("features.payment"),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Hero
|
||||
title={t("hero.title")}
|
||||
subtitle={t("hero.subtitle")}
|
||||
description={t("hero.description")}
|
||||
cta={t("hero.cta")}
|
||||
ctaHref={`/${locale}/houses`}
|
||||
/>
|
||||
|
||||
<BookingBanner locale={locale} />
|
||||
|
||||
<section className="py-20 bg-brand-grey-soft/50">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div className="text-center mb-16">
|
||||
<p className="text-brand-green font-semibold mb-2 uppercase tracking-wider text-sm">{t("houses.subtitle")}</p>
|
||||
<h2 className="text-4xl font-bold mb-4 text-brand-grey-dark">{t("houses.title")}</h2>
|
||||
<div className="w-20 h-1 bg-brand-green mx-auto rounded-full" />
|
||||
</div>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-10">
|
||||
{houses.map((house) => (
|
||||
<HouseCard key={house.id} house={house} locale={locale} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<Impressions
|
||||
title={t("impressions.title")}
|
||||
subtitle={t("impressions.subtitle")}
|
||||
/>
|
||||
|
||||
<section className="py-20">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div className="text-center mb-16">
|
||||
<p className="text-brand-green font-semibold mb-2 uppercase tracking-wider text-sm">{t("features.title")}</p>
|
||||
<h2 className="text-4xl font-bold mb-4 text-brand-grey-dark">{t("features.title")}</h2>
|
||||
<div className="w-20 h-1 bg-brand-green mx-auto rounded-full" />
|
||||
</div>
|
||||
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-5 gap-6">
|
||||
{features.map((feature, index) => {
|
||||
const Icon = feature.icon;
|
||||
return (
|
||||
<div
|
||||
key={index}
|
||||
className="group p-6 md:p-8 bg-white rounded-2xl shadow-md border-2 border-brand-green/20 hover:bg-brand-green hover:border-brand-green hover:-translate-y-2 transition-all duration-300 flex flex-col items-center text-center"
|
||||
>
|
||||
<div className="p-4 bg-brand-green/10 rounded-2xl mb-5 group-hover:bg-white/20 transition-colors">
|
||||
<Icon className="w-8 h-8 text-brand-green group-hover:text-white transition-colors" />
|
||||
</div>
|
||||
<p className="text-brand-grey-dark font-bold text-lg leading-snug group-hover:text-white transition-colors">{feature.text}</p>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="relative py-24 overflow-hidden">
|
||||
<div className="absolute inset-0 bg-brand-green" />
|
||||
<div className="absolute inset-0 bg-[url('/images/cta-bg.jpg')] opacity-10 bg-cover bg-center" />
|
||||
<div className="relative z-10 max-w-4xl mx-auto px-4 text-center">
|
||||
<Shield className="w-12 h-12 text-white/80 mx-auto mb-6" />
|
||||
<h2 className="text-4xl font-bold mb-6 text-white">
|
||||
{t("ctaSection.title")}
|
||||
</h2>
|
||||
<p className="text-xl mb-10 text-white/90 max-w-2xl mx-auto">
|
||||
{t("ctaSection.description")}
|
||||
</p>
|
||||
<Link
|
||||
href={`/${locale}/houses`}
|
||||
className="inline-flex items-center gap-2 bg-white text-brand-green-dark font-semibold px-8 py-4 rounded-2xl hover:bg-brand-grey-soft hover:shadow-lg hover:-translate-y-0.5 transition-all"
|
||||
>
|
||||
{t("ctaSection.cta")}
|
||||
<ArrowRight className="w-5 h-5" />
|
||||
</Link>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
import { getTranslations } from "next-intl/server";
|
||||
|
||||
export async function generateMetadata({
|
||||
params: { locale },
|
||||
}: {
|
||||
params: { locale: string };
|
||||
}) {
|
||||
const t = await getTranslations({ locale, namespace: "footer" });
|
||||
return {
|
||||
title: t("privacy"),
|
||||
};
|
||||
}
|
||||
|
||||
export default function PrivacyPage({
|
||||
params: { locale },
|
||||
}: {
|
||||
params: { locale: string };
|
||||
}) {
|
||||
const isDe = locale === "de";
|
||||
|
||||
return (
|
||||
<div className="py-16">
|
||||
<div className="max-w-3xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<h1 className="text-4xl font-bold mb-8">
|
||||
{isDe ? "Datenschutzerklärung" : "Privacy Policy"}
|
||||
</h1>
|
||||
<div className="prose prose-lg max-w-none">
|
||||
<p className="text-brand-grey mb-8">
|
||||
{isDe
|
||||
? "Der Schutz Ihrer personenbezogenen Daten ist uns wichtig. Wir verarbeiten Ihre Daten nur, wenn dies technisch oder zur Vertragsabwicklung erforderlich ist."
|
||||
: "The protection of your personal data is important to us. We only process your data when this is technically necessary or required for contract processing."}
|
||||
</p>
|
||||
|
||||
<h2 className="text-2xl font-bold mb-4">
|
||||
{isDe ? "Verantwortlicher" : "Controller"}
|
||||
</h2>
|
||||
<p className="text-brand-grey mb-6">
|
||||
<strong>
|
||||
{isDe ? "Grundstücksgemeinschaft Allami / Allawy" : "Allami / Allawy Property Community"}
|
||||
</strong>
|
||||
<br />
|
||||
Forchenweg 1
|
||||
<br />
|
||||
71063 Sindelfingen
|
||||
<br />
|
||||
{isDe ? "Deutschland" : "Germany"}
|
||||
<br />
|
||||
<br />
|
||||
{isDe ? "Vertreten durch:" : "Represented by:"} Ali Allawy
|
||||
<br />
|
||||
E-Mail: info@bb-apartment.de
|
||||
<br />
|
||||
{isDe ? "Telefon:" : "Phone:"} 07031 7631132
|
||||
</p>
|
||||
|
||||
<h2 className="text-2xl font-bold mb-4">
|
||||
{isDe ? "Keine Cookies & kein Tracking" : "No Cookies & No Tracking"}
|
||||
</h2>
|
||||
<p className="text-brand-grey mb-6">
|
||||
{isDe
|
||||
? "Diese Website verwendet keine Cookies, Tracking-Tools, Analyse-Dienste oder Werbe-Technologien. Wir speichern keine personenbezogenen Daten zu Marketing- oder Profiling-Zwecken."
|
||||
: "This website does not use cookies, tracking tools, analytics services or advertising technologies. We do not store personal data for marketing or profiling purposes."}
|
||||
</p>
|
||||
|
||||
<h2 className="text-2xl font-bold mb-4">
|
||||
{isDe ? "Server-Logfiles" : "Server Log Files"}
|
||||
</h2>
|
||||
<p className="text-brand-grey mb-6">
|
||||
{isDe
|
||||
? "Unser Hosting-Anbieter erhebt automatisch technische Daten (z. B. Browsertyp, IP-Adresse, Uhrzeit der Anfrage), die für die technische Bereitstellung und Sicherheit der Website erforderlich sind. Diese Daten werden automatisch gelöscht und nicht mit anderen Datenquellen zusammengeführt."
|
||||
: "Our hosting provider automatically collects technical data (e.g. browser type, IP address, time of request) required for the technical provision and security of the website. This data is automatically deleted and not merged with other data sources."}
|
||||
</p>
|
||||
|
||||
<h2 className="text-2xl font-bold mb-4">
|
||||
{isDe ? "Kontaktanfragen" : "Contact Inquiries"}
|
||||
</h2>
|
||||
<p className="text-brand-grey mb-6">
|
||||
{isDe
|
||||
? "Wenn Sie uns per E-Mail oder Kontaktformular kontaktieren, speichern wir die von Ihnen freiwillig übermittelten Daten ausschließlich zur Bearbeitung Ihrer Anfrage. Eine Weitergabe an Dritte erfolgt nicht."
|
||||
: "If you contact us by email or contact form, we store the data you voluntarily provide solely for the purpose of processing your inquiry. This data is not passed on to third parties."}
|
||||
</p>
|
||||
|
||||
<h2 className="text-2xl font-bold mb-4">
|
||||
{isDe ? "Ihre Rechte" : "Your Rights"}
|
||||
</h2>
|
||||
<p className="text-brand-grey mb-6">
|
||||
{isDe
|
||||
? "Sie haben das Recht auf Auskunft, Berichtigung, Löschung, Einschränkung der Verarbeitung sowie Widerspruch gegen die Verarbeitung Ihrer personenbezogenen Daten. Bitte wenden Sie sich dazu an info@bb-apartment.de."
|
||||
: "You have the right to access, rectification, erasure, restriction of processing and objection to the processing of your personal data. Please contact info@bb-apartment.de for this."}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user