This commit is contained in:
2025-12-13 12:33:15 +01:00
parent 0c10cfa320
commit 7f5a88be0d
16 changed files with 509 additions and 497 deletions
-1
View File
@@ -1 +0,0 @@
<svg stroke="currentColor" fill="currentColor" stroke-width="0" viewBox="0 0 640 512" class="fill-current transition-transform duration-300 ease-in-out hover:text-[#ad87ed] hover:scale-105 focus:outline-none" aria-hidden="true" height="1em" width="1em" xmlns="http://www.w3.org/2000/svg"><path d="M524.531,69.836a1.5,1.5,0,0,0-.764-.7A485.065,485.065,0,0,0,404.081,32.03a1.816,1.816,0,0,0-1.923.91,337.461,337.461,0,0,0-14.9,30.6,447.848,447.848,0,0,0-134.426,0,309.541,309.541,0,0,0-15.135-30.6,1.89,1.89,0,0,0-1.924-.91A483.689,483.689,0,0,0,116.085,69.137a1.712,1.712,0,0,0-.788.676C39.068,183.651,18.186,294.69,28.43,404.354a2.016,2.016,0,0,0,.765,1.375A487.666,487.666,0,0,0,176.02,479.918a1.9,1.9,0,0,0,2.063-.676A348.2,348.2,0,0,0,208.12,430.4a1.86,1.86,0,0,0-1.019-2.588,321.173,321.173,0,0,1-45.868-21.853,1.885,1.885,0,0,1-.185-3.126c3.082-2.309,6.166-4.711,9.109-7.137a1.819,1.819,0,0,1,1.9-.256c96.229,43.917,200.41,43.917,295.5,0a1.812,1.812,0,0,1,1.924.233c2.944,2.426,6.027,4.851,9.132,7.16a1.884,1.884,0,0,1-.162,3.126,301.407,301.407,0,0,1-45.89,21.83,1.875,1.875,0,0,0-1,2.611,391.055,391.055,0,0,0,30.014,48.815,1.864,1.864,0,0,0,2.063.7A486.048,486.048,0,0,0,610.7,405.729a1.882,1.882,0,0,0,.765-1.352C623.729,277.594,590.933,167.465,524.531,69.836ZM222.491,337.58c-28.972,0-52.844-26.587-52.844-59.239S193.056,219.1,222.491,219.1c29.665,0,53.306,26.82,52.843,59.239C275.334,310.993,251.924,337.58,222.491,337.58Zm195.38,0c-28.971,0-52.843-26.587-52.843-59.239S388.437,219.1,417.871,219.1c29.667,0,53.307,26.82,52.844,59.239C470.715,310.993,447.538,337.58,417.871,337.58Z"></path></svg>

Before

Width:  |  Height:  |  Size: 1.6 KiB

-1
View File
@@ -1 +0,0 @@
<svg stroke="currentColor" fill="currentColor" stroke-width="0" viewBox="0 0 496 512" class="fill-current transition-transform duration-300 ease-in-out hover:text-[#ad87ed] hover:scale-105 focus:outline-none" aria-hidden="true" height="1em" width="1em" xmlns="http://www.w3.org/2000/svg"><path d="M165.9 397.4c0 2-2.3 3.6-5.2 3.6-3.3.3-5.6-1.3-5.6-3.6 0-2 2.3-3.6 5.2-3.6 3-.3 5.6 1.3 5.6 3.6zm-31.1-4.5c-.7 2 1.3 4.3 4.3 4.9 2.6 1 5.6 0 6.2-2s-1.3-4.3-4.3-5.2c-2.6-.7-5.5.3-6.2 2.3zm44.2-1.7c-2.9.7-4.9 2.6-4.6 4.9.3 2 2.9 3.3 5.9 2.6 2.9-.7 4.9-2.6 4.6-4.6-.3-1.9-3-3.2-5.9-2.9zM244.8 8C106.1 8 0 113.3 0 252c0 110.9 69.8 205.8 169.5 239.2 12.8 2.3 17.3-5.6 17.3-12.1 0-6.2-.3-40.4-.3-61.4 0 0-70 15-84.7-29.8 0 0-11.4-29.1-27.8-36.6 0 0-22.9-15.7 1.6-15.4 0 0 24.9 2 38.6 25.8 21.9 38.6 58.6 27.5 72.9 20.9 2.3-16 8.8-27.1 16-33.7-55.9-6.2-112.3-14.3-112.3-110.5 0-27.5 7.6-41.3 23.6-58.9-2.6-6.5-11.1-33.3 2.6-67.9 20.9-6.5 69 27 69 27 20-5.6 41.5-8.5 62.8-8.5s42.8 2.9 62.8 8.5c0 0 48.1-33.6 69-27 13.7 34.7 5.2 61.4 2.6 67.9 16 17.7 25.8 31.5 25.8 58.9 0 96.5-58.9 104.2-114.8 110.5 9.2 7.9 17 22.9 17 46.4 0 33.7-.3 75.4-.3 83.6 0 6.5 4.6 14.4 17.3 12.1C428.2 457.8 496 362.9 496 252 496 113.3 383.5 8 244.8 8zM97.2 352.9c-1.3 1-1 3.3.7 5.2 1.6 1.6 3.9 2.3 5.2 1 1.3-1 1-3.3-.7-5.2-1.6-1.6-3.9-2.3-5.2-1zm-10.8-8.1c-.7 1.3.3 2.9 2.3 3.9 1.6 1 3.6.7 4.3-.7.7-1.3-.3-2.9-2.3-3.9-2-.6-3.6-.3-4.3.7zm32.4 35.6c-1.6 1.3-1 4.3 1.3 6.2 2.3 2.3 5.2 2.6 6.5 1 1.3-1.3.7-4.3-1.3-6.2-2.2-2.3-5.2-2.6-6.5-1zm-11.4-14.7c-1.6 1-1.6 3.6 0 5.9 1.6 2.3 4.3 3.3 5.6 2.3 1.6-1.3 1.6-3.9 0-6.2-1.4-2.3-4-3.3-5.6-2z"></path></svg>

