Skip to content

Commit

Permalink
feat(ui): change avatar
Browse files Browse the repository at this point in the history
  • Loading branch information
wwayne committed Mar 28, 2024
1 parent 3c32788 commit 773eb1c
Show file tree
Hide file tree
Showing 7 changed files with 181 additions and 12 deletions.
57 changes: 52 additions & 5 deletions ee/tabby-ui/app/(dashboard)/profile/components/avatar.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,64 @@
import NiceAvatar, { genConfig } from 'react-nice-avatar'
'use client'

import { ChangeEvent } from 'react'

import { cn } from '@/lib/utils'
import { useMe } from '@/lib/hooks/use-me'
import { graphql } from '@/lib/gql/generates'
import { useMutation } from '@/lib/tabby/gql'
import { toast } from 'sonner'

import { buttonVariants } from "@/components/ui/button"
import { UserAvatar, mutateAvatar } from '@/components/user-avatar'

const uploadUserAvatarMutation = graphql(/* GraphQL */ `
mutation uploadUserAvatarBase64($id: ID!, $avatarBase64: String!) {
uploadUserAvatarBase64(id: $id, avatarBase64: $avatarBase64)
}
`)

export const Avatar = () => {
const [{ data }] = useMe()

const uploadUserAvatar = useMutation(uploadUserAvatarMutation)
if (!data?.me?.email) return null

const config = genConfig(data?.me?.email)
const onUploadAvatar = (e: ChangeEvent<HTMLInputElement>) => {
const file = e.target.files ? e.target.files[0] : null;
if (file) {
const reader = new FileReader();

reader.onloadend = async () => {
try {
const imageString = reader.result as string;
const mimeHeaderMatcher = new RegExp("^data:image/.+;base64,")
const avatarBase64 = imageString.replace(mimeHeaderMatcher, "")

const response = await uploadUserAvatar({
avatarBase64,
id: data.me.id
})
if (response?.error) throw response.error
if (response?.data?.uploadUserAvatarBase64 === false) throw new Error('Upload failed')
mutateAvatar(data.me.id)
toast.success('Avatar uploaded successfully.')
} catch (err: any) {
toast.error(err.message || 'Upload failed')
}
};

reader.readAsDataURL(file);
}
}

return (
<div className="flex h-16 w-16 rounded-full border">
<NiceAvatar className="w-full" {...config} />
<div className="flex items-center">
<UserAvatar className="h-16 w-16 border" />

<div className="ml-3">
<label htmlFor="avatar-file" className={cn("relative cursor-pointer", buttonVariants({ variant: 'outline' }))}>Upload new picture</label>
<input id="avatar-file" type="file" accept='image/*' className="hidden" onChange={onUploadAvatar} />
<p className="mt-1.5 text-xs text-muted-foreground">Recommended: Square JPG, PNG, at least 1,000 pixels per side.</p>
</div>
</div>
)
}
2 changes: 1 addition & 1 deletion ee/tabby-ui/app/(dashboard)/profile/components/profile.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ export default function Profile() {
<ProfileCard
title="Your Avatar"
description="This is your avatar image."
footer="The avatar customization feature will be available in a future release."
footerClassname="pb-0"
>
<Avatar />
</ProfileCard>
Expand Down
50 changes: 50 additions & 0 deletions ee/tabby-ui/components/ui/avatar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
"use client"

import * as React from "react"
import * as AvatarPrimitive from "@radix-ui/react-avatar"

import { cn } from "@/lib/utils"

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 }
64 changes: 64 additions & 0 deletions ee/tabby-ui/components/user-avatar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
'use client'

import NiceAvatar, { genConfig } from 'react-nice-avatar'
import { mutate } from 'swr';
import useSWRImmutable from 'swr/immutable'

import { useMe } from '@/lib/hooks/use-me'
import fetcher from '@/lib/tabby/fetcher'
import { cn } from '@/lib/utils';

import {
Avatar as AvatarComponent,
AvatarFallback,
AvatarImage,
} from "@/components/ui/avatar"
import { Skeleton } from '@/components/ui/skeleton'

