This commit is contained in:
Daniel Ledda
2024-10-31 23:46:23 +01:00
parent 314ccaa677
commit bcb820f35e
36 changed files with 4427 additions and 61 deletions

21
app/DJEmail.tsx Normal file
View File

@@ -0,0 +1,21 @@
import { defineComponent } from "vue";
export default defineComponent({
name: "dj-email",
setup(_props, { slots }) {
function clickMail() {
const a = "gmail";
const b = "com";
const c = "danjledda";
let link = "ma" + "il" + "" + "to:" + c + a + b;
link = link.slice(0, 10) + "." + link.slice(10, 11) + "." + link.slice(11, 16) + "@" + link.slice(16, 21) +
"." + link.slice(21);
window.location.href = link;
}
return () => (
<a href="#" onClick={clickMail}>
{ slots.default ? slots.default() : 'dan.j.ledda [at] gmail [dot] com' }
</a>
);
},
});

View File

@@ -0,0 +1,355 @@
import { defineComponent, computed, ref } from 'vue';
/*
class GrainInput {
static inputs = [];
name;
unit;
mpg;
inputEl;
reference;
* @param {{
* name: string,
* mpg: number,
* reference: GrainInput | 'self',
* unit: string,
* step?: number,
* }} opts
constructor(opts) {
this.name = opts.name;
this.mpg = opts.mpg;
this.unit = opts.unit;
this.reference = opts.reference === "self" ? this : opts.reference;
this.inputEl = h("input", { type: "number", min: 0, step: opts.step ?? 1 });
eventBus.addListener(this);
}
attachInput(insertionPoint) {
insertionPoint.appendChild(this.inputEl);
this.inputEl.valueAsNumber = this.mpg;
this.inputEl.addEventListener("input", (e) => {
const newVal = (e.currentTarget)?.valueAsNumber ?? 0;
if (this !== this.reference) {
this.reference.inputEl.valueAsNumber = newVal / this.mpg;
}
eventBus.sendChange(this);
});
}
onChange() {
if (!this.reference || this === this.reference) return;
this.inputEl.valueAsNumber = this.mpg * this.reference.inputEl.valueAsNumber;
}
getCurrentValue() {
return this.inputEl.valueAsNumber;
}
}
class RatiosController {
t3Ratio = h("input", { type: "number", min: 0, step: 1 });
t4Ratio = h("input", { type: "number", min: 0, step: 1 });
t3Syn = h("input", { type: "number", min: 0 });
t4Syn = h("input", { type: "number", min: 0 });
t3 = 1;
t4 = 4;
reference;
constructor(referenceInput) {
this.reference = referenceInput;
}
attachInputs(inputs) {
inputs.t3Ratio.replaceWith(this.t3Ratio);
inputs.t4Ratio.replaceWith(this.t4Ratio);
inputs.t3Syn.replaceWith(this.t3Syn);
inputs.t4Syn.replaceWith(this.t4Syn);
this.t3Ratio.addEventListener("input", (e) => {
const newVal = (e.currentTarget)?.valueAsNumber ?? 0;
this.t3 = newVal;
this.onChange();
});
this.t4Ratio.addEventListener("input", (e) => {
const newVal = (e.currentTarget)?.valueAsNumber ?? 0;
this.t4 = newVal;
this.onChange();
});
this.t3Syn.addEventListener("input", (e) => {
const newVal = (e.currentTarget)?.valueAsNumber ?? 0;
this.reference.inputEl.valueAsNumber = newVal / MPG_T3_SYN + this.t4Syn.valueAsNumber / MPG_T4_SYN;
this.t3 = newVal;
this.t4 = this.t4Syn.valueAsNumber;
this.updateRatio();
this.updateSyn();
eventBus.sendChange(this);
});
this.t4Syn.addEventListener("input", (e) => {
const newVal = (e.currentTarget)?.valueAsNumber ?? 0;
this.reference.inputEl.valueAsNumber = newVal / MPG_T4_SYN + this.t3Syn.valueAsNumber / MPG_T3_SYN;
this.t3 = this.t3Syn.valueAsNumber;
this.t4 = newVal;
this.updateRatio();
this.updateSyn();
eventBus.sendChange(this);
});
eventBus.addListener(this);
this.onChange();
}
onChange() {
this.updateRatio();
this.updateSyn();
}
updateRatio() {
this.t3Ratio.valueAsNumber = this.t3;
this.t4Ratio.valueAsNumber = this.t4;
}
updateSyn() {
const total = this.t3 + this.t4;
const t3Proportion = this.t3 / total;
const t4Proportion = this.t4 / total;
const grainsSyn = t3Proportion / MPG_T3_SYN + t4Proportion / MPG_T4_SYN;
const multiplierSyn = this.reference.getCurrentValue() / grainsSyn;
this.t3Syn.valueAsNumber = t3Proportion * multiplierSyn;
this.t4Syn.valueAsNumber = t4Proportion * multiplierSyn;
}
}
class ThyroidConverter extends DJElement {
static styles = css``;
static template = html;
constructor() {
super();
const mainGrainInput = new GrainInput({
name: "Grains",
mpg: 1,
reference: "self",
unit: "",
step: 0.1,
});
const ratiosController = new RatiosController(mainGrainInput);
const inputs = [
mainGrainInput,
new GrainInput({
name: "Armour, Natural Dessicated Thyroid",
mpg: 60,
unit: "mg",
reference: mainGrainInput,
}),
new GrainInput({
name: 'Liothyronine (Triiodothyronine, "Cytomel", T3)',
mpg: MPG_T3_SYN,
unit: "mcg",
reference: mainGrainInput,
}),
new GrainInput({
name: 'Levothyroxine (Thyroxine, "Cynoplus", T4)',
mpg: MPG_T4_SYN,
unit: "mcg",
reference: mainGrainInput,
}),
];
const tableBody = qs("#table-body", this.root);
const compoundedStart = qs("#compounded-start", this.root);
for (const field of inputs) {
const newRow = h("tr", {}, [
h("td", { textContent: field.name }),
h("td", {}, [
h("div", { className: "input", style: "display: inline-block;" }),
h("span", { className: "breathe", textContent: field.unit }),
]),
]);
field.attachInput(qs("div.input", newRow));
tableBody.insertBefore(newRow, compoundedStart);
}
ratiosController.attachInputs({
t3Ratio: qs(".ratios .t3", tableBody),
t4Ratio: qs(".ratios .t4", tableBody),
t3Syn: qs(".synthetic .t3", tableBody),
t4Syn: qs(".synthetic .t4", tableBody),
});
}
}
*/
export default defineComponent({
name: 'ge-calculator',
setup() {
const t3Ratio = ref(1);
const t4Ratio = ref(4);
const MPG_T3_SYN = 25;
const MPG_T4_SYN = 100;
const inputDefs = [
{
name: "Grains",
mpg: 1,
unit: "",
step: 0.1,
},
{
name: "Armour, Natural Dessicated Thyroid",
mpg: 60,
unit: "mg",
},
{
name: 'Liothyronine (Triiodothyronine, "Cytomel", T3)',
mpg: MPG_T3_SYN,
unit: "mcg",
},
{
name: 'Levothyroxine (Thyroxine, "Cynoplus", T4)',
mpg: MPG_T4_SYN,
unit: "mcg",
},
];
const numGrains = ref(2);
const ratio = computed(() => t3Ratio.value + t4Ratio.value);
const compounded = computed(() => {
const total = t3Ratio.value + t4Ratio.value;
const t3Proportion = t3Ratio.value / total;
const t4Proportion = t4Ratio.value / total;
const grainsSyn = t3Proportion / MPG_T3_SYN + t4Proportion / MPG_T4_SYN;
const multiplierSyn = numGrains.value / grainsSyn;
return {
t3Syn: t3Proportion * multiplierSyn,
t4Syn: t4Proportion * multiplierSyn,
};
});
return () => <>
<header>
<h1>Thyroid Calculator</h1>
</header>
<div class="text-slab">
<p>
Use this calculator to convert between different forms and units of thyroid hormone. Common
synthetic, pure and natural dessicated forms are listed. The last section of the table provides
input fields for a specified ratio of T3 to T4, and will give the dosages required for both the
synthetic and pure forms. All dosages given are based on the "Grains" field at the beginning of the
table, but editing any field will automatically update the rest to keep them in sync.
</p>
</div>
<div class="text-slab ge-calculator">
<table>
<tbody>
{ inputDefs.map((_, i) => (
<tr key={_.name}>
<td>
{ _.name }
</td>
<td class="right">
<div style="display: inline-block;">
<input
value={_.name === 'Grains' ? numGrains.value : _.mpg * numGrains.value}
onInput={(e) => {
const val = (e.target as HTMLInputElement).valueAsNumber;
if (_.name === 'Grains') {
numGrains.value = val;
} else {
numGrains.value = _.mpg / val;
}
}}
min="0"
step={ _.step ?? 1 }
type="number" />
</div>
<span class="breathe">{ _.unit }</span>
</td>
</tr>
))}
<tr><td colspan="2"><strong>Compounded (T3 and T4, "Cynpolus")</strong></td></tr>
<tr class="ratios">
<td>
Desired Ratio (T3:T4)
</td>
<td class="right">
<div>
<input
value={t3Ratio.value}
onInput={(e) => {
t3Ratio.value = (e.currentTarget as HTMLInputElement).valueAsNumber;
}}
min="0"
step="1"
type="number" />
</div> : <div>
<input
value={t4Ratio.value}
onInput={(e) => {
t4Ratio.value = (e.currentTarget as HTMLInputElement).valueAsNumber;
}}
min="0"
step="1"
type="number" />
</div>
</td>
</tr>
<tr class="synthetic">
<td>
Synthetic T3/T4 Combo
</td>
<td class="right">
<div>
<input
value={compounded.value.t3Syn}
onInput={(e) => {
t4Ratio.value = compounded.value.t4Syn;
t3Ratio.value = (e.currentTarget as HTMLInputElement).valueAsNumber;
numGrains.value = t3Ratio.value / MPG_T3_SYN + t4Ratio.value / MPG_T4_SYN;
}}
min="0"
step="1"
type="number" />
</div> : <div>
<input
value={compounded.value.t4Syn}
onInput={(e) => {
t3Ratio.value = compounded.value.t3Syn;
t4Ratio.value = (e.currentTarget as HTMLInputElement).valueAsNumber;
numGrains.value = t3Ratio.value / MPG_T3_SYN + t4Ratio.value / MPG_T4_SYN;
}}
min="0"
step="1"
type="number" />
</div>
</td>
</tr>
</tbody>
</table>
</div>
<div class="text-slab">
<strong>Changelog:</strong>
<p>
<ul>
<li>
November 2024: Migrated to new web framework and fixed some buggy input.
</li>
<li>
13th March 2024: Removed the synthetic/pure distinction as it was confusing and unnecessary
bloat.
</li>
</ul>
</p>
</div>
</>;
}
});

