7f271ef2e0
Render the stored user avatar in the Chat Header when available while preserving the existing Profile icon fallback, navigation, accessibility label, and analytics.
105 lines
3.8 KiB
TypeScript
105 lines
3.8 KiB
TypeScript
"use client";
|
|
/**
|
|
* ChatHeader 顶部栏
|
|
*
|
|
* Profile 优先展示用户头像;头像缺失时回退到 lucide-react <UserRound />。
|
|
*/
|
|
import type { ReactNode } from "react";
|
|
import { Lock, UserRound } from "lucide-react";
|
|
|
|
import { BackButton, UserMessageAvatar } from "@/app/_components";
|
|
import { useActiveCharacterRoutes } from "@/providers/character-provider";
|
|
import { buildGlobalPageUrl } from "@/router/global-route-context";
|
|
import { ROUTES } from "@/router/routes";
|
|
import { useAppNavigator } from "@/router/use-app-navigator";
|
|
import { useUserSelector } from "@/stores/user/user-context";
|
|
|
|
import { BrowserHintOverlay } from "./browser-hint-overlay";
|
|
|
|
export interface ChatHeaderProps {
|
|
isGuest: boolean;
|
|
offerBanner?: ReactNode;
|
|
showBrowserHint?: boolean;
|
|
}
|
|
|
|
export function ChatHeader({
|
|
isGuest,
|
|
offerBanner,
|
|
showBrowserHint = false,
|
|
}: ChatHeaderProps) {
|
|
const navigator = useAppNavigator();
|
|
const characterRoutes = useActiveCharacterRoutes();
|
|
const avatarUrl = useUserSelector((state) => state.context.avatarUrl);
|
|
const handleOpenProfile = () => {
|
|
navigator.push(buildGlobalPageUrl(ROUTES.profile, characterRoutes.chat));
|
|
};
|
|
|
|
return (
|
|
<header className="flex shrink-0 flex-col items-stretch bg-transparent p-0">
|
|
{isGuest ? (
|
|
<button
|
|
type="button"
|
|
data-analytics-key="chat.open_signup"
|
|
data-analytics-label="Open sign up"
|
|
className="flex w-full cursor-pointer items-center justify-center gap-(--spacing-sm,8px) border-0 bg-accent px-(--spacing-md,12px) py-(--spacing-sm,8px) text-center text-(length:--responsive-caption,var(--font-size-sm,12px)) font-medium text-white"
|
|
onClick={() => navigator.openAuth(characterRoutes.chat)}
|
|
aria-label="Sign up to unlock more features"
|
|
>
|
|
<Lock
|
|
className="size-(--icon-size-sm,16px)"
|
|
size={16}
|
|
aria-hidden="true"
|
|
/>
|
|
<span>Sign up to unlock more features</span>
|
|
</button>
|
|
) : (
|
|
offerBanner
|
|
)}
|
|
|
|
<div
|
|
className={
|
|
isGuest
|
|
? "flex items-center px-(--chat-inline-padding,16px) py-(--spacing-sm,8px)"
|
|
: "grid grid-cols-[auto_minmax(0,1fr)_auto] items-center gap-(--spacing-sm,8px) px-(--chat-inline-padding,16px) py-(--spacing-sm,8px)"
|
|
}
|
|
>
|
|
<BackButton
|
|
href={characterRoutes.splash}
|
|
variant="dark"
|
|
ariaLabel="Back to home"
|
|
analyticsKey="chat.back_to_home"
|
|
/>
|
|
|
|
{!isGuest ? (
|
|
<>
|
|
<div className="flex min-w-0 justify-center">
|
|
{showBrowserHint ? <BrowserHintOverlay /> : null}
|
|
</div>
|
|
|
|
{avatarUrl ? (
|
|
<UserMessageAvatar
|
|
avatarUrl={avatarUrl}
|
|
size="var(--responsive-icon-button-size, 42px)"
|
|
actionLabel="Profile"
|
|
analyticsKey="chat.open_profile"
|
|
onClick={handleOpenProfile}
|
|
/>
|
|
) : (
|
|
<button
|
|
type="button"
|
|
data-analytics-key="chat.open_profile"
|
|
data-analytics-label="Open profile"
|
|
className="flex size-(--responsive-icon-button-size,42px) cursor-pointer items-center justify-center rounded-full border border-[rgba(255,255,255,0.12)] bg-[rgba(13,11,20,0.7)] text-(--color-text-primary,#fff) shadow-[0_10px_24px_rgba(0,0,0,0.2)] backdrop-blur-md transition-[background,transform] duration-150 hover:bg-[rgba(28,24,39,0.82)] active:scale-96 focus-visible:outline-2 focus-visible:outline-offset-3 focus-visible:outline-[#f657a0]"
|
|
onClick={handleOpenProfile}
|
|
aria-label="Profile"
|
|
>
|
|
<UserRound size={24} aria-hidden="true" />
|
|
</button>
|
|
)}
|
|
</>
|
|
) : null}
|
|
</div>
|
|
</header>
|
|
);
|
|
}
|