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
+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>
import { SpotifyClient } from '../scripts/spotify.js';
const spotifyClient = new SpotifyClient("wss://ws.albert.lol", "nowplaying");
spotifyClient.start();
import { SpotifyClient } from '../lib/spotify'
new SpotifyClient('wss://ws.albert.lol', 'nowplaying').start()
</script>
<style>
@@ -17,5 +16,6 @@
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
min-height: 1em;
}
</style>
+81 -94
View File
@@ -1,99 +1,86 @@
---
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: "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." },
];
{
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: '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">
<details>
<summary>~/projects</summary>
<div>
<dl>
{projects.map((project) => (
<>
<dt>
<a href={project.url} target="_blank" rel="noopener noreferrer">
{project.name}
</a>
</dt>
<dd set:html={project.desc} />
</>
))}
</dl>
</div>
</details>
<section class='projects details-list'>
<details>
<summary>~/projects</summary>
<div>
<dl>
{
projects.map(project => (
<>
<dt>
<a href={project.url} target='_blank' rel='noopener noreferrer'>
{project.name}
</a>
</dt>
<dd set:html={project.desc} />
</>
))
}
</dl>
</div>
</details>
</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>
+58 -119
View File
@@ -1,128 +1,67 @@
<section class="setup">
<details>
<summary>~/setup</summary>
<div>
<dl>
<dt>PC</dt>
<dd><b>OS:</b> Windows 11 Pro</dd>
<dd><b>CPU:</b> Ryzen 5 5600X</dd>
<dd><b>GPU:</b> RX 6700</dd>
<dd><b>RAM:</b> 48 GB</dd>
<dd>
<div class="storage-layout">
<p><b>Storage:</b></p>
<div class="storage-items">
<p>Samsung 990 Pro 1TB</p>
<p>Crucial P1 1TB</p>
<p>Seagate Barracuda 2TB</p>
</div>
</div>
</dd>
<section class='setup details-list'>
<details>
<summary>~/setup</summary>
<div>
<dl>
<dt>PC</dt>
<dd><b>OS:</b> Windows 11 Pro</dd>
<dd><b>CPU:</b> Ryzen 5 5600X</dd>
<dd><b>GPU:</b> RX 6700</dd>
<dd><b>RAM:</b> 48 GB</dd>
<dd>
<div class='storage-layout'>
<p><b>Storage:</b></p>
<div class='storage-items'>
<p>Samsung 990 Pro 1TB</p>
<p>Crucial P1 1TB</p>
<p>Seagate Barracuda 2TB</p>
</div>
</div>
</dd>
<dt>Server</dt>
<dd><b>OS:</b> Proxmox VE</dd>
<dd><b>CPU:</b> Dual Xeon E5-2680 v4</dd>
<dd><b>RAM:</b> 128 GB</dd>
<dd>
<div class="storage-layout">
<p><b>Storage:</b></p>
<div class="storage-items">
<p>2x WD Black SN770 1TB (MIRROR)</p>
<p>8x Toshiba Enterprise 6TB (RAID-Z2)</p>
</div>
</div>
</dd>
<dt>Server</dt>
<dd><b>OS:</b> Proxmox VE</dd>
<dd><b>CPU:</b> Dual Xeon E5-2680 v4</dd>
<dd><b>RAM:</b> 128 GB</dd>
<dd>
<div class='storage-layout'>
<p><b>Storage:</b></p>
<div class='storage-items'>
<p>2x WD Black SN770 1TB (MIRROR)</p>
<p>8x Toshiba Enterprise 6TB (RAID-Z2)</p>
</div>
</div>
</dd>
<dt>Laptop</dt>
<dd><b>OS:</b> Windows 11 Pro</dd>
<dd><b>CPU:</b> Ryzen 5 7520U</dd>
<dd><b>GPU:</b> Radeon 610M</dd>
<dd><b>RAM:</b> 16 GB</dd>
<dd><b>Storage:</b> Samsung 480GB</dd>
<dt>Laptop</dt>
<dd><b>OS:</b> Windows 11 Pro</dd>
<dd><b>CPU:</b> Ryzen 5 7520U</dd>
<dd><b>GPU:</b> Radeon 610M</dd>
<dd><b>RAM:</b> 16 GB</dd>
<dd><b>Storage:</b> Samsung 480GB</dd>
<dt>Test</dt>
<dd><b>CPU:</b> Ryzen 3 1300X</dd>
<dd><b>RAM:</b> 32 GB</dd>
<dd><b>Storage:</b> Samsung 970 EVO Plus 250GB</dd>
</dl>
</div>
</details>
<dt>Test</dt>
<dd><b>CPU:</b> Ryzen 3 1300X</dd>
<dd><b>RAM:</b> 32 GB</dd>
<dd><b>Storage:</b> Samsung 970 EVO Plus 250GB</dd>
</dl>
</div>
</details>
</section>
<style>
a {
color: var(--primary-color);
text-decoration: underline;
}
.storage-layout {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
}
a:hover {
color: var(--hover-color);
}
.storage-layout p,
.storage-items p {
margin-inline-start: 0;
}
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 {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
}
.storage-layout p,
.storage-items p {
margin-inline-start: 0;
}
.storage-items {
margin-inline-start: 1ch;
}
.storage-items {
margin-inline-start: 1ch;
}
</style>
+54 -41
View File
@@ -1,47 +1,60 @@
<section class="socials">
<h2>~/socials</h2>
<nav>
<a href="https://github.com/skidoodle" target="_blank" rel="noopener noreferrer" aria-label="GitHub">
<img src="/static/icons/github.svg" alt="GitHub Profile">
</a>
<a href="https://steamcommunity.com/id/_albert" target="_blank" rel="noopener noreferrer" aria-label="Steam">
<img src="/static/icons/steam.svg" alt="Steam Profile">
</a>
<a href="mailto:contact@albert.lol" aria-label="Email">
<img src="/static/icons/mail.svg" alt="Email Contact">
</a>
<a href="https://discord.com/users/637745537369767936" target="_blank" rel="noopener noreferrer" aria-label="Discord">
<img src="/static/icons/discord.svg" alt="Discord Profile">
</a>
</nav>
---
import Icon from './icon.astro'
---
<section class='socials'>
<h2>~/socials</h2>
<nav>
<a
href='https://github.com/skidoodle'
target='_blank'
rel='noopener noreferrer'
aria-label='GitHub'>
<Icon name='github' />
</a>
<a
href='https://steamcommunity.com/id/_albert'
target='_blank'
rel='noopener noreferrer'
aria-label='Steam'>
<Icon name='steam' />
</a>
<a href='mailto:contact@albert.lol' aria-label='Email'>
<Icon name='mail' />
</a>
<a
href='https://discord.com/users/637745537369767936'
target='_blank'
rel='noopener noreferrer'
aria-label='Discord'>
<Icon name='discord' />
</a>
</nav>
</section>
<style>
.socials nav {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 1.5rem;
margin-inline-start: var(--spacing-md);
}
.socials nav {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 1.5rem;
margin-inline-start: var(--spacing-md);
}
.socials a {
display: inline-flex;
justify-content: center;
align-items: center;
padding: 0.3em;
border-radius: 50%;
transition: opacity 0.2s, box-shadow 0.2s;
}
.socials a {
display: inline-flex;
justify-content: center;
align-items: center;
padding: 0.3em;
border-radius: 50%;
color: var(--text-color);
transition:
color 0.2s,
transform 0.2s;
}
.socials a:hover {
opacity: 0.7;
}
.socials img {
display: block;
width: 1.5em;
height: 1.5em;
filter: brightness(0) saturate(100%) invert(1);
}
.socials a:hover {
color: var(--primary-color);
transform: scale(1.1);
}
</style>
+21 -9
View File
@@ -1,16 +1,28 @@
---
const birthDate = new Date('2004-07-22');
const today = new Date();
const age = Math.floor((today.getTime() - birthDate.getTime()) / 31557600000);
const birthDate = new Date('2004-07-22')
const today = new Date()
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">
<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>
<section class='whoami'>
<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>
</section>
<style>
.whoami p {
margin-inline-start: var(--spacing-md);
}
.whoami p {
margin-inline-start: var(--spacing-md);
}
</style>
+70 -48
View File
@@ -1,62 +1,84 @@
---
import '../styles/global.css';
import '../styles/global.css'
interface Props {
title: string;
description: string;
title: string
description: string
}
const { title, description } = Astro.props;
const { title, description } = Astro.props
const schema = JSON.stringify({
"@context": "https://schema.org",
"@type": "Person",
"name": "Albert",
"alternateName": "skidoodle",
"url": "https://albert.lol/",
"image": "https://albert.lol/static/preview.png",
"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.",
"knowsAbout": ["Web Development", "TypeScript", "Go", "Homelab", "Linux", "Open Source Projects"],
"sameAs": [
"https://github.com/skidoodle",
"https://steamcommunity.com/id/_albert",
"https://discord.com/users/637745537369767936"
],
"worksFor": {
"@type": "Organization",
"name": "Personal Projects / Open Source"
}
});
'@context': 'https://schema.org',
'@type': 'Person',
name: 'Albert',
alternateName: 'skidoodle',
url: 'https://albert.lol/',
image: 'https://albert.lol/static/preview.png',
jobTitle: 'Developer and Tech Enthusiast',
description: description,
knowsAbout: [
'Web Development',
'TypeScript',
'Go',
'Homelab',
'Linux',
'Open Source Projects',
],
sameAs: [
'https://github.com/skidoodle',
'https://steamcommunity.com/id/_albert',
'https://discord.com/users/637745537369767936',
],
worksFor: {
'@type': 'Organization',
name: 'Personal Projects / Open Source',
},
})
---
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{title}</title>
<link rel="icon" href="/favicon.ico" type="image/x-icon">
<meta name="theme-color" content="#bb86fc">
<meta name="description" content={description}>
<!doctype html>
<html lang='en'>
<head>
<meta charset='UTF-8' />
<meta name='viewport' content='width=device-width, initial-scale=1.0' />
<title>{title}</title>
<link rel='icon' href='/favicon.ico' type='image/x-icon' />
<meta name='theme-color' content='#bb86fc' />
<meta name='description' content={description} />
<meta property="og:title" content={title}>
<meta property="og:description" content={description}>
<meta property="og:type" content="website">
<meta property="og:url" content="https://albert.lol/">
<meta property='og:title' content={title} />
<meta property='og:description' content={description} />
<meta property='og:type' content='website' />
<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:title" content={title}>
<meta name="twitter:description" content={description}>
<meta name='twitter:card' content='summary_large_image' />
<meta name='twitter:title' content={title} />
<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 is:inline type="application/ld+json" set:html={schema} />
</head>
<body>
<main>
<slot />
</main>
</body>
<script
is:inline
defer
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>
<slot />
</main>
</body>
</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);
}
}
}
+16 -15
View File
@@ -1,21 +1,22 @@
---
import Layout from '../layouts/layout.astro';
import Nowplaying from '../components/nowplaying.astro';
import Whoami from '../components/whoami.astro';
import Socials from '../components/socials.astro';
import Projects from '../components/projects.astro';
import Setup from '../components/setup.astro';
import Layout from '../layouts/layout.astro'
import Nowplaying from '../components/nowplaying.astro'
import Whoami from '../components/whoami.astro'
import Socials from '../components/socials.astro'
import Projects from '../components/projects.astro'
import Setup from '../components/setup.astro'
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 title = 'albert'
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}>
<h1>[{title}]</h1>
<Nowplaying />
<hr>
<Whoami />
<Socials />
<Projects />
<Setup />
<h1>[{title}]</h1>
<Nowplaying />
<hr />
<Whoami />
<Socials />
<Projects />
<Setup />
</Layout>
-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;
}
a {
color: var(--primary-color);
text-decoration: underline;
text-underline-offset: 2px;
}
a:hover {
color: var(--hover-color);
}
a:focus-visible {
outline: none;
box-shadow: 0 0 0 3px var(--focus-shadow-color);
@@ -64,6 +74,58 @@ hr {
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) {
body {
padding: 1rem;