This commit is contained in:
Daniel Ledda
2024-11-01 15:42:09 +01:00
parent f60e975765
commit 7539e6ed48
50 changed files with 2004 additions and 750 deletions

16
app/DJDonate.tsx Normal file
View File

@@ -0,0 +1,16 @@
import { defineComponent } from "vue";
export default defineComponent({
name: "dj-donate",
setup: () => {
const imgsrc =
"https://img.buymeacoffee.com/button-api/?text=Buy me a coffee&emoji=&slug=djledda&button_colour=FFDD00&font_colour=000000&font_family=Cookie&outline_colour=000000&coffee_colour=ffffff";
return () => (
<div class="dj-donate">
<a href="https://www.buymeacoffee.com/djledda">
<img src={imgsrc} />
</a>
</div>
);
},
});

View File

@@ -14,7 +14,7 @@ export default defineComponent({
}
return () => (
<a href="#" onClick={clickMail}>
{ slots.default ? slots.default() : 'dan.j.ledda [at] gmail [dot] com' }
{slots.default ? slots.default() : "dan.j.ledda [at] gmail [dot] com"}
</a>
);
},

80
app/DJTooltip.tsx Normal file
View File

@@ -0,0 +1,80 @@
import { watchEffect, watch, onMounted, type CSSProperties, defineComponent, 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 = {
fontSize: '16px',
fontFamily: "Roboto, serif",
display: "block",
overflow: "hidden",
} satisfies CSSProperties;
const defaultWidth = 350;
export default defineComponent({
name: "dj-sexy-tooltip",
props: {
tooltip: {
type: String,
required: true,
},
},
setup(props, { slots, attrs }) {
const active = ref(false);
const carrier = ref<HTMLElement | null>(null);
const textCarrier = ref<HTMLElement | null>(null);
onMounted(() => {
document.addEventListener("mousemove", (event) => {
const pos = { x: event.pageX, y: event.pageY };
if (carrier.value && getComputedStyle(carrier.value).opacity !== "0") {
if (pos.x + 15 + carrier.value.clientWidth <= document.body.scrollWidth) {
carrier.value.style.left = (pos.x + 15) + "px";
} else {
carrier.value.style.left = (document.body.scrollWidth - carrier.value.clientWidth - 5) + "px";
}
if (pos.y + carrier.value.clientHeight <= document.body.scrollHeight) {
carrier.value.style.top = pos.y + "px";
} else {
carrier.value.style.top = (document.body.scrollHeight - carrier.value.clientHeight - 5) + "px";
}
}
});
});
watchEffect(() => {
if (carrier.value) {
carrier.value.style.height = active.value ? '16px' : '0';
carrier.value.style.opacity = active.value ? '1' : '0';
carrier.value.style.width = active.value ? '350px' : '0';
}
});
return () => <>
<div class="tooltip-container" {...attrs}
onMouseenter={() => { active.value = true; }}
onMouseleave={() => { active.value = false; }}
>
{slots.default && <slots.default />}
</div>
<div style={carrierStyle} ref={carrier}>
<span style={textCarrierStyle}>{props.tooltip}</span>
</div>
</>;
},
});

14
app/api.ts Normal file
View File

@@ -0,0 +1,14 @@
export type DJAPIEndpoint = "/rp-articles";
export interface DJAPIResultMap extends Record<DJAPIEndpoint, unknown> {
"/rp-articles": { slug: string; name: string }[];
}
export type DJAPIResult = DJAPIResultMap[DJAPIEndpoint];
export default async function getDJAPI<T extends DJAPIEndpoint>(
hostUrl: string,
endpoint: T,
): Promise<DJAPIResultMap[typeof endpoint]> {
return await (await fetch(`${hostUrl}/api${endpoint}`)).json();
}

View File

@@ -1,194 +1,7 @@
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),
});
}
}
*/
import { computed, defineComponent, ref } from "vue";
export default defineComponent({
name: 'ge-calculator',
name: "ge-calculator",
setup() {
const t3Ratio = ref(1);
const t4Ratio = ref(4);
@@ -209,12 +22,12 @@ export default defineComponent({
unit: "mg",
},
{
name: 'Liothyronine (Triiodothyronine, "Cytomel", T3)',
name: 'Liothyronine (Triiodothyronine, "Cytomel/Cynomel", T3)',
mpg: MPG_T3_SYN,
unit: "mcg",
},
{
name: 'Levothyroxine (Thyroxine, "Cynoplus", T4)',
name: "Levothyroxine (Thyroxine, T4)",
mpg: MPG_T4_SYN,
unit: "mcg",
},
@@ -222,7 +35,6 @@ export default defineComponent({
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;
@@ -235,121 +47,138 @@ export default defineComponent({
};
});
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>
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((_) => (
<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, "Cynoplus")</strong>
</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>
</>;
}
<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

@@ -1,61 +1,76 @@
import { defineComponent } from 'vue';
import { RouterLink } from 'vue-router';
import { defineComponent } from "vue";
import { RouterLink } from "vue-router";
import DJEmail from "@/DJEmail.tsx";
import useHead from "@/useHead.ts";
import useAsyncState from "@/useAsyncState.ts";
import getDJAPI from "@/api.ts";
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>
</>;
}
name: "ge-deutsch",
async setup() {
useHead({ title: "Ray Peat Artikel auf Deutsch" });
const {
result: rpArticles,
stateIsReady,
} = useAsyncState("rp-articles", ({ hostUrl }) => getDJAPI(hostUrl, "/rp-articles"));
await stateIsReady;
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">
{rpArticles.value && rpArticles.value.map((_) => (
<li>
<RouterLink to={{ name: "GEDeutschArticle", params: { articleName: _.slug } }}>
{_.name}
</RouterLink>
</li>
))}
</ul>
</div>
</>
);
},
});

View File

@@ -1,9 +1,10 @@
import { h, inject, defineComponent, ref, createTextVNode, type VNode } from 'vue';
import { RouterLink } from 'vue-router';
import { createTextVNode, defineComponent, h, inject, onServerPrefetch, ref, type VNode, watchEffect } from "vue";
import { RouterLink } from "vue-router";
import useAsyncState from "@/useAsyncState.ts";
import useHead from "@/useHead.ts";
export default defineComponent({
name: 'ge-deutsch-article',
name: "ge-deutsch-article",
props: {
articleName: {
type: String,
@@ -17,24 +18,50 @@ export default defineComponent({
currentLang.value = currentLang.value === "en" ? "de" : "en";
}
const parseDom = inject('dom-parse', (innerHTML: string) => Object.assign(document.createElement('div'), { innerHTML }));
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();
});
const title = ref("");
const { result: articleContent, stateIsReady } = useAsyncState(
"ge-deutsch-article-data",
async ({ hostUrl }) => {
const articleResponse = await fetch(`${hostUrl}/generative-energy/content/${props.articleName}.html`);
const result = await articleResponse.text();
title.value = result.split('<h1 lang="de">')[1].split("</h1>")[0];
return result;
},
);
useHead({ title });
onServerPrefetch(() =>
new Promise<void>((res) => {
watchEffect(() => {
if (title.value !== "") {
console.log("resolve", title.value);
res();
}
});
})
);
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(_));
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');
} }, '↻'));
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++) {
@@ -46,30 +73,32 @@ export default defineComponent({
return h((node as Element).tagName, attrs, children);
} else {
return createTextVNode(node.textContent ?? '');
return createTextVNode(node.textContent ?? "");
}
}
function ArticleContentTransformed() {
if (articleContent.value) {
const dom = parseDom(articleContent.value);
return h('div', {}, [...dom.children].map(_ => transformArticleNode(_)));
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>
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>
<article class={`lang-${ currentLang.value }`}>
<ArticleContentTransformed />
</article>
</div>;
}
);
},
});

