← 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 &amp; 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();
})();