Compare commits
60 Commits
c7c808023a
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| bbb9d37ec7 | |||
| bcaaae90e9 | |||
| b4a037388a | |||
| c710845288 | |||
| 946548d001 | |||
| 7ed8895038 | |||
| 51697622a7 | |||
| 6b6da07ddf | |||
| 2403734bf3 | |||
| 9184d759f3 | |||
| 9c94934ff3 | |||
| e72e2e6800 | |||
| 65e81a7e6b | |||
| c23cb63817 | |||
| 5bdf1ed940 | |||
| 2a23f1dedc | |||
| da10b19960 | |||
| 5b0ef233c4 | |||
| 1d50bc3839 | |||
| 479dbe5c2d | |||
| 4e324502ae | |||
| d6cfdf66c6 | |||
| a64d9fea31 | |||
| b04a4732cf | |||
| bd71bce450 | |||
| cfb6b2609b | |||
| 564f2d47b4 | |||
| f22ff07577 | |||
| 47583e36dc | |||
| 7d54f6c5ce | |||
| 698c41544c | |||
| 5de174b162 | |||
| 341f7c8091 | |||
| 12344272ae | |||
| 762ec15a30 | |||
| c7eb174a01 | |||
| a1526ddc67 | |||
| cbc9be1f63 | |||
| 6216bd2c84 | |||
| 368628c070 | |||
| e0a03f00bf | |||
| 76b084b541 | |||
| 21d5f56a39 | |||
| 4c70a3f72d | |||
| b20e0c3980 | |||
| 8448a865a7 | |||
| b45bcf03ab | |||
| 421a004667 | |||
| 370507d5a6 | |||
| 67d8dac8c0 | |||
| 3eadd5c15d | |||
| 5ea702e7bb | |||
| 407abec1bf | |||
| b03a442444 | |||
| 28ee97782c | |||
| 7ad74db834 | |||
| 5ee177e4cf | |||
| 80eb434cde | |||
| 9f7981de65 | |||
| aff31e4c1d |
@@ -8,3 +8,6 @@ wallpapers/
|
|||||||
frontend/node_modules/
|
frontend/node_modules/
|
||||||
frontend/.next/
|
frontend/.next/
|
||||||
frontend/src/data/types.ts
|
frontend/src/data/types.ts
|
||||||
|
backend/session_*.log
|
||||||
|
backend/*.csv
|
||||||
|
*.bak
|
||||||
|
|||||||
@@ -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
@@ -8,9 +8,19 @@ https://docs.djangoproject.com/en/4.2/howto/deployment/asgi/
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
import os
|
import os
|
||||||
|
|
||||||
from django.core.asgi import get_asgi_application
|
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')
|
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.authentication import BaseAuthentication
|
||||||
from rest_framework.exceptions import AuthenticationFailed
|
from rest_framework.exceptions import AuthenticationFailed
|
||||||
from mozilla_django_oidc.auth import OIDCAuthenticationBackend
|
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):
|
class OIDCBearerTokenAuthentication(BaseAuthentication):
|
||||||
def authenticate(self, request):
|
def authenticate(self, request):
|
||||||
@@ -9,15 +21,38 @@ class OIDCBearerTokenAuthentication(BaseAuthentication):
|
|||||||
if not auth.startswith('Bearer '):
|
if not auth.startswith('Bearer '):
|
||||||
return None
|
return None
|
||||||
token = auth.split(' ')[1]
|
token = auth.split(' ')[1]
|
||||||
|
user = get_user_from_token(token)
|
||||||
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()
|
|
||||||
if not user:
|
if not user:
|
||||||
raise AuthenticationFailed(f'Unknown user: {claims}')
|
raise AuthenticationFailed(f'Unknown user: {token}')
|
||||||
return (user, None)
|
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.
|
CURRENT_HOST = 'http://127.0.0.1:8000' # Needs to be set for spotify callback to work.
|
||||||
|
|
||||||
INSTALLED_APPS = [
|
INSTALLED_APPS = [
|
||||||
|
'daphne',
|
||||||
'playlist.apps.PlaylistConfig',
|
'playlist.apps.PlaylistConfig',
|
||||||
|
'voting.apps.VotingConfig',
|
||||||
'django.contrib.admin',
|
'django.contrib.admin',
|
||||||
'django.contrib.auth',
|
'django.contrib.auth',
|
||||||
'mozilla_django_oidc',
|
'mozilla_django_oidc',
|
||||||
@@ -30,6 +32,7 @@ INSTALLED_APPS = [
|
|||||||
'rest_framework',
|
'rest_framework',
|
||||||
'corsheaders',
|
'corsheaders',
|
||||||
'drf_spectacular',
|
'drf_spectacular',
|
||||||
|
'channels',
|
||||||
]
|
]
|
||||||
|
|
||||||
MIDDLEWARE = [
|
MIDDLEWARE = [
|
||||||
@@ -48,7 +51,7 @@ ROOT_URLCONF = 'muzak.urls'
|
|||||||
TEMPLATES = [
|
TEMPLATES = [
|
||||||
{
|
{
|
||||||
'BACKEND': 'django.template.backends.django.DjangoTemplates',
|
'BACKEND': 'django.template.backends.django.DjangoTemplates',
|
||||||
'DIRS': [],
|
'DIRS': [BASE_DIR / 'templates'],
|
||||||
'APP_DIRS': True,
|
'APP_DIRS': True,
|
||||||
'OPTIONS': {
|
'OPTIONS': {
|
||||||
'context_processors': [
|
'context_processors': [
|
||||||
@@ -62,6 +65,16 @@ TEMPLATES = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
WSGI_APPLICATION = 'muzak.wsgi.application'
|
WSGI_APPLICATION = 'muzak.wsgi.application'
|
||||||
|
ASGI_APPLICATION = 'muzak.asgi.application'
|
||||||
|
|
||||||
|
CHANNEL_LAYERS = {
|
||||||
|
"default": {
|
||||||
|
"BACKEND": "channels_redis.core.RedisChannelLayer",
|
||||||
|
"CONFIG": {
|
||||||
|
"hosts": [("valkey", 6379)],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
DATABASES = {
|
DATABASES = {
|
||||||
'default': {
|
'default': {
|
||||||
|
|||||||
@@ -20,5 +20,6 @@ from django.urls import path, include
|
|||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path('admin/', admin.site.urls),
|
path('admin/', admin.site.urls),
|
||||||
path('oidc/', include('mozilla_django_oidc.urls')),
|
path('oidc/', include('mozilla_django_oidc.urls')),
|
||||||
|
path('voting/', include('voting.urls')),
|
||||||
path('', include('playlist.urls')),
|
path('', include('playlist.urls')),
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
from django.utils.http import urlencode
|
from django.utils.http import urlencode
|
||||||
from django.shortcuts import redirect
|
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):
|
class TrackAdmin(admin.ModelAdmin):
|
||||||
pass
|
pass
|
||||||
class ArtistAdmin(admin.ModelAdmin):
|
class ArtistAdmin(admin.ModelAdmin):
|
||||||
@@ -34,3 +36,4 @@ admin.site.register(Album, AlbumAdmin)
|
|||||||
admin.site.register(Background, BackgroundAdmin)
|
admin.site.register(Background, BackgroundAdmin)
|
||||||
admin.site.register(Vote, VoteAdmin)
|
admin.site.register(Vote, VoteAdmin)
|
||||||
admin.site.register(Profile, ProfileAdmin)
|
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),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -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")},
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -4,3 +4,4 @@ from .album import Album
|
|||||||
from .vote import Vote
|
from .vote import Vote
|
||||||
from .profile import Profile, Background
|
from .profile import Profile, Background
|
||||||
from .playlist import Playlist
|
from .playlist import Playlist
|
||||||
|
from .session import Session
|
||||||
|
|||||||
@@ -131,7 +131,6 @@ class Profile(models.Model):
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
def save(self, *args, **kwargs):
|
def save(self, *args, **kwargs):
|
||||||
self.update_quota()
|
|
||||||
super().save(*args, **kwargs)
|
super().save(*args, **kwargs)
|
||||||
|
|
||||||
@receiver(post_save, sender=User)
|
@receiver(post_save, sender=User)
|
||||||
|
|||||||
@@ -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)
|
||||||
@@ -1,11 +1,13 @@
|
|||||||
from django.db import models
|
from django.db import models
|
||||||
from django.contrib.auth.models import User
|
from django.contrib.auth.models import User
|
||||||
from django.core.validators import MinValueValidator, MaxValueValidator
|
from django.core.validators import MinValueValidator, MaxValueValidator
|
||||||
|
from .session import Session
|
||||||
|
|
||||||
class Vote(models.Model):
|
class Vote(models.Model):
|
||||||
class Meta:
|
class Meta:
|
||||||
unique_together = ["user", "track"]
|
unique_together = ["user", "track", "session"]
|
||||||
track = models.ForeignKey('Track', on_delete=models.CASCADE)
|
track = models.ForeignKey('Track', on_delete=models.CASCADE)
|
||||||
user = models.ForeignKey(User, on_delete=models.CASCADE)
|
user = models.ForeignKey(User, on_delete=models.CASCADE)
|
||||||
points = models.IntegerField(validators=[MinValueValidator(1), MaxValueValidator(5)])
|
points = models.IntegerField(validators=[MinValueValidator(1), MaxValueValidator(5)])
|
||||||
skipped = models.BooleanField(default=False)
|
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 .album import *
|
||||||
from .profile import *
|
from .profile import *
|
||||||
from .vote 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__'
|
||||||
@@ -19,6 +19,7 @@ urlpatterns = [
|
|||||||
path('api/vote/', views.VoteListView.as_view(), name="api-votes"),
|
path('api/vote/', views.VoteListView.as_view(), name="api-votes"),
|
||||||
path('api/vote/<id>', views.VoteDetailView.as_view(), name="api-vote"),
|
path('api/vote/<id>', views.VoteDetailView.as_view(), name="api-vote"),
|
||||||
path('api/user/', views.UserListView.as_view(), name="api-users"),
|
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"),
|
path('api/profile/me', views.ProfileMeView.as_view(), name="api-profile-me"),
|
||||||
|
|
||||||
### Classic
|
### Classic
|
||||||
|
|||||||
@@ -29,8 +29,6 @@ class TrackListView(APIView):
|
|||||||
if not can_nom:
|
if not can_nom:
|
||||||
return Response({'error': reason}, status=403)
|
return Response({'error': reason}, status=403)
|
||||||
(track, message) = Track.objects.create_from_spotify(request.data['spotify_link'], profile)
|
(track, message) = Track.objects.create_from_spotify(request.data['spotify_link'], profile)
|
||||||
profile.quota -= 1
|
|
||||||
profile.save()
|
|
||||||
serializer = TrackSerializer(track, context={'request': request})
|
serializer = TrackSerializer(track, context={'request': request})
|
||||||
return Response(serializer.data)
|
return Response(serializer.data)
|
||||||
|
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ class VoteView(LoginRequiredMixin, View):
|
|||||||
class NominateView(LoginRequiredMixin, View):
|
class NominateView(LoginRequiredMixin, View):
|
||||||
def get(self, request):
|
def get(self, request):
|
||||||
(profile,_) = Profile.objects.get_or_create(user=request.user)
|
(profile,_) = Profile.objects.get_or_create(user=request.user)
|
||||||
profile.update_quota()
|
#profile.update_quota()
|
||||||
profile.save()
|
profile.save()
|
||||||
return TemplateResponse(request, "Nominate.html", {'end_time': os.getenv('DATE_NOM_END')})
|
return TemplateResponse(request, "Nominate.html", {'end_time': os.getenv('DATE_NOM_END')})
|
||||||
def post(self, request):
|
def post(self, request):
|
||||||
|
|||||||
@@ -11,3 +11,6 @@ markdown
|
|||||||
PyYAML
|
PyYAML
|
||||||
Pygments
|
Pygments
|
||||||
drf-spectacular
|
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,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}))
|
||||||
@@ -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>
|
||||||
@@ -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"),
|
||||||
|
]
|
||||||
@@ -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})
|
||||||
@@ -30,6 +30,12 @@ services:
|
|||||||
- "3003:3000"
|
- "3003:3000"
|
||||||
networks:
|
networks:
|
||||||
- app_network
|
- app_network
|
||||||
|
valkey:
|
||||||
|
image: valkey/valkey
|
||||||
|
ports:
|
||||||
|
- "6379:6379"
|
||||||
|
networks:
|
||||||
|
- app_network
|
||||||
|
|
||||||
networks:
|
networks:
|
||||||
app_network:
|
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
@@ -7,31 +7,60 @@ import { Source_Sans_3, Nunito } from "next/font/google";
|
|||||||
import AuthStatus from "muzak/components/AuthStatus";
|
import AuthStatus from "muzak/components/AuthStatus";
|
||||||
import NavigationBar from "muzak/components/NavigationBar";
|
import NavigationBar from "muzak/components/NavigationBar";
|
||||||
import Quota from "muzak/components/Quota";
|
import Quota from "muzak/components/Quota";
|
||||||
|
import SessionChecker from "muzak/components/SessionChecker";
|
||||||
const sourceSans = Source_Sans_3({
|
import { AnimationContext } from "muzak/contexts/AnimationContext";
|
||||||
weight: ["400"],
|
import { QuotaContext } from "muzak/contexts/QuotaContext";
|
||||||
subsets: ["latin"],
|
import { SessionContext } from "muzak/contexts/SessionContext";
|
||||||
});
|
import { usePathname } from "next/navigation";
|
||||||
|
|
||||||
const nunito = Nunito({
|
const nunito = Nunito({
|
||||||
subsets: ["latin"],
|
subsets: ["latin"],
|
||||||
});
|
});
|
||||||
|
|
||||||
function LayoutContent({
|
const navToggle = true; //voor dev navigatie
|
||||||
children,
|
|
||||||
quota,
|
const pageConfig: Record<
|
||||||
setQuota,
|
string,
|
||||||
}: {
|
{
|
||||||
children: React.ReactNode;
|
showAuthStatus: boolean;
|
||||||
quota: number | null;
|
showNav: boolean;
|
||||||
setQuota: React.Dispatch<React.SetStateAction<number | null>>;
|
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 (
|
return (
|
||||||
<>
|
<>
|
||||||
<AuthStatus />
|
<SessionChecker />
|
||||||
|
{config.showAuthStatus && <AuthStatus />}
|
||||||
{children}
|
{children}
|
||||||
<NavigationBar />
|
{config.showNav && <NavigationBar />}
|
||||||
<Quota quota={quota} setQuota={setQuota} />
|
{config.showQuota && <Quota />}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -43,6 +72,7 @@ export default function RootLayout({
|
|||||||
}) {
|
}) {
|
||||||
const [isAnimate, setIsAnimate] = useState(false);
|
const [isAnimate, setIsAnimate] = useState(false);
|
||||||
const [quota, setQuota] = useState<number | null>(null);
|
const [quota, setQuota] = useState<number | null>(null);
|
||||||
|
const [sessionId, setSessionId] = useState<string | null>(null);
|
||||||
|
|
||||||
const toggleAnimation = () => {
|
const toggleAnimation = () => {
|
||||||
setIsAnimate(!isAnimate);
|
setIsAnimate(!isAnimate);
|
||||||
@@ -52,16 +82,14 @@ export default function RootLayout({
|
|||||||
<html lang="en" className={nunito.className}>
|
<html lang="en" className={nunito.className}>
|
||||||
<body className={`bg-game-show ${isAnimate ? "animate" : ""} h-screen`}>
|
<body className={`bg-game-show ${isAnimate ? "animate" : ""} h-screen`}>
|
||||||
<AuthProvider {...oidcConfig}>
|
<AuthProvider {...oidcConfig}>
|
||||||
<LayoutContent
|
<AnimationContext.Provider value={{ isAnimate, setIsAnimate }}>
|
||||||
children={children}
|
<QuotaContext.Provider value={{ quota, setQuota }}>
|
||||||
quota={quota}
|
<SessionContext.Provider value={{ sessionId, setSessionId }}>
|
||||||
setQuota={setQuota}
|
<LayoutContent children={children} />
|
||||||
/>
|
</SessionContext.Provider>
|
||||||
|
</QuotaContext.Provider>
|
||||||
|
</AnimationContext.Provider>
|
||||||
</AuthProvider>
|
</AuthProvider>
|
||||||
<button
|
|
||||||
className={`absolute top-1 left-1 ${isAnimate ? "pause" : "play"}`}
|
|
||||||
onClick={toggleAnimation}
|
|
||||||
></button>
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
);
|
);
|
||||||
|
|||||||
+448
-41
@@ -1,9 +1,15 @@
|
|||||||
"use client";
|
"use client";
|
||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect } from "react";
|
||||||
import { Markazi_Text, Nunito } from "next/font/google";
|
import { Markazi_Text, Nunito } from "next/font/google";
|
||||||
import users from "muzak/data/users.json";
|
import { useRouter } from "next/navigation";
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
|
import { useAuth } from "react-oidc-context";
|
||||||
|
import { 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({
|
const nunito = Nunito({
|
||||||
weight: "900",
|
weight: "900",
|
||||||
@@ -13,33 +19,257 @@ const nunito = Nunito({
|
|||||||
const MAX_PARTY_SIZE = 8;
|
const MAX_PARTY_SIZE = 8;
|
||||||
|
|
||||||
export default function Lobby() {
|
export default function Lobby() {
|
||||||
const [partySize, setPartySize] = useState(0);
|
const [userList, setUserList] = useState<any[]>([]);
|
||||||
const [userList, setUserList] = useState([]);
|
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(() => {
|
useEffect(() => {
|
||||||
populateUserList();
|
console.log("useEffect state:", state);
|
||||||
}, [partySize]);
|
if (state == "waiting") {
|
||||||
|
setRevealed(true);
|
||||||
function populateUserList() {
|
setTimeout(() => {
|
||||||
const mappedUsers = users.slice(0, partySize).map((user) => ({
|
setState("next");
|
||||||
id: user.id,
|
}, 12000);
|
||||||
name: user.name,
|
} else if (state == "next") {
|
||||||
avatar: user.avatar,
|
nextTrack();
|
||||||
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));
|
|
||||||
}
|
}
|
||||||
|
}, [state]);
|
||||||
|
|
||||||
|
if (sessionState == "lobby") {
|
||||||
return (
|
return (
|
||||||
<main className="flex min-h-screen flex-col items-center justify-between p-24">
|
<main className="relative flex min-h-screen flex-col items-center justify-between p-24">
|
||||||
|
<div className="absolute top-[-1000px]">
|
||||||
|
<div id="splayer"></div>
|
||||||
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h1
|
<h1
|
||||||
className={`${nunito.className} text-outline text-[42px] tracking-tighter text-yellow-500`}
|
className={`${nunito.className} text-outline text-[42px] tracking-tighter text-yellow-500`}
|
||||||
@@ -48,20 +278,21 @@ export default function Lobby() {
|
|||||||
</h1>
|
</h1>
|
||||||
</div>
|
</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="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="flex w-full flex-row items-center justify-between px-1">
|
||||||
<div className="text-3xl font-bold text-yellow-500">
|
<div className="text-3xl font-bold text-yellow-500">
|
||||||
Players: {partySize}
|
Players: {userList.length}
|
||||||
</div>
|
</div>
|
||||||
<Link
|
<button
|
||||||
href="/voting"
|
onClick={startSession}
|
||||||
className="rounded-lg border-2 border-black bg-yellow-500 p-4 text-2xl font-extrabold text-blue-600 hover:bg-yellow-400"
|
className="rounded-lg border-2 border-black bg-yellow-500 p-4 text-2xl font-extrabold text-blue-600 hover:bg-yellow-400 disabled:bg-yellow-900 disabled:text-gray-400"
|
||||||
|
disabled={sessionId === null}
|
||||||
>
|
>
|
||||||
START
|
START
|
||||||
</Link>
|
</button>
|
||||||
</div>
|
</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">
|
<div className="mt-31 min-w-52 text-3xl font-semibold text-white drop-shadow-sm drop-shadow-gray-900">
|
||||||
Waiting for players...
|
Waiting for players...
|
||||||
</div>
|
</div>
|
||||||
@@ -70,7 +301,7 @@ export default function Lobby() {
|
|||||||
{userList.map((user) => (
|
{userList.map((user) => (
|
||||||
<div
|
<div
|
||||||
key={user.id}
|
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"
|
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
|
<img
|
||||||
src={user.avatar}
|
src={user.avatar}
|
||||||
@@ -96,21 +327,197 @@ export default function Lobby() {
|
|||||||
</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="mt-4 flex items-center justify-between gap-4">
|
<div className="flex flex-col items-center justify-center">
|
||||||
<button
|
<div className="flex flex-row items-center justify-center gap-x-36">
|
||||||
className="rounded-md bg-blue-500 px-4 py-2 text-white hover:bg-blue-600"
|
<div className="relative w-fit overflow-hidden">
|
||||||
onClick={() => deleteUserFromParty()}
|
<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"
|
||||||
>
|
>
|
||||||
del
|
<img
|
||||||
</button>
|
src={user.avatar}
|
||||||
<button
|
alt={user.name}
|
||||||
className="rounded-md bg-blue-500 px-4 py-2 text-white hover:bg-blue-600"
|
width={100}
|
||||||
onClick={() => addUserToParty()}
|
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%)",
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
add
|
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>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="rounded-md border-8 border-black bg-blue-950/80 py-[1px]">
|
||||||
|
<Image
|
||||||
|
src="/cdspin.gif"
|
||||||
|
alt="Loading..."
|
||||||
|
width={500}
|
||||||
|
height={500}
|
||||||
|
className="rounded-md"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="relative w-fit overflow-hidden">
|
||||||
|
<div className="grid min-h-[516px] min-w-[516px] grid-flow-row grid-cols-3 place-items-center items-start justify-center gap-4 rounded bg-blue-950/80 px-4 pt-10">
|
||||||
|
{yesList.map((user) => (
|
||||||
|
<div
|
||||||
|
key={user.id}
|
||||||
|
className="flex h-[120px] w-[120px] flex-col items-center justify-center p-4"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src={user.avatar}
|
||||||
|
alt={user.name}
|
||||||
|
width={100}
|
||||||
|
height={100}
|
||||||
|
/>
|
||||||
|
<div className="mt-1 min-w-[90px] rounded-3xl px-3 text-center text-3xl font-semibold text-white">
|
||||||
|
{user.name}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}{" "}
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className={`absolute inset-0 flex items-center justify-center rounded bg-red-800 text-5xl font-bold text-white shadow-[inset_0_0_20px_rgba(0,0,0,0.3)] transition-transform duration-1000 ease-in-out ${
|
||||||
|
revealed ? "-translate-y-full" : "translate-y-0"
|
||||||
|
}`}
|
||||||
|
style={{
|
||||||
|
backgroundImage: `
|
||||||
|
linear-gradient(to bottom, rgba(0,0,0,0.2), rgba(0,0,0,0) 20%, rgba(0,0,0,0.2) 80%, rgba(0,0,0,0.2)),
|
||||||
|
repeating-linear-gradient(
|
||||||
|
90deg,
|
||||||
|
rgba(255,255,255,0.1),
|
||||||
|
rgba(255,255,255,0.1) 30px,
|
||||||
|
rgba(0,0,0,0) 30px,
|
||||||
|
rgba(0,0,0,0) 60px
|
||||||
|
)`,
|
||||||
|
clipPath:
|
||||||
|
"polygon(0 0, 100% 0, 100% 98%, 95% 100%, 90% 98%, 85% 100%, 80% 98%, 75% 100%, 70% 98%, 65% 100%, 60% 98%, 55% 100%, 50% 98%, 45% 100%, 40% 98%, 35% 100%, 30% 98%, 25% 100%, 20% 98%, 15% 100%, 10% 98%, 5% 100%, 0 98%)",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Yeah
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{!songLoading && currentSong ? (
|
||||||
|
<div className="flex flex-col items-center justify-start">
|
||||||
|
<h2
|
||||||
|
className={`${nunito.className} text-outline text-[42px] tracking-tighter text-yellow-500`}
|
||||||
|
>
|
||||||
|
{currentSong.artist}
|
||||||
|
</h2>
|
||||||
|
<h2
|
||||||
|
className={`${nunito.className} text-outline text-[42px] tracking-tighter text-yellow-500`}
|
||||||
|
>
|
||||||
|
{currentSong.name}
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="flex flex-col items-center justify-start">
|
||||||
|
<h2
|
||||||
|
className={`${nunito.className} text-outline text-[42px] tracking-tighter text-yellow-500`}
|
||||||
|
>
|
||||||
|
Loading
|
||||||
|
</h2>
|
||||||
|
<h2
|
||||||
|
className={`${nunito.className} text-outline text-[42px] tracking-tighter text-yellow-500`}
|
||||||
|
>
|
||||||
|
...
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
className={`flex min-h-33 min-w-1 flex-row items-start justify-start gap-10 rounded p-4 ${userList.some((u) => !u.hidden) ? "bg-blue-950/80" : "bg-transparent p-0"} `}
|
||||||
|
>
|
||||||
|
{userList.map((user) => (
|
||||||
|
<div
|
||||||
|
key={user.id}
|
||||||
|
className={`flex h-[100px] w-[100px] flex-col items-center justify-center p-4 ${user.hidden ? "hidden" : ""}`}
|
||||||
|
>
|
||||||
|
<img src={user.avatar} alt={user.name} width={75} height={75} />
|
||||||
|
<div className="mt-1 min-w-[90px] rounded-3xl px-3 text-center text-3xl font-semibold text-white">
|
||||||
|
{user.name}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
</main>
|
</main>
|
||||||
);
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,31 +1,49 @@
|
|||||||
"use client";
|
"use client";
|
||||||
import { useState } from "react";
|
import { useState, useEffect } from "react";
|
||||||
import { Nunito } from "next/font/google";
|
import { Nunito } from "next/font/google";
|
||||||
import { api } from "muzak/data/fetcher";
|
import { api } from "muzak/data/fetcher";
|
||||||
|
import { useAnimation } from "muzak/contexts/AnimationContext";
|
||||||
|
import { useQuota } from "muzak/contexts/QuotaContext";
|
||||||
|
|
||||||
const nunito = Nunito({
|
const nunito = Nunito({
|
||||||
weight: "900",
|
weight: "900",
|
||||||
subsets: ["latin"],
|
subsets: ["latin"],
|
||||||
});
|
});
|
||||||
|
|
||||||
async function nominate(spotifyLink: string) {
|
export default function Nominations() {
|
||||||
console.log(spotifyLink);
|
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 {
|
try {
|
||||||
|
setIsAnimate(false);
|
||||||
const response = await api.tracks.nominate({
|
const response = await api.tracks.nominate({
|
||||||
spotify_link: spotifyLink,
|
spotify_link: spotifyLink,
|
||||||
});
|
});
|
||||||
console.log("Nomination successful:", response);
|
console.log("Nomination successful:", response);
|
||||||
|
setIsAnimate(true);
|
||||||
|
setQuota(quota - 1);
|
||||||
return { success: true, data: response.data };
|
return { success: true, data: response.data };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Nomination failed:", error);
|
console.error("Nomination failed:", error);
|
||||||
return { success: false, error };
|
return { success: false, error };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function Nominations() {
|
useEffect(() => {
|
||||||
const [songURL, setSongURL] = useState("");
|
if (quota > 0) {
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
setCanNominate(true);
|
||||||
const [message, setMessage] = useState("");
|
} else {
|
||||||
|
setCanNominate(false);
|
||||||
|
}
|
||||||
|
}, [quota]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<main className="flex min-h-screen flex-col items-center justify-between p-24">
|
<main className="flex min-h-screen flex-col items-center justify-between p-24">
|
||||||
@@ -34,7 +52,7 @@ export default function Nominations() {
|
|||||||
>
|
>
|
||||||
NOMINATIES
|
NOMINATIES
|
||||||
</h1>
|
</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">
|
<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">
|
<p className="text-2xl font-semibold text-white drop-shadow-sm drop-shadow-gray-900">
|
||||||
Dump hier je Spotify-link:
|
Dump hier je Spotify-link:
|
||||||
@@ -52,7 +70,9 @@ export default function Nominations() {
|
|||||||
<button
|
<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"
|
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"
|
type="submit"
|
||||||
disabled={isLoading || !songURL.trim()}
|
disabled={
|
||||||
|
isLoading || !songURL.trim() || !isSpotifyURL || !canNominate
|
||||||
|
}
|
||||||
onClick={async (e) => {
|
onClick={async (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (!songURL.trim()) return;
|
if (!songURL.trim()) return;
|
||||||
@@ -76,11 +96,12 @@ export default function Nominations() {
|
|||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
{message && (
|
{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}
|
{message}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<footer>
|
<footer>
|
||||||
<p>© 2020-2025 Hoestende Draken PartyCo</p>
|
<p>© 2020-2025 Hoestende Draken PartyCo</p>
|
||||||
</footer>
|
</footer>
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ const nunito = Nunito({
|
|||||||
});
|
});
|
||||||
|
|
||||||
export default function Page() {
|
export default function Page() {
|
||||||
const [userName, setUserName] = useState("Stroop");
|
|
||||||
const auth = useAuth();
|
const auth = useAuth();
|
||||||
console.log(auth);
|
console.log(auth);
|
||||||
|
|
||||||
@@ -37,7 +36,7 @@ export default function Page() {
|
|||||||
{!auth.isAuthenticated && (
|
{!auth.isAuthenticated && (
|
||||||
<div className="mb-15 flex flex-col items-center justify-between gap-4 rounded-md bg-blue-950/80 p-8">
|
<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">
|
<p className="text-2xl font-semibold text-white drop-shadow-sm drop-shadow-gray-900">
|
||||||
Welkom, {userName}!
|
Welkom bij de HDcon Muzak
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
|
|||||||
@@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -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() {
|
export default function PostGame() {
|
||||||
|
const { sessionId, setSessionId } = useSession();
|
||||||
|
const [trackCount, setTrackCount] = useState(0);
|
||||||
|
const [voteCount, setVoteCount] = useState(0);
|
||||||
|
const [averageVote, setAverageVote] = useState(0);
|
||||||
|
|
||||||
|
async function getInfo() {
|
||||||
|
const { data: info } = await api.sessions.info({ session_id: sessionId });
|
||||||
|
console.log(info);
|
||||||
|
setTrackCount(info["track_count"]);
|
||||||
|
setVoteCount(info["vote_count"]);
|
||||||
|
setAverageVote(info["average_vote"]);
|
||||||
|
}
|
||||||
|
useEffect(() => {
|
||||||
|
getInfo();
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<main className="flex min-h-screen flex-col items-center justify-between p-24">
|
<main className="flex min-h-screen flex-col items-center justify-between p-24">
|
||||||
<h1>Post-game</h1>
|
<p
|
||||||
|
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>
|
</main>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,7 +15,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.bg-game-show.animate {
|
.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 {
|
@keyframes rotate_bg {
|
||||||
@@ -41,3 +41,8 @@
|
|||||||
.text-outline-small {
|
.text-outline-small {
|
||||||
-webkit-text-stroke: 1px black;
|
-webkit-text-stroke: 1px black;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
iframe {
|
||||||
|
position: absolute;
|
||||||
|
top: -1000px;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
"use client";
|
"use client";
|
||||||
import { getUser } from "muzak/data/fetch";
|
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { api, User } from "muzak/data/fetcher";
|
import { api, User } from "muzak/data/fetcher";
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -5,6 +5,8 @@ const source = Source_Sans_3({
|
|||||||
subsets: ["latin"],
|
subsets: ["latin"],
|
||||||
});
|
});
|
||||||
const font = source.className;
|
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() {
|
export default function NavigationBar() {
|
||||||
const auth = useAuth();
|
const auth = useAuth();
|
||||||
@@ -12,11 +14,26 @@ export default function NavigationBar() {
|
|||||||
return (
|
return (
|
||||||
<nav className="absolute right-0 bottom-0 left-0 flex justify-center">
|
<nav className="absolute right-0 bottom-0 left-0 flex justify-center">
|
||||||
<div
|
<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 className={btnStyle} href="/nominations">
|
||||||
<Link href="/lobby">Start spel (test)</Link>
|
Nomineren
|
||||||
<Link href="/users">Users</Link>
|
</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>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -3,18 +3,14 @@ import { useAuth } from "react-oidc-context";
|
|||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { Nunito } from "next/font/google";
|
import { Nunito } from "next/font/google";
|
||||||
import { api, Profile } from "muzak/data/fetcher";
|
import { api, Profile } from "muzak/data/fetcher";
|
||||||
|
import { useQuota } from "muzak/contexts/QuotaContext";
|
||||||
const nunito = Nunito({
|
const nunito = Nunito({
|
||||||
subsets: ["latin"],
|
subsets: ["latin"],
|
||||||
});
|
});
|
||||||
|
|
||||||
export default function Quota({
|
export default function Quota() {
|
||||||
quota,
|
|
||||||
setQuota,
|
|
||||||
}: {
|
|
||||||
quota: number | null;
|
|
||||||
setQuota: React.Dispatch<React.SetStateAction<number | null>>;
|
|
||||||
}) {
|
|
||||||
const auth = useAuth();
|
const auth = useAuth();
|
||||||
|
const { quota, setQuota } = useQuota();
|
||||||
async function getProfile() {
|
async function getProfile() {
|
||||||
if (!auth.isLoading && !auth.error && auth.isAuthenticated) {
|
if (!auth.isLoading && !auth.error && auth.isAuthenticated) {
|
||||||
const { data: profile } = await api?.profiles?.me({});
|
const { data: profile } = await api?.profiles?.me({});
|
||||||
@@ -28,7 +24,7 @@ export default function Quota({
|
|||||||
if (!auth.isLoading && !auth.error && auth.isAuthenticated) {
|
if (!auth.isLoading && !auth.error && auth.isAuthenticated) {
|
||||||
return (
|
return (
|
||||||
<h1
|
<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}
|
{quota}
|
||||||
</h1>
|
</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 <> </>;
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { UserManager } from "oidc-client-ts";
|
import { UserManager, WebStorageStateStore } from "oidc-client-ts";
|
||||||
import { AuthProviderProps } from "react-oidc-context";
|
import { AuthProviderProps } from "react-oidc-context";
|
||||||
|
|
||||||
const ORIGIN_URI = globalThis?.window?.location.origin;
|
const ORIGIN_URI = globalThis?.window?.location.origin;
|
||||||
@@ -7,7 +7,6 @@ const REDIRECT_URI = `${ORIGIN_URI}/auth/callback`;
|
|||||||
const userConfig: AuthProviderProps = {
|
const userConfig: AuthProviderProps = {
|
||||||
authority: process.env.NEXT_PUBLIC_AUTHORITY!,
|
authority: process.env.NEXT_PUBLIC_AUTHORITY!,
|
||||||
client_id: process.env.NEXT_PUBLIC_CLIENT_ID!,
|
client_id: process.env.NEXT_PUBLIC_CLIENT_ID!,
|
||||||
client_secret: process.env.NEXT_PUBLIC_CLIENT_SECRET!,
|
|
||||||
redirect_uri: REDIRECT_URI,
|
redirect_uri: REDIRECT_URI,
|
||||||
response_type: "code",
|
response_type: "code",
|
||||||
//includeIdTokenInSilentRenew: true,
|
//includeIdTokenInSilentRenew: true,
|
||||||
@@ -18,8 +17,15 @@ const userConfig: AuthProviderProps = {
|
|||||||
post_logout_redirect_uri: ORIGIN_URI,
|
post_logout_redirect_uri: ORIGIN_URI,
|
||||||
response_mode: "query",
|
response_mode: "query",
|
||||||
revokeTokensOnSignout: true,
|
revokeTokensOnSignout: true,
|
||||||
|
staleStateAgeInSeconds: 60000,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (globalThis?.window != undefined) {
|
||||||
|
userConfig.userStore = new WebStorageStateStore({
|
||||||
|
store: window.localStorage,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export const userManager = new UserManager(userConfig);
|
export const userManager = new UserManager(userConfig);
|
||||||
|
|
||||||
// Some handling of token expiration.
|
// Some handling of token expiration.
|
||||||
|
|||||||
@@ -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 };
|
||||||
@@ -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 };
|
||||||
@@ -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 };
|
||||||
@@ -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;
|
||||||
|
};
|
||||||
@@ -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);
|
|
||||||
}
|
|
||||||
@@ -1,7 +1,25 @@
|
|||||||
"use client";
|
"use client";
|
||||||
import { Fetcher } from "openapi-typescript-fetch";
|
import { Fetcher } from "openapi-typescript-fetch";
|
||||||
import { getUser } from "./fetch";
|
|
||||||
import { paths, components } from "muzak/data/types";
|
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 fetcher = Fetcher.for<paths>();
|
||||||
const BASE_URL = process.env.NEXT_PUBLIC_BACKEND_HOST!;
|
const BASE_URL = process.env.NEXT_PUBLIC_BACKEND_HOST!;
|
||||||
@@ -72,11 +90,20 @@ export const api = {
|
|||||||
profiles: {
|
profiles: {
|
||||||
me: fetcher.path("/api/profile/me").method("get").create(),
|
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 User = components["schemas"]["User"];
|
||||||
export type Track = components["schemas"]["Track"];
|
export type Track = components["schemas"]["Track"];
|
||||||
export type Album = components["schemas"]["Album"];
|
export type Album = components["schemas"]["Album"];
|
||||||
export type Profile = components["schemas"]["Profile"];
|
export type Profile = components["schemas"]["Profile"];
|
||||||
|
export type Session = components["schemas"]["Session"];
|
||||||
|
|
||||||
export default api;
|
export default api;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
//}
|
||||||
|
}
|
||||||
@@ -9,7 +9,8 @@
|
|||||||
"muzak/config/*": ["./src/config/*"],
|
"muzak/config/*": ["./src/config/*"],
|
||||||
"muzak/data/*": ["./src/data/*"],
|
"muzak/data/*": ["./src/data/*"],
|
||||||
"muzak/app/*": ["./src/app/*"],
|
"muzak/app/*": ["./src/app/*"],
|
||||||
"muzak/components/*": ["./src/components/*"]
|
"muzak/components/*": ["./src/components/*"],
|
||||||
|
"muzak/contexts/*": ["./src/contexts/*"]
|
||||||
},
|
},
|
||||||
|
|
||||||
"allowJs": false,
|
"allowJs": false,
|
||||||
|
|||||||
Reference in New Issue
Block a user