now people can join and leave the game

This commit is contained in:
RezHackXYZ 2025-05-12 10:22:19 +05:30
parent bf582efc46
commit f83f50d9b2
No known key found for this signature in database
12 changed files with 408 additions and 22 deletions

2
.env.example Normal file
View file

@ -0,0 +1,2 @@
VITE_SUPABASE_URL=https://yourproject.supabase.co
VITE_SUPABASE_ANON_KEY=your-service-role-key

View file

@ -3,5 +3,6 @@
"editor.inlineSuggest.enabled": true, "editor.inlineSuggest.enabled": true,
"editor.quickSuggestions": { "editor.quickSuggestions": {
"strings": true "strings": true
} },
"cSpell.words": ["SUPABASE"]
} }

135
package-lock.json generated
View file

@ -7,6 +7,9 @@
"": { "": {
"name": "kahootclone", "name": "kahootclone",
"version": "0.0.1", "version": "0.0.1",
"dependencies": {
"@supabase/supabase-js": "^2.49.4"
},
"devDependencies": { "devDependencies": {
"@sveltejs/adapter-auto": "^4.0.0", "@sveltejs/adapter-auto": "^4.0.0",
"@sveltejs/kit": "^2.16.0", "@sveltejs/kit": "^2.16.0",
@ -1000,6 +1003,73 @@
"win32" "win32"
] ]
}, },
"node_modules/@supabase/auth-js": {
"version": "2.69.1",
"resolved": "https://registry.npmjs.org/@supabase/auth-js/-/auth-js-2.69.1.tgz",
"integrity": "sha512-FILtt5WjCNzmReeRLq5wRs3iShwmnWgBvxHfqapC/VoljJl+W8hDAyFmf1NVw3zH+ZjZ05AKxiKxVeb0HNWRMQ==",
"dependencies": {
"@supabase/node-fetch": "^2.6.14"
}
},
"node_modules/@supabase/functions-js": {
"version": "2.4.4",
"resolved": "https://registry.npmjs.org/@supabase/functions-js/-/functions-js-2.4.4.tgz",
"integrity": "sha512-WL2p6r4AXNGwop7iwvul2BvOtuJ1YQy8EbOd0dhG1oN1q8el/BIRSFCFnWAMM/vJJlHWLi4ad22sKbKr9mvjoA==",
"dependencies": {
"@supabase/node-fetch": "^2.6.14"
}
},
"node_modules/@supabase/node-fetch": {
"version": "2.6.15",
"resolved": "https://registry.npmjs.org/@supabase/node-fetch/-/node-fetch-2.6.15.tgz",
"integrity": "sha512-1ibVeYUacxWYi9i0cf5efil6adJ9WRyZBLivgjs+AUpewx1F3xPi7gLgaASI2SmIQxPoCEjAsLAzKPgMJVgOUQ==",
"dependencies": {
"whatwg-url": "^5.0.0"
},
"engines": {
"node": "4.x || >=6.0.0"
}
},
"node_modules/@supabase/postgrest-js": {
"version": "1.19.4",
"resolved": "https://registry.npmjs.org/@supabase/postgrest-js/-/postgrest-js-1.19.4.tgz",
"integrity": "sha512-O4soKqKtZIW3olqmbXXbKugUtByD2jPa8kL2m2c1oozAO11uCcGrRhkZL0kVxjBLrXHE0mdSkFsMj7jDSfyNpw==",
"dependencies": {
"@supabase/node-fetch": "^2.6.14"
}
},
"node_modules/@supabase/realtime-js": {
"version": "2.11.2",
"resolved": "https://registry.npmjs.org/@supabase/realtime-js/-/realtime-js-2.11.2.tgz",
"integrity": "sha512-u/XeuL2Y0QEhXSoIPZZwR6wMXgB+RQbJzG9VErA3VghVt7uRfSVsjeqd7m5GhX3JR6dM/WRmLbVR8URpDWG4+w==",
"dependencies": {
"@supabase/node-fetch": "^2.6.14",
"@types/phoenix": "^1.5.4",
"@types/ws": "^8.5.10",
"ws": "^8.18.0"
}
},
"node_modules/@supabase/storage-js": {
"version": "2.7.1",
"resolved": "https://registry.npmjs.org/@supabase/storage-js/-/storage-js-2.7.1.tgz",
"integrity": "sha512-asYHcyDR1fKqrMpytAS1zjyEfvxuOIp1CIXX7ji4lHHcJKqyk+sLl/Vxgm4sN6u8zvuUtae9e4kDxQP2qrwWBA==",
"dependencies": {
"@supabase/node-fetch": "^2.6.14"
}
},
"node_modules/@supabase/supabase-js": {
"version": "2.49.4",
"resolved": "https://registry.npmjs.org/@supabase/supabase-js/-/supabase-js-2.49.4.tgz",
"integrity": "sha512-jUF0uRUmS8BKt37t01qaZ88H9yV1mbGYnqLeuFWLcdV+x1P4fl0yP9DGtaEhFPZcwSom7u16GkLEH9QJZOqOkw==",
"dependencies": {
"@supabase/auth-js": "2.69.1",
"@supabase/functions-js": "2.4.4",
"@supabase/node-fetch": "2.6.15",
"@supabase/postgrest-js": "1.19.4",
"@supabase/realtime-js": "2.11.2",
"@supabase/storage-js": "2.7.1"
}
},
"node_modules/@sveltejs/acorn-typescript": { "node_modules/@sveltejs/acorn-typescript": {
"version": "1.0.5", "version": "1.0.5",
"resolved": "https://registry.npmjs.org/@sveltejs/acorn-typescript/-/acorn-typescript-1.0.5.tgz", "resolved": "https://registry.npmjs.org/@sveltejs/acorn-typescript/-/acorn-typescript-1.0.5.tgz",
@ -1383,6 +1453,27 @@
"dev": true, "dev": true,
"peer": true "peer": true
}, },
"node_modules/@types/node": {
"version": "22.15.17",
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.17.tgz",
"integrity": "sha512-wIX2aSZL5FE+MR0JlvF87BNVrtFWf6AE6rxSE9X7OwnVvoyCQjpzSRJ+M87se/4QCkCiebQAqrJ0y6fwIyi7nw==",
"dependencies": {
"undici-types": "~6.21.0"
}
},
"node_modules/@types/phoenix": {
"version": "1.6.6",
"resolved": "https://registry.npmjs.org/@types/phoenix/-/phoenix-1.6.6.tgz",
"integrity": "sha512-PIzZZlEppgrpoT2QgbnDU+MMzuR6BbCjllj0bM70lWoejMeNJAxCchxnv7J3XFkI8MpygtRpzXrIlmWUBclP5A=="
},
"node_modules/@types/ws": {
"version": "8.18.1",
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz",
"integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==",
"dependencies": {
"@types/node": "*"
}
},
"node_modules/accepts": { "node_modules/accepts": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz",
@ -4029,6 +4120,11 @@
"node": ">=6" "node": ">=6"
} }
}, },
"node_modules/tr46": {
"version": "0.0.3",
"resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
"integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="
},
"node_modules/type-check": { "node_modules/type-check": {
"version": "0.4.0", "version": "0.4.0",
"resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
@ -4057,6 +4153,11 @@
"node": ">= 0.6" "node": ">= 0.6"
} }
}, },
"node_modules/undici-types": {
"version": "6.21.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="
},
"node_modules/unpipe": { "node_modules/unpipe": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
@ -4205,6 +4306,20 @@
} }
} }
}, },
"node_modules/webidl-conversions": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
"integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="
},
"node_modules/whatwg-url": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
"integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==",
"dependencies": {
"tr46": "~0.0.3",
"webidl-conversions": "^3.0.0"
}
},
"node_modules/which": { "node_modules/which": {
"version": "2.0.2", "version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
@ -4238,6 +4353,26 @@
"dev": true, "dev": true,
"peer": true "peer": true
}, },
"node_modules/ws": {
"version": "8.18.2",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.18.2.tgz",
"integrity": "sha512-DMricUmwGZUVr++AEAe2uiVM7UoO9MAVZMDu05UQOaUII0lp+zOzLLU4Xqh/JvTqklB1T4uELaaPBKyjE1r4fQ==",
"engines": {
"node": ">=10.0.0"
},
"peerDependencies": {
"bufferutil": "^4.0.1",
"utf-8-validate": ">=5.0.2"
},
"peerDependenciesMeta": {
"bufferutil": {
"optional": true
},
"utf-8-validate": {
"optional": true
}
}
},
"node_modules/yallist": { "node_modules/yallist": {
"version": "5.0.0", "version": "5.0.0",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz",

View file

@ -25,5 +25,8 @@
"svelte": "^5.0.0", "svelte": "^5.0.0",
"tailwindcss": "^4.0.0", "tailwindcss": "^4.0.0",
"vite": "^6.0.0" "vite": "^6.0.0"
},
"dependencies": {
"@supabase/supabase-js": "^2.49.4"
} }
} }

