Add Modal component

This commit is contained in:
2024-03-30 13:46:53 +01:00
parent 5c91df206c
commit cf9737fc76
6 changed files with 342 additions and 0 deletions

231
src/components/modal.rs Normal file
View File

@@ -0,0 +1,231 @@
use std::collections::HashMap;
use std::fmt;
use std::sync::OnceLock;
use dioxus::prelude::*;
use rand::distributions::{Alphanumeric, DistString};
use tracing::{error, warn};
use super::button::{ErrorButton, SuccessButton, WarningButton};
include!(concat!(env!("OUT_DIR"), "/style_vars.rs"));
use style::{COLOR_CRITICAL_100, COLOR_SUCCESS_100, COLOR_WARNING_100};
turf::style_sheet!("src/components/modal.scss");
#[derive(Clone, Eq, PartialEq, Hash)]
pub enum Severity {
Ok,
Warning,
Critical,
}
impl fmt::Display for Severity {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let repr = match self {
Self::Ok => "Severity::Ok",
Self::Warning => "Severity::Warning",
Self::Critical => "Severity::Critical",
};
write!(f, "{repr}")
}
}
struct DicebearConfig<'a> {
gesture: &'a str,
color: &'a str,
browns: Vec<u32>,
eyes: Vec<u32>,
lips: Vec<u32>,
}
#[derive(Props)]
pub struct ModalProps<'a> {
pub severity: Severity,
#[props(optional)]
pub title: Option<&'a str>,
pub children: Element<'a>,
#[props(optional)]
pub on_confirm: Option<EventHandler<'a, MouseEvent>>,
}
fn dicebear_variants() -> &'static HashMap<Severity, DicebearConfig<'static>> {
static HASHMAP: OnceLock<HashMap<Severity, DicebearConfig>> = OnceLock::new();
HASHMAP.get_or_init(|| {
let mut variants = HashMap::new();
variants.insert(
Severity::Critical,
DicebearConfig {
gesture: "wavePointLongArms",
color: COLOR_CRITICAL_100,
browns: vec![2, 6, 11, 13],
eyes: vec![2, 4],
lips: vec![1, 2, 7, 11, 19, 20, 24, 27],
},
);
variants.insert(
Severity::Warning,
DicebearConfig {
gesture: "pointLongArm",
color: COLOR_WARNING_100,
browns: vec![2, 5, 10, 13],
eyes: vec![1, 3],
lips: vec![1, 2, 4, 8, 10, 13, 18, 21, 29],
},
);
variants.insert(
Severity::Ok,
DicebearConfig {
gesture: "okLongArm",
color: COLOR_SUCCESS_100,
browns: vec![1, 3, 4, 7, 8, 9, 12],
eyes: vec![5],
lips: vec![3, 5, 9, 14, 17, 22, 23, 25, 30],
},
);
variants
})
}
fn render_dicebear_variants(values: &[u32]) -> String {
values
.iter()
.map(|l| format!("variant{:02}", l))
.collect::<Vec<String>>()
.join(",")
}
async fn generate_random_figure(url: &String, severity: Severity) -> Option<String> {
let mut res: Option<String> = None;
let config = match dicebear_variants().get(&severity) {
Some(config) => config,
None => {
error!("No dicebear configuration found for \"{severity}\"");
return res;
}
};
let seed = Alphanumeric.sample_string(&mut rand::thread_rng(), 16);
let color = config.color;
let gesture = config.gesture;
let rendered_browns = render_dicebear_variants(&config.browns);
let rendered_eyes = render_dicebear_variants(&config.eyes);
let rendered_lips = render_dicebear_variants(&config.lips);
let req = format!(
"https://{url}/7.x/notionists/svg?\
seed={seed}&\
backgroundColor={color}&\
gestureProbability=100&gesture={gesture}&\
browsProbability=100&brows={rendered_browns}&\
eyesProbability=100&eyes={rendered_eyes}&\
lipsProbability=100&lips={rendered_lips}"
);
match reqwest::get(req).await {
Ok(result) => {
match result.text().await {
Ok(svg) => {
res = Some(svg);
}
Err(err) => {
error!("Error during placeholder loading: {}", err);
}
};
}
Err(err) => {
error!("Error during placeholder loading: {}", err);
}
};
res
}
#[component]
pub fn Modal<'a>(cx: Scope<'a, ModalProps<'a>>) -> Element<'a> {
// TODO: Use configuration file
let url = "dicebear.tools.adrien.run".to_string();
let severity = cx.props.severity.clone();
let random_figure_future = use_future(cx, &url, |url| async move {
generate_random_figure(&url, severity).await
});
let figure = match random_figure_future.value() {
Some(Some(svg)) => Some(rsx! {
div {
class: ClassName::MODAL_CONTENT_ICON_PLACEHOLDER,
dangerous_inner_html: svg.as_str(),
}
}),
Some(None) => {
warn!("No profile image set or generated, display the placeholder");
let path = match cx.props.severity {
Severity::Ok => "./images/modal-default-ok-icon.svg",
Severity::Warning => "./images/modal-default-warning-icon.svg",
Severity::Critical => "./images/modal-default-critical-icon.svg",
};
Some(rsx! {
div {
class: ClassName::MODAL_CONTENT_ICON_PLACEHOLDER,
img {
src: path
}
}
})
}
None => None,
};
let button_class = match cx.props.severity {
Severity::Ok => SuccessButton,
Severity::Warning => WarningButton,
Severity::Critical => ErrorButton,
};
if figure.is_none() {
return None;
}
cx.render(rsx! {
style { STYLE_SHEET },
div {
class: ClassName::MODAL,
div {
class: ClassName::MODAL_CONTENT,
div {
class: ClassName::MODAL_CONTENT_ICON,
{figure}
},
div {
class: ClassName::MODAL_CONTENT_TITLE,
cx.props.title,
},
div {
class: ClassName::MODAL_CONTENT_MSG,
&cx.props.children,
},
div {
class: ClassName::MODAL_CONTENT_BUTTONS,
button_class {
onclick: move |evt| {
if let Some(cb) = &cx.props.on_confirm {
cb.call(evt);
}
},
},
},
},
},
})
}