27 Commits

Author SHA1 Message Date
mark bbb9d37ec7 adds export script. does some cleanup 2025-08-23 13:50:01 +02:00
guus bcaaae90e9 post-game opleuken 2025-08-22 12:20:11 +02:00
mark b4a037388a adds next track short animation 2025-08-21 20:43:00 +02:00
mark c710845288 post-game update prepare 2025-08-21 20:03:37 +02:00
mark 946548d001 updates post-game time to 12s 2025-08-21 19:54:18 +02:00
mark 7ed8895038 hard next on album click 2025-08-21 19:53:53 +02:00
mark 51697622a7 minimum height on klokje 2025-08-21 19:53:28 +02:00
guus 6b6da07ddf Merge pull request 'localStorage token and websocket reconnect' (#28) from auth-session-updates into master
Reviewed-on: #28
2025-08-21 18:40:06 +02:00
guus 2403734bf3 fix clock position 2025-08-21 18:36:46 +02:00
mark 9184d759f3 localStorage token and websocket reconnect 2025-08-21 09:25:30 +02:00
mark 9c94934ff3 post-game info 2025-08-20 22:48:01 +02:00
guus e72e2e6800 Merge pull request 'auto connects user to session if available' (#26) from auto-session into master
Reviewed-on: #26
2025-08-20 21:55:00 +02:00
guus 65e81a7e6b Merge pull request 'game on' (#27) from game-mode into master
Reviewed-on: #27
2025-08-20 21:54:49 +02:00
mark c23cb63817 game on 2025-08-20 21:51:51 +02:00
mark 5bdf1ed940 auto connects user to session if available 2025-08-20 19:53:32 +02:00
guus 2a23f1dedc changed loading image 2025-08-19 21:02:43 +02:00
guus da10b19960 add gordijntje 2025-08-19 12:54:03 +02:00
guus 5b0ef233c4 update vote boxes 2025-08-19 00:29:17 +02:00
mark 1d50bc3839 updates avatar. bye pravatar 2025-08-17 22:10:41 +02:00
mark 479dbe5c2d remove pause page 2025-08-17 22:07:20 +02:00
mark 4e324502ae cleanup 2025-08-17 21:58:44 +02:00
mark d6cfdf66c6 adds loading text to lobby 2025-08-17 21:56:16 +02:00
mark a64d9fea31 fixes issue with vote not updating 2025-08-17 21:26:10 +02:00
mark b04a4732cf Merge pull request 'spotify player' (#15) from spotify-player into master
Reviewed-on: #15
2025-08-17 20:28:38 +02:00
mark bd71bce450 fixes compilation 2025-08-17 20:28:19 +02:00
mark cfb6b2609b ignores logs 2025-08-17 20:25:47 +02:00
mark 564f2d47b4 spotify player 2025-08-17 20:17:46 +02:00
19 changed files with 421 additions and 1349 deletions
+3
View File
@@ -8,3 +8,6 @@ wallpapers/
frontend/node_modules/ frontend/node_modules/
frontend/.next/ frontend/.next/
frontend/src/data/types.ts frontend/src/data/types.ts
backend/session_*.log
backend/*.csv
*.bak
View File
+23
View File
@@ -0,0 +1,23 @@
-- sqlite3 -csv db.sqlite3 '.read export.sql' > export.csv
SELECT
CONCAT('https://open.spotify.com/track/', T.id) as spotify_id,
T.name as track_name,
A.name as artist_name,
(CAST(T.duration_ms AS REAL) / 86400000) as duration,
V.point_total,
V.vote_count,
CAST(V.point_total AS REAL) / CAST(V.vote_count AS REAL) as average_points,
U.username as nominated_by
FROM
(
SELECT track_id, SUM(points) as point_total, COUNT(*) as vote_count
FROM playlist_vote
GROUP BY track_id
) AS V
INNER JOIN playlist_track T ON V.track_id = T.id
INNER JOIN (
SELECT track_id, MIN(artist_id) as artist_id FROM playlist_track_artists GROUP BY track_id
) PTA ON T.id = PTA.track_id
INNER JOIN playlist_artist A ON PTA.artist_id = A.id
INNER JOIN auth_user U ON T.nominated_by_id = U.id
ORDER BY average_points DESC, vote_count DESC;
File diff suppressed because it is too large Load Diff
+18 -2
View File
@@ -73,8 +73,10 @@ class SessionConsumer(WebsocketConsumer):
async_to_sync(self.channel_layer.group_send)( async_to_sync(self.channel_layer.group_send)(
self.session_name, {"type": "voted", "user": { "id": user.id, "username": user.username}, "track": track.id, "points": points} self.session_name, {"type": "voted", "user": { "id": user.id, "username": user.username}, "track": track.id, "points": points}
) )
if action == "pause": if action == "start":
pass async_to_sync(self.channel_layer.group_send)(
self.session_name, {"type": "announce.start"}
)
if action == "next": if action == "next":
random.seed(session.seed) random.seed(session.seed)
tracks = Track.objects.order_by("?") tracks = Track.objects.order_by("?")
@@ -83,12 +85,21 @@ class SessionConsumer(WebsocketConsumer):
for track in tracks: for track in tracks:
if next: if next:
session.current_track = track session.current_track = track
next = False
break break
if session.current_track == track: if session.current_track == track:
next = True next = True
if next:
# We didnt get a next track, so it must have been the last one
session.current_track = None
else: else:
session.current_track = tracks.first() session.current_track = tracks.first()
session.save() session.save()
if session.current_track is None:
async_to_sync(self.channel_layer.group_send)(
self.session_name, {"type": "announce.done"}
)
else:
async_to_sync(self.channel_layer.group_send)( async_to_sync(self.channel_layer.group_send)(
self.session_name, {"type": "announce.track"} self.session_name, {"type": "announce.track"}
) )
@@ -98,8 +109,13 @@ class SessionConsumer(WebsocketConsumer):
user = event["user"] user = event["user"]
self.send(text_data=json.dumps({"joined": user})) self.send(text_data=json.dumps({"joined": user}))
# TODO: maybe this could be one function.
def announce_track(self, event): def announce_track(self, event):
self.send(text_data=json.dumps({"track": {}})) self.send(text_data=json.dumps({"track": {}}))
def announce_done(self, event):
self.send(text_data=json.dumps({"done": {}}))
def announce_start(self, event):
self.send(text_data=json.dumps({"start": {}}))
def voted(self, event): def voted(self, event):
user = event["user"] user = event["user"]
+1
View File
@@ -7,4 +7,5 @@ urlpatterns = [
path("session/<str:session_id>/test", views.session, name="session"), path("session/<str:session_id>/test", views.session, name="session"),
path("session/", views.SessionListView.as_view(), name="api-session-list"), path("session/", views.SessionListView.as_view(), name="api-session-list"),
path("session/<str:session_id>/", views.SessionDetailView.as_view(), name="api-session-detail"), path("session/<str:session_id>/", views.SessionDetailView.as_view(), name="api-session-detail"),
path("session/<str:session_id>/info", views.SessionInfoView.as_view(), name="api-session-info"),
] ]
+9
View File
@@ -1,4 +1,5 @@
from django.shortcuts import render from django.shortcuts import render
from django.db.models import Avg
from rest_framework.views import APIView from rest_framework.views import APIView
from rest_framework.response import Response from rest_framework.response import Response
from playlist.models import Session, Profile, Track, Vote from playlist.models import Session, Profile, Track, Vote
@@ -31,3 +32,11 @@ class SessionDetailView(APIView):
def get(self, request, session_id): def get(self, request, session_id):
session = Session.objects.get(session_id=session_id) session = Session.objects.get(session_id=session_id)
return Response(SessionSerializer(session).data) return Response(SessionSerializer(session).data)
class SessionInfoView(APIView):
def get(self, request, session_id):
track_count = len(Track.objects.all())
vote_count = len(Vote.objects.all())
average_vote = Vote.objects.all().aggregate(Avg("points", default=0))['points__avg']
return Response({'track_count': track_count, 'vote_count': vote_count, 'average_vote': average_vote})
Binary file not shown.

After

Width:  |  Height:  |  Size: 787 KiB

+1 -5
View File
@@ -23,26 +23,22 @@ const pageConfig: Record<
string, string,
{ {
showAuthStatus: boolean; showAuthStatus: boolean;
showSessionChecker: boolean;
showNav: boolean; showNav: boolean;
showQuota: boolean; showQuota: boolean;
} }
> = { > = {
"/nominations": { "/nominations": {
showAuthStatus: true, showAuthStatus: true,
showSessionChecker: false,
showQuota: true, showQuota: true,
showNav: navToggle, showNav: navToggle,
}, },
"/mobile-voting": { "/mobile-voting": {
showAuthStatus: false, showAuthStatus: false,
showSessionChecker: false,
showQuota: false, showQuota: false,
showNav: false, showNav: false,
}, },
"/lobby": { "/lobby": {
showAuthStatus: true, showAuthStatus: true,
showSessionChecker: false,
showQuota: true, showQuota: true,
showNav: navToggle, showNav: navToggle,
}, },
@@ -60,7 +56,7 @@ function LayoutContent({ children }: { children: React.ReactNode }) {
return ( return (
<> <>
{config.showSessionChecker && <SessionChecker />} <SessionChecker />
{config.showAuthStatus && <AuthStatus />} {config.showAuthStatus && <AuthStatus />}
{children} {children}
{config.showNav && <NavigationBar />} {config.showNav && <NavigationBar />}
+186 -24
View File
@@ -1,12 +1,15 @@
"use client"; "use client";
import { useState, useEffect } from "react"; import { useState, useEffect } from "react";
import { Markazi_Text, Nunito } from "next/font/google"; import { Markazi_Text, Nunito } from "next/font/google";
import { useRouter } from "next/navigation";
import Image from "next/image"; import Image from "next/image";
import Link from "next/link"; import Link from "next/link";
import { useAuth } from "react-oidc-context"; import { useAuth } from "react-oidc-context";
import { useSession } from "muzak/contexts/SessionContext"; import { useSession } from "muzak/contexts/SessionContext";
import { getUser, api, Track } from "muzak/data/fetcher"; import { getUser, api, Track } from "muzak/data/fetcher";
import getSessionId from "muzak/data/session"; import getSessionId from "muzak/data/session";
import { useSpotifyIframe } from "muzak/contexts/SpotifyIframe";
import { useAnimation } from "muzak/contexts/AnimationContext";
const nunito = Nunito({ const nunito = Nunito({
weight: "900", weight: "900",
@@ -24,8 +27,16 @@ export default function Lobby() {
const [sessionState, setSessionState] = useState("lobby"); const [sessionState, setSessionState] = useState("lobby");
const [currentSong, setCurrentSong] = useState<Track>(); const [currentSong, setCurrentSong] = useState<Track>();
const [songLoading, setSongLoading] = useState(false); const [songLoading, setSongLoading] = useState(false);
const [spotifyId, setSpotifyId] = useState<string | null>(null);
const auth = useAuth(); const auth = useAuth();
const user = getUser(); 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
const { isAnimate, setIsAnimate } = useAnimation();
// Main message handler and dispatcher // Main message handler and dispatcher
function onMessage(e) { function onMessage(e) {
@@ -35,9 +46,13 @@ export default function Lobby() {
if (key === "joined") { if (key === "joined") {
addUser(data[key]); addUser(data[key]);
} else if (key == "track") { } else if (key == "track") {
setIsAnimate(false);
setTimeout(() => setIsAnimate(true), 1000);
getTrack(); getTrack();
} else if (key == "voted") { } else if (key == "voted") {
voteUser(data[key]); voteUser(data[key]);
} else if (key == "done") {
router.push("/post-game");
} }
} }
console.log(data); console.log(data);
@@ -74,14 +89,21 @@ export default function Lobby() {
function startSession() { function startSession() {
setSessionState("voting"); setSessionState("voting");
setState("loading");
getTrack(); getTrack();
sessionSocket.send(
JSON.stringify({
action: "start",
token: user.access_token,
}),
);
} }
function addUser(user: object) { function addUser(user: object) {
const newUser = { const newUser = {
id: user["id"], id: user["id"],
name: user["username"], name: user["username"],
avatar: `https://i.pravatar.cc/150?img=${user["id"]}`, avatar: `https://api.dicebear.com/9.x/bottts-neutral/svg?seed=${user["id"]}`,
hidden: false, hidden: false,
}; };
@@ -113,7 +135,7 @@ export default function Lobby() {
{ {
id: userId, id: userId,
name: userName, name: userName,
avatar: `https://i.pravatar.cc/150?img=${userId}`, avatar: `https://api.dicebear.com/9.x/bottts-neutral/svg?seed=${userId}`,
}, },
]; ];
}); });
@@ -130,7 +152,7 @@ export default function Lobby() {
{ {
id: userId, id: userId,
name: userName, name: userName,
avatar: `https://i.pravatar.cc/150?img=${userId}`, avatar: `https://api.dicebear.com/9.x/bottts-neutral/svg?seed=${userId}`,
}, },
]; ];
}); });
@@ -148,6 +170,7 @@ export default function Lobby() {
async function getTrack() { async function getTrack() {
setSongLoading(true); setSongLoading(true);
setState("loading");
setUserList((prevUserList) => { setUserList((prevUserList) => {
return prevUserList.map((user) => { return prevUserList.map((user) => {
return { ...user, hidden: false }; return { ...user, hidden: false };
@@ -165,10 +188,14 @@ export default function Lobby() {
} else { } else {
nextTrack(); nextTrack();
} }
setSpotifyId(session?.current_track);
setSongLoading(false); setSongLoading(false);
} }
async function nextTrack() { async function nextTrack() {
console.log("nextTrack");
setState("loading");
setRevealed(false);
setSongLoading(true); setSongLoading(true);
sessionSocket.send( sessionSocket.send(
JSON.stringify({ JSON.stringify({
@@ -178,9 +205,71 @@ export default function Lobby() {
); );
} }
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");
}, 12000);
} else if (state == "next") {
nextTrack();
}
}, [state]);
if (sessionState == "lobby") { if (sessionState == "lobby") {
return ( return (
<main className="relative flex min-h-screen flex-col items-center justify-between p-24"> <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> <div>
<h1 <h1
className={`${nunito.className} text-outline text-[42px] tracking-tighter text-yellow-500`} className={`${nunito.className} text-outline text-[42px] tracking-tighter text-yellow-500`}
@@ -196,7 +285,8 @@ export default function Lobby() {
</div> </div>
<button <button
onClick={startSession} 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" 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 START
</button> </button>
@@ -244,24 +334,34 @@ export default function Lobby() {
<main className="flex min-h-screen flex-col items-center justify-between gap-4 p-24"> <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 w-full items-start justify-between text-center">
<div className="flex-1"></div> <div className="flex-1"></div>
<div className="flex flex-1 items-center justify-center"> <div className="flex-1"></div>
<div className="flex h-[170px] w-[170px] items-center justify-center rounded-full border-2 bg-orange-400"> <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"> <div className="text-5xl text-white drop-shadow-sm drop-shadow-black">
<button onClick={getTrack}>30</button> {secondsLeft}
</div> </div>
</div> </div>
)}
</div> </div>
<div className="flex flex-1 flex-row items-center justify-end gap-4"> <div className="flex flex-1 flex-row items-center justify-end gap-4">
<Link <button
href="/pause" 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" 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 PAUSE
</Link> </button>
<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" 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={nextTrack} onClick={endPlay}
> >
NEXT NEXT
</button> </button>
@@ -269,8 +369,9 @@ export default function Lobby() {
</div> </div>
<div className="flex flex-col items-center justify-center"> <div className="flex flex-col items-center justify-center">
<div className="flex flex-row items-center justify-center gap-x-32"> <div className="flex flex-row items-center justify-center gap-x-36">
<div className="grid min-w-52 grid-flow-row grid-cols-3 items-start justify-start gap-4 rounded bg-blue-950/80 p-4"> <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) => ( {noList.map((user) => (
<div <div
key={user.id} key={user.id}
@@ -279,8 +380,8 @@ export default function Lobby() {
<img <img
src={user.avatar} src={user.avatar}
alt={user.name} alt={user.name}
width={75} width={100}
height={75} height={100}
/> />
<div className="mt-1 min-w-[90px] rounded-3xl px-3 text-center text-3xl font-semibold text-white"> <div className="mt-1 min-w-[90px] rounded-3xl px-3 text-center text-3xl font-semibold text-white">
{user.name} {user.name}
@@ -288,26 +389,51 @@ export default function Lobby() {
</div> </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 ? ( {!songLoading && currentSong ? (
<div className="rounded-md bg-black p-2"> <div className="rounded-md bg-black p-2">
<button onClick={nextTrack}>
<Image <Image
src={currentSong.album_cover} src={currentSong.album_cover}
alt={currentSong.name} alt={currentSong.name}
width={500} width={500}
height={500} height={500}
/> />
</button>
</div> </div>
) : ( ) : (
<div className="rounded-md bg-black p-2"> <div className="rounded-md border-8 border-black bg-blue-950/80 py-[1px]">
<Image <Image
src="/meowl.png" src="/cdspin.gif"
alt="Loading.." alt="Loading..."
width={500} width={500}
height={500} height={500}
className="rounded-md"
/> />
</div> </div>
)} )}
<div className="grid min-w-52 grid-flow-row grid-cols-3 items-start justify-start gap-4 rounded bg-blue-950/80 p-4"> <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) => ( {yesList.map((user) => (
<div <div
key={user.id} key={user.id}
@@ -316,8 +442,8 @@ export default function Lobby() {
<img <img
src={user.avatar} src={user.avatar}
alt={user.name} alt={user.name}
width={75} width={100}
height={75} height={100}
/> />
<div className="mt-1 min-w-[90px] rounded-3xl px-3 text-center text-3xl font-semibold text-white"> <div className="mt-1 min-w-[90px] rounded-3xl px-3 text-center text-3xl font-semibold text-white">
{user.name} {user.name}
@@ -325,8 +451,29 @@ export default function Lobby() {
</div> </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>
{!songLoading && currentSong && ( </div>
</div>
{!songLoading && currentSong ? (
<div className="flex flex-col items-center justify-start"> <div className="flex flex-col items-center justify-start">
<h2 <h2
className={`${nunito.className} text-outline text-[42px] tracking-tighter text-yellow-500`} className={`${nunito.className} text-outline text-[42px] tracking-tighter text-yellow-500`}
@@ -339,10 +486,25 @@ export default function Lobby() {
{currentSong.name} {currentSong.name}
</h2> </h2>
</div> </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>
<div className="flex flex-row items-start justify-start gap-10 rounded bg-blue-950/80 p-4"> <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) => ( {userList.map((user) => (
<div <div
key={user.id} key={user.id}
+14 -6
View File
@@ -43,10 +43,19 @@ export default function MobileVoting() {
console.log(sessionSocket?.readyState); console.log(sessionSocket?.readyState);
await waitForOpenSocket(); await waitForOpenSocket();
console.log("socket is open"); console.log("socket is open");
sessionSocket.onclose = function (e) {
console.log("Chat socket closed unexpectedly");
setYesHighlight(false);
setNoHighlight(false);
setVote(0.5);
setSessionId(null);
setTimeout(() => {
setSession();
}, 2000);
};
sessionSocket.send( sessionSocket.send(
JSON.stringify({ JSON.stringify({
action: "login", action: "login",
userr: user.profile.nickname,
token: user.access_token, token: user.access_token,
}), }),
); );
@@ -57,13 +66,13 @@ export default function MobileVoting() {
console.log(e); console.log(e);
let data = JSON.parse(e.data); let data = JSON.parse(e.data);
for (let key in data) { for (let key in data) {
if (key == "track") { if (key == "track" || key == "start") {
setYesHighlight(false); setYesHighlight(false);
setNoHighlight(false); setNoHighlight(false);
setVote(0.5);
} }
} }
console.log(data); console.log(data);
//}, 1000);
} }
// Initialize WebSocket connection when sessionId changes // Initialize WebSocket connection when sessionId changes
@@ -75,7 +84,6 @@ export default function MobileVoting() {
); );
socket.onmessage = onMessage; socket.onmessage = onMessage;
setSessionSocket(socket); setSessionSocket(socket);
//setTimeout(() => connect(), 2000);
} }
}, [sessionId]); }, [sessionId]);
@@ -100,7 +108,7 @@ export default function MobileVoting() {
}, [auth]); }, [auth]);
useEffect(() => { useEffect(() => {
if (sessionSocket?.readyState == WebSocket.OPEN) { if (vote != 0.5 && sessionSocket?.readyState == WebSocket.OPEN) {
sessionSocket.send( sessionSocket.send(
JSON.stringify({ JSON.stringify({
action: "vote", action: "vote",
@@ -112,7 +120,7 @@ export default function MobileVoting() {
}, [vote]); }, [vote]);
return ( return (
<main className="m-3 h-screen rounded bg-blue-950/80 p-8 sm:p-14 md:p-20"> <main className="m-0 h-screen rounded bg-blue-950/80 p-8 sm:p-14 md:p-20">
<div className="flex flex-row items-center justify-between gap-8 rounded-md"> <div className="flex flex-row items-center justify-between gap-8 rounded-md">
<button <button
className={`flex aspect-square w-1/2 max-w-[200px] items-center rounded bg-black/20 p-0 text-center font-bold text-white ${noHighlight ? "ring-5 ring-amber-400" : "ring-0 ring-black/20"}`} className={`flex aspect-square w-1/2 max-w-[200px] items-center rounded bg-black/20 p-0 text-center font-bold text-white ${noHighlight ? "ring-5 ring-amber-400" : "ring-0 ring-black/20"}`}
-32
View File
@@ -1,32 +0,0 @@
"use client";
import { Nunito } from "next/font/google";
import Link from "next/link";
const nunito = Nunito({
weight: "900",
subsets: ["latin"],
});
export default function Pause() {
return (
<main className="flex min-h-screen flex-col items-center justify-between p-24">
<div>
<h1
className={`${nunito.className} text-outline text-[42px] tracking-tighter text-yellow-500`}
>
PAUZE
</h1>
</div>
<img
src="https://c.tenor.com/uWx6oZf6NnsAAAAC/tenor.gif"
className="w-1/3 rounded-full"
/>
<Link
href="/voting"
className="rounded-lg border-2 border-black bg-yellow-500 p-4 text-2xl font-extrabold text-blue-600 hover:bg-yellow-400"
>
HERVATTEN
</Link>
</main>
);
}
+54 -3
View File
@@ -1,9 +1,60 @@
"use client";
import { useEffect, useState } from "react";
import { api } from "muzak/data/fetcher";
import { useSession } from "muzak/contexts/SessionContext";
import { Nunito } from "next/font/google";
const nunito = Nunito({
weight: "900",
subsets: ["latin"],
});
export default function PostGame() { export default function PostGame() {
const { sessionId, setSessionId } = useSession();
const [trackCount, setTrackCount] = useState(0);
const [voteCount, setVoteCount] = useState(0);
const [averageVote, setAverageVote] = useState(0);
async function getInfo() {
const { data: info } = await api.sessions.info({ session_id: sessionId });
console.log(info);
setTrackCount(info["track_count"]);
setVoteCount(info["vote_count"]);
setAverageVote(info["average_vote"]);
}
useEffect(() => {
getInfo();
}, []);
return ( return (
<main className="flex min-h-screen flex-col items-center justify-between p-24"> <main className="flex min-h-screen flex-col items-center justify-between p-24">
<h1>Post-game</h1> <p
<p>Bedankt voor het stemmen!</p> className={`${nunito.className} text-outline text-[42px] tracking-tighter text-yellow-500`}
<p>;)</p> >
Bedankt voor het stemmen!
</p>
<div className="flex flex-col text-center">
<p
className={`${nunito.className} text-outline text-[42px] tracking-tighter text-yellow-500`}
>
Jullie hebben {voteCount} stemmen uitgebracht op {trackCount} nummers.
</p>
<p
className={`${nunito.className} text-outline text-[42px] tracking-tighter text-yellow-500`}
>
Gemiddeld vonden jullie {(averageVote * 100).toFixed(0)}% van de
nummers best oké.
</p>
<p
className={`${nunito.className} text-outline text-[42px] tracking-tighter text-yellow-500`}
>
Applaus voor jezelf!
</p>
</div>
<p
className={`${nunito.className} text-outline text-[42px] tracking-tighter text-yellow-500`}
>
Tot HDcon!
</p>
</main> </main>
); );
} }
+5
View File
@@ -41,3 +41,8 @@
.text-outline-small { .text-outline-small {
-webkit-text-stroke: 1px black; -webkit-text-stroke: 1px black;
} }
iframe {
position: absolute;
top: -1000px;
}
-164
View File
@@ -1,164 +0,0 @@
"use client";
import { useState, useEffect } from "react";
import { Nunito } from "next/font/google";
import users from "muzak/data/users.json";
import Image from "next/image";
import Link from "next/link";
import { api, Track } from "muzak/data/fetcher";
const nunito = Nunito({
weight: "900",
subsets: ["latin"],
});
export default function Lobby() {
const [partySize, setPartySize] = useState(8);
const [userList, setUserList] = useState([]);
const [yesVoteList, setYesVoteList] = useState([]);
const [noVoteList, setNoVoteList] = useState([]);
const [currentSong, setCurrentSong] = useState<Track>();
const [songLoading, setSongLoading] = useState(false);
useEffect(() => {
populateUserList();
}, [partySize]);
async function getTrack() {
setSongLoading(true);
const { data: track } = await api.tracks.vote({});
setCurrentSong(track);
console.log("I got a track!", track.name);
setSongLoading(false);
}
useEffect(() => {
if (!currentSong && !songLoading) getTrack();
}, []);
function populateUserList() {
const mappedUsers = users.slice(0, partySize).map((user) => ({
id: user.id,
name: user.name,
avatar: user.avatar,
host: user.host,
}));
setUserList(mappedUsers);
}
// function handleYesVote(user) {
// setYesVoteList([...yesVoteList, user]);
// }
// function handleNoVote(user) {
// setNoVoteList([...noVoteList, user]);
// }
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 flex-1 items-center justify-center">
<div className="flex h-[170px] w-[170px] items-center justify-center rounded-full border-2 bg-orange-400">
<div className="text-5xl text-white drop-shadow-sm drop-shadow-black">
<button onClick={getTrack}>30</button>
</div>
</div>
</div>
<div className="flex flex-1 flex-row items-center justify-end gap-4">
<Link
href="/pause"
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
</Link>
<Link
href="/voting"
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"
>
NEXT
</Link>
</div>
</div>
<div className="flex flex-col items-center justify-center">
<div className="flex flex-row items-center justify-center gap-x-32">
<div className="grid min-w-52 grid-flow-row grid-cols-3 items-start justify-start gap-4 rounded bg-blue-950/80 p-4">
{userList.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={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>
{!songLoading && currentSong ? (
<div className="rounded-md bg-black p-2">
<Image
src={currentSong.album_cover}
alt={currentSong.name}
width={500}
height={500}
/>
</div>
) : (
<div className="rounded-md bg-black p-2">
<Image
src="/meowl.png"
alt="Loading.."
width={500}
height={500}
/>
</div>
)}
<div className="grid min-w-52 grid-flow-row grid-cols-3 items-start justify-start gap-4 rounded bg-blue-950/80 p-4">
{userList.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={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>
</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>
<div className="flex flex-row items-start justify-start gap-10 rounded bg-blue-950/80 p-4">
{userList.map((user) => (
<div
key={user.id}
className="flex h-[100px] w-[100px] flex-col items-center justify-center p-4"
>
<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>
);
}
+18 -34
View File
@@ -11,44 +11,28 @@ import getSessionId from "muzak/data/session";
export default function SessionChecker() { export default function SessionChecker() {
const auth = useAuth(); const auth = useAuth();
const router = useRouter(); const router = useRouter();
const path = usePathname();
const [isSession, setIsSession] = useState(false);
const [link, setLink] = useState("/lobby");
const { sessionId, setSessionId } = useSession(); const { sessionId, setSessionId } = useSession();
// async function getSessions() { async function getSessions() {
// if (!auth.isLoading && !auth.error && auth.isAuthenticated) { if (!auth.isLoading && !auth.error && auth.isAuthenticated) {
// const { data: sessions } = await api?.sessions?.list({}); const { data: sessions } = await api?.sessions?.list({});
// console.log(sessions); console.log(sessions);
// if (sessions?.length > 0) { if (sessions?.length > 0) {
// setIsSession(true); const user = getUser();
// const user = getUser(); setSessionId(sessions[0].session_id);
// setSessionId(sessions[0].session_id); if (sessions[0].host !== user?.profile?.email) {
// if (sessions[0].host === user?.profile?.email) { if (path !== "/mobile-voting") {
// setLink("/lobby"); router.push("/mobile-voting");
// } else { }
// setLink("/session"); }
// } }
// } else { }
// setIsSession(false); }
// }
// }
// }
useEffect(() => { useEffect(() => {
//getSessionId(); getSessions();
console.log("hheufheufh"); }, [auth, path]);
}, [auth]);
if (sessionId) {
return (
<Link
href={link}
className="bold text-l absolute top-2 left-2 rounded-[50%] bg-red-500 p-3 font-bold text-white"
>
Session available!
</Link>
);
}
return <> </>; return <> </>;
} }
+8 -1
View File
@@ -1,4 +1,4 @@
import { UserManager } from "oidc-client-ts"; import { UserManager, WebStorageStateStore } from "oidc-client-ts";
import { AuthProviderProps } from "react-oidc-context"; import { AuthProviderProps } from "react-oidc-context";
const ORIGIN_URI = globalThis?.window?.location.origin; const ORIGIN_URI = globalThis?.window?.location.origin;
@@ -17,8 +17,15 @@ const userConfig: AuthProviderProps = {
post_logout_redirect_uri: ORIGIN_URI, post_logout_redirect_uri: ORIGIN_URI,
response_mode: "query", response_mode: "query",
revokeTokensOnSignout: true, revokeTokensOnSignout: true,
staleStateAgeInSeconds: 60000,
}; };
if (globalThis?.window != undefined) {
userConfig.userStore = new WebStorageStateStore({
store: window.localStorage,
});
}
export const userManager = new UserManager(userConfig); export const userManager = new UserManager(userConfig);
// Some handling of token expiration. // Some handling of token expiration.
+35
View File
@@ -0,0 +1,35 @@
"use client";
import { useEffect, useState } from "react";
export const useSpotifyIframe = () => {
const [controller, setController] = useState(null);
const elementId = "splayer";
useEffect(() => {
const script = document.createElement("script");
const spotifyScriptUrl = "https://open.spotify.com/embed/iframe-api/v1";
const spotifyScriptElementId = "spotify-iframe-api";
script.id = spotifyScriptElementId;
script.setAttribute("id", spotifyScriptElementId);
script.src = spotifyScriptUrl;
document.body.appendChild(script);
script.onload = () => {
(window as any).onSpotifyIframeApiReady = (IFrameAPI) => {
const element = document.getElementById(elementId);
const options = {
width: 400,
height: 400,
};
const callback = (EmbedController) => {
setController(EmbedController);
};
IFrameAPI.createController(element, options, callback);
};
};
return () => {
document.body.removeChild(script);
};
}, []);
return controller;
};
+9
View File
@@ -9,6 +9,11 @@ export function getUser() {
let oidcStorage = window?.sessionStorage.getItem( let oidcStorage = window?.sessionStorage.getItem(
`oidc.user:${oidcConfig.authority}:${oidcConfig.client_id}`, `oidc.user:${oidcConfig.authority}:${oidcConfig.client_id}`,
); );
if (!oidcStorage) {
oidcStorage = window?.localStorage.getItem(
`oidc.user:${oidcConfig.authority}:${oidcConfig.client_id}`,
);
}
if (!oidcStorage) { if (!oidcStorage) {
return null; return null;
} }
@@ -87,6 +92,10 @@ export const api = {
}, },
sessions: { sessions: {
get: fetcher.path("/voting/session/{session_id}/").method("get").create(), get: fetcher.path("/voting/session/{session_id}/").method("get").create(),
info: fetcher
.path("/voting/session/{session_id}/info")
.method("get")
.create(),
list: fetcher.path("/voting/session/").method("get").create(), list: fetcher.path("/voting/session/").method("get").create(),
}, },
}; };