aboutsummaryrefslogtreecommitdiffstats
path: root/apps/web/components/ui
diff options
context:
space:
mode:
authorMohamed Bassem <me@mbassem.com>2025-12-24 12:18:08 +0200
committerGitHub <noreply@github.com>2025-12-24 10:18:08 +0000
commit314c363e5ca69a50626650ade8968feec583e5ce (patch)
tree2251691c2a79598b50b4417ee5632b602e5faf78 /apps/web/components/ui
parent3408e6e4854dc79b963eef455e9a69231de3cd28 (diff)
downloadkarakeep-314c363e5ca69a50626650ade8968feec583e5ce.tar.zst
feat: add support for user avatars (#2296)
* feat: add support for user avatars * more fixes * more fixes * more fixes * more fixes
Diffstat (limited to 'apps/web/components/ui')
-rw-r--r--apps/web/components/ui/avatar.tsx49
-rw-r--r--apps/web/components/ui/user-avatar.tsx52
2 files changed, 101 insertions, 0 deletions
diff --git a/apps/web/components/ui/avatar.tsx b/apps/web/components/ui/avatar.tsx
new file mode 100644
index 00000000..7afec626
--- /dev/null
+++ b/apps/web/components/ui/avatar.tsx
@@ -0,0 +1,49 @@
+"use client";
+
+import * as React from "react";
+import { cn } from "@/lib/utils";
+import * as AvatarPrimitive from "@radix-ui/react-avatar";
+
+const Avatar = React.forwardRef<
+ React.ElementRef<typeof AvatarPrimitive.Root>,
+ React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Root>
+>(({ className, ...props }, ref) => (
+ <AvatarPrimitive.Root
+ ref={ref}
+ className={cn(
+ "relative flex h-10 w-10 shrink-0 overflow-hidden rounded-full",
+ className,
+ )}
+ {...props}
+ />
+));
+Avatar.displayName = AvatarPrimitive.Root.displayName;
+
+const AvatarImage = React.forwardRef<
+ React.ElementRef<typeof AvatarPrimitive.Image>,
+ React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Image>
+>(({ className, ...props }, ref) => (
+ <AvatarPrimitive.Image
+ ref={ref}
+ className={cn("aspect-square h-full w-full", className)}
+ {...props}
+ />
+));
+AvatarImage.displayName = AvatarPrimitive.Image.displayName;
+
+const AvatarFallback = React.forwardRef<
+ React.ElementRef<typeof AvatarPrimitive.Fallback>,
+ React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Fallback>
+>(({ className, ...props }, ref) => (
+ <AvatarPrimitive.Fallback
+ ref={ref}
+ className={cn(
+ "flex h-full w-full items-center justify-center rounded-full bg-muted",
+ className,
+ )}
+ {...props}
+ />
+));
+AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName;
+
+export { Avatar, AvatarImage, AvatarFallback };
diff --git a/apps/web/components/ui/user-avatar.tsx b/apps/web/components/ui/user-avatar.tsx
new file mode 100644
index 00000000..4ebb6ec3
--- /dev/null
+++ b/apps/web/components/ui/user-avatar.tsx
@@ -0,0 +1,52 @@
+"use client";
+
+import { useMemo } from "react";
+import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
+import { cn } from "@/lib/utils";
+
+import { getAssetUrl } from "@karakeep/shared/utils/assetUtils";
+
+interface UserAvatarProps {
+ image?: string | null;
+ name?: string | null;
+ className?: string;
+ imgClassName?: string;
+ fallbackClassName?: string;
+ fallback?: React.ReactNode;
+}
+
+const isExternalUrl = (value: string) =>
+ value.startsWith("http://") || value.startsWith("https://");
+
+export function UserAvatar({
+ image,
+ name,
+ className,
+ imgClassName,
+ fallbackClassName,
+ fallback,
+}: UserAvatarProps) {
+ const avatarUrl = useMemo(() => {
+ if (!image) {
+ return null;
+ }
+ return isExternalUrl(image) ? image : getAssetUrl(image);
+ }, [image]);
+
+ const fallbackContent = fallback ?? name?.charAt(0) ?? "U";
+
+ return (
+ <Avatar className={className}>
+ {avatarUrl && (
+ <AvatarImage
+ src={avatarUrl}
+ alt={name ?? "User"}
+ className={cn("object-cover", imgClassName)}
+ />
+ )}
+ <AvatarFallback className={cn("text-sm font-medium", fallbackClassName)}>
+ {fallbackContent}
+ </AvatarFallback>
+ </Avatar>
+ );
+}