From b8564482470329ab4937032883668d2fd1d41f72 Mon Sep 17 00:00:00 2001 From: Nathan Woodburn Date: Mon, 26 May 2025 16:56:53 +1000 Subject: [PATCH] feat: Add map to make site easier to use --- db.py | 8 +- server.py | 53 +++--- templates/assets/css/index.css | 140 ++++++++++++++++ templates/index.html | 298 ++++++++++++++++++++++++++++++--- 4 files changed, 451 insertions(+), 48 deletions(-) diff --git a/db.py b/db.py index c906a60..a52d4ff 100644 --- a/db.py +++ b/db.py @@ -24,11 +24,12 @@ def load_locations(): except (json.JSONDecodeError, FileNotFoundError): return [] -def save_location(name, description="", rating=None): +def save_location(latitude, longitude, description="", rating=None): """Save a new kite location to the database Args: - name (str): Name of the kite flying location + latitude (float): Latitude coordinate + longitude (float): Longitude coordinate description (str, optional): Description of the location rating (int, optional): Rating from 1-5 stars @@ -39,7 +40,8 @@ def save_location(name, description="", rating=None): # Create new location entry new_location = { - "name": name, + "latitude": float(latitude), + "longitude": float(longitude), "description": description, "rating": rating, "date_added": datetime.now().isoformat() diff --git a/server.py b/server.py index 328b1a1..50cf457 100644 --- a/server.py +++ b/server.py @@ -13,7 +13,7 @@ from flask import ( import os import json import requests -from datetime import datetime +from datetime import datetime, timedelta import dotenv import db # Import our new db module import re @@ -147,43 +147,57 @@ def index(): # Kite Watch API Routes @app.route("/api/locations", methods=["GET"]) def get_locations(): - locations = db.load_locations() - return jsonify(locations) + # Get all locations from DB + all_locations = db.load_locations() + + # Check if we should filter by time (last 48 hours) + cutoff_time = datetime.now() - timedelta(hours=48) + + # Filter locations by time + recent_locations = [ + loc for loc in all_locations + if datetime.fromisoformat(loc["date_added"]) > cutoff_time + ] + + return jsonify(recent_locations) @app.route("/api/locations", methods=["POST"]) def add_location(): data = request.json - if not data or "name" not in data: - return jsonify({"error": "Name is required"}), 400 + if not data: + return jsonify({"error": "Invalid request data"}), 400 - name = data.get("name") + latitude = data.get("latitude") + longitude = data.get("longitude") description = data.get("description", "") rating = data.get("rating") # Validate required fields - if len(name.strip()) < 5: - return jsonify({"error": "Location name must be at least 5 characters long"}), 400 - if len(name.strip()) > 100: - return jsonify({"error": "Location name is too long (max 100 characters)"}), 400 + if latitude is None or longitude is None: + return jsonify({"error": "Latitude and longitude are required"}), 400 + + try: + latitude = float(latitude) + longitude = float(longitude) + except (ValueError, TypeError): + return jsonify({"error": "Latitude and longitude must be valid numbers"}), 400 + + # Validate coordinate ranges + if not (-90 <= latitude <= 90) or not (-180 <= longitude <= 180): + return jsonify({"error": "Invalid coordinate values"}), 400 + if len(description.strip()) > 2000: return jsonify({"error": "Notes are too long (max 2000 characters)"}), 400 - if not name.strip(): - return jsonify({"error": "Location name cannot be empty"}), 400 - # Check for profanity - if contains_profanity(name): - return jsonify({"error": "Location name contains inappropriate language"}), 400 - if contains_profanity(description): return jsonify({"error": "Notes contain inappropriate language"}), 400 # Validate for JSON safety - if not is_json_safe(name) or not is_json_safe(description): + if not is_json_safe(description): return jsonify({"error": "Input contains invalid characters"}), 400 # Sanitize inputs - name = sanitize_text_input(name) description = sanitize_text_input(description) # Validate rating @@ -197,7 +211,8 @@ def add_location(): else: return jsonify({"error": "Rating is required"}), 400 - location = db.save_location(name, description, rating) + # Save to database + location = db.save_location(latitude, longitude, description, rating) return jsonify(location), 201 @app.route("/") diff --git a/templates/assets/css/index.css b/templates/assets/css/index.css index 0ce6eb0..0005d94 100644 --- a/templates/assets/css/index.css +++ b/templates/assets/css/index.css @@ -46,6 +46,7 @@ h2 { padding: 1.5rem; margin-bottom: 2rem; box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); + overflow: visible; } .form-group { @@ -293,4 +294,143 @@ a:hover { .footer a:hover { text-decoration: underline; +} + +/* Map styling */ +#map-container { + height: 500px; /* Increased height for better resolution */ + width: 100%; + margin-bottom: 20px; + border-radius: 8px; + border: 1px solid #333; + z-index: 1; + /* Improved rendering for high-DPI displays */ + image-rendering: -webkit-optimize-contrast; + image-rendering: crisp-edges; + backface-visibility: hidden; +} + +/* Fix for Leaflet tiles to render at higher resolution */ +.leaflet-container { + image-rendering: high-quality; +} + +.leaflet-retina .leaflet-tile { + box-shadow: none !important; +} + +/* Custom marker styling */ +.custom-marker { + text-align: center; +} + +.marker-pin { + width: 20px; + height: 20px; + border-radius: 50%; + background-color: #ff0000; + border: 2px solid #fff; + box-shadow: 0 0 10px rgba(0, 0, 0, 0.5); + cursor: pointer; +} + +/* Coordinates input styling */ +.location-controls { + display: flex; + flex-direction: column; + gap: 10px; +} + +.coordinates-group { + display: flex; + gap: 10px; +} + +.coordinates-group input { + flex: 1; +} + +#get-location-btn { + background-color: #333; + color: #fff; + border: 1px solid #444; + padding: 8px; + cursor: pointer; +} + +#get-location-btn:hover { + background-color: #444; +} + +/* Popup styling */ +.leaflet-popup-content-wrapper { + background-color: #222; + color: #fff; + border-radius: 5px; +} + +.leaflet-popup-content { + margin: 10px; +} + +.leaflet-popup-tip { + background-color: #222; +} + +.popup-content { + padding: 5px; +} + +.popup-content .rating { + font-weight: bold; + display: flex; + align-items: center; + margin-top: 0; + color: #ffd700; +} + +.popup-content .date { + font-size: 0.8em; + color: #aaa; + margin-bottom: 0; +} + +/* Modifications to existing styles to accommodate map */ +.card { + /* ...existing code... */ + overflow: visible; +} + +@media (max-width: 600px) { + /* ...existing code... */ + + .coordinates-group { + flex-direction: column; + } + + #map-container { + height: 300px; + } +} + +/* Hide the coordinate inputs */ +.hidden { + display: none !important; +} + +/* Input map styling */ +#input-map-container { + height: 300px; + width: 100%; + margin-bottom: 10px; + border-radius: 8px; + border: 1px solid #333; + z-index: 1; +} + +.location-help { + margin: 10px 0; + font-size: 0.9rem; + color: #adb5bd; + text-align: center; } \ No newline at end of file diff --git a/templates/index.html b/templates/index.html index 4226f71..1710c22 100644 --- a/templates/index.html +++ b/templates/index.html @@ -7,6 +7,12 @@ Kite Watch + + + + @@ -19,10 +25,17 @@

Add a Kite Flying Location

-
+
- - + +
+
+ + + + +

Click on the map or use the button above to set your location

+
@@ -40,7 +53,8 @@
-

Great Places to Fly Kites

+

Great Places to Fly Kites in Canberra

+

Loading locations...

@@ -54,28 +68,218 @@