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 00000000..12aed413 --- /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/ModalPopup/ModalEvent2024FallDailyAttendance.tsx b/packages/web/src/components/ModalPopup/ModalEvent2024FallDailyAttendance.tsx new file mode 100644 index 00000000..c745b7c1 --- /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/index.tsx b/packages/web/src/components/ModalPopup/index.tsx index 1b98e6eb..64a31bf4 100644 --- a/packages/web/src/components/ModalPopup/index.tsx +++ b/packages/web/src/components/ModalPopup/index.tsx @@ -12,6 +12,7 @@ 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 ModalEvent2024FallDailyAttendance } from "./ModalEvent2024FallDailyAttendance"; export { default as ModalEvent2024FallItem } from "./ModalEvent2024FallItem"; export { default as ModalEvent2024FallJoin } from "./ModalEvent2024FallJoin"; export { default as ModalEvent2024FallRandomBox } from "./ModalEvent2024FallRandomBox"; diff --git a/packages/web/src/components/Skeleton/index.tsx b/packages/web/src/components/Skeleton/index.tsx index 6ebff3f7..c08c1ca8 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 && (
>; + 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 ( + <> + + + + + + + +