7 changed files with 580 additions and 149 deletions
@ -0,0 +1,183 @@
@@ -0,0 +1,183 @@
|
||||
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; |
||||
} |
||||
} |
||||
Loading…
Reference in new issue