diff options
| author | Guillermo Serrahima <dev@vylion.com> | 2026-06-02 03:15:03 +0200 |
|---|---|---|
| committer | Guillermo Serrahima <dev@vylion.com> | 2026-07-14 23:11:05 +0200 |
| commit | 10e54b1bb7979dc21911afce0d20cffc4a2d038a (patch) | |
| tree | a0513d16a7896031efff51db61f04a50b95f719e /frontend | |
| parent | 9f0a9a6779f00786e4728ad5c3fad36814a293b0 (diff) | |
| download | llostre-10e54b1bb7979dc21911afce0d20cffc4a2d038a.tar.xz llostre-10e54b1bb7979dc21911afce0d20cffc4a2d038a.zip | |
added images
Diffstat (limited to 'frontend')
| -rw-r--r-- | frontend/public/image-unavailable.svg | 1 | ||||
| -rw-r--r-- | frontend/src/lightbox.ts | 82 | ||||
| -rw-r--r-- | frontend/src/main.ts | 31 | ||||
| -rw-r--r-- | frontend/src/style.css | 137 | ||||
| -rw-r--r-- | frontend/vite.config.ts | 5 |
5 files changed, 254 insertions, 2 deletions
diff --git a/frontend/public/image-unavailable.svg b/frontend/public/image-unavailable.svg new file mode 100644 index 0000000..7aa1741 --- /dev/null +++ b/frontend/public/image-unavailable.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M12 22C6.47715 22 2 17.5228 2 12C2 6.47715 6.47715 2 12 2C17.5228 2 22 6.47715 22 12C22 17.5228 17.5228 22 12 22ZM11 15V17H13V15H11ZM11 7V13H13V7H11Z"></path></svg>
\ No newline at end of file diff --git a/frontend/src/lightbox.ts b/frontend/src/lightbox.ts new file mode 100644 index 0000000..cdc628e --- /dev/null +++ b/frontend/src/lightbox.ts @@ -0,0 +1,82 @@ +import type { Note } from './main'; + +let currentMedia: Note['media'] = []; +let currentIndex = 0; + +function updateLightbox() { + const item = currentMedia![currentIndex]; + const img = document.getElementById('lightbox-img') as HTMLImageElement; + const alt = document.getElementById('lightbox-alt')!; + const prev = document.getElementById('lightbox-prev')!; + const next = document.getElementById('lightbox-next')!; + + img.src = item.url; + img.alt = item.alt || ''; + + if (item.alt) { + alt.textContent = item.alt; + alt.style.display = 'block'; + } else { + alt.style.display = 'none'; + } + + prev.style.display = currentIndex > 0 ? 'block' : 'none'; + next.style.display = currentIndex < currentMedia!.length - 1 ? 'block' : 'none'; +} + +function navigateLightbox(dir: number) { + const next = currentIndex + dir; + if (next < 0 || next >= currentMedia!.length) return; + currentIndex = next; + updateLightbox(); +} + +function lightboxKeyHandler(e: KeyboardEvent) { + if (e.key === 'Escape') closeLightbox(); + if (e.key === 'ArrowLeft') navigateLightbox(-1); + if (e.key === 'ArrowRight') navigateLightbox(1); +} + +export function openLightbox(media: NonNullable<Note['media']>, index: number) { + currentMedia = media; + currentIndex = index; + + const overlay = document.createElement('div'); + overlay.id = 'lightbox'; + overlay.addEventListener('click', e => { + if (e.target === overlay) closeLightbox(); + }); + + const close = document.createElement('button'); + close.id = 'lightbox-close'; + close.textContent = '×'; + close.addEventListener('click', closeLightbox); + overlay.appendChild(close); + + const img = document.createElement('img'); + img.id = 'lightbox-img'; + + const alt = document.createElement('div'); + alt.id = 'lightbox-alt'; + + const prev = document.createElement('button'); + prev.id = 'lightbox-prev'; + prev.textContent = '‹'; + prev.addEventListener('click', () => navigateLightbox(-1)); + + const next = document.createElement('button'); + next.id = 'lightbox-next'; + next.textContent = '›'; + next.addEventListener('click', () => navigateLightbox(1)); + + overlay.append(prev, img, alt, next); + document.body.appendChild(overlay); + updateLightbox(); + + document.addEventListener('keydown', lightboxKeyHandler); +} + +export function closeLightbox() { + document.getElementById('lightbox')?.remove(); + document.removeEventListener('keydown', lightboxKeyHandler); +} diff --git a/frontend/src/main.ts b/frontend/src/main.ts index 056e9f0..8f43d25 100644 --- a/frontend/src/main.ts +++ b/frontend/src/main.ts @@ -1,7 +1,8 @@ import { marked } from 'marked'; import './style.css' +import { openLightbox } from './lightbox'; -interface Note { +export interface Note { uri: string; authorDID: string; authorHandle: string; @@ -174,11 +175,39 @@ function createNoteElement(note: Note): HTMLElement { note.media.forEach(item => { const img = document.createElement('img'); img.src = item.url; + img.src = item.url; img.alt = item.alt || ''; + img.className = item.alt ? 'has-alt' : 'no-alt'; if (item.width && item.height) { img.width = item.width; img.height = item.height; } + img.style.cursor = 'pointer'; + const index = note.media!.indexOf(item); + img.addEventListener('click', () => openLightbox(note.media!, index)); + mediaDiv.appendChild(img); + img.onerror = () => { + const errDiv = document.createElement('div'); + errDiv.className = 'note-media-error'; + + const icon = document.createElement('img'); + icon.src = '/image-unavailable.svg'; + icon.alt = ''; + errDiv.appendChild(icon); + + const label = document.createElement('div'); + label.className = 'info'; + label.textContent = 'Image unavailable'; + errDiv.appendChild(label); + + if (item.alt) { + const altText = document.createElement('div'); + altText.textContent = item.alt; + errDiv.appendChild(altText); + } + + img.replaceWith(errDiv); + }; mediaDiv.appendChild(img); }); content.appendChild(mediaDiv); diff --git a/frontend/src/style.css b/frontend/src/style.css index 8e1d095..50836d8 100644 --- a/frontend/src/style.css +++ b/frontend/src/style.css @@ -161,6 +161,141 @@ h1 { white-space: pre-wrap; } +.note-media { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(45%, 1fr)); + grid-auto-rows: 200px; + gap: 0.5rem; + margin-top: 0.5rem; +} + +.note-media:has(> :only-child) { + grid-auto-rows: auto; + justify-content: start; +} + +.note-media:has(> :only-child) img { + width: auto; + max-width: 100%; + height: auto; + max-height: 400px; + object-fit: contain; +} + +.note-media img, +.note-media-error { + width: 100%; + height: 100%; + box-sizing: border-box; + overflow: hidden; + border-radius: 8px; +} + +.note-media img { + object-fit: cover; +} + +.note-media img.has-alt { + border: 2px solid var(--accent); +} + +.note-media img.no-alt { + border: 2px solid var(--error); +} + +.note-media-error { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 0.25rem; + padding: 1rem; + background: var(--bg-secondary); + border: 2px solid var(--error); + text-align: center; + font-size: 0.85rem; +} + +.note-media-error img { + width: 48px; + height: 48px; + border: none !important; + object-fit: contain; + height: auto; + max-height: unset; + grid-auto-rows: unset; +} + +#lightbox { + position: fixed; + inset: 0; + background: rgba(0, 0, 0, 0.85); + display: flex; + align-items: center; + justify-content: center; + z-index: 1000; + flex-direction: column; + gap: 1rem; + padding: 2rem; +} + +#lightbox-img { + max-width: 90vw; + max-height: 80vh; + object-fit: contain; + border-radius: 8px; + border: none !important; +} + +#lightbox-alt { + color: white; + background: rgba(0, 0, 0, 0.6); + padding: 0.5rem 1rem; + border-radius: 8px; + max-width: 60ch; + text-align: center; + font-size: 0.9rem; +} + +#lightbox-prev, +#lightbox-next { + position: fixed; + top: 50%; + transform: translateY(-50%); + background: rgba(0, 0, 0, 0.5); + color: white; + border: none; + font-size: 2rem; + padding: 0.5rem 1rem; + cursor: pointer; + border-radius: 4px; +} + +#lightbox-prev { left: 1rem; } +#lightbox-next { right: 1rem; } + +#lightbox-prev:hover, +#lightbox-next:hover { + background: rgba(0, 0, 0, 0.8); +} + +#lightbox-close { + position: fixed; + top: 1rem; + right: 1rem; + background: rgba(0, 0, 0, 0.5); + color: white; + border: none; + font-size: 1.5rem; + width: 2rem; + height: 2rem; + cursor: pointer; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; +} + .muted { color: var(--text-muted); font-size: 12px; @@ -173,7 +308,7 @@ h1 { } .spinner { - border: 8px solid #f3f3f3; + border: 8px solid var(--border); border-top: 8px solid var(--accent); border-radius: 50%; width: 64px; diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts index 4420756..e8228d4 100644 --- a/frontend/vite.config.ts +++ b/frontend/vite.config.ts @@ -10,4 +10,9 @@ export default defineConfig({ }, }, }, + server: { + proxy: { + '/api': 'http://localhost:8080', + }, + }, }) |
