Show and edit marker images

This commit is contained in:
André Rüegger 2026-06-01 03:41:30 +02:00
parent e4759b8961
commit 806aedb984
3 changed files with 174 additions and 14 deletions

View File

@ -13,10 +13,11 @@ app.secret_key = "your-secret-key-here-change-in-production" # needed for sessi
CORS(app, supports_credentials=True) # allow cookies
# Directories
DATA_DIR = "data"
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
DATA_DIR = os.path.join(BASE_DIR, "data")
USERS_FILE = os.path.join(DATA_DIR, "users.json")
JOURNEYS_FILE = os.path.join(DATA_DIR, 'journeys.json')
UPLOAD_DIR = "uploads"
UPLOAD_DIR = os.path.join(BASE_DIR, "uploads")
ALLOWED_IMAGE_EXTENSIONS = {"png", "jpg", "jpeg", "gif", "webp"}
os.makedirs(DATA_DIR, exist_ok=True)
os.makedirs(UPLOAD_DIR, exist_ok=True)

View File

@ -227,6 +227,20 @@
border-radius: var(--radius-2);
margin: var(--size-2) 0;
}
.chapter-images {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
gap: var(--size-3);
margin: var(--size-3) 0;
}
.chapter-images img {
width: 100%;
aspect-ratio: 4 / 3;
object-fit: cover;
border-radius: var(--radius-2);
box-shadow: var(--shadow-2);
background: var(--surface-2);
}
.chapter-content {
line-height: 1.6;
}
@ -290,7 +304,7 @@
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 images = getMarkerImagesHtml(marker.images || (marker.image ? [marker.image] : []));
const video = marker.videoUrl ? `<iframe src="${marker.videoUrl}" frameborder="0" allowfullscreen></iframe>` : '';
chaptersHtml += `
<div class="chapter" data-marker-id="${marker.id || idx}">
@ -298,7 +312,7 @@
<h3>${escapeHtml(title)}</h3>
${date}
</div>
${image}
${images}
${video}
<div class="chapter-content">${content}</div>
</div>
@ -332,6 +346,36 @@
document.getElementById('delete-journey-btn')?.addEventListener('click', deleteJourney);
}
}
function getUploadUrl(path) {
if (!path) return '';
if (path.startsWith('http')) return path;
return API_BASE.replace('/api', path);
}
function getMarkerImagesHtml(images) {
if (!images || !images.length) return '';
const imageHtml = images.map((image) => {
const url = typeof image === 'string' ? image : image.url;
const alt = typeof image === 'string' ? 'Journey image' : image.originalName || 'Journey image';
if (!url) return '';
return `<img src="${escapeAttribute(getUploadUrl(url))}" alt="${escapeAttribute(alt)}">`;
}).join('');
if (!imageHtml) return '';
return `<div class="chapter-images">${imageHtml}</div>`;
}
function escapeAttribute(value) {
return String(value || '').replace(/[&<>"']/g, (char) => ({
'&': '&amp;',
'<': '&lt;',
'>': '&gt;',
'"': '&quot;',
"'": '&#39;'
})[char]);
}
async function deleteJourney() {
if (!confirm('Delete this journey permanently? All chapters and comments will be lost.')) return;
@ -454,4 +498,4 @@
});
</script>
</body>
</html>
</html>

View File

@ -120,6 +120,8 @@
color: var(--gray-8);
}
input[type="text"],
input[type="date"],
input[type="file"],
textarea,
select {
width: 100%;
@ -169,6 +171,41 @@
color: var(--gray-6);
margin-bottom: var(--size-2);
}
.image-preview-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(88px, 1fr));
gap: var(--size-2);
margin-top: var(--size-2);
}
.image-preview {
position: relative;
aspect-ratio: 1;
border-radius: var(--radius-2);
overflow: hidden;
border: 1px solid var(--surface-4);
background: var(--surface-1);
}
.image-preview img {
width: 100%;
height: 100%;
object-fit: cover;
display: block;
}
.remove-image {
position: absolute;
top: 4px;
right: 4px;
width: 24px;
height: 24px;
border: none;
border-radius: var(--radius-round);
background: rgba(0, 0, 0, 0.7);
color: white;
cursor: pointer;
display: inline-flex;
align-items: center;
justify-content: center;
}
.btn {
padding: var(--size-2) var(--size-4);
border: none;
@ -301,6 +338,68 @@
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 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>
`;
}
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');
@ -330,8 +429,9 @@
<textarea class="marker-description" data-index="${idx}" rows="3" placeholder="Write about this chapter...">${escapeHtml(marker.description || '')}</textarea>
</div>
<div class="form-group">
<label>Image URL</label>
<input type="text" class="marker-image" data-index="${idx}" value="${escapeHtml(marker.image || '')}" placeholder="https://...">
<label>Images</label>
<input type="file" class="marker-images" data-index="${idx}" accept="image/*" multiple>
${renderMarkerImages(marker, idx)}
</div>
<div class="form-group">
<label>Video URL</label>
@ -368,10 +468,25 @@
markersData[idx].description = textarea.value;
});
});
document.querySelectorAll('.marker-image').forEach(input => {
input.addEventListener('input', (e) => {
document.querySelectorAll('.marker-images').forEach(input => {
input.addEventListener('change', async () => {
const idx = parseInt(input.dataset.index);
markersData[idx].image = input.value;
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();
});
});
document.querySelectorAll('.marker-video').forEach(input => {
@ -397,7 +512,7 @@
document.getElementById('journey-title').value = journey.title;
document.getElementById('journey-description').value = journey.description || '';
document.getElementById('journey-visibility').value = journey.visibility || 'private';
markersData = journey.markers || [];
markersData = (journey.markers || []).map(normalizeMarkerImages);
console.log('Markers data loaded:', markersData);
renderMarkers();
document.getElementById('form-title').textContent = 'Edit Journey';
@ -428,7 +543,7 @@
title: m.title || '',
date: m.date || '',
description: m.description || '',
image: m.image || '',
images: m.images || [],
videoUrl: m.videoUrl || ''
}));
@ -473,7 +588,7 @@
title: 'New Chapter',
date: '',
description: '',
image: '',
images: [],
videoUrl: ''
});
renderMarkers();
@ -501,4 +616,4 @@
});
</script>
</body>
</html>
</html>