Compare commits
No commits in common. "e3724a48426295eafb5d8071f9ede52ab5d1d949" and "bcc86be6c4d07cb7fec9c8983310653f9cbf27ce" have entirely different histories.
e3724a4842
...
bcc86be6c4
496
backend/app.py
496
backend/app.py
@ -1,403 +1,219 @@
|
|||||||
from flask import Flask, request, jsonify, session
|
from flask import Flask, request, jsonify
|
||||||
from flask_cors import CORS
|
from flask_cors import CORS
|
||||||
from werkzeug.security import generate_password_hash, check_password_hash
|
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
app = Flask(__name__)
|
app = Flask(__name__)
|
||||||
app.secret_key = "your-secret-key-here-change-in-production" # needed for sessions
|
CORS(app) # Enable CORS for all routes
|
||||||
CORS(app, supports_credentials=True) # allow cookies
|
|
||||||
|
|
||||||
# Directories
|
# Data directory and file
|
||||||
DATA_DIR = "data"
|
DATA_DIR = 'data'
|
||||||
USERS_FILE = os.path.join(DATA_DIR, "users.json")
|
DATA_FILE = os.path.join(DATA_DIR, 'journeys.json')
|
||||||
os.makedirs(DATA_DIR, exist_ok=True)
|
os.makedirs(DATA_DIR, exist_ok=True)
|
||||||
|
|
||||||
|
# In-memory store (loaded from file on startup)
|
||||||
|
journeys = []
|
||||||
|
|
||||||
# ==================== User helpers ====================
|
def load_journeys():
|
||||||
def load_users():
|
"""Load journeys from the JSON file."""
|
||||||
|
global journeys
|
||||||
try:
|
try:
|
||||||
if os.path.exists(USERS_FILE):
|
if os.path.exists(DATA_FILE):
|
||||||
with open(USERS_FILE, "r") as f:
|
with open(DATA_FILE, 'r') as f:
|
||||||
return json.load(f)
|
journeys = json.load(f)
|
||||||
|
else:
|
||||||
|
journeys = []
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Error loading users: {e}")
|
print(f"Error loading journeys: {e}")
|
||||||
return []
|
journeys = []
|
||||||
|
|
||||||
|
def save_journeys():
|
||||||
def save_users(users):
|
"""Save journeys to the JSON file."""
|
||||||
try:
|
try:
|
||||||
with open(USERS_FILE, "w") as f:
|
with open(DATA_FILE, 'w') as f:
|
||||||
json.dump(users, f, indent=2)
|
|
||||||
except Exception as e:
|
|
||||||
print(f"Error saving users: {e}")
|
|
||||||
|
|
||||||
|
|
||||||
def get_next_user_id(users):
|
|
||||||
if not users:
|
|
||||||
return 1
|
|
||||||
return max(u["id"] for u in users) + 1
|
|
||||||
|
|
||||||
|
|
||||||
def get_user_by_username(username):
|
|
||||||
users = load_users()
|
|
||||||
return next((u for u in users if u["username"] == username), None)
|
|
||||||
|
|
||||||
|
|
||||||
def get_user_by_id(user_id):
|
|
||||||
users = load_users()
|
|
||||||
return next((u for u in users if u["id"] == user_id), None)
|
|
||||||
|
|
||||||
|
|
||||||
# ==================== Per‑user data helpers ====================
|
|
||||||
def get_user_data_dir(user_id):
|
|
||||||
user_dir = os.path.join(DATA_DIR, "users", str(user_id))
|
|
||||||
os.makedirs(user_dir, exist_ok=True)
|
|
||||||
return user_dir
|
|
||||||
|
|
||||||
|
|
||||||
def load_user_journeys(user_id):
|
|
||||||
file_path = os.path.join(get_user_data_dir(user_id), "journeys.json")
|
|
||||||
try:
|
|
||||||
if os.path.exists(file_path):
|
|
||||||
with open(file_path, "r") as f:
|
|
||||||
return json.load(f)
|
|
||||||
except Exception as e:
|
|
||||||
print(f"Error loading journeys for user {user_id}: {e}")
|
|
||||||
return []
|
|
||||||
|
|
||||||
|
|
||||||
def save_user_journeys(user_id, journeys):
|
|
||||||
file_path = os.path.join(get_user_data_dir(user_id), "journeys.json")
|
|
||||||
try:
|
|
||||||
with open(file_path, "w") as f:
|
|
||||||
json.dump(journeys, f, indent=2)
|
json.dump(journeys, f, indent=2)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Error saving journeys for user {user_id}: {e}")
|
print(f"Error saving journeys: {e}")
|
||||||
|
|
||||||
|
# Load existing journeys on startup
|
||||||
|
load_journeys()
|
||||||
|
|
||||||
def load_user_posts(user_id):
|
def get_next_id():
|
||||||
file_path = os.path.join(get_user_data_dir(user_id), "posts.json")
|
"""Return the next available ID (simple integer increment)."""
|
||||||
try:
|
|
||||||
if os.path.exists(file_path):
|
|
||||||
with open(file_path, "r") as f:
|
|
||||||
return json.load(f)
|
|
||||||
except Exception as e:
|
|
||||||
print(f"Error loading posts for user {user_id}: {e}")
|
|
||||||
return []
|
|
||||||
|
|
||||||
|
|
||||||
def save_user_posts(user_id, posts):
|
|
||||||
file_path = os.path.join(get_user_data_dir(user_id), "posts.json")
|
|
||||||
try:
|
|
||||||
with open(file_path, "w") as f:
|
|
||||||
json.dump(posts, f, indent=2)
|
|
||||||
except Exception as e:
|
|
||||||
print(f"Error saving posts for user {user_id}: {e}")
|
|
||||||
|
|
||||||
|
|
||||||
# ==================== Authentication endpoints ====================
|
|
||||||
@app.route("/api/register", methods=["POST"])
|
|
||||||
def register():
|
|
||||||
data = request.get_json()
|
|
||||||
username = data.get("username")
|
|
||||||
password = data.get("password")
|
|
||||||
|
|
||||||
if not username or not password:
|
|
||||||
return jsonify({"error": "Username and password required"}), 400
|
|
||||||
|
|
||||||
# Check if username already exists
|
|
||||||
if get_user_by_username(username):
|
|
||||||
return jsonify({"error": "Username already taken"}), 409
|
|
||||||
|
|
||||||
users = load_users()
|
|
||||||
new_id = get_next_user_id(users)
|
|
||||||
|
|
||||||
hashed = generate_password_hash(password)
|
|
||||||
new_user = {
|
|
||||||
"id": new_id,
|
|
||||||
"username": username,
|
|
||||||
"password_hash": hashed,
|
|
||||||
"created_at": datetime.now().isoformat(),
|
|
||||||
}
|
|
||||||
users.append(new_user)
|
|
||||||
save_users(users)
|
|
||||||
|
|
||||||
# Create empty data files for the new user
|
|
||||||
save_user_journeys(new_id, [])
|
|
||||||
save_user_posts(new_id, [])
|
|
||||||
|
|
||||||
# Log the user in automatically
|
|
||||||
session["user_id"] = new_id
|
|
||||||
|
|
||||||
return jsonify(
|
|
||||||
{"id": new_id, "username": username, "message": "Registration successful"}
|
|
||||||
), 201
|
|
||||||
|
|
||||||
|
|
||||||
@app.route("/api/login", methods=["POST"])
|
|
||||||
def login():
|
|
||||||
data = request.get_json()
|
|
||||||
username = data.get("username")
|
|
||||||
password = data.get("password")
|
|
||||||
|
|
||||||
user = get_user_by_username(username)
|
|
||||||
if not user or not check_password_hash(user["password_hash"], password):
|
|
||||||
return jsonify({"error": "Invalid username or password"}), 401
|
|
||||||
|
|
||||||
session["user_id"] = user["id"]
|
|
||||||
return jsonify(
|
|
||||||
{"id": user["id"], "username": user["username"], "message": "Login successful"}
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@app.route("/api/logout", methods=["POST"])
|
|
||||||
def logout():
|
|
||||||
session.pop("user_id", None)
|
|
||||||
return jsonify({"message": "Logged out"})
|
|
||||||
|
|
||||||
|
|
||||||
@app.route("/api/me", methods=["GET"])
|
|
||||||
def me():
|
|
||||||
user_id = session.get("user_id")
|
|
||||||
if not user_id:
|
|
||||||
return jsonify({"error": "Not logged in"}), 401
|
|
||||||
user = get_user_by_id(user_id)
|
|
||||||
if not user:
|
|
||||||
# Should not happen, but clean session
|
|
||||||
session.pop("user_id", None)
|
|
||||||
return jsonify({"error": "User not found"}), 401
|
|
||||||
return jsonify({"id": user["id"], "username": user["username"]})
|
|
||||||
|
|
||||||
|
|
||||||
# ==================== Journey endpoints (protected, user‑specific) ====================
|
|
||||||
def require_login():
|
|
||||||
if "user_id" not in session:
|
|
||||||
return False
|
|
||||||
return True
|
|
||||||
|
|
||||||
|
|
||||||
def get_current_user_id():
|
|
||||||
return session.get("user_id")
|
|
||||||
|
|
||||||
|
|
||||||
def get_journeys_for_current_user():
|
|
||||||
user_id = get_current_user_id()
|
|
||||||
if user_id is None:
|
|
||||||
return None
|
|
||||||
return load_user_journeys(user_id)
|
|
||||||
|
|
||||||
|
|
||||||
@app.route("/api/journeys", methods=["GET"])
|
|
||||||
def get_journeys():
|
|
||||||
if not require_login():
|
|
||||||
return jsonify({"error": "Authentication required"}), 401
|
|
||||||
journeys = get_journeys_for_current_user()
|
|
||||||
return jsonify(journeys)
|
|
||||||
|
|
||||||
|
|
||||||
def get_next_journey_id(journeys):
|
|
||||||
if not journeys:
|
if not journeys:
|
||||||
return 1
|
return 1
|
||||||
return max(j["id"] for j in journeys) + 1
|
return max(j['id'] for j in journeys) + 1
|
||||||
|
|
||||||
|
@app.route('/api/journeys', methods=['POST'])
|
||||||
@app.route("/api/journeys", methods=["POST"])
|
|
||||||
def create_journey():
|
def create_journey():
|
||||||
if not require_login():
|
"""Create a new journey."""
|
||||||
return jsonify({"error": "Authentication required"}), 401
|
|
||||||
data = request.get_json()
|
data = request.get_json()
|
||||||
if not data:
|
if not data:
|
||||||
return jsonify({"error": "No data provided"}), 400
|
return jsonify({'error': 'No data provided'}), 400
|
||||||
|
|
||||||
title = data.get("title")
|
title = data.get('title')
|
||||||
if not title:
|
if not title:
|
||||||
return jsonify({"error": "Journey title is required"}), 400
|
return jsonify({'error': 'Journey title is required'}), 400
|
||||||
|
|
||||||
user_id = get_current_user_id()
|
|
||||||
journeys = get_journeys_for_current_user()
|
|
||||||
new_id = get_next_journey_id(journeys)
|
|
||||||
|
|
||||||
new_journey = {
|
new_journey = {
|
||||||
"id": new_id,
|
'id': get_next_id(),
|
||||||
"title": title,
|
'title': title,
|
||||||
"description": data.get("description", ""),
|
'description': data.get('description', ''),
|
||||||
"markers": data.get("markers", []),
|
'markers': data.get('markers', []), # list of marker objects
|
||||||
"created_at": datetime.now().isoformat(),
|
'created_at': datetime.now().isoformat()
|
||||||
}
|
}
|
||||||
|
|
||||||
journeys.append(new_journey)
|
journeys.append(new_journey)
|
||||||
save_user_journeys(user_id, journeys)
|
save_journeys()
|
||||||
return jsonify(new_journey), 201
|
return jsonify(new_journey), 201
|
||||||
|
|
||||||
|
@app.route('/api/journeys', methods=['GET'])
|
||||||
|
def get_journeys():
|
||||||
|
"""Return all journeys."""
|
||||||
|
return jsonify(journeys)
|
||||||
|
|
||||||
@app.route("/api/journeys/<int:journey_id>", methods=["GET"])
|
@app.route('/api/journeys/<int:journey_id>', methods=['GET'])
|
||||||
def get_journey(journey_id):
|
def get_journey(journey_id):
|
||||||
if not require_login():
|
"""Return a specific journey by ID."""
|
||||||
return jsonify({"error": "Authentication required"}), 401
|
journey = next((j for j in journeys if j['id'] == journey_id), None)
|
||||||
journeys = get_journeys_for_current_user()
|
|
||||||
journey = next((j for j in journeys if j["id"] == journey_id), None)
|
|
||||||
if journey is None:
|
if journey is None:
|
||||||
return jsonify({"error": "Journey not found"}), 404
|
return jsonify({'error': 'Journey not found'}), 404
|
||||||
return jsonify(journey)
|
return jsonify(journey)
|
||||||
|
|
||||||
|
@app.route('/api/journeys/<int:journey_id>', methods=['PUT'])
|
||||||
@app.route("/api/journeys/<int:journey_id>", methods=["PUT"])
|
|
||||||
def update_journey(journey_id):
|
def update_journey(journey_id):
|
||||||
if not require_login():
|
"""Update an existing journey."""
|
||||||
return jsonify({"error": "Authentication required"}), 401
|
journey = next((j for j in journeys if j['id'] == journey_id), None)
|
||||||
journeys = get_journeys_for_current_user()
|
|
||||||
journey = next((j for j in journeys if j["id"] == journey_id), None)
|
|
||||||
if journey is None:
|
if journey is None:
|
||||||
return jsonify({"error": "Journey not found"}), 404
|
return jsonify({'error': 'Journey not found'}), 404
|
||||||
|
|
||||||
data = request.get_json()
|
data = request.get_json()
|
||||||
if not data:
|
if not data:
|
||||||
return jsonify({"error": "No data provided"}), 400
|
return jsonify({'error': 'No data provided'}), 400
|
||||||
|
|
||||||
if "title" in data:
|
# Update allowed fields
|
||||||
journey["title"] = data["title"]
|
if 'title' in data:
|
||||||
if "description" in data:
|
journey['title'] = data['title']
|
||||||
journey["description"] = data["description"]
|
if 'description' in data:
|
||||||
if "markers" in data:
|
journey['description'] = data['description']
|
||||||
journey["markers"] = data["markers"]
|
if 'markers' in data:
|
||||||
|
journey['markers'] = data['markers']
|
||||||
|
|
||||||
save_user_journeys(get_current_user_id(), journeys)
|
save_journeys()
|
||||||
return jsonify(journey)
|
return jsonify(journey)
|
||||||
|
|
||||||
|
@app.route('/api/journeys/<int:journey_id>', methods=['DELETE'])
|
||||||
@app.route("/api/journeys/<int:journey_id>", methods=["DELETE"])
|
|
||||||
def delete_journey(journey_id):
|
def delete_journey(journey_id):
|
||||||
if not require_login():
|
"""Delete a journey."""
|
||||||
return jsonify({"error": "Authentication required"}), 401
|
global journeys
|
||||||
journeys = get_journeys_for_current_user()
|
journey = next((j for j in journeys if j['id'] == journey_id), None)
|
||||||
journey = next((j for j in journeys if j["id"] == journey_id), None)
|
|
||||||
if journey is None:
|
if journey is None:
|
||||||
return jsonify({"error": "Journey not found"}), 404
|
return jsonify({'error': 'Journey not found'}), 404
|
||||||
|
|
||||||
journeys = [j for j in journeys if j["id"] != journey_id]
|
journeys = [j for j in journeys if j['id'] != journey_id]
|
||||||
save_user_journeys(get_current_user_id(), journeys)
|
save_journeys()
|
||||||
return jsonify({"message": "Journey deleted successfully", "journey": journey})
|
return jsonify({'message': 'Journey deleted successfully', 'journey': journey})
|
||||||
|
|
||||||
|
@app.route('/api/journeys/health', methods=['GET'])
|
||||||
# ==================== Blog Post endpoints (protected, user‑specific) ====================
|
|
||||||
def get_posts_for_current_user():
|
|
||||||
user_id = get_current_user_id()
|
|
||||||
if user_id is None:
|
|
||||||
return None
|
|
||||||
return load_user_posts(user_id)
|
|
||||||
|
|
||||||
|
|
||||||
def get_next_post_id(posts):
|
|
||||||
if not posts:
|
|
||||||
return 1
|
|
||||||
return max(p["id"] for p in posts) + 1
|
|
||||||
|
|
||||||
|
|
||||||
@app.route("/api/blog-posts", methods=["GET"])
|
|
||||||
def get_blog_posts():
|
|
||||||
if not require_login():
|
|
||||||
return jsonify({"error": "Authentication required"}), 401
|
|
||||||
posts = get_posts_for_current_user()
|
|
||||||
return jsonify(posts)
|
|
||||||
|
|
||||||
|
|
||||||
@app.route("/api/blog-posts/<int:post_id>", methods=["GET"])
|
|
||||||
def get_blog_post(post_id):
|
|
||||||
if not require_login():
|
|
||||||
return jsonify({"error": "Authentication required"}), 401
|
|
||||||
posts = get_posts_for_current_user()
|
|
||||||
post = next((p for p in posts if p["id"] == post_id), None)
|
|
||||||
if not post:
|
|
||||||
return jsonify({"error": "Post not found"}), 404
|
|
||||||
return jsonify(post)
|
|
||||||
|
|
||||||
|
|
||||||
@app.route("/api/blog-posts", methods=["POST"])
|
|
||||||
def create_blog_post():
|
|
||||||
if not require_login():
|
|
||||||
return jsonify({"error": "Authentication required"}), 401
|
|
||||||
data = request.get_json()
|
|
||||||
title = data.get("title")
|
|
||||||
if not title:
|
|
||||||
return jsonify({"error": "Title required"}), 400
|
|
||||||
|
|
||||||
user_id = get_current_user_id()
|
|
||||||
posts = get_posts_for_current_user()
|
|
||||||
new_id = get_next_post_id(posts)
|
|
||||||
|
|
||||||
new_post = {
|
|
||||||
"id": new_id,
|
|
||||||
"title": title,
|
|
||||||
"content": data.get("content", ""),
|
|
||||||
"journeyId": data.get("journeyId"),
|
|
||||||
"image": data.get("image"),
|
|
||||||
"created_at": datetime.now().isoformat(),
|
|
||||||
}
|
|
||||||
|
|
||||||
posts.append(new_post)
|
|
||||||
save_user_posts(user_id, posts)
|
|
||||||
return jsonify(new_post), 201
|
|
||||||
|
|
||||||
|
|
||||||
@app.route("/api/blog-posts/<int:post_id>", methods=["PUT"])
|
|
||||||
def update_blog_post(post_id):
|
|
||||||
if not require_login():
|
|
||||||
return jsonify({"error": "Authentication required"}), 401
|
|
||||||
posts = get_posts_for_current_user()
|
|
||||||
post = next((p for p in posts if p["id"] == post_id), None)
|
|
||||||
if not post:
|
|
||||||
return jsonify({"error": "Post not found"}), 404
|
|
||||||
|
|
||||||
data = request.get_json()
|
|
||||||
if "title" in data:
|
|
||||||
post["title"] = data["title"]
|
|
||||||
if "content" in data:
|
|
||||||
post["content"] = data["content"]
|
|
||||||
if "journeyId" in data:
|
|
||||||
post["journeyId"] = data["journeyId"]
|
|
||||||
if "image" in data:
|
|
||||||
post["image"] = data["image"]
|
|
||||||
|
|
||||||
save_user_posts(get_current_user_id(), posts)
|
|
||||||
return jsonify(post)
|
|
||||||
|
|
||||||
|
|
||||||
@app.route("/api/blog-posts/<int:post_id>", methods=["DELETE"])
|
|
||||||
def delete_blog_post(post_id):
|
|
||||||
if not require_login():
|
|
||||||
return jsonify({"error": "Authentication required"}), 401
|
|
||||||
posts = get_posts_for_current_user()
|
|
||||||
post = next((p for p in posts if p["id"] == post_id), None)
|
|
||||||
if not post:
|
|
||||||
return jsonify({"error": "Post not found"}), 404
|
|
||||||
|
|
||||||
posts = [p for p in posts if p["id"] != post_id]
|
|
||||||
save_user_posts(get_current_user_id(), posts)
|
|
||||||
return jsonify({"message": "Post deleted"})
|
|
||||||
|
|
||||||
|
|
||||||
# ==================== Health and root ====================
|
|
||||||
@app.route("/api/journeys/health", methods=["GET"])
|
|
||||||
def health_check():
|
def health_check():
|
||||||
return jsonify({"status": "healthy", "timestamp": datetime.now().isoformat()})
|
"""Simple health check endpoint."""
|
||||||
|
return jsonify({'status': 'healthy', 'timestamp': datetime.now().isoformat()})
|
||||||
|
|
||||||
|
@app.route('/')
|
||||||
@app.route("/")
|
|
||||||
def index():
|
def index():
|
||||||
return """
|
"""Root endpoint – just a welcome message."""
|
||||||
|
return '''
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html>
|
<html>
|
||||||
<head><title>Journey Mapper API</title></head>
|
<head><title>Journey Mapper Backend</title></head>
|
||||||
<body>
|
<body>
|
||||||
<h1>Journey Mapper API</h1>
|
<h1>Journey Mapper API</h1>
|
||||||
<p>Backend is running. Use <code>/api/journeys</code> endpoints.</p>
|
<p>Backend is running. Use <code>/api/journeys</code> endpoints.</p>
|
||||||
<p>Authentication required: register, login, then use session cookies.</p>
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
"""
|
'''
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
# Blog posts data file
|
||||||
app.run(debug=True, port=5000)
|
BLOG_DATA_FILE = os.path.join(DATA_DIR, 'blog_posts.json')
|
||||||
|
|
||||||
|
def load_blog_posts():
|
||||||
|
try:
|
||||||
|
if os.path.exists(BLOG_DATA_FILE):
|
||||||
|
with open(BLOG_DATA_FILE, 'r') as f:
|
||||||
|
return json.load(f)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
return []
|
||||||
|
|
||||||
|
def save_blog_posts(posts):
|
||||||
|
with open(BLOG_DATA_FILE, 'w') as f:
|
||||||
|
json.dump(posts, f, indent=2)
|
||||||
|
|
||||||
|
blog_posts = load_blog_posts()
|
||||||
|
|
||||||
|
def get_next_blog_id():
|
||||||
|
if not blog_posts:
|
||||||
|
return 1
|
||||||
|
return max(p['id'] for p in blog_posts) + 1
|
||||||
|
|
||||||
|
@app.route('/api/blog-posts', methods=['GET'])
|
||||||
|
def get_blog_posts():
|
||||||
|
return jsonify(blog_posts)
|
||||||
|
|
||||||
|
@app.route('/api/blog-posts/<int:post_id>', methods=['GET'])
|
||||||
|
def get_blog_post(post_id):
|
||||||
|
post = next((p for p in blog_posts if p['id'] == post_id), None)
|
||||||
|
if not post:
|
||||||
|
return jsonify({'error': 'Post not found'}), 404
|
||||||
|
return jsonify(post)
|
||||||
|
|
||||||
|
@app.route('/api/blog-posts', methods=['POST'])
|
||||||
|
def create_blog_post():
|
||||||
|
data = request.get_json()
|
||||||
|
title = data.get('title')
|
||||||
|
if not title:
|
||||||
|
return jsonify({'error': 'Title required'}), 400
|
||||||
|
new_post = {
|
||||||
|
'id': get_next_blog_id(),
|
||||||
|
'title': title,
|
||||||
|
'content': data.get('content', ''),
|
||||||
|
'journeyId': data.get('journeyId'),
|
||||||
|
'image': data.get('image'),
|
||||||
|
'created_at': datetime.now().isoformat()
|
||||||
|
}
|
||||||
|
blog_posts.append(new_post)
|
||||||
|
save_blog_posts(blog_posts)
|
||||||
|
return jsonify(new_post), 201
|
||||||
|
|
||||||
|
@app.route('/api/blog-posts/<int:post_id>', methods=['PUT'])
|
||||||
|
def update_blog_post(post_id):
|
||||||
|
post = next((p for p in blog_posts if p['id'] == post_id), None)
|
||||||
|
if not post:
|
||||||
|
return jsonify({'error': 'Post not found'}), 404
|
||||||
|
data = request.get_json()
|
||||||
|
if 'title' in data:
|
||||||
|
post['title'] = data['title']
|
||||||
|
if 'content' in data:
|
||||||
|
post['content'] = data['content']
|
||||||
|
if 'journeyId' in data:
|
||||||
|
post['journeyId'] = data['journeyId']
|
||||||
|
if 'image' in data:
|
||||||
|
post['image'] = data['image']
|
||||||
|
save_blog_posts(blog_posts)
|
||||||
|
return jsonify(post)
|
||||||
|
|
||||||
|
@app.route('/api/blog-posts/<int:post_id>', methods=['DELETE'])
|
||||||
|
def delete_blog_post(post_id):
|
||||||
|
global blog_posts
|
||||||
|
post = next((p for p in blog_posts if p['id'] == post_id), None)
|
||||||
|
if not post:
|
||||||
|
return jsonify({'error': 'Post not found'}), 404
|
||||||
|
blog_posts = [p for p in blog_posts if p['id'] != post_id]
|
||||||
|
save_blog_posts(blog_posts)
|
||||||
|
return jsonify({'message': 'Post deleted'})
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
app.run(debug=True, port=5000)
|
||||||
@ -1,8 +0,0 @@
|
|||||||
[
|
|
||||||
{
|
|
||||||
"id": 1,
|
|
||||||
"username": "josh",
|
|
||||||
"password_hash": "scrypt:32768:8:1$HA70PiOwbBrIwlDq$2ab80bdc08bb3bb4214258566aded836062323380491a7f4c7f2e67bdccb8686367789f57b3c6c5eb3e2f08c8c07186f47f9c89d1e72179ddd3758b509f23fbe",
|
|
||||||
"created_at": "2026-03-27T20:32:43.107028"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
@ -1,42 +0,0 @@
|
|||||||
[
|
|
||||||
{
|
|
||||||
"id": 1,
|
|
||||||
"title": "test",
|
|
||||||
"description": "11",
|
|
||||||
"markers": [
|
|
||||||
{
|
|
||||||
"lat": 48.356249029540734,
|
|
||||||
"lng": 4.866943359375,
|
|
||||||
"title": "New Marker",
|
|
||||||
"date": "",
|
|
||||||
"description": "",
|
|
||||||
"videoUrl": ""
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"lat": 46.961510504873104,
|
|
||||||
"lng": 9.371337890625002,
|
|
||||||
"title": "New Marker",
|
|
||||||
"date": "",
|
|
||||||
"description": "",
|
|
||||||
"videoUrl": ""
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"lat": 45.51404592560427,
|
|
||||||
"lng": 11.656494140625002,
|
|
||||||
"title": "New Marker",
|
|
||||||
"date": "",
|
|
||||||
"description": "",
|
|
||||||
"videoUrl": ""
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"lat": 43.52465500687188,
|
|
||||||
"lng": 11.162109375,
|
|
||||||
"title": "New Marker",
|
|
||||||
"date": "",
|
|
||||||
"description": "",
|
|
||||||
"videoUrl": ""
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"created_at": "2026-03-27T21:49:26.885353"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
@ -1,10 +0,0 @@
|
|||||||
[
|
|
||||||
{
|
|
||||||
"id": 1,
|
|
||||||
"title": "test",
|
|
||||||
"content": "ksafladjsfk",
|
|
||||||
"journeyId": "1",
|
|
||||||
"image": null,
|
|
||||||
"created_at": "2026-03-27T21:23:39.755057"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
@ -12,7 +12,7 @@
|
|||||||
<!-- Font Awesome -->
|
<!-- Font Awesome -->
|
||||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
||||||
|
|
||||||
<!-- Google Fonts -->
|
<!-- Google Fonts: Poppins -->
|
||||||
<link href="https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;500;600;700&display=swap" rel="stylesheet">
|
<link href="https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;500;600;700&display=swap" rel="stylesheet">
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
@ -28,7 +28,7 @@
|
|||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Header */
|
/* Header (same as map page) */
|
||||||
.site-header {
|
.site-header {
|
||||||
background: var(--gray-9);
|
background: var(--gray-9);
|
||||||
padding: var(--size-4) var(--size-6);
|
padding: var(--size-4) var(--size-6);
|
||||||
@ -70,31 +70,6 @@
|
|||||||
background: var(--surface-2);
|
background: var(--surface-2);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* User menu */
|
|
||||||
.user-menu {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: var(--size-2);
|
|
||||||
}
|
|
||||||
.user-menu .username {
|
|
||||||
color: var(--gray-2);
|
|
||||||
font-weight: var(--font-weight-5);
|
|
||||||
}
|
|
||||||
.logout-btn {
|
|
||||||
background: var(--indigo-7);
|
|
||||||
color: white;
|
|
||||||
border: none;
|
|
||||||
border-radius: var(--radius-2);
|
|
||||||
padding: var(--size-1) var(--size-3);
|
|
||||||
cursor: pointer;
|
|
||||||
font-size: var(--font-size-1);
|
|
||||||
font-weight: var(--font-weight-5);
|
|
||||||
transition: background 0.2s;
|
|
||||||
}
|
|
||||||
.logout-btn:hover {
|
|
||||||
background: var(--indigo-8);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Main content */
|
/* Main content */
|
||||||
.blog-container {
|
.blog-container {
|
||||||
max-width: 1200px;
|
max-width: 1200px;
|
||||||
@ -182,51 +157,35 @@
|
|||||||
padding: var(--size-8);
|
padding: var(--size-8);
|
||||||
font-style: italic;
|
font-style: italic;
|
||||||
}
|
}
|
||||||
.toast {
|
|
||||||
position: fixed;
|
|
||||||
bottom: var(--size-4);
|
|
||||||
right: var(--size-4);
|
|
||||||
background: var(--green-7);
|
|
||||||
color: white;
|
|
||||||
padding: var(--size-2) var(--size-4);
|
|
||||||
border-radius: var(--radius-2);
|
|
||||||
display: none;
|
|
||||||
z-index: 1100;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<header class="site-header">
|
<header class="site-header">
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<h1 class="site-title"><a href="map-page.html">Journey Mapper</a></h1>
|
<h1 class="site-title"><a href="map-page.html">Journey Mapper</a></h1>
|
||||||
<div style="display: flex; align-items: center; gap: var(--size-4);">
|
<nav class="site-nav">
|
||||||
<nav class="site-nav">
|
<a href="map-page.html">Map</a>
|
||||||
<a href="map-page.html">Map</a>
|
<a href="blog-list.html" class="active">Blog</a>
|
||||||
<a href="blog-list.html" class="active">Blog</a>
|
</nav>
|
||||||
</nav>
|
|
||||||
<div class="user-menu" id="user-menu"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<main class="blog-container">
|
<main class="blog-container">
|
||||||
<div class="blog-header">
|
<div class="blog-header">
|
||||||
<h1><i class="fas fa-newspaper"></i> Blog Posts</h1>
|
<h1><i class="fas fa-newspaper"></i> Blog Posts</h1>
|
||||||
<a href="blog-post-edit.html?new" class="btn"><i class="fas fa-plus"></i> New Post</a>
|
<a href="blog-post.html?new" class="btn"><i class="fas fa-plus"></i> New Post</a>
|
||||||
</div>
|
</div>
|
||||||
<div id="posts-grid" class="posts-grid">
|
<div id="posts-grid" class="posts-grid">
|
||||||
<!-- Posts loaded dynamically -->
|
<!-- Posts loaded dynamically -->
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<div id="toast" class="toast"></div>
|
|
||||||
|
|
||||||
<script src="js/auth.js"></script>
|
|
||||||
<script>
|
<script>
|
||||||
// ==================== BLOG POSTS ====================
|
const API_BASE = 'http://127.0.0.1:5000/api';
|
||||||
|
|
||||||
async function loadPosts() {
|
async function loadPosts() {
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`${API_BASE}/blog-posts`, { credentials: 'include' });
|
const res = await fetch(`${API_BASE}/blog-posts`);
|
||||||
if (!res.ok) throw new Error('Failed to fetch posts');
|
if (!res.ok) throw new Error('Failed to fetch posts');
|
||||||
const posts = await res.json();
|
const posts = await res.json();
|
||||||
renderPosts(posts);
|
renderPosts(posts);
|
||||||
@ -247,7 +206,7 @@
|
|||||||
<article class="post-card">
|
<article class="post-card">
|
||||||
${post.image ? `<img class="post-card-image" src="${post.image}" alt="${post.title}">` : '<div class="post-card-image" style="background: var(--surface-3); display: flex; align-items: center; justify-content: center;"><i class="fas fa-image" style="font-size: 3rem; color: var(--gray-5);"></i></div>'}
|
${post.image ? `<img class="post-card-image" src="${post.image}" alt="${post.title}">` : '<div class="post-card-image" style="background: var(--surface-3); display: flex; align-items: center; justify-content: center;"><i class="fas fa-image" style="font-size: 3rem; color: var(--gray-5);"></i></div>'}
|
||||||
<div class="post-card-content">
|
<div class="post-card-content">
|
||||||
<h2 class="post-card-title"><a href="blog-post-edit.html?id=${post.id}">${escapeHtml(post.title)}</a></h2>
|
<h2 class="post-card-title"><a href="blog-post.html?id=${post.id}">${escapeHtml(post.title)}</a></h2>
|
||||||
<div class="post-card-meta">
|
<div class="post-card-meta">
|
||||||
<i class="fas fa-calendar-alt"></i> ${new Date(post.created_at).toLocaleDateString()}
|
<i class="fas fa-calendar-alt"></i> ${new Date(post.created_at).toLocaleDateString()}
|
||||||
${post.journeyId ? `<span style="margin-left: 12px;"><i class="fas fa-route"></i> Journey #${post.journeyId}</span>` : ''}
|
${post.journeyId ? `<span style="margin-left: 12px;"><i class="fas fa-route"></i> Journey #${post.journeyId}</span>` : ''}
|
||||||
@ -258,13 +217,17 @@
|
|||||||
`).join('');
|
`).join('');
|
||||||
}
|
}
|
||||||
|
|
||||||
// ==================== INITIALIZATION ====================
|
function escapeHtml(str) {
|
||||||
document.addEventListener('DOMContentLoaded', async () => {
|
if (!str) return '';
|
||||||
const authenticated = await checkAuthAndRedirect();
|
return str.replace(/[&<>]/g, function(m) {
|
||||||
if (!authenticated) return;
|
if (m === '&') return '&';
|
||||||
updateUserMenu();
|
if (m === '<') return '<';
|
||||||
loadPosts();
|
if (m === '>') return '>';
|
||||||
});
|
return m;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener('DOMContentLoaded', loadPosts);
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
370
blog-page.html
Normal file
370
blog-page.html
Normal file
@ -0,0 +1,370 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Blog Post – Journey Mapper</title>
|
||||||
|
|
||||||
|
<!-- Open Props CSS -->
|
||||||
|
<link rel="stylesheet" href="https://unpkg.com/open-props"/>
|
||||||
|
<link rel="stylesheet" href="https://unpkg.com/open-props/normalize.min.css"/>
|
||||||
|
|
||||||
|
<!-- Font Awesome -->
|
||||||
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
||||||
|
|
||||||
|
<!-- Google Fonts: Poppins (matching map page) -->
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;500;600;700&display=swap" rel="stylesheet">
|
||||||
|
|
||||||
|
<style>
|
||||||
|
/* ===== GLOBAL RESET USING OPEN PROPS ===== */
|
||||||
|
* {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
font-family: var(--font-sans);
|
||||||
|
background: var(--gray-0);
|
||||||
|
color: var(--gray-9);
|
||||||
|
line-height: var(--font-lineheight-3);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== HEADER ===== */
|
||||||
|
.site-header {
|
||||||
|
background: var(--gray-9);
|
||||||
|
padding: var(--size-4) var(--size-6);
|
||||||
|
border-bottom: 1px solid var(--surface-4);
|
||||||
|
}
|
||||||
|
.site-header .container {
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: var(--size-2);
|
||||||
|
}
|
||||||
|
.site-title {
|
||||||
|
margin: 0;
|
||||||
|
font-size: var(--font-size-4);
|
||||||
|
font-weight: var(--font-weight-6);
|
||||||
|
}
|
||||||
|
.site-title a {
|
||||||
|
color: var(--indigo-4);
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
.site-nav {
|
||||||
|
display: flex;
|
||||||
|
gap: var(--size-4);
|
||||||
|
}
|
||||||
|
.site-nav a {
|
||||||
|
color: var(--gray-2);
|
||||||
|
text-decoration: none;
|
||||||
|
font-weight: var(--font-weight-5);
|
||||||
|
transition: color 0.2s;
|
||||||
|
}
|
||||||
|
.site-nav a:hover {
|
||||||
|
color: var(--indigo-4);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== MAIN LAYOUT (two‑column) ===== */
|
||||||
|
.post-page {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--size-6);
|
||||||
|
padding: var(--size-6) var(--size-4);
|
||||||
|
max-width: 1400px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.post {
|
||||||
|
background: var(--surface-1);
|
||||||
|
border-radius: var(--radius-3);
|
||||||
|
padding: var(--size-6);
|
||||||
|
box-shadow: var(--shadow-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-title {
|
||||||
|
font-size: var(--font-size-6);
|
||||||
|
font-weight: var(--font-weight-7);
|
||||||
|
margin: 0 0 var(--size-2) 0;
|
||||||
|
color: var(--gray-9);
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-meta {
|
||||||
|
color: var(--gray-6);
|
||||||
|
font-size: var(--font-size-1);
|
||||||
|
margin-bottom: var(--size-4);
|
||||||
|
border-bottom: 1px solid var(--surface-4);
|
||||||
|
padding-bottom: var(--size-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-hero {
|
||||||
|
margin: var(--size-4) 0;
|
||||||
|
}
|
||||||
|
.post-hero img {
|
||||||
|
width: 100%;
|
||||||
|
height: auto;
|
||||||
|
border-radius: var(--radius-2);
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-content {
|
||||||
|
line-height: var(--font-lineheight-4);
|
||||||
|
color: var(--gray-7);
|
||||||
|
}
|
||||||
|
.post-content p {
|
||||||
|
margin-bottom: var(--size-4);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Sidebar */
|
||||||
|
.sidebar {
|
||||||
|
background: var(--surface-1);
|
||||||
|
border-radius: var(--radius-3);
|
||||||
|
padding: var(--size-4);
|
||||||
|
box-shadow: var(--shadow-2);
|
||||||
|
}
|
||||||
|
.sidebar section {
|
||||||
|
margin-bottom: var(--size-6);
|
||||||
|
}
|
||||||
|
.sidebar h3 {
|
||||||
|
font-size: var(--font-size-3);
|
||||||
|
margin-top: 0;
|
||||||
|
margin-bottom: var(--size-3);
|
||||||
|
color: var(--indigo-7);
|
||||||
|
border-left: 3px solid var(--indigo-6);
|
||||||
|
padding-left: var(--size-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Posts list */
|
||||||
|
#posts-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--size-3);
|
||||||
|
}
|
||||||
|
.post-preview {
|
||||||
|
background: var(--surface-2);
|
||||||
|
border-radius: var(--radius-2);
|
||||||
|
padding: var(--size-3);
|
||||||
|
transition: background 0.2s;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.post-preview:hover {
|
||||||
|
background: var(--surface-3);
|
||||||
|
}
|
||||||
|
.post-preview-title {
|
||||||
|
font-weight: var(--font-weight-6);
|
||||||
|
color: var(--gray-9);
|
||||||
|
margin: 0 0 var(--size-1) 0;
|
||||||
|
}
|
||||||
|
.post-preview-meta {
|
||||||
|
font-size: var(--font-size-0);
|
||||||
|
color: var(--gray-6);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Form for creating/editing posts */
|
||||||
|
.form-group {
|
||||||
|
margin-bottom: var(--size-3);
|
||||||
|
}
|
||||||
|
.form-group label {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: var(--size-1);
|
||||||
|
font-weight: var(--font-weight-5);
|
||||||
|
color: var(--gray-7);
|
||||||
|
}
|
||||||
|
.form-group input,
|
||||||
|
.form-group textarea {
|
||||||
|
width: 100%;
|
||||||
|
padding: var(--size-2) var(--size-3);
|
||||||
|
border: 1px solid var(--surface-4);
|
||||||
|
border-radius: var(--radius-2);
|
||||||
|
background: var(--surface-2);
|
||||||
|
color: var(--text-1);
|
||||||
|
font-family: inherit;
|
||||||
|
font-size: var(--font-size-2);
|
||||||
|
}
|
||||||
|
.form-group input:focus,
|
||||||
|
.form-group textarea:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: var(--indigo-6);
|
||||||
|
box-shadow: 0 0 0 3px var(--indigo-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Buttons (reuse map page classes) */
|
||||||
|
.btn {
|
||||||
|
padding: var(--size-2) var(--size-4);
|
||||||
|
border: none;
|
||||||
|
border-radius: var(--radius-2);
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: var(--font-size-2);
|
||||||
|
font-weight: var(--font-weight-5);
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--size-2);
|
||||||
|
transition: all 0.2s var(--ease-2);
|
||||||
|
box-shadow: var(--shadow-2);
|
||||||
|
}
|
||||||
|
.btn-primary {
|
||||||
|
background: var(--indigo-7);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
.btn-primary:hover {
|
||||||
|
background: var(--indigo-8);
|
||||||
|
box-shadow: var(--shadow-3);
|
||||||
|
}
|
||||||
|
.btn-secondary {
|
||||||
|
background: var(--gray-7);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
.btn-secondary:hover {
|
||||||
|
background: var(--gray-8);
|
||||||
|
}
|
||||||
|
.btn-danger {
|
||||||
|
background: var(--red-7);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
.btn-danger:hover {
|
||||||
|
background: var(--red-8);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Comments section */
|
||||||
|
.comments {
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: var(--size-6) auto 0;
|
||||||
|
padding: 0 var(--size-4);
|
||||||
|
}
|
||||||
|
.comments h3 {
|
||||||
|
font-size: var(--font-size-4);
|
||||||
|
margin-bottom: var(--size-4);
|
||||||
|
color: var(--indigo-7);
|
||||||
|
}
|
||||||
|
.comments-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--size-4);
|
||||||
|
}
|
||||||
|
.comment-item {
|
||||||
|
background: var(--surface-1);
|
||||||
|
border-radius: var(--radius-2);
|
||||||
|
padding: var(--size-4);
|
||||||
|
box-shadow: var(--shadow-1);
|
||||||
|
}
|
||||||
|
.comment-item .meta {
|
||||||
|
color: var(--gray-6);
|
||||||
|
font-size: var(--font-size-0);
|
||||||
|
margin-bottom: var(--size-2);
|
||||||
|
}
|
||||||
|
.comment-item p {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Footer */
|
||||||
|
.site-footer {
|
||||||
|
background: var(--surface-2);
|
||||||
|
padding: var(--size-4);
|
||||||
|
margin-top: var(--size-6);
|
||||||
|
text-align: center;
|
||||||
|
color: var(--gray-6);
|
||||||
|
font-size: var(--font-size-1);
|
||||||
|
border-top: 1px solid var(--surface-4);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive two‑column layout */
|
||||||
|
@media (min-width: 768px) {
|
||||||
|
.post-page {
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: var(--size-6);
|
||||||
|
padding: var(--size-6);
|
||||||
|
}
|
||||||
|
.post {
|
||||||
|
flex: 1;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
.sidebar {
|
||||||
|
width: 320px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
position: sticky;
|
||||||
|
top: var(--size-4);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<header class="site-header">
|
||||||
|
<div class="container">
|
||||||
|
<h1 class="site-title"><a href="map-page.html">Journey Mapper</a></h1>
|
||||||
|
<nav class="site-nav">
|
||||||
|
<a href="map-page.html">Map</a>
|
||||||
|
<a href="blog-page.html" class="active">Blog</a>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main class="post-page">
|
||||||
|
<article class="post">
|
||||||
|
<h2 class="post-title">Post Title</h2>
|
||||||
|
<div class="post-meta">By <span class="author">Author</span> — <time datetime="2026-01-01">Jan 1, 2026</time></div>
|
||||||
|
<figure class="post-hero">
|
||||||
|
<img src="https://via.placeholder.com/800x350?text=Hero+Image" alt="Post image">
|
||||||
|
</figure>
|
||||||
|
<div class="post-content">
|
||||||
|
<p>This is a placeholder paragraph for the blog post content. Replace with real content.</p>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
|
||||||
|
<aside class="sidebar">
|
||||||
|
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:12px;">
|
||||||
|
<h3 style="margin:0">Posts</h3>
|
||||||
|
<button id="create-post-btn" class="btn btn-primary">New</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="posts-list">
|
||||||
|
<!-- JS will render list of posts here -->
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<section class="about" id="post-form" style="display:none;margin-top:16px;">
|
||||||
|
<h3 id="form-title">New Post</h3>
|
||||||
|
<form id="blog-form">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="post-title-input">Title</label>
|
||||||
|
<input id="post-title-input" name="title" required />
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="post-journey-input">Journey ID</label>
|
||||||
|
<input id="post-journey-input" name="journeyId" placeholder="optional" />
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="post-image-input">Image URL</label>
|
||||||
|
<input id="post-image-input" name="image" placeholder="https://..." />
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="post-content-input">Content</label>
|
||||||
|
<textarea id="post-content-input" name="content" rows="6" required></textarea>
|
||||||
|
</div>
|
||||||
|
<div style="display:flex;gap:8px">
|
||||||
|
<button type="submit" class="btn btn-primary">Save</button>
|
||||||
|
<button type="button" id="cancel-post-btn" class="btn btn-secondary">Cancel</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="about" style="margin-top:18px;">
|
||||||
|
<h3>About</h3>
|
||||||
|
<p>Short author bio or related links.</p>
|
||||||
|
</section>
|
||||||
|
</aside>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<section class="comments">
|
||||||
|
<h3>Comments</h3>
|
||||||
|
<div class="comments-list"></div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<footer class="site-footer">
|
||||||
|
<div class="container">© 2026 My Blog</div>
|
||||||
|
</footer>
|
||||||
|
|
||||||
|
<!-- JavaScript (keep your existing logic) -->
|
||||||
|
<script src="js/main.js"></script>
|
||||||
|
<script src="js/blog-posts.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@ -1,19 +1,19 @@
|
|||||||
<!doctype html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<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>Blog Post – Journey Mapper</title>
|
<title>Blog Post – Journey Mapper</title>
|
||||||
|
|
||||||
<!-- Open Props CSS -->
|
<!-- Open Props CSS -->
|
||||||
<link rel="stylesheet" href="https://unpkg.com/open-props" />
|
<link rel="stylesheet" href="https://unpkg.com/open-props"/>
|
||||||
<link rel="stylesheet" href="https://unpkg.com/open-props/normalize.min.css" />
|
<link rel="stylesheet" href="https://unpkg.com/open-props/normalize.min.css"/>
|
||||||
|
|
||||||
<!-- Font Awesome -->
|
<!-- Font Awesome -->
|
||||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" />
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
||||||
|
|
||||||
<!-- Google Fonts -->
|
<!-- Google Fonts: Poppins -->
|
||||||
<link href="https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;500;600;700&display=swap" rel="stylesheet" />
|
<link href="https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;500;600;700&display=swap" rel="stylesheet">
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
* { box-sizing: border-box; }
|
* { box-sizing: border-box; }
|
||||||
@ -69,31 +69,6 @@
|
|||||||
background: var(--surface-2);
|
background: var(--surface-2);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* User menu */
|
|
||||||
.user-menu {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: var(--size-2);
|
|
||||||
}
|
|
||||||
.user-menu .username {
|
|
||||||
color: var(--gray-2);
|
|
||||||
font-weight: var(--font-weight-5);
|
|
||||||
}
|
|
||||||
.logout-btn {
|
|
||||||
background: var(--indigo-7);
|
|
||||||
color: white;
|
|
||||||
border: none;
|
|
||||||
border-radius: var(--radius-2);
|
|
||||||
padding: var(--size-1) var(--size-3);
|
|
||||||
cursor: pointer;
|
|
||||||
font-size: var(--font-size-1);
|
|
||||||
font-weight: var(--font-weight-5);
|
|
||||||
transition: background 0.2s;
|
|
||||||
}
|
|
||||||
.logout-btn:hover {
|
|
||||||
background: var(--indigo-8);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Main content */
|
/* Main content */
|
||||||
.post-container {
|
.post-container {
|
||||||
max-width: 800px;
|
max-width: 800px;
|
||||||
@ -195,13 +170,10 @@
|
|||||||
<header class="site-header">
|
<header class="site-header">
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<h1 class="site-title"><a href="map-page.html">Journey Mapper</a></h1>
|
<h1 class="site-title"><a href="map-page.html">Journey Mapper</a></h1>
|
||||||
<div style="display: flex; align-items: center; gap: var(--size-4);">
|
<nav class="site-nav">
|
||||||
<nav class="site-nav">
|
<a href="map-page.html">Map</a>
|
||||||
<a href="map-page.html">Map</a>
|
<a href="blog-list.html">Blog</a>
|
||||||
<a href="blog-list.html" class="active">Blog</a>
|
</nav>
|
||||||
</nav>
|
|
||||||
<div class="user-menu" id="user-menu"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
@ -211,7 +183,7 @@
|
|||||||
<form id="post-form">
|
<form id="post-form">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="post-title">Title</label>
|
<label for="post-title">Title</label>
|
||||||
<input type="text" id="post-title" required />
|
<input type="text" id="post-title" required>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="post-content">Content</label>
|
<label for="post-content">Content</label>
|
||||||
@ -219,13 +191,13 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="post-journey">Associated Journey ID (optional)</label>
|
<label for="post-journey">Associated Journey ID (optional)</label>
|
||||||
<input type="text" id="post-journey" placeholder="e.g., 3" />
|
<input type="text" id="post-journey" placeholder="e.g., 3">
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="post-image">Image URL or Upload</label>
|
<label for="post-image">Image URL or Upload</label>
|
||||||
<input type="text" id="post-image-url" placeholder="https://..." />
|
<input type="text" id="post-image-url" placeholder="https://...">
|
||||||
<div style="margin: 8px 0; text-align: center">or</div>
|
<div style="margin: 8px 0; text-align: center;">or</div>
|
||||||
<input type="file" id="image-upload" accept="image/*" />
|
<input type="file" id="image-upload" accept="image/*">
|
||||||
<div class="image-preview" id="image-preview"></div>
|
<div class="image-preview" id="image-preview"></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="button-group">
|
<div class="button-group">
|
||||||
@ -238,19 +210,19 @@
|
|||||||
</main>
|
</main>
|
||||||
|
|
||||||
<div id="toast" class="toast"></div>
|
<div id="toast" class="toast"></div>
|
||||||
|
|
||||||
<script src="js/auth.js"></script>
|
|
||||||
<script>
|
<script>
|
||||||
|
const API_BASE = 'http://127.0.0.1:5000/api';
|
||||||
let currentPostId = null;
|
let currentPostId = null;
|
||||||
|
|
||||||
// ==================== POST CRUD ====================
|
// Get post id from URL
|
||||||
const urlParams = new URLSearchParams(window.location.search);
|
const urlParams = new URLSearchParams(window.location.search);
|
||||||
const postId = urlParams.get('id');
|
const postId = urlParams.get('id');
|
||||||
const isNew = urlParams.has('new');
|
const isNew = urlParams.has('new');
|
||||||
|
|
||||||
async function loadPost(id) {
|
async function loadPost(id) {
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`${API_BASE}/blog-posts/${id}`, { credentials: 'include' });
|
const res = await fetch(`${API_BASE}/blog-posts/${id}`);
|
||||||
if (!res.ok) throw new Error('Post not found');
|
if (!res.ok) throw new Error('Post not found');
|
||||||
const post = await res.json();
|
const post = await res.json();
|
||||||
currentPostId = post.id;
|
currentPostId = post.id;
|
||||||
@ -286,7 +258,12 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const payload = { title, content, journeyId: journeyId || null, image: image || null };
|
const payload = {
|
||||||
|
title,
|
||||||
|
content,
|
||||||
|
journeyId: journeyId || null,
|
||||||
|
image: image || null
|
||||||
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
let url, method;
|
let url, method;
|
||||||
@ -301,14 +278,17 @@
|
|||||||
const res = await fetch(url, {
|
const res = await fetch(url, {
|
||||||
method,
|
method,
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify(payload),
|
body: JSON.stringify(payload)
|
||||||
credentials: 'include'
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!res.ok) throw new Error('Save failed');
|
if (!res.ok) throw new Error('Save failed');
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
showToast('Post saved successfully');
|
showToast('Post saved successfully');
|
||||||
|
// Redirect to the new post if it was created
|
||||||
if (isNew) {
|
if (isNew) {
|
||||||
window.location.href = `blog-post-edit.html?id=${data.id}`;
|
window.location.href = `blog-post.html?id=${data.id}`;
|
||||||
|
} else {
|
||||||
|
// reload if needed
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
showToast('Error saving post', true);
|
showToast('Error saving post', true);
|
||||||
@ -319,10 +299,7 @@
|
|||||||
if (!currentPostId) return;
|
if (!currentPostId) return;
|
||||||
if (!confirm('Delete this post?')) return;
|
if (!confirm('Delete this post?')) return;
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`${API_BASE}/blog-posts/${currentPostId}`, {
|
const res = await fetch(`${API_BASE}/blog-posts/${currentPostId}`, { method: 'DELETE' });
|
||||||
method: 'DELETE',
|
|
||||||
credentials: 'include'
|
|
||||||
});
|
|
||||||
if (!res.ok) throw new Error('Delete failed');
|
if (!res.ok) throw new Error('Delete failed');
|
||||||
showToast('Post deleted');
|
showToast('Post deleted');
|
||||||
setTimeout(() => { window.location.href = 'blog-list.html'; }, 1000);
|
setTimeout(() => { window.location.href = 'blog-list.html'; }, 1000);
|
||||||
@ -331,7 +308,15 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Image upload (convert to base64)
|
function showToast(message, isError = false) {
|
||||||
|
const toast = document.getElementById('toast');
|
||||||
|
toast.textContent = message;
|
||||||
|
toast.style.backgroundColor = isError ? 'var(--red-7)' : 'var(--green-7)';
|
||||||
|
toast.style.display = 'block';
|
||||||
|
setTimeout(() => { toast.style.display = 'none'; }, 3000);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Image upload (convert to base64 and set as image URL)
|
||||||
document.getElementById('image-upload').addEventListener('change', function(e) {
|
document.getElementById('image-upload').addEventListener('change', function(e) {
|
||||||
const file = e.target.files[0];
|
const file = e.target.files[0];
|
||||||
if (!file) return;
|
if (!file) return;
|
||||||
@ -347,23 +332,16 @@
|
|||||||
document.getElementById('post-form').addEventListener('submit', savePost);
|
document.getElementById('post-form').addEventListener('submit', savePost);
|
||||||
document.getElementById('delete-post').addEventListener('click', deletePost);
|
document.getElementById('delete-post').addEventListener('click', deletePost);
|
||||||
|
|
||||||
// ==================== INITIALIZATION ====================
|
if (!isNew && postId) {
|
||||||
document.addEventListener('DOMContentLoaded', async () => {
|
loadPost(postId);
|
||||||
const authenticated = await checkAuthAndRedirect();
|
} else if (isNew) {
|
||||||
if (!authenticated) return;
|
currentPostId = null;
|
||||||
updateUserMenu();
|
document.getElementById('form-title').textContent = 'New Post';
|
||||||
|
document.getElementById('delete-post').style.display = 'none';
|
||||||
if (!isNew && postId) {
|
} else {
|
||||||
loadPost(postId);
|
// No id and not new – maybe go to list
|
||||||
} else if (isNew) {
|
window.location.href = 'blog-list.html';
|
||||||
currentPostId = null;
|
}
|
||||||
document.getElementById('form-title').textContent = 'New Post';
|
|
||||||
document.getElementById('delete-post').style.display = 'none';
|
|
||||||
} else {
|
|
||||||
// No id and not new – redirect to list
|
|
||||||
window.location.href = 'blog-list.html';
|
|
||||||
}
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
67
js/auth.js
67
js/auth.js
@ -1,67 +0,0 @@
|
|||||||
const API_BASE = "http://127.0.0.1:5000/api";
|
|
||||||
let currentUser = null;
|
|
||||||
|
|
||||||
async function checkAuth() {
|
|
||||||
try {
|
|
||||||
const res = await fetch(`${API_BASE}/me`, { credentials: "include" });
|
|
||||||
if (res.ok) {
|
|
||||||
currentUser = await res.json();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
} catch (err) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function checkAuthAndRedirect() {
|
|
||||||
const ok = await checkAuth();
|
|
||||||
if (!ok) {
|
|
||||||
window.location.href = "login.html";
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateUserMenu() {
|
|
||||||
const container = document.getElementById("user-menu");
|
|
||||||
if (!container) return;
|
|
||||||
if (currentUser) {
|
|
||||||
container.innerHTML = `
|
|
||||||
<span class="username"><i class="fas fa-user"></i> ${escapeHtml(currentUser.username)}</span>
|
|
||||||
<button id="logout-btn" class="logout-btn"><i class="fas fa-sign-out-alt"></i> Logout</button>
|
|
||||||
`;
|
|
||||||
document.getElementById("logout-btn")?.addEventListener("click", logout);
|
|
||||||
} else {
|
|
||||||
container.innerHTML = `<button id="login-open-btn" class="login-btn"><i class="fas fa-sign-in-alt"></i> Login</button>`;
|
|
||||||
document.getElementById("login-open-btn")?.addEventListener("click", () => {
|
|
||||||
window.location.href = "login.html";
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function logout() {
|
|
||||||
await fetch(`${API_BASE}/logout`, { method: "POST", credentials: "include" });
|
|
||||||
window.location.href = "login.html";
|
|
||||||
}
|
|
||||||
|
|
||||||
function escapeHtml(str) {
|
|
||||||
if (!str) return "";
|
|
||||||
return str.replace(/[&<>]/g, function (m) {
|
|
||||||
if (m === "&") return "&";
|
|
||||||
if (m === "<") return "<";
|
|
||||||
if (m === ">") return ">";
|
|
||||||
return m;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function showToast(msg, isError = false) {
|
|
||||||
const toast = document.getElementById("toast");
|
|
||||||
if (!toast) return;
|
|
||||||
toast.textContent = msg;
|
|
||||||
toast.style.backgroundColor = isError ? "var(--red-7)" : "var(--green-7)";
|
|
||||||
toast.style.display = "block";
|
|
||||||
setTimeout(() => {
|
|
||||||
toast.style.display = "none";
|
|
||||||
}, 3000);
|
|
||||||
}
|
|
||||||
223
login.html
223
login.html
@ -1,223 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<title>Login – Journey Mapper</title>
|
|
||||||
|
|
||||||
<!-- Open Props CSS -->
|
|
||||||
<link rel="stylesheet" href="https://unpkg.com/open-props"/>
|
|
||||||
<link rel="stylesheet" href="https://unpkg.com/open-props/normalize.min.css"/>
|
|
||||||
|
|
||||||
<!-- Font Awesome -->
|
|
||||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
|
||||||
|
|
||||||
<!-- Google Fonts -->
|
|
||||||
<link href="https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;500;600;700&display=swap" rel="stylesheet">
|
|
||||||
|
|
||||||
<style>
|
|
||||||
* { box-sizing: border-box; }
|
|
||||||
body {
|
|
||||||
font-family: var(--font-sans);
|
|
||||||
background: var(--gray-0);
|
|
||||||
color: var(--gray-9);
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
min-height: 100vh;
|
|
||||||
margin: 0;
|
|
||||||
padding: var(--size-4);
|
|
||||||
}
|
|
||||||
|
|
||||||
.login-container {
|
|
||||||
max-width: 400px;
|
|
||||||
width: 100%;
|
|
||||||
background: var(--surface-1);
|
|
||||||
border-radius: var(--radius-3);
|
|
||||||
padding: var(--size-6);
|
|
||||||
box-shadow: var(--shadow-4);
|
|
||||||
}
|
|
||||||
|
|
||||||
.login-header {
|
|
||||||
text-align: center;
|
|
||||||
margin-bottom: var(--size-6);
|
|
||||||
}
|
|
||||||
|
|
||||||
.login-header h1 {
|
|
||||||
color: var(--indigo-8);
|
|
||||||
margin: 0;
|
|
||||||
font-size: var(--font-size-5);
|
|
||||||
}
|
|
||||||
|
|
||||||
.auth-tabs {
|
|
||||||
display: flex;
|
|
||||||
gap: var(--size-2);
|
|
||||||
border-bottom: 1px solid var(--surface-4);
|
|
||||||
margin-bottom: var(--size-4);
|
|
||||||
}
|
|
||||||
.auth-tab {
|
|
||||||
padding: var(--size-2) var(--size-4);
|
|
||||||
cursor: pointer;
|
|
||||||
background: none;
|
|
||||||
border: none;
|
|
||||||
font-size: var(--font-size-2);
|
|
||||||
font-weight: var(--font-weight-5);
|
|
||||||
color: var(--gray-6);
|
|
||||||
transition: all 0.2s;
|
|
||||||
}
|
|
||||||
.auth-tab.active {
|
|
||||||
color: var(--indigo-7);
|
|
||||||
border-bottom: 2px solid var(--indigo-7);
|
|
||||||
}
|
|
||||||
.auth-form {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
.auth-form.active {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
.form-group {
|
|
||||||
margin-bottom: var(--size-4);
|
|
||||||
}
|
|
||||||
label {
|
|
||||||
display: block;
|
|
||||||
margin-bottom: var(--size-1);
|
|
||||||
font-weight: var(--font-weight-5);
|
|
||||||
}
|
|
||||||
input {
|
|
||||||
width: 100%;
|
|
||||||
padding: var(--size-2) var(--size-3);
|
|
||||||
border: 1px solid var(--surface-4);
|
|
||||||
border-radius: var(--radius-2);
|
|
||||||
background: var(--surface-2);
|
|
||||||
font-size: var(--font-size-2);
|
|
||||||
}
|
|
||||||
.btn {
|
|
||||||
width: 100%;
|
|
||||||
padding: var(--size-2);
|
|
||||||
border: none;
|
|
||||||
border-radius: var(--radius-2);
|
|
||||||
background: var(--indigo-7);
|
|
||||||
color: white;
|
|
||||||
font-size: var(--font-size-2);
|
|
||||||
font-weight: var(--font-weight-5);
|
|
||||||
cursor: pointer;
|
|
||||||
transition: background 0.2s;
|
|
||||||
}
|
|
||||||
.btn:hover {
|
|
||||||
background: var(--indigo-8);
|
|
||||||
}
|
|
||||||
.toast {
|
|
||||||
position: fixed;
|
|
||||||
bottom: var(--size-4);
|
|
||||||
right: var(--size-4);
|
|
||||||
background: var(--green-7);
|
|
||||||
color: white;
|
|
||||||
padding: var(--size-2) var(--size-4);
|
|
||||||
border-radius: var(--radius-2);
|
|
||||||
display: none;
|
|
||||||
z-index: 1100;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div class="login-container">
|
|
||||||
<div class="login-header">
|
|
||||||
<h1><i class="fas fa-map-marked-alt"></i> Journey Mapper</h1>
|
|
||||||
<p>Sign in to continue</p>
|
|
||||||
</div>
|
|
||||||
<div class="auth-tabs">
|
|
||||||
<button class="auth-tab active" data-tab="login">Login</button>
|
|
||||||
<button class="auth-tab" data-tab="register">Register</button>
|
|
||||||
</div>
|
|
||||||
<div id="login-form" class="auth-form active">
|
|
||||||
<div class="form-group">
|
|
||||||
<label>Username</label>
|
|
||||||
<input type="text" id="login-username" required>
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label>Password</label>
|
|
||||||
<input type="password" id="login-password" required>
|
|
||||||
</div>
|
|
||||||
<button id="login-submit" class="btn">Login</button>
|
|
||||||
</div>
|
|
||||||
<div id="register-form" class="auth-form">
|
|
||||||
<div class="form-group">
|
|
||||||
<label>Username</label>
|
|
||||||
<input type="text" id="register-username" required>
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label>Password</label>
|
|
||||||
<input type="password" id="register-password" required>
|
|
||||||
</div>
|
|
||||||
<button id="register-submit" class="btn">Register</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div id="toast" class="toast"></div>
|
|
||||||
|
|
||||||
<script src="js/auth.js"></script>
|
|
||||||
<script>
|
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
|
||||||
// Tab switching
|
|
||||||
document.querySelectorAll('.auth-tab').forEach(tab => {
|
|
||||||
tab.addEventListener('click', () => {
|
|
||||||
const target = tab.dataset.tab;
|
|
||||||
document.querySelectorAll('.auth-tab').forEach(t => t.classList.remove('active'));
|
|
||||||
tab.classList.add('active');
|
|
||||||
document.querySelectorAll('.auth-form').forEach(f => f.classList.remove('active'));
|
|
||||||
document.getElementById(`${target}-form`).classList.add('active');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
// Login button
|
|
||||||
document.getElementById('login-submit').addEventListener('click', async () => {
|
|
||||||
const username = document.getElementById('login-username').value.trim();
|
|
||||||
const password = document.getElementById('login-password').value;
|
|
||||||
if (!username || !password) {
|
|
||||||
showToast('Please enter username and password', true);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
const res = await fetch(`${API_BASE}/login`, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
body: JSON.stringify({ username, password }),
|
|
||||||
credentials: 'include'
|
|
||||||
});
|
|
||||||
const data = await res.json();
|
|
||||||
if (!res.ok) throw new Error(data.error || 'Login failed');
|
|
||||||
showToast(`Welcome, ${data.username}`);
|
|
||||||
window.location.href = 'map-page.html';
|
|
||||||
} catch (err) {
|
|
||||||
showToast(err.message, true);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
// Register button
|
|
||||||
document.getElementById('register-submit').addEventListener('click', async () => {
|
|
||||||
const username = document.getElementById('register-username').value.trim();
|
|
||||||
const password = document.getElementById('register-password').value;
|
|
||||||
if (!username || !password) {
|
|
||||||
showToast('Please enter username and password', true);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (password.length < 4) {
|
|
||||||
showToast('Password must be at least 4 characters', true);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
const res = await fetch(`${API_BASE}/register`, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
body: JSON.stringify({ username, password }),
|
|
||||||
credentials: 'include'
|
|
||||||
});
|
|
||||||
const data = await res.json();
|
|
||||||
if (!res.ok) throw new Error(data.error || 'Registration failed');
|
|
||||||
showToast(`Registered as ${data.username}. Logging in...`);
|
|
||||||
window.location.href = 'map-page.html';
|
|
||||||
} catch (err) {
|
|
||||||
showToast(err.message, true);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
2446
map-page.html
2446
map-page.html
File diff suppressed because it is too large
Load Diff
Loading…
x
Reference in New Issue
Block a user