View File

@@ -1,33 +1,39 @@
import { RouterLink } from 'vue-router';
import { RouterLink } from "vue-router";
import useHead from "@/useHead.ts";
export default {
name: 'ge-main',
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>
</>
useHead({ title: "Generative Energy" });
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

@@ -1,21 +0,0 @@
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

@@ -1,34 +1,34 @@
import { defineComponent, type VNode, Suspense } from "vue";
import { useRoute, RouterLink, RouterView, type RouteRecordRaw } from 'vue-router';
import { defineComponent, Suspense, type VNode } from "vue";
import { type RouteRecordRaw, RouterLink, RouterView, useRoute } 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";
import DJDonate from "@/DJDonate.tsx";
export const routes: RouteRecordRaw[] = [
{
path: '/',
name: 'GEMain',
path: "/",
name: "GEMain",
component: GEMain,
},
{
path: '/calculator',
name: 'GECalculator',
path: "/calculator",
name: "GECalculator",
component: GECalculator,
},
{
path: '/raypeat-deutsch',
name: 'GEDeutsch',
path: "/raypeat-deutsch",
name: "GEDeutsch",
component: GEDeutsch,
},
{
path: '/raypeat-deutsch/article/:articleName',
name: 'GEDeutschArticle',
path: "/raypeat-deutsch/article/:articleName",
name: "GEDeutschArticle",
component: GEDeutschArticle,
props: ({ params }) => {
if ('articleName' in params) {
if ("articleName" in params) {
return { articleName: params.articleName };
} else {
return false;
@@ -41,27 +41,35 @@ 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>
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="/">djledda.de</a> 2023 - <DJEmail>{() => "Contact"}</DJEmail>
</div>
<DJDonate />
</div>
<GEPaypal />
</div>
</footer>
</main>
</>;
</footer>
</main>
</>
);
},
});

View File

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

View File

@@ -0,0 +1,12 @@
import { createSSRApp } from "vue";
import { createMemoryHistory, createRouter } from "vue-router";
import GERoot, { routes } from "@/generative-energy/GERoot.tsx";
export default function createApp() {
const router = createRouter({
routes,
history: createMemoryHistory("/generative-energy"),
});
const app = createSSRApp(GERoot).use(router);
return { app, router };
}

View File

@@ -1,19 +1,75 @@
import { computed, defineComponent, ref } from "vue";
import { defineComponent } from "vue";
import useHead from "@/useHead.ts";
import DJTooltip from "@/DJTooltip.tsx";
import DJEmail from "@/DJEmail.tsx";
export default defineComponent({
name: "app-root",
setup() {
const count = ref(0);
const countDouble = computed(() => count.value * 2);
count.value++;
useHead({ title: "DJ Ledda's Homepage" });
return () => (
<div class="app-main">
<button class="test" onClick={() => count.value++}>Click me!</button>
<div>Count: {count.value}</div>
<div>Count Double: {countDouble.value}</div>
<p>
<a href="/generative-energy/">Go to this other site hosted here but a different Vue app!</a>
</p>
<div class="supercontainer">
<div class="shakeable">
<div class="title_name">
<DJTooltip tooltip="I wonder what he's listening to?">
<img src="/home/img/dj.gif" alt="dj legt krasse Mucke auf" class="dude" />
</DJTooltip>
<DJTooltip tooltip="Easily the coolest guy out there.">
<span>DJ Ledda</span>
</DJTooltip>
<DJTooltip tooltip="I once heard this guy played at revs.">
<img src="/home/img/dj.gif" alt="dj laying down some sick beats" class="dude" />
</DJTooltip>
</div>
<div class="main">
<div class="subject">
<div class="resourcelist">
<a href="https://drum-slayer.com">
<DJTooltip class="resource" tooltip="Small app for designing multitrack looped rhythms with local save and multiple files. Originally built using just vanilla TypeScript and CSS, now with Vue.">
Drum Slayer
</DJTooltip>
</a>
<a href="/somaesque">
<DJTooltip class="resource" tooltip="Puzzle solver app for puzzle cubes resembling the original Soma Cube puzzle. Save and edit your own puzzles! Built with Svelte, THREE.js and AssemblyScript.">
Somaesque
</DJTooltip>
</a>
<a href="/generative-energy">
<DJTooltip class="resource" tooltip="Thyroid calculator, German translations, and more...">
Generative Energy - Ray Peat Resources
</DJTooltip>
</a>
<a href="/home/muenchen-auf-englisch.html">
<DJTooltip class="resource" tooltip="
Authentic historically accurate translations of all of Munich's S-Bahn and U-Bahn
stations, as well as the main municipalities, into English. You live in Allach? It's
Axleigh now. Giesing? Nope! Kyesing! This is a WIP.
">
München auf Englisch - Munich in English
</DJTooltip>
</a>
<a href="/kadi/">
<DJTooltip class="resource" tooltip="Make an account and start saving paper and tracking your Yatzy stats with your
friends! Make your own rulesets, and more. Built with React, express.js, and
MongoDB. Currently inactive.">
K A D I: Online Yatzy Scoresheets
</DJTooltip>
</a>
<a href="http://git.djledda.de/Ledda">
<DJTooltip class="resource" tooltip="Check out what I'm coding!">
My git projects
</DJTooltip>
</a>
<DJEmail>
<DJTooltip class="resource" tooltip="You'll see my address when you click here.">
Click here to get in touch
</DJTooltip>
</DJEmail>
</div>
</div>
</div>
<div id="tooltipCarrier"></div>
</div>
</div>
);
},

4
app/home/client.ts Normal file
View File

@@ -0,0 +1,4 @@
import { createSSRApp } from "vue";
import App from "@/home/App.tsx";
createSSRApp(App).mount("#app-root");

7
app/home/server.ts Normal file
View File

@@ -0,0 +1,7 @@
import { createSSRApp } from "vue";
import App from "@/home/App.tsx";
export default function createApp() {
const app = createSSRApp(App);
return { app, router: null };
}

View File

@@ -1,13 +0,0 @@
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: {},
};
}
}

View File

@@ -1,122 +0,0 @@
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>
);
},
});

