520 lines
18 KiB
TypeScript
520 lines
18 KiB
TypeScript
"use client";
|
|
import { useState, useEffect } from "react";
|
|
import { Markazi_Text, Nunito } from "next/font/google";
|
|
import { useRouter } from "next/navigation";
|
|
import Image from "next/image";
|
|
import Link from "next/link";
|
|
import { useAuth } from "react-oidc-context";
|
|
import { useSession } from "muzak/contexts/SessionContext";
|
|
import { getUser, api, Track } from "muzak/data/fetcher";
|
|
import getSessionId from "muzak/data/session";
|
|
import { useSpotifyIframe } from "muzak/contexts/SpotifyIframe";
|
|
|
|
const nunito = Nunito({
|
|
weight: "900",
|
|
subsets: ["latin"],
|
|
});
|
|
|
|
const MAX_PARTY_SIZE = 8;
|
|
|
|
export default function Lobby() {
|
|
const [userList, setUserList] = useState<any[]>([]);
|
|
const [noList, setNoList] = useState<any[]>([]);
|
|
const [yesList, setYesList] = useState<any[]>([]);
|
|
const [sessionSocket, setSessionSocket] = useState<WebSocket | null>(null);
|
|
const { sessionId, setSessionId } = useSession();
|
|
const [sessionState, setSessionState] = useState("lobby");
|
|
const [currentSong, setCurrentSong] = useState<Track>();
|
|
const [songLoading, setSongLoading] = useState(false);
|
|
const [spotifyId, setSpotifyId] = useState<string | null>(null);
|
|
const auth = useAuth();
|
|
const user = getUser();
|
|
const router = useRouter();
|
|
const controller = useSpotifyIframe();
|
|
const [revealed, setRevealed] = useState(false);
|
|
const [playingPercentage, setPlayingPercentage] = useState(0);
|
|
const [secondsLeft, setSecondsLeft] = useState(30);
|
|
const [state, setState] = useState("lobby"); // lobby, playing, waiting, loading
|
|
|
|
// Main message handler and dispatcher
|
|
function onMessage(e) {
|
|
console.log(e);
|
|
let data = JSON.parse(e.data);
|
|
for (let key in data) {
|
|
if (key === "joined") {
|
|
addUser(data[key]);
|
|
} else if (key == "track") {
|
|
getTrack();
|
|
} else if (key == "voted") {
|
|
voteUser(data[key]);
|
|
} else if (key == "done") {
|
|
router.push("/post-game");
|
|
}
|
|
}
|
|
console.log(data);
|
|
}
|
|
|
|
// Setup session
|
|
useEffect(() => {
|
|
//console.log("session: ", sessionId);
|
|
let socket: WebSocket | null;
|
|
if (sessionId) {
|
|
socket = new WebSocket(
|
|
`${process.env.NEXT_PUBLIC_WS_HOST}/voting/session/${sessionId}/`,
|
|
);
|
|
socket.onmessage = onMessage;
|
|
setSessionSocket(socket);
|
|
// console.log("session made: ", sessionId);
|
|
}
|
|
return () => {
|
|
if (socket) {
|
|
socket.close();
|
|
}
|
|
};
|
|
}, [sessionId]);
|
|
async function setSession() {
|
|
const id = await getSessionId();
|
|
setSessionId(id);
|
|
console.log("Session ID:", id);
|
|
}
|
|
useEffect(() => {
|
|
if (!auth.isLoading && !auth.error && auth.isAuthenticated) {
|
|
setSession();
|
|
}
|
|
}, [auth]);
|
|
|
|
function startSession() {
|
|
setSessionState("voting");
|
|
setState("loading");
|
|
getTrack();
|
|
sessionSocket.send(
|
|
JSON.stringify({
|
|
action: "start",
|
|
token: user.access_token,
|
|
}),
|
|
);
|
|
}
|
|
|
|
function addUser(user: object) {
|
|
const newUser = {
|
|
id: user["id"],
|
|
name: user["username"],
|
|
avatar: `https://api.dicebear.com/9.x/bottts-neutral/svg?seed=${user["id"]}`,
|
|
hidden: false,
|
|
};
|
|
|
|
setUserList((prevUserList) => {
|
|
if (prevUserList.find((u) => u.id === user["id"])) {
|
|
console.log("User already exists, not adding");
|
|
return prevUserList;
|
|
}
|
|
return [...prevUserList, newUser];
|
|
});
|
|
}
|
|
|
|
// Visually processes the incoming vote for a user.
|
|
function voteUser(vote: object) {
|
|
const userId = vote["id"];
|
|
const userName = vote["username"];
|
|
const points = vote["points"];
|
|
if (points === 0) {
|
|
setYesList((prevYesList) => {
|
|
//if (prevYesList.find((u) => u.id === userId)) return prevYesList;
|
|
return prevYesList.filter((u) => u.id !== userId);
|
|
});
|
|
setNoList((prevNoList) => {
|
|
if (prevNoList.find((u) => u.id === userId)) {
|
|
return prevNoList;
|
|
}
|
|
return [
|
|
...prevNoList,
|
|
{
|
|
id: userId,
|
|
name: userName,
|
|
avatar: `https://api.dicebear.com/9.x/bottts-neutral/svg?seed=${userId}`,
|
|
},
|
|
];
|
|
});
|
|
} else {
|
|
setNoList((prevNoList) => {
|
|
return prevNoList.filter((u) => u.id !== userId);
|
|
});
|
|
setYesList((prevYesList) => {
|
|
if (prevYesList.find((u) => u.id === userId)) {
|
|
return prevYesList;
|
|
}
|
|
return [
|
|
...prevYesList,
|
|
{
|
|
id: userId,
|
|
name: userName,
|
|
avatar: `https://api.dicebear.com/9.x/bottts-neutral/svg?seed=${userId}`,
|
|
},
|
|
];
|
|
});
|
|
}
|
|
setUserList((prevUserList) => {
|
|
// Set hidden true on the user that voted:
|
|
return prevUserList.map((user) => {
|
|
if (user.id === userId) {
|
|
return { ...user, hidden: true };
|
|
}
|
|
return user;
|
|
});
|
|
});
|
|
}
|
|
|
|
async function getTrack() {
|
|
setSongLoading(true);
|
|
setState("loading");
|
|
setUserList((prevUserList) => {
|
|
return prevUserList.map((user) => {
|
|
return { ...user, hidden: false };
|
|
});
|
|
});
|
|
setYesList([]);
|
|
setNoList([]);
|
|
const { data: session } = await api.sessions.get({ session_id: sessionId });
|
|
const trackId = session?.current_track;
|
|
if (trackId) {
|
|
const { data: track } = await api.tracks.detail({ id: trackId });
|
|
console.log(session);
|
|
console.log(track);
|
|
setCurrentSong(track);
|
|
} else {
|
|
nextTrack();
|
|
}
|
|
setSpotifyId(session?.current_track);
|
|
setSongLoading(false);
|
|
}
|
|
|
|
async function nextTrack() {
|
|
console.log("nextTrack");
|
|
setState("loading");
|
|
setRevealed(false);
|
|
setSongLoading(true);
|
|
sessionSocket.send(
|
|
JSON.stringify({
|
|
action: "next",
|
|
token: user.access_token,
|
|
}),
|
|
);
|
|
}
|
|
|
|
async function endPlay() {
|
|
controller?.seek(100);
|
|
}
|
|
|
|
// Spotify Player stuff
|
|
useEffect(() => {
|
|
if (controller) {
|
|
console.log("Loaded track:", spotifyId);
|
|
controller.loadUri(`spotify:track:${spotifyId}`);
|
|
controller.play();
|
|
// Note: do not set playing state here, but later when its actually playing.
|
|
console.log(controller._listeners);
|
|
if (controller._listeners["playback_update"].length === 0) {
|
|
controller.addListener("playback_update", (e) => {
|
|
updatePlayback(e);
|
|
});
|
|
}
|
|
} else {
|
|
console.log("Spotify player not initialized");
|
|
}
|
|
}, [controller, spotifyId]);
|
|
function updatePlayback(e) {
|
|
let duration = e.data.duration;
|
|
let playingPosition = e.data.position;
|
|
const playingSeconds = Math.floor((duration - playingPosition) / 1000);
|
|
setPlayingPercentage((playingPosition / duration) * 100);
|
|
setSecondsLeft((s) => {
|
|
setState((state) => {
|
|
if (playingSeconds >= 1) {
|
|
return "playing";
|
|
}
|
|
if (state == "playing" && s <= 0) {
|
|
console.log("track ended");
|
|
return "waiting";
|
|
}
|
|
return state;
|
|
});
|
|
return playingSeconds;
|
|
});
|
|
}
|
|
async function playTrack() {
|
|
controller.play();
|
|
}
|
|
async function pauseTrack() {
|
|
controller.togglePlay();
|
|
}
|
|
|
|
useEffect(() => {
|
|
console.log("useEffect state:", state);
|
|
if (state == "waiting") {
|
|
setRevealed(true);
|
|
setTimeout(() => {
|
|
setState("next");
|
|
}, 5000);
|
|
} else if (state == "next") {
|
|
nextTrack();
|
|
}
|
|
}, [state]);
|
|
|
|
if (sessionState == "lobby") {
|
|
return (
|
|
<main className="relative flex min-h-screen flex-col items-center justify-between p-24">
|
|
<div className="absolute top-[-1000px]">
|
|
<div id="splayer"></div>
|
|
</div>
|
|
<div>
|
|
<h1
|
|
className={`${nunito.className} text-outline text-[42px] tracking-tighter text-yellow-500`}
|
|
>
|
|
LOBBY
|
|
</h1>
|
|
</div>
|
|
|
|
<div className="absolute inset-1/2 flex min-h-[532px] min-w-[500px] -translate-x-1/2 -translate-y-1/2 flex-col items-center justify-start gap-4 rounded-md bg-blue-950/80 p-8">
|
|
<div className="flex w-full flex-row items-center justify-between px-1">
|
|
<div className="text-3xl font-bold text-yellow-500">
|
|
Players: {userList.length}
|
|
</div>
|
|
<button
|
|
onClick={startSession}
|
|
className="rounded-lg border-2 border-black bg-yellow-500 p-4 text-2xl font-extrabold text-blue-600 hover:bg-yellow-400 disabled:bg-yellow-900 disabled:text-gray-400"
|
|
disabled={sessionId === null}
|
|
>
|
|
START
|
|
</button>
|
|
</div>
|
|
|
|
{userList.length === 0 ? (
|
|
<div className="mt-31 min-w-52 text-3xl font-semibold text-white drop-shadow-sm drop-shadow-gray-900">
|
|
Waiting for players...
|
|
</div>
|
|
) : (
|
|
<div className="grid min-w-52 grid-flow-row grid-cols-2 items-start justify-start gap-4">
|
|
{userList.map((user) => (
|
|
<div
|
|
key={user.id}
|
|
className="flex min-w-51 items-center gap-x-4 rounded-l-[40px] rounded-r-lg border-2 bg-orange-400/90 p-2"
|
|
>
|
|
<img
|
|
src={user.avatar}
|
|
alt={user.name}
|
|
className="h-16 w-16 rounded-full bg-gray-300"
|
|
/>
|
|
<span className="text-2xl font-semibold text-white">
|
|
{user.name}
|
|
</span>
|
|
{user.host && (
|
|
<div className="rounded-full bg-blue-950 p-[5px]">
|
|
<Image
|
|
src="/crown.png"
|
|
width={24}
|
|
height={24}
|
|
alt="Crown image"
|
|
className="p-[1px]"
|
|
/>
|
|
</div>
|
|
)}
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</main>
|
|
);
|
|
} else if (sessionState == "voting") {
|
|
return (
|
|
<main className="flex min-h-screen flex-col items-center justify-between gap-4 p-24">
|
|
<div className="flex w-full items-start justify-between text-center">
|
|
<div className="flex-1"></div>
|
|
<div className="flex-1"></div>
|
|
<div className="flex min-h-[170px] flex-1 items-center justify-center">
|
|
{(state == "playing" || state == "waiting") && (
|
|
<div
|
|
className={`flex h-[170px] w-[170px] items-center justify-center rounded-full border-2 ${state == "playing" ? "bg-orange-400" : ""} ${state == "waiting" ? "bg-green-400" : ""}`}
|
|
>
|
|
<div className="text-5xl text-white drop-shadow-sm drop-shadow-black">
|
|
{secondsLeft}
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
<div className="flex flex-1 flex-row items-center justify-end gap-4">
|
|
<button
|
|
className="w-30 rounded-lg border-2 border-black bg-yellow-500 p-4 text-2xl font-extrabold text-blue-600 hover:bg-yellow-400"
|
|
onClick={playTrack}
|
|
>
|
|
PLAY
|
|
</button>
|
|
<button
|
|
onClick={pauseTrack}
|
|
className="w-30 rounded-lg border-2 border-black bg-yellow-500 p-4 text-2xl font-extrabold text-blue-600 hover:bg-yellow-400"
|
|
>
|
|
PAUSE
|
|
</button>
|
|
<button
|
|
className="w-30 rounded-lg border-2 border-black bg-yellow-500 p-4 text-2xl font-extrabold text-blue-600 hover:bg-yellow-400"
|
|
onClick={endPlay}
|
|
>
|
|
NEXT
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex flex-col items-center justify-center">
|
|
<div className="flex flex-row items-center justify-center gap-x-36">
|
|
<div className="relative w-fit overflow-hidden">
|
|
<div className="grid min-h-[516px] min-w-[516px] grid-flow-row grid-cols-3 place-items-center items-start justify-center gap-4 rounded bg-blue-950/80 px-4 pt-10">
|
|
{noList.map((user) => (
|
|
<div
|
|
key={user.id}
|
|
className="flex h-[120px] w-[120px] flex-col items-center justify-center p-4"
|
|
>
|
|
<img
|
|
src={user.avatar}
|
|
alt={user.name}
|
|
width={100}
|
|
height={100}
|
|
/>
|
|
<div className="mt-1 min-w-[90px] rounded-3xl px-3 text-center text-3xl font-semibold text-white">
|
|
{user.name}
|
|
</div>
|
|
</div>
|
|
))}{" "}
|
|
</div>
|
|
<div
|
|
className={`absolute inset-0 flex items-center justify-center rounded bg-red-800 text-5xl font-bold text-white shadow-[inset_0_0_20px_rgba(0,0,0,0.3)] transition-transform duration-1000 ease-in-out ${
|
|
revealed ? "-translate-y-full" : "translate-y-0"
|
|
}`}
|
|
style={{
|
|
backgroundImage: `
|
|
linear-gradient(to bottom, rgba(0,0,0,0.2), rgba(0,0,0,0) 20%, rgba(0,0,0,0.2) 80%, rgba(0,0,0,0.2)),
|
|
repeating-linear-gradient(
|
|
90deg,
|
|
rgba(255,255,255,0.1),
|
|
rgba(255,255,255,0.1) 30px,
|
|
rgba(0,0,0,0) 30px,
|
|
rgba(0,0,0,0) 60px
|
|
)`,
|
|
clipPath:
|
|
"polygon(0 0, 100% 0, 100% 98%, 95% 100%, 90% 98%, 85% 100%, 80% 98%, 75% 100%, 70% 98%, 65% 100%, 60% 98%, 55% 100%, 50% 98%, 45% 100%, 40% 98%, 35% 100%, 30% 98%, 25% 100%, 20% 98%, 15% 100%, 10% 98%, 5% 100%, 0 98%)",
|
|
}}
|
|
>
|
|
Meh
|
|
</div>
|
|
</div>
|
|
{!songLoading && currentSong ? (
|
|
<div className="rounded-md bg-black p-2">
|
|
<button onClick={nextTrack}>
|
|
<Image
|
|
src={currentSong.album_cover}
|
|
alt={currentSong.name}
|
|
width={500}
|
|
height={500}
|
|
/>
|
|
</button>
|
|
</div>
|
|
) : (
|
|
<div className="rounded-md border-8 border-black bg-blue-950/80 py-[1px]">
|
|
<Image
|
|
src="/cdspin.gif"
|
|
alt="Loading..."
|
|
width={500}
|
|
height={500}
|
|
className="rounded-md"
|
|
/>
|
|
</div>
|
|
)}
|
|
<div className="relative w-fit overflow-hidden">
|
|
<div className="grid min-h-[516px] min-w-[516px] grid-flow-row grid-cols-3 place-items-center items-start justify-center gap-4 rounded bg-blue-950/80 px-4 pt-10">
|
|
{yesList.map((user) => (
|
|
<div
|
|
key={user.id}
|
|
className="flex h-[120px] w-[120px] flex-col items-center justify-center p-4"
|
|
>
|
|
<img
|
|
src={user.avatar}
|
|
alt={user.name}
|
|
width={100}
|
|
height={100}
|
|
/>
|
|
<div className="mt-1 min-w-[90px] rounded-3xl px-3 text-center text-3xl font-semibold text-white">
|
|
{user.name}
|
|
</div>
|
|
</div>
|
|
))}{" "}
|
|
</div>
|
|
<div
|
|
className={`absolute inset-0 flex items-center justify-center rounded bg-red-800 text-5xl font-bold text-white shadow-[inset_0_0_20px_rgba(0,0,0,0.3)] transition-transform duration-1000 ease-in-out ${
|
|
revealed ? "-translate-y-full" : "translate-y-0"
|
|
}`}
|
|
style={{
|
|
backgroundImage: `
|
|
linear-gradient(to bottom, rgba(0,0,0,0.2), rgba(0,0,0,0) 20%, rgba(0,0,0,0.2) 80%, rgba(0,0,0,0.2)),
|
|
repeating-linear-gradient(
|
|
90deg,
|
|
rgba(255,255,255,0.1),
|
|
rgba(255,255,255,0.1) 30px,
|
|
rgba(0,0,0,0) 30px,
|
|
rgba(0,0,0,0) 60px
|
|
)`,
|
|
clipPath:
|
|
"polygon(0 0, 100% 0, 100% 98%, 95% 100%, 90% 98%, 85% 100%, 80% 98%, 75% 100%, 70% 98%, 65% 100%, 60% 98%, 55% 100%, 50% 98%, 45% 100%, 40% 98%, 35% 100%, 30% 98%, 25% 100%, 20% 98%, 15% 100%, 10% 98%, 5% 100%, 0 98%)",
|
|
}}
|
|
>
|
|
Yeah
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{!songLoading && currentSong ? (
|
|
<div className="flex flex-col items-center justify-start">
|
|
<h2
|
|
className={`${nunito.className} text-outline text-[42px] tracking-tighter text-yellow-500`}
|
|
>
|
|
{currentSong.artist}
|
|
</h2>
|
|
<h2
|
|
className={`${nunito.className} text-outline text-[42px] tracking-tighter text-yellow-500`}
|
|
>
|
|
{currentSong.name}
|
|
</h2>
|
|
</div>
|
|
) : (
|
|
<div className="flex flex-col items-center justify-start">
|
|
<h2
|
|
className={`${nunito.className} text-outline text-[42px] tracking-tighter text-yellow-500`}
|
|
>
|
|
Loading
|
|
</h2>
|
|
<h2
|
|
className={`${nunito.className} text-outline text-[42px] tracking-tighter text-yellow-500`}
|
|
>
|
|
...
|
|
</h2>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
<div
|
|
className={`flex min-h-33 min-w-1 flex-row items-start justify-start gap-10 rounded p-4 ${userList.some((u) => !u.hidden) ? "bg-blue-950/80" : "bg-transparent p-0"} `}
|
|
>
|
|
{userList.map((user) => (
|
|
<div
|
|
key={user.id}
|
|
className={`flex h-[100px] w-[100px] flex-col items-center justify-center p-4 ${user.hidden ? "hidden" : ""}`}
|
|
>
|
|
<img src={user.avatar} alt={user.name} width={75} height={75} />
|
|
<div className="mt-1 min-w-[90px] rounded-3xl px-3 text-center text-3xl font-semibold text-white">
|
|
{user.name}
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</main>
|
|
);
|
|
}
|
|
}
|