Implement basic auth on portal

This commit is contained in:
Dominic Ferrando
2026-06-30 00:24:48 -04:00
parent 9778c05bf0
commit 235c072421
12 changed files with 101 additions and 27 deletions
@@ -0,0 +1,44 @@
<script lang="ts">
import { page } from "$app/state";
import favicon from "$lib/assets/favicon.svg";
import "../app.css";
const links = [
{ name: "Calculator", path: "/calculator" },
{ name: "Commerce", path: "/commerce" },
];
let { children } = $props();
</script>
<svelte:head>
<link rel="icon" href={favicon} />
</svelte:head>
<div class="drawer lg:drawer-open">
<input id="sidebar" type="checkbox" class="drawer-toggle" />
<div class="drawer-content p-10 flex flex-col items-center">
{@render children?.()}
<!-- <label for="sidebar" class="btn btn-primary drawer-button lg:hidden"> -->
<!-- Open drawer -->
<!-- </label> -->
</div>
<div class="drawer-side">
<label for="sidebar" aria-label="close sidebar" class="drawer-overlay"
></label>
<ul
class="menu menu-lg bg-base-200 text-base-content min-h-full w-72 p-4"
>
{#each links as link}
<li>
<a
class={{
"menu-active": page.url.pathname === link.path,
}}
href={link.path}>{link.name}</a
>
</li>
{/each}
</ul>
</div>
</div>
@@ -0,0 +1,6 @@
import { redirect } from "@sveltejs/kit";
import type { PageServerLoad } from "./$types";
export const load: PageServerLoad = ({}) => {
redirect(308, "/calculator");
};
@@ -0,0 +1,376 @@
<script lang="ts">
import {
DEFAULT_STANDARDS_DATA,
Standards,
type StandardsConfig,
} from "@blade-and-brawn/calculator";
import {
Activity,
Gender,
lbToKg,
type Metrics,
} from "@blade-and-brawn/domain";
import StandardsTable from "$lib/components/StandardsTable.svelte";
import PlayersTable from "$lib/components/PlayersTable.svelte";
let activeTab = $state("standards");
const defaultStandards = new Standards(DEFAULT_STANDARDS_DATA);
// TODO: try to clean this up
let input = $state({
activity: Activity.BenchPress,
metrics: {
gender: Gender.Male,
age: "",
weight: "",
},
cfg: {
global: {
maxLevel: String(defaultStandards.cfg.global.maxLevel),
},
activity: Object.fromEntries(
Object.values(Activity).map((activity) => {
return [
activity,
{
enableGeneration:
defaultStandards.cfg.activity[activity]
.enableGeneration,
weightAdvantage: String(
defaultStandards.cfg.activity[activity]
.weightModifier,
),
ageModifier: String(
defaultStandards.cfg.activity[activity]
.ageModifier,
),
difficultyModifier: String(
defaultStandards.cfg.activity[activity]
.difficultyModifier,
),
peakAge: String(
defaultStandards.cfg.activity[activity].peakAge,
),
stretch: {
upper: String(
defaultStandards.cfg.activity[activity]
.stretch.upper,
),
lower: String(
defaultStandards.cfg.activity[activity]
.stretch.lower,
),
},
},
];
}),
),
},
});
const selected = $derived({
activity: input.activity,
metrics: {
gender: input.metrics.gender,
age: +input.metrics.age,
weight: lbToKg(+input.metrics.weight),
} as Metrics,
cfg: {
global: {
maxLevel: +input.cfg.global.maxLevel,
},
activity: Object.fromEntries(
Object.values(Activity).map((activity) => [
activity,
{
weightModifier:
+input.cfg.activity[activity].weightAdvantage,
ageModifier: +input.cfg.activity[activity].ageModifier,
enableGeneration:
input.cfg.activity[activity].enableGeneration,
difficultyModifier:
+input.cfg.activity[activity].difficultyModifier,
peakAge: +input.cfg.activity[activity].peakAge,
stretch: {
lower: +input.cfg.activity[activity].stretch.lower,
upper: +input.cfg.activity[activity].stretch.upper,
},
},
]),
),
} as StandardsConfig,
});
const allStandards = $derived(
new Standards(DEFAULT_STANDARDS_DATA, selected.cfg),
);
$effect(() => {
const savedParameters = localStorage.getItem("parameters");
if (savedParameters) input = JSON.parse(savedParameters);
});
$effect(() => {
localStorage.setItem("parameters", JSON.stringify(input));
});
</script>
<section class="w-full flex justify-between gap-2 pt-10 items-center">
<fieldset class="fieldset bg-base-200 p-6 max-w-lg">
<legend class="fieldset-legend text-lg font-semibold"> Data </legend>
<div class="flex gap-4">
<fieldset class="fieldset bg-base-300 p-3 max-w-xl">
<legend class="fieldset-legend text-sm font-semibold">
General
</legend>
<div class="grid grid-cols-1 gap-4">
<label>
<span class="label mb-1">Activity</span>
<select
class="select w-full"
name="activities"
bind:value={input.activity}
>
{#each Object.values(Activity) as activity}
<option value={activity}>
{allStandards
.byActivity(activity)
.getMetadata().name}
</option>
{/each}
</select>
</label>
<label>
<span class="label mb-1">Max level</span>
<input
class="input w-full"
type="number"
min="1"
max="100"
placeholder="5"
bind:value={input.cfg.global.maxLevel}
/>
</label>
</div>
</fieldset>
<fieldset class="fieldset bg-base-300 p-3 max-w-3xs">
<legend class="fieldset-legend text-sm font-semibold">
Data Generation
</legend>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<label class="label">
<input
type="checkbox"
bind:checked={
input.cfg.activity[selected.activity]
.enableGeneration
}
class="toggle toggle-lg m-auto"
/>
</label>
<label>
<span class="label mb-1">Weight significance</span>
<input
class="input w-full"
type="number"
min="-1"
max="1"
step=".05"
placeholder=".1"
bind:value={
input.cfg.activity[selected.activity]
.weightAdvantage
}
disabled={!selected.cfg.activity[selected.activity]
.enableGeneration}
/>
</label>
<!-- <label> -->
<!-- <span class="label mb-1">Weight skew</span> -->
<!-- <input -->
<!-- class="input w-full" -->
<!-- type="number" -->
<!-- min="-1" -->
<!-- max="1" -->
<!-- step=".05" -->
<!-- bind:value={ -->
<!-- inputSelected.cfg.activity[selected.activity] -->
<!-- .weightSkew -->
<!-- } -->
<!-- disabled={!selected.cfg.activity[selected.activity] -->
<!-- .enableGeneration} -->
<!-- /> -->
<!-- </label> -->
<label>
<span class="label mb-1">Difficulty modifier</span>
<input
class="input w-full"
type="number"
min="-1"
max="1"
step=".05"
bind:value={
input.cfg.activity[selected.activity]
.difficultyModifier
}
disabled={!selected.cfg.activity[selected.activity]
.enableGeneration}
/>
</label>
<label>
<span class="label mb-1">Age significance</span>
<input
class="input w-full"
type="number"
min="-1"
max="1"
step=".05"
bind:value={
input.cfg.activity[selected.activity]
.ageModifier
}
disabled={!selected.cfg.activity[selected.activity]
.enableGeneration}
/>
</label>
<label>
<span class="label mb-1">Peak age</span>
<input
class="input w-full"
type="number"
min="1"
max="100"
step="1"
bind:value={
input.cfg.activity[selected.activity].peakAge
}
disabled={!selected.cfg.activity[selected.activity]
.enableGeneration}
/>
</label>
<fieldset class="fieldset bg-base-200 p-3 max-w-xl">
<legend class="fieldset-legend text-sm font-semibold">
Stretch
</legend>
<label>
<span class="label mb-1">Lower</span>
<input
class="input w-full"
type="number"
min="0"
max="10"
step="1"
bind:value={
input.cfg.activity[selected.activity]
.stretch.lower
}
disabled={!selected.cfg.activity[
selected.activity
].enableGeneration}
/>
</label>
<label>
<span class="label mb-1">Upper</span>
<input
class="input w-full"
type="number"
min="0"
max="10"
step="1"
bind:value={
input.cfg.activity[selected.activity]
.stretch.upper
}
disabled={!selected.cfg.activity[
selected.activity
].enableGeneration}
/>
</label>
</fieldset>
</div>
</fieldset>
</div>
</fieldset>
{#if activeTab === "standards"}
<fieldset class="fieldset bg-base-200 p-6 max-w-lg">
<legend class="fieldset-legend text-lg font-semibold">
Metrics
</legend>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<label>
<span class="label mb-1">Gender</span>
<select
class="select w-full"
name="genders"
bind:value={input.metrics.gender}
>
{#each Object.values(Gender) as gender}
<option value={gender}>{gender}</option>
{/each}
</select>
</label>
<label>
<span class="label mb-1">Age</span>
<input
class="input w-full"
type="number"
min="0"
max="100"
placeholder="18"
bind:value={input.metrics.age}
/>
</label>
<label>
<span class="label mb-1">Weight (lb)</span>
<input
class="input w-full"
type="number"
min="0"
max="600"
placeholder="170"
bind:value={input.metrics.weight}
/>
</label>
</div>
</fieldset>
{/if}
</section>
<div class="divider"></div>
<div role="tablist" class="tabs tabs-border">
<button
role="tab"
class="tab"
class:tab-active={activeTab === "standards"}
onclick={() => (activeTab = "standards")}
>
Standards
</button>
<button
role="tab"
class="tab"
class:tab-active={activeTab === "players"}
onclick={() => (activeTab = "players")}
>
Players
</button>
</div>
{#if activeTab === "standards"}
<StandardsTable {selected} {allStandards} />
{/if}
{#if activeTab === "players"}
<PlayersTable {allStandards} />
{/if}
@@ -0,0 +1,27 @@
import { PrintfulClient, WebflowClient } from "@blade-and-brawn/commerce";
import {
PRINTFUL_AUTH,
PRINTFUL_STORE_ID,
WEBFLOW_SITE_ID,
WEBFLOW_COLLECTION_ID,
WEBFLOW_AUTH,
WEBFLOW_WEBHOOK_SECRET,
} from "$env/static/private";
import type { PageServerLoad } from "./$types";
const printful = new PrintfulClient({ token: PRINTFUL_AUTH, storeId: PRINTFUL_STORE_ID });
const webflow = new WebflowClient({
siteId: WEBFLOW_SITE_ID,
collectionsId: WEBFLOW_COLLECTION_ID,
token: WEBFLOW_AUTH,
webhookSecret: WEBFLOW_WEBHOOK_SECRET,
});
export const load = async ({ }: Parameters<PageServerLoad>[0]) => {
return {
products: {
printful: printful.Products.list({ forceAll: true }),
webflow: webflow.Products.list({ forceAll: true }),
},
};
};
@@ -0,0 +1,128 @@
<script lang="ts">
import type { Printful } from "@blade-and-brawn/commerce";
import { onMount } from "svelte";
import { api } from "$lib/api.js";
const { data } = $props();
const synchronizer = $state({
isRunning: false,
printfulProductIds: [] as number[],
poll: async function () {
const res = await api.products.sync.get();
if (res.data) {
synchronizer.isRunning = res.data.isRunning;
synchronizer.printfulProductIds =
res.data.state.printfulProductIds;
}
},
startPolling: () => {
const intervalId = setInterval(async () => {
await synchronizer.poll();
if (!synchronizer.isRunning) clearInterval(intervalId);
}, 100);
},
syncAll: async () => {
synchronizer.startPolling();
await api.products.sync.post();
},
sync: async (printfulProductId: number) => {
synchronizer.startPolling();
await api.products.sync.post({ printfulProductId });
},
});
onMount(async () => {
await synchronizer.poll();
if (synchronizer.isRunning) {
synchronizer.startPolling();
}
});
const isProductSynced = async (
printfulProduct: Printful.Products.SyncProduct,
) => {
const webflowProducts = await data.products.webflow;
return webflowProducts.some(
(p) => p.product.id === printfulProduct.external_id.split("-")[0],
);
};
</script>
{#await data.products.printful}
<p>Loading...</p>
{:then printfulProducts}
<button
disabled={synchronizer.isRunning}
onclick={() => synchronizer.syncAll()}
class="btn btn-xl mb-5"
>
{#if synchronizer.isRunning}
Syncing...
{:else}
Sync all
{/if}
</button>
<div class="w-full overflow-x-auto">
<table class="table">
<thead>
<tr>
<th>Name</th>
<th>Status</th>
<th>Sync</th>
</tr>
</thead>
<tbody>
{#each printfulProducts as printfulProduct}
<tr class="hover:bg-base-300">
<td
><button
onclick={async () => {
const res = await api
.products({
printfulProductId:
printfulProduct.id,
})
.get();
console.log(res.data);
}}
class="cursor-pointer underline"
>
{printfulProduct.name}
</button></td
>
<td>
{#await isProductSynced(printfulProduct) then isSynced}
{#if synchronizer.printfulProductIds.includes(printfulProduct.id)}
<div class="badge badge-warning">
Syncing...
</div>
{:else if isSynced}
<div class="badge badge-success">
Synced
</div>
{:else}
<div class="badge badge-error">
Desynced
</div>
{/if}
{/await}
</td>
<td>
<button
disabled={synchronizer.isRunning}
onclick={() =>
synchronizer.sync(printfulProduct.id)}
class="btn"
>
Sync
</button>
</td>
</tr>
{/each}
</tbody>
</table>
</div>
{:catch error}
<p>Something went wrong: {error.message}</p>
{/await}