Move domain models into domain package with schemas and validate with zod + elysia. Some maintenence as well
This commit is contained in:
@@ -8,11 +8,13 @@
|
|||||||
"start": "bun run src/index.ts"
|
"start": "bun run src/index.ts"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@elysiajs/cors": "^1.4.2",
|
|
||||||
"elysia": "^1.4.28",
|
|
||||||
"zipcodes-us": "^1.1.3",
|
|
||||||
"@blade-and-brawn/calculator": "workspace:*",
|
"@blade-and-brawn/calculator": "workspace:*",
|
||||||
"@blade-and-brawn/commerce": "workspace:*",
|
"@blade-and-brawn/commerce": "workspace:*",
|
||||||
"ml-levenberg-marquardt": "^5.0.1"
|
"@blade-and-brawn/domain": "workspace:*",
|
||||||
|
"@elysiajs/cors": "^1.4.2",
|
||||||
|
"elysia": "^1.4.29",
|
||||||
|
"ml-levenberg-marquardt": "^5.0.1",
|
||||||
|
"zipcodes-us": "^1.1.3",
|
||||||
|
"zod": "^4.4.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+37
-18
@@ -2,9 +2,14 @@ import {
|
|||||||
DEFAULT_STANDARDS_DATA,
|
DEFAULT_STANDARDS_DATA,
|
||||||
LevelCalculator,
|
LevelCalculator,
|
||||||
Standards,
|
Standards,
|
||||||
|
} from "@blade-and-brawn/calculator";
|
||||||
|
|
||||||
|
import {
|
||||||
|
ActivityPerformanceSchema,
|
||||||
|
PlayerSchema,
|
||||||
type ActivityPerformance,
|
type ActivityPerformance,
|
||||||
type Player,
|
type Player,
|
||||||
} from "@blade-and-brawn/calculator";
|
} from "@blade-and-brawn/domain"
|
||||||
import { cors } from "@elysiajs/cors";
|
import { cors } from "@elysiajs/cors";
|
||||||
import { Elysia, NotFoundError } from "elysia";
|
import { Elysia, NotFoundError } from "elysia";
|
||||||
import {
|
import {
|
||||||
@@ -16,6 +21,7 @@ import {
|
|||||||
FetchError,
|
FetchError,
|
||||||
} from "@blade-and-brawn/commerce";
|
} from "@blade-and-brawn/commerce";
|
||||||
import zipcodesUs from "zipcodes-us";
|
import zipcodesUs from "zipcodes-us";
|
||||||
|
import z from "zod";
|
||||||
|
|
||||||
const levelCalculator = new LevelCalculator(
|
const levelCalculator = new LevelCalculator(
|
||||||
new Standards(DEFAULT_STANDARDS_DATA),
|
new Standards(DEFAULT_STANDARDS_DATA),
|
||||||
@@ -25,13 +31,18 @@ export const app = new Elysia()
|
|||||||
.use(
|
.use(
|
||||||
cors({
|
cors({
|
||||||
origin: [
|
origin: [
|
||||||
// bladeandbrawn.com (apex + any subdomain)
|
// WEBSITE
|
||||||
|
// -----------
|
||||||
|
// production
|
||||||
/^https?:\/\/([a-z0-9-]+\.)?bladeandbrawn\.com$/i,
|
/^https?:\/\/([a-z0-9-]+\.)?bladeandbrawn\.com$/i,
|
||||||
// Webflow preview domains
|
// testing
|
||||||
/^https?:\/\/([a-z0-9-]+\.)?bladeandbrawn\.webflow\.io$/i,
|
/^https?:\/\/([a-z0-9-]+\.)?bladeandbrawn\.webflow\.io$/i,
|
||||||
// Fly app host (this one already works)
|
|
||||||
|
// PORTAL
|
||||||
|
// -----------
|
||||||
|
// production
|
||||||
/^https?:\/\/blade-and-brawn\.fly\.dev$/i,
|
/^https?:\/\/blade-and-brawn\.fly\.dev$/i,
|
||||||
// Local dev portal
|
// development
|
||||||
"http://localhost:5173",
|
"http://localhost:5173",
|
||||||
],
|
],
|
||||||
}),
|
}),
|
||||||
@@ -65,14 +76,9 @@ export const app = new Elysia()
|
|||||||
|
|
||||||
// CALCULATOR
|
// CALCULATOR
|
||||||
|
|
||||||
.post("/calculate", async ({ body, query }) => {
|
.post("/calculate", async ({ body }) => {
|
||||||
interface CalcRequest {
|
|
||||||
player: Player;
|
|
||||||
activityPerformances: ActivityPerformance[];
|
|
||||||
}
|
|
||||||
const { player, activityPerformances } = body as CalcRequest;
|
|
||||||
const output = {
|
const output = {
|
||||||
levels: levelCalculator.calculate(player, activityPerformances),
|
levels: levelCalculator.calculate(body.player, body.activityPerformances),
|
||||||
};
|
};
|
||||||
|
|
||||||
// console.log({ log: query?.log });
|
// console.log({ log: query?.log });
|
||||||
@@ -85,6 +91,11 @@ export const app = new Elysia()
|
|||||||
// }
|
// }
|
||||||
|
|
||||||
return output;
|
return output;
|
||||||
|
}, {
|
||||||
|
body: z.object({
|
||||||
|
player: PlayerSchema,
|
||||||
|
activityPerformances: z.array(ActivityPerformanceSchema)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
// COMMERCE
|
// COMMERCE
|
||||||
@@ -93,18 +104,26 @@ export const app = new Elysia()
|
|||||||
app
|
app
|
||||||
.group("/sync", (app) =>
|
.group("/sync", (app) =>
|
||||||
app
|
app
|
||||||
|
// Sync status
|
||||||
.get("/", async ({ }) => SyncService.state)
|
.get("/", async ({ }) => SyncService.state)
|
||||||
.post("/:printfulProductId?", async ({ params, set }) => {
|
// Full sync
|
||||||
|
.post("/", async ({ set }) => {
|
||||||
if (SyncService.state.isSyncing) {
|
if (SyncService.state.isSyncing) {
|
||||||
set.status = 409;
|
set.status = 409;
|
||||||
return { error: "Sync already in progress" };
|
return { error: "Sync already in progress" };
|
||||||
}
|
}
|
||||||
const printfulProductId = params.printfulProductId
|
await SyncService.sync();
|
||||||
? +params.printfulProductId
|
|
||||||
: undefined;
|
|
||||||
await SyncService.sync(printfulProductId);
|
|
||||||
return { ok: true };
|
return { ok: true };
|
||||||
}),
|
})
|
||||||
|
// Per-product sync
|
||||||
|
.post("/:printfulProductId", async ({ params, set }) => {
|
||||||
|
if (SyncService.state.isSyncing) {
|
||||||
|
set.status = 409;
|
||||||
|
return { error: "Sync already in progress" };
|
||||||
|
}
|
||||||
|
await SyncService.sync(params.printfulProductId);
|
||||||
|
return { ok: true };
|
||||||
|
}, { params: z.object({ printfulProductId: z.number() }) }),
|
||||||
)
|
)
|
||||||
.get("/:printfulProductId", async ({ params }) => {
|
.get("/:printfulProductId", async ({ params }) => {
|
||||||
const printfulProductId = +params.printfulProductId;
|
const printfulProductId = +params.printfulProductId;
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
"name": "@blade-and-brawn/portal",
|
"name": "@blade-and-brawn/portal",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@sveltejs/kit": "^2.65.1",
|
"@sveltejs/kit": "^2.66.0",
|
||||||
"@sveltejs/vite-plugin-svelte": "^6.2.4",
|
"@sveltejs/vite-plugin-svelte": "^6.2.4",
|
||||||
"@types/bun": "^1.3.14",
|
"@types/bun": "^1.3.14",
|
||||||
"svelte": "^5.56.3",
|
"svelte": "^5.56.3",
|
||||||
@@ -22,9 +22,9 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@tailwindcss/vite": "^4.3.1",
|
"@tailwindcss/vite": "^4.3.1",
|
||||||
"daisyui": "^5.5.23",
|
"daisyui": "^5.5.23",
|
||||||
"memoirist": "^0.4.0",
|
|
||||||
"tailwindcss": "^4.3.1",
|
"tailwindcss": "^4.3.1",
|
||||||
"@blade-and-brawn/calculator": "workspace:*",
|
"@blade-and-brawn/calculator": "workspace:*",
|
||||||
"@blade-and-brawn/commerce": "workspace:*"
|
"@blade-and-brawn/commerce": "workspace:*",
|
||||||
|
"@blade-and-brawn/domain": "workspace:*"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,16 +1,18 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {
|
import {
|
||||||
LevelCalculator,
|
LevelCalculator,
|
||||||
|
type LevelCalculatorOutput,
|
||||||
|
type Standards,
|
||||||
|
} from "@blade-and-brawn/calculator";
|
||||||
|
import {
|
||||||
Activity,
|
Activity,
|
||||||
Attribute,
|
Attribute,
|
||||||
Gender,
|
Gender,
|
||||||
lbToKg,
|
lbToKg,
|
||||||
range,
|
range,
|
||||||
type LevelCalculatorOutput,
|
|
||||||
type Standards,
|
|
||||||
type ActivityPerformance,
|
type ActivityPerformance,
|
||||||
type Player,
|
type Player,
|
||||||
} from "@blade-and-brawn/calculator";
|
} from "@blade-and-brawn/domain";
|
||||||
|
|
||||||
interface CalcData {
|
interface CalcData {
|
||||||
levels?: LevelCalculatorOutput;
|
levels?: LevelCalculatorOutput;
|
||||||
|
|||||||
@@ -1,14 +1,16 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {
|
import {
|
||||||
Standards,
|
Standards,
|
||||||
|
type Standard,
|
||||||
|
type StandardsConfig,
|
||||||
|
} from "@blade-and-brawn/calculator";
|
||||||
|
import {
|
||||||
Activity,
|
Activity,
|
||||||
cmToIn,
|
cmToIn,
|
||||||
kgToLb,
|
kgToLb,
|
||||||
range,
|
range,
|
||||||
type Metrics,
|
type Metrics,
|
||||||
type Standard,
|
} from "@blade-and-brawn/domain";
|
||||||
type StandardsConfig,
|
|
||||||
} from "@blade-and-brawn/calculator";
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
allStandards: Standards;
|
allStandards: Standards;
|
||||||
|
|||||||
@@ -1,13 +1,15 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {
|
import {
|
||||||
Activity,
|
|
||||||
DEFAULT_STANDARDS_DATA,
|
DEFAULT_STANDARDS_DATA,
|
||||||
Gender,
|
|
||||||
lbToKg,
|
|
||||||
Standards,
|
Standards,
|
||||||
type Metrics,
|
|
||||||
type StandardsConfig,
|
type StandardsConfig,
|
||||||
} from "@blade-and-brawn/calculator";
|
} from "@blade-and-brawn/calculator";
|
||||||
|
import {
|
||||||
|
Activity,
|
||||||
|
Gender,
|
||||||
|
lbToKg,
|
||||||
|
type Metrics,
|
||||||
|
} from "@blade-and-brawn/domain";
|
||||||
import StandardsTable from "$lib/components/StandardsTable.svelte";
|
import StandardsTable from "$lib/components/StandardsTable.svelte";
|
||||||
import PlayersTable from "$lib/components/PlayersTable.svelte";
|
import PlayersTable from "$lib/components/PlayersTable.svelte";
|
||||||
|
|
||||||
@@ -15,6 +17,7 @@
|
|||||||
|
|
||||||
const defaultStandards = new Standards(DEFAULT_STANDARDS_DATA);
|
const defaultStandards = new Standards(DEFAULT_STANDARDS_DATA);
|
||||||
|
|
||||||
|
// TODO: try to clean this up
|
||||||
let input = $state({
|
let input = $state({
|
||||||
activity: Activity.BenchPress,
|
activity: Activity.BenchPress,
|
||||||
metrics: {
|
metrics: {
|
||||||
|
|||||||
@@ -16,10 +16,12 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@blade-and-brawn/calculator": "workspace:*",
|
"@blade-and-brawn/calculator": "workspace:*",
|
||||||
"@blade-and-brawn/commerce": "workspace:*",
|
"@blade-and-brawn/commerce": "workspace:*",
|
||||||
|
"@blade-and-brawn/domain": "workspace:*",
|
||||||
"@elysiajs/cors": "^1.4.2",
|
"@elysiajs/cors": "^1.4.2",
|
||||||
"elysia": "^1.4.28",
|
"elysia": "^1.4.29",
|
||||||
"ml-levenberg-marquardt": "^5.0.1",
|
"ml-levenberg-marquardt": "^5.0.1",
|
||||||
"zipcodes-us": "^1.1.3",
|
"zipcodes-us": "^1.1.3",
|
||||||
|
"zod": "^4.4.3",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"apps/portal": {
|
"apps/portal": {
|
||||||
@@ -27,26 +29,27 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@blade-and-brawn/calculator": "workspace:*",
|
"@blade-and-brawn/calculator": "workspace:*",
|
||||||
"@blade-and-brawn/commerce": "workspace:*",
|
"@blade-and-brawn/commerce": "workspace:*",
|
||||||
"@tailwindcss/vite": "^4.2.1",
|
"@blade-and-brawn/domain": "workspace:*",
|
||||||
"daisyui": "^5.5.19",
|
"@tailwindcss/vite": "^4.3.1",
|
||||||
"memoirist": "^0.4.0",
|
"daisyui": "^5.5.23",
|
||||||
"tailwindcss": "^4.2.1",
|
"tailwindcss": "^4.3.1",
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@sveltejs/kit": "^2.54.0",
|
"@sveltejs/kit": "^2.66.0",
|
||||||
"@sveltejs/vite-plugin-svelte": "^6.2.4",
|
"@sveltejs/vite-plugin-svelte": "^6.2.4",
|
||||||
"@types/bun": "^1.3.10",
|
"@types/bun": "^1.3.14",
|
||||||
"svelte": "^5.53.10",
|
"svelte": "^5.56.3",
|
||||||
"svelte-adapter-bun": "^0.5.2",
|
"svelte-adapter-bun": "^0.5.2",
|
||||||
"svelte-check": "^4.4.5",
|
"svelte-check": "^4.6.0",
|
||||||
"typescript": "^5.9.3",
|
"typescript": "^5.9.3",
|
||||||
"vite": "^7.3.1",
|
"vite": "^7.3.5",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"packages/calculator": {
|
"packages/calculator": {
|
||||||
"name": "@blade-and-brawn/calculator",
|
"name": "@blade-and-brawn/calculator",
|
||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@blade-and-brawn/domain": "workspace:*",
|
||||||
"ml-levenberg-marquardt": "^5.0.0",
|
"ml-levenberg-marquardt": "^5.0.0",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -54,6 +57,14 @@
|
|||||||
"name": "@blade-and-brawn/commerce",
|
"name": "@blade-and-brawn/commerce",
|
||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
},
|
},
|
||||||
|
"packages/domain": {
|
||||||
|
"name": "@blade-and-brawn/domain",
|
||||||
|
"version": "0.0.0",
|
||||||
|
"dependencies": {
|
||||||
|
"@blade-and-brawn/domain": "workspace:*",
|
||||||
|
"zod": "^4.4.3",
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
"packages": {
|
"packages": {
|
||||||
"@blade-and-brawn/api": ["@blade-and-brawn/api@workspace:apps/api"],
|
"@blade-and-brawn/api": ["@blade-and-brawn/api@workspace:apps/api"],
|
||||||
@@ -62,6 +73,8 @@
|
|||||||
|
|
||||||
"@blade-and-brawn/commerce": ["@blade-and-brawn/commerce@workspace:packages/commerce"],
|
"@blade-and-brawn/commerce": ["@blade-and-brawn/commerce@workspace:packages/commerce"],
|
||||||
|
|
||||||
|
"@blade-and-brawn/domain": ["@blade-and-brawn/domain@workspace:packages/domain"],
|
||||||
|
|
||||||
"@blade-and-brawn/portal": ["@blade-and-brawn/portal@workspace:apps/portal"],
|
"@blade-and-brawn/portal": ["@blade-and-brawn/portal@workspace:apps/portal"],
|
||||||
|
|
||||||
"@borewit/text-codec": ["@borewit/text-codec@0.2.1", "", {}, "sha512-k7vvKPbf7J2fZ5klGRD9AeKfUvojuZIQ3BT5u7Jfv+puwXkUBUT5PVyMDfJZpy30CBDXGMgw7fguK/lpOMBvgw=="],
|
"@borewit/text-codec": ["@borewit/text-codec@0.2.1", "", {}, "sha512-k7vvKPbf7J2fZ5klGRD9AeKfUvojuZIQ3BT5u7Jfv+puwXkUBUT5PVyMDfJZpy30CBDXGMgw7fguK/lpOMBvgw=="],
|
||||||
@@ -188,7 +201,7 @@
|
|||||||
|
|
||||||
"@sveltejs/acorn-typescript": ["@sveltejs/acorn-typescript@1.0.10", "", { "peerDependencies": { "acorn": "^8.9.0" } }, "sha512-4WfKk68eTih+MiJD4fSbxN7E8kVBmTMPWHUPYjvl2N0rMs53YLTT8/YjKU5Dtnz5LqDjl7LEw4U7lXR2W3J5WA=="],
|
"@sveltejs/acorn-typescript": ["@sveltejs/acorn-typescript@1.0.10", "", { "peerDependencies": { "acorn": "^8.9.0" } }, "sha512-4WfKk68eTih+MiJD4fSbxN7E8kVBmTMPWHUPYjvl2N0rMs53YLTT8/YjKU5Dtnz5LqDjl7LEw4U7lXR2W3J5WA=="],
|
||||||
|
|
||||||
"@sveltejs/kit": ["@sveltejs/kit@2.65.1", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "@sveltejs/acorn-typescript": "^1.0.9", "@types/cookie": "^0.6.0", "acorn": "^8.16.0", "cookie": "^0.6.0", "devalue": "^5.8.1", "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 || ^6.0.0", "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-Sa1rFYYqBB+zv3rIxAg/CsFskR/x4aj5BY/hvLxBd9r/mqbipxM945As1K3PqsDicJAyekPR0BlWoVIiw2OHYg=="],
|
"@sveltejs/kit": ["@sveltejs/kit@2.66.0", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "@sveltejs/acorn-typescript": "^1.0.9", "@types/cookie": "^0.6.0", "acorn": "^8.16.0", "cookie": "^0.6.0", "devalue": "^5.8.1", "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 || ^6.0.0", "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-7nN4Ur4+nofZ36DVo83JbRe02m61Vc+I441mML/DYa1pUTZ/x26+lbrdqPen8gjmsUc6flMtHEqAtn0UfmfvAw=="],
|
||||||
|
|
||||||
"@sveltejs/load-config": ["@sveltejs/load-config@0.1.1", "", {}, "sha512-BXXm+VOH/9X4N7Dd1iZ2MqA1h7M+9i2noI8QYuLDY8QcN2WHYn7D/VK/+IJNfcAmRw7ACNJ538UT9GXIhnBTiA=="],
|
"@sveltejs/load-config": ["@sveltejs/load-config@0.1.1", "", {}, "sha512-BXXm+VOH/9X4N7Dd1iZ2MqA1h7M+9i2noI8QYuLDY8QcN2WHYn7D/VK/+IJNfcAmRw7ACNJ538UT9GXIhnBTiA=="],
|
||||||
|
|
||||||
@@ -264,7 +277,7 @@
|
|||||||
|
|
||||||
"devalue": ["devalue@5.8.1", "", {}, "sha512-4CXDYRBGqN+57wVJkuXBYmpAVUSg3L6JAQa/DFqm238G73E1wuyc/JhGQJzN7vUf/CMphYau2zXbfWzDR5aTEw=="],
|
"devalue": ["devalue@5.8.1", "", {}, "sha512-4CXDYRBGqN+57wVJkuXBYmpAVUSg3L6JAQa/DFqm238G73E1wuyc/JhGQJzN7vUf/CMphYau2zXbfWzDR5aTEw=="],
|
||||||
|
|
||||||
"elysia": ["elysia@1.4.28", "", { "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-Vrx8sBnvq8squS/3yNBzR1jBXI+SgmnmvwawPjNuEHndUe5l1jV2Gp6JJ4ulDkEB8On6bWmmuyPpA+bq4t+WYg=="],
|
"elysia": ["elysia@1.4.29", "", { "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-GwMRGGwSdjfPt+w3LA0fqTuYJtS8uVRJicvoar98/HrO5qdFKDc9CwjIb6Kja+v39lkY+58hr2JvdR9jQzlUuA=="],
|
||||||
|
|
||||||
"enhanced-resolve": ["enhanced-resolve@5.21.6", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.3.3" } }, "sha512-aNnGCvbJ/RIyWo1IuhNdVjnNF+EjH9wpzpNHt+ci/m9He9LJvUN8wrCcXjp9cWsGNAuvSpVFTx/vraAFQ8qGjQ=="],
|
"enhanced-resolve": ["enhanced-resolve@5.21.6", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.3.3" } }, "sha512-aNnGCvbJ/RIyWo1IuhNdVjnNF+EjH9wpzpNHt+ci/m9He9LJvUN8wrCcXjp9cWsGNAuvSpVFTx/vraAFQ8qGjQ=="],
|
||||||
|
|
||||||
@@ -404,6 +417,8 @@
|
|||||||
|
|
||||||
"zipcodes-us": ["zipcodes-us@1.1.3", "", {}, "sha512-gOz8WAO6iWBU4aUc0lljpa+TKZckRKmgoHKcc1jWnn4eTbdc9XO5cXEaGccYn7GAjykueO0Wn+N7zdmzG0Uf5w=="],
|
"zipcodes-us": ["zipcodes-us@1.1.3", "", {}, "sha512-gOz8WAO6iWBU4aUc0lljpa+TKZckRKmgoHKcc1jWnn4eTbdc9XO5cXEaGccYn7GAjykueO0Wn+N7zdmzG0Uf5w=="],
|
||||||
|
|
||||||
|
"zod": ["zod@4.4.3", "", {}, "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ=="],
|
||||||
|
|
||||||
"@sveltejs/kit/cookie": ["cookie@0.6.0", "", {}, "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw=="],
|
"@sveltejs/kit/cookie": ["cookie@0.6.0", "", {}, "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw=="],
|
||||||
|
|
||||||
"@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.11.1", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.2", "tslib": "^2.4.0" }, "bundled": true }, "sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ=="],
|
"@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.11.1", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.2", "tslib": "^2.4.0" }, "bundled": true }, "sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ=="],
|
||||||
|
|||||||
@@ -4,7 +4,8 @@
|
|||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"ml-levenberg-marquardt": "^5.0.0"
|
"ml-levenberg-marquardt": "^5.0.0",
|
||||||
|
"@blade-and-brawn/domain": "workspace:*"
|
||||||
},
|
},
|
||||||
"exports": {
|
"exports": {
|
||||||
".": "./src/index.ts"
|
".": "./src/index.ts"
|
||||||
|
|||||||
@@ -0,0 +1,30 @@
|
|||||||
|
import type { Gender, Metrics, StandardUnit } from "@blade-and-brawn/domain";
|
||||||
|
import avgWeightDataJson from "./data/avg-weights.json" assert { type: "json" };
|
||||||
|
|
||||||
|
export const getAvgWeight = (gender: Gender, age: number) => {
|
||||||
|
type AvgWeightData = {
|
||||||
|
metadata: {
|
||||||
|
unit: StandardUnit;
|
||||||
|
};
|
||||||
|
weights: Metrics[];
|
||||||
|
};
|
||||||
|
const avgWeightData = avgWeightDataJson 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;
|
||||||
|
};
|
||||||
@@ -2,17 +2,18 @@ import {
|
|||||||
Activity,
|
Activity,
|
||||||
Attribute,
|
Attribute,
|
||||||
Gender,
|
Gender,
|
||||||
getAvgWeight,
|
|
||||||
kgToLb,
|
kgToLb,
|
||||||
lbToKg,
|
lbToKg,
|
||||||
range,
|
range,
|
||||||
|
type Metrics,
|
||||||
type ActivityPerformance,
|
type ActivityPerformance,
|
||||||
type Player,
|
type Player,
|
||||||
type StandardUnit,
|
type StandardUnit,
|
||||||
} from "../src/util";
|
} from "@blade-and-brawn/domain";
|
||||||
import { levenbergMarquardt as LM } from "ml-levenberg-marquardt";
|
import { levenbergMarquardt as LM } from "ml-levenberg-marquardt";
|
||||||
import defaultConfig from "./config.json" assert { type: "json" };
|
import defaultConfig from "./config.json" assert { type: "json" };
|
||||||
import defaultStandardsJson from "./data/default-standards.json" assert { type: "json" };
|
import defaultStandardsJson from "./data/default-standards.json" assert { type: "json" };
|
||||||
|
import { getAvgWeight } from "./avg-weights";
|
||||||
|
|
||||||
// SOURCES
|
// SOURCES
|
||||||
// Squat, Bench, Dead Lift:
|
// Squat, Bench, Dead Lift:
|
||||||
@@ -24,8 +25,6 @@ import defaultStandardsJson from "./data/default-standards.json" assert { type:
|
|||||||
// 3 Cone drill:
|
// 3 Cone drill:
|
||||||
// https://nflsavant.com/combine.php?utm_source=chatgpt.com
|
// https://nflsavant.com/combine.php?utm_source=chatgpt.com
|
||||||
|
|
||||||
export * from "./util";
|
|
||||||
|
|
||||||
export type LevelCalculatorOutput = {
|
export type LevelCalculatorOutput = {
|
||||||
player: number;
|
player: number;
|
||||||
attributes: Record<Attribute, number>;
|
attributes: Record<Attribute, number>;
|
||||||
@@ -33,12 +32,6 @@ export type LevelCalculatorOutput = {
|
|||||||
|
|
||||||
export type Levels = Record<string, number>;
|
export type Levels = Record<string, number>;
|
||||||
|
|
||||||
export interface Metrics {
|
|
||||||
age: number;
|
|
||||||
weight: number;
|
|
||||||
gender: Gender;
|
|
||||||
}
|
|
||||||
|
|
||||||
type NumberMetric = Extract<keyof Metrics, "age" | "weight">;
|
type NumberMetric = Extract<keyof Metrics, "age" | "weight">;
|
||||||
|
|
||||||
export interface Standard {
|
export interface Standard {
|
||||||
@@ -72,7 +65,7 @@ const metricPriority = (m: NumberMetric) => {
|
|||||||
export const DEFAULT_STANDARDS_DATA = defaultStandardsJson as StandardsData;
|
export const DEFAULT_STANDARDS_DATA = defaultStandardsJson as StandardsData;
|
||||||
|
|
||||||
export class LevelCalculator {
|
export class LevelCalculator {
|
||||||
standards: Standards;
|
readonly standards: Standards;
|
||||||
|
|
||||||
public constructor(standards: Standards) {
|
public constructor(standards: Standards) {
|
||||||
this.standards = standards;
|
this.standards = standards;
|
||||||
|
|||||||
@@ -1,100 +0,0 @@
|
|||||||
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;
|
|
||||||
};
|
|
||||||
@@ -24,7 +24,7 @@ export class SyncService {
|
|||||||
syncingIds: [] as number[],
|
syncingIds: [] as number[],
|
||||||
};
|
};
|
||||||
|
|
||||||
static async sync(printfulProductId: number | undefined) {
|
static async sync(printfulProductId?: number) {
|
||||||
this.state.isSyncing = true;
|
this.state.isSyncing = true;
|
||||||
|
|
||||||
// Populate products
|
// Populate products
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"name": "@blade-and-brawn/domain",
|
||||||
|
"private": true,
|
||||||
|
"version": "0.0.0",
|
||||||
|
"type": "module",
|
||||||
|
"dependencies": {
|
||||||
|
"@blade-and-brawn/domain": "workspace:*",
|
||||||
|
"zod": "^4.4.3"
|
||||||
|
},
|
||||||
|
"exports": {
|
||||||
|
".": "./src/index.ts"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
export * from "./models"
|
||||||
|
export * from "./utils"
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
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 enum Gender {
|
||||||
|
Male = "Male",
|
||||||
|
Female = "Female",
|
||||||
|
}
|
||||||
|
|
||||||
|
export const MetricsSchema = z.object({
|
||||||
|
age: z.number(),
|
||||||
|
weight: z.number(),
|
||||||
|
gender: z.enum(Gender),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const PlayerSchema = z.object({
|
||||||
|
name: z.string().optional(),
|
||||||
|
metrics: MetricsSchema,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const ActivityPerformanceSchema = z.object({
|
||||||
|
activity: z.enum(Activity),
|
||||||
|
performance: z.number(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type StandardUnit = "ms" | "cm" | "kg";
|
||||||
|
|
||||||
|
export type Metrics = z.infer<typeof MetricsSchema>;
|
||||||
|
export type Player = z.infer<typeof PlayerSchema>;
|
||||||
|
export type ActivityPerformance = z.infer<typeof ActivityPerformanceSchema>;
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
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));
|
||||||
Reference in New Issue
Block a user