import gleam/dynamic/decode import gleam/int import gleam/json import gleam/list import gleam/option.{type Option, None, Some} import gleam/result import gleam/string import lustre import lustre/attribute.{class} import lustre/effect.{type Effect} import lustre/element.{type Element} import lustre/element/html import lustre/event import plinth/browser/document import plinth/browser/element as plinth_element import rsvp import shared.{type Room} pub fn main() { let initial_items = document.query_selector("#model") |> result.map(plinth_element.inner_text) |> result.try(fn(json) { json.parse(json, shared.grocery_list_decoder()) |> result.replace_error(Nil) }) |> result.unwrap([]) let app = lustre.application(init, update, view) let assert Ok(_) = lustre.start(app, "#app", #(initial_items, None)) Nil } type Model { Model(rooms: List(Room), state: State, ohno: Option(String)) } fn init(initial: #(List(Room), Option(String))) -> #(Model, Effect(Msg)) { let #(rooms, ohno) = initial let model = Model(rooms:, state: Empty, ohno:) #(model, effect.none()) } type State { Empty EnterPin(room: String, pin: String) AwaitPlayers(room: String, pin: String) PickPlayer(room: String, pin: String, players: List(String)) } type Msg { Initialize SelectRoom(String) SelectPlayer(String) KeyPin(String) Players(Result(String, rsvp.Error)) } fn update(model: Model, msg: Msg) -> #(Model, Effect(Msg)) { case msg { Initialize -> init(#(model.rooms, None)) SelectRoom(room) -> #( Model(..model, state: EnterPin(room:, pin: "")), effect.none(), ) KeyPin(pin) -> case model.state { EnterPin(room, _) -> { let decode_answer = { use id <- decode.field("id", decode.string) // use text <- decode.field("text", decode.string) decode.success(id) } // let decode_list = { // decode.list(decode.string) // } case string.length(pin) < 4 { False -> { #( Model( ..model, state: PickPlayer(room:, pin:, players: ["Player a", "Player b", "Player c", "Player d", "Player e", "Player f"]), ), effect.none(), ) // Model(..model, state: AwaitPlayers(room:, pin:)), // rsvp.post( // "http://localhost:1234/api/room/players", // json.object([#("id", json.string("1234"))]), // rsvp.expect_json(decode_answer, Players), // ), // ) } True -> #( Model(..model, state: EnterPin(room:, pin:)), effect.none(), ) } } _ -> init(#(model.rooms, Some("(EnterPin) Invalid state, starting over"))) // Invalid model state, start over } Players(Ok(players)) -> { echo "got players" case model.state { AwaitPlayers(room:, pin:) -> #( Model(..model, state: PickPlayer(room:, pin:, players: [players])), effect.none(), ) _ -> init(#(model.rooms, Some("Invalid state, expected AwaitPlayers"))) // invalid state, start over } } Players(Error(x)) -> init(#( model.rooms, Some("Error fetching players " <> decode_rsvp_error(x)), )) SelectPlayer(x) -> init(#(model.rooms, Some("Players " <> x))) } } fn decode_rsvp_error(rsvp_error: rsvp.Error) { case rsvp_error { rsvp.BadBody -> "Bad body" rsvp.BadUrl(x) -> "Bad url" rsvp.HttpError(x) -> "Http error" rsvp.JsonError(x) -> "Json error" <> decode_decode_error(x) rsvp.NetworkError -> "Network error" rsvp.UnhandledResponse(_) -> "Unhandled response" } } fn decode_decode_error(json_error: json.DecodeError) -> String { case json_error { json.UnableToDecode(x) -> "[unable to decode" <> string.concat( list.map(x, fn(x) { "(" <> decode_ddecode_error(x) <> ")" }), ) <> "]" json.UnexpectedSequence(s) -> "[unexpected sequence " <> s <> "]" json.UnexpectedByte(x) -> "[unexpected byte " <> x <> "]" json.UnexpectedEndOfInput -> "[unexpected end of input]" } } fn decode_ddecode_error(decode_error: decode.DecodeError) -> String { case decode_error { decode.DecodeError(expected:, found:, path:) -> "(e: " <> expected <> ", f: " <> found <> ", p: " <> string.concat(path) } } fn view(model: Model) -> Element(Msg) { html.div([], [ html.div([class("terminal-header")], [ html.div([class("terminal-status")], [ html.span([class("status-blink")], [html.text("●")]), html.div([], [ html.text(" SYSTEM READY"), ]), html.div([], [ case model.ohno { None -> element.none() Some(x) -> html.h3([], [html.text("Fail: " <> x)]) }, ]), html.span([class("ml-8")], [ html.text("<< Please Log On to use QuizTerm. >>"), ]), ]), ]), case model.state { Empty -> view_room_list(model.rooms) EnterPin(_, _) -> pin() AwaitPlayers(_, _) -> html.text("FETCHING USERS FOR ROOM") PickPlayer(_, _, players) -> view_player_list(players) }, ]) } fn pin() -> Element(Msg) { html.div([attribute.class("terminal-section")], [ html.div([attribute.class("terminal-label mb-4")], [ html.text("Select room to play in"), ]), html.div([attribute.class("participants-grid")], [ html.text("Enter PIN code for room"), html.input([ attribute.type_("password"), event.on_input(KeyPin), attribute.autofocus(True), ]), ]), ]) } fn view_room_list(items: List(Room)) -> Element(Msg) { html.div([attribute.class("terminal-section")], [ html.div([attribute.class("terminal-label mb-4")], [ html.text("Select room to play in"), ]), html.div([attribute.class("participants-grid")], case items { [] -> [html.text("No items in your list yet.")] _ -> { list.index_map(items, fn(item, index) { room_cell(index, item, SelectRoom) }) } }), ]) } fn view_player_list(items: List(String)) -> Element(Msg) { html.div([attribute.class("terminal-section")], [ html.div([attribute.class("terminal-label mb-4")], [ html.text("Select room to play in"), ]), html.div([attribute.class("participants-grid")], case items { [] -> [html.text("No items in your list yet.")] _ -> { list.index_map(items, fn(item, index) { player_cell(index, item, SelectPlayer) }) } }), ]) } fn player_cell( number: Int, player: String, on_click: fn(String) -> msg, ) -> Element(msg) { html.div([class("participant-login"), event.on_click(on_click(player))], [ html.div([class("participant-name")], [ html.text("► " <> "[#" <> int.to_string(number) <> "] " <> player), ]), ]) } fn room_cell( number: Int, room: Room, on_click: fn(String) -> msg, ) -> Element(msg) { html.div([class("participant-login"), event.on_click(on_click(room.name))], [ html.div([class("participant-name")], [ html.text("► " <> "[#" <> int.to_string(number) <> "] Team " <> room.name), ]), ]) }