Merge pull request #3 from RezHackXYZ/new-ui-for-kahhot-clone
New UI for kahhot clone
This commit is contained in:
commit
1652045fcd
24 changed files with 456 additions and 508 deletions
|
@ -59,20 +59,28 @@ export let AnswersSymbolAndColorScheme = [
|
||||||
|
|
||||||
export let DefaultQuestions = [
|
export let DefaultQuestions = [
|
||||||
{
|
{
|
||||||
name: "What should you do when you're free?",
|
questionText: "What should you do when you're free?",
|
||||||
answers: ["Do something in real life!", "Play video games", "Code!", "Touch grass!"],
|
timeLimit: 15,
|
||||||
correctAnswer: 2,
|
type: "SingleAnswer",
|
||||||
timeLimit: 30,
|
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?",
|
questionText: "Is RezHackXYZ the best programmer in the world?",
|
||||||
answers: ["Yes :)", "No :("],
|
|
||||||
correctAnswer: 0,
|
|
||||||
timeLimit: 5,
|
timeLimit: 5,
|
||||||
|
type: "SingleAnswer",
|
||||||
|
options: ["Yes :)", "No :("],
|
||||||
|
CorrectOption: { SingleAnswer: 0 },
|
||||||
|
hasMedia: true,
|
||||||
|
mediaURL: "https://github.com/RezHackXYZ.png",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Best place in the world?",
|
questionText: "Best place in the world?",
|
||||||
answers: [
|
timeLimit: 5,
|
||||||
|
type: "SingleAnswer",
|
||||||
|
options: [
|
||||||
"Google",
|
"Google",
|
||||||
"Microsoft",
|
"Microsoft",
|
||||||
"Apple",
|
"Apple",
|
||||||
|
@ -82,55 +90,8 @@ export let DefaultQuestions = [
|
||||||
"Facebook",
|
"Facebook",
|
||||||
"Twitter",
|
"Twitter",
|
||||||
],
|
],
|
||||||
correctAnswer: 4,
|
CorrectOption: { SingleAnswer: 4 },
|
||||||
timeLimit: 120,
|
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]
|
|
||||||
`,
|
|
||||||
};
|
|
||||||
|
|
|
@ -1,2 +0,0 @@
|
||||||
import JSConfetti from "js-confetti";
|
|
||||||
const jsConfetti = new JSConfetti();
|
|
|
@ -1,28 +1,12 @@
|
||||||
<script>
|
<script>
|
||||||
import UseDemoQuestions from "./components/buttons/UseDemoQuestions.svelte";
|
import HowTheQuestionWillLook from "./HowTheQuestionWillLook.svelte";
|
||||||
import NewQuestion from "./components/buttons/NewQuestion.svelte";
|
import QuestionsList from "./QuestionsList.svelte";
|
||||||
import StartGame from "./components/buttons/StartGame.svelte";
|
import QuestionOptions from "./QuestionOptions.svelte";
|
||||||
import Question from "./components/Questions/question.svelte";
|
import Buttons from "./Buttons.svelte";
|
||||||
import { questions, Wait } from "./logic/GameCreateData.svelte.js";
|
|
||||||
import WaitStartGame from "./components/buttons/WaitStartGame.svelte";
|
|
||||||
import GenerateQuetionsUsingAi from "./components/buttons/GenerateQuetionsUsingAI.svelte";
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="bg-grey-900 flex justify-center p-5">
|
<div class="flex h-full justify-between">
|
||||||
<div
|
<QuestionsList />
|
||||||
class="flex flex-col items-center justify-center gap-1 rounded-lg bg-gray-900 p-8 shadow-lg"
|
<div class="flex h-full w-[54%] flex-col gap-2"><HowTheQuestionWillLook /><Buttons /></div>
|
||||||
>
|
<QuestionOptions />
|
||||||
<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>
|
</div>
|
99
src/routes/kahootclone/create/Buttons.svelte
Normal file
99
src/routes/kahootclone/create/Buttons.svelte
Normal 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>
|
57
src/routes/kahootclone/create/HowTheQuestionWillLook.svelte
Normal file
57
src/routes/kahootclone/create/HowTheQuestionWillLook.svelte
Normal 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>
|
183
src/routes/kahootclone/create/QuestionOptions.svelte
Normal file
183
src/routes/kahootclone/create/QuestionOptions.svelte
Normal 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>
|
31
src/routes/kahootclone/create/QuestionsList.svelte
Normal file
31
src/routes/kahootclone/create/QuestionsList.svelte
Normal 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>
|
|
@ -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>
|
|
|
@ -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>
|
|
|
@ -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
|
|
||||||
>
|
|
|
@ -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}
|
|
|
@ -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}
|
|
|
@ -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
|
|
||||||
>
|
|
|
@ -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
|
|
||||||
>
|
|
|
@ -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
|
|
||||||
>
|
|
|
@ -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
|
|
||||||
>
|
|
15
src/routes/kahootclone/create/create.svelte.js
Normal file
15
src/routes/kahootclone/create/create.svelte.js
Normal 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 });
|
|
@ -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 toast from "svelte-5-french-toast";
|
||||||
import { Wait } from "./GameCreateData.svelte.js";
|
|
||||||
|
|
||||||
export async function createGame(questions, gamePin) {
|
export async function createGame() {
|
||||||
// Insert game
|
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({
|
const insertGamePromise = supabase.from("games").insert({
|
||||||
creator: "anonymous",
|
creator: "anonymous",
|
||||||
creationdate: new Date().toISOString(),
|
creationdate: new Date().toISOString(),
|
||||||
|
@ -19,19 +34,10 @@ export async function createGame(questions, gamePin) {
|
||||||
});
|
});
|
||||||
|
|
||||||
if (gameError) {
|
if (gameError) {
|
||||||
Wait.v = false;
|
wait.v = false;
|
||||||
return;
|
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 insertQuestionsPromise = supabase.from("questions").insert(questionsData).select("id");
|
||||||
|
|
||||||
const { data: questionsResult, error: questionsError } = await toast.promise(
|
const { data: questionsResult, error: questionsError } = await toast.promise(
|
||||||
|
@ -47,14 +53,13 @@ export async function createGame(questions, gamePin) {
|
||||||
);
|
);
|
||||||
|
|
||||||
if (questionsError) {
|
if (questionsError) {
|
||||||
Wait.v = false;
|
wait.v = false;
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const answersData = [];
|
const answersData = [];
|
||||||
questionsResult.forEach((question, index) => {
|
questionsResult.forEach((question, index) => {
|
||||||
questions[index].answers.forEach((answer) => {
|
QuestionsData.v[index].options.forEach((answer) => {
|
||||||
answersData.push({
|
answersData.push({
|
||||||
questionid: question.id,
|
questionid: question.id,
|
||||||
content: answer,
|
content: answer,
|
||||||
|
@ -72,9 +77,10 @@ export async function createGame(questions, gamePin) {
|
||||||
});
|
});
|
||||||
|
|
||||||
if (answersError) {
|
if (answersError) {
|
||||||
Wait.v = false
|
wait.v = false;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
window.location.href = `/kahootclone/host?gamepin=${gamePin}`;
|
window.location.href = `/kahootclone/host?gamepin=${gamePin}`;
|
||||||
|
wait.v = false;
|
||||||
}
|
}
|
|
@ -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.");
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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),
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -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,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -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);}
|
|
|
@ -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;
|
|
||||||
}
|
|
|
@ -1,34 +1,39 @@
|
||||||
@import "https://www.nerdfonts.com/assets/css/webfont.css";
|
@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=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";
|
@import "tailwindcss";
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
@apply bg-gray-950 font-[Sour_Gummy] text-white;
|
@apply bg-gray-950 font-[Space_Grotesk] text-white;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn {
|
.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;
|
@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;
|
||||||
|
|
||||||
&.compact {
|
|
||||||
@apply px-1 py-0.5;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.circular {
|
|
||||||
@apply rounded-full;
|
|
||||||
&.compact {
|
|
||||||
@apply px-0.75 py-0.75;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&.dull {
|
&.dull {
|
||||||
@apply border-gray-700 bg-transparent text-gray-700 hover:bg-gray-700 hover:text-white;
|
@apply border-gray-700 bg-transparent text-gray-700 hover:bg-gray-700 hover:text-white;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.slim {
|
||||||
|
@apply px-1 py-3;
|
||||||
|
}
|
||||||
|
|
||||||
&.green {
|
&.green {
|
||||||
@apply bg-green-600 hover:bg-green-500;
|
@apply bg-green-600 hover:bg-green-500;
|
||||||
}
|
}
|
||||||
|
&.red {
|
||||||
|
@apply bg-red-600 hover:bg-red-500;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.card {
|
.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];
|
@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;
|
||||||
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue