Skip to content

Commit

Permalink
Merge pull request #395 from softeerbootcamp-2nd/feat/apply-loading-c…
Browse files Browse the repository at this point in the history
…omponent

[REFACTOR] #381: 옵션 이미지 최적화 및 로딩 컴포넌트 추가
  • Loading branch information
jijiseong authored Aug 22, 2023
2 parents f7b001e + 1dcf71b commit 7502240
Show file tree
Hide file tree
Showing 11 changed files with 219 additions and 48 deletions.
23 changes: 13 additions & 10 deletions frontend/src/components/cards/OptionCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,24 +3,25 @@ import { BodyKrRegular4, HeadingKrMedium6, HeadingKrMedium7 } from '../../styles
import { CheckIcon } from '../common/icons/Icons';
import DefaultCardStyle from '../common/card/DefaultCardStyle';
import { HTMLAttributes, useContext } from 'react';
import { IMG_URL } from '../../utils/apis';
import { flexCenterCss } from '../../utils/commonStyle';
import { ItemContext } from '../../context/ItemProvider';
import { IDefaultOption } from '../../context/DefaultOptionProvider';
import { ISubOption } from '../../context/SubOptionProvider';

import { PERCENTAGE_LIMIT_VALUE } from '../../utils/constants';
interface IOptionCard extends HTMLAttributes<HTMLDivElement> {
type: 'default' | 'sub';
active: boolean;
option: ISubOption | IDefaultOption;
handleSelectOption?: React.MouseEventHandler<HTMLDivElement>;
imgBlobUrl: { [key: string]: string };
}

