Compare commits

..

10 Commits

Author SHA1 Message Date
alex f9f8cc4ad6 responsiveness fixes 2026-08-11 19:35:00 -04:00
alex cc6e00d777 redesign 2026-08-08 18:30:36 -04:00
alex 03e82380db publish docker package 2026-07-28 02:39:19 -04:00
alex b19b5b1bd2 ssr 2026-07-15 00:52:18 -04:00
alex 780bb5b65a update theme 2026-06-03 00:02:29 -04:00
alex 86682f6c92 add yankees score 2026-05-31 01:30:14 -04:00
alex 31c93b0876 improve picture carousel 2026-05-19 20:07:57 -04:00
alex d2c59f878d fix type errors 2026-05-19 20:02:19 -04:00
alex 1779ce0bcf clean up sports tab 2026-05-19 20:00:56 -04:00
alex d219024719 Update Home.tsx 2026-05-12 14:36:11 -04:00
33 changed files with 3868 additions and 4216 deletions
+18
View File
@@ -0,0 +1,18 @@
node_modules
dist
dist-ssr
.git
.gitignore
*.log
npm-debug.log*
pnpm-debug.log*
.vscode
.idea
.DS_Store
Dockerfile
.dockerignore
README.md
+53
View File
@@ -0,0 +1,53 @@
name: docker
on:
push:
branches: [master]
tags: ["v*"]
workflow_dispatch:
env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
jobs:
build:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- uses: actions/checkout@v5
- uses: docker/setup-buildx-action@v3
- uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
type=raw,value=latest,enable={{is_default_branch}}
type=sha
type=ref,event=tag
type=semver,pattern={{version}}
- uses: docker/build-push-action@v6
with:
context: .
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
# keeps the package listing clean - attestations show up in ghcr as
# extra untagged "unknown/unknown" entries
provenance: false
# add linux/arm64 here if the host running this is arm
platforms: linux/amd64
+35
View File
@@ -0,0 +1,35 @@
# syntax=docker/dockerfile:1
# --- build: needs devDependencies (vite) to produce dist/client + dist/server
FROM node:22-alpine AS build
WORKDIR /app
RUN npm install -g pnpm@10
COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./
RUN pnpm install --frozen-lockfile
COPY . .
RUN pnpm build
# --- deps: runtime dependencies only (express, sirv, compression, react)
FROM node:22-alpine AS deps
WORKDIR /app
RUN npm install -g pnpm@10
COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./
RUN pnpm install --frozen-lockfile --prod
# --- runtime
FROM node:22-alpine AS runtime
WORKDIR /app
ENV NODE_ENV=production
ENV PORT=5173
COPY --from=deps /app/node_modules ./node_modules
COPY --from=build /app/dist ./dist
COPY package.json server.js ./
EXPOSE 5173
USER node
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s \
CMD wget -qO- "http://127.0.0.1:${PORT}/" >/dev/null || exit 1
CMD ["node", "server.js"]
+34 -13
View File
@@ -2,36 +2,57 @@
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Pathway+Gothic+One&display=swap" rel="stylesheet">
<link href="https://mst.fntz.net/@alex" rel="me" />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
href="https://fonts.googleapis.com/css2?family=Open+Sans:ital,wght@0,300..800;1,300..800&display=swap"
rel="stylesheet"
/>
<!-- opengraph tags -->
<meta property="og:image" content="https://storage.alexav.gg/content/picturev2.jpg" />
<meta
property="og:image"
content="https://storage.alexav.gg/content/picturev2.jpg"
/>
<meta property="og:title" content="alex" />
<meta property="og:description" content="hi, i'm alex! i'm obsessed with the last of us and i do a lot of code things. about it tbh" />
<meta
property="og:description"
content="hi, i'm alex! i'm obsessed with the last of us and i do a lot of code things. about it tbh"
/>
<meta property="og:url" content="https://alexav.gg" />
<meta property="og:site_name" content="alex" />
<!-- twitter tags -->
<meta name="twitter:title" content="alex" />
<meta name="twitter:description" content="hi, i'm alex! i'm obsessed with the last of us and i do a lot of code things. about it tbh" />
<meta
name="twitter:description"
content="hi, i'm alex! i'm obsessed with the last of us and i do a lot of code things. about it tbh"
/>
<meta name="twitter:url" content="https://alexav.gg" />
<meta name="twitter:image" content="https://storage.alexav.gg/content/picturev2.jpg" />
<meta
name="twitter:image"
content="https://storage.alexav.gg/content/picturev2.jpg"
/>
<meta name="twitter:card" content="summary" />
<!-- metadata -->
<meta name="title" content="alex" />
<meta name="description" content="hi, i'm alex! i'm obsessed with the last of us and i do a lot of code things. about it tbh" />
<meta name="keywords" content="alex frantz, frantz, alex, qa, javascript, html, react, python, java, alexavier frantz, alexavier, alexav, alexfrantz" />
<meta
name="description"
content="hi, i'm alex! i'm obsessed with the last of us and i do a lot of code things. about it tbh"
/>
<meta
name="keywords"
content="alex frantz, frantz, alex, qa, javascript, html, react, python, java, alexavier frantz, alexavier, alexav, alexfrantz"
/>
<title>alex</title>
<!--app-head-->
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
<script defer data-domain="alexav.gg" src="https://metrics.alexav.gg/js/script.js"></script>
<div id="root"><!--app-html--></div>
<script type="module" src="/src/entry-client.tsx"></script>
</body>
</html>
-3888
View File
File diff suppressed because it is too large Load Diff
+14 -6
View File
@@ -4,25 +4,33 @@
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"dev:expose": "vite --host",
"build": "tsc -b && vite build",
"lint": "eslint .",
"preview": "vite preview"
"dev": "node server",
"build": "npm run build:client && npm run build:server",
"build:client": "vite build --outDir dist/client",
"build:server": "vite build --ssr src/entry-server.tsx --outDir dist/server",
"preview": "cross-env NODE_ENV=production node server",
"start": "cross-env NODE_ENV=production node server"
},
"dependencies": {
"@tailwindcss/vite": "4.1.4",
"compression": "^1.8.1",
"express": "^5.2.1",
"lucide-react": "0.488.0",
"react": "19.0.0",
"react-dom": "19.0.0",
"react-router-dom": "7.6.2",
"sirv": "^3.0.2",
"tailwindcss": "4.1.4"
},
"devDependencies": {
"@eslint/js": "9.22.0",
"@types/compression": "^1.8.1",
"@types/express": "^5.0.6",
"@types/node": "^26.1.1",
"@types/react": "19.0.10",
"@types/react-dom": "19.0.4",
"@vitejs/plugin-react": "4.3.4",
"cross-env": "^10.1.0",
"eslint": "9.22.0",
"eslint-plugin-react-hooks": "5.2.0",
"eslint-plugin-react-refresh": "0.4.19",
@@ -32,4 +40,4 @@
"typescript-eslint": "8.26.1",
"vite": "6.3.1"
}
}
}
+3247
View File
File diff suppressed because it is too large Load Diff
+2
View File
@@ -0,0 +1,2 @@
allowBuilds:
esbuild: false
Binary file not shown.
+75
View File
@@ -0,0 +1,75 @@
import fs from "node:fs/promises";
import path from "node:path";
import express from "express";
const isProduction = process.env.NODE_ENV === "production";
const port = process.env.PORT || 5173;
const base = process.env.BASE || "/";
// resolve against this file rather than the working directory, so the server
// boots the same whether it is started from the repo root or anywhere else
const root = import.meta.dirname;
const clientDir = path.resolve(root, "dist/client");
const templateHtml = isProduction
? await fs.readFile(path.resolve(clientDir, "index.html"), "utf-8")
: "";
const app = express();
let vite;
if (!isProduction) {
const { createServer } = await import("vite");
vite = await createServer({
server: { middlewareMode: true },
appType: "custom",
base,
});
app.use(vite.middlewares);
} else {
const compression = (await import("compression")).default;
const sirv = (await import("sirv")).default;
app.use(compression());
app.use(base, sirv(clientDir, { extensions: [] }));
// a hashed asset that sirv did not find is genuinely gone, usually a stale
// cached document asking for a previous build. 404 it here, otherwise it
// falls through to the catch-all and answers a .js request with html
app.use(base, (req, res, next) => {
if (req.path.startsWith("/assets/")) {
res.status(404).end();
return;
}
next();
});
}
app.use("*all", async (req, res) => {
try {
const url = req.originalUrl.replace(base, "");
let template;
let render;
if (!isProduction) {
template = await fs.readFile(path.resolve(root, "index.html"), "utf-8");
template = await vite.transformIndexHtml(url, template);
render = (await vite.ssrLoadModule("/src/entry-server.tsx")).render;
} else {
template = templateHtml;
render = (await import("./dist/server/entry-server.js")).render;
}
const rendered = await render(url);
const html = template
?.replace("<!--app-head-->", rendered.head ?? "")
.replace("<!--app-html-->", rendered.html ?? "");
res.status(200).set({ "Content-Type": "text/html" }).send(html);
} catch (e) {
vite?.ssrFixStacktrace(e);
console.log(e.stack);
res.status(500).end(e.stack);
}
});
app.listen(port, () => {
console.log(`Server started at http://localhost:${port}`);
});
+2 -2
View File
@@ -1,12 +1,12 @@
@import "tailwindcss";
body {
font-family: "Pathway Gothic One", sans-serif;
font-family: "Open Sans", sans-serif;
background-color: black;
::-webkit-scrollbar {
width: 5px;
height: 10px;
height: 5px;
border-radius: var(--radius-lg);
background-color: white;
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

+17 -25
View File
@@ -18,38 +18,30 @@ export default function FantasyStatus() {
)) as NFLAPIData;
// rosters
const ownMatchup = data.matchups[1] as NFLMatchupState;
const oppMatchup = data.matchups[0] as NFLMatchupState;
const ownMatchup = data.team2 as NFLMatchupState;
const oppMatchup = data.team1 as NFLMatchupState;
setPersonalRoster({
...ownMatchup,
owner: data.users[1],
});
setOpposingRoster({
...oppMatchup,
owner: data.users[0],
});
setPersonalRoster(ownMatchup);
setOpposingRoster(oppMatchup);
}
fetchData();
}, []);
return (
<div className="p-2">
<div className="mb-4 space-y-4">
{personalRoster && opposingRoster && (
<>
<TeamRow
isWinning={personalRoster.points > opposingRoster.points}
roster={personalRoster}
/>
<TeamRow
isWinning={opposingRoster.points > personalRoster.points}
roster={opposingRoster}
/>
</>
)}
</div>
<div className="mt-2 space-y-4">
{personalRoster && opposingRoster && (
<>
<TeamRow
isWinning={personalRoster.points > opposingRoster.points}
roster={personalRoster}
/>
<TeamRow
isWinning={opposingRoster.points > personalRoster.points}
roster={opposingRoster}
/>
</>
)}
</div>
);
}
+14
View File
@@ -0,0 +1,14 @@
export default function Footer() {
return (
<div className="w-full flex justify-between">
<a
href="#"
onClick={() => navigator.clipboard.writeText("alex@alexav.gg")}
className="text-xs opacity-85 hover:underline"
>
alex@alexav.gg
</a>
<p className="text-xs opacity-85">&copy; 2026 Alex Frantz</p>
</div>
);
}
+25
View File
@@ -0,0 +1,25 @@
export default function Header() {
return (
<div className="flex flex-wrap items-center justify-between gap-x-4 gap-y-1 py-4">
<h1 className="font-bold">
Alex <span className="font-normal text-blue-500">/</span> dev + QA
</h1>
<div className="flex gap-3 text-sm sm:text-base">
<a
href="https://github.com/azpha"
target="_blank"
className="hover:underline"
>
GitHub
</a>
<a
href="#"
className="hover:underline"
onClick={() => navigator.clipboard.writeText("alex@alexav.gg")}
>
alex@alexav.gg
</a>
</div>
</div>
);
}
+3 -1
View File
@@ -1,8 +1,10 @@
import type { ReactNode } from "react";
import Header from "./Header";
export default function Layout({ children }: { children: ReactNode }) {
return (
<div className="bg-neutral-800 text-white min-h-screen flex justify-center items-center select-none">
<div className="bg-black text-white min-h-screen w-full max-w-[42rem] 2xl:max-w-[48rem] mx-auto select-none px-5">
<Header />
<div className="relative">{children}</div>
</div>
);
+46
View File
@@ -0,0 +1,46 @@
interface ProjectCardProps {
title: string;
description: string;
link: string;
pills: string[];
}
interface PillProps {
name: string;
}
export default function ProjectCard({
title,
description,
link,
pills,
}: ProjectCardProps) {
const Pill = ({ name }: PillProps) => {
return (
<div className="bg-zinc-500 rounded-lg text-xs p-1 font-semibold">
{name}
</div>
);
};
return (
<div className="bg-neutral-800 rounded-lg p-2 border border-white">
<div className="flex items-start justify-between gap-3">
<h1 className="text-base font-bold">{title}</h1>
<a href={link} target="_blank" className="shrink-0">
<button className="bg-gray-500 border border-zinc-500 rounded-lg px-2 py-1 text-sm whitespace-nowrap">
View &gt;
</button>
</a>
</div>
<div className="space-y-2">
<p className="text-sm">{description}</p>
<div className="flex flex-wrap gap-2">
{pills.map((v, k) => {
return <Pill key={k} name={v} />;
})}
</div>
</div>
</div>
);
}
+2 -2
View File
@@ -14,9 +14,9 @@ export default function PlayerCell({ player }: { player: NFLPlayer | null }) {
if (player && player.full_name) {
return (
<div className="flex-wrap flex space-x-2">
<div className="flex items-center gap-2">
<div>
<h1 className="font-semibold text-1xl">{player.abv_name}</h1>
<h1 className="font-semibold text-base">{player.full_name}</h1>
<p>
{player?.position} · {player?.team} {injuryStatus && "·"}{" "}
{player?.injury_status && (
+1 -1
View File
@@ -24,7 +24,7 @@ export function TeamRow({
</h1>
</div>
<div className="space-y-2 space-x-2 whitespace-nowrap overflow-x-scroll">
<div className="space-x-2 whitespace-nowrap overflow-x-auto pb-2">
{roster?.starters.map((v, k) => {
return (
<div className="inline-block" key={k}>
-28
View File
@@ -1,28 +0,0 @@
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>
);
}
@@ -1,68 +0,0 @@
import { useEffect, useRef } from "react";
export default function CountdownModule() {
const countdownRef = useRef<HTMLParagraphElement | null>(null);
const audioRef = useRef<HTMLAudioElement | null>(null);
useEffect(() => {
const dateOfStart = new Date("Sep 7, 2025 20:15:00").getTime();
setInterval(() => {
const currentDate = new Date().getTime();
const difference = dateOfStart - currentDate;
const days = Math.floor(difference / (1000 * 60 * 60 * 24));
const hours = Math.floor(
(difference % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60)
);
const minutes = Math.floor((difference % (1000 * 60 * 60)) / (1000 * 60));
const seconds = Math.floor((difference % (1000 * 60)) / 1000);
if (countdownRef?.current) {
let timerString = "";
timerString += `${days || "0"}d, `;
timerString += `${hours || "0"}h, `;
timerString += `${minutes || "0"}m, `;
timerString += `${seconds || "0"}s`;
if (
days <= 0 &&
hours <= 0 &&
minutes <= 0 &&
seconds <= 0 &&
timerString === ""
) {
timerString = "GO BILLS";
}
countdownRef.current.innerText = timerString;
}
}, 1000);
}, []);
return (
<div className="border border-white bg-blue-800 border-solid rounded-lg w-full mb-4">
<div className="flex items-center justify-between p-2">
<img
onClick={() => {
if (audioRef?.current) {
audioRef.current.play();
}
}}
width="50"
height="50"
className="hover:cursor-pointer"
src={"https://storage.alexav.gg/content/bills.png"}
/>
<p className="text-2xl font-bold" ref={countdownRef}></p>
</div>
<audio
ref={audioRef}
autoPlay={false}
src={
"https://storage.alexav.gg/content/0f4ce103-62d0-4ff1-8b8b-e38becced35d.mp3"
}
/>
</div>
);
}
+11 -13
View File
@@ -15,19 +15,17 @@ const images = [
export default function PictureCarousel() {
return (
<div className="rounded-lg">
<div className="my-2 space-x-2 space-y-2">
{images.map((v, k) => {
return (
<img
key={k}
loading="lazy"
src={v}
className="inline w-[200px] max-h-[100px] object-cover"
/>
);
})}
</div>
<div className="my-2 grid grid-cols-2 sm:grid-cols-3 gap-2">
{images.map((v, k) => {
return (
<img
key={k}
loading="lazy"
src={v}
className="w-full h-24 object-cover rounded-md"
/>
);
})}
</div>
);
}
+109 -99
View File
@@ -1,9 +1,68 @@
import { useEffect, useState } from "react";
import { BillsScore, SabresScore } from "../../utils/types";
import { BillsScore, SabresScore, YankeesScore } from "../../utils/types";
interface TeamRowProps {
functionClick?: () => void;
width?: number;
venue: string;
date: string;
homeTeam: {
name: string;
abbreviation: string;
score: number;
logo: string;
};
awayTeam: {
name: string;
abbreviation: string;
score: number;
logo: string;
};
}
function TeamRow({
homeTeam,
awayTeam,
width = 50,
venue,
date,
functionClick,
}: TeamRowProps) {
return (
<div className="flex flex-row justify-between">
<div className="flex flex-row py-2 space-x-2">
<img width={width} className="object-cover h-20" src={homeTeam.logo} />
<img
onClick={() => {
if (
(awayTeam.abbreviation === "BUF" ||
homeTeam.abbreviation === "BUF") &&
functionClick
) {
functionClick();
}
}}
width={width}
className="object-cover h-20"
src={awayTeam.logo}
/>
</div>
<div className="p-2 text-2xl flex items-end flex-col justify-center">
<p>
{awayTeam.abbreviation} {awayTeam.score} - {homeTeam.abbreviation}{" "}
{homeTeam.score}
</p>
<p className="text-sm">
{venue} - {new Date(date || Date.now()).toLocaleDateString()}
</p>
</div>
</div>
);
}
export default function ScoreModule() {
const [billsScore, setBillsScore] = useState<BillsScore | null>(null);
const [sabresScore, setSabresScore] = useState<SabresScore | null>(null);
const [yankeesScore, setYankeesScore] = useState<YankeesScore | null>(null);
const [warpath, setWarpath] = useState<boolean>(false);
useEffect(() => {
@@ -22,113 +81,64 @@ export default function ScoreModule() {
const data = await res.json();
setSabresScore(data);
} else {
console.error("Failed to fetch Sabresscore; " + res.statusText);
console.error("Failed to fetch Sabres score; " + res.statusText);
}
}
async function fetchYankeesScore() {
const res = await fetch("https://api.alexav.gg/v4/sports/scores/yankees");
if (res.ok) {
const data = await res.json();
setYankeesScore(data);
} else {
console.error("Failed to fetch Yankees score; " + res.statusText);
}
}
fetchYankeesScore();
fetchBillsScore();
fetchSabresScore();
}, []);
return (
<div className="rounded-lg w-full mb-2">
<div className="flex flex-row justify-between">
<div className="flex flex-row py-2">
<img
onClick={() => {
if (sabresScore?.homeTeam.abbreviation === "BUF")
setWarpath(true);
}}
width="80"
className="shrink-0"
src={sabresScore?.homeTeam.logo}
/>
<img
onClick={() => {
if (sabresScore?.awayTeam.abbreviation === "BUF")
setWarpath(true);
}}
width="80"
className="shrink-0"
src={sabresScore?.awayTeam.logo}
/>
</div>
<div className="p-2 text-2xl flex items-end flex-col">
<p>
{sabresScore?.homeTeam.abbreviation} {sabresScore?.homeTeam.score} -{" "}
{sabresScore?.awayTeam.abbreviation} {sabresScore?.awayTeam.score}
</p>
<p className="text-sm">
{sabresScore?.venue} -{" "}
{new Date(sabresScore?.date || Date.now()).toLocaleDateString()}
</p>
</div>
<div className="rounded-lg w-full">
{sabresScore?.homeTeam && sabresScore.awayTeam && (
<TeamRow
homeTeam={sabresScore.homeTeam}
awayTeam={sabresScore.awayTeam}
date={sabresScore.date}
venue={sabresScore.venue}
functionClick={() => setWarpath(true)}
/>
)}
{billsScore?.homeTeam && billsScore.awayTeam && (
<TeamRow
homeTeam={billsScore.homeTeam}
awayTeam={billsScore.awayTeam}
width={50}
date={billsScore.gameDay}
venue={billsScore.venue}
/>
)}
{yankeesScore?.homeTeam && yankeesScore.awayTeam && (
<TeamRow
homeTeam={yankeesScore.homeTeam}
awayTeam={yankeesScore.awayTeam}
venue={yankeesScore.venue}
date={yankeesScore.date}
/>
)}
{warpath && (
<audio
src={"https://storage.alexav.gg/content/warpath.mp3"}
autoPlay={true}
onTimeUpdate={(e) => {
if (e.currentTarget.duration === e.currentTarget.currentTime) {
setWarpath(false);
}
}}
/>
)}
</div>
<div className="flex flex-row justify-between">
<div className="flex flex-row space-x-6 px-4 items-center">
<img
width="80"
className="w-full h-[50px]"
src={billsScore?.homeTeam.logo}
/>
<img
width="80"
className="w-full h-[50px]"
src={billsScore?.awayTeam.logo}
/>
</div>
<div className="p-2 text-2xl flex items-end flex-col">
<p>
{billsScore?.homeTeam.abbreviation} {billsScore?.homeTeam.score} -{" "}
{billsScore?.awayTeam.abbreviation} {billsScore?.awayTeam.score}
</p>
<p className="text-sm">
{billsScore?.venue} -{" "}
{new Date(billsScore?.gameDay || Date.now()).toLocaleDateString()}
</p>
</div>
</div>
{/* <div className="flex justify-between">
<div>
<img
onClick={() => {
if (type === "sabres") {
setWarpath(true);
}
}}
width="100"
className="p-4"
src={IMAGES[type]}
/>
</div>
<div className="flex justify-center items-center px-6">
<div>
<h1 className="text-2xl">
{type === "bills"
? billsScore?.name
: `${sabresScore?.awayTeam.abbrev || "BUF"} @ ${sabresScore?.homeTeam.abbrev || "BUF"}`}
</h1>
<p className="float-right text-[20px]">
{type === "bills"
? billsScore?.score
: `${sabresScore && sabresScore?.sabresScore > sabresScore?.oppScore ? "W" : "L"} ${sabresScore?.sabresScore || 0}-${sabresScore?.oppScore || 0}`}
</p>
</div>
</div>
</div>
*/}
{warpath && (
<audio
src={"https://storage.alexav.gg/content/warpath.mp3"}
autoPlay={true}
onTimeUpdate={(e) => {
if (e.currentTarget.duration === e.currentTarget.currentTime) {
setWarpath(false);
}
}}
/>
)}
</div>
);
}
+11
View File
@@ -0,0 +1,11 @@
import { hydrateRoot } from "react-dom/client";
import { BrowserRouter } from "react-router-dom";
import "./assets/main.css";
import App from "./pages/App.tsx";
hydrateRoot(
document.getElementById("root")!,
<BrowserRouter>
<App />
</BrowserRouter>,
);
+17
View File
@@ -0,0 +1,17 @@
import { renderToString } from "react-dom/server";
import * as RouterExports from "react-router-dom";
import App from "./pages/App";
// Vite's SSR runner resolves react-router-dom to its CommonJS build, so the
// named exports land on `.default`; fall back to the namespace when not.
const { StaticRouter } =
(RouterExports as { default?: typeof RouterExports }).default ?? RouterExports;
export function render(url: string) {
const html = renderToString(
<StaticRouter location={url}>
<App />
</StaticRouter>,
);
return { html };
}
-15
View File
@@ -1,15 +0,0 @@
import { createRoot } from "react-dom/client";
import { createBrowserRouter, RouterProvider } from "react-router-dom";
import "./assets/main.css";
import App from "./pages/App.tsx";
const router = createBrowserRouter([
{
path: "/",
element: <App />,
},
]);
createRoot(document.getElementById("root")!).render(
<RouterProvider router={router} />
);
+90 -49
View File
@@ -1,63 +1,104 @@
import { useState } from "react";
import Layout from "../components/Layout";
import Clock from "../components/modules/Clock";
import Button from "../components/Button";
import Home from "./Home";
import ScoreModule from "../components/modules/ScoreModule";
import PicturesPage from "./Pictures";
import ProjectCard from "../components/ProjectCard";
import FantasyStatus from "../components/FantasyStatus";
import PictureCarousel from "../components/modules/PictureCarousel";
import Footer from "../components/Footer";
export default function App() {
const [tab, setTab] = useState<"home" | "scores" | "fantasy" | "pictures">(
"home",
);
return (
<Layout>
<div className="border-3 bg-gradient-to-t from-[#003087] to-[#FFB81C] border-black w-full sm:w-[500px]">
<div className="bg-[#003087] w-full">
<div className="grid grid-cols-3 gap-2 p-2 text-2xl">
<h1 className="font-semibold text-white">welcome</h1>
<Clock />
<h1 className="text-right text-transparent font-bold bg-clip-text bg-gradient-to-t from-[#003087] to-[#FFB81C]">
GO SABRES!
<main className="my-12 sm:my-20">
<div className="py-2">
<p className="opacity-85 text-sm text-left">HI, I'M ALEX</p>
<h1 className="text-3xl sm:text-4xl text-balance py-2">
I write software sometimes, then I spend twice as long breaking it
</h1>
</div>
<div className="py-2 space-y-6">
<p>
My day job is software QA at{" "}
<a
className="text-lime-300 hover:underline"
href="https://medal.tv/u/alexav"
target="_blank"
>
Medal.tv
</a>{" "}
(
<a
href="https://medal.tv/careers"
target="_blank"
className="underline"
>
we're hiring!
</a>
), then by night I play with my homelab rack or write some mediocre
JavaScript.
</p>
<p>
Based in Western New York (go bills, go sabres). Working on learning
Java, C#. Pointless software for funzies is my passion.
</p>
</div>
<hr className="my-4 border-zinc-800" />
<p className="opacity-85 text-sm text-left pb-2">THINGS I'VE BUILT</p>
<div className="my-2 space-y-2">
<ProjectCard
title={"LED Matrix Scoreboard"}
description={
"A physical scoreboard driven by an LED matrix, pulls live sports data and renders pixel by pixel."
}
link="https://git.alexav.gg/alex/scoreboard"
pills={["Python", "Raspberry Pi", "Hardware"]}
/>
<ProjectCard
title={"Subber"}
description={
"Keeps track on your subscriptions and generates clear metrics for you to manage."
}
link="https://github.com/azpha/subber"
pills={["TypeScript", "Tailwind", "Prisma"]}
/>
<ProjectCard
title={"Snapchat Memory Extractor"}
description={
"Extracts your Snapchat memories from your data export in a cleaner format than their horrible index.html."
}
link="https://github.com/azpha/snapchat-memory-extractor"
pills={["Python", "Automation"]}
/>
<ProjectCard
title={"Homelab"}
description={
"My tinkering sandbox. Full CI/CD pipeline for people to learn & take inspiration from."
}
link="https://git.alexav.gg/alex/homelab"
pills={["Docker", "Ansible", "Python", "Automation"]}
/>
</div>
<hr className="my-4 border-zinc-800" />
<p className="opacity-85 text-sm text-left pb-2">FUN STUFF</p>
<div className="my-2 space-y-6">
<div>
<h1 className="text-sm opacity-85 font-semibold">Fantasy</h1>
<FantasyStatus />
</div>
<div>
<h1 className="text-sm opacity-85 font-semibold">
My Life, Pictured
</h1>
<PictureCarousel />
</div>
</div>
<img
className="h-[100px] object-[25%_35%] w-full object-cover border-b-black border-b-2 border-t-2 border-t-black"
style={{
objectPosition: "",
}}
src="https://storage.alexav.gg/content/sabres-board.jpg"
/>
<hr className="my-4 border-zinc-800" />
<div className="flex flex-row space-x-2 justify-center text-2xl my-2">
<Button active={tab === "home"} onClick={() => setTab("home")}>
home
</Button>
<Button active={tab === "scores"} onClick={() => setTab("scores")}>
scores
</Button>
<Button
active={tab === "pictures"}
onClick={() => setTab("pictures")}
>
pictures
</Button>
<Button active={tab === "fantasy"} onClick={() => setTab("fantasy")}>
fantasy
</Button>
</div>
<hr className="border-black border-1" />
{tab === "home" && <Home />}
{tab === "scores" && <ScoreModule />}
{tab === "pictures" && <PicturesPage />}
{tab === "fantasy" && <FantasyStatus />}
</div>
<Footer />
</main>
</Layout>
);
}
+1 -1
View File
@@ -68,7 +68,7 @@ export default function Home() {
</div>
<div>
<span className="bg-black text-white px-1">teams</span> bills,
sabres, jays
sabres, yanks
</div>
</div>
</div>
+1 -3
View File
@@ -2,9 +2,7 @@ import PictureCarousel from "../components/modules/PictureCarousel";
export default function PicturesPage() {
return (
<div className="p-2 flex flex-col justify-center">
<h1 className="text-2xl font-bold">pictures</h1>
<div className="h-96 overflow-y-auto">
<div className="h-96 w-full overflow-y-auto">
<PictureCarousel />
</div>
</div>
+19
View File
@@ -0,0 +1,19 @@
import FantasyStatus from "../components/FantasyStatus";
import ScoreModule from "../components/modules/ScoreModule";
export default function Sports() {
return (
<div>
<div className="p-2">
<ScoreModule />
</div>
<hr className="border-black border-1" />
<div className="p-2">
<h1 className="text-2xl font-semibold">fantasy</h1>
<FantasyStatus />
</div>
</div>
);
}
+19 -2
View File
@@ -12,13 +12,15 @@ type LastFMImageObject = {
// nfl stuff
interface NFLAPIData {
nflState: NFLState;
matchups: NFLMatchupState[];
team1: NFLMatchupState;
team2: NFLMatchupState;
users: SleeperUser[];
}
interface NFLMatchupState {
points: number;
owner: SleeperUser;
starters: NFLPlayer[];
players: NFLPlayer[];
starters_points: number[];
players_points: object;
}
@@ -63,9 +65,17 @@ type SabresScore = {
awayTeam: NHLTeam;
homeTeam: NHLTeam;
};
type YankeesScore = {
season: number;
week: number;
date: string;
venue: string;
awayTeam: MLBTeam;
homeTeam: MLBTeam;
};
type NFLTeam = {
abbreviation: string;
score: string;
score: number;
name: string;
nickname: string;
logo: string;
@@ -76,6 +86,12 @@ type NHLTeam = {
score: number;
logo: string;
};
type MLBTeam = {
name: string;
abbreviation: string;
logo: string;
score: number;
};
export type {
LastFMMusicObject,
@@ -87,4 +103,5 @@ export type {
NFLPlayer,
BillsScore,
SabresScore,
YankeesScore,
};
+1
View File
@@ -14,6 +14,7 @@
"moduleDetection": "force",
"noEmit": true,
"jsx": "react-jsx",
"types": ["node"],
/* Linting */
"strict": true,
+1
View File
@@ -12,6 +12,7 @@
"isolatedModules": true,
"moduleDetection": "force",
"noEmit": true,
"types": ["node"],
/* Linting */
"strict": true,