Paris/app.js

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);
});