-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #57 from googleinterns/activity-viewer-react
Activity viewer react
Showing
15 changed files
with
390 additions
and
14 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,36 @@ | ||
/** | ||
* Format a timestamp (in milliseconds) into a pretty string with just the time. | ||
* | ||
* @param {int} msTimestamp | ||
* @param {string} timezone | ||
* @returns {string} Time formatted into a string like '10:19 AM'. | ||
*/ | ||
export function timestampToTimeFormatted(msTimestamp, timezone = 'America/New_York') { | ||
const date = new Date(msTimestamp); | ||
const formatOptions = { | ||
hour: 'numeric', | ||
minute: '2-digit', | ||
timeZone: timezone | ||
}; | ||
const formatted = date.toLocaleTimeString('en-US', formatOptions); | ||
return formatted; | ||
} | ||
|
||
/** | ||
* Format a timestamp (in milliseconds) into a pretty string with just the date. | ||
* | ||
* @param {int} msTimestamp | ||
* @param {string} timezone | ||
* @returns {string} Time formatted into a string like 'Monday, January 19, 1970'. | ||
*/ | ||
export function timestampToDateFormatted(msTimestamp, timezone='America/New_York') { | ||
const date = new Date(msTimestamp); | ||
const formatOptions = { | ||
weekday: 'long', | ||
year: 'numeric', | ||
month: 'long', | ||
day: 'numeric', | ||
timeZone: timezone | ||
}; | ||
return date.toLocaleDateString('en-US', formatOptions); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,37 @@ | ||
import * as utils from './utils'; | ||
|
||
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); | ||
}); | ||
|
||
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, 'America/Chicago'); | ||
const actualSingapore = utils.timestampToDateFormatted(testDate, 'Asia/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); | ||
}); | ||
|
||
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, 'America/Chicago'); | ||
const actualSingapore = utils.timestampToTimeFormatted(testDate, 'Asia/Singapore'); | ||
expect(actualCentral).toEqual(expectedCentral); | ||
expect(actualSingapore).toEqual(expectedSingapore); | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
import React from 'react'; | ||
import Card from 'react-bootstrap/Card'; | ||
import * as utils from '../Utils/utils.js'; | ||
import * as DBUTILS from '../../constants/database.js' | ||
import '../../styles/activities.css'; | ||
|
||
class Activity extends React.Component { | ||
/** @inheritdoc */ | ||
render() { | ||
const activity = this.props.activity; // guaranteed to be defined. | ||
return ( | ||
<Card className='activity'> | ||
<p>title: {activity[DBUTILS.ACTIVITIES_TITLE]}</p> | ||
<p>start time: {utils.timestampToTimeFormatted(activity[DBUTILS.ACTIVITIES_START_TIME])} </p> | ||
<p>end time: {utils.timestampToTimeFormatted(activity[DBUTILS.ACTIVITIES_END_TIME])} </p> | ||
</Card> | ||
); | ||
} | ||
}; | ||
|
||
export default Activity; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,36 @@ | ||
import React from 'react'; | ||
import Accordion from 'react-bootstrap/Accordion'; | ||
import Card from 'react-bootstrap/Card'; | ||
import Activity from './activity.js'; | ||
import * as activityFns from './activityfns.js'; | ||
import * as utils from '../Utils/utils.js' | ||
|
||
class ActivityDay extends React.Component { | ||
/** @inheritdoc */ | ||
render() { | ||
// this.props.activities and this.props.date are guaranteed to be defined. | ||
if (this.props.activities.length == 0){ | ||
return <div></div>; | ||
} | ||
const sortedActivities = Array.from(this.props.activities) | ||
.sort(activityFns.compareActivities); | ||
let date = new Date(this.props.date); | ||
let id = date.getTime(); | ||
return ( | ||
<Card> | ||
<Accordion.Toggle as={Card.Header} eventKey='0' align='center'> | ||
{utils.timestampToDateFormatted(date.getTime())} | ||
</Accordion.Toggle> | ||
<Accordion.Collapse eventKey='0'> | ||
<Card.Body> | ||
{sortedActivities.map((activity, index) => { | ||
return ( <Activity activity={activity} key={index + id}/> ); | ||
})} | ||
</Card.Body> | ||
</Accordion.Collapse> | ||
</Card> | ||
); | ||
} | ||
} | ||
|
||
export default ActivityDay; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,77 @@ | ||
import * as DBFIELDS from '../../constants/database.js'; | ||
import app from '../Firebase'; | ||
|
||
const db = app.firestore(); | ||
|
||
/** | ||
* Put a and b in display order. | ||
* | ||
* @param {dictionary} a Dictionary representing activity a and its fields. | ||
* @param {dictionary} b Dictionary representing activity b and its fields. | ||
*/ | ||
export function compareActivities(a, b) { | ||
if (a[DBFIELDS.ACTIVITIES_START_TIME] < b[DBFIELDS.ACTIVITIES_START_TIME]) { | ||
return -1; | ||
} else if (a[DBFIELDS.ACTIVITIES_START_TIME] > b[DBFIELDS.ACTIVITIES_START_TIME]) { | ||
return 1; | ||
} else if (a[DBFIELDS.ACTIVITIES_END_TIME] > b[DBFIELDS.ACTIVITIES_END_TIME]) { | ||
return 1; | ||
} | ||
return -1; | ||
} | ||
|
||
/** | ||
* Gets the list of activities from the server. | ||
* | ||
* @param {string} tripId The trip ID. | ||
*/ | ||
export async function getActivityList(tripId) { | ||
return new Promise(function(resolve, reject) { | ||
let tripActivities = []; | ||
console.log(tripId); | ||
|
||
db.collection(DBFIELDS.COLLECTION_TRIPS).doc(tripId) | ||
.collection(DBFIELDS.COLLECTION_ACTIVITIES).get() | ||
.then(querySnapshot => { | ||
querySnapshot.forEach(doc => { | ||
let data = doc.data(); | ||
data['id'] = doc.id; | ||
|
||
// TODO: if start date != end date, split into 2 days. (#37) | ||
|
||
// Eliminate nanoseconds, convert to milliseconds. | ||
data[DBFIELDS.ACTIVITIES_START_TIME] = | ||
data[DBFIELDS.ACTIVITIES_START_TIME]['seconds'] * 1000; | ||
data[DBFIELDS.ACTIVITIES_END_TIME] = | ||
data[DBFIELDS.ACTIVITIES_END_TIME]['seconds'] * 1000; | ||
|
||
tripActivities.push(data); | ||
}) | ||
}).catch(error => { | ||
tripActivities = null; | ||
}).then( () => resolve(tripActivities) ); | ||
}) | ||
} | ||
|
||
/** | ||
* Sort a list of trip activities by date. | ||
* | ||
* @param {Array} tripActivities Array of activities. | ||
* @returns List of trip activities in the form | ||
* [ ['MM/DD/YYYY', [activities on that day]], ...] in chronological order by date. | ||
*/ | ||
export function sortByDate(tripActivities) { | ||
let activities = new Map(); // { MM/DD/YYYY: [activities] }. | ||
for (let activity of tripActivities) { | ||
const activityDate = new Date(activity[DBFIELDS.ACTIVITIES_START_TIME]); | ||
const dateKey = activityDate.toLocaleDateString() | ||
if (activities.has(dateKey)) { | ||
activities.get(dateKey).add(activity); | ||
} else { | ||
activities.set(dateKey, new Set([activity])); | ||
} | ||
} | ||
|
||
// Sort activities by date. | ||
return Array.from(activities).sort(compareActivities); | ||
} |
97 changes: 97 additions & 0 deletions
97
frontend/src/components/ViewActivities/activityfns.test.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,97 @@ | ||
import * as activityFns from './activityfns.js'; | ||
|
||
const ten = new Date(Date.UTC(2020, 4, 2, 10, 0)); // May 2, 2020 10:00 | ||
const eleven = new Date(Date.UTC(2020, 4, 2, 11, 0)); // May 2, 2020 11:00 | ||
const elevenThirty = new Date(Date.UTC(2020, 4, 2, 11, 30)); // May 2, 2020 11:30 | ||
const twelve = new Date(Date.UTC(2020, 4, 2, 12, 0)); // May 2, 2020 12:00 | ||
const one = new Date(Date.UTC(2020, 4, 2, 13, 0)); // May 2, 2020 13:00 | ||
const may102pm = new Date(Date.UTC(2020, 4, 10, 14, 0)); // May 10, 2020 14:00 | ||
const may014pm = new Date(Date.UTC(2020, 4, 1, 16, 0)); // May 1, 2020 16:00 | ||
const may153am = new Date(Date.UTC(2020, 4, 15, 3, 0)); // May 15, 2020 3:00 | ||
|
||
function createActivity(startTime, endTime){ | ||
return {'start_time': startTime, 'end_time': endTime}; | ||
} | ||
|
||
describe('Same day activity compareActivities', () => { | ||
const tenToTwelve = createActivity(ten, twelve); | ||
const elevenToOne = createActivity(eleven, one); | ||
const elevenToElevenThirty = createActivity(eleven, elevenThirty); | ||
const tenToEleven = createActivity(ten, eleven); | ||
const elevenToTwelve = createActivity(eleven, twelve); | ||
|
||
test('Overlapping activities', () => { | ||
expect(activityFns.compareActivities(elevenToOne, tenToTwelve)).toBe(1); | ||
expect(activityFns.compareActivities(tenToTwelve, elevenToOne)).toBe(-1); | ||
}) | ||
|
||
test('One activity completely during another', () => { | ||
expect(activityFns.compareActivities(tenToTwelve, elevenToElevenThirty)).toBe(-1); | ||
expect(activityFns.compareActivities(elevenToElevenThirty, tenToTwelve)).toBe(1); | ||
}) | ||
|
||
test('Activities with same start time', () => { | ||
expect(activityFns.compareActivities(tenToEleven, tenToTwelve)).toBe(-1); | ||
expect(activityFns.compareActivities(tenToTwelve, tenToEleven)).toBe(1); | ||
}) | ||
|
||
test('Sequential activities', () => { | ||
expect(activityFns.compareActivities(tenToEleven, elevenToTwelve)).toBe(-1); | ||
expect(activityFns.compareActivities(elevenToTwelve, tenToEleven)).toBe(1); | ||
}) | ||
|
||
test('Activities with same end time', () => { | ||
expect(activityFns.compareActivities(elevenToTwelve, tenToTwelve)).toBe(1); | ||
expect(activityFns.compareActivities(tenToTwelve, elevenToTwelve)).toBe(-1); | ||
}) | ||
}) | ||
|
||
test('compareActivities on different days', () => { | ||
const may10 = createActivity(may102pm, may102pm); | ||
const may15 = createActivity(may153am, may153am); | ||
const may01 = createActivity(may014pm, may014pm); | ||
expect(activityFns.compareActivities(may10, may01)).toBe(1); | ||
expect(activityFns.compareActivities(may15, may01)).toBe(1); | ||
}) | ||
|
||
describe('sortByDate tests', () => { | ||
const act1 = createActivity(ten, eleven); | ||
const act2 = createActivity(elevenThirty, twelve); | ||
const act3 = createActivity(twelve, one); | ||
const act4 = createActivity(may102pm, may102pm); | ||
const act5 = createActivity(may153am, may153am); | ||
|
||
test('sortByDate all same date', () => { | ||
const tripActivities = [act1, act2, act3]; | ||
|
||
let expected = new Map(); | ||
expected.set(ten.toLocaleDateString(), new Set([act1, act2, act3])); | ||
expected = Array.from(expected); | ||
|
||
expect(activityFns.sortByDate(tripActivities)).toEqual(expected); | ||
}) | ||
|
||
test('sortByDate all differentDates', () => { | ||
const tripActivities = [act3, act4, act5]; | ||
|
||
let expected = new Map(); | ||
expected.set(ten.toLocaleDateString(), new Set([act3])); | ||
expected.set(may102pm.toLocaleDateString(), new Set([act4])); | ||
expected.set(may153am.toLocaleDateString(), new Set([act5])); | ||
expected = Array.from(expected); | ||
|
||
expect(activityFns.sortByDate(tripActivities)).toEqual(expected); | ||
}) | ||
|
||
test('sortByDate mixed dates', () => { | ||
const tripActivities = [act3, act4, act1, act5, act2]; | ||
|
||
let expected = new Map(); | ||
expected.set(ten.toLocaleDateString(), new Set([act3, act1, act2])); | ||
expected.set(may102pm.toLocaleDateString(), new Set([act4])); | ||
expected.set(may153am.toLocaleDateString(), new Set([act5])); | ||
expected = Array.from(expected); | ||
|
||
expect(activityFns.sortByDate(tripActivities)).toEqual(expected); | ||
}) | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,47 @@ | ||
import React from 'react'; | ||
import app from '../Firebase'; | ||
import * as activityFns from './activityfns.js'; | ||
import ActivityDay from './activityday.js'; | ||
import Accordion from 'react-bootstrap/Accordion'; | ||
import '../../styles/activities.css'; | ||
|
||
class ActivityList extends React.Component { | ||
/** @inheritdoc */ | ||
constructor(props) { | ||
super(props); | ||
this.state = { days : [] }; | ||
} | ||
|
||
/** @inheritdoc */ | ||
async componentDidMount() { | ||
if (this.state === null) { return; } | ||
let tripActivities = await activityFns.getActivityList(this.props.tripId); | ||
if (tripActivities === null) { | ||
this.setState({days: null}); | ||
return; | ||
} | ||
this.setState({days: activityFns.sortByDate(tripActivities)}); | ||
} | ||
|
||
/** @inheritdoc */ | ||
render() { | ||
if (this.state === null) { return (<div></div>); } | ||
if (this.state.days === null) { | ||
return (<p className='activity-list'>An error has occurred :(</p> ); | ||
} else if (this.state.days.length == 0) { | ||
return (<p className='activity-list'>Plan your trip here!</p>); | ||
} | ||
return ( | ||
<div className='activity-list'> | ||
{this.state.days.map((item, index) => ( | ||
<Accordion defaultActiveKey='1' key={index} className='activity-day-dropdown'> | ||
<ActivityDay date={item[0]} activities={item[1]} /> | ||
</Accordion> | ||
) | ||
)} | ||
</div> | ||
); | ||
} | ||
} | ||
|
||
export default ActivityList; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,16 +1,15 @@ | ||
import React from 'react'; | ||
import ActivityList from './activitylist.js'; | ||
|
||
/** | ||
* ViewActivities component. | ||
*/ | ||
class ViewActivities extends React.Component { | ||
/** @inheritdoc */ | ||
render() { | ||
return ( | ||
<div> | ||
<h1>View Activities</h1> | ||
<div className='activity-page'> | ||
<ActivityList tripId={this.props.match.params.tripId}/> | ||
</div> | ||
); | ||
) | ||
} | ||
}; | ||
} | ||
|
||
export default ViewActivities; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
export const COLLECTION_TRIPS = 'trips'; | ||
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'; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
.activity-list { | ||
width: 100% | ||
} | ||
|
||
.activity-page { | ||
display: flex; | ||
flex-direction: column; | ||
flex-wrap: wrap; | ||
margin-top: 1.5em; | ||
padding: 0 1em; | ||
justify-content: center; | ||
} | ||
|
||
.activity { | ||
margin: 1em; | ||
} | ||
|
||
.activity-day-dropdown { | ||
border: black; | ||
margin: 0.5em auto; | ||
max-width: 50pc; | ||
} |