diff --git a/packages/web/src/atoms/event2024FallInfo.ts b/packages/web/src/atoms/event2024FallInfo.ts new file mode 100644 index 000000000..373b464a5 --- /dev/null +++ b/packages/web/src/atoms/event2024FallInfo.ts @@ -0,0 +1,23 @@ +import { Quest, QuestId } from "@/types/event2024fall"; + +import { atom } from "recoil"; + +type CompletedQuest = { + questId: QuestId; + completedAt: Date; +}; + +export type Event2024FallInfoType = Nullable<{ + isAgreeOnTermsOfEvent: boolean; + isBanned: boolean; + creditAmount: number; + quests: Quest[]; + completedQuests: CompletedQuest[]; +}>; + +const event2024FallInfoAtom = atom({ + key: "event2024FallInfoAtom", + default: null, +}); + +export default event2024FallInfoAtom; diff --git a/packages/web/src/components/Event/CreditAmountStatusContainer/index.tsx b/packages/web/src/components/Event/CreditAmountStatusContainer/index.tsx index 40fbd2fa7..f79a31982 100644 --- a/packages/web/src/components/Event/CreditAmountStatusContainer/index.tsx +++ b/packages/web/src/components/Event/CreditAmountStatusContainer/index.tsx @@ -5,8 +5,11 @@ import WhiteContainer from "@/components/WhiteContainer"; import theme from "@/tools/theme"; import { ReactComponent as CreditIcon } from "@/static/events/2023fallCredit.svg"; +// ToDo : 2023fall 이미지 import { ReactComponent as Ticket1Icon } from "@/static/events/2023fallTicket1.svg"; +// ToDo : 2023fall 이미지 import { ReactComponent as Ticket2Icon } from "@/static/events/2023fallTicket2.svg"; +// ToDo : 2023fall 이미지 type CreditAmountStatusContainerProps = { type?: "credit" | "ticket"; diff --git a/packages/web/src/components/Event/WhiteContainerSuggestJoinEvent/index.tsx b/packages/web/src/components/Event/WhiteContainerSuggestJoinEvent/index.tsx index 4bc543d32..c9dae33e2 100644 --- a/packages/web/src/components/Event/WhiteContainerSuggestJoinEvent/index.tsx +++ b/packages/web/src/components/Event/WhiteContainerSuggestJoinEvent/index.tsx @@ -1,24 +1,24 @@ -import { useMemo, useState } from "react"; +import { useState } from "react"; import { useIsLogin, useValueRecoilState } from "@/hooks/useFetchRecoilState"; import Button from "@/components/Button"; -import LinkEvent2023FallInstagramStoryShare from "@/components/Link/LinkEvent2023FallInstagramStoryShare"; import { - ModalEvent2023FallJoin, + ModalEvent2024FallJoin, ModalNotification, } from "@/components/ModalPopup"; import WhiteContainer from "@/components/WhiteContainer"; -import { deviceType } from "@/tools/loadenv"; import theme from "@/tools/theme"; const WhiteContainerSuggestJoinEvent = () => { const isLogin = useIsLogin(); const { isAgreeOnTermsOfEvent, completedQuests } = - useValueRecoilState("event2023FallInfo") || {}; + useValueRecoilState("event2024FallInfo") || {}; + const isAdPushAgreementCompleted = completedQuests?.some( + ({ questId }) => questId === "adPushAgreement" + ); - const randomToken = useMemo(() => !!Math.floor(Math.random() * 2), []); const [isOpenJoin, setIsOpenJoin] = useState(false); const [isOpenNotification, setIsOpenNotification] = useState(false); @@ -51,44 +51,7 @@ const WhiteContainerSuggestJoinEvent = () => { 이벤트 참여하기 - ) : randomToken && - completedQuests && - !completedQuests.includes("adPushAgreement") ? ( - -
- 🌟 Taxi의 소울메이트 -
-
- Taxi 서비스를 잊지 않도록 가끔 찾아갈게요! 광고성 푸시 알림 수신 - 동의를 해주시면 방이 많이 모이는 시즌, 주변에 택시앱 사용자가 있을 - 때 알려드릴 수 있어요. -
- -
- ) : completedQuests && - !completedQuests.includes("eventSharingOnInstagram") && - deviceType.startsWith("app/") ? ( - -
- 🌟 나만 알기에는 아까운 이벤트 -
-
- 추석에 맞춰 쏟아지는 혜택들. 나만 알 순 없죠. 인스타그램 친구들에게 - 스토리로 공유해보아요. -
- - - -
- ) : completedQuests && !completedQuests.includes("adPushAgreement") ? ( + ) : completedQuests && !isAdPushAgreementCompleted ? (
🌟 Taxi의 소울메이트 @@ -107,7 +70,7 @@ const WhiteContainerSuggestJoinEvent = () => { ) : null} - diff --git a/packages/web/src/components/Footer/index.tsx b/packages/web/src/components/Footer/index.tsx index 0a2a38ad5..8403b81bf 100644 --- a/packages/web/src/components/Footer/index.tsx +++ b/packages/web/src/components/Footer/index.tsx @@ -9,7 +9,12 @@ import { ReactComponent as SparcsLogo } from "@/static/assets/sparcsLogos/Sparcs import { ReactComponent as SparcsLogoWhite } from "@/static/events/2024SparcsLogoWithTextWhite.svg"; type FooterProps = { - type?: "only-logo" | "full" | "event-2023fall" | "event-2024spring"; + type?: + | "only-logo" + | "full" + | "event-2023fall" + | "event-2024spring" + | "event-2024fall"; children?: ReactNode; }; @@ -105,6 +110,33 @@ const Footer = ({ type = "full", children }: FooterProps) => {
)} + {type === "event-2024fall" && ( + <> + + + + + + + + + + + + )} {type !== "event-2024spring" && (
diff --git a/packages/web/src/components/ModalPopup/ModalCredit.tsx b/packages/web/src/components/ModalPopup/ModalCredit.tsx index 7c9696a05..eaa76e121 100644 --- a/packages/web/src/components/ModalPopup/ModalCredit.tsx +++ b/packages/web/src/components/ModalPopup/ModalCredit.tsx @@ -14,6 +14,7 @@ import { members, members2023FallEvent, members2023SpringEvent, + members2024FallEvent, members2024SpringEvent, } from "@/static/members"; @@ -129,6 +130,11 @@ const ModalCredit = ({ name: t("page_credit.category_all"), body: , }, + { + key: "2024FallEvent", + name: t("page_credit.category_2024fall_event"), + body: , + }, { key: "2024SpringEvent", name: t("page_credit.category_2024spring_event"), diff --git a/packages/web/src/components/ModalPopup/ModalEvent2024FallItem.tsx b/packages/web/src/components/ModalPopup/ModalEvent2024FallItem.tsx new file mode 100644 index 000000000..11aafc192 --- /dev/null +++ b/packages/web/src/components/ModalPopup/ModalEvent2024FallItem.tsx @@ -0,0 +1,183 @@ +import { Dispatch, SetStateAction, useCallback, useMemo, useRef } from "react"; + +import type { EventItem } from "@/types/event2024fall"; + +import { useDelayBoolean } from "@/hooks/useDelay"; +import { + useFetchRecoilState, + useIsLogin, + useValueRecoilState, +} from "@/hooks/useFetchRecoilState"; +import { useAxios } from "@/hooks/useTaxiAPI"; + +import Button from "@/components/Button"; +import BodyRandomBox from "@/components/Event/BodyRandomBox"; +import Loading from "@/components/Loading"; +import Modal from "@/components/Modal"; + +import alertAtom from "@/atoms/alert"; +import { useSetRecoilState } from "recoil"; + +import { eventMode } from "@/tools/loadenv"; +import theme from "@/tools/theme"; + +import { ReactComponent as CreditIcon } from "@/static/events/2023fallCredit.svg"; +// ToDo : 2023fall 이미지 +import AccountBalanceWalletRoundedIcon from "@mui/icons-material/AccountBalanceWalletRounded"; + +type ModalEvent2024FallItemProps = Parameters[0] & { + itemInfo: EventItem; + fetchItems?: () => void; + setRewardItem?: Dispatch>>; + setShareItem?: Dispatch>>; +}; + +const ModalEvent2024FallItem = ({ + itemInfo, + fetchItems, + setRewardItem, + setShareItem, + ...modalProps +}: ModalEvent2024FallItemProps) => { + const fetchEvent2024FallInfo = useFetchRecoilState("event2024FallInfo"); + const event2024FallInfo = useValueRecoilState("event2024FallInfo"); + const isLogin = useIsLogin(); + + const axios = useAxios(); + const setAlert = useSetRecoilState(alertAtom); + const isDisplayRandomBox = !useDelayBoolean(!modalProps.isOpen, 500); + const isRequesting = useRef(false); + + const onClickOk = useCallback(async () => { + if (isRequesting.current) return; + isRequesting.current = true; + await axios({ + url: `/events/2024fall/items/purchase/${itemInfo._id}`, + method: "post", + onSuccess: ({ reward }) => { + fetchEvent2024FallInfo(); + fetchItems?.(); + modalProps.onChangeIsOpen?.(false); + if (itemInfo.itemType === 3 && reward) { + setRewardItem?.(reward); + } else { + setShareItem?.(itemInfo); + } + }, + onError: () => setAlert("구매를 실패하였습니다."), + }); + isRequesting.current = false; + }, [ + itemInfo._id, + fetchItems, + modalProps.onChangeIsOpen, + fetchEvent2024FallInfo, + ]); + + const [isDisabled, buttonText] = useMemo( + () => + eventMode !== "2024fall" + ? [true, "이벤트 기간이 아닙니다"] + : !event2024FallInfo || !isLogin + ? [true, "로그인 후 구매가 가능합니다"] + : event2024FallInfo.creditAmount < itemInfo.price + ? [true, "송편코인이 부족합니다"] + : [false, "구매하기"], + [eventMode, event2024FallInfo, itemInfo] + ); + + const styleTitle = { + ...theme.font18, + display: "flex", + alignItems: "center", + margin: "0 8px 12px", + }; + const styleIcon = { + fontSize: "21px", + margin: "0 4px 0 0", + }; + + return ( + +
+ + 구매하기 +
+ {itemInfo.itemType === 3 ? ( + isDisplayRandomBox ? ( + + ) : ( +
+ +
+ ) + ) : ( + {itemInfo.name} + )} +
+
{itemInfo.name}
+
{itemInfo.description}
+
+ +
{itemInfo.price}
+
+
+ +
+ + +
+
+ ); +}; + +export default ModalEvent2024FallItem; diff --git a/packages/web/src/components/ModalPopup/ModalEvent2024FallJoin.tsx b/packages/web/src/components/ModalPopup/ModalEvent2024FallJoin.tsx new file mode 100644 index 000000000..d80ea9600 --- /dev/null +++ b/packages/web/src/components/ModalPopup/ModalEvent2024FallJoin.tsx @@ -0,0 +1,249 @@ +import { useCallback, useEffect, useMemo, useState } from "react"; + +import { useEvent2024FallQuestComplete } from "@/hooks/event/useEvent2024FallQuestComplete"; +import { + useFetchRecoilState, + useIsLogin, + useValueRecoilState, +} from "@/hooks/useFetchRecoilState"; +import { useAxios } from "@/hooks/useTaxiAPI"; + +import Button from "@/components/Button"; +import DottedLine from "@/components/DottedLine"; +import Input from "@/components/Input"; +import Modal from "@/components/Modal"; + +import ProfileImage from "../User/ProfileImage"; + +import alertAtom from "@/atoms/alert"; +import { useSetRecoilState } from "recoil"; + +import regExpTest from "@/tools/regExpTest"; +import theme from "@/tools/theme"; + +import FestivalRoundedIcon from "@mui/icons-material/FestivalRounded"; + +type ModalEvent2024FallJoinProps = Parameters[0] & { + inviterId?: string; +}; + +const ModalEvent2024FallJoin = ({ + inviterId, + ...modalProps +}: ModalEvent2024FallJoinProps) => { + const axios = useAxios(); + const setAlert = useSetRecoilState(alertAtom); + const isLogin = useIsLogin(); + const { phoneNumber: phoneNumberFromLoginInfo } = + useValueRecoilState("loginInfo") || {}; + const { isAgreeOnTermsOfEvent } = + useValueRecoilState("event2024FallInfo") || {}; + const fetchLoginInfo = useFetchRecoilState("loginInfo"); + //#region event2024fall + const event2024FallQuestComplete = useEvent2024FallQuestComplete(); + //#endregion + + const [phoneNumber, setPhoneNumber] = useState(""); + const isValidPhoneNumber = useMemo( + () => regExpTest.phoneNumber(phoneNumber), + [phoneNumber] + ); + + const [inviterInfo, setInviterInfo] = useState<{ + profileImageUrl: string; + nickname: string; + }>(); + const isInvited = !!inviterId; + + useEffect(() => { + if (isAgreeOnTermsOfEvent || !isInvited) return; + axios({ + url: `/events/2024fall/invites/search/${inviterId}`, + method: "get", + onSuccess: (data) => { + setInviterInfo(data); + }, + onError: () => setAlert("올바르지 않은 추천인입니다."), + }); + }, [inviterId]); + + const onClickJoin = useCallback( + () => + axios({ + url: "/events/2024fall/globalState/create", + method: "post", + data: { phoneNumber, inviter: inviterId }, + onSuccess: () => { + fetchLoginInfo(); + //#region event2024fall + event2024FallQuestComplete("firstLogin"); + //#endregion + modalProps.onChangeIsOpen?.(false); + }, + onError: () => setAlert("이벤트 참여에 실패하였습니다."), + }), + [phoneNumber, setPhoneNumber, event2024FallQuestComplete] + ); + + const styleTitle = { + ...theme.font18, + display: "flex", + alignItems: "center", + margin: "0 8px 12px", + }; + const styleIcon = { + fontSize: "21px", + margin: "0 4px 0 0", + }; + const styleText = { + ...theme.font12, + color: theme.gray_text, + margin: "0 8px", + }; + const styleInputWrap = { + margin: "12px 8px", + display: "flex", + alignItems: "center", + color: theme.gray_text, + whiteSpace: "nowrap", + ...theme.font14, + } as const; + + // ToDo : 글 작성 + return ( + +
+ + 2024 추석 이벤트 이름 지어줘 +
+
+ • 택시 동승을 하지 않는 사용자는{" "} + + 택시 출발 시각이 지나기 전에 탑승 취소 + + 를 하여 방에서 나가야 합니다. +
+
+
+ • 실제 Taxi 동승을 하지 않고{" "} + 허위로 방을 개설하거나 참여하여 + 이벤트 퀘스트를 달성하는 것은{" "} + 부정 이용에 해당됩니다. Taxi 서비스 + 이용 중 서비스를 부정 이용하였다고 판단되거나, 신고를 받은 사용자에게는 + 사안에 따라{" "} + + 이벤트 상품이 지급되지 않을 수 있습니다. + {" "} + 위 경우, SPARCS Taxi팀 서비스 관리자는 서비스 부정 이용을 방지하기 위해 + 택시 탑승을 인증할 수 있는{" "} + 영수증 또는 카카오T 이용기록을 + 요청할 수 있습니다. 또한, 본 서비스를 부정 이용하는 사용자에게는 택시 + 서비스 이용 제한 및 법적 조치를 취할 수 있습니다. +
+
+
+ •{" "} + + 입력해주신 연락처로 이벤트 상품을 전달해드립니다. + {" "} + 또한, 서비스 신고 대응 및 본인 확인을 위해 사용될 수 있습니다.{" "} + + 입력해주신 연락처는 이후 수정이 불가능합니다. + +
+
+
+ •{" "} + + 추천인 이벤트 참여를 위해서는 추천인이 발송한 링크로 이벤트에 참여해야 + 합니다. + {" "} + 추천인을 통해 이벤트에 참여할 시, 참가자와 추천인 모두에게 700 + 송편코인이 지급됩니다. +
+
+
+ • 본 약관은 동의 이후에도 {'"'}마이페이지{">"}한가위 송편 이벤트 참여 + 약관{'"'}에서 다시 확인하실 수 있습니다.{" "} +
+ {isLogin && + (isAgreeOnTermsOfEvent ? ( + <> +
+ +
+ 전화번호 + +
+ + + ) : ( + <> +
+ +
+ 전화번호 + +
+ + + ))} + {isInvited && inviterInfo && ( +
+ 추천인 +
+ +
+ + {inviterInfo?.nickname} + +
+ )} + + ); +}; + +export default ModalEvent2024FallJoin; diff --git a/packages/web/src/components/ModalPopup/ModalEvent2024FallRandomBox.tsx b/packages/web/src/components/ModalPopup/ModalEvent2024FallRandomBox.tsx new file mode 100644 index 000000000..4af3da2b0 --- /dev/null +++ b/packages/web/src/components/ModalPopup/ModalEvent2024FallRandomBox.tsx @@ -0,0 +1,121 @@ +import { memo, useCallback, useEffect, useState } from "react"; + +import type { EventItem } from "@/types/event2024fall"; + +import { useDelay, useDelayBoolean } from "@/hooks/useDelay"; + +import Button from "@/components/Button"; +import DottedLine from "@/components/DottedLine"; +import BodyRandomBox from "@/components/Event/BodyRandomBox"; +import Loading from "@/components/Loading"; +import Modal from "@/components/Modal"; + +import "./ModalEvent2024FallRandomBoxBackground.css"; + +import theme from "@/tools/theme"; + +import HelpCenterRoundedIcon from "@mui/icons-material/HelpCenterRounded"; + +const Background = () => ( +
+
+
+
+); + +type ModalEvent2024FallRandomBoxProps = { + item?: EventItem; +} & Parameters[0]; + +const ModalEvent2024FallRandomBox = ({ + item, + ...modalProps +}: ModalEvent2024FallRandomBoxProps) => { + const [isBoxOpend, setIsBoxOpend] = useState(false); + const isDisplayRandomBox = !useDelayBoolean(!modalProps.isOpen, 500); + const isDisplayItemName = useDelay(isBoxOpend, !isBoxOpend, 6000); + const onClickOk = useCallback(() => setIsBoxOpend(true), []); + + const onChangeIsOpen = useCallback( + (isOpen: boolean) => { + modalProps?.onChangeIsOpen?.(isOpen); + }, + [item, modalProps] + ); + + useEffect(() => { + if (!modalProps.isOpen) setIsBoxOpend(false); + }, [modalProps.isOpen]); + + const styleTitle = { + ...theme.font18, + display: "flex", + alignItems: "center", + margin: "0 8px 12px", + }; + const styleIcon = { + fontSize: "21px", + margin: "0 4px 0 0", + }; + const styleText = { + ...theme.font12, + color: theme.gray_text, + margin: "0 8px 12px", + }; + + return ( + : undefined} + onEnter={onClickOk} + onChangeIsOpen={onChangeIsOpen} + > +
+ + 랜덤박스 열기 +
+
+ 랜덤박스를 획득했어요. 상자{" "} + 또는 열기 버튼을 눌러 상자 안 상품을 확인해세요! +
+ + {isDisplayRandomBox ? ( + + ) : ( +
+ +
+ )} + {isDisplayItemName && ( +
+ 축하합니다! 랜덤박스에서{" "} + + {'"'} + {item?.name || ""} + {'"'} + + 을(를) 획득하였습니다 +
+ )} + +
+ ); +}; + +export default memo(ModalEvent2024FallRandomBox); diff --git a/packages/web/src/components/ModalPopup/ModalEvent2024FallRandomBoxBackground.css b/packages/web/src/components/ModalPopup/ModalEvent2024FallRandomBoxBackground.css new file mode 100644 index 000000000..012f499b8 --- /dev/null +++ b/packages/web/src/components/ModalPopup/ModalEvent2024FallRandomBoxBackground.css @@ -0,0 +1,98 @@ +.c2024fallevent-before, +.c2024fallevent-after { + position: absolute; + width: 8px; + height: 8px; + border-radius: 50%; + box-shadow: -120px -218.66667px blue, 248px -16.66667px #00ff84, + 190px 16.33333px #002bff, -113px -308.66667px #ff009d, + -109px -287.66667px #ffb300, -50px -313.66667px #ff006e, + 226px -31.66667px #ff4000, 180px -351.66667px #ff00d0, + -12px -338.66667px #00f6ff, 220px -388.66667px #99ff00, + -69px -27.66667px #ff0400, -111px -339.66667px #6200ff, + 155px -237.66667px #00ddff, -152px -380.66667px #00ffd0, + -50px -37.66667px #00ffdd, -95px -175.66667px #a6ff00, + -88px 10.33333px #0d00ff, 112px -309.66667px #005eff, + 69px -415.66667px #ff00a6, 168px -100.66667px #ff004c, + -244px 24.33333px #ff6600, 97px -325.66667px #ff0066, + -211px -182.66667px #00ffa2, 236px -126.66667px #b700ff, + 140px -196.66667px #9000ff, 125px -175.66667px #00bbff, + 118px -381.66667px #ff002f, 144px -111.66667px #ffae00, + 36px -78.66667px #f600ff, -63px -196.66667px #c800ff, + -218px -227.66667px #d4ff00, -134px -377.66667px #ea00ff, + -36px -412.66667px #ff00d4, 209px -106.66667px #00fff2, + 91px -278.66667px #000dff, -22px -191.66667px #9dff00, + 139px -392.66667px #a6ff00, 56px -2.66667px #0099ff, + -156px -276.66667px #ea00ff, -163px -233.66667px #00fffb, + -238px -346.66667px #00ff73, 62px -363.66667px #0088ff, + 244px -170.66667px #0062ff, 224px -142.66667px #b300ff, + 141px -208.66667px #9000ff, 211px -285.66667px #ff6600, + 181px -128.66667px #1e00ff, 90px -123.66667px #c800ff, + 189px 70.33333px #00ffc8, -18px -383.66667px #00ff33, + 100px -6.66667px #ff008c; + animation: 2s c2024fallevent-bang ease-out infinite backwards, + 1s c2024fallevent-gravity ease-in infinite backwards, + 5s c2024fallevent-position linear infinite backwards; +} + +.c2024fallevent-after { + animation-delay: 2s, 3s, 15s; + animation-duration: 2s, 3s, 15s; +} + +@keyframes c2024fallevent-bang { + from { + box-shadow: 0 0 white, 0 0 white, 0 0 white, 0 0 white, 0 0 white, 0 0 white, + 0 0 white, 0 0 white, 0 0 white, 0 0 white, 0 0 white, 0 0 white, + 0 0 white, 0 0 white, 0 0 white, 0 0 white, 0 0 white, 0 0 white, + 0 0 white, 0 0 white, 0 0 white, 0 0 white, 0 0 white, 0 0 white, + 0 0 white, 0 0 white, 0 0 white, 0 0 white, 0 0 white, 0 0 white, + 0 0 white, 0 0 white, 0 0 white, 0 0 white, 0 0 white, 0 0 white, + 0 0 white, 0 0 white, 0 0 white, 0 0 white, 0 0 white, 0 0 white, + 0 0 white, 0 0 white, 0 0 white, 0 0 white, 0 0 white, 0 0 white, + 0 0 white, 0 0 white, 0 0 white; + } +} + +@keyframes c2024fallevent-gravity { + from { + transform: translateY(0); + opacity: 1; + } + to { + transform: translateY(500px); + opacity: 0; + } +} + +@keyframes c2024fallevent-position { + 0%, + 19.9% { + margin-top: 10%; + margin-left: 40%; + } + + 20%, + 39.9% { + margin-top: 40%; + margin-left: 30%; + } + + 40%, + 59.9% { + margin-top: 20%; + margin-left: 70%; + } + + 60%, + 79.9% { + margin-top: 30%; + margin-left: 20%; + } + + 80%, + 99.9% { + margin-top: 30%; + margin-left: 80%; + } +} diff --git a/packages/web/src/components/ModalPopup/index.tsx b/packages/web/src/components/ModalPopup/index.tsx index 04d3daa20..dbcff316e 100644 --- a/packages/web/src/components/ModalPopup/index.tsx +++ b/packages/web/src/components/ModalPopup/index.tsx @@ -12,6 +12,9 @@ export { default as ModalEvent2023FallRandomBox } from "./ModalEvent2023FallRand export { default as ModalEvent2024SpringAbuseWarning } from "./ModalEvent2024SpringAbuseWarning"; export { default as ModalEvent2024SpringJoin } from "./ModalEvent2024SpringJoin"; export { default as ModalEvent2024SpringShare } from "./ModalEvent2024SpringShare"; +export { default as ModalEvent2024FallItem } from "./ModalEvent2024FallItem"; +export { default as ModalEvent2024FallJoin } from "./ModalEvent2024FallJoin"; +export { default as ModalEvent2024FallRandomBox } from "./ModalEvent2024FallRandomBox"; export { default as ModalMypageModify } from "./ModalMypageModify"; export { default as ModalNotification } from "./ModalNotification"; diff --git a/packages/web/src/components/Skeleton/Routes.tsx b/packages/web/src/components/Skeleton/Routes.tsx index e72fe0b31..e77895bc6 100644 --- a/packages/web/src/components/Skeleton/Routes.tsx +++ b/packages/web/src/components/Skeleton/Routes.tsx @@ -23,12 +23,7 @@ const routeProps = [ exact: true, }, { - path: ["/home", "/home/:roomId", "/home/startEvent/:inviterId"], - component: lazy(() => import("@/pages/Home")), - exact: true, - }, - { - path: "/event/2024spring-invite/:eventStatusId", + path: ["/home", "/home/:roomId", "/home/eventJoin/:inviterId"], component: lazy(() => import("@/pages/Home")), exact: true, }, diff --git a/packages/web/src/components/Skeleton/index.tsx b/packages/web/src/components/Skeleton/index.tsx index 46a2bb9db..6ebff3f70 100644 --- a/packages/web/src/components/Skeleton/index.tsx +++ b/packages/web/src/components/Skeleton/index.tsx @@ -1,7 +1,7 @@ import { ReactNode, useMemo } from "react"; import { useLocation } from "react-router-dom"; -import { useEvent2024SpringEffect } from "@/hooks/event/useEvent2024SpringEffect"; +import { useEvent2024FallEffect } from "@/hooks/event/useEvent2024FallEffect"; import useCSSVariablesEffect from "@/hooks/skeleton/useCSSVariablesEffect"; import useChannelTalkEffect from "@/hooks/skeleton/useChannelTalkEffect"; import useFirebaseMessagingEffect from "@/hooks/skeleton/useFirebaseMessagingEffect"; @@ -64,8 +64,8 @@ const Skeleton = ({ children }: SkeletonProps) => { [pathname] ); - //#region event2023Fall - useEvent2024SpringEffect(); + //#region event2024Fall + useEvent2024FallEffect(); //#endregion useSyncRecoilStateEffect(); // loginIngo, taxiLocations, myRooms, notificationOptions 초기화 및 동기화 useI18nextEffect(); diff --git a/packages/web/src/hooks/event/useEvent2024FallEffect.ts b/packages/web/src/hooks/event/useEvent2024FallEffect.ts new file mode 100644 index 000000000..a15060b58 --- /dev/null +++ b/packages/web/src/hooks/event/useEvent2024FallEffect.ts @@ -0,0 +1,40 @@ +import { useEffect, useRef } from "react"; + +import { useValueRecoilState } from "@/hooks/useFetchRecoilState"; + +import toast from "@/tools/toast"; + +export const useEvent2024FallEffect = () => { + const { completedQuests, quests } = + useValueRecoilState("event2024FallInfo") || {}; + + const prevCompletedQuestsRef = useRef(); + + useEffect(() => { + if (!completedQuests || !quests) return; + prevCompletedQuestsRef.current = + prevCompletedQuestsRef.current ?? completedQuests.length; + + const lengthDiff = completedQuests.length - prevCompletedQuestsRef.current; + if (lengthDiff <= 0) { + prevCompletedQuestsRef.current = completedQuests.length; + return; + } + + const newCompletedQuests = completedQuests.slice(-lengthDiff); + newCompletedQuests.forEach(({ questId }) => { + const quest = quests.find(({ id }) => id === questId); + if (!quest) return; + const notificationValue = { + type: "default" as const, + imageUrl: quest.imageUrl, + title: "퀘스트 완료", + subtitle: "추석 이벤트", + content: `축하합니다! "${quest.name}" 퀘스트를 완료하여 송편코인 ${quest.reward.credit}개를 획득하셨습니다.`, + button: { text: "확인하기", path: "/event/2024fall-missions" }, + }; + toast(notificationValue); + }); + prevCompletedQuestsRef.current = completedQuests.length; + }, [completedQuests]); +}; diff --git a/packages/web/src/hooks/event/useEvent2024FallQuestComplete.ts b/packages/web/src/hooks/event/useEvent2024FallQuestComplete.ts new file mode 100644 index 000000000..bb4aef945 --- /dev/null +++ b/packages/web/src/hooks/event/useEvent2024FallQuestComplete.ts @@ -0,0 +1,36 @@ +import { useCallback } from "react"; + +import type { QuestId } from "@/types/event2024fall"; + +import { + useFetchRecoilState, + useValueRecoilState, +} from "@/hooks/useFetchRecoilState"; +import { useAxios } from "@/hooks/useTaxiAPI"; + +export const useEvent2024FallQuestComplete = () => { + const axios = useAxios(); + const fetchEvent2024FallInfo = useFetchRecoilState("event2024FallInfo"); + const { completedQuests, quests } = + useValueRecoilState("event2024FallInfo") || {}; + + return useCallback( + (id: QuestId) => { + if (!completedQuests || !quests) return; + const questMaxCount = + quests.find((quest) => quest.id === id)?.maxCount ?? 0; + const questCompletedCount = completedQuests?.filter( + ({ questId }) => questId === id + ).length; + if (questMaxCount > 0 && questCompletedCount >= questMaxCount) return; + if (["roomSharing", "dailyAttendance"].includes(id)) { + axios({ + url: `/events/2024fall/quests/complete/${id}`, + method: "post", + onSuccess: () => fetchEvent2024FallInfo(), + }); + } else fetchEvent2024FallInfo(); + }, + [completedQuests, fetchEvent2024FallInfo, quests] + ); +}; diff --git a/packages/web/src/hooks/useFetchRecoilState/index.tsx b/packages/web/src/hooks/useFetchRecoilState/index.tsx index a6701ce67..5d817d523 100644 --- a/packages/web/src/hooks/useFetchRecoilState/index.tsx +++ b/packages/web/src/hooks/useFetchRecoilState/index.tsx @@ -5,6 +5,11 @@ import { useSetEvent2023FallInfo, useValueEvent2023FallInfo, } from "./useFetchEvent2023FallInfo"; +import { + useFetchEvent2024FallInfo, + useSetEvent2024FallInfo, + useValueEvent2024FallInfo, +} from "./useFetchEvent2024FallInfo"; import { useFetchEvent2024SpringInfo, useSetEvent2024SpringInfo, @@ -32,6 +37,7 @@ import { } from "./useFetchTaxiLocations"; import { Event2023FallInfoType } from "@/atoms/event2023FallInfo"; +import { Event2024FallInfoType } from "@/atoms/event2024FallInfo"; import { Event2024SpringInfoType } from "@/atoms/event2024SpringInfo"; import { LoginInfoType } from "@/atoms/loginInfo"; import { MyRoomsType } from "@/atoms/myRooms"; @@ -44,7 +50,8 @@ export type AtomName = | "myRooms" | "notificationOptions" | "event2023FallInfo" - | "event2024SpringInfo"; + | "event2024SpringInfo" + | "event2024FallInfo"; type useValueRecoilStateType = { (atomName: "loginInfo"): LoginInfoType; @@ -53,6 +60,7 @@ type useValueRecoilStateType = { (atomName: "notificationOptions"): notificationOptionsType; (atomName: "event2023FallInfo"): Event2023FallInfoType; (atomName: "event2024SpringInfo"): Event2024SpringInfoType; + (atomName: "event2024FallInfo"): Event2024FallInfoType; }; const _useValueRecoilState = (atomName: AtomName) => { switch (atomName) { @@ -68,6 +76,8 @@ const _useValueRecoilState = (atomName: AtomName) => { return useValueEvent2023FallInfo(); case "event2024SpringInfo": return useValueEvent2024SpringInfo(); + case "event2024FallInfo": + return useValueEvent2024FallInfo(); } }; export const useValueRecoilState = @@ -87,6 +97,8 @@ export const useSetRecoilState = (atomName: AtomName) => { return useSetEvent2023FallInfo(); case "event2024SpringInfo": return useSetEvent2024SpringInfo(); + case "event2024FallInfo": + return useSetEvent2024FallInfo(); } }; @@ -104,6 +116,8 @@ export const useFetchRecoilState = (atomName: AtomName) => { return useFetchEvent2023FallInfo(); case "event2024SpringInfo": return useFetchEvent2024SpringInfo(); + case "event2024FallInfo": + return useFetchEvent2024FallInfo(); } }; @@ -134,6 +148,10 @@ export const useSyncRecoilStateEffect = () => { // event2024SpringInfo 초기화 및 동기화 const fetchEvent2024SpringInfo = useFetchRecoilState("event2024SpringInfo"); useEffect(fetchEvent2024SpringInfo, [userId]); + + // event2024FallInfo 초기화 및 동기화 + const fetchEvent2024FallInfo = useFetchRecoilState("event2024FallInfo"); + useEffect(fetchEvent2024FallInfo, [userId]); }; export const useIsLogin = (): boolean => { diff --git a/packages/web/src/hooks/useFetchRecoilState/useFetchEvent2024FallInfo.tsx b/packages/web/src/hooks/useFetchRecoilState/useFetchEvent2024FallInfo.tsx new file mode 100644 index 000000000..46a4c0140 --- /dev/null +++ b/packages/web/src/hooks/useFetchRecoilState/useFetchEvent2024FallInfo.tsx @@ -0,0 +1,31 @@ +import { useCallback } from "react"; + +import { useAxios } from "@/hooks/useTaxiAPI"; +import { AxiosOption } from "@/hooks/useTaxiAPI/useAxios"; + +import event2024FallInfoAtom from "@/atoms/event2024FallInfo"; +import { useRecoilValue, useSetRecoilState } from "recoil"; + +import { eventMode } from "@/tools/loadenv"; + +export const useValueEvent2024FallInfo = () => + useRecoilValue(event2024FallInfoAtom); +export const useSetEvent2024FallInfo = () => + useSetRecoilState(event2024FallInfoAtom); +export const useFetchEvent2024FallInfo = () => { + const setEvent2024FallInfo = useSetEvent2024FallInfo(); + const axios = useAxios(); + + return useCallback((onError?: AxiosOption["onError"]) => { + if (eventMode === "2024fall") { + axios({ + url: "/events/2024fall/globalState/", + method: "get", + onSuccess: (data) => setEvent2024FallInfo(data), + onError: onError, + }); + } else { + setEvent2024FallInfo(null); + } + }, []); +}; diff --git a/packages/web/src/pages/Addroom/index.tsx b/packages/web/src/pages/Addroom/index.tsx index 15bdb6cbb..e87057ad4 100644 --- a/packages/web/src/pages/Addroom/index.tsx +++ b/packages/web/src/pages/Addroom/index.tsx @@ -2,7 +2,7 @@ import { useEffect, useMemo, useRef, useState } from "react"; import { useCookies } from "react-cookie"; import { useHistory } from "react-router-dom"; -import { useEvent2024SpringQuestComplete } from "@/hooks/event/useEvent2024SpringQuestComplete"; +import { useEvent2024FallQuestComplete } from "@/hooks/event/useEvent2024FallQuestComplete"; import { useFetchRecoilState, useIsLogin, @@ -64,8 +64,8 @@ const AddRoom = () => { const isLogin = useIsLogin(); const myRooms = useValueRecoilState("myRooms"); const fetchMyRooms = useFetchRecoilState("myRooms"); - //#region event2024Spring - const event2024SpringQuestComplete = useEvent2024SpringQuestComplete(); + //#region event2024fall + const event2024FallQuestComplete = useEvent2024FallQuestComplete(); const [isOpenModalEventAbuseWarning, setIsOpenModalEventAbuseWarning] = useState(false); //#endregion @@ -141,10 +141,10 @@ const AddRoom = () => { if (!onCall.current) { onCall.current = true; - // #region event2024Spring + // #region event2024fall let isAgreeOnTermsOfEvent = false; await axios({ - url: "/events/2024spring/globalState", + url: "/events/2024fall/globalState", method: "get", onSuccess: (data) => { if (data.isAgreeOnTermsOfEvent) { @@ -191,8 +191,8 @@ const AddRoom = () => { }, onSuccess: () => { fetchMyRooms(); - //#region event2024Spring - event2024SpringQuestComplete("firstRoomCreation"); + //#region event2024fall + event2024FallQuestComplete("firstRoomCreation"); //#endregion history.push("/myroom"); }, @@ -271,8 +271,8 @@ const AddRoom = () => { }, onSuccess: () => { fetchMyRooms(); - //#region event2024spring - event2024SpringQuestComplete("firstRoomCreation"); + //#region event2024fall + event2024FallQuestComplete("firstRoomCreation"); //#endregion history.push("/myroom"); }, diff --git a/packages/web/src/pages/Event/Event2024Fall.tsx b/packages/web/src/pages/Event/Event2024Fall.tsx new file mode 100644 index 000000000..77c74f28b --- /dev/null +++ b/packages/web/src/pages/Event/Event2024Fall.tsx @@ -0,0 +1,260 @@ +import { memo } from "react"; + +import AdaptiveDiv from "@/components/AdaptiveDiv"; +import Button from "@/components/Button"; +import Footer from "@/components/Footer"; +import HeaderWithBackButton from "@/components/Header/HeaderWithBackButton"; +import WhiteContainer from "@/components/WhiteContainer"; + +import theme from "@/tools/theme"; + +import { ReactComponent as TaxiLogoIcon } from "@/static/assets/sparcsLogos/TaxiLogo.svg"; +import { ReactComponent as MissionCompleteIcon } from "@/static/events/2023fallMissionComplete.svg"; +import { ReactComponent as MainSection1 } from "@/static/events/2024fallMainSection1.svg"; +import { ReactComponent as MainSection2 } from "@/static/events/2024fallMainSection2.svg"; +import { ReactComponent as MainSection4 } from "@/static/events/2024fallMainSection4.svg"; +import { ReactComponent as MainSection6 } from "@/static/events/2024fallMainSection6.svg"; +import { ReactComponent as MainStep2 } from "@/static/events/2024fallMainStep2.svg"; +import { ReactComponent as MainStep3 } from "@/static/events/2024fallMainStep3.svg"; +import { ReactComponent as MainTitle } from "@/static/events/2024fallMainTitle.svg"; + +const EVENT_INSTAGRAM_URL = + "https://www.instagram.com/p/C_H7YTfPEGZ/?igsh=MXh3MWc0NnJsZml3MQ=="; + +const Event2024Fall = () => ( + <> + +
이벤트 안내
+
+ + + +
+ 2024.9.7.(토) ~ 9.23.(월) +
+ +
+
+ + + +
+
+ + +
+ STEP 1 +
+
+
+ Taxi 퀘스트 달성하고 +
+ 송편코인을 모아보세요! +
+
+ +
+
+ Taxi 웹 사이트와 앱에서 퀘스트 내용 확인 +
+ 이벤트 참여 동의만 해도 송편코인 200개 지급 +
+
+ {/* */} + + {/* */} + +
+ +
+ STEP 2 +
+
+
+ 응모권 교환소에서 +
+ 경품 응모권을 구매해 보세요! +
+
+ +
+
+ 응모권은 경품마다 별개로 발급됨 +
+ 경품 추첨 결과는 9월 30일에 발표 +
+
+ {/* */} + + {/* */} + +
+ +
+ STEP 3 +
+
+
+ 경품 당첨 확률 +
+ 리더보드를 확인하세요! +
+
+ +
+
+ 응모권 개수가 많을수록 당첨 확률이 상승함 +
위 이미지는 실제와 다를 수 있음 +
+
+ {/* */} + + {/* */} + + +
+
window.open(EVENT_INSTAGRAM_URL, "_blank")} + > + +
EVENT
+
+
+ 인스타그램 스토리 공유하고 +
+ 공유 이벤트에 참여하세요! +
+
+ +
+
+ {/* 추첨 결과는 인스타그램, Ara, Taxi 홈페이지에 발표 +
+ 실물 상품 또는 기프티콘으로 지급 +
*/} + 인스타그램 게시물은 9월 6일 정오에 업로드 예정 +
+ +
+
+ + +
+
+ 본 이벤트는 현대모비스와 에이핀아이앤씨의 후원으로 진행되었습니다. +
+ +
+