diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..5c97876 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,5 @@ +Dockerfile +.dockerignore +.git +.gitignore +README.md diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..73b52cc --- /dev/null +++ b/Dockerfile @@ -0,0 +1,18 @@ +# Use an official Nginx image as the base image +FROM nginx:alpine + +# Set the working directory in the container +WORKDIR /usr/share/nginx/html + +# Remove default Nginx welcome page +RUN rm -rf ./* + +# Copy all the static assets from your project to the Nginx public directory +# This includes index.html, app.js, style.css, and any images or data files +COPY . . + +# Expose port 80 (the default Nginx port) +EXPOSE 80 + +# Command to run Nginx when the container starts +CMD ["nginx", "-g", "daemon off;"] diff --git a/app.js b/app.js new file mode 100644 index 0000000..ca7d019 --- /dev/null +++ b/app.js @@ -0,0 +1,543 @@ +// 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: `
`, + iconSize: [24, 24], + iconAnchor: [12, 12] + }) + }); + + // Create popup content + const popupContent = ` + + `; + + 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 = ` +
+

No hotels found

+

Try adjusting your filters to see more results.

+
+ `; + return; + } + + listContainer.innerHTML = filteredHotels.map(hotel => ` +
+
+

${hotel.name}

+
${hotel.price_range}
+
+
+ ${generateStars(hotel.rating)} + ${hotel.rating} +
+
${hotel.address}
+
${hotel.description}
+
+ ${hotel.amenities.map(amenity => `${amenity}`).join('')} +
+
+
${hotel.arrondissement} Arrondissement
+ + Visit Website + +
+
+ `).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 += ''; + } + + if (hasHalfStar) { + stars += ''; + } + + const emptyStars = 5 - fullStars - (hasHalfStar ? 1 : 0); + for (let i = 0; i < emptyStars; i++) { + stars += ''; + } + + 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); +}); \ No newline at end of file diff --git a/index.html b/index.html new file mode 100644 index 0000000..611c8a2 --- /dev/null +++ b/index.html @@ -0,0 +1,126 @@ + + + + + + Paris Hotels Under 300€ Per Night + + + + + + + +
+
+

Paris Hotels Under 300€ Per Night

+
+ + +
+
+
+ +
+
+ + + + +
+ +
+
+
+

Map Legend

+
+
+ Hotel +
+
+
+ Selected Hotel +
+
+
+ + +
+
+ +
+
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/paris_hotels_chart.png b/paris_hotels_chart.png new file mode 100644 index 0000000..0bde0d8 Binary files /dev/null and b/paris_hotels_chart.png differ diff --git a/paris_hotels_map.html b/paris_hotels_map.html new file mode 100644 index 0000000..f7bb63d --- /dev/null +++ b/paris_hotels_map.html @@ -0,0 +1,150 @@ + + + + + Paris Hotels Under 300€ + + + + + + +

Paris Hotels Under 300€ Per Night

+
+ + + +

Top 5 Hotels Under 300€

+
+ +
+
Hôtel Signature Saint Germain des Prés
+
Rating: 4.8/5.0
+
Price Range: €220-290
+
Address: 5 Rue Chomel, 75007 Paris (Arr. 7th)
+
Chic boutique property in Saint-Germain with elegant, colorful rooms and vintage-chic lobby
+
Visit Website
+
+ +
+
Hôtel Saint-André des Arts
+
Rating: 4.8/5.0
+
Price Range: €180-280
+
Address: 66 Rue Saint-André des Arts, 75006 Paris (Arr. 6th)
+
Boutique hotel in Saint-Germain with unique rooms and bar
+
Visit Website
+
+ +
+
Paris France Hotel
+
Rating: 4.7/5.0
+
Price Range: €180-280
+
Address: 72 R. de Turbigo, 75003 Paris (Arr. 3rd)
+
Traditional hotel with a cafe/bar and lounge in the heart of the Marais district
+
Visit Website
+
+ +
+
Hôtel Bourg Tibourg
+
Rating: 4.7/5.0
+
Price Range: €250-300
+
Address: 19 Rue du Bourg Tibourg, 75004 Paris (Arr. 4th)
+
Lavish boutique hotel with opulent rooms and sophisticated dramatic atmosphere
+
Visit Website
+
+ +
+
Boutique Hôtel Mareuil Paris
+
Rating: 4.7/5.0
+
Price Range: €200-280
+
Address: 51 Rue de Malte, 75011 Paris (Arr. 11th)
+
Hip property with modern rooms & suites, plus bar, hammam & exercise room
+
Visit Website
+
+ +
+ +

Budget-Friendly Options (Under 200€)

+
+ +
+
Generator Paris
+
Rating: 4.1/5.0
+
Price Range: €90-150
+
Address: 9-11 Pl. du Colonel Fabien, 75010 Paris (Arr. 10th)
+
Trendy hostel with stylish rooms & streamlined dorms, rooftop terrace & vibrant eatery
+
Visit Website
+
+ +
+
Hotel Marignan
+
Rating: 4.1/5.0
+
Price Range: €120-180
+
Address: 13 Rue du Sommerard, 75005 Paris (Arr. 5th)
+
Laid-back hotel with simple rooms, guest kitchen and library, plus free breakfast
+
Visit Website
+
+ +
+
Hotel Paradis
+
Rating: 4.3/5.0
+
Price Range: €120-180
+
Address: 41 Rue Des Petities Ecuries, 75010 Paris (Arr. 10th)
+
Tastefully decorated rooms in a top-notch location near trendy hotspots
+
Visit Website
+
+ +
+
Grand Hôtel des Gobelins
+
Rating: 4.1/5.0
+
Price Range: €130-180
+
Address: 57 Bd Saint-Marcel, 75013 Paris (Arr. 13th)
+
Casual hotel with warmly furnished rooms and lobby bar
+
Visit Website
+
+ +
+
Hôtel Henriette
+
Rating: 4.5/5.0
+
Price Range: €130-180
+
Address: 9 rue des Gobelins, 75013 Paris (Arr. 13th)
+
Stylish boutique hotel with subtly decorated rooms and courtyard breakfast area
+
Visit Website
+
+ +
+ + diff --git a/paris_hotels_map_data.json b/paris_hotels_map_data.json new file mode 100644 index 0000000..715023f --- /dev/null +++ b/paris_hotels_map_data.json @@ -0,0 +1,209 @@ +{ + "center": [ + 48.8566, + 2.3522 + ], + "zoom": 12, + "markers": [ + { + "name": "Hotel Britannique", + "coords": [ + 48.8567, + 2.3471 + ], + "price": "€200-320", + "rating": 4.6, + "arrondissement": "1st" + }, + { + "name": "Hôtel Signature Saint Germain des Prés", + "coords": [ + 48.8485, + 2.3244 + ], + "price": "€220-290", + "rating": 4.8, + "arrondissement": "7th" + }, + { + "name": "Paris France Hotel", + "coords": [ + 48.8644, + 2.3614 + ], + "price": "€180-280", + "rating": 4.7, + "arrondissement": "3rd" + }, + { + "name": "Hôtel Lenox Montparnasse", + "coords": [ + 48.8427, + 2.3269 + ], + "price": "€170-260", + "rating": 4.5, + "arrondissement": "14th" + }, + { + "name": "Hôtel Caron de Beaumarchais", + "coords": [ + 48.8572, + 2.3598 + ], + "price": "€200-280", + "rating": 4.6, + "arrondissement": "4th" + }, + { + "name": "Hôtel Bourg Tibourg", + "coords": [ + 48.8547, + 2.3581 + ], + "price": "€250-300", + "rating": 4.7, + "arrondissement": "4th" + }, + { + "name": "Hôtel de l'Abbaye Saint-Germain", + "coords": [ + 48.8495, + 2.3309 + ], + "price": "€280-300", + "rating": 4.6, + "arrondissement": "6th" + }, + { + "name": "Hotel Quartier Latin", + "coords": [ + 48.849, + 2.3468 + ], + "price": "€160-250", + "rating": 4.3, + "arrondissement": "5th" + }, + { + "name": "CitizenM Paris Gare de Lyon", + "coords": [ + 48.8448, + 2.3732 + ], + "price": "€150-220", + "rating": 4.4, + "arrondissement": "12th" + }, + { + "name": "Hôtel Migny Opéra-Montmartre", + "coords": [ + 48.881, + 2.3375 + ], + "price": "€140-200", + "rating": 4.1, + "arrondissement": "9th" + }, + { + "name": "Hôtel Saint-André des Arts", + "coords": [ + 48.854, + 2.3401 + ], + "price": "€180-280", + "rating": 4.8, + "arrondissement": "6th" + }, + { + "name": "Grand Hôtel des Gobelins", + "coords": [ + 48.8375, + 2.3556 + ], + "price": "€130-180", + "rating": 4.1, + "arrondissement": "13th" + }, + { + "name": "Hotel Marignan", + "coords": [ + 48.8504, + 2.3445 + ], + "price": "€120-180", + "rating": 4.1, + "arrondissement": "5th" + }, + { + "name": "Hôtel La Comtesse Tour Eiffel", + "coords": [ + 48.8548, + 2.3078 + ], + "price": "€250-300", + "rating": 4.6, + "arrondissement": "7th" + }, + { + "name": "Boutique Hôtel Mareuil Paris", + "coords": [ + 48.864, + 2.3677 + ], + "price": "€200-280", + "rating": 4.7, + "arrondissement": "11th" + }, + { + "name": "Generator Paris", + "coords": [ + 48.8772, + 2.3711 + ], + "price": "€90-150", + "rating": 4.1, + "arrondissement": "10th" + }, + { + "name": "Hôtel Henriette", + "coords": [ + 48.8363, + 2.3518 + ], + "price": "€130-180", + "rating": 4.5, + "arrondissement": "13th" + }, + { + "name": "Hotel Paradis", + "coords": [ + 48.8718, + 2.3502 + ], + "price": "€120-180", + "rating": 4.3, + "arrondissement": "10th" + }, + { + "name": "COQ Hotel Paris", + "coords": [ + 48.8261, + 2.3542 + ], + "price": "€140-190", + "rating": 4.4, + "arrondissement": "13th" + }, + { + "name": "Hotel Diana", + "coords": [ + 48.8485, + 2.3456 + ], + "price": "€150-200", + "rating": 4.2, + "arrondissement": "5th" + } + ] +} \ No newline at end of file diff --git a/paris_hotels_price_range.png b/paris_hotels_price_range.png new file mode 100644 index 0000000..0879cd2 Binary files /dev/null and b/paris_hotels_price_range.png differ diff --git a/paris_hotels_under_300.csv b/paris_hotels_under_300.csv new file mode 100644 index 0000000..899d71c --- /dev/null +++ b/paris_hotels_under_300.csv @@ -0,0 +1,21 @@ +name,address,rating,price_range,arrondissement,latitude,longitude,description,website,amenities +Hotel Britannique,"20 Av. Victoria, 75001 Paris",4.6,€200-320,1st,48.8567,2.3471,Classic 19th-century hotel near Châtelet with theatrical red decor and excellent Metro access,https://hotel-britannique.fr,"Free WiFi, Minibar, Air Conditioning, Elevator" +Hôtel Signature Saint Germain des Prés,"5 Rue Chomel, 75007 Paris",4.8,€220-290,7th,48.8485,2.3244,"Chic boutique property in Saint-Germain with elegant, colorful rooms and vintage-chic lobby",http://www.signature-saintgermain.com,"Free WiFi, Air Conditioning, Buffet Breakfast, 24-hour Reception" +Paris France Hotel,"72 R. de Turbigo, 75003 Paris",4.7,€180-280,3rd,48.8644,2.3614,Traditional hotel with a cafe/bar and lounge in the heart of the Marais district,https://www.paris-france-hotel.com,"Free WiFi, Café/Bar, Lounge, Optional Breakfast" +Hôtel Lenox Montparnasse,"15 Rue Delambre, 75014 Paris",4.5,€170-260,14th,48.8427,2.3269,Classic rooms & suites with warmly decorated honesty bar near Montparnasse,http://www.paris-hotel-lenox.com,"Free WiFi, Honesty Bar, Classic Decor, Cozy Atmosphere" +Hôtel Caron de Beaumarchais,"12 Rue Vieille du Temple, 75004 Paris",4.6,€200-280,4th,48.8572,2.3598,18th-century lodging with antiques-filled rooms and private balconies in Le Marais,http://www.carondebeaumarchais.com,"Free WiFi, Private Balconies, Antique Furnishings, 18th Century Charm" +Hôtel Bourg Tibourg,"19 Rue du Bourg Tibourg, 75004 Paris",4.7,€250-300,4th,48.8547,2.3581,Lavish boutique hotel with opulent rooms and sophisticated dramatic atmosphere,https://www.bourgtibourg.com,"Free WiFi, Opulent Decor, Sophisticated Atmosphere, Dramatic Design" +Hôtel de l'Abbaye Saint-Germain,"10 Rue Cassette, 75006 Paris",4.6,€280-300,6th,48.8495,2.3309,"Refined hotel with plush lounges and garden, plus free WiFi & breakfast",https://www.hotelabbayeparis.com,"Free WiFi, Garden, Plush Lounges, Breakfast Included" +Hotel Quartier Latin,"9 Rue des Écoles, 75005 Paris",4.3,€160-250,5th,48.849,2.3468,"Traditional hotel with simple rooms, minibars and breakfast buffet option",https://hotelquartierlatin.com,"Free WiFi, Minibar, Breakfast Buffet, Traditional Decor" +CitizenM Paris Gare de Lyon,"8 Rue Van Gogh, 75012 Paris",4.4,€150-220,12th,48.8448,2.3732,"Trendy budget hotel with compact rooms, ambient lighting and 24-hour cafe/bar",https://www.citizenm.com/hotels/europe/paris/gare-de-lyon-hotel,"24-hour Café/Bar, Ambient Lighting, Compact Design, Modern Amenities" +Hôtel Migny Opéra-Montmartre,"13 Rue Victor Massé, 75009 Paris",4.1,€140-200,9th,48.881,2.3375,"Colorful rooms with satellite TV, casual breakfast area and lounge near Opéra",https://www.hotelmigny.com,"Free WiFi, Satellite TV, Breakfast Area, LGBTQ+ Friendly" +Hôtel Saint-André des Arts,"66 Rue Saint-André des Arts, 75006 Paris",4.8,€180-280,6th,48.854,2.3401,Boutique hotel in Saint-Germain with unique rooms and bar,https://www.saintandredesarts.com,"Bar, Unique Rooms, Saint-Germain Location, Boutique Style" +Grand Hôtel des Gobelins,"57 Bd Saint-Marcel, 75013 Paris",4.1,€130-180,13th,48.8375,2.3556,Casual hotel with warmly furnished rooms and lobby bar,http://www.grandhotelgobelins.com,"Free WiFi, Lobby Bar, Warm Furnishings, Relaxed Atmosphere" +Hotel Marignan,"13 Rue du Sommerard, 75005 Paris",4.1,€120-180,5th,48.8504,2.3445,"Laid-back hotel with simple rooms, guest kitchen and library, plus free breakfast",http://www.hotel-marignan.com,"Free Breakfast, Free WiFi, Guest Kitchen, Library" +Hôtel La Comtesse Tour Eiffel,"29 Av. de Tourville, 75007 Paris",4.6,€250-300,7th,48.8548,2.3078,Traditional hotel with classic rooms and Eiffel Tower views,http://www.comtesse-hotel.com,"Free WiFi, Eiffel Tower Views, Bar, Traditional Decor" +Boutique Hôtel Mareuil Paris,"51 Rue de Malte, 75011 Paris",4.7,€200-280,11th,48.864,2.3677,"Hip property with modern rooms & suites, plus bar, hammam & exercise room",https://www.hotelmareuil.com/fr/,"Bar, Hammam, Exercise Room, Modern Design" +Generator Paris,"9-11 Pl. du Colonel Fabien, 75010 Paris",4.1,€90-150,10th,48.8772,2.3711,"Trendy hostel with stylish rooms & streamlined dorms, rooftop terrace & vibrant eatery",https://staygenerator.com/hostels/paris?lang=fr-FR,"Rooftop Terrace, Restaurant, Bar, Budget-Friendly" +Hôtel Henriette,"9 rue des Gobelins, 75013 Paris",4.5,€130-180,13th,48.8363,2.3518,Stylish boutique hotel with subtly decorated rooms and courtyard breakfast area,https://www.hotelhenriette.com,"Free WiFi, Courtyard, Breakfast, Design Interiors" +Hotel Paradis,"41 Rue Des Petities Ecuries, 75010 Paris",4.3,€120-180,10th,48.8718,2.3502,Tastefully decorated rooms in a top-notch location near trendy hotspots,https://hotelparadisparis.com,"Free WiFi, Designer Decor, Trendy Area, Modern Amenities" +COQ Hotel Paris,"15 Rue Edouard Manet, 75013 Paris",4.4,€140-190,13th,48.8261,2.3542,"Stylish design hotel with cozy bar, extensive breakfast and warm atmosphere",https://coq-hotel-paris.com,"Bar, Breakfast, Design Interiors, Cozy Atmosphere" +Hotel Diana,"73 Rue Saint-Jacques, 75005 Paris",4.2,€150-200,5th,48.8485,2.3456,Simple but comfortable 2-star hotel with bright rooms and modern bathrooms,http://www.hoteldiana.fr,"Free WiFi, Modern Bathrooms, Central Location, Budget-Friendly" diff --git a/style.css b/style.css new file mode 100644 index 0000000..9f583de --- /dev/null +++ b/style.css @@ -0,0 +1,1165 @@ + +:root { + /* Colors */ + --color-background: rgba(252, 252, 249, 1); + --color-surface: rgba(255, 255, 253, 1); + --color-text: rgba(19, 52, 59, 1); + --color-text-secondary: rgba(98, 108, 113, 1); + --color-primary: rgba(33, 128, 141, 1); + --color-primary-hover: rgba(29, 116, 128, 1); + --color-primary-active: rgba(26, 104, 115, 1); + --color-secondary: rgba(94, 82, 64, 0.12); + --color-secondary-hover: rgba(94, 82, 64, 0.2); + --color-secondary-active: rgba(94, 82, 64, 0.25); + --color-border: rgba(94, 82, 64, 0.2); + --color-btn-primary-text: rgba(252, 252, 249, 1); + --color-card-border: rgba(94, 82, 64, 0.12); + --color-card-border-inner: rgba(94, 82, 64, 0.12); + --color-error: rgba(192, 21, 47, 1); + --color-success: rgba(33, 128, 141, 1); + --color-warning: rgba(168, 75, 47, 1); + --color-info: rgba(98, 108, 113, 1); + --color-focus-ring: rgba(33, 128, 141, 0.4); + --color-select-caret: rgba(19, 52, 59, 0.8); + + /* Common style patterns */ + --focus-ring: 0 0 0 3px var(--color-focus-ring); + --focus-outline: 2px solid var(--color-primary); + --status-bg-opacity: 0.15; + --status-border-opacity: 0.25; + --select-caret-light: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 24 24' fill='none' stroke='%23134252' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'%3E%3C/polyline%3E%3C/svg%3E"); + --select-caret-dark: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 24 24' fill='none' stroke='%23f5f5f5' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'%3E%3C/polyline%3E%3C/svg%3E"); + + /* RGB versions for opacity control */ + --color-success-rgb: 33, 128, 141; + --color-error-rgb: 192, 21, 47; + --color-warning-rgb: 168, 75, 47; + --color-info-rgb: 98, 108, 113; + + /* Typography */ + --font-family-base: "FKGroteskNeue", "Geist", "Inter", -apple-system, + BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; + --font-family-mono: "Berkeley Mono", ui-monospace, SFMono-Regular, Menlo, + Monaco, Consolas, monospace; + --font-size-xs: 11px; + --font-size-sm: 12px; + --font-size-base: 14px; + --font-size-md: 14px; + --font-size-lg: 16px; + --font-size-xl: 18px; + --font-size-2xl: 20px; + --font-size-3xl: 24px; + --font-size-4xl: 30px; + --font-weight-normal: 400; + --font-weight-medium: 500; + --font-weight-semibold: 550; + --font-weight-bold: 600; + --line-height-tight: 1.2; + --line-height-normal: 1.5; + --letter-spacing-tight: -0.01em; + + /* Spacing */ + --space-0: 0; + --space-1: 1px; + --space-2: 2px; + --space-4: 4px; + --space-6: 6px; + --space-8: 8px; + --space-10: 10px; + --space-12: 12px; + --space-16: 16px; + --space-20: 20px; + --space-24: 24px; + --space-32: 32px; + + /* Border Radius */ + --radius-sm: 6px; + --radius-base: 8px; + --radius-md: 10px; + --radius-lg: 12px; + --radius-full: 9999px; + + /* Shadows */ + --shadow-xs: 0 1px 2px rgba(0, 0, 0, 0.02); + --shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.04), 0 1px 2px rgba(0, 0, 0, 0.02); + --shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.04), + 0 2px 4px -1px rgba(0, 0, 0, 0.02); + --shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.04), + 0 4px 6px -2px rgba(0, 0, 0, 0.02); + --shadow-inset-sm: inset 0 1px 0 rgba(255, 255, 255, 0.15), + inset 0 -1px 0 rgba(0, 0, 0, 0.03); + + /* Animation */ + --duration-fast: 150ms; + --duration-normal: 250ms; + --ease-standard: cubic-bezier(0.16, 1, 0.3, 1); + + /* Layout */ + --container-sm: 640px; + --container-md: 768px; + --container-lg: 1024px; + --container-xl: 1280px; +} + +/* Dark mode colors */ +@media (prefers-color-scheme: dark) { + :root { + --color-background: rgba(31, 33, 33, 1); + --color-surface: rgba(38, 40, 40, 1); + --color-text: rgba(245, 245, 245, 1); + --color-text-secondary: rgba(167, 169, 169, 0.7); + --color-primary: rgba(50, 184, 198, 1); + --color-primary-hover: rgba(45, 166, 178, 1); + --color-primary-active: rgba(41, 150, 161, 1); + --color-secondary: rgba(119, 124, 124, 0.15); + --color-secondary-hover: rgba(119, 124, 124, 0.25); + --color-secondary-active: rgba(119, 124, 124, 0.3); + --color-border: rgba(119, 124, 124, 0.3); + --color-error: rgba(255, 84, 89, 1); + --color-success: rgba(50, 184, 198, 1); + --color-warning: rgba(230, 129, 97, 1); + --color-info: rgba(167, 169, 169, 1); + --color-focus-ring: rgba(50, 184, 198, 0.4); + --color-btn-primary-text: rgba(19, 52, 59, 1); + --color-card-border: rgba(119, 124, 124, 0.2); + --color-card-border-inner: rgba(119, 124, 124, 0.15); + --shadow-inset-sm: inset 0 1px 0 rgba(255, 255, 255, 0.1), + inset 0 -1px 0 rgba(0, 0, 0, 0.15); + --button-border-secondary: rgba(119, 124, 124, 0.2); + --color-border-secondary: rgba(119, 124, 124, 0.2); + --color-select-caret: rgba(245, 245, 245, 0.8); + + /* Common style patterns - updated for dark mode */ + --focus-ring: 0 0 0 3px var(--color-focus-ring); + --focus-outline: 2px solid var(--color-primary); + --status-bg-opacity: 0.15; + --status-border-opacity: 0.25; + --select-caret-light: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 24 24' fill='none' stroke='%23134252' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'%3E%3C/polyline%3E%3C/svg%3E"); + --select-caret-dark: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 24 24' fill='none' stroke='%23f5f5f5' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'%3E%3C/polyline%3E%3C/svg%3E"); + + /* RGB versions for dark mode */ + --color-success-rgb: 50, 184, 198; + --color-error-rgb: 255, 84, 89; + --color-warning-rgb: 230, 129, 97; + --color-info-rgb: 167, 169, 169; + } +} + +/* Data attribute for manual theme switching */ +[data-color-scheme="dark"] { + --color-background: rgba(31, 33, 33, 1); + --color-surface: rgba(38, 40, 40, 1); + --color-text: rgba(245, 245, 245, 1); + --color-text-secondary: rgba(167, 169, 169, 0.7); + --color-primary: rgba(50, 184, 198, 1); + --color-primary-hover: rgba(45, 166, 178, 1); + --color-primary-active: rgba(41, 150, 161, 1); + --color-secondary: rgba(119, 124, 124, 0.15); + --color-secondary-hover: rgba(119, 124, 124, 0.25); + --color-secondary-active: rgba(119, 124, 124, 0.3); + --color-border: rgba(119, 124, 124, 0.3); + --color-error: rgba(255, 84, 89, 1); + --color-success: rgba(50, 184, 198, 1); + --color-warning: rgba(230, 129, 97, 1); + --color-info: rgba(167, 169, 169, 1); + --color-focus-ring: rgba(50, 184, 198, 0.4); + --color-btn-primary-text: rgba(19, 52, 59, 1); + --color-card-border: rgba(119, 124, 124, 0.15); + --color-card-border-inner: rgba(119, 124, 124, 0.15); + --shadow-inset-sm: inset 0 1px 0 rgba(255, 255, 255, 0.1), + inset 0 -1px 0 rgba(0, 0, 0, 0.15); + --color-border-secondary: rgba(119, 124, 124, 0.2); + --color-select-caret: rgba(245, 245, 245, 0.8); + + /* Common style patterns - updated for dark mode */ + --focus-ring: 0 0 0 3px var(--color-focus-ring); + --focus-outline: 2px solid var(--color-primary); + --status-bg-opacity: 0.15; + --status-border-opacity: 0.25; + --select-caret-light: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 24 24' fill='none' stroke='%23134252' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'%3E%3C/polyline%3E%3C/svg%3E"); + --select-caret-dark: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 24 24' fill='none' stroke='%23f5f5f5' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'%3E%3C/polyline%3E%3C/svg%3E"); + + /* RGB versions for dark mode */ + --color-success-rgb: 50, 184, 198; + --color-error-rgb: 255, 84, 89; + --color-warning-rgb: 230, 129, 97; + --color-info-rgb: 167, 169, 169; +} + +[data-color-scheme="light"] { + --color-background: rgba(252, 252, 249, 1); + --color-surface: rgba(255, 255, 253, 1); + --color-text: rgba(19, 52, 59, 1); + --color-text-secondary: rgba(98, 108, 113, 1); + --color-primary: rgba(33, 128, 141, 1); + --color-primary-hover: rgba(29, 116, 128, 1); + --color-primary-active: rgba(26, 104, 115, 1); + --color-secondary: rgba(94, 82, 64, 0.12); + --color-secondary-hover: rgba(94, 82, 64, 0.2); + --color-secondary-active: rgba(94, 82, 64, 0.25); + --color-border: rgba(94, 82, 64, 0.2); + --color-btn-primary-text: rgba(252, 252, 249, 1); + --color-card-border: rgba(94, 82, 64, 0.12); + --color-card-border-inner: rgba(94, 82, 64, 0.12); + --color-error: rgba(192, 21, 47, 1); + --color-success: rgba(33, 128, 141, 1); + --color-warning: rgba(168, 75, 47, 1); + --color-info: rgba(98, 108, 113, 1); + --color-focus-ring: rgba(33, 128, 141, 0.4); + + /* RGB versions for light mode */ + --color-success-rgb: 33, 128, 141; + --color-error-rgb: 192, 21, 47; + --color-warning-rgb: 168, 75, 47; + --color-info-rgb: 98, 108, 113; +} + +/* Base styles */ +html { + font-size: var(--font-size-base); + font-family: var(--font-family-base); + line-height: var(--line-height-normal); + color: var(--color-text); + background-color: var(--color-background); + -webkit-font-smoothing: antialiased; + box-sizing: border-box; +} + +body { + margin: 0; + padding: 0; +} + +*, +*::before, +*::after { + box-sizing: inherit; +} + +/* Typography */ +h1, +h2, +h3, +h4, +h5, +h6 { + margin: 0; + font-weight: var(--font-weight-semibold); + line-height: var(--line-height-tight); + color: var(--color-text); + letter-spacing: var(--letter-spacing-tight); +} + +h1 { + font-size: var(--font-size-4xl); +} +h2 { + font-size: var(--font-size-3xl); +} +h3 { + font-size: var(--font-size-2xl); +} +h4 { + font-size: var(--font-size-xl); +} +h5 { + font-size: var(--font-size-lg); +} +h6 { + font-size: var(--font-size-md); +} + +p { + margin: 0 0 var(--space-16) 0; +} + +a { + color: var(--color-primary); + text-decoration: none; + transition: color var(--duration-fast) var(--ease-standard); +} + +a:hover { + color: var(--color-primary-hover); +} + +code, +pre { + font-family: var(--font-family-mono); + font-size: calc(var(--font-size-base) * 0.95); + background-color: var(--color-secondary); + border-radius: var(--radius-sm); +} + +code { + padding: var(--space-1) var(--space-4); +} + +pre { + padding: var(--space-16); + margin: var(--space-16) 0; + overflow: auto; + border: 1px solid var(--color-border); +} + +pre code { + background: none; + padding: 0; +} + +/* Buttons */ +.btn { + display: inline-flex; + align-items: center; + justify-content: center; + padding: var(--space-8) var(--space-16); + border-radius: var(--radius-base); + font-size: var(--font-size-base); + font-weight: 500; + line-height: 1.5; + cursor: pointer; + transition: all var(--duration-normal) var(--ease-standard); + border: none; + text-decoration: none; + position: relative; +} + +.btn:focus-visible { + outline: none; + box-shadow: var(--focus-ring); +} + +.btn--primary { + background: var(--color-primary); + color: var(--color-btn-primary-text); +} + +.btn--primary:hover { + background: var(--color-primary-hover); +} + +.btn--primary:active { + background: var(--color-primary-active); +} + +.btn--secondary { + background: var(--color-secondary); + color: var(--color-text); +} + +.btn--secondary:hover { + background: var(--color-secondary-hover); +} + +.btn--secondary:active { + background: var(--color-secondary-active); +} + +.btn--outline { + background: transparent; + border: 1px solid var(--color-border); + color: var(--color-text); +} + +.btn--outline:hover { + background: var(--color-secondary); +} + +.btn--sm { + padding: var(--space-4) var(--space-12); + font-size: var(--font-size-sm); + border-radius: var(--radius-sm); +} + +.btn--lg { + padding: var(--space-10) var(--space-20); + font-size: var(--font-size-lg); + border-radius: var(--radius-md); +} + +.btn--full-width { + width: 100%; +} + +.btn:disabled { + opacity: 0.5; + cursor: not-allowed; +} + +/* Form elements */ +.form-control { + display: block; + width: 100%; + padding: var(--space-8) var(--space-12); + font-size: var(--font-size-md); + line-height: 1.5; + color: var(--color-text); + background-color: var(--color-surface); + border: 1px solid var(--color-border); + border-radius: var(--radius-base); + transition: border-color var(--duration-fast) var(--ease-standard), + box-shadow var(--duration-fast) var(--ease-standard); +} + +textarea.form-control { + font-family: var(--font-family-base); + font-size: var(--font-size-base); +} + +select.form-control { + padding: var(--space-8) var(--space-12); + -webkit-appearance: none; + -moz-appearance: none; + appearance: none; + background-image: var(--select-caret-light); + background-repeat: no-repeat; + background-position: right var(--space-12) center; + background-size: 16px; + padding-right: var(--space-32); +} + +/* Add a dark mode specific caret */ +@media (prefers-color-scheme: dark) { + select.form-control { + background-image: var(--select-caret-dark); + } +} + +/* Also handle data-color-scheme */ +[data-color-scheme="dark"] select.form-control { + background-image: var(--select-caret-dark); +} + +[data-color-scheme="light"] select.form-control { + background-image: var(--select-caret-light); +} + +.form-control:focus { + border-color: var(--color-primary); + outline: var(--focus-outline); +} + +.form-label { + display: block; + margin-bottom: var(--space-8); + font-weight: var(--font-weight-medium); + font-size: var(--font-size-sm); +} + +.form-group { + margin-bottom: var(--space-16); +} + +/* Card component */ +.card { + background-color: var(--color-surface); + border-radius: var(--radius-lg); + border: 1px solid var(--color-card-border); + box-shadow: var(--shadow-sm); + overflow: hidden; + transition: box-shadow var(--duration-normal) var(--ease-standard); +} + +.card:hover { + box-shadow: var(--shadow-md); +} + +.card__body { + padding: var(--space-16); +} + +.card__header, +.card__footer { + padding: var(--space-16); + border-bottom: 1px solid var(--color-card-border-inner); +} + +/* Status indicators - simplified with CSS variables */ +.status { + display: inline-flex; + align-items: center; + padding: var(--space-6) var(--space-12); + border-radius: var(--radius-full); + font-weight: var(--font-weight-medium); + font-size: var(--font-size-sm); +} + +.status--success { + background-color: rgba( + var(--color-success-rgb, 33, 128, 141), + var(--status-bg-opacity) + ); + color: var(--color-success); + border: 1px solid + rgba(var(--color-success-rgb, 33, 128, 141), var(--status-border-opacity)); +} + +.status--error { + background-color: rgba( + var(--color-error-rgb, 192, 21, 47), + var(--status-bg-opacity) + ); + color: var(--color-error); + border: 1px solid + rgba(var(--color-error-rgb, 192, 21, 47), var(--status-border-opacity)); +} + +.status--warning { + background-color: rgba( + var(--color-warning-rgb, 168, 75, 47), + var(--status-bg-opacity) + ); + color: var(--color-warning); + border: 1px solid + rgba(var(--color-warning-rgb, 168, 75, 47), var(--status-border-opacity)); +} + +.status--info { + background-color: rgba( + var(--color-info-rgb, 98, 108, 113), + var(--status-bg-opacity) + ); + color: var(--color-info); + border: 1px solid + rgba(var(--color-info-rgb, 98, 108, 113), var(--status-border-opacity)); +} + +/* Container layout */ +.container { + width: 100%; + margin-right: auto; + margin-left: auto; + padding-right: var(--space-16); + padding-left: var(--space-16); +} + +@media (min-width: 640px) { + .container { + max-width: var(--container-sm); + } +} +@media (min-width: 768px) { + .container { + max-width: var(--container-md); + } +} +@media (min-width: 1024px) { + .container { + max-width: var(--container-lg); + } +} +@media (min-width: 1280px) { + .container { + max-width: var(--container-xl); + } +} + +/* Utility classes */ +.flex { + display: flex; +} +.flex-col { + flex-direction: column; +} +.items-center { + align-items: center; +} +.justify-center { + justify-content: center; +} +.justify-between { + justify-content: space-between; +} +.gap-4 { + gap: var(--space-4); +} +.gap-8 { + gap: var(--space-8); +} +.gap-16 { + gap: var(--space-16); +} + +.m-0 { + margin: 0; +} +.mt-8 { + margin-top: var(--space-8); +} +.mb-8 { + margin-bottom: var(--space-8); +} +.mx-8 { + margin-left: var(--space-8); + margin-right: var(--space-8); +} +.my-8 { + margin-top: var(--space-8); + margin-bottom: var(--space-8); +} + +.p-0 { + padding: 0; +} +.py-8 { + padding-top: var(--space-8); + padding-bottom: var(--space-8); +} +.px-8 { + padding-left: var(--space-8); + padding-right: var(--space-8); +} +.py-16 { + padding-top: var(--space-16); + padding-bottom: var(--space-16); +} +.px-16 { + padding-left: var(--space-16); + padding-right: var(--space-16); +} + +.block { + display: block; +} +.hidden { + display: none; +} + +/* Accessibility */ +.sr-only { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0, 0, 0, 0); + white-space: nowrap; + border-width: 0; +} + +:focus-visible { + outline: var(--focus-outline); + outline-offset: 2px; +} + +/* Dark mode specifics */ +[data-color-scheme="dark"] .btn--outline { + border: 1px solid var(--color-border-secondary); +} + +@font-face { + font-family: 'FKGroteskNeue'; + src: url('https://r2cdn.perplexity.ai/fonts/FKGroteskNeue.woff2') + format('woff2'); +} + +/* Application Header */ +.app-header { + background-color: var(--color-surface); + border-bottom: 1px solid var(--color-border); + padding: var(--space-16) 0; + box-shadow: var(--shadow-sm); + position: sticky; + top: 0; + z-index: 1000; +} + +.app-header h1 { + color: var(--color-primary); + font-size: var(--font-size-2xl); + margin: 0; +} + +.view-toggle { + display: flex; + gap: var(--space-8); +} + +.view-toggle .btn { + display: flex; + align-items: center; + gap: var(--space-8); + padding: var(--space-8) var(--space-16); +} + +.view-toggle .btn.active { + background: var(--color-primary); + color: var(--color-btn-primary-text); +} + +/* Main Layout */ +.app-layout { + display: grid; + grid-template-columns: 350px 1fr; + gap: var(--space-24); + padding: var(--space-24) 0; + min-height: calc(100vh - 200px); +} + +/* Sidebar */ +.sidebar { + background-color: var(--color-surface); + border-radius: var(--radius-lg); + border: 1px solid var(--color-border); + height: fit-content; + position: sticky; + top: 120px; + overflow-y: auto; + max-height: calc(100vh - 140px); +} + +.filter-section { + padding: var(--space-24); + border-bottom: 1px solid var(--color-border); +} + +.filter-section h2 { + color: var(--color-text); + font-size: var(--font-size-xl); + margin-bottom: var(--space-20); +} + +.price-filters { + display: grid; + grid-template-columns: 1fr 1fr; + gap: var(--space-8); +} + +.price-filter { + padding: var(--space-8) var(--space-12); + border: 1px solid var(--color-border); + background: var(--color-surface); + color: var(--color-text); + border-radius: var(--radius-base); + cursor: pointer; + transition: all var(--duration-fast) var(--ease-standard); + font-size: var(--font-size-sm); + text-align: center; +} + +.price-filter:hover { + background: var(--color-secondary-hover); +} + +.price-filter.active { + background: var(--color-primary); + color: var(--color-btn-primary-text); + border-color: var(--color-primary); +} + +.filter-stats { + margin-top: var(--space-20); + padding-top: var(--space-16); + border-top: 1px solid var(--color-border); + text-align: center; +} + +.filter-stats p { + font-weight: var(--font-weight-medium); + color: var(--color-text-secondary); + margin-bottom: var(--space-12); +} + +.chart-section { + padding: var(--space-24); +} + +.chart-section h3 { + color: var(--color-text); + font-size: var(--font-size-lg); + margin-bottom: var(--space-16); +} + +.chart-container { + margin-bottom: var(--space-24); +} + +.chart { + width: 100%; + height: auto; + border-radius: var(--radius-base); + border: 1px solid var(--color-border); +} + +/* Content Area */ +.content { + position: relative; + min-height: 600px; +} + +.view { + display: none; + width: 100%; + height: 100%; +} + +.view.active { + display: block; +} + +/* Map Styles */ +#map-view { + position: relative; +} + +#map { + width: 100%; + height: 700px; + border-radius: var(--radius-lg); + border: 1px solid var(--color-border); + box-shadow: var(--shadow-md); +} + +.map-legend { + position: absolute; + top: var(--space-16); + right: var(--space-16); + background: var(--color-surface); + padding: var(--space-16); + border-radius: var(--radius-base); + border: 1px solid var(--color-border); + box-shadow: var(--shadow-sm); + z-index: 1000; +} + +.map-legend h4 { + margin: 0 0 var(--space-12) 0; + font-size: var(--font-size-sm); + color: var(--color-text); +} + +.legend-item { + display: flex; + align-items: center; + gap: var(--space-8); + margin-bottom: var(--space-8); +} + +.legend-item:last-child { + margin-bottom: 0; +} + +.marker { + width: 12px; + height: 12px; + border-radius: 50%; + border: 2px solid white; +} + +.blue-marker { + background-color: var(--color-primary); +} + +.highlighted-marker { + background-color: var(--color-warning); +} + +/* List View Styles */ +#list-view { + max-height: 700px; + overflow-y: auto; + border-radius: var(--radius-lg); + border: 1px solid var(--color-border); +} + +.hotel-card { + background: var(--color-surface); + border-bottom: 1px solid var(--color-border); + padding: var(--space-20); + cursor: pointer; + transition: background-color var(--duration-fast) var(--ease-standard); +} + +.hotel-card:hover { + background: var(--color-secondary); +} + +.hotel-card:last-child { + border-bottom: none; +} + +.hotel-header { + display: flex; + justify-content: space-between; + align-items: flex-start; + margin-bottom: var(--space-12); +} + +.hotel-name { + font-size: var(--font-size-lg); + font-weight: var(--font-weight-semibold); + color: var(--color-text); + margin: 0; +} + +.hotel-price { + font-size: var(--font-size-lg); + font-weight: var(--font-weight-bold); + color: var(--color-primary); +} + +.hotel-rating { + display: flex; + align-items: center; + gap: var(--space-4); + margin-bottom: var(--space-8); +} + +.stars { + color: #FFD700; +} + +.rating-value { + font-weight: var(--font-weight-medium); + color: var(--color-text-secondary); +} + +.hotel-address { + color: var(--color-text-secondary); + font-size: var(--font-size-sm); + margin-bottom: var(--space-12); +} + +.hotel-description { + color: var(--color-text); + margin-bottom: var(--space-16); + line-height: var(--line-height-normal); +} + +.hotel-amenities { + display: flex; + flex-wrap: wrap; + gap: var(--space-8); + margin-bottom: var(--space-16); +} + +.amenity-tag { + background: var(--color-secondary); + color: var(--color-text-secondary); + padding: var(--space-4) var(--space-8); + border-radius: var(--radius-full); + font-size: var(--font-size-xs); +} + +.hotel-actions { + display: flex; + justify-content: space-between; + align-items: center; +} + +.arrondissement-badge { + background: var(--color-primary); + color: var(--color-btn-primary-text); + padding: var(--space-4) var(--space-8); + border-radius: var(--radius-full); + font-size: var(--font-size-xs); + font-weight: var(--font-weight-medium); +} + +.visit-website { + color: var(--color-primary); + text-decoration: none; + font-weight: var(--font-weight-medium); + display: flex; + align-items: center; + gap: var(--space-4); +} + +.visit-website:hover { + color: var(--color-primary-hover); +} + +/* Custom Leaflet Popup Styles */ +.leaflet-popup-content-wrapper { + border-radius: var(--radius-base); + border: 1px solid var(--color-border); +} + +.leaflet-popup-content { + margin: var(--space-16); + line-height: var(--line-height-normal); +} + +.popup-hotel-name { + font-weight: var(--font-weight-semibold); + color: var(--color-primary); + margin-bottom: var(--space-8); +} + +.popup-rating { + display: flex; + align-items: center; + gap: var(--space-4); + margin-bottom: var(--space-8); +} + +.popup-price { + font-weight: var(--font-weight-bold); + color: var(--color-warning); + margin-bottom: var(--space-8); +} + +.popup-arrondissement { + color: var(--color-text-secondary); + font-size: var(--font-size-sm); +} + +/* Footer */ +footer { + background-color: var(--color-surface); + border-top: 1px solid var(--color-border); + padding: var(--space-16) 0; + margin-top: var(--space-32); + text-align: center; + color: var(--color-text-secondary); +} + +/* Responsive Design */ +@media (max-width: 1024px) { + .app-layout { + grid-template-columns: 300px 1fr; + gap: var(--space-16); + } + + .sidebar { + top: 100px; + } +} + +@media (max-width: 768px) { + .app-header .container { + flex-direction: column; + gap: var(--space-16); + text-align: center; + } + + .app-header h1 { + font-size: var(--font-size-xl); + } + + .app-layout { + grid-template-columns: 1fr; + gap: var(--space-16); + } + + .sidebar { + position: static; + max-height: none; + } + + .filter-section { + padding: var(--space-16); + } + + .chart-section { + padding: var(--space-16); + } + + .price-filters { + grid-template-columns: 1fr; + } + + #map { + height: 500px; + } + + .map-legend { + position: static; + margin-top: var(--space-16); + } +} + +@media (max-width: 480px) { + .container { + padding-left: var(--space-12); + padding-right: var(--space-12); + } + + .app-layout { + padding: var(--space-16) 0; + } + + .view-toggle { + flex-direction: column; + width: 100%; + } + + .view-toggle .btn { + justify-content: center; + } + + .hotel-header { + flex-direction: column; + gap: var(--space-8); + } + + .hotel-actions { + flex-direction: column; + gap: var(--space-8); + align-items: flex-start; + } + + #map { + height: 400px; + } +} + +/* Loading and Empty States */ +.loading { + display: flex; + justify-content: center; + align-items: center; + height: 200px; + color: var(--color-text-secondary); +} + +.empty-state { + text-align: center; + padding: var(--space-32); + color: var(--color-text-secondary); +} + +.empty-state h3 { + margin-bottom: var(--space-16); +} + +/* Accessibility improvements */ +.btn:focus-visible, +.form-control:focus-visible, +.price-filter:focus-visible { + outline: var(--focus-outline); + outline-offset: 2px; +} + +/* Range slider styling */ +input[type="range"] { + width: 100%; + height: 6px; + border-radius: var(--radius-sm); + background: var(--color-secondary); + outline: none; + -webkit-appearance: none; +} + +input[type="range"]::-webkit-slider-thumb { + -webkit-appearance: none; + appearance: none; + width: 20px; + height: 20px; + border-radius: 50%; + background: var(--color-primary); + cursor: pointer; + border: 2px solid var(--color-surface); + box-shadow: var(--shadow-sm); +} + +input[type="range"]::-moz-range-thumb { + width: 20px; + height: 20px; + border-radius: 50%; + background: var(--color-primary); + cursor: pointer; + border: 2px solid var(--color-surface); + box-shadow: var(--shadow-sm); +} \ No newline at end of file