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

[Feat] UX 개선 #161

Merged
merged 17 commits into from
Aug 19, 2024
Merged
Show file tree
Hide file tree
Changes from 14 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
22 changes: 19 additions & 3 deletions client/src/components/PopUp/index.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { FormEvent, useCallback, useEffect, useState } from "react";
import { FormEvent, useCallback, useEffect, useMemo, useState } from "react";
import { PHONE_NUMBER_FORMAT, formatPhoneNumber } from "@/utils/formatPhoneNumber";
import CTAButton from "../CTAButton";
import CheckBox from "../CheckBox";
Expand Down Expand Up @@ -43,9 +43,21 @@ export default function PopUp({
setIsMarketingInfoCheck(isChecked);
}, []);

const errorMessage = useMemo(() => {
if (phoneNumber.length >= 11 && !phoneNumber.match(PHONE_NUMBER_FORMAT)) {
return "전화번호는 010으로 시작해야합니다!";
}
if (!isMarketingInfoCheck || !isUserInfoCheck) {
return "필수 약관에 동의해주세요!";
}
return "";
}, [phoneNumber, isUserInfoCheck, isMarketingInfoCheck]);

const handleConfirm = (e: FormEvent) => {
e.preventDefault();
handlePhoneNumberConfirm(phoneNumber);
if (!errorMessage) {
handlePhoneNumberConfirm(phoneNumber);
}
};

