Skip to content

Commit

Permalink
Merge pull request #57 from googleinterns/activity-viewer-react
Browse files Browse the repository at this point in the history
Activity viewer react
anan-ya-y authored Jul 13, 2020

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
2 parents 467b595 + 4bad612 commit c7b4319
Show file tree
Hide file tree
Showing 15 changed files with 390 additions and 14 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# SLURP

This is the repo for the SLURP web application. The frontend is built using the React web framework and the Firebase development platform, with the backend built using Google's App Engine computing platform.
This is the repo for the SLURP web application. The frontend is built using the React web framework and the Firebase development platform, with the backend built using Google's App Engine computing platform.

## CLI Tools
This project makes use of the `gcloud` SDK for project deployment to Google Cloud, the `npm` and `yarn` package managers for Node.js, and the `nvm` Node.js version manager. Node.js 10 is the specific version required for this project.
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/components/App/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ class App extends React.Component {
<Route exact path={ROUTES.LANDING} component={LandingPage} />
<Route path={ROUTES.SIGN_IN} component={SignInPage} />
<Route path={ROUTES.VIEW_TRIPS} component={ViewTripsPage} />
<Route path={ROUTES.VIEW_ACTIVITIES} component={ViewActivitiesPage} />
<Route path={ROUTES.VIEW_ACTIVITIES + "/:tripId"} component={ViewActivitiesPage} />
</div>
</Router>
);
Expand Down
4 changes: 1 addition & 3 deletions frontend/src/components/Landing/index.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
import React from 'react';

import SignInButton from './signin-button.js';
import Card from 'react-bootstrap/Card';

/**
* Landing component that defines the first page the user encounters in the
* application.
* Landing component.
*/
class Landing extends React.Component {
/** @inheritdoc */
Expand Down
1 change: 0 additions & 1 deletion frontend/src/components/SignIn/index.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import React from 'react';

import * as firebase from 'firebase/app';
import app from '../Firebase';
import StyledFirebaseAuth from 'react-firebaseui/StyledFirebaseAuth';
Expand Down
36 changes: 36 additions & 0 deletions frontend/src/components/Utils/utils.js
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);
}
37 changes: 37 additions & 0 deletions frontend/src/components/Utils/utils.test.js
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);
})
21 changes: 21 additions & 0 deletions frontend/src/components/ViewActivities/activity.js
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;
36 changes: 36 additions & 0 deletions frontend/src/components/ViewActivities/activityday.js
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;
77 changes: 77 additions & 0 deletions frontend/src/components/ViewActivities/activityfns.js
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 frontend/src/components/ViewActivities/activityfns.test.js
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);
})
})
47 changes: 47 additions & 0 deletions frontend/src/components/ViewActivities/activitylist.js
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;
13 changes: 6 additions & 7 deletions frontend/src/components/ViewActivities/index.js
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;
7 changes: 7 additions & 0 deletions frontend/src/constants/database.js
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';
2 changes: 1 addition & 1 deletion frontend/src/serviceWorker.js
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ function registerValidSW(swUrl, config) {
} else {
// At this point, everything has been precached.
// It's the perfect time to display a
// "Content is cached for offline use." message.
// 'Content is cached for offline use.' message.
console.log('Content is cached for offline use.');

// Execute callback
Expand Down
22 changes: 22 additions & 0 deletions frontend/src/styles/activities.css
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;
}

0 comments on commit c7b4319

Please sign in to comment.