7 Commits

Author SHA1 Message Date
thiloho
ce351183a6 Fix tracks loading script 2025-12-29 20:25:55 +01:00
thiloho
90d8ec0a2c Fix tracks loading script 2025-12-29 20:21:19 +01:00
thiloho
2f7392e7ff Update tracks 2025-12-29 20:11:26 +01:00
thiloho
abba678dd6 Update flake 2025-12-29 20:06:05 +01:00
thiloho
bfad268f1f Fix height 2025-12-10 22:46:16 +01:00
thiloho
70a0434236 Update tracks list 2025-12-10 22:41:35 +01:00
thiloho
323ec46753 Add loading spinner for music collection 2025-12-10 22:10:03 +01:00
10 changed files with 1537 additions and 826 deletions

42
flake.lock generated
View File

@@ -1,45 +1,12 @@
{
"nodes": {
"home-manager": {
"inputs": {
"nixpkgs": "nixpkgs"
},
"locked": {
"lastModified": 1763228015,
"narHash": "sha256-1rYieMVUyZ3kK/cBIr8mOusxrOEJ1/+2MsOg0oJ7b3A=",
"owner": "nix-community",
"repo": "home-manager",
"rev": "96156a9e86281c4bfc451236bc2ddfe4317e6f39",
"type": "github"
},
"original": {
"id": "home-manager",
"type": "indirect"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1762111121,
"narHash": "sha256-4vhDuZ7OZaZmKKrnDpxLZZpGIJvAeMtK6FKLJYUtAdw=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "b3d51a0365f6695e7dd5cdf3e180604530ed33b4",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixos-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"nixpkgs_2": {
"locked": {
"lastModified": 1762977756,
"narHash": "sha256-4PqRErxfe+2toFJFgcRKZ0UI9NSIOJa+7RXVtBhy4KE=",
"lastModified": 1766070988,
"narHash": "sha256-G/WVghka6c4bAzMhTwT2vjLccg/awmHkdKSd2JrycLc=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "c5ae371f1a6a7fd27823bc500d9390b38c05fa55",
"rev": "c6245e83d836d0433170a16eb185cefe0572f8b8",
"type": "github"
},
"original": {
@@ -51,8 +18,7 @@
},
"root": {
"inputs": {
"home-manager": "home-manager",
"nixpkgs": "nixpkgs_2"
"nixpkgs": "nixpkgs"
}
}
},

View File

@@ -4,7 +4,7 @@
inputs.nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
outputs =
inputs@{ nixpkgs, home-manager, ... }:
inputs@{ nixpkgs, ... }:
let
systems = [
"x86_64-linux"

8
package-lock.json generated
View File

@@ -4582,6 +4582,7 @@
"integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==",
"dev": true,
"license": "MIT",
"peer": true,
"bin": {
"prettier": "bin/prettier.cjs"
},
@@ -4598,6 +4599,7 @@
"integrity": "sha512-RiBETaaP9veVstE4vUwSIcdATj6dKmXljouXc/DDNwBSPTp8FRkLGDSGFClKsAFeeg+13SB0Z1JZvbD76bigJw==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@astrojs/compiler": "^2.9.1",
"prettier": "^3.0.0",
@@ -5028,6 +5030,7 @@
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.40.0.tgz",
"integrity": "sha512-Noe455xmA96nnqH5piFtLobsGbCij7Tu+tb3c1vYjNbTkfzGqXqQXG3wJaYXkRZuQ0vEYN4bhwg7QnIrqB5B+w==",
"license": "MIT",
"peer": true,
"dependencies": {
"@types/estree": "1.0.7"
},
@@ -5322,7 +5325,8 @@
"version": "4.1.14",
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.14.tgz",
"integrity": "sha512-b7pCxjGO98LnxVkKjaZSDeNuljC4ueKUddjENJOADtubtdo8llTaJy7HwBMeLNSSo2N5QIAgklslK1+Ir8r6CA==",
"license": "MIT"
"license": "MIT",
"peer": true
},
"node_modules/tapable": {
"version": "2.3.0",
@@ -5806,6 +5810,7 @@
"resolved": "https://registry.npmjs.org/vite/-/vite-6.4.1.tgz",
"integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==",
"license": "MIT",
"peer": true,
"dependencies": {
"esbuild": "^0.25.0",
"fdir": "^6.4.4",
@@ -6013,6 +6018,7 @@
"resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz",
"integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==",
"license": "MIT",
"peer": true,
"funding": {
"url": "https://github.com/sponsors/colinhacks"
}

View File

@@ -9,7 +9,7 @@ interface Props {
const { href, variant = "text", title, id } = Astro.props;
const baseClasses =
"border-transparent border-b-2 p-2 cursor-pointer hover:border-[var(--color-accent-cyan)] hover:bg-neutral-200 hover:dark:border-[var(--color-accent-cyan-light)] hover:dark:bg-neutral-700 active:bg-neutral-200 active:dark:bg-neutral-700 active:border-[var(--color-accent-cyan)] active:dark:border-[var(--color-accent-cyan-light)] transition-colors duration-200";
"border-transparent border-b-2 p-2 cursor-pointer hover:border-neutral-300 hover:bg-neutral-200 hover:dark:border-neutral-600 hover:dark:bg-neutral-700 active:bg-neutral-200 active:dark:bg-neutral-700 active:border-neutral-300 active:dark:border-neutral-600";
const classes = `${baseClasses} ${variant === "icon" && href ? "inline-grid place-content-center" : "inline-block"}`;
---

View File

@@ -4,7 +4,6 @@ import Icon from "./Icon.astro";
import Button from "./Button.astro";
const routes = ["blog", "tracks", "services"];
const currentPath = Astro.url.pathname;
---
<nav class="sticky top-0 z-20 max-w-none bg-white dark:bg-neutral-800">
@@ -16,32 +15,18 @@ const currentPath = Astro.url.pathname;
</a>
<div class="flex overflow-x-auto">
{
routes.map((route) => {
const isActive = currentPath.startsWith(`/${route}`);
return (
<span class="relative">
<Button href={`/${route}`}>
{route
.split(" ")
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
.join(" ")}
</Button>
{isActive && (
<span class="absolute right-0 bottom-0 left-0 h-0.5 bg-gradient-to-r from-[var(--color-accent-blue)] to-[var(--color-accent-cyan)] dark:from-[var(--color-accent-blue-light)] dark:to-[var(--color-accent-cyan-light)]" />
)}
</span>
);
})
routes.map((route) => (
<Button href={`/${route}`}>
{route
.split(" ")
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
.join(" ")}
</Button>
))
}
<Button id="theme-toggle" variant="icon" title="Toggle dark mode">
<Icon
name="moon"
class="transition-colors duration-200 hover:text-[var(--color-accent-cyan)] dark:hidden"
/>
<Icon
name="sun"
class="hidden transition-colors duration-200 hover:text-[var(--color-accent-cyan-light)] dark:block"
/>
<Icon name="moon" class="dark:hidden" />
<Icon name="sun" class="hidden dark:block" />
</Button>
<Button variant="icon" href="/rss.xml" title="RSS feed">
<Icon name="rss" />

View File

@@ -19,8 +19,8 @@ const thumbnail = `https://img.youtube.com/vi/${videoId}/maxresdefault.jpg`;
<a
href={youtubeLink}
class="relative mt-4 block p-4 duration-300 after:absolute after:inset-0 after:z-0 after:bg-[rgba(255,255,255,0.75)] after:content-[''] first:mt-0 hover:scale-105 dark:after:bg-[rgba(38,38,38,0.75)]"
style={`word-break: break-word; background-image: url('${thumbnail}'); background-size: cover; background-position: center;`}
class="relative mt-4 block bg-cover bg-center p-4 duration-300 after:absolute after:inset-0 after:z-0 after:bg-[rgba(255,255,255,0.75)] after:content-[''] first:mt-0 hover:scale-105 dark:after:bg-[rgba(38,38,38,0.75)]"
style={`word-break: break-word; background-image: url('${thumbnail}')`}
>
<div
class="relative z-10 flex flex-col gap-2 text-neutral-900 dark:text-white"

File diff suppressed because it is too large Load Diff

View File

@@ -22,7 +22,7 @@ const { title, description, pubDate, modDate, slug } = Astro.props;
<Header {title} {pubDate} {modDate} {slug} />
<main class="flex-1 bg-white dark:bg-neutral-800">
<div
class={`relative prose prose-neutral dark:prose-invert mx-auto px-4 ${pubDate ? "pt-0" : "pt-8"} pb-16 ${Astro.originPathname === "/" ? "prose-headings:scroll-mt-12" : "prose-headings:scroll-mt-24 lg:prose-headings:scroll-mt-16"} prose-table:text-base prose-h1:font-bold prose-pre:!bg-neutral-700 prose-a:font-normal prose-a:not-in-prose-headings:text-blue-800 prose-a:not-in-prose-headings:dark:text-blue-300 prose-a:hover:no-underline prose-a:active:bg-neutral-200 prose-a:active:dark:bg-neutral-700 prose-a:in-prose-headings:font-bold prose-a:in-prose-headings:decoration-2 prose-a:in-prose-headings:no-underline prose-a:in-prose-headings:hover:underline`}
class={`relative prose prose-neutral dark:prose-invert mx-auto px-4 ${pubDate ? "pt-0" : "pt-8"} pb-16 ${Astro.originPathname === "/" ? "prose-headings:scroll-mt-12" : "prose-headings:scroll-mt-24 lg:prose-headings:scroll-mt-16"} prose-table:text-base prose-h1:font-bold prose-pre:!bg-neutral-700 prose-a:font-normal prose-table:m-0 prose-a:not-in-prose-headings:text-blue-800 prose-a:not-in-prose-headings:dark:text-blue-300 prose-a:hover:no-underline prose-a:active:bg-neutral-200 prose-a:active:dark:bg-neutral-700 prose-a:in-prose-headings:font-bold prose-a:in-prose-headings:decoration-2 prose-a:in-prose-headings:no-underline prose-a:in-prose-headings:hover:underline`}
>
<slot />
</div>

View File

@@ -7,10 +7,20 @@ const tracks = await getCollection("tracks");
---
<PageLayout
title="Tracks"
title={`Tracks (${tracks.length})`}
description="My entire music playlist. It contains all kinds of songs."
>
<div class="not-prose">
<div id="loading-indicator" class="flex flex-col items-center gap-2">
<div
class="h-8 w-8 animate-spin rounded-full border-4 border-neutral-300 border-t-neutral-900 dark:border-neutral-600 dark:border-t-white"
>
</div>
<span
id="loading-percentage"
class="text-sm text-neutral-900 dark:text-white">0%</span
>
</div>
<div id="tracks-container" class="not-prose hidden">
{
tracks.map(({ data: { title, artist, album, youtubeLink } }, index) => (
<Track {title} {artist} {album} {youtubeLink} index={++index} />
@@ -18,3 +28,47 @@ const tracks = await getCollection("tracks");
}
</div>
</PageLayout>
<script is:inline define:vars={{ tracks }}>
const loadThumbnails = async () => {
const tracksContainer = document.getElementById("tracks-container");
const loadingIndicator = document.getElementById("loading-indicator");
const percentageEl = document.getElementById("loading-percentage");
if (!tracksContainer || !loadingIndicator || !percentageEl) {
return;
}
const thumbnailUrls = tracks.map(({ data: { youtubeLink } }) => {
const videoId = youtubeLink.split("v=")[1];
return `https://img.youtube.com/vi/${videoId}/maxresdefault.jpg`;
});
let loadedCount = 0;
const total = thumbnailUrls.length;
const preloadImages = thumbnailUrls.map((url) => {
return new Promise((resolve) => {
const img = new Image();
const handleComplete = () => {
loadedCount++;
const percentage = Math.round((loadedCount / total) * 100);
percentageEl.textContent = `${percentage}%`;
resolve();
};
img.addEventListener("load", handleComplete, { once: true });
img.addEventListener("error", handleComplete, { once: true });
img.src = url;
});
});
await Promise.all(preloadImages);
loadingIndicator.classList.add("hidden");
tracksContainer.classList.remove("hidden");
};
loadThumbnails();
document.addEventListener("astro:after-swap", loadThumbnails);
</script>

View File

@@ -2,13 +2,6 @@
@plugin "@tailwindcss/typography";
@custom-variant dark (&:where(.dark, .dark *));
@theme {
--color-accent-cyan: #0891b2;
--color-accent-cyan-light: #67e8f9;
--color-accent-blue: #1e40af;
--color-accent-blue-light: #93c5fd;
}
@layer base {
body {
font-family: "Tiempos Text", serif;
@@ -23,32 +16,6 @@
font-family: "Styrene A", sans-serif;
}
/* Gradient effect on h1 and h2 */
h1,
h2 {
background: linear-gradient(
135deg,
var(--color-accent-blue) 0%,
var(--color-accent-cyan) 100%
);
background-clip: text;
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-size: 100%;
}
.dark h1,
.dark h2 {
background: linear-gradient(
135deg,
var(--color-accent-blue-light) 0%,
var(--color-accent-cyan-light) 100%
);
background-clip: text;
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
code,
kbd,
samp,
@@ -57,15 +24,6 @@
font-family: "Styrene B", monospace;
}
/* Accent border on code blocks */
pre {
border-left: 4px solid var(--color-accent-cyan);
}
.dark pre {
border-left-color: var(--color-accent-cyan-light);
}
mark {
@apply bg-neutral-200 text-current dark:bg-neutral-600;
}