First bits of pure lustre-javascript for user login session
This commit is contained in:
parent
a1e4eb1dff
commit
e6851255dc
41 changed files with 8413 additions and 733 deletions
182
server/src/web/components/answerlist.gleam
Normal file
182
server/src/web/components/answerlist.gleam
Normal file
|
|
@ -0,0 +1,182 @@
|
|||
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,
|
||||
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, initial_array, handler), effect.none())
|
||||
}
|
||||
|
||||
pub opaque type Msg {
|
||||
Initial
|
||||
SharedMessage(message: NotifyClient)
|
||||
ReceiveName(message: String)
|
||||
AcceptName(accept: Option(String))
|
||||
GiveQuestion(name: String, question: String)
|
||||
GiveAnswer(name: String, question: Int, answer: String)
|
||||
}
|
||||
|
||||
fn update(model: Model, msg: Msg) -> #(Model, Effect(Msg)) {
|
||||
case msg {
|
||||
Initial | SharedMessage(_) -> #(model, effect.none())
|
||||
AcceptName(None) -> #(Model(Initial, [], model.handler), effect.none())
|
||||
AcceptName(Some(name)) -> {
|
||||
#(Model(..model, state: GiveQuestion(name, "")), effect.none())
|
||||
}
|
||||
GiveQuestion(name, question) ->
|
||||
case int.parse(question) {
|
||||
Ok(question) if question >= 1 && question <= 14 -> #(
|
||||
Model(..model, state: GiveAnswer(name:, question:, answer: "")),
|
||||
effect.none(),
|
||||
)
|
||||
_ -> #(
|
||||
Model(..model, state: GiveQuestion(name:, question: "")),
|
||||
effect.none(),
|
||||
)
|
||||
}
|
||||
GiveAnswer(name, question, answer) -> {
|
||||
actor.send(
|
||||
model.handler.data,
|
||||
message.GiveSingleAnswer(name:, question:, answer:),
|
||||
)
|
||||
let new_value = case list.key_find(model.answers, question) {
|
||||
Ok(pair) -> {
|
||||
let #(a, _) = pair
|
||||
#(a, answer)
|
||||
}
|
||||
Error(_) -> #("", answer)
|
||||
}
|
||||
#(
|
||||
Model(
|
||||
..model,
|
||||
state: GiveQuestion(name, ""),
|
||||
answers: list.key_set(model.answers, question, new_value),
|
||||
),
|
||||
effect.none(),
|
||||
)
|
||||
}
|
||||
ReceiveName(_) -> #(Model(..model, state: msg), effect.none())
|
||||
}
|
||||
}
|
||||
|
||||
fn view(model: Model) -> Element(Msg) {
|
||||
element.fragment([
|
||||
html.div([attribute.class("terminal-prompt")], [
|
||||
case model.state {
|
||||
Initial ->
|
||||
step_prompt(
|
||||
"Hello stranger. To join the quiz, I need to know your name",
|
||||
fn() { view_input(ReceiveName) },
|
||||
)
|
||||
ReceiveName(name) ->
|
||||
step_prompt(
|
||||
"Your name is " <> name <> "? Are you absolutely sure???",
|
||||
fn() { view_yes_no(name, AcceptName) },
|
||||
)
|
||||
GiveQuestion(name, _) ->
|
||||
step_prompt(
|
||||
"Enter the number of the question you want to answer",
|
||||
fn() { view_named_input(name, GiveQuestion) },
|
||||
)
|
||||
GiveAnswer(name, question, _) ->
|
||||
step_prompt(
|
||||
"Enter the answer to question number " <> int.to_string(question),
|
||||
fn() { view_named_keyed_input(question, name, GiveAnswer) },
|
||||
)
|
||||
_ -> html.h3([], [html.text("Waiting for next question")])
|
||||
},
|
||||
]),
|
||||
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 input your name")
|
||||
ReceiveName(_) -> html.text("STATUS: Please validate your name")
|
||||
GiveQuestion(_, _) -> html.text("STATUS: Pick question to answer")
|
||||
GiveAnswer(_, _, _) -> html.text("STATUS: Give your answer")
|
||||
_ -> html.text("STATUS: Waiting for next question")
|
||||
},
|
||||
]),
|
||||
]),
|
||||
]),
|
||||
terminal_section(model.answers, "[ACTIVE TRANSMISSIONS]", fn(answer) {
|
||||
content_cell(answer)
|
||||
}),
|
||||
])
|
||||
}
|
||||
|
||||
fn terminal_section(
|
||||
answers: List(#(Int, #(String, String))),
|
||||
header: String,
|
||||
extract: fn(#(Int, #(String, String))) -> Element(Msg),
|
||||
) {
|
||||
html.div([attribute.class("terminal-section")], [
|
||||
html.div([attribute.class("terminal-label mb-4")], [
|
||||
html.text(header),
|
||||
]),
|
||||
html.div([attribute.class("participants-grid")], list.map(answers, extract)),
|
||||
])
|
||||
}
|
||||
|
||||
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),
|
||||
]),
|
||||
],
|
||||
)
|
||||
}
|
||||
275
server/src/web/components/card.gleam
Normal file
275
server/src/web/components/card.gleam
Normal file
|
|
@ -0,0 +1,275 @@
|
|||
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 group_registry.{type GroupRegistry}
|
||||
import lustre
|
||||
import lustre/attribute.{class}
|
||||
import lustre/effect.{type Effect}
|
||||
import lustre/element.{type Element}
|
||||
import lustre/element/html
|
||||
import lustre/server_component
|
||||
import shared/message.{type NotifyClient, type NotifyServer, type User, User}
|
||||
import web/components/shared.{
|
||||
step_prompt, view_input, view_named_input, view_yes_no,
|
||||
}
|
||||
|
||||
pub fn component() -> lustre.App(message.ClientsServer, Model, Msg) {
|
||||
lustre.application(init, update, view)
|
||||
}
|
||||
|
||||
type State {
|
||||
AskName
|
||||
NameOk(String)
|
||||
WaitForQuiz(String)
|
||||
Answer(String)
|
||||
}
|
||||
|
||||
pub opaque type Model {
|
||||
Model(
|
||||
state: State,
|
||||
lobby: #(String, List(User)),
|
||||
registry: GroupRegistry(NotifyClient),
|
||||
handler: Started(Subject(NotifyServer)),
|
||||
)
|
||||
}
|
||||
|
||||
fn init(handlers: message.ClientsServer) -> #(Model, Effect(Msg)) {
|
||||
let #(registry, handler) = handlers
|
||||
|
||||
let model = Model(AskName, #("", []), registry, handler)
|
||||
#(model, subscribe(registry, SharedMessage))
|
||||
}
|
||||
|
||||
fn subscribe(
|
||||
registry: GroupRegistry(topic),
|
||||
on_msg handle_msg: fn(topic) -> msg,
|
||||
) -> Effect(msg) {
|
||||
use _, _ <- server_component.select
|
||||
let subject = group_registry.join(registry, "quiz", process.self())
|
||||
|
||||
let selector =
|
||||
process.new_selector()
|
||||
|> process.select_map(subject, handle_msg)
|
||||
|
||||
selector
|
||||
}
|
||||
|
||||
pub opaque type Msg {
|
||||
SharedMessage(message: NotifyClient)
|
||||
ReceiveName(message: String)
|
||||
AcceptName(accept: Option(String))
|
||||
GiveAnswer(name: String, answer: String)
|
||||
}
|
||||
|
||||
fn update(model: Model, msg: Msg) -> #(Model, Effect(Msg)) {
|
||||
let handler = model.handler
|
||||
|
||||
case msg {
|
||||
ReceiveName(name) -> #(Model(..model, state: NameOk(name)), effect.none())
|
||||
AcceptName(Some(name)) -> {
|
||||
actor.send(handler.data, message.GiveName(name:))
|
||||
#(Model(..model, state: WaitForQuiz(name)), effect.none())
|
||||
}
|
||||
AcceptName(None) -> #(Model(..model, state: AskName), effect.none())
|
||||
GiveAnswer(name, answer) -> {
|
||||
actor.send(handler.data, message.GiveAnswer(name, Some(answer)))
|
||||
#(Model(..model, state: WaitForQuiz(name)), effect.none())
|
||||
}
|
||||
SharedMessage(shared_msg) -> #(
|
||||
handle_server_message(model, shared_msg),
|
||||
effect.none(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_server_message(model: Model, notify_client) {
|
||||
case notify_client {
|
||||
message.Lobby(question, lobby) -> Model(..model, lobby: #(question, lobby))
|
||||
message.Exit -> Model(AskName, #("", []), model.registry, model.handler)
|
||||
message.Answer ->
|
||||
case model.state {
|
||||
// We are currently waiting for next quiz question, ok to switch to answer mode
|
||||
WaitForQuiz(name) -> Model(..model, state: Answer(name))
|
||||
// We are not in a state to react, ignore switch to answer mode.
|
||||
_ -> model
|
||||
}
|
||||
message.Await ->
|
||||
case model.state {
|
||||
Answer(name) -> Model(..model, state: WaitForQuiz(name))
|
||||
_ -> model
|
||||
}
|
||||
message.Ping -> {
|
||||
let has_name = case model.state {
|
||||
Answer(name) -> Some(name)
|
||||
WaitForQuiz(name) -> Some(name)
|
||||
_ -> None
|
||||
}
|
||||
case has_name {
|
||||
Some(name) -> actor.send(model.handler.data, message.Pong(name))
|
||||
_ -> Nil
|
||||
}
|
||||
model
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn view(model: Model) -> Element(Msg) {
|
||||
let #(question, lobby) = model.lobby
|
||||
|
||||
element.fragment([
|
||||
html.div([attribute.class("terminal-prompt")], [
|
||||
case model.state {
|
||||
AskName ->
|
||||
step_prompt(
|
||||
"Hello stranger. To join the quiz, I need to know your name",
|
||||
fn() { view_input(ReceiveName) },
|
||||
)
|
||||
NameOk(name) ->
|
||||
step_prompt(
|
||||
"Your name is " <> name <> "? Are you absolutely sure???",
|
||||
fn() { view_yes_no(name, AcceptName) },
|
||||
)
|
||||
Answer(name) ->
|
||||
step_prompt(
|
||||
"The Quiz Lead will now ask the question, and you may answer.",
|
||||
fn() { view_named_input(name, GiveAnswer) },
|
||||
)
|
||||
_ -> html.h3([], [html.text("Waiting for next question")])
|
||||
},
|
||||
]),
|
||||
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 {
|
||||
AskName -> html.text("STATUS: Please input your name")
|
||||
NameOk(_) -> html.text("STATUS: Please validate your name")
|
||||
Answer(_) ->
|
||||
html.div([], [
|
||||
html.div([], [html.text("STATUS: Answer the following:")]),
|
||||
html.div([], [html.text(question)]),
|
||||
])
|
||||
_ -> html.text("STATUS: Waiting for next question")
|
||||
},
|
||||
]),
|
||||
]),
|
||||
]),
|
||||
html.div([class("terminal-section")], case lobby {
|
||||
[] -> []
|
||||
lobby -> {
|
||||
let answered =
|
||||
list.filter(lobby, fn(x) {
|
||||
case x.answer {
|
||||
message.IDontKnow | message.HasAnswered | message.GivenAnswer(_) ->
|
||||
True
|
||||
_ -> False
|
||||
}
|
||||
})
|
||||
|> list.length
|
||||
|> int.to_string
|
||||
let size = lobby |> list.length |> int.to_string
|
||||
[
|
||||
html.div([attribute.class("terminal-box")], [
|
||||
html.span([attribute.class("terminal-label")], [
|
||||
html.text("[PROGRESS] "),
|
||||
]),
|
||||
html.text("Answered: "),
|
||||
case answered == size {
|
||||
True -> html.text("Everyone!")
|
||||
False -> html.text(answered <> "/" <> size)
|
||||
},
|
||||
]),
|
||||
]
|
||||
}
|
||||
}),
|
||||
terminal_section(
|
||||
lobby,
|
||||
"[ACTIVE TRANSMISSIONS]",
|
||||
fn(x) {
|
||||
case x.answer {
|
||||
message.GivenAnswer(_) | message.HasAnswered -> True
|
||||
_ -> False
|
||||
}
|
||||
},
|
||||
fn(user) {
|
||||
let User(name, ping_time, answer) = user
|
||||
case answer {
|
||||
message.GivenAnswer(answer) -> answer
|
||||
message.HasAnswered -> "Answer Given"
|
||||
_ -> "Odd State..."
|
||||
}
|
||||
|> content_cell(name, ping_time, _)
|
||||
},
|
||||
),
|
||||
terminal_section(
|
||||
lobby,
|
||||
"[P A S S]",
|
||||
fn(x) {
|
||||
case x.answer {
|
||||
message.IDontKnow -> True
|
||||
_ -> False
|
||||
}
|
||||
},
|
||||
fn(user) {
|
||||
let User(name, ping_time, _) = user
|
||||
content_cell(name, ping_time, "P.A.S.S :(")
|
||||
},
|
||||
),
|
||||
terminal_section(
|
||||
lobby,
|
||||
"[AWAITING RESPONSE]",
|
||||
fn(x) {
|
||||
case x.answer {
|
||||
message.NotAnswered -> True
|
||||
_ -> False
|
||||
}
|
||||
},
|
||||
fn(user) {
|
||||
case user {
|
||||
User(name, ping_time, _) ->
|
||||
content_cell(name, ping_time, "Not Answered")
|
||||
}
|
||||
},
|
||||
),
|
||||
])
|
||||
}
|
||||
|
||||
fn terminal_section(
|
||||
lobby: List(User),
|
||||
header: String,
|
||||
filter: fn(User) -> Bool,
|
||||
extract: fn(User) -> Element(Msg),
|
||||
) {
|
||||
html.div([attribute.class("terminal-section")], [
|
||||
html.div([attribute.class("terminal-label mb-4")], [
|
||||
html.text(header),
|
||||
]),
|
||||
html.div(
|
||||
[attribute.class("participants-grid")],
|
||||
list.filter(lobby, filter)
|
||||
|> list.map(extract),
|
||||
),
|
||||
])
|
||||
}
|
||||
|
||||
fn content_cell(header: String, ping_time: Int, content: String) -> Element(Msg) {
|
||||
html.div(
|
||||
[
|
||||
class(case ping_time > 1 {
|
||||
True -> "participant-disconnect"
|
||||
False -> "participant-box"
|
||||
}),
|
||||
],
|
||||
[
|
||||
html.div([class("participant-name")], [
|
||||
html.text("► " <> header),
|
||||
]),
|
||||
html.div([class("participant-answer")], [
|
||||
html.text(content),
|
||||
]),
|
||||
],
|
||||
)
|
||||
}
|
||||
124
server/src/web/components/control.gleam
Normal file
124
server/src/web/components/control.gleam
Normal file
|
|
@ -0,0 +1,124 @@
|
|||
// IMPORTS ---------------------------------------------------------------------
|
||||
import gleam/dynamic/decode
|
||||
import gleam/erlang/process.{type Subject}
|
||||
import gleam/otp/actor.{type Started}
|
||||
import gleam/pair
|
||||
import group_registry.{type GroupRegistry}
|
||||
import lustre
|
||||
import lustre/attribute
|
||||
import lustre/effect.{type Effect}
|
||||
import lustre/element.{type Element}
|
||||
import lustre/element/html
|
||||
import lustre/element/keyed
|
||||
import lustre/event
|
||||
import lustre/server_component
|
||||
import shared/message.{
|
||||
type NotifyClient, type NotifyServer, AnswerQuiz, RevealAnswer,
|
||||
}
|
||||
|
||||
pub fn component() -> lustre.App(message.ClientsServer, Model, Msg) {
|
||||
lustre.application(init, update, view)
|
||||
}
|
||||
|
||||
type State {
|
||||
Quiz
|
||||
Reveal
|
||||
}
|
||||
|
||||
pub opaque type Model {
|
||||
Model(
|
||||
state: State,
|
||||
registry: GroupRegistry(NotifyClient),
|
||||
handler: Started(Subject(NotifyServer)),
|
||||
)
|
||||
}
|
||||
|
||||
pub opaque type Msg {
|
||||
AnnounceQuiz
|
||||
AnnounceAnswer
|
||||
PurgePlayers
|
||||
SharedMessage(message: message.NotifyClient)
|
||||
}
|
||||
|
||||
fn init(handlers: message.ClientsServer) -> #(Model, Effect(Msg)) {
|
||||
let #(registry, handler) = handlers
|
||||
|
||||
let model = Model(state: Quiz, registry:, handler:)
|
||||
#(model, subscribe(pair.first(handlers), SharedMessage))
|
||||
}
|
||||
|
||||
fn subscribe(
|
||||
registry: GroupRegistry(topic),
|
||||
on_msg handle_msg: fn(topic) -> msg,
|
||||
) -> Effect(msg) {
|
||||
use _, _ <- server_component.select
|
||||
let subject = group_registry.join(registry, "quiz", process.self())
|
||||
|
||||
let selector =
|
||||
process.new_selector()
|
||||
|> process.select_map(subject, handle_msg)
|
||||
|
||||
selector
|
||||
}
|
||||
|
||||
fn update(model: Model, msg: Msg) -> #(Model, Effect(Msg)) {
|
||||
let handler = model.handler
|
||||
#(
|
||||
case msg {
|
||||
PurgePlayers -> {
|
||||
// Temp removed button to issue this action.
|
||||
actor.send(handler.data, message.PurgePlayers)
|
||||
model
|
||||
}
|
||||
AnnounceQuiz -> {
|
||||
actor.send(handler.data, AnswerQuiz)
|
||||
Model(..model, state: Quiz)
|
||||
}
|
||||
AnnounceAnswer -> {
|
||||
actor.send(handler.data, RevealAnswer)
|
||||
Model(..model, state: Reveal)
|
||||
}
|
||||
SharedMessage(message.Await) -> Model(..model, state: Reveal)
|
||||
SharedMessage(_) -> model
|
||||
},
|
||||
effect.none(),
|
||||
)
|
||||
}
|
||||
|
||||
fn view(model: Model) -> Element(Msg) {
|
||||
html.div([attribute.class("terminal-section")], [
|
||||
html.div([attribute.class("participants-grid")], [
|
||||
element.fragment([
|
||||
keyed.div([attribute.class("participand-hidden")], [
|
||||
#("reveal", html.text("")),
|
||||
]),
|
||||
keyed.div([attribute.class("participand-hidden")], [
|
||||
#("reveal", html.text("")),
|
||||
]),
|
||||
keyed.div([attribute.class("control")], [
|
||||
#("reveal", html.text("")),
|
||||
]),
|
||||
case model.state {
|
||||
Quiz -> {
|
||||
keyed.div([attribute.class("control")], [
|
||||
#("reveal", view_button("Reveal answers", AnnounceAnswer)),
|
||||
])
|
||||
}
|
||||
Reveal -> {
|
||||
keyed.div([attribute.class("control")], [
|
||||
#("next", view_button("Ask next question", AnnounceQuiz)),
|
||||
])
|
||||
}
|
||||
},
|
||||
]),
|
||||
]),
|
||||
])
|
||||
}
|
||||
|
||||
fn view_button(text: String, on_submit handle_keydown: msg) -> Element(msg) {
|
||||
let on_keydown = event.on("click", { decode.success(handle_keydown) })
|
||||
|
||||
html.button([attribute.class("controlbutton"), on_keydown], [
|
||||
html.text(text),
|
||||
])
|
||||
}
|
||||
104
server/src/web/components/shared.gleam
Normal file
104
server/src/web/components/shared.gleam
Normal file
|
|
@ -0,0 +1,104 @@
|
|||
import gleam/dynamic/decode
|
||||
import gleam/option.{type Option, None, Some}
|
||||
import lustre/attribute
|
||||
import lustre/element.{type Element}
|
||||
import lustre/element/html
|
||||
import lustre/element/keyed
|
||||
import lustre/event
|
||||
import lustre/server_component
|
||||
|
||||
// Components use "keyed.div" rather than "html.div" for input fields
|
||||
// The value of the fields are uncontrolled, so this is needed to
|
||||
// get a new state between each input, or else the value transfers between
|
||||
// input fields.
|
||||
//
|
||||
// see: https://hexdocs.pm/lustre/lustre/element/keyed.html
|
||||
|
||||
pub fn view_named_input(
|
||||
name: String,
|
||||
on_submit handle_keydown: fn(String, String) -> msg,
|
||||
) -> Element(msg) {
|
||||
prompt_input(
|
||||
"nameinput",
|
||||
key_down(fn(a: String) { decode.success(handle_keydown(name, a)) }, fn() {
|
||||
decode.failure(handle_keydown(name, ""), "")
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn view_named_keyed_input(
|
||||
question: Int,
|
||||
name: String,
|
||||
on_submit handle_keydown: fn(String, Int, String) -> msg,
|
||||
) -> Element(msg) {
|
||||
prompt_input(
|
||||
"keyput",
|
||||
key_down(
|
||||
fn(a: String) { decode.success(handle_keydown(name, question, a)) },
|
||||
fn() { decode.failure(handle_keydown(name, question, ""), "") },
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn view_input(on_submit handle_keydown: fn(String) -> msg) -> Element(msg) {
|
||||
prompt_input(
|
||||
"input",
|
||||
key_down(fn(a: String) { decode.success(handle_keydown(a)) }, fn() {
|
||||
decode.failure(handle_keydown(""), "")
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn view_yes_no(
|
||||
accepted: String,
|
||||
on_submit handle_button: fn(Option(String)) -> msg,
|
||||
) -> Element(msg) {
|
||||
html.div([], [
|
||||
html.button([event.on_click(handle_button(Some(accepted)))], [
|
||||
html.text(" <Yes> "),
|
||||
]),
|
||||
html.text(" - "),
|
||||
html.button([event.on_click(handle_button(None))], [html.text(" <No> ")]),
|
||||
])
|
||||
}
|
||||
|
||||
fn key_down(
|
||||
success: fn(String) -> decode.Decoder(msg),
|
||||
fail: fn() -> decode.Decoder(msg),
|
||||
) {
|
||||
event.on("keydown", {
|
||||
use key <- decode.field("key", decode.string)
|
||||
use value <- decode.subfield(["target", "value"], decode.string)
|
||||
|
||||
case key {
|
||||
"Enter" if value != "" -> success(value)
|
||||
_ -> fail()
|
||||
}
|
||||
})
|
||||
|> server_component.include(["key", "target.value"])
|
||||
}
|
||||
|
||||
fn prompt_input(key, on_keydown) {
|
||||
keyed.div([], [
|
||||
#(key <> "header", html.text("$>")),
|
||||
#(
|
||||
key,
|
||||
html.input([
|
||||
attribute.type_("text"),
|
||||
on_keydown,
|
||||
attribute.autofocus(True),
|
||||
]),
|
||||
),
|
||||
])
|
||||
}
|
||||
|
||||
pub fn step_prompt(text: String, fetch: fn() -> Element(a)) {
|
||||
html.div([attribute.class("prompt-line")], [
|
||||
html.div([attribute.class("prompt-text")], [
|
||||
html.div([], [
|
||||
html.text(text),
|
||||
]),
|
||||
fetch(),
|
||||
]),
|
||||
])
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue