aboutsummaryrefslogtreecommitdiff
path: root/frontend
diff options
context:
space:
mode:
authorGuillermo Serrahima <dev@vylion.com>2026-06-02 03:15:03 +0200
committerGuillermo Serrahima <dev@vylion.com>2026-07-14 23:11:05 +0200
commit10e54b1bb7979dc21911afce0d20cffc4a2d038a (patch)
treea0513d16a7896031efff51db61f04a50b95f719e /frontend
parent9f0a9a6779f00786e4728ad5c3fad36814a293b0 (diff)
downloadllostre-10e54b1bb7979dc21911afce0d20cffc4a2d038a.tar.xz
llostre-10e54b1bb7979dc21911afce0d20cffc4a2d038a.zip
added images
Diffstat (limited to 'frontend')
-rw-r--r--frontend/public/image-unavailable.svg1
-rw-r--r--frontend/src/lightbox.ts82
-rw-r--r--frontend/src/main.ts31
-rw-r--r--frontend/src/style.css137
-rw-r--r--frontend/vite.config.ts5
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',
+ },
+ },
})