diff --git a/next.config.ts b/next.config.ts index 810a8dd..9d5b0f1 100644 --- a/next.config.ts +++ b/next.config.ts @@ -8,6 +8,14 @@ const nextConfig: NextConfig = { fullUrl: true, }, }, + images: { + remotePatterns: [ + { + protocol: 'https', + hostname: 'images.unsplash.com', + }, + ], + }, }; export default nextConfig; diff --git a/src/components/ui/Avatar/Avatar.tsx b/src/components/ui/Avatar/Avatar.tsx new file mode 100644 index 0000000..b7eea99 --- /dev/null +++ b/src/components/ui/Avatar/Avatar.tsx @@ -0,0 +1,70 @@ +'use client'; + +import { DEFAULT_COLORS } from '@/constants/colors'; +import { cn, getColorByString } from '@/utils/helper'; +import { cva, VariantProps } from 'class-variance-authority'; +import Image from 'next/image'; +import { HTMLAttributes, useState } from 'react'; + +/** + * 커스터마이징 가능한 기본 공용 Avatar 컴포넌트입니다. + * + * ### Variants: + * - `size`: + * - `default`: width:38px, font-size:16px; + * - `md`: width:34px, font-size:16px; + * - `sm`: width:24px, font-size:12px; + * + * ### Default Variants (기본 스타일): + * - `size`: `default` + * + * + * @example + * + * + * 반응형으로 쓰고 싶을땐, classname을 추가하세요 + * + */ + +const avatarVariants = cva( + //prettier-ignore + 'relative inline-flex items-center justify-center aspect-square rounded-full overflow-hidden border-2 border-white leading-none', + { + variants: { + size: { + default: 'w-[38px] text-lg', + md: 'w-[34px] text-lg', + sm: 'w-6 text-xs', + }, + }, + + defaultVariants: { + size: 'default', + }, + }, +); + +interface AvatarProps extends HTMLAttributes, VariantProps { + email: string; + profileImageUrl?: string; +} + +export default function Avatar({ email, profileImageUrl, size, className, ...props }: AvatarProps) { + const [imgError, setImgError] = useState(false); + const colorCode = getColorByString(email, DEFAULT_COLORS); + const firstChar = email.charAt(0); + + const isFallback = !profileImageUrl || imgError; + + return ( +
+ {!isFallback ? ( + {email} setImgError(true)} /> + ) : ( + + {firstChar} + + )} +
+ ); +} diff --git a/src/components/ui/Avatar/StackAvatars.tsx b/src/components/ui/Avatar/StackAvatars.tsx new file mode 100644 index 0000000..df31494 --- /dev/null +++ b/src/components/ui/Avatar/StackAvatars.tsx @@ -0,0 +1,26 @@ +import Avatar from './Avatar'; + +interface StackAvatarsProps { + members: { email: string; profileImageUrl?: string }[]; + visibleCount: number; +} + +export default function StackAvatars({ members, visibleCount = 3 }: StackAvatarsProps) { + const chunkedMembers = members.slice(0, visibleCount); + const restMembersCount = members.length - visibleCount; + + return ( +
    + {chunkedMembers.map((member) => ( +
  • + +
  • + ))} + {restMembersCount > 0 && ( +
  • + +{restMembersCount} +
  • + )} +
+ ); +} diff --git a/src/constants/colors.ts b/src/constants/colors.ts new file mode 100644 index 0000000..7abcc32 --- /dev/null +++ b/src/constants/colors.ts @@ -0,0 +1,3 @@ +type HexColor = `#${string}`; + +export const DEFAULT_COLORS: HexColor[] = ['#7AC555', '#760DDE', '#FFA500', '#76A5EA', '#E876EA']; diff --git a/src/utils/helper.ts b/src/utils/helper.ts index 2819a83..b4de34b 100644 --- a/src/utils/helper.ts +++ b/src/utils/helper.ts @@ -4,3 +4,9 @@ import { twMerge } from 'tailwind-merge'; export function cn(...inputs: ClassValue[]) { return twMerge(clsx(inputs)); } + +export function getColorByString(value: string, colorArray: string[]) { + const charCode = value.toLowerCase().charCodeAt(0); + const index = charCode % colorArray.length; + return colorArray[index]; +}