Offer Detail Page (Public)

Full-page template for the public experience-focused offer detail view. Composes the Welcome Section (hero) with a glassy Contact Form (name, email, phone, message CTA), and a main content section with a two-column layout: left column holds description, gallery, upcoming offers table, and right column holds amenities, optional extras, FAQ and cancellation policy links. Rental options are House A, House B, or Both.

Full Page Public / Guest Contact Form Rental Options A/B/Both Trust Signals Inquiry CTA Optional Extras

๐Ÿ“‹ Template Context & Composition Notes

This is a standard-context public page (not the glassy app shell). The booking widget uses form-card--glassy placed over the hero background โ€” its glassy surface is scoped to the hero section only. All sections below the hero use standard DS classes on light/neutral surfaces.

Full Page Preview

Two-column main content: left column (description + gallery + upcoming offers), right column (amenities + extras + FAQ/cancellation). Footer is not rendered here โ€” compose from navigations/footer.html.

Live Preview

๐Ÿ“Œ Navbar is composed separately โ€” see navigations/navbar.html.

Forest cabin in Kampinos
๐ŸŒฟ Seasonal Experience โœ“ Available

Majรณwka w Puszczy

Celebrate the long May weekend surrounded by blooming nature. BBQ, campfire evenings, and forest walks await your family or group.

๐Ÿ—“ 1โ€“4 May 2026 3 nights minimum ๐Ÿ‘ฅ Up to 10 guests ๐Ÿพ Pet friendly ๐Ÿ”ฅ BBQ included

Get in Touch

Interested in this offer? Send us a message and we'll get back to you shortly.

By sending a message, you agree to provide your email address which will be used solely for the purpose of this engagement. The content of your message will be automatically verified against spam.

The Experience

Your Forest Weekend Getaway

Escape the city and immerse yourself in the tranquility of Kampinos National Park. Our seasonal May weekend retreat gives you exclusive access to our forest property surrounded by pristine wilderness, wild flowers, and the gentle sounds of nature.

Whether you choose House A, House B, or book both for a larger group, you will enjoy dedicated outdoor spaces, a communal BBQ terrace, fire pit evenings, and direct access to forest trails starting from the property gate.

Gallery

Photo 1
Photo 2
Photo 3
Photo 4
Photo 5
Photo 6
Upcoming Offers

Check Availability

Future dates only ยท 6 per page

May 2026

1 โ€“ 4

3 nights

โœ“ Available
๐Ÿ  House A โœ“ ๐Ÿ  House B โœ“

from

450 PLN

/ night

Jun 2026

5 โ€“ 8

3 nights

โœ“ Available
๐Ÿ  House A โœ“ ๐Ÿ  House B โœ“

from

390 PLN

/ night

Jun 2026

15 โ€“ 22

7 nights

โšก Last Minute
๐Ÿ  House A โœ“ ๐Ÿ  House B โ€”

from

320 PLN

/ night

Jul 2026

1 โ€“ 7

6 nights

โœ“ Available
๐Ÿ  House A โœ“ ๐Ÿ  House B โœ“

from

580 PLN

/ night

Aug 2026

10 โ€“ 17

7 nights

Sold Out
๐Ÿ  House A โ€” ๐Ÿ  House B โ€”

โ€”

Sold Out

Sep 2026

5 โ€“ 8

3 nights

โœ“ Available
๐Ÿ  House A โœ“ ๐Ÿ  House B โœ“

from

340 PLN

/ night

โ† Previous

Page 1 of 3 ยท 12 upcoming offers

Next โ†’
Amenities

What's Included

  • ๐Ÿ”ฅ

    BBQ Area

    Covered BBQ terrace with grill and garden seating

  • ๐ŸŒฟ

    Garden & Fire Pit

    Shared outdoor space with campfire area

  • ๐Ÿ“ถ

    Free Wi-Fi

    High-speed internet available in both houses

  • ๐Ÿพ

    Pet Friendly

    Pets welcome (prior notification required)

  • ๐Ÿ…ฟ๏ธ

    Free Parking

    Private parking on property grounds

  • ๐Ÿšฟ

    Private Bathroom

    Each house has its own full bathroom

Optional Extras

Available at Extra Cost

  • ๐Ÿšด

    Bike Rental

    Per bike per day โ€” book in advance

  • ๐Ÿณ

    Breakfast Basket

    Fresh local produce delivered to your door each morning

  • ๐Ÿชต

    Firewood Bundle

    Ready-to-use firewood for fire pit evenings

