├── log └── .keep ├── app ├── mailers │ └── .keep ├── models │ ├── .keep │ ├── concerns │ │ └── .keep │ ├── reward.rb │ ├── contribution.rb │ ├── category.rb │ ├── campaign.rb │ └── user.rb ├── assets │ ├── images │ │ ├── .keep │ │ ├── 1.png │ │ ├── 2.png │ │ ├── 3.png │ │ ├── 4.png │ │ ├── back.png │ │ ├── Github.png │ │ ├── forward.png │ │ ├── future.png │ │ ├── gleft.png │ │ ├── gright.png │ │ ├── party.png │ │ ├── briefcase.png │ │ ├── coachella.png │ │ ├── creative.png │ │ ├── gardenbot.png │ │ ├── linkedin.png │ │ ├── IndieFomoFavicon.ico │ │ ├── categories │ │ │ ├── health.png │ │ │ ├── home.png │ │ │ ├── music.png │ │ │ ├── travel.png │ │ │ ├── fashion.png │ │ │ ├── outdoors.png │ │ │ ├── projects.png │ │ │ └── technology.png │ │ ├── img_placeholder.png │ │ ├── minimalistic_shelf.png │ │ ├── profile_placeholder.png │ │ ├── GitHub-Mark-Light-32px.png │ │ └── github.svg │ ├── stylesheets │ │ ├── table.css │ │ ├── errors.css │ │ ├── api │ │ │ ├── rewards.scss │ │ │ ├── session.scss │ │ │ ├── users.scss │ │ │ ├── campaigns.scss │ │ │ ├── contributions.scss │ │ │ ├── reset.css │ │ │ └── modal_style.js │ │ ├── categories.scss │ │ ├── contributions.scss │ │ ├── static_pages_controller.scss │ │ ├── user_contributions_list.css │ │ ├── progress_bar.css │ │ ├── footer.css │ │ ├── user_campaign_list.css │ │ ├── searchbar.css │ │ ├── category_show.css │ │ ├── modal_style.js │ │ ├── campaign_new.css │ │ ├── home_page.css │ │ ├── categories.css │ │ ├── campaign_edit.css │ │ ├── reward_tile.css │ │ ├── newRewards.css │ │ ├── campaign_index.scss │ │ ├── discovery.css │ │ └── campaign_show.css │ └── javascripts │ │ ├── api │ │ ├── users.coffee │ │ ├── campaigns.coffee │ │ ├── rewards.coffee │ │ ├── session.coffee │ │ └── contributions.coffee │ │ ├── campaigns.coffee │ │ ├── categories.coffee │ │ ├── search.coffee │ │ ├── contributions.coffee │ │ ├── static_pages_controller.coffee │ │ └── application.js ├── controllers │ ├── concerns │ │ └── .keep │ ├── static_pages_controller.rb │ ├── api │ │ ├── categories_controller.rb │ │ ├── search_controller.rb │ │ ├── sessions_controller.rb │ │ ├── contributions_controller.rb │ │ ├── charges_controller.rb │ │ ├── users_controller.rb │ │ ├── rewards_controller.rb │ │ └── campaigns_controller.rb │ └── application_controller.rb ├── helpers │ ├── search_helper.rb │ ├── api │ │ ├── users_helper.rb │ │ ├── rewards_helper.rb │ │ ├── session_helper.rb │ │ ├── campaigns_helper.rb │ │ └── contributions_helper.rb │ ├── campaigns_helper.rb │ ├── categories_helper.rb │ ├── application_helper.rb │ ├── contributions_helper.rb │ └── static_pages_controller_helper.rb └── views │ ├── api │ ├── sessions │ │ ├── show.json.jbuilder │ │ └── _user.json.jbuilder │ ├── users │ │ ├── show.json.jbuilder │ │ ├── new.json.jbuilder │ │ └── _user.json.jbuilder │ ├── search │ │ └── index.json.jbuilder │ ├── categories │ │ └── index.json.jbuilder │ ├── rewards │ │ ├── show.json.jbuilder │ │ └── index.json.jbuilder │ ├── contributions │ │ └── show.json.jbuilder │ └── campaigns │ │ ├── show.json.jbuilder │ │ └── index.json.jbuilder │ ├── layouts │ └── application.html.erb │ └── static_pages │ └── root.html.erb ├── lib ├── assets │ └── .keep └── tasks │ └── .keep ├── public ├── favicon.ico ├── robots.txt ├── 500.html ├── 422.html └── 404.html ├── test ├── helpers │ └── .keep ├── mailers │ └── .keep ├── models │ ├── .keep │ ├── user_test.rb │ ├── campaign_test.rb │ ├── reward_test.rb │ └── api │ │ ├── category_test.rb │ │ └── contribution_test.rb ├── controllers │ ├── .keep │ ├── search_controller_test.rb │ ├── api │ │ ├── users_controller_test.rb │ │ ├── campaigns_controller_test.rb │ │ ├── rewards_controller_test.rb │ │ ├── session_controller_test.rb │ │ └── contributions_controller_test.rb │ ├── campaigns_controller_test.rb │ ├── categories_controller_test.rb │ ├── contributions_controller_test.rb │ └── static_pages_controller_controller_test.rb ├── fixtures │ ├── .keep │ ├── campaigns.yml │ ├── rewards.yml │ ├── users.yml │ └── api │ │ ├── categories.yml │ │ └── contributions.yml ├── integration │ └── .keep └── test_helper.rb ├── .ruby-version ├── vendor └── assets │ ├── javascripts │ └── .keep │ └── stylesheets │ └── .keep ├── docs ├── images │ ├── rewards.png │ ├── categories.png │ ├── indiefomo.png │ ├── campaign_show.png │ ├── progress-bar.png │ └── campaign-creation.png ├── wireframes │ ├── SignUp.png │ ├── Landing Page.png │ ├── CampaignShowPage.png │ ├── RewardsComponent.png │ ├── CampaignComponent.png │ ├── CampaignIndexPage.png │ ├── CreateACampaignForm.png │ ├── NavigationComponent.png │ ├── UserProfileShowPage.png │ ├── CreateCampaignRewards.png │ ├── UserConrtibutionPage.png │ ├── CampaignCategoryShowPage.png │ └── CreateACampaignForm-Edit.png ├── sample_data.md ├── api-endpoints ├── sample-state.md ├── schema.md ├── PRODUCTION_README.md ├── README.md └── component-hierarchy.md ├── frontend ├── util │ ├── category_api_util.js │ ├── search_api_util.js │ ├── contributions_api_util.js │ ├── user_api_util.js │ ├── auth_api_util.js │ ├── rewards_api_util.js │ └── campaign_api_util.js ├── reducers │ ├── search_reducer.js │ ├── category_reducer.js │ ├── selectors.js │ ├── tile_slider_reducer.js │ ├── user_reducer.js │ ├── session_reducer.js │ ├── root_reducer.js │ ├── rewards_reducer.js │ ├── contribution_reducer.js │ └── campaign_reducer.js ├── store │ └── store.js ├── components │ ├── app.jsx │ ├── categories │ │ ├── category_container.js │ │ ├── category_tile.jsx │ │ ├── category_show_container.js │ │ ├── category_index.jsx │ │ └── category_show.jsx │ ├── campaign │ │ ├── new_campaign_container.js │ │ ├── edit_campaign_container.js │ │ ├── campaign_index_container.js │ │ ├── user_profile_component.jsx │ │ ├── campaign_show_container.js │ │ ├── campaign_index.jsx │ │ ├── progress_block.jsx │ │ ├── campaign_index_item.jsx │ │ ├── campaign_show.jsx │ │ ├── rewards │ │ │ ├── rewards_showpage.jsx │ │ │ └── reward.jsx │ │ └── new_campaign.jsx │ ├── home_page │ │ ├── tile_slider_container.js │ │ ├── discovery.js │ │ ├── home_page.js │ │ ├── carousel_component.jsx │ │ └── tile_slider.jsx │ ├── session │ │ ├── session_form_container.js │ │ └── session_form.jsx │ ├── user │ │ ├── user_container.js │ │ ├── campaigns.jsx │ │ ├── contributions.jsx │ │ ├── profile.jsx │ │ └── user_show.jsx │ ├── header │ │ └── header_container.js │ ├── footer.jsx │ └── root.jsx ├── actions │ ├── search_action.js │ ├── category_actions.js │ ├── user_actions.js │ ├── contribution_actions.js │ ├── session_actions.js │ ├── campaign_actions.js │ └── reward_actions.js └── indie_fomo.jsx ├── bin ├── bundle ├── rake ├── rails ├── spring └── setup ├── config ├── boot.rb ├── initializers │ ├── cookies_serializer.rb │ ├── session_store.rb │ ├── mime_types.rb │ ├── stripe.rb │ ├── filter_parameter_logging.rb │ ├── backtrace_silencers.rb │ ├── assets.rb │ ├── to_time_preserves_timezone.rb │ ├── wrap_parameters.rb │ └── inflections.rb ├── environment.rb ├── database.yml ├── locales │ └── en.yml ├── secrets.yml ├── application.rb ├── environments │ ├── development.rb │ ├── test.rb │ └── production.rb └── routes.rb ├── db └── migrate │ ├── 20170419202534_remove_index.rb │ ├── 20170513234717_add_date_to_contribution.rb │ ├── 20170418215930_drop_index.rb │ ├── 20170419150144_remove_constraints.rb │ ├── 20170514002446_fixdate.rb │ ├── 20170426011902_add_col_to_contribution.rb │ ├── 20170514001735_migration.rb │ ├── 20170425215609_fix_category.rb │ ├── 20170507020926_add_photo_to_categories.rb │ ├── 20170427133706_fix_date_in_campaigns.rb │ ├── 20170421185402_add_attachment_image_to_users.rb │ ├── 20170421192056_add_attachment_image_to_campaigns.rb │ ├── 20170427185626_create_rewards.rb │ ├── 20170425214923_fix.rb │ ├── 20170420142900_create_campaigns.rb │ ├── 20170514165849_create_pg_search_documents.rb │ └── 20170418155621_create_users.rb ├── config.ru ├── Rakefile ├── .gitignore ├── webpack.config.js ├── package.json ├── Guardfile ├── Gemfile └── README.md /log/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/mailers/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/models/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /lib/assets/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /lib/tasks/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/helpers/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/mailers/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/models/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.ruby-version: -------------------------------------------------------------------------------- 1 | 2.3.1 2 | -------------------------------------------------------------------------------- /app/assets/images/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/controllers/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/fixtures/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/integration/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/models/concerns/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/controllers/concerns/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /vendor/assets/javascripts/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /vendor/assets/stylesheets/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/helpers/search_helper.rb: -------------------------------------------------------------------------------- 1 | module SearchHelper 2 | end 3 | -------------------------------------------------------------------------------- /app/helpers/api/users_helper.rb: -------------------------------------------------------------------------------- 1 | module Api::UsersHelper 2 | end 3 | -------------------------------------------------------------------------------- /app/helpers/campaigns_helper.rb: -------------------------------------------------------------------------------- 1 | module CampaignsHelper 2 | end 3 | -------------------------------------------------------------------------------- /app/helpers/categories_helper.rb: -------------------------------------------------------------------------------- 1 | module CategoriesHelper 2 | end 3 | -------------------------------------------------------------------------------- /app/assets/stylesheets/table.css: -------------------------------------------------------------------------------- 1 | .table { 2 | width: 100%; 3 | } 4 | -------------------------------------------------------------------------------- /app/helpers/api/rewards_helper.rb: -------------------------------------------------------------------------------- 1 | module Api::RewardsHelper 2 | end 3 | -------------------------------------------------------------------------------- /app/helpers/api/session_helper.rb: -------------------------------------------------------------------------------- 1 | module Api::SessionHelper 2 | end 3 | -------------------------------------------------------------------------------- /app/helpers/application_helper.rb: -------------------------------------------------------------------------------- 1 | module ApplicationHelper 2 | end 3 | -------------------------------------------------------------------------------- /app/helpers/api/campaigns_helper.rb: -------------------------------------------------------------------------------- 1 | module Api::CampaignsHelper 2 | end 3 | -------------------------------------------------------------------------------- /app/helpers/contributions_helper.rb: -------------------------------------------------------------------------------- 1 | module ContributionsHelper 2 | end 3 | -------------------------------------------------------------------------------- /app/helpers/api/contributions_helper.rb: -------------------------------------------------------------------------------- 1 | module Api::ContributionsHelper 2 | end 3 | -------------------------------------------------------------------------------- /app/helpers/static_pages_controller_helper.rb: -------------------------------------------------------------------------------- 1 | module StaticPagesControllerHelper 2 | end 3 | -------------------------------------------------------------------------------- /app/models/reward.rb: -------------------------------------------------------------------------------- 1 | class Reward < ActiveRecord::Base 2 | belongs_to :campaign 3 | end 4 | -------------------------------------------------------------------------------- /app/assets/images/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/plupinska/IndieFomo/HEAD/app/assets/images/1.png -------------------------------------------------------------------------------- /app/assets/images/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/plupinska/IndieFomo/HEAD/app/assets/images/2.png -------------------------------------------------------------------------------- /app/assets/images/3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/plupinska/IndieFomo/HEAD/app/assets/images/3.png -------------------------------------------------------------------------------- /app/assets/images/4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/plupinska/IndieFomo/HEAD/app/assets/images/4.png -------------------------------------------------------------------------------- /docs/images/rewards.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/plupinska/IndieFomo/HEAD/docs/images/rewards.png -------------------------------------------------------------------------------- /app/assets/images/back.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/plupinska/IndieFomo/HEAD/app/assets/images/back.png -------------------------------------------------------------------------------- /app/views/api/sessions/show.json.jbuilder: -------------------------------------------------------------------------------- 1 | json.partial! "api/sessions/user", :locals => {user: @user} 2 | -------------------------------------------------------------------------------- /docs/images/categories.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/plupinska/IndieFomo/HEAD/docs/images/categories.png -------------------------------------------------------------------------------- /docs/images/indiefomo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/plupinska/IndieFomo/HEAD/docs/images/indiefomo.png -------------------------------------------------------------------------------- /docs/wireframes/SignUp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/plupinska/IndieFomo/HEAD/docs/wireframes/SignUp.png -------------------------------------------------------------------------------- /app/assets/images/Github.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/plupinska/IndieFomo/HEAD/app/assets/images/Github.png -------------------------------------------------------------------------------- /app/assets/images/forward.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/plupinska/IndieFomo/HEAD/app/assets/images/forward.png -------------------------------------------------------------------------------- /app/assets/images/future.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/plupinska/IndieFomo/HEAD/app/assets/images/future.png -------------------------------------------------------------------------------- /app/assets/images/gleft.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/plupinska/IndieFomo/HEAD/app/assets/images/gleft.png -------------------------------------------------------------------------------- /app/assets/images/gright.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/plupinska/IndieFomo/HEAD/app/assets/images/gright.png -------------------------------------------------------------------------------- /app/assets/images/party.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/plupinska/IndieFomo/HEAD/app/assets/images/party.png -------------------------------------------------------------------------------- /docs/images/campaign_show.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/plupinska/IndieFomo/HEAD/docs/images/campaign_show.png -------------------------------------------------------------------------------- /docs/images/progress-bar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/plupinska/IndieFomo/HEAD/docs/images/progress-bar.png -------------------------------------------------------------------------------- /app/assets/images/briefcase.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/plupinska/IndieFomo/HEAD/app/assets/images/briefcase.png -------------------------------------------------------------------------------- /app/assets/images/coachella.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/plupinska/IndieFomo/HEAD/app/assets/images/coachella.png -------------------------------------------------------------------------------- /app/assets/images/creative.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/plupinska/IndieFomo/HEAD/app/assets/images/creative.png -------------------------------------------------------------------------------- /app/assets/images/gardenbot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/plupinska/IndieFomo/HEAD/app/assets/images/gardenbot.png -------------------------------------------------------------------------------- /app/assets/images/linkedin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/plupinska/IndieFomo/HEAD/app/assets/images/linkedin.png -------------------------------------------------------------------------------- /docs/images/campaign-creation.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/plupinska/IndieFomo/HEAD/docs/images/campaign-creation.png -------------------------------------------------------------------------------- /docs/wireframes/Landing Page.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/plupinska/IndieFomo/HEAD/docs/wireframes/Landing Page.png -------------------------------------------------------------------------------- /docs/wireframes/CampaignShowPage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/plupinska/IndieFomo/HEAD/docs/wireframes/CampaignShowPage.png -------------------------------------------------------------------------------- /docs/wireframes/RewardsComponent.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/plupinska/IndieFomo/HEAD/docs/wireframes/RewardsComponent.png -------------------------------------------------------------------------------- /app/assets/images/IndieFomoFavicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/plupinska/IndieFomo/HEAD/app/assets/images/IndieFomoFavicon.ico -------------------------------------------------------------------------------- /app/assets/images/categories/health.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/plupinska/IndieFomo/HEAD/app/assets/images/categories/health.png -------------------------------------------------------------------------------- /app/assets/images/categories/home.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/plupinska/IndieFomo/HEAD/app/assets/images/categories/home.png -------------------------------------------------------------------------------- /app/assets/images/categories/music.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/plupinska/IndieFomo/HEAD/app/assets/images/categories/music.png -------------------------------------------------------------------------------- /app/assets/images/categories/travel.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/plupinska/IndieFomo/HEAD/app/assets/images/categories/travel.png -------------------------------------------------------------------------------- /app/assets/images/img_placeholder.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/plupinska/IndieFomo/HEAD/app/assets/images/img_placeholder.png -------------------------------------------------------------------------------- /app/controllers/static_pages_controller.rb: -------------------------------------------------------------------------------- 1 | class StaticPagesController < ApplicationController 2 | def root 3 | end 4 | end 5 | -------------------------------------------------------------------------------- /app/views/api/users/show.json.jbuilder: -------------------------------------------------------------------------------- 1 | json.partial! "api/users/user", :locals => {user: @user, contributions: @contributions} 2 | -------------------------------------------------------------------------------- /docs/wireframes/CampaignComponent.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/plupinska/IndieFomo/HEAD/docs/wireframes/CampaignComponent.png -------------------------------------------------------------------------------- /docs/wireframes/CampaignIndexPage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/plupinska/IndieFomo/HEAD/docs/wireframes/CampaignIndexPage.png -------------------------------------------------------------------------------- /docs/wireframes/CreateACampaignForm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/plupinska/IndieFomo/HEAD/docs/wireframes/CreateACampaignForm.png -------------------------------------------------------------------------------- /docs/wireframes/NavigationComponent.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/plupinska/IndieFomo/HEAD/docs/wireframes/NavigationComponent.png -------------------------------------------------------------------------------- /docs/wireframes/UserProfileShowPage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/plupinska/IndieFomo/HEAD/docs/wireframes/UserProfileShowPage.png -------------------------------------------------------------------------------- /app/assets/images/categories/fashion.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/plupinska/IndieFomo/HEAD/app/assets/images/categories/fashion.png -------------------------------------------------------------------------------- /app/assets/images/categories/outdoors.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/plupinska/IndieFomo/HEAD/app/assets/images/categories/outdoors.png -------------------------------------------------------------------------------- /app/assets/images/categories/projects.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/plupinska/IndieFomo/HEAD/app/assets/images/categories/projects.png -------------------------------------------------------------------------------- /app/assets/images/minimalistic_shelf.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/plupinska/IndieFomo/HEAD/app/assets/images/minimalistic_shelf.png -------------------------------------------------------------------------------- /app/assets/images/profile_placeholder.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/plupinska/IndieFomo/HEAD/app/assets/images/profile_placeholder.png -------------------------------------------------------------------------------- /docs/sample_data.md: -------------------------------------------------------------------------------- 1 | p = User.create(username: 'p', password: 'password', first_name: 'P', 2 | last_name: 'L', email: 'pl@gmail.com') 3 | -------------------------------------------------------------------------------- /docs/wireframes/CreateCampaignRewards.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/plupinska/IndieFomo/HEAD/docs/wireframes/CreateCampaignRewards.png -------------------------------------------------------------------------------- /docs/wireframes/UserConrtibutionPage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/plupinska/IndieFomo/HEAD/docs/wireframes/UserConrtibutionPage.png -------------------------------------------------------------------------------- /app/assets/images/GitHub-Mark-Light-32px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/plupinska/IndieFomo/HEAD/app/assets/images/GitHub-Mark-Light-32px.png -------------------------------------------------------------------------------- /app/assets/images/categories/technology.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/plupinska/IndieFomo/HEAD/app/assets/images/categories/technology.png -------------------------------------------------------------------------------- /docs/wireframes/CampaignCategoryShowPage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/plupinska/IndieFomo/HEAD/docs/wireframes/CampaignCategoryShowPage.png -------------------------------------------------------------------------------- /docs/wireframes/CreateACampaignForm-Edit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/plupinska/IndieFomo/HEAD/docs/wireframes/CreateACampaignForm-Edit.png -------------------------------------------------------------------------------- /frontend/util/category_api_util.js: -------------------------------------------------------------------------------- 1 | export const getCategories = () => { 2 | return $.ajax({ 3 | url: '/api/categories' 4 | }); 5 | }; 6 | -------------------------------------------------------------------------------- /app/views/api/users/new.json.jbuilder: -------------------------------------------------------------------------------- 1 | if @user 2 | json.extract! @user, :first_name, :last_name, :email, :id 3 | else 4 | {user: null} 5 | end 6 | -------------------------------------------------------------------------------- /bin/bundle: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__) 3 | load Gem.bin_path('bundler', 'bundle') 4 | -------------------------------------------------------------------------------- /app/views/api/search/index.json.jbuilder: -------------------------------------------------------------------------------- 1 | 2 | 3 | json.array! @campaigns do |campaign| 4 | json.id campaign.id 5 | json.title campaign.title 6 | end 7 | -------------------------------------------------------------------------------- /config/boot.rb: -------------------------------------------------------------------------------- 1 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__) 2 | 3 | require 'bundler/setup' # Set up gems listed in the Gemfile. 4 | -------------------------------------------------------------------------------- /app/assets/stylesheets/errors.css: -------------------------------------------------------------------------------- 1 | .new-campaign-errors { 2 | box-sizing: border-box; 3 | height: 10px; 4 | padding-left: 50px; 5 | color: red; 6 | } 7 | -------------------------------------------------------------------------------- /app/views/api/sessions/_user.json.jbuilder: -------------------------------------------------------------------------------- 1 | 2 | if user 3 | json.extract! user, :first_name, :last_name, :email, :id 4 | else 5 | {user: null} 6 | end 7 | -------------------------------------------------------------------------------- /db/migrate/20170419202534_remove_index.rb: -------------------------------------------------------------------------------- 1 | class RemoveIndex < ActiveRecord::Migration 2 | def change 3 | remove_column :users, :username 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /test/models/user_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class UserTest < ActiveSupport::TestCase 4 | # test "the truth" do 5 | # assert true 6 | # end 7 | end 8 | -------------------------------------------------------------------------------- /config.ru: -------------------------------------------------------------------------------- 1 | # This file is used by Rack-based servers to start the application. 2 | 3 | require ::File.expand_path('../config/environment', __FILE__) 4 | run Rails.application 5 | -------------------------------------------------------------------------------- /test/models/campaign_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class CampaignTest < ActiveSupport::TestCase 4 | # test "the truth" do 5 | # assert true 6 | # end 7 | end 8 | -------------------------------------------------------------------------------- /test/models/reward_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class RewardTest < ActiveSupport::TestCase 4 | # test "the truth" do 5 | # assert true 6 | # end 7 | end 8 | -------------------------------------------------------------------------------- /config/initializers/cookies_serializer.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | Rails.application.config.action_dispatch.cookies_serializer = :json 4 | -------------------------------------------------------------------------------- /test/models/api/category_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class Api::CategoryTest < ActiveSupport::TestCase 4 | # test "the truth" do 5 | # assert true 6 | # end 7 | end 8 | -------------------------------------------------------------------------------- /config/environment.rb: -------------------------------------------------------------------------------- 1 | # Load the Rails application. 2 | require File.expand_path('../application', __FILE__) 3 | 4 | # Initialize the Rails application. 5 | Rails.application.initialize! 6 | -------------------------------------------------------------------------------- /config/initializers/session_store.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | Rails.application.config.session_store :cookie_store, key: '_IndieFomo_session' 4 | -------------------------------------------------------------------------------- /app/views/api/categories/index.json.jbuilder: -------------------------------------------------------------------------------- 1 | 2 | json.array! @categories do |category| 3 | 4 | json.id category.id 5 | json.cat category.cat 6 | json.image category.image 7 | end 8 | -------------------------------------------------------------------------------- /db/migrate/20170513234717_add_date_to_contribution.rb: -------------------------------------------------------------------------------- 1 | class AddDateToContribution < ActiveRecord::Migration 2 | def change 3 | add_column :contributions, :date, :date 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /test/models/api/contribution_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class Api::ContributionTest < ActiveSupport::TestCase 4 | # test "the truth" do 5 | # assert true 6 | # end 7 | end 8 | -------------------------------------------------------------------------------- /db/migrate/20170418215930_drop_index.rb: -------------------------------------------------------------------------------- 1 | class DropIndex < ActiveRecord::Migration 2 | def change 3 | remove_index :users, :first_name 4 | remove_index :users, :last_name 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /db/migrate/20170419150144_remove_constraints.rb: -------------------------------------------------------------------------------- 1 | class RemoveConstraints < ActiveRecord::Migration 2 | def change 3 | change_column :users, :username, :string, :null => true 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /frontend/util/search_api_util.js: -------------------------------------------------------------------------------- 1 | export const searchCampaigns = (searchField) => { 2 | 3 | return $.ajax({ 4 | url: 'api/search', 5 | data: {searchField: searchField} 6 | }); 7 | }; 8 | -------------------------------------------------------------------------------- /test/controllers/search_controller_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class SearchControllerTest < ActionController::TestCase 4 | # test "the truth" do 5 | # assert true 6 | # end 7 | end 8 | -------------------------------------------------------------------------------- /app/views/api/rewards/show.json.jbuilder: -------------------------------------------------------------------------------- 1 | 2 | json.campaign_id @reward.campaign_id 3 | json.title @reward.title 4 | json.id @reward.id 5 | json.description @reward.description 6 | json.price @reward.price 7 | -------------------------------------------------------------------------------- /config/initializers/mime_types.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Add new mime types for use in respond_to blocks: 4 | # Mime::Type.register "text/richtext", :rtf 5 | -------------------------------------------------------------------------------- /db/migrate/20170514002446_fixdate.rb: -------------------------------------------------------------------------------- 1 | class Fixdate < ActiveRecord::Migration 2 | def change 3 | remove_column :contributions, :end_date 4 | add_column :contributions, :date, :string 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /test/controllers/api/users_controller_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class Api::UsersControllerTest < ActionController::TestCase 4 | # test "the truth" do 5 | # assert true 6 | # end 7 | end 8 | -------------------------------------------------------------------------------- /test/controllers/campaigns_controller_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class CampaignsControllerTest < ActionController::TestCase 4 | # test "the truth" do 5 | # assert true 6 | # end 7 | end 8 | -------------------------------------------------------------------------------- /test/controllers/categories_controller_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class CategoriesControllerTest < ActionController::TestCase 4 | # test "the truth" do 5 | # assert true 6 | # end 7 | end 8 | -------------------------------------------------------------------------------- /db/migrate/20170426011902_add_col_to_contribution.rb: -------------------------------------------------------------------------------- 1 | class AddColToContribution < ActiveRecord::Migration 2 | def change 3 | add_column :contributions, :campaign_id, :integer, null: false 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20170514001735_migration.rb: -------------------------------------------------------------------------------- 1 | class Migration < ActiveRecord::Migration 2 | def change 3 | remove_column :contributions, :date 4 | add_column :contributions, :end_date, :string 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /test/controllers/api/campaigns_controller_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class Api::CampaignsControllerTest < ActionController::TestCase 4 | # test "the truth" do 5 | # assert true 6 | # end 7 | end 8 | -------------------------------------------------------------------------------- /test/controllers/api/rewards_controller_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class Api::RewardsControllerTest < ActionController::TestCase 4 | # test "the truth" do 5 | # assert true 6 | # end 7 | end 8 | -------------------------------------------------------------------------------- /test/controllers/api/session_controller_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class Api::SessionControllerTest < ActionController::TestCase 4 | # test "the truth" do 5 | # assert true 6 | # end 7 | end 8 | -------------------------------------------------------------------------------- /test/controllers/contributions_controller_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class ContributionsControllerTest < ActionController::TestCase 4 | # test "the truth" do 5 | # assert true 6 | # end 7 | end 8 | -------------------------------------------------------------------------------- /db/migrate/20170425215609_fix_category.rb: -------------------------------------------------------------------------------- 1 | class FixCategory < ActiveRecord::Migration 2 | def change 3 | remove_column :categories, :type 4 | add_column :categories, :cat, :string, null: false 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /db/migrate/20170507020926_add_photo_to_categories.rb: -------------------------------------------------------------------------------- 1 | class AddPhotoToCategories < ActiveRecord::Migration 2 | def change 3 | change_table :categories do |t| 4 | t.attachment :image 5 | end 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /test/controllers/api/contributions_controller_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class Api::ContributionsControllerTest < ActionController::TestCase 4 | # test "the truth" do 5 | # assert true 6 | # end 7 | end 8 | -------------------------------------------------------------------------------- /app/assets/stylesheets/api/rewards.scss: -------------------------------------------------------------------------------- 1 | // Place all the styles related to the api/rewards controller here. 2 | // They will automatically be included in application.css. 3 | // You can use Sass (SCSS) here: http://sass-lang.com/ 4 | -------------------------------------------------------------------------------- /app/assets/stylesheets/api/session.scss: -------------------------------------------------------------------------------- 1 | // Place all the styles related to the api/session controller here. 2 | // They will automatically be included in application.css. 3 | // You can use Sass (SCSS) here: http://sass-lang.com/ 4 | -------------------------------------------------------------------------------- /app/assets/stylesheets/api/users.scss: -------------------------------------------------------------------------------- 1 | // Place all the styles related to the api/users controller here. 2 | // They will automatically be included in application.css. 3 | // You can use Sass (SCSS) here: http://sass-lang.com/ 4 | -------------------------------------------------------------------------------- /app/assets/stylesheets/categories.scss: -------------------------------------------------------------------------------- 1 | // Place all the styles related to the Categories controller here. 2 | // They will automatically be included in application.css. 3 | // You can use Sass (SCSS) here: http://sass-lang.com/ 4 | -------------------------------------------------------------------------------- /app/assets/stylesheets/api/campaigns.scss: -------------------------------------------------------------------------------- 1 | // Place all the styles related to the api/campaigns controller here. 2 | // They will automatically be included in application.css. 3 | // You can use Sass (SCSS) here: http://sass-lang.com/ 4 | -------------------------------------------------------------------------------- /app/assets/stylesheets/contributions.scss: -------------------------------------------------------------------------------- 1 | // Place all the styles related to the contributions controller here. 2 | // They will automatically be included in application.css. 3 | // You can use Sass (SCSS) here: http://sass-lang.com/ 4 | -------------------------------------------------------------------------------- /db/migrate/20170427133706_fix_date_in_campaigns.rb: -------------------------------------------------------------------------------- 1 | class FixDateInCampaigns < ActiveRecord::Migration 2 | def change 3 | remove_column :campaigns, :end_date 4 | add_column :campaigns, :end_date, :datetime 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /test/controllers/static_pages_controller_controller_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class StaticPagesControllerControllerTest < ActionController::TestCase 4 | # test "the truth" do 5 | # assert true 6 | # end 7 | end 8 | -------------------------------------------------------------------------------- /config/initializers/stripe.rb: -------------------------------------------------------------------------------- 1 | # Rails.configuration.stripe = { 2 | # :publishable_key => ENV['PUBLISHABLE_KEY'], 3 | # :secret_key => ENV['SECRET_KEY'] 4 | # } 5 | # 6 | # Stripe.api_key = Rails.configuration.stripe[:secret_key] 7 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # See http://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file 2 | # 3 | # To ban all spiders from the entire site uncomment the next two lines: 4 | # User-agent: * 5 | # Disallow: / 6 | -------------------------------------------------------------------------------- /app/assets/stylesheets/api/contributions.scss: -------------------------------------------------------------------------------- 1 | // Place all the styles related to the api/contributions controller here. 2 | // They will automatically be included in application.css. 3 | // You can use Sass (SCSS) here: http://sass-lang.com/ 4 | -------------------------------------------------------------------------------- /app/views/api/contributions/show.json.jbuilder: -------------------------------------------------------------------------------- 1 | 2 | json.user_id @contribution.user_id 3 | json.reward_id @contribution.reward_id 4 | json.amount @contribution.amount 5 | json.campaign_id @contribution.campaign_id 6 | json.date @contribution.date 7 | -------------------------------------------------------------------------------- /app/assets/stylesheets/static_pages_controller.scss: -------------------------------------------------------------------------------- 1 | // Place all the styles related to the StaticPagesController controller here. 2 | // They will automatically be included in application.css. 3 | // You can use Sass (SCSS) here: http://sass-lang.com/ 4 | -------------------------------------------------------------------------------- /config/initializers/filter_parameter_logging.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Configure sensitive parameters which will be filtered from the log file. 4 | Rails.application.config.filter_parameters += [:password] 5 | -------------------------------------------------------------------------------- /app/assets/javascripts/api/users.coffee: -------------------------------------------------------------------------------- 1 | # Place all the behaviors and hooks related to the matching controller here. 2 | # All this logic will automatically be available in application.js. 3 | # You can use CoffeeScript in this file: http://coffeescript.org/ 4 | -------------------------------------------------------------------------------- /app/assets/javascripts/campaigns.coffee: -------------------------------------------------------------------------------- 1 | # Place all the behaviors and hooks related to the matching controller here. 2 | # All this logic will automatically be available in application.js. 3 | # You can use CoffeeScript in this file: http://coffeescript.org/ 4 | -------------------------------------------------------------------------------- /app/assets/javascripts/categories.coffee: -------------------------------------------------------------------------------- 1 | # Place all the behaviors and hooks related to the matching controller here. 2 | # All this logic will automatically be available in application.js. 3 | # You can use CoffeeScript in this file: http://coffeescript.org/ 4 | -------------------------------------------------------------------------------- /app/assets/javascripts/search.coffee: -------------------------------------------------------------------------------- 1 | # Place all the behaviors and hooks related to the matching controller here. 2 | # All this logic will automatically be available in application.js. 3 | # You can use CoffeeScript in this file: http://coffeescript.org/ 4 | -------------------------------------------------------------------------------- /app/assets/javascripts/api/campaigns.coffee: -------------------------------------------------------------------------------- 1 | # Place all the behaviors and hooks related to the matching controller here. 2 | # All this logic will automatically be available in application.js. 3 | # You can use CoffeeScript in this file: http://coffeescript.org/ 4 | -------------------------------------------------------------------------------- /app/assets/javascripts/api/rewards.coffee: -------------------------------------------------------------------------------- 1 | # Place all the behaviors and hooks related to the matching controller here. 2 | # All this logic will automatically be available in application.js. 3 | # You can use CoffeeScript in this file: http://coffeescript.org/ 4 | -------------------------------------------------------------------------------- /app/assets/javascripts/api/session.coffee: -------------------------------------------------------------------------------- 1 | # Place all the behaviors and hooks related to the matching controller here. 2 | # All this logic will automatically be available in application.js. 3 | # You can use CoffeeScript in this file: http://coffeescript.org/ 4 | -------------------------------------------------------------------------------- /app/assets/javascripts/contributions.coffee: -------------------------------------------------------------------------------- 1 | # Place all the behaviors and hooks related to the matching controller here. 2 | # All this logic will automatically be available in application.js. 3 | # You can use CoffeeScript in this file: http://coffeescript.org/ 4 | -------------------------------------------------------------------------------- /bin/rake: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | begin 3 | load File.expand_path('../spring', __FILE__) 4 | rescue LoadError => e 5 | raise unless e.message.include?('spring') 6 | end 7 | require_relative '../config/boot' 8 | require 'rake' 9 | Rake.application.run 10 | -------------------------------------------------------------------------------- /app/assets/javascripts/api/contributions.coffee: -------------------------------------------------------------------------------- 1 | # Place all the behaviors and hooks related to the matching controller here. 2 | # All this logic will automatically be available in application.js. 3 | # You can use CoffeeScript in this file: http://coffeescript.org/ 4 | -------------------------------------------------------------------------------- /app/assets/javascripts/static_pages_controller.coffee: -------------------------------------------------------------------------------- 1 | # Place all the behaviors and hooks related to the matching controller here. 2 | # All this logic will automatically be available in application.js. 3 | # You can use CoffeeScript in this file: http://coffeescript.org/ 4 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # Add your own tasks in files placed in lib/tasks ending in .rake, 2 | # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake. 3 | 4 | require File.expand_path('../config/application', __FILE__) 5 | 6 | Rails.application.load_tasks 7 | -------------------------------------------------------------------------------- /app/models/contribution.rb: -------------------------------------------------------------------------------- 1 | class Contribution < ActiveRecord::Base 2 | validates_numericality_of :amount, {greater_than_or_equal_to: 1, allow_nil: false, message: "Please enter an amount greater than zero!"} 3 | 4 | belongs_to :user 5 | belongs_to :campaign 6 | belongs_to :reward 7 | end 8 | -------------------------------------------------------------------------------- /bin/rails: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | begin 3 | load File.expand_path('../spring', __FILE__) 4 | rescue LoadError => e 5 | raise unless e.message.include?('spring') 6 | end 7 | APP_PATH = File.expand_path('../../config/application', __FILE__) 8 | require_relative '../config/boot' 9 | require 'rails/commands' 10 | -------------------------------------------------------------------------------- /db/migrate/20170421185402_add_attachment_image_to_users.rb: -------------------------------------------------------------------------------- 1 | class AddAttachmentImageToUsers < ActiveRecord::Migration 2 | def self.up 3 | change_table :users do |t| 4 | t.attachment :image 5 | end 6 | end 7 | 8 | def self.down 9 | remove_attachment :users, :image 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /app/controllers/api/categories_controller.rb: -------------------------------------------------------------------------------- 1 | class Api::CategoriesController < ApplicationController 2 | 3 | def index 4 | @categories = Category.all 5 | if @categories 6 | render :index 7 | else 8 | render json: @categories.errors.full_messages 9 | end 10 | end 11 | 12 | 13 | end 14 | -------------------------------------------------------------------------------- /app/models/category.rb: -------------------------------------------------------------------------------- 1 | class Category < ActiveRecord::Base 2 | include PgSearch 3 | pg_search_scope :search_by_title, :against => [:cat] 4 | has_attached_file :image, default_url: "profile_placeholder.png" 5 | validates_attachment_content_type :image, content_type: /\Aimage\/.*\z/ 6 | 7 | has_many :campaigns 8 | end 9 | -------------------------------------------------------------------------------- /db/migrate/20170421192056_add_attachment_image_to_campaigns.rb: -------------------------------------------------------------------------------- 1 | class AddAttachmentImageToCampaigns < ActiveRecord::Migration 2 | def self.up 3 | change_table :campaigns do |t| 4 | t.attachment :image 5 | end 6 | end 7 | 8 | def self.down 9 | remove_attachment :campaigns, :image 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /app/views/api/rewards/index.json.jbuilder: -------------------------------------------------------------------------------- 1 | 2 | if @campaign 3 | # 4 | @campaign.rewards.each do |reward| 5 | json.id reward.id 6 | json.title reward.title 7 | json.description reward.description 8 | json.price reward.price 9 | json.campaign_id reward.campaign_id 10 | end 11 | else 12 | nil 13 | end 14 | -------------------------------------------------------------------------------- /app/controllers/api/search_controller.rb: -------------------------------------------------------------------------------- 1 | class Api::SearchController < ApplicationController 2 | 3 | def index 4 | 5 | if params[:searchField] 6 | @campaigns = Campaign.search_by_title(params[:searchField]).take(5) 7 | render :index 8 | else 9 | render json: "No results found" 10 | end 11 | end 12 | 13 | end 14 | -------------------------------------------------------------------------------- /frontend/util/contributions_api_util.js: -------------------------------------------------------------------------------- 1 | export const makeContribution = (contribution) => { 2 | 3 | return $.ajax({ 4 | method: 'POST', 5 | url: `api/contributions`, 6 | data: contribution 7 | }); 8 | }; 9 | 10 | export const fetchContribution = (id) => { 11 | return $.ajax({ 12 | url: `api/contributions/${id}` 13 | }); 14 | }; 15 | -------------------------------------------------------------------------------- /db/migrate/20170427185626_create_rewards.rb: -------------------------------------------------------------------------------- 1 | class CreateRewards < ActiveRecord::Migration 2 | def change 3 | create_table :rewards do |t| 4 | t.integer :campaign_id 5 | t.string :title 6 | t.string :description 7 | t.integer :price 8 | t.attachment :image 9 | 10 | t.timestamps null: false 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /test/test_helper.rb: -------------------------------------------------------------------------------- 1 | ENV['RAILS_ENV'] ||= 'test' 2 | require File.expand_path('../../config/environment', __FILE__) 3 | require 'rails/test_help' 4 | 5 | class ActiveSupport::TestCase 6 | # Setup all fixtures in test/fixtures/*.yml for all tests in alphabetical order. 7 | fixtures :all 8 | 9 | # Add more helper methods to be used by all tests here... 10 | end 11 | -------------------------------------------------------------------------------- /frontend/reducers/search_reducer.js: -------------------------------------------------------------------------------- 1 | import { RECEIVE_SEARCH } from '../actions/search_action'; 2 | 3 | const SearchReducer = (state = {}, action) => { 4 | Object.freeze(state); 5 | 6 | switch (action.type) { 7 | case RECEIVE_SEARCH: 8 | return action.campaigns; 9 | default: 10 | return state; 11 | 12 | } 13 | }; 14 | 15 | export default SearchReducer; 16 | -------------------------------------------------------------------------------- /frontend/store/store.js: -------------------------------------------------------------------------------- 1 | import { createStore, applyMiddleware } from 'redux'; 2 | import thunk from 'redux-thunk'; 3 | import RootReducer from '../reducers/root_reducer'; 4 | 5 | const configureStore = (preloadedState = {}) => { 6 | 7 | return createStore( 8 | RootReducer, 9 | preloadedState, 10 | applyMiddleware(thunk) 11 | ); 12 | }; 13 | 14 | export default configureStore; 15 | -------------------------------------------------------------------------------- /frontend/util/user_api_util.js: -------------------------------------------------------------------------------- 1 | export const getUser = (id) => { 2 | 3 | return $.ajax({ 4 | url: `api/users/${id}` 5 | }); 6 | }; 7 | 8 | export const editUser = (user) => { 9 | 10 | return $.ajax({ 11 | url: `api/users/${user.get("user[id]")}`, 12 | method: 'PATCH', 13 | dataType: "json", 14 | contentType: false, 15 | processData: false, 16 | data: user 17 | }); 18 | }; 19 | -------------------------------------------------------------------------------- /test/fixtures/campaigns.yml: -------------------------------------------------------------------------------- 1 | # Read about fixtures at http://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html 2 | 3 | # This model initially had no columns defined. If you add columns to the 4 | # model remove the '{}' from the fixture names and add the columns immediately 5 | # below each fixture, per the syntax in the comments below 6 | # 7 | one: {} 8 | # column: value 9 | # 10 | two: {} 11 | # column: value 12 | -------------------------------------------------------------------------------- /test/fixtures/rewards.yml: -------------------------------------------------------------------------------- 1 | # Read about fixtures at http://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html 2 | 3 | # This model initially had no columns defined. If you add columns to the 4 | # model remove the '{}' from the fixture names and add the columns immediately 5 | # below each fixture, per the syntax in the comments below 6 | # 7 | one: {} 8 | # column: value 9 | # 10 | two: {} 11 | # column: value 12 | -------------------------------------------------------------------------------- /test/fixtures/users.yml: -------------------------------------------------------------------------------- 1 | # Read about fixtures at http://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html 2 | 3 | # This model initially had no columns defined. If you add columns to the 4 | # model remove the '{}' from the fixture names and add the columns immediately 5 | # below each fixture, per the syntax in the comments below 6 | # 7 | one: {} 8 | # column: value 9 | # 10 | two: {} 11 | # column: value 12 | -------------------------------------------------------------------------------- /test/fixtures/api/categories.yml: -------------------------------------------------------------------------------- 1 | # Read about fixtures at http://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html 2 | 3 | # This model initially had no columns defined. If you add columns to the 4 | # model remove the '{}' from the fixture names and add the columns immediately 5 | # below each fixture, per the syntax in the comments below 6 | # 7 | one: {} 8 | # column: value 9 | # 10 | two: {} 11 | # column: value 12 | -------------------------------------------------------------------------------- /frontend/components/app.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import HeaderContainer from './header/header_container'; 3 | import Footer from './footer'; 4 | import { Link } from 'react-router'; 5 | 6 | 7 | const App = ({children}) => { 8 | return( 9 |
10 | 11 | {children} 12 |
14 | ); 15 | }; 16 | 17 | 18 | 19 | export default App; 20 | -------------------------------------------------------------------------------- /frontend/reducers/category_reducer.js: -------------------------------------------------------------------------------- 1 | import {RECEIVE_CATEGORIES, RECEIVE_CATEGORY_ERRORS} from '../actions/category_actions'; 2 | import merge from 'lodash/merge'; 3 | 4 | 5 | const categoryReducer = (state = null, action) => { 6 | switch (action.type) { 7 | case RECEIVE_CATEGORIES: 8 | return action.categories; 9 | default: 10 | return state; 11 | } 12 | }; 13 | 14 | export default categoryReducer; 15 | -------------------------------------------------------------------------------- /test/fixtures/api/contributions.yml: -------------------------------------------------------------------------------- 1 | # Read about fixtures at http://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html 2 | 3 | # This model initially had no columns defined. If you add columns to the 4 | # model remove the '{}' from the fixture names and add the columns immediately 5 | # below each fixture, per the syntax in the comments below 6 | # 7 | one: {} 8 | # column: value 9 | # 10 | two: {} 11 | # column: value 12 | -------------------------------------------------------------------------------- /config/initializers/backtrace_silencers.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # You can add backtrace silencers for libraries that you're using but don't wish to see in your backtraces. 4 | # Rails.backtrace_cleaner.add_silencer { |line| line =~ /my_noisy_library/ } 5 | 6 | # You can also remove all the silencers if you're trying to debug a problem that might stem from framework code. 7 | # Rails.backtrace_cleaner.remove_silencers! 8 | -------------------------------------------------------------------------------- /db/migrate/20170425214923_fix.rb: -------------------------------------------------------------------------------- 1 | class Fix < ActiveRecord::Migration 2 | def change 3 | #NOT NEEDED 4 | # drop_table :category 5 | # drop_table :contribution 6 | 7 | create_table :categories do |t| 8 | t.string :type 9 | 10 | t.timestamps null: false 11 | end 12 | 13 | create_table :contributions do |t| 14 | t.integer :user_id 15 | t.integer :reward_id 16 | t.integer :amount 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /frontend/actions/search_action.js: -------------------------------------------------------------------------------- 1 | import * as SearchApi from '../util/search_api_util'; 2 | export const RECEIVE_SEARCH = "RECEIVE_SEARCH"; 3 | 4 | export const receiveSearch = (campaigns) => { 5 | return { 6 | type: RECEIVE_SEARCH, 7 | campaigns: campaigns 8 | }; 9 | }; 10 | 11 | export const searchCampaigns = (searchField) => (dispatch) => { 12 | return SearchApi.searchCampaigns(searchField) 13 | .then((camps) => dispatch(receiveSearch(camps))); 14 | }; 15 | -------------------------------------------------------------------------------- /db/migrate/20170420142900_create_campaigns.rb: -------------------------------------------------------------------------------- 1 | class CreateCampaigns < ActiveRecord::Migration 2 | def change 3 | create_table :campaigns do |t| 4 | t.integer :user_id, null: false 5 | t.string :title 6 | t.text :descriptions 7 | t.string :tagline 8 | t.integer :category_id 9 | t.string :image_url 10 | t.date :end_date 11 | t.float :target_amount 12 | 13 | t.timestamps null: false 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /app/controllers/api/sessions_controller.rb: -------------------------------------------------------------------------------- 1 | class Api::SessionsController < ApplicationController 2 | 3 | def create 4 | @user = User.find_by_credentials(params[:user][:email], params[:user][:password]) 5 | 6 | if @user 7 | login!(@user) 8 | render :show 9 | else 10 | render json: @user.errors.full_messages, status: 422 11 | end 12 | end 13 | 14 | def destroy 15 | @user = current_user 16 | logout! 17 | render :show 18 | end 19 | 20 | end 21 | -------------------------------------------------------------------------------- /frontend/reducers/selectors.js: -------------------------------------------------------------------------------- 1 | export const selectCampaigns = (state) => { 2 | 3 | if (state.campaigns.campaign) { 4 | return Object.keys(state.campaigns.campaign).map(key => state.campaigns.campaign[key]); 5 | } else { 6 | return [null]; 7 | } 8 | 9 | }; 10 | 11 | export const selectRewards = (state) => { 12 | 13 | if (state.rewards) { 14 | return Object.keys(state.rewards).map(key => state.rewards[key]); 15 | } else { 16 | return [null]; 17 | } 18 | 19 | }; 20 | -------------------------------------------------------------------------------- /frontend/util/auth_api_util.js: -------------------------------------------------------------------------------- 1 | export const signUp = (user) => { 2 | 3 | return $.ajax({ 4 | url: '/api/users', 5 | method: 'POST', 6 | data: {user} 7 | }); 8 | }; 9 | 10 | 11 | export const signIn = (user) => { 12 | 13 | return $.ajax({ 14 | url: '/api/session', 15 | method: 'POST', 16 | data: {user} 17 | }); 18 | }; 19 | 20 | export const signOut = () => { 21 | 22 | return $.ajax({ 23 | url: `/api/session/`, 24 | method: 'DELETE' 25 | }); 26 | }; 27 | -------------------------------------------------------------------------------- /frontend/reducers/tile_slider_reducer.js: -------------------------------------------------------------------------------- 1 | import { RECEIVE_CAMPAIGNS, RECEIVE_ERRORS} from '../actions/campaign_actions'; 2 | import merge from 'lodash/merge'; 3 | 4 | const TileSliderReducer = (oldState={}, action) => { 5 | 6 | Object.freeze(oldState); 7 | switch(action.type) { 8 | case "RECEIVE_CAMPAIGNS": 9 | const allCampaigns = action.campaigns; 10 | // this returns an array 11 | return allCampaigns; 12 | default: 13 | return oldState; 14 | } 15 | 16 | }; 17 | 18 | export default TileSliderReducer; 19 | -------------------------------------------------------------------------------- /config/initializers/assets.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Version of your assets, change this if you want to expire all your assets. 4 | Rails.application.config.assets.version = '1.0' 5 | 6 | # Add additional assets to the asset load path 7 | # Rails.application.config.assets.paths << Emoji.images_path 8 | 9 | # Precompile additional assets. 10 | # application.js, application.css, and all non-JS/CSS in app/assets folder are already added. 11 | # Rails.application.config.assets.precompile += %w( search.js ) 12 | -------------------------------------------------------------------------------- /bin/spring: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | # This file loads spring without using Bundler, in order to be fast. 4 | # It gets overwritten when you run the `spring binstub` command. 5 | 6 | unless defined?(Spring) 7 | require 'rubygems' 8 | require 'bundler' 9 | 10 | lockfile = Bundler::LockfileParser.new(Bundler.default_lockfile.read) 11 | spring = lockfile.specs.detect { |spec| spec.name == "spring" } 12 | if spring 13 | Gem.use_paths Gem.dir, Bundler.bundle_path.to_s, *Gem.path 14 | gem 'spring', spring.version 15 | require 'spring/binstub' 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /config/initializers/to_time_preserves_timezone.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Preserve the timezone of the receiver when calling to `to_time`. 4 | # Ruby 2.4 will change the behavior of `to_time` to preserve the timezone 5 | # when converting to an instance of `Time` instead of the previous behavior 6 | # of converting to the local system timezone. 7 | # 8 | # Rails 5.0 introduced this config option so that apps made with earlier 9 | # versions of Rails are not affected when upgrading. 10 | ActiveSupport.to_time_preserves_timezone = true 11 | -------------------------------------------------------------------------------- /frontend/reducers/user_reducer.js: -------------------------------------------------------------------------------- 1 | import { RECEIVE_USER, UPDATE_USER, RECEIVE_ERRORS } from '../actions/user_actions'; 2 | import { RECEIVE_CURRENT_USER } from '../actions/session_actions'; 3 | import merge from 'lodash/merge'; 4 | 5 | const UserReducer = (state = {}, action) => { 6 | 7 | switch(action.type) { 8 | case RECEIVE_USER: 9 | let x = merge({}, state, action.user); 10 | return x; 11 | case UPDATE_USER: 12 | return merge({}, state, action.user); 13 | default: 14 | return state; 15 | } 16 | }; 17 | 18 | export default UserReducer; 19 | -------------------------------------------------------------------------------- /db/migrate/20170514165849_create_pg_search_documents.rb: -------------------------------------------------------------------------------- 1 | class CreatePgSearchDocuments < ActiveRecord::Migration 2 | def self.up 3 | say_with_time("Creating table for pg_search multisearch") do 4 | create_table :pg_search_documents do |t| 5 | t.text :content 6 | t.belongs_to :searchable, :polymorphic => true, :index => true 7 | t.timestamps null: false 8 | end 9 | end 10 | end 11 | 12 | def self.down 13 | say_with_time("Dropping table for pg_search multisearch") do 14 | drop_table :pg_search_documents 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /frontend/components/categories/category_container.js: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux'; 2 | import {getCategories} from '../../actions/category_actions'; 3 | import CategoryIndex from './category_index'; 4 | 5 | 6 | const mapStateToProps = (state) => { 7 | 8 | return { 9 | categories: state.categories, 10 | errors: state.errors 11 | }; 12 | }; 13 | 14 | const mapDispatchToProps = (dispatch) => { 15 | return { 16 | getCategories: () => dispatch(getCategories()) 17 | }; 18 | }; 19 | 20 | export default connect(mapStateToProps, mapDispatchToProps)(CategoryIndex); 21 | -------------------------------------------------------------------------------- /config/initializers/wrap_parameters.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # This file contains settings for ActionController::ParamsWrapper which 4 | # is enabled by default. 5 | 6 | # Enable parameter wrapping for JSON. You can disable this by setting :format to an empty array. 7 | ActiveSupport.on_load(:action_controller) do 8 | wrap_parameters format: [:json] if respond_to?(:wrap_parameters) 9 | end 10 | 11 | # To enable root element in JSON for ActiveRecord objects. 12 | # ActiveSupport.on_load(:active_record) do 13 | # self.include_root_in_json = true 14 | # end 15 | -------------------------------------------------------------------------------- /frontend/components/campaign/new_campaign_container.js: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux'; 2 | import { createCampaign } from '../../actions/campaign_actions'; 3 | import NewCampaign from './new_campaign'; 4 | 5 | const mapStateToProps = (state) => { 6 | 7 | return { 8 | user: state.session.currentUser, 9 | errors: state.campaigns.errors 10 | }; 11 | }; 12 | 13 | const mapDispatchToProps = (dispatch) => { 14 | 15 | return { 16 | createCampaign: (camp) => dispatch(createCampaign(camp)) 17 | }; 18 | }; 19 | 20 | export default connect(mapStateToProps, mapDispatchToProps)(NewCampaign); 21 | -------------------------------------------------------------------------------- /frontend/components/home_page/tile_slider_container.js: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux'; 2 | import { fetchCampaigns } from '../../actions/campaign_actions'; 3 | import TileSlider from './tile_slider'; 4 | import { selectCampaigns } from '../../reducers/selectors'; 5 | 6 | const mapStateToProps = (state) => { 7 | 8 | return { 9 | campaigns: selectCampaigns(state), 10 | }; 11 | }; 12 | 13 | const mapDispatchToProps = (dispatch) => { 14 | 15 | return { 16 | fetchCampaigns: (num) => dispatch(fetchCampaigns(num)) 17 | }; 18 | }; 19 | 20 | export default connect(mapStateToProps, mapDispatchToProps)(TileSlider); 21 | -------------------------------------------------------------------------------- /frontend/components/home_page/discovery.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import TileSliderContainer from './tile_slider_container'; 3 | import { Link } from 'react-router'; 4 | class Discovery extends React.Component { 5 | 6 | render() { 7 | 8 | return( 9 |
10 |
11 |

