1(function($) { 2 'use strict'; 3 4 // === GALLERY === 5 $(document).on('click', '.hcb-gallery-thumb', function() { 6 var fullSrc = $(this).data('full'); 7 $('#hcb-gallery-main-img').attr('src', fullSrc); 8 $('.hcb-gallery-thumb').removeClass('active'); 9 $(this).addClass('active'); 10 }); 11 $('.hcb-gallery-thumb:first').addClass('active'); 12 13 // === CALENDAR (Frontend) === 14 function initFrontendCalendars() { 15 $('.hcb-calendar').each(function() { 16 var $cal = $(this); 17 var itemId = $cal.data('item-id'); 18 var itemType = $cal.data('item-type'); 19 var calendarEvents = []; // Stockage local des événements 20 21 $cal.fullCalendar({ 22 locale: 'fr', 23 header: { 24 left: 'prev,next today', 25 center: 'title', 26 right: 'month,agendaWeek' 27 }, 28 defaultView: 'month', 29 editable: false, 30 eventLimit: true, 31 selectable: true, 32 selectHelper: true, 33 unselectAuto: true, 34 eventSources: [ 35 // Source 1 : réservations existantes 36 { 37 events: function(start, end, timezone, callback) { 38 $.ajax({ 39 url: hcb_front.ajax_url, 40 data: { 41 action: 'hcb_get_events', 42 item_id: itemId, 43 item_type: itemType, 44 start: start.format(), 45 end: end.format() 46 }, 47 success: function(data) { 48 calendarEvents = data; 49 callback(data); 50 } 51 }); 52 } 53 }, 54 // Source 2 : créneaux de disponibilité (fond vert) 55 { 56 events: function(start, end, timezone, callback) { 57 $.ajax({ 58 url: hcb_front.ajax_url, 59 data: { 60 action: 'hcb_get_availability_slots', 61 item_id: itemId, 62 item_type: itemType, 63 start: start.format(), 64 end: end.format() 65 }, 66 success: function(data) { 67 if (data && data.success) { 68 callback(data.data.slots || []); 69 } else { 70 callback([]); 71 } 72 }, 73 error: function() { 74 callback([]); 75 } 76 }); 77 } 78 } 79 ], 80 eventRender: function(event, element) { 81 if (event.rendering === 'background') { 82 element.attr('title', hcb_front.strings.click_to_book || 'Cliquer pour réserver'); 83 } else { 84 element.attr('title', event.title + ' (' + event.status_label + ')'); 85 } 86 }, 87 select: function(start, end, jsEvent, view) { 88 var dateStr, dateEnd; 89 90 if (view.name === 'agendaWeek' || view.name === 'agendaDay') { 91 // Vue semaine/jour : plage horaire sélectionnée 92 dateStr = start.format('YYYY-MM-DD[T]HH:mm'); 93 // Si clic simple (30 min par défaut), étendre à 1 heure 94 var diff = end.diff(start, 'minutes'); 95 if (diff <= 30) { 96 dateEnd = start.clone().add(1, 'hour').format('YYYY-MM-DD[T]HH:mm'); 97 } else { 98 dateEnd = end.format('YYYY-MM-DD[T]HH:mm'); 99 } 100 } else { 101 // Vue mois : heures par défaut 102 dateStr = start.format('YYYY-MM-DD') + 'T09:00'; 103 dateEnd = start.format('YYYY-MM-DD') + 'T
10318:00'; 104 } 105 106 // Vérifier si le créneau chevauche une réservation 107 var clickStart = moment(dateStr); 108 var clickEnd = moment(dateEnd); 109 var conflict = false; 110 111 for (var i = 0; i < calendarEvents.length; i++) { 112 var ev = calendarEvents[i]; 113 if (ev.status === 'accepted' || ev.status === 'studying') { 114 var evStart = moment(ev.start); 115 var evEnd = moment(ev.end); 116 if (clickStart.isBefore(evEnd) && clickEnd.isAfter(evStart)) { 117 conflict = true; 118 break; 119 } 120 } 121 } 122 123 if (conflict) { 124 hcbShowToast(hcb_front.strings.slot_busy || 'Ce créneau est déjà réservé.'); 125 $cal.fullCalendar('unselect'); 126 return; 127 } 128 129 // Pré-remplir le formulaire 130 $('#hcb-date-start').val(dateStr); 131 $('#hcb-date-end').val(dateEnd); 132 133 $('html, body').animate({ 134 scrollTop: $('#hcb-booking-form-section').offset().top - 80 135 }, 500); 136 } 137 }); 138 }); 139 } 140 141 /** 142 * Affiche un message toast temporaire. 143 */ 144 function hcbShowToast(message) { 145 var $toast = $('<div class="hcb-toast">' + message + '</div>'); 146 $('body').append($toast); 147 setTimeout(function() { $toast.addClass('hcb-toast-visible'); }, 10); 148 setTimeout(function() { 149 $toast.removeClass('hcb-toast-visible'); 150 setTimeout(function() { $toast.remove(); }, 300); 151 }, 3000); 152 } 153 154 // === BOOKING FORM === 155 $(document).on('submit', '.hcb-booking-form', function(e) { 156 e.preventDefault(); 157 158 var $form = $(this); 159 var $btn = $form.find('button[type=submit]'); 160 var $msg = $form.find('.hcb-form-message'); 161 162 // Validate 163 var required = ['first_name', 'last_name', 'email', 'date_start', 'date_end']; 164 var valid = true; 165 required.forEach(function(field) { 166 var val = $form.find('[name="' + field + '"]').val(); 167 if (!val || val.trim() === '') { 168 valid = false; 169 $form.find('[name="' + field + '"]').css('border-color', '#d63638'); 170 } else { 171 $form.find('[name="' + field + '"]').css('border-color', ''); 172 } 173 }); 174 175 // Check GDPR 176 if (!$form.find('[name="gdpr_consent"]').is(':checked')) { 177 valid = false; 178 $form.find('.hcb-form-gdpr').css('color', '#d63638'); 179 } else { 180 $form.find('.hcb-form-gdpr').css('color', ''); 181 } 182 183 if (!valid) { 184 $msg.removeClass('success').addClass('error').text(hcb_front.strings.fill_required).show(); 185 return; 186 } 187 188 // Validation des fichiers joints 189 var allowedTypes = [ 190 'application/pdf', 'image/jpeg', 'image/png', 191 'application/msword', 192 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' 193 ]; 194 var maxSizeBytes = (hcb_front.max_file_size_mb || 5) * 1024 * 1024; 195 var fileValid = true; 196 197 $form.find('input[type="file"]').each(function() { 198 var input = this; 199 var label = $(input).data('label') || input.name; 200 201 if (input.required && (!input.files || input.files.length === 0)) { 202 fileValid = false; 203 $(input).css('border-color', '#d63638'); 204 return true; // continue 205 } 206 207 if (input.files && input.files.length > 0) { 208 var file = input.files[0]; 209 $(input).css('border-color', ''); 210 211 if (file.size > maxSizeBytes) { 212 fileValid = false;
213 $msg.removeClass('success').addClass('error') 214 .text((hcb_front.strings.file_too_large || '').replace('%s', label)).show(); 215 $(input).css('border-color', '#d63638'); 216 return false; // break 217 } 218 219 if (allowedTypes.indexOf(file.type) === -1 && file.type !== '') { 220 fileValid = false; 221 $msg.removeClass('success').addClass('error') 222 .text((hcb_front.strings.file_type_invalid || '').replace('%s', label)).show(); 223 $(input).css('border-color', '#d63638'); 224 return false; // break 225 } 226 } 227 }); 228 229 if (!fileValid) { 230 if (!$msg.is(':visible')) { 231 $msg.removeClass('success').addClass('error').text(hcb_front.strings.docs_required || '').show(); 232 } 233 return; 234 } 235 236 // Submit avec FormData (supporte les fichiers) 237 $btn.addClass('hcb-btn-loading').prop('disabled', true); 238 $msg.hide(); 239 240 var fd = new FormData($form[0]); 241 fd.append('action', 'hcb_submit_booking'); 242 fd.append('nonce', hcb_front.nonce); 243 fd.append('item_id', $form.data('item-id')); 244 fd.append('item_type', $form.data('item-type')); 245 246 $.ajax({ 247 url: hcb_front.ajax_url, 248 method: 'POST', 249 data: fd, 250 processData: false, 251 contentType: false, 252 success: function(response) { 253 $btn.removeClass('hcb-btn-loading').prop('disabled', false); 254 if (response.success) { 255 $msg.removeClass('error').addClass('success').text(response.data.message).show(); 256 $form[0].reset(); 257 $('.hcb-calendar').each(function() { 258 $(this).fullCalendar('refetchEvents'); 259 }); 260 } else { 261 $msg.removeClass('success').addClass('error').text(response.data.message || hcb_front.strings.error).show(); 262 } 263 }, 264 error: function() { 265 $btn.removeClass('hcb-btn-loading').prop('disabled', false); 266 $msg.removeClass('success').addClass('error').text(hcb_front.strings.error).show(); 267 } 268 }); 269 }); 270 271 // === FILTRES AVANCÃS === 272 var filterTimeout; 273 274 function applyRoomFilters() { 275 var $bar = $('#hcb-room-filters'); 276 var $results = $('#hcb-rooms-results'); 277 if (!$bar.length || !$results.length) return; 278 279 var filters = { 280 action: 'hcb_filter_rooms', 281 nonce: hcb_front.nonce, 282 capacity_min: $bar.find('[name="capacity_min"]').val(), 283 capacity_max: $bar.find('[name="capacity_max"]').val(), 284 amenities: [], 285 accessibility: $bar.find('[name="accessibility"]').val(), 286 parking: $bar.find('[name="parking"]').val(), 287 price_min: $bar.find('[name="price_min"]').val(), 288 price_max: $bar.find('[name="price_max"]').val(), 289 available_date: $bar.find('[name="available_date"]').val(), 290 category: $bar.find('[name="category"]').val(), 291 free_for_assoc: $bar.find('[name="free_for_assoc"]').is(':checked') ? 1 : 0 292 }; 293 294 $bar.find('[name="amenities[]"]:checked').each(function() { 295 filters.amenities.push($(this).val()); 296 }); 297 298 $results.addClass('hcb-loading'); 299 300 $.ajax({ 301 url: hcb_front.ajax_url, 302 method: 'POST', 303 data: filters, 304 success: function(response) { 305 $results.removeClass('hcb-loading'); 306 if (response.success) { 307 $results.html(response.data.html); 308 $bar.find('.hcb-filter-count').text(response.data.count + ' résultat(s)'); 309 // Restaurer l'état des checkboxes de comparaison 310 restoreCompareCheckboxes(); 311 } 312 }, 313 error: function() { 314 $results.removeClass('hcb-loading'); 315 } 316 }); 317 } 318 319 function applyEquipmentFilters() { 320 var $bar = $('#hcb-equipment-filters'); 321 var $results = $('#hcb-equipment-results'); 322 if (!$bar.length || !$results.length) return; 323 324 var filters = { 325 action: 'hcb_filter_equipment', 326 nonce: hcb_front.nonce, 327 category: $bar.find('[name="category"]').val(), 328 price_min: $bar.find('[name="price_min"]').val(), 329 price_max: $bar.find('[name="price_max"]').val(), 330 available_date: $bar.find('[name="available_date"]').val() 331 }; 332 333 $results.addClass('hcb-loading'); 334 335 $.ajax({ 336 url: hcb_front.ajax_url, 337 method: 'POST', 338 data: filters, 339 success: function(response) { 340 $results.removeClass('hcb-loading'); 341 if (response.success) { 342 $results.html(response.data.html); 343 $bar.find('.hcb-filter-count').text(response.data.count + ' résultat(s)'); 344 } 345 }, 346 error: function() { 347 $results.removeClass('hcb-loading'); 348 } 349 }); 350 } 351 352 // Filtres salles : debounce 353 $(document).on('change input', '#hcb-room-filters input, #hcb-room-filters select', function() { 354 clearTimeout(filterTimeout); 355 filterTimeout = setTimeout(applyRoomFilters, 400); 356 }); 357 358 // Filtres équipements : debounce 359 $(document).on('change input', '#hcb-equipment-filters input, #hcb-equipment-filters select', function() { 360 clearTimeout(filterTimeout); 361 filterTimeout = setTimeout(applyEquipmentFilters, 400); 362 }); 363 364 // Réinitialiser les filtres 365 $(document).on('click', '#hcb-room-filters .hcb-filter-reset', function() { 366 var $bar = $('#hcb-room-filters'); 367 $bar.find('input[type="number"], input[type="date"]').val(''); 368 $bar.find('select').val(''); 369 $bar.find('input[type="checkbox"]').prop('checked', false); 370 $bar.find('.hcb-filter-count').text(''); 371 applyRoomFilters(); 372 }); 373 374 $(document).on('click', '#hcb-equipment-filters .hcb-filter-reset', function() { 375 var $bar = $('#hcb-equipment-filters'); 376 $bar.find('input[type="number"], input[type="date"]').val(''); 377 $bar.find('select').val(''); 378 $bar.find('.hcb-filter-count').text(''); 379 applyEquipmentFilters(); 380 }); 381 382 // Dérouler/replier les équipements 383 $(document).on('click', '.hcb-filter-amenities-toggle', function() { 384 $(this).closest('.hcb-filter-amenities-section').find('.hcb-filter-amenities').slideToggle(200); 385 }); 386 387 // === COMPARAISON DE SALLES === 388 var MAX_COMPARE = 3; 389 390 function getSelectedRooms() { 391 try { 392 return JSON.parse(sessionStorage.getItem('hcb_compare') || '[]'); 393 } catch (e) { 394 return []; 395 } 396 } 397 398 function saveSelectedRooms(rooms) { 399 sessionStorage.setItem('hcb_compare', JSON.stringify(rooms)); 400 } 401 402 function restoreCompareCheckboxes() { 403 var selected = getSelectedRooms(); 404 $('.hcb-compare-check').each(function() { 405 var id = parseInt($(this).val()); 406 $(this).prop('checked', selected.indexOf(id) !== -1); 407 }); 408 updateCompareBar(); 409 } 410 411 function updateCompareBar() { 412 var selected = getSelectedRooms(); 413 var $bar = $('#hcb-compare-bar'); 414 if (!$bar.length) return; 415 416 if (selected.length === 0) {
417 $bar.slideUp(200); 418 return; 419 } 420 421 $bar.slideDown(200); 422 $bar.find('.hcb-compare-count').text(selected.length + '/' + MAX_COMPARE); 423 $bar.find('.hcb-compare-btn').prop('disabled', selected.length < 2); 424 425 // Construire la liste des miniatures 426 var $items = $bar.find('.hcb-compare-items'); 427 $items.empty(); 428 selected.forEach(function(id) { 429 var $card = $('[data-room-id="' + id + '"]'); 430 var title = $card.find('.hcb-item-title a').text() || '#' + id; 431 var thumb = $card.find('.hcb-item-image img').attr('src') || ''; 432 var html = '<div class="hcb-compare-item-mini">'; 433 if (thumb) { 434 html += '<img src="' + thumb + '" alt="">'; 435 } 436 html += '<span>' + title + '</span>'; 437 html += '<button type="button" class="hcb-compare-remove" data-id="' + id + '">×</button>'; 438 html += '</div>'; 439 $items.append(html); 440 }); 441 442 // Désactiver les checkboxes restantes si max atteint 443 if (selected.length >= MAX_COMPARE) { 444 $('.hcb-compare-check').each(function() { 445 if (!$(this).is(':checked')) { 446 $(this).prop('disabled', true); 447 } 448 }); 449 } else { 450 $('.hcb-compare-check').prop('disabled', false); 451 } 452 } 453 454 // Clic sur checkbox de comparaison 455 $(document).on('change', '.hcb-compare-check', function() { 456 var id = parseInt($(this).val()); 457 var selected = getSelectedRooms(); 458 459 if ($(this).is(':checked')) { 460 if (selected.length < MAX_COMPARE && selected.indexOf(id) === -1) { 461 selected.push(id); 462 } 463 } else { 464 selected = selected.filter(function(v) { return v !== id; }); 465 } 466 467 saveSelectedRooms(selected); 468 updateCompareBar(); 469 }); 470 471 // Supprimer de la barre de comparaison 472 $(document).on('click', '.hcb-compare-remove', function() { 473 var id = parseInt($(this).data('id')); 474 var selected = getSelectedRooms().filter(function(v) { return v !== id; }); 475 saveSelectedRooms(selected); 476 $('.hcb-compare-check[value="' + id + '"]').prop('checked', false); 477 updateCompareBar(); 478 }); 479 480 // Vider la comparaison 481 $(document).on('click', '.hcb-compare-clear', function() { 482 saveSelectedRooms([]); 483 $('.hcb-compare-check').prop('checked', false); 484 updateCompareBar(); 485 }); 486 487 // Ouvrir la modale de comparaison 488 $(document).on('click', '.hcb-compare-btn', function() { 489 var selected = getSelectedRooms(); 490 if (selected.length < 2) return; 491 492 var $modal = $('#hcb-compare-modal'); 493 var $wrap = $modal.find('.hcb-compare-table-wrap'); 494 $wrap.html('<p style="text-align:center;">' + (hcb_front.strings.loading || 'Chargement...') + '</p>'); 495 $modal.fadeIn(200); 496 497 $.ajax({ 498 url: hcb_front.ajax_url, 499 method: 'POST', 500 data: { 501 action: 'hcb_get_comparison_data', 502 nonce: hcb_front.nonce, 503 room_ids: selected 504 }, 505 success: function(response) { 506 if (response.success) { 507 $wrap.html(buildComparisonTable(response.data.rooms, response.data.amenity_labels)); 508 } else { 509 $wrap.html('<p class="hcb-no-results">' + (response.data.message || hcb_front.strings.error) + '</p>'); 510 } 511 }, 512 error: function() { 513 $wrap.html('<p class="hcb-no-results">' + hcb_front.strings.error + '</p>'); 514 } 515 }); 516 }); 517 518 // Construire le tableau de comparaison 519 function buildComparisonTable(rooms, amenityLabels) { 520 var html = '<table class="hcb-compare-table"><thead><tr><th></th>'; 521 rooms.forEach(function(r) { html += '<th>' + escHtml(r.title) + '</th>'; }); 522 html += '</tr></thead><tbody>'; 523 524 // Photo 525 html += '<tr><td><strong>Photo</strong></td>';
526 rooms.forEach(function(r) { 527 html += '<td>'; 528 if (r.thumbnail) html += '<img src="' + r.thumbnail + '" class="hcb-compare-thumb" alt="">'; 529 else html += '-'; 530 html += '</td>'; 531 }); 532 html += '</tr>'; 533 534 // Lignes de données 535 var rows = [ 536 ['Capacité assise', function(r) { return r.capacity ? r.capacity + ' pers.' : '-'; }], 537 ['Capacité debout', function(r) { return r.capacity_standing ? r.capacity_standing + ' pers.' : '-'; }], 538 ['Superficie', function(r) { return r.surface ? r.surface + ' m²' : '-'; }], 539 ['Prix / heure', function(r) { return r.price_hour ? formatPrice(r.price_hour) + ' â¬' : '-'; }], 540 ['Prix / jour', function(r) { return r.price_day ? formatPrice(r.price_day) + ' â¬' : '-'; }], 541 ['Caution', function(r) { return r.deposit ? formatPrice(r.deposit) + ' â¬' : '-'; }], 542 ['Accessibilité PMR', function(r) { return r.accessibility || '-'; }], 543 ['Parking', function(r) { return r.parking || '-'; }], 544 ['Gratuit assos', function(r) { return r.free_for_assoc ? 'â' : 'â'; }] 545 ]; 546 547 rows.forEach(function(row) { 548 html += '<tr><td><strong>' + row[0] + '</strong></td>'; 549 rooms.forEach(function(r) { html += '<td>' + row[1](r) + '</td>'; }); 550 html += '</tr>'; 551 }); 552 553 // Ãquipements 554 if (amenityLabels) { 555 for (var key in amenityLabels) { 556 html += '<tr><td><strong>' + escHtml(amenityLabels[key]) + '</strong></td>'; 557 rooms.forEach(function(r) { 558 var has = r.amenities && r.amenities[key]; 559 html += '<td class="' + (has ? 'hcb-amenity-yes' : 'hcb-amenity-no') + '">' + (has ? 'â' : 'â') + '</td>'; 560 }); 561 html += '</tr>'; 562 } 563 } 564 565 // Boutons 566 html += '<tr><td></td>'; 567 rooms.forEach(function(r) { 568 html += '<td><a href="' + r.permalink + '" class="hcb-btn hcb-btn-primary">Voir & Réserver</a></td>'; 569 }); 570 html += '</tr>'; 571 572 html += '</tbody></table>'; 573 return html; 574 } 575 576 function formatPrice(val) { 577 return parseFloat(val).toFixed(2).replace('.', ','); 578 } 579 580 function escHtml(str) { 581 var div = document.createElement('div'); 582 div.appendChild(document.createTextNode(str)); 583 return div.innerHTML; 584 } 585 586 // Fermer la modale 587 $(document).on('click', '.hcb-compare-modal-close', function() { 588 $('#hcb-compare-modal').fadeOut(200); 589 }); 590 $(document).on('click', '#hcb-compare-modal', function(e) { 591 if ($(e.target).is('#hcb-compare-modal')) { 592 $(this).fadeOut(200); 593 } 594 }); 595 $(document).on('keydown', function(e) { 596 if (e.key === 'Escape') { 597 $('#hcb-compare-modal').fadeOut(200); 598 } 599 }); 600 601 // === PRÃ-REMPLISSAGE FORMULAIRE UTILISATEUR CONNECTà === 602 function prefillUserData() { 603 if (typeof hcb_front.user !== 'undefined' && hcb_front.user.email) { 604 var $form = $('.hcb-booking-form'); 605 if ($form.length) { 606 $form.find('[name="first_name"]').val(hcb_front.user.first_name); 607 $form.find('[name="last_name"]').val(hcb_front.user.last_name); 608 $form.find('[name="email"]').val(hcb_front.user.email).prop('readonly', true); 609 } 610 } 611 } 612 613 // === INIT === 614 $(document).ready(function() { 615 initFrontendCalendars(); 616 restoreCompareCheckboxes(); 617 prefillUserData(); 618 }); 619 620})(jQuery);
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.