diff --git a/app/DJTooltip.tsx b/app/DJTooltip.tsx index 4500d2e..da92132 100644 --- a/app/DJTooltip.tsx +++ b/app/DJTooltip.tsx @@ -1,5 +1,5 @@ import { nextTick, inject, provide, watch, type InjectionKey, onBeforeUnmount, watchEffect, onMounted, type Ref, type CSSProperties, defineComponent, ref } from "vue"; -import { h as djh } from "@/util.ts"; +import { addCSS, css, h as djh } from "@/util.ts"; type TooltipContext = { show: (newText: string, x: number, y: number) => void, @@ -8,9 +8,42 @@ type TooltipContext = { const tooltipContext = Symbol('tooltip') as InjectionKey; +const tooltipStyles = css` +.tooltip-container { + display: inline-block; +} + +.tooltip-carrier { + opacity: 0; + display: block; + pointer-events: none; + background-color: black; + border: white solid 1px; + color: white; + padding: 10px; + position: absolute; + z-index: 1; + overflow: hidden; + height: 0; + width: 0; + position: absolute; + transition: opacity 200ms, height 200ms, width 200ms; + + .text-carrier { + position: absolute; + width: 350px; + font-size: 16px; + font-family: "Roboto", serif; + display: block; + overflow: hidden; + } +}`; + export function setupTooltip(options: { carrier: Ref }) { const { carrier } = options; + addCSS('tooltip-carrier', tooltipStyles); + watchEffect(() => { if (carrier.value) { carrier.value.classList.add('tooltip-carrier'); @@ -82,6 +115,8 @@ export default defineComponent({ setup(props, { slots, attrs }) { const tooltip = inject(tooltipContext, () => { throw new Error('No tooltip context'); }, true); + onBeforeUnmount(() => tooltip.hide()); + return () => <>
{ - "/rp-articles": { slug: string; name: string }[]; + "/rp-articles": { slug: string; title: string, tags?: string[] }[]; } export type DJAPIResult = DJAPIResultMap[DJAPIEndpoint]; diff --git a/app/devtools-shim.ts b/app/devtools-shim.ts new file mode 100644 index 0000000..8e9db5f --- /dev/null +++ b/app/devtools-shim.ts @@ -0,0 +1 @@ +export function setupDevtoolsPlugin() {} diff --git a/app/generative-energy/GECalculator.tsx b/app/generative-energy/GECalculator.tsx index 071a56c..1274d69 100644 --- a/app/generative-energy/GECalculator.tsx +++ b/app/generative-energy/GECalculator.tsx @@ -17,17 +17,17 @@ export default defineComponent({ step: 0.1, }, { - name: "Armour, Natural Dessicated Thyroid", + name: 'Natural Dessicated Thyroid ("Armour")', mpg: 60, unit: "mg", }, { - name: 'Liothyronine (Triiodothyronine, "Cytomel/Cynomel", T3)', + name: 'Liothyronine (T3 - "Cytomel/Cynomel")', mpg: MPG_T3_SYN, unit: "mcg", }, { - name: "Levothyroxine (Thyroxine, T4)", + name: "Levothyroxine (T4)", mpg: MPG_T4_SYN, unit: "mcg", }, @@ -67,7 +67,7 @@ export default defineComponent({ {inputDefs.map((_) => ( - {_.name} + {_.name}{_.unit && (', ' + _.unit)}
@@ -86,20 +86,19 @@ export default defineComponent({ type="number" />
- {_.unit} ))} - Compounded (T3 and T4, "Cynoplus") + Compounded (T3 and T4 - "Cynoplus") Desired Ratio (T3:T4) - +
-
{" "} - :{" "} +
+
- Synthetic T3/T4 Combo + T3 mcg : T4 mcg - +
-
{" "} - :{" "} +
+
  • - 1st November 2024: Migrated to new web framework and fixed some buggy input. + 1st November 2024: Migrated to new web framework and fixed some buggy input/ugly styling.
  • 13th March 2024: Removed the synthetic/pure distinction as it was confusing and diff --git a/app/generative-energy/GEDeutsch.tsx b/app/generative-energy/GEDeutsch.tsx index 16b1dfa..15977b7 100644 --- a/app/generative-energy/GEDeutsch.tsx +++ b/app/generative-energy/GEDeutsch.tsx @@ -64,8 +64,9 @@ export default defineComponent({ {rpArticles.value && rpArticles.value.map((_) => (
  • - {_.name} + {_.title} + {_.tags?.map(tag => {tag})}
  • ))}
diff --git a/app/generative-energy/GEDeutschArticle.tsx b/app/generative-energy/GEDeutschArticle.tsx index ecd15df..7689d7d 100644 --- a/app/generative-energy/GEDeutschArticle.tsx +++ b/app/generative-energy/GEDeutschArticle.tsx @@ -1,7 +1,9 @@ -import { createTextVNode, defineComponent, h, inject, onServerPrefetch, ref, type VNode, watchEffect } from "vue"; +import { createTextVNode, computed, 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"; +import DJEmail from "@/DJEmail.tsx"; +import getDJAPI from "@/api.ts"; export default defineComponent({ name: "ge-deutsch-article", @@ -23,25 +25,32 @@ export default defineComponent({ (innerHTML: string) => Object.assign(document.createElement("div"), { innerHTML }), ); - 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('

')[1].split("

")[0]; return result; }, ); + const { + result: articleData, + stateIsReady: articleDataReady, + } = useAsyncState('article-data', ({hostUrl}) => getDJAPI(hostUrl, '/rp-articles')); + + const articleMetadata = computed(() => articleData.value?.find(_ => _.slug === props.articleName)); + + const title = computed(() => { + return articleContent.value?.split('

')[1].split("

")[0] ?? 'Artikel'; + }); + useHead({ title }); onServerPrefetch(() => new Promise((res) => { watchEffect(() => { if (title.value !== "") { - console.log("resolve", title.value); res(); } }); @@ -59,7 +68,7 @@ export default defineComponent({ children.unshift(h("button", { class: "swap", onClick: (e) => { - e.target.parentElement.classList.toggle("swap"); + (e.target as HTMLButtonElement).parentElement?.classList.toggle("swap"); }, }, "↻")); } @@ -71,7 +80,11 @@ export default defineComponent({ } } - return h((node as Element).tagName, attrs, children); + if (el.tagName === "H1") { + return h("header", attrs, h("h1", {}, children)); + } else { + return h((node as Element).tagName, attrs, children); + } } else { return createTextVNode(node.textContent ?? ""); } @@ -85,7 +98,8 @@ export default defineComponent({ return
Artikel lädt...
; } - await stateIsReady; + await Promise.all([stateIsReady, articleDataReady]); + console.log(articleMetadata.value); return () => (
@@ -95,6 +109,12 @@ export default defineComponent({ Sprache auf {currentLang.value === "en" ? "Deutsch" : "Englisch"} umschalten
+

+ Bei dem untenstehenden Artikel handelt es sich um eine hobbymäßige, amateurhafte Übersetzung des + Artikels „{ title.value }“ von Ray Peat. Bei Ungenauigkeiten oder Fehlübersetzungen freue ich mich über eine Mail! +

+ { articleMetadata.value?.tags?.includes('in-arbeit') &&
🚧 Bitte beachte, dass diese Übersetzung noch in Arbeit und darum nicht fertig ist! 🚧
} +
diff --git a/app/generative-energy/GEMain.tsx b/app/generative-energy/GEMain.tsx index 3dbbcc7..5411d41 100644 --- a/app/generative-energy/GEMain.tsx +++ b/app/generative-energy/GEMain.tsx @@ -1,5 +1,6 @@ import { RouterLink } from "vue-router"; import useHead from "@/useHead.ts"; +import DJTooltip from "@/DJTooltip.tsx"; export default { name: "ge-main", @@ -20,16 +21,14 @@ export default {

Links

  • - Thyroid Calculator - + + Thyroid Calculator +
  • - Ray Peat Articles in German - + + Ray Peat Articles in German +
diff --git a/app/generative-energy/GERoot.tsx b/app/generative-energy/GERoot.tsx index 988b2b2..1b2e118 100644 --- a/app/generative-energy/GERoot.tsx +++ b/app/generative-energy/GERoot.tsx @@ -1,4 +1,4 @@ -import { defineComponent, Suspense, type VNode } from "vue"; +import { defineComponent, ref, 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"; @@ -6,6 +6,7 @@ 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"; +import { setupTooltip } from "@/DJTooltip.tsx"; export const routes: RouteRecordRaw[] = [ { @@ -41,8 +42,11 @@ export default defineComponent({ name: "ge-root", setup() { const route = useRoute(); + const carrier = ref(null); + setupTooltip({ carrier }); return () => ( <> +
Generative Energy Home diff --git a/app/generative-energy/client.ts b/app/generative-energy/client.ts index 75b305f..f9c4947 100644 --- a/app/generative-energy/client.ts +++ b/app/generative-energy/client.ts @@ -1,8 +1,10 @@ import { createSSRApp } from "vue"; import { createRouter, createWebHistory } from "vue-router"; import GERoot, { routes } from "@/generative-energy/GERoot.tsx"; +import { cssRegistry } from "@/util.ts"; createSSRApp(GERoot) + .provide(cssRegistry, new Set()) .use(createRouter({ routes, history: createWebHistory("/generative-energy"), diff --git a/app/home/client.ts b/app/home/client.ts index aea082e..178f133 100644 --- a/app/home/client.ts +++ b/app/home/client.ts @@ -1,4 +1,7 @@ import { createSSRApp } from "vue"; import DJHomeRoot from "@/home/DJHomeRoot.tsx"; +import { cssRegistry } from "@/util.ts"; -createSSRApp(DJHomeRoot).mount("#app-root"); +createSSRApp(DJHomeRoot) + .provide(cssRegistry, new Set()) + .mount("#app-root"); diff --git a/app/useDJSSRContext.ts b/app/useDJSSRContext.ts index 0f2928f..36abcec 100644 --- a/app/useDJSSRContext.ts +++ b/app/useDJSSRContext.ts @@ -5,6 +5,7 @@ export type DJSSRContext = { title: MaybeRefOrGetter; }; registry: Record; + styles: Record; }; export default function useDJSSRContext() { diff --git a/app/util.ts b/app/util.ts index 199d2fb..6c1ebb2 100644 --- a/app/util.ts +++ b/app/util.ts @@ -1,3 +1,6 @@ +import { type InjectionKey, inject } from 'vue'; +import useDJSSRContext from "@/useDJSSRContext.ts"; + export function gid(id: string, doc: (Document | ShadowRoot) | undefined) { return ((doc ?? document).getElementById(id)); } @@ -37,9 +40,23 @@ export function css(strs: TemplateStringsArray, ...vals: string[]) { for (let i = 1; i < strs.length; i++) { result += vals[i] + strs[i]; } - const sheet = new CSSStyleSheet(); - sheet.replaceSync(result); - return sheet; + return result; +} + + +export const cssRegistry = Symbol('css-registry') as InjectionKey>; +export function addCSS(key: string, css: string) { + const context = useDJSSRContext(); + if (context && !context.styles[key]) { + context.styles[key] = css; + } else { + const registry = inject(cssRegistry); + if (!registry?.has(key)) { + const stylesheet = new CSSStyleSheet(); + stylesheet.replace(css); + document.adoptedStyleSheets.push(stylesheet); + } + } } /* diff --git a/main.ts b/main.ts index 3152d64..cfc50a8 100644 --- a/main.ts +++ b/main.ts @@ -13,20 +13,23 @@ const utf8Decoder = new TextDecoder("utf-8"); const parser = new DOMParser(); -function appHeaderScript(params: { appstate: Record; entryPath: string }) { +function appHeaderScript(params: { ssrContext: DJSSRContext, entryPath: string }) { return ` + `; } @@ -47,7 +50,7 @@ for await (const path of publicFiles) { sites.push(path); } -function getAPIResponse(apiReq: Request): Response { +async function getAPIResponse(apiReq: Request): Promise { let jsonResponse: DJAPIResult | { error: string } | null = null; let status = 200; @@ -62,22 +65,42 @@ function getAPIResponse(apiReq: Request): Response { const apiPath = pathname.split("/api")[1]; if (apiPath === "/rp-articles") { - jsonResponse = [ - { name: "Koffein: ein vitamin-ähnlicher Nährstoff, oder Adaptogen", slug: "caffeine" }, - { - name: "TSH, Temperatur, Puls, und andere Indikatoren bei einer Schilddrüsenunterfunktion", - slug: "hypothyroidism", - }, - ] satisfies DJAPIResultMap["/rp-articles"]; - } + const paths: string[] = []; + const contentDir = './public/generative-energy/content/'; + for await (const dirEnt of Deno.readDir(contentDir)) { + if (dirEnt.isFile && dirEnt.name.endsWith('.html')) { + paths.push(`${contentDir}${dirEnt.name}`); + } + } + const result: DJAPIResultMap['/rp-articles'] = []; + for (const filePath of paths) { + const content = await Deno.readTextFile(filePath); + const dom = parser.parseFromString(content, 'text/html'); + const metadata = { title: '', tags: [] as string[], slug: '' }; + const metaTags = dom.querySelectorAll('meta') as unknown as NodeListOf; + for (const metaTag of metaTags) { + const name = metaTag.attributes.getNamedItem('name')?.value ?? ''; + const content = metaTag.attributes.getNamedItem('content')?.value ?? ''; + if (name === 'title') { + metadata.title = content; + } else if (name === 'tags') { + metadata.tags = content ? content.split(",") : []; + } else if (name === 'slug') { + metadata.slug = content; + } + } + result.push(metadata); + } + jsonResponse = result; + console.log(result); + } + } if (!jsonResponse) { jsonResponse = { error: `API route ${ apiPath } not found.` }; status = 404; } - } - const headers = new Headers(); headers.set("Content-Type", "application/json"); return new Response(JSON.stringify(jsonResponse), { @@ -93,13 +116,17 @@ Deno.serve({ console.log(`Listening on port http://${hostname}:${port}/`); }, }, async (req, _conn) => { + const timeStart = new Date().getTime(); + let response: Response | null = null; + const url = URL.parse(req.url); + if (req.method === "GET") { - const pathname = URL.parse(req.url)?.pathname ?? "/"; + const pathname = url?.pathname ?? "/"; if (pathname.startsWith("/api/")) { - response = getAPIResponse(req); + response = await getAPIResponse(req); } // Public/static files @@ -144,7 +171,7 @@ Deno.serve({ app.provide("dom-parse", (innerHTML: string) => { return parser.parseFromString(innerHTML, "text/html").documentElement; }); - const ssrContext: DJSSRContext = { registry: {}, head: { title: "" } }; + const ssrContext: DJSSRContext = { styles: {}, registry: {}, head: { title: "" } }; if (router) { await router.replace(pathname.split("/generative-energy")[1]); await router.isReady(); @@ -155,7 +182,7 @@ Deno.serve({ .replaceAll("%TITLE%", toValue(ssrContext.head?.title) ?? "Site") .replace( ``, - appHeaderScript({ appstate: ssrContext.registry, entryPath: clientEntry }), + appHeaderScript({ ssrContext, entryPath: clientEntry }), ); response = new Response(content, { headers: { "Content-Type": "text/html" } }); } @@ -164,7 +191,13 @@ Deno.serve({ response = new Response("Only GET allowed.", { status: 500 }); } - return response ?? new Response("Not found.", { status: 404 }); + response ??= new Response("Not found.", { status: 404 }) + + const timeEnd = new Date().getTime(); + + console.log(`Request ${ url?.pathname ?? 'malformed' }\tStatus ${ response.status }, Duration ${ timeEnd - timeStart }ms`); + + return response; }); Deno.addSignalListener("SIGINT", () => { diff --git a/public/generative-energy/content/caffeine.html b/public/generative-energy/content/caffeine.html index 7dcedff..b79d2b6 100644 --- a/public/generative-energy/content/caffeine.html +++ b/public/generative-energy/content/caffeine.html @@ -1,3 +1,7 @@ + + + +

Caffeine: A vitamin-like nutrient, or adaptogen

Koffein: ein vitamin-ähnlicher Nährstoff, oder Adaptogen

diff --git a/public/generative-energy/content/hypothyroidism.html b/public/generative-energy/content/hypothyroidism.html index bb819b2..905cad8 100644 --- a/public/generative-energy/content/hypothyroidism.html +++ b/public/generative-energy/content/hypothyroidism.html @@ -1,3 +1,7 @@ + + + +

TSH, temperature, pulse rate, and other indicators in hypothyroidism

TSH, Temperatur, Puls, und andere Indikatoren bei einer Schilddrüsenunterfunktion

diff --git a/public/generative-energy/styles.css b/public/generative-energy/styles.css index e5368dd..e1885ea 100644 --- a/public/generative-energy/styles.css +++ b/public/generative-energy/styles.css @@ -14,6 +14,11 @@ h1, h2, h3, h4, h5 { font-family: "Roboto Slab", "Segoe UI", Tahoma, Geneva, Verdana, sans-serif; } +.baustelle { + text-align: center; + display: block; +} + a.home-btn { display: block; text-align: center; @@ -23,6 +28,15 @@ a.home-btn { } } +span.tag { + margin-left: 4px; + font-style: italic; + + &::before { + content: '#'; + } +} + .ge-article { .header { margin-top: 20px; @@ -78,28 +92,28 @@ a.home-btn { display: none; } - .lang-en *:lang(de) { + .lang-en span:lang(de) { display: none; } - .lang-de *:lang(en) { + .lang-de span:lang(en) { display: none; } } main { - width: 100%; + width: calc(100% - 20px); + margin: auto; } -@media (min-width: 600px) { +@media (min-width: 900px) { main { - max-width: 1340px; - margin: auto; + max-width: 943px; min-width: 600px; - width: 60%; } } + header { margin: auto; text-align: center; @@ -174,22 +188,45 @@ footer { 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; } + + td.right { + text-align: center; + } + .breathe { padding-left: 4px; padding-right: 4px; } + + .separator::before { + content: '—'; + } + @media (min-width: 600px) { + .separator::before { + content: ':'; + padding-left: 4px; + padding-right: 4px; + } + + td.ratio { + text-align: left; + } + td.right div { display: inline-block; } diff --git a/public/home/main.css b/public/home/main.css index b4a1547..821e0dc 100644 --- a/public/home/main.css +++ b/public/home/main.css @@ -183,35 +183,3 @@ a { text-decoration: none; cursor: pointer; } - -.tooltip-container { - display: inline-block; -} - -.tooltip-carrier { - opacity: 0; - display: block; - pointer-events: none; - background-color: black; - border: white solid 1px; - color: white; - padding: 10px; - position: absolute; - z-index: 1; - overflow: hidden; - height: 0; - width: 0; - position: absolute; - transition: opacity 200ms, height 200ms, width 200ms; - - .text-carrier { - position: absolute; - width: 350px; - font-size: 16px; - font-family: "Roboto", serif; - display: block; - overflow: hidden; - } -} - -