generated from nathanwoodburn/python-webserver-template
feat: Initial prototype
All checks were successful
Build Docker / BuildImage (push) Successful in 36s
All checks were successful
Build Docker / BuildImage (push) Successful in 36s
This commit is contained in:
55
db.py
Normal file
55
db.py
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
import os
|
||||||
|
import json
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
DB_FILE = "kite_locations.json"
|
||||||
|
|
||||||
|
def get_db_path():
|
||||||
|
"""Get the path to the database file"""
|
||||||
|
# Check if there's a data directory we should use
|
||||||
|
if os.path.isdir("/data"):
|
||||||
|
return os.path.join("/data", DB_FILE)
|
||||||
|
return os.path.join(os.path.dirname(__file__), DB_FILE)
|
||||||
|
|
||||||
|
def load_locations():
|
||||||
|
"""Load all kite locations from the database"""
|
||||||
|
db_path = get_db_path()
|
||||||
|
|
||||||
|
if not os.path.exists(db_path):
|
||||||
|
return []
|
||||||
|
|
||||||
|
try:
|
||||||
|
with open(db_path, "r") as f:
|
||||||
|
return json.load(f)
|
||||||
|
except (json.JSONDecodeError, FileNotFoundError):
|
||||||
|
return []
|
||||||
|
|
||||||
|
def save_location(name, description="", rating=None):
|
||||||
|
"""Save a new kite location to the database
|
||||||
|
|
||||||
|
Args:
|
||||||
|
name (str): Name of the kite flying location
|
||||||
|
description (str, optional): Description of the location
|
||||||
|
rating (int, optional): Rating from 1-5 stars
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict: The saved location object
|
||||||
|
"""
|
||||||
|
locations = load_locations()
|
||||||
|
|
||||||
|
# Create new location entry
|
||||||
|
new_location = {
|
||||||
|
"name": name,
|
||||||
|
"description": description,
|
||||||
|
"rating": rating,
|
||||||
|
"date_added": datetime.now().isoformat()
|
||||||
|
}
|
||||||
|
|
||||||
|
# Add to list and save
|
||||||
|
locations.append(new_location)
|
||||||
|
|
||||||
|
db_path = get_db_path()
|
||||||
|
with open(db_path, "w") as f:
|
||||||
|
json.dump(locations, f, indent=2)
|
||||||
|
|
||||||
|
return new_location
|
||||||
37
kite_locations.json
Normal file
37
kite_locations.json
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"name": "Arboretum",
|
||||||
|
"description": "Lots of wind",
|
||||||
|
"date_added": "2025-05-26T11:56:29.508930"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Park",
|
||||||
|
"description": "Still but still works",
|
||||||
|
"rating": 3,
|
||||||
|
"date_added": "2025-05-26T12:02:15.338125"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "The Lake",
|
||||||
|
"description": "",
|
||||||
|
"rating": 5,
|
||||||
|
"date_added": "2025-05-26T12:02:42.853429"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Test",
|
||||||
|
"description": "Test",
|
||||||
|
"rating": 3,
|
||||||
|
"date_added": "2025-05-26T12:16:19.288452"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Test",
|
||||||
|
"description": "",
|
||||||
|
"rating": 3,
|
||||||
|
"date_added": "2025-05-26T12:18:16.435249"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": " ",
|
||||||
|
"description": "Hello",
|
||||||
|
"rating": 3,
|
||||||
|
"date_added": "2025-05-26T12:18:29.691415"
|
||||||
|
}
|
||||||
|
]
|
||||||
134
server.py
134
server.py
@@ -1,4 +1,4 @@
|
|||||||
from functools import cache
|
from functools import cache, lru_cache
|
||||||
import json
|
import json
|
||||||
from flask import (
|
from flask import (
|
||||||
Flask,
|
Flask,
|
||||||
@@ -15,17 +15,87 @@ import json
|
|||||||
import requests
|
import requests
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
import dotenv
|
import dotenv
|
||||||
|
import db # Import our new db module
|
||||||
|
import re
|
||||||
|
|
||||||
dotenv.load_dotenv()
|
dotenv.load_dotenv()
|
||||||
|
|
||||||
app = Flask(__name__)
|
app = Flask(__name__)
|
||||||
|
|
||||||
|
# Use PurgoMalum API for profanity checking
|
||||||
|
PROFANITY_API_URL = "https://vector.profanity.dev"
|
||||||
|
|
||||||
|
|
||||||
|
@lru_cache(maxsize=1000) # Cache results to avoid repeated API calls
|
||||||
|
def contains_profanity(text):
|
||||||
|
"""Check if text contains profanity using the PurgoMalum API"""
|
||||||
|
if not text:
|
||||||
|
return False
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Call the API
|
||||||
|
response = requests.post(
|
||||||
|
f"{PROFANITY_API_URL}",
|
||||||
|
json={"message": text},
|
||||||
|
timeout=5 # Set a timeout for the request
|
||||||
|
)
|
||||||
|
|
||||||
|
# Check if the API returned success
|
||||||
|
if response.status_code == 200:
|
||||||
|
data = response.json()
|
||||||
|
# Check if the response indicates profanity
|
||||||
|
return data.get("isProfanity", False)
|
||||||
|
|
||||||
|
# Fall back to a basic check if API fails
|
||||||
|
print(f"Warning: Profanity API returned status code {response.status_code}")
|
||||||
|
return False
|
||||||
|
except requests.RequestException as e:
|
||||||
|
# Handle timeouts, connection errors, etc.
|
||||||
|
print(f"Warning: Profanity API request failed: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def is_json_safe(text):
|
||||||
|
"""Ensure text is safe for JSON serialization"""
|
||||||
|
if not text:
|
||||||
|
return True
|
||||||
|
|
||||||
|
# Check for control characters that could break JSON
|
||||||
|
if re.search(r"[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]", text):
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Check if text would be valid in JSON (simplified)
|
||||||
|
try:
|
||||||
|
# Test serialize to ensure valid JSON characters
|
||||||
|
json.dumps(text)
|
||||||
|
return True
|
||||||
|
except:
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def sanitize_text_input(text):
|
||||||
|
"""Sanitize text for JSON storage"""
|
||||||
|
if not text:
|
||||||
|
return ""
|
||||||
|
|
||||||
|
# Replace problematic characters for JSON
|
||||||
|
# This is a simplified approach - more complex sanitization could be added
|
||||||
|
text = text.replace("\0", "").strip()
|
||||||
|
|
||||||
|
# Limit length to prevent abuse
|
||||||
|
max_length = 500
|
||||||
|
if len(text) > max_length:
|
||||||
|
text = text[:max_length]
|
||||||
|
|
||||||
|
return text
|
||||||
|
|
||||||
|
|
||||||
def find(name, path):
|
def find(name, path):
|
||||||
for root, dirs, files in os.walk(path):
|
for root, dirs, files in os.walk(path):
|
||||||
if name in files:
|
if name in files:
|
||||||
return os.path.join(root, name)
|
return os.path.join(root, name)
|
||||||
|
|
||||||
|
|
||||||
# Assets routes
|
# Assets routes
|
||||||
@app.route("/assets/<path:path>")
|
@app.route("/assets/<path:path>")
|
||||||
def send_assets(path):
|
def send_assets(path):
|
||||||
@@ -66,8 +136,6 @@ def wellknown(path):
|
|||||||
return make_response(
|
return make_response(
|
||||||
req.content, 200, {"Content-Type": req.headers["Content-Type"]}
|
req.content, 200, {"Content-Type": req.headers["Content-Type"]}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
# endregion
|
# endregion
|
||||||
|
|
||||||
|
|
||||||
@@ -76,6 +144,61 @@ def wellknown(path):
|
|||||||
def index():
|
def index():
|
||||||
return render_template("index.html")
|
return render_template("index.html")
|
||||||
|
|
||||||
|
# Kite Watch API Routes
|
||||||
|
@app.route("/api/locations", methods=["GET"])
|
||||||
|
def get_locations():
|
||||||
|
locations = db.load_locations()
|
||||||
|
return jsonify(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
|
||||||
|
|
||||||
|
name = data.get("name")
|
||||||
|
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 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):
|
||||||
|
return jsonify({"error": "Input contains invalid characters"}), 400
|
||||||
|
|
||||||
|
# Sanitize inputs
|
||||||
|
name = sanitize_text_input(name)
|
||||||
|
description = sanitize_text_input(description)
|
||||||
|
|
||||||
|
# Validate rating
|
||||||
|
if rating is not None:
|
||||||
|
try:
|
||||||
|
rating = int(rating)
|
||||||
|
if not (1 <= rating <= 5):
|
||||||
|
return jsonify({"error": "Rating must be between 1 and 5"}), 400
|
||||||
|
except ValueError:
|
||||||
|
return jsonify({"error": "Rating must be a number"}), 400
|
||||||
|
else:
|
||||||
|
return jsonify({"error": "Rating is required"}), 400
|
||||||
|
|
||||||
|
location = db.save_location(name, description, rating)
|
||||||
|
return jsonify(location), 201
|
||||||
|
|
||||||
@app.route("/<path:path>")
|
@app.route("/<path:path>")
|
||||||
def catch_all(path: str):
|
def catch_all(path: str):
|
||||||
@@ -97,8 +220,6 @@ def catch_all(path: str):
|
|||||||
return send_file(filename)
|
return send_file(filename)
|
||||||
|
|
||||||
return render_template("404.html"), 404
|
return render_template("404.html"), 404
|
||||||
|
|
||||||
|
|
||||||
# endregion
|
# endregion
|
||||||
|
|
||||||
|
|
||||||
@@ -107,8 +228,7 @@ def catch_all(path: str):
|
|||||||
@app.errorhandler(404)
|
@app.errorhandler(404)
|
||||||
def not_found(e):
|
def not_found(e):
|
||||||
return render_template("404.html"), 404
|
return render_template("404.html"), 404
|
||||||
|
|
||||||
|
|
||||||
# endregion
|
# endregion
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
app.run(debug=True, port=5000, host="0.0.0.0")
|
app.run(debug=True, port=5000, host="0.0.0.0")
|
||||||
|
|||||||
@@ -1,20 +1,146 @@
|
|||||||
body {
|
body {
|
||||||
background-color: #000000;
|
background-color: #000000;
|
||||||
color: #ffffff;
|
color: #ffffff;
|
||||||
|
font-family: Arial, sans-serif;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
line-height: 1.6;
|
||||||
}
|
}
|
||||||
|
|
||||||
h1 {
|
h1 {
|
||||||
font-size: 50px;
|
font-size: 50px;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
margin-top: 0;
|
||||||
|
color: #4dabf7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
background-color: #111;
|
||||||
|
padding: 2rem 0;
|
||||||
|
text-align: center;
|
||||||
|
border-bottom: 3px solid #4dabf7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tagline {
|
||||||
|
color: #adb5bd;
|
||||||
|
font-size: 1.2rem;
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
max-width: 800px;
|
||||||
|
margin: 2rem auto;
|
||||||
|
padding: 0 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
background-color: #111;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 1.5rem;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group {
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
label {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
input, textarea {
|
||||||
|
width: 100%;
|
||||||
|
padding: 0.75rem;
|
||||||
|
background-color: #222;
|
||||||
|
border: 1px solid #444;
|
||||||
|
border-radius: 4px;
|
||||||
|
color: #fff;
|
||||||
|
font-size: 1rem;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
textarea {
|
||||||
|
min-height: 100px;
|
||||||
|
resize: vertical;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
background-color: #4dabf7;
|
||||||
|
color: #000;
|
||||||
|
border: none;
|
||||||
|
padding: 0.75rem 1.5rem;
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: bold;
|
||||||
|
cursor: pointer;
|
||||||
|
border-radius: 4px;
|
||||||
|
transition: background-color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
button:hover {
|
||||||
|
background-color: #74c0fc;
|
||||||
|
}
|
||||||
|
|
||||||
|
button:disabled {
|
||||||
|
background-color: #495057;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.location-item {
|
||||||
|
border-bottom: 1px solid #333;
|
||||||
|
padding: 1rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.location-item:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.location-item h3 {
|
||||||
|
margin: 0 0 0.5rem 0;
|
||||||
|
color: #4dabf7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.date {
|
||||||
|
color: #adb5bd;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading {
|
||||||
|
text-align: center;
|
||||||
|
color: #adb5bd;
|
||||||
|
}
|
||||||
|
|
||||||
.centre {
|
.centre {
|
||||||
margin-top: 10%;
|
margin-top: 10%;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
a {
|
a {
|
||||||
color: #ffffff;
|
color: #4dabf7;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
a:hover {
|
a:hover {
|
||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Responsive adjustments */
|
||||||
|
@media (max-width: 600px) {
|
||||||
|
h1 {
|
||||||
|
font-size: 36px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
padding: 0 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,16 +4,213 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>Nathan.Woodburn/</title>
|
<title>Kite Watch - Nathan.Woodburn/</title>
|
||||||
<link rel="icon" href="/assets/img/favicon.png" type="image/png">
|
<link rel="icon" href="/assets/img/favicon.png" type="image/png">
|
||||||
<link rel="stylesheet" href="/assets/css/index.css">
|
<link rel="stylesheet" href="/assets/css/index.css">
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
<div class="spacer"></div>
|
<div class="header">
|
||||||
<div class="centre">
|
<h1>Kite Watch</h1>
|
||||||
<h1>Nathan.Woodburn/</h1>
|
<p class="tagline">Find perfect places to fly your kite!</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="container">
|
||||||
|
<div class="card">
|
||||||
|
<h2>Add a Kite Flying Location</h2>
|
||||||
|
<div id="notification" class="notification hidden"></div>
|
||||||
|
<form id="location-form">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="location-name">Location Name *</label>
|
||||||
|
<input type="text" id="location-name" required placeholder="Arboretum, Weston Park, etc.">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="location-description">Notes</label>
|
||||||
|
<textarea id="location-description" placeholder="Wind conditions, accessibility, tips, etc."></textarea>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="rating">Rating (1-5):</label>
|
||||||
|
<div class="rating-container">
|
||||||
|
<input type="range" id="rating" name="rating" min="1" max="5" value="3" step="1" class="form-control">
|
||||||
|
<span id="rating-value">3</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button type="submit" id="submit-button">Submit Location</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<h2>Great Places to Fly Kites</h2>
|
||||||
|
<div id="locations-list">
|
||||||
|
<p class="loading">Loading locations...</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// Load locations when the page loads
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
loadLocations();
|
||||||
|
|
||||||
|
// Handle form submission
|
||||||
|
document.getElementById('location-form').addEventListener('submit', (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
const name = document.getElementById('location-name').value;
|
||||||
|
const description = document.getElementById('location-description').value;
|
||||||
|
const rating = parseInt(document.getElementById('rating').value);
|
||||||
|
|
||||||
|
if (!name) {
|
||||||
|
showNotification('Please enter a location name', 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Submit the location
|
||||||
|
submitLocation(name, description, rating);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Function to show notifications instead of alerts
|
||||||
|
function showNotification(message, type = 'success') {
|
||||||
|
const notification = document.getElementById('notification');
|
||||||
|
notification.textContent = message;
|
||||||
|
notification.className = `notification ${type}`;
|
||||||
|
|
||||||
|
// Auto hide after 5 seconds
|
||||||
|
setTimeout(() => {
|
||||||
|
notification.className = 'notification hidden';
|
||||||
|
}, 5000);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Function to load locations from the API
|
||||||
|
function loadLocations() {
|
||||||
|
const locationsList = document.getElementById('locations-list');
|
||||||
|
|
||||||
|
fetch('/api/locations')
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(locations => {
|
||||||
|
locationsList.innerHTML = '';
|
||||||
|
|
||||||
|
if (locations.length === 0) {
|
||||||
|
locationsList.innerHTML = '<p>No locations added yet. Be the first!</p>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort locations with newest first
|
||||||
|
locations.sort((a, b) => new Date(b.date_added) - new Date(a.date_added));
|
||||||
|
|
||||||
|
locations.forEach(location => {
|
||||||
|
const date = new Date(location.date_added);
|
||||||
|
const formattedDate = date.toLocaleDateString() + ' ' + date.toLocaleTimeString();
|
||||||
|
|
||||||
|
const locationElement = document.createElement('div');
|
||||||
|
locationElement.className = 'location-item';
|
||||||
|
locationElement.innerHTML = `
|
||||||
|
<h3>${location.name}</h3>
|
||||||
|
<p>${location.description || 'No notes provided'}</p>
|
||||||
|
<p class="date">Added on: ${formattedDate}</p>
|
||||||
|
<p class="rating">Rating: ${location.rating}</p>
|
||||||
|
`;
|
||||||
|
|
||||||
|
locationsList.appendChild(locationElement);
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('Error loading locations:', error);
|
||||||
|
locationsList.innerHTML = '<p>Error loading locations. Please try again.</p>';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Function to submit a new location
|
||||||
|
function submitLocation(name, description, rating) {
|
||||||
|
const submitButton = document.getElementById('submit-button');
|
||||||
|
submitButton.disabled = true;
|
||||||
|
submitButton.textContent = 'Submitting...';
|
||||||
|
|
||||||
|
fetch('/api/locations', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ name, description, rating })
|
||||||
|
})
|
||||||
|
.then(response => {
|
||||||
|
if (!response.ok) {
|
||||||
|
// Parse the error response to get the specific error message
|
||||||
|
return response.json().then(errorData => {
|
||||||
|
throw new Error(errorData.error || 'Failed to submit location');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return response.json();
|
||||||
|
})
|
||||||
|
.then(data => {
|
||||||
|
// Clear form
|
||||||
|
document.getElementById('location-name').value = '';
|
||||||
|
document.getElementById('location-description').value = '';
|
||||||
|
document.getElementById('rating').value = 3;
|
||||||
|
document.getElementById('rating-value').textContent = 3;
|
||||||
|
|
||||||
|
// Reload locations
|
||||||
|
loadLocations();
|
||||||
|
showNotification('Location added successfully!');
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('Error submitting location:', error);
|
||||||
|
// Display the specific error message from the server
|
||||||
|
showNotification(error.message || 'Failed to add location. Please try again.', 'error');
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
submitButton.disabled = false;
|
||||||
|
submitButton.textContent = 'Submit Location';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
document.getElementById('rating').addEventListener('input', function() {
|
||||||
|
document.getElementById('rating-value').textContent = this.value;
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.rating-container {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
#rating {
|
||||||
|
flex: 1;
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#rating-value {
|
||||||
|
font-weight: bold;
|
||||||
|
width: 20px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Notification styling */
|
||||||
|
.notification {
|
||||||
|
padding: 10px 15px;
|
||||||
|
border-radius: 4px;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
transition: opacity 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification.success {
|
||||||
|
background-color: #d4edda;
|
||||||
|
color: #155724;
|
||||||
|
border: 1px solid #c3e6cb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification.error {
|
||||||
|
background-color: #f8d7da;
|
||||||
|
color: #721c24;
|
||||||
|
border: 1px solid #f5c6cb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification.hidden {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
Reference in New Issue
Block a user