This commit is contained in:
Dominic Ferrando
2025-11-04 21:41:50 -05:00
parent 15aa6e44f0
commit b1d95721d8
27 changed files with 7204 additions and 6811 deletions
+3
View File
@@ -0,0 +1,3 @@
{
"tabWidth": 4
}
View File
+39 -49
View File
@@ -6,16 +6,16 @@
"@elysiajs/cors": "^1.4.0", "@elysiajs/cors": "^1.4.0",
"@elysiajs/eden": "^1.4.4", "@elysiajs/eden": "^1.4.4",
"@tailwindcss/vite": "^4.1.14", "@tailwindcss/vite": "^4.1.14",
"daisyui": "^5.3.2", "daisyui": "^5.3.7",
"elysia": "^1.4.12", "elysia": "^1.4.12",
"memoirist": "^0.4.0", "memoirist": "^0.4.0",
"tailwindcss": "^4.1.14", "tailwindcss": "^4.1.14",
}, },
"devDependencies": { "devDependencies": {
"@sveltejs/kit": "^2.46.5", "@sveltejs/kit": "^2.47.1",
"@sveltejs/vite-plugin-svelte": "^6.2.1", "@sveltejs/vite-plugin-svelte": "^6.2.1",
"@types/bun": "^1.3.0", "@types/bun": "^1.3.0",
"svelte": "^5.40.0", "svelte": "^5.40.2",
"svelte-adapter-bun": "^0.5.2", "svelte-adapter-bun": "^0.5.2",
"svelte-check": "^4.3.3", "svelte-check": "^4.3.3",
"typescript": "^5.9.3", "typescript": "^5.9.3",
@@ -82,8 +82,6 @@
"@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.11", "", { "os": "win32", "cpu": "x64" }, "sha512-D7Hpz6A2L4hzsRpPaCYkQnGOotdUpDzSGRIv9I+1ITdHROSFUWW95ZPZWQmGka1Fg7W3zFJowyn9WGwMJ0+KPA=="], "@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.11", "", { "os": "win32", "cpu": "x64" }, "sha512-D7Hpz6A2L4hzsRpPaCYkQnGOotdUpDzSGRIv9I+1ITdHROSFUWW95ZPZWQmGka1Fg7W3zFJowyn9WGwMJ0+KPA=="],
"@isaacs/fs-minipass": ["@isaacs/fs-minipass@4.0.1", "", { "dependencies": { "minipass": "^7.0.4" } }, "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w=="],
"@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="], "@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="],
"@jridgewell/remapping": ["@jridgewell/remapping@2.3.5", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ=="], "@jridgewell/remapping": ["@jridgewell/remapping@2.3.5", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ=="],
@@ -146,47 +144,47 @@
"@sveltejs/acorn-typescript": ["@sveltejs/acorn-typescript@1.0.6", "", { "peerDependencies": { "acorn": "^8.9.0" } }, "sha512-4awhxtMh4cx9blePWl10HRHj8Iivtqj+2QdDCSMDzxG+XKa9+VCNupQuCuvzEhYPzZSrX+0gC+0lHA/0fFKKQQ=="], "@sveltejs/acorn-typescript": ["@sveltejs/acorn-typescript@1.0.6", "", { "peerDependencies": { "acorn": "^8.9.0" } }, "sha512-4awhxtMh4cx9blePWl10HRHj8Iivtqj+2QdDCSMDzxG+XKa9+VCNupQuCuvzEhYPzZSrX+0gC+0lHA/0fFKKQQ=="],
"@sveltejs/kit": ["@sveltejs/kit@2.47.1", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "@sveltejs/acorn-typescript": "^1.0.5", "@types/cookie": "^0.6.0", "acorn": "^8.14.1", "cookie": "^0.6.0", "devalue": "^5.3.2", "esm-env": "^1.2.2", "kleur": "^4.1.5", "magic-string": "^0.30.5", "mrmime": "^2.0.0", "sade": "^1.8.1", "set-cookie-parser": "^2.6.0", "sirv": "^3.0.0" }, "peerDependencies": { "@opentelemetry/api": "^1.0.0", "@sveltejs/vite-plugin-svelte": "^3.0.0 || ^4.0.0-next.1 || ^5.0.0 || ^6.0.0-next.0", "svelte": "^4.0.0 || ^5.0.0-next.0", "vite": "^5.0.3 || ^6.0.0 || ^7.0.0-beta.0" }, "optionalPeers": ["@opentelemetry/api"], "bin": { "svelte-kit": "svelte-kit.js" } }, "sha512-1v+MbMHxTi6ctQyxmz3owLKqZGaBHyx4EQqTdq/PvDswPFzw3WlqhrOKOh2ZzH23+XpQGEF9G+KDIgYJE+byvg=="], "@sveltejs/kit": ["@sveltejs/kit@2.48.4", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "@sveltejs/acorn-typescript": "^1.0.5", "@types/cookie": "^0.6.0", "acorn": "^8.14.1", "cookie": "^0.6.0", "devalue": "^5.3.2", "esm-env": "^1.2.2", "kleur": "^4.1.5", "magic-string": "^0.30.5", "mrmime": "^2.0.0", "sade": "^1.8.1", "set-cookie-parser": "^2.6.0", "sirv": "^3.0.0" }, "peerDependencies": { "@opentelemetry/api": "^1.0.0", "@sveltejs/vite-plugin-svelte": "^3.0.0 || ^4.0.0-next.1 || ^5.0.0 || ^6.0.0-next.0", "svelte": "^4.0.0 || ^5.0.0-next.0", "vite": "^5.0.3 || ^6.0.0 || ^7.0.0-beta.0" }, "optionalPeers": ["@opentelemetry/api"], "bin": { "svelte-kit": "svelte-kit.js" } }, "sha512-TGFX1pZUt9qqY20Cv5NyYvy0iLWHf2jXi8s+eCGsig7jQMdwZWKUFMR6TbvFNhfDSUpc1sH/Y5EHv20g3HHA3g=="],
"@sveltejs/vite-plugin-svelte": ["@sveltejs/vite-plugin-svelte@6.2.1", "", { "dependencies": { "@sveltejs/vite-plugin-svelte-inspector": "^5.0.0", "debug": "^4.4.1", "deepmerge": "^4.3.1", "magic-string": "^0.30.17", "vitefu": "^1.1.1" }, "peerDependencies": { "svelte": "^5.0.0", "vite": "^6.3.0 || ^7.0.0" } }, "sha512-YZs/OSKOQAQCnJvM/P+F1URotNnYNeU3P2s4oIpzm1uFaqUEqRxUB0g5ejMjEb5Gjb9/PiBI5Ktrq4rUUF8UVQ=="], "@sveltejs/vite-plugin-svelte": ["@sveltejs/vite-plugin-svelte@6.2.1", "", { "dependencies": { "@sveltejs/vite-plugin-svelte-inspector": "^5.0.0", "debug": "^4.4.1", "deepmerge": "^4.3.1", "magic-string": "^0.30.17", "vitefu": "^1.1.1" }, "peerDependencies": { "svelte": "^5.0.0", "vite": "^6.3.0 || ^7.0.0" } }, "sha512-YZs/OSKOQAQCnJvM/P+F1URotNnYNeU3P2s4oIpzm1uFaqUEqRxUB0g5ejMjEb5Gjb9/PiBI5Ktrq4rUUF8UVQ=="],
"@sveltejs/vite-plugin-svelte-inspector": ["@sveltejs/vite-plugin-svelte-inspector@5.0.1", "", { "dependencies": { "debug": "^4.4.1" }, "peerDependencies": { "@sveltejs/vite-plugin-svelte": "^6.0.0-next.0", "svelte": "^5.0.0", "vite": "^6.3.0 || ^7.0.0" } }, "sha512-ubWshlMk4bc8mkwWbg6vNvCeT7lGQojE3ijDh3QTR6Zr/R+GXxsGbyH4PExEPpiFmqPhYiVSVmHBjUcVc1JIrA=="], "@sveltejs/vite-plugin-svelte-inspector": ["@sveltejs/vite-plugin-svelte-inspector@5.0.1", "", { "dependencies": { "debug": "^4.4.1" }, "peerDependencies": { "@sveltejs/vite-plugin-svelte": "^6.0.0-next.0", "svelte": "^5.0.0", "vite": "^6.3.0 || ^7.0.0" } }, "sha512-ubWshlMk4bc8mkwWbg6vNvCeT7lGQojE3ijDh3QTR6Zr/R+GXxsGbyH4PExEPpiFmqPhYiVSVmHBjUcVc1JIrA=="],
"@tailwindcss/node": ["@tailwindcss/node@4.1.14", "", { "dependencies": { "@jridgewell/remapping": "^2.3.4", "enhanced-resolve": "^5.18.3", "jiti": "^2.6.0", "lightningcss": "1.30.1", "magic-string": "^0.30.19", "source-map-js": "^1.2.1", "tailwindcss": "4.1.14" } }, "sha512-hpz+8vFk3Ic2xssIA3e01R6jkmsAhvkQdXlEbRTk6S10xDAtiQiM3FyvZVGsucefq764euO/b8WUW9ysLdThHw=="], "@tailwindcss/node": ["@tailwindcss/node@4.1.16", "", { "dependencies": { "@jridgewell/remapping": "^2.3.4", "enhanced-resolve": "^5.18.3", "jiti": "^2.6.1", "lightningcss": "1.30.2", "magic-string": "^0.30.19", "source-map-js": "^1.2.1", "tailwindcss": "4.1.16" } }, "sha512-BX5iaSsloNuvKNHRN3k2RcCuTEgASTo77mofW0vmeHkfrDWaoFAFvNHpEgtu0eqyypcyiBkDWzSMxJhp3AUVcw=="],
"@tailwindcss/oxide": ["@tailwindcss/oxide@4.1.14", "", { "dependencies": { "detect-libc": "^2.0.4", "tar": "^7.5.1" }, "optionalDependencies": { "@tailwindcss/oxide-android-arm64": "4.1.14", "@tailwindcss/oxide-darwin-arm64": "4.1.14", "@tailwindcss/oxide-darwin-x64": "4.1.14", "@tailwindcss/oxide-freebsd-x64": "4.1.14", "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.14", "@tailwindcss/oxide-linux-arm64-gnu": "4.1.14", "@tailwindcss/oxide-linux-arm64-musl": "4.1.14", "@tailwindcss/oxide-linux-x64-gnu": "4.1.14", "@tailwindcss/oxide-linux-x64-musl": "4.1.14", "@tailwindcss/oxide-wasm32-wasi": "4.1.14", "@tailwindcss/oxide-win32-arm64-msvc": "4.1.14", "@tailwindcss/oxide-win32-x64-msvc": "4.1.14" } }, "sha512-23yx+VUbBwCg2x5XWdB8+1lkPajzLmALEfMb51zZUBYaYVPDQvBSD/WYDqiVyBIo2BZFa3yw1Rpy3G2Jp+K0dw=="], "@tailwindcss/oxide": ["@tailwindcss/oxide@4.1.16", "", { "optionalDependencies": { "@tailwindcss/oxide-android-arm64": "4.1.16", "@tailwindcss/oxide-darwin-arm64": "4.1.16", "@tailwindcss/oxide-darwin-x64": "4.1.16", "@tailwindcss/oxide-freebsd-x64": "4.1.16", "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.16", "@tailwindcss/oxide-linux-arm64-gnu": "4.1.16", "@tailwindcss/oxide-linux-arm64-musl": "4.1.16", "@tailwindcss/oxide-linux-x64-gnu": "4.1.16", "@tailwindcss/oxide-linux-x64-musl": "4.1.16", "@tailwindcss/oxide-wasm32-wasi": "4.1.16", "@tailwindcss/oxide-win32-arm64-msvc": "4.1.16", "@tailwindcss/oxide-win32-x64-msvc": "4.1.16" } }, "sha512-2OSv52FRuhdlgyOQqgtQHuCgXnS8nFSYRp2tJ+4WZXKgTxqPy7SMSls8c3mPT5pkZ17SBToGM5LHEJBO7miEdg=="],
"@tailwindcss/oxide-android-arm64": ["@tailwindcss/oxide-android-arm64@4.1.14", "", { "os": "android", "cpu": "arm64" }, "sha512-a94ifZrGwMvbdeAxWoSuGcIl6/DOP5cdxagid7xJv6bwFp3oebp7y2ImYsnZBMTwjn5Ev5xESvS3FFYUGgPODQ=="], "@tailwindcss/oxide-android-arm64": ["@tailwindcss/oxide-android-arm64@4.1.16", "", { "os": "android", "cpu": "arm64" }, "sha512-8+ctzkjHgwDJ5caq9IqRSgsP70xhdhJvm+oueS/yhD5ixLhqTw9fSL1OurzMUhBwE5zK26FXLCz2f/RtkISqHA=="],
"@tailwindcss/oxide-darwin-arm64": ["@tailwindcss/oxide-darwin-arm64@4.1.14", "", { "os": "darwin", "cpu": "arm64" }, "sha512-HkFP/CqfSh09xCnrPJA7jud7hij5ahKyWomrC3oiO2U9i0UjP17o9pJbxUN0IJ471GTQQmzwhp0DEcpbp4MZTA=="], "@tailwindcss/oxide-darwin-arm64": ["@tailwindcss/oxide-darwin-arm64@4.1.16", "", { "os": "darwin", "cpu": "arm64" }, "sha512-C3oZy5042v2FOALBZtY0JTDnGNdS6w7DxL/odvSny17ORUnaRKhyTse8xYi3yKGyfnTUOdavRCdmc8QqJYwFKA=="],
"@tailwindcss/oxide-darwin-x64": ["@tailwindcss/oxide-darwin-x64@4.1.14", "", { "os": "darwin", "cpu": "x64" }, "sha512-eVNaWmCgdLf5iv6Qd3s7JI5SEFBFRtfm6W0mphJYXgvnDEAZ5sZzqmI06bK6xo0IErDHdTA5/t7d4eTfWbWOFw=="], "@tailwindcss/oxide-darwin-x64": ["@tailwindcss/oxide-darwin-x64@4.1.16", "", { "os": "darwin", "cpu": "x64" }, "sha512-vjrl/1Ub9+JwU6BP0emgipGjowzYZMjbWCDqwA2Z4vCa+HBSpP4v6U2ddejcHsolsYxwL5r4bPNoamlV0xDdLg=="],
"@tailwindcss/oxide-freebsd-x64": ["@tailwindcss/oxide-freebsd-x64@4.1.14", "", { "os": "freebsd", "cpu": "x64" }, "sha512-QWLoRXNikEuqtNb0dhQN6wsSVVjX6dmUFzuuiL09ZeXju25dsei2uIPl71y2Ic6QbNBsB4scwBoFnlBfabHkEw=="], "@tailwindcss/oxide-freebsd-x64": ["@tailwindcss/oxide-freebsd-x64@4.1.16", "", { "os": "freebsd", "cpu": "x64" }, "sha512-TSMpPYpQLm+aR1wW5rKuUuEruc/oOX3C7H0BTnPDn7W/eMw8W+MRMpiypKMkXZfwH8wqPIRKppuZoedTtNj2tg=="],
"@tailwindcss/oxide-linux-arm-gnueabihf": ["@tailwindcss/oxide-linux-arm-gnueabihf@4.1.14", "", { "os": "linux", "cpu": "arm" }, "sha512-VB4gjQni9+F0VCASU+L8zSIyjrLLsy03sjcR3bM0V2g4SNamo0FakZFKyUQ96ZVwGK4CaJsc9zd/obQy74o0Fw=="], "@tailwindcss/oxide-linux-arm-gnueabihf": ["@tailwindcss/oxide-linux-arm-gnueabihf@4.1.16", "", { "os": "linux", "cpu": "arm" }, "sha512-p0GGfRg/w0sdsFKBjMYvvKIiKy/LNWLWgV/plR4lUgrsxFAoQBFrXkZ4C0w8IOXfslB9vHK/JGASWD2IefIpvw=="],
"@tailwindcss/oxide-linux-arm64-gnu": ["@tailwindcss/oxide-linux-arm64-gnu@4.1.14", "", { "os": "linux", "cpu": "arm64" }, "sha512-qaEy0dIZ6d9vyLnmeg24yzA8XuEAD9WjpM5nIM1sUgQ/Zv7cVkharPDQcmm/t/TvXoKo/0knI3me3AGfdx6w1w=="], "@tailwindcss/oxide-linux-arm64-gnu": ["@tailwindcss/oxide-linux-arm64-gnu@4.1.16", "", { "os": "linux", "cpu": "arm64" }, "sha512-DoixyMmTNO19rwRPdqviTrG1rYzpxgyYJl8RgQvdAQUzxC1ToLRqtNJpU/ATURSKgIg6uerPw2feW0aS8SNr/w=="],
"@tailwindcss/oxide-linux-arm64-musl": ["@tailwindcss/oxide-linux-arm64-musl@4.1.14", "", { "os": "linux", "cpu": "arm64" }, "sha512-ISZjT44s59O8xKsPEIesiIydMG/sCXoMBCqsphDm/WcbnuWLxxb+GcvSIIA5NjUw6F8Tex7s5/LM2yDy8RqYBQ=="], "@tailwindcss/oxide-linux-arm64-musl": ["@tailwindcss/oxide-linux-arm64-musl@4.1.16", "", { "os": "linux", "cpu": "arm64" }, "sha512-H81UXMa9hJhWhaAUca6bU2wm5RRFpuHImrwXBUvPbYb+3jo32I9VIwpOX6hms0fPmA6f2pGVlybO6qU8pF4fzQ=="],
"@tailwindcss/oxide-linux-x64-gnu": ["@tailwindcss/oxide-linux-x64-gnu@4.1.14", "", { "os": "linux", "cpu": "x64" }, "sha512-02c6JhLPJj10L2caH4U0zF8Hji4dOeahmuMl23stk0MU1wfd1OraE7rOloidSF8W5JTHkFdVo/O7uRUJJnUAJg=="], "@tailwindcss/oxide-linux-x64-gnu": ["@tailwindcss/oxide-linux-x64-gnu@4.1.16", "", { "os": "linux", "cpu": "x64" }, "sha512-ZGHQxDtFC2/ruo7t99Qo2TTIvOERULPl5l0K1g0oK6b5PGqjYMga+FcY1wIUnrUxY56h28FxybtDEla+ICOyew=="],
"@tailwindcss/oxide-linux-x64-musl": ["@tailwindcss/oxide-linux-x64-musl@4.1.14", "", { "os": "linux", "cpu": "x64" }, "sha512-TNGeLiN1XS66kQhxHG/7wMeQDOoL0S33x9BgmydbrWAb9Qw0KYdd8o1ifx4HOGDWhVmJ+Ul+JQ7lyknQFilO3Q=="], "@tailwindcss/oxide-linux-x64-musl": ["@tailwindcss/oxide-linux-x64-musl@4.1.16", "", { "os": "linux", "cpu": "x64" }, "sha512-Oi1tAaa0rcKf1Og9MzKeINZzMLPbhxvm7rno5/zuP1WYmpiG0bEHq4AcRUiG2165/WUzvxkW4XDYCscZWbTLZw=="],
"@tailwindcss/oxide-wasm32-wasi": ["@tailwindcss/oxide-wasm32-wasi@4.1.14", "", { "dependencies": { "@emnapi/core": "^1.5.0", "@emnapi/runtime": "^1.5.0", "@emnapi/wasi-threads": "^1.1.0", "@napi-rs/wasm-runtime": "^1.0.5", "@tybys/wasm-util": "^0.10.1", "tslib": "^2.4.0" }, "cpu": "none" }, "sha512-uZYAsaW/jS/IYkd6EWPJKW/NlPNSkWkBlaeVBi/WsFQNP05/bzkebUL8FH1pdsqx4f2fH/bWFcUABOM9nfiJkQ=="], "@tailwindcss/oxide-wasm32-wasi": ["@tailwindcss/oxide-wasm32-wasi@4.1.16", "", { "dependencies": { "@emnapi/core": "^1.5.0", "@emnapi/runtime": "^1.5.0", "@emnapi/wasi-threads": "^1.1.0", "@napi-rs/wasm-runtime": "^1.0.7", "@tybys/wasm-util": "^0.10.1", "tslib": "^2.4.0" }, "cpu": "none" }, "sha512-B01u/b8LteGRwucIBmCQ07FVXLzImWESAIMcUU6nvFt/tYsQ6IHz8DmZ5KtvmwxD+iTYBtM1xwoGXswnlu9v0Q=="],
"@tailwindcss/oxide-win32-arm64-msvc": ["@tailwindcss/oxide-win32-arm64-msvc@4.1.14", "", { "os": "win32", "cpu": "arm64" }, "sha512-Az0RnnkcvRqsuoLH2Z4n3JfAef0wElgzHD5Aky/e+0tBUxUhIeIqFBTMNQvmMRSP15fWwmvjBxZ3Q8RhsDnxAA=="], "@tailwindcss/oxide-win32-arm64-msvc": ["@tailwindcss/oxide-win32-arm64-msvc@4.1.16", "", { "os": "win32", "cpu": "arm64" }, "sha512-zX+Q8sSkGj6HKRTMJXuPvOcP8XfYON24zJBRPlszcH1Np7xuHXhWn8qfFjIujVzvH3BHU+16jBXwgpl20i+v9A=="],
"@tailwindcss/oxide-win32-x64-msvc": ["@tailwindcss/oxide-win32-x64-msvc@4.1.14", "", { "os": "win32", "cpu": "x64" }, "sha512-ttblVGHgf68kEE4om1n/n44I0yGPkCPbLsqzjvybhpwa6mKKtgFfAzy6btc3HRmuW7nHe0OOrSeNP9sQmmH9XA=="], "@tailwindcss/oxide-win32-x64-msvc": ["@tailwindcss/oxide-win32-x64-msvc@4.1.16", "", { "os": "win32", "cpu": "x64" }, "sha512-m5dDFJUEejbFqP+UXVstd4W/wnxA4F61q8SoL+mqTypId2T2ZpuxosNSgowiCnLp2+Z+rivdU0AqpfgiD7yCBg=="],
"@tailwindcss/vite": ["@tailwindcss/vite@4.1.14", "", { "dependencies": { "@tailwindcss/node": "4.1.14", "@tailwindcss/oxide": "4.1.14", "tailwindcss": "4.1.14" }, "peerDependencies": { "vite": "^5.2.0 || ^6 || ^7" } }, "sha512-BoFUoU0XqgCUS1UXWhmDJroKKhNXeDzD7/XwabjkDIAbMnc4ULn5e2FuEuBbhZ6ENZoSYzKlzvZ44Yr6EUDUSA=="], "@tailwindcss/vite": ["@tailwindcss/vite@4.1.16", "", { "dependencies": { "@tailwindcss/node": "4.1.16", "@tailwindcss/oxide": "4.1.16", "tailwindcss": "4.1.16" }, "peerDependencies": { "vite": "^5.2.0 || ^6 || ^7" } }, "sha512-bbguNBcDxsRmi9nnlWJxhfDWamY3lmcyACHcdO1crxfzuLpOhHLLtEIN/nCbbAtj5rchUgQD17QVAKi1f7IsKg=="],
"@tokenizer/inflate": ["@tokenizer/inflate@0.2.7", "", { "dependencies": { "debug": "^4.4.0", "fflate": "^0.8.2", "token-types": "^6.0.0" } }, "sha512-MADQgmZT1eKjp06jpI2yozxaU9uVs4GzzgSL+uEq7bVcJ9V1ZXQkeGNql1fsSI0gMy1vhvNTNbUqrx+pZfJVmg=="], "@tokenizer/inflate": ["@tokenizer/inflate@0.2.7", "", { "dependencies": { "debug": "^4.4.0", "fflate": "^0.8.2", "token-types": "^6.0.0" } }, "sha512-MADQgmZT1eKjp06jpI2yozxaU9uVs4GzzgSL+uEq7bVcJ9V1ZXQkeGNql1fsSI0gMy1vhvNTNbUqrx+pZfJVmg=="],
"@tokenizer/token": ["@tokenizer/token@0.3.0", "", {}, "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A=="], "@tokenizer/token": ["@tokenizer/token@0.3.0", "", {}, "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A=="],
"@types/bun": ["@types/bun@1.3.0", "", { "dependencies": { "bun-types": "1.3.0" } }, "sha512-+lAGCYjXjip2qY375xX/scJeVRmZ5cY0wyHYyCYxNcdEXrQ4AOe3gACgd4iQ8ksOslJtW4VNxBJ8llUwc3a6AA=="], "@types/bun": ["@types/bun@1.3.1", "", { "dependencies": { "bun-types": "1.3.1" } }, "sha512-4jNMk2/K9YJtfqwoAa28c8wK+T7nvJFOjxI4h/7sORWcypRNxBpr+TPNaCfVWq70tLCJsqoFwcf0oI0JU/fvMQ=="],
"@types/cookie": ["@types/cookie@0.6.0", "", {}, "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA=="], "@types/cookie": ["@types/cookie@0.6.0", "", {}, "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA=="],
@@ -202,19 +200,17 @@
"axobject-query": ["axobject-query@4.1.0", "", {}, "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ=="], "axobject-query": ["axobject-query@4.1.0", "", {}, "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ=="],
"bun-types": ["bun-types@1.3.0", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-u8X0thhx+yJ0KmkxuEo9HAtdfgCBaM/aI9K90VQcQioAmkVp3SG3FkwWGibUFz3WdXAdcsqOcbU40lK7tbHdkQ=="], "bun-types": ["bun-types@1.3.1", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-NMrcy7smratanWJ2mMXdpatalovtxVggkj11bScuWuiOoXTiKIu2eVS1/7qbyI/4yHedtsn175n4Sm4JcdHLXw=="],
"chokidar": ["chokidar@4.0.3", "", { "dependencies": { "readdirp": "^4.0.1" } }, "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA=="], "chokidar": ["chokidar@4.0.3", "", { "dependencies": { "readdirp": "^4.0.1" } }, "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA=="],
"chownr": ["chownr@3.0.0", "", {}, "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g=="],
"clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="], "clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="],
"cookie": ["cookie@0.6.0", "", {}, "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw=="], "cookie": ["cookie@0.6.0", "", {}, "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw=="],
"csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="], "csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="],
"daisyui": ["daisyui@5.3.7", "", {}, "sha512-0+8PaSGift0HlIQABCeZzWOBV5Nx/vsI2TihB9hbaEyZENPlZZz+se2JnAH5rz9gBYTyDLB7NJup8hkREr6WBw=="], "daisyui": ["daisyui@5.4.3", "", {}, "sha512-dfDCJnN4utErGoWfElgdEE252FtfHV9Mxj5Dq1+JzUq3nVkluWdF3JYykP0Xy/y/yArnPXYztO1tLNCow3kjmg=="],
"debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
@@ -224,7 +220,7 @@
"devalue": ["devalue@5.3.2", "", {}, "sha512-UDsjUbpQn9kvm68slnrs+mfxwFkIflOhkanmyabZ8zOYk8SMEIbJ3TK+88g70hSIeytu4y18f0z/hYHMTrXIWw=="], "devalue": ["devalue@5.3.2", "", {}, "sha512-UDsjUbpQn9kvm68slnrs+mfxwFkIflOhkanmyabZ8zOYk8SMEIbJ3TK+88g70hSIeytu4y18f0z/hYHMTrXIWw=="],
"elysia": ["elysia@1.4.12", "", { "dependencies": { "cookie": "^1.0.2", "exact-mirror": "0.2.2", "fast-decode-uri-component": "^1.0.1" }, "peerDependencies": { "@sinclair/typebox": ">= 0.34.0 < 1", "file-type": ">= 20.0.0", "openapi-types": ">= 12.0.0", "typescript": ">= 5.0.0" }, "optionalPeers": ["typescript"] }, "sha512-wbd0BkrobsjWSloIfYeF3f7G7rR0UWMa6tuLUhf6ZvwjiCEX3FVfhDsM+KaqqRRxkZpPDw42q4yIZlBLyE32ww=="], "elysia": ["elysia@1.4.15", "", { "dependencies": { "cookie": "^1.0.2", "exact-mirror": "0.2.2", "fast-decode-uri-component": "^1.0.1", "memoirist": "^0.4.0" }, "peerDependencies": { "@sinclair/typebox": ">= 0.34.0 < 1", "@types/bun": ">= 1.2.0", "file-type": ">= 20.0.0", "openapi-types": ">= 12.0.0", "typescript": ">= 5.0.0" }, "optionalPeers": ["@types/bun", "typescript"] }, "sha512-RaDqqZdLuC4UJetfVRQ4Z5aVpGgEtQ+pZnsbI4ZzEaf3l/MzuHcqSVoL/Fue3d6qE4RV9HMB2rAZaHyPIxkyzg=="],
"enhanced-resolve": ["enhanced-resolve@5.18.3", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.2.0" } }, "sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww=="], "enhanced-resolve": ["enhanced-resolve@5.18.3", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.2.0" } }, "sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww=="],
@@ -260,27 +256,29 @@
"kleur": ["kleur@4.1.5", "", {}, "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ=="], "kleur": ["kleur@4.1.5", "", {}, "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ=="],
"lightningcss": ["lightningcss@1.30.1", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-darwin-arm64": "1.30.1", "lightningcss-darwin-x64": "1.30.1", "lightningcss-freebsd-x64": "1.30.1", "lightningcss-linux-arm-gnueabihf": "1.30.1", "lightningcss-linux-arm64-gnu": "1.30.1", "lightningcss-linux-arm64-musl": "1.30.1", "lightningcss-linux-x64-gnu": "1.30.1", "lightningcss-linux-x64-musl": "1.30.1", "lightningcss-win32-arm64-msvc": "1.30.1", "lightningcss-win32-x64-msvc": "1.30.1" } }, "sha512-xi6IyHML+c9+Q3W0S4fCQJOym42pyurFiJUHEcEyHS0CeKzia4yZDEsLlqOFykxOdHpNy0NmvVO31vcSqAxJCg=="], "lightningcss": ["lightningcss@1.30.2", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.30.2", "lightningcss-darwin-arm64": "1.30.2", "lightningcss-darwin-x64": "1.30.2", "lightningcss-freebsd-x64": "1.30.2", "lightningcss-linux-arm-gnueabihf": "1.30.2", "lightningcss-linux-arm64-gnu": "1.30.2", "lightningcss-linux-arm64-musl": "1.30.2", "lightningcss-linux-x64-gnu": "1.30.2", "lightningcss-linux-x64-musl": "1.30.2", "lightningcss-win32-arm64-msvc": "1.30.2", "lightningcss-win32-x64-msvc": "1.30.2" } }, "sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ=="],
"lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.30.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-c8JK7hyE65X1MHMN+Viq9n11RRC7hgin3HhYKhrMyaXflk5GVplZ60IxyoVtzILeKr+xAJwg6zK6sjTBJ0FKYQ=="], "lightningcss-android-arm64": ["lightningcss-android-arm64@1.30.2", "", { "os": "android", "cpu": "arm64" }, "sha512-BH9sEdOCahSgmkVhBLeU7Hc9DWeZ1Eb6wNS6Da8igvUwAe0sqROHddIlvU06q3WyXVEOYDZ6ykBZQnjTbmo4+A=="],
"lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.30.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-k1EvjakfumAQoTfcXUcHQZhSpLlkAuEkdMBsI/ivWw9hL+7FtilQc0Cy3hrx0AAQrVtQAbMI7YjCgYgvn37PzA=="], "lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.30.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-ylTcDJBN3Hp21TdhRT5zBOIi73P6/W0qwvlFEk22fkdXchtNTOU4Qc37SkzV+EKYxLouZ6M4LG9NfZ1qkhhBWA=="],
"lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.30.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-kmW6UGCGg2PcyUE59K5r0kWfKPAVy4SltVeut+umLCFoJ53RdCUWxcRDzO1eTaxf/7Q2H7LTquFHPL5R+Gjyig=="], "lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.30.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-oBZgKchomuDYxr7ilwLcyms6BCyLn0z8J0+ZZmfpjwg9fRVZIR5/GMXd7r9RH94iDhld3UmSjBM6nXWM2TfZTQ=="],
"lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.30.1", "", { "os": "linux", "cpu": "arm" }, "sha512-MjxUShl1v8pit+6D/zSPq9S9dQ2NPFSQwGvxBCYaBYLPlCWuPh9/t1MRS8iUaR8i+a6w7aps+B4N0S1TYP/R+Q=="], "lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.30.2", "", { "os": "freebsd", "cpu": "x64" }, "sha512-c2bH6xTrf4BDpK8MoGG4Bd6zAMZDAXS569UxCAGcA7IKbHNMlhGQ89eRmvpIUGfKWNVdbhSbkQaWhEoMGmGslA=="],
"lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.30.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-gB72maP8rmrKsnKYy8XUuXi/4OctJiuQjcuqWNlJQ6jZiWqtPvqFziskH3hnajfvKB27ynbVCucKSm2rkQp4Bw=="], "lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.30.2", "", { "os": "linux", "cpu": "arm" }, "sha512-eVdpxh4wYcm0PofJIZVuYuLiqBIakQ9uFZmipf6LF/HRj5Bgm0eb3qL/mr1smyXIS1twwOxNWndd8z0E374hiA=="],
"lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.30.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-jmUQVx4331m6LIX+0wUhBbmMX7TCfjF5FoOH6SD1CttzuYlGNVpA7QnrmLxrsub43ClTINfGSYyHe2HWeLl5CQ=="], "lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.30.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-UK65WJAbwIJbiBFXpxrbTNArtfuznvxAJw4Q2ZGlU8kPeDIWEX1dg3rn2veBVUylA2Ezg89ktszWbaQnxD/e3A=="],
"lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.30.1", "", { "os": "linux", "cpu": "x64" }, "sha512-piWx3z4wN8J8z3+O5kO74+yr6ze/dKmPnI7vLqfSqI8bccaTGY5xiSGVIJBDd5K5BHlvVLpUB3S2YCfelyJ1bw=="], "lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.30.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-5Vh9dGeblpTxWHpOx8iauV02popZDsCYMPIgiuw97OJ5uaDsL86cnqSFs5LZkG3ghHoX5isLgWzMs+eD1YzrnA=="],
"lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.30.1", "", { "os": "linux", "cpu": "x64" }, "sha512-rRomAK7eIkL+tHY0YPxbc5Dra2gXlI63HL+v1Pdi1a3sC+tJTcFrHX+E86sulgAXeI7rSzDYhPSeHHjqFhqfeQ=="], "lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.30.2", "", { "os": "linux", "cpu": "x64" }, "sha512-Cfd46gdmj1vQ+lR6VRTTadNHu6ALuw2pKR9lYq4FnhvgBc4zWY1EtZcAc6EffShbb1MFrIPfLDXD6Xprbnni4w=="],
"lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.30.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-mSL4rqPi4iXq5YVqzSsJgMVFENoa4nGTT/GjO2c0Yl9OuQfPsIfncvLrEW6RbbB24WtZ3xP/2CCmI3tNkNV4oA=="], "lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.30.2", "", { "os": "linux", "cpu": "x64" }, "sha512-XJaLUUFXb6/QG2lGIW6aIk6jKdtjtcffUT0NKvIqhSBY3hh9Ch+1LCeH80dR9q9LBjG3ewbDjnumefsLsP6aiA=="],
"lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.30.1", "", { "os": "win32", "cpu": "x64" }, "sha512-PVqXh48wh4T53F/1CCu8PIPCxLzWyCnn/9T5W1Jpmdy5h9Cwd+0YQS6/LwhHXSafuc61/xg9Lv5OrCby6a++jg=="], "lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.30.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-FZn+vaj7zLv//D/192WFFVA0RgHawIcHqLX9xuWiQt7P0PtdFEVaxgF9rjM/IRYHQXNnk61/H/gb2Ei+kUQ4xQ=="],
"lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.30.2", "", { "os": "win32", "cpu": "x64" }, "sha512-5g1yc73p+iAkid5phb4oVFMB45417DkRevRbt/El/gKXJk4jid+vPFF/AXbxn05Aky8PapwzZrdJShv5C0avjw=="],
"locate-character": ["locate-character@3.0.0", "", {}, "sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA=="], "locate-character": ["locate-character@3.0.0", "", {}, "sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA=="],
@@ -288,10 +286,6 @@
"memoirist": ["memoirist@0.4.0", "", {}, "sha512-zxTgA0mSYELa66DimuNQDvyLq36AwDlTuVRbnQtB+VuTcKWm5Qc4z3WkSpgsFWHNhexqkIooqpv4hdcqrX5Nmg=="], "memoirist": ["memoirist@0.4.0", "", {}, "sha512-zxTgA0mSYELa66DimuNQDvyLq36AwDlTuVRbnQtB+VuTcKWm5Qc4z3WkSpgsFWHNhexqkIooqpv4hdcqrX5Nmg=="],
"minipass": ["minipass@7.1.2", "", {}, "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw=="],
"minizlib": ["minizlib@3.1.0", "", { "dependencies": { "minipass": "^7.1.2" } }, "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw=="],
"mri": ["mri@1.2.0", "", {}, "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA=="], "mri": ["mri@1.2.0", "", {}, "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA=="],
"mrmime": ["mrmime@2.0.1", "", {}, "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ=="], "mrmime": ["mrmime@2.0.1", "", {}, "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ=="],
@@ -322,18 +316,16 @@
"strtok3": ["strtok3@10.3.4", "", { "dependencies": { "@tokenizer/token": "^0.3.0" } }, "sha512-KIy5nylvC5le1OdaaoCJ07L+8iQzJHGH6pWDuzS+d07Cu7n1MZ2x26P8ZKIWfbK02+XIL8Mp4RkWeqdUCrDMfg=="], "strtok3": ["strtok3@10.3.4", "", { "dependencies": { "@tokenizer/token": "^0.3.0" } }, "sha512-KIy5nylvC5le1OdaaoCJ07L+8iQzJHGH6pWDuzS+d07Cu7n1MZ2x26P8ZKIWfbK02+XIL8Mp4RkWeqdUCrDMfg=="],
"svelte": ["svelte@5.40.2", "", { "dependencies": { "@jridgewell/remapping": "^2.3.4", "@jridgewell/sourcemap-codec": "^1.5.0", "@sveltejs/acorn-typescript": "^1.0.5", "@types/estree": "^1.0.5", "acorn": "^8.12.1", "aria-query": "^5.3.1", "axobject-query": "^4.1.0", "clsx": "^2.1.1", "esm-env": "^1.2.1", "esrap": "^2.1.0", "is-reference": "^3.0.3", "locate-character": "^3.0.0", "magic-string": "^0.30.11", "zimmerframe": "^1.1.2" } }, "sha512-wr/SwBVCVfeHU8FZr48vRrzSpWdBBzGo5mlErjGzeW4reJhK/CWutLZbk/eHwhKqO17ccjeTcvsqjrT4aK3wZA=="], "svelte": ["svelte@5.43.3", "", { "dependencies": { "@jridgewell/remapping": "^2.3.4", "@jridgewell/sourcemap-codec": "^1.5.0", "@sveltejs/acorn-typescript": "^1.0.5", "@types/estree": "^1.0.5", "acorn": "^8.12.1", "aria-query": "^5.3.1", "axobject-query": "^4.1.0", "clsx": "^2.1.1", "esm-env": "^1.2.1", "esrap": "^2.1.0", "is-reference": "^3.0.3", "locate-character": "^3.0.0", "magic-string": "^0.30.11", "zimmerframe": "^1.1.2" } }, "sha512-kjkAjCk41mJfvJZG56XcJNOdJSke94JxtcX8zFzzz2vrt47E0LnoBzU6azIZ1aBxJgUep8qegAkguSf1GjxLXQ=="],
"svelte-adapter-bun": ["svelte-adapter-bun@0.5.2", "", { "dependencies": { "tiny-glob": "^0.2.9" } }, "sha512-xEtFgaal6UgrCwwkSIcapO9kopoFNUYCYqyKCikdqxX9bz2TDYnrWQZ7qBnkunMxi1HOIERUCvTcebYGiarZLA=="], "svelte-adapter-bun": ["svelte-adapter-bun@0.5.2", "", { "dependencies": { "tiny-glob": "^0.2.9" } }, "sha512-xEtFgaal6UgrCwwkSIcapO9kopoFNUYCYqyKCikdqxX9bz2TDYnrWQZ7qBnkunMxi1HOIERUCvTcebYGiarZLA=="],
"svelte-check": ["svelte-check@4.3.3", "", { "dependencies": { "@jridgewell/trace-mapping": "^0.3.25", "chokidar": "^4.0.1", "fdir": "^6.2.0", "picocolors": "^1.0.0", "sade": "^1.7.4" }, "peerDependencies": { "svelte": "^4.0.0 || ^5.0.0-next.0", "typescript": ">=5.0.0" }, "bin": { "svelte-check": "bin/svelte-check" } }, "sha512-RYP0bEwenDXzfv0P1sKAwjZSlaRyqBn0Fz1TVni58lqyEiqgwztTpmodJrGzP6ZT2aHl4MbTvWP6gbmQ3FOnBg=="], "svelte-check": ["svelte-check@4.3.3", "", { "dependencies": { "@jridgewell/trace-mapping": "^0.3.25", "chokidar": "^4.0.1", "fdir": "^6.2.0", "picocolors": "^1.0.0", "sade": "^1.7.4" }, "peerDependencies": { "svelte": "^4.0.0 || ^5.0.0-next.0", "typescript": ">=5.0.0" }, "bin": { "svelte-check": "bin/svelte-check" } }, "sha512-RYP0bEwenDXzfv0P1sKAwjZSlaRyqBn0Fz1TVni58lqyEiqgwztTpmodJrGzP6ZT2aHl4MbTvWP6gbmQ3FOnBg=="],
"tailwindcss": ["tailwindcss@4.1.14", "", {}, "sha512-b7pCxjGO98LnxVkKjaZSDeNuljC4ueKUddjENJOADtubtdo8llTaJy7HwBMeLNSSo2N5QIAgklslK1+Ir8r6CA=="], "tailwindcss": ["tailwindcss@4.1.16", "", {}, "sha512-pONL5awpaQX4LN5eiv7moSiSPd/DLDzKVRJz8Q9PgzmAdd1R4307GQS2ZpfiN7ZmekdQrfhZZiSE5jkLR4WNaA=="],
"tapable": ["tapable@2.3.0", "", {}, "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg=="], "tapable": ["tapable@2.3.0", "", {}, "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg=="],
"tar": ["tar@7.5.1", "", { "dependencies": { "@isaacs/fs-minipass": "^4.0.0", "chownr": "^3.0.0", "minipass": "^7.1.2", "minizlib": "^3.1.0", "yallist": "^5.0.0" } }, "sha512-nlGpxf+hv0v7GkWBK2V9spgactGOp0qvfWRxUMjqHyzrt3SgwE48DIv/FhqPHJYLHpgW1opq3nERbz5Anq7n1g=="],
"tiny-glob": ["tiny-glob@0.2.9", "", { "dependencies": { "globalyzer": "0.1.0", "globrex": "^0.1.2" } }, "sha512-g/55ssRPUjShh+xkfx9UPDXqhckHEsHr4Vd9zX55oSdGZc/MD0m3sferOkwWtp98bv+kcVfEHtRJgBVJzelrzg=="], "tiny-glob": ["tiny-glob@0.2.9", "", { "dependencies": { "globalyzer": "0.1.0", "globrex": "^0.1.2" } }, "sha512-g/55ssRPUjShh+xkfx9UPDXqhckHEsHr4Vd9zX55oSdGZc/MD0m3sferOkwWtp98bv+kcVfEHtRJgBVJzelrzg=="],
"tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="], "tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="],
@@ -348,12 +340,10 @@
"undici-types": ["undici-types@7.14.0", "", {}, "sha512-QQiYxHuyZ9gQUIrmPo3IA+hUl4KYk8uSA7cHrcKd/l3p1OTpZcM0Tbp9x7FAtXdAYhlasd60ncPpgu6ihG6TOA=="], "undici-types": ["undici-types@7.14.0", "", {}, "sha512-QQiYxHuyZ9gQUIrmPo3IA+hUl4KYk8uSA7cHrcKd/l3p1OTpZcM0Tbp9x7FAtXdAYhlasd60ncPpgu6ihG6TOA=="],
"vite": ["vite@7.1.10", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-CmuvUBzVJ/e3HGxhg6cYk88NGgTnBoOo7ogtfJJ0fefUWAxN/WDSUa50o+oVBxuIhO8FoEZW0j2eW7sfjs5EtA=="], "vite": ["vite@7.1.12", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-ZWyE8YXEXqJrrSLvYgrRP7p62OziLW7xI5HYGWFzOvupfAlrLvURSzv/FyGyy0eidogEM3ujU+kUG1zuHgb6Ug=="],
"vitefu": ["vitefu@1.1.1", "", { "peerDependencies": { "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0-beta.0" }, "optionalPeers": ["vite"] }, "sha512-B/Fegf3i8zh0yFbpzZ21amWzHmuNlLlmJT6n7bu5e+pCHUKQIfXSYokrqOBGEMMe9UG2sostKQF9mml/vYaWJQ=="], "vitefu": ["vitefu@1.1.1", "", { "peerDependencies": { "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0-beta.0" }, "optionalPeers": ["vite"] }, "sha512-B/Fegf3i8zh0yFbpzZ21amWzHmuNlLlmJT6n7bu5e+pCHUKQIfXSYokrqOBGEMMe9UG2sostKQF9mml/vYaWJQ=="],
"yallist": ["yallist@5.0.0", "", {}, "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw=="],
"zimmerframe": ["zimmerframe@1.1.4", "", {}, "sha512-B58NGBEoc8Y9MWWCQGl/gq9xBCe4IiKM0a2x7GZdQKOW5Exr8S1W24J6OgM1njK8xCRGvAJIL/MxXHf6SkmQKQ=="], "zimmerframe": ["zimmerframe@1.1.4", "", {}, "sha512-B58NGBEoc8Y9MWWCQGl/gq9xBCe4IiKM0a2x7GZdQKOW5Exr8S1W24J6OgM1njK8xCRGvAJIL/MxXHf6SkmQKQ=="],
"@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.5.0", "", { "dependencies": { "@emnapi/wasi-threads": "1.1.0", "tslib": "^2.4.0" }, "bundled": true }, "sha512-sbP8GzB1WDzacS8fgNPpHlp6C9VZe+SJP3F90W9rLemaQj2PzIuTEl1qDOYQf58YIpyjViI24y9aPWCjEzY2cg=="], "@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.5.0", "", { "dependencies": { "@emnapi/wasi-threads": "1.1.0", "tslib": "^2.4.0" }, "bundled": true }, "sha512-sbP8GzB1WDzacS8fgNPpHlp6C9VZe+SJP3F90W9rLemaQj2PzIuTEl1qDOYQf58YIpyjViI24y9aPWCjEzY2cg=="],
+28 -28
View File
@@ -1,30 +1,30 @@
{ {
"type": "module", "type": "module",
"devDependencies": { "devDependencies": {
"@sveltejs/kit": "^2.47.1", "@sveltejs/kit": "^2.48.4",
"@sveltejs/vite-plugin-svelte": "^6.2.1", "@sveltejs/vite-plugin-svelte": "^6.2.1",
"@types/bun": "^1.3.0", "@types/bun": "^1.3.1",
"svelte": "^5.40.2", "svelte": "^5.43.3",
"svelte-adapter-bun": "^0.5.2", "svelte-adapter-bun": "^0.5.2",
"svelte-check": "^4.3.3", "svelte-check": "^4.3.3",
"typescript": "^5.9.3", "typescript": "^5.9.3",
"vite": "^7.1.10" "vite": "^7.1.12"
}, },
"scripts": { "scripts": {
"dev": "vite dev", "dev": "vite dev",
"build": "vite build", "build": "vite build",
"preview": "vite preview", "preview": "vite preview",
"prepare": "svelte-kit sync || echo ''", "prepare": "svelte-kit sync || echo ''",
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch" "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch"
}, },
"dependencies": { "dependencies": {
"@elysiajs/cors": "^1.4.0", "@elysiajs/cors": "^1.4.0",
"@elysiajs/eden": "^1.4.4", "@elysiajs/eden": "^1.4.4",
"@tailwindcss/vite": "^4.1.14", "@tailwindcss/vite": "^4.1.16",
"daisyui": "^5.3.7", "daisyui": "^5.4.3",
"elysia": "^1.4.12", "elysia": "^1.4.15",
"memoirist": "^0.4.0", "memoirist": "^0.4.0",
"tailwindcss": "^4.1.14" "tailwindcss": "^4.1.16"
} }
} }
+7 -7
View File
@@ -1,13 +1,13 @@
// See https://svelte.dev/docs/kit/types#app.d.ts // See https://svelte.dev/docs/kit/types#app.d.ts
// for information about these interfaces // for information about these interfaces
declare global { declare global {
namespace App { namespace App {
// interface Error {} // interface Error {}
// interface Locals {} // interface Locals {}
// interface PageData {} // interface PageData {}
// interface PageState {} // interface PageState {}
// interface Platform {} // interface Platform {}
} }
} }
export {}; export {};
+8 -8
View File
@@ -1,11 +1,11 @@
<!doctype html> <!doctype html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="utf-8" /> <meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="viewport" content="width=device-width, initial-scale=1" />
%sveltekit.head% %sveltekit.head%
</head> </head>
<body data-sveltekit-preload-data="hover"> <body data-sveltekit-preload-data="hover">
<div style="display: contents">%sveltekit.body%</div> <div style="display: contents">%sveltekit.body%</div>
</body> </body>
</html> </html>
+3 -4
View File
@@ -1,9 +1,8 @@
import { treaty } from "@elysiajs/eden"; import { treaty } from "@elysiajs/eden";
import type { App } from "./server/types"; import type { App } from "./server/types";
const baseURL = const baseURL = import.meta.env.PROD
import.meta.env.PROD ? "https://blade-and-brawn.fly.dev"
? "https://blade-and-brawn.fly.dev" : "http://localhost:5173";
: "http://localhost:5173";
export const api = treaty<App>(baseURL).api; export const api = treaty<App>(baseURL).api;
+104 -104
View File
@@ -1,107 +1,107 @@
{ {
"metadata": { "metadata": {
"unit": "kg" "unit": "kg"
},
"weights": [
{
"gender": "Male",
"age": 5,
"weight": 21.1
}, },
{ "weights": [
"gender": "Male", {
"age": 10, "gender": "Male",
"weight": 38.7 "age": 5,
}, "weight": 21.1
{ },
"gender": "Male", {
"age": 15, "gender": "Male",
"weight": 66.1 "age": 10,
}, "weight": 38.7
{ },
"gender": "Male", {
"age": 20, "gender": "Male",
"weight": 81.3 "age": 15,
}, "weight": 66.1
{ },
"gender": "Male", {
"age": 30, "gender": "Male",
"weight": 89.7 "age": 20,
}, "weight": 81.3
{ },
"gender": "Male", {
"age": 40, "gender": "Male",
"weight": 90.5 "age": 30,
}, "weight": 89.7
{ },
"gender": "Male", {
"age": 50, "gender": "Male",
"weight": 89.6 "age": 40,
}, "weight": 90.5
{ },
"gender": "Male", {
"age": 60, "gender": "Male",
"weight": 89.5 "age": 50,
}, "weight": 89.6
{ },
"gender": "Male", {
"age": 70, "gender": "Male",
"weight": 89.3 "age": 60,
}, "weight": 89.5
{ },
"gender": "Male", {
"age": 80, "gender": "Male",
"weight": 79.6 "age": 70,
}, "weight": 89.3
{ },
"gender": "Female", {
"age": 5, "gender": "Male",
"weight": 21.0 "age": 80,
}, "weight": 79.6
{ },
"gender": "Female", {
"age": 10, "gender": "Female",
"weight": 39.5 "age": 5,
}, "weight": 21.0
{ },
"gender": "Female", {
"age": 15, "gender": "Female",
"weight": 58.1 "age": 10,
}, "weight": 39.5
{ },
"gender": "Female", {
"age": 20, "gender": "Female",
"weight": 69.5 "age": 15,
}, "weight": 58.1
{ },
"gender": "Female", {
"age": 30, "gender": "Female",
"weight": 73.3 "age": 20,
}, "weight": 69.5
{ },
"gender": "Female", {
"age": 40, "gender": "Female",
"weight": 75.2 "age": 30,
}, "weight": 73.3
{ },
"gender": "Female", {
"age": 50, "gender": "Female",
"weight": 74.6 "age": 40,
}, "weight": 75.2
{ },
"gender": "Female", {
"age": 60, "gender": "Female",
"weight": 75.1 "age": 50,
}, "weight": 74.6
{ },
"gender": "Female", {
"age": 70, "gender": "Female",
"weight": 73.2 "age": 60,
}, "weight": 75.1
{ },
"gender": "Female", {
"age": 80, "gender": "Female",
"weight": 66.3 "age": 70,
} "weight": 73.2
] },
{
"gender": "Female",
"age": 80,
"weight": 66.3
}
]
} }
+5307 -5307
View File
File diff suppressed because it is too large Load Diff
+171 -147
View File
@@ -1,176 +1,200 @@
import { Elysia, NotFoundError } from "elysia"; import rawStandards from "$lib/data/standards.json" assert { type: "json" };
import {
LevelCalculator,
Standards,
type ActivityStandards,
} from "$lib/services/calculator/main";
import type {
ActivityPerformance,
Player,
} from "$lib/services/calculator/util";
import { cors } from "@elysiajs/cors"; import { cors } from "@elysiajs/cors";
import { LevelCalculator, Standards, type ActivityStandards, type LevelCalculatorOutput } from "$lib/services/calculator/main"; import { Elysia, NotFoundError } from "elysia";
import type { ActivityPerformance, Player } from "$lib/services/calculator/util"; import PrintfulService from "../services/commerce/printful";
import rawStandards from "$lib/data/standards.json" assert { type: "json" } import SyncService from "../services/commerce/sync";
import { FetchError, getStateFromZip } from "../services/commerce/util/misc";
import { Printful, Webflow } from "../services/commerce/util/types"; import { Printful, Webflow } from "../services/commerce/util/types";
import WebflowService from "../services/commerce/webflow"; import WebflowService from "../services/commerce/webflow";
import SyncService from "../services/commerce/sync";
import PrintfulService from "../services/commerce/printful";
import { FetchError, getStateFromZip } from "../services/commerce/util/misc";
const MAIN_STANDARDS = new Standards(rawStandards as ActivityStandards); const MAIN_STANDARDS = new Standards(rawStandards as ActivityStandards);
const levelCalculator = new LevelCalculator(MAIN_STANDARDS); const levelCalculator = new LevelCalculator(MAIN_STANDARDS);
export const app = new Elysia({ prefix: "/api" }) export const app = new Elysia({ prefix: "/api" })
.use( .use(
cors({ cors({
origin: [ origin: [
// bladeandbrawn.com (apex + any subdomain) // bladeandbrawn.com (apex + any subdomain)
/^https?:\/\/([a-z0-9-]+\.)?bladeandbrawn\.com$/i, /^https?:\/\/([a-z0-9-]+\.)?bladeandbrawn\.com$/i,
// Webflow preview domains // Webflow preview domains
/^https?:\/\/([a-z0-9-]+\.)?bladeandbrawn\.webflow\.io$/i, /^https?:\/\/([a-z0-9-]+\.)?bladeandbrawn\.webflow\.io$/i,
// Fly app host (this one already works) // Fly app host (this one already works)
/^https?:\/\/blade-and-brawn\.fly\.dev$/i, /^https?:\/\/blade-and-brawn\.fly\.dev$/i,
// Local dev // Local dev
"http://localhost:5173", "http://localhost:5173",
] ],
}) }),
) )
.error({ .error({
FetchError FetchError,
}) })
.onError(async ({ code, error }) => { .onError(async ({ code, error }) => {
switch (code) { switch (code) {
case "FetchError": case "FetchError":
console.error(error.message, await error.parse()); console.error(error.message, await error.parse());
return error; return error;
} }
}) })
.onAfterHandle(({ request, set }) => { .onAfterHandle(({ request, set }) => {
console.log(JSON.stringify({ console.log(
lvl: "info", JSON.stringify({
msg: "req", lvl: "info",
method: request.method, msg: "req",
path: new URL(request.url).pathname, method: request.method,
status: set.status ?? 200 path: new URL(request.url).pathname,
})); status: set.status ?? 200,
}) }),
);
})
.get("/", () => "Hello world") .get("/", () => "Hello world")
// CALCULATOR // CALCULATOR
.post("/calculate", async ({ body, query }) => { .post("/calculate", async ({ body, query }) => {
interface CalcRequest { interface CalcRequest {
player: Player; player: Player;
activityPerformances: ActivityPerformance[]; activityPerformances: ActivityPerformance[];
} }
const { player, activityPerformances } = body as CalcRequest; const { player, activityPerformances } = body as CalcRequest;
const output = { levels: levelCalculator.calculate(player, activityPerformances) }; const output = {
levels: levelCalculator.calculate(player, activityPerformances),
};
console.log({ log: query?.log }); console.log({ log: query?.log });
if (query?.log === "true") { if (query?.log === "true") {
void fetch("https://kv-logger.xominus.workers.dev", { void fetch("https://kv-logger.xominus.workers.dev", {
method: "POST", method: "POST",
headers: { "content-type": "application/json" }, headers: { "content-type": "application/json" },
body: JSON.stringify({ output, player, activityPerformances }) body: JSON.stringify({ output, player, activityPerformances }),
}).catch(e => { }); }).catch(() => {});
} }
return output; return output;
}) })
// COMMERCE // COMMERCE
// TODO: protect with api key/token // TODO: protect with api key/token
.group("/products", app => app .group("/products", (app) =>
.group("/sync", app => app app
.get("/", async ({ }) => SyncService.state) .group("/sync", (app) =>
.post("/:printfulProductId?", async ({ params, set }) => { app
if (SyncService.state.isSyncing) { .get("/", async ({}) => SyncService.state)
set.status = 409; .post("/:printfulProductId?", async ({ params, set }) => {
return { error: "Sync already in progress" }; if (SyncService.state.isSyncing) {
} set.status = 409;
const printfulProductId = params.printfulProductId ? return { error: "Sync already in progress" };
+params.printfulProductId : }
undefined; const printfulProductId = params.printfulProductId
await SyncService.sync(printfulProductId); ? +params.printfulProductId
return { ok: true }; : undefined;
}) await SyncService.sync(printfulProductId);
) return { ok: true };
.get("/:printfulProductId", async ({ params }) => { }),
const printfulProductId = +params.printfulProductId; )
const printfulProduct = await PrintfulService.Products.get(printfulProductId); .get("/:printfulProductId", async ({ params }) => {
if (!printfulProduct) { const printfulProductId = +params.printfulProductId;
return new NotFoundError(); const printfulProduct =
} await PrintfulService.Products.get(printfulProductId);
if (!printfulProduct) {
return new NotFoundError();
}
const webflowProductId = printfulProduct.sync_product.external_id.split("-")[0]; const webflowProductId =
const webflowProduct = await WebflowService.Products.get(webflowProductId); printfulProduct.sync_product.external_id.split("-")[0];
const webflowProduct =
await WebflowService.Products.get(webflowProductId);
return { printfulProduct, webflowProduct }; return { printfulProduct, webflowProduct };
}) }),
) )
// WEBHOOKS // WEBHOOKS
.post("/webhook/printful", async ({ body }) => { .post("/webhook/printful", async ({ body }) => {
const payload = body as Printful.Webhook.EventPayload; const payload = body as Printful.Webhook.EventPayload;
switch (payload.type) { switch (payload.type) {
case Printful.Webhook.Event.ProductUpdated: { case Printful.Webhook.Event.ProductUpdated: {
const printfulProduct = payload.data.sync_product; const printfulProduct = payload.data.sync_product;
await SyncService.sync(printfulProduct.id); await SyncService.sync(printfulProduct.id);
break; break;
} }
case Printful.Webhook.Event.ProductDeleted: { case Printful.Webhook.Event.ProductDeleted: {
await WebflowService.Products.remove(payload.data.sync_product.external_id); await WebflowService.Products.remove(
break; payload.data.sync_product.external_id,
} );
case Printful.Webhook.Event.PackageShipped: { break;
const webflowOrderId = payload.data.order.external_id; }
const shipInfo = payload.data.shipment; case Printful.Webhook.Event.PackageShipped: {
const webflowOrderId = payload.data.order.external_id;
const shipInfo = payload.data.shipment;
await WebflowService.Orders.update(webflowOrderId, { await WebflowService.Orders.update(webflowOrderId, {
shippingTrackingURL: shipInfo.tracking_url, shippingTrackingURL: shipInfo.tracking_url,
shippingTracking: shipInfo.tracking_number, shippingTracking: shipInfo.tracking_number,
shippingProvider: shipInfo.carrier shippingProvider: shipInfo.carrier,
}); });
await WebflowService.Orders.fulfill(webflowOrderId, { sendOrderFulfilledEmail: true }); await WebflowService.Orders.fulfill(webflowOrderId, {
break; sendOrderFulfilledEmail: true,
} });
} break;
}
}
return { ok: true }; return { ok: true };
}) })
.post("/webhook/webflow", async ({ request, body, set }) => { .post("/webhook/webflow", async ({ request, body, set }) => {
if (!WebflowService.Util.verifyWebflowSignature(request, body)) { if (!WebflowService.Util.verifyWebflowSignature(request, body)) {
set.status = 400; set.status = 400;
return "Invalid signature"; return "Invalid signature";
} }
const payload = body as Webflow.Webhook.EventPayload; const payload = body as Webflow.Webhook.EventPayload;
switch (payload.triggerType) { switch (payload.triggerType) {
case Webflow.Webhook.Event.OrderCreated: { case Webflow.Webhook.Event.OrderCreated: {
const webflowOrder = payload.payload; const webflowOrder = payload.payload;
await PrintfulService.Orders.create({ await PrintfulService.Orders.create({
external_id: webflowOrder.orderId, external_id: webflowOrder.orderId,
// TODO: derive from webflow // TODO: derive from webflow
shipping: "STANDARD", shipping: "STANDARD",
recipient: { recipient: {
name: webflowOrder.shippingAddress.addressee, name: webflowOrder.shippingAddress.addressee,
address1: webflowOrder.shippingAddress.line1, address1: webflowOrder.shippingAddress.line1,
address2: webflowOrder.shippingAddress.line2, address2: webflowOrder.shippingAddress.line2,
city: webflowOrder.shippingAddress.city, city: webflowOrder.shippingAddress.city,
state_code: getStateFromZip(webflowOrder.shippingAddress.postalCode), state_code: getStateFromZip(
country_code: webflowOrder.shippingAddress.country, webflowOrder.shippingAddress.postalCode,
zip: webflowOrder.shippingAddress.postalCode ),
}, country_code: webflowOrder.shippingAddress.country,
items: webflowOrder.purchasedItems.map(webflowOrderSku => ({ zip: webflowOrder.shippingAddress.postalCode,
external_variant_id: webflowOrderSku.variantId, },
quantity: webflowOrderSku.count items: webflowOrder.purchasedItems.map(
})) (webflowOrderSku) => ({
}); external_variant_id: webflowOrderSku.variantId,
quantity: webflowOrderSku.count,
}),
),
});
console.log("Complete"); console.log("Complete");
break; break;
} }
} }
return { ok: true }; return { ok: true };
}); });
+1 -1
View File
@@ -1,2 +1,2 @@
import type { app } from './app'; import type { app } from "./app";
export type App = typeof app; export type App = typeof app;
+305 -168
View File
@@ -1,4 +1,14 @@
import { Activity, Attribute, Gender, getAvgWeight, kgToLb, lbToKg, type ActivityPerformance, type Player, type StandardUnit } from "./util"; import {
Activity,
Attribute,
Gender,
getAvgWeight,
kgToLb,
lbToKg,
type ActivityPerformance,
type Player,
type StandardUnit,
} from "./util";
// SOURCES // SOURCES
// Squat, Bench, Dead Lift: // Squat, Bench, Dead Lift:
@@ -11,17 +21,17 @@ import { Activity, Attribute, Gender, getAvgWeight, kgToLb, lbToKg, type Activit
// https://nflsavant.com/combine.php?utm_source=chatgpt.com // https://nflsavant.com/combine.php?utm_source=chatgpt.com
export type LevelCalculatorOutput = { export type LevelCalculatorOutput = {
player: number, player: number;
attributes: Record<Attribute, number> attributes: Record<Attribute, number>;
} };
export type Levels = Record<string, number> export type Levels = Record<string, number>;
export interface Metrics { export interface Metrics {
age: number; age: number;
weight: number; weight: number;
gender: Gender; gender: Gender;
}; }
type NumberMetric = Extract<keyof Metrics, "age" | "weight">; type NumberMetric = Extract<keyof Metrics, "age" | "weight">;
@@ -31,18 +41,21 @@ export interface Standard {
} }
type Generator = { type Generator = {
metric: NumberMetric metric: NumberMetric;
} };
export type ActivityStandards = Record<Activity, { export type ActivityStandards = Record<
metadata: { Activity,
attribute: Attribute, {
generators: Generator[], metadata: {
unit: StandardUnit, attribute: Attribute;
name: string generators: Generator[];
}, unit: StandardUnit;
standards: Standard[] name: string;
}>; };
standards: Standard[];
}
>;
export class LevelCalculator { export class LevelCalculator {
standards: Standards; standards: Standards;
@@ -55,7 +68,10 @@ export class LevelCalculator {
// Level calculations // Level calculations
// ------------------------------------------------------------------------------------------------- // -------------------------------------------------------------------------------------------------
public calculate(player: Player, activityPerformances: ActivityPerformance[]): LevelCalculatorOutput { public calculate(
player: Player,
activityPerformances: ActivityPerformance[],
): LevelCalculatorOutput {
const levels: LevelCalculatorOutput = { const levels: LevelCalculatorOutput = {
player: 0, player: 0,
attributes: { attributes: {
@@ -63,22 +79,28 @@ export class LevelCalculator {
[Attribute.Power]: 0, [Attribute.Power]: 0,
[Attribute.Endurance]: 0, [Attribute.Endurance]: 0,
[Attribute.Agility]: 0, [Attribute.Agility]: 0,
} as const } as const,
}; };
for (const value of Object.values(player)) { for (const value of Object.values(player)) {
if (!value) if (!value) return levels;
return levels;
} }
levels.attributes = this.calculateAllAttributeLevels(player, activityPerformances); levels.attributes = this.calculateAllAttributeLevels(
player,
activityPerformances,
);
for (const level of Object.values(levels.attributes)) { for (const level of Object.values(levels.attributes)) {
if (!level) if (!level) return levels;
return levels;
} }
const attrLevelsSum = Object.values(levels.attributes).reduce((sum, lvl) => sum + lvl, 0); const attrLevelsSum = Object.values(levels.attributes).reduce(
levels.player = Math.round(attrLevelsSum / Object.keys(Attribute).length); (sum, lvl) => sum + lvl,
0,
);
levels.player = Math.round(
attrLevelsSum / Object.keys(Attribute).length,
);
return levels; return levels;
} }
@@ -95,42 +117,54 @@ export class LevelCalculator {
} as const; } as const;
for (const value of Object.values(player)) { for (const value of Object.values(player)) {
if (!value) if (!value) return attrLevels;
return attrLevels;
} }
(Object.values(Attribute) as Attribute[]).forEach((attribute) => { (Object.values(Attribute) as Attribute[]).forEach((attribute) => {
const attrActivityPerformances = activityPerformances.filter( const attrActivityPerformances = activityPerformances.filter(
(p) => this.standards.byActivity(p.activity).getMetadata().attribute === attribute, (p) =>
this.standards.byActivity(p.activity).getMetadata()
.attribute === attribute,
);
attrLevels[attribute] = this.calculateAttributeLevel(
attribute,
player,
attrActivityPerformances,
); );
attrLevels[attribute] = this.calculateAttributeLevel(attribute, player, attrActivityPerformances);
}); });
return attrLevels; return attrLevels;
} }
private calculateAttributeLevel( private calculateAttributeLevel(
attribute: Attribute, attribute: Attribute,
player: Player, player: Player,
activityPerformances: ActivityPerformance[], activityPerformances: ActivityPerformance[],
): number { ): number {
// must be activities of the given attribute // must be activities of the given attribute
if (activityPerformances.some((p) => this.standards.byActivity(p.activity).getMetadata().attribute !== attribute)) { if (
activityPerformances.some(
(p) =>
this.standards.byActivity(p.activity).getMetadata()
.attribute !== attribute,
)
) {
throw new Error("Wrong activity performance attribute"); throw new Error("Wrong activity performance attribute");
} }
// must perform all of an attributes activities // must perform all of an attributes activities
for (const activity of this.standards.getAttributeActivities(attribute)) { for (const activity of this.standards.getAttributeActivities(
const filteredActivites = activityPerformances.map((p) => p.activity); attribute,
if (!filteredActivites.includes(activity)) )) {
return 0; const filteredActivites = activityPerformances.map(
(p) => p.activity,
);
if (!filteredActivites.includes(activity)) return 0;
} }
// must have all activties using a valid performance value (> 0) // must have all activties using a valid performance value (> 0)
for (const activityPerformance of activityPerformances) { for (const activityPerformance of activityPerformances) {
if (activityPerformance.performance <= 0) if (activityPerformance.performance <= 0) return 0;
return 0
} }
const activityLevels = activityPerformances.map((p) => { const activityLevels = activityPerformances.map((p) => {
@@ -142,7 +176,8 @@ export class LevelCalculator {
}); });
const activityLevelsAvg = Math.round( const activityLevelsAvg = Math.round(
activityLevels.reduce((sum, curr) => sum + curr, 0) / activityLevels.length, activityLevels.reduce((sum, curr) => sum + curr, 0) /
activityLevels.length,
); );
return activityLevelsAvg; return activityLevelsAvg;
@@ -171,85 +206,109 @@ export class LevelCalculator {
export type StandardsConfig = { export type StandardsConfig = {
global: { global: {
maxLevel: number, maxLevel: number;
}, };
activity: Record<Activity, { activity: Record<
weightModifier: number, Activity,
weightSkew: number, {
ageModifier: number, weightModifier: number;
enableGeneration: boolean, weightSkew: number;
difficultyModifier: number ageModifier: number;
}> enableGeneration: boolean;
} difficultyModifier: number;
peakAge: number;
}
>;
};
export class Standards { export class Standards {
readonly cfg: StandardsConfig; readonly cfg: StandardsConfig;
private activityStandards: ActivityStandards; private activityStandards: ActivityStandards;
constructor(activityStandards: ActivityStandards, cfg?: Partial<StandardsConfig>) { constructor(
activityStandards: ActivityStandards,
cfg?: Partial<StandardsConfig>,
) {
// TODO: set these somewhere // TODO: set these somewhere
const defaultActivitiesConfig = Object.fromEntries( const defaultActivitiesConfig = Object.fromEntries(
Object.values(Activity).map(activity => [ Object.values(Activity).map((activity) => [
activity, activity,
{ {
enableGeneration: true, enableGeneration: true,
weightModifier: activity === Activity.BroadJump ? weightModifier:
-0.1 : activity === Activity.BroadJump ? -0.1 : 0.1,
0.1,
weightSkew: 0, weightSkew: 0,
ageModifier: 0.5, ageModifier: 0.5,
difficultyModifier: activity === Activity.BroadJump ? difficultyModifier:
0.05 : activity === Activity.BroadJump ? 0.05 : 0,
0 peakAge: 27,
}, },
]) ]),
) as Record<Activity, StandardsConfig["activity"][Activity]>; ) as Record<Activity, StandardsConfig["activity"][Activity]>;
this.cfg = { this.cfg = {
global: { global: {
maxLevel: cfg?.global?.maxLevel ?? 100, maxLevel: cfg?.global?.maxLevel ?? 100,
}, },
activity: Object.assign(defaultActivitiesConfig, cfg?.activity) activity: Object.assign(defaultActivitiesConfig, cfg?.activity),
}; };
// prepare data // prepare data
this.activityStandards = structuredClone(activityStandards); this.activityStandards = structuredClone(activityStandards);
for (const activity of Object.keys(this.activityStandards) as Activity[]) { for (const activity of Object.keys(
this.activityStandards,
) as Activity[]) {
for (const standard of this.activityStandards[activity].standards) { for (const standard of this.activityStandards[activity].standards) {
// expand // expand
let i = 1; let i = 1;
while (Object.keys(standard.levels).length < this.cfg.global.maxLevel) { while (
Object.keys(standard.levels).length <
this.cfg.global.maxLevel
) {
standard.levels = this.expandLevels(standard.levels, i++); standard.levels = this.expandLevels(standard.levels, i++);
} }
// compress // compress
if (Object.keys(standard.levels).length > this.cfg.global.maxLevel) { if (
standard.levels = this.compressLevels(standard.levels, this.cfg.global.maxLevel); Object.keys(standard.levels).length >
this.cfg.global.maxLevel
) {
standard.levels = this.compressLevels(
standard.levels,
this.cfg.global.maxLevel,
);
} }
// apply skew config // apply skew config
for (const level in standard.levels) { for (const level in standard.levels) {
standard.levels[level] = standard.levels[level] * (1 + this.cfg.activity[activity].difficultyModifier) standard.levels[level] =
standard.levels[level] *
(1 + this.cfg.activity[activity].difficultyModifier);
} }
} }
// generate data // generate data
const allGenerators = this.cfg.activity[activity].enableGeneration ? const allGenerators = this.cfg.activity[activity].enableGeneration
this.activityStandards[activity].metadata.generators : ? this.activityStandards[activity].metadata.generators
[]; : [];
const ageGenerators = allGenerators.filter(g => g.metric === "age"); const ageGenerators = allGenerators.filter(
(g) => g.metric === "age",
);
for (const ageGenerator of ageGenerators) { for (const ageGenerator of ageGenerators) {
for (const gender of Object.values(Gender)) { for (const gender of Object.values(Gender)) {
const peakAge = 27; const peakAge = this.cfg.activity[activity].peakAge;
const ageStep = 10; const ageStep = 10;
const minAge = 0; const minAge = 0;
const maxAge = 100; const maxAge = 100;
const referenceStandard = this const referenceStandard = this.byActivity(activity)
.byActivity(activity) .byMetrics({
.byMetrics({ weight: getAvgWeight(gender, peakAge), gender, age: peakAge }) weight: getAvgWeight(gender, peakAge),
gender,
age: peakAge,
})
.getOneInterpolated(); .getOneInterpolated();
let currAge = minAge; let currAge = minAge;
@@ -258,9 +317,9 @@ export class Standards {
metrics: { metrics: {
weight: getAvgWeight(gender, currAge), weight: getAvgWeight(gender, currAge),
age: currAge, age: currAge,
gender gender,
}, },
levels: {} levels: {},
}; };
for (const lvl in referenceStandard.levels) { for (const lvl in referenceStandard.levels) {
@@ -275,12 +334,17 @@ export class Standards {
const youngFloor = 1 - (1 - youngFloor0) * s; const youngFloor = 1 - (1 - youngFloor0) * s;
const oldFloor = 1 - (1 - oldFloor0) * s; const oldFloor = 1 - (1 - oldFloor0) * s;
const cy = s * (1 - youngFloor0) / Math.pow(peakAge - minAge, 2); const cy =
const co = s * (1 - oldFloor0) / Math.pow(maxAge - peakAge, 2); (s * (1 - youngFloor0)) /
Math.pow(peakAge - minAge, 2);
const co =
(s * (1 - oldFloor0)) /
Math.pow(maxAge - peakAge, 2);
let f = currAge <= peakAge let f =
? 1 - cy * Math.pow(peakAge - currAge, 2) currAge <= peakAge
: 1 - co * Math.pow(currAge - peakAge, 2); ? 1 - cy * Math.pow(peakAge - currAge, 2)
: 1 - co * Math.pow(currAge - peakAge, 2);
const floor = Math.min(youngFloor, oldFloor); const floor = Math.min(youngFloor, oldFloor);
const fCurr = Math.max(floor, Math.min(1, f)); const fCurr = Math.max(floor, Math.min(1, f));
@@ -289,34 +353,41 @@ export class Standards {
} }
// prefer real data over generated data // prefer real data over generated data
const overlappingStandard = this const overlappingStandard = this.byActivity(activity)
.byActivity(activity)
.byMetrics(newStandard.metrics) .byMetrics(newStandard.metrics)
.getOne(); .getOne();
if (!overlappingStandard) if (!overlappingStandard)
this.activityStandards[activity].standards.push(newStandard); this.activityStandards[activity].standards.push(
newStandard,
);
currAge += ageStep; currAge += ageStep;
} }
} }
// now that we generated metric data, remove the old data // now that we generated metric data, remove the old data
this.activityStandards[activity].standards = this.activityStandards[activity].standards this.activityStandards[activity].standards =
.filter(s => s.metrics.weight) this.activityStandards[activity].standards.filter(
(s) => s.metrics.weight,
);
} }
const weightGenerators = allGenerators.filter(g => g.metric === "weight"); const weightGenerators = allGenerators.filter(
(g) => g.metric === "weight",
);
for (const weightGenerator of weightGenerators) { for (const weightGenerator of weightGenerators) {
for (const gender of Object.values(Gender)) { for (const gender of Object.values(Gender)) {
for (const age of this.agesFor(activity, gender)) { for (const age of this.agesFor(activity, gender)) {
const weightStep = 12; const weightStep = 12;
const referenceWeight = getAvgWeight(gender, age); const referenceWeight = getAvgWeight(gender, age);
const minWeight = Math.max(referenceWeight - (3 * weightStep), 1); const minWeight = Math.max(
const maxWeight = referenceWeight + (3 * weightStep); referenceWeight - 3 * weightStep,
1,
);
const maxWeight = referenceWeight + 3 * weightStep;
const referenceStandard = this const referenceStandard = this.byActivity(activity)
.byActivity(activity)
.byMetrics({ weight: referenceWeight, gender, age }) .byMetrics({ weight: referenceWeight, gender, age })
.getOneInterpolated(); .getOneInterpolated();
@@ -326,28 +397,35 @@ export class Standards {
metrics: { metrics: {
weight: currWeight, weight: currWeight,
age, age,
gender gender,
}, },
levels: {} levels: {},
}; };
// apply allometric scaling to generate levels // apply allometric scaling to generate levels
for (const lvl in referenceStandard.levels) { for (const lvl in referenceStandard.levels) {
if (!referenceStandard.levels[lvl]) continue; if (!referenceStandard.levels[lvl]) continue;
const scalingExponent = this.cfg.activity[activity].weightModifier const scalingExponent =
const coefficient = currWeight / referenceWeight; this.cfg.activity[activity].weightModifier;
const coefficient =
currWeight / referenceWeight;
newStandard.levels[lvl] = referenceStandard.levels[lvl] * (coefficient ** scalingExponent); newStandard.levels[lvl] =
referenceStandard.levels[lvl] *
coefficient ** scalingExponent;
} }
// prefer real data over generated data // prefer real data over generated data
const overlappingStandard = this const overlappingStandard = this.byActivity(
.byActivity(activity) activity,
)
.byMetrics(newStandard.metrics) .byMetrics(newStandard.metrics)
.getOne(); .getOne();
if (!overlappingStandard) if (!overlappingStandard)
this.activityStandards[activity].standards.push(newStandard); this.activityStandards[activity].standards.push(
newStandard,
);
currWeight += weightStep; currWeight += weightStep;
} }
@@ -355,20 +433,26 @@ export class Standards {
} }
// now that we generated metric data, remove the old data // now that we generated metric data, remove the old data
this.activityStandards[activity].standards = this.activityStandards[activity].standards this.activityStandards[activity].standards =
.filter(s => s.metrics.weight); this.activityStandards[activity].standards.filter(
(s) => s.metrics.weight,
);
} }
this.activityStandards[activity].standards = this.activityStandards[activity].standards this.activityStandards[activity].standards = this.activityStandards[
.sort((a, b) => { activity
const genderOrder: Record<Gender, number> = { Male: 0, Female: 1 }; ].standards.sort((a, b) => {
const gA = genderOrder[b.metrics.gender] ?? 99; const genderOrder: Record<Gender, number> = {
const gB = genderOrder[b.metrics.gender] ?? 99; Male: 0,
if (gA !== gB) return gA - gB; Female: 1,
if (a.metrics.age !== b.metrics.age) };
return a.metrics.age - b.metrics.age; const gA = genderOrder[b.metrics.gender] ?? 99;
return a.metrics.weight - b.metrics.weight; const gB = genderOrder[b.metrics.gender] ?? 99;
}); if (gA !== gB) return gA - gB;
if (a.metrics.age !== b.metrics.age)
return a.metrics.age - b.metrics.age;
return a.metrics.weight - b.metrics.weight;
});
} }
} }
@@ -387,32 +471,57 @@ export class Standards {
const out: Standard[] = []; const out: Standard[] = [];
for (let i = 0; i < src.length; i++) { for (let i = 0; i < src.length; i++) {
const s = src[i]; const s = src[i];
if (state.gender != null && s.metrics.gender !== state.gender) continue; if (
if (state.age != null && s.metrics.age !== state.age) continue; state.gender != null &&
if (state.weight != null && s.metrics.weight !== state.weight) continue; s.metrics.gender !== state.gender
)
continue;
if (state.age != null && s.metrics.age !== state.age)
continue;
if (
state.weight != null &&
s.metrics.weight !== state.weight
)
continue;
out.push(s); out.push(s);
} }
return out; return out;
}, },
getOne: function(): Standard | undefined { getOne: function (): Standard | undefined {
return execMethods.exact.getAll()[0]; return execMethods.exact.getAll()[0];
}, },
getNearest(metric: NumberMetric, target: number): { lower: Standard; upper: Standard } { getNearest(
metric: NumberMetric,
target: number,
): { lower: Standard; upper: Standard } {
const standards = execMethods.exact.getAll(); const standards = execMethods.exact.getAll();
if (standards.length === 0) throw new Error('no standards'); if (standards.length === 0) throw new Error("no standards");
let lower = standards[0]; // best <= target let lower = standards[0]; // best <= target
let upper = standards[0]; // best >= target let upper = standards[0]; // best >= target
let hasLower = false, hasUpper = false; let hasLower = false,
hasUpper = false;
for (const standard of standards) { for (const standard of standards) {
const standardMetricValue = standard.metrics[metric]; const standardMetricValue = standard.metrics[metric];
if (standardMetricValue <= target) { if (standardMetricValue <= target) {
if (!hasLower || standardMetricValue > lower.metrics[metric]) { lower = standard; hasLower = true; } if (
!hasLower ||
standardMetricValue > lower.metrics[metric]
) {
lower = standard;
hasLower = true;
}
} }
if (standardMetricValue >= target) { if (standardMetricValue >= target) {
if (!hasUpper || standardMetricValue < upper.metrics[metric]) { upper = standard; hasUpper = true; } if (
!hasUpper ||
standardMetricValue < upper.metrics[metric]
) {
upper = standard;
hasUpper = true;
}
} }
} }
@@ -420,14 +529,16 @@ export class Standards {
if (!hasLower) lower = standards[0]; if (!hasLower) lower = standards[0];
if (!hasUpper) upper = standards[standards.length - 1]; if (!hasUpper) upper = standards[standards.length - 1];
return { lower, upper }; return { lower, upper };
} },
} },
}; };
const interpolateByAgeAndWeight = (metrics: Metrics): Standard => { const interpolateByAgeAndWeight = (metrics: Metrics): Standard => {
const interpolateByWeight = (targetAge: number): Levels => { const interpolateByWeight = (targetAge: number): Levels => {
const { lower: lowerAgeWeightStandard, upper: upperAgeWeightStandard } = this const {
.byActivity(activity) lower: lowerAgeWeightStandard,
upper: upperAgeWeightStandard,
} = this.byActivity(activity)
.byGender(metrics.gender) .byGender(metrics.gender)
.byAge(targetAge) .byAge(targetAge)
.getNearest("weight", metrics.weight); .getNearest("weight", metrics.weight);
@@ -435,49 +546,67 @@ export class Standards {
const weightLower = lowerAgeWeightStandard.metrics.weight; const weightLower = lowerAgeWeightStandard.metrics.weight;
const weightUpper = upperAgeWeightStandard.metrics.weight; const weightUpper = upperAgeWeightStandard.metrics.weight;
let weightRatio = weightUpper === weightLower ? 1 : (metrics.weight - weightLower) / (weightUpper - weightLower); let weightRatio =
weightUpper === weightLower
? 1
: (metrics.weight - weightLower) /
(weightUpper - weightLower);
weightRatio = Math.max(0, Math.min(1, weightRatio)); weightRatio = Math.max(0, Math.min(1, weightRatio));
// metrics.weight = lowerAgeWeightStandard.metrics.weight + (upperAgeWeightStandard.metrics.weight - lowerAgeWeightStandard.metrics.weight) * weightRatio; // metrics.weight = lowerAgeWeightStandard.metrics.weight + (upperAgeWeightStandard.metrics.weight - lowerAgeWeightStandard.metrics.weight) * weightRatio;
return this.interpolateLevels(lowerAgeWeightStandard.levels, upperAgeWeightStandard.levels, weightRatio); return this.interpolateLevels(
} lowerAgeWeightStandard.levels,
upperAgeWeightStandard.levels,
weightRatio,
);
};
// Find nearest age standards // Find nearest age standards
const { lower: lowerAgeStandard, upper: upperAgeStandard } = this const { lower: lowerAgeStandard, upper: upperAgeStandard } =
.byActivity(activity) this.byActivity(activity)
.byGender(metrics.gender) .byGender(metrics.gender)
.getNearest("age", metrics.age); .getNearest("age", metrics.age);
const ageLower = lowerAgeStandard.metrics.age; const ageLower = lowerAgeStandard.metrics.age;
const ageUpper = upperAgeStandard.metrics.age; const ageUpper = upperAgeStandard.metrics.age;
// Interpolate by age and weight // Interpolate by age and weight
let ageRatio = ageUpper === ageLower ? 1 : (metrics.age - ageLower) / (ageUpper - ageLower); let ageRatio =
ageUpper === ageLower
? 1
: (metrics.age - ageLower) / (ageUpper - ageLower);
ageRatio = Math.max(0, Math.min(1, ageRatio)); ageRatio = Math.max(0, Math.min(1, ageRatio));
const interpolatedLevels = this.interpolateLevels( const interpolatedLevels = this.interpolateLevels(
interpolateByWeight(ageLower), interpolateByWeight(ageLower),
interpolateByWeight(ageUpper), interpolateByWeight(ageUpper),
ageRatio ageRatio,
); );
return { return {
metrics: metrics, metrics: metrics,
levels: interpolatedLevels levels: interpolatedLevels,
} };
}; };
const getAllInterpolated = (opt?: { normalizeForLb?: boolean }): Standard[] => { const getAllInterpolated = (opt?: {
normalizeForLb?: boolean;
}): Standard[] => {
const metrics: Metrics = state as Metrics; const metrics: Metrics = state as Metrics;
let weights = this.weightsFor(activity, metrics.gender, metrics.age); let weights = this.weightsFor(
activity,
metrics.gender,
metrics.age,
);
if (opt?.normalizeForLb) { if (opt?.normalizeForLb) {
weights = weights.map(w => lbToKg(Math.round(kgToLb(w)))); weights = weights.map((w) => lbToKg(Math.round(kgToLb(w))));
} }
const interpolatedStandards: Standard[] = weights const interpolatedStandards: Standard[] = weights.map((weight) =>
.map(weight => interpolateByAgeAndWeight({ ...metrics, weight })); interpolateByAgeAndWeight({ ...metrics, weight }),
);
return interpolatedStandards; return interpolatedStandards;
}; };
@@ -494,31 +623,40 @@ export class Standards {
const byGender = (gender: Gender) => { const byGender = (gender: Gender) => {
state.gender = gender; state.gender = gender;
return { byAge, ...execMethods.exact }; return { byAge, ...execMethods.exact };
} };
const byAge = (age: number) => { const byAge = (age: number) => {
state.age = age; state.age = age;
return { byWeight, ...execMethods.exact, getAllInterpolated }; return { byWeight, ...execMethods.exact, getAllInterpolated };
} };
const byWeight = (weight: number) => { const byWeight = (weight: number) => {
state.weight = weight; state.weight = weight;
return { ...execMethods.exact, getOneInterpolated }; return { ...execMethods.exact, getOneInterpolated };
} };
const byMetrics = (metrics: Metrics) => { const byMetrics = (metrics: Metrics) => {
Object.assign(state, metrics); Object.assign(state, metrics);
return { ...execMethods.exact, getOneInterpolated }; return { ...execMethods.exact, getOneInterpolated };
} };
const initialMethods = { byGender, byMetrics, getMetadata, ...execMethods.exact }; const initialMethods = {
byGender,
byMetrics,
getMetadata,
...execMethods.exact,
};
return initialMethods; return initialMethods;
} }
public getAttributeActivities(attribute: Attribute): Activity[] { public getAttributeActivities(attribute: Attribute): Activity[] {
const activities: Activity[] = []; const activities: Activity[] = [];
for (const activity of Object.keys(this.activityStandards) as Activity[]) { for (const activity of Object.keys(
const activityStandard = this.activityStandards[activity] as ActivityStandards[Activity]; this.activityStandards,
) as Activity[]) {
const activityStandard = this.activityStandards[
activity
] as ActivityStandards[Activity];
if (activityStandard.metadata.attribute === attribute) { if (activityStandard.metadata.attribute === attribute) {
activities.push(activity as Activity); activities.push(activity as Activity);
} }
@@ -528,36 +666,33 @@ export class Standards {
// TODO: turn these into queries? // TODO: turn these into queries?
public weightsFor(activity: Activity, gender: Gender, age: number) { public weightsFor(activity: Activity, gender: Gender, age: number) {
const { lower, upper } = this const { lower, upper } = this.byActivity(activity)
.byActivity(activity)
.byGender(gender) .byGender(gender)
.getNearest("age", age); .getNearest("age", age);
const lowerDiff = Math.abs(lower.metrics.age - age); const lowerDiff = Math.abs(lower.metrics.age - age);
const upperDiff = Math.abs(upper.metrics.age - age); const upperDiff = Math.abs(upper.metrics.age - age);
const closestAge = lowerDiff < upperDiff ? const closestAge =
lower.metrics.age : lowerDiff < upperDiff ? lower.metrics.age : upper.metrics.age;
upper.metrics.age;
const weights = this const weights = this.byActivity(activity)
.byActivity(activity)
.byGender(gender) .byGender(gender)
.byAge(closestAge) .byAge(closestAge)
.getAll().map((s) => s.metrics.weight); .getAll()
.map((s) => s.metrics.weight);
return weights; return weights;
} }
public agesFor(activity: Activity, gender: Gender) { public agesFor(activity: Activity, gender: Gender) {
const standardsByGender = this.byActivity(activity).byGender(gender).getAll(); const standardsByGender = this.byActivity(activity)
const ages = [...new Set(standardsByGender.map(s => s.metrics.age))]; .byGender(gender)
.getAll();
const ages = [...new Set(standardsByGender.map((s) => s.metrics.age))];
return ages; return ages;
} }
private expandLevels( private expandLevels(levels: Levels, i: number): Levels {
levels: Levels,
i: number
): Levels {
if (i == 0) { if (i == 0) {
return levels; return levels;
} }
@@ -578,10 +713,7 @@ export class Standards {
return this.expandLevels(newLevels, i - 1); return this.expandLevels(newLevels, i - 1);
} }
private compressLevels( private compressLevels(levels: Levels, targetLevel: number): Levels {
levels: Levels,
targetLevel: number,
): Levels {
const levelsAmount = Object.keys(levels).length; const levelsAmount = Object.keys(levels).length;
if (levelsAmount < targetLevel) { if (levelsAmount < targetLevel) {
throw new Error( throw new Error(
@@ -601,7 +733,8 @@ export class Standards {
const upperValue = levels[upperIndex + 1]; const upperValue = levels[upperIndex + 1];
const weight = ratioIndex - lowerIndex; const weight = ratioIndex - lowerIndex;
compressedLevels[i + 1] = lowerValue + (upperValue - lowerValue) * weight; compressedLevels[i + 1] =
lowerValue + (upperValue - lowerValue) * weight;
} }
return compressedLevels; return compressedLevels;
@@ -613,9 +746,13 @@ export class Standards {
ratio: number, ratio: number,
): Levels { ): Levels {
if (Object.keys(lower).length !== Object.keys(upper).length) if (Object.keys(lower).length !== Object.keys(upper).length)
throw new Error("Cannot interpolate between varying number of levels"); throw new Error(
"Cannot interpolate between varying number of levels",
);
const lerp = (lvl: string) => (lower[lvl] as number) + ((upper[lvl] as number) - (lower[lvl] as number)) * ratio; const lerp = (lvl: string) =>
(lower[lvl] as number) +
((upper[lvl] as number) - (lower[lvl] as number)) * ratio;
const interpolatedLevels: Levels = {}; const interpolatedLevels: Levels = {};
for (const lvl in lower) { for (const lvl in lower) {
+12 -11
View File
@@ -1,5 +1,5 @@
import { type Metrics } from "$lib/services/calculator/main"; import { type Metrics } from "$lib/services/calculator/main";
import avgWeightDataRaw from "$lib/data/avg-weights.json" import avgWeightDataRaw from "$lib/data/avg-weights.json";
// ------------------------------------------------------------------------------------------------- // -------------------------------------------------------------------------------------------------
// DATA MODELS // DATA MODELS
@@ -8,7 +8,7 @@ import avgWeightDataRaw from "$lib/data/avg-weights.json"
export type Player = { export type Player = {
name?: string; name?: string;
metrics: Metrics; metrics: Metrics;
} };
export enum Attribute { export enum Attribute {
Strength = "Strength", Strength = "Strength",
@@ -33,7 +33,7 @@ export interface ActivityPerformance {
export enum Gender { export enum Gender {
Male = "Male", Male = "Male",
Female = "Female" Female = "Female",
} }
// ------------------------------------------------------------------------------------------------- // -------------------------------------------------------------------------------------------------
@@ -65,23 +65,24 @@ export const msToTime = (ms: number, includeMs = false): string => {
return formattedTime; return formattedTime;
}; };
export const range = (length: number) => Array.from({ length: length }, (_, i) => i + 1); export const range = (length: number) =>
Array.from({ length: length }, (_, i) => i + 1);
export const getAvgWeight = (gender: Gender, age: number) => { export const getAvgWeight = (gender: Gender, age: number) => {
type AvgWeightData = { type AvgWeightData = {
metadata: { metadata: {
unit: StandardUnit unit: StandardUnit;
}, };
weights: Metrics[] weights: Metrics[];
} };
let avgWeightData = avgWeightDataRaw as AvgWeightData; let avgWeightData = avgWeightDataRaw as AvgWeightData;
const avgWeights = avgWeightData.weights.filter(a => a.gender === gender); const avgWeights = avgWeightData.weights.filter((a) => a.gender === gender);
if (!avgWeights.length) throw new Error("No average weights"); if (!avgWeights.length) throw new Error("No average weights");
const closest = { const closest = {
diff: Math.abs(avgWeights[0].age - age), diff: Math.abs(avgWeights[0].age - age),
avg: avgWeights[0] avg: avgWeights[0],
}; };
for (const avg of avgWeights) { for (const avg of avgWeights) {
const diff = Math.abs(avg.age - age); const diff = Math.abs(avg.age - age);
@@ -92,4 +93,4 @@ export const getAvgWeight = (gender: Gender, age: number) => {
} }
return closest.avg.weight; return closest.avg.weight;
} };
+91 -51
View File
@@ -4,11 +4,16 @@ import { FetchError, type DeepPartial } from "./util/misc";
export default class PrintfulService { export default class PrintfulService {
static Products = class { static Products = class {
static Variants = class { static Variants = class {
static async get(variantId: number | string): Promise<Printful.Products.SyncVariant | undefined> { static async get(
const res = await fetch(`${env().API_URL}/store/variants/${variantId}`, { variantId: number | string,
method: "GET", ): Promise<Printful.Products.SyncVariant | undefined> {
headers: { ...env().AUTH_HEADERS } const res = await fetch(
}); `${env().API_URL}/store/variants/${variantId}`,
{
method: "GET",
headers: { ...env().AUTH_HEADERS },
},
);
if (res.status === 404 || res.status === 400) { if (res.status === 404 || res.status === 400) {
return; return;
@@ -18,32 +23,41 @@ export default class PrintfulService {
throw new FetchError("Failed to get Printful variant", res); throw new FetchError("Failed to get Printful variant", res);
} }
const payload: Printful.Products.MetaDataSingle<Printful.Products.SyncVariant> = await res.json(); const payload: Printful.Products.MetaDataSingle<Printful.Products.SyncVariant> =
await res.json();
return payload.result; return payload.result;
} }
} };
static async getAll(offset: number = 0): Promise<Printful.Products.SyncProduct[]> { static async getAll(
offset: number = 0,
): Promise<Printful.Products.SyncProduct[]> {
const allSyncProducts: Printful.Products.SyncProduct[] = []; const allSyncProducts: Printful.Products.SyncProduct[] = [];
while (true) { while (true) {
try { try {
const res = await fetch(`${env().API_URL}/store/products?offset=${offset}`, { const res = await fetch(
method: "GET", `${env().API_URL}/store/products?offset=${offset}`,
headers: { ...env().AUTH_HEADERS } {
}) method: "GET",
headers: { ...env().AUTH_HEADERS },
},
);
if (!res.ok) { if (!res.ok) {
throw new FetchError("Failed to get all Printful products", res); throw new FetchError(
"Failed to get all Printful products",
res,
);
} }
const payload: Printful.Products.MetaDataMulti<Printful.Products.SyncProduct> = await res.json(); const payload: Printful.Products.MetaDataMulti<Printful.Products.SyncProduct> =
await res.json();
allSyncProducts.push(...payload.result); allSyncProducts.push(...payload.result);
offset = allSyncProducts.length; offset = allSyncProducts.length;
if (allSyncProducts.length >= payload.paging.total) if (allSyncProducts.length >= payload.paging.total) break;
break;
} catch (error) { } catch (error) {
throw error; throw error;
} }
@@ -52,11 +66,16 @@ export default class PrintfulService {
return allSyncProducts; return allSyncProducts;
} }
static async get(productId: number | string): Promise<Printful.Products.Product | undefined> { static async get(
const res = await fetch(`${env().API_URL}/store/products/${productId}`, { productId: number | string,
method: "GET", ): Promise<Printful.Products.Product | undefined> {
headers: { ...env().AUTH_HEADERS } const res = await fetch(
}); `${env().API_URL}/store/products/${productId}`,
{
method: "GET",
headers: { ...env().AUTH_HEADERS },
},
);
if (res.status === 404 || res.status === 400) { if (res.status === 404 || res.status === 400) {
return; return;
@@ -66,25 +85,32 @@ export default class PrintfulService {
throw new FetchError("Failed to get Printful product", res); throw new FetchError("Failed to get Printful product", res);
} }
const payload: Printful.Products.MetaDataSingle<Printful.Products.Product> = await res.json(); const payload: Printful.Products.MetaDataSingle<Printful.Products.Product> =
await res.json();
return payload.result; return payload.result;
} }
static async update(printfulProductId: number, printfulProduct: DeepPartial<Printful.Products.Product>) { static async update(
const res = await fetch(`${env().API_URL}/store/products/${printfulProductId}`, { printfulProductId: number,
method: "PUT", printfulProduct: DeepPartial<Printful.Products.Product>,
headers: { ) {
"Content-Type": "application/json", const res = await fetch(
...env().AUTH_HEADERS `${env().API_URL}/store/products/${printfulProductId}`,
{
method: "PUT",
headers: {
"Content-Type": "application/json",
...env().AUTH_HEADERS,
},
body: JSON.stringify(printfulProduct),
}, },
body: JSON.stringify(printfulProduct) );
});
if (!res.ok) { if (!res.ok) {
throw new FetchError("Failed to update Printful product", res); throw new FetchError("Failed to update Printful product", res);
} }
} }
} };
static Orders = class { static Orders = class {
static async create(printfulOrder: Printful.Orders.Order) { static async create(printfulOrder: Printful.Orders.Order) {
@@ -92,9 +118,9 @@ export default class PrintfulService {
method: "POST", method: "POST",
headers: { headers: {
"Content-Type": "application/json", "Content-Type": "application/json",
...env().AUTH_HEADERS ...env().AUTH_HEADERS,
}, },
body: JSON.stringify(printfulOrder) body: JSON.stringify(printfulOrder),
}); });
if (!res.ok) { if (!res.ok) {
@@ -102,27 +128,35 @@ export default class PrintfulService {
} }
} }
static async getAll(offset: number = 0): Promise<Printful.Orders.Order[]> { static async getAll(
offset: number = 0,
): Promise<Printful.Orders.Order[]> {
const allPrintfulOrders: Printful.Orders.Order[] = []; const allPrintfulOrders: Printful.Orders.Order[] = [];
while (true) { while (true) {
try { try {
const res = await fetch(`${env().API_URL}/orders?offset=${offset}`, { const res = await fetch(
method: "GET", `${env().API_URL}/orders?offset=${offset}`,
headers: { ...env().AUTH_HEADERS } {
}) method: "GET",
headers: { ...env().AUTH_HEADERS },
},
);
if (!res.ok) { if (!res.ok) {
throw new FetchError("Failed to get all Printful orders", res); throw new FetchError(
"Failed to get all Printful orders",
res,
);
} }
const payload: Printful.Products.MetaDataMulti<Printful.Orders.Order> = await res.json(); const payload: Printful.Products.MetaDataMulti<Printful.Orders.Order> =
await res.json();
allPrintfulOrders.push(...payload.result); allPrintfulOrders.push(...payload.result);
offset = allPrintfulOrders.length; offset = allPrintfulOrders.length;
if (allPrintfulOrders.length >= payload.paging.total) if (allPrintfulOrders.length >= payload.paging.total) break;
break;
} catch (error) { } catch (error) {
throw error; throw error;
} }
@@ -130,32 +164,38 @@ export default class PrintfulService {
return allPrintfulOrders; return allPrintfulOrders;
} }
} };
static Util = class { static Util = class {
static getVariantMainImage(syncVariant: Printful.Products.SyncVariant): string { static getVariantMainImage(
const previewFile = syncVariant.files.find(f => f.type === "preview"); syncVariant: Printful.Products.SyncVariant,
): string {
const previewFile = syncVariant.files.find(
(f) => f.type === "preview",
);
return previewFile?.preview_url ?? syncVariant.product.image; return previewFile?.preview_url ?? syncVariant.product.image;
} }
} };
} }
export const env = () => { export const env = () => {
if (typeof Bun === "undefined") { if (typeof Bun === "undefined") {
throw new Error("Must be in a server context. Make sure to run using --bun."); throw new Error(
"Must be in a server context. Make sure to run using --bun.",
);
} }
const vars = { const vars = {
API_URL: "https://api.printful.com", API_URL: "https://api.printful.com",
AUTH_TOKEN: Bun.env.PRINTFUL_AUTH, AUTH_TOKEN: Bun.env.PRINTFUL_AUTH,
STORE_ID: Bun.env.PRINTFUL_STORE_ID STORE_ID: Bun.env.PRINTFUL_STORE_ID,
}; };
return { return {
...vars, ...vars,
AUTH_HEADERS: { AUTH_HEADERS: {
"Authorization": `Bearer ${vars.AUTH_TOKEN}`, Authorization: `Bearer ${vars.AUTH_TOKEN}`,
"X-PF-Store-Id": `${vars.STORE_ID}` "X-PF-Store-Id": `${vars.STORE_ID}`,
} },
}; };
}; };
+203 -141
View File
@@ -3,26 +3,26 @@ import { FetchError, formatSlug, type DeepPartial } from "./util/misc";
import WebflowService from "./webflow"; import WebflowService from "./webflow";
import PrintfulService from "./printful"; import PrintfulService from "./printful";
const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms)); const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
export const R2_WORKER_URL = "https://r2-worker.xominus.workers.dev"; export const R2_WORKER_URL = "https://r2-worker.xominus.workers.dev";
export const WEBSITE_MEDIA_URL = "https://website-media.bladeandbrawn.com"; export const WEBSITE_MEDIA_URL = "https://website-media.bladeandbrawn.com";
type MainProduct = { type MainProduct = {
name: string, name: string;
externalId: string, externalId: string;
colors: Set<string>, colors: Set<string>;
variants: { variants: {
color: string, color: string;
product: Printful.Products.Product product: Printful.Products.Product;
}[] }[];
} };
export default class SyncService { export default class SyncService {
static state = { static state = {
isSyncing: false, isSyncing: false,
syncingIds: [] as number[] syncingIds: [] as number[],
} };
static async sync(printfulProductId: number | undefined) { static async sync(printfulProductId: number | undefined) {
this.state.isSyncing = true; this.state.isSyncing = true;
@@ -32,43 +32,54 @@ export default class SyncService {
try { try {
let printfulProducts = await PrintfulService.Products.getAll(); let printfulProducts = await PrintfulService.Products.getAll();
if (printfulProductId) { if (printfulProductId) {
const printfulProduct = printfulProducts.find(p => p.id === printfulProductId); const printfulProduct = printfulProducts.find(
(p) => p.id === printfulProductId,
);
if (printfulProduct) { if (printfulProduct) {
const mainProductName = this.getMainProductName(printfulProduct.name); const mainProductName = this.getMainProductName(
printfulProducts = printfulProducts.filter(p => p.name.includes(mainProductName)); printfulProduct.name,
);
printfulProducts = printfulProducts.filter((p) =>
p.name.includes(mainProductName),
);
} }
} }
webflowProducts.push(...await WebflowService.Products.getAll()); webflowProducts.push(...(await WebflowService.Products.getAll()));
// Generate main printful products // Generate main printful products
console.log("Generating main products..."); console.log("Generating main products...");
for (const printfulProduct of printfulProducts) { for (const printfulProduct of printfulProducts) {
const mainProductName = this.getMainProductName(printfulProduct.name); const mainProductName = this.getMainProductName(
printfulProduct.name,
);
let mainPrintfulProduct = mainPrintfulProducts let mainPrintfulProduct = mainPrintfulProducts.find((mp) =>
.find(mp => mp.name.includes(mainProductName)); mp.name.includes(mainProductName),
);
if (!mainPrintfulProduct) { if (!mainPrintfulProduct) {
mainPrintfulProduct = { mainPrintfulProduct = {
name: mainProductName, name: mainProductName,
variants: [], variants: [],
externalId: printfulProduct.external_id, externalId: printfulProduct.external_id,
colors: new Set() colors: new Set(),
} };
mainPrintfulProducts.push(mainPrintfulProduct); mainPrintfulProducts.push(mainPrintfulProduct);
} }
const productColor = this.findColorInProductName(printfulProduct.name); const productColor = this.findColorInProductName(
printfulProduct.name,
);
mainPrintfulProduct.variants.push({ mainPrintfulProduct.variants.push({
color: productColor, color: productColor,
product: (await PrintfulService.Products.get(printfulProduct.id))! product: (await PrintfulService.Products.get(
printfulProduct.id,
))!,
}); });
mainPrintfulProduct.colors.add(productColor); mainPrintfulProduct.colors.add(productColor);
} }
} } catch (error) {
catch (error) {
throw error; throw error;
} } finally {
finally {
this.state.isSyncing = false; this.state.isSyncing = false;
this.state.syncingIds = []; this.state.syncingIds = [];
} }
@@ -80,19 +91,26 @@ export default class SyncService {
for (const mainPrintfulProduct of mainPrintfulProducts) { for (const mainPrintfulProduct of mainPrintfulProducts) {
try { try {
this.state.isSyncing = true; this.state.isSyncing = true;
this.state.syncingIds = mainPrintfulProduct.variants.map(v => v.product.sync_product.id); this.state.syncingIds = mainPrintfulProduct.variants.map(
(v) => v.product.sync_product.id,
);
const foundColors = mainPrintfulProduct.colors; const foundColors = mainPrintfulProduct.colors;
const foundSizes: Set<string> = new Set(); const foundSizes: Set<string> = new Set();
const webflowSkus: DeepPartial<Webflow.Products.Skus.Sku>[] = []; const webflowSkus: DeepPartial<Webflow.Products.Skus.Sku>[] =
[];
for (const specialVariant of mainPrintfulProduct.variants) { for (const specialVariant of mainPrintfulProduct.variants) {
for (const printfulVariant of specialVariant.product.sync_variants) { for (const printfulVariant of specialVariant.product
.sync_variants) {
// check if this variant size is in all other special variants // check if this variant size is in all other special variants
let sizeIsInAllVariants = true; let sizeIsInAllVariants = true;
for (const specialVariant of mainPrintfulProduct.variants) { for (const specialVariant of mainPrintfulProduct.variants) {
const sizes = specialVariant.product.sync_variants.map(v => v.size); const sizes =
specialVariant.product.sync_variants.map(
(v) => v.size,
);
if (!sizes.includes(printfulVariant.size)) if (!sizes.includes(printfulVariant.size))
sizeIsInAllVariants = false; sizeIsInAllVariants = false;
} }
@@ -101,29 +119,37 @@ export default class SyncService {
foundSizes.add(printfulVariant.size); foundSizes.add(printfulVariant.size);
webflowSkus.push({ webflowSkus.push({
"id": printfulVariant.external_id, id: printfulVariant.external_id,
"fieldData": { fieldData: {
"name": printfulVariant.name, name: printfulVariant.name,
"slug": formatSlug(printfulVariant.name), slug: formatSlug(printfulVariant.name),
"sku-values": { "sku-values": {
"color": specialVariant.color, color: specialVariant.color,
"size": printfulVariant.size, size: printfulVariant.size,
}, },
"price": { price: {
"value": Math.floor(+printfulVariant.retail_price * 100), value: Math.floor(
"unit": printfulVariant.currency, +printfulVariant.retail_price * 100,
"currency": printfulVariant.currency ),
unit: printfulVariant.currency,
currency: printfulVariant.currency,
}, },
"main-image": PrintfulService.Util.getVariantMainImage(printfulVariant) "main-image":
} PrintfulService.Util.getVariantMainImage(
}) printfulVariant,
),
},
});
} }
} }
} }
// SYNC PRODUCT DATA // SYNC PRODUCT DATA
let existingWebflowProduct = webflowProducts let existingWebflowProduct = webflowProducts.find(
.find(webflowProduct => webflowProduct.product.id === mainPrintfulProduct.externalId.split("-")[0]); (webflowProduct) =>
webflowProduct.product.id ===
mainPrintfulProduct.externalId.split("-")[0],
);
if (existingWebflowProduct) { if (existingWebflowProduct) {
// SYNC PRODUCT UPDATE // SYNC PRODUCT UPDATE
@@ -132,131 +158,165 @@ export default class SyncService {
delete webflowSku.fieldData?.["main-image"]; delete webflowSku.fieldData?.["main-image"];
} }
const webflowProductId = mainPrintfulProduct.externalId.split("-")[0]; const webflowProductId =
mainPrintfulProduct.externalId.split("-")[0];
WebflowService.Products.update(webflowProductId, { WebflowService.Products.update(webflowProductId, {
"product": { product: {
"fieldData": { fieldData: {
"name": mainPrintfulProduct.name, name: mainPrintfulProduct.name,
"slug": formatSlug(mainPrintfulProduct.name), slug: formatSlug(mainPrintfulProduct.name),
"shippable": true, shippable: true,
"tax-category": "standard-taxable", "tax-category": "standard-taxable",
"sku-properties": [ "sku-properties": [
{ {
"id": "color", id: "color",
"name": "Color", name: "Color",
"enum": Array.from(foundColors).map(color => ({ enum: Array.from(foundColors).map(
"id": color, (color) => ({
"slug": formatSlug(color), id: color,
"name": color slug: formatSlug(color),
})) name: color,
}),
),
}, },
{ {
"id": "size", id: "size",
"name": "Size", name: "Size",
"enum": Array.from(foundSizes).map(size => ({ enum: Array.from(foundSizes).map(
"id": size, (size) => ({
"slug": formatSlug(size), id: size,
"name": size slug: formatSlug(size),
})) name: size,
}),
),
}, },
] ],
} },
}, },
"sku": webflowSkus[0] sku: webflowSkus[0],
}); });
for (const webflowSku of webflowSkus.slice(1)) { for (const webflowSku of webflowSkus.slice(1)) {
const existingWebflowSku = existingWebflowProduct.skus.find(sku => sku.id === webflowSku.id); const existingWebflowSku =
existingWebflowProduct.skus.find(
(sku) => sku.id === webflowSku.id,
);
if (webflowSku.id && existingWebflowSku) { if (webflowSku.id && existingWebflowSku) {
await WebflowService.Products.Skus.update(webflowProductId, webflowSku.id, webflowSku); await WebflowService.Products.Skus.update(
} webflowProductId,
else { webflowSku.id,
await WebflowService.Products.Skus.create(webflowProductId, [webflowSku]); webflowSku,
);
} else {
await WebflowService.Products.Skus.create(
webflowProductId,
[webflowSku],
);
} }
} }
} } else {
else {
// SYNC PRODUCT CREATE // SYNC PRODUCT CREATE
const webflowProductId = await WebflowService.Products.create({ const webflowProductId =
"product": { await WebflowService.Products.create({
"fieldData": { product: {
"name": mainPrintfulProduct.name, fieldData: {
"slug": formatSlug(mainPrintfulProduct.name), name: mainPrintfulProduct.name,
"shippable": true, slug: formatSlug(mainPrintfulProduct.name),
"tax-category": "standard-taxable", shippable: true,
"sku-properties": [ "tax-category": "standard-taxable",
{ "sku-properties": [
"id": "color", {
"name": "Color", id: "color",
"enum": Array.from(foundColors).map(color => ({ name: "Color",
"id": color, enum: Array.from(foundColors).map(
"slug": formatSlug(color), (color) => ({
"name": color id: color,
})) slug: formatSlug(color),
}, name: color,
{ }),
"id": "size", ),
"name": "Size", },
"enum": Array.from(foundSizes).map(size => ({ {
"id": size, id: "size",
"slug": formatSlug(size), name: "Size",
"name": size enum: Array.from(foundSizes).map(
})) (size) => ({
}, id: size,
] slug: formatSlug(size),
} name: size,
}, }),
"sku": webflowSkus[0], ),
}); },
],
},
},
sku: webflowSkus[0],
});
// create webflow product SKUs // create webflow product SKUs
await WebflowService.Products.Skus.create(webflowProductId, webflowSkus.slice(1)); await WebflowService.Products.Skus.create(
webflowProductId,
webflowSkus.slice(1),
);
existingWebflowProduct = await WebflowService.Products.get(webflowProductId); existingWebflowProduct =
await WebflowService.Products.get(webflowProductId);
if (!existingWebflowProduct) { if (!existingWebflowProduct) {
console.error("Missing webflow product"); console.error("Missing webflow product");
throw new Error("Failed to create Webflow product"); throw new Error("Failed to create Webflow product");
} }
for (const specialVariant of mainPrintfulProduct.variants) { for (const specialVariant of mainPrintfulProduct.variants) {
const newPrintfulVariants: DeepPartial<Printful.Products.SyncVariant>[] = []; const newPrintfulVariants: DeepPartial<Printful.Products.SyncVariant>[] =
for (const printfulVariant of specialVariant.product.sync_variants) { [];
const associatedWebflowSku = existingWebflowProduct.skus for (const printfulVariant of specialVariant.product
.find(sku => sku.fieldData["sku-values"]?.["color"] === printfulVariant.color && .sync_variants) {
sku.fieldData["sku-values"]?.["size"] === printfulVariant.size); const associatedWebflowSku =
existingWebflowProduct.skus.find(
(sku) =>
sku.fieldData["sku-values"]?.[
"color"
] === printfulVariant.color &&
sku.fieldData["sku-values"]?.[
"size"
] === printfulVariant.size,
);
if (associatedWebflowSku) { if (associatedWebflowSku) {
newPrintfulVariants.push({ newPrintfulVariants.push({
"id": printfulVariant.id, // printful variant id id: printfulVariant.id, // printful variant id
"external_id": String(associatedWebflowSku.id), // webflow variant id external_id: String(
associatedWebflowSku.id,
), // webflow variant id
}); });
} }
} }
await sleep(10000); await sleep(10000);
await PrintfulService.Products.update(specialVariant.product.sync_product.id, { await PrintfulService.Products.update(
"sync_product": { specialVariant.product.sync_product.id,
"id": specialVariant.product.sync_product.id, {
"external_id": `${webflowProductId}-${specialVariant.color}` sync_product: {
id: specialVariant.product.sync_product.id,
external_id: `${webflowProductId}-${specialVariant.color}`,
},
sync_variants: newPrintfulVariants,
}, },
"sync_variants": newPrintfulVariants );
});
} }
} }
} } catch (err) {
// SYNC IMAGES
// SYNC IMAGES // for (const sku of existingWebflowProduct.skus) {
// for (const sku of existingWebflowProduct.skus) { // const skuColor = sku.fieldData["sku-values"]?.["color"]?.toLowerCase() ?? "None";
// const skuColor = sku.fieldData["sku-values"]?.["color"]?.toLowerCase() ?? "None"; // const skuSlug = `${existingWebflowProduct.product.fieldData.slug}-${skuColor}`;
// const skuSlug = `${existingWebflowProduct.product.fieldData.slug}-${skuColor}`; // const skuImageUrls = await this.getProductImageUrls(skuSlug);
// const skuImageUrls = await this.getProductImageUrls(skuSlug); //
// // sku.fieldData["main-image"] = skuImageUrls[0] ?? sku.fieldData["main-image"];
// sku.fieldData["main-image"] = skuImageUrls[0] ?? sku.fieldData["main-image"]; // sku.fieldData["more-images"] = skuImageUrls.slice(1).map(url => ({ url }));
// sku.fieldData["more-images"] = skuImageUrls.slice(1).map(url => ({ url })); //
// // await WebflowService.Products.Skus.update(existingWebflowProduct.product.id, sku.id, sku);
// await WebflowService.Products.Skus.update(existingWebflowProduct.product.id, sku.id, sku); // }
// } //
//
catch (err) {
if (err instanceof FetchError) { if (err instanceof FetchError) {
console.error(err.message, err.payload); console.error(err.message, err.payload);
} }
@@ -268,14 +328,17 @@ export default class SyncService {
} }
private static getProductImageUrls = async (slug: string) => { private static getProductImageUrls = async (slug: string) => {
const res = await (fetch(`${R2_WORKER_URL}/product-images/${slug}`)); const res = await fetch(`${R2_WORKER_URL}/product-images/${slug}`);
if (!res.ok) { if (!res.ok) {
console.error("Failed to get product image keys:", await res.json()); console.error(
"Failed to get product image keys:",
await res.json(),
);
throw new Error("Failed to get product image keys"); throw new Error("Failed to get product image keys");
} }
const productImageKeys: string[] = await res.json(); const productImageKeys: string[] = await res.json();
return productImageKeys.map(key => `${WEBSITE_MEDIA_URL}/${key}`); return productImageKeys.map((key) => `${WEBSITE_MEDIA_URL}/${key}`);
} };
public static findColorInProductName = (productName: string): string => { public static findColorInProductName = (productName: string): string => {
const m = productName.match(/\[([^\]]+)\]/); const m = productName.match(/\[([^\]]+)\]/);
@@ -286,8 +349,7 @@ export default class SyncService {
const colorInName = this.findColorInProductName(productName); const colorInName = this.findColorInProductName(productName);
if (colorInName) { if (colorInName) {
return productName.replace(`[${colorInName}]`, "").trimEnd(); return productName.replace(`[${colorInName}]`, "").trimEnd();
} } else {
else {
return productName; return productName;
} }
}; };
+162 -129
View File
@@ -1,21 +1,26 @@
export type DeepPartial<T> = T extends object ? { export type DeepPartial<T> = T extends object
[P in keyof T]?: DeepPartial<T[P]>; ? {
} : T; [P in keyof T]?: DeepPartial<T[P]>;
}
: T;
export const formatSlug = (name: string): string => { export const formatSlug = (name: string): string => {
return name return name
.toLowerCase() .toLowerCase()
.replace(/[^a-z0-9-_]/g, '-') // replace illegal chars with dashes .replace(/[^a-z0-9-_]/g, "-") // replace illegal chars with dashes
.replace(/^-+/, '') // remove leading dashes .replace(/^-+/, "") // remove leading dashes
.replace(/-+$/, '') // remove trailing dashes .replace(/-+$/, "") // remove trailing dashes
.replace(/--+/g, '-') // collapse multiple dashes .replace(/--+/g, "-") // collapse multiple dashes
.replace(/^([^a-z0-9_])/, '_$1'); // if it starts with an invalid char, prefix underscore .replace(/^([^a-z0-9_])/, "_$1"); // if it starts with an invalid char, prefix underscore
}; };
export class FetchError extends Error { export class FetchError extends Error {
public payload: Object | undefined; public payload: Object | undefined;
constructor(message: string, public response: Response) { constructor(
message: string,
public response: Response,
) {
super(message); super(message);
this.name = "FetchError"; this.name = "FetchError";
} }
@@ -25,24 +30,22 @@ export class FetchError extends Error {
this.payload = await this.response.clone().json(); this.payload = await this.response.clone().json();
} catch { } catch {
this.payload = await this.response.text().catch(() => undefined); this.payload = await this.response.text().catch(() => undefined);
} } finally {
finally {
return this.payload; return this.payload;
} }
} }
} }
export function getStateFromZip(zipString: string): string { export function getStateFromZip(zipString: string): string {
/* Ensure param is a string to prevent unpredictable parsing results */ /* Ensure param is a string to prevent unpredictable parsing results */
if (typeof zipString !== 'string') { if (typeof zipString !== "string") {
console.error('Must pass the zipcode as a string.'); console.error("Must pass the zipcode as a string.");
return "none"; return "none";
} }
/* Ensure we have exactly 5 characters to parse */ /* Ensure we have exactly 5 characters to parse */
if (zipString.length !== 5) { if (zipString.length !== 5) {
console.error('Must pass a 5-digit zipcode.'); console.error("Must pass a 5-digit zipcode.");
return "none"; return "none";
} }
@@ -54,164 +57,194 @@ export function getStateFromZip(zipString: string): string {
/* Code cases alphabetized by state */ /* Code cases alphabetized by state */
if (zipcode >= 35000 && zipcode <= 36999) { if (zipcode >= 35000 && zipcode <= 36999) {
st = 'AL'; st = "AL";
state = 'Alabama'; state = "Alabama";
} else if (zipcode >= 99500 && zipcode <= 99999) { } else if (zipcode >= 99500 && zipcode <= 99999) {
st = 'AK'; st = "AK";
state = 'Alaska'; state = "Alaska";
} else if (zipcode >= 85000 && zipcode <= 86999) { } else if (zipcode >= 85000 && zipcode <= 86999) {
st = 'AZ'; st = "AZ";
state = 'Arizona'; state = "Arizona";
} else if (zipcode >= 71600 && zipcode <= 72999) { } else if (zipcode >= 71600 && zipcode <= 72999) {
st = 'AR'; st = "AR";
state = 'Arkansas'; state = "Arkansas";
} else if (zipcode >= 90000 && zipcode <= 96699) { } else if (zipcode >= 90000 && zipcode <= 96699) {
st = 'CA'; st = "CA";
state = 'California'; state = "California";
} else if (zipcode >= 80000 && zipcode <= 81999) { } else if (zipcode >= 80000 && zipcode <= 81999) {
st = 'CO'; st = "CO";
state = 'Colorado'; state = "Colorado";
} else if ((zipcode >= 6000 && zipcode <= 6389) || (zipcode >= 6391 && zipcode <= 6999)) { } else if (
st = 'CT'; (zipcode >= 6000 && zipcode <= 6389) ||
state = 'Connecticut'; (zipcode >= 6391 && zipcode <= 6999)
) {
st = "CT";
state = "Connecticut";
} else if (zipcode >= 19700 && zipcode <= 19999) { } else if (zipcode >= 19700 && zipcode <= 19999) {
st = 'DE'; st = "DE";
state = 'Delaware'; state = "Delaware";
} else if (zipcode >= 32000 && zipcode <= 34999) { } else if (zipcode >= 32000 && zipcode <= 34999) {
st = 'FL'; st = "FL";
state = 'Florida'; state = "Florida";
} else if ((zipcode >= 30000 && zipcode <= 31999) || (zipcode >= 39800 && zipcode <= 39999)) { } else if (
st = 'GA'; (zipcode >= 30000 && zipcode <= 31999) ||
state = 'Georgia'; (zipcode >= 39800 && zipcode <= 39999)
) {
st = "GA";
state = "Georgia";
} else if (zipcode >= 96700 && zipcode <= 96999) { } else if (zipcode >= 96700 && zipcode <= 96999) {
st = 'HI'; st = "HI";
state = 'Hawaii'; state = "Hawaii";
} else if (zipcode >= 83200 && zipcode <= 83999 && zipcode != 83414) { } else if (zipcode >= 83200 && zipcode <= 83999 && zipcode != 83414) {
st = 'ID'; st = "ID";
state = 'Idaho'; state = "Idaho";
} else if (zipcode >= 60000 && zipcode <= 62999) { } else if (zipcode >= 60000 && zipcode <= 62999) {
st = 'IL'; st = "IL";
state = 'Illinois'; state = "Illinois";
} else if (zipcode >= 46000 && zipcode <= 47999) { } else if (zipcode >= 46000 && zipcode <= 47999) {
st = 'IN'; st = "IN";
state = 'Indiana'; state = "Indiana";
} else if (zipcode >= 50000 && zipcode <= 52999) { } else if (zipcode >= 50000 && zipcode <= 52999) {
st = 'IA'; st = "IA";
state = 'Iowa'; state = "Iowa";
} else if (zipcode >= 66000 && zipcode <= 67999) { } else if (zipcode >= 66000 && zipcode <= 67999) {
st = 'KS'; st = "KS";
state = 'Kansas'; state = "Kansas";
} else if (zipcode >= 40000 && zipcode <= 42999) { } else if (zipcode >= 40000 && zipcode <= 42999) {
st = 'KY'; st = "KY";
state = 'Kentucky'; state = "Kentucky";
} else if (zipcode >= 70000 && zipcode <= 71599) { } else if (zipcode >= 70000 && zipcode <= 71599) {
st = 'LA'; st = "LA";
state = 'Louisiana'; state = "Louisiana";
} else if (zipcode >= 3900 && zipcode <= 4999) { } else if (zipcode >= 3900 && zipcode <= 4999) {
st = 'ME'; st = "ME";
state = 'Maine'; state = "Maine";
} else if (zipcode >= 20600 && zipcode <= 21999) { } else if (zipcode >= 20600 && zipcode <= 21999) {
st = 'MD'; st = "MD";
state = 'Maryland'; state = "Maryland";
} else if ((zipcode >= 1000 && zipcode <= 2799) || (zipcode == 5501) || (zipcode == 5544)) { } else if (
st = 'MA'; (zipcode >= 1000 && zipcode <= 2799) ||
state = 'Massachusetts'; zipcode == 5501 ||
zipcode == 5544
) {
st = "MA";
state = "Massachusetts";
} else if (zipcode >= 48000 && zipcode <= 49999) { } else if (zipcode >= 48000 && zipcode <= 49999) {
st = 'MI'; st = "MI";
state = 'Michigan'; state = "Michigan";
} else if (zipcode >= 55000 && zipcode <= 56899) { } else if (zipcode >= 55000 && zipcode <= 56899) {
st = 'MN'; st = "MN";
state = 'Minnesota'; state = "Minnesota";
} else if (zipcode >= 38600 && zipcode <= 39999) { } else if (zipcode >= 38600 && zipcode <= 39999) {
st = 'MS'; st = "MS";
state = 'Mississippi'; state = "Mississippi";
} else if (zipcode >= 63000 && zipcode <= 65999) { } else if (zipcode >= 63000 && zipcode <= 65999) {
st = 'MO'; st = "MO";
state = 'Missouri'; state = "Missouri";
} else if (zipcode >= 59000 && zipcode <= 59999) { } else if (zipcode >= 59000 && zipcode <= 59999) {
st = 'MT'; st = "MT";
state = 'Montana'; state = "Montana";
} else if (zipcode >= 27000 && zipcode <= 28999) { } else if (zipcode >= 27000 && zipcode <= 28999) {
st = 'NC'; st = "NC";
state = 'North Carolina'; state = "North Carolina";
} else if (zipcode >= 58000 && zipcode <= 58999) { } else if (zipcode >= 58000 && zipcode <= 58999) {
st = 'ND'; st = "ND";
state = 'North Dakota'; state = "North Dakota";
} else if (zipcode >= 68000 && zipcode <= 69999) { } else if (zipcode >= 68000 && zipcode <= 69999) {
st = 'NE'; st = "NE";
state = 'Nebraska'; state = "Nebraska";
} else if (zipcode >= 88900 && zipcode <= 89999) { } else if (zipcode >= 88900 && zipcode <= 89999) {
st = 'NV'; st = "NV";
state = 'Nevada'; state = "Nevada";
} else if (zipcode >= 3000 && zipcode <= 3899) { } else if (zipcode >= 3000 && zipcode <= 3899) {
st = 'NH'; st = "NH";
state = 'New Hampshire'; state = "New Hampshire";
} else if (zipcode >= 7000 && zipcode <= 8999) { } else if (zipcode >= 7000 && zipcode <= 8999) {
st = 'NJ'; st = "NJ";
state = 'New Jersey'; state = "New Jersey";
} else if (zipcode >= 87000 && zipcode <= 88499) { } else if (zipcode >= 87000 && zipcode <= 88499) {
st = 'NM'; st = "NM";
state = 'New Mexico'; state = "New Mexico";
} else if ((zipcode >= 10000 && zipcode <= 14999) || (zipcode == 6390) || (zipcode == 501) || (zipcode == 544)) { } else if (
st = 'NY'; (zipcode >= 10000 && zipcode <= 14999) ||
state = 'New York'; zipcode == 6390 ||
zipcode == 501 ||
zipcode == 544
) {
st = "NY";
state = "New York";
} else if (zipcode >= 43000 && zipcode <= 45999) { } else if (zipcode >= 43000 && zipcode <= 45999) {
st = 'OH'; st = "OH";
state = 'Ohio'; state = "Ohio";
} else if ((zipcode >= 73000 && zipcode <= 73199) || (zipcode >= 73400 && zipcode <= 74999)) { } else if (
st = 'OK'; (zipcode >= 73000 && zipcode <= 73199) ||
state = 'Oklahoma'; (zipcode >= 73400 && zipcode <= 74999)
) {
st = "OK";
state = "Oklahoma";
} else if (zipcode >= 97000 && zipcode <= 97999) { } else if (zipcode >= 97000 && zipcode <= 97999) {
st = 'OR'; st = "OR";
state = 'Oregon'; state = "Oregon";
} else if (zipcode >= 15000 && zipcode <= 19699) { } else if (zipcode >= 15000 && zipcode <= 19699) {
st = 'PA'; st = "PA";
state = 'Pennsylvania'; state = "Pennsylvania";
} else if (zipcode >= 300 && zipcode <= 999) { } else if (zipcode >= 300 && zipcode <= 999) {
st = 'PR'; st = "PR";
state = 'Puerto Rico'; state = "Puerto Rico";
} else if (zipcode >= 2800 && zipcode <= 2999) { } else if (zipcode >= 2800 && zipcode <= 2999) {
st = 'RI'; st = "RI";
state = 'Rhode Island'; state = "Rhode Island";
} else if (zipcode >= 29000 && zipcode <= 29999) { } else if (zipcode >= 29000 && zipcode <= 29999) {
st = 'SC'; st = "SC";
state = 'South Carolina'; state = "South Carolina";
} else if (zipcode >= 57000 && zipcode <= 57999) { } else if (zipcode >= 57000 && zipcode <= 57999) {
st = 'SD'; st = "SD";
state = 'South Dakota'; state = "South Dakota";
} else if (zipcode >= 37000 && zipcode <= 38599) { } else if (zipcode >= 37000 && zipcode <= 38599) {
st = 'TN'; st = "TN";
state = 'Tennessee'; state = "Tennessee";
} else if ((zipcode >= 75000 && zipcode <= 79999) || (zipcode >= 73301 && zipcode <= 73399) || (zipcode >= 88500 && zipcode <= 88599)) { } else if (
st = 'TX'; (zipcode >= 75000 && zipcode <= 79999) ||
state = 'Texas'; (zipcode >= 73301 && zipcode <= 73399) ||
(zipcode >= 88500 && zipcode <= 88599)
) {
st = "TX";
state = "Texas";
} else if (zipcode >= 84000 && zipcode <= 84999) { } else if (zipcode >= 84000 && zipcode <= 84999) {
st = 'UT'; st = "UT";
state = 'Utah'; state = "Utah";
} else if (zipcode >= 5000 && zipcode <= 5999) { } else if (zipcode >= 5000 && zipcode <= 5999) {
st = 'VT'; st = "VT";
state = 'Vermont'; state = "Vermont";
} else if ((zipcode >= 20100 && zipcode <= 20199) || (zipcode >= 22000 && zipcode <= 24699) || (zipcode == 20598)) { } else if (
st = 'VA'; (zipcode >= 20100 && zipcode <= 20199) ||
state = 'Virginia'; (zipcode >= 22000 && zipcode <= 24699) ||
} else if ((zipcode >= 20000 && zipcode <= 20099) || (zipcode >= 20200 && zipcode <= 20599) || (zipcode >= 56900 && zipcode <= 56999)) { zipcode == 20598
st = 'DC'; ) {
state = 'Washington DC'; st = "VA";
state = "Virginia";
} else if (
(zipcode >= 20000 && zipcode <= 20099) ||
(zipcode >= 20200 && zipcode <= 20599) ||
(zipcode >= 56900 && zipcode <= 56999)
) {
st = "DC";
state = "Washington DC";
} else if (zipcode >= 98000 && zipcode <= 99499) { } else if (zipcode >= 98000 && zipcode <= 99499) {
st = 'WA'; st = "WA";
state = 'Washington'; state = "Washington";
} else if (zipcode >= 24700 && zipcode <= 26999) { } else if (zipcode >= 24700 && zipcode <= 26999) {
st = 'WV'; st = "WV";
state = 'West Virginia'; state = "West Virginia";
} else if (zipcode >= 53000 && zipcode <= 54999) { } else if (zipcode >= 53000 && zipcode <= 54999) {
st = 'WI'; st = "WI";
state = 'Wisconsin'; state = "Wisconsin";
} else if ((zipcode >= 82000 && zipcode <= 83199) || zipcode == 83414) { } else if ((zipcode >= 82000 && zipcode <= 83199) || zipcode == 83414) {
st = 'WY'; st = "WY";
state = 'Wyoming'; state = "Wyoming";
} else { } else {
st = 'none'; st = "none";
state = 'none'; state = "none";
} }
/* Return `state` for full name or `st` for postal abbreviation */ /* Return `state` for full name or `st` for postal abbreviation */
+238 -216
View File
@@ -11,16 +11,16 @@ export namespace Printful {
code: number; code: number;
result: T[]; result: T[];
paging: { paging: {
total: number, total: number;
offset: number, offset: number;
limit: number limit: number;
} };
} }
type Option = { type Option = {
id: string; id: string;
value: string; value: string;
} };
type File = { type File = {
type: string; type: string;
@@ -41,12 +41,12 @@ export namespace Printful {
visible: boolean; visible: boolean;
is_temporary: boolean; is_temporary: boolean;
stitch_count_tier: string; stitch_count_tier: string;
} };
export type Product = { export type Product = {
sync_product: SyncProduct, sync_product: SyncProduct;
sync_variants: SyncVariant[] sync_variants: SyncVariant[];
} };
export type SyncProduct = { export type SyncProduct = {
id: number; id: number;
@@ -56,7 +56,7 @@ export namespace Printful {
synced: number; synced: number;
thumbnail_url: string; thumbnail_url: string;
is_ignored: boolean; is_ignored: boolean;
} };
export type SyncVariant = { export type SyncVariant = {
id: number; id: number;
@@ -83,30 +83,30 @@ export namespace Printful {
size: string; size: string;
color: string; color: string;
availability_status: string; availability_status: string;
} };
} }
export namespace Orders { export namespace Orders {
export type Order = { export type Order = {
external_id: string, external_id: string;
shipping: string shipping: string;
recipient: { recipient: {
name: string name: string;
company?: string company?: string;
address1: string address1: string;
address2: string address2: string;
city: string city: string;
state_code: string state_code: string;
state_name?: string state_name?: string;
country_code: string country_code: string;
country_name?: string country_name?: string;
zip: string zip: string;
phone?: string phone?: string;
email?: string email?: string;
tax_number?: string tax_number?: string;
} };
items: Array<{ external_variant_id: string, quantity: number }> items: Array<{ external_variant_id: string; quantity: number }>;
} };
} }
export namespace Webhook { export namespace Webhook {
@@ -116,55 +116,70 @@ export namespace Printful {
created: number; created: number;
retries: number; retries: number;
store: number; store: number;
data: D data: D;
} }
export enum Event { export enum Event {
ProductUpdated = "product_updated", ProductUpdated = "product_updated",
ProductDeleted = "product_deleted", ProductDeleted = "product_deleted",
PackageShipped = "package_shipped" PackageShipped = "package_shipped",
} }
export interface ProductUpdated extends MetaData<Event.ProductUpdated, { export interface ProductUpdated
sync_product: { extends MetaData<
id: number; Event.ProductUpdated,
external_id: string; {
name: string; sync_product: {
variants: number; id: number;
synced: number; external_id: string;
thumbnail_url: string; name: string;
is_ignored: boolean; variants: number;
}; synced: number;
}> { } thumbnail_url: string;
is_ignored: boolean;
};
}
> {}
export interface ProductDeleted extends MetaData<Event.ProductDeleted, { export interface ProductDeleted
sync_product: { extends MetaData<
id: number; Event.ProductDeleted,
external_id: string; {
name: string; sync_product: {
}; id: number;
}> { } external_id: string;
name: string;
};
}
> {}
export interface PackageShipped extends MetaData<Event.PackageShipped, { export interface PackageShipped
shipment: { extends MetaData<
id: number Event.PackageShipped,
carrier: string, {
service: string, shipment: {
tracking_number: string id: number;
tracking_url: string carrier: string;
created: number service: string;
ship_date: string tracking_number: string;
shipped_at: number tracking_url: string;
reshipment: boolean created: number;
} ship_date: string;
order: { shipped_at: number;
id: number reshipment: boolean;
external_id: string };
status: string order: {
} id: number;
}> { } external_id: string;
status: string;
};
}
> {}
export type EventPayload = ProductUpdated | ProductDeleted | PackageShipped export type EventPayload =
| ProductUpdated
| ProductDeleted
| PackageShipped;
} }
} }
@@ -172,29 +187,28 @@ export namespace Webflow {
export namespace Products { export namespace Products {
export namespace Skus { export namespace Skus {
export type Sku = { export type Sku = {
id: string, id: string;
fieldData: { fieldData: {
name: string; name: string;
slug: string; slug: string;
"sku-values"?: Record<string, string>, "sku-values"?: Record<string, string>;
price: { price: {
value: number; value: number;
unit: string; unit: string;
currency: string; currency: string;
}; };
"main-image"?: string; "main-image"?: string;
"more-images"?: "more-images"?: {
{ fileId?: string;
fileId?: string, url: string;
url: string, alt?: string;
alt?: string, }[];
}[] };
} };
}
} }
export type Product = { export type Product = {
id: string, id: string;
fieldData: { fieldData: {
name: string; name: string;
slug: string; slug: string;
@@ -210,12 +224,12 @@ export namespace Webflow {
slug: string; slug: string;
}[]; }[];
}[]; }[];
} };
} };
export interface ProductAndSkus { export interface ProductAndSkus {
product: Product product: Product;
skus: Skus.Sku[] skus: Skus.Sku[];
} }
export interface ProductAndSku { export interface ProductAndSku {
@@ -227,85 +241,91 @@ export namespace Webflow {
export namespace Orders { export namespace Orders {
export type Order = { export type Order = {
orderId: string orderId: string;
status: "pending" | "unfulfilled" | "fulfilled" | "disputed" | "dispute-lost" | "refunded" status:
comment: string | "pending"
orderComment: string | "unfulfilled"
acceptedOn: string | "fulfilled"
fulfilledOn: string | null | "disputed"
refundedOn: string | null | "dispute-lost"
disputedOn: string | null | "refunded";
disputeUpdatedOn: string | null comment: string;
disputeLastStatus: string | null orderComment: string;
acceptedOn: string;
fulfilledOn: string | null;
refundedOn: string | null;
disputedOn: string | null;
disputeUpdatedOn: string | null;
disputeLastStatus: string | null;
customerPaid: { customerPaid: {
unit: string unit: string;
value: string value: string;
string: string string: string;
} };
netAmount: { netAmount: {
unit: string unit: string;
value: string value: string;
string: string string: string;
} };
applicationFee: { applicationFee: {
unit: string unit: string;
value: string value: string;
string: string string: string;
} };
allAddresses: Array<{ allAddresses: Array<{
type: string type: string;
addressee: string addressee: string;
line1: string line1: string;
line2: string line2: string;
city: string city: string;
state: string state: string;
country: string country: string;
postalCode: string postalCode: string;
}> }>;
shippingAddress: { shippingAddress: {
type: string type: string;
japanType: string japanType: string;
addressee: string addressee: string;
line1: string line1: string;
line2: string line2: string;
city: string city: string;
state: string state: string;
country: string country: string;
postalCode: string postalCode: string;
} };
billingAddress: { billingAddress: {
type: string type: string;
addressee: string addressee: string;
line1: string line1: string;
line2: string line2: string;
city: string city: string;
state: string state: string;
country: string country: string;
postalCode: string postalCode: string;
} };
shippingProvider: string shippingProvider: string;
shippingTracking: string shippingTracking: string;
shippingTrackingURL: string shippingTrackingURL: string;
customerInfo: { customerInfo: {
fullName: string fullName: string;
email: string email: string;
} };
purchasedItems: Array<{ purchasedItems: Array<{
count: number count: number;
rowTotal: { rowTotal: {
unit: string unit: string;
value: string value: string;
string: string string: string;
} };
productId: string productId: string;
productName: string productName: string;
productSlug: string productSlug: string;
variantId: string variantId: string;
variantName: string variantName: string;
variantSlug: string variantSlug: string;
variantSKU: string variantSKU: string;
variantImage: { variantImage: {
url: string url: string;
file: { file: {
size: number; size: number;
originalFileName: string; originalFileName: string;
@@ -315,95 +335,97 @@ export namespace Webflow {
height: number; height: number;
variants: { variants: {
size: number; size: number;
url: string, url: string;
originalFileName: string; originalFileName: string;
width: number; width: number;
height: number; height: number;
}[] }[];
} };
} };
variantPrice: { variantPrice: {
unit: string unit: string;
value: string value: string;
string: string string: string;
} };
weight: number weight: number;
width: number width: number;
height: number height: number;
length: number length: number;
}> }>;
purchasedItemsCount: number purchasedItemsCount: number;
stripeDetails: { stripeDetails: {
subscriptionId: string | null subscriptionId: string | null;
paymentMethod: string paymentMethod: string;
paymentIntentId: string paymentIntentId: string;
customerId: string customerId: string;
chargeId: string chargeId: string;
disputeId: string | null disputeId: string | null;
refundId: string refundId: string;
refundReason: string refundReason: string;
} };
stripeCard: { stripeCard: {
last4: string last4: string;
brand: string brand: string;
ownerName: string ownerName: string;
expires: { expires: {
year: number year: number;
month: number month: number;
} };
} };
paypalDetails: Object paypalDetails: Object;
customData: Array<Object> customData: Array<Object>;
metadata: { metadata: {
isBuyNow: boolean isBuyNow: boolean;
hasDownloads: boolean hasDownloads: boolean;
paymentProcessor: string paymentProcessor: string;
} };
isCustomerDeleted: boolean isCustomerDeleted: boolean;
isShippingRequired: boolean isShippingRequired: boolean;
totals: { totals: {
subtotal: { subtotal: {
unit: string unit: string;
value: string value: string;
string: string string: string;
} };
extras: Array<{ extras: Array<{
type: string type: string;
name: string name: string;
description: string description: string;
price: { price: {
unit: string unit: string;
value: string value: string;
string: string string: string;
} };
}> }>;
total: { total: {
unit: string unit: string;
value: string value: string;
string: string string: string;
} };
} };
downloadFiles: Array<{ downloadFiles: Array<{
id: string id: string;
name: string name: string;
url: string url: string;
}> }>;
} };
} }
export namespace Webhook { export namespace Webhook {
interface MetaData<E extends Event, D> { interface MetaData<E extends Event, D> {
triggerType: E; triggerType: E;
payload: D payload: D;
} }
export enum Event { export enum Event {
OrderCreated = "ecomm_new_order", OrderCreated = "ecomm_new_order",
OrderUpdated = "ecomm_order_changed" OrderUpdated = "ecomm_order_changed",
} }
export interface OrderCreated extends MetaData<Event.OrderCreated, Orders.Order> { } export interface OrderCreated
export interface OrderUpdated extends MetaData<Event.OrderUpdated, Orders.Order> { } extends MetaData<Event.OrderCreated, Orders.Order> {}
export interface OrderUpdated
extends MetaData<Event.OrderUpdated, Orders.Order> {}
export type EventPayload = OrderCreated | OrderUpdated; export type EventPayload = OrderCreated | OrderUpdated;
} }
+150 -86
View File
@@ -5,48 +5,69 @@ import crypto from "node:crypto";
export default class WebflowService { export default class WebflowService {
static Products = class { static Products = class {
static Skus = class { static Skus = class {
static async create(webflowProductId: string, skus: DeepPartial<Webflow.Products.Skus.Sku>[]) { static async create(
const res = await fetch(`${env().API_SITES_URL}/products/${webflowProductId}/skus`, { webflowProductId: string,
method: "POST", skus: DeepPartial<Webflow.Products.Skus.Sku>[],
headers: { ) {
"Content-Type": "application/json", const res = await fetch(
...env().AUTH_HEADER `${env().API_SITES_URL}/products/${webflowProductId}/skus`,
{
method: "POST",
headers: {
"Content-Type": "application/json",
...env().AUTH_HEADER,
},
body: JSON.stringify({
skus: skus,
}),
}, },
body: JSON.stringify({ );
"skus": skus
})
});
if (!res.ok) { if (!res.ok) {
throw new FetchError("Failed to create Webflow product SKU", res); throw new FetchError(
"Failed to create Webflow product SKU",
res,
);
} }
} }
static async update(webflowProductId: string, webflowSkuId: string, webflowSku: DeepPartial<Webflow.Products.Skus.Sku>) { static async update(
const res = await fetch(`${env().API_SITES_URL}/products/${webflowProductId}/skus/${webflowSkuId}`, { webflowProductId: string,
method: "PATCH", webflowSkuId: string,
headers: { webflowSku: DeepPartial<Webflow.Products.Skus.Sku>,
"Content-Type": "application/json", ) {
...env().AUTH_HEADER const res = await fetch(
`${env().API_SITES_URL}/products/${webflowProductId}/skus/${webflowSkuId}`,
{
method: "PATCH",
headers: {
"Content-Type": "application/json",
...env().AUTH_HEADER,
},
body: JSON.stringify({
sku: webflowSku,
}),
}, },
body: JSON.stringify({ );
"sku": webflowSku
})
});
if (!res.ok) { if (!res.ok) {
throw new FetchError("Failed to update Webflow product SKU", res); throw new FetchError(
"Failed to update Webflow product SKU",
res,
);
} }
} }
} };
static async create(webflowProductAndSku: DeepPartial<Webflow.Products.ProductAndSku>) { static async create(
webflowProductAndSku: DeepPartial<Webflow.Products.ProductAndSku>,
) {
const res = await fetch(`${env().API_SITES_URL}/products`, { const res = await fetch(`${env().API_SITES_URL}/products`, {
method: "POST", method: "POST",
headers: { headers: {
"Content-Type": "application/json", "Content-Type": "application/json",
...env().AUTH_HEADER ...env().AUTH_HEADER,
}, },
body: JSON.stringify(webflowProductAndSku) body: JSON.stringify(webflowProductAndSku),
}); });
if (!res.ok) { if (!res.ok) {
@@ -54,7 +75,7 @@ export default class WebflowService {
} }
const createdWebflowProduct = await res.json(); const createdWebflowProduct = await res.json();
return createdWebflowProduct.product.id return createdWebflowProduct.product.id;
} }
static async getAll(): Promise<Webflow.Products.ProductAndSkus[]> { static async getAll(): Promise<Webflow.Products.ProductAndSkus[]> {
@@ -62,7 +83,7 @@ export default class WebflowService {
method: "GET", method: "GET",
headers: { headers: {
...env().AUTH_HEADER, ...env().AUTH_HEADER,
} },
}); });
if (!res.ok) { if (!res.ok) {
@@ -73,13 +94,18 @@ export default class WebflowService {
return payload.items as Webflow.Products.ProductAndSkus[]; return payload.items as Webflow.Products.ProductAndSkus[];
} }
static async get(webflowProductId: string): Promise<Webflow.Products.ProductAndSkus | undefined> { static async get(
const res = await fetch(`${env().API_SITES_URL}/products/${webflowProductId}`, { webflowProductId: string,
method: "GET", ): Promise<Webflow.Products.ProductAndSkus | undefined> {
headers: { const res = await fetch(
...env().AUTH_HEADER, `${env().API_SITES_URL}/products/${webflowProductId}`,
} {
}); method: "GET",
headers: {
...env().AUTH_HEADER,
},
},
);
if (res.status === 404 || res.status === 400) { if (res.status === 404 || res.status === 400) {
return; return;
@@ -93,15 +119,21 @@ export default class WebflowService {
return payload as Webflow.Products.ProductAndSkus; return payload as Webflow.Products.ProductAndSkus;
} }
static async update(webflowProductId: string, webflowProduct: DeepPartial<Webflow.Products.ProductAndSku>) { static async update(
const res = await fetch(`${env().API_SITES_URL}/products/${webflowProductId}`, { webflowProductId: string,
method: "PATCH", webflowProduct: DeepPartial<Webflow.Products.ProductAndSku>,
headers: { ) {
"Content-Type": "application/json", const res = await fetch(
...env().AUTH_HEADER `${env().API_SITES_URL}/products/${webflowProductId}`,
{
method: "PATCH",
headers: {
"Content-Type": "application/json",
...env().AUTH_HEADER,
},
body: JSON.stringify(webflowProduct),
}, },
body: JSON.stringify(webflowProduct) );
});
if (!res.ok) { if (!res.ok) {
throw new FetchError("Failed to update Webflow product", res); throw new FetchError("Failed to update Webflow product", res);
@@ -109,33 +141,41 @@ export default class WebflowService {
} }
static async remove(webflowProductId: string) { static async remove(webflowProductId: string) {
const res = await fetch(`${env().API_COLLECTIONS_URL}/items/${webflowProductId}`, { const res = await fetch(
method: "DELETE", `${env().API_COLLECTIONS_URL}/items/${webflowProductId}`,
headers: { {
"Content-Type": "application/json", method: "DELETE",
...env().AUTH_HEADER headers: {
"Content-Type": "application/json",
...env().AUTH_HEADER,
},
body: JSON.stringify({}),
}, },
body: JSON.stringify({}) );
});
if (!res.ok) { if (!res.ok) {
throw new FetchError("Failed to remove Webflow product", res); throw new FetchError("Failed to remove Webflow product", res);
} }
} }
} };
static Orders = class { static Orders = class {
static async getAll(opt: { status?: Webflow.Orders.Order["status"] } = {}): Promise<Webflow.Orders.Order[]> { static async getAll(
opt: { status?: Webflow.Orders.Order["status"] } = {},
): Promise<Webflow.Orders.Order[]> {
// TODO: pagination // TODO: pagination
const params = new URLSearchParams(); const params = new URLSearchParams();
if (opt.status !== undefined) { if (opt.status !== undefined) {
params.set("status", opt.status); params.set("status", opt.status);
} }
const res = await fetch(`${env().API_SITES_URL}/orders?${params.toString()}`, { const res = await fetch(
method: "GET", `${env().API_SITES_URL}/orders?${params.toString()}`,
headers: { ...env().AUTH_HEADER }, {
}); method: "GET",
headers: { ...env().AUTH_HEADER },
},
);
if (!res.ok) { if (!res.ok) {
throw new FetchError("Failed to get all Webflow orders", res); throw new FetchError("Failed to get all Webflow orders", res);
@@ -145,36 +185,53 @@ export default class WebflowService {
return payload.orders as Webflow.Orders.Order[]; return payload.orders as Webflow.Orders.Order[];
} }
static async update(webflowOrderId: string, webflowOrderUpdate: { comment?: string, shippingProvider?: string, shippingTracking?: string, shippingTrackingURL: string }) { static async update(
const res = await fetch(`${env().API_SITES_URL}/orders/${webflowOrderId}`, { webflowOrderId: string,
method: "PATCH", webflowOrderUpdate: {
headers: { comment?: string;
"Content-Type": "application/json", shippingProvider?: string;
...env().AUTH_HEADER shippingTracking?: string;
shippingTrackingURL: string;
},
) {
const res = await fetch(
`${env().API_SITES_URL}/orders/${webflowOrderId}`,
{
method: "PATCH",
headers: {
"Content-Type": "application/json",
...env().AUTH_HEADER,
},
body: JSON.stringify(webflowOrderUpdate),
}, },
body: JSON.stringify(webflowOrderUpdate) );
});
if (!res.ok) { if (!res.ok) {
throw new FetchError("Failed to update Webflow order", res); throw new FetchError("Failed to update Webflow order", res);
} }
} }
static async fulfill(webflowOrderId: string, opt: { sendOrderFulfilledEmail?: boolean } = {}) { static async fulfill(
const res = await fetch(`${env().API_SITES_URL}/orders/${webflowOrderId}/fulfill`, { webflowOrderId: string,
method: "POST", opt: { sendOrderFulfilledEmail?: boolean } = {},
headers: { ) {
"Content-Type": "application/json", const res = await fetch(
...env().AUTH_HEADER `${env().API_SITES_URL}/orders/${webflowOrderId}/fulfill`,
{
method: "POST",
headers: {
"Content-Type": "application/json",
...env().AUTH_HEADER,
},
body: JSON.stringify(opt),
}, },
body: JSON.stringify(opt) );
});
if (!res.ok) { if (!res.ok) {
throw new FetchError("Failed to fulfill Webflow order", res); throw new FetchError("Failed to fulfill Webflow order", res);
} }
} }
} };
static Util = class { static Util = class {
static verifyWebflowSignature(request: Request, body: unknown) { static verifyWebflowSignature(request: Request, body: unknown) {
@@ -183,7 +240,9 @@ export default class WebflowService {
if (!timestamp) { if (!timestamp) {
throw new Error("No timestamp provided"); throw new Error("No timestamp provided");
} }
const providedSignature = request.headers.get("x-webflow-signature"); const providedSignature = request.headers.get(
"x-webflow-signature",
);
if (!providedSignature) { if (!providedSignature) {
throw new Error("No signature provided"); throw new Error("No signature provided");
} }
@@ -199,47 +258,52 @@ export default class WebflowService {
const requestTimestamp = parseInt(timestamp, 10); const requestTimestamp = parseInt(timestamp, 10);
const data = `${requestTimestamp}:${JSON.stringify(body)}`; const data = `${requestTimestamp}:${JSON.stringify(body)}`;
const hash = crypto.createHmac('sha256', secret) const hash = crypto
.createHmac("sha256", secret)
.update(data) .update(data)
.digest('hex'); .digest("hex");
if (!crypto.timingSafeEqual(Buffer.from(hash, 'hex'), Buffer.from(providedSignature, 'hex'))) { if (
throw new Error('Invalid signature'); !crypto.timingSafeEqual(
Buffer.from(hash, "hex"),
Buffer.from(providedSignature, "hex"),
)
) {
throw new Error("Invalid signature");
} }
const currentTime = Date.now(); const currentTime = Date.now();
if (currentTime - requestTimestamp > 300000) { if (currentTime - requestTimestamp > 300000) {
throw new Error('Request is older than 5 minutes'); throw new Error("Request is older than 5 minutes");
} }
return true; return true;
} catch (err) { } catch (err) {
console.error(`Error verifying signature: ${err}`); console.error(`Error verifying signature: ${err}`);
return false; return false;
} }
} }
} };
} }
const env = () => { const env = () => {
if (typeof Bun === "undefined") { if (typeof Bun === "undefined") {
throw new Error("Must be in a server context. Make sure to run using --bun."); throw new Error(
"Must be in a server context. Make sure to run using --bun.",
);
} }
const vars = { const vars = {
SITE_ID: Bun.env.WEBFLOW_SITE_ID, SITE_ID: Bun.env.WEBFLOW_SITE_ID,
COLLECTIONS_ID: Bun.env.WEBFLOW_COLLECTION_ID, COLLECTIONS_ID: Bun.env.WEBFLOW_COLLECTION_ID,
AUTH_TOKEN: Bun.env.WEBFLOW_AUTH, AUTH_TOKEN: Bun.env.WEBFLOW_AUTH,
WEBHOOK_SECRET: Bun.env.WEBFLOW_WEBHOOK_SECRET WEBHOOK_SECRET: Bun.env.WEBFLOW_WEBHOOK_SECRET,
}; };
return { return {
...vars, ...vars,
API_SITES_URL: `https://api.webflow.com/v2/sites/${vars.SITE_ID}`, API_SITES_URL: `https://api.webflow.com/v2/sites/${vars.SITE_ID}`,
API_COLLECTIONS_URL: `https://api.webflow.com/v2/collections/${vars.COLLECTIONS_ID}`, API_COLLECTIONS_URL: `https://api.webflow.com/v2/collections/${vars.COLLECTIONS_ID}`,
AUTH_HEADER: { "Authorization": `bearer ${vars.AUTH_TOKEN}` } AUTH_HEADER: { Authorization: `bearer ${vars.AUTH_TOKEN}` },
}; };
}; };
+17 -6
View File
@@ -1,25 +1,37 @@
import { Activity, ftToCm, Gender, getAvgWeight, inToCm, kgToLb, lbToKg, minToMs, secToMs, type ActivityPerformance, type Player } from "$lib/services/calculator/util"; import {
Activity,
ftToCm,
Gender,
getAvgWeight,
inToCm,
kgToLb,
lbToKg,
minToMs,
secToMs,
type ActivityPerformance,
type Player,
} from "$lib/services/calculator/util";
import PrintfulService from "$lib/services/commerce/printful"; import PrintfulService from "$lib/services/commerce/printful";
import SyncService from "$lib/services/commerce/sync"; import SyncService from "$lib/services/commerce/sync";
import { FetchError, type DeepPartial } from "$lib/services/commerce/util/misc"; import { FetchError, type DeepPartial } from "$lib/services/commerce/util/misc";
import type { Printful } from "$lib/services/commerce/util/types"; import type { Printful } from "$lib/services/commerce/util/types";
import WebflowService from "$lib/services/commerce/webflow"; import WebflowService from "$lib/services/commerce/webflow";
const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms)); const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
const player: Player = { const player: Player = {
metrics: { metrics: {
age: 25, age: 25,
weight: lbToKg(190), weight: lbToKg(190),
gender: Gender.Male gender: Gender.Male,
} },
}; };
const computedPerformances: ActivityPerformance[] = [ const computedPerformances: ActivityPerformance[] = [
// STRENGTH // STRENGTH
{ {
activity: Activity.BenchPress, activity: Activity.BenchPress,
performance: lbToKg(225) performance: lbToKg(225),
}, },
{ {
activity: Activity.Deadlift, activity: Activity.Deadlift,
@@ -115,7 +127,6 @@ console.log(pOrders);
// } // }
// console.log("DONE"); // console.log("DONE");
// await WebflowService.Products.update(wProduct?.product.id!, wProduct!); // await WebflowService.Products.update(wProduct?.product.id!, wProduct!);
// //
// const sku = { // const sku = {
+26 -18
View File
@@ -2,12 +2,12 @@ import PrintfulService from "$lib/services/commerce/printful";
import SyncService from "$lib/services/commerce/sync"; import SyncService from "$lib/services/commerce/sync";
import WebflowService from "$lib/services/commerce/webflow"; import WebflowService from "$lib/services/commerce/webflow";
const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms)); const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
type Validation = { type Validation = {
isValid: boolean, isValid: boolean;
message: string message: string;
} };
const validateEquality = (wValue: any, pValue: any): Validation => { const validateEquality = (wValue: any, pValue: any): Validation => {
const validation = { const validation = {
@@ -17,12 +17,11 @@ const validateEquality = (wValue: any, pValue: any): Validation => {
if (wValue !== pValue) { if (wValue !== pValue) {
validation.isValid = false; validation.isValid = false;
validation.message = `${wValue} !== ${pValue}` validation.message = `${wValue} !== ${pValue}`;
}; }
return validation; return validation;
} };
const wProducts = await WebflowService.Products.getAll(); const wProducts = await WebflowService.Products.getAll();
@@ -31,14 +30,25 @@ for (const wProduct of wProducts) {
for (const sku of wProduct.skus ?? []) { for (const sku of wProduct.skus ?? []) {
console.log("Webflow: " + sku.id, sku.fieldData.name); console.log("Webflow: " + sku.id, sku.fieldData.name);
const pVariant = await PrintfulService.Products.Variants.get(`@${sku.id}`); const pVariant = await PrintfulService.Products.Variants.get(
`@${sku.id}`,
);
if (pVariant) { if (pVariant) {
console.log("Printful: " + pVariant.id, pVariant.name); console.log("Printful: " + pVariant.id, pVariant.name);
const validations = [ const validations = [
// validateEquality(sku.fieldData.name, pVariant.name), // validateEquality(sku.fieldData.name, pVariant.name),
validateEquality(sku.fieldData["sku-values"]?.["color"], SyncService.findColorInProductName(pVariant.name)), validateEquality(
validateEquality(sku.fieldData["sku-values"]?.["size"], pVariant.size), sku.fieldData["sku-values"]?.["color"],
validateEquality(sku.fieldData.price.value, Math.floor(+pVariant.retail_price * 100)), SyncService.findColorInProductName(pVariant.name),
),
validateEquality(
sku.fieldData["sku-values"]?.["size"],
pVariant.size,
),
validateEquality(
sku.fieldData.price.value,
Math.floor(+pVariant.retail_price * 100),
),
]; ];
// if (!validateEquality(sku.fieldData.name, pVariant.name).isValid) { // if (!validateEquality(sku.fieldData.name, pVariant.name).isValid) {
@@ -46,17 +56,15 @@ for (const wProduct of wProducts) {
// await WebflowService.Products.Skus.update(wProduct.product.id, sku.id, sku); // await WebflowService.Products.Skus.update(wProduct.product.id, sku.id, sku);
// } // }
const errors = validations.filter(v => !v.isValid); const errors = validations.filter((v) => !v.isValid);
if (errors.length) { if (errors.length) {
++invalidCount; ++invalidCount;
console.log("INVALID: "); console.log("INVALID: ");
console.log(errors.map(e => e.message).join(", ")); console.log(errors.map((e) => e.message).join(", "));
} } else {
else {
console.log("VALID"); console.log("VALID");
} }
} } else {
else {
++invalidCount; ++invalidCount;
console.log("INVALID"); console.log("INVALID");
console.log("Not found"); console.log("Not found");
+4 -4
View File
@@ -1,6 +1,6 @@
import { redirect } from '@sveltejs/kit'; import { redirect } from "@sveltejs/kit";
import type { PageServerLoad } from './$types'; import type { PageServerLoad } from "./$types";
export const load: PageServerLoad = ({ }) => { export const load: PageServerLoad = ({}) => {
redirect(308, '/calculator'); redirect(308, "/calculator");
}; };
+3 -3
View File
@@ -1,4 +1,4 @@
import { app } from "$lib/server/app" import { app } from "$lib/server/app";
type RequestHandler = (v: { request: Request }) => Response | Promise<Response> type RequestHandler = (v: { request: Request }) => Response | Promise<Response>;
export const fallback: RequestHandler = ({ request }) => app.handle(request) export const fallback: RequestHandler = ({ request }) => app.handle(request);
+276 -276
View File
@@ -1,309 +1,309 @@
<script lang="ts"> <script lang="ts">
import { import {
Standards, Standards,
type ActivityStandards, type ActivityStandards,
type Metrics, type Metrics,
type StandardsConfig, type StandardsConfig,
} from "$lib/services/calculator/main"; } from "$lib/services/calculator/main";
import allStandardsRaw from "$lib/data/standards.json" assert { type: "json" }; import allStandardsRaw from "$lib/data/standards.json" assert { type: "json" };
import { Activity, Gender, lbToKg } from "$lib/services/calculator/util"; import { Activity, Gender, lbToKg } from "$lib/services/calculator/util";
import StandardsTable from "$lib/components/StandardsTable.svelte"; import StandardsTable from "$lib/components/StandardsTable.svelte";
import PlayersTable from "$lib/components/PlayersTable.svelte"; import PlayersTable from "$lib/components/PlayersTable.svelte";
let activeTab = $state("standards"); let activeTab = $state("standards");
const defaultStandards = new Standards( const defaultStandards = new Standards(
allStandardsRaw as ActivityStandards, allStandardsRaw as ActivityStandards,
); );
let input = $state({ let input = $state({
activity: Activity.BenchPress, activity: Activity.BenchPress,
metrics: { metrics: {
gender: Gender.Male, gender: Gender.Male,
age: "", age: "",
weight: "", weight: "",
}, },
cfg: { cfg: {
global: { global: {
maxLevel: String(defaultStandards.cfg.global.maxLevel), maxLevel: String(defaultStandards.cfg.global.maxLevel),
}, },
activity: Object.fromEntries( activity: Object.fromEntries(
Object.values(Activity).map((activity) => { Object.values(Activity).map((activity) => {
return [ return [
activity, activity,
{ {
enableGeneration: enableGeneration:
defaultStandards.cfg.activity[activity] defaultStandards.cfg.activity[activity]
.enableGeneration, .enableGeneration,
weightAdvantage: String( weightAdvantage: String(
defaultStandards.cfg.activity[activity] defaultStandards.cfg.activity[activity]
.weightModifier, .weightModifier,
), ),
ageModifier: String( ageModifier: String(
defaultStandards.cfg.activity[activity] defaultStandards.cfg.activity[activity]
.ageModifier, .ageModifier,
), ),
difficultyModifier: String( difficultyModifier: String(
defaultStandards.cfg.activity[activity] defaultStandards.cfg.activity[activity]
.difficultyModifier, .difficultyModifier,
), ),
}, },
]; ];
}), }),
), ),
}, },
}); });
const selected = $derived({ const selected = $derived({
activity: input.activity, activity: input.activity,
metrics: { metrics: {
gender: input.metrics.gender, gender: input.metrics.gender,
age: +input.metrics.age, age: +input.metrics.age,
weight: lbToKg(+input.metrics.weight), weight: lbToKg(+input.metrics.weight),
} as Metrics, } as Metrics,
cfg: { cfg: {
global: { global: {
maxLevel: +input.cfg.global.maxLevel, maxLevel: +input.cfg.global.maxLevel,
}, },
activity: Object.fromEntries( activity: Object.fromEntries(
Object.values(Activity).map((activity) => [ Object.values(Activity).map((activity) => [
activity, activity,
{ {
weightModifier: weightModifier:
+input.cfg.activity[activity].weightAdvantage, +input.cfg.activity[activity].weightAdvantage,
ageModifier: +input.cfg.activity[activity].ageModifier, ageModifier: +input.cfg.activity[activity].ageModifier,
enableGeneration: enableGeneration:
input.cfg.activity[activity].enableGeneration, input.cfg.activity[activity].enableGeneration,
difficultyModifier: difficultyModifier:
+input.cfg.activity[activity].difficultyModifier, +input.cfg.activity[activity].difficultyModifier,
}, },
]), ]),
), ),
} as StandardsConfig, } as StandardsConfig,
}); });
const allStandards = $derived( const allStandards = $derived(
new Standards(allStandardsRaw as ActivityStandards, selected.cfg), new Standards(allStandardsRaw as ActivityStandards, selected.cfg),
); );
$effect(() => { $effect(() => {
console.log(selected); console.log(selected);
if (!input.metrics.age) input.metrics.weight = ""; if (!input.metrics.age) input.metrics.weight = "";
}); });
$effect(() => { $effect(() => {
const savedParameters = localStorage.getItem("parameters"); const savedParameters = localStorage.getItem("parameters");
if (savedParameters) input = JSON.parse(savedParameters); if (savedParameters) input = JSON.parse(savedParameters);
}); });
$effect(() => { $effect(() => {
localStorage.setItem("parameters", JSON.stringify(input)); localStorage.setItem("parameters", JSON.stringify(input));
}); });
</script> </script>
<section class="w-full flex justify-between gap-2 pt-10 items-center"> <section class="w-full flex justify-between gap-2 pt-10 items-center">
<fieldset class="fieldset bg-base-200 p-6 max-w-lg"> <fieldset class="fieldset bg-base-200 p-6 max-w-lg">
<legend class="fieldset-legend text-lg font-semibold"> Data </legend> <legend class="fieldset-legend text-lg font-semibold"> Data </legend>
<div class="flex gap-4"> <div class="flex gap-4">
<fieldset class="fieldset bg-base-300 p-3 max-w-xl"> <fieldset class="fieldset bg-base-300 p-3 max-w-xl">
<legend class="fieldset-legend text-sm font-semibold"> <legend class="fieldset-legend text-sm font-semibold">
General General
</legend> </legend>
<div class="grid grid-cols-1 gap-4"> <div class="grid grid-cols-1 gap-4">
<label> <label>
<span class="label mb-1">Max level</span> <span class="label mb-1">Max level</span>
<input <input
class="input w-full" class="input w-full"
type="number" type="number"
min="1" min="1"
max="100" max="100"
placeholder="5" placeholder="5"
bind:value={input.cfg.global.maxLevel} bind:value={input.cfg.global.maxLevel}
/> />
</label> </label>
</div> </div>
</fieldset> </fieldset>
<fieldset class="fieldset bg-base-300 p-3 max-w-3xs"> <fieldset class="fieldset bg-base-300 p-3 max-w-3xs">
<legend class="fieldset-legend text-sm font-semibold"> <legend class="fieldset-legend text-sm font-semibold">
Data Generation Data Generation
</legend> </legend>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4"> <div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<label class="label"> <label class="label">
<input <input
type="checkbox" type="checkbox"
bind:checked={ bind:checked={
input.cfg.activity[selected.activity] input.cfg.activity[selected.activity]
.enableGeneration .enableGeneration
} }
class="toggle toggle-lg m-auto" class="toggle toggle-lg m-auto"
/> />
</label> </label>
<label> <label>
<span class="label mb-1">Weight significance</span> <span class="label mb-1">Weight significance</span>
<input <input
class="input w-full" class="input w-full"
type="number" type="number"
min="-1" min="-1"
max="1" max="1"
step=".05" step=".05"
placeholder=".1" placeholder=".1"
bind:value={ bind:value={
input.cfg.activity[selected.activity] input.cfg.activity[selected.activity]
.weightAdvantage .weightAdvantage
} }
disabled={!selected.cfg.activity[selected.activity] disabled={!selected.cfg.activity[selected.activity]
.enableGeneration} .enableGeneration}
/> />
</label> </label>
<!-- <label> --> <!-- <label> -->
<!-- <span class="label mb-1">Weight skew</span> --> <!-- <span class="label mb-1">Weight skew</span> -->
<!-- <input --> <!-- <input -->
<!-- class="input w-full" --> <!-- class="input w-full" -->
<!-- type="number" --> <!-- type="number" -->
<!-- min="-1" --> <!-- min="-1" -->
<!-- max="1" --> <!-- max="1" -->
<!-- step=".05" --> <!-- step=".05" -->
<!-- bind:value={ --> <!-- bind:value={ -->
<!-- inputSelected.cfg.activity[selected.activity] --> <!-- inputSelected.cfg.activity[selected.activity] -->
<!-- .weightSkew --> <!-- .weightSkew -->
<!-- } --> <!-- } -->
<!-- disabled={!selected.cfg.activity[selected.activity] --> <!-- disabled={!selected.cfg.activity[selected.activity] -->
<!-- .enableGeneration} --> <!-- .enableGeneration} -->
<!-- /> --> <!-- /> -->
<!-- </label> --> <!-- </label> -->
<label> <label>
<span class="label mb-1">Difficulty modifier</span> <span class="label mb-1">Difficulty modifier</span>
<input <input
class="input w-full" class="input w-full"
type="number" type="number"
min="-1" min="-1"
max="1" max="1"
step=".05" step=".05"
bind:value={ bind:value={
input.cfg.activity[selected.activity] input.cfg.activity[selected.activity]
.difficultyModifier .difficultyModifier
} }
disabled={!selected.cfg.activity[selected.activity] disabled={!selected.cfg.activity[selected.activity]
.enableGeneration} .enableGeneration}
/> />
</label> </label>
<label> <label>
<span class="label mb-1">Age significance</span> <span class="label mb-1">Age significance</span>
<input <input
class="input w-full" class="input w-full"
type="number" type="number"
min="-1" min="-1"
max="1" max="1"
step=".05" step=".05"
bind:value={ bind:value={
input.cfg.activity[selected.activity] input.cfg.activity[selected.activity]
.ageModifier .ageModifier
} }
disabled={!selected.cfg.activity[selected.activity] disabled={!selected.cfg.activity[selected.activity]
.enableGeneration} .enableGeneration}
/> />
</label> </label>
</div> </div>
</fieldset> </fieldset>
</div> </div>
</fieldset> </fieldset>
{#if activeTab === "standards"} {#if activeTab === "standards"}
<fieldset class="fieldset bg-base-200 p-6 max-w-lg"> <fieldset class="fieldset bg-base-200 p-6 max-w-lg">
<legend class="fieldset-legend text-lg font-semibold"> <legend class="fieldset-legend text-lg font-semibold">
Metrics Metrics
</legend> </legend>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4"> <div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<label> <label>
<span class="label mb-1">Activity</span> <span class="label mb-1">Activity</span>
<select <select
class="select w-full" class="select w-full"
name="activities" name="activities"
bind:value={input.activity} bind:value={input.activity}
> >
{#each Object.values(Activity) as activity} {#each Object.values(Activity) as activity}
<option value={activity}> <option value={activity}>
{allStandards.byActivity(activity).getMetadata() {allStandards.byActivity(activity).getMetadata()
.name} .name}
</option> </option>
{/each} {/each}
</select> </select>
</label> </label>
<label> <label>
<span class="label mb-1">Gender</span> <span class="label mb-1">Gender</span>
<select <select
class="select w-full" class="select w-full"
name="genders" name="genders"
bind:value={input.metrics.gender} bind:value={input.metrics.gender}
> >
{#each Object.values(Gender) as gender} {#each Object.values(Gender) as gender}
<option value={gender}>{gender}</option> <option value={gender}>{gender}</option>
{/each} {/each}
</select> </select>
</label> </label>
<label> <label>
<span class="label mb-1">Age</span> <span class="label mb-1">Age</span>
<input <input
class="input w-full" class="input w-full"
type="number" type="number"
min="0" min="0"
max="100" max="100"
placeholder="18" placeholder="18"
bind:value={input.metrics.age} bind:value={input.metrics.age}
/> />
</label> </label>
<label> <label>
<span class="label mb-1">Weight (lb)</span> <span class="label mb-1">Weight (lb)</span>
<input <input
class="input w-full" class="input w-full"
type="number" type="number"
min="0" min="0"
max="600" max="600"
placeholder={selected.metrics.age placeholder={selected.metrics.age
? "170" ? "170"
: "Requires age"} : "Requires age"}
bind:value={input.metrics.weight} bind:value={input.metrics.weight}
disabled={!selected.metrics.age} disabled={!selected.metrics.age}
/> />
</label> </label>
</div> </div>
</fieldset> </fieldset>
{/if} {/if}
</section> </section>
<div class="divider"></div> <div class="divider"></div>
<div role="tablist" class="tabs tabs-border"> <div role="tablist" class="tabs tabs-border">
<button <button
role="tab" role="tab"
class="tab" class="tab"
class:tab-active={activeTab === "standards"} class:tab-active={activeTab === "standards"}
onclick={() => (activeTab = "standards")} onclick={() => (activeTab = "standards")}
> >
Standards Standards
</button> </button>
<button <button
role="tab" role="tab"
class="tab" class="tab"
class:tab-active={activeTab === "players"} class:tab-active={activeTab === "players"}
onclick={() => (activeTab = "players")} onclick={() => (activeTab = "players")}
> >
Players Players
</button> </button>
</div> </div>
{#if activeTab === "standards"} {#if activeTab === "standards"}
<StandardsTable {selected} {allStandards} /> <StandardsTable {selected} {allStandards} />
{/if} {/if}
{#if activeTab === "players"} {#if activeTab === "players"}
<PlayersTable {allStandards} /> <PlayersTable {allStandards} />
{/if} {/if}
+10 -10
View File
@@ -1,12 +1,12 @@
import PrintfulService from '$lib/services/commerce/printful'; import PrintfulService from "$lib/services/commerce/printful";
import WebflowService from '$lib/services/commerce/webflow'; import WebflowService from "$lib/services/commerce/webflow";
import type { PageServerLoad } from './$types'; import type { PageServerLoad } from "./$types";
export const load = async ({ }: Parameters<PageServerLoad>[0]) => { export const load = async ({}: Parameters<PageServerLoad>[0]) => {
return { return {
products: { products: {
printful: PrintfulService.Products.getAll(), printful: PrintfulService.Products.getAll(),
webflow: WebflowService.Products.getAll(), webflow: WebflowService.Products.getAll(),
} },
}; };
}; };
+10 -10
View File
@@ -1,18 +1,18 @@
import adapter from "svelte-adapter-bun"; import adapter from "svelte-adapter-bun";
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'; import { vitePreprocess } from "@sveltejs/vite-plugin-svelte";
/** @type {import('@sveltejs/kit').Config} */ /** @type {import('@sveltejs/kit').Config} */
const config = { const config = {
// Consult https://svelte.dev/docs/kit/integrations // Consult https://svelte.dev/docs/kit/integrations
// for more information about preprocessors // for more information about preprocessors
preprocess: vitePreprocess(), preprocess: vitePreprocess(),
kit: { kit: {
// adapter-auto only supports some environments, see https://svelte.dev/docs/kit/adapter-auto for a list. // adapter-auto only supports some environments, see https://svelte.dev/docs/kit/adapter-auto for a list.
// If your environment is not supported, or you settled on a specific environment, switch out the adapter. // If your environment is not supported, or you settled on a specific environment, switch out the adapter.
// See https://svelte.dev/docs/kit/adapters for more information about adapters. // See https://svelte.dev/docs/kit/adapters for more information about adapters.
adapter: adapter(), adapter: adapter(),
} },
}; };
export default config; export default config;
+18 -19
View File
@@ -1,21 +1,20 @@
{ {
"extends": "./.svelte-kit/tsconfig.json", "extends": "./.svelte-kit/tsconfig.json",
"compilerOptions": { "compilerOptions": {
"allowJs": true, "allowJs": true,
"checkJs": true, "checkJs": true,
"esModuleInterop": true, "esModuleInterop": true,
"forceConsistentCasingInFileNames": true, "forceConsistentCasingInFileNames": true,
"resolveJsonModule": true, "resolveJsonModule": true,
"verbatimModuleSyntax": true, "verbatimModuleSyntax": true,
"skipLibCheck": true, "skipLibCheck": true,
"sourceMap": true, "sourceMap": true,
"strict": true, "strict": true,
"moduleResolution": "bundler", "moduleResolution": "bundler"
"importsNotUsedAsValue": true }
} // Path aliases are handled by https://svelte.dev/docs/kit/configuration#alias
// Path aliases are handled by https://svelte.dev/docs/kit/configuration#alias // except $lib which is handled by https://svelte.dev/docs/kit/configuration#files
// except $lib which is handled by https://svelte.dev/docs/kit/configuration#files //
// // To make changes to top-level options such as include and exclude, we recommend extending
// To make changes to top-level options such as include and exclude, we recommend extending // the generated config; see https://svelte.dev/docs/kit/configuration#typescript
// the generated config; see https://svelte.dev/docs/kit/configuration#typescript
} }
+3 -3
View File
@@ -1,7 +1,7 @@
import tailwindcss from "@tailwindcss/vite"; import tailwindcss from "@tailwindcss/vite";
import { sveltekit } from '@sveltejs/kit/vite'; import { sveltekit } from "@sveltejs/kit/vite";
import { defineConfig } from 'vite'; import { defineConfig } from "vite";
export default defineConfig({ export default defineConfig({
plugins: [tailwindcss(), sveltekit()] plugins: [tailwindcss(), sveltekit()],
}); });