← back to Designer Line
server.js
88 lines
// DesignerLine — trade designer's everything-app
// Combines: WallcoverDM (commerce) + DesignerDemands (advocacy) + PulseForDesigners (community)
// Target: 60k DW trade-verified designers. Single auth, single notification system, single billing.
const express = require('express');
const path = require('path');
const { Pool } = require('pg');
const app = express();
const PORT = process.env.PORT || 9780;
// dw_unified for trade-verified designer auth (shared with DW main).
// Connection is optional at boot — only commerce/advocacy/community APIs require it.
const pool = process.env.DATABASE_URL
? new Pool({ connectionString: process.env.DATABASE_URL })
: new Pool({ host: '/tmp', database: 'dw_unified', user: process.env.USER || 'stevestudio2' });
app.use(express.json());
// Static-root 404 guard — never serve snapshot/backup artifacts from public/
// even if one accidentally lands on disk (.bak / .bak.<ts> / .pre-* / .orig).
app.use((req, res, next) => {
if (/\.(bak|orig)(\.|$)|\.pre-/i.test(req.path)) return res.status(404).end();
next();
});
app.use(express.static(path.join(__dirname, 'public')));
// ---- shared health + identity ---------------------------------------------
app.get('/api/health', (req, res) => res.json({ ok: true, app: 'designerline', port: PORT, ts: new Date().toISOString() }));
// ---- COMMERCE — sample requests + trade pricing ---------------------------
// Pulls real Scalamandre wallcoverings as the seed catalog (1,117 active in dw_unified).
app.get('/api/commerce/products', async (req, res) => {
const { vendor = '', q = '', limit = 60, sort = 'newest' } = req.query;
const lim = Math.min(parseInt(limit) || 60, 200);
try {
const params = [];
let where = "WHERE status='ACTIVE' AND product_type ILIKE '%wallcover%'";
if (vendor) { params.push(`%${vendor}%`); where += ` AND vendor ILIKE $${params.length}`; }
if (q) { params.push(`%${q}%`); where += ` AND (title ILIKE $${params.length} OR sku ILIKE $${params.length})`; }
const orderBy = {
'newest': 'updated_at_shopify DESC NULLS LAST',
'title': 'title ASC',
'title-desc': 'title DESC',
'vendor': 'vendor ASC, title ASC',
'sku': 'sku ASC NULLS LAST',
'sku-desc': 'sku DESC NULLS LAST',
'price-asc': 'COALESCE(net_price, retail_price) ASC NULLS LAST',
'price-desc': 'COALESCE(net_price, retail_price) DESC NULLS LAST',
}[sort] || 'updated_at_shopify DESC NULLS LAST';
params.push(lim);
const { rows } = await pool.query(
`SELECT handle, title, vendor, product_type, sku, image_url, retail_price, net_price
FROM shopify_products ${where}
ORDER BY ${orderBy}
LIMIT $${params.length}`,
params
);
res.json({ count: rows.length, sort, vendor, q, products: rows });
} catch (e) {
res.status(500).json({ error: e.message });
}
});
// ---- ADVOCACY — petitions on IP + trade pricing transparency --------------
const STARTER_PETITIONS = [
{ id: 'p_ip_protection', title: 'Strengthen wallcovering pattern IP protection', desc: 'Petition Congress + WIPO to recognize wallcovering patterns as protectable design works under Hague Agreement.', supports: 0 },
{ id: 'p_trade_price_transparency', title: 'Mandatory trade-pricing disclosure from manufacturers', desc: 'Vendors must publish trade discount tiers (% off MSRP) so independent designers can compete with chains.', supports: 0 },
{ id: 'p_lead_time_honesty', title: 'Real lead-time disclosure law', desc: 'Manufacturer-published lead times must reflect 90-day rolling actuals, not best-case scenarios.', supports: 0 },
];
app.get('/api/advocacy/petitions', (req, res) => res.json({ petitions: STARTER_PETITIONS }));
// ---- COMMUNITY — designer feed (news + town halls) ------------------------
const STARTER_FEED = [
{ id: 'f1', kind: 'news', title: 'Schumacher acquires textile rights to Maison Lévy archive', ts: '2026-05-11T14:00:00Z' },
{ id: 'f2', kind: 'townhall', title: 'May Town Hall — vendor MAP enforcement after the Q1 lawsuits', ts: '2026-05-13T18:00:00Z' },
{ id: 'f3', kind: 'news', title: 'WIPO releases draft on wallcovering pattern protection', ts: '2026-05-10T09:30:00Z' },
];
app.get('/api/community/feed', (req, res) => res.json({ items: STARTER_FEED }));
// ---- ROOT -----------------------------------------------------------------
app.get('/', (req, res) => res.sendFile(path.join(__dirname, 'public', 'index.html')));
app.listen(PORT, '127.0.0.1', () => {
console.log(`[designerline] listening at http://127.0.0.1:${PORT}/`);
});