@@ -1,5 +1,5 @@ | |||||
import { nextTick, inject, provide, watch, type InjectionKey, onBeforeUnmount, watchEffect, onMounted, type Ref, type CSSProperties, defineComponent, ref } from "vue"; | 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 = { | type TooltipContext = { | ||||
show: (newText: string, x: number, y: number) => void, | show: (newText: string, x: number, y: number) => void, | ||||
@@ -8,9 +8,42 @@ type TooltipContext = { | |||||
const tooltipContext = Symbol('tooltip') as InjectionKey<TooltipContext>; | const tooltipContext = Symbol('tooltip') as InjectionKey<TooltipContext>; | ||||
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<HTMLElement | null> }) { | export function setupTooltip(options: { carrier: Ref<HTMLElement | null> }) { | ||||
const { carrier } = options; | const { carrier } = options; | ||||
addCSS('tooltip-carrier', tooltipStyles); | |||||
watchEffect(() => { | watchEffect(() => { | ||||
if (carrier.value) { | if (carrier.value) { | ||||
carrier.value.classList.add('tooltip-carrier'); | carrier.value.classList.add('tooltip-carrier'); | ||||
@@ -82,6 +115,8 @@ export default defineComponent({ | |||||
setup(props, { slots, attrs }) { | setup(props, { slots, attrs }) { | ||||
const tooltip = inject(tooltipContext, () => { throw new Error('No tooltip context'); }, true); | const tooltip = inject(tooltipContext, () => { throw new Error('No tooltip context'); }, true); | ||||
onBeforeUnmount(() => tooltip.hide()); | |||||
return () => <> | return () => <> | ||||
<div class="tooltip-container" | <div class="tooltip-container" | ||||
{...attrs} | {...attrs} | ||||
@@ -1,7 +1,7 @@ | |||||
export type DJAPIEndpoint = "/rp-articles"; | export type DJAPIEndpoint = "/rp-articles"; | ||||
export interface DJAPIResultMap extends Record<DJAPIEndpoint, unknown> { | export interface DJAPIResultMap extends Record<DJAPIEndpoint, unknown> { | ||||
"/rp-articles": { slug: string; name: string }[]; | |||||
"/rp-articles": { slug: string; title: string, tags?: string[] }[]; | |||||
} | } | ||||
export type DJAPIResult = DJAPIResultMap[DJAPIEndpoint]; | export type DJAPIResult = DJAPIResultMap[DJAPIEndpoint]; | ||||
@@ -0,0 +1 @@ | |||||
export function setupDevtoolsPlugin() {} |
@@ -17,17 +17,17 @@ export default defineComponent({ | |||||
step: 0.1, | step: 0.1, | ||||
}, | }, | ||||
{ | { | ||||
name: "Armour, Natural Dessicated Thyroid", | |||||
name: 'Natural Dessicated Thyroid ("Armour")', | |||||
mpg: 60, | mpg: 60, | ||||
unit: "mg", | unit: "mg", | ||||
}, | }, | ||||
{ | { | ||||
name: 'Liothyronine (Triiodothyronine, "Cytomel/Cynomel", T3)', | |||||
name: 'Liothyronine (T3 - "Cytomel/Cynomel")', | |||||
mpg: MPG_T3_SYN, | mpg: MPG_T3_SYN, | ||||
unit: "mcg", | unit: "mcg", | ||||
}, | }, | ||||
{ | { | ||||
name: "Levothyroxine (Thyroxine, T4)", | |||||
name: "Levothyroxine (T4)", | |||||
mpg: MPG_T4_SYN, | mpg: MPG_T4_SYN, | ||||
unit: "mcg", | unit: "mcg", | ||||
}, | }, | ||||
@@ -67,7 +67,7 @@ export default defineComponent({ | |||||
{inputDefs.map((_) => ( | {inputDefs.map((_) => ( | ||||
<tr key={_.name}> | <tr key={_.name}> | ||||
<td> | <td> | ||||
{_.name} | |||||
{_.name}{_.unit && (', ' + _.unit)} | |||||
</td> | </td> | ||||
<td class="right"> | <td class="right"> | ||||
<div style="display: inline-block;"> | <div style="display: inline-block;"> | ||||
@@ -86,20 +86,19 @@ export default defineComponent({ | |||||
type="number" | type="number" | ||||
/> | /> | ||||
</div> | </div> | ||||
<span class="breathe">{_.unit}</span> | |||||
</td> | </td> | ||||
</tr> | </tr> | ||||
))} | ))} | ||||
<tr> | <tr> | ||||
<td colspan="2"> | <td colspan="2"> | ||||
<strong>Compounded (T3 and T4, "Cynoplus")</strong> | |||||
<strong>Compounded (T3 and T4 - "Cynoplus")</strong> | |||||
</td> | </td> | ||||
</tr> | </tr> | ||||
<tr class="ratios"> | <tr class="ratios"> | ||||
<td> | <td> | ||||
Desired Ratio (T3:T4) | Desired Ratio (T3:T4) | ||||
</td> | </td> | ||||
<td class="right"> | |||||
<td class="right ratio"> | |||||
<div> | <div> | ||||
<input | <input | ||||
value={t3Ratio.value} | value={t3Ratio.value} | ||||
@@ -110,8 +109,8 @@ export default defineComponent({ | |||||
step="1" | step="1" | ||||
type="number" | type="number" | ||||
/> | /> | ||||
</div>{" "} | |||||
:{" "} | |||||
</div> | |||||
<span class="separator"/> | |||||
<div> | <div> | ||||
<input | <input | ||||
value={t4Ratio.value} | value={t4Ratio.value} | ||||
@@ -127,9 +126,9 @@ export default defineComponent({ | |||||
</tr> | </tr> | ||||
<tr class="synthetic"> | <tr class="synthetic"> | ||||
<td> | <td> | ||||
Synthetic T3/T4 Combo | |||||
T3 mcg : T4 mcg | |||||
</td> | </td> | ||||
<td class="right"> | |||||
<td class="right ratio"> | |||||
<div> | <div> | ||||
<input | <input | ||||
value={compounded.value.t3Syn} | value={compounded.value.t3Syn} | ||||
@@ -143,8 +142,8 @@ export default defineComponent({ | |||||
step="1" | step="1" | ||||
type="number" | type="number" | ||||
/> | /> | ||||
</div>{" "} | |||||
:{" "} | |||||
</div> | |||||
<span class="separator"/> | |||||
<div> | <div> | ||||
<input | <input | ||||
value={compounded.value.t4Syn} | value={compounded.value.t4Syn} | ||||
@@ -169,7 +168,7 @@ export default defineComponent({ | |||||
<p> | <p> | ||||
<ul> | <ul> | ||||
<li> | <li> | ||||
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. | |||||
</li> | </li> | ||||
<li> | <li> | ||||
13th March 2024: Removed the synthetic/pure distinction as it was confusing and | 13th March 2024: Removed the synthetic/pure distinction as it was confusing and | ||||
@@ -64,8 +64,9 @@ export default defineComponent({ | |||||
{rpArticles.value && rpArticles.value.map((_) => ( | {rpArticles.value && rpArticles.value.map((_) => ( | ||||
<li> | <li> | ||||
<RouterLink to={{ name: "GEDeutschArticle", params: { articleName: _.slug } }}> | <RouterLink to={{ name: "GEDeutschArticle", params: { articleName: _.slug } }}> | ||||
{_.name} | |||||
{_.title} | |||||
</RouterLink> | </RouterLink> | ||||
{_.tags?.map(tag => <span class="tag">{tag}</span>)} | |||||
</li> | </li> | ||||
))} | ))} | ||||
</ul> | </ul> | ||||
@@ -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 { RouterLink } from "vue-router"; | ||||
import useAsyncState from "@/useAsyncState.ts"; | import useAsyncState from "@/useAsyncState.ts"; | ||||
import useHead from "@/useHead.ts"; | import useHead from "@/useHead.ts"; | ||||
import DJEmail from "@/DJEmail.tsx"; | |||||
import getDJAPI from "@/api.ts"; | |||||
export default defineComponent({ | export default defineComponent({ | ||||
name: "ge-deutsch-article", | name: "ge-deutsch-article", | ||||
@@ -23,25 +25,32 @@ export default defineComponent({ | |||||
(innerHTML: string) => Object.assign(document.createElement("div"), { innerHTML }), | (innerHTML: string) => Object.assign(document.createElement("div"), { innerHTML }), | ||||
); | ); | ||||
const title = ref(""); | |||||
const { result: articleContent, stateIsReady } = useAsyncState( | const { result: articleContent, stateIsReady } = useAsyncState( | ||||
"ge-deutsch-article-data", | "ge-deutsch-article-data", | ||||
async ({ hostUrl }) => { | async ({ hostUrl }) => { | ||||
const articleResponse = await fetch(`${hostUrl}/generative-energy/content/${props.articleName}.html`); | const articleResponse = await fetch(`${hostUrl}/generative-energy/content/${props.articleName}.html`); | ||||
const result = await articleResponse.text(); | const result = await articleResponse.text(); | ||||
title.value = result.split('<h1 lang="de">')[1].split("</h1>")[0]; | |||||
return result; | 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('<h1 lang="de">')[1].split("</h1>")[0] ?? 'Artikel'; | |||||
}); | |||||
useHead({ title }); | useHead({ title }); | ||||
onServerPrefetch(() => | onServerPrefetch(() => | ||||
new Promise<void>((res) => { | new Promise<void>((res) => { | ||||
watchEffect(() => { | watchEffect(() => { | ||||
if (title.value !== "") { | if (title.value !== "") { | ||||
console.log("resolve", title.value); | |||||
res(); | res(); | ||||
} | } | ||||
}); | }); | ||||
@@ -59,7 +68,7 @@ export default defineComponent({ | |||||
children.unshift(h("button", { | children.unshift(h("button", { | ||||
class: "swap", | class: "swap", | ||||
onClick: (e) => { | 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 { | } else { | ||||
return createTextVNode(node.textContent ?? ""); | return createTextVNode(node.textContent ?? ""); | ||||
} | } | ||||
@@ -85,7 +98,8 @@ export default defineComponent({ | |||||
return <div>Artikel lädt...</div>; | return <div>Artikel lädt...</div>; | ||||
} | } | ||||
await stateIsReady; | |||||
await Promise.all([stateIsReady, articleDataReady]); | |||||
console.log(articleMetadata.value); | |||||
return () => ( | return () => ( | ||||
<div class="ge-article"> | <div class="ge-article"> | ||||
@@ -95,6 +109,12 @@ export default defineComponent({ | |||||
Sprache auf <span>{currentLang.value === "en" ? "Deutsch" : "Englisch"}</span> umschalten | Sprache auf <span>{currentLang.value === "en" ? "Deutsch" : "Englisch"}</span> umschalten | ||||
</button> | </button> | ||||
</div> | </div> | ||||
<p class="text-slab"> | |||||
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 <DJEmail>eine Mail</DJEmail>! | |||||
</p> | |||||
{ articleMetadata.value?.tags?.includes('in-arbeit') && <h5 class="baustelle">🚧 Bitte beachte, dass diese Übersetzung noch in Arbeit und darum nicht fertig ist! 🚧</h5> } | |||||
<hr /> | |||||
<article class={`lang-${currentLang.value}`}> | <article class={`lang-${currentLang.value}`}> | ||||
<ArticleContentTransformed /> | <ArticleContentTransformed /> | ||||
</article> | </article> | ||||
@@ -1,5 +1,6 @@ | |||||
import { RouterLink } from "vue-router"; | import { RouterLink } from "vue-router"; | ||||
import useHead from "@/useHead.ts"; | import useHead from "@/useHead.ts"; | ||||
import DJTooltip from "@/DJTooltip.tsx"; | |||||
export default { | export default { | ||||
name: "ge-main", | name: "ge-main", | ||||
@@ -20,16 +21,14 @@ export default { | |||||
<h2>Links</h2> | <h2>Links</h2> | ||||
<ul> | <ul> | ||||
<li> | <li> | ||||
<RouterLink to={{ name: "GECalculator" }}>Thyroid Calculator</RouterLink> | |||||
<span style="display: none" class="tooltip"> | |||||
Convert to and from grains, set ratios, etc. | |||||
</span> | |||||
<DJTooltip tooltip="Convert to and from grains, set ratios, etc."> | |||||
<RouterLink to={{ name: "GECalculator" }}>Thyroid Calculator</RouterLink> | |||||
</DJTooltip> | |||||
</li> | </li> | ||||
<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> | |||||
<DJTooltip tooltip="A selection of articles by Ray that I have translated in my spare time into German."> | |||||
<RouterLink to={{ name: "GEDeutsch" }}>Ray Peat Articles in German</RouterLink> | |||||
</DJTooltip> | |||||
</li> | </li> | ||||
</ul> | </ul> | ||||
</div> | </div> | ||||
@@ -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 { type RouteRecordRaw, RouterLink, RouterView, useRoute } from "vue-router"; | ||||
import GEMain from "@/generative-energy/GEMain.tsx"; | import GEMain from "@/generative-energy/GEMain.tsx"; | ||||
import DJEmail from "@/DJEmail.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 GEDeutschArticle from "@/generative-energy/GEDeutschArticle.tsx"; | ||||
import GECalculator from "@/generative-energy/GECalculator.tsx"; | import GECalculator from "@/generative-energy/GECalculator.tsx"; | ||||
import DJDonate from "@/DJDonate.tsx"; | import DJDonate from "@/DJDonate.tsx"; | ||||
import { setupTooltip } from "@/DJTooltip.tsx"; | |||||
export const routes: RouteRecordRaw[] = [ | export const routes: RouteRecordRaw[] = [ | ||||
{ | { | ||||
@@ -41,8 +42,11 @@ export default defineComponent({ | |||||
name: "ge-root", | name: "ge-root", | ||||
setup() { | setup() { | ||||
const route = useRoute(); | const route = useRoute(); | ||||
const carrier = ref<HTMLDivElement | null>(null); | |||||
setupTooltip({ carrier }); | |||||
return () => ( | return () => ( | ||||
<> | <> | ||||
<div ref={carrier} class="tooltip-carrier" /> | |||||
<main> | <main> | ||||
<RouterLink class={"home-btn" + (route.name === "GEMain" ? " hide" : "")} to={{ name: "GEMain" }}> | <RouterLink class={"home-btn" + (route.name === "GEMain" ? " hide" : "")} to={{ name: "GEMain" }}> | ||||
Generative Energy Home | Generative Energy Home | ||||
@@ -1,8 +1,10 @@ | |||||
import { createSSRApp } from "vue"; | import { createSSRApp } from "vue"; | ||||
import { createRouter, createWebHistory } from "vue-router"; | import { createRouter, createWebHistory } from "vue-router"; | ||||
import GERoot, { routes } from "@/generative-energy/GERoot.tsx"; | import GERoot, { routes } from "@/generative-energy/GERoot.tsx"; | ||||
import { cssRegistry } from "@/util.ts"; | |||||
createSSRApp(GERoot) | createSSRApp(GERoot) | ||||
.provide(cssRegistry, new Set()) | |||||
.use(createRouter({ | .use(createRouter({ | ||||
routes, | routes, | ||||
history: createWebHistory("/generative-energy"), | history: createWebHistory("/generative-energy"), | ||||
@@ -1,4 +1,7 @@ | |||||
import { createSSRApp } from "vue"; | import { createSSRApp } from "vue"; | ||||
import DJHomeRoot from "@/home/DJHomeRoot.tsx"; | 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"); |
@@ -5,6 +5,7 @@ export type DJSSRContext = { | |||||
title: MaybeRefOrGetter<string>; | title: MaybeRefOrGetter<string>; | ||||
}; | }; | ||||
registry: Record<string, unknown>; | registry: Record<string, unknown>; | ||||
styles: Record<string, string>; | |||||
}; | }; | ||||
export default function useDJSSRContext() { | export default function useDJSSRContext() { | ||||
@@ -1,3 +1,6 @@ | |||||
import { type InjectionKey, inject } from 'vue'; | |||||
import useDJSSRContext from "@/useDJSSRContext.ts"; | |||||
export function gid(id: string, doc: (Document | ShadowRoot) | undefined) { | export function gid(id: string, doc: (Document | ShadowRoot) | undefined) { | ||||
return ((doc ?? document).getElementById(id)); | return ((doc ?? document).getElementById(id)); | ||||
} | } | ||||
@@ -37,9 +40,23 @@ export function css(strs: TemplateStringsArray, ...vals: string[]) { | |||||
for (let i = 1; i < strs.length; i++) { | for (let i = 1; i < strs.length; i++) { | ||||
result += vals[i] + strs[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<Set<string>>; | |||||
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); | |||||
} | |||||
} | |||||
} | } | ||||
/* | /* | ||||
@@ -13,20 +13,23 @@ const utf8Decoder = new TextDecoder("utf-8"); | |||||
const parser = new DOMParser(); | const parser = new DOMParser(); | ||||
function appHeaderScript(params: { appstate: Record<string, unknown>; entryPath: string }) { | |||||
function appHeaderScript(params: { ssrContext: DJSSRContext, entryPath: string }) { | |||||
return `<script type="importmap"> | return `<script type="importmap"> | ||||
{ | { | ||||
"imports": { | "imports": { | ||||
"vue": "/deps/vue/dist/vue.esm-browser.prod.js", | "vue": "/deps/vue/dist/vue.esm-browser.prod.js", | ||||
"vue-router": "/deps/vue-router/dist/vue-router.esm-browser.js", | "vue-router": "/deps/vue-router/dist/vue-router.esm-browser.js", | ||||
"vue/jsx-runtime": "/deps/vue/jsx-runtime/index.mjs", | "vue/jsx-runtime": "/deps/vue/jsx-runtime/index.mjs", | ||||
"@vue/devtools-api": "/deps/@vue/devtools-api/lib/esm/index.js", | |||||
"@vue/devtools-api": "/app/devtools-shim.ts", | |||||
"@/": "/app/" | "@/": "/app/" | ||||
} | } | ||||
} | } | ||||
</script> | </script> | ||||
<style> | |||||
${ Object.values(params.ssrContext.styles).join('\n') } | |||||
</style> | |||||
<script type="module"> | <script type="module"> | ||||
window.appstate = ${JSON.stringify(params.appstate)}; | |||||
window.appstate = ${JSON.stringify(params.ssrContext.registry)}; | |||||
import('${params.entryPath}'); | import('${params.entryPath}'); | ||||
</script>`; | </script>`; | ||||
} | } | ||||
@@ -47,7 +50,7 @@ for await (const path of publicFiles) { | |||||
sites.push(path); | sites.push(path); | ||||
} | } | ||||
function getAPIResponse(apiReq: Request): Response { | |||||
async function getAPIResponse(apiReq: Request): Promise<Response> { | |||||
let jsonResponse: DJAPIResult | { error: string } | null = null; | let jsonResponse: DJAPIResult | { error: string } | null = null; | ||||
let status = 200; | let status = 200; | ||||
@@ -62,22 +65,42 @@ function getAPIResponse(apiReq: Request): Response { | |||||
const apiPath = pathname.split("/api")[1]; | const apiPath = pathname.split("/api")[1]; | ||||
if (apiPath === "/rp-articles") { | 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<HTMLMetaElement>; | |||||
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) { | if (!jsonResponse) { | ||||
jsonResponse = { error: `API route ${ apiPath } not found.` }; | jsonResponse = { error: `API route ${ apiPath } not found.` }; | ||||
status = 404; | status = 404; | ||||
} | } | ||||
} | |||||
const headers = new Headers(); | const headers = new Headers(); | ||||
headers.set("Content-Type", "application/json"); | headers.set("Content-Type", "application/json"); | ||||
return new Response(JSON.stringify(jsonResponse), { | return new Response(JSON.stringify(jsonResponse), { | ||||
@@ -93,13 +116,17 @@ Deno.serve({ | |||||
console.log(`Listening on port http://${hostname}:${port}/`); | console.log(`Listening on port http://${hostname}:${port}/`); | ||||
}, | }, | ||||
}, async (req, _conn) => { | }, async (req, _conn) => { | ||||
const timeStart = new Date().getTime(); | |||||
let response: Response | null = null; | let response: Response | null = null; | ||||
const url = URL.parse(req.url); | |||||
if (req.method === "GET") { | if (req.method === "GET") { | ||||
const pathname = URL.parse(req.url)?.pathname ?? "/"; | |||||
const pathname = url?.pathname ?? "/"; | |||||
if (pathname.startsWith("/api/")) { | if (pathname.startsWith("/api/")) { | ||||
response = getAPIResponse(req); | |||||
response = await getAPIResponse(req); | |||||
} | } | ||||
// Public/static files | // Public/static files | ||||
@@ -144,7 +171,7 @@ Deno.serve({ | |||||
app.provide("dom-parse", (innerHTML: string) => { | app.provide("dom-parse", (innerHTML: string) => { | ||||
return parser.parseFromString(innerHTML, "text/html").documentElement; | return parser.parseFromString(innerHTML, "text/html").documentElement; | ||||
}); | }); | ||||
const ssrContext: DJSSRContext = { registry: {}, head: { title: "" } }; | |||||
const ssrContext: DJSSRContext = { styles: {}, registry: {}, head: { title: "" } }; | |||||
if (router) { | if (router) { | ||||
await router.replace(pathname.split("/generative-energy")[1]); | await router.replace(pathname.split("/generative-energy")[1]); | ||||
await router.isReady(); | await router.isReady(); | ||||
@@ -155,7 +182,7 @@ Deno.serve({ | |||||
.replaceAll("%TITLE%", toValue(ssrContext.head?.title) ?? "Site") | .replaceAll("%TITLE%", toValue(ssrContext.head?.title) ?? "Site") | ||||
.replace( | .replace( | ||||
`<!-- SSR HEAD OUTLET -->`, | `<!-- SSR HEAD OUTLET -->`, | ||||
appHeaderScript({ appstate: ssrContext.registry, entryPath: clientEntry }), | |||||
appHeaderScript({ ssrContext, entryPath: clientEntry }), | |||||
); | ); | ||||
response = new Response(content, { headers: { "Content-Type": "text/html" } }); | response = new Response(content, { headers: { "Content-Type": "text/html" } }); | ||||
} | } | ||||
@@ -164,7 +191,13 @@ Deno.serve({ | |||||
response = new Response("Only GET allowed.", { status: 500 }); | 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", () => { | Deno.addSignalListener("SIGINT", () => { | ||||
@@ -1,3 +1,7 @@ | |||||
<meta name="slug" content="caffeine"> | |||||
<meta name="title" content="Koffein: ein vitamin-ähnlicher Nährstoff, oder Adaptogen"> | |||||
<meta name="tags" content="in-arbeit"> | |||||
<h1 lang="en">Caffeine: A vitamin-like nutrient, or adaptogen</h1> | <h1 lang="en">Caffeine: A vitamin-like nutrient, or adaptogen</h1> | ||||
<h1 lang="de">Koffein: ein vitamin-ähnlicher Nährstoff, oder Adaptogen</h1> | <h1 lang="de">Koffein: ein vitamin-ähnlicher Nährstoff, oder Adaptogen</h1> | ||||
@@ -1,3 +1,7 @@ | |||||
<meta name="title" content="TSH, Temperatur, Puls, und andere Indikatoren bei einer Schilddrüsenunterfunktion"> | |||||
<meta name="slug" content="hypothyroidism"> | |||||
<meta name="tags" content=""> | |||||
<h1 lang="en">TSH, temperature, pulse rate, and other indicators in hypothyroidism</h1> | <h1 lang="en">TSH, temperature, pulse rate, and other indicators in hypothyroidism</h1> | ||||
<h1 lang="de">TSH, Temperatur, Puls, und andere Indikatoren bei einer Schilddrüsenunterfunktion</h1> | <h1 lang="de">TSH, Temperatur, Puls, und andere Indikatoren bei einer Schilddrüsenunterfunktion</h1> | ||||
@@ -14,6 +14,11 @@ h1, h2, h3, h4, h5 { | |||||
font-family: "Roboto Slab", "Segoe UI", Tahoma, Geneva, Verdana, sans-serif; | font-family: "Roboto Slab", "Segoe UI", Tahoma, Geneva, Verdana, sans-serif; | ||||
} | } | ||||
.baustelle { | |||||
text-align: center; | |||||
display: block; | |||||
} | |||||
a.home-btn { | a.home-btn { | ||||
display: block; | display: block; | ||||
text-align: center; | text-align: center; | ||||
@@ -23,6 +28,15 @@ a.home-btn { | |||||
} | } | ||||
} | } | ||||
span.tag { | |||||
margin-left: 4px; | |||||
font-style: italic; | |||||
&::before { | |||||
content: '#'; | |||||
} | |||||
} | |||||
.ge-article { | .ge-article { | ||||
.header { | .header { | ||||
margin-top: 20px; | margin-top: 20px; | ||||
@@ -78,28 +92,28 @@ a.home-btn { | |||||
display: none; | display: none; | ||||
} | } | ||||
.lang-en *:lang(de) { | |||||
.lang-en span:lang(de) { | |||||
display: none; | display: none; | ||||
} | } | ||||
.lang-de *:lang(en) { | |||||
.lang-de span:lang(en) { | |||||
display: none; | display: none; | ||||
} | } | ||||
} | } | ||||
main { | main { | ||||
width: 100%; | |||||
width: calc(100% - 20px); | |||||
margin: auto; | |||||
} | } | ||||
@media (min-width: 600px) { | |||||
@media (min-width: 900px) { | |||||
main { | main { | ||||
max-width: 1340px; | |||||
margin: auto; | |||||
max-width: 943px; | |||||
min-width: 600px; | min-width: 600px; | ||||
width: 60%; | |||||
} | } | ||||
} | } | ||||
header { | header { | ||||
margin: auto; | margin: auto; | ||||
text-align: center; | text-align: center; | ||||
@@ -174,22 +188,45 @@ footer { | |||||
input { | input { | ||||
width: 70px; | width: 70px; | ||||
} | } | ||||
tr:last-of-type td { | tr:last-of-type td { | ||||
border-bottom: none; | border-bottom: none; | ||||
} | } | ||||
td { | td { | ||||
border-bottom: 1px solid var(--text-color); | border-bottom: 1px solid var(--text-color); | ||||
border-right: 1px solid var(--text-color); | border-right: 1px solid var(--text-color); | ||||
padding: 10px; | padding: 10px; | ||||
} | } | ||||
td:last-of-type { | td:last-of-type { | ||||
border-right: none; | border-right: none; | ||||
} | } | ||||
td.right { | |||||
text-align: center; | |||||
} | |||||
.breathe { | .breathe { | ||||
padding-left: 4px; | padding-left: 4px; | ||||
padding-right: 4px; | padding-right: 4px; | ||||
} | } | ||||
.separator::before { | |||||
content: '—'; | |||||
} | |||||
@media (min-width: 600px) { | @media (min-width: 600px) { | ||||
.separator::before { | |||||
content: ':'; | |||||
padding-left: 4px; | |||||
padding-right: 4px; | |||||
} | |||||
td.ratio { | |||||
text-align: left; | |||||
} | |||||
td.right div { | td.right div { | ||||
display: inline-block; | display: inline-block; | ||||
} | } | ||||
@@ -183,35 +183,3 @@ a { | |||||
text-decoration: none; | text-decoration: none; | ||||
cursor: pointer; | 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; | |||||
} | |||||
} | |||||