impulse_thaw/avatar/
mod.rs1use leptos::{either::Either, prelude::*};
2use thaw_components::OptionComp;
3use thaw_utils::{class_list, mount_style};
4
5#[component]
6pub fn Avatar(
7 #[prop(optional, into)] class: MaybeProp<String>,
8 #[prop(optional, into)]
10 src: MaybeProp<String>,
11 #[prop(optional, into)]
13 name: MaybeProp<String>,
14 #[prop(optional, into)]
16 initials: MaybeProp<String>,
17 #[prop(optional, into)]
19 shape: Signal<AvatarShape>,
20 #[prop(optional, into)]
22 size: MaybeProp<u8>,
23) -> impl IntoView {
24 mount_style("avatar", include_str!("./avatar.css"));
25
26 let style = move || {
27 let size = size.get()?;
28
29 let mut style = format!("width: {0}px; height: {0}px;", size);
30
31 if let Some(font_size) = match size {
32 0..=24 => Some(100),
33 25..=28 => Some(200),
34 29..=40 => None,
35 41..=56 => Some(400),
36 57..=96 => Some(500),
37 97..=128 => Some(600),
38 _ => Some(600),
39 } {
40 style.push_str(&format!("font-size: var(--fontSizeBase{});", font_size))
41 }
42
43 Some(style)
44 };
45
46 let image_hidden = RwSignal::new(false);
47 let is_show_default_icon = Memo::new(move |_| {
48 if name.with(|n| n.is_some()) {
49 false
50 } else if src.with(|s| s.is_some()) && !image_hidden.get() {
51 false
52 } else if initials.with(|i| i.is_some()) {
53 false
54 } else {
55 true
56 }
57 });
58
59 let on_load = move |_| {
60 image_hidden.maybe_update(|hidden| {
61 if *hidden {
62 *hidden = false;
63 true
64 } else {
65 true
66 }
67 });
68 };
69
70 let on_error = move |_| {
71 image_hidden.set(true);
72 };
73
74 view! {
75 <span
76 class=class_list![
77 "thaw-avatar",
78 move || format!("thaw-avatar--{}", shape.get().as_str()),
79 class
80 ]
81 style=move || style()
82 role="img"
83 aria-label=move || name.get()
84 >
85 {move || {
86 let mut initials = initials.get();
87 if initials.is_none() {
88 if let Some(name) = name.get() {
89 initials = Some(initials_name(name));
90 }
91 }
92 view! {
93 <OptionComp value=initials let:initials>
94 <span class="thaw-avatar__initials">{initials}</span>
95 </OptionComp>
96 }
97 }}
98 {move || {
99 view! {
100 <OptionComp value=src.get() let:src>
101 <img
102 src=src
103 class="thaw-avatar__image"
104 role="presentation"
105 aria-hidden="true"
106 hidden=move || image_hidden.get()
107 on:load=on_load
108 on:error=on_error
109 />
110 </OptionComp>
111 }
112 }}
113 {move || {
114 if is_show_default_icon.get() {
115 Either::Left(
116 view! {
117 <span aria-hidden="true" class="thaw-avatar__icon">
118 <svg
119 fill="currentColor"
120 aria-hidden="true"
121 width="1em"
122 height="1em"
123 viewBox="0 0 20 20"
124 xmlns="http://www.w3.org/2000/svg"
125 >
126 <path
127 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"
128 fill="currentColor"
129 ></path>
130 </svg>
131 </span>
132 },
133 )
134 } else {
135 Either::Right(())
136 }
137 }}
138 </span>
139 }
140}
141
142fn initials_name(name: String) -> String {
143 let initials: Vec<_> = name
144 .split_whitespace()
145 .filter_map(|word| word.chars().next().and_then(|c| c.to_uppercase().next()))
146 .collect();
147
148 match initials.as_slice() {
149 [first, .., last] => format!("{first}{last}"),
150 [first] => first.to_string(),
151 [] => String::new(),
152 }
153}
154
155#[derive(Default, Clone)]
156pub enum AvatarShape {
157 #[default]
158 Circular,
159 Square,
160}
161
162impl AvatarShape {
163 pub fn as_str(&self) -> &'static str {
164 match self {
165 Self::Circular => "circular",
166 Self::Square => "square",
167 }
168 }
169}
170
171#[test]
172fn test_initials_name() {
173 assert_eq!(initials_name("Jane Doe".into()), "JD".to_string());
174 assert_eq!(initials_name("Ben".into()), "B".to_string());
175 assert_eq!(
176 initials_name("ÇFoo Bar 1Name too ÉLong".into()),
177 "ÇÉ".to_string()
178 );
179 assert_eq!(initials_name("ffl ß".into()), "FS".to_string());
180 assert_eq!(initials_name("".into()), "".to_string());
181 assert_eq!(initials_name("山".into()), "山".to_string());
182}