return (
Expand Down Expand Up @@ -84,7 +96,11 @@ export default function PopUp({
handleValueChange={handleTextFieldChange}
/>

<div className="pt-400" />
<div className="pt-200" />

<p className="h-body-2-medium text-s-red pt-400">{errorMessage}</p>

<div className="pt-500" />

<div className="flex flex-col gap-500">
<div className="flex gap-500">
Expand Down
9 changes: 7 additions & 2 deletions client/src/components/Scroll/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { cva } from "class-variance-authority";
export interface ScrollProps {
type: "light" | "dark";
children: ReactNode;
onClick?: () => void;
}

const scrollTextVariants = cva(`h-body-2-regular`, {
Expand All @@ -15,9 +16,13 @@ const scrollTextVariants = cva(`h-body-2-regular`, {
},
});

export default function Scroll({ type, children }: ScrollProps) {
export default function Scroll({ type, children, onClick }: ScrollProps) {
return (
<div className="inline-flex flex-col items-center gap-500">
<div
className="inline-flex flex-col items-center gap-500"
onClick={onClick}
{...(onClick && { style: { cursor: "pointer" } })}
>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

스프레드 연산자를 사용하는 이유가 따로 있을까용?
제 생각에는 스프레드 연산자 말고, 그냥 조건부로 넣어줘도 클래스 속성 자체 텍스트가 분리 되지는 않아서 동적 스타일 적용 오류는 안날 것 같은데 아래처럼 하는건 어떨까여?

<div
    className={`inline-flex flex-col items-center gap-500 ${onClick && "cursor-pointer"}`}
    onClick={onClick}
>

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

아하 tailwind 안 쓰던 시절에 사용하던 방식이 익숙해서 저렇게 했었던 것 같네용,, 수정했습니다!

d779294

<div className={scrollTextVariants({ type })}>{children}</div>
<img
alt="아래 스크롤 아이콘"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,12 @@ export function CasperCardBackUI({
style={{ width: CARD_WIDTH - 100 }}
>
<p className="text-n-neutral-500">작성한 기대평</p>
<p className="text-n-black">{expectations}</p>
<p
className="text-n-black max-w-full text-center"
style={{ overflowWrap: "break-word" }}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

tailiwindCSS에 사용할 수 있는 속성이 있어서 이걸로 바꿔도 괜찮을 것 같아요! 혹시 적용이 안되는 문제였을까요?
image

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

오 이런 속성 있는줄 몰랐네용 적용했어요! 7b3cc4b

>
{expectations}
</p>
</div>
)}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { memo } from "react";
import { motion } from "framer-motion";
import { CASPER_CARD_SIZE, CASPER_SIZE_OPTION } from "@/constants/CasperCustom/casper";
import { CasperCardType } from "@/features/CasperShowCase/TransitionCasperCards";
import type { CasperCardType } from "@/types/casper";
import { CasperCardBackUI } from "./CasperCardBackUI";
import { CasperCardFrontUI } from "./CasperCardFrontUI";

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ export function CasperCustomFinish({
const { showToast, ToastComponent } = useToast(
isErrorGetShareLink
? "공유 링크 생성에 실패했습니다! 캐스퍼 봇 생성 후 다시 시도해주세요."
: "링크가 복사되었어요!"
: "🔗 링크가 복사되었어요!"
);

const dispatch = useCasperCustomDispatchContext();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ import { CASPER_SIZE_OPTION } from "@/constants/CasperCustom/casper";
import { DISSOLVE } from "@/constants/animation";
import { SCROLL_MOTION } from "@/constants/animation";
import { CasperFlipCard } from "@/features/CasperCustom/CasperCard/CasperFlipCard";
import { CasperCardType } from "@/features/CasperShowCase/TransitionCasperCards";
import useCasperCustomStateContext from "@/hooks/useCasperCustomStateContext";
import useToast from "@/hooks/useToast";
import type { CasperCardType } from "@/types/casper";

interface CasperCustomFinishingProps {
navigateNextStep: () => void;
Expand Down
23 changes: 17 additions & 6 deletions client/src/features/CasperShowCase/CasperCards.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { useMemo } from "react";
import { CASPER_CARD_SIZE, CASPER_SIZE_OPTION } from "@/constants/CasperCustom/casper";
import { CasperCardType, TransitionCasperCards } from "./TransitionCasperCards";
import type { CasperCardType } from "@/types/casper";
import { TransitionCasperCards } from "./TransitionCasperCards";

interface CasperCardsProps {
cardList: CasperCardType[];
Expand All @@ -8,12 +10,20 @@ interface CasperCardsProps {
export function CasperCards({ cardList }: CasperCardsProps) {
const cardLength = cardList.length;
const cardLengthHalf = Math.floor(cardLength / 2);
const topCardList = cardList.slice(0, cardLengthHalf);
const bottomCardList = cardList.slice(cardLengthHalf, cardLength);
const visibleCardCount = useMemo(() => {
const width = window.innerWidth;
const cardWidth = CASPER_CARD_SIZE[CASPER_SIZE_OPTION.SM].CARD_WIDTH;

return Math.ceil(width / cardWidth);
}, []);
const isMultipleLine = visibleCardCount * 2 <= cardLength;

const topCardList = cardList.slice(0, isMultipleLine ? cardLengthHalf : cardLength);
const bottomCardList = isMultipleLine ? cardList.slice(cardLengthHalf, cardLength) : [];

const itemWidth = CASPER_CARD_SIZE[CASPER_SIZE_OPTION.SM].CARD_WIDTH;
const gap = 40;
const totalWidth = (itemWidth + gap) * topCardList.length;
const totalWidth = (itemWidth + gap) * visibleCardCount;

const isEndTopCard = (latestX: number) => {
return latestX <= -totalWidth;
Expand All @@ -29,16 +39,17 @@ export function CasperCards({ cardList }: CasperCardsProps) {
initialX={0}
gap={gap}
diffX={-totalWidth}
totalWidth={totalWidth}
visibleCardCount={visibleCardCount}
isEndCard={isEndTopCard}
/>
<TransitionCasperCards
cardList={bottomCardList}
initialX={-totalWidth}
gap={gap}
diffX={totalWidth}
totalWidth={totalWidth}
visibleCardCount={visibleCardCount}
isEndCard={isEndBottomCard}
isReverseCards
/>
</div>
);
Expand Down
44 changes: 44 additions & 0 deletions client/src/features/CasperShowCase/TransitionCasperCardItem.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { useState } from "react";
import { CASPER_CARD_SIZE, CASPER_SIZE_OPTION } from "@/constants/CasperCustom/casper";
import { CasperFlipCard } from "@/features/CasperCustom/CasperCard/CasperFlipCard";
import type { CasperCardType } from "@/types/casper";

interface TransitionCasperCardItemProps {
cardItem: CasperCardType;
id: string;
stopAnimation?: () => void;
startAnimation?: () => void;
}

export function TransitionCasperCardItem({
cardItem,
id,
stopAnimation,
startAnimation,
}: TransitionCasperCardItemProps) {
const [isFlipped, setIsFlipped] = useState<boolean>(false);

const handleMouseEnter = () => {
stopAnimation && stopAnimation();
setIsFlipped(true);
};

const handleMouseLeave = () => {
startAnimation && startAnimation();
setIsFlipped(false);
};

return (
<li
key={id}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
style={{
width: CASPER_CARD_SIZE[CASPER_SIZE_OPTION.SM].CARD_WIDTH,
height: CASPER_CARD_SIZE[CASPER_SIZE_OPTION.SM].CARD_HEIGHT,
}}
>
<CasperFlipCard card={cardItem} size={CASPER_SIZE_OPTION.SM} isFlipped={isFlipped} />
</li>
);
}
137 changes: 74 additions & 63 deletions client/src/features/CasperShowCase/TransitionCasperCards.tsx
Original file line number Diff line number Diff line change
@@ -1,43 +1,41 @@
import { useEffect, useRef, useState } from "react";
import { AnimatePresence, motion, useAnimation } from "framer-motion";
import { CASPER_CARD_SIZE, CASPER_SIZE_OPTION } from "@/constants/CasperCustom/casper";
import { useEffect, useMemo, useRef, useState } from "react";
import { AnimatePresence, type ResolvedValues, motion, useAnimation } from "framer-motion";
import { CARD_TRANSITION } from "@/constants/CasperShowCase/showCase";
import { CasperFlipCard } from "@/features/CasperCustom/CasperCard/CasperFlipCard";
import useLazyLoading from "@/hooks/useLazyLoading";
import { SelectedCasperIdxType } from "@/types/casperCustom";
import type { CasperCardType } from "@/types/casper";
import { TransitionCasperCardItem } from "./TransitionCasperCardItem";

export interface CasperCardType {
id: number;
casperName: string;
expectations: string;
selectedCasperIdx: SelectedCasperIdxType;
}
interface TransitionCasperCardsProps {
cardList: CasperCardType[];
initialX: number;
diffX: number;
totalWidth: number;
visibleCardCount: number;
gap: number;
isEndCard: (latestX: number) => boolean;
isReverseCards?: boolean;
}

export function TransitionCasperCards({
cardList,
initialX,
diffX,
totalWidth,
gap,
visibleCardCount,
isEndCard,
isReverseCards = false,
}: TransitionCasperCardsProps) {
const isAnimated = visibleCardCount <= cardList.length;
const expandedCardList = useMemo(() => [...cardList, ...cardList, ...cardList], [cardList]);

const containerRef = useRef<HTMLUListElement>(null);
const transitionControls = useAnimation();

const [x, setX] = useState<number>(initialX);
const [visibleCardListIdx, setVisibleCardListIdx] = useState(0);

const startAnimation = (x: number) => {
transitionControls.start({
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

startAnimation, stopAnimation 함수는 useCallback으로 감싸줘도 될 것 같은데 어떻게 생각하시나용..?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

64cf244 좋아용 수정햇습니다!

x: [x, x + diffX],
transition: CARD_TRANSITION(cardList.length),
x: [x, x + diffX * 2],
transition: CARD_TRANSITION(visibleCardCount * 2),
});
};

Expand All @@ -50,62 +48,75 @@ export function TransitionCasperCards({
}
};

const visibleCardList = useMemo(() => {
const list = expandedCardList.slice(
visibleCardListIdx,
visibleCardListIdx + visibleCardCount * 2
);

if (isAnimated && isReverseCards) {
return list.reverse();
}

return isAnimated ? list : cardList;
}, [
isReverseCards,
expandedCardList,
cardList,
isAnimated,
visibleCardCount,
visibleCardListIdx,
]);

useEffect(() => {
startAnimation(x);
}, [transitionControls, totalWidth]);

const renderCardItem = (cardItem: CasperCardType, id: string) => {
const [isFlipped, setIsFlipped] = useState<boolean>(false);
const { isInView, cardRef } = useLazyLoading<HTMLLIElement>();
}, []);

const handleMouseEnter = () => {
stopAnimation();
setIsFlipped(true);
};
const handleUpdateAnimation = (latest: ResolvedValues) => {
if (isEndCard(parseInt(String(latest.x)))) {
let nextIdx = visibleCardListIdx + visibleCardCount;

const handleMouseLeave = () => {
startAnimation(x);
setIsFlipped(false);
};
// 만약 nextIdx가 cardList의 길이를 초과하면 배열의 처음부터 다시 index를 카운트하도록 함
if (nextIdx >= cardList.length) {
nextIdx = nextIdx % cardList.length;
}

return (
<li
ref={cardRef}
key={id}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
style={{
width: CASPER_CARD_SIZE[CASPER_SIZE_OPTION.SM].CARD_WIDTH,
height: CASPER_CARD_SIZE[CASPER_SIZE_OPTION.SM].CARD_HEIGHT,
}}
>
{isInView && (
<CasperFlipCard
card={cardItem}
size={CASPER_SIZE_OPTION.SM}
isFlipped={isFlipped}
/>
)}
</li>
);
setVisibleCardListIdx(nextIdx);
startAnimation(initialX);
}
};

return (
<AnimatePresence>
<motion.ul
ref={containerRef}
className="flex"
animate={transitionControls}
style={{ gap: `${gap}px` }}
onUpdate={(latest) => {
if (isEndCard(parseInt(String(latest.x)))) {
startAnimation(initialX);
}
}}
>
{cardList.map((card) => renderCardItem(card, `${card.id}`))}
{cardList.map((card) => renderCardItem(card, `${card.id}-clone`))}
</motion.ul>
{isAnimated ? (
<motion.ul
ref={containerRef}
className="flex"
animate={transitionControls}
style={{ gap: `${gap}px` }}
onUpdate={handleUpdateAnimation}
>
{visibleCardList.map((card, idx) => (
<TransitionCasperCardItem
key={`${card.id}-${idx}`}
cardItem={card}
id={`${card.id}-${idx}`}
stopAnimation={stopAnimation}
startAnimation={() => startAnimation(x)}
/>
))}
</motion.ul>
) : (
<ul className="flex w-screen justify-center" style={{ gap: `${gap}px` }}>
{visibleCardList.map((card, idx) => (
<TransitionCasperCardItem
key={`${card.id}-${idx}`}
cardItem={card}
id={`${card.id}-${idx}`}
/>
))}
</ul>
)}
</AnimatePresence>
);
}
Loading
Loading