diff --git a/packages/web/src/components/Event/BodyRandomBox/index.tsx b/packages/web/src/components/Event/BodyRandomBox/index.tsx index 0f73a735b..dbd85e79f 100644 --- a/packages/web/src/components/Event/BodyRandomBox/index.tsx +++ b/packages/web/src/components/Event/BodyRandomBox/index.tsx @@ -14,12 +14,14 @@ type BodyRandomBoxProps = { isBoxOpend: boolean; onClickBox?: () => void; itemImageUrl?: string; + nonClick?: boolean; }; const BodyRandomBox = ({ isBoxOpend, onClickBox, itemImageUrl, + nonClick, }: BodyRandomBoxProps) => { const bodyRef = useRef(null); const getBodyWidth = useCallback(() => bodyRef.current?.clientWidth || 0, []); @@ -81,7 +83,7 @@ const BodyRandomBox = ({ height: `${boxSize}px`, aspectRatio: 1, margin: `0 auto`, - ...theme.cursor(isBoxOpend), + ...theme.cursor(isBoxOpend || nonClick), transform: `scale(${ (boxSize / 500) * 0.8 }) translateX(-160px) translateY(-70px)`, diff --git a/packages/web/src/components/Event/CreditAmountStatusContainer/index.tsx b/packages/web/src/components/Event/CreditAmountStatusContainer/index.tsx index f79a31982..bb1726cef 100644 --- a/packages/web/src/components/Event/CreditAmountStatusContainer/index.tsx +++ b/packages/web/src/components/Event/CreditAmountStatusContainer/index.tsx @@ -5,11 +5,9 @@ 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 이미지 + +// import { ReactComponent as Ticket1Icon } from "@/static/events/2023fallTicket1.svg"; +// import { ReactComponent as Ticket2Icon } from "@/static/events/2023fallTicket2.svg"; type CreditAmountStatusContainerProps = { type?: "credit" | "ticket"; @@ -19,8 +17,7 @@ const CreditAmountStatusContainer = ({ type = "credit", ...whiteContainerProps }: CreditAmountStatusContainerProps) => { - const { creditAmount, ticket1Amount, ticket2Amount } = - useValueRecoilState("event2023FallInfo") || {}; + const { creditAmount } = useValueRecoilState("event2024FallInfo") || {}; return (
- {type === "credit" ? "내가 모은 송편" : "일반 / 고급 응모권"} + {type === "credit" ? "내가 모은 송편코인" : "일반 / 고급 응모권"}
- {type === "credit" ? ( - <> - -
- {creditAmount || 0} -
- + <> + +
+ {creditAmount || 0} +
+ + {/* {type === "credit" ? ( ) : ( <> - )} + )} */}
); }; diff --git a/packages/web/src/components/Event/DailyAttendanceCalendar/index.tsx b/packages/web/src/components/Event/DailyAttendanceCalendar/index.tsx new file mode 100644 index 000000000..12aed4133 --- /dev/null +++ b/packages/web/src/components/Event/DailyAttendanceCalendar/index.tsx @@ -0,0 +1,211 @@ +import { memo } from "react"; + +import { useValueRecoilState } from "@/hooks/useFetchRecoilState"; + +import MiniCircle from "@/components/MiniCircle"; + +import moment, { getToday } from "@/tools/moment"; +import theme from "@/tools/theme"; + +import { ReactComponent as MissionCompleteIcon } from "@/static/events/2023fallMissionComplete.svg"; + +const getCalendarDates = () => { + const startDate = moment("2024-09-06", "YYYY-MM-DD"); + const endDate = moment("2024-09-24", "YYYY-MM-DD"); + const endDateOfMonth = moment("2024-09-30", "YYYY-MM-DD"); + const today = getToday(); + // const today = moment("2024-09-10", "YYYY-MM-DD"); // FIXME: 배포 전에 수정 + const date = startDate.clone(); + date.subtract(date.day(), "day"); + const event2024FallInfo = useValueRecoilState("event2024FallInfo"); + const completedDates = event2024FallInfo?.completedQuests.reduce( + (acc, { questId, completedAt }) => { + if (questId === "dailyAttendance" && completedAt) { + acc.push(moment(completedAt).format("YYYY-MM-DD")); + } + return acc; + }, + [] as string[] + ); + + const calendar = []; + + for (let i = 0; i < 5; i++) { + const week = []; + for (let i = 0; i < 7; i++) { + let available = null; + let checked = false; + if (date.isSame(today)) { + available = "today"; + } else if (date.isAfter(startDate) && date.isBefore(today)) { + available = "past"; + } else if (date.isBefore(endDate) && date.isAfter(startDate, "day")) { + available = true; + } + + if (completedDates?.includes(date.format("YYYY-MM-DD"))) { + checked = true; + } + + week.push({ + year: date.year(), + month: date.month() + 1, + date: date.date(), + available, + checked, + }); + if (date.isSame(endDateOfMonth)) { + break; + } + date.add(1, "day"); + } + calendar.push(week); + } + return calendar; +}; +type DateProps = { + index: number; + year: number; + month: number; + date: number; + available: string | boolean | null; + checked: boolean; +}; + +const Date = ({ index, date, available, checked }: DateProps) => { + const style = { + width: "calc((100% - 36px) / 7)", + aspectRatio: "1 / 1", + height: "100%", + }; + const styleBox: React.CSSProperties = { + ...style, + borderRadius: "6px", + position: "relative", + display: "flex", + alignItems: "center", + justifyContent: "center", + background: available ? theme.white : theme.gray_background, + transitionDuration: theme.duration, + }; + const styleDate = { + ...theme.font12, + letterSpacing: undefined, + marginTop: "3px", + color: + available === "past" || !available + ? theme.gray_line + : index === 0 + ? theme.red_text + : index === 6 + ? theme.blue_text + : theme.black, + }; + const styleToday: React.CSSProperties = { + position: "absolute", + top: "calc(50% + 8px)", + left: "calc(50% - 2px)", + }; + const styleCompleteIcon: React.CSSProperties = { + position: "absolute", + height: "34px", + width: "34px", + }; + + if (!date) return
; + return ( +
+
{date}
+ {available === "today" && ( +
+ +
+ )} + {checked && } +
+ ); +}; +const MemoizedDate = memo(Date); + +const DailyAttendanceCalendar = () => { + const dateInfo = getCalendarDates(); + + const styleMonth: React.CSSProperties = { + display: "flex", + flexDirection: "column", + rowGap: "6px", + marginBottom: "5px", + }; + const styleDay: React.CSSProperties = { + display: "flex", + margin: "12px 0 8px", + columnGap: "6px", + }; + const styleDayItem: React.CSSProperties = { + width: "calc((100% - 36px) / 7)", + fontSize: "10px", + height: "12px", + textAlign: "center", + }; + const styleWeek = { + display: "flex", + columnGap: "6px", + }; + + const week: { color: string; text: string }[] = [ + { color: theme.red_text, text: "일" }, + { color: theme.black, text: "월" }, + { color: theme.black, text: "화" }, + { color: theme.black, text: "수" }, + { color: theme.black, text: "목" }, + { color: theme.black, text: "금" }, + { color: theme.blue_text, text: "토" }, + ]; + + return ( +
+
+ {week.map((item, index) => ( +
+ {item.text} +
+ ))} +
+
+ {dateInfo.map((item, index) => { + return ( +
+ {item.map((item, index) => ( + + ))} +
+ ); + })} +
+
+ ); +}; + +export default DailyAttendanceCalendar; diff --git a/packages/web/src/components/Event/EventItemContainer/index.tsx b/packages/web/src/components/Event/EventItemContainer/index.tsx new file mode 100644 index 000000000..4812f032e --- /dev/null +++ b/packages/web/src/components/Event/EventItemContainer/index.tsx @@ -0,0 +1,136 @@ +import { useState } from "react"; +import { useHistory } from "react-router-dom"; + +import type { EventItem, RandomBoxResult } from "@/types/event2024fall"; + +import { useFetchRecoilState } from "@/hooks/useFetchRecoilState"; + +import { + ModalEvent2024FallItem, + ModalEvent2024FallRandomBox, +} from "@/components/ModalPopup"; +import WhiteContainer from "@/components/WhiteContainer"; + +import theme from "@/tools/theme"; + +// ToDo : 2023fall 이미지 +import { ReactComponent as CreditIcon } from "@/static/events/2023fallCredit.svg"; + +type EventItemComponentProps = { + value: EventItem; + fetchItems?: () => void; + clickable?: boolean; + showDescription?: boolean; +}; + +const EventItemContainer = ({ + value, + fetchItems, + clickable, + showDescription, +}: EventItemComponentProps) => { + const fetchEvent2024FallInfo = useFetchRecoilState("event2024FallInfo"); + const history = useHistory(); + const [isOpen, setIsOpen] = useState(false); + const [randomBoxResult, setRandomBoxResult] = + useState>(null); + const onClickHandler = () => { + if (value.itemType !== 3) { + history.push(`/event/2024fall-store/item/${value._id}`); + } else { + // setRewardItem(value); + setIsOpen(true); + } + }; + + return ( + +
+ {value.name} +
+
+ {value.name} +
+ {showDescription && ( +
+ {value.description} +
+ )} +
+ +
+ {value.price} +
+
+ + {value.itemType === 3 && ( + { + setRandomBoxResult(null); + fetchEvent2024FallInfo(); + }} + randomBoxResult={randomBoxResult || undefined} + /> + )} +
+ ); +}; + +export default EventItemContainer; diff --git a/packages/web/src/components/Event/WhiteContainerSuggestShareEvent/index.tsx b/packages/web/src/components/Event/WhiteContainerSuggestShareEvent/index.tsx new file mode 100644 index 000000000..0fb4050ae --- /dev/null +++ b/packages/web/src/components/Event/WhiteContainerSuggestShareEvent/index.tsx @@ -0,0 +1,81 @@ +import { useEffect, useState } from "react"; + +import { useIsLogin, useValueRecoilState } from "@/hooks/useFetchRecoilState"; +import { useAxios } from "@/hooks/useTaxiAPI"; + +import Button from "@/components/Button"; +import { ModalEvent2024FallShare } from "@/components/ModalPopup"; +import WhiteContainer from "@/components/WhiteContainer"; + +import alertAtom from "@/atoms/alert"; +import { useSetRecoilState } from "recoil"; + +import theme from "@/tools/theme"; + +const WhiteContainerSuggestShareEvent = () => { + const isLogin = useIsLogin(); + const { isAgreeOnTermsOfEvent } = + useValueRecoilState("event2024FallInfo") || {}; + const [inviteUrl, setInviteUrl] = useState(""); + const [isOpenShare, setIsOpenShare] = useState(false); + const axios = useAxios(); + const setAlert = useSetRecoilState(alertAtom); + + const styleText = { + ...theme.font14, + marginBottom: "12px", + }; + const styleButton = { + padding: "14px 0 13px", + borderRadius: "12px", + ...theme.font14_bold, + }; + + useEffect(() => { + if (isAgreeOnTermsOfEvent) + axios({ + url: `/events/2024fall/invites/create`, + method: "post", + onSuccess: ({ inviteUrl }) => { + setInviteUrl(inviteUrl); + }, + onError: () => setAlert("초대 링크를 생성하지 못했습니다."), + }); + }, [isAgreeOnTermsOfEvent]); + + return ( + <> + {isLogin && isAgreeOnTermsOfEvent && ( + +
+ 🎊 이벤트 공유하기 +
+
+ 이벤트를 공유하여 친구가 이벤트에 참여하면, 친구와 함께 송편코인 + 700개를 받을 수 있어요! +
+ + +
+ )} + + ); +}; + +export default WhiteContainerSuggestShareEvent; diff --git a/packages/web/src/components/Header/HeaderWithLeftNav.tsx b/packages/web/src/components/Header/HeaderWithLeftNav.tsx index a72d89409..8853b361d 100644 --- a/packages/web/src/components/Header/HeaderWithLeftNav.tsx +++ b/packages/web/src/components/Header/HeaderWithLeftNav.tsx @@ -53,7 +53,7 @@ const HeaderWithLeftNav = ({ value, options = [] }: HeaderWithLeftNavProps) => (
{options.map(({ value: _value, label, to }) => ( - + {label} diff --git a/packages/web/src/components/ModalPopup/Body/BodyEvent2024FallAbuseWarning.tsx b/packages/web/src/components/ModalPopup/Body/BodyEvent2024FallAbuseWarning.tsx new file mode 100644 index 000000000..418279fd1 --- /dev/null +++ b/packages/web/src/components/ModalPopup/Body/BodyEvent2024FallAbuseWarning.tsx @@ -0,0 +1,132 @@ +import { useState } from "react"; + +import Button from "@/components/Button"; +import DottedLine from "@/components/DottedLine"; + +import theme from "@/tools/theme"; + +import CheckRoundedIcon from "@mui/icons-material/CheckRounded"; + +type BodyEvent2024FallAbuseWarningProps = { + onChangeIsOpen?: (isOpen: boolean) => void; +}; + +const BodyEvent2024FallAbuseWarning = ({ + onChangeIsOpen, +}: BodyEvent2024FallAbuseWarningProps) => { + const styleText = { + ...theme.font12, + color: theme.gray_text, + margin: "0 8px 12px", + }; + const styleTextWarn = { + ...theme.font12, + display: "flex", + alignItem: "center", + marginLeft: "5px", + lineHeight: "16px", + color: theme.red_text, + }; + const styleButtons = { + position: "relative", + display: "flex", + justifyContent: "space-between", + gap: "10px", + } as const; + const styleWarn = { + display: "flex", + margin: "12px 8px", + gap: "6px", + ...theme.cursor(), + }; + const styleCheckBox = { + width: "16px", + height: "16px", + overflow: "hidden", + borderRadius: "50%", + background: theme.purple_light, + boxShadow: theme.shadow_purple_input_inset, + transitionDuration: theme.duration, + display: "flex", + alignItems: "center", + justifyContent: "center", + }; + const styleCheckBoxIcon = { + width: "14px", + height: "14px", + fill: theme.white, + }; + const [isAgree, setIsAgree] = useState(false); + + return ( + <> +
+ 짧은 시간 동안 반복적으로 여러 개의 방을 생성하신 것 같습니다. +
+
+ • 방 생성 후 정산/송금 기능을 사용 + 하실 경우, 실제로 택시 탑승이 이루어졌는지 확인하기 위해 Taxi팀에서{" "} + 영수증 등 증빙 서류의 제출을 요청 + 드릴 수 있습니다. +
+
+ • 실제로 택시에 탑승하지 않으신 경우,{" "} + + 획득한 송편코인과 경품 응모권이 모두 회수되고 + {" "} + 추가적인 이벤트 참여가 제한될 수 + 있습니다. +
+
+ • 자세한 이벤트 약관은 {'"'}마이 페이지{">"}추석 이벤트 참여 약관{'"'} + 에서 확인하실 수 있습니다.{" "} +
+
그래도 방을 생성할까요?
+ +
+
setIsAgree(!isAgree)}> +
+ +
+
+ 이 방은 실제로 택시에 탑승하기 위한 목적의 방입니다. +
+
+
+
+ + +
+ + ); +}; + +export default BodyEvent2024FallAbuseWarning; diff --git a/packages/web/src/components/ModalPopup/Body/BodyEvent2024FallShare.tsx b/packages/web/src/components/ModalPopup/Body/BodyEvent2024FallShare.tsx new file mode 100644 index 000000000..c26c1e2ab --- /dev/null +++ b/packages/web/src/components/ModalPopup/Body/BodyEvent2024FallShare.tsx @@ -0,0 +1,114 @@ +import { useCallback, useEffect, useState } from "react"; +import QRCode from "react-qr-code"; + +import ButtonShare from "@/components/Button/ButtonShare"; +import DottedLine from "@/components/DottedLine"; +import LinkCopy from "@/components/Link/LinkCopy"; +import LinkKakaotalkShare from "@/components/Link/LinkKakaotalkShare"; + +import { ogServer } from "@/tools/loadenv"; +import theme from "@/tools/theme"; + +import { ReactComponent as KakaoTalkLogo } from "@/static/assets/serviceLogos/KakaoTalkLogo.svg"; +import CheckIcon from "@mui/icons-material/Check"; +import ContentCopyIcon from "@mui/icons-material/ContentCopy"; + +export type BodyEvent2024FallShareProps = { + inviteUrl: string; + height?: number; +}; + +const BodyEvent2024FallShare = ({ + height, + inviteUrl, +}: BodyEvent2024FallShareProps) => { + const [isCopied, setIsCopied] = useState(false); + const onCopy = useCallback(() => setIsCopied(true), [setIsCopied]); + + useEffect(() => { + if (isCopied) { + const timer = setTimeout(() => setIsCopied(false), 1000); + return () => clearTimeout(timer); + } + }, [isCopied]); + + const styleWrapper = height + ? { + height, + display: "flex", + flexDirection: "column" as any, + } + : {}; + + const styleGuide = { + ...theme.font12, + color: theme.gray_text, + margin: "0 8px 12px", + }; + const styleQRSection = { + marginTop: "12px", + position: "relative" as any, + overflow: "hidden", + textAlign: "center" as any, + }; + const styleButtonSection = { + display: "flex", + justifyContent: "center", + gap: "10px", + margin: "12px 0px 0", + }; + + return ( +
+
+ 이벤트를 다른 사람들에게 공유할 수 있습니다. 이 링크를 통해 다른 + 사용자가 이벤트에 참여하면, 회원님과 새 참여자 모두{" "} + 송편코인 700개를 획득합니다. +
+ +
+
+ +
+
+
+ + } + background="#FFE812" + /> + + + + ) : ( + + ) + } + background={theme.gray_background} + /> + +
+
+ ); +}; + +export default BodyEvent2024FallShare; diff --git a/packages/web/src/components/ModalPopup/Body/BodyEvent2024AbuseWarning.tsx b/packages/web/src/components/ModalPopup/Body/BodyEvent2024SpringAbuseWarning.tsx similarity index 95% rename from packages/web/src/components/ModalPopup/Body/BodyEvent2024AbuseWarning.tsx rename to packages/web/src/components/ModalPopup/Body/BodyEvent2024SpringAbuseWarning.tsx index 38a898a3a..82c1f2c80 100644 --- a/packages/web/src/components/ModalPopup/Body/BodyEvent2024AbuseWarning.tsx +++ b/packages/web/src/components/ModalPopup/Body/BodyEvent2024SpringAbuseWarning.tsx @@ -7,13 +7,13 @@ import theme from "@/tools/theme"; import CheckRoundedIcon from "@mui/icons-material/CheckRounded"; -type BodyChatReportSelectUserProps = { +type BodyEvent2024SpringAbuseWarningProps = { onChangeIsOpen?: (isOpen: boolean) => void; }; -const BodyChatReportSelectUser = ({ +const BodyEvent2024SpringAbuseWarning = ({ onChangeIsOpen, -}: BodyChatReportSelectUserProps) => { +}: BodyEvent2024SpringAbuseWarningProps) => { const styleText = { ...theme.font12, color: theme.gray_text, @@ -127,4 +127,4 @@ const BodyChatReportSelectUser = ({ ); }; -export default BodyChatReportSelectUser; +export default BodyEvent2024SpringAbuseWarning; diff --git a/packages/web/src/components/ModalPopup/ModalChatPayment.tsx b/packages/web/src/components/ModalPopup/ModalChatPayment.tsx index e59b85157..d19b55b12 100644 --- a/packages/web/src/components/ModalPopup/ModalChatPayment.tsx +++ b/packages/web/src/components/ModalPopup/ModalChatPayment.tsx @@ -1,7 +1,7 @@ import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import useAccountFromChats from "@/hooks/chat/useAccountFromChats"; -import { useEvent2024SpringQuestComplete } from "@/hooks/event/useEvent2024SpringQuestComplete"; +import { useEvent2024FallQuestComplete } from "@/hooks/event/useEvent2024FallQuestComplete"; import { useValueRecoilState } from "@/hooks/useFetchRecoilState"; import { useAxios } from "@/hooks/useTaxiAPI"; @@ -50,8 +50,8 @@ const ModalChatPayment = ({ [userOid, roomInfo] ); const onCopy = useCallback(() => setIsCopied(true), [setIsCopied]); - //#region event2024Spring - const event2024SpringQuestComplete = useEvent2024SpringQuestComplete(); + //#region event2024Fall + const event2024FallQuestComplete = useEvent2024FallQuestComplete(); //#endregion useEffect(() => { @@ -69,9 +69,8 @@ const ModalChatPayment = ({ method: "post", data: { roomId: roomInfo._id }, onSuccess: () => { - //#region event2024Spring - event2024SpringQuestComplete("payingAndSending"); - event2024SpringQuestComplete("paying"); + //#region event2024Fall + event2024FallQuestComplete("farePayment"); //#endregion modalProps.onChangeIsOpen?.(false); onRecall?.(); diff --git a/packages/web/src/components/ModalPopup/ModalChatSaveAccount.tsx b/packages/web/src/components/ModalPopup/ModalChatSaveAccount.tsx index 43d714e43..e5ce6b201 100644 --- a/packages/web/src/components/ModalPopup/ModalChatSaveAccount.tsx +++ b/packages/web/src/components/ModalPopup/ModalChatSaveAccount.tsx @@ -1,6 +1,6 @@ import { useCallback, useEffect, useState } from "react"; -import { useEvent2024SpringQuestComplete } from "@/hooks/event/useEvent2024SpringQuestComplete"; +import { useEvent2024FallQuestComplete } from "@/hooks/event/useEvent2024FallQuestComplete"; import { useFetchRecoilState, useValueRecoilState, @@ -32,8 +32,8 @@ const ModalChatSaveAcount = ({ const { account: accountOrigin } = useValueRecoilState("loginInfo") || {}; const [account, setAccount] = useState(accountDefault || ""); const fetchLoginInfo = useFetchRecoilState("loginInfo"); - //#region event2024Spring - const event2024SpringQuestComplete = useEvent2024SpringQuestComplete(); + //#region event2024Fall + const event2024FallQuestComplete = useEvent2024FallQuestComplete(); //#endregion useEffect(() => setAccount(accountDefault || ""), [accountDefault]); @@ -45,14 +45,14 @@ const ModalChatSaveAcount = ({ method: "post", data: { account }, onSuccess: () => { - //#region event2024Spring - event2024SpringQuestComplete("accountChanging"); + //#region event2024Fall + event2024FallQuestComplete("accountChanging"); //#endregion fetchLoginInfo(); }, onError: () => setAlert("계좌번호 저장을 실패하였습니다."), }); - }, [account, event2024SpringQuestComplete]); + }, [account]); const styleTitle = { ...theme.font18, diff --git a/packages/web/src/components/ModalPopup/ModalChatSettlement.tsx b/packages/web/src/components/ModalPopup/ModalChatSettlement.tsx index 75d15d7a0..d91799942 100644 --- a/packages/web/src/components/ModalPopup/ModalChatSettlement.tsx +++ b/packages/web/src/components/ModalPopup/ModalChatSettlement.tsx @@ -1,7 +1,7 @@ import { useMemo, useRef, useState } from "react"; import useSendMessage from "@/hooks/chat/useSendMessage"; -import { useEvent2024SpringQuestComplete } from "@/hooks/event/useEvent2024SpringQuestComplete"; +import { useEvent2024FallQuestComplete } from "@/hooks/event/useEvent2024FallQuestComplete"; import { useValueRecoilState } from "@/hooks/useFetchRecoilState"; import { useAxios } from "@/hooks/useTaxiAPI"; @@ -40,7 +40,7 @@ const ModalChatSettlement = ({ const isValidAccount = useMemo(() => regExpTest.account(account), [account]); const isRequesting = useRef(false); const sendMessage = useSendMessage(roomInfo._id, isRequesting); - const event2024SpringQuestComplete = useEvent2024SpringQuestComplete(); + const event2024FallQuestComplete = useEvent2024FallQuestComplete(); const onClickOk = () => { if (isRequesting.current || !isValidAccount) return; @@ -57,9 +57,8 @@ const ModalChatSettlement = ({ isRequesting.current = false; if (account !== defaultAccount) openSaveAccountModal?.(account); } - //#region event2024Spring - event2024SpringQuestComplete("payingAndSending"); - event2024SpringQuestComplete("sending"); + //#region event2024Fall + event2024FallQuestComplete("fareSettlement"); //#endregion modalProps.onChangeIsOpen?.(false); }, diff --git a/packages/web/src/components/ModalPopup/ModalEvent2024FallAbuseWarning.tsx b/packages/web/src/components/ModalPopup/ModalEvent2024FallAbuseWarning.tsx new file mode 100644 index 000000000..cac457b37 --- /dev/null +++ b/packages/web/src/components/ModalPopup/ModalEvent2024FallAbuseWarning.tsx @@ -0,0 +1,41 @@ +import Modal from "@/components/Modal"; + +import BodyEvent2024FallAbuseWarning from "./Body/BodyEvent2024FallAbuseWarning"; + +import theme from "@/tools/theme"; + +import ReportGmailerrorredRoundedIcon from "@mui/icons-material/ReportGmailerrorredRounded"; + +type ModalEvent2024FallAbuseWarningProps = Omit< + Parameters[0], + "padding" | "children" | "onEnter" +>; + +const ModalEvent2024FallAbuseWarning = ({ + ...modalProps +}: ModalEvent2024FallAbuseWarningProps) => { + const styleTitle = { + ...theme.font18, + display: "flex", + alignItems: "center", + margin: "0 8px 12px", + }; + const styleIcon = { + fontSize: "21px", + margin: "0 4px 0 0", + }; + + return ( + +
+ + 경고 +
+ +
+ ); +}; + +export default ModalEvent2024FallAbuseWarning; diff --git a/packages/web/src/components/ModalPopup/ModalEvent2024FallDailyAttendance.tsx b/packages/web/src/components/ModalPopup/ModalEvent2024FallDailyAttendance.tsx new file mode 100644 index 000000000..c745b7c1f --- /dev/null +++ b/packages/web/src/components/ModalPopup/ModalEvent2024FallDailyAttendance.tsx @@ -0,0 +1,94 @@ +import { useEffect, useState } from "react"; + +import { useEvent2024FallQuestComplete } from "@/hooks/event/useEvent2024FallQuestComplete"; +import { useValueRecoilState } from "@/hooks/useFetchRecoilState"; + +import Button from "@/components/Button"; +import CreditAmountStatusContainer from "@/components/Event/CreditAmountStatusContainer"; +import DailyAttendanceCalendar from "@/components/Event/DailyAttendanceCalendar"; +import Modal from "@/components/Modal"; +import WhiteContainer from "@/components/WhiteContainer"; + +import moment, { getToday } from "@/tools/moment"; +import theme from "@/tools/theme"; + +import { ReactComponent as DailyAttendance } from "@/static/events/2024fallDailyAttendance.svg"; + +type DateSectionProps = { + value: Array>; + handler: (newValue: Array) => void; +}; + +const DateSection = (props: DateSectionProps) => { + return ( + + + + ); +}; + +interface ModalEvent2024FallDailyAttendanceProps { + isOpen: boolean; + onChangeIsOpen?: ((isOpen: boolean) => void) | undefined; +} + +const ModalEvent2024FallDailyAttendance = ({ + isOpen, + onChangeIsOpen, +}: ModalEvent2024FallDailyAttendanceProps) => { + const today = getToday(); + + const [valueDate, setDate] = useState>>([ + today.year(), + today.month() + 1, + today.date(), + ]); + + const event2024FallQuestComplete = useEvent2024FallQuestComplete(); + + const { isAgreeOnTermsOfEvent = false, completedQuests = [] } = + useValueRecoilState("event2024FallInfo") || {}; + + const todayInitial = completedQuests?.filter( + ({ questId, completedAt }) => + questId === "dailyAttendance" && moment(completedAt).isSame(today, "day") + ); + + useEffect(() => { + const modalOpened = isAgreeOnTermsOfEvent && todayInitial.length === 0; + + if (onChangeIsOpen && modalOpened) { + onChangeIsOpen(modalOpened); // 모달 열기 상태 변경 + event2024FallQuestComplete("dailyAttendance"); + } + }, [isAgreeOnTermsOfEvent, todayInitial.length]); + + return ( + + + + + + + ); +}; + +export default ModalEvent2024FallDailyAttendance; diff --git a/packages/web/src/components/ModalPopup/ModalEvent2024FallItem.tsx b/packages/web/src/components/ModalPopup/ModalEvent2024FallItem.tsx index 11aafc192..38b89ccfa 100644 --- a/packages/web/src/components/ModalPopup/ModalEvent2024FallItem.tsx +++ b/packages/web/src/components/ModalPopup/ModalEvent2024FallItem.tsx @@ -1,13 +1,16 @@ -import { Dispatch, SetStateAction, useCallback, useMemo, useRef } from "react"; +import { + Dispatch, + SetStateAction, + useCallback, + useMemo, + useRef, + useState, +} from "react"; -import type { EventItem } from "@/types/event2024fall"; +import type { EventItem, RandomBoxResult } from "@/types/event2024fall"; import { useDelayBoolean } from "@/hooks/useDelay"; -import { - useFetchRecoilState, - useIsLogin, - useValueRecoilState, -} from "@/hooks/useFetchRecoilState"; +import { useIsLogin, useValueRecoilState } from "@/hooks/useFetchRecoilState"; import { useAxios } from "@/hooks/useTaxiAPI"; import Button from "@/components/Button"; @@ -28,38 +31,54 @@ import AccountBalanceWalletRoundedIcon from "@mui/icons-material/AccountBalanceW type ModalEvent2024FallItemProps = Parameters[0] & { itemInfo: EventItem; fetchItems?: () => void; - setRewardItem?: Dispatch>>; + setRandomboxResult?: Dispatch>>; setShareItem?: Dispatch>>; }; const ModalEvent2024FallItem = ({ itemInfo, fetchItems, - setRewardItem, + setRandomboxResult, setShareItem, ...modalProps }: ModalEvent2024FallItemProps) => { - const fetchEvent2024FallInfo = useFetchRecoilState("event2024FallInfo"); const event2024FallInfo = useValueRecoilState("event2024FallInfo"); const isLogin = useIsLogin(); const axios = useAxios(); const setAlert = useSetRecoilState(alertAtom); + const [bettingAmount, setBettingAmount] = useState(100); const isDisplayRandomBox = !useDelayBoolean(!modalProps.isOpen, 500); const isRequesting = useRef(false); + const changeBettingAmountHandler = ( + e: React.ChangeEvent + ) => { + const value = e.target.valueAsNumber; + if (value < 100 || isNaN(value)) { + setBettingAmount(100); + } else { + setBettingAmount(value); + } + }; + const onClickOk = useCallback(async () => { if (isRequesting.current) return; isRequesting.current = true; + if (bettingAmount % 100 !== 0) { + setAlert("베팅 수량은 100의 배수로 입력해주세요."); + isRequesting.current = false; + return; + } await axios({ url: `/events/2024fall/items/purchase/${itemInfo._id}`, method: "post", - onSuccess: ({ reward }) => { - fetchEvent2024FallInfo(); + data: { amount: bettingAmount / 100 }, + onSuccess: (result) => { fetchItems?.(); modalProps.onChangeIsOpen?.(false); - if (itemInfo.itemType === 3 && reward) { - setRewardItem?.(reward); + if (itemInfo.itemType === 3) { + setRandomboxResult?.({ ...result, amount: bettingAmount }); } else { setShareItem?.(itemInfo); } @@ -67,23 +86,20 @@ const ModalEvent2024FallItem = ({ onError: () => setAlert("구매를 실패하였습니다."), }); isRequesting.current = false; - }, [ - itemInfo._id, - fetchItems, - modalProps.onChangeIsOpen, - fetchEvent2024FallInfo, - ]); + }, [itemInfo._id, fetchItems, modalProps.onChangeIsOpen, bettingAmount]); const [isDisabled, buttonText] = useMemo( () => eventMode !== "2024fall" ? [true, "이벤트 기간이 아닙니다"] : !event2024FallInfo || !isLogin - ? [true, "로그인 후 구매가 가능합니다"] - : event2024FallInfo.creditAmount < itemInfo.price + ? [true, "로그인해야 합니다"] + : event2024FallInfo.isAgreeOnTermsOfEvent === false + ? [true, "이벤트에 참여해야 합니다"] + : event2024FallInfo.creditAmount < bettingAmount ? [true, "송편코인이 부족합니다"] : [false, "구매하기"], - [eventMode, event2024FallInfo, itemInfo] + [eventMode, event2024FallInfo, itemInfo, bettingAmount] ); const styleTitle = { @@ -105,7 +121,7 @@ const ModalEvent2024FallItem = ({
{itemInfo.itemType === 3 ? ( isDisplayRandomBox ? ( - + ) : (
@@ -133,16 +149,51 @@ const ModalEvent2024FallItem = ({ >
{itemInfo.name}
{itemInfo.description}
+ {itemInfo.itemType !== 3 && ( +
+ +
{itemInfo.price}
+
+ )} +
+ {itemInfo.itemType === 3 && (
- -
{itemInfo.price}
+ + 베팅 수량: +
-
+ )}
[0] & { inviterId?: string; + defaultPhoneNumber?: string; }; const ModalEvent2024FallJoin = ({ @@ -43,7 +45,9 @@ const ModalEvent2024FallJoin = ({ const event2024FallQuestComplete = useEvent2024FallQuestComplete(); //#endregion - const [phoneNumber, setPhoneNumber] = useState(""); + const [phoneNumber, setPhoneNumber] = useState( + phoneNumberFromLoginInfo || "" + ); const isValidPhoneNumber = useMemo( () => regExpTest.phoneNumber(phoneNumber), [phoneNumber] @@ -55,6 +59,10 @@ const ModalEvent2024FallJoin = ({ }>(); const isInvited = !!inviterId; + useEffect(() => { + setPhoneNumber(phoneNumberFromLoginInfo || ""); + }, [modalProps.isOpen]); + useEffect(() => { if (isAgreeOnTermsOfEvent || !isInvited) return; axios({ @@ -101,7 +109,7 @@ const ModalEvent2024FallJoin = ({ margin: "0 8px", }; const styleInputWrap = { - margin: "12px 8px", + margin: "0 8px 12px", display: "flex", alignItems: "center", color: theme.gray_text, @@ -109,12 +117,11 @@ const ModalEvent2024FallJoin = ({ ...theme.font14, } as const; - // ToDo : 글 작성 return (
- 2024 추석 이벤트 이름 지어줘 + Taxi 추석 이벤트
• 택시 동승을 하지 않는 사용자는{" "} @@ -137,18 +144,18 @@ const ModalEvent2024FallJoin = ({ 위 경우, SPARCS Taxi팀 서비스 관리자는 서비스 부정 이용을 방지하기 위해 택시 탑승을 인증할 수 있는{" "} 영수증 또는 카카오T 이용기록을 - 요청할 수 있습니다. 또한, 본 서비스를 부정 이용하는 사용자에게는 택시 + 요청할 수 있습니다. 또한, 본 서비스를 부정 이용하는 사용자에게는 Taxi 서비스 이용 제한 및 법적 조치를 취할 수 있습니다.
•{" "} - 입력해주신 연락처로 이벤트 상품을 전달해드립니다. + 입력하신 연락처로 이벤트 상품을 전달해 드립니다. {" "} 또한, 서비스 신고 대응 및 본인 확인을 위해 사용될 수 있습니다.{" "} - 입력해주신 연락처는 이후 수정이 불가능합니다. + 입력하신 연락처는 이후 수정이 불가능합니다.
@@ -158,19 +165,20 @@ const ModalEvent2024FallJoin = ({ 추천인 이벤트 참여를 위해서는 추천인이 발송한 링크로 이벤트에 참여해야 합니다. {" "} - 추천인을 통해 이벤트에 참여할 시, 참가자와 추천인 모두에게 700 - 송편코인이 지급됩니다. + 추천인을 통해 이벤트에 참여할 시, 참가자와 추천인 모두에게 송편코인 + 700개가 지급됩니다.
- • 본 약관은 동의 이후에도 {'"'}마이페이지{">"}한가위 송편 이벤트 참여 - 약관{'"'}에서 다시 확인하실 수 있습니다.{" "} + • 본 약관은 동의 이후에도 {'"'}마이페이지{">"}추석 이벤트 참여 약관{'"'} + 에서 다시 확인하실 수 있습니다.{" "}
- {isLogin && - (isAgreeOnTermsOfEvent ? ( +
+ +
+ {isLogin ? ( + isAgreeOnTermsOfEvent ? ( <> -
-
전화번호 ) : ( <> -
-
전화번호
+ {isInvited && inviterInfo && ( +
+ 추천인 +
+ +
+ + {inviterInfo?.nickname} + +
+ )}
- - {inviterInfo?.nickname} - -
+ 로그인 후 이벤트 참여하기 + + )} ); diff --git a/packages/web/src/components/ModalPopup/ModalEvent2024FallRandomBox.tsx b/packages/web/src/components/ModalPopup/ModalEvent2024FallRandomBox.tsx index 4af3da2b0..3546d7946 100644 --- a/packages/web/src/components/ModalPopup/ModalEvent2024FallRandomBox.tsx +++ b/packages/web/src/components/ModalPopup/ModalEvent2024FallRandomBox.tsx @@ -1,6 +1,6 @@ import { memo, useCallback, useEffect, useState } from "react"; -import type { EventItem } from "@/types/event2024fall"; +import type { RandomBoxResult } from "@/types/event2024fall"; import { useDelay, useDelayBoolean } from "@/hooks/useDelay"; @@ -14,6 +14,8 @@ import "./ModalEvent2024FallRandomBoxBackground.css"; import theme from "@/tools/theme"; +import JackpotFailImage from "@/static/events/2024fallJackpotFail.png"; +import JackpotSuccessImage from "@/static/events/2024fallJackpotSuccess.png"; import HelpCenterRoundedIcon from "@mui/icons-material/HelpCenterRounded"; const Background = () => ( @@ -24,23 +26,23 @@ const Background = () => ( ); type ModalEvent2024FallRandomBoxProps = { - item?: EventItem; + randomBoxResult?: RandomBoxResult; } & Parameters[0]; const ModalEvent2024FallRandomBox = ({ - item, + randomBoxResult, ...modalProps }: ModalEvent2024FallRandomBoxProps) => { const [isBoxOpend, setIsBoxOpend] = useState(false); const isDisplayRandomBox = !useDelayBoolean(!modalProps.isOpen, 500); - const isDisplayItemName = useDelay(isBoxOpend, !isBoxOpend, 6000); + const isDisplayResult = useDelay(isBoxOpend, !isBoxOpend, 6000); const onClickOk = useCallback(() => setIsBoxOpend(true), []); const onChangeIsOpen = useCallback( (isOpen: boolean) => { modalProps?.onChangeIsOpen?.(isOpen); }, - [item, modalProps] + [randomBoxResult, modalProps] ); useEffect(() => { @@ -82,7 +84,9 @@ const ModalEvent2024FallRandomBox = ({ {isDisplayRandomBox ? ( @@ -91,15 +95,29 @@ const ModalEvent2024FallRandomBox = ({
)} - {isDisplayItemName && ( + {isDisplayResult && (
- 축하합니다! 랜덤박스에서{" "} - - {'"'} - {item?.name || ""} - {'"'} - - 을(를) 획득하였습니다 + {randomBoxResult?.isJackpot ? ( + <> + 축하합니다!{" "} + + {'"'} + {randomBoxResult?.amount || ""} + {'"'} + + 개를 베팅하여 대박을 터뜨렸습니다 + + ) : ( + <> + 저런..!{" "} + + {'"'} + {randomBoxResult?.amount || ""} + {'"'} + + 개를 베팅하였지만 쪽박을 찼습니다 + + )}
)} ); diff --git a/packages/web/src/components/ModalPopup/ModalEvent2024FallShare.tsx b/packages/web/src/components/ModalPopup/ModalEvent2024FallShare.tsx new file mode 100644 index 000000000..4a4b40410 --- /dev/null +++ b/packages/web/src/components/ModalPopup/ModalEvent2024FallShare.tsx @@ -0,0 +1,49 @@ +import Modal from "@/components/Modal"; + +import BodyEvent2024FallShare, { + BodyEvent2024FallShareProps, +} from "./Body/BodyEvent2024FallShare"; + +import theme from "@/tools/theme"; + +import ShareRoundedIcon from "@mui/icons-material/ShareRounded"; + +type ModalEvent2024FallShareProps = { + isOpen: boolean; + onChangeIsOpen?: (isOpen: boolean) => void; + inviteUrl: BodyEvent2024FallShareProps["inviteUrl"]; +}; + +const ModalEvent2024FallShare = ({ + isOpen, + onChangeIsOpen, + inviteUrl, +}: ModalEvent2024FallShareProps) => { + const styleTitle = { + ...theme.font18, + display: "flex", + alignItems: "center", + margin: "0 8px 12px", + }; + const styleIcon = { + fontSize: "21px", + margin: "0 4px 0 0", + }; + + return ( + +
+ + 이벤트 공유하기 +
+ +
+ ); +}; + +export default ModalEvent2024FallShare; +export { BodyEvent2024FallShare }; diff --git a/packages/web/src/components/ModalPopup/ModalEvent2024SpringAbuseWarning.tsx b/packages/web/src/components/ModalPopup/ModalEvent2024SpringAbuseWarning.tsx index 56f1112eb..e4ab666f8 100644 --- a/packages/web/src/components/ModalPopup/ModalEvent2024SpringAbuseWarning.tsx +++ b/packages/web/src/components/ModalPopup/ModalEvent2024SpringAbuseWarning.tsx @@ -1,19 +1,19 @@ import Modal from "@/components/Modal"; -import BodyEvent2024AbuseWarning from "./Body/BodyEvent2024AbuseWarning"; +import BodyEvent2024AbuseWarning from "./Body/BodyEvent2024SpringAbuseWarning"; import theme from "@/tools/theme"; import ReportGmailerrorredRoundedIcon from "@mui/icons-material/ReportGmailerrorredRounded"; -type ModalEvent2024AbuseWarningProps = Omit< +type ModalEvent2024SpringAbuseWarningProps = Omit< Parameters[0], "padding" | "children" | "onEnter" >; -const ModalChatReport = ({ +const ModalEvent2024SpringAbuseWarning = ({ ...modalProps -}: ModalEvent2024AbuseWarningProps) => { +}: ModalEvent2024SpringAbuseWarningProps) => { const styleTitle = { ...theme.font18, display: "flex", @@ -36,4 +36,4 @@ const ModalChatReport = ({ ); }; -export default ModalChatReport; +export default ModalEvent2024SpringAbuseWarning; diff --git a/packages/web/src/components/ModalPopup/ModalMypageModify.tsx b/packages/web/src/components/ModalPopup/ModalMypageModify.tsx index dcd267021..b85ed44d5 100644 --- a/packages/web/src/components/ModalPopup/ModalMypageModify.tsx +++ b/packages/web/src/components/ModalPopup/ModalMypageModify.tsx @@ -2,7 +2,7 @@ import axiosOri from "axios"; import { useCallback, useEffect, useRef, useState } from "react"; import { useTranslation } from "react-i18next"; -import { useEvent2024SpringQuestComplete } from "@/hooks/event/useEvent2024SpringQuestComplete"; +import { useEvent2024FallQuestComplete } from "@/hooks/event/useEvent2024FallQuestComplete"; import { useFetchRecoilState, useValueRecoilState, @@ -144,8 +144,8 @@ const ModalMypageModify = ({ ...modalProps }: ModalMypageModifyProps) => { const loginInfo = useValueRecoilState("loginInfo"); const fetchLoginInfo = useFetchRecoilState("loginInfo"); - //#region event2024Spring - const event2024SpringQuestComplete = useEvent2024SpringQuestComplete(); + //#region event2024Fall + const event2024FallQuestComplete = useEvent2024FallQuestComplete(); //#endregion const setAlert = useSetRecoilState(alertAtom); @@ -169,8 +169,8 @@ const ModalMypageModify = ({ ...modalProps }: ModalMypageModifyProps) => { method: "post", data: { nickname }, onError: () => setAlert(t("page_modify.nickname_failed")), - //#region event2024Spring - onSuccess: () => event2024SpringQuestComplete("nicknameChanging"), // event2024Spring + //#region event2024Fall + onSuccess: () => event2024FallQuestComplete("nicknameChanging"), // event2024Fall //#endregion }); } @@ -181,8 +181,8 @@ const ModalMypageModify = ({ ...modalProps }: ModalMypageModifyProps) => { method: "post", data: { account }, onError: () => setAlert(t("page_modify.account_failed")), - //#region event2024Spring - onSuccess: () => event2024SpringQuestComplete("accountChanging"), // event2024Spring + //#region event2024Fall + onSuccess: () => event2024FallQuestComplete("accountChanging"), // event2024Fall //#endregion }); } diff --git a/packages/web/src/components/ModalPopup/ModalNotification.tsx b/packages/web/src/components/ModalPopup/ModalNotification.tsx index f0f7f871a..fbd19675b 100644 --- a/packages/web/src/components/ModalPopup/ModalNotification.tsx +++ b/packages/web/src/components/ModalPopup/ModalNotification.tsx @@ -1,7 +1,7 @@ import { useCallback, useRef } from "react"; import { useTranslation } from "react-i18next"; -import { useEvent2024SpringQuestComplete } from "@/hooks/event/useEvent2024SpringQuestComplete"; +import { useEvent2024FallQuestComplete } from "@/hooks/event/useEvent2024FallQuestComplete"; import { useFetchRecoilState, useValueRecoilState, @@ -65,8 +65,8 @@ const ModalNotification = ({ const notificationOptions = useValueRecoilState("notificationOptions"); const fetchNotificationOptions = useFetchRecoilState("notificationOptions"); const isAxiosCalled = useRef(false); - //#region event2024Spring - const event2024SpringQuestComplete = useEvent2024SpringQuestComplete(); + //#region event2024Fall + const event2024FallQuestComplete = useEvent2024FallQuestComplete(); //#endregion const styleTitle = { @@ -113,12 +113,12 @@ const ModalNotification = ({ fetchNotificationOptions(); isAxiosCalled.current = false; - //#region event2024Spring + //#region event2024Fall if (optionName === "advertisement" && value) - event2024SpringQuestComplete("adPushAgreement"); + event2024FallQuestComplete("adPushAgreement"); //#endregion }, - [deviceToken, event2024SpringQuestComplete] + [deviceToken] ); const onChangeNotificationAll = useCallback( async (value: boolean) => { @@ -149,11 +149,11 @@ const ModalNotification = ({ fetchNotificationOptions(); isAxiosCalled.current = false; - //#region event2024Spring - if (value) event2024SpringQuestComplete("adPushAgreement"); + //#region event2024Fall + if (value) event2024FallQuestComplete("adPushAgreement"); //#endregion }, - [deviceToken, event2024SpringQuestComplete] + [deviceToken] ); const onChangeNotificationChatting = useCallback( onChangeNotificationOption("chatting"), diff --git a/packages/web/src/components/ModalPopup/ModalPurchaseSuccess.tsx b/packages/web/src/components/ModalPopup/ModalPurchaseSuccess.tsx new file mode 100644 index 000000000..5c756473d --- /dev/null +++ b/packages/web/src/components/ModalPopup/ModalPurchaseSuccess.tsx @@ -0,0 +1,79 @@ +import Button from "@/components/Button"; +import DottedLine from "@/components/DottedLine"; +import Modal from "@/components/Modal"; + +import theme from "@/tools/theme"; + +import WbIncandescentIcon from "@mui/icons-material/WbIncandescent"; + +type ModalPurchaseSuccessProps = { + isOpen: boolean; + onChangeIsOpen?: (isOpen: boolean) => void; +}; + +const ModalPurchaseSuccess = ({ + isOpen, + onChangeIsOpen, +}: ModalPurchaseSuccessProps) => { + const styleTitle = { + ...theme.font18, + display: "flex", + alignItems: "center", + marginBottom: "12px", + }; + const styleLogo = { + fontSize: "21px", + margin: "0 4px 0 0px", + }; + const styleBody = { display: "grid", rowGap: "12px" }; + + return ( + +
+ + 응모권 구매에 성공했습니다. +
+ <> +
+ +
+ 정상적으로 구입되었습니다. +
+ +
+ +
+ ); +}; + +export default ModalPurchaseSuccess; diff --git a/packages/web/src/components/ModalPopup/ModalRoomShare.tsx b/packages/web/src/components/ModalPopup/ModalRoomShare.tsx index daee028b2..087d04bee 100644 --- a/packages/web/src/components/ModalPopup/ModalRoomShare.tsx +++ b/packages/web/src/components/ModalPopup/ModalRoomShare.tsx @@ -1,4 +1,4 @@ -import { useEvent2024SpringQuestComplete } from "@/hooks/event/useEvent2024SpringQuestComplete"; +import { useEvent2024FallQuestComplete } from "@/hooks/event/useEvent2024FallQuestComplete"; import Modal from "@/components/Modal"; @@ -19,8 +19,8 @@ const ModalRoomShare = ({ onChangeIsOpen, roomInfo, }: ModalRoomShareProps) => { - //#region event2024Spring - const event2024SpringQuestComplete = useEvent2024SpringQuestComplete(); + //#region event2024Fall + const event2024FallQuestComplete = useEvent2024FallQuestComplete(); //#endregion const styleTitle = { ...theme.font18, @@ -32,17 +32,17 @@ const ModalRoomShare = ({ fontSize: "21px", margin: "0 4px 0 0", }; - //#region event2024Spring - const onChangeIsOpenWithEvent2023Fall = (isOpen: boolean) => { + //#region event2024Fall + const onChangeIsOpenWithEvent2024Fall = (isOpen: boolean) => { onChangeIsOpen?.(isOpen); - !isOpen && event2024SpringQuestComplete("roomSharing"); + !isOpen && event2024FallQuestComplete("roomSharing"); }; //#endregion return (
diff --git a/packages/web/src/components/ModalPopup/index.tsx b/packages/web/src/components/ModalPopup/index.tsx index dbcff316e..5026c8e50 100644 --- a/packages/web/src/components/ModalPopup/index.tsx +++ b/packages/web/src/components/ModalPopup/index.tsx @@ -12,9 +12,12 @@ 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 ModalEvent2024FallAbuseWarning } from "./ModalEvent2024FallAbuseWarning"; +export { default as ModalEvent2024FallDailyAttendance } from "./ModalEvent2024FallDailyAttendance"; export { default as ModalEvent2024FallItem } from "./ModalEvent2024FallItem"; export { default as ModalEvent2024FallJoin } from "./ModalEvent2024FallJoin"; export { default as ModalEvent2024FallRandomBox } from "./ModalEvent2024FallRandomBox"; +export { default as ModalEvent2024FallShare } from "./ModalEvent2024FallShare"; 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 e77895bc6..11c200609 100644 --- a/packages/web/src/components/Skeleton/Routes.tsx +++ b/packages/web/src/components/Skeleton/Routes.tsx @@ -28,7 +28,7 @@ const routeProps = [ exact: true, }, { - path: "/event/:eventName", + path: ["/event/:eventName", "/event/:eventName/item/:itemId"], component: lazy(() => import("@/pages/Event")), exact: true, }, diff --git a/packages/web/src/components/Skeleton/index.tsx b/packages/web/src/components/Skeleton/index.tsx index 6ebff3f70..c08c1ca80 100644 --- a/packages/web/src/components/Skeleton/index.tsx +++ b/packages/web/src/components/Skeleton/index.tsx @@ -1,4 +1,4 @@ -import { ReactNode, useMemo } from "react"; +import { ReactNode, useMemo, useState } from "react"; import { useLocation } from "react-router-dom"; import { useEvent2024FallEffect } from "@/hooks/event/useEvent2024FallEffect"; @@ -17,7 +17,10 @@ import { import HeaderBar from "@/components/Header/HeaderBar"; import Loading from "@/components/Loading"; -import { ModalTerms } from "@/components/ModalPopup"; +import { + ModalEvent2024FallDailyAttendance, + ModalTerms, +} from "@/components/ModalPopup"; import Error from "@/pages/Error"; import Navigation from "./Navigation"; @@ -64,6 +67,9 @@ const Skeleton = ({ children }: SkeletonProps) => { [pathname] ); + const [dailyAttendanceOpened, setDailyAttendanceOpened] = + useState(false); + //#region event2024Fall useEvent2024FallEffect(); //#endregion @@ -92,6 +98,11 @@ const Skeleton = ({ children }: SkeletonProps) => { )} {children} + + {isDisplayNavigation && (
{ )}
- {/* #region event2024Spring */} - { if (data === true) { diff --git a/packages/web/src/pages/Event/Event2024Fall.tsx b/packages/web/src/pages/Event/Event2024Fall.tsx index 77c74f28b..61ea687f4 100644 --- a/packages/web/src/pages/Event/Event2024Fall.tsx +++ b/packages/web/src/pages/Event/Event2024Fall.tsx @@ -1,11 +1,19 @@ -import { memo } from "react"; +import { memo, useEffect, useState } from "react"; +import { Link } from "react-router-dom"; + +import { useValueRecoilState } from "@/hooks/useFetchRecoilState"; +import { useAxios } from "@/hooks/useTaxiAPI"; import AdaptiveDiv from "@/components/AdaptiveDiv"; import Button from "@/components/Button"; import Footer from "@/components/Footer"; import HeaderWithBackButton from "@/components/Header/HeaderWithBackButton"; +import { ModalEvent2024FallShare } from "@/components/ModalPopup"; import WhiteContainer from "@/components/WhiteContainer"; +import alertAtom from "@/atoms/alert"; +import { useSetRecoilState } from "recoil"; + import theme from "@/tools/theme"; import { ReactComponent as TaxiLogoIcon } from "@/static/assets/sparcsLogos/TaxiLogo.svg"; @@ -19,242 +27,317 @@ import { ReactComponent as MainStep3 } from "@/static/events/2024fallMainStep3.s import { ReactComponent as MainTitle } from "@/static/events/2024fallMainTitle.svg"; const EVENT_INSTAGRAM_URL = - "https://www.instagram.com/p/C_H7YTfPEGZ/?igsh=MXh3MWc0NnJsZml3MQ=="; + "https://www.instagram.com/p/C_j1gibhTOa/?igsh=eWoyMnhweGNzeWR2"; -const Event2024Fall = () => ( - <> - -
이벤트 안내
-
- - - -
{ + const [isOpenShare, setIsOpenShare] = useState(false); + const [inviteUrl, setInviteUrl] = useState(""); + const setAlert = useSetRecoilState(alertAtom); + const { isAgreeOnTermsOfEvent } = + useValueRecoilState("event2024FallInfo") || {}; + const axios = useAxios(); + + useEffect(() => { + if (isAgreeOnTermsOfEvent) + axios({ + url: `/events/2024fall/invites/create`, + method: "post", + onSuccess: ({ inviteUrl }) => { + setInviteUrl(inviteUrl); + }, + onError: () => setAlert("초대 링크를 생성하지 못했습니다."), + }); + }, [isAgreeOnTermsOfEvent]); + + return ( + <> + +
이벤트 안내
+
+ - 2024.9.7.(토) ~ 9.23.(월) -
- -
-
- - - -
-
- - + +
-
+ + +
+ + + +
+
+ + - STEP 1 -
+
+ STEP 1 +
+
+
+ Taxi 퀘스트 달성하고 +
+ 송편코인을 모아보세요! +
+
+ +
+
+ Taxi 웹 사이트와 앱에서 퀘스트 내용 확인 +
+ 이벤트 참여 동의만 해도 송편코인 200개 지급 +
+
+ + + +
-
- Taxi 퀘스트 달성하고 -
- 송편코인을 모아보세요! -
-
- +
+ STEP 2 +
+
+
+ 응모권 교환소에서 +
+ 경품 응모권을 구매해 보세요! +
+
+ +
+
+ 응모권은 경품마다 별개로 발급됨 +
+ 경품 추첨 결과는 9월 30일에 발표 +
+
+ + + +
-
- Taxi 웹 사이트와 앱에서 퀘스트 내용 확인 -
- 이벤트 참여 동의만 해도 송편코인 200개 지급 -
+
+ STEP 3 +
+
+
+ 경품 당첨 확률 +
+ 리더보드를 확인하세요! +
+
+ +
+
+ 응모권 개수가 많을수록 당첨 확률이 상승함 +
위 이미지는 실제와 다를 수 있음 +
+
+ + + +
- {/* */} - - {/* */} - -
- + BONUS +
+
+
+ 이벤트를 친구에게 공유하고 +
+ 친구와 함께 송편코인 받아가세요! +
+
+
+ 나의 초대 링크로 친구가 이벤트에 참여하면 +
+ 친구와 나 모두 송편코인 700개 획득! +
+
+ + + + +
+
window.open(EVENT_INSTAGRAM_URL, "_blank")} + > + -
- STEP 2 -
+
EVENT
-
- 응모권 교환소에서 +
+ 인스타그램 스토리 공유하고
- 경품 응모권을 구매해 보세요! + 공유 이벤트에 참여하세요!
- +
-
- 응모권은 경품마다 별개로 발급됨 +
+ 이 영역을 누르면 인스타그램 게시물로 이동
- 경품 추첨 결과는 9월 30일에 발표 + 추첨 결과는 인스타그램, Ara, Taxi 홈페이지에 발표
-
- {/* */} - - {/* */} - -
- -
- STEP 3 -
-
-
- 경품 당첨 확률 -
- 리더보드를 확인하세요! -
-
- -
-
- 응모권 개수가 많을수록 당첨 확률이 상승함 -
위 이미지는 실제와 다를 수 있음 + +
+
+ + +
+
+ 본 이벤트는 현대모비스와 에이핀아이앤씨의 후원으로 진행되었습니다.
-
- {/* */} - - {/* */} - - -
-
window.open(EVENT_INSTAGRAM_URL, "_blank")} - > - -
EVENT
-
-
- 인스타그램 스토리 공유하고 -
- 공유 이벤트에 참여하세요! -
-
- -
-
- {/* 추첨 결과는 인스타그램, Ara, Taxi 홈페이지에 발표 -
- 실물 상품 또는 기프티콘으로 지급 -
*/} - 인스타그램 게시물은 9월 6일 정오에 업로드 예정 -
- -
-
- - -
-
- 본 이벤트는 현대모비스와 에이핀아이앤씨의 후원으로 진행되었습니다. -
- -
-
- -); + +
+
+ + ); +}; export default memo(Event2024Fall); diff --git a/packages/web/src/pages/Event/Event2024FallDailyAttendance.tsx b/packages/web/src/pages/Event/Event2024FallDailyAttendance.tsx new file mode 100644 index 000000000..1b15495ab --- /dev/null +++ b/packages/web/src/pages/Event/Event2024FallDailyAttendance.tsx @@ -0,0 +1,77 @@ +import { memo, useState } from "react"; + +import AdaptiveDiv from "@/components/AdaptiveDiv"; +import Button from "@/components/Button"; +import CreditAmountStatusContainer from "@/components/Event/CreditAmountStatusContainer"; +import DailyAttendanceCalendar from "@/components/Event/DailyAttendanceCalendar"; +import Footer from "@/components/Footer"; +import HeaderWithLeftNav from "@/components/Header/HeaderWithLeftNav"; +import WhiteContainer from "@/components/WhiteContainer"; + +import { getToday } from "@/tools/moment"; +import theme from "@/tools/theme"; + +import { ReactComponent as DailyAttendance } from "@/static/events/2024fallDailyAttendance.svg"; + +type DateSectionProps = { + value: Array>; + handler: (newValue: Array) => void; +}; + +const DateSection = (props: DateSectionProps) => { + return ( + + + + ); +}; + +const Event2024FallMissions = () => { + const today = getToday(); + + const [valueDate, setDate] = useState>>([ + today.year(), + today.month() + 1, + today.date(), + ]); + + return ( + <> + + + + + + + +
+ + + ); +}; + +export default memo(Event2024FallMissions); diff --git a/packages/web/src/pages/Event/Event2024FallHistory.tsx b/packages/web/src/pages/Event/Event2024FallHistory.tsx index d1f80f36a..90a9d8a52 100644 --- a/packages/web/src/pages/Event/Event2024FallHistory.tsx +++ b/packages/web/src/pages/Event/Event2024FallHistory.tsx @@ -7,7 +7,6 @@ import useQuery from "@/hooks/useTaxiAPI"; import AdaptiveDiv from "@/components/AdaptiveDiv"; import Empty from "@/components/Empty"; -import CreditAmountStatusContainer from "@/components/Event/CreditAmountStatusContainer"; import Footer from "@/components/Footer"; import HeaderWithLeftNav from "@/components/Header/HeaderWithLeftNav"; import Title from "@/components/Title"; @@ -85,10 +84,6 @@ const HistorySection = () => { return ( <> - - 획득한 응모권 - - 구매 이력 @@ -133,17 +128,16 @@ const Event2024FallHistory = () => { @@ -164,7 +158,7 @@ const Event2024FallHistory = () => { marginBottom: "5px", }} > - 📌 상품 지급일 : 10월 13일(금) + 📌 결과 발표일 : 9월 30일(화)
{ marginBottom: "15px", }} > - 구매하신 모든 아이템은 이벤트 종료 후 교환권 또는 기프티콘 형태로 - 일괄 지급됩니다. +
+ 1. 당첨되신 모든 경품은 이벤트 종료 후 실물 또는 기프티콘 형태로 + 일괄 지급됩니다. +
+
+ 2. 하나의 경품은 1번만 당첨될 수 있지만, 여러 경품에 동시에 + 당첨될 수는 있습니다. +
{ marginBottom: "5px", }} > - 🎁 지급 방법 : 각 상품 별 지급 방법은 아래와 같습니다. + 🎁 지급 방법 : 각 경품별 지급 방법은 아래와 같습니다.
- 1. 북측 매점 교환권 / 교내 엔제리너스 아이스 아메리카노 S: (엔터 - 넣어주세요) 교양분관 SPARCS 동방에서 교환권을 수령하실 수 - 있습니다. + 1. 에어팟 3 / 로지텍 K380 : 교양분관 SPARCS 동아리방에서 실물을 + 수령하실 수 있습니다.
-
- 2. BBQ 황금올리브+콜라 1.25L / 오색송편: 이벤트 참여 때 등록한 - 연락처로 기프티콘을 발송해 드립니다. +
+ 2. 그 외 경품 : 이벤트 참여 동의 때 입력한 연락처로 기프티콘을 + 발송해 드립니다.
diff --git a/packages/web/src/pages/Event/Event2024FallLeaderboard.tsx b/packages/web/src/pages/Event/Event2024FallLeaderboard.tsx deleted file mode 100644 index eada05d3a..000000000 --- a/packages/web/src/pages/Event/Event2024FallLeaderboard.tsx +++ /dev/null @@ -1,378 +0,0 @@ -import { useMemo } from "react"; - -import { useValueRecoilState } from "@/hooks/useFetchRecoilState"; -import useQuery from "@/hooks/useTaxiAPI"; - -import AdaptiveDiv from "@/components/AdaptiveDiv"; -import Empty from "@/components/Empty"; -import Footer from "@/components/Footer"; -import HeaderWithLeftNav from "@/components/Header/HeaderWithLeftNav"; -import Title from "@/components/Title"; -import ProfileImage from "@/components/User/ProfileImage"; -import WhiteContainer from "@/components/WhiteContainer"; - -import theme from "@/tools/theme"; - -// ToDo : 2023fall 이미지 -import { ReactComponent as LeaderBoardItems } from "@/static/events/2023fallLeaderBoardItems.svg"; -import { ReactComponent as Ticket1Icon } from "@/static/events/2023fallTicket1.svg"; -import { ReactComponent as Ticket2Icon } from "@/static/events/2023fallTicket2.svg"; - -const LeaderboardTopBar = () => ( -
- 순위 - 닉네임 - - - 추첨 확률 -
-); - -type LeaderboardElem = { - nickname: string; - profileImageUrl: string; - ticket1Amount: number; - ticket2Amount: number; - probability: number; - probabilityV2: number; -}; - -type LeaderboardItemProps = { - value: LeaderboardElem; - rank: number; - isMe?: boolean; -}; - -const LeaderboardItem = ({ - value, - rank, - isMe = false, -}: LeaderboardItemProps) => { - const styleContainer = (index: number) => { - switch (index) { - case 0: - return { - color: "#C6B200", - border: "0.5px solid #E4CD00", - background: "#FFEE5A", - boxShadow: "0px 1px 5px 0px #E4CD00", - ...theme.font20, - fontSize: "24px", - }; - case 1: - return { - color: "#96BCC6", - border: "0.5px solid #BBD4DA", - background: "#EEF6F8", - boxShadow: "0px 1px 5px 0px #BBD4DA", - ...theme.font20, - fontSize: "24px", - }; - case 2: - return { - color: "#CD6830", - border: "0.5px solid #DE8C5D", - background: "#FFC5A4", - boxShadow: "0px 1px 5px 0px #DE8C5D", - ...theme.font20, - fontSize: "24px", - }; - case -1: - return { - color: theme.purple_disabled, - background: theme.purple, - boxShadow: theme.shadow, - ...theme.font20, - }; - default: - return { - color: theme.purple_disabled, - background: theme.white, - boxShadow: theme.shadow, - ...theme.font20, - }; - } - }; - - const styleText = (index: number) => { - switch (index) { - case 0: - return "#6B6000"; - case 1: - return "#337182"; - case 2: - return "#9E3800"; - case -1: - return theme.white; - default: - return theme.purple; - } - }; - - const styleTicketText = { - ...theme.font16, - width: "30px", - flexShrink: 0, - textAlign: "center" as const, - }; - - return ( - - {rank + 1} -
- -
- {isMe && ( -
- 나 -
- )} -
- {value.nickname} -
- - {value.ticket1Amount || 0} - - {value.ticket2Amount || 0} -
- - {Math.trunc(value.probabilityV2 * 100) || 0} - - .{Math.floor(((value.probabilityV2 * 100) % 1) * 10)}% -
-
- ); -}; - -const Event2024FallLeaderboard = () => { - const { - leaderboard, - rank, - probability, - probabilityV2, - totalUserAmount, - totalTicket1Amount, - totalTicket2Amount, - } = useQuery.get("/events/2024fall/publicNotice/leaderboard")[1] || { - leaderboard: [], - rank: 0, - }; - const { ticket1Amount, ticket2Amount } = { - ticket1Amount: 0, - ticket2Amount: 0, - }; // TODO: FIXME - const { nickname, profileImgUrl } = useValueRecoilState("loginInfo") || {}; - const myLeaderboardInfo = useMemo>(() => { - if (!nickname || !profileImgUrl || !probability) return null; - return { - nickname, - profileImageUrl: profileImgUrl, - ticket1Amount: ticket1Amount || 0, - ticket2Amount: ticket2Amount || 0, - probability, - probabilityV2, - }; - }, [nickname, profileImgUrl, ticket1Amount, ticket2Amount, probability]); - - return ( - <> - - - - 안내 - - -
-
- 🌟 참여 방법 -
-
- 퀘스트 달성, 달토끼 상점을 통해 응모권을 얻을 수 있습니다. -
- 고급응모권은 일반응모권 당첨 확률의 5배입니다. -
- 여러 개의 응모권으로 중복 참여가 가능합니다. -
-
- 📌 경품 추첨 결과 발표일 : 10월 13일(금) -
-
- 추첨 결과는 인스타그램, Ara, Taxi 홈페이지를 통해 발표됩니다. -
-
- 🎁 경품 : 에어팟 3세대 (1명), 택시비 카카오페이 상품권 - 5000원 (14명) -
-
- -
-
- 🏆 리더보드 : 이벤트 기간 중, 실시간으로 변동되는 내 자신과 - 상위 참여자들의 추첨 확률이 공개됩니다. -
-
-
- - 리더보드 - - {leaderboard.length > 0 ? ( - <> - - {leaderboard.map((elem: LeaderboardElem, index: number) => ( - - ))} - {rank > 20 && myLeaderboardInfo && ( - - )} -
-
- • 리더보드의 추첨 확률은 정확한 확률이 아닌 내부 모델을 사용하여 - 계산한 근삿값입니다. -
-
- • 경품 추첨 전체 참여자 수 : {totalUserAmount || 0}명 -
-
- • 발급된 전체 일반 응모권 개수 : {totalTicket1Amount || 0}개 -
-
- • 발급된 전체 고급 응모권 개수 : {totalTicket2Amount || 0}개 -
-
- - ) : ( - 리더보드가 비어있습니다. - )} -
-
- - ); -}; // ToDo : 2023fall 문구 및 footer - -export default Event2024FallLeaderboard; diff --git a/packages/web/src/pages/Event/Event2024FallMissions.tsx b/packages/web/src/pages/Event/Event2024FallMissions.tsx index 879168fc7..227e4af7b 100644 --- a/packages/web/src/pages/Event/Event2024FallMissions.tsx +++ b/packages/web/src/pages/Event/Event2024FallMissions.tsx @@ -7,13 +7,13 @@ import { useValueRecoilState } from "@/hooks/useFetchRecoilState"; import AdaptiveDiv from "@/components/AdaptiveDiv"; import CreditAmountStatusContainer from "@/components/Event/CreditAmountStatusContainer"; import WhiteContainerSuggestJoinEvent from "@/components/Event/WhiteContainerSuggestJoinEvent"; +import WhiteContainerSuggestShareEvent from "@/components/Event/WhiteContainerSuggestShareEvent"; import Footer from "@/components/Footer"; -import HeaderWithBackButton from "@/components/Header/HeaderWithBackButton"; +import HeaderWithLeftNav from "@/components/Header/HeaderWithLeftNav"; import WhiteContainer from "@/components/WhiteContainer"; import theme from "@/tools/theme"; -// ToDo : 2023fall 이미지 import { ReactComponent as CreditIcon } from "@/static/events/2023fallCredit.svg"; import { ReactComponent as MissionCompleteIcon } from "@/static/events/2023fallMissionComplete.svg"; @@ -150,13 +150,22 @@ const Event2024FallMissions = () => { return ( <> - -
퀘스트
-
+
+ {quests?.map((quest) => ( ))} @@ -164,6 +173,6 @@ const Event2024FallMissions = () => { ); -}; // ToDo : 2023fall 문구 및 footer +}; export default memo(Event2024FallMissions); diff --git a/packages/web/src/pages/Event/Event2024FallStore/Item.tsx b/packages/web/src/pages/Event/Event2024FallStore/Item.tsx new file mode 100644 index 000000000..6cbbee128 --- /dev/null +++ b/packages/web/src/pages/Event/Event2024FallStore/Item.tsx @@ -0,0 +1,489 @@ +import { useCallback, useMemo, useRef, useState } from "react"; + +import { + useFetchRecoilState, + useIsLogin, + useValueRecoilState, +} from "@/hooks/useFetchRecoilState"; +import useQuery from "@/hooks/useTaxiAPI"; +import { useAxios } from "@/hooks/useTaxiAPI"; + +import AdaptiveDiv from "@/components/AdaptiveDiv"; +import Button from "@/components/Button"; +import Empty from "@/components/Empty"; +import CreditAmountStatusContainer from "@/components/Event/CreditAmountStatusContainer"; +import EventItemContainer from "@/components/Event/EventItemContainer"; +import Footer from "@/components/Footer"; +import HeaderWithLeftNav from "@/components/Header/HeaderWithLeftNav"; +import ModalPurchaseSuccess from "@/components/ModalPopup/ModalPurchaseSuccess"; +import ProfileImage from "@/components/User/ProfileImage"; +import WhiteContainer from "@/components/WhiteContainer"; + +import alertAtom from "@/atoms/alert"; +import { useSetRecoilState } from "recoil"; + +import { eventMode } from "@/tools/loadenv"; +import theme from "@/tools/theme"; + +// ToDo : 2023fall 이미지 +import { ReactComponent as TicketIcon } from "@/static/events/2024fallTicket.svg"; +import ShoppingCartIcon from "@mui/icons-material/ShoppingCart"; + +const LeaderboardTopBar = () => ( +
+ 순위 + 닉네임 + + 당첨 확률 +
+); + +type LeaderboardElem = { + nickname: string; + profileImageUrl: string; + amount: number; + probability: number; + rank: number; +}; + +type LeaderboardItemProps = { + value: LeaderboardElem; + rank: number; + isMe?: boolean; +}; + +const LeaderboardItem = ({ + value, + rank, + isMe = false, +}: LeaderboardItemProps) => { + const styleContainer = (index: number) => { + switch (index) { + case 0: + return { + color: "#C6B200", + border: "0.5px solid #E4CD00", + background: "#FFEE5A", + boxShadow: "0px 1px 5px 0px #E4CD00", + ...theme.font20, + fontSize: "24px", + }; + case 1: + return { + color: "#96BCC6", + border: "0.5px solid #BBD4DA", + background: "#EEF6F8", + boxShadow: "0px 1px 5px 0px #BBD4DA", + ...theme.font20, + fontSize: "24px", + }; + case 2: + return { + color: "#CD6830", + border: "0.5px solid #DE8C5D", + background: "#FFC5A4", + boxShadow: "0px 1px 5px 0px #DE8C5D", + ...theme.font20, + fontSize: "24px", + }; + case -1: + return { + color: theme.purple_disabled, + background: theme.purple, + boxShadow: theme.shadow, + ...theme.font20, + }; + default: + return { + color: theme.purple_disabled, + background: theme.white, + boxShadow: theme.shadow, + ...theme.font20, + }; + } + }; + + const styleText = (index: number) => { + switch (index) { + case 0: + return "#6B6000"; + case 1: + return "#337182"; + case 2: + return "#9E3800"; + case -1: + return theme.white; + default: + return theme.purple; + } + }; + + const styleTicketText = { + ...theme.font16, + width: "30px", + flexShrink: 0, + textAlign: "center" as const, + }; + + return ( + + {rank} +
+ +
+ {isMe && ( +
+ 나 +
+ )} +
+ {value.nickname} +
+ + {value.amount || 0} + +
+ + {Math.trunc(value.probability * 100) || 0} + + .{Math.floor(((value.probability * 100) % 1) * 10)}% +
+
+ ); +}; + +interface Event2024FallStoreItemProps { + itemId: string; +} + +const Event2024FallStoreItem = ({ itemId }: Event2024FallStoreItemProps) => { + const fetchEvent2024FallInfo = useFetchRecoilState("event2024FallInfo"); + const event2024FallInfo = useValueRecoilState("event2024FallInfo"); + const isLogin = useIsLogin(); + const isRequesting = useRef(false); + const [purchaseAmount, setPurchaseAmount] = useState(1); + const [isOpenPurchaseSuccess, setIsOpenPurchaseSuccess] = + useState(false); + const setAlert = useSetRecoilState(alertAtom); + const axios = useAxios(); + const changePurchaseAmountHandler = ( + e: React.ChangeEvent + ) => { + const value = e.target.valueAsNumber; + if (value < 1 || isNaN(value)) { + setPurchaseAmount(1); + } else { + setPurchaseAmount(value); + } + }; + + const { leaderboard, totalAmount, totalUser, amount, probability, rank } = + useQuery.get(`/events/2024fall/items/leaderboard/${itemId}`, null, [ + event2024FallInfo, + ])[1] || { + leaderboard: [], + totalAmount: 0, + totalUser: 0, + amount: 0, + probability: 0, + }; + + const { item } = useQuery.get("/events/2024fall/items/" + itemId)[1] || { + item: null, + }; + const { nickname, profileImgUrl } = useValueRecoilState("loginInfo") || {}; + const myLeaderboardInfo = useMemo>(() => { + if (!nickname || !profileImgUrl || !probability) return null; + return { + nickname, + profileImageUrl: profileImgUrl, + amount: amount || 0, + probability, + rank, + }; + }, [nickname, profileImgUrl, probability]); + + const [isDisabled, buttonText] = useMemo( + () => + eventMode !== "2024fall" + ? [true, "이벤트 기간이 아닙니다"] + : !event2024FallInfo || !isLogin + ? [true, "로그인해야 합니다"] + : event2024FallInfo.isAgreeOnTermsOfEvent === false + ? [true, "이벤트에 참여해야 합니다"] + : event2024FallInfo.creditAmount < (item?.price * purchaseAmount || 0) + ? [true, "송편코인이 부족합니다"] + : [false, "응모권 구매하기"], + [eventMode, event2024FallInfo, item, purchaseAmount] + ); + + const onClickOk = useCallback(async () => { + if (isRequesting.current) return; + isRequesting.current = true; + await axios({ + url: `/events/2024fall/items/purchase/${item?._id}`, + method: "post", + data: { + amount: purchaseAmount, + }, + onSuccess: () => { + fetchEvent2024FallInfo(); + setPurchaseAmount(1); + setIsOpenPurchaseSuccess(true); + }, + onError: () => setAlert("구매를 실패하였습니다."), + }); + isRequesting.current = false; + }, [item?._id, fetchEvent2024FallInfo, purchaseAmount]); + + // fetchItems, + return ( + <> + + +
+ +
+
+ {item && ( + {}} + showDescription + /> + )} + +
+ 경품 수량 +
: {item?.realStock || 0}개 +
+
+ 발급한 사용자 수 +
: {totalUser}명 +
+
+ 발급된 총 응모권 수 +
+ : + {" "} + X {totalAmount}개 +
+
+
+ + 구매 수량: + +
+ +
+
+ {leaderboard.length > 0 ? ( + <> + + {leaderboard.map((elem: LeaderboardElem, index: number) => ( + + ))} + {rank > 20 && myLeaderboardInfo && ( + + )} +
+ • 리더보드에 표시되는 확률은 이상적인 가정 하에 계산된{" "} + 상품에 당첨될 확률의 근삿값으로, 실제 확률과 다를 수 + 있습니다.
+
• 확률의 총 합이 100%가 아닌 이유는 여러 사람이 상품에 + 당첨될 수 있기 때문으로, 잘못된 계산이 아닙니다. +
+
• 확률은 다른 사용자의 참여에 따라 계속 변동될 수 있습니다. +
+ + ) : ( + 리더보드가 비어있습니다. + )} + +
+
+ + ); +}; + +export default Event2024FallStoreItem; diff --git a/packages/web/src/pages/Event/Event2024FallStore/ItemListSection.tsx b/packages/web/src/pages/Event/Event2024FallStore/ItemListSection.tsx index 81e17ddf9..859e99765 100644 --- a/packages/web/src/pages/Event/Event2024FallStore/ItemListSection.tsx +++ b/packages/web/src/pages/Event/Event2024FallStore/ItemListSection.tsx @@ -1,107 +1,9 @@ -import { memo, useState } from "react"; +import { memo } from "react"; import type { EventItem } from "@/types/event2024fall"; import Empty from "@/components/Empty"; -import { - ModalEvent2024FallItem, - ModalEvent2024FallRandomBox, -} from "@/components/ModalPopup"; -import WhiteContainer from "@/components/WhiteContainer"; - -import theme from "@/tools/theme"; - -// ToDo : 2023fall 이미지 -import { ReactComponent as CreditIcon } from "@/static/events/2023fallCredit.svg"; - -type EventItemComponentProps = { - value: EventItem; - fetchItems?: () => void; -}; - -const EventItemContainer = ({ value, fetchItems }: EventItemComponentProps) => { - const [isOpen, setIsOpen] = useState(false); - const [rewardItem, setRewardItem] = useState>(null); - - return ( - setIsOpen(true)} - > -
- {value.name} -
-
- {value.name} -
-
- -
- {value.price} -
-
- - {value.itemType === 3 && ( - setRewardItem(null)} - item={rewardItem || undefined} - /> - )} -
- ); -}; +import EventItemContainer from "@/components/Event/EventItemContainer"; type ItemListSectionProps = { items: Array; @@ -122,6 +24,7 @@ const ItemListSection = ({ items, fetchItems }: ItemListSectionProps) => { key={item._id} value={item} fetchItems={fetchItems} + clickable /> ))}
diff --git a/packages/web/src/pages/Event/Event2024FallStore/PublicNoticeContainer.tsx b/packages/web/src/pages/Event/Event2024FallStore/PublicNoticeContainer.tsx index 17f6ba900..a35d5a8ef 100644 --- a/packages/web/src/pages/Event/Event2024FallStore/PublicNoticeContainer.tsx +++ b/packages/web/src/pages/Event/Event2024FallStore/PublicNoticeContainer.tsx @@ -1,44 +1,17 @@ import { css, keyframes } from "@emotion/react"; import { useMemo } from "react"; -import useQuery from "@/hooks/useTaxiAPI"; - import WhiteContainer from "@/components/WhiteContainer"; -import dayjs, { - dayDifference2str, - dayNowServer, - dayServerToClient, -} from "@/tools/day"; import theme from "@/tools/theme"; import CampaignRoundedIcon from "@mui/icons-material/CampaignRounded"; -type Transaction = { - text: string; - createAt: Date; -}; - const PublicNoticeContainer = () => { - const { transactions }: { transactions: Array } = useQuery.get( - "/events/2024fall/publicNotice/recentTransactions" - )[1] || { transactions: [] }; - const notices = useMemo(() => { - const publicNotices = transactions - .sort((a, b) => dayjs(b.createAt).diff(a.createAt)) - .map( - ({ text, createAt }) => - `[${dayDifference2str( - dayServerToClient(createAt), - dayNowServer() - )}] ${text}` - ); - return [ - "[공지] 아이템이 조기 소진될 경우 9월 30일(토), 10월 5일(목)에 추가 입고될 예정입니다.", - ...publicNotices, - "[공지] 이벤트가 종료되면 아이템을 구매할 수 없습니다. 종료 전에 송편을 모두 소진해주세요.", - ]; - }, [transactions]); + const notices = [ + "[공지] 이벤트가 종료되면 응모권과 랜덤박스를 구매할 수 없습니다. 종료 전에 송편코인을 모두 소진해 주세요.", + "[공지] 랜덤박스는 경품 응모권이 아닙니다. 일정 확률로 송편코인을 얻거나 잃을 수 있는 특수 아이템입니다.", + ]; const animationDuration = useMemo( () => notices.reduce((acc, text) => acc + text.length, 0) * 0.2, [notices] diff --git a/packages/web/src/pages/Event/Event2024FallStore/index.tsx b/packages/web/src/pages/Event/Event2024FallStore/index.tsx index 08bc733fd..b17c03394 100644 --- a/packages/web/src/pages/Event/Event2024FallStore/index.tsx +++ b/packages/web/src/pages/Event/Event2024FallStore/index.tsx @@ -29,7 +29,7 @@ const Event2024FallStore = () => { ), [items] ); - const [itemTypeZeros, itemTypeOnes] = useMemo( + const [itemTypeZeros, _] = useMemo( () => [getItemFilteredList([0, 3]), getItemFilteredList([1, 2])], [getItemFilteredList] ); @@ -39,17 +39,16 @@ const Event2024FallStore = () => { @@ -66,12 +65,8 @@ const Event2024FallStore = () => {
- - 응모권 - - - 아이템 + 경품 응모권 @@ -79,6 +74,5 @@ const Event2024FallStore = () => { ); }; -// ToDo : 2023fall 문구 및 footer export default memo(Event2024FallStore); diff --git a/packages/web/src/pages/Event/index.tsx b/packages/web/src/pages/Event/index.tsx index 5b2a39eb0..8de3b769f 100644 --- a/packages/web/src/pages/Event/index.tsx +++ b/packages/web/src/pages/Event/index.tsx @@ -9,16 +9,23 @@ import Event2023FallStore from "./Event2023FallStore"; import Event2023Spring from "./Event2023Spring"; import Event2023SpringGuide from "./Event2023SpringGuide"; import Event2024Fall from "./Event2024Fall"; +import Event2024FallDailyAttendance from "./Event2024FallDailyAttendance"; import Event2024FallHistory from "./Event2024FallHistory"; -import Event2024FallLeaderboard from "./Event2024FallLeaderboard"; import Event2024FallMissions from "./Event2024FallMissions"; import Event2024FallStore from "./Event2024FallStore"; +import Item from "./Event2024FallStore/Item"; import Event2024Spring from "./Event2024Spring"; import Event2024SpringLeaderboard from "./Event2024SpringLeaderboard"; import Event2024SpringMissions from "./Event2024SpringMissions"; const Event = () => { - const { eventName } = useParams() as { eventName: string }; + const { eventName, itemId } = useParams() as { + eventName: string; + itemId: string; + }; + if (eventName === "2024fall-store" && itemId) { + return ; + } switch (eventName) { case "2022beta": @@ -49,10 +56,10 @@ const Event = () => { return ; case "2024fall-history": return ; - case "2024fall-leaderboard": - return ; case "2024fall-missions": return ; + case "2024fall-daily-attendance": + return ; default: return ; } diff --git a/packages/web/src/pages/Home/EventSection/EventSection2024Fall.tsx b/packages/web/src/pages/Home/EventSection/EventSection2024Fall.tsx index c41b964fe..845318853 100644 --- a/packages/web/src/pages/Home/EventSection/EventSection2024Fall.tsx +++ b/packages/web/src/pages/Home/EventSection/EventSection2024Fall.tsx @@ -2,8 +2,8 @@ import { ReactElement } from "react"; import { Link } from "react-router-dom"; import AdaptiveDiv from "@/components/AdaptiveDiv"; -// import CreditAmountStatusContainer from "@/components/Event/CreditAmountStatusContainer"; -// import WhiteContainerSuggestJoinEvent from "@/components/Event/WhiteContainerSuggestJoinEvent"; +import CreditAmountStatusContainer from "@/components/Event/CreditAmountStatusContainer"; +import WhiteContainerSuggestJoinEvent from "@/components/Event/WhiteContainerSuggestJoinEvent"; import Title from "@/components/Title"; import WhiteContainer from "@/components/WhiteContainer"; @@ -71,11 +71,7 @@ const EventSection2024Fall = () => { 추석 이벤트 - {/* */} +
{ <> 2024/09/07 - 09/23
- 진행 예정! + 절찬리 진행 중! } >
-
- {/* */} - 이지피지하게 달성하고 송편코인 받기} - > - - - {/* */} +
+ + 이지피지하게 달성하고 송편코인 받기} + > + + +
-
- {/* */} - - 응모권 구매해서 -
- 경품 추첨에 참여하기 - - } - > - -
- {/* */} +
+ + + 응모권 구매해서 +
+ 경품 추첨에 참여하기 + + } + > + +
+
- {/* */} + ); }; diff --git a/packages/web/src/pages/Home/EventSection/EventSection2024FallResult.tsx b/packages/web/src/pages/Home/EventSection/EventSection2024FallResult.tsx deleted file mode 100644 index a4533438f..000000000 --- a/packages/web/src/pages/Home/EventSection/EventSection2024FallResult.tsx +++ /dev/null @@ -1,63 +0,0 @@ -import { Link } from "react-router-dom"; - -import AdaptiveDiv from "@/components/AdaptiveDiv"; -import Button from "@/components/Button"; -import Title from "@/components/Title"; -import WhiteContainer from "@/components/WhiteContainer"; - -import theme from "@/tools/theme"; - -const EventSection2024FallResult = () => { - const styleText = { - ...theme.font14, - marginBottom: "12px", - }; - const styleButton = { - padding: "14px 0 13px", - borderRadius: "12px", - ...theme.font14_bold, - }; - - return ( - - - 한가위 송편 이벤트 - - -
- 🎉 경품 추첨 결과 발표 및 상품 수령 안내 -
-
- 인스타그램 게시글을 통해 추첨 결과 및 수령 방법을 확인하실 수 - 있습니다. -
-
- 또한 이벤트 참여 때 등록해주신 연락처로 상품 수령 방법을 - 안내해드렸으나 받지 못하신 분들은 마이페이지 {">"} 채널톡 문의하기를 - 통해 연락주시면 감사하겠습니다. -
-
- 많은 관심을 가지고 이벤트에 참여해주셔서 감사합니다 🙇 -
- - - -
- - - - - - ); -}; - -export default EventSection2024FallResult; diff --git a/packages/web/src/static/events/2024fallDailyAttendance.svg b/packages/web/src/static/events/2024fallDailyAttendance.svg new file mode 100644 index 000000000..12ac92371 --- /dev/null +++ b/packages/web/src/static/events/2024fallDailyAttendance.svg @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/web/src/static/events/2024fallJackpotFail.png b/packages/web/src/static/events/2024fallJackpotFail.png new file mode 100644 index 000000000..b087b3f86 Binary files /dev/null and b/packages/web/src/static/events/2024fallJackpotFail.png differ diff --git a/packages/web/src/static/events/2024fallJackpotSuccess.png b/packages/web/src/static/events/2024fallJackpotSuccess.png new file mode 100644 index 000000000..9a90b65e5 Binary files /dev/null and b/packages/web/src/static/events/2024fallJackpotSuccess.png differ diff --git a/packages/web/src/static/events/2024fallMainSection2.svg b/packages/web/src/static/events/2024fallMainSection2.svg index 651b19b1f..3fdcac14d 100644 --- a/packages/web/src/static/events/2024fallMainSection2.svg +++ b/packages/web/src/static/events/2024fallMainSection2.svg @@ -13,7 +13,7 @@ - + diff --git a/packages/web/src/static/events/2024fallMainSection4.svg b/packages/web/src/static/events/2024fallMainSection4.svg index c311eca89..7d15753ca 100644 --- a/packages/web/src/static/events/2024fallMainSection4.svg +++ b/packages/web/src/static/events/2024fallMainSection4.svg @@ -1,13 +1,13 @@ - - + + - + - + @@ -18,8 +18,8 @@ - - + + @@ -33,8 +33,8 @@ - - + + @@ -45,25 +45,25 @@ - - + + - - + + - - + + - - + + - - + + - - - - - + + + + + diff --git a/packages/web/src/static/events/2024fallTicket.svg b/packages/web/src/static/events/2024fallTicket.svg new file mode 100644 index 000000000..b192644c7 --- /dev/null +++ b/packages/web/src/static/events/2024fallTicket.svg @@ -0,0 +1,73 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/web/src/tools/random/event2024FallStoreSpeeches.ts b/packages/web/src/tools/random/event2024FallStoreSpeeches.ts index 090669042..632103b7d 100644 --- a/packages/web/src/tools/random/event2024FallStoreSpeeches.ts +++ b/packages/web/src/tools/random/event2024FallStoreSpeeches.ts @@ -1,37 +1,36 @@ -// ToDo : 2023fall 을 그대로 복사해 온 값입니다. export default [ - "와 이런 상품도 있다고? 빨리 구매해야겠어!", - "이런 상품이 있다니! 빨리 구매해야겠어!", - "아니 어떻게 이런 상품이 있지? 빨리 구매해야겠어!", - "이거 안사면 후회할거야", - "와, 이런 아이템은 처음 봐! 놓치면 아쉬울 것 같아!", - "진짜 이런 제품이 있다고? 바로 장바구니에 넣어야겠어!", - "이걸 안 사면 정말 손해야. 빨리 구매해!", - "와, 이 가격에 이런 상품이? 당장 사야겠다!", - "정말 이렇게 좋은 걸 지금 발견했어? 얼른 구매해야겠어!", - "다들 이거 어떻게 놓치고 있지? 나는 사야겠다!", - "이런 제품 오랜만이야. 재고 빨리 사라질 것 같은데?", - "이거 진짜 놓치면 후회할 거야, 당장 사자!", - "말도 안 되는 제품이네. 어떻게 이걸 지금 봤지? 사야지!", - "와, 이 상품은 뭐야? 재고가 얼마나 남았을까? 빨리 사야겠어.", - "이런 걸 안 사면 정말 후회할 것 같아. 지금 바로 구매!", - "아, 이런 상품을 찾고 있었는데! 빨리 사야겠다.", - "와, 이건 진짜 특별한 아이템이네! 구매하기 전에 또 누가 사가지 않을까?", - "뭐야, 이런 것도 팔고 있었어? 바로 주문해야겠다!", - "아직 이런 상품을 모르는 사람 있을까? 어서 구매해야지.", - "와, 이 제품 정말 멋있어. 빠르게 주문하자!", - "이런 제품을 이 가격에? 믿기지 않아. 얼른 사야겠어!", - "다른 사람들은 어떻게 이걸 놓치고 있지? 나는 당장 사야겠다.", - "이 상품은 정말 특별한 거 같아. 빨리 구매하자!", - "어떻게 이런 품질의 제품을 이렇게 싸게 팔고 있지? 당장 구매!", - "와, 이건 정말 나만 알고 싶은 제품이네! 그래도 빨리 사야겠다.", - "이런 제품을 찾고 있었는데 드디어 발견! 바로 주문해야지.", - "아, 이건 정말 내 스타일이야! 빨리 구매하고 싶다.", - "이 제품이 이렇게 인기 있는 이유를 알겠어. 당장 사야겠다!", - "다른 사람들도 이걸 알게 되면 재고 없어질 거 같아. 빨리 구매해야지!", - "이런 상품을 지금 발견한 나는 정말 운이 좋아! 얼른 사야겠다.", - "와, 이런 것도 있다고? 놓치면 나중에 후회할 것 같아. 바로 구매!", - "아, 이런 제품이 있었던 거야? 빨리 구매해야겠다.", - "정말 이런 특별한 제품을 놓칠 수 없어. 지금 바로 구매!", - "와, 이런 제품은 정말 길게 찾았어. 빨리 사야겠다!", + "이거 당첨되면 진짜 대박인데… 응모해봐야지!", + "송편코인 모아놨는데, 이제 응모권 살 때가 됐나?", + "에어팟 갖고 싶다… 이번에 응모하면 당첨될 수 있겠지?", + "K380 키보드 갖고 싶다… 이번에 응모하면 당첨될 수 있겠지?", + "경품 진짜 탐난다… 송편코인 더 모아야 하나?", + "어? 응모권이 모자라네… 더 사야 하나?", + "이거 응모하면 나도 당첨될 수 있을거야!", + "다들 응모할 텐데… 나도 한번 해볼까?", + "이 경품은 놓치면 안 돼! 지금 바로 응모해야지.", + "혹시 이번엔 당첨될지도 모르니까 응모 한번 해봐야겠다!", + "와, 송편코인 모았으니까 드디어 응모할 수 있겠다!", + "이번 기회는 놓치면 안 될 것 같아… 얼른 응모해야지!", + "이거 나한테 딱인데! 응모권 바로 구매할래.", + "송편코인으로 에어팟 응모해야겠다! 이건 놓치면 안 돼!", + "K380 키보드 응모하고 싶어서 송편코인 모으는 중이야!", + "배달의민족 상품권으로 저녁 해결할까…", + "카카오T 상품권이 있으면 집 갈 때 편하게 택시 타고 갈 수 있겠어!", + "아이스크림은 바로 당첨될 것 같은데? 한 번 응모해봐야지!", + "아이스 아메리카노 응모권으로 시원한 커피 마시고 싶다!", + "송편코인 조금만 더 모으면 에어팟도 도전해볼 수 있겠다!", + "K380 키보드 갖고 싶어서 송편코인 쓰는 게 아깝지가 않아!", + "이번에 아이스크림 당첨되면 친구랑 같이 먹어야지!", + "송편코인 모으기 진짜 재밌네, 에어팟 도전!", + "로지텍 키보드 너무 예뻐… 송편코인 더 필요해!", + "이 아메리카노 응모해서 당첨되면 바로 카페 가야지!", + "이번 기회 놓치면 아쉬울 것 같아… 송편코인부터 준비해야지.", + "응모권 더 모으면 당첨 확률도 올라가겠지?", + "응모권만 있으면 내가 원하는 걸 얻을 수 있겠지!", + "다들 응모권 사는 것 같던데, 나도 하나 더 사야겠다.", + "응모권이 쌓일 때마다 더 설레는 기분!", + "응모는 나중에, 오늘은 랜덤박스로 대박 노려볼까?", + "랜덤박스로 대박 나면 응모권 더 많이 살 수 있을 거야!", + "응모권을 살까, 아니면 랜덤박스로 송편코인 2배를 노려볼까?", + "응모권보다 스릴 넘치는 랜덤박스, 이번에 도전할까?", ]; diff --git a/packages/web/src/tools/random/index.ts b/packages/web/src/tools/random/index.ts index 75da73c0d..9163ce413 100644 --- a/packages/web/src/tools/random/index.ts +++ b/packages/web/src/tools/random/index.ts @@ -13,7 +13,6 @@ export const randomTaxiSloganGenerator = randomGenerator(taxiSlogans); export const randomEvent2023FallStoreSpeechGenerator = randomGenerator( event2023FallStoreSpeeches ); - export const randomEvent2024FallStoreSpeechGenerator = randomGenerator( event2024FallStoreSpeeches ); diff --git a/packages/web/src/types/event2024fall.d.ts b/packages/web/src/types/event2024fall.d.ts index 5d1849932..d72ccac8e 100644 --- a/packages/web/src/types/event2024fall.d.ts +++ b/packages/web/src/types/event2024fall.d.ts @@ -31,6 +31,11 @@ export type EventItem = { itemType: number; }; +export type RandomBoxResult = { + isJackpot: boolean; + amount: number; +}; + export type Transaction = { type: "get" | "use"; amount: number; diff --git a/serve/invite-event.html b/serve/invite-event.html index 95823b342..955cbf348 100644 --- a/serve/invite-event.html +++ b/serve/invite-event.html @@ -38,10 +38,10 @@ - +