diff --git a/api/data.go b/api/data.go index a5895e3..2488246 100644 --- a/api/data.go +++ b/api/data.go @@ -2,7 +2,12 @@ package api type QuestionReq struct { 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 { @@ -15,8 +20,8 @@ type TextQuestion struct { Question string `json:"question"` CorrectAnswer string `json:"correct_answer"` Type string `json:"type"` + Category string `json:"category"` IncorrectAnswers []string `json:"incorrect_answers"` - Category uint `json:"category"` } type SessionResp struct { diff --git a/api/handlers.go b/api/handlers.go index 6f80db7..b564bd0 100644 --- a/api/handlers.go +++ b/api/handlers.go @@ -4,7 +4,10 @@ import ( "encoding/json" "fmt" "io" + "log" "net/http" + "strconv" + "strings" ) 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.Write([]byte(session.Token)) - w.WriteHeader(200) } func GetQuestion(w http.ResponseWriter, r *http.Request) { defer r.Body.Close() - req, err := io.ReadAll(r.Body) + params, err := io.ReadAll(r.Body) if err != nil { http.Error(w, fmt.Sprintf("Error parsing request body: %s", err), 421) return } 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 { http.Error(w, fmt.Sprintf("Failed to request questions: %s", err), 500) return @@ -61,9 +71,43 @@ func GetQuestion(w http.ResponseWriter, r *http.Request) { result, err := json.Marshal(questionResp.Results[len(questionResp.Results)-1]) if err != nil { http.Error(w, fmt.Sprintf("Error encoding question: %s", err), 500) + return } w.Header().Add("content-type", "application/json") 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!")) } diff --git a/api/routes.go b/api/routes.go index 9ab503f..221ebf9 100644 --- a/api/routes.go +++ b/api/routes.go @@ -18,6 +18,7 @@ func NewRoutes() *http.ServeMux { // api routes mux.HandleFunc("POST /api/question", GetQuestion) mux.HandleFunc("GET /api/session", OpenSession) + mux.HandleFunc("POST /api/answer", AnswerQuestion) return mux } diff --git a/assets/scripts.js b/assets/scripts.js index 3ccdfc4..f81c292 100644 --- a/assets/scripts.js +++ b/assets/scripts.js @@ -1,7 +1,8 @@ -import 'htmx.org' +import htmx from 'htmx.org' import Alpine from 'alpinejs' -// Add Alpine instance to window object. +// Add Alpine and htmx instance to window object. +window.htmx = htmx window.Alpine = Alpine // Start Alpine. diff --git a/static/dice-six-faces-five.svg b/static/dice-six-faces-five.svg new file mode 100644 index 0000000..0ea3171 --- /dev/null +++ b/static/dice-six-faces-five.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/static/dice-six-faces-four.svg b/static/dice-six-faces-four.svg new file mode 100644 index 0000000..21fbfe1 --- /dev/null +++ b/static/dice-six-faces-four.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/static/dice-six-faces-one.svg b/static/dice-six-faces-one.svg new file mode 100644 index 0000000..a775e80 --- /dev/null +++ b/static/dice-six-faces-one.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/static/dice-six-faces-six.svg b/static/dice-six-faces-six.svg new file mode 100644 index 0000000..2903dd2 --- /dev/null +++ b/static/dice-six-faces-six.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/static/dice-six-faces-three.svg b/static/dice-six-faces-three.svg new file mode 100644 index 0000000..47031dc --- /dev/null +++ b/static/dice-six-faces-three.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/static/dice-six-faces-two.svg b/static/dice-six-faces-two.svg new file mode 100644 index 0000000..a0beb1b --- /dev/null +++ b/static/dice-six-faces-two.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/static/scripts.js b/static/scripts.js index 48fd8ca..fd1fa05 100644 --- a/static/scripts.js +++ b/static/scripts.js @@ -6784,6 +6784,7 @@ ${expression ? 'Expression: "' + expression + '"\n\n' : ""}`, el); var module_default = src_default; // assets/scripts.js + window.htmx = htmx_esm_default; window.Alpine = module_default; module_default.start(); })(); diff --git a/static/styles.css b/static/styles.css index fa2cd21..8474e8c 100644 --- a/static/styles.css +++ b/static/styles.css @@ -554,11 +554,34 @@ video { --tw-contain-style: ; } -.bg-green-200 { - --tw-bg-opacity: 1; - background-color: rgb(187 247 208 / var(--tw-bg-opacity)); +.flex { + display: flex; } -.p-5 { - padding: 1.25rem; +.h-20 { + 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; } diff --git a/views/layout/base.templ b/views/layout/base.templ index 7ab68e2..a4aad76 100644 --- a/views/layout/base.templ +++ b/views/layout/base.templ @@ -10,7 +10,7 @@ type PageInfo struct { templ BaseLayout(pageInfo PageInfo) { - + @@ -20,9 +20,10 @@ templ BaseLayout(pageInfo PageInfo) { + - + { children... } diff --git a/views/layout/base_templ.go b/views/layout/base_templ.go index 5b5b693..691e21b 100644 --- a/views/layout/base_templ.go +++ b/views/layout/base_templ.go @@ -34,7 +34,7 @@ func BaseLayout(pageInfo PageInfo) templ.Component { templ_7745c5c3_Var1 = templ.NopComponent } ctx = templ.ClearChildren(ctx) - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("") + _, 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 { return templ_7745c5c3_Err } @@ -73,7 +73,7 @@ func BaseLayout(pageInfo PageInfo) templ.Component { if templ_7745c5c3_Err != nil { 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 { return templ_7745c5c3_Err } diff --git a/views/pages/index.templ b/views/pages/index.templ index 16be051..8a7dd34 100644 --- a/views/pages/index.templ +++ b/views/pages/index.templ @@ -6,9 +6,87 @@ import ( templ Home() { @l.BaseLayout(l.PageInfo{Title: "Trivia Chase"}) { - <div @click.capture="console.log('I will log first')" x-data=""> - Hello, World! - <button @click="console.log('I will log second')" class="bg-green-200 p-5">Hello!</button> + <div + class="flex w-full h-full justify-center items-center flex-col" + 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> } } diff --git a/views/pages/index_templ.go b/views/pages/index_templ.go index ca4c2bd..b22ebd0 100644 --- a/views/pages/index_templ.go +++ b/views/pages/index_templ.go @@ -42,7 +42,7 @@ func Home() templ.Component { }() } ctx = templ.InitializeContext(ctx) - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<div @click.capture=\"console.log('I will log first')\" x-data=\"\">Hello, World! <button @click=\"console.log('I will log second')\" 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: '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 => {\n if (rolled) {\n window.htmx.process(document.querySelector('main#question'))\n window.htmx.process(document.querySelectorAll('#answer'))\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 && `/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>") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err }