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/eden": "^1.4.4",
"@tailwindcss/vite": "^4.1.14",
"daisyui": "^5.3.2",
"daisyui": "^5.3.7",
"elysia": "^1.4.12",
"memoirist": "^0.4.0",
"tailwindcss": "^4.1.14",
},
"devDependencies": {
"@sveltejs/kit": "^2.46.5",
"@sveltejs/kit": "^2.47.1",
"@sveltejs/vite-plugin-svelte": "^6.2.1",
"@types/bun": "^1.3.0",
"svelte": "^5.40.0",
"svelte": "^5.40.2",
"svelte-adapter-bun": "^0.5.2",
"svelte-check": "^4.3.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=="],
"@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/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/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-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/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=="],
@@ -202,19 +200,17 @@
"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=="],
"chownr": ["chownr@3.0.0", "", {}, "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g=="],
"clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="],
"cookie": ["cookie@0.6.0", "", {}, "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw=="],
"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=="],
@@ -224,7 +220,7 @@
"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=="],
@@ -260,27 +256,29 @@
"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=="],
@@ -288,10 +286,6 @@
"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=="],
"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=="],
"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-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=="],
"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=="],
"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=="],
"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=="],
"yallist": ["yallist@5.0.0", "", {}, "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw=="],
"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=="],
+8 -8
View File
@@ -1,14 +1,14 @@
{
"type": "module",
"devDependencies": {
"@sveltejs/kit": "^2.47.1",
"@sveltejs/kit": "^2.48.4",
"@sveltejs/vite-plugin-svelte": "^6.2.1",
"@types/bun": "^1.3.0",
"svelte": "^5.40.2",
"@types/bun": "^1.3.1",
"svelte": "^5.43.3",
"svelte-adapter-bun": "^0.5.2",
"svelte-check": "^4.3.3",
"typescript": "^5.9.3",
"vite": "^7.1.10"
"vite": "^7.1.12"
},
"scripts": {
"dev": "vite dev",
@@ -21,10 +21,10 @@
"dependencies": {
"@elysiajs/cors": "^1.4.0",
"@elysiajs/eden": "^1.4.4",
"@tailwindcss/vite": "^4.1.14",
"daisyui": "^5.3.7",
"elysia": "^1.4.12",
"@tailwindcss/vite": "^4.1.16",
"daisyui": "^5.4.3",
"elysia": "^1.4.15",
"memoirist": "^0.4.0",
"tailwindcss": "^4.1.14"
"tailwindcss": "^4.1.16"
}
}
+1 -2
View File
@@ -1,8 +1,7 @@
import { treaty } from "@elysiajs/eden";
import type { App } from "./server/types";
const baseURL =
import.meta.env.PROD
const baseURL = import.meta.env.PROD
? "https://blade-and-brawn.fly.dev"
: "http://localhost:5173";
+59 -35
View File
@@ -1,13 +1,20 @@
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 { LevelCalculator, Standards, type ActivityStandards, type LevelCalculatorOutput } from "$lib/services/calculator/main";
import type { ActivityPerformance, Player } from "$lib/services/calculator/util";
import rawStandards from "$lib/data/standards.json" assert { type: "json" }
import { Elysia, NotFoundError } from "elysia";
import PrintfulService from "../services/commerce/printful";
import SyncService from "../services/commerce/sync";
import { FetchError, getStateFromZip } from "../services/commerce/util/misc";
import { Printful, Webflow } from "../services/commerce/util/types";
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 levelCalculator = new LevelCalculator(MAIN_STANDARDS);
@@ -24,12 +31,12 @@ export const app = new Elysia({ prefix: "/api" })
/^https?:\/\/blade-and-brawn\.fly\.dev$/i,
// Local dev
"http://localhost:5173",
]
})
],
}),
)
.error({
FetchError
FetchError,
})
.onError(async ({ code, error }) => {
@@ -41,13 +48,15 @@ export const app = new Elysia({ prefix: "/api" })
})
.onAfterHandle(({ request, set }) => {
console.log(JSON.stringify({
console.log(
JSON.stringify({
lvl: "info",
msg: "req",
method: request.method,
path: new URL(request.url).pathname,
status: set.status ?? 200
}));
status: set.status ?? 200,
}),
);
})
.get("/", () => "Hello world")
@@ -60,15 +69,17 @@ export const app = new Elysia({ prefix: "/api" })
activityPerformances: ActivityPerformance[];
}
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 });
if (query?.log === "true") {
void fetch("https://kv-logger.xominus.workers.dev", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ output, player, activityPerformances })
}).catch(e => { });
body: JSON.stringify({ output, player, activityPerformances }),
}).catch(() => {});
}
return output;
@@ -76,33 +87,38 @@ export const app = new Elysia({ prefix: "/api" })
// COMMERCE
// TODO: protect with api key/token
.group("/products", app => app
.group("/sync", app => app
.get("/", async ({ }) => SyncService.state)
.group("/products", (app) =>
app
.group("/sync", (app) =>
app
.get("/", async ({}) => SyncService.state)
.post("/:printfulProductId?", async ({ params, set }) => {
if (SyncService.state.isSyncing) {
set.status = 409;
return { error: "Sync already in progress" };
}
const printfulProductId = params.printfulProductId ?
+params.printfulProductId :
undefined;
const printfulProductId = params.printfulProductId
? +params.printfulProductId
: undefined;
await SyncService.sync(printfulProductId);
return { ok: true };
})
}),
)
.get("/:printfulProductId", async ({ params }) => {
const printfulProductId = +params.printfulProductId;
const printfulProduct = await PrintfulService.Products.get(printfulProductId);
const printfulProduct =
await PrintfulService.Products.get(printfulProductId);
if (!printfulProduct) {
return new NotFoundError();
}
const webflowProductId = printfulProduct.sync_product.external_id.split("-")[0];
const webflowProduct = await WebflowService.Products.get(webflowProductId);
const webflowProductId =
printfulProduct.sync_product.external_id.split("-")[0];
const webflowProduct =
await WebflowService.Products.get(webflowProductId);
return { printfulProduct, webflowProduct };
})
}),
)
// WEBHOOKS
@@ -116,7 +132,9 @@ export const app = new Elysia({ prefix: "/api" })
break;
}
case Printful.Webhook.Event.ProductDeleted: {
await WebflowService.Products.remove(payload.data.sync_product.external_id);
await WebflowService.Products.remove(
payload.data.sync_product.external_id,
);
break;
}
case Printful.Webhook.Event.PackageShipped: {
@@ -126,9 +144,11 @@ export const app = new Elysia({ prefix: "/api" })
await WebflowService.Orders.update(webflowOrderId, {
shippingTrackingURL: shipInfo.tracking_url,
shippingTracking: shipInfo.tracking_number,
shippingProvider: shipInfo.carrier
shippingProvider: shipInfo.carrier,
});
await WebflowService.Orders.fulfill(webflowOrderId, {
sendOrderFulfilledEmail: true,
});
await WebflowService.Orders.fulfill(webflowOrderId, { sendOrderFulfilledEmail: true });
break;
}
}
@@ -156,14 +176,18 @@ export const app = new Elysia({ prefix: "/api" })
address1: webflowOrder.shippingAddress.line1,
address2: webflowOrder.shippingAddress.line2,
city: webflowOrder.shippingAddress.city,
state_code: getStateFromZip(webflowOrder.shippingAddress.postalCode),
state_code: getStateFromZip(
webflowOrder.shippingAddress.postalCode,
),
country_code: webflowOrder.shippingAddress.country,
zip: webflowOrder.shippingAddress.postalCode
zip: webflowOrder.shippingAddress.postalCode,
},
items: webflowOrder.purchasedItems.map(webflowOrderSku => ({
items: webflowOrder.purchasedItems.map(
(webflowOrderSku) => ({
external_variant_id: webflowOrderSku.variantId,
quantity: webflowOrderSku.count
}))
quantity: webflowOrderSku.count,
}),
),
});
console.log("Complete");
+1 -1
View File
@@ -1,2 +1,2 @@
import type { app } from './app';
import type { app } from "./app";
export type App = typeof app;
+291 -154
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
// 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
export type LevelCalculatorOutput = {
player: number,
attributes: Record<Attribute, number>
}
player: number;
attributes: Record<Attribute, number>;
};
export type Levels = Record<string, number>
export type Levels = Record<string, number>;
export interface Metrics {
age: number;
weight: number;
gender: Gender;
};
}
type NumberMetric = Extract<keyof Metrics, "age" | "weight">;
@@ -31,18 +41,21 @@ export interface Standard {
}
type Generator = {
metric: NumberMetric
}
metric: NumberMetric;
};
export type ActivityStandards = Record<Activity, {
export type ActivityStandards = Record<
Activity,
{
metadata: {
attribute: Attribute,
generators: Generator[],
unit: StandardUnit,
name: string
},
standards: Standard[]
}>;
attribute: Attribute;
generators: Generator[];
unit: StandardUnit;
name: string;
};
standards: Standard[];
}
>;
export class LevelCalculator {
standards: Standards;
@@ -55,7 +68,10 @@ export class LevelCalculator {
// Level calculations
// -------------------------------------------------------------------------------------------------
public calculate(player: Player, activityPerformances: ActivityPerformance[]): LevelCalculatorOutput {
public calculate(
player: Player,
activityPerformances: ActivityPerformance[],
): LevelCalculatorOutput {
const levels: LevelCalculatorOutput = {
player: 0,
attributes: {
@@ -63,22 +79,28 @@ export class LevelCalculator {
[Attribute.Power]: 0,
[Attribute.Endurance]: 0,
[Attribute.Agility]: 0,
} as const
} as const,
};
for (const value of Object.values(player)) {
if (!value)
return levels;
if (!value) return levels;
}
levels.attributes = this.calculateAllAttributeLevels(player, activityPerformances);
levels.attributes = this.calculateAllAttributeLevels(
player,
activityPerformances,
);
for (const level of Object.values(levels.attributes)) {
if (!level)
return levels;
if (!level) return levels;
}
const attrLevelsSum = Object.values(levels.attributes).reduce((sum, lvl) => sum + lvl, 0);
levels.player = Math.round(attrLevelsSum / Object.keys(Attribute).length);
const attrLevelsSum = Object.values(levels.attributes).reduce(
(sum, lvl) => sum + lvl,
0,
);
levels.player = Math.round(
attrLevelsSum / Object.keys(Attribute).length,
);
return levels;
}
@@ -95,42 +117,54 @@ export class LevelCalculator {
} as const;
for (const value of Object.values(player)) {
if (!value)
return attrLevels;
if (!value) return attrLevels;
}
(Object.values(Attribute) as Attribute[]).forEach((attribute) => {
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;
}
private calculateAttributeLevel(
attribute: Attribute,
player: Player,
activityPerformances: ActivityPerformance[],
): number {
// 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");
}
// must perform all of an attributes activities
for (const activity of this.standards.getAttributeActivities(attribute)) {
const filteredActivites = activityPerformances.map((p) => p.activity);
if (!filteredActivites.includes(activity))
return 0;
for (const activity of this.standards.getAttributeActivities(
attribute,
)) {
const filteredActivites = activityPerformances.map(
(p) => p.activity,
);
if (!filteredActivites.includes(activity)) return 0;
}
// must have all activties using a valid performance value (> 0)
for (const activityPerformance of activityPerformances) {
if (activityPerformance.performance <= 0)
return 0
if (activityPerformance.performance <= 0) return 0;
}
const activityLevels = activityPerformances.map((p) => {
@@ -142,7 +176,8 @@ export class LevelCalculator {
});
const activityLevelsAvg = Math.round(
activityLevels.reduce((sum, curr) => sum + curr, 0) / activityLevels.length,
activityLevels.reduce((sum, curr) => sum + curr, 0) /
activityLevels.length,
);
return activityLevelsAvg;
@@ -171,85 +206,109 @@ export class LevelCalculator {
export type StandardsConfig = {
global: {
maxLevel: number,
},
activity: Record<Activity, {
weightModifier: number,
weightSkew: number,
ageModifier: number,
enableGeneration: boolean,
difficultyModifier: number
}>
}
maxLevel: number;
};
activity: Record<
Activity,
{
weightModifier: number;
weightSkew: number;
ageModifier: number;
enableGeneration: boolean;
difficultyModifier: number;
peakAge: number;
}
>;
};
export class Standards {
readonly cfg: StandardsConfig;
private activityStandards: ActivityStandards;
constructor(activityStandards: ActivityStandards, cfg?: Partial<StandardsConfig>) {
constructor(
activityStandards: ActivityStandards,
cfg?: Partial<StandardsConfig>,
) {
// TODO: set these somewhere
const defaultActivitiesConfig = Object.fromEntries(
Object.values(Activity).map(activity => [
Object.values(Activity).map((activity) => [
activity,
{
enableGeneration: true,
weightModifier: activity === Activity.BroadJump ?
-0.1 :
0.1,
weightModifier:
activity === Activity.BroadJump ? -0.1 : 0.1,
weightSkew: 0,
ageModifier: 0.5,
difficultyModifier: activity === Activity.BroadJump ?
0.05 :
0
difficultyModifier:
activity === Activity.BroadJump ? 0.05 : 0,
peakAge: 27,
},
])
]),
) as Record<Activity, StandardsConfig["activity"][Activity]>;
this.cfg = {
global: {
maxLevel: cfg?.global?.maxLevel ?? 100,
},
activity: Object.assign(defaultActivitiesConfig, cfg?.activity)
activity: Object.assign(defaultActivitiesConfig, cfg?.activity),
};
// prepare data
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) {
// expand
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++);
}
// compress
if (Object.keys(standard.levels).length > this.cfg.global.maxLevel) {
standard.levels = this.compressLevels(standard.levels, this.cfg.global.maxLevel);
if (
Object.keys(standard.levels).length >
this.cfg.global.maxLevel
) {
standard.levels = this.compressLevels(
standard.levels,
this.cfg.global.maxLevel,
);
}
// apply skew config
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
const allGenerators = this.cfg.activity[activity].enableGeneration ?
this.activityStandards[activity].metadata.generators :
[];
const allGenerators = this.cfg.activity[activity].enableGeneration
? 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 gender of Object.values(Gender)) {
const peakAge = 27;
const peakAge = this.cfg.activity[activity].peakAge;
const ageStep = 10;
const minAge = 0;
const maxAge = 100;
const referenceStandard = this
.byActivity(activity)
.byMetrics({ weight: getAvgWeight(gender, peakAge), gender, age: peakAge })
const referenceStandard = this.byActivity(activity)
.byMetrics({
weight: getAvgWeight(gender, peakAge),
gender,
age: peakAge,
})
.getOneInterpolated();
let currAge = minAge;
@@ -258,9 +317,9 @@ export class Standards {
metrics: {
weight: getAvgWeight(gender, currAge),
age: currAge,
gender
gender,
},
levels: {}
levels: {},
};
for (const lvl in referenceStandard.levels) {
@@ -275,10 +334,15 @@ export class Standards {
const youngFloor = 1 - (1 - youngFloor0) * s;
const oldFloor = 1 - (1 - oldFloor0) * s;
const cy = s * (1 - youngFloor0) / Math.pow(peakAge - minAge, 2);
const co = s * (1 - oldFloor0) / Math.pow(maxAge - peakAge, 2);
const cy =
(s * (1 - youngFloor0)) /
Math.pow(peakAge - minAge, 2);
const co =
(s * (1 - oldFloor0)) /
Math.pow(maxAge - peakAge, 2);
let f = currAge <= peakAge
let f =
currAge <= peakAge
? 1 - cy * Math.pow(peakAge - currAge, 2)
: 1 - co * Math.pow(currAge - peakAge, 2);
@@ -289,34 +353,41 @@ export class Standards {
}
// prefer real data over generated data
const overlappingStandard = this
.byActivity(activity)
const overlappingStandard = this.byActivity(activity)
.byMetrics(newStandard.metrics)
.getOne();
if (!overlappingStandard)
this.activityStandards[activity].standards.push(newStandard);
this.activityStandards[activity].standards.push(
newStandard,
);
currAge += ageStep;
}
}
// now that we generated metric data, remove the old data
this.activityStandards[activity].standards = this.activityStandards[activity].standards
.filter(s => s.metrics.weight)
this.activityStandards[activity].standards =
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 gender of Object.values(Gender)) {
for (const age of this.agesFor(activity, gender)) {
const weightStep = 12;
const referenceWeight = getAvgWeight(gender, age);
const minWeight = Math.max(referenceWeight - (3 * weightStep), 1);
const maxWeight = referenceWeight + (3 * weightStep);
const minWeight = Math.max(
referenceWeight - 3 * weightStep,
1,
);
const maxWeight = referenceWeight + 3 * weightStep;
const referenceStandard = this
.byActivity(activity)
const referenceStandard = this.byActivity(activity)
.byMetrics({ weight: referenceWeight, gender, age })
.getOneInterpolated();
@@ -326,28 +397,35 @@ export class Standards {
metrics: {
weight: currWeight,
age,
gender
gender,
},
levels: {}
levels: {},
};
// apply allometric scaling to generate levels
for (const lvl in referenceStandard.levels) {
if (!referenceStandard.levels[lvl]) continue;
const scalingExponent = this.cfg.activity[activity].weightModifier
const coefficient = currWeight / referenceWeight;
const scalingExponent =
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
const overlappingStandard = this
.byActivity(activity)
const overlappingStandard = this.byActivity(
activity,
)
.byMetrics(newStandard.metrics)
.getOne();
if (!overlappingStandard)
this.activityStandards[activity].standards.push(newStandard);
this.activityStandards[activity].standards.push(
newStandard,
);
currWeight += weightStep;
}
@@ -355,13 +433,19 @@ export class Standards {
}
// now that we generated metric data, remove the old data
this.activityStandards[activity].standards = this.activityStandards[activity].standards
.filter(s => s.metrics.weight);
this.activityStandards[activity].standards =
this.activityStandards[activity].standards.filter(
(s) => s.metrics.weight,
);
}
this.activityStandards[activity].standards = this.activityStandards[activity].standards
.sort((a, b) => {
const genderOrder: Record<Gender, number> = { Male: 0, Female: 1 };
this.activityStandards[activity].standards = this.activityStandards[
activity
].standards.sort((a, b) => {
const genderOrder: Record<Gender, number> = {
Male: 0,
Female: 1,
};
const gA = genderOrder[b.metrics.gender] ?? 99;
const gB = genderOrder[b.metrics.gender] ?? 99;
if (gA !== gB) return gA - gB;
@@ -387,32 +471,57 @@ export class Standards {
const out: Standard[] = [];
for (let i = 0; i < src.length; i++) {
const s = src[i];
if (state.gender != null && 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;
if (
state.gender != null &&
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);
}
return out;
},
getOne: function(): Standard | undefined {
getOne: function (): Standard | undefined {
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();
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 upper = standards[0]; // best >= target
let hasLower = false, hasUpper = false;
let hasLower = false,
hasUpper = false;
for (const standard of standards) {
const standardMetricValue = standard.metrics[metric];
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 (!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 (!hasUpper) upper = standards[standards.length - 1];
return { lower, upper };
}
}
},
},
};
const interpolateByAgeAndWeight = (metrics: Metrics): Standard => {
const interpolateByWeight = (targetAge: number): Levels => {
const { lower: lowerAgeWeightStandard, upper: upperAgeWeightStandard } = this
.byActivity(activity)
const {
lower: lowerAgeWeightStandard,
upper: upperAgeWeightStandard,
} = this.byActivity(activity)
.byGender(metrics.gender)
.byAge(targetAge)
.getNearest("weight", metrics.weight);
@@ -435,17 +546,25 @@ export class Standards {
const weightLower = lowerAgeWeightStandard.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));
// 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
const { lower: lowerAgeStandard, upper: upperAgeStandard } = this
.byActivity(activity)
const { lower: lowerAgeStandard, upper: upperAgeStandard } =
this.byActivity(activity)
.byGender(metrics.gender)
.getNearest("age", metrics.age);
@@ -453,31 +572,41 @@ export class Standards {
const ageUpper = upperAgeStandard.metrics.age;
// 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));
const interpolatedLevels = this.interpolateLevels(
interpolateByWeight(ageLower),
interpolateByWeight(ageUpper),
ageRatio
ageRatio,
);
return {
metrics: metrics,
levels: interpolatedLevels
}
levels: interpolatedLevels,
};
};
const getAllInterpolated = (opt?: { normalizeForLb?: boolean }): Standard[] => {
const getAllInterpolated = (opt?: {
normalizeForLb?: boolean;
}): Standard[] => {
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) {
weights = weights.map(w => lbToKg(Math.round(kgToLb(w))));
weights = weights.map((w) => lbToKg(Math.round(kgToLb(w))));
}
const interpolatedStandards: Standard[] = weights
.map(weight => interpolateByAgeAndWeight({ ...metrics, weight }));
const interpolatedStandards: Standard[] = weights.map((weight) =>
interpolateByAgeAndWeight({ ...metrics, weight }),
);
return interpolatedStandards;
};
@@ -494,31 +623,40 @@ export class Standards {
const byGender = (gender: Gender) => {
state.gender = gender;
return { byAge, ...execMethods.exact };
}
};
const byAge = (age: number) => {
state.age = age;
return { byWeight, ...execMethods.exact, getAllInterpolated };
}
};
const byWeight = (weight: number) => {
state.weight = weight;
return { ...execMethods.exact, getOneInterpolated };
}
};
const byMetrics = (metrics: Metrics) => {
Object.assign(state, metrics);
return { ...execMethods.exact, getOneInterpolated };
}
};
const initialMethods = { byGender, byMetrics, getMetadata, ...execMethods.exact };
const initialMethods = {
byGender,
byMetrics,
getMetadata,
...execMethods.exact,
};
return initialMethods;
}
public getAttributeActivities(attribute: Attribute): Activity[] {
const activities: Activity[] = [];
for (const activity of Object.keys(this.activityStandards) as Activity[]) {
const activityStandard = this.activityStandards[activity] as ActivityStandards[Activity];
for (const activity of Object.keys(
this.activityStandards,
) as Activity[]) {
const activityStandard = this.activityStandards[
activity
] as ActivityStandards[Activity];
if (activityStandard.metadata.attribute === attribute) {
activities.push(activity as Activity);
}
@@ -528,36 +666,33 @@ export class Standards {
// TODO: turn these into queries?
public weightsFor(activity: Activity, gender: Gender, age: number) {
const { lower, upper } = this
.byActivity(activity)
const { lower, upper } = this.byActivity(activity)
.byGender(gender)
.getNearest("age", age);
const lowerDiff = Math.abs(lower.metrics.age - age);
const upperDiff = Math.abs(upper.metrics.age - age);
const closestAge = lowerDiff < upperDiff ?
lower.metrics.age :
upper.metrics.age;
const closestAge =
lowerDiff < upperDiff ? lower.metrics.age : upper.metrics.age;
const weights = this
.byActivity(activity)
const weights = this.byActivity(activity)
.byGender(gender)
.byAge(closestAge)
.getAll().map((s) => s.metrics.weight);
.getAll()
.map((s) => s.metrics.weight);
return weights;
}
public agesFor(activity: Activity, gender: Gender) {
const standardsByGender = this.byActivity(activity).byGender(gender).getAll();
const ages = [...new Set(standardsByGender.map(s => s.metrics.age))];
const standardsByGender = this.byActivity(activity)
.byGender(gender)
.getAll();
const ages = [...new Set(standardsByGender.map((s) => s.metrics.age))];
return ages;
}
private expandLevels(
levels: Levels,
i: number
): Levels {
private expandLevels(levels: Levels, i: number): Levels {
if (i == 0) {
return levels;
}
@@ -578,10 +713,7 @@ export class Standards {
return this.expandLevels(newLevels, i - 1);
}
private compressLevels(
levels: Levels,
targetLevel: number,
): Levels {
private compressLevels(levels: Levels, targetLevel: number): Levels {
const levelsAmount = Object.keys(levels).length;
if (levelsAmount < targetLevel) {
throw new Error(
@@ -601,7 +733,8 @@ export class Standards {
const upperValue = levels[upperIndex + 1];
const weight = ratioIndex - lowerIndex;
compressedLevels[i + 1] = lowerValue + (upperValue - lowerValue) * weight;
compressedLevels[i + 1] =
lowerValue + (upperValue - lowerValue) * weight;
}
return compressedLevels;
@@ -613,9 +746,13 @@ export class Standards {
ratio: number,
): Levels {
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 = {};
for (const lvl in lower) {
+12 -11
View File
@@ -1,5 +1,5 @@
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
@@ -8,7 +8,7 @@ import avgWeightDataRaw from "$lib/data/avg-weights.json"
export type Player = {
name?: string;
metrics: Metrics;
}
};
export enum Attribute {
Strength = "Strength",
@@ -33,7 +33,7 @@ export interface ActivityPerformance {
export enum Gender {
Male = "Male",
Female = "Female"
Female = "Female",
}
// -------------------------------------------------------------------------------------------------
@@ -65,23 +65,24 @@ export const msToTime = (ms: number, includeMs = false): string => {
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) => {
type AvgWeightData = {
metadata: {
unit: StandardUnit
},
weights: Metrics[]
}
unit: StandardUnit;
};
weights: Metrics[];
};
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");
const closest = {
diff: Math.abs(avgWeights[0].age - age),
avg: avgWeights[0]
avg: avgWeights[0],
};
for (const avg of avgWeights) {
const diff = Math.abs(avg.age - age);
@@ -92,4 +93,4 @@ export const getAvgWeight = (gender: Gender, age: number) => {
}
return closest.avg.weight;
}
};
+84 -44
View File
@@ -4,11 +4,16 @@ import { FetchError, type DeepPartial } from "./util/misc";
export default class PrintfulService {
static Products = class {
static Variants = class {
static async get(variantId: number | string): Promise<Printful.Products.SyncVariant | undefined> {
const res = await fetch(`${env().API_URL}/store/variants/${variantId}`, {
static async get(
variantId: number | string,
): Promise<Printful.Products.SyncVariant | undefined> {
const res = await fetch(
`${env().API_URL}/store/variants/${variantId}`,
{
method: "GET",
headers: { ...env().AUTH_HEADERS }
});
headers: { ...env().AUTH_HEADERS },
},
);
if (res.status === 404 || res.status === 400) {
return;
@@ -18,32 +23,41 @@ export default class PrintfulService {
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;
}
}
};
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[] = [];
while (true) {
try {
const res = await fetch(`${env().API_URL}/store/products?offset=${offset}`, {
const res = await fetch(
`${env().API_URL}/store/products?offset=${offset}`,
{
method: "GET",
headers: { ...env().AUTH_HEADERS }
})
headers: { ...env().AUTH_HEADERS },
},
);
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);
offset = allSyncProducts.length;
if (allSyncProducts.length >= payload.paging.total)
break;
if (allSyncProducts.length >= payload.paging.total) break;
} catch (error) {
throw error;
}
@@ -52,11 +66,16 @@ export default class PrintfulService {
return allSyncProducts;
}
static async get(productId: number | string): Promise<Printful.Products.Product | undefined> {
const res = await fetch(`${env().API_URL}/store/products/${productId}`, {
static async get(
productId: number | string,
): Promise<Printful.Products.Product | undefined> {
const res = await fetch(
`${env().API_URL}/store/products/${productId}`,
{
method: "GET",
headers: { ...env().AUTH_HEADERS }
});
headers: { ...env().AUTH_HEADERS },
},
);
if (res.status === 404 || res.status === 400) {
return;
@@ -66,25 +85,32 @@ export default class PrintfulService {
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;
}
static async update(printfulProductId: number, printfulProduct: DeepPartial<Printful.Products.Product>) {
const res = await fetch(`${env().API_URL}/store/products/${printfulProductId}`, {
static async update(
printfulProductId: number,
printfulProduct: DeepPartial<Printful.Products.Product>,
) {
const res = await fetch(
`${env().API_URL}/store/products/${printfulProductId}`,
{
method: "PUT",
headers: {
"Content-Type": "application/json",
...env().AUTH_HEADERS
...env().AUTH_HEADERS,
},
body: JSON.stringify(printfulProduct)
});
body: JSON.stringify(printfulProduct),
},
);
if (!res.ok) {
throw new FetchError("Failed to update Printful product", res);
}
}
}
};
static Orders = class {
static async create(printfulOrder: Printful.Orders.Order) {
@@ -92,9 +118,9 @@ export default class PrintfulService {
method: "POST",
headers: {
"Content-Type": "application/json",
...env().AUTH_HEADERS
...env().AUTH_HEADERS,
},
body: JSON.stringify(printfulOrder)
body: JSON.stringify(printfulOrder),
});
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[] = [];
while (true) {
try {
const res = await fetch(`${env().API_URL}/orders?offset=${offset}`, {
const res = await fetch(
`${env().API_URL}/orders?offset=${offset}`,
{
method: "GET",
headers: { ...env().AUTH_HEADERS }
})
headers: { ...env().AUTH_HEADERS },
},
);
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);
offset = allPrintfulOrders.length;
if (allPrintfulOrders.length >= payload.paging.total)
break;
if (allPrintfulOrders.length >= payload.paging.total) break;
} catch (error) {
throw error;
}
@@ -130,32 +164,38 @@ export default class PrintfulService {
return allPrintfulOrders;
}
}
};
static Util = class {
static getVariantMainImage(syncVariant: Printful.Products.SyncVariant): string {
const previewFile = syncVariant.files.find(f => f.type === "preview");
static getVariantMainImage(
syncVariant: Printful.Products.SyncVariant,
): string {
const previewFile = syncVariant.files.find(
(f) => f.type === "preview",
);
return previewFile?.preview_url ?? syncVariant.product.image;
}
}
};
}
export const env = () => {
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 = {
API_URL: "https://api.printful.com",
AUTH_TOKEN: Bun.env.PRINTFUL_AUTH,
STORE_ID: Bun.env.PRINTFUL_STORE_ID
STORE_ID: Bun.env.PRINTFUL_STORE_ID,
};
return {
...vars,
AUTH_HEADERS: {
"Authorization": `Bearer ${vars.AUTH_TOKEN}`,
"X-PF-Store-Id": `${vars.STORE_ID}`
}
Authorization: `Bearer ${vars.AUTH_TOKEN}`,
"X-PF-Store-Id": `${vars.STORE_ID}`,
},
};
};
+183 -121
View File
@@ -3,26 +3,26 @@ import { FetchError, formatSlug, type DeepPartial } from "./util/misc";
import WebflowService from "./webflow";
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 WEBSITE_MEDIA_URL = "https://website-media.bladeandbrawn.com";
type MainProduct = {
name: string,
externalId: string,
colors: Set<string>,
name: string;
externalId: string;
colors: Set<string>;
variants: {
color: string,
product: Printful.Products.Product
}[]
}
color: string;
product: Printful.Products.Product;
}[];
};
export default class SyncService {
static state = {
isSyncing: false,
syncingIds: [] as number[]
}
syncingIds: [] as number[],
};
static async sync(printfulProductId: number | undefined) {
this.state.isSyncing = true;
@@ -32,43 +32,54 @@ export default class SyncService {
try {
let printfulProducts = await PrintfulService.Products.getAll();
if (printfulProductId) {
const printfulProduct = printfulProducts.find(p => p.id === printfulProductId);
const printfulProduct = printfulProducts.find(
(p) => p.id === printfulProductId,
);
if (printfulProduct) {
const mainProductName = this.getMainProductName(printfulProduct.name);
printfulProducts = printfulProducts.filter(p => p.name.includes(mainProductName));
const mainProductName = this.getMainProductName(
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
console.log("Generating main products...");
for (const printfulProduct of printfulProducts) {
const mainProductName = this.getMainProductName(printfulProduct.name);
const mainProductName = this.getMainProductName(
printfulProduct.name,
);
let mainPrintfulProduct = mainPrintfulProducts
.find(mp => mp.name.includes(mainProductName));
let mainPrintfulProduct = mainPrintfulProducts.find((mp) =>
mp.name.includes(mainProductName),
);
if (!mainPrintfulProduct) {
mainPrintfulProduct = {
name: mainProductName,
variants: [],
externalId: printfulProduct.external_id,
colors: new Set()
}
colors: new Set(),
};
mainPrintfulProducts.push(mainPrintfulProduct);
}
const productColor = this.findColorInProductName(printfulProduct.name);
const productColor = this.findColorInProductName(
printfulProduct.name,
);
mainPrintfulProduct.variants.push({
color: productColor,
product: (await PrintfulService.Products.get(printfulProduct.id))!
product: (await PrintfulService.Products.get(
printfulProduct.id,
))!,
});
mainPrintfulProduct.colors.add(productColor);
}
}
catch (error) {
} catch (error) {
throw error;
}
finally {
} finally {
this.state.isSyncing = false;
this.state.syncingIds = [];
}
@@ -80,19 +91,26 @@ export default class SyncService {
for (const mainPrintfulProduct of mainPrintfulProducts) {
try {
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 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 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
let sizeIsInAllVariants = true;
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))
sizeIsInAllVariants = false;
}
@@ -101,29 +119,37 @@ export default class SyncService {
foundSizes.add(printfulVariant.size);
webflowSkus.push({
"id": printfulVariant.external_id,
"fieldData": {
"name": printfulVariant.name,
"slug": formatSlug(printfulVariant.name),
id: printfulVariant.external_id,
fieldData: {
name: printfulVariant.name,
slug: formatSlug(printfulVariant.name),
"sku-values": {
"color": specialVariant.color,
"size": printfulVariant.size,
color: specialVariant.color,
size: printfulVariant.size,
},
"price": {
"value": Math.floor(+printfulVariant.retail_price * 100),
"unit": printfulVariant.currency,
"currency": printfulVariant.currency
price: {
value: Math.floor(
+printfulVariant.retail_price * 100,
),
unit: printfulVariant.currency,
currency: printfulVariant.currency,
},
"main-image": PrintfulService.Util.getVariantMainImage(printfulVariant)
}
})
"main-image":
PrintfulService.Util.getVariantMainImage(
printfulVariant,
),
},
});
}
}
}
// SYNC PRODUCT DATA
let existingWebflowProduct = webflowProducts
.find(webflowProduct => webflowProduct.product.id === mainPrintfulProduct.externalId.split("-")[0]);
let existingWebflowProduct = webflowProducts.find(
(webflowProduct) =>
webflowProduct.product.id ===
mainPrintfulProduct.externalId.split("-")[0],
);
if (existingWebflowProduct) {
// SYNC PRODUCT UPDATE
@@ -132,118 +158,153 @@ export default class SyncService {
delete webflowSku.fieldData?.["main-image"];
}
const webflowProductId = mainPrintfulProduct.externalId.split("-")[0];
const webflowProductId =
mainPrintfulProduct.externalId.split("-")[0];
WebflowService.Products.update(webflowProductId, {
"product": {
"fieldData": {
"name": mainPrintfulProduct.name,
"slug": formatSlug(mainPrintfulProduct.name),
"shippable": true,
product: {
fieldData: {
name: mainPrintfulProduct.name,
slug: formatSlug(mainPrintfulProduct.name),
shippable: true,
"tax-category": "standard-taxable",
"sku-properties": [
{
"id": "color",
"name": "Color",
"enum": Array.from(foundColors).map(color => ({
"id": color,
"slug": formatSlug(color),
"name": color
}))
id: "color",
name: "Color",
enum: Array.from(foundColors).map(
(color) => ({
id: color,
slug: formatSlug(color),
name: color,
}),
),
},
{
"id": "size",
"name": "Size",
"enum": Array.from(foundSizes).map(size => ({
"id": size,
"slug": formatSlug(size),
"name": size
}))
id: "size",
name: "Size",
enum: Array.from(foundSizes).map(
(size) => ({
id: size,
slug: formatSlug(size),
name: size,
}),
),
},
]
}
],
},
"sku": webflowSkus[0]
},
sku: webflowSkus[0],
});
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) {
await WebflowService.Products.Skus.update(webflowProductId, webflowSku.id, webflowSku);
}
else {
await WebflowService.Products.Skus.create(webflowProductId, [webflowSku]);
await WebflowService.Products.Skus.update(
webflowProductId,
webflowSku.id,
webflowSku,
);
} else {
await WebflowService.Products.Skus.create(
webflowProductId,
[webflowSku],
);
}
}
}
else {
} else {
// SYNC PRODUCT CREATE
const webflowProductId = await WebflowService.Products.create({
"product": {
"fieldData": {
"name": mainPrintfulProduct.name,
"slug": formatSlug(mainPrintfulProduct.name),
"shippable": true,
const webflowProductId =
await WebflowService.Products.create({
product: {
fieldData: {
name: mainPrintfulProduct.name,
slug: formatSlug(mainPrintfulProduct.name),
shippable: true,
"tax-category": "standard-taxable",
"sku-properties": [
{
"id": "color",
"name": "Color",
"enum": Array.from(foundColors).map(color => ({
"id": color,
"slug": formatSlug(color),
"name": color
}))
id: "color",
name: "Color",
enum: Array.from(foundColors).map(
(color) => ({
id: color,
slug: formatSlug(color),
name: color,
}),
),
},
{
"id": "size",
"name": "Size",
"enum": Array.from(foundSizes).map(size => ({
"id": size,
"slug": formatSlug(size),
"name": size
}))
id: "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
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) {
console.error("Missing webflow product");
throw new Error("Failed to create Webflow product");
}
for (const specialVariant of mainPrintfulProduct.variants) {
const newPrintfulVariants: DeepPartial<Printful.Products.SyncVariant>[] = [];
for (const printfulVariant of specialVariant.product.sync_variants) {
const associatedWebflowSku = existingWebflowProduct.skus
.find(sku => sku.fieldData["sku-values"]?.["color"] === printfulVariant.color &&
sku.fieldData["sku-values"]?.["size"] === printfulVariant.size);
const newPrintfulVariants: DeepPartial<Printful.Products.SyncVariant>[] =
[];
for (const printfulVariant of specialVariant.product
.sync_variants) {
const associatedWebflowSku =
existingWebflowProduct.skus.find(
(sku) =>
sku.fieldData["sku-values"]?.[
"color"
] === printfulVariant.color &&
sku.fieldData["sku-values"]?.[
"size"
] === printfulVariant.size,
);
if (associatedWebflowSku) {
newPrintfulVariants.push({
"id": printfulVariant.id, // printful variant id
"external_id": String(associatedWebflowSku.id), // webflow variant id
id: printfulVariant.id, // printful variant id
external_id: String(
associatedWebflowSku.id,
), // webflow variant id
});
}
}
await sleep(10000);
await PrintfulService.Products.update(specialVariant.product.sync_product.id, {
"sync_product": {
"id": specialVariant.product.sync_product.id,
"external_id": `${webflowProductId}-${specialVariant.color}`
await PrintfulService.Products.update(
specialVariant.product.sync_product.id,
{
sync_product: {
id: specialVariant.product.sync_product.id,
external_id: `${webflowProductId}-${specialVariant.color}`,
},
"sync_variants": newPrintfulVariants
});
sync_variants: newPrintfulVariants,
},
);
}
}
}
} catch (err) {
// SYNC IMAGES
// for (const sku of existingWebflowProduct.skus) {
// const skuColor = sku.fieldData["sku-values"]?.["color"]?.toLowerCase() ?? "None";
@@ -256,7 +317,6 @@ export default class SyncService {
// await WebflowService.Products.Skus.update(existingWebflowProduct.product.id, sku.id, sku);
// }
//
catch (err) {
if (err instanceof FetchError) {
console.error(err.message, err.payload);
}
@@ -268,14 +328,17 @@ export default class SyncService {
}
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) {
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");
}
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 => {
const m = productName.match(/\[([^\]]+)\]/);
@@ -286,8 +349,7 @@ export default class SyncService {
const colorInName = this.findColorInProductName(productName);
if (colorInName) {
return productName.replace(`[${colorInName}]`, "").trimEnd();
}
else {
} else {
return productName;
}
};
+161 -128
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;
}
: T;
export const formatSlug = (name: string): string => {
return name
.toLowerCase()
.replace(/[^a-z0-9-_]/g, '-') // replace illegal chars with dashes
.replace(/^-+/, '') // remove leading dashes
.replace(/-+$/, '') // remove trailing dashes
.replace(/--+/g, '-') // collapse multiple dashes
.replace(/^([^a-z0-9_])/, '_$1'); // if it starts with an invalid char, prefix underscore
.replace(/[^a-z0-9-_]/g, "-") // replace illegal chars with dashes
.replace(/^-+/, "") // remove leading dashes
.replace(/-+$/, "") // remove trailing dashes
.replace(/--+/g, "-") // collapse multiple dashes
.replace(/^([^a-z0-9_])/, "_$1"); // if it starts with an invalid char, prefix underscore
};
export class FetchError extends Error {
public payload: Object | undefined;
constructor(message: string, public response: Response) {
constructor(
message: string,
public response: Response,
) {
super(message);
this.name = "FetchError";
}
@@ -25,24 +30,22 @@ export class FetchError extends Error {
this.payload = await this.response.clone().json();
} catch {
this.payload = await this.response.text().catch(() => undefined);
}
finally {
} finally {
return this.payload;
}
}
}
export function getStateFromZip(zipString: string): string {
/* Ensure param is a string to prevent unpredictable parsing results */
if (typeof zipString !== 'string') {
console.error('Must pass the zipcode as a string.');
if (typeof zipString !== "string") {
console.error("Must pass the zipcode as a string.");
return "none";
}
/* Ensure we have exactly 5 characters to parse */
if (zipString.length !== 5) {
console.error('Must pass a 5-digit zipcode.');
console.error("Must pass a 5-digit zipcode.");
return "none";
}
@@ -54,164 +57,194 @@ export function getStateFromZip(zipString: string): string {
/* Code cases alphabetized by state */
if (zipcode >= 35000 && zipcode <= 36999) {
st = 'AL';
state = 'Alabama';
st = "AL";
state = "Alabama";
} else if (zipcode >= 99500 && zipcode <= 99999) {
st = 'AK';
state = 'Alaska';
st = "AK";
state = "Alaska";
} else if (zipcode >= 85000 && zipcode <= 86999) {
st = 'AZ';
state = 'Arizona';
st = "AZ";
state = "Arizona";
} else if (zipcode >= 71600 && zipcode <= 72999) {
st = 'AR';
state = 'Arkansas';
st = "AR";
state = "Arkansas";
} else if (zipcode >= 90000 && zipcode <= 96699) {
st = 'CA';
state = 'California';
st = "CA";
state = "California";
} else if (zipcode >= 80000 && zipcode <= 81999) {
st = 'CO';
state = 'Colorado';
} else if ((zipcode >= 6000 && zipcode <= 6389) || (zipcode >= 6391 && zipcode <= 6999)) {
st = 'CT';
state = 'Connecticut';
st = "CO";
state = "Colorado";
} else if (
(zipcode >= 6000 && zipcode <= 6389) ||
(zipcode >= 6391 && zipcode <= 6999)
) {
st = "CT";
state = "Connecticut";
} else if (zipcode >= 19700 && zipcode <= 19999) {
st = 'DE';
state = 'Delaware';
st = "DE";
state = "Delaware";
} else if (zipcode >= 32000 && zipcode <= 34999) {
st = 'FL';
state = 'Florida';
} else if ((zipcode >= 30000 && zipcode <= 31999) || (zipcode >= 39800 && zipcode <= 39999)) {
st = 'GA';
state = 'Georgia';
st = "FL";
state = "Florida";
} else if (
(zipcode >= 30000 && zipcode <= 31999) ||
(zipcode >= 39800 && zipcode <= 39999)
) {
st = "GA";
state = "Georgia";
} else if (zipcode >= 96700 && zipcode <= 96999) {
st = 'HI';
state = 'Hawaii';
st = "HI";
state = "Hawaii";
} else if (zipcode >= 83200 && zipcode <= 83999 && zipcode != 83414) {
st = 'ID';
state = 'Idaho';
st = "ID";
state = "Idaho";
} else if (zipcode >= 60000 && zipcode <= 62999) {
st = 'IL';
state = 'Illinois';
st = "IL";
state = "Illinois";
} else if (zipcode >= 46000 && zipcode <= 47999) {
st = 'IN';
state = 'Indiana';
st = "IN";
state = "Indiana";
} else if (zipcode >= 50000 && zipcode <= 52999) {
st = 'IA';
state = 'Iowa';
st = "IA";
state = "Iowa";
} else if (zipcode >= 66000 && zipcode <= 67999) {
st = 'KS';
state = 'Kansas';
st = "KS";
state = "Kansas";
} else if (zipcode >= 40000 && zipcode <= 42999) {
st = 'KY';
state = 'Kentucky';
st = "KY";
state = "Kentucky";
} else if (zipcode >= 70000 && zipcode <= 71599) {
st = 'LA';
state = 'Louisiana';
st = "LA";
state = "Louisiana";
} else if (zipcode >= 3900 && zipcode <= 4999) {
st = 'ME';
state = 'Maine';
st = "ME";
state = "Maine";
} else if (zipcode >= 20600 && zipcode <= 21999) {
st = 'MD';
state = 'Maryland';
} else if ((zipcode >= 1000 && zipcode <= 2799) || (zipcode == 5501) || (zipcode == 5544)) {
st = 'MA';
state = 'Massachusetts';
st = "MD";
state = "Maryland";
} else if (
(zipcode >= 1000 && zipcode <= 2799) ||
zipcode == 5501 ||
zipcode == 5544
) {
st = "MA";
state = "Massachusetts";
} else if (zipcode >= 48000 && zipcode <= 49999) {
st = 'MI';
state = 'Michigan';
st = "MI";
state = "Michigan";
} else if (zipcode >= 55000 && zipcode <= 56899) {
st = 'MN';
state = 'Minnesota';
st = "MN";
state = "Minnesota";
} else if (zipcode >= 38600 && zipcode <= 39999) {
st = 'MS';
state = 'Mississippi';
st = "MS";
state = "Mississippi";
} else if (zipcode >= 63000 && zipcode <= 65999) {
st = 'MO';
state = 'Missouri';
st = "MO";
state = "Missouri";
} else if (zipcode >= 59000 && zipcode <= 59999) {
st = 'MT';
state = 'Montana';
st = "MT";
state = "Montana";
} else if (zipcode >= 27000 && zipcode <= 28999) {
st = 'NC';
state = 'North Carolina';
st = "NC";
state = "North Carolina";
} else if (zipcode >= 58000 && zipcode <= 58999) {
st = 'ND';
state = 'North Dakota';
st = "ND";
state = "North Dakota";
} else if (zipcode >= 68000 && zipcode <= 69999) {
st = 'NE';
state = 'Nebraska';
st = "NE";
state = "Nebraska";
} else if (zipcode >= 88900 && zipcode <= 89999) {
st = 'NV';
state = 'Nevada';
st = "NV";
state = "Nevada";
} else if (zipcode >= 3000 && zipcode <= 3899) {
st = 'NH';
state = 'New Hampshire';
st = "NH";
state = "New Hampshire";
} else if (zipcode >= 7000 && zipcode <= 8999) {
st = 'NJ';
state = 'New Jersey';
st = "NJ";
state = "New Jersey";
} else if (zipcode >= 87000 && zipcode <= 88499) {
st = 'NM';
state = 'New Mexico';
} else if ((zipcode >= 10000 && zipcode <= 14999) || (zipcode == 6390) || (zipcode == 501) || (zipcode == 544)) {
st = 'NY';
state = 'New York';
st = "NM";
state = "New Mexico";
} else if (
(zipcode >= 10000 && zipcode <= 14999) ||
zipcode == 6390 ||
zipcode == 501 ||
zipcode == 544
) {
st = "NY";
state = "New York";
} else if (zipcode >= 43000 && zipcode <= 45999) {
st = 'OH';
state = 'Ohio';
} else if ((zipcode >= 73000 && zipcode <= 73199) || (zipcode >= 73400 && zipcode <= 74999)) {
st = 'OK';
state = 'Oklahoma';
st = "OH";
state = "Ohio";
} else if (
(zipcode >= 73000 && zipcode <= 73199) ||
(zipcode >= 73400 && zipcode <= 74999)
) {
st = "OK";
state = "Oklahoma";
} else if (zipcode >= 97000 && zipcode <= 97999) {
st = 'OR';
state = 'Oregon';
st = "OR";
state = "Oregon";
} else if (zipcode >= 15000 && zipcode <= 19699) {
st = 'PA';
state = 'Pennsylvania';
st = "PA";
state = "Pennsylvania";
} else if (zipcode >= 300 && zipcode <= 999) {
st = 'PR';
state = 'Puerto Rico';
st = "PR";
state = "Puerto Rico";
} else if (zipcode >= 2800 && zipcode <= 2999) {
st = 'RI';
state = 'Rhode Island';
st = "RI";
state = "Rhode Island";
} else if (zipcode >= 29000 && zipcode <= 29999) {
st = 'SC';
state = 'South Carolina';
st = "SC";
state = "South Carolina";
} else if (zipcode >= 57000 && zipcode <= 57999) {
st = 'SD';
state = 'South Dakota';
st = "SD";
state = "South Dakota";
} else if (zipcode >= 37000 && zipcode <= 38599) {
st = 'TN';
state = 'Tennessee';
} else if ((zipcode >= 75000 && zipcode <= 79999) || (zipcode >= 73301 && zipcode <= 73399) || (zipcode >= 88500 && zipcode <= 88599)) {
st = 'TX';
state = 'Texas';
st = "TN";
state = "Tennessee";
} else if (
(zipcode >= 75000 && zipcode <= 79999) ||
(zipcode >= 73301 && zipcode <= 73399) ||
(zipcode >= 88500 && zipcode <= 88599)
) {
st = "TX";
state = "Texas";
} else if (zipcode >= 84000 && zipcode <= 84999) {
st = 'UT';
state = 'Utah';
st = "UT";
state = "Utah";
} else if (zipcode >= 5000 && zipcode <= 5999) {
st = 'VT';
state = 'Vermont';
} else if ((zipcode >= 20100 && zipcode <= 20199) || (zipcode >= 22000 && zipcode <= 24699) || (zipcode == 20598)) {
st = 'VA';
state = 'Virginia';
} else if ((zipcode >= 20000 && zipcode <= 20099) || (zipcode >= 20200 && zipcode <= 20599) || (zipcode >= 56900 && zipcode <= 56999)) {
st = 'DC';
state = 'Washington DC';
st = "VT";
state = "Vermont";
} else if (
(zipcode >= 20100 && zipcode <= 20199) ||
(zipcode >= 22000 && zipcode <= 24699) ||
zipcode == 20598
) {
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) {
st = 'WA';
state = 'Washington';
st = "WA";
state = "Washington";
} else if (zipcode >= 24700 && zipcode <= 26999) {
st = 'WV';
state = 'West Virginia';
st = "WV";
state = "West Virginia";
} else if (zipcode >= 53000 && zipcode <= 54999) {
st = 'WI';
state = 'Wisconsin';
st = "WI";
state = "Wisconsin";
} else if ((zipcode >= 82000 && zipcode <= 83199) || zipcode == 83414) {
st = 'WY';
state = 'Wyoming';
st = "WY";
state = "Wyoming";
} else {
st = 'none';
state = 'none';
st = "none";
state = "none";
}
/* Return `state` for full name or `st` for postal abbreviation */
+221 -199
View File
@@ -11,16 +11,16 @@ export namespace Printful {
code: number;
result: T[];
paging: {
total: number,
offset: number,
limit: number
}
total: number;
offset: number;
limit: number;
};
}
type Option = {
id: string;
value: string;
}
};
type File = {
type: string;
@@ -41,12 +41,12 @@ export namespace Printful {
visible: boolean;
is_temporary: boolean;
stitch_count_tier: string;
}
};
export type Product = {
sync_product: SyncProduct,
sync_variants: SyncVariant[]
}
sync_product: SyncProduct;
sync_variants: SyncVariant[];
};
export type SyncProduct = {
id: number;
@@ -56,7 +56,7 @@ export namespace Printful {
synced: number;
thumbnail_url: string;
is_ignored: boolean;
}
};
export type SyncVariant = {
id: number;
@@ -83,30 +83,30 @@ export namespace Printful {
size: string;
color: string;
availability_status: string;
}
};
}
export namespace Orders {
export type Order = {
external_id: string,
shipping: string
external_id: string;
shipping: string;
recipient: {
name: string
company?: string
address1: string
address2: string
city: string
state_code: string
state_name?: string
country_code: string
country_name?: string
zip: string
phone?: string
email?: string
tax_number?: string
}
items: Array<{ external_variant_id: string, quantity: number }>
}
name: string;
company?: string;
address1: string;
address2: string;
city: string;
state_code: string;
state_name?: string;
country_code: string;
country_name?: string;
zip: string;
phone?: string;
email?: string;
tax_number?: string;
};
items: Array<{ external_variant_id: string; quantity: number }>;
};
}
export namespace Webhook {
@@ -116,16 +116,19 @@ export namespace Printful {
created: number;
retries: number;
store: number;
data: D
data: D;
}
export enum Event {
ProductUpdated = "product_updated",
ProductDeleted = "product_deleted",
PackageShipped = "package_shipped"
PackageShipped = "package_shipped",
}
export interface ProductUpdated extends MetaData<Event.ProductUpdated, {
export interface ProductUpdated
extends MetaData<
Event.ProductUpdated,
{
sync_product: {
id: number;
external_id: string;
@@ -135,36 +138,48 @@ export namespace Printful {
thumbnail_url: string;
is_ignored: boolean;
};
}> { }
}
> {}
export interface ProductDeleted extends MetaData<Event.ProductDeleted, {
export interface ProductDeleted
extends MetaData<
Event.ProductDeleted,
{
sync_product: {
id: number;
external_id: string;
name: string;
};
}> { }
}
> {}
export interface PackageShipped extends MetaData<Event.PackageShipped, {
export interface PackageShipped
extends MetaData<
Event.PackageShipped,
{
shipment: {
id: number
carrier: string,
service: string,
tracking_number: string
tracking_url: string
created: number
ship_date: string
shipped_at: number
reshipment: boolean
}
id: number;
carrier: string;
service: string;
tracking_number: string;
tracking_url: string;
created: number;
ship_date: string;
shipped_at: number;
reshipment: boolean;
};
order: {
id: number
external_id: string
status: string
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 Skus {
export type Sku = {
id: string,
id: string;
fieldData: {
name: string;
slug: string;
"sku-values"?: Record<string, string>,
"sku-values"?: Record<string, string>;
price: {
value: number;
unit: string;
currency: string;
};
"main-image"?: string;
"more-images"?:
{
fileId?: string,
url: string,
alt?: string,
}[]
}
}
"more-images"?: {
fileId?: string;
url: string;
alt?: string;
}[];
};
};
}
export type Product = {
id: string,
id: string;
fieldData: {
name: string;
slug: string;
@@ -210,12 +224,12 @@ export namespace Webflow {
slug: string;
}[];
}[];
}
}
};
};
export interface ProductAndSkus {
product: Product
skus: Skus.Sku[]
product: Product;
skus: Skus.Sku[];
}
export interface ProductAndSku {
@@ -227,85 +241,91 @@ export namespace Webflow {
export namespace Orders {
export type Order = {
orderId: string
status: "pending" | "unfulfilled" | "fulfilled" | "disputed" | "dispute-lost" | "refunded"
comment: string
orderComment: string
acceptedOn: string
fulfilledOn: string | null
refundedOn: string | null
disputedOn: string | null
disputeUpdatedOn: string | null
disputeLastStatus: string | null
orderId: string;
status:
| "pending"
| "unfulfilled"
| "fulfilled"
| "disputed"
| "dispute-lost"
| "refunded";
comment: string;
orderComment: string;
acceptedOn: string;
fulfilledOn: string | null;
refundedOn: string | null;
disputedOn: string | null;
disputeUpdatedOn: string | null;
disputeLastStatus: string | null;
customerPaid: {
unit: string
value: string
string: string
}
unit: string;
value: string;
string: string;
};
netAmount: {
unit: string
value: string
string: string
}
unit: string;
value: string;
string: string;
};
applicationFee: {
unit: string
value: string
string: string
}
unit: string;
value: string;
string: string;
};
allAddresses: Array<{
type: string
addressee: string
line1: string
line2: string
city: string
state: string
country: string
postalCode: string
}>
type: string;
addressee: string;
line1: string;
line2: string;
city: string;
state: string;
country: string;
postalCode: string;
}>;
shippingAddress: {
type: string
japanType: string
addressee: string
line1: string
line2: string
city: string
state: string
country: string
postalCode: string
}
type: string;
japanType: string;
addressee: string;
line1: string;
line2: string;
city: string;
state: string;
country: string;
postalCode: string;
};
billingAddress: {
type: string
addressee: string
line1: string
line2: string
city: string
state: string
country: string
postalCode: string
}
shippingProvider: string
shippingTracking: string
shippingTrackingURL: string
type: string;
addressee: string;
line1: string;
line2: string;
city: string;
state: string;
country: string;
postalCode: string;
};
shippingProvider: string;
shippingTracking: string;
shippingTrackingURL: string;
customerInfo: {
fullName: string
email: string
}
fullName: string;
email: string;
};
purchasedItems: Array<{
count: number
count: number;
rowTotal: {
unit: string
value: string
string: string
}
productId: string
productName: string
productSlug: string
variantId: string
variantName: string
variantSlug: string
variantSKU: string
unit: string;
value: string;
string: string;
};
productId: string;
productName: string;
productSlug: string;
variantId: string;
variantName: string;
variantSlug: string;
variantSKU: string;
variantImage: {
url: string
url: string;
file: {
size: number;
originalFileName: string;
@@ -315,95 +335,97 @@ export namespace Webflow {
height: number;
variants: {
size: number;
url: string,
url: string;
originalFileName: string;
width: number;
height: number;
}[]
}
}
}[];
};
};
variantPrice: {
unit: string
value: string
string: string
}
weight: number
width: number
height: number
length: number
}>
purchasedItemsCount: number
unit: string;
value: string;
string: string;
};
weight: number;
width: number;
height: number;
length: number;
}>;
purchasedItemsCount: number;
stripeDetails: {
subscriptionId: string | null
paymentMethod: string
paymentIntentId: string
customerId: string
chargeId: string
disputeId: string | null
refundId: string
refundReason: string
}
subscriptionId: string | null;
paymentMethod: string;
paymentIntentId: string;
customerId: string;
chargeId: string;
disputeId: string | null;
refundId: string;
refundReason: string;
};
stripeCard: {
last4: string
brand: string
ownerName: string
last4: string;
brand: string;
ownerName: string;
expires: {
year: number
month: number
}
}
paypalDetails: Object
customData: Array<Object>
year: number;
month: number;
};
};
paypalDetails: Object;
customData: Array<Object>;
metadata: {
isBuyNow: boolean
hasDownloads: boolean
paymentProcessor: string
}
isCustomerDeleted: boolean
isShippingRequired: boolean
isBuyNow: boolean;
hasDownloads: boolean;
paymentProcessor: string;
};
isCustomerDeleted: boolean;
isShippingRequired: boolean;
totals: {
subtotal: {
unit: string
value: string
string: string
}
unit: string;
value: string;
string: string;
};
extras: Array<{
type: string
name: string
description: string
type: string;
name: string;
description: string;
price: {
unit: string
value: string
string: string
}
}>
unit: string;
value: string;
string: string;
};
}>;
total: {
unit: string
value: string
string: string
}
}
unit: string;
value: string;
string: string;
};
};
downloadFiles: Array<{
id: string
name: string
url: string
}>
}
id: string;
name: string;
url: string;
}>;
};
}
export namespace Webhook {
interface MetaData<E extends Event, D> {
triggerType: E;
payload: D
payload: D;
}
export enum Event {
OrderCreated = "ecomm_new_order",
OrderUpdated = "ecomm_order_changed"
OrderUpdated = "ecomm_order_changed",
}
export interface OrderCreated extends MetaData<Event.OrderCreated, Orders.Order> { }
export interface OrderUpdated extends MetaData<Event.OrderUpdated, Orders.Order> { }
export interface OrderCreated
extends MetaData<Event.OrderCreated, Orders.Order> {}
export interface OrderUpdated
extends MetaData<Event.OrderUpdated, Orders.Order> {}
export type EventPayload = OrderCreated | OrderUpdated;
}
+125 -61
View File
@@ -5,48 +5,69 @@ import crypto from "node:crypto";
export default class WebflowService {
static Products = class {
static Skus = class {
static async create(webflowProductId: string, skus: DeepPartial<Webflow.Products.Skus.Sku>[]) {
const res = await fetch(`${env().API_SITES_URL}/products/${webflowProductId}/skus`, {
static async create(
webflowProductId: string,
skus: DeepPartial<Webflow.Products.Skus.Sku>[],
) {
const res = await fetch(
`${env().API_SITES_URL}/products/${webflowProductId}/skus`,
{
method: "POST",
headers: {
"Content-Type": "application/json",
...env().AUTH_HEADER
...env().AUTH_HEADER,
},
body: JSON.stringify({
"skus": skus
})
});
skus: skus,
}),
},
);
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>) {
const res = await fetch(`${env().API_SITES_URL}/products/${webflowProductId}/skus/${webflowSkuId}`, {
static async update(
webflowProductId: string,
webflowSkuId: string,
webflowSku: DeepPartial<Webflow.Products.Skus.Sku>,
) {
const res = await fetch(
`${env().API_SITES_URL}/products/${webflowProductId}/skus/${webflowSkuId}`,
{
method: "PATCH",
headers: {
"Content-Type": "application/json",
...env().AUTH_HEADER
...env().AUTH_HEADER,
},
body: JSON.stringify({
"sku": webflowSku
})
});
sku: webflowSku,
}),
},
);
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`, {
method: "POST",
headers: {
"Content-Type": "application/json",
...env().AUTH_HEADER
...env().AUTH_HEADER,
},
body: JSON.stringify(webflowProductAndSku)
body: JSON.stringify(webflowProductAndSku),
});
if (!res.ok) {
@@ -54,7 +75,7 @@ export default class WebflowService {
}
const createdWebflowProduct = await res.json();
return createdWebflowProduct.product.id
return createdWebflowProduct.product.id;
}
static async getAll(): Promise<Webflow.Products.ProductAndSkus[]> {
@@ -62,7 +83,7 @@ export default class WebflowService {
method: "GET",
headers: {
...env().AUTH_HEADER,
}
},
});
if (!res.ok) {
@@ -73,13 +94,18 @@ export default class WebflowService {
return payload.items as Webflow.Products.ProductAndSkus[];
}
static async get(webflowProductId: string): Promise<Webflow.Products.ProductAndSkus | undefined> {
const res = await fetch(`${env().API_SITES_URL}/products/${webflowProductId}`, {
static async get(
webflowProductId: string,
): Promise<Webflow.Products.ProductAndSkus | undefined> {
const res = await fetch(
`${env().API_SITES_URL}/products/${webflowProductId}`,
{
method: "GET",
headers: {
...env().AUTH_HEADER,
}
});
},
},
);
if (res.status === 404 || res.status === 400) {
return;
@@ -93,15 +119,21 @@ export default class WebflowService {
return payload as Webflow.Products.ProductAndSkus;
}
static async update(webflowProductId: string, webflowProduct: DeepPartial<Webflow.Products.ProductAndSku>) {
const res = await fetch(`${env().API_SITES_URL}/products/${webflowProductId}`, {
static async update(
webflowProductId: string,
webflowProduct: DeepPartial<Webflow.Products.ProductAndSku>,
) {
const res = await fetch(
`${env().API_SITES_URL}/products/${webflowProductId}`,
{
method: "PATCH",
headers: {
"Content-Type": "application/json",
...env().AUTH_HEADER
...env().AUTH_HEADER,
},
body: JSON.stringify(webflowProduct)
});
body: JSON.stringify(webflowProduct),
},
);
if (!res.ok) {
throw new FetchError("Failed to update Webflow product", res);
@@ -109,33 +141,41 @@ export default class WebflowService {
}
static async remove(webflowProductId: string) {
const res = await fetch(`${env().API_COLLECTIONS_URL}/items/${webflowProductId}`, {
const res = await fetch(
`${env().API_COLLECTIONS_URL}/items/${webflowProductId}`,
{
method: "DELETE",
headers: {
"Content-Type": "application/json",
...env().AUTH_HEADER
...env().AUTH_HEADER,
},
body: JSON.stringify({})
});
body: JSON.stringify({}),
},
);
if (!res.ok) {
throw new FetchError("Failed to remove Webflow product", res);
}
}
}
};
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
const params = new URLSearchParams();
if (opt.status !== undefined) {
params.set("status", opt.status);
}
const res = await fetch(`${env().API_SITES_URL}/orders?${params.toString()}`, {
const res = await fetch(
`${env().API_SITES_URL}/orders?${params.toString()}`,
{
method: "GET",
headers: { ...env().AUTH_HEADER },
});
},
);
if (!res.ok) {
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[];
}
static async update(webflowOrderId: string, webflowOrderUpdate: { comment?: string, shippingProvider?: string, shippingTracking?: string, shippingTrackingURL: string }) {
const res = await fetch(`${env().API_SITES_URL}/orders/${webflowOrderId}`, {
static async update(
webflowOrderId: string,
webflowOrderUpdate: {
comment?: string;
shippingProvider?: string;
shippingTracking?: string;
shippingTrackingURL: string;
},
) {
const res = await fetch(
`${env().API_SITES_URL}/orders/${webflowOrderId}`,
{
method: "PATCH",
headers: {
"Content-Type": "application/json",
...env().AUTH_HEADER
...env().AUTH_HEADER,
},
body: JSON.stringify(webflowOrderUpdate)
});
body: JSON.stringify(webflowOrderUpdate),
},
);
if (!res.ok) {
throw new FetchError("Failed to update Webflow order", res);
}
}
static async fulfill(webflowOrderId: string, opt: { sendOrderFulfilledEmail?: boolean } = {}) {
const res = await fetch(`${env().API_SITES_URL}/orders/${webflowOrderId}/fulfill`, {
static async fulfill(
webflowOrderId: string,
opt: { sendOrderFulfilledEmail?: boolean } = {},
) {
const res = await fetch(
`${env().API_SITES_URL}/orders/${webflowOrderId}/fulfill`,
{
method: "POST",
headers: {
"Content-Type": "application/json",
...env().AUTH_HEADER
...env().AUTH_HEADER,
},
body: JSON.stringify(opt)
});
body: JSON.stringify(opt),
},
);
if (!res.ok) {
throw new FetchError("Failed to fulfill Webflow order", res);
}
}
}
};
static Util = class {
static verifyWebflowSignature(request: Request, body: unknown) {
@@ -183,7 +240,9 @@ export default class WebflowService {
if (!timestamp) {
throw new Error("No timestamp provided");
}
const providedSignature = request.headers.get("x-webflow-signature");
const providedSignature = request.headers.get(
"x-webflow-signature",
);
if (!providedSignature) {
throw new Error("No signature provided");
}
@@ -199,47 +258,52 @@ export default class WebflowService {
const requestTimestamp = parseInt(timestamp, 10);
const data = `${requestTimestamp}:${JSON.stringify(body)}`;
const hash = crypto.createHmac('sha256', secret)
const hash = crypto
.createHmac("sha256", secret)
.update(data)
.digest('hex');
.digest("hex");
if (!crypto.timingSafeEqual(Buffer.from(hash, 'hex'), Buffer.from(providedSignature, 'hex'))) {
throw new Error('Invalid signature');
if (
!crypto.timingSafeEqual(
Buffer.from(hash, "hex"),
Buffer.from(providedSignature, "hex"),
)
) {
throw new Error("Invalid signature");
}
const currentTime = Date.now();
if (currentTime - requestTimestamp > 300000) {
throw new Error('Request is older than 5 minutes');
throw new Error("Request is older than 5 minutes");
}
return true;
} catch (err) {
console.error(`Error verifying signature: ${err}`);
return false;
}
}
}
};
}
const env = () => {
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 = {
SITE_ID: Bun.env.WEBFLOW_SITE_ID,
COLLECTIONS_ID: Bun.env.WEBFLOW_COLLECTION_ID,
AUTH_TOKEN: Bun.env.WEBFLOW_AUTH,
WEBHOOK_SECRET: Bun.env.WEBFLOW_WEBHOOK_SECRET
WEBHOOK_SECRET: Bun.env.WEBFLOW_WEBHOOK_SECRET,
};
return {
...vars,
API_SITES_URL: `https://api.webflow.com/v2/sites/${vars.SITE_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 SyncService from "$lib/services/commerce/sync";
import { FetchError, type DeepPartial } from "$lib/services/commerce/util/misc";
import type { Printful } from "$lib/services/commerce/util/types";
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 = {
metrics: {
age: 25,
weight: lbToKg(190),
gender: Gender.Male
}
gender: Gender.Male,
},
};
const computedPerformances: ActivityPerformance[] = [
// STRENGTH
{
activity: Activity.BenchPress,
performance: lbToKg(225)
performance: lbToKg(225),
},
{
activity: Activity.Deadlift,
@@ -115,7 +127,6 @@ console.log(pOrders);
// }
// console.log("DONE");
// await WebflowService.Products.update(wProduct?.product.id!, wProduct!);
//
// 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 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 = {
isValid: boolean,
message: string
}
isValid: boolean;
message: string;
};
const validateEquality = (wValue: any, pValue: any): Validation => {
const validation = {
@@ -17,12 +17,11 @@ const validateEquality = (wValue: any, pValue: any): Validation => {
if (wValue !== pValue) {
validation.isValid = false;
validation.message = `${wValue} !== ${pValue}`
};
validation.message = `${wValue} !== ${pValue}`;
}
return validation;
}
};
const wProducts = await WebflowService.Products.getAll();
@@ -31,14 +30,25 @@ for (const wProduct of wProducts) {
for (const sku of wProduct.skus ?? []) {
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) {
console.log("Printful: " + pVariant.id, pVariant.name);
const validations = [
// validateEquality(sku.fieldData.name, pVariant.name),
validateEquality(sku.fieldData["sku-values"]?.["color"], SyncService.findColorInProductName(pVariant.name)),
validateEquality(sku.fieldData["sku-values"]?.["size"], pVariant.size),
validateEquality(sku.fieldData.price.value, Math.floor(+pVariant.retail_price * 100)),
validateEquality(
sku.fieldData["sku-values"]?.["color"],
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) {
@@ -46,17 +56,15 @@ for (const wProduct of wProducts) {
// 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) {
++invalidCount;
console.log("INVALID: ");
console.log(errors.map(e => e.message).join(", "));
}
else {
console.log(errors.map((e) => e.message).join(", "));
} else {
console.log("VALID");
}
}
else {
} else {
++invalidCount;
console.log("INVALID");
console.log("Not found");
+4 -4
View File
@@ -1,6 +1,6 @@
import { redirect } from '@sveltejs/kit';
import type { PageServerLoad } from './$types';
import { redirect } from "@sveltejs/kit";
import type { PageServerLoad } from "./$types";
export const load: PageServerLoad = ({ }) => {
redirect(308, '/calculator');
export const load: PageServerLoad = ({}) => {
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>
export const fallback: RequestHandler = ({ request }) => app.handle(request)
type RequestHandler = (v: { request: Request }) => Response | Promise<Response>;
export const fallback: RequestHandler = ({ request }) => app.handle(request);
+5 -5
View File
@@ -1,12 +1,12 @@
import PrintfulService from '$lib/services/commerce/printful';
import WebflowService from '$lib/services/commerce/webflow';
import type { PageServerLoad } from './$types';
import PrintfulService from "$lib/services/commerce/printful";
import WebflowService from "$lib/services/commerce/webflow";
import type { PageServerLoad } from "./$types";
export const load = async ({ }: Parameters<PageServerLoad>[0]) => {
export const load = async ({}: Parameters<PageServerLoad>[0]) => {
return {
products: {
printful: PrintfulService.Products.getAll(),
webflow: WebflowService.Products.getAll(),
}
},
};
};
+2 -2
View File
@@ -1,5 +1,5 @@
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} */
const config = {
@@ -12,7 +12,7 @@ const config = {
// 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.
adapter: adapter(),
}
},
};
export default config;
+1 -2
View File
@@ -10,8 +10,7 @@
"skipLibCheck": true,
"sourceMap": true,
"strict": true,
"moduleResolution": "bundler",
"importsNotUsedAsValue": true
"moduleResolution": "bundler"
}
// 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
+3 -3
View File
@@ -1,7 +1,7 @@
import tailwindcss from "@tailwindcss/vite";
import { sveltekit } from '@sveltejs/kit/vite';
import { defineConfig } from 'vite';
import { sveltekit } from "@sveltejs/kit/vite";
import { defineConfig } from "vite";
export default defineConfig({
plugins: [tailwindcss(), sveltekit()]
plugins: [tailwindcss(), sveltekit()],
});