View File

@@ -0,0 +1,61 @@
import { defineComponent } from 'vue';
import { RouterLink } from 'vue-router';
import DJEmail from "@/DJEmail.tsx";
export default defineComponent({
name: 'ge-deutsch',
setup() {
return () => <>
<header>
<h1>Ray Peat Deutsche Übersetzungen</h1>
</header>
<div class="text-slab">
<p>
Auf dieser Seite befindet sich eine Auswahl der Artikel von Dr. Raymond Peat, die ich in meiner
Freizeit ins Deutsche übersetzt habe.
</p>
<p>
Ray Peat war ein US-Amerikaner aus Eugene, Oregon, der neben seinem Studium von Linguistik,
Literatur, und Malerei eine Promotion in der Biologie und Physiologie des Alterns und degenerativer
Krankheiten absolvierte, mit besonderem Fokus auf Zellen-Stoffwechsel, Hormone, und
Ernährungsphysiologie.
</p>
<p>
Nach mehreren Jahren als Professor an diversen Universitäten in den USA und Mexiko begann er Artikel
in Form eines Newsletters herauszugeben, von denen mehrere auf <a href="http://raypeat.com/"
>seiner Website</a> zu finden sind.
</p>
<p>
Da er in meinem Leben zu einem enormen Verständnis meines Körpers und einem unglaublichen
Zurückerlangen meiner Gesundheit und meines Wohlbefindens trotz dem Scheitern mehrerer Ärzte
beigetragen hat, und sein Nachlass in Deutschland kaum Publikum aufgrund schwieriger physiologischer
Texte genießt, habe ich entschieden hauptsächlich für Freunde und Bekannte einige seiner Artikel ins
Deutsche zu übersetzen, damit vielleicht auch ein breiteres Publikum von seinen Ideen profitieren
könnte.
</p>
<p>
Falls was bei der Übersetzung auffällt oder besonders unidiomatisch klingt, bzw. der deutschen
Fachsprache der Medizin nicht gerecht sein sollte, kannst du mir unter <DJEmail /> eine Mail senden.
Meine Muttersprache ist schließlich Englisch und ich bin kein professioneller Übersetzer!
</p>
<p>
Zusätzlich zu der Funktion, den Artikel mit dem Button oben in der Originalversion anzusehen, hat
jeder Absatz oben rechts beim drüberhovern mit der Maus einen zusätzlichen Button über den man den
jeweiligen Absatz in die andere Sprache wechseln kann, um die Versionen beim Lesen schnell zu
vergleichen zu können.
</p>
</div>
<div class="text-slab">
<h3>Artikel auswählen:</h3>
<ul id="article">
<li>
<RouterLink to={{ name: 'GEDeutschArticle', params: { articleName: 'hypothyroidism'}}}>Indikatoren Schilddrüsenunterfunktion</RouterLink>
</li>
<li>
<RouterLink to={{ name: 'GEDeutschArticle', params: { articleName: 'caffeine'}}}>Koffein</RouterLink>
</li>
</ul>
</div>
</>;
}
});

