more work on pre-game pages

This commit is contained in:
Lett Osprey 2026-04-06 14:03:33 +02:00
parent 987e2b5576
commit 7a8acf27a7
5 changed files with 259 additions and 180 deletions

View file

@ -1,20 +1,20 @@
import gleam/dynamic/decode import gleam/dynamic/decode
import gleam/int
import gleam/json import gleam/json
import gleam/list import gleam/list
import gleam/option.{type Option, None, Some} import gleam/option.{type Option, None, Some}
import gleam/result import gleam/result
import gleam/string import gleam/string
import lustre import lustre
import lustre/attribute.{class}
import lustre/effect.{type Effect} import lustre/effect.{type Effect}
import lustre/element.{type Element} import model.{
import lustre/element/html type Model, type Msg, AwaitPlayers, Empty, EnterPin, Initialize, KeyPin, Model,
import lustre/event PickPlayer, Players, SelectedGamestyle, SelectedPlayer, SelectedRoom,
}
import plinth/browser/document import plinth/browser/document
import plinth/browser/element as plinth_element import plinth/browser/element as plinth_element
import rsvp import rsvp
import shared.{type Room} import shared.{type Room}
import view.{view}
pub fn main() { pub fn main() {
let initial_items = let initial_items =
@ -32,10 +32,6 @@ pub fn main() {
Nil Nil
} }
type Model {
Model(rooms: List(Room), state: State, ohno: Option(String))
}
fn init(initial: #(List(Room), Option(String))) -> #(Model, Effect(Msg)) { fn init(initial: #(List(Room), Option(String))) -> #(Model, Effect(Msg)) {
let #(rooms, ohno) = initial let #(rooms, ohno) = initial
let model = Model(rooms:, state: Empty, ohno:) let model = Model(rooms:, state: Empty, ohno:)
@ -43,67 +39,68 @@ fn init(initial: #(List(Room), Option(String))) -> #(Model, Effect(Msg)) {
#(model, effect.none()) #(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)) { fn update(model: Model, msg: Msg) -> #(Model, Effect(Msg)) {
case msg { case msg {
Initialize -> init(#(model.rooms, None)) Initialize -> init(#(model.rooms, None))
SelectRoom(room) -> #( SelectedRoom(room) -> #(
Model(..model, state: EnterPin(room:, pin: "")), Model(..model, state: EnterPin(room:, pin: "")),
effect.none(), 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 { case model.state {
EnterPin(room, _) -> { EnterPin(room, _) -> #(
let decode_answer = { Model(..model, state: case string.length(pin) < 4 {
use id <- decode.field("id", decode.string) False -> model.SelectGamestyle(room:, pin:)
// use text <- decode.field("text", decode.string) True -> EnterPin(room:, pin:)
decode.success(id) }),
} effect.none(),
// 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"))) init(#(
// Invalid model state, start over 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)) -> { Players(Ok(players)) -> {
echo "got players" echo "got players"
case model.state { case model.state {
@ -111,8 +108,11 @@ fn update(model: Model, msg: Msg) -> #(Model, Effect(Msg)) {
Model(..model, state: PickPlayer(room:, pin:, players: [players])), Model(..model, state: PickPlayer(room:, pin:, players: [players])),
effect.none(), 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)) -> Players(Error(x)) ->
@ -120,15 +120,26 @@ fn update(model: Model, msg: Msg) -> #(Model, Effect(Msg)) {
model.rooms, model.rooms,
Some("Error fetching players " <> decode_rsvp_error(x)), 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) { fn decode_rsvp_error(rsvp_error: rsvp.Error) {
case rsvp_error { case rsvp_error {
rsvp.BadBody -> "Bad body" rsvp.BadBody -> "Bad body"
rsvp.BadUrl(x) -> "Bad url" rsvp.BadUrl(_x) -> "Bad url"
rsvp.HttpError(x) -> "Http error" rsvp.HttpError(_x) -> "Http error"
rsvp.JsonError(x) -> "Json error" <> decode_decode_error(x) rsvp.JsonError(x) -> "Json error" <> decode_decode_error(x)
rsvp.NetworkError -> "Network error" rsvp.NetworkError -> "Network error"
rsvp.UnhandledResponse(_) -> "Unhandled response" 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) "(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),
]),
])
}

26
client/src/model.gleam Normal file
View file

@ -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))
}

142
client/src/view.gleam Normal file
View file

@ -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),
]),
])
}

View file

@ -8,24 +8,24 @@
<script id="model" type="application/json"> <script id="model" type="application/json">
[ [
{ {
"id": "abt", "id": "1234",
"name": "Billettering", "name": "Team A",
"key": "T5X6" "key": "1234"
}, },
{ {
"id": "slg", "id": "1235",
"name": "Salg", "name": "Team B",
"key": "6B4T" "key": "1235"
}, },
{ {
"id": "prs", "id": "1236",
"name": "Personalisering", "name": "Team C",
"key": "P2Q5" "key": "1236"
}, },
{ {
"id": "srp", "id": "1237",
"name": "Support", "name": "Team D",
"key": "P2Q5" "key": "1237"
} }
] ]
</script> </script>

View file

@ -38,11 +38,11 @@ pub fn main() {
[] | ["index.html"] -> serve_static("root.html") [] | ["index.html"] -> serve_static("root.html")
["client.js"] -> serve_static("client.js") ["client.js"] -> serve_static("client.js")
["static", file] -> serve_static(file) ["static", file] -> serve_static(file)
["socket", "card", id] -> ["socket", "live", id] ->
sockethandler.serve(req, card.component(), id, room_handler) sockethandler.serve(req, card.component(), id, room_handler)
["socket", "control", id] -> ["socket", "control", id] ->
sockethandler.serve(req, control.component(), id, room_handler) sockethandler.serve(req, control.component(), id, room_handler)
["socket", "slow", id] -> ["socket", "single", id] ->
sockethandler.serve_slow( sockethandler.serve_slow(
req, req,
answerlist.component(), answerlist.component(),