From 99e1ce3cd588e7b33359caf14390af81ee356c5e Mon Sep 17 00:00:00 2001 From: Mendy Berger <12537668+MendyBerger@users.noreply.github.com> Date: Tue, 19 Mar 2024 10:43:31 -0400 Subject: [PATCH] feat(fe): Added QR codes --- frontend/apps/Cargo.lock | 127 ++++++++++++++++++ frontend/apps/Cargo.toml | 1 + frontend/apps/crates/components/Cargo.toml | 3 + frontend/apps/crates/components/src/lib.rs | 2 + .../components/src/qr_dialog/actions.rs | 11 ++ .../components/src/qr_dialog/callbacks.rs | 11 ++ .../crates/components/src/qr_dialog/dom.rs | 50 +++++++ .../crates/components/src/qr_dialog/mod.rs | 8 ++ .../crates/components/src/qr_dialog/state.rs | 45 +++++++ .../components/src/qr_dialog/styles.css | 16 +++ .../components/src/share_asset/actions.rs | 20 +++ .../crates/components/src/share_asset/dom.rs | 22 ++- .../components/src/share_asset/state.rs | 4 + .../src/codes/jig_code_sessions/actions.rs | 55 ++++---- .../src/codes/jig_code_sessions/dom.rs | 31 ++++- .../src/codes/jig_code_sessions/state.rs | 3 + .../src/codes/jig_code_sessions/styles.css | 6 +- frontend/apps/crates/utils/src/js_wrappers.rs | 15 +++ .../elements/src/core/share-jig/students.ts | 1 + 19 files changed, 394 insertions(+), 37 deletions(-) create mode 100644 frontend/apps/crates/components/src/qr_dialog/actions.rs create mode 100644 frontend/apps/crates/components/src/qr_dialog/callbacks.rs create mode 100644 frontend/apps/crates/components/src/qr_dialog/dom.rs create mode 100644 frontend/apps/crates/components/src/qr_dialog/mod.rs create mode 100644 frontend/apps/crates/components/src/qr_dialog/state.rs create mode 100644 frontend/apps/crates/components/src/qr_dialog/styles.css diff --git a/frontend/apps/Cargo.lock b/frontend/apps/Cargo.lock index 2cf676a69d..37b3da26c2 100644 --- a/frontend/apps/Cargo.lock +++ b/frontend/apps/Cargo.lock @@ -2,6 +2,12 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "adler" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" + [[package]] name = "aho-corasick" version = "0.7.19" @@ -1306,6 +1312,12 @@ version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb" +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + [[package]] name = "bumpalo" version = "3.11.1" @@ -1318,6 +1330,12 @@ version = "1.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "aaa3a8d9a1ca92e282c96a32d6511b695d7d994d1d102ba85d279f9b2756947f" +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + [[package]] name = "bytes" version = "1.2.1" @@ -1391,6 +1409,12 @@ dependencies = [ "unicode-width", ] +[[package]] +name = "color_quant" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" + [[package]] name = "components" version = "0.1.0" @@ -1414,6 +1438,7 @@ dependencies = [ "num-derive", "num-traits", "once_cell", + "qrcode-generator", "rand", "regex", "rgb", @@ -1486,6 +1511,15 @@ version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc" +[[package]] +name = "crc32fast" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3855a8a784b474f333699ef2bbca9db2c4a1f6d9088a90a2d25b1eb53111eaa" +dependencies = [ + "cfg-if 1.0.0", +] + [[package]] name = "csv" version = "1.3.0" @@ -1759,6 +1793,25 @@ dependencies = [ "regex", ] +[[package]] +name = "fdeflate" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f9bfee30e4dedf0ab8b422f03af778d9612b63f502710fc500a334ebe2de645" +dependencies = [ + "simd-adler32", +] + +[[package]] +name = "flate2" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46303f565772937ffe1d394a4fac6f411c6013172fadde9dcdb1e147a086940e" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + [[package]] name = "fnv" version = "1.0.7" @@ -2009,6 +2062,15 @@ version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" +[[package]] +name = "html-escape" +version = "0.2.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d1ad449764d627e22bfd7cd5e8868264fc9236e07c752972b4080cd351cb476" +dependencies = [ + "utf8-width", +] + [[package]] name = "htmlescape" version = "0.3.1" @@ -2066,6 +2128,19 @@ dependencies = [ "unicode-normalization", ] +[[package]] +name = "image" +version = "0.24.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5690139d2f55868e080017335e4b94cb7414274c74f1669c84fb5feba2c9f69d" +dependencies = [ + "bytemuck", + "byteorder", + "color_quant", + "num-traits", + "png", +] + [[package]] name = "itertools" version = "0.10.5" @@ -2181,6 +2256,16 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8452105ba047068f40ff7093dd1d9da90898e63dd61736462e9cdda6a90ad3c3" +[[package]] +name = "miniz_oxide" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d811f3e15f28568be3407c8e7fdb6514c1cda3cb30683f15b6a1a1dc4ea14a7" +dependencies = [ + "adler", + "simd-adler32", +] + [[package]] name = "num-derive" version = "0.3.3" @@ -2303,6 +2388,19 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +[[package]] +name = "png" +version = "0.17.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06e4b0d3d1312775e782c86c91a111aa1f910cbb65e1337f9975b5f9a554b5e1" +dependencies = [ + "bitflags", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide", +] + [[package]] name = "ppv-lite86" version = "0.2.17" @@ -2318,6 +2416,23 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "qrcode-generator" +version = "4.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d06cb9646c7a14096231a2474d7f21e5e8c13de090c68d13bde6157cfe7f159" +dependencies = [ + "html-escape", + "image", + "qrcodegen", +] + +[[package]] +name = "qrcodegen" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4339fc7a1021c9c1621d87f5e3505f2805c8c105420ba2f2a4df86814590c142" + [[package]] name = "quick-error" version = "2.0.1" @@ -2571,6 +2686,12 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "simd-adler32" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" + [[package]] name = "siphasher" version = "0.3.10" @@ -2770,6 +2891,12 @@ dependencies = [ "serde", ] +[[package]] +name = "utf8-width" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86bd8d4e895da8537e5315b8254664e6b769c4ff3db18321b297a1e7004392e3" + [[package]] name = "utils" version = "0.1.0" diff --git a/frontend/apps/Cargo.toml b/frontend/apps/Cargo.toml index 9d219339d0..788ea0df3e 100644 --- a/frontend/apps/Cargo.toml +++ b/frontend/apps/Cargo.toml @@ -101,6 +101,7 @@ const_format = "0.2.5" num-traits = "0.2" num-derive = "0.3" csv = "1.3" +qrcode-generator = "4.1.9" web-sys = { version = "0.3.55", features = [ 'Url', 'Request', diff --git a/frontend/apps/crates/components/Cargo.toml b/frontend/apps/crates/components/Cargo.toml index 33d7cb5c8e..46cb2a598d 100644 --- a/frontend/apps/crates/components/Cargo.toml +++ b/frontend/apps/crates/components/Cargo.toml @@ -45,6 +45,7 @@ rgb = { workspace = true } regex = { workspace = true } num-traits = { workspace = true } num-derive = { workspace = true } +qrcode-generator = { workspace = true } [features] quiet = ["utils/quiet"] @@ -88,6 +89,7 @@ hebrew_buttons = [] page_header = [] page_footer = [] pdf = [] +qr_dialog = [] share_jig = [] player_popup = [] box_outline = [] @@ -135,6 +137,7 @@ all = [ "page_header", "page_footer", "pdf", + "qr_dialog", "share_jig", "player_popup", "box_outline", diff --git a/frontend/apps/crates/components/src/lib.rs b/frontend/apps/crates/components/src/lib.rs index 973a16dde3..5b31e06858 100644 --- a/frontend/apps/crates/components/src/lib.rs +++ b/frontend/apps/crates/components/src/lib.rs @@ -59,6 +59,8 @@ pub mod page_header; pub mod pdf; #[cfg(feature = "player_popup")] pub mod player_popup; +#[cfg(feature = "qr_dialog")] +pub mod qr_dialog; #[cfg(feature = "share_jig")] pub mod share_asset; #[cfg(feature = "stickers")] diff --git a/frontend/apps/crates/components/src/qr_dialog/actions.rs b/frontend/apps/crates/components/src/qr_dialog/actions.rs new file mode 100644 index 0000000000..f227ce0d09 --- /dev/null +++ b/frontend/apps/crates/components/src/qr_dialog/actions.rs @@ -0,0 +1,11 @@ +use std::rc::Rc; + +use utils::js_wrappers::download_url; + +use super::QrDialog; + +impl QrDialog { + pub fn download(self: &Rc) { + download_url(&self.file_label, &self.url) + } +} diff --git a/frontend/apps/crates/components/src/qr_dialog/callbacks.rs b/frontend/apps/crates/components/src/qr_dialog/callbacks.rs new file mode 100644 index 0000000000..735e3de56b --- /dev/null +++ b/frontend/apps/crates/components/src/qr_dialog/callbacks.rs @@ -0,0 +1,11 @@ +pub struct QrDialogCallbacks { + pub on_close: Box, +} + +impl QrDialogCallbacks { + pub fn new(on_close: impl Fn() + 'static) -> Self { + Self { + on_close: Box::new(on_close), + } + } +} diff --git a/frontend/apps/crates/components/src/qr_dialog/dom.rs b/frontend/apps/crates/components/src/qr_dialog/dom.rs new file mode 100644 index 0000000000..9a3185647f --- /dev/null +++ b/frontend/apps/crates/components/src/qr_dialog/dom.rs @@ -0,0 +1,50 @@ +use std::rc::Rc; + +use dominator::{clone, DomBuilder, EventOptions}; +use utils::{component::Component, dialog, events}; +use web_sys::ShadowRoot; + +use super::QrDialog; + +impl Component for Rc { + fn styles() -> &'static str { + include_str!("./styles.css") + } + + fn dom(&self, dom: DomBuilder) -> DomBuilder { + let state = self; + dom.child(dialog! { + .class("qr-dialog") + .event_with_options(&EventOptions::bubbles(), clone!(state => move |e: events::Click| { + e.stop_propagation(); + (state.callbacks.on_close)(); + })) + .child(html!("div", { + .class("body") + .event_with_options(&EventOptions::bubbles(), |e: events::Click| { + e.stop_propagation(); + }) + .child(html!("fa-button", { + .class("close") + .prop("icon", "fa-regular fa-xmark") + .event(clone!(state => move |_: events::Click| { + (state.callbacks.on_close)(); + })) + })) + .child(html!("img", { + .style("max-height", "5cm") + .style("max-width", "5cm") + .prop("src", &state.url) + })) + .child(html!("fa-button", { + .class("download") + .prop("icon", "fa-solid fa-square-down") + .prop("title", "Download") + .event(clone!(state => move |_: events::Click| { + state.download(); + })) + })) + })) + }) + } +} diff --git a/frontend/apps/crates/components/src/qr_dialog/mod.rs b/frontend/apps/crates/components/src/qr_dialog/mod.rs new file mode 100644 index 0000000000..1ed633702e --- /dev/null +++ b/frontend/apps/crates/components/src/qr_dialog/mod.rs @@ -0,0 +1,8 @@ +mod actions; +mod callbacks; +mod dom; +mod state; + +pub use callbacks::*; +pub use dom::*; +pub use state::*; diff --git a/frontend/apps/crates/components/src/qr_dialog/state.rs b/frontend/apps/crates/components/src/qr_dialog/state.rs new file mode 100644 index 0000000000..a250d974e5 --- /dev/null +++ b/frontend/apps/crates/components/src/qr_dialog/state.rs @@ -0,0 +1,45 @@ +use std::rc::Rc; + +use qrcode_generator::QrCodeEcc; +use utils::{routes::Route, unwrap::UnwrapJiExt}; +use wasm_bindgen::JsValue; + +use super::QrDialogCallbacks; + +pub struct QrDialog { + pub url: String, + pub file_label: String, + pub callbacks: QrDialogCallbacks, +} + +impl QrDialog { + pub fn new(route: Route, file_label: String, callbacks: QrDialogCallbacks) -> Rc { + let url = qr_core_file_from_route(route); + Rc::new(Self { + url, + file_label, + callbacks, + }) + } +} + +fn file_to_object_url(filetype: &str, data: &str) -> String { + let data = JsValue::from_str(data); + let blob = web_sys::Blob::new_with_str_sequence_and_options( + &js_sys::Array::from_iter(vec![data]), + web_sys::BlobPropertyBag::new().type_(filetype), + ) + .unwrap_ji(); + let url = web_sys::Url::create_object_url_with_blob(&blob).unwrap_ji(); + url +} + +pub fn qr_core_file_from_route(route: Route) -> String { + let result: String = + qrcode_generator::to_svg_to_string(route.to_string(), QrCodeEcc::High, 200, None::<&str>) + .unwrap(); + + let url = file_to_object_url("image/svg+xml", &result); + + url +} diff --git a/frontend/apps/crates/components/src/qr_dialog/styles.css b/frontend/apps/crates/components/src/qr_dialog/styles.css new file mode 100644 index 0000000000..612e7f08f7 --- /dev/null +++ b/frontend/apps/crates/components/src/qr_dialog/styles.css @@ -0,0 +1,16 @@ +.body { + display: grid; + justify-items: end; + gap: 10px; + padding: 20px; +} +fa-button { + color: grey; + transition: color 0.2s; +} +fa-button:hover { + color: #585858; +} +.download { + font-size: 20px; +} diff --git a/frontend/apps/crates/components/src/share_asset/actions.rs b/frontend/apps/crates/components/src/share_asset/actions.rs index fd1f2423d1..a8535b0acc 100644 --- a/frontend/apps/crates/components/src/share_asset/actions.rs +++ b/frontend/apps/crates/components/src/share_asset/actions.rs @@ -9,6 +9,8 @@ use shared::{ }; use utils::prelude::*; +use crate::qr_dialog::{QrDialog, QrDialogCallbacks}; + use super::state::ShareAsset; const COPIED_TIMEOUT: u32 = 3_000; @@ -46,4 +48,22 @@ impl ShareAsset { ); timeout.forget(); } + + pub fn show_qr_code(self: &Rc) { + let state = self; + let code = self + .student_code + .get_cloned() + .unwrap_or_default() + .to_string(); + let qr_dialog = QrDialog::new( + Route::Kids(KidsRoute::StudentCode(Some(code.clone()))), + code, + QrDialogCallbacks::new(clone!(state => move || { + state.qr_dialog.set(None); + })), + ); + self.qr_dialog.set(Some(qr_dialog)); + self.active_popup.set(None); + } } diff --git a/frontend/apps/crates/components/src/share_asset/dom.rs b/frontend/apps/crates/components/src/share_asset/dom.rs index 9f8f5f37f8..cf4b4ec337 100644 --- a/frontend/apps/crates/components/src/share_asset/dom.rs +++ b/frontend/apps/crates/components/src/share_asset/dom.rs @@ -9,7 +9,9 @@ use shared::{ domain::{asset::Asset, jig::TextDirection}, }; use utils::{ - clipboard, events, paywall, + clipboard, + component::Component, + events, paywall, prelude::SETTINGS, routes::{KidsRoute, Route}, unwrap::UnwrapJiExt, @@ -61,7 +63,7 @@ impl ShareAsset { .child(anchor) })) .apply(OverlayHandle::lifecycle( - move || { + clone!(state => move || { html!("overlay-content", { .prop("target", &elem) .prop("contentAnchor", "oppositeH") @@ -86,9 +88,14 @@ impl ShareAsset { }))) })) }) - } + }) )) }) + .child_signal(state.qr_dialog.signal_ref(move |qr_dialog| { + qr_dialog.as_ref().map(move |qr_dialog| { + qr_dialog.render() + }) + })) }) } @@ -338,6 +345,15 @@ impl ShareAsset { ShareAsset::set_copied_mutable(state.copied_student_code.clone()); })) }), + html!("button-rect", { + .prop("slot", "qr") + .prop("color", "blue") + .prop("kind", "text") + .text("Show QR code") + .event(clone!(state => move |_: events::Click| { + state.show_qr_code(); + })) + }), ]) }) } diff --git a/frontend/apps/crates/components/src/share_asset/state.rs b/frontend/apps/crates/components/src/share_asset/state.rs index 0556eb45c1..3dc787250b 100644 --- a/frontend/apps/crates/components/src/share_asset/state.rs +++ b/frontend/apps/crates/components/src/share_asset/state.rs @@ -11,6 +11,8 @@ use utils::routes::{AssetRoute, Route}; use utils::prelude::*; +use crate::qr_dialog::QrDialog; + pub struct ShareAsset { pub active_popup: Mutable>, pub student_code: Mutable>, @@ -20,6 +22,7 @@ pub struct ShareAsset { pub link_copied: Mutable, pub copied_student_url: Mutable, pub copied_student_code: Mutable, + pub qr_dialog: Mutable>>, // play settings pub direction: Mutable, pub scoring: Mutable, @@ -44,6 +47,7 @@ impl ShareAsset { link_copied: Mutable::new(false), copied_student_url: Mutable::new(false), copied_student_code: Mutable::new(false), + qr_dialog: Mutable::new(None), direction: Mutable::new(direction), scoring: Mutable::new(scoring), }) diff --git a/frontend/apps/crates/entry/classroom/src/codes/jig_code_sessions/actions.rs b/frontend/apps/crates/entry/classroom/src/codes/jig_code_sessions/actions.rs index 1f89483726..4009ae3719 100644 --- a/frontend/apps/crates/entry/classroom/src/codes/jig_code_sessions/actions.rs +++ b/frontend/apps/crates/entry/classroom/src/codes/jig_code_sessions/actions.rs @@ -1,5 +1,6 @@ use std::{collections::HashMap, rc::Rc}; +use components::qr_dialog::{QrDialog, QrDialogCallbacks}; use dominator::clone; use futures::{future::try_join_all, join}; use shared::{ @@ -16,9 +17,14 @@ use shared::{ }, }; use utils::{ - bail_on_err, date_formatters, error_ext::ErrorExt, prelude::ApiEndpointExt, unwrap::UnwrapJiExt, + bail_on_err, date_formatters, + error_ext::ErrorExt, + js_wrappers::download_url, + prelude::ApiEndpointExt, + routes::{KidsRoute, Route}, + unwrap::UnwrapJiExt, }; -use wasm_bindgen::{JsCast, JsValue}; +use wasm_bindgen::JsValue; use wasm_bindgen_futures::spawn_local; use super::{CodeSessions, JigWithModules}; @@ -61,11 +67,24 @@ impl CodeSessions { JigCodeSessionsPath(self.code), None, ) - .await; + .await + .toast_on_err(); let res = bail_on_err!(res); self.infos.lock_mut().extend(res.sessions); } + pub fn show_qr_code(self: &Rc) { + let state = self; + let qr_dialog = QrDialog::new( + Route::Kids(KidsRoute::StudentCode(Some(self.code.to_string()))), + self.code.to_string(), + QrDialogCallbacks::new(clone!(state => move || { + state.qr_dialog.set(None); + })), + ); + self.qr_dialog.set(Some(qr_dialog)); + } + fn generate_csv_string(&self) -> String { let mut wtr = csv::WriterBuilder::new().from_writer(vec![]); if let Some(jig) = self.jig.lock_ref().as_ref() { @@ -131,27 +150,13 @@ impl CodeSessions { pub fn export_sessions(&self) { let data = self.generate_csv_string(); - download_file(&self.code.to_string(), &data); - } -} - -fn download_file(filename: &str, data: &str) { - let data = JsValue::from_str(data); - let blob = web_sys::Blob::new_with_str_sequence_and_options( - &js_sys::Array::from_iter(vec![data]), - web_sys::BlobPropertyBag::new().type_("text/csv"), - ) - .unwrap_ji(); - let url = web_sys::Url::create_object_url_with_blob(&blob).unwrap_ji(); - let a = web_sys::window() - .unwrap_ji() - .document() - .unwrap_ji() - .create_element("a") - .unwrap_ji() - .dyn_into::() + let data = JsValue::from_str(&data); + let blob = web_sys::Blob::new_with_str_sequence_and_options( + &js_sys::Array::from_iter(vec![data]), + web_sys::BlobPropertyBag::new().type_("text/csv"), + ) .unwrap_ji(); - a.set_href(&url); - a.set_download(&filename); - a.click(); + let url = web_sys::Url::create_object_url_with_blob(&blob).unwrap_ji(); + download_url(&self.code.to_string(), &url); + } } diff --git a/frontend/apps/crates/entry/classroom/src/codes/jig_code_sessions/dom.rs b/frontend/apps/crates/entry/classroom/src/codes/jig_code_sessions/dom.rs index 1334aef443..d701bbfd35 100644 --- a/frontend/apps/crates/entry/classroom/src/codes/jig_code_sessions/dom.rs +++ b/frontend/apps/crates/entry/classroom/src/codes/jig_code_sessions/dom.rs @@ -93,13 +93,25 @@ impl Component for Rc { })) })) })) - .child(html!("button-rect", { - .class("export-button") - .prop("color", "blue") - .prop("kind", "text") - .text("Export CSV") - .event(clone!(state => move |_: events::Click| { - state.export_sessions(); + .child(html!("div", { + .class("export-and-qr") + .child(html!("button-rect", { + .class("qr-button") + .prop("color", "blue") + .prop("kind", "text") + .text("Show QR code") + .event(clone!(state => move |_: events::Click| { + state.show_qr_code(); + })) + })) + .child(html!("button-rect", { + .class("export-button") + .prop("color", "blue") + .prop("kind", "text") + .text("Export CSV") + .event(clone!(state => move |_: events::Click| { + state.export_sessions(); + })) })) })) })) @@ -134,6 +146,11 @@ impl Component for Rc { }) })), ) + .child_signal(state.qr_dialog.signal_ref(move |qr_dialog| { + qr_dialog.as_ref().map(move |qr_dialog| { + qr_dialog.render() + }) + })) } } diff --git a/frontend/apps/crates/entry/classroom/src/codes/jig_code_sessions/state.rs b/frontend/apps/crates/entry/classroom/src/codes/jig_code_sessions/state.rs index a5cf516b25..1dee566c14 100644 --- a/frontend/apps/crates/entry/classroom/src/codes/jig_code_sessions/state.rs +++ b/frontend/apps/crates/entry/classroom/src/codes/jig_code_sessions/state.rs @@ -1,5 +1,6 @@ use std::{collections::HashMap, rc::Rc}; +use components::qr_dialog::QrDialog; use futures_signals::signal::Mutable; use shared::domain::{ jig::{ @@ -15,6 +16,7 @@ pub struct CodeSessions { pub(super) jig: Mutable>, pub(super) infos: Mutable>, pub(super) preview_open: Mutable, + pub(super) qr_dialog: Mutable>>, } impl CodeSessions { @@ -25,6 +27,7 @@ impl CodeSessions { jig: Default::default(), infos: Default::default(), preview_open: Default::default(), + qr_dialog: Default::default(), }) } } diff --git a/frontend/apps/crates/entry/classroom/src/codes/jig_code_sessions/styles.css b/frontend/apps/crates/entry/classroom/src/codes/jig_code_sessions/styles.css index f3ec066b43..64c01d9ad2 100644 --- a/frontend/apps/crates/entry/classroom/src/codes/jig_code_sessions/styles.css +++ b/frontend/apps/crates/entry/classroom/src/codes/jig_code_sessions/styles.css @@ -27,7 +27,7 @@ header .jig-name { } header .preview-button { grid-row: 2; - justify-self: start; + justify-self: end; background-color: #ffffff; } header .code-link { @@ -52,9 +52,11 @@ header .copy:hover { header .copy:active { background-color: rgba(0, 0, 0, 0.1); } -header .export-button { +header .export-and-qr { grid-row: 3; grid-column: 3; + display: flex; + gap: 20px; } .table { overflow-x: auto; diff --git a/frontend/apps/crates/utils/src/js_wrappers.rs b/frontend/apps/crates/utils/src/js_wrappers.rs index fae3491468..314320482f 100644 --- a/frontend/apps/crates/utils/src/js_wrappers.rs +++ b/frontend/apps/crates/utils/src/js_wrappers.rs @@ -30,3 +30,18 @@ pub fn is_iframe() -> bool { let top = window.top().unwrap_ji().unwrap_ji(); window != top } + +/// Force a download from a url +pub fn download_url(filename: &str, url: &str) { + let a = web_sys::window() + .unwrap_ji() + .document() + .unwrap_ji() + .create_element("a") + .unwrap_ji() + .dyn_into::() + .unwrap_ji(); + a.set_href(&url); + a.set_download(&filename); + a.click(); +} diff --git a/frontend/elements/src/core/share-jig/students.ts b/frontend/elements/src/core/share-jig/students.ts index 601a3c0fce..cc2de8bc62 100644 --- a/frontend/elements/src/core/share-jig/students.ts +++ b/frontend/elements/src/core/share-jig/students.ts @@ -177,6 +177,7 @@ export class _ extends LitElement { + ` }