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

feat: migrate projects & last played song to ATProto

Changed files
+232 -167
lexicons
dev
indexx
public
src
+37
lexicons/dev/indexx/www/project.json
···
+
{
+
"lexicon": 1,
+
"id": "dev.indexx.www.project",
+
"defs": {
+
"main": {
+
"type": "record",
+
"description": "Record containing details about one of my projects.",
+
"key": "tid",
+
"record": {
+
"type": "object",
+
"required": ["name", "description", "url", "dateLabel"],
+
"properties": {
+
"name": {
+
"type": "string",
+
"description": "The name of the project."
+
},
+
"description": {
+
"type": "string",
+
"description": "The description of the project."
+
},
+
"url": {
+
"type": "string",
+
"description": "The URL of the project."
+
},
+
"note": {
+
"type": "string",
+
"description": "OPTIONAL: A note about the project."
+
},
+
"dateLabel": {
+
"type": "string",
+
"description": "The creation date of the project, not in any specific format."
+
}
+
}
+
}
+
}
+
}
+
}
+12 -1
public/style.css
···
padding-bottom: 20px;
overflow-y: scroll;
overflow-x: hidden;
+
+
scrollbar-width: none;
+
-ms-overflow-style: none;
+
}
+
+
#projects-pane ul::-webkit-scrollbar {
+
display: none;
}
.project-card {
···
min-width: 300px;
outline: 2px solid #0000001a;
outline-offset: 5px;
-
transition: opacity 0.2s;
+
transition: opacity 0.2s, left 0.5s ease;
+
}
+
+
body:has(main#page.side) #now-playing {
+
left: 35%;
}
#now-playing img {
+1 -1
src/components/SocialLinks.astro
···
---
<ul
-
class="d-flex mt-4 mb-3"
+
class="d-flex mt-5 mb-3"
style="padding: 0px; width: 50%; margin: auto; justify-content: space-evenly; flex-basis: 33%;"
>
{links.map((link) => <SocialLink {...link} />)}
-100
src/components/islands/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://workers.indexx.dev/misc/lastfm",
-
);
-
if (!res.ok) throw new Error(`HTTP ${res.status}`);
-
-
const json = await res.json();
-
const status = {
-
song: json.track,
-
artist: json.artist,
-
albumArt: json.cover,
-
createdAt: json.createdAt,
-
link: json.url,
-
nowPlaying: !json.createdAt,
-
};
-
setData(status);
-
} catch (err) {
-
console.error("Fetch failed:", err);
-
setError(err.message);
-
}
-
};
-
-
fetchNowPlaying();
-
}, []);
-
-
if (error) return <span>Error: {error}</span>;
-
if (!data) return null;
-
-
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
-
id="now-playing"
-
href={data.link}
-
target="_blank"
-
className={oldStatusClasses}
-
>
-
<img
-
src={data.albumArt}
-
alt={`${data.album} cover`}
-
/>
-
<div
-
style={{
-
display: "flex",
-
flexDirection: "column",
-
gap: "2px",
-
width: "100%",
-
}}
-
>
-
<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>
-
);
-
}
+78
src/components/islands/ProjectsPane.jsx
···
+
import { useEffect, useState } from "react";
+
+
export default function NowPlaying() {
+
const [data, setData] = useState(null);
+
const [error, setError] = useState(null);
+
+
useEffect(() => {
+
const fetchProjectData = async () => {
+
try {
+
const res = await fetch(
+
"https://pds.indexx.dev/xrpc/com.atproto.repo.listRecords?repo=did%3Aplc%3Asfjxpxxyvewb2zlxwoz2vduw&collection=dev.indexx.www.project&limit=100&reverse=false",
+
);
+
if (!res.ok) throw new Error(`HTTP ${res.status}`);
+
+
const data = await res.json();
+
const projects = data.records.map((record) => record.value);
+
+
setData(projects.map((project) => ({
+
title: project.name,
+
description: project.description,
+
date: project.dateLabel,
+
href: project.url,
+
note: project.note ?? "",
+
})));
+
} catch (err) {
+
console.error("Fetch failed:", err);
+
setError(err.message);
+
}
+
};
+
+
fetchProjectData();
+
}, []);
+
+
if (error) return <span>Error: {error}</span>;
+
if (!data) return null;
+
+
return (
+
<section id="projects-pane" data-bs-theme="dark">
+
<h6
+
className="text-muted text-center"
+
style={{ textTransform: "uppercase", letterSpacing: "5px" }}
+
>
+
My Recent Projects
+
</h6>
+
+
<ul className="list-unstyled">
+
{data.map((project) => (
+
<li>
+
<a
+
href={project.href}
+
target="_blank"
+
className="text-reset"
+
>
+
<div
+
className="project-card"
+
style={{ color: "#fff" }}
+
>
+
<small className="text-muted">
+
{project.date}
+
</small>
+
<h4>{project.title}</h4>
+
{project.description}
+
{project.note && (
+
<>
+
<br />
+
<small className="text-muted">
+
{project.note}
+
</small>
+
</>
+
)}
+
</div>
+
</a>
+
</li>
+
))}
+
</ul>
+
</section>
+
);
+
}
+95
src/components/islands/Tealfm.jsx
···
+
import { useEffect, useState } from "react";
+
+
export default function NowPlaying() {
+
const [data, setData] = useState(null);
+
const [error, setError] = useState(null);
+
+
useEffect(() => {
+
const fetchTealfmData = async () => {
+
try {
+
const res = await fetch(
+
"https://pds.indexx.dev/xrpc/com.atproto.repo.listRecords?repo=did%3Aplc%3Asfjxpxxyvewb2zlxwoz2vduw&collection=fm.teal.alpha.feed.play&limit=1&reverse=false",
+
);
+
if (!res.ok) throw new Error(`HTTP ${res.status}`);
+
+
const data = await res.json();
+
const latest = data.records[0].value;
+
+
const status = {
+
song: latest.trackName,
+
artist: latest.artists.map((artist) => artist.artistName)
+
.join(", "),
+
albumArt:
+
`https://coverartarchive.org/release/${latest.releaseMbId}/front-500`,
+
createdAt: latest.playedTime,
+
link: latest.originUrl ?? "",
+
};
+
setData(status);
+
} catch (err) {
+
console.error("Fetch failed:", err);
+
setError(err.message);
+
}
+
};
+
+
fetchTealfmData();
+
}, []);
+
+
if (error) return <span>Error: {error}</span>;
+
if (!data) return null;
+
+
let timeAgo = "";
+
let oldStatusClasses = "";
+
+
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
+
id="now-playing"
+
href={data.link}
+
target="_blank"
+
className={oldStatusClasses}
+
>
+
<img
+
src={data.albumArt}
+
alt={`${data.album} cover`}
+
/>
+
<div
+
style={{
+
display: "flex",
+
flexDirection: "column",
+
gap: "2px",
+
width: "100%",
+
}}
+
>
+
<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",
+
}}
+
>
+
{timeAgo}
+
</div>
+
</div>
+
</a>
+
);
+
}
-56
src/data/projects.ts
···
-
export interface Project {
-
title: string;
-
description: string;
-
date: string;
-
href: string;
-
note?: string;
-
}
-
-
/*
-
This array will go unused for now, it used to be shown but I removed the sidebar since it was ugly
-
*/
-
export const projects: Project[] = [
-
{
-
title: "Aero",
-
description:
-
"A Bluesky bot to make sense of the noise, with responses powered by Gemini, similar to Grok.",
-
date: "October 2025",
-
href: "https://tangled.org/@indexx.dev/aero",
-
},
-
{
-
title: "Echo",
-
description: "A Bluesky reply bot with responses powered by Gemini.",
-
date: "July 2025",
-
href: "https://tangled.org/@indexx.dev/echo",
-
},
-
{
-
title: "Localbox",
-
description:
-
"A Typescript server emulator for Box Critters, a defunct virtual world.",
-
date: "November 2024",
-
href: "https://tangled.org/@indexx.dev/localbox",
-
},
-
{
-
title: "Toonkins Retooned",
-
description:
-
"Looking to relive Toonkins, a defunct virtual world made by Shenanigames, well you're in luck! In this Github organization, you can find: a Typescript, unofficial server remake of the game server, and a decompiled Unity project reassembly of the game (Unity project source coming soon).",
-
date: "October 2024",
-
href: "https://github.com/ToonkinsRetooned/",
-
note:
-
'Note: "Retooned" is purposely spelled wrong, I know how to spell I swear 😭',
-
},
-
{
-
title: "Poly+ Rewrite",
-
description:
-
"A rewrite of Poly+, my quality-of-life browser extension for Polytoria. Built entirely fresh using the WXT extension framework, Typescript, and with added better overall code quality.",
-
date: "since October 2024, never finished",
-
href: "https://github.com/indexxing/PolyPlus-Rewrite",
-
},
-
{
-
title: "Poly+",
-
description:
-
"Poly+ is a quality-of-life Chromium-based extension for Polytoria!",
-
date: "since February 2023, archived",
-
href: "https://github.com/indexxing/PolyPlus/",
-
},
-
];
+9 -9
src/pages/index.astro
···
//import ProjectsPane from "../components/ProjectsPane.astro";
import Status from "../components/islands/Status.jsx";
-
import NowPlaying from "../components/islands/NowPlaying.jsx";
+
import Tealfm from "../components/islands/Tealfm.jsx";
+
import ProjectsPane from "../components/islands/ProjectsPane.jsx";
---
<Layout title="hello" description="insert wave emoji">
<main id="page" class="text-center">
<Header />
<Status client:load />
-
<!--
-
<NowPlaying client:load />
-
-->
+
<Tealfm client:load />
<SocialLinks />
-
<!--
+
<img
+
src="https://ziit.indexx.dev/api/public/badge/cmh6t4rlk0001lz1wau6e8c83/all/month"
+
alt="Ziit Coding Statistics"
+
/>
+
<br />
<a
id="projects-button"
href="#"
···
>
. . .
</a>
-
-->
</main>
-
<!--
-
<ProjectsPane />
+
<ProjectsPane client:load />
<script>
import { initializeProjects } from "../scripts/projects";
initializeProjects();
</script>
-
-->
</Layout>