From 21d5f56a39794f6e2c5e60e03acde5a96e809b48 Mon Sep 17 00:00:00 2001 From: Mark Hoekveen Date: Sat, 9 Aug 2025 16:28:49 +0200 Subject: [PATCH] sessions, or something --- backend/muzak/asgi.py | 14 ++- backend/muzak/auth.py | 57 ++++++++--- backend/muzak/settings.py | 15 ++- backend/muzak/urls.py | 1 + backend/playlist/admin.py | 5 +- ..._alter_vote_points_session_vote_session.py | 98 +++++++++++++++++++ .../migrations/0024_alter_session_seed.py | 18 ++++ ...on_players_connected_alter_session_seed.py | 27 +++++ ...on_players_connected_alter_session_seed.py | 27 +++++ backend/playlist/models/__init__.py | 1 + backend/playlist/models/session.py | 33 +++++++ backend/playlist/models/vote.py | 2 + backend/playlist/serializers/__init__.py | 1 + backend/playlist/serializers/session.py | 9 ++ backend/requirements.txt | 3 + backend/voting/__init__.py | 0 backend/voting/apps.py | 5 + backend/voting/consumers.py | 71 ++++++++++++++ backend/voting/routing.py | 7 ++ backend/voting/templates/voting/index.html | 25 +++++ backend/voting/templates/voting/session.html | 63 ++++++++++++ backend/voting/urls.py | 9 ++ backend/voting/views.py | 24 +++++ docker-compose.yml | 6 ++ frontend/src/app/layout.tsx | 8 +- frontend/src/app/lobby/page.tsx | 60 +++++++----- frontend/src/app/session/page.tsx | 62 ++++++++++++ frontend/src/components/SessionChecker.tsx | 52 ++++++++++ frontend/src/contexts/SessionContext.tsx | 14 +++ frontend/src/data/fetcher.ts | 4 + 30 files changed, 683 insertions(+), 38 deletions(-) create mode 100644 backend/playlist/migrations/0023_alter_vote_points_session_vote_session.py create mode 100644 backend/playlist/migrations/0024_alter_session_seed.py create mode 100644 backend/playlist/migrations/0025_alter_session_players_connected_alter_session_seed.py create mode 100644 backend/playlist/migrations/0026_alter_session_players_connected_alter_session_seed.py create mode 100644 backend/playlist/models/session.py create mode 100644 backend/playlist/serializers/session.py create mode 100644 backend/voting/__init__.py create mode 100644 backend/voting/apps.py create mode 100644 backend/voting/consumers.py create mode 100644 backend/voting/routing.py create mode 100644 backend/voting/templates/voting/index.html create mode 100644 backend/voting/templates/voting/session.html create mode 100644 backend/voting/urls.py create mode 100644 backend/voting/views.py create mode 100644 frontend/src/app/session/page.tsx create mode 100644 frontend/src/components/SessionChecker.tsx create mode 100644 frontend/src/contexts/SessionContext.tsx diff --git a/backend/muzak/asgi.py b/backend/muzak/asgi.py index daa6808..15696b8 100644 --- a/backend/muzak/asgi.py +++ b/backend/muzak/asgi.py @@ -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 + ) + ), +}) diff --git a/backend/muzak/auth.py b/backend/muzak/auth.py index 6a775f8..886539f 100644 --- a/backend/muzak/auth.py +++ b/backend/muzak/auth.py @@ -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) diff --git a/backend/muzak/settings.py b/backend/muzak/settings.py index 8470425..4f8fff0 100644 --- a/backend/muzak/settings.py +++ b/backend/muzak/settings.py @@ -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': { diff --git a/backend/muzak/urls.py b/backend/muzak/urls.py index 925251a..9f90588 100644 --- a/backend/muzak/urls.py +++ b/backend/muzak/urls.py @@ -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')), ] diff --git a/backend/playlist/admin.py b/backend/playlist/admin.py index b832f3d..e3ad8de 100644 --- a/backend/playlist/admin.py +++ b/backend/playlist/admin.py @@ -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) diff --git a/backend/playlist/migrations/0023_alter_vote_points_session_vote_session.py b/backend/playlist/migrations/0023_alter_vote_points_session_vote_session.py new file mode 100644 index 0000000..9bcdcd2 --- /dev/null +++ b/backend/playlist/migrations/0023_alter_vote_points_session_vote_session.py @@ -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", + ), + ), + ] diff --git a/backend/playlist/migrations/0024_alter_session_seed.py b/backend/playlist/migrations/0024_alter_session_seed.py new file mode 100644 index 0000000..3d98259 --- /dev/null +++ b/backend/playlist/migrations/0024_alter_session_seed.py @@ -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), + ), + ] diff --git a/backend/playlist/migrations/0025_alter_session_players_connected_alter_session_seed.py b/backend/playlist/migrations/0025_alter_session_players_connected_alter_session_seed.py new file mode 100644 index 0000000..8aa8d42 --- /dev/null +++ b/backend/playlist/migrations/0025_alter_session_players_connected_alter_session_seed.py @@ -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), + ), + ] diff --git a/backend/playlist/migrations/0026_alter_session_players_connected_alter_session_seed.py b/backend/playlist/migrations/0026_alter_session_players_connected_alter_session_seed.py new file mode 100644 index 0000000..9a862c6 --- /dev/null +++ b/backend/playlist/migrations/0026_alter_session_players_connected_alter_session_seed.py @@ -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), + ), + ] diff --git a/backend/playlist/models/__init__.py b/backend/playlist/models/__init__.py index 7213a1b..70754be 100644 --- a/backend/playlist/models/__init__.py +++ b/backend/playlist/models/__init__.py @@ -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 diff --git a/backend/playlist/models/session.py b/backend/playlist/models/session.py new file mode 100644 index 0000000..b6643c2 --- /dev/null +++ b/backend/playlist/models/session.py @@ -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 + + 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) diff --git a/backend/playlist/models/vote.py b/backend/playlist/models/vote.py index 92af7b4..ad6a8e2 100644 --- a/backend/playlist/models/vote.py +++ b/backend/playlist/models/vote.py @@ -1,6 +1,7 @@ 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: @@ -9,3 +10,4 @@ class Vote(models.Model): 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) diff --git a/backend/playlist/serializers/__init__.py b/backend/playlist/serializers/__init__.py index 4299cf2..f8dfd12 100644 --- a/backend/playlist/serializers/__init__.py +++ b/backend/playlist/serializers/__init__.py @@ -4,3 +4,4 @@ from .user import * from .album import * from .profile import * from .vote import * +from .session import * diff --git a/backend/playlist/serializers/session.py b/backend/playlist/serializers/session.py new file mode 100644 index 0000000..6fbb0df --- /dev/null +++ b/backend/playlist/serializers/session.py @@ -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__' diff --git a/backend/requirements.txt b/backend/requirements.txt index 6b3e994..1aacc33 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -11,3 +11,6 @@ markdown PyYAML Pygments drf-spectacular +channels +daphne +channels-redis diff --git a/backend/voting/__init__.py b/backend/voting/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/voting/apps.py b/backend/voting/apps.py new file mode 100644 index 0000000..59411e3 --- /dev/null +++ b/backend/voting/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + +class VotingConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'voting' diff --git a/backend/voting/consumers.py b/backend/voting/consumers.py new file mode 100644 index 0000000..c987a4d --- /dev/null +++ b/backend/voting/consumers.py @@ -0,0 +1,71 @@ +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 + + +class SessionConsumer(WebsocketConsumer): + def connect(self): + self.session_id = self.scope["url_route"]["kwargs"]["session_id"] + self.session_name = f"session_{self.session_id}" + # print("---") + # print(self.scope) + + # Join room group + 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"] + text_data_json = json.loads(text_data) + action = text_data_json["action"] + + # Niet lachen, middleware is moeilijk oké? + username = text_data_json.get("username") + token = text_data_json.get("token") + if token: + user = get_user_from_token(token, log=True) + print(user) + elif username: + pass + #user = User.objects.get(username=username) + + + 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": + print("LOGIN", str(user)) + async_to_sync(self.channel_layer.group_send)( + self.session_name, {"type": "announce", "user": str(user)} + ) + + def announce(self, event): + user = event["user"] + self.send(text_data=json.dumps({"joined": user})) + + + # Receive message from room group + def chat_message(self, event): + message = event["message"] + + print(self.scope["user"]) + + # Send message to WebSocket + self.send(text_data=json.dumps({"message": message})) diff --git a/backend/voting/routing.py b/backend/voting/routing.py new file mode 100644 index 0000000..17970f8 --- /dev/null +++ b/backend/voting/routing.py @@ -0,0 +1,7 @@ +from django.urls import re_path + +from . import consumers + +websocket_urlpatterns = [ + re_path(r"voting/session/(?P[\w\-]+)/$", consumers.SessionConsumer.as_asgi()), +] diff --git a/backend/voting/templates/voting/index.html b/backend/voting/templates/voting/index.html new file mode 100644 index 0000000..80210da --- /dev/null +++ b/backend/voting/templates/voting/index.html @@ -0,0 +1,25 @@ + + + + + Sessions + + +
+ + + + diff --git a/backend/voting/templates/voting/session.html b/backend/voting/templates/voting/session.html new file mode 100644 index 0000000..667d79a --- /dev/null +++ b/backend/voting/templates/voting/session.html @@ -0,0 +1,63 @@ + + + + + Votign Session + + +
+
+ + {{ room_name|json_script:"room-name" }} + + + diff --git a/backend/voting/urls.py b/backend/voting/urls.py new file mode 100644 index 0000000..562721a --- /dev/null +++ b/backend/voting/urls.py @@ -0,0 +1,9 @@ +from django.urls import path + +from . import views + +urlpatterns = [ + path("", views.index, name="index"), + path("session//test", views.session, name="session"), + path("session/", views.SessionListView.as_view(), name="api-session-list"), +] diff --git a/backend/voting/views.py b/backend/voting/views.py new file mode 100644 index 0000000..f684a43 --- /dev/null +++ b/backend/voting/views.py @@ -0,0 +1,24 @@ +from django.shortcuts import render +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) diff --git a/docker-compose.yml b/docker-compose.yml index b2dc692..9f86467 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -30,6 +30,12 @@ services: - "3003:3000" networks: - app_network + valkey: + image: valkey/valkey + ports: + - "6379:6379" + networks: + - app_network networks: app_network: diff --git a/frontend/src/app/layout.tsx b/frontend/src/app/layout.tsx index 2d0767c..60d4080 100644 --- a/frontend/src/app/layout.tsx +++ b/frontend/src/app/layout.tsx @@ -7,8 +7,10 @@ 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"; +import SessionChecker from "muzak/components/SessionChecker"; import { AnimationContext } from "muzak/contexts/AnimationContext"; import { QuotaContext } from "muzak/contexts/QuotaContext"; +import { SessionContext } from "muzak/contexts/SessionContext"; const nunito = Nunito({ subsets: ["latin"], @@ -17,6 +19,7 @@ const nunito = Nunito({ function LayoutContent({ children }: { children: React.ReactNode }) { return ( <> + {children} {/* */} @@ -32,6 +35,7 @@ export default function RootLayout({ }) { const [isAnimate, setIsAnimate] = useState(false); const [quota, setQuota] = useState(null); + const [sessionId, setSessionId] = useState(null); const toggleAnimation = () => { setIsAnimate(!isAnimate); @@ -43,7 +47,9 @@ export default function RootLayout({ - + + + diff --git a/frontend/src/app/lobby/page.tsx b/frontend/src/app/lobby/page.tsx index 16923d7..3c50d5a 100644 --- a/frontend/src/app/lobby/page.tsx +++ b/frontend/src/app/lobby/page.tsx @@ -4,6 +4,7 @@ import { Markazi_Text, Nunito } from "next/font/google"; import users from "muzak/data/users.json"; import Image from "next/image"; import Link from "next/link"; +import { useSession } from "muzak/contexts/SessionContext"; const nunito = Nunito({ weight: "900", @@ -13,29 +14,44 @@ 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([]); + const [sessionSocket, setSessionSocket] = useState(null); + const { sessionId, setSessionId } = useSession(); + + function onMessage(e) { + setTimeout(() => { + console.log(e); + let data = JSON.parse(e.data); + for (let key in data) { + if (key === "joined") { + addUser(data[key]); + } + } + console.log(data); + }, 1000); + } useEffect(() => { - populateUserList(); - }, [partySize]); + if (sessionId) { + const socket = new WebSocket( + `${process.env.NEXT_PUBLIC_WS_HOST}/voting/session/${sessionId}/`, + ); + socket.onmessage = onMessage; + setSessionSocket(socket); + } + }, [sessionId]); - 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)); + function addUser(userName: string) { + if (userList.find((user) => user.name === userName)) { + return; + } + const newUser = { + id: userList.length + 1, + name: userName, + avatar: `https://i.pravatar.cc/150?img=${userList.length + 1}`, + host: userList.length === 0, + }; + setUserList((prevUserList) => [...prevUserList, newUser]); } return ( @@ -51,7 +67,7 @@ export default function Lobby() {
- Players: {partySize} + Players: {userList.length}
- {partySize === 0 ? ( + {userList.length === 0 ? (
Waiting for players...
diff --git a/frontend/src/app/session/page.tsx b/frontend/src/app/session/page.tsx new file mode 100644 index 0000000..a5526db --- /dev/null +++ b/frontend/src/app/session/page.tsx @@ -0,0 +1,62 @@ +"use client"; +import { useSession } from "muzak/contexts/SessionContext"; +import { useState, useEffect } from "react"; +import { useAuth } from "react-oidc-context"; +import { getUser } from "muzak/data/fetcher"; +import { Nunito } from "next/font/google"; + +const nunito = Nunito({ + weight: "900", + subsets: ["latin"], +}); + +export default function Session() { + const auth = useAuth(); + const { sessionId, setSessionId } = useSession(); + const user = getUser(); + const [sessionSocket, setSessionSocket] = useState(null); + + function connect() { + console.log(`ik ben ${user.profile.nickname}`); + console.log(sessionSocket?.readyState); + if (sessionSocket?.readyState === WebSocket.OPEN) { + console.log("socket is open"); + sessionSocket.send( + JSON.stringify({ + action: "login", + userr: user.profile.nickname, + token: user.access_token, + }), + ); + } + } + + useEffect(() => { + if (sessionId) { + const socket = new WebSocket( + `${process.env.NEXT_PUBLIC_WS_HOST}/voting/session/${sessionId}/`, + ); + setSessionSocket(socket); + } + }, [sessionId]); + return ( +
+
+

+ LOGIN +

+
+
+

{user.profile.nickname}

+ +
+
+ ); +} diff --git a/frontend/src/components/SessionChecker.tsx b/frontend/src/components/SessionChecker.tsx new file mode 100644 index 0000000..76c3525 --- /dev/null +++ b/frontend/src/components/SessionChecker.tsx @@ -0,0 +1,52 @@ +"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"; + +export default function SessionChecker() { + const auth = useAuth(); + const router = useRouter(); + + const [isSession, setIsSession] = useState(false); + const [link, setLink] = useState("/lobby"); + 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) { + setIsSession(true); + const user = getUser(); + setSessionId(sessions[0].session_id); + if (sessions[0].host === user?.profile?.email) { + setLink("/lobby"); + } else { + setLink("/session"); + } + } else { + setIsSession(false); + } + } + } + + useEffect(() => { + getSessions(); + }, [auth]); + + if (isSession) { + return ( + + Session available! + + ); + } + return <> ; +} diff --git a/frontend/src/contexts/SessionContext.tsx b/frontend/src/contexts/SessionContext.tsx new file mode 100644 index 0000000..65e8602 --- /dev/null +++ b/frontend/src/contexts/SessionContext.tsx @@ -0,0 +1,14 @@ +"use client"; +import { createContext, useContext } from "react"; + +interface SessionContextType { + sessionId: string; + setSessionId: React.Dispatch>; +} +const SessionContext = createContext( + {} as SessionContextType, +); + +export const useSession = () => useContext(SessionContext); + +export { SessionContext }; diff --git a/frontend/src/data/fetcher.ts b/frontend/src/data/fetcher.ts index 54b6124..ac7b64a 100644 --- a/frontend/src/data/fetcher.ts +++ b/frontend/src/data/fetcher.ts @@ -83,11 +83,15 @@ export const api = { profiles: { me: fetcher.path("/api/profile/me").method("get").create(), }, + sessions: { + 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;