nextjs
This commit is contained in:
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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={"/"}
|
||||
/>
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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">© {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
@@ -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>
|
||||
// )
|
||||
// }
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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={"/"}
|
||||
/>
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user