View File

@@ -0,0 +1,75 @@
import { h, inject, defineComponent, ref, createTextVNode, type VNode } from 'vue';
import { RouterLink } from 'vue-router';
import useAsyncState from "@/useAsyncState.ts";
export default defineComponent({
name: 'ge-deutsch-article',
props: {
articleName: {
type: String,
required: true,
},
},
async setup(props) {
const currentLang = ref<"en" | "de">("de");
function clickBtn() {
currentLang.value = currentLang.value === "en" ? "de" : "en";
}
const parseDom = inject('dom-parse', (innerHTML: string) => Object.assign(document.createElement('div'), { innerHTML }));
const { result: articleContent, stateIsReady } = useAsyncState('ge-deutsch-article-data', async ({ hostUrl }) => {
const articleResponse = await fetch(`${ hostUrl }/generative-energy/static/content/${ props.articleName }.html`);
return await articleResponse.text();
});
function transformArticleNode(node: Node): VNode | string {
if (node.nodeType === node.ELEMENT_NODE) {
const el = node as Element;
const attrs: Record<string, string> = {};
const children = [...node.childNodes].map(_ => transformArticleNode(_));
if (el.tagName === 'P') {
el.classList.add('text-slab');
children.unshift(h('button', { class: 'swap', onClick: (e) => {
e.target.parentElement.classList.toggle('swap');
} }, '↻'));
}
for (let i = 0; i < el.attributes.length; i++) {
const item = el.attributes.item(i);
if (item) {
attrs[item.name] = item.value;
}
}
return h((node as Element).tagName, attrs, children);
} else {
return createTextVNode(node.textContent ?? '');
}
}
function ArticleContentTransformed() {
if (articleContent.value) {
const dom = parseDom(articleContent.value);
return h('div', {}, [...dom.children].map(_ => transformArticleNode(_)));
}
return <div>Artikel lädt...</div>;
}
await stateIsReady;
return () => <div class="ge-article">
<div class="header">
<RouterLink to={{ name: 'GEDeutsch' }}>Zur Artikelübersicht</RouterLink>
<button onClick={clickBtn}>
Sprache auf <span>{currentLang.value === "en" ? "Deutsch" : "Englisch"}</span> umschalten
</button>
</div>
<article class={`lang-${ currentLang.value }`}>
<ArticleContentTransformed />
</article>
</div>;
}
});

