60 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
guus f22ff07577 modify lobby and pause look 2025-08-17 18:54:39 +02:00
mark 47583e36dc vote feedback 2025-08-17 11:47:14 +02:00
mark 7d54f6c5ce fixes userlist 2025-08-17 10:05:51 +02:00
mark 698c41544c process user as object 2025-08-16 23:30:49 +02:00
mark 5de174b162 adds some logging 2025-08-16 23:30:16 +02:00
mark 341f7c8091 move websockets to new path 2025-08-16 15:10:13 +02:00
mark 12344272ae stops sessionchecker from fucking up 2025-08-16 14:34:11 +02:00
mark 762ec15a30 fixes build 2025-08-16 14:11:38 +02:00
mark c7eb174a01 betere sessies lol lmao even 2025-08-16 09:51:29 +02:00
mark a1526ddc67 session link with mobile voting 2025-08-15 18:30:57 +02:00
guus cbc9be1f63 improved pause screen 2025-08-14 21:40:59 +02:00
guus 6216bd2c84 update mobile voting screen 2025-08-14 21:23:05 +02:00
mark 368628c070 session updates. mostly 2025-08-14 15:38:48 +02:00
guus e0a03f00bf responsive mobile-voting btns 2025-08-12 23:27:55 +02:00
guus 76b084b541 add mobile-voting 2025-08-12 15:47:05 +02:00
mark 21d5f56a39 sessions, or something 2025-08-09 16:28:49 +02:00
mark 4c70a3f72d merge fetch and fetcher 2025-08-09 10:46:11 +02:00
mark b20e0c3980 changes login message 2025-08-06 21:00:57 +02:00
mark 8448a865a7 disable nominate when quota 0 2025-08-06 20:57:07 +02:00
guus b45bcf03ab Merge pull request 'final checks' (#14) from mark into master
Reviewed-on: #14
2025-08-03 20:22:15 +02:00
mark 421a004667 small regex update 2025-08-03 20:21:13 +02:00
guus 370507d5a6 add form validation 2025-08-03 20:13:12 +02:00
mark 67d8dac8c0 quota context hook 2025-08-03 20:09:11 +02:00
mark 3eadd5c15d no more update quota 2025-08-03 20:07:49 +02:00
guus 5ea702e7bb fix message styling issue 2025-08-03 19:52:33 +02:00
mark 407abec1bf disables navbar for now 2025-08-03 19:42:36 +02:00
guus b03a442444 styling nom 2025-08-03 19:40:03 +02:00
mark 28ee97782c fix context error 2025-08-03 14:04:10 +02:00
mark 7ad74db834 stop quota increase 2025-08-03 13:58:05 +02:00
mark 5ee177e4cf nominate animation 2025-08-03 13:56:41 +02:00
mark 80eb434cde dont double lower quota 2025-08-03 12:39:19 +02:00
mark 9f7981de65 removes client secret 2025-08-03 12:36:54 +02:00
mark aff31e4c1d fixes hyperlink bug 2025-08-03 12:36:27 +02:00
60 changed files with 1674 additions and 436 deletions
+3
View File
@@ -8,3 +8,6 @@ wallpapers/
frontend/node_modules/
frontend/.next/
frontend/src/data/types.ts
backend/session_*.log
backend/*.csv
*.bak
+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;
+12 -2
View File
@@ -8,9 +8,19 @@ https://docs.djangoproject.com/en/4.2/howto/deployment/asgi/
"""
import os
from django.core.asgi import get_asgi_application
from channels.routing import ProtocolTypeRouter, URLRouter
from channels.auth import AuthMiddlewareStack
from muzak.auth import QueryAuthMiddleware
import voting.routing
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'muzak.settings')
application = get_asgi_application()
application = ProtocolTypeRouter({
"http": get_asgi_application(),
"websocket": QueryAuthMiddleware(
URLRouter(
voting.routing.websocket_urlpatterns
)
),
})
+46 -11
View File
@@ -1,7 +1,19 @@
from rest_framework.authentication import BaseAuthentication
from rest_framework.exceptions import AuthenticationFailed
from mozilla_django_oidc.auth import OIDCAuthenticationBackend
from django.contrib.auth.models import User
from django.contrib.auth.models import User, AnonymousUser
from channels.db import database_sync_to_async
def get_user_from_token(token, log=False):
if log:
print("verifying token:", token)
backend = OIDCAuthenticationBackend()
try:
claims = backend.verify_token(token)
except Exception as e:
raise AuthenticationFailed(f'Invalid token: {e}')
return backend.filter_users_by_claims(claims).first()
class OIDCBearerTokenAuthentication(BaseAuthentication):
def authenticate(self, request):
@@ -9,15 +21,38 @@ class OIDCBearerTokenAuthentication(BaseAuthentication):
if not auth.startswith('Bearer '):
return None
token = auth.split(' ')[1]
backend = OIDCAuthenticationBackend()
try:
claims = backend.verify_token(token)
except Exception as e:
raise AuthenticationFailed(f'Invalid token: {e}')
user = backend.filter_users_by_claims(claims).first()
user = get_user_from_token(token)
if not user:
raise AuthenticationFailed(f'Unknown user: {claims}')
raise AuthenticationFailed(f'Unknown user: {token}')
return (user, None)
# @database_sync_to_async
# def get_user(user_id):
# try:
# return User.objects.get(id=user_id)
# except User.DoesNotExist:
# return AnonymousUser()
class QueryAuthMiddleware:
"""
Custom middleware that takes user from passed token.
"""
def __init__(self, app):
# Store the ASGI application we were passed
self.app = app
async def __call__(self, scope, receive, send):
scope['user'] = AnonymousUser()
# print("Authing", scope)
# data = await receive()
# print("data: ", data)
# token = data.get('token')
# if token:
# user = database_sync_to_async(get_user_from_token(token, print=True))
# print("USER?", user)
# if user:
# scope['user'] = user
# else:
# pass
return await self.app(scope, receive, send)
+14 -1
View File
@@ -18,7 +18,9 @@ CORS_ALLOW_ALL_ORIGINS = True
CURRENT_HOST = 'http://127.0.0.1:8000' # Needs to be set for spotify callback to work.
INSTALLED_APPS = [
'daphne',
'playlist.apps.PlaylistConfig',
'voting.apps.VotingConfig',
'django.contrib.admin',
'django.contrib.auth',
'mozilla_django_oidc',
@@ -30,6 +32,7 @@ INSTALLED_APPS = [
'rest_framework',
'corsheaders',
'drf_spectacular',
'channels',
]
MIDDLEWARE = [
@@ -48,7 +51,7 @@ ROOT_URLCONF = 'muzak.urls'
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [],
'DIRS': [BASE_DIR / 'templates'],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
@@ -62,6 +65,16 @@ TEMPLATES = [
]
WSGI_APPLICATION = 'muzak.wsgi.application'
ASGI_APPLICATION = 'muzak.asgi.application'
CHANNEL_LAYERS = {
"default": {
"BACKEND": "channels_redis.core.RedisChannelLayer",
"CONFIG": {
"hosts": [("valkey", 6379)],
},
},
}
DATABASES = {
'default': {
+1
View File
@@ -20,5 +20,6 @@ from django.urls import path, include
urlpatterns = [
path('admin/', admin.site.urls),
path('oidc/', include('mozilla_django_oidc.urls')),
path('voting/', include('voting.urls')),
path('', include('playlist.urls')),
]
+4 -1
View File
@@ -1,8 +1,10 @@
from django.contrib import admin
from django.utils.http import urlencode
from django.shortcuts import redirect
from .models import Track, Artist, Album, Background, Vote, Profile
from .models import Track, Artist, Album, Background, Vote, Profile, Session
class SessionAdmin(admin.ModelAdmin):
pass
class TrackAdmin(admin.ModelAdmin):
pass
class ArtistAdmin(admin.ModelAdmin):
@@ -34,3 +36,4 @@ admin.site.register(Album, AlbumAdmin)
admin.site.register(Background, BackgroundAdmin)
admin.site.register(Vote, VoteAdmin)
admin.site.register(Profile, ProfileAdmin)
admin.site.register(Session, SessionAdmin)
@@ -0,0 +1,98 @@
# Generated by Django 5.2.4 on 2025-08-07 17:36
import django.core.validators
import django.db.models.deletion
import uuid
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("playlist", "0022_background_cover"),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.AlterField(
model_name="vote",
name="points",
field=models.IntegerField(
validators=[
django.core.validators.MinValueValidator(1),
django.core.validators.MaxValueValidator(5),
]
),
),
migrations.CreateModel(
name="Session",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("opens", models.DateTimeField(default=None, null=True)),
("session_id", models.UUIDField(default=uuid.uuid4, editable=False)),
("seed", models.IntegerField(default=892933)),
(
"status",
models.CharField(
choices=[
("lobby", "Lobby"),
("voting", "Voting"),
("waiting_for_next_track", "Waiting for Next Track"),
("paused", "Paused"),
("voting_done", "Voting Done"),
],
default="lobby",
max_length=30,
),
),
("voting_open", models.BooleanField(default=False)),
(
"current_track",
models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.SET_NULL,
to="playlist.track",
),
),
(
"host",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to=settings.AUTH_USER_MODEL,
),
),
(
"players_connected",
models.ManyToManyField(
related_name="sessions_joined", to=settings.AUTH_USER_MODEL
),
),
(
"players_wanted",
models.ManyToManyField(
related_name="players_wanted", to=settings.AUTH_USER_MODEL
),
),
],
),
migrations.AddField(
model_name="vote",
name="session",
field=models.ForeignKey(
default=None,
null=True,
on_delete=django.db.models.deletion.CASCADE,
to="playlist.session",
),
),
]
@@ -0,0 +1,18 @@
# Generated by Django 5.2.4 on 2025-08-09 06:41
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("playlist", "0023_alter_vote_points_session_vote_session"),
]
operations = [
migrations.AlterField(
model_name="session",
name="seed",
field=models.IntegerField(default=689663),
),
]
@@ -0,0 +1,27 @@
# Generated by Django 5.2.4 on 2025-08-09 08:42
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("playlist", "0024_alter_session_seed"),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.AlterField(
model_name="session",
name="players_connected",
field=models.ManyToManyField(
null=True, related_name="players_joined", to=settings.AUTH_USER_MODEL
),
),
migrations.AlterField(
model_name="session",
name="seed",
field=models.IntegerField(default=804599),
),
]
@@ -0,0 +1,27 @@
# Generated by Django 5.2.4 on 2025-08-09 08:42
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("playlist", "0025_alter_session_players_connected_alter_session_seed"),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.AlterField(
model_name="session",
name="players_connected",
field=models.ManyToManyField(
blank=True, related_name="players_joined", to=settings.AUTH_USER_MODEL
),
),
migrations.AlterField(
model_name="session",
name="seed",
field=models.IntegerField(default=456463),
),
]
@@ -0,0 +1,24 @@
# Generated by Django 5.2.4 on 2025-08-15 19:14
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("playlist", "0026_alter_session_players_connected_alter_session_seed"),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.AlterField(
model_name="session",
name="seed",
field=models.IntegerField(default=406213),
),
migrations.AlterUniqueTogether(
name="vote",
unique_together={("user", "track", "session")},
),
]
+1
View File
@@ -4,3 +4,4 @@ from .album import Album
from .vote import Vote
from .profile import Profile, Background
from .playlist import Playlist
from .session import Session
-1
View File
@@ -131,7 +131,6 @@ class Profile(models.Model):
return None
def save(self, *args, **kwargs):
self.update_quota()
super().save(*args, **kwargs)
@receiver(post_save, sender=User)
+33
View File
@@ -0,0 +1,33 @@
from django.db import models
from django.contrib.auth.models import User
import uuid
import random
from datetime import datetime
class Session(models.Model):
def __str__(self):
return f"Session {self.session_id} - Host: {self.host.username}"
@property
def is_open(self):
return (datetime.now() < self.opens) and self.voting_open
opens = models.DateTimeField(null=True, default=None)
session_id = models.UUIDField(default=uuid.uuid4, editable=False)
host = models.ForeignKey(User, on_delete=models.CASCADE)
players_wanted = models.ManyToManyField(User, related_name='players_wanted')
players_connected= models.ManyToManyField(User, related_name='players_joined', blank=True)
seed = models.IntegerField(default=random.randint(1, 1000000))
status = models.CharField(
max_length=30,
choices=[
('lobby', 'Lobby'),
('voting', 'Voting'),
('waiting_for_next_track', 'Waiting for Next Track'),
('paused', 'Paused'),
('voting_done', 'Voting Done'),
],
default='lobby'
)
current_track = models.ForeignKey('Track', on_delete=models.SET_NULL, null=True, blank=True)
voting_open = models.BooleanField(default=False)
+3 -1
View File
@@ -1,11 +1,13 @@
from django.db import models
from django.contrib.auth.models import User
from django.core.validators import MinValueValidator, MaxValueValidator
from .session import Session
class Vote(models.Model):
class Meta:
unique_together = ["user", "track"]
unique_together = ["user", "track", "session"]
track = models.ForeignKey('Track', on_delete=models.CASCADE)
user = models.ForeignKey(User, on_delete=models.CASCADE)
points = models.IntegerField(validators=[MinValueValidator(1), MaxValueValidator(5)])
skipped = models.BooleanField(default=False)
session = models.ForeignKey(Session, on_delete=models.CASCADE, null=True, default=None)
+1
View File
@@ -4,3 +4,4 @@ from .user import *
from .album import *
from .profile import *
from .vote import *
from .session import *
+9
View File
@@ -0,0 +1,9 @@
from rest_framework import serializers
from playlist.models import Session
class SessionSerializer(serializers.ModelSerializer):
host = serializers.EmailField(source='host.email', read_only=True)
class Meta:
model = Session
fields = '__all__'
+1
View File
@@ -19,6 +19,7 @@ urlpatterns = [
path('api/vote/', views.VoteListView.as_view(), name="api-votes"),
path('api/vote/<id>', views.VoteDetailView.as_view(), name="api-vote"),
path('api/user/', views.UserListView.as_view(), name="api-users"),
path('api/user/<int:id>', views.UserDetailView.as_view(), name="api-user"),
path('api/profile/me', views.ProfileMeView.as_view(), name="api-profile-me"),
### Classic
-2
View File
@@ -29,8 +29,6 @@ class TrackListView(APIView):
if not can_nom:
return Response({'error': reason}, status=403)
(track, message) = Track.objects.create_from_spotify(request.data['spotify_link'], profile)
profile.quota -= 1
profile.save()
serializer = TrackSerializer(track, context={'request': request})
return Response(serializer.data)
+1 -1
View File
@@ -62,7 +62,7 @@ class VoteView(LoginRequiredMixin, View):
class NominateView(LoginRequiredMixin, View):
def get(self, request):
(profile,_) = Profile.objects.get_or_create(user=request.user)
profile.update_quota()
#profile.update_quota()
profile.save()
return TemplateResponse(request, "Nominate.html", {'end_time': os.getenv('DATE_NOM_END')})
def post(self, request):
+3
View File
@@ -11,3 +11,6 @@ markdown
PyYAML
Pygments
drf-spectacular
channels
daphne
channels-redis
View File
+5
View File
@@ -0,0 +1,5 @@
from django.apps import AppConfig
class VotingConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'voting'
+133
View File
@@ -0,0 +1,133 @@
import json
from asgiref.sync import async_to_sync
from channels.generic.websocket import WebsocketConsumer
from django.contrib.auth.models import User
from muzak.auth import get_user_from_token
from channels.db import database_sync_to_async
from playlist.models import Session, Track, Vote
import random
import logging
class SessionConsumer(WebsocketConsumer):
def connect(self):
self.session_id = self.scope["url_route"]["kwargs"]["session_id"]
self.session_name = f"session_{self.session_id}"
self.logger = logging.getLogger(self.session_name)
logging.basicConfig(filename=self.session_name+'.log', level=logging.INFO, format='%(asctime)s %(levelname)s %(message)s', filemode='w')
async_to_sync(self.channel_layer.group_add)(
self.session_name, self.channel_name
)
self.accept()
def disconnect(self, close_code):
# Leave room group
async_to_sync(self.channel_layer.group_discard)(
self.session_name, self.channel_name
)
# Receive message from WebSocket
def receive(self, text_data):
user = self.scope["user"] # Waarschijnlijk altijd AnonymousUser
#self.logger.info(self.scope)
sessionId = self.scope["url_route"].get("kwargs", {}).get("session_id", None)
text_data_json = json.loads(text_data)
# Niet lachen, middleware is moeilijk oké?
token = text_data_json.get("token")
if token:
user = get_user_from_token(token, log=False)
session = Session.objects.get(session_id=sessionId)
action = text_data_json.get("action", None)
if action == "message":
message = text_data_json["message"]
# Send message to room group
async_to_sync(self.channel_layer.group_send)(
self.session_name, {"type": "chat.message", "message": str(user) + ": " + message}
)
if action == "login":
self.logger.info(f"Announced: {user}")
async_to_sync(self.channel_layer.group_send)(
self.session_name, {"type": "announce", "user": { "id": user.id, "username": user.username}}
)
if action == "vote":
points = text_data_json.get("points", None)
track = session.current_track;
vote = None
try:
vote = Vote.objects.get(user=user, track=track, session=session)
vote.points = points
self.logger.info(f"Updated vote for {track} by {user} to {points} points")
vote.save()
except Vote.DoesNotExist:
vote = Vote.objects.create(user=user, track=track, points=points, session=session)
self.logger.info(f"Created vote for {track} by {user} for {points} points")
vote.save()
except Exception as e:
self.logger.error(f"Error voting on {track} by {user} for {points} points: {e}")
if vote:
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}
)
if action == "start":
async_to_sync(self.channel_layer.group_send)(
self.session_name, {"type": "announce.start"}
)
if action == "next":
random.seed(session.seed)
tracks = Track.objects.order_by("?")
next = False
if session.current_track:
for track in tracks:
if next:
session.current_track = track
next = False
break
if session.current_track == track:
next = True
if next:
# We didnt get a next track, so it must have been the last one
session.current_track = None
else:
session.current_track = tracks.first()
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)(
self.session_name, {"type": "announce.track"}
)
def announce(self, event):
user = event["user"]
self.send(text_data=json.dumps({"joined": user}))
# TODO: maybe this could be one function.
def announce_track(self, event):
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):
user = event["user"]
#self.session_name, {"type": "voted", "user": { "id": user.id, "username": user.username}, "track": track.id, "points": points}
self.send(text_data=json.dumps({"voted": { "id": user["id"], "username": user["username"], "points": event["points"], "track": event["track"]}}))
# Receive message from room group
def chat_message(self, event):
message = event["message"]
self.logger.info(self.scope["user"])
# Send message to WebSocket
self.send(text_data=json.dumps({"message": message}))
+7
View File
@@ -0,0 +1,7 @@
from django.urls import re_path
from . import consumers
websocket_urlpatterns = [
re_path(r"ws/voting/session/(?P<session_id>[\w\-]+)/$", consumers.SessionConsumer.as_asgi()),
]
@@ -0,0 +1,25 @@
<!doctype html>
<html>
<head>
<meta charset="utf-8" />
<title>Sessions</title>
</head>
<body>
<input id="room-name-input" type="text" size="100" /><br />
<input id="room-name-submit" type="button" value="Enter" />
<script>
document.querySelector("#room-name-input").focus();
document.querySelector("#room-name-input").onkeyup = function (e) {
if (e.key === "Enter") {
// enter, return
document.querySelector("#room-name-submit").click();
}
};
document.querySelector("#room-name-submit").onclick = function (e) {
var roomName = document.querySelector("#room-name-input").value;
window.location.pathname =
"/voting/session/" + roomName + "/test";
};
</script>
</body>
</html>
@@ -0,0 +1,63 @@
<!doctype html>
<html>
<head>
<meta charset="utf-8" />
<title>Votign Session</title>
</head>
<body>
<textarea id="chat-log" cols="100" rows="20"></textarea><br />
<input id="chat-message-input" type="text" size="100" /><br />
<input id="chat-message-submit" type="button" value="Send" />
{{ room_name|json_script:"room-name" }}
<script>
const roomName = JSON.parse(
document.getElementById("room-name").textContent,
);
const host =
"ws://" +
window.location.host +
"/voting/session/" +
roomName +
"/";
const chatSocket = new WebSocket(host);
console.log(host);
chatSocket.onmessage = function (e) {
const data = JSON.parse(e.data);
document.querySelector("#chat-log").value +=
data.message + "\n";
};
chatSocket.onclose = function (e) {
console.error("Chat socket closed unexpectedly");
};
document.querySelector("#chat-message-input").focus();
document.querySelector("#chat-message-input").onkeyup = function (
e,
) {
if (e.key === "Enter") {
// enter, return
document.querySelector("#chat-message-submit").click();
}
};
document.querySelector("#chat-message-submit").onclick = function (
e,
) {
const messageInputDom = document.querySelector(
"#chat-message-input",
);
const message = messageInputDom.value;
chatSocket.send(
JSON.stringify({
action: "message",
message: message,
}),
);
messageInputDom.value = "";
};
</script>
</body>
</html>
+11
View File
@@ -0,0 +1,11 @@
from django.urls import path
from . import views
urlpatterns = [
path("", views.index, name="index"),
path("session/<str:session_id>/test", views.session, name="session"),
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>/info", views.SessionInfoView.as_view(), name="api-session-info"),
]
+42
View File
@@ -0,0 +1,42 @@
from django.shortcuts import render
from django.db.models import Avg
from rest_framework.views import APIView
from rest_framework.response import Response
from playlist.models import Session, Profile, Track, Vote
from playlist.serializers import TrackSerializer, SessionSerializer
from drf_spectacular.utils import extend_schema
def index(request):
return render(request, 'voting/index.html')
def session(request, session_id):
return render(request, 'voting/session.html', {'room_name': session_id})
@extend_schema(
responses={200: SessionSerializer(many=True)},
description="Retrieve a list of all available sessions."
)
class SessionListView(APIView):
def get(self, request):
if request.user.is_superuser:
sessions = Session.objects.all()
else:
sessions = Session.objects.filter(voting_open=True)
return Response(SessionSerializer(sessions, many=True).data)
@extend_schema(
responses={200: SessionSerializer()},
description="Gets session details."
)
class SessionDetailView(APIView):
def get(self, request, session_id):
session = Session.objects.get(session_id=session_id)
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})
+6
View File
@@ -30,6 +30,12 @@ services:
- "3003:3000"
networks:
- app_network
valkey:
image: valkey/valkey
ports:
- "6379:6379"
networks:
- app_network
networks:
app_network:
Binary file not shown.

After

Width:  |  Height:  |  Size: 787 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 108 KiB

+54 -26
View File
@@ -7,31 +7,60 @@ import { Source_Sans_3, Nunito } from "next/font/google";
import AuthStatus from "muzak/components/AuthStatus";
import NavigationBar from "muzak/components/NavigationBar";
import Quota from "muzak/components/Quota";
const sourceSans = Source_Sans_3({
weight: ["400"],
subsets: ["latin"],
});
import SessionChecker from "muzak/components/SessionChecker";
import { AnimationContext } from "muzak/contexts/AnimationContext";
import { QuotaContext } from "muzak/contexts/QuotaContext";
import { SessionContext } from "muzak/contexts/SessionContext";
import { usePathname } from "next/navigation";
const nunito = Nunito({
subsets: ["latin"],
});
function LayoutContent({
children,
quota,
setQuota,
}: {
children: React.ReactNode;
quota: number | null;
setQuota: React.Dispatch<React.SetStateAction<number | null>>;
}) {
const navToggle = true; //voor dev navigatie
const pageConfig: Record<
string,
{
showAuthStatus: boolean;
showNav: boolean;
showQuota: boolean;
}
> = {
"/nominations": {
showAuthStatus: true,
showQuota: true,
showNav: navToggle,
},
"/mobile-voting": {
showAuthStatus: false,
showQuota: false,
showNav: false,
},
"/lobby": {
showAuthStatus: true,
showQuota: true,
showNav: navToggle,
},
};
function LayoutContent({ children }: { children: React.ReactNode }) {
const pathname = usePathname();
const config = pageConfig[pathname] ?? {
showAuthStatus: true,
showSessionChecker: true,
showQuota: false,
showNav: navToggle,
};
return (
<>
<AuthStatus />
<SessionChecker />
{config.showAuthStatus && <AuthStatus />}
{children}
<NavigationBar />
<Quota quota={quota} setQuota={setQuota} />
{config.showNav && <NavigationBar />}
{config.showQuota && <Quota />}
</>
);
}
@@ -43,6 +72,7 @@ export default function RootLayout({
}) {
const [isAnimate, setIsAnimate] = useState(false);
const [quota, setQuota] = useState<number | null>(null);
const [sessionId, setSessionId] = useState<string | null>(null);
const toggleAnimation = () => {
setIsAnimate(!isAnimate);
@@ -52,16 +82,14 @@ export default function RootLayout({
<html lang="en" className={nunito.className}>
<body className={`bg-game-show ${isAnimate ? "animate" : ""} h-screen`}>
<AuthProvider {...oidcConfig}>
<LayoutContent
children={children}
quota={quota}
setQuota={setQuota}
/>
<AnimationContext.Provider value={{ isAnimate, setIsAnimate }}>
<QuotaContext.Provider value={{ quota, setQuota }}>
<SessionContext.Provider value={{ sessionId, setSessionId }}>
<LayoutContent children={children} />
</SessionContext.Provider>
</QuotaContext.Provider>
</AnimationContext.Provider>
</AuthProvider>
<button
className={`absolute top-1 left-1 ${isAnimate ? "pause" : "play"}`}
onClick={toggleAnimation}
></button>
</body>
</html>
);
+499 -92
View File
@@ -1,9 +1,15 @@
"use client";
import { useState, useEffect } from "react";
import { Markazi_Text, Nunito } from "next/font/google";
import users from "muzak/data/users.json";
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";
import { useAnimation } from "muzak/contexts/AnimationContext";
const nunito = Nunito({
weight: "900",
@@ -13,104 +19,505 @@ const nunito = Nunito({
const MAX_PARTY_SIZE = 8;
export default function Lobby() {
const [partySize, setPartySize] = useState(0);
const [userList, setUserList] = useState([]);
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
const { isAnimate, setIsAnimate } = useAnimation();
// 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") {
setIsAnimate(false);
setTimeout(() => setIsAnimate(true), 1000);
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(() => {
populateUserList();
}, [partySize]);
console.log("useEffect state:", state);
if (state == "waiting") {
setRevealed(true);
setTimeout(() => {
setState("next");
}, 12000);
} else if (state == "next") {
nextTrack();
}
}, [state]);
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 addUserToParty() {
setPartySize((prev) => Math.min(prev + 1, MAX_PARTY_SIZE));
}
function deleteUserFromParty() {
setPartySize((prev) => Math.max(prev - 1, 0));
}
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`}
>
LOBBY
</h1>
</div>
<div className="flex min-h-[532px] min-w-[500px] 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: {partySize}
</div>
<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"
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`}
>
START
</Link>
LOBBY
</h1>
</div>
{partySize === 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 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>
) : (
<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-32 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>
<div className="mt-4 flex items-center justify-between gap-4">
<button
className="rounded-md bg-blue-500 px-4 py-2 text-white hover:bg-blue-600"
onClick={() => deleteUserFromParty()}
{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"} `}
>
del
</button>
<button
className="rounded-md bg-blue-500 px-4 py-2 text-white hover:bg-blue-600"
onClick={() => addUserToParty()}
>
add
</button>
</div>
</main>
);
{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>
);
}
}
-48
View File
@@ -1,48 +0,0 @@
"use client";
import { Nunito } from "next/font/google";
import { useState } from "react";
import Link from "next/link";
const nunito = Nunito({
weight: "900",
subsets: ["latin"],
});
export default function Login() {
const [userName, setUserName] = useState("Stroop");
const [pin, setPin] = useState("");
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`}
>
LOGIN
</h1>
</div>
<div className="mb-12 flex flex-col items-center justify-between gap-4 rounded-md bg-blue-950/80 p-8">
<p className="text-2xl font-semibold text-white drop-shadow-sm drop-shadow-gray-900">
Welkom, {userName}!
</p>
<form className="flex flex-col items-center justify-center gap-4">
<input
className="w-full rounded border border-white px-4 py-2 font-bold text-white/70 focus:border-yellow-500 focus:outline-2 focus:outline-yellow-500"
type="text"
placeholder="Game PIN"
value={pin}
onChange={(e) => setPin(e.target.value.toUpperCase())}
/>
<Link
href="/lobby"
className="w-full rounded bg-blue-500 px-4 py-2 text-center font-bold text-white hover:bg-blue-400"
>
Join
</Link>
</form>
</div>
<div></div>
</main>
);
}
+181
View File
@@ -0,0 +1,181 @@
"use client";
import { Nunito } from "next/font/google";
import { useState, useEffect } from "react";
import { useRouter } from "next/navigation";
import { useAuth } from "react-oidc-context";
import { getUser } from "muzak/data/fetcher";
import Image from "next/image";
import getSessionId from "muzak/data/session";
import { useSession } from "muzak/contexts/SessionContext";
import Router from "next/router";
const nunito = Nunito({
weight: "900",
subsets: ["latin"],
});
export default function MobileVoting() {
const [vote, setVote] = useState(0.5);
const [noHighlight, setNoHighlight] = useState(false);
const [yesHighlight, setYesHighlight] = useState(false);
const auth = useAuth();
const router = useRouter();
const { sessionId, setSessionId } = useSession();
const [sessionSocket, setSessionSocket] = useState<WebSocket | null>(null);
async function waitForOpenSocket() {
await new Promise<void>((resolve) => {
const interval = setInterval(() => {
console.log("Checking socket state...");
if (sessionSocket?.readyState === WebSocket.OPEN) {
clearInterval(interval);
resolve();
}
}, 1000);
});
}
const user = getUser();
async function connect() {
console.log(`ik ben ${user.profile.nickname}`);
console.log(sessionSocket);
console.log(sessionSocket?.readyState);
await waitForOpenSocket();
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(
JSON.stringify({
action: "login",
token: user.access_token,
}),
);
}
// Main message handler and dispatcher
function onMessage(e) {
console.log(e);
let data = JSON.parse(e.data);
for (let key in data) {
if (key == "track" || key == "start") {
setYesHighlight(false);
setNoHighlight(false);
setVote(0.5);
}
}
console.log(data);
}
// Initialize WebSocket connection when sessionId changes
useEffect(() => {
if (sessionId) {
console.log("Socket session ID:", sessionId);
const socket = new WebSocket(
`${process.env.NEXT_PUBLIC_WS_HOST}/voting/session/${sessionId}/`,
);
socket.onmessage = onMessage;
setSessionSocket(socket);
}
}, [sessionId]);
useEffect(() => {
if (sessionSocket) {
connect();
}
}, [sessionSocket]);
async function setSession() {
const id = await getSessionId();
setSessionId(id);
console.log("Session ID:", id);
}
useEffect(() => {
if (!auth.isLoading && !auth.error && auth.isAuthenticated) {
setSession();
} else if (!auth.isLoading && !auth.isAuthenticated) {
router.push("/");
}
}, [auth]);
useEffect(() => {
if (vote != 0.5 && sessionSocket?.readyState == WebSocket.OPEN) {
sessionSocket.send(
JSON.stringify({
action: "vote",
token: user.access_token,
points: vote,
}),
);
}
}, [vote]);
return (
<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">
<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"}`}
onClick={() => {
setYesHighlight(false);
setNoHighlight(true);
setVote(0.0);
}}
>
{noHighlight ? (
<Image
src="/no-cat.webp"
alt="icon"
className="h-full w-full rounded object-contain"
width={200}
height={200}
/>
) : (
<Image
src="/no-cat-static.gif"
alt="icon"
className="h-full w-full rounded object-contain grayscale filter"
width={200}
height={200}
/>
)}
</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 ${yesHighlight ? "ring-5 ring-amber-400" : "ring-0 ring-black/20"}`}
onClick={() => {
setYesHighlight(true);
setNoHighlight(false);
setVote(1.0);
}}
>
{yesHighlight ? (
<Image
src="/yes-cat.webp"
alt="icon"
className="h-full w-full rounded object-contain"
width={200}
height={200}
/>
) : (
<Image
src="/yes-cat-static2.gif"
alt="icon"
className="h-full w-full rounded object-contain grayscale filter"
width={200}
height={200}
/>
)}
</button>
</div>
</main>
);
}
+39 -18
View File
@@ -1,31 +1,49 @@
"use client";
import { useState } from "react";
import { useState, useEffect } from "react";
import { Nunito } from "next/font/google";
import { api } from "muzak/data/fetcher";
import { useAnimation } from "muzak/contexts/AnimationContext";
import { useQuota } from "muzak/contexts/QuotaContext";
const nunito = Nunito({
weight: "900",
subsets: ["latin"],
});
async function nominate(spotifyLink: string) {
console.log(spotifyLink);
try {
const response = await api.tracks.nominate({
spotify_link: spotifyLink,
});
console.log("Nomination successful:", response);
return { success: true, data: response.data };
} catch (error) {
console.error("Nomination failed:", error);
return { success: false, error };
}
}
export default function Nominations() {
const { isAnimate, setIsAnimate } = useAnimation();
const [songURL, setSongURL] = useState("");
const [isLoading, setIsLoading] = useState(false);
const [message, setMessage] = useState("");
const [canNominate, setCanNominate] = useState(true);
const isSpotifyURL =
/^https:\/\/open\.spotify\.com\/track\/[a-zA-Z0-9]{22}(\?.*)?$/.test(
songURL,
);
const { quota, setQuota } = useQuota();
async function nominate(spotifyLink: string) {
try {
setIsAnimate(false);
const response = await api.tracks.nominate({
spotify_link: spotifyLink,
});
console.log("Nomination successful:", response);
setIsAnimate(true);
setQuota(quota - 1);
return { success: true, data: response.data };
} catch (error) {
console.error("Nomination failed:", error);
return { success: false, error };
}
}
useEffect(() => {
if (quota > 0) {
setCanNominate(true);
} else {
setCanNominate(false);
}
}, [quota]);
return (
<main className="flex min-h-screen flex-col items-center justify-between p-24">
@@ -34,7 +52,7 @@ export default function Nominations() {
>
NOMINATIES
</h1>
<div className="mt-4 mb-12 flex flex-col items-center justify-between gap-4 rounded-md bg-blue-950/80 p-8">
<div className="relative mt-4 mb-12 flex flex-col items-center justify-between gap-4 rounded-md bg-blue-950/80 p-8">
<form className="flex flex-col items-center justify-center gap-4">
<p className="text-2xl font-semibold text-white drop-shadow-sm drop-shadow-gray-900">
Dump hier je Spotify-link:
@@ -52,7 +70,9 @@ export default function Nominations() {
<button
className="w-full rounded bg-blue-500 px-4 py-2 text-center font-bold text-white hover:bg-blue-400 disabled:cursor-not-allowed disabled:bg-gray-500"
type="submit"
disabled={isLoading || !songURL.trim()}
disabled={
isLoading || !songURL.trim() || !isSpotifyURL || !canNominate
}
onClick={async (e) => {
e.preventDefault();
if (!songURL.trim()) return;
@@ -76,11 +96,12 @@ export default function Nominations() {
</button>
</form>
{message && (
<div className="mt-4 rounded bg-gray-800 p-3 text-center text-white">
<div className="absolute top-50 mt-4 rounded bg-gray-800 p-3 text-center text-white">
{message}
</div>
)}
</div>
<footer>
<p>© 2020-2025 Hoestende Draken PartyCo</p>
</footer>
+1 -2
View File
@@ -14,7 +14,6 @@ const nunito = Nunito({
});
export default function Page() {
const [userName, setUserName] = useState("Stroop");
const auth = useAuth();
console.log(auth);
@@ -37,7 +36,7 @@ export default function Page() {
{!auth.isAuthenticated && (
<div className="mb-15 flex flex-col items-center justify-between gap-4 rounded-md bg-blue-950/80 p-8">
<p className="text-2xl font-semibold text-white drop-shadow-sm drop-shadow-gray-900">
Welkom, {userName}!
Welkom bij de HDcon Muzak
</p>
<button
-34
View File
@@ -1,34 +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>
<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"
>
START
</Link>
<h1
className={`${nunito.className} text-outline text-[42px] tracking-tighter text-yellow-500`}
>
PAUZE
</h1>
</main>
);
}
+54 -1
View File
@@ -1,7 +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() {
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 (
<main className="flex min-h-screen flex-col items-center justify-between p-24">
<h1>Post-game</h1>
<p
className={`${nunito.className} text-outline text-[42px] tracking-tighter text-yellow-500`}
>
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>
);
}
+6 -1
View File
@@ -15,7 +15,7 @@
}
.bg-game-show.animate {
animation: rotate_bg 5s cubic-bezier(0.55, 0.45, 0.45, 0.55) infinite;
animation: rotate_bg 1.5s ease-in-out;
}
@keyframes rotate_bg {
@@ -41,3 +41,8 @@
.text-outline-small {
-webkit-text-stroke: 1px black;
}
iframe {
position: absolute;
top: -1000px;
}
-1
View File
@@ -1,5 +1,4 @@
"use client";
import { getUser } from "muzak/data/fetch";
import { useEffect, useState } from "react";
import { api, User } from "muzak/data/fetcher";
-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>
);
}
+21 -4
View File
@@ -5,6 +5,8 @@ const source = Source_Sans_3({
subsets: ["latin"],
});
const font = source.className;
const btnStyle =
"rounded-md px-4 py-2 text-sm font-medium text-white bg-green-600 hover:bg-green-700";
export default function NavigationBar() {
const auth = useAuth();
@@ -12,11 +14,26 @@ export default function NavigationBar() {
return (
<nav className="absolute right-0 bottom-0 left-0 flex justify-center">
<div
className={`${font} flex flex-row items-center gap-6 rounded-tl-lg rounded-tr-lg border-4 border-b-0 border-black bg-sky-900 px-4 py-6 text-white`}
className={`${font} flex flex-row items-center gap-6 rounded-tl-lg rounded-tr-lg border-2 border-b-0 border-black bg-sky-900 p-4 text-white`}
>
<Link href="/nominations">Nomineren</Link>
<Link href="/lobby">Start spel (test)</Link>
<Link href="/users">Users</Link>
<Link className={btnStyle} href="/nominations">
Nomineren
</Link>
<Link className={btnStyle} href="/lobby">
Start spel (test)
</Link>
<Link className={btnStyle} href="/session">
Session Connect
</Link>
<Link className={btnStyle} href="/mobile-voting">
Mobile Voting
</Link>
<Link className={btnStyle} href="/post-game">
Post Game
</Link>
<Link className={btnStyle} href="/users">
Users
</Link>
</div>
</nav>
);
+4 -8
View File
@@ -3,18 +3,14 @@ import { useAuth } from "react-oidc-context";
import { useEffect, useState } from "react";
import { Nunito } from "next/font/google";
import { api, Profile } from "muzak/data/fetcher";
import { useQuota } from "muzak/contexts/QuotaContext";
const nunito = Nunito({
subsets: ["latin"],
});
export default function Quota({
quota,
setQuota,
}: {
quota: number | null;
setQuota: React.Dispatch<React.SetStateAction<number | null>>;
}) {
export default function Quota() {
const auth = useAuth();
const { quota, setQuota } = useQuota();
async function getProfile() {
if (!auth.isLoading && !auth.error && auth.isAuthenticated) {
const { data: profile } = await api?.profiles?.me({});
@@ -28,7 +24,7 @@ export default function Quota({
if (!auth.isLoading && !auth.error && auth.isAuthenticated) {
return (
<h1
className={`${nunito.className} text-outline absolute right-0 bottom-0 p-6 text-[96px] font-bold tracking-tighter text-yellow-500`}
className={`${nunito.className} text-outline absolute right-0 bottom-0 p-6 text-[42px] font-bold tracking-tighter text-yellow-500`}
>
{quota}
</h1>
@@ -0,0 +1,38 @@
"use client";
import Link from "next/link";
import { useAuth } from "react-oidc-context";
import { useState, useEffect, use } from "react";
import { useRouter, usePathname } from "next/navigation";
import { api, Session } from "muzak/data/fetcher";
import { getUser } from "muzak/data/fetcher";
import { useSession } from "muzak/contexts/SessionContext";
import getSessionId from "muzak/data/session";
export default function SessionChecker() {
const auth = useAuth();
const router = useRouter();
const path = usePathname();
const { sessionId, setSessionId } = useSession();
async function getSessions() {
if (!auth.isLoading && !auth.error && auth.isAuthenticated) {
const { data: sessions } = await api?.sessions?.list({});
console.log(sessions);
if (sessions?.length > 0) {
const user = getUser();
setSessionId(sessions[0].session_id);
if (sessions[0].host !== user?.profile?.email) {
if (path !== "/mobile-voting") {
router.push("/mobile-voting");
}
}
}
}
}
useEffect(() => {
getSessions();
}, [auth, path]);
return <> </>;
}
+8 -2
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";
const ORIGIN_URI = globalThis?.window?.location.origin;
@@ -7,7 +7,6 @@ const REDIRECT_URI = `${ORIGIN_URI}/auth/callback`;
const userConfig: AuthProviderProps = {
authority: process.env.NEXT_PUBLIC_AUTHORITY!,
client_id: process.env.NEXT_PUBLIC_CLIENT_ID!,
client_secret: process.env.NEXT_PUBLIC_CLIENT_SECRET!,
redirect_uri: REDIRECT_URI,
response_type: "code",
//includeIdTokenInSilentRenew: true,
@@ -18,8 +17,15 @@ const userConfig: AuthProviderProps = {
post_logout_redirect_uri: ORIGIN_URI,
response_mode: "query",
revokeTokensOnSignout: true,
staleStateAgeInSeconds: 60000,
};
if (globalThis?.window != undefined) {
userConfig.userStore = new WebStorageStateStore({
store: window.localStorage,
});
}
export const userManager = new UserManager(userConfig);
// Some handling of token expiration.
@@ -0,0 +1,21 @@
"use client";
import { createContext, useContext } from "react";
interface AnimationContextType {
isAnimate: boolean;
setIsAnimate: React.Dispatch<React.SetStateAction<boolean>>;
}
const AnimationContext = createContext<AnimationContextType | undefined>(
undefined,
);
export const useAnimation = () => {
const context = useContext(AnimationContext);
if (context === undefined) {
throw new Error("useAnimation must be used within an AnimationProvider");
}
return context;
};
export { AnimationContext };
+12
View File
@@ -0,0 +1,12 @@
"use client";
import { createContext, useContext } from "react";
interface QuotaContextType {
quota: number;
setQuota: React.Dispatch<React.SetStateAction<number>>;
}
const QuotaContext = createContext<QuotaContextType>({} as QuotaContextType);
export const useQuota = () => useContext(QuotaContext);
export { QuotaContext };
+14
View File
@@ -0,0 +1,14 @@
"use client";
import { createContext, useContext } from "react";
interface SessionContextType {
sessionId: string;
setSessionId: React.Dispatch<React.SetStateAction<string>>;
}
const SessionContext = createContext<SessionContextType>(
{} as SessionContextType,
);
export const useSession = () => useContext(SessionContext);
export { SessionContext };
+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;
};
-13
View File
@@ -1,13 +0,0 @@
"use client";
import { oidcConfig } from "muzak/config/auth";
import { User } from "oidc-client-ts";
export function getUser() {
let oidcStorage = window?.sessionStorage.getItem(
`oidc.user:${oidcConfig.authority}:${oidcConfig.client_id}`,
);
if (!oidcStorage) {
return null;
}
return User.fromStorageString(oidcStorage);
}
+28 -1
View File
@@ -1,7 +1,25 @@
"use client";
import { Fetcher } from "openapi-typescript-fetch";
import { getUser } from "./fetch";
import { paths, components } from "muzak/data/types";
import { oidcConfig } from "muzak/config/auth";
import { User as OidcUser } from "oidc-client-ts";
export function getUser() {
if (typeof window !== "undefined") {
let oidcStorage = window?.sessionStorage.getItem(
`oidc.user:${oidcConfig.authority}:${oidcConfig.client_id}`,
);
if (!oidcStorage) {
oidcStorage = window?.localStorage.getItem(
`oidc.user:${oidcConfig.authority}:${oidcConfig.client_id}`,
);
}
if (!oidcStorage) {
return null;
}
return OidcUser.fromStorageString(oidcStorage);
}
}
const fetcher = Fetcher.for<paths>();
const BASE_URL = process.env.NEXT_PUBLIC_BACKEND_HOST!;
@@ -72,11 +90,20 @@ export const api = {
profiles: {
me: fetcher.path("/api/profile/me").method("get").create(),
},
sessions: {
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(),
},
};
export type User = components["schemas"]["User"];
export type Track = components["schemas"]["Track"];
export type Album = components["schemas"]["Album"];
export type Profile = components["schemas"]["Profile"];
export type Session = components["schemas"]["Session"];
export default api;
+16
View File
@@ -0,0 +1,16 @@
"use client";
import { useAuth } from "react-oidc-context";
import { api, getUser, Session } from "muzak/data/fetcher";
import { useSession } from "muzak/contexts/SessionContext";
export default async function getSessionId() {
//const auth = useAuth();
//const { sessionId, setSessionId } = useSession();
//if (!auth.isLoading && !auth.error && auth.isAuthenticated) {
const { data: sessions } = await api?.sessions?.list({});
if (sessions?.length > 0) {
return sessions[0].session_id;
}
//}
}
+2 -1
View File
@@ -9,7 +9,8 @@
"muzak/config/*": ["./src/config/*"],
"muzak/data/*": ["./src/data/*"],
"muzak/app/*": ["./src/app/*"],
"muzak/components/*": ["./src/components/*"]
"muzak/components/*": ["./src/components/*"],
"muzak/contexts/*": ["./src/contexts/*"]
},
"allowJs": false,