← back to Commercialdesignreps
initial scaffold + GA gtag
80b81392cefb0ff0bcf8ef2d6efdc79ab78b0e31 · 2026-05-10 02:40:14 -0700 · Steve Abrams
Files touched
A .gitignoreA README.mdA data/reps.jsonA package-lock.jsonA package.jsonA public/css/styles.cssA public/favicon.svgA public/js/main.jsA public/robots.txtA server.jsA views/404.ejsA views/about.ejsA views/index.ejsA views/partials/foot.ejsA views/partials/head.ejsA views/rep.ejs
Diff
commit 80b81392cefb0ff0bcf8ef2d6efdc79ab78b0e31
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Sun May 10 02:40:14 2026 -0700
initial scaffold + GA gtag
---
.gitignore | 9 +
README.md | 29 +
data/reps.json | 42 ++
package-lock.json | 1418 +++++++++++++++++++++++++++++++++++++++++++++++
package.json | 24 +
public/css/styles.css | 196 +++++++
public/favicon.svg | 11 +
public/js/main.js | 45 ++
public/robots.txt | 3 +
server.js | 111 ++++
views/404.ejs | 10 +
views/about.ejs | 25 +
views/index.ejs | 100 ++++
views/partials/foot.ejs | 29 +
views/partials/head.ejs | 47 ++
views/rep.ejs | 69 +++
16 files changed, 2168 insertions(+)
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..4a0b3ea
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,9 @@
+node_modules/
+.env
+.env.*
+tmp/
+*.log
+.DS_Store
+dist/
+build/
+.next/
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..b9b0a18
--- /dev/null
+++ b/README.md
@@ -0,0 +1,29 @@
+# commercialdesignreps.com
+
+Commercial design sales-rep directory. Independent reps and showroom agencies covering U.S. commercial, hospitality, healthcare, and contract interior design lines.
+
+## Stack
+
+- Express + EJS (slim, no DB at MVP)
+- Seed data in `data/reps.json` (40 reps)
+- pm2 port 9922 on Kamatera
+- GA4 `G-PRJ51PTW5G`
+
+## Run locally
+
+```bash
+npm ci
+npm start # → http://localhost:9922
+```
+
+## Deploy
+
+Rsync to `root@45.61.58.125:/root/Projects/commercialdesignreps/`, then:
+
+```
+npm ci
+pm2 start server.js --name commercialdesignreps
+pm2 save
+```
+
+nginx vhost at `/etc/nginx/sites-enabled/commercialdesignreps.com` proxies port 9922.
diff --git a/data/reps.json b/data/reps.json
new file mode 100644
index 0000000..fa371f8
--- /dev/null
+++ b/data/reps.json
@@ -0,0 +1,42 @@
+[
+ {"id":1,"name":"Patricia Langham","agency":"Langham Associates","territories":["CA","NV","AZ"],"lines":["Innovations USA","Phillip Jeffries","Elitis"],"city":"Los Angeles","vertical":["Residential","Contract"],"contact_url":"https://langhamassociates.com/contact"},
+ {"id":2,"name":"Marcus Rivera","agency":"Rivera Design Group","territories":["CA","OR","WA"],"lines":["Schumacher","Stark Carpet","Clarence House"],"city":"San Francisco","vertical":["Residential","Hospitality"],"contact_url":"https://riveradesigngroup.com/contact"},
+ {"id":3,"name":"Diane Forsythe","agency":"KDR Designer Showrooms","territories":["IL","WI","MN"],"lines":["Romo","Osborne & Little","Zoffany"],"city":"Chicago","vertical":["Residential","Commercial"],"contact_url":"https://kdrshowrooms.com/contact"},
+ {"id":4,"name":"Thomas Beaumont","agency":"Beaumont Rep Services","territories":["NY","NJ","CT"],"lines":["Pollack","S. Harris","Pindler"],"city":"New York","vertical":["Contract","Healthcare"],"contact_url":"https://beaumontrepservices.com/contact"},
+ {"id":5,"name":"Sandra Okoro","agency":"Pacific Rep Alliance","territories":["CA","HI"],"lines":["Lee Jofa","Groundworks","GP & J Baker"],"city":"Los Angeles","vertical":["Residential","Hospitality"],"contact_url":"https://pacificrepalliance.com/contact"},
+ {"id":6,"name":"James Whitfield","agency":"Southeast Design Alliance","territories":["GA","FL","SC","NC"],"lines":["Kravet","Fabricut","Stout"],"city":"Atlanta","vertical":["Residential","Commercial"],"contact_url":"https://southeastdesignalliance.com/contact"},
+ {"id":7,"name":"Rachel Kim","agency":"Kim & Associates","territories":["TX","OK","NM"],"lines":["Cowtan & Tout","Brunswick & Fils","Colefax & Fowler"],"city":"Dallas","vertical":["Residential"],"contact_url":"https://kimassociatesrep.com/contact"},
+ {"id":8,"name":"David Marchetti","agency":"Marchetti Contract Group","territories":["NY","PA","MD","DC"],"lines":["Designtex","Maharam","Arc-Com"],"city":"New York","vertical":["Contract","Healthcare","Education"],"contact_url":"https://marchetticontract.com/contact"},
+ {"id":9,"name":"Laura Chen","agency":"West Coast Trade Reps","territories":["CA","WA","OR","ID"],"lines":["Holly Hunt","Donghia","Edelman Leather"],"city":"Los Angeles","vertical":["Residential","Hospitality"],"contact_url":"https://westcoasttradereps.com/contact"},
+ {"id":10,"name":"Gregory Hoffman","agency":"Hoffman Design Source","territories":["CO","UT","WY","MT"],"lines":["Brunschwig & Fils","Manuel Canovas","Pierre Frey"],"city":"Denver","vertical":["Residential"],"contact_url":"https://hoffmandesignsource.com/contact"},
+ {"id":11,"name":"Monica Sullivan","agency":"Hospitality Rep Group","territories":["NV","AZ","CA"],"lines":["Carnegie","CF Stinson","Momentum"],"city":"Las Vegas","vertical":["Hospitality","F&B","Resort"],"contact_url":"https://hospitalityrepgroup.com/contact"},
+ {"id":12,"name":"William Park","agency":"Park & Partners","territories":["WA","OR","ID","MT"],"lines":["Innovations USA","Bolta","Bolon"],"city":"Seattle","vertical":["Contract","Commercial"],"contact_url":"https://parkandpartners.com/contact"},
+ {"id":13,"name":"Elizabeth Torres","agency":"Torres Textile Reps","territories":["TX","LA","MS","AR"],"lines":["Robert Allen","Duralee","P/Kaufmann"],"city":"Houston","vertical":["Residential","Contract"],"contact_url":"https://torrestextilesreps.com/contact"},
+ {"id":14,"name":"Christopher Vance","agency":"Vance Commercial Interiors","territories":["IL","IN","OH","MI"],"lines":["Interface","Milliken","Shaw Contract"],"city":"Chicago","vertical":["Commercial","Healthcare","Education"],"contact_url":"https://vancecommercialinteriors.com/contact"},
+ {"id":15,"name":"Ashley Greenwood","agency":"Greenwood Luxury Reps","territories":["FL","GA","AL"],"lines":["Elitis","Casamance","Texam"],"city":"Miami","vertical":["Residential","Hospitality","Resort"],"contact_url":"https://greenwoodluxuryreps.com/contact"},
+ {"id":16,"name":"Nathan Brody","agency":"Brody & Associates","territories":["MA","RI","VT","NH","ME"],"lines":["Jim Thompson","Osborne & Little","Etro Home"],"city":"Boston","vertical":["Residential","Hospitality"],"contact_url":"https://brodyassociatesrep.com/contact"},
+ {"id":17,"name":"Samantha Wu","agency":"Wu Design Reps","territories":["CA","NV"],"lines":["Arte International","Elitis","Nlxl"],"city":"Los Angeles","vertical":["Residential","Boutique"],"contact_url":"https://wudesignreps.com/contact"},
+ {"id":18,"name":"Robert Ashton","agency":"Ashton Contract Solutions","territories":["NY","NJ","PA","CT","MA"],"lines":["Knoll Textiles","Herman Miller","Kvadrat"],"city":"New York","vertical":["Contract","Office","Education"],"contact_url":"https://ashtoncontractsolutions.com/contact"},
+ {"id":19,"name":"Jennifer Caldwell","agency":"Caldwell Showroom Alliance","territories":["CA","AZ","NM"],"lines":["Schumacher","Groundworks","Stark Carpet"],"city":"Phoenix","vertical":["Residential","Commercial"],"contact_url":"https://caldwellshowroomalliance.com/contact"},
+ {"id":20,"name":"Michael Stern","agency":"Stern Rep Network","territories":["NY","FL","TX"],"lines":["Dedar","Loro Piana Interiors","Zimmer + Rohde"],"city":"New York","vertical":["Residential","Luxury"],"contact_url":"https://sternrepnetwork.com/contact"},
+ {"id":21,"name":"Caroline Baptiste","agency":"Baptiste Hotel Reps","territories":["FL","TN","NC","SC"],"lines":["Maharam","Momentum","CF Stinson"],"city":"Miami","vertical":["Hospitality","Hotel Brand","F&B"],"contact_url":"https://baptistehotelreps.com/contact"},
+ {"id":22,"name":"Steven Nakamura","agency":"Nakamura Pacific Group","territories":["CA","OR","WA","HI","AK"],"lines":["Innovations USA","Phillip Jeffries","Knoll Textiles"],"city":"Los Angeles","vertical":["Contract","Commercial"],"contact_url":"https://nakamurapacificgroup.com/contact"},
+ {"id":23,"name":"Diana Leblanc","agency":"Leblanc Design Associates","territories":["LA","MS","AL","TN"],"lines":["Duralee","Fabricut","Trend"],"city":"New Orleans","vertical":["Residential","Hospitality"],"contact_url":"https://leblancdesignassociates.com/contact"},
+ {"id":24,"name":"Richard Flores","agency":"Flores Healthcare Interiors","territories":["TX","NM","OK"],"lines":["Designtex","Arc-Com","Bolta"],"city":"Dallas","vertical":["Healthcare","Commercial"],"contact_url":"https://floreshealthcareinteriors.com/contact"},
+ {"id":25,"name":"Allison Hayes","agency":"Hayes Boutique Rep","territories":["NY","CT","NJ"],"lines":["Dedar","Etro Home","Loro Piana Interiors"],"city":"New York","vertical":["Residential","Boutique","Luxury"],"contact_url":"https://hayesboutiquerep.com/contact"},
+ {"id":26,"name":"Kevin Marsh","agency":"Marsh Education Group","territories":["CA","OR","WA","NV"],"lines":["Interface","Milliken","Patcraft"],"city":"San Francisco","vertical":["Education","Healthcare","Commercial"],"contact_url":"https://marsheducationgroup.com/contact"},
+ {"id":27,"name":"Tara Coleman","agency":"Coleman Resort Alliance","territories":["HI","CA","FL"],"lines":["Elitis","Casamance","Perennials"],"city":"Los Angeles","vertical":["Resort","Hospitality","F&B"],"contact_url":"https://colemanresortalliance.com/contact"},
+ {"id":28,"name":"Peter Donovan","agency":"Donovan Office Solutions","territories":["IL","MN","WI","IA"],"lines":["Knoll Textiles","Herman Miller","Kvadrat"],"city":"Chicago","vertical":["Office","Contract"],"contact_url":"https://donovanofficesolutions.com/contact"},
+ {"id":29,"name":"Angela Ross","agency":"Ross Residential Reps","territories":["GA","FL","SC"],"lines":["Kravet","Lee Jofa","GP & J Baker"],"city":"Atlanta","vertical":["Residential"],"contact_url":"https://rossresidentialreps.com/contact"},
+ {"id":30,"name":"Frank Deluca","agency":"DeLuca Contract Group","territories":["CA","NV","AZ"],"lines":["Carnegie","Designtex","Boltaflex"],"city":"Los Angeles","vertical":["Healthcare","Commercial","Office"],"contact_url":"https://delucacontractgroup.com/contact"},
+ {"id":31,"name":"Nicole Huang","agency":"Huang Luxury Interiors Rep","territories":["NY","CA"],"lines":["Zimmer + Rohde","Dedar","Sahco"],"city":"New York","vertical":["Residential","Luxury","Boutique"],"contact_url":"https://huangluxuryinteriors.com/contact"},
+ {"id":32,"name":"Andrew Blake","agency":"Blake Hospitality Reps","territories":["TN","KY","VA","WV"],"lines":["Maharam","Momentum","Bolta"],"city":"Nashville","vertical":["Hospitality","Hotel Brand","F&B"],"contact_url":"https://blakehospitalityreps.com/contact"},
+ {"id":33,"name":"Stephanie Osei","agency":"Osei Commercial Alliance","territories":["GA","AL","TN","SC"],"lines":["Interface","Shaw Contract","Patcraft"],"city":"Atlanta","vertical":["Commercial","Office","Retail"],"contact_url":"https://oseicommercialalliance.com/contact"},
+ {"id":34,"name":"Timothy Grant","agency":"Grant & Grant Design Reps","territories":["PA","DE","MD","VA"],"lines":["Schumacher","Brunschwig & Fils","Clarence House"],"city":"Philadelphia","vertical":["Residential","Contract"],"contact_url":"https://grantgrantdesignreps.com/contact"},
+ {"id":35,"name":"Lisa Johansson","agency":"Nordic West Reps","territories":["WA","OR","ID","MT","WY"],"lines":["Bolon","Baux","Kvadrat"],"city":"Seattle","vertical":["Commercial","Office","Education"],"contact_url":"https://nordicwestreps.com/contact"},
+ {"id":36,"name":"Omar Farouk","agency":"Farouk Luxury Reps","territories":["CA","TX","NY"],"lines":["Loro Piana Interiors","Dedar","Sahco"],"city":"Los Angeles","vertical":["Luxury","Residential","Boutique"],"contact_url":"https://faroukluxuryreps.com/contact"},
+ {"id":37,"name":"Catherine Moore","agency":"Moore Wallcovering Reps","territories":["IL","MO","KS","NE"],"lines":["Innovations USA","Arte International","Nlxl"],"city":"Chicago","vertical":["Residential","Contract"],"contact_url":"https://moorewallcoveringreps.com/contact"},
+ {"id":38,"name":"Henry Vasquez","agency":"Vasquez Southwest Reps","territories":["AZ","NM","TX","CO"],"lines":["Pollack","Pindler","Duralee"],"city":"Phoenix","vertical":["Residential","Commercial"],"contact_url":"https://vasquezSouthwestreps.com/contact"},
+ {"id":39,"name":"Pamela Nkosi","agency":"Nkosi Retail Design Reps","territories":["NY","NJ","CT","MA"],"lines":["Maharam","Momentum","Interface"],"city":"New York","vertical":["Retail","Commercial","Office"],"contact_url":"https://nkosiretaildesignreps.com/contact"},
+ {"id":40,"name":"Jonathan Reyes","agency":"Reyes Pacific Hospitality","territories":["CA","HI","NV"],"lines":["Perennials","Elitis","Casamance"],"city":"Los Angeles","vertical":["Hospitality","Resort","Boutique"],"contact_url":"https://reyespacifichospitality.com/contact"}
+]
diff --git a/package-lock.json b/package-lock.json
new file mode 100644
index 0000000..e9f6314
--- /dev/null
+++ b/package-lock.json
@@ -0,0 +1,1418 @@
+{
+ "name": "commercialdesignreps",
+ "version": "0.1.0",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "commercialdesignreps",
+ "version": "0.1.0",
+ "dependencies": {
+ "compression": "^1.7.4",
+ "ejs": "^3.1.10",
+ "express": "^4.21.0",
+ "helmet": "^8.1.0",
+ "morgan": "^1.10.0"
+ },
+ "devDependencies": {
+ "nodemon": "^3.1.7"
+ },
+ "engines": {
+ "node": ">=20"
+ }
+ },
+ "node_modules/accepts": {
+ "version": "1.3.8",
+ "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
+ "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==",
+ "license": "MIT",
+ "dependencies": {
+ "mime-types": "~2.1.34",
+ "negotiator": "0.6.3"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/accepts/node_modules/negotiator": {
+ "version": "0.6.3",
+ "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
+ "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/anymatch": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz",
+ "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "normalize-path": "^3.0.0",
+ "picomatch": "^2.0.4"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/array-flatten": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
+ "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==",
+ "license": "MIT"
+ },
+ "node_modules/async": {
+ "version": "3.2.6",
+ "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz",
+ "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==",
+ "license": "MIT"
+ },
+ "node_modules/balanced-match": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
+ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
+ "license": "MIT"
+ },
+ "node_modules/basic-auth": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.1.tgz",
+ "integrity": "sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==",
+ "license": "MIT",
+ "dependencies": {
+ "safe-buffer": "5.1.2"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/basic-auth/node_modules/safe-buffer": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
+ "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
+ "license": "MIT"
+ },
+ "node_modules/binary-extensions": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
+ "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/body-parser": {
+ "version": "1.20.5",
+ "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.5.tgz",
+ "integrity": "sha512-3grm+/2tUOvu2cjJkvsIxrv/wVpfXQW4PsQHYm7yk4vfpu7Ekl6nEsYBoJUL6qDwZUx8wUhQ8tR2qz+ad9c9OA==",
+ "license": "MIT",
+ "dependencies": {
+ "bytes": "~3.1.2",
+ "content-type": "~1.0.5",
+ "debug": "2.6.9",
+ "depd": "2.0.0",
+ "destroy": "~1.2.0",
+ "http-errors": "~2.0.1",
+ "iconv-lite": "~0.4.24",
+ "on-finished": "~2.4.1",
+ "qs": "~6.15.1",
+ "raw-body": "~2.5.3",
+ "type-is": "~1.6.18",
+ "unpipe": "~1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.8",
+ "npm": "1.2.8000 || >= 1.4.16"
+ }
+ },
+ "node_modules/body-parser/node_modules/qs": {
+ "version": "6.15.1",
+ "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.1.tgz",
+ "integrity": "sha512-6YHEFRL9mfgcAvql/XhwTvf5jKcOiiupt2FiJxHkiX1z4j7WL8J/jRHYLluORvc1XxB5rV20KoeK00gVJamspg==",
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "side-channel": "^1.1.0"
+ },
+ "engines": {
+ "node": ">=0.6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/brace-expansion": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.0.tgz",
+ "integrity": "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==",
+ "license": "MIT",
+ "dependencies": {
+ "balanced-match": "^1.0.0"
+ }
+ },
+ "node_modules/braces": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
+ "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "fill-range": "^7.1.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/bytes": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
+ "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/call-bind-apply-helpers": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
+ "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "function-bind": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/call-bound": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz",
+ "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.2",
+ "get-intrinsic": "^1.3.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/chokidar": {
+ "version": "3.6.0",
+ "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
+ "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "anymatch": "~3.1.2",
+ "braces": "~3.0.2",
+ "glob-parent": "~5.1.2",
+ "is-binary-path": "~2.1.0",
+ "is-glob": "~4.0.1",
+ "normalize-path": "~3.0.0",
+ "readdirp": "~3.6.0"
+ },
+ "engines": {
+ "node": ">= 8.10.0"
+ },
+ "funding": {
+ "url": "https://paulmillr.com/funding/"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.2"
+ }
+ },
+ "node_modules/compressible": {
+ "version": "2.0.18",
+ "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz",
+ "integrity": "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==",
+ "license": "MIT",
+ "dependencies": {
+ "mime-db": ">= 1.43.0 < 2"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/compression": {
+ "version": "1.8.1",
+ "resolved": "https://registry.npmjs.org/compression/-/compression-1.8.1.tgz",
+ "integrity": "sha512-9mAqGPHLakhCLeNyxPkK4xVo746zQ/czLH1Ky+vkitMnWfWZps8r0qXuwhwizagCRttsL4lfG4pIOvaWLpAP0w==",
+ "license": "MIT",
+ "dependencies": {
+ "bytes": "3.1.2",
+ "compressible": "~2.0.18",
+ "debug": "2.6.9",
+ "negotiator": "~0.6.4",
+ "on-headers": "~1.1.0",
+ "safe-buffer": "5.2.1",
+ "vary": "~1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/content-disposition": {
+ "version": "0.5.4",
+ "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz",
+ "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==",
+ "license": "MIT",
+ "dependencies": {
+ "safe-buffer": "5.2.1"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/content-type": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz",
+ "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/cookie": {
+ "version": "0.7.2",
+ "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz",
+ "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/cookie-signature": {
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz",
+ "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==",
+ "license": "MIT"
+ },
+ "node_modules/debug": {
+ "version": "2.6.9",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
+ "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+ "license": "MIT",
+ "dependencies": {
+ "ms": "2.0.0"
+ }
+ },
+ "node_modules/depd": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
+ "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/destroy": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz",
+ "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8",
+ "npm": "1.2.8000 || >= 1.4.16"
+ }
+ },
+ "node_modules/dunder-proto": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
+ "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.1",
+ "es-errors": "^1.3.0",
+ "gopd": "^1.2.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/ee-first": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
+ "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==",
+ "license": "MIT"
+ },
+ "node_modules/ejs": {
+ "version": "3.1.10",
+ "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz",
+ "integrity": "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "jake": "^10.8.5"
+ },
+ "bin": {
+ "ejs": "bin/cli.js"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/encodeurl": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
+ "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/es-define-property": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
+ "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-errors": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
+ "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-object-atoms": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
+ "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/escape-html": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
+ "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==",
+ "license": "MIT"
+ },
+ "node_modules/etag": {
+ "version": "1.8.1",
+ "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
+ "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/express": {
+ "version": "4.22.1",
+ "resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz",
+ "integrity": "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==",
+ "license": "MIT",
+ "dependencies": {
+ "accepts": "~1.3.8",
+ "array-flatten": "1.1.1",
+ "body-parser": "~1.20.3",
+ "content-disposition": "~0.5.4",
+ "content-type": "~1.0.4",
+ "cookie": "~0.7.1",
+ "cookie-signature": "~1.0.6",
+ "debug": "2.6.9",
+ "depd": "2.0.0",
+ "encodeurl": "~2.0.0",
+ "escape-html": "~1.0.3",
+ "etag": "~1.8.1",
+ "finalhandler": "~1.3.1",
+ "fresh": "~0.5.2",
+ "http-errors": "~2.0.0",
+ "merge-descriptors": "1.0.3",
+ "methods": "~1.1.2",
+ "on-finished": "~2.4.1",
+ "parseurl": "~1.3.3",
+ "path-to-regexp": "~0.1.12",
+ "proxy-addr": "~2.0.7",
+ "qs": "~6.14.0",
+ "range-parser": "~1.2.1",
+ "safe-buffer": "5.2.1",
+ "send": "~0.19.0",
+ "serve-static": "~1.16.2",
+ "setprototypeof": "1.2.0",
+ "statuses": "~2.0.1",
+ "type-is": "~1.6.18",
+ "utils-merge": "1.0.1",
+ "vary": "~1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.10.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/filelist": {
+ "version": "1.0.6",
+ "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.6.tgz",
+ "integrity": "sha512-5giy2PkLYY1cP39p17Ech+2xlpTRL9HLspOfEgm0L6CwBXBTgsK5ou0JtzYuepxkaQ/tvhCFIJ5uXo0OrM2DxA==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "minimatch": "^5.0.1"
+ }
+ },
+ "node_modules/fill-range": {
+ "version": "7.1.1",
+ "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
+ "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "to-regex-range": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/finalhandler": {
+ "version": "1.3.2",
+ "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz",
+ "integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==",
+ "license": "MIT",
+ "dependencies": {
+ "debug": "2.6.9",
+ "encodeurl": "~2.0.0",
+ "escape-html": "~1.0.3",
+ "on-finished": "~2.4.1",
+ "parseurl": "~1.3.3",
+ "statuses": "~2.0.2",
+ "unpipe": "~1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/forwarded": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
+ "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/fresh": {
+ "version": "0.5.2",
+ "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
+ "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/fsevents": {
+ "version": "2.3.3",
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
+ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+ }
+ },
+ "node_modules/function-bind": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
+ "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/get-intrinsic": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
+ "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.2",
+ "es-define-property": "^1.0.1",
+ "es-errors": "^1.3.0",
+ "es-object-atoms": "^1.1.1",
+ "function-bind": "^1.1.2",
+ "get-proto": "^1.0.1",
+ "gopd": "^1.2.0",
+ "has-symbols": "^1.1.0",
+ "hasown": "^2.0.2",
+ "math-intrinsics": "^1.1.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/get-proto": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
+ "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
+ "license": "MIT",
+ "dependencies": {
+ "dunder-proto": "^1.0.1",
+ "es-object-atoms": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/glob-parent": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
+ "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "is-glob": "^4.0.1"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/gopd": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
+ "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/has-flag": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
+ "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/has-symbols": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
+ "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/hasown": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.3.tgz",
+ "integrity": "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==",
+ "license": "MIT",
+ "dependencies": {
+ "function-bind": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/helmet": {
+ "version": "8.1.0",
+ "resolved": "https://registry.npmjs.org/helmet/-/helmet-8.1.0.tgz",
+ "integrity": "sha512-jOiHyAZsmnr8LqoPGmCjYAaiuWwjAPLgY8ZX2XrmHawt99/u1y6RgrZMTeoPfpUbV96HOalYgz1qzkRbw54Pmg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/http-errors": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz",
+ "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==",
+ "license": "MIT",
+ "dependencies": {
+ "depd": "~2.0.0",
+ "inherits": "~2.0.4",
+ "setprototypeof": "~1.2.0",
+ "statuses": "~2.0.2",
+ "toidentifier": "~1.0.1"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/iconv-lite": {
+ "version": "0.4.24",
+ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
+ "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
+ "license": "MIT",
+ "dependencies": {
+ "safer-buffer": ">= 2.1.2 < 3"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/ignore-by-default": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz",
+ "integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/inherits": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
+ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
+ "license": "ISC"
+ },
+ "node_modules/ipaddr.js": {
+ "version": "1.9.1",
+ "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
+ "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
+ "node_modules/is-binary-path": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
+ "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "binary-extensions": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/is-extglob": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
+ "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-glob": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
+ "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-extglob": "^2.1.1"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-number": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
+ "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.12.0"
+ }
+ },
+ "node_modules/jake": {
+ "version": "10.9.4",
+ "resolved": "https://registry.npmjs.org/jake/-/jake-10.9.4.tgz",
+ "integrity": "sha512-wpHYzhxiVQL+IV05BLE2Xn34zW1S223hvjtqk0+gsPrwd/8JNLXJgZZM/iPFsYc1xyphF+6M6EvdE5E9MBGkDA==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "async": "^3.2.6",
+ "filelist": "^1.0.4",
+ "picocolors": "^1.1.1"
+ },
+ "bin": {
+ "jake": "bin/cli.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/math-intrinsics": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
+ "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/media-typer": {
+ "version": "0.3.0",
+ "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
+ "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/merge-descriptors": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz",
+ "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/methods": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
+ "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/mime": {
+ "version": "1.6.0",
+ "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
+ "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==",
+ "license": "MIT",
+ "bin": {
+ "mime": "cli.js"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/mime-db": {
+ "version": "1.54.0",
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz",
+ "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/mime-types": {
+ "version": "2.1.35",
+ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
+ "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
+ "license": "MIT",
+ "dependencies": {
+ "mime-db": "1.52.0"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/mime-types/node_modules/mime-db": {
+ "version": "1.52.0",
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
+ "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/minimatch": {
+ "version": "5.1.9",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.9.tgz",
+ "integrity": "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==",
+ "license": "ISC",
+ "dependencies": {
+ "brace-expansion": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/morgan": {
+ "version": "1.10.1",
+ "resolved": "https://registry.npmjs.org/morgan/-/morgan-1.10.1.tgz",
+ "integrity": "sha512-223dMRJtI/l25dJKWpgij2cMtywuG/WiUKXdvwfbhGKBhy1puASqXwFzmWZ7+K73vUPoR7SS2Qz2cI/g9MKw0A==",
+ "license": "MIT",
+ "dependencies": {
+ "basic-auth": "~2.0.1",
+ "debug": "2.6.9",
+ "depd": "~2.0.0",
+ "on-finished": "~2.3.0",
+ "on-headers": "~1.1.0"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/morgan/node_modules/on-finished": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz",
+ "integrity": "sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww==",
+ "license": "MIT",
+ "dependencies": {
+ "ee-first": "1.1.1"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/ms": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
+ "license": "MIT"
+ },
+ "node_modules/negotiator": {
+ "version": "0.6.4",
+ "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.4.tgz",
+ "integrity": "sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/nodemon": {
+ "version": "3.1.14",
+ "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.14.tgz",
+ "integrity": "sha512-jakjZi93UtB3jHMWsXL68FXSAosbLfY0In5gtKq3niLSkrWznrVBzXFNOEMJUfc9+Ke7SHWoAZsiMkNP3vq6Jw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "chokidar": "^3.5.2",
+ "debug": "^4",
+ "ignore-by-default": "^1.0.1",
+ "minimatch": "^10.2.1",
+ "pstree.remy": "^1.1.8",
+ "semver": "^7.5.3",
+ "simple-update-notifier": "^2.0.0",
+ "supports-color": "^5.5.0",
+ "touch": "^3.1.0",
+ "undefsafe": "^2.0.5"
+ },
+ "bin": {
+ "nodemon": "bin/nodemon.js"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/nodemon"
+ }
+ },
+ "node_modules/nodemon/node_modules/balanced-match": {
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz",
+ "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "18 || 20 || >=22"
+ }
+ },
+ "node_modules/nodemon/node_modules/brace-expansion": {
+ "version": "5.0.6",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz",
+ "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "balanced-match": "^4.0.2"
+ },
+ "engines": {
+ "node": "18 || 20 || >=22"
+ }
+ },
+ "node_modules/nodemon/node_modules/debug": {
+ "version": "4.4.3",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
+ "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ms": "^2.1.3"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/nodemon/node_modules/minimatch": {
+ "version": "10.2.5",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz",
+ "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==",
+ "dev": true,
+ "license": "BlueOak-1.0.0",
+ "dependencies": {
+ "brace-expansion": "^5.0.5"
+ },
+ "engines": {
+ "node": "18 || 20 || >=22"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/nodemon/node_modules/ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/normalize-path": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
+ "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/object-inspect": {
+ "version": "1.13.4",
+ "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
+ "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/on-finished": {
+ "version": "2.4.1",
+ "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
+ "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
+ "license": "MIT",
+ "dependencies": {
+ "ee-first": "1.1.1"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/on-headers": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.1.0.tgz",
+ "integrity": "sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/parseurl": {
+ "version": "1.3.3",
+ "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
+ "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/path-to-regexp": {
+ "version": "0.1.13",
+ "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.13.tgz",
+ "integrity": "sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA==",
+ "license": "MIT"
+ },
+ "node_modules/picocolors": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
+ "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
+ "license": "ISC"
+ },
+ "node_modules/picomatch": {
+ "version": "2.3.2",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz",
+ "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8.6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
+ "node_modules/proxy-addr": {
+ "version": "2.0.7",
+ "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
+ "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==",
+ "license": "MIT",
+ "dependencies": {
+ "forwarded": "0.2.0",
+ "ipaddr.js": "1.9.1"
+ },
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
+ "node_modules/pstree.remy": {
+ "version": "1.1.8",
+ "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz",
+ "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/qs": {
+ "version": "6.14.2",
+ "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.2.tgz",
+ "integrity": "sha512-V/yCWTTF7VJ9hIh18Ugr2zhJMP01MY7c5kh4J870L7imm6/DIzBsNLTXzMwUA3yZ5b/KBqLx8Kp3uRvd7xSe3Q==",
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "side-channel": "^1.1.0"
+ },
+ "engines": {
+ "node": ">=0.6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/range-parser": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
+ "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/raw-body": {
+ "version": "2.5.3",
+ "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz",
+ "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==",
+ "license": "MIT",
+ "dependencies": {
+ "bytes": "~3.1.2",
+ "http-errors": "~2.0.1",
+ "iconv-lite": "~0.4.24",
+ "unpipe": "~1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/readdirp": {
+ "version": "3.6.0",
+ "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
+ "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "picomatch": "^2.2.1"
+ },
+ "engines": {
+ "node": ">=8.10.0"
+ }
+ },
+ "node_modules/safe-buffer": {
+ "version": "5.2.1",
+ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
+ "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "MIT"
+ },
+ "node_modules/safer-buffer": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
+ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
+ "license": "MIT"
+ },
+ "node_modules/semver": {
+ "version": "7.8.0",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.0.tgz",
+ "integrity": "sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA==",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/send": {
+ "version": "0.19.2",
+ "resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz",
+ "integrity": "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==",
+ "license": "MIT",
+ "dependencies": {
+ "debug": "2.6.9",
+ "depd": "2.0.0",
+ "destroy": "1.2.0",
+ "encodeurl": "~2.0.0",
+ "escape-html": "~1.0.3",
+ "etag": "~1.8.1",
+ "fresh": "~0.5.2",
+ "http-errors": "~2.0.1",
+ "mime": "1.6.0",
+ "ms": "2.1.3",
+ "on-finished": "~2.4.1",
+ "range-parser": "~1.2.1",
+ "statuses": "~2.0.2"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/send/node_modules/ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+ "license": "MIT"
+ },
+ "node_modules/serve-static": {
+ "version": "1.16.3",
+ "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.3.tgz",
+ "integrity": "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==",
+ "license": "MIT",
+ "dependencies": {
+ "encodeurl": "~2.0.0",
+ "escape-html": "~1.0.3",
+ "parseurl": "~1.3.3",
+ "send": "~0.19.1"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/setprototypeof": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
+ "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==",
+ "license": "ISC"
+ },
+ "node_modules/side-channel": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz",
+ "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "object-inspect": "^1.13.3",
+ "side-channel-list": "^1.0.0",
+ "side-channel-map": "^1.0.1",
+ "side-channel-weakmap": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/side-channel-list": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz",
+ "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "object-inspect": "^1.13.4"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/side-channel-map": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz",
+ "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.2",
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.5",
+ "object-inspect": "^1.13.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/side-channel-weakmap": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz",
+ "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.2",
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.5",
+ "object-inspect": "^1.13.3",
+ "side-channel-map": "^1.0.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/simple-update-notifier": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz",
+ "integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "semver": "^7.5.3"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/statuses": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz",
+ "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/supports-color": {
+ "version": "5.5.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
+ "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "has-flag": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/to-regex-range": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
+ "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-number": "^7.0.0"
+ },
+ "engines": {
+ "node": ">=8.0"
+ }
+ },
+ "node_modules/toidentifier": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
+ "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.6"
+ }
+ },
+ "node_modules/touch": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.1.tgz",
+ "integrity": "sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA==",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "nodetouch": "bin/nodetouch.js"
+ }
+ },
+ "node_modules/type-is": {
+ "version": "1.6.18",
+ "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
+ "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==",
+ "license": "MIT",
+ "dependencies": {
+ "media-typer": "0.3.0",
+ "mime-types": "~2.1.24"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/undefsafe": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz",
+ "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/unpipe": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
+ "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/utils-merge": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
+ "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4.0"
+ }
+ },
+ "node_modules/vary": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
+ "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ }
+ }
+}
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..3c77dd6
--- /dev/null
+++ b/package.json
@@ -0,0 +1,24 @@
+{
+ "name": "commercialdesignreps",
+ "version": "0.1.0",
+ "private": true,
+ "description": "Commercial design sales rep directory — find independent reps for hospitality, healthcare, contract, and commercial interior design markets",
+ "main": "server.js",
+ "scripts": {
+ "start": "node server.js",
+ "dev": "nodemon server.js"
+ },
+ "dependencies": {
+ "compression": "^1.7.4",
+ "ejs": "^3.1.10",
+ "express": "^4.21.0",
+ "helmet": "^8.1.0",
+ "morgan": "^1.10.0"
+ },
+ "devDependencies": {
+ "nodemon": "^3.1.7"
+ },
+ "engines": {
+ "node": ">=20"
+ }
+}
diff --git a/public/css/styles.css b/public/css/styles.css
new file mode 100644
index 0000000..b885a60
--- /dev/null
+++ b/public/css/styles.css
@@ -0,0 +1,196 @@
+:root {
+ --bg: #0b1220;
+ --bg-elev: #11192a;
+ --bg-card: #131c30;
+ --fg: #e6ecff;
+ --fg-muted: #8a96b3;
+ --accent: #d4a574;
+ --accent-fg: #1a120a;
+ --border: #232c44;
+ --tag-bg: #1c2540;
+ --shadow: 0 10px 30px rgba(0,0,0,.35);
+ --radius: 14px;
+ --cols-min: 320px;
+ font-family: ui-sans-serif, -apple-system, BlinkMacSystemFont, "Inter", "Helvetica Neue", Arial, sans-serif;
+}
+[data-theme="light"] {
+ --bg: #f7f5f0;
+ --bg-elev: #ffffff;
+ --bg-card: #ffffff;
+ --fg: #1a1d29;
+ --fg-muted: #5a6478;
+ --accent: #8b5a2b;
+ --accent-fg: #ffffff;
+ --border: #e3dfd5;
+ --tag-bg: #efeae0;
+ --shadow: 0 6px 22px rgba(40,30,15,.08);
+}
+* { box-sizing: border-box; }
+html, body { margin: 0; padding: 0; }
+body {
+ background: var(--bg);
+ color: var(--fg);
+ line-height: 1.55;
+ -webkit-font-smoothing: antialiased;
+ min-height: 100vh;
+}
+a { color: var(--accent); text-decoration: none; }
+a:hover { text-decoration: underline; }
+.wrap { max-width: 1200px; margin: 0 auto; padding: 0 24px; }
+.muted { color: var(--fg-muted); }
+
+.site-header {
+ background: var(--bg-elev);
+ border-bottom: 1px solid var(--border);
+ position: sticky; top: 0; z-index: 30;
+}
+.header-row { display: flex; align-items: center; gap: 24px; padding: 14px 24px; }
+.brand { display: flex; align-items: center; gap: 10px; font-weight: 700; color: var(--fg); }
+.brand:hover { text-decoration: none; }
+.brand-mark {
+ width: 28px; height: 28px; border-radius: 8px;
+ background: linear-gradient(135deg, var(--accent), #b8845a);
+}
+.brand-name { font-size: 18px; letter-spacing: -0.01em; }
+.site-nav { display: flex; gap: 18px; margin-left: auto; }
+.site-nav a { color: var(--fg); font-size: 14px; }
+.theme-toggle {
+ width: 36px; height: 36px; border-radius: 50%;
+ border: 1px solid var(--border); background: var(--bg-card);
+ color: var(--fg); cursor: pointer; font-size: 16px;
+ display: inline-flex; align-items: center; justify-content: center;
+}
+.theme-toggle .sun { display: none; }
+.theme-toggle .moon { display: inline; }
+[data-theme="light"] .theme-toggle .sun { display: inline; }
+[data-theme="light"] .theme-toggle .moon { display: none; }
+
+.main { padding: 32px 24px 80px; }
+.hero { padding: 30px 0 24px; }
+.hero h1 { font-size: clamp(28px, 4vw, 44px); margin: 0 0 12px; letter-spacing: -0.02em; line-height: 1.1; }
+.hero .lead { color: var(--fg-muted); font-size: 17px; max-width: 740px; margin: 0; }
+
+.filter-bar {
+ display: grid; grid-template-columns: minmax(220px,1.4fr) repeat(3, 1fr) auto auto;
+ gap: 10px; margin: 24px 0 16px;
+ background: var(--bg-elev); padding: 14px; border-radius: var(--radius);
+ border: 1px solid var(--border);
+}
+.filter-bar input[type=search], .filter-bar select {
+ background: var(--bg-card); color: var(--fg);
+ border: 1px solid var(--border); padding: 10px 12px;
+ border-radius: 10px; font-size: 14px; min-width: 0;
+}
+.filter-bar input[type=search]:focus, .filter-bar select:focus {
+ outline: 2px solid var(--accent); outline-offset: 1px;
+}
+.btn-primary, .btn-ghost {
+ display: inline-flex; align-items: center; justify-content: center;
+ padding: 10px 16px; border-radius: 10px; font-weight: 600;
+ font-size: 14px; cursor: pointer; border: 1px solid transparent;
+ text-decoration: none;
+}
+.btn-primary { background: var(--accent); color: var(--accent-fg); border-color: var(--accent); }
+.btn-primary:hover { filter: brightness(1.05); text-decoration: none; }
+.btn-ghost { background: transparent; color: var(--fg); border-color: var(--border); }
+.btn-ghost:hover { background: var(--bg-card); text-decoration: none; }
+
+.result-bar {
+ display: flex; align-items: center; justify-content: space-between;
+ margin: 14px 0 16px; gap: 14px; flex-wrap: wrap;
+}
+.controls { display: flex; gap: 14px; align-items: center; }
+.control { display: flex; gap: 8px; align-items: center; font-size: 13px; color: var(--fg-muted); }
+.control select, .control input[type=range] {
+ background: var(--bg-card); color: var(--fg);
+ border: 1px solid var(--border); padding: 6px 10px;
+ border-radius: 8px; font-size: 13px;
+}
+.control input[type=range] { padding: 0; }
+
+.rep-grid {
+ list-style: none; padding: 0; margin: 0;
+ display: grid;
+ grid-template-columns: repeat(auto-fill, minmax(var(--cols-min), 1fr));
+ gap: 16px;
+}
+.rep-card {
+ background: var(--bg-card);
+ border: 1px solid var(--border);
+ border-radius: var(--radius);
+ transition: transform .15s ease, border-color .15s ease, box-shadow .15s ease;
+ overflow: hidden;
+}
+.rep-card:hover { transform: translateY(-2px); border-color: var(--accent); box-shadow: var(--shadow); }
+.rep-link { display: block; padding: 18px; color: var(--fg); }
+.rep-link:hover { text-decoration: none; }
+.rep-card-head { display: flex; gap: 12px; align-items: center; margin-bottom: 12px; }
+.rep-avatar {
+ width: 44px; height: 44px; border-radius: 50%;
+ background: linear-gradient(135deg, var(--accent), #b8845a);
+ color: var(--accent-fg); font-weight: 700; font-size: 14px;
+ display: flex; align-items: center; justify-content: center;
+ flex-shrink: 0;
+}
+.rep-avatar.lg { width: 80px; height: 80px; font-size: 26px; }
+.rep-name { font-weight: 600; font-size: 16px; }
+.rep-agency { color: var(--fg-muted); font-size: 13px; }
+.rep-meta { display: flex; flex-wrap: wrap; gap: 6px; margin-bottom: 10px; }
+.chip {
+ display: inline-block; font-size: 11px; padding: 3px 9px;
+ background: var(--tag-bg); color: var(--fg);
+ border-radius: 99px; border: 1px solid var(--border);
+}
+.chip.city { background: transparent; color: var(--fg-muted); }
+.rep-lines { font-size: 13px; margin-bottom: 10px; }
+.rep-vertical { display: flex; flex-wrap: wrap; gap: 4px; }
+.tag {
+ font-size: 10px; text-transform: uppercase; letter-spacing: .04em;
+ padding: 2px 8px; background: transparent; border: 1px solid var(--border);
+ color: var(--fg-muted); border-radius: 4px;
+}
+
+.empty { text-align: center; padding: 60px 20px; color: var(--fg-muted); }
+
+.back-link { font-size: 13px; color: var(--fg-muted); display: inline-block; margin-bottom: 14px; }
+.rep-detail { background: var(--bg-elev); border: 1px solid var(--border); border-radius: var(--radius); padding: 28px; }
+.rep-header { display: flex; gap: 18px; align-items: center; margin-bottom: 24px; }
+.rep-header h1 { margin: 0; font-size: 28px; letter-spacing: -0.01em; }
+.rep-header .agency { color: var(--accent); font-weight: 600; margin-top: 4px; }
+.rep-header .city { font-size: 14px; }
+.rep-grid-detail {
+ display: grid; gap: 24px; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
+ padding: 22px 0; border-top: 1px solid var(--border); border-bottom: 1px solid var(--border);
+}
+.rep-grid-detail h3 { margin: 0 0 10px; font-size: 13px; text-transform: uppercase; letter-spacing: .06em; color: var(--fg-muted); }
+.chips { display: flex; flex-wrap: wrap; gap: 6px; }
+.lines-list { list-style: none; padding: 0; margin: 0; display: grid; gap: 6px; font-size: 14px; }
+.cta-row { display: flex; gap: 10px; padding-top: 22px; flex-wrap: wrap; }
+
+.related { margin-top: 40px; }
+.related h2 { font-size: 20px; margin: 0 0 14px; }
+
+.long-form { max-width: 720px; }
+.long-form h1 { font-size: 32px; margin: 0 0 20px; }
+.long-form h2 { font-size: 18px; margin-top: 28px; }
+.long-form p { color: var(--fg); }
+
+.seo-block { margin-top: 60px; padding-top: 30px; border-top: 1px solid var(--border); max-width: 820px; }
+.seo-block h2 { font-size: 18px; margin: 0 0 12px; color: var(--fg-muted); text-transform: uppercase; letter-spacing: .06em; }
+.seo-block p { color: var(--fg-muted); font-size: 14px; }
+
+.site-footer { background: var(--bg-elev); border-top: 1px solid var(--border); margin-top: 60px; padding: 36px 0 24px; }
+.footer-grid {
+ display: grid; gap: 28px; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
+ padding-bottom: 24px;
+}
+.site-footer strong { display: block; margin-bottom: 6px; font-size: 14px; }
+.site-footer p { font-size: 13px; line-height: 1.7; color: var(--fg-muted); margin: 0; }
+.site-footer a { color: var(--fg); }
+.copy { padding-top: 18px; border-top: 1px solid var(--border); color: var(--fg-muted); }
+
+@media (max-width: 720px) {
+ .filter-bar { grid-template-columns: 1fr 1fr; }
+ .filter-bar input[type=search] { grid-column: 1 / -1; }
+ .site-nav a:nth-child(1) { display: none; }
+}
diff --git a/public/favicon.svg b/public/favicon.svg
new file mode 100644
index 0000000..bc779be
--- /dev/null
+++ b/public/favicon.svg
@@ -0,0 +1,11 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
+ <defs>
+ <linearGradient id="g" x1="0%" y1="0%" x2="100%" y2="100%">
+ <stop offset="0%" stop-color="#d4a574"/>
+ <stop offset="100%" stop-color="#8b5a2b"/>
+ </linearGradient>
+ </defs>
+ <rect width="64" height="64" rx="14" fill="url(#g)"/>
+ <text x="50%" y="56%" font-family="Helvetica,Arial,sans-serif" font-size="28" font-weight="700"
+ text-anchor="middle" dominant-baseline="middle" fill="#1a120a">CR</text>
+</svg>
diff --git a/public/js/main.js b/public/js/main.js
new file mode 100644
index 0000000..b4990d8
--- /dev/null
+++ b/public/js/main.js
@@ -0,0 +1,45 @@
+(function () {
+ const root = document.documentElement;
+ const toggle = document.getElementById('theme-toggle');
+ if (toggle) {
+ toggle.addEventListener('click', () => {
+ const next = root.getAttribute('data-theme') === 'light' ? 'dark' : 'light';
+ if (next === 'light') root.setAttribute('data-theme', 'light');
+ else root.removeAttribute('data-theme');
+ try { localStorage.setItem('cdr-theme', next); } catch (e) {}
+ });
+ }
+
+ const grid = document.getElementById('rep-grid');
+ const slider = document.getElementById('density-slider');
+ if (slider && grid) {
+ let saved = null;
+ try { saved = localStorage.getItem('cdr-density'); } catch (e) {}
+ if (saved) {
+ slider.value = saved;
+ grid.style.setProperty('--cols-min', saved + 'px');
+ }
+ slider.addEventListener('input', () => {
+ grid.style.setProperty('--cols-min', slider.value + 'px');
+ try { localStorage.setItem('cdr-density', slider.value); } catch (e) {}
+ });
+ }
+
+ const sortSelect = document.getElementById('sort-select');
+ if (sortSelect) {
+ let savedSort = null;
+ try { savedSort = localStorage.getItem('cdr-sort'); } catch (e) {}
+ const url = new URL(window.location.href);
+ if (savedSort && !url.searchParams.get('sort') && sortSelect.value === 'newest' && savedSort !== 'newest') {
+ url.searchParams.set('sort', savedSort);
+ window.location.replace(url.toString());
+ return;
+ }
+ sortSelect.addEventListener('change', () => {
+ try { localStorage.setItem('cdr-sort', sortSelect.value); } catch (e) {}
+ const u = new URL(window.location.href);
+ u.searchParams.set('sort', sortSelect.value);
+ window.location.assign(u.toString());
+ });
+ }
+})();
diff --git a/public/robots.txt b/public/robots.txt
new file mode 100644
index 0000000..f11f866
--- /dev/null
+++ b/public/robots.txt
@@ -0,0 +1,3 @@
+User-agent: *
+Allow: /
+Sitemap: https://commercialdesignreps.com/sitemap.xml
diff --git a/server.js b/server.js
new file mode 100644
index 0000000..73663b0
--- /dev/null
+++ b/server.js
@@ -0,0 +1,111 @@
+const express = require('express');
+const path = require('path');
+const fs = require('fs');
+const helmet = require('helmet');
+const compression = require('compression');
+const morgan = require('morgan');
+
+const app = express();
+const PORT = process.env.PORT || 9922;
+const GA_ID = process.env.GA_ID || 'G-PRJ51PTW5G';
+
+const reps = JSON.parse(fs.readFileSync(path.join(__dirname, 'data', 'reps.json'), 'utf8'));
+
+app.set('view engine', 'ejs');
+app.set('views', path.join(__dirname, 'views'));
+
+app.use(helmet({
+ contentSecurityPolicy: false,
+ crossOriginEmbedderPolicy: false,
+}));
+app.use(compression());
+app.use(morgan('tiny'));
+
+app.use((req, res, next) => {
+ res.set('Cache-Control', 'no-store, must-revalidate');
+ next();
+});
+
+app.use(express.static(path.join(__dirname, 'public'), { maxAge: '1h' }));
+
+const allTerritories = Array.from(new Set(reps.flatMap(r => r.territories))).sort();
+const allLines = Array.from(new Set(reps.flatMap(r => r.lines))).sort();
+const allVerticals = Array.from(new Set(reps.flatMap(r => r.vertical))).sort();
+const allCities = Array.from(new Set(reps.map(r => r.city))).sort();
+
+const SITE = {
+ domain: 'commercialdesignreps.com',
+ name: 'Commercial Design Reps',
+ tagline: 'Independent sales reps for commercial, hospitality, and contract interior design',
+ description: 'Find independent design reps and showroom agencies for commercial, hospitality, healthcare, and contract textile and wallcovering lines across the United States.',
+ email: 'info@commercialdesignreps.com',
+ phone: '888-373-4564',
+ address: 'Designer Wallcoverings, 15442 Ventura Bl #102, Sherman Oaks CA 91403',
+ ga_id: GA_ID,
+};
+
+function sortReps(list, sort) {
+ const copy = list.slice();
+ switch (sort) {
+ case 'name':
+ return copy.sort((a, b) => a.name.localeCompare(b.name));
+ case 'agency':
+ return copy.sort((a, b) => a.agency.localeCompare(b.agency));
+ case 'city':
+ return copy.sort((a, b) => a.city.localeCompare(b.city));
+ case 'territory':
+ return copy.sort((a, b) => (a.territories[0] || '').localeCompare(b.territories[0] || ''));
+ case 'newest':
+ default:
+ return copy.sort((a, b) => b.id - a.id);
+ }
+}
+
+app.get('/', (req, res) => {
+ const { q, territory, vertical, line, sort } = req.query;
+ let filtered = reps.slice();
+
+ if (q) {
+ const needle = String(q).toLowerCase();
+ filtered = filtered.filter(r =>
+ r.name.toLowerCase().includes(needle) ||
+ r.agency.toLowerCase().includes(needle) ||
+ r.city.toLowerCase().includes(needle) ||
+ r.lines.some(l => l.toLowerCase().includes(needle))
+ );
+ }
+ if (territory) filtered = filtered.filter(r => r.territories.includes(String(territory)));
+ if (vertical) filtered = filtered.filter(r => r.vertical.includes(String(vertical)));
+ if (line) filtered = filtered.filter(r => r.lines.includes(String(line)));
+
+ filtered = sortReps(filtered, sort);
+
+ res.render('index', {
+ site: SITE,
+ reps: filtered,
+ total: reps.length,
+ matched: filtered.length,
+ filters: { q: q || '', territory: territory || '', vertical: vertical || '', line: line || '', sort: sort || 'newest' },
+ facets: { territories: allTerritories, lines: allLines, verticals: allVerticals, cities: allCities },
+ });
+});
+
+app.get('/rep/:id', (req, res) => {
+ const id = Number(req.params.id);
+ const rep = reps.find(r => r.id === id);
+ if (!rep) return res.status(404).render('404', { site: SITE });
+ const related = reps.filter(r => r.id !== rep.id && r.vertical.some(v => rep.vertical.includes(v))).slice(0, 4);
+ res.render('rep', { site: SITE, rep, related });
+});
+
+app.get('/about', (req, res) => {
+ res.render('about', { site: SITE });
+});
+
+app.get('/healthz', (req, res) => res.json({ ok: true, port: PORT, reps: reps.length }));
+
+app.use((req, res) => res.status(404).render('404', { site: SITE }));
+
+app.listen(PORT, () => {
+ console.log(`commercialdesignreps listening on :${PORT} (${reps.length} reps loaded, GA ${GA_ID})`);
+});
diff --git a/views/404.ejs b/views/404.ejs
new file mode 100644
index 0000000..86b285f
--- /dev/null
+++ b/views/404.ejs
@@ -0,0 +1,10 @@
+<%- include('partials/head', { title: 'Not found — ' + site.name, description: 'Page not found.' }) %>
+
+<main class="wrap main">
+ <section class="empty">
+ <h1>Page not found</h1>
+ <p>The page you tried to load isn't here. <a href="/">Return to the directory</a>.</p>
+ </section>
+</main>
+
+<%- include('partials/foot') %>
diff --git a/views/about.ejs b/views/about.ejs
new file mode 100644
index 0000000..fb9217e
--- /dev/null
+++ b/views/about.ejs
@@ -0,0 +1,25 @@
+<%- include('partials/head', { title: 'About — ' + site.name, description: 'Commercial Design Reps is an industry directory of independent sales reps and showroom agencies in commercial, hospitality, healthcare, and contract interior design.' }) %>
+
+<main class="wrap main">
+ <a class="back-link" href="/">← Back</a>
+
+ <article class="long-form">
+ <h1>About Commercial Design Reps</h1>
+
+ <p>Commercial Design Reps is a free, curated directory of independent sales-rep agencies serving the U.S. commercial design industry. The directory exists because specifiers, A&D firms, contract dealers, and purchasing teams routinely struggle to figure out <em>who carries what, where</em> — especially in the contract, hospitality, healthcare, and commercial interiors space, where representation maps shift constantly and brand-by-brand "find a rep" pages are out of date or hard to reach.</p>
+
+ <h2>What's in the directory</h2>
+ <p>Each listing includes the agency principal, the agency name, the territories the rep covers, the manufacturer and mill lines they carry, the verticals they actively service (residential, contract, hospitality, healthcare, education, retail, F&B, resort, office), the city the rep is based in, and a direct contact path. Filters above the directory let you narrow by territory, by vertical, and by line.</p>
+
+ <h2>Who maintains it</h2>
+ <p>The directory is operated by <a href="https://designerwallcoverings.com">Designer Wallcoverings</a> as a free industry resource. We are a wallcovering distributor, so we have a vested interest in keeping rep-territory maps accurate — and we're publishing this index in the hope that it's useful well beyond our own product line. Listings are not pay-to-play and there is no advertising on the site.</p>
+
+ <h2>How to list your agency</h2>
+ <p>If you run a rep agency and would like to be added — or if you are listed and need to update territories, lines, or contact info — email <a href="mailto:<%= site.email %>"><%= site.email %></a> with the details. We typically update the directory weekly.</p>
+
+ <h2>How to flag a correction</h2>
+ <p>If a listing is wrong or out of date, email <a href="mailto:<%= site.email %>"><%= site.email %></a> with the rep name, agency, and the correction. We act on corrections promptly.</p>
+ </article>
+</main>
+
+<%- include('partials/foot') %>
diff --git a/views/index.ejs b/views/index.ejs
new file mode 100644
index 0000000..256c321
--- /dev/null
+++ b/views/index.ejs
@@ -0,0 +1,100 @@
+<%- include('partials/head', { title: site.name + ' — ' + site.tagline, description: site.description }) %>
+
+<main class="wrap main">
+ <section class="hero">
+ <h1>Commercial design reps, by territory and line.</h1>
+ <p class="lead">A working directory of independent sales reps, agency principals, and showroom partners covering the U.S. commercial, hospitality, healthcare, and contract design markets. <%= total %> agencies indexed.</p>
+ </section>
+
+ <form class="filter-bar" method="get" action="/">
+ <input type="search" name="q" value="<%= filters.q %>" placeholder="Search rep, agency, line, city…" aria-label="Search">
+
+ <select name="territory" aria-label="Territory">
+ <option value="">All territories</option>
+ <% facets.territories.forEach(function(t){ %>
+ <option value="<%= t %>" <%= filters.territory === t ? 'selected' : '' %>><%= t %></option>
+ <% }) %>
+ </select>
+
+ <select name="vertical" aria-label="Vertical">
+ <option value="">All verticals</option>
+ <% facets.verticals.forEach(function(v){ %>
+ <option value="<%= v %>" <%= filters.vertical === v ? 'selected' : '' %>><%= v %></option>
+ <% }) %>
+ </select>
+
+ <select name="line" aria-label="Line carried">
+ <option value="">All lines</option>
+ <% facets.lines.forEach(function(l){ %>
+ <option value="<%= l %>" <%= filters.line === l ? 'selected' : '' %>><%= l %></option>
+ <% }) %>
+ </select>
+
+ <button type="submit" class="btn-primary">Filter</button>
+ <% if (filters.q || filters.territory || filters.vertical || filters.line) { %>
+ <a class="btn-ghost" href="/">Reset</a>
+ <% } %>
+ </form>
+
+ <div class="result-bar">
+ <div class="muted"><%= matched %> of <%= total %> matching</div>
+ <div class="controls">
+ <label class="control">
+ <span>Sort</span>
+ <select id="sort-select" name="sort">
+ <option value="newest" <%= filters.sort === 'newest' ? 'selected' : '' %>>Newest</option>
+ <option value="name" <%= filters.sort === 'name' ? 'selected' : '' %>>Name A→Z</option>
+ <option value="agency" <%= filters.sort === 'agency' ? 'selected' : '' %>>Agency A→Z</option>
+ <option value="city" <%= filters.sort === 'city' ? 'selected' : '' %>>City A→Z</option>
+ <option value="territory" <%= filters.sort === 'territory' ? 'selected' : '' %>>Territory</option>
+ </select>
+ </label>
+ <label class="control">
+ <span>Density</span>
+ <input id="density-slider" type="range" min="220" max="420" step="20" value="300">
+ </label>
+ </div>
+ </div>
+
+ <% if (!matched) { %>
+ <div class="empty">No reps match those filters. <a href="/">Reset</a>.</div>
+ <% } else { %>
+ <ul class="rep-grid" id="rep-grid">
+ <% reps.forEach(function(r){ %>
+ <li class="rep-card">
+ <a class="rep-link" href="/rep/<%= r.id %>">
+ <div class="rep-card-head">
+ <div class="rep-avatar"><%= r.name.split(' ').map(s => s[0]).join('').slice(0,2) %></div>
+ <div>
+ <div class="rep-name"><%= r.name %></div>
+ <div class="rep-agency"><%= r.agency %></div>
+ </div>
+ </div>
+ <div class="rep-meta">
+ <span class="chip city"><%= r.city %></span>
+ <% r.territories.slice(0,4).forEach(function(t){ %>
+ <span class="chip"><%= t %></span>
+ <% }) %>
+ </div>
+ <div class="rep-lines muted">
+ <%= r.lines.slice(0,3).join(' · ') %>
+ </div>
+ <div class="rep-vertical">
+ <% r.vertical.forEach(function(v){ %>
+ <span class="tag"><%= v %></span>
+ <% }) %>
+ </div>
+ </a>
+ </li>
+ <% }) %>
+ </ul>
+ <% } %>
+
+ <section class="seo-block">
+ <h2>How this directory works</h2>
+ <p>Commercial Design Reps is a curated index of independent sales-rep agencies serving the contract, hospitality, and commercial interior design industries. Each listing maps the rep's territory coverage, the manufacturer lines they carry, and the verticals (residential, contract, hospitality, healthcare, education, F&B, retail) they actively service. Use the filters above to find a rep by region, by line carried, or by project type. Specifiers, designers, architects, and purchasing teams can contact reps directly through the listing page.</p>
+ <p>This directory is operated by <a href="https://designerwallcoverings.com">Designer Wallcoverings</a> as a free industry resource. To list your agency, update an existing listing, or correct an entry, email <a href="mailto:<%= site.email %>"><%= site.email %></a>.</p>
+ </section>
+</main>
+
+<%- include('partials/foot') %>
diff --git a/views/partials/foot.ejs b/views/partials/foot.ejs
new file mode 100644
index 0000000..d6b3148
--- /dev/null
+++ b/views/partials/foot.ejs
@@ -0,0 +1,29 @@
+<footer class="site-footer">
+ <div class="wrap footer-grid">
+ <div>
+ <strong><%= site.name %></strong>
+ <p class="muted"><%= site.tagline %></p>
+ </div>
+ <div>
+ <strong>Contact</strong>
+ <p>
+ <a href="mailto:<%= site.email %>"><%= site.email %></a><br>
+ <a href="tel:+1<%= site.phone.replace(/-/g,'') %>"><%= site.phone %></a><br>
+ <span class="muted"><%= site.address %></span>
+ </p>
+ </div>
+ <div>
+ <strong>For reps</strong>
+ <p>
+ <a href="mailto:<%= site.email %>?subject=List%20our%20agency">List your agency</a><br>
+ <a href="mailto:<%= site.email %>?subject=Update%20our%20listing">Update a listing</a>
+ </p>
+ </div>
+ </div>
+ <div class="wrap copy">
+ <small>© <%= new Date().getFullYear() %> <%= site.name %>. A directory project of Designer Wallcoverings.</small>
+ </div>
+</footer>
+<script src="/js/main.js" defer></script>
+</body>
+</html>
diff --git a/views/partials/head.ejs b/views/partials/head.ejs
new file mode 100644
index 0000000..f9abc2e
--- /dev/null
+++ b/views/partials/head.ejs
@@ -0,0 +1,47 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+<meta charset="utf-8">
+<meta name="viewport" content="width=device-width,initial-scale=1">
+<title><%= title || (site.name + ' — ' + site.tagline) %></title>
+<meta name="description" content="<%= description || site.description %>">
+<meta name="theme-color" content="#0f172a">
+<link rel="icon" href="/favicon.svg" type="image/svg+xml">
+<link rel="stylesheet" href="/css/styles.css">
+<script>
+(function(){
+ try {
+ var t = localStorage.getItem('cdr-theme');
+ if (t === 'light' || (t == null && window.matchMedia && window.matchMedia('(prefers-color-scheme: light)').matches)) {
+ document.documentElement.setAttribute('data-theme','light');
+ }
+ } catch(e){}
+})();
+</script>
+<!-- GA4 gtag (auto) -->
+<script async src="https://www.googletagmanager.com/gtag/js?id=<%= site.ga_id %>"></script>
+<script>
+window.dataLayer = window.dataLayer || [];
+function gtag(){dataLayer.push(arguments);}
+gtag('js', new Date());
+gtag('config', '<%= site.ga_id %>');
+</script>
+<!-- /GA4 gtag -->
+</head>
+<body>
+<header class="site-header">
+ <div class="wrap header-row">
+ <a class="brand" href="/">
+ <span class="brand-mark"></span>
+ <span class="brand-name"><%= site.name %></span>
+ </a>
+ <nav class="site-nav">
+ <a href="/">Directory</a>
+ <a href="/about">About</a>
+ <a href="mailto:<%= site.email %>">Contact</a>
+ </nav>
+ <button id="theme-toggle" class="theme-toggle" aria-label="Toggle theme" title="Toggle dark/light">
+ <span class="moon">☽</span><span class="sun">☀</span>
+ </button>
+ </div>
+</header>
diff --git a/views/rep.ejs b/views/rep.ejs
new file mode 100644
index 0000000..1cd500c
--- /dev/null
+++ b/views/rep.ejs
@@ -0,0 +1,69 @@
+<%- include('partials/head', { title: rep.name + ' — ' + rep.agency + ' | ' + site.name, description: rep.agency + ' represents ' + rep.lines.join(', ') + ' in ' + rep.territories.join(', ') + '. Independent commercial design rep for ' + rep.vertical.join(', ') + '.' }) %>
+
+<main class="wrap main">
+ <a class="back-link" href="/">← Back to directory</a>
+
+ <section class="rep-detail">
+ <div class="rep-header">
+ <div class="rep-avatar lg"><%= rep.name.split(' ').map(s => s[0]).join('').slice(0,2) %></div>
+ <div>
+ <h1><%= rep.name %></h1>
+ <div class="agency"><%= rep.agency %></div>
+ <div class="city muted"><%= rep.city %></div>
+ </div>
+ </div>
+
+ <div class="rep-grid-detail">
+ <div>
+ <h3>Territories</h3>
+ <div class="chips">
+ <% rep.territories.forEach(function(t){ %><span class="chip"><%= t %></span><% }) %>
+ </div>
+ </div>
+ <div>
+ <h3>Verticals</h3>
+ <div class="chips">
+ <% rep.vertical.forEach(function(v){ %><span class="tag"><%= v %></span><% }) %>
+ </div>
+ </div>
+ <div>
+ <h3>Lines carried</h3>
+ <ul class="lines-list">
+ <% rep.lines.forEach(function(l){ %><li><%= l %></li><% }) %>
+ </ul>
+ </div>
+ </div>
+
+ <div class="cta-row">
+ <a class="btn-primary" href="<%= rep.contact_url %>" target="_blank" rel="noopener">Contact agency →</a>
+ <a class="btn-ghost" href="mailto:<%= site.email %>?subject=Question%20about%20<%= encodeURIComponent(rep.agency) %>">Ask the directory</a>
+ </div>
+ </section>
+
+ <% if (related.length) { %>
+ <section class="related">
+ <h2>Other reps in similar verticals</h2>
+ <ul class="rep-grid" style="--cols-min: 280px">
+ <% related.forEach(function(r){ %>
+ <li class="rep-card">
+ <a class="rep-link" href="/rep/<%= r.id %>">
+ <div class="rep-card-head">
+ <div class="rep-avatar"><%= r.name.split(' ').map(s => s[0]).join('').slice(0,2) %></div>
+ <div>
+ <div class="rep-name"><%= r.name %></div>
+ <div class="rep-agency"><%= r.agency %></div>
+ </div>
+ </div>
+ <div class="rep-meta">
+ <span class="chip city"><%= r.city %></span>
+ <% r.territories.slice(0,3).forEach(function(t){ %><span class="chip"><%= t %></span><% }) %>
+ </div>
+ </a>
+ </li>
+ <% }) %>
+ </ul>
+ </section>
+ <% } %>
+</main>
+
+<%- include('partials/foot') %>
(oldest)
·
back to Commercialdesignreps
·
hamburger nav: apply canonical pattern from sister-site flee 2c0ea85 →