Before

Width:  |  Height:  |  Size: 1.6 KiB

-1
View File
@@ -1 +0,0 @@
<svg stroke="currentColor" fill="currentColor" stroke-width="0" viewBox="0 0 512 512" class="fill-current transition-transform duration-300 ease-in-out hover:text-[#ad87ed] hover:scale-105 focus:outline-none" aria-hidden="true" height="1em" width="1em" xmlns="http://www.w3.org/2000/svg"><path d="M502.3 190.8c3.9-3.1 9.7-.2 9.7 4.7V400c0 26.5-21.5 48-48 48H48c-26.5 0-48-21.5-48-48V195.6c0-5 5.7-7.8 9.7-4.7 22.4 17.4 52.1 39.5 154.1 113.6 21.1 15.4 56.7 47.8 92.2 47.6 35.7.3 72-32.8 92.3-47.6 102-74.1 131.6-96.3 154-113.7zM256 320c23.2.4 56.6-29.2 73.4-41.4 132.7-96.3 142.8-104.7 173.4-128.7 5.8-4.5 9.2-11.5 9.2-18.9v-19c0-26.5-21.5-48-48-48H48C21.5 64 0 85.5 0 112v19c0 7.4 3.4 14.3 9.2 18.9 30.6 23.9 40.7 32.4 173.4 128.7 16.8 12.2 50.2 41.8 73.4 41.4z"></path></svg>

Before

Width:  |  Height:  |  Size: 777 B

-1
View File
@@ -1 +0,0 @@
<svg stroke="currentColor" fill="currentColor" stroke-width="0" viewBox="0 0 496 512" class="fill-current transition-transform duration-300 ease-in-out hover:text-[#ad87ed] hover:scale-105 focus:outline-none" aria-hidden="true" height="1em" width="1em" xmlns="http://www.w3.org/2000/svg"><path d="M496 256c0 137-111.2 248-248.4 248-113.8 0-209.6-76.3-239-180.4l95.2 39.3c6.4 32.1 34.9 56.4 68.9 56.4 39.2 0 71.9-32.4 70.2-73.5l84.5-60.2c52.1 1.3 95.8-40.9 95.8-93.5 0-51.6-42-93.5-93.7-93.5s-93.7 42-93.7 93.5v1.2L176.6 279c-15.5-.9-30.7 3.4-43.5 12.1L0 236.1C10.2 108.4 117.1 8 247.6 8 384.8 8 496 119 496 256zM155.7 384.3l-30.5-12.6a52.79 52.79 0 0 0 27.2 25.8c26.9 11.2 57.8-1.6 69-28.4 5.4-13 5.5-27.3.1-40.3-5.4-13-15.5-23.2-28.5-28.6-12.9-5.4-26.7-5.2-38.9-.6l31.5 13c19.8 8.2 29.2 30.9 20.9 50.7-8.3 19.9-31 29.2-50.8 21zm173.8-129.9c-34.4 0-62.4-28-62.4-62.3s28-62.3 62.4-62.3 62.4 28 62.4 62.3-27.9 62.3-62.4 62.3zm.1-15.6c25.9 0 46.9-21 46.9-46.8 0-25.9-21-46.8-46.9-46.8s-46.9 21-46.9 46.8c.1 25.8 21.1 46.8 46.9 46.8z"></path></svg>

Before

Width:  |  Height:  |  Size: 1.0 KiB

