├── .babelrc
├── .editorconfig
├── .gitignore
├── LICENSE
├── README.md
├── package.json
├── public
├── favicon.ico
├── index.html
└── manifest.json
├── src
├── App.css
├── App.js
├── App.test.js
├── components
│ ├── Calendar
│ │ ├── index.js
│ │ └── styles.js
│ └── Datepicker
│ │ ├── index.js
│ │ └── styles.js
├── helpers
│ └── calendar.js
├── index.css
├── index.js
├── logo.svg
└── registerServiceWorker.js
└── yarn.lock
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "plugins": [
3 | ["babel-plugin-styled-components", {
4 | "displayName": false,
5 | "pure": true
6 | }]
7 | ]
8 | }
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | indent_style = space
5 | indent_size = 2
6 | end_of_line = lf
7 | charset = utf-8
8 | trim_trailing_whitespace = false
9 | insert_final_newline = false
10 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 |
8 | # Runtime data
9 | pids
10 | *.pid
11 | *.seed
12 | *.pid.lock
13 |
14 | # Directory for instrumented libs generated by jscoverage/JSCover
15 | lib-cov
16 |
17 | # Coverage directory used by tools like istanbul
18 | coverage
19 |
20 | # nyc test coverage
21 | .nyc_output
22 |
23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
24 | .grunt
25 |
26 | # Bower dependency directory (https://bower.io/)
27 | bower_components
28 |
29 | # node-waf configuration
30 | .lock-wscript
31 |
32 | # Compiled binary addons (https://nodejs.org/api/addons.html)
33 | build/Release
34 |
35 | # Dependency directories
36 | node_modules/
37 | jspm_packages/
38 |
39 | # TypeScript v1 declaration files
40 | typings/
41 |
42 | # Optional npm cache directory
43 | .npm
44 |
45 | # Optional eslint cache
46 | .eslintcache
47 |
48 | # Optional REPL history
49 | .node_repl_history
50 |
51 | # Output of 'npm pack'
52 | *.tgz
53 |
54 | # Yarn Integrity file
55 | .yarn-integrity
56 |
57 | # dotenv environment variables file
58 | .env
59 |
60 | # next.js build output
61 | .next
62 | # See https://help.github.com/ignore-files/ for more about ignoring files.
63 |
64 | # dependencies
65 | /node_modules
66 |
67 | # testing
68 | /coverage
69 |
70 | # production
71 | /build
72 |
73 | # misc
74 | .DS_Store
75 | .env.local
76 | .env.development.local
77 | .env.test.local
78 | .env.production.local
79 |
80 | npm-debug.log*
81 | yarn-debug.log*
82 | yarn-error.log*
83 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2018 Glad Chinda
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Building Custom DatePicker (React Demo)
2 |
3 | This project contains the source code of a demo app showing how to build a custom datepicker component for a [React][react] application, which can be used whenever valid date values are required.
4 |
5 | Prior experience working with the React framework is required for complete understanding of the demo code. You can checkout the [docs][react-docs] to learn more about React.
6 |
7 | Here is a screenshot of the demo React application:
8 |
9 | 
10 |
11 | Before you begin, make sure you have [`npm`][npm] and [`node`][node] installed on your system. It is recommended that you use [`yarn`][yarn] instead of `npm` to run and manage the package. Follow this [guide to install yarn][yarn-install] on your system.
12 |
13 | Once you have either `yarn` or `npm` installed, run the following commands to get started.
14 |
15 | > Note that this demo was bootstrapped using the `create-react-app` package. You can look through this [extensive guide for the available scripts](https://github.com/facebook/create-react-app/blob/master/packages/react-scripts/template/README.md#available-scripts).
16 |
17 | **Using NPM**
18 |
19 | ```sh
20 | npm install
21 | npm start
22 | ```
23 |
24 | **Using Yarn**
25 |
26 | ```sh
27 | yarn
28 | yarn start
29 | ```
30 |
31 | You can also get a [live demo on Code Sandbox][code-demo].
32 |
33 | [][code-demo]
34 |
35 |
36 | [react-docs]: https://reactjs.org/docs/
37 | [react]: https://reactjs.org/
38 | [code-demo]: https://codesandbox.io/s/o952xlqq9y
39 | [node]: https://nodejs.org/en/
40 | [npm]: https://npmjs.com/
41 | [yarn]: https://yarnpkg.com/
42 | [yarn-install]: https://yarnpkg.com/lang/en/docs/install/
43 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-datepicker",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "bootstrap": "^4.1.3",
7 | "prop-types": "^15.6.2",
8 | "react": "^16.5.2",
9 | "react-dom": "^16.5.2",
10 | "react-scripts": "1.1.5",
11 | "reactstrap": "^6.4.0",
12 | "styled-components": "^4.0.0-beta.8"
13 | },
14 | "scripts": {
15 | "start": "react-scripts start",
16 | "build": "react-scripts build",
17 | "test": "react-scripts test --env=jsdom",
18 | "eject": "react-scripts eject"
19 | },
20 | "devDependencies": {
21 | "babel-plugin-styled-components": "^1.7.1"
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gladchinda/react-datepicker-demo/13b58d7ed2f43a04c3fae1309be55d37b785b062/public/favicon.ico
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
11 |
12 |
13 |
22 | React App
23 |
24 |
25 |
28 |
29 |
39 |
40 |
41 |
--------------------------------------------------------------------------------
/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "React App",
3 | "name": "Create React App Sample",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | }
10 | ],
11 | "start_url": "./index.html",
12 | "display": "standalone",
13 | "theme_color": "#000000",
14 | "background_color": "#ffffff"
15 | }
16 |
--------------------------------------------------------------------------------
/src/App.css:
--------------------------------------------------------------------------------
1 | .App {
2 | text-align: center;
3 | }
4 |
5 | .App-logo {
6 | animation: App-logo-spin infinite 20s linear;
7 | height: 80px;
8 | }
9 |
10 | .App-header {
11 | background-color: #222;
12 | height: 150px;
13 | padding: 20px;
14 | color: white;
15 | }
16 |
17 | .App-title {
18 | font-size: 1.5em;
19 | }
20 |
21 | .App-intro {
22 | font-size: large;
23 | }
24 |
25 | @keyframes App-logo-spin {
26 | from { transform: rotate(0deg); }
27 | to { transform: rotate(360deg); }
28 | }
29 |
--------------------------------------------------------------------------------
/src/App.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import DatePicker from './components/DatePicker';
3 |
4 | class App extends Component {
5 | render() {
6 | return (
7 |
8 |
9 |
10 | )
11 | }
12 | }
13 |
14 | export default App;
15 |
--------------------------------------------------------------------------------
/src/App.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import App from './App';
4 |
5 | it('renders without crashing', () => {
6 | const div = document.createElement('div');
7 | ReactDOM.render(, div);
8 | ReactDOM.unmountComponentAtNode(div);
9 | });
10 |
--------------------------------------------------------------------------------
/src/components/Calendar/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import { ArrowLeft, ArrowRight, CalendarContainer, CalendarHeader, CalendarGrid, CalendarDay, CalendarDate, CalendarMonth, HighlightedCalendarDate, TodayCalendarDate } from './styles';
4 | import calendar, { isDate, dateDiff, isBeforeDay, isAfterDay, isSameDay, isSameMonth, getDateISO, getDateArray, getNextMonth, getPreviousMonth, getMonthDays, WEEK_DAYS, CALENDAR_MONTHS } from '../../helpers/calendar';
5 |
6 | class Calendar extends React.Component {
7 |
8 | state = { ...this.stateFromProp(), today: new Date }
9 |
10 | stateFromDate({ date, min, max } = {}) {
11 | const { mindate: stateMinDate, maxdate: stateMaxDate, current: stateCurrent } = this.state || {};
12 | const minDate = min || stateMinDate;
13 | const maxDate = max || stateMaxDate;
14 | const currentDate = date || stateCurrent;
15 |
16 | const mindate = isAfterDay(minDate, maxDate) ? null : minDate;
17 | const maxdate = isBeforeDay(maxDate, minDate) ? null : maxDate;
18 | const current = this.calendarWithinRange(currentDate, { mindate, maxdate }) ? currentDate : null;
19 |
20 | const calendarDate = current || new Date;
21 |
22 | const [ year, month ] = getDateArray(
23 | this.calendarWithinRange(calendarDate, { mindate, maxdate }) ? calendarDate : maxdate
24 | );
25 |
26 | return { current, month, year, mindate, maxdate };
27 | }
28 |
29 | stateFromProp() {
30 | return this.stateFromDate(this.props);
31 | }
32 |
33 | changeHandler = date => () => {
34 | const { onDateChanged } = this.props;
35 | (typeof onDateChanged === 'function') && onDateChanged(date);
36 | }
37 |
38 | calendarWithinRange(date, { maxdate, mindate } = this.state) {
39 | if (!isDate(date)) return false;
40 | const min = new Date(new Date(+mindate).setDate(1)).setHours(0, 0, 0, 0);
41 | const max = new Date(new Date(+maxdate).setDate(getMonthDays(maxdate))).setHours(23, 59, 59, 999);
42 |
43 | return (date >= min || !mindate) && (date <= max || !maxdate);
44 | }
45 |
46 | getCalendarDates = () => {
47 | const { current, month, year } = this.state;
48 | const [ currentYear, currentMonth ] = current ? getDateArray(current) : [];
49 | return calendar(new Date([year || currentYear, month || currentMonth]));
50 | }
51 |
52 | gotoDate = date => evt => {
53 | evt && evt.preventDefault();
54 | const { current, maxdate, mindate } = this.state;
55 | const isCurrent = current && isSameDay(date, current);
56 | const outOfRange = isBeforeDay(date, mindate) || isAfterDay(date, maxdate);
57 |
58 | !(isCurrent || outOfRange) && this.setState(this.stateFromDate({ date }), this.changeHandler(date));
59 | }
60 |
61 | gotoPreviousMonth = () => {
62 | const { month, year } = this.state;
63 | const previousMonth = getPreviousMonth(new Date([year, month]));
64 | const previous = new Date([ previousMonth.year, previousMonth.month ]);
65 |
66 | this.calendarWithinRange(previous) && this.setState(previousMonth);
67 | }
68 |
69 | gotoNextMonth = () => {
70 | const { month, year } = this.state;
71 | const nextMonth = getNextMonth(new Date([year, month]));
72 | const next = new Date([ nextMonth.year, nextMonth.month ]);
73 |
74 | this.calendarWithinRange(next) && this.setState(nextMonth);
75 | }
76 |
77 | gotoPreviousYear = () => {
78 | const { month, year } = this.state;
79 | const previous = new Date([year - 1, month]);
80 | this.calendarWithinRange(previous) && this.setState({ year: year - 1 });
81 | }
82 |
83 | gotoNextYear = () => {
84 | const { month, year } = this.state;
85 | const next = new Date([year + 1, month]);
86 | this.calendarWithinRange(next) && this.setState({ year: year + 1 });
87 | }
88 |
89 | handlePressure = evt => (fn, fnShift) => {
90 | if (typeof fn === 'function' && typeof fnShift === 'function') {
91 | this.pressureShift = evt.shiftKey;
92 | this.pressureShift ? fnShift() : fn();
93 |
94 | this.pressureTimeout = setTimeout(() => {
95 | this.pressureTimer = setInterval(() => this.pressureShift ? fnShift() : fn(), 100);
96 | }, 500);
97 |
98 | document.addEventListener('keyup', this.handlePressureKeyup);
99 | document.addEventListener('keydown', this.handlePressureKeydown);
100 | }
101 | }
102 |
103 | handlePressureKeyup = evt => {
104 | evt.preventDefault();
105 | !evt.shiftKey && (this.pressureShift = !evt.shiftKey && false);
106 | }
107 |
108 | handlePressureKeydown = evt => {
109 | evt.preventDefault();
110 | evt.shiftKey && (this.pressureShift = true);
111 | }
112 |
113 | clearPressureTimer = () => {
114 | this.pressureTimer && clearInterval(this.pressureTimer);
115 | this.pressureTimeout && clearTimeout(this.pressureTimeout);
116 |
117 | this.pressureShift = false;
118 |
119 | document.removeEventListener('keyup', this.handlePressureKeyup);
120 | document.removeEventListener('keydown', this.handlePressureKeydown);
121 | }
122 |
123 | clearDayTimeout = () => {
124 | this.dayTimeout && clearTimeout(this.dayTimeout);
125 | }
126 |
127 | handlePrevious = evt => {
128 | evt && (
129 | evt.preventDefault(),
130 | this.handlePressure(evt)(this.gotoPreviousMonth, this.gotoPreviousYear)
131 | );
132 | }
133 |
134 | handleNext = evt => {
135 | evt && (
136 | evt.preventDefault(),
137 | this.handlePressure(evt)(this.gotoNextMonth, this.gotoNextYear)
138 | );
139 | }
140 |
141 | renderMonthAndYear = () => {
142 | const { month, year } = this.state;
143 | const monthname = Object.keys(CALENDAR_MONTHS)[ Math.max(0, Math.min(month - 1, 11)) ];
144 | const props = { onMouseUp: this.clearPressureTimer };
145 |
146 | return (
147 |
148 |
149 | {monthname} {year}
150 |
151 |
152 | )
153 | }
154 |
155 | renderDayLabels = (day, index) => {
156 | const daylabel = WEEK_DAYS[day].toUpperCase();
157 | return {daylabel}
158 | }
159 |
160 | renderCalendarDate = (date, index) => {
161 | const { current, month, year, today, maxdate, mindate } = this.state;
162 | const thisDay = new Date(date);
163 | const monthFirstDay = new Date([year, month]);
164 |
165 | const isToday = !!today && isSameDay(thisDay, today);
166 | const isCurrent = !!current && isSameDay(thisDay, current);
167 | const inMonth = !!(month && year) && isSameMonth(thisDay, monthFirstDay);
168 | const inRange = !(isBeforeDay(thisDay, mindate) || isAfterDay(thisDay, maxdate));
169 |
170 | const props = {
171 | index,
172 | inMonth,
173 | inRange,
174 | onClick: this.gotoDate(thisDay),
175 | title: thisDay.toDateString()
176 | };
177 |
178 | const DateComponent = isCurrent
179 | ? HighlightedCalendarDate
180 | : isToday ? TodayCalendarDate : CalendarDate;
181 |
182 | return (
183 |
184 | { thisDay.getDate() }
185 |
186 | )
187 | }
188 |
189 | componentDidMount() {
190 | const tomorrow = new Date().setHours(0, 0, 0, 0) + (24 * 60 * 60 * 1000);
191 |
192 | this.dayTimeout = setTimeout(() => {
193 | this.setState({ today: new Date }, this.clearDayTimeout);
194 | }, dateDiff(tomorrow));
195 | }
196 |
197 | componentDidUpdate(prevProps) {
198 | const { date, min, max } = this.props;
199 | const { date: prevDate, min: prevMin, max: prevMax } = prevProps;
200 |
201 | const dateMatch = (date === prevDate) || isSameDay(date, prevDate);
202 | const minMatch = (min === prevMin) || isSameDay(min, prevMin);
203 | const maxMatch = (max === prevMax) || isSameDay(max, prevMax);
204 |
205 | !(dateMatch && minMatch && maxMatch) && this.setState(this.stateFromDate(date), this.changeHandler(date));
206 | }
207 |
208 | componentWillUnmount() {
209 | this.clearPressureTimer();
210 | this.clearDayTimeout();
211 | }
212 |
213 | render() {
214 | return (
215 |
216 |
217 | { this.renderMonthAndYear() }
218 |
219 |
220 |
221 | { Object.keys(WEEK_DAYS).map(this.renderDayLabels) }
222 |
223 |
224 |
225 | { this.getCalendarDates().map(this.renderCalendarDate) }
226 |
227 |
228 |
229 |
230 | )
231 | }
232 |
233 | }
234 |
235 | Calendar.propTypes = {
236 | min: PropTypes.instanceOf(Date),
237 | max: PropTypes.instanceOf(Date),
238 | date: PropTypes.instanceOf(Date),
239 | onDateChanged: PropTypes.func
240 | };
241 |
242 | export default Calendar;
243 |
--------------------------------------------------------------------------------
/src/components/Calendar/styles.js:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 |
3 | export const Arrow = styled.button`
4 | appearance: none;
5 | user-select: none;
6 | outline: none !important;
7 | display: inline-block;
8 | position: relative;
9 | cursor: pointer;
10 | padding: 0;
11 | border: none;
12 | border-top: 1.6em solid transparent;
13 | border-bottom: 1.6em solid transparent;
14 | transition: all .25s ease-out;
15 | `;
16 |
17 | export const ArrowLeft = styled(Arrow)`
18 | border-right: 2.4em solid #ccc;
19 | left: 1.5rem;
20 | :hover {
21 | border-right-color: #06c;
22 | }
23 | `;
24 |
25 | export const ArrowRight = styled(Arrow)`
26 | border-left: 2.4em solid #ccc;
27 | right: 1.5rem;
28 | :hover {
29 | border-left-color: #06c;
30 | }
31 | `;
32 |
33 | export const CalendarContainer = styled.div`
34 | font-size: 5px;
35 | border: 2px solid #06c;
36 | border-radius: 5px;
37 | overflow: hidden;
38 | `;
39 |
40 | export const CalendarHeader = styled.div`
41 | display: flex;
42 | align-items: center;
43 | justify-content: space-between;
44 | `;
45 |
46 | export const CalendarGrid = styled.div`
47 | display: grid;
48 | grid-template: repeat(7, auto) / repeat(7, auto);
49 | `;
50 |
51 | export const CalendarMonth = styled.div`
52 | font-weight: 500;
53 | font-size: 5em;
54 | color: #06c;
55 | text-align: center;
56 | padding: 0.5em 0.25em;
57 | word-spacing: 5px;
58 | user-select: none;
59 | `;
60 |
61 | export const CalendarCell = styled.div`
62 | text-align: center;
63 | align-self: center;
64 | letter-spacing: 0.1rem;
65 | padding: 0.6em 0.25em;
66 | position: relative;
67 | user-select: none;
68 | grid-column: ${props => (props.index % 7) + 1} / span 1;
69 | `;
70 |
71 | export const CalendarDay = styled(CalendarCell)`
72 | font-weight: 600;
73 | font-size: 2.25em;
74 | color: #06c;
75 | border-top: 2px solid #06c;
76 | border-bottom: 2px solid #06c;
77 | border-right: ${props => (props.index % 7) + 1 === 7 ? `none` : `2px solid #06c`};
78 | `;
79 |
80 | export const CalendarDate = styled(CalendarCell)`
81 | font-weight: ${props => props.inMonth ? 500 : 300};
82 | font-size: 4em;
83 | cursor: ${props => props.inRange ? 'pointer' : 'default'};
84 | border-bottom: ${props => ((props.index + 1) / 7) <= 5 ? `1px solid #ddd` : `none`};
85 | border-right: ${props => (props.index % 7) + 1 === 7 ? `none` : `1px solid #ddd`};
86 | color: ${props => props.inRange ? props.inMonth ? `#333` : `#ddd` : `#ddd !important`};
87 | background: ${props => props.inRange ? 'transparent' : `rgba(102, 25, 0, 0.04) !important`};
88 | grid-row: ${props => Math.floor(props.index / 7) + 2} / span 1;
89 | transition: all .4s ease-out;
90 | :hover {
91 | color: #06c;
92 | background: rgba(0, 102, 204, 0.075);
93 | }
94 | `;
95 |
96 | export const HighlightedCalendarDate = styled(CalendarDate)`
97 | ${props => props.inRange && `
98 | color: #fff !important;
99 | background: #06c !important;
100 | ::before {
101 | content: '';
102 | position: absolute;
103 | top: -1px;
104 | left: -1px;
105 | width: calc(100% + 2px);
106 | height: calc(100% + 2px);
107 | border: 2px solid #06c;
108 | }
109 | `}
110 | `;
111 |
112 | export const TodayCalendarDate = styled(HighlightedCalendarDate)`
113 | ${props => props.inRange && `
114 | color: #06c !important;
115 | background: transparent !important;
116 | ::after {
117 | content: '';
118 | position: absolute;
119 | right: 0;
120 | bottom: 0;
121 | border-bottom: 0.75em solid #06c;
122 | border-left: 0.75em solid transparent;
123 | border-top: 0.75em solid transparent;
124 | }
125 | :hover {
126 | color: #06c !important;
127 | background: rgba(0, 102, 204, 0.075) !important;
128 | }
129 | `}
130 | `;
131 |
--------------------------------------------------------------------------------
/src/components/Datepicker/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import Calendar from '../Calendar';
4 | import { getDateISO } from '../../helpers/calendar';
5 | import { DatePickerContainer, DatePickerFormGroup, DatePickerLabel, DatePickerInput, DatePickerDropdown, DatePickerDropdownToggle, DatePickerDropdownMenu } from './styles';
6 |
7 | class DatePicker extends React.Component {
8 |
9 | state = { date: null, min: null, max: null, calendarOpen: false }
10 |
11 | toggleCalendar = () => this.setState({ calendarOpen: !this.state.calendarOpen })
12 |
13 | handleChange = evt => evt.preventDefault()
14 |
15 | handleDateChange = date => {
16 | const { onDateChange } = this.props;
17 | const { date: currentDate } = this.state;
18 | const newDate = getDateISO(date);
19 |
20 | (currentDate !== newDate) && this.setState({ date: newDate, calendarOpen: false }, () => {
21 | (typeof onDatePicked === 'function') && onDateChange(newDate);
22 | });
23 | }
24 |
25 | get value() {
26 | return this.state.date || '';
27 | }
28 |
29 | get date() {
30 | const { date } = this.state;
31 | return date ? new Date(date) : null;
32 | }
33 |
34 | get mindate() {
35 | const { min } = this.state;
36 | return min ? new Date(min) : null;
37 | }
38 |
39 | get maxdate() {
40 | const { max } = this.state;
41 | return max ? new Date(max) : null;
42 | }
43 |
44 | componentDidMount() {
45 | const { value: date, min, max } = this.props;
46 |
47 | const minDate = getDateISO(min ? new Date(min) : null);
48 | const maxDate = getDateISO(max ? new Date(max) : null);
49 | const newDate = getDateISO(date ? new Date(date) : null);
50 |
51 | minDate && this.setState({ min: minDate });
52 | maxDate && this.setState({ max: maxDate });
53 | newDate && this.setState({ date: newDate });
54 | }
55 |
56 | componentDidUpdate(prevProps) {
57 | const { value: date, min, max } = this.props;
58 | const { value: prevDate, min: prevMin, max: prevMax } = prevProps;
59 |
60 | const dateISO = getDateISO(new Date(date));
61 | const prevDateISO = getDateISO(new Date(prevDate));
62 |
63 | const minISO = getDateISO(new Date(min));
64 | const prevMinISO = getDateISO(new Date(prevMin));
65 |
66 | const maxISO = getDateISO(new Date(max));
67 | const prevMaxISO = getDateISO(new Date(prevMax));
68 |
69 | (minISO !== prevMinISO) && this.setState({ min: minISO });
70 | (maxISO !== prevMaxISO) && this.setState({ max: maxISO });
71 | (dateISO !== prevDateISO) && this.setState({ date: dateISO });
72 | }
73 |
74 | render() {
75 | const { label } = this.props;
76 | const { calendarOpen } = this.state;
77 | const [value, placeholder] = [this.value, 'YYYY-MM-DD'].map(v => v.replace(/-/g, ' / '));
78 |
79 | return (
80 |
81 |
82 |
83 | { label || 'Enter Date' }
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 | { calendarOpen && }
92 |
93 |
94 |
95 |
96 | )
97 | }
98 |
99 | }
100 |
101 | DatePicker.propTypes = {
102 | min: PropTypes.string,
103 | max: PropTypes.string,
104 | label: PropTypes.string,
105 | value: PropTypes.string,
106 | onDateChange: PropTypes.func
107 | };
108 |
109 | export default DatePicker;
110 |
--------------------------------------------------------------------------------
/src/components/Datepicker/styles.js:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 | import { FormGroup, Label, Input, Dropdown, DropdownToggle, DropdownMenu } from 'reactstrap';
3 |
4 | export const DatePickerContainer = styled.div`
5 | position: relative;
6 | `;
7 |
8 | export const DatePickerFormGroup = styled(FormGroup)`
9 | display: flex;
10 | justify-content: space-between;
11 | position: relative;
12 | width: 100%;
13 | border: 2px solid #06c;
14 | border-radius: 5px;
15 | overflow: hidden;
16 | `;
17 |
18 | export const DatePickerLabel = styled(Label)`
19 | margin: 0;
20 | padding: 0 2rem;
21 | font-weight: 600;
22 | font-size: 0.7rem;
23 | letter-spacing: 2px;
24 | text-transform: uppercase;
25 | color: #06c;
26 | border-right: 2px solid #06c;
27 | display: flex;
28 | align-items: center;
29 | justify-content: center;
30 | background: rgba(0, 102, 204, 0.05);
31 | `;
32 |
33 | export const DatePickerInput = styled(Input)`
34 | padding: 1rem 2rem;
35 | font-weight: 500;
36 | font-size: 1rem;
37 | color: #333;
38 | box-shadow: none;
39 | border: none;
40 | text-align: center;
41 | letter-spacing: 1px;
42 | background: transparent !important;
43 | display: flex;
44 | align-items: center;
45 | appearance: textfield;
46 | -webkit-appearance: textfield;
47 | -moz-appearance: textfield;
48 |
49 | ::placeholder {
50 | color: #999;
51 | font-size: 0.9rem;
52 | }
53 | `;
54 |
55 | export const DatePickerDropdown = styled(Dropdown)`
56 | position: absolute;
57 | width: 100%;
58 | height: 100%;
59 | top: 0;
60 | left: 0;
61 | `
62 |
63 | export const DatePickerDropdownToggle = styled(DropdownToggle)`
64 | position: relative;
65 | width: 100%;
66 | height: 100%;
67 | background: transparent;
68 | opacity: 0;
69 | filter: alpha(opacity=0);
70 | `;
71 |
72 | export const DatePickerDropdownMenu = styled(DropdownMenu)`
73 | position: absolute;
74 | top: 0;
75 | left: 0;
76 | width: 100%;
77 | border: none;
78 | padding: 0;
79 | margin: 0;
80 | transform: none !important;
81 | `;
82 |
--------------------------------------------------------------------------------
/src/helpers/calendar.js:
--------------------------------------------------------------------------------
1 | export const WEEK_DAYS = {
2 | Sunday: "Sun",
3 | Monday: "Mon",
4 | Tuesday: "Tue",
5 | Wednesday: "Wed",
6 | Thursday: "Thu",
7 | Friday: "Fri",
8 | Saturday: "Sat"
9 | }
10 |
11 | export const CALENDAR_MONTHS = {
12 | January: "Jan",
13 | February: "Feb",
14 | March: "Mar",
15 | April: "Apr",
16 | May: "May",
17 | June: "Jun",
18 | July: "Jul",
19 | August: "Aug",
20 | September: "Sep",
21 | October: "Oct",
22 | November: "Nov",
23 | December: "Dec"
24 | }
25 |
26 | export const CALENDAR_WEEKS = 6;
27 |
28 | export const CALENDAR_MONTHS_30 = [4, 6, 9, 11];
29 |
30 | export const isDate = date => {
31 | const isDate = Object.prototype.toString.call(date) === '[object Date]';
32 | const isValidDate = date && !Number.isNaN(+date);
33 | return isDate && isValidDate;
34 | }
35 |
36 | export const getDateISO = (date = new Date) => {
37 | return isDate(date)
38 | ? [ date.getFullYear(), date.getMonth() + 1, date.getDate() ]
39 | .map(v => String(v).padStart(2, '0'))
40 | .join('-')
41 | : null;
42 | }
43 |
44 | export const getDateArray = (date = new Date) => {
45 | const [year = null, month = null, day = null] = (getDateISO(date) || '').split('-').map(v => +v);
46 | return [ year, month, day ];
47 | }
48 |
49 | export const getMonthDays = (date = new Date) => {
50 | const [ year, month ] = getDateArray(date);
51 | return month === 2
52 | ? (year % 4 === 0) ? 29 : 28
53 | : (CALENDAR_MONTHS_30.includes(month)) ? 30 : 31;
54 | }
55 |
56 | export const getMonthFirstDay = (date = new Date) => {
57 | return new Date(new Date(+date).setDate(1)).getDay() + 1;
58 | }
59 |
60 | export const getPreviousMonth = (date = new Date) => {
61 | const [ year, month ] = getDateArray(date);
62 | return {
63 | month: month > 1 ? month - 1 : 12,
64 | year: month > 1 ? year : year - 1
65 | }
66 | }
67 |
68 | export const getNextMonth = (date = new Date) => {
69 | const [ year, month ] = getDateArray(date);
70 | return {
71 | month: month < 12 ? month + 1 : 1,
72 | year: month < 12 ? year : year + 1
73 | };
74 | }
75 |
76 | export const dateDiff = (date1, date2 = new Date) => {
77 | return isDate(date1) && isDate(date2)
78 | ? (new Date(+date1).setHours(0, 0, 0, 0)) - (new Date(+date2).setHours(0, 0, 0, 0))
79 | : null;
80 | }
81 |
82 | export const isBeforeDay = (date1, date2) => +dateDiff(date1, date2) < 0
83 |
84 | export const isAfterDay = (date1, date2) => +dateDiff(date1, date2) > 0
85 |
86 | export const isSameDay = (date1, date2) => dateDiff(date1, date2) === 0
87 |
88 | export const isSameMonth = (date1, date2) => {
89 | return isDate(date1) && isDate(date2)
90 | ? new Date(+date1).setDate(1) - new Date(+date2).setDate(1) === 0
91 | : false;
92 | }
93 |
94 | export default (date = new Date) => {
95 | const monthDays = getMonthDays(date);
96 | const monthFirstDay = getMonthFirstDay(date);
97 | const [ year, month ] = getDateArray(date);
98 |
99 | const daysFromPrevMonth = monthFirstDay - 1;
100 | const daysFromNextMonth = (CALENDAR_WEEKS * 7) - (daysFromPrevMonth + monthDays);
101 |
102 | const { month: prevMonth, year: prevMonthYear } = getPreviousMonth(date);
103 | const { month: nextMonth, year: nextMonthYear } = getNextMonth(date);
104 |
105 | const prevMonthDays = getMonthDays(new Date([ prevMonthYear, prevMonth ]));
106 |
107 | const prevMonthDates = [...new Array(daysFromPrevMonth)]
108 | .map((n, index) => [ prevMonthYear, prevMonth, index + 1 + (prevMonthDays - daysFromPrevMonth) ]);
109 |
110 | const thisMonthDates = [...new Array(monthDays)]
111 | .map((n, index) => [ year, month, index + 1 ]);
112 |
113 | const nextMonthDates = [...new Array(daysFromNextMonth)]
114 | .map((n, index) => [ nextMonthYear, nextMonth, index + 1 ]);
115 |
116 | return [ ...prevMonthDates, ...thisMonthDates, ...nextMonthDates ];
117 | }
118 |
--------------------------------------------------------------------------------
/src/index.css:
--------------------------------------------------------------------------------
1 | body {
2 | margin: 0;
3 | padding: 0;
4 | font-family: sans-serif;
5 | }
6 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import 'bootstrap/dist/css/bootstrap.min.css';
2 | import React from 'react';
3 | import ReactDOM from 'react-dom';
4 | import './index.css';
5 | import App from './App';
6 | import registerServiceWorker from './registerServiceWorker';
7 |
8 | ReactDOM.render(, document.getElementById('root'));
9 | registerServiceWorker();
10 |
--------------------------------------------------------------------------------
/src/logo.svg:
--------------------------------------------------------------------------------
1 |
8 |
--------------------------------------------------------------------------------
/src/registerServiceWorker.js:
--------------------------------------------------------------------------------
1 | // In production, we register a service worker to serve assets from local cache.
2 |
3 | // This lets the app load faster on subsequent visits in production, and gives
4 | // it offline capabilities. However, it also means that developers (and users)
5 | // will only see deployed updates on the "N+1" visit to a page, since previously
6 | // cached resources are updated in the background.
7 |
8 | // To learn more about the benefits of this model, read https://goo.gl/KwvDNy.
9 | // This link also includes instructions on opting out of this behavior.
10 |
11 | const isLocalhost = Boolean(
12 | window.location.hostname === 'localhost' ||
13 | // [::1] is the IPv6 localhost address.
14 | window.location.hostname === '[::1]' ||
15 | // 127.0.0.1/8 is considered localhost for IPv4.
16 | window.location.hostname.match(
17 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/
18 | )
19 | );
20 |
21 | export default function register() {
22 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
23 | // The URL constructor is available in all browsers that support SW.
24 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location);
25 | if (publicUrl.origin !== window.location.origin) {
26 | // Our service worker won't work if PUBLIC_URL is on a different origin
27 | // from what our page is served on. This might happen if a CDN is used to
28 | // serve assets; see https://github.com/facebookincubator/create-react-app/issues/2374
29 | return;
30 | }
31 |
32 | window.addEventListener('load', () => {
33 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;
34 |
35 | if (isLocalhost) {
36 | // This is running on localhost. Lets check if a service worker still exists or not.
37 | checkValidServiceWorker(swUrl);
38 |
39 | // Add some additional logging to localhost, pointing developers to the
40 | // service worker/PWA documentation.
41 | navigator.serviceWorker.ready.then(() => {
42 | console.log(
43 | 'This web app is being served cache-first by a service ' +
44 | 'worker. To learn more, visit https://goo.gl/SC7cgQ'
45 | );
46 | });
47 | } else {
48 | // Is not local host. Just register service worker
49 | registerValidSW(swUrl);
50 | }
51 | });
52 | }
53 | }
54 |
55 | function registerValidSW(swUrl) {
56 | navigator.serviceWorker
57 | .register(swUrl)
58 | .then(registration => {
59 | registration.onupdatefound = () => {
60 | const installingWorker = registration.installing;
61 | installingWorker.onstatechange = () => {
62 | if (installingWorker.state === 'installed') {
63 | if (navigator.serviceWorker.controller) {
64 | // At this point, the old content will have been purged and
65 | // the fresh content will have been added to the cache.
66 | // It's the perfect time to display a "New content is
67 | // available; please refresh." message in your web app.
68 | console.log('New content is available; please refresh.');
69 | } else {
70 | // At this point, everything has been precached.
71 | // It's the perfect time to display a
72 | // "Content is cached for offline use." message.
73 | console.log('Content is cached for offline use.');
74 | }
75 | }
76 | };
77 | };
78 | })
79 | .catch(error => {
80 | console.error('Error during service worker registration:', error);
81 | });
82 | }
83 |
84 | function checkValidServiceWorker(swUrl) {
85 | // Check if the service worker can be found. If it can't reload the page.
86 | fetch(swUrl)
87 | .then(response => {
88 | // Ensure service worker exists, and that we really are getting a JS file.
89 | if (
90 | response.status === 404 ||
91 | response.headers.get('content-type').indexOf('javascript') === -1
92 | ) {
93 | // No service worker found. Probably a different app. Reload the page.
94 | navigator.serviceWorker.ready.then(registration => {
95 | registration.unregister().then(() => {
96 | window.location.reload();
97 | });
98 | });
99 | } else {
100 | // Service worker found. Proceed as normal.
101 | registerValidSW(swUrl);
102 | }
103 | })
104 | .catch(() => {
105 | console.log(
106 | 'No internet connection found. App is running in offline mode.'
107 | );
108 | });
109 | }
110 |
111 | export function unregister() {
112 | if ('serviceWorker' in navigator) {
113 | navigator.serviceWorker.ready.then(registration => {
114 | registration.unregister();
115 | });
116 | }
117 | }
118 |
--------------------------------------------------------------------------------