Simple UI and finished API

This commit is contained in:
Sammy Shear 2024-07-22 13:17:19 -04:00
parent 5b6e22075d
commit c1ab08bc36
Signed by: sammyshear
GPG Key ID: 69B1EDA35F4F4C09
16 changed files with 182 additions and 22 deletions

View File

@ -2,7 +2,12 @@ package api
type QuestionReq struct { type QuestionReq struct {
Token string `json:"token"` Token string `json:"token"`
Category uint `json:"category"` Category string `json:"category"`
}
type AnswerReq struct {
Answer string `json:"answer"`
Question string `json:"question"`
} }
type QuestionResp struct { type QuestionResp struct {
@ -15,8 +20,8 @@ type TextQuestion struct {
Question string `json:"question"` Question string `json:"question"`
CorrectAnswer string `json:"correct_answer"` CorrectAnswer string `json:"correct_answer"`
Type string `json:"type"` Type string `json:"type"`
Category string `json:"category"`
IncorrectAnswers []string `json:"incorrect_answers"` IncorrectAnswers []string `json:"incorrect_answers"`
Category uint `json:"category"`
} }
type SessionResp struct { type SessionResp struct {

View File

@ -4,7 +4,10 @@ import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"io" "io"
"log"
"net/http" "net/http"
"strconv"
"strings"
) )
func OpenSession(w http.ResponseWriter, r *http.Request) { func OpenSession(w http.ResponseWriter, r *http.Request) {
@ -26,21 +29,28 @@ func OpenSession(w http.ResponseWriter, r *http.Request) {
w.Header().Add("content-type", "text/plain") w.Header().Add("content-type", "text/plain")
w.Write([]byte(session.Token)) w.Write([]byte(session.Token))
w.WriteHeader(200)
} }
func GetQuestion(w http.ResponseWriter, r *http.Request) { func GetQuestion(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close() defer r.Body.Close()
req, err := io.ReadAll(r.Body) params, err := io.ReadAll(r.Body)
if err != nil { if err != nil {
http.Error(w, fmt.Sprintf("Error parsing request body: %s", err), 421) http.Error(w, fmt.Sprintf("Error parsing request body: %s", err), 421)
return return
} }
var body QuestionReq var body QuestionReq
json.Unmarshal(req, &body) err = json.Unmarshal(params, &body)
if err != nil {
http.Error(w, err.Error(), 500)
}
category, err := strconv.Atoi(body.Category)
if err != nil {
http.Error(w, "Error parsing category", 500)
return
}
res, err := http.Get(fmt.Sprintf("https://opentdb.com/api.php?amount=1&category=%d&type=multiple&token=%s", body.Category, body.Token)) res, err := http.Get(fmt.Sprintf("https://opentdb.com/api.php?amount=1&category=%d&type=multiple&token=%s", category, body.Token))
if err != nil { if err != nil {
http.Error(w, fmt.Sprintf("Failed to request questions: %s", err), 500) http.Error(w, fmt.Sprintf("Failed to request questions: %s", err), 500)
return return
@ -61,9 +71,43 @@ func GetQuestion(w http.ResponseWriter, r *http.Request) {
result, err := json.Marshal(questionResp.Results[len(questionResp.Results)-1]) result, err := json.Marshal(questionResp.Results[len(questionResp.Results)-1])
if err != nil { if err != nil {
http.Error(w, fmt.Sprintf("Error encoding question: %s", err), 500) http.Error(w, fmt.Sprintf("Error encoding question: %s", err), 500)
return
} }
w.Header().Add("content-type", "application/json") w.Header().Add("content-type", "application/json")
w.Write(result) w.Write(result)
w.WriteHeader(200) }
func AnswerQuestion(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
params, err := io.ReadAll(r.Body)
if err != nil {
http.Error(w, err.Error(), 500)
return
}
log.Print(string(params))
var body AnswerReq
err = json.Unmarshal(params, &body)
if err != nil {
http.Error(w, err.Error(), 500)
return
}
var question TextQuestion
err = json.Unmarshal([]byte(body.Question), &question)
if err != nil {
http.Error(w, err.Error(), 500)
return
}
w.Header().Add("content-type", "text/plain")
log.Printf("Body: %v", body)
log.Printf("Answer: %s, CorrectAnswer: %s", body.Answer, question.CorrectAnswer)
if strings.Compare(body.Answer, question.CorrectAnswer) != 0 {
w.Write([]byte("Incorrect!"))
return
}
w.Write([]byte("Correct!"))
} }

View File

@ -18,6 +18,7 @@ func NewRoutes() *http.ServeMux {
// api routes // api routes
mux.HandleFunc("POST /api/question", GetQuestion) mux.HandleFunc("POST /api/question", GetQuestion)
mux.HandleFunc("GET /api/session", OpenSession) mux.HandleFunc("GET /api/session", OpenSession)
mux.HandleFunc("POST /api/answer", AnswerQuestion)
return mux return mux
} }

View File

@ -1,7 +1,8 @@
import 'htmx.org' import htmx from 'htmx.org'
import Alpine from 'alpinejs' import Alpine from 'alpinejs'
// Add Alpine instance to window object. // Add Alpine and htmx instance to window object.
window.htmx = htmx
window.Alpine = Alpine window.Alpine = Alpine
// Start Alpine. // Start Alpine.

View File

@ -0,0 +1 @@
<svg style="height: 512px; width: 512px;" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path d="M0 0h512v512H0z" fill="#000" fill-opacity="1"></path><g class="" style="" transform="translate(0,0)"><path d="M74.5 36A38.5 38.5 0 0 0 36 74.5v363A38.5 38.5 0 0 0 74.5 476h363a38.5 38.5 0 0 0 38.5-38.5v-363A38.5 38.5 0 0 0 437.5 36h-363zm48.97 36.03A50 50 0 0 1 172 122a50 50 0 0 1-100 0 50 50 0 0 1 51.47-49.97zm268 0A50 50 0 0 1 440 122a50 50 0 0 1-100 0 50 50 0 0 1 51.47-49.97zM256 206a50 50 0 0 1 0 100 50 50 0 0 1 0-100zM123.47 340.03A50 50 0 0 1 172 390a50 50 0 0 1-100 0 50 50 0 0 1 51.47-49.97zm268 0A50 50 0 0 1 440 390a50 50 0 0 1-100 0 50 50 0 0 1 51.47-49.97z" fill="#fff" fill-opacity="1"></path></g></svg>

After

Width:  |  Height:  |  Size: 728 B

View File

@ -0,0 +1 @@
<svg style="height: 512px; width: 512px;" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path d="M0 0h512v512H0z" fill="#000" fill-opacity="1"></path><g class="" style="" transform="translate(0,0)"><path d="M74.5 36A38.5 38.5 0 0 0 36 74.5v363A38.5 38.5 0 0 0 74.5 476h363a38.5 38.5 0 0 0 38.5-38.5v-363A38.5 38.5 0 0 0 437.5 36h-363zm48.97 36.03A50 50 0 0 1 172 122a50 50 0 0 1-100 0 50 50 0 0 1 51.47-49.97zm268 0A50 50 0 0 1 440 122a50 50 0 0 1-100 0 50 50 0 0 1 51.47-49.97zm-268 268A50 50 0 0 1 172 390a50 50 0 0 1-100 0 50 50 0 0 1 51.47-49.97zm268 0A50 50 0 0 1 440 390a50 50 0 0 1-100 0 50 50 0 0 1 51.47-49.97z" fill="#fff" fill-opacity="1"></path></g></svg>

After

Width:  |  Height:  |  Size: 678 B

View File

@ -0,0 +1 @@
<svg style="height: 512px; width: 512px;" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path d="M0 0h512v512H0z" fill="#000" fill-opacity="1"></path><g class="" style="" transform="translate(0,0)"><path d="M74.5 36A38.5 38.5 0 0 0 36 74.5v363A38.5 38.5 0 0 0 74.5 476h363a38.5 38.5 0 0 0 38.5-38.5v-363A38.5 38.5 0 0 0 437.5 36h-363zM256 206a50 50 0 0 1 0 100 50 50 0 0 1 0-100z" fill="#fff" fill-opacity="1"></path></g></svg>

After

Width:  |  Height:  |  Size: 438 B

View File

@ -0,0 +1 @@
<svg style="height: 512px; width: 512px;" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path d="M0 0h512v512H0z" fill="#000" fill-opacity="1"></path><g class="" style="" transform="translate(0,0)"><path d="M74.5 36A38.5 38.5 0 0 0 36 74.5v363A38.5 38.5 0 0 0 74.5 476h363a38.5 38.5 0 0 0 38.5-38.5v-363A38.5 38.5 0 0 0 437.5 36h-363zm48.97 36.03A50 50 0 0 1 172 122a50 50 0 0 1-100 0 50 50 0 0 1 51.47-49.97zm268 0A50 50 0 0 1 440 122a50 50 0 0 1-100 0 50 50 0 0 1 51.47-49.97zM122 206a50 50 0 0 1 0 100 50 50 0 0 1 0-100zm268 0a50 50 0 0 1 0 100 50 50 0 0 1 0-100zM123.47 340.03A50 50 0 0 1 172 390a50 50 0 0 1-100 0 50 50 0 0 1 51.47-49.97zm268 0A50 50 0 0 1 440 390a50 50 0 0 1-100 0 50 50 0 0 1 51.47-49.97z" fill="#fff" fill-opacity="1"></path></g></svg>

After

Width:  |  Height:  |  Size: 771 B

View File

@ -0,0 +1 @@
<svg style="height: 512px; width: 512px;" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path d="M0 0h512v512H0z" fill="#000" fill-opacity="1"></path><g class="" style="" transform="translate(0,0)"><path d="M74.5 36A38.5 38.5 0 0 0 36 74.5v363A38.5 38.5 0 0 0 74.5 476h363a38.5 38.5 0 0 0 38.5-38.5v-363A38.5 38.5 0 0 0 437.5 36h-363zm316.97 36.03A50 50 0 0 1 440 122a50 50 0 0 1-100 0 50 50 0 0 1 51.47-49.97zM256 206a50 50 0 0 1 0 100 50 50 0 0 1 0-100zM123.47 340.03A50 50 0 0 1 172 390a50 50 0 0 1-100 0 50 50 0 0 1 51.47-49.97z" fill="#fff" fill-opacity="1"></path></g></svg>

After

Width:  |  Height:  |  Size: 591 B

View File

@ -0,0 +1 @@
<svg style="height: 512px; width: 512px;" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path d="M0 0h512v512H0z" fill="#000" fill-opacity="1"></path><g class="" style="" transform="translate(0,0)"><path d="M74.5 36A38.5 38.5 0 0 0 36 74.5v363A38.5 38.5 0 0 0 74.5 476h363a38.5 38.5 0 0 0 38.5-38.5v-363A38.5 38.5 0 0 0 437.5 36h-363zm316.97 36.03A50 50 0 0 1 440 122a50 50 0 0 1-100 0 50 50 0 0 1 51.47-49.97zm-268 268A50 50 0 0 1 172 390a50 50 0 0 1-100 0 50 50 0 0 1 51.47-49.97z" fill="#fff" fill-opacity="1"></path></g></svg>

After

Width:  |  Height:  |  Size: 541 B

View File

@ -6784,6 +6784,7 @@ ${expression ? 'Expression: "' + expression + '"\n\n' : ""}`, el);
var module_default = src_default; var module_default = src_default;
// assets/scripts.js // assets/scripts.js
window.htmx = htmx_esm_default;
window.Alpine = module_default; window.Alpine = module_default;
module_default.start(); module_default.start();
})(); })();

View File

@ -554,11 +554,34 @@ video {
--tw-contain-style: ; --tw-contain-style: ;
} }
.bg-green-200 { .flex {
--tw-bg-opacity: 1; display: flex;
background-color: rgb(187 247 208 / var(--tw-bg-opacity));
} }
.p-5 { .h-20 {
padding: 1.25rem; height: 5rem;
}
.h-full {
height: 100%;
}
.w-20 {
width: 5rem;
}
.w-full {
width: 100%;
}
.flex-col {
flex-direction: column;
}
.items-center {
align-items: center;
}
.justify-center {
justify-content: center;
} }

View File

@ -10,7 +10,7 @@ type PageInfo struct {
templ BaseLayout(pageInfo PageInfo) { templ BaseLayout(pageInfo PageInfo) {
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en" class="w-full h-full">
<head> <head>
<meta charset="utf-8"/> <meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/> <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
@ -20,9 +20,10 @@ templ BaseLayout(pageInfo PageInfo) {
<meta property="og:description" content={ pageInfo.Description }/> <meta property="og:description" content={ pageInfo.Description }/>
<link rel="stylesheet" href="static/styles.css"/> <link rel="stylesheet" href="static/styles.css"/>
<script defer src="/static/scripts.js"></script> <script defer src="/static/scripts.js"></script>
<script defer src="https://unpkg.com/htmx-ext-json-enc@2.0.0/json-enc.js"></script>
<!-- Add other head elements like favicons, canonical links, etc. --> <!-- Add other head elements like favicons, canonical links, etc. -->
</head> </head>
<body> <body class="w-full h-full">
{ children... } { children... }
</body> </body>
</html> </html>

View File

@ -34,7 +34,7 @@ func BaseLayout(pageInfo PageInfo) templ.Component {
templ_7745c5c3_Var1 = templ.NopComponent templ_7745c5c3_Var1 = templ.NopComponent
} }
ctx = templ.ClearChildren(ctx) ctx = templ.ClearChildren(ctx)
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<!doctype html><html lang=\"en\"><head><meta charset=\"utf-8\"><meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\"><meta name=\"google\" content=\"notranslate\"><title>") _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<!doctype html><html lang=\"en\" class=\"w-full h-full\"><head><meta charset=\"utf-8\"><meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\"><meta name=\"google\" content=\"notranslate\"><title>")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
@ -73,7 +73,7 @@ func BaseLayout(pageInfo PageInfo) templ.Component {
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("\"><link rel=\"stylesheet\" href=\"static/styles.css\"><script defer src=\"/static/scripts.js\"></script><!-- Add other head elements like favicons, canonical links, etc. --></head><body>") _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("\"><link rel=\"stylesheet\" href=\"static/styles.css\"><script defer src=\"/static/scripts.js\"></script><script defer src=\"https://unpkg.com/htmx-ext-json-enc@2.0.0/json-enc.js\"></script><!-- Add other head elements like favicons, canonical links, etc. --></head><body class=\"w-full h-full\">")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }

View File

@ -6,9 +6,87 @@ import (
templ Home() { templ Home() {
@l.BaseLayout(l.PageInfo{Title: "Trivia Chase"}) { @l.BaseLayout(l.PageInfo{Title: "Trivia Chase"}) {
<div @click.capture="console.log('I will log first')" x-data=""> <div
Hello, World! class="flex w-full h-full justify-center items-center flex-col"
<button @click="console.log('I will log second')" class="bg-green-200 p-5">Hello!</button> x-data="{ rolled: false, die: 'one', faces: ['one', 'two', 'three', 'four', 'five', 'six'], categories: { one: 17, two: 23, three: 25, four: 22, five: 24, six: 9 }, session: '', score: 0 }"
x-init="$watch('rolled', value => {
if (rolled) {
window.htmx.process(document.querySelector('main#question'))
window.htmx.process(document.querySelectorAll('#answer'))
}
})"
>
<span x-text="`Score: ${score}`"></span>
<label>Session ID:</label>
<span
hx-get="/api/session"
hx-trigger="load once"
hx-swap="innerHTML"
@htmx:before-swap.camel="session = $event.detail.xhr.response"
></span>
<button class=" flex" @click="die = faces[Math.floor((Math.random() * 5) + 1)]; rolled = true;">
<img :src="die && `/static/dice-six-faces-${die}.svg`" class="w-20 h-20"/>
</button>
<template x-if="rolled">
<main
class="flex flex-col"
id="question"
x-data="{ question: {}, answered: false }"
@htmx:before-swap.camel="question = JSON.parse($event.detail.xhr.response); $dispatch('updateAnswers', { answers: [...question.incorrect_answers, question.correct_answer ] } );"
>
<span
hx-post="/api/question"
hx-trigger="load once"
hx-ext="json-enc"
:hx-vals="JSON.stringify({'token': session, 'category': categories[die]})"
hx-swap="none"
></span>
<span id="#question" x-html="question.question"></span>
<form
x-data="{ answers: [] }"
@update-answers.camel.window="answers = $event.detail.answers;"
hx-post="/api/answer"
:hx-vals="JSON.stringify({question})"
hx-ext="json-enc"
@htmx:before-swap.camel.self="answered = true; if ($event.detail.xhr.response == 'Correct!') score++;"
>
<template x-if="answers[0]">
<input type="radio" name="answer" id="answer" :value="answers[0]"/>
</template>
<template x-if="answers">
<label for="answer" x-html="answers[0]"></label>
</template>
<br/>
<template x-if="answers[1]">
<input type="radio" name="answer" id="answer" :value="answers[1]"/>
</template>
<template x-if="answers[1]">
<label for="answers[1]" x-html="answers[1]"></label>
</template>
<br/>
<template x-if="answers[2]">
<input type="radio" name="answer" id="answer" :value="answers[2]"/>
</template>
<template x-if="answers[2]">
<label for="answer" x-html="answers[2]"></label>
</template>
<br/>
<template x-if="answers[3]">
<input type="radio" name="answer" id="answer" :value="answers[3]"/>
</template>
<template x-if="answers[3]">
<label for="answer" x-html="answers[3]"></label>
</template>
<br/>
<template x-if="answers.length > 0">
<button type="submit">Submit Answer</button>
</template>
</form>
<template x-if="answered">
<button @click="answered = false; rolled = false;">Next Question</button>
</template>
</main>
</template>
</div> </div>
} }
} }

View File

@ -42,7 +42,7 @@ func Home() templ.Component {
}() }()
} }
ctx = templ.InitializeContext(ctx) ctx = templ.InitializeContext(ctx)
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<div @click.capture=\"console.log(&#39;I will log first&#39;)\" x-data=\"\">Hello, World! <button @click=\"console.log(&#39;I will log second&#39;)\" class=\"bg-green-200 p-5\">Hello!</button></div>") _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<div class=\"flex w-full h-full justify-center items-center flex-col\" x-data=\"{ rolled: false, die: &#39;one&#39;, faces: [&#39;one&#39;, &#39;two&#39;, &#39;three&#39;, &#39;four&#39;, &#39;five&#39;, &#39;six&#39;], categories: { one: 17, two: 23, three: 25, four: 22, five: 24, six: 9 }, session: &#39;&#39;, score: 0 }\" x-init=\"$watch(&#39;rolled&#39;, value =&gt; {\n if (rolled) {\n window.htmx.process(document.querySelector(&#39;main#question&#39;))\n window.htmx.process(document.querySelectorAll(&#39;#answer&#39;))\n }\n })\"><span x-text=\"`Score: ${score}`\"></span> <label>Session ID:</label> <span hx-get=\"/api/session\" hx-trigger=\"load once\" hx-swap=\"innerHTML\" @htmx:before-swap.camel=\"session = $event.detail.xhr.response\"></span> <button class=\" flex\" @click=\"die = faces[Math.floor((Math.random() * 5) + 1)]; rolled = true;\"><img :src=\"die &amp;&amp; `/static/dice-six-faces-${die}.svg`\" class=\"w-20 h-20\"></button><template x-if=\"rolled\"><main class=\"flex flex-col\" id=\"question\" x-data=\"{ question: {}, answered: false }\" @htmx:before-swap.camel=\"question = JSON.parse($event.detail.xhr.response); $dispatch(&#39;updateAnswers&#39;, { answers: [...question.incorrect_answers, question.correct_answer ] } );\"><span hx-post=\"/api/question\" hx-trigger=\"load once\" hx-ext=\"json-enc\" :hx-vals=\"JSON.stringify({&#39;token&#39;: session, &#39;category&#39;: categories[die]})\" hx-swap=\"none\"></span> <span id=\"#question\" x-html=\"question.question\"></span><form x-data=\"{ answers: [] }\" @update-answers.camel.window=\"answers = $event.detail.answers;\" hx-post=\"/api/answer\" :hx-vals=\"JSON.stringify({question})\" hx-ext=\"json-enc\" @htmx:before-swap.camel.self=\"answered = true; if ($event.detail.xhr.response == &#39;Correct!&#39;) score++;\"><template x-if=\"answers[0]\"><input type=\"radio\" name=\"answer\" id=\"answer\" :value=\"answers[0]\"></template><template x-if=\"answers\"><label for=\"answer\" x-html=\"answers[0]\"></label></template><br><template x-if=\"answers[1]\"><input type=\"radio\" name=\"answer\" id=\"answer\" :value=\"answers[1]\"></template><template x-if=\"answers[1]\"><label for=\"answers[1]\" x-html=\"answers[1]\"></label></template><br><template x-if=\"answers[2]\"><input type=\"radio\" name=\"answer\" id=\"answer\" :value=\"answers[2]\"></template><template x-if=\"answers[2]\"><label for=\"answer\" x-html=\"answers[2]\"></label></template><br><template x-if=\"answers[3]\"><input type=\"radio\" name=\"answer\" id=\"answer\" :value=\"answers[3]\"></template><template x-if=\"answers[3]\"><label for=\"answer\" x-html=\"answers[3]\"></label></template><br><template x-if=\"answers.length &gt; 0\"><button type=\"submit\">Submit Answer</button></template></form><template x-if=\"answered\"><button @click=\"answered = false; rolled = false;\">Next Question</button></template></main></template></div>")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }