// — Données pour le tableau (intégrées pour éviter l’API externe pour cette donnée spécifique) —
// Rationale: L’information “destinations pour 500€ tout compris” est très spécifique et ne proviendrait pas d’une API publique générique sans clé.
// La donnée ci-dessous est une extension du fragment fourni par l’utilisateur, complétée pour créer un tableau cohérent.
const travelData = {
“title”: “Comparaison de destinations européennes abordables (Objectif 500€, tout compris)”,
“description”: “Ce tableau compare des destinations où un voyage « tout compris » (transport, hébergement, repas, activités) pour environ 500€ est envisageable, souvent pour une courte durée, en optant pour des solutions très économiques ou en profitant de promotions. Les prix peuvent varier fortement selon la saison, la flexibilité des dates et les offres disponibles.”,
“headers”: [
{ “id”: “destination”, “name”: “Destination” },
{ “id”: “period”, “name”: “Période Idéale” },
{ “id”: “experience”, “name”: “Type d’Expérience” },
{ “id”: “activities”, “name”: “Activités Phares (pour 500€)” },
{ “id”: “duration”, “name”: “Durée Estimée” },
{ “id”: “budgetTip”, “name”: “Conseil Budget (500€)” }
],
“data”: [
{
“destination”: “Lisbonne, Portugal”,
“period”: “Printemps, Automne”,
“experience”: “Culture, Ville, Gastronomie”,
“activities”: “Exploration des quartiers historiques (Alfama), dégustation de Pastel de Nata, tram 28, visite de musées gratuits. Hébergement en auberge de jeunesse.”,
“duration”: “2-3 jours”,
“budgetTip”: “Vols : Rechercher activement les offres low-cost bien à l’avance. Sur place : Repas locaux bon marché et transports en commun efficaces.”
},
{
“destination”: “Budapest, Hongrie”,
“period”: “Printemps, Été, Automne”,
“experience”: “Culture, Bains thermaux, Vie nocturne”,
“activities”: “Bains Széchenyi (entrée), Parlement (extérieur), Pont des Chaînes, Ruin Bars, marché central. Hébergement en auberge ou petit hôtel économique.”,
“duration”: “3-4 jours”,
“budgetTip”: “Coût de la vie très abordable sur place. Les vols peuvent être le point le plus onéreux, à réserver très tôt et hors périodes de pointe.”
},
{
“destination”: “Cracovie, Pologne”,
“period”: “Printemps, Été, Automne”,
“experience”: “Histoire, Culture, Ville”,
“activities”: “Place du Marché, Château de Wawel (extérieur/partie gratuite), Quartier juif Kazimierz, musées locaux. Repas traditionnels bon marché.”,
“duration”: “3-4 jours”,
“budgetTip”: “Excellente destination pour un budget serré. Transport public efficace et peu cher. Viser les vols low-cost depuis les grands aéroports.”
},
{
“destination”: “Riga, Lettonie”,
“period”: “Été (pour le temps), Hiver (marchés de Noël)”,
“experience”: “Architecture Art Nouveau, Culture Baltique”,
“activities”: “Vieille ville médiévale, Marché Central, promenade le long de la Daugava, quartiers Art Nouveau. Hébergement en auberge.”,
“duration”: “2-3 jours”,
“budgetTip”: “Petite capitale facile à explorer à pied. Très abordable sur place. Vols low-cost à surveiller attentivement et flexibilité sur les dates.”
},
{
“destination”: “Sofia, Bulgarie”,
“period”: “Printemps, Automne”,
“experience”: “Culture Orthodoxe, Montagnes, Ville”,
“activities”: “Cathédrale Alexandre Nevski, Vitosha Boulevard, Église de Boyana (UNESCO, si budget), Musées gratuits. Repas locaux.”,
“duration”: “3-4 jours”,
“budgetTip”: “Très bon marché une fois sur place. Moins touristique, donc potentiellement plus authentique pour un petit budget. Les vols peuvent être plus chers et moins fréquents.”
},
{
“destination”: “Porto, Portugal”,
“period”: “Printemps, Automne”,
“experience”: “Culture, Gastronomie, Vin”,
“activities”: “Ribeira, Tour des Clérigos (vue), dégustation de Porto (simple), librairie Lello (extérieur/coût si entrée), tramway. Hébergement en auberge.”,
“duration”: “2-3 jours”,
“budgetTip”: “Similaire à Lisbonne, les vols sont clés. Se concentrer sur les activités gratuites ou très abordables. Profiter des spécialités locales.”
},
{
“destination”: “Bucarest, Roumanie”,
“period”: “Printemps, Automne”,
“experience”: “Architecture, Histoire, Vie nocturne”,
“activities”: “Palais du Parlement (extérieur), Vieille Ville, musées comme le Musée National d’Art. Découverte de la cuisine roumaine à prix doux.”,
“duration”: “3-4 jours”,
“budgetTip”: “Coût de la vie très bas. Les vols low-cost vers Bucarest sont courants. Idéal pour un long week-end économique.”
}
]
};
// — Variables d’état pour le tri et le filtre —
let currentSortColumn = null; // ID de la colonne triée actuellement
let sortDirection = ‘asc’; // Direction du tri (‘asc’ pour ascendant, ‘desc’ pour descendant)
let filteredData = travelData.data; // Données actuellement affichées (filtrées et potentiellement triées)
// — Références aux éléments DOM —
const appContainer = document.getElementById(‘travel-comparator-app’);
const tableHeadersRow = document.getElementById(‘table-headers’);
const tableBody = document.getElementById(‘table-body’);
const searchInput = document.getElementById(‘searchInput’);
// — Fonction pour initialiser l’en-tête du tableau —
function initializeHeader() {
// Met à jour le titre et la description dans les éléments H2 et P dédiés.
appContainer.querySelector(‘h2’).textContent = travelData.title;
appContainer.querySelector(‘p’).textContent = travelData.description;
tableHeadersRow.innerHTML = ”; // Nettoyer les en-têtes existants pour éviter les doublons
// Parcourt la liste des en-têtes définis dans travelData pour créer les TH
travelData.headers.forEach(header => {
const th = document.createElement(‘th’);
th.scope = “col”; // Attribut pour l’accessibilité
// Classes Tailwind pour le style, l’interactivité (cursor-pointer, group) et le texte
th.className = “px-4 py-3 text-left text-xs font-medium uppercase tracking-wider cursor-pointer select-none group relative”;
th.textContent = header.name;
th.dataset.columnId = header.id; // Stocke l’ID de la colonne pour le tri JavaScript
// Ajout de l’icône de tri (initialement masquée ou invisible)
const sortIcon = document.createElement(‘span’);
// Classes pour positionner l’icône et la rendre visible au survol (group-hover)
sortIcon.className = “ml-2 text-white opacity-0 group-hover:opacity-75 transition-opacity duration-200 absolute right-2 top-1/2 -translate-y-1/2″;
sortIcon.setAttribute(‘aria-hidden’, ‘true’); // Cache l’icône des lecteurs d’écran car le tri est aussi annoncé
th.appendChild(sortIcon);
// Ajout de l’écouteur d’événement pour le tri au clic sur l’en-tête
th.addEventListener(‘click’, () => handleSort(header.id));
tableHeadersRow.appendChild(th);
});
}
// — Fonction pour rendre (afficher) les lignes du tableau —
function renderTableRows(dataToRender) {
tableBody.innerHTML = ”; // Nettoyer le corps du tableau avant de le remplir
// Message si aucune donnée ne correspond au filtre
if (dataToRender.length === 0) {
const noResultsRow = document.createElement(‘tr’);
// Colspan doit correspondre au nombre réel de colonnes
noResultsRow.innerHTML = `
Aucune destination trouvée correspondant à votre recherche.`;
tableBody.appendChild(noResultsRow);
return;
}
// Parcourt les données à rendre et crée une ligne de tableau pour chaque élément
dataToRender.forEach((rowData, index) => {
const tr = document.createElement(‘tr’);
// Alternance de couleurs pour une meilleure lisibilité et effet hover
tr.className = index % 2 === 0 ? ‘bg-white’ : ‘bg-gray-50’;
tr.classList.add(‘hover:bg-blue-50’, ‘transition-colors’, ‘duration-150’);
// Pour chaque en-tête, crée une cellule de données (TD)
travelData.headers.forEach(header => {
const td = document.createElement(‘td’);
// Classes pour le style des cellules
td.className = “px-4 py-3 text-sm text-gray-700 align-top”;
td.textContent = rowData[header.id] || ”; // Affiche la donnée correspondante, ou vide si non définie
tr.appendChild(td);
});
tableBody.appendChild(tr);
});
}
// — Fonction pour mettre à jour les icônes de tri dans les en-têtes —
// Affiche une flèche montrant la direction de tri pour la colonne active.
function updateSortIcons() {
tableHeadersRow.querySelectorAll(‘th’).forEach(th => {
const icon = th.querySelector(‘span’);
if (icon) {
icon.innerHTML = ”; // Nettoie l’icône existante
// Réinitialise les classes d’opacité et de couleur pour toutes les icônes
icon.classList.remove(‘opacity-100’, ‘text-white’);
icon.classList.add(‘opacity-0’); // Cache l’icône par défaut
if (th.dataset.columnId === currentSortColumn) {
// Si c’est la colonne triée, rend l’icône visible et définit la flèche
icon.classList.remove(‘opacity-0’, ‘group-hover:opacity-75’); // Retire l’opacité par défaut et hover
icon.classList.add(‘opacity-100’, ‘text-white’); // Rend l’icône entièrement visible et blanche
icon.innerHTML = sortDirection === ‘asc’ ? ‘ ↑’ : ‘ ↓’; // Flèche haut/bas
// Ajout d’un aria-sort pour l’accessibilité
th.setAttribute(‘aria-sort’, sortDirection === ‘asc’ ? ‘ascending’ : ‘descending’);
} else {
// Pour les colonnes non triées, assure que aria-sort est ‘none’
th.setAttribute(‘aria-sort’, ‘none’);
// Assure que l’icône est cachée mais réapparaît au hover
icon.classList.add(‘opacity-0’, ‘group-hover:opacity-75’);
}
}
});
}
// — Fonction de gestion du tri au clic sur l’en-tête de colonne —
function handleSort(columnId) {
if (currentSortColumn === columnId) {
// Si la même colonne est cliquée, inverse la direction de tri
sortDirection = sortDirection === ‘asc’ ? ‘desc’ : ‘asc’;
} else {
// Si une nouvelle colonne est cliquée, définit la colonne et trie par défaut en ascendant
currentSortColumn = columnId;
sortDirection = ‘asc’;
}
// Applique le tri sur les données actuellement filtrées (filteredData)
// Utilise une copie pour éviter de modifier directement le tableau original lors du tri
const sortedData = […filteredData].sort((a, b) => {
const valA = String(a[columnId] || ”).toLowerCase(); // Assure que la valeur est une chaîne et gère les null/undefined
const valB = String(b[columnId] || ”).toLowerCase();
if (valA valB) return sortDirection === ‘asc’ ? 1 : -1;
return 0; // Les valeurs sont égales
});
filteredData = sortedData; // Met à jour filteredData avec les données triées
renderTableRows(filteredData); // Re-rend le tableau avec les données triées
updateSortIcons(); // Met à jour les icônes de tri dans les en-têtes
}
// — Fonction de gestion du filtre de recherche par mot-clé —
function handleSearch() {
const searchTerm = searchInput.value.toLowerCase().trim(); // Convertit en minuscules et retire les espaces superflus
// Filtre les données originales (travelData.data) pour permettre une recherche complète
let searchResults = travelData.data.filter(rowData => {
// Vérifie si le terme de recherche est présent dans n’importe quelle cellule de la ligne
return travelData.headers.some(header => {
const cellValue = String(rowData[header.id] || ”).toLowerCase();
return cellValue.includes(searchTerm);
});
});
// Si un tri est actuellement actif, applique ce tri aux résultats de la recherche
if (currentSortColumn) {
searchResults.sort((a, b) => {
const valA = String(a[currentSortColumn] || ”).toLowerCase();
const valB = String(b[currentSortColumn] || ”).toLowerCase();
if (valA valB) return sortDirection === ‘asc’ ? 1 : -1;
return 0;
});
}
filteredData = searchResults; // Met à jour filteredData avec les résultats de recherche (et de tri)
renderTableRows(filteredData); // Re-rend le tableau
}
// — Écouteur d’événement pour le champ de recherche —
searchInput.addEventListener(‘input’, handleSearch);
// — Initialisation du tableau au chargement du DOM —
document.addEventListener(‘DOMContentLoaded’, () => {
initializeHeader(); // Crée les en-têtes et le titre/description
renderTableRows(travelData.data); // Affiche toutes les données initialement
updateSortIcons(); // S’assure que les icônes de tri sont initialisées correctement (aucun tri par défaut)
});
{“@context”:”https://schema.org”,”@type”:”FAQPage”,”mainEntity”:[{“@type”:”Question”,”name”:”Est-il ru00e9ellement possible de trouver un voyage tout compris u00e0 500 euros en 2026 ? “,”acceptedAnswer”:{“@type”:”Answer”,”text”:”Oui, absolument ! Gru00e2ce u00e0 la concurrence accrue et aux offres optimisu00e9es des tour-opu00e9rateurs, de nombreux forfaits tout compris sont disponibles u00e0 ce prix, surtout si les voyageurs font preuve de flexibilitu00e9 sur les dates et les destinations. Il est essentiel de chercher en dehors des pu00e9riodes de pointe et d’u00eatre ru00e9actif aux offres de derniu00e8re minute.”}},{“@type”:”Question”,”name”:”Quelles sont les destinations les plus propices pour un voyage u00e0 ce budget ? “,”acceptedAnswer”:{“@type”:”Answer”,”text”:”Les destinations europu00e9ennes comme la Bulgarie, l’Albanie, certaines ru00e9gions d’Espagne ou du Portugal en basse saison, ainsi que des pays d’Afrique du Nord comme le Maroc et la Tunisie, sont d’excellents choix. Les croisiu00e8res de courte duru00e9e ou en basse saison peuvent u00e9galement s’inscrire dans ce budget.”}},{“@type”:”Question”,”name”:”Que comprend gu00e9nu00e9ralement un forfait ‘tout compris’ u00e0 500 euros ? “,”acceptedAnswer”:{“@type”:”Answer”,”text”:”Un forfait ‘tout compris’ u00e0 ce prix inclut gu00e9nu00e9ralement le vol aller-retour depuis une ville majeure, l’hu00e9bergement pour une duru00e9e d’environ 6 u00e0 7 nuits dans un hu00f4tel ou un complexe, et la pension complu00e8te (repas et parfois boissons). Les activitu00e9s et divertissements peuvent varier selon les offres.”}},{“@type”:”Question”,”name”:”Quelles astuces pour du00e9nicher les meilleures offres ? “,”acceptedAnswer”:{“@type”:”Answer”,”text”:”La clu00e9 est la flexibilitu00e9 des dates et des au00e9roports de du00e9part. Il est recommandu00e9 de s’abonner aux newsletters des agences de voyages, de consulter les comparateurs en ligne, de surveiller les ventes flash et les offres de derniu00e8re minute. Une bonne lecture des conditions de l’offre est u00e9galement cruciale.”}}]}
Découvrez la limonade Bonbon Anglais, la boisson gazeuse emblématique de Madagascar qui séduit petits et grands par son goût unique et rafraîchissant. Fabriquée avec des ingrédients de qualité et des arômes naturels, Bonbon Anglais vous offre une expérience gustative inégalée. Parfaite pour toutes les occasions, cette limonade apportera une touche d’exotisme et de fraîcheur à vos moments de détente. Essayez-la dès aujourd’hui et laissez-vous emporter par le peps et l’authenticité de Bonbon Anglais, la star des boissons malgaches !