View update
This commit is contained in:
+88
@@ -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;
|
||||
Binary file not shown.
@@ -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
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
Vendored
+271
@@ -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";
|
||||
@@ -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
@@ -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 & {};
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
]
|
||||
}
|
||||
@@ -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
|
||||
};
|
||||
};
|
||||
@@ -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;
|
||||
}
|
||||
Vendored
+13
@@ -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 {};
|
||||
@@ -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>
|
||||
@@ -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 |
@@ -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;
|
||||
|
||||
@@ -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
|
||||
};
|
||||
};
|
||||
@@ -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?.()}
|
||||
@@ -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)
|
||||
@@ -0,0 +1,3 @@
|
||||
# allow crawling everything by default
|
||||
User-agent: *
|
||||
Disallow:
|
||||
@@ -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;
|
||||
@@ -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
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
import { sveltekit } from '@sveltejs/kit/vite';
|
||||
import { defineConfig } from 'vite';
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [sveltekit()]
|
||||
});
|
||||
Reference in New Issue
Block a user