🚸 Refreshed admin-screen and qr-endpoint!
This commit is contained in:
@@ -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
@@ -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,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))
|
||||||
|
|||||||
@@ -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")
|
||||||
@@ -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",
|
||||||
|
|||||||
Generated
+18
@@ -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'}
|
||||||
|
|||||||
@@ -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'
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
if (session.authenticated) {
|
if (session.authenticated) {
|
||||||
return {
|
return {
|
||||||
status: 302,
|
status: 302,
|
||||||
redirect: '/'
|
redirect: '/overview'
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
return {};
|
return {};
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
if (session.authenticated) {
|
if (session.authenticated) {
|
||||||
return {
|
return {
|
||||||
status: 302,
|
status: 302,
|
||||||
redirect: '/'
|
redirect: '/overview'
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
return {};
|
return {};
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
Reference in New Issue
Block a user