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"
|
||||
},
|
||||
"dependencies": {
|
||||
"@elysiajs/cors": "^1.4.2",
|
||||
"elysia": "^1.4.28",
|
||||
"zipcodes-us": "^1.1.3",
|
||||
"@blade-and-brawn/calculator": "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,
|
||||
LevelCalculator,
|
||||
Standards,
|
||||
} from "@blade-and-brawn/calculator";
|
||||
|
||||
import {
|
||||
ActivityPerformanceSchema,
|
||||
PlayerSchema,
|
||||
type ActivityPerformance,
|
||||
type Player,
|
||||
} from "@blade-and-brawn/calculator";
|
||||
} from "@blade-and-brawn/domain"
|
||||
import { cors } from "@elysiajs/cors";
|
||||
import { Elysia, NotFoundError } from "elysia";
|
||||
import {
|
||||
@@ -16,6 +21,7 @@ import {
|
||||
FetchError,
|
||||
} from "@blade-and-brawn/commerce";
|
||||
import zipcodesUs from "zipcodes-us";
|
||||
import z from "zod";
|
||||
|
||||
const levelCalculator = new LevelCalculator(
|
||||
new Standards(DEFAULT_STANDARDS_DATA),
|
||||
@@ -25,13 +31,18 @@ export const app = new Elysia()
|
||||
.use(
|
||||
cors({
|
||||
origin: [
|
||||
// bladeandbrawn.com (apex + any subdomain)
|
||||
// WEBSITE
|
||||
// -----------
|
||||
// production
|
||||
/^https?:\/\/([a-z0-9-]+\.)?bladeandbrawn\.com$/i,
|
||||
// Webflow preview domains
|
||||
// testing
|
||||
/^https?:\/\/([a-z0-9-]+\.)?bladeandbrawn\.webflow\.io$/i,
|
||||
// Fly app host (this one already works)
|
||||
|
||||
// PORTAL
|
||||
// -----------
|
||||
// production
|
||||
/^https?:\/\/blade-and-brawn\.fly\.dev$/i,
|
||||
// Local dev portal
|
||||
// development
|
||||
"http://localhost:5173",
|
||||
],
|
||||
}),
|
||||
@@ -65,14 +76,9 @@ export const app = new Elysia()
|
||||
|
||||
// CALCULATOR
|
||||
|
||||
.post("/calculate", async ({ body, query }) => {
|
||||
interface CalcRequest {
|
||||
player: Player;
|
||||
activityPerformances: ActivityPerformance[];
|
||||
}
|
||||
const { player, activityPerformances } = body as CalcRequest;
|
||||
.post("/calculate", async ({ body }) => {
|
||||
const output = {
|
||||
levels: levelCalculator.calculate(player, activityPerformances),
|
||||
levels: levelCalculator.calculate(body.player, body.activityPerformances),
|
||||
};
|
||||
|
||||
// console.log({ log: query?.log });
|
||||
@@ -85,6 +91,11 @@ export const app = new Elysia()
|
||||
// }
|
||||
|
||||
return output;
|
||||
}, {
|
||||
body: z.object({
|
||||
player: PlayerSchema,
|
||||
activityPerformances: z.array(ActivityPerformanceSchema)
|
||||
})
|
||||
})
|
||||
|
||||
// COMMERCE
|
||||
@@ -93,18 +104,26 @@ export const app = new Elysia()
|
||||
app
|
||||
.group("/sync", (app) =>
|
||||
app
|
||||
// Sync status
|
||||
.get("/", async ({ }) => SyncService.state)
|
||||
.post("/:printfulProductId?", async ({ params, set }) => {
|
||||
// Full sync
|
||||
.post("/", async ({ 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);
|
||||
await SyncService.sync();
|
||||
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 }) => {
|
||||
const printfulProductId = +params.printfulProductId;
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"name": "@blade-and-brawn/portal",
|
||||
"type": "module",
|
||||
"devDependencies": {
|
||||
"@sveltejs/kit": "^2.65.1",
|
||||
"@sveltejs/kit": "^2.66.0",
|
||||
"@sveltejs/vite-plugin-svelte": "^6.2.4",
|
||||
"@types/bun": "^1.3.14",
|
||||
"svelte": "^5.56.3",
|
||||
@@ -22,9 +22,9 @@
|
||||
"dependencies": {
|
||||
"@tailwindcss/vite": "^4.3.1",
|
||||
"daisyui": "^5.5.23",
|
||||
"memoirist": "^0.4.0",
|
||||
"tailwindcss": "^4.3.1",
|
||||
"@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">
|
||||
import {
|
||||
LevelCalculator,
|
||||
type LevelCalculatorOutput,
|
||||
type Standards,
|
||||
} from "@blade-and-brawn/calculator";
|
||||
import {
|
||||
Activity,
|
||||
Attribute,
|
||||
Gender,
|
||||
lbToKg,
|
||||
range,
|
||||
type LevelCalculatorOutput,
|
||||
type Standards,
|
||||
type ActivityPerformance,
|
||||
type Player,
|
||||
} from "@blade-and-brawn/calculator";
|
||||
} from "@blade-and-brawn/domain";
|
||||
|
||||
interface CalcData {
|
||||
levels?: LevelCalculatorOutput;
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
<script lang="ts">
|
||||
import {
|
||||
Standards,
|
||||
type Standard,
|
||||
type StandardsConfig,
|
||||
} from "@blade-and-brawn/calculator";
|
||||
import {
|
||||
Activity,
|
||||
cmToIn,
|
||||
kgToLb,
|
||||
range,
|
||||
type Metrics,
|
||||
type Standard,
|
||||
type StandardsConfig,
|
||||
} from "@blade-and-brawn/calculator";
|
||||
} from "@blade-and-brawn/domain";
|
||||
|
||||
interface Props {
|
||||
allStandards: Standards;
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
<script lang="ts">
|
||||
import {
|
||||
Activity,
|
||||
DEFAULT_STANDARDS_DATA,
|
||||
Gender,
|
||||
lbToKg,
|
||||
Standards,
|
||||
type Metrics,
|
||||
type StandardsConfig,
|
||||
} from "@blade-and-brawn/calculator";
|
||||
import {
|
||||
Activity,
|
||||
Gender,
|
||||
lbToKg,
|
||||
type Metrics,
|
||||
} from "@blade-and-brawn/domain";
|
||||
import StandardsTable from "$lib/components/StandardsTable.svelte";
|
||||
import PlayersTable from "$lib/components/PlayersTable.svelte";
|
||||
|
||||
@@ -15,6 +17,7 @@
|
||||
|
||||
const defaultStandards = new Standards(DEFAULT_STANDARDS_DATA);
|
||||
|
||||
// TODO: try to clean this up
|
||||
let input = $state({
|
||||
activity: Activity.BenchPress,
|
||||
metrics: {
|
||||
|
||||
@@ -16,10 +16,12 @@
|
||||
"dependencies": {
|
||||
"@blade-and-brawn/calculator": "workspace:*",
|
||||
"@blade-and-brawn/commerce": "workspace:*",
|
||||
"@blade-and-brawn/domain": "workspace:*",
|
||||
"@elysiajs/cors": "^1.4.2",
|
||||
"elysia": "^1.4.28",
|
||||
"elysia": "^1.4.29",
|
||||
"ml-levenberg-marquardt": "^5.0.1",
|
||||
"zipcodes-us": "^1.1.3",
|
||||
"zod": "^4.4.3",
|
||||
},
|
||||
},
|
||||
"apps/portal": {
|
||||
@@ -27,26 +29,27 @@
|
||||
"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",
|
||||
"@blade-and-brawn/domain": "workspace:*",
|
||||
"@tailwindcss/vite": "^4.3.1",
|
||||
"daisyui": "^5.5.23",
|
||||
"tailwindcss": "^4.3.1",
|
||||
},
|
||||
"devDependencies": {
|
||||
"@sveltejs/kit": "^2.54.0",
|
||||
"@sveltejs/kit": "^2.66.0",
|
||||
"@sveltejs/vite-plugin-svelte": "^6.2.4",
|
||||
"@types/bun": "^1.3.10",
|
||||
"svelte": "^5.53.10",
|
||||
"@types/bun": "^1.3.14",
|
||||
"svelte": "^5.56.3",
|
||||
"svelte-adapter-bun": "^0.5.2",
|
||||
"svelte-check": "^4.4.5",
|
||||
"svelte-check": "^4.6.0",
|
||||
"typescript": "^5.9.3",
|
||||
"vite": "^7.3.1",
|
||||
"vite": "^7.3.5",
|
||||
},
|
||||
},
|
||||
"packages/calculator": {
|
||||
"name": "@blade-and-brawn/calculator",
|
||||
"version": "0.0.0",
|
||||
"dependencies": {
|
||||
"@blade-and-brawn/domain": "workspace:*",
|
||||
"ml-levenberg-marquardt": "^5.0.0",
|
||||
},
|
||||
},
|
||||
@@ -54,6 +57,14 @@
|
||||
"name": "@blade-and-brawn/commerce",
|
||||
"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": {
|
||||
"@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/domain": ["@blade-and-brawn/domain@workspace:packages/domain"],
|
||||
|
||||
"@blade-and-brawn/portal": ["@blade-and-brawn/portal@workspace:apps/portal"],
|
||||
|
||||
"@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/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=="],
|
||||
|
||||
@@ -264,7 +277,7 @@
|
||||
|
||||
"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=="],
|
||||
|
||||
@@ -404,6 +417,8 @@
|
||||
|
||||
"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=="],
|
||||
|
||||
"@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",
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"ml-levenberg-marquardt": "^5.0.0"
|
||||
"ml-levenberg-marquardt": "^5.0.0",
|
||||
"@blade-and-brawn/domain": "workspace:*"
|
||||
},
|
||||
"exports": {
|
||||
".": "./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,
|
||||
Attribute,
|
||||
Gender,
|
||||
getAvgWeight,
|
||||
kgToLb,
|
||||
lbToKg,
|
||||
range,
|
||||
type Metrics,
|
||||
type ActivityPerformance,
|
||||
type Player,
|
||||
type StandardUnit,
|
||||
} from "../src/util";
|
||||
} from "@blade-and-brawn/domain";
|
||||
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" };
|
||||
import { getAvgWeight } from "./avg-weights";
|
||||
|
||||
// SOURCES
|
||||
// Squat, Bench, Dead Lift:
|
||||
@@ -24,8 +25,6 @@ import defaultStandardsJson from "./data/default-standards.json" assert { type:
|
||||
// 3 Cone drill:
|
||||
// https://nflsavant.com/combine.php?utm_source=chatgpt.com
|
||||
|
||||
export * from "./util";
|
||||
|
||||
export type LevelCalculatorOutput = {
|
||||
player: number;
|
||||
attributes: Record<Attribute, number>;
|
||||
@@ -33,12 +32,6 @@ export type LevelCalculatorOutput = {
|
||||
|
||||
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 {
|
||||
@@ -72,7 +65,7 @@ const metricPriority = (m: NumberMetric) => {
|
||||
export const DEFAULT_STANDARDS_DATA = defaultStandardsJson as StandardsData;
|
||||
|
||||
export class LevelCalculator {
|
||||
standards: Standards;
|
||||
readonly standards: Standards;
|
||||
|
||||
public constructor(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[],
|
||||
};
|
||||
|
||||
static async sync(printfulProductId: number | undefined) {
|
||||
static async sync(printfulProductId?: number) {
|
||||
this.state.isSyncing = true;
|
||||
|
||||
// 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