← back to NationalPaperHangers

scripts/lead-list.js

84 lines

#!/usr/bin/env node
// Lead list — studios running paid ads, ranked by platform count.
// Output: pretty terminal table, or CSV with --csv.
//
// Usage:
//   node scripts/lead-list.js               # all paid-ads studios, pretty print
//   node scripts/lead-list.js --csv         # CSV to stdout
//   node scripts/lead-list.js --min=2       # only studios on 2+ paid platforms
//   node scripts/lead-list.js --state=CA    # filter by state

require('dotenv').config();
const db = require('../lib/db');

const CSV = process.argv.includes('--csv');
const MIN = parseInt((process.argv.find(a => a.startsWith('--min=')) || '--min=1').split('=')[1], 10);
const STATE = (process.argv.find(a => a.startsWith('--state=')) || '').split('=')[1] || null;

async function main() {
  const params = [MIN];
  let where = `claim_status='unclaimed' AND ad_signals IS NOT NULL AND (ad_signals->>'paid_ads_count')::int >= $1`;
  if (STATE) {
    params.push(STATE.toUpperCase());
    where += ` AND state = $${params.length}`;
  }
  const rows = await db.many(
    `SELECT business_name, city, state, website, instagram_handle, ad_signals
       FROM installers
      WHERE ${where}
      ORDER BY (ad_signals->>'paid_ads_count')::int DESC, business_name`,
    params
  );

  if (CSV) {
    console.log('business_name,city,state,website,instagram,paid_count,platforms');
    for (const r of rows) {
      const platforms = paidPlatforms(r.ad_signals).join('|');
      console.log([
        csv(r.business_name), csv(r.city), csv(r.state), csv(r.website),
        csv(r.instagram_handle ? '@' + r.instagram_handle : ''),
        r.ad_signals.paid_ads_count, csv(platforms)
      ].join(','));
    }
  } else {
    console.log(`\n=== Lead list — ${rows.length} unclaimed studios running paid ads ===\n`);
    for (const r of rows) {
      const platforms = paidPlatforms(r.ad_signals);
      const tag = '💰'.repeat(Math.min(r.ad_signals.paid_ads_count, 5));
      console.log(`${tag} ${r.business_name}`);
      console.log(`   ${r.city || '?'}, ${r.state || '?'} · ${r.website}`);
      if (r.instagram_handle) console.log(`   IG @${r.instagram_handle}`);
      console.log(`   Platforms: ${platforms.join(', ')}`);
      console.log('');
    }
    if (rows.length === 0) console.log('  (none yet — run scripts/scan-ad-signals.js --commit first)');
  }

  await db.pool.end();
}

function paidPlatforms(s) {
  if (!s) return [];
  const map = {
    google_ads: 'Google Ads',
    meta_pixel: 'Meta',
    tiktok_pixel: 'TikTok',
    pinterest_tag: 'Pinterest',
    linkedin_insight: 'LinkedIn',
    twitter_pixel: 'Twitter/X',
    reddit_pixel: 'Reddit',
    bing_uet: 'Bing',
    snap_pixel: 'Snap'
  };
  return Object.entries(map).filter(([k]) => s[k] === true).map(([, v]) => v);
}

function csv(v) {
  if (v == null) return '';
  const s = String(v);
  if (/[",\n]/.test(s)) return '"' + s.replace(/"/g, '""') + '"';
  return s;
}

main().catch(e => { console.error(e); process.exit(1); });