Compare commits

...

4 Commits

Author SHA1 Message Date
Josh-Dev-Quest
bcc86be6c4
Merge branch 'blog-map-fusion' into dev 2026-03-27 20:17:56 +01:00
Josh-Dev-Quest
50aae732aa
Reworked blog overview website 2026-03-27 20:14:20 +01:00
Josh-Dev-Quest
4b09f09ccf
Unified pages navigation 2026-03-27 19:37:09 +01:00
Josh-Dev-Quest
5c70d85e48
README ready for submission 2026-02-27 07:14:23 +01:00
8 changed files with 1041 additions and 21 deletions

2
.gitignore vendored
View File

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

View File

@ -1,15 +1,37 @@
# Journey Mapper Backend
# Travel Journal (Draft README)
## Projectdescription
On this website you can document your travelstories.
Where you where, 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.
A Flask server for the Journey Mapper web application.
## Pages
### Map Page
Here you can create a journey through setting marker on a map.
At each marker you can write notes or implement images.
### Blog Page
Here you can look at all notes from one journey.
The markers have a date so they are in a time order.
### Journey Explorer
Here you can search for all journeys. Like overview page from a blogwebsite.
## Features
- Blog entry
- Log places
- Add pictures to specific places
- RESTful API for managing journeys
- Persistent storage using JSON file
- CORS enabled for frontend integration
- Health check endpoint
- Easy to set up and run
## Installation
TBD
## Getting Started
## Tech-Stack
- HTML
- CSS
- JavaScript
- WebGL - MapLibre
## Members
- Flepp Stiafen
- Kohler Joshua
- Rüegger André
1. Install dependencies:

View File