View File

@@ -0,0 +1,33 @@
import { RouterLink } from 'vue-router';
export default {
name: 'ge-main',
setup() {
return () => <>
<header>
<h1>Generative Energy</h1>
</header>
<div class="text-slab">
<p>
This page is dedicated to Dr. Raymond Peat ( 2022), who has had a profound impact on my health and
my life in general. Hover over the links below for more detail.
</p>
</div>
<div class="text-slab">
<h2>Links</h2>
<ul>
<li>
<RouterLink to={{ name: 'GECalculator' }}>Thyroid Calculator</RouterLink>
<span style="display: none" class="tooltip">Convert to and from grains, set ratios, etc.</span>
</li>
<li>
<RouterLink to={{ name: 'GEDeutsch' }}>Ray Peat Articles in German</RouterLink>
<span style="display: none" class="tooltip">
A selection of articles by Ray that I have translated in my spare time into German.
</span>
</li>
</ul>
</div>
</>
},
};

View File

@@ -0,0 +1,21 @@
import { defineComponent } from "vue";
export default defineComponent({
name: "dj-paypal-donate",
setup: () => {
return () => (
<form action="https://www.paypal.com/donate" method="post" target="_top">
<input type="hidden" name="hosted_button_id" value="9NXC6V5HDPGFL" />
<input
id="ppbutton"
type="image"
src="https://www.paypalobjects.com/en_US/i/btn/btn_donate_LG.gif"
name="submit"
title="Thanks for your support!"
alt="Donate with PayPal button"
/>
<img alt="" src="https://www.paypal.com/en_DE/i/scr/pixel.gif" width="1" height="1" />
</form>
);
},
});

View File

@@ -0,0 +1,67 @@
import { defineComponent, type VNode, Suspense } from "vue";
import { useRoute, RouterLink, RouterView, type RouteRecordRaw } from 'vue-router';
import GEMain from "@/generative-energy/GEMain.tsx";
import DJEmail from "@/DJEmail.tsx";
import GEPaypal from "@/generative-energy/GEPaypal.tsx";
import GEDeutsch from "@/generative-energy/GEDeutsch.tsx";
import GEDeutschArticle from "@/generative-energy/GEDeutschArticle.tsx";
import GECalculator from "@/generative-energy/GECalculator.tsx";
export const routes: RouteRecordRaw[] = [
{
path: '/',
name: 'GEMain',
component: GEMain,
},
{
path: '/calculator',
name: 'GECalculator',
component: GECalculator,
},
{
path: '/raypeat-deutsch',
name: 'GEDeutsch',
component: GEDeutsch,
},
{
path: '/raypeat-deutsch/article/:articleName',
name: 'GEDeutschArticle',
component: GEDeutschArticle,
props: ({ params }) => {
if ('articleName' in params) {
return { articleName: params.articleName };
} else {
return false;
}
},
},
];
export default defineComponent({
name: "ge-root",
setup() {
const route = useRoute();
return () => <>
<main>
<RouterLink class={"home-btn" + (route.name === 'GEMain' ? ' hide' : '') }
to={{ name: 'GEMain' }}>
Generative Energy Home
</RouterLink>
<RouterView>{{ default: ({ Component }: { Component: VNode }) => (Component &&
<Suspense>{{
default: () => Component,
fallback: () => <div>Page loading...</div>,
}}</Suspense>
)}}</RouterView>
<footer>
<div class="bottom">
<div>
<a href="https://djledda.de/">djledda.de</a> 2023 - <DJEmail>{ () => 'Contact' }</DJEmail>
</div>
<GEPaypal />
</div>
</footer>
</main>
</>;
},
});

View File

