add lyric, game status, split components

This commit is contained in:
2025-05-17 02:51:49 -04:00
parent 3e05722d91
commit f50ed9de54
5 changed files with 418 additions and 231 deletions
+67 -228
View File
@@ -1,243 +1,82 @@
import React, { useState, useEffect } from "react"; import { Twitter, Github } from "lucide-react";
import { Flashlight, FlashlightOff, Twitter, Github } from "lucide-react";
import CordycepsImage from "./assets/cordyceps.png";
import MusicComponent from "./components/MusicComponent"; import MusicComponent from "./components/MusicComponent";
import TVComponent from "./components/TVComponent"; import TVComponent from "./components/TVComponent";
import Layout from "./components/Layout";
import GameComponent from "./components/GameComponent";
import RandomLyric from "./components/RandomLyric";
export default function MouseFlashlight() { export default function App() {
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 ( return (
<div <Layout>
className="relative h-screen w-full overflow-hidden bg-black text-white" <div className="border border-white border-solid rounded-lg w-full sm:w-[500px] p-2">
onMouseMove={handleMouseMove} <div className="grid grid-cols-1 xs:grid-cols-2">
onMouseLeave={handleMouseLeave} <div className="mx-auto">
onTouchStart={handleTouchStart} <div>
onTouchMove={handleTouchMove} <h1 className="text-2xl font-bold italic opacity-95 inline">
onTouchEnd={handleTouchEnd} alex, 18
> </h1>
<div className="min-h-screen flex flex-col justify-center items-center mx-auto"> <RandomLyric speed={0.2} />
<div className="border border-white border-solid p-2 rounded-lg w-full sm:w-[500px] mb-4"> <img
<div className="flex items-center justify-between"> src="https://storage.alexav.gg/content/picture.gif"
<div width="150"
className="ml-2" className="rounded-md"
onClick={() => setIsFlashlightEnabled(!isFlashlightEnabled)} alt="Profile"
> />
{isFlashlightEnabled ? <Flashlight /> : <FlashlightOff />}
<div className="pt-2">
<MusicComponent />
<a
href="https://last.fm/user/lulawex"
target="_blank"
className="hover:underline w-fit opacity-50"
>
powered by last.fm
</a>
</div>
</div> </div>
<h1 className="text-2xl font-bold">welcome !</h1>
</div> </div>
</div> <div className="mx-auto">
<h1 className="font-bold text-2xl italic">what i do</h1>
<p>
qa @{" "}
<a
href="https://medal.tv"
target="_blank"
rel="noopener noreferrer"
className="hover:underline"
>
medal.tv
</a>
, game, code
</p>
<div className="border border-white border-solid rounded-lg w-full sm:w-[500px] p-2"> <h1 className="font-bold text-2xl italic">obsessed with</h1>
<div className="grid grid-cols-1 xs:grid-cols-2"> <p>tlou, twd, rdr, dexter</p>
<div className="mx-auto">
<div>
<h1 className="text-2xl font-bold italic opacity-95 inline">
alex, 18
</h1>
<img
src="https://storage.alexav.gg/content/picturev2.jpg"
width="150"
className="rounded-md"
alt="Profile"
/>
<div className="pt-2"> <h1 className="font-bold text-2xl italic">status</h1>
<MusicComponent /> <TVComponent />
<a <GameComponent />
href="https://last.fm/user/lulawex"
target="_blank"
className="hover:underline w-fit opacity-50"
>
powered by last.fm
</a>
</div>
</div>
</div>
<div className="mx-auto">
<h1 className="font-bold text-2xl italic">what i do</h1>
<p>
qa @{" "}
<a
href="https://medal.tv"
target="_blank"
rel="noopener noreferrer"
className="hover:underline"
>
medal.tv
</a>
, game, code
</p>
<h1 className="font-bold text-2xl italic">obsessed with</h1> <h1 className="font-bold text-2xl italic">socials</h1>
<p>tlou, twd, rdr</p> <div className="space-x-2">
<a
<h1 className="font-bold text-2xl italic">watching</h1> href="https://twitter.com/carlgrimesdupe"
<TVComponent /> target="_blank"
rel="noopener noreferrer"
<h1 className="font-bold text-2xl italic">socials</h1> >
<div className="space-x-2"> <Twitter className="inline" />
<a </a>
href="https://twitter.com/carlgrimesdupe" <a
target="_blank" href="https://github.com/azpha"
rel="noopener noreferrer" target="_blank"
> rel="noopener noreferrer"
<Twitter className="inline" /> >
</a> <Github className="inline" />
<a </a>
href="https://github.com/azpha"
target="_blank"
rel="noopener noreferrer"
>
<Github className="inline" />
</a>
</div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</Layout>
{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"
alt="Cordyceps"
/>
</div>
<audio loop autoPlay={true} src={"/clicker-sounds.mp3"} />
</>
)}
</div>
); );
} }
+62
View File
@@ -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>
);
}
}
}
+175
View File
@@ -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>
);
}
+113
View File
@@ -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">
&nbsp;&nbsp;
<span className={`text-xl font-medium text-white hover:underline`}>
{lyric?.string}
</span>
</div>
</div>
</a>
);
}
};
export default RandomLyric;
+1 -3
View File
@@ -2,6 +2,4 @@ import { createRoot } from "react-dom/client";
import "./assets/main.css"; import "./assets/main.css";
import App from "./App.tsx"; import App from "./App.tsx";
createRoot(document.getElementById("root")!).render( createRoot(document.getElementById("root")!).render(<App />);
<App />,
);