Inspired by Kickstarter, Quickstarter is a single-page web application where users can start and fund projects. It was built with Ruby on Rails, ES6, React and Redux.
Users can create projects and add rewards on the project form page.
The project form consists of two elements - the Basics form which contains all the project information, and the Rewards form which contains reward information. Users are able to navigate between the two forms without losing previously typed information. The rewards form also contains an "Add a new reward" button which allows users to dynamically add as many rewards as desired.
The challenge was having two different form components (Basics and Rewards), while being able to access information from both in order to create one project. To achieve this, I created a ProjectForm
component that houses the BasicsForm
and RewardsForm
components. ProjectForm
then contains a local state that stores information from both forms.
this.state = {
formType: "basics",
title: "",
description: "",
end_date: "",
funding_goal: 0,
details: "",
category_id: 0,
rewardsNums: [1],
rewards: {
1: {title: "", description: "", cost: 0, delivery_date: ""}
},
imageFile: null,
imageUrl: null
};
The state also keeps track of the formType which updates when a user clicks on either the Basics or Rewards navigation buttons. This information is passed down to the child components, which only render when the formType matches their own ("basics" for the BasicsForm
and "rewards" for the RewardsForm
).
In the backend, I implemented the accepts_nested_attributes_for
ActiveRecord method in the Project
model in order to create projects and rewards simultaneously while nesting rewards with their associated projects. My ProjectsController
accounts for this as well.
def project_params
params
.require(:project)
.permit(:title, :image, :url, :description, :end_date, :funding_goal, :details, :category_id, rewards_attributes: [:title, :description, :cost, :delivery_date])
end
When a user submits a project, the data passed into the createProject
action also contains the rewards.
handleSubmit(e) {
e.preventDefault();
const formData = new FormData();
formData.append("project[title]", this.state.title);
formData.append("project[description]", this.state.description);
formData.append("project[end_date]", this.state.end_date);
formData.append("project[funding_goal]", this.state.funding_goal);
formData.append("project[details]", this.state.details);
formData.append("project[category_id]", this.state.category_id);
formData.append("project[rewards_attributes]", JSON.stringify(values(this.state.rewards)));
if (this.state.imageFile) {
formData.append("project[image]", this.state.imageFile);
}
if (this.props.project) {
this.props.updateProject(this.props.project.id, formData)
.then(data => this.props.history.push(`/projects/${data.project.id}`));
} else {
this.props.createProject(formData)
.then(data => this.props.history.push(`/projects/${data.project.id}`));
}
}
In order to keep my code DRY, I use the ProjectForm
component for my project edit functionality as well, which is why the handleSubmit
function checks for a project in the props. If there is a project, the updateProject
action is fired. Otherwise, the createProject
action is fired.
Here, I needed the capability to add rewards and also access new rewards in my ProjectForm
. I achieved this by first keeping track of the rewardsNums
in my ProjectForm
state. When a user clicks the "Add a new reward" button, the updateReward
function is invoked. updateReward
is a function passed down from ProjectForm
to RewardsForm
as a prop, and is actually bound to ProjectForm
, setting its state.
updateReward(rewardNum, field) {
if (this.state.rewardsNums.includes(rewardNum)) {
return e => {
this.setState({rewards: merge({}, this.state.rewards, {[rewardNum]: {[field]: e.currentTarget.value}})});
};
} else {
let rewardsNums = this.state.rewardsNums.slice();
rewardsNums.push(rewardNum);
const newRewards = merge({}, this.state.rewards, {[rewardNum]: {title: "", description: "", cost: 0, delivery_date: ""}});
this.setState({rewardsNums, rewards: newRewards});
}
}
The ProjectForm
passes the new state to RewardsForm
, where it renders new RewardsFormItem
s based on rewardsNums
.
{this.props.state.rewardsNums.map(
num => <RewardsFormItem key={num} rewardNum={num} updateReward={this.props.updateReward} state={this.props.state} />
)}
Users can make a live search for projects whose titles, descriptions, details, or project creators match the search.
In order to implement a live search, I added a listener for changes to the search input. Each change fires an AJAX request to fetch search results. The SearchForm
component receives the results as props and renders the new results each time.
In the backend I created a search route nested under projects.
resources :projects, except: [:new, :edit] do
get "search", on: :collection
end
The ProjectsController
has a search method that makes an ActiveRecord query for case-insensitve matches.
def search
search = params[:search].downcase
if params[:search].present?
@projects = Project
.joins(:creator)
.where(
"lower(title) ~ :search OR lower(description) ~ :search OR lower(details) ~ :search OR lower(users.name) ~ :search",
{search: search})
render :search
end
end
Users can make a pledge to either projects or rewards.
This is accomplished with polymorphic associations between the Pledge
, Project
, and Reward
models.
Pledge
model:
class Pledge < ApplicationRecord
validates :amount, :pledgeable_id, :pledgeable_type, :backer_id, presence: true
validates_numericality_of :amount, greater_than: 0
belongs_to :pledgeable, polymorphic: true
belongs_to :backer,
class_name: :User,
primary_key: :id,
foreign_key: :backer_id
end
Both Project
and Reward
models have the following association:
has_many :pledges, as: :pledgeable
I also wanted to create an interactive experience for users making a pledge. Clicking a reward or project pledge box opens up a form and highlights the border to indicate activity. To achieve this, I nested a RewardPledgeForm
component inside my RewardListItem
component. RewardListItem
's local state indicates whether or not a form should be rendered.
I plan on continuing to improve upon the already implemented features and also adding the features below.
Users will be able to "like" projects so they can quickly save and reference the projects they've liked.
In order for users to keep track of their activity, I plan on building out the user profile. Users will be able to see the projects they've started as well as funded. They will also be able to upload an avatar photo and change account details.
I plan on adding credit card payment and authentication functionality to fully equip the app for consumer use.