Fallagassrini Bypass Shell

echo"
Fallagassrini
";
Current Path : /home/h/a/p/happyrenas/find.myreco.online/v2/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/h/a/p/happyrenas/find.myreco.online/v2/js/script_halt.js

/* =============================================================================
   Find.MyReco -
   - Autocomplete villes (JSON prefix)
   - Recherche (cache BDD)
   - Affichage des cards + chips équipements + favoris
   - Modale contact (demandes de disponibilité)
   - Carte Leaflet : markers + interactions cards <-> markers
   - Photo caching (ajax/photo_cache.php)

   Organisation :
   - Configuration / constantes
   - Helpers (DOM, chaînes, formatage)
   - Etat & filtres (UI home/results)
   - Rendering (cards, mini-pills, erreurs)
   - Autocomplete
   - Photos (lazy cache)
   - Carte Leaflet
   - Home (top villes / sections)
   - Recherche (API)
   - Modale contact
   - Events & init
============================================================================= */

/* ----------------------------------------------------------------------------
   Config / Flags
---------------------------------------------------------------------------- */
const DEBUG_PHOTOS = false; // Debug box sur les cards (désactiver en prod)

const SITE_BASE_URL = 'https://www.myreco.online';
const PLACEHOLDER_IMG = 'https://find.myreco.online/img/placeholder.svg';


// Likes en mémoire (perdus si refresh)
const likedTokens = new Set();       // token likés
const likedAtByToken = new Map();    // token -> timestamp (pour le tri "récemment liké")


/* ----------------------------------------------------------------------------
   Helpers (string / DOM / formatting)
---------------------------------------------------------------------------- */
const $ = (s) => document.querySelector(s);

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


function toAbsoluteUrl(url){
  const u = String(url ?? '').trim();
  if (!u) return '';
  if (u.startsWith('http://') || u.startsWith('https://')) return u;
  if (u.startsWith('//')) return 'https:' + u;
  if (u.startsWith('/')) return SITE_BASE_URL + u;
  return SITE_BASE_URL + '/' + u;
}

function normalizePhotosLocal(arr){
  if (!Array.isArray(arr)) return [];
  return arr
    .map(x => String(x ?? '').trim())
    .filter(Boolean);
}

function buildLocalPhotoUrl(relPath){
  const rel = String(relPath ?? '').trim();
  if (!rel) return '';
  return SITE_BASE_URL + '/upload/hebergement_multiple/' + rel;
}

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

function normalizeForPrefix2(str){
  const city = (str.split(',')[0] || '').trim().toLowerCase();
  if (city.length < 2) return '';
  const deacc = city.normalize('NFD').replace(/[\u0300-\u036f]/g, '');
  const clean = deacc.replace(/[^a-z]/g, '');
  return clean.slice(0,2);
}

function formatRating(rating){
  
  return rating;
}

function formatReviews(reviews){
  const n = parseInt(reviews, 10);
  if (!isFinite(n) || n <= 0) return '';
  return n.toLocaleString('fr-FR');
}

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

