Begin Your Journey
Rwanda landscape
Rwanda awaits

Step 1 of 5

How would you like
to explore Rwanda?

Step 2 of 5

When would you like
to travel?

Step 3 of 5

How much would you like
to spend per person?

Step 4 of 5

Your preferences

Step 5 of 5

Your details

We'll respond within 24 hours with your personalised itinerary. No obligation.

Quick Response

Quick Response

We respond within 24 hours with a personalized itinerary proposal crafted by our expert team.

Expert Planning

Expert Planning

Over 10 years of experience crafting unforgettable Rwanda adventures with local insights.

No Obligation

No Obligation

Completely free consultation. Book only when you're ready and confident in your itinerary.

1
Personal
2
Dates
3
Preferences
4
Finalize
01

Personal Information

Tell us about yourself so we can personalize your Rwanda experience

02

Travel Style

What type of experience resonates with you?

🎒
Solo Explorer
Independent adventure
💑
Romantic Escape
Couples & honeymoons
👨‍👩‍👧‍👦
Family Journey
Multi-generational
👥
Group Adventure
Friends & parties
03

Travel Dates

When would you like to visit the Land of a Thousand Hills?

04

Activities & Experiences

Select all that interest you

05

Accommodation Preferences

Choose your preferred level of comfort

5-Star Luxury
$500+ per night
🏨
Boutique Lodges
$250-500 per night
🏡
Mid-Range
$100-250 per night
🎪
Budget-Friendly
Under $100 per night
06

Budget Range

Select your estimated budget per person (USD)

Entry
$500 – $999
Essential experiences on a considered budget
Comfort
$1,000 – $2,499
Quality lodges and curated experiences
Premium
$2,500 – $4,999
Boutique lodges, private guides, premium access
Luxury
$5,000 – $9,999
5-star properties and exclusive experiences
Ultra
$10,000 – $14,999
Private camps, helicopter transfers, VIP access
No Limit
$15,000+
Fully bespoke — we craft around your vision
Custom amount $ USD / person
07

Special Requests & Additional Information

Tell us more about your dream Rwanda journey

We'll respond within 24 hours with a personalized itinerary

Quick Response

Quick Response

We respond within 24 hours with a personalized itinerary proposal crafted by our expert team.

Expert Planning

Expert Planning

Over 10 years of experience crafting unforgettable Rwanda adventures with local insights.

No Obligation

No Obligation

Completely free consultation. Book only when you're ready and confident in your itinerary.

