Skip to content

Commit

Permalink
Merge pull request #826 from sparcs-kaist/818-2024-fall-event-daily-a…
Browse files Browse the repository at this point in the history
…ttendance

#818 - 2024 추석 이벤트 출석 체크
  • Loading branch information
kmc7468 authored Sep 6, 2024
2 parents f6298b3 + 2867c55 commit d0da633
Show file tree
Hide file tree
Showing 8 changed files with 456 additions and 6 deletions.
211 changes: 211 additions & 0 deletions packages/web/src/components/Event/DailyAttendanceCalendar/index.tsx
Original file line number Diff line number Diff line change
@@ -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 <div style={style} />;
return (
<div style={styleBox}>
<div style={styleDate}>{date}</div>
{available === "today" && (
<div style={styleToday}>
<MiniCircle type="date" />
</div>
)}
{checked && <MissionCompleteIcon style={styleCompleteIcon} />}
</div>
);
};
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 (
<div
className="datepicker"
style={{
transition: "max-height 0.3s ease-in-out",
margin: "-10px -15px",
padding: "10px 15px",
}}
>
<div style={styleDay}>
{week.map((item, index) => (
<div
key={index}
style={{
...styleDayItem,
color: item.color,
opacity: 0.632,
}}
>
{item.text}
</div>
))}
</div>
<div className="month" style={styleMonth}>
{dateInfo.map((item, index) => {
return (
<div key={index} style={styleWeek}>
{item.map((item, index) => (
<MemoizedDate
key={index}
index={index}
year={item.year}
month={item.month}
date={item.date}
available={item.available}
checked={item.checked}
/>
))}
</div>
);
})}
</div>
</div>
);
};

export default DailyAttendanceCalendar;
Original file line number Diff line number Diff line change
@@ -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<Nullable<number>>;
handler: (newValue: Array<number>) => void;
};

const DateSection = (props: DateSectionProps) => {
return (
<WhiteContainer css={{ padding: "10px 15px" }}>
<DailyAttendanceCalendar />
</WhiteContainer>
);
};

interface ModalEvent2024FallDailyAttendanceProps {
isOpen: boolean;
onChangeIsOpen?: ((isOpen: boolean) => void) | undefined;
}

const ModalEvent2024FallDailyAttendance = ({
isOpen,
onChangeIsOpen,
}: ModalEvent2024FallDailyAttendanceProps) => {
const today = getToday();

const [valueDate, setDate] = useState<Array<Nullable<number>>>([
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 (
<Modal
padding="16px 12px 12px"
isOpen={isOpen}
onChangeIsOpen={onChangeIsOpen}
css={{ display: "flex", flexDirection: "column" }}
>
<DailyAttendance
css={{ width: "92%", height: "200px", margin: "0 4%" }}
/>
<CreditAmountStatusContainer />
<DateSection value={valueDate} handler={setDate} />
<Button
type="purple"
disabled={true}
css={{
width: "100%",
padding: "14px 0 13px",
borderRadius: "12px",
...theme.font16_bold,
}}
>
오늘자 출석이 완료되었습니다.
</Button>
</Modal>
);
};

export default ModalEvent2024FallDailyAttendance;
1 change: 1 addition & 0 deletions packages/web/src/components/ModalPopup/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down
15 changes: 13 additions & 2 deletions packages/web/src/components/Skeleton/index.tsx
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -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";
Expand Down Expand Up @@ -64,6 +67,9 @@ const Skeleton = ({ children }: SkeletonProps) => {
[pathname]
);

const [dailyAttendanceOpened, setDailyAttendanceOpened] =
useState<boolean>(false);

//#region event2024Fall
useEvent2024FallEffect();
//#endregion
Expand Down Expand Up @@ -92,6 +98,11 @@ const Skeleton = ({ children }: SkeletonProps) => {
)}
{children}
<ModalTerms isOpen={!!userId && !isAgreeOnTermsOfService} />
<ModalEvent2024FallDailyAttendance
isOpen={dailyAttendanceOpened}
onChangeIsOpen={setDailyAttendanceOpened}
/>

{isDisplayNavigation && (
<div
css={{
Expand Down
Loading

0 comments on commit d0da633

Please sign in to comment.