Compare commits

..

1 Commits

Author SHA1 Message Date
130d84472c Update README.md
corrected spelling mistake (where -> were)
2026-02-27 10:23:23 +01:00
23 changed files with 2 additions and 4741 deletions

4
.gitignore vendored
View File

@ -1,3 +1 @@
Lession_material .DS_Store
*.DS_Store
.aider*

View File

@ -1,7 +1,7 @@
# Travel Journal (Draft README) # Travel Journal (Draft README)
## Projectdescription ## Projectdescription
On this website you can document your travelstories. On this website you can document your travelstories.
Where you where, what you did and what videos and pictures you took. Where you were, what you did and what videos and pictures you took.
Keep your journal and share it with friends or just log it for your memory. Keep your journal and share it with friends or just log it for your memory.
## Pages ## Pages
@ -34,4 +34,3 @@ TBD
- Flepp Stiafen - Flepp Stiafen
- Kohler Joshua - Kohler Joshua
- Rüegger André - Rüegger André

View File

@ -1,403 +0,0 @@
from flask import Flask, request, jsonify, session
from flask_cors import CORS
from werkzeug.security import generate_password_hash, check_password_hash
import json
import os
from datetime import datetime
app = Flask(__name__)
app.secret_key = "your-secret-key-here-change-in-production" # needed for sessions
CORS(app, supports_credentials=True) # allow cookies
# Directories
DATA_DIR = "data"
USERS_FILE = os.path.join(DATA_DIR, "users.json")
os.makedirs(DATA_DIR, exist_ok=True)
# ==================== User helpers ====================
def load_users():
try:
if os.path.exists(USERS_FILE):
with open(USERS_FILE, "r") as f:
return json.load(f)
except Exception as e:
print(f"Error loading users: {e}")
return []
def save_users(users):
try:
with open(USERS_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)
# ==================== Peruser 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)
except Exception as e:
print(f"Error saving journeys for user {user_id}: {e}")
def load_user_posts(user_id):
file_path = os.path.join(get_user_data_dir(user_id), "posts.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 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, userspecific) ====================
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:
return 1
return max(j["id"] for j in journeys) + 1
@app.route("/api/journeys", methods=["POST"])
def create_journey():
if not require_login():
return jsonify({"error": "Authentication required"}), 401
data = request.get_json()
if not data:
return jsonify({"error": "No data provided"}), 400
title = data.get("title")
if not title:
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 = {
"id": new_id,
"title": title,
"description": data.get("description", ""),
"markers": data.get("markers", []),
"created_at": datetime.now().isoformat(),
}
journeys.append(new_journey)
save_user_journeys(user_id, journeys)
return jsonify(new_journey), 201
@app.route("/api/journeys/<int:journey_id>", methods=["GET"])
def get_journey(journey_id):
if not require_login():
return jsonify({"error": "Authentication required"}), 401
journeys = get_journeys_for_current_user()
journey = next((j for j in journeys if j["id"] == journey_id), None)
if journey is None:
return jsonify({"error": "Journey not found"}), 404
return jsonify(journey)
@app.route("/api/journeys/<int:journey_id>", methods=["PUT"])
def update_journey(journey_id):
if not require_login():
return jsonify({"error": "Authentication required"}), 401
journeys = get_journeys_for_current_user()
journey = next((j for j in journeys if j["id"] == journey_id), None)
if journey is None:
return jsonify({"error": "Journey not found"}), 404
data = request.get_json()
if not data:
return jsonify({"error": "No data provided"}), 400
if "title" in data:
journey["title"] = data["title"]
if "description" in data:
journey["description"] = data["description"]
if "markers" in data:
journey["markers"] = data["markers"]
save_user_journeys(get_current_user_id(), journeys)
return jsonify(journey)
@app.route("/api/journeys/<int:journey_id>", methods=["DELETE"])
def delete_journey(journey_id):
if not require_login():
return jsonify({"error": "Authentication required"}), 401
journeys = get_journeys_for_current_user()
journey = next((j for j in journeys if j["id"] == journey_id), None)
if journey is None:
return jsonify({"error": "Journey not found"}), 404
journeys = [j for j in journeys if j["id"] != journey_id]
save_user_journeys(get_current_user_id(), journeys)
return jsonify({"message": "Journey deleted successfully", "journey": journey})
# ==================== Blog Post endpoints (protected, userspecific) ====================
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():
return jsonify({"status": "healthy", "timestamp": datetime.now().isoformat()})
@app.route("/")
def index():
return """
<!DOCTYPE html>
<html>
<head><title>Journey Mapper API</title></head>
<body>
<h1>Journey Mapper API</h1>
<p>Backend is running. Use <code>/api/journeys</code> endpoints.</p>
<p>Authentication required: register, login, then use session cookies.</p>
</body>
</html>
"""
if __name__ == "__main__":
app.run(debug=True, port=5000)

View File

@ -1,10 +0,0 @@
[
{
"id": 1,
"title": "test",
"content": "sfsfsfsaf",
"journeyId": "1",
"image": null,
"created_at": "2026-03-27T19:49:49.410806"
}
]

View File

@ -1,82 +0,0 @@
[
{
"id": 1,
"title": "test",
"description": "sdfsf\n",
"markers": [
{
"lat": 48.22467264956519,
"lng": 9.536132812500002,
"title": "New Marker",
"date": "",
"description": "",
"videoUrl": ""
},
{
"lat": 49.937079756975294,
"lng": 8.789062500000002,
"title": "New Marker",
"date": "",
"description": "",
"videoUrl": ""
},
{
"lat": 50.583236614805905,
"lng": 9.689941406250002,
"title": "New Marker",
"date": "",
"description": "",
"videoUrl": ""
}
],
"created_at": "2026-03-01T19:02:15.679031"
},
{
"id": 2,
"title": "test 1",
"description": "sdfdsfafsfsdsf",
"markers": [
{
"lat": 48.705462895790575,
"lng": 2.4334716796875,
"title": "New Marker",
"date": "",
"description": "",
"videoUrl": ""
},
{
"lat": 47.37603463349758,
"lng": 2.0654296875000004,
"title": "New Marker",
"date": "",
"description": "",
"videoUrl": ""
},
{
"lat": 47.25686404408872,
"lng": 5.020751953125,
"title": "New Marker",
"date": "",
"description": "",
"videoUrl": ""
},
{
"lat": 47.25686404408872,
"lng": 5.954589843750001,
"title": "New Marker",
"date": "",
"description": "",
"videoUrl": ""
},
{
"lat": 47.357431944587034,
"lng": 7.289428710937501,
"title": "New Marker",
"date": "",
"description": "",
"videoUrl": ""
}
],
"created_at": "2026-03-05T13:00:48.757539"
}
]

View File

@ -1,8 +0,0 @@
[
{
"id": 1,
"username": "josh",
"password_hash": "scrypt:32768:8:1$HA70PiOwbBrIwlDq$2ab80bdc08bb3bb4214258566aded836062323380491a7f4c7f2e67bdccb8686367789f57b3c6c5eb3e2f08c8c07186f47f9c89d1e72179ddd3758b509f23fbe",
"created_at": "2026-03-27T20:32:43.107028"
}
]

View File

@ -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"
}
]

View File

@ -1,10 +0,0 @@
[
{
"id": 1,
"title": "test",
"content": "ksafladjsfk",
"journeyId": "1",
"image": null,
"created_at": "2026-03-27T21:23:39.755057"
}
]

View File

@ -1,8 +0,0 @@
[project]
name = "backend"
version = "0.1.0"
requires-python = ">=3.12"
dependencies = [
"flask>=3.1.3",
"flask-cors>=6.0.2",
]

Binary file not shown.

174
backend/uv.lock generated
View File

