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.
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.
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