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

feat: changes

Index 5d7cb85b 9f4bea3b

+4 -4
package-lock.json
···
"@atproto/api": "^0.15.6",
"@types/react": "^19.1.6",
"@types/react-dom": "^19.1.6",
-
"astro": "^5.9.2",
+
"astro": "^5.11.0",
"fs": "^0.0.1-security",
"marked": "^15.0.11",
"node-cache": "^5.1.2",
···
},
"node_modules/astro": {
-
"version": "5.9.2",
-
"resolved": "https://registry.npmjs.org/astro/-/astro-5.9.2.tgz",
-
"integrity": "sha512-K/zZlQOWMpamfLDOls5jvG7lrsjH1gkk3ESRZyZDCkVBtKHMF4LbjwCicm/iBb3mX3V/PerqRYzLbOy3/4JLCQ==",
+
"version": "5.11.0",
+
"resolved": "https://registry.npmjs.org/astro/-/astro-5.11.0.tgz",
+
"integrity": "sha512-MEICntERthUxJPSSDsDiZuwiCMrsaYy3fnDhp4c6ScUfldCB8RBnB/myYdpTFXpwYBy6SgVsHQ1H4MuuA7ro/Q==",
"dependencies": {
"@astrojs/compiler": "^2.12.2",
"@astrojs/internal-helpers": "0.6.1",
+1 -1
package.json
···
"@atproto/api": "^0.15.6",
"@types/react": "^19.1.6",
"@types/react-dom": "^19.1.6",
-
"astro": "^5.9.2",
+
"astro": "^5.11.0",
"fs": "^0.0.1-security",
"marked": "^15.0.11",
"node-cache": "^5.1.2",
+123 -98
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),
···
initial-value: 0deg;
}
-
/*
-
ENTIRE PAGE
-
*/
+
/* ---------------------------------------------------
+
GLOBAL STYLES
+
--------------------------------------------------- */
html,
body {
font-family: "Nunito Sans", sans-serif;
···
background: var(--bg-gradient);
}
+
a {
+
color: rgb(0, 151, 188) !important;
+
text-decoration: none;
+
}
+
+
a:hover .project-card {
+
border-color: rgb(0, 83, 104);
+
}
+
+
.unselectable {
+
user-select: none;
+
-webkit-user-select: none;
+
-moz-user-select: none;
+
-ms-user-select: none;
+
}
+
+
/* ---------------------------------------------------
+
PAGE LAYOUT
+
--------------------------------------------------- */
main#page {
position: absolute;
top: 50%;
left: 0;
width: 100%;
margin-top: -100px;
-
color: rgb(0, 92, 116);
+
color: rgb(0, 151, 188);
transition: width 0.5s ease;
}
···
width: 70%;
}
-
/*
-
TEXT
-
*/
+
/* ---------------------------------------------------
+
TEXT STYLES
+
--------------------------------------------------- */
h1 {
color: #fff;
-
margin-bottom: 0px;
+
margin-bottom: 0;
font-weight: 1000;
font-size: 4rem;
}
h1.spin {
-
animation-name: textSpin;
-
animation-duration: 4.5s;
-
animation-iteration-count: infinite;
-
animation-timing-function: linear;
-
+
animation: textSpin 4.5s linear infinite;
position: relative;
}
···
content: "index" !important;
}
-
a {
-
color: rgb(0, 151, 188) !important;
-
text-decoration: none;
-
}
-
+
/* ---------------------------------------------------
+
CIRCLE HOVER LINK
+
--------------------------------------------------- */
.circle-hover {
display: inline-block;
position: relative;
margin: 0 var(--spacing, 0px);
-
transition: margin 0.25s;
-
flex: 1;
min-width: 0;
-
-
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;
-
}
+
transition: margin 0.25s;
+
}
-
&:hover {
-
--spacing: 4px;
-
--stroke: var(--line-active);
-
--stroke-delay: 0.1s;
-
--offset: 180px;
-
color: #fff !important;
-
}
+
.circle-hover:hover {
+
--spacing: 4px;
+
--stroke: var(--line-active);
+
--stroke-delay: 0.1s;
+
--offset: 180px;
+
color: #fff !important;
}
-
/*
-
UTILITY
-
*/
-
.unselectable {
-
-webkit-user-select: none;
-
/* Safari */
-
-moz-user-select: none;
-
/* Firefox */
-
-ms-user-select: none;
-
/* Internet Explorer/Edge */
-
user-select: none;
-
/* Standard syntax */
+
.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 SIDE PANE
+
--------------------------------------------------- */
#projects-pane {
-
opacity: 0;
-
pointer-events: none;
-
visibility: hidden;
-
position: absolute;
top: 0;
right: 0;
width: 30%;
-
background: rgb(0 23 29 / 48%);
+
height: 97%;
margin: 15px;
padding: 20px;
border-radius: 10px;
-
height: 97%;
+
background: rgba(0, 23, 29, 0.48);
+
opacity: 0;
+
pointer-events: none;
+
visibility: hidden;
+
}
+
+
#projects-pane ul {
+
height: 100%;
+
padding-bottom: 20px;
+
overflow-y: scroll;
+
overflow-x: hidden;
+
}
+
+
.project-card {
+
background: rgba(0, 23, 29, 0.641);
+
border: 1px solid rgb(0, 44, 55);
+
padding: 10px;
+
border-radius: 10px;
+
margin-bottom: 10px;
+
color: #fff !important;
}
-
/*
-
ANIMATED BORDERS UTILITY
-
*/
+
/* ---------------------------------------------------
+
NOW PLAYING WIDGET
+
--------------------------------------------------- */
+
#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 {
+
width: 64px;
+
height: 64px;
+
border-radius: 4px;
+
object-fit: cover;
+
}
+
+
#now-playing #content {
+
width: 100%;
+
}
+
+
/* ---------------------------------------------------
+
ANIMATED BORDER EFFECT
+
--------------------------------------------------- */
.animated-border::before,
.animated-border::after {
content: "";
position: absolute;
top: 50%;
left: 50%;
-
translate: -50% -50%;
+
transform: translate(-50%, -50%);
font-size: 70px;
z-index: 1;
-
background: conic-gradient(
from var(--animatedBorder),
transparent,
···
);
background-clip: text;
-webkit-text-fill-color: transparent;
-
-
animation: 3s animatedBorder linear infinite;
+
animation: animatedBorder 3s linear infinite;
}
.animated-border::before {
···
opacity: 0.5;
}
-
.project-card {
-
background: rgba(0, 23, 29, 0.641);
-
border: 1px solid rgb(0, 44, 55);
-
padding: 10px;
-
border-radius: 10px;
-
margin-bottom: 10px;
-
color: #fff !important;
-
}
-
-
#projects-pane ul {
-
overflow-x: hidden;
-
overflow-y: scroll;
-
height: 100%;
-
padding-bottom: 20px;
-
}
-
-
a:hover .project-card {
-
border-color: rgb(0, 83, 104);
-
}
-
+
/* ---------------------------------------------------
+
ANIMATIONS
+
--------------------------------------------------- */
@keyframes textSpin {
0% {
transform: rotateY(0deg);
}
-
100% {
transform: rotateY(359deg);
}
···
from {
--animatedBorder: 0deg;
}
-
to {
--animatedBorder: 360deg;
}
-1
src/assets/astro.svg
···
-
<svg xmlns="http://www.w3.org/2000/svg" fill="none" width="115" height="48"><path fill="#17191E" d="M7.77 36.35C6.4 35.11 6 32.51 6.57 30.62c.99 1.2 2.35 1.57 3.75 1.78 2.18.33 4.31.2 6.33-.78.23-.12.44-.27.7-.42.18.55.23 1.1.17 1.67a4.56 4.56 0 0 1-1.94 3.23c-.43.32-.9.61-1.34.91-1.38.94-1.76 2.03-1.24 3.62l.05.17a3.63 3.63 0 0 1-1.6-1.38 3.87 3.87 0 0 1-.63-2.1c0-.37 0-.74-.05-1.1-.13-.9-.55-1.3-1.33-1.32a1.56 1.56 0 0 0-1.63 1.26c0 .06-.03.12-.05.2Z"/><path fill="url(#a)" d="M7.77 36.35C6.4 35.11 6 32.51 6.57 30.62c.99 1.2 2.35 1.57 3.75 1.78 2.18.33 4.31.2 6.33-.78.23-.12.44-.27.7-.42.18.55.23 1.1.17 1.67a4.56 4.56 0 0 1-1.94 3.23c-.43.32-.9.61-1.34.91-1.38.94-1.76 2.03-1.24 3.62l.05.17a3.63 3.63 0 0 1-1.6-1.38 3.87 3.87 0 0 1-.63-2.1c0-.37 0-.74-.05-1.1-.13-.9-.55-1.3-1.33-1.32a1.56 1.56 0 0 0-1.63 1.26c0 .06-.03.12-.05.2Z"/><path fill="#17191E" d="M.02 30.31s4.02-1.95 8.05-1.95l3.04-9.4c.11-.45.44-.76.82-.76.37 0 .7.31.82.76l3.04 9.4c4.77 0 8.05 1.95 8.05 1.95L17 11.71c-.2-.56-.53-.91-.98-.91H7.83c-.44 0-.76.35-.97.9L.02 30.31Zm42.37-5.97c0 1.64-2.05 2.62-4.88 2.62-1.85 0-2.5-.45-2.5-1.41 0-1 .8-1.49 2.65-1.49 1.67 0 3.09.03 4.73.23v.05Zm.03-2.04a21.37 21.37 0 0 0-4.37-.36c-5.32 0-7.82 1.25-7.82 4.18 0 3.04 1.71 4.2 5.68 4.2 3.35 0 5.63-.84 6.46-2.92h.14c-.03.5-.05 1-.05 1.4 0 1.07.18 1.16 1.06 1.16h4.15a16.9 16.9 0 0 1-.36-4c0-1.67.06-2.93.06-4.62 0-3.45-2.07-5.64-8.56-5.64-2.8 0-5.9.48-8.26 1.19.22.93.54 2.83.7 4.06 2.04-.96 4.95-1.37 7.2-1.37 3.11 0 3.97.71 3.97 2.15v.57Zm11.37 3c-.56.07-1.33.07-2.12.07-.83 0-1.6-.03-2.12-.1l-.02.58c0 2.85 1.87 4.52 8.45 4.52 6.2 0 8.2-1.64 8.2-4.55 0-2.74-1.33-4.09-7.2-4.39-4.58-.2-4.99-.7-4.99-1.28 0-.66.59-1 3.65-1 3.18 0 4.03.43 4.03 1.35v.2a46.13 46.13 0 0 1 4.24.03l.02-.55c0-3.36-2.8-4.46-8.2-4.46-6.08 0-8.13 1.49-8.13 4.39 0 2.6 1.64 4.23 7.48 4.48 4.3.14 4.77.62 4.77 1.28 0 .7-.7 1.03-3.71 1.03-3.47 0-4.35-.48-4.35-1.47v-.13Zm19.82-12.05a17.5 17.5 0 0 1-6.24 3.48c.03.84.03 2.4.03 3.24l1.5.02c-.02 1.63-.04 3.6-.04 4.9 0 3.04 1.6 5.32 6.58 5.32 2.1 0 3.5-.23 5.23-.6a43.77 43.77 0 0 1-.46-4.13c-1.03.34-2.34.53-3.78.53-2 0-2.82-.55-2.82-2.13 0-1.37 0-2.65.03-3.84 2.57.02 5.13.07 6.64.11-.02-1.18.03-2.9.1-4.04-2.2.04-4.65.07-6.68.07l.07-2.93h-.16Zm13.46 6.04a767.33 767.33 0 0 1 .07-3.18H82.6c.07 1.96.07 3.98.07 6.92 0 2.95-.03 4.99-.07 6.93h5.18c-.09-1.37-.11-3.68-.11-5.65 0-3.1 1.26-4 4.12-4 1.33 0 2.28.16 3.1.46.03-1.16.26-3.43.4-4.43-.86-.25-1.81-.41-2.96-.41-2.46-.03-4.26.98-5.1 3.38l-.17-.02Zm22.55 3.65c0 2.5-1.8 3.66-4.64 3.66-2.81 0-4.61-1.1-4.61-3.66s1.82-3.52 4.61-3.52c2.82 0 4.64 1.03 4.64 3.52Zm4.71-.11c0-4.96-3.87-7.18-9.35-7.18-5.5 0-9.23 2.22-9.23 7.18 0 4.94 3.49 7.59 9.21 7.59 5.77 0 9.37-2.65 9.37-7.6Z"/><defs><linearGradient id="a" x1="6.33" x2="19.43" y1="40.8" y2="34.6" gradientUnits="userSpaceOnUse"><stop stop-color="#D83333"/><stop offset="1" stop-color="#F041FF"/></linearGradient></defs></svg>
-1
src/assets/background.svg
···
-
<svg xmlns="http://www.w3.org/2000/svg" width="1440" height="1024" fill="none"><path fill="url(#a)" fill-rule="evenodd" d="M-217.58 475.75c91.82-72.02 225.52-29.38 341.2-44.74C240 415.56 372.33 315.14 466.77 384.9c102.9 76.02 44.74 246.76 90.31 366.31 29.83 78.24 90.48 136.14 129.48 210.23 57.92 109.99 169.67 208.23 155.9 331.77-13.52 121.26-103.42 264.33-224.23 281.37-141.96 20.03-232.72-220.96-374.06-196.99-151.7 25.73-172.68 330.24-325.85 315.72-128.6-12.2-110.9-230.73-128.15-358.76-12.16-90.14 65.87-176.25 44.1-264.57-26.42-107.2-167.12-163.46-176.72-273.45-10.15-116.29 33.01-248.75 124.87-320.79Z" clip-rule="evenodd" style="opacity:.154"/><path fill="url(#b)" fill-rule="evenodd" d="M1103.43 115.43c146.42-19.45 275.33-155.84 413.5-103.59 188.09 71.13 409 212.64 407.06 413.88-1.94 201.25-259.28 278.6-414.96 405.96-130 106.35-240.24 294.39-405.6 265.3-163.7-28.8-161.93-274.12-284.34-386.66-134.95-124.06-436-101.46-445.82-284.6-9.68-180.38 247.41-246.3 413.54-316.9 101.01-42.93 207.83 21.06 316.62 6.61Z" clip-rule="evenodd" style="opacity:.154"/><defs><linearGradient id="b" x1="373" x2="1995.44" y1="1100" y2="118.03" gradientUnits="userSpaceOnUse"><stop stop-color="#D83333"/><stop offset="1" stop-color="#F041FF"/></linearGradient><linearGradient id="a" x1="107.37" x2="1130.66" y1="1993.35" y2="1026.31" gradientUnits="userSpaceOnUse"><stop stop-color="#3245FF"/><stop offset="1" stop-color="#BC52EE"/></linearGradient></defs></svg>
-9
src/assets/favicon.svg
···
-
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 128 128">
-
<path d="M50.4 78.5a75.1 75.1 0 0 0-28.5 6.9l24.2-65.7c.7-2 1.9-3.2 3.4-3.2h29c1.5 0 2.7 1.2 3.4 3.2l24.2 65.7s-11.6-7-28.5-7L67 45.5c-.4-1.7-1.6-2.8-2.9-2.8-1.3 0-2.5 1.1-2.9 2.7L50.4 78.5Zm-1.1 28.2Zm-4.2-20.2c-2 6.6-.6 15.8 4.2 20.2a17.5 17.5 0 0 1 .2-.7 5.5 5.5 0 0 1 5.7-4.5c2.8.1 4.3 1.5 4.7 4.7.2 1.1.2 2.3.2 3.5v.4c0 2.7.7 5.2 2.2 7.4a13 13 0 0 0 5.7 4.9v-.3l-.2-.3c-1.8-5.6-.5-9.5 4.4-12.8l1.5-1a73 73 0 0 0 3.2-2.2 16 16 0 0 0 6.8-11.4c.3-2 .1-4-.6-6l-.8.6-1.6 1a37 37 0 0 1-22.4 2.7c-5-.7-9.7-2-13.2-6.2Z" />
-
<style>
-
path { fill: #000; }
-
@media (prefers-color-scheme: dark) {
-
path { fill: #FFF; }
-
}
-
</style>
-
</svg>
+35 -24
src/components/Header.astro
···
-
<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;"
+
<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;"
+
>
+
i usually mess around in Typescript, and recently i've been exploring the
+
<a href="https://atproto.com/" target="_blank"><b>AT protocol</b>.</a>
+
</small>
+
<!--
+
<small
+
style="display: block; margin-top: -5px;color: rgb(0, 84, 106);font-size: 0.8rem;"
>
-
<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;color: rgb(0, 84, 106);font-size: 0.8rem;">
-
i usually mess around in Typescript, and recently i've been exploring the
-
<a href="https://atproto.com/" target="_blank"><b>AT protocol</b>.</a>
-
</small>
-
<small style="display: block; margin-top: -5px;color: rgb(0, 84, 106);font-size: 0.8rem;">
-
i'm interested in making an open-source Goodreads alternative on the protocol :)
-
</small>
+
i'm interested in making an open-source Goodreads alternative on the
+
protocol :)
+
</small>
+
-->
+
</section>
-121
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://indexx.dev/api/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
-
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 -1
src/components/ProjectCard.astro
···
)
}
</div>
-
</a>
+
</a>
+1 -3
src/components/ProjectsPane.astro
···
<ul class="list-unstyled">
<li>
-
{projects.map((project) => (
-
<ProjectCard {...project} />
-
))}
+
{projects.map((project) => <ProjectCard {...project} />)}
</li>
</ul>
</section>
+23 -12
src/components/SocialLink.astro
···
---
+
import type { JSX } from "astro/jsx-runtime";
+
interface Props {
+
icon?: any;
name: string;
href: string;
tooltip?: {
···
};
}
-
const { name, href, tooltip } = Astro.props;
+
const { icon, name, href, tooltip } = Astro.props;
+
+
const Icon = icon;
---
<a
href={href}
target="_blank"
-
class="circle-hover"
{...tooltip && {
-
'data-bs-toggle': 'tooltip',
-
'data-bs-title': tooltip.title,
-
'data-bs-placement': tooltip.placement || 'bottom'
+
"data-bs-toggle": "tooltip",
+
"data-bs-title": tooltip.title,
+
"data-bs-placement": tooltip.placement || "bottom",
}}
>
-
<svg viewBox="0 0 70 36">
-
<path
-
d="M6.9739 30.8153H63.0244C65.5269 30.8152 75.5358 -3.68471 35.4998 2.81531C-16.1598 11.2025 0.894099 33.9766 26.9922 34.3153C104.062 35.3153 54.5169 -6.68469 23.489 9.31527"
-
></path>
-
</svg>
-
<span>{name}</span>
-
</a>
+
<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>
+33 -18
src/components/SocialLinks.astro
···
---
-
import SocialLink from './SocialLink.astro';
+
import Bluesky from "./icons/Bluesky.astro";
+
import Discord from "./icons/Discord.astro";
+
import Github from "./icons/Github.astro";
+
import Lastfm from "./icons/Lastfm.astro";
+
import Mastodon from "./icons/Mastodon.astro";
+
import SocialLink from "./SocialLink.astro";
const links = [
{
-
name: 'Bluesky',
-
href: 'https://bsky.app/profile/did:plc:sfjxpxxyvewb2zlxwoz2vduw'
+
icon: Bluesky,
+
name: "Bluesky",
+
href: "https://bsky.app/profile/did:plc:sfjxpxxyvewb2zlxwoz2vduw",
},
{
-
name: 'Discord',
-
href: 'https://discord.com/users/589386198011871233',
+
icon: Discord,
+
name: "Discord",
+
href: "https://discord.com/users/589386198011871233",
tooltip: {
-
title: 'index.lua',
-
placement: "top"
-
}
+
title: "index.lua",
+
placement: "top",
+
},
+
},
+
{
+
icon: Github,
+
name: "GitHub",
+
href: "https://github.com/indexxing/",
},
{
-
name: 'GitHub',
-
href: 'https://github.com/indexxing/'
+
icon: Mastodon,
+
name: "Mastodon",
+
href: "https://mastodon.social/@indexcard",
},
{
-
name: 'last.fm',
-
href: 'https://last.fm/user/Index_Card'
-
}
+
icon: Lastfm,
+
name: "last.fm",
+
href: "https://last.fm/user/Index_Card",
+
},
];
---
-
<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>
+
<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>
src/components/Status.jsx src/components/islands/Status.jsx
+6
src/components/icons/Bluesky.astro
···
+
---
+
const props = Astro.props
+
---
+
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" {...props}>
+
<path fill="#a4ccff" d="M12 11.388c-.906-1.761-3.372-5.044-5.665-6.662c-2.197-1.55-3.034-1.283-3.583-1.033C2.116 3.978 2 4.955 2 5.528c0 .575.315 4.709.52 5.4c.68 2.28 3.094 3.05 5.32 2.803c-3.26.483-6.157 1.67-2.36 5.898c4.178 4.325 5.726-.927 6.52-3.59c.794 2.663 1.708 7.726 6.444 3.59c3.556-3.59.977-5.415-2.283-5.898c2.225.247 4.64-.523 5.319-2.803c.205-.69.52-4.825.52-5.399c0-.575-.116-1.55-.752-1.838c-.549-.248-1.386-.517-3.583 1.033c-2.293 1.621-4.76 4.904-5.665 6.664" />
+
</svg>
+6
src/components/icons/Discord.astro
···
+
---
+
const props = Astro.props
+
---
+
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" {...props}>
+
<path fill="#a4ccff" d="M19.27 5.33C17.94 4.71 16.5 4.26 15 4a.1.1 0 0 0-.07.03c-.18.33-.39.76-.53 1.09a16.1 16.1 0 0 0-4.8 0c-.14-.34-.35-.76-.54-1.09c-.01-.02-.04-.03-.07-.03c-1.5.26-2.93.71-4.27 1.33c-.01 0-.02.01-.03.02c-2.72 4.07-3.47 8.03-3.1 11.95c0 .02.01.04.03.05c1.8 1.32 3.53 2.12 5.24 2.65c.03.01.06 0 .07-.02c.4-.55.76-1.13 1.07-1.74c.02-.04 0-.08-.04-.09c-.57-.22-1.11-.48-1.64-.78c-.04-.02-.04-.08-.01-.11c.11-.08.22-.17.33-.25c.02-.02.05-.02.07-.01c3.44 1.57 7.15 1.57 10.55 0c.02-.01.05-.01.07.01c.11.09.22.17.33.26c.04.03.04.09-.01.11c-.52.31-1.07.56-1.64.78c-.04.01-.05.06-.04.09c.32.61.68 1.19 1.07 1.74c.03.01.06.02.09.01c1.72-.53 3.45-1.33 5.25-2.65c.02-.01.03-.03.03-.05c.44-4.53-.73-8.46-3.1-11.95c-.01-.01-.02-.02-.04-.02M8.52 14.91c-1.03 0-1.89-.95-1.89-2.12s.84-2.12 1.89-2.12c1.06 0 1.9.96 1.89 2.12c0 1.17-.84 2.12-1.89 2.12m6.97 0c-1.03 0-1.89-.95-1.89-2.12s.84-2.12 1.89-2.12c1.06 0 1.9.96 1.89 2.12c0 1.17-.83 2.12-1.89 2.12" />
+
</svg>
+6
src/components/icons/Github.astro
···
+
---
+
const props = Astro.props
+
---
+
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" {...props}>
+
<path fill="#a4ccff" d="M12 2A10 10 0 0 0 2 12c0 4.42 2.87 8.17 6.84 9.5c.5.08.66-.23.66-.5v-1.69c-2.77.6-3.36-1.34-3.36-1.34c-.46-1.16-1.11-1.47-1.11-1.47c-.91-.62.07-.6.07-.6c1 .07 1.53 1.03 1.53 1.03c.87 1.52 2.34 1.07 2.91.83c.09-.65.35-1.09.63-1.34c-2.22-.25-4.55-1.11-4.55-4.92c0-1.11.38-2 1.03-2.71c-.1-.25-.45-1.29.1-2.64c0 0 .84-.27 2.75 1.02c.79-.22 1.65-.33 2.5-.33s1.71.11 2.5.33c1.91-1.29 2.75-1.02 2.75-1.02c.55 1.35.2 2.39.1 2.64c.65.71 1.03 1.6 1.03 2.71c0 3.82-2.34 4.66-4.57 4.91c.36.31.69.92.69 1.85V21c0 .27.16.59.67.5C19.14 20.16 22 16.42 22 12A10 10 0 0 0 12 2" />
+
</svg>
+6
src/components/icons/Lastfm.astro
···
+
---
+
const props = Astro.props
+
---
+
<svg xmlns="http://www.w3.org/2000/svg" width="512" height="512" viewBox="0 0 512 512" {...props}>
+
<path fill="#a4ccff" d="m225.8 367.1l-18.8-51s-30.5 34-76.2 34c-40.5 0-69.2-35.2-69.2-91.5c0-72.1 36.4-97.9 72.1-97.9c66.5 0 74.8 53.3 100.9 134.9c18.8 56.9 54 102.6 155.4 102.6c72.7 0 122-22.3 122-80.9c0-72.9-62.7-80.6-115-92.1c-25.8-5.9-33.4-16.4-33.4-34c0-19.9 15.8-31.7 41.6-31.7c28.2 0 43.4 10.6 45.7 35.8l58.6-7c-4.7-52.8-41.1-74.5-100.9-74.5c-52.8 0-104.4 19.9-104.4 83.9c0 39.9 19.4 65.1 68 76.8c44.9 10.6 79.8 13.8 79.8 45.7c0 21.7-21.1 30.5-61 30.5c-59.2 0-83.9-31.1-97.9-73.9c-32-96.8-43.6-163-161.3-163C45.7 113.8 0 168.3 0 261c0 89.1 45.7 137.2 127.9 137.2c66.2 0 97.9-31.1 97.9-31.1" />
+
</svg>
+6
src/components/icons/Mastodon.astro
···
+
---
+
const props = Astro.props
+
---
+
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" {...props}>
+
<path fill="#a4ccff" d="M20.94 14c-.28 1.41-2.44 2.96-4.97 3.26c-1.31.15-2.6.3-3.97.24c-2.25-.11-4-.54-4-.54v.62c.32 2.22 2.22 2.35 4.03 2.42c1.82.05 3.44-.46 3.44-.46l.08 1.65s-1.28.68-3.55.81c-1.25.07-2.81-.03-4.62-.5c-3.92-1.05-4.6-5.24-4.7-9.5l-.01-3.43c0-4.34 2.83-5.61 2.83-5.61C6.95 2.3 9.41 2 11.97 2h.06c2.56 0 5.02.3 6.47.96c0 0 2.83 1.27 2.83 5.61c0 0 .04 3.21-.39 5.43M18 8.91c0-1.08-.3-1.91-.85-2.56c-.56-.63-1.3-.96-2.23-.96c-1.06 0-1.87.41-2.42 1.23l-.5.88l-.5-.88c-.56-.82-1.36-1.23-2.43-1.23c-.92 0-1.66.33-2.23.96C6.29 7 6 7.83 6 8.91v5.26h2.1V9.06c0-1.06.45-1.62 1.36-1.62c1 0 1.5.65 1.5 1.93v2.79h2.07V9.37c0-1.28.5-1.93 1.51-1.93c.9 0 1.35.56 1.35 1.62v5.11H18z" />
+
</svg>
+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://indexx.dev/api/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>
+
);
+
}
src/components/status.astro

This is a binary file and will not be displayed.

+8 -3
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";
+
//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">
···
<Status client:load />
<NowPlaying client:load />
<SocialLinks />
+
<!--
<a
id="projects-button"
href="#"
···
>
. . .
</a>
+
-->
</main>
+
<!--
<ProjectsPane />
<script>
import { initializeProjects } from "../scripts/projects";
initializeProjects();
</script>
+
-->
</Layout>