Get the site functional for the game release
This commit is contained in:
parent
4b61e73573
commit
33ab4afe52
16 changed files with 575 additions and 30 deletions
|
|
@ -3,7 +3,7 @@ import { ServerRoute } from "@hapi/hapi";
|
||||||
import Joi from "joi";
|
import Joi from "joi";
|
||||||
|
|
||||||
const route: ServerRoute = {
|
const route: ServerRoute = {
|
||||||
method: `POST`, path: `/{channel}/questions/{question_id}`,
|
method: `PATCH`, path: `/{channel}/questions/{question_id}`,
|
||||||
options: {
|
options: {
|
||||||
validate: {
|
validate: {
|
||||||
params: Joi.object({
|
params: Joi.object({
|
||||||
|
|
@ -21,6 +21,7 @@ const route: ServerRoute = {
|
||||||
question: Joi.string().optional(),
|
question: Joi.string().optional(),
|
||||||
asker: Joi.string().optional(),
|
asker: Joi.string().optional(),
|
||||||
answered: Joi.boolean().optional(),
|
answered: Joi.boolean().optional(),
|
||||||
|
hidden: Joi.boolean().optional(),
|
||||||
id: Joi.forbidden(),
|
id: Joi.forbidden(),
|
||||||
})
|
})
|
||||||
.min(1),
|
.min(1),
|
||||||
|
|
@ -41,17 +42,24 @@ const route: ServerRoute = {
|
||||||
let values = [];
|
let values = [];
|
||||||
for (const key in payload) {
|
for (const key in payload) {
|
||||||
let v = payload[key];
|
let v = payload[key];
|
||||||
if (v.startsWith(`__`)) {
|
|
||||||
|
let type = typeof v;
|
||||||
|
if (type == "boolean") {
|
||||||
setters.push(`${key} = ${v}`);
|
setters.push(`${key} = ${v}`);
|
||||||
} else {
|
}
|
||||||
setters.push(`${key} = ?`);
|
else if (type == "string") {
|
||||||
values.push(v);
|
if (v.startsWith(`__`)) {
|
||||||
|
setters.push(`${key} = ${v.slice(2)}`);
|
||||||
|
} else {
|
||||||
|
setters.push(`${key} = ?`);
|
||||||
|
values.push(v);
|
||||||
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
await db.query(
|
await db.query(
|
||||||
`update tqna.questions
|
`update tqna.questions
|
||||||
( ${setters.join(`, `)} )
|
set ${setters.join(`, `)}
|
||||||
where channel = ? and id = ?
|
where channel = ? and id = ?
|
||||||
limit 1`,
|
limit 1`,
|
||||||
[...values, channel, question_id ]
|
[...values, channel, question_id ]
|
||||||
|
|
@ -64,8 +72,9 @@ const route: ServerRoute = {
|
||||||
question = questions[0];
|
question = questions[0];
|
||||||
|
|
||||||
await conn.commit();
|
await conn.commit();
|
||||||
} catch {
|
} catch (e) {
|
||||||
log.error(`Failed to add the question`);
|
log.error(e)
|
||||||
|
log.error(`Failed to save the question`);
|
||||||
await conn.rollback();
|
await conn.rollback();
|
||||||
} finally {
|
} finally {
|
||||||
conn.release();
|
conn.release();
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import { Server } from "@hapi/hapi";
|
import { Server } from "@hapi/hapi";
|
||||||
|
import { isBoom } from "@hapi/boom";
|
||||||
import { globSync } from "glob";
|
import { globSync } from "glob";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
import { log } from "./main";
|
import { log } from "./main";
|
||||||
|
|
@ -10,6 +11,42 @@ const server = new Server({
|
||||||
debug: {
|
debug: {
|
||||||
request: [ `*` ],
|
request: [ `*` ],
|
||||||
},
|
},
|
||||||
|
router: {
|
||||||
|
stripTrailingSlash: true,
|
||||||
|
},
|
||||||
|
routes: {
|
||||||
|
cors: {
|
||||||
|
origin: [ `*` ],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
/*
|
||||||
|
This event listener makes it so that the error that is returned from the system
|
||||||
|
is more user-friendly when it's a validation error, and so that nothing gets
|
||||||
|
leaked accidentally through allowing other data to make it out of the API.
|
||||||
|
*/
|
||||||
|
server.ext(`onPreResponse`, (req, h) => {
|
||||||
|
if (isBoom(req.response)) {
|
||||||
|
let oldResponse = req.response.output.payload as any;
|
||||||
|
let newResponse: any = {
|
||||||
|
statusCode: oldResponse.statusCode,
|
||||||
|
error: oldResponse.error,
|
||||||
|
message: oldResponse.message,
|
||||||
|
};
|
||||||
|
|
||||||
|
let deets = (req.response as any).details as any[];
|
||||||
|
if (deets) {
|
||||||
|
let messages = deets.map(e => e.message);
|
||||||
|
newResponse.message = (req.response as any).output.payload.validation.source + ` failed to validate`;
|
||||||
|
newResponse.violations = messages;
|
||||||
|
};
|
||||||
|
|
||||||
|
req.response.output.payload = newResponse;
|
||||||
|
return h.continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
return h.continue;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
91
frontend/package-lock.json
generated
91
frontend/package-lock.json
generated
|
|
@ -8,6 +8,7 @@
|
||||||
"name": "frontend",
|
"name": "frontend",
|
||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"axios": "^1.5.0",
|
||||||
"svelte-i18n": "^3.7.0"
|
"svelte-i18n": "^3.7.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|
@ -599,6 +600,21 @@
|
||||||
"dequal": "^2.0.3"
|
"dequal": "^2.0.3"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/asynckit": {
|
||||||
|
"version": "0.4.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
|
||||||
|
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="
|
||||||
|
},
|
||||||
|
"node_modules/axios": {
|
||||||
|
"version": "1.5.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/axios/-/axios-1.5.0.tgz",
|
||||||
|
"integrity": "sha512-D4DdjDo5CY50Qms0qGQTTw6Q44jl7zRwY7bthds06pUGfChBCTcQs+N743eFWGEd6pRTMd6A+I87aWyFV5wiZQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"follow-redirects": "^1.15.0",
|
||||||
|
"form-data": "^4.0.0",
|
||||||
|
"proxy-from-env": "^1.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/axobject-query": {
|
"node_modules/axobject-query": {
|
||||||
"version": "3.2.1",
|
"version": "3.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-3.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-3.2.1.tgz",
|
||||||
|
|
@ -716,6 +732,17 @@
|
||||||
"periscopic": "^3.1.0"
|
"periscopic": "^3.1.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/combined-stream": {
|
||||||
|
"version": "1.0.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
|
||||||
|
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
|
||||||
|
"dependencies": {
|
||||||
|
"delayed-stream": "~1.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/concat-map": {
|
"node_modules/concat-map": {
|
||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
|
||||||
|
|
@ -768,6 +795,14 @@
|
||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/delayed-stream": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/dequal": {
|
"node_modules/dequal": {
|
||||||
"version": "2.0.3",
|
"version": "2.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz",
|
||||||
|
|
@ -939,6 +974,38 @@
|
||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/follow-redirects": {
|
||||||
|
"version": "1.15.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz",
|
||||||
|
"integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "individual",
|
||||||
|
"url": "https://github.com/sponsors/RubenVerborgh"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=4.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"debug": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/form-data": {
|
||||||
|
"version": "4.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
|
||||||
|
"integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==",
|
||||||
|
"dependencies": {
|
||||||
|
"asynckit": "^0.4.0",
|
||||||
|
"combined-stream": "^1.0.8",
|
||||||
|
"mime-types": "^2.1.12"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/fs.realpath": {
|
"node_modules/fs.realpath": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
|
||||||
|
|
@ -1180,6 +1247,25 @@
|
||||||
"node": ">=8.6"
|
"node": ">=8.6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/mime-db": {
|
||||||
|
"version": "1.52.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
|
||||||
|
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/mime-types": {
|
||||||
|
"version": "2.1.35",
|
||||||
|
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
|
||||||
|
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
|
||||||
|
"dependencies": {
|
||||||
|
"mime-db": "1.52.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/min-indent": {
|
"node_modules/min-indent": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz",
|
||||||
|
|
@ -1354,6 +1440,11 @@
|
||||||
"node": "^10 || ^12 || >=14"
|
"node": "^10 || ^12 || >=14"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/proxy-from-env": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="
|
||||||
|
},
|
||||||
"node_modules/queue-microtask": {
|
"node_modules/queue-microtask": {
|
||||||
"version": "1.2.3",
|
"version": "1.2.3",
|
||||||
"resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,7 @@
|
||||||
"vite": "^4.4.5"
|
"vite": "^4.4.5"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"axios": "^1.5.0",
|
||||||
"svelte-i18n": "^3.7.0"
|
"svelte-i18n": "^3.7.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
BIN
frontend/public/OpenDyslexic3-Bold.ttf
Normal file
BIN
frontend/public/OpenDyslexic3-Bold.ttf
Normal file
Binary file not shown.
BIN
frontend/public/OpenDyslexic3-Regular.ttf
Normal file
BIN
frontend/public/OpenDyslexic3-Regular.ttf
Normal file
Binary file not shown.
|
|
@ -1,3 +1,17 @@
|
||||||
{
|
{
|
||||||
"docs.svelte": "Svelte Docs"
|
"option.streamer-mode": "Streamer Mode",
|
||||||
}
|
"option.dyslexic-font": "Use Dyslexic Font",
|
||||||
|
"option.refresh-interval": "Refresh Interval",
|
||||||
|
"option.locale": "Language",
|
||||||
|
"option.join-channel": "Join Twitch Channel",
|
||||||
|
"button.refresh": "Refresh",
|
||||||
|
"button.extra-settings": "Extra Settings",
|
||||||
|
"display.question.button.answered": "Answered",
|
||||||
|
"display.question.button.hide-from-streamer": "Hide from Streamer",
|
||||||
|
"display.question.button.show-for-streamer": "Show to Streamer",
|
||||||
|
"display.question.asked-by": "asked by:",
|
||||||
|
"display.modal.settings.title": "Extra Settings",
|
||||||
|
"display.modal.settings.description": "Any changes made to these options are automatically saved to your browser and will be restored on page re-load.",
|
||||||
|
"display.modal.close": "Close",
|
||||||
|
"option.delete-questions": "Delete All Questions"
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,19 +1,82 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { _, locale, locales } from "svelte-i18n";
|
import { t } from "svelte-i18n";
|
||||||
import localeNames from "./locales";
|
import Integer from "./components/integer.svelte";
|
||||||
|
import Question from "./components/question.svelte";
|
||||||
|
import { dyslexiaFont, isStreamer, questions, refreshInterval, visibleModal } from "./stores";
|
||||||
|
import { api } from "./main";
|
||||||
|
import SettingsModal from "./components/modals/settings.svelte";
|
||||||
|
|
||||||
|
async function loadQuestions() {
|
||||||
|
let r = await api.get(`./questions`, { validateStatus: null });
|
||||||
|
switch (r.status) {
|
||||||
|
case 200:
|
||||||
|
questions.set(r.data);
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
$: if ($dyslexiaFont) {
|
||||||
|
window.document.body.classList.add(`dyslexic`);
|
||||||
|
localStorage.setItem(`tqna-dyslexic`, `true`);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
window.document.body.classList.remove(`dyslexic`);
|
||||||
|
localStorage.setItem(`tqna-dyslexic`, `false`);
|
||||||
|
};
|
||||||
|
|
||||||
|
let intervalId = -1;
|
||||||
|
$: {
|
||||||
|
if (intervalId >= 0) {
|
||||||
|
clearInterval(intervalId);
|
||||||
|
};
|
||||||
|
if ($refreshInterval > 0) {
|
||||||
|
intervalId = setInterval(loadQuestions, $refreshInterval * 1_000);
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
$: visibleQuestions = $questions
|
||||||
|
.filter(q => !(
|
||||||
|
$isStreamer
|
||||||
|
&& (
|
||||||
|
q.answered
|
||||||
|
|| q.hidden
|
||||||
|
)));
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<main>
|
<main>
|
||||||
<a href="https://svelte.dev/docs" target="_blank" rel="noreferrer">
|
<svelte:component this={$visibleModal} />
|
||||||
{$_("docs.svelte")}
|
<div class="option-row">
|
||||||
</a>
|
<Integer
|
||||||
<br><br>
|
value={refreshInterval}
|
||||||
<select bind:value={$locale}>
|
label="{$t("option.refresh-interval")}"
|
||||||
{#each $locales as locale}
|
min={0}
|
||||||
<option value="{locale}">{localeNames[locale]}</option>
|
max={30}
|
||||||
|
unitName="s"
|
||||||
|
/>
|
||||||
|
<button on:click|stopPropagation={loadQuestions}>
|
||||||
|
{$t("button.refresh")}
|
||||||
|
</button>
|
||||||
|
<button on:click|stopPropagation={() => $visibleModal = SettingsModal}>
|
||||||
|
{$t("button.extra-settings")}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<hr>
|
||||||
|
<div class="questions">
|
||||||
|
{#each visibleQuestions as q}
|
||||||
|
<Question {q} streamer={isStreamer} on:loadQuestions={loadQuestions} />
|
||||||
{/each}
|
{/each}
|
||||||
</select>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
.option-row {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
}
|
||||||
|
|
||||||
|
.questions {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
justify-content: space-evenly;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
||||||
|
|
@ -1,17 +1,56 @@
|
||||||
html, body {
|
@font-face {
|
||||||
padding: 0;
|
font-family: openDyslexic;
|
||||||
margin: 0;
|
src: url(/OpenDyslexic3-Regular.ttf) format("truetype");
|
||||||
min-height: 100vh;
|
font-style: normal;
|
||||||
min-width: 100vw;
|
}
|
||||||
|
@font-face {
|
||||||
|
font-family: openDyslexic;
|
||||||
|
src: url(/OpenDyslexic3-Bold.ttf) format("truetype");
|
||||||
|
font-style: bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body, html {
|
||||||
background-color: #2c2c32;
|
width: 100vw;
|
||||||
|
background: #2a2d2f;
|
||||||
|
color: white;
|
||||||
|
font-family: sans-serif;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.dyslexic {
|
||||||
|
font-family: openDyslexic, sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
font-size: inherit;
|
||||||
|
margin: 2px;
|
||||||
|
padding: 10px 14px;
|
||||||
|
border-radius: 5px;
|
||||||
|
outline: none;
|
||||||
|
border-style: none;
|
||||||
|
background: rgba(0,0,0, 0.25);
|
||||||
|
color: white;
|
||||||
|
font-family: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-background {
|
||||||
|
position: fixed;
|
||||||
|
width: 100vw;
|
||||||
|
height: 100vh;
|
||||||
|
background: rgba(0,0,0, 0.25);
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
a {
|
.modal-content {
|
||||||
color: white;
|
background: #2a2d2f;
|
||||||
|
min-width: 30%;
|
||||||
|
min-height: 25%;
|
||||||
|
padding: 25px;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1, h2, h3 {
|
||||||
|
margin: 0;
|
||||||
}
|
}
|
||||||
39
frontend/src/components/integer.svelte
Normal file
39
frontend/src/components/integer.svelte
Normal file
|
|
@ -0,0 +1,39 @@
|
||||||
|
<script lang="ts">
|
||||||
|
export let value;
|
||||||
|
export let label;
|
||||||
|
export let min = 0;
|
||||||
|
export let max = null;
|
||||||
|
export let unitName = ``;
|
||||||
|
|
||||||
|
let id = `input-number-${label.replace(/ /g, `-`)}`;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="int-input">
|
||||||
|
<label for={id}>{label}</label>
|
||||||
|
<input
|
||||||
|
type="range"
|
||||||
|
{id}
|
||||||
|
{min}
|
||||||
|
{max}
|
||||||
|
bind:value={$value}
|
||||||
|
>
|
||||||
|
( {$value}{unitName} )
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.int-input {
|
||||||
|
margin: 5px 10px;
|
||||||
|
border-radius: 7px;
|
||||||
|
border-style: none;
|
||||||
|
border-width: 0px;
|
||||||
|
background: rgba(0,0,0, 0.25);
|
||||||
|
padding: 5px 7px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
input {
|
||||||
|
margin: 0 6px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
94
frontend/src/components/modals/settings.svelte
Normal file
94
frontend/src/components/modals/settings.svelte
Normal file
|
|
@ -0,0 +1,94 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { t, locales, locale } from "svelte-i18n";
|
||||||
|
import { isStreamer, dyslexiaFont, visibleModal } from "../../stores";
|
||||||
|
import localeNames from "../../locales";
|
||||||
|
import Toggle from "../toggle.svelte";
|
||||||
|
import { api } from "../../main";
|
||||||
|
|
||||||
|
async function startBot() {
|
||||||
|
let r = await api.post(`.`, { validateStatus: null });
|
||||||
|
switch (r.status) {
|
||||||
|
case 202:
|
||||||
|
alert(`Bot already listening to channel`);
|
||||||
|
break;
|
||||||
|
case 200:
|
||||||
|
alert(`Started bot in channel`);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="modal-background">
|
||||||
|
<div class="modal-content">
|
||||||
|
<h1>{$t("display.modal.settings.title")}</h1>
|
||||||
|
<p>{$t("display.modal.settings.description")}</p>
|
||||||
|
<div class="options">
|
||||||
|
<Toggle value={isStreamer} label="{$t("option.streamer-mode")}" />
|
||||||
|
<Toggle value={dyslexiaFont} label="{$t("option.dyslexic-font")}" />
|
||||||
|
<select bind:value={$locale}>
|
||||||
|
{#each $locales as lang}
|
||||||
|
<option value="{lang}">{localeNames[lang]}</option>
|
||||||
|
{/each}
|
||||||
|
</select>
|
||||||
|
<button
|
||||||
|
on:click={startBot}
|
||||||
|
>
|
||||||
|
{$t("option.join-channel")}
|
||||||
|
</button>
|
||||||
|
<!--
|
||||||
|
<button>
|
||||||
|
{$t("option.delete-questions")}
|
||||||
|
</button>
|
||||||
|
-->
|
||||||
|
</div>
|
||||||
|
<div class="button-row">
|
||||||
|
<button
|
||||||
|
on:click={() => $visibleModal = null}
|
||||||
|
>
|
||||||
|
{$t("display.modal.close")}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.modal-background {
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.modal-content {
|
||||||
|
border-radius: 25px;
|
||||||
|
max-width: 60%;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.options {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
.button-row {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
select {
|
||||||
|
margin: 5px 10px;
|
||||||
|
border-radius: 7px;
|
||||||
|
border-style: none;
|
||||||
|
border-width: 0px;
|
||||||
|
background: rgba(0,0,0, 0.25);
|
||||||
|
padding: 10px 14px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
color: white;
|
||||||
|
font-family: inherit;
|
||||||
|
font-size: inherit;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
88
frontend/src/components/question.svelte
Normal file
88
frontend/src/components/question.svelte
Normal file
|
|
@ -0,0 +1,88 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { t } from "svelte-i18n";
|
||||||
|
import type { Readable } from "svelte/store";
|
||||||
|
import { api } from "../main";
|
||||||
|
import { createEventDispatcher } from "svelte";
|
||||||
|
|
||||||
|
export let q: Question;
|
||||||
|
export let streamer: Readable<boolean>;
|
||||||
|
|
||||||
|
const dispatch = createEventDispatcher();
|
||||||
|
|
||||||
|
async function markAsAnswered() {
|
||||||
|
await api.patch(
|
||||||
|
`./questions/${q.id}`,
|
||||||
|
{ answered: true }
|
||||||
|
);
|
||||||
|
dispatch("loadQuestions");
|
||||||
|
alert("Marked question as answered");
|
||||||
|
};
|
||||||
|
async function toggleVisibility() {
|
||||||
|
await api.patch(
|
||||||
|
`./questions/${q.id}`,
|
||||||
|
{ hidden: !q.hidden }
|
||||||
|
);
|
||||||
|
dispatch("loadQuestions");
|
||||||
|
alert("Toggled question visibility for streamer");
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="question"
|
||||||
|
class:answered={q.answered}
|
||||||
|
class:hidden={q.hidden}
|
||||||
|
>
|
||||||
|
<q>{q.question}</q>
|
||||||
|
<br>
|
||||||
|
<span>{$t("display.question.asked-by")} <b>{q.asker}</b></span>
|
||||||
|
<hr>
|
||||||
|
<div class="options">
|
||||||
|
{#if !q.answered}
|
||||||
|
<button
|
||||||
|
on:click={markAsAnswered}
|
||||||
|
>
|
||||||
|
{$t("display.question.button.answered")}
|
||||||
|
</button>
|
||||||
|
{/if}
|
||||||
|
{#if !$streamer}
|
||||||
|
<button
|
||||||
|
on:click={toggleVisibility}
|
||||||
|
>
|
||||||
|
{#if q.hidden}
|
||||||
|
{$t("display.question.button.hide-from-streamer")}
|
||||||
|
{:else}
|
||||||
|
{$t("display.question.button.show-for-streamer")}
|
||||||
|
{/if}
|
||||||
|
</button>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.question {
|
||||||
|
width: 25%;
|
||||||
|
margin: 4px;
|
||||||
|
padding: 15px;
|
||||||
|
background: rgba(0,0,0, 0.25);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
q {
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
hr {
|
||||||
|
width: 100%
|
||||||
|
}
|
||||||
|
|
||||||
|
.options {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
35
frontend/src/components/toggle.svelte
Normal file
35
frontend/src/components/toggle.svelte
Normal file
|
|
@ -0,0 +1,35 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import type { Writable } from "svelte/store";
|
||||||
|
|
||||||
|
export let value: Writable<boolean>;
|
||||||
|
export let label: string;
|
||||||
|
|
||||||
|
let id = `checkbox-${label.replace(/ /g, `-`)}`;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="toggle">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
{id}
|
||||||
|
bind:checked={$value}
|
||||||
|
>
|
||||||
|
<label for={id}>{label}</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.toggle {
|
||||||
|
margin: 5px 10px;
|
||||||
|
border-radius: 7px;
|
||||||
|
border-style: none;
|
||||||
|
border-width: 0px;
|
||||||
|
background: rgba(0,0,0, 0.25);
|
||||||
|
padding: 10px 14px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
input {
|
||||||
|
margin-right: 7px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -1,6 +1,11 @@
|
||||||
import { getLocaleFromNavigator, init, register } from "svelte-i18n";
|
import { getLocaleFromNavigator, init, register } from "svelte-i18n";
|
||||||
import "./app.css";
|
|
||||||
import App from "./App.svelte";
|
import App from "./App.svelte";
|
||||||
|
import axios from "axios";
|
||||||
|
import "./app.css";
|
||||||
|
|
||||||
|
export const api = axios.create({
|
||||||
|
baseURL: import.meta.env.DEV ? `http://localhost:6969${window.location.pathname}` : import.meta.env.BASE_URL,
|
||||||
|
});
|
||||||
|
|
||||||
// Get all of the internationalization stuff registered and operational
|
// Get all of the internationalization stuff registered and operational
|
||||||
import locales from "./locales";
|
import locales from "./locales";
|
||||||
|
|
|
||||||
23
frontend/src/stores.ts
Normal file
23
frontend/src/stores.ts
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
import { writable } from "svelte/store";
|
||||||
|
|
||||||
|
export const questions = writable<Question[]>([]);
|
||||||
|
export const visibleModal = writable(null);
|
||||||
|
|
||||||
|
/*
|
||||||
|
These stores are persisted between page reloads by updating the values in local
|
||||||
|
storage as soon as the value gets updated
|
||||||
|
*/
|
||||||
|
|
||||||
|
let sm = localStorage.getItem(`tqna-streamerMode`) ?? `false`;
|
||||||
|
export const isStreamer = writable<boolean>(sm == `true`);
|
||||||
|
isStreamer.subscribe(v => localStorage.setItem(`tqna-streamerMode`, String(v)));
|
||||||
|
|
||||||
|
|
||||||
|
let df = localStorage.getItem(`tqna-dyslexic`) ?? `false`;
|
||||||
|
export const dyslexiaFont = writable<boolean>(df == `true`);
|
||||||
|
dyslexiaFont.subscribe(v => localStorage.setItem(`tqna-dyslexic`, String(v)))
|
||||||
|
|
||||||
|
|
||||||
|
let ri = JSON.parse(localStorage.getItem(`tqna-refreshRate`) ?? `5`);
|
||||||
|
export const refreshInterval = writable<number>(ri ?? 5);
|
||||||
|
refreshInterval.subscribe(v => localStorage.setItem(`tqna-refreshRate`, JSON.stringify(v)));
|
||||||
7
frontend/src/types/Question.ts
Normal file
7
frontend/src/types/Question.ts
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
interface Question {
|
||||||
|
id: number;
|
||||||
|
question: string;
|
||||||
|
asker: string;
|
||||||
|
answered: boolean;
|
||||||
|
hidden: boolean;
|
||||||
|
};
|
||||||
Loading…
Add table
Add a link
Reference in a new issue