543 lines
20 KiB
JavaScript
543 lines
20 KiB
JavaScript
// Hotel data
|
|
const hotelData = [
|
|
{
|
|
"name": "Hotel Britannique",
|
|
"address": "20 Av. Victoria, 75001 Paris",
|
|
"rating": 4.6,
|
|
"price_range": "€200-320",
|
|
"arrondissement": "1st",
|
|
"coordinates": [48.8567, 2.3471],
|
|
"description": "Classic 19th-century hotel near Châtelet with theatrical red decor and excellent Metro access",
|
|
"website": "https://hotel-britannique.fr",
|
|
"amenities": ["Free WiFi", "Minibar", "Air Conditioning", "Elevator"]
|
|
},
|
|
{
|
|
"name": "Hôtel Signature Saint Germain des Prés",
|
|
"address": "5 Rue Chomel, 75007 Paris",
|
|
"rating": 4.8,
|
|
"price_range": "€220-290",
|
|
"arrondissement": "7th",
|
|
"coordinates": [48.8485, 2.3244],
|
|
"description": "Chic boutique property in Saint-Germain with elegant, colorful rooms and vintage-chic lobby",
|
|
"website": "http://www.signature-saintgermain.com",
|
|
"amenities": ["Free WiFi", "Air Conditioning", "Buffet Breakfast", "24-hour Reception"]
|
|
},
|
|
{
|
|
"name": "Paris France Hotel",
|
|
"address": "72 R. de Turbigo, 75003 Paris",
|
|
"rating": 4.7,
|
|
"price_range": "€180-280",
|
|
"arrondissement": "3rd",
|
|
"coordinates": [48.8644, 2.3614],
|
|
"description": "Traditional hotel with a cafe/bar and lounge in the heart of the Marais district",
|
|
"website": "https://www.paris-france-hotel.com",
|
|
"amenities": ["Free WiFi", "Café/Bar", "Lounge", "Optional Breakfast"]
|
|
},
|
|
{
|
|
"name": "Hôtel Lenox Montparnasse",
|
|
"address": "15 Rue Delambre, 75014 Paris",
|
|
"rating": 4.5,
|
|
"price_range": "€170-260",
|
|
"arrondissement": "14th",
|
|
"coordinates": [48.8427, 2.3269],
|
|
"description": "Classic rooms & suites with warmly decorated honesty bar near Montparnasse",
|
|
"website": "http://www.paris-hotel-lenox.com",
|
|
"amenities": ["Free WiFi", "Honesty Bar", "Classic Decor", "Cozy Atmosphere"]
|
|
},
|
|
{
|
|
"name": "Hôtel Caron de Beaumarchais",
|
|
"address": "12 Rue Vieille du Temple, 75004 Paris",
|
|
"rating": 4.6,
|
|
"price_range": "€200-280",
|
|
"arrondissement": "4th",
|
|
"coordinates": [48.8572, 2.3598],
|
|
"description": "18th-century lodging with antiques-filled rooms and private balconies in Le Marais",
|
|
"website": "http://www.carondebeaumarchais.com",
|
|
"amenities": ["Free WiFi", "Private Balconies", "Antique Furnishings", "18th Century Charm"]
|
|
},
|
|
{
|
|
"name": "Hôtel Bourg Tibourg",
|
|
"address": "19 Rue du Bourg Tibourg, 75004 Paris",
|
|
"rating": 4.7,
|
|
"price_range": "€250-300",
|
|
"arrondissement": "4th",
|
|
"coordinates": [48.8547, 2.3581],
|
|
"description": "Lavish boutique hotel with opulent rooms and sophisticated dramatic atmosphere",
|
|
"website": "https://www.bourgtibourg.com",
|
|
"amenities": ["Free WiFi", "Opulent Decor", "Sophisticated Atmosphere", "Dramatic Design"]
|
|
},
|
|
{
|
|
"name": "Hôtel de l'Abbaye Saint-Germain",
|
|
"address": "10 Rue Cassette, 75006 Paris",
|
|
"rating": 4.6,
|
|
"price_range": "€280-300",
|
|
"arrondissement": "6th",
|
|
"coordinates": [48.8495, 2.3309],
|
|
"description": "Refined hotel with plush lounges and garden, plus free WiFi & breakfast",
|
|
"website": "https://www.hotelabbayeparis.com",
|
|
"amenities": ["Free WiFi", "Garden", "Plush Lounges", "Breakfast Included"]
|
|
},
|
|
{
|
|
"name": "Hotel Quartier Latin",
|
|
"address": "9 Rue des Écoles, 75005 Paris",
|
|
"rating": 4.3,
|
|
"price_range": "€160-250",
|
|
"arrondissement": "5th",
|
|
"coordinates": [48.8490, 2.3468],
|
|
"description": "Traditional hotel with simple rooms, minibars and breakfast buffet option",
|
|
"website": "https://hotelquartierlatin.com",
|
|
"amenities": ["Free WiFi", "Minibar", "Breakfast Buffet", "Traditional Decor"]
|
|
},
|
|
{
|
|
"name": "CitizenM Paris Gare de Lyon",
|
|
"address": "8 Rue Van Gogh, 75012 Paris",
|
|
"rating": 4.4,
|
|
"price_range": "€150-220",
|
|
"arrondissement": "12th",
|
|
"coordinates": [48.8448, 2.3732],
|
|
"description": "Trendy budget hotel with compact rooms, ambient lighting and 24-hour cafe/bar",
|
|
"website": "https://www.citizenm.com/hotels/europe/paris/gare-de-lyon-hotel",
|
|
"amenities": ["24-hour Café/Bar", "Ambient Lighting", "Compact Design", "Modern Amenities"]
|
|
},
|
|
{
|
|
"name": "Hôtel Migny Opéra-Montmartre",
|
|
"address": "13 Rue Victor Massé, 75009 Paris",
|
|
"rating": 4.1,
|
|
"price_range": "€140-200",
|
|
"arrondissement": "9th",
|
|
"coordinates": [48.8810, 2.3375],
|
|
"description": "Colorful rooms with satellite TV, casual breakfast area and lounge near Opéra",
|
|
"website": "https://www.hotelmigny.com",
|
|
"amenities": ["Free WiFi", "Satellite TV", "Breakfast Area", "LGBTQ+ Friendly"]
|
|
},
|
|
{
|
|
"name": "Hôtel Saint-André des Arts",
|
|
"address": "66 Rue Saint-André des Arts, 75006 Paris",
|
|
"rating": 4.8,
|
|
"price_range": "€180-280",
|
|
"arrondissement": "6th",
|
|
"coordinates": [48.8540, 2.3401],
|
|
"description": "Boutique hotel in Saint-Germain with unique rooms and bar",
|
|
"website": "https://www.saintandredesarts.com",
|
|
"amenities": ["Bar", "Unique Rooms", "Saint-Germain Location", "Boutique Style"]
|
|
},
|
|
{
|
|
"name": "Grand Hôtel des Gobelins",
|
|
"address": "57 Bd Saint-Marcel, 75013 Paris",
|
|
"rating": 4.1,
|
|
"price_range": "€130-180",
|
|
"arrondissement": "13th",
|
|
"coordinates": [48.8375, 2.3556],
|
|
"description": "Casual hotel with warmly furnished rooms and lobby bar",
|
|
"website": "http://www.grandhotelgobelins.com",
|
|
"amenities": ["Free WiFi", "Lobby Bar", "Warm Furnishings", "Relaxed Atmosphere"]
|
|
},
|
|
{
|
|
"name": "Hotel Marignan",
|
|
"address": "13 Rue du Sommerard, 75005 Paris",
|
|
"rating": 4.1,
|
|
"price_range": "€120-180",
|
|
"arrondissement": "5th",
|
|
"coordinates": [48.8504, 2.3445],
|
|
"description": "Laid-back hotel with simple rooms, guest kitchen and library, plus free breakfast",
|
|
"website": "http://www.hotel-marignan.com",
|
|
"amenities": ["Free Breakfast", "Free WiFi", "Guest Kitchen", "Library"]
|
|
},
|
|
{
|
|
"name": "Hôtel La Comtesse Tour Eiffel",
|
|
"address": "29 Av. de Tourville, 75007 Paris",
|
|
"rating": 4.6,
|
|
"price_range": "€250-300",
|
|
"arrondissement": "7th",
|
|
"coordinates": [48.8548, 2.3078],
|
|
"description": "Traditional hotel with classic rooms and Eiffel Tower views",
|
|
"website": "http://www.comtesse-hotel.com",
|
|
"amenities": ["Free WiFi", "Eiffel Tower Views", "Bar", "Traditional Decor"]
|
|
},
|
|
{
|
|
"name": "Boutique Hôtel Mareuil Paris",
|
|
"address": "51 Rue de Malte, 75011 Paris",
|
|
"rating": 4.7,
|
|
"price_range": "€200-280",
|
|
"arrondissement": "11th",
|
|
"coordinates": [48.8640, 2.3677],
|
|
"description": "Hip property with modern rooms & suites, plus bar, hammam & exercise room",
|
|
"website": "https://www.hotelmareuil.com/fr/",
|
|
"amenities": ["Bar", "Hammam", "Exercise Room", "Modern Design"]
|
|
},
|
|
{
|
|
"name": "Generator Paris",
|
|
"address": "9-11 Pl. du Colonel Fabien, 75010 Paris",
|
|
"rating": 4.1,
|
|
"price_range": "€90-150",
|
|
"arrondissement": "10th",
|
|
"coordinates": [48.8772, 2.3711],
|
|
"description": "Trendy hostel with stylish rooms & streamlined dorms, rooftop terrace & vibrant eatery",
|
|
"website": "https://staygenerator.com/hostels/paris?lang=fr-FR",
|
|
"amenities": ["Rooftop Terrace", "Restaurant", "Bar", "Budget-Friendly"]
|
|
},
|
|
{
|
|
"name": "Hôtel Henriette",
|
|
"address": "9 rue des Gobelins, 75013 Paris",
|
|
"rating": 4.5,
|
|
"price_range": "€130-180",
|
|
"arrondissement": "13th",
|
|
"coordinates": [48.8363, 2.3518],
|
|
"description": "Stylish boutique hotel with subtly decorated rooms and courtyard breakfast area",
|
|
"website": "https://www.hotelhenriette.com",
|
|
"amenities": ["Free WiFi", "Courtyard", "Breakfast", "Design Interiors"]
|
|
},
|
|
{
|
|
"name": "Hotel Paradis",
|
|
"address": "41 Rue Des Petities Ecuries, 75010 Paris",
|
|
"rating": 4.3,
|
|
"price_range": "€120-180",
|
|
"arrondissement": "10th",
|
|
"coordinates": [48.8718, 2.3502],
|
|
"description": "Tastefully decorated rooms in a top-notch location near trendy hotspots",
|
|
"website": "https://hotelparadisparis.com",
|
|
"amenities": ["Free WiFi", "Designer Decor", "Trendy Area", "Modern Amenities"]
|
|
},
|
|
{
|
|
"name": "COQ Hotel Paris",
|
|
"address": "15 Rue Edouard Manet, 75013 Paris",
|
|
"rating": 4.4,
|
|
"price_range": "€140-190",
|
|
"arrondissement": "13th",
|
|
"coordinates": [48.8261, 2.3542],
|
|
"description": "Stylish design hotel with cozy bar, extensive breakfast and warm atmosphere",
|
|
"website": "https://coq-hotel-paris.com",
|
|
"amenities": ["Bar", "Breakfast", "Design Interiors", "Cozy Atmosphere"]
|
|
},
|
|
{
|
|
"name": "Hotel Diana",
|
|
"address": "73 Rue Saint-Jacques, 75005 Paris",
|
|
"rating": 4.2,
|
|
"price_range": "€150-200",
|
|
"arrondissement": "5th",
|
|
"coordinates": [48.8485, 2.3456],
|
|
"description": "Simple but comfortable 2-star hotel with bright rooms and modern bathrooms",
|
|
"website": "http://www.hoteldiana.fr",
|
|
"amenities": ["Free WiFi", "Modern Bathrooms", "Central Location", "Budget-Friendly"]
|
|
}
|
|
];
|
|
|
|
// Global variables
|
|
let map;
|
|
let markers = [];
|
|
let filteredHotels = hotelData;
|
|
let selectedHotel = null;
|
|
|
|
// Initialize the application
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
initializeApp();
|
|
});
|
|
|
|
function initializeApp() {
|
|
initializeMap();
|
|
populateArrondissementFilter();
|
|
bindEventListeners();
|
|
applyFilters();
|
|
updateHotelCount();
|
|
}
|
|
|
|
// Initialize Leaflet map
|
|
function initializeMap() {
|
|
// Initialize map centered on Paris
|
|
map = L.map('map').setView([48.8566, 2.3522], 12);
|
|
|
|
// Add OpenStreetMap tiles
|
|
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
|
attribution: '© OpenStreetMap contributors',
|
|
maxZoom: 18
|
|
}).addTo(map);
|
|
|
|
// Add hotels to map
|
|
addHotelsToMap(hotelData);
|
|
}
|
|
|
|
// Add hotel markers to map
|
|
function addHotelsToMap(hotels) {
|
|
// Clear existing markers
|
|
markers.forEach(marker => map.removeLayer(marker));
|
|
markers = [];
|
|
|
|
hotels.forEach(hotel => {
|
|
const marker = L.marker([hotel.coordinates[0], hotel.coordinates[1]], {
|
|
icon: L.divIcon({
|
|
className: 'custom-marker',
|
|
html: `<div class="marker-pin" style="background-color: #21808D; width: 24px; height: 24px; border-radius: 50%; border: 3px solid white; box-shadow: 0 2px 4px rgba(0,0,0,0.3);"></div>`,
|
|
iconSize: [24, 24],
|
|
iconAnchor: [12, 12]
|
|
})
|
|
});
|
|
|
|
// Create popup content
|
|
const popupContent = `
|
|
<div class="popup-content">
|
|
<h3 class="popup-hotel-name">${hotel.name}</h3>
|
|
<div class="popup-rating">
|
|
<span class="stars">${generateStars(hotel.rating)}</span>
|
|
<span>${hotel.rating}</span>
|
|
</div>
|
|
<div class="popup-price">${hotel.price_range}</div>
|
|
<div class="popup-arrondissement">${hotel.arrondissement} Arrondissement</div>
|
|
</div>
|
|
`;
|
|
|
|
marker.bindPopup(popupContent);
|
|
marker.addTo(map);
|
|
markers.push(marker);
|
|
|
|
// Store hotel data with marker for easy access
|
|
marker.hotelData = hotel;
|
|
});
|
|
}
|
|
|
|
// Populate arrondissement dropdown
|
|
function populateArrondissementFilter() {
|
|
const select = document.getElementById('arrondissement');
|
|
const arrondissements = [...new Set(hotelData.map(hotel => hotel.arrondissement))].sort();
|
|
|
|
arrondissements.forEach(arr => {
|
|
const option = document.createElement('option');
|
|
option.value = arr;
|
|
option.textContent = `${arr} Arrondissement`;
|
|
select.appendChild(option);
|
|
});
|
|
}
|
|
|
|
// Bind event listeners
|
|
function bindEventListeners() {
|
|
// View toggle buttons
|
|
document.getElementById('map-view-btn').addEventListener('click', () => switchView('map'));
|
|
document.getElementById('list-view-btn').addEventListener('click', () => switchView('list'));
|
|
|
|
// Search input
|
|
document.getElementById('search').addEventListener('input', applyFilters);
|
|
|
|
// Price filter buttons
|
|
document.querySelectorAll('.price-filter').forEach(button => {
|
|
button.addEventListener('click', function() {
|
|
document.querySelectorAll('.price-filter').forEach(btn => btn.classList.remove('active'));
|
|
this.classList.add('active');
|
|
applyFilters();
|
|
});
|
|
});
|
|
|
|
// Arrondissement filter
|
|
document.getElementById('arrondissement').addEventListener('change', applyFilters);
|
|
|
|
// Rating slider
|
|
const ratingSlider = document.getElementById('rating');
|
|
const ratingValue = document.getElementById('rating-value');
|
|
ratingSlider.addEventListener('input', function() {
|
|
ratingValue.textContent = this.value;
|
|
applyFilters();
|
|
});
|
|
|
|
// Reset filters button
|
|
document.getElementById('reset-filters').addEventListener('click', resetFilters);
|
|
}
|
|
|
|
// Switch between map and list view
|
|
function switchView(view) {
|
|
const mapViewBtn = document.getElementById('map-view-btn');
|
|
const listViewBtn = document.getElementById('list-view-btn');
|
|
const mapView = document.getElementById('map-view');
|
|
const listView = document.getElementById('list-view');
|
|
|
|
if (view === 'map') {
|
|
mapViewBtn.classList.add('active');
|
|
mapViewBtn.classList.remove('btn--secondary');
|
|
mapViewBtn.classList.add('btn--primary');
|
|
listViewBtn.classList.remove('active');
|
|
listViewBtn.classList.remove('btn--primary');
|
|
listViewBtn.classList.add('btn--secondary');
|
|
mapView.classList.add('active');
|
|
listView.classList.remove('active');
|
|
|
|
// Refresh map after view switch
|
|
setTimeout(() => map.invalidateSize(), 100);
|
|
} else {
|
|
listViewBtn.classList.add('active');
|
|
listViewBtn.classList.remove('btn--secondary');
|
|
listViewBtn.classList.add('btn--primary');
|
|
mapViewBtn.classList.remove('active');
|
|
mapViewBtn.classList.remove('btn--primary');
|
|
mapViewBtn.classList.add('btn--secondary');
|
|
listView.classList.add('active');
|
|
mapView.classList.remove('active');
|
|
|
|
renderHotelList();
|
|
}
|
|
}
|
|
|
|
// Apply all filters
|
|
function applyFilters() {
|
|
const searchTerm = document.getElementById('search').value.toLowerCase();
|
|
const selectedArrondissement = document.getElementById('arrondissement').value;
|
|
const minRating = parseFloat(document.getElementById('rating').value);
|
|
const activePriceFilter = document.querySelector('.price-filter.active');
|
|
const minPrice = parseInt(activePriceFilter.dataset.min);
|
|
const maxPrice = parseInt(activePriceFilter.dataset.max);
|
|
|
|
filteredHotels = hotelData.filter(hotel => {
|
|
// Search filter
|
|
if (searchTerm && !hotel.name.toLowerCase().includes(searchTerm)) {
|
|
return false;
|
|
}
|
|
|
|
// Arrondissement filter
|
|
if (selectedArrondissement !== 'all' && hotel.arrondissement !== selectedArrondissement) {
|
|
return false;
|
|
}
|
|
|
|
// Rating filter
|
|
if (hotel.rating < minRating) {
|
|
return false;
|
|
}
|
|
|
|
// Price filter
|
|
const hotelMinPrice = parseInt(hotel.price_range.match(/€(\d+)/)[1]);
|
|
const hotelMaxPrice = parseInt(hotel.price_range.match(/€\d+-(\d+)/)[1]);
|
|
|
|
// Check if hotel price range overlaps with selected price range
|
|
if (hotelMaxPrice < minPrice || hotelMinPrice > maxPrice) {
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
});
|
|
|
|
// Update map markers
|
|
addHotelsToMap(filteredHotels);
|
|
|
|
// Update list view if active
|
|
if (document.getElementById('list-view').classList.contains('active')) {
|
|
renderHotelList();
|
|
}
|
|
|
|
updateHotelCount();
|
|
}
|
|
|
|
// Update hotel count display
|
|
function updateHotelCount() {
|
|
document.getElementById('hotel-count').textContent = filteredHotels.length;
|
|
}
|
|
|
|
// Reset all filters
|
|
function resetFilters() {
|
|
document.getElementById('search').value = '';
|
|
document.getElementById('arrondissement').value = 'all';
|
|
document.getElementById('rating').value = '4.0';
|
|
document.getElementById('rating-value').textContent = '4.0';
|
|
|
|
// Reset price filter to "Under €150" to focus on budget options
|
|
document.querySelectorAll('.price-filter').forEach(btn => btn.classList.remove('active'));
|
|
document.querySelector('.price-filter[data-max="150"]').classList.add('active');
|
|
|
|
applyFilters();
|
|
}
|
|
|
|
// Render hotel list
|
|
function renderHotelList() {
|
|
const listContainer = document.getElementById('hotels-list');
|
|
|
|
if (filteredHotels.length === 0) {
|
|
listContainer.innerHTML = `
|
|
<div class="empty-state">
|
|
<h3>No hotels found</h3>
|
|
<p>Try adjusting your filters to see more results.</p>
|
|
</div>
|
|
`;
|
|
return;
|
|
}
|
|
|
|
listContainer.innerHTML = filteredHotels.map(hotel => `
|
|
<div class="hotel-card" data-hotel-name="${hotel.name}" onclick="centerMapOnHotel('${hotel.name}')">
|
|
<div class="hotel-header">
|
|
<h3 class="hotel-name">${hotel.name}</h3>
|
|
<div class="hotel-price">${hotel.price_range}</div>
|
|
</div>
|
|
<div class="hotel-rating">
|
|
<span class="stars">${generateStars(hotel.rating)}</span>
|
|
<span class="rating-value">${hotel.rating}</span>
|
|
</div>
|
|
<div class="hotel-address">${hotel.address}</div>
|
|
<div class="hotel-description">${hotel.description}</div>
|
|
<div class="hotel-amenities">
|
|
${hotel.amenities.map(amenity => `<span class="amenity-tag">${amenity}</span>`).join('')}
|
|
</div>
|
|
<div class="hotel-actions">
|
|
<div class="arrondissement-badge">${hotel.arrondissement} Arrondissement</div>
|
|
<a href="${hotel.website}" target="_blank" class="visit-website">
|
|
Visit Website <i class="fas fa-external-link-alt"></i>
|
|
</a>
|
|
</div>
|
|
</div>
|
|
`).join('');
|
|
}
|
|
|
|
// Center map on selected hotel
|
|
function centerMapOnHotel(hotelName) {
|
|
const hotel = hotelData.find(h => h.name === hotelName);
|
|
if (hotel) {
|
|
// Switch to map view
|
|
switchView('map');
|
|
|
|
// Center map on hotel
|
|
map.setView([hotel.coordinates[0], hotel.coordinates[1]], 15);
|
|
|
|
// Find and open the marker popup
|
|
const marker = markers.find(m => m.hotelData && m.hotelData.name === hotelName);
|
|
if (marker) {
|
|
marker.openPopup();
|
|
|
|
// Highlight the marker temporarily
|
|
const markerElement = marker.getElement();
|
|
if (markerElement) {
|
|
const pin = markerElement.querySelector('.marker-pin');
|
|
if (pin) {
|
|
pin.style.backgroundColor = '#A84B2F'; // Warning color
|
|
setTimeout(() => {
|
|
pin.style.backgroundColor = '#21808D'; // Back to primary
|
|
}, 2000);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Generate star rating HTML
|
|
function generateStars(rating) {
|
|
const fullStars = Math.floor(rating);
|
|
const hasHalfStar = rating % 1 >= 0.5;
|
|
let stars = '';
|
|
|
|
for (let i = 0; i < fullStars; i++) {
|
|
stars += '<i class="fas fa-star"></i>';
|
|
}
|
|
|
|
if (hasHalfStar) {
|
|
stars += '<i class="fas fa-star-half-alt"></i>';
|
|
}
|
|
|
|
const emptyStars = 5 - fullStars - (hasHalfStar ? 1 : 0);
|
|
for (let i = 0; i < emptyStars; i++) {
|
|
stars += '<i class="far fa-star"></i>';
|
|
}
|
|
|
|
return stars;
|
|
}
|
|
|
|
// Initialize with focus on budget hotels (under €150)
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
// Small delay to ensure everything is loaded
|
|
setTimeout(() => {
|
|
// Set default filter to show budget hotels under €150
|
|
document.querySelectorAll('.price-filter').forEach(btn => btn.classList.remove('active'));
|
|
document.querySelector('.price-filter[data-max="150"]').classList.add('active');
|
|
applyFilters();
|
|
}, 500);
|
|
}); |