← back to Fabricfriday
initial scaffold: fabricfriday.com cinematic saffron site
9f391d63d741a796c0a14b74ece716bafdd64b41 · 2026-05-07 16:41:31 -0700 · Steve Abrams
Files touched
A .gitignoreA data/products.jsonA package-lock.jsonA package.jsonA public/index.htmlA server.js
Diff
commit 9f391d63d741a796c0a14b74ece716bafdd64b41
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Thu May 7 16:41:31 2026 -0700
initial scaffold: fabricfriday.com cinematic saffron site
---
.gitignore | 8 +
data/products.json | 27 ++
package-lock.json | 842 +++++++++++++++++++++++++++++++++++++++++++++++++++++
package.json | 12 +
public/index.html | 499 +++++++++++++++++++++++++++++++
server.js | 37 +++
6 files changed, 1425 insertions(+)
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..fc59ee5
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,8 @@
+node_modules/
+.env*
+tmp/
+*.log
+.DS_Store
+dist/
+build/
+data/subscribers.jsonl
diff --git a/data/products.json b/data/products.json
new file mode 100644
index 0000000..eae6b82
--- /dev/null
+++ b/data/products.json
@@ -0,0 +1,27 @@
+{
+ "hero": {
+ "handle": "dwkk-g60746833",
+ "title": "Entoto Stripe",
+ "subtitle": "Ivory & Flax Beige — Lee Jofa Breckenridge Collection",
+ "vendor": "Lee Jofa",
+ "attribute": "Hand-loomed Ethiopian cotton, 55-inch width",
+ "img_hero": "https://cdn.shopify.com/s/files/1/0015/4117/7456/files/room-hotel_lobby-2017143-116-0.jpg?v=1775081808",
+ "img_room": "https://cdn.shopify.com/s/files/1/0015/4117/7456/files/room-living_room-Entoto-Stripe---Ivory-Flax-Beige-By-Lee.png?v=1775100774",
+ "img_macro": "https://cdn.shopify.com/s/files/1/0015/4117/7456/files/2017143_116_bf8f13ed-4c49-44bf-9dc7-fb8ac9aebb92.jpg?v=1726797466",
+ "img_detail": "https://cdn.shopify.com/s/files/1/0015/4117/7456/files/2017143_116_bf8f13ed-4c49-44bf-9dc7-fb8ac9aebb92.jpg?v=1726797466"
+ },
+ "archive": [
+ { "handle": "dwkk-g7057da14", "title": "Brink — Delft Indigo Chenille", "vendor": "Lee Jofa", "img": "https://cdn.shopify.com/s/files/1/0015/4117/7456/files/GWF-3733_151_8c242f7c-5c7d-4f82-878d-09235ca6ae41.jpg?v=1726797466", "week": "2026-02-06" },
+ { "handle": "dwkk-ga47c675d", "title": "Blyth Weave — Navy Herringbone", "vendor": "Lee Jofa", "img": "https://cdn.shopify.com/s/files/1/0015/4117/7456/files/2020108_50_2afea9fc-ad82-4485-8dce-e1fed6676b2a.jpg?v=1726797466", "week": "2026-02-13" },
+ { "handle": "dwkk-g778c767f", "title": "Seaford Weave — Diamond Ivory", "vendor": "Lee Jofa", "img": "https://cdn.shopify.com/s/files/1/0015/4117/7456/files/2020106_1_04b30aa2-2cba-4f92-94f6-f4d200a15cfe.jpg?v=1726797466", "week": "2026-02-20" },
+ { "handle": "dwkk-g37826aec", "title": "Maldon Weave — Fog Grey Diamond", "vendor": "Lee Jofa", "img": "https://cdn.shopify.com/s/files/1/0015/4117/7456/files/2020102_11_7d6fa794-8480-4b38-8ee5-1b0c8f53b22e.jpg?v=1726797466", "week": "2026-02-27" },
+ { "handle": "dwkk-gdc6143f9", "title": "Bale — Radicchio Pink Bouclé", "vendor": "Lee Jofa", "img": "https://cdn.shopify.com/s/files/1/0015/4117/7456/files/2019155_710_95e89ba7-f723-451b-8611-47aa1d4aa06e.jpg?v=1726797466", "week": "2026-03-06" },
+ { "handle": "dwkk-gb60bfc9c", "title": "Levens Velvet — Leaf Green", "vendor": "Lee Jofa", "img": "https://cdn.shopify.com/s/files/1/0015/4117/7456/files/2019118_3_104d33bf-4a11-4708-b90f-0d274e118a5a.jpg?v=1726797466", "week": "2026-03-13" },
+ { "handle": "dwkk-134918", "title": "Gemma Velvet — Pacific Turquoise", "vendor": "Lee Jofa", "img": "https://cdn.shopify.com/s/files/1/0015/4117/7456/files/2018148_135_79ba2f07-1ec7-45f0-841c-87b4c84cc2fd.jpg?v=1726797466", "week": "2026-03-20" },
+ { "handle": "canal-stripe-texture-durable-walls-xwd-52116", "title": "Canal Stripe Texture — Durable Type II", "vendor": "Hollywood Wallcoverings", "img": "https://cdn.shopify.com/s/files/1/0015/4117/7456/files/XWD-52116.jpg?v=1726797466", "week": "2026-03-27" },
+ { "handle": "faux-glass-bead-wallpaper-110-deep-gold-fgb-110", "title": "Faux Glass Bead — Deep Gold", "vendor": "Designer Wallcoverings", "img": "https://cdn.shopify.com/s/files/1/0015/4117/7456/files/FGB-110.jpg?v=1726797466", "week": "2026-04-03" },
+ { "handle": "faux-glass-bead-wallpaper-109-jade-green-fgb-109", "title": "Faux Glass Bead — Jade Green", "vendor": "Designer Wallcoverings", "img": "https://cdn.shopify.com/s/files/1/0015/4117/7456/files/FGB-109.jpg?v=1726797466", "week": "2026-04-10" },
+ { "handle": "frank-s-faux-finish-fff-2934", "title": "Frank's Faux Finish — Artisan Plaster", "vendor": "Hollywood Wallcoverings", "img": "https://cdn.shopify.com/s/files/1/0015/4117/7456/files/FFF-2934.jpg?v=1726797466", "week": "2026-04-17" },
+ { "handle": "calais-red-grain-stripe-wallpaper-cca-83190", "title": "Calais Grain Stripe — Red", "vendor": "Designer Wallcoverings", "img": "https://cdn.shopify.com/s/files/1/0015/4117/7456/files/CCA-83190.jpg?v=1726797466", "week": "2026-04-24" }
+ ]
+}
diff --git a/package-lock.json b/package-lock.json
new file mode 100644
index 0000000..5eb3cae
--- /dev/null
+++ b/package-lock.json
@@ -0,0 +1,842 @@
+{
+ "name": "fabricfriday",
+ "version": "1.0.0",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "fabricfriday",
+ "version": "1.0.0",
+ "dependencies": {
+ "express": "^4.18.2"
+ }
+ },
+ "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/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/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/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/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/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/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/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/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-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/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/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/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.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/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/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.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/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/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/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/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/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/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/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/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/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/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..199e230
--- /dev/null
+++ b/package.json
@@ -0,0 +1,12 @@
+{
+ "name": "fabricfriday",
+ "version": "1.0.0",
+ "description": "Fabric Friday — weekly fabric feature by Designer Wallcoverings",
+ "main": "server.js",
+ "scripts": {
+ "start": "node server.js"
+ },
+ "dependencies": {
+ "express": "^4.18.2"
+ }
+}
diff --git a/public/index.html b/public/index.html
new file mode 100644
index 0000000..22261c8
--- /dev/null
+++ b/public/index.html
@@ -0,0 +1,499 @@
+<!DOCTYPE html>
+<html lang="en" data-theme="dark">
+<head>
+ <meta charset="UTF-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
+ <title>Fabric Friday — Every Week, One Exceptional Fabric | Designer Wallcoverings</title>
+ <meta name="description" content="Free memo samples, trade pricing, and expert hands-on service. Every Friday we spotlight one extraordinary fabric — curated from the world's finest mills by Designer Wallcoverings.">
+ <meta property="og:title" content="Fabric Friday | Designer Wallcoverings">
+ <meta property="og:description" content="One exceptional fabric, every Friday. Free memo samples. Trade service.">
+ <meta property="og:image" content="https://cdn.shopify.com/s/files/1/0015/4117/7456/files/room-hotel_lobby-2017143-116-0.jpg?v=1775081808">
+ <link rel="preconnect" href="https://fonts.googleapis.com">
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
+ <link href="https://fonts.googleapis.com/css2?family=Cormorant+Garamond:ital,wght@0,300;0,400;0,500;0,600;1,300;1,400;1,500&display=swap" rel="stylesheet">
+ <script type="application/ld+json">
+ {
+ "@context": "https://schema.org",
+ "@type": "Organization",
+ "name": "Fabric Friday by Designer Wallcoverings",
+ "url": "https://fabricfriday.com",
+ "email": "info@fabricfriday.com",
+ "sameAs": ["https://designerwallcoverings.com"]
+ }
+ </script>
+ <style>
+ /* ── TOKENS ──────────────────────────────────────── */
+ :root {
+ --bg: #0a0a0a;
+ --ink: #f5f1e8;
+ --ink-soft: rgba(245,241,232,0.70);
+ --ink-faint: rgba(245,241,232,0.35);
+ --line: rgba(245,241,232,0.08);
+ --card-bg: rgba(245,241,232,0.05);
+ --header-bg: rgba(10,10,10,0.92);
+ /* Saffron accent — stays constant across themes */
+ --accent: #e09b3e;
+ --accent-light: #f5c06a;
+ --accent-mid: #e09b3e;
+ --accent-dark: #b37420;
+ --accent-glow: rgba(224,155,62,0.30);
+ }
+ [data-theme="light"] {
+ --bg: #faf8f4;
+ --ink: #1a1714;
+ --ink-soft: rgba(26,23,20,0.70);
+ --ink-faint: rgba(26,23,20,0.40);
+ --line: rgba(26,23,20,0.10);
+ --card-bg: rgba(26,23,20,0.04);
+ --header-bg: rgba(250,248,244,0.92);
+ }
+
+ /* ── RESET ───────────────────────────────────────── */
+ *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
+ html { scroll-behavior: smooth; font-size: 16px; }
+ body {
+ background: var(--bg);
+ color: var(--ink);
+ font-family: -apple-system, "SF Pro Display", system-ui, sans-serif;
+ transition: background 0.3s, color 0.3s;
+ overflow-x: hidden;
+ }
+
+ /* ── HEADER ──────────────────────────────────────── */
+ .header {
+ position: sticky; top: 0; z-index: 100;
+ background: var(--header-bg);
+ backdrop-filter: blur(8px);
+ -webkit-backdrop-filter: blur(8px);
+ border-bottom: 1px solid var(--line);
+ display: flex; align-items: center; justify-content: space-between;
+ padding: 0 2rem; height: 60px;
+ }
+ .header-brand { display: flex; align-items: center; gap: 12px; text-decoration: none; }
+ .brand-dot {
+ width: 28px; height: 28px; border-radius: 50%; flex-shrink: 0;
+ background: radial-gradient(circle at 35% 35%, var(--accent-light) 0%, var(--accent-mid) 60%, var(--accent-dark) 100%);
+ box-shadow: 0 0 0 1px rgba(245,241,232,0.10), 0 2px 12px var(--accent-glow);
+ }
+ .brand-name {
+ font-family: 'Cormorant Garamond', Georgia, serif;
+ font-size: 1.25rem; font-weight: 500; letter-spacing: 0.06em;
+ color: var(--ink); text-transform: uppercase;
+ }
+ .brand-name span { color: var(--accent); }
+ .header-nav { display: flex; align-items: center; gap: 1.5rem; }
+ .header-nav a {
+ color: var(--ink-soft); font-size: 0.78rem; letter-spacing: 0.12em;
+ text-transform: uppercase; text-decoration: none;
+ transition: color 0.2s;
+ }
+ .header-nav a:hover { color: var(--accent); }
+ .theme-toggle {
+ background: none; border: 1px solid var(--line); border-radius: 50%;
+ width: 34px; height: 34px; cursor: pointer;
+ display: flex; align-items: center; justify-content: center;
+ color: var(--ink-soft); transition: border-color 0.2s, color 0.2s;
+ }
+ .theme-toggle:hover { border-color: var(--accent); color: var(--accent); }
+ .theme-toggle svg { width: 16px; height: 16px; }
+
+ /* ── DAY MARKER CHIP ─────────────────────────────── */
+ .day-chip {
+ display: inline-flex; align-items: center; gap: 8px;
+ padding: 6px 14px; border-radius: 999px;
+ background: rgba(224,155,62,0.12);
+ backdrop-filter: blur(12px);
+ border: 1px solid rgba(224,155,62,0.25);
+ font-size: 0.7rem; letter-spacing: 0.18em; text-transform: uppercase;
+ color: var(--accent); font-weight: 600;
+ }
+ .day-chip::before { content: ''; width: 6px; height: 6px; border-radius: 50%; background: var(--accent); }
+
+ /* ── HERO — full-screen scroll-snap ─────────────── */
+ .hero {
+ position: relative; height: 100vh; min-height: 600px;
+ display: flex; flex-direction: column; justify-content: flex-end;
+ overflow: hidden;
+ cursor: none;
+ }
+ .hero-bg {
+ position: absolute; inset: 0;
+ background-size: cover; background-position: center;
+ background-image: url('https://cdn.shopify.com/s/files/1/0015/4117/7456/files/room-hotel_lobby-2017143-116-0.jpg?v=1775081808');
+ transform: scale(1.06);
+ transition: transform 0.1s linear;
+ }
+ .hero-overlay {
+ position: absolute; inset: 0;
+ background: linear-gradient(to top, rgba(10,10,10,0.85) 0%, rgba(10,10,10,0.20) 60%, transparent 100%);
+ }
+ .hero-glass {
+ position: relative; z-index: 2;
+ margin: 0 2rem 3.5rem;
+ padding: 2.5rem 2.5rem 2rem;
+ backdrop-filter: blur(20px) saturate(180%);
+ -webkit-backdrop-filter: blur(20px) saturate(180%);
+ background: rgba(10,10,10,0.55);
+ border: 1px solid rgba(245,241,232,0.10);
+ border-radius: 4px;
+ max-width: 680px;
+ }
+ .hero-eyebrow { margin-bottom: 1rem; }
+ .hero-title {
+ font-family: 'Cormorant Garamond', Georgia, serif;
+ font-size: clamp(2.5rem, 5vw, 4rem); font-weight: 300;
+ line-height: 1.05; letter-spacing: -0.01em;
+ color: var(--ink); margin-bottom: 0.75rem;
+ }
+ .hero-title em { font-style: italic; color: var(--accent); }
+ .hero-sub {
+ font-size: 0.9rem; color: var(--ink-soft); letter-spacing: 0.04em;
+ margin-bottom: 1.5rem; line-height: 1.5;
+ }
+ .hero-cta {
+ display: inline-block;
+ padding: 12px 28px; border-radius: 2px;
+ background: var(--accent); color: #0a0a0a;
+ font-size: 0.78rem; font-weight: 600; letter-spacing: 0.14em; text-transform: uppercase;
+ text-decoration: none; transition: background 0.2s, transform 0.15s;
+ }
+ .hero-cta:hover { background: var(--accent-light); transform: translateY(-1px); }
+ .drag-cursor {
+ position: fixed; width: 64px; height: 64px; border-radius: 50%;
+ background: var(--accent-glow); backdrop-filter: blur(4px);
+ border: 1px solid rgba(224,155,62,0.4);
+ display: flex; align-items: center; justify-content: center;
+ pointer-events: none; z-index: 9999; opacity: 0;
+ transform: translate(-50%, -50%);
+ font-size: 0.6rem; letter-spacing: 0.1em; text-transform: uppercase;
+ color: var(--accent); transition: opacity 0.2s;
+ }
+
+ /* ── SECTION SCAFFOLD ────────────────────────────── */
+ section { padding: 6rem 2rem; max-width: 1200px; margin: 0 auto; }
+ .section-eyebrow {
+ font-size: 0.68rem; letter-spacing: 0.22em; text-transform: uppercase;
+ color: var(--accent); margin-bottom: 1.5rem; display: flex; align-items: center; gap: 12px;
+ }
+ .section-eyebrow::after { content: ''; flex: 1; height: 1px; background: var(--line); }
+ h2.section-title {
+ font-family: 'Cormorant Garamond', Georgia, serif;
+ font-size: clamp(2rem, 4vw, 3rem); font-weight: 300; color: var(--ink);
+ margin-bottom: 2rem;
+ }
+
+ /* ── WHY THIS PICK ───────────────────────────────── */
+ .why-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 4rem; align-items: start; }
+ @media (max-width: 768px) { .why-grid { grid-template-columns: 1fr; } }
+ .why-body p {
+ font-size: 1.05rem; line-height: 1.75; color: var(--ink-soft);
+ margin-bottom: 1.25rem;
+ }
+ .why-pullquote {
+ font-family: 'Cormorant Garamond', Georgia, serif;
+ font-size: clamp(1.6rem, 3vw, 2.25rem); font-weight: 300;
+ font-style: italic; line-height: 1.3; color: var(--ink);
+ border-left: 2px solid var(--accent); padding-left: 1.5rem;
+ margin: 0;
+ }
+
+ /* ── DETAIL SHOTS ────────────────────────────────── */
+ .shots-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 1.5rem; }
+ @media (max-width: 768px) { .shots-grid { grid-template-columns: 1fr; } }
+ .shot-card {
+ border-radius: 3px; overflow: hidden;
+ backdrop-filter: blur(16px);
+ background: var(--card-bg);
+ border: 1px solid var(--line);
+ transition: transform 0.3s, box-shadow 0.3s;
+ }
+ .shot-card:hover { transform: translateY(-4px); box-shadow: 0 16px 40px rgba(0,0,0,0.4); }
+ .shot-card img { width: 100%; aspect-ratio: 3/4; object-fit: cover; display: block; }
+ .shot-label {
+ padding: 1rem 1.25rem;
+ font-size: 0.7rem; letter-spacing: 0.14em; text-transform: uppercase;
+ color: var(--ink-faint);
+ }
+
+ /* ── ARCHIVE ─────────────────────────────────────── */
+ .archive-section { padding: 6rem 0; overflow: hidden; max-width: 100%; }
+ .archive-header { padding: 0 2rem; margin-bottom: 2rem; }
+ .archive-scroll {
+ display: flex; gap: 1.5rem; overflow-x: auto; padding: 1rem 2rem 2rem;
+ scrollbar-width: thin; scrollbar-color: var(--accent) transparent;
+ scroll-snap-type: x mandatory;
+ }
+ .archive-scroll::-webkit-scrollbar { height: 3px; }
+ .archive-scroll::-webkit-scrollbar-track { background: transparent; }
+ .archive-scroll::-webkit-scrollbar-thumb { background: var(--accent); border-radius: 2px; }
+ .archive-card {
+ flex: 0 0 240px; border-radius: 3px; overflow: hidden;
+ background: var(--card-bg); border: 1px solid var(--line);
+ scroll-snap-align: start;
+ transition: transform 0.25s, border-color 0.25s;
+ cursor: pointer;
+ text-decoration: none;
+ }
+ .archive-card:hover { transform: translateY(-4px); border-color: var(--accent); }
+ .archive-card img { width: 100%; aspect-ratio: 4/5; object-fit: cover; display: block; }
+ .archive-meta { padding: 0.9rem 1rem; }
+ .archive-week { font-size: 0.62rem; letter-spacing: 0.16em; text-transform: uppercase; color: var(--accent); margin-bottom: 4px; }
+ .archive-title { font-family: 'Cormorant Garamond', Georgia, serif; font-size: 0.95rem; color: var(--ink); line-height: 1.35; }
+ .archive-vendor { font-size: 0.68rem; color: var(--ink-faint); margin-top: 4px; }
+
+ /* ── SUBSCRIBE ───────────────────────────────────── */
+ .subscribe-panel {
+ backdrop-filter: blur(16px);
+ background: var(--card-bg);
+ border: 1px solid var(--line);
+ border-radius: 4px;
+ padding: 3.5rem; text-align: center; max-width: 560px; margin: 0 auto;
+ }
+ .subscribe-panel h2 { font-family: 'Cormorant Garamond', Georgia, serif; font-size: 2.25rem; font-weight: 300; margin-bottom: 1rem; }
+ .subscribe-panel p { color: var(--ink-soft); margin-bottom: 2rem; line-height: 1.6; }
+ .subscribe-form { display: flex; gap: 0; max-width: 420px; margin: 0 auto; }
+ .subscribe-form input {
+ flex: 1; padding: 12px 16px;
+ background: rgba(245,241,232,0.06); border: 1px solid var(--line);
+ border-right: none; border-radius: 2px 0 0 2px;
+ color: var(--ink); font-size: 0.9rem; outline: none;
+ transition: border-color 0.2s;
+ }
+ .subscribe-form input::placeholder { color: var(--ink-faint); }
+ .subscribe-form input:focus { border-color: var(--accent); }
+ .subscribe-form button {
+ padding: 12px 24px; background: var(--accent); color: #0a0a0a;
+ border: none; border-radius: 0 2px 2px 0;
+ font-size: 0.75rem; font-weight: 600; letter-spacing: 0.12em; text-transform: uppercase;
+ cursor: pointer; transition: background 0.2s; white-space: nowrap;
+ }
+ .subscribe-form button:hover { background: var(--accent-light); }
+ .subscribe-msg { margin-top: 1rem; font-size: 0.85rem; color: var(--accent); min-height: 1.2em; }
+
+ /* ── FOOTER ──────────────────────────────────────── */
+ footer {
+ border-top: 1px solid var(--line);
+ padding: 3rem 2rem;
+ display: flex; flex-direction: column; align-items: center; gap: 1rem;
+ text-align: center;
+ }
+ .footer-logo { font-family: 'Cormorant Garamond', Georgia, serif; font-size: 1.1rem; letter-spacing: 0.1em; text-transform: uppercase; color: var(--ink-soft); }
+ .footer-logo span { color: var(--accent); }
+ .footer-links { display: flex; gap: 2rem; flex-wrap: wrap; justify-content: center; }
+ .footer-links a { font-size: 0.75rem; letter-spacing: 0.1em; text-transform: uppercase; color: var(--ink-faint); text-decoration: none; transition: color 0.2s; }
+ .footer-links a:hover { color: var(--accent); }
+ footer small { font-size: 0.7rem; color: var(--ink-faint); }
+ </style>
+</head>
+<body>
+ <!-- Anti-flash inline theme script -->
+ <script>
+ (function(){
+ var t = localStorage.getItem('ff-theme') || 'dark';
+ document.documentElement.setAttribute('data-theme', t);
+ })();
+ </script>
+
+ <!-- Drag cursor -->
+ <div class="drag-cursor" id="dragCursor">DRAG</div>
+
+ <!-- Header -->
+ <header class="header">
+ <a href="/" class="header-brand">
+ <div class="brand-dot"></div>
+ <span class="brand-name">Fabric<span>Friday</span></span>
+ </a>
+ <nav class="header-nav">
+ <a href="#picks">Archive</a>
+ <a href="#subscribe">Subscribe</a>
+ <a href="https://designerwallcoverings.com" target="_blank" rel="noopener">Shop DW</a>
+ <button class="theme-toggle" id="themeToggle" aria-label="Toggle dark/light mode">
+ <!-- Sun -->
+ <svg id="iconSun" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.5">
+ <circle cx="12" cy="12" r="4"/><line x1="12" y1="2" x2="12" y2="5"/><line x1="12" y1="19" x2="12" y2="22"/>
+ <line x1="4.22" y1="4.22" x2="6.34" y2="6.34"/><line x1="17.66" y1="17.66" x2="19.78" y2="19.78"/>
+ <line x1="2" y1="12" x2="5" y2="12"/><line x1="19" y1="12" x2="22" y2="12"/>
+ <line x1="4.22" y1="19.78" x2="6.34" y2="17.66"/><line x1="17.66" y1="6.34" x2="19.78" y2="4.22"/>
+ </svg>
+ <!-- Moon -->
+ <svg id="iconMoon" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.5" style="display:none">
+ <path d="M21 12.79A9 9 0 1111.21 3 7 7 0 0021 12.79z"/>
+ </svg>
+ </button>
+ </nav>
+ </header>
+
+ <!-- SECTION 1: HERO -->
+ <section class="hero" id="hero">
+ <div class="hero-bg" id="heroBg"></div>
+ <div class="hero-overlay"></div>
+ <div class="hero-glass">
+ <div class="hero-eyebrow">
+ <span class="day-chip">Friday · 2026-05-08</span>
+ </div>
+ <h1 class="hero-title">This Week's<br><em>Fabric Pick</em></h1>
+ <p class="hero-sub">Entoto Stripe — Ivory & Flax Beige • Lee Jofa Breckenridge • Hand-loomed Ethiopian cotton, 55” wide</p>
+ <a href="https://designerwallcoverings.com/products/dwkk-g60746833#sample" class="hero-cta" target="_blank" rel="noopener">Order Memo Sample (free)</a>
+ </div>
+ </section>
+
+ <!-- SECTION 2: WHY THIS PICK -->
+ <section id="why">
+ <div class="section-eyebrow">Editorial</div>
+ <div class="why-grid">
+ <div class="why-body">
+ <h2 class="section-title">Why This Pick</h2>
+ <p>The Entoto Stripe arrives from Lee Jofa's Breckenridge collection with a story rooted in East African craft tradition. Woven in the highlands outside Addis Ababa, each stripe is the product of a hand-tensioned loom where the ivory warp threads are carefully counted against a warm flax weft — a technique unchanged across three generations of the same weaving family.</p>
+ <p>What sets this fabric apart is the intentional irregularity in the stripe width. At exactly 55 inches, it's built for generous upholstery yardage, yet the optical weight reads far lighter than its cotton-heavy content suggests. The ivory-to-flax color story functions as a genuine neutral: neither too warm for a coastal room nor too cool for a darker library scheme.</p>
+ <p>Designer Wallcoverings carries full bolt and memo sample — one free, shipped within two business days. Trade pricing is available immediately on account application.</p>
+ </div>
+ <blockquote class="why-pullquote">
+ "The best fabrics carry their geography in every thread. This one smells like altitude and morning light."
+ </blockquote>
+ </div>
+ </section>
+
+ <!-- SECTION 3: THREE DETAIL SHOTS -->
+ <section id="shots">
+ <div class="section-eyebrow">Detail</div>
+ <h2 class="section-title">Three Ways of Seeing</h2>
+ <div class="shots-grid">
+ <div class="shot-card">
+ <img src="https://cdn.shopify.com/s/files/1/0015/4117/7456/files/room-hotel_lobby-2017143-116-0.jpg?v=1775081808"
+ alt="Room install — Entoto Stripe in a hotel lobby setting" loading="lazy">
+ <div class="shot-label">Room Install</div>
+ </div>
+ <div class="shot-card">
+ <img src="https://cdn.shopify.com/s/files/1/0015/4117/7456/files/2017143_116_bf8f13ed-4c49-44bf-9dc7-fb8ac9aebb92.jpg?v=1726797466"
+ alt="Macro weave — close-up textile detail of Entoto Stripe" loading="lazy">
+ <div class="shot-label">Macro Weave</div>
+ </div>
+ <div class="shot-card">
+ <img src="https://cdn.shopify.com/s/files/1/0015/4117/7456/files/room-living_room-Entoto-Stripe---Ivory-Flax-Beige-By-Lee.png?v=1775100774"
+ alt="Scale detail — Entoto Stripe in a living room context" loading="lazy">
+ <div class="shot-label">Scale Detail</div>
+ </div>
+ </div>
+ </section>
+
+ <!-- SECTION 4: ARCHIVE -->
+ <section class="archive-section" id="picks">
+ <div class="archive-header">
+ <div class="section-eyebrow">Past Picks</div>
+ <h2 class="section-title" style="margin-bottom:0.5rem">Every Friday, One Fabric</h2>
+ <p style="color:var(--ink-faint); font-size:0.85rem; margin-bottom:1rem;">Scroll to browse twelve weeks of previous features.</p>
+ </div>
+ <div class="archive-scroll" id="archiveScroll"></div>
+ </section>
+
+ <!-- SECTION 5: SUBSCRIBE -->
+ <section id="subscribe">
+ <div class="section-eyebrow">Never Miss a Friday</div>
+ <div class="subscribe-panel">
+ <h2>The Friday Fabric</h2>
+ <p>One email. One exceptional fabric. Every Friday morning. Free memo sample always available for every pick.</p>
+ <form class="subscribe-form" id="subForm">
+ <input type="email" id="subEmail" placeholder="your@email.com" autocomplete="email" required>
+ <button type="submit">Subscribe</button>
+ </form>
+ <div class="subscribe-msg" id="subMsg"></div>
+ </div>
+ </section>
+
+ <!-- FOOTER -->
+ <footer>
+ <div class="footer-logo">Fabric<span>Friday</span> · by Designer Wallcoverings</div>
+ <nav class="footer-links">
+ <a href="https://designerwallcoverings.com" target="_blank" rel="noopener">Shop DW</a>
+ <a href="https://designerwallcoverings.com/pages/trade" target="_blank" rel="noopener">Trade Program</a>
+ <a href="mailto:info@fabricfriday.com">info@fabricfriday.com</a>
+ <a href="#subscribe">Subscribe</a>
+ </nav>
+ <small>© 2026 Designer Wallcoverings — fabricfriday.com — All images © respective vendors via DW catalog</small>
+ </footer>
+
+ <script>
+ // ── Theme toggle ──────────────────────────────────
+ const root = document.documentElement;
+ const btn = document.getElementById('themeToggle');
+ const sun = document.getElementById('iconSun');
+ const moon = document.getElementById('iconMoon');
+ function applyTheme(t) {
+ root.setAttribute('data-theme', t);
+ localStorage.setItem('ff-theme', t);
+ if (t === 'dark') { sun.style.display = ''; moon.style.display = 'none'; }
+ else { sun.style.display = 'none'; moon.style.display = ''; }
+ }
+ applyTheme(root.getAttribute('data-theme') || 'dark');
+ btn.addEventListener('click', () => applyTheme(root.getAttribute('data-theme') === 'dark' ? 'light' : 'dark'));
+
+ // ── Parallax drag cursor ──────────────────────────
+ const cursor = document.getElementById('dragCursor');
+ const hero = document.getElementById('hero');
+ const heroBg = document.getElementById('heroBg');
+ let mouseX = 0, mouseY = 0;
+ hero.addEventListener('mouseenter', () => cursor.style.opacity = '1');
+ hero.addEventListener('mouseleave', () => cursor.style.opacity = '0');
+ document.addEventListener('mousemove', (e) => {
+ mouseX = e.clientX; mouseY = e.clientY;
+ cursor.style.left = mouseX + 'px';
+ cursor.style.top = mouseY + 'px';
+ // Parallax: move bg slightly opposite to cursor
+ const rect = hero.getBoundingClientRect();
+ if (rect.height > 0) {
+ const rx = (e.clientX - rect.left) / rect.width - 0.5;
+ const ry = (e.clientY - rect.top) / rect.height - 0.5;
+ heroBg.style.transform = `scale(1.06) translate(${rx * -18}px, ${ry * -12}px)`;
+ }
+ });
+
+ // ── Archive ───────────────────────────────────────
+ const archive = [
+ { handle: "dwkk-g7057da14", title: "Brink — Delft Indigo Chenille", vendor: "Lee Jofa", img: "https://cdn.shopify.com/s/files/1/0015/4117/7456/files/GWF-3733_151_8c242f7c-5c7d-4f82-878d-09235ca6ae41.jpg?v=1726797466", week: "Feb 6" },
+ { handle: "dwkk-ga47c675d", title: "Blyth Weave — Navy Herringbone", vendor: "Lee Jofa", img: "https://cdn.shopify.com/s/files/1/0015/4117/7456/files/2020108_50_2afea9fc-ad82-4485-8dce-e1fed6676b2a.jpg?v=1726797466", week: "Feb 13" },
+ { handle: "dwkk-g778c767f", title: "Seaford Weave — Diamond Ivory", vendor: "Lee Jofa", img: "https://cdn.shopify.com/s/files/1/0015/4117/7456/files/2020106_1_04b30aa2-2cba-4f92-94f6-f4d200a15cfe.jpg?v=1726797466", week: "Feb 20" },
+ { handle: "dwkk-g37826aec", title: "Maldon Weave — Fog Grey", vendor: "Lee Jofa", img: "https://cdn.shopify.com/s/files/1/0015/4117/7456/files/2020102_11_7d6fa794-8480-4b38-8ee5-1b0c8f53b22e.jpg?v=1726797466", week: "Feb 27" },
+ { handle: "dwkk-gdc6143f9", title: "Bale — Radicchio Bouclé", vendor: "Lee Jofa", img: "https://cdn.shopify.com/s/files/1/0015/4117/7456/files/2019155_710_95e89ba7-f723-451b-8611-47aa1d4aa06e.jpg?v=1726797466", week: "Mar 6" },
+ { handle: "dwkk-gb60bfc9c", title: "Levens Velvet — Leaf Green", vendor: "Lee Jofa", img: "https://cdn.shopify.com/s/files/1/0015/4117/7456/files/2019118_3_104d33bf-4a11-4708-b90f-0d274e118a5a.jpg?v=1726797466", week: "Mar 13" },
+ { handle: "dwkk-134918", title: "Gemma Velvet — Pacific Turquoise", vendor: "Lee Jofa", img: "https://cdn.shopify.com/s/files/1/0015/4117/7456/files/2018148_135_79ba2f07-1ec7-45f0-841c-87b4c84cc2fd.jpg?v=1726797466", week: "Mar 20" },
+ { handle: "dwkk-g0933c476", title: "Boutique Floral — Delft Blue Linen", vendor: "Lee Jofa", img: "https://cdn.shopify.com/s/files/1/0015/4117/7456/files/2022101_5_060796c6-a8c9-450d-a5b7-c0e35f41109a.jpg?v=1726530286", week: "Mar 27" },
+ { handle: "dwkk-ge157df51", title: "Marion Print — Indigo Botanical", vendor: "Lee Jofa", img: "https://cdn.shopify.com/s/files/1/0015/4117/7456/files/2020205_50_a98ae134-3d4e-44a9-91a1-1e0e93c34ae7.jpg?v=1726797466", week: "Apr 3" },
+ { handle: "dwkk-g21eeb3da", title: "Chinese Lantern — Coral Print", vendor: "Lee Jofa", img: "https://cdn.shopify.com/s/files/1/0015/4117/7456/files/2006101_517_86aa6f6e-b809-41ef-903c-7aa4fd878ef5.jpg?v=1726797466", week: "Apr 10" },
+ { handle: "dwkk-140832", title: "Danakil Tape — Red/Blue Multi", vendor: "Lee Jofa", img: "https://cdn.shopify.com/s/files/1/0015/4117/7456/files/TL10169_965_380212c8-6464-4a24-9792-eae1fc082a0c.jpg?v=1726797466", week: "Apr 17" },
+ { handle: "dwkk-140830", title: "Danakil Tape — Blue Slate Braid", vendor: "Lee Jofa", img: "https://cdn.shopify.com/s/files/1/0015/4117/7456/files/TL10169_515_db906396-623b-40d9-90e7-6d6e4ebad975.jpg?v=1726797466", week: "Apr 24" }
+ ];
+ const scroll = document.getElementById('archiveScroll');
+ archive.forEach(p => {
+ const a = document.createElement('a');
+ a.href = `https://designerwallcoverings.com/products/${p.handle}#sample`;
+ a.target = '_blank'; a.rel = 'noopener';
+ a.className = 'archive-card';
+ a.innerHTML = `
+ <img src="${p.img}" alt="${p.title}" loading="lazy" onerror="this.style.background='rgba(224,155,62,0.08)';this.style.minHeight='200px'">
+ <div class="archive-meta">
+ <div class="archive-week">${p.week}</div>
+ <div class="archive-title">${p.title}</div>
+ <div class="archive-vendor">${p.vendor}</div>
+ </div>`;
+ scroll.appendChild(a);
+ });
+
+ // ── Subscribe form ────────────────────────────────
+ document.getElementById('subForm').addEventListener('submit', async (e) => {
+ e.preventDefault();
+ const email = document.getElementById('subEmail').value;
+ const msg = document.getElementById('subMsg');
+ try {
+ const r = await fetch('/api/subscribe', {
+ method: 'POST', headers: {'Content-Type':'application/json'},
+ body: JSON.stringify({ email })
+ });
+ const d = await r.json();
+ msg.textContent = d.message || d.error || 'Done.';
+ } catch(err) {
+ msg.textContent = 'Something went wrong. Try again.';
+ }
+ });
+ </script>
+</body>
+</html>
diff --git a/server.js b/server.js
new file mode 100644
index 0000000..15a6458
--- /dev/null
+++ b/server.js
@@ -0,0 +1,37 @@
+const express = require('express');
+const path = require('path');
+const fs = require('fs');
+
+const app = express();
+const PORT = process.env.PORT || 9898;
+
+app.use(express.json());
+app.use(express.static(path.join(__dirname, 'public')));
+
+const products = JSON.parse(fs.readFileSync(path.join(__dirname, 'data/products.json'), 'utf8'));
+
+app.get('/', (req, res) => {
+ res.sendFile(path.join(__dirname, 'public/index.html'));
+});
+
+app.get('/health', (req, res) => {
+ res.json({ status: 'ok', site: 'fabricfriday.com', port: PORT });
+});
+
+app.get('/api/products', (req, res) => {
+ res.json(products);
+});
+
+app.post('/api/subscribe', (req, res) => {
+ const { email } = req.body;
+ if (!email || !email.includes('@')) {
+ return res.status(400).json({ error: 'Valid email required' });
+ }
+ const entry = JSON.stringify({ email, site: 'fabricfriday.com', ts: new Date().toISOString() }) + '\n';
+ fs.appendFileSync(path.join(__dirname, 'data/subscribers.jsonl'), entry);
+ res.json({ ok: true, message: 'You\'re on the list. Every Friday, one exceptional fabric.' });
+});
+
+app.listen(PORT, () => {
+ console.log(`fabricfriday.com running on port ${PORT}`);
+});
(oldest)
·
back to Fabricfriday
·
snapshot before hamburger nav rollout e860975 →