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

Compare changes

Choose any two refs to compare.

+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."
+
}
+
}
+
}
+
}
+
}
+
}
+34 -50
public/style.css
···
/* ---------------------------------------------------
VARIABLES & PROPERTIES
--------------------------------------------------- */
-
@import url("https://fonts.googleapis.com/css2?family=Nunito+Sans:ital,opsz,wght@0,6..12,200..1000;1,6..12,200..1000&display=swap");
:root {
--line: transparent;
--line-active: #ffffff;
--bg-gradient: linear-gradient(
45deg,
-
rgb(0, 59, 74),
rgb(0, 35, 44),
rgb(0, 29, 36)
);
}
@property --animatedBorder {
···
font-family: "Nunito Sans", sans-serif;
width: 100%;
height: 100%;
-
background: var(--bg-gradient);
}
a {
···
h1 {
color: #fff;
margin-bottom: 0;
-
font-weight: 1000;
font-size: 4rem;
}
···
}
/* ---------------------------------------------------
-
CIRCLE HOVER LINK
-
--------------------------------------------------- */
-
.circle-hover {
-
display: inline-block;
-
position: relative;
-
margin: 0 var(--spacing, 0px);
-
flex: 1;
-
min-width: 0;
-
transition: margin 0.25s;
-
}
-
-
.circle-hover:hover {
-
--spacing: 4px;
-
--stroke: var(--line-active);
-
--stroke-delay: 0.1s;
-
--offset: 180px;
-
color: #fff !important;
-
}
-
-
.circle-hover svg {
-
width: 76px;
-
height: 40px;
-
position: absolute;
-
left: 50%;
-
bottom: 0;
-
transform: translate(-50%, 7px) translateZ(0);
-
fill: none;
-
stroke: var(--stroke, var(--line));
-
stroke-linecap: round;
-
stroke-width: 2px;
-
stroke-dasharray: var(--offset, 69px) 278px;
-
stroke-dashoffset: 361px;
-
transition: stroke 0.25s ease var(--stroke-delay, 0s), stroke-dasharray 0.35s;
-
}
-
-
/* ---------------------------------------------------
PROJECTS SIDE PANE
--------------------------------------------------- */
#projects-pane {
···
padding-bottom: 20px;
overflow-y: scroll;
overflow-x: hidden;
}
.project-card {
···
#now-playing {
position: fixed;
bottom: 20px;
-
left: 50%;
-
transform: translateX(-50%);
display: flex;
align-items: center;
gap: 12px;
padding: 8px;
-
background-color: rgba(8, 61, 74, 0.42);
border-radius: 8px;
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1);
-
color: #000;
-
text-align: right;
text-decoration: none;
-
max-width: 400px;
-
min-width: 300px;
-
outline: 2px solid #0000001a;
-
outline-offset: 5px;
-
transition: opacity 0.2s;
}
#now-playing img {
···
/* ---------------------------------------------------
VARIABLES & PROPERTIES
--------------------------------------------------- */
+
@import url("https://fonts.googleapis.com/css2?family=Jua&family=Nunito+Sans:ital,opsz,wght@0,6..12,200..1000;1,6..12,200..1000&display=swap");
:root {
--line: transparent;
--line-active: #ffffff;
--bg-gradient: linear-gradient(
45deg,
+
rgb(0, 29, 36),
rgb(0, 35, 44),
rgb(0, 29, 36)
);
+
--ring-center-x: 50%;
+
--ring-pattern: repeating-radial-gradient(
+
circle at var(--ring-center-x) center,
+
transparent 0px,
+
transparent 113px,
+
rgba(0, 27, 34, 0.3) 203px,
+
rgba(0, 74, 92, 0.136) 206px
+
);
+
+
transition: --ring-center-x 0.5s ease;
}
@property --animatedBorder {
···
font-family: "Nunito Sans", sans-serif;
width: 100%;
height: 100%;
+
background: var(--ring-pattern), var(--bg-gradient);
+
background-blend-mode: normal;
+
}
+
+
:root:has(main#page.side) {
+
--ring-center-x: 35%;
}
a {
···
h1 {
color: #fff;
margin-bottom: 0;
+
font-family: "Jua", sans-serif;
+
font-weight: 400;
+
font-style: normal;
font-size: 4rem;
}
···
}
/* ---------------------------------------------------
PROJECTS SIDE PANE
--------------------------------------------------- */
#projects-pane {
···
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 {
···
#now-playing {
position: fixed;
bottom: 20px;
+
left: 20px;
display: flex;
align-items: center;
gap: 12px;
padding: 8px;
+
background-color: rgba(0, 0, 0, 0.271);
border-radius: 8px;
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1);
+
color: #fff;
+
text-align: left;
text-decoration: none;
+
width: 300px;
+
transition: opacity 0.2s, left 0.5s ease;
}
#now-playing img {
+1 -14
src/assets/crown.svg
···
-
<svg
-
width="24"
-
height="24"
-
viewBox="0 0 24 24"
-
fill="none"
-
xmlns="http://www.w3.org/2000/svg"
-
>
-
<path
-
fill-rule="evenodd"
-
clip-rule="evenodd"
-
d="M2.5 6.09143L7.21997 10.8114L12.0005 6.03088L16.7811 10.8114L21.5 6.09245V14.9691C21.5 16.626 20.1569 17.9691 18.5 17.9691H5.5C3.84314 17.9691 2.5 16.626 2.5 14.9691V6.09143ZM19.5 10.9087V14.9691C19.5 15.5214 19.0523 15.9691 18.5 15.9691H5.5C4.94771 15.9691 4.5 15.5214 4.5 14.9691V10.9077L7.21997 13.6277L12.0005 8.84717L16.7811 13.6277L19.5 10.9087Z"
-
fill="currentColor"
-
/>
-
</svg>
···
+
<svg xmlns="http://www.w3.org/2000/svg" width="512" height="512" viewBox="0 0 24 24"><path fill="#ffd700" d="M5 16L3 5l5.5 5L12 4l3.5 6L21 5l-2 11zm14 3c0 .6-.4 1-1 1H6c-.6 0-1-.4-1-1v-1h14z"/></svg>
-27
src/components/Header.astro
···
-
<section id="header">
-
<h1 class="animated-border unselectable spin">
-
<svg
-
width="24"
-
height="24"
-
viewBox="0 0 24 24"
-
fill="none"
-
xmlns="http://www.w3.org/2000/svg"
-
style="position: absolute; top: -5px; margin-left: -2px; transform: rotate(-10deg); color: gold;"
-
>
-
<path
-
fill-rule="evenodd"
-
clip-rule="evenodd"
-
d="M2.5 6.09143L7.21997 10.8114L12.0005 6.03088L16.7811 10.8114L21.5 6.09245V14.9691C21.5 16.626 20.1569 17.9691 18.5 17.9691H5.5C3.84314 17.9691 2.5 16.626 2.5 14.9691V6.09143ZM19.5 10.9087V14.9691C19.5 15.5214 19.0523 15.9691 18.5 15.9691H5.5C4.94771 15.9691 4.5 15.5214 4.5 14.9691V10.9077L7.21997 13.6277L12.0005 8.84717L16.7811 13.6277L19.5 10.9087Z"
-
fill="currentColor"></path>
-
</svg>
-
<span style="position: relative; z-index: 2;">index</span>
-
</h1>
-
<span style="display: block; margin-top: -5px;"
-
>hey, i'm index. have a great day ๐Ÿ‘‹</span
-
>
-
<small
-
style="display: block; margin-top: -5px;margin-bottom:10px;color: rgb(0, 84, 106);font-size: 0.9rem;"
-
>
-
he/him
-
</small>
-
</section>
···
-27
src/components/ProjectCard.astro
···
-
---
-
interface Props {
-
title: string;
-
description: string;
-
date: string;
-
href: string;
-
note?: string;
-
}
-
-
const { title, description, date, href, note } = Astro.props;
-
---
-
-
<a href={href} target="_blank" class="text-reset">
-
<div class="project-card" style="color: #fff;">
-
<small class="text-muted">{date}</small>
-
<h4>{title}</h4>
-
{description}
-
{
-
note && (
-
<>
-
<br />
-
<small class="text-muted">{note}</small>
-
</>
-
)
-
}
-
</div>
-
</a>
···
-19
src/components/ProjectsPane.astro
···
-
---
-
import { projects } from "../data/projects";
-
import ProjectCard from "./ProjectCard.astro";
-
---
-
-
<section id="projects-pane" data-bs-theme="dark">
-
<h6
-
class="text-muted text-center"
-
style="text-transform: uppercase; letter-spacing: 5px;"
-
>
-
(SOME OF) MY PROJECTS
-
</h6>
-
-
<ul class="list-unstyled">
-
<li>
-
{projects.map((project) => <ProjectCard {...project} />)}
-
</li>
-
</ul>
-
</section>
···
-43
src/components/SocialLink.astro
···
-
---
-
import type { JSX } from "astro/jsx-runtime";
-
-
interface Props {
-
icon?: any;
-
name: string;
-
href: string;
-
tooltip?: {
-
title: string;
-
placement?: string;
-
};
-
rel?: string;
-
}
-
-
const { icon, name, href, tooltip, rel } = Astro.props;
-
-
const Icon = icon;
-
---
-
-
<a
-
rel={rel}
-
href={href}
-
target="_blank"
-
{...tooltip && {
-
"data-bs-toggle": "tooltip",
-
"data-bs-title": tooltip.title,
-
"data-bs-placement": tooltip.placement || "bottom",
-
}}
-
>
-
<span class="icon-hover">
-
{Icon ? <Icon style="width: 35px; height: 35px;" /> : name}
-
</span>
-
</a>
-
-
<style>
-
.icon-hover {
-
display: inline-block;
-
transition: transform 0.2s ease;
-
}
-
a:hover .icon-hover {
-
transform: scale(1.12);
-
}
-
</style>
···
-35
src/components/SocialLinks.astro
···
-
---
-
import Bluesky from "./icons/Bluesky.astro";
-
import Discord from "./icons/Discord.astro";
-
import SocialLink from "./SocialLink.astro";
-
import Tangled from "./icons/Tangled.astro";
-
-
const links = [
-
{
-
icon: Bluesky,
-
name: "Bluesky",
-
href: "https://bsky.app/profile/did:plc:sfjxpxxyvewb2zlxwoz2vduw",
-
},
-
{
-
icon: Discord,
-
name: "Discord",
-
href: "https://discord.com/users/589386198011871233",
-
tooltip: {
-
title: "index.lua",
-
placement: "top",
-
},
-
},
-
{
-
icon: Tangled,
-
name: "Tangled",
-
href: "https://tangled.org/@indexx.dev",
-
},
-
];
-
---
-
-
<ul
-
class="d-flex mt-4 mb-3"
-
style="padding: 0px; width: 50%; margin: auto; justify-content: space-evenly; flex-basis: 33%;"
-
>
-
{links.map((link) => <SocialLink {...link} />)}
-
</ul>
···
+27
src/components/astro/Header.astro
···
···
+
<section id="header">
+
<h1 class="animated-border unselectable spin">
+
<svg
+
xmlns="http://www.w3.org/2000/svg"
+
width="24"
+
height="24"
+
viewBox="0 0 24 24"
+
style="position: absolute; top: -5px; margin-left: -7px; transform: rotate(-10deg); z-index: 2000;"
+
><path
+
fill="gold"
+
d="M5 16 3 5l5.5 5L12 4l3.5 6L21 5l-2 11zm14 3c0 .6-.4 1-1 1H6c-.6 0-1-.4-1-1v-1h14z"
+
></path></svg
+
>
+
<span style="position: relative; z-index: 2;">index</span>
+
</h1>
+
<span style="display: block; margin-top: -5px;"
+
>hey, i'm index. have a great day ๐Ÿ‘‹</span
+
>
+
<span style="display: block; margin-top: -5px;"
+
>i do stuff in typescript sometimes</span
+
>
+
<small
+
style="display: block; margin-top: -5px;margin-bottom:10px;color: rgb(0, 84, 106);font-size: 0.9rem;"
+
>
+
he/him
+
</small>
+
</section>
+27
src/components/astro/ProjectCard.astro
···
···
+
---
+
interface Props {
+
title: string;
+
description: string;
+
date: string;
+
href: string;
+
note?: string;
+
}
+
+
const { title, description, date, href, note } = Astro.props;
+
---
+
+
<a href={href} target="_blank" class="text-reset">
+
<div class="project-card" style="color: #fff;">
+
<small class="text-muted">{date}</small>
+
<h4>{title}</h4>
+
{description}
+
{
+
note && (
+
<>
+
<br />
+
<small class="text-muted">{note}</small>
+
</>
+
)
+
}
+
</div>
+
</a>
+43
src/components/astro/SocialLink.astro
···
···
+
---
+
import type { JSX } from "astro/jsx-runtime";
+
+
interface Props {
+
icon?: any;
+
name: string;
+
href: string;
+
tooltip?: {
+
title: string;
+
placement?: string;
+
};
+
rel?: string;
+
}
+
+
const { icon, name, href, tooltip, rel } = Astro.props;
+
+
const Icon = icon;
+
---
+
+
<a
+
rel={rel}
+
href={href}
+
target="_blank"
+
{...tooltip && {
+
"data-bs-toggle": "tooltip",
+
"data-bs-title": tooltip.title,
+
"data-bs-placement": tooltip.placement || "bottom",
+
}}
+
>
+
<span class="icon-hover">
+
{Icon ? <Icon style="width: 35px; height: 35px;" /> : name}
+
</span>
+
</a>
+
+
<style>
+
.icon-hover {
+
display: inline-block;
+
transition: transform 0.2s ease;
+
}
+
a:hover .icon-hover {
+
transform: scale(1.12);
+
}
+
</style>
+35
src/components/astro/SocialLinks.astro
···
···
+
---
+
import Bluesky from "../icons/Bluesky.astro";
+
import Discord from "../icons/Discord.astro";
+
import SocialLink from "./SocialLink.astro";
+
import Tangled from "../icons/Tangled.astro";
+
+
const links = [
+
{
+
icon: Bluesky,
+
name: "Bluesky",
+
href: "https://bsky.app/profile/did:plc:sfjxpxxyvewb2zlxwoz2vduw",
+
},
+
{
+
icon: Discord,
+
name: "Discord",
+
href: "https://discord.com/users/589386198011871233",
+
tooltip: {
+
title: "index.lua",
+
placement: "top",
+
},
+
},
+
{
+
icon: Tangled,
+
name: "Tangled",
+
href: "https://tangled.org/@indexx.dev",
+
},
+
];
+
---
+
+
<ul
+
class="d-flex mt-3 mb-3"
+
style="padding: 0px; width: 50%; margin: auto; justify-content: center; gap: 20px;"
+
>
+
{links.map((link) => <SocialLink {...link} />)}
+
</ul>
-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>
-
);
-
}
···
-98
src/components/islands/Status.jsx
···
-
import { useEffect, useState } from "react";
-
const actorDid = import.meta.env.PUBLIC_ACTOR_DID;
-
-
if (!actorDid) {
-
console.error(
-
"PUBLIC_ACTOR_DID is not defined. Please check your .env file.",
-
);
-
}
-
-
export default function Status() {
-
const [data, setData] = useState(null);
-
const [error, setError] = useState(null);
-
-
useEffect(() => {
-
const fetchStatus = async () => {
-
if (!actorDid) {
-
setError("Configuration error: Actor DID is missing.");
-
return;
-
}
-
try {
-
const res = await fetch(
-
`https://public.api.bsky.app/xrpc/app.bsky.feed.getAuthorFeed?actor=${actorDid}&limit=1&filter=posts_no_replies`,
-
);
-
if (!res.ok) throw new Error(`HTTP ${res.status}`);
-
-
const json = await res.json();
-
const latestPost = json.feed[0].post;
-
-
const status = {
-
text: latestPost.record.text,
-
createdAt: latestPost.record.createdAt,
-
link: `https://bsky.app/profile/${actorDid}/post/${
-
latestPost.uri.split("/")[4]
-
}`,
-
};
-
setData(status);
-
} catch (err) {
-
console.error("Fetch failed:", err);
-
setError(err.message);
-
}
-
};
-
-
fetchStatus();
-
}, [actorDid]);
-
-
if (error) {
-
return (
-
<span
-
className="badge bg-dark"
-
style={{ color: "#595959 !important" }}
-
>
-
Error: {error}
-
</span>
-
);
-
}
-
if (!data) {
-
return (
-
<span
-
className="badge bg-dark"
-
style={{ color: "#595959 !important" }}
-
>
-
Loading status...
-
</span>
-
);
-
}
-
-
const date = new Date(data.createdAt);
-
const now = new Date();
-
const diff = now.getTime() - date.getTime();
-
-
const days = Math.floor(diff / (1000 * 60 * 60 * 24)); // Calculate days
-
-
if (days >= 2) {
-
return null; // Don't render the component if it's been 2 or more days
-
}
-
-
const minutes = Math.floor(diff / 60000);
-
const hours = Math.floor(minutes / 60);
-
-
let timeAgo = "just now";
-
if (days > 0) timeAgo = `${days} days ago`;
-
else if (hours > 0) timeAgo = `${hours} hours ago`;
-
else if (minutes > 0) timeAgo = `${minutes} minutes ago`;
-
-
const oldStatusClasses = days >= 1
-
? "opacity-75 text-decoration-line-through"
-
: "";
-
-
return (
-
<a
-
href={data.link}
-
target="_blank"
-
className={`badge bg-white ${oldStatusClasses}`}
-
>
-
Index is.. "{data.text}", {timeAgo}
-
</a>
-
);
-
}
···
+80
src/components/jsx/AudioVisualizer.jsx
···
···
+
import React from "react";
+
+
const BAR_COUNT = 15;
+
const BAR_ANIMATION_DELAYS = [
+
"0.3s",
+
"0.7s",
+
"0.1s",
+
"0.9s",
+
"0.4s",
+
"0.2s",
+
"0.8s",
+
"0.5s",
+
"0s",
+
"0.6s",
+
"0.3s",
+
"0.9s",
+
"0.1s",
+
"0.7s",
+
"0.4s",
+
];
+
+
export default function AudioVisualizer({ isSilent = false }) {
+
// Define the base animation style
+
const activeAnimation = "wave 2.5s ease-in-out infinite";
+
+
// Define the style for the silent state (dots)
+
const silentStyle = {
+
animation: "none",
+
height: "5px",
+
};
+
+
return (
+
<div className="visualizer">
+
{Array(BAR_COUNT).fill(0).map((_, index) => {
+
// Determine the specific style for each bar
+
const barStyle = isSilent
+
? silentStyle // If silent, use the fixed dot style
+
: { // If not silent, use the full animation with its unique delay
+
animation: activeAnimation,
+
animationDelay: BAR_ANIMATION_DELAYS[index],
+
};
+
+
return (
+
<div
+
key={index}
+
className="bar"
+
style={barStyle} // Apply the determined style object
+
>
+
</div>
+
);
+
})}
+
+
<style jsx>
+
{`
+
.visualizer {
+
display: flex;
+
align-items: center;
+
gap: 1px;
+
height: 50px;
+
}
+
+
.bar {
+
width: 2px;
+
background: rgb(0, 151, 188);
+
border-radius: 1px;
+
}
+
+
@keyframes wave {
+
0%, 100% {
+
height: 5px;
+
}
+
50% {
+
height: 30px;
+
}
+
}
+
`}
+
</style>
+
</div>
+
);
+
}
+95
src/components/jsx/Lastfm.jsx
···
···
+
import { useEffect, useState } from "react";
+
import AudioVisualizer from "./AudioVisualizer";
+
+
export default function Lastfm() {
+
const [data, setData] = useState(null);
+
const [error, setError] = useState(null);
+
+
useEffect(() => {
+
const fetchLastfmData = 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);
+
}
+
};
+
+
fetchLastfmData();
+
}, []);
+
+
if (error || !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";
+
+
if (days == 1) timeAgo = "1 day ago";
+
+
oldStatusClasses = days > 3
+
? "opacity-75 text-decoration-line-through"
+
: "";
+
}
+
+
return (
+
<a
+
id="now-playing"
+
href={data.link}
+
target="_blank"
+
className={oldStatusClasses}
+
>
+
<AudioVisualizer isSilent={!data.nowPlaying} />
+
<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",
+
}}
+
>
+
<small style={{ fontSize: "0.7rem" }}>
+
^ what I'm listening (or last listened) to
+
</small>
+
</div>
+
</div>
+
</a>
+
);
+
}
+78
src/components/jsx/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>
+
);
+
}
+109
src/components/jsx/Status.jsx
···
···
+
import { useEffect, useState } from "react";
+
const actorDid = import.meta.env.PUBLIC_ACTOR_DID;
+
+
if (!actorDid) {
+
console.error(
+
"PUBLIC_ACTOR_DID is not defined. Please check your .env file.",
+
);
+
}
+
+
export default function Status() {
+
const [data, setData] = useState(null);
+
const [error, setError] = useState(null);
+
+
useEffect(() => {
+
const fetchStatus = async () => {
+
if (!actorDid) {
+
setError("Configuration error: Actor DID is missing.");
+
return;
+
}
+
try {
+
const res = await fetch(
+
`https://public.api.bsky.app/xrpc/app.bsky.feed.getAuthorFeed?actor=${actorDid}&limit=1&filter=posts_no_replies`,
+
);
+
if (!res.ok) throw new Error(`HTTP ${res.status}`);
+
+
const json = await res.json();
+
const latestPost = json.feed[0].post;
+
+
const status = {
+
text: latestPost.record.text,
+
createdAt: latestPost.record.createdAt,
+
link: `https://bsky.app/profile/${actorDid}/post/${
+
latestPost.uri.split("/")[4]
+
}`,
+
};
+
setData(status);
+
} catch (err) {
+
console.error("Fetch failed:", err);
+
setError(err.message);
+
}
+
};
+
+
fetchStatus();
+
}, [actorDid]);
+
+
if (error) {
+
return (
+
<span
+
className="badge bg-dark"
+
style={{
+
color: "#595959 !important",
+
//outline: "1px solid white",
+
//outlineOffset: "1px",
+
}}
+
>
+
Error: {error}
+
</span>
+
);
+
}
+
if (!data) {
+
return (
+
<span
+
className="badge bg-dark"
+
style={{
+
color: "#595959 !important",
+
//outline: "1px solid white",
+
//outlineOffset: "1px",
+
}}
+
>
+
Loading status...
+
</span>
+
);
+
}
+
+
const date = new Date(data.createdAt);
+
const now = new Date();
+
const diff = now.getTime() - date.getTime();
+
+
const days = Math.floor(diff / (1000 * 60 * 60 * 24));
+
+
if (days >= 2) {
+
return null;
+
}
+
+
const minutes = Math.floor(diff / 60000);
+
const hours = Math.floor(minutes / 60);
+
+
let timeAgo = "just now";
+
if (days > 0) timeAgo = `${days} days ago`;
+
else if (hours > 0) timeAgo = `${hours} hours ago`;
+
else if (minutes > 0) timeAgo = `${minutes} minutes ago`;
+
+
if (days == 1) timeAgo = "1 day ago";
+
+
const oldStatusClasses = days >= 1
+
? "opacity-75 text-decoration-line-through"
+
: "";
+
+
return (
+
<a
+
href={data.link}
+
target="_blank"
+
className={`badge bg-white ${oldStatusClasses}`}
+
//style={{ outline: "1px solid white", outlineOffset: "1px" }}
+
>
+
I'm.. "{data.text}", {timeAgo}
+
</a>
+
);
+
}
+154
src/components/jsx/Tealfm.jsx
···
···
+
import { useEffect, useState } from "react";
+
import AudioVisualizer from "./AudioVisualizer";
+
+
export default function Tealfm() {
+
const [data, setData] = useState(null);
+
const [error, setError] = useState(null);
+
const [isCurrentlyPlaying, setIsCurrentlyPlaying] = useState(false);
+
+
useEffect(() => {
+
const fetchStatus = async () => {
+
const repo = "did:plc:sfjxpxxyvewb2zlxwoz2vduw";
+
const statusCollection = "fm.teal.alpha.actor.status";
+
const lastPlayedCollection = "fm.teal.alpha.feed.play";
+
+
try {
+
const statusUrl =
+
`https://pds.indexx.dev/xrpc/com.atproto.repo.getRecord?repo=${repo}&collection=${statusCollection}&rkey=self`;
+
const statusRes = await fetch(statusUrl);
+
+
if (!statusRes.ok) {
+
throw new Error(`HTTP ${statusRes.status} on status fetch`);
+
}
+
+
const statusData = await statusRes.json();
+
const statusValue = statusData?.value;
+
const nowTimestamp = Math.floor(Date.now() / 1000);
+
+
const isExpired = statusValue?.expiry &&
+
nowTimestamp > parseInt(statusValue.expiry, 10);
+
const isItemEmpty = !statusValue?.item?.trackName;
+
+
let status;
+
+
if (statusValue && !isExpired && !isItemEmpty) {
+
const latest = statusValue.item;
+
+
status = {
+
song: latest.trackName,
+
artist: latest.artists.map((artist) =>
+
artist.artistName
+
).join(", "),
+
createdAt: parseInt(statusValue.time, 10) * 1000,
+
link: latest.originUrl ?? "",
+
};
+
setData(status);
+
setIsCurrentlyPlaying(true);
+
return;
+
}
+
+
console.log(
+
"Status expired or empty. Falling back to last played record.",
+
);
+
+
const lastPlayedUrl =
+
`https://pds.indexx.dev/xrpc/com.atproto.repo.listRecords?repo=${repo}&collection=${lastPlayedCollection}&limit=1&reverse=false`;
+
const lastPlayedRes = await fetch(lastPlayedUrl);
+
+
if (!lastPlayedRes.ok) {
+
throw new Error(
+
`HTTP ${lastPlayedRes.status} on listRecords fetch`,
+
);
+
}
+
+
const lastPlayedData = await lastPlayedRes.json();
+
+
if (
+
lastPlayedData.records && lastPlayedData.records.length > 0
+
) {
+
const latest = lastPlayedData.records[0].value;
+
+
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);
+
setIsCurrentlyPlaying(false);
+
} else {
+
console.log("No records found in last played collection.");
+
setIsCurrentlyPlaying(false);
+
}
+
} catch (err) {
+
console.error("Fetch failed:", err);
+
setError(err.message);
+
setIsCurrentlyPlaying(false);
+
}
+
};
+
+
fetchStatus();
+
}, []);
+
+
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}
+
>
+
<AudioVisualizer isSilent={!isCurrentlyPlaying} />
+
<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",
+
}}
+
>
+
<small style={{ fontSize: "0.7rem" }}>
+
^ what I'm listening (or last listened) to
+
</small>
+
</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/",
-
},
-
];
···
+1 -1
src/layouts/Layout.astro
···
<!-- RESOURCES -->
<link href="/bootstrap.min.css" rel="stylesheet" />
<script src="/bootstrap.bundle.min.js" is:inline></script>
-
<link rel="stylesheet" href="/style.css?v=2" type="text/css" />
</head>
<body>
<slot />
···
<!-- RESOURCES -->
<link href="/bootstrap.min.css" rel="stylesheet" />
<script src="/bootstrap.bundle.min.js" is:inline></script>
+
<link rel="stylesheet" href="/style.css?v=3" type="text/css" />
</head>
<body>
<slot />
+13 -12
src/pages/index.astro
···
---
import Layout from "../layouts/Layout.astro";
-
import Header from "../components/Header.astro";
-
import SocialLinks from "../components/SocialLinks.astro";
-
//import ProjectsPane from "../components/ProjectsPane.astro";
-
import Status from "../components/islands/Status.jsx";
-
import NowPlaying from "../components/islands/NowPlaying.jsx";
---
<Layout title="hello" description="insert wave emoji">
<main id="page" class="text-center">
<Header />
<Status client:load />
<!--
-
<NowPlaying client:load />
-->
-
<SocialLinks />
-
<!--
<a
id="projects-button"
href="#"
···
>
. . .
</a>
-
-->
</main>
-
<!--
-
<ProjectsPane />
<script>
import { initializeProjects } from "../scripts/projects";
initializeProjects();
</script>
-
-->
</Layout>
···
---
import Layout from "../layouts/Layout.astro";
+
import Header from "../components/astro/Header.astro";
+
import SocialLinks from "../components/astro/SocialLinks.astro";
+
import ProjectsPane from "../components/jsx/ProjectsPane.jsx";
+
import Status from "../components/jsx/Status.jsx";
+
import Tealfm from "../components/jsx/Tealfm";
---
<Layout title="hello" description="insert wave emoji">
<main id="page" class="text-center">
<Header />
<Status 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 client:load />
<script>
import { initializeProjects } from "../scripts/projects";
initializeProjects();
</script>
</Layout>