✨ Add Modal component
This commit is contained in:
1
images/modal-default-critical-icon.svg
Normal file
1
images/modal-default-critical-icon.svg
Normal file
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 20 KiB |
1
images/modal-default-ok-icon.svg
Normal file
1
images/modal-default-ok-icon.svg
Normal file
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 17 KiB |
1
images/modal-default-warning-icon.svg
Normal file
1
images/modal-default-warning-icon.svg
Normal file
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 17 KiB |
@@ -7,6 +7,7 @@ pub mod icons;
|
||||
pub mod loading;
|
||||
pub mod login;
|
||||
pub mod main_window;
|
||||
pub mod modal;
|
||||
pub mod spinner;
|
||||
pub mod text_input;
|
||||
pub mod wallpaper;
|
||||
|
231
src/components/modal.rs
Normal file
231
src/components/modal.rs
Normal 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);
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
107
src/components/modal.scss
Normal file
107
src/components/modal.scss
Normal file
@@ -0,0 +1,107 @@
|
||||
@import "../_base.scss"
|
||||
|
||||
$modal-min-height: 15vh;
|
||||
$modal-max-height: 55vh;
|
||||
|
||||
.modal {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
position: relative;
|
||||
top: -100%;
|
||||
margin-bottom: -100%;
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
|
||||
&__content {
|
||||
$content-width: 70vw;
|
||||
$title-height: 3vh;
|
||||
$space-height: 1vh;
|
||||
$buttons-height: 5vh;
|
||||
$icon-width: 10vw;
|
||||
$msg-max-width: 50vw;
|
||||
|
||||
$msg-min-height: calc($modal-min-height - $title-height - $space-height - $buttons-height);
|
||||
$msg-max-height: calc($modal-max-height - $title-height - $space-height - $buttons-height);
|
||||
|
||||
min-height: $modal-min-height;
|
||||
max-height: $modal-max-height;
|
||||
|
||||
width: min-content;
|
||||
|
||||
display: grid;
|
||||
grid-template-columns: $icon-width 1vw minmax(max-content, $msg-max-width);
|
||||
grid-template-rows: $title-height $space-height max-content $space-height $buttons-height;
|
||||
grid-template-areas:
|
||||
"icon . title"
|
||||
"icon . ."
|
||||
"icon . msg"
|
||||
". . ."
|
||||
"buttons buttons buttons"
|
||||
;
|
||||
|
||||
padding: 2.5%;
|
||||
|
||||
background: get-color(greyscale, 0);
|
||||
|
||||
border: $border-normal;
|
||||
border-radius: $border-radius;
|
||||
border-color: get-color(greyscale, 90);
|
||||
|
||||
&__icon {
|
||||
grid-area: icon;
|
||||
|
||||
aspect-ratio: 1;
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
overflow: hidden;
|
||||
|
||||
border: $border-normal;
|
||||
border-radius: $border-radius;
|
||||
|
||||
&__placeholder {
|
||||
width: calc(100% + (2 * $border-normal-width));
|
||||
height: calc(100% - (2 * $border-normal-width));
|
||||
}
|
||||
}
|
||||
|
||||
&__title {
|
||||
grid-area: title;
|
||||
|
||||
width: max-content;
|
||||
|
||||
font-size: calc($title-height - 0.5vh);
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
&__msg {
|
||||
grid-area: msg;
|
||||
|
||||
height: fit-content;
|
||||
|
||||
min-height: $msg-min-height;
|
||||
max-height: $msg-max-height;
|
||||
max-width: $msg-max-width;
|
||||
|
||||
overflow: scroll;
|
||||
|
||||
font-size: 1.5vh;
|
||||
color: get-color(greyscale, 80);
|
||||
}
|
||||
|
||||
&__buttons {
|
||||
grid-area: buttons;
|
||||
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user