View update

This commit is contained in:
Dominic Ferrando
2025-09-18 00:01:27 -04:00
parent bd0b7ea828
commit e9f9821222
40 changed files with 1646 additions and 242 deletions
+88
View File
@@ -0,0 +1,88 @@
import { Elysia } from "elysia";
import { cors } from "@elysiajs/cors";
import { ActivityStandards, LevelCalculator, Standards } from "./services/calculator/main";
import { ActivityPerformance, Player } from "./services/calculator/util";
import { Printful } from "./services/commerce/printful";
import { Webflow } from "./services/commerce/webflow";
import rawStandards from "./data/standards.json" assert { type: "json" }
import { fileURLToPath } from 'url';
import { dirname, join } from 'path';
const __dirname = dirname(fileURLToPath(import.meta.url));
export const app = new Elysia({ prefix: "/api" })
.use(cors())
.get("/", () => "Hello world")
// CALCULATOR
.post("/calculate", async ({ body }) => {
const MAIN_STANDARDS = new Standards(rawStandards as ActivityStandards);
interface CalcRequest {
player: Player;
activityPerformances: ActivityPerformance[];
}
const { player, activityPerformances } = body as CalcRequest;
const levelCalculator = new LevelCalculator(MAIN_STANDARDS);
return { levels: levelCalculator.calculate(player, activityPerformances) };
})
// COMMERCE
.post("/products/sync", async () => {
const printfulProducts = await Printful.Products.getAll();
for (const p of printfulProducts) {
const exists = await Webflow.Products.exists(p.sync_product.external_id);
if (exists) await Webflow.Products.updateUsingPrintful(p);
else await Webflow.Products.createUsingPrintful(p);
}
return {};
})
// DATA
.get("/data/standards", async () => {
try {
return Bun.file(join(__dirname, "data/standards.json")).json();
} catch (error) {
console.error(error);
return new Response("Failed to load standards", { status: 500 });
}
})
// WEBHOOKS
.post("/webhook/printful", async ({ body, set }) => {
const payload = body as Printful.Webhook.EventPayload;
try {
switch (payload.type) {
case Printful.Webhook.Event.ProductUpdated: {
const prod = await Printful.Products.get(payload.data.sync_product.id);
const exists = await Webflow.Products.exists(prod.sync_product.external_id);
if (exists) await Webflow.Products.updateUsingPrintful(prod);
else await Webflow.Products.createUsingPrintful(prod);
break;
}
case Printful.Webhook.Event.ProductDeleted: {
await Webflow.Products.remove(payload.data.sync_product.external_id);
break;
}
}
} catch (error) {
console.error(error);
set.status = 500;
return { error: "internal_error" };
}
return "";
});
if (import.meta.main) {
app.listen(3000);
console.log(
`Listening at ${app.server?.hostname}:${app.server?.port}`,
);
}
export type App = typeof app;
BIN
View File
Binary file not shown.
+5
View File
@@ -0,0 +1,5 @@
CREATE TABLE IF NOT EXISTS product_records (
webflowProductId INTEGER NOT NULL,
printfulProductId INTEGER NOT NULL,
UNIQUE(webflowProductId, printfulProductId)
)
File diff suppressed because it is too large Load Diff
+499
View File
@@ -0,0 +1,499 @@
import { Activity, Attribute, Player, Gender, ActivityPerformance, lbToKg, StandardUnit } from "./util";
// 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
// Dash:
// https://marathonhandbook.com/average-100-meter-time/
// Broad Jump:
//
type LevelCalculatorConfig = {
expandIters?: number;
compressTo?: number;
}
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,
spread: number,
step: number,
ratio: "normal" | "inverse"
}
export type ActivityStandards = Record<Activity, {
metadata: {
attribute: Attribute,
generators: Generator[],
unit: StandardUnit,
name: string
},
standards: Standard[]
}>;
export class LevelCalculator {
cfg: Required<LevelCalculatorConfig>;
standards: Standards;
public constructor(standards: Standards, cfg: LevelCalculatorConfig = {}) {
this.cfg = {
expandIters: cfg.expandIters ?? 5,
compressTo: cfg.compressTo ?? 100
}
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;
}
}
type StandardsConfig = {
maxLevel: number
}
export class Standards {
readonly cfg: StandardsConfig;
private activityStandards: ActivityStandards;
constructor(activityStandards: ActivityStandards, cfg?: Partial<StandardsConfig>) {
this.cfg = {
maxLevel: cfg?.maxLevel ?? 100
};
// prepare data
this.activityStandards = structuredClone(activityStandards);
for (const activity of Object.keys(this.activityStandards) as Activity[]) {
// expand/compress
for (const standard of this.activityStandards[activity].standards) {
const expandedLevels = this.expandLevels(standard.levels, 5);
const compressedLevels = this.compressLevels(expandedLevels, this.cfg.maxLevel);
standard.levels = compressedLevels;
}
// generate data
const allGenerators = this.activityStandards[activity].metadata.generators;
const ageGenerators = allGenerators.filter(g => g.metric === "age");
for (const ageGenerator of ageGenerators) {
// TODO
}
const weightGenerators = allGenerators.filter(g => g.metric === "weight");
for (const weightGenerator of weightGenerators) {
for (const gender of Object.values(Gender)) {
const standardsByGender = this.byActivity(activity).byGender(gender).getAll();
const ages = [...new Set(standardsByGender.map(s => s.metrics.age))];
for (const age of ages) {
const referenceWeight = gender === Gender.Male ?
lbToKg(170) :
lbToKg(137);
const minWeight = referenceWeight - (weightGenerator.step * weightGenerator.spread);
const maxWeight = referenceWeight + (weightGenerator.step * weightGenerator.spread);
const referenceStandard = this
.byActivity(activity)
.byMetrics({ weight: referenceWeight, gender: gender, age: age })
.getOneInterpolated();
let currWeight = minWeight;
while (currWeight <= maxWeight) {
const newStandard: Standard = {
metrics: {
weight: currWeight,
age: age,
gender: gender
},
levels: {}
};
// apply allometric scaling to generate levels
for (const lvl in referenceStandard.levels) {
if (!referenceStandard.levels[lvl]) continue;
const scalingExponent = .1;
const coefficient = weightGenerator.ratio === "inverse" ?
referenceWeight / currWeight :
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 += weightGenerator.step;
}
}
}
// now that we generated metric data, remove the old data
this.activityStandards[activity].standards = this.activityStandards[activity].standards
.filter(s => s.metrics.weight !== -1);
}
}
}
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));
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 = (): Standard[] => {
const metrics: Metrics = state as Metrics;
const weights = [... new Set(this
.byActivity(activity)
.byGender(metrics.gender)
.getAll().map((s) => s.metrics.weight))];
const interpolatedStandards: Standard[] = weights
.map((w) => interpolateByAgeAndWeight({ gender: metrics.gender, age: metrics.age, weight: w }));
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;
}
private expandLevels(
levels: Levels,
i: number
): Levels {
if (i == 0) {
return levels;
}
const newLevels = {};
let j = 1;
for (let k = 0; k < Object.keys(levels).length; ++k) {
const currLevel = Object.keys(levels)[k];
const nextLevel = Object.keys(levels)[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,
targetLevel: number,
): Levels {
const levelsAmount = Object.keys(levels).length;
if (levelsAmount < targetLevel) {
throw new Error(
"Target levels amount must be greater than or equal to the current levels amount",
);
}
const ratio = levelsAmount / targetLevel;
const compressedLevels: Levels = {};
for (let i = 0; i < targetLevel; ++i) {
const ratioIndex = i * ratio;
const lowerIndex = Math.floor(ratioIndex);
const upperIndex = Math.ceil(ratioIndex);
const lowerValue = levels[lowerIndex + 1];
const upperValue = levels[upperIndex + 1];
const weight = ratioIndex - lowerIndex;
compressedLevels[i + 1] = lowerValue + (upperValue - lowerValue) * weight;
}
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 = {};
for (const lvl in lower) {
interpolatedLevels[lvl] = lerp(lvl);
}
return interpolatedLevels;
}
}
+67
View File
@@ -0,0 +1,67 @@
import { Metrics } from "../calculator/main";
// -------------------------------------------------------------------------------------------------
// DATA MODELS
// -------------------------------------------------------------------------------------------------
export interface Player {
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 feetToCm = (feet: number) => feet * 30.48;
export const inchesToCm = (inches: number) => inches * 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 + 1);
+162
View File
@@ -0,0 +1,162 @@
export namespace Printful {
export const API_URL = "https://api.printful.com";
export namespace Webhook {
// meta
interface MetaData {
created: number;
retries: number;
store: number;
}
export enum Event {
ProductUpdated = "product_updated",
ProductDeleted = "product_deleted"
}
// product updated
export interface ProductUpdated extends MetaData {
type: Event.ProductUpdated,
data: {
sync_product: {
id: number;
external_id: string;
name: string;
variants: number;
synced: number;
thumbnail_url: string;
is_ignored: boolean;
};
}
}
// product deleted
export interface ProductDeleted extends MetaData {
type: Event.ProductDeleted,
data: {
sync_product: {
id: number;
external_id: string;
name: string;
};
}
}
export type EventPayload = ProductUpdated | ProductDeleted
}
export namespace Products {
export interface MetaDataSingle<T = any> {
code: number;
result: T;
}
export interface MetaDataMulti<T = any> {
code: number;
result: T[];
}
interface Option {
id: string;
value: string;
}
interface File {
type: string;
id: number;
url: string;
options: Option[];
hash: string;
filename: string;
mime_type: string;
size: number;
width: number;
height: number;
dpi: number;
status: string;
created: number;
thumbnail_url: string;
preview_url: string;
visible: boolean;
is_temporary: boolean;
stitch_count_tier: string;
}
export interface SyncProduct {
sync_product: {
id: number;
external_id: string;
name: string;
variants: number;
synced: number;
thumbnail_url: string;
is_ignored: boolean;
},
sync_variants: {
id: number;
external_id: string;
sync_product_id: number;
name: string;
synced: boolean;
variant_id: number;
retail_price: string;
currency: string;
is_ignored: boolean;
sku: string;
product: {
variant_id: number;
product_id: number;
image: string;
name: string;
};
files: File[];
options: Option[];
main_category_id: number;
warehouse_product_id: number;
warehouse_product_variant_id: number;
size: string;
color: string;
availability_status: string;
}[]
}
export async function get(syncProductId: number): Promise<SyncProduct> {
const payload: MetaDataSingle<SyncProduct> = await (
await fetch(`${Printful.API_URL}/store/products/${syncProductId}`, {
method: "GET",
headers: {
"Authorization": `Bearer ${Bun.env.PRINTFUL_AUTH}`,
"X-PF-Store-Id": `${Bun.env.PRINTFUL_STORE_ID}`
}
})
).json();
return payload.result;
}
export async function getAll(): Promise<SyncProduct[]> {
const payload: MetaDataMulti<SyncProduct["sync_product"]> = await (
await fetch(`${Printful.API_URL}/store/products`, {
method: "GET",
headers: {
"Authorization": `Bearer ${Bun.env.PRINTFUL_AUTH}`,
"X-PF-Store-Id": `${Bun.env.PRINTFUL_STORE_ID}`
}
})
).json();
// need to fetch variants
const syncProducts = await Promise.all(
payload.result.map(p => get(p.id))
);
return syncProducts;
}
export function getVariantPreviewUrl(syncVariant: Printful.Products.SyncProduct["sync_variants"][number]): string {
const previewFile = syncVariant.files.find(f => f.type === "preview");
return previewFile?.preview_url ?? syncVariant.product.image;
}
}
}
+278
View File
@@ -0,0 +1,278 @@
import { Printful } from "./printful"
export namespace Webflow {
export const API_SITES_URL = `https://api.webflow.com/v2/sites/${Bun.env.WEBFLOW_SITE_ID}`;
export const API_COLLECTIONS_URL = `https://api.webflow.com/v2/collections/${Bun.env.WEBFLOW_COLLECTION_ID}`;
const formatSlug = (name: string): string => {
return name
.toLowerCase()
.replace(/[^a-z0-9-_]/g, '-') // replace illegal chars with dashes
.replace(/^-+/, '') // remove leading dashes
.replace(/-+$/, '') // remove trailing dashes
.replace(/--+/g, '-') // collapse multiple dashes
.replace(/^([^a-z0-9_])/, '_$1'); // if it starts with an invalid char, prefix underscore
};
export namespace Products {
export interface Sku {
fieldData: {
name: string;
slug: string;
"sku-values"?: Record<string, string>,
price: {
value: number;
unit: string;
currency: string;
};
"main-image"?: string;
}
}
export interface UpsertProduct {
product: {
fieldData: {
name: string;
slug: string;
description: string;
shippable?: boolean;
"tax-category"?: string;
"sku-properties": {
id: string;
name: string;
enum: {
id: string;
name: string;
slug: string;
}[];
}[];
}
};
sku: Sku;
publishStatus?: "staging" | "live";
}
export async function remove(webflowProductId: string) {
const res = await fetch(`${Webflow.API_COLLECTIONS_URL}/items/${webflowProductId}`, {
method: "DELETE",
headers: {
"Content-Type": "application/json",
"Authorization": `bearer ${Bun.env.WEBFLOW_AUTH}`
},
body: JSON.stringify({})
});
}
export async function exists(webflowProductId: string): Promise<boolean> {
let findProductResponse = await fetch(`${Webflow.API_SITES_URL}/products/${webflowProductId}`, {
method: "GET",
headers: {
"Authorization": `bearer ${Bun.env.WEBFLOW_AUTH}`,
}
});
return findProductResponse.status === 200;
}
export async function updateUsingPrintful(printfulProduct: Printful.Products.SyncProduct) {
const webflowProductId = printfulProduct.sync_product.external_id;
const printfulVariants = printfulProduct.sync_variants;
const webflowVariants: Products.Sku[] = [];
for (const printfulVariant of printfulVariants) {
webflowVariants.push({
"fieldData": {
"name": printfulVariant.name,
"slug": formatSlug(printfulVariant.name),
"sku-values": {
"color": printfulVariant.color,
"size": printfulVariant.size,
},
"price": {
"value": +printfulVariant.retail_price * 100,
"unit": printfulVariant.currency,
"currency": printfulVariant.currency
},
"main-image": Printful.Products.getVariantPreviewUrl(printfulVariant)
}
});
}
const foundColors = Array.from(new Set(printfulVariants.map(v => v.color)));
const foundSizes = Array.from(new Set(printfulVariants.map(v => v.size)));
let updateProductResponse = await fetch(`${Webflow.API_SITES_URL}/products/${webflowProductId}`, {
method: "PATCH",
headers: {
"Content-Type": "application/json",
"Authorization": `bearer ${Bun.env.WEBFLOW_AUTH}`,
},
body: JSON.stringify({
"product": {
"fieldData": {
"name": printfulProduct.sync_product.name,
"slug": formatSlug(printfulProduct.sync_product.name),
"shippable": true,
"tax-category": "standard-taxable",
"sku-properties": [
{
"id": "color",
"name": "Color",
"enum": foundColors.map(color => ({
"id": color,
"slug": formatSlug(color),
"name": color
}))
},
{
"id": "size",
"name": "Size",
"enum": foundSizes.map(size => ({
"id": size,
"slug": formatSlug(size),
"name": size
}))
},
]
}
},
"sku": webflowVariants[0],
} as Products.UpsertProduct)
});
updateProductResponse = await updateProductResponse.json();
const getProductResponse = await fetch(`${Webflow.API_SITES_URL}/products/${webflowProductId}`, {
method: "GET",
headers: {
"Authorization": `bearer ${Bun.env.WEBFLOW_AUTH}`
}
});
const webflowProduct = await getProductResponse.json();
for (let i = 0; i < webflowProduct["skus"].length; ++i) {
const webflowSkuId = printfulVariants[i].external_id;
const res = await fetch(`${Webflow.API_SITES_URL}/products/${webflowProductId}/skus/${webflowSkuId}`, {
method: "PATCH",
headers: {
"Content-Type": "application/json",
"Authorization": `bearer ${Bun.env.WEBFLOW_AUTH}`
},
body: JSON.stringify({
"sku": webflowVariants[i]
})
});
}
}
export async function createUsingPrintful(printfulProduct: Printful.Products.SyncProduct) {
const printfulVariants = printfulProduct.sync_variants;
const webflowVariants: Products.Sku[] = [];
for (const printfulVariant of printfulVariants) {
webflowVariants.push({
"fieldData": {
"name": printfulVariant.name,
"slug": formatSlug(printfulVariant.name),
"sku-values": {
"color": printfulVariant.color,
"size": printfulVariant.size,
},
"price": {
"value": +printfulVariant.retail_price * 100,
"unit": printfulVariant.currency,
"currency": printfulVariant.currency
},
"main-image": Printful.Products.getVariantPreviewUrl(printfulVariant)
}
});
}
const foundColors = Array.from(new Set(printfulVariants.map(v => v.color)));
const foundSizes = Array.from(new Set(printfulVariants.map(v => v.size)));
// CREATE WEBFLOW PRODUCT
let addProductResponse = await (await fetch(`${Webflow.API_SITES_URL}/products`, {
method: "POST",
headers: {
"Content-Type": "application/json",
"Authorization": `bearer ${Bun.env.WEBFLOW_AUTH}`,
},
body: JSON.stringify({
"product": {
"fieldData": {
"name": printfulProduct.sync_product.name,
"slug": formatSlug(printfulProduct.sync_product.name),
"shippable": true,
"tax-category": "standard-taxable",
"sku-properties": [
{
"id": "color",
"name": "Color",
"enum": foundColors.map(color => ({
"id": color,
"slug": formatSlug(color),
"name": color
}))
},
{
"id": "size",
"name": "Size",
"enum": foundSizes.map(size => ({
"id": size,
"slug": formatSlug(size),
"name": size
}))
},
]
}
},
"sku": webflowVariants[0],
} as Products.UpsertProduct)
}))?.json();
if (!addProductResponse?.product?.id) {
console.error("Webflow product creation failed:", addProductResponse);
throw new Error("Failed to create Webflow product");
}
const webflowProductId = addProductResponse["product"]["id"];
const printfulProductId = printfulProduct.sync_product.id;
// CREATE WEBFLOW PRODUCT SKUs
let createSkuResponse = await fetch(`${Webflow.API_SITES_URL}/products/${webflowProductId}/skus`, {
method: "POST",
headers: {
"Content-Type": "application/json",
"Authorization": `bearer ${Bun.env.WEBFLOW_AUTH}`
},
body: JSON.stringify({
"skus": webflowVariants.slice(1)
})
});
const createSkuObj = await createSkuResponse.json();
const responseSkus = [addProductResponse["sku"], ...createSkuObj["skus"]];
// SYNC WEBFLOW/PRINTFUL PRODUCT AND VARIANT IDs
let modifyPrintfulProductResponse = await fetch(`${Printful.API_URL}/store/products/${printfulProductId}`, {
method: "PUT",
headers: {
"Content-Type": "application/json",
"Authorization": `Bearer ${Bun.env.PRINTFUL_AUTH}`,
"X-PF-Store-Id": Bun.env.PRINTFUL_STORE_ID ?? ""
},
body: JSON.stringify({
"sync_product": {
"external_id": String(webflowProductId)
},
"sync_variants": responseSkus.map((sku, i) => ({
"id": Number(printfulVariants[i].id), // printful variant id
"external_id": String(sku.id), // webflow variant id
}))
})
});
modifyPrintfulProductResponse = await modifyPrintfulProductResponse.json();
}
}
}
+75
View File
@@ -0,0 +1,75 @@
import { Activity, ActivityPerformance, feetToCm, Gender, inchesToCm, lbToKg, minToMs, Player, secToMs } from "../services/calculator/util";
import { ActivityStandards, LevelCalculator, Standards } from "../services/calculator/main"
import rawStandards from "../data/standards.json" assert { type: "json" }
const player: Player = {
metrics: {
age: 25,
weight: lbToKg(190),
gender: Gender.Male
}
};
const computedPerformances: ActivityPerformance[] = [
// STRENGTH
{
activity: Activity.BenchPress,
performance: lbToKg(225)
},
{
activity: Activity.Deadlift,
performance: lbToKg(270),
},
{
activity: Activity.BackSquat,
performance: lbToKg(190),
},
// POWER
{
activity: Activity.BroadJump,
performance: feetToCm(105) + inchesToCm(5),
},
// ENDURANCE
{
activity: Activity.Run,
performance: minToMs(7) + secToMs(15),
},
// AGILITY
{
activity: Activity.ConeDrill,
performance: secToMs(9),
},
];
// const newStandards: Standards = {
// "Back Squat": [],
// "Deadlift": [],
// "Bench Press": [],
// "Run": [],
// "Dash": [],
// "Treadmill Dash": [],
// "Broad Jump": [],
// "Cone Drill": [],
// };
//
// for (const item of standards) {
// if (!newStandards[item.activityType])
// continue;
//
// newStandards[item.activityType].push({
// metrics: {
// age: item.age,
// weight: item.bodyWeight,
// gender: item.gender
// },
// levels: {
// physicallyActive: item.physicallyActive,
// beginner: item.beginner,
// intermediate: item.intermediate,
// advanced: item.advanced,
// elite: item.elite
// }
// });
// }
//
// Bun.write("./newStandards.json", JSON.stringify(newStandards, null, 2));
+271
View File
@@ -0,0 +1,271 @@
// this file is generated — do not edit it
/// <reference types="@sveltejs/kit" />
/**
* Environment variables [loaded by Vite](https://vitejs.dev/guide/env-and-mode.html#env-files) from `.env` files and `process.env`. Like [`$env/dynamic/private`](https://svelte.dev/docs/kit/$env-dynamic-private), this module cannot be imported into client-side code. This module only includes variables that _do not_ begin with [`config.kit.env.publicPrefix`](https://svelte.dev/docs/kit/configuration#env) _and do_ start with [`config.kit.env.privatePrefix`](https://svelte.dev/docs/kit/configuration#env) (if configured).
*
* _Unlike_ [`$env/dynamic/private`](https://svelte.dev/docs/kit/$env-dynamic-private), the values exported from this module are statically injected into your bundle at build time, enabling optimisations like dead code elimination.
*
* ```ts
* import { API_KEY } from '$env/static/private';
* ```
*
* Note that all environment variables referenced in your code should be declared (for example in an `.env` file), even if they don't have a value until the app is deployed:
*
* ```
* MY_FEATURE_FLAG=""
* ```
*
* You can override `.env` values from the command line like so:
*
* ```sh
* MY_FEATURE_FLAG="enabled" npm run dev
* ```
*/
declare module '$env/static/private' {
export const IMSETTINGS_INTEGRATE_DESKTOP: string;
export const SHELL: string;
export const npm_command: string;
export const SESSION_MANAGER: string;
export const WINDOWID: string;
export const COLORTERM: string;
export const XDG_CONFIG_DIRS: string;
export const KGLOBALACCELD_PLATFORM: string;
export const XDG_SESSION_PATH: string;
export const HISTCONTROL: string;
export const XDG_MENU_PREFIX: string;
export const HISTSIZE: string;
export const HOSTNAME: string;
export const ICEAUTHORITY: string;
export const LANGUAGE: string;
export const NODE: string;
export const DOTNET_ROOT: string;
export const SSH_AUTH_SOCK: string;
export const FLYCTL_INSTALL: string;
export const SHELL_SESSION_ID: string;
export const MEMORY_PRESSURE_WRITE: string;
export const npm_config_local_prefix: string;
export const XMODIFIERS: string;
export const DESKTOP_SESSION: string;
export const SSH_AGENT_PID: string;
export const GTK_RC_FILES: string;
export const GDK_CORE_DEVICE_EVENTS: string;
export const GPG_TTY: string;
export const EDITOR: string;
export const XDG_SEAT: string;
export const PWD: string;
export const NIX_PROFILES: string;
export const LOGNAME: string;
export const XDG_SESSION_DESKTOP: string;
export const XDG_SESSION_TYPE: string;
export const SYSTEMD_EXEC_PID: string;
export const XAUTHORITY: string;
export const SDL_VIDEO_MINIMIZE_ON_FOCUS_LOSS: string;
export const XKB_DEFAULT_MODEL: string;
export const GTK2_RC_FILES: string;
export const HOME: string;
export const SSH_ASKPASS: string;
export const LANG: string;
export const LS_COLORS: string;
export const XDG_CURRENT_DESKTOP: string;
export const KONSOLE_DBUS_SERVICE: string;
export const MEMORY_PRESSURE_WATCH: string;
export const WAYLAND_DISPLAY: string;
export const NIX_SSL_CERT_FILE: string;
export const KONSOLE_DBUS_SESSION: string;
export const PROFILEHOME: string;
export const XDG_SEAT_PATH: string;
export const INVOCATION_ID: string;
export const KONSOLE_VERSION: string;
export const MANAGERPID: string;
export const IMSETTINGS_MODULE: string;
export const DOTNET_BUNDLE_EXTRACT_BASE_DIR: string;
export const STEAM_FRAME_FORCE_CLOSE: string;
export const KDE_SESSION_UID: string;
export const npm_lifecycle_script: string;
export const MOZ_GMP_PATH: string;
export const NVM_DIR: string;
export const XKB_DEFAULT_LAYOUT: string;
export const XDG_SESSION_CLASS: string;
export const TERM: string;
export const LESSOPEN: string;
export const USER: string;
export const COLORFGBG: string;
export const QT_WAYLAND_RECONNECT: string;
export const KDE_SESSION_VERSION: string;
export const PAM_KWALLET5_LOGIN: string;
export const DISPLAY: string;
export const npm_lifecycle_event: string;
export const SHLVL: string;
export const XDG_VTNR: string;
export const XDG_SESSION_ID: string;
export const npm_config_user_agent: string;
export const npm_execpath: string;
export const XDG_RUNTIME_DIR: string;
export const DEBUGINFOD_URLS: string;
export const npm_package_json: string;
export const BUN_INSTALL: string;
export const DEBUGINFOD_IMA_CERT_PATH: string;
export const KDEDIRS: string;
export const JOURNAL_STREAM: string;
export const XDG_DATA_DIRS: string;
export const KDE_FULL_SESSION: string;
export const PATH: string;
export const DBUS_SESSION_BUS_ADDRESS: string;
export const KDE_APPLICATIONS_AS_SCOPE: string;
export const MAIL: string;
export const npm_node_execpath: string;
export const OLDPWD: string;
export const GOPATH: string;
export const KONSOLE_DBUS_WINDOW: string;
export const _: string;
export const NODE_ENV: string;
}
/**
* Similar to [`$env/static/private`](https://svelte.dev/docs/kit/$env-static-private), except that it only includes environment variables that begin with [`config.kit.env.publicPrefix`](https://svelte.dev/docs/kit/configuration#env) (which defaults to `PUBLIC_`), and can therefore safely be exposed to client-side code.
*
* Values are replaced statically at build time.
*
* ```ts
* import { PUBLIC_BASE_URL } from '$env/static/public';
* ```
*/
declare module '$env/static/public' {
}
/**
* This module provides access to runtime environment variables, as defined by the platform you're running on. For example if you're using [`adapter-node`](https://github.com/sveltejs/kit/tree/main/packages/adapter-node) (or running [`vite preview`](https://svelte.dev/docs/kit/cli)), this is equivalent to `process.env`. This module only includes variables that _do not_ begin with [`config.kit.env.publicPrefix`](https://svelte.dev/docs/kit/configuration#env) _and do_ start with [`config.kit.env.privatePrefix`](https://svelte.dev/docs/kit/configuration#env) (if configured).
*
* This module cannot be imported into client-side code.
*
* ```ts
* import { env } from '$env/dynamic/private';
* console.log(env.DEPLOYMENT_SPECIFIC_VARIABLE);
* ```
*
* > [!NOTE] In `dev`, `$env/dynamic` always includes environment variables from `.env`. In `prod`, this behavior will depend on your adapter.
*/
declare module '$env/dynamic/private' {
export const env: {
IMSETTINGS_INTEGRATE_DESKTOP: string;
SHELL: string;
npm_command: string;
SESSION_MANAGER: string;
WINDOWID: string;
COLORTERM: string;
XDG_CONFIG_DIRS: string;
KGLOBALACCELD_PLATFORM: string;
XDG_SESSION_PATH: string;
HISTCONTROL: string;
XDG_MENU_PREFIX: string;
HISTSIZE: string;
HOSTNAME: string;
ICEAUTHORITY: string;
LANGUAGE: string;
NODE: string;
DOTNET_ROOT: string;
SSH_AUTH_SOCK: string;
FLYCTL_INSTALL: string;
SHELL_SESSION_ID: string;
MEMORY_PRESSURE_WRITE: string;
npm_config_local_prefix: string;
XMODIFIERS: string;
DESKTOP_SESSION: string;
SSH_AGENT_PID: string;
GTK_RC_FILES: string;
GDK_CORE_DEVICE_EVENTS: string;
GPG_TTY: string;
EDITOR: string;
XDG_SEAT: string;
PWD: string;
NIX_PROFILES: string;
LOGNAME: string;
XDG_SESSION_DESKTOP: string;
XDG_SESSION_TYPE: string;
SYSTEMD_EXEC_PID: string;
XAUTHORITY: string;
SDL_VIDEO_MINIMIZE_ON_FOCUS_LOSS: string;
XKB_DEFAULT_MODEL: string;
GTK2_RC_FILES: string;
HOME: string;
SSH_ASKPASS: string;
LANG: string;
LS_COLORS: string;
XDG_CURRENT_DESKTOP: string;
KONSOLE_DBUS_SERVICE: string;
MEMORY_PRESSURE_WATCH: string;
WAYLAND_DISPLAY: string;
NIX_SSL_CERT_FILE: string;
KONSOLE_DBUS_SESSION: string;
PROFILEHOME: string;
XDG_SEAT_PATH: string;
INVOCATION_ID: string;
KONSOLE_VERSION: string;
MANAGERPID: string;
IMSETTINGS_MODULE: string;
DOTNET_BUNDLE_EXTRACT_BASE_DIR: string;
STEAM_FRAME_FORCE_CLOSE: string;
KDE_SESSION_UID: string;
npm_lifecycle_script: string;
MOZ_GMP_PATH: string;
NVM_DIR: string;
XKB_DEFAULT_LAYOUT: string;
XDG_SESSION_CLASS: string;
TERM: string;
LESSOPEN: string;
USER: string;
COLORFGBG: string;
QT_WAYLAND_RECONNECT: string;
KDE_SESSION_VERSION: string;
PAM_KWALLET5_LOGIN: string;
DISPLAY: string;
npm_lifecycle_event: string;
SHLVL: string;
XDG_VTNR: string;
XDG_SESSION_ID: string;
npm_config_user_agent: string;
npm_execpath: string;
XDG_RUNTIME_DIR: string;
DEBUGINFOD_URLS: string;
npm_package_json: string;
BUN_INSTALL: string;
DEBUGINFOD_IMA_CERT_PATH: string;
KDEDIRS: string;
JOURNAL_STREAM: string;
XDG_DATA_DIRS: string;
KDE_FULL_SESSION: string;
PATH: string;
DBUS_SESSION_BUS_ADDRESS: string;
KDE_APPLICATIONS_AS_SCOPE: string;
MAIL: string;
npm_node_execpath: string;
OLDPWD: string;
GOPATH: string;
KONSOLE_DBUS_WINDOW: string;
_: string;
NODE_ENV: string;
[key: `PUBLIC_${string}`]: undefined;
[key: `${string}`]: string | undefined;
}
}
/**
* Similar to [`$env/dynamic/private`](https://svelte.dev/docs/kit/$env-dynamic-private), but only includes variables that begin with [`config.kit.env.publicPrefix`](https://svelte.dev/docs/kit/configuration#env) (which defaults to `PUBLIC_`), and can therefore safely be exposed to client-side code.
*
* Note that public dynamic environment variables must all be sent from the server to the client, causing larger network requests — when possible, use `$env/static/public` instead.
*
* ```ts
* import { env } from '$env/dynamic/public';
* console.log(env.PUBLIC_DEPLOYMENT_SPECIFIC_VARIABLE);
* ```
*/
declare module '$env/dynamic/public' {
export const env: {
[key: `PUBLIC_${string}`]: string | undefined;
}
}
@@ -0,0 +1,28 @@
export { matchers } from './matchers.js';
export const nodes = [
() => import('./nodes/0'),
() => import('./nodes/1'),
() => import('./nodes/2')
];
export const server_loads = [0];
export const dictionary = {
"/": [2]
};
export const hooks = {
handleError: (({ error }) => { console.error(error) }),
reroute: (() => {}),
transport: {}
};
export const decoders = Object.fromEntries(Object.entries(hooks.transport).map(([k, v]) => [k, v.decode]));
export const hash = false;
export const decode = (type, value) => decoders[type](value);
export { default as root } from '../root.js';
@@ -0,0 +1 @@
export const matchers = {};
@@ -0,0 +1 @@
export { default as component } from "../../../../src/routes/+layout.svelte";
@@ -0,0 +1 @@
export { default as component } from "../../../../../../node_modules/@sveltejs/kit/src/runtime/components/svelte-5/error.svelte";
@@ -0,0 +1 @@
export { default as component } from "../../../../src/routes/+page.svelte";
+3
View File
@@ -0,0 +1,3 @@
import { asClassComponent } from 'svelte/legacy';
import Root from './root.svelte';
export default asClassComponent(Root);
@@ -0,0 +1,66 @@
<!-- This file is generated by @sveltejs/kit — do not edit it! -->
<svelte:options runes={true} />
<script>
import { setContext, onMount, tick } from 'svelte';
import { browser } from '$app/environment';
// stores
let { stores, page, constructors, components = [], form, data_0 = null, data_1 = null } = $props();
if (!browser) {
setContext('__svelte__', stores);
}
if (browser) {
$effect.pre(() => stores.page.set(page));
} else {
stores.page.set(page);
}
$effect(() => {
stores;page;constructors;components;form;data_0;data_1;
stores.page.notify();
});
let mounted = $state(false);
let navigated = $state(false);
let title = $state(null);
onMount(() => {
const unsubscribe = stores.page.subscribe(() => {
if (mounted) {
navigated = true;
tick().then(() => {
title = document.title || 'untitled page';
});
}
});
mounted = true;
return unsubscribe;
});
const Pyramid_1=$derived(constructors[1])
</script>
{#if constructors[1]}
{@const Pyramid_0 = constructors[0]}
<!-- svelte-ignore binding_property_non_reactive -->
<Pyramid_0 bind:this={components[0]} data={data_0} {form} params={page.params}>
<!-- svelte-ignore binding_property_non_reactive -->
<Pyramid_1 bind:this={components[1]} data={data_1} {form} params={page.params} />
</Pyramid_0>
{:else}
{@const Pyramid_0 = constructors[0]}
<!-- svelte-ignore binding_property_non_reactive -->
<Pyramid_0 bind:this={components[0]} data={data_0} {form} params={page.params} />
{/if}
{#if mounted}
<div id="svelte-announcer" aria-live="assertive" aria-atomic="true" style="position: absolute; left: 0; top: 0; clip: rect(0 0 0 0); clip-path: inset(50%); overflow: hidden; white-space: nowrap; width: 1px; height: 1px">
{#if navigated}
{title}
{/if}
</div>
{/if}
@@ -0,0 +1,52 @@
import root from '../root.js';
import { set_building, set_prerendering } from '__sveltekit/environment';
import { set_assets } from '__sveltekit/paths';
import { set_manifest, set_read_implementation } from '__sveltekit/server';
import { set_private_env, set_public_env } from '../../../../../node_modules/@sveltejs/kit/src/runtime/shared-server.js';
export const options = {
app_template_contains_nonce: false,
csp: {"mode":"auto","directives":{"upgrade-insecure-requests":false,"block-all-mixed-content":false},"reportOnly":{"upgrade-insecure-requests":false,"block-all-mixed-content":false}},
csrf_check_origin: true,
csrf_trusted_origins: [],
embedded: false,
env_public_prefix: 'PUBLIC_',
env_private_prefix: '',
hash_routing: false,
hooks: null, // added lazily, via `get_hooks`
preload_strategy: "modulepreload",
root,
service_worker: false,
service_worker_options: undefined,
templates: {
app: ({ head, body, assets, nonce, env }) => "<!doctype html>\n<html lang=\"en\">\n\t<head>\n\t\t<meta charset=\"utf-8\" />\n\t\t<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n\t\t" + head + "\n\t</head>\n\t<body data-sveltekit-preload-data=\"hover\">\n\t\t<div style=\"display: contents\">" + body + "</div>\n\t</body>\n</html>\n",
error: ({ status, message }) => "<!doctype html>\n<html lang=\"en\">\n\t<head>\n\t\t<meta charset=\"utf-8\" />\n\t\t<title>" + message + "</title>\n\n\t\t<style>\n\t\t\tbody {\n\t\t\t\t--bg: white;\n\t\t\t\t--fg: #222;\n\t\t\t\t--divider: #ccc;\n\t\t\t\tbackground: var(--bg);\n\t\t\t\tcolor: var(--fg);\n\t\t\t\tfont-family:\n\t\t\t\t\tsystem-ui,\n\t\t\t\t\t-apple-system,\n\t\t\t\t\tBlinkMacSystemFont,\n\t\t\t\t\t'Segoe UI',\n\t\t\t\t\tRoboto,\n\t\t\t\t\tOxygen,\n\t\t\t\t\tUbuntu,\n\t\t\t\t\tCantarell,\n\t\t\t\t\t'Open Sans',\n\t\t\t\t\t'Helvetica Neue',\n\t\t\t\t\tsans-serif;\n\t\t\t\tdisplay: flex;\n\t\t\t\talign-items: center;\n\t\t\t\tjustify-content: center;\n\t\t\t\theight: 100vh;\n\t\t\t\tmargin: 0;\n\t\t\t}\n\n\t\t\t.error {\n\t\t\t\tdisplay: flex;\n\t\t\t\talign-items: center;\n\t\t\t\tmax-width: 32rem;\n\t\t\t\tmargin: 0 1rem;\n\t\t\t}\n\n\t\t\t.status {\n\t\t\t\tfont-weight: 200;\n\t\t\t\tfont-size: 3rem;\n\t\t\t\tline-height: 1;\n\t\t\t\tposition: relative;\n\t\t\t\ttop: -0.05rem;\n\t\t\t}\n\n\t\t\t.message {\n\t\t\t\tborder-left: 1px solid var(--divider);\n\t\t\t\tpadding: 0 0 0 1rem;\n\t\t\t\tmargin: 0 0 0 1rem;\n\t\t\t\tmin-height: 2.5rem;\n\t\t\t\tdisplay: flex;\n\t\t\t\talign-items: center;\n\t\t\t}\n\n\t\t\t.message h1 {\n\t\t\t\tfont-weight: 400;\n\t\t\t\tfont-size: 1em;\n\t\t\t\tmargin: 0;\n\t\t\t}\n\n\t\t\t@media (prefers-color-scheme: dark) {\n\t\t\t\tbody {\n\t\t\t\t\t--bg: #222;\n\t\t\t\t\t--fg: #ddd;\n\t\t\t\t\t--divider: #666;\n\t\t\t\t}\n\t\t\t}\n\t\t</style>\n\t</head>\n\t<body>\n\t\t<div class=\"error\">\n\t\t\t<span class=\"status\">" + status + "</span>\n\t\t\t<div class=\"message\">\n\t\t\t\t<h1>" + message + "</h1>\n\t\t\t</div>\n\t\t</div>\n\t</body>\n</html>\n"
},
version_hash: "hxbxsl"
};
export async function get_hooks() {
let handle;
let handleFetch;
let handleError;
let handleValidationError;
let init;
let reroute;
let transport;
return {
handle,
handleFetch,
handleError,
handleValidationError,
init,
reroute,
transport
};
}
export { set_assets, set_building, set_manifest, set_prerendering, set_private_env, set_public_env, set_read_implementation };
+42
View File
@@ -0,0 +1,42 @@
// this file is generated — do not edit it
declare module "svelte/elements" {
export interface HTMLAttributes<T> {
'data-sveltekit-keepfocus'?: true | '' | 'off' | undefined | null;
'data-sveltekit-noscroll'?: true | '' | 'off' | undefined | null;
'data-sveltekit-preload-code'?:
| true
| ''
| 'eager'
| 'viewport'
| 'hover'
| 'tap'
| 'off'
| undefined
| null;
'data-sveltekit-preload-data'?: true | '' | 'hover' | 'tap' | 'off' | undefined | null;
'data-sveltekit-reload'?: true | '' | 'off' | undefined | null;
'data-sveltekit-replacestate'?: true | '' | 'off' | undefined | null;
}
}
export {};
declare module "$app/types" {
export interface AppTypes {
RouteId(): "/" | "/[...slugs]";
RouteParams(): {
"/[...slugs]": { slugs: string }
};
LayoutParams(): {
"/": { slugs?: string };
"/[...slugs]": { slugs: string }
};
Pathname(): "/" | `/${string}` & {} | `/${string}/` & {};
ResolvedPathname(): `${"" | `/${string}`}${ReturnType<AppTypes['Pathname']>}`;
Asset(): "/robots.txt" | string & {};
}
}
+52
View File
@@ -0,0 +1,52 @@
{
"compilerOptions": {
"paths": {
"$lib": [
"../src/lib"
],
"$lib/*": [
"../src/lib/*"
],
"$app/types": [
"./types/index.d.ts"
]
},
"rootDirs": [
"..",
"./types"
],
"verbatimModuleSyntax": true,
"isolatedModules": true,
"lib": [
"esnext",
"DOM",
"DOM.Iterable"
],
"moduleResolution": "bundler",
"module": "esnext",
"noEmit": true,
"target": "esnext"
},
"include": [
"ambient.d.ts",
"non-ambient.d.ts",
"./types/**/$types.d.ts",
"../vite.config.js",
"../vite.config.ts",
"../src/**/*.js",
"../src/**/*.ts",
"../src/**/*.svelte",
"../tests/**/*.js",
"../tests/**/*.ts",
"../tests/**/*.svelte"
],
"exclude": [
"../node_modules/**",
"../src/service-worker.js",
"../src/service-worker/**/*.js",
"../src/service-worker.ts",
"../src/service-worker/**/*.ts",
"../src/service-worker.d.ts",
"../src/service-worker/**/*.d.ts"
]
}
@@ -0,0 +1,9 @@
{
"/": [
"src/routes/+layout.server.ts",
"src/routes/+layout.server.ts"
],
"/[...slugs]": [
"src/routes/[...slugs]/+server.ts"
]
}
+28
View File
@@ -0,0 +1,28 @@
import type * as Kit from '@sveltejs/kit';
type Expand<T> = T extends infer O ? { [K in keyof O]: O[K] } : never;
// @ts-ignore
type MatcherParam<M> = M extends (param : string) => param is infer U ? U extends string ? U : string : string;
type RouteParams = { };
type RouteId = '/';
type MaybeWithVoid<T> = {} extends T ? T | void : T;
export type RequiredKeys<T> = { [K in keyof T]-?: {} extends { [P in K]: T[K] } ? never : K; }[keyof T];
type OutputDataShape<T> = MaybeWithVoid<Omit<App.PageData, RequiredKeys<T>> & Partial<Pick<App.PageData, keyof T & keyof App.PageData>> & Record<string, any>>
type EnsureDefined<T> = T extends null | undefined ? {} : T;
type OptionalUnion<U extends Record<string, any>, A extends keyof U = U extends U ? keyof U : never> = U extends unknown ? { [P in Exclude<A, keyof U>]?: never } & U : never;
export type Snapshot<T = any> = Kit.Snapshot<T>;
type PageParentData = EnsureDefined<LayoutData>;
type LayoutRouteId = RouteId | "/" | null
type LayoutParams = RouteParams & { }
type LayoutServerParentData = EnsureDefined<{}>;
type LayoutParentData = EnsureDefined<{}>;
export type PageServerData = null;
export type PageData = Expand<PageParentData>;
export type PageProps = { params: RouteParams; data: PageData }
export type LayoutServerLoad<OutputData extends OutputDataShape<LayoutServerParentData> = OutputDataShape<LayoutServerParentData>> = Kit.ServerLoad<LayoutParams, LayoutServerParentData, OutputData, LayoutRouteId>;
export type LayoutServerLoadEvent = Parameters<LayoutServerLoad>[0];
export type LayoutServerData = Expand<OptionalUnion<EnsureDefined<Kit.LoadProperties<Awaited<ReturnType<typeof import('./proxy+layout.server.js').load>>>>>>;
export type LayoutData = Expand<Omit<LayoutParentData, keyof LayoutServerData> & EnsureDefined<LayoutServerData>>;
export type LayoutProps = { params: LayoutParams; data: LayoutData; children: import("svelte").Snippet }
export type RequestEvent = Kit.RequestEvent<RouteParams, RouteId>;
@@ -0,0 +1,11 @@
import type * as Kit from '@sveltejs/kit';
type Expand<T> = T extends infer O ? { [K in keyof O]: O[K] } : never;
// @ts-ignore
type MatcherParam<M> = M extends (param : string) => param is infer U ? U extends string ? U : string : string;
type RouteParams = { slugs: string };
type RouteId = '/[...slugs]';
export type EntryGenerator = () => Promise<Array<RouteParams>> | Array<RouteParams>;
export type RequestHandler = Kit.RequestHandler<RouteParams, RouteId>;
export type RequestEvent = Kit.RequestEvent<RouteParams, RouteId>;
@@ -0,0 +1,12 @@
// @ts-nocheck
import { api } from "$lib";
import type { ActivityStandards } from "../../../services/calculator/main";
import type { LayoutServerLoad } from "./$types";
export const load = async ({ }: Parameters<LayoutServerLoad>[0]) => {
const { data: allStandardsRaw } = await api.data.standards.get();
return {
allStandardsRaw: allStandardsRaw as ActivityStandards
};
};
+27
View File
@@ -0,0 +1,27 @@
:root {
--bg: #0b0f14;
--fg: #e5e7eb;
--muted: #9ca3af;
--card: #0f141a;
--border: #1f2937;
--accent: #e5e7eb;
}
* {
box-sizing: border-box;
}
html,
body {
margin: 0;
padding: 0;
background: var(--bg);
color: var(--fg);
font:
14px/1.45 system-ui,
-apple-system,
Segoe UI,
Roboto,
Inter,
sans-serif;
}
+13
View File
@@ -0,0 +1,13 @@
// See https://svelte.dev/docs/kit/types#app.d.ts
// for information about these interfaces
declare global {
namespace App {
// interface Error {}
// interface Locals {}
// interface PageData {}
// interface PageState {}
// interface Platform {}
}
}
export {};
+11
View File
@@ -0,0 +1,11 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
%sveltekit.head%
</head>
<body data-sveltekit-preload-data="hover">
<div style="display: contents">%sveltekit.body%</div>
</body>
</html>
+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="107" height="128" viewBox="0 0 107 128"><title>svelte-logo</title><path d="M94.157 22.819c-10.4-14.885-30.94-19.297-45.792-9.835L22.282 29.608A29.92 29.92 0 0 0 8.764 49.65a31.5 31.5 0 0 0 3.108 20.231 30 30 0 0 0-4.477 11.183 31.9 31.9 0 0 0 5.448 24.116c10.402 14.887 30.942 19.297 45.791 9.835l26.083-16.624A29.92 29.92 0 0 0 98.235 78.35a31.53 31.53 0 0 0-3.105-20.232 30 30 0 0 0 4.474-11.182 31.88 31.88 0 0 0-5.447-24.116" style="fill:#ff3e00"/><path d="M45.817 106.582a20.72 20.72 0 0 1-22.237-8.243 19.17 19.17 0 0 1-3.277-14.503 18 18 0 0 1 .624-2.435l.49-1.498 1.337.981a33.6 33.6 0 0 0 10.203 5.098l.97.294-.09.968a5.85 5.85 0 0 0 1.052 3.878 6.24 6.24 0 0 0 6.695 2.485 5.8 5.8 0 0 0 1.603-.704L69.27 76.28a5.43 5.43 0 0 0 2.45-3.631 5.8 5.8 0 0 0-.987-4.371 6.24 6.24 0 0 0-6.698-2.487 5.7 5.7 0 0 0-1.6.704l-9.953 6.345a19 19 0 0 1-5.296 2.326 20.72 20.72 0 0 1-22.237-8.243 19.17 19.17 0 0 1-3.277-14.502 17.99 17.99 0 0 1 8.13-12.052l26.081-16.623a19 19 0 0 1 5.3-2.329 20.72 20.72 0 0 1 22.237 8.243 19.17 19.17 0 0 1 3.277 14.503 18 18 0 0 1-.624 2.435l-.49 1.498-1.337-.98a33.6 33.6 0 0 0-10.203-5.1l-.97-.294.09-.968a5.86 5.86 0 0 0-1.052-3.878 6.24 6.24 0 0 0-6.696-2.485 5.8 5.8 0 0 0-1.602.704L37.73 51.72a5.42 5.42 0 0 0-2.449 3.63 5.79 5.79 0 0 0 .986 4.372 6.24 6.24 0 0 0 6.698 2.486 5.8 5.8 0 0 0 1.602-.704l9.952-6.342a19 19 0 0 1 5.295-2.328 20.72 20.72 0 0 1 22.237 8.242 19.17 19.17 0 0 1 3.277 14.503 18 18 0 0 1-8.13 12.053l-26.081 16.622a19 19 0 0 1-5.3 2.328" style="fill:#fff"/></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

+6
View File
@@ -0,0 +1,6 @@
import { treaty } from "@elysiajs/eden";
import { app, type App } from "../../../api";
const apiWrapper = treaty<App>(app);
export const api = apiWrapper.api;
+11
View File
@@ -0,0 +1,11 @@
import { api } from "$lib";
import type { ActivityStandards } from "../../../services/calculator/main";
import type { LayoutServerLoad } from "./$types";
export const load: LayoutServerLoad = async ({ }) => {
const { data: allStandardsRaw } = await api.data.standards.get();
return {
allStandardsRaw: allStandardsRaw as ActivityStandards
};
};
+12
View File
@@ -0,0 +1,12 @@
<script lang="ts">
import favicon from "$lib/assets/favicon.svg";
import "../app.css";
let { children } = $props();
</script>
<svelte:head>
<link rel="icon" href={favicon} />
</svelte:head>
{@render children?.()}
+407
View File
@@ -0,0 +1,407 @@
<script lang="ts">
import {
Standards,
type Standard,
} from "../../../services/calculator/main";
import {
Activity,
Gender,
kgToLb,
lbToKg,
msToTime,
range,
} from "../../../services/calculator/util";
// data
const { data } = $props();
const selected = $state({
activity: Activity.BenchPress,
gender: Gender.Male,
age: 18,
weight: 0,
maxLevel: 5,
});
const allStandards = $derived(
new Standards(data.allStandardsRaw, { maxLevel: selected.maxLevel }),
);
const getFormattedLevelValue = (
activity: Activity,
standard: Standard,
lvl: number,
): string => {
const rawValue = standard.levels[lvl];
const unit = allStandards.byActivity(activity).getMetadata().unit;
switch (unit) {
case "ms":
return msToTime(rawValue);
case "cm":
return String(Math.round(rawValue));
case "kg":
return String(Math.round(kgToLb(rawValue)));
default:
return "";
}
};
</script>
<div id="activityTables">
<div class="activity-section">
<div class="toolbar">
<fieldset class="group">
<legend>Metrics</legend>
<label class="field">
<span>Activity</span>
<select
class="select"
name="activities"
bind:value={selected.activity}
>
{#each Object.values(Activity) as activity}
<option value={activity}
>{allStandards
.byActivity(activity)
.getMetadata().name}</option
>
{/each}
</select>
</label>
<label class="field">
<span>Gender</span>
<select
class="select"
name="genders"
bind:value={selected.gender}
>
{#each Object.values(Gender) as gender}
<option value={gender}>{gender}</option>
{/each}
</select>
</label>
<label class="field w-20">
<span>Age</span>
<input
type="number"
min="0"
max="100"
placeholder="18"
bind:value={selected.age}
/>
</label>
<label class="field w-24">
<span>Weight (lb)</span>
<input
type="number"
min="0"
max="600"
placeholder="170"
bind:value={selected.weight}
/>
</label>
</fieldset>
<fieldset class="group">
<legend>Parameters</legend>
<label class="field w-24">
<span>Max level</span>
<input
type="number"
min="1"
max="100"
placeholder="5"
bind:value={selected.maxLevel}
/>
</label>
</fieldset>
</div>
{#if !selected.gender || !selected.age}
<p>Not enough info entered</p>
{:else}
{@render standardsTable(
selected.gender,
selected.age,
selected.weight,
)}
{/if}
</div>
</div>
{#snippet standardsTable(gender: Gender, age: number, weight: number)}
<section class="gender-section">
<div class="age-section">
<div class="standards-table">
<div class="scroll">
<table>
<thead>
<tr>
<th class="levels-th">Body weight</th>
{#each range(selected.maxLevel) as lvl}
<th class="levels-th">{lvl}</th>
{/each}
</tr>
</thead>
<tbody>
{#if weight}
{@const standard = allStandards
.byActivity(selected.activity)
.byGender(gender)
.byAge(age)
.byWeight(lbToKg(weight))
.getOneInterpolated()}
<tr>
<th scope="row"
>{Math.round(
kgToLb(standard.metrics.weight),
)}</th
>
{#each range(selected.maxLevel) as lvl}
<td
>{getFormattedLevelValue(
selected.activity,
standard,
lvl,
)}</td
>
{/each}
</tr>
{:else}
{@const standards = allStandards
.byActivity(selected.activity)
.byGender(gender)
.byAge(age)
.getAllInterpolated()}
{#each standards as standard}
<tr>
<th scope="row"
>{Math.round(
kgToLb(standard.metrics.weight),
)}</th
>
{#each range(allStandards.cfg.maxLevel) as lvl}
<td
>{getFormattedLevelValue(
selected.activity,
standard,
lvl,
)}</td
>
{/each}
</tr>
{/each}
{/if}
</tbody>
</table>
</div>
</div>
</div>
</section>
{/snippet}
<style>
#activityTables {
max-width: 1600px;
margin: 24px auto;
padding: 0 16px;
}
h1,
h2,
h3 {
line-height: 1.2;
margin: 0 0 8px;
color: var(--accent);
}
h1 {
font-size: 1.4rem;
margin-top: 24px;
}
h2 {
font-size: 1.1rem;
margin-top: 16px;
}
h3 {
font-size: 1rem;
color: var(--muted);
font-weight: 600;
}
/* Toolbar container */
.toolbar {
display: grid;
grid-template-columns: 1fr auto; /* metrics | params */
gap: 16px;
align-items: start;
}
/* Stack groups on small screens */
@media (max-width: 840px) {
.toolbar {
grid-template-columns: 1fr;
}
}
/* Group card */
.group {
margin: 0;
padding: 12px;
border: 1px solid var(--border);
background: var(--card);
display: flex;
flex-wrap: wrap;
gap: 12px 16px;
}
/* Group title */
.group > legend {
font-size: 12px;
font-weight: 700;
color: var(--muted);
padding: 0 6px;
}
/* Field (label+control) */
.field {
display: grid;
grid-template-rows: auto auto;
gap: 6px;
min-width: 160px;
}
/* Label text */
.field > span {
font-size: 12px;
font-weight: 600;
color: var(--muted);
}
/* Inputs/selects inherit your existing styles.
If you used the "select-as-title" style, keep it;
otherwise add a minimal baseline here: */
.field input[type="number"],
.field .select {
font-size: 1rem;
color: var(--fg);
background: transparent;
border: none;
border-bottom: 2px solid var(--border);
padding: 6px 0;
outline: none;
}
/* Focus */
.field input[type="number"]:focus,
.field .select:focus {
border-bottom-color: var(--accent);
}
/* Width helpers */
.w-20 {
width: 8rem;
} /* ~128px */
.w-24 {
width: 10rem;
} /* ~160px */
.activity-section {
padding: 16px;
margin: 16px 0;
background: var(--card);
border: 1px solid var(--border);
box-shadow: 0 1px 0 rgba(0, 0, 0, 0.03);
}
.gender-section {
margin-top: 8px;
}
.age-section {
margin: 12px 0 20px;
}
.standards-table {
margin-top: 8px;
border: 1px solid var(--border);
overflow: hidden;
}
/* Horizontal scroll wrapper for very wide tables */
.standards-table > .scroll {
overflow-x: auto;
-webkit-overflow-scrolling: touch;
}
/* Table */
.standards-table table {
width: max-content;
/* prevents squish; enables horizontal scroll */
min-width: 100%;
/* fill container if not too wide */
border-collapse: separate;
border-spacing: 0;
font-variant-numeric: tabular-nums;
}
.standards-table th,
.standards-table td {
padding: 6px 10px;
border-bottom: 1px solid var(--border);
border-right: 1px solid var(--border);
text-align: center;
white-space: nowrap;
}
.standards-table th:first-child,
.standards-table td:first-child {
border-left: 1px solid var(--border);
}
/* Header row */
.standards-table thead th {
position: sticky;
top: 0;
z-index: 2;
background: var(--bg);
font-weight: 700;
}
.standards-table thead th:first-child {
left: 0;
z-index: 3;
}
/* Row header (first column) */
.standards-table tbody th[scope="row"],
.standards-table tbody td:first-child {
position: sticky;
left: 0;
background: var(--card);
text-align: left;
font-weight: 600;
}
.standards-table tbody tr:nth-child(even) td {
background: rgba(0, 0, 0, 0.02);
}
.standards-table tbody tr:hover td {
background: rgba(0, 128, 255, 0.06);
}
/* Compact header for the “levels” row */
.levels-th {
font-size: 12px;
color: var(--muted);
}
</style>
@@ -0,0 +1,4 @@
import { app } from "../../../../api"
type RequestHandler = (v: { request: Request }) => Response | Promise<Response>
export const fallback: RequestHandler = ({ request }) => app.handle(request)
+3
View File
@@ -0,0 +1,3 @@
# allow crawling everything by default
User-agent: *
Disallow:
+18
View File
@@ -0,0 +1,18 @@
import adapter from "svelte-adapter-bun";
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
/** @type {import('@sveltejs/kit').Config} */
const config = {
// Consult https://svelte.dev/docs/kit/integrations
// for more information about preprocessors
preprocess: vitePreprocess(),
kit: {
// adapter-auto only supports some environments, see https://svelte.dev/docs/kit/adapter-auto for a list.
// If your environment is not supported, or you settled on a specific environment, switch out the adapter.
// See https://svelte.dev/docs/kit/adapters for more information about adapters.
adapter: adapter(),
}
};
export default config;
+20
View File
@@ -0,0 +1,20 @@
{
"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
}
+6
View File
@@ -0,0 +1,6 @@
import { sveltekit } from '@sveltejs/kit/vite';
import { defineConfig } from 'vite';
export default defineConfig({
plugins: [sveltekit()]
});