Compare commits
10 Commits
9410af0fb4
...
f9f8cc4ad6
| Author | SHA1 | Date | |
|---|---|---|---|
|
f9f8cc4ad6
|
|||
|
cc6e00d777
|
|||
|
03e82380db
|
|||
|
b19b5b1bd2
|
|||
|
780bb5b65a
|
|||
|
86682f6c92
|
|||
|
31c93b0876
|
|||
|
d2c59f878d
|
|||
|
1779ce0bcf
|
|||
| d219024719 |
@@ -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
|
||||
@@ -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
@@ -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
@@ -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>
|
||||
|
||||
Generated
-3888
File diff suppressed because it is too large
Load Diff
+13
-5
@@ -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",
|
||||
|
||||
Generated
+3247
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,2 @@
|
||||
allowBuilds:
|
||||
esbuild: false
|
||||
Binary file not shown.
@@ -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
@@ -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 |
@@ -18,25 +18,18 @@ 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">
|
||||
<div className="mt-2 space-y-4">
|
||||
{personalRoster && opposingRoster && (
|
||||
<>
|
||||
<TeamRow
|
||||
@@ -50,6 +43,5 @@ export default function FantasyStatus() {
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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">© 2026 Alex Frantz</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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 >
|
||||
</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>
|
||||
);
|
||||
}
|
||||
@@ -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 && (
|
||||
|
||||
@@ -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}>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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">
|
||||
<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="inline w-[200px] max-h-[100px] object-cover"
|
||||
className="w-full h-24 object-cover rounded-md"
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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,47 +81,52 @@ 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}
|
||||
<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)}
|
||||
/>
|
||||
<img
|
||||
onClick={() => {
|
||||
if (sabresScore?.awayTeam.abbreviation === "BUF")
|
||||
setWarpath(true);
|
||||
}}
|
||||
width="80"
|
||||
className="shrink-0"
|
||||
src={sabresScore?.awayTeam.logo}
|
||||
)}
|
||||
{billsScore?.homeTeam && billsScore.awayTeam && (
|
||||
<TeamRow
|
||||
homeTeam={billsScore.homeTeam}
|
||||
awayTeam={billsScore.awayTeam}
|
||||
width={50}
|
||||
date={billsScore.gameDay}
|
||||
venue={billsScore.venue}
|
||||
/>
|
||||
</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>
|
||||
)}
|
||||
{yankeesScore?.homeTeam && yankeesScore.awayTeam && (
|
||||
<TeamRow
|
||||
homeTeam={yankeesScore.homeTeam}
|
||||
awayTeam={yankeesScore.awayTeam}
|
||||
venue={yankeesScore.venue}
|
||||
date={yankeesScore.date}
|
||||
/>
|
||||
)}
|
||||
|
||||
{warpath && (
|
||||
<audio
|
||||
@@ -76,59 +140,5 @@ export default function ScoreModule() {
|
||||
/>
|
||||
)}
|
||||
</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>
|
||||
*/}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>,
|
||||
);
|
||||
@@ -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 };
|
||||
}
|
||||
@@ -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} />
|
||||
);
|
||||
+88
-47
@@ -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>
|
||||
|
||||
<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"
|
||||
/>
|
||||
|
||||
<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")}
|
||||
<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"
|
||||
>
|
||||
pictures
|
||||
</Button>
|
||||
<Button active={tab === "fantasy"} onClick={() => setTab("fantasy")}>
|
||||
fantasy
|
||||
</Button>
|
||||
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="border-black border-1" />
|
||||
<hr className="my-4 border-zinc-800" />
|
||||
|
||||
{tab === "home" && <Home />}
|
||||
{tab === "scores" && <ScoreModule />}
|
||||
{tab === "pictures" && <PicturesPage />}
|
||||
{tab === "fantasy" && <FantasyStatus />}
|
||||
<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>
|
||||
|
||||
<hr className="my-4 border-zinc-800" />
|
||||
|
||||
<Footer />
|
||||
</main>
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
|
||||
+1
-1
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
@@ -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,
|
||||
};
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
"moduleDetection": "force",
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx",
|
||||
"types": ["node"],
|
||||
|
||||
/* Linting */
|
||||
"strict": true,
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
"isolatedModules": true,
|
||||
"moduleDetection": "force",
|
||||
"noEmit": true,
|
||||
"types": ["node"],
|
||||
|
||||
/* Linting */
|
||||
"strict": true,
|
||||
|
||||
Reference in New Issue
Block a user