Added basic html, js and css for blogpost page. Still mainly vibe code, need to review later. Backend not yet updated. Added venv and requirements.

This commit is contained in:
André Rüegger 2026-03-12 00:33:49 +01:00
parent 471d629a93
commit 3f5fdc2ff7
4 changed files with 415 additions and 0 deletions

BIN
backend/requirements.txt Normal file

Binary file not shown.

87
blog-page.html Normal file
View File

@ -0,0 +1,87 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Blog Post</title>
<link rel="stylesheet" href="css/style.css">
<link rel="stylesheet" href="css/blog.css">
</head>
<body>
<header class="site-header">
<div class="container">
<h1 class="site-title"><a href="/">My Blog</a></h1>
<nav class="site-nav">
<a href="map-page.html">Map</a>
<a href="blog-page.html">Blog</a>
</nav>
</div>
</header>
<main class="container post-page">
<article class="post">
<h2 class="post-title">Post Title</h2>
<div class="post-meta">By <span class="author">Author</span><time datetime="2026-01-01">Jan 1, 2026</time></div>
<figure class="post-hero">
<img src="https://via.placeholder.com/800x350?text=Hero+Image" alt="Post image">
</figure>
<div class="post-content">
<p>This is a placeholder paragraph for the blog post content. Replace with real content.</p>
</div>
</article>
<aside class="sidebar">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:12px;">
<h3 style="margin:0">Posts</h3>
<button id="create-post-btn" class="btn btn-primary">New</button>
</div>
<div id="posts-list">
<!-- JS will render list of posts here -->
</div>
<section class="about" id="post-form" style="display:none;margin-top:16px;">
<h3 id="form-title">New Post</h3>
<form id="blog-form">
<div class="form-group">
<label for="post-title-input">Title</label>
<input id="post-title-input" name="title" required />
</div>
<div class="form-group">
<label for="post-journey-input">Journey ID</label>
<input id="post-journey-input" name="journeyId" placeholder="optional" />
</div>
<div class="form-group">
<label for="post-image-input">Image URL</label>
<input id="post-image-input" name="image" placeholder="https://..." />
</div>
<div class="form-group">
<label for="post-content-input">Content</label>
<textarea id="post-content-input" name="content" rows="6" required></textarea>
</div>
<div style="display:flex;gap:8px">
<button type="submit" class="btn btn-primary">Save</button>
<button type="button" id="cancel-post-btn" class="btn btn-secondary">Cancel</button>
</div>
</form>
</section>
<section class="about" style="margin-top:18px;">
<h3>About</h3>
<p>Short author bio or related links.</p>
</section>
</aside>
</main>
<section class="comments container">
<h3>Comments</h3>
<div class="comments-list"></div>
</section>
<footer class="site-footer">
<div class="container">&copy; 2026 My Blog</div>
</footer>
<script src="js/main.js"></script>
<script src="js/blog-posts.js"></script>
</body>
</html>

154
css/blog.css Normal file
View File

@ -0,0 +1,154 @@
/* 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

@ -0,0 +1,174 @@
/* 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();
});
});