diff --git a/client/src/components/PopUp/index.tsx b/client/src/components/PopUp/index.tsx index e3ed990b..4fbac935 100644 --- a/client/src/components/PopUp/index.tsx +++ b/client/src/components/PopUp/index.tsx @@ -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"; @@ -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 ( @@ -84,7 +96,11 @@ export default function PopUp({ handleValueChange={handleTextFieldChange} /> -
+
+ +

{errorMessage}

+ +
diff --git a/client/src/components/Scroll/index.tsx b/client/src/components/Scroll/index.tsx index 3b172b68..df0cdb71 100644 --- a/client/src/components/Scroll/index.tsx +++ b/client/src/components/Scroll/index.tsx @@ -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`, { @@ -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 ( -
+
{children}
아래 스크롤 아이콘

작성한 기대평

-

{expectations}

+

+ {expectations} +

)} diff --git a/client/src/features/CasperCustom/CasperCard/CasperFlipCard.tsx b/client/src/features/CasperCustom/CasperCard/CasperFlipCard.tsx index dd73c174..3f40dd73 100644 --- a/client/src/features/CasperCustom/CasperCard/CasperFlipCard.tsx +++ b/client/src/features/CasperCustom/CasperCard/CasperFlipCard.tsx @@ -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"; diff --git a/client/src/features/CasperCustom/CustomProcess/CasperCustomFinish.tsx b/client/src/features/CasperCustom/CustomProcess/CasperCustomFinish.tsx index 56aee58c..d1092a5d 100644 --- a/client/src/features/CasperCustom/CustomProcess/CasperCustomFinish.tsx +++ b/client/src/features/CasperCustom/CustomProcess/CasperCustomFinish.tsx @@ -50,7 +50,7 @@ export function CasperCustomFinish({ const { showToast, ToastComponent } = useToast( isErrorGetShareLink ? "공유 링크 생성에 실패했습니다! 캐스퍼 봇 생성 후 다시 시도해주세요." - : "링크가 복사되었어요!" + : "🔗 링크가 복사되었어요!" ); const dispatch = useCasperCustomDispatchContext(); diff --git a/client/src/features/CasperCustom/CustomProcess/CasperCustomFinishing.tsx b/client/src/features/CasperCustom/CustomProcess/CasperCustomFinishing.tsx index 344fa134..74c53203 100644 --- a/client/src/features/CasperCustom/CustomProcess/CasperCustomFinishing.tsx +++ b/client/src/features/CasperCustom/CustomProcess/CasperCustomFinishing.tsx @@ -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; diff --git a/client/src/features/CasperShowCase/CasperCards.tsx b/client/src/features/CasperShowCase/CasperCards.tsx index d1a1cdde..a960cc48 100644 --- a/client/src/features/CasperShowCase/CasperCards.tsx +++ b/client/src/features/CasperShowCase/CasperCards.tsx @@ -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[]; @@ -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; @@ -29,7 +39,7 @@ export function CasperCards({ cardList }: CasperCardsProps) { initialX={0} gap={gap} diffX={-totalWidth} - totalWidth={totalWidth} + visibleCardCount={visibleCardCount} isEndCard={isEndTopCard} />
); diff --git a/client/src/features/CasperShowCase/TransitionCasperCardItem.tsx b/client/src/features/CasperShowCase/TransitionCasperCardItem.tsx new file mode 100644 index 00000000..13eaa09f --- /dev/null +++ b/client/src/features/CasperShowCase/TransitionCasperCardItem.tsx @@ -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(false); + + const handleMouseEnter = () => { + stopAnimation && stopAnimation(); + setIsFlipped(true); + }; + + const handleMouseLeave = () => { + startAnimation && startAnimation(); + setIsFlipped(false); + }; + + return ( +
  • + +
  • + ); +} diff --git a/client/src/features/CasperShowCase/TransitionCasperCards.tsx b/client/src/features/CasperShowCase/TransitionCasperCards.tsx index 460e4731..49673914 100644 --- a/client/src/features/CasperShowCase/TransitionCasperCards.tsx +++ b/client/src/features/CasperShowCase/TransitionCasperCards.tsx @@ -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(null); const transitionControls = useAnimation(); const [x, setX] = useState(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(false); - const { isInView, cardRef } = useLazyLoading(); + 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 ( -
  • - {isInView && ( - - )} -
  • - ); + setVisibleCardListIdx(nextIdx); + startAnimation(initialX); + } }; return ( - { - if (isEndCard(parseInt(String(latest.x)))) { - startAnimation(initialX); - } - }} - > - {cardList.map((card) => renderCardItem(card, `${card.id}`))} - {cardList.map((card) => renderCardItem(card, `${card.id}-clone`))} - + {isAnimated ? ( + + {visibleCardList.map((card, idx) => ( + startAnimation(x)} + /> + ))} + + ) : ( +
      + {visibleCardList.map((card, idx) => ( + + ))} +
    + )}
    ); } diff --git a/client/src/features/Lottery/Headline.tsx b/client/src/features/Lottery/Headline.tsx index 2443b71e..0acd1958 100644 --- a/client/src/features/Lottery/Headline.tsx +++ b/client/src/features/Lottery/Headline.tsx @@ -7,9 +7,10 @@ import { SectionKeyProps } from "@/types/sections.ts"; interface HeadlineProps extends SectionKeyProps { handleClickShortCutButton: () => void; + handleClickScroll: () => void; } -function Headline({ id, handleClickShortCutButton }: HeadlineProps) { +function Headline({ id, handleClickShortCutButton, handleClickScroll }: HeadlineProps) { return (
    - +

    캐스퍼 일렉트릭 봇이 어디서 왔는지 궁금하다면

    스크롤

    해보세요

    diff --git a/client/src/features/Lottery/Intro.tsx b/client/src/features/Lottery/Intro.tsx index 240a91b9..4a8e4bf3 100644 --- a/client/src/features/Lottery/Intro.tsx +++ b/client/src/features/Lottery/Intro.tsx @@ -1,11 +1,11 @@ -import { memo } from "react"; +import { forwardRef, memo } from "react"; import { motion } from "framer-motion"; import { ASCEND, DISSOLVE, SCROLL_MOTION } from "@/constants/animation.ts"; import { SectionKeyProps } from "@/types/sections.ts"; -function Intro({ id }: SectionKeyProps) { +const Intro = forwardRef(function Intro({ id }, ref) { return ( -
    +
    ); -} +}); const MemoizedIntro = memo(Intro); export { MemoizedIntro as Intro }; diff --git a/client/src/features/Main/Headline.tsx b/client/src/features/Main/Headline.tsx index 031b61fb..4f05c807 100644 --- a/client/src/features/Main/Headline.tsx +++ b/client/src/features/Main/Headline.tsx @@ -11,7 +11,11 @@ import { SectionKeyProps } from "@/types/sections.ts"; import { GetTotalEventDateResponse } from "@/types/totalApi.ts"; import { formatEventDateRangeWithDot } from "@/utils/formatDate.ts"; -function Headline({ id }: SectionKeyProps) { +interface HeadlineProps extends SectionKeyProps { + handleClickScroll: () => void; +} + +function Headline({ id, handleClickScroll }: HeadlineProps) { // DATA RESET TEST API const { fetchData: getRushTodayEventTest } = useFetch(() => RushAPI.getRushTodayEventTest() @@ -54,7 +58,7 @@ function Headline({ id }: SectionKeyProps) {

    - +

    이벤트에 대해 궁금하다면

    스크롤

    해 보세요

    diff --git a/client/src/features/Main/Lottery.tsx b/client/src/features/Main/Lottery.tsx index 34e5cd91..2f0ccb4c 100644 --- a/client/src/features/Main/Lottery.tsx +++ b/client/src/features/Main/Lottery.tsx @@ -1,4 +1,4 @@ -import { memo, useEffect } from "react"; +import { forwardRef, memo, useEffect } from "react"; import { Link } from "react-router-dom"; import { LotteryAPI } from "@/apis/lotteryAPI.ts"; import LotteryEvent from "@/components/LotteryEvent"; @@ -10,7 +10,7 @@ import { SectionKeyProps } from "@/types/sections.ts"; import { formatEventDateRangeWithDot } from "@/utils/formatDate.ts"; import ArrowRightIcon from "/public/assets/icons/arrow-line-right.svg?react"; -function Lottery({ id }: SectionKeyProps) { +const Lottery = forwardRef(function Lottery({ id }, ref) { const { data: lotteryData, isSuccess: isSuccessLottery, @@ -27,6 +27,7 @@ function Lottery({ id }: SectionKeyProps) { return (
    ); -} +}); const MemoizedLottery = memo(Lottery); export { MemoizedLottery as Lottery }; diff --git a/client/src/features/Main/Section.tsx b/client/src/features/Main/Section.tsx index 4550643f..09e216ed 100644 --- a/client/src/features/Main/Section.tsx +++ b/client/src/features/Main/Section.tsx @@ -1,4 +1,4 @@ -import { PropsWithChildren } from "react"; +import { PropsWithChildren, forwardRef } from "react"; import { motion } from "framer-motion"; import CTAButton from "@/components/CTAButton"; import { ASCEND, SCROLL_MOTION } from "@/constants/animation.ts"; @@ -15,21 +15,25 @@ interface SectionProps extends PropsWithChildren, SectionKeyProps { onClick?: () => void; } -export function Section({ - id, - backgroundColor, - title, - titleColor, - subtitle, - description, - descriptionColor, - children, - url, - onClick, -}: SectionProps) { +export const Section = forwardRef(function Section( + { + id, + backgroundColor, + title, + titleColor, + subtitle, + description, + descriptionColor, + children, + url, + onClick, + }, + ref +) { return (
    ); -} +}); diff --git a/client/src/features/Rush/Common/Headline.tsx b/client/src/features/Rush/Common/Headline.tsx index a1c0d5d2..379fb7cf 100644 --- a/client/src/features/Rush/Common/Headline.tsx +++ b/client/src/features/Rush/Common/Headline.tsx @@ -11,7 +11,11 @@ import { GetTotalRushEventsResponse } from "@/types/rushApi.ts"; import { SectionKeyProps } from "@/types/sections.ts"; import { getMsTime } from "@/utils/getMsTime.ts"; -export function Headline({ id }: SectionKeyProps) { +interface HeadlineProps extends SectionKeyProps { + handleClickScroll: () => void; +} + +export function Headline({ id, handleClickScroll }: HeadlineProps) { const rushData = useLoaderData() as GetTotalRushEventsResponse; const { phoneNumberState, handlePhoneNumberChange, handlePhoneNumberConfirm } = @@ -63,7 +67,7 @@ export function Headline({ id }: SectionKeyProps) {
    - +

    스크롤

    하고 캐스퍼 일렉트릭의 놀라운 성능을 알아보세요

    diff --git a/client/src/features/Rush/Common/Intro.tsx b/client/src/features/Rush/Common/Intro.tsx index 1b46a306..41e06263 100644 --- a/client/src/features/Rush/Common/Intro.tsx +++ b/client/src/features/Rush/Common/Intro.tsx @@ -1,12 +1,13 @@ -import { memo } from "react"; +import { forwardRef, memo } from "react"; import { motion } from "framer-motion"; import { ASCEND, SCROLL_MOTION } from "@/constants/animation.ts"; import { SectionKeyProps } from "@/types/sections.ts"; -function Intro({ id }: SectionKeyProps) { +const Intro = forwardRef(function Intro({ id }, ref) { return (
    @@ -29,7 +30,7 @@ function Intro({ id }: SectionKeyProps) {
    ); -} +}); const MemoizedIntro = memo(Intro); export { MemoizedIntro as Intro }; diff --git a/client/src/hooks/useScrollToTarget.ts b/client/src/hooks/useScrollToTarget.ts new file mode 100644 index 00000000..46cbd199 --- /dev/null +++ b/client/src/hooks/useScrollToTarget.ts @@ -0,0 +1,11 @@ +import { useRef } from "react"; + +export default function useScrollToTarget() { + const targetRef = useRef(null); + + const handleScrollToTarget = () => { + targetRef.current?.scrollIntoView({ behavior: "smooth" }); + }; + + return { targetRef, handleScrollToTarget }; +} diff --git a/client/src/pages/CasperShowCase/index.tsx b/client/src/pages/CasperShowCase/index.tsx index c005e155..0c98833a 100644 --- a/client/src/pages/CasperShowCase/index.tsx +++ b/client/src/pages/CasperShowCase/index.tsx @@ -53,7 +53,7 @@ export default function CasperShowCase() { const { showToast, ToastComponent } = useToast( isErrorGetShareLink ? "공유 링크 생성에 실패했습니다! 캐스퍼 봇 생성 후 다시 시도해주세요." - : "링크가 복사되었어요!" + : "🔗 링크가 복사되었어요!" ); const casperList = useLoaderData() as GetCasperListResponse; @@ -79,16 +79,32 @@ export default function CasperShowCase() { id={CASPER_SHOWCASE_SECTIONS.SHOWCASE} className="flex flex-col justify-center items-center gap-800 w-full h-screen bg-n-neutral-950 overflow-hidden pt-1000" > - -

    - 카드 위에 커서를 올리면 기대평을 볼 수 있어요 -

    + {cardListData.length === 0 ? ( + +

    + 나만의 캐스퍼 일렉트릭 봇을 만들어주세요! +

    + 캐스퍼 봇 아이콘 +
    + ) : ( + +

    + 카드 위에 커서를 올리면 기대평을 볼 수 있어요 +

    - -
    + +
    + )} diff --git a/client/src/pages/Lottery/index.tsx b/client/src/pages/Lottery/index.tsx index 766bead7..9c6922bc 100644 --- a/client/src/pages/Lottery/index.tsx +++ b/client/src/pages/Lottery/index.tsx @@ -17,6 +17,7 @@ import { import { useAuth } from "@/hooks/useAuth.ts"; import useHeaderStyleObserver from "@/hooks/useHeaderStyleObserver.ts"; import usePopup from "@/hooks/usePopup"; +import useScrollToTarget from "@/hooks/useScrollToTarget"; import useScrollTop from "@/hooks/useScrollTop"; import useToast from "@/hooks/useToast"; import { GetLotteryResponse } from "@/types/lotteryApi"; @@ -28,6 +29,8 @@ export default function Lottery() { darkSections: [LOTTERY_SECTIONS.HEADLINE, LOTTERY_SECTIONS.SHORT_CUT], }); + const { targetRef, handleScrollToTarget } = useScrollToTarget(); + const lotteryData = useLoaderData() as GetLotteryResponse; const { phoneNumberState, handlePhoneNumberChange, handlePhoneNumberConfirm } = @@ -60,8 +63,9 @@ export default function Lottery() { - + diff --git a/client/src/pages/Main/index.tsx b/client/src/pages/Main/index.tsx index 05e60f44..ed58bd12 100644 --- a/client/src/pages/Main/index.tsx +++ b/client/src/pages/Main/index.tsx @@ -3,6 +3,7 @@ import Notice from "@/components/Notice"; import { MAIN_SECTIONS } from "@/constants/PageSections/sections.ts"; import { Headline, LearnMore, Lottery, Rush } from "@/features/Main"; import useHeaderStyleObserver from "@/hooks/useHeaderStyleObserver.ts"; +import useScrollToTarget from "@/hooks/useScrollToTarget"; import useScrollTop from "@/hooks/useScrollTop.tsx"; export default function Main() { @@ -11,10 +12,12 @@ export default function Main() { darkSections: [MAIN_SECTIONS.LOTTERY, MAIN_SECTIONS.LEARN_MORE], }); + const { targetRef, handleScrollToTarget } = useScrollToTarget(); + return (
    - - + + diff --git a/client/src/pages/Rush/index.tsx b/client/src/pages/Rush/index.tsx index 43db10e0..0f8e5c00 100644 --- a/client/src/pages/Rush/index.tsx +++ b/client/src/pages/Rush/index.tsx @@ -17,6 +17,7 @@ import { ReasonSecond, } from "@/features/Rush"; import useHeaderStyleObserver from "@/hooks/useHeaderStyleObserver.ts"; +import useScrollToTarget from "@/hooks/useScrollToTarget"; import useScrollTop from "@/hooks/useScrollTop.tsx"; export default function Rush() { @@ -25,10 +26,12 @@ export default function Rush() { darkSections: [RUSH_SECTIONS.INTRO], }); + const { targetRef, handleScrollToTarget } = useScrollToTarget(); + return (
    - - + + diff --git a/client/src/types/casper.ts b/client/src/types/casper.ts new file mode 100644 index 00000000..7a358979 --- /dev/null +++ b/client/src/types/casper.ts @@ -0,0 +1,8 @@ +import { SelectedCasperIdxType } from "./casperCustom"; + +export interface CasperCardType { + id: number; + casperName: string; + expectations: string; + selectedCasperIdx: SelectedCasperIdxType; +}