add good looking lobby ui with websocket support with claude sonnet 3.7

This commit is contained in:
Pablu23
2025-10-07 14:55:30 +02:00
parent 81ad47960a
commit eb6e85a099
19 changed files with 1477 additions and 86 deletions

View File

@@ -0,0 +1,31 @@
CREATE TABLE `lobbys` (
`id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
`host_email` text,
FOREIGN KEY (`host_email`) REFERENCES `users`(`email`) ON UPDATE no action ON DELETE no action
);
--> statement-breakpoint
CREATE TABLE `sessions` (
`id` text PRIMARY KEY NOT NULL,
`access_token` text,
`refresh_token` text,
`user_email` text,
FOREIGN KEY (`user_email`) REFERENCES `users`(`email`) ON UPDATE no action ON DELETE no action
);
--> statement-breakpoint
CREATE TABLE `auth_states` (
`id` text PRIMARY KEY NOT NULL,
`code_verifier` text NOT NULL
);
--> statement-breakpoint
CREATE TABLE `user_in_lobby` (
`user_email` text NOT NULL,
`lobby_id` integer NOT NULL,
PRIMARY KEY(`user_email`, `lobby_id`),
FOREIGN KEY (`user_email`) REFERENCES `users`(`email`) ON UPDATE no action ON DELETE no action,
FOREIGN KEY (`lobby_id`) REFERENCES `lobbys`(`id`) ON UPDATE no action ON DELETE no action
);
--> statement-breakpoint
CREATE TABLE `users` (
`email` text PRIMARY KEY NOT NULL,
`username` text
);

View File

@@ -0,0 +1,215 @@
{
"version": "6",
"dialect": "sqlite",
"id": "6bf15c3f-ef71-4979-b04a-1a93b3274a23",
"prevId": "00000000-0000-0000-0000-000000000000",
"tables": {
"lobbys": {
"name": "lobbys",
"columns": {
"id": {
"name": "id",
"type": "integer",
"primaryKey": true,
"notNull": true,
"autoincrement": true
},
"host_email": {
"name": "host_email",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
}
},
"indexes": {},
"foreignKeys": {
"lobbys_host_email_users_email_fk": {
"name": "lobbys_host_email_users_email_fk",
"tableFrom": "lobbys",
"tableTo": "users",
"columnsFrom": [
"host_email"
],
"columnsTo": [
"email"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
},
"sessions": {
"name": "sessions",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"access_token": {
"name": "access_token",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"refresh_token": {
"name": "refresh_token",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"user_email": {
"name": "user_email",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
}
},
"indexes": {},
"foreignKeys": {
"sessions_user_email_users_email_fk": {
"name": "sessions_user_email_users_email_fk",
"tableFrom": "sessions",
"tableTo": "users",
"columnsFrom": [
"user_email"
],
"columnsTo": [
"email"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
},
"auth_states": {
"name": "auth_states",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"code_verifier": {
"name": "code_verifier",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
},
"user_in_lobby": {
"name": "user_in_lobby",
"columns": {
"user_email": {
"name": "user_email",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"lobby_id": {
"name": "lobby_id",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
}
},
"indexes": {},
"foreignKeys": {
"user_in_lobby_user_email_users_email_fk": {
"name": "user_in_lobby_user_email_users_email_fk",
"tableFrom": "user_in_lobby",
"tableTo": "users",
"columnsFrom": [
"user_email"
],
"columnsTo": [
"email"
],
"onDelete": "no action",
"onUpdate": "no action"
},
"user_in_lobby_lobby_id_lobbys_id_fk": {
"name": "user_in_lobby_lobby_id_lobbys_id_fk",
"tableFrom": "user_in_lobby",
"tableTo": "lobbys",
"columnsFrom": [
"lobby_id"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {
"user_in_lobby_user_email_lobby_id_pk": {
"columns": [
"user_email",
"lobby_id"
],
"name": "user_in_lobby_user_email_lobby_id_pk"
}
},
"uniqueConstraints": {},
"checkConstraints": {}
},
"users": {
"name": "users",
"columns": {
"email": {
"name": "email",
"type": "text",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"username": {
"name": "username",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
}
},
"views": {},
"enums": {},
"_meta": {
"schemas": {},
"tables": {},
"columns": {}
},
"internal": {
"indexes": {}
}
}

View File

@@ -0,0 +1,13 @@
{
"version": "7",
"dialect": "sqlite",
"entries": [
{
"idx": 0,
"version": "6",
"when": 1758887320368,
"tag": "0000_numerous_chronomancer",
"breakpoints": true
}
]
}

694
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -39,7 +39,9 @@
"vite": "^7.0.4"
},
"dependencies": {
"@tailwindcss/vite": "^4.1.14",
"better-sqlite3": "^11.8.0",
"drizzle-orm": "^0.40.0"
"drizzle-orm": "^0.40.0",
"tailwindcss": "^4.1.14"
}
}

1
src/app.css Normal file
View File

@@ -0,0 +1 @@
@import "tailwindcss";

33
src/app.d.ts vendored
View File

@@ -18,4 +18,35 @@ declare global {
}
}
export { };
export interface Player {
id: number;
name: string;
isHost: boolean;
}
export interface GameMode {
id: string;
name: string;
}
export interface Playlist {
id: number;
name: string;
imageUrl: string;
songCount: number;
}
export interface GameSettings {
maxPlayers: number;
gameMode: string;
selectedPlaylist: number;
}
export type WebSocketMessage = {
type: string;
[key: string]: any;
}
export {
};

View File

@@ -0,0 +1,158 @@
<script lang="ts">
import type { GameMode, Playlist, Settings } from '$lib/types';
import PlaylistSelector from './PlaylistSelector.svelte';
let {
settings,
gameModes,
playlists,
isHost,
onUpdate
}: {
settings: Settings;
gameModes: GameMode[];
playlists: Playlist[];
isHost: boolean;
onUpdate: (settings: Settings) => void;
} = $props();
let localSettings = $state<Settings>({
maxPlayers: settings.maxPlayers,
gameMode: settings.gameMode,
selectedPlaylistId: settings.selectedPlaylistId
});
$effect(() => {
// Update local settings when change
localSettings = {
maxPlayers: settings.maxPlayers,
gameMode: settings.gameMode,
selectedPlaylistId: settings.selectedPlaylistId
};
});
function updateMaxPlayers(value: number) {
if (value >= 2 && value <= 16) {
localSettings.maxPlayers = value;
onUpdate(localSettings);
}
}
function updateGameMode(modeId: string) {
localSettings.gameMode = modeId;
onUpdate(localSettings);
}
function updatePlaylist(playlistId: number) {
localSettings.selectedPlaylistId = playlistId;
onUpdate(localSettings);
}
let selectedPlaylist = $derived(
playlists.find((p: Playlist) => p.id === localSettings.selectedPlaylistId)
);
let selectedGameMode = $derived(gameModes.find((m: GameMode) => m.id === localSettings.gameMode));
</script>
<div class="bg-white rounded-lg shadow-sm p-5 border border-gray-100">
<h2 class="text-xl font-semibold text-gray-800 mb-4">Game Settings</h2>
{#if isHost}
<div class="space-y-6">
<div>
<!-- svelte-ignore a11y_label_has_associated_control -->
<label class="block text-sm font-medium text-gray-700 mb-1">Max Players</label>
<div class="flex items-center">
<!-- svelte-ignore a11y_consider_explicit_label -->
<button
class="flex-shrink-0 bg-gray-200 hover:bg-gray-300 text-gray-600 h-10 w-10 rounded-l flex items-center justify-center"
onclick={() => updateMaxPlayers(localSettings.maxPlayers - 1)}
disabled={localSettings.maxPlayers <= 2}
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-5 w-5"
viewBox="0 0 20 20"
fill="currentColor"
>
<path
fill-rule="evenodd"
d="M3 10a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1z"
clip-rule="evenodd"
/>
</svg>
</button>
<span
class="h-10 bg-gray-100 text-gray-800 font-medium px-4 flex items-center justify-center min-w-[50px]"
>
{localSettings.maxPlayers}
</span>
<!-- svelte-ignore a11y_consider_explicit_label -->
<button
class="flex-shrink-0 bg-gray-200 hover:bg-gray-300 text-gray-600 h-10 w-10 rounded-r flex items-center justify-center"
onclick={() => updateMaxPlayers(localSettings.maxPlayers + 1)}
disabled={localSettings.maxPlayers >= 16}
>
<svg
xmlns="http:/www.w3.org/2000/svg"
class="h-5 w-5"
viewBox="0 0 20 20"
fill="currentColor"
>
<path
fill-rule="evenodd"
d="M10 3a1 1 0 011 1v5h5a1 1 0 110 2h-5v5a1 1 0 11-2 0v-5H4a1 1 0 110-2h5V4a1 1 0 011-1z"
clip-rule="evenodd"
/>
</svg>
</button>
</div>
</div>
<div>
<!-- svelte-ignore a11y_label_has_associated_control -->
<label class="block text-sm font-medium text-gray-700 mb-2">Game Mode</label>
<div class="grid grid-cols-1 sm:grid-cols-3 gap-3">
{#each gameModes as mode (mode.id)}
<button
class="p-3 rounded-lg text-center transition-colors {localSettings.gameMode ===
mode.id
? 'bg-blue-100 border-blue-200 text-blue-700'
: 'bg-gray-50 border-gray-100 text-gray-700'} border"
onclick={() => updateGameMode(mode.id)}
>
{mode.name}
</button>
{/each}
</div>
</div>
<div class="mt-4">
<!-- svelte-ignore a11y_label_has_associated_control -->
<label class="block text-sm font-medium text-gray-700 mb-2">Playlist</label>
<PlaylistSelector
{playlists}
selectedPlaylistId={localSettings.selectedPlaylistId}
onSelect={updatePlaylist}
/>
</div>
</div>
{:else}
<p class="text-gray-600 italic mb-4">The host is configuring game settings...</p>
<div class="grid grid-cols-1 sm:grid-cols-3 gap-4">
<div class="bg-gray-50 p-3 rounded-md">
<p class="text-sm text-gray-500 mb-1">Max Players</p>
<p class="font-medium">{settings.maxPlayers}</p>
</div>
<div class="bg-gray-50 p-3 rounded-md">
<p class="text-sm text-gray-500 mb-1">Game Mode</p>
<p class="font-medium">{selectedGameMode?.name || 'Unknown'}</p>
</div>
<div class="bg-gray-50 p-3 rounded-md">
<p class="text-sm text-gray-500 mb-1">Playlist</p>
<p class="font-medium">{selectedPlaylist?.name || 'Unknown'}</p>
</div>
</div>
{/if}
</div>

View File

@@ -0,0 +1,41 @@
<script lang="ts">
import type { Player } from '$lib/types';
let {players = [], maxPlayers = 8 }: { players: Player[], maxPlayers: number} = $props();
</script>
<div class="bg-white rounded-lg shadow-sm p-5 border border-gray-100">
<div class="flex justify-between items-center mb-4">
<h2 class="text-xl font-semibold text-gray-800">Players</h2>
<span class="text-sm text-gray-500">{players.length}/{maxPlayers}</span>
</div>
<ul class="space-y-2">
{#each players as player (player.id)}
<li class="flex items-center p-2 rounded-md {player.isHost ? '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">
{player.name.substring(0, 2).toUpperCase()}
</div>
<span class="ml-2 text-gray-800 font-medium">{player.name}</span>
{#if player.isHost}
<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">
<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 fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm0-2a6 6 0 100-12 6 6 0 000 12z" clip-rule="evenodd" />
</svg>
Host
</span>
{/if}
</li>
{/each}
{#if players.length < maxPlayers}
{#each Array(maxPlayers - players.length) as _}
<li class="p-2 rounded-md border border-dashed border-gray-200 text-center text-gray-400 text-sm">
Waiting for player...
</li>
{/each}
{/if}
</ul>
</div>

View File

@@ -0,0 +1,55 @@
<script lang="ts">
import type { Playlist } from '$lib/types';
const props = $props<{
playlists: Playlist[];
selectedPlaylistId: number;
onSelect: (id: number) => void;
}>();
let searchQuery = $state('');
let filteredPlaylists = $derived(props.playlists.filter((playlist: Playlist) =>
playlist.name.toLowerCase().includes(searchQuery.toLowerCase())
));
</script>
<div class="bg-white rounded-lg shadow-sm p-5 border border-gray-100">
<h2 class="text-xl font-semibold text-gray-800 mb-4">Select Playlist</h2>
<div class="mb-3">
<input
type="text"
placeholder="Search playlists..."
bind:value={searchQuery}
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
/>
</div>
<div class="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-3 max-h-64 overflow-y-auto p-1">
{#each filteredPlaylists as playlist (playlist.id)}
<button
class="flex flex-col items-center text-left p-3 rounded-lg transition-all {props.selectedPlaylistId === playlist.id ? 'bg-blue-50 border-blue-200 border' : 'bg-gray-50 hover:bg-gray-100 border-gray-100 border'}"
onclick={() => props.onSelect(playlist.id)}
>
<div class="w-full aspect-square mb-2 rounded-md overflow-hidden">
<img
src={playlist.imageUrl}
alt={playlist.name}
class="w-full h-full object-cover"
/>
</div>
<div class="w-full">
<h3 class="font-medium text-gray-900 truncate">{playlist.name}</h3>
<p class="text-sm text-gray-500">{playlist.songCount} songs</p>
</div>
</button>
{/each}
{#if filteredPlaylists.length === 0}
<div class="col-span-3 py-8 text-center text-gray-500">
No playlists found matching "{searchQuery}"
</div>
{/if}
</div>
</div>

28
src/lib/types.ts Normal file
View File

@@ -0,0 +1,28 @@
export interface Player {
id: number;
name: string;
isHost: boolean;
}
export interface GameMode {
id: string;
name: string;
}
export interface Playlist {
id: number;
name: string;
imageUrl: string;
songCount: number;
}
export interface Settings {
maxPlayers: number;
gameMode: string;
selectedPlaylistId: number;
}
export interface WebSocketMessage {
type: string;
[key: string]: any;
}

View File

@@ -0,0 +1,83 @@
import type { Player, Settings, WebSocketMessage } from './types';
export function createWebSocketClient() {
let socket: WebSocket | null = null;
let connected = false;
let players: Player[] = [];
let gameSettings: Settings = {
maxPlayers: 8,
gameMode: 'classic',
selectedPlaylistId: 1
};
function connect(url: string): void {
if (socket) socket.close();
socket = new WebSocket(url);
socket.onopen = () => {
connected = true;
};
socket.onclose = () => {
connected = false;
};
socket.onerror = (error) => {
console.error('WebSocket error:', error);
};
socket.onmessage = (event: MessageEvent) => {
try {
const message = JSON.parse(event.data) as WebSocketMessage;
switch (message.type) {
case 'playerJoin':
players = [...players, message.player];
break;
case 'playerLeave':
players = players.filter(p => p.id !== message.playerId);
break;
case 'playerList':
players = message.players;
break;
case 'settingsUpdate':
gameSettings = message.settings;
break;
}
} catch (error) {
console.error('Failed to parse WebSocket message:', error);
}
};
}
function sendMessage(message: WebSocketMessage): void {
if (socket && socket.readyState === WebSocket.OPEN) {
socket.send(JSON.stringify(message));
} else {
console.warn('Cannot send message, WebSocket is not connected');
}
}
function disconnect(): void {
if (socket) {
socket.close();
socket = null;
}
connected = false;
players = [];
}
return {
connected,
players,
gameSettings,
connect,
sendMessage,
disconnect
};
}

View File

@@ -1,6 +1,6 @@
<script lang="ts">
import favicon from '$lib/assets/favicon.svg';
import "../app.css";
let { children } = $props();
</script>

View File

@@ -28,6 +28,8 @@
<form method="POST" action="?/deleteUsers">
<button type="submit">Delete all Users</button>
</form>
<button type="submit" onclick={async () => await goto("/lobby/create")}>Create Lobby</button>
{/if}
<!--
<button

View File

@@ -1,10 +1,10 @@
import { db } from '$lib/server/db';
import { lobbysRelations, lobbysTable, usersInLobby, usersTable } from '$lib/server/db/schema';
import { json } from '@sveltejs/kit';
import { lobbysTable, usersInLobby } from '$lib/server/db/schema';
import { json, type RequestHandler } from '@sveltejs/kit';
import { eq } from 'drizzle-orm';
export async function POST({ request }) {
export const POST: RequestHandler = async ({ request }) => {
const userReq = await request.json();
const userInLobby = (await db.$count(usersInLobby, eq(usersInLobby.userEmail, userReq.email))) > 0

View File

@@ -1,8 +1,8 @@
import { db } from "$lib/server/db";
import { usersTable } from "$lib/server/db/schema";
import { json } from "@sveltejs/kit";
import { json, type RequestHandler } from "@sveltejs/kit";
export async function POST({ request }) {
export const POST: RequestHandler = async ({ request }) => {
const user = await request.json();
const u: typeof usersTable.$inferInsert = {
email: user.email,

View File

@@ -0,0 +1,168 @@
<script lang="ts">
import { onMount, onDestroy } from 'svelte';
import { createWebSocketClient } from '$lib/websocketClient';
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
let wsClient = $state(createWebSocketClient());
// 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.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
}
// Initialize mock data for demo
function initializeMockData() {
// Simulate WebSocket connection and receiving initial player data
setTimeout(() => {
console.log("Init mock data");
wsClient.players = [
{ id: 1, name: 'YourName', isHost: true },
{ id: 2, name: 'Player2', isHost: false },
{ id: 3, name: 'GamerX', isHost: false }
];
wsClient.connected = true;
}, 500);
}
onMount(() => {
// In a real app, connect to WebSocket server
// wsClient.connect('ws://example.com/game-lobby');
// For demo purposes, we'll simulate a connection
initializeMockData();
});
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>
<button
class="ml-2 p-1.5 text-gray-500 hover:text-blue-600 transition-colors"
title="Copy to clipboard"
on:click={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"
on:click={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"
on:click={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,13 +1,16 @@
<script lang="ts">
import type { PageProps } from './$types';
import { onDestroy } from 'svelte';
let { data }: PageProps = $props();
onDestroy(() => console.log('left site'));
</script>
<h1>You are in lobby with ID: {data.lobby?.id}</h1>
<h2>Youre username is: {data.username}</h2>
<h2>Your username is: {data.username}</h2>
<h2>Players in Lobby: </h2>
<h2>Players in Lobby:</h2>
<ul>
{#each data.lobby.usersInLobby as player (player.userEmail)}
<li>{player.user.username}</li>

View File

@@ -1,6 +1,10 @@
import { sveltekit } from '@sveltejs/kit/vite';
import { defineConfig } from 'vite';
import tailwindcss from '@tailwindcss/vite';
export default defineConfig({
plugins: [sveltekit()]
plugins: [
tailwindcss(),
sveltekit()
]
});