All components

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.

Open in the catalogue

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

index.tsx
"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>
  );
}

Similar components