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, 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, "")) }) #( Model( Initial, actor.call(handler.data, 1000, message.FetchPlayers), None, initial_array, handler, ), effect.none(), ) } pub opaque type Msg { Initial PickedName(name: String) SharedMessage(message: NotifyClient) ReceiveName(message: String) AcceptName(accept: Option(String)) PickQuestion PickedQuestion(question: String) GiveAnswer(question: String, answer: String) } fn update(model: Model, msg: Msg) -> #(Model, Effect(Msg)) { case msg { 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(), ) } 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) { 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 { Initial -> html.text("STATUS: Please select player") ReceiveName(_) -> html.text("STATUS: Please validate your name") PickQuestion -> html.text("STATUS: Pick question to answer") GiveAnswer(_, _) -> html.text("STATUS: Give your answer") _ -> html.text("STATUS: Waiting for next question") }, ]), ]), ]), 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"))) }, ]) } 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)], ), ), ]) } 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), ]), ], ) }