View file

@ -1 +0,0 @@
// place files you want to import through the `$lib` alias in this folder.

6
src/lib/supabase.js Normal file
View file

@ -0,0 +1,6 @@
import { createClient } from '@supabase/supabase-js';
export const supabase = createClient(
import.meta.env.VITE_SUPABASE_URL,
import.meta.env.VITE_SUPABASE_ANON_KEY
);

View file

@ -1,27 +1,37 @@
<script> <script>
let questions = $state([ import { supabase } from '$lib/supabase';
import { goto } from '$app/navigation';
let questions = [
{ {
name: '', name: '',
answers: ['', '', '', ''], answers: ['', '', '', ''],
correctAnswer: undefined correctAnswer: undefined
} }
]); ];
function startGame() { async function startGame() {
if (questions.some((question) => question.name === '')) { if (questions.some((q) => q.name === '')) return alert('Please fill all questions');
alert('Please fill in the question for each question.'); if (questions.some((q) => q.answers.some((a) => a === ''))) return alert('Fill all options');
return; if (questions.some((q) => q.correctAnswer === undefined))
} return alert('Select correct answers');
if (questions.some((question) => question.answers.some((answer) => answer === ''))) {
alert('Please fill in each of the options for each question.'); const gamePin = Math.floor(Math.random() * 1000000)
return; .toString()
} .padStart(6, '0');
if (questions.some((question) => question.correctAnswer === undefined)) {
alert('Please select a correct answer for each question.'); const { error } = await supabase.from('games').insert({
gamePIN: gamePin,
gameStatus: 'lobby',
questions: questions,
players: []
});
if (error) {
alert('Failed to create game: ' + error.message + '\n\nPlease try again.');
return; return;
} }
alert('Game started!'); goto('/host/' + gamePin);
} }
</script> </script>

