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.
- User Authentication
- Prana's user authentication uses the
BCrypt
gem
- Prana's user authentication uses the
- Team, Project, and Task CRUD
- Live forms for presentation
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 type
s share a name input while only task
s an taskdetail
s 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 type
s 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}`);
}
});
}
-
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