mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-05-23 19:05:16 +02:00
refactor: extract user avatar color and initials logic into a new utility module, update related components to use the new functions
This commit is contained in:
parent
87caa4b6d0
commit
e0ecea61f8
6 changed files with 65 additions and 63 deletions
|
|
@ -27,14 +27,12 @@ export function ApiKeyContent() {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6 min-w-0 overflow-hidden">
|
<div className="space-y-6 min-w-0 overflow-hidden">
|
||||||
<Alert className="bg-muted/50 py-3 md:py-4">
|
<Alert>
|
||||||
<Info className="h-3 w-3 md:h-4 md:w-4 shrink-0" />
|
<Info />
|
||||||
<AlertDescription className="text-xs md:text-sm">
|
<AlertDescription>{t("api_key_warning_description")}</AlertDescription>
|
||||||
{t("api_key_warning_description")}
|
|
||||||
</AlertDescription>
|
|
||||||
</Alert>
|
</Alert>
|
||||||
|
|
||||||
<div className="rounded-lg border border-border/60 bg-card p-6 min-w-0 overflow-hidden">
|
<div className="min-w-0 overflow-hidden">
|
||||||
<h3 className="mb-4 text-sm font-semibold tracking-tight">{t("your_api_key")}</h3>
|
<h3 className="mb-4 text-sm font-semibold tracking-tight">{t("your_api_key")}</h3>
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
<div className="h-12 w-full animate-pulse rounded-md border border-border/60 bg-muted/30" />
|
<div className="h-12 w-full animate-pulse rounded-md border border-border/60 bg-muted/30" />
|
||||||
|
|
@ -70,7 +68,7 @@ export function ApiKeyContent() {
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="rounded-lg border border-border/60 bg-card p-6 min-w-0 overflow-hidden">
|
<div className="min-w-0 overflow-hidden">
|
||||||
<h3 className="mb-2 text-sm font-semibold tracking-tight">{t("usage_title")}</h3>
|
<h3 className="mb-2 text-sm font-semibold tracking-tight">{t("usage_title")}</h3>
|
||||||
<p className="mb-4 text-[11px] text-muted-foreground/60">{t("usage_description")}</p>
|
<p className="mb-4 text-[11px] text-muted-foreground/60">{t("usage_description")}</p>
|
||||||
<div className="flex items-center gap-2 rounded-md border border-border/60 bg-muted/30 px-2.5 py-1.5">
|
<div className="flex items-center gap-2 rounded-md border border-border/60 bg-muted/30 px-2.5 py-1.5">
|
||||||
|
|
|
||||||
|
|
@ -11,8 +11,17 @@ import { Button } from "@/components/ui/button";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { Label } from "@/components/ui/label";
|
import { Label } from "@/components/ui/label";
|
||||||
import { Spinner } from "@/components/ui/spinner";
|
import { Spinner } from "@/components/ui/spinner";
|
||||||
|
import { getUserAvatarColor, getUserInitials } from "@/lib/user-avatar";
|
||||||
|
|
||||||
function AvatarDisplay({ url, fallback }: { url?: string; fallback: string }) {
|
function AvatarDisplay({
|
||||||
|
url,
|
||||||
|
fallback,
|
||||||
|
bgColor,
|
||||||
|
}: {
|
||||||
|
url?: string;
|
||||||
|
fallback: string;
|
||||||
|
bgColor: string;
|
||||||
|
}) {
|
||||||
const [errorUrl, setErrorUrl] = useState<string>();
|
const [errorUrl, setErrorUrl] = useState<string>();
|
||||||
const hasError = errorUrl === url;
|
const hasError = errorUrl === url;
|
||||||
|
|
||||||
|
|
@ -23,15 +32,19 @@ function AvatarDisplay({ url, fallback }: { url?: string; fallback: string }) {
|
||||||
alt="Avatar"
|
alt="Avatar"
|
||||||
width={64}
|
width={64}
|
||||||
height={64}
|
height={64}
|
||||||
className="h-16 w-16 rounded-xl object-cover"
|
className="h-16 w-16 rounded-full object-cover select-none"
|
||||||
onError={() => setErrorUrl(url)}
|
onError={() => setErrorUrl(url)}
|
||||||
|
referrerPolicy="no-referrer"
|
||||||
unoptimized
|
unoptimized
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex h-16 w-16 items-center justify-center rounded-xl bg-muted text-xl font-semibold text-muted-foreground">
|
<div
|
||||||
|
className="flex h-16 w-16 shrink-0 items-center justify-center rounded-full text-xl font-semibold text-white select-none"
|
||||||
|
style={{ backgroundColor: bgColor }}
|
||||||
|
>
|
||||||
{fallback}
|
{fallback}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
@ -50,11 +63,6 @@ export function ProfileContent() {
|
||||||
}
|
}
|
||||||
}, [user]);
|
}, [user]);
|
||||||
|
|
||||||
const getInitials = (email: string) => {
|
|
||||||
const name = email.split("@")[0];
|
|
||||||
return name.slice(0, 2).toUpperCase();
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSubmit = async (e: React.FormEvent) => {
|
const handleSubmit = async (e: React.FormEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
|
|
@ -69,6 +77,7 @@ export function ProfileContent() {
|
||||||
};
|
};
|
||||||
|
|
||||||
const hasChanges = displayName !== (user?.display_name || "");
|
const hasChanges = displayName !== (user?.display_name || "");
|
||||||
|
const avatarBgColor = getUserAvatarColor(user?.email || "");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
|
|
@ -78,13 +87,13 @@ export function ProfileContent() {
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<form onSubmit={handleSubmit} className="space-y-6">
|
<form onSubmit={handleSubmit} className="space-y-6">
|
||||||
<div className="rounded-lg bg-card">
|
<div className="rounded-lg bg-main-panel">
|
||||||
<div className="flex flex-col gap-6">
|
<div className="flex flex-col gap-6">
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label>{t("profile_avatar")}</Label>
|
|
||||||
<AvatarDisplay
|
<AvatarDisplay
|
||||||
url={user?.avatar_url || undefined}
|
url={user?.avatar_url || undefined}
|
||||||
fallback={getInitials(user?.email || "")}
|
fallback={getUserInitials(user?.email || "")}
|
||||||
|
bgColor={avatarBgColor}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -316,7 +316,7 @@ const MessageInfoDropdown: FC<{ chatTurnId: string | null | undefined }> = ({ ch
|
||||||
)}
|
)}
|
||||||
{hasUsage && (
|
{hasUsage && (
|
||||||
<>
|
<>
|
||||||
<ActionBarMorePrimitive.Separator className="bg-popover-border mx-2 my-1 h-px" />
|
<ActionBarMorePrimitive.Separator className="bg-popover-border mx-1 my-1 h-px" />
|
||||||
{models.length > 0 ? (
|
{models.length > 0 ? (
|
||||||
models.map(([model, counts]) => {
|
models.map(([model, counts]) => {
|
||||||
const { name, icon } = resolveModel(model);
|
const { name, icon } = resolveModel(model);
|
||||||
|
|
@ -586,7 +586,7 @@ const AssistantActionBar: FC = () => {
|
||||||
className="aui-assistant-action-bar-root -ml-1 col-start-3 row-start-2 flex gap-1 text-muted-foreground md:data-floating:absolute md:data-floating:rounded-md md:data-floating:p-1 [&>button]:opacity-100 md:[&>button]:opacity-[var(--aui-button-opacity,1)]"
|
className="aui-assistant-action-bar-root -ml-1 col-start-3 row-start-2 flex gap-1 text-muted-foreground md:data-floating:absolute md:data-floating:rounded-md md:data-floating:p-1 [&>button]:opacity-100 md:[&>button]:opacity-[var(--aui-button-opacity,1)]"
|
||||||
>
|
>
|
||||||
<ActionBarPrimitive.Copy asChild>
|
<ActionBarPrimitive.Copy asChild>
|
||||||
<TooltipIconButton tooltip="Copy to clipboard">
|
<TooltipIconButton tooltip="Copy">
|
||||||
<AuiIf condition={({ message }) => message.isCopied}>
|
<AuiIf condition={({ message }) => message.isCopied}>
|
||||||
<CheckIcon />
|
<CheckIcon />
|
||||||
</AuiIf>
|
</AuiIf>
|
||||||
|
|
|
||||||
|
|
@ -38,6 +38,7 @@ import { usePlatform } from "@/hooks/use-platform";
|
||||||
import { GITHUB_RELEASES_URL, usePrimaryDownload } from "@/lib/desktop-download-utils";
|
import { GITHUB_RELEASES_URL, usePrimaryDownload } from "@/lib/desktop-download-utils";
|
||||||
import { APP_VERSION } from "@/lib/env-config";
|
import { APP_VERSION } from "@/lib/env-config";
|
||||||
import { trackDesktopDownloadClicked } from "@/lib/posthog/events";
|
import { trackDesktopDownloadClicked } from "@/lib/posthog/events";
|
||||||
|
import { getUserAvatarColor, getUserInitials } from "@/lib/user-avatar";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import type { User } from "../../types/layout.types";
|
import type { User } from "../../types/layout.types";
|
||||||
|
|
||||||
|
|
@ -81,46 +82,6 @@ function formatAnnouncementCount(count: number): string {
|
||||||
return `${thousands}k+`;
|
return `${thousands}k+`;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Generates a consistent color based on email
|
|
||||||
*/
|
|
||||||
function stringToColor(str: string): string {
|
|
||||||
let hash = 0;
|
|
||||||
for (let i = 0; i < str.length; i++) {
|
|
||||||
hash = str.charCodeAt(i) + ((hash << 5) - hash);
|
|
||||||
}
|
|
||||||
const colors = [
|
|
||||||
"#6366f1",
|
|
||||||
"#8b5cf6",
|
|
||||||
"#a855f7",
|
|
||||||
"#d946ef",
|
|
||||||
"#ec4899",
|
|
||||||
"#f43f5e",
|
|
||||||
"#ef4444",
|
|
||||||
"#f97316",
|
|
||||||
"#eab308",
|
|
||||||
"#84cc16",
|
|
||||||
"#22c55e",
|
|
||||||
"#14b8a6",
|
|
||||||
"#06b6d4",
|
|
||||||
"#0ea5e9",
|
|
||||||
"#3b82f6",
|
|
||||||
];
|
|
||||||
return colors[Math.abs(hash) % colors.length];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets initials from email
|
|
||||||
*/
|
|
||||||
function getInitials(email: string): string {
|
|
||||||
const name = email.split("@")[0];
|
|
||||||
const parts = name.split(/[._-]/);
|
|
||||||
if (parts.length >= 2) {
|
|
||||||
return (parts[0][0] + parts[1][0]).toUpperCase();
|
|
||||||
}
|
|
||||||
return name.slice(0, 2).toUpperCase();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* User avatar component - shows image if available, otherwise falls back to initials
|
* User avatar component - shows image if available, otherwise falls back to initials
|
||||||
*/
|
*/
|
||||||
|
|
@ -180,8 +141,8 @@ export function SidebarUserProfile({
|
||||||
const isDesktopViewport = useMediaQuery("(min-width: 768px)");
|
const isDesktopViewport = useMediaQuery("(min-width: 768px)");
|
||||||
const { os, primary } = usePrimaryDownload();
|
const { os, primary } = usePrimaryDownload();
|
||||||
const [isLoggingOut, setIsLoggingOut] = useState(false);
|
const [isLoggingOut, setIsLoggingOut] = useState(false);
|
||||||
const bgColor = stringToColor(user.email);
|
const bgColor = getUserAvatarColor(user.email);
|
||||||
const initials = getInitials(user.email);
|
const initials = getUserInitials(user.email);
|
||||||
const displayName = user.name || user.email.split("@")[0];
|
const displayName = user.name || user.email.split("@")[0];
|
||||||
const downloadUrl = primary?.url ?? GITHUB_RELEASES_URL;
|
const downloadUrl = primary?.url ?? GITHUB_RELEASES_URL;
|
||||||
const downloadLabel = t("download_for_os", { os });
|
const downloadLabel = t("download_for_os", { os });
|
||||||
|
|
|
||||||
|
|
@ -251,7 +251,7 @@ export function UserSettingsPanel({
|
||||||
<div className="min-w-0 flex-1">
|
<div className="min-w-0 flex-1">
|
||||||
<div className="hidden md:block">
|
<div className="hidden md:block">
|
||||||
<h2 className="text-lg font-semibold">{selectedLabel}</h2>
|
<h2 className="text-lg font-semibold">{selectedLabel}</h2>
|
||||||
<Separator className="mt-4" />
|
<Separator className="mt-4 bg-border" />
|
||||||
</div>
|
</div>
|
||||||
<div className="min-w-0 pt-4 md:max-w-3xl">
|
<div className="min-w-0 pt-4 md:max-w-3xl">
|
||||||
{selectedTab === "profile" && <ProfileContent />}
|
{selectedTab === "profile" && <ProfileContent />}
|
||||||
|
|
|
||||||
34
surfsense_web/lib/user-avatar.ts
Normal file
34
surfsense_web/lib/user-avatar.ts
Normal file
|
|
@ -0,0 +1,34 @@
|
||||||
|
const USER_AVATAR_COLORS = [
|
||||||
|
"#6366f1",
|
||||||
|
"#8b5cf6",
|
||||||
|
"#a855f7",
|
||||||
|
"#d946ef",
|
||||||
|
"#ec4899",
|
||||||
|
"#f43f5e",
|
||||||
|
"#ef4444",
|
||||||
|
"#f97316",
|
||||||
|
"#eab308",
|
||||||
|
"#84cc16",
|
||||||
|
"#22c55e",
|
||||||
|
"#14b8a6",
|
||||||
|
"#06b6d4",
|
||||||
|
"#0ea5e9",
|
||||||
|
"#3b82f6",
|
||||||
|
];
|
||||||
|
|
||||||
|
export function getUserAvatarColor(email: string): string {
|
||||||
|
let hash = 0;
|
||||||
|
for (let i = 0; i < email.length; i++) {
|
||||||
|
hash = email.charCodeAt(i) + ((hash << 5) - hash);
|
||||||
|
}
|
||||||
|
return USER_AVATAR_COLORS[Math.abs(hash) % USER_AVATAR_COLORS.length];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getUserInitials(email: string): string {
|
||||||
|
const name = email.split("@")[0];
|
||||||
|
const parts = name.split(/[._-]/);
|
||||||
|
if (parts.length >= 2 && parts[0]?.[0] && parts[1]?.[0]) {
|
||||||
|
return (parts[0][0] + parts[1][0]).toUpperCase();
|
||||||
|
}
|
||||||
|
return name.slice(0, 2).toUpperCase();
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue