diff --git a/src/features/follow-user/ui/FollowButton.tsx b/src/features/follow-user/ui/FollowButton.tsx new file mode 100644 index 0000000..db1f412 --- /dev/null +++ b/src/features/follow-user/ui/FollowButton.tsx @@ -0,0 +1,64 @@ +import * as React from 'react' + +import { Button } from '@/shared/ui/button' +import { cn } from '@/shared/lib/utils' + +type FollowButtonProps = { + className?: string + defaultIsFollowing?: boolean + onToggle?: (nextIsFollowing: boolean) => Promise | void +} + +const FOLLOW_BUTTON_LABEL = { + follow: '팔로우', + following: '팔로잉', +} + +const FOLLOW_BUTTON_PRIMARY_CLASSNAME = + 'bg-[#0095f6] text-white hover:bg-[#0095f6]/90' + +export function FollowButton({ + className, + defaultIsFollowing = false, + onToggle, +}: FollowButtonProps) { + const [isFollowing, setIsFollowing] = React.useState(defaultIsFollowing) + const [isPending, startTransition] = React.useTransition() + + const handleClick = () => { + const nextIsFollowing = !isFollowing + + startTransition(() => { + void (async () => { + try { + await onToggle?.(nextIsFollowing) + setIsFollowing(nextIsFollowing) + } catch { + return + } + })() + }) + } + + const label = isFollowing + ? FOLLOW_BUTTON_LABEL.following + : FOLLOW_BUTTON_LABEL.follow + + return ( + + ) +} diff --git a/src/shared/lib/numberFormat.ts b/src/shared/lib/numberFormat.ts new file mode 100644 index 0000000..0b94491 --- /dev/null +++ b/src/shared/lib/numberFormat.ts @@ -0,0 +1,5 @@ +const LOCALE_KO_KR = 'ko-KR' + +export function numberFormat(value: number, locale: string = LOCALE_KO_KR) { + return value.toLocaleString(locale) +} diff --git a/src/widgets/profile-header/ui/ProfileAvatar.tsx b/src/widgets/profile-header/ui/ProfileAvatar.tsx new file mode 100644 index 0000000..4198060 --- /dev/null +++ b/src/widgets/profile-header/ui/ProfileAvatar.tsx @@ -0,0 +1,34 @@ +import { cn } from '@/shared/lib/utils' + +import { AVATAR_SIZE_CLASSNAME } from './constants' + +type ProfileAvatarProps = { + avatarUrl?: string | null + nickname: string +} + +export function ProfileAvatar({ avatarUrl, nickname }: ProfileAvatarProps) { + const initial = nickname.trim().slice(0, 1).toUpperCase() + + if (avatarUrl) { + return ( + {`${nickname} + ) + } + + return ( +
+ {initial || '?'} +
+ ) +} diff --git a/src/widgets/profile-header/ui/ProfileHeader.tsx b/src/widgets/profile-header/ui/ProfileHeader.tsx new file mode 100644 index 0000000..2045b13 --- /dev/null +++ b/src/widgets/profile-header/ui/ProfileHeader.tsx @@ -0,0 +1,61 @@ +import { FollowButton } from '@/features/follow-user/ui/FollowButton' +import { cn } from '@/shared/lib/utils' +import { PROFILE_CONTAINER_CLASSNAME } from './constants' +import { ProfileAvatar } from './ProfileAvatar' +import { ProfileStat } from './ProfileStat' + +interface ProfileHeaderProps { + className?: string + avatarUrl?: string | null + nickname: string + bio?: string | null + postsCount: number + followersCount: number + followingCount: number + defaultIsFollowing?: boolean + onFollowToggle?: (nextIsFollowing: boolean) => Promise | void +} + +export function ProfileHeader({ + className, + avatarUrl, + nickname, + bio, + postsCount, + followersCount, + followingCount, + defaultIsFollowing = false, + onFollowToggle, +}: ProfileHeaderProps) { + return ( +
+
+
+ +
+ +
+
+

{nickname}

+ +
+ +
+ + + +
+ + {bio ? ( +

+ {bio} +

+ ) : null} +
+
+
+ ) +} diff --git a/src/widgets/profile-header/ui/ProfileStat.tsx b/src/widgets/profile-header/ui/ProfileStat.tsx new file mode 100644 index 0000000..0ba24aa --- /dev/null +++ b/src/widgets/profile-header/ui/ProfileStat.tsx @@ -0,0 +1,15 @@ +import { numberFormat } from '@/shared/lib/numberFormat' + +type ProfileStatProps = { + label: string + value: number +} + +export function ProfileStat({ label, value }: ProfileStatProps) { + return ( +
+ {numberFormat(value)} + {label} +
+ ) +} diff --git a/src/widgets/profile-header/ui/constants.ts b/src/widgets/profile-header/ui/constants.ts new file mode 100644 index 0000000..2f69924 --- /dev/null +++ b/src/widgets/profile-header/ui/constants.ts @@ -0,0 +1,3 @@ +export const PROFILE_CONTAINER_CLASSNAME = + 'mx-auto w-full max-w-[935px] px-4 py-8' +export const AVATAR_SIZE_CLASSNAME = 'size-[150px]'