VIP web application

by Hancheng Zhao

UD version

User Story

Roles

Admin

Advisor

Student

Sections

Announcements

Projects

Peer Review

Dashboard

User Story

Roles

Admin

Advisor

Student

Sections

Announcements

Projects

Peer Review

Dashboard

CRUD

View

View

User Story

Roles

Admin

Advisor

Student

Sections

Announcements

Projects

Peer Review

Dashboard

CRUD

CRUD own 

Apply

Email Service

User Story

Roles

Admin

Advisor

Student

Sections

Announcements

Projects

Peer Review

Dashboard

Generate & Check

Generate & Check own

Review

User Story

Roles

Admin

Advisor

Student

Sections

Announcements

Projects

Peer Review

Dashboard

Roster/Application/Semester

Roster/Application

Team info

Structure

React.js

  • Mainly view layer
  • Component-Based
  • Virtual dom & JSX
  • CLI tooling

React.js

  • UI = f(state)
  • Declarative

class App extends Component {
  constructor () {
    this.state = {
      loading: true
    }
  }
  ...
  render() {
    return (
        <div>
          <Header />
            {this.state.loading === true 
            ? (<h2>Loading...</h2>)
            : (
              <div className="App">
                <Route exact path="/" component={Home}/>
                <Route path="/announcement" component={Announcement}/>
                <Route path="/projects" component={Projects}/>
                <Route path="/peer-review" component={PeerReview}/>
              </div>
            )}
          <Footer />
        </div>
    );
  }
}
export default App;

React.js

Componet-Based


class App extends Component {
  constructor () {
    this.state = {
      loading: true
    }
  }
  ...
  render() {
    return (
        <div>
          <Header />
            {this.state.loading === true 
            ? (<h2>Loading...</h2>)
            : (
              <div className="App">
                <Route exact path="/" component={Home}/>
                <Route path="/announcement" component={Announcement}/>
                <Route path="/projects" component={Projects}/>
                <Route path="/peer-review" component={PeerReview}/>
              </div>
            )}
          <Footer />
        </div>
    );
  }
}
export default App;
class Footer extends Component {

  render() {
    return (
      <div className="container">
        <div className="footer">
          <div id="innovation-footer">
            <div id="innovation-bar">
              <div className="innovation-top">
                <div className="innovation-status">
                  <a href="https://yourfuture.asu.edu/rankings"><span>ASU is #1 in the U.S. for Innovation</span></a>
                </div>
                <div className="innovation-hidden">
                  <a href="https://yourfuture.asu.edu/rankings"><img src="//www.asu.edu/asuthemes/4.6/assets/best-college-2017.png" alt="Best Colleges U.S. News Most Innovative 2017"/></a>
                </div>
              </div>
            </div>
            <div className="footer-menu">
              <ul className="default">
                <li className="links-footer"><a href="http://www.asu.edu/copyright/">Copyright &amp; Trademark</a></li>
                <li className="links-footer"><a href="http://www.asu.edu/accessibility/">Accessibility</a></li>
                <li className="links-footer"><a href="http://www.asu.edu/privacy/">Privacy</a></li>
                <li className="links-footer"><a href="http://www.asu.edu/asujobs">Jobs</a></li>
                <li className="links-footer"><a href="http://www.asu.edu/emergency/">Emergency</a></li>
                <li className="no-border links-footer"><a href="http://www.asu.edu/contactasu/">Contact ASU</a></li>
            </ul>
          </div>
        </div>
        </div>
      </div>
    );
  }
}

React.js

Componet-Based

React.js

Componet-Based

QuestionCard

React.js

Componet-Based

QuestionContainer

React.js

Componet-Based

QuestionContainer

{questions.map((question, i) => (
  <QuestionCard
    key={question.id}
    index={i}
    id={question.id}
    type={question.type}
    data={question.data}
    moveQuestion={this.moveQuestion}
    removeQuestion={this.removeQuestion}
    updateQuestion={this.updateQuestion}
  />
))}

React.js

Componet-Based

QuestionContainer

React.js

  • CLI tooling
"scripts": {
    "start": "node scripts/start.js",
    "build": "node scripts/build.js",
    "test": "node scripts/test.js --env=jsdom",
    "deploy": "cp -r ./build/. ../firebase/public && cd ../firebase/ && firebase deploy --only hosting"
  }

Mobx

1. Define your state and make it observable

2. Create a view that responds to changes in the State

3. Modify the State

Mobx

state container

decorator

 

import { observable } from "mobx";

class UserStore {
    @observable authed = false;

    login() {
        this.authed = true;
    }

    logout() {
        this.authed = false;
    }
}

const userStore = new UserStore();
export default userStore;

Mobx

import { observer } from "mobx-react";

@observer
class App extends Component {
  constructor () {
  ...
  }

  componentDidMount () {
    this.userStateChange = firebase.auth().onAuthStateChanged((user) => {
      if (user) {
        userStore.login()
        ...
        }).catch((noRole) => { //unable to retrieve role from db
          userStore.fetchUserRole(noRole);
        })
      } else {
        userStore.logout()
      }
      
    })
  }
 ...
}

React router

  • also component-based
export const AdminRoute = ({component: Component, user, ...rest}) => (
  <Route {...rest}
    render={(props) => user.role === "admin"
      ? <Component {...props} />
      : <Redirect to='/login'/>}
  />  
)

export const PrivateRoute = ({component: Component, authed, ...rest}) => (
  <Route {...rest}
    render={(props) => authed === true
      ? <Component {...props} />
      : <Redirect to={{pathname: '/login', state: {from: props.location}}} />}
  />
)

React router

  • dynamic
