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

200 lines
5.4 KiB
Gleam
Raw Normal View History

2026-04-11 14:11:11 +02:00
import components.{click_cell}
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,
2026-04-11 14:11:11 +02:00
players: List(#(String, String)),
player_id: Option(String),
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, ""))
})
2026-04-11 14:11:11 +02:00
#(
Model(
Initial,
actor.call(handler.data, 1000, message.FetchPlayers),
None,
initial_array,
handler,
),
effect.none(),
)
}
pub opaque type Msg {
Initial
2026-04-11 14:11:11 +02:00
PickedName(name: String)
SharedMessage(message: NotifyClient)
ReceiveName(message: String)
AcceptName(accept: Option(String))
2026-04-11 14:11:11 +02:00
PickQuestion
PickedQuestion(question: String)
GiveAnswer(question: String, answer: String)
}
fn update(model: Model, msg: Msg) -> #(Model, Effect(Msg)) {
case msg {
2026-04-11 14:11:11 +02:00
Initial -> #(Model(..model, state: msg), effect.none())
PickedName(player_id) -> #(
Model(..model, player_id: Some(player_id), state: PickQuestion),
effect.none(),
)
SharedMessage(_) -> #(model, effect.none())
AcceptName(None) -> #(
Model(Initial, model.players, None, [], model.handler),
effect.none(),
)
AcceptName(Some(player_id)) -> {
#(
Model(..model, player_id: Some(player_id), state: PickQuestion),
effect.none(),
)
}
2026-04-11 14:11:11 +02:00
PickQuestion -> #(model, effect.none())
PickedQuestion(question) -> {
#(Model(..model, state: GiveAnswer(question, "")), effect.none())
}
GiveAnswer(question, answer) -> {
let assert Some(player_id) = model.player_id
case int.parse(question) {
2026-04-11 14:11:11 +02:00
Ok(question) -> {
actor.send(
model.handler.data,
message.GiveSingleAnswer(id: player_id, question:, answer:),
)
let new_value = case list.key_find(model.answers, question) {
Ok(pair) -> {
let #(a, _) = pair
#(a, answer)
}
Error(_) -> #("", answer)
}
#(
Model(
..model,
state: PickQuestion,
answers: list.key_set(model.answers, question, new_value),
),
effect.none(),
)
}
_ -> {
echo "bad index"
#(model, effect.none())
}
}
}
ReceiveName(_) -> #(Model(..model, state: msg), effect.none())
}
}
fn view(model: Model) -> Element(Msg) {
element.fragment([
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 {
2026-04-11 14:11:11 +02:00
Initial -> html.text("STATUS: Please select player")
ReceiveName(_) -> html.text("STATUS: Please validate your name")
2026-04-11 14:11:11 +02:00
PickQuestion -> html.text("STATUS: Pick question to answer")
GiveAnswer(_, _) -> html.text("STATUS: Give your answer")
_ -> html.text("STATUS: Waiting for next question")
},
]),
]),
]),
2026-04-11 14:11:11 +02:00
html.div([attribute.class("terminal-section")], [
html.div([attribute.class("terminal-label mb-4")], [
html.text("[ACTIVE TRANSMISSIONS]"),
]),
]),
case model.state {
Initial -> view_players(model.players)
PickQuestion -> view_questions(model.answers)
_ -> content_cell(#(10, #("Answer", "Answer question")))
},
])
}
2026-04-11 14:11:11 +02:00
fn view_players(players: List(#(String, String))) {
html.div([], [
html.div(
[],
list.append(
list.index_map(players, fn(item, index) {
click_cell(Some(int.to_string(index)), item, PickedName)
}),
[click_cell(Some("NEW"), #("new", "New Player!"), PickedName)],
),
),
])
}
2026-04-11 14:11:11 +02:00
fn view_questions(answers: List(#(Int, #(String, String)))) {
html.div(
[attribute.class("singles-grid")],
list.map(answers, fn(content) { content_cell(content) }),
)
}
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),
]),
],
)
}