diff --git a/app/bookings/routes.py b/app/bookings/routes.py index abb9ba245dc0959320b9252925f08a0b897eed9d..31707fa7e69e51cc1cc3cd4d171f39ce5d5d82dc 100644 --- a/app/bookings/routes.py +++ b/app/bookings/routes.py @@ -1,6 +1,7 @@ -from flask import render_template, redirect, url_for, g +from flask import render_template, redirect, url_for, request, jsonify from app.bookings import bp -from app.models import Listings, ListingImages +from app.models import Listings +from app import db import json @@ -11,9 +12,71 @@ def redirect_index(): @bp.route('/listings') def listings(): + page = request.args.get('page', 1, type=int) + locations = Listings.get_all_locations(True) + per_page = 10 # Define how many items per page + + # Assuming get_all_listings returns a list, manually paginate all_listings = Listings.get_all_listings() + total_items = len(all_listings) + + paginated_listings = all_listings[(page - 1) * per_page: page * per_page] + + # Process images + process_images(paginated_listings) + + return render_template('bookings/listings.html', + items=paginated_listings, + page=page, + total_pages=(total_items + per_page - 1) // per_page, + locations=locations) + + + + +@bp.route('/listing/<int:id>') +def show_listing(id): + return render_template('bookings/listings.html', id=1) + + +@bp.route('/filter', methods=['POST']) +def filter_bookings(): + try: + # Get filter criteria from the request + data = request.get_json() + depart_location = data.get('depart_location', []) + destination_location = data.get('destination_location', []) + min_fair_cost = data.get('min_fair_cost') + max_fair_cost = data.get('max_fair_cost') + + # Construct the query + query = db.session.query(Listings) - for item in all_listings: + if depart_location: + depart_locations = depart_location.split(',') + query = query.filter(Listings.depart_location.in_(depart_locations)) + if destination_location: + destination_locations = destination_location.split(',') + query = query.filter(Listings.destination_location.in_(destination_locations)) + if min_fair_cost: + query = query.filter(Listings.fair_cost >= float(min_fair_cost)) + if max_fair_cost: + query = query.filter(Listings.fair_cost <= float(max_fair_cost)) + + filtered_items = query.all() + + # Process images + process_images(filtered_items) + + # Render only the relevant portion of the results + results_html = render_template('_results.html', items=filtered_items) + return jsonify({'html': results_html}) + + except Exception as e: + return jsonify({'error': str(e)}), 400 + +def process_images(listings): + for item in listings: main_image = next((img for img in item.listing_images if img.main_image), None) if main_image: item.main_image_url = url_for('main.upload_file', filename=main_image.image_location) @@ -23,12 +86,3 @@ def listings(): item.main_image_url = url_for('main.upload_file', filename='booking_image_not_found.jpg') # Must be a single quote JSON otherwise doesn't work in frontend item.image_urls = json.dumps([url_for('main.upload_file', filename=img.image_location) for img in item.listing_images]).replace('"', '"') - - return render_template('bookings/listings.html', items=all_listings) - - - - -@bp.route('/listing/<int:id>') -def show_listing(id): - return render_template('bookings/listings.html', id=1) \ No newline at end of file diff --git a/app/static/generic.js b/app/static/generic.js index a9d8acf66cdfc1e857c5bc823b3b7ec5e9ecb147..8dee71ee2a65c03879b178ef6426516e565f9db0 100644 --- a/app/static/generic.js +++ b/app/static/generic.js @@ -20,3 +20,12 @@ const populateTimeDropdowns = (dropdownIds) => { }); }); }; + + //Ensure CSRF token added to any internal requests including forms + $.ajaxSetup({ + beforeSend: function(xhr, settings) { + if (!/^http(s)?:/.test(settings.url)) { + xhr.setRequestHeader("X-CSRFToken", "{{ csrf_token() }}"); + } + } + }); \ No newline at end of file diff --git a/app/templates/_results.html b/app/templates/_results.html new file mode 100644 index 0000000000000000000000000000000000000000..b1aa476545892bee12342a167dfc15a4bfc70210 --- /dev/null +++ b/app/templates/_results.html @@ -0,0 +1,13 @@ +{% for item in items %} + <div class="results" onclick="handleClick(event, {{ item.id }})"> + <div> + <img src="{{ item.main_image_url }}" alt="Main Image" class="main-image" onclick="event.stopPropagation(); showModal({{ item.image_urls | safe }});"> + </div> + <div> + <div class="title">{{ item.depart_location }}</div> + <div class="price">{{ item.fair_cost }}</div> + <div class="delivery">{{ item.destination_location }}</div> + <div>Save up to 10% with multi-buy</div> + </div> + </div> +{% endfor %} diff --git a/app/templates/bookings/listings.html b/app/templates/bookings/listings.html index e7807399cc69580ea16667d7bc7cb5557e71c1fd..6341d44528086a34c00a0a803567dcf1d73c6772 100644 --- a/app/templates/bookings/listings.html +++ b/app/templates/bookings/listings.html @@ -1,55 +1,58 @@ {% extends 'base.html' %} {% block content %} - <style> - .container { - width: 60%; - margin: 0 auto; - font-family: Arial, sans-serif; - } - .search-bar { - margin-bottom: 20px; - display: flex; - gap: 10px; - } - .results { - border: 1px solid #ccc; - padding: 10px; - margin-bottom: 20px; - display: flex; - gap: 20px; - cursor: pointer; - } - .main-image { - max-width: 150px; - cursor: pointer; - } - .carousel-item img { - width: 100%; - height: 100%; - object-fit: cover; - } - .carousel-item { - height: 400px; - } - .carousel-inner { - position: relative; - height: 400px; - overflow: hidden; - display: flex; - } - .carousel-indicators li { - background-color: #000; - } - </style> +<style> +.select2-container { + width: 100% !important; +} + + +.results-container { + width: 60%; + margin: 0 auto; + font-family: Arial, sans-serif; +} + +.filter-button-container { + margin-top: 20px; + margin-bottom: 20px; + text-align: right; +} + +.results { + border: 1px solid #ccc; + padding: 10px; + margin-bottom: 20px; + display: flex; + gap: 20px; + cursor: pointer; +} + +.main-image { + max-width: 150px; + cursor: pointer; +} + +.modal-footer { + display: flex; + justify-content: space-between; +} + +.form-control { + width: 100%; +} + +</style> +<head> + <script src="{{ url_for('static', filename='generic.js') }}"></script> </head> -<body> - <div class="container"> - <div class="search-bar"> - <input type="text" id="searchInput" placeholder="Search..."> - <button onclick="filterResults()">Search</button> - </div> - - <div id="resultsContainer"> +<div class="filter-button-container"> + <button type="button" class="btn btn-primary me-2" data-bs-toggle="modal" data-bs-target="#filterModal"> + <i class="fa-solid fa-filter"></i> Filter + </button> +</div> + +<div class="results-container"> + <div id="resultsContainer"> {% for item in items %} <div class="results" onclick="handleClick(event, {{ item.id }})"> <div> @@ -63,110 +66,216 @@ </div> </div> {% endfor %} - </div> </div> - <!-- Modal --> - <div class="modal fade" id="imageModal" tabindex="-1" role="dialog" aria-labelledby="imageModalLabel" aria-hidden="true"> - <div class="modal-dialog modal-lg" role="document"> - <div class="modal-content"> - <div class="modal-header"> - <h5 class="modal-title" id="imageModalLabel">Image Gallery</h5> - <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button> + <!-- Pagination Controls --> + <nav aria-label="Page navigation"> + <ul class="pagination"> + {% if page > 1 %} + <li class="page-item"> + <a class="page-link" href="{{ url_for('bookings.listings', page=page - 1) }}" aria-label="Previous"> + <span aria-hidden="true">«</span> + </a> + </li> + {% endif %} + + {%- for p in range(1, total_pages + 1) %} + <li class="page-item {% if p == page %}active{% endif %}"> + <a class="page-link" href="{{ url_for('bookings.listings', page=p) }}">{{ p }}</a> + </li> + {%- endfor %} + + {% if page < total_pages %} + <li class="page-item"> + <a class="page-link" href="{{ url_for('bookings.listings', page=page + 1) }}" aria-label="Next"> + <span aria-hidden="true">»</span> + </a> + </li> + {% endif %} + </ul> + </nav> +</div> + +<!-- Image Modal --> +<div class="modal fade" id="imageModal" tabindex="-1" role="dialog" aria-labelledby="imageModalLabel" aria-hidden="true"> + <div class="modal-dialog modal-lg" role="document"> + <div class="modal-content"> + <div class="modal-header"> + <h5 class="modal-title" id="imageModalLabel">Image Gallery</h5> + <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button> + </div> + <div class="modal-body"> + <div id="carouselExampleIndicators" class="carousel slide" data-bs-ride="carousel"> + <ol class="carousel-indicators" id="carouselIndicators"></ol> + <div class="carousel-inner" id="carouselInner"></div> + <button class="carousel-control-prev" type="button" data-bs-target="#carouselExampleIndicators" data-bs-slide="prev"> + <span class="carousel-control-prev-icon" aria-hidden="true"></span> + <span class="visually-hidden">Previous</span> + </button> + <button class="carousel-control-next" type="button" data-bs-target="#carouselExampleIndicators" data-bs-slide="next"> + <span class="carousel-control-next-icon" aria-hidden="true"></span> + <span class="visually-hidden">Next</span> + </button> </div> - <div class="modal-body"> - <div id="carouselExampleIndicators" class="carousel slide" data-bs-ride="carousel"> - <ol class="carousel-indicators" id="carouselIndicators"></ol> - <div class="carousel-inner" id="carouselInner"></div> - <button class="carousel-control-prev" type="button" data-bs-target="#carouselExampleIndicators" data-bs-slide="prev"> - <span class="carousel-control-prev-icon" aria-hidden="true"></span> - <span class="visually-hidden">Previous</span> - </button> - <button class="carousel-control-next" type="button" data-bs-target="#carouselExampleIndicators" data-bs-slide="next"> - <span class="carousel-control-next-icon" aria-hidden="true"></span> - <span class="visually-hidden">Next</span> - </button> + </div> + <div class="modal-footer"> + <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button> + </div> + </div> + </div> +</div> +<!-- Hidden Filter Modal --> +<div class="modal fade" id="filterModal" tabindex="-1" aria-labelledby="filterModalLabel" aria-hidden="true"> + <div class="modal-dialog"> + <div class="modal-content"> + <div class="modal-header"> + <h5 class="modal-title" id="filterModalLabel"><i class="fa-solid fa-filter"></i> Filter Options</h5> + <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button> + </div> + <div class="modal-body"> + <form id="filter-form"> + <div class="form-group"> + <label for="depart_location" class="form-label">Depart Location:</label> + <select class="form-control select2-multiple" id="depart_location" name="depart_location[]" multiple="multiple"> + </select> </div> - </div> - <div class="modal-footer"> - <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button> + <div class="form-group"> + <label for="destination_location" class="form-label">Destination Location:</label> + <select class="form-control select2-multiple" id="destination_location" name="destination_location[]" multiple="multiple"> + </select> + </div> + <div class="form-group"> + <label for="min_fair_cost" class="form-label">Minimum Fair Cost:</label> + <input type="number" class="form-control" id="min_fair_cost" name="min_fair_cost"> + </div> + <div class="form-group"> + <label for="max_fair_cost" class="form-label">Maximum Fair Cost:</label> + <input type="number" class="form-control" id="max_fair_cost" name="max_fair_cost"> + </div> + <div class="form-group"> + <label for="transport_type" class="form-label">Transport Type:</label> + <input type="text" class="form-control" id="transport_type" name="transport_type" value="Airplane" disabled> + </div> + </form> + </div> + <div class="modal-footer"> + <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button> + <div> + <button type="button" class="btn btn-primary" id="apply-filters">Apply Filters</button> + <button type="button" class="btn btn-warning" id="reset-filters">Reset Filters</button> </div> </div> </div> </div> +</div> + +<script> + $(document).ready(function () { + $('.select2-multiple').select2({ + placeholder: "Select locations", + dropdownParent: $('#filterModal'), + width: '100%' + }); - <script> - // Initialize Bootstrap modal with jQuery - $(document).ready(function() { - $('.select2-multiple').select2({ - width: 'resolve', // Automatically adjust width - placeholder: 'Select an option', }); - $('#imageModal .btn-close, #imageModal .btn-secondary').on('click', function() { - console.log("Close button clicked"); - $('#imageModal').modal('hide'); + const locations = JSON.parse('{{ locations|tojson|safe }}'); + locations.forEach(location => { + $('#depart_location').append(new Option(location, location)); + $('#destination_location').append(new Option(location, location)); + }); + + + // Apply filters from the modal + $('#apply-filters').click(function () { + const filterData = $('#filter-form').serializeArray().reduce((acc, field) => { + acc[field.name] = field.value; + return acc; + }, {}); + + //Get bookings which match the filter results user inputs + $.ajax({ + url: "{{ url_for('bookings.filter_bookings') }}", + type: 'POST', + contentType: 'application/json', + data: JSON.stringify(filterData), + success: function (response) { + $('#resultsContainer').html(response.html); + $('#filterModal').modal('hide'); + $('.pagination').hide(); // Hide pagination as causes it to reset when changing page + }, + error: function () { + alert('Error applying filters. Please try again'); + } }); }); - // Shows pop-up with images attached to specific booking - function showModal(imageUrls) { - console.log("showModal called with imageUrls:", imageUrls); - - var carouselInner = document.getElementById('carouselInner'); - var carouselIndicators = document.getElementById('carouselIndicators'); - carouselInner.innerHTML = ''; - carouselIndicators.innerHTML = ''; - - imageUrls.forEach((imageUrl, index) => { - var activeClass = (index === 0) ? ' active' : ''; - - // Create carousel item - var div = document.createElement('div'); - div.className = 'carousel-item' + activeClass; - var img = document.createElement('img'); - img.className = 'd-block w-100'; - img.src = imageUrl; - div.appendChild(img); - carouselInner.appendChild(div); - - // Create carousel indicators - var li = document.createElement('li'); - li.setAttribute('data-bs-target', '#carouselExampleIndicators'); - li.setAttribute('data-bs-slide-to', index); - if (index === 0) { - li.className = 'active'; + // Reset filters + $('#reset-filters').click(function () { + $('#filter-form')[0].reset(); // Reset filter modal + $('.pagination').show(); // Re-show pagination + + $.ajax({ + url: "{{ url_for('bookings.filter_bookings') }}", + type: 'POST', + contentType: 'application/json', + data: JSON.stringify({}), // Send empty filter data to re-show all listings + success: function (response) { + $('#resultsContainer').html(response.html); + $('#filterModal').modal('hide'); + }, + error: function () { + alert('Error resetting filters.'); } - carouselIndicators.appendChild(li); }); + }); - var imageModal = new bootstrap.Modal(document.getElementById('imageModal')); - imageModal.show(); - } + $('.select2-multiple').select2({ + width: 'resolve', + placeholder: 'Select an option' + }); - function filterResults() { - const searchTerm = document.getElementById('searchInput').value.toLowerCase(); - const results = document.querySelectorAll('.results'); + // Close image modal + $('#imageModal .btn-close, #imageModal .btn-secondary').on('click', function() { + $('#imageModal').modal('hide'); + }); + }); - results.forEach(result => { - const title = result.querySelector('.title').textContent.toLowerCase(); - const price = result.querySelector('.price').textContent.toLowerCase(); - const delivery = result.querySelector('.delivery').textContent.toLowerCase(); + // Shows pop-up with images attached to specific booking + function showModal(imageUrls) { + console.log("showModal called with imageUrls:", imageUrls); - if (title.includes(searchTerm) || price.includes(searchTerm) || delivery.includes(searchTerm)) { - result.style.display = 'flex'; - } else { - result.style.display = 'none'; - } - }); - } + var carouselInner = document.getElementById('carouselInner'); + var carouselIndicators = document.getElementById('carouselIndicators'); + carouselInner.innerHTML = ''; + carouselIndicators.innerHTML = ''; - // Only redirects when image is NOT clicked - function handleClick(event, id) { - if (!event.target.classList.contains('main-image')) { - window.location.href = '/bookings/listing/' + id; + imageUrls.forEach((imageUrl, index) => { + var activeClass = (index === 0) ? ' active' : ''; + + // Create carousel item + var div = document.createElement('div'); + div.className = 'carousel-item' + activeClass; + var img = document.createElement('img'); + img.className = 'd-block w-100'; + img.src = imageUrl; + div.appendChild(img); + carouselInner.appendChild(div); + + // Create carousel indicators + var li = document.createElement('li'); + li.setAttribute('data-bs-target', '#carouselExampleIndicators'); + li.setAttribute('data-bs-slide-to', index); + if (index === 0) { + li.className = 'active'; } - } - </script> + carouselIndicators.appendChild(li); + }); + + var imageModal = new bootstrap.Modal(document.getElementById('imageModal')); + imageModal.show(); + } + +</script> </body> </html> {% endblock %}