feat(user-avatar): refactor avatar component into user-avatar with improved structure

This commit is contained in:
cfngc4594 2025-05-05 18:39:47 +08:00
parent a3ef5d88e6
commit 500113fe8f
6 changed files with 117 additions and 110 deletions

View File

@ -7,7 +7,7 @@
"Dark": "Dark" "Dark": "Dark"
} }
}, },
"AvatarButton": { "UserAvatar": {
"Settings": "Settings", "Settings": "Settings",
"LogIn": "LogIn", "LogIn": "LogIn",
"LogOut": "LogOut" "LogOut": "LogOut"
@ -109,7 +109,8 @@
"description": "Enter your email below to sign in to your account", "description": "Enter your email below to sign in to your account",
"or": "Or", "or": "Or",
"noAccount": "Don't have an account?", "noAccount": "Don't have an account?",
"signUp": "Sign up" "signUp": "Sign up",
"oauth": "Sign in with {provider}"
}, },
"signInWithCredentials": { "signInWithCredentials": {
"userNotFound": "User not found.", "userNotFound": "User not found.",

View File

@ -7,7 +7,7 @@
"Dark": "深色" "Dark": "深色"
} }
}, },
"AvatarButton": { "UserAvatar": {
"Settings": "设置", "Settings": "设置",
"LogIn": "登录", "LogIn": "登录",
"LogOut": "登出" "LogOut": "登出"
@ -109,7 +109,8 @@
"description": "请输入你的邮箱以登录账户", "description": "请输入你的邮箱以登录账户",
"or": "或者", "or": "或者",
"noAccount": "还没有账户?", "noAccount": "还没有账户?",
"signUp": "注册" "signUp": "注册",
"oauth": "使用 {provider} 登录"
}, },
"signInWithCredentials": { "signInWithCredentials": {
"userNotFound": "未找到用户。", "userNotFound": "未找到用户。",

View File

@ -1,77 +0,0 @@
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { LogOutIcon } from "lucide-react";
import { auth, signOut } from "@/lib/auth";
import { getTranslations } from "next-intl/server";
import { Skeleton } from "@/components/ui/skeleton";
import LogInButton from "@/components/log-in-button";
import { Avatar, AvatarImage } from "@/components/ui/avatar";
import { SettingsButton } from "@/components/settings-button";
const UserAvatar = ({ image, name }: { image: string; name: string }) => (
<Avatar className="h-8 w-8 rounded-lg">
<AvatarImage src={image} alt={name} />
<Skeleton className="h-full w-full" />
</Avatar>
);
async function handleLogOut() {
"use server";
await signOut();
}
export async function AvatarButton() {
const session = await auth();
const t = await getTranslations("AvatarButton");
const isLoggedIn = !!session?.user;
const image = session?.user?.image ?? "/shadcn.jpg";
const name = session?.user?.name ?? "unknown";
const email = session?.user?.email ?? "unknwon@example.com";
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<UserAvatar image={image} name={name} />
</DropdownMenuTrigger>
<DropdownMenuContent
className="w-[--radix-dropdown-menu-trigger-width] min-w-56 rounded-lg"
align="end"
sideOffset={8}
>
{!isLoggedIn ? (
<DropdownMenuGroup>
<SettingsButton />
<LogInButton />
</DropdownMenuGroup>
) : (
<>
<DropdownMenuLabel className="p-0 font-normal">
<div className="flex items-center gap-2 px-1 py-1.5 text-left text-sm">
<UserAvatar image={image} name={name} />
<div className="grid flex-1 text-left text-sm leading-tight">
<span className="truncate font-semibold">{name}</span>
<span className="truncate text-xs">{email}</span>
</div>
</div>
</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuGroup>
<SettingsButton />
<DropdownMenuItem onClick={handleLogOut}>
<LogOutIcon />
{t("LogOut")}
</DropdownMenuItem>
</DropdownMenuGroup>
</>
)}
</DropdownMenuContent>
</DropdownMenu>
);
}

View File

@ -1,26 +0,0 @@
"use client";
import { LogIn } from "lucide-react";
import { useTranslations } from "next-intl";
import { DropdownMenuItem } from "@/components/ui/dropdown-menu";
import { usePathname, useRouter, useSearchParams } from "next/navigation";
export default function LogInButton() {
const router = useRouter();
const pathname = usePathname();
const searchParams = useSearchParams();
const t = useTranslations("AvatarButton");
const handleLogIn = () => {
const params = new URLSearchParams(searchParams.toString());
params.set("redirectTo", pathname);
router.push(`/sign-in?${params.toString()}`);
};
return (
<DropdownMenuItem onClick={handleLogIn}>
<LogIn />
{t("LogIn")}
</DropdownMenuItem>
);
}

View File

@ -6,7 +6,7 @@ import { useSettingsStore } from "@/stores/useSettingsStore";
import { DropdownMenuItem } from "@/components/ui/dropdown-menu"; import { DropdownMenuItem } from "@/components/ui/dropdown-menu";
export function SettingsButton() { export function SettingsButton() {
const t = useTranslations("AvatarButton"); const t = useTranslations("UserAvatar");
const { setDialogOpen } = useSettingsStore(); const { setDialogOpen } = useSettingsStore();
return ( return (

View File

@ -0,0 +1,108 @@
import { cn } from "@/lib/utils";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { LogIn, LogOutIcon } from "lucide-react";
import { auth, signIn, signOut } from "@/lib/auth";
import { getTranslations } from "next-intl/server";
import { Skeleton } from "@/components/ui/skeleton";
import { SettingsButton } from "@/components/settings-button";
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
const handleLogIn = async () => {
"use server";
await signIn();
};
const handleLogOut = async () => {
"use server";
await signOut();
};
interface UserAvatarIconProps {
image?: string | null;
name?: string | null;
className?: string;
}
const UserAvatarIcon = ({ image, name, className }: UserAvatarIconProps) => {
return (
<Avatar className={cn("h-8 w-8 cursor-pointer", className)}>
<AvatarImage src={image ?? undefined} />
<AvatarFallback>{name?.charAt(0) ?? "U"}</AvatarFallback>
</Avatar>
);
};
interface UserProfileInfoProps {
name?: string | null;
email?: string | null;
}
const UserProfileInfo = ({ name, email }: UserProfileInfoProps) => {
return (
<div className="grid flex-1 text-left text-sm leading-tight">
<span className="truncate font-semibold">{name ?? "undefined"}</span>
<span className="truncate text-xs">{email ?? "undefined"}</span>
</div>
);
};
const UserAvatar = async () => {
const session = await auth();
const user = session?.user;
const isLoggedIn = !!user;
const t = await getTranslations("UserAvatar");
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<UserAvatarIcon image={user?.image} name={user?.name} />
</DropdownMenuTrigger>
<DropdownMenuContent
className="w-[--radix-dropdown-menu-trigger-width] min-w-56 rounded-lg"
align="end"
sideOffset={8}
>
{!isLoggedIn ? (
<DropdownMenuGroup>
<SettingsButton />
<DropdownMenuItem onClick={handleLogIn}>
<LogIn />
{t("LogIn")}
</DropdownMenuItem>
</DropdownMenuGroup>
) : (
<>
<DropdownMenuLabel className="p-0 font-normal">
<div className="flex items-center gap-2 px-1 py-1.5 text-left text-sm">
<UserAvatarIcon image={user.image} name={user.name} />
<UserProfileInfo name={user.name} email={user.email} />
</div>
</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuGroup>
<SettingsButton />
<DropdownMenuItem onClick={handleLogOut}>
<LogOutIcon />
{t("LogOut")}
</DropdownMenuItem>
</DropdownMenuGroup>
</>
)}
</DropdownMenuContent>
</DropdownMenu>
);
};
const UserAvatarSkeleton = () => {
return <Skeleton className="h-8 w-8 rounded-full" />;
};
export { UserAvatar, UserAvatarSkeleton };