// ── Travel Type Selection + Inline Traveller Count ──────────────────────── (function () { const DEFAULTS = { solo: 1, couple: 2, family: 4, group: 8 }; const MINIMUMS = { solo: 1, couple: 2, family: 2, group: 3 }; const SUFFIXES = { solo: 'person', couple: 'people', family: 'people', group: 'people' }; const LABELS = { solo: 'Travelling solo', couple: 'Number of travellers', family: 'Number of travellers', group: 'Number of travellers' }; const section = document.getElementById('travelerCountSection'); const numInput = document.getElementById('travelerNumInput'); const hiddenEl = document.getElementById('travelerHiddenInput'); const soloNote = document.getElementById('soloNote'); const suffix = document.getElementById('travelerSuffix'); const labelEl = document.getElementById('travelerLabel'); function syncHidden() { if (hiddenEl) hiddenEl.value = numInput ? numInput.value : 1; } function showForType(type) { const isSolo = (type === 'solo'); const def = DEFAULTS[type] || 1; const min = MINIMUMS[type] || 1; if (section) section.style.display = 'block'; if (numInput) { numInput.min = min; numInput.value = def; numInput.disabled = isSolo; } if (suffix) suffix.textContent = SUFFIXES[type] || 'people'; if (labelEl) labelEl.textContent = LABELS[type] || 'Number of travellers'; if (soloNote) soloNote.style.display = isSolo ? 'flex' : 'none'; syncHidden(); } window.selectTravelType = function (card, type) { document.querySelectorAll('.travel-type-card').forEach(c => c.classList.remove('selected')); card.classList.add('selected'); const travelTypeInput = document.querySelector('[name="travel_type"]'); if (travelTypeInput) travelTypeInput.value = type; showForType(type); }; // Keep hidden in sync as user types if (numInput) { numInput.addEventListener('input', function () { const min = parseInt(this.min) || 1; if (this.value !== '' && parseInt(this.value) < min) this.value = min; if (parseInt(this.value) > 99) this.value = 99; syncHidden(); }); numInput.addEventListener('blur', function () { if (!this.value || parseInt(this.value) < 1) this.value = parseInt(this.min) || 1; syncHidden(); }); } })(); // Accommodation Selection function selectAccommodation(card, type) { document.querySelectorAll('.accommodation-card').forEach(c => c.classList.remove('selected')); card.classList.add('selected'); document.querySelector('[name="accommodation"]').value = type; } // ── Budget Tier Selector ────────────────────────────────────────────────── (function () { const tiers = document.querySelectorAll('.budget-tier'); const hidden = document.getElementById('budgetHidden'); const customInput = document.getElementById('budgetCustomInput'); const customRow = document.getElementById('budgetCustomRow'); let activeTier = null; function selectTier(tile) { tiers.forEach(t => t.classList.remove('selected')); tile.classList.add('selected'); activeTier = tile; const val = tile.dataset.val; if (hidden) hidden.value = val; // Clear custom if a tile is chosen if (customInput) { customInput.value = ''; } if (customRow) { customRow.classList.remove('active'); } } tiers.forEach(tile => { tile.addEventListener('click', () => selectTier(tile)); }); // Custom input — clears tile selection if (customInput) { customInput.addEventListener('input', function () { const v = parseInt(this.value); if (this.value !== '') { tiers.forEach(t => t.classList.remove('selected')); activeTier = null; if (hidden) hidden.value = isNaN(v) ? '' : v; customRow && customRow.classList.add('active'); } else { customRow && customRow.classList.remove('active'); } }); customInput.addEventListener('blur', function () { const v = parseInt(this.value); if (!isNaN(v) && v < 500) { this.value = 500; if (hidden) hidden.value = 500; } }); } })(); // Category Filter for Activities const categoryButtons = document.querySelectorAll('.category-btn'); const activityItems = document.querySelectorAll('.activity-item'); categoryButtons.forEach(button => { button.addEventListener('click', function() { // Remove active class from all buttons categoryButtons.forEach(btn => btn.classList.remove('active')); // Add active class to clicked button this.classList.add('active'); // Get selected category const selectedCategory = this.dataset.category; // Filter activities activityItems.forEach(item => { if (selectedCategory === 'all') { item.style.display = 'block'; } else if (item.dataset.category === selectedCategory) { item.style.display = 'block'; } else { item.style.display = 'none'; } }); }); }); // ══════════════════════════════════════════════ // PROGRESS BAR — SCROLL-BASED, SECTION-MAPPED // // 4 steps map to 7 form sections: // Step 1 "Personal" → sections 0–1 (Personal Info, Travel Style) // Step 2 "Dates" → section 2 (Travel Dates) // Step 3 "Preferences" → sections 3–5 (Activities, Accommodation, Budget) // Step 4 "Finalize" → section 6 (Special Requests) // ══════════════════════════════════════════════ (function () { const sections = Array.from(document.querySelectorAll('.form-section')); const progressSteps = Array.from(document.querySelectorAll('.progress-step')); const progressFill = document.querySelector('.progress-line-fill'); if (!sections.length || !progressSteps.length || !progressFill) return; // Map each section index → which step (0-based) it belongs to const sectionToStep = [ 0, // 01 Personal Information → Step 1 0, // 02 Travel Style → Step 1 1, // 03 Travel Dates → Step 2 2, // 04 Activities → Step 3 2, // 05 Accommodation → Step 3 2, // 06 Budget Range → Step 3 3, // 07 Special Requests → Step 4 ]; function updateProgress() { // Use top 40% of viewport as trigger point — feels natural on scroll const triggerY = window.scrollY + window.innerHeight * 0.40; let activeStep = 0; sections.forEach((section, i) => { const top = section.getBoundingClientRect().top + window.scrollY; if (triggerY >= top) { activeStep = Math.max(activeStep, sectionToStep[i] ?? 0); } }); // Clamp to valid range activeStep = Math.min(activeStep, progressSteps.length - 1); // Update step circles progressSteps.forEach((step, i) => { if (i <= activeStep) { step.classList.add('active'); } else { step.classList.remove('active'); } }); // Animate fill line (0% at step 0, 100% at last step) const fillPct = progressSteps.length > 1 ? (activeStep / (progressSteps.length - 1)) * 100 : 0; progressFill.style.width = fillPct + '%'; } // Throttle scroll for performance let ticking = false; window.addEventListener('scroll', function () { if (!ticking) { requestAnimationFrame(function () { updateProgress(); ticking = false; }); ticking = true; } }, { passive: true }); // Run on load updateProgress(); })(); // Form Section Animations const observerOptions = { threshold: 0.2, rootMargin: '0px 0px -100px 0px' }; const observer = new IntersectionObserver(function(entries) { entries.forEach(entry => { if (entry.isIntersecting) { entry.target.style.opacity = '1'; entry.target.style.transform = 'translateY(0)'; } }); }, observerOptions); document.querySelectorAll('.form-section').forEach(section => { section.style.opacity = '0'; section.style.transform = 'translateY(30px)'; section.style.transition = 'all 0.8s cubic-bezier(0.4, 0, 0.2, 1)'; observer.observe(section); }); // Form validation with activities check document.querySelector('.booking-form').addEventListener('submit', function(e) { // Check if at least one activity is selected const selectedActivities = document.querySelectorAll('input[name="activities"]:checked'); if (selectedActivities.length === 0) { e.preventDefault(); // Scroll to activities section const activitiesSection = document.querySelector('.form-section:nth-child(4)'); activitiesSection.scrollIntoView({ behavior: 'smooth', block: 'center' }); // Highlight the section activitiesSection.style.border = '3px solid #ff4444'; setTimeout(() => { activitiesSection.style.border = ''; }, 2000); alert('Please select at least one activity that interests you.'); return false; } // Check other required fields const requiredInputs = this.querySelectorAll('input[required], select[required], textarea[required]'); for (let input of requiredInputs) { if (!input.value) { e.preventDefault(); input.focus(); input.style.borderColor = '#ff4444'; setTimeout(() => { input.style.borderColor = ''; }, 2000); alert('Please complete all required fields: ' + (input.labels[0]?.textContent || 'Missing field')); return false; } } // If all validations pass, show loading state const submitBtn = this.querySelector('.submit-button'); submitBtn.textContent = 'SUBMITTING...'; submitBtn.style.opacity = '0.7'; submitBtn.disabled = true; }); // Set fields as required (except special requests) document.querySelectorAll('input, select, textarea').forEach(el => { // Skip activities checkboxes (we validate manually) and special requests textarea if (el.name === 'activities' || el.name === 'special_requests') { return; } // Skip hidden inputs for travel_type and accommodation if (el.type === 'hidden') { return; } el.setAttribute('required', 'required'); }); // Add visual feedback for activity selection document.querySelectorAll('input[name="activities"]').forEach(checkbox => { checkbox.addEventListener('change', function() { const selectedCount = document.querySelectorAll('input[name="activities"]:checked').length; const activitySection = document.querySelector('.form-section:nth-child(4)'); if (selectedCount > 0) { activitySection.style.borderLeft = '4px solid #28a745'; } else { activitySection.style.borderLeft = ''; } }); }); // Show selected activities count function updateActivityCount() { const selectedCount = document.querySelectorAll('input[name="activities"]:checked').length; const subtitle = document.querySelector('.form-section:nth-child(4) .form-section-subtitle'); if (selectedCount > 0) { subtitle.textContent = `Select all that interest you (${selectedCount} selected)`; subtitle.style.color = '#28a745'; subtitle.style.fontWeight = '600'; } else { subtitle.textContent = 'Select all that interest you (Please select at least one)'; subtitle.style.color = '#ff4444'; subtitle.style.fontWeight = '600'; } } // Update count on checkbox change document.querySelectorAll('input[name="activities"]').forEach(checkbox => { checkbox.addEventListener('change', updateActivityCount); }); // Initial count update updateActivityCount(); // Set fields as required (except activities and special requests) document.querySelectorAll('input, select, textarea').forEach(el => { // Skip activities checkboxes and special requests textarea if (el.name === 'activities' || el.name === 'special_requests') { return; } el.setAttribute('required', 'required'); }); // ══════════════════════════════════════════════ // SMART DATE LOGIC // - Start date: cannot be in the past (today or later) // - End date: cannot be before start date // - Duration: auto-calculates and shows human-friendly text // - Date flexibility: labels replaced with professional descriptions // ══════════════════════════════════════════════ (function () { const startInput = document.querySelector('#id_start_date'); const endInput = document.querySelector('#id_end_date'); const durationInput = document.querySelector('#id_duration'); const flexSelect = document.querySelector('#id_date_flexibility'); if (!startInput || !endInput) return; // ── 1. Set today as minimum for start date ────────────────────── const today = new Date(); const todayStr = today.toISOString().split('T')[0]; startInput.setAttribute('min', todayStr); // ── 2. Update end date minimum whenever start date changes ────── function syncEndMin() { if (startInput.value) { endInput.setAttribute('min', startInput.value); // If end date is now before start, clear it and show message if (endInput.value && endInput.value < startInput.value) { endInput.value = ''; showDateError(endInput, 'End date must be after your start date'); if (durationInput) { durationInput.value = ''; updateDurationDisplay(''); } } else { clearDateError(endInput); } } else { endInput.setAttribute('min', todayStr); } } // ── 3. Calculate and display duration ─────────────────────────── function calcDuration() { if (!durationInput) return; const s = startInput.value ? new Date(startInput.value) : null; const e = endInput.value ? new Date(endInput.value) : null; if (!s || !e) { durationInput.value = ''; updateDurationDisplay(''); return; } if (e < s) { showDateError(endInput, 'End date must be after your start date'); durationInput.value = ''; updateDurationDisplay(''); return; } clearDateError(endInput); const diffMs = e - s; const diffDays = Math.round(diffMs / (1000 * 60 * 60 * 24)); const nights = diffDays; const days = diffDays + 1; // travel days inclusive // Store the numeric day count for form submission durationInput.value = days; // Human-friendly label let label = ''; if (nights === 0) { label = '1 day (same-day trip)'; } else if (nights === 1) { label = '2 days / 1 night'; } else if (nights < 7) { label = `${days} days / ${nights} nights`; } else { const weeks = Math.floor(nights / 7); const remDays = nights % 7; const weekLabel = weeks === 1 ? '1 week' : `${weeks} weeks`; const dayLabel = remDays === 0 ? '' : ` + ${remDays} day${remDays > 1 ? 's' : ''}`; label = `${days} days / ${nights} nights (${weekLabel}${dayLabel})`; } updateDurationDisplay(label); } // ── 4. Inject a display span next to duration input ───────────── let durationDisplay = document.querySelector('#duration-display'); if (durationInput && !durationDisplay) { durationDisplay = document.createElement('div'); durationDisplay.id = 'duration-display'; durationDisplay.style.cssText = ` margin-top: 8px; font-size: 13px; font-weight: 600; color: #1a7a4a; letter-spacing: 0.3px; min-height: 20px; transition: all 0.3s ease; `; durationInput.parentNode.insertBefore(durationDisplay, durationInput.nextSibling); } function updateDurationDisplay(text) { if (!durationDisplay) return; durationDisplay.textContent = text ? `✓ ${text}` : ''; } // ── 5. Error helpers ───────────────────────────────────────────── function showDateError(input, message) { let err = input.parentNode.querySelector('.date-error-msg'); if (!err) { err = document.createElement('div'); err.className = 'date-error-msg'; err.style.cssText = ` margin-top: 6px; font-size: 12px; font-weight: 600; color: #d63031; letter-spacing: 0.3px; `; input.parentNode.appendChild(err); } err.textContent = '⚠ ' + message; input.style.borderColor = '#d63031'; input.style.boxShadow = '0 0 0 3px rgba(214,48,49,0.12)'; } function clearDateError(input) { const err = input.parentNode.querySelector('.date-error-msg'); if (err) err.textContent = ''; input.style.borderColor = ''; input.style.boxShadow = ''; } // ── 6. Wire up events ──────────────────────────────────────────── startInput.addEventListener('change', function () { syncEndMin(); calcDuration(); }); endInput.addEventListener('change', function () { if (startInput.value && endInput.value < startInput.value) { showDateError(endInput, 'End date must be after your start date'); endInput.value = ''; if (durationInput) { durationInput.value = ''; updateDurationDisplay(''); } } else { clearDateError(endInput); calcDuration(); } }); // Run on load in case form is pre-filled syncEndMin(); calcDuration(); // ── 7. Date Flexibility — replace option labels with professional descriptions ── if (flexSelect) { const flexLabels = { // Common Django form value patterns → professional label 'exact': 'Fixed Dates — I need these exact dates', 'fixed': 'Fixed Dates — I need these exact dates', 'flexible_week': '± 1 Week Flexible — within a week either side', 'flexible_2week': '± 2 Weeks Flexible — within two weeks either side', 'flexible_month': '± 1 Month Flexible — within a month either side', 'flexible': 'Planning Ahead — flexible on specific dates', 'very_flexible': 'Very Flexible — open to best available timing', 'tbd': 'Dates TBD — exploring options, not yet decided', // Also handle if values come through as the original verbose labels 'Exact dates': 'Fixed Dates — I need these exact dates', 'Flexible by a week': '± 1 Week Flexible — within a week either side', 'Flexible by 2 weeks': '± 2 Weeks Flexible — within two weeks either side', 'Flexible by a month': '± 1 Month Flexible — within a month either side', 'Flexible - planning months in advance': 'Planning Ahead — flexible, not decided yet', 'Very flexible': 'Very Flexible — open to best available timing', 'To be determined': 'Dates TBD — exploring options, not yet decided', }; Array.from(flexSelect.options).forEach(opt => { const val = opt.value.trim(); const txt = opt.text.trim(); if (flexLabels[val]) { opt.text = flexLabels[val]; } else if (flexLabels[txt]) { opt.text = flexLabels[txt]; } else { // Auto-clean any remaining verbose labels let clean = txt .replace(/Flexible\s*-\s*planning months in advance/i, 'Planning Ahead — flexible, not decided yet') .replace(/Very flexible/i, 'Very Flexible — open to best available timing') .replace(/Exact dates?/i, 'Fixed Dates — I need these exact dates') .replace(/To be determined/i, 'Dates TBD — exploring options, not yet decided'); opt.text = clean; } }); } })();