User Profile

← Back to components

Quick Start

Drop-in avatar with login/logout handling. Shows a login button when logged out, Gravatar avatar when logged in. Optional dropdown menu with dashboard, profile, and logout links.

Astro 6

<!-- src/components/UserAvatar.astro -->
<div id="user-avatar"></div>

<script>
  import { initUserProfile } from '@marketdataapp/ui/user-profile';

  initUserProfile({
    container: document.getElementById('user-avatar'),
    dropdown: true,
  });
</script>

That's it. The component handles fetching user state, rendering, and cleanup automatically.

In a navbar

<!-- src/components/Navbar.astro -->
<nav class="flex items-center justify-between px-4 py-2">
  <a href="/">Logo</a>
  <div id="user-nav"></div>
</nav>

<script>
  import { initUserProfile } from '@marketdataapp/ui/user-profile';

  const cleanup = await initUserProfile({
    container: document.getElementById('user-nav'),
    dropdown: true,
    loginUrl: 'https://dashboard.marketdata.app/marketdata/login',
    dashboardUrl: 'https://dashboard.marketdata.app/marketdata/member',
  });

  // cleanup() removes DOM and unsubscribes — call on page teardown if needed
</script>

Avatar only (no dropdown)

<div id="avatar"></div>

<script>
  import { initUserProfile } from '@marketdataapp/ui/user-profile';

  initUserProfile({
    container: document.getElementById('avatar'),
    dropdown: false, // default
  });
</script>

Live Demos

Live (real API)

Hits the real API. Result depends on your login state.

dropdown: false
dropdown: true

Skeleton (loading state)

Shown immediately while the API request is in flight. Same markup as the login pill but with user-profile-skeleton — text is transparent, SVGs are invisible, pointer events disabled.

dropdown: false
dropdown: true

Simulated

Driven by the state picker above. Switch states to see the avatar react.

dropdown: false
dropdown: true

Options

Option Default Description
container required DOM element to render into.
dropdown false Enable dropdown menu on avatar click.
loginUrl .../login Where the login button links to.
loginText "Log in" Login button label.
signupUrl .../signup Where the signup button links to (guest dropdown).
signupText "Start Free Trial" Signup button label.
logoutUrl .../logout Logout link in the dropdown.
dashboardUrl .../member Dashboard link in the dropdown.
profileUrl .../profile Profile link in the dropdown.
planUrl .../signup Plan/upgrade link in the dropdown.
menuItems [] Additional dropdown items. Array of { label, url }.
apiUrl .../api/user/ Override the user API endpoint (useful for testing/mocking).

Using fetchUser Directly

If you need user data without the avatar component, import the user state module directly.

<!-- src/components/Greeting.astro -->
<p id="greeting"></p>

<script>
  import { fetchUser, onUserChange } from '@marketdataapp/ui/user';

  const el = document.getElementById('greeting');

  function render(user) {
    el.textContent = user ? `Hello, ${user.name}` : 'Welcome, guest';
  }

  // Initial render
  render(await fetchUser());

  // React to login/logout changes
  onUserChange(render);
</script>
Function Returns Description
fetchUser(options?) Promise<Object|null> Get the current user. Returns cached data within TTL, revalidates in background when stale.
onUserChange(cb) () => void Subscribe to user state changes. Returns an unsubscribe function.
_clearCache() void Reset all caches and state. For testing only.

Live State

Current cache state on this page. Hit "Force Revalidate" to clear the cache and re-fetch.

fetchUser() result
Last fetch time
Cache age
Cache status
Subscriber notifications 0

User State Visibility

Declarative show/hide for any element based on user state. Mark elements with data-user-state and the hidden attribute. After initUserState() resolves, matching elements are revealed.

Usage

<div data-user-state="logged-in" hidden>Welcome back!</div>
<div data-user-state="logged-out" hidden>Please log in</div>
<div data-user-state="paid" hidden>Premium content</div>
<div data-user-state="free" hidden>Upgrade now</div>
<div data-user-state="trial" hidden>Trial ends soon</div>
<div data-user-state="product:quant" hidden>Quant features</div>
<div data-user-state="paid trial" hidden>All non-free users (OR)</div>

<script type="module">
  import { initUserState } from '@marketdataapp/ui/user-state';
  initUserState();
</script>

Conditions

Condition Matches when
logged-in User is authenticated
logged-out User is not authenticated
paid user.paid === true
free Logged in and user.paid === false
trial user.trial === true
product:<slug> User has the named product (case-insensitive)

Space-separated conditions use OR logic — the element is shown if any condition matches.

Live Demo

Use the state picker above to see which badges appear for each user type.

CTA Banner

A call-to-action banner that dynamically swaps content based on user state. Uses data-user-state to show different headings, subtext, and buttons for logged-out, free/trial, and paid users.

Use the state picker above to see the CTA change for each user type.

How It Works

The user module handles caching and request deduplication so you don't have to. Multiple components on the same page share a single user state with zero redundant API calls.

Caching

fetchUser() checks three tiers before hitting the network: an in-memory cache, then sessionStorage, then GET /api/user/. Results are fresh for 60 seconds. After that, stale data is returned immediately while a background revalidation runs.

Request Deduplication

If a fetch is already in flight, additional calls return the same promise. Three components calling fetchUser() simultaneously result in a single network request.

Subscriber Notifications

onUserChange(callback) fires when a background revalidation detects changed data (profile update) or an error after a previously cached user (logout). It does not fire when the cache is fresh or when revalidation returns the same data.

fetchUser()
  |
  +-- cache hit?
  |     +-- fresh (< 60s) --> return cached (no network)
  |     +-- stale (>= 60s) --> return cached + revalidate in background
  |
  +-- cache miss --> await network fetch (deduplicated)
        +-- success --> cache result, notify subscribers if data changed
        +-- failure --> retry up to 2x with exponential backoff