big style update

This commit is contained in:
Daniel Ledda
2025-12-20 21:16:00 +01:00
parent 51e44db779
commit 498cb37561
17 changed files with 562 additions and 322 deletions

View File

@@ -17,9 +17,10 @@ const tooltipStyles = css`
opacity: 0;
display: block;
pointer-events: none;
background-color: black;
border: white solid 1px;
color: white;
background-color: var(--dj-bgpalette1);
box-shadow: 0 0 12px 1px rgb(10 12 15 / 70%);
border: var(--dj-palette3) solid 1px;
color: var(--dj-palette3);
padding: 10px;
position: absolute;
z-index: 1;
@@ -119,7 +120,7 @@ export default defineComponent({
return () => <>
<div class="tooltip-container"
{...attrs}
{...attrs}
onMouseenter={(e) => tooltip.show(props.tooltip, e.pageX, e.pageY)}
onMouseleave={() => tooltip.hide()}>
{slots.default?.()}

View File

@@ -1,4 +1,15 @@
export type DjAPIEndpoint = "/rp-articles";
export type DjAPIEndpoint =
| "/rp-articles"
| "/blog-entries"
;
type BlogEntry = {
title: string,
slug: string;
createdAt: string,
updatedAt: string,
tags?: string[],
};
type RPArticle = {
title: string,
@@ -11,6 +22,7 @@ type RPArticle = {
export interface DjAPIResultMap extends Record<DjAPIEndpoint, unknown> {
"/rp-articles": RPArticle[];
"/blog-entries": BlogEntry[];
}
export type DjAPIResult = DjAPIResultMap[DjAPIEndpoint];

98
app/blog/DjBlogEntry.tsx Normal file
View File

@@ -0,0 +1,98 @@
import { computed, createTextVNode, defineComponent, h, type VNode } from "vue";
import useHead from "@/useHead.ts";
import useAsyncState from "@/useAsyncState.ts";
import getDjAPI from "@/api.ts";
import getDomParser from "@/domParse.ts";
import { addCSS, css } from "../util.ts";
const style = css`
.byline {
font-style: italic;
color: gray;
}
h1 {
color: var(--dj-palette1);
}
p {
margin-bottom: 30px;
}
`;
export default defineComponent({
name: "DjBlogEntry",
props: {
slug: {
type: String,
required: true,
},
},
async setup(props) {
addCSS('DjBlogEntry', style);
const parseDom = getDomParser();
const blogpostContent = useAsyncState(
`dj-blog-article-content-${ props.slug }`,
async ({ hostUrl }) => {
const blogpostResponse = await fetch(`${hostUrl}/blog/content/${ props.slug }.html`);
const result = await blogpostResponse.text();
return result;
},
);
const blogpostsMetadata = useAsyncState('article-metadata', ({ hostUrl }) => getDjAPI(hostUrl, '/blog-entries'));
const blogpostMetadata = computed(() => blogpostsMetadata.result.value?.find(_ => _.slug === props.slug));
useHead({
title: () => blogpostMetadata.value?.title ?? '',
metatags: () => blogpostMetadata.value ? [
{ name: 'title', content: blogpostMetadata.value.title },
{ name: 'author', content: 'Daniel Ledda' },
] : [],
});
function transformPostNode(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((_) => transformPostNode(_));
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 PostContentTransformed() {
if (blogpostContent.result.value) {
const dom = parseDom(blogpostContent.result.value);
return h("div", {}, [...dom.children].map((_) => transformPostNode(_)));
}
return <div>Blog post loading...</div>;
}
await Promise.allSettled([ blogpostContent.done, blogpostsMetadata.done ]);
return () => <>
{ blogpostMetadata.value
? <>
<h1>{ blogpostMetadata.value.title }</h1>
<div class="byline">by Daniel Ledda, first published { new Date(blogpostMetadata.value.createdAt).toLocaleDateString() }</div>
<PostContentTransformed />
</>
: "Sorry, this blog post doesn't seem to exist."
}
</>;
}
});

42
app/blog/DjBlogMain.tsx Normal file
View File

@@ -0,0 +1,42 @@
import { defineComponent } from "vue";
import useAsyncState from "@/useAsyncState.ts";
import getDjAPI from "@/api.ts";
import { RouterLink } from "vue-router";
import { addCSS, css } from "@/util.ts";
const style = css`
.entry {
display: flex;
flex-direction: row;
gap: 4px;
}
`;
export default defineComponent({
name: "DjBlogMain",
async setup() {
addCSS('DjBlogMain', style);
const blogEntries = useAsyncState('blog-entries-meta', ({ hostUrl }) => getDjAPI(hostUrl, "/blog-entries"));
await blogEntries.done;
return () => <>
<main>
<h2>Entries</h2>
<ul>
{blogEntries.result.value?.map(_ => (
<li key={_.slug}>
<div class="entry">
<RouterLink to={{ name: 'DjBlogEntry', params: { slug: _.slug }}}>{ _.title }</RouterLink>
<span>-</span>
<time datetime={ _.createdAt }>{ new Date(_.createdAt).toLocaleDateString() }</time>
</div>
</li>
)) ?? <li>Blog posts loading...</li>}
</ul>
</main>
</>;
},
});

View File

@@ -1,7 +1,31 @@
import { defineComponent, ref } from "vue";
import useHead from "@/useHead.ts";
import { defineComponent, ref, type VNode, Suspense } from "vue";
import { type RouteRecordRaw, RouterLink, RouterView } from "vue-router";
import DjTooltip, { setupTooltip } from "@/DjTooltip.tsx";
import DjBlogEntry from "@/blog/DjBlogEntry.tsx";
import DjBlogMain from "@/blog/DjBlogMain.tsx";
import DjEmail from "@/DjEmail.tsx";
import { addCSS, css } from "@/util.ts";
import useHead from "@/useHead.ts";
export const routes: RouteRecordRaw[] = [
{
path: "/",
name: "DjBlogMain",
component: DjBlogMain,
},
{
path: "/post/:slug",
name: "DjBlogEntry",
component: DjBlogEntry,
props: ({ params }) => {
if ("slug" in params) {
return { slug: params.slug };
} else {
return false;
}
},
},
];
const styles = css`
.supercontainer {
@@ -9,31 +33,85 @@ const styles = css`
display: flex;
flex-direction: column;
align-items: center;
margin-bottom: 100px;
}
.container {
width: 800px;
}
footer {
color: gray;
font-style: italic;
margin-top: 40px;
}
a {
color: var(--dj-palette3);
text-decoration: solid line;
&:visited {
color: var(--dj-visited);
}
}
nav {
font-size: 40px;
margin-bottom: 40px;
text-decoration: none;
text-align: right;
width: 100%;
a, a:visited {
color: var(--dj-palette3);
}
}
`;
export default defineComponent({
name: "app-root",
name: "DjBlogRoot",
setup() {
addCSS('dj-blog-root', styles);
const carrier = ref<HTMLDivElement | null>(null);
setupTooltip({ carrier });
addCSS('dj-blog-root', styles);
useHead({ title: "djblog Home" });
const tooltipCarrier = ref<HTMLDivElement | null>(null);
setupTooltip({ carrier: tooltipCarrier });
return () => <>
<div ref={tooltipCarrier} class="tooltip-carrier" />
<div class="supercontainer">
<div class="container">
<DjTooltip tooltip="come in and find out...">
<h1>dj blog</h1>
</DjTooltip>
return () => (
<>
<div ref={carrier} class="tooltip-carrier" />
<div class="supercontainer">
<div class="container">
<nav>
<DjTooltip tooltip="flog, clog, bog, frog, cog, log, grog, fog, snog...">
<RouterLink to={{ name: 'DjBlogMain' }}>
dj blog
</RouterLink>
</DjTooltip>
</nav>
<RouterView>
{{
default: ({ Component }: { Component: VNode }) => (Component &&
(
<Suspense>
{{
default: () => Component,
fallback: () => <div>Page loading...</div>,
}}
</Suspense>
)),
}}
</RouterView>
<footer>
<div class="bottom">
<div>
<a href="/">djledda.net</a> {new Date().getFullYear()} - <DjEmail>{() => "Contact"}</DjEmail>
</div>
</div>
</footer>
</div>
</div>
</div>
</>;
</>
);
},
});

View File

@@ -1,7 +1,12 @@
import { createSSRApp } from "vue";
import DjBlogRoot from "@/blog//DjBlogRoot.tsx";
import { createRouter, createWebHistory } from "vue-router";
import DjBlogRoot, { routes } from "@/blog//DjBlogRoot.tsx";
import { cssRegistry } from "@/util.ts";
createSSRApp(DjBlogRoot)
.provide(cssRegistry, new Set())
.use(createRouter({
routes,
history: createWebHistory("/blog"),
}))
.mount("#app-root");

View File

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

8
app/domParse.ts Normal file
View File

@@ -0,0 +1,8 @@
import { inject } from "vue";
export default function getDomParser() {
return inject(
"dom-parse",
(innerHTML: string) => Object.assign(document.createElement("div"), { innerHTML }),
);
}

View File

@@ -96,7 +96,7 @@ export default defineComponent({
return <div>Artikel lädt...</div>;
}
await Promise.all([ articleContent.done, articlesMetadata.done ]);
await Promise.allSettled([ articleContent.done, articlesMetadata.done ]);
return () => (
<div class="ge-article">

View File

@@ -1,63 +1,88 @@
import { defineComponent, computed, ref, type Ref } from "vue";
import { defineComponent, ref, type Ref } from "vue";
import useHead from "@/useHead.ts";
import DjTooltip, { setupTooltip } from "@/DjTooltip.tsx";
import DjEmail from "@/DjEmail.tsx";
import { addCSS, css } from "@/util.ts";
const styles = css`
:root {
--subject-spacing: 40px;
}
.resource {
margin-bottom: 10px;
}
.title_name {
font-size: 48px;
color: var(--dj-palette3);
text-align: center;
}
.supercontainer {
padding-top: 3em;
margin: auto;
}
.main {
width: 50em;
margin: 20px auto;
text-align: center;
}
@media only screen and (max-width: 1024px) {
.main {
width: 35em;
padding: 20px;
}
:root {
--subject-spacing: 20px;
}
}
@media only screen and (max-width: 768px) {
.title_name {
font-size: 30px;
}
.main {
width: 20em;
padding: 20px;
}
}
a {
color: var(--dj-palette3);
&:visited {
color: var(--dj-visited);
}
}
`;
export default defineComponent({
name: "app-root",
name: "DjHomeRoot",
setup() {
useHead({ title: "DJ Ledda's Homepage" });
addCSS('DjHomeRoot', styles);
useHead({ title: "djledda" });
const tooltipCarrier = ref<HTMLDivElement | null>(null);
setupTooltip({ carrier: tooltipCarrier });
const dude1Spinning = ref(false);
const dude2Spinning = ref(false);
function toggleDude(event: MouseEvent, dudeRef: Ref<boolean>) {
const dude = event.target as HTMLImageElement;
if (dudeRef.value) {
dude.addEventListener("animationiteration", function listener() {
dudeRef.value = false;
dude.removeEventListener("animationiteration", listener as EventListenerOrEventListenerObject);
});
} else {
dudeRef.value = true;
}
}
const shaking = computed(() => dude1Spinning.value || dude2Spinning.value);
return () => <>
<div ref={tooltipCarrier} class="tooltip-carrier" />
<div class="supercontainer">
<div class={{ shakeable: true, shakeMe: shaking.value }}>
<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: true, spinMe: dude1Spinning.value }}
onClick={ (e) => toggleDude(e, dude1Spinning)} />
</DjTooltip>
<div>
<div class="dj-title title_name">
<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: true, spinMe: dude2Spinning.value }}
onClick={ (e) => toggleDude(e, dude2Spinning) } />
<span>dj ledda</span>
</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/index.html">
<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
<a href="/blog">
<DjTooltip class="resource" tooltip="My musings, my losings, my winnings, my thoughts">
Blog
</DjTooltip>
</a>
<a href="/generative-energy">
@@ -65,6 +90,11 @@ export default defineComponent({
Generative Energy - Ray Peat Resources
</DjTooltip>
</a>
<a href="https://git.djledda.net/Ledda">
<DjTooltip class="resource" tooltip="Check out what I'm coding!">
My git projects
</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
@@ -74,6 +104,16 @@ export default defineComponent({
M&uuml;nchen auf Englisch - Munich in English
</DjTooltip>
</a>
<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/index.html">
<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="/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
@@ -81,11 +121,6 @@ export default defineComponent({
K A D I: Online Yatzy Scoresheets
</DjTooltip>
</a>
<a href="https://git.djledda.net/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
@@ -94,7 +129,6 @@ export default defineComponent({
</div>
</div>
</div>
<div id="tooltipCarrier"></div>
</div>
</div>
</>;