Compare commits
26 Commits
db872df672
...
v0.0.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
44a80255bb | ||
|
|
d4da09dc5a | ||
|
|
307e11d4c7 | ||
|
|
2dda72cfa3 | ||
|
|
b3c18b56ed | ||
|
|
c16b6b04ff | ||
|
|
44c670b7db | ||
|
|
f750f055b7 | ||
|
|
f5bd9f27ce | ||
|
|
2520e8156b | ||
|
|
a405193f79 | ||
|
|
fa22d5cb0e | ||
|
|
74830411d0 | ||
|
|
ce1c1e0205 | ||
|
|
706522eeef | ||
|
|
b737b440b1 | ||
|
|
7005dc8292 | ||
|
|
18d64d328c | ||
|
|
a1d9e30030 | ||
|
|
4241b83721 | ||
|
|
9686d1c7dd | ||
|
|
324d4f430e | ||
|
|
ec8d451df8 | ||
|
|
9be5d8235e | ||
|
|
f958dcacc4 | ||
|
|
c8bd8a591d |
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
/cmd/server/server
|
||||
124
cmd/server/internal/public/index.html
Normal file
124
cmd/server/internal/public/index.html
Normal file
@@ -0,0 +1,124 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<header>
|
||||
<!--<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/water.css@2/out/light.css">-->
|
||||
<link rel="stylesheet" href="/light.css">
|
||||
<style>
|
||||
body {
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
:not(#answers-list[hidden]) + #question-text,
|
||||
:not(#answers-list[hidden]) + #question-text + #answer-form,
|
||||
:not(#question-options:empty) + #answers-freeform,
|
||||
#question-text:empty + #answer-form
|
||||
{
|
||||
display: none;
|
||||
}
|
||||
</style>
|
||||
</header>
|
||||
<body>
|
||||
<div id="answers-list"></div>
|
||||
<div id="question-text"></div>
|
||||
<form id="answer-form" action="#" onsubmit="submitAnswersForm(this); return false;">
|
||||
<fieldset id="question-options" name="question-options"></fieldset>
|
||||
<input id="answers-freeform" name="answers-freeform" type="text" hidden/>
|
||||
<button type="submit">Submit</button>
|
||||
</form>
|
||||
</body>
|
||||
<footer>
|
||||
<script>
|
||||
let g_questions = [];
|
||||
let g_live_question = {};
|
||||
|
||||
function pollState() {
|
||||
http("GET", "/api/v1/questions", (body) => {
|
||||
if (!body)
|
||||
return;
|
||||
g_questions = JSON.parse(body);
|
||||
pollLiveAnswer();
|
||||
pollLiveQuestion();
|
||||
});
|
||||
}
|
||||
|
||||
function pollLiveQuestion() {
|
||||
let live_questions = g_questions.filter((q) => q.Live);
|
||||
if (live_questions) {
|
||||
g_live_question = live_questions[0];
|
||||
}
|
||||
if (g_live_question) {
|
||||
let options = "";
|
||||
for (let i of g_live_question.Options)
|
||||
options += `<div>
|
||||
<input type="radio" value="${i}" name="g_live_question.Options" />
|
||||
<label for="${i}">${i}</label>
|
||||
</div>`
|
||||
if (document.getElementById("question-text").value != g_live_question.Text) {
|
||||
document.getElementById("question-text").innerHTML = `<h1>${g_live_question.Text}</h1>`;
|
||||
document.getElementById("question-text").value = g_live_question.Text;
|
||||
document.getElementById("question-options").innerHTML = options;
|
||||
}
|
||||
} else {
|
||||
document.getElementById("question-text").innerHTML = "";
|
||||
document.getElementById("question-options").innerHTML = "";
|
||||
}
|
||||
}
|
||||
|
||||
function pollLiveAnswer() {
|
||||
if (!g_live_question || !g_live_question.Closed) {
|
||||
document.getElementById("answers-list").hidden = true;
|
||||
return;
|
||||
}
|
||||
document.getElementById("answers-list").hidden = false;
|
||||
http("GET", `/api/v1/questions/${g_live_question.ID}/answers`, (body) => {
|
||||
let answers = JSON.parse(body).map((a) => a.Text);
|
||||
answers.sort((a, b) => {
|
||||
a = a.toLowerCase();
|
||||
b = b.toLowerCase();
|
||||
return (a > b) - (a < b)
|
||||
});
|
||||
let result = "";
|
||||
for (let i of answers)
|
||||
result += `<li>${i}</li>`;
|
||||
document.getElementById("answers-list").innerHTML = `<ul>${result}</ul>`;
|
||||
});
|
||||
}
|
||||
|
||||
function submitAnswersForm(form) {
|
||||
let children = [];
|
||||
for (let e of form["question-options"].elements) {
|
||||
children.push(e);
|
||||
}
|
||||
let checked_children = children.filter((c) => c.checked);
|
||||
let text = form["answers-freeform"].value;
|
||||
if (checked_children)
|
||||
text = checked_children[0].value;
|
||||
http("POST", `/api/v1/questions/${g_live_question.ID}/answers`, (body, status) => {
|
||||
console.log(status, body);
|
||||
}, JSON.stringify({
|
||||
Text: text,
|
||||
}));
|
||||
}
|
||||
|
||||
function http(method, remote, callback, body) {
|
||||
var xmlhttp = new XMLHttpRequest();
|
||||
xmlhttp.onreadystatechange = function() {
|
||||
if (xmlhttp.readyState == XMLHttpRequest.DONE) {
|
||||
callback(xmlhttp.responseText, xmlhttp.status)
|
||||
}
|
||||
};
|
||||
xmlhttp.open(method, remote, true);
|
||||
if(typeof body == "undefined") {
|
||||
body = null
|
||||
}
|
||||
xmlhttp.send(body);
|
||||
}
|
||||
|
||||
pollState();
|
||||
setInterval(() => {
|
||||
try {
|
||||
pollState();
|
||||
} catch {}
|
||||
}, 1000);
|
||||
</script>
|
||||
</footer>
|
||||
</html>
|
||||
886
cmd/server/internal/public/light.css
Normal file
886
cmd/server/internal/public/light.css
Normal file
@@ -0,0 +1,886 @@
|
||||
/**
|
||||
* Forced light theme version
|
||||
*/
|
||||
|
||||
:root {
|
||||
--background-body: #fff;
|
||||
--background: #efefef;
|
||||
--background-alt: #f7f7f7;
|
||||
--selection: #9e9e9e;
|
||||
--text-main: #363636;
|
||||
--text-bright: #000;
|
||||
--text-muted: #70777f;
|
||||
--links: #0076d1;
|
||||
--focus: #0096bfab;
|
||||
--border: #dbdbdb;
|
||||
--code: #000;
|
||||
--animation-duration: 0.1s;
|
||||
--button-base: #d0cfcf;
|
||||
--button-hover: #9b9b9b;
|
||||
--scrollbar-thumb: rgb(170, 170, 170);
|
||||
--scrollbar-thumb-hover: var(--button-hover);
|
||||
--form-placeholder: #949494;
|
||||
--form-text: #1d1d1d;
|
||||
--variable: #39a33c;
|
||||
--highlight: #ff0;
|
||||
--select-arrow: url("data:image/svg+xml;charset=utf-8,%3C?xml version='1.0' encoding='utf-8'?%3E %3Csvg version='1.1' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' height='62.5' width='116.9' fill='%23161f27'%3E %3Cpath d='M115.3,1.6 C113.7,0 111.1,0 109.5,1.6 L58.5,52.7 L7.4,1.6 C5.8,0 3.2,0 1.6,1.6 C0,3.2 0,5.8 1.6,7.4 L55.5,61.3 C56.3,62.1 57.3,62.5 58.4,62.5 C59.4,62.5 60.5,62.1 61.3,61.3 L115.2,7.4 C116.9,5.8 116.9,3.2 115.3,1.6Z'/%3E %3C/svg%3E");
|
||||
}
|
||||
|
||||
html {
|
||||
scrollbar-color: rgb(170, 170, 170) #fff;
|
||||
scrollbar-color: var(--scrollbar-thumb) var(--background-body);
|
||||
scrollbar-width: thin;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 'Segoe UI Emoji', 'Apple Color Emoji', 'Noto Color Emoji', sans-serif;
|
||||
line-height: 1.4;
|
||||
max-width: 800px;
|
||||
margin: 20px auto;
|
||||
padding: 0 10px;
|
||||
word-wrap: break-word;
|
||||
color: #363636;
|
||||
color: var(--text-main);
|
||||
background: #fff;
|
||||
background: var(--background-body);
|
||||
text-rendering: optimizeLegibility;
|
||||
}
|
||||
|
||||
button {
|
||||
transition:
|
||||
background-color 0.1s linear,
|
||||
border-color 0.1s linear,
|
||||
color 0.1s linear,
|
||||
box-shadow 0.1s linear,
|
||||
transform 0.1s ease;
|
||||
transition:
|
||||
background-color var(--animation-duration) linear,
|
||||
border-color var(--animation-duration) linear,
|
||||
color var(--animation-duration) linear,
|
||||
box-shadow var(--animation-duration) linear,
|
||||
transform var(--animation-duration) ease;
|
||||
}
|
||||
|
||||
input {
|
||||
transition:
|
||||
background-color 0.1s linear,
|
||||
border-color 0.1s linear,
|
||||
color 0.1s linear,
|
||||
box-shadow 0.1s linear,
|
||||
transform 0.1s ease;
|
||||
transition:
|
||||
background-color var(--animation-duration) linear,
|
||||
border-color var(--animation-duration) linear,
|
||||
color var(--animation-duration) linear,
|
||||
box-shadow var(--animation-duration) linear,
|
||||
transform var(--animation-duration) ease;
|
||||
}
|
||||
|
||||
textarea {
|
||||
transition:
|
||||
background-color 0.1s linear,
|
||||
border-color 0.1s linear,
|
||||
color 0.1s linear,
|
||||
box-shadow 0.1s linear,
|
||||
transform 0.1s ease;
|
||||
transition:
|
||||
background-color var(--animation-duration) linear,
|
||||
border-color var(--animation-duration) linear,
|
||||
color var(--animation-duration) linear,
|
||||
box-shadow var(--animation-duration) linear,
|
||||
transform var(--animation-duration) ease;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 2.2em;
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6 {
|
||||
margin-bottom: 12px;
|
||||
margin-top: 24px;
|
||||
}
|
||||
|
||||
h1 {
|
||||
color: #000;
|
||||
color: var(--text-bright);
|
||||
}
|
||||
|
||||
h2 {
|
||||
color: #000;
|
||||
color: var(--text-bright);
|
||||
}
|
||||
|
||||
h3 {
|
||||
color: #000;
|
||||
color: var(--text-bright);
|
||||
}
|
||||
|
||||
h4 {
|
||||
color: #000;
|
||||
color: var(--text-bright);
|
||||
}
|
||||
|
||||
h5 {
|
||||
color: #000;
|
||||
color: var(--text-bright);
|
||||
}
|
||||
|
||||
h6 {
|
||||
color: #000;
|
||||
color: var(--text-bright);
|
||||
}
|
||||
|
||||
strong {
|
||||
color: #000;
|
||||
color: var(--text-bright);
|
||||
}
|
||||
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6,
|
||||
b,
|
||||
strong,
|
||||
th {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
q::before {
|
||||
content: none;
|
||||
}
|
||||
|
||||
q::after {
|
||||
content: none;
|
||||
}
|
||||
|
||||
blockquote {
|
||||
border-left: 4px solid #0096bfab;
|
||||
border-left: 4px solid var(--focus);
|
||||
margin: 1.5em 0;
|
||||
padding: 0.5em 1em;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
q {
|
||||
border-left: 4px solid #0096bfab;
|
||||
border-left: 4px solid var(--focus);
|
||||
margin: 1.5em 0;
|
||||
padding: 0.5em 1em;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
blockquote > footer {
|
||||
font-style: normal;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
blockquote cite {
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
address {
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
a[href^='mailto\:']::before {
|
||||
content: '📧 ';
|
||||
}
|
||||
|
||||
a[href^='tel\:']::before {
|
||||
content: '📞 ';
|
||||
}
|
||||
|
||||
a[href^='sms\:']::before {
|
||||
content: '💬 ';
|
||||
}
|
||||
|
||||
mark {
|
||||
background-color: #ff0;
|
||||
background-color: var(--highlight);
|
||||
border-radius: 2px;
|
||||
padding: 0 2px 0 2px;
|
||||
color: #000;
|
||||
}
|
||||
|
||||
a > code,
|
||||
a > strong {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
button,
|
||||
select,
|
||||
input[type='submit'],
|
||||
input[type='reset'],
|
||||
input[type='button'],
|
||||
input[type='checkbox'],
|
||||
input[type='range'],
|
||||
input[type='radio'] {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
input,
|
||||
select {
|
||||
display: block;
|
||||
}
|
||||
|
||||
[type='checkbox'],
|
||||
[type='radio'] {
|
||||
display: initial;
|
||||
}
|
||||
|
||||
input {
|
||||
color: #1d1d1d;
|
||||
color: var(--form-text);
|
||||
background-color: #efefef;
|
||||
background-color: var(--background);
|
||||
font-family: inherit;
|
||||
font-size: inherit;
|
||||
margin-right: 6px;
|
||||
margin-bottom: 6px;
|
||||
padding: 10px;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
button {
|
||||
color: #1d1d1d;
|
||||
color: var(--form-text);
|
||||
background-color: #efefef;
|
||||
background-color: var(--background);
|
||||
font-family: inherit;
|
||||
font-size: inherit;
|
||||
margin-right: 6px;
|
||||
margin-bottom: 6px;
|
||||
padding: 10px;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
textarea {
|
||||
color: #1d1d1d;
|
||||
color: var(--form-text);
|
||||
background-color: #efefef;
|
||||
background-color: var(--background);
|
||||
font-family: inherit;
|
||||
font-size: inherit;
|
||||
margin-right: 6px;
|
||||
margin-bottom: 6px;
|
||||
padding: 10px;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
select {
|
||||
color: #1d1d1d;
|
||||
color: var(--form-text);
|
||||
background-color: #efefef;
|
||||
background-color: var(--background);
|
||||
font-family: inherit;
|
||||
font-size: inherit;
|
||||
margin-right: 6px;
|
||||
margin-bottom: 6px;
|
||||
padding: 10px;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
button {
|
||||
background-color: #d0cfcf;
|
||||
background-color: var(--button-base);
|
||||
padding-right: 30px;
|
||||
padding-left: 30px;
|
||||
}
|
||||
|
||||
input[type='submit'] {
|
||||
background-color: #d0cfcf;
|
||||
background-color: var(--button-base);
|
||||
padding-right: 30px;
|
||||
padding-left: 30px;
|
||||
}
|
||||
|
||||
input[type='reset'] {
|
||||
background-color: #d0cfcf;
|
||||
background-color: var(--button-base);
|
||||
padding-right: 30px;
|
||||
padding-left: 30px;
|
||||
}
|
||||
|
||||
input[type='button'] {
|
||||
background-color: #d0cfcf;
|
||||
background-color: var(--button-base);
|
||||
padding-right: 30px;
|
||||
padding-left: 30px;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
background: #9b9b9b;
|
||||
background: var(--button-hover);
|
||||
}
|
||||
|
||||
input[type='submit']:hover {
|
||||
background: #9b9b9b;
|
||||
background: var(--button-hover);
|
||||
}
|
||||
|
||||
input[type='reset']:hover {
|
||||
background: #9b9b9b;
|
||||
background: var(--button-hover);
|
||||
}
|
||||
|
||||
input[type='button']:hover {
|
||||
background: #9b9b9b;
|
||||
background: var(--button-hover);
|
||||
}
|
||||
|
||||
input[type='color'] {
|
||||
min-height: 2rem;
|
||||
padding: 8px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
input[type='checkbox'],
|
||||
input[type='radio'] {
|
||||
height: 1em;
|
||||
width: 1em;
|
||||
}
|
||||
|
||||
input[type='radio'] {
|
||||
border-radius: 100%;
|
||||
}
|
||||
|
||||
input {
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
label {
|
||||
vertical-align: middle;
|
||||
margin-bottom: 4px;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
input:not([type='checkbox']):not([type='radio']),
|
||||
input[type='range'],
|
||||
select,
|
||||
button,
|
||||
textarea {
|
||||
-webkit-appearance: none;
|
||||
}
|
||||
|
||||
textarea {
|
||||
display: block;
|
||||
margin-right: 0;
|
||||
box-sizing: border-box;
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
textarea:not([cols]) {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
textarea:not([rows]) {
|
||||
min-height: 40px;
|
||||
height: 140px;
|
||||
}
|
||||
|
||||
select {
|
||||
background: #efefef url("data:image/svg+xml;charset=utf-8,%3C?xml version='1.0' encoding='utf-8'?%3E %3Csvg version='1.1' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' height='62.5' width='116.9' fill='%23161f27'%3E %3Cpath d='M115.3,1.6 C113.7,0 111.1,0 109.5,1.6 L58.5,52.7 L7.4,1.6 C5.8,0 3.2,0 1.6,1.6 C0,3.2 0,5.8 1.6,7.4 L55.5,61.3 C56.3,62.1 57.3,62.5 58.4,62.5 C59.4,62.5 60.5,62.1 61.3,61.3 L115.2,7.4 C116.9,5.8 116.9,3.2 115.3,1.6Z'/%3E %3C/svg%3E") calc(100% - 12px) 50% / 12px no-repeat;
|
||||
background: var(--background) var(--select-arrow) calc(100% - 12px) 50% / 12px no-repeat;
|
||||
padding-right: 35px;
|
||||
}
|
||||
|
||||
select::-ms-expand {
|
||||
display: none;
|
||||
}
|
||||
|
||||
select[multiple] {
|
||||
padding-right: 10px;
|
||||
background-image: none;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
input:focus {
|
||||
box-shadow: 0 0 0 2px #0096bfab;
|
||||
box-shadow: 0 0 0 2px var(--focus);
|
||||
}
|
||||
|
||||
select:focus {
|
||||
box-shadow: 0 0 0 2px #0096bfab;
|
||||
box-shadow: 0 0 0 2px var(--focus);
|
||||
}
|
||||
|
||||
button:focus {
|
||||
box-shadow: 0 0 0 2px #0096bfab;
|
||||
box-shadow: 0 0 0 2px var(--focus);
|
||||
}
|
||||
|
||||
textarea:focus {
|
||||
box-shadow: 0 0 0 2px #0096bfab;
|
||||
box-shadow: 0 0 0 2px var(--focus);
|
||||
}
|
||||
|
||||
input[type='checkbox']:active,
|
||||
input[type='radio']:active,
|
||||
input[type='submit']:active,
|
||||
input[type='reset']:active,
|
||||
input[type='button']:active,
|
||||
input[type='range']:active,
|
||||
button:active {
|
||||
transform: translateY(2px);
|
||||
}
|
||||
|
||||
input:disabled,
|
||||
select:disabled,
|
||||
button:disabled,
|
||||
textarea:disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
::-moz-placeholder {
|
||||
color: #949494;
|
||||
color: var(--form-placeholder);
|
||||
}
|
||||
|
||||
:-ms-input-placeholder {
|
||||
color: #949494;
|
||||
color: var(--form-placeholder);
|
||||
}
|
||||
|
||||
::-ms-input-placeholder {
|
||||
color: #949494;
|
||||
color: var(--form-placeholder);
|
||||
}
|
||||
|
||||
::placeholder {
|
||||
color: #949494;
|
||||
color: var(--form-placeholder);
|
||||
}
|
||||
|
||||
fieldset {
|
||||
border: 1px #0096bfab solid;
|
||||
border: 1px var(--focus) solid;
|
||||
border-radius: 6px;
|
||||
margin: 0;
|
||||
margin-bottom: 12px;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
legend {
|
||||
font-size: 0.9em;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
input[type='range'] {
|
||||
margin: 10px 0;
|
||||
padding: 10px 0;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
input[type='range']:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
input[type='range']::-webkit-slider-runnable-track {
|
||||
width: 100%;
|
||||
height: 9.5px;
|
||||
-webkit-transition: 0.2s;
|
||||
transition: 0.2s;
|
||||
background: #efefef;
|
||||
background: var(--background);
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
input[type='range']::-webkit-slider-thumb {
|
||||
box-shadow: 0 1px 1px #000, 0 0 1px #0d0d0d;
|
||||
height: 20px;
|
||||
width: 20px;
|
||||
border-radius: 50%;
|
||||
background: #dbdbdb;
|
||||
background: var(--border);
|
||||
-webkit-appearance: none;
|
||||
margin-top: -7px;
|
||||
}
|
||||
|
||||
input[type='range']:focus::-webkit-slider-runnable-track {
|
||||
background: #efefef;
|
||||
background: var(--background);
|
||||
}
|
||||
|
||||
input[type='range']::-moz-range-track {
|
||||
width: 100%;
|
||||
height: 9.5px;
|
||||
-moz-transition: 0.2s;
|
||||
transition: 0.2s;
|
||||
background: #efefef;
|
||||
background: var(--background);
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
input[type='range']::-moz-range-thumb {
|
||||
box-shadow: 1px 1px 1px #000, 0 0 1px #0d0d0d;
|
||||
height: 20px;
|
||||
width: 20px;
|
||||
border-radius: 50%;
|
||||
background: #dbdbdb;
|
||||
background: var(--border);
|
||||
}
|
||||
|
||||
input[type='range']::-ms-track {
|
||||
width: 100%;
|
||||
height: 9.5px;
|
||||
background: transparent;
|
||||
border-color: transparent;
|
||||
border-width: 16px 0;
|
||||
color: transparent;
|
||||
}
|
||||
|
||||
input[type='range']::-ms-fill-lower {
|
||||
background: #efefef;
|
||||
background: var(--background);
|
||||
border: 0.2px solid #010101;
|
||||
border-radius: 3px;
|
||||
box-shadow: 1px 1px 1px #000, 0 0 1px #0d0d0d;
|
||||
}
|
||||
|
||||
input[type='range']::-ms-fill-upper {
|
||||
background: #efefef;
|
||||
background: var(--background);
|
||||
border: 0.2px solid #010101;
|
||||
border-radius: 3px;
|
||||
box-shadow: 1px 1px 1px #000, 0 0 1px #0d0d0d;
|
||||
}
|
||||
|
||||
input[type='range']::-ms-thumb {
|
||||
box-shadow: 1px 1px 1px #000, 0 0 1px #0d0d0d;
|
||||
border: 1px solid #000;
|
||||
height: 20px;
|
||||
width: 20px;
|
||||
border-radius: 50%;
|
||||
background: #dbdbdb;
|
||||
background: var(--border);
|
||||
}
|
||||
|
||||
input[type='range']:focus::-ms-fill-lower {
|
||||
background: #efefef;
|
||||
background: var(--background);
|
||||
}
|
||||
|
||||
input[type='range']:focus::-ms-fill-upper {
|
||||
background: #efefef;
|
||||
background: var(--background);
|
||||
}
|
||||
|
||||
a {
|
||||
text-decoration: none;
|
||||
color: #0076d1;
|
||||
color: var(--links);
|
||||
}
|
||||
|
||||
a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
code {
|
||||
background: #efefef;
|
||||
background: var(--background);
|
||||
color: #000;
|
||||
color: var(--code);
|
||||
padding: 2.5px 5px;
|
||||
border-radius: 6px;
|
||||
font-size: 1em;
|
||||
}
|
||||
|
||||
samp {
|
||||
background: #efefef;
|
||||
background: var(--background);
|
||||
color: #000;
|
||||
color: var(--code);
|
||||
padding: 2.5px 5px;
|
||||
border-radius: 6px;
|
||||
font-size: 1em;
|
||||
}
|
||||
|
||||
time {
|
||||
background: #efefef;
|
||||
background: var(--background);
|
||||
color: #000;
|
||||
color: var(--code);
|
||||
padding: 2.5px 5px;
|
||||
border-radius: 6px;
|
||||
font-size: 1em;
|
||||
}
|
||||
|
||||
pre > code {
|
||||
padding: 10px;
|
||||
display: block;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
var {
|
||||
color: #39a33c;
|
||||
color: var(--variable);
|
||||
font-style: normal;
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
kbd {
|
||||
background: #efefef;
|
||||
background: var(--background);
|
||||
border: 1px solid #dbdbdb;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 2px;
|
||||
color: #363636;
|
||||
color: var(--text-main);
|
||||
padding: 2px 4px 2px 4px;
|
||||
}
|
||||
|
||||
img,
|
||||
video {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
hr {
|
||||
border: none;
|
||||
border-top: 1px solid #dbdbdb;
|
||||
border-top: 1px solid var(--border);
|
||||
}
|
||||
|
||||
table {
|
||||
border-collapse: collapse;
|
||||
margin-bottom: 10px;
|
||||
width: 100%;
|
||||
table-layout: fixed;
|
||||
}
|
||||
|
||||
table caption {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
td,
|
||||
th {
|
||||
padding: 6px;
|
||||
text-align: left;
|
||||
vertical-align: top;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
thead {
|
||||
border-bottom: 1px solid #dbdbdb;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
|
||||
tfoot {
|
||||
border-top: 1px solid #dbdbdb;
|
||||
border-top: 1px solid var(--border);
|
||||
}
|
||||
|
||||
tbody tr:nth-child(even) {
|
||||
background-color: #efefef;
|
||||
background-color: var(--background);
|
||||
}
|
||||
|
||||
tbody tr:nth-child(even) button {
|
||||
background-color: #f7f7f7;
|
||||
background-color: var(--background-alt);
|
||||
}
|
||||
|
||||
tbody tr:nth-child(even) button:hover {
|
||||
background-color: #fff;
|
||||
background-color: var(--background-body);
|
||||
}
|
||||
|
||||
::-webkit-scrollbar {
|
||||
height: 10px;
|
||||
width: 10px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: #efefef;
|
||||
background: var(--background);
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: rgb(170, 170, 170);
|
||||
background: var(--scrollbar-thumb);
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: #9b9b9b;
|
||||
background: var(--scrollbar-thumb-hover);
|
||||
}
|
||||
|
||||
::-moz-selection {
|
||||
background-color: #9e9e9e;
|
||||
background-color: var(--selection);
|
||||
color: #000;
|
||||
color: var(--text-bright);
|
||||
}
|
||||
|
||||
::selection {
|
||||
background-color: #9e9e9e;
|
||||
background-color: var(--selection);
|
||||
color: #000;
|
||||
color: var(--text-bright);
|
||||
}
|
||||
|
||||
details {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
background-color: #f7f7f7;
|
||||
background-color: var(--background-alt);
|
||||
padding: 10px 10px 0;
|
||||
margin: 1em 0;
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
details[open] {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
details > :last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
details[open] summary {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
summary {
|
||||
display: list-item;
|
||||
background-color: #efefef;
|
||||
background-color: var(--background);
|
||||
padding: 10px;
|
||||
margin: -10px -10px 0;
|
||||
cursor: pointer;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
summary:hover,
|
||||
summary:focus {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
details > :not(summary) {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
summary::-webkit-details-marker {
|
||||
color: #363636;
|
||||
color: var(--text-main);
|
||||
}
|
||||
|
||||
dialog {
|
||||
background-color: #f7f7f7;
|
||||
background-color: var(--background-alt);
|
||||
color: #363636;
|
||||
color: var(--text-main);
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
border-color: #dbdbdb;
|
||||
border-color: var(--border);
|
||||
padding: 10px 30px;
|
||||
}
|
||||
|
||||
dialog > header:first-child {
|
||||
background-color: #efefef;
|
||||
background-color: var(--background);
|
||||
border-radius: 6px 6px 0 0;
|
||||
margin: -10px -30px 10px;
|
||||
padding: 10px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
dialog::-webkit-backdrop {
|
||||
background: #0000009c;
|
||||
-webkit-backdrop-filter: blur(4px);
|
||||
backdrop-filter: blur(4px);
|
||||
}
|
||||
|
||||
dialog::backdrop {
|
||||
background: #0000009c;
|
||||
-webkit-backdrop-filter: blur(4px);
|
||||
backdrop-filter: blur(4px);
|
||||
}
|
||||
|
||||
footer {
|
||||
border-top: 1px solid #dbdbdb;
|
||||
border-top: 1px solid var(--border);
|
||||
padding-top: 10px;
|
||||
color: #70777f;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
body > footer {
|
||||
margin-top: 40px;
|
||||
}
|
||||
|
||||
@media print {
|
||||
body,
|
||||
pre,
|
||||
code,
|
||||
summary,
|
||||
details,
|
||||
button,
|
||||
input,
|
||||
textarea {
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
button,
|
||||
input,
|
||||
textarea {
|
||||
border: 1px solid #000;
|
||||
}
|
||||
|
||||
body,
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6,
|
||||
pre,
|
||||
code,
|
||||
button,
|
||||
input,
|
||||
textarea,
|
||||
footer,
|
||||
summary,
|
||||
strong {
|
||||
color: #000;
|
||||
}
|
||||
|
||||
summary::marker {
|
||||
color: #000;
|
||||
}
|
||||
|
||||
summary::-webkit-details-marker {
|
||||
color: #000;
|
||||
}
|
||||
|
||||
tbody tr:nth-child(even) {
|
||||
background-color: #f2f2f2;
|
||||
}
|
||||
|
||||
a {
|
||||
color: #00f;
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
309
cmd/server/main.go
Normal file
309
cmd/server/main.go
Normal file
@@ -0,0 +1,309 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"embed"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
"path"
|
||||
"regexp"
|
||||
"strings"
|
||||
"syscall"
|
||||
|
||||
"golang.org/x/time/rate"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
Addr string
|
||||
RPS int
|
||||
fsDB string
|
||||
}
|
||||
|
||||
type Handler struct {
|
||||
cfg Config
|
||||
limiter *rate.Limiter
|
||||
db DB
|
||||
}
|
||||
|
||||
type DB interface {
|
||||
GetQuestions() ([]Question, error)
|
||||
GetQuestion(string) (Question, error)
|
||||
InsertAnswer(string, string, Answer) error
|
||||
GetAnswers(string) ([]Answer, error)
|
||||
}
|
||||
|
||||
type fsDB string
|
||||
|
||||
type Question struct {
|
||||
ID string
|
||||
Live bool
|
||||
Closed bool
|
||||
Text string
|
||||
Options []string
|
||||
}
|
||||
|
||||
type Answer struct {
|
||||
Text string
|
||||
}
|
||||
|
||||
type Session struct {
|
||||
User struct {
|
||||
ID string
|
||||
Name string
|
||||
}
|
||||
}
|
||||
|
||||
func main() {
|
||||
ctx, can := signal.NotifyContext(context.Background(), syscall.SIGINT)
|
||||
defer can()
|
||||
if err := run(ctx); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
func run(ctx context.Context) error {
|
||||
cfg, err := newConfig()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return runHTTP(ctx, cfg)
|
||||
}
|
||||
|
||||
func newConfig() (Config, error) {
|
||||
cfg := Config{}
|
||||
|
||||
fs := flag.NewFlagSet(os.Args[0], flag.ContinueOnError)
|
||||
fs.StringVar(&cfg.Addr, "addr", ":8080", "address to listen on")
|
||||
fs.IntVar(&cfg.RPS, "rps", 100, "requests per second to serve")
|
||||
fs.StringVar(&cfg.fsDB, "fs-db", "/tmp/live-audience.d", "api dir to serve")
|
||||
|
||||
err := fs.Parse(os.Args[1:])
|
||||
return cfg, err
|
||||
}
|
||||
|
||||
func (cfg Config) NewHandler() Handler {
|
||||
return Handler{
|
||||
cfg: cfg,
|
||||
limiter: rate.NewLimiter(rate.Limit(cfg.RPS), 10),
|
||||
db: fsDB(cfg.fsDB),
|
||||
}
|
||||
}
|
||||
|
||||
func (s Session) Empty() bool {
|
||||
return s == (Session{})
|
||||
}
|
||||
|
||||
func runHTTP(ctx context.Context, cfg Config) error {
|
||||
server := &http.Server{
|
||||
Addr: cfg.Addr,
|
||||
Handler: cfg.NewHandler(),
|
||||
}
|
||||
go func() {
|
||||
<-ctx.Done()
|
||||
server.Close()
|
||||
}()
|
||||
log.Println("listening on", cfg.Addr)
|
||||
if err := server.ListenAndServe(); err != nil && ctx.Err() == nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
if err := h.serveHTTP(w, r); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
}
|
||||
|
||||
func (h Handler) serveHTTP(w http.ResponseWriter, r *http.Request) error {
|
||||
h.limiter.Wait(r.Context())
|
||||
|
||||
session, err := h.auth(r)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if session.Empty() {
|
||||
w.Header().Set("WWW-Authenticate", "Basic realm=xyz")
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
w.Write([]byte(`IDENTIFY YOURSELF!`))
|
||||
return nil
|
||||
}
|
||||
|
||||
return h.handle(session, w, r)
|
||||
}
|
||||
|
||||
func (h Handler) auth(r *http.Request) (Session, error) {
|
||||
user, pass, ok := r.BasicAuth()
|
||||
if !ok {
|
||||
return Session{}, nil
|
||||
}
|
||||
session := Session{}
|
||||
session.User.ID = base64.StdEncoding.EncodeToString([]byte(fmt.Sprintf("%s:%s", user, pass)))
|
||||
session.User.Name = user
|
||||
return session, nil
|
||||
}
|
||||
|
||||
//go:embed internal/public
|
||||
var _public embed.FS
|
||||
var public = func() http.FileSystem {
|
||||
d, err := fs.Sub(_public, "internal/public")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return http.FS(d)
|
||||
}()
|
||||
|
||||
func (h Handler) handle(session Session, w http.ResponseWriter, r *http.Request) error {
|
||||
if !strings.HasPrefix(r.URL.Path, "/api/") {
|
||||
w.Header().Set("Cache-Control", "private, max-age=60")
|
||||
http.FileServer(public).ServeHTTP(w, r)
|
||||
return nil
|
||||
}
|
||||
|
||||
handlers := map[string]func(Session, http.ResponseWriter, *http.Request) error{
|
||||
`^/api/v1/questions$`: h.handleAPIV1Questions,
|
||||
`^/api/v1/questions/[^/]*$`: h.handleAPIV1Question,
|
||||
`^/api/v1/questions/[^/]*/answers$`: h.handleAPIV1QuestionsAnswers,
|
||||
}
|
||||
for k, v := range handlers {
|
||||
if regexp.MustCompile(k).MatchString(r.URL.Path) {
|
||||
return v(session, w, r)
|
||||
}
|
||||
}
|
||||
http.NotFound(w, r)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h Handler) handleAPIV1Question(session Session, w http.ResponseWriter, r *http.Request) error {
|
||||
qid := path.Base(r.URL.Path)
|
||||
q, err := h.db.GetQuestion(qid)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return json.NewEncoder(w).Encode(q)
|
||||
}
|
||||
|
||||
func (h Handler) handleAPIV1Questions(session Session, w http.ResponseWriter, r *http.Request) error {
|
||||
qs, err := h.db.GetQuestions()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return json.NewEncoder(w).Encode(qs)
|
||||
}
|
||||
|
||||
func (h Handler) handleAPIV1QuestionsAnswers(session Session, w http.ResponseWriter, r *http.Request) error {
|
||||
switch r.Method {
|
||||
case http.MethodGet:
|
||||
return h.handleAPIV1QuestionsAnswersGet(session, w, r)
|
||||
case http.MethodPost:
|
||||
return h.handleAPIV1QuestionsAnswersPost(session, w, r)
|
||||
}
|
||||
http.NotFound(w, r)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h Handler) handleAPIV1QuestionsAnswersGet(session Session, w http.ResponseWriter, r *http.Request) error {
|
||||
qid := path.Base(path.Dir(r.URL.Path))
|
||||
as, err := h.db.GetAnswers(qid)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return json.NewEncoder(w).Encode(as)
|
||||
}
|
||||
|
||||
func (h Handler) handleAPIV1QuestionsAnswersPost(session Session, w http.ResponseWriter, r *http.Request) error {
|
||||
qid := path.Base(path.Dir(r.URL.Path))
|
||||
uid := session.User.ID
|
||||
var a Answer
|
||||
if err := json.NewDecoder(r.Body).Decode(&a); err != nil {
|
||||
return fmt.Errorf("failed to read answer: %w", err)
|
||||
}
|
||||
return h.db.InsertAnswer(qid, uid, a)
|
||||
}
|
||||
|
||||
func (db fsDB) GetQuestion(qid string) (Question, error) {
|
||||
p := db.path(qid)
|
||||
b, err := os.ReadFile(p)
|
||||
if err != nil {
|
||||
return Question{}, err
|
||||
}
|
||||
|
||||
var q Question
|
||||
if err := json.Unmarshal(b, &q); err != nil {
|
||||
return Question{}, fmt.Errorf("failed to parse %s as question: %w", b, err)
|
||||
}
|
||||
q.ID = qid
|
||||
return q, nil
|
||||
}
|
||||
|
||||
func (db fsDB) InsertAnswer(qid, uid string, a Answer) error {
|
||||
p := path.Join(db.path(qid)+".d", uid)
|
||||
os.MkdirAll(path.Dir(p), os.ModePerm)
|
||||
b, err := json.Marshal(a)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := os.Stat(p); !os.IsNotExist(err) {
|
||||
return nil
|
||||
}
|
||||
return os.WriteFile(p, b, os.ModePerm)
|
||||
}
|
||||
|
||||
func (db fsDB) GetQuestions() ([]Question, error) {
|
||||
p := db.path("")
|
||||
entries, err := os.ReadDir(p)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
results := []Question{}
|
||||
for _, entry := range entries {
|
||||
if strings.HasPrefix(path.Base(entry.Name()), ".") || entry.IsDir() {
|
||||
continue
|
||||
}
|
||||
qid := path.Base(entry.Name())
|
||||
q, err := db.GetQuestion(qid)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
results = append(results, q)
|
||||
}
|
||||
return results, nil
|
||||
}
|
||||
|
||||
func (db fsDB) GetAnswers(qid string) ([]Answer, error) {
|
||||
p := db.path(qid) + ".d"
|
||||
entries, err := os.ReadDir(p)
|
||||
if os.IsNotExist(err) {
|
||||
return nil, nil
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
results := []Answer{}
|
||||
for _, entry := range entries {
|
||||
if strings.HasPrefix(path.Base(entry.Name()), ".") {
|
||||
continue
|
||||
}
|
||||
b, err := os.ReadFile(path.Join(p, entry.Name()))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var a Answer
|
||||
if err := json.Unmarshal(b, &a); err != nil {
|
||||
return nil, fmt.Errorf("failed to parse %s as answer: %w", path.Join(p, entry.Name()), err)
|
||||
}
|
||||
results = append(results, a)
|
||||
}
|
||||
return results, nil
|
||||
}
|
||||
|
||||
func (db fsDB) path(q string) string {
|
||||
return path.Join(string(db), q)
|
||||
}
|
||||
146
cmd/server/main_test.go
Normal file
146
cmd/server/main_test.go
Normal file
@@ -0,0 +1,146 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"path"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestRunHTTP(t *testing.T) {
|
||||
cfg := Config{
|
||||
fsDB: t.TempDir(),
|
||||
}
|
||||
if err := func() error {
|
||||
b, _ := json.Marshal(Question{
|
||||
Text: "QUESTION TEXT",
|
||||
Options: []string{"X", "Y"},
|
||||
})
|
||||
return os.WriteFile(path.Join(string(cfg.fsDB), "0"), b, os.ModePerm)
|
||||
}(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
h := cfg.NewHandler()
|
||||
|
||||
t.Run("requires auth", func(t *testing.T) {
|
||||
r := httptest.NewRequest(http.MethodGet, "/", nil)
|
||||
w := httptest.NewRecorder()
|
||||
t.Logf("%s %s", r.Method, r.URL)
|
||||
h.ServeHTTP(w, r)
|
||||
t.Logf("(%d) %s", w.Code, w.Body.Bytes())
|
||||
if w.Code != 401 {
|
||||
t.Error(w.Code)
|
||||
}
|
||||
if w.Header().Get("WWW-Authenticate") == "" {
|
||||
t.Errorf("expected WWW-Authenticate header but got %+v", w.Header())
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("/", func(t *testing.T) {
|
||||
r := httptest.NewRequest(http.MethodGet, "/", nil)
|
||||
r.SetBasicAuth("b", "b")
|
||||
w := httptest.NewRecorder()
|
||||
t.Logf("%s %s", r.Method, r.URL)
|
||||
h.ServeHTTP(w, r)
|
||||
t.Logf("(%d) %s", w.Code, w.Body.Bytes())
|
||||
if w.Code != http.StatusOK {
|
||||
t.Error(w.Code)
|
||||
}
|
||||
if !bytes.Contains(w.Body.Bytes(), []byte("<html>")) {
|
||||
t.Errorf("%s", w.Body.Bytes())
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("/api/notfound", func(t *testing.T) {
|
||||
r := httptest.NewRequest(http.MethodGet, "/api/notfound", nil)
|
||||
r.SetBasicAuth("b", "b")
|
||||
w := httptest.NewRecorder()
|
||||
t.Logf("%s %s", r.Method, r.URL)
|
||||
h.ServeHTTP(w, r)
|
||||
t.Logf("(%d) %s", w.Code, w.Body.Bytes())
|
||||
if w.Code != http.StatusNotFound {
|
||||
t.Error(w.Code)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("/api/v1/questions", func(t *testing.T) {
|
||||
r := httptest.NewRequest(http.MethodGet, "/api/v1/questions", nil)
|
||||
r.SetBasicAuth("b", "b")
|
||||
w := httptest.NewRecorder()
|
||||
t.Logf("%s %s", r.Method, r.URL)
|
||||
h.ServeHTTP(w, r)
|
||||
t.Logf("(%d) %s", w.Code, w.Body.Bytes())
|
||||
if w.Code != http.StatusOK {
|
||||
t.Error(w.Code)
|
||||
}
|
||||
var result []Question
|
||||
if err := json.Unmarshal(w.Body.Bytes(), &result); err != nil {
|
||||
t.Error(err)
|
||||
} else if fmt.Sprint(result) != fmt.Sprint([]Question{{ID: "0", Live: false, Closed: false, Text: "QUESTION TEXT", Options: []string{"X", "Y"}}}) {
|
||||
t.Error(result)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("/api/v1/questions/0", func(t *testing.T) {
|
||||
r := httptest.NewRequest(http.MethodGet, "/api/v1/questions/0", nil)
|
||||
r.SetBasicAuth("b", "b")
|
||||
w := httptest.NewRecorder()
|
||||
t.Logf("%s %s", r.Method, r.URL)
|
||||
h.ServeHTTP(w, r)
|
||||
t.Logf("(%d) %s", w.Code, w.Body.Bytes())
|
||||
if w.Code != http.StatusOK {
|
||||
t.Error(w.Code)
|
||||
}
|
||||
var result Question
|
||||
if err := json.Unmarshal(w.Body.Bytes(), &result); err != nil {
|
||||
t.Error(err)
|
||||
} else if fmt.Sprint(result) != fmt.Sprint(Question{ID: "0", Text: "QUESTION TEXT", Options: []string{"X", "Y"}}) {
|
||||
t.Error(result)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("POST /api/v1/questions/0/answers", func(t *testing.T) {
|
||||
r := httptest.NewRequest(http.MethodPost, "/api/v1/questions/0/answers", strings.NewReader(`{"Text": "teehee"}`))
|
||||
r.SetBasicAuth("b", "b")
|
||||
w := httptest.NewRecorder()
|
||||
t.Logf("%s %s", r.Method, r.URL)
|
||||
h.ServeHTTP(w, r)
|
||||
t.Logf("(%d) %s", w.Code, w.Body.Bytes())
|
||||
if w.Code != http.StatusOK {
|
||||
t.Error(w.Code)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("GET /api/v1/questions/0/answers", func(t *testing.T) {
|
||||
r := httptest.NewRequest(http.MethodGet, "/api/v1/questions/0/answers", nil)
|
||||
r.SetBasicAuth("b", "b")
|
||||
w := httptest.NewRecorder()
|
||||
t.Logf("%s %s", r.Method, r.URL)
|
||||
h.ServeHTTP(w, r)
|
||||
t.Logf("(%d) %s", w.Code, w.Body.Bytes())
|
||||
if w.Code != http.StatusOK {
|
||||
t.Error(w.Code)
|
||||
}
|
||||
var result []Answer
|
||||
if err := json.Unmarshal(w.Body.Bytes(), &result); err != nil {
|
||||
t.Error(err)
|
||||
} else if fmt.Sprint(result) != fmt.Sprint([]Answer{{Text: "teehee"}}) {
|
||||
t.Error(result)
|
||||
}
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
func TestPublic(t *testing.T) {
|
||||
f, err := public.Open("index.html")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer f.Close()
|
||||
}
|
||||
6
go.mod
6
go.mod
@@ -1,3 +1,9 @@
|
||||
module live-studio-audience
|
||||
|
||||
go 1.21.4
|
||||
|
||||
require (
|
||||
gitea.inhome.blapointe.com/local/gziphttp v0.0.0-20240109214739-0d639b0a9eb9 // indirect
|
||||
github.com/julienschmidt/httprouter v1.3.0 // indirect
|
||||
golang.org/x/time v0.5.0 // indirect
|
||||
)
|
||||
|
||||
6
go.sum
Normal file
6
go.sum
Normal file
@@ -0,0 +1,6 @@
|
||||
gitea.inhome.blapointe.com/local/gziphttp v0.0.0-20240109214739-0d639b0a9eb9 h1:Vq4jPYz6pCDMx7rxySw7SbKH3FHBywt1oS1TpXx4noM=
|
||||
gitea.inhome.blapointe.com/local/gziphttp v0.0.0-20240109214739-0d639b0a9eb9/go.mod h1:YSOO/quInfKxfgqHH8exd71292hkLgqu2BO5oz6EATE=
|
||||
github.com/julienschmidt/httprouter v1.3.0 h1:U0609e9tgbseu3rBINet9P48AI/D3oJs4dN7jwJOQ1U=
|
||||
github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=
|
||||
golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
|
||||
golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
||||
Reference in New Issue
Block a user