Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

홈 화면의 아이콘 버튼 레이아웃 조정 및 리팩토링 #570

Merged
merged 1 commit into from
Oct 12, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
114 changes: 49 additions & 65 deletions client/src/components/CafeActionBar.tsx
Original file line number Diff line number Diff line change
@@ -1,55 +1,78 @@
import { Suspense, useState, type PropsWithChildren } from 'react';
import { PiReadCvLogoFill } from 'react-icons/pi';
import { styled } from 'styled-components';
import { Suspense, useState } from 'react';
import { BiSolidInfoCircle } from 'react-icons/bi';
import { FaShare } from 'react-icons/fa';
import { PiHeartFill, PiReadCvLogoFill } from 'react-icons/pi';
import { styled, useTheme } from 'styled-components';
import { useToast } from '../context/ToastContext';
import useCafeLikes from '../hooks/useCafeLikes';
import useClipboard from '../hooks/useClipboard';
import useUser from '../hooks/useUser';
import type { Cafe } from '../types';
import CafeMenuBottomSheet from './CafeMenuBottomSheet';
import LikeButton from './LikeButton';
import ShareButton from './ShareButton';
import IconButton from './IconButton';

type CafeActionBarProps = {
cafe: Cafe;
};

const CafeActionBar = (props: CafeActionBarProps) => {
const { cafe } = props;
const theme = useTheme();
const showToast = useToast();
const clipboard = useClipboard();

const { isLiked, setLiked } = useCafeLikes(cafe);
const { data: user } = useUser();
const [isMenuOpened, setIsMenuOpened] = useState(false);

const likeCount = cafe.likeCount + (isLiked ? 1 : 0);

const handleShare = async () => {
try {
await clipboard.copyToClipboard(`https://yozm.cafe/cafes/${cafe.id}`);
showToast('success', 'URL이 복사되었습니다!');
} catch (error) {
showToast('error', `URL 복사 실패: ${error}`);
}
};

const handleLikeCountIncrease = () => {
if (!user) {
alert('로그인이 필요합니다!');
showToast('error', '로그인이 필요합니다!');
return;
}

setLiked({ isLiked: !isLiked });
};

const handlePreventClickPropagation: React.MouseEventHandler<HTMLDivElement> = (event) => {
event.stopPropagation();
const handleMenuOpen = () => {
setIsMenuOpened(true);
};

const handleMenuClose = () => {
setIsMenuOpened(false);
};

return (
<Container onClick={handlePreventClickPropagation}>
<Action>
<ShareButton url={`https://yozm.cafe/cafes/${cafe.id}`} />
<LikeButton
likeCount={cafe.likeCount + (isLiked ? 1 : 0)}
active={isLiked}
onChange={handleLikeCountIncrease}
/>
</Action>
<Action onClick={() => setIsMenuOpened(true)}>
<ActionButton label="메뉴">
<PiReadCvLogoFill />
</ActionButton>
</Action>
<Container>
<IconButton label="공유" onClick={handleShare}>
<FaShare />
</IconButton>

<IconButton label={String(likeCount)} onClick={handleLikeCountIncrease}>
<PiHeartFill fill={isLiked ? theme.color.primary : theme.color.white} />
</IconButton>

<IconButton label="메뉴" onClick={handleMenuOpen}>
<PiReadCvLogoFill />
</IconButton>

<IconButton label="더보기">
<BiSolidInfoCircle />
</IconButton>

{isMenuOpened && (
<Suspense>
<CafeMenuBottomSheet cafe={cafe} onClose={() => setIsMenuOpened(false)} />
<CafeMenuBottomSheet cafe={cafe} onClose={handleMenuClose} />
</Suspense>
)}
</Container>
Expand All @@ -59,51 +82,12 @@ const CafeActionBar = (props: CafeActionBarProps) => {
export default CafeActionBar;

const Container = styled.aside`
position: absolute;
right: 0;
bottom: 133px;

display: flex;
flex-direction: column;
gap: ${({ theme }) => theme.space[3]};
gap: ${({ theme }) => theme.space[5]};
align-self: flex-end;

padding-right: ${({ theme }) => theme.space[3]};
`;
padding: ${({ theme }) => theme.space[3]};

const Action = styled.button`
padding: 0;
color: white;
background: none;
border: none;
`;

type ActionButtonProps = PropsWithChildren<{
label: string;
}>;

const ActionButton = (props: ActionButtonProps) => {
const { label, children } = props;

return (
<ActionButtonContainer>
<ActionButtonIcon>{children}</ActionButtonIcon>
{label}
</ActionButtonContainer>
);
};

const ActionButtonContainer = styled.button`
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
`;

const ActionButtonIcon = styled.div`
font-size: ${({ theme }) => theme.fontSize['4xl']};

& > svg {
display: block;
}
`;
45 changes: 35 additions & 10 deletions client/src/components/CafeCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,30 +32,40 @@ const CafeCard = (props: CardProps) => {
{`${currentImageIndex + 1}`}/{cafe.images.length}
</CardQuantityContents>
</CardQuantityContainer>

<CarouselImageList onScroll={handleScroll}>
{cafe.images.map((image, index) => (
<CarouselImage
key={index}
src={Resource.getImageUrl({ size: '500', filename: image })}
alt={`${cafe}의 이미지`}
alt={`${cafe.name}의 ${index + 1}번째 이미지`}
loading={Math.abs(currentImageIndex - index) <= 1 ? 'eager' : 'lazy'}
/>
))}
</CarouselImageList>

<Bottom>
<BottomItem $fullWidth>
<CafeSummary
title={cafe.name}
address={cafe.address}
onClick={(event) => {
event.stopPropagation();
setIsShowDetail(true);
}}
/>
</BottomItem>
<BottomItem $align="right">
<CafeActionBar cafe={cafe} />
</BottomItem>
</Bottom>

<DotsContainer>
{cafe.images.map((_, index) => (
<Dot key={index} $active={index === currentImageIndex} />
))}
</DotsContainer>
<CafeSummary
title={cafe.name}
address={cafe.address}
onClick={(event) => {
event.stopPropagation();
setIsShowDetail(true);
}}
/>
<CafeActionBar cafe={cafe} />

{isShowDetail && <CafeDetailBottomSheet cafe={cafe} onClose={() => setIsShowDetail(false)} />}
</Container>
);
Expand Down Expand Up @@ -138,3 +148,18 @@ const CardQuantityContents = styled.div`
background-color: ${({ theme }) => theme.color.background.secondary};
border-radius: 10px;
`;

const Bottom = styled.div`
position: absolute;
bottom: 40px;
display: flex;
width: 100%;
`;

const BottomItem = styled.div<{ $fullWidth?: boolean; $align?: 'left' | 'right' }>`
position: absolute;
bottom: 0;
${({ $align }) => $align === 'left' && 'left: 0;'}
${({ $align }) => $align === 'right' && 'right: 0;'}
${({ $fullWidth }) => $fullWidth && 'width: 100%;'}
`;
30 changes: 0 additions & 30 deletions client/src/components/CafeSummary.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { BiSolidInfoCircle } from 'react-icons/bi';
import { SlLocationPin } from 'react-icons/sl';
import { styled } from 'styled-components';

Expand All @@ -20,26 +19,14 @@ const CafeSummary = (props: CafeSummaryProps) => {
{address}
</Address>
</Summary>
<ButtonList>
<Button>
<SolidInfoCircleIcon />
<ButtonText>더보기</ButtonText>
</Button>
</ButtonList>
</Container>
);
};

const Container = styled.div`
cursor: pointer;

position: absolute;
bottom: 0;

display: flex;

width: 100%;
margin-bottom: 40px;
padding: ${({ theme }) => theme.space[3]};
`;

Expand All @@ -63,21 +50,4 @@ const LocationPinIcon = styled(SlLocationPin)`
font-size: ${({ theme }) => theme.fontSize.sm};
`;

const ButtonList = styled.div`
margin-left: auto;
font-size: ${({ theme }) => theme.fontSize.sm};
`;

const ButtonText = styled.span``;

const Button = styled.button`
display: flex;
flex-direction: column;
align-items: flex-end;
`;

const SolidInfoCircleIcon = styled(BiSolidInfoCircle)`
font-size: ${({ theme }) => theme.fontSize['4xl']};
`;

export default CafeSummary;
15 changes: 0 additions & 15 deletions client/src/components/CommentButton.stories.tsx

This file was deleted.

32 changes: 0 additions & 32 deletions client/src/components/CommentButton.tsx

This file was deleted.

25 changes: 25 additions & 0 deletions client/src/components/IconButton.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import type { Meta, StoryObj } from '@storybook/react';
import { FaShare } from 'react-icons/fa';
import IconButton from './IconButton';

type Story = StoryObj<typeof IconButton>;

const meta: Meta<typeof IconButton> = {
title: 'IconButton',
component: IconButton,
};

export default meta;

export const Default: Story = {
args: {
children: <FaShare />,
},
};

export const WithLabel: Story = {
args: {
children: <FaShare />,
label: '공유',
},
};
36 changes: 36 additions & 0 deletions client/src/components/IconButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import type { ComponentPropsWithoutRef } from 'react';
import styled from 'styled-components';

type IconButtonProps = ComponentPropsWithoutRef<'button'> & {
label?: string;
};

const IconButton = (props: IconButtonProps) => {
const { label, children, ...restProps } = props;

return (
<Container aria-label={label} {...restProps}>
<Icon>{children}</Icon>

{label && <Label>{label}</Label>}
</Container>
);
};

export default IconButton;

const Container = styled.button`
display: flex;
flex-direction: column;
align-items: center;
`;

const Icon = styled.div`
font-size: ${({ theme }) => theme.fontSize['4xl']};

& > * {
display: block;
}
`;

const Label = styled.span``;
Loading
Loading