diff --git a/frontend/package-lock.json b/frontend/package-lock.json index aa16d2a6..2f17113e 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -8608,6 +8608,19 @@ "minimist": "^1.2.5" } }, + "moment": { + "version": "2.27.0", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.27.0.tgz", + "integrity": "sha512-al0MUK7cpIcglMv3YF13qSgdAIqxHTO7brRtaz3DlSULbqfazqkc5kEjNrLDOM7fsjshoFIihnU8snrP7zUvhQ==" + }, + "moment-timezone": { + "version": "0.5.31", + "resolved": "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.5.31.tgz", + "integrity": "sha512-+GgHNg8xRhMXfEbv81iDtrVeTcWt0kWmTEY1XQK14dICTXnWJnT0dxdlPspwqF3keKMVPXwayEsk1DI0AA/jdA==", + "requires": { + "moment": ">= 2.9.0" + } + }, "move-concurrently": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/move-concurrently/-/move-concurrently-1.0.1.tgz", diff --git a/frontend/package.json b/frontend/package.json index 61b59421..7ba15ffe 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -8,7 +8,8 @@ "@testing-library/user-event": "^7.2.1", "bootstrap": "^4.5.0", "firebase": "^7.15.5", - "history": "^5.0.0", + "moment-timezone": "^0.5.31", + "history": "^5.0.0" "react": "^16.13.1", "react-bootstrap": "1.0.1", "react-dom": "^16.13.1", diff --git a/frontend/src/components/Utils/time.js b/frontend/src/components/Utils/time.js index b633080d..ff6679dc 100644 --- a/frontend/src/components/Utils/time.js +++ b/frontend/src/components/Utils/time.js @@ -1,3 +1,5 @@ +import * as moment from 'moment-timezone'; +import { countryCodes } from '../../constants/countries.js'; import * as firebase from 'firebase/app'; /** @@ -6,7 +8,7 @@ import * as firebase from 'firebase/app'; * * @param {int} msTimestamp Timestamp in milliseconds of desired date. * @param {string} timezone Timezone in which to convert. - * @returns {string} Time formatted into desired pretty string. + * @return {string} Time formatted into desired pretty string. */ export function timestampToTimeFormatted(msTimestamp, timezone = 'America/New_York') { const date = new Date(msTimestamp); @@ -24,7 +26,7 @@ export function timestampToTimeFormatted(msTimestamp, timezone = 'America/New_Yo * * @param {int} msTimestamp Timestamp in milliseconds of desired date. * @param {string} timezone Timezone in which to convert. - * @returns {string} Time formatted into desired pretty string. + * @return {string} Time formatted into desired pretty string. */ export function timestampToDateFormatted(msTimestamp, timezone='America/New_York') { const date = new Date(msTimestamp); @@ -44,7 +46,7 @@ export function timestampToDateFormatted(msTimestamp, timezone='America/New_York * * @param {int} msTimestamp Timestamp in milliseconds of desired date. * @param {string} timezone Timezone in which to convert. - * @returns {string} Time formatted into desired pretty string. + * @return {string} Time formatted into desired pretty string. */ export function timestampToFormatted(msTimestamp, timezone = 'America/New_York') { let date = new Date(msTimestamp); @@ -85,3 +87,22 @@ export function getTimestampFromDateString(dateStr) { export function timestampToISOString(timestamp) { return timestamp.toDate().toISOString().substring(0,10); } + +/** + * Returns all the time zones in a country (in displayable format). + * + * @param {string} countryName The name of the country for which to get the time zones. + * @return {string[]} The list of time zones in the provided country. + */ +export function timezonesForCountry(countryName) { + let zones; + try { + const countryCode = countryCodes[countryName]; + zones = moment.tz.zonesForCountry(countryCode); + } catch (e) { + zones = moment.tz.names(); // List of all timezones. + } + return zones.map(e => { + return e.replace(/[_]/g, ' '); + }); +} \ No newline at end of file diff --git a/frontend/src/components/Utils/time.test.js b/frontend/src/components/Utils/time.test.js index b02a9102..2ad87fbd 100644 --- a/frontend/src/components/Utils/time.test.js +++ b/frontend/src/components/Utils/time.test.js @@ -6,48 +6,51 @@ import * as utils from './time.js'; const TZ_CHICAGO = 'America/Chicago'; const TZ_SINGAPORE = 'Asia/Singapore'; +const ALLTZS = ["Africa/Abidjan", "Africa/Accra", "Africa/Addis Ababa", "Africa/Algiers", "Africa/Asmara", "Africa/Asmera", "Africa/Bamako", "Africa/Bangui", "Africa/Banjul", "Africa/Bissau", "Africa/Blantyre", "Africa/Brazzaville", "Africa/Bujumbura", "Africa/Cairo", "Africa/Casablanca", "Africa/Ceuta", "Africa/Conakry", "Africa/Dakar", "Africa/Dar es Salaam", "Africa/Djibouti", "Africa/Douala", "Africa/El Aaiun", "Africa/Freetown", "Africa/Gaborone", "Africa/Harare", "Africa/Johannesburg", "Africa/Juba", "Africa/Kampala", "Africa/Khartoum", "Africa/Kigali", "Africa/Kinshasa", "Africa/Lagos", "Africa/Libreville", "Africa/Lome", "Africa/Luanda", "Africa/Lubumbashi", "Africa/Lusaka", "Africa/Malabo", "Africa/Maputo", "Africa/Maseru", "Africa/Mbabane", "Africa/Mogadishu", "Africa/Monrovia", "Africa/Nairobi", "Africa/Ndjamena", "Africa/Niamey", "Africa/Nouakchott", "Africa/Ouagadougou", "Africa/Porto-Novo", "Africa/Sao Tome", "Africa/Timbuktu", "Africa/Tripoli", "Africa/Tunis", "Africa/Windhoek", "America/Adak", "America/Anchorage", "America/Anguilla", "America/Antigua", "America/Araguaina", "America/Argentina/Buenos Aires", "America/Argentina/Catamarca", "America/Argentina/ComodRivadavia", "America/Argentina/Cordoba", "America/Argentina/Jujuy", "America/Argentina/La Rioja", "America/Argentina/Mendoza", "America/Argentina/Rio Gallegos", "America/Argentina/Salta", "America/Argentina/San Juan", "America/Argentina/San Luis", "America/Argentina/Tucuman", "America/Argentina/Ushuaia", "America/Aruba", "America/Asuncion", "America/Atikokan", "America/Atka", "America/Bahia", "America/Bahia Banderas", "America/Barbados", "America/Belem", "America/Belize", "America/Blanc-Sablon", "America/Boa Vista", "America/Bogota", "America/Boise", "America/Buenos Aires", "America/Cambridge Bay", "America/Campo Grande", "America/Cancun", "America/Caracas", "America/Catamarca", "America/Cayenne", "America/Cayman", "America/Chicago", "America/Chihuahua", "America/Coral Harbour", "America/Cordoba", "America/Costa Rica", "America/Creston", "America/Cuiaba", "America/Curacao", "America/Danmarkshavn", "America/Dawson", "America/Dawson Creek", "America/Denver", "America/Detroit", "America/Dominica", "America/Edmonton", "America/Eirunepe", "America/El Salvador", "America/Ensenada", "America/Fort Nelson", "America/Fort Wayne", "America/Fortaleza", "America/Glace Bay", "America/Godthab", "America/Goose Bay", "America/Grand Turk", "America/Grenada", "America/Guadeloupe", "America/Guatemala", "America/Guayaquil", "America/Guyana", "America/Halifax", "America/Havana", "America/Hermosillo", "America/Indiana/Indianapolis", "America/Indiana/Knox", "America/Indiana/Marengo", "America/Indiana/Petersburg", "America/Indiana/Tell City", "America/Indiana/Vevay", "America/Indiana/Vincennes", "America/Indiana/Winamac", "America/Indianapolis", "America/Inuvik", "America/Iqaluit", "America/Jamaica", "America/Jujuy", "America/Juneau", "America/Kentucky/Louisville", "America/Kentucky/Monticello", "America/Knox IN", "America/Kralendijk", "America/La Paz", "America/Lima", "America/Los Angeles", "America/Louisville", "America/Lower Princes", "America/Maceio", "America/Managua", "America/Manaus", "America/Marigot", "America/Martinique", "America/Matamoros", "America/Mazatlan", "America/Mendoza", "America/Menominee", "America/Merida", "America/Metlakatla", "America/Mexico City", "America/Miquelon", "America/Moncton", "America/Monterrey", "America/Montevideo", "America/Montreal", "America/Montserrat", "America/Nassau", "America/New York", "America/Nipigon", "America/Nome", "America/Noronha", "America/North Dakota/Beulah", "America/North Dakota/Center", "America/North Dakota/New Salem", "America/Nuuk", "America/Ojinaga", "America/Panama", "America/Pangnirtung", "America/Paramaribo", "America/Phoenix", "America/Port-au-Prince", "America/Port of Spain", "America/Porto Acre", "America/Porto Velho", "America/Puerto Rico", "America/Punta Arenas", "America/Rainy River", "America/Rankin Inlet", "America/Recife", "America/Regina", "America/Resolute", "America/Rio Branco", "America/Rosario", "America/Santa Isabel", "America/Santarem", "America/Santiago", "America/Santo Domingo", "America/Sao Paulo", "America/Scoresbysund", "America/Shiprock", "America/Sitka", "America/St Barthelemy", "America/St Johns", "America/St Kitts", "America/St Lucia", "America/St Thomas", "America/St Vincent", "America/Swift Current", "America/Tegucigalpa", "America/Thule", "America/Thunder Bay", "America/Tijuana", "America/Toronto", "America/Tortola", "America/Vancouver", "America/Virgin", "America/Whitehorse", "America/Winnipeg", "America/Yakutat", "America/Yellowknife", "Antarctica/Casey", "Antarctica/Davis", "Antarctica/DumontDUrville", "Antarctica/Macquarie", "Antarctica/Mawson", "Antarctica/McMurdo", "Antarctica/Palmer", "Antarctica/Rothera", "Antarctica/South Pole", "Antarctica/Syowa", "Antarctica/Troll", "Antarctica/Vostok", "Arctic/Longyearbyen", "Asia/Aden", "Asia/Almaty", "Asia/Amman", "Asia/Anadyr", "Asia/Aqtau", "Asia/Aqtobe", "Asia/Ashgabat", "Asia/Ashkhabad", "Asia/Atyrau", "Asia/Baghdad", "Asia/Bahrain", "Asia/Baku", "Asia/Bangkok", "Asia/Barnaul", "Asia/Beirut", "Asia/Bishkek", "Asia/Brunei", "Asia/Calcutta", "Asia/Chita", "Asia/Choibalsan", "Asia/Chongqing", "Asia/Chungking", "Asia/Colombo", "Asia/Dacca", "Asia/Damascus", "Asia/Dhaka", "Asia/Dili", "Asia/Dubai", "Asia/Dushanbe", "Asia/Famagusta", "Asia/Gaza", "Asia/Harbin", "Asia/Hebron", "Asia/Ho Chi Minh", "Asia/Hong Kong", "Asia/Hovd", "Asia/Irkutsk", "Asia/Istanbul", "Asia/Jakarta", "Asia/Jayapura", "Asia/Jerusalem", "Asia/Kabul", "Asia/Kamchatka", "Asia/Karachi", "Asia/Kashgar", "Asia/Kathmandu", "Asia/Katmandu", "Asia/Khandyga", "Asia/Kolkata", "Asia/Krasnoyarsk", "Asia/Kuala Lumpur", "Asia/Kuching", "Asia/Kuwait", "Asia/Macao", "Asia/Macau", "Asia/Magadan", "Asia/Makassar", "Asia/Manila", "Asia/Muscat", "Asia/Nicosia", "Asia/Novokuznetsk", "Asia/Novosibirsk", "Asia/Omsk", "Asia/Oral", "Asia/Phnom Penh", "Asia/Pontianak", "Asia/Pyongyang", "Asia/Qatar", "Asia/Qostanay", "Asia/Qyzylorda", "Asia/Rangoon", "Asia/Riyadh", "Asia/Saigon", "Asia/Sakhalin", "Asia/Samarkand", "Asia/Seoul", "Asia/Shanghai", "Asia/Singapore", "Asia/Srednekolymsk", "Asia/Taipei", "Asia/Tashkent", "Asia/Tbilisi", "Asia/Tehran", "Asia/Tel Aviv", "Asia/Thimbu", "Asia/Thimphu", "Asia/Tokyo", "Asia/Tomsk", "Asia/Ujung Pandang", "Asia/Ulaanbaatar", "Asia/Ulan Bator", "Asia/Urumqi", "Asia/Ust-Nera", "Asia/Vientiane", "Asia/Vladivostok", "Asia/Yakutsk", "Asia/Yangon", "Asia/Yekaterinburg", "Asia/Yerevan", "Atlantic/Azores", "Atlantic/Bermuda", "Atlantic/Canary", "Atlantic/Cape Verde", "Atlantic/Faeroe", "Atlantic/Faroe", "Atlantic/Jan Mayen", "Atlantic/Madeira", "Atlantic/Reykjavik", "Atlantic/South Georgia", "Atlantic/St Helena", "Atlantic/Stanley", "Australia/ACT", "Australia/Adelaide", "Australia/Brisbane", "Australia/Broken Hill", "Australia/Canberra", "Australia/Currie", "Australia/Darwin", "Australia/Eucla", "Australia/Hobart", "Australia/LHI", "Australia/Lindeman", "Australia/Lord Howe", "Australia/Melbourne", "Australia/NSW", "Australia/North", "Australia/Perth", "Australia/Queensland", "Australia/South", "Australia/Sydney", "Australia/Tasmania", "Australia/Victoria", "Australia/West", "Australia/Yancowinna", "Brazil/Acre", "Brazil/DeNoronha", "Brazil/East", "Brazil/West", "CET", "CST6CDT", "Canada/Atlantic", "Canada/Central", "Canada/Eastern", "Canada/Mountain", "Canada/Newfoundland", "Canada/Pacific", "Canada/Saskatchewan", "Canada/Yukon", "Chile/Continental", "Chile/EasterIsland", "Cuba", "EET", "EST", "EST5EDT", "Egypt", "Eire", "Etc/GMT", "Etc/GMT+0", "Etc/GMT+1", "Etc/GMT+10", "Etc/GMT+11", "Etc/GMT+12", "Etc/GMT+2", "Etc/GMT+3", "Etc/GMT+4", "Etc/GMT+5", "Etc/GMT+6", "Etc/GMT+7", "Etc/GMT+8", "Etc/GMT+9", "Etc/GMT-0", "Etc/GMT-1", "Etc/GMT-10", "Etc/GMT-11", "Etc/GMT-12", "Etc/GMT-13", "Etc/GMT-14", "Etc/GMT-2", "Etc/GMT-3", "Etc/GMT-4", "Etc/GMT-5", "Etc/GMT-6", "Etc/GMT-7", "Etc/GMT-8", "Etc/GMT-9", "Etc/GMT0", "Etc/Greenwich", "Etc/UCT", "Etc/UTC", "Etc/Universal", "Etc/Zulu", "Europe/Amsterdam", "Europe/Andorra", "Europe/Astrakhan", "Europe/Athens", "Europe/Belfast", "Europe/Belgrade", "Europe/Berlin", "Europe/Bratislava", "Europe/Brussels", "Europe/Bucharest", "Europe/Budapest", "Europe/Busingen", "Europe/Chisinau", "Europe/Copenhagen", "Europe/Dublin", "Europe/Gibraltar", "Europe/Guernsey", "Europe/Helsinki", "Europe/Isle of Man", "Europe/Istanbul", "Europe/Jersey", "Europe/Kaliningrad", "Europe/Kiev", "Europe/Kirov", "Europe/Lisbon", "Europe/Ljubljana", "Europe/London", "Europe/Luxembourg", "Europe/Madrid", "Europe/Malta", "Europe/Mariehamn", "Europe/Minsk", "Europe/Monaco", "Europe/Moscow", "Europe/Nicosia", "Europe/Oslo", "Europe/Paris", "Europe/Podgorica", "Europe/Prague", "Europe/Riga", "Europe/Rome", "Europe/Samara", "Europe/San Marino", "Europe/Sarajevo", "Europe/Saratov", "Europe/Simferopol", "Europe/Skopje", "Europe/Sofia", "Europe/Stockholm", "Europe/Tallinn", "Europe/Tirane", "Europe/Tiraspol", "Europe/Ulyanovsk", "Europe/Uzhgorod", "Europe/Vaduz", "Europe/Vatican", "Europe/Vienna", "Europe/Vilnius", "Europe/Volgograd", "Europe/Warsaw", "Europe/Zagreb", "Europe/Zaporozhye", "Europe/Zurich", "GB", "GB-Eire", "GMT", "GMT+0", "GMT-0", "GMT0", "Greenwich", "HST", "Hongkong", "Iceland", "Indian/Antananarivo", "Indian/Chagos", "Indian/Christmas", "Indian/Cocos", "Indian/Comoro", "Indian/Kerguelen", "Indian/Mahe", "Indian/Maldives", "Indian/Mauritius", "Indian/Mayotte", "Indian/Reunion", "Iran", "Israel", "Jamaica", "Japan", "Kwajalein", "Libya", "MET", "MST", "MST7MDT", "Mexico/BajaNorte", "Mexico/BajaSur", "Mexico/General", "NZ", "NZ-CHAT", "Navajo", "PRC", "PST8PDT", "Pacific/Apia", "Pacific/Auckland", "Pacific/Bougainville", "Pacific/Chatham", "Pacific/Chuuk", "Pacific/Easter", "Pacific/Efate", "Pacific/Enderbury", "Pacific/Fakaofo", "Pacific/Fiji", "Pacific/Funafuti", "Pacific/Galapagos", "Pacific/Gambier", "Pacific/Guadalcanal", "Pacific/Guam", "Pacific/Honolulu", "Pacific/Johnston", "Pacific/Kiritimati", "Pacific/Kosrae", "Pacific/Kwajalein", "Pacific/Majuro", "Pacific/Marquesas", "Pacific/Midway", "Pacific/Nauru", "Pacific/Niue", "Pacific/Norfolk", "Pacific/Noumea", "Pacific/Pago Pago", "Pacific/Palau", "Pacific/Pitcairn", "Pacific/Pohnpei", "Pacific/Ponape", "Pacific/Port Moresby", "Pacific/Rarotonga", "Pacific/Saipan", "Pacific/Samoa", "Pacific/Tahiti", "Pacific/Tarawa", "Pacific/Tongatapu", "Pacific/Truk", "Pacific/Wake", "Pacific/Wallis", "Pacific/Yap", "Poland", "Portugal", "ROC", "ROK", "Singapore", "Turkey", "UCT", "US/Alaska", "US/Aleutian", "US/Arizona", "US/Central", "US/East-Indiana", "US/Eastern", "US/Hawaii", "US/Indiana-Starke", "US/Michigan", "US/Mountain", "US/Pacific", "US/Pacific-New", "US/Samoa", "UTC", "Universal", "W-SU", "WET", "Zulu"] +const USTZS = ["America/New York", "America/Detroit", "America/Kentucky/Louisville", "America/Kentucky/Monticello", "America/Indiana/Indianapolis", "America/Indiana/Vincennes", "America/Indiana/Winamac", "America/Indiana/Marengo", "America/Indiana/Petersburg", "America/Indiana/Vevay", "America/Chicago", "America/Indiana/Tell City", "America/Indiana/Knox", "America/Menominee", "America/North Dakota/Center", "America/North Dakota/New Salem", "America/North Dakota/Beulah", "America/Denver", "America/Boise", "America/Phoenix", "America/Los Angeles", "America/Anchorage", "America/Juneau", "America/Sitka", "America/Metlakatla", "America/Yakutat", "America/Nome", "America/Adak", "Pacific/Honolulu"] + test('new york date timestamp format', () => { - // Month parameter is zero indexed so it's actually the 10th month. - const testDate = new Date(Date.UTC(2020, 9, 3, 14, 19, 4, 23)).getTime(); - const expected = 'Saturday, October 3, 2020'; - const actual = utils.timestampToDateFormatted(testDate); - expect(actual).toEqual(expected); + // Month parameter is zero indexed so it's actually the 10th month. + const testDate = new Date(Date.UTC(2020, 9, 3, 14, 19, 4, 23)).getTime(); + const expected = 'Saturday, October 3, 2020'; + const actual = utils.timestampToDateFormatted(testDate); + expect(actual).toEqual(expected); }); test('other date timestamp format', () => { - const testDate = new Date(Date.UTC(2020, 7, 23, 2, 3, 2, 4)).getTime(); - const expectedCentral = 'Saturday, August 22, 2020'; - const expectedSingapore = 'Sunday, August 23, 2020'; - const actualCentral = utils.timestampToDateFormatted(testDate, TZ_CHICAGO); - const actualSingapore = utils.timestampToDateFormatted(testDate, TZ_SINGAPORE); - expect(actualCentral).toEqual(expectedCentral); - expect(actualSingapore).toEqual(expectedSingapore); + const testDate = new Date(Date.UTC(2020, 7, 23, 2, 3, 2, 4)).getTime(); + const expectedCentral = 'Saturday, August 22, 2020'; + const expectedSingapore = 'Sunday, August 23, 2020'; + const actualCentral = utils.timestampToDateFormatted(testDate, TZ_CHICAGO); + const actualSingapore = utils.timestampToDateFormatted(testDate, TZ_SINGAPORE); + expect(actualCentral).toEqual(expectedCentral); + expect(actualSingapore).toEqual(expectedSingapore); }) test('new york time timestamp format', () => { - // Month parameter is zero indexed so it's actually the 10th month. - const testDate = new Date(Date.UTC(2020, 9, 3, 14, 19, 4, 23)).getTime(); - const expected = '10:19 AM'; - const actual = utils.timestampToTimeFormatted(testDate); - expect(actual).toEqual(expected); + // Month parameter is zero indexed so it's actually the 10th month. + const testDate = new Date(Date.UTC(2020, 9, 3, 14, 19, 4, 23)).getTime(); + const expected = '10:19 AM'; + const actual = utils.timestampToTimeFormatted(testDate); + expect(actual).toEqual(expected); }); test('other time timestamp format', () => { - const testDate = new Date(Date.UTC(2020, 7, 23, 2, 3, 2, 4)).getTime(); - const expectedCentral = '9:03 PM'; - const expectedSingapore = '10:03 AM'; - const actualCentral = utils.timestampToTimeFormatted(testDate, TZ_CHICAGO); - const actualSingapore = utils.timestampToTimeFormatted(testDate, TZ_SINGAPORE); - expect(actualCentral).toEqual(expectedCentral); - expect(actualSingapore).toEqual(expectedSingapore); + const testDate = new Date(Date.UTC(2020, 7, 23, 2, 3, 2, 4)).getTime(); + const expectedCentral = '9:03 PM'; + const expectedSingapore = '10:03 AM'; + const actualCentral = utils.timestampToTimeFormatted(testDate, TZ_CHICAGO); + const actualSingapore = utils.timestampToTimeFormatted(testDate, TZ_SINGAPORE); + expect(actualCentral).toEqual(expectedCentral); + expect(actualSingapore).toEqual(expectedSingapore); }) test('new york full timestamp format', () => { - // Month parameter is zero indexed so it's actually the 10th month. - const testDate = new Date(Date.UTC(2020, 9, 3, 14, 19, 4, 23)).getTime(); - const expected = 'Saturday, October 3, 2020, 10:19 AM'; - const actual = utils.timestampToFormatted(testDate); - expect(actual).toEqual(expected); + // Month parameter is zero indexed so it's actually the 10th month. + const testDate = new Date(Date.UTC(2020, 9, 3, 14, 19, 4, 23)).getTime(); + const expected = 'Saturday, October 3, 2020, 10:19 AM'; + const actual = utils.timestampToFormatted(testDate); + expect(actual).toEqual(expected); }); test('other full timestamp format', () => { @@ -60,6 +63,23 @@ test('other full timestamp format', () => { expect(actualSingapore).toEqual(expectedSingapore); }) +describe('timezones for country', () => { + test('legit country no spaces', () => { + const actual = utils.timezonesForCountry('China'); + const expected = ['Asia/Shanghai', 'Asia/Urumqi']; + expect(new Set(actual.sort())).toEqual(new Set(expected.sort())); + }) + + test('legit country, yes spaces', () => { + const actual = utils.timezonesForCountry('United States of America'); + expect(new Set(actual)).toEqual(new Set(USTZS)); + }) + + test('not legit country (spaces and non spaces)', () => { + const actual = utils.timezonesForCountry('MURICA'); + expect(new Set(actual)).toEqual(new Set(ALLTZS)); + }) +}) const mockTimeNow = 0; jest.mock('firebase/app', () => ({ firestore: { diff --git a/frontend/src/components/ViewActivities/activity.js b/frontend/src/components/ViewActivities/activity.js index 862102ec..9efb4aca 100644 --- a/frontend/src/components/ViewActivities/activity.js +++ b/frontend/src/components/ViewActivities/activity.js @@ -2,18 +2,10 @@ import React from 'react'; import * as time from '../Utils/time.js'; import * as DB from '../../constants/database.js' import '../../styles/activities.css'; -import { getField, writeActivity } from './activityfns.js'; -import { Accordion, Button, Card, Col, Form, Row } from 'react-bootstrap'; +import EditActivity from './editActivity.js'; +import { Accordion, Card } from 'react-bootstrap'; +import * as utils from './activityfns.js'; -/** - * Return a dropdown of all the timezones. - * - * @return {HTML} Dropdown of all the timezones. - */ -function timezonePicker() { - // TODO: Make this dropdown. (#51) - return
-} /** * React component for a single activity. @@ -33,32 +25,6 @@ class Activity extends React.Component { this.setEditActivity = this.setEditActivity.bind(this); this.finishEditActivity = this.finishEditActivity.bind(this); this.displayCard = this.displayCard.bind(this); - this.editActivity = this.editActivity.bind(this); - - // References. - this.editTitleRef = React.createRef(); - this.editStartDateRef = React.createRef(); - this.editEndDateRef = React.createRef(); - this.editStartTimeRef = React.createRef(); - this.editEndTimeRef = React.createRef(); - this.editDescriptionRef = React.createRef(); - } - - /** - * Edit an activity in the database upon form submission. - * TODO: Update times as well! This only does the text field forms (#64). - */ - editActivity() { - let newVals = {}; - if (this.editTitleRef.current.value !== '') { - newVals[DB.ACTIVITIES_TITLE] = this.editTitleRef.current.value; - } - if (this.editDescriptionRef.current.value !== '') { - newVals[DB.ACTIVITIES_DESCRIPTION] = this.editDescriptionRef.current.value; - } - if (Object.keys(newVals).length !== 0) { - writeActivity(this.props.activity.tripId, this.props.activity.id, newVals); - } } /** @@ -75,8 +41,6 @@ class Activity extends React.Component { */ finishEditActivity(event) { this.setState({editing: false}); - event.preventDefault(); - this.editActivity(); }; /** @@ -87,40 +51,15 @@ class Activity extends React.Component { if (!this.state.editing) { // View mode. return ( -

Start time: {time.timestampToFormatted(activity[DB.ACTIVITIES_START_TIME])}

-

End time: {time.timestampToFormatted(activity[DB.ACTIVITIES_END_TIME])}

+

{utils.getField(activity, DB.ACTIVITIES_DESCRIPTION, '')}

+

Start time: {time.timestampToFormatted(activity[DB.ACTIVITIES_START_TIME])} + {utils.getField(activity, DB.ACTIVITIES_START_COUNTRY, '', ' at ')}

+

End time: {time.timestampToFormatted(activity[DB.ACTIVITIES_END_TIME])} + {utils.getField(activity, DB.ACTIVITIES_END_COUNTRY, '', ' at ')}

); } else { // Edit mode. - return ( - // TODO: Save form. (#48) -
- - Title: - - - - From: - - - {timezonePicker()} - - - To: - - - {timezonePicker()} - - - Description: - - - - -
- ) + return ( ); } } diff --git a/frontend/src/components/ViewActivities/activityfns.js b/frontend/src/components/ViewActivities/activityfns.js index c77c6282..ecf550f9 100644 --- a/frontend/src/components/ViewActivities/activityfns.js +++ b/frontend/src/components/ViewActivities/activityfns.js @@ -17,7 +17,7 @@ const db = app.firestore(); /** * A single activity day. A single instance looks like: - *
 ['MM/DD/YYYY', [activities on that day]] 
+ *
 ['MM/DD/YYYY', [activities on that day]]
* @typedef {Array.} DayOfActivities * */ @@ -53,7 +53,7 @@ export function sortByDate(tripActivities) { } /** - * Put a and b in display order. + * Puta andb in display order. * This function is a comparator. * @param {ActivityInfo} a Dictionary representing activity a and its fields. * @param {ActivityInfo} b Dictionary representing activity b and its fields. @@ -77,13 +77,14 @@ export function compareActivities(a, b) { * @param {ActivityInfo} activity The activity from which to get the field. * @param {string} fieldName Name of field to get. * @param {*} defaultValue Value if field is not found/is null. + * @param {string} prefix The prefix to put before a returned value if the field exists. * @return {*} activity[fieldName] if possible, else defaultValue. */ -export function getField(activity, fieldName, defaultValue) { +export function getField(activity, fieldName, defaultValue, prefix=''){ if (activity[fieldName] === null || activity[fieldName] === undefined) { return defaultValue; } - return activity[fieldName]; + return prefix + activity[fieldName]; } /** diff --git a/frontend/src/components/ViewActivities/activityfns.test.js b/frontend/src/components/ViewActivities/activityfns.test.js index 99d82ade..1295b5d8 100644 --- a/frontend/src/components/ViewActivities/activityfns.test.js +++ b/frontend/src/components/ViewActivities/activityfns.test.js @@ -96,4 +96,6 @@ test('getField', () => { const activity = {field1: 'yes'}; expect(activityFns.getField(activity, 'field1', 'nooo')).toBe('yes'); expect(activityFns.getField(activity, 'field2', 4)).toBe(4); + expect(activityFns.getField(activity, 'field1', 'nooo', 'aww ')).toBe('aww yes'); + expect(activityFns.getField(activity, 'field2', 4, ' and')).toBe(4); }) \ No newline at end of file diff --git a/frontend/src/components/ViewActivities/editActivity.js b/frontend/src/components/ViewActivities/editActivity.js new file mode 100644 index 00000000..1c6b7403 --- /dev/null +++ b/frontend/src/components/ViewActivities/editActivity.js @@ -0,0 +1,181 @@ +import React from 'react'; +import { Button, Col, Form, Row } from 'react-bootstrap'; +import { getField, writeActivity } from './activityfns.js'; +import * as DB from '../../constants/database.js' +import { countryList } from '../../constants/countries.js'; +import * as time from '../Utils/time.js'; +import * as formElements from './editActivityFormElements.js'; + +/** + * React component for the form that's used when the user is editing an activity. + * + * @property {Object} props ReactJS props. + * @property {ActivityInfo} props.activity The activity to display. + * @property {function} props.submitFunction The function to run upon submission. + */ +class EditActivity extends React.Component { + /** @override */ + constructor(props){ + super(props); + + this.state = {startTz: false, endTz: false}; + + // Bind state users/modifiers to `this`. + this.editActivity = this.editActivity.bind(this); + this.finishEditActivity = this.finishEditActivity.bind(this); + this.timezoneDropdown = this.timezoneDropdown.bind(this); + + // References. + this.editTitleRef = React.createRef(); + this.editStartDateRef = React.createRef(); + this.editEndDateRef = React.createRef(); + this.editStartTimeRef = React.createRef(); + this.editEndTimeRef = React.createRef(); + this.editDescriptionRef = React.createRef(); + this.editStartLocRef = React.createRef(); + this.editEndLocRef = React.createRef(); + this.startTz = React.createRef(); + this.endTz = React.createRef(); + } + + /** + * Edit an activity in the database upon form submission. + * TODO: Update times as well! This only does the text field forms (#64). + */ + editActivity() { + let newVals = {}; + if (this.editTitleRef.current.value !== '') { + newVals[DB.ACTIVITIES_TITLE] = this.editTitleRef.current.value; + } + if (this.editDescriptionRef.current.value !== '') { + newVals[DB.ACTIVITIES_DESCRIPTION] = this.editDescriptionRef.current.value; + } + if (this.editStartLocRef.current.value !== 'No Change'){ + newVals[DB.ACTIVITIES_START_COUNTRY] = this.editStartLocRef.current.value; + } + if (this.editEndLocRef.current.value !== 'No Change'){ + newVals[DB.ACTIVITIES_END_COUNTRY] = this.editEndLocRef.current.value; + } + if (Object.keys(newVals).length !== 0) { + writeActivity(this.props.activity.tripId, this.props.activity.id, newVals); + } + } + + /** Runs when the `submit` button on the form is pressed. */ + finishEditActivity(event) { + event.preventDefault(); + this.editActivity(); + this.props.submitFunction(); + } + + // "Flip switch" on timezone dropdown so the dropdown's contents update to the + // selected country's timezones. + startTimeTzUpdate = () => { this.setState({startTz : !this.state.startTz})}; + endTimeTzUpdate = () => { this.setState({endTz : !this.state.endTz})}; + + /** + * Returns a dropdown of all the timezones. + * The dropdown's values change based on the corrresponding country dropdown to + * reduce scrolling and ensure that the location corresponds to the time zone. + * + * Tests done manually using UI. + * + * @param {string} st Either 'start' or 'end' depending on whether the + * timezone is for the start or end timezone. + * @return {HTML} HTML dropdown item. + */ + timezoneDropdown(st) { + const ref = st === 'start' ? this.editStartLocRef : this.editEndLocRef; + const dbEntry = st === 'start' ? DB.ACTIVITIES_START_COUNTRY : DB.ACTIVITIES_END_COUNTRY; + let timezones; + if (ref.current == null) { + // If activity[key] DNE, then timezones will just return all tzs anyway. + timezones = time.timezonesForCountry(this.props.activity[dbEntry]); + } else { + timezones = time.timezonesForCountry(ref.current.value); + } + return ( + + {timezones.map((item, index) => { + return (); + })} + + ) + } + /** + * Create a dropdown of all the countries. + * This dropdown is linked to the corresponding timezone dropdown, + * so when the country changes here, the values in the timezone dropdown + * change as well. + * + * @param {ref} ref The reference to attach to the dropdown. + * @param {ref} tzref The corresponding time zone reference field. + * @return {HTML} HTML dropdown of all the countries with timezones. + */ + countriesDropdown(ref, tzref) { + return ( + + + {countryList.map((item, index) => { + return ( + + ); + })} + + ); + } + + render() { + const activity = this.props.activity; + return ( +
+ {formElements.textElementFormGroup( + 'formActivityTitle', // controlId + 'Title:', // formLabel + activity[DB.ACTIVITIES_TITLE],// placeHolder + this.editTitleRef // ref + )} + {formElements.locationElementFormGroup( + 'formActivityStartLocation', // controlId + 'Start Location:', // formLabel + this.countriesDropdown(this.editStartLocRef, this.startTimeTzUpdate) // dropdown + )} + {formElements.locationElementFormGroup( + 'formActivityEndLocation', // controlId + 'End Location:', // formLabel + this.countriesDropdown(this.editEndLocRef, this.endTimeTzUpdate) // dropdown + )} + {formElements.dateTimeTzFormGroup( + 'formActivityStartTime', // controlId + 'From:', // formLabel + this.editStartDateRef, // dateRef + null, // dateDefault + this.editStartTimeRef, // timeRef, + null, // timeDefault, + this.timezoneDropdown('start') // tzpicker + )} + {formElements.dateTimeTzFormGroup( + 'formActivityEndTime', // controlId + 'To:', // formLabel + this.editEndDateRef, // dateRef + null, // dateDefault + this.editEndTimeRef, // timeRef, + null, //timeDefault, + this.timezoneDropdown('end') // tzpicker + )} + {formElements.textElementFormGroup( + 'formActivityDescription', // controlId + 'Description:', // formLabel + getField(activity, DB.ACTIVITIES_DESCRIPTION, 'Add some details!'), // placeHolder + this.editDescriptionRef // ref + )} + +
+ ); + } +} + +export default EditActivity; diff --git a/frontend/src/components/ViewActivities/editActivityFormElements.js b/frontend/src/components/ViewActivities/editActivityFormElements.js new file mode 100644 index 00000000..f53ccaba --- /dev/null +++ b/frontend/src/components/ViewActivities/editActivityFormElements.js @@ -0,0 +1,79 @@ +import React from 'react'; +import { Button, Col, Form, Row } from 'react-bootstrap'; + +// This file waas written after #87 was created. +// As a result, some fields and functions may not be used yet. +const TITLEWIDTH = 3; +const COUNTRYWIDTH = 6; +const DATEWIDTH = 4; +const TIMEWIDTH = 2; +const TZPICKERWIDTH = 3; + +/** + * Create a Text element Form Group for the editActivity form. + * + * @param {string} controlId FormGroup's control ID. + * @param {string} formLabel The label of the field for this FormGroup. + * @param {string} placeHolder The input's placeholder. + * @param {ref} ref The input's reference. + * @returns {HTML} A text element form group. + */ +export function textElementFormGroup(controlId, formLabel, placeHolder, ref) { + return ( + + {formLabel} + + + + + ); +} + +/** + * Create a Location Dropdown element Form Group for the editActivity form. + * + * @param {string} controlId FormGroup's control ID. + * @param {string} formLabel The label of the field for this FormGroup. + * @param {string} dropdown The dropdown. + * @returns {HTML} a location dropdown form group. + */ +export function locationElementFormGroup(controlId, formLabel, dropdown) { + return ( + + {formLabel} + {dropdown} + + ); +} + +/** + * Create a Form Group for inserting date, time, and timezone for + * the editActivity form.. + * + * @param {string} controlId FormGroup's control ID. + * @param {string} formLabel Label of the field for this FormGroup. + * @param {ref} dateRef Date's reference. + * @param {string} dateDefault Default date. + * @param {ref} timeRef Time's reference. + * @param {ref} timeDefault Default time. + * @param {HTML} tzpicker Timezone picker dropdown. + * @returns {HTML} A FormGroup for date, time, and timezone. + */ +export function dateTimeTzFormGroup(controlId, formLabel, dateRef, + dateDefault, timeRef, timeDefault, tzpicker) { + return ( + + {formLabel} + + + + + + + {tzpicker} + + ); +} \ No newline at end of file diff --git a/frontend/src/constants/countries.js b/frontend/src/constants/countries.js new file mode 100644 index 00000000..15c37e96 --- /dev/null +++ b/frontend/src/constants/countries.js @@ -0,0 +1,255 @@ +// This file contains all the countries and their 2-letter country codes. +// It also contains a list of all the countries for which there are accompanying country codes- +// called countryList. It's at the bottom of this (very long) file. +export const topNCountries = 10; // Number of "popular countries" to display first +export const countryCodes = { +"United States Minor Outlying Islands" : "UM", +"United States of America" : "US", +"Denmark" : "DK", +"Spain" : "ES", +"France" : "FR", +"Greece" : "GR", +"Ireland" : "IE", +"India" : "IN", +"Italy" : "IT", +"Jamaica" : "JM", +"Andorra" : "AD", +"United Arab Emirates" : "AE", +"Afghanistan" : "AF", +"Antigua and Barbuda" : "AG", +"Anguilla" : "AI", +"Albania" : "AL", +"Armenia" : "AM", +"Angola" : "AO", +"Antarctica" : "AQ", +"Argentina" : "AR", +"American Samoa" : "AS", +"Austria" : "AT", +"Australia" : "AU", +"Aruba" : "AW", +"Åland Islands" : "AX", +"Azerbaijan" : "AZ", +"Bosnia and Herzegovina" : "BA", +"Barbados" : "BB", +"Bangladesh" : "BD", +"Belgium" : "BE", +"Burkina Faso" : "BF", +"Bulgaria" : "BG", +"Bahrain" : "BH", +"Burundi" : "BI", +"Benin" : "BJ", +"Saint Barthélemy" : "BL", +"Bermuda" : "BM", +"Brunei Darussalam" : "BN", +"Bolivia (Plurinational State of)" : "BO", +"Bonaire, Sint Eustatius and Saba" : "BQ", +"Brazil" : "BR", +"Bahamas" : "BS", +"Bhutan" : "BT", +"Bouvet Island" : "BV", +"Botswana" : "BW", +"Belarus" : "BY", +"Belize" : "BZ", +"Canada" : "CA", +"Cocos (Keeling) Islands" : "CC", +"Congo (the Democratic Republic of the)" : "CD", +"Central African Republic" : "CF", +"Congo" : "CG", +"Switzerland" : "CH", +"Côte d'Ivoire" : "CI", +"Cook Islands" : "CK", +"Chile" : "CL", +"Cameroon" : "CM", +"China" : "CN", +"Colombia" : "CO", +"Costa Rica" : "CR", +"Cuba" : "CU", +"Cabo Verde" : "CV", +"Curaçao" : "CW", +"Christmas Island" : "CX", +"Cyprus" : "CY", +"Czechia" : "CZ", +"Germany" : "DE", +"Djibouti" : "DJ", +"Dominica" : "DM", +"Dominican Republic" : "DO", +"Algeria" : "DZ", +"Ecuador" : "EC", +"Estonia" : "EE", +"Egypt" : "EG", +"Western Sahara" : "EH", +"Eritrea" : "ER", +"Ethiopia" : "ET", +"Finland" : "FI", +"Fiji" : "FJ", +"Falkland Islands [Malvinas]" : "FK", +"Micronesia (Federated States of)" : "FM", +"Faroe Islands" : "FO", +"Gabon" : "GA", +"United Kingdom of Great Britain and Northern Ireland" : "GB", +"Grenada" : "GD", +"Georgia" : "GE", +"French Guiana" : "GF", +"Guernsey" : "GG", +"Ghana" : "GH", +"Gibraltar" : "GI", +"Greenland" : "GL", +"Gambia" : "GM", +"Guinea" : "GN", +"Guadeloupe" : "GP", +"Equatorial Guinea" : "GQ", +"South Georgia and the South Sandwich Islands" : "GS", +"Guatemala" : "GT", +"Guam" : "GU", +"Guinea-Bissau" : "GW", +"Guyana" : "GY", +"Hong Kong" : "HK", +"Heard Island and McDonald Islands" : "HM", +"Honduras" : "HN", +"Croatia" : "HR", +"Haiti" : "HT", +"Hungary" : "HU", +"Indonesia" : "ID", +"Israel" : "IL", +"Isle of Man" : "IM", +"British Indian Ocean Territory" : "IO", +"Iraq" : "IQ", +"Iran (Islamic Republic of)" : "IR", +"Iceland" : "IS", +"Jersey" : "JE", +"Jordan" : "JO", +"Japan" : "JP", +"Kenya" : "KE", +"Kyrgyzstan" : "KG", +"Cambodia" : "KH", +"Kiribati" : "KI", +"Comoros" : "KM", +"Saint Kitts and Nevis" : "KN", +"Korea (the Democratic People's Republic of)" : "KP", +"Korea (the Republic of)" : "KR", +"Kuwait" : "KW", +"Cayman Islands" : "KY", +"Kazakhstan" : "KZ", +"Lao People's Democratic Republic" : "LA", +"Lebanon" : "LB", +"Saint Lucia" : "LC", +"Liechtenstein" : "LI", +"Sri Lanka" : "LK", +"Liberia" : "LR", +"Lesotho" : "LS", +"Lithuania" : "LT", +"Luxembourg" : "LU", +"Latvia" : "LV", +"Libya" : "LY", +"Morocco" : "MA", +"Monaco" : "MC", +"Moldova (the Republic of)" : "MD", +"Montenegro" : "ME", +"Saint Martin (French part)" : "MF", +"Madagascar" : "MG", +"Marshall Islands" : "MH", +"Republic of North Macedonia" : "MK", +"Mali" : "ML", +"Myanmar" : "MM", +"Mongolia" : "MN", +"Macao" : "MO", +"Northern Mariana Islands" : "MP", +"Martinique" : "MQ", +"Mauritania" : "MR", +"Montserrat" : "MS", +"Malta" : "MT", +"Mauritius" : "MU", +"Maldives" : "MV", +"Malawi" : "MW", +"Mexico" : "MX", +"Malaysia" : "MY", +"Mozambique" : "MZ", +"Namibia" : "NA", +"New Caledonia" : "NC", +"Niger" : "NE", +"Norfolk Island" : "NF", +"Nigeria" : "NG", +"Nicaragua" : "NI", +"Netherlands" : "NL", +"Norway" : "NO", +"Nepal" : "NP", +"Nauru" : "NR", +"Niue" : "NU", +"New Zealand" : "NZ", +"Oman" : "OM", +"Panama" : "PA", +"Peru" : "PE", +"French Polynesia" : "PF", +"Papua New Guinea" : "PG", +"Philippines" : "PH", +"Pakistan" : "PK", +"Poland" : "PL", +"Saint Pierre and Miquelon" : "PM", +"Pitcairn" : "PN", +"Puerto Rico" : "PR", +"Palestine, State of" : "PS", +"Portugal" : "PT", +"Palau" : "PW", +"Paraguay" : "PY", +"Qatar" : "QA", +"Réunion" : "RE", +"Romania" : "RO", +"Serbia" : "RS", +"Russian Federation" : "RU", +"Rwanda" : "RW", +"Saudi Arabia" : "SA", +"Solomon Islands" : "SB", +"Seychelles" : "SC", +"Sudan" : "SD", +"Singapore" : "SG", +"Saint Helena, Ascension and Tristan da Cunha" : "SH", +"Slovenia" : "SI", +"Svalbard and Jan Mayen" : "SJ", +"Slovakia" : "SK", +"Sierra Leone" : "SL", +"San Marino" : "SM", +"Senegal" : "SN", +"Somalia" : "SO", +"Suriname" : "SR", +"South Sudan" : "SS", +"Sweden" : "SE", +"Sao Tome and Principe" : "ST", +"El Salvador" : "SV", +"Sint Maarten (Dutch part)" : "SX", +"Syrian Arab Republic" : "SY", +"Eswatini" : "SZ", +"Turks and Caicos Islands" : "TC", +"Chad" : "TD", +"French Southern Territories" : "TF", +"Togo" : "TG", +"Thailand" : "TH", +"Tajikistan" : "TJ", +"Tokelau" : "TK", +"Timor-Leste" : "TL", +"Turkmenistan" : "TM", +"Tunisia" : "TN", +"Tonga" : "TO", +"Turkey" : "TR", +"Trinidad and Tobago" : "TT", +"Tuvalu" : "TV", +"Taiwan (Province of China)" : "TW", +"Tanzania, United Republic of" : "TZ", +"Ukraine" : "UA", +"Uganda" : "UG", +"Uruguay" : "UY", +"Uzbekistan" : "UZ", +"Holy See" : "VA", +"Saint Vincent and the Grenadines" : "VC", +"Venezuela (Bolivarian Republic of)" : "VE", +"Virgin Islands (British)" : "VG", +"Virgin Islands (U.S.)" : "VI", +"Viet Nam" : "VN", +"Vanuatu" : "VU", +"Wallis and Futuna" : "WF", +"Samoa" : "WS", +"Yemen" : "YE", +"Mayotte" : "YT", +"South Africa" : "ZA", +"Zambia" : "ZM", +"Zimbabwe" : "ZW"} +export const countryList = Object.keys(countryCodes); diff --git a/frontend/src/constants/database.js b/frontend/src/constants/database.js index 5f7586d6..d3b0aaba 100644 --- a/frontend/src/constants/database.js +++ b/frontend/src/constants/database.js @@ -14,4 +14,6 @@ export const COLLECTION_ACTIVITIES = 'activities'; export const ACTIVITIES_START_TIME = 'start_time'; export const ACTIVITIES_END_TIME = 'end_time'; export const ACTIVITIES_TITLE = 'title'; -export const ACTIVITIES_DESCRIPTION = 'description'; \ No newline at end of file +export const ACTIVITIES_DESCRIPTION = 'description'; +export const ACTIVITIES_START_COUNTRY = 'start_country'; +export const ACTIVITIES_END_COUNTRY = 'end_country'; \ No newline at end of file diff --git a/frontend/src/styles/activities.css b/frontend/src/styles/activities.css index 67d4ed4d..016d7ea1 100644 --- a/frontend/src/styles/activities.css +++ b/frontend/src/styles/activities.css @@ -22,11 +22,11 @@ /* TODO: auto height, transition. (#49)*/ .view-activity.edit { - height: 20em; + height: 35em; } .view-activity { overflow: scroll; - height: 7.5em; + height: 10em; transition: height 0.25s linear; }