use leptos::{either::Either, prelude::*};
use thaw_components::OptionComp;
use thaw_utils::{class_list, mount_style};
#[component]
pub fn Avatar(
#[prop(optional, into)] class: MaybeProp<String>,
#[prop(optional, into)]
src: MaybeProp<String>,
#[prop(optional, into)]
name: MaybeProp<String>,
#[prop(optional, into)]
initials: MaybeProp<String>,
#[prop(optional, into)]
shape: Signal<AvatarShape>,
#[prop(optional, into)]
size: MaybeProp<u8>,
) -> impl IntoView {
mount_style("avatar", include_str!("./avatar.css"));
let style = move || {
let size = size.get()?;
let mut style = format!("width: {0}px; height: {0}px;", size);
if let Some(font_size) = match size {
0..=24 => Some(100),
25..=28 => Some(200),
29..=40 => None,
41..=56 => Some(400),
57..=96 => Some(500),
97..=128 => Some(600),
_ => Some(600),
} {
style.push_str(&format!("font-size: var(--fontSizeBase{});", font_size))
}
Some(style)
};
let image_hidden = RwSignal::new(false);
let is_show_default_icon = Memo::new(move |_| {
if name.with(|n| n.is_some()) {
false
} else if src.with(|s| s.is_some()) && !image_hidden.get() {
false
} else if initials.with(|i| i.is_some()) {
false
} else {
true
}
});
let on_load = move |_| {
image_hidden.maybe_update(|hidden| {
if *hidden {
*hidden = false;
true
} else {
true
}
});
};
let on_error = move |_| {
image_hidden.set(true);
};
view! {
<span
class=class_list![
"thaw-avatar",
move || format!("thaw-avatar--{}", shape.get().as_str()),
class
]
style=move || style()
role="img"
aria-label=move || name.get()
>
{move || {
let mut initials = initials.get();
if initials.is_none() {
if let Some(name) = name.get() {
initials = Some(initials_name(name));
}
}
view! {
<OptionComp value=initials let:initials>
<span class="thaw-avatar__initials">{initials}</span>
</OptionComp>
}
}}
{move || {
view! {
<OptionComp value=src.get() let:src>
<img
src=src
class="thaw-avatar__image"
role="presentation"
aria-hidden="true"
hidden=move || image_hidden.get()
on:load=on_load
on:error=on_error
/>
</OptionComp>
}
}}
{move || {
if is_show_default_icon.get() {
Either::Left(
view! {
<span aria-hidden="true" class="thaw-avatar__icon">
<svg
fill="currentColor"
aria-hidden="true"
width="1em"
height="1em"
viewBox="0 0 20 20"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M10 2a4 4 0 1 0 0 8 4 4 0 0 0 0-8ZM7 6a3 3 0 1 1 6 0 3 3 0 0 1-6 0Zm-2 5a2 2 0 0 0-2 2c0 1.7.83 2.97 2.13 3.8A9.14 9.14 0 0 0 10 18c1.85 0 3.58-.39 4.87-1.2A4.35 4.35 0 0 0 17 13a2 2 0 0 0-2-2H5Zm-1 2a1 1 0 0 1 1-1h10a1 1 0 0 1 1 1c0 1.3-.62 2.28-1.67 2.95A8.16 8.16 0 0 1 10 17a8.16 8.16 0 0 1-4.33-1.05A3.36 3.36 0 0 1 4 13Z"
fill="currentColor"
></path>
</svg>
</span>
},
)
} else {
Either::Right(())
}
}}
</span>
}
}
fn initials_name(name: String) -> String {
let initials: Vec<_> = name
.split_whitespace()
.filter_map(|word| word.chars().next().and_then(|c| c.to_uppercase().next()))
.collect();
match initials.as_slice() {
[first, .., last] => format!("{first}{last}"),
[first] => first.to_string(),
[] => String::new(),
}
}
#[derive(Default, Clone)]
pub enum AvatarShape {
#[default]
Circular,
Square,
}
impl AvatarShape {
pub fn as_str(&self) -> &'static str {
match self {
Self::Circular => "circular",
Self::Square => "square",
}
}
}
#[test]
fn test_initials_name() {
assert_eq!(initials_name("Jane Doe".into()), "JD".to_string());
assert_eq!(initials_name("Ben".into()), "B".to_string());
assert_eq!(
initials_name("ÇFoo Bar 1Name too ÉLong".into()),
"ÇÉ".to_string()
);
assert_eq!(initials_name("ffl ß".into()), "FS".to_string());
assert_eq!(initials_name("".into()), "".to_string());
assert_eq!(initials_name("山".into()), "山".to_string());
}