Move domain models into domain package with schemas and validate with zod + elysia. Some maintenence as well
This commit is contained in:
@@ -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;
|
||||
@@ -191,7 +184,7 @@ export class LevelCalculator {
|
||||
|
||||
const activityLevelsAvg = Math.round(
|
||||
activityLevels.reduce((sum, curr) => sum + curr, 0) /
|
||||
activityLevels.length,
|
||||
activityLevels.length,
|
||||
);
|
||||
|
||||
return activityLevelsAvg;
|
||||
@@ -392,9 +385,9 @@ export class Standards {
|
||||
// generate data
|
||||
const allGenerators = this.cfg.activity[activity].enableGeneration
|
||||
? this.standardsData[activity].metadata.generators.sort(
|
||||
(a, b) =>
|
||||
metricPriority(a.metric) - metricPriority(b.metric),
|
||||
)
|
||||
(a, b) =>
|
||||
metricPriority(a.metric) - metricPriority(b.metric),
|
||||
)
|
||||
: [];
|
||||
|
||||
for (const generator of allGenerators) {
|
||||
@@ -664,7 +657,7 @@ export class Standards {
|
||||
weightUpper === weightLower
|
||||
? 1
|
||||
: (metrics.weight - weightLower) /
|
||||
(weightUpper - weightLower);
|
||||
(weightUpper - weightLower);
|
||||
weightRatio = Math.max(0, Math.min(1, weightRatio));
|
||||
|
||||
// metrics.weight = lowerAgeWeightStandard.metrics.weight + (upperAgeWeightStandard.metrics.weight - lowerAgeWeightStandard.metrics.weight) * weightRatio;
|
||||
|
||||
@@ -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