From bb8d077c3876a1913d277faec859862908741ae9 Mon Sep 17 00:00:00 2001 From: kunlee Date: Fri, 29 Sep 2023 23:28:35 +0900 Subject: [PATCH] feat(web): profile page --- apps/web/src/app/profile/[id]/page.tsx | 19 +++++ apps/web/src/components/profile/Profile.tsx | 51 ++++++++++++ .../profile/mock/profileMockData.ts | 33 ++++++++ apps/web/src/components/profile/useProfile.ts | 39 ++++++++++ .../components/profile/useProfileRouter.ts | 77 +++++++++++++++++++ 5 files changed, 219 insertions(+) create mode 100644 apps/web/src/app/profile/[id]/page.tsx create mode 100644 apps/web/src/components/profile/Profile.tsx create mode 100644 apps/web/src/components/profile/mock/profileMockData.ts create mode 100644 apps/web/src/components/profile/useProfile.ts create mode 100644 apps/web/src/components/profile/useProfileRouter.ts diff --git a/apps/web/src/app/profile/[id]/page.tsx b/apps/web/src/app/profile/[id]/page.tsx new file mode 100644 index 0000000..bc5f364 --- /dev/null +++ b/apps/web/src/app/profile/[id]/page.tsx @@ -0,0 +1,19 @@ +import Header from '@/components/common/Header' +import FeedList from '@/components/main/FeedList' +import { listMockData } from '@/components/main/list/mock/listMockData' +import { profileMockData } from '@/components/profile/mock/profileMockData' +import Profile from '@/components/profile/Profile' + +export default function ProfilePage() { + const profileResult = profileMockData + const feedsResult = listMockData + + return ( +
+
+ + + +
+ ) +} diff --git a/apps/web/src/components/profile/Profile.tsx b/apps/web/src/components/profile/Profile.tsx new file mode 100644 index 0000000..3764a42 --- /dev/null +++ b/apps/web/src/components/profile/Profile.tsx @@ -0,0 +1,51 @@ +'use client' + +import { IconChip } from '@80000coding/ui' +import classNames from 'classnames' +import Link from 'next/link' + +import { ProfileType } from './mock/profileMockData' +import useProfile from './useProfile' +import useProfileRouter from './useProfileRouter' +import ImageProfile from '../shared/ImageProfile' + +export default function Profile({ data }: { data: ProfileType }) { + const { profile, isMyProfile } = useProfile(data) + const { moveToParams, profileTabs } = useProfileRouter() + const { displayFeedCount, displayProjectCount, organizations, profile_image_url, user_name } = profile + + return ( +
+
+ +
+
+ {user_name} + {isMyProfile && ( + + 설정 + + )} +
+
+ 게시글 {displayFeedCount} + 프로젝트 {displayProjectCount} +
+
+
+
+ {organizations.map(({ icon, name }) => ( + + ))} +
+
+ {profileTabs.map(({ icon: Icon, label, url, selected, selectedIcon: SelectedIcon }) => ( +
moveToParams({ tab: url })} className='flex items-center gap-x-1'> + {selected ? : } + {label} +
+ ))} +
+
+ ) +} diff --git a/apps/web/src/components/profile/mock/profileMockData.ts b/apps/web/src/components/profile/mock/profileMockData.ts new file mode 100644 index 0000000..5aa8443 --- /dev/null +++ b/apps/web/src/components/profile/mock/profileMockData.ts @@ -0,0 +1,33 @@ +import { CategoryIconNames } from '@80000coding/ui' + +export type OrganiztionType = { + name: string + icon: CategoryIconNames +} + +export type ProfileType = { + profile_image_url: string + user_id: number + user_name: string + feeds_count: number + projects_count: number + organizations: OrganiztionType[] +} + +export const profileMockData: ProfileType = { + profile_image_url: 'https://picsum.photos/200/300', + user_id: 1, + user_name: 'kunlee', + feeds_count: 13, + projects_count: 10, + organizations: [ + { + name: '42Seoul', + icon: '42Seoul-rounded', + }, + { + name: 'Android', + icon: 'Android-rounded', + }, + ], +} diff --git a/apps/web/src/components/profile/useProfile.ts b/apps/web/src/components/profile/useProfile.ts new file mode 100644 index 0000000..0a6124c --- /dev/null +++ b/apps/web/src/components/profile/useProfile.ts @@ -0,0 +1,39 @@ +import { useState } from 'react' + +import { ProfileType } from './mock/profileMockData' + +export type Refined = { + /** 원본데이터 */ + _raw: T +} & T + +export type RefinedProfileType = Refined & { + /** 게시글 수 9999 -> 9,999 */ + displayFeedCount: string + /** 프로젝트 수 9999 -> 9,999 */ + displayProjectCount: string +} + +function refine(item: ProfileType): RefinedProfileType { + const { feeds_count, projects_count } = item + + return { + _raw: item, + ...item, + displayFeedCount: feeds_count.toLocaleString(), + displayProjectCount: projects_count.toLocaleString(), + } +} + +export default function useProfile(data: ProfileType) { + /** TODO: 로그인된 유저 아이디 context로 가져와서 대체 */ + const loggedInUserId = 1 + const [profile, setProfile] = useState(refine(data)) + + const isMyProfile = data.user_id === loggedInUserId + + return { + profile, + isMyProfile, + } +} diff --git a/apps/web/src/components/profile/useProfileRouter.ts b/apps/web/src/components/profile/useProfileRouter.ts new file mode 100644 index 0000000..cf8011e --- /dev/null +++ b/apps/web/src/components/profile/useProfileRouter.ts @@ -0,0 +1,77 @@ +import { + StaticLikeIcon, + StaticLikeWhiteIcon, + StaticPostIcon, + StaticPostWhiteIcon, + StaticProjectIcon, + StaticProjectWhiteIcon, +} from '@80000coding/web-icons' +import { usePathname, useRouter, useSearchParams } from 'next/navigation' +import { useCallback, useMemo } from 'react' + +import { as } from '@/lib/utils/as' + +const tabs = [ + { + label: '게시글', + url: 'feeds', + selectedIcon: StaticPostIcon, + icon: StaticPostWhiteIcon, + selected: false, + }, + { + label: '프로젝트', + url: 'projects', + selectedIcon: StaticProjectIcon, + icon: StaticProjectWhiteIcon, + selected: false, + }, + { + label: '좋아요', + url: 'likes', + selectedIcon: StaticLikeIcon, + icon: StaticLikeWhiteIcon, + selected: false, + }, +] + +export default function useProfileRouter() { + const router = useRouter() + const pathname = usePathname() + const searchParams = useSearchParams() + + const query = as(searchParams.get('tab') || '') + const profileTabs = useMemo( + () => + tabs.map((tab, index) => + (!query && index === 0) || tab.url === query + ? { + ...tab, + selected: true, + } + : tab, + ), + [query], + ) + + const moveToParams = useCallback( + (params: { tab: string }) => { + const current = new URLSearchParams(Array.from(searchParams.entries())) + Object.entries(params).forEach(([key, value]) => { + current.set(key, value as string) + }) + + const newQueryString = current.toString() + const hasQueryString = newQueryString.length > 0 + const url = hasQueryString ? `${pathname}?${newQueryString}` : pathname + + router.push(url) + }, + [router, pathname, searchParams], + ) + + return { + moveToParams, + profileTabs, + } +}