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 canEdit = canEditJourney(); // Build chapters from markers let chaptersHtml = ''; if (currentJourney.markers && currentJourney.markers.length) { chaptersHtml = '
'; currentJourney.markers.forEach((marker, idx) => { const title = marker.title || `Chapter ${idx + 1}`; const date = marker.date ? `${new Date(marker.date).toLocaleDateString()}` : ''; const content = marker.description ? renderMarkdown(marker.description) : ''; marker.images = getMarkerImages(marker); const images = getMarkerImagesHtml(marker.images, idx, canEdit); chaptersHtml += `

${escapeHtml(title)}

${date}
${images} ${canEdit ? getImageToolsHtml(idx) : ''}
${content}
`; }); chaptersHtml += '
'; } else { chaptersHtml = '

No chapters yet.

'; } container.innerHTML = `

${escapeHtml(currentJourney.title)}

${new Date(currentJourney.created_at).toLocaleDateString()} ${currentJourney.visibility === 'public' ? 'Public' : ''} ${currentJourney.visibility === 'shared' && !isOwner ? `${canEdit ? 'Shared edit' : 'Shared'}` : ''}
${currentJourney.image ? `${escapeAttribute(currentJourney.title)}` : ''}
${renderMarkdown(currentJourney.description)}
${chaptersHtml} ${canEdit || isOwner ? `
${canEdit ? ` Edit` : ''} ${isOwner ? `` : ''}
` : ''}
`; if (isOwner) { document.getElementById('delete-journey-btn')?.addEventListener('click', deleteJourney); } if (canEdit) { attachChapterImageControls(); } } function getUploadUrl(path) { if (!path) return ''; if (path.startsWith('http')) return sanitizeDisplayUrl(path); return API_BASE.replace('/api', path); } function canEditJourney() { if (!currentUser || !currentJourney) return false; if (currentUser.id === currentJourney.owner_id) return true; return currentJourney.visibility === 'shared' && Array.isArray(currentJourney.shared_edit) && currentJourney.shared_edit.includes(currentUser.id); } function canViewJourney() { if (!currentUser || !currentJourney) return false; if (currentUser.id === currentJourney.owner_id) return true; if (currentJourney.visibility === 'public') return true; return currentJourney.visibility === 'shared' && ( (Array.isArray(currentJourney.shared_read) && currentJourney.shared_read.includes(currentUser.id)) || (Array.isArray(currentJourney.shared_edit) && currentJourney.shared_edit.includes(currentUser.id)) ); } function getMarkerImages(marker) { return marker.images || (marker.image ? [marker.image] : []); } function getMarkerImagesHtml(images, markerIndex, canEdit) { if (!images || !images.length) return ''; const imageHtml = images.map((image, imageIndex) => { const url = typeof image === 'string' ? image : image.url; const alt = typeof image === 'string' ? 'Journey image' : image.originalName || 'Journey image'; const displayUrl = sanitizeDisplayUrl(getUploadUrl(url)); if (!displayUrl) return ''; return `
${escapeAttribute(alt)} ${canEdit ? ` ` : ''}
`; }).join(''); if (!imageHtml) return ''; return `
${imageHtml}
`; } function getImageToolsHtml(markerIndex) { return `
`; } function attachChapterImageControls() { document.querySelectorAll('.chapter-image-input').forEach(input => { input.addEventListener('change', () => uploadChapterImages(input)); }); document.querySelectorAll('.remove-chapter-image').forEach(button => { button.addEventListener('click', () => removeChapterImage( parseInt(button.dataset.markerIndex), parseInt(button.dataset.imageIndex) )); }); } async function uploadImages(files) { if (!files.length) return []; const formData = new FormData(); files.forEach(file => formData.append('images', file)); const res = await fetch(`${API_BASE}/uploads/images`, { method: 'POST', body: formData, credentials: 'include' }); const data = await res.json(); if (!res.ok) throw new Error(data.error || 'Image upload failed'); return data.images || []; } async function uploadChapterImages(input) { const markerIndex = parseInt(input.dataset.markerIndex); const files = Array.from(input.files || []); if (!files.length || !currentJourney.markers[markerIndex]) return; const originalImages = [...getMarkerImages(currentJourney.markers[markerIndex])]; try { showToast('Uploading images...'); const uploadedImages = await uploadImages(files); const marker = currentJourney.markers[markerIndex]; marker.images = [...originalImages, ...uploadedImages]; await saveJourneyMarkers(); showToast('Images uploaded'); renderJourney(); } catch (err) { currentJourney.markers[markerIndex].images = originalImages; showToast(err.message || 'Error uploading images', true); } finally { input.value = ''; } } async function removeChapterImage(markerIndex, imageIndex) { const marker = currentJourney.markers[markerIndex]; if (!marker) return; const originalImages = [...getMarkerImages(marker)]; marker.images = getMarkerImages(marker).filter((_, idx) => idx !== imageIndex); try { await saveJourneyMarkers(); showToast('Image removed'); renderJourney(); } catch (err) { marker.images = originalImages; showToast('Error removing image', true); } } async function saveJourneyMarkers() { const res = await fetch(`${API_BASE}/journeys/${currentJourney.id}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ markers: currentJourney.markers }), credentials: 'include' }); const data = await res.json(); if (!res.ok) throw new Error(data.error || 'Could not save images'); currentJourney = data; } function escapeAttribute(value) { return String(value || '').replace(/[&<>"']/g, (char) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' })[char]); } 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 = canViewJourney(); if (!comments.length) { container.innerHTML = `

Comments

No comments yet. Be the first to comment!

${canComment && currentUser ? getCommentFormHtml() : (!currentUser ? '

Login to comment.

' : '')} `; if (canComment && currentUser) attachCommentForm(); return; } let commentsHtml = '

Comments

'; comments.forEach(comment => { const isOwnerOrAuthor = currentUser && (currentUser.id === comment.author_id || currentUser.id === currentJourney.owner_id); commentsHtml += `
${escapeHtml(comment.author_name)} – ${new Date(comment.created_at).toLocaleString()} ${isOwnerOrAuthor ? `` : ''}

${escapeHtml(comment.text)}

`; }); container.innerHTML = commentsHtml + (canComment && currentUser ? getCommentFormHtml() : (canComment ? '

Login to comment.

' : '')); document.querySelectorAll('.delete-comment').forEach(btn => { btn.addEventListener('click', () => deleteComment(parseInt(btn.dataset.id))); }); if (canComment && currentUser) attachCommentForm(); } function getCommentFormHtml() { return `
`; } 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(); });