← back to Dw Marketing Viewer

server.js

198 lines

#!/usr/bin/env node
// DW Marketing — Wallpaper's Back social command viewer
// Style matches dw-staged-active-viewer (:9846). Zero deps (raw http).
const http = require('http');
const fs = require('fs');
const path = require('path');
const PORT = process.env.PORT || 9847;
const AVATAR_DIR = path.join(__dirname, 'avatars');
const DEFAULT_AVATAR = '/avatars/avatar-wpb-dark.png';

const WPB = 'https://wallpapersback.com';
const IMG = '/designs/img/';

const ACCOUNTS = [
  {
    platform: 'TikTok', icon: '🎵', handle: 'wallpapersback',
    url: 'https://www.tiktok.com/@wallpapersback',
    status: 'live', statusNote: 'Existing TikTok Shop account — logged in',
    pwLast4: '911*', envKey: 'WPB_TIKTOK_*',
    bioLimit: 80,
    bio: "🌿 Original wallpaper, reborn daily — fresh AI patterns for your walls. Shop ↓",
  },
  {
    platform: 'Instagram', icon: '📷', handle: 'wallpaperisback',
    url: 'https://www.instagram.com/wallpaperisback',
    status: 'live', statusNote: 'Created 2026-06-23 · verified',
    pwLast4: '911*', envKey: 'WPB_INSTAGRAM_*',
    bioLimit: 150,
    bio: "Wallpaper's Back 🌿✨\nOriginal AI-crafted wallpaper patterns — new designs daily.\nPeel · paste · transform your space.\n🛕 Shop the collection ↓",
  },
  {
    platform: 'X / Twitter', icon: '✕', handle: 'WallpaperBack',
    url: 'https://x.com/WallpaperBack',
    status: 'live', statusNote: 'Created on phone 2026-06-23 (web automation blocked)',
    pwLast4: '911*', envKey: 'WPB_X_*',
    bioLimit: 160,
    bio: "Original wallpaper, reborn. 🌿 AI-crafted patterns + murals, new daily. Transform any wall → wallpapersback.com",
  },
];

const SHARED = { email: 'info@wallpapersback.com', phone: '310-713-0489', dob: 'Jan 3, 1971', linkInBio: 'https://wallpapersback.com' };

const CAROUSEL = {
  images: [
    '1781036121748_1084855833.png','1781036020017_47709849.png','1781035921400_2016604802.png',
    '1781035821844_1409088740.png','1781035653657_1404626502.png','1781035651286_493073813.png',
    '1781034900507_1678143317.png','1780549976580_1965836513.png','1780549072364_1488626861.png',
    '1780510863396_1320598381.png',
  ],
  caption: "Wallpaper's back. 🌿 Ten fresh patterns, hot off the press — which one's going on your wall?\n#wallpaper #wallpapersback #homedecor #interiordesign #maximalist #accentwall #wallpaperdesign #patterndesign #interiorinspo #homedesign",
  status: 'ready', statusNote: 'Prepped — post via TikTok app ➕ → Photo',
};

