User Account Avatar
User Account Avatar is a React and Tailwind CSS media component from SmoothUI, licensed MIT. Copy it and paste it into your project.
SmoothUI
MIT
media
What it is
User Account Avatar is a React and Tailwind CSS media component from SmoothUI, licensed MIT. Copy it and paste it into your project.
How to use it
Open it in the catalogue, press copy, and paste it into your project. The prompt you copy carries the code and the rules that tell your agent to reproduce it without changing anything.
Five free copies a month. No card.
Needs
npm i motion lucide-react @radix-ui/react-popover
Licence
This component comes from SmoothUI and is redistributed under the MIT licence, which permits commercial use. The copyright notice travels with the code.
User Account Avatar
"use client";
import {
Content as PopoverContent,
Portal as PopoverPortal,
Root as PopoverRoot,
Trigger as PopoverTrigger,
} from "@radix-ui/react-popover";
import { Eye, Package, User } from "lucide-react";
import { AnimatePresence, motion, useReducedMotion } from "motion/react";
import { useState } from "react";
export interface UserData {
avatar: string;
email: string;
name: string;
}
export interface Order {
date: string;
id: string;
progress: number;
status: "processing" | "shipped" | "delivered";
}
export interface UserAccountAvatarProps {
className?: string;
onOrderView?: (orderId: string) => void;
onProfileSave?: (user: UserData) => void;
orders?: Order[];
user: UserData;
}
const mockOrders: Order[] = [
{ date: "2023-03-15", id: "ORD001", progress: 100, status: "delivered" },
{ date: "2023-03-20", id: "ORD002", progress: 66, status: "shipped" },
];
export default function UserAccountAvatar({
user,
orders = mockOrders,
onProfileSave,
onOrderView,
className = "",
}: UserAccountAvatarProps) {
const [activeSection, setActiveSection] = useState<string | null>(null);
const [userData, setUserData] = useState<UserData>(user);
const shouldReduceMotion = useReducedMotion();
const handleSectionClick = (section: string) => {
setActiveSection(activeSection === section ? null : section);
};
const handleProfileSave = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
const formData = new FormData(e.currentTarget);
const updatedUser = {
...userData,
email: formData.get("email") as string,
name: formData.get("name") as string,
};
setUserData(updatedUser);
if (onProfileSave) {
onProfileSave(updatedUser);
}
setActiveSection(null);
};
const renderEditProfile = () => (
<form className="flex flex-col gap-3 p-4" onSubmit={handleProfileSave}>
<div className="flex flex-col gap-1.5">
<label
className="font-medium text-muted-foreground text-xs"
htmlFor="name"
>
Name
</label>
<input
className="rounded-md border border-border bg-background px-3 py-2 text-foreground text-sm outline-none transition-colors placeholder:text-muted-foreground focus:border-primary focus:ring-1 focus:ring-primary"
defaultValue={userData.name}
id="name"
name="name"
placeholder="Enter your name"
type="text"
/>
</div>
<div className="flex flex-col gap-1.5">
<label
className="font-medium text-muted-foreground text-xs"
htmlFor="email"
>
Email
</label>
<input
className="rounded-md border border-border bg-background px-3 py-2 text-foreground text-sm outline-none transition-colors placeholder:text-muted-foreground focus:border-primary focus:ring-1 focus:ring-primary"
defaultValue={userData.email}
id="email"
name="email"
placeholder="Enter your email"
type="email"
/>
</div>
<button
className="mt-2 cursor-pointer rounded-md bg-brand px-4 py-2.5 font-semibold text-sm text-white shadow-sm transition-all hover:bg-brand/90 hover:shadow-md active:scale-[0.98] active:bg-brand"
type="submit"
>
Save Changes
</button>
</form>
);
const getStatusColor = (status: Order["status"]) => {
if (status === "processing") {
return "bg-blue-500";
}
if (status === "shipped") {
return "bg-amber-500";
}
return "bg-emerald-500";
};
const renderLastOrders = () => (
<div className="flex flex-col gap-3 p-4">
{orders.map((order) => (
<div
className="flex flex-col gap-3 rounded-lg border border-border bg-muted/30 p-3 transition-colors hover:bg-muted/50"
key={order.id}
>
<div className="flex items-center justify-between">
<div className="font-semibold text-sm">{order.id}</div>
<div className="text-muted-foreground text-xs">{order.date}</div>
</div>
<div className="flex items-center gap-3">
<div className="flex-1 space-y-2">
<div className="flex items-center justify-between text-xs">
<span className="font-medium text-foreground capitalize">
{order.status}
</span>
<span className="text-muted-foreground">{order.progress}%</span>
</div>
<div className="h-1.5 w-full overflow-hidden rounded-full bg-muted">
<motion.div
animate={
shouldReduceMotion ? {} : { width: `${order.progress}%` }
}
className={`h-full rounded-full ${getStatusColor(order.status)}`}
initial={shouldReduceMotion ? {} : { width: 0 }}
transition={
shouldReduceMotion
? { duration: 0 }
: {
damping: 30,
duration: 0.4,
stiffness: 300,
type: "spring" as const,
}
}
/>
</div>
</div>
<button
aria-label="View Order"
className="flex shrink-0 cursor-pointer items-center justify-center rounded-md border border-border bg-background p-2 transition-colors hover:border-primary hover:bg-muted"
onClick={() => {
onOrderView?.(order.id);
}}
type="button"
>
<Eye className="text-muted-foreground" size={16} />
</button>
</div>
</div>
))}
</div>
);
return (
<PopoverRoot>
<PopoverTrigger asChild>
<button
className={`flex cursor-pointer items-center gap-2 rounded-full border bg-background ${className}`}
type="button"
>
<img
alt="User Avatar"
className="rounded-full"
draggable={false}
height={48}
src={userData.avatar}
width={48}
/>
</button>
</PopoverTrigger>
<PopoverPortal>
<PopoverContent
className="z-50 w-64 overflow-hidden rounded-xl border bg-background shadow-xl"
onOpenAutoFocus={(e) => e.preventDefault()}
sideOffset={8}
style={{ pointerEvents: "auto" }}
>
<motion.div
animate={shouldReduceMotion ? {} : { height: "auto" }}
initial={shouldReduceMotion ? {} : { height: "auto" }}
style={{ pointerEvents: "auto" }}
transition={
shouldReduceMotion
? { duration: 0 }
: { bounce: 0, duration: 0.25, type: "spring" as const }
}
>
<div
className="flex flex-col divide-y divide-border"
style={{ pointerEvents: "auto" }}
>
<button
className={`flex w-full cursor-pointer items-center gap-2 rounded-lg px-3 py-2.5 font-medium text-sm transition-colors ${
activeSection === "profile"
? "bg-primary text-primary-foreground"
: "text-foreground hover:bg-muted"
}`}
onClick={() => {
handleSectionClick("profile");
}}
onKeyDown={(e) => {
if (e.key === "Enter" || e.key === " ") {
e.preventDefault();
handleSectionClick("profile");
}
}}
type="button"
>
<User className="shrink-0" size={16} />
Edit Profile
</button>
<AnimatePresence initial={false}>
{activeSection === "profile" && (
<motion.div
animate={
shouldReduceMotion
? { height: "auto", opacity: 1 }
: {
filter: "blur(0px)",
height: "auto",
opacity: 1,
}
}
exit={
shouldReduceMotion
? { height: 0, opacity: 0, transition: { duration: 0 } }
: { filter: "blur(10px)", height: 0, opacity: 0 }
}
initial={
shouldReduceMotion
? { height: 0, opacity: 0 }
: { filter: "blur(10px)", height: 0, opacity: 0 }
}
transition={
shouldReduceMotion
? { duration: 0 }
: { bounce: 0, duration: 0.25, type: "spring" as const }
}
>
{renderEditProfile()}
</motion.div>
)}
</AnimatePresence>
<button
className={`flex w-full cursor-pointer items-center gap-2 rounded-lg px-3 py-2.5 font-medium text-sm transition-colors ${
activeSection === "orders"
? "bg-primary text-primary-foreground"
: "text-foreground hover:bg-muted"
}`}
onClick={() => {
handleSectionClick("orders");
}}
onKeyDown={(e) => {
if (e.key === "Enter" || e.key === " ") {
e.preventDefault();
handleSectionClick("orders");
}
}}
type="button"
>
<Package className="shrink-0" size={16} />
Last Orders
</button>
<AnimatePresence initial={false}>
{activeSection === "orders" && (
<motion.div
animate={
shouldReduceMotion
? { height: "auto", opacity: 1 }
: {
filter: "blur(0px)",
height: "auto",
opacity: 1,
}
}
exit={
shouldReduceMotion
? { height: 0, opacity: 0, transition: { duration: 0 } }
: { filter: "blur(10px)", height: 0, opacity: 0 }
}
initial={
shouldReduceMotion
? { height: 0, opacity: 0 }
: { filter: "blur(10px)", height: 0, opacity: 0 }
}
transition={
shouldReduceMotion
? { duration: 0 }
: { bounce: 0, duration: 0.25, type: "spring" as const }
}
>
{renderLastOrders()}
</motion.div>
)}
</AnimatePresence>
</div>
</motion.div>
</PopoverContent>
</PopoverPortal>
</PopoverRoot>
);
}