Skip to content

Commit 76ed547

Browse files
authored
Merge pull request #169 from Boggle-Boggle/develop
Develop -> Staging
2 parents 4d54ce5 + 37f8b91 commit 76ed547

20 files changed

Lines changed: 827 additions & 233 deletions

File tree

src/components/BookCover.tsx

Lines changed: 113 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,129 @@
1+
type BookCoverBadgeType = 'adult' | 'reading' | 'stopped' | 'readCount';
2+
3+
type BookCoverBadge = {
4+
type: BookCoverBadgeType;
5+
readCount?: number;
6+
};
7+
18
type BookCoverProps = {
29
url: string;
310
label?: string;
411
ratio?: number;
512
rounded?: 'sm' | 'lg';
613
shadowLeftBar?: boolean;
14+
shadowRightTriangle?: boolean;
15+
topRightBadge?: BookCoverBadge;
16+
bottomRightBadge?: BookCoverBadge;
717
className?: string;
818
};
919

10-
export const BookCover = (props: BookCoverProps) => {
11-
const { url, label = '', shadowLeftBar, ratio = 3 / 4, className = '', rounded = 'lg' } = props;
12-
const roundedClass = rounded === 'sm' ? 'rounded' : 'rounded-md';
20+
const MSG_BOOK_COVER_BADGE_ADULT = '성인';
21+
const MSG_BOOK_COVER_BADGE_READING = '읽는중';
22+
const MSG_BOOK_COVER_BADGE_STOPPED = '중단';
23+
const MSG_BOOK_COVER_BADGE_READ_COUNT = '회독';
24+
25+
const getBadgeLabel = (badge: BookCoverBadge) => {
26+
if (badge.type === 'adult') return MSG_BOOK_COVER_BADGE_ADULT;
27+
if (badge.type === 'reading') return MSG_BOOK_COVER_BADGE_READING;
28+
if (badge.type === 'stopped') return MSG_BOOK_COVER_BADGE_STOPPED;
29+
30+
return `${badge.readCount ?? 0}${MSG_BOOK_COVER_BADGE_READ_COUNT}`;
31+
};
32+
33+
const getBadgeClassName = (type: BookCoverBadgeType) => {
34+
if (type === 'adult') {
35+
return 'bg-[#FF4D4F]/90 text-white';
36+
}
37+
38+
return 'bg-[#888888]/80 text-white';
39+
};
40+
41+
const Badge = (props: { badge: BookCoverBadge }) => {
42+
const { badge } = props;
43+
const label = getBadgeLabel(badge);
44+
const badgeClassName = getBadgeClassName(badge.type);
1345

1446
return (
15-
<div
16-
className={`relative inline-block overflow-hidden border border-neutral-20 ${roundedClass} ${className}`}
17-
style={{ aspectRatio: ratio }}
47+
<span className={`text-caption3 inline-flex items-center rounded-full px-2 py-0.5 ${badgeClassName}`}>{label}</span>
48+
);
49+
};
50+
51+
const ShadowLeftBar = () => (
52+
<span
53+
className="pointer-events-none absolute left-0 top-0 z-bookShadow h-full w-[9px] mix-blend-multiply"
54+
style={{ background: 'linear-gradient(90deg, #FFFFFF 65%, #E0E0E0 100%)' }}
55+
/>
56+
);
57+
58+
const ShadowRightTriangleSvg = () => {
59+
return (
60+
<svg
61+
width="16"
62+
height="21"
63+
viewBox="0 0 16 21"
64+
fill="none"
65+
xmlns="http://www.w3.org/2000/svg"
66+
className="pointer-events-none absolute -right-[11px] bottom-0 z-bookShadow w-4"
67+
aria-hidden="true"
68+
preserveAspectRatio="none"
1869
>
19-
<img className="relative z-20 size-full object-cover" src={url} alt={label} />
20-
{shadowLeftBar && (
21-
<span
22-
className="pointer-events-none absolute left-0 top-0 z-30 h-full w-[9px] mix-blend-multiply"
23-
style={{ background: 'linear-gradient(90deg, #FFFFFF 65%, #E0E0E0 100%)' }}
70+
<g opacity="0.6" style={{ mixBlendMode: 'multiply' }}>
71+
<path
72+
d="M4.65332 17.2529C3.65271 18.8944 1.90719 19.9246 0 20.0225C1.99315 19.888 3.56836 18.2304 3.56836 16.2031V0H15.1689L4.65332 17.2529Z"
73+
fill="url(#book-cover-shadow-right)"
2474
/>
25-
)}
75+
</g>
76+
<defs>
77+
<linearGradient
78+
id="book-cover-shadow-right"
79+
x1="9.08767"
80+
y1="0"
81+
x2="9.08767"
82+
y2="20.0313"
83+
gradientUnits="userSpaceOnUse"
84+
>
85+
<stop stopColor="#EEEEEE" />
86+
<stop offset="1" stopColor="#888888" />
87+
</linearGradient>
88+
</defs>
89+
</svg>
90+
);
91+
};
92+
93+
export const BookCover = (props: BookCoverProps) => {
94+
const {
95+
url,
96+
label = '',
97+
shadowLeftBar = false,
98+
shadowRightTriangle = true,
99+
topRightBadge,
100+
bottomRightBadge,
101+
ratio = 3 / 4,
102+
className = '',
103+
rounded = 'lg',
104+
} = props;
105+
const roundedClass = rounded === 'sm' ? 'rounded' : 'rounded-md';
106+
107+
return (
108+
<div className={`relative w-full ${className}`}>
109+
<div
110+
className={`relative w-full overflow-hidden border border-neutral-20 ${roundedClass}`}
111+
style={{ aspectRatio: ratio }}
112+
>
113+
<img className="absolute inset-0 z-bookShadow h-full w-full object-cover" src={url} alt={label} />
114+
{shadowLeftBar && <ShadowLeftBar />}
115+
{topRightBadge && (
116+
<div className="z-bookText absolute right-1 top-1">
117+
<Badge badge={topRightBadge} />
118+
</div>
119+
)}
120+
{bottomRightBadge && (
121+
<div className="z-bookText absolute bottom-1 right-1">
122+
<Badge badge={bottomRightBadge} />
123+
</div>
124+
)}
125+
</div>
126+
{shadowRightTriangle && <ShadowRightTriangleSvg />}
26127
</div>
27128
);
28129
};

src/components/Button/BottomButton.tsx

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,10 @@ import Button from './Button';
22
import { ButtonProps } from './type';
33

44
const BottomButton = (props: ButtonProps) => {
5-
const { size } = props;
6-
75
return (
8-
<>
9-
<div className={`mb-safe-bottom ${size === 'small' ? 'h-11' : 'h-14'}`} />
10-
<div className="fixed bottom-0 left-1/2 z-fixedBtn w-full max-w-mobile -translate-x-1/2 bg-neutral-0 bg-opacity-0 px-mobile pb-safe-bottom pt-2">
11-
<Button {...props} />
12-
</div>
13-
</>
6+
<div className="fixed inset-x-0 bottom-0 z-fixedBtn mx-auto w-full max-w-mobile bg-neutral-0 px-mobile pb-safe-bottom pt-4">
7+
<Button {...props} />
8+
</div>
149
);
1510
};
1611

src/components/Button/Button.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ const Button = ({
1919
}: ButtonProps) => {
2020
const base = 'inline-flex items-center justify-center gap-1';
2121
const disabledClass = 'bg-neutral-0 text-neutral-40 border-neutral-20';
22-
const widthClass = width === 'long' ? 'w-full max-w-[21.1875rem]' : 'w-fit';
22+
const widthClass = width === 'long' ? 'w-full ' : 'w-fit';
2323
const borderClass = size === 'small' ? 'border-[1.5px]' : 'border';
2424
const sizeClass =
2525
size === 'small'

src/components/Popover/index.tsx

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { ReactNode, useEffect, useRef, useState } from 'react';
22

3+
import { IconButton } from 'components/Button';
34
import { IconCircleInfo } from 'components/icons';
45

56
type PopoverProps = {
@@ -8,6 +9,8 @@ type PopoverProps = {
89
defaultOpen?: boolean;
910
};
1011

12+
const MSG_POPOVER_INFO_BUTTON_LABEL = '정보 팝오버 열기';
13+
1114
export const Popover = (props: PopoverProps) => {
1215
const { content, placement = 'center', defaultOpen = false } = props;
1316

@@ -38,19 +41,18 @@ export const Popover = (props: PopoverProps) => {
3841
return () => {
3942
document.removeEventListener('mousedown', handleOutsideClick);
4043
};
41-
// eslint-disable-next-line react-hooks/exhaustive-deps
4244
}, [internalOpen]);
4345

4446
return (
4547
<div className="bg-slate-0 relative flex" ref={containerRef}>
46-
<IconCircleInfo onClick={handleToggle} />
48+
<IconButton icon={IconCircleInfo} label={MSG_POPOVER_INFO_BUTTON_LABEL} onClick={handleToggle} size="sm" />
4749
{internalOpen && (
4850
<div
4951
className={`absolute top-full ${placementClass} ${popoverPaddingClass}`}
5052
style={{ filter: 'drop-shadow(0px 2px 8px #0000001F)' }}
5153
>
5254
{/* Arrow */}
53-
<div className={`pt-1" flex ${arrowAlignClass} ${arrowPaddingClass}`}>
55+
<div className={`flex pt-1 ${arrowAlignClass} ${arrowPaddingClass}`}>
5456
<svg
5557
className="h-[9px] w-fit text-neutral-0"
5658
viewBox="0 0 12 9"
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
import { Meta, StoryObj } from '@storybook/react-vite';
2+
3+
import { ComponentProps, useEffect, useState } from 'react';
4+
5+
import { StarRating } from 'components/StarRating';
6+
7+
const meta = {
8+
title: 'Components/StarRating',
9+
component: StarRating,
10+
tags: ['autodocs'],
11+
argTypes: {
12+
value: { control: { type: 'number' } },
13+
size: { control: { type: 'number' } },
14+
max: { control: { type: 'number' } },
15+
readOnly: { control: { type: 'boolean' } },
16+
onChange: { action: 'changed' },
17+
ariaLabel: { control: { type: 'text' } },
18+
},
19+
} satisfies Meta<typeof StarRating>;
20+
21+
export default meta;
22+
23+
type Story = StoryObj<typeof meta>;
24+
25+
const starRatingArgs: ComponentProps<typeof StarRating> = {
26+
value: 3.5,
27+
size: 20,
28+
max: 5,
29+
readOnly: true,
30+
onChange: () => {},
31+
};
32+
33+
export const ReadOnly: Story = {
34+
args: {
35+
...starRatingArgs,
36+
readOnly: true,
37+
},
38+
};
39+
40+
export const Editable: Story = {
41+
args: {
42+
...starRatingArgs,
43+
readOnly: false,
44+
},
45+
render: (args) => {
46+
const [value, setValue] = useState<number>(args.value);
47+
48+
useEffect(() => {
49+
setValue(args.value);
50+
}, [args.value]);
51+
52+
const handleChange = (nextValue: number) => {
53+
setValue(nextValue);
54+
args.onChange?.(nextValue);
55+
};
56+
57+
return <StarRating {...args} value={value} onChange={handleChange} />;
58+
},
59+
};
60+
61+
export const Half: Story = {
62+
args: {
63+
...starRatingArgs,
64+
value: 2.5,
65+
readOnly: true,
66+
},
67+
};
68+
69+
export const CustomSize: Story = {
70+
args: {
71+
...starRatingArgs,
72+
value: 4,
73+
size: 32,
74+
readOnly: true,
75+
},
76+
};

0 commit comments

Comments
 (0)