Optional extras can be requested in the inquiry message or confirmed with the host after booking.

๐Ÿ“Œ Footer is composed separately โ€” see navigations/footer.html.

Upcoming Offers Table

Spaced card-row design. Each offer is a horizontal flex card with a date accent strip on the left. Status badge (Available / Last Minute / Sold Out), House A/B availability pills, price from, and two CTA buttons (Reserve / Ask Question). Shows 6 future offers per page with Prev/Next pagination.

Upcoming Offers Table
Upcoming Offers

Check Availability

Future dates only ยท 6 per page

May 2026

1 โ€“ 4

3 nights

โœ“ Available
๐Ÿ  House A โœ“ ๐Ÿ  House B โœ“

from

450 PLN

/ night

Jun 2026

15 โ€“ 22

7 nights

โšก Last Minute
๐Ÿ  House A โœ“ ๐Ÿ  House B โ€”

from

320 PLN

/ night

Aug 2026

10 โ€“ 17

7 nights

Sold Out
๐Ÿ  House A โ€” ๐Ÿ  House B โ€”

โ€”

Sold Out
โ† Previous

Page 1 of 3 ยท 12 upcoming offers

Next โ†’

HTML Snippet โ€” Full Page Template

Copy-ready page template. Replace placeholder content with dynamic PHP output. Compose Navbar and Footer from their respective DS pages. Translation key comments mark all visible labels. Always include a CSRF token in the inquiry form.

Full Page Template
<!-- Navbar: compose from navigations/navbar.html (Guest variant) -->

<!-- HERO: Offer Header + Booking Widget -->
<section class="welcome-section">
  <div class="welcome-section__bg">
    <img
      src="<?= htmlspecialchars($offer['hero_image_url']) ?>"
      alt="<?= htmlspecialchars($offer['title']) ?>"
      class="welcome-section__bg-image"
      loading="eager"
    />
    <div class="welcome-section__bg-overlay"></div>
  </div>
  <div class="welcome-section__content">
    <div class="grid grid-cols-1 lg:grid-cols-2 gap-12 lg:gap-16 items-center">

      <!-- Left: Offer Info -->
      <div>
        <span class="badge badge--primary mb-4"><?= __('offer.seasonal_badge') ?></span>
        <span class="badge badge--overlay mb-6 inline-block"><?= __('offer.status.available') ?></span>
        <h1 class="t-display-glass mb-4"><?= htmlspecialchars($offer['title']) ?></h1>
        <p class="t-lead-glass mb-6"><?= htmlspecialchars($offer['lead']) ?></p>
        <div class="badge-group mb-8">
          <span class="badge badge--muted">๐Ÿ—“ <?= htmlspecialchars($offer['dates_label']) ?></span>
          <span class="badge badge--muted"><?= __('offer.min_nights', ['n' => $offer['min_nights']]) ?></span>
          <span class="badge badge--muted">๐Ÿ‘ฅ <?= __('offer.max_guests', ['n' => $offer['max_guests']]) ?></span>
          <?php foreach ($offer['feature_badges'] as $badge): ?>
          <span class="badge badge--muted"><?= htmlspecialchars($badge) ?></span>
          <?php endforeach; ?>
        </div>
      </div>

      <!-- Right: Contact Form (Glassy) -->
      <div class="form-card form-card--glassy">
        <div class="form-card__header">
          <p class="t-lead-glass"><?= __('contact.widget.title') ?></p>
          <p class="t-small-glass"><?= __('contact.widget.subtitle') ?></p>
        </div>
        <form class="form-card__body" action="/<?= $lang ?>/contact" method="post">
          <input type="hidden" name="csrf_token" value="<?= htmlspecialchars($_SESSION['csrf_token']) ?>" />
          <input type="hidden" name="offer_ref" value="<?= htmlspecialchars($offer['slug'] ?? '') ?>" />
          <div class="form-layout">

            <!-- Message -->
            <div class="field field--glass">
              <label class="field__label"><?= __('contact.message') ?></label>
              <div class="field__control">
                <textarea class="input input--glass" name="message" rows="4"><?= htmlspecialchars($_POST['message'] ?? '') ?></textarea>
              </div>
            </div>

            <!-- Email + Phone -->
            <div class="form-grid">
              <div class="field field--glass">
                <label class="field__label"><?= __('contact.email') ?></label>
                <div class="field__control">
                  <input type="email" class="input input--glass" name="email" required
                    value="<?= htmlspecialchars($_POST['email'] ?? '') ?>" />
                </div>
              </div>
              <div class="field field--glass">
                <label class="field__label"><?= __('contact.phone') ?> <span class="t-small t-muted"><?= __('form.optional') ?></span></label>
                <div class="field__control">
                  <input type="tel" class="input input--glass" name="phone"
                    value="<?= htmlspecialchars($_POST['phone'] ?? '') ?>" />
                </div>
              </div>
            </div>

          </div><!-- /form-layout -->

          <!-- Copy checkbox + Disclaimer -->
          <div class="field field--glass mt-4">
            <div class="field__control">
              <label class="form-field__option">
                <input type="checkbox" class="checkbox" name="send_copy" value="1"
                  <?= !empty($_POST['send_copy']) ? 'checked' : '' ?> />
                <span class="t-body"><?= __('contact.send_copy') ?></span>
              </label>
            </div>
            <p class="field__hint"><?= __('contact.disclaimer') ?></p>
          </div>

          <div class="form-card__footer">
            <button type="submit" class="btn btn--primary btn--full">
              <?= __('contact.cta_send') ?>
            </button>
          </div>
        </form>
      </div><!-- /contact form -->

    </div>
  </div>