Top Picks

12 |
13 | See All 14 |
15 |
16 | 17 |
18 | ); 19 | } 20 | } 21 | 22 | export default Discovery; 23 | -------------------------------------------------------------------------------- /app/views/api/campaigns/show.json.jbuilder: -------------------------------------------------------------------------------- 1 | # json.extract! @campaign, :user_id, :title, :descriptions, 2 | # :tagline, :category_id, asset_path(:image[:url]), :end_date, :target_amount 3 | 4 | 5 | json.id @campaign.id 6 | json.total_contributions @total_contributions 7 | json.num_contributions @num_contributions 8 | json.user_id @campaign.user_id 9 | json.title @campaign.title 10 | json.descriptions @campaign.descriptions 11 | json.tagline @campaign.tagline 12 | json.category_id @campaign.category_id 13 | json.image_url asset_path(@campaign.image.url) 14 | json.end_date @campaign.end_date 15 | json.target_amount @campaign.target_amount 16 | 17 | -------------------------------------------------------------------------------- /config/database.yml: -------------------------------------------------------------------------------- 1 | # SQLite version 3.x 2 | # gem install sqlite3 3 | # 4 | # Ensure the SQLite 3 gem is defined in your Gemfile 5 | # gem 'sqlite3' 6 | # 7 | default: &default 8 | adapter: postgresql 9 | pool: 5 10 | timeout: 5000 11 | 12 | development: 13 | <<: *default 14 | database: IndieFomo_development 15 | 16 | # Warning: The database defined as "test" will be erased and 17 | # re-generated from your development database when you run "rake". 18 | # Do not set this db to the same as development or production. 19 | test: 20 | <<: *default 21 | database: IndieFomo_test 22 | 23 | production: 24 | <<: *default 25 | database: IndieFomo_production 26 | -------------------------------------------------------------------------------- /app/assets/stylesheets/user_contributions_list.css: -------------------------------------------------------------------------------- 1 | 2 | .table-names { 3 | height: 30px; 4 | width: 100%; 5 | display: flex; 6 | flex-direction: row; 7 | justify-content: space-between; 8 | align-items: center; 9 | } 10 | 11 | .table-col { 12 | font-size: 18px; 13 | border-bottom: 1px solid #e4e4e4; 14 | height: 100%; 15 | width: 25%; 16 | } 17 | 18 | .link-nav { 19 | color: black; 20 | } 21 | 22 | .user-reward-item { 23 | height: 30px; 24 | display: flex; 25 | flex-direction: row; 26 | justify-content: space-between; 27 | align-items: center; 28 | } 29 | 30 | .table-row { 31 | margin-top: 20px; 32 | height: 100%; 33 | width: 25%; 34 | } 35 | -------------------------------------------------------------------------------- /app/assets/stylesheets/progress_bar.css: -------------------------------------------------------------------------------- 1 | .progress-bar { 2 | height: 25%; 3 | display: flex; 4 | flex-direction: column; 5 | justify-content: flex-start; 6 | } 7 | 8 | 9 | .dollars { 10 | font-size: 20px; 11 | } 12 | 13 | .funds-1 { 14 | font-size: 14px; 15 | } 16 | 17 | .bar { 18 | display: inline-block; 19 | min-height: 7px; 20 | background-color: #f5f5f5; 21 | width: 100%; 22 | margin-top: 5px; 23 | margin-bottom: 5px; 24 | } 25 | 26 | .percentage { 27 | max-width: 100%; 28 | background-color: #0eb4b6; 29 | min-height: 7px; 30 | } 31 | 32 | .funds-2 { 33 | margin-top: 2px; 34 | font-size: 14px; 35 | } 36 | 37 | .moment { 38 | font-size: 14px; 39 | } 40 | -------------------------------------------------------------------------------- /app/controllers/application_controller.rb: -------------------------------------------------------------------------------- 1 | class ApplicationController < ActionController::Base 2 | # Prevent CSRF attacks by raising an exception. 3 | # For APIs, you may want to use :null_session instead. 4 | protect_from_forgery with: :exception 5 | 6 | helper_method :current_user, :logged_in? 7 | 8 | def current_user 9 | @current_user ||= User.find_by(session_token: session[:session_token]) 10 | end 11 | 12 | def login!(user) 13 | @current_user = user 14 | session[:session_token] = user.reset_session_token! 15 | end 16 | 17 | def logged_in? 18 | !!current_user 19 | end 20 | 21 | def logout! 22 | session[:session_token] = nil 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /app/assets/javascripts/application.js: -------------------------------------------------------------------------------- 1 | // This is a manifest file that'll be compiled into application.js, which will include all the files 2 | // listed below. 3 | // 4 | // Any JavaScript/Coffee file within this directory, lib/assets/javascripts, vendor/assets/javascripts, 5 | // or any plugin's vendor/assets/javascripts directory can be referenced here using a relative path. 6 | // 7 | // It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the 8 | // compiled file. 9 | // 10 | // Read Sprockets README (https://github.com/rails/sprockets#sprockets-directives) for details 11 | // about supported directives. 12 | // 13 | //= require jquery 14 | //= require jquery_ujs 15 | //= require_tree . 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files for more about ignoring files. 2 | # 3 | # If you find yourself ignoring temporary files generated by your text editor 4 | # or operating system, you probably want to add a global ignore instead: 5 | # git config --global core.excludesfile '~/.gitignore_global' 6 | 7 | # Ignore bundler config. 8 | /.bundle 9 | 10 | # Ignore the default SQLite database. 11 | /db/*.sqlite3 12 | /db/*.sqlite3-journal 13 | 14 | # Ignore all logfiles and tempfiles. 15 | /log/* 16 | !/log/.keep 17 | /tmp 18 | 19 | node_modules/ 20 | bundle.js 21 | bundle.js.map 22 | .byebug_history 23 | .DS_Store 24 | npm-debug.log 25 | 26 | # Ignore application configuration 27 | /config/application.yml 28 | -------------------------------------------------------------------------------- /app/controllers/api/contributions_controller.rb: -------------------------------------------------------------------------------- 1 | class Api::ContributionsController < ApplicationController 2 | 3 | def create 4 | 5 | @contribution = Contribution.create(contribution_params) 6 | if contribution_params[:amount].to_i <= 0 7 | render json: "Please enter a valid amount.", status: 422 8 | 9 | elsif @contribution.save 10 | render :show 11 | else 12 | render json: @contribution.errors.full_messages, status: 422 13 | end 14 | end 15 | 16 | def show 17 | @contrubution = Contribution.find(params[:id]) 18 | render :show 19 | end 20 | 21 | private 22 | def contribution_params 23 | params.permit(:user_id, :reward_id, :amount, :campaign_id, :date) 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /config/initializers/inflections.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Add new inflection rules using the following format. Inflections 4 | # are locale specific, and you may define rules for as many different 5 | # locales as you wish. All of these examples are active by default: 6 | # ActiveSupport::Inflector.inflections(:en) do |inflect| 7 | # inflect.plural /^(ox)$/i, '\1en' 8 | # inflect.singular /^(ox)en/i, '\1' 9 | # inflect.irregular 'person', 'people' 10 | # inflect.uncountable %w( fish sheep ) 11 | # end 12 | 13 | # These inflection rules are supported but not enabled by default: 14 | # ActiveSupport::Inflector.inflections(:en) do |inflect| 15 | # inflect.acronym 'RESTful' 16 | # end 17 | -------------------------------------------------------------------------------- /config/locales/en.yml: -------------------------------------------------------------------------------- 1 | # Files in the config/locales directory are used for internationalization 2 | # and are automatically loaded by Rails. If you want to use locales other 3 | # than English, add the necessary files in this directory. 4 | # 5 | # To use the locales, use `I18n.t`: 6 | # 7 | # I18n.t 'hello' 8 | # 9 | # In views, this is aliased to just `t`: 10 | # 11 | # <%= t('hello') %> 12 | # 13 | # To use a different locale, set it with `I18n.locale`: 14 | # 15 | # I18n.locale = :es 16 | # 17 | # This would use the information in config/locales/es.yml. 18 | # 19 | # To learn more, please read the Rails Internationalization guide 20 | # available at http://guides.rubyonrails.org/i18n.html. 21 | 22 | en: 23 | hello: "Hello world" 24 | -------------------------------------------------------------------------------- /frontend/actions/category_actions.js: -------------------------------------------------------------------------------- 1 | import * as CategoryApiUtil from '../util/category_api_util'; 2 | export const RECEIVE_CATEGORIES = 'RECEIVE_CATEGORIES'; 3 | export const RECEIVE_CATEGORY_ERRORS = 'RECEIVE_CATEGORY_ERRORS'; 4 | 5 | export const receiveCategories = (categories) => { 6 | return { 7 | type: RECEIVE_CATEGORIES, 8 | categories: categories 9 | }; 10 | }; 11 | 12 | export const receiveCategoryErrors = (err) => { 13 | return { 14 | type: RECEIVE_CATEGORY_ERRORS, 15 | error: err 16 | }; 17 | }; 18 | 19 | export const getCategories = () => (dispatch) => { 20 | return CategoryApiUtil.getCategories() 21 | .then((cat) => dispatch(receiveCategories(cat)), 22 | (err) => dispatch(receiveCategoryErrors(err))); 23 | }; 24 | -------------------------------------------------------------------------------- /frontend/components/session/session_form_container.js: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux'; 2 | import { login, logout, signup } from '../../actions/session_actions'; 3 | import SessionForm from './session_form'; 4 | 5 | const mapStateToProps = ( { session }) => { 6 | 7 | return { 8 | loggedIn: Boolean(session.currentUser), 9 | errors: session.errors 10 | }; 11 | }; 12 | 13 | const mapDispatchToProps = (dispatch, ownProps) => { 14 | const formType = ownProps.formType; 15 | const processForm = (formType === 'login') ? login : signup; 16 | 17 | return { 18 | processForm: (user) => dispatch(processForm(user)), 19 | formType 20 | }; 21 | }; 22 | 23 | export default connect(mapStateToProps, mapDispatchToProps)(SessionForm); 24 | -------------------------------------------------------------------------------- /frontend/indie_fomo.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import { signUp, signIn, signOut} from './util/auth_api_util'; 4 | import configureStore from './store/store'; 5 | import Root from './components/root'; 6 | import Modal from 'react-modal'; 7 | 8 | 9 | 10 | document.addEventListener('DOMContentLoaded', () => { 11 | let store; 12 | if (window.currentUser) { 13 | const preloadedState = {session: {currentUser: window.currentUser, errors: {}}}; 14 | store = configureStore(preloadedState); 15 | } else { 16 | store = configureStore(); 17 | } 18 | 19 | Modal.setAppElement(document.body); 20 | const root = document.getElementById('root'); 21 | ReactDOM.render(, root); 22 | }); 23 | -------------------------------------------------------------------------------- /app/controllers/api/charges_controller.rb: -------------------------------------------------------------------------------- 1 | # class Api::ChargesController < ApplicationController 2 | # 3 | # def new 4 | # end 5 | # 6 | # def create 7 | # # default for now. Stripe ammounts must be in cents 8 | # 9 | # @amount = params[:amount] 10 | # 11 | # customer = Stripe::Customer.create( 12 | # :email => params[:stripeEmail], 13 | # :source => params[:stripeToken] 14 | # ) 15 | # 16 | # charge = Stripe::Charge.create( 17 | # :customer => customer.id, 18 | # :amount => @amount, 19 | # :description => 'Rails Stripe customer', 20 | # :currency => 'usd' 21 | # ) 22 | # 23 | # rescue = Stripe::CardError => e 24 | # flash[:error] = e.message 25 | # redirect_to new_charge_path 26 | # end 27 | # 28 | # end 29 | -------------------------------------------------------------------------------- /app/views/layouts/application.html.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | IndieFomo 5 | 6 | 7 | <%= favicon_link_tag 'IndieFomoFavicon.ico' %> 8 | 9 | <%= stylesheet_link_tag 'application', media: 'all' %> 10 | <%= javascript_include_tag 'application' %> 11 | <%= csrf_meta_tags %> 12 | 13 | 14 | 15 | <%= yield %> 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /db/migrate/20170418155621_create_users.rb: -------------------------------------------------------------------------------- 1 | class CreateUsers < ActiveRecord::Migration 2 | def change 3 | create_table :users do |t| 4 | t.string :username, null: false 5 | t.string :first_name, null: false 6 | t.string :last_name, null: false 7 | t.text :about_me, default: "" 8 | t.string :email, null: false 9 | t.string :password_digest, null: false 10 | t.string :session_token, null: false 11 | 12 | t.timestamps null: false 13 | end 14 | 15 | add_index :users, :username, unique: true 16 | add_index :users, :first_name, unique: true 17 | add_index :users, :last_name, unique: true 18 | add_index :users, :email, unique: true 19 | add_index :users, :session_token, unique: true 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /app/assets/stylesheets/footer.css: -------------------------------------------------------------------------------- 1 | .footer { 2 | width: 100%; 3 | height: 200px; 4 | background-color: black; 5 | display: flex; 6 | margin-top: 150px; 7 | color: white; 8 | display: flex; 9 | flex-direction: column; 10 | justify-content: space-around; 11 | align-items: center; 12 | } 13 | 14 | .contact { 15 | display: flex; 16 | width: 100%; 17 | justify-content: space-around; 18 | align-items: center; 19 | } 20 | 21 | .linkedin { 22 | width: 70%; 23 | height: 70%; 24 | padding: 10px; 25 | } 26 | 27 | .personal { 28 | width: 55%; 29 | height: 55%; 30 | padding: 10px; 31 | } 32 | 33 | .github { 34 | width: 120%; 35 | height: 120%; 36 | } 37 | 38 | .patrycja { 39 | width: 75%; 40 | font-size: 14px; 41 | text-align: left; 42 | } 43 | -------------------------------------------------------------------------------- /app/assets/stylesheets/api/reset.css: -------------------------------------------------------------------------------- 1 | html, body, div, span, h1, h2, h3, h4, p, a, img, 2 | figure, canvas, aside article, table, section, 3 | nav, menu, main, video, Link, button, label, input, form, 4 | ul, li, main{ 5 | margin: 0; 6 | padding: 0; 7 | border: 0; 8 | font-size: 100%; 9 | font-family: 'Maven Pro', sans-serif;; 10 | font: inherit; 11 | background: transparent; 12 | } 13 | 14 | a { 15 | text-decoration: none; 16 | } 17 | 18 | ul, li { 19 | list-style: none; 20 | display: inline-block; 21 | } 22 | /* This is useful when implementing a grid system so that all image elements 23 | will take up 100% width of their containers */ 24 | img { 25 | width: 100% 26 | } 27 | 28 | html, body{ 29 | height:100%; 30 | font-family: 'Maven Pro', sans-serif;; 31 | } 32 | -------------------------------------------------------------------------------- /frontend/components/home_page/home_page.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Link, withRouter } from 'react-router'; 3 | import Carousel from './carousel_component'; 4 | import Discovery from './discovery'; 5 | import CategoryContainer from '../categories/category_container'; 6 | 7 | 8 | class HomePage extends React.Component { 9 | 10 | render() { 11 | return( 12 |
13 |
14 | 15 |
16 | 17 |
18 | 19 |
20 | 21 |
22 | 23 |
24 |
25 | ); 26 | } 27 | } 28 | 29 | export default withRouter(HomePage); 30 | -------------------------------------------------------------------------------- /app/assets/stylesheets/user_campaign_list.css: -------------------------------------------------------------------------------- 1 | .usr-campaign-list { 2 | display: flex; 3 | flex-direction: column; 4 | } 5 | 6 | .profile-content-container { 7 | width: 85%; 8 | } 9 | 10 | .user-campaign-item { 11 | display: flex; 12 | flex-direction: row; 13 | border-bottom: 1px solid #e4e4e4; 14 | padding-top: 10px; 15 | padding-bottom: 10px; 16 | } 17 | 18 | .list-img { 19 | width: 160px; 20 | height: 160px; 21 | } 22 | 23 | .user-campaign-item-details { 24 | margin-left: 30px; 25 | } 26 | 27 | .stats-u { 28 | padding-top: 10px; 29 | height: 50px; 30 | display: flex; 31 | flex-direction: column; 32 | justify-content: flex-start; 33 | } 34 | 35 | .UCTitle { 36 | font-size: 18px; 37 | color: #FC3468; 38 | } 39 | 40 | .stats-span { 41 | color: black; 42 | } 43 | -------------------------------------------------------------------------------- /app/views/api/campaigns/index.json.jbuilder: -------------------------------------------------------------------------------- 1 | 2 | @campaigns.each do |campaign| 3 | 4 | json.set! campaign.id do 5 | json.num_contributions Contribution.where(campaign_id: campaign.id).count 6 | json.total_contributions Contribution.where(campaign_id: campaign.id).sum(:amount) 7 | json.user campaign.user 8 | json.id campaign.id 9 | json.title campaign.title 10 | json.descriptions campaign.descriptions 11 | json.tagline campaign.tagline 12 | json.category_id campaign.category_id 13 | json.image_url campaign.image_url 14 | json.end_date campaign.end_date 15 | json.target_amount campaign.target_amount 16 | json.image_url asset_path(campaign.image.url) 17 | if campaign.category 18 | json.cat campaign.category.cat 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /docs/api-endpoints: -------------------------------------------------------------------------------- 1 | # API Endpoints 2 | 3 | ## HTML API 4 | 5 | ### Root 6 | 7 | - `GET /` - loads React web app 8 | 9 | ## JSON API 10 | 11 | ### Users 12 | 13 | - `POST /api/profile` 14 | - `PATCH /api/profile/:id` 15 | - `GET /api/profile/:id` 16 | 17 | ### Session 18 | 19 | - `POST /api/session` 20 | - `DELETE /api/session` 21 | 22 | ### Campaign 23 | 24 | - `GET /api/campaign/:id` 25 | - `GET /api/campaign/new` 26 | - `PATCH /api/campaign/:id` 27 | - `POST /api/campaign` 28 | - `DELETE /api/campaign/:id` 29 | 30 | ### Category 31 | - `GET /api/category/:id` 32 | 33 | ### Contribution 34 | - `GET /api/contribution/:id` 35 | - `POST /api/contribution` 36 | 37 | ### Reward 38 | - `GET api/reward/:id/contributions` 39 | - `POST /api/reward/contributions` 40 | - `PATCH /api/reward/:id/contributions` 41 | -------------------------------------------------------------------------------- /app/assets/images/github.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/components/categories/category_tile.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Link, withRouter, hashHistory } from 'react-router'; 3 | 4 | class CategoryTile extends React.Component { 5 | constructor(props) { 6 | super(props); 7 | } 8 | 9 | handleClick(e) { 10 | e.preventDefault(); 11 | const url = `/categories/${this.props.category.id}/campaigns`; 12 | this.props.router.push(url); 13 | } 14 | 15 | render() { 16 | return( 17 |
18 | 19 | 20 |
{this.props.category.cat}
21 |
22 |
23 | ); 24 | } 25 | 26 | } 27 | export default withRouter(CategoryTile); 28 | -------------------------------------------------------------------------------- /app/assets/stylesheets/searchbar.css: -------------------------------------------------------------------------------- 1 | .searchResults-true { 2 | background: white; 3 | position: fixed; 4 | margin-top: 35px; 5 | width: 300px; 6 | z-index: 10; 7 | display: flex; 8 | overflow: hidden; 9 | overflow-y: hidden; 10 | flex-direction: column; 11 | } 12 | 13 | .searchResults-false { 14 | position: fixed; 15 | width: 273px; 16 | height: 5px; 17 | } 18 | 19 | .s-result { 20 | background: white; 21 | border: 1px solid #dad7d7; 22 | padding: 10px; 23 | color: black; 24 | height: 20px; 25 | 26 | } 27 | 28 | .s-result:hover { 29 | cursor: pointer; 30 | color: #FC3468; 31 | } 32 | 33 | .explore-dropdown { 34 | width: 100px; 35 | } 36 | 37 | .search { 38 | display: flex; 39 | justify-content: center; 40 | height: 34px; 41 | width: 200px; 42 | } 43 | 44 | .searchBar { 45 | width: 200px; 46 | } 47 | -------------------------------------------------------------------------------- /frontend/components/user/user_container.js: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux'; 2 | import { getUser, editUser } from '../../actions/user_actions'; 3 | import UserShow from './user_show'; 4 | import { fetchCampaign } from '../../actions/campaign_actions'; 5 | 6 | const mapStateToProps = (state, ownProps) => { 7 | 8 | return { 9 | user: state.user, 10 | loggedIn: Boolean(state.session.currentUser), 11 | campaigns: state.user.campaigns, 12 | contributions: state.user.contributions 13 | }; 14 | }; 15 | 16 | const mapDispatchToProps = (dispatch) => { 17 | 18 | return { 19 | getUser: (id) => dispatch(getUser(id)), 20 | editUser: (user) => dispatch(editUser(user)), 21 | fetchCampaign: (id) => dispatch(fetchCampaign(id)) 22 | }; 23 | }; 24 | 25 | export default connect(mapStateToProps, mapDispatchToProps)(UserShow); 26 | -------------------------------------------------------------------------------- /frontend/reducers/session_reducer.js: -------------------------------------------------------------------------------- 1 | import { RECEIVE_CURRENT_USER, RECEIVE_SESSION_ERRORS} from '../actions/session_actions'; 2 | import merge from 'lodash/merge'; 3 | 4 | const _nullUser = Object.freeze({ 5 | currentUser: null, 6 | errors: null 7 | }); 8 | 9 | const SessionReducer = (state = _nullUser, action) => { 10 | 11 | switch (action.type) { 12 | case RECEIVE_CURRENT_USER: 13 | const currentUser = action.currentUser; 14 | if (currentUser) { 15 | return merge({}, state, {currentUser: currentUser, errors: null} ); 16 | } else { 17 | return _nullUser; 18 | } 19 | case RECEIVE_SESSION_ERRORS: 20 | const errors = action.errors.base[0]; 21 | return merge({}, _nullUser, {errors: errors}); 22 | default: 23 | return state; 24 | } 25 | }; 26 | 27 | export default SessionReducer; 28 | -------------------------------------------------------------------------------- /app/assets/stylesheets/category_show.css: -------------------------------------------------------------------------------- 1 | .category-show-wrapper { 2 | display: flex; 3 | flex-direction: column; 4 | justify-content: center; 5 | align-items: center; 6 | width: 80%; 7 | display: block; 8 | align-items: center; 9 | margin-top: 25px; 10 | margin-bottom: 50px; 11 | } 12 | 13 | .category-show-wrapper > h1 { 14 | /*border: 4px solid blue;*/ 15 | height: 30px; 16 | color: black; 17 | font-size: 24px; 18 | width: 65%; 19 | margin-top: 40px; 20 | margin-bottom: 40px; 21 | margin-left: 50px; 22 | } 23 | 24 | .category-campaign-tile { 25 | box-sizing: border-box; 26 | display: flex; 27 | flex-direction: row; 28 | justify-content: flex-start; 29 | align-items: center; 30 | margin-top: 1%; 31 | height: 90%; 32 | width: 100%; 33 | margin: 0; 34 | flex-wrap: wrap; 35 | margin-left: 50px; 36 | 37 | } 38 | -------------------------------------------------------------------------------- /app/views/static_pages/root.html.erb: -------------------------------------------------------------------------------- 1 | 2 | 19 |
20 | -------------------------------------------------------------------------------- /frontend/reducers/root_reducer.js: -------------------------------------------------------------------------------- 1 | import {combineReducers} from 'redux'; 2 | import SessionReducer from './session_reducer'; 3 | import CampaignsReducer from './campaign_reducer'; 4 | import UserReducer from './user_reducer'; 5 | import TileSliderReducer from './tile_slider_reducer'; 6 | import ContributionReducer from './contribution_reducer'; 7 | import RewardsReducer from './rewards_reducer'; 8 | import categoryReducer from './category_reducer'; 9 | import SearchReducer from './search_reducer'; 10 | 11 | 12 | const RootReducer = combineReducers({ 13 | session: SessionReducer, 14 | campaigns: CampaignsReducer, 15 | user: UserReducer, 16 | slider: TileSliderReducer, 17 | contribution: ContributionReducer, 18 | rewards: RewardsReducer, 19 | categories: categoryReducer, 20 | searchResults: SearchReducer 21 | }); 22 | 23 | export default RootReducer; 24 | -------------------------------------------------------------------------------- /frontend/reducers/rewards_reducer.js: -------------------------------------------------------------------------------- 1 | import { RECEIVE_REWARD, RECEIVE_ALL_REWARDS, RECEIVE_REWARD_ERRORS, DELETE_REWARD} from '../actions/reward_actions'; 2 | import merge from 'lodash/merge'; 3 | 4 | 5 | const RewardsReducer = (state = {}, action) => { 6 | 7 | switch (action.type) { 8 | case RECEIVE_REWARD: 9 | return merge({}, state, {[action.reward.id]: action.reward}); 10 | case RECEIVE_ALL_REWARDS: 11 | const oldS = merge({}, state); 12 | if (action.rewards.id) { 13 | return merge({}, oldS, {[action.rewards.id]: action.rewards}); 14 | } else { 15 | return state; 16 | } 17 | case DELETE_REWARD: 18 | let newState = merge({}, state); 19 | delete newState[action.reward.id]; 20 | 21 | return newState; 22 | default: 23 | return state; 24 | } 25 | }; 26 | 27 | export default RewardsReducer; 28 | -------------------------------------------------------------------------------- /app/assets/stylesheets/modal_style.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | overlay : { 3 | position : 'fixed', 4 | top : 0, 5 | left : 0, 6 | right : 0, 7 | bottom : 0, 8 | backgroundColor : 'rgba(255, 255, 255, 0.75)' 9 | }, 10 | content : { 11 | position : 'absolute', 12 | top : '20%', 13 | left : '35%', 14 | right : '35%', 15 | bottom : '20%', 16 | border : '1px solid #ccc', 17 | background : '#fff', 18 | overflow : 'auto', 19 | WebkitOverflowScrolling : 'touch', 20 | borderRadius : '4px', 21 | outline : 'none', 22 | padding : '20px' 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /frontend/components/campaign/edit_campaign_container.js: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux'; 2 | import { fetchCampaign, updateCampaign, updateCampaignForm } from '../../actions/campaign_actions'; 3 | import EditCampaign from './edit_campaign_component'; 4 | 5 | const mapStateToProps = (state, ownProps) => { 6 | 7 | let campaign = state.campaigns.campaign; 8 | 9 | return { 10 | campaign, 11 | user: state.session.currentUser, 12 | errors: state.campaigns.errors 13 | }; 14 | }; 15 | 16 | const mapDispatchToProps = (dispatch) => { 17 | return { 18 | fetchCampaign: (id) => dispatch(fetchCampaign(id)), 19 | updateCampaign: (campaign) => dispatch(updateCampaign(campaign)), 20 | updateCampaignForm: (campaign) => dispatch(updateCampaignForm(campaign)) 21 | }; 22 | }; 23 | 24 | export default connect(mapStateToProps, mapDispatchToProps)(EditCampaign); 25 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | const webpack = require('webpack'); 3 | const prod = process.argv.indexOf('-p') !== -1; 4 | 5 | module.exports = { 6 | entry: "./frontend/indie_fomo.jsx", 7 | output: { 8 | path: path.resolve(__dirname, 'app', 'assets', 'javascripts'), 9 | filename: "bundle.js" 10 | }, 11 | module: { 12 | loaders: [ 13 | { 14 | test: [/\.jsx?$/], 15 | exclude: /(node_modules)/, 16 | loader: 'babel-loader', 17 | query: { 18 | presets: ['es2015', 'react'] 19 | } 20 | } 21 | ] 22 | }, 23 | devtool: 'source-map', 24 | resolve: { 25 | extensions: ['.js', '.jsx', '*'] 26 | }, 27 | plugins: [ 28 | new webpack.DefinePlugin({ 29 | 'process.env': { 30 | NODE_ENV: JSON.stringify('production') 31 | } 32 | }), 33 | new webpack.optimize.UglifyJsPlugin() 34 | ], 35 | }; 36 | -------------------------------------------------------------------------------- /frontend/util/rewards_api_util.js: -------------------------------------------------------------------------------- 1 | export const createReward = (reward) => { 2 | 3 | return $.ajax({ 4 | url: `api/campaigns/${reward.campaign_id}/rewards`, 5 | method: 'POST', 6 | data: reward, 7 | }); 8 | }; 9 | 10 | export const editReward = (reward) => { 11 | return $.ajax({ 12 | url: `api/campaigns/${reward.campaign_id}/rewards/${reward.id}`, 13 | method: 'PATCH', 14 | data: reward 15 | }); 16 | }; 17 | 18 | export const deleteReward = (reward) => { 19 | 20 | return $.ajax({ 21 | url: `api/campaigns/${reward.campaign_id}/rewards/${reward.id}`, 22 | method: 'DELETE' 23 | }); 24 | }; 25 | 26 | export const getReward = (reward) => { 27 | return $.ajax({ 28 | url: `api/campaigns/${reward.campaign_id}/rewards/${reward.id}`, 29 | }); 30 | }; 31 | 32 | export const getAllRewards = (id) => { 33 | return $.ajax({ 34 | url: `api/campaigns/${id}/rewards` 35 | }); 36 | }; 37 | -------------------------------------------------------------------------------- /bin/setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require 'pathname' 3 | 4 | # path to your application root. 5 | APP_ROOT = Pathname.new File.expand_path('../../', __FILE__) 6 | 7 | Dir.chdir APP_ROOT do 8 | # This script is a starting point to setup your application. 9 | # Add necessary setup steps to this file: 10 | 11 | puts "== Installing dependencies ==" 12 | system "gem install bundler --conservative" 13 | system "bundle check || bundle install" 14 | 15 | # puts "\n== Copying sample files ==" 16 | # unless File.exist?("config/database.yml") 17 | # system "cp config/database.yml.sample config/database.yml" 18 | # end 19 | 20 | puts "\n== Preparing database ==" 21 | system "bin/rake db:setup" 22 | 23 | puts "\n== Removing old logs and tempfiles ==" 24 | system "rm -f log/*" 25 | system "rm -rf tmp/cache" 26 | 27 | puts "\n== Restarting application server ==" 28 | system "touch tmp/restart.txt" 29 | end 30 | -------------------------------------------------------------------------------- /app/assets/stylesheets/api/modal_style.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | overlay : { 3 | position : 'fixed', 4 | top : 0, 5 | left : 0, 6 | right : 0, 7 | bottom : 0, 8 | backgroundColor : 'rgba(255, 255, 255, 0.75)' 9 | }, 10 | content : { 11 | position : 'absolute', 12 | top : '10%', 13 | left : '40%', 14 | right : '40%', 15 | bottom : '10%', 16 | border : '1px solid #ccc', 17 | background : '#f5f5f5', 18 | overflow : 'auto', 19 | WebkitOverflowScrolling : 'touch', 20 | borderRadius : '4px', 21 | outline : 'none', 22 | padding : '20px', 23 | 24 | 25 | } 26 | }; 27 | Contact GitHub API Training Shop Blog About 28 | -------------------------------------------------------------------------------- /app/models/campaign.rb: -------------------------------------------------------------------------------- 1 | class Campaign < ActiveRecord::Base 2 | include PgSearch 3 | validates_numericality_of :target_amount, {greater_than_or_equal_to: 1, allow_nil: false, message: "Please enter an amount greater than zero!"} 4 | validates :title, length: { minimum: 1, allow_nil: false, message: "Please enter a title!"} 5 | 6 | pg_search_scope :search_by_title, :against => { 7 | :title => 'A', 8 | :tagline => 'B', 9 | }, :using => { 10 | :tsearch => {:prefix => true} 11 | } 12 | 13 | has_many :rewards 14 | belongs_to :category 15 | has_many :contributions, 16 | primary_key: :id, 17 | foreign_key: :campaign_id, 18 | class_name: "Contribution" 19 | 20 | belongs_to :user, 21 | primary_key: :id, 22 | foreign_key: :user_id, 23 | class_name: "User" 24 | 25 | has_attached_file :image, default_url: "img_placeholder.png" 26 | validates_attachment_content_type :image, content_type: /\Aimage\/.*\z/ 27 | 28 | end 29 | -------------------------------------------------------------------------------- /app/controllers/api/users_controller.rb: -------------------------------------------------------------------------------- 1 | class Api::UsersController < ApplicationController 2 | 3 | def create 4 | @user = User.create(user_params) 5 | 6 | if @user.save 7 | login!(@user) 8 | render :show 9 | else 10 | render json: @user.errors.full_messages, status:422 11 | end 12 | end 13 | 14 | def update 15 | @user = User.find(params[:id]) 16 | @contributions = Contribution.where(user_id: params[:id]) 17 | 18 | if @user.update(user_params) 19 | @user.save 20 | render :show 21 | 22 | else 23 | render json: @user.errors.full_messages, status:422 24 | 25 | end 26 | end 27 | 28 | def show 29 | @user = User.find(params[:id]) 30 | @contributions = Contribution.where(user_id: params[:id]) 31 | render :show 32 | end 33 | 34 | private 35 | def user_params 36 | params.require(:user).permit(:password, :email, :first_name, :last_name, :image, :about_me) 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /frontend/components/header/header_container.js: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux'; 2 | import { logout, loginguest } from '../../actions/session_actions'; 3 | import Header from './header'; 4 | import { searchCampaigns } from '../../actions/search_action'; 5 | import { getCategories } from '../../actions/category_actions'; 6 | 7 | const mapStateToProps = (state) => { 8 | 9 | return { 10 | currentUser: state.session.currentUser, 11 | loggedIn: Boolean(state.session.currentUser), 12 | searchResults: state.searchResults, 13 | categories: state.categories 14 | }; 15 | }; 16 | 17 | const mapDispatchToProps = (dispatch) => { 18 | 19 | return { 20 | logout: () => dispatch(logout()), 21 | loginguest: () => dispatch(loginguest()), 22 | searchCampaigns: (sf) => dispatch(searchCampaigns(sf)), 23 | getCategories: () => dispatch(getCategories()) 24 | }; 25 | }; 26 | 27 | export default connect(mapStateToProps, mapDispatchToProps)(Header); 28 | -------------------------------------------------------------------------------- /app/controllers/api/rewards_controller.rb: -------------------------------------------------------------------------------- 1 | class Api::RewardsController < ApplicationController 2 | 3 | def index 4 | @campaign = Campaign.find(params[:campaign_id]) 5 | render :index 6 | end 7 | 8 | def show 9 | @reward = Reward.find(params[:id]) 10 | render :show 11 | end 12 | 13 | 14 | def create 15 | 16 | @reward = Reward.create(reward_params) 17 | 18 | if @reward.save 19 | render :show 20 | else 21 | render json: "Error, please try again!", status: 422 22 | end 23 | end 24 | 25 | def update 26 | @reward = Reward.find(params[:id]) 27 | if @reward.save 28 | render :show 29 | else 30 | render json: "Update uncussesful, please try again!", status: 422 31 | end 32 | end 33 | 34 | def destroy 35 | 36 | @reward = Reward.find(params[:id]) 37 | @reward.destroy 38 | render :show 39 | end 40 | 41 | def reward_params 42 | params.permit(:campaign_id, :title, :description, :price) 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /frontend/components/campaign/campaign_index_container.js: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux'; 2 | import { fetchCampaigns, fetchCampaign } from '../../actions/campaign_actions'; 3 | import CampaignIndex from './campaign_index'; 4 | import { selectCampaigns } from '../../reducers/selectors'; 5 | import { makeContribution } from '../../actions/contribution_actions'; 6 | 7 | const mapStateToProps = (state) => { 8 | let camps = null; 9 | if (state.campaigns){ 10 | camps = Object.keys(state.campaigns).map((el) => state.campaigns[el]); 11 | } 12 | return { 13 | campaigns: camps, 14 | errors: state.errors 15 | }; 16 | }; 17 | 18 | const mapDispatchToProps = (dispatch) => { 19 | 20 | return { 21 | fetchCampaigns: () => dispatch(fetchCampaigns()), 22 | fetchCampaign: (id) => dispatch(fetchCampaign(id)), 23 | makeContribution: (contribution) => dispatch(makeContribution(contribution)) 24 | }; 25 | }; 26 | 27 | export default connect(mapStateToProps, mapDispatchToProps)(CampaignIndex); 28 | -------------------------------------------------------------------------------- /frontend/reducers/contribution_reducer.js: -------------------------------------------------------------------------------- 1 | import { RECEIVE_CONTRIBUTION, RECEIVE_CONTRIBUTION_ERRORS } from '../actions/contribution_actions'; 2 | import { RECEIVE_CAMPAIGNS, RECEIVE_CAMPAIGN } from '../actions/campaign_actions'; 3 | import merge from 'lodash/merge'; 4 | 5 | 6 | const ContributionReducer = (state = {}, action) => { 7 | 8 | switch (action.type) { 9 | case RECEIVE_CONTRIBUTION: 10 | let old = merge({}, state); 11 | if (old.errors) { 12 | old.errors = ""; 13 | } 14 | return merge(old, action.contribution); 15 | case RECEIVE_CONTRIBUTION_ERRORS: 16 | let old1 = merge({}, state); 17 | return merge(old1, { errors: action.errors.responseText}); 18 | case RECEIVE_CAMPAIGN: 19 | let old2 = merge({}, state); 20 | return merge(old2, { errors: ""}); 21 | case RECEIVE_CAMPAIGNS: 22 | let old3 = merge({}, state); 23 | return merge(old3, { errors: ""}); 24 | default: 25 | return state; 26 | } 27 | }; 28 | 29 | export default ContributionReducer; 30 | -------------------------------------------------------------------------------- /frontend/components/home_page/carousel_component.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Slider from 'react-slick'; 3 | 4 | class Carousel extends React.Component { 5 | render() { 6 | var settings = { 7 | dots: false, 8 | slidesToShow: 3, 9 | infinite: true, 10 | slidesToScroll: 1, 11 | autoplay: true, 12 | autoplaySpeed: 2000, 13 | arrows: false 14 | }; 15 | 16 | return ( 17 | 18 |

19 |

20 |

21 |

22 |
23 | ); 24 | } 25 | } 26 | 27 | export default Carousel; 28 | -------------------------------------------------------------------------------- /config/secrets.yml: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Your secret key is used for verifying the integrity of signed cookies. 4 | # If you change this key, all old signed cookies will become invalid! 5 | 6 | # Make sure the secret is at least 30 characters and all random, 7 | # no regular words or you'll be exposed to dictionary attacks. 8 | # You can use `rake secret` to generate a secure secret key. 9 | 10 | # Make sure the secrets in this file are kept private 11 | # if you're sharing your code publicly. 12 | 13 | development: 14 | secret_key_base: dca024cf7cfa24fc3eb1d7fcfc0316b408b14316e83db2b4c956695744c8b9acab21decd5f802ecec319d38b045b5aa3c2a35ef0f9885728fb1dd94507bdbe69 15 | 16 | test: 17 | secret_key_base: b9c3d4aff5b163055214d9dd6c03dca1c86cf0f87bf23154bf21a07afc7a58d7bd7acfde29f2ad2ed8b4a949f23c0dd4ddc8eb36517bd8a8cf72dd45dae2a8a0 18 | 19 | # Do not keep production secrets in the repository, 20 | # instead read values from the environment. 21 | production: 22 | secret_key_base: <%= ENV["SECRET_KEY_BASE"] %> 23 | -------------------------------------------------------------------------------- /frontend/actions/user_actions.js: -------------------------------------------------------------------------------- 1 | import * as UserAPIUtil from '../util/user_api_util'; 2 | export const RECEIVE_USER = "RECEIVE_USER"; 3 | export const UPDATE_USER = "UPDATE_USER"; 4 | export const RECEIVE_ERRORS = "RECEIVE_ERRORS"; 5 | 6 | export const receiveUser = (user) => { 7 | 8 | return { 9 | type: RECEIVE_USER, 10 | user 11 | }; 12 | }; 13 | 14 | export const updateUser = (user) => { 15 | 16 | return { 17 | type: UPDATE_USER, 18 | user 19 | }; 20 | }; 21 | 22 | export const receiveErrors = (errors) => { 23 | 24 | return { 25 | type: RECEIVE_ERRORS, 26 | errors 27 | }; 28 | }; 29 | 30 | export const getUser = (id) => (dispatch) => { 31 | 32 | return UserAPIUtil.getUser(id) 33 | .then((usr) => dispatch(receiveUser(usr)), 34 | (err) => dispatch(receiveErrors(err.responseJSON))); 35 | }; 36 | 37 | export const editUser = (user) => (dispatch) => { 38 | 39 | return UserAPIUtil.editUser(user) 40 | .then((usr) => dispatch(receiveUser(usr)), 41 | (err) => dispatch(receiveErrors(err.responseJSON))); 42 | }; 43 | -------------------------------------------------------------------------------- /frontend/actions/contribution_actions.js: -------------------------------------------------------------------------------- 1 | import * as ContributionApiUtil from '../util/contributions_api_util'; 2 | export const RECEIVE_CONTRIBUTION = 'RECEIVE_CONTRIBUTION'; 3 | export const RECEIVE_CONTRIBUTION_ERRORS = 'RECEIVE_ERRORS'; 4 | 5 | export const receiveContribution = (contribution) => { 6 | return { 7 | type: RECEIVE_CONTRIBUTION, 8 | contribution: contribution 9 | }; 10 | }; 11 | 12 | export const receiveContributionErrors = (errors) => { 13 | return { 14 | type: RECEIVE_CONTRIBUTION_ERRORS, 15 | errors: errors 16 | }; 17 | }; 18 | 19 | export const getContribution = (id) => (dispatch) => { 20 | 21 | return ContributionApiUtil.getContribution(id) 22 | .then((cont) => dispatch(receiveContribution(cont)), 23 | (err) => dispatch(receiveContributionErrors(err))); 24 | }; 25 | 26 | export const makeContribution = (contribution) => (dispatch) => { 27 | 28 | return ContributionApiUtil.makeContribution(contribution) 29 | .then((cont) => dispatch(receiveContribution(cont)), 30 | (err) => dispatch(receiveContributionErrors(err))); 31 | }; 32 | -------------------------------------------------------------------------------- /frontend/components/footer.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | class Footer extends React.Component { 4 | 5 | render() { 6 | return( 7 |
8 |
9 |
10 | 11 | 12 | 13 |
14 |
15 | 16 | 17 | 18 |
19 |
20 | 21 | 22 | 23 |
24 |
25 |
Made by: Patrycja Lupinska
26 |
27 | ); 28 | } 29 | } 30 | 31 | 32 | 33 | export default Footer; 34 | -------------------------------------------------------------------------------- /frontend/util/campaign_api_util.js: -------------------------------------------------------------------------------- 1 | export const getCampaigns = (number = 'all', category = "none") => { 2 | 3 | return $.ajax({ 4 | url: `/api/campaigns`, 5 | data: { num: number, category: category}, 6 | method: 'GET' 7 | }); 8 | }; 9 | 10 | export const getCampaign = (id) => { 11 | return $.ajax({ 12 | url: `/api/campaigns/${id}` 13 | }); 14 | }; 15 | 16 | export const updateCampaignForm = (campaign) => { 17 | // Campaign is nested inside of form data, we retreive it with get 18 | return $.ajax({ 19 | url: `/api/campaigns/${campaign.get("campaign[id]")}`, 20 | method: 'PATCH', 21 | dataType: "json", 22 | contentType: false, 23 | processData: false, 24 | data: campaign 25 | }); 26 | }; 27 | 28 | export const updateCampaign = (campaign) => { 29 | return $.ajax({ 30 | url: `/api/campaigns/${campaign.id}`, 31 | method: 'PATCH', 32 | data: campaign 33 | }); 34 | }; 35 | 36 | export const createCampaign = (campaign) => { 37 | 38 | return $.ajax({ 39 | url: `api/campaigns`, 40 | method: `POST`, 41 | data: {campaign}, 42 | }); 43 | }; 44 | -------------------------------------------------------------------------------- /frontend/components/categories/category_show_container.js: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux'; 2 | import { fetchCampaigns, fetchCampaign } from '../../actions/campaign_actions'; 3 | import CategoryShow from './category_show'; 4 | import { selectCampaigns } from '../../reducers/selectors'; 5 | import { getCategories } from '../../actions/category_actions'; 6 | 7 | const mapStateToProps = (state, ownProps) => { 8 | let camps; 9 | 10 | if (state.campaigns) { 11 | if (!state.campaigns.campaign && !Object.keys(state.campaigns)) { 12 | camps = []; 13 | } else if (state.campaigns) { 14 | 15 | camps = Object.keys(state.campaigns).map((key) => state.campaigns[key]); 16 | } 17 | } 18 | 19 | 20 | return { 21 | categoryId: ownProps.params.id, 22 | campaigns: camps 23 | }; 24 | }; 25 | 26 | const mapDispatchToProps = (dispatch) => { 27 | return { 28 | fetchCampaigns: (number, category) => dispatch(fetchCampaigns(number, category)), 29 | fetchCampaign: (id) => dispatch(fetchCampaign(id)), 30 | getCategories: () => dispatch(getCategories()) 31 | }; 32 | }; 33 | 34 | export default connect(mapStateToProps, mapDispatchToProps)(CategoryShow); 35 | -------------------------------------------------------------------------------- /frontend/components/campaign/user_profile_component.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | class UserProfileBlock extends React.Component { 4 | constructor(props) { 5 | super(props); 6 | this.state = { 7 | user: null 8 | }; 9 | } 10 | 11 | componentDidMount() { 12 | this.props.getUser(this.props.userId).then(usr => { 13 | this.setState({user: usr}); 14 | }); 15 | } 16 | 17 | render() { 18 | if (this.state.user) { 19 | 20 | return ( 21 |
22 |
23 | 24 |
25 | 26 |
27 |
28 | {this.state.user.user.first_name} {this.state.user.user.last_name} 29 |
30 |
31 | {this.state.user.user.about_me} 32 |
33 |
34 |
35 | ); 36 | } else { 37 | return( 38 |
39 | ); 40 | } 41 | } 42 | } 43 | 44 | export default UserProfileBlock; 45 | 46 | 47 | // export default UserProfileBlock; 48 | -------------------------------------------------------------------------------- /app/assets/stylesheets/campaign_new.css: -------------------------------------------------------------------------------- 1 | .create-campaign { 2 | height: 100%; 3 | width: 80%; 4 | 5 | } 6 | 7 | .c-form { 8 | display: flex; 9 | flex-direction: column; 10 | font-size: 18px; 11 | } 12 | 13 | .create-campaign > h1 { 14 | padding-top: 40px; 15 | padding-left: 50px; 16 | font-weight: 400; 17 | font-size: 32px; 18 | font-weight: bold; 19 | } 20 | 21 | .create-campaign > h2 { 22 | padding-left: 50px; 23 | padding-top: 2px; 24 | padding-bottom: 10px; 25 | font-size: 24px; 26 | } 27 | 28 | .target-amount { 29 | padding-left: 50px; 30 | padding-top: 50px; 31 | padding-bottom: 50px; 32 | width: 300px; 33 | } 34 | 35 | .target-amount > input { 36 | width: 50px; 37 | height: 20px; 38 | } 39 | 40 | span { 41 | color: #0eb4b6; 42 | } 43 | 44 | .campaign-title { 45 | padding-left: 50px; 46 | padding-top: 50px; 47 | padding-bottom: 50px; 48 | width: 600px; 49 | } 50 | 51 | .submit-button1 > input { 52 | margin-left: 50px; 53 | margin: 20px 0px; 54 | background-color: #FC3468; 55 | color: white; 56 | border: none; 57 | text-align: center; 58 | min-height: 30px; 59 | margin-left: 50px; 60 | } 61 | 62 | .submit-button1 >input:hover { 63 | background: #ff1854; 64 | cursor: pointer; 65 | } 66 | -------------------------------------------------------------------------------- /frontend/components/categories/category_index.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import CategoryTile from './category_tile'; 3 | 4 | class CategoryIndex extends React.Component { 5 | 6 | constructor(props) { 7 | super(props); 8 | this.makeCategoryTiles = this.makeCategoryTiles.bind(this); 9 | } 10 | 11 | componentWillMount() { 12 | this.props.getCategories(); 13 | } 14 | 15 | makeCategoryTiles() { 16 | const tils = this.props.categories.map((cat, idx) => { 17 | return( 18 |
  • 19 | 20 |
  • 21 | ); 22 | }); 23 | 24 | return tils; 25 | } 26 | 27 | render() { 28 | let catTiles; 29 | if (this.props.categories) { 30 | catTiles = this.makeCategoryTiles(); 31 | } 32 | 33 | if (catTiles) { 34 | 35 | return( 36 |
    37 |

    Explore categories

    38 |
    39 | 40 | {catTiles} 41 |
    42 |
    43 | ); 44 | } else { 45 | 46 | return( 47 |

    Waiting...

    48 | ); 49 | } 50 | 51 | } 52 | 53 | } 54 | 55 | export default CategoryIndex; 56 | -------------------------------------------------------------------------------- /frontend/components/campaign/campaign_show_container.js: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux'; 2 | import { fetchCampaign } from '../../actions/campaign_actions'; 3 | import { getUser } from '../../actions/user_actions'; 4 | import CampaignShow from './campaign_show'; 5 | import { makeContribution } from '../../actions/contribution_actions'; 6 | import { selectRewards } from '../../reducers/selectors'; 7 | import { getAllRewards } from '../../actions/reward_actions'; 8 | 9 | 10 | const mapStateToProps = (state, ownProps) => { 11 | let campaignId = ownProps.params.id; 12 | let usr = state.session.currentUser; 13 | 14 | 15 | return { 16 | rewards: selectRewards(state), 17 | campaignId, 18 | campaign: state.campaigns.campaign, 19 | currentUser: usr, 20 | contributionErrors: state.contribution.errors 21 | }; 22 | }; 23 | 24 | const mapDispatchToProps = (dispatch) => { 25 | 26 | return { 27 | fetchCampaign: (id) => dispatch(fetchCampaign(id)), 28 | getUser: (id) => dispatch(getUser(id)), 29 | makeContribution: (contribution) => dispatch(makeContribution(contribution)), 30 | getAllRewards: (id) => dispatch(getAllRewards(id)) 31 | }; 32 | }; 33 | 34 | export default connect(mapStateToProps, mapDispatchToProps)(CampaignShow); 35 | -------------------------------------------------------------------------------- /app/assets/stylesheets/home_page.css: -------------------------------------------------------------------------------- 1 | .home-page { 2 | display: flex; 3 | flex-direction: column; 4 | width: 100%; 5 | /*height: ;*/ 6 | background: white; 7 | justify-content: flex start; 8 | align-items: center; 9 | position: relative; 10 | } 11 | 12 | .ReactModalPortal { 13 | z-index: 100; 14 | } 15 | .carousel > div { 16 | box-sizing: border-box; 17 | display: flex; 18 | flex-direction: row; 19 | align-items: center; 20 | justify-content: flex start; 21 | height: 95%; 22 | } 23 | 24 | .carousel { 25 | width: 100%; 26 | min-height: 250px; 27 | max-height: 249px; 28 | overflow-x: hidden; 29 | overflow-y: hidden; 30 | } 31 | 32 | .slick-arrow { 33 | display: none; 34 | } 35 | 36 | /*.discover-slider > div { 37 | height: 100%; 38 | padding-top: 200px; 39 | width: 100%; 40 | }*/ 41 | 42 | 43 | .slide > h3 { 44 | height: 100%; 45 | width: 100%; 46 | border: 1px solid #eee; 47 | } 48 | 49 | .slick-track { 50 | height: 100%; 51 | } 52 | 53 | .slider-img { 54 | min-height: 100%; 55 | overflow: hidden; 56 | } 57 | 58 | .carousel-img { 59 | margin: 0; 60 | width: 100%; 61 | height: 100%; 62 | min-height: 250px; 63 | max-height: 320px; 64 | } 65 | .slick-initialized { 66 | height: 100%; 67 | width: 100%; 68 | } 69 | -------------------------------------------------------------------------------- /frontend/components/campaign/campaign_index.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Link, withRouter, hashHistory } from 'react-router'; 3 | import CampaignIndexItem from './campaign_index_item'; 4 | 5 | 6 | class CampaignIndex extends React.Component { 7 | constructor(props) { 8 | super(props); 9 | 10 | } 11 | 12 | componentWillMount() { 13 | this.props.fetchCampaigns(); 14 | } 15 | 16 | render() { 17 | 18 | if (this.props.campaigns[0] ) { 19 | 20 | const AllCampaigns = this.props.campaigns.map((campaign) => { 21 | return( 22 |
  • 23 | 25 |
  • 26 | ); 27 | }); 28 | 29 | return( 30 |
    31 |
    32 |

    All Campaigns

    33 |
    34 |
    35 | {AllCampaigns} 36 |
    37 |
    38 | ); 39 | } else { 40 | return( 41 |

    Waiting...

    42 | ); 43 | } 44 | 45 | } 46 | } 47 | 48 | export default withRouter(CampaignIndex); 49 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "IndieFomo", 3 | "version": "1.0.0", 4 | "description": "== README", 5 | "main": "index.js", 6 | "directories": { 7 | "doc": "docs", 8 | "test": "test" 9 | }, 10 | "scripts": { 11 | "test": "echo \"Error: no test specified\" && exit 1", 12 | "postinstall": "webpack" 13 | }, 14 | "repository": { 15 | "type": "git", 16 | "url": "git+https://github.com/plupinska/IndieFomo.git" 17 | }, 18 | "keywords": [], 19 | "author": "", 20 | "license": "ISC", 21 | "bugs": { 22 | "url": "https://github.com/plupinska/IndieFomo/issues" 23 | }, 24 | "homepage": "https://github.com/plupinska/IndieFomo#readme", 25 | "dependencies": { 26 | "babel-core": "^6.24.1", 27 | "babel-loader": "^6.4.1", 28 | "babel-preset-es2015": "^6.24.1", 29 | "babel-preset-react": "^6.24.1", 30 | "loadash": "0.0.1", 31 | "react": "^15.5.4", 32 | "react-dom": "^15.5.4", 33 | "react-fontawesome": "^1.6.1", 34 | "react-modal": "^1.7.7", 35 | "react-redux": "^5.0.4", 36 | "react-redux-modal": "^0.5.2", 37 | "react-router": "^3.0.5", 38 | "react-slick": "^0.14.11", 39 | "redux": "^3.6.0", 40 | "redux-logger": "^3.0.1", 41 | "redux-thunk": "^2.2.0", 42 | "webpack": "^2.4.1" 43 | }, 44 | "engines": { 45 | "node": "6.9.2", 46 | "npm": "3.10.9" 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /frontend/reducers/campaign_reducer.js: -------------------------------------------------------------------------------- 1 | import { RECEIVE_CAMPAIGNS, RECEIVE_CAMPAIGN, RECEIVE_CAMPAIGN_ERRORS} from '../actions/campaign_actions'; 2 | import merge from 'lodash/merge'; 3 | import { RECEIVE_CONTRIBUTION } from '../actions/contribution_actions'; 4 | 5 | let _oldState = Object.freeze({ 6 | campaign: null, 7 | errors: null 8 | }); 9 | 10 | 11 | const CampaignsReducer = (state = _oldState, action) => { 12 | 13 | switch(action.type) { 14 | case RECEIVE_CAMPAIGNS: 15 | 16 | return action.campaigns; 17 | case RECEIVE_CAMPAIGN: 18 | const newState = merge({}, state); 19 | 20 | return merge(newState, {campaign: action.campaign, errors: ""}); 21 | case RECEIVE_CAMPAIGN_ERRORS: 22 | 23 | const old = merge({}, state); 24 | return merge(old, {errors: action.errors.responseText}); 25 | case "RECEIVE_CONTRIBUTION": 26 | let z = merge({}, state); 27 | let updated = merge(z, {campaign: action.campaign, errors: ""}); 28 | let newAmount = updated.campaign.total_contributions + action.contribution.amount; 29 | let newCount = updated.campaign.num_contributions + 1; 30 | updated.campaign.total_contributions = newAmount; 31 | updated.campaign.num_contributions = newCount; 32 | return updated; 33 | default: 34 | return state; 35 | } 36 | }; 37 | 38 | export default CampaignsReducer; 39 | -------------------------------------------------------------------------------- /frontend/components/campaign/progress_block.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { connect } from 'react-redux'; 3 | 4 | class ProgressBlock extends React.Component { 5 | constructor(props) { 6 | super(props); 7 | 8 | } 9 | 10 | render() { 11 | 12 | const num_backers = this.props.campaign.num_contributions; 13 | let percentage = Math.round((this.props.campaign.total_contributions / this.props.campaign.target_amount)*100) || 0; 14 | return ( 15 |
    16 |
    17 | ${this.props.campaign.total_contributions} USD raised by {this.props.campaign.num_contributions} backers 18 |
    19 |
    20 |
    21 |
    22 |
    23 | {percentage}% of ${this.props.campaign.target_amount} goal 24 |
    25 |
    30 Days left
    26 |
    27 | ); 28 | } 29 | } 30 | 31 | const mapStateToProps = (state) => { 32 | 33 | return { 34 | campaign: state.campaigns.campaign, 35 | }; 36 | }; 37 | 38 | const mapDispatchToProps = (dispatch) => { 39 | return { 40 | 41 | } 42 | } 43 | 44 | 45 | 46 | export default connect(mapStateToProps, mapDispatchToProps)(ProgressBlock); 47 | -------------------------------------------------------------------------------- /app/models/user.rb: -------------------------------------------------------------------------------- 1 | class User < ActiveRecord::Base 2 | validates :email, :password_digest, :session_token, presence: true 3 | validates :password, length: {minimum: 6, allow_nil: true} 4 | after_initialize :ensure_session_token 5 | 6 | has_attached_file :image, default_url: "profile_placeholder.png", 7 | :styles => { 8 | :thumb => "40x40", 9 | :medium => "355x225" 10 | } 11 | validates_attachment_content_type :image, content_type: /\Aimage\/.*\z/ 12 | 13 | has_many :campaigns 14 | has_many :contributions 15 | 16 | attr_reader :password 17 | 18 | def self.generate_session_token! 19 | SecureRandom::urlsafe_base64(16) 20 | end 21 | 22 | def password=(password) 23 | @password = password 24 | self.password_digest = BCrypt::Password.create(password) 25 | end 26 | 27 | def is_password?(password) 28 | BCrypt::Password.new(self.password_digest).is_password?(password) 29 | end 30 | 31 | def self.find_by_credentials(email, password) 32 | user = User.find_by(email: email) 33 | return user if user && user.is_password?(password) 34 | nil 35 | end 36 | 37 | def reset_session_token! 38 | self.session_token = User.generate_session_token! 39 | self.save! 40 | self.session_token 41 | end 42 | 43 | private 44 | def ensure_session_token 45 | self.session_token ||= User.generate_session_token! 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /frontend/actions/session_actions.js: -------------------------------------------------------------------------------- 1 | import * as SessionAPIUtil from '../util/auth_api_util'; 2 | export const RECEIVE_CURRENT_USER = "RECEIVE_CURRENT_USER"; 3 | export const RECEIVE_SESSION_ERRORS = "RECEIVE_SESSION_ERRORS"; 4 | 5 | export const receiveCurrentUser = (currentUser) => { 6 | 7 | return { 8 | type: RECEIVE_CURRENT_USER, 9 | currentUser: currentUser 10 | }; 11 | }; 12 | 13 | export const receiveErrors = (errors) => { 14 | 15 | return { 16 | type: RECEIVE_SESSION_ERRORS, 17 | errors: errors 18 | }; 19 | }; 20 | 21 | export const signup = (user) => (dispatch) => { 22 | 23 | return SessionAPIUtil.signUp(user) 24 | .then((usr) => { 25 | 26 | dispatch(receiveCurrentUser(usr) ); 27 | }, 28 | (err) => { 29 | dispatch(receiveErrors(err.responseJSON)); 30 | 31 | }); 32 | }; 33 | 34 | export const login = (user) => (dispatch) => { 35 | 36 | return SessionAPIUtil.signIn(user) 37 | .then((usr) => dispatch(receiveCurrentUser(usr)), 38 | (err) => dispatch(receiveErrors(err.responseJSON))); 39 | }; 40 | 41 | export const loginguest = () => (dispatch) => { 42 | return SessionAPIUtil.signIn({email: "guest@gmail.com", password: "password" }) 43 | .then((usr) => dispatch(receiveCurrentUser(usr)), 44 | (err) => dispatch(receiveErrors(err.responseJSON))); 45 | }; 46 | 47 | export const logout = () => (dispatch) => { 48 | 49 | return SessionAPIUtil.signOut() 50 | .then(user => dispatch(receiveCurrentUser(null))); 51 | }; 52 | -------------------------------------------------------------------------------- /frontend/components/user/campaigns.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {Link} from 'react-router'; 3 | class UserCampaignList extends React.Component { 4 | 5 | constructor(props) { 6 | super(props); 7 | 8 | } 9 | 10 | makeTile() { 11 | 12 | return this.props.campaigns.map((camp, idx) => { 13 | 14 | return( 15 |
    16 | 17 | 18 | 19 |
    20 |
    21 | Campaign Title: {camp.title} 22 |
    23 |
    24 | 25 |
    26 |
    27 |
    Total Contributions: {camp.total_contributions}
    28 |
    % of Target: {Math.floor((camp.total_contributions/camp.target_amount) * 100)}
    29 |
    30 |
    31 |
    32 | ); 33 | }); 34 | } 35 | 36 | render() { 37 | const tiles = this.makeTile(); 38 | return( 39 |
    40 | {tiles} 41 |
    42 | ); 43 | } 44 | 45 | } 46 | 47 | export default UserCampaignList; 48 | -------------------------------------------------------------------------------- /app/assets/stylesheets/categories.css: -------------------------------------------------------------------------------- 1 | .cat-wrap { 2 | display: flex; 3 | flex-direction: column; 4 | min-height: 575px; 5 | } 6 | 7 | .cat-wrap > h1 { 8 | margin-left: 11px; 9 | } 10 | 11 | .categories { 12 | margin: 30px; 13 | display: flex; 14 | flex-direction: column; 15 | justify-content: center; 16 | align-items: center; 17 | font-size: 18px; width: 950px; 18 | min-height: 550px; 19 | } 20 | 21 | .category-container { 22 | display: flex; 23 | flex-direction: row; 24 | flex-wrap: wrap; 25 | justify-content: space-around; 26 | align-items: center; 27 | width: 950px; 28 | min-height: 500px; 29 | } 30 | 31 | .category-tile { 32 | background: black; 33 | /*z-index: 7;*/ 34 | } 35 | 36 | .category-tile img { 37 | opacity: 0.5; 38 | } 39 | 40 | .category-tile img:hover { 41 | opacity: 0.8; 42 | cursor: pointer; 43 | } 44 | 45 | 46 | .category-tile > a { 47 | display: flex; 48 | flex-direction: column; 49 | align-items: center; 50 | text-align: center; 51 | color: white; 52 | z-index: 10; 53 | } 54 | 55 | .category-tile > a:hover { 56 | display: flex; 57 | flex-direction: column; 58 | align-items: center; 59 | text-align: center; 60 | color: white; 61 | z-index: 10; 62 | 63 | } 64 | 65 | .title:hover { 66 | opacity: 0.8; 67 | cursor: pointer; 68 | } 69 | 70 | .title { 71 | position: absolute; 72 | margin-top: 100px; 73 | width: 140px; 74 | background: transparent; 75 | font-weight: bold; 76 | text-align: center; 77 | font-size: 16px; 78 | } 79 | -------------------------------------------------------------------------------- /config/application.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path('../boot', __FILE__) 2 | 3 | require 'rails/all' 4 | 5 | # Require the gems listed in Gemfile, including any gems 6 | # you've limited to :test, :development, or :production. 7 | Bundler.require(*Rails.groups) 8 | 9 | module IndieFomo 10 | class Application < Rails::Application 11 | # Settings in config/environments/* take precedence over those specified here. 12 | # Application configuration should go into files in config/initializers 13 | # -- all .rb files in that directory are automatically loaded. 14 | 15 | # Set Time.zone default to the specified zone and make Active Record auto-convert to this zone. 16 | # Run "rake -D time" for a list of tasks for finding time zone names. Default is UTC. 17 | # config.time_zone = 'Central Time (US & Canada)' 18 | 19 | # The default locale is :en and all translations from config/locales/*.rb,yml are auto loaded. 20 | # config.i18n.load_path += Dir[Rails.root.join('my', 'locales', '*.{rb,yml}').to_s] 21 | # config.i18n.default_locale = :de 22 | 23 | config.paperclip_defaults = { 24 | :storage => :s3, 25 | :s3_protocol => :https, 26 | :s3_credentials => { 27 | :bucket => ENV["s3_bucket"], 28 | :access_key_id => ENV["s3_access_key_id"], 29 | :secret_access_key => ENV["s3_secret_access_key"], 30 | :s3_region => ENV["s3_region"] 31 | } 32 | } 33 | 34 | # Do not swallow errors in after_commit/after_rollback callbacks. 35 | config.active_record.raise_in_transactional_callbacks = true 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /app/views/api/users/_user.json.jbuilder: -------------------------------------------------------------------------------- 1 | 2 | if (@user) 3 | json.id @user.id 4 | json.first_name @user.first_name 5 | json.last_name @user.last_name 6 | json.email @user.email 7 | json.image_url asset_path(@user.image.url) 8 | json.about_me @user.about_me 9 | 10 | if (@contributions) 11 | json.contributions @contributions.each do |contribution| 12 | json.user_id contribution.user_id 13 | json.reward_id contribution.reward_id 14 | 15 | if (contribution.reward) 16 | json.reward Reward.find(contribution.reward_id) 17 | end 18 | json.amount contribution.amount 19 | json.campaign_id contribution.campaign_id 20 | json.date contribution.date 21 | json.campaign_title Campaign.find(contribution.campaign_id).title 22 | end 23 | end 24 | 25 | if (@user.campaigns) 26 | json.campaigns @user.campaigns.each do |campaign| 27 | json.num_contributions Contribution.where(campaign_id: campaign.id).count 28 | json.total_contributions Contribution.where(campaign_id: campaign.id).sum(:amount) 29 | json.user campaign.user 30 | json.id campaign.id 31 | json.title campaign.title 32 | json.descriptions campaign.descriptions 33 | json.tagline campaign.tagline 34 | json.category_id campaign.category_id 35 | json.image_url campaign.image_url 36 | json.end_date campaign.end_date 37 | json.target_amount campaign.target_amount 38 | json.image_url asset_path(campaign.image.url) 39 | end 40 | end 41 | else 42 | {user: nil} 43 | end 44 | -------------------------------------------------------------------------------- /docs/sample-state.md: -------------------------------------------------------------------------------- 1 | ```js 2 | // session slice 3 | { 4 | session: { 5 | currentUser: { 6 | id: 1, 7 | username: 'patrycja' 8 | }, 9 | errors: [] 10 | } 11 | } 12 | 13 | 14 | { 15 | currentUser: { 16 | id: 1, 17 | fname: 'p' 18 | lname: 'l', 19 | username: 'pl' 20 | Mycampaigns: { 21 | 1: { 22 | owner: {username, name}, 23 | title: "Taco Stand", 24 | description: "Veggie Tacos", 25 | tagline: "Taco your soul", 26 | category: "Food", 27 | image: 'url', 28 | time_left: '5', 29 | target_amount: '10000000', 30 | contributions: '4', 31 | reward: { 32 | id: { 33 | title: title, 34 | description: description, 35 | image_url: url, 36 | price: price 37 | } 38 | } 39 | } 40 | } 41 | }, 42 | 43 | forms: { 44 | signup: {errors: []}, 45 | login: {errors: []}, 46 | createCampaign: {errors: []} 47 | addReward: {errors: []} 48 | }, 49 | 50 | campaigns: { 51 | id: { 52 | owner: {username, name}, 53 | title: "Taco Stand", 54 | description: "Veggie Tacos", 55 | tagline: "Taco your soul", 56 | category: "Food", 57 | image: 'url', 58 | time_left: '5', 59 | target_amount: '10000000', 60 | contributions: '4', 61 | rewards: { 62 | id: { 63 | title: title, 64 | description: description, 65 | image_url: url, 66 | price: price 67 | } 68 | } 69 | } 70 | } 71 | 72 | 73 | 74 | 75 | } 76 | 77 | 78 | 79 | 80 | 81 | ``` 82 | -------------------------------------------------------------------------------- /public/500.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | We're sorry, but something went wrong (500) 5 | 6 | 55 | 56 | 57 | 58 | 59 |
    60 |
    61 |

    We're sorry, but something went wrong.

    62 |
    63 |

    If you are the application owner check the logs for more information.

    64 |
    65 | 66 | 67 | -------------------------------------------------------------------------------- /public/422.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | The change you wanted was rejected (422) 5 | 6 | 55 | 56 | 57 | 58 | 59 |
    60 |
    61 |

    The change you wanted was rejected.

    62 |

    Maybe you tried to change something you didn't have access to.

    63 |
    64 |

    If you are the application owner check the logs for more information.

    65 |
    66 | 67 | 68 | -------------------------------------------------------------------------------- /Guardfile: -------------------------------------------------------------------------------- 1 | # A sample Guardfile 2 | # More info at https://github.com/guard/guard#readme 3 | 4 | ## Uncomment and set this to only include directories you want to watch 5 | # directories %w(app lib config test spec features) \ 6 | # .select{|d| Dir.exists?(d) ? d : UI.warning("Directory #{d} does not exist")} 7 | 8 | ## Note: if you are using the `directories` clause above and you are not 9 | ## watching the project directory ('.'), then you will want to move 10 | ## the Guardfile to a watched dir and symlink it back, e.g. 11 | # 12 | # $ mkdir config 13 | # $ mv Guardfile config/ 14 | # $ ln -s config/Guardfile . 15 | # 16 | # and, you'll have to watch "config/Guardfile" instead of "Guardfile" 17 | 18 | guard 'livereload' do 19 | extensions = { 20 | css: :css, 21 | scss: :css, 22 | sass: :css, 23 | js: :js, 24 | coffee: :js, 25 | html: :html, 26 | png: :png, 27 | gif: :gif, 28 | jpg: :jpg, 29 | jpeg: :jpeg, 30 | # less: :less, # uncomment if you want LESS stylesheets done in browser 31 | } 32 | 33 | rails_view_exts = %w(erb haml slim) 34 | 35 | # file types LiveReload may optimize refresh for 36 | compiled_exts = extensions.values.uniq 37 | watch(%r{public/.+\.(#{compiled_exts * '|'})}) 38 | 39 | extensions.each do |ext, type| 40 | watch(%r{ 41 | (?:app|vendor) 42 | (?:/assets/\w+/(?[^.]+) # path+base without extension 43 | (?\.#{ext})) # matching extension (must be first encountered) 44 | (?:\.\w+|$) # other extensions 45 | }x) do |m| 46 | path = m[1] 47 | "/assets/#{path}.#{type}" 48 | end 49 | end 50 | 51 | # file needing a full reload of the page anyway 52 | watch(%r{app/views/.+\.(#{rails_view_exts * '|'})$}) 53 | watch(%r{app/helpers/.+\.rb}) 54 | watch(%r{config/locales/.+\.yml}) 55 | end 56 | -------------------------------------------------------------------------------- /public/404.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | The page you were looking for doesn't exist (404) 5 | 6 | 55 | 56 | 57 | 58 | 59 |
    60 |
    61 |

    The page you were looking for doesn't exist.

    62 |

    You may have mistyped the address or the page may have moved.

    63 |
    64 |

    If you are the application owner check the logs for more information.

    65 |
    66 | 67 | 68 | -------------------------------------------------------------------------------- /frontend/components/campaign/campaign_index_item.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { withRouter } from 'react-router'; 3 | 4 | class CampaignIndexItem extends React.Component { 5 | 6 | constructor(props) { 7 | super(props); 8 | this.handleClick = this.handleClick.bind(this); 9 | 10 | } 11 | 12 | handleClick(e) { 13 | e.preventDefault(); 14 | const url = `/campaigns/${this.props.campaign.id}`; 15 | this.props.router.push(url); 16 | } 17 | 18 | render() { 19 | 20 | const percentage = Math.round((this.props.campaign.total_contributions / this.props.campaign.target_amount)*100); 21 | 22 | return( 23 |
    24 | {this.props.campaign.title} 26 | 27 |
    28 |
    29 |
    {this.props.campaign.title}
    30 |
    {this.props.campaign.tagline}
    31 |
    32 | 33 |
    34 |
    35 | ${this.props.campaign.total_contributions} USD raised by {this.props.campaign.num_contributions} backers 36 |
    37 | 38 |
    39 |
    40 |
    41 | 42 |
    43 | {percentage}% of ${this.props.campaign.target_amount} goal 44 |
    45 |
    30 days left
    46 |
    47 |
    48 | 49 |
    50 | ); 51 | } 52 | 53 | } 54 | 55 | export default withRouter(CampaignIndexItem); 56 | -------------------------------------------------------------------------------- /config/environments/development.rb: -------------------------------------------------------------------------------- 1 | Rails.application.configure do 2 | # Settings specified here will take precedence over those in config/application.rb. 3 | 4 | # In the development environment your application's code is reloaded on 5 | # every request. This slows down response time but is perfect for development 6 | # since you don't have to restart the web server when you make code changes. 7 | config.cache_classes = false 8 | 9 | # Do not eager load code on boot. 10 | config.eager_load = false 11 | 12 | # Show full error reports and disable caching. 13 | config.consider_all_requests_local = true 14 | config.action_controller.perform_caching = false 15 | 16 | # Don't care if the mailer can't send. 17 | config.action_mailer.raise_delivery_errors = false 18 | 19 | # Automatically inject JavaScript needed for LiveReload 20 | config.middleware.insert_after(ActionDispatch::Static, Rack::LiveReload) 21 | 22 | # Print deprecation notices to the Rails logger. 23 | config.active_support.deprecation = :log 24 | 25 | # Raise an error on page load if there are pending migrations. 26 | config.active_record.migration_error = :page_load 27 | 28 | # Debug mode disables concatenation and preprocessing of assets. 29 | # This option may cause significant delays in view rendering with a large 30 | # number of complex assets. 31 | config.assets.debug = true 32 | 33 | # Asset digests allow you to set far-future HTTP expiration dates on all assets, 34 | # yet still be able to expire them through the digest params. 35 | config.assets.digest = true 36 | 37 | # Adds additional error checking when serving assets at runtime. 38 | # Checks for improperly declared sprockets dependencies. 39 | # Raises helpful error messages. 40 | config.assets.raise_runtime_errors = true 41 | 42 | # Raises error for missing translations 43 | # config.action_view.raise_on_missing_translations = true 44 | end 45 | -------------------------------------------------------------------------------- /config/environments/test.rb: -------------------------------------------------------------------------------- 1 | Rails.application.configure do 2 | # Settings specified here will take precedence over those in config/application.rb. 3 | 4 | # The test environment is used exclusively to run your application's 5 | # test suite. You never need to work with it otherwise. Remember that 6 | # your test database is "scratch space" for the test suite and is wiped 7 | # and recreated between test runs. Don't rely on the data there! 8 | config.cache_classes = true 9 | 10 | # Do not eager load code on boot. This avoids loading your whole application 11 | # just for the purpose of running a single test. If you are using a tool that 12 | # preloads Rails for running tests, you may have to set it to true. 13 | config.eager_load = false 14 | 15 | # Configure static file server for tests with Cache-Control for performance. 16 | config.serve_static_files = true 17 | config.static_cache_control = 'public, max-age=3600' 18 | 19 | # Show full error reports and disable caching. 20 | config.consider_all_requests_local = true 21 | config.action_controller.perform_caching = false 22 | 23 | # Raise exceptions instead of rendering exception templates. 24 | config.action_dispatch.show_exceptions = false 25 | 26 | # Disable request forgery protection in test environment. 27 | config.action_controller.allow_forgery_protection = false 28 | 29 | # Tell Action Mailer not to deliver emails to the real world. 30 | # The :test delivery method accumulates sent emails in the 31 | # ActionMailer::Base.deliveries array. 32 | config.action_mailer.delivery_method = :test 33 | 34 | # Randomize the order test cases are executed. 35 | config.active_support.test_order = :random 36 | 37 | # Print deprecation notices to the stderr. 38 | config.active_support.deprecation = :stderr 39 | 40 | # Raises error for missing translations 41 | # config.action_view.raise_on_missing_translations = true 42 | end 43 | -------------------------------------------------------------------------------- /docs/schema.md: -------------------------------------------------------------------------------- 1 | # Schema Information 2 | 3 | 4 | ## user 5 | 6 | Column Name | Data Type | Details 7 | ------------ | ------------- | ------------ 8 | id | integer | not null, primary key 9 | username | string | not null, indexed, unique 10 | first_name | string| not null, indexed 11 | last_name | string | not null, indexed 12 | about me | text | not null 13 | email | string | not null, indexed, unique 14 | password_digest | string | not null 15 | session_token | string | not null, indexed, unique 16 | location | string| not null 17 | image_url | string | indexed 18 | 19 | ## campaign 20 | 21 | Column Name | Data Type | Details 22 | ------------ | ------------- | ------------ 23 | id | integer | not null, primary key 24 | user_id | integer | not null, indexed, unique, foreign key 25 | title | string | not null, indexed 26 | description | text | not null, indexed 27 | tagline | string | not null, indexed 28 | category_id | integer | not null 29 | image_url | integer | indexed 30 | end_date | date | not null, indexed 31 | target_amount | integer | not null, indexed 32 | 33 | 34 | ## contribution 35 | 36 | Column Name | Data Type | Details 37 | ------------ | ------------- | ------------ 38 | id | integer | not null, primary key 39 | user_id | string | not null, indexed, unique, foreign key 40 | reward_id | integer | not null, foreign_key 41 | amount | float | not null, indexed 42 | 43 | ## reward 44 | 45 | Column Name | Data Type | Details 46 | ------------ | ------------- | ------------ 47 | id | integer | not null, primary key 48 | campaign_id | integer | not null, indexed, foreign key 49 | title | string| not null, indexed 50 | description | string | not null, indexed 51 | image_url | string | not null, indexed 52 | price | integer | not null 53 | 54 | ## category 55 | 56 | Column Name | Data Type | Details 57 | ------------ | ------------- | ------------ 58 | id | integer | not null, primary key 59 | type | string | not null, unique 60 | -------------------------------------------------------------------------------- /frontend/actions/campaign_actions.js: -------------------------------------------------------------------------------- 1 | import * as CampaignApiUtil from '../util/campaign_api_util'; 2 | export const RECEIVE_CAMPAIGNS = "RECEIVE_CAMPAIGNS"; 3 | export const RECEIVE_CAMPAIGN = "RECEIVE_CAMPAIGN"; 4 | export const RECEIVE_CAMPAIGN_ERRORS = "RECEIVE_CAMPAIGN_ERRORS"; 5 | 6 | export const receiveCampaigns = (campaigns) => { 7 | 8 | return { 9 | type: RECEIVE_CAMPAIGNS, 10 | campaigns: campaigns 11 | }; 12 | }; 13 | 14 | export const receiveCampaign = (campaign) => { 15 | 16 | return { 17 | type: RECEIVE_CAMPAIGN, 18 | campaign: campaign 19 | }; 20 | }; 21 | 22 | export const receiveErrors = (errors) => { 23 | return { 24 | type: RECEIVE_CAMPAIGN_ERRORS, 25 | errors: errors 26 | }; 27 | }; 28 | 29 | export const fetchCampaigns = (num, cat) => (dispatch) => { 30 | 31 | return CampaignApiUtil.getCampaigns(num, cat) 32 | .then((campaigns) => dispatch(receiveCampaigns(campaigns)), 33 | (err) => dispatch(receiveErrors(err))); 34 | }; 35 | 36 | export const fetchCampaign = (id) => (dispatch) => { 37 | return CampaignApiUtil.getCampaign(id) 38 | .then((camp) => dispatch(receiveCampaign(camp)), 39 | (err) => dispatch(receiveErrors(err))); 40 | }; 41 | 42 | export const createCampaign = (campaign) => (dispatch) => { 43 | 44 | return CampaignApiUtil.createCampaign(campaign) 45 | .then((camp) => dispatch(receiveCampaign(camp)), 46 | (err) => dispatch(receiveErrors(err))); 47 | }; 48 | 49 | export const updateCampaign = (campaign) => (dispatch) => { 50 | 51 | return CampaignApiUtil.updateCampaign(campaign) 52 | .then((camp) => dispatch(receiveCampaign(camp)), 53 | (err) => dispatch(receiveErrors(err))); 54 | }; 55 | 56 | export const updateCampaignForm = (campaign) => (dispatch) => { 57 | 58 | return CampaignApiUtil.updateCampaignForm(campaign) 59 | .then((camp) => dispatch(receiveCampaign(camp)), 60 | (err) => dispatch(receiveErrors(err))); 61 | } 62 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | 4 | # Bundle edge Rails instead: gem 'rails', github: 'rails/rails' 5 | gem 'rails', '4.2.8' 6 | # Use sqlite3 as the database for Active Record 7 | gem 'pg' 8 | # Use SCSS for stylesheets 9 | gem 'sass-rails', '~> 5.0' 10 | # Use Uglifier as compressor for JavaScript assets 11 | gem 'uglifier', '>= 1.3.0' 12 | # Use CoffeeScript for .coffee assets and views 13 | gem 'coffee-rails', '~> 4.1.0' 14 | # See https://github.com/rails/execjs#readme for more supported runtimes 15 | # gem 'therubyracer', platforms: :ruby 16 | 17 | # Use jquery as the JavaScript library 18 | gem 'jquery-rails' 19 | # Build JSON APIs with ease. Read more: https://github.com/rails/jbuilder 20 | gem 'jbuilder', '~> 2.0' 21 | # bundle exec rake doc:rails generates the API under doc/api. 22 | gem 'sdoc', '~> 0.4.0', group: :doc 23 | 24 | # Use ActiveModel has_secure_password 25 | gem 'bcrypt', '~> 3.1.7' 26 | gem 'paperclip' 27 | gem 'aws-sdk' 28 | gem "figaro" 29 | # Use Unicorn as the app server 30 | # gem 'unicorn' 31 | gem 'momentjs-rails' 32 | # Use Capistrano for deployment 33 | # gem 'capistrano-rails', group: :development 34 | gem 'stripe' 35 | gem 'faker' 36 | group :development, :test do 37 | # Call 'byebug' anywhere in the code to stop execution and get a console 38 | gem 'byebug' 39 | gem 'annotate' 40 | gem 'better_errors' 41 | gem 'binding_of_caller' 42 | gem 'pry-rails' 43 | end 44 | 45 | group :development do 46 | # Access an IRB console on exception pages or by using <%= console %> in views 47 | gem 'web-console', '~> 2.0' 48 | # for css uploads 49 | gem 'guard' 50 | gem 'guard-livereload' 51 | gem 'rack-livereload' 52 | gem 'rb-fsevent' 53 | # Spring speeds up development by keeping your application running in the background. Read more: https://github.com/rails/spring 54 | gem 'spring' 55 | gem "pg_search" 56 | end 57 | 58 | group :production do 59 | gem 'rails_12factor' 60 | gem "pg_search" 61 | end 62 | -------------------------------------------------------------------------------- /frontend/components/categories/category_show.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import CampaignIndexItem from '../campaign/campaign_index_item'; 3 | 4 | class CategoryShow extends React.Component { 5 | 6 | constructor(props) { 7 | super(props); 8 | this.makeCampaignTiles = this.makeCampaignTiles.bind(this); 9 | this.allOfCategory = this.allOfCategory.bind(this); 10 | this.state = { 11 | category: null, 12 | }; 13 | } 14 | 15 | componentWillMount() { 16 | this.props.fetchCampaigns("all", this.props.categoryId).then((cats) => { 17 | 18 | cats = Object.keys(cats.campaigns).map(camp => cats.campaigns[camp]) 19 | this.setState({ 20 | category: cats[0].cat 21 | }) 22 | }); 23 | } 24 | 25 | makeCampaignTiles() { 26 | const tiles = this.props.campaigns.map((camp, idx) => { 27 | return( 28 |
  • 29 | 31 |
  • 32 | ); 33 | }); 34 | 35 | return tiles; 36 | } 37 | 38 | allOfCategory() { 39 | let catId = this.props.categoryId; 40 | let uniformCategory = true; 41 | 42 | this.props.campaigns.forEach((camp) => { 43 | if (camp.category_id != catId) { 44 | uniformCategory = false; 45 | } 46 | }); 47 | 48 | return uniformCategory; 49 | } 50 | 51 | render() { 52 | 53 | let tiles = []; 54 | if (this.props.campaigns[0] && this.allOfCategory()) { 55 | tiles = this.makeCampaignTiles(); 56 | } 57 | 58 | if (tiles.length > 0) { 59 | return( 60 |
    61 |

    {this.state.category}

    62 |
    63 | {tiles} 64 |
    65 |
    66 | ); 67 | } else { 68 | return( 69 |

    Waiting..

    70 | ); 71 | } 72 | } 73 | } 74 | 75 | export default CategoryShow; 76 | -------------------------------------------------------------------------------- /frontend/actions/reward_actions.js: -------------------------------------------------------------------------------- 1 | import * as RewardsApiUtil from '../util/rewards_api_util'; 2 | export const RECEIVE_REWARD = 'RECEIVE_REWARD'; 3 | export const RECEIVE_REWARD_ERRORS = 'RECEIVE_REWARD_ERRORS'; 4 | export const RECEIVE_ALL_REWARDS = 'RECEIVE_ALL_REWARDS'; 5 | export const DELETE_REWARD = 'DELETE_REWARD'; 6 | 7 | export const receiveReward = (reward) => { 8 | return { 9 | type: RECEIVE_REWARD, 10 | reward: reward 11 | }; 12 | }; 13 | 14 | export const receiveAllRewards = (rewards) => { 15 | return { 16 | type: RECEIVE_ALL_REWARDS, 17 | rewards: rewards 18 | }; 19 | }; 20 | 21 | export const receiveRewardErrors = (errors) => { 22 | return { 23 | type: RECEIVE_REWARD_ERRORS, 24 | errors: errors 25 | }; 26 | }; 27 | 28 | export const deleteReward = (reward) => { 29 | 30 | return { 31 | type: DELETE_REWARD, 32 | reward 33 | } 34 | } 35 | 36 | export const createReward = (reward) => (dispatch) => { 37 | 38 | return RewardsApiUtil.createReward(reward) 39 | .then((rew) => dispatch(receiveReward(rew)), 40 | (err) => dispatch(receiveRewardErrors(err))); 41 | }; 42 | 43 | export const editReward = (reward) => (dispatch) => { 44 | return RewardsApiUtil.editReward(reward) 45 | .then((rew) => dispatch(receiveReward(rew)), 46 | (err) => dispatch(receiveRewardErrors(err))); 47 | }; 48 | 49 | export const removeReward = (reward) => (dispatch) => { 50 | 51 | return RewardsApiUtil.deleteReward(reward) 52 | .then((rew) => dispatch(deleteReward(rew)), 53 | (err) => dispatch(receiveRewardErrors(err))); 54 | }; 55 | 56 | export const getReward = (reward) => (dispatch) => { 57 | return RewardsApiUtil.getReward(reward) 58 | .then((rew) => dispatch(receiveReward(rew)), 59 | (err) => dispatch(receiveRewardErrors(err))); 60 | }; 61 | 62 | export const getAllRewards = (id) => (dispatch) => { 63 | return RewardsApiUtil.getAllRewards(id) 64 | .then((rewards) => dispatch(receiveAllRewards(rewards), 65 | (err) => dispatch(receiveAllRewards))); 66 | }; 67 | -------------------------------------------------------------------------------- /docs/PRODUCTION_README.md: -------------------------------------------------------------------------------- 1 | [IndieFomo](https://github.com/plupinska/IndieFomo/blob/master/docs/api-endpoints) 2 | 3 | # IndieFomo 4 | Indie Fomo is a web aplication inspired by Indiegogo built using Ruby on Rails on the backend, a PostgresQL database, and React/Redux front end framework. 5 | ![HomePage](/docs/images/IndieFomo Homepage.png) 6 | ## Features And Implementation 7 | 8 | ### Campaign Creation 9 | On the backend, campaigns are stored in a single table with foreign keys connecting each campaign to it's owner and category. Each campaign has many associated rewards which are stored in a separate table. Each respective campaign has two associated views. First of which is the campaign show page that lists campaign funding details, a description, and its associated rewards. The second view is an overview tile that links the user to the campaign show page. 10 | ![Campaign](/docs/images/campaign-creation.png) 11 | ### Reward Creation 12 | On the backend, rewards are stored in a single table with a foreign key associating each reward to its campaign. Each reward is structured as a nested resource under its campaign so users can add rewards as they create their campaigns. Rewards are rendered twice in a 'preview' mode where users can delete and edit rewards before launching their finished campaigns as well as in a final show view that is rendered on the campaign show page. 13 | ![Rewards](/docs/images/rewards.png) 14 | ### Contributions 15 | On the backend, contributions are stored in a single table with foreign keys connecting them to a user, optional reward, and campaign associated with each contribution. Users can make contributions on the campaign show page by either opting to purchase a reward or by creating a custom amount contribution. Total contributions as well as the contribution count are rendered on the campaign show page. The contributions are represented as a progress bar that expresses the total contributions as a percentage of target campaign amount. The progress bar dynamically updates as users donate to the campaign. 16 | ![ProgressBar](/docs/images/progress-bar.png) 17 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # IndieFomo 2 | 3 | 4 | ## Minimum Viable Product 5 | Indie Fomo is a web aplication inspired by Indiegogo built using Ruby on Rails 6 | on the backend, a PostgresQL database, and React/Redux front end framework. 7 | By the end of week 9, this app will have at minimum: 8 | 9 | - [] Hosting on Heroku 10 | - [] A production README, replacing this README 11 | - [] New account creation, login, and guest/demo login 12 | - [] Profiles (User has a profile, can edit certain information) 13 | - [] Campaigns (Create and Update) 14 | - [] Contributions (users can contribute to campaigns) 15 | - [] Rewards (Listed on Campaign Show pages as well as user profile) 16 | - [] Bonus: Categories 17 | - [] Bonus: Search 18 | 19 | ## Design Docs 20 | * [Api-Endpoints](https://github.com/plupinska/IndieFomo/blob/master/docs/api-endpoints) 21 | * [Component-Hierarchy](https://github.com/plupinska/IndieFomo/blob/master/docs/component-hierarchy) 22 | * [Sample-State](https://github.com/plupinska/IndieFomo/blob/master/docs/sample-state.md) 23 | * [Schema](https://github.com/plupinska/IndieFomo/blob/master/docs/schema.md) 24 | * [Link To Heroku](https://indiefomo.herokuapp.com/) 25 | ## Implementation Timeline 26 | 27 | ### Phase 1: Backend setup and Front End User Authentication (2 days) 28 | 29 | **Objective:** Functioning rails project with front-end Authentication. Work on Nav bar. 30 | 31 | ### Phase 2: User Model, API, and components (2 days) 32 | 33 | **Objective:** User can sign up, log in, and view/edit their Profile. 34 | 35 | ### Phase 3: Campaigns (2 days) 36 | 37 | **Objective:** Finish create and Edit forms for Campaigns. A Campaign can be created, user can navigate to its show page. 38 | 39 | ### Phase 4: Rewards (1 day) 40 | 41 | **Objective:** User is able to Add a Reward Component. Add Reward component to Campaign Show page and User Profile. 42 | 43 | ### Phase 5: Work on Landing Page (2/3 days) 44 | 45 | **Objective:** Work on creating components for landing page. Work on Styling. 46 | 47 | 48 | 49 | ### Bonus Features (TBD) 50 | - [ ] Search Component 51 | - [ ] Category 52 | -------------------------------------------------------------------------------- /frontend/components/campaign/campaign_show.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { withRouter } from 'react-router'; 3 | import CampaignOverview from './campaign_overview_component'; 4 | import RewardsShowPage from './rewards/rewards_showpage'; 5 | 6 | class CampaignShow extends React.Component { 7 | 8 | constructor(props) { 9 | super(props); 10 | } 11 | 12 | componentWillMount() { 13 | this.props.fetchCampaign(this.props.campaignId); 14 | this.props.getAllRewards(this.props.campaignId); 15 | } 16 | 17 | componentWillReceiveProps(newProps) { 18 | 19 | if (this.props.campaignId !== newProps.params.id) { 20 | this.props.fetchCampaign(newProps.params.id); 21 | this.props.getAllRewards(newProps.params.id); 22 | } 23 | } 24 | 25 | render() { 26 | 27 | if (this.props.campaign) { 28 | return( 29 |
    30 |
    31 | 41 |
    42 | 43 |
    44 |
    45 |

    Overview

    46 | {this.props.campaign.descriptions} 47 |
    48 | 49 |
    50 |

    Rewards

    51 | 53 |
    54 |
    55 |
    56 | ); 57 | } else { 58 | return ( 59 |

    Waiting..

    60 | ); 61 | } 62 | } 63 | } 64 | export default withRouter(CampaignShow); 65 | -------------------------------------------------------------------------------- /frontend/components/user/contributions.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {Link} from 'react-router'; 3 | class UserContributions extends React.Component { 4 | constructor(props) { 5 | super(props); 6 | 7 | } 8 | 9 | formatDate(str) { 10 | let monthHash = {1: 'Jan', 2: 'Feb', 3: 'Mar', 4: 'Apr', 5: 'May', 11 | 6: 'Jun', 7: 'Jul', 8: 'Aug', 9: 'Sept', 10: 'Oct', 11: 'Nov', 12: 'Dec'}; 12 | 13 | let date = str; 14 | if (str.includes('-')) { 15 | date = str.split('-'); 16 | let yy = date[0]; 17 | let dd = date[2]; 18 | let month = parseInt(date[1]); 19 | month = monthHash[month]; 20 | 21 | date = month + ' ' + dd + ' ' + yy; 22 | } 23 | 24 | return date; 25 | } 26 | 27 | makeTile() { 28 | 29 | return this.props.contributions.map((contribution, idx) => { 30 | let date = this.formatDate(contribution.date); 31 | let reward; 32 | 33 | if (contribution.reward_id) { 34 | reward = contribution.reward.title; 35 | } else { 36 | reward = "No reward for this one :("; 37 | } 38 | return( 39 |
    40 |
    {date}
    41 |
    {contribution.campaign_title}
    42 |
    {contribution.amount}
    43 |
    {reward}
    44 |
    45 | ); 46 | }); 47 | } 48 | 49 | render() { 50 | const contributions = this.makeTile(); 51 | 52 | return( 53 |
    54 |
    55 |
    Date
    56 |
    Campaign
    57 |
    Amount
    58 |
    Reward
    59 |
    60 | 61 |
    62 | {contributions} 63 |
    64 |
    65 | ); 66 | } 67 | } 68 | 69 | export default UserContributions; 70 | -------------------------------------------------------------------------------- /app/controllers/api/campaigns_controller.rb: -------------------------------------------------------------------------------- 1 | class Api::CampaignsController < ApplicationController 2 | 3 | def index 4 | #look into services(Service Objects) 5 | if params["num"] == "all"; 6 | if params["category"] === "none" 7 | @campaigns = Campaign.includes(:user, :contributions).all 8 | elsif params["category"] != "none" 9 | if params["category"].to_i > 0 10 | @campaigns = Campaign.includes(:category).where(category_id: params["category"].to_i) 11 | else 12 | category_id = Category.find(cat: params["category"]) 13 | @campaigns = Campaign.where(category_id: category_id) 14 | end 15 | else 16 | @campaigns = Campaign.where(cat: params["cat"]) 17 | end 18 | else params["num"].to_i.is_a?(Integer) 19 | @campaigns = Campaign.includes(:user, :contributions).take(params["num"].to_i) 20 | end 21 | render :index 22 | end 23 | 24 | def create 25 | @campaign = Campaign.create(campaign_params) 26 | 27 | if @campaign.save 28 | render :show 29 | else 30 | render json: @campaign.errors.full_messages, status:422 31 | end 32 | end 33 | 34 | def show 35 | @campaign = Campaign.find(params[:id]) 36 | contribution = Contribution.includes(:campaign).where(campaign_id: params[:id]) 37 | @num_contributions = contribution.count 38 | @total_contributions = contribution.sum(:amount) 39 | render :show 40 | end 41 | 42 | def update 43 | @campaign = Campaign.find(params[:id]) 44 | 45 | if @campaign.update(campaign_params) 46 | category = Category.find_by(cat: category_title_param[:category_name]).id 47 | @campaign.update(category_id: category) 48 | render :show 49 | else 50 | render json: @campaign.errors.full_messages, status: 422 51 | end 52 | end 53 | 54 | def destroy 55 | @campaign = Campaign.find(params[:id]) 56 | @campaign.destroy! 57 | render :show 58 | end 59 | 60 | private 61 | def campaign_params 62 | params.require(:campaign).permit(:user_id, :title, :descriptions, 63 | :tagline, :image, :end_date, :target_amount) 64 | end 65 | 66 | def category_title_param 67 | params.require(:campaign).permit(:category_name) 68 | end 69 | end 70 | -------------------------------------------------------------------------------- /frontend/components/campaign/rewards/rewards_showpage.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { connect } from 'react-redux'; 3 | import Reward from './reward'; 4 | import {removeReward} from '../../../actions/reward_actions'; 5 | import {makeContribution} from '../../../actions/contribution_actions'; 6 | 7 | class RewardsShowPage extends React.Component { 8 | constructor(props) { 9 | super(props); 10 | } 11 | 12 | render() { 13 | let rewardTiles = null; 14 | 15 | let theseRewards = []; 16 | let campId = parseInt(this.props.campaignId); 17 | this.props.rewards.forEach((reward) => { 18 | if (reward.campaign_id === campId) { 19 | theseRewards.push(reward); 20 | } 21 | }); 22 | 23 | rewardTiles= theseRewards.map((rew, idx) => { 24 | 25 | return( 26 | 28 | ); 29 | }); 30 | 31 | 32 | if (this.props.rewards.length > 0 ) { 33 | 34 | return( 35 |
    36 | { rewardTiles } 37 |
    38 | ); 39 | } else { 40 | 41 | return( 42 |
    43 |
    44 | ); 45 | } 46 | } 47 | } 48 | 49 | const mapStateToProps = (state, ownProps) => { 50 | 51 | let campid = null; 52 | if (ownProps.params) { 53 | campid = ownProps.campaignId; 54 | } else { 55 | campid = state.campaigns.campaign.id; 56 | } 57 | let rews = []; 58 | if (Object.keys(state.rewards).length > 0) { 59 | rews = Object.keys(state.rewards).map((key) => state.rewards[key]); 60 | } 61 | 62 | let showbool = ownProps.onShow; 63 | 64 | return { 65 | rewards: rews, 66 | campaignId: parseInt(campid), 67 | user: state.session.currentUser, 68 | onShow: showbool 69 | }; 70 | }; 71 | 72 | const mapDispatchToProps = (dispatch) => { 73 | return{ 74 | removeReward: (reward) => dispatch(removeReward(reward)), 75 | makeContribution: (contribution) => dispatch(makeContribution(contribution)) 76 | }; 77 | }; 78 | 79 | 80 | export default connect(mapStateToProps, mapDispatchToProps)(RewardsShowPage); 81 | -------------------------------------------------------------------------------- /frontend/components/root.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Provider } from 'react-redux'; 3 | import { Router, Route, IndexRoute, hashHistory } from 'react-router'; 4 | import App from './app'; 5 | import CampaignIndexContainer from './campaign/campaign_index_container'; 6 | import CampaignShowContainer from './campaign/campaign_show_container'; 7 | import UserShowContainer from './user/user_container'; 8 | import HomePage from './home_page/home_page'; 9 | import NewCampaignContainer from './campaign/new_campaign_container'; 10 | import EditCampaignComponent from './campaign/edit_campaign_container'; 11 | import NewRewardContainer from './campaign/rewards/new_reward_component'; 12 | import CategoryShowContainer from './categories/category_show_container'; 13 | 14 | const Root = ({store}) => { 15 | 16 | const _redirectIfLoggedIn = (nextState, replace) => { 17 | const currentUser = store.getState().session.currentUser; 18 | if (currentUser) { 19 | replace('/'); 20 | } 21 | }; 22 | 23 | const _redirectUnlessLoggedIn = (nextState, replace) => { 24 | const currentUser = store.getState().session.currentUser; 25 | 26 | const url = location.hash.split('/'); 27 | const userId = url[2]; 28 | if (currentUser && currentUser.id !== parseInt(userId)) { 29 | replace('/'); 30 | } 31 | }; 32 | 33 | const redirect = (nextState, replace) => { 34 | replace('/'); 35 | }; 36 | 37 | 38 | return( 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | ); 54 | }; 55 | 56 | export default Root; 57 | -------------------------------------------------------------------------------- /config/routes.rb: -------------------------------------------------------------------------------- 1 | Rails.application.routes.draw do 2 | # The priority is based upon order of creation: first created -> highest priority. 3 | # See how all your routes lay out with "rake routes". 4 | root "static_pages#root" 5 | 6 | namespace :api, defaults: {format: :json} do 7 | 8 | resources :users, only: [:update, :show, :create] 9 | resource :session, only: [:create, :destroy] 10 | resources :contributions, only: [:create, :show] 11 | resources :search, only: [:index] 12 | # resources :charges 13 | 14 | resources :campaigns do 15 | resources :rewards, only: [:create, :destroy, :update, :index, :show] 16 | end 17 | 18 | resources :categories do 19 | resources :campaigns, only: [:index] 20 | end 21 | end 22 | # You can have the root of your site routed with "root" 23 | # root 'welcome#index' 24 | 25 | # Example of regular route: 26 | # get 'products/:id' => 'catalog#view' 27 | 28 | # Example of named route that can be invoked with purchase_url(id: product.id) 29 | # get 'products/:id/purchase' => 'catalog#purchase', as: :purchase 30 | 31 | # Example resource route (maps HTTP verbs to controller actions automatically): 32 | # resources :products 33 | 34 | # Example resource route with options: 35 | # resources :products do 36 | # member do 37 | # get 'short' 38 | # post 'toggle' 39 | # end 40 | # 41 | # collection do 42 | # get 'sold' 43 | # end 44 | # end 45 | 46 | # Example resource route with sub-resources: 47 | # resources :products do 48 | # resources :comments, :sales 49 | # resource :seller 50 | # end 51 | 52 | # Example resource route with more complex sub-resources: 53 | # resources :products do 54 | # resources :comments 55 | # resources :sales do 56 | # get 'recent', on: :collection 57 | # end 58 | # end 59 | 60 | # Example resource route with concerns: 61 | # concern :toggleable do 62 | # post 'toggle' 63 | # end 64 | # resources :posts, concerns: :toggleable 65 | # resources :photos, concerns: :toggleable 66 | 67 | # Example resource route within a namespace: 68 | # namespace :admin do 69 | # # Directs /admin/products/* to Admin::ProductsController 70 | # # (app/controllers/admin/products_controller.rb) 71 | # resources :products 72 | # end 73 | end 74 | -------------------------------------------------------------------------------- /docs/component-hierarchy.md: -------------------------------------------------------------------------------- 1 | # Component Hierarchy 2 | 3 | ### Landing Page 4 | 5 | **NavBarComponent** 6 | - Logo 7 | - Link to Github 8 | - Link to eventual Portfolio 9 | - SearchForm Container/Component 10 | - Auth Form Container/Component 11 | - Sign Up 12 | - Sign In 13 | - Demo Account 14 | 15 | **HomePageComponent** 16 | - Carousel Component 17 | - Image Component(Link to Campaign) 18 | -Campaign Index Slider Component 19 | - Slider Menu 20 | - Most Popular label 21 | - button to Campaign Index Page 22 | - CampaignIndexComponent 23 | - CampaignTile 24 | - Category Index Component 25 | -Category Components(Link to Campaign Category Index page) 26 | 27 | 28 | ### UserProfileComponent 29 | - Nav Bar 30 | - Header, showing username 31 | - Profile Component 32 | - User Image Upload 33 | - About me User Edit 34 | - About me show Component 35 | - Contributions Component 36 | - table with campaign, contribution, and perk 37 | 38 | ### Create A Campaign Component 39 | - Tagline 40 | - Campaign Form Component 41 | 42 | ### Edit A Campaign Page 43 | - Main Detail Label 44 | - FormtoEditComponent 45 | 46 | ### Add A Reward 47 | - Input Form Component to create rewards associated with campaigns 48 | 49 | ### User Contribution Index page 50 | - Table Component listing Contributions 51 | - Only accessible by user 52 | 53 | ### User Campaign Index Page 54 | - All Campaign Tiles that belong to user 55 | - Campaign Tiles link to Campaign Show page 56 | 57 | ### Campaign Index Page 58 | - All Campaign Tiles 59 | 60 | ### Category Show Page 61 | - All Campaign Tiles matching Category 62 | 63 | 64 | 65 | ## Routes 66 | 67 | |Path | Component | 68 | |-------|-------------| 69 | | "/sign-up" | "Auth Form Container" | 70 | | "/sign-in" | "Auth Form Container" | 71 | | "/" | "Landing Page" | 72 | | "/profile/:id" | "UserProfile Container" | 73 | | "/profile/:id/contributions" | "User Contributions" | 74 | | "/profile/:id/campaigns" | "User Campaigns" 75 | | "/campaign/new" | "Create A Campaign Container" | 76 | | "/campaign/:id/edit" | "Edit A Campaign Container" | 77 | | "/campaign/:id/addrewards" | "Add A Reward Component" 78 | | "/search-results" | "SearchResultsContainer" | 79 | | "/campaigns" | "Campaign Index Page" | 80 | | "/campaign/:id" | "Campaign Show Page" | 81 | | "/categories/:id" | "Category Show Page" 82 | -------------------------------------------------------------------------------- /app/assets/stylesheets/campaign_edit.css: -------------------------------------------------------------------------------- 1 | .edit-campaign { 2 | height: 100%; 3 | width: 80%; 4 | } 5 | 6 | .edit-campaign > h1 { 7 | padding-top: 40px; 8 | padding-left: 50px; 9 | font-weight: 400; 10 | font-size: 32px; 11 | font-weight: bold; 12 | } 13 | 14 | .c-form { 15 | font-size: 18px; 16 | } 17 | 18 | /*note that cform inherits from campaign new cform css*/ 19 | 20 | .campaign-description { 21 | padding-left: 25px; 22 | margin-left: 25px; 23 | margin-top: 10px; 24 | padding-top: 20px; 25 | width: 600px; 26 | } 27 | 28 | .campaign-description > input { 29 | width: 375px; 30 | height: 45px; 31 | margin-top: 5px; 32 | } 33 | 34 | .tagline { 35 | padding-left: 25px; 36 | margin-left: 25px; 37 | margin-top: 10px; 38 | padding-top: 20px; 39 | width: 600px; 40 | } 41 | 42 | .tagline > input { 43 | width: 375px; 44 | height: 45px; 45 | margin-top: 5px; 46 | } 47 | 48 | .category_dropdown { 49 | margin-top: 10px; 50 | padding-left: 25px; 51 | margin-left: 25px; 52 | width: 600px; 53 | padding-top: 20px; 54 | } 55 | 56 | .campaign-img-preview { 57 | display: flex; 58 | flex-direction: column; 59 | justify-content: space-around; 60 | margin-top: 10px; 61 | padding-left: 25px; 62 | margin-left: 25px; 63 | width: 600px; 64 | padding-top: 20px; 65 | padding-bottom: 20px; 66 | border: none; 67 | } 68 | 69 | .row-div { 70 | position: relative; 71 | display: flex; 72 | flex-direction: row-reverse; 73 | justify-content: flex-end; 74 | align-items: center; 75 | } 76 | 77 | .campaign-img > img { 78 | position: absolute; 79 | top:0; 80 | left: 0; 81 | margin-left: 508px; 82 | width: 80px; 83 | height: 80px; 84 | margin-top: -27px; 85 | background-size: cover; 86 | background-color: #c3c3c3; 87 | border-radius: 100%; 88 | border: 1px solid #e4e4e4; 89 | } 90 | 91 | .category { 92 | padding-top: 5px; 93 | } 94 | 95 | .submit-button > input { 96 | margin-left: 50px; 97 | margin-top: 10px; 98 | background-color: #FC3468; 99 | color: white; 100 | align-items: center; 101 | min-height: 30px; 102 | padding:5px; 103 | border: none; 104 | } 105 | 106 | .submit-button > input:hover { 107 | cursor: pointer; 108 | background: #ff1854; 109 | } 110 | 111 | .file-upload > input { 112 | text-align: center; 113 | font-size: 12px; 114 | } 115 | 116 | .campaign-errors { 117 | color: red; 118 | } 119 | -------------------------------------------------------------------------------- /frontend/components/session/session_form.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { withRouter } from 'react-router'; 3 | 4 | class SessionForm extends React.Component { 5 | constructor(props) { 6 | super(props); 7 | this.state = { 8 | password: "Password", 9 | first_name: "First Name", 10 | last_name: "Last Name", 11 | email: "Email", 12 | errors: this.props.errors 13 | }; 14 | this.handleSubmit = this.handleSubmit.bind(this); 15 | } 16 | 17 | handleSubmit(e) { 18 | e.preventDefault(); 19 | const user = this.state; 20 | 21 | this.props.processForm(user).then(() => this.props.modalClose()); 22 | } 23 | 24 | update(field) { 25 | return e => this.setState({ 26 | [field]: e.currentTarget.value 27 | }); 28 | } 29 | 30 | 31 | 32 | handleFormType(type) { 33 | if (this.props.formType === "signup") { 34 | return( 35 |
    36 | 40 | 41 | 42 |
    43 | 44 | 48 | 49 |
    50 | ); 51 | } 52 | } 53 | 54 | renderErrors() { 55 | if (this.props.errors) { 56 | 57 | return( 58 |
    59 | {this.props.errors} 60 |
    61 | ); 62 | } 63 | } 64 | 65 | render() { 66 | return( 67 |
    68 |
    69 | 70 | {this.handleFormType(this.props.formType)} 71 | 72 | 76 | 77 | 78 |
    79 | 80 | 84 | 85 |
    86 | 88 | 89 | {this.renderErrors()} 90 |
    91 |
    92 | ); 93 | } 94 | } 95 | 96 | export default withRouter(SessionForm); 97 | -------------------------------------------------------------------------------- /frontend/components/user/profile.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | 4 | class UserProfile extends React.Component { 5 | constructor(props) { 6 | super(props); 7 | 8 | this.state = { 9 | user: null, 10 | imageFile: null, 11 | imageUrl: null, 12 | }; 13 | 14 | this.updateFile = this.updateFile.bind(this); 15 | this.handleSubmit = this.handleSubmit.bind(this); 16 | } 17 | 18 | componentWillReceiveProps(newProps) { 19 | if (this.props.user.id != newProps.user.id) { 20 | this.setState({user: newProps.user.id, imageUrl: newProps.user.image_ur}); 21 | } 22 | } 23 | 24 | updateFile(e) { 25 | let file = e.currentTarget.files[0]; 26 | var fileReader = new FileReader(); 27 | fileReader.onloadend = () => { 28 | this.setState({imageFile: file, imageUrl: fileReader.result}); 29 | }; 30 | if (file) { 31 | fileReader.readAsDataURL(file); 32 | } 33 | } 34 | 35 | handleSubmit(e) { 36 | var formData = new FormData(); 37 | formData.append("user[image]", this.state.imageFile); 38 | formData.append("user[id]", this.props.user.id); 39 | this.props.editUser(formData).then(usr => { 40 | this.setState({user: usr}); 41 | }); 42 | } 43 | 44 | render() { 45 | return( 46 |
    47 |
    48 |
    49 |

    User Image

    50 |
    51 | 52 |
    53 | 54 |
    55 | 56 |
    57 |
    58 | 60 | 61 |
    62 | 63 |
    64 |
    65 |
    66 |
    67 |
    68 |
    69 |
    70 |
    71 | 72 |
    73 |
    74 |

    About Me

    75 |
    76 | {this.props.user.about_me} 77 |
    78 |
    79 |
    80 |
    81 | ); 82 | 83 | } 84 | } 85 | 86 | export default UserProfile; 87 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [IndieFomo](http://www.indiefomo.com/#/) 2 | 3 | # IndieFomo 4 | Indie Fomo is a web aplication inspired by Indiegogo built using Ruby on Rails on the backend, a PostgreSQL database, and React with Redux on the front end. 5 | ![HomePage](/docs/images/indiefomo.png) 6 | 7 | ## Features And Implementation 8 | 9 | ### Campaign Creation 10 | On the backend, campaigns are stored in a single table with foreign keys connecting each campaign to it's owner and category. Each campaign has many associated rewards which are stored in a separate table. Each respective campaign has two associated views. First of which is the campaign show page that lists campaign funding details, a description, and its associated rewards. The second view is an overview tile that links the user to the campaign show page. 11 | ![Campaign](/docs/images/campaign-creation.png) 12 | ### Reward Creation 13 | On the backend, rewards are stored in a single table with a foreign key associating each reward to its campaign. Each reward is structured as a nested resource under its campaign so users can add rewards as they create their campaigns. Rewards are rendered twice in a 'preview' mode where users can delete and edit rewards before launching their finished campaigns as well as in a final show view that is rendered on the campaign show page. 14 | ![Rewards](/docs/images/rewards.png) 15 | ### Contributions 16 | On the backend, contributions are stored in a single table with foreign keys connecting them to a user, optional reward, and campaign associated with each contribution. Users can make contributions on the campaign show page by either opting to purchase a reward or by creating a custom amount contribution. This contribution history can then be accessed via the users profile page. Total contributions as well as the contribution count are rendered on the campaign show page. The contributions are represented as a progress bar that expresses the total contributions as a percentage of target campaign amount. The progress bar dynamically updates as users donate to the campaign. 17 | ![ProgressBar](/docs/images/progress-bar.png) 18 | ### Users and Profiles 19 | The Users table stores profile and user data including passwords and session tokens. Password authentication achieved with the BCrypt Gem. 20 | ### Search and Categories 21 | Users can explore available campaigns by either looking through the category tiles that link to a category show page that filters campaigns by category, or they can user the search tool to find campaigns by title or tagline. The search function uses the pg_search gem uses search scopes against the Campaign table, lending higher weight to results containing keywords in the title. 22 | ![Categories](/docs/images/categories.png) 23 | -------------------------------------------------------------------------------- /app/assets/stylesheets/reward_tile.css: -------------------------------------------------------------------------------- 1 | .reward-show { 2 | border: 1px solid #dddddd; 3 | height: 200px; 4 | width: 220px; 5 | display: flex; 6 | flex-direction: column; 7 | justify-content: space-between; 8 | align-items: flex-start; 9 | margin-bottom: 30px; 10 | } 11 | 12 | .reward-show:hover { 13 | cursor: pointer; 14 | } 15 | 16 | .reward { 17 | margin-bottom: 30px; 18 | border: 1px solid #dddddd; 19 | height: 200px; 20 | width: 220px; 21 | display: flex; 22 | flex-direction: column; 23 | justify-content: space-between; 24 | align-items: flex-start; 25 | } 26 | 27 | .reward > ul{ 28 | display: flex; 29 | padding: 5px; 30 | box-sizing: border-box; 31 | justify-content: space-between; 32 | flex-direction: column; 33 | width: 100%; 34 | height: 50%; 35 | } 36 | 37 | .reward-show > ul { 38 | padding: 5px; 39 | justify-content: space-between; 40 | display: flex; 41 | flex-direction: column; 42 | width: 100%; 43 | height: 50%; 44 | } 45 | 46 | .time-left{ 47 | width: 100%; 48 | display: flex; 49 | flex-direction: row; 50 | justify-content: center; 51 | justify-content: center; 52 | } 53 | .get-it { 54 | display: flex; 55 | flex-direction: column; 56 | justify-content: center; 57 | background-color: #FC3468; 58 | color: white; 59 | align-items: center; 60 | height: 30px; 61 | width: 100%; 62 | } 63 | 64 | .contribute-thanks { 65 | display: flex; 66 | flex-direction: row; 67 | font-size: 14px; 68 | justify-content: center; 69 | background-color: #FC3468; 70 | color: white; 71 | align-items: center; 72 | height: 30px; 73 | width: 100%; 74 | } 75 | 76 | .get-it:hover { 77 | cursor: pointer; 78 | } 79 | 80 | 81 | .eta { 82 | background-color: #f5f5f5; 83 | width: 100%; 84 | height: 30px; 85 | display: flex; 86 | flex-direction: column; 87 | justify-content: center; 88 | align-items: center; 89 | } 90 | 91 | .delete { 92 | background-color: #f5f5f5; 93 | width: 100%; 94 | height: 30px; 95 | display: flex; 96 | flex-direction: column; 97 | justify-content: center; 98 | align-items: center; 99 | } 100 | 101 | .delete:hover { 102 | background-color: #FC3468; 103 | color: white; 104 | cursor: pointer; 105 | width: 100%; 106 | height: 30px; 107 | display: flex; 108 | flex-direction: column; 109 | justify-content: center; 110 | align-items: center; 111 | } 112 | 113 | 114 | .reward-prices { 115 | font-weight: bold; 116 | font-size: 18px; 117 | } 118 | 119 | .reward-titles { 120 | color: #878787; 121 | font-size: 18px; 122 | } 123 | 124 | .reward-descriptions { 125 | font-size: 14px; 126 | width: 212px; 127 | } 128 | -------------------------------------------------------------------------------- /frontend/components/home_page/tile_slider.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Slider from 'react-slick'; 3 | import CampaignIndexItem from '../campaign/campaign_index_item'; 4 | import {selectCampaigns} from '../../reducers/selectors'; 5 | 6 | class TileSlider extends React.Component { 7 | 8 | constructor(props) { 9 | super(props); 10 | this.state = { 11 | index: 0, 12 | tiles: [], 13 | move: false, 14 | direction: 'right' 15 | }; 16 | 17 | this.movement = this.movement.bind(this); 18 | } 19 | 20 | componentDidMount() { 21 | this.props.fetchCampaigns(6).then((camps) => { 22 | let campaigns = Object.keys(camps.campaigns).map(key => camps.campaigns[key]) 23 | this.setState({tiles: campaigns}); 24 | }); 25 | } 26 | 27 | slideLeft(e){ 28 | this.movement(e); 29 | } 30 | 31 | slideRight(e){ 32 | this.movement(e); 33 | } 34 | 35 | movement(e){ 36 | const campaignTiles = this.state.tiles; 37 | 38 | if (e.currentTarget.className === 'slider-left-button') { 39 | campaignTiles.push(campaignTiles.shift()); 40 | } else if (e.currentTarget.className === 'slider-right-button') { 41 | campaignTiles.unshift(campaignTiles.pop()); 42 | } 43 | 44 | this.setState({move:false, tiles: campaignTiles}); 45 | } 46 | 47 | 48 | render() { 49 | let camps = this.state.tiles; 50 | let slidercname = ""; 51 | 52 | if(this.state.move){ 53 | slidercname = (this.state.dir === 'left' ? " slider-move-left" : " slider-move-right"); 54 | } 55 | 56 | if (camps[0]) { 57 | return( 58 |
    59 |
    60 |
    61 |
    62 | 63 |
    64 | 65 |
    66 | 67 |
    68 | 69 |
    70 | 71 |
    72 | 73 |
    74 | 75 |
    76 |
    77 |
    78 |
    79 | ); 80 | } else { 81 | return( 82 |
    Waiting..
    83 | ); 84 | } 85 | } 86 | } 87 | 88 | export default TileSlider; 89 | -------------------------------------------------------------------------------- /frontend/components/user/user_show.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Link, withRouter } from 'react-router'; 3 | import UserProfile from './profile'; 4 | import UserCampaignList from './campaigns'; 5 | import UserContributions from './contributions'; 6 | 7 | class UserShow extends React.Component { 8 | constructor(props) { 9 | super(props); 10 | this.state = { 11 | user: null, 12 | show: "profile" 13 | }; 14 | 15 | this.handleClick = this.handleClick.bind(this); 16 | this.properContent = this.properContent.bind(this); 17 | } 18 | 19 | componentDidMount() { 20 | this.props.getUser(this.props.params.id); 21 | } 22 | 23 | handleClick(e) { 24 | 25 | switch (e.target.className) { 26 | case "user-nav-location loc-profile": 27 | return this.setState({show: "profile"}); 28 | case "user-nav-location loc-campaigns": 29 | return this.setState({show: "campaigns"}); 30 | case "user-nav-location loc-contributions": 31 | return this.setState({show: "contributions"}); 32 | default: 33 | break; 34 | } 35 | } 36 | 37 | 38 | properContent() { 39 | switch (this.state.show) { 40 | case "profile": 41 | return( 42 | 44 | ); 45 | case "campaigns": 46 | return( 47 | 49 | ); 50 | case "contributions": 51 | return( 52 | 53 | ); 54 | default: 55 | return( 56 | 59 | ); 60 | } 61 | } 62 | 63 | render() { 64 | const name = this.props.user ? this.props.user.first_name : ""; 65 | const type = this.state.show; 66 | const content = this.properContent(); 67 | 68 | if (this.props.user.id) { 69 | return( 70 |
    71 |
    72 | 73 |
    74 |
    75 | Profile 76 |
    77 |
    78 | Campaigns: {this.props.campaigns.count} 79 |
    80 |
    81 | Contributions: {this.props.contributions.count} 82 |
    83 |
    84 | 85 |
    86 |

    Hi, {name}

    87 |
    88 | 89 |
    90 | {content} 91 |
    92 |
    93 | ); 94 | } else { 95 | return ( 96 |

    97 | ); 98 | } 99 | } 100 | } 101 | 102 | 103 | 104 | export default withRouter(UserShow); 105 | -------------------------------------------------------------------------------- /app/assets/stylesheets/newRewards.css: -------------------------------------------------------------------------------- 1 | .create-reward { 2 | display: flex; 3 | flex-direction: column; 4 | width: 80%; 5 | height: 100%; 6 | justify-content: flex-start; 7 | align-items: flex-start; 8 | } 9 | 10 | .reward-- { 11 | width: 50%; 12 | padding-left: 50px; 13 | 14 | } 15 | 16 | .reward-- > h1 { 17 | margin-top: 30px; 18 | font-weight: bold; 19 | font-size: 32px; 20 | height: 100%; 21 | } 22 | 23 | .reward-- > h2 { 24 | font-size: 24px; 25 | } 26 | 27 | .create-reward > main { 28 | display: flex; 29 | flex-direction: row; 30 | justify-content: space-between; 31 | } 32 | 33 | .reward-form { 34 | display: flex; 35 | flex-direction: column; 36 | width: 50%; 37 | font-size: 18px; 38 | align-items: space-between; 39 | } 40 | 41 | .rewards-show { 42 | padding-left: 150px; 43 | height: 100%; 44 | display: block; 45 | } 46 | 47 | .create-reward > h1 { 48 | padding-top: 10px; 49 | padding-left: 50px; 50 | font-weight: 400; 51 | font-size: 24px; 52 | } 53 | 54 | .create-reward > h2 { 55 | padding-top: 10px; 56 | padding-left: 50px; 57 | font-weight: 400; 58 | font-size: 14px; 59 | } 60 | 61 | .reward-title { 62 | padding-left: 25px; 63 | margin-left: 25px; 64 | padding-top: 20px; 65 | padding-bottom: 20px; 66 | width: 90%; 67 | /*border: 1px solid #e4e4e4;*/ 68 | 69 | } 70 | 71 | .reward-title > input { 72 | width: 375px; 73 | height: 45px; 74 | 75 | } 76 | 77 | .rewards-img { 78 | padding-top: 10px; 79 | padding-left: 50px; 80 | font-weight: 400; 81 | font-size: 14px; 82 | 83 | } 84 | 85 | .reward-description { 86 | padding-left: 25px; 87 | margin-left: 25px; 88 | margin-top: 10px; 89 | padding-top: 20px; 90 | padding-bottom: 20px; 91 | width: 90%; 92 | } 93 | 94 | .reward-description > input { 95 | width: 375px; 96 | height: 45px; 97 | 98 | } 99 | 100 | .reward-price { 101 | padding-left: 25px; 102 | margin-left: 25px; 103 | margin-top: 10px; 104 | padding-top: 20px; 105 | padding-bottom: 20px; 106 | width: 90%; 107 | /*border: 1px solid #e4e4e4;*/ 108 | 109 | } 110 | 111 | .reward-price > input { 112 | width: 150px; 113 | height: 35px; 114 | 115 | } 116 | 117 | .submit-buttons > input{ 118 | margin-left: 50px; 119 | margin-top: 10px; 120 | background-color: #FC3468; 121 | color: white; 122 | width: 130px; 123 | text-align: center; 124 | align-items: center; 125 | min-height: 30px; 126 | padding:5px; 127 | border: none; 128 | } 129 | 130 | .submit-buttons > input:hover { 131 | cursor: pointer; 132 | background: #ff1854; 133 | } 134 | 135 | .r-form { 136 | width: 100%; 137 | display: flex; 138 | flex-direction: column; 139 | } 140 | 141 | .file-upload { 142 | max-width:500px; 143 | } 144 | 145 | .file-upload > input { 146 | text-align: center; 147 | font-size: 12px; 148 | } 149 | 150 | .rewards-show { 151 | display: flex; 152 | width: 500px; 153 | height: 500px; 154 | /*border: 1px solid red;*/ 155 | } 156 | 157 | .rewards-item { 158 | width: 230px; 159 | height: 300px; 160 | display: flex; 161 | flex-direction: column; 162 | justify-content: space-between; 163 | padding-bottom: 25px; 164 | } 165 | 166 | .rewards-details { 167 | width: 100; 168 | height: 40%; 169 | display: flex; 170 | flex-direction: column; 171 | justify-content: space-between; 172 | padding-left: 10px; 173 | 174 | /*border: 1px solid #e4e4e4;*/ 175 | } 176 | -------------------------------------------------------------------------------- /frontend/components/campaign/rewards/reward.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | class Reward extends React.Component { 4 | 5 | constructor(props) { 6 | super(props); 7 | this.state = { 8 | hover: false, 9 | madeContribution: false 10 | }; 11 | 12 | this.contribute = this.contribute.bind(this); 13 | this.handleButtonType = this.handleButtonType.bind(this); 14 | this.deleteReward = this.deleteReward.bind(this); 15 | this.onhoverIn = this.onhoverIn.bind(this); 16 | this.onhoverOut = this.onhoverOut.bind(this); 17 | } 18 | 19 | onhoverIn() { 20 | this.setState({hover: true}); 21 | } 22 | 23 | onhoverOut() { 24 | this.setState({hover: false}); 25 | } 26 | 27 | deleteReward() { 28 | this.props.deleteReward({campaign_id: this.props.campaignId, id: this.props.reward.id}) 29 | } 30 | 31 | handleButtonType() { 32 | if (this.state.madeContribution) { 33 | return( 34 |
    THANK YOU FOR CONTRIBUTING!
    35 | ); 36 | } 37 | else if (this.state.hover && this.props.user.id) { 38 | return( 39 |
    GET THIS PERK
    40 | ); 41 | } else { 42 | return( 43 |
    ESTIMATED June 2018
    44 | ); 45 | } 46 | } 47 | 48 | getThisDate() { 49 | let monthHash = {1: 'Jan', 2: 'Feb', 3: 'Mar', 4: 'Apr', 5: 'May', 50 | 6: 'Jun', 7: 'Jul', 8: 'Aug', 9: 'Sept', 10: 'Oct', 11: 'Nov', 12: 'Dec'}; 51 | let today = new Date(); 52 | let day = today.getDate(); 53 | let mm = today.getMonth() + 1; 54 | let yy = today.getFullYear(); 55 | 56 | if (day < 10) { 57 | day = '0' + day; 58 | } 59 | mm = parseInt(mm); 60 | mm = monthHash[mm]; 61 | 62 | today = mm + ' ' + day + ' ' + yy; 63 | return today; 64 | } 65 | 66 | contribute() { 67 | let date = this.getThisDate(); 68 | 69 | this.props.makeContribution({user_id: this.props.user.id, reward_id: this.props.reward.id, 70 | amount: this.props.reward.price, campaign_id: this.props.campaignId, date: date }).then((contribution) => { 71 | this.setState({madeContribution: true}); 72 | }); 73 | } 74 | 75 | 76 | render() { 77 | 78 | if (this.props.onShow) { 79 | return( 80 |
    81 |
      82 |
    • ${this.props.reward.price}
    • 83 |
    • {this.props.reward.title}
    • 84 |
    • {this.props.reward.description}
    • 85 |
    86 |
    87 | {this.handleButtonType()} 88 |
    89 |
    90 | ); 91 | } else { 92 | return( 93 |
    94 |
      95 |
    • ${this.props.reward.price}
    • 96 |
    • {this.props.reward.title}
    • 97 |
    • {this.props.reward.description}
    • 98 |
    99 | 100 |
    101 | ); 102 | } 103 | 104 | } 105 | } 106 | 107 | export default Reward; 108 | -------------------------------------------------------------------------------- /app/assets/stylesheets/campaign_index.scss: -------------------------------------------------------------------------------- 1 | // Place all the styles related to the Campaigns controller here. 2 | // They will automatically be included in application.css. 3 | // You can use Sass (SCSS) here: http://sass-lang.com/ 4 | 5 | .all-campaigns { 6 | display: flex; 7 | flex-direction: column; 8 | justify-content: center; 9 | width: 80%; 10 | display: block; 11 | align-items: center; 12 | margin-top: 25px; 13 | margin-bottom: 50px; 14 | // border: 1px solid black; 15 | } 16 | 17 | .campaigns-x { 18 | box-sizing: border-box; 19 | display: flex; 20 | flex-direction: row; 21 | justify-content: flex-start; 22 | align-items: center; 23 | margin-top: 1%; 24 | height: 90%; 25 | width: 100%; 26 | margin: 0; 27 | flex-wrap: wrap; 28 | margin-left: 100px; 29 | // border: 1px solid yellow; 30 | 31 | } 32 | 33 | .title- { 34 | font-size: 24px; 35 | width: 65%; 36 | margin-top: 40px; 37 | margin-bottom: 40px; 38 | margin-left: 100px; 39 | } 40 | 41 | .campaign-tile { 42 | position: relative; 43 | display: flex; 44 | justify-content: space-between; 45 | flex-direction: row; 46 | flex-wrap: wrap; 47 | border: 1px solid rgba(0, 0, 0, 0.0980392); 48 | margin: 4px; 49 | width: 275px; 50 | height: 450px; 51 | margin-bottom: 10px; 52 | } 53 | 54 | .campaign-tile:hover { 55 | cursor: pointer; 56 | border: 1px solid lightgrey; 57 | } 58 | 59 | img { 60 | display: block; 61 | align-self: flex-start; 62 | justify-content: center; 63 | height: 222px; 64 | width: 100%; 65 | overflow: hidden; 66 | 67 | } 68 | 69 | .description-index-tiles { 70 | // padding: 2%; 71 | align-items: flex-start; 72 | justify-content: space-between; 73 | display: flex; 74 | flex-direction: column; 75 | flex: auto; 76 | width: 100%; 77 | } 78 | 79 | .description-container { 80 | display: flex; 81 | flex-direction: column; 82 | padding: 2px; 83 | justify-content: flex-start; 84 | align-items: baseline; 85 | height: 40%; 86 | } 87 | 88 | 89 | .text { 90 | padding: 2%; 91 | display: block; 92 | width: 100%; 93 | } 94 | 95 | .tagline-text { 96 | color: #878787; 97 | padding-left: 2%; 98 | padding-right: 2%; 99 | width: 95%; 100 | font: 10px 101 | } 102 | 103 | #item { 104 | // display: flex; 105 | // width: 21%; 106 | // height: auto; 107 | // padding: 10px; 108 | } 109 | 110 | 111 | .campaign-tile { 112 | display: flex; 113 | // width: 225px;; 114 | // height: auto; 115 | // overflow-y: visible; 116 | } 117 | 118 | .description-container { 119 | // border: 1px solid black; 120 | height: 200px; 121 | width: 100%; 122 | display: flex; 123 | flex-direction: column; 124 | justify-content: center; 125 | } 126 | 127 | 128 | .progress-tile-bar { 129 | // border: 1px solid black; 130 | height: 100%; 131 | width: 95%; 132 | display: flex; 133 | flex-direction: column; 134 | justify-content: flex-end; 135 | padding: 5px; 136 | } 137 | 138 | .dollars-tile { 139 | font-size: 12px; 140 | } 141 | 142 | .funds-tile{ 143 | font-size: 12px; 144 | } 145 | 146 | .bar-tile { 147 | display: inline-block; 148 | background-color: #f5f5f5; 149 | min-height: 5px; 150 | width: 100%; 151 | margin-top: 5px; 152 | margin-bottom: 5px; 153 | } 154 | 155 | .percentage-tile { 156 | max-width: 100%; 157 | background-color: #ff1854; 158 | min-height: 5px; 159 | } 160 | 161 | .funds-tile { 162 | margin-top: 2px; 163 | font-size: 12px; 164 | } 165 | 166 | .moment-tile { 167 | font-size: 12px; 168 | } 169 | -------------------------------------------------------------------------------- /config/environments/production.rb: -------------------------------------------------------------------------------- 1 | Rails.application.configure do 2 | # Settings specified here will take precedence over those in config/application.rb. 3 | 4 | # Code is not reloaded between requests. 5 | config.cache_classes = true 6 | 7 | # Eager load code on boot. This eager loads most of Rails and 8 | # your application in memory, allowing both threaded web servers 9 | # and those relying on copy on write to perform better. 10 | # Rake tasks automatically ignore this option for performance. 11 | config.eager_load = true 12 | 13 | # Full error reports are disabled and caching is turned on. 14 | config.consider_all_requests_local = false 15 | config.action_controller.perform_caching = true 16 | 17 | # Enable Rack::Cache to put a simple HTTP cache in front of your application 18 | # Add `rack-cache` to your Gemfile before enabling this. 19 | # For large-scale production use, consider using a caching reverse proxy like 20 | # NGINX, varnish or squid. 21 | # config.action_dispatch.rack_cache = true 22 | 23 | # Disable serving static files from the `/public` folder by default since 24 | # Apache or NGINX already handles this. 25 | config.serve_static_files = ENV['RAILS_SERVE_STATIC_FILES'].present? 26 | 27 | # Compress JavaScripts and CSS. 28 | config.assets.js_compressor = :uglifier 29 | # config.assets.css_compressor = :sass 30 | 31 | # Do not fallback to assets pipeline if a precompiled asset is missed. 32 | config.assets.compile = false 33 | 34 | # Asset digests allow you to set far-future HTTP expiration dates on all assets, 35 | # yet still be able to expire them through the digest params. 36 | config.assets.digest = true 37 | 38 | # `config.assets.precompile` and `config.assets.version` have moved to config/initializers/assets.rb 39 | 40 | # Specifies the header that your server uses for sending files. 41 | # config.action_dispatch.x_sendfile_header = 'X-Sendfile' # for Apache 42 | # config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' # for NGINX 43 | 44 | # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies. 45 | config.force_ssl = true 46 | 47 | # Use the lowest log level to ensure availability of diagnostic information 48 | # when problems arise. 49 | config.log_level = :debug 50 | 51 | # Prepend all log lines with the following tags. 52 | # config.log_tags = [ :subdomain, :uuid ] 53 | 54 | # Use a different logger for distributed setups. 55 | # config.logger = ActiveSupport::TaggedLogging.new(SyslogLogger.new) 56 | 57 | # Use a different cache store in production. 58 | # config.cache_store = :mem_cache_store 59 | 60 | # Enable serving of images, stylesheets, and JavaScripts from an asset server. 61 | # config.action_controller.asset_host = 'http://assets.example.com' 62 | 63 | # Ignore bad email addresses and do not raise email delivery errors. 64 | # Set this to true and configure the email server for immediate delivery to raise delivery errors. 65 | # config.action_mailer.raise_delivery_errors = false 66 | 67 | # Enable locale fallbacks for I18n (makes lookups for any locale fall back to 68 | # the I18n.default_locale when a translation cannot be found). 69 | config.i18n.fallbacks = true 70 | 71 | # Send deprecation notices to registered listeners. 72 | config.active_support.deprecation = :notify 73 | 74 | # Use default logging formatter so that PID and timestamp are not suppressed. 75 | config.log_formatter = ::Logger::Formatter.new 76 | 77 | # Do not dump schema after migrations. 78 | config.active_record.dump_schema_after_migration = false 79 | end 80 | -------------------------------------------------------------------------------- /frontend/components/campaign/new_campaign.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Link, withRouter } from 'react-router'; 3 | 4 | 5 | class NewCampaign extends React.Component { 6 | constructor(props) { 7 | super(props); 8 | this.state = { 9 | user_id: null, 10 | title: null, 11 | target_amount: 0, 12 | descriptions: "", 13 | errors: "" 14 | }; 15 | 16 | this.handleSubmit = this.handleSubmit.bind(this); 17 | } 18 | 19 | handleSubmit(e) { 20 | e.preventDefault(); 21 | let newCamp = this.state; 22 | delete newCamp["errors"]; 23 | 24 | return this.props.createCampaign(newCamp) 25 | .then((camp) => this.success(camp), 26 | (err) => this.fail(err)); 27 | } 28 | 29 | success(camp) { 30 | 31 | const url = `campaign/${camp.campaign.id}/edit`; 32 | this.props.router.push(url); 33 | } 34 | 35 | fail(err) { 36 | 37 | this.setState({errors: err}); 38 | } 39 | 40 | componentWillMount() { 41 | if (!this.props.user) { 42 | this.props.router.push('/'); 43 | } else { 44 | this.setState({user_id: this.props.user.id}); 45 | } 46 | } 47 | 48 | update(field) { 49 | return e => this.setState({ 50 | [field]: e.currentTarget.value 51 | }); 52 | } 53 | 54 | getErrors() { 55 | 56 | if (this.props.errors.includes("Target") && this.props.errors.includes("Title")) { 57 | return ( 58 |
    59 |
    Please enter a valid Amount and Title!
    60 |
    61 | ); 62 | } else if (this.props.errors.includes("Title")) { 63 | return ( 64 |
    65 |
    Please enter a valid Title!
    66 |
    67 | ); 68 | } else if (this.props.errors.includes("Target")) { 69 | return ( 70 |
    71 |
    Please enter a valid Amount!
    72 |
    73 | ); 74 | } 75 | 76 | } 77 | 78 | render() { 79 | let errs = null; 80 | 81 | if (this.props.errors) { 82 | errs = this.getErrors(); 83 | } 84 | 85 | return( 86 |
    87 |

    Start a Campaign

    88 |

    Crowdfund your Fomo

    89 | 90 |
    91 |
    92 |
    93 | 94 |
    95 |
    96 |

    How much would you like to raise?

    97 | 100 | USD 101 |
    102 | 103 |
    104 |

    What would you like to call your campaign?

    105 | 108 | 109 |
    110 | 111 |
    112 | 114 | 115 |
    116 |
    117 | 118 | {errs} 119 |
    120 |
    121 | ); 122 | } 123 | } 124 | 125 | export default withRouter(NewCampaign); 126 | -------------------------------------------------------------------------------- /app/assets/stylesheets/discovery.css: -------------------------------------------------------------------------------- 1 | .discovery { 2 | display: flex; 3 | flex-direction: column; 4 | width: 1100px; 5 | justify-content: space-around; 6 | align-items: center; 7 | min-height: 600px; 8 | margin-top: 20px; 9 | box-sizing: border-box; 10 | /*border: 1px solid blue;*/ 11 | } 12 | 13 | 14 | .discover-slider { 15 | display: flex; 16 | flex-direction: row; 17 | align-items: center; 18 | justify-content: center; 19 | width: 100%; 20 | /*border: 1px solid black;*/ 21 | margin: 20px; 22 | } 23 | 24 | .tile-slider { 25 | display: flex; 26 | justify-content: space-around; 27 | align-items: center; 28 | display: flex; 29 | /*border: 1px solid red;*/ 30 | width: calc(100% - 50px); 31 | 32 | /*height: 450px;*/ 33 | } 34 | 35 | .slider-move-left { 36 | display: flex; 37 | justify-content: space-around; 38 | align-items: center; 39 | display: flex; 40 | /*border: 1px solid red;*/ 41 | width: calc(100% - 50px); 42 | overflow: hidden; 43 | } 44 | 45 | .slider-move-right { 46 | display: flex; 47 | justify-content: space-around; 48 | align-items: center; 49 | display: flex; 50 | /*border: 1px solid black;*/ 51 | width: calc(100% - 50px); 52 | overflow: hidden; 53 | } 54 | 55 | /*CSS Animation for Discovery Slider*/ 56 | .slider-move-right > div{ 57 | animation-name: slidermoveright; 58 | animation-duration: 0.5s; 59 | } 60 | @keyframes slidermoveright{ 61 | 0% {} 62 | } 63 | .slider-move-left > div { 64 | animation-name: slidermoveleft; 65 | animation-duration: 0.5s; 66 | } 67 | @keyframes slidermoveleft{ 68 | 0% {} 69 | } 70 | 71 | 72 | .slider-left-button { 73 | /*border: 1px solid green;*/ 74 | width: 50px; 75 | height: 50px; 76 | color: black; 77 | } 78 | 79 | .discover-tile > div { 80 | max-width: 230px; 81 | } 82 | 83 | .arrows { 84 | width: 100%; 85 | height:100%; 86 | color: grey; 87 | } 88 | 89 | .arrows:hover { 90 | cursor: pointer; 91 | } 92 | 93 | .arrows:active { 94 | width: 95%; 95 | height: 95%; 96 | } 97 | 98 | .slider-right-button { 99 | /*border: 1px solid green;*/ 100 | width: 50px; 101 | height: 50px; 102 | color: black; 103 | } 104 | 105 | .tile-slide-container { 106 | /*border: 1px solid yellow;*/ 107 | display: flex; 108 | margin: 20px; 109 | padding-top: 150px; 110 | } 111 | 112 | .discovery-2 { 113 | width: 100%; 114 | height: 550px; 115 | display: flex; 116 | flex-direction: column; 117 | justify-content: space-between; 118 | align-items: center; 119 | margin: 10px; 120 | } 121 | 122 | .explore { 123 | color: black; 124 | z-index: 90; 125 | } 126 | 127 | .explore:hover { 128 | cursor: pointer; 129 | color: #ff1854; 130 | } 131 | 132 | .explore-nav { 133 | display: flex; 134 | flex-direction: row; 135 | justify-content: space-between; 136 | align-items: center; 137 | text-align: start; 138 | color: black; 139 | height: 50px; 140 | width: 90%; 141 | font-size: 18px; 142 | z-index: 90; 143 | padding: 10px; 144 | margin: 10px; 145 | /*position: relative;*/ 146 | } 147 | 148 | .discovery > h1 { 149 | align-items: flex-start; 150 | width: 100%; 151 | /*border-bottom: 1px solid black;*/ 152 | } 153 | 154 | .slick-prev { 155 | padding-right: 7px; 156 | } 157 | 158 | .slick-next { 159 | padding-left: 7px; 160 | } 161 | 162 | .slick-arrow, .slick-prev, .slick-next { 163 | color: transparent; 164 | width: 24px; 165 | height: 24px; 166 | align-self: center; 167 | } 168 | 169 | .slick-prev:before { 170 | content: '<'; 171 | /*zoom: 8%;*/ 172 | } 173 | 174 | .slick-next:before { 175 | content: '>'; 176 | } 177 | -------------------------------------------------------------------------------- /app/assets/stylesheets/campaign_show.css: -------------------------------------------------------------------------------- 1 | .campaign-show { 2 | display: flex; 3 | flex-direction: column; 4 | align-items: center; 5 | justify-content: flex-start; 6 | width: 1200px; 7 | margin-top: 50px; 8 | margin-bottom: 50px; 9 | } 10 | 11 | .overview-tile { 12 | height: 400px; 13 | width: 100%; 14 | display: flex; 15 | flex-direction: column; 16 | justify-content: center; 17 | align-items: center; 18 | margin-top: 40px; 19 | } 20 | /*SHOW AND OVERVIEW COMPONENTS COMBINED*/ 21 | .photo { 22 | width: 620px; 23 | height: 400; 24 | } 25 | 26 | .show-img { 27 | width: 100%; 28 | height: 280px; 29 | } 30 | 31 | .campaign-basics { 32 | width: 600px; 33 | display: flex; 34 | flex-direction: column; 35 | justify-content: flex-start; 36 | box-sizing: border-box; 37 | margin-left: 40px; 38 | } 39 | 40 | .campaign-basics > h1{ 41 | font-size: 32px; 42 | padding: 5px; 43 | font-weight: bold; 44 | } 45 | .campaign-basics > h2 { 46 | padding-left: 5px; 47 | padding-right:5px; 48 | color: #878787; 49 | padding-bottom: 5px; 50 | font-size: 24px; 51 | } 52 | 53 | .user-profile { 54 | display: flex; 55 | flex-direction: row; 56 | justify-content: flex-start; 57 | align-items: flex-start; 58 | padding: 5px; 59 | padding-bottom: 10px; 60 | } 61 | 62 | .campaign-overview { 63 | display: flex; 64 | flex-direction: row; 65 | justify-content: space-around; 66 | margin-top: 30px; 67 | width: 80%; 68 | height: 500px; 69 | } 70 | 71 | .contribute { 72 | display: flex; 73 | flex-direction: row; 74 | height: 43px; 75 | justify-content: space-between; 76 | align-items: center; 77 | margin-top: 10px; 78 | } 79 | 80 | .contribution-errors { 81 | color: red; 82 | margin-top: 5px; 83 | height: 20px; 84 | } 85 | 86 | .back-it { 87 | background-color: #FC3468; 88 | color: white; 89 | align-items: center; 90 | min-height: 30px; 91 | width: 120px; 92 | padding:5px; 93 | margin: 3px; 94 | 95 | } 96 | 97 | .check-out { 98 | background-color: #FC3468; 99 | color: white; 100 | align-items: center; 101 | min-height: 30px; 102 | padding:5px; 103 | margin: 3px; 104 | width: 120px; 105 | } 106 | 107 | .back-it:hover { 108 | cursor: pointer; 109 | background: #ff1854; 110 | } 111 | 112 | 113 | .check-out:hover { 114 | cursor: pointer; 115 | background: #ff1854; 116 | } 117 | 118 | .usr-tag { 119 | display: flex; 120 | flex-direction: row; 121 | align-items: center; 122 | } 123 | 124 | .right-profile { 125 | font-size: 14px; 126 | } 127 | 128 | 129 | .about_me { 130 | color: #878787; 131 | } 132 | 133 | .user-profile { 134 | display: flex; 135 | flex-direction: row; 136 | justify-content: flex-start; 137 | align-items: center; 138 | 139 | } 140 | 141 | .user-tag { 142 | border-radius: 100%; 143 | height: 40px; 144 | width: 40px; 145 | margin-right: 5px; 146 | } 147 | 148 | .more-details { 149 | display: flex; 150 | flex-direction: row; 151 | justify-content: space-between; 152 | margin-top: 40px; 153 | width: 80%; 154 | height: auto; 155 | margin-bottom: 100px; 156 | } 157 | 158 | .show-description { 159 | font-size: 18px; 160 | } 161 | 162 | .show-description > h1 { 163 | font-size: 24px; 164 | color: #878787; 165 | padding-bottom: 5px; 166 | } 167 | 168 | .rewards > h1 { 169 | font-size: 24px; 170 | color: #878787; 171 | padding-bottom: 5px; 172 | } 173 | 174 | .show-description { 175 | display: flex; 176 | flex-direction: column; 177 | justify-content: flex-start; 178 | border-top: 2px solid #eee; 179 | width:465px; 180 | padding-top: 10px; 181 | } 182 | 183 | .rewards { 184 | width: 450px; 185 | display: flex; 186 | flex-direction: column; 187 | justify-content: flex-start; 188 | height: 280px; 189 | border-top: 2px solid #eee; 190 | padding-top: 10px; 191 | } 192 | --------------------------------------------------------------------------------