WIP: Working login, and creation of Lobby, aswell as showing new Users

This commit is contained in:
Pablu23
2025-10-10 16:02:32 +02:00
parent 9a44b91099
commit 9dffac4de3
11 changed files with 334 additions and 277 deletions

22
src/app.d.ts vendored
View File

@@ -5,11 +5,7 @@ declare global {
namespace App { namespace App {
// interface Error {} // interface Error {}
interface Locals { interface Locals {
// user: { user: User | null;
// isLoggedIn: boolean;
// email: string | null;
// username: string | null;
// }
} }
// interface PageData {} // interface PageData {}
// interface PageState {} // interface PageState {}
@@ -18,15 +14,15 @@ declare global {
} }
export interface User { export interface User {
email: string | null; email: string;
username: string | null; username: string;
}
export interface Player {
id: number;
name: string;
isHost: boolean;
} }
//
// export interface Player {
// id: number;
// name: string;
// isHost: boolean;
// }
export interface GameMode { export interface GameMode {
id: string; id: string;

View File

@@ -1,44 +1,50 @@
// import { redirect, type Handle } from '@sveltejs/kit'; import { type Handle, type HandleFetch } from '@sveltejs/kit';
//
// export const handle: Handle = async ({ event, resolve }) => {
// const sessionId = event.cookies.get('session_id');
// let user = {
// isLoggedIn: false,
// email: '',
// username: ''
// };
//
// if (sessionId) {
// const response = await fetch('http://hitstar.xyz/api/user/me', {
// headers: {
// 'Content-Type': 'application/json'
// },
// credentials: 'include'
// });
// console.log(response.status);
// console.log(await response.text());
//
// if (response.status >= 200 && response.status < 300) {
// const uBody = await response.json();
// user = {
// isLoggedIn: true,
// email: uBody.email,
// username: uBody.display_name || 'Unknown username'
// };
// }
// }
//
// if (event.url.pathname.startsWith('/private') && !user.isLoggedIn) {
// redirect(307, '/error');
// } else if (event.url.pathname.startsWith('/api') && !user.isLoggedIn) {
// return new Response(null, { status: 401 });
// }
//
// event.locals.user = user;
// const response = await resolve(event);
// return response;
// };
// export const handleFetch: HandleFetch = async({request, fetch}) => { export const handle: Handle = async ({ event, resolve }) => {
// if (request.url const sessionId = event.cookies.get('session_id');
// } event.locals.user = null;
if (sessionId) {
const request = new Request('http://hitstar.xyz/api/user/me');
request.headers.set(
'cookie',
event.cookies
.getAll()
.filter(({ value }) => value !== '') // account for cookie that got deleted in the current request
.map(({ name, value }) => `${name}=${encodeURIComponent(value)}`)
.join('; ')
);
const response = await fetch(request);
if (response.status >= 200 && response.status < 300) {
const uBody = await response.json();
const user = {
email: uBody.email,
username: uBody.username || 'Unknown username'
};
event.locals.user = user;
}
}
const response = await resolve(event);
return response;
};
// export const handleFetch = (async ({ event, request, fetch }) => {
// console.log("fetch request to " + request.url)
//
// if (request.url.startsWith('/api/')) {
// console.log("rerouting");
// request = new Request(request.url, request);
//
// request.headers.set(
// 'cookie',
// event.cookies
// .getAll()
// .filter(({ value }) => value !== '') // account for cookie that got deleted in the current request
// .map(({ name, value }) => `${name}=${encodeURIComponent(value)}`)
// .join('; ')
// );
// }
// return fetch(request);
// }) satisfies HandleFetch;

View File

@@ -1,7 +1,7 @@
<script lang="ts"> <script lang="ts">
import type { Player } from '$lib/types'; import type { User } from "../../app";
let {players = [], maxPlayers = 8 }: { players: Player[], maxPlayers: number} = $props(); let {players = [], maxPlayers = 8, host }: { players: User[], maxPlayers: number, host: User} = $props();
</script> </script>
<div class="bg-white rounded-lg shadow-sm p-5 border border-gray-100"> <div class="bg-white rounded-lg shadow-sm p-5 border border-gray-100">
@@ -11,14 +11,14 @@
</div> </div>
<ul class="space-y-2"> <ul class="space-y-2">
{#each players as player (player.id)} {#each players as player (player.email)}
<li class="flex items-center p-2 rounded-md {player.isHost ? 'bg-blue-50' : 'bg-gray-50'}"> <li class="flex items-center p-2 rounded-md {player.email === host.email ? 'bg-blue-50' : 'bg-gray-50'}">
<div class="bg-gradient-to-br from-blue-500 to-indigo-600 h-8 w-8 rounded-full flex items-center justify-center text-white font-medium text-sm"> <div class="bg-gradient-to-br from-blue-500 to-indigo-600 h-8 w-8 rounded-full flex items-center justify-center text-white font-medium text-sm">
{player.name.substring(0, 2).toUpperCase()} {player.username.substring(0, 2).toUpperCase()}
</div> </div>
<span class="ml-2 text-gray-800 font-medium">{player.name}</span> <span class="ml-2 text-gray-800 font-medium">{player.username}</span>
{#if player.isHost} {#if player.email === host.email}
<span class="ml-auto flex items-center text-blue-600 text-sm font-medium"> <span class="ml-auto flex items-center text-blue-600 text-sm font-medium">
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 mr-1" viewBox="0 0 20 20" fill="currentColor"> <svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 mr-1" viewBox="0 0 20 20" fill="currentColor">
<path d="M9.504 5.071a.5.5 0 01.992 0l1.5 10a.5.5 0 01-.992 0l-1.5-10zM8 9a1 1 0 100 2h4a1 1 0 100-2H8z" /> <path d="M9.504 5.071a.5.5 0 01.992 0l1.5 10a.5.5 0 01-.992 0l-1.5-10zM8 9a1 1 0 100 2h4a1 1 0 100-2H8z" />

View File

@@ -1,5 +1,5 @@
export interface Player { export interface Player {
id: number; id: string;
name: string; name: string;
isHost: boolean; isHost: boolean;
} }

View File

@@ -1,8 +1,9 @@
import type { User } from '../app';
import type { Player, Settings, WebSocketMessage } from './types'; import type { Player, Settings, WebSocketMessage } from './types';
export class WebsocketClient { export class WebsocketClient {
connected = $state(false); connected = $state(false);
players: Player[] = $state([]); players: User[] = $state([]);
gameSettings: Settings = $state({ gameSettings: Settings = $state({
maxPlayers: 8, maxPlayers: 8,
gameMode: 'classic', gameMode: 'classic',
@@ -40,7 +41,7 @@ export class WebsocketClient {
break; break;
case 'playerLeave': case 'playerLeave':
this.players = this.players.filter((p) => p.id !== message.playerId); this.players = this.players.filter((p) => p.email !== message.email);
break; break;
case 'playerList': case 'playerList':

View File

@@ -0,0 +1,7 @@
import type { PageServerLoad } from './$types';
export const load: PageServerLoad = async ({ locals }) => {
return {
user: locals.user
};
};

View File

@@ -14,7 +14,7 @@
isLoggingIn = true; isLoggingIn = true;
loginError = ''; loginError = '';
goto('/login'); goto('api/auth/login');
} }
function handleLogout() { function handleLogout() {
@@ -23,8 +23,18 @@
goto('/logout'); goto('/logout');
} }
function createLobby() { async function createLobby() {
goto('/lobby/create'); const res = await fetch('/api/lobby/create', {
method: 'POST'
});
if (res.status === 200) {
const lobby = await res.json()
lobbyCode = lobby.id
goto(`/lobby/${lobbyCode}`);
} else {
console.log(await res.text());
}
} }
function joinLobby() { function joinLobby() {
@@ -113,7 +123,7 @@
<button <button
class="w-full py-3 px-4 bg-indigo-600 hover:bg-indigo-700 text-white font-medium rounded-md transition-colors flex items-center justify-center" class="w-full py-3 px-4 bg-indigo-600 hover:bg-indigo-700 text-white font-medium rounded-md transition-colors flex items-center justify-center"
onclick={createLobby} onclick={async () => await createLobby()}
> >
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"

View File

@@ -1,21 +0,0 @@
import type { User } from '../app';
import type { PageLoad } from './$types';
export const load: PageLoad = async ({ fetch }) => {
const response = await fetch('http://hitstar.xyz/api/user/me', {
headers: {
'Content-Type': 'application/json'
}
});
if (response.status >= 200 && response.status < 300) {
const user: User = await response.json();
return {
user
};
} else {
console.log(await response.text());
return {
user: null
}
}
};

View File

@@ -1,151 +0,0 @@
<script lang="ts">
import { onMount, onDestroy } from 'svelte';
import { wsClient } from '$lib/websocketClient.svelte';
import type { Settings, GameMode, Playlist } from '$lib/types';
import PlayerList from '$lib/components/PlayerList.svelte';
import GameSettings from '$lib/components/GameSettings.svelte';
// Create WebSocket client
// Current user state
let isHost = $state(true); // Assume current user is host for this example
let lobbyCode = $state('GAME123');
// Available options for settings
let gameModes = $state<GameMode[]>([
{ id: 'classic', name: 'Classic Mode' },
{ id: 'team', name: 'Team Battle' },
{ id: 'speed', name: 'Speed Round' }
]);
let playlists = $state<Playlist[]>([
{ id: 1, name: 'Pop Hits 2023', imageUrl: 'https://example.com/images/pop2023.jpg', songCount: 25 },
{ id: 2, name: 'Rock Classics', imageUrl: 'https://example.com/images/rock.jpg', songCount: 30 },
{ id: 3, name: '80s Mixtape', imageUrl: 'https://example.com/images/80s.jpg', songCount: 20 },
{ id: 4, name: 'Movie Soundtracks', imageUrl: 'https://example.com/images/movies.jpg', songCount: 15 },
{ id: 5, name: 'Hip Hop Essentials', imageUrl: 'https://example.com/images/hiphop.jpg', songCount: 40 },
{ id: 6, name: 'Indie Discoveries', imageUrl: 'https://example.com/images/indie.jpg', songCount: 35 }
]);
function handleSettingsUpdate(settings: Settings) {
// Send updated settings to other players via websocket
wsClient.gameSettings = settings;
wsClient.sendMessage({
type: 'settingsUpdate',
settings: settings
});
}
function startGame() {
wsClient.sendMessage({
type: 'startGame',
settings: wsClient.gameSettings
});
// In a real app, this would navigate to the game screen
}
function copyLobbyCode() {
navigator.clipboard.writeText(lobbyCode);
// Would show a toast notification in a real app
}
function leaveLobby() {
wsClient.disconnect();
// In a real app, you'd likely redirect to another page here
}
onMount(() => {
wsClient.connect(`ws://localhost/api/lobby?id=${lobbyCode}`);
});
onDestroy(() => {
// Clean up WebSocket connection when component is destroyed
wsClient.disconnect();
});
</script>
<div class="min-h-screen bg-gray-50 py-8 px-4 sm:px-6 lg:px-8">
<div class="max-w-5xl mx-auto">
<!-- Header -->
<div class="mb-8 text-center">
<h1 class="text-3xl font-bold text-gray-900">Game Lobby</h1>
<div class="mt-3 flex items-center justify-center">
<span class="text-gray-600 mr-2">Invite your friends using code:</span>
<span class="font-mono bg-white px-3 py-1.5 rounded-md border border-gray-200 text-blue-600 font-semibold">{lobbyCode}</span>
<!-- svelte-ignore a11y_consider_explicit_label -->
<button
class="ml-2 p-1.5 text-gray-500 hover:text-blue-600 transition-colors"
title="Copy to clipboard"
onclick={copyLobbyCode}
>
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
<path d="M8 3a1 1 0 011-1h2a1 1 0 110 2H9a1 1 0 01-1-1z" />
<path d="M6 3a2 2 0 00-2 2v11a2 2 0 002 2h8a2 2 0 002-2V5a2 2 0 00-2-2 3 3 0 01-3 3H9a3 3 0 01-3-3z" />
</svg>
</button>
</div>
</div>
<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
<!-- Left column: Player list -->
<div class="md:col-span-1">
{#if wsClient.connected}
<PlayerList
players={wsClient.players}
maxPlayers={wsClient.gameSettings.maxPlayers}
/>
{:else}
<div class="bg-white rounded-lg shadow-sm p-5 flex items-center justify-center">
<div class="flex items-center text-amber-600">
<svg class="animate-spin -ml-1 mr-3 h-5 w-5" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
Connecting to lobby...
</div>
</div>
{/if}
</div>
<!-- Right column: Game settings and controls -->
<div class="md:col-span-2 space-y-6">
<GameSettings
settings={wsClient.gameSettings}
gameModes={gameModes}
playlists={playlists}
isHost={isHost}
onUpdate={handleSettingsUpdate}
/>
<!-- Action buttons -->
<div class="flex justify-between">
<button
class="px-4 py-2.5 bg-white border border-gray-300 text-gray-700 rounded-lg hover:bg-gray-50 transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
onclick={leaveLobby}
>
Leave Lobby
</button>
{#if isHost}
<button
class="px-6 py-2.5 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 flex items-center"
onclick={startGame}
>
<span>Start Game</span>
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 ml-1.5" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M10.293 5.293a1 1 0 011.414 0l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414-1.414L12.586 11H5a1 1 0 110-2h7.586l-2.293-2.293a1 1 0 010-1.414z" clip-rule="evenodd" />
</svg>
</button>
{:else}
<div class="text-gray-500 italic self-center flex items-center">
<svg class="animate-pulse h-5 w-5 mr-2 text-blue-400" viewBox="0 0 24 24" fill="currentColor">
<circle cx="12" cy="12" r="10" />
</svg>
Waiting for host to start...
</div>
{/if}
</div>
</div>
</div>
</div>
</div>

View File

@@ -1,29 +1,39 @@
import { db } from "$lib/server/db"; import { redirect } from '@sveltejs/kit';
import { eq } from "drizzle-orm"; import type { PageServerLoad } from './$types';
import type { PageServerLoad } from "./$types"; import type { User } from '../../../app';
import { lobbysTable } from "$lib/server/db/schema"; import type { Settings } from '$lib/types';
import { redirect } from "@sveltejs/kit";
export const load: PageServerLoad = async ({ params, locals }) => { export const load: PageServerLoad = async ({ params, fetch, locals: { user } }) => {
const id = Number(params.id); if (!user) {
redirect(307, '/');
}
const lobby = await db.query.lobbysTable.findFirst({ const response = await fetch(`http://hitstar.xyz/api/lobby/${params.id}`, {
with: { headers: {
usersInLobby: { 'Content-Type': 'application/json'
with: {
user: true
}
}
},
where: eq(lobbysTable.id, id)
})
if (!lobby) {
redirect(307, "/error");
} }
});
return { if (response.status != 200) {
lobby: lobby, redirect(307, '/');
username: locals.user.username }
const {
id,
host,
players,
gameSettings
}: { id: string; host: User; players: User[]; gameSettings: Settings } = await response.json();
console.log('Successful request for lobby');
console.log(id);
return {
user: user,
lobby: {
id,
host,
players,
gameSettings
} }
}; };
};

View File

@@ -1,18 +1,217 @@
<script lang="ts"> <script lang="ts">
import { onMount, onDestroy } from 'svelte';
import { wsClient } from '$lib/websocketClient.svelte';
import type { Settings, GameMode, Playlist } from '$lib/types';
import PlayerList from '$lib/components/PlayerList.svelte';
import GameSettings from '$lib/components/GameSettings.svelte';
import type { PageProps } from './$types'; import type { PageProps } from './$types';
import { onDestroy } from 'svelte';
// Create WebSocket client
// Current user state
let { data }: PageProps = $props(); let { data }: PageProps = $props();
onDestroy(() => console.log('left site')); let lobbyCode = $state(data.lobby.id);
let isHost = $state(data.user.email === data.lobby.host.email); // Assume current user is host for this example
// Available options for settings
let gameModes = $state<GameMode[]>([
{ id: 'classic', name: 'Classic Mode' },
{ id: 'team', name: 'Team Battle' },
{ id: 'speed', name: 'Speed Round' }
]);
let playlists = $state<Playlist[]>([
{
id: 1,
name: 'Pop Hits 2023',
imageUrl: 'https://example.com/images/pop2023.jpg',
songCount: 25
},
{
id: 2,
name: 'Rock Classics',
imageUrl: 'https://example.com/images/rock.jpg',
songCount: 30
},
{ id: 3, name: '80s Mixtape', imageUrl: 'https://example.com/images/80s.jpg', songCount: 20 },
{
id: 4,
name: 'Movie Soundtracks',
imageUrl: 'https://example.com/images/movies.jpg',
songCount: 15
},
{
id: 5,
name: 'Hip Hop Essentials',
imageUrl: 'https://example.com/images/hiphop.jpg',
songCount: 40
},
{
id: 6,
name: 'Indie Discoveries',
imageUrl: 'https://example.com/images/indie.jpg',
songCount: 35
}
]);
function handleSettingsUpdate(settings: Settings) {
// Send updated settings to other players via websocket
wsClient.gameSettings = settings;
wsClient.sendMessage({
type: 'settingsUpdate',
settings: settings
});
}
function startGame() {
wsClient.sendMessage({
type: 'startGame',
settings: wsClient.gameSettings
});
// In a real app, this would navigate to the game screen
}
function copyLobbyCode() {
navigator.clipboard.writeText(lobbyCode);
// Would show a toast notification in a real app
}
function leaveLobby() {
wsClient.disconnect();
// In a real app, you'd likely redirect to another page here
}
onMount(() => {
wsClient.connect(`ws://hitstar.xyz/api/lobby?id=${lobbyCode}`);
wsClient.gameSettings = data.lobby.gameSettings
});
onDestroy(() => {
// Clean up WebSocket connection when component is destroyed
wsClient.disconnect();
});
</script> </script>
<h1>You are in lobby with ID: {data.lobby?.id}</h1> <div class="min-h-screen bg-gray-50 py-8 px-4 sm:px-6 lg:px-8">
<h2>Your username is: {data.username}</h2> <div class="max-w-5xl mx-auto">
<!-- Header -->
<div class="mb-8 text-center">
<h1 class="text-3xl font-bold text-gray-900">Game Lobby</h1>
<div class="mt-3 flex items-center justify-center">
<span class="text-gray-600 mr-2">Invite your friends using code:</span>
<span
class="font-mono bg-white px-3 py-1.5 rounded-md border border-gray-200 text-blue-600 font-semibold"
>{lobbyCode}</span
>
<!-- svelte-ignore a11y_consider_explicit_label -->
<button
class="ml-2 p-1.5 text-gray-500 hover:text-blue-600 transition-colors"
title="Copy to clipboard"
onclick={copyLobbyCode}
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-5 w-5"
viewBox="0 0 20 20"
fill="currentColor"
>
<path d="M8 3a1 1 0 011-1h2a1 1 0 110 2H9a1 1 0 01-1-1z" />
<path
d="M6 3a2 2 0 00-2 2v11a2 2 0 002 2h8a2 2 0 002-2V5a2 2 0 00-2-2 3 3 0 01-3 3H9a3 3 0 01-3-3z"
/>
</svg>
</button>
</div>
</div>
<h2>Players in Lobby:</h2> <div class="grid grid-cols-1 md:grid-cols-3 gap-6">
<ul> <!-- Left column: Player list -->
{#each data.lobby.usersInLobby as player (player.userEmail)} <div class="md:col-span-1">
<li>{player.user.username}</li> {#if wsClient.connected}
{/each} <PlayerList players={wsClient.players} maxPlayers={wsClient.gameSettings.maxPlayers} host={data.lobby.host} />
</ul> {:else}
<div class="bg-white rounded-lg shadow-sm p-5 flex items-center justify-center">
<div class="flex items-center text-amber-600">
<svg
class="animate-spin -ml-1 mr-3 h-5 w-5"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
>
<circle
class="opacity-25"
cx="12"
cy="12"
r="10"
stroke="currentColor"
stroke-width="4"
></circle>
<path
class="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
></path>
</svg>
Connecting to lobby...
</div>
</div>
{/if}
</div>
<!-- Right column: Game settings and controls -->
<div class="md:col-span-2 space-y-6">
<GameSettings
settings={wsClient.gameSettings}
{gameModes}
{playlists}
{isHost}
onUpdate={handleSettingsUpdate}
/>
<!-- Action buttons -->
<div class="flex justify-between">
<button
class="px-4 py-2.5 bg-white border border-gray-300 text-gray-700 rounded-lg hover:bg-gray-50 transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
onclick={leaveLobby}
>
Leave Lobby
</button>
{#if isHost}
<button
class="px-6 py-2.5 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 flex items-center"
onclick={startGame}
>
<span>Start Game</span>
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-5 w-5 ml-1.5"
viewBox="0 0 20 20"
fill="currentColor"
>
<path
fill-rule="evenodd"
d="M10.293 5.293a1 1 0 011.414 0l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414-1.414L12.586 11H5a1 1 0 110-2h7.586l-2.293-2.293a1 1 0 010-1.414z"
clip-rule="evenodd"
/>
</svg>
</button>
{:else}
<div class="text-gray-500 italic self-center flex items-center">
<svg
class="animate-pulse h-5 w-5 mr-2 text-blue-400"
viewBox="0 0 24 24"
fill="currentColor"
>
<circle cx="12" cy="12" r="10" />
</svg>
Waiting for host to start...
</div>
{/if}
</div>
</div>
</div>
</div>
</div>