mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-05-23 19:05:16 +02:00
refactor: update SidebarUserProfile and Composer components with improved styling and tooltip integration
This commit is contained in:
parent
4083d33b5c
commit
2bdd59611a
3 changed files with 187 additions and 170 deletions
|
|
@ -31,7 +31,9 @@ import {
|
|||
} from "@/components/ui/dropdown-menu";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Spinner } from "@/components/ui/spinner";
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
|
||||
import { useLocaleContext } from "@/contexts/LocaleContext";
|
||||
import { useMediaQuery } from "@/hooks/use-media-query";
|
||||
import { usePlatform } from "@/hooks/use-platform";
|
||||
import { GITHUB_RELEASES_URL, usePrimaryDownload } from "@/lib/desktop-download-utils";
|
||||
import { APP_VERSION } from "@/lib/env-config";
|
||||
|
|
@ -133,15 +135,15 @@ function UserAvatar({
|
|||
bgColor: string;
|
||||
size?: "sm" | "md";
|
||||
}) {
|
||||
const sizeClass = size === "md" ? "h-9 w-9" : "h-8 w-8";
|
||||
const sizeClass = size === "md" ? "h-10 w-10" : "h-8 w-8";
|
||||
|
||||
if (avatarUrl) {
|
||||
return (
|
||||
<Image
|
||||
src={avatarUrl}
|
||||
alt="User avatar"
|
||||
width={size === "md" ? 36 : 32}
|
||||
height={size === "md" ? 36 : 32}
|
||||
width={size === "md" ? 40 : 32}
|
||||
height={size === "md" ? 40 : 32}
|
||||
className={cn(sizeClass, "shrink-0 rounded-full object-cover select-none")}
|
||||
referrerPolicy="no-referrer"
|
||||
unoptimized
|
||||
|
|
@ -175,6 +177,7 @@ export function SidebarUserProfile({
|
|||
const t = useTranslations("sidebar");
|
||||
const { locale, setLocale } = useLocaleContext();
|
||||
const { isDesktop } = usePlatform();
|
||||
const isDesktopViewport = useMediaQuery("(min-width: 768px)");
|
||||
const { os, primary } = usePrimaryDownload();
|
||||
const [isLoggingOut, setIsLoggingOut] = useState(false);
|
||||
const bgColor = stringToColor(user.email);
|
||||
|
|
@ -182,6 +185,7 @@ export function SidebarUserProfile({
|
|||
const displayName = user.name || user.email.split("@")[0];
|
||||
const downloadUrl = primary?.url ?? GITHUB_RELEASES_URL;
|
||||
const downloadLabel = t("download_for_os", { os });
|
||||
const showDownloadCta = !isDesktop && isDesktopViewport;
|
||||
|
||||
const handleLanguageChange = (newLocale: "en" | "es" | "pt" | "hi" | "zh") => {
|
||||
setLocale(newLocale);
|
||||
|
|
@ -204,100 +208,140 @@ export function SidebarUserProfile({
|
|||
// Collapsed view - just show avatar with dropdown
|
||||
if (isCollapsed) {
|
||||
return (
|
||||
<div className="border-t px-1.5 py-2">
|
||||
<Button
|
||||
asChild
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="mx-auto mb-2 h-9 w-9 rounded-md bg-muted hover:bg-accent"
|
||||
>
|
||||
<a
|
||||
href={downloadUrl}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
aria-label={downloadLabel}
|
||||
onClick={() =>
|
||||
trackDesktopDownloadClicked({ os, placement: "sidebar_collapsed" })
|
||||
}
|
||||
>
|
||||
<Download className="h-4 w-4" strokeWidth={2.5} />
|
||||
</a>
|
||||
</Button>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
className={cn(
|
||||
"mx-auto h-9 w-9 rounded-full p-0",
|
||||
"transition-opacity hover:bg-transparent hover:opacity-90",
|
||||
"focus:outline-none focus-visible:outline-none",
|
||||
"data-[state=open]:opacity-90"
|
||||
)}
|
||||
>
|
||||
<UserAvatar
|
||||
avatarUrl={user.avatarUrl}
|
||||
initials={initials}
|
||||
bgColor={bgColor}
|
||||
size="md"
|
||||
/>
|
||||
<span className="sr-only">{displayName}</span>
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
|
||||
<DropdownMenuContent className="w-48" side="right" align="end" sideOffset={8}>
|
||||
<DropdownMenuLabel className="font-normal">
|
||||
<div className="flex items-center gap-2">
|
||||
<UserAvatar avatarUrl={user.avatarUrl} initials={initials} bgColor={bgColor} />
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="truncate text-sm font-medium">{displayName}</p>
|
||||
<p className="truncate text-xs text-muted-foreground">{user.email}</p>
|
||||
</div>
|
||||
</div>
|
||||
</DropdownMenuLabel>
|
||||
|
||||
<DropdownMenuSeparator />
|
||||
|
||||
<DropdownMenuItem onClick={onUserSettings}>
|
||||
<UserCog className="h-4 w-4" />
|
||||
{t("user_settings")}
|
||||
</DropdownMenuItem>
|
||||
|
||||
{onAnnouncements && (
|
||||
<DropdownMenuItem onClick={onAnnouncements}>
|
||||
<Megaphone className="h-4 w-4" />
|
||||
<span className="flex-1">What's New</span>
|
||||
{announcementUnreadCount > 0 && (
|
||||
<span className="inline-flex items-center justify-center min-w-4 h-4 px-1 rounded-full bg-red-500 text-white text-[10px] font-medium">
|
||||
{formatAnnouncementCount(announcementUnreadCount)}
|
||||
</span>
|
||||
<div className="w-full border-t px-1.5 py-2">
|
||||
<div className="flex flex-col items-center gap-2">
|
||||
{showDownloadCta && (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
asChild
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-10 w-10 rounded-lg bg-muted hover:bg-accent"
|
||||
>
|
||||
<a
|
||||
href={downloadUrl}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
aria-label={downloadLabel}
|
||||
onClick={() =>
|
||||
trackDesktopDownloadClicked({ os, placement: "sidebar_collapsed" })
|
||||
}
|
||||
>
|
||||
<Download className="h-4 w-4" strokeWidth={2.5} />
|
||||
</a>
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="right" sideOffset={8}>
|
||||
{downloadLabel}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
)}
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
className={cn(
|
||||
"h-10 w-10 rounded-full p-0",
|
||||
"transition-opacity hover:bg-transparent hover:opacity-90",
|
||||
"focus:outline-none focus-visible:outline-none",
|
||||
"data-[state=open]:opacity-90"
|
||||
)}
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
>
|
||||
<UserAvatar
|
||||
avatarUrl={user.avatarUrl}
|
||||
initials={initials}
|
||||
bgColor={bgColor}
|
||||
size="md"
|
||||
/>
|
||||
<span className="sr-only">{displayName}</span>
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
|
||||
<DropdownMenuContent className="w-48" side="right" align="end" sideOffset={8}>
|
||||
<DropdownMenuLabel className="font-normal">
|
||||
<div className="flex items-center gap-2">
|
||||
<UserAvatar avatarUrl={user.avatarUrl} initials={initials} bgColor={bgColor} />
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="truncate text-sm font-medium">{displayName}</p>
|
||||
<p className="truncate text-xs text-muted-foreground">{user.email}</p>
|
||||
</div>
|
||||
</div>
|
||||
</DropdownMenuLabel>
|
||||
|
||||
<DropdownMenuSeparator />
|
||||
|
||||
<DropdownMenuItem onClick={onUserSettings}>
|
||||
<UserCog className="h-4 w-4" />
|
||||
{t("user_settings")}
|
||||
</DropdownMenuItem>
|
||||
|
||||
{onAnnouncements && (
|
||||
<DropdownMenuItem onClick={onAnnouncements}>
|
||||
<Megaphone className="h-4 w-4" />
|
||||
<span className="flex-1">What's New</span>
|
||||
{announcementUnreadCount > 0 && (
|
||||
<span className="inline-flex items-center justify-center min-w-4 h-4 px-1 rounded-full bg-red-500 text-white text-[10px] font-medium">
|
||||
{formatAnnouncementCount(announcementUnreadCount)}
|
||||
</span>
|
||||
)}
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
|
||||
{setTheme && (
|
||||
<DropdownMenuSub>
|
||||
<DropdownMenuSubTrigger>
|
||||
<Sun className="h-4 w-4" />
|
||||
{t("theme")}
|
||||
</DropdownMenuSubTrigger>
|
||||
<DropdownMenuPortal>
|
||||
<DropdownMenuSubContent className="gap-1">
|
||||
{THEMES.map((themeOption) => {
|
||||
const Icon = themeOption.icon;
|
||||
const isSelected = theme === themeOption.value;
|
||||
return (
|
||||
<DropdownMenuItem
|
||||
key={themeOption.value}
|
||||
onClick={() => handleThemeChange(themeOption.value)}
|
||||
className={cn(
|
||||
"mb-1 last:mb-0 transition-all",
|
||||
"hover:bg-accent hover:text-accent-foreground",
|
||||
isSelected && "text-primary"
|
||||
)}
|
||||
>
|
||||
<Icon className="h-4 w-4" />
|
||||
<span className="flex-1">{t(themeOption.value)}</span>
|
||||
{isSelected && <Check className="h-4 w-4 shrink-0" />}
|
||||
</DropdownMenuItem>
|
||||
);
|
||||
})}
|
||||
</DropdownMenuSubContent>
|
||||
</DropdownMenuPortal>
|
||||
</DropdownMenuSub>
|
||||
)}
|
||||
|
||||
{setTheme && (
|
||||
<DropdownMenuSub>
|
||||
<DropdownMenuSubTrigger>
|
||||
<Sun className="h-4 w-4" />
|
||||
{t("theme")}
|
||||
<Languages className="h-4 w-4" />
|
||||
{t("language")}
|
||||
</DropdownMenuSubTrigger>
|
||||
<DropdownMenuPortal>
|
||||
<DropdownMenuSubContent className="gap-1">
|
||||
{THEMES.map((themeOption) => {
|
||||
const Icon = themeOption.icon;
|
||||
const isSelected = theme === themeOption.value;
|
||||
{LANGUAGES.map((language) => {
|
||||
const isSelected = locale === language.code;
|
||||
return (
|
||||
<DropdownMenuItem
|
||||
key={themeOption.value}
|
||||
onClick={() => handleThemeChange(themeOption.value)}
|
||||
key={language.code}
|
||||
onClick={() => handleLanguageChange(language.code)}
|
||||
className={cn(
|
||||
"mb-1 last:mb-0 transition-all",
|
||||
"hover:bg-accent hover:text-accent-foreground",
|
||||
isSelected && "text-primary"
|
||||
)}
|
||||
>
|
||||
<Icon className="h-4 w-4" />
|
||||
<span className="flex-1">{t(themeOption.value)}</span>
|
||||
<span className="mr-2">{language.flag}</span>
|
||||
<span className="flex-1">{language.name}</span>
|
||||
{isSelected && <Check className="h-4 w-4 shrink-0" />}
|
||||
</DropdownMenuItem>
|
||||
);
|
||||
|
|
@ -305,81 +349,52 @@ export function SidebarUserProfile({
|
|||
</DropdownMenuSubContent>
|
||||
</DropdownMenuPortal>
|
||||
</DropdownMenuSub>
|
||||
)}
|
||||
|
||||
<DropdownMenuSub>
|
||||
<DropdownMenuSubTrigger>
|
||||
<Languages className="h-4 w-4" />
|
||||
{t("language")}
|
||||
</DropdownMenuSubTrigger>
|
||||
<DropdownMenuPortal>
|
||||
<DropdownMenuSubContent className="gap-1">
|
||||
{LANGUAGES.map((language) => {
|
||||
const isSelected = locale === language.code;
|
||||
return (
|
||||
<DropdownMenuItem
|
||||
key={language.code}
|
||||
onClick={() => handleLanguageChange(language.code)}
|
||||
className={cn(
|
||||
"mb-1 last:mb-0 transition-all",
|
||||
"hover:bg-accent hover:text-accent-foreground",
|
||||
isSelected && "text-primary"
|
||||
)}
|
||||
>
|
||||
<span className="mr-2">{language.flag}</span>
|
||||
<span className="flex-1">{language.name}</span>
|
||||
{isSelected && <Check className="h-4 w-4 shrink-0" />}
|
||||
<DropdownMenuSub>
|
||||
<DropdownMenuSubTrigger>
|
||||
<Info className="h-4 w-4" />
|
||||
{t("learn_more")}
|
||||
</DropdownMenuSubTrigger>
|
||||
<DropdownMenuPortal>
|
||||
<DropdownMenuSubContent className="min-w-[180px] gap-1">
|
||||
{LEARN_MORE_LINKS.map((link) => (
|
||||
<DropdownMenuItem key={link.key} asChild>
|
||||
<a href={link.href} target="_blank" rel="noopener noreferrer">
|
||||
<span className="flex-1">{t(link.key)}</span>
|
||||
<ExternalLink className="h-3.5 w-3.5 shrink-0 text-muted-foreground" />
|
||||
</a>
|
||||
</DropdownMenuItem>
|
||||
);
|
||||
})}
|
||||
</DropdownMenuSubContent>
|
||||
</DropdownMenuPortal>
|
||||
</DropdownMenuSub>
|
||||
))}
|
||||
<DropdownMenuSeparator />
|
||||
<p className="select-none px-2 py-1.5 text-xs text-muted-foreground/50">
|
||||
v{APP_VERSION}
|
||||
</p>
|
||||
</DropdownMenuSubContent>
|
||||
</DropdownMenuPortal>
|
||||
</DropdownMenuSub>
|
||||
|
||||
<DropdownMenuSub>
|
||||
<DropdownMenuSubTrigger>
|
||||
<Info className="h-4 w-4" />
|
||||
{t("learn_more")}
|
||||
</DropdownMenuSubTrigger>
|
||||
<DropdownMenuPortal>
|
||||
<DropdownMenuSubContent className="min-w-[180px] gap-1">
|
||||
{LEARN_MORE_LINKS.map((link) => (
|
||||
<DropdownMenuItem key={link.key} asChild>
|
||||
<a href={link.href} target="_blank" rel="noopener noreferrer">
|
||||
<span className="flex-1">{t(link.key)}</span>
|
||||
<ExternalLink className="h-3.5 w-3.5 shrink-0 text-muted-foreground" />
|
||||
</a>
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
<DropdownMenuSeparator />
|
||||
<p className="select-none px-2 py-1.5 text-xs text-muted-foreground/50">
|
||||
v{APP_VERSION}
|
||||
</p>
|
||||
</DropdownMenuSubContent>
|
||||
</DropdownMenuPortal>
|
||||
</DropdownMenuSub>
|
||||
|
||||
{!isDesktop && (
|
||||
<DropdownMenuItem asChild className="font-medium">
|
||||
<a href={downloadUrl} target="_blank" rel="noopener noreferrer">
|
||||
<Download className="h-4 w-4" strokeWidth={2.5} />
|
||||
{downloadLabel}
|
||||
</a>
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
|
||||
<DropdownMenuSeparator />
|
||||
|
||||
<DropdownMenuItem onClick={handleLogout} disabled={isLoggingOut}>
|
||||
{isLoggingOut ? (
|
||||
<Spinner size="sm" className="mr-2" />
|
||||
) : (
|
||||
<LogOut className="h-4 w-4" />
|
||||
{!isDesktop && (
|
||||
<DropdownMenuItem asChild className="font-medium">
|
||||
<a href={downloadUrl} target="_blank" rel="noopener noreferrer">
|
||||
<Download className="h-4 w-4" strokeWidth={2.5} />
|
||||
{downloadLabel}
|
||||
</a>
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
{isLoggingOut ? t("loggingOut") : t("logout")}
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
|
||||
<DropdownMenuSeparator />
|
||||
|
||||
<DropdownMenuItem onClick={handleLogout} disabled={isLoggingOut}>
|
||||
{isLoggingOut ? (
|
||||
<Spinner size="sm" className="mr-2" />
|
||||
) : (
|
||||
<LogOut className="h-4 w-4" />
|
||||
)}
|
||||
{isLoggingOut ? t("loggingOut") : t("logout")}
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -387,21 +402,23 @@ export function SidebarUserProfile({
|
|||
// Expanded view
|
||||
return (
|
||||
<div className="border-t">
|
||||
<Button
|
||||
asChild
|
||||
variant="ghost"
|
||||
className="mx-2 mt-2 mb-1 h-10 w-[calc(100%-1rem)] justify-start gap-2 rounded-md bg-muted px-3 text-sm font-semibold hover:bg-accent"
|
||||
>
|
||||
<a
|
||||
href={downloadUrl}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
onClick={() => trackDesktopDownloadClicked({ os, placement: "sidebar_expanded" })}
|
||||
{showDownloadCta && (
|
||||
<Button
|
||||
asChild
|
||||
variant="ghost"
|
||||
className="mx-2 mt-2 mb-1 h-10 w-[calc(100%-1rem)] justify-start gap-2 rounded-md bg-muted px-3 text-sm font-semibold hover:bg-accent"
|
||||
>
|
||||
<Download className="h-4 w-4" strokeWidth={2.5} />
|
||||
{downloadLabel}
|
||||
</a>
|
||||
</Button>
|
||||
<a
|
||||
href={downloadUrl}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
onClick={() => trackDesktopDownloadClicked({ os, placement: "sidebar_expanded" })}
|
||||
>
|
||||
<Download className="h-4 w-4" strokeWidth={2.5} />
|
||||
{downloadLabel}
|
||||
</a>
|
||||
</Button>
|
||||
)}
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue