2026-03-28 19:28:42 +01:00

455 lines
17 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!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>
<link rel="stylesheet" href="https://unpkg.com/open-props"/>
<link rel="stylesheet" href="https://unpkg.com/open-props/normalize.min.css"/>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<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;
}
.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 {
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);
}
.post-container {
max-width: 800px;
margin: var(--size-6) auto;
padding: 0 var(--size-4);
}
.post-card {
background: var(--surface-1);
border-radius: var(--radius-3);
padding: var(--size-6);
box-shadow: var(--shadow-2);
margin-bottom: var(--size-6);
}
.post-title {
font-size: var(--font-size-6);
margin-top: 0;
margin-bottom: var(--size-2);
}
.post-meta {
color: var(--gray-6);
margin-bottom: var(--size-4);
border-bottom: 1px solid var(--surface-4);
padding-bottom: var(--size-2);
}
.post-image {
max-width: 100%;
border-radius: var(--radius-2);
margin: var(--size-4) 0;
}
.post-content {
line-height: 1.6;
}
.comments-section {
background: var(--surface-1);
border-radius: var(--radius-3);
padding: var(--size-6);
box-shadow: var(--shadow-2);
}
.comment {
border-bottom: 1px solid var(--surface-4);
padding: var(--size-4) 0;
}
.comment:last-child {
border-bottom: none;
}
.comment-meta {
font-size: var(--font-size-1);
color: var(--gray-6);
margin-bottom: var(--size-2);
}
.comment-text {
margin: 0;
}
.delete-comment {
background: none;
border: none;
color: var(--red-6);
cursor: pointer;
font-size: var(--font-size-1);
margin-left: var(--size-2);
}
.delete-comment:hover {
text-decoration: underline;
}
.comment-form {
margin-top: var(--size-4);
}
.comment-form textarea {
width: 100%;
padding: var(--size-2);
border: 1px solid var(--surface-4);
border-radius: var(--radius-2);
background: var(--surface-2);
font-family: inherit;
font-size: var(--font-size-2);
resize: vertical;
}
.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-secondary {
background: var(--gray-7);
}
.btn-danger {
background: var(--red-7);
}
.btn-sm {
padding: var(--size-1) var(--size-2);
font-size: var(--font-size-1);
}
.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;
}
.chapters {
margin-top: var(--size-4);
}
.chapter {
margin-bottom: var(--size-6);
border-left: 3px solid var(--indigo-6);
padding-left: var(--size-4);
}
.chapter-header {
display: flex;
justify-content: space-between;
align-items: baseline;
margin-bottom: var(--size-2);
}
.chapter-header h3 {
margin: 0;
font-size: var(--font-size-4);
}
.marker-date {
font-size: var(--font-size-1);
color: var(--gray-6);
}
.chapter-image {
max-width: 100%;
border-radius: var(--radius-2);
margin: var(--size-2) 0;
}
.chapter-content {
line-height: 1.6;
}
</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">Blog</a>
</nav>
<div class="user-menu" id="user-menu"></div>
</div>
</div>
</header>
<main class="post-container">
<div id="post-content"></div>
<div id="comments-section" class="comments-section"></div>
</main>
<div id="toast" class="toast"></div>
<script src="js/auth.js"></script>
<script>
let currentJourney = null;
const urlParams = new URLSearchParams(window.location.search);
const journeyId = urlParams.get('id');
// ==================== LOAD JOURNEY ====================
async function loadJourney() {
if (!journeyId) {
window.location.href = 'blog-list.html';
return;
}
try {
const res = await fetch(`${API_BASE}/journeys/${journeyId}`, { credentials: 'include' });
if (!res.ok) throw new Error('Journey not found');
currentJourney = await res.json();
renderJourney();
loadComments();
} catch (err) {
showToast('Error loading journey', true);
setTimeout(() => window.location.href = 'blog-list.html', 2000);
}
}
function renderJourney() {
const container = document.getElementById('post-content');
const isOwner = currentUser && currentUser.id === currentJourney.owner_id;
const canComment = currentJourney.visibility === 'public' || isOwner;
// Build chapters from markers
let chaptersHtml = '';
if (currentJourney.markers && currentJourney.markers.length) {
chaptersHtml = '<div class="chapters">';
currentJourney.markers.forEach((marker, idx) => {
const title = marker.title || `Chapter ${idx + 1}`;
const date = marker.date ? `<span class="marker-date">${new Date(marker.date).toLocaleDateString()}</span>` : '';
const content = marker.description ? escapeHtml(marker.description).replace(/\n/g, '<br>') : '';
const image = marker.image ? `<img src="${marker.image}" class="chapter-image">` : '';
const video = marker.videoUrl ? `<iframe src="${marker.videoUrl}" frameborder="0" allowfullscreen></iframe>` : '';
chaptersHtml += `
<div class="chapter" data-marker-id="${marker.id || idx}">
<div class="chapter-header">
<h3>${escapeHtml(title)}</h3>
${date}
</div>
${image}
${video}
<div class="chapter-content">${content}</div>
</div>
`;
});
chaptersHtml += '</div>';
} else {
chaptersHtml = '<p>No chapters yet.</p>';
}
container.innerHTML = `
<article class="post-card">
<h1 class="post-title">${escapeHtml(currentJourney.title)}</h1>
<div class="post-meta">
<i class="fas fa-calendar-alt"></i> ${new Date(currentJourney.created_at).toLocaleDateString()}
${currentJourney.visibility === 'public' ? '<span class="badge">Public</span>' : ''}
</div>
${currentJourney.image ? `<img class="post-image" src="${currentJourney.image}" alt="${currentJourney.title}">` : ''}
<div class="post-description">${escapeHtml(currentJourney.description).replace(/\n/g, '<br>')}</div>
${chaptersHtml}
${isOwner ? `
<div style="margin-top: var(--size-4); display: flex; gap: var(--size-2);">
<a href="journey-edit.html?id=${currentJourney.id}" class="btn btn-sm"><i class="fas fa-edit"></i> Edit</a>
<button id="delete-journey-btn" class="btn btn-danger btn-sm"><i class="fas fa-trash"></i> Delete</button>
</div>
` : ''}
</article>
`;
if (isOwner) {
document.getElementById('delete-journey-btn')?.addEventListener('click', deleteJourney);
}
}
async function deleteJourney() {
if (!confirm('Delete this journey permanently? All chapters and comments will be lost.')) return;
try {
const res = await fetch(`${API_BASE}/journeys/${currentJourney.id}`, {
method: 'DELETE',
credentials: 'include'
});
if (!res.ok) throw new Error('Delete failed');
showToast('Journey deleted');
setTimeout(() => window.location.href = 'blog-list.html', 1000);
} catch (err) {
showToast('Error deleting journey', true);
}
}
// ==================== COMMENTS ====================
async function loadComments() {
try {
const res = await fetch(`${API_BASE}/journeys/${journeyId}/comments`, { credentials: 'include' });
if (!res.ok) throw new Error('Failed to load comments');
const comments = await res.json();
renderComments(comments);
} catch (err) {
console.error(err);
}
}
function renderComments(comments) {
const container = document.getElementById('comments-section');
const isOwner = currentUser && currentUser.id === currentJourney.owner_id;
const canComment = currentJourney.visibility === 'public' || isOwner;
if (!comments.length) {
container.innerHTML = `
<h3><i class="fas fa-comments"></i> Comments</h3>
<p class="empty-state">No comments yet. Be the first to comment!</p>
${canComment && currentUser ? getCommentFormHtml() : (!currentUser ? '<p><a href="login.html">Login</a> to comment.</p>' : '')}
`;
if (canComment && currentUser) attachCommentForm();
return;
}
let commentsHtml = '<h3><i class="fas fa-comments"></i> Comments</h3>';
comments.forEach(comment => {
const isOwnerOrAuthor = currentUser && (currentUser.id === comment.author_id || currentUser.id === currentJourney.owner_id);
commentsHtml += `
<div class="comment" data-comment-id="${comment.id}">
<div class="comment-meta">
<strong>${escapeHtml(comment.author_name)}</strong> ${new Date(comment.created_at).toLocaleString()}
${isOwnerOrAuthor ? `<button class="delete-comment" data-id="${comment.id}"><i class="fas fa-trash-alt"></i> Delete</button>` : ''}
</div>
<p class="comment-text">${escapeHtml(comment.text)}</p>
</div>
`;
});
container.innerHTML = commentsHtml + (canComment && currentUser ? getCommentFormHtml() : (canComment ? '<p><a href="login.html">Login</a> to comment.</p>' : ''));
document.querySelectorAll('.delete-comment').forEach(btn => {
btn.addEventListener('click', () => deleteComment(parseInt(btn.dataset.id)));
});
if (canComment && currentUser) attachCommentForm();
}
function getCommentFormHtml() {
return `
<div class="comment-form">
<textarea id="comment-text" rows="3" placeholder="Write a comment..."></textarea>
<button id="submit-comment" class="btn btn-sm" style="margin-top: var(--size-2);"><i class="fas fa-paper-plane"></i> Post Comment</button>
</div>
`;
}
function attachCommentForm() {
document.getElementById('submit-comment')?.addEventListener('click', submitComment);
}
async function submitComment() {
const text = document.getElementById('comment-text').value.trim();
if (!text) {
showToast('Comment cannot be empty', true);
return;
}
try {
const res = await fetch(`${API_BASE}/journeys/${journeyId}/comments`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ text }),
credentials: 'include'
});
if (!res.ok) throw new Error('Failed to post comment');
showToast('Comment posted');
loadComments();
} catch (err) {
showToast('Error posting comment', true);
}
}
async function deleteComment(commentId) {
if (!confirm('Delete this comment?')) return;
try {
const res = await fetch(`${API_BASE}/comments/${commentId}`, {
method: 'DELETE',
credentials: 'include'
});
if (!res.ok) throw new Error('Delete failed');
showToast('Comment deleted');
loadComments();
} catch (err) {
showToast('Error deleting comment', true);
}
}
// ==================== INIT ====================
document.addEventListener('DOMContentLoaded', async () => {
const authenticated = await checkAuthAndRedirect();
if (!authenticated) return;
updateUserMenu();
loadJourney();
});
</script>
</body>
</html>