diff --git a/client/src/client.gleam b/client/src/client.gleam index 0f79040..f38ea9d 100644 --- a/client/src/client.gleam +++ b/client/src/client.gleam @@ -1,20 +1,20 @@ 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 model.{ + type Model, type Msg, AwaitPlayers, Empty, EnterPin, Initialize, KeyPin, Model, + PickPlayer, Players, SelectedGamestyle, SelectedPlayer, SelectedRoom, +} import plinth/browser/document import plinth/browser/element as plinth_element import rsvp import shared.{type Room} +import view.{view} pub fn main() { let initial_items = @@ -32,10 +32,6 @@ pub fn main() { 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:) @@ -43,67 +39,68 @@ fn init(initial: #(List(Room), Option(String))) -> #(Model, Effect(Msg)) { #(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) -> #( + SelectedRoom(room) -> #( Model(..model, state: EnterPin(room:, pin: "")), effect.none(), ) - KeyPin(pin) -> + KeyPin(pin) -> { + //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 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(), - ) - } - } + EnterPin(room, _) -> #( + Model(..model, state: case string.length(pin) < 4 { + False -> model.SelectGamestyle(room:, pin:) + True -> EnterPin(room:, pin:) + }), + effect.none(), + ) _ -> - init(#(model.rooms, Some("(EnterPin) Invalid state, starting over"))) - // Invalid model state, start over + init(#( + model.rooms, + Some("(fail: enterpin) Invalid state, starting over"), + )) } - + } + SelectedGamestyle(style) -> { + case model.state { + model.SelectGamestyle(room:, pin:) -> #( + Model(..model, state: case style { + "Single Game" -> + PickPlayer(room:, pin:, players: [ + "Player a", + "Player b", + "Player c", + "Player d", + "Player e", + "Player f", + ]) + _ -> model.JoinLive(room:, pin:) + }), + effect.none(), + ) + _ -> + init(#( + model.rooms, + Some("(fail: selectgamestyle) Invalid state, starting over"), + )) + } + // 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), + // ), + // ) + } Players(Ok(players)) -> { echo "got players" case model.state { @@ -111,8 +108,11 @@ fn update(model: Model, msg: Msg) -> #(Model, Effect(Msg)) { Model(..model, state: PickPlayer(room:, pin:, players: [players])), effect.none(), ) - _ -> init(#(model.rooms, Some("Invalid state, expected AwaitPlayers"))) - // invalid state, start over + _ -> + init(#( + model.rooms, + Some("(fail: awaitplayers) Invalid state, starting over"), + )) } } Players(Error(x)) -> @@ -120,15 +120,26 @@ fn update(model: Model, msg: Msg) -> #(Model, Effect(Msg)) { model.rooms, Some("Error fetching players " <> decode_rsvp_error(x)), )) - SelectPlayer(x) -> init(#(model.rooms, Some("Players " <> x))) + SelectedPlayer(player) -> + case model.state { + PickPlayer(room:, pin:, players: _) -> #( + Model(..model, state: model.JoinSingle(room:, pin:, player:)), + effect.none(), + ) + _ -> + init(#( + model.rooms, + Some("(fail: pickplayer) Invalid state, starting over"), + )) + } } } 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.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" @@ -155,103 +166,3 @@ fn decode_ddecode_error(decode_error: decode.DecodeError) -> String { "(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), - ]), - ]) -} diff --git a/client/src/model.gleam b/client/src/model.gleam new file mode 100644 index 0000000..3a03594 --- /dev/null +++ b/client/src/model.gleam @@ -0,0 +1,26 @@ +import gleam/option.{type Option} +import rsvp.{type Error} +import shared.{type Room} + +pub type Model { + Model(rooms: List(Room), state: State, ohno: Option(String)) +} + +pub type State { + Empty + EnterPin(room: String, pin: String) + SelectGamestyle(room: String, pin: String) + AwaitPlayers(room: String, pin: String) + PickPlayer(room: String, pin: String, players: List(String)) + JoinLive(room: String, pin: String) + JoinSingle(room: String, pin: String, player: String) +} + +pub type Msg { + Initialize + SelectedRoom(String) + SelectedPlayer(String) + SelectedGamestyle(String) + KeyPin(String) + Players(Result(String, Error)) +} diff --git a/client/src/view.gleam b/client/src/view.gleam new file mode 100644 index 0000000..35f6ef4 --- /dev/null +++ b/client/src/view.gleam @@ -0,0 +1,142 @@ +import gleam/int +import gleam/list +import gleam/option.{None, Some} +import lustre/attribute.{class} +import lustre/element.{type Element} +import lustre/element/html +import lustre/event +import lustre/server_component +import model.{ + type Model, type Msg, AwaitPlayers, Empty, EnterPin, JoinLive, JoinSingle, + KeyPin, PickPlayer, SelectGamestyle, SelectedPlayer, SelectedRoom, +} +import shared.{type Room} + +pub fn view(model: Model) -> Element(Msg) { + case model.state { + Empty -> view_room_list(model.rooms) + EnterPin(_, _) -> view_enter_pin() + SelectGamestyle(_, _) -> view_live_or_single() + AwaitPlayers(_, _) -> html.text("FETCHING USERS FOR ROOM") + PickPlayer(_, _, players) -> view_player_list(players) + JoinLive(room:, pin:) -> view_join_live(room, pin) + JoinSingle(room:, pin:, player:) -> view_join_single(room, pin, player) + } +} + +fn layout(header: String, ohno: option.Option(String), body: List(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 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. >>"), + ]), + ]), + ]), + html.div([attribute.class("terminal-section")], [ + html.div([attribute.class("terminal-label mb-4")], [ + html.text(header), + ]), + html.div([attribute.class("participants-grid")], body), + ]), + ]) +} + +fn view_room_list(items: List(Room)) -> Element(Msg) { + layout("Select room to play in", None, case items { + [] -> [html.text("No items in your list yet.")] + _ -> { + list.index_map(items, fn(item, index) { + room_cell(index, item, SelectedRoom) + }) + } + }) +} + +fn view_enter_pin() -> Element(Msg) { + layout("Enter PIN code for room", None, [ + html.input([ + attribute.type_("password"), + event.on_input(KeyPin), + attribute.autofocus(True), + ]), + ]) +} + +fn view_join_live(room: String, pin: String) -> Element(Msg) { + html.div([attribute.class("terminal-section")], [ + html.div([attribute.class("terminal-label mb-4")], [ + server_component.element( + [server_component.route("/socket/live/" <> room)], + [], + ), + server_component.element( + [server_component.route("/socket/control/" <> room)], + [], + ), + ]), + ]) +} + +fn view_join_single(room: String, pin: String, player: String) -> Element(Msg) { + html.div([attribute.class("terminal-section")], [ + html.div([attribute.class("terminal-label mb-4")], [ + server_component.element( + [server_component.route("/socket/single/" <> room)], + [], + ), + ]), + ]) +} + +fn view_live_or_single() -> Element(Msg) { + layout("Select type of play", None, [ + click_cell(1, "Live Game", model.SelectedGamestyle), + click_cell(2, "Single Game", model.SelectedGamestyle), + ]) +} + +fn view_player_list(items: List(String)) -> Element(Msg) { + layout("Select or enter your player", None, case items { + [] -> [html.text("No items in your list yet.")] + _ -> { + list.index_map(items, fn(item, index) { + click_cell(index, item, SelectedPlayer) + }) + } + }) +} + +fn click_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.id))], [ + html.div([class("participant-name")], [ + html.text("► " <> "[#" <> int.to_string(number) <> "] Team " <> room.name), + ]), + ]) +} diff --git a/server/priv/static/root.html b/server/priv/static/root.html index 993a821..c0443ea 100644 --- a/server/priv/static/root.html +++ b/server/priv/static/root.html @@ -8,24 +8,24 @@ diff --git a/server/src/quizterm.gleam b/server/src/quizterm.gleam index 653a210..283a38c 100644 --- a/server/src/quizterm.gleam +++ b/server/src/quizterm.gleam @@ -38,11 +38,11 @@ pub fn main() { [] | ["index.html"] -> serve_static("root.html") ["client.js"] -> serve_static("client.js") ["static", file] -> serve_static(file) - ["socket", "card", id] -> + ["socket", "live", id] -> sockethandler.serve(req, card.component(), id, room_handler) ["socket", "control", id] -> sockethandler.serve(req, control.component(), id, room_handler) - ["socket", "slow", id] -> + ["socket", "single", id] -> sockethandler.serve_slow( req, answerlist.component(),