add lyric, game status, split components
This commit is contained in:
@@ -0,0 +1,62 @@
|
||||
import { useState, useEffect } from "react";
|
||||
|
||||
type GameObject = {
|
||||
name: string;
|
||||
location: string;
|
||||
startedOn: number;
|
||||
finished: boolean;
|
||||
};
|
||||
|
||||
export default function GameComponent() {
|
||||
const [game, setGame] = useState<GameObject | null>(null);
|
||||
const [failedFetch, setFailedFetch] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
async function fetchGame() {
|
||||
const response = await fetch(
|
||||
"https://api.alexav.gg/v4/tracker/latestGame"
|
||||
);
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
if (data.data && data.data.length <= 0) {
|
||||
setFailedFetch(true);
|
||||
} else setGame(data.data as GameObject);
|
||||
} else setFailedFetch(true);
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
fetchGame();
|
||||
}, 60000);
|
||||
fetchGame();
|
||||
}, []);
|
||||
|
||||
if (game) {
|
||||
return (
|
||||
<div className="flex align-middle">
|
||||
<a
|
||||
className="hover:underline"
|
||||
href={`https://medal.tv/u/alexav`}
|
||||
target="_blank"
|
||||
>
|
||||
<h1 className="lowercase">
|
||||
{!game.finished ? "playing" : "last played"} {game.name}
|
||||
</h1>
|
||||
</a>
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
if (failedFetch) {
|
||||
return (
|
||||
<div>
|
||||
<p>failed to get game :(</p>
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<div>
|
||||
<p>loading game..</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,175 @@
|
||||
import { useState, useEffect, type ReactNode } from "react";
|
||||
import { Flashlight, FlashlightOff } from "lucide-react";
|
||||
import CordycepsImage from "../assets/cordyceps.png";
|
||||
|
||||
export default function Layout({ children }: { children: ReactNode }) {
|
||||
const [pointerPosition, setPointerPosition] = useState({ x: 500, y: 500 });
|
||||
const [isActive, setIsActive] = useState(false);
|
||||
const [isTouchDevice, setIsTouchDevice] = useState(false);
|
||||
const [isFlashlightEnabled, setIsFlashlightEnabled] = useState(false);
|
||||
|
||||
// Detect touch device on component mount
|
||||
useEffect(() => {
|
||||
const isTouchCapable =
|
||||
"ontouchstart" in window || navigator.maxTouchPoints > 0;
|
||||
|
||||
setIsTouchDevice(isTouchCapable);
|
||||
}, []);
|
||||
|
||||
// Handle mouse events for desktop
|
||||
const handleMouseMove = (e: React.MouseEvent) => {
|
||||
setPointerPosition({ x: e.clientX, y: e.clientY });
|
||||
setIsActive(true);
|
||||
};
|
||||
|
||||
const handleMouseLeave = () => {
|
||||
setIsActive(false);
|
||||
};
|
||||
|
||||
// Handle touch events for mobile
|
||||
const handleTouchMove = (e: React.TouchEvent) => {
|
||||
if (e.touches && e.touches[0]) {
|
||||
e.preventDefault(); // Prevent scrolling while touching
|
||||
const touch = e.touches[0];
|
||||
setPointerPosition({ x: touch.clientX, y: touch.clientY });
|
||||
setIsActive(true);
|
||||
}
|
||||
};
|
||||
|
||||
const handleTouchStart = (e: React.TouchEvent) => {
|
||||
if (e.touches && e.touches[0]) {
|
||||
const touch = e.touches[0];
|
||||
setPointerPosition({ x: touch.clientX, y: touch.clientY });
|
||||
setIsActive(true);
|
||||
}
|
||||
};
|
||||
|
||||
const handleTouchEnd = () => {
|
||||
// On mobile, we'll keep the last position visible for a moment
|
||||
setTimeout(() => {
|
||||
setIsActive(false);
|
||||
}, 1000);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
// Define window event handlers with correct types
|
||||
const windowMouseMove = (e: MouseEvent) => {
|
||||
setPointerPosition({ x: e.clientX, y: e.clientY });
|
||||
setIsActive(true);
|
||||
};
|
||||
|
||||
const windowMouseLeave = () => {
|
||||
setIsActive(false);
|
||||
};
|
||||
|
||||
const windowTouchStart = (e: TouchEvent) => {
|
||||
if (e.touches && e.touches[0]) {
|
||||
const touch = e.touches[0];
|
||||
setPointerPosition({ x: touch.clientX, y: touch.clientY });
|
||||
setIsActive(true);
|
||||
}
|
||||
};
|
||||
|
||||
const windowTouchMove = (e: TouchEvent) => {
|
||||
if (e.touches && e.touches[0]) {
|
||||
e.preventDefault?.(); // Optional chaining in case preventDefault isn't available
|
||||
const touch = e.touches[0];
|
||||
setPointerPosition({ x: touch.clientX, y: touch.clientY });
|
||||
setIsActive(true);
|
||||
}
|
||||
};
|
||||
|
||||
const windowTouchEnd = () => {
|
||||
// On mobile, we'll keep the last position visible for a moment
|
||||
setTimeout(() => {
|
||||
setIsActive(false);
|
||||
}, 1000);
|
||||
};
|
||||
|
||||
if (isFlashlightEnabled) {
|
||||
// Add desktop event listeners
|
||||
window.addEventListener("mousemove", windowMouseMove);
|
||||
window.addEventListener("mouseleave", windowMouseLeave);
|
||||
|
||||
// Add mobile event listeners
|
||||
window.addEventListener("touchstart", windowTouchStart, {
|
||||
passive: false,
|
||||
});
|
||||
window.addEventListener("touchmove", windowTouchMove, { passive: false });
|
||||
window.addEventListener("touchend", windowTouchEnd);
|
||||
} else {
|
||||
// Remove desktop event listeners
|
||||
window.removeEventListener("mousemove", windowMouseMove);
|
||||
window.removeEventListener("mouseleave", windowMouseLeave);
|
||||
|
||||
// Remove mobile event listeners
|
||||
window.removeEventListener("touchstart", windowTouchStart);
|
||||
window.removeEventListener("touchmove", windowTouchMove);
|
||||
window.removeEventListener("touchend", windowTouchEnd);
|
||||
}
|
||||
|
||||
// Cleanup when component unmounts
|
||||
return () => {
|
||||
window.removeEventListener("mousemove", windowMouseMove);
|
||||
window.removeEventListener("mouseleave", windowMouseLeave);
|
||||
window.removeEventListener("touchstart", windowTouchStart);
|
||||
window.removeEventListener("touchmove", windowTouchMove);
|
||||
window.removeEventListener("touchend", windowTouchEnd);
|
||||
};
|
||||
}, [isFlashlightEnabled]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className="relative h-screen w-full overflow-hidden bg-black text-white"
|
||||
onMouseMove={handleMouseMove}
|
||||
onMouseLeave={handleMouseLeave}
|
||||
onTouchStart={handleTouchStart}
|
||||
onTouchMove={handleTouchMove}
|
||||
onTouchEnd={handleTouchEnd}
|
||||
>
|
||||
<div className="min-h-screen flex flex-col justify-center items-center mx-auto select-none">
|
||||
<div className="border border-white border-solid p-2 rounded-lg w-full sm:w-[500px] mb-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div
|
||||
className="ml-2"
|
||||
onClick={() => setIsFlashlightEnabled(!isFlashlightEnabled)}
|
||||
>
|
||||
{isFlashlightEnabled ? <Flashlight /> : <FlashlightOff />}
|
||||
</div>
|
||||
<h1 className="text-2xl font-bold">welcome !</h1>
|
||||
</div>
|
||||
</div>
|
||||
{children}
|
||||
</div>
|
||||
|
||||
{isFlashlightEnabled && (
|
||||
<>
|
||||
<div
|
||||
className={`absolute inset-0 bg-black pointer-events-none transition-opacity duration-300 ${
|
||||
isActive ? "opacity-95" : "opacity-0"
|
||||
}`}
|
||||
style={{
|
||||
maskImage: `radial-gradient(circle ${isTouchDevice ? "12rem" : "8rem"} at ${pointerPosition.x}px ${pointerPosition.y}px, transparent 0%, black 100%)`,
|
||||
WebkitMaskImage: `radial-gradient(circle ${isTouchDevice ? "12rem" : "8rem"} at ${pointerPosition.x}px ${pointerPosition.y}px, transparent 0%, black 100%)`,
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
className="absolute inset-0 pointer-events-none z-20"
|
||||
style={{
|
||||
maskImage: `radial-gradient(circle ${isTouchDevice ? "12rem" : "8rem"} at ${pointerPosition.x}px ${pointerPosition.y}px, black 0%, transparent 100%)`,
|
||||
WebkitMaskImage: `radial-gradient(circle ${isTouchDevice ? "12rem" : "8rem"} at ${pointerPosition.x}px ${pointerPosition.y}px, black 0%, transparent 100%)`,
|
||||
}}
|
||||
>
|
||||
<img
|
||||
src={CordycepsImage}
|
||||
className="absolute bottom-0 select-none"
|
||||
draggable={false}
|
||||
alt="Cordyceps"
|
||||
/>
|
||||
</div>
|
||||
<audio loop autoPlay={true} src={"/clicker-sounds.mp3"} />
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
import { useEffect, useRef, useState, FC } from "react";
|
||||
|
||||
interface AutoScrollingTextProps {
|
||||
speed?: number;
|
||||
}
|
||||
const lyrics: LyricItem[] = [
|
||||
{
|
||||
string: "and we're laying on the roof of my car",
|
||||
url: "https://open.spotify.com/track/4Pt2R2EJ3rZBRn2JpH7WnJ?si=7a167c7b22354d24",
|
||||
},
|
||||
{
|
||||
string: "your funeral was beautiful",
|
||||
url: "https://open.spotify.com/track/4ZJ4vzLQekI0WntDbanNC7?si=914245f4e48a4c00",
|
||||
},
|
||||
{
|
||||
string: "she was always the one",
|
||||
url: "https://open.spotify.com/track/21n769QrJtrmN0pXuBkYOm?si=b4a5e7f627b54fd9",
|
||||
},
|
||||
{
|
||||
string: "how lucky are we?",
|
||||
url: "https://open.spotify.com/track/5iJKGpnFfvbjZJeAtwXfCj?si=a0b160c9cead4863",
|
||||
},
|
||||
{
|
||||
string: "i'm yours to take darlin'",
|
||||
url: "https://open.spotify.com/track/2nATTe1i7aMzIZ8klEpgQ7?si=1bcd8e6aa9324b81",
|
||||
},
|
||||
{
|
||||
string: "wet, hot, american nights",
|
||||
url: "https://open.spotify.com/track/3c9EsIo34kil8Oj1reaozB?si=8c0aaf0fdb7a4e5f",
|
||||
},
|
||||
{
|
||||
string: "it ain't the whiskey that gets me",
|
||||
url: "https://open.spotify.com/track/0AsrOS8HktHe9TaB1i9W3v?si=c186a29f4e804d05",
|
||||
},
|
||||
];
|
||||
|
||||
type LyricItem = {
|
||||
string: string;
|
||||
url: string;
|
||||
};
|
||||
|
||||
const RandomLyric: FC<AutoScrollingTextProps> = ({ speed = 0.5 }) => {
|
||||
const scrollContainerRef = useRef<HTMLDivElement>(null);
|
||||
const contentRef = useRef<HTMLDivElement>(null);
|
||||
const [lyric, setLyric] = useState<LyricItem | null>();
|
||||
|
||||
const getRandomLyric = () => {
|
||||
const randomNumber = Math.floor(Math.random() * lyrics.length);
|
||||
console.log(randomNumber);
|
||||
setLyric(lyrics[randomNumber]);
|
||||
};
|
||||
useEffect(() => {
|
||||
getRandomLyric();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const scrollContainer = scrollContainerRef.current;
|
||||
const contentElement = contentRef.current;
|
||||
|
||||
if (!scrollContainer || !contentElement) return;
|
||||
|
||||
// Create a clone of the content element
|
||||
const clone = contentElement.cloneNode(true) as HTMLDivElement;
|
||||
scrollContainer.appendChild(clone);
|
||||
|
||||
let animationId: number;
|
||||
let scrollPosition = 0;
|
||||
|
||||
const animate = () => {
|
||||
if (!scrollContainer || !contentElement) return;
|
||||
|
||||
// Increment scroll position based on speed
|
||||
scrollPosition += speed;
|
||||
|
||||
// Reset position when first element is scrolled out of view
|
||||
if (scrollPosition >= contentElement.offsetWidth) {
|
||||
scrollPosition = 0;
|
||||
}
|
||||
|
||||
// Apply scroll position
|
||||
scrollContainer.scrollLeft = scrollPosition;
|
||||
|
||||
animationId = requestAnimationFrame(animate);
|
||||
};
|
||||
|
||||
animationId = requestAnimationFrame(animate);
|
||||
|
||||
return () => {
|
||||
cancelAnimationFrame(animationId);
|
||||
};
|
||||
}, [speed, lyric]);
|
||||
|
||||
if (lyric) {
|
||||
return (
|
||||
<a href={lyric.url} target="_blank">
|
||||
<div
|
||||
ref={scrollContainerRef}
|
||||
className={`w-37 overflow-hidden rounded-lg`}
|
||||
style={{ whiteSpace: "nowrap" }}
|
||||
>
|
||||
<div ref={contentRef} className="inline-block">
|
||||
|
||||
<span className={`text-xl font-medium text-white hover:underline`}>
|
||||
{lyric?.string}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export default RandomLyric;
|
||||
Reference in New Issue
Block a user