From ccdba0f23953fb7a1efc1017c2df5c989526a75b Mon Sep 17 00:00:00 2001 From: Lett Osprey Date: Mon, 30 Mar 2026 22:19:21 +0200 Subject: [PATCH] Fixes to routing and new build for Dockerfile --- .gitignore | 7 +++- Dockerfile | 6 +++ client/src/client.gleam | 77 +++++++++++++++++++++++++++------------ server/src/quizterm.gleam | 54 +++++++++++++++------------ 4 files changed, 97 insertions(+), 47 deletions(-) diff --git a/.gitignore b/.gitignore index 095eb04..877ce67 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,6 @@ -localbuild.sh +**/.* +/localbuild.sh +/client/build +/server/build +/server/priv/static/client.js +/server/priv/static/index.html diff --git a/Dockerfile b/Dockerfile index a72311e..4486743 100644 --- a/Dockerfile +++ b/Dockerfile @@ -6,9 +6,15 @@ FROM ghcr.io/gleam-lang/gleam:${GLEAM_VERSION}-erlang-alpine AS builder COPY ./server/priv /quizterm/server/priv COPY ./server/src /quizterm/server/src COPY ./server/gleam.toml /quizterm/server/ +COPY ./client/src /quizterm/client/src +COPY ./client/gleam.toml /quizterm/client/ RUN cd /quizterm/server && gleam deps download +# Compile client code and move generated javascript to server project +RUN cd /quizterm/client \ + && gleam run -m lustre/dev build --outdir=/quizterm/server/priv/static + # Compile the server code RUN cd /quizterm/server \ && gleam export erlang-shipment diff --git a/client/src/client.gleam b/client/src/client.gleam index 1f2ecdb..2402959 100644 --- a/client/src/client.gleam +++ b/client/src/client.gleam @@ -1,7 +1,9 @@ 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} @@ -10,7 +12,7 @@ import lustre/element/html import lustre/event import plinth/browser/document import plinth/browser/element as plinth_element -import shared +import shared.{type Room} pub fn main() { let initial_items = @@ -29,37 +31,47 @@ pub fn main() { } type Model { - Model(rooms: List(shared.Room), name: String) + Model( + rooms: List(Room), + name: Option(String), + pin: Option(String), + typed: String, + ) } -fn init(items: List(shared.Room)) -> #(Model, Effect(Msg)) { - let model = Model(rooms: items, name: "") +fn init(items: List(Room)) -> #(Model, Effect(State)) { + let model = Model(rooms: items, name: None, pin: None, typed: "") #(model, effect.none()) } -type Msg { - UserTypedNewItem(String) - UserAddedItem +type State { + SelectRoom(String) + Initial + KeyIn(String) } -fn update(model: Model, msg: Msg) -> #(Model, Effect(Msg)) { +fn update(model: Model, msg: State) -> #(Model, Effect(msg)) { case msg { - UserAddedItem -> + Initial -> case model.name { _ -> #(model, effect.none()) } - UserTypedNewItem(text) -> { - echo "CLICK " <> text <> "aa" - #(Model(..model, name: text), effect.none()) + SelectRoom(text) -> { + #(Model(..model, name: Some(text)), effect.none()) + } + KeyIn(newpin) -> { + case string.length(newpin) < 4 { + False -> #(Model(..model, pin: Some(newpin)), effect.none()) + True -> #(Model(..model, typed: newpin), effect.none()) + } } } } -fn view(model: Model) -> Element(Msg) { - echo "NAME " <> model.name - case model.name { - "" -> +fn view(model: Model) -> Element(State) { + case model.name, model.pin { + None, _ -> html.div([], [ html.div([class("terminal-header")], [ html.div([class("terminal-status")], [ @@ -72,7 +84,7 @@ fn view(model: Model) -> Element(Msg) { ]), view_room_list(model.rooms), ]) - _ -> { + Some(_), None -> { html.div([], [ html.div([class("terminal-header")], [ html.div([class("terminal-status")], [ @@ -86,21 +98,40 @@ fn view(model: Model) -> Element(Msg) { pin(), ]) } + Some(_), Some(_) -> { + html.div([], [ + 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")], [ + html.text("<< Please Log On to use QuizTerm. >>"), + ]), + ]), + ]), + html.text("FETCHING USERS FOR ROOM"), + ]) + } } } -fn pin() -> Element(Msg) { +fn pin() -> Element(State) { 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(KeyIn), + attribute.autofocus(True), + ]), ]), ]) } -fn view_room_list(items: List(shared.Room)) -> Element(Msg) { +fn view_room_list(items: List(Room)) -> Element(State) { html.div([attribute.class("terminal-section")], [ html.div([attribute.class("terminal-label mb-4")], [ html.text("Select room to play in"), @@ -109,7 +140,7 @@ fn view_room_list(items: List(shared.Room)) -> Element(Msg) { [] -> [html.text("No items in your list yet.")] _ -> { list.index_map(items, fn(item, index) { - content_cell(index, item, UserTypedNewItem) + content_cell(index, item, SelectRoom) }) } }), @@ -118,9 +149,9 @@ fn view_room_list(items: List(shared.Room)) -> Element(Msg) { fn content_cell( number: Int, - room: shared.Room, - on_click: fn(String) -> Msg, -) -> Element(Msg) { + 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/server/src/quizterm.gleam b/server/src/quizterm.gleam index 7030a77..653a210 100644 --- a/server/src/quizterm.gleam +++ b/server/src/quizterm.gleam @@ -4,13 +4,14 @@ import backend/statehandler import gleam/bytes_tree import gleam/erlang/application import gleam/erlang/process +import gleam/http import gleam/http/request import gleam/http/response.{type Response} import gleam/list import gleam/option.{None} import gleam/result import gleam/string -import mist.{type ResponseData, File} +import mist.{type ResponseData} import web/components/answerlist import web/components/card import web/components/control @@ -25,29 +26,36 @@ pub fn main() { let assert Ok(room_handler) = roomhandler.initialize(state_handler) let assert Ok(_) = - fn(req) { - case request.path_segments(req) { - ["lustre", "runtime.mjs"] -> serve_runtime() - [] | ["index.html"]-> serve_static("root.html") - ["client.js"] -> serve_static("client.js") - ["static", file] -> serve_static(file) - ["socket", "card", id] -> - sockethandler.serve(req, card.component(), id, room_handler) - ["socket", "control", id] -> - sockethandler.serve(req, control.component(), id, room_handler) - ["socket", "slow", id] -> - sockethandler.serve_slow( - req, - answerlist.component(), - id, - room_handler, - state_handler, - ) + fn(req: request.Request(mist.Connection)) { + case req.method { + // Filter out Head requests, + http.Head -> + response.new(200) + |> response.set_body(mist.Bytes(bytes_tree.new())) _ -> - wisp_mist.handler( - router.handle_request(room_handler, state_handler, _), - "very_secret", - )(req) + case request.path_segments(req) { + ["lustre", "runtime.mjs"] -> serve_runtime() + [] | ["index.html"] -> serve_static("root.html") + ["client.js"] -> serve_static("client.js") + ["static", file] -> serve_static(file) + ["socket", "card", id] -> + sockethandler.serve(req, card.component(), id, room_handler) + ["socket", "control", id] -> + sockethandler.serve(req, control.component(), id, room_handler) + ["socket", "slow", id] -> + sockethandler.serve_slow( + req, + answerlist.component(), + id, + room_handler, + state_handler, + ) + _ -> + wisp_mist.handler( + router.handle_request(room_handler, state_handler, _), + "very_secret", + )(req) + } } } |> mist.new