Data Access Layer #13
@@ -7,3 +7,4 @@ albums/
|
|||||||
wallpapers/
|
wallpapers/
|
||||||
frontend/node_modules/
|
frontend/node_modules/
|
||||||
frontend/.next/
|
frontend/.next/
|
||||||
|
frontend/src/data/types.ts
|
||||||
|
|||||||
@@ -104,6 +104,11 @@ class Track(models.Model):
|
|||||||
artists = list(map(lambda a:a.name, self.artists.all()))
|
artists = list(map(lambda a:a.name, self.artists.all()))
|
||||||
return " & ".join(artists)
|
return " & ".join(artists)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def album_cover(self):
|
||||||
|
if self.album:
|
||||||
|
return self.album.image_url
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def name_sanitized(self):
|
def name_sanitized(self):
|
||||||
return re.match("[^\-\(]*\w", self.name)[0]
|
return re.match("[^\-\(]*\w", self.name)[0]
|
||||||
|
|||||||
@@ -47,6 +47,9 @@ from drf_spectacular.utils import extend_schema_serializer, OpenApiExample
|
|||||||
]
|
]
|
||||||
)
|
)
|
||||||
class TrackSerializer(serializers.HyperlinkedModelSerializer):
|
class TrackSerializer(serializers.HyperlinkedModelSerializer):
|
||||||
|
artist = serializers.CharField()
|
||||||
|
album_cover = serializers.CharField()
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Track
|
model = Track
|
||||||
fields = '__all__'
|
fields = '__all__'
|
||||||
@@ -54,5 +57,5 @@ class TrackSerializer(serializers.HyperlinkedModelSerializer):
|
|||||||
'url': {'view_name': 'api-track', 'lookup_field': 'id'},
|
'url': {'view_name': 'api-track', 'lookup_field': 'id'},
|
||||||
'artists': {'view_name': 'api-artist', 'lookup_field': 'id'},
|
'artists': {'view_name': 'api-artist', 'lookup_field': 'id'},
|
||||||
'album': {'view_name': 'api-album', 'lookup_field': 'id'},
|
'album': {'view_name': 'api-album', 'lookup_field': 'id'},
|
||||||
'nominated_by': {'view_name': 'api-user', 'lookup_field': 'id'}
|
'nominated_by': {'view_name': 'api-user', 'lookup_field': 'id'},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ urlpatterns = [
|
|||||||
path('api/schema/', SpectacularAPIView.as_view(), name='schema'),
|
path('api/schema/', SpectacularAPIView.as_view(), name='schema'),
|
||||||
path('api/schema/swagger-ui/', SpectacularSwaggerView.as_view(url_name='schema'), name='swagger-ui'),
|
path('api/schema/swagger-ui/', SpectacularSwaggerView.as_view(url_name='schema'), name='swagger-ui'),
|
||||||
path('api/track/', views.TrackListView.as_view(), name="api-tracks"),
|
path('api/track/', views.TrackListView.as_view(), name="api-tracks"),
|
||||||
|
path('api/track/vote', views.TrackVoteView.as_view(), name="api-track-vote"),
|
||||||
path('api/track/<id>', views.TrackDetailView.as_view(), name="api-track"),
|
path('api/track/<id>', views.TrackDetailView.as_view(), name="api-track"),
|
||||||
path('api/artist/<id>', views.ArtistDetailView.as_view(), name="api-artist"),
|
path('api/artist/<id>', views.ArtistDetailView.as_view(), name="api-artist"),
|
||||||
path('api/artist', views.ArtistListView.as_view(), name="api-artists"),
|
path('api/artist', views.ArtistListView.as_view(), name="api-artists"),
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ from playlist.serializers import TrackSerializer
|
|||||||
from playlist.spotify import spt
|
from playlist.spotify import spt
|
||||||
from playlist.tasks import get_banter, get_and_dither_image
|
from playlist.tasks import get_banter, get_and_dither_image
|
||||||
from drf_spectacular.utils import extend_schema
|
from drf_spectacular.utils import extend_schema
|
||||||
|
from random import choice
|
||||||
|
|
||||||
class TrackListView(APIView):
|
class TrackListView(APIView):
|
||||||
@extend_schema(
|
@extend_schema(
|
||||||
@@ -64,3 +65,16 @@ class TrackDetailView(APIView):
|
|||||||
return Response(status=204)
|
return Response(status=204)
|
||||||
else:
|
else:
|
||||||
return Response({'error': 'Only superusers can delete tracks'}, status=403)
|
return Response({'error': 'Only superusers can delete tracks'}, status=403)
|
||||||
|
|
||||||
|
class TrackVoteView(APIView):
|
||||||
|
@extend_schema(
|
||||||
|
request=None,
|
||||||
|
responses={200: TrackSerializer},
|
||||||
|
description="Retrieve a random track for voting."
|
||||||
|
)
|
||||||
|
def get(self, request):
|
||||||
|
# TODO implement a session-based vote tracking system. Random return for now.
|
||||||
|
random_pk = choice(Track.objects.values_list('pk', flat=True))
|
||||||
|
track = Track.objects.get(pk=random_pk)
|
||||||
|
serializer = TrackSerializer(track, context={'request': request})
|
||||||
|
return Response(serializer.data)
|
||||||
|
|||||||
Executable
+16
@@ -0,0 +1,16 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
if [ -z "$1" ]; then
|
||||||
|
set -- "BACKEND_HOST"
|
||||||
|
fi
|
||||||
|
|
||||||
|
HOST=$(grep "$1" .env 2>/dev/null | cut -d '=' -f 2 | tr -d '"')
|
||||||
|
echo $1
|
||||||
|
echo $HOST
|
||||||
|
cmd="npx openapi-typescript $HOST/api/schema/ -o src/data/types.tmp.ts"
|
||||||
|
if eval $cmd
|
||||||
|
then
|
||||||
|
echo $cmd
|
||||||
|
mv src/data/types.tmp.ts src/data/types.ts
|
||||||
|
fi
|
||||||
|
chmod go+rw src/data/types.ts
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
const nextConfig = {
|
||||||
|
images: {
|
||||||
|
remotePatterns: [new URL("https://i.scdn.co/**")],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default nextConfig;
|
||||||
Generated
+11
@@ -8,6 +8,7 @@
|
|||||||
"@next/font": "^14.2.15",
|
"@next/font": "^14.2.15",
|
||||||
"next": "^15.3.3",
|
"next": "^15.3.3",
|
||||||
"oidc-client-ts": "^3.3.0",
|
"oidc-client-ts": "^3.3.0",
|
||||||
|
"openapi-typescript-fetch": "^2.2.1",
|
||||||
"react": "^19.1.0",
|
"react": "^19.1.0",
|
||||||
"react-dom": "^19.1.0",
|
"react-dom": "^19.1.0",
|
||||||
"react-oidc-context": "^3.3.0"
|
"react-oidc-context": "^3.3.0"
|
||||||
@@ -1532,6 +1533,16 @@
|
|||||||
"node": ">=18"
|
"node": ">=18"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/openapi-typescript-fetch": {
|
||||||
|
"version": "2.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/openapi-typescript-fetch/-/openapi-typescript-fetch-2.2.1.tgz",
|
||||||
|
"integrity": "sha512-aBp1cR5FTNxp4HA8bb2ST53aIqEiJgoOMyXiyzKi6YF7vogW8KkyyUQ1FeDz8D05uspxFrKvFbkVU2YiiKkULA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 12.0.0",
|
||||||
|
"npm": ">= 7.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/picocolors": {
|
"node_modules/picocolors": {
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
|
||||||
|
|||||||
@@ -3,15 +3,17 @@
|
|||||||
"@next/font": "^14.2.15",
|
"@next/font": "^14.2.15",
|
||||||
"next": "^15.3.3",
|
"next": "^15.3.3",
|
||||||
"oidc-client-ts": "^3.3.0",
|
"oidc-client-ts": "^3.3.0",
|
||||||
|
"openapi-typescript-fetch": "^2.2.1",
|
||||||
"react": "^19.1.0",
|
"react": "^19.1.0",
|
||||||
"react-dom": "^19.1.0",
|
"react-dom": "^19.1.0",
|
||||||
"react-oidc-context": "^3.3.0"
|
"react-oidc-context": "^3.3.0"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "next dev",
|
"dev": "npm run generate:types && next dev",
|
||||||
"build": "next build",
|
"build": "npm run generate:types && next build",
|
||||||
"start": "next start",
|
"start": "next start",
|
||||||
"lint": "next lint"
|
"lint": "next lint",
|
||||||
|
"generate:types": "bash generate_types.sh DOCKER_BACKEND"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tailwindcss/postcss": "^4.1.10",
|
"@tailwindcss/postcss": "^4.1.10",
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ import { useState } from "react";
|
|||||||
import { AuthProvider } from "react-oidc-context";
|
import { AuthProvider } from "react-oidc-context";
|
||||||
import { oidcConfig, userManager } from "muzak/config/auth";
|
import { oidcConfig, userManager } from "muzak/config/auth";
|
||||||
import { Source_Sans_3, Nunito } from "next/font/google";
|
import { Source_Sans_3, Nunito } from "next/font/google";
|
||||||
|
import AuthStatus from "muzak/components/AuthStatus";
|
||||||
|
import NavigationBar from "muzak/components/NavigationBar";
|
||||||
|
|
||||||
const sourceSans = Source_Sans_3({
|
const sourceSans = Source_Sans_3({
|
||||||
weight: ["400"],
|
weight: ["400"],
|
||||||
@@ -14,6 +16,16 @@ const nunito = Nunito({
|
|||||||
subsets: ["latin"],
|
subsets: ["latin"],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function LayoutContent({ children }: { children: React.ReactNode }) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<AuthStatus />
|
||||||
|
{children}
|
||||||
|
<NavigationBar />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export default function RootLayout({
|
export default function RootLayout({
|
||||||
children,
|
children,
|
||||||
}: {
|
}: {
|
||||||
@@ -28,7 +40,9 @@ export default function RootLayout({
|
|||||||
return (
|
return (
|
||||||
<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}>{children}</AuthProvider>
|
<AuthProvider {...oidcConfig}>
|
||||||
|
<LayoutContent>{children}</LayoutContent>
|
||||||
|
</AuthProvider>
|
||||||
<button
|
<button
|
||||||
className={`absolute top-1 left-1 ${isAnimate ? "pause" : "play"}`}
|
className={`absolute top-1 left-1 ${isAnimate ? "pause" : "play"}`}
|
||||||
onClick={toggleAnimation}
|
onClick={toggleAnimation}
|
||||||
|
|||||||
+21
-24
@@ -1,43 +1,40 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
|
import Link from "next/link";
|
||||||
import { Log } from "oidc-client-ts";
|
import { Log } from "oidc-client-ts";
|
||||||
import { useAuth } from "react-oidc-context";
|
import { useAuth } from "react-oidc-context";
|
||||||
|
import { Nunito } from "next/font/google";
|
||||||
|
|
||||||
Log.setLogger(console);
|
Log.setLogger(console);
|
||||||
|
const nunito = Nunito({
|
||||||
|
weight: "900",
|
||||||
|
subsets: ["latin"],
|
||||||
|
});
|
||||||
|
|
||||||
export default function Page() {
|
export default function Page() {
|
||||||
const auth = useAuth();
|
const auth = useAuth();
|
||||||
console.log(auth);
|
console.log(auth);
|
||||||
|
|
||||||
switch (auth.activeNavigator) {
|
|
||||||
case "signinSilent":
|
|
||||||
return <div>Signing you in...</div>;
|
|
||||||
case "signoutRedirect":
|
|
||||||
return <div>Signing you out...</div>;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (auth.isLoading) {
|
|
||||||
return <div>Loading...</div>;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<main
|
<main
|
||||||
className={`flex min-h-screen flex-col items-center justify-between p-24`}
|
className={`flex min-h-screen flex-col items-center justify-between p-24`}
|
||||||
>
|
>
|
||||||
{auth.error && <div>{auth.error.message}</div>}
|
<div>
|
||||||
{auth.isAuthenticated && (
|
<h1
|
||||||
<div>
|
className={`${nunito.className} text-outline text-[42px] tracking-tighter text-yellow-500`}
|
||||||
<p>Hello {auth.user?.profile.name} </p>
|
>
|
||||||
</div>
|
Herzlich Willkommen bei Muzak!
|
||||||
)}
|
</h1>
|
||||||
{auth.isAuthenticated && (
|
{auth.isAuthenticated && <></>}
|
||||||
<button onClick={() => void auth.removeUser()}>Log out</button>
|
{!auth.isAuthenticated && (
|
||||||
)}
|
<h2
|
||||||
{!auth.isAuthenticated && (
|
className={`${nunito.className} text-outline text-[32px] tracking-tighter text-yellow-500`}
|
||||||
<button onClick={() => void auth.signinRedirect()}>Log in</button>
|
>
|
||||||
)}
|
Bitte authentificeren.
|
||||||
<Image src={"/meowl.png"} width="460" height="460" alt="Meowl image" />
|
</h2>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</main>
|
</main>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,20 +1,15 @@
|
|||||||
"use client";
|
"use client";
|
||||||
import { getUser } from "muzak/data/fetch";
|
import { getUser } from "muzak/data/fetch";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
|
import { api, User } from "muzak/data/fetcher";
|
||||||
|
|
||||||
export default function Page() {
|
export default function Page() {
|
||||||
const [userData, setUserData] = useState<User[]>([]);
|
const [userData, setUserData] = useState<User[]>([]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
async function getUsers() {
|
async function getUsers() {
|
||||||
const user = getUser();
|
const { data: users } = await api.users.list({});
|
||||||
const access_token = user?.access_token;
|
setUserData(users);
|
||||||
const headers = new Headers();
|
|
||||||
headers.set("Authorization", `Bearer ${access_token}`);
|
|
||||||
headers.set("Content-Type", "application/json; charset=utf-8");
|
|
||||||
const users = await fetch("http://127.0.0.1:8001/api/user/", { headers });
|
|
||||||
const userData = await users.json();
|
|
||||||
setUserData(userData);
|
|
||||||
}
|
}
|
||||||
getUsers();
|
getUsers();
|
||||||
}, []);
|
}, []);
|
||||||
@@ -22,7 +17,7 @@ export default function Page() {
|
|||||||
return (
|
return (
|
||||||
<div className="flex min-h-screen flex-col items-center justify-around">
|
<div className="flex min-h-screen flex-col items-center justify-around">
|
||||||
{userData.length > 0 ? (
|
{userData.length > 0 ? (
|
||||||
userData.map((user) => <div key={user.id}>{user.email}</div>)
|
userData.map((user: User) => <div key={user.id}>{user.email}</div>)
|
||||||
) : (
|
) : (
|
||||||
<div>Loading users...</div>
|
<div>Loading users...</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { Nunito } from "next/font/google";
|
|||||||
import users from "muzak/data/users.json";
|
import users from "muzak/data/users.json";
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
|
import { api, Track } from "muzak/data/fetcher";
|
||||||
|
|
||||||
const nunito = Nunito({
|
const nunito = Nunito({
|
||||||
weight: "900",
|
weight: "900",
|
||||||
@@ -16,15 +17,25 @@ export default function Lobby() {
|
|||||||
const [yesVoteList, setYesVoteList] = useState([]);
|
const [yesVoteList, setYesVoteList] = useState([]);
|
||||||
const [noVoteList, setNoVoteList] = useState([]);
|
const [noVoteList, setNoVoteList] = useState([]);
|
||||||
|
|
||||||
const [currentSong, setCurrentSong] = useState({
|
const [currentSong, setCurrentSong] = useState<Track>();
|
||||||
artist: "The Meowls",
|
const [songLoading, setSongLoading] = useState(false);
|
||||||
title: "The Owls Are Not What They Seem",
|
|
||||||
});
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
populateUserList();
|
populateUserList();
|
||||||
}, [partySize]);
|
}, [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() {
|
function populateUserList() {
|
||||||
const mappedUsers = users.slice(0, partySize).map((user) => ({
|
const mappedUsers = users.slice(0, partySize).map((user) => ({
|
||||||
id: user.id,
|
id: user.id,
|
||||||
@@ -50,7 +61,7 @@ export default function Lobby() {
|
|||||||
<div className="flex flex-1 items-center justify-center">
|
<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="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">
|
<div className="text-5xl text-white drop-shadow-sm drop-shadow-black">
|
||||||
30
|
<button onClick={getTrack}>30</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -86,10 +97,25 @@ export default function Lobby() {
|
|||||||
</div>
|
</div>
|
||||||
))}{" "}
|
))}{" "}
|
||||||
</div>
|
</div>
|
||||||
|
{!songLoading && currentSong ? (
|
||||||
<div className="rounded-md bg-black p-2">
|
<div className="rounded-md bg-black p-2">
|
||||||
<Image src="/meowl.png" alt="meowl" width={500} height={500} />
|
<Image
|
||||||
</div>
|
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">
|
<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) => (
|
{userList.map((user) => (
|
||||||
<div
|
<div
|
||||||
@@ -104,18 +130,20 @@ export default function Lobby() {
|
|||||||
))}{" "}
|
))}{" "}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col items-center justify-start">
|
{!songLoading && currentSong && (
|
||||||
<h2
|
<div className="flex flex-col items-center justify-start">
|
||||||
className={`${nunito.className} text-outline text-[42px] tracking-tighter text-yellow-500`}
|
<h2
|
||||||
>
|
className={`${nunito.className} text-outline text-[42px] tracking-tighter text-yellow-500`}
|
||||||
{currentSong.artist}
|
>
|
||||||
</h2>
|
{currentSong.artist}
|
||||||
<h2
|
</h2>
|
||||||
className={`${nunito.className} text-outline text-[42px] tracking-tighter text-yellow-500`}
|
<h2
|
||||||
>
|
className={`${nunito.className} text-outline text-[42px] tracking-tighter text-yellow-500`}
|
||||||
{currentSong.title}
|
>
|
||||||
</h2>
|
{currentSong.name}
|
||||||
</div>
|
</h2>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-row items-start justify-start gap-10 rounded bg-blue-950/80 p-4">
|
<div className="flex flex-row items-start justify-start gap-10 rounded bg-blue-950/80 p-4">
|
||||||
|
|||||||
@@ -0,0 +1,83 @@
|
|||||||
|
"use client";
|
||||||
|
import { useAuth } from "react-oidc-context";
|
||||||
|
import { useState, useEffect } from "react";
|
||||||
|
import { useRouter } from "next/navigation";
|
||||||
|
import { Source_Sans_3, Nunito } from "next/font/google";
|
||||||
|
|
||||||
|
const nunito = Nunito({
|
||||||
|
subsets: ["latin"],
|
||||||
|
});
|
||||||
|
|
||||||
|
export default function AuthStatus() {
|
||||||
|
const auth = useAuth();
|
||||||
|
const router = useRouter();
|
||||||
|
const [persistentAuthState, setPersistentAuthState] = useState<{
|
||||||
|
isAuthenticated: boolean;
|
||||||
|
user: any;
|
||||||
|
hasInitialized: boolean;
|
||||||
|
}>({
|
||||||
|
isAuthenticated: false,
|
||||||
|
user: null,
|
||||||
|
hasInitialized: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// Only update persistent state when we have a definitive auth state
|
||||||
|
if (!auth.isLoading && !auth.error) {
|
||||||
|
setPersistentAuthState({
|
||||||
|
isAuthenticated: auth.isAuthenticated,
|
||||||
|
user: auth.user,
|
||||||
|
hasInitialized: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [auth.isLoading, auth.isAuthenticated, auth.user, auth.error]);
|
||||||
|
|
||||||
|
// Show loading only on first load, not on subsequent navigations
|
||||||
|
if (!persistentAuthState.hasInitialized && auth.isLoading) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={`${nunito.className} absolute top-4 right-4 rounded-lg bg-white/10 px-3 py-2 backdrop-blur-sm`}
|
||||||
|
>
|
||||||
|
<span className="text-sm">...</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const displayState = persistentAuthState.hasInitialized
|
||||||
|
? persistentAuthState
|
||||||
|
: { isAuthenticated: auth.isAuthenticated, user: auth.user };
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={`${nunito.className} absolute top-4 right-4 flex items-center gap-2 rounded-lg border-4 border-black bg-white/10 px-3 py-2`}
|
||||||
|
>
|
||||||
|
{displayState.isAuthenticated ? (
|
||||||
|
<>
|
||||||
|
<div className="h-2 w-2 rounded-full bg-green-500"></div>
|
||||||
|
<span className="text-sm font-medium">
|
||||||
|
{displayState.user?.profile.name || "Authenticated"}
|
||||||
|
</span>
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
void auth.removeUser();
|
||||||
|
router.push("/");
|
||||||
|
}}
|
||||||
|
className="rounded bg-red-500/20 px-2 py-1 text-xs transition-colors hover:bg-red-500/30"
|
||||||
|
>
|
||||||
|
Uitloggen
|
||||||
|
</button>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<div className="h-2 w-2 rounded-full bg-red-500"></div>
|
||||||
|
<button
|
||||||
|
onClick={() => void auth.signinRedirect()}
|
||||||
|
className="rounded bg-blue-500/20 px-2 py-1 text-xs transition-colors hover:bg-blue-500/30"
|
||||||
|
>
|
||||||
|
Inloggen
|
||||||
|
</button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
import Link from "next/link";
|
||||||
|
import { useAuth } from "react-oidc-context";
|
||||||
|
import { Source_Sans_3, Nunito } from "next/font/google";
|
||||||
|
const source = Source_Sans_3({
|
||||||
|
subsets: ["latin"],
|
||||||
|
});
|
||||||
|
const font = source.className;
|
||||||
|
|
||||||
|
export default function NavigationBar() {
|
||||||
|
const auth = useAuth();
|
||||||
|
if (!auth.isLoading && !auth.error && auth.isAuthenticated) {
|
||||||
|
return (
|
||||||
|
<nav className="absolute right-0 bottom-0 left-0 flex justify-center">
|
||||||
|
<div
|
||||||
|
className={`${font} flex flex-row items-center gap-6 rounded-tl-lg rounded-tr-lg border-4 border-b-0 border-black bg-sky-900 px-4 py-6 text-white`}
|
||||||
|
>
|
||||||
|
<Link href="/nominations">Nomineren</Link>
|
||||||
|
<Link href="/lobby">Start spel (test)</Link>
|
||||||
|
<Link href="/users">Users</Link>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
"use client";
|
||||||
|
import { Fetcher } from "openapi-typescript-fetch";
|
||||||
|
import { getUser } from "./fetch";
|
||||||
|
import { paths, components } from "muzak/data/types";
|
||||||
|
|
||||||
|
const fetcher = Fetcher.for<paths>();
|
||||||
|
const BASE_URL = process.env.BACKEND_HOST || "http://127.0.0.1:8001";
|
||||||
|
|
||||||
|
// Configure the fetcher
|
||||||
|
fetcher.configure({
|
||||||
|
baseUrl: BASE_URL,
|
||||||
|
init: {
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
use: [
|
||||||
|
// Add authentication token to request
|
||||||
|
async (url, init, next) => {
|
||||||
|
const user = getUser();
|
||||||
|
if (user?.access_token) {
|
||||||
|
init.headers = {
|
||||||
|
...init.headers,
|
||||||
|
Authorization: `Bearer ${user.access_token}`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return next(url, init);
|
||||||
|
},
|
||||||
|
// Handle errors
|
||||||
|
async (url, init, next) => {
|
||||||
|
const response = await next(url, init);
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
if (response.status === 401) {
|
||||||
|
console.warn("Authentication failed - token may be expired");
|
||||||
|
}
|
||||||
|
console.error(`API Error: ${response.status} ${response.statusText}`, {
|
||||||
|
url,
|
||||||
|
method: init.method,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return response;
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
export const api = {
|
||||||
|
tracks: {
|
||||||
|
list: fetcher.path("/api/track/").method("get").create(),
|
||||||
|
detail: fetcher.path("/api/track/{id}").method("get").create(),
|
||||||
|
vote: fetcher.path("/api/track/vote").method("get").create(),
|
||||||
|
},
|
||||||
|
artists: {
|
||||||
|
list: fetcher.path("/api/artist").method("get").create(),
|
||||||
|
detail: fetcher.path("/api/artist/{id}").method("get").create(),
|
||||||
|
},
|
||||||
|
albums: {
|
||||||
|
list: fetcher.path("/api/album/").method("get").create(),
|
||||||
|
detail: fetcher.path("/api/album/{id}").method("get").create(),
|
||||||
|
},
|
||||||
|
votes: {
|
||||||
|
list: fetcher.path("/api/vote/").method("get").create(),
|
||||||
|
detail: fetcher.path("/api/vote/{id}").method("get").create(),
|
||||||
|
userVotes: fetcher.path("/api/user/{user_id}/votes").method("get").create(),
|
||||||
|
},
|
||||||
|
users: {
|
||||||
|
list: fetcher.path("/api/user/").method("get").create(),
|
||||||
|
detail: fetcher.path("/api/user/{id}").method("get").create(),
|
||||||
|
},
|
||||||
|
profiles: {
|
||||||
|
detail: fetcher.path("/api/profile/{id}").method("get").create(),
|
||||||
|
backgrounds: {
|
||||||
|
list: fetcher.path("/api/profile/background").method("get").create(),
|
||||||
|
detail: fetcher
|
||||||
|
.path("/api/profile/background/{id}")
|
||||||
|
.method("get")
|
||||||
|
.create(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export type User = components["schemas"]["User"];
|
||||||
|
export type Track = components["schemas"]["Track"];
|
||||||
|
export type Album = components["schemas"]["Album"];
|
||||||
|
export type Vote = components["schemas"]["Vote"];
|
||||||
|
|
||||||
|
export default api;
|
||||||
@@ -8,7 +8,8 @@
|
|||||||
"paths": {
|
"paths": {
|
||||||
"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/*"]
|
||||||
},
|
},
|
||||||
|
|
||||||
"allowJs": false,
|
"allowJs": false,
|
||||||
|
|||||||
Reference in New Issue
Block a user