View file

@ -0,0 +1,6 @@
// Example of what your +page.js or +page.server.js might need
export function load({ params }) {
return {
gamePin: params.gamePin
};
}

View file

@ -0,0 +1,78 @@
<script>
import { onMount } from 'svelte';
import { page } from '$app/stores';
import { supabase } from '$lib/supabase';
export let data;
const gamePin = data.gamePin;
const name = $page.state?.name;
let players = [];
// Subscribe to realtime changes in players
function subscribeToPlayers() {
const channel = supabase
.channel(`game-${gamePin}`)
.on(
'postgres_changes',
{
event: 'UPDATE',
schema: 'public',
table: 'games',
filter: `gamePIN=eq.${gamePin}`
},
(payload) => {
players = payload.new.players || [];
}
)
.subscribe();
}
onMount(async () => {
const { data: gameData } = await supabase
.from('games')
.select('players')
.eq('gamePIN', Number(gamePin))
.maybeSingle();
if (gameData?.players) {
players = gameData.players;
}
subscribeToPlayers();
});
</script>
<div class="bg-grey-900 flex h-full items-center justify-center">
<div
class="flex max-w-[700px] flex-col items-center justify-center gap-1 rounded-lg bg-gray-900 p-8 shadow-lg"
>
<h1 class="m-[0] text-9xl">HOSTING</h1>
<h1 class="m-[0] text-7xl">Game Pin:</h1>
<h1 class="m-[0] rounded-2xl bg-gray-700 pt-1.5 pr-2 pb-0 pl-2 font-mono text-5xl">
{gamePin}
</h1>
<h1 class="m-[0] mt-10 text-6xl">Players Joined:</h1>
<h1 class="m-[0] text-4xl text-gray-400">(Total Players: {players.length})</h1>
<div class="mt-2 flex flex-wrap justify-center gap-2">
{#each players as player}
<span class="m-[0] rounded-xl bg-gray-700 pt-1 pr-2 pb-0 pl-2 font-mono text-3xl"
>{player.name}</span
>
{/each}
</div>
<button
class="mt-5 cursor-pointer rounded-2xl bg-green-700 p-2 text-4xl transition-all hover:scale-110 hover:-rotate-10"
on:click={() => {
if (players.length > 0) {
if (confirm('Are you sure you want to start the game?')) {
// Logic to start the game
alert('Game started!'); // Replace with actual game start logic
}
} else {
alert('You need at least one player to start the game.');
}
}}>Start the game</button
>
</div>
</div>

View file

@ -1,19 +1,72 @@
<script> <script>
import { goto } from '$app/navigation'; import { goto } from '$app/navigation';
import { supabase } from '$lib/supabase';
let pin;
let name;
let Checking = false;
async function validateGamePin() {
const { data, error } = await supabase
.from('games')
.select('gamePIN')
.eq('gamePIN', Number(pin))
.maybeSingle();
// Return true if valid
return data !== null && !error;
}
async function joinGame() {
if (!pin || !name) {
alert('Please fill in the game pin and your name.');
return;
}
Checking = true;
const isValid = await validateGamePin();
if (!isValid) {
alert('Invalid game pin. Please try again.');
Checking = false;
return;
}
goto('/play/' + pin, {
state: {
name
}
});
}
</script> </script>
<div class="bg-grey-900 flex h-full items-center justify-center"> <div class="bg-grey-900 flex h-full items-center justify-center">
<div class="flex flex-col items-center justify-center gap-1 rounded-lg bg-gray-900 p-8 shadow-lg"> <div class="flex flex-col items-center justify-center gap-1 rounded-lg bg-gray-900 p-8 shadow-lg">
<h1 class="m-[0] mb-3 text-5xl">Join a game here</h1> <h1 class="m-[0] mb-3 text-5xl">Join a game here</h1>
<input placeholder="Enter game pin" class="rounded-lg bg-gray-800 p-2 text-center text-white" />
<input
placeholder="Enter game pin"
class="rounded-lg bg-gray-800 p-2 text-center text-white"
bind:value={pin}
/>
<input <input
placeholder="Enter your name" placeholder="Enter your name"
bind:value={name}
class="rounded-lg bg-gray-800 p-2 text-center text-white" class="rounded-lg bg-gray-800 p-2 text-center text-white"
/> />
{#if !Checking}
<button <button
class="cursor-pointer rounded-full bg-green-700 p-2 transition-all hover:scale-110 hover:-rotate-10" class="mt-4 cursor-pointer rounded-full bg-green-700 p-2 transition-all hover:scale-110 hover:-rotate-10"
>Join the game</button on:click={joinGame}
> >
Join game
</button>
{:else}<button
class="mt-4 cursor-pointer rounded-full bg-gray-700 p-2 transition-all hover:scale-110 hover:-rotate-10"
>
Checking if pin is valid...
</button>{/if}
</div> </div>
</div> </div>

View file

@ -0,0 +1,6 @@
// Example of what your +page.js or +page.server.js might need
export function load({ params }) {
return {
gamePin: params.gamePin
};
}

View file

@ -0,0 +1,87 @@
<script>
import { onMount } from 'svelte';
import { page } from '$app/stores';
import { supabase } from '$lib/supabase';
export let data;
const gamePin = data.gamePin;
const name = $page.state?.name;
let players = [];
async function addPlayer() {
const { data: gameData, error } = await supabase
.from('games')
.select('players')
.eq('gamePIN', Number(gamePin))
.maybeSingle();
let updatedPlayers = gameData.players || [];
const alreadyExists = updatedPlayers.some((p) => p.name === name);
if (!alreadyExists) {
updatedPlayers.push({ name: name, score: 0 });
const { error: updateError } = await supabase
.from('games')
.update({ players: updatedPlayers })
.eq('gamePIN', Number(gamePin));
}
}
// Subscribe to realtime changes in players
function subscribeToPlayers() {
const channel = supabase
.channel(`game-${gamePin}`)
.on(
'postgres_changes',
{
event: 'UPDATE',
schema: 'public',
table: 'games',
filter: `gamePIN=eq.${gamePin}`
},
(payload) => {
players = payload.new.players || [];
}
)
.subscribe();
}
onMount(async () => {
await addPlayer();
const { data: gameData } = await supabase
.from('games')
.select('players')
.eq('gamePIN', Number(gamePin))
.maybeSingle();
if (gameData?.players) {
players = gameData.players;
}
subscribeToPlayers();
});
</script>
<div class="bg-grey-900 flex h-full items-center justify-center">
<div
class="flex max-w-[700px] flex-col items-center justify-center gap-1 rounded-lg bg-gray-900 p-8 shadow-lg"
>
<h1 class="m-[0] text-9xl">PLAYING</h1>
<h1 class="m-[0] text-7xl">Game Pin:</h1>
<h1 class="m-[0] rounded-2xl bg-gray-700 pt-1.5 pr-2 pb-0 pl-2 font-mono text-5xl">
{gamePin}
</h1>
<h1 class="m-[0] mt-10 text-6xl">Players Joined:</h1>
<h1 class="m-[0] text-4xl text-gray-400">(Total Players: {players.length})</h1>
<div class="mt-2 flex flex-wrap justify-center gap-2">
{#each players as player}
<span class="m-[0] rounded-xl bg-gray-700 pt-1 pr-2 pb-0 pl-2 font-mono text-3xl"
>{player.name}</span
>
{/each}
</div>
</div>
</div>