Fetch track thumbnails remotely
@@ -3,17 +3,17 @@ import Logo from "../content/TH.svg";
|
|||||||
import Icon from "./Icon.astro";
|
import Icon from "./Icon.astro";
|
||||||
import Button from "./Button.astro";
|
import Button from "./Button.astro";
|
||||||
|
|
||||||
const routes = ["blog", "tracks"];
|
const routes = ["blog", "tracks", "services"];
|
||||||
---
|
---
|
||||||
|
|
||||||
<nav class="sticky top-0 z-10 max-w-none bg-white dark:bg-neutral-800">
|
<nav class="sticky top-0 z-10 max-w-none bg-white dark:bg-neutral-800">
|
||||||
<div
|
<div
|
||||||
class="mx-auto flex max-w-screen-xl items-center justify-between ps-4 pe-2 text-neutral-700 dark:text-neutral-300"
|
class="mx-auto flex max-w-screen-xl items-center justify-between gap-4 ps-4 pe-2 text-neutral-700 dark:text-neutral-300"
|
||||||
>
|
>
|
||||||
<a href="/" title="Home">
|
<a href="/" title="Home">
|
||||||
<Logo width={42} height={42} />
|
<Logo width={42} height={42} />
|
||||||
</a>
|
</a>
|
||||||
<div class="flex">
|
<div class="flex overflow-x-auto">
|
||||||
{
|
{
|
||||||
routes.map((route) => (
|
routes.map((route) => (
|
||||||
<Button href={`/${route}`}>
|
<Button href={`/${route}`}>
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ const { headings } = Astro.props;
|
|||||||
|
|
||||||
<details
|
<details
|
||||||
id="toc"
|
id="toc"
|
||||||
class="sticky top-10 z-10 -translate-y-px border border-neutral-300 bg-white lg:top-2 dark:border-neutral-600 dark:bg-neutral-800"
|
class="sticky top-10 z-10 -translate-y-px border border-neutral-300 bg-white xl:top-2 dark:border-neutral-600 dark:bg-neutral-800"
|
||||||
>
|
>
|
||||||
<summary
|
<summary
|
||||||
title="Table of contents"
|
title="Table of contents"
|
||||||
|
|||||||
@@ -1,28 +1,46 @@
|
|||||||
---
|
---
|
||||||
import { Image } from "astro:assets";
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
title: string;
|
title: string;
|
||||||
artist: string;
|
artist: string;
|
||||||
album: string;
|
album: string;
|
||||||
youtubeLink: string;
|
youtubeLink: string;
|
||||||
cover: any;
|
index: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { title, artist, album, youtubeLink, cover } = Astro.props;
|
const { title, artist, album, youtubeLink, index } = Astro.props;
|
||||||
|
|
||||||
|
const uniqueArtists = [...new Set(artist.split(",").map((a) => a.trim()))].join(
|
||||||
|
", ",
|
||||||
|
);
|
||||||
|
|
||||||
|
const videoId = youtubeLink.split("v=")[1];
|
||||||
|
const thumbnail = `https://img.youtube.com/vi/${videoId}/maxresdefault.jpg`;
|
||||||
---
|
---
|
||||||
|
|
||||||
<figure>
|
<figure
|
||||||
|
class="relative flex flex-col border border-neutral-400 duration-300 hover:scale-105 active:scale-105 dark:border-neutral-500"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="absolute -start-2 -top-2 border border-neutral-400 bg-white px-2 text-lg font-bold dark:border-neutral-500 dark:bg-neutral-800"
|
||||||
|
>{index}</span
|
||||||
|
>
|
||||||
<a href={youtubeLink}>
|
<a href={youtubeLink}>
|
||||||
<Image
|
<img
|
||||||
src={cover}
|
src={thumbnail}
|
||||||
alt={`Cover for the song '${title}' by artist(s) '${artist}'`}
|
alt={`Cover for the song '${title}' by artist(s) '${artist}'`}
|
||||||
class="border border-neutral-400 duration-300 hover:scale-105 active:scale-105 dark:border-neutral-500"
|
class="aspect-video w-full border-b border-neutral-400 dark:border-neutral-500"
|
||||||
/>
|
/>
|
||||||
</a>
|
</a>
|
||||||
<figcaption class="flex flex-col p-4 text-center">
|
<figcaption
|
||||||
|
class="flex flex-1 flex-col p-6 text-center"
|
||||||
|
style="word-break: break-word;"
|
||||||
|
>
|
||||||
<p class="text-lg font-bold">{title}</p>
|
<p class="text-lg font-bold">{title}</p>
|
||||||
<p>{artist}</p>
|
<p class="mb-3">{uniqueArtists}</p>
|
||||||
<p class="text-sm">{album}</p>
|
<p
|
||||||
|
class="mt-auto border-t border-neutral-400 pt-3 text-sm dark:border-neutral-500"
|
||||||
|
>
|
||||||
|
{album}
|
||||||
|
</p>
|
||||||
</figcaption>
|
</figcaption>
|
||||||
</figure>
|
</figure>
|
||||||
|
|||||||
@@ -13,14 +13,12 @@ const blog = defineCollection({
|
|||||||
|
|
||||||
const tracks = defineCollection({
|
const tracks = defineCollection({
|
||||||
loader: file("./src/content/tracks.json"),
|
loader: file("./src/content/tracks.json"),
|
||||||
schema: ({ image }) =>
|
schema: z.object({
|
||||||
z.object({
|
|
||||||
id: z.number().positive(),
|
id: z.number().positive(),
|
||||||
title: z.string(),
|
title: z.string(),
|
||||||
youtubeLink: z.string().url(),
|
youtubeLink: z.string().url(),
|
||||||
artist: z.string(),
|
artist: z.string(),
|
||||||
album: z.string(),
|
album: z.string(),
|
||||||
cover: image(),
|
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 68 KiB |
|
Before Width: | Height: | Size: 39 KiB |
|
Before Width: | Height: | Size: 41 KiB |
|
Before Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 44 KiB |
|
Before Width: | Height: | Size: 81 KiB |
|
Before Width: | Height: | Size: 55 KiB |
|
Before Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 48 KiB |
|
Before Width: | Height: | Size: 49 KiB |
|
Before Width: | Height: | Size: 57 KiB |
|
Before Width: | Height: | Size: 54 KiB |
|
Before Width: | Height: | Size: 68 KiB |
|
Before Width: | Height: | Size: 96 KiB |
|
Before Width: | Height: | Size: 50 KiB |
|
Before Width: | Height: | Size: 21 KiB |
|
Before Width: | Height: | Size: 41 KiB |
|
Before Width: | Height: | Size: 42 KiB |
|
Before Width: | Height: | Size: 70 KiB |
|
Before Width: | Height: | Size: 22 KiB |
@@ -245,4 +245,3 @@ public class ExampleTest extends BaseTest {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -17,9 +17,9 @@ I am a software developer from Germany who is passionate about building high qua
|
|||||||
|
|
||||||
## Software
|
## Software
|
||||||
|
|
||||||
- [NixOS](https://nixos.org)
|
- [Arch Linux](https://archlinux.org)
|
||||||
- [GNOME](https://www.gnome.org)
|
- [KDE Plasma](https://kde.org/plasma-desktop)
|
||||||
- [VSCodium](https://vscodium.com)
|
- [Visual Studio Code](https://code.visualstudio.com)
|
||||||
- [Firefox Developer Edition](https://www.mozilla.org/en-US/firefox/developer)
|
- [Firefox Developer Edition](https://www.mozilla.org/en-US/firefox/developer)
|
||||||
- [Tuta Mail](https://tuta.com)
|
- [Tuta Mail](https://tuta.com)
|
||||||
- [Mullvad VPN](https://mullvad.net)
|
- [Mullvad VPN](https://mullvad.net)
|
||||||
|
|||||||
7
src/content/services.md
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
This is an overview of popular services that are self-hosted and provided via my Hetzner NixOS VPS. The goal is to provide everyone with a central place where they can reliably access most of them.
|
||||||
|
|
||||||
|
## List
|
||||||
|
|
||||||
|
| Service | URL | Description |
|
||||||
|
| ------- | ----------------------------- | ---------------------------- |
|
||||||
|
| Redlib | https://redlib.thilohohlt.com | Private front-end for Reddit |
|
||||||
@@ -25,12 +25,14 @@ export const GET: APIRoute = async (context) => {
|
|||||||
<lastBuildDate>${latestModDate.toUTCString()}</lastBuildDate>
|
<lastBuildDate>${latestModDate.toUTCString()}</lastBuildDate>
|
||||||
<atom:link href="${context.url.origin}/rss.xml" rel="self" type="application/rss+xml" />
|
<atom:link href="${context.url.origin}/rss.xml" rel="self" type="application/rss+xml" />
|
||||||
`,
|
`,
|
||||||
items: blog.map(({ id, body, data }) => ({
|
items: blog
|
||||||
|
.map(({ id, body, data }) => ({
|
||||||
link: `/blog/${id}/`,
|
link: `/blog/${id}/`,
|
||||||
content: sanitizeHtml(parser.render(body ?? ""), {
|
content: sanitizeHtml(parser.render(body ?? ""), {
|
||||||
allowedTags: sanitizeHtml.defaults.allowedTags.concat(["img"]),
|
allowedTags: sanitizeHtml.defaults.allowedTags.concat(["img"]),
|
||||||
}),
|
}),
|
||||||
...data,
|
...data,
|
||||||
})),
|
}))
|
||||||
|
.sort((a, b) => b.pubDate.getTime() - a.pubDate.getTime()),
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
11
src/pages/services.astro
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
---
|
||||||
|
import PageLayout from "../layouts/PageLayout.astro";
|
||||||
|
import { Content } from "../content/services.md";
|
||||||
|
---
|
||||||
|
|
||||||
|
<PageLayout
|
||||||
|
title="Services"
|
||||||
|
description="Self-hosted instances of popular services provided via my Hetzner NixOS VPS."
|
||||||
|
>
|
||||||
|
<Content />
|
||||||
|
</PageLayout>
|
||||||
@@ -8,19 +8,21 @@ const tracks = await getCollection("tracks");
|
|||||||
|
|
||||||
<PageLayout
|
<PageLayout
|
||||||
title="Tracks"
|
title="Tracks"
|
||||||
description="Collection of some of my favourite music tracks."
|
description="My entire music playlist. It contains all kinds of songs."
|
||||||
>
|
>
|
||||||
<p class="mb-8">
|
<p class="mb-8 text-center">
|
||||||
This is a collection of some of my favourite music tracks, each listed by
|
My entire music playlist. It contains all kinds of songs. <br />
|
||||||
artist and song title. Click on an album cover to go straight to the song on
|
Current total amount of songs: <strong class="text-lg"
|
||||||
YouTube!
|
>{tracks.length}</strong
|
||||||
|
>
|
||||||
|
<br />
|
||||||
</p>
|
</p>
|
||||||
<div
|
<div
|
||||||
class="not-prose relative start-1/2 -ms-[min(50vw-1rem,50ch)] grid max-w-[calc(min(100vw-2rem,100ch))] grid-cols-[repeat(auto-fit,minmax(min(100%,200px),1fr))] place-content-center gap-4"
|
class="not-prose relative start-1/2 -ms-[min(50vw-1rem,50ch)] grid max-w-[calc(min(100vw-2rem,100ch))] grid-cols-[repeat(auto-fit,minmax(min(100%,200px),1fr))] place-content-center gap-6"
|
||||||
>
|
>
|
||||||
{
|
{
|
||||||
tracks.map(({ data: { title, artist, album, youtubeLink, cover } }) => (
|
tracks.map(({ data: { title, artist, album, youtubeLink } }, index) => (
|
||||||
<Track {title} {artist} {album} {youtubeLink} {cover} />
|
<Track {title} {artist} {album} {youtubeLink} index={++index} />
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||