</section>

<!-- MAIN CONTENT: Description + Gallery + Amenities -->
<section class="bg-white dark:bg-neutral-900 py-16">
  <div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
    <div class="grid grid-cols-1 lg:grid-cols-3 gap-12">

      <!-- Description + Gallery (2 of 3 cols) -->
      <div class="lg:col-span-2 space-y-12">
        <div>
          <span class="badge badge--primary mb-4"><?= __('offer.section.experience') ?></span>
          <h2 class="t-h2 mb-4"><?= htmlspecialchars($offer['description_heading']) ?></h2>
          <?php foreach ($offer['description_paragraphs'] as $para): ?>
          <p class="t-body t-muted mb-4"><?= htmlspecialchars($para) ?></p>
          <?php endforeach; ?>
        </div>
        <div>
          <h3 class="t-h4 mb-4"><?= __('offer.gallery.heading') ?></h3>
          <div class="grid grid-cols-2 md:grid-cols-3 gap-3">
            <?php foreach ($offer['gallery_images'] as $idx => $img): ?>
            <div class="aspect-video rounded-lg overflow-hidden">
              <img
                src="<?= htmlspecialchars($img['url']) ?>"
                alt="<?= htmlspecialchars($img['alt']) ?>"
                class="w-full h-full object-cover"
                loading="<?= $idx === 0 ? 'eager' : 'lazy' ?>"
              />
            </div>
            <?php endforeach; ?>
          </div>
        </div>
        <!-- Upcoming Offers -->
        <div>
          <div class="flex flex-col sm:flex-row sm:items-end sm:justify-between mb-4 gap-2">
            <div>
              <span class="badge badge--primary mb-2"><?= __('offer.upcoming.badge') ?></span>
              <h3 class="t-h3"><?= __('offer.upcoming.heading') ?></h3>
            </div>
            <p class="t-small t-muted"><?= __('offer.upcoming.subtext') ?></p>
          </div>
          <div class="space-y-3">
            <?php foreach ($upcoming_offers as $upcoming): ?>
            <div class="flex flex-col sm:flex-row items-stretch bg-white dark:bg-neutral-800 rounded-2xl shadow-sm border <?= $upcoming['status'] === 'last_minute' ? 'border-amber-200 dark:border-amber-700/50' : 'border-neutral-200 dark:border-neutral-700' ?> hover:shadow-md transition-shadow overflow-hidden <?= $upcoming['status'] === 'sold_out' ? 'opacity-55' : '' ?>">
              <div class="sm:w-44 flex-shrink-0 <?= $upcoming['status'] === 'last_minute' ? 'bg-amber-50 dark:bg-amber-900/20' : ($upcoming['status'] === 'sold_out' ? 'bg-neutral-100 dark:bg-neutral-700/30' : 'bg-primary-50 dark:bg-primary-900/30') ?> flex flex-col items-center justify-center p-4 border-r <?= $upcoming['status'] === 'last_minute' ? 'border-amber-100 dark:border-amber-700/40' : 'border-neutral-100 dark:border-neutral-700' ?>">
                <p class="text-xs font-semibold uppercase tracking-wider"><?= htmlspecialchars($upcoming['month_label']) ?></p>
                <p class="text-2xl font-bold leading-tight"><?= htmlspecialchars($upcoming['day_range']) ?></p>
                <p class="text-xs text-neutral-500 dark:text-neutral-400 mt-1"><?= __('offer.nights', ['n' => $upcoming['nights']]) ?></p>
              </div>
              <div class="flex flex-1 flex-col sm:flex-row items-start sm:items-center gap-4 p-4 sm:px-6">
                <div class="flex-1">
                  <?php if ($upcoming['status'] === 'available'): ?>
                  <span class="badge badge--overlay mb-2"><?= __('status.available') ?></span>
                  <?php elseif ($upcoming['status'] === 'last_minute'): ?>
                  <span class="badge badge--accent mb-2"><?= __('status.last_minute') ?></span>
                  <?php else: ?>
                  <span class="badge badge--muted mb-2"><?= __('status.sold_out') ?></span>
                  <?php endif; ?>
                  <div class="flex gap-3 mt-2">
                    <span class="inline-flex items-center gap-1 text-xs font-medium <?= $upcoming['house_a'] ? 'text-green-700 dark:text-green-400 bg-green-50 dark:bg-green-900/30' : 'text-neutral-400 bg-neutral-100 dark:bg-neutral-700' ?> px-2 py-1 rounded-full">
                      ๐Ÿ  <?= __('col.house_a') ?> <span class="font-bold"><?= $upcoming['house_a'] ? 'โœ“' : 'โ€”' ?></span>
                    </span>
                    <span class="inline-flex items-center gap-1 text-xs font-medium <?= $upcoming['house_b'] ? 'text-green-700 dark:text-green-400 bg-green-50 dark:bg-green-900/30' : 'text-neutral-400 bg-neutral-100 dark:bg-neutral-700' ?> px-2 py-1 rounded-full">
                      ๐Ÿ  <?= __('col.house_b') ?> <span class="font-bold"><?= $upcoming['house_b'] ? 'โœ“' : 'โ€”' ?></span>
                    </span>
                  </div>
                </div>
                <div class="text-right flex-shrink-0">
                  <?php if ($upcoming['price_from']): ?>
                  <p class="text-xs text-neutral-400"><?= __('price.from') ?></p>
                  <p class="text-xl font-bold text-primary-700 dark:text-primary-400 leading-tight"><?= htmlspecialchars($upcoming['price_from']) ?></p>
                  <p class="text-xs text-neutral-400"><?= __('price.per_night') ?></p>
                  <?php else: ?>
                  <p class="text-xl font-bold text-neutral-400">โ€”</p>
                  <?php endif; ?>
                </div>
                <div class="flex sm:flex-col gap-2 flex-shrink-0">
                  <?php if ($upcoming['status'] !== 'sold_out'): ?>
                  <a href="/<?= $lang ?>/inquiry?offer=<?= (int)$upcoming['id'] ?>" class="btn btn--primary btn--sm"><?= __('cta.reserve') ?></a>
                  <a href="/<?= $lang ?>/contact?offer=<?= (int)$upcoming['id'] ?>" class="btn btn--secondary btn--sm"><?= __('cta.ask') ?></a>
                  <?php else: ?>
                  <span class="btn btn--secondary btn--sm cursor-not-allowed"><?= __('status.sold_out') ?></span>
                  <?php endif; ?>
                </div>
              </div>
            </div>
            <?php endforeach; ?>
          </div>
          <!-- Pagination -->
          <div class="flex items-center justify-between mt-6">
            <?php if ($pagination['prev_page']): ?>
            <a href="?page=<?= $pagination['prev_page'] ?>" class="btn btn--secondary btn--sm"><?= __('pagination.prev') ?></a>
            <?php else: ?>
            <span class="btn btn--secondary btn--sm opacity-40 cursor-not-allowed"><?= __('pagination.prev') ?></span>
            <?php endif; ?>
            <p class="t-small t-muted"><?= __('pagination.info', ['current' => $pagination['current'], 'total' => $pagination['total'], 'count' => $pagination['count']]) ?></p>
            <?php if ($pagination['next_page']): ?>
            <a href="?page=<?= $pagination['next_page'] ?>" class="btn btn--secondary btn--sm"><?= __('pagination.next') ?></a>
            <?php else: ?>
            <span class="btn btn--secondary btn--sm opacity-40 cursor-not-allowed"><?= __('pagination.next') ?></span>
            <?php endif; ?>
          </div>
        </div>
      </div>

      <!-- Amenities Sidebar (1 of 3 cols) -->
      <div>
        <span class="badge badge--primary mb-4"><?= __('offer.section.amenities') ?></span>
        <h3 class="t-h4 mb-6"><?= __('offer.amenities.heading') ?></h3>
        <ul class="space-y-4">
          <?php foreach ($offer['amenities'] as $amenity): ?>
          <li class="flex items-start gap-3">
            <span class="text-lg leading-none mt-0.5"><?= htmlspecialchars($amenity['icon']) ?></span>
            <div>
              <p class="t-body t-bold"><?= htmlspecialchars($amenity['label']) ?></p>
              <p class="t-small t-muted"><?= htmlspecialchars($amenity['description']) ?></p>
            </div>
          </li>
          <?php endforeach; ?>
        </ul>

        <!-- Optional / Extra Amenities (paid) -->
        <?php if (!empty($offer['extras'])): ?>
        <div class="mt-8 pt-6 border-t border-neutral-200 dark:border-neutral-700">
          <span class="badge badge--accent mb-4"><?= __('offer.section.extras') ?></span>
          <h4 class="t-h5 mb-4"><?= __('offer.extras.heading') ?></h4>
          <ul class="space-y-4">
            <?php foreach ($offer['extras'] as $extra): ?>
            <li class="flex items-start gap-3">
              <span class="text-lg leading-none mt-0.5"><?= htmlspecialchars($extra['icon']) ?></span>
              <div>
                <p class="t-body t-bold"><?= htmlspecialchars($extra['label']) ?></p>
                <p class="t-small t-muted"><?= htmlspecialchars($extra['description']) ?></p>
              </div>
            </li>
            <?php endforeach; ?>
          </ul>
          <p class="t-small t-muted mt-4"><?= __('offer.extras.note') ?></p>
        </div>
        <?php endif; ?>
        <!-- Trust Signals: FAQ + Cancellation -->
        <div class="mt-8 pt-6 border-t border-neutral-200 dark:border-neutral-700 space-y-3">
          <span class="badge badge--primary mb-2"><?= __('offer.section.trust') ?></span>
          <a href="/<?= $lang ?>/faq" class="flex items-start gap-3 p-3 bg-neutral-50 dark:bg-neutral-700/50 border border-neutral-200 dark:border-neutral-600 rounded-lg hover:bg-neutral-100 dark:hover:bg-neutral-700 transition">
            <span class="text-xl leading-none">โ“</span>
            <div>
              <p class="t-small t-bold"><?= __('trust.faq.title') ?></p>
              <span class="t-small text-primary-600 dark:text-primary-400"><?= __('trust.faq.cta') ?></span>
            </div>
          </a>
          <a href="/<?= $lang ?>/cancellation-policy" class="flex items-start gap-3 p-3 bg-neutral-50 dark:bg-neutral-700/50 border border-neutral-200 dark:border-neutral-600 rounded-lg hover:bg-neutral-100 dark:hover:bg-neutral-700 transition">
            <span class="text-xl leading-none">๐Ÿ“‹</span>
            <div>
              <p class="t-small t-bold"><?= __('trust.cancellation.title') ?></p>
              <p class="t-small t-muted"><?= __('trust.cancellation.summary') ?></p>
              <span class="t-small text-primary-600 dark:text-primary-400"><?= __('trust.cancellation.cta') ?></span>
            </div>
          </a>
        </div>
      </div>

    </div>
  </div>
</section>

<!-- Footer: compose from navigations/footer.html -->

<!-- Footer: compose from navigations/footer.html -->

๐Ÿ’ก UX & Performance Notes

  • โ€ข Mobile-first: The booking widget stacks below the offer text in single-column layout on small screens. Place it at the top of the page flow so it appears early on mobile.
  • โ€ข Image loading: Hero image uses loading="eager". All gallery images below the fold must use loading="lazy".
  • โ€ข Booking flow (MVP): CTA submits an inquiry form. Flow: Inquiry โ†’ Pending โ†’ Confirmed / Cancelled (landlord approval). No payment step at MVP.
  • โ€ข CSRF security: Always include a hidden CSRF token in the inquiry form. Never omit it.
  • โ€ข Date conventions: Check-in is inclusive (first night). Check-out is exclusive (guest departs that day, no overnight). Communicate this in field hints.
  • โ€ข Upcoming offers: Query only future offers (start_date > TODAY). Page size = 6. Order by start_date ASC.
  • โ€ข Exclusivity: A confirmed booking on any rental option blocks overlapping options within the same offer. Rental option filtering reflects current availability.

Related Documentation