diff --git a/.eslintrc.json b/.eslintrc.json index 7be1644..29b8b33 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -37,6 +37,7 @@ "import/prefer-default-export": 0, "import/newline-after-import": 0, "react/jsx-props-no-spreading": 0, + "no-shadow": 0, "no-unused-vars": 0, "import/extensions": [ "error", diff --git a/components/Carousel/index.tsx b/components/Carousel/index.tsx index 8cf0f2c..0c62165 100644 --- a/components/Carousel/index.tsx +++ b/components/Carousel/index.tsx @@ -24,20 +24,20 @@ function Carousel({ images, showDots }: CarouselType): ReactElement { slideRef.current.style.transform = `translateX(-${currentSlide}00%)`; }, [slideRef, currentSlide]); return ( - <> - - + + + {images.map((image) => ( {image} ))} - + {hasPrev && } {hasNext && ( )} - + {showDots && ( {images.map((_, index) => ( @@ -45,7 +45,7 @@ function Carousel({ images, showDots }: CarouselType): ReactElement { ))} )} - + ); } @@ -55,13 +55,15 @@ Carousel.defaultProps = { export default Carousel; -const Container = styled.div` +const Container = styled.div``; + +const SliderContainer = styled.div` width: 100%; overflow-x: hidden; position: relative; `; -const SliderContainer = styled.div` +const ImageContainer = styled.div` display: flex; transition: all 0.3s ease-in-out; `; @@ -92,9 +94,10 @@ const Button = styled.button` `; const DotsContainer = styled.div` - text-align: center; - height: 20px; - margin-bottom: -20px; + display: flex; + justify-content: center; + align-items: center; + height: 40px; `; type DotType = { diff --git a/components/Feed/Anchor.tsx b/components/Feed/Anchor.tsx new file mode 100644 index 0000000..5f9d814 --- /dev/null +++ b/components/Feed/Anchor.tsx @@ -0,0 +1,22 @@ +import React, { ReactElement, ReactNode } from 'react'; +import styled from '@emotion/styled'; +import Link, { LinkProps } from 'next/link'; + +export type AnchorProps = { + children: ReactNode; +} & LinkProps; + +function Anchor({ children, ...linkProps }: AnchorProps): ReactElement { + return ( + + {children} + + ); +} + +export default Anchor; + +const StyledAnchor = styled.a` + cursor: pointer; + font-weight: 600; +`; diff --git a/components/Feed/ButtonGroup.tsx b/components/Feed/ButtonGroup.tsx new file mode 100644 index 0000000..fd8e2a5 --- /dev/null +++ b/components/Feed/ButtonGroup.tsx @@ -0,0 +1,56 @@ +import React, { ReactElement } from 'react'; +import Icon from 'components/Icon'; +import styled from '@emotion/styled'; + +export type ButtonGroupProps = { + isLike: boolean; +}; + +function ButtonGroup({ isLike }: ButtonGroupProps): ReactElement { + return ( + + + + + + + + + + + + + + + + ); +} + +export default ButtonGroup; + +const IconButton = styled.button` + all: unset; + height: 40px; + width: 40px; + padding: 8px; + box-sizing: border-box; + cursor: pointer; +`; + +const Container = styled.section` + height: 40px; + width: 100%; + margin-top: -40px; + display: flex; + align-items: center; + ${IconButton}:first-child { + margin-left: -8px; + } + ${IconButton}:last-child { + margin-right: -8px; + } +`; + +const FlexGap = styled.div` + flex-grow: 1; +`; diff --git a/components/Feed/CommentMore.tsx b/components/Feed/CommentMore.tsx new file mode 100644 index 0000000..f9cab5e --- /dev/null +++ b/components/Feed/CommentMore.tsx @@ -0,0 +1,16 @@ +import React, { ReactElement } from 'react'; +import styled from '@emotion/styled'; + +export type CommentMoreProps = { + commentLength: number; +}; + +function CommentMore({ commentLength }: CommentMoreProps): ReactElement { + return 댓글 {commentLength}개 모두 보기; +} + +export default CommentMore; + +const Container = styled.div` + margin-bottom: 4px; +`; diff --git a/components/Feed/CommentPreview.tsx b/components/Feed/CommentPreview.tsx new file mode 100644 index 0000000..c3a81c6 --- /dev/null +++ b/components/Feed/CommentPreview.tsx @@ -0,0 +1,19 @@ +import React, { ReactElement } from 'react'; +import { PreviewComment } from 'types/index'; +import Anchor from 'components/Feed/Anchor'; +import styled from '@emotion/styled'; + +function CommentPreview({ author: { displayId }, content }: PreviewComment): ReactElement { + return ( + + {displayId} +  {content} + + ); +} + +export default CommentPreview; + +const Container = styled.div` + margin-bottom: 4px; +`; diff --git a/components/Feed/ContentBody.tsx b/components/Feed/ContentBody.tsx new file mode 100644 index 0000000..fa8af77 --- /dev/null +++ b/components/Feed/ContentBody.tsx @@ -0,0 +1,24 @@ +import React, { ReactElement } from 'react'; +import { FeedAuthor } from 'types/index'; +import Anchor from 'components/Feed/Anchor'; +import styled from '@emotion/styled'; + +export type AuthorBodyProps = { + body: string; + author: FeedAuthor; +}; + +function ContentBody({ body, author: { displayId } }: AuthorBodyProps): ReactElement { + return ( + + {displayId} +  {body} + + ); +} + +export default ContentBody; + +const Container = styled.div` + margin-bottom: 4px; +`; diff --git a/components/Feed/Feed.stories.tsx b/components/Feed/Feed.stories.tsx new file mode 100644 index 0000000..db45ca8 --- /dev/null +++ b/components/Feed/Feed.stories.tsx @@ -0,0 +1,64 @@ +import React from 'react'; +import { Story, Meta } from '@storybook/react/types-6-0'; + +import Feed, { FeedProps } from './Feed'; + +export default { + title: 'Feed', + component: Feed, + parameters: { + layout: 'fullscreen', + }, +} as Meta; + +const Template: Story = (args) => ; + +export const Default = Template.bind({}); +Default.args = { + author: { + displayId: '_dohakim', + isFollowedByMe: true, + }, + images: [ + 'https://via.placeholder.com/300?text=1', + 'https://via.placeholder.com/300?text=2', + 'https://via.placeholder.com/300?text=3', + 'https://via.placeholder.com/300?text=4', + 'https://via.placeholder.com/300?text=5', + 'https://via.placeholder.com/300?text=6', + 'https://via.placeholder.com/300?text=7', + 'https://via.placeholder.com/300?text=8', + 'https://via.placeholder.com/300?text=9', + 'https://via.placeholder.com/300?text=10', + ], + isLike: true, + likeLength: 1, + likeUser: { + displayId: 'dongha', + profileImageUrl: + 'https://post-phinf.pstatic.net/MjAxODA3MTlfMTIg/MDAxNTMxOTg5ODE5OTAw.edb-H-Rmhr2dFvKAqKA11flZ2k45cRi4Q4IaHirlMF4g.It6ziXN3vtf0R7B2p9DdwOy1hovG7aynuCPwAysStMcg.JPEG/jy180719b2.jpg?type=w1200', + isFollowedByMe: false, + }, + body: 'in 송도', + commentLength: 2, + commentPreview: [ + { + author: { + displayId: 'dongha', + profileImageUrl: + 'https://post-phinf.pstatic.net/MjAxODA3MTlfMTIg/MDAxNTMxOTg5ODE5OTAw.edb-H-Rmhr2dFvKAqKA11flZ2k45cRi4Q4IaHirlMF4g.It6ziXN3vtf0R7B2p9DdwOy1hovG7aynuCPwAysStMcg.JPEG/jy180719b2.jpg?type=w1200', + isFollowedByMe: false, + }, + content: '와 멋지다', + }, + { + author: { + displayId: 'dongha', + profileImageUrl: + 'https://post-phinf.pstatic.net/MjAxODA3MTlfMTIg/MDAxNTMxOTg5ODE5OTAw.edb-H-Rmhr2dFvKAqKA11flZ2k45cRi4Q4IaHirlMF4g.It6ziXN3vtf0R7B2p9DdwOy1hovG7aynuCPwAysStMcg.JPEG/jy180719b2.jpg?type=w1200', + isFollowedByMe: false, + }, + content: '와 멋지다2', + }, + ], +}; diff --git a/components/Feed/Feed.tsx b/components/Feed/Feed.tsx new file mode 100644 index 0000000..60b3207 --- /dev/null +++ b/components/Feed/Feed.tsx @@ -0,0 +1,39 @@ +import React from 'react'; +import type { ReactElement } from 'react'; +import type { Feed as FeedType } from 'types/index'; +import Carousel from 'components/Carousel'; +import FeedBody from 'components/Feed/FeedBody'; +import FeedHeader from './FeedHeader'; + +export type FeedProps = FeedType; + +function Feed({ + author, + body, + commentLength, + commentPreview, + createdAt, + images, + isLike, + likeLength, + likeUser, +}: FeedProps): ReactElement { + return ( +
+ + + +
+ ); +} + +export default Feed; diff --git a/components/Feed/FeedBody.tsx b/components/Feed/FeedBody.tsx new file mode 100644 index 0000000..d4ca7aa --- /dev/null +++ b/components/Feed/FeedBody.tsx @@ -0,0 +1,54 @@ +import React, { ReactElement } from 'react'; +import styled from '@emotion/styled'; +import ButtonGroup from 'components/Feed/ButtonGroup'; +import LikeSection from 'components/Feed/LikeSection'; +import { FeedAuthor, PreviewComment } from 'types/index'; +import ContentBody from 'components/Feed/ContentBody'; +import CommentPreview from 'components/Feed/CommentPreview'; + +export type FeedBodyProps = { + isLike: boolean; + likeUser: FeedAuthor; + likeLength: number; + author: FeedAuthor; + body: string; + commentLength: number; + commentPreview: PreviewComment[]; + createdAt: string; +}; + +function FeedBody({ + isLike, + likeUser, + likeLength, + body, + author, + commentLength, + commentPreview, +}: FeedBodyProps): ReactElement { + return ( + + + {likeLength > 0 && } + + {commentLength > 0 && 댓글 {commentLength}개 모두 보기} + {commentPreview.map((previewComment) => ( + + ))} + + ); +} + +export default FeedBody; + +const Container = styled.div` + display: flex; + padding: 0 16px; + flex-direction: column; + font-size: 14px; +`; + +const CommentLengthContainer = styled.div` + color: rgb(142, 142, 142); + margin-bottom: 4px; +`; diff --git a/components/Feed/FeedHeader.tsx b/components/Feed/FeedHeader.tsx new file mode 100644 index 0000000..ea5add5 --- /dev/null +++ b/components/Feed/FeedHeader.tsx @@ -0,0 +1,36 @@ +import React from 'react'; +import type { ReactElement } from 'react'; +import styled from '@emotion/styled'; +import UserAvatar from 'components/UserAvatar'; +import type { FeedAuthor } from 'types/index'; +import Icon from 'components/Icon'; + +export type FeedHeaderProps = { + author: FeedAuthor; +}; + +function FeedHeader({ author: { profileImageUrl, displayId } }: FeedHeaderProps): ReactElement { + return ( +
+ + {displayId} + +
+ ); +} + +export default FeedHeader; + +const Header = styled.header` + display: flex; + align-items: center; + padding: 16px; + height: 60px; + box-sizing: border-box; +`; + +const DisplayIdContainer = styled.span` + margin-left: 12px; + font-weight: bold; + flex: 1; +`; diff --git a/components/Feed/LikeSection.tsx b/components/Feed/LikeSection.tsx new file mode 100644 index 0000000..4de5afa --- /dev/null +++ b/components/Feed/LikeSection.tsx @@ -0,0 +1,37 @@ +import React, { ReactElement, useMemo } from 'react'; +import { FeedAuthor } from 'types/index'; +import styled from '@emotion/styled'; +import UserAvatar from 'components/UserAvatar'; +import Anchor from 'components/Feed/Anchor'; + +export type LikeSectionProps = { + likeUser: FeedAuthor; + likeLength: number; +}; + +function LikeSection({ likeUser: { profileImageUrl, displayId }, likeLength }: LikeSectionProps): ReactElement { + const count = useMemo(() => likeLength - 1, [likeLength]); + return ( + + + +   + {displayId}님{count > 0 && 외 {count}명}이 + 좋아합니다. + + + ); +} + +export default LikeSection; + +const Container = styled.section` + margin-bottom: 8px; + height: 20px; +`; + +const Paragraph = styled.p` + display: flex; + align-items: center; + margin: 0; +`; diff --git a/components/Feed/index.ts b/components/Feed/index.ts new file mode 100644 index 0000000..e69de29 diff --git a/components/Icon/Icon.stories.tsx b/components/Icon/Icon.stories.tsx index 3832b33..9250209 100644 --- a/components/Icon/Icon.stories.tsx +++ b/components/Icon/Icon.stories.tsx @@ -37,9 +37,9 @@ Search.args = { size: 'big', }; -export const Favarite = Template.bind({}); -Favarite.args = { - name: 'favorite', +export const Like = Template.bind({}); +Like.args = { + name: 'like', size: 'big', }; diff --git a/components/Icon/svg/index.ts b/components/Icon/svg/index.ts index f118bc8..301403f 100644 --- a/components/Icon/svg/index.ts +++ b/components/Icon/svg/index.ts @@ -2,9 +2,10 @@ export { ReactComponent as menu } from './menu.svg'; export { ReactComponent as home } from './home.svg'; export { ReactComponent as activity } from './activity.svg'; export { ReactComponent as search } from './search.svg'; -export { ReactComponent as favorite } from './favorite.svg'; +export { ReactComponent as likeFilled } from './likeFilled.svg'; export { ReactComponent as comment } from './comment.svg'; export { ReactComponent as share } from './share.svg'; export { ReactComponent as bookmark } from './bookmark.svg'; export { ReactComponent as newpost } from './newpost.svg'; export { ReactComponent as back } from './back.svg'; +export { ReactComponent as like } from './like.svg'; diff --git a/components/Icon/svg/like.svg b/components/Icon/svg/like.svg new file mode 100644 index 0000000..c54c5bd --- /dev/null +++ b/components/Icon/svg/like.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/components/Icon/svg/favorite.svg b/components/Icon/svg/likeFilled.svg similarity index 100% rename from components/Icon/svg/favorite.svg rename to components/Icon/svg/likeFilled.svg diff --git a/components/UserAvatar/index.stories.tsx b/components/UserAvatar/index.stories.tsx index f11df4e..336b04b 100644 --- a/components/UserAvatar/index.stories.tsx +++ b/components/UserAvatar/index.stories.tsx @@ -1,7 +1,7 @@ import React from 'react'; // also exported from '@storybook/react' if you can deal with breaking changes in 6.1 import { Story, Meta } from '@storybook/react/types-6-0'; -import UserAvatar, { UserAvatarType } from '../UserAvatar'; +import UserAvatar, { UserAvatarType } from '.'; export default { title: 'UserAvatar', @@ -11,16 +11,17 @@ export default { const Template: Story = (args) => ; export const Default = Template.bind({}); -Default.args = { -}; +Default.args = {}; export const Photo = Template.bind({}); Photo.args = { - thumbnail: 'https://post-phinf.pstatic.net/MjAxODA3MTlfMTIg/MDAxNTMxOTg5ODE5OTAw.edb-H-Rmhr2dFvKAqKA11flZ2k45cRi4Q4IaHirlMF4g.It6ziXN3vtf0R7B2p9DdwOy1hovG7aynuCPwAysStMcg.JPEG/jy180719b2.jpg?type=w1200' -} + profileImageUrl: + 'https://post-phinf.pstatic.net/MjAxODA3MTlfMTIg/MDAxNTMxOTg5ODE5OTAw.edb-H-Rmhr2dFvKAqKA11flZ2k45cRi4Q4IaHirlMF4g.It6ziXN3vtf0R7B2p9DdwOy1hovG7aynuCPwAysStMcg.JPEG/jy180719b2.jpg?type=w1200', +}; export const Big = Template.bind({}); Big.args = { - size:"big", - thumbnail: 'https://post-phinf.pstatic.net/MjAxODA3MTlfMTIg/MDAxNTMxOTg5ODE5OTAw.edb-H-Rmhr2dFvKAqKA11flZ2k45cRi4Q4IaHirlMF4g.It6ziXN3vtf0R7B2p9DdwOy1hovG7aynuCPwAysStMcg.JPEG/jy180719b2.jpg?type=w1200' -} \ No newline at end of file + size: 'big', + profileImageUrl: + 'https://post-phinf.pstatic.net/MjAxODA3MTlfMTIg/MDAxNTMxOTg5ODE5OTAw.edb-H-Rmhr2dFvKAqKA11flZ2k45cRi4Q4IaHirlMF4g.It6ziXN3vtf0R7B2p9DdwOy1hovG7aynuCPwAysStMcg.JPEG/jy180719b2.jpg?type=w1200', +}; diff --git a/components/UserAvatar/index.tsx b/components/UserAvatar/index.tsx index 4f96d35..2a49aa8 100644 --- a/components/UserAvatar/index.tsx +++ b/components/UserAvatar/index.tsx @@ -2,22 +2,21 @@ import React from 'react'; import styled from '@emotion/styled'; export type UserAvatarType = { - thumbnail?: null | string; - size?: 'small' | 'big'; + profileImageUrl?: null | string; + size: keyof typeof sizes; }; -const DEFAULT_THUMBNAIL = - 'https://scontent-mxp1-2.cdninstagram.com/v/t51.2885-19/44884218_345707102882519_2446069589734326272_n.jpg?_nc_ht=scontent-mxp1-2.cdninstagram.com&_nc_ohc=q2X-4RcAgecAX-QGx5q&oh=911ea87522a15a1d1657ee9b1070086b&oe=606FB88F&ig_cache_key=YW5vbnltb3VzX3Byb2ZpbGVfcGlj.2'; +const DEFAULT_THUMBNAIL = '/img/defaultAvatar.jpg'; const defaultProps = { - thumbnail: DEFAULT_THUMBNAIL, - size: 'small', + profileImageUrl: DEFAULT_THUMBNAIL, + size: 'default', }; -function UserAvatar({ size, thumbnail }: UserAvatarType) { +function UserAvatar({ size, profileImageUrl }: UserAvatarType) { return ( - 유저 이미지 + 유저 이미지 ); } @@ -26,13 +25,19 @@ UserAvatar.defaultProps = defaultProps; export default UserAvatar; +const sizes = { + small: '20px', + default: '32px', + big: '77px', +}; + type ContainerType = { - size?: 'small' | 'big'; + size: keyof typeof sizes; }; const Container = styled.div` - height: ${({ size }) => (size === 'big' ? '77px' : '32px')}; - width: ${({ size }) => (size === 'big' ? '77px' : '32px')}; + height: ${({ size }) => sizes[size]}; + width: ${({ size }) => sizes[size]}; border-radius: 50%; overflow: hidden; `; diff --git a/components/UserItem/UserItem.tsx b/components/UserItem/UserItem.tsx index e46a25e..46db785 100644 --- a/components/UserItem/UserItem.tsx +++ b/components/UserItem/UserItem.tsx @@ -26,7 +26,7 @@ function UserItem({ }: UserItemProps & typeof defaultProps): ReactElement { return ( - + {displayId} {nickname && {nickname}} diff --git a/public/img/defaultAvatar.jpg b/public/img/defaultAvatar.jpg new file mode 100644 index 0000000..7a8a843 Binary files /dev/null and b/public/img/defaultAvatar.jpg differ diff --git a/types/index.ts b/types/index.ts index 28bd906..e538b6a 100644 --- a/types/index.ts +++ b/types/index.ts @@ -20,11 +20,20 @@ export type FeedAuthor = { profileImageUrl?: string; }; +export type PreviewComment = { + id: number; + content: string; + author: Member; + isLike: boolean; + likeLength: number; + created: string; +}; + export type Feed = { author: FeedAuthor; body: string; commentLength: number; - commentPreview: any[]; + commentPreview: PreviewComment[]; createdAt: string; id: number; images: string[];