diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..69f2792 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,10 @@ +node_modules +.next +.env +.env.local +.env.*.local +coverage +.DS_Store +*.log +npm-debug.log* +.prisma diff --git a/.env.local.example b/.env.local.example new file mode 100644 index 0000000..1625885 --- /dev/null +++ b/.env.local.example @@ -0,0 +1,39 @@ +# Beds24 API (v2) — each house has its own separate Beds24 account/login, +# so each needs its own invite code exchanged for its own refresh token: +# 1. Log into that house's Beds24 account: Settings > Account > Apps/API > +# "Generate invite code" (scopes: inventory, later also "bookings") +# 2. Exchange once: curl -H "code: " https://api.beds24.com/v2/authentication/setup +# 3. Paste the returned "refreshToken" value below. +BEDS24_REFRESH_TOKEN_HERDWEG=your_beds24_refresh_token_for_herdweg +BEDS24_REFRESH_TOKEN_SCHOENAICHER=your_beds24_refresh_token_for_schoenaicher + +# Stripe (one account per house) +NEXT_PUBLIC_BASE_URL=https://www.BB-Apartment.de +STRIPE_ACCOUNT_HERDWEWEG=acct_xxxxxxxxxxxxxxxx +STRIPE_ACCOUNT_SCHOENAICHER=acct_xxxxxxxxxxxxxxxx +STRIPE_SECRET_KEY_HERDWEWEG=sk_test_xxxxxxxxxxxxxxxx +STRIPE_SECRET_KEY_SCHOENAICHER=sk_test_xxxxxxxxxxxxxxxx +STRIPE_WEBHOOK_SECRET_HERDWEWEG=whsec_xxxxxxxxxxxxxxxx +STRIPE_WEBHOOK_SECRET_SCHOENAICHER=whsec_xxxxxxxxxxxxxxxx + +# Database (used by Next.js and n8n) +DATABASE_URL=postgresql://bbapartment:changeme@postgres:5432/bb_apartment +POSTGRES_USER=bbapartment +POSTGRES_PASSWORD=changeme +POSTGRES_DB=bb_apartment + +# n8n +N8N_BASIC_AUTH_USER=admin +N8N_BASIC_AUTH_PASSWORD=changeme +N8N_WEBHOOK_URL=https://n8n.bb-apartment.de/ + +# SMTP (own server) — used by the contact form (/api/contact) and, later, +# booking confirmation emails. Without these set, contact messages are just +# logged to the server console instead of sent (useful for local dev). +SMTP_HOST=mail.your-server.de +SMTP_PORT=587 +SMTP_USER=your_smtp_user +SMTP_PASSWORD=your_smtp_password +SMTP_FROM=noreply@bb-apartment.de +# Inbox that receives contact form submissions (defaults to info@bb-apartment.de) +CONTACT_INBOX_EMAIL=info@bb-apartment.de diff --git a/DEPLOYMENT.md b/DEPLOYMENT.md new file mode 100644 index 0000000..b7be1c5 --- /dev/null +++ b/DEPLOYMENT.md @@ -0,0 +1,157 @@ +# Deployment-Anleitung (Docker) + +Diese Anleitung beschreibt, wie die BB-Apartment-Website auf einem eigenen Server per Docker deployt wird. Das Projekt bringt bereits ein fertiges `Dockerfile` und `docker-compose.yml` mit. + +## 1. Voraussetzungen auf dem Server + +Docker & Docker Compose installieren (falls noch nicht vorhanden), z. B. auf Debian/Ubuntu: + +```bash +curl -fsSL https://get.docker.com | sh +sudo usermod -aG docker $USER +``` + +## 2. Code auf den Server bringen + +Git ist **nicht zwingend erforderlich** – es gibt mehrere gleichwertige Wege: + +### Option A: Mit Git (am einfachsten für spätere Updates) + +```bash +git clone bb-apartment-website +cd bb-apartment-website +``` + +Updates später einfach per `git pull` (siehe Abschnitt "Updates einspielen"). + +### Option B: Ohne Git – Dateien direkt kopieren (rsync/scp) + +Von deinem lokalen Rechner aus, z. B. per `rsync`: + +```bash +rsync -avz --exclude 'node_modules' --exclude '.next' --exclude '.git' \ + ./bb-apartment-website/ \ + user@dein-server:/pfad/zu/bb-apartment-website/ +``` + +Alternativ per `scp` oder einem SFTP-Programm (z. B. FileZilla) den kompletten Ordner hochladen. `.env.local` am besten separat/sicher übertragen oder direkt auf dem Server neu anlegen (Schritt 3), statt sie mitzukopieren. + +### Option C: Docker-Image lokal bauen und übertragen + +Falls der Server gar keinen Zugriff auf den Quellcode/Registry haben soll: + +```bash +# Lokal bauen und als Datei exportieren +docker build -t bb-apartment-website . +docker save bb-apartment-website | gzip > bb-apartment.tar.gz + +# Auf den Server kopieren und laden +scp bb-apartment.tar.gz user@dein-server:/tmp/ +ssh user@dein-server "gunzip -c /tmp/bb-apartment.tar.gz | docker load" +``` + +Auf dem Server dann in Schritt 4 `docker compose up -d` **ohne** `--build` verwenden, da das Image schon vorhanden ist (ggf. `image: bb-apartment-website` statt `build:` im `docker-compose.yml` für den `nextjs`-Service eintragen). + +### Option D: Docker Registry (Docker Hub, GitHub Container Registry, ...) + +Lokal bauen → Image in eine Registry pushen → auf dem Server nur pullen. Praktisch bei häufigeren Updates über mehrere Server hinweg, aber etwas mehr initiales Setup (Registry-Zugang einrichten). + +--- + +Für die restliche Anleitung wird der einfachste Fall (Git) angenommen – bei den anderen Optionen einfach `git pull` in Schritt "Updates einspielen" durch dein jeweiliges Kopier-/Pull-Verfahren ersetzen. + +## 3. `.env.local` mit echten Werten anlegen + +Diese Datei liegt **nicht** im Git-Repo (enthält Secrets). Auf dem Server einmalig erstellen: + +```bash +cp .env.local.example .env.local +nano .env.local # alle Platzhalter durch echte Werte ersetzen +``` + +Wichtig einzutragen: + +- **Beds24**: `BEDS24_REFRESH_TOKEN_HERDWEG` und `BEDS24_REFRESH_TOKEN_SCHOENAICHER` (je Haus ein eigener Zugang, siehe README für die Anleitung zum Beschaffen des Tokens) +- **Stripe**: Account-IDs, Secret Keys und Webhook-Secrets für **beide** Häuser +- **Datenbank**: `DATABASE_URL` / `POSTGRES_USER` / `POSTGRES_PASSWORD` / `POSTGRES_DB` +- **SMTP**: Zugangsdaten für den Versand der Kontaktformular-Mails +- **`NEXT_PUBLIC_BASE_URL`**: die echte Domain, z. B. `https://www.bb-apartment.de` +- **`N8N_*`**: Zugangsdaten für die n8n-Automatisierung (Buchungsbestätigungen etc.) + +## 4. Container bauen und starten + +```bash +docker compose up -d --build +``` + +Das startet automatisch: + +- **Next.js** (Port 3000) – die eigentliche Website +- **PostgreSQL** – Datenbank für Buchungen +- **n8n** (Port 5678) – Automatisierungen + +### Falls Port 3000 (oder 5678) auf dem Server schon belegt ist + +Der Container selbst lauscht intern immer auf Port 3000 (bzw. 5678 bei n8n) — nur der **Host-Port** (die linke Seite von `"3000:3000"` in `docker-compose.yml`) muss angepasst werden. Das ist bereits über Umgebungsvariablen konfigurierbar. + +Wichtig: Docker Compose liest diese Variablen für die `ports:`-Zuordnung aus einer Datei namens **`.env`** (nicht `.env.local`!) im selben Ordner wie `docker-compose.yml`, oder aus der Shell-Umgebung. Eine Datei `.env` mit folgendem Inhalt anlegen: + +```bash +cat > .env <<'EOF' +NEXTJS_HOST_PORT=8080 +N8N_HOST_PORT=8679 +EOF +``` + +Dann wie gewohnt starten: + +```bash +docker compose up -d --build +``` + +Die Website ist danach unter `http://localhost:8080` erreichbar (bzw. dem von dir gewählten Port). Der Reverse Proxy (siehe Schritt 7) muss dann auf diesen Port statt auf 3000 zeigen. + +Alternativ – ohne `.env`-Datei – geht es auch einmalig inline: + +```bash +NEXTJS_HOST_PORT=8080 docker compose up -d --build +``` + +## 5. Datenbank-Migration einmalig ausführen + +```bash +docker compose exec nextjs npx prisma migrate deploy +``` + +## 6. Prüfen, ob alles läuft + +```bash +docker compose ps +docker compose logs -f nextjs +curl http://localhost:3000 # bzw. der Port aus deiner .env, falls angepasst +``` + +## 7. Reverse Proxy (Domain + HTTPS) + +Das Compose-Setup exponiert Next.js nur auf einem HTTP-Port (Standard 3000) ohne SSL. Für die echte Domain braucht es einen Reverse Proxy (nginx, Caddy o. ä.) davor, der z. B. `www.bb-apartment.de` → `localhost:3000` (bzw. den gewählten Host-Port) weiterleitet und das SSL-Zertifikat (Let's Encrypt) terminiert. + +## Updates einspielen + +```bash +git pull +docker compose up -d --build # bei Bedarf gezielt nur einen Service: docker compose up -d --build nextjs +docker compose exec nextjs npx prisma migrate deploy # nur falls sich das DB-Schema geändert hat +``` + +## Fehlersuche + +```bash +docker compose logs -f nextjs # Next.js-Logs live verfolgen +docker compose logs -f postgres # Datenbank-Logs +docker compose restart nextjs # Einzelnen Service neu starten +docker compose down # Alles stoppen (Daten in Volumes bleiben erhalten) +``` + +## Hinweise zu Beds24-Rate-Limits + +Die Website cached Beds24-Anfragen serverseitig (5 Minuten bis 1 Stunde, je nach Endpunkt), um innerhalb der Beds24-Anfragelimits zu bleiben. Nach Änderungen in Beds24 (z. B. Mindestaufenthalt, Preise) kann es daher bis zu einer Stunde dauern, bis diese auf der Website sichtbar sind. Details siehe Kommentare in `src/lib/beds24.ts`. diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..47fd24c --- /dev/null +++ b/Dockerfile @@ -0,0 +1,42 @@ +FROM node:20-alpine AS base + +FROM base AS deps +RUN apk add --no-cache libc6-compat +WORKDIR /app +COPY package.json package-lock.json* ./ +RUN npm ci --legacy-peer-deps + +FROM base AS builder +WORKDIR /app +COPY --from=deps /app/node_modules ./node_modules +COPY . . + +ENV NEXT_TELEMETRY_DISABLED=1 +ENV NODE_ENV=production + +RUN npx prisma generate +RUN npm run build + +FROM base AS runner +WORKDIR /app + +ENV NODE_ENV=production +ENV NEXT_TELEMETRY_DISABLED=1 + +RUN addgroup --system --gid 1001 nodejs +RUN adduser --system --uid 1001 nextjs + +COPY --from=builder /app/public ./public +COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./ +COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static +COPY --from=builder --chown=nextjs:nodejs /app/prisma ./prisma +COPY --from=builder --chown=nextjs:nodejs /app/node_modules/.prisma ./node_modules/.prisma + +USER nextjs + +EXPOSE 3000 + +ENV PORT=3000 +ENV HOSTNAME="0.0.0.0" + +CMD ["node", "server.js"] diff --git a/README.md b/README.md index e215bc4..df5b747 100644 --- a/README.md +++ b/README.md @@ -1,36 +1,145 @@ -This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app). +# BB-Apartment Website -## Getting Started +Zweisprachige (DE/EN), buchungsfähige Website für BB-Apartment mit Next.js, Tailwind CSS, Beds24-Integration, Stripe-Zahlung und n8n-Automatisierung. -First, run the development server: +## Technologie-Stack -```bash -npm run dev -# or -yarn dev -# or -pnpm dev -# or -bun dev +- **Framework:** Next.js 14 (App Router, TypeScript) +- **Styling:** Tailwind CSS +- **Internationalisierung:** next-intl (Deutsch/Englisch) +- **Zahlungen:** Stripe Checkout (Anzahlung) + Stripe Billing (monatliche Rechnungen) + Stripe Customer Portal +- **PMS:** Beds24 API +- **Workflows:** n8n (Self-Hosted) +- **Datenbank:** PostgreSQL (via Prisma) +- **Deployment:** Docker Compose auf eigenem Server + +## Projektstruktur + +``` +app/[locale]/ # Öffentliche Seiten (i18n-Routing) +app/api/ # API-Endpunkte (Beds24, Stripe, Portal) +components/ # React-Komponenten +messages/ # Übersetzungsdateien (de.json, en.json) +prisma/ # Prisma Schema +public/images/ # Bilder & Logo +src/config/ # Apartment-Konfiguration & Preislogik +src/lib/ # API-Clients (Beds24, Stripe, Prisma) ``` -Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. +## Lokal entwickeln -You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. +1. Abhängigkeiten installieren: + ```bash + npm install + ``` -This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel. +2. Umgebungsvariablen kopieren und ausfüllen: + ```bash + cp .env.local.example .env.local + ``` -## Learn More +3. Datenbank migrieren (lokale Postgres oder Docker): + ```bash + npx prisma migrate dev + ``` -To learn more about Next.js, take a look at the following resources: +4. Dev-Server starten: + ```bash + npm run dev + ``` -- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. -- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. + Die Seite ist unter `http://localhost:3000/de` bzw. `http://localhost:3000/en` erreichbar. -You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome! +## Produktiv-Deployment (Docker Compose) -## Deploy on Vercel +1. `.env.local` auf dem Server mit echten Werten befüllen. -The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. +2. Container starten: + ```bash + docker compose up -d --build + ``` -Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details. + Dies startet: + - Next.js App auf Port 3000 + - PostgreSQL + - n8n auf Port 5678 + +3. Erste Datenbank-Migration im Container ausführen: + ```bash + docker compose exec nextjs npx prisma migrate deploy + ``` + +## Wichtige API-Endpunkte + +- `GET /api/availability?apartmentSlug=...&start=YYYY-MM-DD&end=YYYY-MM-DD` + Verfügbarkeit und Preis (inkl. Rabatt/Anzahlung) +- `POST /api/checkout-session` + Stripe Checkout für Anzahlung erstellen +- `POST /api/portal-session` + Stripe Customer Portal für Rechnungen/Adresse +- `POST /api/webhooks/stripe` + Stripe Webhook Empfänger +- `GET /api/rooms` + Apartment-Informationen aus Beds24 + +## Konfiguration + +### Apartments + +Die 8 Apartments mit Beds24 Property-/Room-IDs sind in `src/config/apartments.ts` hinterlegt: + +- **Herdweg** (Property-ID `121184`): 4 Apartments +- **Schönaicher Straße** (Property-ID `36159`): 4 Apartments + +### Preise & Anzahlung + +Rabattstaffel und Anzahlungshöhe sind in `src/config/apartments.ts` (`bookingConfig`) zentral konfigurierbar: + +- Mindestaufenthalt: 20 Tage +- Standard-Rabatte: 30 Nächte = 5%, 60 Nächte = 10%, 90 Nächte = 15% +- Anzahlung: 20% der Gesamtkosten + +### Stripe + +Pro Haus wird ein separates Stripe-Konto verwendet. Die API-Keys und Webhook-Secrets müssen in `.env.local` hinterlegt werden. + +### Beds24 + +Die Anbindung nutzt **Beds24 API v2** (Token-Auth), siehe Kommentare in `src/lib/beds24.ts`. + +**Jedes Haus hat einen eigenen, separaten Beds24-Zugang** (eigener Login, nicht nur eine eigene Unterkunft innerhalb eines Accounts) — analog zu den pro Haus getrennten Stripe-Konten. Es werden daher zwei Invite-Codes/Refresh-Tokens benötigt: + +1. In **jedem** der beiden Beds24-Konten: Settings → Account → Apps/API → "Generate invite code" (Scope `inventory`, später auch `bookings` für automatische Buchungsanlage). +2. Jeden Code einmalig gegen einen Refresh-Token tauschen: + ```bash + curl -H "code: " https://api.beds24.com/v2/authentication/setup + ``` +3. Die zurückgegebenen `refreshToken`-Werte als `BEDS24_REFRESH_TOKEN_HERDWEG` bzw. `BEDS24_REFRESH_TOKEN_SCHOENAICHER` in `.env.local` hinterlegen. + +Jeder Refresh-Token bleibt gültig, solange er mindestens alle 30 Tage einmal genutzt wird (passiert automatisch bei jedem Seitenaufruf mit Verfügbarkeitsprüfung für das jeweilige Haus). Ohne konfigurierten Token liefert der jeweilige Endpunkt Mock-Daten für die Entwicklung. + +**Wichtig:** Die genaue Feldstruktur der Antwort von `/inventory/rooms/offers` und `/properties` wurde anhand der offiziellen Beds24-Dokumentation modelliert, aber noch nicht gegen einen echten Account verifiziert. Nach Eintragen der echten Tokens unbedingt einmal `/api/rooms` aufrufen und mit der Swagger-UI (https://beds24.com/api/v2/#/) abgleichen, ob Feldnamen wie `maxPeople`/`minNights` exakt passen. + +## Nächste Schritte / Offene Punkte + +- Für beide Häuser je einen echten Beds24-Refresh-Token hinterlegen und Antwortstruktur gegen Swagger-UI verifizieren +- Stripe API-Keys & Webhooks für beide Häuser konfigurieren +- n8n-Workflows für Buchungsbestätigung, Beds24-Buchungsanlage und monatliche Rechnungen bauen +- E-Mail-Vorlagen (Deutsch/Englisch) finalisieren +- Haus-/Apartment-Adressen vervollständigen +- Optional: 3D-Rundgänge (Insta360) und KI-Assistent (OpenRouter) nach Go-Live + +### Geplant: Monatliche Zahlweise (zurückgestellt) + +Entscheidung: **Option A – automatisch per Stripe Subscription Schedule**, Monatsrate = (Gesamtpreis − Anzahlung) ÷ Anzahl Monate, gleichmäßig verteilt. + +Umsetzungsschritte, sobald wir weitermachen: +1. Bei der Anzahlungs-Checkout-Session `payment_intent_data.setup_future_usage: "off_session"` setzen, um die Karte zu speichern (inkl. klarer Einwilligungs-Formulierung für den Gast). +2. Nach erfolgreicher Anzahlung (Webhook `checkout.session.completed`) automatisch einen Stripe **Subscription Schedule** mit fester Phasenzahl (= Aufenthaltsmonate) und `end_behavior: "cancel"` anlegen. +3. Prisma-Schema um `stripeSubscriptionScheduleId` / `stripeSubscriptionId` auf `Booking` erweitern. +4. Webhook um `invoice.paid` / `invoice.payment_failed` erweitern (Status-Update + Benachrichtigung bei Fehlschlag). +5. Optional später: SEPA-Lastschrift als zusätzliche Zahlmethode ergänzen. + +## Lizenz + +Privat – alle Rechte vorbehalten. diff --git a/app/[locale]/apartments/[apartmentSlug]/page.tsx b/app/[locale]/apartments/[apartmentSlug]/page.tsx new file mode 100644 index 0000000..1220252 --- /dev/null +++ b/app/[locale]/apartments/[apartmentSlug]/page.tsx @@ -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 ( +
+
+
+ + ← {houseName} + +
+ +
+ 0 ? apartment.images : ["/images/placeholder.jpg"]} + alt={displayName} + /> +
+