@ -1,174 +0,0 @@
version = 1
revision = 3
requires-python = ">=3.12"
[[package]]
name = "backend"
version = "0.1.0"
source = { virtual = "." }
dependencies = [
{ name = "flask" },
{ name = "flask-cors" },
]
[package.metadata]
requires-dist = [
{ name = "flask", specifier = ">=3.1.3" },
{ name = "flask-cors", specifier = ">=6.0.2" },
]
[[package]]
name = "blinker"
version = "1.9.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/21/28/9b3f50ce0e048515135495f198351908d99540d69bfdc8c1d15b73dc55ce/blinker-1.9.0.tar.gz", hash = "sha256:b4ce2265a7abece45e7cc896e98dbebe6cead56bcf805a3d23136d145f5445bf", size = 22460, upload-time = "2024-11-08T17:25:47.436Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/10/cb/f2ad4230dc2eb1a74edf38f1a38b9b52277f75bef262d8908e60d957e13c/blinker-1.9.0-py3-none-any.whl", hash = "sha256:ba0efaa9080b619ff2f3459d1d500c57bddea4a6b424b60a91141db6fd2f08bc", size = 8458, upload-time = "2024-11-08T17:25:46.184Z" },
]
[[package]]
name = "click"
version = "8.3.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "colorama", marker = "sys_platform == 'win32'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/3d/fa/656b739db8587d7b5dfa22e22ed02566950fbfbcdc20311993483657a5c0/click-8.3.1.tar.gz", hash = "sha256:12ff4785d337a1bb490bb7e9c2b1ee5da3112e94a8622f26a6c77f5d2fc6842a", size = 295065, upload-time = "2025-11-15T20:45:42.706Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/98/78/01c019cdb5d6498122777c1a43056ebb3ebfeef2076d9d026bfe15583b2b/click-8.3.1-py3-none-any.whl", hash = "sha256:981153a64e25f12d547d3426c367a4857371575ee7ad18df2a6183ab0545b2a6", size = 108274, upload-time = "2025-11-15T20:45:41.139Z" },
]
[[package]]
name = "colorama"
version = "0.4.6"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
]
[[package]]
name = "flask"
version = "3.1.3"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "blinker" },
{ name = "click" },
{ name = "itsdangerous" },
{ name = "jinja2" },
{ name = "markupsafe" },
{ name = "werkzeug" },
]
sdist = { url = "https://files.pythonhosted.org/packages/26/00/35d85dcce6c57fdc871f3867d465d780f302a175ea360f62533f12b27e2b/flask-3.1.3.tar.gz", hash = "sha256:0ef0e52b8a9cd932855379197dd8f94047b359ca0a78695144304cb45f87c9eb", size = 759004, upload-time = "2026-02-19T05:00:57.678Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/7f/9c/34f6962f9b9e9c71f6e5ed806e0d0ff03c9d1b0b2340088a0cf4bce09b18/flask-3.1.3-py3-none-any.whl", hash = "sha256:f4bcbefc124291925f1a26446da31a5178f9483862233b23c0c96a20701f670c", size = 103424, upload-time = "2026-02-19T05:00:56.027Z" },
]
[[package]]
name = "flask-cors"
version = "6.0.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "flask" },
{ name = "werkzeug" },
]
sdist = { url = "https://files.pythonhosted.org/packages/70/74/0fc0fa68d62f21daef41017dafab19ef4b36551521260987eb3a5394c7ba/flask_cors-6.0.2.tar.gz", hash = "sha256:6e118f3698249ae33e429760db98ce032a8bf9913638d085ca0f4c5534ad2423", size = 13472, upload-time = "2025-12-12T20:31:42.861Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/4f/af/72ad54402e599152de6d067324c46fe6a4f531c7c65baf7e96c63db55eaf/flask_cors-6.0.2-py3-none-any.whl", hash = "sha256:e57544d415dfd7da89a9564e1e3a9e515042df76e12130641ca6f3f2f03b699a", size = 13257, upload-time = "2025-12-12T20:31:41.3Z" },
]
[[package]]
name = "itsdangerous"
version = "2.2.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/9c/cb/8ac0172223afbccb63986cc25049b154ecfb5e85932587206f42317be31d/itsdangerous-2.2.0.tar.gz", hash = "sha256:e0050c0b7da1eea53ffaf149c0cfbb5c6e2e2b69c4bef22c81fa6eb73e5f6173", size = 54410, upload-time = "2024-04-16T21:28:15.614Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/04/96/92447566d16df59b2a776c0fb82dbc4d9e07cd95062562af01e408583fc4/itsdangerous-2.2.0-py3-none-any.whl", hash = "sha256:c6242fc49e35958c8b15141343aa660db5fc54d4f13a1db01a3f5891b98700ef", size = 16234, upload-time = "2024-04-16T21:28:14.499Z" },
]
[[package]]
name = "jinja2"
version = "3.1.6"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "markupsafe" },
]
sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" },
]
[[package]]
name = "markupsafe"
version = "3.0.3"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", size = 80313, upload-time = "2025-09-27T18:37:40.426Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/5a/72/147da192e38635ada20e0a2e1a51cf8823d2119ce8883f7053879c2199b5/markupsafe-3.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d53197da72cc091b024dd97249dfc7794d6a56530370992a5e1a08983ad9230e", size = 11615, upload-time = "2025-09-27T18:36:30.854Z" },
{ url = "https://files.pythonhosted.org/packages/9a/81/7e4e08678a1f98521201c3079f77db69fb552acd56067661f8c2f534a718/markupsafe-3.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1872df69a4de6aead3491198eaf13810b565bdbeec3ae2dc8780f14458ec73ce", size = 12020, upload-time = "2025-09-27T18:36:31.971Z" },
{ url = "https://files.pythonhosted.org/packages/1e/2c/799f4742efc39633a1b54a92eec4082e4f815314869865d876824c257c1e/markupsafe-3.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a7e8ae81ae39e62a41ec302f972ba6ae23a5c5396c8e60113e9066ef893da0d", size = 24332, upload-time = "2025-09-27T18:36:32.813Z" },
{ url = "https://files.pythonhosted.org/packages/3c/2e/8d0c2ab90a8c1d9a24f0399058ab8519a3279d1bd4289511d74e909f060e/markupsafe-3.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d6dd0be5b5b189d31db7cda48b91d7e0a9795f31430b7f271219ab30f1d3ac9d", size = 22947, upload-time = "2025-09-27T18:36:33.86Z" },
{ url = "https://files.pythonhosted.org/packages/2c/54/887f3092a85238093a0b2154bd629c89444f395618842e8b0c41783898ea/markupsafe-3.0.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:94c6f0bb423f739146aec64595853541634bde58b2135f27f61c1ffd1cd4d16a", size = 21962, upload-time = "2025-09-27T18:36:35.099Z" },
{ url = "https://files.pythonhosted.org/packages/c9/2f/336b8c7b6f4a4d95e91119dc8521402461b74a485558d8f238a68312f11c/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:be8813b57049a7dc738189df53d69395eba14fb99345e0a5994914a3864c8a4b", size = 23760, upload-time = "2025-09-27T18:36:36.001Z" },
{ url = "https://files.pythonhosted.org/packages/32/43/67935f2b7e4982ffb50a4d169b724d74b62a3964bc1a9a527f5ac4f1ee2b/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:83891d0e9fb81a825d9a6d61e3f07550ca70a076484292a70fde82c4b807286f", size = 21529, upload-time = "2025-09-27T18:36:36.906Z" },
{ url = "https://files.pythonhosted.org/packages/89/e0/4486f11e51bbba8b0c041098859e869e304d1c261e59244baa3d295d47b7/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:77f0643abe7495da77fb436f50f8dab76dbc6e5fd25d39589a0f1fe6548bfa2b", size = 23015, upload-time = "2025-09-27T18:36:37.868Z" },
{ url = "https://files.pythonhosted.org/packages/2f/e1/78ee7a023dac597a5825441ebd17170785a9dab23de95d2c7508ade94e0e/markupsafe-3.0.3-cp312-cp312-win32.whl", hash = "sha256:d88b440e37a16e651bda4c7c2b930eb586fd15ca7406cb39e211fcff3bf3017d", size = 14540, upload-time = "2025-09-27T18:36:38.761Z" },
{ url = "https://files.pythonhosted.org/packages/aa/5b/bec5aa9bbbb2c946ca2733ef9c4ca91c91b6a24580193e891b5f7dbe8e1e/markupsafe-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:26a5784ded40c9e318cfc2bdb30fe164bdb8665ded9cd64d500a34fb42067b1c", size = 15105, upload-time = "2025-09-27T18:36:39.701Z" },
{ url = "https://files.pythonhosted.org/packages/e5/f1/216fc1bbfd74011693a4fd837e7026152e89c4bcf3e77b6692fba9923123/markupsafe-3.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:35add3b638a5d900e807944a078b51922212fb3dedb01633a8defc4b01a3c85f", size = 13906, upload-time = "2025-09-27T18:36:40.689Z" },
{ url = "https://files.pythonhosted.org/packages/38/2f/907b9c7bbba283e68f20259574b13d005c121a0fa4c175f9bed27c4597ff/markupsafe-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795", size = 11622, upload-time = "2025-09-27T18:36:41.777Z" },
{ url = "https://files.pythonhosted.org/packages/9c/d9/5f7756922cdd676869eca1c4e3c0cd0df60ed30199ffd775e319089cb3ed/markupsafe-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219", size = 12029, upload-time = "2025-09-27T18:36:43.257Z" },
{ url = "https://files.pythonhosted.org/packages/00/07/575a68c754943058c78f30db02ee03a64b3c638586fba6a6dd56830b30a3/markupsafe-3.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6", size = 24374, upload-time = "2025-09-27T18:36:44.508Z" },
{ url = "https://files.pythonhosted.org/packages/a9/21/9b05698b46f218fc0e118e1f8168395c65c8a2c750ae2bab54fc4bd4e0e8/markupsafe-3.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676", size = 22980, upload-time = "2025-09-27T18:36:45.385Z" },
{ url = "https://files.pythonhosted.org/packages/7f/71/544260864f893f18b6827315b988c146b559391e6e7e8f7252839b1b846a/markupsafe-3.0.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9", size = 21990, upload-time = "2025-09-27T18:36:46.916Z" },
{ url = "https://files.pythonhosted.org/packages/c2/28/b50fc2f74d1ad761af2f5dcce7492648b983d00a65b8c0e0cb457c82ebbe/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1", size = 23784, upload-time = "2025-09-27T18:36:47.884Z" },
{ url = "https://files.pythonhosted.org/packages/ed/76/104b2aa106a208da8b17a2fb72e033a5a9d7073c68f7e508b94916ed47a9/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc", size = 21588, upload-time = "2025-09-27T18:36:48.82Z" },
{ url = "https://files.pythonhosted.org/packages/b5/99/16a5eb2d140087ebd97180d95249b00a03aa87e29cc224056274f2e45fd6/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12", size = 23041, upload-time = "2025-09-27T18:36:49.797Z" },
{ url = "https://files.pythonhosted.org/packages/19/bc/e7140ed90c5d61d77cea142eed9f9c303f4c4806f60a1044c13e3f1471d0/markupsafe-3.0.3-cp313-cp313-win32.whl", hash = "sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed", size = 14543, upload-time = "2025-09-27T18:36:51.584Z" },
{ url = "https://files.pythonhosted.org/packages/05/73/c4abe620b841b6b791f2edc248f556900667a5a1cf023a6646967ae98335/markupsafe-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5", size = 15113, upload-time = "2025-09-27T18:36:52.537Z" },
{ url = "https://files.pythonhosted.org/packages/f0/3a/fa34a0f7cfef23cf9500d68cb7c32dd64ffd58a12b09225fb03dd37d5b80/markupsafe-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485", size = 13911, upload-time = "2025-09-27T18:36:53.513Z" },
{ url = "https://files.pythonhosted.org/packages/e4/d7/e05cd7efe43a88a17a37b3ae96e79a19e846f3f456fe79c57ca61356ef01/markupsafe-3.0.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73", size = 11658, upload-time = "2025-09-27T18:36:54.819Z" },
{ url = "https://files.pythonhosted.org/packages/99/9e/e412117548182ce2148bdeacdda3bb494260c0b0184360fe0d56389b523b/markupsafe-3.0.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37", size = 12066, upload-time = "2025-09-27T18:36:55.714Z" },
{ url = "https://files.pythonhosted.org/packages/bc/e6/fa0ffcda717ef64a5108eaa7b4f5ed28d56122c9a6d70ab8b72f9f715c80/markupsafe-3.0.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19", size = 25639, upload-time = "2025-09-27T18:36:56.908Z" },
{ url = "https://files.pythonhosted.org/packages/96/ec/2102e881fe9d25fc16cb4b25d5f5cde50970967ffa5dddafdb771237062d/markupsafe-3.0.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025", size = 23569, upload-time = "2025-09-27T18:36:57.913Z" },
{ url = "https://files.pythonhosted.org/packages/4b/30/6f2fce1f1f205fc9323255b216ca8a235b15860c34b6798f810f05828e32/markupsafe-3.0.3-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6", size = 23284, upload-time = "2025-09-27T18:36:58.833Z" },
{ url = "https://files.pythonhosted.org/packages/58/47/4a0ccea4ab9f5dcb6f79c0236d954acb382202721e704223a8aafa38b5c8/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f", size = 24801, upload-time = "2025-09-27T18:36:59.739Z" },
{ url = "https://files.pythonhosted.org/packages/6a/70/3780e9b72180b6fecb83a4814d84c3bf4b4ae4bf0b19c27196104149734c/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb", size = 22769, upload-time = "2025-09-27T18:37:00.719Z" },
{ url = "https://files.pythonhosted.org/packages/98/c5/c03c7f4125180fc215220c035beac6b9cb684bc7a067c84fc69414d315f5/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009", size = 23642, upload-time = "2025-09-27T18:37:01.673Z" },
{ url = "https://files.pythonhosted.org/packages/80/d6/2d1b89f6ca4bff1036499b1e29a1d02d282259f3681540e16563f27ebc23/markupsafe-3.0.3-cp313-cp313t-win32.whl", hash = "sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354", size = 14612, upload-time = "2025-09-27T18:37:02.639Z" },
{ url = "https://files.pythonhosted.org/packages/2b/98/e48a4bfba0a0ffcf9925fe2d69240bfaa19c6f7507b8cd09c70684a53c1e/markupsafe-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218", size = 15200, upload-time = "2025-09-27T18:37:03.582Z" },
{ url = "https://files.pythonhosted.org/packages/0e/72/e3cc540f351f316e9ed0f092757459afbc595824ca724cbc5a5d4263713f/markupsafe-3.0.3-cp313-cp313t-win_arm64.whl", hash = "sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287", size = 13973, upload-time = "2025-09-27T18:37:04.929Z" },
{ url = "https://files.pythonhosted.org/packages/33/8a/8e42d4838cd89b7dde187011e97fe6c3af66d8c044997d2183fbd6d31352/markupsafe-3.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe", size = 11619, upload-time = "2025-09-27T18:37:06.342Z" },
{ url = "https://files.pythonhosted.org/packages/b5/64/7660f8a4a8e53c924d0fa05dc3a55c9cee10bbd82b11c5afb27d44b096ce/markupsafe-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026", size = 12029, upload-time = "2025-09-27T18:37:07.213Z" },
{ url = "https://files.pythonhosted.org/packages/da/ef/e648bfd021127bef5fa12e1720ffed0c6cbb8310c8d9bea7266337ff06de/markupsafe-3.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737", size = 24408, upload-time = "2025-09-27T18:37:09.572Z" },
{ url = "https://files.pythonhosted.org/packages/41/3c/a36c2450754618e62008bf7435ccb0f88053e07592e6028a34776213d877/markupsafe-3.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97", size = 23005, upload-time = "2025-09-27T18:37:10.58Z" },
{ url = "https://files.pythonhosted.org/packages/bc/20/b7fdf89a8456b099837cd1dc21974632a02a999ec9bf7ca3e490aacd98e7/markupsafe-3.0.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d", size = 22048, upload-time = "2025-09-27T18:37:11.547Z" },
{ url = "https://files.pythonhosted.org/packages/9a/a7/591f592afdc734f47db08a75793a55d7fbcc6902a723ae4cfbab61010cc5/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda", size = 23821, upload-time = "2025-09-27T18:37:12.48Z" },
{ url = "https://files.pythonhosted.org/packages/7d/33/45b24e4f44195b26521bc6f1a82197118f74df348556594bd2262bda1038/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf", size = 21606, upload-time = "2025-09-27T18:37:13.485Z" },
{ url = "https://files.pythonhosted.org/packages/ff/0e/53dfaca23a69fbfbbf17a4b64072090e70717344c52eaaaa9c5ddff1e5f0/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe", size = 23043, upload-time = "2025-09-27T18:37:14.408Z" },
{ url = "https://files.pythonhosted.org/packages/46/11/f333a06fc16236d5238bfe74daccbca41459dcd8d1fa952e8fbd5dccfb70/markupsafe-3.0.3-cp314-cp314-win32.whl", hash = "sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9", size = 14747, upload-time = "2025-09-27T18:37:15.36Z" },
{ url = "https://files.pythonhosted.org/packages/28/52/182836104b33b444e400b14f797212f720cbc9ed6ba34c800639d154e821/markupsafe-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581", size = 15341, upload-time = "2025-09-27T18:37:16.496Z" },
{ url = "https://files.pythonhosted.org/packages/6f/18/acf23e91bd94fd7b3031558b1f013adfa21a8e407a3fdb32745538730382/markupsafe-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4", size = 14073, upload-time = "2025-09-27T18:37:17.476Z" },
{ url = "https://files.pythonhosted.org/packages/3c/f0/57689aa4076e1b43b15fdfa646b04653969d50cf30c32a102762be2485da/markupsafe-3.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab", size = 11661, upload-time = "2025-09-27T18:37:18.453Z" },
{ url = "https://files.pythonhosted.org/packages/89/c3/2e67a7ca217c6912985ec766c6393b636fb0c2344443ff9d91404dc4c79f/markupsafe-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175", size = 12069, upload-time = "2025-09-27T18:37:19.332Z" },
{ url = "https://files.pythonhosted.org/packages/f0/00/be561dce4e6ca66b15276e184ce4b8aec61fe83662cce2f7d72bd3249d28/markupsafe-3.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634", size = 25670, upload-time = "2025-09-27T18:37:20.245Z" },
{ url = "https://files.pythonhosted.org/packages/50/09/c419f6f5a92e5fadde27efd190eca90f05e1261b10dbd8cbcb39cd8ea1dc/markupsafe-3.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50", size = 23598, upload-time = "2025-09-27T18:37:21.177Z" },
{ url = "https://files.pythonhosted.org/packages/22/44/a0681611106e0b2921b3033fc19bc53323e0b50bc70cffdd19f7d679bb66/markupsafe-3.0.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e", size = 23261, upload-time = "2025-09-27T18:37:22.167Z" },
{ url = "https://files.pythonhosted.org/packages/5f/57/1b0b3f100259dc9fffe780cfb60d4be71375510e435efec3d116b6436d43/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5", size = 24835, upload-time = "2025-09-27T18:37:23.296Z" },
{ url = "https://files.pythonhosted.org/packages/26/6a/4bf6d0c97c4920f1597cc14dd720705eca0bf7c787aebc6bb4d1bead5388/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523", size = 22733, upload-time = "2025-09-27T18:37:24.237Z" },
{ url = "https://files.pythonhosted.org/packages/14/c7/ca723101509b518797fedc2fdf79ba57f886b4aca8a7d31857ba3ee8281f/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc", size = 23672, upload-time = "2025-09-27T18:37:25.271Z" },
{ url = "https://files.pythonhosted.org/packages/fb/df/5bd7a48c256faecd1d36edc13133e51397e41b73bb77e1a69deab746ebac/markupsafe-3.0.3-cp314-cp314t-win32.whl", hash = "sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d", size = 14819, upload-time = "2025-09-27T18:37:26.285Z" },
{ url = "https://files.pythonhosted.org/packages/1a/8a/0402ba61a2f16038b48b39bccca271134be00c5c9f0f623208399333c448/markupsafe-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9", size = 15426, upload-time = "2025-09-27T18:37:27.316Z" },
{ url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146, upload-time = "2025-09-27T18:37:28.327Z" },
]
[[package]]
name = "werkzeug"
version = "3.1.6"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "markupsafe" },
]
sdist = { url = "https://files.pythonhosted.org/packages/61/f1/ee81806690a87dab5f5653c1f146c92bc066d7f4cebc603ef88eb9e13957/werkzeug-3.1.6.tar.gz", hash = "sha256:210c6bede5a420a913956b4791a7f4d6843a43b6fcee4dfa08a65e93007d0d25", size = 864736, upload-time = "2026-02-19T15:17:18.884Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/4d/ec/d58832f89ede95652fd01f4f24236af7d32b70cab2196dfcc2d2fd13c5c2/werkzeug-3.1.6-py3-none-any.whl", hash = "sha256:7ddf3357bb9564e407607f988f683d72038551200c704012bb9a4c523d42f131", size = 225166, upload-time = "2026-02-19T15:17:17.475Z" },
]

