← back to Wallco Ai
public/js/social-share.js
398 lines
/* ───────────────────────────────────────────────────────────────────────────
* social-share.js — wallco.ai universal social-share layer (client-side).
*
* Loaded on the /designs grid and every /design/:id page. It:
* • adds a "Share" affordance to every design card (incl. lazy-loaded ones)
* • upgrades the detail-page #cta-share button into a full share sheet
* • adds a "Make a Reel" button on the detail page
* • the share sheet posts to Facebook / X / Pinterest / LinkedIn / WhatsApp /
* Email, copies a ready-made caption, downloads the image for Instagram &
* TikTok (which have no web-post URL), and links into the /reel maker.
*
* No dependencies. Self-injecting styles. Safe to load more than once.
* ───────────────────────────────────────────────────────────────────────── */
(function () {
'use strict';
if (window.__wallcoShareLoaded) return;
window.__wallcoShareLoaded = true;
var ORIGIN = location.origin;
// ── styles ────────────────────────────────────────────────────────────────
var css = '\
.ss-overlay{position:fixed;inset:0;background:rgba(20,15,8,.62);backdrop-filter:blur(3px);\
display:none;align-items:center;justify-content:center;z-index:9000;padding:20px}\
.ss-overlay.on{display:flex}\
.ss-modal{background:#faf8f3;color:#2a2014;width:100%;max-width:420px;border-radius:14px;\
box-shadow:0 18px 60px rgba(0,0,0,.4);overflow:hidden;font:14px/1.5 \"Helvetica Neue\",Arial,sans-serif;\
max-height:92vh;overflow-y:auto;animation:ssin .18s ease}\
@keyframes ssin{from{transform:translateY(14px) scale(.98);opacity:0}to{transform:none;opacity:1}}\
.ss-head{display:flex;gap:12px;align-items:center;padding:16px 16px 12px;border-bottom:1px solid #e6dcc6}\
.ss-head img{width:52px;height:52px;border-radius:8px;object-fit:cover;border:1px solid #ddd3bf;flex:none}\
.ss-head .t{flex:1;min-width:0}\
.ss-head .t b{display:block;font:400 16px Georgia,serif;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}\
.ss-head .t span{font-size:11.5px;color:#9b8f78}\
.ss-x{border:0;background:transparent;font-size:20px;cursor:pointer;color:#9b8f78;line-height:1;padding:4px}\
.ss-body{padding:14px 16px 18px}\
.ss-lbl{font:600 10.5px \"Helvetica Neue\",Arial,sans-serif;letter-spacing:.09em;text-transform:uppercase;\
color:#9b8f78;margin:14px 0 8px}\
.ss-lbl:first-child{margin-top:0}\
.ss-net{display:grid;grid-template-columns:repeat(4,1fr);gap:8px}\
.ss-net button,.ss-net a{display:flex;flex-direction:column;align-items:center;gap:6px;border:1px solid #ddd3bf;\
background:#fff;border-radius:10px;padding:11px 4px;cursor:pointer;text-decoration:none;color:#2a2014;\
font:600 10.5px \"Helvetica Neue\",Arial,sans-serif;transition:border-color .12s,transform .1s}\
.ss-net button:hover,.ss-net a:hover{border-color:#2a2014;transform:translateY(-1px)}\
.ss-ico{width:30px;height:30px;border-radius:50%;display:flex;align-items:center;justify-content:center;\
color:#fff;font-size:14px;font-weight:700}\
.ss-act{display:flex;flex-direction:column;gap:7px;margin-top:4px}\
.ss-act button{display:flex;align-items:center;gap:9px;border:1px solid #ddd3bf;background:#fff;\
border-radius:9px;padding:10px 13px;cursor:pointer;font:500 13px \"Helvetica Neue\",Arial,sans-serif;\
color:#2a2014;text-align:left;width:100%}\
.ss-act button:hover{border-color:#2a2014}\
.ss-act button .k{font-size:15px;width:20px;text-align:center;flex:none}\
.ss-postall{margin-top:12px;width:100%;border:0;border-radius:9px;background:#2a2014;color:#faf8f3;\
padding:13px;font:600 13.5px \"Helvetica Neue\",Arial,sans-serif;cursor:pointer}\
.ss-postall:hover{background:#3a2e1c}\
.ss-reel{margin-top:8px;width:100%;display:flex;align-items:center;justify-content:center;gap:8px;\
border:1px solid #c9a14b;border-radius:9px;background:#fffaf0;color:#7a5e1a;padding:12px;\
font:600 13px \"Helvetica Neue\",Arial,sans-serif;cursor:pointer;text-decoration:none}\
.ss-reel:hover{background:#fff3da}\
.ss-note{font-size:11px;color:#9b8f78;line-height:1.5;margin-top:11px}\
.ss-toast{position:fixed;left:50%;bottom:30px;transform:translateX(-50%);background:#2a2014;color:#faf8f3;\
padding:11px 22px;border-radius:30px;font:13px \"Helvetica Neue\",Arial,sans-serif;z-index:9100;\
opacity:0;transition:opacity .25s;pointer-events:none}\
.ss-toast.on{opacity:1}\
.chov-btn.ss-card-btn{cursor:pointer}\
.ss-fab{position:absolute;top:8px;left:8px;width:30px;height:30px;border-radius:50%;border:0;\
background:rgba(255,255,255,.92);box-shadow:0 1px 5px rgba(0,0,0,.22);cursor:pointer;font-size:13px;\
color:#2a2014;display:flex;align-items:center;justify-content:center;opacity:0;transition:opacity .14s;z-index:5}\
.design-card:hover .ss-fab,.design-card:focus-within .ss-fab{opacity:1}\
.design-card.has-bulk-cb .ss-fab{left:38px}';
var st = document.createElement('style'); st.textContent = css; document.head.appendChild(st);
// ── toast ────────────────────────────────────────────────────────────────
var toastEl, toastT;
function toast(msg) {
if (!toastEl) { toastEl = document.createElement('div'); toastEl.className = 'ss-toast'; document.body.appendChild(toastEl); }
toastEl.textContent = msg; toastEl.classList.add('on');
clearTimeout(toastT); toastT = setTimeout(function () { toastEl.classList.remove('on'); }, 2400);
}
// ── helpers ──────────────────────────────────────────────────────────────
function absUrl(u) { return /^https?:/.test(u) ? u : ORIGIN + (u.charAt(0) === '/' ? '' : '/') + u; }
function pageUrl(id) { return ORIGIN + '/design/' + id; }
function popup(url) {
window.open(url, '_blank', 'noopener,noreferrer,width=640,height=620');
}
function copyText(txt) {
if (navigator.clipboard && navigator.clipboard.writeText) {
return navigator.clipboard.writeText(txt);
}
return new Promise(function (res, rej) {
try {
var ta = document.createElement('textarea'); ta.value = txt;
ta.style.position = 'fixed'; ta.style.opacity = '0'; document.body.appendChild(ta);
ta.select(); document.execCommand('copy'); document.body.removeChild(ta); res();
} catch (e) { rej(e); }
});
}
function downloadImage(imgUrl, name) {
return fetch(imgUrl).then(function (r) { return r.blob(); }).then(function (b) {
var a = document.createElement('a');
a.href = URL.createObjectURL(b);
a.download = (name || 'wallco-design').replace(/[^\w.-]+/g, '-') + '.png';
document.body.appendChild(a); a.click();
setTimeout(function () { URL.revokeObjectURL(a.href); a.remove(); }, 4000);
});
}
// caption cache
var capCache = {};
function getCaption(id) {
if (capCache[id]) return Promise.resolve(capCache[id]);
return fetch('/api/social/caption/' + id).then(function (r) { return r.json(); })
.then(function (j) {
capCache[id] = j && j.full ? j : { full: 'Designed at wallco.ai', caption: '', hashtags: '' };
return capCache[id];
}).catch(function () { return { full: 'Designed at wallco.ai', caption: '', hashtags: '' }; });
}
// native share of the image file (mobile → Instagram / TikTok / etc.)
function shareImageNative(ctx, capFull) {
return fetch(ctx.imageUrl).then(function (r) { return r.blob(); }).then(function (blob) {
var file = new File([blob], 'wallco-design.png', { type: blob.type || 'image/png' });
if (navigator.canShare && navigator.canShare({ files: [file] })) {
return navigator.share({ files: [file], title: ctx.title, text: capFull });
}
if (navigator.share) return navigator.share({ title: ctx.title, text: capFull, url: ctx.pageUrl });
throw new Error('no native share');
});
}
// ── share modal (built once, reused) ─────────────────────────────────────
var overlay, modal, cur = null;
function buildModal() {
overlay = document.createElement('div');
overlay.className = 'ss-overlay';
overlay.innerHTML =
'<div class="ss-modal" role="dialog" aria-modal="true" aria-label="Share design">' +
'<div class="ss-head">' +
'<img class="ss-thumb" alt="">' +
'<div class="t"><b class="ss-title"></b><span>Share to your socials</span></div>' +
'<button class="ss-x" aria-label="Close">✕</button>' +
'</div>' +
'<div class="ss-body">' +
'<div class="ss-lbl">Post to a network</div>' +
'<div class="ss-net">' +
netBtn('fb', 'Facebook', '#1877f2', 'f') +
netBtn('x', 'X', '#0f1419', '𝕏') +
netBtn('pin', 'Pinterest', '#e60023', 'P') +
netBtn('li', 'LinkedIn', '#0a66c2', 'in') +
netBtn('wa', 'WhatsApp', '#25d366', '☎') +
netBtn('ig', 'Instagram', 'linear-gradient(45deg,#f09433,#e6683c,#dc2743,#cc2366,#bc1888)', '◉') +
netBtn('tt', 'TikTok', '#010101', '♪') +
netBtn('em', 'Email', '#6b5e4a', '✉') +
'</div>' +
'<button class="ss-postall">↗ Post to all networks</button>' +
'<a class="ss-reel" href="/reel">🎬 Make a video reel with this design</a>' +
'<div class="ss-lbl">Or grab it yourself</div>' +
'<div class="ss-act">' +
'<button data-a="copylink"><span class="k">🔗</span> Copy link to this design</button>' +
'<button data-a="copycap"><span class="k">✎</span> Copy caption + hashtags</button>' +
'<button data-a="dl"><span class="k">⬇</span> Download image</button>' +
'</div>' +
'<p class="ss-note">Instagram & TikTok don’t allow posting from a browser. ' +
'Those buttons download the image and copy a caption — then post it in the app. ' +
'On a phone, they open your share sheet directly.</p>' +
'</div>' +
'</div>';
document.body.appendChild(overlay);
overlay.addEventListener('click', function (e) { if (e.target === overlay) close(); });
overlay.querySelector('.ss-x').addEventListener('click', close);
document.addEventListener('keydown', function (e) { if (e.key === 'Escape') close(); });
overlay.querySelectorAll('.ss-net [data-net]').forEach(function (b) {
b.addEventListener('click', function (e) { e.preventDefault(); doNetwork(b.getAttribute('data-net')); });
});
overlay.querySelector('.ss-postall').addEventListener('click', postAll);
overlay.querySelectorAll('.ss-act [data-a]').forEach(function (b) {
b.addEventListener('click', function () { doAction(b.getAttribute('data-a')); });
});
modal = overlay.querySelector('.ss-modal');
}
function netBtn(net, label, bg, glyph) {
return '<button data-net="' + net + '" aria-label="Share to ' + label + '">' +
'<span class="ss-ico" style="background:' + bg + '">' + glyph + '</span>' + label + '</button>';
}
function open(ctx) {
if (!overlay) buildModal();
cur = ctx;
overlay.querySelector('.ss-thumb').src = ctx.imageUrl;
overlay.querySelector('.ss-title').textContent = ctx.title;
overlay.querySelector('.ss-reel').setAttribute('href', '/reel');
overlay.classList.add('on');
document.documentElement.style.overflow = 'hidden';
getCaption(ctx.id); // warm the cache
}
function close() {
if (overlay) overlay.classList.remove('on');
document.documentElement.style.overflow = '';
}
function doNetwork(net) {
var c = cur; if (!c) return;
getCaption(c.id).then(function (cap) {
var u = encodeURIComponent(c.pageUrl);
var img = encodeURIComponent(c.imageUrl);
var text = encodeURIComponent(cap.caption || c.title);
var full = encodeURIComponent(cap.full || c.title);
switch (net) {
case 'fb': popup('https://www.facebook.com/sharer/sharer.php?u=' + u); break;
case 'x': popup('https://twitter.com/intent/tweet?text=' + text + '&url=' + u); break;
case 'pin': popup('https://www.pinterest.com/pin/create/button/?url=' + u + '&media=' + img + '&description=' + text); break;
case 'li': popup('https://www.linkedin.com/sharing/share-offsite/?url=' + u); break;
case 'wa': popup('https://wa.me/?text=' + full + '%20' + u); break;
case 'em':
location.href = 'mailto:?subject=' + encodeURIComponent(c.title + ' — wallco.ai') +
'&body=' + full + '%0A%0A' + u; break;
case 'ig': case 'tt': instagramTikTok(net, c, cap); break;
}
});
}
// Instagram / TikTok: native share on mobile, else download + copy caption.
function instagramTikTok(net, c, cap) {
var name = net === 'ig' ? 'Instagram' : 'TikTok';
shareImageNative(c, cap.full).then(function () {
toast('Shared — pick ' + name + ' from the menu');
}).catch(function () {
Promise.all([
downloadImage(c.imageUrl, c.title),
copyText(cap.full),
]).then(function () {
toast('Image saved + caption copied — open ' + name + ' to post');
}).catch(function () { toast('Could not prepare the post'); });
});
}
function doAction(a) {
var c = cur; if (!c) return;
if (a === 'copylink') {
copyText(c.pageUrl).then(function () { toast('Link copied'); })
.catch(function () { window.prompt('Copy this link:', c.pageUrl); });
} else if (a === 'copycap') {
getCaption(c.id).then(function (cap) {
copyText(cap.full).then(function () { toast('Caption + hashtags copied'); })
.catch(function () { window.prompt('Copy caption:', cap.full); });
});
} else if (a === 'dl') {
downloadImage(c.imageUrl, c.title).then(function () { toast('Image downloaded'); })
.catch(function () { toast('Download failed'); });
}
}
// "Post to all" — copy caption + save image (for IG/TikTok) then open every
// web-intent network. Browsers may queue the popups; that's expected.
function postAll() {
var c = cur; if (!c) return;
getCaption(c.id).then(function (cap) {
copyText(cap.full).catch(function () {});
downloadImage(c.imageUrl, c.title).catch(function () {});
var u = encodeURIComponent(c.pageUrl);
var img = encodeURIComponent(c.imageUrl);
var text = encodeURIComponent(cap.caption || c.title);
var urls = [
'https://www.facebook.com/sharer/sharer.php?u=' + u,
'https://twitter.com/intent/tweet?text=' + text + '&url=' + u,
'https://www.pinterest.com/pin/create/button/?url=' + u + '&media=' + img + '&description=' + text,
'https://www.linkedin.com/sharing/share-offsite/?url=' + u,
];
urls.forEach(function (url, i) { setTimeout(function () { popup(url); }, i * 350); });
toast('Caption copied + image saved — networks opening');
});
}
// ── context extraction ───────────────────────────────────────────────────
function ctxFromCard(card) {
var id = parseInt(card.getAttribute('data-design-id') || card.getAttribute('data-id'), 10);
if (!id) return null;
var titleEl = card.querySelector('.card-title');
var imgEl = card.querySelector('.card-img');
var bg = imgEl && imgEl.style.backgroundImage || '';
var m = bg.match(/url\(['"]?([^'")]+)['"]?\)/);
var img = m ? m[1] : (imgEl && imgEl.querySelector('img') && imgEl.querySelector('img').src) || '';
return {
id: id,
title: (titleEl && titleEl.textContent.trim()) || ('Design #' + id),
imageUrl: absUrl(img),
pageUrl: pageUrl(id),
};
}
function ctxFromDetail() {
var btn = document.getElementById('cta-share');
var m = (btn && btn.dataset.shareUrl || location.href).match(/\/design\/(\d+)/);
var id = m ? parseInt(m[1], 10) : 0;
var img = document.getElementById('detail-img');
var title = (btn && btn.dataset.shareTitle) ||
(document.querySelector('h1') && document.querySelector('h1').textContent.trim()) || 'wallco.ai design';
title = title.replace(/\s*—\s*wallco\.ai\s*$/i, '').trim();
return {
id: id,
title: title,
imageUrl: img ? absUrl(img.getAttribute('src')) : '',
pageUrl: id ? pageUrl(id) : location.href,
};
}
// ── wire design cards (grid) ─────────────────────────────────────────────
function wireCard(card) {
if (card.__ssWired) return;
card.__ssWired = true;
var ctx = ctxFromCard(card);
if (!ctx) return;
// corner FAB (always reachable, even without the hover row)
var fab = document.createElement('button');
fab.className = 'ss-fab';
fab.type = 'button';
fab.title = 'Share this design';
fab.setAttribute('aria-label', 'Share ' + ctx.title);
fab.textContent = '↗';
fab.addEventListener('click', function (e) {
e.preventDefault(); e.stopPropagation();
open(ctxFromCard(card) || ctx);
});
card.appendChild(fab);
// also drop a chip into the hover action row when present
var row = card.querySelector('.card-hover-actions');
if (row && !row.querySelector('.ss-card-btn')) {
var b = document.createElement('a');
b.className = 'chov-btn ss-card-btn';
b.href = '#';
b.textContent = 'Share';
b.addEventListener('click', function (e) {
e.preventDefault(); e.stopPropagation();
open(ctxFromCard(card) || ctx);
});
row.appendChild(b);
}
}
function wireAllCards() {
document.querySelectorAll('.design-card[data-design-id], .design-card[data-id]').forEach(wireCard);
}
// ── wire detail page ─────────────────────────────────────────────────────
function wireDetail() {
var btn = document.getElementById('cta-share');
if (btn && !btn.__ssWired) {
btn.__ssWired = true;
// capture-phase + stopImmediatePropagation overrides the basic inline handler
btn.addEventListener('click', function (e) {
e.preventDefault();
e.stopImmediatePropagation();
open(ctxFromDetail());
}, true);
}
// add a "Make a Reel" button to the trade-tools row
var tools = document.querySelector('.pro-tools');
if (tools && !tools.querySelector('.ss-reel-btn')) {
var ctx = ctxFromDetail();
var a = document.createElement('a');
a.className = 'btn-outline ss-reel-btn';
a.href = '/reel';
a.style.cssText = 'font:12px var(--sans);padding:7px 14px;text-decoration:none;display:inline-flex;align-items:center;gap:6px';
a.title = 'Build a TikTok / Instagram reel';
a.innerHTML = '<span aria-hidden="true">🎬</span> Make a Reel';
a.addEventListener('click', function () {
try {
var picks = JSON.parse(localStorage.getItem('wallco.reel.picks') || '[]');
if (picks.indexOf(ctx.id) < 0) { picks.push(ctx.id); }
localStorage.setItem('wallco.reel.picks', JSON.stringify(picks.slice(0, 6)));
} catch (e) {}
});
tools.appendChild(a);
}
}
// ── init ─────────────────────────────────────────────────────────────────
function init() {
wireAllCards();
wireDetail();
// catch lazily-appended cards (infinite scroll)
var grid = document.getElementById('grid') || document.querySelector('.designs-grid, .grid');
if (grid && window.MutationObserver) {
new MutationObserver(function () { wireAllCards(); }).observe(grid, { childList: true });
}
}
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
else init();
})();