Skip to content

Commit

Permalink
[#403] [모임 상세] 모임 정보 컴포넌트 (#423)
Browse files Browse the repository at this point in the history
* fix: svg width, height 속성 제거

* feat: BookGroupInfo 컴포넌트 작성

* feat: BookGroupInfo 컴포넌트 스토리 작성

* chore: 공백 제거

* chore: 불필요한 로그 제거

* chore: BookGroupInfo 스토리 파일 위치 변경

* refactor: avatar size 수정 및 svgr wrapper 태그 제거

- 3.5rem -> 3.2rem 으로 통일

* feat: next/image placeholder 속성 적용

* style: 책 이미지 shadow 및 크기 수정

* feat: BookCover 컴포넌트 작성

* refactor: Dday 컴포넌트 -> BookGroupStatus로 파일 분리

* feat: BookGroupStatus 스토리북 작성
  • Loading branch information
gxxrxn committed Jun 17, 2024
1 parent de6588c commit 378828c
Show file tree
Hide file tree
Showing 10 changed files with 331 additions and 1 deletion.
4 changes: 4 additions & 0 deletions src/constants/dataUrl.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export const DATA_URL = {
placeholder:
'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAAAXNSR0IArs4c6QAAAA1JREFUGFdjWL9+/X8ABysDDapsaG4AAAAASUVORK5CYII=', // data url for placeholder color (#AFAFAF)
};
19 changes: 19 additions & 0 deletions src/stories/bookgroup/BookGroupStatus.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import BookGroupStatus from '@/ui/bookgroup/BookGroupStatus';
import { Meta, StoryObj } from '@storybook/react';

const meta: Meta<typeof BookGroupStatus> = {
title: 'bookgroup/BookGroupStatus',
component: BookGroupStatus,
tags: ['autodocs'],
};

export default meta;

type Story = StoryObj<typeof BookGroupStatus>;

export const Default: Story = {
args: {
start: '2023-12-31',
end: '2024-01-08',
},
};
40 changes: 40 additions & 0 deletions src/stories/bookgroup/detail/BookGroupInfo.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { Meta, StoryObj } from '@storybook/react';

import BookGroupInfo from '@/ui/bookgroup/detail/BookGroupInfo';

const meta: Meta<typeof BookGroupInfo> = {
title: 'bookgroup/detail/BookGroupInfo',
component: BookGroupInfo,
tags: ['autodocs'],
};

export default meta;

type Story = StoryObj<typeof BookGroupInfo>;

export const Default: Story = {
args: {
title: '프롱이 리팩터링 스터디',
description:
'제 1차 프롱이 기수연합 독서 스터디 입니다. 마틴 파울러의 저서 ‘리팩터링 2판’과 함께 진행합니다.',
book: {
title: '리팩터링 2판',
author: '마틴 파울러',
bookImageSrc: 'https://image.yes24.com/goods/89649360/XL',
},
date: {
start: '2023-10-31',
end: '2023-11-27',
},
memberCount: {
current: 3,
max: 20,
},
owner: {
isMe: true,
name: '소피아',
profileImageSrc: '/icons/logo.svg',
},
isPublic: false,
},
};
1 change: 1 addition & 0 deletions src/types/dday.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export type DdayStatus = 'before' | 'dday' | 'ongoing' | 'end';
2 changes: 1 addition & 1 deletion src/ui/BookArchive/BookArchiveForUnAuth.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ const BookArchiveForUnAuth = () => {
if (!isSuccess) return null;

return (
<div className="flex flex-col gap-[1.5rem] w-full">
<div className="flex w-full flex-col gap-[1.5rem]">
<div className="text-md font-bold">🔥 인기 책장</div>
<div className="flex w-full flex-col gap-[3rem]">
{data.bookshelfResponses.map(bookshelf => (
Expand Down
61 changes: 61 additions & 0 deletions src/ui/book/BookCover.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { ComponentPropsWithoutRef } from 'react';
import Image from 'next/image';

import { DATA_URL } from '@/constants/dataUrl';

type BookCoverSize =
| 'xsmall'
| 'small'
| 'medium'
| 'large'
| 'xlarge'
| '2xlarge';

type BookCoverProps = Required<
Pick<ComponentPropsWithoutRef<typeof Image>, 'src'>
> & {
title: string;
size?: BookCoverSize;
};

const getCoverSizeClasses = (size: BookCoverSize) => {
switch (size) {
case 'xsmall': {
return 'w-[6.5rem] h-[9.1rem]';
}
case 'small': {
return 'w-[7.0rem] h-[9.8rem]';
}
case 'medium': {
return 'w-[7.5rem] h-[10.5rem]';
}
case 'large': {
return 'w-[9.0rem] h-[12.6rem]';
}
case 'xlarge': {
return 'w-[11.0rem] h-[15.4rem]';
}
case '2xlarge': {
return 'w-[18.0rem] h-[25.2rem]';
}
}
};

const BookCover = ({ src, title, size = 'medium' }: BookCoverProps) => {
const sizeClasses = getCoverSizeClasses(size);

return (
<span className={`relative ${sizeClasses}`}>
<Image
src={src}
alt={title}
placeholder="blur"
blurDataURL={DATA_URL['placeholder']}
className="object-fit rounded-[0.5rem] shadow-bookcover"
fill
/>
</span>
);
};

export default BookCover;
57 changes: 57 additions & 0 deletions src/ui/bookgroup/BookGroupStatus.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { DdayStatus } from '@/types/dday';

import Badge from '@/ui/Base/Badge';
import { getDdayCount } from '@/utils/date';

const getDdayStatus = (ddayByStart: number, ddayByEnd: number) => {
if (ddayByStart > 0) {
return 'before' as const;
} else if (ddayByStart === 0 && ddayByEnd > 0) {
return 'dday' as const;
} else if (ddayByStart < 0 && ddayByEnd >= 0) {
return 'ongoing' as const;
} else {
return 'end' as const;
}
};

const getBadgeProps = (status: DdayStatus, ddayCount: number) => {
switch (status) {
case 'before':
return {
colorScheme: 'main' as const,
isFilled: true,
text: `D-${ddayCount}`,
};
case 'dday':
return {
colorScheme: 'main' as const,
isFilled: false,
text: 'D-day',
};
case 'ongoing':
return {
colorScheme: 'main' as const,
isFilled: true,
text: '진행중',
};
case 'end':
return {
colorScheme: 'grey' as const,
isFilled: true,
text: '모임종료',
};
}
};

const BookGroupStatus = ({ start, end }: { start: string; end: string }) => {
const ddayByStart = getDdayCount(new Date(start));
const ddayByEnd = getDdayCount(new Date(end));
const ddayStatus = getDdayStatus(ddayByStart, ddayByEnd);

const { text, ...badgeProps } = getBadgeProps(ddayStatus, ddayByStart);

return <Badge {...badgeProps}>{text}</Badge>;
};

export default BookGroupStatus;
139 changes: 139 additions & 0 deletions src/ui/bookgroup/detail/BookGroupInfo.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
import Image from 'next/image';

import Badge from '@/ui/Base/Badge';
import BookCover from '@/ui/book/BookCover';
import BookGroupStatus from '@/ui/bookgroup/BookGroupStatus';
import { IconArrowLeft, IconCalendar, IconMembers } from '@public/icons';
import { DATA_URL } from '@/constants/dataUrl';

interface BookGroupInfoProps {
title: string;
description: string;
book: { title: string; author: string; bookImageSrc: string };
date: { start: string; end: string };
memberCount: { current: number; max: number };
owner: { isMe: boolean; name: string; profileImageSrc: string };
isPublic: boolean;
}

const BookGroupInfo = ({
title,
description,
book,
date,
memberCount,
owner,
isPublic,
}: BookGroupInfoProps) => {
return (
<div className="flex flex-col gap-[1rem]">
<div className="flex gap-[0.5rem]">
<BookGroupStatus start={date.start} end={date.end} />
<Public isPublic={isPublic} />
</div>
<Owner
name={owner.name}
isMe={owner.isMe}
profileImageSrc={owner.profileImageSrc}
/>
<Title title={title} />
<BookInfoCard
title={book.title}
bookImageSrc={book.bookImageSrc}
author={book.author}
/>
<div className="flex flex-col gap-[0.3rem]">
<Duration start={date.start} end={date.end} />
<MemberCapacity current={memberCount.current} max={memberCount.max} />
</div>
<Description content={description} />
</div>
);
};

export default BookGroupInfo;

const Public = ({ isPublic }: { isPublic: boolean }) => (
<Badge colorScheme="grey">{isPublic ? '공개' : '비공개'}</Badge>
);

const Owner = ({
profileImageSrc,
name,
isMe,
}: {
profileImageSrc: string;
name: string;
isMe: boolean;
}) => {
return (
<div className="flex items-center gap-[1rem]">
{/** FIXME: Avatar 컴포넌트로 변경 */}
<Image
width={32}
height={32}
alt={name}
src={profileImageSrc}
className="rounded-full"
placeholder="blur"
blurDataURL={DATA_URL['placeholder']}
/>
<span className="text-center text-sm font-bold">
{name} {isMe && ' 👑'}
</span>
</div>
);
};

const Title = ({ title }: { title: string }) => {
return <p className="text-xl font-bold">{title}</p>;
};

const BookInfoCard = ({
bookImageSrc,
title,
author,
}: {
bookImageSrc: string;
title: string;
author: string;
}) => {
return (
<div className="flex min-h-[10rem] w-full cursor-pointer gap-[2.4rem] rounded-[0.5rem] border-[0.05rem] border-cancel px-[2.2rem] py-[1.8rem]">
<BookCover size="xsmall" src={bookImageSrc} title={title} />
<div className="flex flex-grow flex-col">
<span className="text-sm font-bold">{title}</span>
<span className="text-xs text-placeholder">{author}</span>
</div>
{/** 왼쪽 방향의 화살표를 180도 회전하여 사용 */}
<IconArrowLeft className="h-[1.5rem] w-[1.5rem] rotate-180" />
</div>
);
};

const Duration = ({ start, end }: { start: string; end: string }) => {
return (
<div className="flex items-center gap-[1rem]">
<IconCalendar className="h-auto w-[1.6rem] fill-placeholder" />
<span className="text-sm text-placeholder">
{start} - {end}
</span>
</div>
);
};

const MemberCapacity = ({ current, max }: { current: number; max: number }) => {
return (
<div className="flex items-center gap-[1rem]">
<IconMembers className="h-auto w-[1.6rem] fill-placeholder" />
<span className="text-sm text-placeholder">
<span className="text-main-900">{current}</span>
{` / ${max}명`}
</span>
</div>
);
};

const Description = ({ content }: { content: string }) => {
return <p className="text-md leading-snug">{content}</p>;
};
5 changes: 5 additions & 0 deletions src/utils/date.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export const toDayFromMillseconds = (value: number) =>
Math.ceil(value / (1000 * 60 * 60 * 24));

export const getDdayCount = (target: Date) =>
toDayFromMillseconds(target.getTime() - new Date().getTime());
4 changes: 4 additions & 0 deletions tailwind.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,10 @@ module.exports = {
fontFamily: {
lineseed: ['var(--font-lineseed)'],
},
boxShadow: {
bookcover:
'0px 0px 2px rgba(0, 0, 0, 0.2), 2px 2px 6px rgba(0, 0, 0, 0.1)',
},
keyframes: {
'page-transition': {
from: { opacity: 0 },
Expand Down

0 comments on commit 378828c

Please sign in to comment.