// Début du script JavaScript pour le Tableau Comparateur Interactif de Dubaï.
// Ce bloc est autonome et peut être inséré directement dans votre page HTML.
// — Données du tableau —
// Cette structure de données contient toutes les informations nécessaires pour le tableau.
// Chaque objet représente une catégorie avec son titre, les en-têtes des colonnes et les lignes de données.
// Les URLs d’API publiques gratuites et exemples de réponses JSON ne sont pas nécessaires ici
// car les données sont intégrées directement dans le script pour des raisons de performance
// et pour éviter toute dépendance externe non gérée par le client.
const dubaiData = [
{
title: “Activités en extérieur à Dubaï”,
headers: [“Type d’activité”, “Coût estimé (EUR)”, “Description”, “Conseil pour octobre”],
rows: [
[“Safari dans le désert”, “50-100”, “Excursion en 4×4, sandboarding, dîner bédouin”, “Températures parfaites, surtout au coucher du soleil. Réserver à l’avance.”],
[“Visite Burj Khalifa”, “40-70”, “Montée aux plateformes d’observation, vue panoramique”, “Moins d’attente qu’en haute saison, privilégier le créneau du matin ou du soir pour la vue.”],
[“Plages de Dubaï”, “Gratuit / 10-30 (club)”, “Détente, baignade, sports nautiques”, “L’eau est chaude et agréable. JBR Beach, La Mer sont d’excellents choix.”],
[“Parcs aquatiques (Aquaventure, Wild Wadi)”, “70-90”, “Toboggans, piscines à vagues, attractions aquatiques”, “Idéal pour se rafraîchir. Octobre est moins bondé que l’été.”],
[“Croisière en Dhow/Yacht”, “30-150”, “Dîner-croisière dans la Marina ou sur la Crique”, “Profitez des soirées douces d’octobre pour une vue imprenable sur la ville illuminée.”],
[“Vol en montgolfière”, “200-300”, “Expérience au lever du soleil au-dessus du désert”, “Spectaculaire en octobre, car le temps est plus stable et clair.”],
]
},
{
title: “Sites Culturels et Historiques”,
headers: [“Site”, “Coût estimé (EUR)”, “Description”, “Conseil pour octobre”],
rows: [
[“Quartier Al Fahidi (Bastakiya)”, “Gratuit / 5-10 (musée)”, “Ancien quartier avec tours à vent, galeries d’art, cafés”, “Flânez le matin ou en fin d’après-midi, moins de chaleur et ambiance plus authentique.”],
[“Musée de Dubaï (Fort Al Fahidi)”, “2-3”, “Histoire de Dubaï, artefacts traditionnels”, “Petite visite instructive pour comprendre l’évolution de la ville.”],
[“Souks de l’Or et des Épices”, “Gratuit (achats optionnels)”, “Marchés traditionnels pour bijoux et épices”, “Négociez les prix. Idéal en soirée pour l’ambiance.”],
[“Mosquée de Jumeirah”, “5-7 (visite guidée)”, “Seule mosquée de Dubaï ouverte aux non-musulmans”, “Vérifiez les horaires de visite guidée. Tenue vestimentaire appropriée requise.”],
[“Centre Sheikh Mohammed pour la compréhension culturelle”, “20-40”, “Repas et dialogue sur la culture émiratie”, “Une expérience immersive pour une meilleure compréhension des traditions locales.”],
]
},
{
title: “Divertissement et Shopping”,
headers: [“Lieu”, “Coût estimé (EUR)”, “Description”, “Conseil pour octobre”],
rows: [
[“The Dubai Mall”, “Gratuit (achats optionnels)”, “Un des plus grands centres commerciaux du monde, aquarium, patinoire”, “Passez-y une demi-journée ou plus. Assistez au spectacle des fontaines en soirée.”],
[“Mall of the Emirates”, “Gratuit (achats optionnels)”, “Shopping de luxe, Ski Dubai”, “Combinez shopping et une expérience de ski unique dans le désert.”],
[“Opéra de Dubaï”, “Variable (selon spectacle)”, “Architecture impressionnante, spectacles de renommée mondiale”, “Réservez à l’avance si un spectacle vous intéresse.”],
[“Global Village (à partir de fin octobre)”, “5-10”, “Parc d’attractions, marchés du monde, spectacles”, “Ouvre généralement en octobre. Une madagasikara.fr/vivez-lexperience-culturelle-unique-de-madagascar-9/">expérience culturelle et de divertissement unique.”],
[“Ain Dubai (Roue d’observation)”, “30-40”, “La plus grande roue d’observation du monde, vues panoramiques”, “Vérifiez les horaires d’ouverture et réservez pour le coucher du soleil pour des vues spectaculaires.”],
]
}
];
// — Variables d’état globales pour la gestion du tableau —
let currentCategoryIndex = 0; // Index de la catégorie actuellement affichée.
let currentSortColumn = -1; // Index de la colonne actuellement triée (-1 si aucune).
let currentSortDirection = ‘asc’; // Direction du tri : ‘asc’ (ascendant) ou ‘desc’ (descendant).
// — Références aux éléments DOM —
const categoryTabsContainer = document.getElementById(‘category-tabs’);
const tableHeadersRow = document.getElementById(‘table-headers’);
const tableBody = document.getElementById(‘table-body’);
const searchInput = document.getElementById(‘table-search-input’);
const noResultsMessage = document.getElementById(‘no-results-message’);
// — Fonction pour rendre (afficher) le tableau —
// categoryIndex : l’index de la catégorie de données à afficher.
// searchTerm : le texte à utiliser pour filtrer les lignes du tableau.
function renderTable(categoryIndex, searchTerm = ”) {
const category = dubaiData[categoryIndex];
// 1. Vider les en-têtes et le corps du tableau avant de rendre les nouvelles données.
tableHeadersRow.innerHTML = ”;
tableBody.innerHTML = ”;
noResultsMessage.classList.add(‘hidden’); // Masque le message “aucun résultat” par défaut.
// 2. Générer les en-têtes de colonne
category.headers.forEach((headerText, index) => {
const th = document.createElement(‘th’);
th.textContent = headerText;
th.classList.add(‘px-6’, ‘py-3’, ‘text-left’, ‘text-xs’, ‘font-medium’, ‘text-indigo-700’, ‘uppercase’, ‘tracking-wider’, ‘cursor-pointer’, ‘select-none’, ‘hover:bg-indigo-100’, ‘transition-colors’, ‘duration-200’);
th.setAttribute(‘scope’, ‘col’); // Pour l’accessibilité : indique que c’est un en-tête de colonne.
th.setAttribute(‘aria-sort’, ‘none’); // Indique que la colonne n’est pas triée initialement.
th.tabIndex = 0; // Rend l’en-tête focusable au clavier.
// Ajoute un indicateur visuel de tri si cette colonne est actuellement triée.
if (index === currentSortColumn) {
th.classList.add(‘bg-indigo-200’); // Style pour la colonne actuellement triée.
const sortArrow = document.createElement(‘span’);
sortArrow.classList.add(‘ml-2’, ‘inline-block’);
sortArrow.innerHTML = currentSortDirection === ‘asc’ ? ‘▲’ : ‘▼’; // Flèche haut ou bas.
th.appendChild(sortArrow);
th.setAttribute(‘aria-sort’, currentSortDirection === ‘asc’ ? ‘ascending’ : ‘descending’);
}
// Gestionnaire d’événement pour le tri au clic sur l’en-tête.
th.addEventListener(‘click’, () => sortTable(index));
// Gestionnaire d’événement pour le tri au clavier (touche Entrée ou Espace).
th.addEventListener(‘keydown’, (e) => {
if (e.key === ‘Enter’ || e.key === ‘ ‘) {
e.preventDefault(); // Empêche le défilement de la page avec la barre d’espace.
sortTable(index);
}
});
tableHeadersRow.appendChild(th);
});
// 3. Filtrer les lignes de données en fonction du terme de recherche.
let filteredRows = category.rows.filter(row => {
if (!searchTerm) return true; // Si le terme de recherche est vide, toutes les lignes sont affichées.
return row.some(cell => // Vérifie si le terme de recherche se trouve dans n’importe quelle cellule de la ligne.
String(cell).toLowerCase().includes(searchTerm.toLowerCase())
);
});
// 4. Trier les lignes filtrées si une colonne de tri est sélectionnée.
if (currentSortColumn !== -1) {
filteredRows.sort((a, b) => {
const valA = String(a[currentSortColumn]).toLowerCase();
const valB = String(b[currentSortColumn]).toLowerCase();
// Gestion spécifique pour le tri des coûts (tentative de tri numérique).
if (currentSortColumn === 1) { // L’index 1 correspond à “Coût estimé (EUR)”.
// Extrait les nombres des chaînes (ex: “50-100” devient 50 ou 100, “Gratuit” devient NaN).
const numA = parseFloat(valA.replace(/[^0-9.]/g, ”)) || 0; // Utilise 0 si pas de nombre valide.
const numB = parseFloat(valB.replace(/[^0-9.]/g, ”)) || 0;
if (!isNaN(numA) && !isNaN(numB)) {
return currentSortDirection === ‘asc’ ? numA – numB : numB – numA;
}
}
// Tri alphabétique par défaut pour les autres colonnes.
if (valA valB) return currentSortDirection === ‘asc’ ? 1 : -1;
return 0; // Les valeurs sont égales.
});
}
// 5. Rendre les lignes de données dans le corps du tableau.
if (filteredRows.length === 0) {
noResultsMessage.classList.remove(‘hidden’); // Affiche le message s’il n’y a pas de résultats.
} else {
filteredRows.forEach(row => {
const tr = document.createElement(‘tr’);
tr.classList.add(‘bg-white’, ‘hover:bg-indigo-50’, ‘transition-colors’, ‘duration-150’); // Effet visuel au survol.
row.forEach((cellData, index) => {
const td = document.createElement(‘td’);
td.textContent = cellData;
td.classList.add(‘px-6’, ‘py-4’, ‘whitespace-normal’, ‘text-sm’, ‘text-gray-700’); // ‘whitespace-normal’ permet au texte de s’enrouler.
if (index === 0) { // Mets la première colonne en gras pour la rendre plus distinctive.
td.classList.add(‘font-medium’, ‘text-gray-900’);
}
tr.appendChild(td);
});
tableBody.appendChild(tr);
});
}
}
// — Fonction pour gérer le tri des colonnes —
// columnIndex : l’index de la colonne sur laquelle l’utilisateur a cliqué.
function sortTable(columnIndex) {
if (currentSortColumn === columnIndex) {
// Si on clique sur la même colonne, on inverse la direction du tri.
currentSortDirection = currentSortDirection === ‘asc’ ? ‘desc’ : ‘asc’;
} else {
// Si on clique sur une nouvelle colonne, on la définit comme colonne de tri et on réinitialise la direction à ‘asc’.
currentSortColumn = columnIndex;
currentSortDirection = ‘asc’;
}
// Re-rend le tableau avec les données triées et le terme de recherche actuel.
renderTable(currentCategoryIndex, searchInput.value);
}
// — Fonction pour générer et rendre les boutons d’onglets de catégorie —
function renderCategoryTabs() {
categoryTabsContainer.innerHTML = ”; // Vide le conteneur des onglets.
dubaiData.forEach((category, index) => {
const button = document.createElement(‘button’);
button.textContent = category.title;
button.classList.add(‘px-5’, ‘py-2’, ‘rounded-md’, ‘font-medium’, ‘text-sm’, ‘transition-all’, ‘duration-200’, ‘ease-in-out’, ‘focus:outline-none’, ‘focus:ring-2’, ‘focus:ring-indigo-400’, ‘focus:ring-offset-2’);
button.setAttribute(‘role’, ‘tab’); // Pour l’accessibilité : indique que c’est un onglet.
button.setAttribute(‘aria-selected’, index === currentCategoryIndex ? ‘true’ : ‘false’); // Indique si l’onglet est sélectionné.
button.tabIndex = index === currentCategoryIndex ? 0 : -1; // Rend l’onglet actif focusable, les autres non.
// Applique les styles CSS Tailwind en fonction de l’état sélectionné.
if (index === currentCategoryIndex) {
button.classList.add(‘bg-indigo-600’, ‘text-white’, ‘shadow-md’, ‘hover:bg-indigo-700’);
} else {
button.classList.add(‘bg-indigo-200’, ‘text-indigo-800’, ‘hover:bg-indigo-300’);
}
// Gestionnaire d’événement au clic sur un onglet.
button.addEventListener(‘click’, () => {
currentCategoryIndex = index; // Met à jour l’index de la catégorie active.
currentSortColumn = -1; // Réinitialise le tri lors du changement de catégorie.
currentSortDirection = ‘asc’; // Réinitialise la direction du tri.
renderCategoryTabs(); // Met à jour l’apparence des onglets.
renderTable(currentCategoryIndex, searchInput.value); // Affiche le nouveau tableau.
});
// Gestion de la navigation clavier pour les onglets (flèches gauche/droite).
button.addEventListener(‘keydown’, (e) => {
const buttons = Array.from(categoryTabsContainer.children);
let nextIndex = index;
if (e.key === ‘ArrowRight’) {
nextIndex = (index + 1) % buttons.length;
} else if (e.key === ‘ArrowLeft’) {
nextIndex = (index – 1 + buttons.length) % buttons.length;
}
if (nextIndex !== index) {
buttons[nextIndex].focus(); // Déplace le focus vers le prochain onglet.
e.preventDefault(); // Empêche le défilement de la page.
}
});
categoryTabsContainer.appendChild(button);
});
}
// — Initialisation du composant au chargement du DOM —
document.addEventListener(‘DOMContentLoaded’, () => {
// 1. Rendre les onglets de catégorie.
renderCategoryTabs();
// 2. Rendre le premier tableau par défaut (première catégorie).
renderTable(currentCategoryIndex);
// 3. Attacher un écouteur d’événements au champ de recherche.
// Chaque fois que l’utilisateur tape, le tableau est re-rendu avec le filtre appliqué.
searchInput.addEventListener(‘input’, () => {
renderTable(currentCategoryIndex, searchInput.value);
});
// 4. Assurez-vous que Tailwind CSS v4 est bien initialisé.
// Le script CDN `@tailwindcss/browser@4` s’exécute généralement automatiquement
// et scanne le DOM pour appliquer les classes.
// Aucune action manuelle spécifique n’est généralement requise pour v4 après le chargement du DOM.
});
// Fin du script JavaScript
{“@context”:”https://schema.org”,”@type”:”FAQPage”,”mainEntity”:[{“@type”:”Question”,”name”:”Quel type de vu00eatements emporter pour Dubau00ef en octobre ? “,”acceptedAnswer”:{“@type”:”Answer”,”text”:”Il est recommandu00e9 d’emporter des vu00eatements lu00e9gers et respirants (coton, lin) pour la journu00e9e, car les tempu00e9ratures restent chaudes (30-35u00b0C). Pru00e9voyez un maillot de bain pour les plages et parcs aquatiques. Pour les soiru00e9es, une veste lu00e9gu00e8re ou un chu00e2le peut u00eatre utile car les tempu00e9ratures descendent u00e0 25u00b0C et la climatisation est forte u00e0 l’intu00e9rieur. Pour les visites de sites religieux, des vu00eatements couvrants les u00e9paules et les genoux sont essentiels.”}},{“@type”:”Question”,”name”:”Est-ce que Dubau00ef est trop fru00e9quentu00e9e en octobre ? “,”acceptedAnswer”:{“@type”:”Answer”,”text”:”Octobre marque le du00e9but de la haute saison touristique u00e0 Dubau00ef. Il y a plus de monde qu’en u00e9tu00e9, mais la fru00e9quentation est gu00e9nu00e9ralement modu00e9ru00e9e comparu00e9e aux pics de du00e9cembre ou janvier. Les foules sont gu00e9rables, et il est possible de profiter des attractions sans trop d’attente en planifiant ses visites tu00f4t le matin ou en semaine.”}},{“@type”:”Question”,”name”:”Les prix sont-ils u00e9levu00e9s u00e0 Dubau00ef en octobre ? “,”acceptedAnswer”:{“@type”:”Answer”,”text”:”Les prix des vols et des hu00e9bergements commencent u00e0 augmenter en octobre par rapport u00e0 l’u00e9tu00e9, car c’est le du00e9but de la saison favorable. Cependant, ils restent souvent plus avantageux qu’en pleine haute saison (novembre u00e0 fu00e9vrier). Il est fortement conseillu00e9 de ru00e9server ses vols et hu00f4tels plusieurs mois u00e0 l’avance pour bu00e9nu00e9ficier des meilleurs tarifs.”}},{“@type”:”Question”,”name”:”Quelles sont les activitu00e9s incontournables en octobre ? “,”acceptedAnswer”:{“@type”:”Answer”,”text”:”En octobre, profitez du Global Village (qui rouvre), des parcs aquatiques comme Aquaventure, des safaris dans le du00e9sert, des visites de la Mosquu00e9e de Jumeirah et des souks traditionnels. Les plages sont u00e9galement tru00e8s agru00e9ables pour la baignade et la du00e9tente.”}}]}