Todos os componentes

User Account Avatar

User Account Avatar é um componente de media para React e Tailwind CSS, da biblioteca SmoothUI, com licença MIT. Copia e cola no teu projeto.

SmoothUI MIT media

O que é

User Account Avatar é um componente de media para React e Tailwind CSS, da biblioteca SmoothUI, com licença MIT. Copia e cola no teu projeto.

Como usar

Abre o componente no catálogo, carrega em copiar, e cola no teu projeto. O prompt copiado leva o código e as regras para o teu agente o reproduzir sem alterar nada.

Abrir no catálogo

Cinco cópias grátis por mês. Sem cartão.

Precisa de

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

Licença

Este componente vem de SmoothUI e é redistribuído sob a licença MIT, que permite uso comercial. O aviso de copyright viaja com o código.

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>
  );
}

Componentes parecidos