This commit is contained in:
2025-02-09 14:28:51 -05:00
parent d171572400
commit 1a51cd28c7
66 changed files with 4122 additions and 4298 deletions
+45 -90
View File
@@ -1,47 +1,30 @@
'use client';
import { useState, useEffect } from 'react';
import InputBox from './InputBox';
import { submitContact } from '@/actions/contact';
export default function ContactForm() {
const [ error, setError ] = useState<string>("");
const [ success, setSuccess ] = useState<boolean>(false);
const [ inputState, setInputState ] = useState<{name: string, email: string, subject: string, message: string}>({
name: "",
email: "",
subject: "",
message: ""
});
const [ pending, setPending ] = useState<boolean>(false);
const onSubmit = () => {
if (
inputState.name === ""
|| inputState.email === ""
|| inputState.subject === ""
|| inputState.message === ""
) {
setError("A required field was missing")
} else {
console.log("Form submitted")
const handleSubmission = async (formData: FormData) => {
setPending(true)
fetch(import.meta.env.VITE_API_BASE_URL + "/contact", {
method: "post",
headers: {
'content-type': 'application/json'
},
body: JSON.stringify(inputState)
}).then((res) => {
if (res.ok) {
setSuccess(true)
} else {
if (res.status === 429) {
setError("Too many tries! Try again later")
} else {
setError("An error occurred while submitting your info.")
}
}
}).catch((e) => {
console.error(e)
setError("An error occurred while submitting your info.")
})
try {
const result = await submitContact(formData)
if (result.error || !result.success) {
setError('An unexpected error occurred')
} else setSuccess(true)
const form = document.getElementById('contact-form') as HTMLFormElement
form.reset()
} catch (e) {
setError('An unexpected error occurred')
} finally {
setPending(false)
}
}
@@ -60,82 +43,54 @@ export default function ContactForm() {
<div className="rounded-lg p-2 w-full md:w-[50%] mx-auto">
<h1 className="text-2xl font-bold text-center pb-8">Contact Me</h1>
<div className="space-y-2 text-center">
<form id="contact-form" action={handleSubmission} className="space-y-2 text-center">
<InputBox
isText={true}
name="name"
placeholder="Your name.."
onChange={(value: string) => {
setInputState((prevState) => {
return {
...prevState,
name: value
}
})
}}
disabled={pending}
/>
<InputBox
name="email"
type="email"
isText={true}
placeholder="your@supercool.email"
onChange={(value: string) => {
setInputState((prevState) => {
return {
...prevState,
email: value
}
})
}}
disabled={pending}
/>
<InputBox
isText={true}
name="subject"
placeholder="Subject.."
onChange={(value: string) => {
setInputState((prevState) => {
return {
...prevState,
subject: value
}
})
}}
disabled={pending}
/>
<InputBox
isText={false}
name="message"
placeholder="Your message here.."
onChange={(value: string) => {
setInputState((prevState) => {
return {
...prevState,
message: value
}
})
}}
disabled={pending}
/>
</div>
<div className="block text-center">
<button
type="button"
className="bg-white w-full text-black font-bold rounded-lg max-w-fit p-2"
onClick={onSubmit}
>
Submit
</button>
{
error && (
<p className="text-red-400 font-bold text-center pt-2">{error}</p>
)
}
<div className="block text-center">
<button
type="submit"
className="bg-white w-full text-black font-bold rounded-lg max-w-fit p-2"
>
Submit
</button>
{
error && (
<p className="text-red-400 font-bold text-center pt-2">{error}</p>
)
}
{
success && (
<p className="font-bold text-green-500 text-center pt-2">Success! Your message has been received</p>
)
}
</div>
{
success && (
<p className="font-bold text-green-500 text-center pt-2">Success! Your message has been received</p>
)
}
</div>
</form>
</div>
)
}
-26
View File
@@ -1,26 +0,0 @@
export default function CustomButton({
to,
icon,
children
}: {
to: string,
icon?: JSX.Element,
children: string | JSX.Element | JSX.Element[]
}) {
return (
<div>
<a href={to} target="_blank">
<div className={`bg-black text-white text-lg font-semibold text-left p-2 w-52 rounded-lg`}>
<div className="px-1 space-x-2">
<div className="inline">
{ icon && icon }
</div>
<div className="inline">
{children}
</div>
</div>
</div>
</a>
</div>
)
}
+34
View File
@@ -0,0 +1,34 @@
export default async function DataComponent({
url,
header,
projectHeader,
projectSubheader,
projectImage
}: {
url: string,
header: string,
projectHeader: string,
projectSubheader: string,
projectImage?: string
}) {
return (
<a href={url} target="_blank">
<div className="bg-white rounded-lg text-black p-2">
<h1 className="text-center font-semibold">{header}</h1>
<div className="block">
<div className="flex items-center">
{
projectImage && <img alt="Project Image" src={projectImage} width={100} height={100} className="w-[80px] h-[50px] object-cover" />
}
<div className="flex flex-col mx-2 min-w-0 flex-grow">
<h1 className="font-bold text-left truncate">{projectHeader}</h1>
<h1 className="text-left truncate">{projectSubheader}</h1>
</div>
</div>
</div>
</div>
</a>
)
}
@@ -0,0 +1,45 @@
import DataComponent from '../DataComponent';
import type { ContentTrackerItem } from '@/utils/types';
const fetchData = async () => {
const { data } = await fetch("https://api.alexav.gg/v4/tracker/latest/book", {
method: 'get'
}).then((res) => res.json())
return data
}
export default async function RecentBook() {
const data = await fetchData() as ContentTrackerItem
const getDateString = (startedOn: Date, finished: boolean) => {
if (startedOn && !finished) {
const startedOnDate = new Date(startedOn);
return "Started on " + startedOnDate.toLocaleDateString();
} else if (finished) {
return "Finished!"
} else {
return "Just Added"
}
}
if (data) {
return (
<DataComponent
header={"What I'm reading"}
projectHeader={data.title}
projectSubheader={getDateString(data.startedOn, data.finished)}
url={"https://tracker.alexav.gg/?type=books&id=" + data.id}
projectImage={data.image}
/>
)
} else {
return (
<DataComponent
header={"What I'm watching"}
projectHeader={"No data :("}
projectSubheader={"Nothing was returned"}
url={"/"}
/>
)
}
}
@@ -0,0 +1,46 @@
import DataComponent from '../DataComponent';
import type { MedalClip } from '@/utils/types';
const fetchData = async () => {
const { data } = await fetch("https://api.alexav.gg/v4/social/medal", {
method: 'get'
}).then((res) => res.json())
return data[0]
}
export default async function RecentClip() {
const data = await fetchData() as MedalClip
const translateToWebsiteUrl = (url: string) => {
return url.replace('/clip/', "/clips/")
}
// i'm a neat man what can i say
const likeOrLikes = (likes: number) => {
return (likes > 1 && likes !== 0) ? "likes" : "like"
}
const viewOrViews = (views: number) => {
return (views > 1 && views !== 0) ? "views" : "view"
}
if (data) {
return (
<DataComponent
url={translateToWebsiteUrl(data.directClipUrl)}
header={"Clips I've taken"}
projectHeader={data.contentTitle}
projectSubheader={`${data.contentLikes} ${likeOrLikes(data.contentLikes)} | ${data.contentViews} ${viewOrViews(data.contentViews)}`}
projectImage={data.contentThumbnail}
/>
)
} else {
return (
<DataComponent
header={"What I'm watching"}
projectHeader={"No data :("}
projectSubheader={"Nothing was returned"}
url={"/"}
/>
)
}
}
@@ -0,0 +1,43 @@
import DataComponent from '../DataComponent';
import type { GameData } from '@/utils/types';
const fetchData = async () => {
const { data } = await fetch("https://api.alexav.gg/v4/game", {
method: 'get'
}).then((res) => res.json())
return data
}
export default async function RecentGame() {
const data = await fetchData() as GameData
const getTime = (time: number) => {
const date = new Date(time*1000);
return `${date.toLocaleDateString()} at ${date.toLocaleTimeString()}`
}
if (data) {
return (
<DataComponent
url={"https://medal.tv/u/alexav"}
header={"Games I'm playing"}
projectHeader={data.name}
projectSubheader={
data.finished ? `Last played on ${getTime(data.startedOn)}`
: `Started on ${getTime(data.startedOn)}`
}
projectImage={"https://storage.alexav.gg/content/933d5523-5e20-4040-9fe9-b73552eac8ab.png"}
/>
)
} else {
return (
<DataComponent
header={"What I'm watching"}
projectHeader={"No data :("}
projectSubheader={"Nothing was returned"}
url={"/"}
/>
)
}
}
@@ -0,0 +1,34 @@
import DataComponent from '../DataComponent';
import type { MusicData } from '@/utils/types';
const fetchData = async () => {
const { data } = await fetch("https://api.alexav.gg/v4/social/music", {
method: 'get'
}).then((res) => res.json())
return data.tracks[0]
}
export default async function RecentListen() {
const data = await fetchData() as MusicData
if (data) {
return (
<DataComponent
url={"https://last.fm/user/lulawex"}
header={"Music I'm listening to"}
projectHeader={data.name}
projectSubheader={data.artist["#text"]}
projectImage={data.image.filter((v) => v.size === "medium")[0]["#text"]}
/>
)
} else {
return (
<DataComponent
header={"What I'm watching"}
projectHeader={"No data :("}
projectSubheader={"Nothing was returned"}
url={"/"}
/>
)
}
}
@@ -0,0 +1,44 @@
import DataComponent from '../DataComponent';
import type { WatchData } from '@/utils/types';
const fetchData = async () => {
const data = await fetch("https://api.alexav.gg/v4/tracker/latest/tv", {
method: 'get'
});
return await data.json()
}
export default async function RecentWatch() {
const data = await fetchData() as WatchData
const getSubheaderText = () => {
if (data?.data.finished) {
return "Finished!"
} else if (data?.data.startedOn) {
return "Last update on " + new Date(data.data.updatedAt).toLocaleDateString()
} else {
return "Just Added"
}
}
if (data) {
return (
<DataComponent
header={"What I'm watching"}
projectHeader={data.data.title}
projectSubheader={ getSubheaderText() }
url={"https://tracker.alexav.gg/?type=tv&id=" + data.data.id}
projectImage={data.data.image}
/>
)
} else {
return (
<DataComponent
header={"What I'm watching"}
projectHeader={"No data :("}
projectSubheader={"Nothing was returned"}
url={"/"}
/>
)
}
}
-93
View File
@@ -1,93 +0,0 @@
import { useState, useEffect, useCallback } from 'react';
interface Particle {
id: number;
angle: number;
color: string;
distance: number;
opacity: number;
x: number;
y: number;
}
export default function Firework() {
const [particles, setParticles] = useState<Particle[]>([]);
const getRandomPosition = (): { x: number; y: number } => {
// Get container dimensions (in percentage to be responsive)
return {
x: Math.random() * 100, // Random position from 0% to 100% of container width
y: Math.random() * 70 + 15, // Random position from 15% to 85% of container height
};
};
const createParticles = useCallback((): Particle[] => {
const particleCount = 30;
const colors = ['red', 'gold', 'blue', 'green', 'purple', 'pink'];
const position = getRandomPosition();
return Array.from({ length: particleCount }, (_, i) => ({
id: Date.now() + i,
angle: (i * 360) / particleCount,
color: colors[Math.floor(Math.random() * colors.length)],
distance: 0,
opacity: 1,
x: position.x,
y: position.y,
}));
}, []);
const explode = useCallback(() => {
const newParticles = createParticles();
setParticles(prev => [...prev, ...newParticles]);
setTimeout(() => {
setParticles(prev => prev.filter(p => p.id !== newParticles[0].id));
}, 500);
}, [createParticles]);
// Automatic firework launcher
useEffect(() => {
const launchInterval = setInterval(() => {
explode();
}, 2000); // Launch a new firework every 2 seconds
return () => clearInterval(launchInterval);
}, [explode]);
// Particle animation
useEffect(() => {
if (particles.length > 0) {
const animationInterval = setInterval(() => {
setParticles(prev =>
prev.map(particle => ({
...particle,
distance: particle.distance + 2,
opacity: particle.opacity - 0.02
}))
);
}, 20);
return () => clearInterval(animationInterval);
}
}, [particles.length]);
return (
<div className="fixed h-screen w-full overflow-hidden">
{particles.map(particle => (
<div
key={particle.id}
className="absolute h-2 w-2 rounded-full"
style={{
backgroundColor: particle.color,
transform: `rotate(${particle.angle}deg) translateX(${particle.distance}px)`,
opacity: particle.opacity,
transition: 'transform 20ms linear, opacity 20ms linear',
left: `${particle.x}%`,
top: `${particle.y}%`,
}}
/>
))}
</div>
);
}
+4 -9
View File
@@ -1,5 +1,6 @@
"use client";
import { useState, useEffect } from 'react';
import { Link as RouterLink } from 'react-router-dom';
export default function Footer() {
// states
@@ -18,12 +19,6 @@ export default function Footer() {
<h1 className="font-bold text-sm">&copy; {new Date().getFullYear()}, Alex Frantz. All Rights Reserved</h1>
<p>All logos & images on this page belong to their respective owners.</p>
<div className="max-w-fit mx-auto">
<RouterLink to={"/privacy"}>
<p className="underline">Privacy</p>
</RouterLink>
</div>
<hr className="my-2 w-1/2 mx-auto" />
{
@@ -34,8 +29,8 @@ export default function Footer() {
) : (
<button onClick={() => {
setMailState(true)
navigator.clipboard.writeText("alex@alexav.gg")
}} className="hover:underline">alex@alexav.gg</button>
navigator.clipboard.writeText("alex@vierfrantz.com")
}} className="hover:underline">alex@vierfrantz.com</button>
)
}
</div>
+111 -40
View File
@@ -1,43 +1,25 @@
import { useState, useEffect } from "react"
const quoteArray = [
"It works on my machine",
"Revert 'Revert 'Revert",
"'fuck the tests'",
"this is so cooked",
"Professional Production Tester",
"I break hearts to save lives.",
"go bills",
"josh allen is my goat",
"lets go buffalo",
"oorah",
"how bout them bills??",
"where else would you rather be..",
"..than right here, right now?!"
]
export default function Header({
isAudioActive = false
}: {
isAudioActive: boolean
}) {
const quoteArray = [
"It works on my machine",
"Revert 'Revert 'Revert",
"'fuck the tests'",
"this is so cooked",
"Professional Production Tester",
"I break hearts to save lives.",
"go bills",
"josh allen is my goat",
"lets go buffalo",
"oorah",
"how bout them bills??",
"where else would you rather be..",
"..than right here, right now?!"
]
const [ quote, setQuote ] = useState<string>(quoteArray[0]);
async function getQuote() {
return quoteArray[Math.floor(Math.random() * quoteArray.length)]
}
useEffect(() => {
setQuote(quoteArray[Math.floor(Math.random() * quoteArray.length)])
}, [])
const drawNewQuote = () => {
const newQuote = quoteArray[Math.floor(Math.random() * quoteArray.length)]
// redraw if this quote == current quote
if (newQuote !== quote) {
setQuote(newQuote);
} else drawNewQuote();
}
useEffect(() => {
if (isAudioActive) {
setQuote("go bills")
}
}, [isAudioActive])
export default async function Header() {
const quote = await getQuote()
return (
<div className="bg-zinc-900 p-4 flex justify-between">
@@ -45,7 +27,7 @@ export default function Header({
<a href="/" className="hover:underline">
<h1 className="text-2xl font-bold">Alex Frantz</h1>
</a>
<p className="opacity-50 italic select-none" onClick={drawNewQuote}>{quote}</p>
<p className="opacity-50 italic select-none">{quote}</p>
</div>
<div className="space-x-2 items-center justify-center flex select-none">
<a className="hover:underline" href="#projects">Projects</a>
@@ -54,4 +36,93 @@ export default function Header({
</div>
</div>
)
}
}
// "use client";
// import { useState, useEffect, useCallback } from "react"
// export default function Header() {
// const [ shouldShowAudio, setShouldShowAudio ] = useState<boolean>(false)
// const [ pressedKeys, setPressedKeys ] = useState<string[]>([])
// const keyCombination = [
// "ArrowUp",
// "ArrowUp",
// "ArrowDown",
// "ArrowDown",
// "ArrowLeft",
// "ArrowRight",
// "ArrowLeft",
// "ArrowRight",
// "b",
// "a",
// "Enter"
// ]
// const isArraysEqual = (arr1: string[], arr2: string[]) => {
// return arr1.length === arr2.length &&
// arr1.every((val, ind) => val === arr2[ind]);
// }
// const onKeyDown = useCallback((e: KeyboardEvent) => {
// const currentlyPressedKeys = pressedKeys;
// currentlyPressedKeys.push(e.key)
// if (!isArraysEqual(keyCombination, currentlyPressedKeys) && keyCombination.filter((v) => v === e.key) && !e.repeat) {
// setPressedKeys(() => {
// return currentlyPressedKeys
// })
// } else {
// document.removeEventListener('keydown', onKeyDown)
// setShouldShowAudio(true)
// console.log('heeey-ey-ey-ey')
// }
// }, [])
// useEffect(() => {
// document.addEventListener('keydown', onKeyDown);
// })
// const quoteArray = [
// "It works on my machine",
// "Revert 'Revert 'Revert",
// "'fuck the tests'",
// "this is so cooked",
// "Professional Production Tester",
// "I break hearts to save lives.",
// "go bills",
// "josh allen is my goat",
// "lets go buffalo",
// "oorah",
// "how bout them bills??",
// "where else would you rather be..",
// "..than right here, right now?!"
// ]
// const [ quote, setQuote ] = useState<string>(quoteArray[0]);
// useEffect(() => {
// setQuote(quoteArray[Math.floor(Math.random() * quoteArray.length)])
// }, [])
// const drawNewQuote = () => {
// const newQuote = quoteArray[Math.floor(Math.random() * quoteArray.length)]
// // redraw if this quote == current quote
// if (newQuote !== quote) {
// setQuote(newQuote);
// } else drawNewQuote();
// }
// return (
// <div className="bg-zinc-900 p-4 flex justify-between">
// { shouldShowAudio && <audio onEnded={() => setShouldShowAudio(false)} autoPlay={true} src={"https://storage.alexav.gg/content/0f4ce103-62d0-4ff1-8b8b-e38becced35d.mp3"} />}
// <div>
// <a href="/" className="hover:underline">
// <h1 className="text-2xl font-bold">Alex Frantz</h1>
// </a>
// <p className="opacity-50 italic select-none" onClick={drawNewQuote}>{quote}</p>
// </div>
// <div className="space-x-2 items-center justify-center flex select-none">
// <a className="hover:underline" href="#projects">Projects</a>
// <a className="hover:underline" href="#data">Data</a>
// <a className="hover:underline" href="#contact">Contact</a>
// </div>
// </div>
// )
// }
+10 -4
View File
@@ -2,22 +2,27 @@ export default function InputBox({
name,
placeholder = "",
isText = true,
disabled = false,
type = "text",
onChange
}: {
name: string,
isText?: boolean,
disabled?: boolean,
placeholder?: string,
onChange: (value: string) => void
type?: string,
onChange?: (value: string) => void
}) {
if (isText) {
return (
<div className="block">
<input
type="text"
type={type}
name={name}
placeholder={placeholder}
className="rounded-lg p-2 w-1/2 text-black"
onChange={({target}) => onChange(target.value)}
onChange={onChange ? ({target}) => onChange(target.value) : undefined}
disabled={disabled}
/>
</div>
)
@@ -27,7 +32,8 @@ export default function InputBox({
<textarea
name={name}
placeholder={placeholder}
onChange={({ target }) => onChange(target.value)}
onChange={onChange ? ({ target }) => onChange(target.value) : undefined}
disabled={disabled}
className="rounded-lg p-2 w-1/2 h-32 text-black"
/>
</div>
-56
View File
@@ -1,56 +0,0 @@
import Header from "./Header"
import Footer from "./Footer"
import { useState, useEffect, useCallback } from 'react';
export default function Layout({
children
}: {
children: JSX.Element | JSX.Element[]
}) {
// teehee
const [ shouldShowAudio, setShouldShowAudio ] = useState<boolean>(false)
const [ pressedKeys, setPressedKeys ] = useState<string[]>([])
const keyCombination = [
"ArrowUp",
"ArrowUp",
"ArrowDown",
"ArrowDown",
"ArrowLeft",
"ArrowRight",
"ArrowLeft",
"ArrowRight",
"b",
"a",
"Enter"
]
const isArraysEqual = (arr1: string[], arr2: string[]) => {
return arr1.length === arr2.length &&
arr1.every((val, ind) => val === arr2[ind]);
}
const onKeyDown = useCallback((e: KeyboardEvent) => {
const currentlyPressedKeys = pressedKeys;
currentlyPressedKeys.push(e.key)
if (!isArraysEqual(keyCombination, currentlyPressedKeys) && keyCombination.filter((v) => v === e.key) && !e.repeat) {
setPressedKeys(() => {
return currentlyPressedKeys
})
} else {
document.removeEventListener('keydown', onKeyDown)
setShouldShowAudio(true)
console.log('heeey-ey-ey-ey')
}
}, [])
useEffect(() => {
document.addEventListener('keydown', onKeyDown);
})
return (
<main className="bg-black select-none text-white min-h-screen">
<Header isAudioActive={shouldShowAudio} />
{children}
<Footer />
{shouldShowAudio && <audio onEnded={() => setShouldShowAudio(false)} autoPlay={true} src={"https://storage.alexav.gg/content/0f4ce103-62d0-4ff1-8b8b-e38becced35d.mp3"} />}
</main>
)
}
-20
View File
@@ -1,20 +0,0 @@
export default function ListBox({
title,
points
}: {
title: string,
points: string[]
}) {
return (
<div className="bg-slate-100 text-black rounded-lg p-4">
<h3 className="text-lg font-bold">{title}</h3>
<ul className="mt-2 space-y-2 text-muted-foreground">
{
points.map((v,k) => {
return <li key={k}>{v}</li>
})
}
</ul>
</div>
)
}
-30
View File
@@ -1,30 +0,0 @@
export default function Project({
name,
description,
urlTo,
tags
}: {
name: string,
description: string,
urlTo?: string,
tags: string[]
}) {
return (
<a href={urlTo} target="_blank">
<div className="bg-white text-black mx-auto rounded-lg overflow-hidden">
<div className="p-4 space-y-2">
<h1 className="font-bold text-xl">{name}</h1>
<p>{description}</p>
<div className="space-x-2">
{
tags.map((v, k) => (
<div className="bg-zinc-600 inline max-w-fit p-[5px] text-xs font-bold rounded-xl bg-opacity-20" key={k}>{v}</div>
))
}
</div>
</div>
</div>
</a>
)
}
+17 -21
View File
@@ -1,31 +1,27 @@
export default function ProjectComponent({
url,
header,
projectHeader,
projectSubheader,
projectImage
urlTo,
description,
name,
tags
}: {
url: string,
header: string,
projectHeader: string,
projectSubheader: string,
projectImage?: string,
urlTo: string,
description: string,
name: string,
tags: string[]
}) {
return (
<a href={url} target="_blank">
<div className="bg-white rounded-lg text-black p-2">
<h1 className="text-center font-semibold">{header}</h1>
<a href={urlTo} target="_blank">
<div className="bg-white text-black mx-auto rounded-lg overflow-hidden">
<div className="p-4 space-y-2">
<h1 className="font-bold text-xl">{name}</h1>
<p>{description}</p>
<div className="block">
<div className="flex items-center">
<div className="space-x-2">
{
projectImage && <img alt="Project Image" src={projectImage} width={100} height={100} className="w-[80px] h-[50px] object-cover" />
tags.map((v, k) => (
<div className="bg-zinc-600 inline max-w-fit p-[5px] text-xs font-bold rounded-xl bg-opacity-20" key={k}>{v}</div>
))
}
<div className="flex flex-col mx-2 min-w-0 flex-grow">
<h1 className="font-bold text-left truncate">{projectHeader}</h1>
<h1 className="text-left truncate">{projectSubheader}</h1>
</div>
</div>
</div>
</div>
@@ -1,61 +0,0 @@
import { useState, useEffect } from 'react';
import ProjectComponent from '../ProjectComponent';
import type { ContentTrackerItem } from '../../utils/types';
export default function RecentBook() {
const [ data, setData ] = useState<ContentTrackerItem | null>(null);
const [ loading, setLoading ] = useState<boolean>(true);
const [ errorState, setErrorState ] = useState<string>("");
useEffect(() => {
fetch("https://api.alexav.gg/v4/tracker/latest/book", {
method: 'get'
})
.then(async (res) => {
if (res.ok) {
const json = await res.json();
setData(json.data);
setLoading(false);
} else {
setErrorState("Failed to fetch data!");
setLoading(false);
}
})
.catch((e) => {
setLoading(false);
setErrorState(e.message)
})
}, []);
const getDateString = (startedOn: Date, finished: boolean) => {
if (startedOn && !finished) {
const startedOnDate = new Date(startedOn);
return "Started on " + startedOnDate.toLocaleDateString();
} else if (finished) {
return "Finished!"
} else {
return "Just Added"
}
}
if (data && !loading) {
return (
<ProjectComponent
header={"What I'm reading"}
projectHeader={data.title}
projectSubheader={getDateString(data.startedOn, data.finished)}
url={"https://tracker.alexav.gg/?type=books&id=" + data.id}
projectImage={data.image}
/>
)
} else {
return (
<ProjectComponent
header={"What I'm reading"}
projectHeader={"No data :("}
projectSubheader={loading ? "Loading data.." : "Something went wrong fetching data! " + errorState}
url={"/"}
/>
)
}
}
@@ -1,70 +0,0 @@
import { useState, useEffect } from 'react';
import ProjectComponent from '../ProjectComponent';
type MedalAPIResponse = {
data: MedalClip[]
}
type MedalClip = {
contentId: string,
contentTitle: string,
contentViews: number,
contentLikes: number,
contentThumbnail: string,
directClipUrl: string
}
export default function RecentClip() {
const [ data, setData ] = useState<MedalClip[]>();
const [ errorState, setErrorState ] = useState<string>("");
const [ loading, setLoading ] = useState<boolean>(true);
const translateToWebsiteUrl = (url: string) => {
return url.replace('/clip/', "/clips/")
}
// i'm a neat man what can i say
const likeOrLikes = (likes: number) => {
return (likes > 1 && likes !== 0) ? "likes" : "like"
}
const viewOrViews = (views: number) => {
return (views > 1 && views !== 0) ? "views" : "view"
}
useEffect(() => {
fetch("https://api.alexav.gg/v4/social/medal", {
method: "get"
})
.then(async (res) => {
if (res.ok) {
const data = await res.json() as MedalAPIResponse
setData(data.data);
} else {
setErrorState("Failed to load! :(")
}
})
.catch((e: Error) => {
setErrorState(e.message)
})
setLoading(false);
}, []);
if (!loading && data) {
return (
<ProjectComponent
url={translateToWebsiteUrl(data[0].directClipUrl)}
header={"Clips I've taken"}
projectHeader={data[0].contentTitle}
projectSubheader={`${data[0].contentLikes} ${likeOrLikes(data[0].contentLikes)} | ${data[0].contentViews} ${viewOrViews(data[0].contentViews)}`}
projectImage={data[0].contentThumbnail}
/>
)
} else {
return <ProjectComponent
header={"Clips I've taken"}
projectHeader={"No data :("}
projectSubheader={loading ? "Loading data.." : "Something went wrong fetching data! " + errorState}
url={"/"}
/>
}
}
@@ -1,63 +0,0 @@
import { useState, useEffect } from 'react';
import ProjectComponent from '../ProjectComponent';
type GameAPIResponse = {
data: GameData
}
type GameData = {
name: string,
location: string,
startedOn: number,
finished: boolean
}
export default function RecentGame() {
const [ data, setData ] = useState<GameData>();
const [ errorState, setErrorState ] = useState<string>("");
const [ loading, setLoading ] = useState<boolean>(true);
const getTime = (time: number) => {
const date = new Date(time*1000);
return `${date.toLocaleDateString()} at ${date.toLocaleTimeString()}`
}
useEffect(() => {
fetch("https://api.alexav.gg/v4/game", {
method: "get"
})
.then(async (res) => {
if (res.ok) {
const data = await res.json() as GameAPIResponse
setData(data.data);
} else {
setErrorState("Failed to load! :(")
}
})
.catch((e: Error) => {
setErrorState(e.message)
})
setLoading(false);
}, []);
if (!loading && data) {
return (
<ProjectComponent
url={"https://medal.tv/u/alexav"}
header={"Games I'm playing"}
projectHeader={data.name}
projectSubheader={
data.finished ? `Last played on ${getTime(data.startedOn)}`
: `Started on ${getTime(data.startedOn)}`
}
projectImage={"https://storage.alexav.gg/content/933d5523-5e20-4040-9fe9-b73552eac8ab.png"}
/>
)
} else {
return <ProjectComponent
header={"Games I'm playing"}
projectHeader={"No data :("}
projectSubheader={loading ? "Loading data.." : "Something went wrong fetching data! " + errorState}
url={"/"}
/>
}
}
@@ -1,47 +0,0 @@
import { useState, useEffect } from 'react';
import ProjectComponent from '../ProjectComponent';
import type { MusicData } from '../../utils/types';
export default function RecentListens() {
const [ data, setData ] = useState<MusicData[]>();
const [ errorState, setErrorState ] = useState<string>("");
const [ loading, setLoading ] = useState<boolean>(true);
useEffect(() => {
fetch("https://api.alexav.gg/v4/social/music", {
method: "get"
})
.then(async (res) => {
if (res.ok) {
const data = await res.json()
setData(data.data.tracks);
} else {
setErrorState("Failed to load! :(")
}
})
.catch((e: Error) => {
setErrorState(e.message)
})
setLoading(false);
}, []);
if (!loading && data) {
return (
<ProjectComponent
url={"https://last.fm/user/lulawex"}
header={"Music I'm listening to"}
projectHeader={data[0].name}
projectSubheader={data[0].artist["#text"]}
projectImage={data[0].image.filter((v) => v.size === "medium")[0]["#text"]}
/>
)
} else {
return <ProjectComponent
header={"Music I'm listening to"}
projectHeader={"No data :("}
projectSubheader={loading ? "Loading data.." : "Something went wrong fetching data! " + errorState}
url={"/"}
/>
}
}
@@ -1,60 +0,0 @@
import { useState, useEffect } from 'react';
import ProjectComponent from '../ProjectComponent';
type PhotoData = {
id: number,
name: string,
imageUrl: string,
dateTaken: Date
}
export default function RecentPhoto() {
const [ data, setData ] = useState<PhotoData | null>(null);
const [ loading, setLoading ] = useState<boolean>(true);
const [ errorState, setErrorState ] = useState<string | null>(null);
useEffect(() => {
fetch("https://api.alexav.gg/v4/social/photos", {
method: 'get',
headers: {
'content-type': 'application/json'
}
})
.then(async (res) => {
if (res.ok) {
const json = await res.json()
setData(json.data[0]);
setLoading(false)
} else {
setErrorState("Failed to fetch data!");
setLoading(false);
}
})
.catch((e) => {
setLoading(false);
setErrorState(e.message)
})
}, []);
if (data && !loading) {
return (
<ProjectComponent
url={"https://photos.alexav.gg?id=" + data.id}
header={"Photos I've captured"}
projectHeader={data.name}
projectSubheader={`Taken on ${new Date(data.dateTaken).toLocaleDateString()}`}
projectImage={data.imageUrl}
/>
)
} else {
return (
<ProjectComponent
header={"Photos I've captured"}
projectHeader={"No data :("}
projectSubheader={loading ? "Loading data.." : "Something went wrong fetching data! " + errorState}
url={"/"}
/>
)
}
}
@@ -1,74 +0,0 @@
import { useState, useEffect } from 'react';
import ProjectComponent from '../ProjectComponent';
type WatchData = {
id: number,
title: string,
description: string,
author?: string,
image: string,
episode?: string,
tmdbId?: string,
finished: boolean,
startedOn?: Date,
createdAt: Date,
updatedAt: Date,
createdById: string
}
export default function RecentWatch() {
const [ data, setData ] = useState<WatchData | null>(null);
const [ loading, setLoading ] = useState<boolean>(true);
const [ errorState, setErrorState ] = useState<string>("");
useEffect(() => {
fetch("https://api.alexav.gg/v4/tracker/latest/tv", {
method: 'get'
})
.then(async (res) => {
if (res.ok) {
const json = await res.json();
setData(json.data);
setLoading(false)
} else {
setErrorState("Failed to fetch data!");
setLoading(false);
}
})
.catch((e) => {
setLoading(false);
setErrorState(e.message)
})
}, []);
const getSubheaderText = () => {
if (data?.finished) {
return "Finished!"
} else if (data?.startedOn) {
return "Last update on " + new Date(data.updatedAt).toLocaleDateString()
} else {
return "Just Added"
}
}
if (data && !loading) {
return (
<ProjectComponent
header={"What I'm watching"}
projectHeader={data.title}
projectSubheader={ getSubheaderText() }
url={"https://tracker.alexav.gg/?type=tv&id=" + data.id}
projectImage={data.image}
/>
)
} else {
return (
<ProjectComponent
header={"What I'm watching"}
projectHeader={"No data :("}
projectSubheader={loading ? "Loading data.." : "Something went wrong fetching data! " + errorState}
url={"/"}
/>
)
}
}
-49
View File
@@ -1,49 +0,0 @@
import { useEffect, useState } from 'react';
import '../assets/snow.css';
type Snowflake = {
id: number;
left: number;
animationDuration: number;
opacity: number;
};
export default function Snow() {
const [snowflakes, setSnowflakes] = useState<Snowflake[]>([]);
useEffect(() => {
const createSnowflake = (): Snowflake => ({
id: Math.random(),
left: Math.random() * 100, // percentage across screen
animationDuration: 3 + Math.random() * 7, // between 3-10 seconds
opacity: 0.4 + Math.random() * 0.6, // between 0.4-1
});
// Create initial snowflakes
const initialSnowflakes = Array.from({ length: 50 }, createSnowflake);
setSnowflakes(initialSnowflakes);
// Add new snowflakes periodically
const interval = setInterval(() => {
setSnowflakes(prev => [...prev.slice(-49), createSnowflake()]);
}, 200);
return () => clearInterval(interval);
}, []);
return (
<div className="snow-container">
{snowflakes.map(flake => (
<div
key={flake.id}
className="snowflake"
style={{
left: `${flake.left}%`,
animationDuration: `${flake.animationDuration}s`,
opacity: flake.opacity,
}}
/>
))}
</div>
);
}