diff --git a/src/bookmarks/ui/Detail/SkeletonBookmarkDetail.tsx b/src/bookmarks/ui/Detail/SkeletonBookmarkDetail.tsx new file mode 100644 index 00000000..39a153cc --- /dev/null +++ b/src/bookmarks/ui/Detail/SkeletonBookmarkDetail.tsx @@ -0,0 +1,143 @@ +/* eslint-disable no-irregular-whitespace */ +import getRem from '@/utils/getRem'; +import Button from '@/common-ui/Button'; +import styled from '@emotion/styled'; +import { theme } from '@/styles/theme'; + +import { skeletonAnimation1 } from '@/common-ui/utils/skeletonAnimations'; +import { ReactNode } from 'react'; +import Icon from '@/common-ui/assets/Icon'; + +const SkeletonBookmarkDetail = () => { + return ( + <> + + + + + + + ​ + + + + + + } + content={} + /> + } + content={} + /> + + + + ); +}; + +export default SkeletonBookmarkDetail; + +const BookMarkInfo = ({ + icon, + content, +}: { + icon: ReactNode; + content: ReactNode; +}) => { + return ( + + {icon} + + + {content} + + + ); +}; + +const Container = styled.article` + padding: ${getRem(0, 20)}; +`; + +const BookMarkImage = styled.div` + width: 100%; + height: ${getRem(247)}; + border-radius: ${getRem(0, 0, 32, 32)}; + background-color: ${theme.colors.grey800}; + ${skeletonAnimation1}; +`; + +const CategoryAndIconsWrapper = styled.div` + display: flex; + justify-content: space-between; + margin-top: ${getRem(15)}; +`; + +const CategoryButtonWrapper = styled.div` + width: ${getRem(154)}; +`; + +const CategoryButton = styled(Button)` + color: ${theme.colors.black}; +`; + +const IconWrapper = styled.div` + display: flex; +`; + +const LikeAndMessageIconWrapper = styled.div` + display: flex; + align-items: center; + column-gap: ${getRem(12)}; + width: 30%; + height: 1.7rem; + border-radius: ${getRem(5)}; + align-self: center; + ${skeletonAnimation1}; + background-color: ${theme.colors.grey800}; +`; + +const BookMarkInfoWrapper = styled.div` + padding: ${getRem(20)} 0px; +`; +const InfoRow = styled.div` + display: flex; + align-items: center; + column-gap: ${getRem(10)}; + width: 100%; + overflow: hidden; +`; + +const InfoTextWrapper = styled.div` + width: 100%; + display: flex; + height: 2rem; + align-items: center; + column-gap: 2rem; +`; + +const SkeletonTitleText = styled.div` + width: 100%; + height: 1.8rem; + background-color: ${theme.colors.grey800}; + margin-top: ${getRem(28)}; + border-radius: ${getRem(5)}; + ${skeletonAnimation1}; +`; + +const SkeletonSubText = styled.div` + width: 20%; + background-color: ${theme.colors.grey800}; + ${skeletonAnimation1}; + border-radius: ${getRem(5)}; +`; + +const SkeletonText = styled.div` + width: 50%; + height: 1.1rem; + background-color: ${theme.colors.grey800}; + ${skeletonAnimation1}; + border-radius: ${getRem(5)}; +`; diff --git a/src/bookmarks/ui/SkeletonBookmarkUserInfo.tsx b/src/bookmarks/ui/SkeletonBookmarkUserInfo.tsx new file mode 100644 index 00000000..9cab0561 --- /dev/null +++ b/src/bookmarks/ui/SkeletonBookmarkUserInfo.tsx @@ -0,0 +1,73 @@ +import SkeletonText from '@/common-ui/skeleton/SkeletonText'; +import { skeletonAnimation1 } from '@/common-ui/utils/skeletonAnimations'; +import { theme } from '@/styles/theme'; +import getRem from '@/utils/getRem'; +import styled from '@emotion/styled'; + +interface SkeletonBookmarkUserInfoProps { + isFriendPage?: { + isFollowing: boolean; + friendId: number; + memberId: number; + isBlocked: boolean; + }; +} + +const SkeletonBookmarkUserInfo = ({ + isFriendPage, +}: SkeletonBookmarkUserInfoProps) => { + return ( + + + + {!!isFriendPage && ( + + )} + {!isFriendPage && } + + {!!isFriendPage && ( + <> + + + )} + + ); +}; + +export default SkeletonBookmarkUserInfo; + +const StyleWrapper = styled.div` + display: flex; + align-items: center; + justify-content: space-between; +`; + +const UserBox = styled.div` + display: flex; + width: 4rem; + height: 4rem; + border-radius: 50%; + background-color: ${theme.colors.grey800}; + justify-content: center; + align-items: center; + flex-shrink: 0; + ${skeletonAnimation1}; +`; + +const TextWrapper = styled.div` + display: flex; + align-items: center; + column-gap: 0.5rem; + width: 100%; +`; + +const StyledButton = styled.div` + width: ${getRem(70)}; + height: 1.4rem; + font-size: ${getRem(14)}; + padding: ${getRem(4, 15)}; + border-radius: ${getRem(5)}; + background-color: ${theme.colors.grey800}; + font-weight: bold; + ${skeletonAnimation1} +`; diff --git a/src/comment/api/Comment.ts b/src/comment/api/Comment.ts index c4f6cdfe..f1ee44a5 100644 --- a/src/comment/api/Comment.ts +++ b/src/comment/api/Comment.ts @@ -45,6 +45,7 @@ export const useGETCommentListQuery = ({ userId }: GETCommentListRequest) => { refetchOnWindowFocus: false, retry: 0, enabled: !!userId, + suspense: true, }); }; diff --git a/src/comment/ui/bookmark/SkeletonCommentList.tsx b/src/comment/ui/bookmark/SkeletonCommentList.tsx new file mode 100644 index 00000000..696f22e6 --- /dev/null +++ b/src/comment/ui/bookmark/SkeletonCommentList.tsx @@ -0,0 +1,78 @@ +/* eslint-disable @typescript-eslint/no-empty-function */ +import styled from '@emotion/styled'; +import { theme } from '@/styles/theme'; +import Icon from '@/common-ui/assets/Icon'; +import getRem from '@/utils/getRem'; +import SkeletonText from '@/common-ui/skeleton/SkeletonText'; +import { skeletonAnimation2 } from '@/common-ui/utils/skeletonAnimations'; + +const SkeletonCommentList = () => { + return ( + + {[...Array(3)].map((_, index) => ( + + ))} + + ); +}; + +const CommentListWrapper = styled.div` + padding: 0 ${getRem(20)}; +`; + +const SkeletonCommentItem = () => { + return ( + + + + + + + {/*
+
*/} + + + + + + + + ); +}; + +export default SkeletonCommentList; + +const Container = styled.div` + display: grid; + flex-direction: column; + row-gap: 0.8rem; + padding: ${getRem(15, 20)}; + border-radius: ${getRem(7)}; + background-color: ${theme.colors.grey700}; + ${skeletonAnimation2} + margin-bottom: 1rem; + :nth-last-of-type(1) { + margin-bottom: 5rem; + } +`; + +const CommentHeader = styled.div` + display: flex; + justify-content: flex-end; + align-items: center; + height: 1.5rem; +`; + +const NicknameTextAndIconWrapper = styled.div` + display: flex; + align-items: center; + column-gap: ${getRem(8)}; + margin-right: auto; // 추가 + width: 100%; +`; + +const IconAndTextWrapper = styled.div` + display: flex; + align-items: center; + column-gap: ${getRem(8)}; +`; diff --git a/src/comment/ui/comment-list/CommentItem.tsx b/src/comment/ui/comment-list/CommentItem.tsx index f979fe44..efe79283 100644 --- a/src/comment/ui/comment-list/CommentItem.tsx +++ b/src/comment/ui/comment-list/CommentItem.tsx @@ -46,10 +46,10 @@ const CommentItem = ({ + {} {nickName} - {} {content} diff --git a/src/comment/ui/comment-list/CommentList.tsx b/src/comment/ui/comment-list/CommentList.tsx new file mode 100644 index 00000000..1882bacd --- /dev/null +++ b/src/comment/ui/comment-list/CommentList.tsx @@ -0,0 +1,40 @@ +import { useGETCommentListQuery } from '@/comment/api/Comment'; +import BlankItem from '@/common-ui/BlankItem'; +import useAuthStore from '@/store/auth'; +import CommentItem from './CommentItem'; +import styled from '@emotion/styled'; +import getRem from '@/utils/getRem'; + +const CommentList = () => { + const { memberId } = useAuthStore(); + const { data: commentList } = useGETCommentListQuery({ + userId: memberId, + }); + return ( + + {!commentList?.length && } + {!!commentList && + commentList.map((comment) => ( + + ))} + + ); +}; + +export default CommentList; + +const CommentListWrapper = styled.div` + > * + * { + margin-top: ${getRem(10)}; + margin-bottom: ${getRem(10)}; + } +`; diff --git a/src/comment/ui/comment-list/SkeletonCommentList.tsx b/src/comment/ui/comment-list/SkeletonCommentList.tsx new file mode 100644 index 00000000..35e35fad --- /dev/null +++ b/src/comment/ui/comment-list/SkeletonCommentList.tsx @@ -0,0 +1,78 @@ +import styled from '@emotion/styled'; +import getRem from '@/utils/getRem'; +import { theme } from '@/styles/theme'; +import SkeletonText from '@/common-ui/skeleton/SkeletonText'; + +const SkeletonCommentList = () => { + return ( + + {Array.from({ length: 5 }).map((_, index) => ( + + ))} + + ); +}; + +export default SkeletonCommentList; + +const CommentListWrapper = styled.div` + > * + * { + margin-top: ${getRem(10)}; + margin-bottom: ${getRem(10)}; + } +`; +const CommentItem = () => { + return ( + <> + + + + + + + + + + + + + + + + + ); +}; + +const Container = styled.div` + display: grid; + row-gap: ${getRem(10)}; + width: 100%; + padding: ${getRem(15, 20)}; + border-radius: ${getRem(7)}; + background-color: ${theme.colors.grey850}; +`; + +const CommentHeader = styled.div` + display: flex; + justify-content: space-between; + align-items: center; +`; + +const IconAndTitleWrapper = styled.div` + display: flex; + align-items: center; + column-gap: ${getRem(8)}; + width: 100%; +`; + +const IconAndNickNameWrapper = styled.div` + display: flex; + align-items: center; + column-gap: ${getRem(8)}; +`; + +const IconAndTimeAndCategoryWrapper = styled.div` + display: flex; + align-items: center; + column-gap: ${getRem(8)}; +`; diff --git a/src/common-ui/skeleton/SkeletonText.tsx b/src/common-ui/skeleton/SkeletonText.tsx new file mode 100644 index 00000000..d1491b01 --- /dev/null +++ b/src/common-ui/skeleton/SkeletonText.tsx @@ -0,0 +1,36 @@ +/* eslint-disable no-irregular-whitespace */ +import { theme } from '@/styles/theme'; +import { css } from '@emotion/react'; +import { + skeletonAnimation1, + skeletonAnimation2, +} from '../utils/skeletonAnimations'; + +interface SkeletonTextProps { + width?: number; + height?: number; + animationType?: 'normal' | 'reverse'; +} + +const SkeletonText = ({ + width = 100, + height = 1, + animationType = 'normal', +}: SkeletonTextProps) => { + return ( +
+ ​ +
+ ); +}; + +export default SkeletonText; diff --git a/src/friend/api/friends.tsx b/src/friend/api/friends.tsx index 98fcb924..3eba2c2a 100644 --- a/src/friend/api/friends.tsx +++ b/src/friend/api/friends.tsx @@ -72,6 +72,7 @@ export const useGETFollowingListQuery = ( } return undefined; }, + suspense: true, }, ); }; @@ -139,6 +140,7 @@ export const useGETFollowerListQuery = ( } return undefined; }, + suspense: true, }, ); }; diff --git a/src/friend/ui/FriendSkeletonItem.tsx b/src/friend/ui/FriendSkeletonItem.tsx index 31e0b8a5..82d67f8d 100644 --- a/src/friend/ui/FriendSkeletonItem.tsx +++ b/src/friend/ui/FriendSkeletonItem.tsx @@ -1,29 +1,100 @@ import styled from '@emotion/styled'; import getRem from '@/utils/getRem'; -import { - skeletonBackgroundStyle, - skeletonContentStyle, -} from '@/common-ui/utils/skeletonStyles'; +import { FriendType } from '@/store/friend'; +import Button from '@/common-ui/Button'; +import { css } from '@emotion/react'; +import { theme } from '@/styles/theme'; +import SkeletonText from '@/common-ui/skeleton/SkeletonText'; +import { skeletonAnimation1 } from '@/common-ui/utils/skeletonAnimations'; const FriendSkeletonItem = () => { return ( - - - + <> + + + {Array.from({ length: 5 }).map((_, index) => ( + + + + + ))} + + ); }; export default FriendSkeletonItem; +const Wrapper = styled.div` + padding: ${getRem(10)}; + > * + * { + margin-top: ${getRem(10)}; + } +`; + const Container = styled.div` - height: ${getRem(45)}; - padding: ${getRem(12, 20)}; - width: 100%; - ${skeletonBackgroundStyle}; - margin-bottom: ${getRem(10)}; + display: flex; + justify-content: space-between; + align-items: center; + height: ${getRem(57)}; + column-gap: ${getRem(10)}; +`; + +const SkeletonIcon = styled.div` + width: ${getRem(25)}; + height: ${getRem(25)}; + border-radius: 50%; + background-color: ${theme.colors.grey800}; + ${skeletonAnimation1}; +`; + +interface FriendTypeSelectProps { + value: FriendType; + + followerTotalCount: number; + followingTotalCount: number; +} + +const FriendTypeSelect = ({ + value, + + followerTotalCount, + followingTotalCount, +}: FriendTypeSelectProps) => { + return ( + + + 팔로워 {followerTotalCount} + + + 팔로잉 {followingTotalCount} + + + ); +}; + +const ButtonWrapper = styled.div` + display: flex; `; -const TitleItem = styled.div` - height: ${getRem(16)}; - ${skeletonContentStyle}; +const FriendTypeButton = styled(Button)<{ active: boolean }>` + border-radius: 0; + ${(p) => + p.active && + css` + color: ${theme.colors.lightPrimary}; + border-bottom: 1px solid ${theme.colors.lightPrimary}; + `} `; diff --git a/src/pages/BookMarkDetailPage.tsx b/src/pages/BookMarkDetailPage.tsx index c6fe2c11..a4cc3998 100644 --- a/src/pages/BookMarkDetailPage.tsx +++ b/src/pages/BookMarkDetailPage.tsx @@ -9,6 +9,9 @@ import TriggerBottomSheet from '@/common-ui/BottomSheet/TriggerBottomSheet'; import IconButton from '@/common/ui/IconButton'; import BSConfirmation from '@/common/ui/BSConfirmation'; import useHandleBookmarkDetailMore from '@/bookmarks/service/hooks/detail/useHandleBookmarkDetailMore'; +import SkeletonBookmarkDetail from '@/bookmarks/ui/Detail/SkeletonBookmarkDetail'; +import SkeletonWrapper from '@/common-ui/SkeletonWrapper'; +import SkeletonCommentList from '@/comment/ui/bookmark/SkeletonCommentList'; const BookMarkDetailPage = () => { const { @@ -56,7 +59,13 @@ const BookMarkDetailPage = () => { /> {/** 북마크 정보 영역 */} - + + + + } + > { /> {/** 댓글 리스트 영역 */} - + + + + } + > diff --git a/src/pages/CommentPage.tsx b/src/pages/CommentPage.tsx index 5684e08e..eb76d4ce 100644 --- a/src/pages/CommentPage.tsx +++ b/src/pages/CommentPage.tsx @@ -1,38 +1,25 @@ import Header from '@/common-ui/Header/Header'; import styled from '@emotion/styled'; -import CommentItem from '@/comment/ui/comment-list/CommentItem'; import getRem from '@/utils/getRem'; -import { useGETCommentListQuery } from '@/comment/api/Comment'; -import useAuthStore from '@/store/auth'; -import BlankItem from '@/common-ui/BlankItem'; +import { Suspense } from 'react'; +import CommentList from '@/comment/ui/comment-list/CommentList'; +import SkeletonWrapper from '@/common-ui/SkeletonWrapper'; +import SkeletonCommentList from '@/comment/ui/comment-list/SkeletonCommentList'; const CommentPage = () => { - const { memberId } = useAuthStore(); - const { data: commentList } = useGETCommentListQuery({ - userId: memberId, - }); - return ( <>
- - {!commentList?.length && } - {commentList && - commentList.length > 0 && - commentList.map((comment) => ( - - ))} - + + + + } + > + + ); @@ -43,10 +30,3 @@ export default CommentPage; const Body = styled.div` padding: ${getRem(10, 20)}; `; - -const CommentListWrapper = styled.div` - > * + * { - margin-top: ${getRem(10)}; - margin-bottom: ${getRem(10)}; - } -`; diff --git a/src/pages/FriendBookmarkPage.tsx b/src/pages/FriendBookmarkPage.tsx index 8ec2a96d..9db9e15c 100644 --- a/src/pages/FriendBookmarkPage.tsx +++ b/src/pages/FriendBookmarkPage.tsx @@ -24,6 +24,7 @@ import SkeletonWrapper from '@/common-ui/SkeletonWrapper'; import BookmarkSkeletonItem from '@/bookmarks/ui/Main/BookmarkSkeletonItem'; import useFriendStore from '@/store/friend'; import useBookmarkStore from '@/store/bookmark'; +import SkeletonBookmarkUserInfo from '@/bookmarks/ui/SkeletonBookmarkUserInfo'; const FriendBookmarkPage = () => { // FIRST RENDER @@ -39,10 +40,11 @@ const FriendBookmarkPage = () => { // SERVER // 1. 친구 프로필 조회 - const { data: profileInfo } = useGETFriendProfileQuery({ - loginId: memberId, - memberId: Number(friendId), - }); + const { data: profileInfo, isLoading: profileLoading } = + useGETFriendProfileQuery({ + loginId: memberId, + memberId: Number(friendId), + }); // USER INTERACTION // 뒤로가기 @@ -97,16 +99,29 @@ const FriendBookmarkPage = () => { } /> - + {profileLoading ? ( + + + + ) : ( + + )} { const router = useNavigate(); @@ -21,9 +22,11 @@ const FriendPage = () => { } /> ( - - ))} + fallback={ + + + + } > diff --git a/src/styles/theme.ts b/src/styles/theme.ts index 3332274d..4fa7a0e0 100644 --- a/src/styles/theme.ts +++ b/src/styles/theme.ts @@ -12,6 +12,7 @@ export const theme = { grey600: '#757575', grey700: '#616161', grey800: '#424242', + grey850: '#303030', grey900: '#212121', darkBlack: '#161514', darkGrey: '#2A2A2A',