diff --git a/api/src/endpoints/questions/update.ts b/api/src/endpoints/questions/update.ts index e1cc557..a986f76 100644 --- a/api/src/endpoints/questions/update.ts +++ b/api/src/endpoints/questions/update.ts @@ -3,7 +3,7 @@ import { ServerRoute } from "@hapi/hapi"; import Joi from "joi"; const route: ServerRoute = { - method: `POST`, path: `/{channel}/questions/{question_id}`, + method: `PATCH`, path: `/{channel}/questions/{question_id}`, options: { validate: { params: Joi.object({ @@ -21,6 +21,7 @@ const route: ServerRoute = { question: Joi.string().optional(), asker: Joi.string().optional(), answered: Joi.boolean().optional(), + hidden: Joi.boolean().optional(), id: Joi.forbidden(), }) .min(1), @@ -41,17 +42,24 @@ const route: ServerRoute = { let values = []; for (const key in payload) { let v = payload[key]; - if (v.startsWith(`__`)) { + + let type = typeof v; + if (type == "boolean") { setters.push(`${key} = ${v}`); - } else { - setters.push(`${key} = ?`); - values.push(v); + } + else if (type == "string") { + if (v.startsWith(`__`)) { + setters.push(`${key} = ${v.slice(2)}`); + } else { + setters.push(`${key} = ?`); + values.push(v); + }; }; }; await db.query( `update tqna.questions - ( ${setters.join(`, `)} ) + set ${setters.join(`, `)} where channel = ? and id = ? limit 1`, [...values, channel, question_id ] @@ -64,8 +72,9 @@ const route: ServerRoute = { question = questions[0]; await conn.commit(); - } catch { - log.error(`Failed to add the question`); + } catch (e) { + log.error(e) + log.error(`Failed to save the question`); await conn.rollback(); } finally { conn.release(); diff --git a/api/src/hapi.ts b/api/src/hapi.ts index aaba203..47e6ddc 100644 --- a/api/src/hapi.ts +++ b/api/src/hapi.ts @@ -1,4 +1,5 @@ import { Server } from "@hapi/hapi"; +import { isBoom } from "@hapi/boom"; import { globSync } from "glob"; import path from "path"; import { log } from "./main"; @@ -10,6 +11,42 @@ const server = new Server({ debug: { 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; }); diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 15821b0..c941bef 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -8,6 +8,7 @@ "name": "frontend", "version": "0.0.0", "dependencies": { + "axios": "^1.5.0", "svelte-i18n": "^3.7.0" }, "devDependencies": { @@ -599,6 +600,21 @@ "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": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-3.2.1.tgz", @@ -716,6 +732,17 @@ "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": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -768,6 +795,14 @@ "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": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", @@ -939,6 +974,38 @@ "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": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", @@ -1180,6 +1247,25 @@ "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": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", @@ -1354,6 +1440,11 @@ "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": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", diff --git a/frontend/package.json b/frontend/package.json index 8e5082b..1fc0422 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -19,6 +19,7 @@ "vite": "^4.4.5" }, "dependencies": { + "axios": "^1.5.0", "svelte-i18n": "^3.7.0" } } diff --git a/frontend/public/OpenDyslexic3-Bold.ttf b/frontend/public/OpenDyslexic3-Bold.ttf new file mode 100644 index 0000000..395dffc Binary files /dev/null and b/frontend/public/OpenDyslexic3-Bold.ttf differ diff --git a/frontend/public/OpenDyslexic3-Regular.ttf b/frontend/public/OpenDyslexic3-Regular.ttf new file mode 100644 index 0000000..0ff4c0b Binary files /dev/null and b/frontend/public/OpenDyslexic3-Regular.ttf differ diff --git a/frontend/public/locales/en-CA.json b/frontend/public/locales/en-CA.json index c3c0339..66deb08 100644 --- a/frontend/public/locales/en-CA.json +++ b/frontend/public/locales/en-CA.json @@ -1,3 +1,17 @@ { - "docs.svelte": "Svelte Docs" -} \ No newline at end of file + "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" +} diff --git a/frontend/src/App.svelte b/frontend/src/App.svelte index 3324498..841c7cb 100644 --- a/frontend/src/App.svelte +++ b/frontend/src/App.svelte @@ -1,19 +1,82 @@
- - {$_("docs.svelte")} - -

- +
diff --git a/frontend/src/app.css b/frontend/src/app.css index ba542c8..707ad88 100644 --- a/frontend/src/app.css +++ b/frontend/src/app.css @@ -1,17 +1,56 @@ -html, body { - padding: 0; - margin: 0; - min-height: 100vh; - min-width: 100vw; +@font-face { + font-family: openDyslexic; + src: url(/OpenDyslexic3-Regular.ttf) format("truetype"); + font-style: normal; +} +@font-face { + font-family: openDyslexic; + src: url(/OpenDyslexic3-Bold.ttf) format("truetype"); + font-style: bold; } -body { - background-color: #2c2c32; +body, html { + 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; justify-content: center; align-items: center; } -a { - color: white; +.modal-content { + background: #2a2d2f; + min-width: 30%; + min-height: 25%; + padding: 25px; +} + +h1, h2, h3 { + margin: 0; } \ No newline at end of file diff --git a/frontend/src/components/integer.svelte b/frontend/src/components/integer.svelte new file mode 100644 index 0000000..eff635a --- /dev/null +++ b/frontend/src/components/integer.svelte @@ -0,0 +1,39 @@ + + +
+ + + ( {$value}{unitName} ) +
+ + \ No newline at end of file diff --git a/frontend/src/components/modals/settings.svelte b/frontend/src/components/modals/settings.svelte new file mode 100644 index 0000000..1822f73 --- /dev/null +++ b/frontend/src/components/modals/settings.svelte @@ -0,0 +1,94 @@ + + + + + \ No newline at end of file diff --git a/frontend/src/components/question.svelte b/frontend/src/components/question.svelte new file mode 100644 index 0000000..dd54f6d --- /dev/null +++ b/frontend/src/components/question.svelte @@ -0,0 +1,88 @@ + + +
+ {q.question} +
+ {$t("display.question.asked-by")} {q.asker} +
+
+ {#if !q.answered} + + {/if} + {#if !$streamer} + + {/if} +
+
+ + \ No newline at end of file diff --git a/frontend/src/components/toggle.svelte b/frontend/src/components/toggle.svelte new file mode 100644 index 0000000..49eae0d --- /dev/null +++ b/frontend/src/components/toggle.svelte @@ -0,0 +1,35 @@ + + +
+ + +
+ + diff --git a/frontend/src/main.ts b/frontend/src/main.ts index efae892..95bfca6 100644 --- a/frontend/src/main.ts +++ b/frontend/src/main.ts @@ -1,6 +1,11 @@ import { getLocaleFromNavigator, init, register } from "svelte-i18n"; -import "./app.css"; 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 import locales from "./locales"; diff --git a/frontend/src/stores.ts b/frontend/src/stores.ts new file mode 100644 index 0000000..b347746 --- /dev/null +++ b/frontend/src/stores.ts @@ -0,0 +1,23 @@ +import { writable } from "svelte/store"; + +export const questions = writable([]); +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(sm == `true`); +isStreamer.subscribe(v => localStorage.setItem(`tqna-streamerMode`, String(v))); + + +let df = localStorage.getItem(`tqna-dyslexic`) ?? `false`; +export const dyslexiaFont = writable(df == `true`); +dyslexiaFont.subscribe(v => localStorage.setItem(`tqna-dyslexic`, String(v))) + + +let ri = JSON.parse(localStorage.getItem(`tqna-refreshRate`) ?? `5`); +export const refreshInterval = writable(ri ?? 5); +refreshInterval.subscribe(v => localStorage.setItem(`tqna-refreshRate`, JSON.stringify(v))); \ No newline at end of file diff --git a/frontend/src/types/Question.ts b/frontend/src/types/Question.ts new file mode 100644 index 0000000..d3cfaf9 --- /dev/null +++ b/frontend/src/types/Question.ts @@ -0,0 +1,7 @@ +interface Question { + id: number; + question: string; + asker: string; + answered: boolean; + hidden: boolean; +}; \ No newline at end of file