@@ -0,0 +1,298 @@
import { css, DJElement, h, html, qs } from "./util";
//@ts-check
const MPG_T3_SYN = 25;
const MPG_T4_SYN = 100;
class GlobalEventBus {
/**
* @private
* @type {{ onChange(): void }[]}
*/
changeSubscribers = [];
/**
* @param {{ onChange(): void }} listener
*/
addListener(listener) {
this.changeSubscribers.push(listener);
}
/**
* @param {any} sender
*/
sendChange(sender) {
for (const sub of this.changeSubscribers) {
if (sub === sender) continue;
sub.onChange();
}
}
}
const eventBus = new GlobalEventBus();
class GrainInput {
/** @type GrainInput[] */
static inputs = [];
/** @type string */
name;
/** @type string */
unit;
/** @type number */
mpg;
/** @type HTMLInputElement */
inputEl;
/** @type GrainInput */
reference;
/**
* @param {{
* name: string,
* mpg: number,
* reference: GrainInput | 'self',
* unit: string,
* step?: number,
* }} opts
*/
constructor(opts) {
this.name = opts.name;
this.mpg = opts.mpg;
this.unit = opts.unit;
this.reference = opts.reference === "self" ? this : opts.reference;
this.inputEl = h("input", { type: "number", min: 0, step: opts.step ?? 1 });
eventBus.addListener(this);
}
/**
* @param {HTMLElement} insertionPoint
*/
attachInput(insertionPoint) {
insertionPoint.appendChild(this.inputEl);
this.inputEl.valueAsNumber = this.mpg;
this.inputEl.addEventListener("input", (e) => {
const newVal = /** @type HTMLInputElement | null */ (e.currentTarget)?.valueAsNumber ?? 0;
if (this !== this.reference) {
this.reference.inputEl.valueAsNumber = newVal / this.mpg;
}
eventBus.sendChange(this);
});
}
onChange() {
if (!this.reference || this === this.reference) return;
this.inputEl.valueAsNumber = this.mpg * this.reference.inputEl.valueAsNumber;
}
getCurrentValue() {
return this.inputEl.valueAsNumber;
}
}
class RatiosController {
/** @type HTMLInputElement */
t3Ratio = h("input", { type: "number", min: 0, step: 1 });
/** @type HTMLInputElement */
t4Ratio = h("input", { type: "number", min: 0, step: 1 });
/** @type HTMLInputElement */
t3Syn = h("input", { type: "number", min: 0 });
/** @type HTMLInputElement */
t4Syn = h("input", { type: "number", min: 0 });
/** @type number */
t3 = 1;
/** @type number */
t4 = 4;
/** @type GrainInput */
reference;
/**
* @param {GrainInput} referenceInput
*/
constructor(referenceInput) {
this.reference = referenceInput;
}
/**
* @param {Record<string, HTMLElement>} inputs
*/
attachInputs(inputs) {
inputs.t3Ratio.replaceWith(this.t3Ratio);
inputs.t4Ratio.replaceWith(this.t4Ratio);
inputs.t3Syn.replaceWith(this.t3Syn);
inputs.t4Syn.replaceWith(this.t4Syn);
this.t3Ratio.addEventListener("input", (e) => {
const newVal = /** @type HTMLInputElement | null */ (e.currentTarget)?.valueAsNumber ?? 0;
this.t3 = newVal;
this.onChange();
});
this.t4Ratio.addEventListener("input", (e) => {
const newVal = /** @type HTMLInputElement | null */ (e.currentTarget)?.valueAsNumber ?? 0;
this.t4 = newVal;
this.onChange();
});
this.t3Syn.addEventListener("input", (e) => {
const newVal = /** @type HTMLInputElement | null */ (e.currentTarget)?.valueAsNumber ?? 0;
this.reference.inputEl.valueAsNumber = newVal / MPG_T3_SYN + this.t4Syn.valueAsNumber / MPG_T4_SYN;
this.t3 = newVal;
this.t4 = this.t4Syn.valueAsNumber;
this.updateRatio();
this.updateSyn();
eventBus.sendChange(this);
});
this.t4Syn.addEventListener("input", (e) => {
const newVal = /** @type HTMLInputElement | null */ (e.currentTarget)?.valueAsNumber ?? 0;
this.reference.inputEl.valueAsNumber = newVal / MPG_T4_SYN + this.t3Syn.valueAsNumber / MPG_T3_SYN;
this.t3 = this.t3Syn.valueAsNumber;
this.t4 = newVal;
this.updateRatio();
this.updateSyn();
eventBus.sendChange(this);
});
eventBus.addListener(this);
this.onChange();
}
onChange() {
this.updateRatio();
this.updateSyn();
}
updateRatio() {
this.t3Ratio.valueAsNumber = this.t3;
this.t4Ratio.valueAsNumber = this.t4;
}
updateSyn() {
const total = this.t3 + this.t4;
const t3Proportion = this.t3 / total;
const t4Proportion = this.t4 / total;
const grainsSyn = t3Proportion / MPG_T3_SYN + t4Proportion / MPG_T4_SYN;
const multiplierSyn = this.reference.getCurrentValue() / grainsSyn;
this.t3Syn.valueAsNumber = t3Proportion * multiplierSyn;
this.t4Syn.valueAsNumber = t4Proportion * multiplierSyn;
}
}
class ThyroidConverter extends DJElement {
static styles = css`
table {
border-collapse: separate;
margin: auto;
margin-top: 20px;
margin-bottom: 20px;
border-radius: 5px;
border-spacing: 0;
border: 1px solid var(--text-color);
}
input {
width: 70px;
}
tr:last-of-type td {
border-bottom: none;
}
td {
border-bottom: 1px solid var(--text-color);
border-right: 1px solid var(--text-color);
padding: 10px;
}
td:last-of-type {
border-right: none;
}
.breathe {
padding-left: 4px;
padding-right: 4px;
}
@media (min-width: 600px) {
td.right div {
display: inline-block;
}
}
`;
static template = html`
<table>
<tbody id="table-body">
<tr id="compounded-start" class="ratios">
<td>
Desired Ratio (T3:T4)
</td>
<td class="right">
<div><slot class="t3"></slot></div> :
<div><slot class="t4"></slot></div>
</td>
</tr>
<tr class="synthetic">
<td>
Synthetic T3/T4 Combo
</td>
<td class="right">
<div><slot class="t3"></slot>mcg</div> :
<div><slot class="t4"></slot>mcg</div>
</td>
</tr>
</tbody>
</table>`;
constructor() {
super();
const mainGrainInput = new GrainInput({
name: "Grains",
mpg: 1,
reference: "self",
unit: "",
step: 0.1,
});
const ratiosController = new RatiosController(mainGrainInput);
const inputs = [
mainGrainInput,
new GrainInput({
name: "Armour, Natural Dessicated Thyroid",
mpg: 60,
unit: "mg",
reference: mainGrainInput,
}),
new GrainInput({
name: 'Liothyronine (Triiodothyronine, "Cytomel", T3)',
mpg: MPG_T3_SYN,
unit: "mcg",
reference: mainGrainInput,
}),
new GrainInput({
name: 'Levothyroxine (Thyroxine, "Cynoplus", T4)',
mpg: MPG_T4_SYN,
unit: "mcg",
reference: mainGrainInput,
}),
];
const tableBody = qs("#table-body", this.root);
const compoundedStart = qs("#compounded-start", this.root);
for (const field of inputs) {
const newRow = h("tr", {}, [
h("td", { textContent: field.name }),
h("td", {}, [
h("div", { className: "input", style: "display: inline-block;" }),
h("span", { className: "breathe", textContent: field.unit }),
]),
]);
field.attachInput(qs("div.input", newRow));
tableBody.insertBefore(newRow, compoundedStart);
}
ratiosController.attachInputs({
t3Ratio: qs(".ratios .t3", tableBody),
t4Ratio: qs(".ratios .t4", tableBody),
t3Syn: qs(".synthetic .t3", tableBody),
t4Syn: qs(".synthetic .t4", tableBody),
});
}
}
customElements.define("thyroid-converter", ThyroidConverter);