const Announcement = ( {match} ) => (
  <div>
    <Switch>
      <AdminRoute exact path={`${match.url}/creation`} user={userStore} component={ AnnouncementCreate }/>
      <Route exact path={`${match.url}/edit/:announcementId`} user={userStore} component={ AnnouncementEdit }/>
      <Route path={`${match.url}/:announcementId`} component={ AnnouncementPage }/>
      <Route exact path={match.url} component={ AnnouncementList }/>
    </Switch>
  </div>
)


export default Announcement;

Firebase

  • OAuth
  • Database
  • Storage
  • Hosting

Firebase

  • OAuth
  googleLogin(user) {
    let provider = new firebase.auth.GoogleAuthProvider();
    firebase.auth().signInWithRedirect(provider);
    firebase.auth().getRedirectResult(provider).then((result) => {
        //...
      }).catch(function(error) {
      // Handle Errors here.
        var errorCode = error.code;
        var errorMessage = error.message;
        console.log("errorMessage: " + errorMessage);
        // The email of the user's account used.
        var email = error.email;
        // The firebase.auth.AuthCredential type that was used.
        var credential = error.credential;
        // ...
      })
   }

Firebase

  • Database
const currentTime = new Date().getTime();
let deleteOverDue = admin.database().ref().child(Announcement_Path)
.orderByChild('endDate')
.endAt(currentTime)
.once("value")
.then(snap => {
   sunset = snap.val();
   console.log(sunset);
   if (sunset) {
     return admin.database().ref()
            .child(Announcement_Sunset)
            .update(sunset);
   }
})

Firebase

  • Storage
  •  
const ref = firebase.storage().ref()
let photoRef = ref.child("announcement_photos/" + newdt + "/" + photo.name);
photoRef.put(photo).then((snap) => {
  this.setState(prevState => ({
    content : prevState.content + `\n\n<img alt="${photo.name}" src="${snap.downloadURL}" width="50%">`
  }))
})

Firebase

  • Hosting:
    • npm build
    • npm deploy

Cloud functions

 

  • Cloud Firestore Triggers

  • Realtime Database Triggers

  • Firebase Authentication Triggers

  • Google Analytics for Firebase Triggers

  • Crashlytics Triggers

  • Cloud Storage Triggers

  • Cloud Pub/Sub Triggers

  • HTTP Triggers

Cloud functions

 

Cron-job

Cloud functions

 

functions.https.onRequest((req, res) => {
  const key = req.query.key;
  // Exit if the keys don't match
  if (!secureCompare(key, functions.config().cron.key)) {
    console.log('The key provided in the request does not match the key set in the environment. Check that', key,
      'matches the cron.key attribute in `firebase env:get`');
    res.status(403).send('Security key does not match. Make sure your "key" URL query parameter matches the ' +
      'cron.key environment variable.');
    return;
  }
  const currentTime = new Date().getTime();
  let newAnnouncement;
  let sunset;
  //check startDate in raw data 
  let addWaitedList = admin.database().ref().child(Announcement_Raw_Data).orderByChild('startDate').endAt(currentTime).once("value")
    .then(snap => {
      newAnnouncement = snap.val();
      console.log(newAnnouncement);
      if (newAnnouncement) {
        return admin.database().ref().child(Announcement_Path).update(newAnnouncement)
      }
    }).then(() => {
      let update = {}
      Object.keys(newAnnouncement).forEach((uuid) => {
        update[uuid] = null;
      });
      console.log(Object.keys(newAnnouncement).length + " announcements have been updated");
      admin.database().ref(Announcement_Raw_Data).update(update);
    }).catch(err => {
      res.send(err)
    })
    //check endDate in announcements
  let deleteOverDue = admin.database().ref().child(Announcement_Path).orderByChild('endDate').endAt(currentTime).once("value")
    .then(snap => {
      sunset = snap.val()
      console.log(sunset)
      if (sunset) {
        return admin.database().ref().child(Announcement_Sunset).update(sunset)
      }
    }).then(() => {
      if (sunset) {
        let update = {}
        Object.keys(sunset).forEach((uuid) => {
          update[uuid] = null;
        });
        console.log(Object.keys(sunset).length + " announcements have been sunset");
        return admin.database().ref().child(Announcement_Path).update(update);
      } else {
        console.log("No announcements need to be sunset")
      }

    }).catch(err => {
      res.send(err)
    })
  Promise.all([addWaitedList, deleteOverDue]).then(value => {
    console.log(`Cron job for ${new Date()} has been completed`)
  });
})

Sendgrid

  • Easy-to-integrate APIs
  • Transaction templates
  • Version control

Sendgrid

Hello -name-,
 
A new team application was submitted, please go to the dashboard to checkout.
<%body%>
 
VIP Email Service
 

Template

Sendgrid

 Object.keys(adminLists).forEach((uuid) => {
          let request = sg.emptyRequest({
            method: 'POST',
            path: '/v3/mail/send',
            body: {
              personalizations: [{
                to: [{ email: adminLists[uuid].email }],
                'substitutions': {
                  '-name-': adminLists[uuid].name,
                },
                subject: 'A new team application is submitted'
              }],
              from: {
                email: 'noreply@vip.udel.edu'
              },
              content: [{
                type: 'text/html',
                value: `<p>Applicaiton information:</p> ${formatted.join("")}`
              }],
              'template_id': functions.config().sendgrid.templateid,
            }
          });
          // With promise
          sg.API(request)
            .then(function(response) {
              console.log(response.statusCode);
              console.log(response.body);
              console.log(response.headers);
            })
            .catch(function(error) {
              // error is an instance of SendGridError
              // The full response is attached to error.response
              console.log(error.response.statusCode);
            });
        });

API

VIP Web

By Henry Zhao