breakout into monorepo
This commit is contained in:
@@ -1,5 +0,0 @@
|
||||
fly.toml
|
||||
Dockerfile
|
||||
.dockerignore
|
||||
node_modules
|
||||
.git
|
||||
+33
-3
@@ -1,4 +1,34 @@
|
||||
node_modules/
|
||||
# dependencies (bun install)
|
||||
node_modules
|
||||
|
||||
# output
|
||||
out
|
||||
dist
|
||||
*.tgz
|
||||
|
||||
# code coverage
|
||||
coverage
|
||||
*.lcov
|
||||
|
||||
# logs
|
||||
logs
|
||||
_.log
|
||||
report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
|
||||
|
||||
# dotenv environment variable files
|
||||
.env
|
||||
.svelte-kit/
|
||||
build/
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
.env.local
|
||||
|
||||
# caches
|
||||
.eslintcache
|
||||
.cache
|
||||
*.tsbuildinfo
|
||||
|
||||
# IntelliJ based IDEs
|
||||
.idea
|
||||
|
||||
# Finder (MacOS) folder config
|
||||
.DS_Store
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
// Folder-specific settings
|
||||
//
|
||||
// For a full list of overridable settings, and general information on folder-specific settings,
|
||||
// see the documentation: https://zed.dev/docs/configuring-zed#settings-files
|
||||
{}
|
||||
|
||||
-39
@@ -1,39 +0,0 @@
|
||||
# syntax = docker/dockerfile:1
|
||||
|
||||
FROM oven/bun:1.2.12-slim as base
|
||||
|
||||
LABEL fly_launch_runtime="Bun"
|
||||
|
||||
# NodeJS app lives here
|
||||
WORKDIR /app
|
||||
|
||||
# Set production environment
|
||||
ENV NODE_ENV=production
|
||||
|
||||
|
||||
# Throw-away build stage to reduce size of final image
|
||||
FROM base as build
|
||||
|
||||
# Install packages needed to build node modules
|
||||
RUN apt-get update -qq && \
|
||||
apt-get install -y python-is-python3 pkg-config build-essential
|
||||
|
||||
# Install node modules
|
||||
COPY --link package.json .
|
||||
# RUN bun install --production
|
||||
RUN bun install
|
||||
|
||||
# Copy application code
|
||||
COPY --link . .
|
||||
|
||||
# Final stage for app image
|
||||
FROM base
|
||||
|
||||
# Copy built application
|
||||
COPY --from=build /app /app
|
||||
|
||||
RUN apt-get update && apt-get install -y sqlite3 && rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Start the server by default, this can be overwritten at runtime
|
||||
CMD [ "bun", "--bun", "run", "build" ]
|
||||
CMD [ "bun", "./build/index.js" ]
|
||||
@@ -1,5 +0,0 @@
|
||||
deploy-secrets:
|
||||
fly secrets import < .env
|
||||
|
||||
deploy:
|
||||
bun vite build && fly deploy
|
||||
@@ -0,0 +1,15 @@
|
||||
# backend
|
||||
|
||||
To install dependencies:
|
||||
|
||||
```bash
|
||||
bun install
|
||||
```
|
||||
|
||||
To run:
|
||||
|
||||
```bash
|
||||
bun run index.ts
|
||||
```
|
||||
|
||||
This project was created using `bun init` in bun v1.3.0. [Bun](https://bun.com) is a fast all-in-one JavaScript runtime.
|
||||
@@ -0,0 +1,81 @@
|
||||
{
|
||||
"lockfileVersion": 1,
|
||||
"workspaces": {
|
||||
"": {
|
||||
"name": "backend",
|
||||
"dependencies": {
|
||||
"@elysiajs/cors": "^1.4.1",
|
||||
"elysia": "^1.4.27",
|
||||
"ml-levenberg-marquardt": "^5.0.0",
|
||||
"zipcodes-us": "^1.1.2",
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/bun": "latest",
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": "^5",
|
||||
},
|
||||
},
|
||||
},
|
||||
"packages": {
|
||||
"@borewit/text-codec": ["@borewit/text-codec@0.2.1", "", {}, "sha512-k7vvKPbf7J2fZ5klGRD9AeKfUvojuZIQ3BT5u7Jfv+puwXkUBUT5PVyMDfJZpy30CBDXGMgw7fguK/lpOMBvgw=="],
|
||||
|
||||
"@elysiajs/cors": ["@elysiajs/cors@1.4.1", "", { "peerDependencies": { "elysia": ">= 1.4.0" } }, "sha512-lQfad+F3r4mNwsxRKbXyJB8Jg43oAOXjRwn7sKUL6bcOW3KjUqUimTS+woNpO97efpzjtDE0tEjGk9DTw8lqTQ=="],
|
||||
|
||||
"@sinclair/typebox": ["@sinclair/typebox@0.34.48", "", {}, "sha512-kKJTNuK3AQOrgjjotVxMrCn1sUJwM76wMszfq1kdU4uYVJjvEWuFQ6HgvLt4Xz3fSmZlTOxJ/Ie13KnIcWQXFA=="],
|
||||
|
||||
"@tokenizer/inflate": ["@tokenizer/inflate@0.4.1", "", { "dependencies": { "debug": "^4.4.3", "token-types": "^6.1.1" } }, "sha512-2mAv+8pkG6GIZiF1kNg1jAjh27IDxEPKwdGul3snfztFerfPGI1LjDezZp3i7BElXompqEtPmoPx6c2wgtWsOA=="],
|
||||
|
||||
"@tokenizer/token": ["@tokenizer/token@0.3.0", "", {}, "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A=="],
|
||||
|
||||
"@types/bun": ["@types/bun@1.3.9", "", { "dependencies": { "bun-types": "1.3.9" } }, "sha512-KQ571yULOdWJiMH+RIWIOZ7B2RXQGpL1YQrBtLIV3FqDcCu6FsbFUBwhdKUlCKUpS3PJDsHlJ1QKlpxoVR+xtw=="],
|
||||
|
||||
"@types/node": ["@types/node@25.3.3", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-DpzbrH7wIcBaJibpKo9nnSQL0MTRdnWttGyE5haGwK86xgMOkFLp7vEyfQPGLOJh5wNYiJ3V9PmUMDhV9u8kkQ=="],
|
||||
|
||||
"bun-types": ["bun-types@1.3.9", "", { "dependencies": { "@types/node": "*" } }, "sha512-+UBWWOakIP4Tswh0Bt0QD0alpTY8cb5hvgiYeWCMet9YukHbzuruIEeXC2D7nMJPB12kbh8C7XJykSexEqGKJg=="],
|
||||
|
||||
"cookie": ["cookie@1.1.1", "", {}, "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ=="],
|
||||
|
||||
"debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
|
||||
|
||||
"elysia": ["elysia@1.4.27", "", { "dependencies": { "cookie": "^1.1.1", "exact-mirror": "^0.2.7", "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-2UlmNEjPJVA/WZVPYKy+KdsrfFwwNlqSBW1lHz6i2AHc75k7gV4Rhm01kFeotH7PDiHIX2G8X3KnRPc33SGVIg=="],
|
||||
|
||||
"exact-mirror": ["exact-mirror@0.2.7", "", { "peerDependencies": { "@sinclair/typebox": "^0.34.15" }, "optionalPeers": ["@sinclair/typebox"] }, "sha512-+MeEmDcLA4o/vjK2zujgk+1VTxPR4hdp23qLqkWfStbECtAq9gmsvQa3LW6z/0GXZyHJobrCnmy1cdeE7BjsYg=="],
|
||||
|
||||
"fast-decode-uri-component": ["fast-decode-uri-component@1.0.1", "", {}, "sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg=="],
|
||||
|
||||
"file-type": ["file-type@21.3.0", "", { "dependencies": { "@tokenizer/inflate": "^0.4.1", "strtok3": "^10.3.4", "token-types": "^6.1.1", "uint8array-extras": "^1.4.0" } }, "sha512-8kPJMIGz1Yt/aPEwOsrR97ZyZaD1Iqm8PClb1nYFclUCkBi0Ma5IsYNQzvSFS9ib51lWyIw5mIT9rWzI/xjpzA=="],
|
||||
|
||||
"ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="],
|
||||
|
||||
"is-any-array": ["is-any-array@2.0.1", "", {}, "sha512-UtilS7hLRu++wb/WBAw9bNuP1Eg04Ivn1vERJck8zJthEvXCBEBpGR/33u/xLKWEQf95803oalHrVDptcAvFdQ=="],
|
||||
|
||||
"memoirist": ["memoirist@0.4.0", "", {}, "sha512-zxTgA0mSYELa66DimuNQDvyLq36AwDlTuVRbnQtB+VuTcKWm5Qc4z3WkSpgsFWHNhexqkIooqpv4hdcqrX5Nmg=="],
|
||||
|
||||
"ml-array-max": ["ml-array-max@1.2.4", "", { "dependencies": { "is-any-array": "^2.0.0" } }, "sha512-BlEeg80jI0tW6WaPyGxf5Sa4sqvcyY6lbSn5Vcv44lp1I2GR6AWojfUvLnGTNsIXrZ8uqWmo8VcG1WpkI2ONMQ=="],
|
||||
|
||||
"ml-array-min": ["ml-array-min@1.2.3", "", { "dependencies": { "is-any-array": "^2.0.0" } }, "sha512-VcZ5f3VZ1iihtrGvgfh/q0XlMobG6GQ8FsNyQXD3T+IlstDv85g8kfV0xUG1QPRO/t21aukaJowDzMTc7j5V6Q=="],
|
||||
|
||||
"ml-array-rescale": ["ml-array-rescale@1.3.7", "", { "dependencies": { "is-any-array": "^2.0.0", "ml-array-max": "^1.2.4", "ml-array-min": "^1.2.3" } }, "sha512-48NGChTouvEo9KBctDfHC3udWnQKNKEWN0ziELvY3KG25GR5cA8K8wNVzracsqSW1QEkAXjTNx+ycgAv06/1mQ=="],
|
||||
|
||||
"ml-levenberg-marquardt": ["ml-levenberg-marquardt@5.0.0", "", { "dependencies": { "is-any-array": "^2.0.1", "ml-matrix": "^6.12.1" } }, "sha512-vCFoO2DyYKGZFp+KxofN2cAvSc/3gufJHj/oaDHA2nV+NZH509fD7m35zQR2ZXA8fepxpVrwgCAQ0aAIsV2WtA=="],
|
||||
|
||||
"ml-matrix": ["ml-matrix@6.12.1", "", { "dependencies": { "is-any-array": "^2.0.1", "ml-array-rescale": "^1.3.7" } }, "sha512-TJ+8eOFdp+INvzR4zAuwBQJznDUfktMtOB6g/hUcGh3rcyjxbz4Te57Pgri8Q9bhSQ7Zys4IYOGhFdnlgeB6Lw=="],
|
||||
|
||||
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
|
||||
|
||||
"openapi-types": ["openapi-types@12.1.3", "", {}, "sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw=="],
|
||||
|
||||
"strtok3": ["strtok3@10.3.4", "", { "dependencies": { "@tokenizer/token": "^0.3.0" } }, "sha512-KIy5nylvC5le1OdaaoCJ07L+8iQzJHGH6pWDuzS+d07Cu7n1MZ2x26P8ZKIWfbK02+XIL8Mp4RkWeqdUCrDMfg=="],
|
||||
|
||||
"token-types": ["token-types@6.1.2", "", { "dependencies": { "@borewit/text-codec": "^0.2.1", "@tokenizer/token": "^0.3.0", "ieee754": "^1.2.1" } }, "sha512-dRXchy+C0IgK8WPC6xvCHFRIWYUbqqdEIKPaKo/AcTUNzwLTK6AH7RjdLWsEZcAN/TBdtfUw3PYEgPr5VPr6ww=="],
|
||||
|
||||
"typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
|
||||
|
||||
"uint8array-extras": ["uint8array-extras@1.5.0", "", {}, "sha512-rvKSBiC5zqCCiDZ9kAOszZcDvdAHwwIKJG33Ykj43OKcWsnmcBRL09YTU4nOeHZ8Y2a7l1MgTd08SBe9A8Qj6A=="],
|
||||
|
||||
"undici-types": ["undici-types@7.18.2", "", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="],
|
||||
|
||||
"zipcodes-us": ["zipcodes-us@1.1.2", "", {}, "sha512-UmUTS0O0/IHb0hbxv5r6IfATvD2B7FKcJTLsSv/dENBRiNACFUrGmhPuVss3kAhuhEpPH6NwKHJ7/rGMNXq5yA=="],
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"name": "@blade-and-brawn/api",
|
||||
"module": "index.ts",
|
||||
"type": "module",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@elysiajs/cors": "^1.4.1",
|
||||
"elysia": "^1.4.27",
|
||||
"zipcodes-us": "^1.1.2",
|
||||
"@blade-and-brawn/calculator": "workspace:*",
|
||||
"@blade-and-brawn/commerce": "workspace:*",
|
||||
"ml-levenberg-marquardt": "^5.0.0"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,195 @@
|
||||
import {
|
||||
DEFAULT_STANDARDS_DATA,
|
||||
LevelCalculator,
|
||||
Standards,
|
||||
type ActivityPerformance,
|
||||
type Player,
|
||||
} from "@blade-and-brawn/calculator";
|
||||
import { cors } from "@elysiajs/cors";
|
||||
import { Elysia, NotFoundError } from "elysia";
|
||||
import { PrintfulService, SyncService, WebflowService, Printful, Webflow, FetchError } from "@blade-and-brawn/commerce";
|
||||
import zipcodesUs from "zipcodes-us";
|
||||
|
||||
const levelCalculator = new LevelCalculator(new Standards(DEFAULT_STANDARDS_DATA));
|
||||
|
||||
export const app = new Elysia()
|
||||
.use(
|
||||
cors({
|
||||
origin: [
|
||||
// bladeandbrawn.com (apex + any subdomain)
|
||||
/^https?:\/\/([a-z0-9-]+\.)?bladeandbrawn\.com$/i,
|
||||
// Webflow preview domains
|
||||
/^https?:\/\/([a-z0-9-]+\.)?bladeandbrawn\.webflow\.io$/i,
|
||||
// Fly app host (this one already works)
|
||||
/^https?:\/\/blade-and-brawn\.fly\.dev$/i,
|
||||
// Local dev portal
|
||||
"http://localhost:5173",
|
||||
],
|
||||
}),
|
||||
)
|
||||
|
||||
.error({
|
||||
FetchError,
|
||||
})
|
||||
|
||||
.onError(async ({ code, error }) => {
|
||||
switch (code) {
|
||||
case "FetchError":
|
||||
console.error(error.message, await error.parse());
|
||||
return error;
|
||||
}
|
||||
})
|
||||
|
||||
.onAfterHandle(({ request, set }) => {
|
||||
console.log(
|
||||
JSON.stringify({
|
||||
lvl: "info",
|
||||
msg: "req",
|
||||
method: request.method,
|
||||
path: new URL(request.url).pathname,
|
||||
status: set.status ?? 200,
|
||||
}),
|
||||
);
|
||||
})
|
||||
|
||||
.get("/", () => "Hello world")
|
||||
|
||||
// CALCULATOR
|
||||
|
||||
.post("/calculate", async ({ body, query }) => {
|
||||
interface CalcRequest {
|
||||
player: Player;
|
||||
activityPerformances: ActivityPerformance[];
|
||||
}
|
||||
const { player, activityPerformances } = body as CalcRequest;
|
||||
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(() => {});
|
||||
// }
|
||||
|
||||
return output;
|
||||
})
|
||||
|
||||
// COMMERCE
|
||||
// TODO: protect with api key/token
|
||||
.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;
|
||||
await SyncService.sync(printfulProductId);
|
||||
return { ok: true };
|
||||
}),
|
||||
)
|
||||
.get("/:printfulProductId", async ({ params }) => {
|
||||
const printfulProductId = +params.printfulProductId;
|
||||
const printfulProduct =
|
||||
await PrintfulService.Products.get(printfulProductId);
|
||||
if (!printfulProduct) {
|
||||
return new NotFoundError();
|
||||
}
|
||||
|
||||
const webflowProductId =
|
||||
printfulProduct.sync_product.external_id.split("-")[0];
|
||||
if (!webflowProductId) {
|
||||
return new NotFoundError();
|
||||
}
|
||||
|
||||
const webflowProduct =
|
||||
await WebflowService.Products.get(webflowProductId);
|
||||
|
||||
return { printfulProduct, webflowProduct };
|
||||
}),
|
||||
)
|
||||
|
||||
// WEBHOOKS
|
||||
.post("/webhook/printful", async ({ body }) => {
|
||||
const payload = body as Printful.Webhook.EventPayload;
|
||||
|
||||
switch (payload.type) {
|
||||
case Printful.Webhook.Event.ProductUpdated: {
|
||||
const printfulProduct = payload.data.sync_product;
|
||||
await SyncService.sync(printfulProduct.id);
|
||||
break;
|
||||
}
|
||||
case Printful.Webhook.Event.ProductDeleted: {
|
||||
await WebflowService.Products.remove(
|
||||
payload.data.sync_product.external_id,
|
||||
);
|
||||
break;
|
||||
}
|
||||
case Printful.Webhook.Event.PackageShipped: {
|
||||
const webflowOrderId = payload.data.order.external_id;
|
||||
const shipInfo = payload.data.shipment;
|
||||
|
||||
await WebflowService.Orders.update(webflowOrderId, {
|
||||
shippingTrackingURL: shipInfo.tracking_url,
|
||||
shippingTracking: shipInfo.tracking_number,
|
||||
shippingProvider: shipInfo.carrier,
|
||||
});
|
||||
await WebflowService.Orders.fulfill(webflowOrderId, {
|
||||
sendOrderFulfilledEmail: true,
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return { ok: true };
|
||||
})
|
||||
.post("/webhook/webflow", async ({ request, body, set }) => {
|
||||
if (!WebflowService.Util.verifyWebflowSignature(request, body)) {
|
||||
set.status = 400;
|
||||
return "Invalid signature";
|
||||
}
|
||||
|
||||
const payload = body as Webflow.Webhook.EventPayload;
|
||||
|
||||
switch (payload.triggerType) {
|
||||
case Webflow.Webhook.Event.OrderCreated: {
|
||||
const webflowOrder = payload.payload;
|
||||
|
||||
await PrintfulService.Orders.create({
|
||||
external_id: webflowOrder.orderId,
|
||||
// TODO: derive from webflow
|
||||
shipping: "STANDARD",
|
||||
recipient: {
|
||||
name: webflowOrder.shippingAddress.addressee,
|
||||
address1: webflowOrder.shippingAddress.line1,
|
||||
address2: webflowOrder.shippingAddress.line2,
|
||||
city: webflowOrder.shippingAddress.city,
|
||||
state_code: zipcodesUs.find(
|
||||
webflowOrder.shippingAddress.postalCode.split("-")[0] ?? "",
|
||||
).stateCode,
|
||||
country_code: webflowOrder.shippingAddress.country,
|
||||
zip: webflowOrder.shippingAddress.postalCode,
|
||||
},
|
||||
items: webflowOrder.purchasedItems.map((webflowOrderSku) => ({
|
||||
external_variant_id: webflowOrderSku.variantId,
|
||||
quantity: webflowOrderSku.count,
|
||||
})),
|
||||
});
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return { ok: true };
|
||||
});
|
||||
|
||||
app.listen(3000);
|
||||
@@ -0,0 +1,87 @@
|
||||
import {
|
||||
type Levels,
|
||||
DEFAULT_STANDARDS_DATA,
|
||||
} from "@blade-and-brawn/calculator";
|
||||
import { Activity, clamp, Gender, range } from "@blade-and-brawn/calculator";
|
||||
import { levenbergMarquardt as LM } from "ml-levenberg-marquardt";
|
||||
|
||||
for (const activity of Object.values(Activity)) {
|
||||
const DATASET_MAX_LEVEL = 5;
|
||||
const NEW_LEVEL_COUNT = 2;
|
||||
const RATIO_CLAMP = 1.4;
|
||||
|
||||
function expDecayModel([A, B, C]: number[]) {
|
||||
return (i: number) => A! * Math.exp(-B! * i) + C!;
|
||||
}
|
||||
|
||||
for (const gender of Object.values(Gender)) {
|
||||
const standardsByGender = DEFAULT_STANDARDS_DATA[activity].standards.filter(
|
||||
(s) => s["metrics"].gender === gender,
|
||||
);
|
||||
const ages = [...new Set(standardsByGender.map((s) => s.metrics.age))];
|
||||
for (const age of ages) {
|
||||
const standards = standardsByGender.filter(
|
||||
(s) => s.metrics.age === age,
|
||||
);
|
||||
|
||||
const data = {
|
||||
x: [] as number[],
|
||||
y: [] as number[],
|
||||
};
|
||||
|
||||
const isIncreasing =
|
||||
standards[0]!.levels["2"]! > standards[0]!.levels["1"]!;
|
||||
|
||||
for (const i of range(DATASET_MAX_LEVEL).slice(0, -1)) {
|
||||
const level = i + 1;
|
||||
const progressionRatios: number[] = [];
|
||||
for (const standard of standards) {
|
||||
const curr = standard.levels[level]!;
|
||||
const next = standard.levels[level]!;
|
||||
progressionRatios.push(
|
||||
isIncreasing ? next / curr : curr / next,
|
||||
);
|
||||
}
|
||||
|
||||
const sum = progressionRatios.reduce((p, c) => p + c, 0);
|
||||
const progressionRatioAvg = sum / progressionRatios.length;
|
||||
|
||||
data.x.push(i);
|
||||
data.y.push(progressionRatioAvg);
|
||||
}
|
||||
|
||||
const fittedParams = LM(data, expDecayModel, {
|
||||
initialValues: [0.4, 0.5, 1.1],
|
||||
minValues: [0.0, 0.0, 1.02],
|
||||
maxValues: [1.0, 2.0, 1.25],
|
||||
maxIterations: 200,
|
||||
});
|
||||
const getDecayRatio = (i: number) =>
|
||||
clamp(
|
||||
expDecayModel(fittedParams.parameterValues)(i),
|
||||
1,
|
||||
RATIO_CLAMP,
|
||||
);
|
||||
|
||||
// UPDATE THE DATA
|
||||
for (const standard of standards) {
|
||||
const newLevels: Levels = {};
|
||||
|
||||
let prev = standard.levels["1"]!;
|
||||
for (const i of range(NEW_LEVEL_COUNT)) {
|
||||
const level = i + 1;
|
||||
const ratio = getDecayRatio(level - NEW_LEVEL_COUNT);
|
||||
prev = isIncreasing ? prev / ratio : prev * ratio;
|
||||
newLevels[level] = Math.round(prev);
|
||||
}
|
||||
|
||||
const oldLevels = standard.levels as Levels;
|
||||
for (const i of range(DATASET_MAX_LEVEL)) {
|
||||
const level = i + 1;
|
||||
newLevels[level + NEW_LEVEL_COUNT] = oldLevels[level]!;
|
||||
}
|
||||
standard.levels = newLevels;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
import { PrintfulService, SyncService, WebflowService } from "@blade-and-brawn/commerce";
|
||||
|
||||
const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
|
||||
|
||||
type Validation = {
|
||||
isValid: boolean;
|
||||
message: string;
|
||||
};
|
||||
|
||||
const validateEquality = (wValue: any, pValue: any): Validation => {
|
||||
const validation = {
|
||||
isValid: true,
|
||||
message: "Valid",
|
||||
};
|
||||
|
||||
if (wValue !== pValue) {
|
||||
validation.isValid = false;
|
||||
validation.message = `${wValue} !== ${pValue}`;
|
||||
}
|
||||
|
||||
return validation;
|
||||
};
|
||||
|
||||
const wProducts = await WebflowService.Products.getAll();
|
||||
|
||||
let invalidCount = 0;
|
||||
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}`,
|
||||
);
|
||||
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),
|
||||
),
|
||||
];
|
||||
|
||||
// if (!validateEquality(sku.fieldData.name, pVariant.name).isValid) {
|
||||
// sku.fieldData.name = pVariant.name;
|
||||
// await WebflowService.Products.Skus.update(wProduct.product.id, sku.id, sku);
|
||||
// }
|
||||
|
||||
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("VALID");
|
||||
}
|
||||
} else {
|
||||
++invalidCount;
|
||||
console.log("INVALID");
|
||||
console.log("Not found");
|
||||
}
|
||||
console.log();
|
||||
}
|
||||
await sleep(3000);
|
||||
}
|
||||
|
||||
console.log("INVALID COUNT: " + invalidCount);
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
.env
|
||||
.svelte-kit/
|
||||
build/
|
||||
@@ -0,0 +1,339 @@
|
||||
{
|
||||
"lockfileVersion": 1,
|
||||
"workspaces": {
|
||||
"": {
|
||||
"dependencies": {
|
||||
"@tailwindcss/vite": "^4.2.1",
|
||||
"daisyui": "^5.5.19",
|
||||
"memoirist": "^0.4.0",
|
||||
"ml-levenberg-marquardt": "^5.0.0",
|
||||
"tailwindcss": "^4.2.1",
|
||||
},
|
||||
"devDependencies": {
|
||||
"@sveltejs/kit": "^2.53.4",
|
||||
"@sveltejs/vite-plugin-svelte": "^6.2.4",
|
||||
"@types/bun": "^1.3.9",
|
||||
"svelte": "^5.53.6",
|
||||
"svelte-adapter-bun": "^0.5.2",
|
||||
"svelte-check": "^4.4.4",
|
||||
"typescript": "^5.9.3",
|
||||
"vite": "^7.3.1",
|
||||
},
|
||||
},
|
||||
},
|
||||
"packages": {
|
||||
"@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.27.3", "", { "os": "aix", "cpu": "ppc64" }, "sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg=="],
|
||||
|
||||
"@esbuild/android-arm": ["@esbuild/android-arm@0.27.3", "", { "os": "android", "cpu": "arm" }, "sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA=="],
|
||||
|
||||
"@esbuild/android-arm64": ["@esbuild/android-arm64@0.27.3", "", { "os": "android", "cpu": "arm64" }, "sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg=="],
|
||||
|
||||
"@esbuild/android-x64": ["@esbuild/android-x64@0.27.3", "", { "os": "android", "cpu": "x64" }, "sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ=="],
|
||||
|
||||
"@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.27.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg=="],
|
||||
|
||||
"@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.27.3", "", { "os": "darwin", "cpu": "x64" }, "sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg=="],
|
||||
|
||||
"@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.27.3", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w=="],
|
||||
|
||||
"@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.27.3", "", { "os": "freebsd", "cpu": "x64" }, "sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA=="],
|
||||
|
||||
"@esbuild/linux-arm": ["@esbuild/linux-arm@0.27.3", "", { "os": "linux", "cpu": "arm" }, "sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw=="],
|
||||
|
||||
"@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.27.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg=="],
|
||||
|
||||
"@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.27.3", "", { "os": "linux", "cpu": "ia32" }, "sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg=="],
|
||||
|
||||
"@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.27.3", "", { "os": "linux", "cpu": "none" }, "sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA=="],
|
||||
|
||||
"@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.27.3", "", { "os": "linux", "cpu": "none" }, "sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw=="],
|
||||
|
||||
"@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.27.3", "", { "os": "linux", "cpu": "ppc64" }, "sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA=="],
|
||||
|
||||
"@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.27.3", "", { "os": "linux", "cpu": "none" }, "sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ=="],
|
||||
|
||||
"@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.27.3", "", { "os": "linux", "cpu": "s390x" }, "sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw=="],
|
||||
|
||||
"@esbuild/linux-x64": ["@esbuild/linux-x64@0.27.3", "", { "os": "linux", "cpu": "x64" }, "sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA=="],
|
||||
|
||||
"@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.27.3", "", { "os": "none", "cpu": "arm64" }, "sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA=="],
|
||||
|
||||
"@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.27.3", "", { "os": "none", "cpu": "x64" }, "sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA=="],
|
||||
|
||||
"@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.27.3", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw=="],
|
||||
|
||||
"@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.27.3", "", { "os": "openbsd", "cpu": "x64" }, "sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ=="],
|
||||
|
||||
"@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.27.3", "", { "os": "none", "cpu": "arm64" }, "sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g=="],
|
||||
|
||||
"@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.27.3", "", { "os": "sunos", "cpu": "x64" }, "sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA=="],
|
||||
|
||||
"@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.27.3", "", { "os": "win32", "cpu": "arm64" }, "sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA=="],
|
||||
|
||||
"@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.27.3", "", { "os": "win32", "cpu": "ia32" }, "sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q=="],
|
||||
|
||||
"@esbuild/win32-x64": ["@esbuild/win32-x64@0.27.3", "", { "os": "win32", "cpu": "x64" }, "sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA=="],
|
||||
|
||||
"@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="],
|
||||
|
||||
"@jridgewell/remapping": ["@jridgewell/remapping@2.3.5", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ=="],
|
||||
|
||||
"@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="],
|
||||
|
||||
"@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="],
|
||||
|
||||
"@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="],
|
||||
|
||||
"@polka/url": ["@polka/url@1.0.0-next.29", "", {}, "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww=="],
|
||||
|
||||
"@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.52.4", "", { "os": "android", "cpu": "arm" }, "sha512-BTm2qKNnWIQ5auf4deoetINJm2JzvihvGb9R6K/ETwKLql/Bb3Eg2H1FBp1gUb4YGbydMA3jcmQTR73q7J+GAA=="],
|
||||
|
||||
"@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.52.4", "", { "os": "android", "cpu": "arm64" }, "sha512-P9LDQiC5vpgGFgz7GSM6dKPCiqR3XYN1WwJKA4/BUVDjHpYsf3iBEmVz62uyq20NGYbiGPR5cNHI7T1HqxNs2w=="],
|
||||
|
||||
"@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.52.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-QRWSW+bVccAvZF6cbNZBJwAehmvG9NwfWHwMy4GbWi/BQIA/laTIktebT2ipVjNncqE6GLPxOok5hsECgAxGZg=="],
|
||||
|
||||
"@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.52.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-hZgP05pResAkRJxL1b+7yxCnXPGsXU0fG9Yfd6dUaoGk+FhdPKCJ5L1Sumyxn8kvw8Qi5PvQ8ulenUbRjzeCTw=="],
|
||||
|
||||
"@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.52.4", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-xmc30VshuBNUd58Xk4TKAEcRZHaXlV+tCxIXELiE9sQuK3kG8ZFgSPi57UBJt8/ogfhAF5Oz4ZSUBN77weM+mQ=="],
|
||||
|
||||
"@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.52.4", "", { "os": "freebsd", "cpu": "x64" }, "sha512-WdSLpZFjOEqNZGmHflxyifolwAiZmDQzuOzIq9L27ButpCVpD7KzTRtEG1I0wMPFyiyUdOO+4t8GvrnBLQSwpw=="],
|
||||
|
||||
"@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.52.4", "", { "os": "linux", "cpu": "arm" }, "sha512-xRiOu9Of1FZ4SxVbB0iEDXc4ddIcjCv2aj03dmW8UrZIW7aIQ9jVJdLBIhxBI+MaTnGAKyvMwPwQnoOEvP7FgQ=="],
|
||||
|
||||
"@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.52.4", "", { "os": "linux", "cpu": "arm" }, "sha512-FbhM2p9TJAmEIEhIgzR4soUcsW49e9veAQCziwbR+XWB2zqJ12b4i/+hel9yLiD8pLncDH4fKIPIbt5238341Q=="],
|
||||
|
||||
"@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.52.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-4n4gVwhPHR9q/g8lKCyz0yuaD0MvDf7dV4f9tHt0C73Mp8h38UCtSCSE6R9iBlTbXlmA8CjpsZoujhszefqueg=="],
|
||||
|
||||
"@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.52.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-u0n17nGA0nvi/11gcZKsjkLj1QIpAuPFQbR48Subo7SmZJnGxDpspyw2kbpuoQnyK+9pwf3pAoEXerJs/8Mi9g=="],
|
||||
|
||||
"@rollup/rollup-linux-loong64-gnu": ["@rollup/rollup-linux-loong64-gnu@4.52.4", "", { "os": "linux", "cpu": "none" }, "sha512-0G2c2lpYtbTuXo8KEJkDkClE/+/2AFPdPAbmaHoE870foRFs4pBrDehilMcrSScrN/fB/1HTaWO4bqw+ewBzMQ=="],
|
||||
|
||||
"@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.52.4", "", { "os": "linux", "cpu": "ppc64" }, "sha512-teSACug1GyZHmPDv14VNbvZFX779UqWTsd7KtTM9JIZRDI5NUwYSIS30kzI8m06gOPB//jtpqlhmraQ68b5X2g=="],
|
||||
|
||||
"@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.52.4", "", { "os": "linux", "cpu": "none" }, "sha512-/MOEW3aHjjs1p4Pw1Xk4+3egRevx8Ji9N6HUIA1Ifh8Q+cg9dremvFCUbOX2Zebz80BwJIgCBUemjqhU5XI5Eg=="],
|
||||
|
||||
"@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.52.4", "", { "os": "linux", "cpu": "none" }, "sha512-1HHmsRyh845QDpEWzOFtMCph5Ts+9+yllCrREuBR/vg2RogAQGGBRC8lDPrPOMnrdOJ+mt1WLMOC2Kao/UwcvA=="],
|
||||
|
||||
"@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.52.4", "", { "os": "linux", "cpu": "s390x" }, "sha512-seoeZp4L/6D1MUyjWkOMRU6/iLmCU2EjbMTyAG4oIOs1/I82Y5lTeaxW0KBfkUdHAWN7j25bpkt0rjnOgAcQcA=="],
|
||||
|
||||
"@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.52.4", "", { "os": "linux", "cpu": "x64" }, "sha512-Wi6AXf0k0L7E2gteNsNHUs7UMwCIhsCTs6+tqQ5GPwVRWMaflqGec4Sd8n6+FNFDw9vGcReqk2KzBDhCa1DLYg=="],
|
||||
|
||||
"@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.52.4", "", { "os": "linux", "cpu": "x64" }, "sha512-dtBZYjDmCQ9hW+WgEkaffvRRCKm767wWhxsFW3Lw86VXz/uJRuD438/XvbZT//B96Vs8oTA8Q4A0AfHbrxP9zw=="],
|
||||
|
||||
"@rollup/rollup-openharmony-arm64": ["@rollup/rollup-openharmony-arm64@4.52.4", "", { "os": "none", "cpu": "arm64" }, "sha512-1ox+GqgRWqaB1RnyZXL8PD6E5f7YyRUJYnCqKpNzxzP0TkaUh112NDrR9Tt+C8rJ4x5G9Mk8PQR3o7Ku2RKqKA=="],
|
||||
|
||||
"@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.52.4", "", { "os": "win32", "cpu": "arm64" }, "sha512-8GKr640PdFNXwzIE0IrkMWUNUomILLkfeHjXBi/nUvFlpZP+FA8BKGKpacjW6OUUHaNI6sUURxR2U2g78FOHWQ=="],
|
||||
|
||||
"@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.52.4", "", { "os": "win32", "cpu": "ia32" }, "sha512-AIy/jdJ7WtJ/F6EcfOb2GjR9UweO0n43jNObQMb6oGxkYTfLcnN7vYYpG+CN3lLxrQkzWnMOoNSHTW54pgbVxw=="],
|
||||
|
||||
"@rollup/rollup-win32-x64-gnu": ["@rollup/rollup-win32-x64-gnu@4.52.4", "", { "os": "win32", "cpu": "x64" }, "sha512-UF9KfsH9yEam0UjTwAgdK0anlQ7c8/pWPU2yVjyWcF1I1thABt6WXE47cI71pGiZ8wGvxohBoLnxM04L/wj8mQ=="],
|
||||
|
||||
"@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.52.4", "", { "os": "win32", "cpu": "x64" }, "sha512-bf9PtUa0u8IXDVxzRToFQKsNCRz9qLYfR/MpECxl4mRoWYjAeFjgxj1XdZr2M/GNVpT05p+LgQOHopYDlUu6/w=="],
|
||||
|
||||
"@standard-schema/spec": ["@standard-schema/spec@1.0.0", "", {}, "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA=="],
|
||||
|
||||
"@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.53.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.6.3", "esm-env": "^1.2.2", "kleur": "^4.1.5", "magic-string": "^0.30.5", "mrmime": "^2.0.0", "set-cookie-parser": "^3.0.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 || ^7.0.0", "svelte": "^4.0.0 || ^5.0.0-next.0", "typescript": "^5.3.3", "vite": "^5.0.3 || ^6.0.0 || ^7.0.0-beta.0 || ^8.0.0" }, "optionalPeers": ["@opentelemetry/api", "typescript"], "bin": { "svelte-kit": "svelte-kit.js" } }, "sha512-iAIPEahFgDJJyvz8g0jP08KvqnM6JvdW8YfsygZ+pMeMvyM2zssWMltcsotETvjSZ82G3VlitgDtBIvpQSZrTA=="],
|
||||
|
||||
"@sveltejs/vite-plugin-svelte": ["@sveltejs/vite-plugin-svelte@6.2.4", "", { "dependencies": { "@sveltejs/vite-plugin-svelte-inspector": "^5.0.0", "deepmerge": "^4.3.1", "magic-string": "^0.30.21", "obug": "^2.1.0", "vitefu": "^1.1.1" }, "peerDependencies": { "svelte": "^5.0.0", "vite": "^6.3.0 || ^7.0.0" } }, "sha512-ou/d51QSdTyN26D7h6dSpusAKaZkAiGM55/AKYi+9AGZw7q85hElbjK3kEyzXHhLSnRISHOYzVge6x0jRZ7DXA=="],
|
||||
|
||||
"@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.2.1", "", { "dependencies": { "@jridgewell/remapping": "^2.3.5", "enhanced-resolve": "^5.19.0", "jiti": "^2.6.1", "lightningcss": "1.31.1", "magic-string": "^0.30.21", "source-map-js": "^1.2.1", "tailwindcss": "4.2.1" } }, "sha512-jlx6sLk4EOwO6hHe1oCGm1Q4AN/s0rSrTTPBGPM0/RQ6Uylwq17FuU8IeJJKEjtc6K6O07zsvP+gDO6MMWo7pg=="],
|
||||
|
||||
"@tailwindcss/oxide": ["@tailwindcss/oxide@4.2.1", "", { "optionalDependencies": { "@tailwindcss/oxide-android-arm64": "4.2.1", "@tailwindcss/oxide-darwin-arm64": "4.2.1", "@tailwindcss/oxide-darwin-x64": "4.2.1", "@tailwindcss/oxide-freebsd-x64": "4.2.1", "@tailwindcss/oxide-linux-arm-gnueabihf": "4.2.1", "@tailwindcss/oxide-linux-arm64-gnu": "4.2.1", "@tailwindcss/oxide-linux-arm64-musl": "4.2.1", "@tailwindcss/oxide-linux-x64-gnu": "4.2.1", "@tailwindcss/oxide-linux-x64-musl": "4.2.1", "@tailwindcss/oxide-wasm32-wasi": "4.2.1", "@tailwindcss/oxide-win32-arm64-msvc": "4.2.1", "@tailwindcss/oxide-win32-x64-msvc": "4.2.1" } }, "sha512-yv9jeEFWnjKCI6/T3Oq50yQEOqmpmpfzG1hcZsAOaXFQPfzWprWrlHSdGPEF3WQTi8zu8ohC9Mh9J470nT5pUw=="],
|
||||
|
||||
"@tailwindcss/oxide-android-arm64": ["@tailwindcss/oxide-android-arm64@4.2.1", "", { "os": "android", "cpu": "arm64" }, "sha512-eZ7G1Zm5EC8OOKaesIKuw77jw++QJ2lL9N+dDpdQiAB/c/B2wDh0QPFHbkBVrXnwNugvrbJFk1gK2SsVjwWReg=="],
|
||||
|
||||
"@tailwindcss/oxide-darwin-arm64": ["@tailwindcss/oxide-darwin-arm64@4.2.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-q/LHkOstoJ7pI1J0q6djesLzRvQSIfEto148ppAd+BVQK0JYjQIFSK3JgYZJa+Yzi0DDa52ZsQx2rqytBnf8Hw=="],
|
||||
|
||||
"@tailwindcss/oxide-darwin-x64": ["@tailwindcss/oxide-darwin-x64@4.2.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-/f/ozlaXGY6QLbpvd/kFTro2l18f7dHKpB+ieXz+Cijl4Mt9AI2rTrpq7V+t04nK+j9XBQHnSMdeQRhbGyt6fw=="],
|
||||
|
||||
"@tailwindcss/oxide-freebsd-x64": ["@tailwindcss/oxide-freebsd-x64@4.2.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-5e/AkgYJT/cpbkys/OU2Ei2jdETCLlifwm7ogMC7/hksI2fC3iiq6OcXwjibcIjPung0kRtR3TxEITkqgn0TcA=="],
|
||||
|
||||
"@tailwindcss/oxide-linux-arm-gnueabihf": ["@tailwindcss/oxide-linux-arm-gnueabihf@4.2.1", "", { "os": "linux", "cpu": "arm" }, "sha512-Uny1EcVTTmerCKt/1ZuKTkb0x8ZaiuYucg2/kImO5A5Y/kBz41/+j0gxUZl+hTF3xkWpDmHX+TaWhOtba2Fyuw=="],
|
||||
|
||||
"@tailwindcss/oxide-linux-arm64-gnu": ["@tailwindcss/oxide-linux-arm64-gnu@4.2.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-CTrwomI+c7n6aSSQlsPL0roRiNMDQ/YzMD9EjcR+H4f0I1SQ8QqIuPnsVp7QgMkC1Qi8rtkekLkOFjo7OlEFRQ=="],
|
||||
|
||||
"@tailwindcss/oxide-linux-arm64-musl": ["@tailwindcss/oxide-linux-arm64-musl@4.2.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-WZA0CHRL/SP1TRbA5mp9htsppSEkWuQ4KsSUumYQnyl8ZdT39ntwqmz4IUHGN6p4XdSlYfJwM4rRzZLShHsGAQ=="],
|
||||
|
||||
"@tailwindcss/oxide-linux-x64-gnu": ["@tailwindcss/oxide-linux-x64-gnu@4.2.1", "", { "os": "linux", "cpu": "x64" }, "sha512-qMFzxI2YlBOLW5PhblzuSWlWfwLHaneBE0xHzLrBgNtqN6mWfs+qYbhryGSXQjFYB1Dzf5w+LN5qbUTPhW7Y5g=="],
|
||||
|
||||
"@tailwindcss/oxide-linux-x64-musl": ["@tailwindcss/oxide-linux-x64-musl@4.2.1", "", { "os": "linux", "cpu": "x64" }, "sha512-5r1X2FKnCMUPlXTWRYpHdPYUY6a1Ar/t7P24OuiEdEOmms5lyqjDRvVY1yy9Rmioh+AunQ0rWiOTPE8F9A3v5g=="],
|
||||
|
||||
"@tailwindcss/oxide-wasm32-wasi": ["@tailwindcss/oxide-wasm32-wasi@4.2.1", "", { "dependencies": { "@emnapi/core": "^1.8.1", "@emnapi/runtime": "^1.8.1", "@emnapi/wasi-threads": "^1.1.0", "@napi-rs/wasm-runtime": "^1.1.1", "@tybys/wasm-util": "^0.10.1", "tslib": "^2.8.1" }, "cpu": "none" }, "sha512-MGFB5cVPvshR85MTJkEvqDUnuNoysrsRxd6vnk1Lf2tbiqNlXpHYZqkqOQalydienEWOHHFyyuTSYRsLfxFJ2Q=="],
|
||||
|
||||
"@tailwindcss/oxide-win32-arm64-msvc": ["@tailwindcss/oxide-win32-arm64-msvc@4.2.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-YlUEHRHBGnCMh4Nj4GnqQyBtsshUPdiNroZj8VPkvTZSoHsilRCwXcVKnG9kyi0ZFAS/3u+qKHBdDc81SADTRA=="],
|
||||
|
||||
"@tailwindcss/oxide-win32-x64-msvc": ["@tailwindcss/oxide-win32-x64-msvc@4.2.1", "", { "os": "win32", "cpu": "x64" }, "sha512-rbO34G5sMWWyrN/idLeVxAZgAKWrn5LiR3/I90Q9MkA67s6T1oB0xtTe+0heoBvHSpbU9Mk7i6uwJnpo4u21XQ=="],
|
||||
|
||||
"@tailwindcss/vite": ["@tailwindcss/vite@4.2.1", "", { "dependencies": { "@tailwindcss/node": "4.2.1", "@tailwindcss/oxide": "4.2.1", "tailwindcss": "4.2.1" }, "peerDependencies": { "vite": "^5.2.0 || ^6 || ^7" } }, "sha512-TBf2sJjYeb28jD2U/OhwdW0bbOsxkWPwQ7SrqGf9sVcoYwZj7rkXljroBO9wKBut9XnmQLXanuDUeqQK0lGg/w=="],
|
||||
|
||||
"@types/bun": ["@types/bun@1.3.9", "", { "dependencies": { "bun-types": "1.3.9" } }, "sha512-KQ571yULOdWJiMH+RIWIOZ7B2RXQGpL1YQrBtLIV3FqDcCu6FsbFUBwhdKUlCKUpS3PJDsHlJ1QKlpxoVR+xtw=="],
|
||||
|
||||
"@types/cookie": ["@types/cookie@0.6.0", "", {}, "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA=="],
|
||||
|
||||
"@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="],
|
||||
|
||||
"@types/node": ["@types/node@24.7.2", "", { "dependencies": { "undici-types": "~7.14.0" } }, "sha512-/NbVmcGTP+lj5oa4yiYxxeBjRivKQ5Ns1eSZeB99ExsEQ6rX5XYU1Zy/gGxY/ilqtD4Etx9mKyrPxZRetiahhA=="],
|
||||
|
||||
"@types/trusted-types": ["@types/trusted-types@2.0.7", "", {}, "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw=="],
|
||||
|
||||
"acorn": ["acorn@8.15.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg=="],
|
||||
|
||||
"aria-query": ["aria-query@5.3.1", "", {}, "sha512-Z/ZeOgVl7bcSYZ/u/rh0fOpvEpq//LZmdbkXyc7syVzjPAhfOa9ebsdTSjEBDU4vs5nC98Kfduj1uFo0qyET3g=="],
|
||||
|
||||
"axobject-query": ["axobject-query@4.1.0", "", {}, "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ=="],
|
||||
|
||||
"bun-types": ["bun-types@1.3.9", "", { "dependencies": { "@types/node": "*" } }, "sha512-+UBWWOakIP4Tswh0Bt0QD0alpTY8cb5hvgiYeWCMet9YukHbzuruIEeXC2D7nMJPB12kbh8C7XJykSexEqGKJg=="],
|
||||
|
||||
"chokidar": ["chokidar@4.0.3", "", { "dependencies": { "readdirp": "^4.0.1" } }, "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA=="],
|
||||
|
||||
"clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="],
|
||||
|
||||
"cookie": ["cookie@0.6.0", "", {}, "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw=="],
|
||||
|
||||
"daisyui": ["daisyui@5.5.19", "", {}, "sha512-pbFAkl1VCEh/MPCeclKL61I/MqRIFFhNU7yiXoDDRapXN4/qNCoMxeCCswyxEEhqL5eiTTfwHvucFtOE71C9sA=="],
|
||||
|
||||
"debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
|
||||
|
||||
"deepmerge": ["deepmerge@4.3.1", "", {}, "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A=="],
|
||||
|
||||
"detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="],
|
||||
|
||||
"devalue": ["devalue@5.6.3", "", {}, "sha512-nc7XjUU/2Lb+SvEFVGcWLiKkzfw8+qHI7zn8WYXKkLMgfGSHbgCEaR6bJpev8Cm6Rmrb19Gfd/tZvGqx9is3wg=="],
|
||||
|
||||
"enhanced-resolve": ["enhanced-resolve@5.20.0", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.3.0" } }, "sha512-/ce7+jQ1PQ6rVXwe+jKEg5hW5ciicHwIQUagZkp6IufBoY3YDgdTTY1azVs0qoRgVmvsNB+rbjLJxDAeHHtwsQ=="],
|
||||
|
||||
"esbuild": ["esbuild@0.27.3", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.3", "@esbuild/android-arm": "0.27.3", "@esbuild/android-arm64": "0.27.3", "@esbuild/android-x64": "0.27.3", "@esbuild/darwin-arm64": "0.27.3", "@esbuild/darwin-x64": "0.27.3", "@esbuild/freebsd-arm64": "0.27.3", "@esbuild/freebsd-x64": "0.27.3", "@esbuild/linux-arm": "0.27.3", "@esbuild/linux-arm64": "0.27.3", "@esbuild/linux-ia32": "0.27.3", "@esbuild/linux-loong64": "0.27.3", "@esbuild/linux-mips64el": "0.27.3", "@esbuild/linux-ppc64": "0.27.3", "@esbuild/linux-riscv64": "0.27.3", "@esbuild/linux-s390x": "0.27.3", "@esbuild/linux-x64": "0.27.3", "@esbuild/netbsd-arm64": "0.27.3", "@esbuild/netbsd-x64": "0.27.3", "@esbuild/openbsd-arm64": "0.27.3", "@esbuild/openbsd-x64": "0.27.3", "@esbuild/openharmony-arm64": "0.27.3", "@esbuild/sunos-x64": "0.27.3", "@esbuild/win32-arm64": "0.27.3", "@esbuild/win32-ia32": "0.27.3", "@esbuild/win32-x64": "0.27.3" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg=="],
|
||||
|
||||
"esm-env": ["esm-env@1.2.2", "", {}, "sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA=="],
|
||||
|
||||
"esrap": ["esrap@2.2.3", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.4.15" } }, "sha512-8fOS+GIGCQZl/ZIlhl59htOlms6U8NvX6ZYgYHpRU/b6tVSh3uHkOHZikl3D4cMbYM0JlpBe+p/BkZEi8J9XIQ=="],
|
||||
|
||||
"fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="],
|
||||
|
||||
"fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
|
||||
|
||||
"globalyzer": ["globalyzer@0.1.0", "", {}, "sha512-40oNTM9UfG6aBmuKxk/giHn5nQ8RVz/SS4Ir6zgzOv9/qC3kKZ9v4etGTcJbEl/NyVQH7FGU7d+X1egr57Md2Q=="],
|
||||
|
||||
"globrex": ["globrex@0.1.2", "", {}, "sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg=="],
|
||||
|
||||
"graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="],
|
||||
|
||||
"is-any-array": ["is-any-array@2.0.1", "", {}, "sha512-UtilS7hLRu++wb/WBAw9bNuP1Eg04Ivn1vERJck8zJthEvXCBEBpGR/33u/xLKWEQf95803oalHrVDptcAvFdQ=="],
|
||||
|
||||
"is-reference": ["is-reference@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.6" } }, "sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw=="],
|
||||
|
||||
"jiti": ["jiti@2.6.1", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ=="],
|
||||
|
||||
"kleur": ["kleur@4.1.5", "", {}, "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ=="],
|
||||
|
||||
"lightningcss": ["lightningcss@1.31.1", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.31.1", "lightningcss-darwin-arm64": "1.31.1", "lightningcss-darwin-x64": "1.31.1", "lightningcss-freebsd-x64": "1.31.1", "lightningcss-linux-arm-gnueabihf": "1.31.1", "lightningcss-linux-arm64-gnu": "1.31.1", "lightningcss-linux-arm64-musl": "1.31.1", "lightningcss-linux-x64-gnu": "1.31.1", "lightningcss-linux-x64-musl": "1.31.1", "lightningcss-win32-arm64-msvc": "1.31.1", "lightningcss-win32-x64-msvc": "1.31.1" } }, "sha512-l51N2r93WmGUye3WuFoN5k10zyvrVs0qfKBhyC5ogUQ6Ew6JUSswh78mbSO+IU3nTWsyOArqPCcShdQSadghBQ=="],
|
||||
|
||||
"lightningcss-android-arm64": ["lightningcss-android-arm64@1.31.1", "", { "os": "android", "cpu": "arm64" }, "sha512-HXJF3x8w9nQ4jbXRiNppBCqeZPIAfUo8zE/kOEGbW5NZvGc/K7nMxbhIr+YlFlHW5mpbg/YFPdbnCh1wAXCKFg=="],
|
||||
|
||||
"lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.31.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-02uTEqf3vIfNMq3h/z2cJfcOXnQ0GRwQrkmPafhueLb2h7mqEidiCzkE4gBMEH65abHRiQvhdcQ+aP0D0g67sg=="],
|
||||
|
||||
"lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.31.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-1ObhyoCY+tGxtsz1lSx5NXCj3nirk0Y0kB/g8B8DT+sSx4G9djitg9ejFnjb3gJNWo7qXH4DIy2SUHvpoFwfTA=="],
|
||||
|
||||
"lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.31.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-1RINmQKAItO6ISxYgPwszQE1BrsVU5aB45ho6O42mu96UiZBxEXsuQ7cJW4zs4CEodPUioj/QrXW1r9pLUM74A=="],
|
||||
|
||||
"lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.31.1", "", { "os": "linux", "cpu": "arm" }, "sha512-OOCm2//MZJ87CdDK62rZIu+aw9gBv4azMJuA8/KB74wmfS3lnC4yoPHm0uXZ/dvNNHmnZnB8XLAZzObeG0nS1g=="],
|
||||
|
||||
"lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.31.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-WKyLWztD71rTnou4xAD5kQT+982wvca7E6QoLpoawZ1gP9JM0GJj4Tp5jMUh9B3AitHbRZ2/H3W5xQmdEOUlLg=="],
|
||||
|
||||
"lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.31.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-mVZ7Pg2zIbe3XlNbZJdjs86YViQFoJSpc41CbVmKBPiGmC4YrfeOyz65ms2qpAobVd7WQsbW4PdsSJEMymyIMg=="],
|
||||
|
||||
"lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.31.1", "", { "os": "linux", "cpu": "x64" }, "sha512-xGlFWRMl+0KvUhgySdIaReQdB4FNudfUTARn7q0hh/V67PVGCs3ADFjw+6++kG1RNd0zdGRlEKa+T13/tQjPMA=="],
|
||||
|
||||
"lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.31.1", "", { "os": "linux", "cpu": "x64" }, "sha512-eowF8PrKHw9LpoZii5tdZwnBcYDxRw2rRCyvAXLi34iyeYfqCQNA9rmUM0ce62NlPhCvof1+9ivRaTY6pSKDaA=="],
|
||||
|
||||
"lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.31.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-aJReEbSEQzx1uBlQizAOBSjcmr9dCdL3XuC/6HLXAxmtErsj2ICo5yYggg1qOODQMtnjNQv2UHb9NpOuFtYe4w=="],
|
||||
|
||||
"lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.31.1", "", { "os": "win32", "cpu": "x64" }, "sha512-I9aiFrbd7oYHwlnQDqr1Roz+fTz61oDDJX7n9tYF9FJymH1cIN1DtKw3iYt6b8WZgEjoNwVSncwF4wx/ZedMhw=="],
|
||||
|
||||
"locate-character": ["locate-character@3.0.0", "", {}, "sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA=="],
|
||||
|
||||
"magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="],
|
||||
|
||||
"memoirist": ["memoirist@0.4.0", "", {}, "sha512-zxTgA0mSYELa66DimuNQDvyLq36AwDlTuVRbnQtB+VuTcKWm5Qc4z3WkSpgsFWHNhexqkIooqpv4hdcqrX5Nmg=="],
|
||||
|
||||
"ml-array-max": ["ml-array-max@1.2.4", "", { "dependencies": { "is-any-array": "^2.0.0" } }, "sha512-BlEeg80jI0tW6WaPyGxf5Sa4sqvcyY6lbSn5Vcv44lp1I2GR6AWojfUvLnGTNsIXrZ8uqWmo8VcG1WpkI2ONMQ=="],
|
||||
|
||||
"ml-array-min": ["ml-array-min@1.2.3", "", { "dependencies": { "is-any-array": "^2.0.0" } }, "sha512-VcZ5f3VZ1iihtrGvgfh/q0XlMobG6GQ8FsNyQXD3T+IlstDv85g8kfV0xUG1QPRO/t21aukaJowDzMTc7j5V6Q=="],
|
||||
|
||||
"ml-array-rescale": ["ml-array-rescale@1.3.7", "", { "dependencies": { "is-any-array": "^2.0.0", "ml-array-max": "^1.2.4", "ml-array-min": "^1.2.3" } }, "sha512-48NGChTouvEo9KBctDfHC3udWnQKNKEWN0ziELvY3KG25GR5cA8K8wNVzracsqSW1QEkAXjTNx+ycgAv06/1mQ=="],
|
||||
|
||||
"ml-levenberg-marquardt": ["ml-levenberg-marquardt@5.0.0", "", { "dependencies": { "is-any-array": "^2.0.1", "ml-matrix": "^6.12.1" } }, "sha512-vCFoO2DyYKGZFp+KxofN2cAvSc/3gufJHj/oaDHA2nV+NZH509fD7m35zQR2ZXA8fepxpVrwgCAQ0aAIsV2WtA=="],
|
||||
|
||||
"ml-matrix": ["ml-matrix@6.12.1", "", { "dependencies": { "is-any-array": "^2.0.1", "ml-array-rescale": "^1.3.7" } }, "sha512-TJ+8eOFdp+INvzR4zAuwBQJznDUfktMtOB6g/hUcGh3rcyjxbz4Te57Pgri8Q9bhSQ7Zys4IYOGhFdnlgeB6Lw=="],
|
||||
|
||||
"mri": ["mri@1.2.0", "", {}, "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA=="],
|
||||
|
||||
"mrmime": ["mrmime@2.0.1", "", {}, "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ=="],
|
||||
|
||||
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
|
||||
|
||||
"nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="],
|
||||
|
||||
"obug": ["obug@2.1.1", "", {}, "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ=="],
|
||||
|
||||
"picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
|
||||
|
||||
"picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="],
|
||||
|
||||
"postcss": ["postcss@8.5.6", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg=="],
|
||||
|
||||
"readdirp": ["readdirp@4.1.2", "", {}, "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg=="],
|
||||
|
||||
"rollup": ["rollup@4.52.4", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.52.4", "@rollup/rollup-android-arm64": "4.52.4", "@rollup/rollup-darwin-arm64": "4.52.4", "@rollup/rollup-darwin-x64": "4.52.4", "@rollup/rollup-freebsd-arm64": "4.52.4", "@rollup/rollup-freebsd-x64": "4.52.4", "@rollup/rollup-linux-arm-gnueabihf": "4.52.4", "@rollup/rollup-linux-arm-musleabihf": "4.52.4", "@rollup/rollup-linux-arm64-gnu": "4.52.4", "@rollup/rollup-linux-arm64-musl": "4.52.4", "@rollup/rollup-linux-loong64-gnu": "4.52.4", "@rollup/rollup-linux-ppc64-gnu": "4.52.4", "@rollup/rollup-linux-riscv64-gnu": "4.52.4", "@rollup/rollup-linux-riscv64-musl": "4.52.4", "@rollup/rollup-linux-s390x-gnu": "4.52.4", "@rollup/rollup-linux-x64-gnu": "4.52.4", "@rollup/rollup-linux-x64-musl": "4.52.4", "@rollup/rollup-openharmony-arm64": "4.52.4", "@rollup/rollup-win32-arm64-msvc": "4.52.4", "@rollup/rollup-win32-ia32-msvc": "4.52.4", "@rollup/rollup-win32-x64-gnu": "4.52.4", "@rollup/rollup-win32-x64-msvc": "4.52.4", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-CLEVl+MnPAiKh5pl4dEWSyMTpuflgNQiLGhMv8ezD5W/qP8AKvmYpCOKRRNOh7oRKnauBZ4SyeYkMS+1VSyKwQ=="],
|
||||
|
||||
"sade": ["sade@1.8.1", "", { "dependencies": { "mri": "^1.1.0" } }, "sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A=="],
|
||||
|
||||
"set-cookie-parser": ["set-cookie-parser@3.0.1", "", {}, "sha512-n7Z7dXZhJbwuAHhNzkTti6Aw9QDDjZtm3JTpTGATIdNzdQz5GuFs22w90BcvF4INfnrL5xrX3oGsuqO5Dx3A1Q=="],
|
||||
|
||||
"sirv": ["sirv@3.0.2", "", { "dependencies": { "@polka/url": "^1.0.0-next.24", "mrmime": "^2.0.0", "totalist": "^3.0.0" } }, "sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g=="],
|
||||
|
||||
"source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
|
||||
|
||||
"svelte": ["svelte@5.53.6", "", { "dependencies": { "@jridgewell/remapping": "^2.3.4", "@jridgewell/sourcemap-codec": "^1.5.0", "@sveltejs/acorn-typescript": "^1.0.5", "@types/estree": "^1.0.5", "@types/trusted-types": "^2.0.7", "acorn": "^8.12.1", "aria-query": "5.3.1", "axobject-query": "^4.1.0", "clsx": "^2.1.1", "devalue": "^5.6.3", "esm-env": "^1.2.1", "esrap": "^2.2.2", "is-reference": "^3.0.3", "locate-character": "^3.0.0", "magic-string": "^0.30.11", "zimmerframe": "^1.1.2" } }, "sha512-lP5DGF3oDDI9fhHcSpaBiJEkFLuS16h92DhM1L5K1lFm0WjOmUh1i2sNkBBk8rkxJRpob0dBE75jRfUzGZUOGA=="],
|
||||
|
||||
"svelte-adapter-bun": ["svelte-adapter-bun@0.5.2", "", { "dependencies": { "tiny-glob": "^0.2.9" } }, "sha512-xEtFgaal6UgrCwwkSIcapO9kopoFNUYCYqyKCikdqxX9bz2TDYnrWQZ7qBnkunMxi1HOIERUCvTcebYGiarZLA=="],
|
||||
|
||||
"svelte-check": ["svelte-check@4.4.4", "", { "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-F1pGqXc710Oi/wTI4d/x7d6lgPwwfx1U6w3Q35n4xsC2e8C/yN2sM1+mWxjlMcpAfWucjlq4vPi+P4FZ8a14sQ=="],
|
||||
|
||||
"tailwindcss": ["tailwindcss@4.2.1", "", {}, "sha512-/tBrSQ36vCleJkAOsy9kbNTgaxvGbyOamC30PRePTQe/o1MFwEKHQk4Cn7BNGaPtjp+PuUrByJehM1hgxfq4sw=="],
|
||||
|
||||
"tapable": ["tapable@2.3.0", "", {}, "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg=="],
|
||||
|
||||
"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=="],
|
||||
|
||||
"totalist": ["totalist@3.0.1", "", {}, "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ=="],
|
||||
|
||||
"typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
|
||||
|
||||
"undici-types": ["undici-types@7.14.0", "", {}, "sha512-QQiYxHuyZ9gQUIrmPo3IA+hUl4KYk8uSA7cHrcKd/l3p1OTpZcM0Tbp9x7FAtXdAYhlasd60ncPpgu6ihG6TOA=="],
|
||||
|
||||
"vite": ["vite@7.3.1", "", { "dependencies": { "esbuild": "^0.27.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-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA=="],
|
||||
|
||||
"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=="],
|
||||
|
||||
"zimmerframe": ["zimmerframe@1.1.4", "", {}, "sha512-B58NGBEoc8Y9MWWCQGl/gq9xBCe4IiKM0a2x7GZdQKOW5Exr8S1W24J6OgM1njK8xCRGvAJIL/MxXHf6SkmQKQ=="],
|
||||
|
||||
"@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.8.1", "", { "dependencies": { "@emnapi/wasi-threads": "1.1.0", "tslib": "^2.4.0" }, "bundled": true }, "sha512-AvT9QFpxK0Zd8J0jopedNm+w/2fIzvtPKPjqyw9jwvBaReTTqPBk9Hixaz7KbjimP+QNz605/XnjFcDAL2pqBg=="],
|
||||
|
||||
"@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.8.1", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg=="],
|
||||
|
||||
"@tailwindcss/oxide-wasm32-wasi/@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.1.0", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ=="],
|
||||
|
||||
"@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.1", "", { "dependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1", "@tybys/wasm-util": "^0.10.1" }, "bundled": true }, "sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A=="],
|
||||
|
||||
"@tailwindcss/oxide-wasm32-wasi/@tybys/wasm-util": ["@tybys/wasm-util@0.10.1", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg=="],
|
||||
|
||||
"@tailwindcss/oxide-wasm32-wasi/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"name": "@blade-and-brawn/portal",
|
||||
"type": "module",
|
||||
"devDependencies": {
|
||||
"@sveltejs/kit": "^2.53.4",
|
||||
"@sveltejs/vite-plugin-svelte": "^6.2.4",
|
||||
"@types/bun": "^1.3.9",
|
||||
"svelte": "^5.53.6",
|
||||
"svelte-adapter-bun": "^0.5.2",
|
||||
"svelte-check": "^4.4.4",
|
||||
"typescript": "^5.9.3",
|
||||
"vite": "^7.3.1"
|
||||
},
|
||||
"scripts": {
|
||||
"dev": "vite dev",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview",
|
||||
"prepare": "svelte-kit sync || echo ''",
|
||||
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
|
||||
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch"
|
||||
},
|
||||
"dependencies": {
|
||||
"@tailwindcss/vite": "^4.2.1",
|
||||
"daisyui": "^5.5.19",
|
||||
"memoirist": "^0.4.0",
|
||||
"tailwindcss": "^4.2.1",
|
||||
"@blade-and-brawn/calculator": "workspace:*",
|
||||
"@blade-and-brawn/commerce": "workspace:*"
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.5 KiB |
@@ -0,0 +1,295 @@
|
||||
<script lang="ts">
|
||||
import {
|
||||
LevelCalculator,
|
||||
Activity,
|
||||
Attribute,
|
||||
Gender,
|
||||
lbToKg,
|
||||
range,
|
||||
type LevelCalculatorOutput,
|
||||
type Standards,
|
||||
type ActivityPerformance,
|
||||
type Player,
|
||||
} from "@blade-and-brawn/calculator";
|
||||
|
||||
interface CalcData {
|
||||
levels?: LevelCalculatorOutput;
|
||||
player: Player;
|
||||
activityPerformances: ActivityPerformance[];
|
||||
}
|
||||
|
||||
interface Props {
|
||||
allStandards: Standards;
|
||||
}
|
||||
|
||||
const { allStandards }: Props = $props();
|
||||
|
||||
const levelCalculator = $derived(new LevelCalculator(allStandards));
|
||||
|
||||
let calculations = $state([] as CalcData[]);
|
||||
|
||||
$effect(() => {
|
||||
for (const calculation of calculations) {
|
||||
calculation.levels = levelCalculator.calculate(
|
||||
calculation.player,
|
||||
calculation.activityPerformances,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
$effect(() => {
|
||||
const savedCalcs = localStorage.getItem("calculations");
|
||||
if (savedCalcs) calculations = JSON.parse(savedCalcs);
|
||||
});
|
||||
|
||||
$effect(() => {
|
||||
localStorage.setItem("calculations", JSON.stringify(calculations));
|
||||
});
|
||||
|
||||
$inspect(calculations[0]);
|
||||
|
||||
const createCalculation = function (name: string) {
|
||||
return {
|
||||
player: {
|
||||
name: name,
|
||||
metrics: {
|
||||
age: 18,
|
||||
weight: lbToKg(180),
|
||||
gender: Gender.Male,
|
||||
},
|
||||
},
|
||||
activityPerformances: Object.values(Activity).map((a) => ({
|
||||
activity: a,
|
||||
performance: 0,
|
||||
})),
|
||||
};
|
||||
};
|
||||
|
||||
function downloadObject(obj: unknown, filename = "data.json") {
|
||||
const json = JSON.stringify(obj, null, 2);
|
||||
const blob = new Blob([json], { type: "application/json" });
|
||||
const url = URL.createObjectURL(blob);
|
||||
|
||||
const a = document.createElement("a");
|
||||
a.href = url;
|
||||
a.download = filename;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
|
||||
a.remove();
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
|
||||
const formatMs = (ms: number) => {
|
||||
const total = Math.floor(ms / 1000);
|
||||
const m = Math.floor(total / 60);
|
||||
const s = total % 60;
|
||||
return `${m}m ${s}s`;
|
||||
};
|
||||
const inchesToFeetInches = (inches: number) => {
|
||||
const ft = Math.floor(inches / 12);
|
||||
const rem = inches - ft * 12;
|
||||
const whole = Math.floor(rem);
|
||||
const isHalf = Math.abs(rem - whole - 0.5) < 1e-9;
|
||||
const inchLabel = isHalf ? `${whole}½` : `${whole}`;
|
||||
return `${ft}' ${inchLabel}"`;
|
||||
};
|
||||
|
||||
const formatSeconds = (ms: number) => (ms / 1000).toFixed(2) + " seconds";
|
||||
|
||||
const performanceOptionsFromActivity = function (activity: Activity) {
|
||||
const options: { name: string; value: string }[] = [];
|
||||
switch (activity) {
|
||||
case Activity.BackSquat:
|
||||
case Activity.Deadlift:
|
||||
case Activity.BenchPress:
|
||||
for (let i = 0; i < 600; ++i) {
|
||||
options.push({
|
||||
name: String(i) + " lb",
|
||||
value: String(lbToKg(i)),
|
||||
});
|
||||
}
|
||||
break;
|
||||
|
||||
case Activity.Run: {
|
||||
const MAX_MIN = 30;
|
||||
for (let ms = 0; ms <= MAX_MIN * 60_000; ms += 1_000) {
|
||||
options.push({ name: formatMs(ms), value: String(ms) });
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case Activity.BroadJump: {
|
||||
const MAX_INCHES = 15 * 12 + 5;
|
||||
for (let halfStep = 0; halfStep <= MAX_INCHES * 2; halfStep++) {
|
||||
const inches = halfStep / 2;
|
||||
options.push({
|
||||
name: inchesToFeetInches(inches),
|
||||
value: (inches * 2.54).toFixed(1),
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case Activity.ConeDrill: {
|
||||
const MIN_MS = 5_000;
|
||||
const MAX_MS = 20_000;
|
||||
for (let ms = MIN_MS; ms <= MAX_MS; ms += 10) {
|
||||
options.push({
|
||||
name: formatSeconds(ms),
|
||||
value: String(ms),
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
return options;
|
||||
};
|
||||
</script>
|
||||
|
||||
<div class="mt-5 mb-5 flex w-full">
|
||||
<div class="ml-auto">
|
||||
<button
|
||||
onclick={() =>
|
||||
calculations.push(
|
||||
createCalculation(String(calculations.length)),
|
||||
)}
|
||||
class="btn btn-primary">New</button
|
||||
>
|
||||
<button
|
||||
onclick={() => downloadObject(calculations)}
|
||||
class="btn btn-secondary">Export</button
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<section
|
||||
class="w-full px-3 grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4"
|
||||
>
|
||||
{#each calculations as calculation, index}
|
||||
<div
|
||||
class="card card-compact bg-base-200 p-4 shadow-lg max-w-sm w-full"
|
||||
>
|
||||
<div class="card-body gap-4 p-4">
|
||||
<input
|
||||
class="input input-bordered input-sm w-full"
|
||||
type="text"
|
||||
bind:value={calculation.player.name}
|
||||
placeholder="Player name"
|
||||
/>
|
||||
|
||||
<ul
|
||||
class="bg-base-100 rounded-box shadow-xs divide-y divide-base-300"
|
||||
>
|
||||
<li class="p-3">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="font-semibold text-sm">OVERALL</div>
|
||||
<div class="badge badge-neutral badge-xs">
|
||||
{calculation?.levels?.player || "N/A"}
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
|
||||
{#each Object.values(Attribute) as attribute}
|
||||
<li class="p-3">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="opacity-80 text-sm">
|
||||
{attribute}
|
||||
</div>
|
||||
<div class="badge badge-ghost badge-xs">
|
||||
{calculation?.levels?.attributes?.[
|
||||
attribute
|
||||
] || "N/A"}
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
|
||||
<fieldset
|
||||
class="fieldset bg-base-200/60 rounded-lg"
|
||||
onchange={() =>
|
||||
(calculation.levels = levelCalculator.calculate(
|
||||
calculation.player,
|
||||
calculation.activityPerformances,
|
||||
))}
|
||||
>
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-3">
|
||||
<label class="form-control">
|
||||
<span class="label mb-1 text-xs">Gender</span>
|
||||
<select
|
||||
class="select select-bordered select-sm w-full"
|
||||
bind:value={calculation.player.metrics.gender}
|
||||
>
|
||||
{#each Object.values(Gender) as gender}
|
||||
<option value={gender}>{gender}</option>
|
||||
{/each}
|
||||
</select>
|
||||
</label>
|
||||
|
||||
<label class="form-control">
|
||||
<span class="label mb-1 text-xs">Age</span>
|
||||
<input
|
||||
class="input input-bordered input-sm w-full"
|
||||
type="number"
|
||||
min="1"
|
||||
max="100"
|
||||
step="1"
|
||||
bind:value={calculation.player.metrics.age}
|
||||
/>
|
||||
</label>
|
||||
|
||||
<label class="form-control">
|
||||
<span class="label mb-1 text-xs">Weight</span>
|
||||
<select
|
||||
class="select select-bordered select-sm w-full"
|
||||
bind:value={calculation.player.metrics.weight}
|
||||
>
|
||||
{#each range(400) as weight}
|
||||
<option value={String(lbToKg(weight))}
|
||||
>{weight}</option
|
||||
>
|
||||
{/each}
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
<fieldset
|
||||
class="fieldset bg-base-200/60 rounded-lg"
|
||||
onchange={() =>
|
||||
(calculation.levels = levelCalculator.calculate(
|
||||
calculation.player,
|
||||
calculation.activityPerformances,
|
||||
))}
|
||||
>
|
||||
<div class="grid grid-cols-2 gap-3">
|
||||
{#each calculation.activityPerformances as activityPerformance}
|
||||
<label class="form-control space-y-1">
|
||||
<span class="label mb-1 text-xs">
|
||||
{activityPerformance.activity}
|
||||
</span>
|
||||
<select
|
||||
class="select select-bordered select-sm w-full"
|
||||
bind:value={activityPerformance.performance}
|
||||
>
|
||||
{#each performanceOptionsFromActivity(activityPerformance.activity) as option}
|
||||
<option value={option.value}
|
||||
>{option.name}</option
|
||||
>
|
||||
{/each}
|
||||
</select>
|
||||
</label>
|
||||
{/each}
|
||||
</div>
|
||||
</fieldset>
|
||||
</div>
|
||||
<button
|
||||
onclick={() =>
|
||||
confirm(`Delete ${calculation.player.name}?`) &&
|
||||
calculations.splice(index, 1)}
|
||||
class="btn btn-error btn-sm w-1/4">Delete</button
|
||||
>
|
||||
</div>
|
||||
{/each}
|
||||
</section>
|
||||
+4
-7
@@ -1,17 +1,14 @@
|
||||
<script lang="ts">
|
||||
import {
|
||||
Standards,
|
||||
type Metrics,
|
||||
type Standard,
|
||||
type StandardsConfig,
|
||||
} from "$lib/services/calculator/main";
|
||||
import {
|
||||
Activity,
|
||||
cmToIn,
|
||||
kgToLb,
|
||||
msToTime,
|
||||
range,
|
||||
} from "$lib/services/calculator/util";
|
||||
type Metrics,
|
||||
type Standard,
|
||||
type StandardsConfig,
|
||||
} from "@blade-and-brawn/calculator";
|
||||
|
||||
interface Props {
|
||||
allStandards: Standards;
|
||||
@@ -0,0 +1,44 @@
|
||||
<script lang="ts">
|
||||
import { page } from "$app/state";
|
||||
import favicon from "$lib/assets/favicon.svg";
|
||||
import "./app.css";
|
||||
|
||||
const links = [
|
||||
{ name: "Calculator", path: "/calculator" },
|
||||
{ name: "Commerce", path: "/commerce" },
|
||||
];
|
||||
|
||||
let { children } = $props();
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<link rel="icon" href={favicon} />
|
||||
</svelte:head>
|
||||
|
||||
<div class="drawer lg:drawer-open">
|
||||
<input id="sidebar" type="checkbox" class="drawer-toggle" />
|
||||
<div class="drawer-content p-10 flex flex-col items-center">
|
||||
{@render children?.()}
|
||||
<!-- <label for="sidebar" class="btn btn-primary drawer-button lg:hidden"> -->
|
||||
<!-- Open drawer -->
|
||||
<!-- </label> -->
|
||||
</div>
|
||||
<div class="drawer-side">
|
||||
<label for="sidebar" aria-label="close sidebar" class="drawer-overlay"
|
||||
></label>
|
||||
<ul
|
||||
class="menu menu-lg bg-base-200 text-base-content min-h-full w-72 p-4"
|
||||
>
|
||||
{#each links as link}
|
||||
<li>
|
||||
<a
|
||||
class={{
|
||||
"menu-active": page.url.pathname === link.path,
|
||||
}}
|
||||
href={link.path}>{link.name}</a
|
||||
>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,20 +1,19 @@
|
||||
<script lang="ts">
|
||||
import {
|
||||
Activity,
|
||||
DEFAULT_STANDARDS_DATA,
|
||||
Gender,
|
||||
lbToKg,
|
||||
Standards,
|
||||
type ActivityStandards,
|
||||
type Metrics,
|
||||
type StandardsConfig,
|
||||
} from "$lib/services/calculator/main";
|
||||
import allStandardsRaw from "$lib/data/standards.json" assert { type: "json" };
|
||||
import { Activity, Gender, lbToKg } from "$lib/services/calculator/util";
|
||||
} from "@blade-and-brawn/calculator";
|
||||
import StandardsTable from "$lib/components/StandardsTable.svelte";
|
||||
import PlayersTable from "$lib/components/PlayersTable.svelte";
|
||||
|
||||
let activeTab = $state("standards");
|
||||
|
||||
const defaultStandards = new Standards(
|
||||
allStandardsRaw as ActivityStandards,
|
||||
);
|
||||
const defaultStandards = new Standards(DEFAULT_STANDARDS_DATA);
|
||||
|
||||
let input = $state({
|
||||
activity: Activity.BenchPress,
|
||||
@@ -101,7 +100,7 @@
|
||||
});
|
||||
|
||||
const allStandards = $derived(
|
||||
new Standards(allStandardsRaw as ActivityStandards, selected.cfg),
|
||||
new Standards(DEFAULT_STANDARDS_DATA, selected.cfg),
|
||||
);
|
||||
|
||||
$effect(() => {
|
||||
@@ -0,0 +1,11 @@
|
||||
import { PrintfulService, WebflowService } from "@blade-and-brawn/commerce";
|
||||
import type { PageServerLoad } from "./$types";
|
||||
|
||||
export const load = async ({ }: Parameters<PageServerLoad>[0]) => {
|
||||
return {
|
||||
products: {
|
||||
printful: PrintfulService.Products.getAll(),
|
||||
webflow: WebflowService.Products.getAll(),
|
||||
},
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,131 @@
|
||||
<script lang="ts">
|
||||
import type { Printful } from "@blade-and-brawn/commerce";
|
||||
import { onMount } from "svelte";
|
||||
|
||||
const { data } = $props();
|
||||
|
||||
const apiUrl = "http://localhost:3000";
|
||||
|
||||
const synchronizer = $state({
|
||||
isSyncing: false,
|
||||
syncingIds: [] as number[],
|
||||
poll: async function () {
|
||||
const res = (await (
|
||||
await fetch(`${apiUrl}/products/sync`)
|
||||
).json()) as any;
|
||||
if (res.data) {
|
||||
synchronizer.isSyncing = res.data.isSyncing;
|
||||
synchronizer.syncingIds = res.data.syncingIds;
|
||||
}
|
||||
},
|
||||
startPolling: () => {
|
||||
const intervalId = setInterval(async () => {
|
||||
await synchronizer.poll();
|
||||
if (!synchronizer.isSyncing) clearInterval(intervalId);
|
||||
}, 100);
|
||||
},
|
||||
syncAll: async () => {
|
||||
synchronizer.startPolling();
|
||||
await fetch(`${apiUrl}/products/sync`, { method: "POST" });
|
||||
},
|
||||
sync: async (printfulProductId: number) => {
|
||||
synchronizer.startPolling();
|
||||
await fetch(`${apiUrl}/products/sync/${printfulProductId}`, {
|
||||
method: "POST",
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
onMount(async () => {
|
||||
await synchronizer.poll();
|
||||
if (synchronizer.isSyncing) {
|
||||
synchronizer.startPolling();
|
||||
}
|
||||
});
|
||||
|
||||
const isProductSynced = async (
|
||||
printfulProduct: Printful.Products.SyncProduct,
|
||||
) => {
|
||||
const webflowProducts = await data.products.webflow;
|
||||
return webflowProducts.some(
|
||||
(p) => p.product.id === printfulProduct.external_id.split("-")[0],
|
||||
);
|
||||
};
|
||||
</script>
|
||||
|
||||
{#await data.products.printful}
|
||||
<p>Loading...</p>
|
||||
{:then printfulProducts}
|
||||
<button
|
||||
disabled={synchronizer.isSyncing}
|
||||
onclick={() => synchronizer.syncAll()}
|
||||
class="btn btn-xl mb-5"
|
||||
>
|
||||
{#if synchronizer.isSyncing}
|
||||
Syncing...
|
||||
{:else}
|
||||
Sync all
|
||||
{/if}
|
||||
</button>
|
||||
<div class="w-full overflow-x-auto">
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Status</th>
|
||||
<th>Sync</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#each printfulProducts as printfulProduct}
|
||||
<tr class="hover:bg-base-300">
|
||||
<td
|
||||
><button
|
||||
onclick={async () => {
|
||||
const productData = (await (
|
||||
await fetch(
|
||||
`${apiUrl}/products/${printfulProduct.id}`,
|
||||
)
|
||||
).json()) as any;
|
||||
console.log(productData);
|
||||
}}
|
||||
class="cursor-pointer underline"
|
||||
>
|
||||
{printfulProduct.name}
|
||||
</button></td
|
||||
>
|
||||
<td>
|
||||
{#await isProductSynced(printfulProduct) then isSynced}
|
||||
{#if synchronizer.syncingIds.includes(printfulProduct.id)}
|
||||
<div class="badge badge-warning">
|
||||
Syncing...
|
||||
</div>
|
||||
{:else if isSynced}
|
||||
<div class="badge badge-success">
|
||||
Synced
|
||||
</div>
|
||||
{:else}
|
||||
<div class="badge badge-error">
|
||||
Desynced
|
||||
</div>
|
||||
{/if}
|
||||
{/await}
|
||||
</td>
|
||||
<td>
|
||||
<button
|
||||
disabled={synchronizer.isSyncing}
|
||||
onclick={() =>
|
||||
synchronizer.sync(printfulProduct.id)}
|
||||
class="btn"
|
||||
>
|
||||
Sync
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{:catch error}
|
||||
<p>Something went wrong: {error.message}</p>
|
||||
{/await}
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"extends": "./.svelte-kit/tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"rewriteRelativeImportExtensions": true,
|
||||
"allowJs": true,
|
||||
"checkJs": true,
|
||||
"esModuleInterop": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"resolveJsonModule": true,
|
||||
"skipLibCheck": true,
|
||||
"sourceMap": true,
|
||||
"strict": true,
|
||||
"moduleResolution": "bundler",
|
||||
},
|
||||
}
|
||||
@@ -2,36 +2,71 @@
|
||||
"lockfileVersion": 1,
|
||||
"workspaces": {
|
||||
"": {
|
||||
"name": "blade-and-brawn",
|
||||
"devDependencies": {
|
||||
"@types/bun": "^1.3.9",
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": "^5",
|
||||
},
|
||||
},
|
||||
"apps/api": {
|
||||
"name": "@blade-and-brawn/api",
|
||||
"dependencies": {
|
||||
"@elysiajs/cors": "^1.4.0",
|
||||
"@elysiajs/eden": "^1.4.5",
|
||||
"@tailwindcss/vite": "^4.1.18",
|
||||
"daisyui": "^5.5.13",
|
||||
"elysia": "^1.4.18",
|
||||
"memoirist": "^0.4.0",
|
||||
"@blade-and-brawn/calculator": "workspace:*",
|
||||
"@blade-and-brawn/commerce": "workspace:*",
|
||||
"@elysiajs/cors": "^1.4.1",
|
||||
"elysia": "^1.4.27",
|
||||
"ml-levenberg-marquardt": "^5.0.0",
|
||||
"tailwindcss": "^4.1.18",
|
||||
"zipcodes-us": "^1.1.2",
|
||||
},
|
||||
"devDependencies": {
|
||||
"@sveltejs/kit": "^2.49.2",
|
||||
"@sveltejs/vite-plugin-svelte": "^6.2.1",
|
||||
"@types/bun": "^1.3.4",
|
||||
"svelte": "^5.45.10",
|
||||
"svelte-adapter-bun": "^0.5.2",
|
||||
"svelte-check": "^4.3.4",
|
||||
"typescript": "^5.9.3",
|
||||
"vite": "^7.2.7",
|
||||
},
|
||||
"apps/portal": {
|
||||
"name": "@blade-and-brawn/portal",
|
||||
"dependencies": {
|
||||
"@blade-and-brawn/calculator": "workspace:*",
|
||||
"@blade-and-brawn/commerce": "workspace:*",
|
||||
"@tailwindcss/vite": "^4.2.1",
|
||||
"daisyui": "^5.5.19",
|
||||
"memoirist": "^0.4.0",
|
||||
"tailwindcss": "^4.2.1",
|
||||
},
|
||||
"devDependencies": {
|
||||
"@sveltejs/kit": "^2.53.4",
|
||||
"@sveltejs/vite-plugin-svelte": "^6.2.4",
|
||||
"@types/bun": "^1.3.9",
|
||||
"svelte": "^5.53.6",
|
||||
"svelte-adapter-bun": "^0.5.2",
|
||||
"svelte-check": "^4.4.4",
|
||||
"typescript": "^5.9.3",
|
||||
"vite": "^7.3.1",
|
||||
},
|
||||
},
|
||||
"packages/calculator": {
|
||||
"name": "@blade-and-brawn/calculator",
|
||||
"version": "0.0.0",
|
||||
"dependencies": {
|
||||
"ml-levenberg-marquardt": "^5.0.0",
|
||||
},
|
||||
},
|
||||
"packages/commerce": {
|
||||
"name": "@blade-and-brawn/commerce",
|
||||
"version": "0.0.0",
|
||||
},
|
||||
},
|
||||
"packages": {
|
||||
"@borewit/text-codec": ["@borewit/text-codec@0.1.1", "", {}, "sha512-5L/uBxmjaCIX5h8Z+uu+kA9BQLkc/Wl06UGR5ajNRxu+/XjonB5i8JpgFMrPj3LXTCPA0pv8yxUvbUi+QthGGA=="],
|
||||
"@blade-and-brawn/api": ["@blade-and-brawn/api@workspace:apps/api"],
|
||||
|
||||
"@blade-and-brawn/calculator": ["@blade-and-brawn/calculator@workspace:packages/calculator"],
|
||||
|
||||
"@blade-and-brawn/commerce": ["@blade-and-brawn/commerce@workspace:packages/commerce"],
|
||||
|
||||
"@blade-and-brawn/portal": ["@blade-and-brawn/portal@workspace:apps/portal"],
|
||||
|
||||
"@borewit/text-codec": ["@borewit/text-codec@0.2.1", "", {}, "sha512-k7vvKPbf7J2fZ5klGRD9AeKfUvojuZIQ3BT5u7Jfv+puwXkUBUT5PVyMDfJZpy30CBDXGMgw7fguK/lpOMBvgw=="],
|
||||
|
||||
"@elysiajs/cors": ["@elysiajs/cors@1.4.1", "", { "peerDependencies": { "elysia": ">= 1.4.0" } }, "sha512-lQfad+F3r4mNwsxRKbXyJB8Jg43oAOXjRwn7sKUL6bcOW3KjUqUimTS+woNpO97efpzjtDE0tEjGk9DTw8lqTQ=="],
|
||||
|
||||
"@elysiajs/eden": ["@elysiajs/eden@1.4.8", "", { "peerDependencies": { "elysia": ">=1.4.19" } }, "sha512-a7oct2kFa49tH+GawZtSUCZR2rQgucNYgGLz8alXUqb4IrU3PASA0T4zXJw4MhdV1Xb6vyiR7p7kkqJcVjgbkA=="],
|
||||
|
||||
"@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.27.3", "", { "os": "aix", "cpu": "ppc64" }, "sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg=="],
|
||||
|
||||
"@esbuild/android-arm": ["@esbuild/android-arm@0.27.3", "", { "os": "android", "cpu": "arm" }, "sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA=="],
|
||||
@@ -96,61 +131,67 @@
|
||||
|
||||
"@polka/url": ["@polka/url@1.0.0-next.29", "", {}, "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww=="],
|
||||
|
||||
"@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.52.4", "", { "os": "android", "cpu": "arm" }, "sha512-BTm2qKNnWIQ5auf4deoetINJm2JzvihvGb9R6K/ETwKLql/Bb3Eg2H1FBp1gUb4YGbydMA3jcmQTR73q7J+GAA=="],
|
||||
"@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.59.0", "", { "os": "android", "cpu": "arm" }, "sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg=="],
|
||||
|
||||
"@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.52.4", "", { "os": "android", "cpu": "arm64" }, "sha512-P9LDQiC5vpgGFgz7GSM6dKPCiqR3XYN1WwJKA4/BUVDjHpYsf3iBEmVz62uyq20NGYbiGPR5cNHI7T1HqxNs2w=="],
|
||||
"@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.59.0", "", { "os": "android", "cpu": "arm64" }, "sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q=="],
|
||||
|
||||
"@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.52.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-QRWSW+bVccAvZF6cbNZBJwAehmvG9NwfWHwMy4GbWi/BQIA/laTIktebT2ipVjNncqE6GLPxOok5hsECgAxGZg=="],
|
||||
"@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.59.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg=="],
|
||||
|
||||
"@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.52.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-hZgP05pResAkRJxL1b+7yxCnXPGsXU0fG9Yfd6dUaoGk+FhdPKCJ5L1Sumyxn8kvw8Qi5PvQ8ulenUbRjzeCTw=="],
|
||||
"@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.59.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w=="],
|
||||
|
||||
"@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.52.4", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-xmc30VshuBNUd58Xk4TKAEcRZHaXlV+tCxIXELiE9sQuK3kG8ZFgSPi57UBJt8/ogfhAF5Oz4ZSUBN77weM+mQ=="],
|
||||
"@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.59.0", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA=="],
|
||||
|
||||
"@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.52.4", "", { "os": "freebsd", "cpu": "x64" }, "sha512-WdSLpZFjOEqNZGmHflxyifolwAiZmDQzuOzIq9L27ButpCVpD7KzTRtEG1I0wMPFyiyUdOO+4t8GvrnBLQSwpw=="],
|
||||
"@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.59.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg=="],
|
||||
|
||||
"@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.52.4", "", { "os": "linux", "cpu": "arm" }, "sha512-xRiOu9Of1FZ4SxVbB0iEDXc4ddIcjCv2aj03dmW8UrZIW7aIQ9jVJdLBIhxBI+MaTnGAKyvMwPwQnoOEvP7FgQ=="],
|
||||
"@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.59.0", "", { "os": "linux", "cpu": "arm" }, "sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw=="],
|
||||
|
||||
"@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.52.4", "", { "os": "linux", "cpu": "arm" }, "sha512-FbhM2p9TJAmEIEhIgzR4soUcsW49e9veAQCziwbR+XWB2zqJ12b4i/+hel9yLiD8pLncDH4fKIPIbt5238341Q=="],
|
||||
"@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.59.0", "", { "os": "linux", "cpu": "arm" }, "sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA=="],
|
||||
|
||||
"@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.52.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-4n4gVwhPHR9q/g8lKCyz0yuaD0MvDf7dV4f9tHt0C73Mp8h38UCtSCSE6R9iBlTbXlmA8CjpsZoujhszefqueg=="],
|
||||
"@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.59.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA=="],
|
||||
|
||||
"@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.52.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-u0n17nGA0nvi/11gcZKsjkLj1QIpAuPFQbR48Subo7SmZJnGxDpspyw2kbpuoQnyK+9pwf3pAoEXerJs/8Mi9g=="],
|
||||
"@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.59.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA=="],
|
||||
|
||||
"@rollup/rollup-linux-loong64-gnu": ["@rollup/rollup-linux-loong64-gnu@4.52.4", "", { "os": "linux", "cpu": "none" }, "sha512-0G2c2lpYtbTuXo8KEJkDkClE/+/2AFPdPAbmaHoE870foRFs4pBrDehilMcrSScrN/fB/1HTaWO4bqw+ewBzMQ=="],
|
||||
"@rollup/rollup-linux-loong64-gnu": ["@rollup/rollup-linux-loong64-gnu@4.59.0", "", { "os": "linux", "cpu": "none" }, "sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg=="],
|
||||
|
||||
"@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.52.4", "", { "os": "linux", "cpu": "ppc64" }, "sha512-teSACug1GyZHmPDv14VNbvZFX779UqWTsd7KtTM9JIZRDI5NUwYSIS30kzI8m06gOPB//jtpqlhmraQ68b5X2g=="],
|
||||
"@rollup/rollup-linux-loong64-musl": ["@rollup/rollup-linux-loong64-musl@4.59.0", "", { "os": "linux", "cpu": "none" }, "sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q=="],
|
||||
|
||||
"@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.52.4", "", { "os": "linux", "cpu": "none" }, "sha512-/MOEW3aHjjs1p4Pw1Xk4+3egRevx8Ji9N6HUIA1Ifh8Q+cg9dremvFCUbOX2Zebz80BwJIgCBUemjqhU5XI5Eg=="],
|
||||
"@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.59.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA=="],
|
||||
|
||||
"@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.52.4", "", { "os": "linux", "cpu": "none" }, "sha512-1HHmsRyh845QDpEWzOFtMCph5Ts+9+yllCrREuBR/vg2RogAQGGBRC8lDPrPOMnrdOJ+mt1WLMOC2Kao/UwcvA=="],
|
||||
"@rollup/rollup-linux-ppc64-musl": ["@rollup/rollup-linux-ppc64-musl@4.59.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA=="],
|
||||
|
||||
"@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.52.4", "", { "os": "linux", "cpu": "s390x" }, "sha512-seoeZp4L/6D1MUyjWkOMRU6/iLmCU2EjbMTyAG4oIOs1/I82Y5lTeaxW0KBfkUdHAWN7j25bpkt0rjnOgAcQcA=="],
|
||||
"@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.59.0", "", { "os": "linux", "cpu": "none" }, "sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg=="],
|
||||
|
||||
"@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.52.4", "", { "os": "linux", "cpu": "x64" }, "sha512-Wi6AXf0k0L7E2gteNsNHUs7UMwCIhsCTs6+tqQ5GPwVRWMaflqGec4Sd8n6+FNFDw9vGcReqk2KzBDhCa1DLYg=="],
|
||||
"@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.59.0", "", { "os": "linux", "cpu": "none" }, "sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg=="],
|
||||
|
||||
"@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.52.4", "", { "os": "linux", "cpu": "x64" }, "sha512-dtBZYjDmCQ9hW+WgEkaffvRRCKm767wWhxsFW3Lw86VXz/uJRuD438/XvbZT//B96Vs8oTA8Q4A0AfHbrxP9zw=="],
|
||||
"@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.59.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w=="],
|
||||
|
||||
"@rollup/rollup-openharmony-arm64": ["@rollup/rollup-openharmony-arm64@4.52.4", "", { "os": "none", "cpu": "arm64" }, "sha512-1ox+GqgRWqaB1RnyZXL8PD6E5f7YyRUJYnCqKpNzxzP0TkaUh112NDrR9Tt+C8rJ4x5G9Mk8PQR3o7Ku2RKqKA=="],
|
||||
"@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.59.0", "", { "os": "linux", "cpu": "x64" }, "sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg=="],
|
||||
|
||||
"@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.52.4", "", { "os": "win32", "cpu": "arm64" }, "sha512-8GKr640PdFNXwzIE0IrkMWUNUomILLkfeHjXBi/nUvFlpZP+FA8BKGKpacjW6OUUHaNI6sUURxR2U2g78FOHWQ=="],
|
||||
"@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.59.0", "", { "os": "linux", "cpu": "x64" }, "sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg=="],
|
||||
|
||||
"@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.52.4", "", { "os": "win32", "cpu": "ia32" }, "sha512-AIy/jdJ7WtJ/F6EcfOb2GjR9UweO0n43jNObQMb6oGxkYTfLcnN7vYYpG+CN3lLxrQkzWnMOoNSHTW54pgbVxw=="],
|
||||
"@rollup/rollup-openbsd-x64": ["@rollup/rollup-openbsd-x64@4.59.0", "", { "os": "openbsd", "cpu": "x64" }, "sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ=="],
|
||||
|
||||
"@rollup/rollup-win32-x64-gnu": ["@rollup/rollup-win32-x64-gnu@4.52.4", "", { "os": "win32", "cpu": "x64" }, "sha512-UF9KfsH9yEam0UjTwAgdK0anlQ7c8/pWPU2yVjyWcF1I1thABt6WXE47cI71pGiZ8wGvxohBoLnxM04L/wj8mQ=="],
|
||||
"@rollup/rollup-openharmony-arm64": ["@rollup/rollup-openharmony-arm64@4.59.0", "", { "os": "none", "cpu": "arm64" }, "sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA=="],
|
||||
|
||||
"@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.52.4", "", { "os": "win32", "cpu": "x64" }, "sha512-bf9PtUa0u8IXDVxzRToFQKsNCRz9qLYfR/MpECxl4mRoWYjAeFjgxj1XdZr2M/GNVpT05p+LgQOHopYDlUu6/w=="],
|
||||
"@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.59.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A=="],
|
||||
|
||||
"@sinclair/typebox": ["@sinclair/typebox@0.34.41", "", {}, "sha512-6gS8pZzSXdyRHTIqoqSVknxolr1kzfy4/CeDnrzsVz8TTIWUbOBr6gnzOmTYJ3eXQNh4IYHIGi5aIL7sOZ2G/g=="],
|
||||
"@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.59.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA=="],
|
||||
|
||||
"@standard-schema/spec": ["@standard-schema/spec@1.0.0", "", {}, "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA=="],
|
||||
"@rollup/rollup-win32-x64-gnu": ["@rollup/rollup-win32-x64-gnu@4.59.0", "", { "os": "win32", "cpu": "x64" }, "sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA=="],
|
||||
|
||||
"@sveltejs/acorn-typescript": ["@sveltejs/acorn-typescript@1.0.6", "", { "peerDependencies": { "acorn": "^8.9.0" } }, "sha512-4awhxtMh4cx9blePWl10HRHj8Iivtqj+2QdDCSMDzxG+XKa9+VCNupQuCuvzEhYPzZSrX+0gC+0lHA/0fFKKQQ=="],
|
||||
"@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.59.0", "", { "os": "win32", "cpu": "x64" }, "sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA=="],
|
||||
|
||||
"@sinclair/typebox": ["@sinclair/typebox@0.34.48", "", {}, "sha512-kKJTNuK3AQOrgjjotVxMrCn1sUJwM76wMszfq1kdU4uYVJjvEWuFQ6HgvLt4Xz3fSmZlTOxJ/Ie13KnIcWQXFA=="],
|
||||
|
||||
"@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
|
||||
|
||||
"@sveltejs/acorn-typescript": ["@sveltejs/acorn-typescript@1.0.9", "", { "peerDependencies": { "acorn": "^8.9.0" } }, "sha512-lVJX6qEgs/4DOcRTpo56tmKzVPtoWAaVbL4hfO7t7NVwl9AAXzQR6cihesW1BmNMPl+bK6dreu2sOKBP2Q9CIA=="],
|
||||
|
||||
"@sveltejs/kit": ["@sveltejs/kit@2.53.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.6.3", "esm-env": "^1.2.2", "kleur": "^4.1.5", "magic-string": "^0.30.5", "mrmime": "^2.0.0", "set-cookie-parser": "^3.0.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 || ^7.0.0", "svelte": "^4.0.0 || ^5.0.0-next.0", "typescript": "^5.3.3", "vite": "^5.0.3 || ^6.0.0 || ^7.0.0-beta.0 || ^8.0.0" }, "optionalPeers": ["@opentelemetry/api", "typescript"], "bin": { "svelte-kit": "svelte-kit.js" } }, "sha512-iAIPEahFgDJJyvz8g0jP08KvqnM6JvdW8YfsygZ+pMeMvyM2zssWMltcsotETvjSZ82G3VlitgDtBIvpQSZrTA=="],
|
||||
|
||||
"@sveltejs/vite-plugin-svelte": ["@sveltejs/vite-plugin-svelte@6.2.4", "", { "dependencies": { "@sveltejs/vite-plugin-svelte-inspector": "^5.0.0", "deepmerge": "^4.3.1", "magic-string": "^0.30.21", "obug": "^2.1.0", "vitefu": "^1.1.1" }, "peerDependencies": { "svelte": "^5.0.0", "vite": "^6.3.0 || ^7.0.0" } }, "sha512-ou/d51QSdTyN26D7h6dSpusAKaZkAiGM55/AKYi+9AGZw7q85hElbjK3kEyzXHhLSnRISHOYzVge6x0jRZ7DXA=="],
|
||||
|
||||
"@sveltejs/vite-plugin-svelte-inspector": ["@sveltejs/vite-plugin-svelte-inspector@5.0.1", "", { "dependencies": { "debug": "^4.4.1" }, "peerDependencies": { "@sveltejs/vite-plugin-svelte": "^6.0.0-next.0", "svelte": "^5.0.0", "vite": "^6.3.0 || ^7.0.0" } }, "sha512-ubWshlMk4bc8mkwWbg6vNvCeT7lGQojE3ijDh3QTR6Zr/R+GXxsGbyH4PExEPpiFmqPhYiVSVmHBjUcVc1JIrA=="],
|
||||
"@sveltejs/vite-plugin-svelte-inspector": ["@sveltejs/vite-plugin-svelte-inspector@5.0.2", "", { "dependencies": { "obug": "^2.1.0" }, "peerDependencies": { "@sveltejs/vite-plugin-svelte": "^6.0.0-next.0", "svelte": "^5.0.0", "vite": "^6.3.0 || ^7.0.0" } }, "sha512-TZzRTcEtZffICSAoZGkPSl6Etsj2torOVrx6Uw0KpXxrec9Gg6jFWQ60Q3+LmNGfZSxHRCZL7vXVZIWmuV50Ig=="],
|
||||
|
||||
"@tailwindcss/node": ["@tailwindcss/node@4.2.1", "", { "dependencies": { "@jridgewell/remapping": "^2.3.5", "enhanced-resolve": "^5.19.0", "jiti": "^2.6.1", "lightningcss": "1.31.1", "magic-string": "^0.30.21", "source-map-js": "^1.2.1", "tailwindcss": "4.2.1" } }, "sha512-jlx6sLk4EOwO6hHe1oCGm1Q4AN/s0rSrTTPBGPM0/RQ6Uylwq17FuU8IeJJKEjtc6K6O07zsvP+gDO6MMWo7pg=="],
|
||||
|
||||
@@ -182,7 +223,7 @@
|
||||
|
||||
"@tailwindcss/vite": ["@tailwindcss/vite@4.2.1", "", { "dependencies": { "@tailwindcss/node": "4.2.1", "@tailwindcss/oxide": "4.2.1", "tailwindcss": "4.2.1" }, "peerDependencies": { "vite": "^5.2.0 || ^6 || ^7" } }, "sha512-TBf2sJjYeb28jD2U/OhwdW0bbOsxkWPwQ7SrqGf9sVcoYwZj7rkXljroBO9wKBut9XnmQLXanuDUeqQK0lGg/w=="],
|
||||
|
||||
"@tokenizer/inflate": ["@tokenizer/inflate@0.2.7", "", { "dependencies": { "debug": "^4.4.0", "fflate": "^0.8.2", "token-types": "^6.0.0" } }, "sha512-MADQgmZT1eKjp06jpI2yozxaU9uVs4GzzgSL+uEq7bVcJ9V1ZXQkeGNql1fsSI0gMy1vhvNTNbUqrx+pZfJVmg=="],
|
||||
"@tokenizer/inflate": ["@tokenizer/inflate@0.4.1", "", { "dependencies": { "debug": "^4.4.3", "token-types": "^6.1.1" } }, "sha512-2mAv+8pkG6GIZiF1kNg1jAjh27IDxEPKwdGul3snfztFerfPGI1LjDezZp3i7BElXompqEtPmoPx6c2wgtWsOA=="],
|
||||
|
||||
"@tokenizer/token": ["@tokenizer/token@0.3.0", "", {}, "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A=="],
|
||||
|
||||
@@ -192,11 +233,11 @@
|
||||
|
||||
"@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="],
|
||||
|
||||
"@types/node": ["@types/node@24.7.2", "", { "dependencies": { "undici-types": "~7.14.0" } }, "sha512-/NbVmcGTP+lj5oa4yiYxxeBjRivKQ5Ns1eSZeB99ExsEQ6rX5XYU1Zy/gGxY/ilqtD4Etx9mKyrPxZRetiahhA=="],
|
||||
"@types/node": ["@types/node@25.3.3", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-DpzbrH7wIcBaJibpKo9nnSQL0MTRdnWttGyE5haGwK86xgMOkFLp7vEyfQPGLOJh5wNYiJ3V9PmUMDhV9u8kkQ=="],
|
||||
|
||||
"@types/trusted-types": ["@types/trusted-types@2.0.7", "", {}, "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw=="],
|
||||
|
||||
"acorn": ["acorn@8.15.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg=="],
|
||||
"acorn": ["acorn@8.16.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw=="],
|
||||
|
||||
"aria-query": ["aria-query@5.3.1", "", {}, "sha512-Z/ZeOgVl7bcSYZ/u/rh0fOpvEpq//LZmdbkXyc7syVzjPAhfOa9ebsdTSjEBDU4vs5nC98Kfduj1uFo0qyET3g=="],
|
||||
|
||||
@@ -208,7 +249,7 @@
|
||||
|
||||
"clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="],
|
||||
|
||||
"cookie": ["cookie@0.6.0", "", {}, "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw=="],
|
||||
"cookie": ["cookie@1.1.1", "", {}, "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ=="],
|
||||
|
||||
"daisyui": ["daisyui@5.5.19", "", {}, "sha512-pbFAkl1VCEh/MPCeclKL61I/MqRIFFhNU7yiXoDDRapXN4/qNCoMxeCCswyxEEhqL5eiTTfwHvucFtOE71C9sA=="],
|
||||
|
||||
@@ -236,9 +277,7 @@
|
||||
|
||||
"fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="],
|
||||
|
||||
"fflate": ["fflate@0.8.2", "", {}, "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A=="],
|
||||
|
||||
"file-type": ["file-type@21.0.0", "", { "dependencies": { "@tokenizer/inflate": "^0.2.7", "strtok3": "^10.2.2", "token-types": "^6.0.0", "uint8array-extras": "^1.4.0" } }, "sha512-ek5xNX2YBYlXhiUXui3D/BXa3LdqPmoLJ7rqEx2bKJ7EAUEfmXgW0Das7Dc6Nr9MvqaOnIqiPV0mZk/r/UpNAg=="],
|
||||
"file-type": ["file-type@21.3.0", "", { "dependencies": { "@tokenizer/inflate": "^0.4.1", "strtok3": "^10.3.4", "token-types": "^6.1.1", "uint8array-extras": "^1.4.0" } }, "sha512-8kPJMIGz1Yt/aPEwOsrR97ZyZaD1Iqm8PClb1nYFclUCkBi0Ma5IsYNQzvSFS9ib51lWyIw5mIT9rWzI/xjpzA=="],
|
||||
|
||||
"fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
|
||||
|
||||
@@ -318,7 +357,7 @@
|
||||
|
||||
"readdirp": ["readdirp@4.1.2", "", {}, "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg=="],
|
||||
|
||||
"rollup": ["rollup@4.52.4", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.52.4", "@rollup/rollup-android-arm64": "4.52.4", "@rollup/rollup-darwin-arm64": "4.52.4", "@rollup/rollup-darwin-x64": "4.52.4", "@rollup/rollup-freebsd-arm64": "4.52.4", "@rollup/rollup-freebsd-x64": "4.52.4", "@rollup/rollup-linux-arm-gnueabihf": "4.52.4", "@rollup/rollup-linux-arm-musleabihf": "4.52.4", "@rollup/rollup-linux-arm64-gnu": "4.52.4", "@rollup/rollup-linux-arm64-musl": "4.52.4", "@rollup/rollup-linux-loong64-gnu": "4.52.4", "@rollup/rollup-linux-ppc64-gnu": "4.52.4", "@rollup/rollup-linux-riscv64-gnu": "4.52.4", "@rollup/rollup-linux-riscv64-musl": "4.52.4", "@rollup/rollup-linux-s390x-gnu": "4.52.4", "@rollup/rollup-linux-x64-gnu": "4.52.4", "@rollup/rollup-linux-x64-musl": "4.52.4", "@rollup/rollup-openharmony-arm64": "4.52.4", "@rollup/rollup-win32-arm64-msvc": "4.52.4", "@rollup/rollup-win32-ia32-msvc": "4.52.4", "@rollup/rollup-win32-x64-gnu": "4.52.4", "@rollup/rollup-win32-x64-msvc": "4.52.4", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-CLEVl+MnPAiKh5pl4dEWSyMTpuflgNQiLGhMv8ezD5W/qP8AKvmYpCOKRRNOh7oRKnauBZ4SyeYkMS+1VSyKwQ=="],
|
||||
"rollup": ["rollup@4.59.0", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.59.0", "@rollup/rollup-android-arm64": "4.59.0", "@rollup/rollup-darwin-arm64": "4.59.0", "@rollup/rollup-darwin-x64": "4.59.0", "@rollup/rollup-freebsd-arm64": "4.59.0", "@rollup/rollup-freebsd-x64": "4.59.0", "@rollup/rollup-linux-arm-gnueabihf": "4.59.0", "@rollup/rollup-linux-arm-musleabihf": "4.59.0", "@rollup/rollup-linux-arm64-gnu": "4.59.0", "@rollup/rollup-linux-arm64-musl": "4.59.0", "@rollup/rollup-linux-loong64-gnu": "4.59.0", "@rollup/rollup-linux-loong64-musl": "4.59.0", "@rollup/rollup-linux-ppc64-gnu": "4.59.0", "@rollup/rollup-linux-ppc64-musl": "4.59.0", "@rollup/rollup-linux-riscv64-gnu": "4.59.0", "@rollup/rollup-linux-riscv64-musl": "4.59.0", "@rollup/rollup-linux-s390x-gnu": "4.59.0", "@rollup/rollup-linux-x64-gnu": "4.59.0", "@rollup/rollup-linux-x64-musl": "4.59.0", "@rollup/rollup-openbsd-x64": "4.59.0", "@rollup/rollup-openharmony-arm64": "4.59.0", "@rollup/rollup-win32-arm64-msvc": "4.59.0", "@rollup/rollup-win32-ia32-msvc": "4.59.0", "@rollup/rollup-win32-x64-gnu": "4.59.0", "@rollup/rollup-win32-x64-msvc": "4.59.0", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg=="],
|
||||
|
||||
"sade": ["sade@1.8.1", "", { "dependencies": { "mri": "^1.1.0" } }, "sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A=="],
|
||||
|
||||
@@ -344,7 +383,7 @@
|
||||
|
||||
"tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="],
|
||||
|
||||
"token-types": ["token-types@6.1.1", "", { "dependencies": { "@borewit/text-codec": "^0.1.0", "@tokenizer/token": "^0.3.0", "ieee754": "^1.2.1" } }, "sha512-kh9LVIWH5CnL63Ipf0jhlBIy0UsrMj/NJDfpsy1SqOXlLKEVyXXYrnFxFT1yOOYVGBSApeVnjPw/sBz5BfEjAQ=="],
|
||||
"token-types": ["token-types@6.1.2", "", { "dependencies": { "@borewit/text-codec": "^0.2.1", "@tokenizer/token": "^0.3.0", "ieee754": "^1.2.1" } }, "sha512-dRXchy+C0IgK8WPC6xvCHFRIWYUbqqdEIKPaKo/AcTUNzwLTK6AH7RjdLWsEZcAN/TBdtfUw3PYEgPr5VPr6ww=="],
|
||||
|
||||
"totalist": ["totalist@3.0.1", "", {}, "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ=="],
|
||||
|
||||
@@ -352,16 +391,18 @@
|
||||
|
||||
"uint8array-extras": ["uint8array-extras@1.5.0", "", {}, "sha512-rvKSBiC5zqCCiDZ9kAOszZcDvdAHwwIKJG33Ykj43OKcWsnmcBRL09YTU4nOeHZ8Y2a7l1MgTd08SBe9A8Qj6A=="],
|
||||
|
||||
"undici-types": ["undici-types@7.14.0", "", {}, "sha512-QQiYxHuyZ9gQUIrmPo3IA+hUl4KYk8uSA7cHrcKd/l3p1OTpZcM0Tbp9x7FAtXdAYhlasd60ncPpgu6ihG6TOA=="],
|
||||
"undici-types": ["undici-types@7.18.2", "", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="],
|
||||
|
||||
"vite": ["vite@7.3.1", "", { "dependencies": { "esbuild": "^0.27.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-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA=="],
|
||||
|
||||
"vitefu": ["vitefu@1.1.1", "", { "peerDependencies": { "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0-beta.0" }, "optionalPeers": ["vite"] }, "sha512-B/Fegf3i8zh0yFbpzZ21amWzHmuNlLlmJT6n7bu5e+pCHUKQIfXSYokrqOBGEMMe9UG2sostKQF9mml/vYaWJQ=="],
|
||||
"vitefu": ["vitefu@1.1.2", "", { "peerDependencies": { "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-beta.0" }, "optionalPeers": ["vite"] }, "sha512-zpKATdUbzbsycPFBN71nS2uzBUQiVnFoOrr2rvqv34S1lcAgMKKkjWleLGeiJlZ8lwCXvtWaRn7R3ZC16SYRuw=="],
|
||||
|
||||
"zimmerframe": ["zimmerframe@1.1.4", "", {}, "sha512-B58NGBEoc8Y9MWWCQGl/gq9xBCe4IiKM0a2x7GZdQKOW5Exr8S1W24J6OgM1njK8xCRGvAJIL/MxXHf6SkmQKQ=="],
|
||||
|
||||
"zipcodes-us": ["zipcodes-us@1.1.2", "", {}, "sha512-UmUTS0O0/IHb0hbxv5r6IfATvD2B7FKcJTLsSv/dENBRiNACFUrGmhPuVss3kAhuhEpPH6NwKHJ7/rGMNXq5yA=="],
|
||||
|
||||
"@sveltejs/kit/cookie": ["cookie@0.6.0", "", {}, "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw=="],
|
||||
|
||||
"@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.8.1", "", { "dependencies": { "@emnapi/wasi-threads": "1.1.0", "tslib": "^2.4.0" }, "bundled": true }, "sha512-AvT9QFpxK0Zd8J0jopedNm+w/2fIzvtPKPjqyw9jwvBaReTTqPBk9Hixaz7KbjimP+QNz605/XnjFcDAL2pqBg=="],
|
||||
|
||||
"@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.8.1", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg=="],
|
||||
@@ -373,7 +414,5 @@
|
||||
"@tailwindcss/oxide-wasm32-wasi/@tybys/wasm-util": ["@tybys/wasm-util@0.10.1", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg=="],
|
||||
|
||||
"@tailwindcss/oxide-wasm32-wasi/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
|
||||
|
||||
"elysia/cookie": ["cookie@1.1.1", "", {}, "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ=="],
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
# fly.toml app configuration file generated for blade-and-brawn on 2025-05-04T14:56:24-04:00
|
||||
#
|
||||
# See https://fly.io/docs/reference/configuration/ for information about how to use this file.
|
||||
#
|
||||
|
||||
app = 'blade-and-brawn'
|
||||
primary_region = 'ord'
|
||||
|
||||
[build]
|
||||
|
||||
[env]
|
||||
PORT = '3000'
|
||||
|
||||
[http_service]
|
||||
internal_port = 3000
|
||||
force_https = true
|
||||
auto_stop_machines = 'stop'
|
||||
auto_start_machines = true
|
||||
min_machines_running = 0
|
||||
processes = ['app']
|
||||
|
||||
[[vm]]
|
||||
memory = '1gb'
|
||||
cpu_kind = 'shared'
|
||||
cpus = 1
|
||||
|
||||
[[mounts]]
|
||||
source = "data"
|
||||
destination = "/data"
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,22 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title></title>
|
||||
<script src="main.js"></script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<h1 id="currentClass">None</h1>
|
||||
<div id="btnContainer">
|
||||
<button id="btn-rogue">Rogue</button>
|
||||
<button id="btn-knight">Knight</button>
|
||||
<button id="btn-warrior">Warrior</button>
|
||||
</div>
|
||||
|
||||
<div id="audioContainer"></div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -0,0 +1,96 @@
|
||||
const $ = (id) => document.getElementById(id);
|
||||
|
||||
let currentAudio = null;
|
||||
function playAudio(newAudio) {
|
||||
// --------------
|
||||
// HELPERS
|
||||
// --------------
|
||||
const TIME_STEP = 500;
|
||||
const VOL_STEP = .05;
|
||||
|
||||
function fadeOut(audio) {
|
||||
const intervalId = setInterval(() => {
|
||||
audio.volume = Math.max(0, audio.volume - VOL_STEP);
|
||||
if (audio.volume <= 0) {
|
||||
clearInterval(intervalId);
|
||||
}
|
||||
}, TIME_STEP)
|
||||
}
|
||||
|
||||
function fadeIn(audio) {
|
||||
audio.volume = 0;
|
||||
audio.play();
|
||||
const intervalId = setInterval(() => {
|
||||
audio.volume = Math.min(1, audio.volume + VOL_STEP);
|
||||
if (audio.volume >= 1) {
|
||||
clearInterval(intervalId);
|
||||
}
|
||||
}, TIME_STEP)
|
||||
}
|
||||
// --------------
|
||||
|
||||
// --------------
|
||||
// MAIN LOGIC
|
||||
// --------------
|
||||
|
||||
const prevAudio = currentAudio;
|
||||
|
||||
if (prevAudio === newAudio)
|
||||
return;
|
||||
|
||||
if (prevAudio) {
|
||||
fadeOut(prevAudio);
|
||||
}
|
||||
|
||||
fadeIn(newAudio);
|
||||
|
||||
currentAudio = newAudio;
|
||||
// --------------
|
||||
}
|
||||
|
||||
document.addEventListener("DOMContentLoaded", async function() {
|
||||
const $ = (id) => document.getElementById(id);
|
||||
|
||||
const classNames = ["rogue", "knight", "warrior"];
|
||||
const audios = {};
|
||||
|
||||
for (const className of classNames) {
|
||||
const audio = document.createElement("audio");
|
||||
audio.src = `https://bladeandbrawn.com/${className}.ogg`;
|
||||
audio.loop = true;
|
||||
audio.volume = 0;
|
||||
audio.id = `audio-${className}`;
|
||||
$("audioContainer").appendChild(audio);
|
||||
audios[className] = audio;
|
||||
}
|
||||
|
||||
await Promise.all(classNames.map(async (className) => {
|
||||
const audio = audios[className];
|
||||
try {
|
||||
await audio.play();
|
||||
audio.pause();
|
||||
audio.currentTime = 0;
|
||||
} catch (e) {
|
||||
console.warn(`Autoplay failed for ${className}:`, e);
|
||||
}
|
||||
}));
|
||||
|
||||
// Start all in perfect sync
|
||||
classNames.forEach(className => {
|
||||
const audio = audios[className];
|
||||
audio.currentTime = 0;
|
||||
audio.play();
|
||||
});
|
||||
|
||||
// Hook up buttons
|
||||
classNames.forEach(className => {
|
||||
$(`btn-${className}`).onclick = () => {
|
||||
$("currentClass").textContent = className;
|
||||
playAudio(audios[className]);
|
||||
};
|
||||
});
|
||||
|
||||
// Set initial state
|
||||
$("currentClass").textContent = "rogue";
|
||||
playAudio(audios["rogue"]);
|
||||
});
|
||||
+10
-28
@@ -1,32 +1,14 @@
|
||||
{
|
||||
"type": "module",
|
||||
"name": "blade-and-brawn",
|
||||
"private": true,
|
||||
"workspaces": [
|
||||
"apps/*",
|
||||
"packages/*"
|
||||
],
|
||||
"peerDependencies": {
|
||||
"typescript": "^5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@sveltejs/kit": "^2.53.4",
|
||||
"@sveltejs/vite-plugin-svelte": "^6.2.4",
|
||||
"@types/bun": "^1.3.9",
|
||||
"svelte": "^5.53.6",
|
||||
"svelte-adapter-bun": "^0.5.2",
|
||||
"svelte-check": "^4.4.4",
|
||||
"typescript": "^5.9.3",
|
||||
"vite": "^7.3.1"
|
||||
},
|
||||
"scripts": {
|
||||
"dev": "vite dev",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview",
|
||||
"prepare": "svelte-kit sync || echo ''",
|
||||
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
|
||||
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch"
|
||||
},
|
||||
"dependencies": {
|
||||
"@elysiajs/cors": "^1.4.1",
|
||||
"@elysiajs/eden": "^1.4.8",
|
||||
"@tailwindcss/vite": "^4.2.1",
|
||||
"daisyui": "^5.5.19",
|
||||
"elysia": "^1.4.27",
|
||||
"memoirist": "^0.4.0",
|
||||
"ml-levenberg-marquardt": "^5.0.0",
|
||||
"tailwindcss": "^4.2.1",
|
||||
"zipcodes-us": "^1.1.2"
|
||||
"@types/bun": "^1.3.9"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"name": "@blade-and-brawn/calculator",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"ml-levenberg-marquardt": "^5.0.0"
|
||||
},
|
||||
"exports": {
|
||||
".": "./src/index.ts"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,902 @@
|
||||
import {
|
||||
Activity,
|
||||
Attribute,
|
||||
Gender,
|
||||
getAvgWeight,
|
||||
kgToLb,
|
||||
lbToKg,
|
||||
range,
|
||||
type ActivityPerformance,
|
||||
type Player,
|
||||
type StandardUnit,
|
||||
} from "../src/util";
|
||||
import { levenbergMarquardt as LM } from "ml-levenberg-marquardt";
|
||||
import defaultConfig from "./config.json" assert { type: "json" };
|
||||
import defaultStandardsJson from "./data/default-standards.json" assert { type: "json" };
|
||||
|
||||
// SOURCES
|
||||
// Squat, Bench, Dead Lift:
|
||||
// http://lonkilgore.com/resources/Lon_Kilgore_Strength_Standard_Tables-Copyright-2023.pdf
|
||||
// 1 mile run:
|
||||
// https://runninglevel.com/running-times/1-mile-times
|
||||
// Broad Jump:
|
||||
// https://nrpt.co.uk/training/tests/power/broad.htm
|
||||
// 3 Cone drill:
|
||||
// https://nflsavant.com/combine.php?utm_source=chatgpt.com
|
||||
|
||||
export * from "./util"
|
||||
|
||||
export type LevelCalculatorOutput = {
|
||||
player: number;
|
||||
attributes: Record<Attribute, number>;
|
||||
};
|
||||
|
||||
export type Levels = Record<string, number>;
|
||||
|
||||
export interface Metrics {
|
||||
age: number;
|
||||
weight: number;
|
||||
gender: Gender;
|
||||
}
|
||||
|
||||
type NumberMetric = Extract<keyof Metrics, "age" | "weight">;
|
||||
|
||||
export interface Standard {
|
||||
metrics: Metrics;
|
||||
levels: Levels;
|
||||
}
|
||||
|
||||
type Generator = {
|
||||
metric: NumberMetric;
|
||||
};
|
||||
|
||||
export type StandardsData = Record<
|
||||
Activity,
|
||||
{
|
||||
metadata: {
|
||||
attribute: Attribute;
|
||||
generators: Generator[];
|
||||
unit: StandardUnit;
|
||||
name: string;
|
||||
};
|
||||
standards: Standard[];
|
||||
}
|
||||
>;
|
||||
|
||||
const metricPriority = (m: NumberMetric) => {
|
||||
if (m === "age") return 0;
|
||||
if (m === "weight") return 1;
|
||||
return 2;
|
||||
};
|
||||
|
||||
export const DEFAULT_STANDARDS_DATA = defaultStandardsJson as StandardsData;
|
||||
|
||||
export class LevelCalculator {
|
||||
standards: Standards;
|
||||
|
||||
public constructor(standards: Standards) {
|
||||
this.standards = standards;
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------------------------------
|
||||
// Level calculations
|
||||
// -------------------------------------------------------------------------------------------------
|
||||
|
||||
public calculate(
|
||||
player: Player,
|
||||
activityPerformances: ActivityPerformance[],
|
||||
): LevelCalculatorOutput {
|
||||
const levels: LevelCalculatorOutput = {
|
||||
player: 0,
|
||||
attributes: {
|
||||
[Attribute.Strength]: 0,
|
||||
[Attribute.Power]: 0,
|
||||
[Attribute.Endurance]: 0,
|
||||
[Attribute.Agility]: 0,
|
||||
} as const,
|
||||
};
|
||||
|
||||
for (const value of Object.values(player)) {
|
||||
if (!value) return levels;
|
||||
}
|
||||
|
||||
levels.attributes = this.calculateAllAttributeLevels(
|
||||
player,
|
||||
activityPerformances,
|
||||
);
|
||||
for (const level of Object.values(levels.attributes)) {
|
||||
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,
|
||||
);
|
||||
|
||||
return levels;
|
||||
}
|
||||
|
||||
private calculateAllAttributeLevels(
|
||||
player: Player,
|
||||
activityPerformances: ActivityPerformance[],
|
||||
): Record<Attribute, number> {
|
||||
const attrLevels: Record<Attribute, number> = {
|
||||
[Attribute.Strength]: 0,
|
||||
[Attribute.Power]: 0,
|
||||
[Attribute.Endurance]: 0,
|
||||
[Attribute.Agility]: 0,
|
||||
} as const;
|
||||
|
||||
for (const value of Object.values(player)) {
|
||||
if (!value) return attrLevels;
|
||||
}
|
||||
|
||||
(Object.values(Attribute) as Attribute[]).forEach((attribute) => {
|
||||
const attrActivityPerformances = activityPerformances.filter(
|
||||
(p) =>
|
||||
this.standards.byActivity(p.activity).getMetadata()
|
||||
.attribute === attribute,
|
||||
);
|
||||
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,
|
||||
)
|
||||
) {
|
||||
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;
|
||||
}
|
||||
|
||||
// must have all activties using a valid performance value (> 0)
|
||||
for (const activityPerformance of activityPerformances) {
|
||||
if (activityPerformance.performance <= 0) return 0;
|
||||
}
|
||||
|
||||
const activityLevels = activityPerformances.map((p) => {
|
||||
const interpolatedStandard = this.standards
|
||||
.byActivity(p.activity)
|
||||
.byMetrics(player.metrics)
|
||||
.getOneInterpolated();
|
||||
return this.findLevel(interpolatedStandard, p.performance);
|
||||
});
|
||||
|
||||
const activityLevelsAvg = Math.round(
|
||||
activityLevels.reduce((sum, curr) => sum + curr, 0) /
|
||||
activityLevels.length,
|
||||
);
|
||||
|
||||
return activityLevelsAvg;
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------------------------------
|
||||
// Level utilities (helpers)
|
||||
// -------------------------------------------------------------------------------------------------
|
||||
|
||||
private findLevel(standard: Standard, performance: number): number {
|
||||
let resultLevel = 1;
|
||||
let resultLevelDiff = Infinity;
|
||||
|
||||
for (const level in standard.levels) {
|
||||
if (!standard.levels[level]) continue;
|
||||
|
||||
const diff = Math.abs(performance - standard.levels[level]);
|
||||
if (diff < resultLevelDiff) {
|
||||
resultLevel = +level;
|
||||
resultLevelDiff = diff;
|
||||
}
|
||||
}
|
||||
return resultLevel;
|
||||
}
|
||||
}
|
||||
|
||||
export type StandardsConfig = {
|
||||
global: {
|
||||
maxLevel: number;
|
||||
};
|
||||
activity: Record<
|
||||
Activity,
|
||||
{
|
||||
weightModifier: number;
|
||||
weightSkew: number;
|
||||
ageModifier: number;
|
||||
enableGeneration: boolean;
|
||||
difficultyModifier: number;
|
||||
peakAge: number;
|
||||
stretch: {
|
||||
upper: number;
|
||||
lower: number;
|
||||
};
|
||||
}
|
||||
>;
|
||||
};
|
||||
|
||||
export class Standards {
|
||||
readonly cfg: StandardsConfig;
|
||||
private standardsData: StandardsData;
|
||||
|
||||
constructor(
|
||||
standardsData: StandardsData,
|
||||
cfg?: Partial<StandardsConfig>,
|
||||
) {
|
||||
this.cfg = Object.assign(defaultConfig, cfg);
|
||||
|
||||
// prepare data
|
||||
this.standardsData = structuredClone(standardsData);
|
||||
|
||||
// STRETCH
|
||||
for (const activity of Object.values(Activity)) {
|
||||
const BASE_MAX_LEVEL = 5;
|
||||
|
||||
function expDecayModel([A, B, C]: number[]) {
|
||||
return (i: number) => A * Math.exp(-B * i) + C;
|
||||
}
|
||||
|
||||
if (!this.cfg.activity[activity].enableGeneration) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (const gender of Object.values(Gender)) {
|
||||
const standardsByGender = this.standardsData[
|
||||
activity
|
||||
].standards.filter((s) => s["metrics"].gender === gender);
|
||||
const ages = [
|
||||
...new Set(standardsByGender.map((s) => s.metrics.age)),
|
||||
];
|
||||
for (const age of ages) {
|
||||
const standards = standardsByGender.filter(
|
||||
(s) => s.metrics.age === age,
|
||||
);
|
||||
|
||||
const data = {
|
||||
x: [] as number[],
|
||||
y: [] as number[],
|
||||
};
|
||||
|
||||
const isIncreasing =
|
||||
standards[0].levels["2"] > standards[0].levels["1"];
|
||||
|
||||
for (const i of range(BASE_MAX_LEVEL).slice(0, -1)) {
|
||||
const level = i + 1;
|
||||
const progressionRatios: number[] = [];
|
||||
for (const standard of standards) {
|
||||
const curr = standard.levels[level];
|
||||
const next = standard.levels[level + 1];
|
||||
progressionRatios.push(
|
||||
isIncreasing ? next / curr : curr / next,
|
||||
);
|
||||
}
|
||||
|
||||
const sum = progressionRatios.reduce(
|
||||
(p, c) => p + c,
|
||||
0,
|
||||
);
|
||||
const progressionRatioAvg =
|
||||
sum / progressionRatios.length;
|
||||
|
||||
data.x.push(i);
|
||||
data.y.push(progressionRatioAvg);
|
||||
}
|
||||
|
||||
const fittedParams = LM(data, expDecayModel, {
|
||||
initialValues: [0.4, 0.5, 1.1],
|
||||
minValues: [0.0, 0.0, 1.02],
|
||||
maxValues: [1.0, 2.0, 1.2],
|
||||
maxIterations: 200,
|
||||
});
|
||||
const getDecayRatio = (i: number) =>
|
||||
expDecayModel(fittedParams.parameterValues)(i);
|
||||
|
||||
// UPDATE THE DATA
|
||||
for (const standard of standards) {
|
||||
const newLevels: Levels = {};
|
||||
const newLowerLevelCount = Math.max(
|
||||
Math.round(
|
||||
this.cfg.activity[activity].stretch.lower,
|
||||
),
|
||||
0,
|
||||
);
|
||||
const newUpperLevelCount = Math.max(
|
||||
Math.round(
|
||||
this.cfg.activity[activity].stretch.upper,
|
||||
),
|
||||
0,
|
||||
);
|
||||
|
||||
const oldLevels = standard.levels as Levels;
|
||||
let prev = oldLevels["1"];
|
||||
for (const i of range(newLowerLevelCount).reverse()) {
|
||||
const level = i + 1;
|
||||
const ratio = getDecayRatio(-i - 1);
|
||||
prev = isIncreasing ? prev / ratio : prev * ratio;
|
||||
newLevels[level] = Math.round(prev);
|
||||
}
|
||||
|
||||
for (const i of range(BASE_MAX_LEVEL)) {
|
||||
const level = i + 1;
|
||||
newLevels[level + newLowerLevelCount] =
|
||||
oldLevels[level];
|
||||
}
|
||||
|
||||
prev = oldLevels[BASE_MAX_LEVEL];
|
||||
for (const i of range(newUpperLevelCount)) {
|
||||
const level =
|
||||
BASE_MAX_LEVEL + newLowerLevelCount + (i + 1);
|
||||
const ratio = getDecayRatio(level - 1);
|
||||
prev = isIncreasing ? prev * ratio : prev / ratio;
|
||||
newLevels[level] = Math.round(prev);
|
||||
}
|
||||
|
||||
standard.levels = newLevels;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// EXPAND, COMPRESS, SKEW
|
||||
for (const activity of Object.values(Activity)) {
|
||||
for (const standard of this.standardsData[activity].standards) {
|
||||
// expand
|
||||
let i = 1;
|
||||
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,
|
||||
);
|
||||
}
|
||||
|
||||
// apply skew config
|
||||
for (const level in standard.levels) {
|
||||
standard.levels[level] =
|
||||
standard.levels[level] *
|
||||
(1 + this.cfg.activity[activity].difficultyModifier);
|
||||
}
|
||||
}
|
||||
|
||||
// generate data
|
||||
const allGenerators = this.cfg.activity[activity].enableGeneration
|
||||
? this.standardsData[activity].metadata.generators.sort(
|
||||
(a, b) =>
|
||||
metricPriority(a.metric) - metricPriority(b.metric),
|
||||
)
|
||||
: [];
|
||||
|
||||
for (const generator of allGenerators) {
|
||||
if (generator.metric === "age") {
|
||||
for (const gender of Object.values(Gender)) {
|
||||
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,
|
||||
})
|
||||
.getOneInterpolated();
|
||||
|
||||
let currAge = minAge;
|
||||
while (currAge <= maxAge) {
|
||||
const newStandard: Standard = {
|
||||
metrics: {
|
||||
weight: getAvgWeight(gender, currAge),
|
||||
age: currAge,
|
||||
gender,
|
||||
},
|
||||
levels: {},
|
||||
};
|
||||
|
||||
for (const lvl in referenceStandard.levels) {
|
||||
const base = referenceStandard.levels[lvl];
|
||||
if (base == null) continue;
|
||||
|
||||
const ageModifier =
|
||||
this.cfg.activity[activity].ageModifier;
|
||||
|
||||
const leftSpan = peakAge - minAge;
|
||||
const rightSpan = maxAge - peakAge;
|
||||
|
||||
// avoid divide-by-zero
|
||||
if (leftSpan <= 0 || rightSpan <= 0) {
|
||||
newStandard.levels[lvl] = base;
|
||||
continue;
|
||||
}
|
||||
|
||||
const cy =
|
||||
(ageModifier * 0.5) / (leftSpan * leftSpan);
|
||||
const co =
|
||||
(ageModifier * 0.5) /
|
||||
(rightSpan * rightSpan);
|
||||
|
||||
const d =
|
||||
currAge <= peakAge
|
||||
? peakAge - currAge
|
||||
: currAge - peakAge;
|
||||
const c = currAge <= peakAge ? cy : co;
|
||||
|
||||
let f = 1 - c * d * d;
|
||||
|
||||
const minFactor = 0.2;
|
||||
const maxFactor = 10.0;
|
||||
f = Math.min(maxFactor, Math.max(minFactor, f));
|
||||
|
||||
newStandard.levels[lvl] = base * f;
|
||||
}
|
||||
|
||||
// prefer real data over generated data
|
||||
const overlappingStandard = this.byActivity(
|
||||
activity,
|
||||
)
|
||||
.byMetrics(newStandard.metrics)
|
||||
.getOne();
|
||||
if (!overlappingStandard)
|
||||
this.standardsData[activity].standards.push(
|
||||
newStandard,
|
||||
);
|
||||
|
||||
currAge += ageStep;
|
||||
}
|
||||
}
|
||||
|
||||
// now that we generated metric data, remove the old data
|
||||
this.standardsData[activity].standards =
|
||||
this.standardsData[activity].standards.filter(
|
||||
(s) => s.metrics.age,
|
||||
);
|
||||
} else if (generator.metric === "weight") {
|
||||
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 referenceStandard = this.byActivity(activity)
|
||||
.byMetrics({
|
||||
weight: referenceWeight,
|
||||
gender,
|
||||
age,
|
||||
})
|
||||
.getOneInterpolated();
|
||||
|
||||
let currWeight = minWeight;
|
||||
while (currWeight <= maxWeight) {
|
||||
const newStandard: Standard = {
|
||||
metrics: {
|
||||
weight: currWeight,
|
||||
age,
|
||||
gender,
|
||||
},
|
||||
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;
|
||||
|
||||
newStandard.levels[lvl] =
|
||||
referenceStandard.levels[lvl] *
|
||||
coefficient ** scalingExponent;
|
||||
}
|
||||
|
||||
// prefer real data over generated data
|
||||
const overlappingStandard = this.byActivity(
|
||||
activity,
|
||||
)
|
||||
.byMetrics(newStandard.metrics)
|
||||
.getOne();
|
||||
if (!overlappingStandard)
|
||||
this.standardsData[
|
||||
activity
|
||||
].standards.push(newStandard);
|
||||
|
||||
currWeight += weightStep;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// now that we generated metric data, remove the old data
|
||||
this.standardsData[activity].standards =
|
||||
this.standardsData[activity].standards.filter(
|
||||
(s) => s.metrics.weight,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
this.standardsData[activity].standards = this.standardsData[
|
||||
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;
|
||||
if (a.metrics.age !== b.metrics.age)
|
||||
return a.metrics.age - b.metrics.age;
|
||||
return a.metrics.weight - b.metrics.weight;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public byActivity(activity: Activity) {
|
||||
const self = this;
|
||||
|
||||
const state: Partial<Metrics> = {};
|
||||
|
||||
// EXEC METHODS
|
||||
const execMethods = {
|
||||
exact: {
|
||||
// these will get EXACT matches
|
||||
getAll(): Standard[] {
|
||||
const src = self.standardsData[activity].standards;
|
||||
|
||||
const out: Standard[] = [];
|
||||
for (let i = 0; i < src.length; i++) {
|
||||
const s = src[i];
|
||||
if (!s) 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 {
|
||||
return execMethods.exact.getAll()[0];
|
||||
},
|
||||
getNearest(
|
||||
metric: NumberMetric,
|
||||
target: number,
|
||||
): { lower: Standard; upper: Standard } {
|
||||
const standards = execMethods.exact.getAll();
|
||||
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;
|
||||
|
||||
for (const standard of standards) {
|
||||
const standardMetricValue = standard.metrics[metric];
|
||||
|
||||
if (standardMetricValue <= target) {
|
||||
if (
|
||||
!hasLower ||
|
||||
standardMetricValue > lower.metrics[metric]
|
||||
) {
|
||||
lower = standard;
|
||||
hasLower = true;
|
||||
}
|
||||
}
|
||||
if (standardMetricValue >= target) {
|
||||
if (
|
||||
!hasUpper ||
|
||||
standardMetricValue < upper.metrics[metric]
|
||||
) {
|
||||
upper = standard;
|
||||
hasUpper = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// clamp to ends if one side missing
|
||||
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)
|
||||
.byGender(metrics.gender)
|
||||
.byAge(targetAge)
|
||||
.getNearest("weight", metrics.weight);
|
||||
|
||||
const weightLower = lowerAgeWeightStandard.metrics.weight;
|
||||
const weightUpper = upperAgeWeightStandard.metrics.weight;
|
||||
|
||||
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,
|
||||
);
|
||||
};
|
||||
|
||||
// Find nearest age standards
|
||||
const { lower: lowerAgeStandard, upper: upperAgeStandard } =
|
||||
this.byActivity(activity)
|
||||
.byGender(metrics.gender)
|
||||
.getNearest("age", metrics.age);
|
||||
|
||||
const ageLower = lowerAgeStandard.metrics.age;
|
||||
const ageUpper = upperAgeStandard.metrics.age;
|
||||
|
||||
// Interpolate by age and weight
|
||||
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,
|
||||
);
|
||||
|
||||
return {
|
||||
metrics: metrics,
|
||||
levels: interpolatedLevels,
|
||||
};
|
||||
};
|
||||
|
||||
const getAllInterpolated = (opt?: {
|
||||
normalizeForLb?: boolean;
|
||||
}): Standard[] => {
|
||||
const metrics: Metrics = state as Metrics;
|
||||
|
||||
let weights = this.weightsFor(
|
||||
activity,
|
||||
metrics.gender,
|
||||
metrics.age,
|
||||
);
|
||||
if (opt?.normalizeForLb) {
|
||||
weights = weights.map((w) => lbToKg(Math.round(kgToLb(w))));
|
||||
}
|
||||
|
||||
const interpolatedStandards: Standard[] = weights.map((weight) =>
|
||||
interpolateByAgeAndWeight({ ...metrics, weight }),
|
||||
);
|
||||
|
||||
return interpolatedStandards;
|
||||
};
|
||||
|
||||
const getOneInterpolated = (): Standard => {
|
||||
const metrics: Metrics = state as Metrics;
|
||||
return interpolateByAgeAndWeight(metrics);
|
||||
};
|
||||
|
||||
const getMetadata = () => this.standardsData[activity].metadata;
|
||||
|
||||
// QUERY METHODS
|
||||
|
||||
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,
|
||||
};
|
||||
return initialMethods;
|
||||
}
|
||||
|
||||
public getAttributeActivities(attribute: Attribute): Activity[] {
|
||||
const activities: Activity[] = [];
|
||||
for (const activity of Object.keys(
|
||||
this.standardsData,
|
||||
) as Activity[]) {
|
||||
const standardData = this.standardsData[
|
||||
activity
|
||||
] as StandardsData[Activity];
|
||||
if (standardData.metadata.attribute === attribute) {
|
||||
activities.push(activity as Activity);
|
||||
}
|
||||
}
|
||||
return activities;
|
||||
}
|
||||
|
||||
// TODO: turn these into queries?
|
||||
public weightsFor(activity: Activity, gender: Gender, age: number) {
|
||||
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 weights = this.byActivity(activity)
|
||||
.byGender(gender)
|
||||
.byAge(closestAge)
|
||||
.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))];
|
||||
return ages;
|
||||
}
|
||||
|
||||
private expandLevels(levels: Levels, i: number): Levels {
|
||||
if (i == 0) {
|
||||
return levels;
|
||||
}
|
||||
const levelKeys = Object.keys(levels)
|
||||
.map(Number)
|
||||
.sort((a, b) => a - b);
|
||||
|
||||
const newLevels: Levels = {};
|
||||
let j = 1;
|
||||
|
||||
for (let k = 0; k < levelKeys.length; ++k) {
|
||||
const currLevel = levelKeys[k];
|
||||
const nextLevel = levelKeys[k + 1];
|
||||
|
||||
newLevels[j++] = levels[currLevel];
|
||||
|
||||
if (nextLevel) {
|
||||
newLevels[j++] = (levels[currLevel] + levels[nextLevel]) / 2;
|
||||
}
|
||||
}
|
||||
|
||||
return this.expandLevels(newLevels, i - 1);
|
||||
}
|
||||
|
||||
private compressLevels(levels: Levels, targetLvl: number): Levels {
|
||||
const keys = Object.keys(levels)
|
||||
.map(Number)
|
||||
.sort((a, b) => a - b);
|
||||
const lvlCnt = keys.length;
|
||||
|
||||
if (targetLvl <= 0) {
|
||||
throw new Error("Target levels amount must be a positive integer");
|
||||
}
|
||||
if (lvlCnt === 0) {
|
||||
return {};
|
||||
}
|
||||
if (targetLvl === 1) {
|
||||
return { 1: levels[keys[0]] };
|
||||
}
|
||||
if (lvlCnt < targetLvl) {
|
||||
throw new Error(
|
||||
"Target levels amount must be less than or equal to the current levels amount",
|
||||
);
|
||||
}
|
||||
|
||||
// Linear resample
|
||||
const compressedLevels: Levels = {};
|
||||
|
||||
for (let targetIndex = 0; targetIndex < targetLvl; ++targetIndex) {
|
||||
const sourcePos = (targetIndex * (lvlCnt - 1)) / (targetLvl - 1);
|
||||
|
||||
const sourceIndexLower = Math.floor(sourcePos);
|
||||
const sourceIndexUpper = Math.min(sourceIndexLower + 1, lvlCnt - 1);
|
||||
|
||||
const interpolationWeight = sourcePos - sourceIndexLower;
|
||||
|
||||
const lowerValue = levels[keys[sourceIndexLower]];
|
||||
const upperValue = levels[keys[sourceIndexUpper]];
|
||||
|
||||
compressedLevels[targetIndex + 1] =
|
||||
lowerValue + (upperValue - lowerValue) * interpolationWeight;
|
||||
}
|
||||
|
||||
return compressedLevels;
|
||||
}
|
||||
|
||||
private interpolateLevels(
|
||||
lower: Levels,
|
||||
upper: Levels,
|
||||
ratio: number,
|
||||
): Levels {
|
||||
if (Object.keys(lower).length !== Object.keys(upper).length)
|
||||
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 interpolatedLevels: Levels = {};
|
||||
for (const lvl in lower) {
|
||||
interpolatedLevels[lvl] = lerp(lvl);
|
||||
}
|
||||
|
||||
return interpolatedLevels;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
import { type Metrics } from ".";
|
||||
import avgWeightDataRaw from "./data/avg-weights.json";
|
||||
|
||||
// -------------------------------------------------------------------------------------------------
|
||||
// DATA MODELS
|
||||
// -------------------------------------------------------------------------------------------------
|
||||
|
||||
export type Player = {
|
||||
name?: string;
|
||||
metrics: Metrics;
|
||||
};
|
||||
|
||||
export enum Attribute {
|
||||
Strength = "Strength",
|
||||
Power = "Power",
|
||||
Endurance = "Endurance",
|
||||
Agility = "Agility",
|
||||
}
|
||||
|
||||
export enum Activity {
|
||||
BackSquat = "BackSquat",
|
||||
Deadlift = "Deadlift",
|
||||
BenchPress = "BenchPress",
|
||||
Run = "Run",
|
||||
BroadJump = "BroadJump",
|
||||
ConeDrill = "ConeDrill",
|
||||
}
|
||||
|
||||
export interface ActivityPerformance {
|
||||
activity: Activity;
|
||||
performance: number;
|
||||
}
|
||||
|
||||
export enum Gender {
|
||||
Male = "Male",
|
||||
Female = "Female",
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------------------------------
|
||||
// Conversion utilities
|
||||
// -------------------------------------------------------------------------------------------------
|
||||
|
||||
export type StandardUnit = "ms" | "cm" | "kg";
|
||||
|
||||
export const lbToKg = (lb: number | string): number => +lb * 0.453592;
|
||||
export const kgToLb = (kg: number | string): number => +kg * 2.20462;
|
||||
|
||||
export const minToMs = (min: number) => min * 60000;
|
||||
export const secToMs = (sec: number) => sec * 1000;
|
||||
export const msToMin = (ms: number) => ms / 60000;
|
||||
|
||||
export const ftToCm = (ft: number) => ft * 30.48;
|
||||
export const inToCm = (inches: number) => inches * 2.54;
|
||||
export const cmToIn = (cm: number) => cm / 2.54;
|
||||
|
||||
export const msToTime = (ms: number, includeMs = false): string => {
|
||||
const minutes = Math.floor(ms / 60000);
|
||||
const seconds = Math.floor((ms % 60000) / 1000);
|
||||
const milliseconds = Math.floor(ms % 1000);
|
||||
|
||||
let formattedTime = `${minutes}:${seconds.toString().padStart(2, "0")}`;
|
||||
if (includeMs) {
|
||||
formattedTime += `.${milliseconds.toString().padStart(3, "0")}`;
|
||||
}
|
||||
return formattedTime;
|
||||
};
|
||||
|
||||
export const range = (length: number) =>
|
||||
Array.from({ length: length }, (_, i) => i);
|
||||
|
||||
export const clamp = (x: number, lo: number, hi: number) =>
|
||||
Math.max(lo, Math.min(hi, x));
|
||||
|
||||
export const getAvgWeight = (gender: Gender, age: number) => {
|
||||
type AvgWeightData = {
|
||||
metadata: {
|
||||
unit: StandardUnit;
|
||||
};
|
||||
weights: Metrics[];
|
||||
};
|
||||
let avgWeightData = avgWeightDataRaw as AvgWeightData;
|
||||
|
||||
const avgWeights = avgWeightData.weights.filter((a) => a.gender === gender);
|
||||
const firstAvgWeight = avgWeights[0];
|
||||
if (!firstAvgWeight) throw new Error("No average weights");
|
||||
|
||||
const closest = {
|
||||
diff: Math.abs(firstAvgWeight.age - age),
|
||||
avg: firstAvgWeight,
|
||||
};
|
||||
for (const avg of avgWeights) {
|
||||
const diff = Math.abs(avg.age - age);
|
||||
if (diff < closest.diff) {
|
||||
closest.diff = diff;
|
||||
closest.avg = avg;
|
||||
}
|
||||
}
|
||||
|
||||
return closest.avg.weight;
|
||||
};
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"noUncheckedIndexedAccess": false,
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"name": "@blade-and-brawn/commerce",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"exports": {
|
||||
".": "./src/index.ts"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
export * from "./printful"
|
||||
export * from "./sync"
|
||||
export * from "./webflow"
|
||||
export * from "./webflow"
|
||||
export * from "./util/misc"
|
||||
export * from "./util/types"
|
||||
@@ -0,0 +1,207 @@
|
||||
import type { Printful } from "./util/types";
|
||||
import { FetchError, type DeepPartial } from "./util/misc";
|
||||
|
||||
export 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}`,
|
||||
{
|
||||
method: "GET",
|
||||
headers: { ...env().AUTH_HEADERS },
|
||||
},
|
||||
);
|
||||
|
||||
if (res.status === 404 || res.status === 400) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!res.ok) {
|
||||
throw new FetchError("Failed to get Printful variant", res);
|
||||
}
|
||||
|
||||
const payload =
|
||||
(await res.json()) as Printful.Products.MetaDataSingle<Printful.Products.SyncVariant>;
|
||||
return payload.result;
|
||||
}
|
||||
};
|
||||
|
||||
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}`,
|
||||
{
|
||||
method: "GET",
|
||||
headers: { ...env().AUTH_HEADERS },
|
||||
},
|
||||
);
|
||||
|
||||
if (!res.ok) {
|
||||
throw new FetchError(
|
||||
"Failed to get all Printful products",
|
||||
res,
|
||||
);
|
||||
}
|
||||
|
||||
const payload =
|
||||
(await res.json()) as Printful.Products.MetaDataMulti<Printful.Products.SyncProduct>;
|
||||
|
||||
allSyncProducts.push(...payload.result);
|
||||
offset = allSyncProducts.length;
|
||||
|
||||
if (allSyncProducts.length >= payload.paging.total) break;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
return allSyncProducts;
|
||||
}
|
||||
|
||||
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 },
|
||||
},
|
||||
);
|
||||
|
||||
if (res.status === 404 || res.status === 400) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!res.ok) {
|
||||
throw new FetchError("Failed to get Printful product", res);
|
||||
}
|
||||
|
||||
const payload =
|
||||
(await res.json()) as Printful.Products.MetaDataSingle<Printful.Products.Product>;
|
||||
return payload.result;
|
||||
}
|
||||
|
||||
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,
|
||||
},
|
||||
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) {
|
||||
const res = await fetch(`${env().API_URL}/orders`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
...env().AUTH_HEADERS,
|
||||
},
|
||||
body: JSON.stringify(printfulOrder),
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
throw new FetchError("Failed to create printful order", res);
|
||||
}
|
||||
}
|
||||
|
||||
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}`,
|
||||
{
|
||||
method: "GET",
|
||||
headers: { ...env().AUTH_HEADERS },
|
||||
},
|
||||
);
|
||||
|
||||
if (!res.ok) {
|
||||
throw new FetchError(
|
||||
"Failed to get all Printful orders",
|
||||
res,
|
||||
);
|
||||
}
|
||||
|
||||
const payload =
|
||||
(await res.json()) as Printful.Products.MetaDataMulti<Printful.Orders.Order>;
|
||||
|
||||
allPrintfulOrders.push(...payload.result);
|
||||
offset = allPrintfulOrders.length;
|
||||
|
||||
if (allPrintfulOrders.length >= payload.paging.total) break;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
return allPrintfulOrders;
|
||||
}
|
||||
};
|
||||
|
||||
static Util = class {
|
||||
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.",
|
||||
);
|
||||
}
|
||||
|
||||
const vars = {
|
||||
AUTH_TOKEN: Bun.env.PRINTFUL_AUTH,
|
||||
STORE_ID: Bun.env.PRINTFUL_STORE_ID,
|
||||
} as Record<string, string>;
|
||||
|
||||
for (const varKey in vars) {
|
||||
if (!vars[varKey]) {
|
||||
console.log(`Missing ${varKey} environment variable`);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
...vars,
|
||||
API_URL: "https://api.printful.com",
|
||||
AUTH_HEADERS: {
|
||||
Authorization: `Bearer ${vars.AUTH_TOKEN}`,
|
||||
"X-PF-Store-Id": `${vars.STORE_ID}`,
|
||||
},
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,359 @@
|
||||
import type { Printful, Webflow } from "./util/types";
|
||||
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));
|
||||
|
||||
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>;
|
||||
variants: {
|
||||
color: string;
|
||||
product: Printful.Products.Product;
|
||||
}[];
|
||||
};
|
||||
|
||||
export class SyncService {
|
||||
static state = {
|
||||
isSyncing: false,
|
||||
syncingIds: [] as number[],
|
||||
};
|
||||
|
||||
static async sync(printfulProductId: number | undefined) {
|
||||
this.state.isSyncing = true;
|
||||
|
||||
const mainPrintfulProducts: MainProduct[] = [];
|
||||
const webflowProducts: Webflow.Products.ProductAndSkus[] = [];
|
||||
try {
|
||||
let printfulProducts = await PrintfulService.Products.getAll();
|
||||
if (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),
|
||||
);
|
||||
}
|
||||
}
|
||||
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,
|
||||
);
|
||||
|
||||
let mainPrintfulProduct = mainPrintfulProducts.find((mp) =>
|
||||
mp.name.includes(mainProductName),
|
||||
);
|
||||
if (!mainPrintfulProduct) {
|
||||
mainPrintfulProduct = {
|
||||
name: mainProductName,
|
||||
variants: [],
|
||||
externalId: printfulProduct.external_id,
|
||||
colors: new Set(),
|
||||
};
|
||||
mainPrintfulProducts.push(mainPrintfulProduct);
|
||||
}
|
||||
|
||||
const productColor = this.findColorInProductName(
|
||||
printfulProduct.name,
|
||||
);
|
||||
mainPrintfulProduct.variants.push({
|
||||
color: productColor,
|
||||
product: (await PrintfulService.Products.get(
|
||||
printfulProduct.id,
|
||||
))!,
|
||||
});
|
||||
mainPrintfulProduct.colors.add(productColor);
|
||||
}
|
||||
} catch (error) {
|
||||
throw error;
|
||||
} finally {
|
||||
this.state.isSyncing = false;
|
||||
this.state.syncingIds = [];
|
||||
}
|
||||
|
||||
// TODO: Validate main printful products
|
||||
|
||||
// Sync the main printful products
|
||||
console.log("Syncing main products...");
|
||||
for (const mainPrintfulProduct of mainPrintfulProducts) {
|
||||
try {
|
||||
this.state.isSyncing = true;
|
||||
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>[] =
|
||||
[];
|
||||
|
||||
for (const specialVariant of mainPrintfulProduct.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,
|
||||
);
|
||||
if (!sizes.includes(printfulVariant.size))
|
||||
sizeIsInAllVariants = false;
|
||||
}
|
||||
|
||||
if (sizeIsInAllVariants) {
|
||||
foundSizes.add(printfulVariant.size);
|
||||
|
||||
webflowSkus.push({
|
||||
id: printfulVariant.external_id,
|
||||
fieldData: {
|
||||
name: printfulVariant.name,
|
||||
slug: formatSlug(printfulVariant.name),
|
||||
"sku-values": {
|
||||
color: specialVariant.color,
|
||||
size: printfulVariant.size,
|
||||
},
|
||||
price: {
|
||||
value: Math.floor(
|
||||
+printfulVariant.retail_price * 100,
|
||||
),
|
||||
unit: printfulVariant.currency,
|
||||
currency: printfulVariant.currency,
|
||||
},
|
||||
"main-image":
|
||||
PrintfulService.Util.getVariantMainImage(
|
||||
printfulVariant,
|
||||
),
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// SYNC PRODUCT DATA
|
||||
let existingWebflowProduct = webflowProducts.find(
|
||||
(webflowProduct) =>
|
||||
webflowProduct.product.id ===
|
||||
mainPrintfulProduct.externalId.split("-")[0],
|
||||
);
|
||||
if (existingWebflowProduct) {
|
||||
// SYNC PRODUCT UPDATE
|
||||
|
||||
// do not update images
|
||||
for (const webflowSku of webflowSkus) {
|
||||
delete webflowSku.fieldData?.["main-image"];
|
||||
}
|
||||
|
||||
const webflowProductId =
|
||||
mainPrintfulProduct.externalId.split("-")[0];
|
||||
if (!webflowProductId) {
|
||||
throw new Error("Malformed printful product ID");
|
||||
}
|
||||
WebflowService.Products.update(webflowProductId, {
|
||||
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: "size",
|
||||
name: "Size",
|
||||
enum: Array.from(foundSizes).map(
|
||||
(size) => ({
|
||||
id: size,
|
||||
slug: formatSlug(size),
|
||||
name: size,
|
||||
}),
|
||||
),
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
sku: webflowSkus[0],
|
||||
});
|
||||
|
||||
for (const webflowSku of webflowSkus.slice(1)) {
|
||||
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],
|
||||
);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// SYNC PRODUCT CREATE
|
||||
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: "size",
|
||||
name: "Size",
|
||||
enum: Array.from(foundSizes).map(
|
||||
(size) => ({
|
||||
id: size,
|
||||
slug: formatSlug(size),
|
||||
name: size,
|
||||
}),
|
||||
),
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
sku: webflowSkus[0],
|
||||
});
|
||||
|
||||
// create webflow product SKUs
|
||||
await WebflowService.Products.Skus.create(
|
||||
webflowProductId,
|
||||
webflowSkus.slice(1),
|
||||
);
|
||||
|
||||
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,
|
||||
);
|
||||
if (associatedWebflowSku) {
|
||||
newPrintfulVariants.push({
|
||||
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}`,
|
||||
},
|
||||
sync_variants: newPrintfulVariants,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
// SYNC IMAGES
|
||||
// for (const sku of existingWebflowProduct.skus) {
|
||||
// const skuColor = sku.fieldData["sku-values"]?.["color"]?.toLowerCase() ?? "None";
|
||||
// const skuSlug = `${existingWebflowProduct.product.fieldData.slug}-${skuColor}`;
|
||||
// const skuImageUrls = await this.getProductImageUrls(skuSlug);
|
||||
//
|
||||
// sku.fieldData["main-image"] = skuImageUrls[0] ?? sku.fieldData["main-image"];
|
||||
// sku.fieldData["more-images"] = skuImageUrls.slice(1).map(url => ({ url }));
|
||||
//
|
||||
// await WebflowService.Products.Skus.update(existingWebflowProduct.product.id, sku.id, sku);
|
||||
// }
|
||||
//
|
||||
if (err instanceof FetchError) {
|
||||
console.error(err.message, err.payload);
|
||||
}
|
||||
}
|
||||
}
|
||||
this.state.isSyncing = false;
|
||||
this.state.syncingIds = [];
|
||||
console.log("Done");
|
||||
}
|
||||
|
||||
private static getProductImageUrls = async (slug: string) => {
|
||||
const res = await fetch(`${R2_WORKER_URL}/product-images/${slug}`);
|
||||
if (!res.ok) {
|
||||
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()) as string[];
|
||||
return productImageKeys.map((key) => `${WEBSITE_MEDIA_URL}/${key}`);
|
||||
};
|
||||
|
||||
public static findColorInProductName = (productName: string): string => {
|
||||
const m = productName.match(/\[([^\]]+)\]/);
|
||||
return m?.[1] ? m[1] : "N/A";
|
||||
};
|
||||
|
||||
public static getMainProductName = (productName: string) => {
|
||||
const colorInName = this.findColorInProductName(productName);
|
||||
if (colorInName) {
|
||||
return productName.replace(`[${colorInName}]`, "").trimEnd();
|
||||
} else {
|
||||
return productName;
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -27,7 +27,7 @@ export class FetchError extends Error {
|
||||
|
||||
async parse() {
|
||||
try {
|
||||
this.payload = await this.response.clone().json();
|
||||
this.payload = (await this.response.clone().json()) as Object;
|
||||
} catch {
|
||||
this.payload = await this.response.text().catch(() => undefined);
|
||||
} finally {
|
||||
@@ -1,5 +1,3 @@
|
||||
import type { DeepPartial } from "./misc";
|
||||
|
||||
export namespace Printful {
|
||||
export namespace Products {
|
||||
export interface MetaDataSingle<T = any> {
|
||||
@@ -125,56 +123,53 @@ export namespace Printful {
|
||||
PackageShipped = "package_shipped",
|
||||
}
|
||||
|
||||
export interface ProductUpdated
|
||||
extends MetaData<
|
||||
Event.ProductUpdated,
|
||||
{
|
||||
sync_product: {
|
||||
id: number;
|
||||
external_id: string;
|
||||
name: string;
|
||||
variants: number;
|
||||
synced: number;
|
||||
thumbnail_url: string;
|
||||
is_ignored: boolean;
|
||||
};
|
||||
}
|
||||
> {}
|
||||
export interface ProductUpdated extends MetaData<
|
||||
Event.ProductUpdated,
|
||||
{
|
||||
sync_product: {
|
||||
id: number;
|
||||
external_id: string;
|
||||
name: string;
|
||||
variants: number;
|
||||
synced: number;
|
||||
thumbnail_url: string;
|
||||
is_ignored: boolean;
|
||||
};
|
||||
}
|
||||
> {}
|
||||
|
||||
export interface ProductDeleted
|
||||
extends MetaData<
|
||||
Event.ProductDeleted,
|
||||
{
|
||||
sync_product: {
|
||||
id: number;
|
||||
external_id: string;
|
||||
name: string;
|
||||
};
|
||||
}
|
||||
> {}
|
||||
export interface ProductDeleted extends MetaData<
|
||||
Event.ProductDeleted,
|
||||
{
|
||||
sync_product: {
|
||||
id: number;
|
||||
external_id: string;
|
||||
name: string;
|
||||
};
|
||||
}
|
||||
> {}
|
||||
|
||||
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;
|
||||
};
|
||||
order: {
|
||||
id: number;
|
||||
external_id: string;
|
||||
status: string;
|
||||
};
|
||||
}
|
||||
> {}
|
||||
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;
|
||||
};
|
||||
order: {
|
||||
id: number;
|
||||
external_id: string;
|
||||
status: string;
|
||||
};
|
||||
}
|
||||
> {}
|
||||
|
||||
export type EventPayload =
|
||||
| ProductUpdated
|
||||
@@ -422,10 +417,14 @@ export namespace Webflow {
|
||||
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;
|
||||
}
|
||||
@@ -0,0 +1,330 @@
|
||||
import type { Webflow } from "./util/types";
|
||||
import { FetchError, type DeepPartial } from "./util/misc";
|
||||
import crypto from "node:crypto";
|
||||
|
||||
export class WebflowService {
|
||||
static Products = class {
|
||||
static Skus = class {
|
||||
static async create(
|
||||
webflowProductId: string,
|
||||
skus: DeepPartial<Webflow.Products.Skus.Sku>[],
|
||||
): Promise<string[]> {
|
||||
const res = await fetch(
|
||||
`${env().API_SITES_URL}/products/${webflowProductId}/skus`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
...env().AUTH_HEADER,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
skus: skus,
|
||||
}),
|
||||
},
|
||||
);
|
||||
|
||||
if (!res.ok) {
|
||||
throw new FetchError(
|
||||
"Failed to create Webflow product SKU",
|
||||
res,
|
||||
);
|
||||
}
|
||||
|
||||
const payload = (await res.json()) as {
|
||||
skus: { id: string }[];
|
||||
};
|
||||
return payload.skus.map((sku) => sku.id);
|
||||
}
|
||||
|
||||
static async update(
|
||||
webflowProductId: string,
|
||||
webflowSkuId: string,
|
||||
webflowSku: DeepPartial<Webflow.Products.Skus.Sku>,
|
||||
): Promise<void> {
|
||||
const res = await fetch(
|
||||
`${env().API_SITES_URL}/products/${webflowProductId}/skus/${webflowSkuId}`,
|
||||
{
|
||||
method: "PATCH",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
...env().AUTH_HEADER,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
sku: webflowSku,
|
||||
}),
|
||||
},
|
||||
);
|
||||
if (!res.ok) {
|
||||
throw new FetchError(
|
||||
"Failed to update Webflow product SKU",
|
||||
res,
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
static async create(
|
||||
webflowProductAndSku: DeepPartial<Webflow.Products.ProductAndSku>,
|
||||
): Promise<string> {
|
||||
const res = await fetch(`${env().API_SITES_URL}/products`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
...env().AUTH_HEADER,
|
||||
},
|
||||
body: JSON.stringify(webflowProductAndSku),
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
throw new FetchError("Failed to create Webflow product", res);
|
||||
}
|
||||
|
||||
const createdWebflowProduct = (await res.json()) as {
|
||||
product: { id: string };
|
||||
};
|
||||
return createdWebflowProduct.product.id;
|
||||
}
|
||||
|
||||
static async getAll(): Promise<Webflow.Products.ProductAndSkus[]> {
|
||||
const res = await fetch(`${env().API_SITES_URL}/products`, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
...env().AUTH_HEADER,
|
||||
},
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
throw new FetchError("Failed to get all Webflow products", res);
|
||||
}
|
||||
|
||||
const payload = (await res.json()) as {
|
||||
items: Webflow.Products.ProductAndSkus[];
|
||||
};
|
||||
return payload.items;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
if (!res.ok) {
|
||||
throw new FetchError("Failed to get Webflow product", res);
|
||||
}
|
||||
|
||||
const payload =
|
||||
(await res.json()) as Webflow.Products.ProductAndSkus;
|
||||
return payload;
|
||||
}
|
||||
|
||||
static async update(
|
||||
webflowProductId: string,
|
||||
webflowProduct: DeepPartial<Webflow.Products.ProductAndSku>,
|
||||
): Promise<void> {
|
||||
const res = await fetch(
|
||||
`${env().API_SITES_URL}/products/${webflowProductId}`,
|
||||
{
|
||||
method: "PATCH",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
...env().AUTH_HEADER,
|
||||
},
|
||||
body: JSON.stringify(webflowProduct),
|
||||
},
|
||||
);
|
||||
|
||||
if (!res.ok) {
|
||||
throw new FetchError("Failed to update Webflow product", res);
|
||||
}
|
||||
}
|
||||
|
||||
static async remove(webflowProductId: string): Promise<void> {
|
||||
const res = await fetch(
|
||||
`${env().API_COLLECTIONS_URL}/items/${webflowProductId}`,
|
||||
{
|
||||
method: "DELETE",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
...env().AUTH_HEADER,
|
||||
},
|
||||
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[]> {
|
||||
// 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()}`,
|
||||
{
|
||||
method: "GET",
|
||||
headers: { ...env().AUTH_HEADER },
|
||||
},
|
||||
);
|
||||
|
||||
if (!res.ok) {
|
||||
throw new FetchError("Failed to get all Webflow orders", res);
|
||||
}
|
||||
|
||||
const payload = (await res.json()) as {
|
||||
orders: Webflow.Orders.Order[];
|
||||
};
|
||||
return payload.orders;
|
||||
}
|
||||
|
||||
static async update(
|
||||
webflowOrderId: string,
|
||||
webflowOrderUpdate: {
|
||||
comment?: string;
|
||||
shippingProvider?: string;
|
||||
shippingTracking?: string;
|
||||
shippingTrackingURL: string;
|
||||
},
|
||||
): Promise<void> {
|
||||
const res = await fetch(
|
||||
`${env().API_SITES_URL}/orders/${webflowOrderId}`,
|
||||
{
|
||||
method: "PATCH",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
...env().AUTH_HEADER,
|
||||
},
|
||||
body: JSON.stringify(webflowOrderUpdate),
|
||||
},
|
||||
);
|
||||
|
||||
if (!res.ok) {
|
||||
throw new FetchError("Failed to update Webflow order", res);
|
||||
}
|
||||
}
|
||||
|
||||
static async fulfill(
|
||||
webflowOrderId: string,
|
||||
opt: { sendOrderFulfilledEmail?: boolean } = {},
|
||||
): Promise<void> {
|
||||
const res = await fetch(
|
||||
`${env().API_SITES_URL}/orders/${webflowOrderId}/fulfill`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
...env().AUTH_HEADER,
|
||||
},
|
||||
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,
|
||||
): Boolean {
|
||||
try {
|
||||
const timestamp = request.headers.get("x-webflow-timestamp");
|
||||
if (!timestamp) {
|
||||
throw new Error("No timestamp provided");
|
||||
}
|
||||
const providedSignature = request.headers.get(
|
||||
"x-webflow-signature",
|
||||
);
|
||||
if (!providedSignature) {
|
||||
throw new Error("No signature provided");
|
||||
}
|
||||
|
||||
const secret = env().WEBHOOK_SECRET;
|
||||
if (!secret) {
|
||||
throw new Error("No secret provided");
|
||||
}
|
||||
|
||||
if (!body) {
|
||||
throw new Error("Body is empty");
|
||||
}
|
||||
|
||||
const requestTimestamp = parseInt(timestamp, 10);
|
||||
const data = `${requestTimestamp}:${JSON.stringify(body)}`;
|
||||
const hash = crypto
|
||||
.createHmac("sha256", secret)
|
||||
.update(data)
|
||||
.digest("hex");
|
||||
|
||||
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");
|
||||
}
|
||||
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.",
|
||||
);
|
||||
}
|
||||
|
||||
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,
|
||||
} as Record<string, string>;
|
||||
|
||||
for (const varKey in vars) {
|
||||
if (!vars[varKey]) {
|
||||
console.log(`Missing ${varKey} environment variable`);
|
||||
}
|
||||
}
|
||||
|
||||
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}` },
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
import { treaty } from "@elysiajs/eden";
|
||||
import type { App } from "./server/types";
|
||||
|
||||
const baseURL = import.meta.env.PROD
|
||||
? "https://blade-and-brawn.fly.dev"
|
||||
: "http://localhost:5173";
|
||||
|
||||
export const api = treaty<App>(baseURL).api;
|
||||
@@ -1,297 +0,0 @@
|
||||
<script lang="ts">
|
||||
import {
|
||||
LevelCalculator,
|
||||
type LevelCalculatorOutput,
|
||||
type Standards,
|
||||
} from "$lib/services/calculator/main";
|
||||
import {
|
||||
Activity,
|
||||
Attribute,
|
||||
Gender,
|
||||
lbToKg,
|
||||
range,
|
||||
type ActivityPerformance,
|
||||
type Player,
|
||||
} from "$lib/services/calculator/util";
|
||||
|
||||
interface CalcData {
|
||||
levels?: LevelCalculatorOutput;
|
||||
player: Player;
|
||||
activityPerformances: ActivityPerformance[];
|
||||
}
|
||||
|
||||
interface Props {
|
||||
allStandards: Standards;
|
||||
}
|
||||
|
||||
const { allStandards }: Props = $props();
|
||||
|
||||
const levelCalculator = $derived(new LevelCalculator(allStandards));
|
||||
|
||||
let calculations = $state([] as CalcData[]);
|
||||
|
||||
$effect(() => {
|
||||
for (const calculation of calculations) {
|
||||
calculation.levels = levelCalculator.calculate(
|
||||
calculation.player,
|
||||
calculation.activityPerformances,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
$effect(() => {
|
||||
const savedCalcs = localStorage.getItem("calculations");
|
||||
if (savedCalcs) calculations = JSON.parse(savedCalcs);
|
||||
});
|
||||
|
||||
$effect(() => {
|
||||
localStorage.setItem("calculations", JSON.stringify(calculations));
|
||||
});
|
||||
|
||||
$inspect(calculations[0]);
|
||||
|
||||
const createCalculation = function (name: string) {
|
||||
return {
|
||||
player: {
|
||||
name: name,
|
||||
metrics: {
|
||||
age: 18,
|
||||
weight: lbToKg(180),
|
||||
gender: Gender.Male,
|
||||
},
|
||||
},
|
||||
activityPerformances: Object.values(Activity).map((a) => ({
|
||||
activity: a,
|
||||
performance: 0,
|
||||
})),
|
||||
};
|
||||
};
|
||||
|
||||
function downloadObject(obj: unknown, filename = "data.json") {
|
||||
const json = JSON.stringify(obj, null, 2);
|
||||
const blob = new Blob([json], { type: "application/json" });
|
||||
const url = URL.createObjectURL(blob);
|
||||
|
||||
const a = document.createElement("a");
|
||||
a.href = url;
|
||||
a.download = filename;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
|
||||
a.remove();
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
|
||||
const formatMs = (ms: number) => {
|
||||
const total = Math.floor(ms / 1000);
|
||||
const m = Math.floor(total / 60);
|
||||
const s = total % 60;
|
||||
return `${m}m ${s}s`;
|
||||
};
|
||||
const inchesToFeetInches = (inches: number) => {
|
||||
const ft = Math.floor(inches / 12);
|
||||
const rem = inches - ft * 12;
|
||||
const whole = Math.floor(rem);
|
||||
const isHalf = Math.abs(rem - whole - 0.5) < 1e-9;
|
||||
const inchLabel = isHalf ? `${whole}½` : `${whole}`;
|
||||
return `${ft}' ${inchLabel}"`;
|
||||
};
|
||||
|
||||
const formatSeconds = (ms: number) => (ms / 1000).toFixed(2) + " seconds";
|
||||
|
||||
const performanceOptionsFromActivity = function (activity: Activity) {
|
||||
const options: { name: string; value: string }[] = [];
|
||||
switch (activity) {
|
||||
case Activity.BackSquat:
|
||||
case Activity.Deadlift:
|
||||
case Activity.BenchPress:
|
||||
for (let i = 0; i < 600; ++i) {
|
||||
options.push({
|
||||
name: String(i) + " lb",
|
||||
value: String(lbToKg(i)),
|
||||
});
|
||||
}
|
||||
break;
|
||||
|
||||
case Activity.Run: {
|
||||
const MAX_MIN = 30;
|
||||
for (let ms = 0; ms <= MAX_MIN * 60_000; ms += 1_000) {
|
||||
options.push({ name: formatMs(ms), value: String(ms) });
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case Activity.BroadJump: {
|
||||
const MAX_INCHES = 15 * 12 + 5;
|
||||
for (let halfStep = 0; halfStep <= MAX_INCHES * 2; halfStep++) {
|
||||
const inches = halfStep / 2;
|
||||
options.push({
|
||||
name: inchesToFeetInches(inches),
|
||||
value: (inches * 2.54).toFixed(1),
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case Activity.ConeDrill: {
|
||||
const MIN_MS = 5_000;
|
||||
const MAX_MS = 20_000;
|
||||
for (let ms = MIN_MS; ms <= MAX_MS; ms += 10) {
|
||||
options.push({
|
||||
name: formatSeconds(ms),
|
||||
value: String(ms),
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
return options;
|
||||
};
|
||||
</script>
|
||||
|
||||
<div class="mt-5 mb-5 flex w-full">
|
||||
<div class="ml-auto">
|
||||
<button
|
||||
onclick={() =>
|
||||
calculations.push(
|
||||
createCalculation(String(calculations.length)),
|
||||
)}
|
||||
class="btn btn-primary">New</button
|
||||
>
|
||||
<button
|
||||
onclick={() => downloadObject(calculations)}
|
||||
class="btn btn-secondary">Export</button
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<section
|
||||
class="w-full px-3 grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4"
|
||||
>
|
||||
{#each calculations as calculation, index}
|
||||
<div
|
||||
class="card card-compact bg-base-200 p-4 shadow-lg max-w-sm w-full"
|
||||
>
|
||||
<div class="card-body gap-4 p-4">
|
||||
<input
|
||||
class="input input-bordered input-sm w-full"
|
||||
type="text"
|
||||
bind:value={calculation.player.name}
|
||||
placeholder="Player name"
|
||||
/>
|
||||
|
||||
<ul
|
||||
class="bg-base-100 rounded-box shadow-xs divide-y divide-base-300"
|
||||
>
|
||||
<li class="p-3">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="font-semibold text-sm">OVERALL</div>
|
||||
<div class="badge badge-neutral badge-xs">
|
||||
{calculation?.levels?.player || "N/A"}
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
|
||||
{#each Object.values(Attribute) as attribute}
|
||||
<li class="p-3">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="opacity-80 text-sm">
|
||||
{attribute}
|
||||
</div>
|
||||
<div class="badge badge-ghost badge-xs">
|
||||
{calculation?.levels?.attributes?.[
|
||||
attribute
|
||||
] || "N/A"}
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
|
||||
<fieldset
|
||||
class="fieldset bg-base-200/60 rounded-lg"
|
||||
onchange={() =>
|
||||
(calculation.levels = levelCalculator.calculate(
|
||||
calculation.player,
|
||||
calculation.activityPerformances,
|
||||
))}
|
||||
>
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-3">
|
||||
<label class="form-control">
|
||||
<span class="label mb-1 text-xs">Gender</span>
|
||||
<select
|
||||
class="select select-bordered select-sm w-full"
|
||||
bind:value={calculation.player.metrics.gender}
|
||||
>
|
||||
{#each Object.values(Gender) as gender}
|
||||
<option value={gender}>{gender}</option>
|
||||
{/each}
|
||||
</select>
|
||||
</label>
|
||||
|
||||
<label class="form-control">
|
||||
<span class="label mb-1 text-xs">Age</span>
|
||||
<input
|
||||
class="input input-bordered input-sm w-full"
|
||||
type="number"
|
||||
min="1"
|
||||
max="100"
|
||||
step="1"
|
||||
bind:value={calculation.player.metrics.age}
|
||||
/>
|
||||
</label>
|
||||
|
||||
<label class="form-control">
|
||||
<span class="label mb-1 text-xs">Weight</span>
|
||||
<select
|
||||
class="select select-bordered select-sm w-full"
|
||||
bind:value={calculation.player.metrics.weight}
|
||||
>
|
||||
{#each range(400) as weight}
|
||||
<option value={String(lbToKg(weight))}
|
||||
>{weight}</option
|
||||
>
|
||||
{/each}
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
<fieldset
|
||||
class="fieldset bg-base-200/60 rounded-lg"
|
||||
onchange={() =>
|
||||
(calculation.levels = levelCalculator.calculate(
|
||||
calculation.player,
|
||||
calculation.activityPerformances,
|
||||
))}
|
||||
>
|
||||
<div class="grid grid-cols-2 gap-3">
|
||||
{#each calculation.activityPerformances as activityPerformance}
|
||||
<label class="form-control space-y-1">
|
||||
<span class="label mb-1 text-xs">
|
||||
{activityPerformance.activity}
|
||||
</span>
|
||||
<select
|
||||
class="select select-bordered select-sm w-full"
|
||||
bind:value={activityPerformance.performance}
|
||||
>
|
||||
{#each performanceOptionsFromActivity(activityPerformance.activity) as option}
|
||||
<option value={option.value}
|
||||
>{option.name}</option
|
||||
>
|
||||
{/each}
|
||||
</select>
|
||||
</label>
|
||||
{/each}
|
||||
</div>
|
||||
</fieldset>
|
||||
</div>
|
||||
<button
|
||||
onclick={() =>
|
||||
confirm(`Delete ${calculation.player.name}?`) &&
|
||||
calculations.splice(index, 1)}
|
||||
class="btn btn-error btn-sm w-1/4">Delete</button
|
||||
>
|
||||
</div>
|
||||
{/each}
|
||||
</section>
|
||||
@@ -1,92 +0,0 @@
|
||||
import {
|
||||
type ActivityStandards,
|
||||
type Levels,
|
||||
} from "$lib/services/calculator/main";
|
||||
import { Activity, clamp, Gender, range } from "$lib/services/calculator/util";
|
||||
import allStandards from "./standards.json";
|
||||
import { levenbergMarquardt as LM } from "ml-levenberg-marquardt";
|
||||
|
||||
const activityStandards = allStandards as ActivityStandards;
|
||||
|
||||
for (const activity of Object.values(Activity)) {
|
||||
const DATASET_MAX_LEVEL = 5;
|
||||
const NEW_LEVEL_COUNT = 2;
|
||||
const RATIO_CLAMP = 1.4;
|
||||
|
||||
function expDecayModel([A, B, C]: number[]) {
|
||||
return (i: number) => A * Math.exp(-B * i) + C;
|
||||
}
|
||||
|
||||
for (const gender of Object.values(Gender)) {
|
||||
const standardsByGender = activityStandards[activity].standards.filter(
|
||||
(s) => s["metrics"].gender === gender,
|
||||
);
|
||||
const ages = [...new Set(standardsByGender.map((s) => s.metrics.age))];
|
||||
for (const age of ages) {
|
||||
const standards = standardsByGender.filter(
|
||||
(s) => s.metrics.age === age,
|
||||
);
|
||||
|
||||
const data = {
|
||||
x: [] as number[],
|
||||
y: [] as number[],
|
||||
};
|
||||
|
||||
const isIncreasing =
|
||||
standards[0].levels["2"] > standards[0].levels["1"];
|
||||
|
||||
for (const i of range(DATASET_MAX_LEVEL).slice(0, -1)) {
|
||||
const level = i + 1;
|
||||
const progressionRatios: number[] = [];
|
||||
for (const standard of standards) {
|
||||
const curr = standard.levels[level];
|
||||
const next = standard.levels[level];
|
||||
progressionRatios.push(
|
||||
isIncreasing ? next / curr : curr / next,
|
||||
);
|
||||
}
|
||||
|
||||
const sum = progressionRatios.reduce((p, c) => p + c, 0);
|
||||
const progressionRatioAvg = sum / progressionRatios.length;
|
||||
|
||||
data.x.push(i);
|
||||
data.y.push(progressionRatioAvg);
|
||||
}
|
||||
|
||||
const fittedParams = LM(data, expDecayModel, {
|
||||
initialValues: [0.4, 0.5, 1.1],
|
||||
minValues: [0.0, 0.0, 1.02],
|
||||
maxValues: [1.0, 2.0, 1.25],
|
||||
maxIterations: 200,
|
||||
});
|
||||
const getDecayRatio = (i: number) =>
|
||||
clamp(
|
||||
expDecayModel(fittedParams.parameterValues)(i),
|
||||
1,
|
||||
RATIO_CLAMP,
|
||||
);
|
||||
|
||||
// UPDATE THE DATA
|
||||
for (const standard of standards) {
|
||||
const newLevels: Levels = {};
|
||||
|
||||
let prev = standard.levels["1"];
|
||||
for (const i of range(NEW_LEVEL_COUNT)) {
|
||||
const level = i + 1;
|
||||
const ratio = getDecayRatio(level - NEW_LEVEL_COUNT);
|
||||
prev = isIncreasing ? prev / ratio : prev * ratio;
|
||||
newLevels[level] = Math.round(prev);
|
||||
}
|
||||
|
||||
const oldLevels = standard.levels as Levels;
|
||||
for (const i of range(DATASET_MAX_LEVEL)) {
|
||||
const level = i + 1;
|
||||
newLevels[level + NEW_LEVEL_COUNT] = oldLevels[level];
|
||||
}
|
||||
standard.levels = newLevels;
|
||||
console.log(newLevels);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Bun.write("./newStandards.json", JSON.stringify(allStandards, null, 2));
|
||||
@@ -1,203 +0,0 @@
|
||||
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 { 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 zipcodesUs from "zipcodes-us";
|
||||
|
||||
const MAIN_STANDARDS = new Standards(rawStandards as ActivityStandards);
|
||||
const levelCalculator = new LevelCalculator(MAIN_STANDARDS);
|
||||
|
||||
export const app = new Elysia({ prefix: "/api" })
|
||||
.use(
|
||||
cors({
|
||||
origin: [
|
||||
// bladeandbrawn.com (apex + any subdomain)
|
||||
/^https?:\/\/([a-z0-9-]+\.)?bladeandbrawn\.com$/i,
|
||||
// Webflow preview domains
|
||||
/^https?:\/\/([a-z0-9-]+\.)?bladeandbrawn\.webflow\.io$/i,
|
||||
// Fly app host (this one already works)
|
||||
/^https?:\/\/blade-and-brawn\.fly\.dev$/i,
|
||||
// Local dev
|
||||
"http://localhost:5173",
|
||||
],
|
||||
}),
|
||||
)
|
||||
|
||||
.error({
|
||||
FetchError,
|
||||
})
|
||||
|
||||
.onError(async ({ code, error }) => {
|
||||
switch (code) {
|
||||
case "FetchError":
|
||||
console.error(error.message, await error.parse());
|
||||
return error;
|
||||
}
|
||||
})
|
||||
|
||||
.onAfterHandle(({ request, set }) => {
|
||||
console.log(
|
||||
JSON.stringify({
|
||||
lvl: "info",
|
||||
msg: "req",
|
||||
method: request.method,
|
||||
path: new URL(request.url).pathname,
|
||||
status: set.status ?? 200,
|
||||
}),
|
||||
);
|
||||
})
|
||||
|
||||
.get("/", () => "Hello world")
|
||||
|
||||
// CALCULATOR
|
||||
|
||||
.post("/calculate", async ({ body, query }) => {
|
||||
interface CalcRequest {
|
||||
player: Player;
|
||||
activityPerformances: ActivityPerformance[];
|
||||
}
|
||||
const { player, activityPerformances } = body as CalcRequest;
|
||||
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(() => {});
|
||||
}
|
||||
|
||||
return output;
|
||||
})
|
||||
|
||||
// COMMERCE
|
||||
// TODO: protect with api key/token
|
||||
.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;
|
||||
await SyncService.sync(printfulProductId);
|
||||
return { ok: true };
|
||||
}),
|
||||
)
|
||||
.get("/:printfulProductId", async ({ params }) => {
|
||||
const printfulProductId = +params.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);
|
||||
|
||||
return { printfulProduct, webflowProduct };
|
||||
}),
|
||||
)
|
||||
|
||||
// WEBHOOKS
|
||||
.post("/webhook/printful", async ({ body }) => {
|
||||
const payload = body as Printful.Webhook.EventPayload;
|
||||
|
||||
switch (payload.type) {
|
||||
case Printful.Webhook.Event.ProductUpdated: {
|
||||
const printfulProduct = payload.data.sync_product;
|
||||
await SyncService.sync(printfulProduct.id);
|
||||
break;
|
||||
}
|
||||
case Printful.Webhook.Event.ProductDeleted: {
|
||||
await WebflowService.Products.remove(
|
||||
payload.data.sync_product.external_id,
|
||||
);
|
||||
break;
|
||||
}
|
||||
case Printful.Webhook.Event.PackageShipped: {
|
||||
const webflowOrderId = payload.data.order.external_id;
|
||||
const shipInfo = payload.data.shipment;
|
||||
|
||||
await WebflowService.Orders.update(webflowOrderId, {
|
||||
shippingTrackingURL: shipInfo.tracking_url,
|
||||
shippingTracking: shipInfo.tracking_number,
|
||||
shippingProvider: shipInfo.carrier,
|
||||
});
|
||||
await WebflowService.Orders.fulfill(webflowOrderId, {
|
||||
sendOrderFulfilledEmail: true,
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return { ok: true };
|
||||
})
|
||||
.post("/webhook/webflow", async ({ request, body, set }) => {
|
||||
if (!WebflowService.Util.verifyWebflowSignature(request, body)) {
|
||||
set.status = 400;
|
||||
return "Invalid signature";
|
||||
}
|
||||
|
||||
const payload = body as Webflow.Webhook.EventPayload;
|
||||
|
||||
switch (payload.triggerType) {
|
||||
case Webflow.Webhook.Event.OrderCreated: {
|
||||
const webflowOrder = payload.payload;
|
||||
|
||||
await PrintfulService.Orders.create({
|
||||
external_id: webflowOrder.orderId,
|
||||
// TODO: derive from webflow
|
||||
shipping: "STANDARD",
|
||||
recipient: {
|
||||
name: webflowOrder.shippingAddress.addressee,
|
||||
address1: webflowOrder.shippingAddress.line1,
|
||||
address2: webflowOrder.shippingAddress.line2,
|
||||
city: webflowOrder.shippingAddress.city,
|
||||
state_code: zipcodesUs.find(
|
||||
webflowOrder.shippingAddress.postalCode.split(
|
||||
"-",
|
||||
)[0],
|
||||
).stateCode,
|
||||
country_code: webflowOrder.shippingAddress.country,
|
||||
zip: webflowOrder.shippingAddress.postalCode,
|
||||
},
|
||||
items: webflowOrder.purchasedItems.map(
|
||||
(webflowOrderSku) => ({
|
||||
external_variant_id: webflowOrderSku.variantId,
|
||||
quantity: webflowOrderSku.count,
|
||||
}),
|
||||
),
|
||||
});
|
||||
|
||||
console.log("Complete");
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return { ok: true };
|
||||
});
|
||||
@@ -1,2 +0,0 @@
|
||||
import type { app } from "./app";
|
||||
export type App = typeof app;
|
||||
@@ -1,896 +0,0 @@
|
||||
import {
|
||||
Activity,
|
||||
Attribute,
|
||||
clamp,
|
||||
Gender,
|
||||
getAvgWeight,
|
||||
kgToLb,
|
||||
lbToKg,
|
||||
range,
|
||||
type ActivityPerformance,
|
||||
type Player,
|
||||
type StandardUnit,
|
||||
} from "./util";
|
||||
import { levenbergMarquardt as LM } from "ml-levenberg-marquardt";
|
||||
import defaultConfig from "$lib/services/calculator/config.json" assert { type: "json" };
|
||||
|
||||
// SOURCES
|
||||
// Squat, Bench, Dead Lift:
|
||||
// http://lonkilgore.com/resources/Lon_Kilgore_Strength_Standard_Tables-Copyright-2023.pdf
|
||||
// 1 mile run:
|
||||
// https://runninglevel.com/running-times/1-mile-times
|
||||
// Broad Jump:
|
||||
// https://nrpt.co.uk/training/tests/power/broad.htm
|
||||
// 3 Cone drill:
|
||||
// https://nflsavant.com/combine.php?utm_source=chatgpt.com
|
||||
|
||||
export type LevelCalculatorOutput = {
|
||||
player: number;
|
||||
attributes: Record<Attribute, number>;
|
||||
};
|
||||
|
||||
export type Levels = Record<string, number>;
|
||||
|
||||
export interface Metrics {
|
||||
age: number;
|
||||
weight: number;
|
||||
gender: Gender;
|
||||
}
|
||||
|
||||
type NumberMetric = Extract<keyof Metrics, "age" | "weight">;
|
||||
|
||||
export interface Standard {
|
||||
metrics: Metrics;
|
||||
levels: Levels;
|
||||
}
|
||||
|
||||
type Generator = {
|
||||
metric: NumberMetric;
|
||||
};
|
||||
|
||||
export type ActivityStandards = Record<
|
||||
Activity,
|
||||
{
|
||||
metadata: {
|
||||
attribute: Attribute;
|
||||
generators: Generator[];
|
||||
unit: StandardUnit;
|
||||
name: string;
|
||||
};
|
||||
standards: Standard[];
|
||||
}
|
||||
>;
|
||||
|
||||
const metricPriority = (m: NumberMetric) => {
|
||||
if (m === "age") return 0;
|
||||
if (m === "weight") return 1;
|
||||
return 2;
|
||||
};
|
||||
|
||||
export class LevelCalculator {
|
||||
standards: Standards;
|
||||
|
||||
public constructor(standards: Standards) {
|
||||
this.standards = standards;
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------------------------------
|
||||
// Level calculations
|
||||
// -------------------------------------------------------------------------------------------------
|
||||
|
||||
public calculate(
|
||||
player: Player,
|
||||
activityPerformances: ActivityPerformance[],
|
||||
): LevelCalculatorOutput {
|
||||
const levels: LevelCalculatorOutput = {
|
||||
player: 0,
|
||||
attributes: {
|
||||
[Attribute.Strength]: 0,
|
||||
[Attribute.Power]: 0,
|
||||
[Attribute.Endurance]: 0,
|
||||
[Attribute.Agility]: 0,
|
||||
} as const,
|
||||
};
|
||||
|
||||
for (const value of Object.values(player)) {
|
||||
if (!value) return levels;
|
||||
}
|
||||
|
||||
levels.attributes = this.calculateAllAttributeLevels(
|
||||
player,
|
||||
activityPerformances,
|
||||
);
|
||||
for (const level of Object.values(levels.attributes)) {
|
||||
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,
|
||||
);
|
||||
|
||||
return levels;
|
||||
}
|
||||
|
||||
private calculateAllAttributeLevels(
|
||||
player: Player,
|
||||
activityPerformances: ActivityPerformance[],
|
||||
): Record<Attribute, number> {
|
||||
const attrLevels: Record<Attribute, number> = {
|
||||
[Attribute.Strength]: 0,
|
||||
[Attribute.Power]: 0,
|
||||
[Attribute.Endurance]: 0,
|
||||
[Attribute.Agility]: 0,
|
||||
} as const;
|
||||
|
||||
for (const value of Object.values(player)) {
|
||||
if (!value) return attrLevels;
|
||||
}
|
||||
|
||||
(Object.values(Attribute) as Attribute[]).forEach((attribute) => {
|
||||
const attrActivityPerformances = activityPerformances.filter(
|
||||
(p) =>
|
||||
this.standards.byActivity(p.activity).getMetadata()
|
||||
.attribute === attribute,
|
||||
);
|
||||
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,
|
||||
)
|
||||
) {
|
||||
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;
|
||||
}
|
||||
|
||||
// must have all activties using a valid performance value (> 0)
|
||||
for (const activityPerformance of activityPerformances) {
|
||||
if (activityPerformance.performance <= 0) return 0;
|
||||
}
|
||||
|
||||
const activityLevels = activityPerformances.map((p) => {
|
||||
const interpolatedStandard = this.standards
|
||||
.byActivity(p.activity)
|
||||
.byMetrics(player.metrics)
|
||||
.getOneInterpolated();
|
||||
return this.findLevel(interpolatedStandard, p.performance);
|
||||
});
|
||||
|
||||
const activityLevelsAvg = Math.round(
|
||||
activityLevels.reduce((sum, curr) => sum + curr, 0) /
|
||||
activityLevels.length,
|
||||
);
|
||||
|
||||
return activityLevelsAvg;
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------------------------------
|
||||
// Level utilities (helpers)
|
||||
// -------------------------------------------------------------------------------------------------
|
||||
|
||||
private findLevel(standard: Standard, performance: number): number {
|
||||
let resultLevel = 1;
|
||||
let resultLevelDiff = Infinity;
|
||||
|
||||
for (const level in standard.levels) {
|
||||
if (!standard.levels[level]) continue;
|
||||
|
||||
const diff = Math.abs(performance - standard.levels[level]);
|
||||
if (diff < resultLevelDiff) {
|
||||
resultLevel = +level;
|
||||
resultLevelDiff = diff;
|
||||
}
|
||||
}
|
||||
return resultLevel;
|
||||
}
|
||||
}
|
||||
|
||||
export type StandardsConfig = {
|
||||
global: {
|
||||
maxLevel: number;
|
||||
};
|
||||
activity: Record<
|
||||
Activity,
|
||||
{
|
||||
weightModifier: number;
|
||||
weightSkew: number;
|
||||
ageModifier: number;
|
||||
enableGeneration: boolean;
|
||||
difficultyModifier: number;
|
||||
peakAge: number;
|
||||
stretch: {
|
||||
upper: number;
|
||||
lower: number;
|
||||
};
|
||||
}
|
||||
>;
|
||||
};
|
||||
|
||||
export class Standards {
|
||||
readonly cfg: StandardsConfig;
|
||||
private activityStandards: ActivityStandards;
|
||||
|
||||
constructor(
|
||||
activityStandards: ActivityStandards,
|
||||
cfg?: Partial<StandardsConfig>,
|
||||
) {
|
||||
this.cfg = Object.assign(defaultConfig, cfg);
|
||||
|
||||
// prepare data
|
||||
this.activityStandards = structuredClone(activityStandards);
|
||||
|
||||
// STRETCH
|
||||
for (const activity of Object.values(Activity)) {
|
||||
const BASE_MAX_LEVEL = 5;
|
||||
|
||||
function expDecayModel([A, B, C]: number[]) {
|
||||
return (i: number) => A * Math.exp(-B * i) + C;
|
||||
}
|
||||
|
||||
if (!this.cfg.activity[activity].enableGeneration) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (const gender of Object.values(Gender)) {
|
||||
const standardsByGender = this.activityStandards[
|
||||
activity
|
||||
].standards.filter((s) => s["metrics"].gender === gender);
|
||||
const ages = [
|
||||
...new Set(standardsByGender.map((s) => s.metrics.age)),
|
||||
];
|
||||
for (const age of ages) {
|
||||
const standards = standardsByGender.filter(
|
||||
(s) => s.metrics.age === age,
|
||||
);
|
||||
|
||||
const data = {
|
||||
x: [] as number[],
|
||||
y: [] as number[],
|
||||
};
|
||||
|
||||
const isIncreasing =
|
||||
standards[0].levels["2"] > standards[0].levels["1"];
|
||||
|
||||
for (const i of range(BASE_MAX_LEVEL).slice(0, -1)) {
|
||||
const level = i + 1;
|
||||
const progressionRatios: number[] = [];
|
||||
for (const standard of standards) {
|
||||
const curr = standard.levels[level];
|
||||
const next = standard.levels[level + 1];
|
||||
progressionRatios.push(
|
||||
isIncreasing ? next / curr : curr / next,
|
||||
);
|
||||
}
|
||||
|
||||
const sum = progressionRatios.reduce(
|
||||
(p, c) => p + c,
|
||||
0,
|
||||
);
|
||||
const progressionRatioAvg =
|
||||
sum / progressionRatios.length;
|
||||
|
||||
data.x.push(i);
|
||||
data.y.push(progressionRatioAvg);
|
||||
}
|
||||
|
||||
const fittedParams = LM(data, expDecayModel, {
|
||||
initialValues: [0.4, 0.5, 1.1],
|
||||
minValues: [0.0, 0.0, 1.02],
|
||||
maxValues: [1.0, 2.0, 1.2],
|
||||
maxIterations: 200,
|
||||
});
|
||||
const getDecayRatio = (i: number) =>
|
||||
expDecayModel(fittedParams.parameterValues)(i);
|
||||
|
||||
// UPDATE THE DATA
|
||||
for (const standard of standards) {
|
||||
const newLevels: Levels = {};
|
||||
const newLowerLevelCount = Math.max(
|
||||
Math.round(
|
||||
this.cfg.activity[activity].stretch.lower,
|
||||
),
|
||||
0,
|
||||
);
|
||||
const newUpperLevelCount = Math.max(
|
||||
Math.round(
|
||||
this.cfg.activity[activity].stretch.upper,
|
||||
),
|
||||
0,
|
||||
);
|
||||
|
||||
const oldLevels = standard.levels as Levels;
|
||||
let prev = oldLevels["1"];
|
||||
for (const i of range(newLowerLevelCount).reverse()) {
|
||||
const level = i + 1;
|
||||
const ratio = getDecayRatio(-i - 1);
|
||||
prev = isIncreasing ? prev / ratio : prev * ratio;
|
||||
newLevels[level] = Math.round(prev);
|
||||
}
|
||||
|
||||
for (const i of range(BASE_MAX_LEVEL)) {
|
||||
const level = i + 1;
|
||||
newLevels[level + newLowerLevelCount] =
|
||||
oldLevels[level];
|
||||
}
|
||||
|
||||
prev = oldLevels[BASE_MAX_LEVEL];
|
||||
for (const i of range(newUpperLevelCount)) {
|
||||
const level =
|
||||
BASE_MAX_LEVEL + newLowerLevelCount + (i + 1);
|
||||
const ratio = getDecayRatio(level - 1);
|
||||
prev = isIncreasing ? prev * ratio : prev / ratio;
|
||||
newLevels[level] = Math.round(prev);
|
||||
}
|
||||
|
||||
standard.levels = newLevels;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// EXPAND, COMPRESS, SKEW
|
||||
for (const activity of Object.values(Activity)) {
|
||||
for (const standard of this.activityStandards[activity].standards) {
|
||||
// expand
|
||||
let i = 1;
|
||||
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,
|
||||
);
|
||||
}
|
||||
|
||||
// apply skew config
|
||||
for (const level in standard.levels) {
|
||||
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.sort(
|
||||
(a, b) =>
|
||||
metricPriority(a.metric) - metricPriority(b.metric),
|
||||
)
|
||||
: [];
|
||||
|
||||
for (const generator of allGenerators) {
|
||||
if (generator.metric === "age") {
|
||||
for (const gender of Object.values(Gender)) {
|
||||
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,
|
||||
})
|
||||
.getOneInterpolated();
|
||||
|
||||
let currAge = minAge;
|
||||
while (currAge <= maxAge) {
|
||||
const newStandard: Standard = {
|
||||
metrics: {
|
||||
weight: getAvgWeight(gender, currAge),
|
||||
age: currAge,
|
||||
gender,
|
||||
},
|
||||
levels: {},
|
||||
};
|
||||
|
||||
for (const lvl in referenceStandard.levels) {
|
||||
const base = referenceStandard.levels[lvl];
|
||||
if (base == null) continue;
|
||||
|
||||
const ageModifier =
|
||||
this.cfg.activity[activity].ageModifier;
|
||||
|
||||
const leftSpan = peakAge - minAge;
|
||||
const rightSpan = maxAge - peakAge;
|
||||
|
||||
// avoid divide-by-zero
|
||||
if (leftSpan <= 0 || rightSpan <= 0) {
|
||||
newStandard.levels[lvl] = base;
|
||||
continue;
|
||||
}
|
||||
|
||||
const cy =
|
||||
(ageModifier * 0.5) / (leftSpan * leftSpan);
|
||||
const co =
|
||||
(ageModifier * 0.5) /
|
||||
(rightSpan * rightSpan);
|
||||
|
||||
const d =
|
||||
currAge <= peakAge
|
||||
? peakAge - currAge
|
||||
: currAge - peakAge;
|
||||
const c = currAge <= peakAge ? cy : co;
|
||||
|
||||
let f = 1 - c * d * d;
|
||||
|
||||
const minFactor = 0.2;
|
||||
const maxFactor = 10.0;
|
||||
f = Math.min(maxFactor, Math.max(minFactor, f));
|
||||
|
||||
newStandard.levels[lvl] = base * f;
|
||||
}
|
||||
|
||||
// prefer real data over generated data
|
||||
const overlappingStandard = this.byActivity(
|
||||
activity,
|
||||
)
|
||||
.byMetrics(newStandard.metrics)
|
||||
.getOne();
|
||||
if (!overlappingStandard)
|
||||
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.age,
|
||||
);
|
||||
} else if (generator.metric === "weight") {
|
||||
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 referenceStandard = this.byActivity(activity)
|
||||
.byMetrics({
|
||||
weight: referenceWeight,
|
||||
gender,
|
||||
age,
|
||||
})
|
||||
.getOneInterpolated();
|
||||
|
||||
let currWeight = minWeight;
|
||||
while (currWeight <= maxWeight) {
|
||||
const newStandard: Standard = {
|
||||
metrics: {
|
||||
weight: currWeight,
|
||||
age,
|
||||
gender,
|
||||
},
|
||||
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;
|
||||
|
||||
newStandard.levels[lvl] =
|
||||
referenceStandard.levels[lvl] *
|
||||
coefficient ** scalingExponent;
|
||||
}
|
||||
|
||||
// prefer real data over generated data
|
||||
const overlappingStandard = this.byActivity(
|
||||
activity,
|
||||
)
|
||||
.byMetrics(newStandard.metrics)
|
||||
.getOne();
|
||||
if (!overlappingStandard)
|
||||
this.activityStandards[
|
||||
activity
|
||||
].standards.push(newStandard);
|
||||
|
||||
currWeight += weightStep;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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.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;
|
||||
if (a.metrics.age !== b.metrics.age)
|
||||
return a.metrics.age - b.metrics.age;
|
||||
return a.metrics.weight - b.metrics.weight;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public byActivity(activity: Activity) {
|
||||
const self = this;
|
||||
|
||||
const state: Partial<Metrics> = {};
|
||||
|
||||
// EXEC METHODS
|
||||
const execMethods = {
|
||||
exact: {
|
||||
// these will get EXACT matches
|
||||
getAll(): Standard[] {
|
||||
const src = self.activityStandards[activity].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;
|
||||
out.push(s);
|
||||
}
|
||||
return out;
|
||||
},
|
||||
getOne: function (): Standard | undefined {
|
||||
return execMethods.exact.getAll()[0];
|
||||
},
|
||||
getNearest(
|
||||
metric: NumberMetric,
|
||||
target: number,
|
||||
): { lower: Standard; upper: Standard } {
|
||||
const standards = execMethods.exact.getAll();
|
||||
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;
|
||||
|
||||
for (const standard of standards) {
|
||||
const standardMetricValue = standard.metrics[metric];
|
||||
|
||||
if (standardMetricValue <= target) {
|
||||
if (
|
||||
!hasLower ||
|
||||
standardMetricValue > lower.metrics[metric]
|
||||
) {
|
||||
lower = standard;
|
||||
hasLower = true;
|
||||
}
|
||||
}
|
||||
if (standardMetricValue >= target) {
|
||||
if (
|
||||
!hasUpper ||
|
||||
standardMetricValue < upper.metrics[metric]
|
||||
) {
|
||||
upper = standard;
|
||||
hasUpper = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// clamp to ends if one side missing
|
||||
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)
|
||||
.byGender(metrics.gender)
|
||||
.byAge(targetAge)
|
||||
.getNearest("weight", metrics.weight);
|
||||
|
||||
const weightLower = lowerAgeWeightStandard.metrics.weight;
|
||||
const weightUpper = upperAgeWeightStandard.metrics.weight;
|
||||
|
||||
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,
|
||||
);
|
||||
};
|
||||
|
||||
// Find nearest age standards
|
||||
const { lower: lowerAgeStandard, upper: upperAgeStandard } =
|
||||
this.byActivity(activity)
|
||||
.byGender(metrics.gender)
|
||||
.getNearest("age", metrics.age);
|
||||
|
||||
const ageLower = lowerAgeStandard.metrics.age;
|
||||
const ageUpper = upperAgeStandard.metrics.age;
|
||||
|
||||
// Interpolate by age and weight
|
||||
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,
|
||||
);
|
||||
|
||||
return {
|
||||
metrics: metrics,
|
||||
levels: interpolatedLevels,
|
||||
};
|
||||
};
|
||||
|
||||
const getAllInterpolated = (opt?: {
|
||||
normalizeForLb?: boolean;
|
||||
}): Standard[] => {
|
||||
const metrics: Metrics = state as Metrics;
|
||||
|
||||
let weights = this.weightsFor(
|
||||
activity,
|
||||
metrics.gender,
|
||||
metrics.age,
|
||||
);
|
||||
if (opt?.normalizeForLb) {
|
||||
weights = weights.map((w) => lbToKg(Math.round(kgToLb(w))));
|
||||
}
|
||||
|
||||
const interpolatedStandards: Standard[] = weights.map((weight) =>
|
||||
interpolateByAgeAndWeight({ ...metrics, weight }),
|
||||
);
|
||||
|
||||
return interpolatedStandards;
|
||||
};
|
||||
|
||||
const getOneInterpolated = (): Standard => {
|
||||
const metrics: Metrics = state as Metrics;
|
||||
return interpolateByAgeAndWeight(metrics);
|
||||
};
|
||||
|
||||
const getMetadata = () => this.activityStandards[activity].metadata;
|
||||
|
||||
// QUERY METHODS
|
||||
|
||||
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,
|
||||
};
|
||||
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];
|
||||
if (activityStandard.metadata.attribute === attribute) {
|
||||
activities.push(activity as Activity);
|
||||
}
|
||||
}
|
||||
return activities;
|
||||
}
|
||||
|
||||
// TODO: turn these into queries?
|
||||
public weightsFor(activity: Activity, gender: Gender, age: number) {
|
||||
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 weights = this.byActivity(activity)
|
||||
.byGender(gender)
|
||||
.byAge(closestAge)
|
||||
.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))];
|
||||
return ages;
|
||||
}
|
||||
|
||||
private expandLevels(levels: Levels, i: number): Levels {
|
||||
if (i == 0) {
|
||||
return levels;
|
||||
}
|
||||
const levelKeys = Object.keys(levels)
|
||||
.map(Number)
|
||||
.sort((a, b) => a - b);
|
||||
|
||||
const newLevels: Levels = {};
|
||||
let j = 1;
|
||||
|
||||
for (let k = 0; k < levelKeys.length; ++k) {
|
||||
const currLevel = levelKeys[k];
|
||||
const nextLevel = levelKeys[k + 1];
|
||||
|
||||
newLevels[j++] = levels[currLevel];
|
||||
|
||||
if (nextLevel) {
|
||||
newLevels[j++] = (levels[currLevel] + levels[nextLevel]) / 2;
|
||||
}
|
||||
}
|
||||
|
||||
return this.expandLevels(newLevels, i - 1);
|
||||
}
|
||||
|
||||
private compressLevels(levels: Levels, targetLvl: number): Levels {
|
||||
const keys = Object.keys(levels)
|
||||
.map(Number)
|
||||
.sort((a, b) => a - b);
|
||||
const lvlCnt = keys.length;
|
||||
|
||||
if (targetLvl <= 0) {
|
||||
throw new Error("Target levels amount must be a positive integer");
|
||||
}
|
||||
if (lvlCnt === 0) {
|
||||
return {};
|
||||
}
|
||||
if (targetLvl === 1) {
|
||||
return { 1: levels[keys[0]] };
|
||||
}
|
||||
if (lvlCnt < targetLvl) {
|
||||
throw new Error(
|
||||
"Target levels amount must be less than or equal to the current levels amount",
|
||||
);
|
||||
}
|
||||
|
||||
// Linear resample
|
||||
const compressedLevels: Levels = {};
|
||||
|
||||
for (let targetIndex = 0; targetIndex < targetLvl; ++targetIndex) {
|
||||
const sourcePos = (targetIndex * (lvlCnt - 1)) / (targetLvl - 1);
|
||||
|
||||
const sourceIndexLower = Math.floor(sourcePos);
|
||||
const sourceIndexUpper = Math.min(sourceIndexLower + 1, lvlCnt - 1);
|
||||
|
||||
const interpolationWeight = sourcePos - sourceIndexLower;
|
||||
|
||||
const lowerValue = levels[keys[sourceIndexLower]];
|
||||
const upperValue = levels[keys[sourceIndexUpper]];
|
||||
|
||||
compressedLevels[targetIndex + 1] =
|
||||
lowerValue + (upperValue - lowerValue) * interpolationWeight;
|
||||
}
|
||||
|
||||
return compressedLevels;
|
||||
}
|
||||
|
||||
private interpolateLevels(
|
||||
lower: Levels,
|
||||
upper: Levels,
|
||||
ratio: number,
|
||||
): Levels {
|
||||
if (Object.keys(lower).length !== Object.keys(upper).length)
|
||||
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 interpolatedLevels: Levels = {};
|
||||
for (const lvl in lower) {
|
||||
interpolatedLevels[lvl] = lerp(lvl);
|
||||
}
|
||||
|
||||
return interpolatedLevels;
|
||||
}
|
||||
}
|
||||
@@ -1,99 +0,0 @@
|
||||
import { type Metrics } from "$lib/services/calculator/main";
|
||||
import avgWeightDataRaw from "$lib/data/avg-weights.json";
|
||||
|
||||
// -------------------------------------------------------------------------------------------------
|
||||
// DATA MODELS
|
||||
// -------------------------------------------------------------------------------------------------
|
||||
|
||||
export type Player = {
|
||||
name?: string;
|
||||
metrics: Metrics;
|
||||
};
|
||||
|
||||
export enum Attribute {
|
||||
Strength = "Strength",
|
||||
Power = "Power",
|
||||
Endurance = "Endurance",
|
||||
Agility = "Agility",
|
||||
}
|
||||
|
||||
export enum Activity {
|
||||
BackSquat = "BackSquat",
|
||||
Deadlift = "Deadlift",
|
||||
BenchPress = "BenchPress",
|
||||
Run = "Run",
|
||||
BroadJump = "BroadJump",
|
||||
ConeDrill = "ConeDrill",
|
||||
}
|
||||
|
||||
export interface ActivityPerformance {
|
||||
activity: Activity;
|
||||
performance: number;
|
||||
}
|
||||
|
||||
export enum Gender {
|
||||
Male = "Male",
|
||||
Female = "Female",
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------------------------------
|
||||
// Conversion utilities
|
||||
// -------------------------------------------------------------------------------------------------
|
||||
|
||||
export type StandardUnit = "ms" | "cm" | "kg";
|
||||
|
||||
export const lbToKg = (lb: number | string): number => +lb * 0.453592;
|
||||
export const kgToLb = (kg: number | string): number => +kg * 2.20462;
|
||||
|
||||
export const minToMs = (min: number) => min * 60000;
|
||||
export const secToMs = (sec: number) => sec * 1000;
|
||||
export const msToMin = (ms: number) => ms / 60000;
|
||||
|
||||
export const ftToCm = (ft: number) => ft * 30.48;
|
||||
export const inToCm = (inches: number) => inches * 2.54;
|
||||
export const cmToIn = (cm: number) => cm / 2.54;
|
||||
|
||||
export const msToTime = (ms: number, includeMs = false): string => {
|
||||
const minutes = Math.floor(ms / 60000);
|
||||
const seconds = Math.floor((ms % 60000) / 1000);
|
||||
const milliseconds = Math.floor(ms % 1000);
|
||||
|
||||
let formattedTime = `${minutes}:${seconds.toString().padStart(2, "0")}`;
|
||||
if (includeMs) {
|
||||
formattedTime += `.${milliseconds.toString().padStart(3, "0")}`;
|
||||
}
|
||||
return formattedTime;
|
||||
};
|
||||
|
||||
export const range = (length: number) =>
|
||||
Array.from({ length: length }, (_, i) => i);
|
||||
|
||||
export const clamp = (x: number, lo: number, hi: number) =>
|
||||
Math.max(lo, Math.min(hi, x));
|
||||
|
||||
export const getAvgWeight = (gender: Gender, age: number) => {
|
||||
type AvgWeightData = {
|
||||
metadata: {
|
||||
unit: StandardUnit;
|
||||
};
|
||||
weights: Metrics[];
|
||||
};
|
||||
let avgWeightData = avgWeightDataRaw as AvgWeightData;
|
||||
|
||||
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],
|
||||
};
|
||||
for (const avg of avgWeights) {
|
||||
const diff = Math.abs(avg.age - age);
|
||||
if (diff < closest.diff) {
|
||||
closest.diff = diff;
|
||||
closest.avg = avg;
|
||||
}
|
||||
}
|
||||
|
||||
return closest.avg.weight;
|
||||
};
|
||||
@@ -1,201 +0,0 @@
|
||||
import type { Printful } from "./util/types";
|
||||
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}`,
|
||||
{
|
||||
method: "GET",
|
||||
headers: { ...env().AUTH_HEADERS },
|
||||
},
|
||||
);
|
||||
|
||||
if (res.status === 404 || res.status === 400) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!res.ok) {
|
||||
throw new FetchError("Failed to get Printful variant", res);
|
||||
}
|
||||
|
||||
const payload: Printful.Products.MetaDataSingle<Printful.Products.SyncVariant> =
|
||||
await res.json();
|
||||
return payload.result;
|
||||
}
|
||||
};
|
||||
|
||||
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}`,
|
||||
{
|
||||
method: "GET",
|
||||
headers: { ...env().AUTH_HEADERS },
|
||||
},
|
||||
);
|
||||
|
||||
if (!res.ok) {
|
||||
throw new FetchError(
|
||||
"Failed to get all Printful products",
|
||||
res,
|
||||
);
|
||||
}
|
||||
|
||||
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;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
return allSyncProducts;
|
||||
}
|
||||
|
||||
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 },
|
||||
},
|
||||
);
|
||||
|
||||
if (res.status === 404 || res.status === 400) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!res.ok) {
|
||||
throw new FetchError("Failed to get Printful product", res);
|
||||
}
|
||||
|
||||
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}`,
|
||||
{
|
||||
method: "PUT",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
...env().AUTH_HEADERS,
|
||||
},
|
||||
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) {
|
||||
const res = await fetch(`${env().API_URL}/orders`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
...env().AUTH_HEADERS,
|
||||
},
|
||||
body: JSON.stringify(printfulOrder),
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
throw new FetchError("Failed to create printful order", res);
|
||||
}
|
||||
}
|
||||
|
||||
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}`,
|
||||
{
|
||||
method: "GET",
|
||||
headers: { ...env().AUTH_HEADERS },
|
||||
},
|
||||
);
|
||||
|
||||
if (!res.ok) {
|
||||
throw new FetchError(
|
||||
"Failed to get all Printful orders",
|
||||
res,
|
||||
);
|
||||
}
|
||||
|
||||
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;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
return allPrintfulOrders;
|
||||
}
|
||||
};
|
||||
|
||||
static Util = class {
|
||||
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.",
|
||||
);
|
||||
}
|
||||
|
||||
const vars = {
|
||||
API_URL: "https://api.printful.com",
|
||||
AUTH_TOKEN: Bun.env.PRINTFUL_AUTH,
|
||||
STORE_ID: Bun.env.PRINTFUL_STORE_ID,
|
||||
};
|
||||
|
||||
return {
|
||||
...vars,
|
||||
AUTH_HEADERS: {
|
||||
Authorization: `Bearer ${vars.AUTH_TOKEN}`,
|
||||
"X-PF-Store-Id": `${vars.STORE_ID}`,
|
||||
},
|
||||
};
|
||||
};
|
||||
@@ -1,356 +0,0 @@
|
||||
import type { Printful, Webflow } from "./util/types";
|
||||
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));
|
||||
|
||||
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>;
|
||||
variants: {
|
||||
color: string;
|
||||
product: Printful.Products.Product;
|
||||
}[];
|
||||
};
|
||||
|
||||
export default class SyncService {
|
||||
static state = {
|
||||
isSyncing: false,
|
||||
syncingIds: [] as number[],
|
||||
};
|
||||
|
||||
static async sync(printfulProductId: number | undefined) {
|
||||
this.state.isSyncing = true;
|
||||
|
||||
const mainPrintfulProducts: MainProduct[] = [];
|
||||
const webflowProducts: Webflow.Products.ProductAndSkus[] = [];
|
||||
try {
|
||||
let printfulProducts = await PrintfulService.Products.getAll();
|
||||
if (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),
|
||||
);
|
||||
}
|
||||
}
|
||||
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,
|
||||
);
|
||||
|
||||
let mainPrintfulProduct = mainPrintfulProducts.find((mp) =>
|
||||
mp.name.includes(mainProductName),
|
||||
);
|
||||
if (!mainPrintfulProduct) {
|
||||
mainPrintfulProduct = {
|
||||
name: mainProductName,
|
||||
variants: [],
|
||||
externalId: printfulProduct.external_id,
|
||||
colors: new Set(),
|
||||
};
|
||||
mainPrintfulProducts.push(mainPrintfulProduct);
|
||||
}
|
||||
|
||||
const productColor = this.findColorInProductName(
|
||||
printfulProduct.name,
|
||||
);
|
||||
mainPrintfulProduct.variants.push({
|
||||
color: productColor,
|
||||
product: (await PrintfulService.Products.get(
|
||||
printfulProduct.id,
|
||||
))!,
|
||||
});
|
||||
mainPrintfulProduct.colors.add(productColor);
|
||||
}
|
||||
} catch (error) {
|
||||
throw error;
|
||||
} finally {
|
||||
this.state.isSyncing = false;
|
||||
this.state.syncingIds = [];
|
||||
}
|
||||
|
||||
// TODO: Validate main printful products
|
||||
|
||||
// Sync the main printful products
|
||||
console.log("Syncing main products...");
|
||||
for (const mainPrintfulProduct of mainPrintfulProducts) {
|
||||
try {
|
||||
this.state.isSyncing = true;
|
||||
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>[] =
|
||||
[];
|
||||
|
||||
for (const specialVariant of mainPrintfulProduct.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,
|
||||
);
|
||||
if (!sizes.includes(printfulVariant.size))
|
||||
sizeIsInAllVariants = false;
|
||||
}
|
||||
|
||||
if (sizeIsInAllVariants) {
|
||||
foundSizes.add(printfulVariant.size);
|
||||
|
||||
webflowSkus.push({
|
||||
id: printfulVariant.external_id,
|
||||
fieldData: {
|
||||
name: printfulVariant.name,
|
||||
slug: formatSlug(printfulVariant.name),
|
||||
"sku-values": {
|
||||
color: specialVariant.color,
|
||||
size: printfulVariant.size,
|
||||
},
|
||||
price: {
|
||||
value: Math.floor(
|
||||
+printfulVariant.retail_price * 100,
|
||||
),
|
||||
unit: printfulVariant.currency,
|
||||
currency: printfulVariant.currency,
|
||||
},
|
||||
"main-image":
|
||||
PrintfulService.Util.getVariantMainImage(
|
||||
printfulVariant,
|
||||
),
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// SYNC PRODUCT DATA
|
||||
let existingWebflowProduct = webflowProducts.find(
|
||||
(webflowProduct) =>
|
||||
webflowProduct.product.id ===
|
||||
mainPrintfulProduct.externalId.split("-")[0],
|
||||
);
|
||||
if (existingWebflowProduct) {
|
||||
// SYNC PRODUCT UPDATE
|
||||
|
||||
// do not update images
|
||||
for (const webflowSku of webflowSkus) {
|
||||
delete webflowSku.fieldData?.["main-image"];
|
||||
}
|
||||
|
||||
const webflowProductId =
|
||||
mainPrintfulProduct.externalId.split("-")[0];
|
||||
WebflowService.Products.update(webflowProductId, {
|
||||
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: "size",
|
||||
name: "Size",
|
||||
enum: Array.from(foundSizes).map(
|
||||
(size) => ({
|
||||
id: size,
|
||||
slug: formatSlug(size),
|
||||
name: size,
|
||||
}),
|
||||
),
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
sku: webflowSkus[0],
|
||||
});
|
||||
|
||||
for (const webflowSku of webflowSkus.slice(1)) {
|
||||
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],
|
||||
);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// SYNC PRODUCT CREATE
|
||||
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: "size",
|
||||
name: "Size",
|
||||
enum: Array.from(foundSizes).map(
|
||||
(size) => ({
|
||||
id: size,
|
||||
slug: formatSlug(size),
|
||||
name: size,
|
||||
}),
|
||||
),
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
sku: webflowSkus[0],
|
||||
});
|
||||
|
||||
// create webflow product SKUs
|
||||
await WebflowService.Products.Skus.create(
|
||||
webflowProductId,
|
||||
webflowSkus.slice(1),
|
||||
);
|
||||
|
||||
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,
|
||||
);
|
||||
if (associatedWebflowSku) {
|
||||
newPrintfulVariants.push({
|
||||
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}`,
|
||||
},
|
||||
sync_variants: newPrintfulVariants,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
// SYNC IMAGES
|
||||
// for (const sku of existingWebflowProduct.skus) {
|
||||
// const skuColor = sku.fieldData["sku-values"]?.["color"]?.toLowerCase() ?? "None";
|
||||
// const skuSlug = `${existingWebflowProduct.product.fieldData.slug}-${skuColor}`;
|
||||
// const skuImageUrls = await this.getProductImageUrls(skuSlug);
|
||||
//
|
||||
// sku.fieldData["main-image"] = skuImageUrls[0] ?? sku.fieldData["main-image"];
|
||||
// sku.fieldData["more-images"] = skuImageUrls.slice(1).map(url => ({ url }));
|
||||
//
|
||||
// await WebflowService.Products.Skus.update(existingWebflowProduct.product.id, sku.id, sku);
|
||||
// }
|
||||
//
|
||||
if (err instanceof FetchError) {
|
||||
console.error(err.message, err.payload);
|
||||
}
|
||||
}
|
||||
}
|
||||
this.state.isSyncing = false;
|
||||
this.state.syncingIds = [];
|
||||
console.log("Done");
|
||||
}
|
||||
|
||||
private static getProductImageUrls = async (slug: string) => {
|
||||
const res = await fetch(`${R2_WORKER_URL}/product-images/${slug}`);
|
||||
if (!res.ok) {
|
||||
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}`);
|
||||
};
|
||||
|
||||
public static findColorInProductName = (productName: string): string => {
|
||||
const m = productName.match(/\[([^\]]+)\]/);
|
||||
return m ? m[1] : "N/A";
|
||||
};
|
||||
|
||||
public static getMainProductName = (productName: string) => {
|
||||
const colorInName = this.findColorInProductName(productName);
|
||||
if (colorInName) {
|
||||
return productName.replace(`[${colorInName}]`, "").trimEnd();
|
||||
} else {
|
||||
return productName;
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -1,309 +0,0 @@
|
||||
import type { Webflow } from "./util/types";
|
||||
import { FetchError, type DeepPartial } from "./util/misc";
|
||||
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`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
...env().AUTH_HEADER,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
skus: skus,
|
||||
}),
|
||||
},
|
||||
);
|
||||
|
||||
if (!res.ok) {
|
||||
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}`,
|
||||
{
|
||||
method: "PATCH",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
...env().AUTH_HEADER,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
sku: webflowSku,
|
||||
}),
|
||||
},
|
||||
);
|
||||
if (!res.ok) {
|
||||
throw new FetchError(
|
||||
"Failed to update Webflow product SKU",
|
||||
res,
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
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,
|
||||
},
|
||||
body: JSON.stringify(webflowProductAndSku),
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
throw new FetchError("Failed to create Webflow product", res);
|
||||
}
|
||||
|
||||
const createdWebflowProduct = await res.json();
|
||||
return createdWebflowProduct.product.id;
|
||||
}
|
||||
|
||||
static async getAll(): Promise<Webflow.Products.ProductAndSkus[]> {
|
||||
const res = await fetch(`${env().API_SITES_URL}/products`, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
...env().AUTH_HEADER,
|
||||
},
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
throw new FetchError("Failed to get all Webflow products", res);
|
||||
}
|
||||
|
||||
const payload = await res.json();
|
||||
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}`,
|
||||
{
|
||||
method: "GET",
|
||||
headers: {
|
||||
...env().AUTH_HEADER,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
if (res.status === 404 || res.status === 400) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!res.ok) {
|
||||
throw new FetchError("Failed to get Webflow product", res);
|
||||
}
|
||||
|
||||
const payload = await res.json();
|
||||
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}`,
|
||||
{
|
||||
method: "PATCH",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
...env().AUTH_HEADER,
|
||||
},
|
||||
body: JSON.stringify(webflowProduct),
|
||||
},
|
||||
);
|
||||
|
||||
if (!res.ok) {
|
||||
throw new FetchError("Failed to update Webflow product", res);
|
||||
}
|
||||
}
|
||||
|
||||
static async remove(webflowProductId: string) {
|
||||
const res = await fetch(
|
||||
`${env().API_COLLECTIONS_URL}/items/${webflowProductId}`,
|
||||
{
|
||||
method: "DELETE",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
...env().AUTH_HEADER,
|
||||
},
|
||||
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[]> {
|
||||
// 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()}`,
|
||||
{
|
||||
method: "GET",
|
||||
headers: { ...env().AUTH_HEADER },
|
||||
},
|
||||
);
|
||||
|
||||
if (!res.ok) {
|
||||
throw new FetchError("Failed to get all Webflow orders", res);
|
||||
}
|
||||
|
||||
const payload = await res.json();
|
||||
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}`,
|
||||
{
|
||||
method: "PATCH",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
...env().AUTH_HEADER,
|
||||
},
|
||||
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`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
...env().AUTH_HEADER,
|
||||
},
|
||||
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) {
|
||||
try {
|
||||
const timestamp = request.headers.get("x-webflow-timestamp");
|
||||
if (!timestamp) {
|
||||
throw new Error("No timestamp provided");
|
||||
}
|
||||
const providedSignature = request.headers.get(
|
||||
"x-webflow-signature",
|
||||
);
|
||||
if (!providedSignature) {
|
||||
throw new Error("No signature provided");
|
||||
}
|
||||
|
||||
const secret = env().WEBHOOK_SECRET;
|
||||
if (!secret) {
|
||||
throw new Error("No secret provided");
|
||||
}
|
||||
|
||||
if (!body) {
|
||||
throw new Error("Body is empty");
|
||||
}
|
||||
|
||||
const requestTimestamp = parseInt(timestamp, 10);
|
||||
const data = `${requestTimestamp}:${JSON.stringify(body)}`;
|
||||
const hash = crypto
|
||||
.createHmac("sha256", secret)
|
||||
.update(data)
|
||||
.digest("hex");
|
||||
|
||||
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");
|
||||
}
|
||||
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.",
|
||||
);
|
||||
}
|
||||
|
||||
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,
|
||||
};
|
||||
|
||||
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}` },
|
||||
};
|
||||
};
|
||||
@@ -1,77 +0,0 @@
|
||||
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));
|
||||
|
||||
type Validation = {
|
||||
isValid: boolean;
|
||||
message: string;
|
||||
};
|
||||
|
||||
const validateEquality = (wValue: any, pValue: any): Validation => {
|
||||
const validation = {
|
||||
isValid: true,
|
||||
message: "Valid",
|
||||
};
|
||||
|
||||
if (wValue !== pValue) {
|
||||
validation.isValid = false;
|
||||
validation.message = `${wValue} !== ${pValue}`;
|
||||
}
|
||||
|
||||
return validation;
|
||||
};
|
||||
|
||||
const wProducts = await WebflowService.Products.getAll();
|
||||
|
||||
let invalidCount = 0;
|
||||
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}`,
|
||||
);
|
||||
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),
|
||||
),
|
||||
];
|
||||
|
||||
// if (!validateEquality(sku.fieldData.name, pVariant.name).isValid) {
|
||||
// sku.fieldData.name = pVariant.name;
|
||||
// await WebflowService.Products.Skus.update(wProduct.product.id, sku.id, sku);
|
||||
// }
|
||||
|
||||
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("VALID");
|
||||
}
|
||||
} else {
|
||||
++invalidCount;
|
||||
console.log("INVALID");
|
||||
console.log("Not found");
|
||||
}
|
||||
console.log();
|
||||
}
|
||||
await sleep(3000);
|
||||
}
|
||||
|
||||
console.log("INVALID COUNT: " + invalidCount);
|
||||
@@ -1,44 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { page } from "$app/state";
|
||||
import favicon from "$lib/assets/favicon.svg";
|
||||
import "../app.css";
|
||||
|
||||
const links = [
|
||||
{ name: "Calculator", path: "/calculator" },
|
||||
{ name: "Commerce", path: "/commerce" },
|
||||
];
|
||||
|
||||
let { children } = $props();
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<link rel="icon" href={favicon} />
|
||||
</svelte:head>
|
||||
|
||||
<div class="drawer lg:drawer-open">
|
||||
<input id="sidebar" type="checkbox" class="drawer-toggle" />
|
||||
<div class="drawer-content p-10 flex flex-col items-center">
|
||||
{@render children?.()}
|
||||
<!-- <label for="sidebar" class="btn btn-primary drawer-button lg:hidden"> -->
|
||||
<!-- Open drawer -->
|
||||
<!-- </label> -->
|
||||
</div>
|
||||
<div class="drawer-side">
|
||||
<label for="sidebar" aria-label="close sidebar" class="drawer-overlay"
|
||||
></label>
|
||||
<ul
|
||||
class="menu menu-lg bg-base-200 text-base-content min-h-full w-72 p-4"
|
||||
>
|
||||
{#each links as link}
|
||||
<li>
|
||||
<a
|
||||
class={{
|
||||
"menu-active": page.url.pathname === link.path,
|
||||
}}
|
||||
href={link.path}>{link.name}</a
|
||||
>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,4 +0,0 @@
|
||||
import { app } from "$lib/server/app";
|
||||
|
||||
type RequestHandler = (v: { request: Request }) => Response | Promise<Response>;
|
||||
export const fallback: RequestHandler = ({ request }) => app.handle(request);
|
||||
@@ -1,12 +0,0 @@
|
||||
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]) => {
|
||||
return {
|
||||
products: {
|
||||
printful: PrintfulService.Products.getAll(),
|
||||
webflow: WebflowService.Products.getAll(),
|
||||
},
|
||||
};
|
||||
};
|
||||
@@ -1,130 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { api } from "$lib/api";
|
||||
import type { Printful } from "$lib/services/commerce/util/types";
|
||||
import { onMount } from "svelte";
|
||||
|
||||
const { data } = $props();
|
||||
|
||||
const synchronizer = $state({
|
||||
isSyncing: false,
|
||||
syncingIds: [] as number[],
|
||||
poll: async function () {
|
||||
const res = await api.products.sync.get();
|
||||
|
||||
if (res.data) {
|
||||
synchronizer.isSyncing = res.data.isSyncing;
|
||||
synchronizer.syncingIds = res.data.syncingIds;
|
||||
}
|
||||
},
|
||||
startPolling: () => {
|
||||
const intervalId = setInterval(async () => {
|
||||
await synchronizer.poll();
|
||||
if (!synchronizer.isSyncing) clearInterval(intervalId);
|
||||
}, 100);
|
||||
},
|
||||
syncAll: async () => {
|
||||
synchronizer.startPolling();
|
||||
await api.products.sync.post();
|
||||
},
|
||||
sync: async (printfulProductId: number) => {
|
||||
synchronizer.startPolling();
|
||||
await api.products.sync({ printfulProductId }).post();
|
||||
},
|
||||
});
|
||||
|
||||
onMount(async () => {
|
||||
await synchronizer.poll();
|
||||
if (synchronizer.isSyncing) {
|
||||
synchronizer.startPolling();
|
||||
}
|
||||
});
|
||||
|
||||
const isProductSynced = async (
|
||||
printfulProduct: Printful.Products.SyncProduct,
|
||||
) => {
|
||||
const webflowProducts = await data.products.webflow;
|
||||
return webflowProducts.some(
|
||||
(p) => p.product.id === printfulProduct.external_id.split("-")[0],
|
||||
);
|
||||
};
|
||||
</script>
|
||||
|
||||
{#await data.products.printful}
|
||||
<p>Loading...</p>
|
||||
{:then printfulProducts}
|
||||
<button
|
||||
disabled={synchronizer.isSyncing}
|
||||
onclick={() => synchronizer.syncAll()}
|
||||
class="btn btn-xl mb-5"
|
||||
>
|
||||
{#if synchronizer.isSyncing}
|
||||
Syncing...
|
||||
{:else}
|
||||
Sync all
|
||||
{/if}
|
||||
</button>
|
||||
<div class="w-full overflow-x-auto">
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Status</th>
|
||||
<th>Sync</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#each printfulProducts as printfulProduct}
|
||||
<tr class="hover:bg-base-300">
|
||||
<td
|
||||
><button
|
||||
onclick={async () => {
|
||||
const productData = await api
|
||||
.products({
|
||||
printfulProductId:
|
||||
printfulProduct.id,
|
||||
})
|
||||
.get();
|
||||
console.log(
|
||||
await productData.response.json(),
|
||||
);
|
||||
}}
|
||||
class="cursor-pointer underline"
|
||||
>
|
||||
{printfulProduct.name}
|
||||
</button></td
|
||||
>
|
||||
<td>
|
||||
{#await isProductSynced(printfulProduct) then isSynced}
|
||||
{#if synchronizer.syncingIds.includes(printfulProduct.id)}
|
||||
<div class="badge badge-warning">
|
||||
Syncing...
|
||||
</div>
|
||||
{:else if isSynced}
|
||||
<div class="badge badge-success">
|
||||
Synced
|
||||
</div>
|
||||
{:else}
|
||||
<div class="badge badge-error">
|
||||
Desynced
|
||||
</div>
|
||||
{/if}
|
||||
{/await}
|
||||
</td>
|
||||
<td>
|
||||
<button
|
||||
disabled={synchronizer.isSyncing}
|
||||
onclick={() =>
|
||||
synchronizer.sync(printfulProduct.id)}
|
||||
class="btn"
|
||||
>
|
||||
Sync
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{:catch error}
|
||||
<p>Something went wrong: {error.message}</p>
|
||||
{/await}
|
||||
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
// Environment setup & latest features
|
||||
"lib": ["ESNext", "DOM"],
|
||||
"target": "ESNext",
|
||||
"module": "Preserve",
|
||||
"moduleDetection": "force",
|
||||
"jsx": "react-jsx",
|
||||
"allowJs": true,
|
||||
|
||||
// Bundler mode
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"verbatimModuleSyntax": true,
|
||||
"noEmit": true,
|
||||
|
||||
// Best practices
|
||||
"strict": true,
|
||||
"skipLibCheck": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noUncheckedIndexedAccess": true,
|
||||
"noImplicitOverride": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
|
||||
// Some stricter flags (disabled by default)
|
||||
"noUnusedLocals": false,
|
||||
"noUnusedParameters": false,
|
||||
"noPropertyAccessFromIndexSignature": false,
|
||||
},
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
{
|
||||
"extends": "./.svelte-kit/tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"allowJs": true,
|
||||
"checkJs": true,
|
||||
"esModuleInterop": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"resolveJsonModule": true,
|
||||
"verbatimModuleSyntax": true,
|
||||
"skipLibCheck": true,
|
||||
"sourceMap": true,
|
||||
"strict": 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
|
||||
//
|
||||
// To make changes to top-level options such as include and exclude, we recommend extending
|
||||
// the generated config; see https://svelte.dev/docs/kit/configuration#typescript
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
deploy:
|
||||
npx wrangler deploy
|
||||
@@ -0,0 +1,17 @@
|
||||
export default {
|
||||
async fetch(request: Request, env: Env) {
|
||||
if (request.method !== "POST") {
|
||||
return new Response("Method Not Allowed", { status: 405 });
|
||||
}
|
||||
|
||||
const body = await request.json().catch(() => null);
|
||||
if (!body) return new Response("Bad Request", { status: 400 });
|
||||
|
||||
const key = `calc:${Date.now()}:${crypto.randomUUID()}`;
|
||||
await env.CALCULATOR_LOGGING.put(key, JSON.stringify(body));
|
||||
|
||||
return new Response(JSON.stringify({ ok: true, key }), {
|
||||
headers: { "content-type": "application/json" },
|
||||
});
|
||||
},
|
||||
} satisfies ExportedHandler<Env>;
|
||||
@@ -0,0 +1,5 @@
|
||||
fetch("https://kv-logger.xominus.workers.dev", {
|
||||
method: "POST",
|
||||
headers: { "content-type": "application/json" },
|
||||
body: JSON.stringify({ "test": 1 })
|
||||
}).catch((e) => { console.error(e) });
|
||||
@@ -0,0 +1,7 @@
|
||||
name = "kv-logger"
|
||||
main = "src/index.ts"
|
||||
compatibility_date = "2025-10-17"
|
||||
|
||||
[[kv_namespaces]]
|
||||
binding = "CALCULATOR_LOGGING"
|
||||
id = "4b585549cf074f729d9ec4ac39e2c879"
|
||||
@@ -0,0 +1,12 @@
|
||||
# http://editorconfig.org
|
||||
root = true
|
||||
|
||||
[*]
|
||||
indent_style = tab
|
||||
end_of_line = lf
|
||||
charset = utf-8
|
||||
trim_trailing_whitespace = true
|
||||
insert_final_newline = true
|
||||
|
||||
[*.yml]
|
||||
indent_style = space
|
||||
@@ -0,0 +1,167 @@
|
||||
# Logs
|
||||
|
||||
logs
|
||||
_.log
|
||||
npm-debug.log_
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
lerna-debug.log*
|
||||
.pnpm-debug.log*
|
||||
|
||||
# Diagnostic reports (https://nodejs.org/api/report.html)
|
||||
|
||||
report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
|
||||
|
||||
# Runtime data
|
||||
|
||||
pids
|
||||
_.pid
|
||||
_.seed
|
||||
\*.pid.lock
|
||||
|
||||
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||
|
||||
lib-cov
|
||||
|
||||
# Coverage directory used by tools like istanbul
|
||||
|
||||
coverage
|
||||
\*.lcov
|
||||
|
||||
# nyc test coverage
|
||||
|
||||
.nyc_output
|
||||
|
||||
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
|
||||
|
||||
.grunt
|
||||
|
||||
# Bower dependency directory (https://bower.io/)
|
||||
|
||||
bower_components
|
||||
|
||||
# node-waf configuration
|
||||
|
||||
.lock-wscript
|
||||
|
||||
# Compiled binary addons (https://nodejs.org/api/addons.html)
|
||||
|
||||
build/Release
|
||||
|
||||
# Dependency directories
|
||||
|
||||
node_modules/
|
||||
jspm_packages/
|
||||
|
||||
# Snowpack dependency directory (https://snowpack.dev/)
|
||||
|
||||
web_modules/
|
||||
|
||||
# TypeScript cache
|
||||
|
||||
\*.tsbuildinfo
|
||||
|
||||
# Optional npm cache directory
|
||||
|
||||
.npm
|
||||
|
||||
# Optional eslint cache
|
||||
|
||||
.eslintcache
|
||||
|
||||
# Optional stylelint cache
|
||||
|
||||
.stylelintcache
|
||||
|
||||
# Microbundle cache
|
||||
|
||||
.rpt2_cache/
|
||||
.rts2_cache_cjs/
|
||||
.rts2_cache_es/
|
||||
.rts2_cache_umd/
|
||||
|
||||
# Optional REPL history
|
||||
|
||||
.node_repl_history
|
||||
|
||||
# Output of 'npm pack'
|
||||
|
||||
\*.tgz
|
||||
|
||||
# Yarn Integrity file
|
||||
|
||||
.yarn-integrity
|
||||
|
||||
# parcel-bundler cache (https://parceljs.org/)
|
||||
|
||||
.cache
|
||||
.parcel-cache
|
||||
|
||||
# Next.js build output
|
||||
|
||||
.next
|
||||
out
|
||||
|
||||
# Nuxt.js build / generate output
|
||||
|
||||
.nuxt
|
||||
dist
|
||||
|
||||
# Gatsby files
|
||||
|
||||
.cache/
|
||||
|
||||
# Comment in the public line in if your project uses Gatsby and not Next.js
|
||||
|
||||
# https://nextjs.org/blog/next-9-1#public-directory-support
|
||||
|
||||
# public
|
||||
|
||||
# vuepress build output
|
||||
|
||||
.vuepress/dist
|
||||
|
||||
# vuepress v2.x temp and cache directory
|
||||
|
||||
.temp
|
||||
.cache
|
||||
|
||||
# Docusaurus cache and generated files
|
||||
|
||||
.docusaurus
|
||||
|
||||
# Serverless directories
|
||||
|
||||
.serverless/
|
||||
|
||||
# FuseBox cache
|
||||
|
||||
.fusebox/
|
||||
|
||||
# DynamoDB Local files
|
||||
|
||||
.dynamodb/
|
||||
|
||||
# TernJS port file
|
||||
|
||||
.tern-port
|
||||
|
||||
# Stores VSCode versions used for testing VSCode extensions
|
||||
|
||||
.vscode-test
|
||||
|
||||
# yarn v2
|
||||
|
||||
.yarn/cache
|
||||
.yarn/unplugged
|
||||
.yarn/build-state.yml
|
||||
.yarn/install-state.gz
|
||||
.pnp.\*
|
||||
|
||||
# wrangler project
|
||||
|
||||
.dev.vars*
|
||||
!.dev.vars.example
|
||||
.env*
|
||||
!.env.example
|
||||
.wrangler/
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"printWidth": 140,
|
||||
"singleQuote": true,
|
||||
"semi": true,
|
||||
"useTabs": true
|
||||
}
|
||||
Vendored
+5
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"files.associations": {
|
||||
"wrangler.json": "jsonc"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,447 @@
|
||||
{
|
||||
"lockfileVersion": 1,
|
||||
"workspaces": {
|
||||
"": {
|
||||
"name": "r2-worker",
|
||||
"devDependencies": {
|
||||
"@cloudflare/vitest-pool-workers": "^0.8.19",
|
||||
"typescript": "^5.5.2",
|
||||
"vitest": "~3.2.0",
|
||||
"wrangler": "^4.38.0",
|
||||
},
|
||||
},
|
||||
},
|
||||
"packages": {
|
||||
"@cloudflare/kv-asset-handler": ["@cloudflare/kv-asset-handler@0.4.0", "", { "dependencies": { "mime": "^3.0.0" } }, "sha512-+tv3z+SPp+gqTIcImN9o0hqE9xyfQjI1XD9pL6NuKjua9B1y7mNYv0S9cP+QEbA4ppVgGZEmKOvHX5G5Ei1CVA=="],
|
||||
|
||||
"@cloudflare/unenv-preset": ["@cloudflare/unenv-preset@2.7.4", "", { "peerDependencies": { "unenv": "2.0.0-rc.21", "workerd": "^1.20250912.0" }, "optionalPeers": ["workerd"] }, "sha512-KIjbu/Dt50zseJIoOOK5y4eYpSojD9+xxkePYVK1Rg9k/p/st4YyMtz1Clju/zrenJHrOH+AAcjNArOPMwH4Bw=="],
|
||||
|
||||
"@cloudflare/vitest-pool-workers": ["@cloudflare/vitest-pool-workers@0.8.71", "", { "dependencies": { "birpc": "0.2.14", "cjs-module-lexer": "^1.2.3", "devalue": "^5.3.2", "miniflare": "4.20250906.0", "semver": "^7.7.1", "wrangler": "4.35.0", "zod": "^3.22.3" }, "peerDependencies": { "@vitest/runner": "2.0.x - 3.2.x", "@vitest/snapshot": "2.0.x - 3.2.x", "vitest": "2.0.x - 3.2.x" } }, "sha512-keu2HCLQfRNwbmLBCDXJgCFpANTaYnQpE01fBOo4CNwiWHUT7SZGN7w64RKiSWRHyYppStXBuE5Ng7F42+flpg=="],
|
||||
|
||||
"@cloudflare/workerd-darwin-64": ["@cloudflare/workerd-darwin-64@1.20250917.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-0kL/kFnKUSycoo7b3PgM0nRyZ+1MGQAKaXtE6a2+SAeUkZ2FLnuFWmASi0s4rlWGsf/rlTw4AwXROePir9dUcQ=="],
|
||||
|
||||
"@cloudflare/workerd-darwin-arm64": ["@cloudflare/workerd-darwin-arm64@1.20250917.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-3/N1QmEJsC8Byxt1SGgVp5o0r+eKjuUEMbIL2yzLk/jrMdErPXy/DGf/tXZoACU68a/gMEbbT1itkYrm85iQHg=="],
|
||||
|
||||
"@cloudflare/workerd-linux-64": ["@cloudflare/workerd-linux-64@1.20250917.0", "", { "os": "linux", "cpu": "x64" }, "sha512-E7sEow7CErbWY3olMmlbj6iss9r7Xb2uMyc+MKzYC9/J6yFlJd/dNHvjey9QIdxzbkC9qGe90a+KxQrjs+fspA=="],
|
||||
|
||||
"@cloudflare/workerd-linux-arm64": ["@cloudflare/workerd-linux-arm64@1.20250917.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-roOnRjxut2FUxo6HA9spbfs32naXAsnSQqsgku3iq6BYKv1QqGiFoY5bReK72N5uxmhxo7+RiTo8ZEkxA/vMIQ=="],
|
||||
|
||||
"@cloudflare/workerd-windows-64": ["@cloudflare/workerd-windows-64@1.20250917.0", "", { "os": "win32", "cpu": "x64" }, "sha512-gslh6Ou9+kshHjR1BJX47OsbPw3/cZCvGDompvaW/URCgr7aMzljbgmBb7p0uhwGy1qCXcIt31St6pd3IEcLng=="],
|
||||
|
||||
"@cspotcode/source-map-support": ["@cspotcode/source-map-support@0.8.1", "", { "dependencies": { "@jridgewell/trace-mapping": "0.3.9" } }, "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw=="],
|
||||
|
||||
"@emnapi/runtime": ["@emnapi/runtime@1.5.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-97/BJ3iXHww3djw6hYIfErCZFee7qCtrneuLa20UXFCOTCfBM2cvQHjWJ2EG0s0MtdNwInarqCTz35i4wWXHsQ=="],
|
||||
|
||||
"@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.4", "", { "os": "aix", "cpu": "ppc64" }, "sha512-1VCICWypeQKhVbE9oW/sJaAmjLxhVqacdkvPLEjwlttjfwENRSClS8EjBz0KzRyFSCPDIkuXW34Je/vk7zdB7Q=="],
|
||||
|
||||
"@esbuild/android-arm": ["@esbuild/android-arm@0.25.4", "", { "os": "android", "cpu": "arm" }, "sha512-QNdQEps7DfFwE3hXiU4BZeOV68HHzYwGd0Nthhd3uCkkEKK7/R6MTgM0P7H7FAs5pU/DIWsviMmEGxEoxIZ+ZQ=="],
|
||||
|
||||
"@esbuild/android-arm64": ["@esbuild/android-arm64@0.25.4", "", { "os": "android", "cpu": "arm64" }, "sha512-bBy69pgfhMGtCnwpC/x5QhfxAz/cBgQ9enbtwjf6V9lnPI/hMyT9iWpR1arm0l3kttTr4L0KSLpKmLp/ilKS9A=="],
|
||||
|
||||
"@esbuild/android-x64": ["@esbuild/android-x64@0.25.4", "", { "os": "android", "cpu": "x64" }, "sha512-TVhdVtQIFuVpIIR282btcGC2oGQoSfZfmBdTip2anCaVYcqWlZXGcdcKIUklfX2wj0JklNYgz39OBqh2cqXvcQ=="],
|
||||
|
||||
"@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.25.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Y1giCfM4nlHDWEfSckMzeWNdQS31BQGs9/rouw6Ub91tkK79aIMTH3q9xHvzH8d0wDru5Ci0kWB8b3up/nl16g=="],
|
||||
|
||||
"@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.25.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-CJsry8ZGM5VFVeyUYB3cdKpd/H69PYez4eJh1W/t38vzutdjEjtP7hB6eLKBoOdxcAlCtEYHzQ/PJ/oU9I4u0A=="],
|
||||
|
||||
"@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.25.4", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-yYq+39NlTRzU2XmoPW4l5Ifpl9fqSk0nAJYM/V/WUGPEFfek1epLHJIkTQM6bBs1swApjO5nWgvr843g6TjxuQ=="],
|
||||
|
||||
"@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.25.4", "", { "os": "freebsd", "cpu": "x64" }, "sha512-0FgvOJ6UUMflsHSPLzdfDnnBBVoCDtBTVyn/MrWloUNvq/5SFmh13l3dvgRPkDihRxb77Y17MbqbCAa2strMQQ=="],
|
||||
|
||||
"@esbuild/linux-arm": ["@esbuild/linux-arm@0.25.4", "", { "os": "linux", "cpu": "arm" }, "sha512-kro4c0P85GMfFYqW4TWOpvmF8rFShbWGnrLqlzp4X1TNWjRY3JMYUfDCtOxPKOIY8B0WC8HN51hGP4I4hz4AaQ=="],
|
||||
|
||||
"@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.25.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-+89UsQTfXdmjIvZS6nUnOOLoXnkUTB9hR5QAeLrQdzOSWZvNSAXAtcRDHWtqAUtAmv7ZM1WPOOeSxDzzzMogiQ=="],
|
||||
|
||||
"@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.25.4", "", { "os": "linux", "cpu": "ia32" }, "sha512-yTEjoapy8UP3rv8dB0ip3AfMpRbyhSN3+hY8mo/i4QXFeDxmiYbEKp3ZRjBKcOP862Ua4b1PDfwlvbuwY7hIGQ=="],
|
||||
|
||||
"@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.25.4", "", { "os": "linux", "cpu": "none" }, "sha512-NeqqYkrcGzFwi6CGRGNMOjWGGSYOpqwCjS9fvaUlX5s3zwOtn1qwg1s2iE2svBe4Q/YOG1q6875lcAoQK/F4VA=="],
|
||||
|
||||
"@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.25.4", "", { "os": "linux", "cpu": "none" }, "sha512-IcvTlF9dtLrfL/M8WgNI/qJYBENP3ekgsHbYUIzEzq5XJzzVEV/fXY9WFPfEEXmu3ck2qJP8LG/p3Q8f7Zc2Xg=="],
|
||||
|
||||
"@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.25.4", "", { "os": "linux", "cpu": "ppc64" }, "sha512-HOy0aLTJTVtoTeGZh4HSXaO6M95qu4k5lJcH4gxv56iaycfz1S8GO/5Jh6X4Y1YiI0h7cRyLi+HixMR+88swag=="],
|
||||
|
||||
"@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.25.4", "", { "os": "linux", "cpu": "none" }, "sha512-i8JUDAufpz9jOzo4yIShCTcXzS07vEgWzyX3NH2G7LEFVgrLEhjwL3ajFE4fZI3I4ZgiM7JH3GQ7ReObROvSUA=="],
|
||||
|
||||
"@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.25.4", "", { "os": "linux", "cpu": "s390x" }, "sha512-jFnu+6UbLlzIjPQpWCNh5QtrcNfMLjgIavnwPQAfoGx4q17ocOU9MsQ2QVvFxwQoWpZT8DvTLooTvmOQXkO51g=="],
|
||||
|
||||
"@esbuild/linux-x64": ["@esbuild/linux-x64@0.25.4", "", { "os": "linux", "cpu": "x64" }, "sha512-6e0cvXwzOnVWJHq+mskP8DNSrKBr1bULBvnFLpc1KY+d+irZSgZ02TGse5FsafKS5jg2e4pbvK6TPXaF/A6+CA=="],
|
||||
|
||||
"@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.25.4", "", { "os": "none", "cpu": "arm64" }, "sha512-vUnkBYxZW4hL/ie91hSqaSNjulOnYXE1VSLusnvHg2u3jewJBz3YzB9+oCw8DABeVqZGg94t9tyZFoHma8gWZQ=="],
|
||||
|
||||
"@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.25.4", "", { "os": "none", "cpu": "x64" }, "sha512-XAg8pIQn5CzhOB8odIcAm42QsOfa98SBeKUdo4xa8OvX8LbMZqEtgeWE9P/Wxt7MlG2QqvjGths+nq48TrUiKw=="],
|
||||
|
||||
"@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.25.4", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-Ct2WcFEANlFDtp1nVAXSNBPDxyU+j7+tId//iHXU2f/lN5AmO4zLyhDcpR5Cz1r08mVxzt3Jpyt4PmXQ1O6+7A=="],
|
||||
|
||||
"@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.25.4", "", { "os": "openbsd", "cpu": "x64" }, "sha512-xAGGhyOQ9Otm1Xu8NT1ifGLnA6M3sJxZ6ixylb+vIUVzvvd6GOALpwQrYrtlPouMqd/vSbgehz6HaVk4+7Afhw=="],
|
||||
|
||||
"@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.25.10", "", { "os": "none", "cpu": "arm64" }, "sha512-AVTSBhTX8Y/Fz6OmIVBip9tJzZEUcY8WLh7I59+upa5/GPhh2/aM6bvOMQySspnCCHvFi79kMtdJS1w0DXAeag=="],
|
||||
|
||||
"@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.25.4", "", { "os": "sunos", "cpu": "x64" }, "sha512-Mw+tzy4pp6wZEK0+Lwr76pWLjrtjmJyUB23tHKqEDP74R3q95luY/bXqXZeYl4NYlvwOqoRKlInQialgCKy67Q=="],
|
||||
|
||||
"@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.25.4", "", { "os": "win32", "cpu": "arm64" }, "sha512-AVUP428VQTSddguz9dO9ngb+E5aScyg7nOeJDrF1HPYu555gmza3bDGMPhmVXL8svDSoqPCsCPjb265yG/kLKQ=="],
|
||||
|
||||
"@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.25.4", "", { "os": "win32", "cpu": "ia32" }, "sha512-i1sW+1i+oWvQzSgfRcxxG2k4I9n3O9NRqy8U+uugaT2Dy7kLO9Y7wI72haOahxceMX8hZAzgGou1FhndRldxRg=="],
|
||||
|
||||
"@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.4", "", { "os": "win32", "cpu": "x64" }, "sha512-nOT2vZNw6hJ+z43oP1SPea/G/6AbN6X+bGNhNuq8NtRHy4wsMhw765IKLNmnjek7GvjWBYQ8Q5VBoYTFg9y1UQ=="],
|
||||
|
||||
"@img/sharp-darwin-arm64": ["@img/sharp-darwin-arm64@0.33.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-arm64": "1.0.4" }, "os": "darwin", "cpu": "arm64" }, "sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ=="],
|
||||
|
||||
"@img/sharp-darwin-x64": ["@img/sharp-darwin-x64@0.33.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-x64": "1.0.4" }, "os": "darwin", "cpu": "x64" }, "sha512-fyHac4jIc1ANYGRDxtiqelIbdWkIuQaI84Mv45KvGRRxSAa7o7d1ZKAOBaYbnepLC1WqxfpimdeWfvqqSGwR2Q=="],
|
||||
|
||||
"@img/sharp-libvips-darwin-arm64": ["@img/sharp-libvips-darwin-arm64@1.0.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-XblONe153h0O2zuFfTAbQYAX2JhYmDHeWikp1LM9Hul9gVPjFY427k6dFEcOL72O01QxQsWi761svJ/ev9xEDg=="],
|
||||
|
||||
"@img/sharp-libvips-darwin-x64": ["@img/sharp-libvips-darwin-x64@1.0.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-xnGR8YuZYfJGmWPvmlunFaWJsb9T/AO2ykoP3Fz/0X5XV2aoYBPkX6xqCQvUTKKiLddarLaxpzNe+b1hjeWHAQ=="],
|
||||
|
||||
"@img/sharp-libvips-linux-arm": ["@img/sharp-libvips-linux-arm@1.0.5", "", { "os": "linux", "cpu": "arm" }, "sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g=="],
|
||||
|
||||
"@img/sharp-libvips-linux-arm64": ["@img/sharp-libvips-linux-arm64@1.0.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA=="],
|
||||
|
||||
"@img/sharp-libvips-linux-s390x": ["@img/sharp-libvips-linux-s390x@1.0.4", "", { "os": "linux", "cpu": "s390x" }, "sha512-u7Wz6ntiSSgGSGcjZ55im6uvTrOxSIS8/dgoVMoiGE9I6JAfU50yH5BoDlYA1tcuGS7g/QNtetJnxA6QEsCVTA=="],
|
||||
|
||||
"@img/sharp-libvips-linux-x64": ["@img/sharp-libvips-linux-x64@1.0.4", "", { "os": "linux", "cpu": "x64" }, "sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw=="],
|
||||
|
||||
"@img/sharp-libvips-linuxmusl-arm64": ["@img/sharp-libvips-linuxmusl-arm64@1.0.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA=="],
|
||||
|
||||
"@img/sharp-libvips-linuxmusl-x64": ["@img/sharp-libvips-linuxmusl-x64@1.0.4", "", { "os": "linux", "cpu": "x64" }, "sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw=="],
|
||||
|
||||
"@img/sharp-linux-arm": ["@img/sharp-linux-arm@0.33.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm": "1.0.5" }, "os": "linux", "cpu": "arm" }, "sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ=="],
|
||||
|
||||
"@img/sharp-linux-arm64": ["@img/sharp-linux-arm64@0.33.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm64": "1.0.4" }, "os": "linux", "cpu": "arm64" }, "sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA=="],
|
||||
|
||||
"@img/sharp-linux-s390x": ["@img/sharp-linux-s390x@0.33.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-s390x": "1.0.4" }, "os": "linux", "cpu": "s390x" }, "sha512-y/5PCd+mP4CA/sPDKl2961b+C9d+vPAveS33s6Z3zfASk2j5upL6fXVPZi7ztePZ5CuH+1kW8JtvxgbuXHRa4Q=="],
|
||||
|
||||
"@img/sharp-linux-x64": ["@img/sharp-linux-x64@0.33.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-x64": "1.0.4" }, "os": "linux", "cpu": "x64" }, "sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA=="],
|
||||
|
||||
"@img/sharp-linuxmusl-arm64": ["@img/sharp-linuxmusl-arm64@0.33.5", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-arm64": "1.0.4" }, "os": "linux", "cpu": "arm64" }, "sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g=="],
|
||||
|
||||
"@img/sharp-linuxmusl-x64": ["@img/sharp-linuxmusl-x64@0.33.5", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-x64": "1.0.4" }, "os": "linux", "cpu": "x64" }, "sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw=="],
|
||||
|
||||
"@img/sharp-wasm32": ["@img/sharp-wasm32@0.33.5", "", { "dependencies": { "@emnapi/runtime": "^1.2.0" }, "cpu": "none" }, "sha512-ykUW4LVGaMcU9lu9thv85CbRMAwfeadCJHRsg2GmeRa/cJxsVY9Rbd57JcMxBkKHag5U/x7TSBpScF4U8ElVzg=="],
|
||||
|
||||
"@img/sharp-win32-ia32": ["@img/sharp-win32-ia32@0.33.5", "", { "os": "win32", "cpu": "ia32" }, "sha512-T36PblLaTwuVJ/zw/LaH0PdZkRz5rd3SmMHX8GSmR7vtNSP5Z6bQkExdSK7xGWyxLw4sUknBuugTelgw2faBbQ=="],
|
||||
|
||||
"@img/sharp-win32-x64": ["@img/sharp-win32-x64@0.33.5", "", { "os": "win32", "cpu": "x64" }, "sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg=="],
|
||||
|
||||
"@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="],
|
||||
|
||||
"@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="],
|
||||
|
||||
"@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.9", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.0.3", "@jridgewell/sourcemap-codec": "^1.4.10" } }, "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ=="],
|
||||
|
||||
"@poppinss/colors": ["@poppinss/colors@4.1.5", "", { "dependencies": { "kleur": "^4.1.5" } }, "sha512-FvdDqtcRCtz6hThExcFOgW0cWX+xwSMWcRuQe5ZEb2m7cVQOAVZOIMt+/v9RxGiD9/OY16qJBXK4CVKWAPalBw=="],
|
||||
|
||||
"@poppinss/dumper": ["@poppinss/dumper@0.6.4", "", { "dependencies": { "@poppinss/colors": "^4.1.5", "@sindresorhus/is": "^7.0.2", "supports-color": "^10.0.0" } }, "sha512-iG0TIdqv8xJ3Lt9O8DrPRxw1MRLjNpoqiSGU03P/wNLP/s0ra0udPJ1J2Tx5M0J3H/cVyEgpbn8xUKRY9j59kQ=="],
|
||||
|
||||
"@poppinss/exception": ["@poppinss/exception@1.2.2", "", {}, "sha512-m7bpKCD4QMlFCjA/nKTs23fuvoVFoA83brRKmObCUNmi/9tVu8Ve3w4YQAnJu4q3Tjf5fr685HYIC/IA2zHRSg=="],
|
||||
|
||||
"@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.52.0", "", { "os": "android", "cpu": "arm" }, "sha512-VxDYCDqOaR7NXzAtvRx7G1u54d2kEHopb28YH/pKzY6y0qmogP3gG7CSiWsq9WvDFxOQMpNEyjVAHZFXfH3o/A=="],
|
||||
|
||||
"@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.52.0", "", { "os": "android", "cpu": "arm64" }, "sha512-pqDirm8koABIKvzL59YI9W9DWbRlTX7RWhN+auR8HXJxo89m4mjqbah7nJZjeKNTNYopqL+yGg+0mhCpf3xZtQ=="],
|
||||
|
||||
"@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.52.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-YCdWlY/8ltN6H78HnMsRHYlPiKvqKagBP1r+D7SSylxX+HnsgXGCmLiV3Y4nSyY9hW8qr8U9LDUx/Lo7M6MfmQ=="],
|
||||
|
||||
"@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.52.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-z4nw6y1j+OOSGzuVbSWdIp1IUks9qNw4dc7z7lWuWDKojY38VMWBlEN7F9jk5UXOkUcp97vA1N213DF+Lz8BRg=="],
|
||||
|
||||
"@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.52.0", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-Q/dv9Yvyr5rKlK8WQJZVrp5g2SOYeZUs9u/t2f9cQ2E0gJjYB/BWoedXfUT0EcDJefi2zzVfhcOj8drWCzTviw=="],
|
||||
|
||||
"@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.52.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-kdBsLs4Uile/fbjZVvCRcKB4q64R+1mUq0Yd7oU1CMm1Av336ajIFqNFovByipciuUQjBCPMxwJhCgfG2re3rg=="],
|
||||
|
||||
"@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.52.0", "", { "os": "linux", "cpu": "arm" }, "sha512-aL6hRwu0k7MTUESgkg7QHY6CoqPgr6gdQXRJI1/VbFlUMwsSzPGSR7sG5d+MCbYnJmJwThc2ol3nixj1fvI/zQ=="],
|
||||
|
||||
"@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.52.0", "", { "os": "linux", "cpu": "arm" }, "sha512-BTs0M5s1EJejgIBJhCeiFo7GZZ2IXWkFGcyZhxX4+8usnIo5Mti57108vjXFIQmmJaRyDwmV59Tw64Ap1dkwMw=="],
|
||||
|
||||
"@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.52.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-uj672IVOU9m08DBGvoPKPi/J8jlVgjh12C9GmjjBxCTQc3XtVmRkRKyeHSmIKQpvJ7fIm1EJieBUcnGSzDVFyw=="],
|
||||
|
||||
"@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.52.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-/+IVbeDMDCtB/HP/wiWsSzduD10SEGzIZX2945KSgZRNi4TSkjHqRJtNTVtVb8IRwhJ65ssI56krlLik+zFWkw=="],
|
||||
|
||||
"@rollup/rollup-linux-loong64-gnu": ["@rollup/rollup-linux-loong64-gnu@4.52.0", "", { "os": "linux", "cpu": "none" }, "sha512-U1vVzvSWtSMWKKrGoROPBXMh3Vwn93TA9V35PldokHGqiUbF6erSzox/5qrSMKp6SzakvyjcPiVF8yB1xKr9Pg=="],
|
||||
|
||||
"@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.52.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-X/4WfuBAdQRH8cK3DYl8zC00XEE6aM472W+QCycpQJeLWVnHfkv7RyBFVaTqNUMsTgIX8ihMjCvFF9OUgeABzw=="],
|
||||
|
||||
"@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.52.0", "", { "os": "linux", "cpu": "none" }, "sha512-xIRYc58HfWDBZoLmWfWXg2Sq8VCa2iJ32B7mqfWnkx5mekekl0tMe7FHpY8I72RXEcUkaWawRvl3qA55og+cwQ=="],
|
||||
|
||||
"@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.52.0", "", { "os": "linux", "cpu": "none" }, "sha512-mbsoUey05WJIOz8U1WzNdf+6UMYGwE3fZZnQqsM22FZ3wh1N887HT6jAOjXs6CNEK3Ntu2OBsyQDXfIjouI4dw=="],
|
||||
|
||||
"@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.52.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-qP6aP970bucEi5KKKR4AuPFd8aTx9EF6BvutvYxmZuWLJHmnq4LvBfp0U+yFDMGwJ+AIJEH5sIP+SNypauMWzg=="],
|
||||
|
||||
"@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.52.0", "", { "os": "linux", "cpu": "x64" }, "sha512-nmSVN+F2i1yKZ7rJNKO3G7ZzmxJgoQBQZ/6c4MuS553Grmr7WqR7LLDcYG53Z2m9409z3JLt4sCOhLdbKQ3HmA=="],
|
||||
|
||||
"@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.52.0", "", { "os": "linux", "cpu": "x64" }, "sha512-2d0qRo33G6TfQVjaMR71P+yJVGODrt5V6+T0BDYH4EMfGgdC/2HWDVjSSFw888GSzAZUwuska3+zxNUCDco6rQ=="],
|
||||
|
||||
"@rollup/rollup-openharmony-arm64": ["@rollup/rollup-openharmony-arm64@4.52.0", "", { "os": "none", "cpu": "arm64" }, "sha512-A1JalX4MOaFAAyGgpO7XP5khquv/7xKzLIyLmhNrbiCxWpMlnsTYr8dnsWM7sEeotNmxvSOEL7F65j0HXFcFsw=="],
|
||||
|
||||
"@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.52.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-YQugafP/rH0eOOHGjmNgDURrpYHrIX0yuojOI8bwCyXwxC9ZdTd3vYkmddPX0oHONLXu9Rb1dDmT0VNpjkzGGw=="],
|
||||
|
||||
"@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.52.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-zYdUYhi3Qe2fndujBqL5FjAFzvNeLxtIqfzNEVKD1I7C37/chv1VxhscWSQHTNfjPCrBFQMnynwA3kpZpZ8w4A=="],
|
||||
|
||||
"@rollup/rollup-win32-x64-gnu": ["@rollup/rollup-win32-x64-gnu@4.52.0", "", { "os": "win32", "cpu": "x64" }, "sha512-fGk03kQylNaCOQ96HDMeT7E2n91EqvCDd3RwvT5k+xNdFCeMGnj5b5hEgTGrQuyidqSsD3zJDQ21QIaxXqTBJw=="],
|
||||
|
||||
"@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.52.0", "", { "os": "win32", "cpu": "x64" }, "sha512-6iKDCVSIUQ8jPMoIV0OytRKniaYyy5EbY/RRydmLW8ZR3cEBhxbWl5ro0rkUNe0ef6sScvhbY79HrjRm8i3vDQ=="],
|
||||
|
||||
"@sindresorhus/is": ["@sindresorhus/is@7.1.0", "", {}, "sha512-7F/yz2IphV39hiS2zB4QYVkivrptHHh0K8qJJd9HhuWSdvf8AN7NpebW3CcDZDBQsUPMoDKWsY2WWgW7bqOcfA=="],
|
||||
|
||||
"@speed-highlight/core": ["@speed-highlight/core@1.2.7", "", {}, "sha512-0dxmVj4gxg3Jg879kvFS/msl4s9F3T9UXC1InxgOf7t5NvcPD97u/WTA5vL/IxWHMn7qSxBozqrnnE2wvl1m8g=="],
|
||||
|
||||
"@types/chai": ["@types/chai@5.2.2", "", { "dependencies": { "@types/deep-eql": "*" } }, "sha512-8kB30R7Hwqf40JPiKhVzodJs2Qc1ZJ5zuT3uzw5Hq/dhNCl3G3l83jfpdI1e20BP348+fV7VIL/+FxaXkqBmWg=="],
|
||||
|
||||
"@types/deep-eql": ["@types/deep-eql@4.0.2", "", {}, "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw=="],
|
||||
|
||||
"@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="],
|
||||
|
||||
"@vitest/expect": ["@vitest/expect@3.2.4", "", { "dependencies": { "@types/chai": "^5.2.2", "@vitest/spy": "3.2.4", "@vitest/utils": "3.2.4", "chai": "^5.2.0", "tinyrainbow": "^2.0.0" } }, "sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig=="],
|
||||
|
||||
"@vitest/mocker": ["@vitest/mocker@3.2.4", "", { "dependencies": { "@vitest/spy": "3.2.4", "estree-walker": "^3.0.3", "magic-string": "^0.30.17" }, "peerDependencies": { "msw": "^2.4.9", "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" }, "optionalPeers": ["msw", "vite"] }, "sha512-46ryTE9RZO/rfDd7pEqFl7etuyzekzEhUbTW3BvmeO/BcCMEgq59BKhek3dXDWgAj4oMK6OZi+vRr1wPW6qjEQ=="],
|
||||
|
||||
"@vitest/pretty-format": ["@vitest/pretty-format@3.2.4", "", { "dependencies": { "tinyrainbow": "^2.0.0" } }, "sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA=="],
|
||||
|
||||
"@vitest/runner": ["@vitest/runner@3.2.4", "", { "dependencies": { "@vitest/utils": "3.2.4", "pathe": "^2.0.3", "strip-literal": "^3.0.0" } }, "sha512-oukfKT9Mk41LreEW09vt45f8wx7DordoWUZMYdY/cyAk7w5TWkTRCNZYF7sX7n2wB7jyGAl74OxgwhPgKaqDMQ=="],
|
||||
|
||||
"@vitest/snapshot": ["@vitest/snapshot@3.2.4", "", { "dependencies": { "@vitest/pretty-format": "3.2.4", "magic-string": "^0.30.17", "pathe": "^2.0.3" } }, "sha512-dEYtS7qQP2CjU27QBC5oUOxLE/v5eLkGqPE0ZKEIDGMs4vKWe7IjgLOeauHsR0D5YuuycGRO5oSRXnwnmA78fQ=="],
|
||||
|
||||
"@vitest/spy": ["@vitest/spy@3.2.4", "", { "dependencies": { "tinyspy": "^4.0.3" } }, "sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw=="],
|
||||
|
||||
"@vitest/utils": ["@vitest/utils@3.2.4", "", { "dependencies": { "@vitest/pretty-format": "3.2.4", "loupe": "^3.1.4", "tinyrainbow": "^2.0.0" } }, "sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA=="],
|
||||
|
||||
"acorn": ["acorn@8.14.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA=="],
|
||||
|
||||
"acorn-walk": ["acorn-walk@8.3.2", "", {}, "sha512-cjkyv4OtNCIeqhHrfS81QWXoCBPExR/J62oyEqepVw8WaQeSqpW2uhuLPh1m9eWhDuOo/jUXVTlifvesOWp/4A=="],
|
||||
|
||||
"assertion-error": ["assertion-error@2.0.1", "", {}, "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA=="],
|
||||
|
||||
"birpc": ["birpc@0.2.14", "", {}, "sha512-37FHE8rqsYM5JEKCnXFyHpBCzvgHEExwVVTq+nUmloInU7l8ezD1TpOhKpS8oe1DTYFqEK27rFZVKG43oTqXRA=="],
|
||||
|
||||
"blake3-wasm": ["blake3-wasm@2.1.5", "", {}, "sha512-F1+K8EbfOZE49dtoPtmxUQrpXaBIl3ICvasLh+nJta0xkz+9kF/7uet9fLnwKqhDrmj6g+6K3Tw9yQPUg2ka5g=="],
|
||||
|
||||
"cac": ["cac@6.7.14", "", {}, "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ=="],
|
||||
|
||||
"chai": ["chai@5.3.3", "", { "dependencies": { "assertion-error": "^2.0.1", "check-error": "^2.1.1", "deep-eql": "^5.0.1", "loupe": "^3.1.0", "pathval": "^2.0.0" } }, "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw=="],
|
||||
|
||||
"check-error": ["check-error@2.1.1", "", {}, "sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw=="],
|
||||
|
||||
"cjs-module-lexer": ["cjs-module-lexer@1.4.3", "", {}, "sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q=="],
|
||||
|
||||
"color": ["color@4.2.3", "", { "dependencies": { "color-convert": "^2.0.1", "color-string": "^1.9.0" } }, "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A=="],
|
||||
|
||||
"color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="],
|
||||
|
||||
"color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="],
|
||||
|
||||
"color-string": ["color-string@1.9.1", "", { "dependencies": { "color-name": "^1.0.0", "simple-swizzle": "^0.2.2" } }, "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg=="],
|
||||
|
||||
"cookie": ["cookie@1.0.2", "", {}, "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA=="],
|
||||
|
||||
"debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
|
||||
|
||||
"deep-eql": ["deep-eql@5.0.2", "", {}, "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q=="],
|
||||
|
||||
"defu": ["defu@6.1.4", "", {}, "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg=="],
|
||||
|
||||
"detect-libc": ["detect-libc@2.1.0", "", {}, "sha512-vEtk+OcP7VBRtQZ1EJ3bdgzSfBjgnEalLTp5zjJrS+2Z1w2KZly4SBdac/WDU3hhsNAZ9E8SC96ME4Ey8MZ7cg=="],
|
||||
|
||||
"devalue": ["devalue@5.3.2", "", {}, "sha512-UDsjUbpQn9kvm68slnrs+mfxwFkIflOhkanmyabZ8zOYk8SMEIbJ3TK+88g70hSIeytu4y18f0z/hYHMTrXIWw=="],
|
||||
|
||||
"error-stack-parser-es": ["error-stack-parser-es@1.0.5", "", {}, "sha512-5qucVt2XcuGMcEGgWI7i+yZpmpByQ8J1lHhcL7PwqCwu9FPP3VUXzT4ltHe5i2z9dePwEHcDVOAfSnHsOlCXRA=="],
|
||||
|
||||
"es-module-lexer": ["es-module-lexer@1.7.0", "", {}, "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA=="],
|
||||
|
||||
"esbuild": ["esbuild@0.25.4", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.4", "@esbuild/android-arm": "0.25.4", "@esbuild/android-arm64": "0.25.4", "@esbuild/android-x64": "0.25.4", "@esbuild/darwin-arm64": "0.25.4", "@esbuild/darwin-x64": "0.25.4", "@esbuild/freebsd-arm64": "0.25.4", "@esbuild/freebsd-x64": "0.25.4", "@esbuild/linux-arm": "0.25.4", "@esbuild/linux-arm64": "0.25.4", "@esbuild/linux-ia32": "0.25.4", "@esbuild/linux-loong64": "0.25.4", "@esbuild/linux-mips64el": "0.25.4", "@esbuild/linux-ppc64": "0.25.4", "@esbuild/linux-riscv64": "0.25.4", "@esbuild/linux-s390x": "0.25.4", "@esbuild/linux-x64": "0.25.4", "@esbuild/netbsd-arm64": "0.25.4", "@esbuild/netbsd-x64": "0.25.4", "@esbuild/openbsd-arm64": "0.25.4", "@esbuild/openbsd-x64": "0.25.4", "@esbuild/sunos-x64": "0.25.4", "@esbuild/win32-arm64": "0.25.4", "@esbuild/win32-ia32": "0.25.4", "@esbuild/win32-x64": "0.25.4" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-8pgjLUcUjcgDg+2Q4NYXnPbo/vncAY4UmyaCm0jZevERqCHZIaWwdJHkf8XQtu4AxSKCdvrUbT0XUr1IdZzI8Q=="],
|
||||
|
||||
"estree-walker": ["estree-walker@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g=="],
|
||||
|
||||
"exit-hook": ["exit-hook@2.2.1", "", {}, "sha512-eNTPlAD67BmP31LDINZ3U7HSF8l57TxOY2PmBJ1shpCvpnxBF93mWCE8YHBnXs8qiUZJc9WDcWIeC3a2HIAMfw=="],
|
||||
|
||||
"expect-type": ["expect-type@1.2.2", "", {}, "sha512-JhFGDVJ7tmDJItKhYgJCGLOWjuK9vPxiXoUFLwLDc99NlmklilbiQJwoctZtt13+xMw91MCk/REan6MWHqDjyA=="],
|
||||
|
||||
"exsolve": ["exsolve@1.0.7", "", {}, "sha512-VO5fQUzZtI6C+vx4w/4BWJpg3s/5l+6pRQEHzFRM8WFi4XffSP1Z+4qi7GbjWbvRQEbdIco5mIMq+zX4rPuLrw=="],
|
||||
|
||||
"fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="],
|
||||
|
||||
"fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
|
||||
|
||||
"glob-to-regexp": ["glob-to-regexp@0.4.1", "", {}, "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw=="],
|
||||
|
||||
"is-arrayish": ["is-arrayish@0.3.4", "", {}, "sha512-m6UrgzFVUYawGBh1dUsWR5M2Clqic9RVXC/9f8ceNlv2IcO9j9J/z8UoCLPqtsPBFNzEpfR3xftohbfqDx8EQA=="],
|
||||
|
||||
"js-tokens": ["js-tokens@9.0.1", "", {}, "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ=="],
|
||||
|
||||
"kleur": ["kleur@4.1.5", "", {}, "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ=="],
|
||||
|
||||
"loupe": ["loupe@3.2.1", "", {}, "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ=="],
|
||||
|
||||
"magic-string": ["magic-string@0.30.19", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-2N21sPY9Ws53PZvsEpVtNuSW+ScYbQdp4b9qUaL+9QkHUrGFKo56Lg9Emg5s9V/qrtNBmiR01sYhUOwu3H+VOw=="],
|
||||
|
||||
"mime": ["mime@3.0.0", "", { "bin": { "mime": "cli.js" } }, "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A=="],
|
||||
|
||||
"miniflare": ["miniflare@4.20250906.0", "", { "dependencies": { "@cspotcode/source-map-support": "0.8.1", "acorn": "8.14.0", "acorn-walk": "8.3.2", "exit-hook": "2.2.1", "glob-to-regexp": "0.4.1", "sharp": "^0.33.5", "stoppable": "1.1.0", "undici": "^7.10.0", "workerd": "1.20250906.0", "ws": "8.18.0", "youch": "4.1.0-beta.10", "zod": "3.22.3" }, "bin": { "miniflare": "bootstrap.js" } }, "sha512-T/RWn1sa0ien80s6NjU+Un/tj12gR6wqScZoiLeMJDD4/fK0UXfnbWXJDubnUED8Xjm7RPQ5ESYdE+mhPmMtuQ=="],
|
||||
|
||||
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
|
||||
|
||||
"nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="],
|
||||
|
||||
"ohash": ["ohash@2.0.11", "", {}, "sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ=="],
|
||||
|
||||
"path-to-regexp": ["path-to-regexp@6.3.0", "", {}, "sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ=="],
|
||||
|
||||
"pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="],
|
||||
|
||||
"pathval": ["pathval@2.0.1", "", {}, "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ=="],
|
||||
|
||||
"picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
|
||||
|
||||
"picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="],
|
||||
|
||||
"postcss": ["postcss@8.5.6", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg=="],
|
||||
|
||||
"rollup": ["rollup@4.52.0", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.52.0", "@rollup/rollup-android-arm64": "4.52.0", "@rollup/rollup-darwin-arm64": "4.52.0", "@rollup/rollup-darwin-x64": "4.52.0", "@rollup/rollup-freebsd-arm64": "4.52.0", "@rollup/rollup-freebsd-x64": "4.52.0", "@rollup/rollup-linux-arm-gnueabihf": "4.52.0", "@rollup/rollup-linux-arm-musleabihf": "4.52.0", "@rollup/rollup-linux-arm64-gnu": "4.52.0", "@rollup/rollup-linux-arm64-musl": "4.52.0", "@rollup/rollup-linux-loong64-gnu": "4.52.0", "@rollup/rollup-linux-ppc64-gnu": "4.52.0", "@rollup/rollup-linux-riscv64-gnu": "4.52.0", "@rollup/rollup-linux-riscv64-musl": "4.52.0", "@rollup/rollup-linux-s390x-gnu": "4.52.0", "@rollup/rollup-linux-x64-gnu": "4.52.0", "@rollup/rollup-linux-x64-musl": "4.52.0", "@rollup/rollup-openharmony-arm64": "4.52.0", "@rollup/rollup-win32-arm64-msvc": "4.52.0", "@rollup/rollup-win32-ia32-msvc": "4.52.0", "@rollup/rollup-win32-x64-gnu": "4.52.0", "@rollup/rollup-win32-x64-msvc": "4.52.0", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-+IuescNkTJQgX7AkIDtITipZdIGcWF0pnVvZTWStiazUmcGA2ag8dfg0urest2XlXUi9kuhfQ+qmdc5Stc3z7g=="],
|
||||
|
||||
"semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="],
|
||||
|
||||
"sharp": ["sharp@0.33.5", "", { "dependencies": { "color": "^4.2.3", "detect-libc": "^2.0.3", "semver": "^7.6.3" }, "optionalDependencies": { "@img/sharp-darwin-arm64": "0.33.5", "@img/sharp-darwin-x64": "0.33.5", "@img/sharp-libvips-darwin-arm64": "1.0.4", "@img/sharp-libvips-darwin-x64": "1.0.4", "@img/sharp-libvips-linux-arm": "1.0.5", "@img/sharp-libvips-linux-arm64": "1.0.4", "@img/sharp-libvips-linux-s390x": "1.0.4", "@img/sharp-libvips-linux-x64": "1.0.4", "@img/sharp-libvips-linuxmusl-arm64": "1.0.4", "@img/sharp-libvips-linuxmusl-x64": "1.0.4", "@img/sharp-linux-arm": "0.33.5", "@img/sharp-linux-arm64": "0.33.5", "@img/sharp-linux-s390x": "0.33.5", "@img/sharp-linux-x64": "0.33.5", "@img/sharp-linuxmusl-arm64": "0.33.5", "@img/sharp-linuxmusl-x64": "0.33.5", "@img/sharp-wasm32": "0.33.5", "@img/sharp-win32-ia32": "0.33.5", "@img/sharp-win32-x64": "0.33.5" } }, "sha512-haPVm1EkS9pgvHrQ/F3Xy+hgcuMV0Wm9vfIBSiwZ05k+xgb0PkBQpGsAA/oWdDobNaZTH5ppvHtzCFbnSEwHVw=="],
|
||||
|
||||
"siginfo": ["siginfo@2.0.0", "", {}, "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g=="],
|
||||
|
||||
"simple-swizzle": ["simple-swizzle@0.2.4", "", { "dependencies": { "is-arrayish": "^0.3.1" } }, "sha512-nAu1WFPQSMNr2Zn9PGSZK9AGn4t/y97lEm+MXTtUDwfP0ksAIX4nO+6ruD9Jwut4C49SB1Ws+fbXsm/yScWOHw=="],
|
||||
|
||||
"source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
|
||||
|
||||
"stackback": ["stackback@0.0.2", "", {}, "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw=="],
|
||||
|
||||
"std-env": ["std-env@3.9.0", "", {}, "sha512-UGvjygr6F6tpH7o2qyqR6QYpwraIjKSdtzyBdyytFOHmPZY917kwdwLG0RbOjWOnKmnm3PeHjaoLLMie7kPLQw=="],
|
||||
|
||||
"stoppable": ["stoppable@1.1.0", "", {}, "sha512-KXDYZ9dszj6bzvnEMRYvxgeTHU74QBFL54XKtP3nyMuJ81CFYtABZ3bAzL2EdFUaEwJOBOgENyFj3R7oTzDyyw=="],
|
||||
|
||||
"strip-literal": ["strip-literal@3.0.0", "", { "dependencies": { "js-tokens": "^9.0.1" } }, "sha512-TcccoMhJOM3OebGhSBEmp3UZ2SfDMZUEBdRA/9ynfLi8yYajyWX3JiXArcJt4Umh4vISpspkQIY8ZZoCqjbviA=="],
|
||||
|
||||
"supports-color": ["supports-color@10.2.2", "", {}, "sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g=="],
|
||||
|
||||
"tinybench": ["tinybench@2.9.0", "", {}, "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg=="],
|
||||
|
||||
"tinyexec": ["tinyexec@0.3.2", "", {}, "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA=="],
|
||||
|
||||
"tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="],
|
||||
|
||||
"tinypool": ["tinypool@1.1.1", "", {}, "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg=="],
|
||||
|
||||
"tinyrainbow": ["tinyrainbow@2.0.0", "", {}, "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw=="],
|
||||
|
||||
"tinyspy": ["tinyspy@4.0.4", "", {}, "sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q=="],
|
||||
|
||||
"tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
|
||||
|
||||
"typescript": ["typescript@5.9.2", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A=="],
|
||||
|
||||
"ufo": ["ufo@1.6.1", "", {}, "sha512-9a4/uxlTWJ4+a5i0ooc1rU7C7YOw3wT+UGqdeNNHWnOF9qcMBgLRS+4IYUqbczewFx4mLEig6gawh7X6mFlEkA=="],
|
||||
|
||||
"undici": ["undici@7.16.0", "", {}, "sha512-QEg3HPMll0o3t2ourKwOeUAZ159Kn9mx5pnzHRQO8+Wixmh88YdZRiIwat0iNzNNXn0yoEtXJqFpyW7eM8BV7g=="],
|
||||
|
||||
"unenv": ["unenv@2.0.0-rc.21", "", { "dependencies": { "defu": "^6.1.4", "exsolve": "^1.0.7", "ohash": "^2.0.11", "pathe": "^2.0.3", "ufo": "^1.6.1" } }, "sha512-Wj7/AMtE9MRnAXa6Su3Lk0LNCfqDYgfwVjwRFVum9U7wsto1imuHqk4kTm7Jni+5A0Hn7dttL6O/zjvUvoo+8A=="],
|
||||
|
||||
"vite": ["vite@7.1.6", "", { "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-SRYIB8t/isTwNn8vMB3MR6E+EQZM/WG1aKmmIUCfDXfVvKfc20ZpamngWHKzAmmu9ppsgxsg4b2I7c90JZudIQ=="],
|
||||
|
||||
"vite-node": ["vite-node@3.2.4", "", { "dependencies": { "cac": "^6.7.14", "debug": "^4.4.1", "es-module-lexer": "^1.7.0", "pathe": "^2.0.3", "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" }, "bin": { "vite-node": "vite-node.mjs" } }, "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg=="],
|
||||
|
||||
"vitest": ["vitest@3.2.4", "", { "dependencies": { "@types/chai": "^5.2.2", "@vitest/expect": "3.2.4", "@vitest/mocker": "3.2.4", "@vitest/pretty-format": "^3.2.4", "@vitest/runner": "3.2.4", "@vitest/snapshot": "3.2.4", "@vitest/spy": "3.2.4", "@vitest/utils": "3.2.4", "chai": "^5.2.0", "debug": "^4.4.1", "expect-type": "^1.2.1", "magic-string": "^0.30.17", "pathe": "^2.0.3", "picomatch": "^4.0.2", "std-env": "^3.9.0", "tinybench": "^2.9.0", "tinyexec": "^0.3.2", "tinyglobby": "^0.2.14", "tinypool": "^1.1.1", "tinyrainbow": "^2.0.0", "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0", "vite-node": "3.2.4", "why-is-node-running": "^2.3.0" }, "peerDependencies": { "@edge-runtime/vm": "*", "@types/debug": "^4.1.12", "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "@vitest/browser": "3.2.4", "@vitest/ui": "3.2.4", "happy-dom": "*", "jsdom": "*" }, "optionalPeers": ["@edge-runtime/vm", "@types/debug", "@types/node", "@vitest/browser", "@vitest/ui", "happy-dom", "jsdom"], "bin": { "vitest": "vitest.mjs" } }, "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A=="],
|
||||
|
||||
"why-is-node-running": ["why-is-node-running@2.3.0", "", { "dependencies": { "siginfo": "^2.0.0", "stackback": "0.0.2" }, "bin": { "why-is-node-running": "cli.js" } }, "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w=="],
|
||||
|
||||
"workerd": ["workerd@1.20250917.0", "", { "optionalDependencies": { "@cloudflare/workerd-darwin-64": "1.20250917.0", "@cloudflare/workerd-darwin-arm64": "1.20250917.0", "@cloudflare/workerd-linux-64": "1.20250917.0", "@cloudflare/workerd-linux-arm64": "1.20250917.0", "@cloudflare/workerd-windows-64": "1.20250917.0" }, "bin": { "workerd": "bin/workerd" } }, "sha512-0D+wWaccyYQb2Zx2DZDC77YDn9kOpkpGMCgyKgIHilghut5hBQ/adUIEseS4iuIZxBPeFSn6zFtICP0SxZ3z0g=="],
|
||||
|
||||
"wrangler": ["wrangler@4.38.0", "", { "dependencies": { "@cloudflare/kv-asset-handler": "0.4.0", "@cloudflare/unenv-preset": "2.7.4", "blake3-wasm": "2.1.5", "esbuild": "0.25.4", "miniflare": "4.20250917.0", "path-to-regexp": "6.3.0", "unenv": "2.0.0-rc.21", "workerd": "1.20250917.0" }, "optionalDependencies": { "fsevents": "~2.3.2" }, "peerDependencies": { "@cloudflare/workers-types": "^4.20250917.0" }, "optionalPeers": ["@cloudflare/workers-types"], "bin": { "wrangler": "bin/wrangler.js", "wrangler2": "bin/wrangler.js" } }, "sha512-ITL4VZ4KWs8LMDEttDTrAKLktwtv1NxHBd5QIqHOczvcjnAQr+GQoE6XYQws+w8jlOjDV7KyvbFqAdyRh5om3g=="],
|
||||
|
||||
"ws": ["ws@8.18.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw=="],
|
||||
|
||||
"youch": ["youch@4.1.0-beta.10", "", { "dependencies": { "@poppinss/colors": "^4.1.5", "@poppinss/dumper": "^0.6.4", "@speed-highlight/core": "^1.2.7", "cookie": "^1.0.2", "youch-core": "^0.3.3" } }, "sha512-rLfVLB4FgQneDr0dv1oddCVZmKjcJ6yX6mS4pU82Mq/Dt9a3cLZQ62pDBL4AUO+uVrCvtWz3ZFUL2HFAFJ/BXQ=="],
|
||||
|
||||
"youch-core": ["youch-core@0.3.3", "", { "dependencies": { "@poppinss/exception": "^1.2.2", "error-stack-parser-es": "^1.0.5" } }, "sha512-ho7XuGjLaJ2hWHoK8yFnsUGy2Y5uDpqSTq1FkHLK4/oqKtyUU1AFbOOxY4IpC9f0fTLjwYbslUz0Po5BpD1wrA=="],
|
||||
|
||||
"zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],
|
||||
|
||||
"@cloudflare/vitest-pool-workers/wrangler": ["wrangler@4.35.0", "", { "dependencies": { "@cloudflare/kv-asset-handler": "0.4.0", "@cloudflare/unenv-preset": "2.7.3", "blake3-wasm": "2.1.5", "esbuild": "0.25.4", "miniflare": "4.20250906.0", "path-to-regexp": "6.3.0", "unenv": "2.0.0-rc.21", "workerd": "1.20250906.0" }, "optionalDependencies": { "fsevents": "~2.3.2" }, "peerDependencies": { "@cloudflare/workers-types": "^4.20250906.0" }, "optionalPeers": ["@cloudflare/workers-types"], "bin": { "wrangler": "bin/wrangler.js", "wrangler2": "bin/wrangler.js" } }, "sha512-HbyXtbrh4Fi3mU8ussY85tVdQ74qpVS1vctUgaPc+bPrXBTqfDLkZ6VRtHAVF/eBhz4SFmhJtCQpN1caY2Ak8A=="],
|
||||
|
||||
"miniflare/workerd": ["workerd@1.20250906.0", "", { "optionalDependencies": { "@cloudflare/workerd-darwin-64": "1.20250906.0", "@cloudflare/workerd-darwin-arm64": "1.20250906.0", "@cloudflare/workerd-linux-64": "1.20250906.0", "@cloudflare/workerd-linux-arm64": "1.20250906.0", "@cloudflare/workerd-windows-64": "1.20250906.0" }, "bin": { "workerd": "bin/workerd" } }, "sha512-ryVyEaqXPPsr/AxccRmYZZmDAkfQVjhfRqrNTlEeN8aftBk6Ca1u7/VqmfOayjCXrA+O547TauebU+J3IpvFXw=="],
|
||||
|
||||
"miniflare/zod": ["zod@3.22.3", "", {}, "sha512-EjIevzuJRiRPbVH4mGc8nApb/lVLKVpmUhAaR5R5doKGfAnGJ6Gr3CViAVjP+4FWSxCsybeWQdcgCtbX+7oZug=="],
|
||||
|
||||
"vite/esbuild": ["esbuild@0.25.10", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.10", "@esbuild/android-arm": "0.25.10", "@esbuild/android-arm64": "0.25.10", "@esbuild/android-x64": "0.25.10", "@esbuild/darwin-arm64": "0.25.10", "@esbuild/darwin-x64": "0.25.10", "@esbuild/freebsd-arm64": "0.25.10", "@esbuild/freebsd-x64": "0.25.10", "@esbuild/linux-arm": "0.25.10", "@esbuild/linux-arm64": "0.25.10", "@esbuild/linux-ia32": "0.25.10", "@esbuild/linux-loong64": "0.25.10", "@esbuild/linux-mips64el": "0.25.10", "@esbuild/linux-ppc64": "0.25.10", "@esbuild/linux-riscv64": "0.25.10", "@esbuild/linux-s390x": "0.25.10", "@esbuild/linux-x64": "0.25.10", "@esbuild/netbsd-arm64": "0.25.10", "@esbuild/netbsd-x64": "0.25.10", "@esbuild/openbsd-arm64": "0.25.10", "@esbuild/openbsd-x64": "0.25.10", "@esbuild/openharmony-arm64": "0.25.10", "@esbuild/sunos-x64": "0.25.10", "@esbuild/win32-arm64": "0.25.10", "@esbuild/win32-ia32": "0.25.10", "@esbuild/win32-x64": "0.25.10" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-9RiGKvCwaqxO2owP61uQ4BgNborAQskMR6QusfWzQqv7AZOg5oGehdY2pRJMTKuwxd1IDBP4rSbI5lHzU7SMsQ=="],
|
||||
|
||||
"wrangler/miniflare": ["miniflare@4.20250917.0", "", { "dependencies": { "@cspotcode/source-map-support": "0.8.1", "acorn": "8.14.0", "acorn-walk": "8.3.2", "exit-hook": "2.2.1", "glob-to-regexp": "0.4.1", "sharp": "^0.33.5", "stoppable": "1.1.0", "undici": "7.14.0", "workerd": "1.20250917.0", "ws": "8.18.0", "youch": "4.1.0-beta.10", "zod": "3.22.3" }, "bin": { "miniflare": "bootstrap.js" } }, "sha512-A7kYEc/Y6ohiiTji4W/qGJj3aJNc/9IMj/6wLy2phD/iMjcoY8t35654gR5mHbMx0AgUolDdr3HOsHB0cYBf+Q=="],
|
||||
|
||||
"@cloudflare/vitest-pool-workers/wrangler/@cloudflare/unenv-preset": ["@cloudflare/unenv-preset@2.7.3", "", { "peerDependencies": { "unenv": "2.0.0-rc.21", "workerd": "^1.20250828.1" }, "optionalPeers": ["workerd"] }, "sha512-tsQQagBKjvpd9baa6nWVIv399ejiqcrUBBW6SZx6Z22+ymm+Odv5+cFimyuCsD/fC1fQTwfRmwXBNpzvHSeGCw=="],
|
||||
|
||||
"@cloudflare/vitest-pool-workers/wrangler/workerd": ["workerd@1.20250906.0", "", { "optionalDependencies": { "@cloudflare/workerd-darwin-64": "1.20250906.0", "@cloudflare/workerd-darwin-arm64": "1.20250906.0", "@cloudflare/workerd-linux-64": "1.20250906.0", "@cloudflare/workerd-linux-arm64": "1.20250906.0", "@cloudflare/workerd-windows-64": "1.20250906.0" }, "bin": { "workerd": "bin/workerd" } }, "sha512-ryVyEaqXPPsr/AxccRmYZZmDAkfQVjhfRqrNTlEeN8aftBk6Ca1u7/VqmfOayjCXrA+O547TauebU+J3IpvFXw=="],
|
||||
|
||||
"miniflare/workerd/@cloudflare/workerd-darwin-64": ["@cloudflare/workerd-darwin-64@1.20250906.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-E+X/YYH9BmX0ew2j/mAWFif2z05NMNuhCTlNYEGLkqMe99K15UewBqajL9pMcMUKxylnlrEoK3VNxl33DkbnPA=="],
|
||||
|
||||
"miniflare/workerd/@cloudflare/workerd-darwin-arm64": ["@cloudflare/workerd-darwin-arm64@1.20250906.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-X5apsZ1SFW4FYTM19ISHf8005FJMPfrcf4U5rO0tdj+TeJgQgXuZ57IG0WeW7SpLVeBo8hM6WC8CovZh41AfnA=="],
|
||||
|
||||
"miniflare/workerd/@cloudflare/workerd-linux-64": ["@cloudflare/workerd-linux-64@1.20250906.0", "", { "os": "linux", "cpu": "x64" }, "sha512-rlKzWgsLnlQ5Nt9W69YBJKcmTmZbOGu0edUsenXPmc6wzULUxoQpi7ZE9k3TfTonJx4WoQsQlzCUamRYFsX+0Q=="],
|
||||
|
||||
"miniflare/workerd/@cloudflare/workerd-linux-arm64": ["@cloudflare/workerd-linux-arm64@1.20250906.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-DdedhiQ+SeLzpg7BpcLrIPEZ33QKioJQ1wvL4X7nuLzEB9rWzS37NNNahQzc1+44rhG4fyiHbXBPOeox4B9XVA=="],
|
||||
|
||||
"miniflare/workerd/@cloudflare/workerd-windows-64": ["@cloudflare/workerd-windows-64@1.20250906.0", "", { "os": "win32", "cpu": "x64" }, "sha512-Q8Qjfs8jGVILnZL6vUpQ90q/8MTCYaGR3d1LGxZMBqte8Vr7xF3KFHPEy7tFs0j0mMjnqCYzlofmPNY+9ZaDRg=="],
|
||||
|
||||
"vite/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.10", "", { "os": "aix", "cpu": "ppc64" }, "sha512-0NFWnA+7l41irNuaSVlLfgNT12caWJVLzp5eAVhZ0z1qpxbockccEt3s+149rE64VUI3Ml2zt8Nv5JVc4QXTsw=="],
|
||||
|
||||
"vite/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.25.10", "", { "os": "android", "cpu": "arm" }, "sha512-dQAxF1dW1C3zpeCDc5KqIYuZ1tgAdRXNoZP7vkBIRtKZPYe2xVr/d3SkirklCHudW1B45tGiUlz2pUWDfbDD4w=="],
|
||||
|
||||
"vite/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.25.10", "", { "os": "android", "cpu": "arm64" }, "sha512-LSQa7eDahypv/VO6WKohZGPSJDq5OVOo3UoFR1E4t4Gj1W7zEQMUhI+lo81H+DtB+kP+tDgBp+M4oNCwp6kffg=="],
|
||||
|
||||
"vite/esbuild/@esbuild/android-x64": ["@esbuild/android-x64@0.25.10", "", { "os": "android", "cpu": "x64" }, "sha512-MiC9CWdPrfhibcXwr39p9ha1x0lZJ9KaVfvzA0Wxwz9ETX4v5CHfF09bx935nHlhi+MxhA63dKRRQLiVgSUtEg=="],
|
||||
|
||||
"vite/esbuild/@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.25.10", "", { "os": "darwin", "cpu": "arm64" }, "sha512-JC74bdXcQEpW9KkV326WpZZjLguSZ3DfS8wrrvPMHgQOIEIG/sPXEN/V8IssoJhbefLRcRqw6RQH2NnpdprtMA=="],
|
||||
|
||||
"vite/esbuild/@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.25.10", "", { "os": "darwin", "cpu": "x64" }, "sha512-tguWg1olF6DGqzws97pKZ8G2L7Ig1vjDmGTwcTuYHbuU6TTjJe5FXbgs5C1BBzHbJ2bo1m3WkQDbWO2PvamRcg=="],
|
||||
|
||||
"vite/esbuild/@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.25.10", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-3ZioSQSg1HT2N05YxeJWYR+Libe3bREVSdWhEEgExWaDtyFbbXWb49QgPvFH8u03vUPX10JhJPcz7s9t9+boWg=="],
|
||||
|
||||
"vite/esbuild/@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.25.10", "", { "os": "freebsd", "cpu": "x64" }, "sha512-LLgJfHJk014Aa4anGDbh8bmI5Lk+QidDmGzuC2D+vP7mv/GeSN+H39zOf7pN5N8p059FcOfs2bVlrRr4SK9WxA=="],
|
||||
|
||||
"vite/esbuild/@esbuild/linux-arm": ["@esbuild/linux-arm@0.25.10", "", { "os": "linux", "cpu": "arm" }, "sha512-oR31GtBTFYCqEBALI9r6WxoU/ZofZl962pouZRTEYECvNF/dtXKku8YXcJkhgK/beU+zedXfIzHijSRapJY3vg=="],
|
||||
|
||||
"vite/esbuild/@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.25.10", "", { "os": "linux", "cpu": "arm64" }, "sha512-5luJWN6YKBsawd5f9i4+c+geYiVEw20FVW5x0v1kEMWNq8UctFjDiMATBxLvmmHA4bf7F6hTRaJgtghFr9iziQ=="],
|
||||
|
||||
"vite/esbuild/@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.25.10", "", { "os": "linux", "cpu": "ia32" }, "sha512-NrSCx2Kim3EnnWgS4Txn0QGt0Xipoumb6z6sUtl5bOEZIVKhzfyp/Lyw4C1DIYvzeW/5mWYPBFJU3a/8Yr75DQ=="],
|
||||
|
||||
"vite/esbuild/@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.25.10", "", { "os": "linux", "cpu": "none" }, "sha512-xoSphrd4AZda8+rUDDfD9J6FUMjrkTz8itpTITM4/xgerAZZcFW7Dv+sun7333IfKxGG8gAq+3NbfEMJfiY+Eg=="],
|
||||
|
||||
"vite/esbuild/@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.25.10", "", { "os": "linux", "cpu": "none" }, "sha512-ab6eiuCwoMmYDyTnyptoKkVS3k8fy/1Uvq7Dj5czXI6DF2GqD2ToInBI0SHOp5/X1BdZ26RKc5+qjQNGRBelRA=="],
|
||||
|
||||
"vite/esbuild/@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.25.10", "", { "os": "linux", "cpu": "ppc64" }, "sha512-NLinzzOgZQsGpsTkEbdJTCanwA5/wozN9dSgEl12haXJBzMTpssebuXR42bthOF3z7zXFWH1AmvWunUCkBE4EA=="],
|
||||
|
||||
"vite/esbuild/@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.25.10", "", { "os": "linux", "cpu": "none" }, "sha512-FE557XdZDrtX8NMIeA8LBJX3dC2M8VGXwfrQWU7LB5SLOajfJIxmSdyL/gU1m64Zs9CBKvm4UAuBp5aJ8OgnrA=="],
|
||||
|
||||
"vite/esbuild/@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.25.10", "", { "os": "linux", "cpu": "s390x" }, "sha512-3BBSbgzuB9ajLoVZk0mGu+EHlBwkusRmeNYdqmznmMc9zGASFjSsxgkNsqmXugpPk00gJ0JNKh/97nxmjctdew=="],
|
||||
|
||||
"vite/esbuild/@esbuild/linux-x64": ["@esbuild/linux-x64@0.25.10", "", { "os": "linux", "cpu": "x64" }, "sha512-QSX81KhFoZGwenVyPoberggdW1nrQZSvfVDAIUXr3WqLRZGZqWk/P4T8p2SP+de2Sr5HPcvjhcJzEiulKgnxtA=="],
|
||||
|
||||
"vite/esbuild/@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.25.10", "", { "os": "none", "cpu": "arm64" }, "sha512-AKQM3gfYfSW8XRk8DdMCzaLUFB15dTrZfnX8WXQoOUpUBQ+NaAFCP1kPS/ykbbGYz7rxn0WS48/81l9hFl3u4A=="],
|
||||
|
||||
"vite/esbuild/@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.25.10", "", { "os": "none", "cpu": "x64" }, "sha512-7RTytDPGU6fek/hWuN9qQpeGPBZFfB4zZgcz2VK2Z5VpdUxEI8JKYsg3JfO0n/Z1E/6l05n0unDCNc4HnhQGig=="],
|
||||
|
||||
"vite/esbuild/@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.25.10", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-5Se0VM9Wtq797YFn+dLimf2Zx6McttsH2olUBsDml+lm0GOCRVebRWUvDtkY4BWYv/3NgzS8b/UM3jQNh5hYyw=="],
|
||||
|
||||
"vite/esbuild/@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.25.10", "", { "os": "openbsd", "cpu": "x64" }, "sha512-XkA4frq1TLj4bEMB+2HnI0+4RnjbuGZfet2gs/LNs5Hc7D89ZQBHQ0gL2ND6Lzu1+QVkjp3x1gIcPKzRNP8bXw=="],
|
||||
|
||||
"vite/esbuild/@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.25.10", "", { "os": "sunos", "cpu": "x64" }, "sha512-fswk3XT0Uf2pGJmOpDB7yknqhVkJQkAQOcW/ccVOtfx05LkbWOaRAtn5SaqXypeKQra1QaEa841PgrSL9ubSPQ=="],
|
||||
|
||||
"vite/esbuild/@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.25.10", "", { "os": "win32", "cpu": "arm64" }, "sha512-ah+9b59KDTSfpaCg6VdJoOQvKjI33nTaQr4UluQwW7aEwZQsbMCfTmfEO4VyewOxx4RaDT/xCy9ra2GPWmO7Kw=="],
|
||||
|
||||
"vite/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.25.10", "", { "os": "win32", "cpu": "ia32" }, "sha512-QHPDbKkrGO8/cz9LKVnJU22HOi4pxZnZhhA2HYHez5Pz4JeffhDjf85E57Oyco163GnzNCVkZK0b/n4Y0UHcSw=="],
|
||||
|
||||
"vite/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.10", "", { "os": "win32", "cpu": "x64" }, "sha512-9KpxSVFCu0iK1owoez6aC/s/EdUQLDN3adTxGCqxMVhrPDj6bt5dbrHDXUuq+Bs2vATFBBrQS5vdQ/Ed2P+nbw=="],
|
||||
|
||||
"wrangler/miniflare/undici": ["undici@7.14.0", "", {}, "sha512-Vqs8HTzjpQXZeXdpsfChQTlafcMQaaIwnGwLam1wudSSjlJeQ3bw1j+TLPePgrCnCpUXx7Ba5Pdpf5OBih62NQ=="],
|
||||
|
||||
"wrangler/miniflare/zod": ["zod@3.22.3", "", {}, "sha512-EjIevzuJRiRPbVH4mGc8nApb/lVLKVpmUhAaR5R5doKGfAnGJ6Gr3CViAVjP+4FWSxCsybeWQdcgCtbX+7oZug=="],
|
||||
|
||||
"@cloudflare/vitest-pool-workers/wrangler/workerd/@cloudflare/workerd-darwin-64": ["@cloudflare/workerd-darwin-64@1.20250906.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-E+X/YYH9BmX0ew2j/mAWFif2z05NMNuhCTlNYEGLkqMe99K15UewBqajL9pMcMUKxylnlrEoK3VNxl33DkbnPA=="],
|
||||
|
||||
"@cloudflare/vitest-pool-workers/wrangler/workerd/@cloudflare/workerd-darwin-arm64": ["@cloudflare/workerd-darwin-arm64@1.20250906.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-X5apsZ1SFW4FYTM19ISHf8005FJMPfrcf4U5rO0tdj+TeJgQgXuZ57IG0WeW7SpLVeBo8hM6WC8CovZh41AfnA=="],
|
||||
|
||||
"@cloudflare/vitest-pool-workers/wrangler/workerd/@cloudflare/workerd-linux-64": ["@cloudflare/workerd-linux-64@1.20250906.0", "", { "os": "linux", "cpu": "x64" }, "sha512-rlKzWgsLnlQ5Nt9W69YBJKcmTmZbOGu0edUsenXPmc6wzULUxoQpi7ZE9k3TfTonJx4WoQsQlzCUamRYFsX+0Q=="],
|
||||
|
||||
"@cloudflare/vitest-pool-workers/wrangler/workerd/@cloudflare/workerd-linux-arm64": ["@cloudflare/workerd-linux-arm64@1.20250906.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-DdedhiQ+SeLzpg7BpcLrIPEZ33QKioJQ1wvL4X7nuLzEB9rWzS37NNNahQzc1+44rhG4fyiHbXBPOeox4B9XVA=="],
|
||||
|
||||
"@cloudflare/vitest-pool-workers/wrangler/workerd/@cloudflare/workerd-windows-64": ["@cloudflare/workerd-windows-64@1.20250906.0", "", { "os": "win32", "cpu": "x64" }, "sha512-Q8Qjfs8jGVILnZL6vUpQ90q/8MTCYaGR3d1LGxZMBqte8Vr7xF3KFHPEy7tFs0j0mMjnqCYzlofmPNY+9ZaDRg=="],
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"name": "r2-worker",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"deploy": "wrangler deploy",
|
||||
"dev": "wrangler dev",
|
||||
"start": "wrangler dev",
|
||||
"test": "vitest",
|
||||
"cf-typegen": "wrangler types"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@cloudflare/vitest-pool-workers": "^0.8.19",
|
||||
"typescript": "^5.5.2",
|
||||
"vitest": "~3.2.0",
|
||||
"wrangler": "^4.38.0"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
export default {
|
||||
async fetch(request, env, ctx): Promise<Response> {
|
||||
const url = new URL(request.url);
|
||||
const prefix = url.pathname.slice(1);
|
||||
|
||||
switch (request.method) {
|
||||
case 'GET':
|
||||
const listResponse = await env.media.list({ prefix, limit: 1000 });
|
||||
const keys = listResponse.objects.map(o => o.key);
|
||||
|
||||
const headers = new Headers();
|
||||
headers.set('Content-Type', 'application/json');
|
||||
|
||||
return new Response(JSON.stringify(keys), {
|
||||
headers,
|
||||
status: 200,
|
||||
});
|
||||
|
||||
default:
|
||||
return new Response(`${request.method} is not allowed.`, {
|
||||
status: 405,
|
||||
headers: {
|
||||
Allow: 'GET',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
},
|
||||
} satisfies ExportedHandler<Env>;
|
||||
Vendored
+3
@@ -0,0 +1,3 @@
|
||||
declare module 'cloudflare:test' {
|
||||
interface ProvidedEnv extends Env {}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
import { env, createExecutionContext, waitOnExecutionContext, SELF } from 'cloudflare:test';
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import worker from '../src/index';
|
||||
|
||||
// For now, you'll need to do something like this to get a correctly-typed
|
||||
// `Request` to pass to `worker.fetch()`.
|
||||
const IncomingRequest = Request<unknown, IncomingRequestCfProperties>;
|
||||
|
||||
describe('Hello World worker', () => {
|
||||
it('responds with Hello World! (unit style)', async () => {
|
||||
const request = new IncomingRequest('http://example.com');
|
||||
// Create an empty context to pass to `worker.fetch()`.
|
||||
const ctx = createExecutionContext();
|
||||
const response = await worker.fetch(request, env, ctx);
|
||||
// Wait for all `Promise`s passed to `ctx.waitUntil()` to settle before running test assertions
|
||||
await waitOnExecutionContext(ctx);
|
||||
expect(await response.text()).toMatchInlineSnapshot(`"Hello World!"`);
|
||||
});
|
||||
|
||||
it('responds with Hello World! (integration style)', async () => {
|
||||
const response = await SELF.fetch('https://example.com');
|
||||
expect(await response.text()).toMatchInlineSnapshot(`"Hello World!"`);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"extends": "../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"types": ["@cloudflare/vitest-pool-workers"]
|
||||
},
|
||||
"include": ["./**/*.ts", "../worker-configuration.d.ts"],
|
||||
"exclude": []
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
/* Visit https://aka.ms/tsconfig.json to read more about this file */
|
||||
|
||||
/* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */
|
||||
"target": "es2021",
|
||||
/* Specify a set of bundled library declaration files that describe the target runtime environment. */
|
||||
"lib": ["es2021"],
|
||||
/* Specify what JSX code is generated. */
|
||||
"jsx": "react-jsx",
|
||||
|
||||
/* Specify what module code is generated. */
|
||||
"module": "es2022",
|
||||
/* Specify how TypeScript looks up a file from a given module specifier. */
|
||||
"moduleResolution": "Bundler",
|
||||
/* Enable importing .json files */
|
||||
"resolveJsonModule": true,
|
||||
|
||||
/* Allow JavaScript files to be a part of your program. Use the `checkJS` option to get errors from these files. */
|
||||
"allowJs": true,
|
||||
/* Enable error reporting in type-checked JavaScript files. */
|
||||
"checkJs": false,
|
||||
|
||||
/* Disable emitting files from a compilation. */
|
||||
"noEmit": true,
|
||||
|
||||
/* Ensure that each file can be safely transpiled without relying on other imports. */
|
||||
"isolatedModules": true,
|
||||
/* Allow 'import x from y' when a module doesn't have a default export. */
|
||||
"allowSyntheticDefaultImports": true,
|
||||
/* Ensure that casing is correct in imports. */
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
|
||||
/* Enable all strict type-checking options. */
|
||||
"strict": true,
|
||||
|
||||
/* Skip type checking all .d.ts files. */
|
||||
"skipLibCheck": true,
|
||||
"types": [
|
||||
"./worker-configuration.d.ts"
|
||||
]
|
||||
},
|
||||
"exclude": ["test"],
|
||||
"include": ["worker-configuration.d.ts", "src/**/*.ts"]
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import { defineWorkersConfig } from '@cloudflare/vitest-pool-workers/config';
|
||||
|
||||
export default defineWorkersConfig({
|
||||
test: {
|
||||
poolOptions: {
|
||||
workers: {
|
||||
wrangler: { configPath: './wrangler.jsonc' },
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
Vendored
+7691
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,49 @@
|
||||
/**
|
||||
* For more details on how to configure Wrangler, refer to:
|
||||
* https://developers.cloudflare.com/workers/wrangler/configuration/
|
||||
*/
|
||||
{
|
||||
"$schema": "node_modules/wrangler/config-schema.json",
|
||||
"name": "r2-worker",
|
||||
"main": "src/index.ts",
|
||||
"compatibility_date": "2025-09-21",
|
||||
"observability": {
|
||||
"enabled": true
|
||||
},
|
||||
"r2_buckets": [
|
||||
{
|
||||
"binding": "media",
|
||||
"bucket_name": "website-media"
|
||||
}
|
||||
]
|
||||
/**
|
||||
* Smart Placement
|
||||
* Docs: https://developers.cloudflare.com/workers/configuration/smart-placement/#smart-placement
|
||||
*/
|
||||
// "placement": { "mode": "smart" }
|
||||
/**
|
||||
* Bindings
|
||||
* Bindings allow your Worker to interact with resources on the Cloudflare Developer Platform, including
|
||||
* databases, object storage, AI inference, real-time communication and more.
|
||||
* https://developers.cloudflare.com/workers/runtime-apis/bindings/
|
||||
*/
|
||||
/**
|
||||
* Environment Variables
|
||||
* https://developers.cloudflare.com/workers/wrangler/configuration/#environment-variables
|
||||
*/
|
||||
// "vars": { "MY_VARIABLE": "production_value" }
|
||||
/**
|
||||
* Note: Use secrets to store sensitive data.
|
||||
* https://developers.cloudflare.com/workers/configuration/secrets/
|
||||
*/
|
||||
/**
|
||||
* Static Assets
|
||||
* https://developers.cloudflare.com/workers/static-assets/binding/
|
||||
*/
|
||||
// "assets": { "directory": "./public/", "binding": "ASSETS" }
|
||||
/**
|
||||
* Service Bindings (communicate between multiple Workers)
|
||||
* https://developers.cloudflare.com/workers/wrangler/configuration/#service-bindings
|
||||
*/
|
||||
// "services": [{ "binding": "MY_SERVICE", "service": "my-service" }]
|
||||
}
|
||||
Reference in New Issue
Block a user