> ## Documentation Index
> Fetch the complete documentation index at: https://travel-info.co.zw/llms.txt
> Use this file to discover all available pages before exploring further.

# Accommodation Directory

> Find the perfect place to stay in Zimbabwe - safari lodges, bush camps, hotels, guesthouses, campsites, and self-catering options across all regions.

export const AccommodationDirectory = ({region: initialRegion = null, showFilters = true}) => {
  const SUPABASE_URL = 'https://tdcpuzqyoodrdsxldgsh.supabase.co';
  const SUPABASE_KEY = 'sb_publishable_aNdSABNOLB3sG7OMjHN0Vw_5SDouXAL';
  const verificationTiers = {
    unverified: {
      label: 'Unverified',
      color: '#6B7280'
    },
    community: {
      label: 'Community',
      color: '#D4A574'
    },
    otp: {
      label: 'OTP Verified',
      color: '#00B0FF'
    },
    government: {
      label: 'Government',
      color: '#FFD740'
    },
    licensed: {
      label: 'Licensed',
      color: '#B388FF'
    }
  };
  const accommodationSubtypes = {
    LodgingBusiness: {
      label: 'All Accommodation',
      icon: '🏨'
    }
  };
  const amenityIcons = {
    has_pool: {
      icon: '🏊',
      label: 'Pool'
    },
    has_wifi: {
      icon: '📶',
      label: 'WiFi'
    },
    has_restaurant: {
      icon: '🍽️',
      label: 'Restaurant'
    },
    has_bar: {
      icon: '🍸',
      label: 'Bar'
    },
    has_spa: {
      icon: '💆',
      label: 'Spa'
    },
    has_gym: {
      icon: '🏋️',
      label: 'Gym'
    },
    has_generator: {
      icon: '⚡',
      label: 'Generator'
    },
    has_conference_room: {
      icon: '💼',
      label: 'Conference'
    },
    has_airstrip: {
      icon: '✈️',
      label: 'Airstrip'
    },
    is_child_friendly: {
      icon: '👶',
      label: 'Child Friendly'
    },
    pets_allowed: {
      icon: '🐾',
      label: 'Pets OK'
    }
  };
  const priceRangeLabels = {
    '$': 'Budget (under $50/night)',
    '$$': 'Mid-range ($50–$150/night)',
    '$$$': 'Upmarket ($150–$350/night)',
    '$$$$': 'Luxury ($350+/night)'
  };
  const statusToTier = status => {
    if (status === 'verified') return 'otp';
    return 'unverified';
  };
  const renderVerificationBadge = (tier, showLabel = false) => {
    const tiers = {
      community: {
        bg: '#C8956C',
        label: 'Community Verified',
        icon: <path fill="currentColor" d="M10 9a3 3 0 100-6 3 3 0 000 6zm-7 9a7 7 0 1114 0H3z" />
      },
      otp: {
        bg: '#1D9BF0',
        label: 'Identity Verified',
        icon: <path fill="currentColor" fillRule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clipRule="evenodd" />
      },
      government: {
        bg: '#F59E0B',
        label: 'Government Verified',
        icon: <path fill="currentColor" fillRule="evenodd" d="M2.166 4.999A11.954 11.954 0 0010 1.944 11.954 11.954 0 0017.834 5c.11.65.166 1.32.166 2.001 0 5.225-3.34 9.67-8 11.317C5.34 16.67 2 12.225 2 7c0-.682.057-1.35.166-2.001zm11.541 3.708a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clipRule="evenodd" />
      },
      licensed: {
        bg: '#7C3AED',
        label: 'Licensed & Verified',
        icon: <path fill="currentColor" d="M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.07 3.292a1 1 0 00.95.69h3.462c.969 0 1.371 1.24.588 1.81l-2.8 2.034a1 1 0 00-.364 1.118l1.07 3.292c.3.921-.755 1.688-1.54 1.118l-2.8-2.034a1 1 0 00-1.175 0l-2.8 2.034c-.784.57-1.838-.197-1.539-1.118l1.07-3.292a1 1 0 00-.364-1.118L2.98 8.72c-.783-.57-.38-1.81.588-1.81h3.461a1 1 0 00.951-.69l1.07-3.292z" />
      }
    };
    const t = tiers[tier];
    if (!t) return null;
    return <span className="inline-flex items-center gap-1 flex-shrink-0" title={t.label}>
        <span className="inline-flex items-center justify-center w-5 h-5 rounded-full" style={{
      backgroundColor: t.bg
    }}>
          <svg className="w-3 h-3 text-white" fill="currentColor" viewBox="0 0 20 20">{t.icon}</svg>
        </span>
        {showLabel && <span className="text-xs font-medium" style={{
      color: t.bg
    }}>{t.label}</span>}
      </span>;
  };
  const renderStars = starRating => {
    if (!starRating) return null;
    const value = typeof starRating === 'object' ? starRating.ratingValue : Number(starRating);
    if (!value || value < 1) return null;
    const stars = Math.round(value);
    return <span className="text-yellow-400 text-sm" title={`${value} star${value !== 1 ? 's' : ''}`}>
        {('★').repeat(stars)}{('☆').repeat(Math.max(0, 5 - stars))}
      </span>;
  };
  const [listings, setListings] = useState([]);
  const [establishments, setEstablishments] = useState({});
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);
  const [selected, setSelected] = useState(null);
  const [searchQuery, setSearchQuery] = useState('');
  const [priceFilter, setPriceFilter] = useState('');
  const fetchAccommodation = useCallback(async () => {
    try {
      setLoading(true);
      setError(null);
      let url = `${SUPABASE_URL}/rest/v1/local_business` + `?select=id,name,alternatename,description,place_id,telephone,email,url,logo,pricerange,verification_status` + `&businesstype=eq.LodgingBusiness&order=name.asc&limit=50`;
      const res = await fetch(url, {
        headers: {
          apikey: SUPABASE_KEY,
          Authorization: `Bearer ${SUPABASE_KEY}`,
          'Accept-Profile': 'commerce'
        }
      });
      if (!res.ok) throw new Error(`HTTP ${res.status}`);
      const data = await res.json();
      setListings(data || []);
      if (data && data.length > 0) {
        const ids = data.map(b => b.id).join(',');
        const estUrl = `${SUPABASE_URL}/rest/v1/establishment` + `?select=id,local_business_id,star_rating,number_of_rooms,checkin_time,checkout_time,` + `meal_plans,has_pool,has_wifi,has_restaurant,has_bar,has_spa,has_gym,has_generator,` + `has_laundry,has_parking,has_conference_room,has_airstrip,is_child_friendly,pets_allowed,` + `price_range,price_range_min,price_range_max,booking_url,cover_image,featured,aggregate_rating` + `&local_business_id=in.(${ids})`;
        try {
          const estRes = await fetch(estUrl, {
            headers: {
              apikey: SUPABASE_KEY,
              Authorization: `Bearer ${SUPABASE_KEY}`,
              'Accept-Profile': 'hospitality'
            }
          });
          if (estRes.ok) {
            const estData = await estRes.json();
            const estMap = {};
            (estData || []).forEach(e => {
              estMap[e.local_business_id] = e;
            });
            setEstablishments(estMap);
          }
        } catch (_) {}
      }
    } catch (err) {
      setError('Unable to load accommodation. Please try again.');
    } finally {
      setLoading(false);
    }
  }, []);
  useEffect(() => {
    fetchAccommodation();
  }, [fetchAccommodation]);
  const filtered = listings.filter(b => {
    if (priceFilter && b.pricerange !== priceFilter) return false;
    if (searchQuery) {
      const q = searchQuery.toLowerCase();
      return b.name?.toLowerCase().includes(q) || b.description?.toLowerCase().includes(q);
    }
    return true;
  });
  const renderAmenities = (est, compact = false) => {
    if (!est) return null;
    const active = Object.entries(amenityIcons).filter(([key]) => est[key]);
    if (active.length === 0) return null;
    const show = compact ? active.slice(0, 4) : active;
    return <div className={`flex flex-wrap gap-1 ${compact ? 'mt-2' : 'mt-3'}`}>
        {show.map(([key, {icon, label}]) => <span key={key} className="inline-flex items-center gap-0.5 px-2 py-0.5 bg-gray-100 dark:bg-gray-700 rounded-full text-xs text-gray-600 dark:text-gray-300" title={label}>
            {icon} {!compact && <span>{label}</span>}
          </span>)}
        {compact && active.length > 4 && <span className="px-2 py-0.5 bg-gray-100 dark:bg-gray-700 rounded-full text-xs text-gray-500 dark:text-gray-400">+{active.length - 4}</span>}
      </div>;
  };
  return <div className="space-y-6">
      {}
      {showFilters && <div className="flex flex-wrap gap-3">
          <input type="search" placeholder="Search accommodation..." value={searchQuery} onChange={e => setSearchQuery(e.target.value)} className="flex-1 min-w-[200px] px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white placeholder-gray-400 text-sm focus:outline-none focus:ring-2 focus:ring-primary-500" />
          <select value={priceFilter} onChange={e => setPriceFilter(e.target.value)} className="px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white text-sm">
            <option value="">All price ranges</option>
            {Object.entries(priceRangeLabels).map(([k, v]) => <option key={k} value={k}>{v}</option>)}
          </select>
        </div>}

      <p className="text-sm text-gray-500 dark:text-gray-400">
        {filtered.length} propert{filtered.length !== 1 ? 'ies' : 'y'} found
      </p>

      {error && <div className="p-4 bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-xl flex items-start gap-3">
          <svg className="w-5 h-5 text-red-500 flex-shrink-0 mt-0.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
            <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
          </svg>
          <div>
            <p className="text-sm font-medium text-red-700 dark:text-red-400">Couldn't load properties</p>
            <p className="text-sm text-red-600 dark:text-red-500 mt-0.5">{error}</p>
            <button onClick={fetchAccommodation} className="mt-2 text-sm font-medium text-red-700 dark:text-red-400 underline">Try again</button>
          </div>
        </div>}

      {}
      {loading ? <div className="grid grid-cols-1 sm:grid-cols-2 gap-6">
          {[...Array(6)].map((_, i) => <div key={i} className="animate-pulse bg-gray-100 dark:bg-gray-800 rounded-xl h-48" />)}
        </div> : filtered.length > 0 ? <div className="grid grid-cols-1 sm:grid-cols-2 gap-6">
          {filtered.map(b => {
    const est = establishments[b.id];
    const tier = statusToTier(b.verification_status);
    const tierInfo = verificationTiers[tier];
    const coverImg = est?.cover_image || b.logo;
    return <div key={b.id} onClick={() => setSelected(b)} className="bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 overflow-hidden hover:shadow-lg hover:border-primary-300 dark:hover:border-primary-600 transition-all cursor-pointer">
                {}
                <div className="h-36 bg-gradient-to-br from-primary-100 to-teal-100 dark:from-primary-900/30 dark:to-teal-900/30 flex items-center justify-center overflow-hidden">
                  {coverImg ? <img src={coverImg} alt={b.name} className="w-full h-full object-cover" /> : <svg className="w-12 h-12 text-primary-300 dark:text-primary-700" fill="none" stroke="currentColor" viewBox="0 0 24 24">
                      <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1} d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6" />
                    </svg>}
                </div>
                <div className="p-4">
                  <div className="flex items-center gap-1.5 flex-wrap">
                    <h3 className="font-semibold text-gray-900 dark:text-white truncate">{b.name}</h3>
                    {renderVerificationBadge(tier)}
                  </div>
                  <div className="flex flex-wrap gap-1.5 mt-1.5">
                    <span className="px-2 py-0.5 rounded-full text-xs font-medium bg-primary-100 dark:bg-primary-900/40 text-primary-700 dark:text-primary-300">Accommodation</span>
                    {est && renderStars(est.star_rating) && <span className="px-2 py-0.5 rounded-full text-xs font-medium bg-yellow-50 dark:bg-yellow-900/20 text-yellow-700 dark:text-yellow-300">{renderStars(est.star_rating)}</span>}
                    {b.pricerange && <span className="px-2 py-0.5 rounded-full text-xs font-medium bg-gray-100 dark:bg-gray-700 text-gray-600 dark:text-gray-300">{b.pricerange}/night</span>}
                    {est?.number_of_rooms && <span className="px-2 py-0.5 rounded-full text-xs font-medium bg-gray-100 dark:bg-gray-700 text-gray-600 dark:text-gray-300">{est.number_of_rooms} rooms</span>}
                  </div>
                  {b.description && <p className="mt-2 text-sm text-gray-600 dark:text-gray-300 line-clamp-2">{b.description}</p>}
                  {renderAmenities(est, true)}
                </div>
              </div>;
  })}
        </div> : searchQuery || priceFilter ? <div className="text-center py-12 bg-gray-50 dark:bg-gray-800/50 rounded-xl border border-dashed border-gray-300 dark:border-gray-700">
          <p className="text-gray-600 dark:text-gray-400 font-medium">No properties match your search</p>
          <button onClick={() => {
    setSearchQuery('');
    setPriceFilter('');
  }} className="mt-4 px-4 py-2 text-sm font-medium text-primary-600 dark:text-primary-400 border border-primary-300 dark:border-primary-700 rounded-lg hover:bg-primary-50 dark:hover:bg-primary-900/20 transition-colors">
            Clear filters
          </button>
        </div> : <div className="text-center py-14 bg-gradient-to-br from-primary-50 to-teal-50 dark:from-primary-900/20 dark:to-teal-900/20 rounded-xl border border-primary-200 dark:border-primary-800">
          <div className="w-14 h-14 mx-auto mb-4 rounded-full bg-primary-100 dark:bg-primary-900 flex items-center justify-center">
            <svg className="w-7 h-7 text-primary-600 dark:text-primary-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
              <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6" />
            </svg>
          </div>
          <h3 className="text-lg font-semibold text-gray-900 dark:text-white">Be the first listed property</h3>
          <p className="text-sm text-gray-600 dark:text-gray-400 mt-2 max-w-sm mx-auto">
            Own or manage a safari lodge, hotel, guesthouse, or camp? Get listed free and reach travelers planning their Zimbabwe trip.
          </p>
          <a href="/get-involved/business-partner-network" className="inline-flex items-center gap-2 mt-5 px-5 py-2.5 bg-primary-600 hover:bg-primary-700 text-white text-sm font-medium rounded-lg transition-colors">
            List your property
            <svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
              <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M14 5l7 7m0 0l-7 7m7-7H3" />
            </svg>
          </a>
        </div>}

      {}
      {selected && (() => {
    const est = establishments[selected.id];
    const tier = statusToTier(selected.verification_status);
    const tierInfo = verificationTiers[tier];
    return <div className="fixed inset-0 bg-black/50 flex items-center justify-center p-4 z-50" onClick={() => setSelected(null)}>
            <div className="bg-white dark:bg-gray-800 rounded-xl max-w-2xl w-full max-h-[90vh] overflow-y-auto shadow-2xl" onClick={e => e.stopPropagation()}>
              {}
              <div className="h-48 bg-gradient-to-br from-primary-200 to-teal-200 dark:from-primary-800 dark:to-teal-800 overflow-hidden">
                {est?.cover_image || selected.logo ? <img src={est?.cover_image || selected.logo} alt={selected.name} className="w-full h-full object-cover" /> : <div className="w-full h-full flex items-center justify-center">
                    <svg className="w-16 h-16 text-white/50" fill="none" stroke="currentColor" viewBox="0 0 24 24">
                      <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1} d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6" />
                    </svg>
                  </div>}
              </div>

              <div className="sticky top-0 bg-white dark:bg-gray-800 border-b border-gray-200 dark:border-gray-700 px-6 py-4 flex justify-between items-start">
                <div>
                  <div className="flex items-center gap-2 flex-wrap">
                    <h2 className="text-xl font-bold text-gray-900 dark:text-white">{selected.name}</h2>
                    {renderVerificationBadge(tier, true)}
                  </div>
                  <div className="flex flex-wrap gap-1.5 mt-1.5">
                    <span className="px-2 py-0.5 rounded-full text-xs font-medium bg-primary-100 dark:bg-primary-900/40 text-primary-700 dark:text-primary-300">Accommodation</span>
                    {est && renderStars(est.star_rating) && <span className="text-yellow-400 text-sm">{renderStars(est.star_rating)}</span>}
                    {est?.number_of_rooms && <span className="px-2 py-0.5 rounded-full text-xs font-medium bg-gray-100 dark:bg-gray-700 text-gray-600 dark:text-gray-300">{est.number_of_rooms} rooms</span>}
                    {selected.pricerange && <span className="px-2 py-0.5 rounded-full text-xs font-medium bg-gray-100 dark:bg-gray-700 text-gray-600 dark:text-gray-300">{selected.pricerange}/night</span>}
                  </div>
                </div>
                <button onClick={() => setSelected(null)} className="text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 text-2xl leading-none" aria-label="Close">×</button>
              </div>

              <div className="p-6 space-y-5">
                {selected.description && <div>
                    <h3 className="text-xs font-semibold text-gray-500 dark:text-gray-400 uppercase tracking-wide mb-2">About</h3>
                    <p className="text-gray-600 dark:text-gray-300 leading-relaxed">{selected.description}</p>
                  </div>}

                {est && (est.checkin_time || est.checkout_time) && <div>
                    <h3 className="text-xs font-semibold text-gray-500 dark:text-gray-400 uppercase tracking-wide mb-2">Check-in / Check-out</h3>
                    <div className="flex gap-6 text-sm text-gray-700 dark:text-gray-300">
                      {est.checkin_time && <span>Check-in: <strong>{est.checkin_time}</strong></span>}
                      {est.checkout_time && <span>Check-out: <strong>{est.checkout_time}</strong></span>}
                    </div>
                  </div>}

                {est && <div>
                    <h3 className="text-xs font-semibold text-gray-500 dark:text-gray-400 uppercase tracking-wide mb-2">Facilities</h3>
                    {renderAmenities(est, false)}
                  </div>}

                {est?.meal_plans && est.meal_plans.length > 0 && <div>
                    <h3 className="text-xs font-semibold text-gray-500 dark:text-gray-400 uppercase tracking-wide mb-2">Meal Plans</h3>
                    <div className="flex flex-wrap gap-2">
                      {est.meal_plans.map(plan => <span key={plan} className="px-3 py-1 bg-primary-50 dark:bg-primary-900/30 text-primary-700 dark:text-primary-300 rounded-full text-sm">{plan}</span>)}
                    </div>
                  </div>}

                <div className="border-t border-gray-200 dark:border-gray-700 pt-5">
                  <h3 className="text-xs font-semibold text-gray-500 dark:text-gray-400 uppercase tracking-wide mb-3">Contact & booking</h3>
                  <div className="flex flex-wrap gap-3">
                    {est?.booking_url && <a href={est.booking_url} target="_blank" rel="noopener noreferrer" className="inline-flex items-center gap-2 px-4 py-2 bg-primary-600 text-white rounded-lg hover:bg-primary-700 transition-colors text-sm font-medium">
                        Book Now
                      </a>}
                    {selected.email && <a href={`mailto:${selected.email}`} className="inline-flex items-center gap-2 px-4 py-2 bg-gray-600 text-white rounded-lg hover:bg-gray-700 transition-colors text-sm font-medium">
                        Email
                      </a>}
                    {selected.telephone && <a href={`tel:${selected.telephone}`} className="inline-flex items-center gap-2 px-4 py-2 bg-green-600 text-white rounded-lg hover:bg-green-700 transition-colors text-sm font-medium">
                        Call
                      </a>}
                    {selected.url && <a href={selected.url} target="_blank" rel="noopener noreferrer" className="inline-flex items-center gap-2 px-4 py-2 border border-gray-300 dark:border-gray-600 text-gray-700 dark:text-gray-300 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors text-sm font-medium">
                        Website
                      </a>}
                  </div>
                </div>
              </div>
            </div>
          </div>;
  })()}
    </div>;
};

