Add media upload JS + CSS: image preview in composer, AJAX upload on submit, Twitter-style gallery grid, video embed styling

This commit is contained in:
Krystie
2026-05-18 11:51:21 -07:00
parent 6c9b06ccb2
commit 07ac9f112d
2 changed files with 290 additions and 0 deletions
+189
View File
@@ -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;
}
+101
View File
@@ -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 '<div class="beep-media-preview-item" data-preview-id="' + img.id + '">' +
'<img src="' + img.previewUrl + '">' +
'<button class="beep-media-remove" type="button" title="Remove" data-remove="' + img.id + '">&times;</button>' +
'</div>';
}).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();