2026-04-11 14:11:11 +02:00
|
|
|
import components.{click_cell}
|
2026-03-29 13:53:16 +02:00
|
|
|
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),
|
2026-03-29 13:53:16 +02:00
|
|
|
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(),
|
|
|
|
|
)
|
2026-03-29 13:53:16 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub opaque type Msg {
|
|
|
|
|
Initial
|
2026-04-11 14:11:11 +02:00
|
|
|
PickedName(name: String)
|
2026-03-29 13:53:16 +02:00
|
|
|
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)
|
2026-03-29 13:53:16 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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-03-29 13:53:16 +02:00
|
|
|
}
|
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
|
2026-03-29 13:53:16 +02:00
|
|
|
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())
|
2026-03-29 13:53:16 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
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")
|
2026-03-29 13:53:16 +02:00
|
|
|
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")
|
2026-03-29 13:53:16 +02:00
|
|
|
_ -> 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-03-29 13:53:16 +02:00
|
|
|
])
|
|
|
|
|
}
|
|
|
|
|
|
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-03-29 13:53:16 +02:00
|
|
|
])
|
|
|
|
|
}
|
|
|
|
|
|
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) }),
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-29 13:53:16 +02:00
|
|
|
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),
|
|
|
|
|
]),
|
|
|
|
|
],
|
|
|
|
|
)
|
|
|
|
|
}
|