Find your perfect stay - from luxury safari lodges to budget-friendly campsites. Browse verified accommodations across Zimbabwe with detailed information and direct contact.

<Note>
  Use the filters below to find accommodation by type, location, and price range.
</Note>

## Browse Accommodation

<AccommodationDirectory />

***

## Accommodation Types

<CardGroup cols={4}>
  <Card title="Safari Lodges" icon="house">
    Luxury lodges in prime wildlife areas
  </Card>

  <Card title="Bush Camps" icon="campground">
    Intimate safari camps close to nature
  </Card>

  <Card title="Hotels" icon="hotel">
    City and resort hotels with full amenities
  </Card>

  <Card title="Guesthouses" icon="house-user">
    Cozy B\&Bs with personal service
  </Card>

  <Card title="Campsites" icon="tent">
    Camping in parks and private areas
  </Card>

  <Card title="Self-Catering" icon="kitchen-set">
    Independent cottages and apartments
  </Card>

  <Card title="Hostels" icon="bed">
    Budget-friendly backpacker options
  </Card>

  <Card title="Resorts" icon="umbrella-beach">
    Full-service resort experiences
  </Card>
</CardGroup>

***

## Popular Regions

<CardGroup cols={3}>
  <Card title="Victoria Falls" icon="water">
    Adventure capital with diverse accommodation options
  </Card>

  <Card title="Hwange" icon="paw">
    Safari lodges near Zimbabwe's largest park
  </Card>

  <Card title="Mana Pools" icon="tree">
    Remote camps in World Heritage wilderness
  </Card>

  <Card title="Eastern Highlands" icon="mountain">
    Mountain lodges and forest retreats
  </Card>

  <Card title="Lake Kariba" icon="sailboat">
    Houseboats and lakeside lodges
  </Card>

  <Card title="Harare & Bulawayo" icon="city">
    City hotels and urban guesthouses
  </Card>
</CardGroup>

***

## List Your Property

Own or manage accommodation in Zimbabwe? Join our directory and reach travelers worldwide.

<Card title="List Your Property" icon="building" href="/get-involved/business-partner-network">
  Free listing with optional verification and promotional packages available.
</Card>
