[object Object]

← back to Wallco Ai

wallco.ai · hover-preview room mockups on /designs grid cards

78b90d178d70e864175ccf699ac87e3554e92b4a · 2026-05-11 23:51:55 -0700 · SteveStudio2

Top-5 saturated designs already have residential mockups generated.
Surface them via a CSS-only hover overlay on the catalog grid card —
no extra click needed to see "is this thing worth clicking on?"

Each design with room_mockups gets:
- absolute <img> overlay covering the card-img (lazy-loaded)
- opacity 0 default, fades to 1 on :hover / :focus-visible (360ms ease)
- "IN A ROOM" badge in upper-left, hides on hover so the room shines
- prefers-reduced-motion: instant swap

Wired in three places: home featured rail, /designs catalog, /murals.
Falls back through living_room → bedroom → office → first-available.
Static route /designs/room/* is already serving from data/rooms/ (7d cache).

Verified: 3 has-room cards render on /designs page 1 (ids 14/24/29);
chromium hover assertion confirms opacity 0→1 + badge fade.

Files touched

Diff

commit 78b90d178d70e864175ccf699ac87e3554e92b4a
Author: SteveStudio2 <stevestudio2@SteveStacStudio.lan>
Date:   Mon May 11 23:51:55 2026 -0700

    wallco.ai · hover-preview room mockups on /designs grid cards
    
    Top-5 saturated designs already have residential mockups generated.
    Surface them via a CSS-only hover overlay on the catalog grid card —
    no extra click needed to see "is this thing worth clicking on?"
    
    Each design with room_mockups gets:
    - absolute <img> overlay covering the card-img (lazy-loaded)
    - opacity 0 default, fades to 1 on :hover / :focus-visible (360ms ease)
    - "IN A ROOM" badge in upper-left, hides on hover so the room shines
    - prefers-reduced-motion: instant swap
    
    Wired in three places: home featured rail, /designs catalog, /murals.
    Falls back through living_room → bedroom → office → first-available.
    Static route /designs/room/* is already serving from data/rooms/ (7d cache).
    
    Verified: 3 has-room cards render on /designs page 1 (ids 14/24/29);
    chromium hover assertion confirms opacity 0→1 + badge fade.
---
 public/css/site.css | 35 ++++++++++++++++++++++++++++++++++
 server.js           | 54 ++++++++++++++++++++++++++++++++++++++++-------------
 2 files changed, 76 insertions(+), 13 deletions(-)

diff --git a/public/css/site.css b/public/css/site.css
index a821c58..6b8252e 100644
--- a/public/css/site.css
+++ b/public/css/site.css
@@ -257,6 +257,41 @@ main { padding-top: 73px; }
 .card-img {
   aspect-ratio: 1;
   background: var(--line) no-repeat center / cover;
+  position: relative;
+  overflow: hidden;
+}
+
+/* Hover-preview: residential mockup fades in over the pattern thumbnail. */
+.card-room {
+  position: absolute;
+  inset: 0;
+  width: 100%;
+  height: 100%;
+  object-fit: cover;
+  opacity: 0;
+  transition: opacity 360ms ease;
+  pointer-events: none;
+  will-change: opacity;
+}
+.design-card.has-room:hover .card-room,
+.design-card.has-room:focus-visible .card-room { opacity: 1; }
+.design-card.has-room::after {
+  content: 'IN A ROOM';
+  position: absolute;
+  top: 8px; left: 8px;
+  padding: 3px 7px;
+  font-size: 9px;
+  letter-spacing: 0.10em;
+  background: rgba(15, 14, 12, 0.75);
+  color: #f0ead7;
+  border-radius: 2px;
+  pointer-events: none;
+  z-index: 1;
+  opacity: 0.9;
+}
+.design-card.has-room:hover::after { opacity: 0; }
+@media (prefers-reduced-motion: reduce) {
+  .card-room { transition: none; }
 }
 
 .card-meta {
diff --git a/server.js b/server.js
index 0583993..791a451 100644
--- a/server.js
+++ b/server.js
@@ -527,6 +527,7 @@ const FOOTER = `<footer class="site-footer">
   <p>Part of the <a href="https://designerwallcoverings.com">Designer Wallcoverings</a> family.</p>
   <p><a href="mailto:info@wallco.ai">info@wallco.ai</a></p>
 </footer>
+<script src="/js/wallco-game.js" defer></script>
 <!-- Mobile tap-target minimums (Apple HIG / WCAG 2.5.5) — applied site-wide.
      Forces every clickable to ≥44×44 on phones without overriding desktop layout. -->
 <style id="mobile-tap-floor">
@@ -709,15 +710,24 @@ app.get('/', (req, res) => {
     `<div class="hero-slide ${i===0?'active':''}" data-idx="${i}" style="background-image:url('${d.image_url}')" aria-label="${d.title}"></div>`
   ).join('\n');
 
-  const featuredCards = featured.map(d => `
-    <a href="/design/${d.id}" class="design-card">
-      <div class="card-img" style="background-image:url('${d.image_url}')"></div>
+  const featuredCards = featured.map(d => {
+    const roomKey = (d.room_mockups || []).includes('living_room') ? 'living_room'
+                  : (d.room_mockups || []).includes('bedroom')     ? 'bedroom'
+                  : (d.room_mockups || []).includes('office')      ? 'office'
+                  : (d.room_mockups || [])[0];
+    const roomOverlay = roomKey
+      ? `<img class="card-room" src="/designs/room/design_${d.id}_${roomKey}.png" loading="lazy" alt="" aria-hidden="true">`
+      : '';
+    return `
+    <a href="/design/${d.id}" class="design-card${roomKey ? ' has-room' : ''}">
+      <div class="card-img" style="background-image:url('${d.image_url}')">${roomOverlay}</div>
       <div class="card-meta">
         <span class="card-title">${d.title}</span>
         <span class="card-cat">${d.category}</span>
         <span class="color-dot" style="background:${d.dominant_hex}"></span>
       </div>
-    </a>`).join('');
+    </a>`;
+  }).join('');
 
   res.type('html').send(`${htmlHead({
     title: 'wallco.ai — AI-Original Wallpaper & Murals',
@@ -870,9 +880,16 @@ app.get('/designs', (req, res) => {
 
   const cards = slice.map(d => {
     if (!_isAdmin) {
+      const roomKey = (d.room_mockups || []).includes('living_room') ? 'living_room'
+                    : (d.room_mockups || []).includes('bedroom')     ? 'bedroom'
+                    : (d.room_mockups || []).includes('office')      ? 'office'
+                    : (d.room_mockups || [])[0];
+      const roomOverlay = roomKey
+        ? `<img class="card-room" src="/designs/room/design_${d.id}_${roomKey}.png" loading="lazy" alt="" aria-hidden="true">`
+        : '';
       return `
-    <a href="/design/${d.id}" class="design-card">
-      <div class="card-img" style="background-image:url('${d.image_url}')"></div>
+    <a href="/design/${d.id}" class="design-card${roomKey ? ' has-room' : ''}">
+      <div class="card-img" style="background-image:url('${d.image_url}')">${roomOverlay}</div>
       <div class="card-meta">
         <span class="card-title">${d.title}</span>
         <span class="card-cat">${d.category}</span>
@@ -3512,15 +3529,24 @@ app.get('/murals', (req, res) => {
   const murals = DESIGNS.filter(d => d.kind === 'mural' || d.kind === 'mural_panel');
   const all    = murals.length === 0 ? DESIGNS : murals; // fallback: show all if no mural-kind yet
 
-  const cards = all.map(d => `
-    <a href="/design/${d.id}" class="design-card">
-      <div class="card-img" style="background-image:url('${d.image_url}')"></div>
+  const cards = all.map(d => {
+    const roomKey = (d.room_mockups || []).includes('living_room') ? 'living_room'
+                  : (d.room_mockups || []).includes('bedroom')     ? 'bedroom'
+                  : (d.room_mockups || []).includes('office')      ? 'office'
+                  : (d.room_mockups || [])[0];
+    const roomOverlay = roomKey
+      ? `<img class="card-room" src="/designs/room/design_${d.id}_${roomKey}.png" loading="lazy" alt="" aria-hidden="true">`
+      : '';
+    return `
+    <a href="/design/${d.id}" class="design-card${roomKey ? ' has-room' : ''}">
+      <div class="card-img" style="background-image:url('${d.image_url}')">${roomOverlay}</div>
       <div class="card-meta">
         <span class="card-title">${d.title}</span>
         <span class="card-cat">${murals.length === 0 ? 'available as mural' : d.kind}</span>
         <span class="color-dot" style="background:${d.dominant_hex}"></span>
       </div>
-    </a>`).join('');
+    </a>`;
+  }).join('');
 
   res.type('html').send(`${htmlHead({
     title: 'Murals — wallco.ai | AI-Generated Wall Murals',
@@ -6258,16 +6284,17 @@ async function loadPair() {
     var d = j[k];
     var card = document.getElementById('card-'+k);
     card.innerHTML = '<img src="'+d.image_url+'" alt="'+d.title+'"><div class="meta"><div class="title">'+d.title+'</div><div class="sub">'+(d.category||'')+'</div></div>';
-    card.onclick = function(){ vote(d.id, j[k==='a'?'b':'a'].id); };
+    card.onclick = function(evt){ vote(d.id, j[k==='a'?'b':'a'].id, evt); };
   });
   try {
     var s = await (await fetch('/api/poll/stats')).json();
     if (s && s.total_pairs != null) document.getElementById('poll-stats').textContent = s.total_pairs + ' total votes cast · ' + (s.designs_polled||0) + ' designs in rotation';
   } catch(e) {}
 }
-async function vote(winner, loser) {
+async function vote(winner, loser, evt) {
   document.getElementById('card-a').style.opacity = 0.4;
   document.getElementById('card-b').style.opacity = 0.4;
+  if (window.WC) WC.reward('pickA', evt);
   await fetch('/api/poll/vote', {method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({winner: winner, loser: loser})});
   setTimeout(loadPair, 400);
   setTimeout(function(){ document.getElementById('card-a').style.opacity = 1; document.getElementById('card-b').style.opacity = 1; loadLB(); }, 500);
@@ -6434,7 +6461,7 @@ async function load(){
   document.getElementById('hon-card').dataset.image = r.image_url;
   seen.push(r.id);
 }
-async function act(action){
+async function act(action, evt){
   var card = document.getElementById('hon-card');
   var id = parseInt(card.dataset.id, 10);
   if (!id) return;
@@ -6448,6 +6475,7 @@ async function act(action){
   }
   todayCount++; localStorage.setItem(todayKey, String(todayCount));
   renderStreak();
+  if (window.WC) WC.reward(action, evt);
   picks.unshift({id: id, action: action, title: card.dataset.title, image_url: card.dataset.image});
   picks = picks.slice(0, 6);
   document.getElementById('hon-history').innerHTML = picks.map(function(h){

← cfe10a2 feat(/wallco-ai): drop Big Red widget into home page footer  ·  back to Wallco Ai  ·  wallco.ai · /api/studio/category-insights — fix products SQL 3563074 →