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

Compare changes

Choose any two refs to compare.

-64
src/components/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() {
-
return (
-
<div className="visualizer">
-
{Array(BAR_COUNT).fill(0).map((_, index) => (
-
<div
-
key={index}
-
className="bar"
-
style={{ animationDelay: BAR_ANIMATION_DELAYS[index] }}
-
>
-
</div>
-
))}
-
-
<style jsx>
-
{`
-
.visualizer {
-
display: flex;
-
align-items: center;
-
gap: 1px;
-
height: 50px;
-
}
-
-
.bar {
-
width: 2px;
-
background: linear-gradient(to top, #1b1bff, #8585fe, #1b1bff);
-
border-radius: 1px;
-
animation: wave 2.5s ease-in-out infinite;
-
}
-
-
@keyframes wave {
-
0%, 100% {
-
/* Start/End Height */
-
height: 5px;
-
}
-
50% {
-
/* Peak Height */
-
height: 30px;
-
}
-
}
-
`}
-
</style>
-
</div>
-
);
-
}
···
-27
src/components/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/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/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-3 mb-3"
-
style="padding: 0px; width: 50%; margin: auto; justify-content: center; gap: 20px;"
-
>
-
{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>
-95
src/components/islands/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 />
-
<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/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>
-
);
-
}
···
-109
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",
-
//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>
-
);
-
}
···
-95
src/components/islands/Tealfm.jsx
···
-
import { useEffect, useState } from "react";
-
-
export default function Tealfm() {
-
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>
-
);
-
}
···
+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>
+
);
+
}
+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 />
+6 -6
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/islands/ProjectsPane.jsx";
-
import Status from "../components/islands/Status.jsx";
-
import Lastfm from "../components/islands/Lastfm";
---
<Layout title="hello" description="insert wave emoji">
<main id="page" class="text-center">
<Header />
<Status client:load />
-
<Lastfm client:load />
<SocialLinks />
<!--
<img
···
---
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