From fa685496e4e3c651a5f10fc31a97016158f528ed Mon Sep 17 00:00:00 2001 From: Hyogyeong8 Date: Fri, 6 Sep 2024 00:47:23 +0900 Subject: [PATCH 1/9] Create: daily-attendance page --- .../Event/DailyAttendanceCalendar/index.tsx | 339 ++++++++++++++++++ .../Event/Event2024FallDailyAttendance.tsx | 83 +++++ .../src/pages/Event/Event2024FallMissions.tsx | 16 +- packages/web/src/pages/Event/index.tsx | 3 + .../static/events/2024fallDailyAttendance.svg | 35 ++ 5 files changed, 472 insertions(+), 4 deletions(-) create mode 100644 packages/web/src/components/Event/DailyAttendanceCalendar/index.tsx create mode 100644 packages/web/src/pages/Event/Event2024FallDailyAttendance.tsx create mode 100644 packages/web/src/static/events/2024fallDailyAttendance.svg 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..db92ee300 --- /dev/null +++ b/packages/web/src/components/Event/DailyAttendanceCalendar/index.tsx @@ -0,0 +1,339 @@ +import PropTypes from "prop-types"; +import { PureComponent, createRef, memo } from "react"; + +import useHoverProps from "@/hooks/theme/useHoverProps"; + +import DottedLine from "@/components/DottedLine"; +import MiniCircle from "@/components/MiniCircle"; + +import { getToday10 } from "@/tools/moment"; +import theme from "@/tools/theme"; + +import TodayRoundedIcon from "@mui/icons-material/TodayRounded"; +import UnfoldLessRoundedIcon from "@mui/icons-material/UnfoldLessRounded"; +import UnfoldMoreRoundedIcon from "@mui/icons-material/UnfoldMoreRounded"; + +const getCalendarDates = () => { + const MAX_AVAILABLE_DATES = 14; + const today = getToday10(); + const date = today.clone(); + date.subtract(date.day(), "day"); + + const calendar = []; + let datesCount = 0; + + while (datesCount < MAX_AVAILABLE_DATES) { + const week = []; + for (let i = 0; i < 7; i++) { + let available = null; + if (date.isSame(today, "day")) { + available = "today"; + datesCount++; + } else if ( + datesCount < MAX_AVAILABLE_DATES && + date.isAfter(today, "day") + ) { + available = true; + datesCount++; + } + week.push({ + year: date.year(), + month: date.month() + 1, + date: date.date(), + available, + }); + date.add(1, "day"); + } + calendar.push(week); + } + return calendar; +}; +type DateProps = { + index: number; + year: any; + month: any; + date: any; + available: any; + selected: any; + handler: (year: any, month: any, date: any) => {}; +}; + +const Date = (props: DateProps) => { + const [hoverProps, isHover] = useHoverProps(); + + const style = { + width: "calc((100% - 36px) / 7)", + aspectRatio: "1 / 1", + height: "100%", + }; + const styleBox = { + ...style, + borderRadius: "6px", + position: "relative", + display: "flex", + alignItems: "center", + justifyContent: "center", + background: props.selected + ? isHover + ? theme.purple_dark + : theme.purple + : props.available + ? isHover + ? theme.purple_hover + : theme.purple_light + : theme.gray_background, + boxShadow: props.available + ? props.selected + ? theme.shadow_purple_button_inset + : theme.shadow_purple_input_inset + : undefined, + ...theme.cursor(!props.available), + transitionDuration: theme.duration, + }; + const styleDate = { + ...theme.font12, + letterSpacing: undefined, + marginTop: "3px", + fontWeight: props.selected ? 500 : undefined, + color: props.selected + ? theme.white + : props.available + ? props.index === 0 + ? theme.red_text + : props.index === 6 + ? theme.blue_text + : theme.black + : theme.gray_line, + }; + const styleToday = { + position: "absolute", + top: "calc(50% + 8px)", + left: "calc(50% - 2px)", + }; + + const onClick = () => { + if (props.available) { + props.handler(props.year, props.month, props.date); + + const scrollTo = + document.querySelector(".scroll-to-button").getBoundingClientRect() + .top + // 화면 상에서 버튼의 현재 위치 + window.scrollY + // 스크롤 위치 + (46 + 15 + 56) - // 버튼의 높이 + 버튼과 네비게이션 사이의 간격 + 네비게이션 높이 + window.innerHeight; // - 화면 높이 + + if (window.scrollY < scrollTo) + window.scrollTo({ + top: scrollTo, + behavior: "smooth", + }); + } + }; + + if (!props.date) return
; + return ( +
+
{props.date}
+ {props.available === "today" && ( +
+ +
+ )} +
+ ); +}; +const MemoizedDate = memo(Date); + +class DailyAttendanceCalendar extends PureComponent { + constructor(props) { + super(props); + this.state = { + isOpen: true, + // maxHeight: undefined, + }; + + this.pickerRef = createRef(null); + this.clicked = false; + + this.dateHandler = this.dateHandler.bind(this); + this.onClickOutside = this.onClickOutside.bind(this); + this.onClickTop = this.onClickTop.bind(this); + this.onOpen = this.onOpen.bind(this); + this.onClose = this.onClose.bind(this); + + this.week = [ + { 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: "토" }, + ]; + + this.styleTop = { + display: "flex", + justifyContent: "space-between", + marginBottom: "10px", + }; + this.styleInfo = { + display: "flex", + alignItems: "center", + ...theme.font14, + }; + this.styleIcon = { + fontSize: "16px", + margin: "0 6px 0 9px", + }; + this.styleArrow = { + width: "24px", + height: "24px", + fill: theme.purple, + ...theme.cursor(), + }; + this.styleMonth = { + display: "flex", + flexDirection: "column", + rowGap: "6px", + marginBottom: "5px", + }; + this.styleDay = { + display: "flex", + margin: "12px 0 8px", + columnGap: "6px", + }; + this.styleDayItem = { + width: "calc((100% - 36px) / 7)", + fontSize: "10px", + height: "12px", + textAlign: "center", + }; + this.styleWeek = { + display: "flex", + columnGap: "6px", + }; + } + + dateHandler(year, month, date) { + if (!this.clicked) this.clicked = true; + this.props.handler(year, month, date); + } + + handleMaxHeight(event, callback) { + event.stopPropagation(); + const monthHeight = + document.getElementsByClassName("month")[0]?.clientHeight ?? 0; + this.setState({ maxHeight: 24 + 10 + 1 + 32 + monthHeight + 5 }, callback); + } + + onOpen(event) { + this.handleMaxHeight(event, () => + this.setState({ isOpen: true }, () => { + setTimeout(() => this.setState({ maxHeight: undefined }), 300); + }) + ); + } + + onClose(event) { + this.handleMaxHeight(event, () => + setTimeout(() => this.setState({ isOpen: false }), 0) + ); + } + + onClickTop(event) { + if (this.state.isOpen) return; + this.clicked = false; + this.onOpen(event); + } + + onClickOutside(event) { + if (this.clicked && !this.pickerRef?.current.contains(event.target)) + this.onClose(event); + } + + render() { + const dateInfo = getCalendarDates(); + const [selectedYear, selectedMonth, selectedDate] = this.props.selectedDate; + + return ( +
+
+
+ + 날짜 : {selectedYear}년 {selectedMonth}월 {selectedDate}일 +
+ {this.state.isOpen ? ( + + ) : ( + + )} +
+ +
+ {this.week.map((item, index) => ( +
+ {item.text} +
+ ))} +
+
+ {dateInfo.map((item, index) => { + return ( +
+ {item.map((item, index) => ( + + ))} +
+ ); + })} +
+
+ ); + } + componentDidMount() { + document.addEventListener("mouseup", this.onClickOutside); + } + componentWillUnmount() { + document.removeEventListener("mouseup", this.onClickOutside); + } +} + +DailyAttendanceCalendar.propTypes = { + selectedDate: PropTypes.array, + handler: PropTypes.func, +}; + +export default DailyAttendanceCalendar; diff --git a/packages/web/src/pages/Event/Event2024FallDailyAttendance.tsx b/packages/web/src/pages/Event/Event2024FallDailyAttendance.tsx new file mode 100644 index 000000000..562fe5f75 --- /dev/null +++ b/packages/web/src/pages/Event/Event2024FallDailyAttendance.tsx @@ -0,0 +1,83 @@ +import { memo, useMemo, useState } from "react"; + +import { useValueRecoilState } from "@/hooks/useFetchRecoilState"; + +import AdaptiveDiv from "@/components/AdaptiveDiv"; +import Button from "@/components/Button"; +import CreditAmountStatusContainer from "@/components/Event/CreditAmountStatusContainer"; +import DailyAttendanceCalendar from "@/components/Event/DailyAttendanceCalendar"; +import WhiteContainerSuggestJoinEvent from "@/components/Event/WhiteContainerSuggestJoinEvent"; +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"; + +// ToDo : 2023fall 이미지 +import { ReactComponent as CreditIcon } from "@/static/events/2023fallCredit.svg"; +import { ReactComponent as MissionCompleteIcon } from "@/static/events/2023fallMissionComplete.svg"; +import { ReactComponent as DailyAttendance } from "@/static/events/2024fallDailyAttendance.svg"; + +type DateSectionProps = { + value: Array>; + handler: (newValue: Array) => void; +}; + +const DateSection = (props: DateSectionProps) => { + return ( + + props.handler([x, y, z])} + /> + + ); +}; + +const Event2024FallMissions = () => { + const today = getToday(); + + const [valueDate, setDate] = useState>>([ + today.year(), + today.month() + 1, + today.date(), + ]); + + return ( + <> + + + + + + + +