Skip to content

Commit

Permalink
feat(dashboard): Avatar picker component
Browse files Browse the repository at this point in the history
  • Loading branch information
desiprisg committed Oct 24, 2024
1 parent f8fcc82 commit 5e8137b
Show file tree
Hide file tree
Showing 5 changed files with 945 additions and 704 deletions.
1 change: 1 addition & 0 deletions apps/dashboard/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
"@hookform/resolvers": "^3.9.0",
"@novu/react": "^2.3.0",
"@novu/shared": "workspace:*",
"@radix-ui/react-avatar": "^1.1.1",
"@radix-ui/react-dialog": "^1.1.2",
"@radix-ui/react-dropdown-menu": "^2.1.1",
"@radix-ui/react-icons": "^1.3.0",
Expand Down
38 changes: 38 additions & 0 deletions apps/dashboard/src/components/primitives/avatar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import * as React from 'react';
import * as AvatarPrimitive from '@radix-ui/react-avatar';

import { cn } from '@/utils/ui';

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('bg-muted flex h-full w-full items-center justify-center rounded-full', className)}
{...props}
/>
));
AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName;

export { Avatar, AvatarImage, AvatarFallback };
90 changes: 90 additions & 0 deletions apps/dashboard/src/components/primitives/form/avatar-picker.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
'use client';

import { Avatar, AvatarImage } from '@/components/primitives/avatar';
import { Button } from '@/components/primitives/button';
import { FormControl, FormMessage } from '@/components/primitives/form/form';
import { Input, InputField } from '@/components/primitives/input';
import { Label } from '@/components/primitives/label';
import { Popover, PopoverContent, PopoverTrigger } from '@/components/primitives/popover';
import { Separator } from '@/components/primitives/separator';
import TextSeparator from '@/components/primitives/text-separator';
import { useState, forwardRef } from 'react';
import { RiEdit2Line, RiImageEditFill } from 'react-icons/ri';

const predefinedAvatars = [
'https://images.unsplash.com/photo-1719937206255-cc337bccfc7d?q=80&w=2970&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDF8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D',
'https://images.unsplash.com/photo-1719937206255-cc337bccfc7d?q=80&w=2970&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDF8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D',
'https://images.unsplash.com/photo-1719937206255-cc337bccfc7d?q=80&w=2970&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDF8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D',
'https://images.unsplash.com/photo-1719937206255-cc337bccfc7d?q=80&w=2970&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDF8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D',
'https://images.unsplash.com/photo-1719937206255-cc337bccfc7d?q=80&w=2970&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDF8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D',
'https://images.unsplash.com/photo-1719937206255-cc337bccfc7d?q=80&w=2970&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDF8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D',
'https://images.unsplash.com/photo-1719937206255-cc337bccfc7d?q=80&w=2970&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDF8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D',
'https://images.unsplash.com/photo-1719937206255-cc337bccfc7d?q=80&w=2970&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDF8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D',
'https://images.unsplash.com/photo-1719937206255-cc337bccfc7d?q=80&w=2970&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDF8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D',
'https://images.unsplash.com/photo-1719937206255-cc337bccfc7d?q=80&w=2970&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDF8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D',
'https://images.unsplash.com/photo-1719937206255-cc337bccfc7d?q=80&w=2970&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDF8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D',
'https://images.unsplash.com/photo-1719937206255-cc337bccfc7d?q=80&w=2970&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDF8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D',
];

type AvatarPickerProps = React.InputHTMLAttributes<HTMLInputElement>;

const AvatarPicker = forwardRef<HTMLInputElement, AvatarPickerProps>(({ id, ...props }, ref) => {
const [isOpen, setIsOpen] = useState(false);

const handlePredefinedAvatarClick = (url: string) => {
props.onChange?.({ target: { value: url } } as React.ChangeEvent<HTMLInputElement>);
setIsOpen(false);
};

return (
<div className="space-y-2">
<Popover modal={true} open={isOpen} onOpenChange={setIsOpen}>
<PopoverTrigger asChild>
<Button variant="outline" size="icon" className="text-foreground-600 size-10">
{props.value ? (
<Avatar>
<AvatarImage src={props.value as string} />
</Avatar>
) : (
<RiImageEditFill className="size-5" />
)}
</Button>
</PopoverTrigger>
<PopoverContent className="w-[300px] p-4">
<div className="space-y-4">
<div className="space-y-2">
<div className="flex items-center gap-2 text-sm font-medium leading-none">
<RiEdit2Line className="size-4" /> Customize avatar
</div>
<Separator />
<div className="space-y-1">
<Label>Avatar URL</Label>
<FormControl>
<InputField>
<Input type="url" id={id} placeholder="Enter avatar URL" ref={ref} {...props} />
</InputField>
</FormControl>
<FormMessage />
</div>
</div>
<TextSeparator text="or" />
<div className="grid grid-cols-6 gap-4">
{predefinedAvatars.map((url, index) => (
<Button key={index} variant="ghost" className="p-0" onClick={() => handlePredefinedAvatarClick(url)}>
<Avatar>
<AvatarImage src={url} />
</Avatar>
</Button>
))}
</div>
</div>
</PopoverContent>
</Popover>
<FormMessage />
</div>
);
});

AvatarPicker.displayName = 'AvatarPicker';

export default AvatarPicker;
20 changes: 20 additions & 0 deletions apps/dashboard/src/components/primitives/text-separator.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { Separator } from '@/components/primitives/separator';
import { cn } from '@/utils/ui';
import * as React from 'react';

interface TextSeparatorProps extends React.HTMLAttributes<HTMLDivElement> {
text: string;
}

export default function TextSeparator({ text, className, ...props }: TextSeparatorProps) {
return (
<div className={cn('relative', className)} {...props}>
<div className="absolute inset-0 flex items-center">
<Separator className="w-full" />
</div>
<div className="relative flex justify-center text-xs uppercase">
<span className="bg-background text-foreground-400 px-2">{text}</span>
</div>
</div>
);
}
Loading

0 comments on commit 5e8137b

Please sign in to comment.