impulse_thaw/avatar/
mod.rs

1use 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    /// The Avatar's image.
9    #[prop(optional, into)]
10    src: MaybeProp<String>,
11    /// The name of the person or entity represented by this Avatar.
12    #[prop(optional, into)]
13    name: MaybeProp<String>,
14    /// Custom initials.
15    #[prop(optional, into)]
16    initials: MaybeProp<String>,
17    /// The avatar can have a circular or square shape.
18    #[prop(optional, into)]
19    shape: Signal<AvatarShape>,
20    /// Size of the avatar in pixels.
21    #[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}