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