Logo SmoothUI
ComponentsUser Account Avatar

User Account Avatar

Component that displays a user's avatar and allows the user to edit their profile information and order history.

Code

Install with shadcn Beta

Terminal

npx shadcn@latest add "https://smoothui.dev/r/user-account-avatar.json"

Manual install

Terminal

npm install motion lucide-react @radix-ui/react-popover

UserAccountAvatar.tsx

"use client"

import { useState } from "react"
import Image from "next/image"
import * as Popover from "@radix-ui/react-popover"
import { Eye, Package, User } from "lucide-react"
import { AnimatePresence, motion } from "motion/react"

interface UserData {
  name: string
  email: string
  avatar: string
}

interface Order {
  id: string
  date: string
  status: "processing" | "shipped" | "delivered"
  progress: number
}

const initialUserData: UserData = {
  name: "John Doe",
  email: "[email protected]",
  avatar: "https://github.com/educlopez.png",
}

const mockOrders: Order[] = [
  { id: "ORD001", date: "2023-03-15", status: "delivered", progress: 100 },
  { id: "ORD002", date: "2023-03-20", status: "shipped", progress: 66 },
]

export default function UserAccountAvatar() {
  const [activeSection, setActiveSection] = useState<string | null>(null)
  const [userData, setUserData] = useState<UserData>(initialUserData)

  const handleSectionClick = (section: string) => {
    setActiveSection(activeSection === section ? null : section)
  }

  const handleProfileSave = (e: React.FormEvent<HTMLFormElement>) => {
    e.preventDefault()
    const formData = new FormData(e.currentTarget)
    setUserData({
      ...userData,
      name: formData.get("name") as string,
      email: formData.get("email") as string,
    })
    setActiveSection(null)
  }

  const renderEditProfile = () => (
    <form onSubmit={handleProfileSave} className="flex flex-col gap-2 p-4">
      <label
        htmlFor="name"
        className="text-light-900 dark:text-dark-900 text-xs font-medium"
      >
        Name
      </label>
      <input
        id="name"
        name="name"
        defaultValue={userData.name}
        className="border-light-500 bg-light-100 text-light-950 dark:border-dark-500 dark:bg-dark-100 dark:text-dark-950 rounded-sm border p-2 text-xs"
        placeholder="Name"
      />
      <label
        htmlFor="email"
        className="text-light-900 dark:text-dark-900 text-xs font-medium"
      >
        Email
      </label>
      <input
        id="email"
        name="email"
        defaultValue={userData.email}
        className="border-light-500 bg-light-100 text-light-950 dark:border-dark-500 dark:bg-dark-100 dark:text-dark-950 rounded-sm border p-2 text-xs"
        placeholder="Email"
      />

      <button
        type="submit"
        className="bg-light-300 text-light-950 hover:bg-light-400 dark:bg-dark-300 dark:text-dark-950 dark:hover:bg-dark-400 cursor-pointer rounded-sm px-4 py-2 text-sm"
      >
        Save
      </button>
    </form>
  )

  const renderLastOrders = () => (
    <div className="flex flex-col gap-2 p-2">
      {mockOrders.map((order) => (
        <div
          key={order.id}
          className="border-light-300 bg-light-100 dark:border-dark-300 dark:bg-dark-100 flex flex-col items-center justify-between gap-3 rounded-sm border p-2 text-xs"
        >
          <div className="flex w-full items-center justify-between">
            <div className="font-medium">{order.id}</div>
            <div className="text-light-900 dark:text-dark-900">
              {order.date}
            </div>
          </div>
          <div className="flex w-full items-center gap-2">
            <div className="w-full">
              <div className="flex justify-between">
                <span>{order.status}</span>
                <span>{order.progress}%</span>
              </div>
              <div className="mt-1 h-1 w-full rounded-sm bg-gray-200">
                <div
                  className={`h-full rounded ${
                    order.status === "processing"
                      ? "bg-blue-500"
                      : order.status === "shipped"
                        ? "bg-yellow-500"
                        : "bg-green-500"
                  }`}
                  style={{ width: `${order.progress}%` }}
                />
              </div>
            </div>
            <button
              className="border-light-300 bg-light-50 dark:border-dark-300 dark:bg-dark-50 rounded-sm border p-1"
              aria-label="View Order"
            >
              <Eye size={14} />
            </button>
          </div>
        </div>
      ))}
    </div>
  )

  return (
    <Popover.Root>
      <Popover.Trigger asChild>
        <button className="border-light-300 bg-light-50 dark:border-dark-300 dark:bg-dark-50 flex cursor-pointer items-center gap-2 rounded-full border">
          <Image
            src={userData.avatar}
            alt="User Avatar"
            width={48}
            height={48}
            className="rounded-full"
          />
        </button>
      </Popover.Trigger>
      <Popover.Portal>
        <Popover.Content
          className="border-light-300 bg-light-50 dark:border-dark-300 dark:bg-dark-50 w-48 overflow-hidden rounded-lg border text-sm shadow-lg"
          sideOffset={5}
        >
          <motion.div
            initial={{ height: "auto" }}
            animate={{ height: "auto" }}
            transition={{ type: "spring", duration: 0.3, bounce: 0 }}
          >
            <div className="flex flex-col">
              <div
                className="hover:bg-light-200 dark:hover:bg-dark-200 cursor-pointer p-2"
                onClick={() => handleSectionClick("profile")}
              >
                <User size={16} className="mr-2 inline" />
                Edit Profile
              </div>
              <AnimatePresence initial={false}>
                {activeSection === "profile" && (
                  <motion.div
                    initial={{ opacity: 0, height: 0, filter: "blur(10px)" }}
                    animate={{
                      opacity: 1,
                      height: "auto",
                      filter: "blur(0px)",
                    }}
                    exit={{ opacity: 0, height: 0, filter: "blur(10px)" }}
                    transition={{ type: "spring", duration: 0.3, bounce: 0 }}
                  >
                    {renderEditProfile()}
                  </motion.div>
                )}
              </AnimatePresence>
              <div
                className="hover:bg-light-200 dark:hover:bg-dark-200 cursor-pointer p-2"
                onClick={() => handleSectionClick("orders")}
              >
                <Package size={16} className="mr-2 inline" />
                Last Orders
              </div>
              <AnimatePresence initial={false}>
                {activeSection === "orders" && (
                  <motion.div
                    initial={{ opacity: 0, height: 0, filter: "blur(10px)" }}
                    animate={{
                      opacity: 1,
                      height: "auto",
                      filter: "blur(0px)",
                    }}
                    exit={{ opacity: 0, height: 0, filter: "blur(10px)" }}
                    transition={{ type: "spring", duration: 0.3, bounce: 0 }}
                  >
                    {renderLastOrders()}
                  </motion.div>
                )}
              </AnimatePresence>
            </div>
          </motion.div>
        </Popover.Content>
      </Popover.Portal>
    </Popover.Root>
  )
}