-83
View File
@@ -1,83 +0,0 @@
class SpotifyClient {
constructor(url, elementId) {
this.url = url;
this.elementId = elementId;
this.element = document.getElementById(this.elementId);
this.ws = null;
this.reconnectTimeout = null;
this.reconnectAttempts = 0;
this.RECONNECT_BASE_DELAY = 1000;
this.RECONNECT_MAX_DELAY = 30000;
}
start() {
if (!this.element) {
console.error(`Spotify-WS: Element with ID "${this.elementId}" not found.`);
return;
}
this.connect();
}
connect() {
if (this.ws) {
return;
}
console.log("Spotify-WS: Connecting...");
this.ws = new WebSocket(this.url);
this.ws.onopen = () => {
console.log("Spotify-WS: Connection established.");
this.reconnectAttempts = 0;
};
this.ws.onmessage = (event) => {
this.updateDOM(event.data);
};
this.ws.onclose = (event) => {
if (!event.wasClean) {
console.warn("Spotify-WS: Connection closed unexpectedly. Attempting to reconnect...");
this.reconnect();
} else {
console.log("Spotify-WS: Connection closed cleanly.");
}
this.ws = null;
};
this.ws.onerror = (error) => {
console.error("Spotify-WS: An error occurred:", error);
};
}
reconnect() {
const delay = Math.min(
this.RECONNECT_MAX_DELAY,
this.RECONNECT_BASE_DELAY * Math.pow(2, this.reconnectAttempts)
) * (0.8 + Math.random() * 0.4);
console.log(`Spotify-WS: Reconnecting in ${Math.round(delay / 1000)}s...`);
this.reconnectAttempts++;
this.reconnectTimeout = setTimeout(() => this.connect(), delay);
}
updateDOM(data) {
try {
const payload = JSON.parse(data);
if (payload.is_playing && payload.item) {
const artists = payload.item.artists.map(a => a.name).join(", ");
this.element.textContent = `${payload.item.name} - ${artists}`;
} else {
this.element.textContent = "";
}
} catch (err) {
console.error("Spotify-WS: Failed to parse message data.", err);
}
}
}
const spotifyClient = new SpotifyClient("wss://ws.albert.lol", "nowplaying");
spotifyClient.start();
+33
View File
@@ -0,0 +1,33 @@
---
interface Props {
name: 'github' | 'steam' | 'mail' | 'discord'
}
const { name } = Astro.props
const paths = {
discord:
'M524.531,69.836a1.5,1.5,0,0,0-.764-.7A485.065,485.065,0,0,0,404.081,32.03a1.816,1.816,0,0,0-1.923.91,337.461,337.461,0,0,0-14.9,30.6,447.848,447.848,0,0,0-134.426,0,309.541,309.541,0,0,0-15.135-30.6,1.89,1.89,0,0,0-1.924-.91A483.689,483.689,0,0,0,116.085,69.137a1.712,1.712,0,0,0-.788.676C39.068,183.651,18.186,294.69,28.43,404.354a2.016,2.016,0,0,0,.765,1.375A487.666,487.666,0,0,0,176.02,479.918a1.9,1.9,0,0,0,2.063-.676A348.2,348.2,0,0,0,208.12,430.4a1.86,1.86,0,0,0-1.019-2.588,321.173,321.173,0,0,1-45.868-21.853,1.885,1.885,0,0,1-.185-3.126c3.082-2.309,6.166-4.711,9.109-7.137a1.819,1.819,0,0,1,1.9-.256c96.229,43.917,200.41,43.917,295.5,0a1.812,1.812,0,0,1,1.924.233c2.944,2.426,6.027,4.851,9.132,7.16a1.884,1.884,0,0,1-.162,3.126,301.407,301.407,0,0,1-45.89,21.83,1.875,1.875,0,0,0-1,2.611,391.055,391.055,0,0,0,30.014,48.815,1.864,1.864,0,0,0,2.063.7A486.048,486.048,0,0,0,610.7,405.729a1.882,1.882,0,0,0,.765-1.352C623.729,277.594,590.933,167.465,524.531,69.836ZM222.491,337.58c-28.972,0-52.844-26.587-52.844-59.239S193.056,219.1,222.491,219.1c29.665,0,53.306,26.82,52.843,59.239C275.334,310.993,251.924,337.58,222.491,337.58Zm195.38,0c-28.971,0-52.843-26.587-52.843-59.239S388.437,219.1,417.871,219.1c29.667,0,53.307,26.82,52.844,59.239C470.715,310.993,447.538,337.58,417.871,337.58Z',
github:
'M165.9 397.4c0 2-2.3 3.6-5.2 3.6-3.3.3-5.6-1.3-5.6-3.6 0-2 2.3-3.6 5.2-3.6 3-.3 5.6 1.3 5.6 3.6zm-31.1-4.5c-.7 2 1.3 4.3 4.3 4.9 2.6 1 5.6 0 6.2-2s-1.3-4.3-4.3-5.2c-2.6-.7-5.5.3-6.2 2.3zm44.2-1.7c-2.9.7-4.9 2.6-4.6 4.9.3 2 2.9 3.3 5.9 2.6 2.9-.7 4.9-2.6 4.6-4.6-.3-1.9-3-3.2-5.9-2.9zM244.8 8C106.1 8 0 113.3 0 252c0 110.9 69.8 205.8 169.5 239.2 12.8 2.3 17.3-5.6 17.3-12.1 0-6.2-.3-40.4-.3-61.4 0 0-70 15-84.7-29.8 0 0-11.4-29.1-27.8-36.6 0 0-22.9-15.7 1.6-15.4 0 0 24.9 2 38.6 25.8 21.9 38.6 58.6 27.5 72.9 20.9 2.3-16 8.8-27.1 16-33.7-55.9-6.2-112.3-14.3-112.3-110.5 0-27.5 7.6-41.3 23.6-58.9-2.6-6.5-11.1-33.3 2.6-67.9 20.9-6.5 69 27 69 27 20-5.6 41.5-8.5 62.8-8.5s42.8 2.9 62.8 8.5c0 0 48.1-33.6 69-27 13.7 34.7 5.2 61.4 2.6 67.9 16 17.7 25.8 31.5 25.8 58.9 0 96.5-58.9 104.2-114.8 110.5 9.2 7.9 17 22.9 17 46.4 0 33.7-.3 75.4-.3 83.6 0 6.5 4.6 14.4 17.3 12.1C428.2 457.8 496 362.9 496 252 496 113.3 383.5 8 244.8 8zM97.2 352.9c-1.3 1-1 3.3.7 5.2 1.6 1.6 3.9 2.3 5.2 1 1.3-1 1-3.3-.7-5.2-1.6-1.6-3.9-2.3-5.2-1zm-10.8-8.1c-.7 1.3.3 2.9 2.3 3.9 1.6 1 3.6.7 4.3-.7.7-1.3-.3-2.9-2.3-3.9-2-.6-3.6-.3-4.3.7zm32.4 35.6c-1.6 1.3-1 4.3 1.3 6.2 2.3 2.3 5.2 2.6 6.5 1 1.3-1.3.7-4.3-1.3-6.2-2.2-2.3-5.2-2.6-6.5-1zm-11.4-14.7c-1.6 1-1.6 3.6 0 5.9 1.6 2.3 4.3 3.3 5.6 2.3 1.6-1.3 1.6-3.9 0-6.2-1.4-2.3-4-3.3-5.6-2z',
mail: 'M502.3 190.8c3.9-3.1 9.7-.2 9.7 4.7V400c0 26.5-21.5 48-48 48H48c-26.5 0-48-21.5-48-48V195.6c0-5 5.7-7.8 9.7-4.7 22.4 17.4 52.1 39.5 154.1 113.6 21.1 15.4 56.7 47.8 92.2 47.6 35.7.3 72-32.8 92.3-47.6 102-74.1 131.6-96.3 154-113.7zM256 320c23.2.4 56.6-29.2 73.4-41.4 132.7-96.3 142.8-104.7 173.4-128.7 5.8-4.5 9.2-11.5 9.2-18.9v-19c0-26.5-21.5-48-48-48H48C21.5 64 0 85.5 0 112v19c0 7.4 3.4 14.3 9.2 18.9 30.6 23.9 40.7 32.4 173.4 128.7 16.8 12.2 50.2 41.8 73.4 41.4z',
steam:
'M496 256c0 137-111.2 248-248.4 248-113.8 0-209.6-76.3-239-180.4l95.2 39.3c6.4 32.1 34.9 56.4 68.9 56.4 39.2 0 71.9-32.4 70.2-73.5l84.5-60.2c52.1 1.3 95.8-40.9 95.8-93.5 0-51.6-42-93.5-93.7-93.5s-93.7 42-93.7 93.5v1.2L176.6 279c-15.5-.9-30.7 3.4-43.5 12.1L0 236.1C10.2 108.4 117.1 8 247.6 8 384.8 8 496 119 496 256zM155.7 384.3l-30.5-12.6a52.79 52.79 0 0 0 27.2 25.8c26.9 11.2 57.8-1.6 69-28.4 5.4-13 5.5-27.3.1-40.3-5.4-13-15.5-23.2-28.5-28.6-12.9-5.4-26.7-5.2-38.9-.6l31.5 13c19.8 8.2 29.2 30.9 20.9 50.7-8.3 19.9-31 29.2-50.8 21zm173.8-129.9c-34.4 0-62.4-28-62.4-62.3s28-62.3 62.4-62.3 62.4 28 62.4 62.3-27.9 62.3-62.4 62.3zm.1-15.6c25.9 0 46.9-21 46.9-46.8 0-25.9-21-46.8-46.9-46.8s-46.9 21-46.9 46.8c.1 25.8 21.1 46.8 46.9 46.8z',
}
const viewBox =
name === 'discord'
? '0 0 640 512'
: name === 'github' || name === 'steam'
? '0 0 496 512'
: '0 0 512 512'
---
<svg
viewBox={viewBox}
width='1.5em'
height='1.5em'
fill='currentColor'
aria-hidden='true'>
<path d={paths[name]}></path>
</svg>
+4 -4
View File
@@ -1,9 +1,8 @@
<span id="nowplaying"></span> <span id='nowplaying'></span>
<script> <script>
import { SpotifyClient } from '../scripts/spotify.js'; import { SpotifyClient } from '../lib/spotify'
const spotifyClient = new SpotifyClient("wss://ws.albert.lol", "nowplaying"); new SpotifyClient('wss://ws.albert.lol', 'nowplaying').start()
spotifyClient.start();
</script> </script>
<style> <style>
@@ -17,5 +16,6 @@
white-space: nowrap; white-space: nowrap;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
min-height: 1em;
} }
</style> </style>
+67 -80
View File
@@ -1,99 +1,86 @@
--- ---
const projects = [ const projects = [
{ name: "spotify-ws", url: "https://github.com/skidoodle/spotify-ws", desc: "Gets now-playing-song from Spotify with WebSockets." }, {
{ name: "ipinfo", url: "https://github.com/skidoodle/ipinfo", desc: "Shows details about any IP address or ASN." }, name: 'spotify-ws',
{ name: "hostinfo", url: "https://github.com/skidoodle/hostinfo", desc: "Browser extension showing website origin details." }, url: 'https://github.com/skidoodle/spotify-ws',
{ name: "mediaproxy", url: "https://github.com/skidoodle/mediaproxy", desc: "Proxy for caching and optimizing media." }, desc: 'Gets now-playing-song from Spotify with WebSockets.',
{ name: "pastebin", url: "https://github.com/skidoodle/pastebin", desc: "Another simple pastebin app." }, },
{ name: "albert.lol", url: "https://github.com/skidoodle/albert.lol", desc: "You're looking at it, built with Astro." }, {
{ name: "budgetable", url: "https://github.com/skidoodle/budgetable", desc: "Tracks items to buy later." }, name: 'ipinfo',
{ name: "watch-together", url: "https://github.com/skidoodle/watch-together", desc: "Watch YouTube together with your friends." }, url: 'https://github.com/skidoodle/ipinfo',
{ name: "erettsegi-browser", url: "https://github.com/skidoodle/erettsegi-browser", desc: "Finds previous Hungarian graduation exams." }, desc: 'Shows details about any IP address or ASN.',
{ name: "ncore-stats", url: "https://github.com/skidoodle/ncore-stats", desc: "Tracks profile activity on nCore." }, },
{ name: "ncore-leaderboard", url: "https://github.com/skidoodle/ncore-leaderboard", desc: "Scrapes and sorts profiles from nCore." }, {
{ name: "iphistory", url: "https://github.com/skidoodle/iphistory", desc: "Monitors and records public IP address history." }, name: 'hostinfo',
]; url: 'https://github.com/skidoodle/hostinfo',
desc: 'Browser extension showing website origin details.',
},
{
name: 'mediaproxy',
url: 'https://github.com/skidoodle/mediaproxy',
desc: 'Proxy for caching and optimizing media.',
},
{
name: 'pastebin',
url: 'https://github.com/skidoodle/pastebin',
desc: 'Another simple pastebin app.',
},
{
name: 'albert.lol',
url: 'https://github.com/skidoodle/albert.lol',
desc: "You're looking at it, built with Astro.",
},
{
name: 'budgetable',
url: 'https://github.com/skidoodle/budgetable',
desc: 'Tracks items to buy later.',
},
{
name: 'watch-together',
url: 'https://github.com/skidoodle/watch-together',
desc: 'Watch YouTube together with your friends.',
},
{
name: 'erettsegi-browser',
url: 'https://github.com/skidoodle/erettsegi-browser',
desc: 'Finds previous Hungarian graduation exams.',
},
{
name: 'ncore-stats',
url: 'https://github.com/skidoodle/ncore-stats',
desc: 'Tracks profile activity on nCore.',
},
{
name: 'ncore-leaderboard',
url: 'https://github.com/skidoodle/ncore-leaderboard',
desc: 'Scrapes and sorts profiles from nCore.',
},
{
name: 'iphistory',
url: 'https://github.com/skidoodle/iphistory',
desc: 'Monitors and records public IP address history.',
},
]
--- ---
<section class="projects"> <section class='projects details-list'>
<details> <details>
<summary>~/projects</summary> <summary>~/projects</summary>
<div> <div>
<dl> <dl>
{projects.map((project) => ( {
projects.map(project => (
<> <>
<dt> <dt>
<a href={project.url} target="_blank" rel="noopener noreferrer"> <a href={project.url} target='_blank' rel='noopener noreferrer'>
{project.name} {project.name}
</a> </a>
</dt> </dt>
<dd set:html={project.desc} /> <dd set:html={project.desc} />
</> </>
))} ))
}
</dl> </dl>
</div> </div>
</details> </details>
</section> </section>
<style>
a {
color: var(--primary-color);
text-decoration: underline;
}
a:hover {
color: var(--hover-color);
}
dt {
color: var(--primary-color);
font-weight: bold;
font-size: 1.17em;
margin-inline-start: var(--spacing-md);
margin-top: 1rem;
}
dt:first-of-type {
margin-top: 0;
}
dd {
margin-inline-start: var(--spacing-lg);
}
details {
font-family: "JetBrains Mono", monospace;
}
summary {
cursor: pointer;
display: inline-flex;
align-items: center;
gap: 0.5rem;
color: var(--primary-color);
font-size: 1.3em;
font-weight: bold;
margin-block: 1.5rem 0.5rem;
list-style: none;
}
summary::-webkit-details-marker {
display: none;
}
summary::after {
content: ' ▼';
font-size: 0.6em;
opacity: 0.7;
margin-inline-start: 0.3rem;
}
details[open] > summary::after {
content: ' ▲';
}
details > div {
margin-top: 1rem;
margin-inline-start: var(--spacing-md);
}
</style>
+5 -66
View File
@@ -1,4 +1,4 @@
<section class="setup"> <section class='setup details-list'>
<details> <details>
<summary>~/setup</summary> <summary>~/setup</summary>
<div> <div>
@@ -9,9 +9,9 @@
<dd><b>GPU:</b> RX 6700</dd> <dd><b>GPU:</b> RX 6700</dd>
<dd><b>RAM:</b> 48 GB</dd> <dd><b>RAM:</b> 48 GB</dd>
<dd> <dd>
<div class="storage-layout"> <div class='storage-layout'>
<p><b>Storage:</b></p> <p><b>Storage:</b></p>
<div class="storage-items"> <div class='storage-items'>
<p>Samsung 990 Pro 1TB</p> <p>Samsung 990 Pro 1TB</p>
<p>Crucial P1 1TB</p> <p>Crucial P1 1TB</p>
<p>Seagate Barracuda 2TB</p> <p>Seagate Barracuda 2TB</p>
@@ -24,9 +24,9 @@
<dd><b>CPU:</b> Dual Xeon E5-2680 v4</dd> <dd><b>CPU:</b> Dual Xeon E5-2680 v4</dd>
<dd><b>RAM:</b> 128 GB</dd> <dd><b>RAM:</b> 128 GB</dd>
<dd> <dd>
<div class="storage-layout"> <div class='storage-layout'>
<p><b>Storage:</b></p> <p><b>Storage:</b></p>
<div class="storage-items"> <div class='storage-items'>
<p>2x WD Black SN770 1TB (MIRROR)</p> <p>2x WD Black SN770 1TB (MIRROR)</p>
<p>8x Toshiba Enterprise 6TB (RAID-Z2)</p> <p>8x Toshiba Enterprise 6TB (RAID-Z2)</p>
</div> </div>
@@ -50,67 +50,6 @@
</section> </section>
<style> <style>
a {
color: var(--primary-color);
text-decoration: underline;
}
a:hover {
color: var(--hover-color);
}
dt {
color: var(--primary-color);
font-weight: bold;
font-size: 1.17em;
margin-inline-start: var(--spacing-md);
margin-top: 1rem;
}
dt:first-of-type {
margin-top: 0;
}
dd {
margin-inline-start: var(--spacing-lg);
}
details {
font-family: "JetBrains Mono", monospace;
}
summary {
cursor: pointer;
display: inline-flex;
align-items: center;
gap: 0.5rem;
color: var(--primary-color);
font-size: 1.3em;
font-weight: bold;
margin-block: 1.5rem 0.5rem;
list-style: none;
}
summary::-webkit-details-marker {
display: none;
}
summary::after {
content: ' ▼';
font-size: 0.6em;
opacity: 0.7;
margin-inline-start: 0.3rem;
}
details[open] > summary::after {
content: ' ▲';
}
details > div {
margin-top: 1rem;
margin-inline-start: var(--spacing-md);
}
.storage-layout { .storage-layout {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
+31 -18
View File
@@ -1,17 +1,33 @@
<section class="socials"> ---
import Icon from './icon.astro'
---
<section class='socials'>
<h2>~/socials</h2> <h2>~/socials</h2>
<nav> <nav>
<a href="https://github.com/skidoodle" target="_blank" rel="noopener noreferrer" aria-label="GitHub"> <a
<img src="/static/icons/github.svg" alt="GitHub Profile"> href='https://github.com/skidoodle'
target='_blank'
rel='noopener noreferrer'
aria-label='GitHub'>
<Icon name='github' />
</a> </a>
<a href="https://steamcommunity.com/id/_albert" target="_blank" rel="noopener noreferrer" aria-label="Steam"> <a
<img src="/static/icons/steam.svg" alt="Steam Profile"> href='https://steamcommunity.com/id/_albert'
target='_blank'
rel='noopener noreferrer'
aria-label='Steam'>
<Icon name='steam' />
</a> </a>
<a href="mailto:contact@albert.lol" aria-label="Email"> <a href='mailto:contact@albert.lol' aria-label='Email'>
<img src="/static/icons/mail.svg" alt="Email Contact"> <Icon name='mail' />
</a> </a>
<a href="https://discord.com/users/637745537369767936" target="_blank" rel="noopener noreferrer" aria-label="Discord"> <a
<img src="/static/icons/discord.svg" alt="Discord Profile"> href='https://discord.com/users/637745537369767936'
target='_blank'
rel='noopener noreferrer'
aria-label='Discord'>
<Icon name='discord' />
</a> </a>
</nav> </nav>
</section> </section>
@@ -31,17 +47,14 @@
align-items: center; align-items: center;
padding: 0.3em; padding: 0.3em;
border-radius: 50%; border-radius: 50%;
transition: opacity 0.2s, box-shadow 0.2s; color: var(--text-color);
transition:
color 0.2s,
transform 0.2s;
} }
.socials a:hover { .socials a:hover {
opacity: 0.7; color: var(--primary-color);
} transform: scale(1.1);
.socials img {
display: block;
width: 1.5em;
height: 1.5em;
filter: brightness(0) saturate(100%) invert(1);
} }
</style> </style>
+17 -5
View File
@@ -1,12 +1,24 @@
--- ---
const birthDate = new Date('2004-07-22'); const birthDate = new Date('2004-07-22')
const today = new Date(); const today = new Date()
const age = Math.floor((today.getTime() - birthDate.getTime()) / 31557600000);
let age = today.getFullYear() - birthDate.getFullYear()
const monthDiff = today.getMonth() - birthDate.getMonth()
if (
monthDiff < 0 ||
(monthDiff === 0 && today.getDate() < birthDate.getDate())
) {
age--
}
--- ---
<section class="whoami"> <section class='whoami'>
<h2>~/whoami</h2> <h2>~/whoami</h2>
<p>I'm a <span id="age">{age}</span>-year-old developer and tech enthusiast. I enjoy working on my homelab and coding in TypeScript and Go.</p> <p>
I'm a <span id='age'>{age}</span>-year-old developer and tech enthusiast. I
enjoy working on my homelab and coding in TypeScript and Go.
</p>
</section> </section>
<style> <style>
+65 -43
View File
@@ -1,62 +1,84 @@
--- ---
import '../styles/global.css'; import '../styles/global.css'
interface Props { interface Props {
title: string; title: string
description: string; description: string
} }
const { title, description } = Astro.props; const { title, description } = Astro.props
const schema = JSON.stringify({ const schema = JSON.stringify({
"@context": "https://schema.org", '@context': 'https://schema.org',
"@type": "Person", '@type': 'Person',
"name": "Albert", name: 'Albert',
"alternateName": "skidoodle", alternateName: 'skidoodle',
"url": "https://albert.lol/", url: 'https://albert.lol/',
"image": "https://albert.lol/static/preview.png", image: 'https://albert.lol/static/preview.png',
"jobTitle": "Developer and Tech Enthusiast", jobTitle: 'Developer and Tech Enthusiast',
"description": "21-year-old developer and tech enthusiast. I enjoy working on my homelab and coding in TypeScript and Go.", description: description,
"knowsAbout": ["Web Development", "TypeScript", "Go", "Homelab", "Linux", "Open Source Projects"], knowsAbout: [
"sameAs": [ 'Web Development',
"https://github.com/skidoodle", 'TypeScript',
"https://steamcommunity.com/id/_albert", 'Go',
"https://discord.com/users/637745537369767936" 'Homelab',
'Linux',
'Open Source Projects',
], ],
"worksFor": { sameAs: [
"@type": "Organization", 'https://github.com/skidoodle',
"name": "Personal Projects / Open Source" 'https://steamcommunity.com/id/_albert',
} 'https://discord.com/users/637745537369767936',
}); ],
worksFor: {
'@type': 'Organization',
name: 'Personal Projects / Open Source',
},
})
--- ---
<!DOCTYPE html> <!doctype html>
<html lang="en"> <html lang='en'>
<head> <head>
<meta charset="UTF-8"> <meta charset='UTF-8' />
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name='viewport' content='width=device-width, initial-scale=1.0' />
<title>{title}</title> <title>{title}</title>
<link rel="icon" href="/favicon.ico" type="image/x-icon"> <link rel='icon' href='/favicon.ico' type='image/x-icon' />
<meta name="theme-color" content="#bb86fc"> <meta name='theme-color' content='#bb86fc' />
<meta name="description" content={description}> <meta name='description' content={description} />
<meta property="og:title" content={title}> <meta property='og:title' content={title} />
<meta property="og:description" content={description}> <meta property='og:description' content={description} />
<meta property="og:type" content="website"> <meta property='og:type' content='website' />
<meta property="og:url" content="https://albert.lol/"> <meta property='og:url' content='https://albert.lol/' />
<meta property='og:image' content='https://albert.lol/static/preview.png' />
<meta name="twitter:card" content="summary"> <meta name='twitter:card' content='summary_large_image' />
<meta name="twitter:title" content={title}> <meta name='twitter:title' content={title} />
<meta name="twitter:description" content={description}> <meta name='twitter:description' content={description} />
<meta
name='twitter:image'
content='https://albert.lol/static/preview.png'
/>
<link rel="preload" href="/static/fonts/jetbrains.woff2" as="font" type="font/woff2" crossorigin> <link
rel='preload'
href='/static/fonts/jetbrains.woff2'
as='font'
type='font/woff2'
crossorigin
/>
<script is:inline defer src="https://analytics.albert.lol/script.js" data-website-id="2c900d5e-c577-4824-ad37-0cdf68383c42"></script> <script
<script is:inline type="application/ld+json" set:html={schema} /> is:inline
</head> defer
<body> src='https://analytics.albert.lol/script.js'
data-website-id='2c900d5e-c577-4824-ad37-0cdf68383c42'></script>
<script is:inline type='application/ld+json' set:html={schema} />
</head>
<body>
<main> <main>
<slot /> <slot />
</main> </main>
</body> </body>
</html> </html>
+110
View File
@@ -0,0 +1,110 @@
interface SpotifyArtist {
name: string;
}
interface SpotifyItem {
name: string;
artists: SpotifyArtist[];
}
interface SpotifyMessage {
is_playing: boolean;
item: SpotifyItem | null;
}
export class SpotifyClient {
private readonly url: string;
private readonly elementId: string;
private element: HTMLElement | null = null;
private ws: WebSocket | null = null;
private reconnectTimeout: ReturnType<typeof setTimeout> | null = null;
private reconnectAttempts: number = 0;
private readonly RECONNECT_BASE_DELAY = 1000;
private readonly RECONNECT_MAX_DELAY = 30000;
constructor(url: string, elementId: string) {
this.url = url;
this.elementId = elementId;
}
public start(): void {
this.element = document.getElementById(this.elementId);
if (!this.element) {
console.warn(`Spotify-WS: Element #${this.elementId} not found. Retrying in 1s...`);
setTimeout(() => this.start(), 1000);
return;
}
this.connect();
}
private connect(): void {
if (this.ws && (this.ws.readyState === WebSocket.OPEN || this.ws.readyState === WebSocket.CONNECTING)) {
return;
}
console.log("Spotify-WS: Connecting...");
this.ws = new WebSocket(this.url);
this.ws.onopen = () => {
console.log("Spotify-WS: Connected");
this.reconnectAttempts = 0;
};
this.ws.onmessage = (event: MessageEvent) => {
this.updateDOM(event.data);
};
this.ws.onclose = (event: CloseEvent) => {
this.ws = null;
if (!event.wasClean) {
this.scheduleReconnect();
}
};
this.ws.onerror = (error: Event) => {
console.error("Spotify-WS Error:", error);
};
}
private scheduleReconnect(): void {
if (this.reconnectTimeout) {
clearTimeout(this.reconnectTimeout);
}
const delay = Math.min(
this.RECONNECT_MAX_DELAY,
this.RECONNECT_BASE_DELAY * Math.pow(2, this.reconnectAttempts)
) * (0.8 + Math.random() * 0.4);
console.log(`Spotify-WS: Reconnecting in ${Math.round(delay / 1000)}s...`);
this.reconnectAttempts++;
this.reconnectTimeout = setTimeout(() => this.connect(), delay);
}
private updateDOM(data: string): void {
if (!this.element) return;
try {
const payload: SpotifyMessage = JSON.parse(data);
if (payload.is_playing && payload.item) {
const artists = payload.item.artists.map((a) => a.name).join(", ");
const text = `${payload.item.name} - ${artists}`;
if (this.element.textContent !== text) {
this.element.textContent = text;
this.element.title = `Playing: ${payload.item.name}`;
}
} else {
this.element.textContent = "";
this.element.title = "";
}
} catch (err) {
console.error("Spotify-WS: Invalid JSON received", err);
}
}
}
+10 -9
View File
@@ -1,19 +1,20 @@
--- ---
import Layout from '../layouts/layout.astro'; import Layout from '../layouts/layout.astro'
import Nowplaying from '../components/nowplaying.astro'; import Nowplaying from '../components/nowplaying.astro'
import Whoami from '../components/whoami.astro'; import Whoami from '../components/whoami.astro'
import Socials from '../components/socials.astro'; import Socials from '../components/socials.astro'
import Projects from '../components/projects.astro'; import Projects from '../components/projects.astro'
import Setup from '../components/setup.astro'; import Setup from '../components/setup.astro'
const title = "albert"; const title = 'albert'
const description = "A 21-year-old developer and tech enthusiast. I enjoy working on my homelab and coding in TypeScript and Go."; const description =
'A 21-year-old developer and tech enthusiast. I enjoy working on my homelab and coding in TypeScript and Go.'
--- ---
<Layout title={title} description={description}> <Layout title={title} description={description}>
<h1>[{title}]</h1> <h1>[{title}]</h1>
<Nowplaying /> <Nowplaying />
<hr> <hr />
<Whoami /> <Whoami />
<Socials /> <Socials />
<Projects /> <Projects />
-80
View File
@@ -1,80 +0,0 @@
export class SpotifyClient {
constructor(url, elementId) {
this.url = url;
this.elementId = elementId;
this.element = document.getElementById(this.elementId);
this.ws = null;
this.reconnectTimeout = null;
this.reconnectAttempts = 0;
this.RECONNECT_BASE_DELAY = 1000;
this.RECONNECT_MAX_DELAY = 30000;
}
start() {
if (!this.element) {
console.error(`Spotify-WS: Element with ID "${this.elementId}" not found.`);
return;
}
this.connect();
}
connect() {
if (this.ws) {
return;
}
console.log("Spotify-WS: Connecting...");
this.ws = new WebSocket(this.url);
this.ws.onopen = () => {
console.log("Spotify-WS: Connection established.");
this.reconnectAttempts = 0;
};
this.ws.onmessage = (event) => {
this.updateDOM(event.data);
};
this.ws.onclose = (event) => {
if (!event.wasClean) {
console.warn("Spotify-WS: Connection closed unexpectedly. Attempting to reconnect...");
this.reconnect();
} else {
console.log("Spotify-WS: Connection closed cleanly.");
}
this.ws = null;
};
this.ws.onerror = (error) => {
console.error("Spotify-WS: An error occurred:", error);
};
}
reconnect() {
const delay = Math.min(
this.RECONNECT_MAX_DELAY,
this.RECONNECT_BASE_DELAY * Math.pow(2, this.reconnectAttempts)
) * (0.8 + Math.random() * 0.4);
console.log(`Spotify-WS: Reconnecting in ${Math.round(delay / 1000)}s...`);
this.reconnectAttempts++;
this.reconnectTimeout = setTimeout(() => this.connect(), delay);
}
updateDOM(data) {
try {
const payload = JSON.parse(data);
if (payload.is_playing && payload.item) {
const artists = payload.item.artists.map(a => a.name).join(", ");
this.element.textContent = `${payload.item.name} - ${artists}`;
} else {
this.element.textContent = "";
}
} catch (err) {
console.error("Spotify-WS: Failed to parse message data.", err);
}
}
}
+62
View File
@@ -53,6 +53,16 @@ h2 {
margin-block: 1.5rem 0.5rem; margin-block: 1.5rem 0.5rem;
} }
a {
color: var(--primary-color);
text-decoration: underline;
text-underline-offset: 2px;
}
a:hover {
color: var(--hover-color);
}
a:focus-visible { a:focus-visible {
outline: none; outline: none;
box-shadow: 0 0 0 3px var(--focus-shadow-color); box-shadow: 0 0 0 3px var(--focus-shadow-color);
@@ -64,6 +74,58 @@ hr {
border-top: 4px solid var(--primary-color); border-top: 4px solid var(--primary-color);
} }
.details-list details {
font-family: "JetBrains Mono", monospace;
}
.details-list summary {
cursor: pointer;
display: inline-flex;
align-items: center;
gap: 0.5rem;
color: var(--primary-color);
font-size: 1.3em;
font-weight: bold;
margin-block: 1.5rem 0.5rem;
list-style: none;
}
.details-list summary::-webkit-details-marker {
display: none;
}
.details-list summary::after {
content: ' ▼';
font-size: 0.6em;
opacity: 0.7;
margin-inline-start: 0.3rem;
}
.details-list details[open]>summary::after {
content: ' ▲';
}
.details-list details>div {
margin-top: 1rem;
margin-inline-start: var(--spacing-md);
}
.details-list dt {
color: var(--primary-color);
font-weight: bold;
font-size: 1.17em;
margin-inline-start: var(--spacing-md);
margin-top: 1rem;
}
.details-list dt:first-of-type {
margin-top: 0;
}
.details-list dd {
margin-inline-start: var(--spacing-lg);
}
@media (max-width: 600px) { @media (max-width: 600px) {
body { body {
padding: 1rem; padding: 1rem;