View File

@ -1,270 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Blog 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);
line-height: var(--font-lineheight-3);
margin: 0;
display: flex;
flex-direction: column;
min-height: 100vh;
}
/* 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: 1400px;
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;
padding: var(--size-1) var(--size-2);
border-radius: var(--radius-2);
}
.site-nav a:hover,
.site-nav a.active {
color: var(--indigo-4);
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 */
.blog-container {
max-width: 1200px;
margin: var(--size-6) auto;
padding: 0 var(--size-4);
}
.blog-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: var(--size-6);
}
.blog-header h1 {
margin: 0;
font-size: var(--font-size-6);
color: var(--indigo-8);
}
.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);
background: var(--indigo-7);
color: white;
text-decoration: none;
}
.btn:hover {
background: var(--indigo-8);
box-shadow: var(--shadow-3);
}
.posts-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: var(--size-6);
}
.post-card {
background: var(--surface-1);
border-radius: var(--radius-3);
overflow: hidden;
box-shadow: var(--shadow-2);
transition: transform 0.2s;
}
.post-card:hover {
transform: translateY(-4px);
}
.post-card-image {
width: 100%;
height: 200px;
object-fit: cover;
background: var(--surface-3);
}
.post-card-content {
padding: var(--size-4);
}
.post-card-title {
margin: 0 0 var(--size-2) 0;
font-size: var(--font-size-4);
}
.post-card-title a {
color: var(--gray-9);
text-decoration: none;
}
.post-card-title a:hover {
color: var(--indigo-7);
}
.post-card-meta {
color: var(--gray-6);
font-size: var(--font-size-1);
margin-bottom: var(--size-3);
}
.post-card-excerpt {
color: var(--gray-7);
line-height: var(--font-lineheight-3);
}
.empty-state {
text-align: center;
color: var(--gray-6);
padding: var(--size-8);
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>
</head>
<body>
<header class="site-header">
<div class="container">
<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">
<a href="map-page.html">Map</a>
<a href="blog-list.html" class="active">Blog</a>
</nav>
<div class="user-menu" id="user-menu"></div>
</div>
</div>
</header>
<main class="blog-container">
<div class="blog-header">
<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>
</div>
<div id="posts-grid" class="posts-grid">
<!-- Posts loaded dynamically -->
</div>
</main>
<div id="toast" class="toast"></div>
<script src="js/auth.js"></script>
<script>
// ==================== BLOG POSTS ====================
async function loadPosts() {
try {
const res = await fetch(`${API_BASE}/blog-posts`, { credentials: 'include' });
if (!res.ok) throw new Error('Failed to fetch posts');
const posts = await res.json();
renderPosts(posts);
} catch (err) {
console.error(err);
document.getElementById('posts-grid').innerHTML = '<p class="empty-state">Failed to load posts. Make sure the backend is running.</p>';
}
}
function renderPosts(posts) {
const container = document.getElementById('posts-grid');
if (!posts.length) {
container.innerHTML = '<p class="empty-state">No posts yet. Click "New Post" to create one.</p>';
return;
}
container.innerHTML = posts.map(post => `
<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>'}
<div class="post-card-content">
<h2 class="post-card-title"><a href="blog-post-edit.html?id=${post.id}">${escapeHtml(post.title)}</a></h2>
<div class="post-card-meta">
<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>` : ''}
</div>
<div class="post-card-excerpt">${escapeHtml(post.excerpt || post.content.substring(0, 150) + '…')}</div>
</div>
</article>
`).join('');
}
// ==================== INITIALIZATION ====================
document.addEventListener('DOMContentLoaded', async () => {
const authenticated = await checkAuthAndRedirect();
if (!authenticated) return;
updateUserMenu();
loadPosts();
});
</script>
</body>
</html>

View File

@ -1,369 +0,0 @@
<!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 -->
<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);
line-height: var(--font-lineheight-3);
margin: 0;
display: flex;
flex-direction: column;
min-height: 100vh;
}
/* 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: 1400px;
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;
padding: var(--size-1) var(--size-2);
border-radius: var(--radius-2);
}
.site-nav a:hover {
color: var(--indigo-4);
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 */
.post-container {
max-width: 800px;
margin: var(--size-6) auto;
padding: 0 var(--size-4);
}
.post-form {
background: var(--surface-1);
border-radius: var(--radius-3);
padding: var(--size-6);
box-shadow: var(--shadow-2);
}
.form-group {
margin-bottom: var(--size-4);
}
label {
display: block;
margin-bottom: var(--size-2);
font-weight: var(--font-weight-6);
color: var(--gray-8);
}
input[type="text"],
textarea,
select {
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);
}
textarea {
min-height: 200px;
resize: vertical;
}
.image-preview {
margin-top: var(--size-2);
max-width: 100%;
border-radius: var(--radius-2);
overflow: hidden;
}
.image-preview img {
max-width: 100%;
height: auto;
display: block;
}
.button-group {
display: flex;
gap: var(--size-3);
margin-top: var(--size-6);
}
.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);
background: var(--gray-7);
color: white;
text-decoration: none;
}
.btn-primary {
background: var(--indigo-7);
}
.btn-primary:hover {
background: var(--indigo-8);
}
.btn-danger {
background: var(--red-7);
}
.btn-danger:hover {
background: var(--red-8);
}
.btn:hover {
box-shadow: var(--shadow-3);
}
.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>
<header class="site-header">
<div class="container">
<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">
<a href="map-page.html">Map</a>
<a href="blog-list.html" class="active">Blog</a>
</nav>
<div class="user-menu" id="user-menu"></div>
</div>
</div>
</header>
<main class="post-container">
<div class="post-form">
<h2 id="form-title">Edit Post</h2>
<form id="post-form">
<div class="form-group">
<label for="post-title">Title</label>
<input type="text" id="post-title" required />
</div>
<div class="form-group">
<label for="post-content">Content</label>
<textarea id="post-content" rows="10" required></textarea>
</div>
<div class="form-group">
<label for="post-journey">Associated Journey ID (optional)</label>
<input type="text" id="post-journey" placeholder="e.g., 3" />
</div>
<div class="form-group">
<label for="post-image">Image URL or Upload</label>
<input type="text" id="post-image-url" placeholder="https://..." />
<div style="margin: 8px 0; text-align: center">or</div>
<input type="file" id="image-upload" accept="image/*" />
<div class="image-preview" id="image-preview"></div>
</div>
<div class="button-group">
<button type="submit" class="btn btn-primary"><i class="fas fa-save"></i> Save</button>
<button type="button" id="delete-post" class="btn btn-danger"><i class="fas fa-trash"></i> Delete</button>
<a href="blog-list.html" class="btn"><i class="fas fa-times"></i> Cancel</a>
</div>
</form>
</div>
</main>
<div id="toast" class="toast"></div>
<script src="js/auth.js"></script>
<script>
let currentPostId = null;
// ==================== POST CRUD ====================
const urlParams = new URLSearchParams(window.location.search);
const postId = urlParams.get('id');
const isNew = urlParams.has('new');
async function loadPost(id) {
try {
const res = await fetch(`${API_BASE}/blog-posts/${id}`, { credentials: 'include' });
if (!res.ok) throw new Error('Post not found');
const post = await res.json();
currentPostId = post.id;
document.getElementById('post-title').value = post.title;
document.getElementById('post-content').value = post.content;
document.getElementById('post-journey').value = post.journeyId || '';
document.getElementById('post-image-url').value = post.image || '';
updateImagePreview(post.image);
document.getElementById('form-title').textContent = 'Edit Post';
} catch (err) {
showToast('Error loading post', true);
}
}
function updateImagePreview(url) {
const preview = document.getElementById('image-preview');
if (url) {
preview.innerHTML = `<img src="${url}" alt="Preview" style="max-width:100%; border-radius: var(--radius-2);">`;
} else {
preview.innerHTML = '';
}
}
async function savePost(event) {
event.preventDefault();
const title = document.getElementById('post-title').value.trim();
const content = document.getElementById('post-content').value.trim();
const journeyId = document.getElementById('post-journey').value.trim();
let image = document.getElementById('post-image-url').value.trim();
if (!title || !content) {
showToast('Title and content are required');
return;
}
const payload = { title, content, journeyId: journeyId || null, image: image || null };
try {
let url, method;
if (isNew) {
url = `${API_BASE}/blog-posts`;
method = 'POST';
} else {
url = `${API_BASE}/blog-posts/${currentPostId}`;
method = 'PUT';
}
const res = await fetch(url, {
method,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
credentials: 'include'
});
if (!res.ok) throw new Error('Save failed');
const data = await res.json();
showToast('Post saved successfully');
if (isNew) {
window.location.href = `blog-post-edit.html?id=${data.id}`;
}
} catch (err) {
showToast('Error saving post', true);
}
}
async function deletePost() {
if (!currentPostId) return;
if (!confirm('Delete this post?')) return;
try {
const res = await fetch(`${API_BASE}/blog-posts/${currentPostId}`, {
method: 'DELETE',
credentials: 'include'
});
if (!res.ok) throw new Error('Delete failed');
showToast('Post deleted');
setTimeout(() => { window.location.href = 'blog-list.html'; }, 1000);
} catch (err) {
showToast('Error deleting post', true);
}
}
// Image upload (convert to base64)
document.getElementById('image-upload').addEventListener('change', function(e) {
const file = e.target.files[0];
if (!file) return;
const reader = new FileReader();
reader.onload = function(evt) {
const base64 = evt.target.result;
document.getElementById('post-image-url').value = base64;
updateImagePreview(base64);
};
reader.readAsDataURL(file);
});
document.getElementById('post-form').addEventListener('submit', savePost);
document.getElementById('delete-post').addEventListener('click', deletePost);
// ==================== INITIALIZATION ====================
document.addEventListener('DOMContentLoaded', async () => {
const authenticated = await checkAuthAndRedirect();
if (!authenticated) return;
updateUserMenu();
if (!isNew && postId) {
loadPost(postId);
} else if (isNew) {
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>
</body>
</html>

View File

@ -1,154 +0,0 @@
/* Blog page styles - mobile first, matching existing palette */
/* Layout container override (keeps existing .container behavior) */
.post-page {
display: flex;
flex-direction: column;
gap: 24px;
padding: 20px 16px;
}
.post {
background: #ffffff;
border-radius: 10px;
padding: 18px;
box-shadow: 0 6px 18px rgba(37, 51, 66, 0.06);
max-width: 900px;
margin: 0 auto;
}
.post-title {
font-size: 1.6rem;
margin: 0 0 8px 0;
color: #253342;
font-weight: 600;
}
.post-meta {
color: #7f8c8d;
font-size: 0.9rem;
margin-bottom: 12px;
}
.post-hero img {
display: block;
width: 100%;
height: auto;
border-radius: 8px;
margin: 12px 0;
}
.post-content p {
line-height: 1.7;
color: #2c3e50;
margin: 0 0 1rem 0;
}
.sidebar {
margin: 0 auto;
max-width: 320px;
}
.sidebar .about {
background: #f8f9fa;
padding: 14px;
border-radius: 8px;
color: #2c3e50;
}
.comments {
max-width: 900px;
margin: 0 auto 32px auto;
padding: 0 16px;
}
.comments-list {
margin-top: 12px;
display: flex;
flex-direction: column;
gap: 12px;
}
.comment-item {
background: #ffffff;
border-radius: 8px;
padding: 12px;
border: 1px solid rgba(37, 51, 66, 0.06);
}
.comment-item .meta {
color: #7f8c8d;
font-size: 0.85rem;
margin-bottom: 6px;
}
.site-header {
background-color: #253342;
color: #ecf0f1;
padding: 14px 16px;
}
.site-header .site-title {
margin: 0;
font-size: 1.1rem;
}
.site-header .site-title a {
color: #3498db;
text-decoration: none;
}
.site-nav {
margin-top: 8px;
display: flex;
gap: 12px;
}
.site-nav a {
color: #ecf0f1;
text-decoration: none;
font-weight: 500;
}
.site-footer {
background: #f7f8f9;
padding: 16px;
margin-top: 30px;
}
/* Larger screens: two-column layout */
@media (min-width: 768px) {
.post-page {
flex-direction: row;
align-items: flex-start;
gap: 28px;
padding: 28px;
}
.post {
flex: 1 1 0;
margin: 0;
}
.sidebar {
width: 320px;
flex: 0 0 320px;
margin: 0;
align-self: flex-start;
}
}
/* Small visual tweaks to match the rest of the site */
.post h3, .sidebar h3 {
color: #253342;
margin-top: 0;
}
.btn-inline {
display: inline-block;
padding: 8px 12px;
background: #3498db;
color: white;
border-radius: 6px;
text-decoration: none;
}

View File

@ -1,586 +0,0 @@
/* Map Page Specific Styles */
/* App Layout */
.app-container {
display: flex;
height: 100vh;
overflow: hidden;
}
.sidebar {
width: 300px;
min-width: 300px;
background-color: #253342;
color: white;
padding: 20px;
}
.map-area {
flex: 1;
width: calc(100% - 300px);
}
#map {
width: 100%;
height: 100vh;
}
.map-area {
flex: 1;
}
.sidebar-header {
padding: 20px;
border-bottom: 1px solid #34495e;
}
.sidebar-header h1 {
margin: 0;
font-size: 1.5rem;
display: flex;
align-items: center;
gap: 10px;
}
.tagline {
margin: 5px 0 0;
font-size: 0.85rem;
color: #bdc3c7;
font-family: 'Roboto', sans-serif;
}
/* Mode Selector */
.mode-selector {
display: flex;
padding: 15px;
gap: 10px;
border-bottom: 1px solid #34495e;
}
.mode-btn {
flex: 1;
padding: 10px;
background-color: #34495e;
border: none;
color: #ecf0f1;
border-radius: 5px;
cursor: pointer;
font-family: 'Poppins', sans-serif;
font-weight: 500;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
transition: all 0.2s ease;
}
.mode-btn:hover {
background-color: #3d566e;
}
.mode-btn.active {
background-color: #3498db;
box-shadow: 0 2px 5px rgba(52, 152, 219, 0.3);
}
/* Panels */
.panel {
padding: 20px;
border-bottom: 1px solid #34495e;
display: none;
}
.panel.active-panel {
display: block;
}
.panel h3 {
margin-top: 0;
display: flex;
align-items: center;
gap: 10px;
font-size: 1.2rem;
}
/* Form Styles */
.form-group {
margin-bottom: 15px;
}
.form-group label {
display: block;
margin-bottom: 5px;
font-weight: 500;
display: flex;
align-items: center;
gap: 5px;
}
.form-group input,
.form-group textarea,
.form-group select {
width: 100%;
padding: 10px;
border-radius: 5px;
border: 1px solid #34495e;
background-color: #34495e;
color: #ecf0f1;
font-family: 'Roboto', sans-serif;
}
.form-group textarea {
min-height: 80px;
resize: vertical;
}
/* Instructions */
.instructions {
background-color: rgba(52, 73, 94, 0.5);
border-radius: 5px;
padding: 15px;
margin: 15px 0;
}
.instructions h4 {
margin-top: 0;
display: flex;
align-items: center;
gap: 8px;
}
.instructions ol {
margin: 10px 0 0;
padding-left: 20px;
}
.instructions li {
margin-bottom: 5px;
font-size: 0.9rem;
}
/* Buttons */
.button-group {
display: flex;
gap: 10px;
margin-top: 15px;
}
.btn {
padding: 10px 15px;
border: none;
border-radius: 5px;
cursor: pointer;
font-family: 'Poppins', sans-serif;
font-weight: 500;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
transition: all 0.2s ease;
}
.btn-primary {
background-color: #3498db;
color: white;
}
.btn-primary:hover {
background-color: #2980b9;
}
.btn-secondary {
background-color: #7f8c8d;
color: white;
}
.btn-secondary:hover {
background-color: #6c7b7d;
}
.btn-danger {
background-color: #e74c3c;
color: white;
}
.btn-danger:hover {
background-color: #c0392b;
}
.btn-small {
padding: 5px 10px;
font-size: 0.85rem;
}
/* Filter Options */
.filter-options {
margin: 15px 0;
}
.checkbox-group {
display: flex;
flex-direction: column;
gap: 8px;
}
.checkbox-group label {
display: flex;
align-items: center;
gap: 8px;
cursor: pointer;
}
/* Journey Info */
.journey-info {
background-color: rgba(52, 73, 94, 0.5);
border-radius: 5px;
padding: 15px;
margin-top: 15px;
}
.info-content p {
margin: 8px 0;
}
/* Markers List */
.markers-list {
flex: 1;
padding: 20px;
overflow-y: auto;
}
.markers-list h3 {
display: flex;
align-items: center;
gap: 10px;
margin-top: 0;
}
#markers-container {
margin-top: 15px;
}
.empty-message {
text-align: center;
color: #7f8c8d;
font-style: italic;
padding: 20px;
}
/* Journey Panel Styles */
.journey-panel {
padding: 20px;
}
.journey-form .form-group {
margin-bottom: 15px;
}
.journey-form label {
display: block;
margin-bottom: 5px;
}
.marker-item {
padding: 8px;
margin: 3px 0;
background-color: #f8f9fa;
border-radius: 4px;
cursor: pointer;
transition: background-color 0.2s;
}
.marker-item:hover {
background-color: #e9ecef;
}
.button-group {
display: flex;
gap: 10px;
margin-top: 15px;
}
.btn {
padding: 8px 12px;
border: none;
border-radius: 4px;
cursor: pointer;
}
.btn-primary {
background-color: #3498db;
color: white;
}
.btn-success {
background-color: #2ecc71;
color: white;
}
.btn-danger {
background-color: #e74c3c;
color: white;
}
/* Footer */
.sidebar-footer {
padding: 15px 20px;
border-top: 1px solid #34495e;
background-color: #253342;
}
.navigation {
display: flex;
justify-content: space-around;
margin-bottom: 10px;
}
.nav-link {
color: #3498db;
text-decoration: none;
display: flex;
align-items: center;
gap: 5px;
font-size: 0.9rem;
}
.nav-link:hover {
text-decoration: underline;
}
.footer-text {
text-align: center;
font-size: 0.8rem;
color: #7f8c8d;
margin: 0;
}
#map {
width: 100%;
height: 100%;
}
/* Map Controls */
.map-controls {
position: absolute;
top: 20px;
right: 20px;
display: flex;
flex-direction: column;
gap: 10px;
z-index: 1;
}
.control-btn {
width: 40px;
height: 40px;
border-radius: 5px;
background-color: white;
border: none;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2);
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
font-size: 1rem;
color: #2c3e50;
transition: all 0.2s ease;
}
.control-btn:hover {
background-color: #f8f9fa;
transform: translateY(-2px);
}
/* Mode Indicator */
.mode-indicator {
position: absolute;
top: 20px;
left: 20px;
background-color: rgba(255, 255, 255, 0.9);
padding: 10px 15px;
border-radius: 5px;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
display: flex;
align-items: center;
gap: 10px;
z-index: 1;
}
.indicator-text {
font-weight: 500;
color: #2c3e50;
}
.indicator-dot {
width: 10px;
height: 10px;
border-radius: 50%;
}
.indicator-dot.creating {
background-color: #3498db;
}
.indicator-dot.viewing {
background-color: #2ecc71;
}
/* Modal */
.modal {
display: none;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
z-index: 100;
align-items: center;
justify-content: center;
}
.modal.active {
display: flex;
}
.modal-content {
background-color: white;
border-radius: 10px;
width: 90%;
max-width: 500px;
max-height: 90vh;
overflow-y: auto;
box-shadow: 0 5px 20px rgba(0, 0, 0, 0.2);
}
.modal-header {
padding: 20px;
border-bottom: 1px solid #eee;
display: flex;
justify-content: space-between;
align-items: center;
}
.modal-header h3 {
margin: 0;
display: flex;
align-items: center;
gap: 10px;
}
.close-btn {
background: none;
border: none;
font-size: 1.5rem;
cursor: pointer;
color: #7f8c8d;
}
.close-btn:hover {
color: #2c3e50;
}
.modal-body {
padding: 20px;
}
/* Image Upload */
.image-upload-area {
border: 2px dashed #ddd;
border-radius: 5px;
padding: 15px;
margin-top: 5px;
}
.image-preview {
display: flex;
flex-wrap: wrap;
gap: 10px;
margin-bottom: 15px;
min-height: 60px;
}
.upload-actions {
display: flex;
gap: 10px;
}
.help-text {
display: block;
margin-top: 5px;
color: #7f8c8d;
font-size: 0.85rem;
}
.coordinates {
padding: 10px;
background-color: #f8f9fa;
border-radius: 5px;
margin-top: 15px;
}
.modal-footer {
padding: 15px 20px;
border-top: 1px solid #eee;
display: flex;
gap: 10px;
justify-content: flex-end;
}
/* Toast */
.toast {
position: fixed;
bottom: 20px;
right: 20px;
background-color: #2ecc71;
color: white;
padding: 15px 20px;
border-radius: 5px;
box-shadow: 0 3px 10px rgba(0, 0, 0, 0.2);
display: none;
z-index: 100;
animation: slideIn 0.3s ease;
}
@keyframes slideIn {
from {
transform: translateX(100%);
opacity: 0;
}
to {
transform: translateX(0);
opacity: 1;
}
}
/* Responsive */
@media (max-width: 768px) {
.sidebar {
position: fixed;
top: 0;
left: 0;
height: 100%;
transform: translateX(-100%);
}
.sidebar.active {
transform: translateX(0);
}
.app-container {
flex-direction: column;
}
.map-controls {
top: auto;
bottom: 20px;
right: 20px;
flex-direction: row;
}
.mode-indicator {
top: 10px;
left: 10px;
font-size: 0.9rem;
}
}

View File

@ -1,47 +0,0 @@
/* Mobile-first approach */
.map-container {
height: 100vh;
width: 100%;
}
/* Tablet and larger */
@media (min-width: 768px) {
.sidebar {
width: 350px;
}
}
/* Journey timeline styles */
.journey-timeline {
padding: 20px;
max-width: 800px;
margin: 0 auto;
}
.timeline-event {
border-left: 3px solid #3498db;
padding: 15px 20px;
margin: 20px 0;
background-color: #f9f9f9;
}
.timeline-event .date {
color: #7f8c8d;
font-style: italic;
}
.timeline-event .location {
font-size: 0.9em;
color: #2c3e50;
}
.images-container {
display: flex;
gap: 10px;
flex-wrap: wrap;
}
.images-container img {
max-width: 100px;
height: auto;
}

View File

@ -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 "&amp;";
if (m === "<") return "&lt;";
if (m === ">") return "&gt;";
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);
}

View File

@ -1,174 +0,0 @@
/* Minimal blog-posts.js
- Stores posts in localStorage under `blogPosts`
- Posts: {id, title, content, image, journeyId, created_at}
- Supports create, edit, delete, view
*/
const STORAGE_KEY = 'blogPosts';
function loadPosts() {
try {
return JSON.parse(localStorage.getItem(STORAGE_KEY) || '[]');
} catch (e) {
console.error('Failed to parse posts', e);
return [];
}
}
function savePosts(posts) {
localStorage.setItem(STORAGE_KEY, JSON.stringify(posts));
}
function getNextId(posts) {
if (!posts || posts.length === 0) return 1;
return Math.max(...posts.map(p => p.id)) + 1;
}
function renderPostList() {
const posts = loadPosts().sort((a,b)=> new Date(b.created_at) - new Date(a.created_at));
const container = document.getElementById('posts-list');
container.innerHTML = '';
if (posts.length === 0) {
container.innerHTML = '<p class="empty-message">No posts yet.</p>';
return;
}
posts.forEach(post => {
const el = document.createElement('div');
el.className = 'marker-item';
el.innerHTML = `
<div style="display:flex;justify-content:space-between;align-items:center;gap:8px;">
<div style="flex:1">
<strong>${escapeHtml(post.title)}</strong>
<div style="font-size:.85rem;color:#7f8c8d">${new Date(post.created_at).toLocaleString()}${post.journeyId ? ' • Journey '+post.journeyId : ''}</div>
</div>
<div style="display:flex;gap:6px">
<button class="btn btn-small" data-action="view" data-id="${post.id}">View</button>
<button class="btn btn-small" data-action="edit" data-id="${post.id}">Edit</button>
<button class="btn btn-small btn-danger" data-action="delete" data-id="${post.id}">Delete</button>
</div>
</div>
`;
container.appendChild(el);
});
}
function escapeHtml(s){
return String(s || '').replace(/[&<>"']/g, (m)=>({
'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'
}[m]));
}
function showPost(id) {
const posts = loadPosts();
const post = posts.find(p=>p.id===id);
if (!post) return;
const titleEl = document.querySelector('.post-title');
const metaEl = document.querySelector('.post-meta');
const imgEl = document.querySelector('.post-hero img');
const contentEl = document.querySelector('.post-content');
titleEl.textContent = post.title;
metaEl.innerHTML = `By <span class="author">Author</span> — <time datetime="${post.created_at}">${new Date(post.created_at).toLocaleString()}</time>`;
if (post.image) imgEl.src = post.image; else imgEl.src = 'https://via.placeholder.com/800x350?text=Hero+Image';
contentEl.innerHTML = post.content;
}
function openForm(mode='create', post=null) {
const formWrap = document.getElementById('post-form');
formWrap.style.display = 'block';
document.getElementById('form-title').textContent = mode === 'create' ? 'New Post' : 'Edit Post';
const form = document.getElementById('blog-form');
form.dataset.mode = mode;
form.dataset.id = post ? post.id : '';
document.getElementById('post-title-input').value = post ? post.title : '';
document.getElementById('post-journey-input').value = post ? (post.journeyId || '') : '';
document.getElementById('post-image-input').value = post ? (post.image || '') : '';
document.getElementById('post-content-input').value = post ? post.content : '';
}
function closeForm() {
const formWrap = document.getElementById('post-form');
formWrap.style.display = 'none';
const form = document.getElementById('blog-form');
form.removeAttribute('data-id');
}
function deletePost(id) {
if (!confirm('Delete this post?')) return;
let posts = loadPosts();
posts = posts.filter(p=>p.id!==id);
savePosts(posts);
renderPostList();
// if the shown post was deleted, clear article
const currentTitle = document.querySelector('.post-title').textContent;
if (!posts.find(p=>p.title===currentTitle)) {
document.querySelector('.post-title').textContent = 'Post Title';
document.querySelector('.post-content').innerHTML = '<p>No post selected.</p>';
}
}
document.addEventListener('DOMContentLoaded', ()=>{
renderPostList();
document.getElementById('create-post-btn').addEventListener('click', ()=> openForm('create'));
document.getElementById('cancel-post-btn').addEventListener('click', (e)=>{ e.preventDefault(); closeForm(); });
document.getElementById('posts-list').addEventListener('click', (e)=>{
const btn = e.target.closest('button[data-action]');
if (!btn) return;
const id = parseInt(btn.dataset.id,10);
const action = btn.dataset.action;
if (action === 'view') showPost(id);
if (action === 'edit') {
const posts = loadPosts();
const post = posts.find(p=>p.id===id);
openForm('edit', post);
}
if (action === 'delete') deletePost(id);
});
document.getElementById('blog-form').addEventListener('submit', (e)=>{
e.preventDefault();
const form = e.target;
const mode = form.dataset.mode || 'create';
const id = parseInt(form.dataset.id,10) || null;
const title = document.getElementById('post-title-input').value.trim();
const journeyId = document.getElementById('post-journey-input').value.trim() || null;
const image = document.getElementById('post-image-input').value.trim() || '';
const content = document.getElementById('post-content-input').value.trim();
let posts = loadPosts();
if (mode === 'create') {
const newPost = {
id: getNextId(posts),
title,
content,
image,
journeyId: journeyId || null,
created_at: new Date().toISOString()
};
posts.push(newPost);
savePosts(posts);
renderPostList();
showPost(newPost.id);
} else if (mode === 'edit' && id) {
const idx = posts.findIndex(p=>p.id===id);
if (idx !== -1) {
posts[idx].title = title;
posts[idx].content = content;
posts[idx].image = image;
posts[idx].journeyId = journeyId || null;
savePosts(posts);
renderPostList();
showPost(id);
}
}
closeForm();
});
});

View File

@ -1,37 +0,0 @@
function displayJourney(journey) {
document.getElementById('journey-title').textContent = journey.name;
const container = document.getElementById('timeline-container');
container.innerHTML = '';
journey.markers.forEach(marker => {
const markerEl = document.createElement('div');
markerEl.className = 'timeline-event';
markerEl.innerHTML = `
<h3>${marker.content.title || 'Untitled Event'}</h3>
<p class="date">${marker.content.date}</p>
<p class="location">${marker.lngLat.lng.toFixed(4)}, ${marker.lngLat.lat.toFixed(4)}</p>
<p>${marker.content.text}</p>
${marker.content.videoUrl ? `<iframe src="${marker.content.videoUrl}" frameborder="0"></iframe>` : ''}
<div class="images-container">
${marker.content.images.map(img => `<img src="${img}" alt="Event image">`).join('')}
</div>
`;
container.appendChild(markerEl);
});
}
// Get journey ID from URL
const urlParams = new URLSearchParams(window.location.search);
const journeyId = urlParams.get('id');
// Load journey data from localStorage
if (journeyId) {
loadJourneysFromLocalStorage();
const journey = journeys.find(j => j.id === parseInt(journeyId));
if (journey) {
displayJourney(journey);
} else {
document.getElementById('journey-title').textContent = 'Journey not found';
}
}

View File

@ -1,553 +0,0 @@
// /Volumes/Data/Code/FHGR/Frontend/js/main.js
document.addEventListener('DOMContentLoaded', function() {
// Journey Management
let currentJourney = {
name: "",
description: "",
markers: []
};
function saveJourney() {
const journeyData = {
name: document.getElementById('journey-name').value,
description: document.getElementById('journey-desc').value,
markers: currentJourney.markers.map(marker => ({
lat: marker.getLatLng().lat,
lng: marker.getLatLng().lng
}))
};
// Save to backend
fetch('/api/journeys', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(journeyData)
})
.then(response => response.json())
.then(data => {
console.log('Journey saved:', data);
alert('Journey saved successfully!');
currentJourney = {
name: "",
description: "",
markers: []
};
document.getElementById('journey-name').value = '';
document.getElementById('journey-desc').value = '';
})
.catch(error => console.error('Error saving journey:', error));
}
function updateMarkersList() {
const container = document.getElementById('markers-list');
container.innerHTML = '';
currentJourney.markers.forEach((marker, index) => {
const markerElement = document.createElement('div');
markerElement.className = 'marker-item';
markerElement.innerHTML = `
<strong>${index + 1}</strong>
${marker.getLatLng().lat.toFixed(4)}, ${marker.getLngLat().lng.toFixed(4)}
`;
// Add click handler to focus on marker
markerElement.addEventListener('click', () => {
map.flyTo(marker.getLatLng(), 10);
});
container.appendChild(markerElement);
});
}
// Event Listeners
document.getElementById('save-journey').addEventListener('click', saveJourney);
// Initialize current journey when page loads
window.currentJourney = {
id: Date.now(),
name: "",
description: "",
markers: [],
path: null
};
// Function to prepare and save the journey
function prepareAndSaveJourney() {
const journeyData = {
name: document.getElementById('journey-title').value,
description: document.getElementById('journey-description').value,
markers: window.currentJourney.markers.map(marker => ({
id: marker.id,
lngLat: [marker.getLatLng().lat, marker.getLatLng().lng],
content: marker.content
}))
};
// Save to backend
fetch('http://localhost:5000/api/journeys', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(journeyData)
})
.then(response => response.json())
.then(data => {
alert('Journey saved successfully!');
window.currentJourney = {
id: Date.now(),
name: "",
description: "",
markers: [],
path: null
};
document.getElementById('journey-title').value = '';
document.getElementById('journey-description').value = '';
})
.catch(error => {
console.error('Error:', error);
alert('Failed to save journey. Please try again.');
});
}
// Event listeners for the buttons
document.getElementById('add-marker-btn').addEventListener('click', function() {
map.on('click', function(e) {
const marker = L.marker(e.latlng, {draggable: true}).addTo(map);
// Add popup with input field
marker.bindPopup('<input type="text" id="marker-title" placeholder="Enter title">');
window.currentJourney.markers.push(marker);
updateMarkersList();
});
});
document.getElementById('save-journey-btn').addEventListener('click', prepareAndSaveJourney);
document.getElementById('clear-markers-btn').addEventListener('click', function() {
map.eachLayer(function(layer) {
if (layer instanceof L.Marker) {
map.removeLayer(layer);
}
});
window.currentJourney.markers = [];
updateMarkersList();
});
function updateMarkersList() {
const markersContainer = document.getElementById('markers-container');
markersContainer.innerHTML = '';
if (window.currentJourney.markers.length === 0) {
markersContainer.innerHTML = '<p class="empty-message">No markers yet. Click on the map to add markers.</p>';
return;
}
window.currentJourney.markers.forEach((marker, index) => {
const markerElement = document.createElement('div');
markerElement.className = 'marker-item';
markerElement.innerHTML = `
<strong>${index + 1}</strong>
${marker.getLatLng().lat.toFixed(4)}, ${marker.getLngLat().lng.toFixed(4)}
`;
// Add click event to edit marker
markerElement.addEventListener('click', () => {
marker.openPopup();
});
markersContainer.appendChild(markerElement);
});
}
window.journeys = [];
window.isCreatingJourney = true;
// Initialize the map
const map = L.map('map').setView([8.5, 47.3], 10);
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: '© OpenStreetMap contributors'
}).addTo(map);
// Add navigation controls
L.control.scale().addTo(map);
// Add geolocate control
map.addControl(new maplibregl.GeolocateControl({
positionOptions: {
enableHighAccuracy: true
},
trackUserLocation: true
}));
// Add fullscreen control
map.addControl(L.control.fullscreen());
// Mode switching
const modeCreateBtn = document.getElementById('mode-create');
const modeViewBtn = document.getElementById('mode-view');
const createPanel = document.getElementById('create-panel');
const viewPanel = document.getElementById('view-panel');
const markersContainer = document.getElementById('markers-container');
const emptyMarkers = document.getElementById('empty-markers');
function switchMode(mode) {
if (mode === 'create') {
modeCreateBtn.classList.add('active');
modeViewBtn.classList.remove('active');
createPanel.classList.add('active-panel');
viewPanel.classList.remove('active-panel');
// Enable marker adding
window.isCreatingJourney = true;
} else { // view mode
modeCreateBtn.classList.remove('active');
modeViewBtn.classList.add('active');
createPanel.classList.remove('active-panel');
viewPanel.classList.add('active-panel');
// Disable marker adding
window.isCreatingJourney = false;
}
}
modeCreateBtn.addEventListener('click', () => switchMode('create'));
modeViewBtn.addEventListener('click', () => switchMode('view'));
async function loadJourneysFromBackend() {
try {
const response = await fetch('http://localhost:5000/api/journeys');
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const journeysData = await response.json();
// Ensure we always have an array
if (!Array.isArray(journeysData)) {
console.warn('Expected array of journeys, got:', journeysData);
window.journeys = [];
return [];
}
window.journeys = journeysData;
populateJourneySelect(journeysData);
return journeysData;
} catch (err) {
console.error('Error loading journeys from backend:', err);
// Fallback to local storage
const savedJourneys = localStorage.getItem('journeys');
if (savedJourneys) {
try {
const parsedJourneys = JSON.parse(savedJourneys);
if (Array.isArray(parsedJourneys)) {
window.journeys = parsedJourneys;
populateJourneySelect(window.journeys);
return window.journeys;
} else {
console.warn('Saved journeys are not an array:', parsedJourneys);
window.journeys = [];
populateJourneySelect([]);
return [];
}
} catch (parseError) {
console.error('Error parsing saved journeys:', parseError);
window.journeys = [];
populateJourneySelect([]);
return [];
}
}
// If no data available, initialize empty array
window.journeys = [];
populateJourneySelect([]);
return [];
}
}
async function saveJourneyToBackend(journey) {
try {
const response = await fetch('http://localhost:5000/api/journeys', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
id: journey.id,
name: journey.name,
description: journey.description,
markers: journey.markers.map(marker => ({
lngLat: marker.getLngLat().toArray(),
content: marker.content
}))
})
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const journeyData = await response.json();
console.log('Journey saved:', journeyData);
return journeyData;
} catch (err) {
console.error('Error saving journey to backend:', err);
// Fallback to local storage
const savedJourneys = localStorage.getItem('journeys') || '[]';
const journeys = JSON.parse(savedJourneys);
// Update or add the journey
const existingIndex = journeys.findIndex(j => j.id === journey.id);
if (existingIndex !== -1) {
journeys[existingIndex] = {
id: journey.id,
name: journey.name,
description: journey.description,
markers: journey.markers.map(marker => ({
id: marker.id,
lngLat: marker.getLngLat(),
content: marker.content
}))
};
} else {
journeys.push({
id: journey.id,
name: journey.name,
description: journey.description,
markers: journey.markers.map(marker => ({
id: marker.id,
lngLat: marker.getLngLat(),
content: marker.content
}))
});
}
localStorage.setItem('journeys', JSON.stringify(journeys));
console.log('Journey saved to local storage');
return journey;
}
}
async function loadCurrentJourneyFromBackend(journeyId) {
try {
const response = await fetch(`http://localhost:5000/api/journeys/${journeyId}`);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const journeyData = await response.json();
// Update the current journey object
window.currentJourney = {
id: journeyData.id,
name: journeyData.name,
description: journeyData.description,
markers: [],
path: null
};
// Create markers from the journey data
if (Array.isArray(journeyData.markers)) {
journeyData.markers.forEach(markerData => {
const lngLat = maplibregl.LngLat.fromArray(markerData.lngLat);
const marker = window.createMarker(lngLat, markerData.content);
window.currentJourney.markers.push(marker);
});
}
// Update the journey path
updateJourneyPath();
return journeyData;
} catch (err) {
console.error('Error loading journey from backend:', err);
// Fallback to local storage
const savedJourneys = localStorage.getItem('journeys');
if (savedJourneys) {
try {
const journeys = JSON.parse(savedJourneys);
if (Array.isArray(journeys)) {
const journey = journeys.find(j => j.id === journeyId);
if (journey) {
// Update the current journey object
window.currentJourney = {
id: journey.id,
name: journey.name,
description: journey.description,
markers: [],
path: null
};
// Create markers from the journey data
if (Array.isArray(journey.markers)) {
journey.markers.forEach(markerData => {
const lngLat = maplibregl.LngLat.fromArray(markerData.lngLat);
const marker = window.createMarker(lngLat, markerData.content);
window.currentJourney.markers.push(marker);
});
}
// Update the journey path
updateJourneyPath();
console.log('Journey loaded from local storage');
return journey;
}
} else {
console.warn('Saved journeys are not an array:', journeys);
}
} catch (parseError) {
console.error('Error parsing saved journeys:', parseError);
}
}
// If no data available, return null
return null;
}
}
// Update the journey select dropdown
function populateJourneySelect(journeys) {
const select = document.getElementById('journey-select');
select.innerHTML = '<option value="">-- Choose a journey --</option>';
select.innerHTML += '<option value="all">Show All Journeys</option>';
// Sort journeys by name for consistent ordering
const sortedJourneys = [...journeys].sort((a, b) => a.name.localeCompare(b.name));
sortedJourneys.forEach(journey => {
const option = document.createElement('option');
option.value = journey.id;
option.textContent = journey.name;
select.appendChild(option);
});
}
// Toggle sidebar
document.getElementById('toggle-sidebar').addEventListener('click', function() {
document.querySelector('.sidebar').classList.toggle('collapsed');
});
// Initialize in create mode
switchMode('create');
// Load journeys from backend when the page loads
loadJourneysFromBackend().then(() => {
// If there are journeys, set the first one as the current journey
if (window.journeys.length > 0) {
// Set the first journey as current
const firstJourney = window.journeys[0];
loadCurrentJourneyFromBackend(firstJourney.id).then(() => {
// Update the journey title and description
document.getElementById('journey-title').value = currentJourney.name;
document.getElementById('journey-description').value = currentJourney.description;
});
}
});
// Add journey selection functionality
document.getElementById('journey-select').addEventListener('change', function() {
const selectedId = this.value;
if (selectedId === 'all') {
// Show all journeys
// Implementation depends on how you want to display multiple journeys
return;
}
if (selectedId) {
// Load the selected journey
loadCurrentJourneyFromBackend(selectedId).then(() => {
// Update the journey title and description
document.getElementById('journey-title').value = currentJourney.name;
document.getElementById('journey-description').value = currentJourney.description;
// Update the journey info panel
document.getElementById('info-title').textContent = currentJourney.name;
document.getElementById('info-description').textContent = currentJourney.description;
document.getElementById('info-marker-count').textContent = currentJourney.markers.length;
document.getElementById('info-date').textContent = new Date().toLocaleDateString();
});
}
});
// View blog post button
document.getElementById('view-blog').addEventListener('click', function() {
// Implementation depends on your blog system
alert('Viewing blog post for this journey...');
});
// Edit journey button
document.getElementById('edit-journey').addEventListener('click', function() {
// Switch to create mode
switchMode('create');
// Update the journey title and description
document.getElementById('journey-title').value = currentJourney.name;
document.getElementById('journey-description').value = currentJourney.description;
});
// Delete journey button
document.getElementById('delete-journey').addEventListener('click', async function() {
if (confirm('Are you sure you want to delete this journey?')) {
try {
const response = await fetch(`http://localhost:5000/api/journeys/${currentJourney.id}`, {
method: 'DELETE'
});
if (response.ok) {
// Remove from the journeys array
const index = window.journeys.findIndex(j => j.id === currentJourney.id);
if (index !== -1) {
window.journeys.splice(index, 1);
}
// Update the journey select dropdown
populateJourneySelect(window.journeys);
// Clear the current journey
currentJourney = {
id: Date.now(),
name: "Untitled Journey",
description: "",
markers: [],
path: null
};
// Clear the map
map.getSource('journey-path').setData({
type: 'Feature',
properties: {},
geometry: {
type: 'LineString',
coordinates: []
}
});
// Clear the form
document.getElementById('journey-title').value = '';
document.getElementById('journey-description').value = '';
alert('Journey deleted successfully!');
} else {
alert('Failed to delete journey.');
}
} catch (err) {
console.error('Error deleting journey:', err);
// Fallback to local storage
const savedJourneys = localStorage.getItem('journeys');
if (savedJourneys) {
try {
const journeys = JSON.parse(savedJourneys);
if (Array.isArray(journeys)) {
const index = journeys.findIndex(j => j.id === currentJourney.id);
if (index !== -1) {
journeys.splice(index, 1);
localStorage.setItem('journeys', JSON.stringify(journeys));
}
}
} catch (parseError) {
console.error('Error parsing saved journeys:', parseError);
}
}
alert('Journey deleted successfully (local storage)');
}
}
});
});

