clone of github.com/decent-newsroom/newsroom
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

183 lines
6.6 KiB

import { Controller } from '@hotwired/stimulus';
/*
* Media Loader Controller
* Handles "Load More" functionality for author media galleries
* Fetches additional media items from cache and appends to masonry grid
*/
export default class extends Controller {
static targets = ['grid', 'button', 'status'];
static values = {
npub: String,
page: { type: Number, default: 2 },
total: Number
};
connect() {
this.isLoading = false;
}
async loadMore() {
if (this.isLoading) return;
this.isLoading = true;
this.buttonTarget.disabled = true;
this.buttonTarget.textContent = 'Loading...';
try {
const url = `/p/${this.npubValue}/media/load-more?page=${this.pageValue}`;
const response = await fetch(url);
const data = await response.json();
// Add new media items to the grid
data.events.forEach(event => {
const item = this.createMediaItem(event);
this.gridTarget.insertAdjacentHTML('beforeend', item);
});
this.pageValue++;
// Update status
const currentCount = this.gridTarget.querySelectorAll('.masonry-item').length;
this.statusTarget.textContent = `Showing ${currentCount} of ${data.total} media items`;
// Hide button if no more items
if (!data.hasMore) {
this.buttonTarget.style.display = 'none';
this.statusTarget.textContent = `All ${data.total} media items loaded`;
}
} catch (error) {
console.error('Error loading more media:', error);
this.buttonTarget.textContent = 'Error - Click to retry';
} finally {
this.isLoading = false;
this.buttonTarget.disabled = false;
if (this.buttonTarget.textContent === 'Loading...') {
this.buttonTarget.textContent = 'Load More';
}
}
}
createMediaItem(event) {
// Extract title
let title = null;
let firstImageUrl = null;
let firstVideoUrl = null;
let imageAlt = null;
let isVideo = false;
// Find title tag
event.tags.forEach(tag => {
if (tag[0] === 'title') {
title = tag[1];
}
});
// Extract first image from imeta tags
event.tags.forEach(tag => {
if (tag[0] === 'imeta') {
let videoUrl = null;
let imageUrl = null;
let previewImage = null;
for (let i = 1; i < tag.length; i++) {
const param = tag[i];
if (param.startsWith('url ')) {
const potentialUrl = param.substring(4);
if (/\.(mp4|webm|ogg|mov)$/i.test(potentialUrl) || /video/i.test(potentialUrl)) {
videoUrl = potentialUrl;
isVideo = true;
} else {
imageUrl = potentialUrl;
}
} else if (param.startsWith('image ')) {
previewImage = param.substring(6);
} else if (param.startsWith('alt ')) {
imageAlt = param.substring(4);
}
}
if (videoUrl && !firstVideoUrl) {
firstVideoUrl = videoUrl;
if (previewImage && !firstImageUrl) {
firstImageUrl = previewImage;
} else if (imageUrl && !firstImageUrl) {
firstImageUrl = imageUrl;
}
}
if (!videoUrl && !firstImageUrl) {
if (imageUrl) {
firstImageUrl = imageUrl;
} else if (previewImage) {
firstImageUrl = previewImage;
}
}
}
});
const eventDate = new Date(event.created_at * 1000).toLocaleDateString('en-US', {
month: 'short',
day: 'numeric',
year: 'numeric'
});
const contentPreview = event.content && event.content.length > 100
? event.content.substring(0, 100) + '...'
: event.content || '';
let imageHtml = '';
if (firstImageUrl) {
imageHtml = `
<div class="masonry-image-container">
<img src="${this.escapeHtml(firstImageUrl)}"
alt="${this.escapeHtml(imageAlt || title || (isVideo ? 'Video' : 'Picture'))}"
class="masonry-image"
loading="lazy" />
${isVideo ? `
<div class="video-overlay">
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 24 24" fill="white" opacity="0.9">
<path d="M8 5v14l11-7z"/>
</svg>
</div>
` : ''}
${title || contentPreview ? `
<div class="masonry-hover-caption">
${title ? `<h4>${this.escapeHtml(title)}</h4>` : ''}
${contentPreview ? `<p>${this.escapeHtml(contentPreview)}</p>` : ''}
</div>
` : ''}
</div>
`;
} else if (isVideo) {
imageHtml = `
<div class="masonry-image-container video-no-preview">
<div class="video-placeholder">
<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 24 24" fill="currentColor" opacity="0.4">
<path d="M8 5v14l11-7z"/>
</svg>
</div>
${title || contentPreview ? `
<div class="masonry-hover-caption">
${title ? `<h4>${this.escapeHtml(title)}</h4>` : ''}
${contentPreview ? `<p>${this.escapeHtml(contentPreview)}</p>` : ''}
</div>
` : ''}
</div>
`;
}
return `
<div class="masonry-item">
<a href="/e/${event.noteId}" class="masonry-link">
${imageHtml}
</a>
</div>
`;
}
escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
}