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:
parent
471d629a93
commit
3f5fdc2ff7
BIN
backend/requirements.txt
Normal file
BIN
backend/requirements.txt
Normal file
Binary file not shown.
87
blog-page.html
Normal file
87
blog-page.html
Normal 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">© 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
154
css/blog.css
Normal 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;
|
||||
}
|
||||
174
js/blog-posts.js
174
js/blog-posts.js
@ -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)=>({
|
||||
'&':'&','<':'<','>':'>','"':'"',"'":'''
|
||||
}[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();
|
||||
});
|
||||
});
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user