@ -134,5 +134,86 @@ def index():
</html>
'''
# Blog posts data file
BLOG_DATA_FILE = os.path.join(DATA_DIR, 'blog_posts.json')
def load_blog_posts():
try:
if os.path.exists(BLOG_DATA_FILE):
with open(BLOG_DATA_FILE, 'r') as f:
return json.load(f)
except:
pass
return []
def save_blog_posts(posts):
with open(BLOG_DATA_FILE, 'w') as f:
json.dump(posts, f, indent=2)
blog_posts = load_blog_posts()
def get_next_blog_id():
if not blog_posts:
return 1
return max(p['id'] for p in blog_posts) + 1
@app.route('/api/blog-posts', methods=['GET'])
def get_blog_posts():
return jsonify(blog_posts)
@app.route('/api/blog-posts/<int:post_id>', methods=['GET'])
def get_blog_post(post_id):
post = next((p for p in blog_posts if p['id'] == post_id), None)
if not post:
return jsonify({'error': 'Post not found'}), 404
return jsonify(post)
@app.route('/api/blog-posts', methods=['POST'])
def create_blog_post():
data = request.get_json()
title = data.get('title')
if not title:
return jsonify({'error': 'Title required'}), 400
new_post = {
'id': get_next_blog_id(),
'title': title,
'content': data.get('content', ''),
'journeyId': data.get('journeyId'),
'image': data.get('image'),
'created_at': datetime.now().isoformat()
}
blog_posts.append(new_post)
save_blog_posts(blog_posts)
return jsonify(new_post), 201
@app.route('/api/blog-posts/<int:post_id>', methods=['PUT'])
def update_blog_post(post_id):
post = next((p for p in blog_posts if p['id'] == post_id), None)
if not post:
return jsonify({'error': 'Post not found'}), 404
data = request.get_json()
if 'title' in data:
post['title'] = data['title']
if 'content' in data:
post['content'] = data['content']
if 'journeyId' in data:
post['journeyId'] = data['journeyId']
if 'image' in data:
post['image'] = data['image']
save_blog_posts(blog_posts)
return jsonify(post)
@app.route('/api/blog-posts/<int:post_id>', methods=['DELETE'])
def delete_blog_post(post_id):
global blog_posts
post = next((p for p in blog_posts if p['id'] == post_id), None)
if not post:
return jsonify({'error': 'Post not found'}), 404
blog_posts = [p for p in blog_posts if p['id'] != post_id]
save_blog_posts(blog_posts)
return jsonify({'message': 'Post deleted'})
if __name__ == '__main__':
app.run(debug=True, port=5000)

View File

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

233
blog-list.html Normal file
View File

@ -0,0 +1,233 @@
<!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: Poppins -->
<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 (same as map page) */
.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);
}
/* 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;
}
</style>
</head>
<body>
<header class="site-header">
<div class="container">
<h1 class="site-title"><a href="map-page.html">Journey Mapper</a></h1>
<nav class="site-nav">
<a href="map-page.html">Map</a>
<a href="blog-list.html" class="active">Blog</a>
</nav>
</div>
</header>
<main class="blog-container">
<div class="blog-header">
<h1><i class="fas fa-newspaper"></i> Blog Posts</h1>
<a href="blog-post.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>
<script>
const API_BASE = 'http://127.0.0.1:5000/api';
async function loadPosts() {
try {
const res = await fetch(`${API_BASE}/blog-posts`);
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.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('');
}
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;
});
}
document.addEventListener('DOMContentLoaded', loadPosts);
</script>
</body>
</html>

View File

@ -3,33 +3,303 @@
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Blog Post</title>
<title>Blog Post Journey Mapper</title>
<!-- Open Props CSS -->
<link rel="stylesheet" href="https://unpkg.com/open-props"/>
<!-- optional: normalize / additional props -->
<link rel="stylesheet" href="https://unpkg.com/open-props/normalize.min.css"/>
<!-- Font Awesome -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<!-- Google Fonts: Poppins -->
<!-- Google Fonts: Poppins (matching map page) -->
<link href="https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;500;600;700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="css/style.css">
<link rel="stylesheet" href="css/blog.css">
<style>
/* ===== GLOBAL RESET USING OPEN PROPS ===== */
* {
box-sizing: border-box;
}
body {
font-family: var(--font-sans);
background: var(--gray-0);
color: var(--gray-9);
line-height: var(--font-lineheight-3);
}
/* ===== HEADER ===== */
.site-header {
background: var(--gray-9);
padding: var(--size-4) var(--size-6);
border-bottom: 1px solid var(--surface-4);
}
.site-header .container {
max-width: 1200px;
margin: 0 auto;
display: flex;
justify-content: space-between;
align-items: center;
flex-wrap: wrap;
gap: var(--size-2);
}
.site-title {
margin: 0;
font-size: var(--font-size-4);
font-weight: var(--font-weight-6);
}
.site-title a {
color: var(--indigo-4);
text-decoration: none;
}
.site-nav {
display: flex;
gap: var(--size-4);
}
.site-nav a {
color: var(--gray-2);
text-decoration: none;
font-weight: var(--font-weight-5);
transition: color 0.2s;
}
.site-nav a:hover {
color: var(--indigo-4);
}
/* ===== MAIN LAYOUT (twocolumn) ===== */
.post-page {
display: flex;
flex-direction: column;
gap: var(--size-6);
padding: var(--size-6) var(--size-4);
max-width: 1400px;
margin: 0 auto;
}
.post {
background: var(--surface-1);
border-radius: var(--radius-3);
padding: var(--size-6);
box-shadow: var(--shadow-2);
}
.post-title {
font-size: var(--font-size-6);
font-weight: var(--font-weight-7);
margin: 0 0 var(--size-2) 0;
color: var(--gray-9);
}
.post-meta {
color: var(--gray-6);
font-size: var(--font-size-1);
margin-bottom: var(--size-4);
border-bottom: 1px solid var(--surface-4);
padding-bottom: var(--size-2);
}
.post-hero {
margin: var(--size-4) 0;
}
.post-hero img {
width: 100%;
height: auto;
border-radius: var(--radius-2);
object-fit: cover;
}
.post-content {
line-height: var(--font-lineheight-4);
color: var(--gray-7);
}
.post-content p {
margin-bottom: var(--size-4);
}
/* Sidebar */
.sidebar {
background: var(--surface-1);
border-radius: var(--radius-3);
padding: var(--size-4);
box-shadow: var(--shadow-2);
}
.sidebar section {
margin-bottom: var(--size-6);
}
.sidebar h3 {
font-size: var(--font-size-3);
margin-top: 0;
margin-bottom: var(--size-3);
color: var(--indigo-7);
border-left: 3px solid var(--indigo-6);
padding-left: var(--size-2);
}
/* Posts list */
#posts-list {
display: flex;
flex-direction: column;
gap: var(--size-3);
}
.post-preview {
background: var(--surface-2);
border-radius: var(--radius-2);
padding: var(--size-3);
transition: background 0.2s;
cursor: pointer;
}
.post-preview:hover {
background: var(--surface-3);
}
.post-preview-title {
font-weight: var(--font-weight-6);
color: var(--gray-9);
margin: 0 0 var(--size-1) 0;
}
.post-preview-meta {
font-size: var(--font-size-0);
color: var(--gray-6);
}
/* Form for creating/editing posts */
.form-group {
margin-bottom: var(--size-3);
}
.form-group label {
display: block;
margin-bottom: var(--size-1);
font-weight: var(--font-weight-5);
color: var(--gray-7);
}
.form-group input,
.form-group textarea {
width: 100%;
padding: var(--size-2) var(--size-3);
border: 1px solid var(--surface-4);
border-radius: var(--radius-2);
background: var(--surface-2);
color: var(--text-1);
font-family: inherit;
font-size: var(--font-size-2);
}
.form-group input:focus,
.form-group textarea:focus {
outline: none;
border-color: var(--indigo-6);
box-shadow: 0 0 0 3px var(--indigo-2);
}
/* Buttons (reuse map page classes) */
.btn {
padding: var(--size-2) var(--size-4);
border: none;
border-radius: var(--radius-2);
cursor: pointer;
font-size: var(--font-size-2);
font-weight: var(--font-weight-5);
display: inline-flex;
align-items: center;
gap: var(--size-2);
transition: all 0.2s var(--ease-2);
box-shadow: var(--shadow-2);
}
.btn-primary {
background: var(--indigo-7);
color: white;
}
.btn-primary:hover {
background: var(--indigo-8);
box-shadow: var(--shadow-3);
}
.btn-secondary {
background: var(--gray-7);
color: white;
}
.btn-secondary:hover {
background: var(--gray-8);
}
.btn-danger {
background: var(--red-7);
color: white;
}
.btn-danger:hover {
background: var(--red-8);
}
/* Comments section */
.comments {
max-width: 1200px;
margin: var(--size-6) auto 0;
padding: 0 var(--size-4);
}
.comments h3 {
font-size: var(--font-size-4);
margin-bottom: var(--size-4);
color: var(--indigo-7);
}
.comments-list {
display: flex;
flex-direction: column;
gap: var(--size-4);
}
.comment-item {
background: var(--surface-1);
border-radius: var(--radius-2);
padding: var(--size-4);
box-shadow: var(--shadow-1);
}
.comment-item .meta {
color: var(--gray-6);
font-size: var(--font-size-0);
margin-bottom: var(--size-2);
}
.comment-item p {
margin: 0;
}
/* Footer */
.site-footer {
background: var(--surface-2);
padding: var(--size-4);
margin-top: var(--size-6);
text-align: center;
color: var(--gray-6);
font-size: var(--font-size-1);
border-top: 1px solid var(--surface-4);
}
/* Responsive twocolumn layout */
@media (min-width: 768px) {
.post-page {
flex-direction: row;
align-items: flex-start;
gap: var(--size-6);
padding: var(--size-6);
}
.post {
flex: 1;
margin: 0;
}
.sidebar {
width: 320px;
flex-shrink: 0;
position: sticky;
top: var(--size-4);
}
}
</style>
</head>
<body>
<header class="site-header">
<div class="container">
<h1 class="site-title"><a href="/">My Blog</a></h1>
<h1 class="site-title"><a href="map-page.html">Journey Mapper</a></h1>
<nav class="site-nav">
<a href="map-page.html">Map</a>
<a href="blog-page.html">Blog</a>
<a href="blog-page.html" class="active">Blog</a>
</nav>
</div>
</header>
<main class="container post-page">
<main class="post-page">
<article class="post">
<h2 class="post-title">Post Title</h2>
<div class="post-meta">By <span class="author">Author</span><time datetime="2026-01-01">Jan 1, 2026</time></div>
@ -40,6 +310,7 @@
<p>This is a placeholder paragraph for the blog post content. Replace with real content.</p>
</div>
</article>
<aside class="sidebar">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:12px;">
<h3 style="margin:0">Posts</h3>
@ -83,7 +354,7 @@
</aside>
</main>
<section class="comments container">
<section class="comments">
<h3>Comments</h3>
<div class="comments-list"></div>
</section>
@ -92,6 +363,7 @@
<div class="container">&copy; 2026 My Blog</div>
</footer>
<!-- JavaScript (keep your existing logic) -->
<script src="js/main.js"></script>
<script src="js/blog-posts.js"></script>
</body>

347
blog-post-edit.html Normal file
View File

@ -0,0 +1,347 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Blog Post Journey Mapper</title>
<!-- Open Props CSS -->
<link rel="stylesheet" href="https://unpkg.com/open-props"/>
<link rel="stylesheet" href="https://unpkg.com/open-props/normalize.min.css"/>
<!-- Font Awesome -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<!-- Google Fonts: Poppins -->
<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);
}
/* 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>
<nav class="site-nav">
<a href="map-page.html">Map</a>
<a href="blog-list.html">Blog</a>
</nav>
</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>
const API_BASE = 'http://127.0.0.1:5000/api';
let currentPostId = null;
// Get post id from URL
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}`);
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)
});
if (!res.ok) throw new Error('Save failed');
const data = await res.json();
showToast('Post saved successfully');
// Redirect to the new post if it was created
if (isNew) {
window.location.href = `blog-post.html?id=${data.id}`;
} else {
// reload if needed
}
} 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' });
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);
}
}
function showToast(message, isError = false) {
const toast = document.getElementById('toast');
toast.textContent = message;
toast.style.backgroundColor = isError ? 'var(--red-7)' : 'var(--green-7)';
toast.style.display = 'block';
setTimeout(() => { toast.style.display = 'none'; }, 3000);
}
// Image upload (convert to base64 and set as image URL)
document.getElementById('image-upload').addEventListener('change', function(e) {
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);
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 maybe go to list
window.location.href = 'blog-list.html';
}
</script>
</body>
</html>

View File

@ -479,9 +479,64 @@
flex-direction: row;
}
}
.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);
}
.app-container {
flex: 1;
display: flex;
overflow: hidden;
}
</style>
</head>
<body>
<body style="display: flex; flex-direction: column; min-height: 100vh;">
<header class="site-header">
<div class="container">
<h1 class="site-title"><a href="/">Journey Mapper</a></h1>
<nav class="site-nav">
<a href="map-page.html" class="active">Map</a>
<a href="blog-list.html">Blog</a>
</nav>
</div>
</header>
<div class="app-container">
<!-- SIDEBAR -->
<aside class="sidebar">
@ -584,7 +639,7 @@
<div class="sidebar-footer">
<div class="navigation">
<a href="#" class="nav-link"><i class="fas fa-list"></i> All Journeys</a>
<a href="#" class="nav-link"><i class="fas fa-question-circle"></i> Help</a>
<a href="blog-page.html" class="nav-link"><i class="fas fa-blog"></i> Blog</a>
</div>
<p class="footer-text">Journey Mapper v1.0</p>
</div>