function esc(s){return String(s).replace(/[&<>"]/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;'}[c]));}

function page(){
  const accCards = ACCOUNTS.map(a=>`
    <div class="card acct">
      <div class="ahd">
        <span class="plat"><img class="av" src="${DEFAULT_AVATAR}" alt="">${a.icon} ${esc(a.platform)}</span>
        <span class="pill ${a.status}">${a.status}</span>
      </div>
      <a class="handle" href="${a.url}" target="_blank" rel="noopener noreferrer">@${esc(a.handle)}</a>
      <div class="anote">${esc(a.statusNote)}</div>
      <div class="biobox">
        <div class="biolbl">BIO <span class="cnt${a.bio.length>a.bioLimit?' over':''}">${a.bio.length}/${a.bioLimit}</span></div>
        <div class="bio">${esc(a.bio).replace(/\n/g,'<br>')}</div>
      </div>
      <div class="meta">
        <span class="kv">🔑 …${esc(a.pwLast4)}</span>
        <span class="kv mono">${esc(a.envKey)}</span>
      </div>
    </div>`).join('');

  const thumbs = CAROUSEL.images.map((f,i)=>`
    <a class="ct" href="${WPB}${IMG}${f}" target="_blank" rel="noopener noreferrer"
       style="background-image:url('${WPB}${IMG}${f}')"><span class="cn">${i+1}</span></a>`).join('');

  return `<!doctype html><html lang="en"><head>
<meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1">
<title>DW Marketing — Wallpaper's Back</title>
<link rel="icon" type="image/png" href="/avatars/favicon.png">
<style>
  :root { --bg:#0f1115; --card:#191c23; --ink:#e7e9ee; --mut:#8b91a0; --line:#2a2e38; --accent:#6ea8fe; }
  *{box-sizing:border-box;}
  body{margin:0;font:14px/1.45 -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif;background:var(--bg);color:var(--ink);}
  header{position:sticky;top:0;z-index:10;background:#12151c;border-bottom:1px solid var(--line);padding:12px 18px;}
  h1{font-size:16px;margin:0 0 6px;font-weight:650;}
  .totals{color:var(--mut);font-size:13px;}
  .totals b{color:var(--ink);}
  .tabs{display:flex;gap:6px;margin-top:10px;}
  .tab{cursor:pointer;border:1px solid var(--line);background:#1b1f28;color:var(--mut);padding:6px 16px;border-radius:8px;font-size:13px;font-weight:600;}
  .tab.active{border-color:var(--accent);background:#22304a;color:#fff;}
  main{padding:16px 18px 60px;}
  section{display:none;} section.active{display:block;}
  .grid{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:12px;}
  @media(max-width:820px){.grid{grid-template-columns:1fr;}}
  .card{background:var(--card);border:1px solid var(--line);border-radius:10px;padding:12px 13px;display:flex;flex-direction:column;gap:8px;}
  .ahd{display:flex;justify-content:space-between;align-items:center;}
  .plat{font-weight:650;font-size:14px;display:flex;align-items:center;gap:8px;}
  .av{width:30px;height:30px;border-radius:50%;object-fit:cover;border:1px solid var(--line);}
  .avopts{display:flex;gap:16px;align-items:center;margin:14px 0 2px;}
  .avopt{display:flex;flex-direction:column;align-items:center;gap:5px;}
  .avopt img{width:84px;height:84px;border-radius:50%;object-fit:cover;border:2px solid var(--line);}
  .avopt.pick img{border-color:var(--accent);}
  .avopt span{font-size:11px;color:var(--mut);}
  .avlbl{color:var(--mut);font-size:12px;font-weight:600;}
  .pill{font-size:10px;border-radius:5px;padding:1px 7px;text-transform:uppercase;font-weight:700;letter-spacing:.04em;}
  .pill.live{color:#9be59b;border:1px solid #1f5c17;background:#122a12;}
  .pill.ready{color:#ffd479;border:1px solid #5c4a17;background:#241d0a;}
  .handle{color:var(--accent);text-decoration:none;font-size:15px;font-weight:600;}
  .handle:hover{text-decoration:underline;}
  .anote{color:var(--mut);font-size:11.5px;}
  .biobox{background:#12151c;border:1px solid var(--line);border-radius:8px;padding:8px 9px;}
  .biolbl{color:var(--mut);font-size:10px;font-weight:700;letter-spacing:.05em;margin-bottom:4px;display:flex;justify-content:space-between;}
  .biolbl .cnt{color:#9be59b;} .biolbl .cnt.over{color:#ff8a8a;}
  .bio{font-size:12.5px;line-height:1.5;color:#dfe3ec;}
  .meta{display:flex;gap:10px;align-items:center;}
  .kv{font-size:11px;color:var(--mut);} .mono{font-family:ui-monospace,Menlo,monospace;color:#aab3c5;}
  .sharebar{display:flex;flex-wrap:wrap;gap:14px;margin:14px 0 4px;color:var(--mut);font-size:12.5px;}
  .sharebar b{color:var(--ink);} .sharebar a{color:var(--accent);text-decoration:none;}
  .cgrid{display:grid;grid-template-columns:repeat(auto-fill,minmax(120px,1fr));gap:8px;margin:12px 0;}
  .ct{aspect-ratio:1/1;background:#0c0e13 center/cover no-repeat;border:1px solid var(--line);border-radius:8px;position:relative;display:block;}
  .ct .cn{position:absolute;top:4px;left:4px;background:rgba(0,0,0,.6);color:#fff;font-size:11px;font-weight:700;border-radius:5px;padding:0 6px;}
  .capbox{background:var(--card);border:1px solid var(--line);border-radius:10px;padding:12px;margin-top:8px;}
  .capbox h3{margin:0 0 6px;font-size:13px;color:var(--mut);font-weight:600;}
  .cap{white-space:pre-wrap;font-size:13px;line-height:1.55;color:#dfe3ec;}
  .steps{margin:10px 0 0;padding-left:18px;color:#cdd6e8;font-size:12.5px;line-height:1.7;}
  .projnote{color:#ffd479;font-size:11.5px;background:#241d0a;border:1px solid #5c4a17;border-radius:6px;padding:5px 9px;margin-bottom:12px;display:inline-block;}
</style></head><body>
<header>
  <h1>🌿 DW Marketing — Wallpaper's Back</h1>
  <div class="totals"><b>${ACCOUNTS.length}</b> social accounts · <b>1</b> carousel queued · brand <b>wallpapersback.com</b></div>
  <div class="tabs">
    <div class="tab active" data-t="accounts">Accounts</div>
    <div class="tab" data-t="carousel">Carousel</div>
  </div>
</header>
<main>
  <section id="accounts" class="active">
    <div class="grid">${accCards}</div>
    <div class="sharebar">
      <span>🔗 Link-in-bio: <a href="${SHARED.linkInBio}" target="_blank" rel="noopener noreferrer">${SHARED.linkInBio}</a></span>
      <span>✉️ <b>${SHARED.email}</b></span>
      <span>📱 <b>${SHARED.phone}</b></span>
      <span>🎂 ${SHARED.dob}</span>
    </div>
    <div class="avopts">
      <span class="avlbl">Brand mark (Optima):</span>
      <div class="avopt pick"><img src="/avatars/avatar-wpb-dark.png" alt=""><span>dark ✓</span></div>
      <div class="avopt"><img src="/avatars/avatar-wpb-light.png" alt=""><span>light</span></div>
      <div class="avopt"><img src="/avatars/avatar-wpb-green.png" alt=""><span>green</span></div>
    </div>
    <div class="projnote">Passwords stored in secrets registry (WPB_* keys) — shown as last-4 only.</div>
  </section>
  <section id="carousel">
    <div class="projnote">${esc(CAROUSEL.statusNote)}</div>
    <div class="cgrid">${thumbs}</div>
    <div class="capbox">
      <h3>CAPTION</h3>
      <div class="cap">${esc(CAROUSEL.caption)}</div>
    </div>
    <div class="capbox">
      <h3>POST STEPS (TikTok app)</h3>
      <ol class="steps">
        <li>Save the 10 images above to your phone (tap → opens full-res)</li>
        <li>TikTok app → ➕ → switch mode to <b>Photo</b></li>
        <li>Select all 10 in order → Next</li>
        <li>Add sound (trending/on-brand) → Next</li>
        <li>Paste caption → <b>Post</b></li>
      </ol>
    </div>
  </section>
</main>
<script>
  document.querySelectorAll('.tab').forEach(t=>t.onclick=()=>{
    document.querySelectorAll('.tab').forEach(x=>x.classList.remove('active'));
    document.querySelectorAll('section').forEach(x=>x.classList.remove('active'));
    t.classList.add('active'); document.getElementById(t.dataset.t).classList.add('active');
  });
</script>
</body></html>`;
}

http.createServer((req,res)=>{
  if(req.url==='/healthz'){res.writeHead(200,{'Content-Type':'text/plain'});return res.end('ok');}
  if(req.url.startsWith('/avatars/')){
    const f=path.join(AVATAR_DIR, path.basename(req.url));
    if(f.startsWith(AVATAR_DIR) && fs.existsSync(f)){
      res.writeHead(200,{'Content-Type':'image/png','Cache-Control':'no-cache'});
      return res.end(fs.readFileSync(f));
    }
    res.writeHead(404); return res.end('not found');
  }
  res.writeHead(200,{'Content-Type':'text/html; charset=utf-8'});
  res.end(page());
}).listen(PORT,'127.0.0.1',()=>console.log('DW Marketing viewer → http://127.0.0.1:'+PORT+'/'));