/Prana

Primary LanguageRuby

Prana

Live version: Prana

Prana is a clone of the popular task management website Asana. It helps organizations arrange tasks into teams and projects. It is built with a RoR backend, PostgreSQL database, a React/Redux frontend architecture.

Features

  • User Authentication
    • Prana's user authentication uses the BCrypt gem
  • Team, Project, and Task CRUD
  • Live forms for presentation

Implementation

The team, project, and task states all share some identical slices and presentational attributes. Prana takes advantage of this by using the same index, show, and form React presentational components to display all three. The differences are handled by duck-typing, conditional rendering based on type, and type-based class names for styling.

Below is the render function for comp_index_item.js. For teams and projects, it renders buttons for the lists in the side nav bar. For tasks, it renders the TaskFormContainer so it can be edited.

render() {
  const comp = this.props.comp;
  const type = this.props.type;
  const Container = this.props.Container;
  return (
    <li className={`${type}-index-item`}>
      { type === "task" ? (
        <Container type={type} comp={this.props.comp} projectId={this.props.projectId} />
      ) : (
        <button onClick={this.handleClick}>{comp.name}</button>
      ) }
    </li>
  );
}

Below is a snippet of the shared form. All three types share a name input while only tasks an taskdetails have a completion checkbox and only a taskdetail has a description.

<form
  className={`${type}-form`}
  key={currId}
  onSubmit={this.handleSubmit} >
  <div className={`${type}-form-header`}>
    <input
      className={`${type}-name-input ${type}-form-input`}
      ref={(input) => { this.nameInput = input; }}
      type="text"
      onKeyDown={this.handleKeyDown("name", this.nameInput)}
      onChange={this.handleChange("name")}
      onFocus={this.handleFocus}
      onBlur={this.handleOnBlur}
      placeholder={`new ${name === "taskdetail" ? "task" : name}` }
      value={this.state.name} />
      {
        (type === "task" || type === "taskdetail") && currId ? (
          <input
            className="finished-button"
            type="checkbox"
            checked={this.state.finished}
            onChange={this.handleFinish} />
        ) : ("")
      }
  </div>
    {
      type === "taskdetail" ? (
        <textarea
          className={`${type}-description-input ${type}-form-input`}
          ref={(input) => { this.descInput = input; }}
          type="text"
          onKeyDown={this.handleKeyDown("description", this.descInput)}
          onChange={this.handleChange("description")}
          onFocus={this.handleFocus}
          onBlur={this.handleOnBlur}
          placeholder="description"
          value={this.state.description} />
      ) : ("")
    }
</form>

Duck-typed functions from the containers (postComp, patchComp) and conditionals handle the differences in types and actions to be taken.

handleOnBlur() {
  if (this.state.id) {
    this.patch();
  }
  else {
    this.post();
  }
}

patch() {
  this.props.patchComp(this.state)
    .fail(() => {
      this.setState({ name: this.props.current.name })
    });
}

post() {
  this.props.postComp(this.state)
    .then(resp => {
      if (this.props.type === "task") {
        const currUrl = this.props.match.url.split("/").slice(0,3).join("/");
        this.props.history.push(`${currUrl}/tasks/${resp.task.id}`);
      }
      else {
        this.props.history.push(`/${this.props.type}s/${resp.current.id}`);
      }
    });
}

Future Directions

  • Additional information on task index sections (what team, project, and user is this task associated with)

  • Mailer for inviting org members

  • User subscribing to teams and projects

  • Choose assignee in task detail (not default to current user).

  • Drag and Drop ordering

  • Email verification

  • Two-factor authentication