{displayName}

+

{houseName} • {displayFloor}

+ + {displayDescription && ( +

+ {displayDescription} +

+ )} + +
+
+ +
+

{t("guests")}

+

{apartment.maxGuests}

+
+
+
+ +
+

{t("minStay")}

+

{minStayNights} {t("nights")}

+
+
+ {amenities.some((a) => + ["Klimaanlage", "Air conditioning"].includes(a) + ) && ( +
+ +
+

+ {locale === "de" ? "Klimaanlage" : "Air conditioning"} +

+

+ {locale === "de" ? "Vorhanden" : "Available"} +

+
+
+ )} +
+ +
+

+ {locale === "de" ? "Vollmöbliert" : "Fully furnished"} +

+

+ {locale === "de" ? "Ja" : "Yes"} +

+
+
+ {apartment.bedrooms && ( +
+ +
+

{locale === "de" ? "Schlafzimmer" : "Bedrooms"}

+

{apartment.bedrooms}

+
+
+ )} + {apartment.bathrooms && ( +
+ +
+

{locale === "de" ? "Bäder" : "Bathrooms"}

+

{apartment.bathrooms}

+
+
+ )} +
+ + +
+
+ + {specificHighlights.length > 0 && ( +
+ {specificHighlights.map((highlight, index) => { + const Icon = highlight.icon; + return ( +
+
+ +
+

+ {highlight.title} +

+
+ ); + })} +
+ )} + +
+

