3 Commits

Author SHA1 Message Date
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
7 changed files with 1279 additions and 786 deletions

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

@@ -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,40 @@ const tracks = await getCollection("tracks");
}
</div>
</PageLayout>
<script is:inline define:vars={{ tracks }}>
const loadThumbnails = async () => {
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 percentageEl = document.getElementById("loading-percentage");
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);
document.getElementById("loading-indicator").classList.add("hidden");
document.getElementById("tracks-container").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;
}