export default function OptionCard({
type,
active,
option,
handleSelectOption,
imgBlobUrl,
...props
}: IOptionCard) {
const { selectedItem } = useContext(ItemContext);
Expand All @@ -45,16 +46,18 @@ export default function OptionCard({
return (
<Card active={active} {...props}>
<ImgWrapper>
<OptionImg src={`${IMG_URL}${option.optionImage}`} />
<OptionImg src={`${imgBlobUrl[option.optionImage]}`} loading="lazy" alt="" />
<HashTagWrapper>{displayHashTag}</HashTagWrapper>
</ImgWrapper>
<OptionCardInfo>
<div>
{type === 'sub' && option.percentage !== null && (
<OptionDesc>
<BlueText>{option.percentage}%</BlueText>가 선택했어요
</OptionDesc>
)}
{type === 'sub' &&
option.percentage !== null &&
option.percentage > PERCENTAGE_LIMIT_VALUE && (
<OptionDesc>
<BlueText $active={active}>{option.percentage}%</BlueText>가 선택했어요.
</OptionDesc>
)}
<OptionTitle>{option.optionName}</OptionTitle>
</div>
{displayCaption}
Expand Down Expand Up @@ -122,8 +125,8 @@ const OptionPrice = styled.div`
const DefaultInfo = styled.div`
color: ${({ theme }) => theme.color.gray500};
`;
const BlueText = styled.span`
color: ${({ theme }) => theme.color.activeBlue2};
const BlueText = styled.span<{ $active: boolean }>`
color: ${({ $active, theme }) => $active && theme.color.activeBlue};
`;

const OptionDesc = styled.div`
Expand Down
4 changes: 2 additions & 2 deletions frontend/src/components/cards/OuterColorCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ export default function OuterColorCard({
</ColorWrapper>
<DescWrapper>
<ColorDesc>
<PointText $active={active}>{desc}%</PointText>가 선택했어요.
<BlueText $active={active}>{desc}%</BlueText>가 선택했어요.
</ColorDesc>
<ColorName>{name}</ColorName>
<Row>
Expand Down Expand Up @@ -73,7 +73,7 @@ const ColorBorder = styled.div<{ $active: boolean }>`
const ColorDesc = styled.div`
${BodyKrMedium4}
`;
const PointText = styled.span<{ $active: boolean }>`
const BlueText = styled.span<{ $active: boolean }>`
color: ${({ $active, theme }) => $active && theme.color.activeBlue};
`;
const ColorName = styled.div`
Expand Down
55 changes: 52 additions & 3 deletions frontend/src/components/common/banner/Banner.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { styled } from 'styled-components';
import { css, keyframes, styled } from 'styled-components';
import { BodyKrRegular3, HeadingKrBold1 } from '../../../styles/typefaces';
import { useCallback, useEffect, useState } from 'react';
import { MAX_TEXT_CNT } from '../../../utils/constants';
import CenterWrapper from '../layout/CenterWrapper';

interface IBanner extends React.HTMLAttributes<HTMLDivElement> {
Expand All @@ -8,13 +10,34 @@ interface IBanner extends React.HTMLAttributes<HTMLDivElement> {
}

export default function Banner({ subtitle, title, ...props }: IBanner) {
const [displayText, setDisplayText] = useState('');
const isOverflow = title && title.length > MAX_TEXT_CNT ? true : false;

const displayOverflow = useCallback(() => {
if (!title) return;
const txt = title.length > MAX_TEXT_CNT ? title.substring(0, MAX_TEXT_CNT) + '...' : title;
setDisplayText(txt);
}, [title]);

useEffect(() => {
displayOverflow();
}, [title, displayOverflow]);

return (
<>
<BannerBg {...props}>
<CenterWrapper>
<InfoWrapper>
{subtitle && <SubTitle>{subtitle}</SubTitle>}
{title && <Title>{title}</Title>}
{title && (
<Title
onMouseOver={() => setDisplayText(title)}
onMouseLeave={displayOverflow}
$isOverflow={isOverflow}
>
{displayText}
</Title>
)}
</InfoWrapper>
</CenterWrapper>
{props.children}
Expand All @@ -38,15 +61,41 @@ const BannerBg = styled.div`
const InfoWrapper = styled.div`
position: absolute;
top: 72px;
width: 448px;
overflow-x: hidden;
`;

const SubTitle = styled.p`
color: ${({ theme }) => theme.color.gray900};
${BodyKrRegular3}
`;

const Title = styled.p`
const textLoop = keyframes`
0% {
-webkit-transform: translate3d(0, 0, 0);
transform: translate3d(0, 0, 0);
}
100% {
-webkit-transform: translate3d(-130%, 0, 0);
transform: translate3d(-130%, 0, 0);
}
`;

const Title = styled.p<{ $isOverflow: boolean }>`
position: relative;
color: ${({ theme }) => theme.color.primaryColor700};
${HeadingKrBold1}
white-space: nowrap;
${({ $isOverflow }) => {
if ($isOverflow)
return css`
z-index: 1;
animation-play-state: paused;
&:hover {
cursor: pointer;
animation: ${textLoop} 10s linear infinite;
}
`;
}}
`;
38 changes: 22 additions & 16 deletions frontend/src/containers/OptionPage/OptionBannerContainer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,11 @@ export default function OptionBannerContainer({
const handleScroll = () => {
setWinY(window.scrollY);
};
const [visibleDesc, setVisibleDesc] = useState(false);

const handleDescVisibility = (visible: boolean) => {
setVisibleDesc(visible);
};
useEffect(() => {
window.addEventListener('scroll', handleScroll);
return () => {
Expand All @@ -73,7 +77,6 @@ export default function OptionBannerContainer({

useEffect(() => {
const target = optionDetail.subOptionList ? optionDetail.subOptionList[0] : optionDetail;

setBannerInfo({
categoryName: target.categoryName,
hmgData: target.hmgData,
Expand All @@ -100,17 +103,23 @@ export default function OptionBannerContainer({
/>
)}
{bannerInfo.optionDescription && (
<Description>
<Description
onMouseLeave={() => handleDescVisibility(false)}
onMouseOver={() => handleDescVisibility(true)}
>
<AdditionalText>{bannerInfo.optionDescription}</AdditionalText>
<HoverCaption>{bannerInfo.optionDescription}</HoverCaption>

<HoverCaption $visible={visibleDesc}>
{bannerInfo.optionDescription}
</HoverCaption>
</Description>
)}

{bannerInfo.hmgData && (
<HmgDataSection>
<HmgTag size="small" />
<DataList>
{bannerInfo.hmgData.overHalf !== null && (
{bannerInfo.hmgData.optionBoughtCount !== null && (
<Data>
<DataTitle>
{bannerInfo.hmgData.overHalf
Expand Down Expand Up @@ -157,18 +166,18 @@ export default function OptionBannerContainer({
</>
);
}
const HoverCaption = styled.div`
display: none;
const HoverCaption = styled.div<{ $visible: boolean }>`
white-space: pre-wrap;
position: absolute;
padding: 4px 12px;
border-radius: 10px;
top: 120%;
position: relative;
color: ${({ theme }) => theme.color.gray50};
opacity: 90%;
margin-top: 10px;
z-index: 1;
width: 448px;
background-color: ${({ theme }) => theme.color.gray900};
${BodyKrRegular4}
&:after {
content: '';
position: absolute;
Expand All @@ -181,6 +190,7 @@ const HoverCaption = styled.div`
border-bottom-color: ${({ theme }) => theme.color.gray900};
border-width: 3px;
}
visibility: ${({ $visible }) => ($visible ? 'visible' : 'hidden')};
`;
const Wrapper = styled.div<{ $isBannerVisible: boolean }>`
z-index: 5;
Expand Down Expand Up @@ -208,12 +218,6 @@ const Container = styled(CenterWrapper)`

const Description = styled.div`
position: relative;
&:hover {
${HoverCaption} {
display: block;
}
}
`;

const AdditionalText = styled.div`
Expand All @@ -227,6 +231,7 @@ const AdditionalText = styled.div`
width: 456px;
color: ${({ theme }) => theme.color.gray800};
${BodyKrRegular4}
cursor: pointer;
`;

const InfoWrapper = styled.div`
Expand Down Expand Up @@ -257,7 +262,8 @@ const ToastPopup = styled.button<{ $offsetY: number; $isBannerVisible: boolean }
($offsetY >= 200 && !$isBannerVisible) || $offsetY ? 'block' : 'none'};
`;
const HmgDataSection = styled.div`
margin-top: 12px;
position: absolute;
top: 80px;
`;
const DataList = styled.ul`
display: flex;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,13 @@ interface IDefaultOptionContainer {
query: string;
setQuery: Dispatch<React.SetStateAction<string>>;
setResult: Dispatch<React.SetStateAction<string[]>>;
imgBlobUrl: { [key: string]: string };
}
export default function DefaultOptionContainer({
query,
setQuery,
setResult,
imgBlobUrl,
}: IDefaultOptionContainer) {
const [filteredByCategory, setFilteredByCategory] = useState<IDefaultOption[]>([]);
const [currentCategory, setCurrentCategory] = useState('전체');
Expand Down Expand Up @@ -105,6 +107,7 @@ export default function DefaultOptionContainer({
type="default"
active={currentOptionIdx === option.optionId}
option={option}
imgBlobUrl={imgBlobUrl}
/>
{option.hasHmgData && (
<HmgWrapper>
Expand Down
Loading

0 comments on commit 7502240

Please sign in to comment.