View File

@@ -1,17 +1,22 @@
import { onMounted, onServerPrefetch, useSSRContext, shallowRef, type ShallowRef } from 'vue';
import { onMounted, onServerPrefetch, type ShallowRef, shallowRef } from "vue";
import useDJSSRContext from "@/useDJSSRContext.ts";
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';
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 = useDJSSRContext();
const isClient = typeof ssrContext === "undefined";
const registry = ssrContext?.registry ?? globalThis?.appstate;
const hostUrl = isClient ? globalThis.location.origin : 'http://localhost:8080';
const hostUrl = isClient ? globalThis.location.origin : "http://localhost:8080";
const state = shallowRef<T | null>(null);

12
app/useDJSSRContext.ts Normal file
View File

@@ -0,0 +1,12 @@
import { type MaybeRefOrGetter, useSSRContext } from "vue";
export type DJSSRContext = {
head: {
title: MaybeRefOrGetter<string>;
};
registry: Record<string, unknown>;
};
export default function useDJSSRContext() {
return useSSRContext<DJSSRContext>();
}

19
app/useHead.ts Normal file
View File

@@ -0,0 +1,19 @@
import { watchEffect, toValue, type MaybeRefOrGetter } from 'vue';
import useDJSSRContext from "@/useDJSSRContext.ts";
export default function useHead(params: {
title: MaybeRefOrGetter<string>,
}) {
const context = useDJSSRContext();
if (context) {
context.head.title = params.title;
} else {
watchEffect(() => {
const newTitle = toValue(params.title);
if (newTitle) {
document.title = newTitle;
}
});
}
}