diff --git a/docs/src/components/Sidebar.js b/docs/src/components/Sidebar.js index 18478b4..e7261bb 100644 --- a/docs/src/components/Sidebar.js +++ b/docs/src/components/Sidebar.js @@ -56,6 +56,10 @@ const sidebarLinks = [ name: 'Tabs', ref: '/tab', }, + { + name: 'Calendar', + ref: '/calendar', + }, ]; export const cssNavLink = css` diff --git a/docs/src/pages/calendar.md b/docs/src/pages/calendar.md new file mode 100644 index 0000000..dc66331 --- /dev/null +++ b/docs/src/pages/calendar.md @@ -0,0 +1,21 @@ +# Button + +Primitive button component with variants + + + +```jsx + console.log(date)} /> +``` + + + +## props + +| prop | type | desc | +| ------------ | ------ | ------------------------------------------------------ | +| onChange | func | Callback fired whenever there is a change in the date. | +| onClickDay | func | callback fired when the day is clicked. | +| onClickMonth | func | callback fired when the month is clicked | +| onClickYear | func | callback fired when the year is clicked | +| variant | string | diff --git a/docs/src/theme.js b/docs/src/theme.js index 8901d56..e65f3f4 100644 --- a/docs/src/theme.js +++ b/docs/src/theme.js @@ -245,7 +245,7 @@ export default { }, }, }, - + inputGroup: { primary: { marginTop: '10px', @@ -259,6 +259,18 @@ export default { }, }, }, + calendar: { + primary: { + wrapper: { + color: '#777777', + height: '351px', + width: '344px', + borderRadius: '5px', + bg: 'background', + boxShadow: '0 0 7px 0 rgba(0, 0, 0, 0.15)', + }, + }, + }, collapse: { primary: { @@ -298,7 +310,6 @@ export default { separater: { mx: 3, }, - }, }, }; diff --git a/src/components/calendar/assets.js b/src/components/calendar/assets.js new file mode 100644 index 0000000..1f08207 --- /dev/null +++ b/src/components/calendar/assets.js @@ -0,0 +1,41 @@ +import React from "react"; + +const ArrowDown = () => ( + + + +); + +const ArrowUp = () => ( + + + +); + +export { ArrowDown, ArrowUp }; diff --git a/src/components/calendar/component.js b/src/components/calendar/component.js new file mode 100644 index 0000000..eff06d8 --- /dev/null +++ b/src/components/calendar/component.js @@ -0,0 +1,138 @@ +import { Box, css, Flex, Text } from 'theme-ui'; +import styled from '@emotion/styled'; +import { applyVariation } from '../../utils/getStyles'; + +const DatesButton = styled(Text)` + ${({ theme, currentMonth = true, active = false }) => + css({ + textTransform: 'uppercase', + fontSize: '14px', + color: active ? 'rgba(0, 166, 251,1)' : 'inherit', + opacity: currentMonth ? 1 : 0.5, + '&:hover': { + color: 'rgba(0, 166, 251,0.8)', + }, + cursor: 'pointer', + })(theme)} + ${({ theme, variant }) => + applyVariation(theme, `${variant}.date`, 'calendar')} +`; + +const Wrapper = styled(Box)(({ theme, variant }) => + applyVariation(theme, `${variant}.wrapper`, 'calendar') +); + +const Line = styled(Box)` + ${({ theme }) => + css({ + width: '10px', + height: '2px', + bg: '#4a4a4a', + mr: 20, + ml: 20, + })(theme)} + ${({ theme, variant }) => + applyVariation(theme, `${variant}.line`, 'calendar')} +`; +const DaysTable = styled.table` + ${({ theme }) => + css({ + height: '92%', + width: '95%', + textAlign: 'center', + })(theme)} + ${({ theme, variant }) => + applyVariation(theme, `${variant}.daysTable`, 'calendar')} +`; +const MonthsTable = styled.table` + ${({ theme }) => + css({ + height: '85%', + width: '90%', + textAlign: 'center', + mt: 10, + alignSelf: 'center', + })(theme)} + ${({ theme, variant }) => + applyVariation(theme, `${variant}.monthsTable`, 'calendar')} +`; +const YearsTable = styled.table` + ${({ theme }) => + css({ + height: '85%', + width: '90%', + textAlign: 'center', + alignSelf: 'center', + })(theme)} + ${({ theme, variant }) => + applyVariation(theme, `${variant}.yearsTable`, 'calendar')} +`; + +const OuterWrapper = styled(Flex)` + ${({ theme }) => + css({ + flexDirection: 'column', + width: '100%', + height: '100%', + p: '20px 0', + alignItems: 'center', + })(theme)} +`; + +const HeaderText = styled(Text)` + ${({ theme }) => + css({ + color: 'inherit', + fontSize: '20px', + textTransform: 'uppercase', + '&:hover': { + color: 'rgba(0, 166, 251,0.8)', + }, + cursor: 'pointer', + })(theme)} + ${({ theme, variant }) => + applyVariation(theme, `${variant}.headerText`, 'calendar')} +`; + +const HeaderContainer = styled(Flex)` + ${({ theme }) => + css({ + width: '100%', + alignItems: 'center', + justifyContent: 'center', + })(theme)} +`; +const DaysText = styled.table` + ${({ theme }) => + css({ + textTransform: 'uppercase', + color: '#5c5c5c', + fontSize: 14, + })(theme)} + ${({ theme, variant }) => + applyVariation(theme, `${variant}.daysText`, 'calendar')} +`; +const ArrowIcon = styled(Box)` + ${({ theme }) => + css({ + cursor: 'pointer', + width: 17, + height: 27, + })(theme)} + ${({ theme, variant }) => + applyVariation(theme, `${variant}.arrowIcon`, 'calendar')} +`; + +export { + Wrapper, + OuterWrapper, + DaysTable, + MonthsTable, + YearsTable, + HeaderText, + HeaderContainer, + Line, + DatesButton, + DaysText, + ArrowIcon, +}; diff --git a/src/components/calendar/dates.js b/src/components/calendar/dates.js new file mode 100644 index 0000000..14d0603 --- /dev/null +++ b/src/components/calendar/dates.js @@ -0,0 +1,26 @@ +const months = [ + 'Jan', + 'Feb', + 'Mar', + 'Apr', + 'May', + 'Jun', + 'Jul', + 'Aug', + 'Sep', + 'Oct', + 'Nov', + 'Dec', +]; + +const days = ['SUN', 'MON', 'TUE', 'WED', 'THU', 'FRI', 'SAT']; + +const initialYears = [ + [2013, 2014, 2015], + [2016, 2017, 2018], + [2019, 2020, 2021], + [2022, 2023, 2024], + [2025, 2026, 2027], +]; + +export { months, days, initialYears }; diff --git a/src/components/calendar/days.js b/src/components/calendar/days.js new file mode 100644 index 0000000..a2cd641 --- /dev/null +++ b/src/components/calendar/days.js @@ -0,0 +1,157 @@ +import React from "react"; +import PropTypes from "prop-types"; +import { months, days } from "./dates"; +import { createChunks } from "./utils"; +import { + OuterWrapper, + DatesButton, + DaysTable, + HeaderText, + HeaderContainer, + DaysText, + Line, +} from "./component"; + +const Days = ({ + dayChangeHandler, + updateScreen, + date: calDate, + onMonthChange, + variant, +}) => { + const [dates, setDates] = React.useState([]); + const getLastDate = (year, month) => new Date(year, month + 1, 0).getDate(); + const generateDays = () => { + const { month, year } = calDate; + const firstDay = new Date(year, month).getDay(); + const daysArray = []; + const previousMonthNYear = + month === 0 ? { year: year - 1, month: 11 } : { year, month: month - 1 }; + const lastDateOfPreviousMonth = getLastDate( + previousMonthNYear.year, + previousMonthNYear.month + ); + let reverseDay = lastDateOfPreviousMonth; + for (let i = 0; i < firstDay; i += 1) { + daysArray.push({ + day: reverseDay - firstDay + 1, + month: "prev", + }); + reverseDay += 1; + } + const lastDate = getLastDate(year, month); + const { length } = daysArray; + let j = 1; + for (let i = length; i < lastDate + length; i += 1) { + daysArray.push({ + day: j, + month: "current", + }); + j += 1; + } + const { length: newLen } = daysArray; + j = 1; + for (let i = newLen; i < 42; i += 1) { + daysArray.push({ day: j, month: "next" }); + j += 1; + } + return daysArray; + }; + + const updateDates = () => { + const daysArray = generateDays(); + const updatedDates = createChunks(daysArray, 7); + setDates(updatedDates); + }; + + React.useEffect(() => { + updateDates(); + }, [calDate]); + + const dayClickHandler = (date) => { + if (date.month === "current") { + dayChangeHandler(date.day); + } else if (date.month === "prev") { + const newMonth = calDate.month - 1; + onMonthChange( + newMonth < 0 ? months.length - 1 : newMonth, + date.day, + newMonth < 0 ? calDate.year - 1 : null + ); + } else { + const newMonth = calDate.month + 1; + onMonthChange( + newMonth === months.length ? 0 : newMonth, + date.day, + newMonth === months.length ? calDate.year + 1 : null + ); + } + }; + return ( + + + { + updateScreen("month"); + }} + > + {months[calDate.month]} + + + { + updateScreen("year"); + }} + > + {calDate.year} + + + + + + {days.map((day) => ( + + {day} + + ))} + + {dates.map((datesGroup, index) => ( + + {datesGroup.map((date) => ( + + dayClickHandler(date)} + > + {date.day} + + + ))} + + ))} + + + + ); +}; + +Days.propTypes = { + dayChangeHandler: PropTypes.func.isRequired, + updateScreen: PropTypes.func.isRequired, + date: PropTypes.shape({ + day: PropTypes.number, + month: PropTypes.number, + year: PropTypes.number, + }).isRequired, + onMonthChange: PropTypes.func.isRequired, + variant: PropTypes.string.isRequired, +}; + +export default Days; diff --git a/src/components/calendar/index.js b/src/components/calendar/index.js new file mode 100644 index 0000000..18a4171 --- /dev/null +++ b/src/components/calendar/index.js @@ -0,0 +1,111 @@ +import React from "react"; +import PropTypes from "prop-types"; +import Months from "./months"; +import Years from "./years"; +import Days from "./days"; +import { Wrapper } from "./component"; + +const Calendar = React.forwardRef( + ( + { onChange, variant, onClickDay, onClickMonth, onClickYear, ...rest }, + ref + ) => { + const [date, setDate] = React.useState({ + day: null, + month: null, + year: null, + }); + const [screen, setScreen] = React.useState("day"); + + React.useEffect(() => { + const currentDate = new Date(); + setDate({ + day: currentDate.getDate(), + month: currentDate.getMonth(), + year: currentDate.getFullYear(), + }); + }, []); + + const updateDate = (newDate) => { + setDate(newDate); + if (onChange) { + onChange({ ...newDate, month: newDate.month + 1 }); + } + }; + const updateCalendarScreen = (screenToShow) => { + setScreen(screenToShow); + }; + + const handle = (key, value, day = null, year = null) => { + const newDate = { ...date }; + newDate[key] = value; + if (key === "month" && day !== null) newDate.day = day; + if (key === "month" && year !== null) newDate.year = year; + updateDate(newDate); + if (key === "month" || key === "year") updateCalendarScreen("day"); + }; + + const handleYearChange = (year) => { + handle("year", year); + onClickYear(); + }; + + const handleMonthChange = (month, day = null, year = null) => { + handle("month", month, day, year); + onClickMonth(month); + }; + + const handleDateChange = (day) => { + handle("day", day); + onClickDay(day); + }; + const calendarScreens = { + day: ( + updateCalendarScreen(newScreen)} + variant={variant} + /> + ), + month: ( + updateCalendarScreen(newScreen)} + variant={variant} + /> + ), + year: ( + + ), + }; + + return ( + + {calendarScreens[screen]} + + ); + } +); + +Calendar.defaultProps = { + onChange: null, + variant: "primary", + toggleOnDayClick: () => {}, + onClickDay: () => null, + onClickMonth: () => null, + onClickYear: () => null, +}; + +Calendar.propTypes = { + onChange: PropTypes.func, + variant: PropTypes.string, + toggleOnDayClick: PropTypes.func, + onClickDay: PropTypes.func, + onClickMonth: PropTypes.func, + onClickYear: PropTypes.func, +}; + +export default Calendar; diff --git a/src/components/calendar/months.js b/src/components/calendar/months.js new file mode 100644 index 0000000..863816e --- /dev/null +++ b/src/components/calendar/months.js @@ -0,0 +1,60 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { months } from './dates'; +import { createChunks } from './utils'; +import { + OuterWrapper, + MonthsTable, + DatesButton, + HeaderText, + HeaderContainer, +} from './component'; + +const Months = ({ onMonthSelect, date, updateScreen, variant }) => { + const monthsGroup = createChunks(months, 3); + + return ( + + + updateScreen('year')} variant={variant}> + {date.year} + + + + + {monthsGroup.map((monthGrp, index) => ( + + {monthGrp.map((month) => ( + + { + onMonthSelect(months.indexOf(month)); + }} + > + {month} + + + ))} + + ))} + + + + ); +}; + +Months.propTypes = { + onMonthSelect: PropTypes.func.isRequired, + date: PropTypes.shape({ + day: PropTypes.number, + month: PropTypes.number, + year: PropTypes.number, + }).isRequired, + updateScreen: PropTypes.func.isRequired, + variant: PropTypes.string.isRequired, +}; + +export default Months; diff --git a/src/components/calendar/utils.js b/src/components/calendar/utils.js new file mode 100644 index 0000000..0bdd959 --- /dev/null +++ b/src/components/calendar/utils.js @@ -0,0 +1,10 @@ +const createChunks = (array, chunkSize) => { + const newArray = []; + for (let i = 0, j = array.length; i < j; i += chunkSize) { + const arrayChunk = array.slice(i, i + chunkSize); + newArray.push(arrayChunk); + } + return newArray; +}; + +export { createChunks }; diff --git a/src/components/calendar/years.js b/src/components/calendar/years.js new file mode 100644 index 0000000..0971808 --- /dev/null +++ b/src/components/calendar/years.js @@ -0,0 +1,61 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { ArrowDown, ArrowUp } from './assets'; +import { initialYears } from './dates'; +import { OuterWrapper, DatesButton, YearsTable, ArrowIcon } from './component'; + +const Years = ({ onYearChange, date, variant }) => { + const [years, setYears] = React.useState(initialYears); + const arrowClickHandler = (downArrow = true) => { + let newYears = [...years]; + newYears = newYears.map((currentYears) => + currentYears.map((year) => (downArrow ? year + 3 : year - 3)) + ); + setYears(newYears); + }; + + return ( + + arrowClickHandler(false)}> + + + + + {years.map((yearChunk, index) => ( + + {yearChunk.map((year) => ( + + { + onYearChange(year); + }} + active={date.year === year} + > + {year} + + + ))} + + ))} + + + + + + + ); +}; + +Years.propTypes = { + onYearChange: PropTypes.func.isRequired, + date: PropTypes.shape({ + day: PropTypes.number, + month: PropTypes.number, + year: PropTypes.number, + }).isRequired, + variant: PropTypes.string.isRequired, +}; + +export default Years; diff --git a/src/components/index.js b/src/components/index.js index dfddf7d..8a6024b 100644 --- a/src/components/index.js +++ b/src/components/index.js @@ -1,24 +1,25 @@ -export * from 'theme-ui'; +export * from "theme-ui"; -export { Relative, Absolute, Fixed, Sticky } from './position'; -export { Inline, InlineBlock, InlineFlex } from './layout'; -export Background from './background'; -export Input from './input'; -export Textarea from './textArea'; -export Select from './select'; -export IconButton from './iconButton'; -export Truncate from './truncate'; -export ReadMore from './readMore'; -export Hide from './hide'; -export withBeforeAfter from './withBeforeAfter'; -export Tooltip from './tooltip'; -export { Modal, openModal, closeModal, PortableModalContainer } from './modal'; -export { Toast, openToast, PortableToastContainer } from './toast'; -export DropdownMenu from './dropdownMenu'; -export Pill from './pill'; -export Switch from './switch'; -export ButtonGroup from './buttonGroup'; -export InputGroup from './inputGroup'; -export Tabs from './tab'; -export Collapse from './collapse'; -export Breadcrumbs from './breadcrumbs'; +export { Relative, Absolute, Fixed, Sticky } from "./position"; +export { Inline, InlineBlock, InlineFlex } from "./layout"; +export Background from "./background"; +export Input from "./input"; +export Textarea from "./textArea"; +export Select from "./select"; +export IconButton from "./iconButton"; +export Truncate from "./truncate"; +export ReadMore from "./readMore"; +export Hide from "./hide"; +export withBeforeAfter from "./withBeforeAfter"; +export Tooltip from "./tooltip"; +export { Modal, openModal, closeModal, PortableModalContainer } from "./modal"; +export { Toast, openToast, PortableToastContainer } from "./toast"; +export DropdownMenu from "./dropdownMenu"; +export Pill from "./pill"; +export Switch from "./switch"; +export ButtonGroup from "./buttonGroup"; +export InputGroup from "./inputGroup"; +export Tabs from "./tab"; +export Collapse from "./collapse"; +export Breadcrumbs from "./breadcrumbs"; +export Calendar from "./calendar";