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 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
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
8 changes: 6 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,12 @@ 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 && "cursor-pointer"}`}
onClick={onClick}
>
<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,9 @@ 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 break-words">
{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>
);
}
152 changes: 83 additions & 69 deletions client/src/features/CasperShowCase/TransitionCasperCards.tsx
Original file line number Diff line number Diff line change
@@ -1,111 +1,125 @@
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 { useCallback, 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({
x: [x, x + diffX],
transition: CARD_TRANSITION(cardList.length),
});
};
const startAnimation = useCallback(
(x: number) => {
transitionControls.start({
x: [x, x + diffX * 2],
transition: CARD_TRANSITION(visibleCardCount * 2),
});
},
[visibleCardCount, transitionControls]
);

const stopAnimation = () => {
const stopAnimation = useCallback(() => {
transitionControls.stop();
if (containerRef.current) {
const computedStyle = window.getComputedStyle(containerRef.current);
const matrix = new DOMMatrix(computedStyle.transform);
setX(matrix.m41);
}
};
}, [transitionControls, containerRef]);

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 handleUpdateAnimation = (latest: ResolvedValues) => {
if (isEndCard(parseInt(String(latest.x)))) {
let nextIdx = visibleCardListIdx + visibleCardCount;

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

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

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