redesign
This commit is contained in:
+1
-1
@@ -7,7 +7,7 @@
|
||||
<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"
|
||||
href="https://fonts.googleapis.com/css2?family=Open+Sans:ital,wght@0,300..800;1,300..800&display=swap"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
<link href="https://mst.fntz.net/@alex" rel="me" />
|
||||
|
||||
Binary file not shown.
+1
-1
@@ -1,7 +1,7 @@
|
||||
@import "tailwindcss";
|
||||
|
||||
body {
|
||||
font-family: "Pathway Gothic One", sans-serif;
|
||||
font-family: "Open Sans", sans-serif;
|
||||
background-color: black;
|
||||
|
||||
::-webkit-scrollbar {
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 17 KiB |
@@ -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 justify-between py-4">
|
||||
<h1 className="font-bold">
|
||||
Alex <span className="font-normal text-blue-500">/</span> dev + QA
|
||||
</h1>
|
||||
<div className="space-x-2">
|
||||
<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 sm:w-[95%] md:w-[75%] lg:w-[50%] xl:w-[25%] mx-auto select-none px-4 md:px-0">
|
||||
<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 max-w-fit text-xs p-1 inline-block font-semibold">
|
||||
{name}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="bg-neutral-800 rounded-lg p-2 border border-white">
|
||||
<div className="flex justify-between">
|
||||
<h1 className="text-1xl font-bold">{title}</h1>
|
||||
<a href={link} target="_blank">
|
||||
<button className="bg-gray-500 border border-zinc-500 rounded-lg px-2 py-1 text-sm">
|
||||
View >
|
||||
</button>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2 w-[75%]">
|
||||
<p className="text-sm">{description}</p>
|
||||
<div className="space-x-2">
|
||||
{pills.map((v, k) => {
|
||||
return <Pill key={k} name={v} />;
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
+92
-36
@@ -5,6 +5,12 @@ import Button from "../components/Button";
|
||||
import Home from "./Home";
|
||||
import PicturesPage from "./Pictures";
|
||||
import Sports from "./Sports";
|
||||
import ProjectCard from "../components/ProjectCard";
|
||||
import FantasyStatus from "../components/FantasyStatus";
|
||||
import GameComponent from "../components/info/GameComponent";
|
||||
import MusicComponent from "../components/info/MusicComponent";
|
||||
import PictureCarousel from "../components/modules/PictureCarousel";
|
||||
import Footer from "../components/Footer";
|
||||
|
||||
export default function App() {
|
||||
const [tab, setTab] = useState<"home" | "scores" | "sports" | "pictures">(
|
||||
@@ -13,44 +19,94 @@ export default function App() {
|
||||
|
||||
return (
|
||||
<Layout>
|
||||
<div className="border-3 bg-zinc-700 border-black w-full sm:w-[500px]">
|
||||
<div className="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 font-bold">welcome</h1>
|
||||
<main className="my-24">
|
||||
<div className="py-2">
|
||||
<p className="opacity-85 text-sm text-left">HI, I'M ALEX</p>
|
||||
<h1 className="w-full whitespace-break-spaces text-4xl 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"]}
|
||||
/>
|
||||
|
||||
<hr className="my-4 border-zinc-800" />
|
||||
<p className="opacity-85 text-sm text-left pb-2">FUN STUFF</p>
|
||||
|
||||
<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>
|
||||
|
||||
<Footer />
|
||||
</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/59c4bbeba39eb04cf91b81aa0c2e519ad6cf6f95725f9bd9cc8238523aea9529.webp"
|
||||
/>
|
||||
|
||||
<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 === "pictures"}
|
||||
onClick={() => setTab("pictures")}
|
||||
>
|
||||
pictures
|
||||
</Button>
|
||||
<Button active={tab === "sports"} onClick={() => setTab("sports")}>
|
||||
sports
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<hr className="border-black border-1" />
|
||||
|
||||
{tab === "home" && <Home />}
|
||||
{tab === "pictures" && <PicturesPage />}
|
||||
{tab === "sports" && <Sports />}
|
||||
</div>
|
||||
</main>
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user