quizterm/server/src/web/components/answerlist.gleam

183 lines
5.4 KiB
Gleam
Raw Normal View History

import gleam/erlang/process.{type Subject}
import gleam/int
import gleam/list
import gleam/option.{type Option, None, Some}
import gleam/otp/actor.{type Started}
import lustre
import lustre/attribute.{class}
import lustre/effect.{type Effect}
import lustre/element.{type Element}
import lustre/element/html
import shared/message.{type NotifyClient, type NotifyServer}
import web/components/shared.{
step_prompt, view_input, view_named_input, view_named_keyed_input, view_yes_no,
}
pub fn component() -> lustre.App(
#(List(#(Int, String)), message.ClientsServer),
Model,
Msg,
) {
lustre.application(init, update, view)
}
pub opaque type Model {
Model(
state: Msg,
answers: List(#(Int, #(String, String))),
handler: Started(Subject(NotifyServer)),
)
}
fn init(
start_args: #(List(#(Int, String)), message.ClientsServer),
) -> #(Model, Effect(Msg)) {
let #(answers, handlers) = start_args
let #(_registry, handler) = handlers
// Convert a "question number -> question text" array to
// "question number" -> #("question text", "users answer" array
// with blank user answers.
let initial_array =
list.filter(answers, fn(x) {
let #(i, _) = x
i <= 14 && i >= 0
})
|> list.map(fn(x) {
let #(a, b) = x
#(a, #(b, ""))
})
#(Model(Initial, initial_array, handler), effect.none())
}
pub opaque type Msg {
Initial
SharedMessage(message: NotifyClient)
ReceiveName(message: String)
AcceptName(accept: Option(String))
GiveQuestion(name: String, question: String)
GiveAnswer(name: String, question: Int, answer: String)
}
fn update(model: Model, msg: Msg) -> #(Model, Effect(Msg)) {
case msg {
Initial | SharedMessage(_) -> #(model, effect.none())
AcceptName(None) -> #(Model(Initial, [], model.handler), effect.none())
AcceptName(Some(name)) -> {
#(Model(..model, state: GiveQuestion(name, "")), effect.none())
}
GiveQuestion(name, question) ->
case int.parse(question) {
Ok(question) if question >= 1 && question <= 14 -> #(
Model(..model, state: GiveAnswer(name:, question:, answer: "")),
effect.none(),
)
_ -> #(
Model(..model, state: GiveQuestion(name:, question: "")),
effect.none(),
)
}
GiveAnswer(name, question, answer) -> {
actor.send(
model.handler.data,
message.GiveSingleAnswer(name:, question:, answer:),
)
let new_value = case list.key_find(model.answers, question) {
Ok(pair) -> {
let #(a, _) = pair
#(a, answer)
}
Error(_) -> #("", answer)
}
#(
Model(
..model,
state: GiveQuestion(name, ""),
answers: list.key_set(model.answers, question, new_value),
),
effect.none(),
)
}
ReceiveName(_) -> #(Model(..model, state: msg), effect.none())
}
}
fn view(model: Model) -> Element(Msg) {
element.fragment([
html.div([attribute.class("terminal-prompt")], [
case model.state {
Initial ->
step_prompt(
"Hello stranger. To join the quiz, I need to know your name",
fn() { view_input(ReceiveName) },
)
ReceiveName(name) ->
step_prompt(
"Your name is " <> name <> "? Are you absolutely sure???",
fn() { view_yes_no(name, AcceptName) },
)
GiveQuestion(name, _) ->
step_prompt(
"Enter the number of the question you want to answer",
fn() { view_named_input(name, GiveQuestion) },
)
GiveAnswer(name, question, _) ->
step_prompt(
"Enter the answer to question number " <> int.to_string(question),
fn() { view_named_keyed_input(question, name, GiveAnswer) },
)
_ -> html.h3([], [html.text("Waiting for next question")])
},
]),
html.div([class("terminal-header")], [
html.div([class("terminal-status")], [
html.span([class("status-blink")], [html.text("")]),
html.text(" SYSTEM READY"),
html.span([class("ml-8")], [
case model.state {
Initial -> html.text("STATUS: Please input your name")
ReceiveName(_) -> html.text("STATUS: Please validate your name")
GiveQuestion(_, _) -> html.text("STATUS: Pick question to answer")
GiveAnswer(_, _, _) -> html.text("STATUS: Give your answer")
_ -> html.text("STATUS: Waiting for next question")
},
]),
]),
]),
terminal_section(model.answers, "[ACTIVE TRANSMISSIONS]", fn(answer) {
content_cell(answer)
}),
])
}
fn terminal_section(
answers: List(#(Int, #(String, String))),
header: String,
extract: fn(#(Int, #(String, String))) -> Element(Msg),
) {
html.div([attribute.class("terminal-section")], [
html.div([attribute.class("terminal-label mb-4")], [
html.text(header),
]),
html.div([attribute.class("participants-grid")], list.map(answers, extract)),
])
}
fn content_cell(answer: #(Int, #(String, String))) -> Element(Msg) {
let #(question, #(question_text, answer)) = answer
html.div(
[
class("participant-box"),
],
[
html.div([class("participant-name")], [
html.text("" <> int.to_string(question) <> " " <> question_text),
]),
html.div([class("participant-answer")], [
html.text(answer),
]),
],
)
}