Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions leptos/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ leptos-use = { version = "0.15.7", features = [
leptos_meta = "0.7.7"
leptos_router = { version = "0.7.7", features = ["tracing"] }
reactive_stores = "0.1.8"
reqwest.workspace = true
serde = { workspace = true, features = ["derive"] }
stylance = "0.5.5"
thaw = { version = "0.4.4", features = ["csr"] }
Expand All @@ -29,4 +30,8 @@ web-sys = { version = "0.3.77", features = [
"CanvasRenderingContext2d",
"MediaRecorder",
"AudioNode",
"File",
"FileList",
] }
itertools.workspace = true
strum = { workspace = true, features = ["derive"] }
12 changes: 11 additions & 1 deletion leptos/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,15 @@ A simple SPA using leptos.

![](https://file.notion.so/f/f/5f1a3a94-a29f-407d-80af-617105cb793d/8938aed8-802c-4a23-9d47-10cd593ef1ca/image.png?table=block&id=1c0e579b-ff89-80df-abb6-c7766ae48094&spaceId=5f1a3a94-a29f-407d-80af-617105cb793d&expirationTimestamp=1742860800000&signature=FwK8fJ19MquzJKhRT0vRtm7JaOl7yjWOgEYxAz95gtE&downloadName=image.png)

- A QR Scanner
## A QR Scanner

Scans QR code in `single` or `multiple` mode.

- A Audio Recorder

## A Form

The values are stored in local storage at key set by field `Code`.

![leptos_gif](https://github.com/user-attachments/assets/41e725d9-bb92-4976-9880-e849d378cbde_gif](https://github.com/user-attachments/assets/41e725d9-bb92-4976-9880-e849d378cbde)

5 changes: 5 additions & 0 deletions leptos/app.css
Original file line number Diff line number Diff line change
Expand Up @@ -46,3 +46,8 @@ main {
max-height: 100px;
background-color: ivory;
}

.ehr_list-372e0a1 label {
width: 150px;
align-items: center;
}
2 changes: 2 additions & 0 deletions leptos/rust-toolchain.toml
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
[toolchain]
channel = "stable"
targets = ["wasm32-unknwon-unknown"]
profile = "minimal"
5 changes: 5 additions & 0 deletions leptos/src/_app.module.scss
Original file line number Diff line number Diff line change
Expand Up @@ -46,3 +46,8 @@ main {
max-height: 100px;
background-color: ivory;
}

.ehr_list label {
width: 150px;
align-items: center;
}
2 changes: 2 additions & 0 deletions leptos/src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ pub fn App() -> impl IntoView {
<main>
<Routes fallback=|| "Page not found.">
<Route path=path!("/") view=Home />
<Route path=path!("/form") view=Form />
<Route path=path!("/qr") view=QrScanner />
<Route path=path!("/audio") view=AudioStream />
</Routes>
Expand All @@ -52,6 +53,7 @@ fn AppSpinner() -> impl IntoView {
let loading = store.loading();

view! {
// Show spinner when global `loading` is set to true.
<Show when=move || loading.get()>
<Spinner />
</Show>
Expand Down
30 changes: 15 additions & 15 deletions leptos/src/components/audio.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,19 +45,21 @@ pub fn AudioStream() -> impl IntoView {
}) as Box<dyn FnMut(JsValue)>);

Effect::new(move |_| {
node.get().map(|v| match stream.get() {
Some(Ok(s)) => {
tracing::info!("Setting stream {s:?} to src...");
v.set_src_object(Some(&s));
let recorder = MediaRecorder::new_with_media_stream(&s).unwrap();
recorder.set_ondataavailable(Some(on_data_available.as_ref().unchecked_ref()));
recorder.start_with_time_slice(500).unwrap();
if let Some(v) = node.get() {
match stream.get() {
Some(Ok(s)) => {
tracing::info!("Setting stream {s:?} to src...");
v.set_src_object(Some(&s));
let recorder = MediaRecorder::new_with_media_stream(&s).unwrap();
recorder.set_ondataavailable(Some(on_data_available.as_ref().unchecked_ref()));
recorder.start_with_time_slice(500).unwrap();
}
Some(Err(e)) => tracing::error!("Failed to get media stream: {e:?}"),
None => tracing::debug!("No stream yet"),
}
Some(Err(e)) => tracing::error!("Failed to get media stream: {e:?}"),
None => tracing::debug!("No stream yet"),
});
}
});

// start/stop recording
let _effect = Effect::watch(
move || start_rec.get(),
Expand Down Expand Up @@ -103,12 +105,10 @@ pub fn AudioStream() -> impl IntoView {
view! {
<Space vertical=true>
// Eventually I was to draw something related to audio stream here.
<canvas node_ref=canvas_node class=styles::canvas />
<canvas node_ref=canvas_node class=styles::canvas />
<audio node_ref=node controls />
<Switch checked=start_rec label="Start Record" />
<div>
"Record and plot every half second of data"
</div>
<div>"Record and plot every half second of data"</div>
</Space>
}
}
65 changes: 65 additions & 0 deletions leptos/src/components/form.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
//! A sample forms

use strum::IntoEnumIterator;

use codee::string::JsonSerdeCodec;
use leptos::prelude::*;
use leptos_qr_scanner::Scan;
use leptos_use::storage::use_local_storage;
use reactive_stores::Store;
use thaw::*;

use crate::components::*;
use crate::css::styles;
use crate::storage::KeyVal;
use crate::storage::{GlobalState, GlobalStateStoreFields};

#[derive(Debug, strum::EnumIter, strum::Display)]
enum Gender {
Male,
Female,
Other,
}

#[component]
pub fn Form() -> impl IntoView {
let storage_key = RwSignal::new("".to_string());

let (state, set_state, _) = use_local_storage::<KeyVal, JsonSerdeCodec>(storage_key);

let upload_patient_consent_form = move |file_list: FileList| {
let len = file_list.length();
for i in 0..len {
if let Some(file) = file_list.get(i) {
tracing::info!("File to upload: {}", file.name());
}
}
};

view! {
<h5>"Form"</h5>
<Space vertical=true class=styles::ehr_list>

// Everything starts with this key
<ListItem label="Code".to_string()>
<input bind:value=storage_key />
</ListItem>

// Patient
<InputWithLabel key="phone".to_string() state set_state></InputWithLabel>
<InputWithLabel key="name".to_string() state set_state></InputWithLabel>
<SelectWithLabel
key="gender".to_string()
options=Gender::iter().map(|x| x.to_string()).collect()
state
set_state
></SelectWithLabel>
<InputWithLabel key="extra".to_string() state set_state></InputWithLabel>

<Upload custom_request=upload_patient_consent_form>
<UploadDragger>"Drag file here"</UploadDragger>
</Upload>

</Space>
}
}
79 changes: 79 additions & 0 deletions leptos/src/components/input.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
//! Input component.

use crate::storage::KeyVal;
use itertools::Itertools;
use leptos::prelude::*;
use thaw::*;

#[component]
pub fn InputWithLabel(
key: String,
state: Signal<KeyVal>,
set_state: WriteSignal<KeyVal>,
) -> impl IntoView {
let label = key.split("_").join(" ");
let key1 = key.to_string();

view! {
<Flex>
<Label>{label}</Label>
// Could not get thaw::Input to change when value in parent changes.
<input
prop:value=move || {
state.get().0.get(&key1).map(|x| x.to_string()).unwrap_or_default()
}
on:input=move |e| {
set_state
.update(|s| {
s.0.insert(key.to_string(), event_target_value(&e));
})
}
/>
</Flex>
}
}

#[component]
pub fn SelectWithLabel(
key: String,
options: Vec<String>,
state: Signal<KeyVal>,
set_state: WriteSignal<KeyVal>,
) -> impl IntoView {

let label = key.split("_").join(" ");
let key1 = key.to_string();

view! {
<Flex>
<Label>{label}</Label>
<select
prop:value=move || {
state.get().0.get(&key1).map(|x| x.to_string()).unwrap_or_default()
}
on:input=move |e| {
set_state
.update(|s| {
s.0.insert(key.to_string(), event_target_value(&e));
})
}
>
{options.iter().map(|v| view! { <option>{v.to_string()}</option> }).collect_view()}

</select>
</Flex>
}
}

#[component]
pub fn ListItem(
label: String,
children: Children,
) -> impl IntoView {
view! {
<Flex>
<Label>{label}</Label>
{children()}
</Flex>
}
}
6 changes: 6 additions & 0 deletions leptos/src/components/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,9 @@ pub use qr::*;

pub(crate) mod audio;
pub use audio::*;

pub(crate) mod form;
pub use form::*;

pub(crate) mod input;
pub use input::*;
16 changes: 12 additions & 4 deletions leptos/src/storage.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,26 @@

#![allow(dead_code)]

use std::collections::HashMap;
use reactive_stores::Store;

// Local store to store data before we
#[derive(serde::Serialize, serde::Deserialize, Clone, PartialEq, Default)]
pub(crate) struct LocalStorage {
pub trainer: String,
pub(crate) struct EhrSection1 {
pub sample_code: String,
pub patient_name: String,
}

impl LocalStorage {
// Local store to store data before we
#[derive(serde::Serialize, serde::Deserialize, Clone, PartialEq, Default)]
pub(crate) struct KeyVal(pub HashMap<String, String>);

impl EhrSection1 {
/// Key to use in local storage
pub const KEY: &'static str = "login-state";
pub const KEY: &'static str = "ehr-section-1";
}


/// Global state to be shared across components.
#[derive(Clone, Debug, Default, Store)]
pub struct GlobalState {
Expand Down
Loading