View File

@@ -0,0 +1,10 @@
import { createSSRApp } from "vue";
import { createRouter, createWebHistory } from "vue-router";
import GERoot, { routes } from "@/generative-energy/GERoot.tsx";
createSSRApp(GERoot)
.use(createRouter({
routes,
history: createWebHistory('/generative-energy')
}))
.mount('#app-root');

View File

@@ -0,0 +1,135 @@
// @ts-check
//
import { css, DJElement, h, html, qs } from "./util";
class HalfLifeGraph extends DJElement {
static styles = css`
.container {
margin: auto;
text-align: center;
}
canvas {
border: solid 1px black;
}
`;
static template = html`
<div class="container">
<div>
<canvas />
</div>
<label for="my-input">Test Input</label>
<input type="range" />
</div>
`;
/** @type number */
height = 480;
/** @type number */
width = 640;
/** @type HTMLInputElement */
input1;
/** @type number */
input1Value;
/** @type Function */
renderCb;
constructor() {
super();
const canvas = /** @type HTMLCanvasElement */ (qs("canvas", this.root));
this.input1 = /** @type HTMLInputElement */ (qs("input", this.root));
canvas.width = this.width;
canvas.height = this.height;
const context = canvas.getContext("2d");
if (!context) {
throw new Error("No context available");
}
this.renderCb = () => {
window.requestAnimationFrame(() => this.renderFrame(context));
};
this.input1.addEventListener("input", (e) => {
this.input1Value = /** @type HTMLInputElement */ (e.target).valueAsNumber;
this.renderCb();
});
this.renderCb();
}
/**
* @param {CanvasRenderingContext2D} ctx
*/
renderFrame(ctx) {
this.drawAxes(ctx);
this.drawGrid(ctx);
this.drawCurve(ctx);
}
/**
* @param {CanvasRenderingContext2D} ctx
*/
drawAxes(ctx) {
const startY = 0;
const endY = 10;
const startX = 0;
const endX = 10;
ctx.beginPath();
ctx.moveTo(startX, this.height - startY);
ctx.lineTo(startX, this.height - endY);
ctx.stroke();
ctx.beginPath();
ctx.moveTo(startX, this.height - startY);
ctx.lineTo(endX, this.height - startY);
ctx.stroke();
}
/**
* @param {CanvasRenderingContext2D} ctx
*/
drawGrid(ctx) {
/**
const step = this.width / V0D
for (let i = 0; i < thing; i++) {
ctx.beginPath();
ctx.moveTo(i, );
ctx.lineTo(i);
}
*/
}
/**
* @param {CanvasRenderingContext2D} ctx
*/
drawCurve(ctx) {
// A = A_0 * (1/2) ^ t / h
// h = half life
// t = point in time
// A_0 = initial
// A = current concentration
// TODO
// https://pharmacy.ufl.edu/files/2013/01/5127-28-equations.pdf
/**
* @param {number} x
*/
function y(x) {
return x ** 2;
}
const xEnd = 2;
const yEnd = 2;
const samples = 100;
const step = this.width / samples;
ctx.beginPath();
ctx.moveTo(0, this.height);
for (let i = 0; i < this.width; i += step) {
ctx.lineTo(i, this.height - y(i / this.width * xEnd) * this.height / yEnd);
}
ctx.stroke();
}
}
customElements.define("half-life-graph", HalfLifeGraph);

