This commit is contained in:
2026-02-14 22:25:51 -05:00
parent 6963dffeb5
commit 566378004b
15 changed files with 329 additions and 291 deletions
-27
View File
@@ -1,9 +1,5 @@
@import "tailwindcss";
@theme {
--breakpoint-xs: 400px;
}
body {
font-family: 'Pathway Gothic One', sans-serif;
background-color: black;
@@ -19,26 +15,3 @@ body {
border-radius: var(--radius-lg);
}
}
.marquee {
overflow: hidden;
white-space: nowrap;
}
.marquee-content {
display: inline-block;
animation: scroll 10s linear infinite;
}
.marquee-content span {
display: inline-block;
}
@keyframes scroll {
0% {
transform: translateX(0);
}
100% {
transform: translateX(-50%);
}
}
+34
View File
@@ -0,0 +1,34 @@
export default function ErrorState({
type = "error",
}: {
type: "error" | "loading" | "unavailable";
}) {
if (type === "error") {
return (
<div className="flex flex-col bg-black border border-black text-white">
<div className="px-2">
<h1 className="text-lg font-semibold">uh oh :(</h1>
<p>something went wrong loading this stat</p>
</div>
</div>
);
} else if (type === "loading") {
return (
<div className="flex flex-col bg-black border border-black text-white">
<div className="px-2">
<h1 className="text-lg font-semibold">loading..</h1>
<p>hang tight..</p>
</div>
</div>
);
} else if (type === "unavailable") {
return (
<div className="flex flex-col bg-black border border-black text-white">
<div className="px-2">
<h1 className="text-lg font-semibold">nothing here :(</h1>
<p>no game launched recently</p>
</div>
</div>
);
}
}
+5 -5
View File
@@ -4,17 +4,17 @@ import { TeamRow } from "./fantasy/TeamRow";
export default function FantasyStatus() {
const [personalRoster, setPersonalRoster] = useState<NFLMatchupState | null>(
null
null,
);
const [opposingRoster, setOpposingRoster] = useState<NFLMatchupState | null>(
null
null,
);
// fetch roster information
useEffect(() => {
async function fetchData() {
const data = (await fetch("https://api.alexav.gg/v4/football").then(
(res) => res.json()
(res) => res.json(),
)) as NFLAPIData;
// rosters
@@ -35,12 +35,12 @@ export default function FantasyStatus() {
}, []);
return (
<div className="border border-white border-solid rounded-lg w-full p-2">
<div className="p-2 text-black">
<div className="mb-4 space-y-4">
<a
href="https://sleeper.com/leagues/1263620000418119680/matchup"
target="_blank"
className="underline"
className="hover:underline"
>
<h1 className="font-semibold text-2xl mb-2">fantasy</h1>
</a>
+1 -1
View File
@@ -2,7 +2,7 @@ import type { ReactNode } from "react";
export default function Layout({ children }: { children: ReactNode }) {
return (
<div className="bg-black text-white min-h-screen select-none">
<div className="bg-neutral-800 text-white min-h-screen select-none">
<div className="relative">{children}</div>
</div>
);
-15
View File
@@ -1,15 +0,0 @@
import { ReactNode } from "react";
export default function Overlay({
children,
onClose,
}: {
children: ReactNode;
onClose: () => void;
}) {
return (
<div onClick={onClose} className="fixed bg-black/50 w-full h-full z-1">
{children}
</div>
);
}
-15
View File
@@ -1,15 +0,0 @@
export default function Picture({
image,
onClick,
}: {
image: string;
onClick: () => void;
}) {
return (
<img
src={image}
className="inline w-[200px] max-h-[100px] object-cover"
onClick={onClick}
/>
);
}
+16 -42
View File
@@ -1,4 +1,5 @@
import { useState, useEffect } from "react";
import ErrorState from "../ErrorState";
type GameObject = {
currentlyPlaying: CurrentlyPlaying;
@@ -18,7 +19,7 @@ export default function GameComponent() {
async function fetchGame() {
const response = await fetch(
"https://api.alexav.gg/v4/tracker/latestGame"
"https://api.alexav.gg/v4/tracker/latestGame",
);
if (response.ok) {
const data = await response.json();
@@ -33,54 +34,27 @@ export default function GameComponent() {
}, []);
if (game) {
const dateOrTime = (game: CurrentlyPlaying) => {
if (game.finishedAt) {
const currentDate = new Date();
const finishedDate = new Date(game.finishedAt);
if (
currentDate.toLocaleDateString() === finishedDate.toLocaleDateString()
) {
return `at ${finishedDate.toLocaleTimeString()}`;
} else {
return `on ${finishedDate.toLocaleDateString()}`;
}
}
};
return (
<div className="border border-white border-solid p-2 rounded-lg space-y-2">
{game.currentlyPlaying && (
<>
<h1 className="text-2xl font-bold italic">currently playing</h1>
<p>
<span className="font-semibold">
<a href={`https://medal.tv/u/alexav`} target="_blank">
<div className="flex flex-col bg-black border border-black text-white">
<div className="flex">
<div className="pl-2 pr-1">
<p className="w-[150px] truncate overflow-hidden whitespace-nowrap">
{game.currentlyPlaying.game}
</span>{" "}
</p>
<p>
{" "}
since{" "}
{new Date(
game.currentlyPlaying.startedPlayingAt
game.currentlyPlaying.startedPlayingAt,
).toLocaleTimeString()}
</p>
</>
)}
{game.history.length > 0 && (
<>
<h1 className="text-2xl font-bold italic">previously played</h1>
<div className="flex flex-wrap space-x-2">
{game.history.splice(0, 5).map((v, k) => {
return (
<div key={k}>
<p className="font-semibold">{v.game}</p>
<p>{dateOrTime(v)}</p>
</div>
);
})}
</div>
</>
)}
</div>
</div>
</div>
</a>
);
} else {
return <ErrorState type="unavailable" />;
}
}
+53 -31
View File
@@ -1,9 +1,10 @@
import { useState, useEffect } from "react";
import type { LastFMMusicObject } from "../../utils/types";
import ErrorState from "../ErrorState";
export default function MusicComponent() {
const [music, setMusic] = useState<LastFMMusicObject | null>(null);
const [failedFetch, setFailedFetch] = useState(false);
const [failedFetch, setFailedFetch] = useState<boolean>(false);
useEffect(() => {
async function fetchMusic() {
@@ -25,46 +26,67 @@ export default function MusicComponent() {
fetchMusic();
}, []);
if (!failedFetch) {
if (music) {
return (
<a href="https://last.fm/user/lulawex" target="_blank">
<div className="flex flex-col">
<div className="flex flex-col bg-black border border-black text-white">
<div className="flex align-middle">
<img width="50" src={music.images[1]?.url} />
<div className="pl-2 max-w-25">
{music.title.length > 15 ? (
<div className="marquee">
<div className="marquee-content">
<span>{music.title}&nbsp;&nbsp;</span>
<span>{music.title}&nbsp;&nbsp;</span>
<img
className="w-[50px] h-[50px] object-cover"
src={music.images[1]?.url}
/>
<div className="pl-2 pr-1">
<p className="w-[150px] truncate overflow-hidden whitespace-nowrap">
{music.title}
</p>
<p> {music.artist}</p>
</div>
</div>
) : (
<h1>{music.title}</h1>
)}
<p className="truncate">{music.artist}</p>
</div>
</div>
<p className="hover:underline w-fit opacity-50">powered by last.fm</p>
</div>
</a>
);
} else {
if (failedFetch) {
return (
<div>
<h1>uh oh!</h1>
<p>failed to get music :(</p>
</div>
);
return <ErrorState type="loading" />;
}
} else {
return (
<div>
<h1>loading music..</h1>
<p>loading artist..</p>
</div>
);
}
return <ErrorState type="error" />;
}
// if (music) {
// return (
// <a href="https://last.fm/user/lulawex" target="_blank">
// <div className="flex flex-col bg-black border border-black text-white">
// <div className="flex align-middle">
// <img
// className="w-[50px] h-[50px] object-cover"
// src={music.images[1]?.url}
// />
// <div className="pl-2 pr-1">
// <p className="w-[150px] truncate overflow-hidden whitespace-nowrap">
// {music.title}
// </p>
// <p> {music.artist}</p>
// </div>
// </div>
// </div>
// </a>
// );
// } else {
// if (failedFetch) {
// return (
// <div>
// <h1>uh oh!</h1>
// <p>failed to get music :(</p>
// </div>
// );
// } else {
// return (
// <div>
// <h1>loading music..</h1>
// <p>loading artist..</p>
// </div>
// );
// }
// }
}
+28
View File
@@ -0,0 +1,28 @@
import { useState, useEffect } from "react";
export default function Clock() {
const [clock, setClock] = useState(
new Date().toLocaleTimeString("en-GB", {
hour: "2-digit",
minute: "2-digit",
}),
);
useEffect(() => {
setInterval(() => {
const date = new Date().toLocaleTimeString("en-GB", {
hour: "2-digit",
minute: "2-digit",
});
if (clock != date) {
setClock(date);
}
}, 500);
}, []);
return (
<h1 className="text-center bg-white text-black max-w-fit mx-auto px-1">
{clock}
</h1>
);
}
-47
View File
@@ -1,47 +0,0 @@
import MusicComponent from "../info/MusicComponent";
export default function InfoModule() {
return (
<div className="border border-white border-solid rounded-lg p-2">
<div className="flex flex-wrap justify-between p-2 space-y-2">
<div className="space-y-2">
<h1 className="text-2xl font-bold italic opacity-95">alex, 18</h1>
<img
src="https://storage.alexav.gg/content/6d87f5ac-e7f0-413c-84a6-6838e590b7d9.png"
className="rounded-md w-[150px] h-[110px] object-cover object-left-top"
alt="Profile"
/>
<div className="pt-2">
<MusicComponent />
</div>
</div>
<div>
<h1 className="font-bold text-2xl italic">what i do</h1>
<p>
qa @{" "}
<a
href="https://medal.tv"
target="_blank"
rel="noopener noreferrer"
className="hover:underline text-[#bff83e]"
>
medal.tv
</a>
, game, code
</p>
<h1
className="italic opacity-50 font-semibold select-text underline"
onClick={() => navigator.clipboard.writeText("alex@vierfrantz.com")}
>
alex@vierfrantz.com
</h1>
<hr className="my-2" />
<h1 className="font-bold text-2xl italic">obsessed with</h1>
<p>dexter, tlou, twd</p>
</div>
</div>
</div>
);
}
+8 -10
View File
@@ -1,5 +1,3 @@
import Picture from "../Picture";
const images = [
"https://storage.alexav.gg/content/1f822215-0768-424d-893a-13bcd10aa98b.jpg",
"https://storage.alexav.gg/content/de890eaf-fc73-42a4-9fbe-a62313394a4c.jpg",
@@ -8,19 +6,19 @@ const images = [
"https://storage.alexav.gg/content/2f36c1dd-eaf8-4fd3-a3cd-1d25fe8f5a07.jpg",
];
export default function PictureCarousel({
onClick,
}: {
onClick: (url: string) => void;
}) {
export default function PictureCarousel() {
return (
<div className="border border-white border-solid rounded-lg p-2">
<h1 className="text-2xl font-bold italic">pictures</h1>
<div className="whitespace-nowrap overflow-x-auto">
<div className="my-2 space-x-2">
{images.map((v, k) => {
return <Picture key={k} image={v} onClick={() => onClick(v)} />;
return (
<img
key={k}
src={v}
className="inline w-[200px] max-h-[100px] object-cover"
/>
);
})}
</div>
</div>
+1 -1
View File
@@ -38,7 +38,7 @@ export default function ScoreModule({ type }: { type: "sabres" | "bills" }) {
}, []);
return (
<div className="border border-white border-solid rounded-lg w-full mb-4">
<div className="text-black rounded-lg w-full">
<div className="flex justify-between">
<div>
<img width="100" className="p-4" src={IMAGES[type]} />
+29 -41
View File
@@ -1,4 +1,5 @@
import { useState, useEffect } from "react";
import ErrorState from "../ErrorState";
type ContentObject = {
id: number;
@@ -17,19 +18,9 @@ type ContentObject = {
};
export default function StatModule() {
const [data, setData] = useState<ContentObject[] | null>(null);
const [data, setData] = useState<ContentObject | null>(null);
const [failedFetch, setFailedFetch] = useState<boolean>(false);
const ErrorState = () => {
return (
<div className="text-center p-2">
<h1 className="text-lg font-semibold">Uh oh :(</h1>
<p>Something went wrong loading this stat</p>
</div>
);
};
useEffect(() => {
async function fetchData() {
const response = await fetch(
@@ -39,49 +30,46 @@ export default function StatModule() {
const data = await response.json();
if (data && data.length <= 0) {
setFailedFetch(true);
} else setData(data.data as ContentObject[]);
} else setData(data.data[0] as ContentObject);
} else setFailedFetch(true);
}
fetchData();
}, []);
if (!failedFetch) {
if (data) {
return (
<div className="border border-white border-solid rounded-lg w-full mb-4">
<h1 className="text-2xl font-bold italic p-2">recent watches</h1>
<div className="grid grid-cols-3 gap-2">
{data && data.length > 0 && !failedFetch ? (
data?.map((v, k) => {
return (
<div key={k} className="text-center p-2 space-y-2">
<img
src={v?.image}
className="object-cover w-full h-[50px] object-top"
/>
<a
className="hover:underline"
href={`https://tracker.alexav.gg/info/${v?.id}`}
target="_blank"
>
<h1 className="font-semibold">{v?.title}</h1>
<p>
<a href={`https://tracker.alexav.gg/info/${data?.id}`} target="_blank">
<div className="flex flex-col bg-black border border-black text-white">
<div className="flex">
<div className="pl-2 pr-1">
<p className="w-[150px] truncate flex justify-end overflow-hidden whitespace-nowrap">
{data?.title}
</p>
<p className="flex justify-end">
{" "}
{v?.finished
{data?.finished
? "finished"
: v?.paused
: data?.paused
? "paused"
: v.startedOn
: data?.startedOn
? "started"
: "not started"}
</p>
</div>
<img
className="w-[50px] h-[50px] object-cover"
src={data?.image}
/>
</div>
</div>
</a>
</div>
);
})
) : (
<ErrorState />
)}
</div>
</div>
);
} else {
return <ErrorState type="loading" />;
}
} else {
return <ErrorState type="error" />;
}
}
-7
View File
@@ -1,7 +0,0 @@
export default function WelcomeModule() {
return (
<div className="border border-white border-solid p-2 rounded-lg flex justify-between w-full">
<h1 className="text-2xl font-bold">welcome to my site !</h1>
</div>
);
}
+134 -29
View File
@@ -1,48 +1,153 @@
import { useState } from "react";
import Layout from "../components/Layout";
import Overlay from "../components/Overlay";
import GameComponent from "../components/info/GameComponent";
import PictureCarousel from "../components/modules/PictureCarousel";
import InfoModule from "../components/modules/InfoModule";
import WelcomeModule from "../components/modules/WelcomeModule";
import ScoreModule from "../components/modules/ScoreModule";
import StatModule from "../components/modules/StatModule";
import FantasyStatus from "../components/FantasyStatus";
import Clock from "../components/modules/Clock";
import MusicComponent from "../components/info/MusicComponent";
export default function App() {
const [selectedImage, setSelectedImage] = useState<string>("");
const [tab, setTab] = useState<string>("home");
return (
<Layout>
{selectedImage !== "" && (
<Overlay onClose={() => setSelectedImage("")}>
<div className="flex justify-center items-center w-full h-full">
<div className="min-h-screen flex justify-center items-center">
<div className="border-3 bg-[#ebebeb] border-black w-full sm:w-[500px]">
<div className="] text-black">
<div className="bg-black w-full text-white">
<div className="grid grid-cols-3 gap-2 p-2 text-2xl">
<h1 className="font-semibold">welcome</h1>
<Clock />
<h1 className="text-right">let it rip</h1>
</div>
</div>
<img
draggable={false}
className="select-none"
width="400"
height="400"
src={selectedImage}
className="h-[100px] object-[25%_75%] w-full object-cover"
src="https://storage.alexav.gg/content/1f822215-0768-424d-893a-13bcd10aa98b.jpg"
/>
<hr className="border-black" />
<div className="p-2">
<div className="grid grid-cols-3 gap-2">
<div>
<img
className="h-40 w-50 object-cover object-top"
src="https://storage.alexav.gg/content/f49b355a-25cd-48ba-8421-da702620fcc6.jpeg"
/>
</div>
</Overlay>
)}
<div className="my-2 mx-auto w-full sm:w-[500px]">
<div className="space-y-4">
<img
className="rounded-lg"
src={
"https://storage.alexav.gg/content/55f3eca0-a5a9-4c71-9d92-ba5d669a7a0c.jpg"
}
/>
<WelcomeModule />
<InfoModule />
<div>
<h1 className="text-2xl font-semibold">hi, i'm alex!</h1>
<p className="text-lg">
i do QA stuff at{" "}
<a
className="underline"
href="https://medal.tv/u/alexav"
target="_blank"
>
Medal.tv{" "}
</a>
and write mediocre code sometimes
</p>
<p className="text-sm space-x-1">
<a
href="https://twitter.com/lostinmyyabyss_"
className="hover:underline"
target="_blank"
>
twt
</a>
<span>•</span>
<a
className="hover:underline"
href="https://instagram.com/alexfrantz07"
target="_blank"
>
instagram
</a>
<span>•</span>
<a
className="hover:underline"
href="https://tiktok.com/@lostinherabyss_"
target="_blank"
>
tiktok
</a>
</p>
</div>
<div className="text-2xl">
<div>
{" "}
<span className="bg-black text-white px-1">shows</span>{" "}
dexter, the pitt, tlou, twd
</div>
<div>
<span className="bg-black text-white px-1">music</span> ptv,
treaty oak, zach top, she wants revenge
</div>
</div>
</div>
</div>
<hr className="border-black" />
<div className="px-2">
<div className="grid grid-cols-2">
<div>
<h1 className="text-2xl font-bold">about me</h1>
<p>
born & raised western new yorker. i've been doing QA work
for the past 4 years and learning software engineering in
between. i also play acoustic guitar, am really into gaming
and love travelling to NYC. i am also incredibly awkward,
but that's besides the point
</p>
</div>
<div className="mx-auto">
<div className="p-2">
<div className="mb-2">
<MusicComponent />
</div>
<div className="mb-2">
<StatModule />
<PictureCarousel onClick={(url) => setSelectedImage(url)} />
</div>
<div>
<GameComponent />
<ScoreModule type={"sabres"} />
{/* <FantasyStatus /> */}
</div>
</div>
</div>
</div>
</div>
<div className="grid grid-cols-4 text-center border-b">
<h1
onClick={() => setTab("home")}
className={`border-r-1 border-t-1 border-black hover:cursor-pointer ${tab === "home" && "underline"}`}
>
home
</h1>
<h1
onClick={() => setTab("fantasy")}
className={`border-r-1 border-t-1 border-black hover:cursor-pointer ${tab === "fantasy" && "underline"}`}
>
fantasy
</h1>
<h1
onClick={() => setTab("pictures")}
className={`border-r-1 border-t-1 border-black hover:cursor-pointer ${tab === "pictures" && "underline"}`}
>
pictures
</h1>
<h1
onClick={() => setTab("scores")}
className={`border-r-1 border-t-1 border-black hover:cursor-pointer ${tab === "scores" && "underline"}`}
>
scores
</h1>
</div>
</div>
{tab === "fantasy" && <FantasyStatus />}
{tab === "pictures" && <PictureCarousel />}
{tab === "scores" && <ScoreModule type="sabres" />}
</div>
</div>
</Layout>