diff --git a/atst/routes/users.py b/atst/routes/users.py
index 5aa720d6..30564196 100644
--- a/atst/routes/users.py
+++ b/atst/routes/users.py
@@ -1,3 +1,4 @@
+import datetime as dt
from flask import Blueprint, render_template, g, request as http_request, redirect
from atst.forms.edit_user import EditUserForm
from atst.domain.users import Users
@@ -16,7 +17,14 @@ def user():
if next_:
flash("user_must_complete_profile")
- return render_template("user/edit.html", next=next_, form=form, user=user)
+ return render_template(
+ "user/edit.html",
+ next=next_,
+ form=form,
+ user=user,
+ mindate=(dt.datetime.now() - dt.timedelta(days=365)),
+ maxdate=dt.datetime.now(),
+ )
@bp.route("/user", methods=["POST"])
diff --git a/js/components/__tests__/date_selector.test.js b/js/components/__tests__/date_selector.test.js
new file mode 100644
index 00000000..660292c8
--- /dev/null
+++ b/js/components/__tests__/date_selector.test.js
@@ -0,0 +1,172 @@
+import Vue from 'vue'
+
+import DateSelector from '../date_selector'
+
+describe('DateSelector', () => {
+ const component = new Vue(DateSelector).$mount()
+
+ describe('isDateValid', () => {
+ it('returns true when a valid date', () => {
+ component.day = 4
+ component.month = 8
+ component.year = 1776
+
+ expect(component.isDateValid).toEqual(true)
+ })
+
+ it('returns false when an invalid date', () => {
+ component.day = 32
+ component.month = 13
+ component.year = 2019
+
+ expect(component.isDateValid).toEqual(false)
+ })
+
+ it('returns false when parts of the date are missing', () => {
+ component.day = 31
+ component.year = 2019
+
+ expect(component.isDateValid).toEqual(false)
+ })
+ })
+
+ describe('daysMaxCalculation', () => {
+ it('calculates correctly for each month', () => {
+ let months = {
+ '1': 31,
+ '2': 29,
+ '3': 31,
+ '4': 30,
+ '5': 31,
+ '6': 30,
+ '7': 31,
+ '8': 31,
+ '9': 30,
+ '10': 31,
+ '11': 30,
+ '12': 31,
+ }
+
+ for (var month in months) {
+ component.month = parseInt(month)
+ expect(component.daysMaxCalculation).toEqual(months[month])
+ }
+ })
+ })
+
+ describe('isMonthValid', () => {
+ it('returns false when over 12', () => {
+ component.month = 13
+ expect(component.isMonthValid).toEqual(false)
+ })
+
+ it('returns true when between 1 and 12', () => {
+ component.month = 3
+ expect(component.isMonthValid).toEqual(true)
+ })
+
+ it('returns false when null', () => {
+ component.month = null
+ expect(component.isMonthValid).toEqual(false)
+ })
+ })
+
+ describe('isDayValid', () => {
+ it('returns true when 31 and no month', () => {
+ component.day = 31
+ component.month = null
+ expect(component.isDayValid).toEqual(true)
+ })
+
+ it('returns false when 31 and in February', () => {
+ component.day = 31
+ component.month = 2
+ expect(component.isDayValid).toEqual(false)
+ })
+
+ it('returns false when 32 and no month', () => {
+ component.day = 32
+ component.month = null
+ expect(component.isDayValid).toEqual(false)
+ })
+
+ it('returns false when null', () => {
+ component.day = null
+ expect(component.isDayValid).toEqual(false)
+ })
+ })
+
+ describe('isWithinDateRange', () => {
+ beforeEach(() => {
+ component.day = 24
+ component.month = 1
+ component.year = 2019
+ })
+
+ it('always returns true when no mindate or maxdate', () => {
+ expect(component.isWithinDateRange).toEqual(true)
+ })
+
+ it('handles mindate only', () => {
+ component.mindate = '2019-01-25'
+ expect(component.isWithinDateRange).toEqual(false)
+
+ component.mindate = '2014-01-25'
+ expect(component.isWithinDateRange).toEqual(true)
+ })
+
+ it('handles maxdate only', () => {
+ component.maxdate = '2019-01-25'
+ expect(component.isWithinDateRange).toEqual(true)
+
+ component.maxdate = '2014-01-25'
+ expect(component.isWithinDateRange).toEqual(false)
+ })
+
+ it('handles mindate and maxdate', () => {
+ component.mindate = '2019-01-25'
+ component.maxdate = '2019-02-28'
+ expect(component.isWithinDateRange).toEqual(false)
+
+ component.mindate = '2013-01-25'
+ component.maxdate = '2016-02-28'
+ expect(component.isWithinDateRange).toEqual(false)
+
+ component.mindate = '2014-01-25'
+ component.maxdate = '2020-02-28'
+ expect(component.isWithinDateRange).toEqual(true)
+ })
+ })
+
+ describe('isYearValid', () => {
+ it('returns false if year is null', () => {
+ component.year = null
+ expect(component.isYearValid).toEqual(false)
+ })
+
+ it('returns true if year is present', () => {
+ component.year = new Date().getFullYear()
+ expect(component.isYearValid).toEqual(true)
+ })
+ })
+
+ describe('formattedDate', () => {
+ it('returns null if not all parts are present', () => {
+ component.day = null
+ component.month = 1
+ component.year = 1988
+
+ expect(component.formattedDate).toBeNull()
+ })
+
+ it('joins date components into a JS date', () => {
+ component.mindate = null
+ component.maxdate = null
+ component.day = 22
+ component.month = 1
+ component.year = 1988
+
+ expect(component.formattedDate).toEqual('01/22/1988')
+ })
+ })
+})
diff --git a/js/components/date_selector.js b/js/components/date_selector.js
new file mode 100644
index 00000000..3093ce42
--- /dev/null
+++ b/js/components/date_selector.js
@@ -0,0 +1,101 @@
+import Vue from 'vue'
+
+var paddedNumber = function(number) {
+ if ((number + '').length === 1) {
+ return `0${number}`
+ } else {
+ return number
+ }
+}
+
+export default Vue.component('date-selector', {
+ props: ['initialday', 'initialmonth', 'initialyear', 'mindate', 'maxdate'],
+
+ data: function() {
+ return {
+ day: this.initialday,
+ month: this.initialmonth,
+ year: this.initialyear,
+ }
+ },
+
+ computed: {
+ formattedDate: function() {
+ if (!this.isDateValid) {
+ return null
+ }
+
+ let day = paddedNumber(this.day)
+ let month = paddedNumber(this.month)
+
+ return `${month}/${day}/${this.year}`
+ },
+
+ isMonthValid: function() {
+ var _month = parseInt(this.month)
+
+ return _month >= 0 && _month <= 12
+ },
+
+ isDayValid: function() {
+ var _day = parseInt(this.day)
+
+ return _day >= 0 && _day <= this.daysMaxCalculation
+ },
+
+ isYearValid: function() {
+ return parseInt(this.year) >= 1
+ },
+
+ isWithinDateRange: function() {
+ let _mindate = this.mindate ? Date.parse(this.mindate) : null
+ let _maxdate = this.maxdate ? Date.parse(this.maxdate) : null
+ let _dateTimestamp = Date.UTC(this.year, this.month - 1, this.day)
+
+ if (_mindate !== null && _mindate >= _dateTimestamp) {
+ return false
+ }
+
+ if (_maxdate !== null && _maxdate <= _dateTimestamp) {
+ return false
+ }
+
+ return true
+ },
+
+ isDateValid: function() {
+ return (
+ this.day &&
+ this.month &&
+ this.year &&
+ this.isDayValid &&
+ this.isMonthValid &&
+ this.isYearValid &&
+ this.isWithinDateRange
+ )
+ },
+
+ daysMaxCalculation: function() {
+ switch (parseInt(this.month)) {
+ case 2: // February
+ return 29
+ break
+
+ case 4: // April
+ case 6: // June
+ case 9: // September
+ case 11: // November
+ return 30
+ break
+
+ default:
+ // All other months, or null, go with 31
+ return 31
+ }
+ },
+ },
+
+ render: function(createElement) {
+ return createElement('p', 'Please implement inline-template')
+ },
+})
diff --git a/js/index.js b/js/index.js
index f5692103..b12de788 100644
--- a/js/index.js
+++ b/js/index.js
@@ -30,6 +30,7 @@ import LocalDatetime from './components/local_datetime'
import RequestsList from './components/requests_list'
import ConfirmationPopover from './components/confirmation_popover'
import { isNotInVerticalViewport } from './lib/viewport'
+import DateSelector from './components/date_selector'
Vue.config.productionTip = false
@@ -62,6 +63,7 @@ const app = new Vue({
RequestsList,
ConfirmationPopover,
funding,
+ DateSelector,
},
mounted: function() {
diff --git a/styles/components/_forms.scss b/styles/components/_forms.scss
index 4b4eb9cc..546cd960 100644
--- a/styles/components/_forms.scss
+++ b/styles/components/_forms.scss
@@ -116,3 +116,25 @@
}
}
+.date-picker {
+ display: inline-block;
+ margin-top: 10px;
+ width: 100%;
+
+ label {
+ font-weight: 400;
+ }
+
+ input[type="number"]::-webkit-inner-spin-button {
+ appearance: none;
+ }
+
+ input.usa-input-error {
+ right: 0;
+ }
+
+ .usa-form-group-date-ok {
+ padding-top: 30px;
+ }
+}
+
diff --git a/templates/components/date_picker.html b/templates/components/date_picker.html
new file mode 100644
index 00000000..5e4376a2
--- /dev/null
+++ b/templates/components/date_picker.html
@@ -0,0 +1,70 @@
+{% from "components/icon.html" import Icon %}
+
+{% macro DatePicker(field, mindate=None, maxdate=None) -%}
+
+