diff --git a/app/api/users/[id]/avatar/route.ts b/app/api/users/[id]/avatar/route.ts new file mode 100644 index 0000000..e600917 --- /dev/null +++ b/app/api/users/[id]/avatar/route.ts @@ -0,0 +1,28 @@ +import { prisma } from '@/lib/prisma' +import { requireUser } from '@/lib/auth' + +export async function GET(_request: Request, { params }: { params: Promise<{ id: string }> }) { + try { + await requireUser() + } catch { + return new Response(null, { status: 401 }) + } + + const { id } = await params + + const user = await prisma.user.findUnique({ + where: { id }, + select: { avatar_data: true }, + }) + + if (!user?.avatar_data) { + return new Response(null, { status: 404 }) + } + + return new Response(user.avatar_data, { + headers: { + 'Content-Type': 'image/webp', + 'Cache-Control': 'private, max-age=3600', + }, + }) +} diff --git a/components/shared/user-avatar.tsx b/components/shared/user-avatar.tsx new file mode 100644 index 0000000..d7ca849 --- /dev/null +++ b/components/shared/user-avatar.tsx @@ -0,0 +1,33 @@ +'use client' + +import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar' +import { cn } from '@/lib/utils' + +type AvatarSize = 'xs' | 'sm' | 'md' | 'lg' + +const SIZE_CLASSES: Record = { + xs: 'size-6 text-[10px]', + sm: 'size-8 text-xs', + md: 'size-10 text-sm', + lg: 'size-12 text-base', +} + +interface UserAvatarProps { + userId: string + username: string + size?: AvatarSize + className?: string +} + +export function UserAvatar({ userId, username, size = 'md', className }: UserAvatarProps) { + const initials = username.slice(0, 2).toUpperCase() + + return ( + + + + {initials} + + + ) +} diff --git a/components/ui/avatar.tsx b/components/ui/avatar.tsx new file mode 100644 index 0000000..e4fed86 --- /dev/null +++ b/components/ui/avatar.tsx @@ -0,0 +1,109 @@ +"use client" + +import * as React from "react" +import { Avatar as AvatarPrimitive } from "@base-ui/react/avatar" + +import { cn } from "@/lib/utils" + +function Avatar({ + className, + size = "default", + ...props +}: AvatarPrimitive.Root.Props & { + size?: "default" | "sm" | "lg" +}) { + return ( + + ) +} + +function AvatarImage({ className, ...props }: AvatarPrimitive.Image.Props) { + return ( + + ) +} + +function AvatarFallback({ + className, + ...props +}: AvatarPrimitive.Fallback.Props) { + return ( + + ) +} + +function AvatarBadge({ className, ...props }: React.ComponentProps<"span">) { + return ( + svg]:hidden", + "group-data-[size=default]/avatar:size-2.5 group-data-[size=default]/avatar:[&>svg]:size-2", + "group-data-[size=lg]/avatar:size-3 group-data-[size=lg]/avatar:[&>svg]:size-2", + className + )} + {...props} + /> + ) +} + +function AvatarGroup({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function AvatarGroupCount({ + className, + ...props +}: React.ComponentProps<"div">) { + return ( +
svg]:size-4 group-has-data-[size=lg]/avatar-group:[&>svg]:size-5 group-has-data-[size=sm]/avatar-group:[&>svg]:size-3", + className + )} + {...props} + /> + ) +} + +export { + Avatar, + AvatarImage, + AvatarFallback, + AvatarGroup, + AvatarGroupCount, + AvatarBadge, +}