390 lines
15 KiB
JavaScript

// ==================== GLOBALS ====================
let currentJourneyId = null;
let currentJourneyOwnerId = null;
let markersData = [];
let availableUsers = [];
let sharedReadIds = [];
let sharedEditIds = [];
const urlParams = new URLSearchParams(window.location.search);
const journeyId = urlParams.get('id');
const isNew = urlParams.has('new');
// ==================== UI HELPERS ====================
function showToast(message, isError = false) {
const toast = document.getElementById('toast');
toast.textContent = message;
toast.style.backgroundColor = isError ? 'var(--red-7)' : 'var(--green-7)';
toast.style.display = 'block';
setTimeout(() => { toast.style.display = 'none'; }, 3000);
}
function getUploadUrl(path) {
if (!path) return '';
if (path.startsWith('http')) return path;
return API_BASE.replace('/api', path);
}
function escapeAttribute(value) {
return String(value || '').replace(/[&<>"']/g, (char) => ({
'&': '&amp;',
'<': '&lt;',
'>': '&gt;',
'"': '&quot;',
"'": '&#39;'
})[char]);
}
function normalizeMarkerImages(marker) {
const images = marker.images || (marker.image ? [marker.image] : []);
return { ...marker, images };
}
function canManageSharing() {
return !currentJourneyOwnerId || currentJourneyOwnerId === currentUser?.id;
}
function uniqueIds(ids) {
return [...new Set((ids || []).map((id) => parseInt(id)).filter(Number.isInteger))];
}
async function loadUsers() {
try {
const res = await fetch(`${API_BASE}/users`, { credentials: 'include' });
if (!res.ok) throw new Error('Could not load users');
availableUsers = await res.json();
} catch (err) {
availableUsers = [];
showToast('Sharing users could not be loaded', true);
}
}
function getUserCheckboxHtml(user, group, checked, disabled) {
return `
<label class="sharing-user">
<input type="checkbox" class="${group}-user" value="${user.id}" ${checked ? 'checked' : ''} ${disabled ? 'disabled' : ''}>
<span>${escapeHtml(user.username)}</span>
</label>
`;
}
function renderSharingControls() {
const panel = document.getElementById('sharing-panel');
const visibilitySelect = document.getElementById('journey-visibility');
const visibility = visibilitySelect.value;
const readContainer = document.getElementById('shared-read-users');
const editContainer = document.getElementById('shared-edit-users');
const isShared = visibility === 'shared';
const disabled = !canManageSharing();
visibilitySelect.disabled = disabled;
panel.classList.toggle('visible', isShared);
if (!isShared) return;
if (!availableUsers.length) {
readContainer.innerHTML = '<p class="empty-share-list">No other users found.</p>';
editContainer.innerHTML = '<p class="empty-share-list">No other users found.</p>';
return;
}
readContainer.innerHTML = availableUsers.map((user) =>
getUserCheckboxHtml(user, 'shared-read', sharedReadIds.includes(user.id), disabled)
).join('');
editContainer.innerHTML = availableUsers.map((user) =>
getUserCheckboxHtml(user, 'shared-edit', sharedEditIds.includes(user.id), disabled)
).join('');
document.querySelectorAll('.shared-read-user').forEach((input) => {
input.addEventListener('change', updateSharedUsersFromForm);
});
document.querySelectorAll('.shared-edit-user').forEach((input) => {
input.addEventListener('change', updateSharedUsersFromForm);
});
}
function updateSharedUsersFromForm() {
sharedReadIds = uniqueIds([...document.querySelectorAll('.shared-read-user:checked')].map((input) => input.value));
sharedEditIds = uniqueIds([...document.querySelectorAll('.shared-edit-user:checked')].map((input) => input.value));
sharedReadIds = uniqueIds([...sharedReadIds, ...sharedEditIds]);
renderSharingControls();
}
function getSharingPayload(visibility) {
if (visibility !== 'shared') {
return { shared_read: [], shared_edit: [] };
}
return {
shared_read: uniqueIds([...sharedReadIds, ...sharedEditIds]),
shared_edit: uniqueIds(sharedEditIds)
};
}
function renderMarkerImages(marker, idx) {
const images = marker.images || [];
if (!images.length) {
return '';
}
return `
<div class="image-preview-grid">
${images.map((image, imageIdx) => {
const url = typeof image === 'string' ? image : image.url;
const alt = typeof image === 'string' ? 'Chapter image' : image.originalName || 'Chapter image';
if (!url) return '';
return `
<div class="image-preview">
<img src="${escapeAttribute(getUploadUrl(url))}" alt="${escapeAttribute(alt)}">
<button type="button" class="remove-image" data-index="${idx}" data-image-index="${imageIdx}" aria-label="Remove image">
<i class="fas fa-times"></i>
</button>
</div>
`;
}).join('')}
</div>
`;
}
function renderMarkdownPreview(markdown) {
const html = renderMarkdown(markdown || '');
return html || '<p class="markdown-empty">Nothing to preview yet.</p>';
}
async function uploadMarkerImages(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 || [];
}
// ==================== RENDER MARKERS ====================
function renderMarkers() {
const container = document.getElementById('markers-container');
if (!markersData.length) {
container.innerHTML = '<p class="empty-state" style="text-align:center; color: var(--gray-6);">No chapters yet. Click "Add Chapter" to create one.</p>';
return;
}
container.innerHTML = markersData.map((marker, idx) => `
<div class="marker-card" data-marker-index="${idx}">
<div class="marker-header">
<h4><i class="fas fa-map-pin"></i> Chapter ${idx+1}</h4>
<button type="button" class="remove-marker" data-index="${idx}"><i class="fas fa-trash-alt"></i></button>
</div>
<div class="marker-coords">
<i class="fas fa-location-dot"></i> ${marker.lat.toFixed(6)}, ${marker.lng.toFixed(6)}
</div>
<div class="form-group">
<label>Chapter Title</label>
<input type="text" class="marker-title" data-index="${idx}" value="${escapeHtml(marker.title || '')}" placeholder="Title">
</div>
<div class="form-group">
<label>Date (optional)</label>
<input type="date" class="marker-date" data-index="${idx}" value="${marker.date || ''}">
</div>
<div class="form-group">
<label>Description</label>
<textarea class="marker-description" data-index="${idx}" rows="5" placeholder="Describe this chapter. You can use Markdown like **bold**, [link](https://example.com), or ![image](https://example.com/image.jpg).">${escapeHtml(marker.description || '')}</textarea>
<div class="markdown-preview markdown-content" data-preview-index="${idx}">
${renderMarkdownPreview(marker.description)}
</div>
</div>
<div class="form-group">
<label>Images</label>
<input type="file" class="marker-images" data-index="${idx}" accept="image/*" multiple>
${renderMarkerImages(marker, idx)}
</div>
</div>
`).join('');
// Attach remove listeners
document.querySelectorAll('.remove-marker').forEach(btn => {
btn.addEventListener('click', (e) => {
const idx = parseInt(btn.dataset.index);
markersData.splice(idx, 1);
renderMarkers();
});
});
// Attach input listeners to update markersData
document.querySelectorAll('.marker-title').forEach(input => {
input.addEventListener('input', (e) => {
const idx = parseInt(input.dataset.index);
markersData[idx].title = input.value;
});
});
document.querySelectorAll('.marker-date').forEach(input => {
input.addEventListener('input', (e) => {
const idx = parseInt(input.dataset.index);
markersData[idx].date = input.value;
});
});
document.querySelectorAll('.marker-description').forEach(textarea => {
textarea.addEventListener('input', (e) => {
const idx = parseInt(textarea.dataset.index);
markersData[idx].description = textarea.value;
const preview = document.querySelector(`.markdown-preview[data-preview-index="${idx}"]`);
if (preview) preview.innerHTML = renderMarkdownPreview(textarea.value);
});
});
document.querySelectorAll('.marker-images').forEach(input => {
input.addEventListener('change', async () => {
const idx = parseInt(input.dataset.index);
try {
const uploadedImages = await uploadMarkerImages(Array.from(input.files));
markersData[idx].images = [...(markersData[idx].images || []), ...uploadedImages];
showToast('Image uploaded');
renderMarkers();
} catch (err) {
showToast(err.message, true);
}
});
});
document.querySelectorAll('.remove-image').forEach(btn => {
btn.addEventListener('click', () => {
const idx = parseInt(btn.dataset.index);
const imageIdx = parseInt(btn.dataset.imageIndex);
markersData[idx].images = (markersData[idx].images || []).filter((_, i) => i !== imageIdx);
renderMarkers();
});
});
}
// ==================== LOAD JOURNEY ====================
async function loadJourney(id) {
console.log('Loading journey with id:', id);
try {
const res = await fetch(`${API_BASE}/journeys/${id}`, { credentials: 'include' });
if (!res.ok) {
console.error('Failed to fetch journey, status:', res.status);
throw new Error('Journey not found');
}
const journey = await res.json();
console.log('Journey data:', journey);
currentJourneyId = journey.id;
currentJourneyOwnerId = journey.owner_id;
document.getElementById('journey-title').value = journey.title;
document.getElementById('journey-description').value = journey.description || '';
document.getElementById('journey-visibility').value = journey.visibility || 'private';
sharedReadIds = uniqueIds(journey.shared_read || []);
sharedEditIds = uniqueIds(journey.shared_edit || []);
markersData = (journey.markers || []).map(normalizeMarkerImages);
console.log('Markers data loaded:', markersData);
renderSharingControls();
renderMarkers();
document.getElementById('form-title').textContent = 'Edit Journey';
} catch (err) {
console.error('Error loading journey:', err);
showToast('Error loading journey: ' + err.message, true);
// Optionally redirect after a delay
setTimeout(() => window.location.href = 'blog-list.html', 2000);
}
}
// ==================== SAVE JOURNEY ====================
async function saveJourney(event) {
event.preventDefault();
const title = document.getElementById('journey-title').value.trim();
const description = document.getElementById('journey-description').value.trim();
const visibility = document.getElementById('journey-visibility').value;
if (!title) {
showToast('Please enter a title', true);
return;
}
// Build markers array from current form data (already in markersData)
const markers = markersData.map(m => ({
lat: m.lat,
lng: m.lng,
title: m.title || '',
date: m.date || '',
description: m.description || '',
images: m.images || []
}));
const payload = { title, description, markers };
if (canManageSharing()) {
payload.visibility = visibility;
Object.assign(payload, getSharingPayload(visibility));
}
console.log('Saving payload:', payload);
try {
let res;
if (isNew) {
res = await fetch(`${API_BASE}/journeys`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
credentials: 'include'
});
} else {
res = await fetch(`${API_BASE}/journeys/${currentJourneyId}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
credentials: 'include'
});
}
if (!res.ok) throw new Error('Save failed');
const data = await res.json();
console.log('Save response:', data);
showToast('Journey saved!');
// Redirect to read view
window.location.href = `blog-post.html?id=${data.id}`;
} catch (err) {
console.error('Error saving journey:', err);
showToast('Error saving journey: ' + err.message, true);
}
}
// ==================== ADD MARKER ====================
function addMarker() {
console.log('Adding new marker');
markersData.push({
lat: 46.8182,
lng: 8.2275,
title: 'New Chapter',
date: '',
description: '',
images: []
});
renderMarkers();
}
// ==================== INITIALIZATION ====================
document.addEventListener('DOMContentLoaded', async () => {
const authenticated = await checkAuthAndRedirect();
if (!authenticated) return;
updateUserMenu();
if (!isNew && journeyId) {
await loadUsers();
await loadJourney(journeyId);
} else if (isNew) {
currentJourneyId = null;
currentJourneyOwnerId = currentUser.id;
document.getElementById('form-title').textContent = 'New Journey';
markersData = [];
sharedReadIds = [];
sharedEditIds = [];
await loadUsers();
renderSharingControls();
renderMarkers();
} else {
window.location.href = 'blog-list.html';
}
document.getElementById('journey-form').addEventListener('submit', saveJourney);
document.getElementById('add-marker').addEventListener('click', addMarker);
document.getElementById('journey-visibility').addEventListener('change', renderSharingControls);
});