diff --git a/assets/beep.css b/assets/beep.css index 01b530a..c1d94ba 100755 --- a/assets/beep.css +++ b/assets/beep.css @@ -758,3 +758,192 @@ opacity: 0.5 !important; cursor: default !important; } + +/* ============================================================ + Media — images & video + ============================================================ */ + +/* ---------- Composer media button ---------- */ +.beep-compose-tools { + display: flex; + align-items: center; + gap: 4px; + margin-right: auto; +} + +.beep-media-btn { + display: inline-flex; + align-items: center; + justify-content: center; + width: 32px; + height: 32px; + border-radius: 50%; + color: var(--beep-accent); + cursor: pointer; + transition: background-color 0.15s ease, color 0.15s ease; +} + +.beep-media-btn:hover { + background: rgba(29, 155, 240, 0.10); + color: var(--beep-accent); +} + +.beep-media-btn svg { + display: block; + flex-shrink: 0; +} + +.beep-media-count { + font-size: 12px; + color: var(--beep-text-secondary); + font-variant-numeric: tabular-nums; +} + +/* ---------- Composer media preview ---------- */ +.beep-compose-media-preview { + margin-top: 10px; + margin-bottom: 4px; + display: flex; + flex-wrap: wrap; + gap: 6px; +} + +.beep-compose-media-preview[hidden] { + display: none; +} + +.beep-media-preview-item { + position: relative; + width: 80px; + height: 80px; + border-radius: 8px; + overflow: hidden; + flex-shrink: 0; + border: 1px solid var(--beep-border); +} + +.beep-media-preview-item img { + width: 100%; + height: 100%; + object-fit: cover; + display: block; +} + +.beep-media-remove { + position: absolute; + top: 2px; + right: 2px; + width: 22px; + height: 22px; + background: rgba(0, 0, 0, 0.6); + border: 0; + border-radius: 50%; + color: #fff; + font-size: 16px; + line-height: 1; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + padding: 0; + transition: background-color 0.15s ease; +} + +.beep-media-remove:hover { + background: rgba(244, 33, 46, 0.8); +} + +/* ---------- Image gallery in posts (Twitter-style grid) ---------- */ +.beep-media-attachments { + margin: 6px 0 2px; +} + +.beep-gallery { + display: grid; + gap: 4px; + border-radius: 12px; + overflow: hidden; + border: 1px solid var(--beep-border); +} + +.beep-gallery-1 { + grid-template-columns: 1fr; +} + +.beep-gallery-2 { + grid-template-columns: 1fr 1fr; +} + +.beep-gallery-3 { + grid-template-columns: 1fr 1fr; + grid-template-rows: 1fr 1fr; +} + +.beep-gallery-3 .beep-gallery-item:first-child { + grid-row: span 2; +} + +.beep-gallery-4 { + grid-template-columns: 1fr 1fr; + grid-template-rows: 1fr 1fr; +} + +.beep-gallery-item { + position: relative; + overflow: hidden; + background: #f0f0f0; + min-height: 80px; +} + +.beep-gallery-item img { + width: 100%; + height: 100%; + object-fit: cover; + display: block; + transition: filter 0.15s ease; +} + +.beep-gallery-item:hover img { + filter: brightness(0.92); +} + +/* Lightbox link */ +.beep-gallery-item a { + display: block; + width: 100%; + height: 100%; +} + +/* ---------- Video embed ---------- */ +.beep-video-embed { + margin: 6px 0 2px; + border-radius: 12px; + overflow: hidden; + border: 1px solid var(--beep-border); +} + +.beep-video-embed iframe, +.beep-video-embed video { + display: block; + width: 100%; + max-height: 375px; +} + +/* Aspect ratio container for responsive video */ +.beep-video-embed { + position: relative; +} + +.beep-video-embed iframe { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; +} + +/* Make the footer layout handle the new left-aligned tools */ +.beep-compose-footer { + justify-content: flex-end; + gap: 8px; +} diff --git a/assets/beep.js b/assets/beep.js index 66248cc..68e8b2b 100755 --- a/assets/beep.js +++ b/assets/beep.js @@ -16,6 +16,7 @@ var compose = feed.querySelector('[data-beep-compose]'); if (compose) initCompose(compose, feed); initSortTabs(feed); + initMedia(compose, feed); feed.addEventListener('click', onClick); feed.addEventListener('input', onInput); feed.addEventListener('keydown', onKeydown); @@ -63,6 +64,15 @@ } input.value = ''; refresh(); + + // Upload pending images to this new beep + var pending = compose._beepPendingImages; + if (pending && pending.length && data.id) { + uploadPendingImages(data.id, pending, compose).then(function () { + // Refresh the beep in the DOM to show uploaded images + reloadFeed(feed, 'recent'); + }); + } }) .catch(function () { window.alert('Could not post. Please try again.'); @@ -325,6 +335,97 @@ return Array.from(s).length; } + + /* ---------- Media upload (images) ---------- */ + function initMedia(compose, feed) { + var input = compose.querySelector('[data-beep-media-input]'); + var preview = compose.querySelector('[data-beep-media-preview]'); + var counter = compose.querySelector('[data-beep-media-count]'); + if (!input) return; + + var pendingImages = []; + + input.addEventListener('change', function (e) { + var files = Array.prototype.slice.call(e.target.files); + if (!files.length) return; + var room = 4 - pendingImages.length; + if (room <= 0) { input.value = ''; return; } + files = files.slice(0, room); + files.forEach(function (file) { + if (file.size > 10 * 1024 * 1024) { + window.alert('Image must be under 10 MB: ' + file.name); + return; + } + var reader = new FileReader(); + reader.onload = function (ev) { + var url = ev.target.result; + var id = 'pending-' + Date.now() + '-' + Math.random().toString(36).substr(2, 6); + pendingImages.push({ file: file, previewUrl: url, id: id }); + renderPreviews(); + }; + reader.readAsDataURL(file); + }); + input.value = ''; + }); + + function renderPreviews() { + if (!pendingImages.length) { + preview.innerHTML = ''; + preview.hidden = true; + if (counter) counter.hidden = true; + return; + } + preview.hidden = false; + if (counter) { + counter.textContent = pendingImages.length + '/4'; + counter.hidden = false; + } + preview.innerHTML = pendingImages.map(function (img) { + return '
' + + '' + + '' + + '
'; + }).join(''); + } + + preview.addEventListener('click', function (e) { + var btn = e.target.closest('[data-remove]'); + if (!btn) return; + var id = btn.dataset.remove; + pendingImages = pendingImages.filter(function (img) { return img.id !== id; }); + renderPreviews(); + }); + + compose._beepPendingImages = pendingImages; + } + + function uploadPendingImages(postId, pendingImages, compose) { + if (!pendingImages || !pendingImages.length) return Promise.resolve(); + var promises = pendingImages.map(function (img) { + var formData = new FormData(); + formData.append('file', img.file); + return fetch(config.restUrl + 'media/' + postId, { + method: 'POST', + headers: { 'X-WP-Nonce': config.nonce }, + credentials: 'same-origin', + body: formData + }) + .then(handleResponse) + .catch(function (err) { + console.warn('Image upload failed:', img.file.name, err); + }); + }); + return Promise.all(promises).then(function () { + pendingImages.length = 0; + if (compose) { + var preview = compose.querySelector('[data-beep-media-preview]'); + if (preview) { preview.innerHTML = ''; preview.hidden = true; } + var counter = compose.querySelector('[data-beep-media-count]'); + if (counter) counter.hidden = true; + } + }); + } + function handleResponse(res) { if (!res.ok) return res.json().then(function (j) { throw j; }, function () { throw res; }); return res.json();