← back to Dw Activation Calendar
Calendar chips are real hrefs to the staged Shopify product (open/cmd-click/right-click); plain click keeps detail modal
09e4fb6fbd9482cefd0b5e2301456c28cd3af1e9 · 2026-07-15 11:05:13 -0700 · Steve
Files touched
M package.jsonM public/index.htmlM server.js
Diff
commit 09e4fb6fbd9482cefd0b5e2301456c28cd3af1e9
Author: Steve <steve@designerwallcoverings.com>
Date: Wed Jul 15 11:05:13 2026 -0700
Calendar chips are real hrefs to the staged Shopify product (open/cmd-click/right-click); plain click keeps detail modal
---
package.json | 2 +-
public/index.html | 39 +++++++++++++++++++++++++++------------
server.js | 4 ++--
3 files changed, 30 insertions(+), 15 deletions(-)
diff --git a/package.json b/package.json
index 975933e..560028d 100644
--- a/package.json
+++ b/package.json
@@ -1 +1 @@
-{"name":"dw-activation-calendar","version":"1.0.0","private":true,"scripts":{"start":"node server.js"}}
+{"name":"dw-activation-calendar","version":"1.0.1","private":true,"scripts":{"start":"node server.js"}}
\ No newline at end of file
diff --git a/public/index.html b/public/index.html
index 68828e0..2b8d0b6 100644
--- a/public/index.html
+++ b/public/index.html
@@ -127,7 +127,7 @@
.scell .chips{display:flex;flex-wrap:wrap;gap:5px;align-content:flex-start;
max-height:178px;overflow:hidden} /* collapsed preview: ~3 chip rows */
.cell.expanded .chips{max-height:none;overflow:visible} /* expanded: the whole day column */
- .chip{position:relative;border-radius:6px;overflow:hidden;background:var(--ground);border:1px solid var(--hairline);width:56px;height:56px;flex:0 0 auto;padding:0;cursor:pointer}
+ .chip{position:relative;display:block;text-decoration:none;color:inherit;border-radius:6px;overflow:hidden;background:var(--ground);border:1px solid var(--hairline);width:56px;height:56px;flex:0 0 auto;padding:0;cursor:pointer}
.chip:hover{border-color:var(--brass);box-shadow:0 0 0 1px var(--brass)}
.chip:focus-visible{outline:2px solid var(--plum);outline-offset:1px}
/* expand / collapse the day column */
@@ -541,12 +541,14 @@ function openSkuModal(s){
const adminUrl = idNum ? 'https://admin.shopify.com/store/designer-laboratory-sandbox/products/'+idNum : null;
const siteUrl = s.handle ? 'https://www.designerwallcoverings.com/products/'+encodeURIComponent(s.handle) : null;
const fx = s.fix_type==='add-sample' ? 's' : 'r';
+ const label = s.title || skuLabel(s);
+ const abbr = s.vendor.slice(0,3).toUpperCase();
const hero = (s.image_url && /^https?:/.test(s.image_url))
- ? `<img src="${esc(s.image_url)}" alt="${esc(s.title||skuLabel(s))}">`
- : `<div class="ph" style="background:${vcolor(s.vendor)}">${esc(s.vendor.slice(0,3).toUpperCase())}</div>`;
+ ? `<img src="${esc(s.image_url)}" alt="${esc(label)}">`
+ : `<div class="ph" style="background:${vcolor(s.vendor)}">${esc(abbr)}</div>`;
const bodyHtml=`<div class="mform skud">
<div class="mhead">
- <div class="mtitle">${esc(s.title || skuLabel(s))}</div>
+ <div class="mtitle">${esc(label)}</div>
<button type="button" class="mclose" id="sk_x" aria-label="Close">✕</button>
</div>
<div class="hero">${hero}</div>
@@ -592,19 +594,32 @@ function renderSchedMonth(containerId, monthDate, data){
if(day){
const chips=document.createElement('div'); chips.className='chips';
day.skus.forEach(s=>{
- // div, not <button>: a button grid-item's aspect-ratio height doesn't
- // feed the auto row track, so rows collapse and chips overlap.
- const el=document.createElement('div'); el.className='chip';
- el.setAttribute('role','button'); el.tabIndex=0;
+ // Every data point is a REAL anchor to its actual staged product (Shopify
+ // admin — staged items 404 on the storefront until active). So a chip is
+ // openable every way: hover shows the URL, cmd/ctrl/middle/right-click opens
+ // the product in a new tab. A plain left-click still opens the rich detail
+ // modal (which also carries the admin + storefront links).
+ const idNum=s.shopify_id?String(s.shopify_id).split('/').pop():null;
+ const prodUrl=idNum?'https://admin.shopify.com/store/designer-laboratory-sandbox/products/'+idNum:null;
+ const el=document.createElement(prodUrl?'a':'div'); el.className='chip';
+ if(prodUrl){ el.href=prodUrl; el.target='_blank'; el.rel='noopener noreferrer'; }
+ else { el.setAttribute('role','button'); }
+ el.tabIndex=0;
el.onkeydown=(e)=>{if(e.key==='Enter'||e.key===' '){e.preventDefault();openSkuModal(s);}};
- el.title=`${skuLabel(s)} · ${s.vendor}${s.title?' · '+s.title:''} · ${s.fix_type}\n⏱ goes active ${fmtGoLive(s.go_live_at)}`;
+ el.title=`${skuLabel(s)} · ${s.vendor}${s.title?' · '+s.title:''} · ${s.fix_type}\n⏱ goes active ${fmtGoLive(s.go_live_at)}${prodUrl?'\n↗ opens the staged product in Shopify admin':''}`;
const fx=s.fix_type==='add-sample'?'s':'r';
+ const abbr=s.vendor.slice(0,3).toUpperCase();
if(s.image_url && /^https?:/.test(s.image_url)){
- el.innerHTML=`<span class="fx ${fx}"></span><img loading="lazy" src="${s.image_url}" onerror="this.replaceWith(Object.assign(document.createElement('div'),{className:'ph',style:'background:${vcolor(s.vendor)}',textContent:'${s.vendor.slice(0,3).toUpperCase()}'}))"><span class="vn">${s.vendor}</span>`;
+ el.innerHTML=`<span class="fx ${fx}"></span><img loading="lazy" src="${s.image_url}" onerror="this.replaceWith(Object.assign(document.createElement('div'),{className:'ph',style:'background:${vcolor(s.vendor)}',textContent:'${abbr}'}))"><span class="vn">${s.vendor}</span>`;
}else{
- el.innerHTML=`<span class="fx ${fx}"></span><div class="ph" style="background:${vcolor(s.vendor)}">${s.vendor.slice(0,3).toUpperCase()}</div><span class="vn">${s.vendor}</span>`;
+ el.innerHTML=`<span class="fx ${fx}"></span><div class="ph" style="background:${vcolor(s.vendor)}">${abbr}</div><span class="vn">${s.vendor}</span>`;
}
- el.onclick=(e)=>{e.stopPropagation();openSkuModal(s);};
+ el.onclick=(e)=>{
+ // modified / non-primary clicks follow the real href → straight to the product;
+ // a plain left-click opens the detail modal (same admin link inside).
+ if(e.metaKey||e.ctrlKey||e.shiftKey||e.altKey||e.button!==0) return;
+ e.preventDefault(); e.stopPropagation(); openSkuModal(s);
+ };
chips.appendChild(el);
});
cell.appendChild(chips);
diff --git a/server.js b/server.js
index dfca6bc..d624b28 100644
--- a/server.js
+++ b/server.js
@@ -16,6 +16,7 @@
const express = require('express');
const path = require('path');
const { Pool } = require('pg');
+const { execFile } = require('child_process');
const gcal = require('./google-cal');
const app = express();
@@ -70,7 +71,7 @@ async function imagesFor(shopifyIds) {
// file is absent so the caller can fall back to the legacy on-the-fly compute.
async function scheduleFromFile() {
const f = loadScheduleFile();
- if (!f || !Array.isArray(f.skus) || !f.skus.length) return null;
+ if (!f || !Array.isArray(f.skus) || !f.skus.length || !f.meta) return null;
const imgs = await imagesFor([...new Set(f.skus.map(s => s.shopify_id).filter(Boolean))]);
const byDate = new Map(); const vendorTotals = {};
for (const s of f.skus) {
@@ -190,7 +191,6 @@ app.get('/api/health', (_q, res) => res.json({ ok: true, port: PORT }));
// Re-project the schedule against the CURRENT worklist (re-stamp every item's
// date+time). Cheap, local, read-only against dw_unified; writes only the JSON.
app.post('/api/schedule/regenerate', (_q, res) => {
- const { execFile } = require('child_process');
execFile('node', [path.join(__dirname, 'scripts', 'generate-schedule.js')],
{ cwd: __dirname, timeout: 120000 }, (err, stdout, stderr) => {
if (err) return res.status(500).json({ ok: false, error: (stderr || err.message).slice(0, 500) });
← 53bb7a5 calendar day columns expand to show all SKUs; every chip cli
·
back to Dw Activation Calendar
·
Activation calendar: drag SKU chips between days to reschedu ca49220 →