[object Object]

← back to Wallco Ai

wallco.ai · /design/:id Share button — Web Share API + clipboard fallback

680a09e2a9033833bcf42ab29908aa1805f0c598 · 2026-05-12 06:53:30 -0700 · SteveStudio2

Drops a "↗ Share" pill into the existing .cta-group row, sibling to
"Request Free Sample" + "Back to Designs". On click:

1. Tries navigator.share() — mobile/iOS/Android native share sheet pops
   with title + body text + canonical URL. Best UX, lets the user pick
   Messages/Mail/AirDrop/IG/etc.
2. Falls through to navigator.clipboard.writeText() if Web Share is
   unavailable or the user cancelled. Button label flashes "Link copied"
   for 1.8s then reverts.
3. Last-resort window.prompt() if clipboard API is blocked (older browsers,
   non-https). Always succeeds at giving the user the URL.

Pairs with the OG/Twitter cards from tick 27 — when the shared URL hydrates
in iMessage/Slack/etc., recipients see the og:image:product preview card.

Reversible: 1 button + 30 lines JS + 15 lines CSS. No new deps.

Files touched

Diff

commit 680a09e2a9033833bcf42ab29908aa1805f0c598
Author: SteveStudio2 <stevestudio2@SteveStacStudio.lan>
Date:   Tue May 12 06:53:30 2026 -0700

    wallco.ai · /design/:id Share button — Web Share API + clipboard fallback
    
    Drops a "↗ Share" pill into the existing .cta-group row, sibling to
    "Request Free Sample" + "Back to Designs". On click:
    
    1. Tries navigator.share() — mobile/iOS/Android native share sheet pops
       with title + body text + canonical URL. Best UX, lets the user pick
       Messages/Mail/AirDrop/IG/etc.
    2. Falls through to navigator.clipboard.writeText() if Web Share is
       unavailable or the user cancelled. Button label flashes "Link copied"
       for 1.8s then reverts.
    3. Last-resort window.prompt() if clipboard API is blocked (older browsers,
       non-https). Always succeeds at giving the user the URL.
    
    Pairs with the OG/Twitter cards from tick 27 — when the shared URL hydrates
    in iMessage/Slack/etc., recipients see the og:image:product preview card.
    
    Reversible: 1 button + 30 lines JS + 15 lines CSS. No new deps.
---
 public/css/site.css | 17 +++++++++++++++++
 server.js           | 40 ++++++++++++++++++++++++++++++++++++++++
 2 files changed, 57 insertions(+)

diff --git a/public/css/site.css b/public/css/site.css
index 85a9787..5dd857d 100644
--- a/public/css/site.css
+++ b/public/css/site.css
@@ -557,6 +557,23 @@ main { padding-top: 73px; }
 .spec-val { font-size: 13px; color: var(--ink); }
 .cta-group { display: flex; gap: 12px; flex-wrap: wrap; margin-bottom: 24px; }
 .cta-sample {}
+.cta-share {
+  display: inline-flex;
+  align-items: center;
+  gap: 7px;
+  font-family: var(--sans);
+  cursor: pointer;
+  transition: background-color 160ms ease, color 160ms ease;
+}
+.cta-share .cta-share-icon {
+  font-size: 14px;
+  line-height: 1;
+  transform: translateY(-1px);
+}
+.cta-share:focus-visible {
+  outline: 2px solid var(--ink);
+  outline-offset: 2px;
+}
 .provenance-note {
   font-size: 13px;
   color: var(--ink-faint);
diff --git a/server.js b/server.js
index ab48b0f..fbd7609 100644
--- a/server.js
+++ b/server.js
@@ -3066,8 +3066,48 @@ ${htmlHeader('/designs')}
       <div class="cta-group">
         <a id="cta-sample-link" href="/samples?design=${design.id}" class="btn-primary cta-sample">Request Free Sample</a>
         <a href="/designs" class="btn-outline">Back to Designs</a>
+        <button id="cta-share" type="button" class="btn-outline cta-share"
+                data-share-title="${design.title} — wallco.ai"
+                data-share-text="Found this in the wallco.ai catalog — ${design.title}"
+                data-share-url="https://wallco.ai/design/${design.id}"
+                aria-label="Share this design">
+          <span class="cta-share-icon" aria-hidden="true">↗</span>
+          <span class="cta-share-label">Share</span>
+        </button>
       </div>
       <script>
+      (function(){
+        var btn = document.getElementById('cta-share');
+        if (!btn) return;
+        var labelEl = btn.querySelector('.cta-share-label');
+        var origLabel = labelEl.textContent;
+        var resetTimer = null;
+        function flash(msg){
+          if (resetTimer) clearTimeout(resetTimer);
+          labelEl.textContent = msg;
+          resetTimer = setTimeout(function(){ labelEl.textContent = origLabel; }, 1800);
+        }
+        btn.addEventListener('click', async function(){
+          var url = btn.dataset.shareUrl;
+          var title = btn.dataset.shareTitle;
+          var text = btn.dataset.shareText;
+          // Prefer native Web Share API when available (mobile + supporting desktop browsers).
+          if (navigator.share) {
+            try { await navigator.share({ title: title, text: text, url: url }); return; }
+            catch (e) { /* user cancelled or unsupported — fall through to clipboard */ }
+          }
+          // Clipboard fallback.
+          try {
+            await navigator.clipboard.writeText(url);
+            flash('Link copied');
+          } catch (e) {
+            // Last resort: prompt() so the user can copy manually.
+            try { window.prompt('Copy this link:', url); } catch (_) {}
+          }
+        });
+      })();
+      </script>
+      <script>
       (function(){
         function syncCtaWidth(){
           var saved = JSON.parse(localStorage.getItem('wallco.material') || 'null');

← f4354f9 spoonflower upload — fix hydration race + add --batch CSV mo  ·  back to Wallco Ai  ·  spoonflower — add scripts/spoonflower_kept_to_csv.py to make 8aa0c7c →