{t("features")}

+
+ {amenities.map((amenity, index) => ( +
+ + {amenity} +
+ ))} +
+
+
+
+ ); +} diff --git a/app/[locale]/booking-success/page.tsx b/app/[locale]/booking-success/page.tsx new file mode 100644 index 0000000..c9fc869 --- /dev/null +++ b/app/[locale]/booking-success/page.tsx @@ -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 ( +
+
+ +

+ {isDe ? "Anzahlung erfolgreich!" : "Deposit successful!"} +

+

+ {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."} +

+ + {isDe ? "Zurück zur Startseite" : "Back to home"} + +
+
+ ); +} diff --git a/app/[locale]/contact/page.tsx b/app/[locale]/contact/page.tsx new file mode 100644 index 0000000..070f745 --- /dev/null +++ b/app/[locale]/contact/page.tsx @@ -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 ( +
+
+
+

{t("title")}

+

+ {t("description")} +

+
+ +
+
+

+ {locale === "de" ? "Kontaktdaten" : "Contact Details"} +

+
+
+ +
+

{t("email")}

+ + info@bb-apartment.de + +
+
+
+ +
+

{t("phone")}

+

+49 (0) 7031 7631132

+
+
+
+ +
+

{locale === "de" ? "Adresse" : "Address"}

+

+ BB-Apartment
+ Böblingen
+ Deutschland +

+
+
+
+
+ +
+

+ {locale === "de" ? "Nachricht senden" : "Send a message"} +

+
+
+ + +
+
+ + +
+
+ +