diff --git a/src/lib/config.js b/src/lib/config.js index 006b120..2089f8d 100644 --- a/src/lib/config.js +++ b/src/lib/config.js @@ -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] -`, -}; diff --git a/src/lib/showAlert.js b/src/lib/showAlert.js deleted file mode 100644 index 957e3d4..0000000 --- a/src/lib/showAlert.js +++ /dev/null @@ -1,2 +0,0 @@ -import JSConfetti from "js-confetti"; -const jsConfetti = new JSConfetti(); diff --git a/src/routes/kahootclone/create/+page.svelte b/src/routes/kahootclone/create/+page.svelte index 06798db..d932189 100644 --- a/src/routes/kahootclone/create/+page.svelte +++ b/src/routes/kahootclone/create/+page.svelte @@ -1,28 +1,12 @@ -
-
-
- {#each questions.v as question, index} - - {/each} -
- - {#if Wait.v == false} - - {:else} - - {/if} -
-
-
+
+ +
+ +
\ No newline at end of file diff --git a/src/routes/kahootclone/create/Buttons.svelte b/src/routes/kahootclone/create/Buttons.svelte new file mode 100644 index 0000000..bd66d33 --- /dev/null +++ b/src/routes/kahootclone/create/Buttons.svelte @@ -0,0 +1,99 @@ + + +
+ + + {#if wait.v == true} + + {:else} + + {/if} +
diff --git a/src/routes/kahootclone/create/HowTheQuestionWillLook.svelte b/src/routes/kahootclone/create/HowTheQuestionWillLook.svelte new file mode 100644 index 0000000..46907c7 --- /dev/null +++ b/src/routes/kahootclone/create/HowTheQuestionWillLook.svelte @@ -0,0 +1,57 @@ + + +
+

How Question number {selectedQuestionIndex.v + 1} will look

+ +
+
+

+ {selectedQuestionIndex.v + 1}. + {QuestionsData.v[selectedQuestionIndex.v].questionText} +

+ {#if QuestionsData.v[selectedQuestionIndex.v].mediaURL && QuestionsData.v[selectedQuestionIndex.v].hasMedia} +
+
+ {#if QuestionsData.v[selectedQuestionIndex.v].mediaURL.match(/\.(mp4|webm|ogg|mov|avi|mkv)$/i)} + + + {:else} + Question media{/if} +
+
+ {/if} +
+ {#each QuestionsData.v[selectedQuestionIndex.v].options as question, questionIndex} +
+ + + {question} + +
+ {/each} +
+
+
+
diff --git a/src/routes/kahootclone/create/QuestionOptions.svelte b/src/routes/kahootclone/create/QuestionOptions.svelte new file mode 100644 index 0000000..046c07e --- /dev/null +++ b/src/routes/kahootclone/create/QuestionOptions.svelte @@ -0,0 +1,183 @@ + + +
+
+

Options

+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + + + {#if QuestionsData.v[selectedQuestionIndex.v].hasMedia} + { + QuestionsData.v[selectedQuestionIndex.v].mediaURL = await UpLoadFiles(files[0]); + }} + accept="image/*,video/*" + /> + + { + QuestionsData.v[selectedQuestionIndex.v].mediaURL = await UpLoadFiles(files[0]); + }} + accept="image/*,video/*" + /> + {/if} +
+ +
+ {#if "SingleAnswer" === "SingleAnswer"} + Options + {#each QuestionsData.v[selectedQuestionIndex.v].options as Option, index} +
+
+ + + +
+
+ {/each} +
+ +
+ {/if} +
+
+
+
+ + +
+
+
diff --git a/src/routes/kahootclone/create/QuestionsList.svelte b/src/routes/kahootclone/create/QuestionsList.svelte new file mode 100644 index 0000000..5a9102b --- /dev/null +++ b/src/routes/kahootclone/create/QuestionsList.svelte @@ -0,0 +1,31 @@ + + +
+

Questions

+ {#each QuestionsData.v as question, questionIndex} + + {/each} +
+ +
+
diff --git a/src/routes/kahootclone/create/components/Questions/answers.svelte b/src/routes/kahootclone/create/components/Questions/answers.svelte deleted file mode 100644 index dd8002e..0000000 --- a/src/routes/kahootclone/create/components/Questions/answers.svelte +++ /dev/null @@ -1,23 +0,0 @@ - - -
- - - -
diff --git a/src/routes/kahootclone/create/components/Questions/question.svelte b/src/routes/kahootclone/create/components/Questions/question.svelte deleted file mode 100644 index e739168..0000000 --- a/src/routes/kahootclone/create/components/Questions/question.svelte +++ /dev/null @@ -1,79 +0,0 @@ - - -
-
-
-

Q{index + 1}.

- - - - -
- -
- {#each questions.v[index].answers as _, answersIndex} - - {/each} -
- - { - questions.v[index].media = await UpLoadFiles(files[0]); - }} - bind:files - accept="image/*,video/*" - /> -
-
diff --git a/src/routes/kahootclone/create/components/buttons/DeleteQuestion.svelte b/src/routes/kahootclone/create/components/buttons/DeleteQuestion.svelte deleted file mode 100644 index 7057ea9..0000000 --- a/src/routes/kahootclone/create/components/buttons/DeleteQuestion.svelte +++ /dev/null @@ -1,20 +0,0 @@ - - - diff --git a/src/routes/kahootclone/create/components/buttons/GenerateOptionsUsingAI.svelte b/src/routes/kahootclone/create/components/buttons/GenerateOptionsUsingAI.svelte deleted file mode 100644 index b657a8c..0000000 --- a/src/routes/kahootclone/create/components/buttons/GenerateOptionsUsingAI.svelte +++ /dev/null @@ -1,19 +0,0 @@ - - -{#if questions.v[index].answers.every((answer) => answer === "")} - -{/if} diff --git a/src/routes/kahootclone/create/components/buttons/GenerateQuetionsUsingAI.svelte b/src/routes/kahootclone/create/components/buttons/GenerateQuetionsUsingAI.svelte deleted file mode 100644 index 8932152..0000000 --- a/src/routes/kahootclone/create/components/buttons/GenerateQuetionsUsingAI.svelte +++ /dev/null @@ -1,14 +0,0 @@ - - -{#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)} - -{/if} diff --git a/src/routes/kahootclone/create/components/buttons/NewQuestion.svelte b/src/routes/kahootclone/create/components/buttons/NewQuestion.svelte deleted file mode 100644 index 63cc2d6..0000000 --- a/src/routes/kahootclone/create/components/buttons/NewQuestion.svelte +++ /dev/null @@ -1,15 +0,0 @@ - - - diff --git a/src/routes/kahootclone/create/components/buttons/StartGame.svelte b/src/routes/kahootclone/create/components/buttons/StartGame.svelte deleted file mode 100644 index 10f8d3a..0000000 --- a/src/routes/kahootclone/create/components/buttons/StartGame.svelte +++ /dev/null @@ -1,18 +0,0 @@ - - - diff --git a/src/routes/kahootclone/create/components/buttons/UseDemoQuestions.svelte b/src/routes/kahootclone/create/components/buttons/UseDemoQuestions.svelte deleted file mode 100644 index ead81cd..0000000 --- a/src/routes/kahootclone/create/components/buttons/UseDemoQuestions.svelte +++ /dev/null @@ -1,15 +0,0 @@ - - - diff --git a/src/routes/kahootclone/create/components/buttons/WaitStartGame.svelte b/src/routes/kahootclone/create/components/buttons/WaitStartGame.svelte deleted file mode 100644 index 0102b86..0000000 --- a/src/routes/kahootclone/create/components/buttons/WaitStartGame.svelte +++ /dev/null @@ -1,18 +0,0 @@ - - - diff --git a/src/routes/kahootclone/create/create.svelte.js b/src/routes/kahootclone/create/create.svelte.js new file mode 100644 index 0000000..53eed4a --- /dev/null +++ b/src/routes/kahootclone/create/create.svelte.js @@ -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 }); \ No newline at end of file diff --git a/src/routes/kahootclone/create/logic/InsertGameInDB.js b/src/routes/kahootclone/create/createGame.js similarity index 72% rename from src/routes/kahootclone/create/logic/InsertGameInDB.js rename to src/routes/kahootclone/create/createGame.js index 8d4e525..38a466c 100644 --- a/src/routes/kahootclone/create/logic/InsertGameInDB.js +++ b/src/routes/kahootclone/create/createGame.js @@ -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; } diff --git a/src/routes/kahootclone/create/logic/GameCreateData.svelte.js b/src/routes/kahootclone/create/logic/GameCreateData.svelte.js deleted file mode 100644 index 33a6316..0000000 --- a/src/routes/kahootclone/create/logic/GameCreateData.svelte.js +++ /dev/null @@ -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."); - } -} diff --git a/src/routes/kahootclone/create/logic/GenerateOptionsUsingAI.js b/src/routes/kahootclone/create/logic/GenerateOptionsUsingAI.js deleted file mode 100644 index 175c8d3..0000000 --- a/src/routes/kahootclone/create/logic/GenerateOptionsUsingAI.js +++ /dev/null @@ -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), - } - ); -} diff --git a/src/routes/kahootclone/create/logic/GenerateQuestionsUsingAI.js b/src/routes/kahootclone/create/logic/GenerateQuestionsUsingAI.js deleted file mode 100644 index 7c369f9..0000000 --- a/src/routes/kahootclone/create/logic/GenerateQuestionsUsingAI.js +++ /dev/null @@ -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, - } - ); -} \ No newline at end of file diff --git a/src/routes/kahootclone/create/logic/StartGame.js b/src/routes/kahootclone/create/logic/StartGame.js deleted file mode 100644 index 6060fac..0000000 --- a/src/routes/kahootclone/create/logic/StartGame.js +++ /dev/null @@ -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);} diff --git a/src/routes/kahootclone/create/logic/UpLoadFiles.js b/src/routes/kahootclone/create/logic/UpLoadFiles.js deleted file mode 100644 index 254d180..0000000 --- a/src/routes/kahootclone/create/logic/UpLoadFiles.js +++ /dev/null @@ -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; -} diff --git a/src/routes/tailwind.css b/src/routes/tailwind.css index f745326..3192c45 100644 --- a/src/routes/tailwind.css +++ b/src/routes/tailwind.css @@ -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; +}