← back to Stars of Design

lib/clients.js

150 lines

'use strict';
// Real DW client designers — sourced from George info@designerwallcoverings.com
// Gmail recon + Steve's personal Gmail recon, materialized via sig-extract into
// sod_designers + sod_firms + sod_links.
//
// This is the second tier of the directory — the 150-entry editorial JSON at
// data/designers.json is the curated marquee (Dorothy Draper, Kelly Wearstler,
// Studio Sofield, etc.); /clients surfaces the actual designers who buy from
// Designer Wallcoverings. They get the same profile-page treatment but their
// data comes from sig-extract + future claim flow.
//
// PII policy: city + state only; no street address, no zip, no phone. Phone
// becomes available only via the claim flow. See
// ~/.claude/projects/-Users-stevestudio2/memory/feedback_designer_directory_pii_policy.md.

const { Pool } = require('pg');

const pool = new Pool({
  connectionString: process.env.DATABASE_URL || 'postgresql:///dw_unified?host=/tmp&user=stevestudio2',
  max: 4,
});

// Per Steve's standing rule: every product/listing grid needs sort + density.
const SORT_KEYS = {
  default:  `(d.premium_tier IS NOT NULL) DESC, d.full_name`,
  name_az:  `d.full_name`,
  name_za:  `d.full_name DESC`,
  city:     `COALESCE(f.city, d.city) NULLS LAST, d.full_name`,
  firm:     `f.name NULLS LAST, d.full_name`,
  claimed:  `(d.claimed_at IS NOT NULL) DESC, d.full_name`,
};

async function listClients({ limit = 200, q = null, area = null, sort = 'default' } = {}) {
  const params = [];
  let where = `(d.role IS NULL OR d.role <> 'Manufacturer Rep')`;
  if (q) {
    params.push(`%${q.toLowerCase()}%`);
    where += ` AND (LOWER(d.full_name) LIKE $${params.length} OR LOWER(f.name) LIKE $${params.length} OR LOWER(d.city) LIKE $${params.length})`;
  }
  if (area) {
    params.push(area);
    where += ` AND ($${params.length} = ANY(d.areas_served) OR $${params.length} = ANY(f.areas_served))`;
  }
  params.push(limit);
  const sql = `
    SELECT d.id, d.slug, d.full_name, d.role,
           d.city                   AS designer_city,
           d.state_or_region        AS designer_state,
           d.country                AS designer_country,
           d.areas_served           AS designer_areas,
           d.headshot_source, d.headshot_url,
           d.premium_tier, d.claimed_at,
           f.id      AS firm_id,
           f.slug    AS firm_slug,
           f.name    AS firm_name,
           f.city            AS firm_city,
           f.state_or_region AS firm_state,
           f.country         AS firm_country,
           f.areas_served    AS firm_areas,
           f.website         AS firm_website,
           f.size_band, f.founded_year,
           (
             SELECT json_agg(json_build_object('url', l.url, 'kind', l.kind))
               FROM sod_links l
              WHERE l.designer_id = d.id OR l.firm_id = f.id
           ) AS links
      FROM sod_designers d
      LEFT JOIN sod_designer_firm df ON df.designer_id = d.id AND df.is_current = true
      LEFT JOIN sod_firms f          ON f.id = df.firm_id
     WHERE ${where}
     ORDER BY ${SORT_KEYS[sort] || SORT_KEYS.default}
     LIMIT $${params.length}`;
  const r = await pool.query(sql, params);
  return r.rows.map(row => {
    // Roll up city/state — prefer firm's, fall back to designer's.
    const city  = row.firm_city  || row.designer_city  || null;
    const state = row.firm_state || row.designer_state || null;
    const country = row.firm_country || row.designer_country || null;
    const areas = [...new Set([...(row.designer_areas || []), ...(row.firm_areas || [])])];
    // Group links by kind.
    const linksByKind = {};
    for (const l of (row.links || [])) {
      (linksByKind[l.kind] = linksByKind[l.kind] || []).push(l.url);
    }
    return {
      id: row.id,
      slug: row.slug,
      full_name: row.full_name,
      role: row.role,
      city, state, country, areas,
      firm: row.firm_name ? {
        id: row.firm_id, slug: row.firm_slug, name: row.firm_name,
        website: row.firm_website, founded_year: row.founded_year, size_band: row.size_band,
      } : null,
      premium_tier: row.premium_tier,
      claimed: !!row.claimed_at,
      links: linksByKind,
      headshot: (row.headshot_source === 'made-with-ai' || !row.headshot_url) ? '/img/made-with-ai.svg' : row.headshot_url,
    };
  });
}

async function getClient(slug) {
  const rows = await listClients({ limit: 1, q: null });
  // Refetch with slug match (cheap on this scale; can be tightened later)
  const r = await pool.query(`
    SELECT d.*, f.id AS firm_id, f.slug AS firm_slug, f.name AS firm_name,
           f.website AS firm_website, f.founded_year, f.size_band,
           f.city AS firm_city, f.state_or_region AS firm_state, f.country AS firm_country,
           f.areas_served AS firm_areas
      FROM sod_designers d
      LEFT JOIN sod_designer_firm df ON df.designer_id = d.id AND df.is_current = true
      LEFT JOIN sod_firms f          ON f.id = df.firm_id
     WHERE d.slug = $1
     LIMIT 1`, [slug]);
  if (!r.rows[0]) return null;
  const d = r.rows[0];
  const lr = await pool.query(
    `SELECT url, kind FROM sod_links WHERE designer_id = $1 OR firm_id = $2`,
    [d.id, d.firm_id]
  );
  const linksByKind = {};
  for (const l of lr.rows) (linksByKind[l.kind] = linksByKind[l.kind] || []).push(l.url);
  return {
    id: d.id, slug: d.slug, full_name: d.full_name, role: d.role,
    city: d.firm_city || d.city || null,
    state: d.firm_state || d.state_or_region || null,
    country: d.firm_country || d.country || null,
    areas: [...new Set([...(d.areas_served || []), ...(d.firm_areas || [])])],
    bio: d.bio,
    premium_tier: d.premium_tier,
    claimed: !!d.claimed_at,
    firm: d.firm_name ? {
      id: d.firm_id, slug: d.firm_slug, name: d.firm_name,
      website: d.firm_website, founded_year: d.founded_year, size_band: d.size_band,
    } : null,
    headshot: (d.headshot_source === 'made-with-ai' || !d.headshot_url) ? '/img/made-with-ai.svg' : d.headshot_url,
    links: linksByKind,
  };
}

async function totalCount() {
  const r = await pool.query(
    `SELECT count(*) AS n FROM sod_designers WHERE role IS NULL OR role <> 'Manufacturer Rep'`
  );
  return parseInt(r.rows[0].n, 10);
}

module.exports = { listClients, getClient, totalCount, SORT_KEYS };