My personal site hosted @ https://indexx.dev

feat: add Now Playing through last.fm

Index a9f32a89 810e2bc0

+77 -58
package-lock.json
···
"@atproto/api": "^0.15.6",
"@types/react": "^19.1.6",
"@types/react-dom": "^19.1.6",
-
"astro": "^5.7.11",
+
"astro": "^5.9.2",
"fs": "^0.0.1-security",
"marked": "^15.0.11",
"node-cache": "^5.1.2",
···
}
},
"node_modules/@astrojs/compiler": {
-
"version": "2.12.0",
-
"resolved": "https://registry.npmjs.org/@astrojs/compiler/-/compiler-2.12.0.tgz",
-
"integrity": "sha512-7bCjW6tVDpUurQLeKBUN9tZ5kSv5qYrGmcn0sG0IwacL7isR2ZbyyA3AdZ4uxsuUFOS2SlgReTH7wkxO6zpqWA=="
+
"version": "2.12.2",
+
"resolved": "https://registry.npmjs.org/@astrojs/compiler/-/compiler-2.12.2.tgz",
+
"integrity": "sha512-w2zfvhjNCkNMmMMOn5b0J8+OmUaBL1o40ipMvqcG6NRpdC+lKxmTi48DT8Xw0SzJ3AfmeFLB45zXZXtmbsjcgw=="
},
"node_modules/@astrojs/internal-helpers": {
"version": "0.6.1",
···
"integrity": "sha512-l5Pqf6uZu31aG+3Lv8nl/3s4DbUzdlxTWDof4pEpto6GUJNhhCbelVi9dEyurOVyqaelwmS9oSyOWOENSfgo9A=="
},
"node_modules/@astrojs/markdown-remark": {
-
"version": "6.3.1",
-
"resolved": "https://registry.npmjs.org/@astrojs/markdown-remark/-/markdown-remark-6.3.1.tgz",
-
"integrity": "sha512-c5F5gGrkczUaTVgmMW9g1YMJGzOtRvjjhw6IfGuxarM6ct09MpwysP10US729dy07gg8y+ofVifezvP3BNsWZg==",
+
"version": "6.3.2",
+
"resolved": "https://registry.npmjs.org/@astrojs/markdown-remark/-/markdown-remark-6.3.2.tgz",
+
"integrity": "sha512-bO35JbWpVvyKRl7cmSJD822e8YA8ThR/YbUsciWNA7yTcqpIAL2hJDToWP5KcZBWxGT6IOdOkHSXARSNZc4l/Q==",
"dependencies": {
"@astrojs/internal-helpers": "0.6.1",
-
"@astrojs/prism": "3.2.0",
+
"@astrojs/prism": "3.3.0",
"github-slugger": "^2.0.0",
"hast-util-from-html": "^2.0.3",
"hast-util-to-text": "^4.0.2",
···
"rehype-stringify": "^10.0.1",
"remark-gfm": "^4.0.1",
"remark-parse": "^11.0.0",
-
"remark-rehype": "^11.1.1",
+
"remark-rehype": "^11.1.2",
"remark-smartypants": "^3.0.2",
-
"shiki": "^3.0.0",
+
"shiki": "^3.2.1",
"smol-toml": "^1.3.1",
"unified": "^11.0.5",
"unist-util-remove-position": "^5.0.0",
···
}
},
"node_modules/@astrojs/prism": {
-
"version": "3.2.0",
-
"resolved": "https://registry.npmjs.org/@astrojs/prism/-/prism-3.2.0.tgz",
-
"integrity": "sha512-GilTHKGCW6HMq7y3BUv9Ac7GMe/MO9gi9GW62GzKtth0SwukCu/qp2wLiGpEujhY+VVhaG9v7kv/5vFzvf4NYw==",
+
"version": "3.3.0",
+
"resolved": "https://registry.npmjs.org/@astrojs/prism/-/prism-3.3.0.tgz",
+
"integrity": "sha512-q8VwfU/fDZNoDOf+r7jUnMC2//H2l0TuQ6FkGJL8vD8nw/q5KiL3DS1KKBI3QhI9UQhpJ5dc7AtqfbXWuOgLCQ==",
"dependencies": {
-
"prismjs": "^1.29.0"
+
"prismjs": "^1.30.0"
},
"engines": {
-
"node": "^18.17.1 || ^20.3.0 || >=22.0.0"
+
"node": "18.20.8 || ^20.3.0 || >=22.0.0"
}
},
"node_modules/@astrojs/react": {
···
}
},
"node_modules/@astrojs/telemetry": {
-
"version": "3.2.1",
-
"resolved": "https://registry.npmjs.org/@astrojs/telemetry/-/telemetry-3.2.1.tgz",
-
"integrity": "sha512-SSVM820Jqc6wjsn7qYfV9qfeQvePtVc1nSofhyap7l0/iakUKywj3hfy3UJAOV4sGV4Q/u450RD4AaCaFvNPlg==",
+
"version": "3.3.0",
+
"resolved": "https://registry.npmjs.org/@astrojs/telemetry/-/telemetry-3.3.0.tgz",
+
"integrity": "sha512-UFBgfeldP06qu6khs/yY+q1cDAaArM2/7AEIqQ9Cuvf7B1hNLq0xDrZkct+QoIGyjq56y8IaE2I3CTvG99mlhQ==",
"dependencies": {
"ci-info": "^4.2.0",
"debug": "^4.4.0",
···
"which-pm-runs": "^1.1.0"
},
"engines": {
-
"node": "^18.17.1 || ^20.3.0 || >=22.0.0"
+
"node": "18.20.8 || ^20.3.0 || >=22.0.0"
}
},
"node_modules/@atproto/api": {
···
},
"node_modules/@shikijs/core": {
-
"version": "3.4.0",
-
"resolved": "https://registry.npmjs.org/@shikijs/core/-/core-3.4.0.tgz",
-
"integrity": "sha512-0YOzTSRDn/IAfQWtK791gs1u8v87HNGToU6IwcA3K7nPoVOrS2Dh6X6A6YfXgPTSkTwR5y6myk0MnI0htjnwrA==",
+
"version": "3.6.0",
+
"resolved": "https://registry.npmjs.org/@shikijs/core/-/core-3.6.0.tgz",
+
"integrity": "sha512-9By7Xb3olEX0o6UeJyPLI1PE1scC4d3wcVepvtv2xbuN9/IThYN4Wcwh24rcFeASzPam11MCq8yQpwwzCgSBRw==",
"dependencies": {
-
"@shikijs/types": "3.4.0",
+
"@shikijs/types": "3.6.0",
"@shikijs/vscode-textmate": "^10.0.2",
"@types/hast": "^3.0.4",
"hast-util-to-html": "^9.0.5"
},
"node_modules/@shikijs/engine-javascript": {
-
"version": "3.4.0",
-
"resolved": "https://registry.npmjs.org/@shikijs/engine-javascript/-/engine-javascript-3.4.0.tgz",
-
"integrity": "sha512-1ywDoe+z/TPQKj9Jw0eU61B003J9DqUFRfH+DVSzdwPUFhR7yOmfyLzUrFz0yw8JxFg/NgzXoQyyykXgO21n5Q==",
+
"version": "3.6.0",
+
"resolved": "https://registry.npmjs.org/@shikijs/engine-javascript/-/engine-javascript-3.6.0.tgz",
+
"integrity": "sha512-7YnLhZG/TU05IHMG14QaLvTW/9WiK8SEYafceccHUSXs2Qr5vJibUwsDfXDLmRi0zHdzsxrGKpSX6hnqe0k8nA==",
"dependencies": {
-
"@shikijs/types": "3.4.0",
+
"@shikijs/types": "3.6.0",
"@shikijs/vscode-textmate": "^10.0.2",
"oniguruma-to-es": "^4.3.3"
},
"node_modules/@shikijs/engine-oniguruma": {
-
"version": "3.4.0",
-
"resolved": "https://registry.npmjs.org/@shikijs/engine-oniguruma/-/engine-oniguruma-3.4.0.tgz",
-
"integrity": "sha512-zwcWlZ4OQuJ/+1t32ClTtyTU1AiDkK1lhtviRWoq/hFqPjCNyLj22bIg9rB7BfoZKOEOfrsGz7No33BPCf+WlQ==",
+
"version": "3.6.0",
+
"resolved": "https://registry.npmjs.org/@shikijs/engine-oniguruma/-/engine-oniguruma-3.6.0.tgz",
+
"integrity": "sha512-nmOhIZ9yT3Grd+2plmW/d8+vZ2pcQmo/UnVwXMUXAKTXdi+LK0S08Ancrz5tQQPkxvjBalpMW2aKvwXfelauvA==",
"dependencies": {
-
"@shikijs/types": "3.4.0",
+
"@shikijs/types": "3.6.0",
"@shikijs/vscode-textmate": "^10.0.2"
},
"node_modules/@shikijs/langs": {
-
"version": "3.4.0",
-
"resolved": "https://registry.npmjs.org/@shikijs/langs/-/langs-3.4.0.tgz",
-
"integrity": "sha512-bQkR+8LllaM2duU9BBRQU0GqFTx7TuF5kKlw/7uiGKoK140n1xlLAwCgXwSxAjJ7Htk9tXTFwnnsJTCU5nDPXQ==",
+
"version": "3.6.0",
+
"resolved": "https://registry.npmjs.org/@shikijs/langs/-/langs-3.6.0.tgz",
+
"integrity": "sha512-IdZkQJaLBu1LCYCwkr30hNuSDfllOT8RWYVZK1tD2J03DkiagYKRxj/pDSl8Didml3xxuyzUjgtioInwEQM/TA==",
"dependencies": {
-
"@shikijs/types": "3.4.0"
+
"@shikijs/types": "3.6.0"
},
"node_modules/@shikijs/themes": {
-
"version": "3.4.0",
-
"resolved": "https://registry.npmjs.org/@shikijs/themes/-/themes-3.4.0.tgz",
-
"integrity": "sha512-YPP4PKNFcFGLxItpbU0ZW1Osyuk8AyZ24YEFaq04CFsuCbcqydMvMUTi40V2dkc0qs1U2uZFrnU6s5zI6IH+uA==",
+
"version": "3.6.0",
+
"resolved": "https://registry.npmjs.org/@shikijs/themes/-/themes-3.6.0.tgz",
+
"integrity": "sha512-Fq2j4nWr1DF4drvmhqKq8x5vVQ27VncF8XZMBuHuQMZvUSS3NBgpqfwz/FoGe36+W6PvniZ1yDlg2d4kmYDU6w==",
"dependencies": {
-
"@shikijs/types": "3.4.0"
+
"@shikijs/types": "3.6.0"
},
"node_modules/@shikijs/types": {
-
"version": "3.4.0",
-
"resolved": "https://registry.npmjs.org/@shikijs/types/-/types-3.4.0.tgz",
-
"integrity": "sha512-EUT/0lGiE//7j5N/yTMNMT3eCWNcHJLrRKxT0NDXWIfdfSmFJKfPX7nMmRBrQnWboAzIsUziCThrYMMhjbMS1A==",
+
"version": "3.6.0",
+
"resolved": "https://registry.npmjs.org/@shikijs/types/-/types-3.6.0.tgz",
+
"integrity": "sha512-cLWFiToxYu0aAzJqhXTQsFiJRTFDAGl93IrMSBNaGSzs7ixkLfdG6pH11HipuWFGW5vyx4X47W8HDQ7eSrmBUg==",
"dependencies": {
"@shikijs/vscode-textmate": "^10.0.2",
"@types/hast": "^3.0.4"
···
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.7.tgz",
"integrity": "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ=="
+
},
+
"node_modules/@types/fontkit": {
+
"version": "2.0.8",
+
"resolved": "https://registry.npmjs.org/@types/fontkit/-/fontkit-2.0.8.tgz",
+
"integrity": "sha512-wN+8bYxIpJf+5oZdrdtaX04qUuWHcKxcDEgRS9Qm9ZClSHjzEn13SxUC+5eRM+4yXIeTYk8mTzLAWGF64847ew==",
+
"dependencies": {
+
"@types/node": "*"
+
}
},
"node_modules/@types/hast": {
"version": "3.0.4",
···
},
"node_modules/astro": {
-
"version": "5.7.11",
-
"resolved": "https://registry.npmjs.org/astro/-/astro-5.7.11.tgz",
-
"integrity": "sha512-9qRVwp8pue3isddLBnTexJsmKFpmms9Fo7Ss+3yrC0aINvbHKpD7q6qf52BtfQEk2xJgyx3SQy3dUsuD90sEqQ==",
+
"version": "5.9.2",
+
"resolved": "https://registry.npmjs.org/astro/-/astro-5.9.2.tgz",
+
"integrity": "sha512-K/zZlQOWMpamfLDOls5jvG7lrsjH1gkk3ESRZyZDCkVBtKHMF4LbjwCicm/iBb3mX3V/PerqRYzLbOy3/4JLCQ==",
"dependencies": {
-
"@astrojs/compiler": "^2.11.0",
+
"@astrojs/compiler": "^2.12.2",
"@astrojs/internal-helpers": "0.6.1",
-
"@astrojs/markdown-remark": "6.3.1",
-
"@astrojs/telemetry": "3.2.1",
+
"@astrojs/markdown-remark": "6.3.2",
+
"@astrojs/telemetry": "3.3.0",
"@capsizecss/unpack": "^2.4.0",
"@oslojs/encoding": "^1.1.0",
"@rollup/pluginutils": "^5.1.4",
···
"esbuild": "^0.25.0",
"estree-walker": "^3.0.3",
"flattie": "^1.1.1",
+
"fontace": "~0.3.0",
"github-slugger": "^2.0.0",
"html-escaper": "3.0.3",
"http-cache-semantics": "^4.1.1",
+
"import-meta-resolve": "^4.1.0",
"js-yaml": "^4.1.0",
"kleur": "^4.1.5",
"magic-string": "^0.30.17",
···
"astro": "astro.js"
},
"engines": {
-
"node": "^18.17.1 || ^20.3.0 || >=22.0.0",
+
"node": "18.20.8 || ^20.3.0 || >=22.0.0",
"npm": ">=9.6.5",
"pnpm": ">=7.1.0"
},
···
"integrity": "sha512-9UbaD6XdAL97+k/n+N7JwX46K/M6Zc6KcFYskrYL8wbBV/Uyk0CTAMY0VT+qiK5PM7AIc9aTWYtq65U7T+aCNQ==",
"engines": {
"node": ">=8"
+
}
+
},
+
"node_modules/fontace": {
+
"version": "0.3.0",
+
"resolved": "https://registry.npmjs.org/fontace/-/fontace-0.3.0.tgz",
+
"integrity": "sha512-czoqATrcnxgWb/nAkfyIrRp6Q8biYj7nGnL6zfhTcX+JKKpWHFBnb8uNMw/kZr7u++3Y3wYSYoZgHkCcsuBpBg==",
+
"dependencies": {
+
"@types/fontkit": "^2.0.8",
+
"fontkit": "^2.0.4"
},
"node_modules/fontkit": {
···
},
"node_modules/shiki": {
-
"version": "3.4.0",
-
"resolved": "https://registry.npmjs.org/shiki/-/shiki-3.4.0.tgz",
-
"integrity": "sha512-Ni80XHcqhOEXv5mmDAvf5p6PAJqbUc/RzFeaOqk+zP5DLvTPS3j0ckvA+MI87qoxTQ5RGJDVTbdl/ENLSyyAnQ==",
+
"version": "3.6.0",
+
"resolved": "https://registry.npmjs.org/shiki/-/shiki-3.6.0.tgz",
+
"integrity": "sha512-tKn/Y0MGBTffQoklaATXmTqDU02zx8NYBGQ+F6gy87/YjKbizcLd+Cybh/0ZtOBX9r1NEnAy/GTRDKtOsc1L9w==",
"dependencies": {
-
"@shikijs/core": "3.4.0",
-
"@shikijs/engine-javascript": "3.4.0",
-
"@shikijs/engine-oniguruma": "3.4.0",
-
"@shikijs/langs": "3.4.0",
-
"@shikijs/themes": "3.4.0",
-
"@shikijs/types": "3.4.0",
+
"@shikijs/core": "3.6.0",
+
"@shikijs/engine-javascript": "3.6.0",
+
"@shikijs/engine-oniguruma": "3.6.0",
+
"@shikijs/langs": "3.6.0",
+
"@shikijs/themes": "3.6.0",
+
"@shikijs/types": "3.6.0",
"@shikijs/vscode-textmate": "^10.0.2",
"@types/hast": "^3.0.4"
+1 -1
package.json
···
"@atproto/api": "^0.15.6",
"@types/react": "^19.1.6",
"@types/react-dom": "^19.1.6",
-
"astro": "^5.7.11",
+
"astro": "^5.9.2",
"fs": "^0.0.1-security",
"marked": "^15.0.11",
"node-cache": "^5.1.2",
+2
robots.txt
···
+
User-agent: *
+
Disallow:
+129
src/components/NowPlaying.jsx
···
+
import { useEffect, useState } from "react";
+
+
export default function NowPlaying() {
+
const [data, setData] = useState(null);
+
const [error, setError] = useState(null);
+
+
useEffect(() => {
+
const fetchNowPlaying = async () => {
+
try {
+
const res = await fetch(
+
"https://lastfm-last-played.biancarosa.com.br/Index_Card/latest-song",
+
);
+
if (!res.ok) throw new Error(`HTTP ${res.status}`);
+
+
const json = await res.json();
+
const track = json.track;
+
const isNowPlaying = track["@attr"]?.nowplaying === "true";
+
+
const status = {
+
song: track.name,
+
artist: track.artist["#text"],
+
album: track.album["#text"],
+
createdAt: track.date
+
? new Date(track.date.uts * 1000).toISOString()
+
: null,
+
link: track.url,
+
nowPlaying: isNowPlaying,
+
albumArt: track.image.find((img) =>
+
img.size === "medium"
+
)["#text"],
+
};
+
setData(status);
+
} catch (err) {
+
console.error("Fetch failed:", err);
+
setError(err.message);
+
}
+
};
+
+
fetchNowPlaying();
+
}, []);
+
+
if (error) return <span>Error: {error}</span>;
+
if (!data) return <span>Loading now playing...</span>;
+
+
let timeAgo = "";
+
let oldStatusClasses = "";
+
+
if (!data.nowPlaying && data.createdAt) {
+
const date = new Date(data.createdAt);
+
const now = new Date();
+
const diff = now.getTime() - date.getTime();
+
+
const minutes = Math.floor(diff / 60000);
+
const hours = Math.floor(minutes / 60);
+
const days = Math.floor(hours / 24);
+
+
if (days > 0) timeAgo = `${days} days ago`;
+
else if (hours > 0) timeAgo = `${hours} hours ago`;
+
else if (minutes > 0) timeAgo = `${minutes} minutes ago`;
+
else timeAgo = "just now";
+
+
oldStatusClasses = days > 3
+
? "opacity-75 text-decoration-line-through"
+
: "";
+
}
+
+
return (
+
<a
+
href={data.link}
+
target="_blank"
+
style={{
+
position: "fixed",
+
bottom: "20px",
+
left: "20px",
+
display: "flex",
+
alignItems: "center",
+
gap: "12px",
+
padding: "8px",
+
backgroundColor: "#1e1d2d",
+
borderRadius: "8px",
+
boxShadow: "0 2px 6px rgba(0, 0, 0, 0.1)",
+
textDecoration: "none",
+
color: "#000",
+
maxWidth: "400px",
+
transition: "opacity 0.2s",
+
textAlign: "left",
+
}}
+
className={oldStatusClasses}
+
>
+
<img
+
src={data.albumArt}
+
alt={`${data.album} cover`}
+
style={{
+
width: "64px",
+
height: "64px",
+
borderRadius: "4px",
+
objectFit: "cover",
+
}}
+
/>
+
<div
+
style={{
+
display: "flex",
+
flexDirection: "column",
+
gap: "2px",
+
}}
+
>
+
<div style={{ fontWeight: "bold" }}>{data.song}</div>
+
<div style={{ fontSize: "0.9em", marginTop: "-5px" }}>
+
{data.artist}
+
</div>
+
<div
+
style={{
+
fontSize: "0.8em",
+
opacity: 0.7,
+
marginTop: "-5px",
+
}}
+
>
+
{data.nowPlaying
+
? (
+
<span style={{ color: "#22c55e" }}>
+
▶ Now Playing
+
</span>
+
)
+
: timeAgo}
+
</div>
+
</div>
+
</a>
+
);
+
}
-1
src/components/Status.jsx
···
const [error, setError] = useState(null);
useEffect(() => {
-
console.log("gettin dat damn sattus directly from bsky");
const fetchStatus = async () => {
if (!actorDid) {
setError("Configuration error: Actor DID is missing.");
src/components/status.astro

This is a binary file and will not be displayed.

+2
src/pages/index.astro
···
import Header from "../components/Header.astro";
import SocialLinks from "../components/SocialLinks.astro";
import Status from '../components/Status.jsx';
+
import NowPlaying from "../components/NowPlaying.jsx";
import ProjectsPane from "../components/ProjectsPane.astro";
---
···
<main id="page" class="text-center">
<Header />
<Status client:load />
+
<NowPlaying client:load />
<SocialLinks />
<a
id="projects-button"