🚸 Refreshed admin-screen and qr-endpoint!

This commit is contained in:
Mawoka
2022-03-03 20:41:45 +01:00
parent 829fbc768d
commit 493a5c0bdf
11 changed files with 221 additions and 29 deletions
+1
View File
@@ -17,6 +17,7 @@ pydantic = "*"
redis = "==4.2.0rc1" redis = "==4.2.0rc1"
aiohttp = "*" aiohttp = "*"
gunicorn = { version = "*"} gunicorn = { version = "*"}
qrcode = "*"
[dev-packages] [dev-packages]
Generated
+10 -3
View File
@@ -1,7 +1,7 @@
{ {
"_meta": { "_meta": {
"hash": { "hash": {
"sha256": "6667340defc4a3152bca65e1359586067a229573e833d055c86a6ef800e64c15" "sha256": "dc7b78e9bce0cdb40c43f8d8bcdc80a2d4dc8d56c430808ea702ae8b0d6740fa"
}, },
"pipfile-spec": 6, "pipfile-spec": 6,
"requires": { "requires": {
@@ -226,7 +226,7 @@
"sha256:081649da27ced5e75709a1ee542136eaba9842a0fe4c03da4fb0a3d3ed1f3c44", "sha256:081649da27ced5e75709a1ee542136eaba9842a0fe4c03da4fb0a3d3ed1f3c44",
"sha256:e79351e032d0b606b98d38a4b0e6e2275b31a5b85c873e587cc11b73aca026d6" "sha256:e79351e032d0b606b98d38a4b0e6e2275b31a5b85c873e587cc11b73aca026d6"
], ],
"markers": "python_version >= '3.6' and python_full_version < '4.0.0'", "markers": "python_version >= '3.6' and python_version < '4'",
"version": "==2.2.0" "version": "==2.2.0"
}, },
"ecdsa": { "ecdsa": {
@@ -783,6 +783,13 @@
], ],
"version": "==6.0" "version": "==6.0"
}, },
"qrcode": {
"hashes": [
"sha256:375a6ff240ca9bd41adc070428b5dfc1dcfbb0f2507f1ac848f6cded38956578"
],
"index": "pypi",
"version": "==7.3.1"
},
"redis": { "redis": {
"hashes": [ "hashes": [
"sha256:95ff6381bc15acbb330d24896601fb8c50468003c0f30c9503afda2907225d08", "sha256:95ff6381bc15acbb330d24896601fb8c50468003c0f30c9503afda2907225d08",
@@ -796,7 +803,7 @@
"sha256:5c6bd9dc7a543b7fe4304a631f8a8a3b674e2bbfc49c2ae96200cdbe55df6b17", "sha256:5c6bd9dc7a543b7fe4304a631f8a8a3b674e2bbfc49c2ae96200cdbe55df6b17",
"sha256:95c5d300c4e879ee69708c428ba566c59478fd653cc3a22243eeb8ed846950bb" "sha256:95c5d300c4e879ee69708c428ba566c59478fd653cc3a22243eeb8ed846950bb"
], ],
"markers": "python_version >= '3.6' and python_full_version < '4.0.0'", "markers": "python_version >= '3.6' and python_version < '4'",
"version": "==4.8" "version": "==4.8"
}, },
"setuptools": { "setuptools": {
+2 -1
View File
@@ -2,7 +2,7 @@ from fastapi import FastAPI
from classquiz.socket_server import sio from classquiz.socket_server import sio
from socketio import ASGIApp from socketio import ASGIApp
from classquiz.routers import users, quiz from classquiz.routers import users, quiz, utils
from classquiz.db import database from classquiz.db import database
app = FastAPI() app = FastAPI()
@@ -25,4 +25,5 @@ async def shutdown() -> None:
app.include_router(users.router, tags=["users"], prefix="/api/v1/users") app.include_router(users.router, tags=["users"], prefix="/api/v1/users")
app.include_router(quiz.router, tags=["quiz"], prefix="/api/v1/quiz") app.include_router(quiz.router, tags=["quiz"], prefix="/api/v1/quiz")
app.include_router(utils.router, tags=["utils"], prefix="/api/v1/utils")
app.mount("/", ASGIApp(sio)) app.mount("/", ASGIApp(sio))
+24
View File
@@ -0,0 +1,24 @@
import io
import qrcode
import qrcode.image.svg
from fastapi import APIRouter
from classquiz.config import settings
from fastapi.responses import Response
router = APIRouter()
@router.get("/qr/{quiz_pin}")
async def get_qr(quiz_pin: str):
"""
Get QR code for quiz
"""
factory = qrcode.image.svg.SvgImage
qr = qrcode.QRCode(image_factory=qrcode.image.svg.SvgPathImage, version=1
, box_size=20, border=0)
qr.add_data(f"{settings.root_address}/play?pin={quiz_pin}")
buf = io.BytesIO()
qr.make(fit=True)
qr.make_image(fill_color="black", back_color="white").save(buf, "SVG")
return Response(content=buf.getvalue(), media_type="image/svg+xml")
+1
View File
@@ -41,6 +41,7 @@
"svelte": "^3.44.0", "svelte": "^3.44.0",
"svelte-check": "^2.2.6", "svelte-check": "^2.2.6",
"svelte-preprocess": "^4.10.1", "svelte-preprocess": "^4.10.1",
"sveltejs-tippy": "^3.0.0",
"tailwindcss": "^3.0.12", "tailwindcss": "^3.0.12",
"tslib": "^2.3.1", "tslib": "^2.3.1",
"typescript": "~4.5.4", "typescript": "~4.5.4",
+18
View File
@@ -29,6 +29,7 @@ specifiers:
svelte: ^3.44.0 svelte: ^3.44.0
svelte-check: ^2.2.6 svelte-check: ^2.2.6
svelte-preprocess: ^4.10.1 svelte-preprocess: ^4.10.1
sveltejs-tippy: ^3.0.0
tailwindcss: ^3.0.12 tailwindcss: ^3.0.12
tslib: ^2.3.1 tslib: ^2.3.1
typescript: ~4.5.4 typescript: ~4.5.4
@@ -63,6 +64,7 @@ devDependencies:
svelte: 3.46.4 svelte: 3.46.4
svelte-check: 2.4.5_bb21b93d557fc716a6ae071c4dce10e1 svelte-check: 2.4.5_bb21b93d557fc716a6ae071c4dce10e1
svelte-preprocess: 4.10.4_c54fd0a3479054b5002c3dc2e70c9c9a svelte-preprocess: 4.10.4_c54fd0a3479054b5002c3dc2e70c9c9a
sveltejs-tippy: 3.0.0
tailwindcss: 3.0.23_autoprefixer@10.4.2 tailwindcss: 3.0.23_autoprefixer@10.4.2
tslib: 2.3.1 tslib: 2.3.1
typescript: 4.5.5 typescript: 4.5.5
@@ -180,6 +182,10 @@ packages:
fastq: 1.13.0 fastq: 1.13.0
dev: true dev: true
/@popperjs/core/2.11.2:
resolution: {integrity: sha512-92FRmppjjqz29VMJ2dn+xdyXZBrMlE42AV6Kq6BwjWV7CNUW1hs2FtxSNLQE+gJhaZ6AAmYuO9y8dshhcBl7vA==}
dev: true
/@rollup/pluginutils/4.1.2: /@rollup/pluginutils/4.1.2:
resolution: {integrity: sha512-ROn4qvkxP9SyPeHaf7uQC/GPFY6L/OWy9+bd9AwcjOAWQwxRscoEyAUD8qCY5o5iL4jqQwoLk2kaTKJPb/HwzQ==} resolution: {integrity: sha512-ROn4qvkxP9SyPeHaf7uQC/GPFY6L/OWy9+bd9AwcjOAWQwxRscoEyAUD8qCY5o5iL4jqQwoLk2kaTKJPb/HwzQ==}
engines: {node: '>= 8.0.0'} engines: {node: '>= 8.0.0'}
@@ -2174,6 +2180,12 @@ packages:
engines: {node: '>= 8'} engines: {node: '>= 8'}
dev: true dev: true
/sveltejs-tippy/3.0.0:
resolution: {integrity: sha512-LAfQikm61AeqZW1hCFWUsmGEMmhMHDvrPV0JhbT38Ell9SqBjmFaY//+YKUxaJvx6nLMxGQdlcHA1rDg2Ml8tQ==}
dependencies:
tippy.js: 6.0.3
dev: true
/table/6.8.0: /table/6.8.0:
resolution: {integrity: sha512-s/fitrbVeEyHKFa7mFdkuQMWlH1Wgw/yEXMt5xACT4ZpzWFluehAxRtUUQKPuWhaLAWhFcVx6w3oC8VKaUfPGA==} resolution: {integrity: sha512-s/fitrbVeEyHKFa7mFdkuQMWlH1Wgw/yEXMt5xACT4ZpzWFluehAxRtUUQKPuWhaLAWhFcVx6w3oC8VKaUfPGA==}
engines: {node: '>=10.0.0'} engines: {node: '>=10.0.0'}
@@ -2229,6 +2241,12 @@ packages:
globrex: 0.1.2 globrex: 0.1.2
dev: true dev: true
/tippy.js/6.0.3:
resolution: {integrity: sha512-buQF6HugTA4YeSA/s9xBhu0ferEAx7sRSn45G+Juh3p+Dz3vEKfxtqdg6JUnHIWDCV/r0u8Lrobs9AB4dtYcUQ==}
dependencies:
'@popperjs/core': 2.11.2
dev: true
/to-regex-range/5.0.1: /to-regex-range/5.0.1:
resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==}
engines: {node: '>=8.0'} engines: {node: '>=8.0'}
+28 -21
View File
@@ -1,7 +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 tippy from 'sveltejs-tippy';
export let game_pin: string; export let game_pin: string;
@@ -10,7 +11,8 @@
let hcaptcha = { let hcaptcha = {
execute: async (_a, _b) => ({ response: '' }), execute: async (_a, _b) => ({ response: '' }),
render: (_a, _b) => {} render: (_a, _b) => {
}
}; };
let hcaptchaWidgetID; let hcaptchaWidgetID;
@@ -31,7 +33,8 @@
if (browser) { if (browser) {
hcaptcha = { hcaptcha = {
execute: async () => ({ response: '' }), execute: async () => ({ response: '' }),
render: () => {} render: () => {
}
}; };
} }
}); });
@@ -50,46 +53,50 @@
}); });
}; };
socket.on('game_not_found', () => { socket.on('game_not_found', () => {
game_pin = '';
alert('Game not found'); alert('Game not found');
}); });
</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" 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'}}-->
<br /> <br />
<button <button
class="bg-amber-800 hover:bg-amber-700 text-white font-bold py-2 px-4 rounded" 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}
> >
Submit Submit
</button> </button>
</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" 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" 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
@@ -98,9 +105,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'
/> />
+1 -1
View File
@@ -3,7 +3,7 @@
if (session.authenticated) { if (session.authenticated) {
return { return {
status: 302, status: 302,
redirect: '/' redirect: '/overview'
}; };
} }
return {}; return {};
+1 -1
View File
@@ -3,7 +3,7 @@
if (session.authenticated) { if (session.authenticated) {
return { return {
status: 302, status: 302,
redirect: '/' redirect: '/overview'
}; };
} }
return {}; return {};
+13 -1
View File
@@ -8,10 +8,15 @@
} }
const token = url.searchParams.get('token'); const token = url.searchParams.get('token');
const pin = url.searchParams.get('pin'); const pin = url.searchParams.get('pin');
let auto_connect = url.searchParams.get('connect') !== null;
if (token === null || pin === null) {
auto_connect = false;
}
return { return {
props: { props: {
game_pin: pin === null ? '' : pin, game_pin: pin === null ? '' : pin,
game_token: token === null ? '' : token game_token: token === null ? '' : token,
auto_connect: auto_connect
} }
}; };
} }
@@ -28,6 +33,7 @@
// game_pin: '66190765' // game_pin: '66190765'
// }; // };
export let game_pin: string; export let game_pin: string;
export let auto_connect: boolean;
export let game_token: string; export let game_token: string;
console.log(game_pin, game_token); console.log(game_pin, game_token);
let success = false; let success = false;
@@ -45,6 +51,9 @@
game_id: game_token game_id: game_token
}); });
}; };
if (auto_connect) {
connect();
}
socket.on('registered_as_admin', (data) => { socket.on('registered_as_admin', (data) => {
console.log('registered_as_admin', data['game']); console.log('registered_as_admin', data['game']);
quiz_data = JSON.parse(data['game']); quiz_data = JSON.parse(data['game']);
@@ -104,8 +113,11 @@
<p class='text-red-700'>{errorMessage}</p> <p class='text-red-700'>{errorMessage}</p>
{/if} {/if}
{:else if !game_started} {:else if !game_started}
<img src='/api/v1/utils/qr/{quiz_data.game_pin}' class='block mx-auto w-1/6' />
<p class='text-3xl text-center'>Pin: {quiz_data.game_pin}</p>
<ul> <ul>
{#if players.length > 0} {#if players.length > 0}
{#each players as player} {#each players as player}
<li> <li>
<span>{player.username} </span> <span>{player.username} </span>
+122 -1
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,3 +13,124 @@
}; };
} }
</script> </script>
<script lang='ts'>
import type { QuizData } from '../app';
import { DateTime } from 'luxon';
const getData = async (): Promise<Array<QuizData>> => {
const res = await fetch('/api/v1/quiz/list');
const data = await res.json();
return data;
};
const formatDate = (date: string): string => {
const dt = DateTime.fromISO(date);
return dt.toLocaleString(DateTime.DATETIME_MED);
};
const startGame = async (id: string): Promise<void> => {
console.log('start game', id);
const res = await fetch(`/api/v1/quiz/start/${id}`, {
method: 'POST'
// headers: {
// 'Content-Type': 'application/json'
// }
});
if (res.status !== 200) {
throw new Error('Failed to start game');
}
const data = await res.json();
window.location.replace(`/admin?token=${data.game_id}&pin=${data.game_pin}&connect=1`);
};
</script>
{#await getData()}
<p>Loading...</p>
{:then quizzes}
<div class='flex flex-col w-fit'>
<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='overflow-hidden shadow-md sm:rounded-lg'>
<table class='min-w-full'>
<thead class='bg-gray-50 dark:bg-gray-700'>
<tr>
<th
scope='col'
class='py-3 px-6 text-xs font-medium tracking-wider text-left text-gray-700 uppercase dark:text-gray-400'
>
Title
</th>
<th
scope='col'
class='py-3 px-6 text-xs font-medium tracking-wider text-left text-gray-700 uppercase dark:text-gray-400'
>
Created at
</th>
<th
scope='col'
class='py-3 px-6 text-xs font-medium tracking-wider text-left text-gray-700 uppercase dark:text-gray-400'
>
Question count
</th>
<th
scope='col'
class='py-3 px-6 text-xs font-medium tracking-wider text-left text-gray-700 uppercase dark:text-gray-400'
>
Play
</th>
<th
scope='col'
class='py-3 px-6 text-xs font-medium tracking-wider text-left text-gray-700 uppercase dark:text-gray-400'
>
Edit
</th>
</tr>
</thead>
<tbody>
<!-- Product 1 -->
{#each quizzes as quiz}
<tr class='bg-white border-b dark:bg-gray-800 dark:border-gray-700'>
<td
class='py-4 px-6 text-sm font-medium text-gray-900 whitespace-nowrap dark:text-white'
>
{quiz.title}
</td>
<td
class='py-4 px-6 text-sm text-gray-500 whitespace-nowrap dark:text-gray-400'
>
{formatDate(quiz.created_at)}
</td>
<td
class='py-4 px-6 text-sm text-gray-500 whitespace-nowrap dark:text-gray-400'
>
{quiz.questions.length}
</td>
<td
class='py-4 px-6 text-sm text-gray-500 whitespace-nowrap dark:text-gray-400'
>
<button on:click={() => {startGame(quiz.id)}} 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'
>
<a href='/edit/{quiz.id}'>Edit</a>
</td>
</tr>
{/each}
</tbody>
</table>
</div>
</div>
</div>
</div>
{:catch err}
<p>{err}</p>
{/await}