function initContactDates(root = document){
  const debut = root.querySelector('#date_debut');
  const fin   = root.querySelector('#date_fin');
  if (!debut || !fin) return;

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

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

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

  // valeurs par défaut (si vide)
  if (!debut.value) debut.value = todayStr;
  if (!fin.value)   fin.value   = plus3Str;

  // min
  debut.min = todayStr;
  fin.min   = todayStr;

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

  debut.removeEventListener('change', syncMinEnd); // évite de cumuler si rappelé
  debut.addEventListener('change', syncMinEnd);
  syncMinEnd();
}

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

  let s = String(str).toLowerCase();

  // enlever accents
  s = s.normalize('NFD').replace(/[\u0300-\u036f]/g, '');

  // remplacer tirets / apostrophes par espace
  s = s.replace(/[-'’]/g, ' ');

  // enlever tout sauf lettres/chiffres/espace
  s = s.replace(/[^a-z0-9 ]/g, '');

  // compacter espaces
  s = s.replace(/\s+/g, ' ').trim();

  return s;
}

/* ----------------------------------------------------------------------------
   Equipements (chips mini)
---------------------------------------------------------------------------- */
function normalizeEquipements(eqs){
  if (!Array.isArray(eqs)) return [];
  return eqs.map(x => String(x ?? '').trim()).filter(Boolean);
}

function renderEquipementsChips(eqs, max = 3){
  const arr = normalizeEquipements(eqs);
  if (!arr.length) return '';

  const shown = arr.slice(0, max);
  const more  = arr.length - shown.length;

  const chips = shown.map(t => `<span class="chip-mini">${escapeHtml(t)}</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>`;
}

//16/12/2025
function fixBrokenUnicode(s){
  // transforme "Pu00e9tanque" -> "Pétanque", "Flu00e9chette" -> "Fléchette"
  return String(s ?? '').replace(/u([0-9a-fA-F]{4})/g, function(_, hex){
    return String.fromCharCode(parseInt(hex, 16));
  });
}

function normalizeList(arr){
  if (!Array.isArray(arr)) return [];
  return arr
    .map(function(x){
      return fixBrokenUnicode(String(x ?? '')).trim();
    })
    .filter(Boolean);
}


function syncContactButtons(){
  const selected = new Set(contactSelected.map(x => String(x.token)));
  document.querySelectorAll('.btnContact[data-token]').forEach(btn => {
    const t = String(btn.getAttribute('data-token') || '');
    const on = selected.has(t);
    btn.classList.toggle('is-active', on);
    btn.setAttribute('aria-pressed', on ? 'true' : 'false');
  });
}


/* --- Parking --- */
function buildParkingHtml(parking){
  const p = String(parking ?? '').toLowerCase();

  // adapte ici si tes valeurs sont différentes (ex: "ouvert", "ferme", "parking_ouvert", etc.)
  let label = '-';
  let icon = 'bi bi-p-circle';
  let cls = 'mini-pill';

  if (!p || p.includes('aucun')) {
    label = 'Pas de parking';
    icon = 'bi bi-ban';
    cls += ' is-muted';
  } else if (p.includes('ouvert')) {
    label = 'Parking ouvert';
    icon = 'bi bi-p-circle-fill';
  } else if (p.includes('ferme') || p.includes('fermé')) {
    label = 'Parking fermé';
    icon = 'bi bi-p-square-fill';
  } else {
    label = parking; // fallback : affiche la valeur brute
    icon = 'bi bi-p-circle';
  }

  return '<span class="' + cls + '"><i class="' + icon + '"></i><span class="lbl">' + escapeHtml(label) + '</span></span>';
}

/* --- Facts: capacité / chambres / sdb --- */
function buildFactsHtml(r){
  const d = (r.donnees && typeof r.donnees === 'object') ? r.donnees : {};
  const capacite = parseInt(d.capacite, 10);
  const chambres = parseInt(d.nb_chambres, 10);
  const sdb      = parseInt(d.nb_sdb, 10);

  const out = [];
  out.push('<div class="facts-row">');

  if (isFinite(capacite) && capacite > 0){
    out.push('<span class="mini-pill"><i class="bi bi-people-fill"></i><span class="lbl">' + capacite + ' pers.</span></span>');
  }
  if (isFinite(chambres) && chambres > 0){
    out.push('<span class="mini-pill"><i class="bi bi-door-closed-fill"></i><span class="lbl">' + chambres + ' ch.</span></span>');
  }
  if (isFinite(sdb) && sdb > 0){
    out.push('<span class="mini-pill"><i class="bi bi-droplet-fill"></i><span class="lbl">' + sdb + ' sdb</span></span>');
  }

  // parking (toujours visible)
  out.push(buildParkingHtml(r.parking));

  out.push('</div>');
  return out.join('');
}

/* --- Icônes pour équipements / activités --- */
function iconForItem(label){
  const t = String(label || '').toLowerCase();

  // Équipements
  if (t.includes('piscine')) return 'bi bi-water';
  if (t.includes('jacuzzi')) return 'bi bi-droplet';
  if (t.includes('hammam'))  return 'bi bi-cloud-haze2';
  if (t.includes('barbecue'))return 'bi bi-fire';
  if (t.includes('plancha')) return 'bi bi-fire';

  // Activités
  if (t.includes('ping')) return 'bi bi-circle';
  if (t.includes('pétanque') || t.includes('petanque')) return 'bi bi-bullseye';
  if (t.includes('babyfoot')) return 'bi bi-controller';
  if (t.includes('billard')) return 'bi bi-record-circle';
  if (t.includes('fléchette') || t.includes('flechette')) return 'bi bi-bullseye';
  if (t.includes('raquette')) return 'bi bi-activity';
  if (t.includes('toboggan')) return 'bi bi-water';

  return 'bi bi-check2-circle';
}

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

  // kind = "equip" ou "act" (juste pour un petit titre optionnel)
  const title = (kind === 'act') ? 'Activités' : 'Équipements';

  let out = [];
  out.push('<div class="mini-line">');
  out.push('<div class="mini-title">' + title + '</div>');
  out.push('<div class="mini-wrap">');

  for (let i=0; i<list.length; i++){
    const lbl = list[i];
    out.push(
      '<span class="mini-pill">' +
        '<i class="' + iconForItem(lbl) + '"></i>' +
        '<span class="lbl">' + escapeHtml(lbl) + '</span>' +
      '</span>'
    );
  }

  out.push('</div></div>');
  return out.join('');
}




/* ----------------------------------------------------------------------------
   DOM refs
---------------------------------------------------------------------------- */
const input        = $('#qCity');
const suggestBox   = $('#suggestBox');
const grid         = $('#grid');
const statusEl     = $('#status');
const metaEl       = $('#meta');
const errorBox     = $('#errorBox');
const errorMsg     = $('#errorMsg');
const btnClearCache= $('#btnClearCache');
const radiusSelect = $('#qRadius');
const guestsSelect = $('#qGuests');

const splitEl      = document.querySelector('#resultsSplit');
const btnToggleMap = document.querySelector('#btnToggleMap');
const btnShuffle   = document.querySelector('#btnShuffle');

const filtersBars = document.querySelectorAll('.filters');

const resultsBlock = document.getElementById('resultsBlock');
const homeSections = document.getElementById('homeSections');

let citySelected = false;

/* ----------------------------------------------------------------------------
   Filters state
   - types: Set("hotel"|"location"|"camping")
     * vide => pas de filtre => affiche tout
   - piscine: boolean
---------------------------------------------------------------------------- */
const filters = {
  types: new Set(),
  piscine: false,
};

function getActiveTypes(){
  return [...filters.types];
}

function setFilterBtnState(){
  document.querySelectorAll('.js-filter[data-filter="type"]').forEach(btn => {
    const t = btn.getAttribute('data-value');
    btn.classList.toggle('active', filters.types.has(t));
  });

  document.querySelectorAll('.js-filter[data-filter="piscine"]').forEach(btn => {
    btn.classList.toggle('active', !!filters.piscine);
  });
}
function resetFilters(){
  filters.types.clear();
  filters.piscine = false;
  setFilterBtnState();
}

/* ----------------------------------------------------------------------------
   UI states (Home / Results)
---------------------------------------------------------------------------- */
function setModeHome(){
  if (resultsBlock) resultsBlock.style.display = 'none';
  if (homeSections) homeSections.style.display = 'block';

  // Reset UI "Résultats"
  grid.innerHTML = '';
  statusEl.textContent = '—';
  metaEl.textContent = '—';
  btnClearCache.style.display = 'none';
}

function setModeResults(){
  if (homeSections) homeSections.style.display = 'none';
  if (resultsBlock) resultsBlock.style.display = 'block';
  
   // Carte ON par défaut en mode résultats
  if (splitEl) splitEl.classList.add('is-map');
  if (btnToggleMap) btnToggleMap.innerHTML = '<i class="bi bi-list me-1"></i>Liste';
}

/* ----------------------------------------------------------------------------
   UI helpers (errors / loading)
---------------------------------------------------------------------------- */
function setError(msg){
  errorMsg.textContent = msg;
  errorBox.style.display = 'block';
  setModeHome();
}

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

function setLoading(){
  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>';
}


/* ----------------------------------------------------------------------------
   Like (UI only)
---------------------------------------------------------------------------- */
function toggleLike(btn){
  // feedback sur le coeur (plus lisible)
  try{
    btn.animate(
      [{ transform: 'scale(1)' }, { transform: 'scale(1.18)' }, { transform: 'scale(1)' }],
      { duration: 220, easing: 'cubic-bezier(.2,.8,.2,1)' }
    );
  }catch(e){}

  btn.classList.toggle('liked');

  const i = btn.querySelector('i');
  const liked = btn.classList.contains('liked');

  // Trouve la colonne Bootstrap contenant la card
  const card = btn.closest('.card-heb');
  const col  = card ? card.parentElement : null;
  const wrap = col ? col.parentElement : null;

  // token (sert à la mémoire + markers)
  const token = card ? normToken(card.getAttribute('data-token')) : '';


  if (liked){

    i?.classList.remove('bi-heart');
    i?.classList.add('bi-heart-fill');

    // --- Mémoire (sans localStorage) ---
    if (token){
      likedTokens.add(token);
      likedAtByToken.set(token, Date.now());
    }

    if (col){
      col.classList.add('is-liked');
      col.dataset.likedAt = String(likedAtByToken.get(token) || Date.now()); // pour mettre celle-ci en 1ère
    }
    if (card){
      card.classList.add('is-active'); // état favori (persistant sur la page)
      pulseCard(card);                 // pulse visuel
    }

  } else {
    i?.classList.add('bi-heart');
    i?.classList.remove('bi-heart-fill');

    // --- Mémoire (sans localStorage) ---
    if (token){
      likedTokens.delete(token);
      likedAtByToken.delete(token);
    }

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

  // --- Sync marker color with "liked" ---
  const m = token ? markerByToken.get(token) : null;
  if (m){
    m.setStyle(liked ? MARKER_STYLE_LIKED : MARKER_STYLE_DEFAULT);
  }

  // Réordonne avec animation, et garde la fiche "sous le doigt" (pas de scroll brutal)
  if (wrap && col){
    const beforeTop = col.getBoundingClientRect().top;

    flipReorder(wrap, () => sortCardsInWrap(wrap), { duration: 280 });

    const afterTop = col.getBoundingClientRect().top;
    const delta = afterTop - beforeTop;

    // ajuste le scroll pour que la fiche reste à la même place à l'écran
    if (Math.abs(delta) > 1){
      window.scrollBy({ top: delta, left: 0 });
    }

    // si malgré tout elle sort de l'écran, on fait un scroll doux minimal
    requestAnimationFrame(() => ensureVisibleIfNeeded(col, 120, 16));
  } else {
    requestAnimationFrame(() => ensureVisibleIfNeeded(card || btn, 120, 16));
  }
}




/* ----------------------------------------------------------------------------
   Card rendering
   - photo_src uniquement (jamais r.photo externe)
---------------------------------------------------------------------------- */

function buildRatingHtml(r){
  var ratingTxt  = formatRating(r.rating);
  var reviewsTxt = formatReviews(r.reviews);

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

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

  return '';
}


function buildDebugHtml(r){
  if (!DEBUG_PHOTOS) return '';

  var token = String(r.token ?? '');
  var photo = String(r.photo ?? '').slice(0, 120);
  var ploc  = String(r.photo_locale ?? '').slice(0, 120);
  var psrc  = String(r.photo_src ?? '').slice(0, 120);
  var okTxt = r.photo_local_exists ? '✅ true' : '❌ false';

  return '<div class="debug-box">' +
           '<div><strong>token:</strong> ' + escapeHtml(token) + '</div>' +
           '<div><strong>photo:</strong> ' + escapeHtml(photo) + '</div>' +
           '<div><strong>photo_locale:</strong> ' + escapeHtml(ploc) + '</div>' +
           '<div><strong>photo_src:</strong> ' + escapeHtml(psrc) + '</div>' +
           '<div><strong>local_exists:</strong> ' + okTxt + '</div>' +
         '</div>';
}


function cardHtml(r){
  var imgSrc = '';
  if (r.photo_src && String(r.photo_src).trim() !== ''){
    imgSrc = toAbsoluteUrl(r.photo_src);
  }
  if (!imgSrc) imgSrc = PLACEHOLDER_IMG;

  var origin  = r.origine ? '<span class="tag">Origine: ' + escapeHtml(r.origine) + '</span>' : '';
  var type    = r.type_hebergement ? '<span class="mini-pill">' + escapeHtml(r.type_hebergement) + '</span>' : '';
  var parking = r.parking ? '<span class="tag"><i class="bi bi-car-front me-1"></i>' + escapeHtml(r.parking) + '</span>' : '';

  var price = (parseFloat(r.tarif_nuit || 0) > 0) ? (escapeHtml(r.tarif_nuit) + '€') : '';

  var ratingHtml = buildRatingHtml(r);
  var eqHtml     = renderEquipementsChips(r.equipements, 3);

  var factsHtml = buildFactsHtml(r);
  var equipAll  = renderMiniPills(r.equipements, 'equip');
  var actAll    = renderMiniPills(r.activites, 'act');

  var dbgHtml    = buildDebugHtml(r);

  // IMPORTANT: token "raw" pour le Set/Map, puis token échappé pour l'HTML
  var rawToken = normToken(r.token);
  
  
  
  var extraPhotos = normalizePhotosLocal(r.photos_local_json);
  var hasExtraPhotos = extraPhotos.length > 0;

  var isLiked  = likedTokens.has(rawToken);
  var likedAt  = likedAtByToken.get(rawToken) || 0;

  var token = escapeHtml(rawToken);
  
  var photosBadgeHtml = hasExtraPhotos
  ? '<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>'
  : '';
  var name  = escapeHtml(r.name);
  var ville = escapeHtml(r.ville);
  //var cc    = escapeHtml(r.country_code);
  var cc    = '';
  var pc    = r.postal_code ? '(' + escapeHtml(r.postal_code) + ')' : '';

  //var catHtml = r.category ? '<span class="tag">' + escapeHtml(r.category) + '</span>' : '';
  var catHtml = '';

  var priceHtml = price
    ? ('<strong>' + price + '</strong> <span style="color:var(--muted)">/ nuit</span>')
    : '<span style="color:var(--muted)">—</span>';

  return ''
    + '<div class="col-12 col-sm-6 col-lg-4'
      + (isLiked ? ' is-liked' : '')
      + '"'
      + (isLiked ? (' data-liked-at="' + String(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='" + PLACEHOLDER_IMG + "';\" />"
	+ '      ' + photosBadgeHtml

    + '      <div class="top-badges">'
    + '        <span class="badge-soft"><i class="bi bi-signpost-2 me-1"></i>' + escapeHtml(r.distance) + ' km</span>'
    + '        <div class="d-flex align-items-center gap-2">'
    + '          ' + (ratingHtml || '')
    + '          <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 + ' ' + pc + '</div>'
    +          (factsHtml || '')
    +          (equipAll || '')
    +          (actAll || '')

    + '      <div class="tags">'
    + '        ' + catHtml
    + '      </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="' + cc + '"'
+ '                data-capacite="' + escapeHtml((r.donnees && r.donnees.capacite) ? r.donnees.capacite : '') + '"'
+ '                data-chambres="' + escapeHtml((r.donnees && r.donnees.nb_chambres) ? r.donnees.nb_chambres : '') + '"'
+ '                data-sdb="' + escapeHtml((r.donnees && r.donnees.nb_sdb) ? r.donnees.nb_sdb : '') + '"'
+ '                data-parking="' + escapeHtml(r.parking || '') + '"'
+ '                data-type="' + escapeHtml(r.type_hebergement || '') + '"'
+ '                data-prix="' + escapeHtml(r.tarif_nuit || '') + '">'
+ '          Contacter l’hôte'
+ '        </button>'
    + '      </div>'

    + (dbgHtml ? ('      ' + dbgHtml) : '')

    + '    </div>'
    + '  </div>'
    + '</div>';
}




function renderResultsInto(results, targetEl, limit = 10){
  const arr = Array.isArray(results) ? results.slice(0, limit) : [];

  if (!arr.length){
    targetEl.innerHTML = `<div class="col-12"><div class="alert alert-light border">Aucun hébergement trouvé.</div></div>`;
	// Mémorise l'ordre initial des colonnes (position d'origine)
		[...targetEl.children].forEach((col, idx) => {
		  col.dataset.order = String(idx);
		});
    return;
  }

  targetEl.innerHTML = arr.map(cardHtml).join('');

  // Mémorise l'ordre initial des colonnes (position d'origine)
  [...targetEl.children].forEach((col, idx) => {
    col.dataset.order = String(idx);
  });
  targetEl.querySelectorAll('.like').forEach(btn => {
	  btn.addEventListener('click', (e) => {
		e.preventDefault();
		e.stopPropagation(); // évite click carte / map
		toggleLike(btn);
	  });
	});
	
	syncContactButtons();


}

function renderResults(results){
  renderResultsInto(results, grid, 500);
}

// Scroll minimal : uniquement si l'élément sort de l'écran
function ensureVisibleIfNeeded(el, offset = 120, padding = 16){
  if (!el) return;
  const r = el.getBoundingClientRect();

  // déjà suffisamment visible -> on ne fait rien
  const topLimit = offset;
  const botLimit = window.innerHeight - padding;

  if (r.top < topLimit){
    const y = window.scrollY + (r.top - topLimit);
    window.scrollTo({ top: y, behavior: 'smooth' });
    return;
  }
  if (r.bottom > botLimit){
    const y = window.scrollY + (r.bottom - botLimit);
    window.scrollTo({ top: y, behavior: 'smooth' });
  }
}

// FLIP animation pour rendre le réordonnancement lisible
function flipReorder(wrap, mutateFn, opts = {}){
  if (!wrap || typeof mutateFn !== 'function') return;

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

  const before = new Map();
  [...wrap.children].forEach(el => before.set(el, el.getBoundingClientRect()));

  mutateFn();

  const afterEls = [...wrap.children];
  afterEls.forEach(el => {
    const a = el.getBoundingClientRect();
    const b = before.get(el);
    if (!b) return;

    const dx = b.left - a.left;
    const dy = b.top  - a.top;

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

function pulseCard(card){
  if (!card) return;
  card.classList.remove('pulse');
  // force reflow pour relancer l'animation
  void card.offsetWidth;
  card.classList.add('pulse');
  window.setTimeout(() => card.classList.remove('pulse'), 900);
}

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

  const cols = [...wrap.children];

  cols.sort((a, b) => {
    const al = a.classList.contains('is-liked');
    const bl = b.classList.contains('is-liked');

    // likés d'abord
    if (al !== bl) return bl - al;

    // si tous les deux likés : le plus récemment liké en premier
    if (al && bl){
      return Number(b.dataset.likedAt || 0) - Number(a.dataset.likedAt || 0);
    }

    // sinon : ordre initial
    return Number(a.dataset.order || 0) - Number(b.dataset.order || 0);
  });

  cols.forEach(c => wrap.appendChild(c));
}


/* ----------------------------------------------------------------------------
   Autocomplete (prefix JSON)
---------------------------------------------------------------------------- */
let prefixCache = {};

async function loadPrefix(prefix2){
  if (!prefix2) return [];
  if (prefixCache[prefix2]) return prefixCache[prefix2];

  const url = `includes/data/villes_prefix/villes_${prefix2}.json`;
  try{
    const res = await fetch(url, {cache:'force-cache'});
    if (!res.ok) return [];
    const data = await res.json();
    prefixCache[prefix2] = Array.isArray(data) ? data : [];
    return prefixCache[prefix2];
  }catch(e){
    return [];
  }
}

function showSuggestions(items){
  if (!suggestBox) {
    return;
  }

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

  var html = '';

  for (var i = 0; i < items.length; i++){
    var item = items[i];

    var city = '';
    var postalCode = '';
    var cc = '';
    var value = '';
    var label = '';

    // Nouveau format JSON objet
    if (typeof item === 'object' && item !== null) {
      city = String(item.city || '').trim();
      postalCode = String(item.postal_code || '').trim();
      cc = String(item.country_code || '').trim();
      value = String(item.value || (city + ', ' + postalCode + ', ' + cc)).trim();
      label = String(item.label || (city + ' (' + postalCode + '), ' + cc)).trim();
    }

    // Ancien format JSON texte : "Ville, FR"
    else {
      value = String(item || '').trim();
      var parts = value.split(',');
      city = (parts[0] || '').trim();
      cc = (parts[1] || '').trim();
      label = value;
    }

    html += ''
      + '<div class="item" data-val="' + escapeHtml(value) + '">'
      + '  <div><strong>' + escapeHtml(city) + '</strong></div>'
      + '  <small>' + escapeHtml(postalCode ? postalCode + ' · ' + cc : cc) + '</small>'
      + '</div>';
  }

  suggestBox.innerHTML = html;
  suggestBox.style.display = 'block';
}


/* ----------------------------------------------------------------------------
   Photos (lazy caching)
   - Appelle ajax/photo_cache.php (direct -> fallback google côté PHP)
---------------------------------------------------------------------------- */
async function cacheMissingPhotos(results){
  const queue = (results || [])
    .filter(r => (!r.photo_locale || String(r.photo_locale).trim() === '') && r.photo && r.token)
    .map(r => ({token: String(r.token), table: 'heb'}));

  if (!queue.length) return;

  let idx = 0;
  const concurrency = 2;

  async function worker(){
    while (idx < queue.length){
      const job = queue[idx++];
      try{
        const form = new FormData();
        form.append('token', job.token);
        form.append('table', job.table);

        const res = await fetch('ajax/photo_cache.php', {method:'POST', body: form});
        const data = await res.json();

        if (data.ok && data.url){
          let imgEl = null;
          try{
            imgEl = document.querySelector(`img.heb-img[data-token="${CSS.escape(job.token)}"]`);
          }catch(e){
            imgEl = Array.from(document.querySelectorAll('img.heb-img'))
              .find(x => x.getAttribute('data-token') === job.token) || null;
          }
          if (imgEl) imgEl.src = toAbsoluteUrl(data.url);
        }
      }catch(e){}
    }
  }

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

/* ----------------------------------------------------------------------------
   Leaflet Map
---------------------------------------------------------------------------- */
let map = null;
let markersLayer = null;
let markerByToken = new Map();
let mapReady = false;

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

let lastResults = [];

function getResultByToken(token){
  token = normToken(token);
  if (!token) return null;
  return (lastResults || []).find(r => normToken(r.token) === token) || null;
}

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

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

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

  markersLayer = L.layerGroup().addTo(map);
  mapReady = true;
}

function clearMap(){
  if (!mapReady) return;
  markersLayer.clearLayers();
  markerByToken.clear();
}

function updateMap(results){
  if (!mapReady) return;

  clearMap();

  const bounds = [];
  let added = 0;

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

    const token = normToken(r.token);

    const title = String(r.name || '');

    //  si déjà liké, on le crée directement en rouge
    const isLiked = likedTokens.has(token);

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

    m.bindTooltip(title, {direction:'top', offset:[0,-10], opacity:0.95});

    markerByToken.set(token, m);
    bounds.push([lat, lon]);
    added++;

    m.on('click', () => {
      const card = document.querySelector(`.card-heb[data-token="${CSS.escape(token)}"]`);
      if (card){
        card.scrollIntoView({behavior:'smooth', block:'center'});
        card.classList.add('is-active');
        setTimeout(() => card.classList.remove('is-active'), 900);
      }
    });
  });

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

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

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


function bindCardsToMap(){
  document.querySelectorAll('.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 m = markerByToken.get(token);
      if (m) m.setStyle({radius:11});
    });

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

    card.addEventListener('click', (e) => {
      if (e.target.closest('a')) return;
      const m = markerByToken.get(token);
      if (m){
        map.panTo(m.getLatLng(), {animate:true, duration:0.5});
        m.openTooltip();
      }
    });
  });
}

/* ----------------------------------------------------------------------------
   Home: Top villes (sections)
---------------------------------------------------------------------------- */
async function loadHomeTop3(){
  const home = document.getElementById('homeSections');
  if (!home) return;

  home.innerHTML = '';

  try{
    const res = await fetch('ajax/top_villes.php?limit=5', {headers:{'Accept':'application/json'}});
    const data = await res.json();

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

    for (const it of data.items){
      const ville_pays = it.ville_pays;
      const rayon = parseInt(it.rayon_km, 10) || 10;

      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(ville_pays) + '" data-rayon="' + escapeHtml(rayon) + '" data-voyageurs="' + (it.nombre_voyageurs || 0) + '">'
	  + '    Hébergements autour de <span style="color:var(--accent)">“' + escapeHtml(ville_pays) + '”</span>'
	  + '  </h5>'
	  + '  <div class="small" style="color:var(--muted)">Rayon ' + escapeHtml(rayon) + ' km'
+ (it.nombre_voyageurs ? ' • ' + it.nombre_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>';

      home.appendChild(section);
	  
	  const clickable = section.querySelector('.home-link');

		if (clickable) {
		  clickable.addEventListener('click', () => {
			input.value = clickable.dataset.ville;
			setRadiusValue(clickable.dataset.rayon);

			// 👇 important (comme top villes)
			const g = document.getElementById('qGuests');
			if (g && clickable.dataset.voyageurs) {
			  g.value = clickable.dataset.voyageurs;
			}

			doSearch({forceLive:false});

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

      const targetRow = section.querySelector('[data-home-grid="1"]');

      const url = `ajax/recherche_hebergements.php?ville_pays=${encodeURIComponent(ville_pays)}&rayon_km=${encodeURIComponent(rayon)}`;
      const r2  = await fetch(url, {headers:{'Accept':'application/json'}});
      const d2  = await r2.json();

      const results = (d2 && d2.ok && Array.isArray(d2.results)) ? d2.results : [];
      renderResultsInto(results, targetRow, 6);
      cacheMissingPhotos(results);
    }
  }catch(e){
    home.innerHTML = '';
  }
}

/* ----------------------------------------------------------------------------
   Top villes (chips) — recherche directe
---------------------------------------------------------------------------- */
function setRadiusValue(rayon){
  const r = parseInt(rayon, 10);
  if (!isFinite(r) || r <= 0) return;

  if (![...radiusSelect.options].some(o => o.value === String(r))){
    const opt = document.createElement('option');
    opt.value = String(r);
    opt.textContent = `${r} km`;
    radiusSelect.appendChild(opt);
  }
  radiusSelect.value = String(r);
}

async function loadTopVilles(){
  const box = $('#topVilles');
  if (!box) return;

  box.innerHTML = '';

  try{
    const res = await fetch('ajax/top_villes.php?limit=15', {headers:{'Accept':'application/json'}});
    const data = await res.json();

    if (!data.ok || !Array.isArray(data.items) || !data.items.length) return;

    box.innerHTML = data.items.map(it => {

  const v = String(it.ville_pays || '');
  const parts = v.split(',');

  const city = (parts[0] || '').trim();
  const postal = (parts[1] || '').trim();
  const cc = (parts[2] || '').trim();

  const label = postal
    ? `${city} (${postal}), ${cc}`
    : v;
	
	const guests = it.nombre_voyageurs
  ? ` • ${it.nombre_voyageurs} pers.`
  : '';

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

    box.querySelectorAll('button[data-ville]').forEach(b => {
      b.addEventListener('click', () => {
        input.value = b.getAttribute('data-ville');
        setRadiusValue(b.getAttribute('data-rayon'));
		if (b.dataset.voyageurs) {
  const g = document.getElementById('qGuests');
  if (g) g.value = b.dataset.voyageurs;
}
        doSearch({forceLive:false});
		// Mobile: remonter en haut
    if (window.matchMedia('(max-width: 768px)').matches) {
      window.scrollTo({ top: 0, behavior: 'smooth' });
    }
      });
    });
  }catch(e){}
}

/* ----------------------------------------------------------------------------
   Search (cache BDD)
---------------------------------------------------------------------------- */
async function doSearch(opts = {forceLive:false}){
  clearError();
  setModeResults();

  const ville_pays = input.value.trim();
  const rayon_km   = radiusSelect.value;
  const nombre_voyageurs = guestsSelect ? parseInt(guestsSelect.value, 10) || 0 : 0;
  

  if (!ville_pays){
    statusEl.textContent = "Tape une ville + pays.";
    input.focus();
    return;
  }

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

  setLoading();
  statusEl.textContent = "Recherche…";
  metaEl.textContent = "—";

  // Construire l'URL avec les filtres
  const params = new URLSearchParams();
  params.set('ville_pays', ville_pays);
  params.set('rayon_km', rayon_km);
  params.set('nombre_voyageurs', nombre_voyageurs);
  params.set('debug', '1');

  if (opts.forceLive) params.set('force_live', '1');

  const types = getActiveTypes();
  if (types.length) params.set('types', types.join(','));

  if (filters.piscine) params.set('piscine', '1');

  const url = `ajax/recherche_hebergements.php?${params.toString()}`;

  const res  = await fetch(url, {headers:{'Accept':'application/json'}});
  const data = await res.json();
  
  const debugBox = document.getElementById('debugRecherche');
  
  console.log(data.debug_recherche?.ville_pays || '');

if (debugBox) {
  if (data.debug_recherche) {
    debugBox.style.display = 'block';
    debugBox.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>
    `;
  } else {
    debugBox.style.display = 'none';
    debugBox.innerHTML = '';
  }
}

  if (!data.ok){
    grid.innerHTML = '';
    statusEl.textContent = "Erreur";
    metaEl.textContent = "—";
    setError(data.error || "Erreur inconnue.");
    return;
  }

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

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

  lastResults = Array.isArray(data.results) ? data.results : [];
  renderResults(lastResults);
  console.log('[LIKES size]', likedTokens.size, '[sample]', [...likedTokens].slice(0,5));

  cacheMissingPhotos(lastResults);

  if (splitEl.classList.contains('is-map')){
    initMapOnce();
    updateMap(lastResults);
    bindCardsToMap();
  }
}

//gestion de modale photos
let photosModal = null;

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

  const modalEl  = document.getElementById('photosModal');
  const titleEl  = document.getElementById('photosModalTitle');
  const metaEl   = document.getElementById('photosModalMeta');
  const mainEl   = document.getElementById('photosModalMain');
  const thumbsEl = document.getElementById('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 extraPhotos = normalizePhotosLocal(item.photos_local_json).map(buildLocalPhotoUrl);

  const allPhotos = [mainPhoto];
  extraPhotos.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, idx) => {
    const border = idx === 0 ? '#111' : '#e5e7eb';

    return ''
      + '<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 ' + border + ';">'
      + '</button>';
  }).join('');

  photosModal.show();
}


/* ----------------------------------------------------------------------------
   Contact modal
---------------------------------------------------------------------------- */
let contactModal = null;

let isContactDocked = true;

function setContactDocked(docked){
  const el = document.getElementById('contactModal');
  if (!el) return;

  isContactDocked = !!docked;
  el.classList.toggle('is-docked', isContactDocked);

  // Icône du toggle (expand vs minimize)
  const icon = el.querySelector('#btnDockToggle i');
  if (icon){
    icon.className = isContactDocked ? 'bi bi-arrows-angle-expand' : 'bi bi-dash-lg';
  }

  // Backdrop : rendu / interaction selon état docké
  const backdrop = document.querySelector('.modal-backdrop');
  if (backdrop){
    backdrop.classList.toggle('docked-backdrop', isContactDocked);
  }

  // Gestion du scroll body :
  // - En mode docké, la page doit rester scrollable.
  // - En mode normal (non docké), on garde le comportement modal standard.
  const isOpen = el.classList.contains('show');

  if (!isOpen){
    // Sécurité : si la modale est fermée, on s'assure que le body n'est pas locké.
    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 el = document.getElementById('contactModal');
  if (!el) return;

  // Toggle button
  el.querySelector('#btnDockToggle')?.addEventListener('click', (e) => {
    e.preventDefault();
    e.stopPropagation();
    setContactDocked(!isContactDocked);
  });

  // Click header : si docké -> ouvre (comme un chat)
  el.querySelector('.modal-header')?.addEventListener('click', () => {
    if (isContactDocked) setContactDocked(false);
  });

  // Quand la modale s’ouvre : dock par défaut
  el.addEventListener('shown.bs.modal', () => {
    setContactDocked(true);
  });

  // Quand elle se ferme : reset
  el.addEventListener('hidden.bs.modal', () => {
    setContactDocked(false);
  });
}

initContactDockUI();



let contactSelected = []; // [{token,name,ville,country}]

function isContactModalOpen(){
  const el = document.getElementById('contactModal');
  return el && el.classList.contains('show');
}

function normalizeToken(t){
  return String(t || '').trim();
}

function renderContactSelection(){
  const listEl = document.getElementById('contactSelectionList');
  const tokensWrap = document.getElementById('contactTokensWrap');
  const titleEl = document.getElementById('contactTitle');
  const jsonEl = document.getElementById('contactSelectionJson');
	if (jsonEl) jsonEl.value = JSON.stringify(contactSelected || []);


  if (!listEl || !tokensWrap) return;

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

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

 // Titre avec compteur (facultatif)
if (titleEl){
  const n = contactSelected.length;
  titleEl.textContent = n ? `${n} hébergement${n > 1 ? 's' : ''} à contacter` : '0 hébergement à contacter';
}

}


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

  const el = document.getElementById('contactModal');
  if (!el) return;

  // Retire l'état docké (et libère le body si besoin)
  setContactDocked(false);

  // Ferme la modale si elle est ouverte
  const inst = bootstrap.Modal.getInstance(el) || contactModal;
  if (inst && el.classList.contains('show')){
    inst.hide();
  }
}

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

  // éviter les doublons
  const exists = contactSelected.some(x => x.token === token);
  if (!exists){
  contactSelected.push({
	  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 || ''
	});
  } else {
    // si déjà présent, on peut mettre à jour le nom/meta au cas où
    contactSelected = contactSelected.map(x => x.token === token ? ({
	  ...x,
	  name: data.name || x.name,
	  ville: data.ville || x.ville,
	  country: data.country || x.country,
	  capacite: data.capacite || x.capacite,
	  chambres: data.chambres || x.chambres,
	  sdb: data.sdb || x.sdb,
	  parking: data.parking || x.parking,
	  type: data.type || x.type,
	  prix: data.prix || x.prix
	}) : x);
  }

  renderContactSelection();
  
  syncContactButtons();


  // Petit feedback visuel sur la pill ajoutée (optionnel)
  const pill = document.querySelector(`#contactSelectionList .contact-pill[data-token="${token}"]`);
  if (pill){
    pill.classList.add('pulse');
    setTimeout(() => pill.classList.remove('pulse'), 350);
  }
}

function openContactModal(data){
  const el = document.getElementById('contactModal');
  if (!el) return;

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

  // Si la modale n'est pas déjà ouverte : reset du form (mais on garde la sélection)
  if (!isContactModalOpen()){
    $('#contactForm').reset();
    $('#contactSuccess').classList.add('d-none');
    // si on veux repartir de zéro à chaque "nouvelle session", décommenter 
    // contactSelected = [];
  }

  // Met à jour la zone meta “globale” (facultatif)
  $('#contactMeta').textContent = 'Ajoutez en d\'autres en cliquant sur "contactez l\'hôte"';
  $('#contactMeta1').textContent = '';
  

  // Ajoute l'hébergement cliqué
  addContactToModal(data);

  // Ouvre la modale (dock ou normal selon système)
  // setContactDocked(true); // sion veux toujours docker à l'ouverture
  setContactDocked(true);
  contactModal.show();
}

document.addEventListener('click', (e) => {
  const btn = e.target.closest('.btnContact');
  if (!btn) return;

  const payload = {
	  token:    btn.getAttribute('data-token'),
	  name:     btn.getAttribute('data-name'),
	  ville:    btn.getAttribute('data-ville'),
	  country:  btn.getAttribute('data-country'),
	  capacite: btn.getAttribute('data-capacite'),
	  chambres: btn.getAttribute('data-chambres'),
	  sdb:      btn.getAttribute('data-sdb'),
	  parking:  btn.getAttribute('data-parking'),
	  type:     btn.getAttribute('data-type'),
	  prix:     btn.getAttribute('data-prix')
	};

  const token = String(payload.token || '').trim();
  if (!token) return;

  const already = contactSelected.some(x => x.token === token);

  if (already){
    // toggle off (désélection)
    contactSelected = contactSelected.filter(x => x.token !== token);
    renderContactSelection();
    syncContactButtons();
  closeContactModalIfEmpty();
    return;
  }

  // toggle on (sélection) + ouvre la modale
  openContactModal(payload);
});



document.getElementById('contactForm')?.addEventListener('submit', async (e) => {
  e.preventDefault();

  const form = e.target;
  const fd   = new FormData(form);

  const tokens = fd.getAll('tokens[]');
  if (!tokens.length){
    alert("Merci de sélectionner au moins un hébergement.");
    return;
  }

  const btn = form.querySelector('button[type="submit"]');
  const old = btn.innerHTML;

  btn.disabled = true;
  btn.innerHTML = '<span class="spinner-border spinner-border-sm me-2"></span>Envoi...';

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

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

    document.getElementById('contactSuccess').classList.remove('d-none');

    // Reset sélection après envoi
    contactSelected = [];
    renderContactSelection();
    syncContactButtons();

    setTimeout(() => {
      const el = document.getElementById('contactModal');
      const inst = bootstrap.Modal.getInstance(el);
      if (inst) inst.hide();
    }, 700);

    btn.innerHTML = '<i class="bi bi-check2 me-1"></i>Envoyé';

    setTimeout(() => {
      btn.disabled = false;
      btn.innerHTML = old;
    }, 1200);

  }catch(err){
    alert("Erreur réseau.");
    btn.disabled = false;
    btn.innerHTML = old;
  }
});


document.getElementById('contactModal')?.addEventListener('hidden.bs.modal', () => {
  document.getElementById('contactForm')?.reset();
  document.getElementById('contactSuccess')?.classList.add('d-none');
});

document.getElementById('contactModal')?.addEventListener('click', (e) => {
  const btn = e.target.closest('[data-remove-token]');
  if (!btn) return;

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


/* ----------------------------------------------------------------------------
   Events
---------------------------------------------------------------------------- */

// recherche auto au changement du nombre de voyageurs
// recherche auto au changement du nombre de voyageurs
$('#qGuests')?.addEventListener('change', () => {

  const enabled = !!guestsSelect.value;

  document.querySelectorAll('.filters .js-filter[data-filter="type"]').forEach(btn => {
    btn.disabled = !enabled;
    btn.classList.toggle('is-disabled', !enabled);
  });

  if (!citySelected) {
    input.focus();
    return;
  }

  if (!guestsSelect.value) return;

  doSearch({ forceLive: false });
});


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

  console.log('clic photo détecté', btn.getAttribute('data-token'));

  e.preventDefault();
  e.stopPropagation();

  const token = btn.getAttribute('data-token');
  if (!token) return;

  openPhotosModal(token);
});

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

  const url = btn.getAttribute('data-photo-url');
  const current = document.getElementById('photosModalCurrent');

  if (!current || !url) return;

  current.src = url;

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

  const img = btn.querySelector('img');
  if (img) {
    img.style.border = '2px solid #111';
  }
});

// Filtres (Type + Piscine)
setFilterBtnState();

filtersBars.forEach(filtersBar => {
  filtersBar.addEventListener('click', (e) => {
    const btn = e.target.closest('.js-filter');
    if (!btn) return;

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

    if (kind === 'reset') {
      resetFilters();

    } else if (kind === 'type') {
      const t = btn.getAttribute('data-value');

      if (filters.types.has(t)) {
        filters.types.clear();
      } else {
        filters.types.clear();
        filters.types.add(t);
      }

      setFilterBtnState();

    } else if (kind === 'piscine') {
      filters.piscine = !filters.piscine;
      setFilterBtnState();
    }

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

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

input?.addEventListener('input', async () => {
	
	citySelected = false;
	
	
	
	

	if (guestsSelect) {
	  guestsSelect.disabled = true;
	  guestsSelect.value = '';
	}
  const val = input.value.trim();
  const p2 = normalizeForPrefix2(val);

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

  const list = await loadPrefix(p2);
  const q = normalizeSearch(val);

  const filtered = 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 value = normalizeSearch(item.value || '');

    return (
      city.startsWith(q) ||
      label.startsWith(q) ||
      value.startsWith(q) ||
      postalCode.startsWith(q) ||
      (city + ' ' + postalCode + ' ' + countryCode).includes(q)
    );
  }

  // ancien format
  return normalizeSearch(item).startsWith(q);

}).slice(0, 12);

  showSuggestions(filtered);
});

suggestBox?.addEventListener('click', (e) => {
	

  const item = e.target.closest('.item');
  if (!item) return;

  input.value = item.getAttribute('data-val');
  citySelected = true;

  if (guestsSelect) {
    guestsSelect.disabled = false;
    guestsSelect.focus();
  }

  showSuggestions([]);
});

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

btnClearCache?.addEventListener('click', async () => {
  const ville_pays = input.value.trim();
  const rayon_km   = radiusSelect.value;
  const nombre_voyageurs = guestsSelect ? parseInt(guestsSelect.value, 10) || 0 : 0;
  if (!ville_pays) return;

  statusEl.textContent = "Suppression du cache…";
  btnClearCache.style.display = 'none';

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

  const res  = await fetch('ajax/cache_clear.php', {method:'POST', body: form});
  const data = await res.json();

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

  await doSearch({forceLive:true});
});

btnToggleMap?.addEventListener('click', () => {
  const on = splitEl.classList.toggle('is-map');

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

  if (on){
    initMapOnce();
    updateMap(lastResults);
    bindCardsToMap();
  }

  setTimeout(() => map && map.invalidateSize(), 80);
});

btnShuffle?.addEventListener('click', () => {
  const items = Array.from(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(x => grid.appendChild(x));
  statusEl.textContent = "Cartes mélangées ✔";
});

/* UX: sélection rapide du champ ville */
input?.addEventListener('focus', () => setTimeout(() => input.select(), 0));
input?.addEventListener('click', () => setTimeout(() => input.select(), 0));

/* ----------------------------------------------------------------------------
   Init
---------------------------------------------------------------------------- */
window.addEventListener('DOMContentLoaded', () => {
	
	
document.querySelectorAll('#filtersBar .js-filter').forEach(btn => {
  btn.disabled = true;
  btn.classList.add('is-disabled');
});
	
  if (guestsSelect) {
  guestsSelect.disabled = true;
}
  if (input) {
    input.value = '';
    showSuggestions([]);
    setModeHome();
    loadTopVilles();
    loadHomeTop3();
  }

  const modalEl = document.getElementById('contactModal');
  if (modalEl) {
    modalEl.addEventListener('shown.bs.modal', () => {
      initContactDates(modalEl);
    });
  } else {
    initContactDates(document);
  }
});



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