457 lines
17 KiB
HTML
457 lines
17 KiB
HTML
<!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">
|
||
|
||
<!-- Responsive Design -->
|
||
<link rel="stylesheet" href="css/responsive.css">
|
||
<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> |