nice
This commit is contained in:
21
app/DJEmail.tsx
Normal file
21
app/DJEmail.tsx
Normal 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>
|
||||
);
|
||||
},
|
||||
});
|
||||
355
app/generative-energy/GECalculator.tsx
Normal file
355
app/generative-energy/GECalculator.tsx
Normal 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>
|
||||
</>;
|
||||
}
|
||||
});
|
||||
61
app/generative-energy/GEDeutsch.tsx
Normal file
61
app/generative-energy/GEDeutsch.tsx
Normal 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>
|
||||
</>;
|
||||
}
|
||||
});
|
||||
75
app/generative-energy/GEDeutschArticle.tsx
Normal file
75
app/generative-energy/GEDeutschArticle.tsx
Normal 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>;
|
||||
}
|
||||
});
|
||||
33
app/generative-energy/GEMain.tsx
Normal file
33
app/generative-energy/GEMain.tsx
Normal 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>
|
||||
</>
|
||||
},
|
||||
};
|
||||
21
app/generative-energy/GEPaypal.tsx
Normal file
21
app/generative-energy/GEPaypal.tsx
Normal 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>
|
||||
);
|
||||
},
|
||||
});
|
||||
67
app/generative-energy/GERoot.tsx
Normal file
67
app/generative-energy/GERoot.tsx
Normal 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>
|
||||
</>;
|
||||
},
|
||||
});
|
||||
298
app/generative-energy/calc.ts
Normal file
298
app/generative-energy/calc.ts
Normal 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);
|
||||
10
app/generative-energy/client.ts
Normal file
10
app/generative-energy/client.ts
Normal 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');
|
||||
135
app/generative-energy/half-life.ts
Normal file
135
app/generative-energy/half-life.ts
Normal 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);
|
||||
@@ -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
13
app/meta.ts
Normal 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
74
app/reactivity.ts
Normal 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();
|
||||
}
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
export default () => <div class="test">Test</div>;
|
||||
122
app/tooltip.tsx
Normal file
122
app/tooltip.tsx
Normal 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
50
app/useAsyncState.ts
Normal 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
59
app/util.ts
Normal 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] : [];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user