View File

@@ -1,5 +1,4 @@
import { defineComponent, computed, ref } from "vue";
import Test from '@/test.tsx';
import { computed, defineComponent, ref } from "vue";
export default defineComponent({
name: "app-root",
@@ -12,7 +11,9 @@ export default defineComponent({
<button class="test" onClick={() => count.value++}>Click me!</button>
<div>Count: {count.value}</div>
<div>Count Double: {countDouble.value}</div>
<Test />
<p>
<a href="/generative-energy/">Go to this other site hosted here but a different Vue app!</a>
</p>
</div>
);
},

13
app/meta.ts Normal file
View File

@@ -0,0 +1,13 @@
import { useSSRContext, toValue, type MaybeRefOrGetter } from 'vue';
export default function useHead(params: {
title: MaybeRefOrGetter<string>,
}) {
const context = useSSRContext();
if (context) {
context.meta ??= {
title: toValue(params.title),
meta: {},
};
}
}

74
app/reactivity.ts Normal file
View File

@@ -0,0 +1,74 @@
const contextStack: Observer[] = [];
export class Observer<T = unknown> {
private effect: () => T;
dependencies: Set<Observable> = new Set();
constructor(effect: { (): T }) {
this.effect = effect;
this.execute();
}
execute() {
this.cleanup();
contextStack.push(this);
try {
this.effect();
} finally {
contextStack.pop();
}
}
cleanup() {
for (const dep of this.dependencies.values()) {
dep.observers.delete(this);
}
this.dependencies.clear();
}
}
export class Observable<T = unknown> {
private value: T;
observers: Set<Observer> = new Set();
constructor(init: T) {
this.value = init;
}
get() {
const activeObserver = contextStack[contextStack.length - 1];
if (activeObserver) {
this.observers.add(activeObserver);
activeObserver.dependencies.add(this);
}
return this.value;
}
set(next: T) {
this.value = next;
for (const observer of Array.from(this.observers.values())) {
observer.execute();
}
}
}
export class Mapping<T extends unknown> {
private observable: Observable<T>;
private observer: Observer<T>;
constructor(mapping: { (): T }) {
this.observable = new Observable(undefined as T);
this.observer = new Observer(() => {
const result = mapping();
this.observable.set(result);
return result;
});
}
get() {
return this.observable.get();
}
}

View File

@@ -1 +0,0 @@
export default () => <div class="test">Test</div>;

122
app/tooltip.tsx Normal file
View File

@@ -0,0 +1,122 @@
import { h, qsa } from "@/util.ts";
import { type CSSProperties, defineComponent, onBeforeUnmount, onMounted, ref } from "vue";
const carrierStyle = {
opacity: "0",
display: "block",
pointerEvents: "none",
backgroundColor: "black",
border: "white solid 1px",
color: "white",
padding: "10px",
position: "absolute",
zIndex: "1",
overflow: "hidden",
height: "0",
width: "0",
transition: "opacity 200ms, height 200ms, width 200ms",
} satisfies CSSProperties;
const textCarrierStyle = {
width: "350px",
display: "block",
overflow: "hidden",
} satisfies CSSProperties;
const defaultWidth = 350;
export default defineComponent({
name: "dj-sexy-tooltip",
setup() {
const active = ref(false);
const carrier = ref<HTMLElement | null>(null);
const textCarrier = ref<HTMLElement | null>(null);
onMounted(() => {
document.body.appendChild(h("style", { textContent: `.tooltip { display: none; }` }));
document.body.style.position = "relative";
document.addEventListener("mousemove", (event) => this.rerenderTooltip({ x: event.pageX, y: event.pageY }));
});
function rerenderTooltip(mousePos: { x: number; y: number }) {
const newText = this.getTooltipTextForHoveredElement(mousePos);
if (newText !== this.textCarrier) {
this.transitionTooltipSize(newText);
const tooltipHasText = newText !== "";
if (tooltipHasText !== this.active) {
this.toggleActive();
}
}
if (this.isStillVisible()) {
this.updatePosition(mousePos);
}
}
function isStillVisible() {
return getComputedStyle(carrier.value).opacity !== "0";
}
function updatePosition(pos: { x: number; y: number }) {
if (pos.x + 15 + this.carrier.clientWidth <= document.body.scrollWidth) {
this.carrier.style.left = (pos.x + 15) + "px";
} else {
this.carrier.style.left = (document.body.scrollWidth - this.carrier.clientWidth - 5) + "px";
}
if (pos.y + this.carrier.clientHeight <= document.body.scrollHeight) {
this.carrier.style.top = pos.y + "px";
} else {
this.carrier.style.top = (document.body.scrollHeight - this.carrier.clientHeight - 5) + "px";
}
}
function toggleActive() {
if (this.active) {
this.active = false;
this.carrier.style.opacity = "0";
this.carrier.style.padding = "0";
} else {
this.active = true;
this.carrier.style.opacity = "1";
this.carrier.style.padding = SexyTooltip.carrierStyle.padding;
}
}
function transitionTooltipSize(text: string) {
const testDiv = h("div");
testDiv.style.height = "auto";
testDiv.style.width = SexyTooltip.defaultWidth + "px";
testDiv.innerHTML = text;
document.body.appendChild(testDiv);
const calculatedHeight = testDiv.scrollHeight;
testDiv.style.display = "inline";
testDiv.style.width = "auto";
document.body.removeChild(testDiv);
const size = { height: calculatedHeight + "px", width: "350px" };
this.carrier.style.height = size.height;
this.carrier.style.width = text === "" ? "0" : size.width;
this.textCarrier.innerHTML = text;
}
function getTooltipTextForHoveredElement(mousePos: { x: number; y: number }) {
for (const elem of this.elementsWithTooltips) {
const boundingRect = elem.getBoundingClientRect();
const inYRange = mousePos.y >= boundingRect.top + window.pageYOffset - 1 &&
mousePos.y <= boundingRect.bottom + window.pageYOffset + 1;
const inXRange = mousePos.x >= boundingRect.left + window.pageXOffset - 1 &&
mousePos.x <= boundingRect.right + window.pageXOffset + 1;
if (inYRange && inXRange) {
return elem.nextElementSibling?.innerText ?? "";
}
}
return "";
}
return () => (
<div ref={carrier}>
<span ref={textCarrier} />
</div>
);
},
});

50
app/useAsyncState.ts Normal file
View File

@@ -0,0 +1,50 @@
import { onMounted, onServerPrefetch, useSSRContext, shallowRef, type ShallowRef } from 'vue';
declare global {
// deno-lint-ignore no-var
var appstate: Partial<Record<string, unknown>>;
}
export default function useAsyncState<T>(key: string, getter: (context: { hostUrl: string }) => Promise<T | null>, options?: { suspensible: boolean }): { result: ShallowRef<T | null>, stateIsReady: Promise<unknown> } {
const ssrContext = useSSRContext<{ registry: Record<string, unknown> }>();
const isClient = typeof ssrContext === 'undefined';
const registry = ssrContext?.registry ?? globalThis?.appstate;
const hostUrl = isClient ? globalThis.location.origin : 'http://localhost:8080';
const state = shallowRef<T | null>(null);
let resolve = () => {};
const promise = new Promise<void>((res) => {
resolve = res;
});
if (key in registry) {
state.value = registry[key] as T;
delete registry[key];
resolve();
} else {
if (!isClient) {
resolve();
}
onServerPrefetch(async () => {
const result = await getter({ hostUrl });
registry[key] = result;
state.value = result;
});
if (options?.suspensible ?? true) {
getter({ hostUrl }).then((result) => {
state.value = result;
resolve();
}).catch(resolve);
} else {
onMounted(async () => {
state.value = await getter({ hostUrl });
resolve();
});
}
}
return { result: state, stateIsReady: promise };
}

59
app/util.ts Normal file
View File

@@ -0,0 +1,59 @@
export function gid(id: string, doc: (Document | ShadowRoot) | undefined) {
return ((doc ?? document).getElementById(id));
}
export function qs(query: string, sub: (HTMLElement | ShadowRoot) | undefined) {
return ((sub ?? document).querySelector(query));
}
export function qsa(query: string, sub?: (HTMLElement | ShadowRoot) | undefined) {
return ((sub ?? document).querySelectorAll(query));
}
export function h<T extends keyof HTMLElementTagNameMap>(
tag: T,
opts?: Partial<HTMLElementTagNameMap[T]> | undefined,
children?: Node[],
) {
const newEl = Object.assign(document.createElement(tag), opts ?? {});
if (children) {
newEl.append(...children);
}
return newEl;
}
export function html(strs: TemplateStringsArray, ...vals: string[]) {
let result = strs[0];
for (let i = 1; i < strs.length; i++) {
result += vals[i] + strs[i];
}
const template = document.createElement("template");
template.innerHTML = result;
return template;
}
export function css(strs: TemplateStringsArray, ...vals: string[]) {
let result = strs[0];
for (let i = 1; i < strs.length; i++) {
result += vals[i] + strs[i];
}
const sheet = new CSSStyleSheet();
sheet.replaceSync(result);
return sheet;
}
export class DJElement extends HTMLElement {
static styles: CSSStyleSheet;
static template: HTMLTemplateElement;
root: ShadowRoot;
constructor() {
super();
const statics = this.constructor as typeof DJElement;
this.root = this.attachShadow({ mode: "open" });
this.root.appendChild(statics.template.content.cloneNode(true));
this.root.adoptedStyleSheets = statics.styles ? [statics.styles] : [];
}
}