diff --git a/app/__init__.py b/app/__init__.py index 82aaa5bb1fa62deaf44c6afa9ab5bedf602cfff1..47ce0e3e1a5657d2b9dc1a54b950dd720039049f 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -1,4 +1,4 @@ -from flask import Flask, g, abort, current_app, request +from flask import Flask, g, abort, current_app, request, session, redirect, url_for from config import Config from flask_sqlalchemy import SQLAlchemy from flask_migrate import Migrate @@ -99,6 +99,18 @@ def create_app(config_class=Config): 'user_permission': g.user_permission, 'super_admin_permission': g.super_admin_permission } + + # @app.errorhandler(Exception) + # def handle_exception(e): + # app.logger.error(f"Unhandled exception: {e}") + # session['error_message'] = str(e) + # return redirect(url_for('errors.quandary')) + + @app.errorhandler(403) + def handle_exception(e): + app.logger.error(f"Unhandled exception: {e}") + session['error_message'] = str(e) + return redirect(url_for('errors.quandary')) @app.before_request def before_request(): @@ -143,10 +155,3 @@ def register_blueprints(app): for module_name, url_prefix in blueprints: module = __import__(f'app.{module_name}', fromlist=['bp']) app.register_blueprint(module.bp, url_prefix=url_prefix) - -# @app.errorhandler(Exception) - # def handle_exception(e): - # app.logger.error(f"Unhandled exception: {e}") - # session['error_message'] = str(e) - # return redirect(url_for('errors.quandary')) - # pass \ No newline at end of file diff --git a/app/admin/routes.py b/app/admin/routes.py index e0d40f09e4f0ddc3e1174cc87728b2d6c24f96fb..9a74904d651621fc43ca7f9cc1742413bb546f3d 100644 --- a/app/admin/routes.py +++ b/app/admin/routes.py @@ -158,6 +158,69 @@ def get_bookings(): return jsonify(result) +@bp.route('create_listing', methods=['GET']) +@permission_required(admin_permission) +def create_listing(): + locations = Listings.get_all_locations() + return render_template('admin/create_listing.html', locations=locations) + + +@bp.route('create_listing', methods=['POST']) +@permission_required(admin_permission) +def create_listing_post(): + # Extract form data + depart_location = request.form.get('departLocation') + destination_location = request.form.get('destinationLocation') + depart_time = request.form.get('departTime') + destination_time = request.form.get('destinationTime') + fair_cost = request.form.get('fairCost') + transport_type = request.form.get('transportType') + images = request.files.getlist('images') + + # Create the listing instance + new_listing = Listings( + depart_location=depart_location, + destination_location=destination_location, + depart_time=depart_time, + destination_time=destination_time, + fair_cost=fair_cost, + transport_type=transport_type + ) + + try: + # Commit the new listing to get the ID + db.session.add(new_listing) + db.session.commit() + + # Save images and track the first one as the main image + main_image_id = None + + for idx, image in enumerate(images): + saved_image = ListingImages.save_image(image, new_listing.id) + + if not saved_image: + continue + + if idx == 0: + main_image_id = saved_image.id + + # Set main image if available + if main_image_id: + ListingImages.set_main_image(new_listing.id, main_image_id) + + db.session.commit() + + except Exception as e: + print(f"Error: {e}") + db.session.rollback() + locations = Listings.get_all_locations() + flash('An error occurred while creating the booking. Please try again', 'error') + return render_template('admin/create_booking.html', locations=locations) + + flash('Successfully created booking', 'success') + return redirect(url_for('admin.manage_bookings')) + + @bp.route('delete_booking', methods=['DELETE']) @permission_required(admin_permission) def delete_booking(): @@ -169,3 +232,28 @@ def delete_booking(): http_code = 200 return jsonify(success), http_code + +@bp.route('/delete_image/<int:image_id>', methods=['POST']) +@permission_required(admin_permission) +def delete_image(image_id): + try: + image_to_delete = ListingImages.query.get_or_404(image_id) + listing_id = image_to_delete.listing_id + + db.session.delete(image_to_delete) + db.session.commit() + + new_main_image_id = None + if image_to_delete.main_image: + # Find another image for the same listing to set as main + new_main_image = ListingImages.query.filter_by(listing_id=listing_id).first() + if new_main_image: + new_main_image.main_image = True + db.session.commit() + new_main_image_id = new_main_image.id + + return jsonify({'success': True, 'message': 'Image deleted successfully.', 'image_id': new_main_image_id}) + + except Exception as e: + db.session.rollback() + return jsonify({'success': False, 'error': str(e)}), 500 diff --git a/app/logger/__init__.py b/app/logger/__init__.py index 8c8e6584d64811fcb1e3c1bbae4b43b0c672eedb..c2ca2febecbbc04ac35ed4aedb688f60a6086e95 100644 --- a/app/logger/__init__.py +++ b/app/logger/__init__.py @@ -36,3 +36,5 @@ logger_config = LoggerConfig() app_logger = logger_config.setup_logger('app', log_file='app.log', level=LOG_LEVELS['debug']) db_logger = logger_config.setup_logger('db', log_file='db.log', level=LOG_LEVELS['info']) auth_logger = logger_config.setup_logger('auth', log_file='auth.log', level=LOG_LEVELS['debug']) +error_logger = logger_config.setup_logger('error', log_file='error.log', level=LOG_LEVELS['debug']) +debug_logger = logger_config.setup_logger('debug', log_file='debug.log', level=LOG_LEVELS['debug']) diff --git a/app/main/routes.py b/app/main/routes.py index 9d37e2641502bb381b11bb7f40997d2bb32b55b3..850ca7c5f4c8db193d10caa12feab71b665d795c 100644 --- a/app/main/routes.py +++ b/app/main/routes.py @@ -1,6 +1,7 @@ -from flask import render_template, send_from_directory +from flask import render_template, send_from_directory, request from app.models import Listings, ListingImages from app.main import bp +from app.logger import * import datetime import os @@ -28,3 +29,27 @@ def upload_file(filename): upload_folder = os.path.join(os.getcwd(), 'app/uploads') return send_from_directory(upload_folder, f'listing_images/{filename}') + +# Should only be used by ajax calls +@bp.route('/log_message', methods=['POST']) +def log_message(): + data = request.get_json() + log_message = data.get('log_message') + log_type = data.get('type') + + if log_type == 'app': + app_logger.info(log_message) + + if log_type == 'db': + db_logger.info(log_message) + + if log_type == 'auth': + auth_logger.info(log_message) + + if log_type == 'error': + error_logger.info(log_message) + + if log_type == 'debug': + debug_logger.info(log_message) + + return True \ No newline at end of file diff --git a/app/models/listing_images.py b/app/models/listing_images.py index e0b41f6ef1054b2607126bd94135725a2e740d25..bddaf71c9a22b9e17a68e625397a8fe06b43edf8 100644 --- a/app/models/listing_images.py +++ b/app/models/listing_images.py @@ -36,22 +36,26 @@ class ListingImages(db.Model): def save_image(file, listing_id): if file and ListingImages.allowed_file(file.filename): extension = file.filename.rsplit('.', 1)[1].lower() - filename = f"{listing_id}_{uuid.uuid4().hex}.{extension}" #Change to make all files uploaded unique to prevent collisions + filename = f"{listing_id}_{uuid.uuid4().hex}.{extension}" # Unique filename upload_folder = current_app.config['BOOKING_IMAGE_UPLOADS'] file_path = os.path.join(upload_folder, filename) try: os.makedirs(upload_folder, exist_ok=True) file.save(file_path) + + # Create and save new image record new_image = ListingImages( listing_id=listing_id, image_location=filename, main_image=False ) db.session.add(new_image) + db.session.commit() # Commit here to ensure `id` is generated return new_image except Exception as e: print(f"Error saving file: {e}") + db.session.rollback() return None else: return False diff --git a/app/models/listings.py b/app/models/listings.py index b74c85871c46356d191ab26f7738d42f38e9a3bb..fae84c2bea9fc3dceb7bad37918ba72c685de983 100644 --- a/app/models/listings.py +++ b/app/models/listings.py @@ -21,8 +21,8 @@ class Listings(db.Model): @classmethod def get_all_locations(cls): - query = text("SELECT depart_location AS location FROM listings UNION SELECT destination_location AS location FROM listings") - result = db.session.execute(query) + all_locations = text("SELECT depart_location AS location FROM listings UNION SELECT destination_location AS location FROM listings") + result = db.session.execute(all_locations) return [location[0] for location in result] @classmethod diff --git a/app/templates/admin/create_listing.html b/app/templates/admin/create_listing.html new file mode 100644 index 0000000000000000000000000000000000000000..18a577033f514dd9c1f3c2573032622b55711d10 --- /dev/null +++ b/app/templates/admin/create_listing.html @@ -0,0 +1,183 @@ +{% extends 'base.html' %} +{% block content %} +<div class="container mt-4"> + <h2>Create New Booking</h2> + <!-- UPDATE BELOW TO MAKE NEW BOOKING DASFHISOD)HAISOD --> + <form id="createBookingForm" class="row g-3" action="{{ url_for('admin.create_listing_post') }}" method="post" enctype="multipart/form-data"> + <input type="hidden" name="csrf_token" value="{{ csrf_token() }}"> + <div class="col-md-6"> + <label for="departLocation" class="form-label">Departure Location:</label> + <select class="form-select select2-multiple" id="departLocation" name="departLocation" required> + <option value="" disabled selected>Select locations</option> + {% for location in locations %} + <option value="{{ location }}">{{ location }}</option> + {% endfor %} + </select> + <button type="button" class="btn btn-primary mt-2" data-bs-toggle="modal" data-bs-target="#addLocationModal">Add New Location</button> + </div> + <div class="col-md-6"> + <label for="destinationLocation" class="form-label">Destination Location:</label> + <select class="form-select select2-multiple" id="destinationLocation" name="destinationLocation" required> + <option value="" disabled selected>Select locations</option> + {% for location in locations %} + <option value="{{ location }}">{{ location }}</option> + {% endfor %} + </select> + </div> + <div class="col-md-6"> + <label for="departTime" class="form-label">Departure Time:</label> + <select class="form-control select2-dropdown" id="departTime" name="departTime" required> + <option value="" disabled>Select a time</option> + {% for time in time_options %} + <option value="{{ time }}">{{ time }}</option> + {% endfor %} + </select> + </div> + <div class="col-md-6"> + <label for="destinationTime" class="form-label">Arrival Time:</label> + <select class="form-control select2-dropdown" id="destinationTime" name="destinationTime" required> + <option value="" disabled>Select a time</option> + {% for time in time_options %} + <option value="{{ time }}">{{ time }}</option> + {% endfor %} + </select> + </div> + <div class="col-md-6"> + <label for="fairCost" class="form-label">Fair Cost:</label> + <input type="number" step="0.01" class="form-control" id="fairCost" name="fairCost" required> + </div> + <div class="col-md-6"> + <label for="transportType" class="form-label">Transport Type:</label> + <select id="transportType" class="form-select" name="transportType" value="Airplane" readonly> + <option value="Airplane">Airplane</option> + </select> + </div> + <div class="col-md-12"> + <label for="images" class="form-label">Upload Images:</label> + <input type="file" class="form-control" id="images" name="images" multiple> + </div> + <div class="col-12"> + <button type="submit" class="btn btn-primary">Create Booking</button> + </div> + </form> +</div> + +<!-- Add New Location Modal --> +<div class="modal fade" id="addLocationModal" tabindex="-1" aria-labelledby="addLocationModalLabel" aria-hidden="true"> + <div class="modal-dialog"> + <div class="modal-content"> + <div class="modal-header bg-primary text-white"> + <h5 class="modal-title" id="addLocationModalLabel">Add New Location</h5> + <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button> + </div> + <div class="modal-body"> + <form id="addLocationForm"> + <div class="mb-3"> + <label for="newLocation" class="form-label">Location Name:</label> + <input type="text" class="form-control" id="newLocation" name="newLocation" required> + </div> + <button type="button" class="btn btn-primary" id="saveLocationBtn">Save Location</button> + </form> + </div> + </div> + </div> +</div> + +<!-- Location added successfully Modal --> +<div class="modal fade" id="locationSuccessModal" tabindex="-1" aria-labelledby="locationSuccessModalLabel" aria-hidden="true"> + <div class="modal-dialog"> + <div class="modal-content"> + <div class="modal-header bg-success text-white"> + <h5 class="modal-title" id="locationSuccessModalLabel">Success</h5> + <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button> + </div> + <div class="modal-body"> + <p>Location added successfully.</p> + <button type="button" class="btn btn-success" data-bs-dismiss="modal">OK</button> + </div> + </div> + </div> +</div> + +<!-- Location already exists or invalid Modal --> +<div class="modal fade" id="locationErrorModal" tabindex="-1" aria-labelledby="locationErrorModalLabel" aria-hidden="true"> + <div class="modal-dialog"> + <div class="modal-content"> + <div class="modal-header bg-danger text-white"> + <h5 class="modal-title" id="locationErrorModalLabel">Error</h5> + <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button> + </div> + <div class="modal-body"> + <p>Location already exists or is invalid.</p> + <button type="button" class="btn btn-danger" data-bs-dismiss="modal">OK</button> + </div> + </div> + </div> +</div> + +<script> + $('.select2-dropdown').select2({ + width: '100%', + minimumResultsForSearch: Infinity + }); + + const time_options = [ + "00:00", "00:30", "01:00", "01:30", "02:00", "02:30", "03:00", "03:30", "04:00", "04:30", + "05:00", "05:30", "06:00", "06:30", "07:00", "07:30", "08:00", "08:30", "09:00", "09:30", + "10:00", "10:30", "11:00", "11:30", "12:00", "12:30", "13:00", "13:30", "14:00", "14:30", + "15:00", "15:30", "16:00", "16:30", "17:00", "17:30", "18:00", "18:30", "19:00", "19:30", + "20:00", "20:30", "21:00", "21:30", "22:00", "22:30", "23:00", "23:30" + ]; + + // Add time_options to filter elements + time_options.forEach(time => { + $('#departTime').append(new Option(time, time)); + $('#destinationTime').append(new Option(time, time)); + }); + + // Function to update dropdowns with new locations + const updateDropdowns = (newLocations, dropdown) => { + newLocations.forEach(location => { + $(dropdown).append(new Option(location, location)); + }); + }; + + // JavaScript to handle adding new locations + let locations = JSON.parse('{{ locations|tojson|safe }}'); + + $('#saveLocationBtn').on('click', function() { + const newLocation = $('#newLocation').val().trim(); + if (newLocation && !locations.includes(newLocation)) { + locations.push(newLocation); + updateDropdowns([newLocation], '#departLocation'); + updateDropdowns([newLocation], '#destinationLocation'); + $('#locationSuccessModal').modal('show'); + $('#addLocationModal').modal('hide'); + $('#newLocation').val(''); + } else { + $('#locationErrorModal').modal('show'); + } + }); +</script> +<style> + .image-container { + position: relative; + width: 100%; + padding-top: 100%; + overflow: hidden; + display: flex; + justify-content: center; + align-items: center; + } + + .image-container img { + position: absolute; + top: 50%; + left: 50%; + width: 100%; + height: 100%; + object-fit: cover; + transform: translate(-50%, -50%); + } +</style> +{% endblock %} diff --git a/app/templates/admin/edit_booking.html b/app/templates/admin/edit_booking.html index 0974cd8e3ca24c817babb5b9554332695967e891..68114bc634ef789e107bbdda8330dc9631a86329 100644 --- a/app/templates/admin/edit_booking.html +++ b/app/templates/admin/edit_booking.html @@ -12,15 +12,7 @@ <option value="{{ location }}" {% if location == listing.depart_location %}selected{% endif %}>{{ location }}</option> {% endfor %} </select> - </div> - <div class="col-md-6"> - <label for="departTime" class="form-label">Departure Time:</label> - <select class="form-control select2-dropdown" id="departTime" name="departTime" required> - <option value="" disabled>Select a time</option> - {% for time in time_options %} - <option value="{{ time }}" {% if time == depart_time_str %}selected{% endif %}>{{ time }}</option> - {% endfor %} - </select> + <button type="button" class="btn btn-primary mt-2" data-bs-toggle="modal" data-bs-target="#addLocationModal">Add New Location</button> </div> <div class="col-md-6"> <label for="destinationLocation" class="form-label">Destination Location:</label> @@ -31,6 +23,15 @@ {% endfor %} </select> </div> + <div class="col-md-6"> + <label for="departTime" class="form-label">Departure Time:</label> + <select class="form-control select2-dropdown" id="departTime" name="departTime" required> + <option value="" disabled>Select a time</option> + {% for time in time_options %} + <option value="{{ time }}" {% if time == depart_time_str %}selected{% endif %}>{{ time }}</option> + {% endfor %} + </select> + </div> <div class="col-md-6"> <label for="destinationTime" class="form-label">Arrival Time:</label> <select class="form-control select2-dropdown" id="destinationTime" name="destinationTime" required> @@ -66,7 +67,7 @@ <input type="radio" class="btn-check" name="main_image" id="{{image.id}}" value="{{ image.id }}" autocomplete="off" {% if image.main_image == 1 %}checked{% endif %}> <label class="btn btn-outline-success w-100" for="{{image.id}}">Main Image</label> - <button type="button" class="btn btn-danger btn-sm mt-2 delete-image-btn w-100" data-image="{{ image.image_location }}">Delete</button> + <button type="button" class="btn btn-danger btn-sm mt-2 delete-image-btn w-100" data-image-id="{{ image.id }}">Delete</button> </div> </div> </div> @@ -80,6 +81,58 @@ </form> </div> +<!-- Add New Location Modal --> +<div class="modal fade" id="addLocationModal" tabindex="-1" aria-labelledby="addLocationModalLabel" aria-hidden="true"> + <div class="modal-dialog"> + <div class="modal-content"> + <div class="modal-header bg-primary text-white"> + <h5 class="modal-title" id="addLocationModalLabel">Add New Location</h5> + <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button> + </div> + <div class="modal-body"> + <form id="addLocationForm"> + <div class="mb-3"> + <label for="newLocation" class="form-label">Location Name:</label> + <input type="text" class="form-control" id="newLocation" name="newLocation" required> + </div> + <button type="button" class="btn btn-primary" id="saveLocationBtn">Save Location</button> + </form> + </div> + </div> + </div> +</div> + +<!-- Add Location Success --> +<div class="modal fade" id="location_success" tabindex="-1" aria-labelledby="location_successLabel" aria-hidden="true"> + <div class="modal-dialog"> + <div class="modal-content"> + <div class="modal-header bg-success text-white"> + <h5 class="modal-title" id="location_successLabel">Success</h5> + <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button> + </div> + <div class="modal-body"> + <p>Location added successfully.</p> + <button type="button" class="btn btn-success" data-bs-dismiss="modal">OK</button> + </div> + </div> + </div> +</div> + +<!-- Add Location error modal does not use js alert or confirm anymore --> +<div class="modal fade" id="location_error_modal" tabindex="-1" aria-labelledby="location_error_modalLabel" aria-hidden="true"> + <div class="modal-dialog"> + <div class="modal-content"> + <div class="modal-header bg-danger text-white"> + <h5 class="modal-title" id="location_error_modalLabel">Error</h5> + <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button> + </div> + <div class="modal-body"> + <p>Location already exists</p> + <button type="button" class="btn btn-danger" data-bs-dismiss="modal">OK</button> + </div> + </div> + </div> +</div> <script> $('.select2-dropdown').select2({ width: '100%', @@ -100,14 +153,99 @@ $('#destinationTime').append(new Option(time, time)); }); - // JavaScript to handle image deletion + // Add New location to dropdowns (will clear upon reload OR if not exist in db) + const updateDropdowns = (newLocations, dropdown) => { + newLocations.forEach(location => { + $(dropdown).append(new Option(location, location)); + }); + }; + + // JavaScript to handle adding new locations + let locations = JSON.parse('{{ locations|tojson|safe }}'); + + $('#saveLocationBtn').on('click', function() { + const newLocation = $('#newLocation').val().trim(); + if (newLocation && !locations.includes(newLocation)) { + locations.push(newLocation); + updateDropdowns([newLocation], '#departLocation'); + updateDropdowns([newLocation], '#destinationLocation'); + $('#location_success').modal('show'); + $('#addLocationModal').modal('hide'); + $('#newLocation').val(''); + } else { + $('#location_error_modal').modal('show'); + } + }); + document.querySelectorAll('.delete-image-btn').forEach(button => { - button.addEventListener('click', function() { - const image = this.getAttribute('data-image'); - this.closest('.col-md-3').remove(); - console.log('Deleted image:', image); + button.addEventListener('click', function () { + const imageId = this.getAttribute('data-image-id'); + if (!imageId) { + logMessage('Image ID not found when trying to delete image', 'error'); + return; + } + + if (!confirm('Are you sure you want to delete this image?')) { + return; + } + + const image_container = this.closest('.col-md-3'); + const delete_url = `{{ url_for('admin.delete_image', image_id=0) }}`.replace('0', imageId); + + fetch(delete_url, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-CSRFToken': '{{ csrf_token() }}' + } + }) + .then(response => response.json()) + .then(data => { + if (data.success) { + image_container.remove(); + const username = "{{ current_user.username }}"; + logMessage(`User ${username} has deleted Image ID: ${imageId}`, 'app'); + + // Auto-select the first image as the new main image if the deleted one was the main image + if (data.image_id) { + const first_image = document.querySelector('input[name="main_image"]'); + if (first_image) { + first_image.checked = true; + logMessage('Selected new main image:', first_image.id, 'debug'); + } + } + } else { + logMessage(`Failed to delete image: ${data.error}`, 'error'); + } + }) + .catch(error => logMessage('Error: ' + error, 'error')); }); }); + + function logMessage(errorMessage, type) { + fetch('{{ url_for("main.log_message") }}', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-CSRFToken': '{{ csrf_token() }}' + }, + body: JSON.stringify({ error: errorMessage, type: type }) + }); + } + + + function logMessage(error_message, type) { + fetch('{{ url_for("main.log_message") }}', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-CSRFToken': '{{ csrf_token() }}' + }, + body: JSON.stringify({ error: error_message, type: type }) + }) + } + + </script> <style> .image-container { diff --git a/app/templates/admin/manage_bookings.html b/app/templates/admin/manage_bookings.html index 88aff7ce090b3d0969600f6380fef1fc66b34583..3fa3ab9fb61306331f1e666c7299be823afca436 100644 --- a/app/templates/admin/manage_bookings.html +++ b/app/templates/admin/manage_bookings.html @@ -3,9 +3,14 @@ <div class="container my-4"> <div class="d-flex justify-content-between mb-3"> <h2>Manage Bookings</h2> - <button type="button" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#filterModal"> - <i class="fa-solid fa-filter"></i> Filter - </button> + <div> + <a href="{{ url_for('admin.create_listing') }}" class="btn btn-success"> + <i class="fa-solid fa-plus"></i> Create New Listing + </a> + <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> <!-- Hidden Filter Modal --> <div class="modal fade" id="filterModal" tabindex="-1" aria-labelledby="filterModalLabel" aria-hidden="true"> diff --git a/app/templates/errors/quandary.html b/app/templates/errors/quandary.html index 1fd93fe88681cc2329384965f1887d6c930fbff3..c699910b54fc50548d541b5d2a5051cb158140be 100644 --- a/app/templates/errors/quandary.html +++ b/app/templates/errors/quandary.html @@ -1,28 +1,59 @@ {% block content %} <head> - <link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='base.css')}}"> + <link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='base.css') }}"> <script src="https://kit.fontawesome.com/11fd621de6.js" crossorigin="anonymous"></script> </head> <div class="quandary-div"> - <h1>500 Server Error</h1> + <h1>Something went wrong</h1> <div class="container"> - <div><span>Something went wrong, if this continues please contact support</span></div> - <div class="button_2"><i class="fa-solid fa-circle-arrow-left"></i><span> Go back</span></div> + <div><span>Something went wrong, if this continues please contact support</span></div> + <div class="button_2" onclick="history.back()"> + <i class="fa-solid fa-circle-arrow-left"></i><span> Go back</span> + </div> + <div class="button_2"> + <a href="{{ url_for('main.index') }}" class="button_2-link"> + <i class="fa-solid fa-home"></i><span> Home</span> + </a> + </div> </div> </div> <style> - .quandary-div { - height: 100vh; - display: flex; - flex-direction: column; - justify-content: center; - align-items: baseline; - align-content: center; - font-size: xx-large; - flex-wrap: wrap; + height: 100vh; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + text-align: center; + font-size: xx-large; +} + +.button_2 { + display: inline-block; + margin: 10px 0; + padding: 10px 20px; + color: white; + background-color: #007bff; + border: none; + border-radius: 5px; + text-align: center; + text-decoration: none; + cursor: pointer; + transition: background-color 0.3s; +} + +.button_2-link { + color: white; + text-decoration: none; +} + +.button_2 i { + margin-right: 5px; } +.button_2:hover { + background-color: #0056b3; +} </style> -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/app/uploads/listing_images/10_8eb674d5d506434eba0e5e17f74155b3.gif b/app/uploads/listing_images/10_8eb674d5d506434eba0e5e17f74155b3.gif new file mode 100644 index 0000000000000000000000000000000000000000..71aede03087e88f8e47af526b1bcb71adb687192 Binary files /dev/null and b/app/uploads/listing_images/10_8eb674d5d506434eba0e5e17f74155b3.gif differ diff --git a/app/uploads/listing_images/11_5a6dd8a340a94f73bd0944d05f3ea031.gif b/app/uploads/listing_images/11_5a6dd8a340a94f73bd0944d05f3ea031.gif new file mode 100644 index 0000000000000000000000000000000000000000..71aede03087e88f8e47af526b1bcb71adb687192 Binary files /dev/null and b/app/uploads/listing_images/11_5a6dd8a340a94f73bd0944d05f3ea031.gif differ diff --git a/app/uploads/listing_images/11_9a0e141b32664485a4287406913b458f.jpeg b/app/uploads/listing_images/11_9a0e141b32664485a4287406913b458f.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..c775255f893e8d4baaad92fc47832c84a101e102 Binary files /dev/null and b/app/uploads/listing_images/11_9a0e141b32664485a4287406913b458f.jpeg differ diff --git a/app/uploads/listing_images/12_ebc6362642ce45a9b368c34ced88a48b.jpeg b/app/uploads/listing_images/12_ebc6362642ce45a9b368c34ced88a48b.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..c775255f893e8d4baaad92fc47832c84a101e102 Binary files /dev/null and b/app/uploads/listing_images/12_ebc6362642ce45a9b368c34ced88a48b.jpeg differ diff --git a/app/uploads/listing_images/13_55d51cf913734e97a5d27e4ee6364bd9.jpeg b/app/uploads/listing_images/13_55d51cf913734e97a5d27e4ee6364bd9.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..c775255f893e8d4baaad92fc47832c84a101e102 Binary files /dev/null and b/app/uploads/listing_images/13_55d51cf913734e97a5d27e4ee6364bd9.jpeg differ diff --git a/app/uploads/listing_images/13_837d8e11b02e42518bc49f852479f593.gif b/app/uploads/listing_images/13_837d8e11b02e42518bc49f852479f593.gif new file mode 100644 index 0000000000000000000000000000000000000000..71aede03087e88f8e47af526b1bcb71adb687192 Binary files /dev/null and b/app/uploads/listing_images/13_837d8e11b02e42518bc49f852479f593.gif differ diff --git a/app/uploads/listing_images/13_bb4e31d876d24c9299c4f4f64b2585c7.jpeg b/app/uploads/listing_images/13_bb4e31d876d24c9299c4f4f64b2585c7.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..c775255f893e8d4baaad92fc47832c84a101e102 Binary files /dev/null and b/app/uploads/listing_images/13_bb4e31d876d24c9299c4f4f64b2585c7.jpeg differ diff --git a/app/uploads/listing_images/14_c42eaa5fc7914c638506387bd28a3240.gif b/app/uploads/listing_images/14_c42eaa5fc7914c638506387bd28a3240.gif new file mode 100644 index 0000000000000000000000000000000000000000..71aede03087e88f8e47af526b1bcb71adb687192 Binary files /dev/null and b/app/uploads/listing_images/14_c42eaa5fc7914c638506387bd28a3240.gif differ diff --git a/app/uploads/listing_images/15_11afc1a79d714669a1c1445b8e30b424.gif b/app/uploads/listing_images/15_11afc1a79d714669a1c1445b8e30b424.gif new file mode 100644 index 0000000000000000000000000000000000000000..71aede03087e88f8e47af526b1bcb71adb687192 Binary files /dev/null and b/app/uploads/listing_images/15_11afc1a79d714669a1c1445b8e30b424.gif differ diff --git a/app/uploads/listing_images/1_0209974166404366aad9241cfcb4d61d.jpeg b/app/uploads/listing_images/1_0209974166404366aad9241cfcb4d61d.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..c775255f893e8d4baaad92fc47832c84a101e102 Binary files /dev/null and b/app/uploads/listing_images/1_0209974166404366aad9241cfcb4d61d.jpeg differ diff --git a/app/uploads/listing_images/1_2a228ec56c17480f9a50d817e13eabdc.gif b/app/uploads/listing_images/1_2a228ec56c17480f9a50d817e13eabdc.gif new file mode 100644 index 0000000000000000000000000000000000000000..71aede03087e88f8e47af526b1bcb71adb687192 Binary files /dev/null and b/app/uploads/listing_images/1_2a228ec56c17480f9a50d817e13eabdc.gif differ diff --git a/app/uploads/listing_images/1_2c58c8f0b12b4464891df99d83277fbd.jpeg b/app/uploads/listing_images/1_2c58c8f0b12b4464891df99d83277fbd.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..c775255f893e8d4baaad92fc47832c84a101e102 Binary files /dev/null and b/app/uploads/listing_images/1_2c58c8f0b12b4464891df99d83277fbd.jpeg differ diff --git a/app/uploads/listing_images/1_3ae897d9101943e3ab05dd4a3c810d93.gif b/app/uploads/listing_images/1_3ae897d9101943e3ab05dd4a3c810d93.gif new file mode 100644 index 0000000000000000000000000000000000000000..71aede03087e88f8e47af526b1bcb71adb687192 Binary files /dev/null and b/app/uploads/listing_images/1_3ae897d9101943e3ab05dd4a3c810d93.gif differ diff --git a/app/uploads/listing_images/1_55c332c9401444dcac2ae2287770fa36.gif b/app/uploads/listing_images/1_55c332c9401444dcac2ae2287770fa36.gif new file mode 100644 index 0000000000000000000000000000000000000000..71aede03087e88f8e47af526b1bcb71adb687192 Binary files /dev/null and b/app/uploads/listing_images/1_55c332c9401444dcac2ae2287770fa36.gif differ diff --git a/app/uploads/listing_images/1_5aa0c3b90c2747c48f871b0adeaca552.jpeg b/app/uploads/listing_images/1_5aa0c3b90c2747c48f871b0adeaca552.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..c775255f893e8d4baaad92fc47832c84a101e102 Binary files /dev/null and b/app/uploads/listing_images/1_5aa0c3b90c2747c48f871b0adeaca552.jpeg differ diff --git a/app/uploads/listing_images/1_5e03e27a16cc4120bb7c4d895337f407.jpeg b/app/uploads/listing_images/1_5e03e27a16cc4120bb7c4d895337f407.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..c775255f893e8d4baaad92fc47832c84a101e102 Binary files /dev/null and b/app/uploads/listing_images/1_5e03e27a16cc4120bb7c4d895337f407.jpeg differ diff --git a/app/uploads/listing_images/1_6eee29decc3e4885887a4b1a8fe1ad79.jpeg b/app/uploads/listing_images/1_6eee29decc3e4885887a4b1a8fe1ad79.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..c775255f893e8d4baaad92fc47832c84a101e102 Binary files /dev/null and b/app/uploads/listing_images/1_6eee29decc3e4885887a4b1a8fe1ad79.jpeg differ diff --git a/app/uploads/listing_images/1_71fa79a869a243f9a618c95ed2061fb2.jpeg b/app/uploads/listing_images/1_71fa79a869a243f9a618c95ed2061fb2.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..c775255f893e8d4baaad92fc47832c84a101e102 Binary files /dev/null and b/app/uploads/listing_images/1_71fa79a869a243f9a618c95ed2061fb2.jpeg differ diff --git a/app/uploads/listing_images/1_7e3cbcb3eaa8478a91ca0977436fc229.jpeg b/app/uploads/listing_images/1_7e3cbcb3eaa8478a91ca0977436fc229.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..c775255f893e8d4baaad92fc47832c84a101e102 Binary files /dev/null and b/app/uploads/listing_images/1_7e3cbcb3eaa8478a91ca0977436fc229.jpeg differ diff --git a/app/uploads/listing_images/1_84c1c9accac447fb88a1250435e6face.jpeg b/app/uploads/listing_images/1_84c1c9accac447fb88a1250435e6face.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..c775255f893e8d4baaad92fc47832c84a101e102 Binary files /dev/null and b/app/uploads/listing_images/1_84c1c9accac447fb88a1250435e6face.jpeg differ diff --git a/app/uploads/listing_images/1_9bfc73b3872b4f78b02081cff0090815.gif b/app/uploads/listing_images/1_9bfc73b3872b4f78b02081cff0090815.gif new file mode 100644 index 0000000000000000000000000000000000000000..71aede03087e88f8e47af526b1bcb71adb687192 Binary files /dev/null and b/app/uploads/listing_images/1_9bfc73b3872b4f78b02081cff0090815.gif differ diff --git a/app/uploads/listing_images/1_a8ba4004ca4b4236971be53fb86462a2.gif b/app/uploads/listing_images/1_a8ba4004ca4b4236971be53fb86462a2.gif new file mode 100644 index 0000000000000000000000000000000000000000..71aede03087e88f8e47af526b1bcb71adb687192 Binary files /dev/null and b/app/uploads/listing_images/1_a8ba4004ca4b4236971be53fb86462a2.gif differ diff --git a/app/uploads/listing_images/1_aa6278b77ec644db96cb6cbde5cebe9b.gif b/app/uploads/listing_images/1_aa6278b77ec644db96cb6cbde5cebe9b.gif new file mode 100644 index 0000000000000000000000000000000000000000..71aede03087e88f8e47af526b1bcb71adb687192 Binary files /dev/null and b/app/uploads/listing_images/1_aa6278b77ec644db96cb6cbde5cebe9b.gif differ diff --git a/app/uploads/listing_images/1_d1400b42a1e34f8d90b3d0df4fbca387.jpeg b/app/uploads/listing_images/1_d1400b42a1e34f8d90b3d0df4fbca387.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..c775255f893e8d4baaad92fc47832c84a101e102 Binary files /dev/null and b/app/uploads/listing_images/1_d1400b42a1e34f8d90b3d0df4fbca387.jpeg differ diff --git a/app/uploads/listing_images/1_d16684c9b0f04b28a03976a492e4b6c3.jpeg b/app/uploads/listing_images/1_d16684c9b0f04b28a03976a492e4b6c3.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..c775255f893e8d4baaad92fc47832c84a101e102 Binary files /dev/null and b/app/uploads/listing_images/1_d16684c9b0f04b28a03976a492e4b6c3.jpeg differ diff --git a/app/uploads/listing_images/1_db1af7ac55e94abeadf4cf67be1cf0ed.gif b/app/uploads/listing_images/1_db1af7ac55e94abeadf4cf67be1cf0ed.gif new file mode 100644 index 0000000000000000000000000000000000000000..71aede03087e88f8e47af526b1bcb71adb687192 Binary files /dev/null and b/app/uploads/listing_images/1_db1af7ac55e94abeadf4cf67be1cf0ed.gif differ diff --git a/app/uploads/listing_images/1_e72ea26e259a4913818d65f74897f94a.gif b/app/uploads/listing_images/1_e72ea26e259a4913818d65f74897f94a.gif new file mode 100644 index 0000000000000000000000000000000000000000..71aede03087e88f8e47af526b1bcb71adb687192 Binary files /dev/null and b/app/uploads/listing_images/1_e72ea26e259a4913818d65f74897f94a.gif differ diff --git a/app/uploads/listing_images/1_e91895bf1dfd49edae36119272db8208.gif b/app/uploads/listing_images/1_e91895bf1dfd49edae36119272db8208.gif new file mode 100644 index 0000000000000000000000000000000000000000..71aede03087e88f8e47af526b1bcb71adb687192 Binary files /dev/null and b/app/uploads/listing_images/1_e91895bf1dfd49edae36119272db8208.gif differ diff --git a/app/uploads/listing_images/1_fd57ab2218734e0e91a35bc210cb2f81.gif b/app/uploads/listing_images/1_fd57ab2218734e0e91a35bc210cb2f81.gif new file mode 100644 index 0000000000000000000000000000000000000000..71aede03087e88f8e47af526b1bcb71adb687192 Binary files /dev/null and b/app/uploads/listing_images/1_fd57ab2218734e0e91a35bc210cb2f81.gif differ