f6ae86617c
Co-authored-by: Copilot <copilot@github.com>
161 lines
4.6 KiB
Markdown
161 lines
4.6 KiB
Markdown
# Avatar
|
||
|
||
A circular user icon. Shows a profile photo when a URL is provided, or falls back to text (typically initials) on a neutral background.
|
||
|
||
---
|
||
|
||
## Quick example
|
||
|
||
```csharp
|
||
// Initials only
|
||
new Avatar(fallback: "JD")
|
||
|
||
// With a profile photo
|
||
new Avatar(fallback: "Jane Doe", src: "/avatars/jane.jpg")
|
||
```
|
||
|
||
---
|
||
|
||
## All the options
|
||
|
||
```csharp
|
||
public Avatar(
|
||
string fallback,
|
||
string? src = null,
|
||
string size = "default")
|
||
```
|
||
|
||
| Parameter | What it does |
|
||
|---|---|
|
||
| `fallback` | The text shown when no image is available — typically initials like `"JD"`. Also used as the `alt` attribute on the image for screen readers. |
|
||
| `src` | URL of the profile photo. If omitted, the fallback is shown. |
|
||
| `size` | How big the circle is: `"sm"` (32px), `"default"` (40px), `"lg"` (56px), `"xl"` (80px) |
|
||
|
||
---
|
||
|
||
## Real-world examples
|
||
|
||
### Initials in different sizes
|
||
|
||
```csharp
|
||
new Avatar(fallback: "SM", size: "sm") // 32×32 — good for compact lists
|
||
new Avatar(fallback: "JD", size: "default") // 40×40 — standard nav bar
|
||
new Avatar(fallback: "LG", size: "lg") // 56×56 — profile card header
|
||
new Avatar(fallback: "XL", size: "xl") // 80×80 — full profile page
|
||
```
|
||
|
||
### Profile page header
|
||
|
||
```csharp
|
||
new Avatar(
|
||
fallback: user.DisplayName ?? "?",
|
||
src: user.AvatarUrl,
|
||
size: "xl")
|
||
```
|
||
|
||
The `fallback` is always required — even with a `src` — because it becomes the `alt` text on the `<img>` tag.
|
||
|
||
### Overlapping avatar stack (e.g. "3 team members")
|
||
|
||
Wrap multiple avatars in a flex container with negative spacing:
|
||
|
||
```html
|
||
<div class="flex -space-x-2">
|
||
$$Avatar1$$
|
||
$$Avatar2$$
|
||
$$Avatar3$$
|
||
</div>
|
||
```
|
||
|
||
---
|
||
|
||
## How it works
|
||
|
||
The avatar is a `<span>` clipped to a circle with `rounded-full overflow-hidden`. When a `src` is given, an `<img>` fills the circle using `object-cover` so the photo does not stretch. When there is no `src`, a `<span>` with a muted background shows the fallback text centred inside the circle.
|
||
|
||
The component does not handle broken image URLs. If you need a fallback when an image 404s, add an `onerror` attribute in the surrounding HTML.
|
||
|
||
The Avatar does not extract initials from full names — do that yourself before constructing it. `"Jane Doe"` → `"JD"` is two lines of C# and is better kept in your own code.
|
||
|
||
---
|
||
|
||
## Complete page example
|
||
|
||
**`Templates/ProfilePage.htmx`**
|
||
```html
|
||
<div class="max-w-lg mx-auto py-10">
|
||
<div class="flex items-center gap-4 mb-6">
|
||
$$UserAvatar$$
|
||
<div>
|
||
<h1 class="text-xl font-bold">$$DisplayName$$</h1>
|
||
<p class="text-sm text-muted-foreground">$$Email$$</p>
|
||
</div>
|
||
</div>
|
||
<p class="text-sm">Member since $$JoinDate$$</p>
|
||
</div>
|
||
```
|
||
|
||
**`Templates/ProfilePage.htmx.cs`**
|
||
```csharp
|
||
namespace Htmx.ApiDemo.Templates;
|
||
|
||
public sealed class ProfilePage : ProfilePageBase
|
||
{
|
||
private readonly IHtmxComponent _avatar;
|
||
private readonly byte[] _displayName;
|
||
private readonly byte[] _email;
|
||
private readonly byte[] _joinDate;
|
||
|
||
public ProfilePage(AppUser user)
|
||
{
|
||
_avatar = new Components.Avatar(
|
||
fallback: GetInitials(user.DisplayName),
|
||
size: "lg");
|
||
|
||
_displayName = (user.DisplayName ?? "Unknown").ToUtf8Bytes();
|
||
_email = user.Email.ToUtf8Bytes();
|
||
_joinDate = user.CreatedAt.ToString("MMMM yyyy").ToUtf8Bytes();
|
||
}
|
||
|
||
private static string GetInitials(string? name)
|
||
{
|
||
if (string.IsNullOrWhiteSpace(name)) return "?";
|
||
var parts = name.Split(' ', StringSplitOptions.RemoveEmptyEntries);
|
||
return parts.Length >= 2
|
||
? $"{parts[0][0]}{parts[^1][0]}"
|
||
: name[..1].ToUpperInvariant();
|
||
}
|
||
|
||
protected override void RenderUserAvatar(HtmxRenderContext ctx)
|
||
=> _avatar.Render(ctx.Next());
|
||
protected override void RenderDisplayName(HtmxRenderContext ctx)
|
||
=> ctx.Writer.WriteUtf8(_displayName);
|
||
protected override void RenderEmail(HtmxRenderContext ctx)
|
||
=> ctx.Writer.WriteUtf8(_email);
|
||
protected override void RenderJoinDate(HtmxRenderContext ctx)
|
||
=> ctx.Writer.WriteUtf8(_joinDate);
|
||
}
|
||
```
|
||
|
||
**GET handler**
|
||
```csharp
|
||
[Handler]
|
||
[MapGet("/profile")]
|
||
public static partial class GetProfileHandler
|
||
{
|
||
public record Query();
|
||
|
||
private static async Task<IResult> HandleAsync(
|
||
Query _,
|
||
HttpContext ctx,
|
||
MongoDbService db,
|
||
CancellationToken ct)
|
||
{
|
||
var email = ctx.User.FindFirst(ClaimTypes.Email)?.Value ?? "";
|
||
var user = await db.FindByNormalizedEmailAsync(email.ToUpperInvariant(), ct);
|
||
if (user is null) return Results.Redirect("/login");
|
||
return await ctx.WriteHtmxPage(new ProfilePage(user), title: "Profile");
|
||
}
|
||
}
|
||
```
|