Created
May 19, 2022 13:23
-
-
Save SebastianUdden/d8d5547ea2ab0dbab8e54beea2224bb6 to your computer and use it in GitHub Desktop.
A baseline responsive calendar component
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import styled from "styled-components"; | |
| import moment from "moment"; | |
| import { useState } from "react"; | |
| const firstDayOfMonth = (date: any) => | |
| moment(date).startOf("month").format("d"); | |
| const getBlanks = (firstDayOfMonth: number) => { | |
| const blanks = []; | |
| for (let i = 0; i < firstDayOfMonth; i++) { | |
| blanks.push(<Empty>{""}</Empty>); | |
| } | |
| return blanks; | |
| }; | |
| const getYears = (year: number) => { | |
| const modifier = 11; | |
| const years = []; | |
| for (let i = -1; i < modifier; i++) { | |
| years.push(year + i); | |
| } | |
| return years; | |
| }; | |
| const getCurrentDate = (dateObject: any, today: any) => { | |
| if ( | |
| dateObject.format("MMMM") === today.format("MMMM") && | |
| dateObject.format("Y") === today.format("Y") | |
| ) { | |
| return +dateObject.format("D"); | |
| } | |
| return -1; | |
| }; | |
| const Calendar = () => { | |
| const [selectedDate, setSelectedDate] = useState<number>(0); | |
| const [showMonths, setShowMonths] = useState(false); | |
| const [showYears, setShowYears] = useState(false); | |
| const [dateObject, setDateObject] = useState(moment()); | |
| const allMonths = moment.months(); | |
| const yearTable = getYears(+moment().format("Y")); | |
| const weekdaysShort = moment.weekdaysShort(); | |
| const currentMoment = moment(); | |
| const thisMonth = currentMoment.format("MMMM"); | |
| const thisYear = currentMoment.format("Y"); | |
| const selectedMonth = dateObject.format("MMMM"); | |
| const selectedYear = dateObject.format("Y"); | |
| const handleMonthChange = (i: number) => { | |
| setSelectedDate(0); | |
| setDateObject(moment(dateObject).set("month", i)); | |
| }; | |
| const handleYearChange = (year: number) => { | |
| setSelectedDate(0); | |
| setDateObject(moment(dateObject).set("year", year)); | |
| setShowMonths(true); | |
| }; | |
| const handlePrev = () => { | |
| setSelectedDate(0); | |
| setDateObject(moment(dateObject.subtract(1, showYears ? "year" : "month"))); | |
| }; | |
| const handleNext = () => { | |
| setSelectedDate(0); | |
| setDateObject(moment(dateObject.add(1, showYears ? "year" : "month"))); | |
| }; | |
| const getDays = (daysInMonth: number) => { | |
| const currentDate = getCurrentDate(dateObject, currentMoment); | |
| const days = []; | |
| for (let d = 1; d <= daysInMonth; d++) { | |
| if (d === currentDate) { | |
| days.push( | |
| <Today | |
| key={d} | |
| onClick={() => setSelectedDate(d)} | |
| isSelected={selectedDate === d} | |
| > | |
| {d} | |
| </Today> | |
| ); | |
| } else { | |
| days.push( | |
| <Day | |
| key={d} | |
| onClick={() => setSelectedDate(d)} | |
| isSelected={selectedDate === d} | |
| > | |
| {d} | |
| </Day> | |
| ); | |
| } | |
| } | |
| return days; | |
| }; | |
| const blanks = getBlanks(+firstDayOfMonth(dateObject)); | |
| const days = getDays(dateObject.daysInMonth()); | |
| return ( | |
| <Wrapper> | |
| <Flex> | |
| <Arrow onClick={() => handlePrev()}>←</Arrow> | |
| <Month | |
| onClick={() => { | |
| setShowMonths(!showMonths); | |
| setShowYears(false); | |
| }} | |
| > | |
| {selectedMonth} | |
| </Month> | |
| <Year | |
| onClick={() => { | |
| setShowYears(!showYears); | |
| setShowMonths(false); | |
| }} | |
| > | |
| {selectedYear} | |
| </Year> | |
| <Arrow onClick={() => handleNext()}>→</Arrow> | |
| </Flex> | |
| <Head> | |
| {showMonths && <Title>Select a month</Title>} | |
| {showYears && <Title>Select a year</Title>} | |
| {!showMonths && | |
| !showYears && | |
| weekdaysShort.map((day) => <div>{day}</div>)} | |
| </Head> | |
| {showMonths && ( | |
| <Months> | |
| {allMonths.map((m, i) => ( | |
| <Month | |
| key={m} | |
| isCurrent={thisMonth === m} | |
| onClick={() => { | |
| setShowMonths(false); | |
| handleMonthChange(i); | |
| }} | |
| > | |
| {m} | |
| </Month> | |
| ))} | |
| </Months> | |
| )} | |
| {showYears && ( | |
| <Months> | |
| {yearTable.map((y) => ( | |
| <Month | |
| isCurrent={+thisYear === y} | |
| onClick={() => { | |
| setShowYears(false); | |
| handleYearChange(y); | |
| }} | |
| > | |
| {y} | |
| </Month> | |
| ))} | |
| </Months> | |
| )} | |
| {!showMonths && !showYears && ( | |
| <Body> | |
| {blanks} | |
| {days} | |
| </Body> | |
| )} | |
| </Wrapper> | |
| ); | |
| }; | |
| const Wrapper = styled.div` | |
| box-shadow: 0 3px 6px rgba(0, 0, 0, 0.16), 0 3px 6px rgba(0, 0, 0, 0.23); | |
| `; | |
| const Flex = styled.div` | |
| display: flex; | |
| justify-content: space-between; | |
| `; | |
| const Button = styled.button` | |
| border: none; | |
| background-color: inherit; | |
| padding: 20px 25px; | |
| font-size: 20px; | |
| font-weight: 700; | |
| cursor: pointer; | |
| :hover { | |
| box-shadow: 0 3px 6px rgba(0, 0, 0, 0.16), 0 3px 6px rgba(0, 0, 0, 0.23); | |
| } | |
| :active { | |
| box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12), 0 1px 2px rgba(0, 0, 0, 0.24); | |
| } | |
| `; | |
| const Month = styled(Button)<{ isCurrent?: boolean }>` | |
| flex: 1; | |
| ${(p) => | |
| p.isCurrent && | |
| ` | |
| background-color: ${p.theme.primary.bgColor}; | |
| color: ${p.theme.primary.color}; | |
| `} | |
| `; | |
| const Year = styled(Button)` | |
| flex: 1; | |
| `; | |
| const Months = styled.div` | |
| div { | |
| text-align: center; | |
| } | |
| padding: 20px; | |
| display: grid; | |
| grid-template-columns: repeat(1, 1fr); | |
| @media (min-width: 400px) { | |
| display: grid; | |
| grid-template-columns: repeat(2, 1fr); | |
| } | |
| @media (min-width: 600px) { | |
| display: grid; | |
| grid-template-columns: repeat(3, 1fr); | |
| } | |
| `; | |
| const Head = styled.div` | |
| box-sizing: border-box; | |
| display: flex; | |
| div { | |
| flex: 1; | |
| box-sizing: border-box; | |
| background-color: ${(p) => p.theme.secondary.bgColor}; | |
| color: ${(p) => p.theme.secondary.color}; | |
| padding: 20px 0; | |
| text-align: center; | |
| } | |
| `; | |
| const Body = styled.div` | |
| display: grid; | |
| grid-template-columns: repeat(7, 1fr); | |
| div { | |
| padding: 20px 0; | |
| text-align: center; | |
| } | |
| `; | |
| const Empty = styled.div` | |
| flex: 1; | |
| `; | |
| const Day = styled.div<{ isSelected: boolean }>` | |
| flex: 1; | |
| cursor: pointer; | |
| user-select: none; | |
| :hover { | |
| box-shadow: 0 3px 6px rgba(0, 0, 0, 0.16), 0 3px 6px rgba(0, 0, 0, 0.23); | |
| } | |
| ${(p) => | |
| p.isSelected && | |
| ` | |
| background-color: ${p.theme.secondary.bgColor}; | |
| color: ${p.theme.secondary.color}; | |
| box-shadow: 0 3px 6px rgba(0,0,0,0.16), 0 3px 6px rgba(0,0,0,0.23); | |
| :hover { | |
| box-shadow: 0 10px 20px rgba(0,0,0,0.19), 0 6px 6px rgba(0,0,0,0.23); | |
| } | |
| `} | |
| `; | |
| const Today = styled(Day)` | |
| background-color: ${(p) => p.theme.primary.bgColor}; | |
| color: ${(p) => p.theme.primary.color}; | |
| box-shadow: 0 3px 6px rgba(0, 0, 0, 0.16), 0 3px 6px rgba(0, 0, 0, 0.23); | |
| :hover { | |
| box-shadow: 0 10px 20px rgba(0, 0, 0, 0.19), 0 6px 6px rgba(0, 0, 0, 0.23); | |
| } | |
| ${(p) => | |
| p.isSelected && | |
| ` | |
| background-color: ${p.theme.secondary.bgColor}; | |
| color: ${p.theme.secondary.color}; | |
| box-shadow: 0 3px 6px rgba(0,0,0,0.16), 0 3px 6px rgba(0,0,0,0.23); | |
| `} | |
| `; | |
| const Title = styled.div` | |
| padding: 20px; | |
| font-weight: 700; | |
| background-color: ${(p) => p.theme.secondary.bgColor}; | |
| color: ${(p) => p.theme.secondary.color}; | |
| `; | |
| const Arrow = styled(Button)``; | |
| export default Calendar; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment