Skip to content

Commit

Permalink
Merge pull request #161 from softeerbootcamp4th/feat/#160-ux
Browse files Browse the repository at this point in the history
[Feat] UX 개선
  • Loading branch information
sooyeoniya authored Aug 19, 2024
2 parents 59b01f0 + 64cf244 commit 06fbe9f
Show file tree
Hide file tree
Showing 22 changed files with 280 additions and 129 deletions.
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

0 comments on commit 06fbe9f

Please sign in to comment.