sessions, or something
This commit is contained in:
+12
-2
@@ -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
@@ -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)
|
||||
|
||||
@@ -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': {
|
||||
|
||||
@@ -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')),
|
||||
]
|
||||
|
||||
@@ -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),
|
||||
),
|
||||
]
|
||||
+27
@@ -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),
|
||||
),
|
||||
]
|
||||
+27
@@ -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),
|
||||
),
|
||||
]
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
|
||||
@@ -4,3 +4,4 @@ from .user import *
|
||||
from .album import *
|
||||
from .profile import *
|
||||
from .vote import *
|
||||
from .session import *
|
||||
|
||||
@@ -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__'
|
||||
@@ -11,3 +11,6 @@ markdown
|
||||
PyYAML
|
||||
Pygments
|
||||
drf-spectacular
|
||||
channels
|
||||
daphne
|
||||
channels-redis
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
class VotingConfig(AppConfig):
|
||||
default_auto_field = 'django.db.models.BigAutoField'
|
||||
name = 'voting'
|
||||
@@ -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}))
|
||||
@@ -0,0 +1,7 @@
|
||||
from django.urls import re_path
|
||||
|
||||
from . import consumers
|
||||
|
||||
websocket_urlpatterns = [
|
||||
re_path(r"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>
|
||||
@@ -0,0 +1,9 @@
|
||||
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"),
|
||||
]
|
||||
@@ -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)
|
||||
@@ -30,6 +30,12 @@ services:
|
||||
- "3003:3000"
|
||||
networks:
|
||||
- app_network
|
||||
valkey:
|
||||
image: valkey/valkey
|
||||
ports:
|
||||
- "6379:6379"
|
||||
networks:
|
||||
- app_network
|
||||
|
||||
networks:
|
||||
app_network:
|
||||
|
||||
@@ -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 (
|
||||
<>
|
||||
<SessionChecker />
|
||||
<AuthStatus />
|
||||
{children}
|
||||
{/* <NavigationBar /> */}
|
||||
@@ -32,6 +35,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);
|
||||
@@ -43,7 +47,9 @@ export default function RootLayout({
|
||||
<AuthProvider {...oidcConfig}>
|
||||
<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>
|
||||
|
||||
@@ -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<any[]>([]);
|
||||
const [sessionSocket, setSessionSocket] = useState<WebSocket | null>(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]);
|
||||
|
||||
function populateUserList() {
|
||||
const mappedUsers = users.slice(0, partySize).map((user) => ({
|
||||
id: user.id,
|
||||
name: user.name,
|
||||
avatar: user.avatar,
|
||||
host: user.host,
|
||||
}));
|
||||
setUserList(mappedUsers);
|
||||
if (sessionId) {
|
||||
const socket = new WebSocket(
|
||||
`${process.env.NEXT_PUBLIC_WS_HOST}/voting/session/${sessionId}/`,
|
||||
);
|
||||
socket.onmessage = onMessage;
|
||||
setSessionSocket(socket);
|
||||
}
|
||||
}, [sessionId]);
|
||||
|
||||
function addUserToParty() {
|
||||
setPartySize((prev) => Math.min(prev + 1, MAX_PARTY_SIZE));
|
||||
function addUser(userName: string) {
|
||||
if (userList.find((user) => user.name === userName)) {
|
||||
return;
|
||||
}
|
||||
|
||||
function deleteUserFromParty() {
|
||||
setPartySize((prev) => Math.max(prev - 1, 0));
|
||||
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() {
|
||||
<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}
|
||||
Players: {userList.length}
|
||||
</div>
|
||||
<Link
|
||||
href="/voting"
|
||||
@@ -61,7 +77,7 @@ export default function Lobby() {
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
{partySize === 0 ? (
|
||||
{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>
|
||||
|
||||
@@ -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<WebSocket | null>(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 (
|
||||
<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">
|
||||
<h1 className="text-xl text-white">{user.profile.nickname}</h1>
|
||||
<button
|
||||
className="rounded-md bg-blue-500 px-4 py-2 text-white hover:bg-blue-600"
|
||||
onClick={connect}
|
||||
>
|
||||
Connect
|
||||
</button>
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
@@ -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 (
|
||||
<Link
|
||||
href={link}
|
||||
className="bold text-l absolute top-2 left-2 rounded-[50%] bg-red-500 p-3 font-bold text-white"
|
||||
>
|
||||
Session available!
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
return <> </>;
|
||||
}
|
||||
@@ -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 };
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user