Merge pull request #3 from RezHackXYZ/new-ui-for-kahhot-clone

New UI for kahhot clone
This commit is contained in:
RezHackXYZ 2025-06-03 16:23:02 +05:30 committed by GitHub
commit 1652045fcd
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
24 changed files with 456 additions and 508 deletions

View file

@ -59,20 +59,28 @@ export let AnswersSymbolAndColorScheme = [
export let DefaultQuestions = [
{
name: "What should you do when you're free?",
answers: ["Do something in real life!", "Play video games", "Code!", "Touch grass!"],
correctAnswer: 2,
timeLimit: 30,
questionText: "What should you do when you're free?",
timeLimit: 15,
type: "SingleAnswer",
options: ["Do something in real life!", "Play video games", "Code!", "Touch grass!"],
CorrectOption: { SingleAnswer: 2 },
hasMedia: false,
mediaURL: null,
},
{
name: "Is RezHackXYZ the best programmer in the world?",
answers: ["Yes :)", "No :("],
correctAnswer: 0,
questionText: "Is RezHackXYZ the best programmer in the world?",
timeLimit: 5,
type: "SingleAnswer",
options: ["Yes :)", "No :("],
CorrectOption: { SingleAnswer: 0 },
hasMedia: true,
mediaURL: "https://github.com/RezHackXYZ.png",
},
{
name: "Best place in the world?",
answers: [
questionText: "Best place in the world?",
timeLimit: 5,
type: "SingleAnswer",
options: [
"Google",
"Microsoft",
"Apple",
@ -82,55 +90,8 @@ export let DefaultQuestions = [
"Facebook",
"Twitter",
],
correctAnswer: 4,
timeLimit: 120,
CorrectOption: { SingleAnswer: 4 },
hasMedia: false,
mediaURL: null,
},
];
export let AiPrompts = {
GenerateQuestionsUsingAI: `
You are the AI of a quiz game.
Generate a list of quiz questions with possible answers and the correct answer index.
Each question must have:
- A "name" (question text)
- An "answers" array (minimum 2, maximum 8 options)
- A "correctAnswer" (index starting from 0)
Ensure the questions are diverse.
Example format:
{
"name": "What is the capital of France?",
"answers": [
"Paris",
"London",
"Berlin",
"Madrid"
],
"correctAnswer": 0
}
JUST PROVIDE THE JSON AND NOTHING ELSE.
The user's topic of interest is:
[topic]`,
GenerateOptionsUsingAI: `
You are the AI of a quiz game.
Generate a list of answers relevant to the Question the correct answer index.
generate 2 things for the question:
- An "answers" array (minimum 2, maximum 8 options)
- A "correctAnswer" (index starting from 0)
Ensure the questions are diverse.
Example format if the question is "What is the capital of France?":
{
"answers": [
"Paris",
"London",
"Berlin",
"Madrid"
],
"correctAnswer": 0
}
JUST PROVIDE THE JSON AND NOTHING ELSE.
The user's Question that they want to generate options for is:
[question]
`,
};

View file

@ -1,2 +0,0 @@
import JSConfetti from "js-confetti";
const jsConfetti = new JSConfetti();

View file

@ -1,28 +1,12 @@
<script>
import UseDemoQuestions from "./components/buttons/UseDemoQuestions.svelte";
import NewQuestion from "./components/buttons/NewQuestion.svelte";
import StartGame from "./components/buttons/StartGame.svelte";
import Question from "./components/Questions/question.svelte";
import { questions, Wait } from "./logic/GameCreateData.svelte.js";
import WaitStartGame from "./components/buttons/WaitStartGame.svelte";
import GenerateQuetionsUsingAi from "./components/buttons/GenerateQuetionsUsingAI.svelte";
import HowTheQuestionWillLook from "./HowTheQuestionWillLook.svelte";
import QuestionsList from "./QuestionsList.svelte";
import QuestionOptions from "./QuestionOptions.svelte";
import Buttons from "./Buttons.svelte";
</script>
<div class="bg-grey-900 flex justify-center p-5">
<div
class="flex flex-col items-center justify-center gap-1 rounded-lg bg-gray-900 p-8 shadow-lg"
>
<div class="flex gap-3"><UseDemoQuestions /> <GenerateQuetionsUsingAi /></div>
{#each questions.v as question, index}
<Question {index} />
{/each}
<div class="flex gap-3">
<NewQuestion />
{#if Wait.v == false}
<StartGame />
{:else}
<WaitStartGame />
{/if}
</div>
</div>
<div class="flex h-full justify-between">
<QuestionsList />
<div class="flex h-full w-[54%] flex-col gap-2"><HowTheQuestionWillLook /><Buttons /></div>
<QuestionOptions />
</div>

View file

@ -0,0 +1,99 @@
<script>
import toast from "svelte-5-french-toast";
import { DefaultQuestions } from "$lib/config.js";
import { QuestionsData, wait } from "./create.svelte";
import { createGame } from "./createGame.js";
let userInput = "";
let AIPrompt = `
You are the AI of a quiz game.
Generate a list of quiz questions with possible answers and the correct answer index.
Each question must have:
- A "questionText" (what the question is about)
- A "timeLimit" (in seconds, any of these: null (no time limit), 5, 10, 15, 30, 60, 120, 300)
- An "type" (only set to "SingleAnswer" for now)
- An "options" (an array of options with at least 2 and at most 8 options)
- A "CorrectOption" (an object with a key "SingleAnswer" and a value that is the index of the correct answer)
- A "hasMedia" (boolean indicating if the question has media) only set to true if you are sure the URL is valid and related to the question.
- A "mediaURL" (a URL to the media, can be null if no media is present) only add this if you are sure the URL is valid and related to the question.
Ensure the questions are diverse.
Example format:
[{"questionText":"What should you do when you're free?","timeLimit":15,"type":"SingleAnswer","options":["Do something in real life!","Play video games","Code!","Touch grass!"],"CorrectOption":{"SingleAnswer":2},"hasMedia":false,"mediaURL":null},{"questionText":"Is RezHackXYZ the best programmer in the world?","timeLimit":5,"type":"SingleAnswer","options":["Yes :)","No :("],"CorrectOption":{"SingleAnswer":0},"hasMedia":true,"mediaURL":"https://github.com/RezHackXYZ.png"},{"questionText":"Best place in the world?","timeLimit":5,"type":"SingleAnswer","options":["Google","Microsoft","Apple","Samsung","Hack Club!! :D","Amazon","Facebook","Twitter"],"CorrectOption":{"SingleAnswer":4},"hasMedia":false,"mediaURL":null}]
JUST PROVIDE THE JSON AND NOTHING ELSE.
The user's topic of interest is:
[topic]
`;
async function ApiCall() {
const response = await fetch("https://ai.hackclub.com/chat/completions", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
messages: [
{
role: "user",
content: AIPrompt.replace("[topic]", userInput),
},
],
}),
});
const data = await response.json();
try {
QuestionsData.v = JSON.parse(data.choices[0].message.content);
} catch (error) {
if (ParsingTry <= 5) {
ParsingTry++;
return ApiCall();
}
throw new Error("Failed to parse AI response after multiple attempts.");
}
}
let ParsingTry = 0;
async function GenerateQuestionsUsingAI() {
ParsingTry = 0;
userInput = prompt(
"Enter the topic and number of questions you want with any instructions for the ai, note: doing this will delete all you previous questions and its not undo able",
);
await toast.promise(
(async () => {
try {
await ApiCall();
} catch (error) {
throw new Error(error);
}
})(),
{
loading: "Generating...",
success: "Questions and Answers Generated and added!",
error: (err) => `Could not generate..\nError: ${err.message}\n\n Please try again.`,
},
);
}
</script>
<div class="flex w-full justify-center gap-2 overflow-y-auto rounded border-2 p-3 pr-5">
<button class="btn flex items-center gap-1" onclick={() => (QuestionsData.v = DefaultQuestions)}>
<i class="nf nf-md-flash"></i> Use Demo Questions
</button>
<button class="btn flex items-center gap-1" onclick={() => GenerateQuestionsUsingAI()}>
<i class="nf nf-md-cpu_64_bit"></i> Generate Questions Using AI
</button>
{#if wait.v == true}
<button class="btn dull flex cursor-not-allowed items-center gap-1" disabled>
<i class="nf nf-md-play"></i> Creating Game
</button>
{:else}
<button class="btn flex items-center gap-1" onclick={() => createGame()}>
<i class="nf nf-md-play"></i> Start Game
</button>
{/if}
</div>

View file

@ -0,0 +1,57 @@
<script>
import { AnswersSymbolAndColorScheme } from "$lib/config.js";
import { QuestionsData, selectedQuestionIndex } from "./create.svelte.js";
</script>
<div class="flex h-full w-full flex-col gap-2 overflow-y-auto rounded border-2 p-3 pr-5">
<h1 class="text-4xl">How Question number {selectedQuestionIndex.v + 1} will look</h1>
<div class="grid h-full place-items-center">
<div class="card w-4/5">
<h1 class="flex flex-col text-3xl">
<span class="text-5xl">{selectedQuestionIndex.v + 1}.</span>
{QuestionsData.v[selectedQuestionIndex.v].questionText}
</h1>
{#if QuestionsData.v[selectedQuestionIndex.v].mediaURL && QuestionsData.v[selectedQuestionIndex.v].hasMedia}
<div class="flex justify-center">
<div class="mt-1 w-fit rounded border-2">
{#if QuestionsData.v[selectedQuestionIndex.v].mediaURL.match(/\.(mp4|webm|ogg|mov|avi|mkv)$/i)}
<!-- svelte-ignore a11y_media_has_caption -->
<video
src={QuestionsData.v[selectedQuestionIndex.v].mediaURL}
controls
class="h-75 rounded"
></video>
{:else}
<img
src={QuestionsData.v[selectedQuestionIndex.v].mediaURL}
alt="Question media"
class="h-75 rounded"
/>{/if}
</div>
</div>
{/if}
<div class="mt-5 grid grid-cols-2 gap-5 gap-x-3">
{#each QuestionsData.v[selectedQuestionIndex.v].options as question, questionIndex}
<div class="flex">
<span
style="
--border-color: {AnswersSymbolAndColorScheme[questionIndex].Color};
--bg-color: {AnswersSymbolAndColorScheme[questionIndex].Color};
--border-color-checked: {AnswersSymbolAndColorScheme[questionIndex].SelectedColor};
--bg-color-checked: {AnswersSymbolAndColorScheme[questionIndex].SelectedColor};
--border-color-hover: {AnswersSymbolAndColorScheme[questionIndex].HoverBorderColor};
--border-color-checked: {AnswersSymbolAndColorScheme[questionIndex].SelectedBorderColor};
--border-color-hover: {AnswersSymbolAndColorScheme[questionIndex].HoverBorderColor};
"
class="w-full cursor-pointer rounded-lg border-[5px] border-[var(--border-color)] bg-[var(--bg-color)] pt-1 pr-2 pb-1 pl-2 text-center text-3xl transition-all peer-checked:border-[var(--border-color-checked)] peer-checked:bg-[var(--bg-color-checked)] hover:border-[var(--border-color-hover)]"
>
<i class="nf {AnswersSymbolAndColorScheme[questionIndex].Symbol}"></i>
{question}
</span>
</div>
{/each}
</div>
</div>
</div>
</div>

View file

@ -0,0 +1,183 @@
<script>
import toast from "svelte-5-french-toast";
import { supabase } from "$lib/supabase.js";
import { QuestionsData, selectedQuestionIndex } from "./create.svelte.js";
let files;
export async function UpLoadFiles(file) {
if (!file) {
toast.error("Please select a file to upload first.");
return;
}
const fileExt = file.name.split(".").pop();
const fileName = `${Date.now()}.${fileExt}`;
const filePath = `${fileName}`;
const uploadPromise = supabase.storage.from("useruploadedcontent").upload(filePath, file);
const result = await toast.promise(uploadPromise, {
loading: "Uploading...",
success: "Upload successful!",
error: (error) => `Upload failed. ${error.message} Please try again.`,
});
if (result.error) {
toast.error("Upload error:" + result.error.message);
return;
}
// Retrieve public URL
const { data: publicData } = supabase.storage
.from("useruploadedcontent")
.getPublicUrl(filePath);
return publicData.publicUrl;
}
</script>
<div
class="flex h-full w-[30%] flex-col justify-between gap-2 overflow-y-auto rounded-l border-2 p-3 pr-5"
>
<div>
<h1 class="text-4xl">Options</h1>
<div class="mt-4">
<label for="QuetionText" class="text-lg leading-0.5 text-gray-500">What the Question?</label>
<input
type="text"
id="QuetionText"
placeholder="Whats the capital of france?"
bind:value={QuestionsData.v[selectedQuestionIndex.v].questionText}
class="input mt-1"
/>
</div>
<div class="mt-3">
<label for="TimeLimit" class="text-lg leading-0.5 text-gray-500">Time limit?</label>
<select
name="TimeLimit"
id="TimeLimit"
class="input mt-1"
bind:value={QuestionsData.v[selectedQuestionIndex.v].timeLimit}
>
<option value={null}>infinite</option>
<option value={5}>5 sec</option>
<option value={10}>10 sec</option>
<option value={15}>15 sec</option>
<option value={30}>30 sec</option>
<option value={60}>1 min</option>
<option value={120}>2 min</option>
<option value={300}>5 min</option>
</select>
</div>
<div class="mt-3">
<label for="QuestionType" class="text-lg leading-0.5 text-gray-500">Question type?</label>
<select
name="QuestionType"
id="QuestionType"
class="input mt-1"
bind:value={QuestionsData.v[selectedQuestionIndex.v].type}
>
<option value="SingleAnswer">Single Answer</option>
</select>
</div>
<div class="mt-3">
<label for="AddSomeMedia" class="text-lg leading-0.5 text-gray-500">Add Some Media?</label>
<input type="checkbox" bind:checked={QuestionsData.v[selectedQuestionIndex.v].hasMedia} />
{#if QuestionsData.v[selectedQuestionIndex.v].hasMedia}
<input
type="file"
class="sr-only"
bind:files
onchange={async () => {
QuestionsData.v[selectedQuestionIndex.v].mediaURL = await UpLoadFiles(files[0]);
}}
accept="image/*,video/*"
/>
<label for="media-upload" class="btn dull mt-2 cursor-pointer">
{QuestionsData.v[selectedQuestionIndex.v].mediaURL
? "choose different file"
: "upload files"}
</label>
<input
id="media-upload"
type="file"
class="hidden"
bind:files
onchange={async () => {
QuestionsData.v[selectedQuestionIndex.v].mediaURL = await UpLoadFiles(files[0]);
}}
accept="image/*,video/*"
/>
{/if}
</div>
<div class="mt-3">
{#if "SingleAnswer" === "SingleAnswer"}
<span class="text-lg leading-0.5 text-gray-500">Options</span>
{#each QuestionsData.v[selectedQuestionIndex.v].options as Option, index}
<div class="mt-1 grid grid-cols-1 gap-2">
<div class="flex items-center gap-2">
<input
type="radio"
name="options"
class="ratio"
value={index}
bind:group={QuestionsData.v[selectedQuestionIndex.v].CorrectOption.SingleAnswer}
/>
<input type="text" placeholder="Option 1" bind:value={Option} class="input w-1/3" />
<button
class="btn slim dull"
aria-label="Delete Option"
onclick={() => {
if (QuestionsData.v[selectedQuestionIndex.v].options.length <= 2) {
toast.error("You need to have a minimum of 2 options");
} else {
QuestionsData.v[selectedQuestionIndex.v].options.splice(index, 1);
}
}}
>
<i class="nf nf-md-delete"></i>
</button>
</div>
</div>
{/each}
<div class="flex justify-center">
<button
class="btn dull mt-2 w-fit"
onclick={() => {
if (QuestionsData.v[selectedQuestionIndex.v].options.length >= 8) {
toast.error("You can only have a max of 8 options");
} else {
QuestionsData.v[selectedQuestionIndex.v].options.push("");
}
}}>Add Option</button
>
</div>
{/if}
</div>
</div>
<div>
<div class="mt-4">
<label for="QuetionText" class="text-lg leading-0.5 text-gray-500">Danger</label>
<button
class="btn dull mt-1 w-full"
onclick={() => {
if (QuestionsData.v.length <= 1) {
toast.error("You need to have at least 1 question");
} else if (confirm("Are you sure you want to delete this question?")) {
QuestionsData.v.splice(selectedQuestionIndex.v, 1);
selectedQuestionIndex.v = Math.max(0, selectedQuestionIndex.v - 1);
toast.success("Question deleted successfully");
}
}}>delete this question</button
>
</div>
</div>
</div>

View file

@ -0,0 +1,31 @@
<script>
import { QuestionsData, selectedQuestionIndex } from "./create.svelte.js";
</script>
<div class="flex h-full w-[15%] flex-col gap-2 overflow-y-auto rounded-r border-2 p-3 pr-5">
<h1 class="text-4xl">Questions</h1>
{#each QuestionsData.v as question, questionIndex}
<button
class="card flex flex-col text-left"
onclick={() => (selectedQuestionIndex.v = questionIndex)}
>
<span class="text-3xl">{questionIndex + 1}.</span><span>{question.questionText}</span>
</button>
{/each}
<div class="mt-2 flex justify-center">
<button
class="btn"
onclick={() => {
QuestionsData.v.push({
questionText: "",
timeLimit: 15,
type: "SingleAnswer",
options: ["", "", "", ""],
CorrectOption: { SingleAnswer:null},
hasMedia: false,
mediaURL: null,
});
}}><i class="nf nf-oct-diff_added"></i> New Question</button
>
</div>
</div>

View file

@ -1,23 +0,0 @@
<script>
import { questions } from '../../logic/GameCreateData.svelte.js';
let props = $props();
let questionsIndex = props.questionsIndex;
let index = props.answersIndex;
</script>
<div class="flex items-center gap-2">
<input
type="radio"
value={index}
name={index.toString()}
bind:group={questions.v[questionsIndex].correctAnswer}
class="input"
/>
<input
placeholder="Option {index + 1}"
bind:value={questions.v[questionsIndex].answers[index]}
class="w-[500px] rounded-lg bg-gray-800 p-1 text-center text-white"
/>
</div>

View file

@ -1,79 +0,0 @@
<script>
import DeleteQuestion from "../buttons/DeleteQuestion.svelte";
import GenerateOptionsUsingAI from "../buttons/GenerateOptionsUsingAI.svelte";
import Answers from "./answers.svelte";
import { questions } from "../../logic/GameCreateData.svelte.js";
import { UpLoadFiles } from "../../logic/UpLoadFiles.js";
let props = $props();
let index = props.index;
let files = $state();
</script>
<div class="flex items-center gap-3">
<div class="mb-3 flex flex-col items-center justify-center gap-1 rounded-2xl bg-gray-600 p-2">
<div class="flex h-fit items-center gap-3">
<h1 class="mt-2 mb-3 text-2xl">Q{index + 1}.</h1>
<input
type="text"
bind:value={questions.v[index].name}
placeholder="Question {index + 1}"
class="h-fit w-[500px] rounded-xl bg-gray-800 p-1 text-center text-2xl text-white"
/>
<select
bind:value={questions.v[index].answers.length}
onchange={(e) => {
const newLength = questions.v[index].answers.length;
const currentAnswers = questions.v[index].answers;
if (newLength > currentAnswers.length) {
// Add more answers
while (questions.v[index].answers.length < newLength) {
questions.v[index].answers.push("");
}
} else if (newLength < currentAnswers.length) {
// Remove excess answers
questions.v[index].answers = currentAnswers.slice(0, newLength);
}
}}
class="h-fit rounded-xl bg-gray-800 p-1 text-center text-white"
>
<option disabled selected>Options</option>
{#each Array(7) as _, i}
<option value={i + 2}>{i + 2}</option>
{/each}
</select>
<select
bind:value={questions.v[index].timeLimit}
class="h-fit rounded-xl bg-gray-800 p-1 text-center text-white"
>
<option disabled selected>Time Limit</option>
<option value={null}>infinite</option>
<option value={5}>5 sec</option>
<option value={10}>10 sec</option>
<option value={15}>15 sec</option>
<option value={30}>30 sec</option>
<option value={60}>1 min</option>
<option value={120}>2 min</option>
<option value={300}>5 min</option>
</select>
<DeleteQuestion {index} />
</div>
<div class="flex flex-col gap-2">
{#each questions.v[index].answers as _, answersIndex}
<Answers questionsIndex={index} {answersIndex} />
{/each}
</div>
<GenerateOptionsUsingAI {index} />
<input
type="file"
onchange={async () => {
questions.v[index].media = await UpLoadFiles(files[0]);
}}
bind:files
accept="image/*,video/*"
/>
</div>
</div>

View file

@ -1,20 +0,0 @@
<script>
import { DeleteQuestion } from '../../logic/GameCreateData.svelte.js';
let props = $props();
</script>
<button
onclick={() => DeleteQuestion(props.index)}
class="flex h-fit cursor-pointer items-center justify-center rounded-xl bg-red-700 p-2 transition-all hover:scale-110 hover:-rotate-10"
><svg
xmlns="http://www.w3.org/2000/svg"
height="24px"
viewBox="0 -960 960 960"
width="24px"
fill="#FFFFFF"
><path
d="M280-120q-33 0-56.5-23.5T200-200v-520h-40v-80h200v-40h240v40h200v80h-40v520q0 33-23.5 56.5T680-120H280Zm400-600H280v520h400v-520ZM360-280h80v-360h-80v360Zm160 0h80v-360h-80v360ZM280-720v520-520Z"
/></svg
>Delete question</button
>

View file

@ -1,19 +0,0 @@
<script>
import { GenerateOptionsUsingAI } from "../../logic/GenerateOptionsUsingAI.js";
import { questions } from "../../logic/GameCreateData.svelte.js";
let props = $props();
let index = props.index;
</script>
{#if questions.v[index].answers.every((answer) => answer === "")}
<button
onclick={() => {
GenerateOptionsUsingAI(index);
}}
class="mt-1 mb-1 flex h-fit cursor-pointer items-center justify-center gap-2 rounded-xl bg-blue-700 p-2 transition-all hover:scale-110 hover:-rotate-5"
>
<i class="nf nf-cod-sparkle"></i>
Generate Options using AI
</button>
{/if}

View file

@ -1,14 +0,0 @@
<script>
import { GenerateQuestionsUsingAI } from "../../logic/GenerateQuestionsUsingAI.js";
import { questions } from "../../logic/GameCreateData.svelte.js";
</script>
{#if questions.v.length === 0 || (questions.v.length === 1 && questions.v[0].name === "" && questions.v[0].answers.every((answer) => answer === "") && questions.v[0].correctAnswer === undefined)}
<button
onclick={GenerateQuestionsUsingAI}
class="-mt-5 mb-3 flex h-fit cursor-pointer items-center justify-center gap-2 rounded-xl bg-blue-700 p-2 transition-all hover:scale-110 hover:-rotate-5"
>
<i class="nf nf-cod-sparkle"></i>
Generate questions using AI
</button>
{/if}

View file

@ -1,15 +0,0 @@
<script>
import { AddQuestion } from '../../logic/GameCreateData.svelte.js';
</script>
<button
onclick={() => AddQuestion()}
class="flex h-fit cursor-pointer items-center justify-center rounded-xl bg-green-700 p-2 transition-all hover:scale-110 hover:-rotate-10"
><svg
xmlns="http://www.w3.org/2000/svg"
height="24px"
viewBox="0 -960 960 960"
width="24px"
fill="#FFFFFF"><path d="M440-440H200v-80h240v-240h80v240h240v80H520v240h-80v-240Z" /></svg
>New question</button
>

View file

@ -1,18 +0,0 @@
<script>
import { startGame } from '../../logic/StartGame.js';
</script>
<button
onclick={startGame}
class="flex h-fit cursor-pointer items-center justify-center gap-1 rounded-xl bg-green-700 p-2 transition-all hover:scale-110 hover:-rotate-10"
><svg
xmlns="http://www.w3.org/2000/svg"
height="24px"
viewBox="0 -960 960 960"
width="24px"
fill="#FFFFFF"
><path
d="M560-360q17 0 29.5-12.5T602-402q0-17-12.5-29.5T560-444q-17 0-29.5 12.5T518-402q0 17 12.5 29.5T560-360Zm-30-128h60q0-29 6-42.5t28-35.5q30-30 40-48.5t10-43.5q0-45-31.5-73.5T560-760q-41 0-71.5 23T446-676l54 22q9-25 24.5-37.5T560-704q24 0 39 13.5t15 36.5q0 14-8 26.5T578-596q-33 29-40.5 45.5T530-488ZM320-240q-33 0-56.5-23.5T240-320v-480q0-33 23.5-56.5T320-880h480q33 0 56.5 23.5T880-800v480q0 33-23.5 56.5T800-240H320Zm0-80h480v-480H320v480ZM160-80q-33 0-56.5-23.5T80-160v-560h80v560h560v80H160Zm160-720v480-480Z"
/></svg
>Start Quiz</button
>

View file

@ -1,15 +0,0 @@
<script>
import { SetQuestionsToDemoQuestions } from '../../logic/GameCreateData.svelte.js';
</script>
<button
onclick={() => SetQuestionsToDemoQuestions()}
class="-mt-5 mb-3 flex h-fit cursor-pointer items-center justify-center rounded-xl bg-green-700 p-2 transition-all hover:scale-110 hover:-rotate-10"
><svg
xmlns="http://www.w3.org/2000/svg"
height="24px"
viewBox="0 -960 960 960"
width="24px"
fill="#FFFFFF"><path d="M440-440H200v-80h240v-240h80v240h240v80H520v240h-80v-240Z" /></svg
>Use demo questions</button
>

View file

@ -1,18 +0,0 @@
<script>
import { startGame } from "../../logic/StartGame.js";
</script>
<button
onclick={startGame}
class="flex h-fit cursor-pointer items-center justify-center gap-1 rounded-xl bg-gray-700 p-2 transition-all hover:scale-110"
><svg
xmlns="http://www.w3.org/2000/svg"
height="24px"
viewBox="0 -960 960 960"
width="24px"
fill="#FFFFFF"
><path
d="M560-360q17 0 29.5-12.5T602-402q0-17-12.5-29.5T560-444q-17 0-29.5 12.5T518-402q0 17 12.5 29.5T560-360Zm-30-128h60q0-29 6-42.5t28-35.5q30-30 40-48.5t10-43.5q0-45-31.5-73.5T560-760q-41 0-71.5 23T446-676l54 22q9-25 24.5-37.5T560-704q24 0 39 13.5t15 36.5q0 14-8 26.5T578-596q-33 29-40.5 45.5T530-488ZM320-240q-33 0-56.5-23.5T240-320v-480q0-33 23.5-56.5T320-880h480q33 0 56.5 23.5T880-800v480q0 33-23.5 56.5T800-240H320Zm0-80h480v-480H320v480ZM160-80q-33 0-56.5-23.5T80-160v-560h80v560h560v80H160Zm160-720v480-480Z"
/></svg
>Wait for game to be created</button
>

View file

@ -0,0 +1,15 @@
export let selectedQuestionIndex = $state({ v: 0 });
export let QuestionsData = $state({
v: [
{
questionText: "",
timeLimit: 15,
type: "SingleAnswer",
options: ["", "", "", ""],
CorrectOption: { SingleAnswer: null },
hasMedia: false,
mediaURL: null
},
],
});
export let wait = $state({ v: false });

View file

@ -1,9 +1,24 @@
import { supabase } from "$lib/supabase";
import { supabase } from "$lib/supabase.js";
import { QuestionsData, wait } from "./create.svelte.js";
import toast from "svelte-5-french-toast";
import { Wait } from "./GameCreateData.svelte.js";
export async function createGame(questions, gamePin) {
// Insert game
export async function createGame() {
if (wait.v) {
return;
}
wait.v = true;
const gamePin = Math.floor(Math.random() * 1000000)
.toString()
.padStart(6, "0");
const questionsData = QuestionsData.v.map((q) => ({
gameid: gamePin,
questionstext: q.questionText,
correctanswer: q.CorrectOption.SingleAnswer,
timeLimit: q.timelimit,
media: q.hasMedia ? q.mediaURL : null,
}));
const insertGamePromise = supabase.from("games").insert({
creator: "anonymous",
creationdate: new Date().toISOString(),
@ -19,19 +34,10 @@ export async function createGame(questions, gamePin) {
});
if (gameError) {
Wait.v = false;
wait.v = false;
return;
}
// Prepare questions and answers for batch insertion
const questionsData = questions.map((q) => ({
gameid: gamePin,
questionstext: q.name,
correctanswer: q.correctAnswer,
timelimit: q.timeLimit,
media: q.media || null,
}));
const insertQuestionsPromise = supabase.from("questions").insert(questionsData).select("id");
const { data: questionsResult, error: questionsError } = await toast.promise(
@ -47,14 +53,13 @@ export async function createGame(questions, gamePin) {
);
if (questionsError) {
Wait.v = false;
wait.v = false;
return;
}
const answersData = [];
questionsResult.forEach((question, index) => {
questions[index].answers.forEach((answer) => {
QuestionsData.v[index].options.forEach((answer) => {
answersData.push({
questionid: question.id,
content: answer,
@ -72,9 +77,10 @@ export async function createGame(questions, gamePin) {
});
if (answersError) {
Wait.v = false
wait.v = false;
return;
}
window.location.href = `/kahootclone/host?gamepin=${gamePin}`;
wait.v = false;
}

View file

@ -1,38 +0,0 @@
import { DefaultQuestions } from "$lib/config.js";
import toast from "svelte-5-french-toast";
export let Wait = $state({ v: false });
export let questions = $state({
v: [
{
name: "",
answers: ["", "", "", ""],
correctAnswer: undefined,
timeLimit: 30,
},
],
});
export function SetQuestionsToDemoQuestions() {
questions.v = DefaultQuestions;
}
export function AddQuestion() {
questions.v.push({
name: "",
answers: ["", "", "", ""],
correctAnswer: undefined,
timeLimit: 30,
});
}
export function DeleteQuestion(index) {
if (questions.v.length > 1) {
if (confirm("Are you sure you want to delete this question? You cant undo this.")) {
questions.v.splice(index, 1);
}
} else {
toast.error("You need at least one question.");
}
}

View file

@ -1,43 +0,0 @@
import { questions } from "./GameCreateData.svelte.js";
import { AiPrompts } from "$lib/config.js";
import toast from "svelte-5-french-toast";
export function GenerateOptionsUsingAI(index) {
if (!questions.v[index].name) {
toast.error("Please enter a question to generate options.");
return;
}
const fetchOptions = async () => {
const response = await fetch("https://ai.hackclub.com/chat/completions", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
messages: [
{
role: "user",
content: AiPrompts.GenerateOptionsUsingAI.replace(
"[question]",
questions.v[index].name,
),
},
],
}),
});
const data = await response.json();
let question = questions.v[index].name;
questions.v[index] = JSON.parse(data.choices[0].message.content);
questions.v[index].name = question;
};
toast.promise(
fetchOptions(),
{
loading: "Generating options...",
success: "Options generated!",
error: (err) => "Error: " + (err?.message || err),
}
);
}

View file

@ -1,41 +0,0 @@
import { questions } from "./GameCreateData.svelte.js";
import { AiPrompts } from "$lib/config.js";
import toast from "svelte-5-french-toast";
export function GenerateQuestionsUsingAI() {
let topic = window.prompt(
"What is the topic of the questions?\nand the number of questions in the topic?",
);
if (!topic) {
return;
}
const fetchQuestions = async () => {
const response = await fetch("https://ai.hackclub.com/chat/completions", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
messages: [
{
role: "user",
content: AiPrompts.GenerateQuestionsUsingAI.replace("[topic]", topic),
},
],
}),
});
const data = await response.json();
questions.v = JSON.parse(data.choices[0].message.content);
};
toast.promise(
fetchQuestions(),
{
loading: "Generating questions...",
success: "Questions added!",
error: (err) => "Error: " + err.message || err,
}
);
}

View file

@ -1,17 +0,0 @@
import { createGame } from "./InsertGameInDB.js";
import { questions,Wait } from "./GameCreateData.svelte.js";
import toast from "svelte-5-french-toast";
export async function startGame() {
if (questions.v.some((q) => q.name === "")) return toast.error("Please fill all questions");
if (questions.v.some((q) => q.answers.some((a) => a === ""))) return toast.error("Fill all options");
if (questions.v.some((q) => q.correctAnswer === undefined))
return toast.error("Select correct answers");
const gamePin = Math.floor(Math.random() * 1000000)
.toString()
.padStart(6, "0");
Wait.v = true;
await createGame(questions.v, gamePin);}

View file

@ -1,31 +0,0 @@
import toast from "svelte-5-french-toast";
import { supabase } from "$lib/supabase.js";
export async function UpLoadFiles(file) {
if (!file) {
toast.error("Please select a file to upload first.");
return;
}
const fileExt = file.name.split(".").pop();
const fileName = `${Date.now()}.${fileExt}`;
const filePath = `${fileName}`;
const uploadPromise = supabase.storage.from("useruploadedcontent").upload(filePath, file);
const result = await toast.promise(uploadPromise, {
loading: "Uploading...",
success: "Upload successful!",
error: (error) => `Upload failed. ${error.message} Please try again.`,
});
if (result.error) {
toast.error("Upload error:"+ result.error.message);
return;
}
// Retrieve public URL
const { data: publicData } = supabase.storage.from("useruploadedcontent").getPublicUrl(filePath);
return publicData.publicUrl;
}

View file

@ -1,34 +1,39 @@
@import "https://www.nerdfonts.com/assets/css/webfont.css";
@import url("https://fonts.googleapis.com/css2?family=Comfortaa:wght@300..700&family=JetBrains+Mono:wght@200&family=Sour+Gummy:wght@300&display=swap");
@import url("https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@300&display=swap");
@import "tailwindcss";
:root {
@apply bg-gray-950 font-[Sour_Gummy] text-white;
@apply bg-gray-950 font-[Space_Grotesk] text-white;
}
.btn {
@apply flex cursor-pointer items-center gap-1 rounded border-2 border-white bg-gray-600 px-4 py-2 font-bold text-white transition-all hover:scale-105 hover:bg-gray-500 hover:shadow-lg;
&.compact {
@apply px-1 py-0.5;
}
&.circular {
@apply rounded-full;
&.compact {
@apply px-0.75 py-0.75;
}
}
@apply flex cursor-pointer items-center gap-1 rounded border-2 border-white bg-gray-700 px-4 py-2 font-bold text-white transition-all hover:scale-105 hover:bg-gray-500 hover:shadow-lg;
&.dull {
@apply border-gray-700 bg-transparent text-gray-700 hover:bg-gray-700 hover:text-white;
}
&.slim {
@apply px-1 py-3;
}
&.green {
@apply bg-green-600 hover:bg-green-500;
}
&.red {
@apply bg-red-600 hover:bg-red-500;
}
}
.card {
@apply cursor-pointer rounded border-2 border-gray-700 bg-gray-800 p-4 shadow-[4px_4px_0px_0px_white] transition-all hover:-translate-0.5 hover:shadow-[6px_6px_0px_0px_white] active:translate-0.5 active:shadow-[2px_2px_0px_0px_white];
}
.input {
@apply w-full rounded border-2 border-gray-700 bg-gray-800 px-4 py-2 text-white transition-all placeholder:text-gray-500 focus:border-white focus:outline-none;
}
.ratio {
@apply h-6 w-6;
}