// Données fournies par l’utilisateur, intégrées directement pour cette démonstration.
//
// Contrainte : APIs/Services 100 % gratuits – Inclure l’URL de l’API et un exemple de réponse JSON en commentaire.
// Étant donné que les données sont directement fournies dans l’énoncé, l’outil ne “nécessite” pas
// de données externes pour fonctionner. Cependant, pour illustrer comment une API serait utilisée,
// voici un exemple structurel :
//
// URL d’API hypothétique (illustrative, non réelle pour ces données spécifiques et gratuites) :
// const API_URL = “https://api.example.com/hurghada-activities”;
//
// Structure d’une réponse JSON d’exemple (similaire aux données fournies) :
/*
[
{
“activité”: “Plongée sous-marine”,
“description”: “Exploration des récifs coralliens et épaves”,
“saison_ideale”: “Novembre, Mai, Octobre”,
“niveau_requis”: “Débutant à Expert”,
“prix_estime_egp”: “800-2500”
},
// … d’autres activités
]
*/
// Si les données devaient être récupérées depuis une API, le processus ressemblerait à ceci :
/*
async function fetchData() {
try {
const response = await fetch(API_URL);
if (!response.ok) throw new Error(`Erreur HTTP ! Statut: ${response.status}`);
const apiData = await response.json();
// Transformer les données de l’API pour correspondre à la structure ‘hurghadaData’ si nécessaire
// Par exemple, si l’API retourne un tableau d’objets, convertir en tableau de tableaux pour ‘rows’
hurghadaData.headers = Object.keys(apiData[0]).map(key => key.replace(/_/g, ‘ ‘)); // Extraction des en-têtes
hurghadaData.rows = apiData.map(item => Object.values(item)); // Extraction des lignes
initializeTable(); // Puis initialiser le tableau avec les données récupérées
} catch (error) {
console.error(“Échec de la récupération des données :”, error);
// Gérer l’erreur, par exemple, afficher un message à l’utilisateur
}
}
*/
// Pour cette tâche spécifique, nous utilisons les données intégrées fournies dans l’énoncé.
const hurghadaData = {
“title”: “Comparaison des Activités Aquatiques à Hurghada”,
“headers”: [“Activité”, “Description”, “Saison Idéale”, “Niveau requis”, “Prix estimé (EGP)”],
“rows”: [
[“Plongée sous-marine”, “Exploration des récifs coralliens et épaves”, “Novembre, Mai, Octobre”, “Débutant à Expert”, “800-2500”],
[“Snorkeling”, “Observation des poissons en surface”, “Toute l’année”, “Aucun”, “300-800”],
[“Kitesurf”, “Glisse sur l’eau avec une voile tractée”, “Avril-Octobre (vent fort)”, “Intermédiaire à Expert”, “1500-4000 (cours inclus)”],
[“Windsurf”, “Navigation sur l’eau avec une planche à voile”, “Avril-Octobre (vent fort)”, “Débutant à Expert”, “1000-3000 (cours inclus)”],
[“Excursion en bateau / Île Giftun”, “Découverte des îles et plages”, “Toute l’année”, “Aucun”, “500-1200”],
[“Quad dans le désert”, “Aventure motorisée dans le sable”, “Toute l’année (matin/fin d’après-midi)”, “Aucun”, “400-1000”],
[“Safari en Jeep”, “Exploration du désert et culture bédouine”, “Toute l’année (matin/fin d’après-midi)”, “Aucun”, “600-1500″]
]
};
// Variables pour gérer l’état de tri du tableau
let currentSortColumn = null; // Index de la colonne actuellement triée
let currentSortDirection = ‘asc’; // Direction du tri : ‘asc’ (ascendant) ou ‘desc’ (descendant)
let currentFilteredRows = […hurghadaData.rows]; // Copie mutable des lignes pour le filtrage et le tri
/**
* Rend dynamiquement les en-têtes du tableau.
* Chaque en-tête est rendu cliquable pour le tri et inclut un attribut ARIA pour l’accessibilité.
*/
function renderTableHeaders() {
const tableHead = document.querySelector(‘#hurghada-activities-comparator thead’);
tableHead.innerHTML = ”; // Efface les en-têtes précédemment rendus
const headerRow = document.createElement(‘tr’);
headerRow.className = ‘border-b border-blue-200’;
hurghadaData.headers.forEach((headerText, index) => {
const th = document.createElement(‘th’);
th.scope = ‘col’; // Sémantique pour les en-têtes de tableau (accessibilité)
th.className = ‘px-6 py-3 text-left font-medium text-blue-700 tracking-wider cursor-pointer select-none relative’;
th.textContent = headerText;
th.setAttribute(‘data-column-index’, index); // Attribut personnalisé pour identifier la colonne pour le tri
th.setAttribute(‘aria-sort’, ‘none’); // Attribut ARIA pour les lecteurs d’écran pour indiquer l’état de tri
// Crée un span pour l’indicateur visuel de tri (flèche)
const sortIndicator = document.createElement(‘span’);
// ‘opacity-0’ le rend invisible par défaut, ‘ml-2’ pour la marge, ‘text-lg leading-none align-middle’ pour le style
sortIndicator.className = ‘ml-2 text-blue-500 opacity-0 transition-opacity duration-200 text-lg leading-none align-middle’;
th.appendChild(sortIndicator);
// Ajoute un écouteur d’événements pour trier lorsque l’en-tête est cliqué
th.addEventListener(‘click’, () => sortTable(index));
headerRow.appendChild(th);
});
tableHead.appendChild(headerRow);
// Après le rendu, si un état de tri était actif, réapplique l’indicateur visuel sur l’en-tête concerné
if (currentSortColumn !== null) {
const activeHeader = document.querySelector(`#hurghada-activities-comparator thead th[data-column-index=”${currentSortColumn}”]`);
if (activeHeader) {
activeHeader.setAttribute(‘aria-sort’, currentSortDirection === ‘asc’ ? ‘ascending’ : ‘descending’);
const activeIndicator = activeHeader.querySelector(‘span’);
if (activeIndicator) {
activeIndicator.textContent = currentSortDirection === ‘asc’ ? ‘▲’ : ‘▼’;
activeIndicator.classList.remove(‘opacity-0’);
}
}
}
}
/**
* Rend le corps du tableau avec le tableau de lignes donné.
* Gère la visibilité du message “aucun résultat”.
* @param {Array<Array>} rowsToDisplay – Les lignes de données à afficher dans le tableau.
*/
function renderTableBody(rowsToDisplay) {
const tableBody = document.getElementById(‘activities-table-body’);
const noResultsMessage = document.getElementById(‘no-results’);
tableBody.innerHTML = ”; // Efface les lignes existantes pour éviter les doublons
// Affiche/masque le message “aucun résultat” en fonction du nombre de lignes
if (rowsToDisplay.length === 0) {
noResultsMessage.classList.remove(‘hidden’);
return; // Sort de la fonction si aucune ligne à afficher
} else {
noResultsMessage.classList.add(‘hidden’);
}
// Itère sur chaque ligne de données et crée les éléments de tableau (
et
)
rowsToDisplay.forEach(rowData => {
const tr = document.createElement(‘tr’);
tr.className = ‘bg-white hover:bg-blue-50 transition duration-150 ease-in-out border-b border-blue-100’;
rowData.forEach((cellData, index) => {
const td = document.createElement(‘td’);
// Applique un style spécifique pour la première colonne (Nom de l’activité) pour l’emphase
td.className = `px-6 py-4 whitespace-normal ${index === 0 ? ‘font-semibold text-blue-900’ : ‘text-gray-700’}`;
td.textContent = cellData;
tr.appendChild(td);
});
tableBody.appendChild(tr);
});
}
/**
* Trie les lignes filtrées actuelles en fonction de la colonne sélectionnée et met à jour le tableau.
* Inverse la direction de tri si le même en-tête de colonne est cliqué plusieurs fois.
* @param {number} columnIndex – L’index de la colonne sur laquelle trier.
*/
function sortTable(columnIndex) {
// Sélectionne tous les en-têtes pour mettre à jour leurs attributs ARIA et indicateurs visuels
const headers = document.querySelectorAll(‘#hurghada-activities-comparator thead th’);
// Réinitialise les attributs ARIA et les indicateurs visuels de tri pour tous les en-têtes
headers.forEach(th => {
th.setAttribute(‘aria-sort’, ‘none’);
const indicator = th.querySelector(‘span’);
if (indicator) {
indicator.textContent = ”;
indicator.classList.add(‘opacity-0’);
}
});
// Détermine la nouvelle direction de tri : inverse si la même colonne, ou ‘asc’ par défaut pour une nouvelle colonne
if (currentSortColumn === columnIndex) {
currentSortDirection = (currentSortDirection === ‘asc’ ? ‘desc’ : ‘asc’);
} else {
currentSortColumn = columnIndex;
currentSortDirection = ‘asc’;
}
// Trie le tableau `currentFilteredRows` (qui contient les données actuellement affichées)
currentFilteredRows.sort((a, b) => {
const valA = String(a[columnIndex]).toLowerCase();
const valB = String(b[columnIndex]).toLowerCase();
// Traitement spécial pour la colonne ‘Prix estimé (EGP)’ pour un tri numérique
if (columnIndex === hurghadaData.headers.indexOf(‘Prix estimé (EGP)’)) {
// Extrait le premier nombre de la plage de prix (ex: “800-2500” devient 800)
const numA = parseFloat(valA.split(‘-‘)[0]);
const numB = parseFloat(valB.split(‘-‘)[0]);
if (!isNaN(numA) && !isNaN(numB)) { // S’assure que les deux sont des nombres valides avant de comparer
return currentSortDirection === ‘asc’ ? numA – numB : numB – numA;
}
}
// Comparaison de chaînes par défaut pour toutes les autres colonnes
if (valA valB) {
return currentSortDirection === ‘asc’ ? 1 : -1;
}
return 0; // Les valeurs sont égales
});
// Met à jour l’attribut ARIA ‘aria-sort’ et l’indicateur visuel pour la colonne activement triée
const activeHeader = headers[columnIndex];
activeHeader.setAttribute(‘aria-sort’, currentSortDirection === ‘asc’ ? ‘ascending’ : ‘descending’);
const activeIndicator = activeHeader.querySelector(‘span’);
if (activeIndicator) {
activeIndicator.textContent = currentSortDirection === ‘asc’ ? ‘▲’ : ‘▼’; // Flèche haut ou bas
activeIndicator.classList.remove(‘opacity-0’); // Rend l’indicateur visible
}
renderTableBody(currentFilteredRows); // Re-rend le tableau avec les données nouvellement triées
}
/**
* Filtre les lignes du tableau en fonction de la valeur de l’entrée de recherche.
* Réapplique le tri actuel après le filtrage.
*/
function filterTable() {
const searchTerm = document.getElementById(‘search-input’).value.toLowerCase().trim();
// Filtre `hurghadaData.rows` (les données originales, non triées)
currentFilteredRows = hurghadaData.rows.filter(row => {
// Vérifie si une cellule de la ligne contient le terme de recherche (insensible à la casse)
return row.some(cellData =>
String(cellData).toLowerCase().includes(searchTerm)
);
});
// Si une colonne était précédemment triée, réapplique ce tri aux résultats filtrés
if (currentSortColumn !== null) {
currentFilteredRows.sort((a, b) => {
const valA = String(a[currentSortColumn]).toLowerCase();
const valB = String(b[currentSortColumn]).toLowerCase();
// Traitement spécial pour le tri numérique de la colonne de prix
if (currentSortColumn === hurghadaData.headers.indexOf(‘Prix estimé (EGP)’)) {
const numA = parseFloat(valA.split(‘-‘)[0]);
const numB = parseFloat(valB.split(‘-‘)[0]);
if (!isNaN(numA) && !isNaN(numB)) {
return currentSortDirection === ‘asc’ ? numA – numB : numB – numA;
}
}
if (valA valB) {
return currentSortDirection === ‘asc’ ? 1 : -1;
}
return 0;
});
}
renderTableBody(currentFilteredRows); // Re-rend le tableau avec les données filtrées (et potentiellement triées)
}
/**
* Une fonction de “debounce” simple pour limiter la fréquence d’appel d’une fonction.
* Utile pour l’optimisation des performances sur des événements comme `keyup`.
* @param {Function} func – La fonction à “debouncer”.
* @param {number} delay – Le délai en millisecondes.
* @returns {Function} Une version “debounced” de la fonction d’entrée.
*/
function debounce(func, delay) {
let timeout;
return function(…args) {
const context = this;
clearTimeout(timeout);
timeout = setTimeout(() => func.apply(context, args), delay);
};
}
/**
* Initialise le tableau et configure tous les écouteurs d’événements lorsque le DOM est entièrement chargé.
*/
function initializeTable() {
// Rend les en-têtes initiaux et le corps du tableau avec toutes les données
renderTableHeaders();
renderTableBody(hurghadaData.rows);
// Récupère les éléments DOM pour l’interactivité
const searchInput = document.getElementById(‘search-input’);
const clearSearchButton = document.getElementById(‘clear-search’);
// Attache l’écouteur d’événements pour l’entrée de recherche, “debounced” pour de meilleures performances
searchInput.addEventListener(‘keyup’, debounce(filterTable, 300));
// Attache l’écouteur d’événements pour le bouton “Effacer la recherche”
clearSearchButton.addEventListener(‘click’, () => {
searchInput.value = ”; // Efface le champ de recherche
currentFilteredRows = […hurghadaData.rows]; // Réinitialise les lignes filtrées au jeu de données original
currentSortColumn = null; // Réinitialise l’état de tri
currentSortDirection = ‘asc’; // Réinitialise la direction de tri
renderTableHeaders(); // Re-rend les en-têtes pour effacer les indicateurs de tri actifs
renderTableBody(currentFilteredRows); // Re-rend le tableau avec toutes les données originales
});
}
// S’assure que le DOM est entièrement chargé avant d’essayer d’accéder aux éléments et d’initialiser le tableau.
document.addEventListener(‘DOMContentLoaded’, initializeTable);
{“@context”:”https://schema.org”,”@type”:”FAQPage”,”mainEntity”:[{“@type”:”Question”,”name”:”Pourquoi choisir Hurghada en novembre pour un voyage ? “,”acceptedAnswer”:{“@type”:”Answer”,”text”:”Novembre offre un climat idu00e9al u00e0 Hurghada, avec des tempu00e9ratures douces autour de 25-27 u00b0C le jour et une mer Rouge u00e0 24-26 u00b0C. C’est parfait pour la plage, la plongu00e9e et les excursions sans la chaleur extru00eame de l’u00e9tu00e9 ou la foule de la haute saison.”}},{“@type”:”Question”,”name”:”Quelles sont les activitu00e9s phares u00e0 Hurghada en novembre ? “,”acceptedAnswer”:{“@type”:”Answer”,”text”:”Les activitu00e9s incontournables incluent la plongu00e9e sous-marine et le snorkeling dans la mer Rouge pour admirer les ru00e9cifs coralliens. Les excursions dans le du00e9sert (quad, buggy, chameau) sont u00e9galement tru00e8s populaires gru00e2ce aux tempu00e9ratures clu00e9mentes. Les visites des u00eeles Giftun sont aussi un must.”}},{“@type”:”Question”,”name”:”Faut-il ru00e9server les excursions u00e0 l’avance en novembre ? “,”acceptedAnswer”:{“@type”:”Answer”,”text”:”Bien que novembre soit considu00e9ru00e9 comme une pu00e9riode d’entre-saison, il est fortement recommandu00e9 de ru00e9server les excursions populaires, notamment celles de plongu00e9e ou les sorties vers les sites historiques comme Louxor, quelques jours u00e0 l’avance pour garantir la disponibilitu00e9 et les meilleurs tarifs.”}},{“@type”:”Question”,”name”:”Quels vu00eatements emporter pour un su00e9jour en novembre u00e0 Hurghada ? “,”acceptedAnswer”:{“@type”:”Answer”,”text”:”Pru00e9voyez des vu00eatements lu00e9gers pour la journu00e9e (coton, lin), un maillot de bain, un chapeau, des lunettes de soleil et de la cru00e8me solaire. Pour les soiru00e9es, un pull lu00e9ger ou une veste fine sera appru00e9ciu00e9 car les tempu00e9ratures peuvent descendre aux alentours de 19-22 u00b0C. Des chaussures confortables pour les explorations sont u00e9galement essentielles.”}},{“@type”:”Question”,”name”:”Est-ce que l’eau est assez chaude pour la baignade en novembre ? “,”acceptedAnswer”:{“@type”:”Answer”,”text”:”Absolument ! La tempu00e9rature de la mer Rouge en novembre se situe gu00e9nu00e9ralement entre 24 et 26 u00b0C, ce qui est tru00e8s agru00e9able et permet de longues sessions de baignade, de snorkeling et de plongu00e9e sous-marine sans aucun problu00e8me.”}}]}