101
js/map.js
View File

@ -1,101 +0,0 @@
// /Volumes/Data/Code/FHGR/Frontend/js/map.js
// Add the createMarker function to the window object
window.createMarker = function(lngLat, content = {}) {
const marker = L.marker(lngLat, {
draggable: true,
title: content.title || 'Untitled'
}).addTo(map);
// Create popup with marker content
marker.bindPopup(`<strong>${content.title || 'Untitled'}</strong>${content.description ? `<br>${content.description}` : ''}`);
// When the marker is clicked, open the editor
marker.on('click', () => {
openMarkerEditor(marker);
});
// Add marker to current journey
const markerData = {
id: Date.now(),
lngLat: [lngLat.lat, lngLat.lng], // Leaflet uses [lat, lng]
content: content,
coordinates: [lngLat.lat, lngLat.lng]
};
if (!currentJourney.markers) currentJourney.markers = [];
currentJourney.markers.push(marker);
updateJourneyPath();
// Add marker to the markers list
const markersContainer = document.getElementById('markers-container');
const markerElement = document.createElement('div');
markerElement.className = 'marker-item';
markerElement.innerHTML = `
<div class="marker-title">${content.title || 'Untitled'}</div>
<div class="marker-coords">${lngLat.lat.toFixed(4)}, ${lngLat.lng.toFixed(4)}</div>
`;
// Add click event to marker item
markerElement.addEventListener('click', function() {
map.flyTo({
center: [lngLat.lat, lngLat.lng],
zoom: 10
});
openMarkerEditor(marker);
});
if (markersContainer.children.length === 1 &&
markersContainer.firstElementChild.id === 'empty-markers') {
markersContainer.removeChild(markersContainer.firstElementChild);
}
markersContainer.appendChild(markerElement);
updateMarkersList(); // Call the new function to update the list
return marker;
};
// Update the updateJourneyPath function to handle cases where markers array is empty
function updateMarkersList() {
const markersContainer = document.getElementById('markers-container');
markersContainer.innerHTML = '';
if (currentJourney.markers.length === 0) {
markersContainer.innerHTML = '<p class="empty-message">No markers yet. Click on the map to add markers.</p>';
return;
}
currentJourney.markers.forEach((marker, index) => {
const markerElement = document.createElement('div');
markerElement.className = 'marker-item';
markerElement.innerHTML = `
<strong>${index + 1}</strong>
${marker.getLatLng().lat.toFixed(4)}, ${marker.getLngLat().lng.toFixed(4)}
`;
// Add click handler to zoom to marker
markerElement.addEventListener('click', () => {
map.flyTo({
center: [marker.getLatLng().lat, marker.getLatLng().lng],
zoom: 10
});
});
markersContainer.appendChild(markerElement);
});
}
function updateJourneyPath() {
if (!map.hasLayer(journeyPath)) {
journeyPath = L.polyline([], {color: '#3887be', weight: 4});
map.addLayer(journeyPath);
}
const coordinates = currentJourney.markers.map(marker => [
marker.getLatLng().lat,
marker.getLatLng().lng
]);
journeyPath.setLatLngs(coordinates);
}

View File

@ -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>

File diff suppressed because it is too large Load Diff