📝 Added documentation about development-setup

This commit is contained in:
Mawoka
2022-03-12 18:55:32 +01:00
parent d48ab734a5
commit c3d5e00a1b
21 changed files with 3755 additions and 2745 deletions
+9
View File
@@ -6,16 +6,25 @@ You can create quizzes and play them remotely with other people.
It is mainly made for teachers, who create a quiz, so students can compete with their knowledge against each other. It is mainly made for teachers, who create a quiz, so students can compete with their knowledge against each other.
## Try it ## Try it
There is a hosted version at [ClassQuiz.Mawoka.eu](https://classquiz.mawoka.eu?utm_medium=Github&utm_source=Readme). There is a hosted version at [ClassQuiz.Mawoka.eu](https://classquiz.mawoka.eu?utm_medium=Github&utm_source=Readme).
It is not intended for real-life usage, although it should work. It is not intended for real-life usage, although it should work.
The server is located in Karlsruhe, Germany and hosted by [netcup](https://mawoka.eu/redir?token=2). The server is located in Karlsruhe, Germany and hosted by [netcup](https://mawoka.eu/redir?token=2).
If you want to use it in real-life, If you want to use it in real-life,
please contact me on [my website](https://mawoka.eu/contact?utm_medium=Github&utm_source=Readme). please contact me on [my website](https://mawoka.eu/contact?utm_medium=Github&utm_source=Readme).
## Self-Host ## Self-Host
Please see https://classquiz.mawoka.eu/docs/self-host Please see https://classquiz.mawoka.eu/docs/self-host
## Development
See https://classquiz.mawoka.eu/docs/develop
## Docs
The docs are https://classquiz.mawoka.eu/docs on the website
### Things to know about the structure ### Things to know about the structure
1. Everything depends on Redis (API and frontend). 1. Everything depends on Redis (API and frontend).
2. The proxy-container is optional (Caddy), but makes your life easier 2. The proxy-container is optional (Caddy), but makes your life easier
3. You need to set your reverse proxy up, so that it allows Socket.io-connections. 3. You need to set your reverse proxy up, so that it allows Socket.io-connections.
+3301 -2426
View File
File diff suppressed because it is too large Load Diff
+26 -22
View File
@@ -1,25 +1,29 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang='en'> <html lang="en">
<head> <head>
<meta charset='utf-8' /> <meta charset="utf-8" />
<meta name='description' content='' /> <meta name="description" content="" />
<!-- <link rel="icon" href="%svelte.assets%/favicon.png" />--> <!-- <link rel="icon" href="%svelte.assets%/favicon.png" />-->
<meta name='viewport' content='width=device-width, initial-scale=1' /> <meta name="viewport" content="width=device-width, initial-scale=1" />
<script async='' defer='' data-domain='classquiz.mawoka.eu' <script
src='https://sugar.mawoka.eu.org/js/plausible.hash.outbound-links.js'></script> async=""
defer=""
data-domain="classquiz.mawoka.eu"
src="https://sugar.mawoka.eu.org/js/plausible.hash.outbound-links.js"
></script>
<link rel='apple-touch-icon' sizes='180x180' href='%svelte.assets%/apple-touch-icon.png'> <link rel="apple-touch-icon" sizes="180x180" href="%svelte.assets%/apple-touch-icon.png" />
<link rel='icon' type='image/png' sizes='32x32' href='%svelte.assets%/favicon-32x32.png'> <link rel="icon" type="image/png" sizes="32x32" href="%svelte.assets%/favicon-32x32.png" />
<link rel='icon' type='image/png' sizes='16x16' href='%svelte.assets%/favicon-16x16.png'> <link rel="icon" type="image/png" sizes="16x16" href="%svelte.assets%/favicon-16x16.png" />
<link rel='manifest' href='%svelte.assets%/site.webmanifest'> <link rel="manifest" href="%svelte.assets%/site.webmanifest" />
<link rel='mask-icon' href='%svelte.assets%/safari-pinned-tab.svg' color='#5bbad5'> <link rel="mask-icon" href="%svelte.assets%/safari-pinned-tab.svg" color="#5bbad5" />
<meta name='apple-mobile-web-app-title' content='ClassQuiz'> <meta name="apple-mobile-web-app-title" content="ClassQuiz" />
<meta name='application-name' content='ClassQuiz'> <meta name="application-name" content="ClassQuiz" />
<meta name='msapplication-TileColor' content='#00a300'> <meta name="msapplication-TileColor" content="#00a300" />
<meta name='theme-color' content='#ffffff'> <meta name="theme-color" content="#ffffff" />
%svelte.head% %svelte.head%
</head> </head>
<body> <body>
<div>%svelte.body%</div> <div>%svelte.body%</div>
</body> </body>
</html> </html>
+14 -2
View File
@@ -23,7 +23,13 @@
<!-- Our open/close buttons --> <!-- Our open/close buttons -->
<!-- Open menu --> <!-- Open menu -->
<button class="lg:hidden" id="open-menu" on:click={toggleMenu} class:hidden={!openMenu} aria-label='Open navbar'> <button
class="lg:hidden"
id="open-menu"
on:click={toggleMenu}
class:hidden={!openMenu}
aria-label="Open navbar"
>
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
width="24" width="24"
@@ -42,7 +48,13 @@
</button> </button>
<!-- Close menu --> <!-- Close menu -->
<button class="hidden" id="close-menu" class:hidden={!closeMenu} on:click={toggleMenu} aria-label='Close navbar'> <button
class="hidden"
id="close-menu"
class:hidden={!closeMenu}
on:click={toggleMenu}
aria-label="Close navbar"
>
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
width="14" width="14"
+23 -25
View File
@@ -1,8 +1,8 @@
<script lang='ts'> <script lang="ts">
import { socket } from '$lib/socket'; import { socket } from '$lib/socket';
import { onDestroy, onMount } from 'svelte'; import { onDestroy, onMount } from 'svelte';
import { browser } from '$app/env'; import { browser } from '$app/env';
import * as Sentry from "@sentry/browser"; import * as Sentry from '@sentry/browser';
export let game_pin: string; export let game_pin: string;
@@ -11,8 +11,7 @@
let hcaptcha = { let hcaptcha = {
execute: async (_a, _b) => ({ response: '' }), execute: async (_a, _b) => ({ response: '' }),
render: (_a, _b) => { render: (_a, _b) => {}
}
}; };
let hcaptchaWidgetID; let hcaptchaWidgetID;
@@ -33,8 +32,7 @@
if (browser) { if (browser) {
hcaptcha = { hcaptcha = {
execute: async () => ({ response: '' }), execute: async () => ({ response: '' }),
render: () => { render: () => {}
}
}; };
} }
}); });
@@ -50,7 +48,7 @@
}); });
captcha_resp = response; captcha_resp = response;
} catch (e) { } catch (e) {
if (import.meta.env.VITE_SENTRY !== null){ if (import.meta.env.VITE_SENTRY !== null) {
Sentry.captureException(e); Sentry.captureException(e);
} }
alert('Captcha failed, reloading the page probably helps!'); alert('Captcha failed, reloading the page probably helps!');
@@ -69,27 +67,27 @@
</script> </script>
<svelte:head> <svelte:head>
<script src='https://js.hcaptcha.com/1/api.js?render=explicit' async defer></script> <script src="https://js.hcaptcha.com/1/api.js?render=explicit" async defer></script>
</svelte:head> </svelte:head>
{#if game_pin === '' || game_pin.length <= 7} {#if game_pin === '' || game_pin.length <= 7}
<div class='flex flex-col justify-center align-center w-screen h-screen'> <div class="flex flex-col justify-center align-center w-screen h-screen">
<form <form
on:submit|preventDefault={() => {}} on:submit|preventDefault={() => {}}
class='flex-col flex justify-center align-center mx-auto' class="flex-col flex justify-center align-center mx-auto"
> >
<h1 class='text-lg text-center'>Game Pin</h1> <h1 class="text-lg text-center">Game Pin</h1>
<input <input
class='border border-amber-800 self-center text-center text-black' class="border border-amber-800 self-center text-center text-black"
bind:value={game_pin} bind:value={game_pin}
maxlength='8' maxlength="8"
/> />
<!-- use:tippy={{content: "Please enter the game pin", sticky: true, placement: 'top'}}--> <!-- use:tippy={{content: "Please enter the game pin", sticky: true, placement: 'top'}}-->
<br /> <br />
<button <button
class='bg-amber-800 hover:bg-amber-700 text-white font-bold py-2 px-4 rounded disabled:opacity-50 disabled:cursor-not-allowed mt-2' class="bg-amber-800 hover:bg-amber-700 text-white font-bold py-2 px-4 rounded disabled:opacity-50 disabled:cursor-not-allowed mt-2"
type='submit' type="submit"
disabled={game_pin.length <= 7} disabled={game_pin.length <= 7}
> >
Submit Submit
@@ -97,16 +95,16 @@
</form> </form>
</div> </div>
{:else} {:else}
<div class='flex flex-col justify-center align-center w-screen h-screen'> <div class="flex flex-col justify-center align-center w-screen h-screen">
<form <form
on:submit|preventDefault={setUsername} on:submit|preventDefault={setUsername}
class='flex-col flex justify-center align-center mx-auto' class="flex-col flex justify-center align-center mx-auto"
> >
<h1 class='text-lg text-center'>Username</h1> <h1 class="text-lg text-center">Username</h1>
<input class='border border-amber-800 text-black text-center' bind:value={username} /> <input class="border border-amber-800 text-black text-center" bind:value={username} />
<button <button
class='bg-amber-800 hover:bg-amber-700 text-white font-bold py-2 px-4 rounded disabled:cursor-not-allowed disabled:opacity-50 mt-2' class="bg-amber-800 hover:bg-amber-700 text-white font-bold py-2 px-4 rounded disabled:cursor-not-allowed disabled:opacity-50 mt-2"
type='submit' type="submit"
disabled={username.length <= 3} disabled={username.length <= 3}
> >
Submit Submit
@@ -115,9 +113,9 @@
</div> </div>
{/if} {/if}
<div <div
id='hcaptcha' id="hcaptcha"
class='h-captcha' class="h-captcha"
data-sitekey={hcaptchaSitekey} data-sitekey={hcaptchaSitekey}
data-size='invisible' data-size="invisible"
data-theme='dark' data-theme="dark"
/> />
+24 -25
View File
@@ -16,42 +16,41 @@
tracesSampleRate: 0.5 tracesSampleRate: 0.5
}); });
} }
</script> </script>
{#if $navbarVisible} {#if $navbarVisible}
<Navbar /> <Navbar />
<div class='pt-16 h-screen'> <div class="pt-16 h-screen">
<slot /> <slot />
</div> </div>
{:else} {:else}
<slot /> <slot />
{/if} {/if}
<style lang='scss'> <style lang="scss">
:global(body) { :global(body) {
// height: 100%; // height: 100%;
// width: 100%; // width: 100%;
background: linear-gradient(to right, #009444, #39b54a, #8dc63f) repeat-y; background: linear-gradient(to right, #009444, #39b54a, #8dc63f) repeat-y;
background-size: cover; background-size: cover;
/*background: linear-gradient(-225deg, #231557 0%, #44107A 29%, #FF1361 67%, #FFF800 100%); */ /*background: linear-gradient(-225deg, #231557 0%, #44107A 29%, #FF1361 67%, #FFF800 100%); */
/*background: linear-gradient(-225deg, #22E1FF 0%, #1D8FE1 48%, #625EB1 100%); */ /*background: linear-gradient(-225deg, #22E1FF 0%, #1D8FE1 48%, #625EB1 100%); */
color: beige; color: beige;
// background-size: 400% 400%; // background-size: 400% 400%;
//animation: background_animation 5s ease infinite; //animation: background_animation 5s ease infinite;
} }
@keyframes background_animation { @keyframes background_animation {
0% { 0% {
background-position: 0% 50%; background-position: 0% 50%;
} }
50% { 50% {
background-position: 100% 50%; background-position: 100% 50%;
} }
100% { 100% {
background-position: 0% 50%; background-position: 0% 50%;
} }
} }
</style> </style>
+1
View File
@@ -66,6 +66,7 @@
isSubmitting = false; isSubmitting = false;
}; };
</script> </script>
<svelte:head> <svelte:head>
<title>ClassQuiz - Login</title> <title>ClassQuiz - Login</title>
</svelte:head> </svelte:head>
@@ -68,6 +68,7 @@
data: '' data: ''
}; };
</script> </script>
<svelte:head> <svelte:head>
<title>ClassQuiz - Register</title> <title>ClassQuiz - Register</title>
</svelte:head> </svelte:head>
+5 -3
View File
@@ -21,9 +21,7 @@
}; };
} }
</script> </script>
<svelte:head>
<title>ClassQuiz - Host</title>
</svelte:head>
<script lang="ts"> <script lang="ts">
import type { QuizData } from '../app'; import type { QuizData } from '../app';
@@ -106,6 +104,10 @@
}; };
</script> </script>
<svelte:head>
<title>ClassQuiz - Host</title>
</svelte:head>
{#if !success} {#if !success}
<input placeholder="game id" bind:value={game_token} /> <input placeholder="game id" bind:value={game_token} />
<input placeholder="game pin" bind:value={game_pin} /> <input placeholder="game pin" bind:value={game_pin} />
+5 -3
View File
@@ -21,9 +21,7 @@
}; };
} }
</script> </script>
<svelte:head>
<title>ClassQuiz - Create</title>
</svelte:head>
<script lang="ts"> <script lang="ts">
import { onMount } from 'svelte'; import { onMount } from 'svelte';
import Editor from '$lib/editor.svelte'; import Editor from '$lib/editor.svelte';
@@ -82,6 +80,10 @@
}; };
</script> </script>
<svelte:head>
<title>ClassQuiz - Create</title>
</svelte:head>
{#if data !== undefined} {#if data !== undefined}
<form on:submit|preventDefault={submit} class="grid grid-cols-1 gap-2"> <form on:submit|preventDefault={submit} class="grid grid-cols-1 gap-2">
<Editor bind:data /> <Editor bind:data />
+95
View File
@@ -0,0 +1,95 @@
<svelte:head>
<title>ClassQuiz/docs - Development-setup</title>
<meta
name="description"
content="How to import quizzes from KAHOOT! into ClassQuiz, the open-source quiz-application, easily"
/>
</svelte:head>
<article
class="prose prose-sm sm:prose lg:prose-lg xl:prose-xl mx-auto mt-10 prose-pink text-yellow-50 px-4"
>
<h1>Development-setup</h1>
<h2>Requirements</h2>
<h4>Backend</h4>
<ul>
<li><a href="https://caddyserver.com/">Caddy</a></li>
<li><a href="https://pipenv.pypa.io/en/latest/">Pipenv</a></li>
<li><a href="https://redis.io/">Redis</a></li>
</ul>
<h4>Frontend</h4>
<ul>
<li><a href="https://pnpm.io/">pNpM</a></li>
</ul>
<h2>Setup</h2>
<ul>
<li>
Clone the repo:
<pre><code>git clone https://github.com/mawoka-myblock/classquiz</code></pre>
</li>
<li>
Install the dependencies
<ul>
<li>
Python:
<pre><code>pipenv install</code></pre>
</li>
<li>
JS:
<pre><code>cd frontend && pnpm i</code></pre>
</li>
</ul>
</li>
<li>
Start the other services
<ul>
<li>
Caddy:
<pre><code>caddy run</code></pre>
</li>
<li>
Redis:
<pre><code>redis-server</code></pre>
</li>
</ul>
</li>
hcaptcha_key: str mail_address: str mail_password: str mail_username: str mail_server: str mail_port:
int secret_key: str
<li>
Set your config up in your .env-file. What you have to set up can you see in the
<code>classquiz/config.py</code>-file. The things you have to set are the following:
<ul>
<li><code>hcaptcha_key</code></li>
<li><code>mail_address</code></li>
<li><code>mail_password</code></li>
<li><code>mail_username</code></li>
<li><code>mail_server</code></li>
<li><code>mail_port</code></li>
<li><code>secret_key</code></li>
</ul>
</li>
<li>
Start the server
<ul>
<li>
Backend:
<pre><code>pipenv run uvicorn classquiz:app --reload</code></pre>
</li>
<li>
Frontend:
<pre><code>cd frontend && pnpm dev</code></pre>
</li>
</ul>
</li>
</ul>
<h2>BEFORE you submit a Pull-Request</h2>
<h4>Frontend</h4>
<p>Please format and lint your code with</p>
<pre><code>pnpm run format && pnpm run lint</code></pre>
<h4>Backend</h4>
<p>IDK, just check that it works!</p>
</article>
@@ -1,17 +1,25 @@
<svelte:head> <svelte:head>
<title>ClassQuiz/docs - Import from Kahoot</title> <title>ClassQuiz/docs - Import from Kahoot</title>
<meta name=description content='How to import quizzes from KAHOOT! into ClassQuiz, the open-source quiz-application, easily'> <meta
name="description"
content="How to import quizzes from KAHOOT! into ClassQuiz, the open-source quiz-application, easily"
/>
</svelte:head> </svelte:head>
<article <article
class='prose prose-sm sm:prose lg:prose-lg xl:prose-xl mx-auto mt-10 prose-pink text-yellow-50 px-4' class="prose prose-sm sm:prose lg:prose-lg xl:prose-xl mx-auto mt-10 prose-pink text-yellow-50 px-4"
> >
<h1>Import Quizzes from Kahoot into ClassQuiz</h1> <h1>Import Quizzes from Kahoot into ClassQuiz</h1>
<p>All in all, this procedure is pretty simple, so just go to <a href='https://create.kahoot.it/discover' <p>
rel='nofollow' target='_blank'>create.kahoot.it/discover</a>, All in all, this procedure is pretty simple, so just go to <a
find the quiz you want to import, so click on it and copy the URL into your clipboard. Then go to <a href="https://create.kahoot.it/discover"
href='https://classquiz.mawoka.eu/import' rel='nofollow' target='_blank'>classquiz.mawoka.eu/import</a> and paste rel="nofollow"
the url into the text field. target="_blank">create.kahoot.it/discover</a
>, find the quiz you want to import, so click on it and copy the URL into your clipboard.
Then go to
<a href="https://classquiz.mawoka.eu/import" rel="nofollow" target="_blank"
>classquiz.mawoka.eu/import</a
> and paste the url into the text field.
</p> </p>
<h2>Limitations</h2> <h2>Limitations</h2>
@@ -21,4 +29,4 @@
<li>Metadata doesn't get imported</li> <li>Metadata doesn't get imported</li>
<li>Point-multipliers don't get imported</li> <li>Point-multipliers don't get imported</li>
</ul> </ul>
</article> </article>
+12 -7
View File
@@ -1,22 +1,27 @@
<svelte:head> <svelte:head>
<title>ClassQuiz/docs - Index</title> <title>ClassQuiz/docs - Index</title>
<meta name=description content='The overview about the docs for ClassQuiz, the open-source quiz-application'> <meta
name="description"
content="The overview about the docs for ClassQuiz, the open-source quiz-application"
/>
</svelte:head> </svelte:head>
<article <article
class='prose prose-sm sm:prose lg:prose-lg xl:prose-xl mx-auto mt-10 prose-pink text-yellow-50 px-4' class="prose prose-sm sm:prose lg:prose-lg xl:prose-xl mx-auto mt-10 prose-pink text-yellow-50 px-4"
> >
<h1>Welcome to the ClassQuiz docs!</h1> <h1>Welcome to the ClassQuiz docs!</h1>
<h2>Index</h2> <h2>Index</h2>
<ul> <ul>
<li> <li>
<a href='/docs/roadmap'>Roadmap</a> <a href="/docs/roadmap">Roadmap</a>
</li> </li>
<li> <li>
<a href='/docs/import-from-kahoot'>Import from Kahoot</a> <a href="/docs/import-from-kahoot">Import from Kahoot</a>
</li> </li>
<li> <li>
<a href='/docs/self-host'>Self-hosting</a> <a href="/docs/self-host">Self-hosting</a>
</li>
<li>
<a href="/docs/develop">Development-Setup</a>
</li> </li>
</ul> </ul>
</article>
</article>
+14 -22
View File
@@ -1,34 +1,26 @@
<svelte:head> <svelte:head>
<title>ClassQuiz/docs - Roadmap</title> <title>ClassQuiz/docs - Roadmap</title>
<meta name=description content='The roadmap for ClassQuiz, the open-source quiz-application'> <meta
name="description"
content="The roadmap for ClassQuiz, the open-source quiz-application"
/>
</svelte:head> </svelte:head>
<article <article
class='prose prose-sm sm:prose lg:prose-lg xl:prose-xl mx-auto mt-10 prose-pink text-yellow-50 px-4' class="prose prose-sm sm:prose lg:prose-lg xl:prose-xl mx-auto mt-10 prose-pink text-yellow-50 px-4"
> >
<h1>Roadmap</h1> <h1>Roadmap</h1>
<ul> <ul>
<li>User-management <li>
User-management
<ul> <ul>
<li> <li>Password-reset</li>
Password-reset <li>Session-management</li>
</li>
<li>
Session-management
</li>
</ul> </ul>
</li> </li>
<li> <li>Images</li>
Images <li>(Better) Admin-screen styling</li>
</li> <li>(Better) Edit/Create-screen styling</li>
<li> <li>Public quiz-discovery</li>
(Better) Admin-screen styling
</li>
<li>
(Better) Edit/Create-screen styling
</li>
<li>
Public quiz-discovery
</li>
</ul> </ul>
</article> </article>
+22 -16
View File
@@ -1,4 +1,4 @@
<script lang='ts'> <script lang="ts">
import { onMount } from 'svelte'; import { onMount } from 'svelte';
import '$lib/hljs.css'; import '$lib/hljs.css';
@@ -10,30 +10,36 @@
<svelte:head> <svelte:head>
<title>ClassQuiz/docs - Self-Host</title> <title>ClassQuiz/docs - Self-Host</title>
<meta name=description content='How to self-host ClassQuiz, the open-source quiz-application'> <meta
name="description"
content="How to self-host ClassQuiz, the open-source quiz-application"
/>
</svelte:head> </svelte:head>
<article <article
class='prose prose-sm sm:prose lg:prose-lg xl:prose-xl mx-auto mt-10 prose-pink text-yellow-50 px-4' class="prose prose-sm sm:prose lg:prose-lg xl:prose-xl mx-auto mt-10 prose-pink text-yellow-50 px-4"
> >
<h1>Self-Hosting</h1> <h1>Self-Hosting</h1>
<p>Since ClassQuiz is open-source, it can also be self-hosted.</p> <p>Since ClassQuiz is open-source, it can also be self-hosted.</p>
<h2>Requirements</h2> <h2>Requirements</h2>
<ul> <ul>
<li><a href='https://docker.com'>Docker</a></li> <li><a href="https://docker.com">Docker</a></li>
<li><a href='https://git-scm.com/'>Git</a></li> <li><a href="https://git-scm.com/">Git</a></li>
<li>A <a href='https://redis.com'>Redis</a>-Server (I recommend <a href='https://upstash.com'>Upstash</a>)</li> <li>
A <a href="https://redis.com">Redis</a>-Server (I recommend
<a href="https://upstash.com">Upstash</a>)
</li>
</ul> </ul>
<h2>Installation</h2> <h2>Installation</h2>
<p> <p>At first, clone the repo:</p>
At first, clone the repo:
</p>
<pre><code class='language-bash'>git clone https://github.com/mawoka-myblock/classquiz && cd ClassQuiz</code></pre> <pre><code class="language-bash"
>git clone https://github.com/mawoka-myblock/classquiz && cd ClassQuiz</code
></pre>
<p> <p>
Now, set a <b>VALID</b> Redis-URI in <code>frontend/Dockerfile</code> and, if you Sentry want, a valid Now, set a <b>VALID</b> Redis-URI in <code>frontend/Dockerfile</code> and, if you Sentry want,
Sentry-DSN. a valid Sentry-DSN.
</p> </p>
<p> <p>
You must set a valid hCaptcha-Sitekey in the <code>frontend/Dockerfile</code>. You must set a valid hCaptcha-Sitekey in the <code>frontend/Dockerfile</code>.
@@ -44,7 +50,8 @@
Before you can start your stack, you have to set some environment-variables in your Before you can start your stack, you have to set some environment-variables in your
<code>docker-compose.yml</code>. <code>docker-compose.yml</code>.
</p> </p>
<pre><code class='language-yaml'>version: "3" <pre><code class="language-yaml"
>version: "3"
services: services:
frontend: frontend:
@@ -112,6 +119,5 @@ volumes:
data: data:
</code></pre> </code></pre>
<p>Now build and deploy:</p> <p>Now build and deploy:</p>
<pre><code class='language-bash'>docker compose build && docker compose up -d</code></pre> <pre><code class="language-bash">docker compose build && docker compose up -d</code></pre>
</article>
</article>
+1
View File
@@ -70,6 +70,7 @@
} }
}; };
</script> </script>
<svelte:head> <svelte:head>
<title>ClassQuiz - Edit</title> <title>ClassQuiz - Edit</title>
</svelte:head> </svelte:head>
+40 -43
View File
@@ -1,4 +1,4 @@
<script context='module' lang='ts'> <script context="module" lang="ts">
export async function load({ session }) { export async function load({ session }) {
if (!session.authenticated) { if (!session.authenticated) {
return { return {
@@ -13,13 +13,9 @@
}; };
} }
</script> </script>
<svelte:head>
<title>ClassQuiz - Import</title>
</svelte:head>
<script lang='ts'>
let url_input = '';
import tippy from 'sveltejs-tippy'; <script lang="ts">
let url_input = '';
let url_valid = false; let url_valid = false;
let kahoot_regex = /^https:\/\/create\.kahoot\.it\/details\/([a-zA-Z-\d]{36})\/?$/; let kahoot_regex = /^https:\/\/create\.kahoot\.it\/details\/([a-zA-Z-\d]{36})\/?$/;
@@ -51,6 +47,10 @@
}; };
</script> </script>
<svelte:head>
<title>ClassQuiz - Import</title>
</svelte:head>
<!--{#if is_loading} <!--{#if is_loading}
<svg class='h-8 w-8 animate-spin mx-auto my-20' viewBox='3 3 18 18'> <svg class='h-8 w-8 animate-spin mx-auto my-20' viewBox='3 3 18 18'>
<path <path
@@ -68,70 +68,69 @@
<input type='text' class="text-black w-2/5" bind:value={url_input} /> <input type='text' class="text-black w-2/5" bind:value={url_input} />
<button type='submit' disabled={!url_valid}>Submit</button> <button type='submit' disabled={!url_valid}>Submit</button>
</form>--> </form>-->
<div class='flex items-center justify-center h-full px-4'> <div class="flex items-center justify-center h-full px-4">
<div> <div>
<span class='p-4' /> <span class="p-4" />
<div <div
class='lg:w-[64rem] lg:max-w-[64rem] w-screen max-w-screen mx-auto overflow-hidden bg-white rounded-lg shadow-md dark:bg-gray-800' class="lg:w-[64rem] lg:max-w-[64rem] w-screen max-w-screen mx-auto overflow-hidden bg-white rounded-lg shadow-md dark:bg-gray-800"
> >
<div class='px-6 py-4'> <div class="px-6 py-4">
<h2 class='text-3xl font-bold text-center text-gray-700 dark:text-white'> <h2 class="text-3xl font-bold text-center text-gray-700 dark:text-white">
ClassQuiz ClassQuiz
</h2> </h2>
<h3 class='mt-1 text-xl font-medium text-center text-gray-600 dark:text-gray-200'> <h3 class="mt-1 text-xl font-medium text-center text-gray-600 dark:text-gray-200">
Welcome Back Welcome Back
</h3> </h3>
<p class='mt-1 text-center text-gray-500 dark:text-gray-400'> <p class="mt-1 text-center text-gray-500 dark:text-gray-400">
Login or create account Login or create account
</p> </p>
<form on:submit|preventDefault={submit}> <form on:submit|preventDefault={submit}>
<div class='w-full mt-4'> <div class="w-full mt-4">
<div class='dark:bg-gray-800 bg-white p-4 rounded-lg'> <div class="dark:bg-gray-800 bg-white p-4 rounded-lg">
<div class='relative bg-inherit w-full'> <div class="relative bg-inherit w-full">
<input <input
id='url' id="url"
bind:value={url_input} bind:value={url_input}
name='email' name="email"
type='url' type="url"
class='w-full peer bg-transparent h-10 rounded-lg text-gray-700 dark:text-white placeholder-transparent ring-2 px-2 ring-gray-500 focus:ring-sky-600 focus:outline-none focus:border-rose-600' class="w-full peer bg-transparent h-10 rounded-lg text-gray-700 dark:text-white placeholder-transparent ring-2 px-2 ring-gray-500 focus:ring-sky-600 focus:outline-none focus:border-rose-600"
placeholder='https://create.kahoot.it/details/something' placeholder="https://create.kahoot.it/details/something"
class:ring-red-700={!url_valid} class:ring-red-700={!url_valid}
class:ring-green-600={url_valid} class:ring-green-600={url_valid}
/> />
<label <label
for='url' for="url"
class='absolute cursor-text left-0 -top-3 text-sm text-gray-700 dark:text-white bg-inherit mx-1 px-1 peer-placeholder-shown:text-base peer-placeholder-shown:text-gray-500 peer-placeholder-shown:top-2 peer-focus:-top-3 peer-focus:text-sky-600 peer-focus:text-sm transition-all' class="absolute cursor-text left-0 -top-3 text-sm text-gray-700 dark:text-white bg-inherit mx-1 px-1 peer-placeholder-shown:text-base peer-placeholder-shown:text-gray-500 peer-placeholder-shown:top-2 peer-focus:-top-3 peer-focus:text-sky-600 peer-focus:text-sm transition-all"
> >
URL URL
</label> </label>
</div> </div>
</div> </div>
<div class='flex items-center justify-between mt-4'> <div class="flex items-center justify-between mt-4">
<span></span> <span />
<button <button
class='px-4 py-2 leading-5 text-white transition-colors duration-200 transform bg-gray-700 rounded hover:bg-gray-600 focus:outline-none disabled:cursor-not-allowed disabled:opacity-50' class="px-4 py-2 leading-5 text-white transition-colors duration-200 transform bg-gray-700 rounded hover:bg-gray-600 focus:outline-none disabled:cursor-not-allowed disabled:opacity-50"
disabled={!url_valid} disabled={!url_valid}
type='submit' type="submit"
> >
{#if is_loading} {#if is_loading}
<svg <svg
class='h-4 w-4 animate-spin mx-auto my-20' class="h-4 w-4 animate-spin mx-auto my-20"
viewBox='3 3 18 18' viewBox="3 3 18 18"
> >
<path <path
class='fill-black' class="fill-black"
d='M12 5C8.13401 5 5 8.13401 5 12C5 15.866 8.13401 19 12 19C15.866 19 19 15.866 19 12C19 8.13401 15.866 5 12 5ZM3 12C3 7.02944 7.02944 3 12 3C16.9706 3 21 7.02944 21 12C21 16.9706 16.9706 21 12 21C7.02944 21 3 16.9706 3 12Z' d="M12 5C8.13401 5 5 8.13401 5 12C5 15.866 8.13401 19 12 19C15.866 19 19 15.866 19 12C19 8.13401 15.866 5 12 5ZM3 12C3 7.02944 7.02944 3 12 3C16.9706 3 21 7.02944 21 12C21 16.9706 16.9706 21 12 21C7.02944 21 3 16.9706 3 12Z"
/> />
<path <path
class='fill-blue-100' class="fill-blue-100"
d='M16.9497 7.05015C14.2161 4.31648 9.78392 4.31648 7.05025 7.05015C6.65973 7.44067 6.02656 7.44067 5.63604 7.05015C5.24551 6.65962 5.24551 6.02646 5.63604 5.63593C9.15076 2.12121 14.8492 2.12121 18.364 5.63593C18.7545 6.02646 18.7545 6.65962 18.364 7.05015C17.9734 7.44067 17.3403 7.44067 16.9497 7.05015Z' d="M16.9497 7.05015C14.2161 4.31648 9.78392 4.31648 7.05025 7.05015C6.65973 7.44067 6.02656 7.44067 5.63604 7.05015C5.24551 6.65962 5.24551 6.02646 5.63604 5.63593C9.15076 2.12121 14.8492 2.12121 18.364 5.63593C18.7545 6.02646 18.7545 6.65962 18.364 7.05015C17.9734 7.44067 17.3403 7.44067 16.9497 7.05015Z"
/> />
</svg> </svg>
{:else} {:else}
@@ -143,19 +142,17 @@
</form> </form>
</div> </div>
<div <div
class='flex items-center justify-center py-4 text-center bg-gray-50 dark:bg-gray-700' class="flex items-center justify-center py-4 text-center bg-gray-50 dark:bg-gray-700"
> >
<span class='text-sm text-gray-600 dark:text-gray-200' <span class="text-sm text-gray-600 dark:text-gray-200">Need help? </span>
>Need help?
</span>
<a <a
href='/docs/import-from-kahoot' href="/docs/import-from-kahoot"
class='mx-2 text-sm font-bold text-blue-500 dark:text-blue-400 hover:underline' class="mx-2 text-sm font-bold text-blue-500 dark:text-blue-400 hover:underline"
>Visit the docs</a >Visit the docs</a
> >
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<!--{/if}--> <!--{/if}-->
+4 -1
View File
@@ -5,7 +5,10 @@
<svelte:head> <svelte:head>
<title>ClassQuiz - Home</title> <title>ClassQuiz - Home</title>
<meta name="description" content="ClassQuiz is a quiz app like KAHOOT! for students, which is open source and free to use."> <meta
name="description"
content="ClassQuiz is a quiz app like KAHOOT! for students, which is open source and free to use."
/>
</svelte:head> </svelte:head>
<section> <section>
+124 -125
View File
@@ -1,4 +1,4 @@
<script context='module' lang='ts'> <script context="module" lang="ts">
export async function load({ session }) { export async function load({ session }) {
if (!session.authenticated) { if (!session.authenticated) {
return { return {
@@ -14,11 +14,7 @@
} }
</script> </script>
<svelte:head> <script lang="ts">
<title>ClassQuiz - Overview</title>
</svelte:head>
<script lang='ts'>
import type { QuizData } from '../app'; import type { QuizData } from '../app';
import { DateTime } from 'luxon'; import { DateTime } from 'luxon';
@@ -48,157 +44,160 @@
}; };
const deleteQuiz = async (to_delete: string) => { const deleteQuiz = async (to_delete: string) => {
const res = await fetch(`/api/v1/quiz/delete/${to_delete}`, { await fetch(`/api/v1/quiz/delete/${to_delete}`, {
method: 'DELETE' method: 'DELETE'
}); });
window.location.reload(); window.location.reload();
}; };
</script> </script>
<svelte:head>
<title>ClassQuiz - Overview</title>
</svelte:head>
{#await getData()} {#await getData()}
<svg class='h-8 w-8 animate-spin mx-auto my-20' viewBox='3 3 18 18'> <svg class="h-8 w-8 animate-spin mx-auto my-20" viewBox="3 3 18 18">
<path <path
class='fill-black' class="fill-black"
d='M12 5C8.13401 5 5 8.13401 5 12C5 15.866 8.13401 19 12 19C15.866 19 19 15.866 19 12C19 8.13401 15.866 5 12 5ZM3 12C3 7.02944 7.02944 3 12 3C16.9706 3 21 7.02944 21 12C21 16.9706 16.9706 21 12 21C7.02944 21 3 16.9706 3 12Z' d="M12 5C8.13401 5 5 8.13401 5 12C5 15.866 8.13401 19 12 19C15.866 19 19 15.866 19 12C19 8.13401 15.866 5 12 5ZM3 12C3 7.02944 7.02944 3 12 3C16.9706 3 21 7.02944 21 12C21 16.9706 16.9706 21 12 21C7.02944 21 3 16.9706 3 12Z"
/> />
<path <path
class='fill-blue-100' class="fill-blue-100"
d='M16.9497 7.05015C14.2161 4.31648 9.78392 4.31648 7.05025 7.05015C6.65973 7.44067 6.02656 7.44067 5.63604 7.05015C5.24551 6.65962 5.24551 6.02646 5.63604 5.63593C9.15076 2.12121 14.8492 2.12121 18.364 5.63593C18.7545 6.02646 18.7545 6.65962 18.364 7.05015C17.9734 7.44067 17.3403 7.44067 16.9497 7.05015Z' d="M16.9497 7.05015C14.2161 4.31648 9.78392 4.31648 7.05025 7.05015C6.65973 7.44067 6.02656 7.44067 5.63604 7.05015C5.24551 6.65962 5.24551 6.02646 5.63604 5.63593C9.15076 2.12121 14.8492 2.12121 18.364 5.63593C18.7545 6.02646 18.7545 6.65962 18.364 7.05015C17.9734 7.44067 17.3403 7.44067 16.9497 7.05015Z"
/> />
</svg> </svg>
{:then quizzes} {:then quizzes}
<div class='flex flex-col w-fit mx-auto'> <div class="flex flex-col w-fit mx-auto">
<!-- <button <!-- <button
class='px-4 py-2 font-medium tracking-wide text-gray-500 whitespace-nowrap dark:text-gray-400 capitalize transition-colors dark:bg-gray-700 duration-200 transform bg-gray-50 rounded-md hover:bg-green-600 focus:outline-none focus:ring focus:ring-blue-300 focus:ring-opacity-80'> class='px-4 py-2 font-medium tracking-wide text-gray-500 whitespace-nowrap dark:text-gray-400 capitalize transition-colors dark:bg-gray-700 duration-200 transform bg-gray-50 rounded-md hover:bg-green-600 focus:outline-none focus:ring focus:ring-blue-300 focus:ring-opacity-80'>
Primary Primary
</button>--> </button>-->
<div class='w-full grid grid-cols-3 gap-2'> <div class="w-full grid grid-cols-3 gap-2">
<a <a
href='/create' href="/create"
class='px-4 py-2 leading-5 text-white transition-colors duration-200 transform bg-gray-700 rounded text-center hover:bg-gray-600 focus:outline-none disabled:cursor-not-allowed disabled:opacity-50' class="px-4 py-2 leading-5 text-white transition-colors duration-200 transform bg-gray-700 rounded text-center hover:bg-gray-600 focus:outline-none disabled:cursor-not-allowed disabled:opacity-50"
>Create >Create
</a> </a>
<a <a
href='/import' href="/import"
class='px-4 py-2 leading-5 text-white transition-colors duration-200 transform bg-gray-700 rounded text-center hover:bg-gray-600 focus:outline-none disabled:cursor-not-allowed disabled:opacity-50' class="px-4 py-2 leading-5 text-white transition-colors duration-200 transform bg-gray-700 rounded text-center hover:bg-gray-600 focus:outline-none disabled:cursor-not-allowed disabled:opacity-50"
>Import >Import
</a> </a>
<a <a
href='/api/v1/users/logout' href="/api/v1/users/logout"
class='px-4 py-2 leading-5 text-white transition-colors duration-200 transform bg-gray-700 rounded text-center hover:bg-gray-600 focus:outline-none disabled:cursor-not-allowed disabled:opacity-50' class="px-4 py-2 leading-5 text-white transition-colors duration-200 transform bg-gray-700 rounded text-center hover:bg-gray-600 focus:outline-none disabled:cursor-not-allowed disabled:opacity-50"
>Logout >Logout
</a> </a>
</div> </div>
<div class='overflow-x-auto sm:-mx-8 lg:-mx-8'> <div class="overflow-x-auto sm:-mx-8 lg:-mx-8">
<div class='inline-block py-2 min-w-full sm:px-6 lg:px-8'> <div class="inline-block py-2 min-w-full sm:px-6 lg:px-8">
<div class='overflow-hidden shadow-md sm:rounded-lg'> <div class="overflow-hidden shadow-md sm:rounded-lg">
<table class='min-w-full'> <table class="min-w-full">
<thead class='bg-gray-50 dark:bg-gray-700'> <thead class="bg-gray-50 dark:bg-gray-700">
<tr> <tr>
<th <th
scope='col' scope="col"
class='py-3 px-6 text-xs font-medium tracking-wider text-left text-gray-700 uppercase dark:text-gray-400' class="py-3 px-6 text-xs font-medium tracking-wider text-left text-gray-700 uppercase dark:text-gray-400"
> >
Title Title
</th> </th>
<th <th
scope='col' scope="col"
class='py-3 px-6 text-xs font-medium tracking-wider text-left text-gray-700 uppercase dark:text-gray-400' class="py-3 px-6 text-xs font-medium tracking-wider text-left text-gray-700 uppercase dark:text-gray-400"
> >
Created at Created at
</th> </th>
<th <th
scope='col' scope="col"
class='py-3 px-6 text-xs font-medium tracking-wider text-left text-gray-700 uppercase dark:text-gray-400' class="py-3 px-6 text-xs font-medium tracking-wider text-left text-gray-700 uppercase dark:text-gray-400"
> >
Question count Question count
</th> </th>
<th <th
scope='col' scope="col"
class='py-3 px-6 text-xs font-medium tracking-wider text-left text-gray-700 uppercase dark:text-gray-400' class="py-3 px-6 text-xs font-medium tracking-wider text-left text-gray-700 uppercase dark:text-gray-400"
> >
Play Play
</th> </th>
<th <th
scope='col' scope="col"
class='py-3 px-6 text-xs font-medium tracking-wider text-left text-gray-700 uppercase dark:text-gray-400' class="py-3 px-6 text-xs font-medium tracking-wider text-left text-gray-700 uppercase dark:text-gray-400"
> >
Edit Edit
</th> </th>
<th <th
scope='col' scope="col"
class='py-3 px-6 text-xs font-medium tracking-wider text-left text-gray-700 uppercase dark:text-gray-400' class="py-3 px-6 text-xs font-medium tracking-wider text-left text-gray-700 uppercase dark:text-gray-400"
> >
Delete Delete
</th> </th>
<th <th
scope='col' scope="col"
class='py-3 px-6 text-xs font-medium tracking-wider text-left text-gray-700 uppercase dark:text-gray-400' class="py-3 px-6 text-xs font-medium tracking-wider text-left text-gray-700 uppercase dark:text-gray-400"
> >
Public Public
</th> </th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<!-- Product 1 --> <!-- Product 1 -->
{#each quizzes as quiz} {#each quizzes as quiz}
<tr class='bg-white border-b dark:bg-gray-800 dark:border-gray-700'> <tr class="bg-white border-b dark:bg-gray-800 dark:border-gray-700">
<td <td
class='py-4 px-6 text-sm font-medium text-gray-900 whitespace-nowrap dark:text-white' class="py-4 px-6 text-sm font-medium text-gray-900 whitespace-nowrap dark:text-white"
> >
{quiz.title} {quiz.title}
</td> </td>
<td <td
class='py-4 px-6 text-sm text-gray-500 whitespace-nowrap dark:text-gray-400' class="py-4 px-6 text-sm text-gray-500 whitespace-nowrap dark:text-gray-400"
> >
{formatDate(quiz.created_at)} {formatDate(quiz.created_at)}
</td> </td>
<td <td
class='py-4 px-6 text-sm text-gray-500 whitespace-nowrap dark:text-gray-400' class="py-4 px-6 text-sm text-gray-500 whitespace-nowrap dark:text-gray-400"
> >
{quiz.questions.length} {quiz.questions.length}
</td> </td>
<td <td
class='py-4 px-6 text-sm text-gray-500 whitespace-nowrap dark:text-gray-400' class="py-4 px-6 text-sm text-gray-500 whitespace-nowrap dark:text-gray-400"
> >
<button <button
on:click={() => { on:click={() => {
startGame(quiz.id); startGame(quiz.id);
}} }}
class='border border-green-600' class="border border-green-600"
>
Start
</button>
</td>
<td
class="py-4 px-6 text-sm text-gray-500 whitespace-nowrap dark:text-gray-400"
> >
Start <a href="/edit?quiz_id={quiz.id}">Edit</a>
</button> </td>
</td> <td
<td class="py-4 px-6 text-sm text-gray-500 whitespace-nowrap dark:text-gray-400"
class='py-4 px-6 text-sm text-gray-500 whitespace-nowrap dark:text-gray-400' >
> <button
<a href='/edit?quiz_id={quiz.id}'>Edit</a> on:click={() => {
</td>
<td
class='py-4 px-6 text-sm text-gray-500 whitespace-nowrap dark:text-gray-400'
>
<button
on:click={() => {
deleteQuiz(quiz.id); deleteQuiz(quiz.id);
}} }}
class='border border-green-600' class="border border-green-600"
>
Delete
</button>
</td>
<td
class="py-4 px-6 text-sm text-gray-500 whitespace-nowrap dark:text-gray-400"
> >
Delete {#if quiz.public}
</button>
</td> {:else}
<td
class='py-4 px-6 text-sm text-gray-500 whitespace-nowrap dark:text-gray-400' {/if}
> </td>
{#if quiz.public} </tr>
{/each}
{:else}
{/if}
</td>
</tr>
{/each}
</tbody> </tbody>
</table> </table>
</div> </div>
+1
View File
@@ -68,6 +68,7 @@
}); });
// The rest // The rest
</script> </script>
<svelte:head> <svelte:head>
<title>ClassQuiz - Play</title> <title>ClassQuiz - Play</title>
</svelte:head> </svelte:head>
+17 -17
View File
@@ -1,19 +1,19 @@
{ {
"name": "ClassQuiz", "name": "ClassQuiz",
"short_name": "ClassQuiz", "short_name": "ClassQuiz",
"icons": [ "icons": [
{ {
"src": "/android-chrome-192x192.png", "src": "/android-chrome-192x192.png",
"sizes": "192x192", "sizes": "192x192",
"type": "image/png" "type": "image/png"
}, },
{ {
"src": "/android-chrome-512x512.png", "src": "/android-chrome-512x512.png",
"sizes": "512x512", "sizes": "512x512",
"type": "image/png" "type": "image/png"
} }
], ],
"theme_color": "#ffffff", "theme_color": "#ffffff",
"background_color": "#ffffff", "background_color": "#ffffff",
"display": "standalone" "display": "standalone"
} }