export function UserAvatar ({
className
}: {
className?: string;
}) {
const [{ data }] = useMe()
const avatarUrl = !data?.me?.email ? null : `/avatar/${data.me.id}`
const {
data: avatarImageSrc,
isLoading
} = useSWRImmutable(avatarUrl, (url: string) => {
return fetcher(url, {
responseFormatter: async response => {
if (!response.ok) return undefined
const blob = await response.blob()
const buffer = Buffer.from(await blob.arrayBuffer())
return `data:image/png;base64,${buffer.toString('base64')}`
}
})
})

if (!data?.me?.email) return null

if (isLoading) {
return (
<Skeleton className={cn('h-16 w-16 rounded-full', className)} />
)
}

if (!avatarImageSrc) {
const config = genConfig(data.me.email)
return (
<NiceAvatar className={cn("h-16 w-16", className)} {...config} />
)
}

return (
<AvatarComponent className={cn("h-16 w-16", className)}>
<AvatarImage src={avatarImageSrc} alt={data.me.email} className="object-cover" />
<AvatarFallback>{data.me?.email.substring(0, 2)}</AvatarFallback>
</AvatarComponent>
)
}

export const mutateAvatar = (userId: string) => {
mutate(`/avatar/${userId}`)
}
8 changes: 2 additions & 6 deletions ee/tabby-ui/components/user-panel.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import React from 'react'
import NiceAvatar, { genConfig } from 'react-nice-avatar'

import { useMe } from '@/lib/hooks/use-me'
import { useIsChatEnabled } from '@/lib/hooks/use-server-info'
Expand All @@ -12,6 +11,7 @@ import {
DropdownMenuSeparator,
DropdownMenuTrigger
} from '@/components/ui/dropdown-menu'
import { UserAvatar } from '@/components/user-avatar'

import {
IconBackpack,
Expand Down Expand Up @@ -39,14 +39,10 @@ export default function UserPanel() {
return
}

const config = genConfig(user.email)

return (
<DropdownMenu>
<DropdownMenuTrigger>
<span className="flex h-10 w-10 rounded-full border">
<NiceAvatar className="w-full" {...config} />
</span>
<UserAvatar className="h-10 w-10 border" />
</DropdownMenuTrigger>
<DropdownMenuContent collisionPadding={{ right: 16 }}>
<DropdownMenuLabel>{user.email}</DropdownMenuLabel>
Expand Down
1 change: 1 addition & 0 deletions ee/tabby-ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
"@curvenote/ansi-to-react": "^7.0.0",
"@hookform/resolvers": "^3.3.2",
"@radix-ui/react-alert-dialog": "1.0.4",
"@radix-ui/react-avatar": "^1.0.4",
"@radix-ui/react-checkbox": "^1.0.4",
"@radix-ui/react-collapsible": "^1.0.3",
"@radix-ui/react-dialog": "1.0.4",
Expand Down
11 changes: 11 additions & 0 deletions ee/tabby-ui/yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -1874,6 +1874,17 @@
"@babel/runtime" "^7.13.10"
"@radix-ui/react-primitive" "1.0.3"

"@radix-ui/react-avatar@^1.0.4":
version "1.0.4"
resolved "https://registry.yarnpkg.com/@radix-ui/react-avatar/-/react-avatar-1.0.4.tgz#de9a5349d9e3de7bbe990334c4d2011acbbb9623"
integrity sha512-kVK2K7ZD3wwj3qhle0ElXhOjbezIgyl2hVvgwfIdexL3rN6zJmy5AqqIf+D31lxVppdzV8CjAfZ6PklkmInZLw==
dependencies:
"@babel/runtime" "^7.13.10"
"@radix-ui/react-context" "1.0.1"
"@radix-ui/react-primitive" "1.0.3"
"@radix-ui/react-use-callback-ref" "1.0.1"
"@radix-ui/react-use-layout-effect" "1.0.1"

"@radix-ui/react-checkbox@^1.0.4":
version "1.0.4"
resolved "https://registry.yarnpkg.com/@radix-ui/react-checkbox/-/react-checkbox-1.0.4.tgz#98f22c38d5010dd6df4c5744cac74087e3275f4b"
Expand Down

0 comments on commit 773eb1c

Please sign in to comment.