← back to Hospitalitydesignreps
initial scaffold + GA gtag (G-M79CJBLRFH, port 9927)
1cdf5f55a500fba79cfc714735365f8a62a10a5d · 2026-05-10 03:37:08 -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 1cdf5f55a500fba79cfc714735365f8a62a10a5d
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Sun May 10 03:37:08 2026 -0700
initial scaffold + GA gtag (G-M79CJBLRFH, port 9927)
---
.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..827ae53
--- /dev/null
+++ b/README.md
@@ -0,0 +1,29 @@
+# hospitalitydesignreps.com
+
+Hospitality design sales-rep directory. Independent reps and showroom agencies covering U.S. boutique hotels, hotel brands, resorts, casinos, cruise lines, F&B, and spa interior design.
+
+## Stack
+
+- Express + EJS (slim, no DB at MVP)
+- Seed data in `data/reps.json` (40 reps, hospitality-vertical focus)
+- pm2 port 9927 on Kamatera
+- GA4 `G-M79CJBLRFH`
+
+## Run locally
+
+```bash
+npm ci
+npm start # → http://localhost:9927
+```
+
+## Deploy
+
+Rsync to `root@45.61.58.125:/root/Projects/hospitalitydesignreps/`, then:
+
+```
+npm ci
+pm2 start server.js --name hospitalitydesignreps
+pm2 save
+```
+
+nginx vhost at `/etc/nginx/sites-enabled/hospitalitydesignreps.com` proxies port 9927.
diff --git a/data/reps.json b/data/reps.json
new file mode 100644
index 0000000..2e9e5ee
--- /dev/null
+++ b/data/reps.json
@@ -0,0 +1,42 @@
+[
+ {"id":1,"name":"Marielle Costanzo","agency":"Costanzo Hospitality Group","territories":["NV","CA","AZ"],"lines":["Maharam","Momentum","CF Stinson"],"city":"Las Vegas","vertical":["Hotel Brand","Casino","Resort"],"contact_url":"https://costanzohospitalitygroup.com/contact"},
+ {"id":2,"name":"Daniel Petrov","agency":"Petrov Resort Reps","territories":["FL","GA","SC","NC"],"lines":["Perennials","Sunbrella Contract","Bella-Dura"],"city":"Miami","vertical":["Resort","Boutique Hotel","F&B"],"contact_url":"https://petrovresortreps.com/contact"},
+ {"id":3,"name":"Yuki Tanaka","agency":"Pacific Hospitality Reps","territories":["CA","HI","OR","WA"],"lines":["Pollack","Pindler","Holly Hunt Hospitality"],"city":"Honolulu","vertical":["Resort","Hotel Brand","Spa"],"contact_url":"https://pacifichospitalityreps.com/contact"},
+ {"id":4,"name":"Brendan O'Hare","agency":"O'Hare Hotel Group","territories":["NY","NJ","CT","PA"],"lines":["Carnegie","Designtex","Architex"],"city":"New York","vertical":["Hotel Brand","Boutique Hotel","F&B"],"contact_url":"https://oharehotelgroup.com/contact"},
+ {"id":5,"name":"Imani Solomon","agency":"Solomon Hospitality Alliance","territories":["GA","AL","TN","FL"],"lines":["Mayer Fabrics","Wolf-Gordon","Pallas Textiles"],"city":"Atlanta","vertical":["Hotel Brand","Convention","F&B"],"contact_url":"https://solomonhospitalityalliance.com/contact"},
+ {"id":6,"name":"Helene Marchetti","agency":"Marchetti Resort Group","territories":["FL","TX","LA"],"lines":["Crypton","Ultrafabrics","Spradling"],"city":"Orlando","vertical":["Resort","Vacation Rental","F&B"],"contact_url":"https://marchettiresortgroup.com/contact"},
+ {"id":7,"name":"Garrison Wells","agency":"Wells Casino Reps","territories":["NV","NJ","MS"],"lines":["Maharam","Knoll Textiles","Designtex"],"city":"Reno","vertical":["Casino","Hotel Brand","F&B"],"contact_url":"https://wellscasinoreps.com/contact"},
+ {"id":8,"name":"Priya Venkatesan","agency":"Venkatesan Hotel Brand Group","territories":["TX","OK","NM","AR"],"lines":["Carnegie","Momentum","CF Stinson"],"city":"Austin","vertical":["Hotel Brand","Boutique Hotel"],"contact_url":"https://venkatesanhotelbrandgroup.com/contact"},
+ {"id":9,"name":"Conrad Voss","agency":"Voss Boutique Hotel Reps","territories":["NY","MA","RI","CT"],"lines":["Phillip Jeffries","Innovations USA","Arte International"],"city":"Brooklyn","vertical":["Boutique Hotel","Hotel Brand","Spa"],"contact_url":"https://vossboutiquehotelreps.com/contact"},
+ {"id":10,"name":"Adelaide Faraj","agency":"Faraj Spa & Wellness Reps","territories":["CA","AZ","NV","CO"],"lines":["Romo Black","Casamance","Elitis"],"city":"Scottsdale","vertical":["Spa","Resort","Boutique Hotel"],"contact_url":"https://farajspawellnessreps.com/contact"},
+ {"id":11,"name":"Tobias Hartwell","agency":"Hartwell Cruise Line Reps","territories":["FL","CA","WA"],"lines":["Maharam","Designtex","Pallas Textiles"],"city":"Fort Lauderdale","vertical":["Cruise","Resort","F&B"],"contact_url":"https://hartwellcruiselinereps.com/contact"},
+ {"id":12,"name":"Sofia Cardenas","agency":"Cardenas F&B Hospitality","territories":["CA","NV","HI"],"lines":["Crypton","Sunbrella Contract","Bella-Dura"],"city":"San Diego","vertical":["F&B","Boutique Hotel","Resort"],"contact_url":"https://cardenasfbhospitality.com/contact"},
+ {"id":13,"name":"Reginald Castellanos","agency":"Castellanos Hospitality Reps","territories":["NY","NJ","FL"],"lines":["Holly Hunt Hospitality","Edelman Leather","Stark Carpet Hospitality"],"city":"Manhattan","vertical":["Luxury Hotel","Boutique Hotel","Spa"],"contact_url":"https://castellanoshospitalityreps.com/contact"},
+ {"id":14,"name":"Mei-Lin Zhao","agency":"Zhao Hotel Standards Group","territories":["CA","WA","OR"],"lines":["Knoll Textiles","Architex","Mayer Fabrics"],"city":"San Francisco","vertical":["Hotel Brand","Convention","F&B"],"contact_url":"https://zhaohotelstandardsgroup.com/contact"},
+ {"id":15,"name":"Jasper Whitlock","agency":"Whitlock Resort Alliance","territories":["CO","UT","WY","MT","ID"],"lines":["Perennials","Pindler","Pollack"],"city":"Denver","vertical":["Resort","Vacation Rental","Boutique Hotel"],"contact_url":"https://whitlockresortalliance.com/contact"},
+ {"id":16,"name":"Esme Beaumarchais","agency":"Beaumarchais Hospitality Group","territories":["LA","MS","AL","TX"],"lines":["Wolf-Gordon","Innovations USA","Mayer Fabrics"],"city":"New Orleans","vertical":["Boutique Hotel","Hotel Brand","F&B"],"contact_url":"https://beaumarchaishospitalitygroup.com/contact"},
+ {"id":17,"name":"Marcus Eldridge","agency":"Eldridge Hotel Reps","territories":["IL","WI","MI","OH","IN"],"lines":["Carnegie","Designtex","CF Stinson"],"city":"Chicago","vertical":["Hotel Brand","Convention","Boutique Hotel"],"contact_url":"https://eldridgehotelreps.com/contact"},
+ {"id":18,"name":"Yelena Voronova","agency":"Voronova Luxury Hospitality","territories":["NY","CA","FL"],"lines":["Dedar","Sahco","Loro Piana Interiors"],"city":"Miami Beach","vertical":["Luxury Hotel","Boutique Hotel","Resort"],"contact_url":"https://voronovaluxuryhospitality.com/contact"},
+ {"id":19,"name":"Devontae Pickering","agency":"Pickering Hospitality Reps","territories":["VA","DC","MD","DE"],"lines":["Momentum","Pallas Textiles","Architex"],"city":"Washington","vertical":["Hotel Brand","Convention","F&B"],"contact_url":"https://pickeringhospitalityreps.com/contact"},
+ {"id":20,"name":"Linnea Bjornstrom","agency":"Bjornstrom Vacation Rental Reps","territories":["FL","NC","SC","TN"],"lines":["Crypton","Sunbrella Contract","Spradling"],"city":"Asheville","vertical":["Vacation Rental","Resort","Boutique Hotel"],"contact_url":"https://bjornstromvacationrentalreps.com/contact"},
+ {"id":21,"name":"Hugo Brandeis","agency":"Brandeis Hotel Brand Reps","territories":["TX","CO","KS","MO"],"lines":["Knoll Textiles","Carnegie","Wolf-Gordon"],"city":"Dallas","vertical":["Hotel Brand","Convention","F&B"],"contact_url":"https://brandeishotelbrandreps.com/contact"},
+ {"id":22,"name":"Camille Roussel","agency":"Roussel Boutique Reps","territories":["CA","NV","UT"],"lines":["Phillip Jeffries","Arte International","Casamance"],"city":"Beverly Hills","vertical":["Boutique Hotel","Spa","Luxury Hotel"],"contact_url":"https://rousselboutiquereps.com/contact"},
+ {"id":23,"name":"Owen Halloran","agency":"Halloran Casino Hospitality","territories":["NV","NJ","MS","CT"],"lines":["Maharam","Designtex","Architex"],"city":"Atlantic City","vertical":["Casino","Hotel Brand","F&B"],"contact_url":"https://hallorancasinohospitality.com/contact"},
+ {"id":24,"name":"Ines Galvao","agency":"Galvao Resort Group","territories":["FL","HI","CA"],"lines":["Perennials","Bella-Dura","Pindler"],"city":"Honolulu","vertical":["Resort","Cruise","Spa"],"contact_url":"https://galvaoresortgroup.com/contact"},
+ {"id":25,"name":"Aleksander Kovac","agency":"Kovac Hospitality Healthcare Reps","territories":["IL","MN","WI","IA"],"lines":["Carnegie","Mayer Fabrics","CF Stinson"],"city":"Minneapolis","vertical":["Hospitality Healthcare","Spa","Boutique Hotel"],"contact_url":"https://kovachospitalityhealthcarereps.com/contact"},
+ {"id":26,"name":"Renata Salgado","agency":"Salgado Convention Reps","territories":["NV","FL","TX","IL"],"lines":["Pallas Textiles","Architex","Designtex"],"city":"Las Vegas","vertical":["Convention","Hotel Brand","F&B"],"contact_url":"https://salgadoconventionreps.com/contact"},
+ {"id":27,"name":"Charles Eastland","agency":"Eastland Hotel Group","territories":["MA","NH","ME","VT","RI"],"lines":["Innovations USA","Wolf-Gordon","Pollack"],"city":"Boston","vertical":["Boutique Hotel","Hotel Brand","Resort"],"contact_url":"https://eastlandhotelgroup.com/contact"},
+ {"id":28,"name":"Talia Greenfield","agency":"Greenfield Spa Reps","territories":["AZ","NM","CA","NV"],"lines":["Romo Black","Elitis","Casamance"],"city":"Tucson","vertical":["Spa","Resort","Wellness"],"contact_url":"https://greenfieldspareps.com/contact"},
+ {"id":29,"name":"Quinn Ardmore","agency":"Ardmore Hospitality Solutions","territories":["WA","OR","ID","AK"],"lines":["Crypton","Ultrafabrics","Spradling"],"city":"Seattle","vertical":["Hotel Brand","Boutique Hotel","Cruise"],"contact_url":"https://ardmorehospitalitysolutions.com/contact"},
+ {"id":30,"name":"Beatriz Olvera","agency":"Olvera F&B Reps","territories":["TX","FL","CA","NY"],"lines":["Crypton","Bella-Dura","Architex"],"city":"Houston","vertical":["F&B","Boutique Hotel","Hotel Brand"],"contact_url":"https://olverafandbreps.com/contact"},
+ {"id":31,"name":"Soren Lindqvist","agency":"Lindqvist Resort Reps","territories":["MN","WI","MI","ND","SD"],"lines":["Pallas Textiles","CF Stinson","Mayer Fabrics"],"city":"Duluth","vertical":["Resort","Vacation Rental","Boutique Hotel"],"contact_url":"https://lindqvistresortreps.com/contact"},
+ {"id":32,"name":"Anaya Rashid","agency":"Rashid Hospitality Group","territories":["NJ","NY","PA"],"lines":["Maharam","Carnegie","Knoll Textiles"],"city":"Newark","vertical":["Hotel Brand","Convention","F&B"],"contact_url":"https://rashidhospitalitygroup.com/contact"},
+ {"id":33,"name":"Lawrence Pemberton","agency":"Pemberton Boutique Hospitality","territories":["TN","KY","NC","VA"],"lines":["Innovations USA","Phillip Jeffries","Wolf-Gordon"],"city":"Nashville","vertical":["Boutique Hotel","F&B","Spa"],"contact_url":"https://pembertonboutiquehospitality.com/contact"},
+ {"id":34,"name":"Greta Aalto","agency":"Aalto Cruise & Marine Reps","territories":["FL","CA","WA","HI"],"lines":["Crypton","Sunbrella Contract","Spradling"],"city":"Miami","vertical":["Cruise","Resort","F&B"],"contact_url":"https://aaltocruisemarinereps.com/contact"},
+ {"id":35,"name":"Augusto Bellucci","agency":"Bellucci Luxury Hotel Reps","territories":["NY","FL","CA"],"lines":["Edelman Leather","Holly Hunt Hospitality","Stark Carpet Hospitality"],"city":"New York","vertical":["Luxury Hotel","Boutique Hotel","Spa"],"contact_url":"https://belluccluxuryhotelreps.com/contact"},
+ {"id":36,"name":"Marisol Tejada","agency":"Tejada Vacation Hospitality","territories":["FL","SC","GA","NC"],"lines":["Bella-Dura","Sunbrella Contract","Crypton"],"city":"Charleston","vertical":["Vacation Rental","Resort","Boutique Hotel"],"contact_url":"https://tejadavacationhospitality.com/contact"},
+ {"id":37,"name":"Niko Stavros","agency":"Stavros Hotel Standards","territories":["CA","NV","AZ"],"lines":["Designtex","Architex","Mayer Fabrics"],"city":"Los Angeles","vertical":["Hotel Brand","Convention","F&B"],"contact_url":"https://stavroshotelstandards.com/contact"},
+ {"id":38,"name":"Wren Ashbury","agency":"Ashbury Hospitality Reps","territories":["OH","PA","WV","KY"],"lines":["Pallas Textiles","Wolf-Gordon","Pollack"],"city":"Cleveland","vertical":["Hotel Brand","Convention","Hospitality Healthcare"],"contact_url":"https://ashburyhospitalityreps.com/contact"},
+ {"id":39,"name":"Cleo Marchand","agency":"Marchand Resort & Spa Reps","territories":["CA","HI","NV"],"lines":["Perennials","Casamance","Romo Black"],"city":"Napa","vertical":["Resort","Spa","Boutique Hotel"],"contact_url":"https://marchandresortspareps.com/contact"},
+ {"id":40,"name":"Bartholomew Quinn","agency":"Quinn Hospitality Alliance","territories":["NY","CT","MA","RI","NJ"],"lines":["Carnegie","Maharam","CF Stinson"],"city":"Stamford","vertical":["Hotel Brand","Convention","F&B"],"contact_url":"https://quinnhospitalityalliance.com/contact"}
+]
diff --git a/package-lock.json b/package-lock.json
new file mode 100644
index 0000000..792dd3b
--- /dev/null
+++ b/package-lock.json
@@ -0,0 +1,1418 @@
+{
+ "name": "hospitalitydesignreps",
+ "version": "0.1.0",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "hospitalitydesignreps",
+ "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..dae905a
--- /dev/null
+++ b/package.json
@@ -0,0 +1,24 @@
+{
+ "name": "hospitalitydesignreps",
+ "version": "0.1.0",
+ "private": true,
+ "description": "Hospitality design sales rep directory — find independent reps for boutique hotels, hotel brands, resorts, F&B, casino, cruise, and spa interior design",
+ "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..d787ef2
--- /dev/null
+++ b/public/css/styles.css
@@ -0,0 +1,196 @@
+:root {
+ --bg: #0c1a26;
+ --bg-elev: #11253a;
+ --bg-card: #142e47;
+ --fg: #e9f2fb;
+ --fg-muted: #8aa3bf;
+ --accent: #e2b85a;
+ --accent-fg: #1a1208;
+ --border: #1e3a55;
+ --tag-bg: #1a3550;
+ --shadow: 0 10px 30px rgba(0,0,0,.45);
+ --radius: 14px;
+ --cols-min: 320px;
+ font-family: ui-sans-serif, -apple-system, BlinkMacSystemFont, "Inter", "Helvetica Neue", Arial, sans-serif;
+}
+[data-theme="light"] {
+ --bg: #f5f1ea;
+ --bg-elev: #ffffff;
+ --bg-card: #ffffff;
+ --fg: #182030;
+ --fg-muted: #5a6a80;
+ --accent: #a07028;
+ --accent-fg: #ffffff;
+ --border: #e3dccf;
+ --tag-bg: #f0e9da;
+ --shadow: 0 6px 22px rgba(40,30,15,.10);
+}
+* { 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), #b6873a);
+}
+.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), #b6873a);
+ 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..f8aac91
--- /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="#e2b85a"/>
+ <stop offset="100%" stop-color="#a07028"/>
+ </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="#1a1208">HR</text>
+</svg>
diff --git a/public/js/main.js b/public/js/main.js
new file mode 100644
index 0000000..4ad964a
--- /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('hdr-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('hdr-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('hdr-density', slider.value); } catch (e) {}
+ });
+ }
+
+ const sortSelect = document.getElementById('sort-select');
+ if (sortSelect) {
+ let savedSort = null;
+ try { savedSort = localStorage.getItem('hdr-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('hdr-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..4734895
--- /dev/null
+++ b/public/robots.txt
@@ -0,0 +1,3 @@
+User-agent: *
+Allow: /
+Sitemap: https://hospitalitydesignreps.com/sitemap.xml
diff --git a/server.js b/server.js
new file mode 100644
index 0000000..350d11e
--- /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 || 9927;
+const GA_ID = process.env.GA_ID || 'G-M79CJBLRFH';
+
+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: 'hospitalitydesignreps.com',
+ name: 'Hospitality Design Reps',
+ tagline: 'Independent sales reps for hotel, resort, F&B, and hospitality interior design',
+ description: 'Find independent design reps and showroom agencies for boutique hotels, hotel brands, resorts, casinos, cruise lines, F&B, and spa interior design across the United States.',
+ email: 'info@hospitalitydesignreps.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(`hospitalitydesignreps 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..acb9e81
--- /dev/null
+++ b/views/about.ejs
@@ -0,0 +1,25 @@
+<%- include('partials/head', { title: 'About — ' + site.name, description: 'Hospitality Design Reps is an industry directory of independent sales reps and showroom agencies serving boutique hotels, hotel brands, resorts, casinos, cruise lines, F&B, and spa interior design.' }) %>
+
+<main class="wrap main">
+ <a class="back-link" href="/">← Back</a>
+
+ <article class="long-form">
+ <h1>About Hospitality Design Reps</h1>
+
+ <p>Hospitality Design Reps is a free, curated directory of independent sales-rep agencies serving the U.S. hospitality interior design industry. The directory exists because hospitality interior designers, FF&E purchasing teams, hotel brand standards groups, and owner-operator project managers routinely struggle to figure out <em>who carries what, where</em> — especially in boutique-hotel, resort, casino, cruise, F&B, and spa work, 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 hospitality segments they actively service (boutique hotel, hotel brand, resort, casino, F&B, cruise, spa, vacation rental, hospitality healthcare), the city the rep is based in, and a direct contact path. Filters above the directory let you narrow by territory, by hospitality segment, 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 with a long hospitality-FF&E history, 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 hospitality 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..7667b54
--- /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>Hospitality design reps, by territory and segment.</h1>
+ <p class="lead">A working directory of independent sales reps, agency principals, and showroom partners covering U.S. boutique hotels, hotel brands, resorts, casinos, cruise lines, F&B, and spa interior design. <%= 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="Segment">
+ <option value="">All segments</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>Hospitality Design Reps is a curated index of independent sales-rep agencies serving the U.S. hospitality interior design industry. Each listing maps the rep's territory coverage, the manufacturer lines they carry, and the hospitality segments (boutique hotel, hotel brand, resort, casino, F&B, cruise, spa, vacation rental, hospitality healthcare) they actively service. Use the filters above to find a rep by region, by line carried, or by hospitality segment. Hospitality interior designers, FF&E purchasing teams, hotel brand standards groups, owner-operators, and project managers 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..ec3ce8e
--- /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="#0c1a26">
+<link rel="icon" href="/favicon.svg" type="image/svg+xml">
+<link rel="stylesheet" href="/css/styles.css">
+<script>
+(function(){
+ try {
+ var t = localStorage.getItem('hdr-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..957f6bd
--- /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 hospitality 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>Hospitality segments</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 segments</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 Hospitalitydesignreps
·
hamburger nav: apply canonical pattern from sister-site flee 442d002 →