From 41f6df6aae47e1d72761820d618c4f870364fbc6 Mon Sep 17 00:00:00 2001 From: Amit Yathirajadasan Date: Mon, 11 Dec 2023 14:59:15 -0800 Subject: [PATCH 1/7] feat: date picker component --- CHANGELOG.md | 4 + README.md | 3 + src/components/date-picker/README.md | 26 ++++++ src/components/date-picker/index.js | 105 +++++++++++++++++++++++++ src/components/date-picker/styles.scss | 59 ++++++++++++++ src/components/date-picker/utils.js | 73 +++++++++++++++++ src/components/index.js | 1 + 7 files changed, 271 insertions(+) create mode 100644 src/components/date-picker/README.md create mode 100644 src/components/date-picker/index.js create mode 100644 src/components/date-picker/styles.scss create mode 100644 src/components/date-picker/utils.js diff --git a/CHANGELOG.md b/CHANGELOG.md index 7b449a2..642b1a0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- DatePicker component by [@amit-y](https://github.com/amit-y) + ## [1.16.0] - 2023-08-09 ### Added diff --git a/README.md b/README.md index bf71ba5..0524cbf 100644 --- a/README.md +++ b/README.md @@ -58,6 +58,9 @@ A component that renders a progress bar. ### [FilterBar](src/components/filter-bar) Component that allows a user to filter options. +### [DatePicker](src/components/date-picker) +Component that displays a calendar to select a date + ## Utilities ### [timeRangeToNrql](src/utils/time-range-to-nrql/) diff --git a/src/components/date-picker/README.md b/src/components/date-picker/README.md new file mode 100644 index 0000000..64d51ae --- /dev/null +++ b/src/components/date-picker/README.md @@ -0,0 +1,26 @@ +# DatePicker + +The `DatePicker` component displays a calendar to select a date. + +## Usage + +To use the component, simply import and use: + +```jsx +import React, { useState } from 'react'; +import { DatePicker } from '@newrelic/nr-labs-components'; + +function MyComponent() { + const [selectedDate, setSelectedDate] = useState(new Date()); + + return ( +
+ +
+ ); +} +``` +### Props + +- date (date): The default date +- onChange (function): A function that is called when the user selects a date diff --git a/src/components/date-picker/index.js b/src/components/date-picker/index.js new file mode 100644 index 0000000..295df6e --- /dev/null +++ b/src/components/date-picker/index.js @@ -0,0 +1,105 @@ +import React, { useEffect, useState } from 'react'; +import PropTypes from 'prop-types'; + +import { Icon, Popover, PopoverTrigger, PopoverBody, TextField } from 'nr1'; + +import { + formattedDateField, + formattedMonthYear, + firstDayOfMonth, + lastDateInMonth, + extractDateParts, + afterToday, + selectedDate, + daysOfWeek, +} from './utils'; + +import styles from './styles.scss'; + +const DatePicker = ({ date, onChange }) => { + const [opened, setOpened] = useState(false); + const [current, setCurrent] = useState(extractDateParts(new Date())); + + useEffect(() => { + if (!date || !(date instanceof Date)) return; + setCurrent(extractDateParts(date)); + }, [date]); + + const prevMonth = () => { + const prevMo = new Date(current.yr, current.mo - 1); + setCurrent(extractDateParts(prevMo)); + }; + + const nextMonth = () => { + const nextMo = new Date(current.yr, current.mo + 1); + setCurrent(extractDateParts(nextMo)); + }; + + const isDateInCurrentMonth = (d = new Date()) => + d.getFullYear() === current.yr && d.getMonth() === current.mo; + + const clickHandler = (dt) => { + if (afterToday(current, dt + 1) || !onChange) return; + onChange(new Date(current.yr, current.mo, dt + 1)); + setOpened(false); + }; + + const changeHandler = (_, o) => setOpened(o); + + return ( + + + + + +
+
+ +
+
+ {formattedMonthYear(new Date(current.yr, current.mo))} +
+
+ +
+ {daysOfWeek().map(({ long, short }) => ( +
+ {short} +
+ ))} + {Array.from({ length: firstDayOfMonth(current) }, (_, i) => ( +
+ ))} + {Array.from({ length: lastDateInMonth(current) }, (_, i) => ( +
clickHandler(i)} + > + {i + 1} +
+ ))} +
+ + + ); +}; + +DatePicker.propTypes = { + date: PropTypes.instanceOf(Date), + onChange: PropTypes.func, +}; + +export default DatePicker; diff --git a/src/components/date-picker/styles.scss b/src/components/date-picker/styles.scss new file mode 100644 index 0000000..ae4523c --- /dev/null +++ b/src/components/date-picker/styles.scss @@ -0,0 +1,59 @@ +.calendar { + padding: 20px; + display: grid; + grid-template-columns: repeat(7, 1fr); +} + +.cell { + display: inline-block; + width: 32px; + height: 30px; + line-height: 30px; + text-align: center; + font-size: 12px; + font-weight: 600; + color: #293338; + user-select: none; + + &.day { + font-size: 11px; + } + + &.mo-yr { + font-size: 14px; + grid-column: 2 / 7; + white-space: nowrap; + justify-self: center; + width: 100%; + } + + &.prev, + &.next, + &.date { + cursor: pointer; + } + + &.selected { + border-radius: 3px; + background-color: #293338; + color: #fafbfb; + } + + &.disabled { + color: #9ea5a9; + font-weight: 400; + cursor: auto; + } + + &.date { + &:not(.selected):not(.disabled) { + &:hover { + color: #0c74df; + } + } + } + + abbr { + text-decoration: none; + } +} diff --git a/src/components/date-picker/utils.js b/src/components/date-picker/utils.js new file mode 100644 index 0000000..0b6d7cb --- /dev/null +++ b/src/components/date-picker/utils.js @@ -0,0 +1,73 @@ +const dateFieldFormatter = new Intl.DateTimeFormat('default', { + year: 'numeric', + month: 'short', + day: 'numeric', +}); + +const formattedDateField = (dt) => + dt && dt instanceof Date ? dateFieldFormatter.format(dt) : ''; + +const monthYearFormatter = new Intl.DateTimeFormat('default', { + year: 'numeric', + month: 'long', +}); + +const formattedMonthYear = (dt) => + dt && dt instanceof Date ? monthYearFormatter.format(dt) : ''; + +const firstDayOfMonth = (d) => new Date(d.yr, d.mo).getDay(); + +const lastDateInMonth = (d) => new Date(d.yr, d.mo + 1, 0).getDate(); + +const extractDateParts = (d) => ({ + yr: d.getFullYear(), + mo: d.getMonth(), + dt: d.getDate(), +}); + +const afterToday = (cur, d) => { + const today = new Date(); + return ( + cur.yr === today.getFullYear() && + cur.mo === today.getMonth() && + d > today.getDate() + ); +}; + +const selectedDate = (index, cur, dt) => { + if (!dt || !(dt instanceof Date)) return false; + return ( + dt.getFullYear() === cur.yr && + dt.getMonth() === cur.mo && + dt.getDate() === index + 1 + ); +}; + +const daysOfWeek = () => { + const now = Date.now(); + const millisecondsInDay = 24 * 60 * 60 * 1000; + const startDayInMs = now - new Date().getDay() * millisecondsInDay; + const formats = ['long', 'short']; + const formatters = formats.map( + (fmt) => new Intl.DateTimeFormat('default', { weekday: fmt }) + ); + + return Array.from({ length: 7 }).map((_, i) => { + const d = new Date(startDayInMs + i * millisecondsInDay); + return formats.reduce( + (acc, fmt, idx) => ({ ...acc, [fmt]: formatters[idx].format(d) }), + {} + ); + }); +}; + +export { + formattedDateField, + formattedMonthYear, + firstDayOfMonth, + lastDateInMonth, + extractDateParts, + afterToday, + selectedDate, + daysOfWeek, +}; diff --git a/src/components/index.js b/src/components/index.js index 00c1634..312e90f 100644 --- a/src/components/index.js +++ b/src/components/index.js @@ -8,3 +8,4 @@ export { default as StatusIcon } from './status-icon'; export { default as StatusIconsLayout } from './status-icons-layout'; export { default as ProgressBar } from './progress-bar'; export { default as FilterBar } from './filter-bar'; +export { default as DatePicker } from './date-picker'; From 1b8b6fe540e27c51281e37c1ad7c5259cfecfeb6 Mon Sep 17 00:00:00 2001 From: Amit Yathirajadasan Date: Mon, 11 Dec 2023 23:36:28 -0800 Subject: [PATCH 2/7] refactor: convert days of week to constant from function --- src/components/date-picker/index.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/components/date-picker/index.js b/src/components/date-picker/index.js index 295df6e..f6c4f73 100644 --- a/src/components/date-picker/index.js +++ b/src/components/date-picker/index.js @@ -14,6 +14,8 @@ import { daysOfWeek, } from './utils'; +const DAYS_OF_WEEK = daysOfWeek(); + import styles from './styles.scss'; const DatePicker = ({ date, onChange }) => { @@ -72,7 +74,7 @@ const DatePicker = ({ date, onChange }) => { >
- {daysOfWeek().map(({ long, short }) => ( + {DAYS_OF_WEEK.map(({ long, short }) => (
{short}
From a2274454f368a2657160ef519dbeda684a96e8a2 Mon Sep 17 00:00:00 2001 From: Amit Yathirajadasan Date: Wed, 13 Dec 2023 14:07:19 -0800 Subject: [PATCH 3/7] feat: validFrom prop for date picker to disallow past dates --- src/components/date-picker/index.js | 13 +++++++++---- src/components/date-picker/utils.js | 16 +++++++++++++++- 2 files changed, 24 insertions(+), 5 deletions(-) diff --git a/src/components/date-picker/index.js b/src/components/date-picker/index.js index f6c4f73..07dcacb 100644 --- a/src/components/date-picker/index.js +++ b/src/components/date-picker/index.js @@ -9,16 +9,16 @@ import { firstDayOfMonth, lastDateInMonth, extractDateParts, - afterToday, selectedDate, daysOfWeek, + isSelectableDate, } from './utils'; const DAYS_OF_WEEK = daysOfWeek(); import styles from './styles.scss'; -const DatePicker = ({ date, onChange }) => { +const DatePicker = ({ date, onChange, validFrom }) => { const [opened, setOpened] = useState(false); const [current, setCurrent] = useState(extractDateParts(new Date())); @@ -41,7 +41,7 @@ const DatePicker = ({ date, onChange }) => { d.getFullYear() === current.yr && d.getMonth() === current.mo; const clickHandler = (dt) => { - if (afterToday(current, dt + 1) || !onChange) return; + if (!isSelectableDate(current, dt + 1, validFrom) || !onChange) return; onChange(new Date(current.yr, current.mo, dt + 1)); setOpened(false); }; @@ -87,7 +87,11 @@ const DatePicker = ({ date, onChange }) => { key={i} className={`${styles.cell} ${styles.date} ${ selectedDate(i, current, date) ? styles.selected : '' - } ${afterToday(current, i + 1) ? styles.disabled : ''}`} + } ${ + !isSelectableDate(current, i + 1, validFrom) + ? styles.disabled + : '' + }`} onClick={() => clickHandler(i)} > {i + 1} @@ -101,6 +105,7 @@ const DatePicker = ({ date, onChange }) => { DatePicker.propTypes = { date: PropTypes.instanceOf(Date), + validFrom: PropTypes.instanceOf(Date), onChange: PropTypes.func, }; diff --git a/src/components/date-picker/utils.js b/src/components/date-picker/utils.js index 0b6d7cb..696ed7b 100644 --- a/src/components/date-picker/utils.js +++ b/src/components/date-picker/utils.js @@ -34,6 +34,20 @@ const afterToday = (cur, d) => { ); }; +const isSelectableDate = (cur, d, validFrom) => { + let isValid = true; + if (validFrom && validFrom instanceof Date) { + const validDate = new Date( + validFrom.getFullYear(), + validFrom.getMonth(), + validFrom.getDate() + ); + const curDt = new Date(cur.yr, cur.mo, d); + isValid = curDt >= validDate; + } + return isValid && !afterToday(cur, d); +}; + const selectedDate = (index, cur, dt) => { if (!dt || !(dt instanceof Date)) return false; return ( @@ -67,7 +81,7 @@ export { firstDayOfMonth, lastDateInMonth, extractDateParts, - afterToday, selectedDate, daysOfWeek, + isSelectableDate, }; From 758eb52c496396ef8c93bc55a98f560f98b3e921 Mon Sep 17 00:00:00 2001 From: Amit Yathirajadasan Date: Wed, 13 Dec 2023 14:08:20 -0800 Subject: [PATCH 4/7] refactor: update key prop in arrays to make them unique --- src/components/date-picker/index.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/components/date-picker/index.js b/src/components/date-picker/index.js index 07dcacb..256b683 100644 --- a/src/components/date-picker/index.js +++ b/src/components/date-picker/index.js @@ -80,11 +80,14 @@ const DatePicker = ({ date, onChange, validFrom }) => { ))} {Array.from({ length: firstDayOfMonth(current) }, (_, i) => ( -
+
))} {Array.from({ length: lastDateInMonth(current) }, (_, i) => (
Date: Wed, 13 Dec 2023 14:14:16 -0800 Subject: [PATCH 5/7] fix: updates date only in onChange --- src/components/date-picker/index.js | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/components/date-picker/index.js b/src/components/date-picker/index.js index 256b683..be014f6 100644 --- a/src/components/date-picker/index.js +++ b/src/components/date-picker/index.js @@ -42,7 +42,13 @@ const DatePicker = ({ date, onChange, validFrom }) => { const clickHandler = (dt) => { if (!isSelectableDate(current, dt + 1, validFrom) || !onChange) return; - onChange(new Date(current.yr, current.mo, dt + 1)); + + const d = date instanceof Date ? new Date(date.getTime()) : new Date(); + d.setFullYear(current.yr); + d.setMonth(current.mo); + d.setDate(dt + 1); + onChange(d); + setOpened(false); }; From 175ff69241978b9b385028f3f8cd636aee2617b9 Mon Sep 17 00:00:00 2001 From: nr-opensource-bot Date: Thu, 14 Dec 2023 05:53:15 +0000 Subject: [PATCH 6/7] chore(release): 1.17.0 [skip ci] # [1.17.0](https://github.com/newrelic/nr-labs-components/compare/v1.16.0...v1.17.0) (2023-12-14) ### Bug Fixes * updates date only in onChange ([980329d](https://github.com/newrelic/nr-labs-components/commit/980329d2f23ebf5e353e231648c82ffb2e6751c0)) ### Features * date picker component ([41f6df6](https://github.com/newrelic/nr-labs-components/commit/41f6df6aae47e1d72761820d618c4f870364fbc6)) * validFrom prop for date picker to disallow past dates ([a227445](https://github.com/newrelic/nr-labs-components/commit/a2274454f368a2657160ef519dbeda684a96e8a2)) --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 06a72be..097ab7b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@newrelic/nr-labs-components", - "version": "1.16.0", + "version": "1.17.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@newrelic/nr-labs-components", - "version": "1.16.0", + "version": "1.17.0", "license": "Apache-2.0", "dependencies": { "dayjs": "^1.11.7" diff --git a/package.json b/package.json index 8949af4..3ccea2c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@newrelic/nr-labs-components", - "version": "1.16.0", + "version": "1.17.0", "description": "New Relic Labs components", "main": "dist/index.js", "module": "dist/index.es.js", From 039ff0cfb8b344adc89efc65b7bea45a1f8135bf Mon Sep 17 00:00:00 2001 From: Amit Yathirajadasan Date: Wed, 13 Dec 2023 22:09:54 -0800 Subject: [PATCH 7/7] chore: update changelog --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 642b1a0..47febd7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [1.17.0] - 2023-12-14 + ### Added - DatePicker component by [@amit-y](https://github.com/amit-y)