├── log └── .keep ├── storage └── .keep ├── tmp └── .keep ├── vendor └── .keep ├── lib ├── assets │ └── .keep └── tasks │ └── .keep ├── public ├── favicon.ico ├── apple-touch-icon.png ├── static_files │ ├── favicon.ico │ └── assets │ │ ├── js │ │ └── .DS_Store │ │ ├── fonts │ │ ├── glyphicons-halflings-regular.eot │ │ ├── glyphicons-halflings-regular.ttf │ │ ├── glyphicons-halflings-regular.woff │ │ └── glyphicons-halflings-regular.woff2 │ │ └── css │ │ └── custom.css ├── apple-touch-icon-precomposed.png ├── .DS_Store ├── robots.txt ├── 500.html ├── 422.html ├── 404.html ├── login1.html ├── new-item.html ├── register1.html ├── index1.html └── detail-page.html ├── app ├── assets │ ├── images │ │ └── .keep │ ├── config │ │ └── manifest.js │ └── stylesheets │ │ ├── users.scss │ │ ├── products.scss │ │ ├── application.css │ │ └── custom.css.scss ├── models │ ├── concerns │ │ └── .keep │ ├── application_record.rb │ ├── comment.rb │ ├── user.rb │ └── product.rb ├── controllers │ ├── concerns │ │ └── .keep │ ├── api │ │ └── v1 │ │ │ ├── sessions_controller.rb │ │ │ ├── users_controller.rb │ │ │ ├── comments_controller.rb │ │ │ └── products_controller.rb │ ├── application_controller.rb │ ├── sessions_controller.rb │ ├── users_controller.rb │ ├── comments_controller.rb │ └── products_controller.rb ├── views │ ├── layouts │ │ ├── mailer.text.erb │ │ ├── mailer.html.erb │ │ └── application.html.erb │ ├── api │ │ └── v1 │ │ │ ├── products │ │ │ ├── show.json.jbuilder │ │ │ ├── create.json.jbuilder │ │ │ ├── destroy.json.jbuilder │ │ │ ├── update.json.jbuilder │ │ │ ├── _product.json.jbuilder │ │ │ ├── index.json.jbuilder │ │ │ ├── _product.html.erb │ │ │ └── index.html.erb │ │ │ ├── users │ │ │ └── get_current_user.json.jbuilder │ │ │ └── comments │ │ │ ├── create.json.jbuilder │ │ │ └── index.json.jbuilder │ ├── shared │ │ ├── _footer.html.erb │ │ ├── _errors.html.erb │ │ ├── _error_messages.html.erb │ │ └── _header.html.erb │ ├── comments │ │ ├── _comment.html.erb │ │ └── _form.html.erb │ ├── products │ │ ├── edit.html.erb │ │ ├── new.html.erb │ │ ├── _product.html.erb │ │ ├── index.html.erb │ │ ├── show.html.erb │ │ └── _form.html.erb │ ├── sessions │ │ └── new.html.erb │ └── users │ │ └── new.html.erb ├── helpers │ ├── users_helper.rb │ ├── products_helper.rb │ └── application_helper.rb ├── channels │ └── application_cable │ │ ├── channel.rb │ │ └── connection.rb ├── mailers │ └── application_mailer.rb ├── javascript │ ├── channels │ │ ├── index.js │ │ └── consumer.js │ ├── components │ │ ├── shared │ │ │ ├── Footer.jsx │ │ │ ├── Button.jsx │ │ │ ├── Form.jsx │ │ │ ├── WithFlash.jsx │ │ │ ├── ErrorMessagesHOC.jsx │ │ │ ├── ErrorMessages.jsx │ │ │ ├── TextArea.jsx │ │ │ ├── Input.jsx │ │ │ └── Header.jsx │ │ ├── comments │ │ │ ├── Comment.jsx │ │ │ ├── CommentList.jsx │ │ │ └── CommentForm.jsx │ │ └── products │ │ │ ├── Jumbotron.jsx │ │ │ ├── Product.jsx │ │ │ ├── NewProductForm.jsx │ │ │ ├── ProductForm.jsx │ │ │ └── WithProductForm.jsx │ ├── shared │ │ └── helpers.js │ ├── packs │ │ ├── hello_react.jsx │ │ └── application.js │ └── containers │ │ ├── App.jsx │ │ ├── ProductsContainer.jsx │ │ ├── SigninFormContainer.jsx │ │ ├── ProductDetailContainer.jsx │ │ ├── EditProductFormContainer.jsx │ │ └── SignupFormContainer.jsx └── jobs │ └── application_job.rb ├── .browserslistrc ├── .ruby-version ├── Procfile ├── config ├── spring.rb ├── environment.rb ├── webpack │ ├── test.js │ ├── production.js │ ├── development.js │ └── environment.js ├── initializers │ ├── mime_types.rb │ ├── filter_parameter_logging.rb │ ├── application_controller_renderer.rb │ ├── cookies_serializer.rb │ ├── backtrace_silencers.rb │ ├── wrap_parameters.rb │ ├── assets.rb │ ├── inflections.rb │ └── content_security_policy.rb ├── boot.rb ├── cable.yml ├── credentials.yml.enc ├── database.yml ├── locales │ └── en.yml ├── routes.rb ├── storage.yml ├── application.rb ├── puma.rb ├── environments │ ├── test.rb │ ├── development.rb │ └── production.rb └── webpacker.yml ├── config.ru ├── Rakefile ├── bin ├── rake ├── rails ├── yarn ├── webpack ├── webpack-dev-server ├── spring ├── setup └── bundle ├── db ├── migrate │ ├── 20200323215249_add_user_id_and_quantity_to_products.rb │ ├── 20200320211348_create_products.rb │ ├── 20200324191705_create_comments.rb │ └── 20200321234054_create_users.rb ├── schema.rb └── seeds.rb ├── postcss.config.js ├── README.md ├── package.json ├── .gitignore ├── Gemfile ├── babel.config.js └── Gemfile.lock /log/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /storage/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tmp/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /vendor/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /lib/assets/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /lib/tasks/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/assets/images/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.browserslistrc: -------------------------------------------------------------------------------- 1 | defaults 2 | -------------------------------------------------------------------------------- /.ruby-version: -------------------------------------------------------------------------------- 1 | ruby-2.6.3 2 | -------------------------------------------------------------------------------- /app/models/concerns/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/apple-touch-icon.png: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/controllers/concerns/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/static_files/favicon.ico: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/apple-touch-icon-precomposed.png: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/views/layouts/mailer.text.erb: -------------------------------------------------------------------------------- 1 | <%= yield %> 2 | -------------------------------------------------------------------------------- /app/helpers/users_helper.rb: -------------------------------------------------------------------------------- 1 | module UsersHelper 2 | end 3 | -------------------------------------------------------------------------------- /app/helpers/products_helper.rb: -------------------------------------------------------------------------------- 1 | module ProductsHelper 2 | end 3 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | rails: bundle exec rails server 2 | webpack: bin/webpack-dev-server 3 | -------------------------------------------------------------------------------- /app/views/api/v1/products/show.json.jbuilder: -------------------------------------------------------------------------------- 1 | json.partial! 'product', product: @product 2 | -------------------------------------------------------------------------------- /app/views/api/v1/products/create.json.jbuilder: -------------------------------------------------------------------------------- 1 | json.partial! 'product', product: @product 2 | -------------------------------------------------------------------------------- /app/views/api/v1/products/destroy.json.jbuilder: -------------------------------------------------------------------------------- 1 | json.partial! 'product', product: @product 2 | -------------------------------------------------------------------------------- /app/views/api/v1/products/update.json.jbuilder: -------------------------------------------------------------------------------- 1 | json.partial! 'product', product: @product 2 | -------------------------------------------------------------------------------- /public/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dragon1207/Rails6_React_sales/HEAD/public/.DS_Store -------------------------------------------------------------------------------- /app/assets/config/manifest.js: -------------------------------------------------------------------------------- 1 | //= link_tree ../images 2 | //= link_directory ../stylesheets .css 3 | -------------------------------------------------------------------------------- /app/models/application_record.rb: -------------------------------------------------------------------------------- 1 | class ApplicationRecord < ActiveRecord::Base 2 | self.abstract_class = true 3 | end 4 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # See https://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file 2 | -------------------------------------------------------------------------------- /app/channels/application_cable/channel.rb: -------------------------------------------------------------------------------- 1 | module ApplicationCable 2 | class Channel < ActionCable::Channel::Base 3 | end 4 | end 5 | -------------------------------------------------------------------------------- /app/views/shared/_footer.html.erb: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /config/spring.rb: -------------------------------------------------------------------------------- 1 | Spring.watch( 2 | ".ruby-version", 3 | ".rbenv-vars", 4 | "tmp/restart.txt", 5 | "tmp/caching-dev.txt" 6 | ) 7 | -------------------------------------------------------------------------------- /public/static_files/assets/js/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dragon1207/Rails6_React_sales/HEAD/public/static_files/assets/js/.DS_Store -------------------------------------------------------------------------------- /app/channels/application_cable/connection.rb: -------------------------------------------------------------------------------- 1 | module ApplicationCable 2 | class Connection < ActionCable::Connection::Base 3 | end 4 | end 5 | -------------------------------------------------------------------------------- /app/mailers/application_mailer.rb: -------------------------------------------------------------------------------- 1 | class ApplicationMailer < ActionMailer::Base 2 | default from: 'from@example.com' 3 | layout 'mailer' 4 | end 5 | -------------------------------------------------------------------------------- /config.ru: -------------------------------------------------------------------------------- 1 | # This file is used by Rack-based servers to start the application. 2 | 3 | require_relative 'config/environment' 4 | 5 | run Rails.application 6 | -------------------------------------------------------------------------------- /config/environment.rb: -------------------------------------------------------------------------------- 1 | # Load the Rails application. 2 | require_relative 'application' 3 | 4 | # Initialize the Rails application. 5 | Rails.application.initialize! 6 | -------------------------------------------------------------------------------- /config/webpack/test.js: -------------------------------------------------------------------------------- 1 | process.env.NODE_ENV = process.env.NODE_ENV || 'development' 2 | 3 | const environment = require('./environment') 4 | 5 | module.exports = environment.toWebpackConfig() 6 | -------------------------------------------------------------------------------- /public/static_files/assets/fonts/glyphicons-halflings-regular.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dragon1207/Rails6_React_sales/HEAD/public/static_files/assets/fonts/glyphicons-halflings-regular.eot -------------------------------------------------------------------------------- /public/static_files/assets/fonts/glyphicons-halflings-regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dragon1207/Rails6_React_sales/HEAD/public/static_files/assets/fonts/glyphicons-halflings-regular.ttf -------------------------------------------------------------------------------- /public/static_files/assets/fonts/glyphicons-halflings-regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dragon1207/Rails6_React_sales/HEAD/public/static_files/assets/fonts/glyphicons-halflings-regular.woff -------------------------------------------------------------------------------- /config/webpack/production.js: -------------------------------------------------------------------------------- 1 | process.env.NODE_ENV = process.env.NODE_ENV || 'production' 2 | 3 | const environment = require('./environment') 4 | 5 | module.exports = environment.toWebpackConfig() 6 | -------------------------------------------------------------------------------- /public/static_files/assets/fonts/glyphicons-halflings-regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dragon1207/Rails6_React_sales/HEAD/public/static_files/assets/fonts/glyphicons-halflings-regular.woff2 -------------------------------------------------------------------------------- /app/models/comment.rb: -------------------------------------------------------------------------------- 1 | class Comment < ApplicationRecord 2 | validates :body, presence: true 3 | belongs_to :product 4 | belongs_to :user 5 | 6 | default_scope { order(created_at: :desc) } 7 | end 8 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /config/webpack/development.js: -------------------------------------------------------------------------------- 1 | process.env.NODE_ENV = process.env.NODE_ENV || 'development' 2 | 3 | const environment = require('./environment') 4 | 5 | module.exports = environment.toWebpackConfig() 6 | -------------------------------------------------------------------------------- /app/assets/stylesheets/users.scss: -------------------------------------------------------------------------------- 1 | // Place all the styles related to the Users controller here. 2 | // They will automatically be included in application.css. 3 | // You can use Sass (SCSS) here: https://sass-lang.com/ 4 | -------------------------------------------------------------------------------- /app/assets/stylesheets/products.scss: -------------------------------------------------------------------------------- 1 | // Place all the styles related to the products controller here. 2 | // They will automatically be included in application.css. 3 | // You can use Sass (SCSS) here: https://sass-lang.com/ 4 | -------------------------------------------------------------------------------- /config/boot.rb: -------------------------------------------------------------------------------- 1 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../Gemfile', __dir__) 2 | 3 | require 'bundler/setup' # Set up gems listed in the Gemfile. 4 | require 'bootsnap/setup' # Speed up boot time by caching expensive operations. 5 | -------------------------------------------------------------------------------- /app/views/api/v1/users/get_current_user.json.jbuilder: -------------------------------------------------------------------------------- 1 | if current_user 2 | json.currentUser do 3 | json.id current_user.id 4 | json.email current_user.email 5 | json.fullname current_user.fullname 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /config/cable.yml: -------------------------------------------------------------------------------- 1 | development: 2 | adapter: async 3 | 4 | test: 5 | adapter: test 6 | 7 | production: 8 | adapter: redis 9 | url: <%= ENV.fetch("REDIS_URL") { "redis://localhost:6379/1" } %> 10 | channel_prefix: o_sales_production 11 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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_relative 'config/application' 5 | 6 | Rails.application.load_tasks 7 | -------------------------------------------------------------------------------- /app/helpers/application_helper.rb: -------------------------------------------------------------------------------- 1 | module ApplicationHelper 2 | 3 | def commented_by(user) 4 | user.fullname 5 | end 6 | 7 | def persisted_comments(comments) 8 | comments.reject { |comment| comment.new_record? } 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /app/javascript/channels/index.js: -------------------------------------------------------------------------------- 1 | // Load all the channels within this directory and all subdirectories. 2 | // Channel files must be named *_channel.js. 3 | 4 | const channels = require.context('.', true, /_channel\.js$/) 5 | channels.keys().forEach(channels) 6 | -------------------------------------------------------------------------------- /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/javascript/components/shared/Footer.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | const Footer = () => ( 4 | 7 | ) 8 | 9 | export default Footer 10 | -------------------------------------------------------------------------------- /db/migrate/20200323215249_add_user_id_and_quantity_to_products.rb: -------------------------------------------------------------------------------- 1 | class AddUserIdAndQuantityToProducts < ActiveRecord::Migration[6.0] 2 | def change 3 | add_reference :products, :user, foreign_key: true 4 | add_column :products, :quantity, :integer 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /config/initializers/application_controller_renderer.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # ActiveSupport::Reloader.to_prepare do 4 | # ApplicationController.renderer.defaults.merge!( 5 | # http_host: 'example.org', 6 | # https: false 7 | # ) 8 | # end 9 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: [ 3 | require('postcss-import'), 4 | require('postcss-flexbugs-fixes'), 5 | require('postcss-preset-env')({ 6 | autoprefixer: { 7 | flexbox: 'no-2009' 8 | }, 9 | stage: 3 10 | }) 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /app/views/api/v1/comments/create.json.jbuilder: -------------------------------------------------------------------------------- 1 | json.comment do 2 | json.id @comment.id 3 | json.body @comment.body 4 | json.created_at @comment.created_at 5 | json.product_id @comment.product_id 6 | json.user do 7 | json.fullname @comment.user.fullname 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /app/views/api/v1/products/_product.json.jbuilder: -------------------------------------------------------------------------------- 1 | json.product do 2 | json.id product.id 3 | json.name product.name 4 | json.price number_to_currency(product.price) 5 | json.description product.description 6 | json.quantity product.quantity 7 | json.user_id product.user_id 8 | end 9 | -------------------------------------------------------------------------------- /config/initializers/cookies_serializer.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Specify a serializer for the signed and encrypted cookie jars. 4 | # Valid options are :json, :marshal, and :hybrid. 5 | Rails.application.config.action_dispatch.cookies_serializer = :json 6 | -------------------------------------------------------------------------------- /app/views/api/v1/products/index.json.jbuilder: -------------------------------------------------------------------------------- 1 | json.products @products do |product| 2 | json.id product.id 3 | json.name product.name 4 | json.description product.description 5 | json.price number_to_currency(product.price) 6 | json.quantity product.quantity 7 | end 8 | # json.array! @products 9 | -------------------------------------------------------------------------------- /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', __dir__) 8 | require_relative '../config/boot' 9 | require 'rails/commands' 10 | -------------------------------------------------------------------------------- /app/javascript/channels/consumer.js: -------------------------------------------------------------------------------- 1 | // Action Cable provides the framework to deal with WebSockets in Rails. 2 | // You can generate new channels where WebSocket features live using the `rails generate channel` command. 3 | 4 | import { createConsumer } from "@rails/actioncable" 5 | 6 | export default createConsumer() 7 | -------------------------------------------------------------------------------- /app/jobs/application_job.rb: -------------------------------------------------------------------------------- 1 | class ApplicationJob < ActiveJob::Base 2 | # Automatically retry jobs that encountered a deadlock 3 | # retry_on ActiveRecord::Deadlocked 4 | 5 | # Most jobs are safe to ignore if the underlying records are no longer available 6 | # discard_on ActiveJob::DeserializationError 7 | end 8 | -------------------------------------------------------------------------------- /app/views/api/v1/comments/index.json.jbuilder: -------------------------------------------------------------------------------- 1 | json.comments (@comments) do |comment| 2 | json.id comment.id 3 | json.body comment.body 4 | json.created_at comment.created_at 5 | json.product_id comment.product_id 6 | json.user do 7 | json.fullname comment.user.fullname 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /app/views/layouts/mailer.html.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 8 | 9 | 10 | 11 | <%= yield %> 12 | 13 | 14 | -------------------------------------------------------------------------------- /config/webpack/environment.js: -------------------------------------------------------------------------------- 1 | const { environment } = require('@rails/webpacker') 2 | 3 | const webpack = require("webpack") 4 | 5 | environment.plugins.append("Provide", new webpack.ProvidePlugin({ 6 | $: 'jquery', 7 | jQuery: 'jquery', 8 | Popper: ['popper.js', 'default'] 9 | })) 10 | 11 | module.exports = environment 12 | -------------------------------------------------------------------------------- /app/views/shared/_errors.html.erb: -------------------------------------------------------------------------------- 1 | <% if flash[:notice] %> 2 | 5 | <% end %> 6 | 7 | <% if flash[:alert] %> 8 | 11 | <% end %> 12 | -------------------------------------------------------------------------------- /db/migrate/20200320211348_create_products.rb: -------------------------------------------------------------------------------- 1 | class CreateProducts < ActiveRecord::Migration[6.0] 2 | def change 3 | create_table :products do |t| 4 | t.string :name 5 | t.text :description 6 | t.decimal :price, precision: 8, scale: 2 7 | t.string :image_url 8 | 9 | t.timestamps 10 | end 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /app/views/comments/_comment.html.erb: -------------------------------------------------------------------------------- 1 |
2 |
3 | <%= comment.body %> 4 |
5 | 6 | Created <%= "#{time_ago_in_words(comment.created_at)}" %> ago by: 7 | <%= "#{commented_by comment.user}" %> 8 | 9 |
10 | -------------------------------------------------------------------------------- /bin/yarn: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | APP_ROOT = File.expand_path('..', __dir__) 3 | Dir.chdir(APP_ROOT) do 4 | begin 5 | exec "yarnpkg", *ARGV 6 | rescue Errno::ENOENT 7 | $stderr.puts "Yarn executable was not detected in the system." 8 | $stderr.puts "Download Yarn at https://yarnpkg.com/en/docs/install" 9 | exit 1 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /db/migrate/20200324191705_create_comments.rb: -------------------------------------------------------------------------------- 1 | class CreateComments < ActiveRecord::Migration[6.0] 2 | def change 3 | create_table :comments do |t| 4 | t.text :body 5 | t.references :product, null: false, foreign_key: true 6 | t.references :user, null: false, foreign_key: true 7 | 8 | t.timestamps 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /db/migrate/20200321234054_create_users.rb: -------------------------------------------------------------------------------- 1 | class CreateUsers < ActiveRecord::Migration[6.0] 2 | def change 3 | create_table :users do |t| 4 | t.string :first_name 5 | t.string :last_name 6 | t.string :email 7 | t.string :password_digest 8 | 9 | t.timestamps 10 | end 11 | add_index :users, :email, unique: true 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /app/views/products/edit.html.erb: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 |

6 | Edit Product 7 |

8 | 9 | <%= render 'form' %> 10 |
11 |
12 |
13 |
14 | -------------------------------------------------------------------------------- /app/views/products/new.html.erb: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 |

6 | New Product 7 |

8 | 9 | <%= render 'form' %> 10 |
11 |
12 |
13 |
14 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /app/views/shared/_error_messages.html.erb: -------------------------------------------------------------------------------- 1 | <% if object.errors.any? %> 2 |
3 |
4 | <%= pluralize(object.errors.count, "error") %> 5 | prohibited this <%= object.class.name.humanize.downcase %> from being saved. 6 |
7 | 12 |
13 | <% end %> 14 | -------------------------------------------------------------------------------- /config/credentials.yml.enc: -------------------------------------------------------------------------------- 1 | +r+x+7T9SlI4l4eJ0KMEWsXtHnt4usrzyATP+OdlawEw7DzEdmbIBr3CFcJTMZtrodxSfoEpl/etEucoBoOL+0x0rR7eFX/TS3l/XLeuzU2V+C9qE4oj28bUtK1dD+2xuHAFCVP/bqGjKBhX6XjU/tSsylkP/YWT2coqpDqqQ41Rdq81lmqtSOI6SlXmCWw4m2ssUy8pDiqmlXGKIUr856oaL7VaG1ZQcmxL3v2UQ+tr5T9aqLsdlbAwUWtWbH0/xGnVpk82+hAn1R2QSUU6N2D5XF1abqmR0hVYwHzcIuadNgOQ8GRl1L9Nn0YLeUQtOVGD0rHQG6+NXR1kD/BPSXvvcScusxb97Q9coM2CYsdHtKM5/mFsZhjDMiE30nq7oBkfv3Z7nXgHEfCl5eY5A8IGaimvm/SvF4kU--pRNob/vo19W8yI2k--ZhR6bNff+1wYN2M3mgFthg== -------------------------------------------------------------------------------- /bin/webpack: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | ENV["RAILS_ENV"] ||= ENV["RACK_ENV"] || "development" 4 | ENV["NODE_ENV"] ||= "development" 5 | 6 | require "pathname" 7 | ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile", 8 | Pathname.new(__FILE__).realpath) 9 | 10 | require "bundler/setup" 11 | 12 | require "webpacker" 13 | require "webpacker/webpack_runner" 14 | 15 | APP_ROOT = File.expand_path("..", __dir__) 16 | Dir.chdir(APP_ROOT) do 17 | Webpacker::WebpackRunner.run(ARGV) 18 | end 19 | -------------------------------------------------------------------------------- /bin/webpack-dev-server: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | ENV["RAILS_ENV"] ||= ENV["RACK_ENV"] || "development" 4 | ENV["NODE_ENV"] ||= "development" 5 | 6 | require "pathname" 7 | ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile", 8 | Pathname.new(__FILE__).realpath) 9 | 10 | require "bundler/setup" 11 | 12 | require "webpacker" 13 | require "webpacker/dev_server_runner" 14 | 15 | APP_ROOT = File.expand_path("..", __dir__) 16 | Dir.chdir(APP_ROOT) do 17 | Webpacker::DevServerRunner.run(ARGV) 18 | end 19 | -------------------------------------------------------------------------------- /app/javascript/components/shared/Button.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | 4 | const Button = (props) => ( 5 |
6 |
7 | 10 |
11 |
12 | ) 13 | 14 | Button.propTypes = { 15 | children: PropTypes.string.isRequired 16 | } 17 | 18 | 19 | export default Button 20 | -------------------------------------------------------------------------------- /app/controllers/api/v1/sessions_controller.rb: -------------------------------------------------------------------------------- 1 | class Api::V1::SessionsController < ApplicationController 2 | 3 | 4 | 5 | def create 6 | user = User.find_by(email: params[:email].downcase) 7 | if user && user.authenticate(params[:password]) 8 | cookies.signed[:user_id] = user.id 9 | else 10 | render json: { error: 'Invalid email/password combination'}, status: 401 11 | end 12 | end 13 | 14 | def destroy 15 | cookies.delete :user_id 16 | 17 | end 18 | 19 | end 20 | -------------------------------------------------------------------------------- /app/controllers/application_controller.rb: -------------------------------------------------------------------------------- 1 | class ApplicationController < ActionController::Base 2 | protect_from_forgery with: :null_session, 3 | only: Proc.new { |c| c.request.format.json? } 4 | 5 | private 6 | 7 | def current_user 8 | @current_user ||= User.find_by(id: cookies.signed[:user_id]) 9 | end 10 | 11 | def require_signin 12 | unless current_user 13 | render json: { error: 'Please sign in first' }, status: 401 14 | end 15 | end 16 | 17 | helper_method :current_user 18 | end 19 | -------------------------------------------------------------------------------- /app/javascript/components/shared/Form.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | 4 | const Form = (props) => { 5 | return ( 6 |
7 |
8 | {props.children} 9 |
10 |
11 | ) 12 | } 13 | 14 | Form.propTypes = { 15 | onSubmit: PropTypes.func.isRequired, 16 | children: PropTypes.array.isRequired 17 | } 18 | 19 | export default Form 20 | -------------------------------------------------------------------------------- /app/models/user.rb: -------------------------------------------------------------------------------- 1 | class User < ApplicationRecord 2 | 3 | has_secure_password 4 | validates :first_name, :last_name, presence: true 5 | validates :email, presence: true, uniqueness: { case_sensitive: false }, 6 | format: { 7 | with: /\A[A-Z0-9#-_~!$&'()*+,;=:.]+@[A-Z0-9.-]+\.[A-Z]{2,4}\z/i } 8 | 9 | before_save :downcase_email 10 | 11 | has_many :products, dependent: :destroy 12 | 13 | def fullname 14 | "#{first_name} #{last_name}" 15 | end 16 | 17 | private 18 | 19 | def downcase_email 20 | self.email.downcase! 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /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] 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 | -------------------------------------------------------------------------------- /app/controllers/api/v1/users_controller.rb: -------------------------------------------------------------------------------- 1 | class Api::V1::UsersController < ApplicationController 2 | 3 | 4 | def create 5 | @user = User.new(user_params) 6 | if @user.save 7 | cookies.signed[:user_id] = @user.id 8 | else 9 | render json: @user.errors.full_messages, status: :unprocessable_entity 10 | end 11 | end 12 | 13 | def get_current_user 14 | current_user 15 | end 16 | 17 | 18 | 19 | private 20 | 21 | def user_params 22 | params.require(:user).permit(:first_name, :last_name, :email, :password) 23 | end 24 | 25 | end 26 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /app/models/product.rb: -------------------------------------------------------------------------------- 1 | class Product < ApplicationRecord 2 | validates :name, presence: true, length: { minimum: 2, maximum: 225 }, uniqueness: true 3 | validates :description, presence: true , length: { minimum: 2, maximum: 1500 } 4 | validates :price, presence: true, numericality: { greater_than_or_equal_to: 100.0 } 5 | 6 | validates :quantity, numericality: { 7 | only_integer: true, 8 | greater_than_or_equal_to: 0 9 | } 10 | 11 | belongs_to :user 12 | has_many :comments, dependent: :destroy 13 | 14 | def owned_by?(owner) 15 | user == owner 16 | end 17 | 18 | end 19 | -------------------------------------------------------------------------------- /app/views/products/_product.html.erb: -------------------------------------------------------------------------------- 1 |
2 |
3 | Card image cap 4 |
5 |
6 | <%= number_to_currency(product[:price]) %> 7 | <%= link_to product[:name], product_path(product.id) %> 8 | 9 |
10 |

11 | <%= truncate(product[:description], length: 150) %> 12 |

13 |
14 |
15 |
16 | -------------------------------------------------------------------------------- /app/views/api/v1/products/_product.html.erb: -------------------------------------------------------------------------------- 1 |
2 |
3 | Card image cap 4 |
5 |
6 | <%= number_to_currency(product[:price]) %> 7 | <%= link_to product[:name], api_v1_product_path(product.id) %> 8 | 9 |
10 |

11 | <%= truncate(product[:description], length: 150) %> 12 |

13 |
14 |
15 |
16 | -------------------------------------------------------------------------------- /app/javascript/components/comments/Comment.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | import moment from 'moment' 4 | 5 | const Comment = ({ comment }) => ( 6 |
7 |
8 | { comment.body } 9 |
10 | 11 | Created  { moment(comment.created_at).startOf('minute').fromNow() } by:  12 | { comment.user.fullname } 13 | 14 |
15 | ) 16 | 17 | Comment.propTypes = { 18 | comment: PropTypes.object.isRequired 19 | } 20 | 21 | export default Comment 22 | -------------------------------------------------------------------------------- /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 | # Add Yarn node_modules folder to the asset load path. 9 | Rails.application.config.assets.paths << Rails.root.join('node_modules') 10 | 11 | # Precompile additional assets. 12 | # application.js, application.css, and all non-JS/CSS in the app/assets 13 | # folder are already added. 14 | # Rails.application.config.assets.precompile += %w( admin.js admin.css ) 15 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # README 2 | 3 | 4 | * Ruby version 2.6.3 5 | 6 | * Rails 6.0.2.1 7 | 8 | * Ecommerce site 9 | 10 | * React for frontend 11 | 12 | * Database creation 13 | 14 | * Database initialization 15 | 16 | * How to run the test suite 17 | 18 | * Services (job queues, cache servers, search engines, etc.) 19 | 20 | * Deployment instructions 21 | 22 | * Server-Side Errors 23 | ..1. The server error messages have to be displayed on top of the form 24 | ..2. The form fields don't have to be cleared of the data 25 | ..3. Re-save the data when errors are fixed 26 | ..4. Set a flag to indicate status of the save action 27 | ..5. Clear the form fields only when the data is saved successfully 28 | 29 | * ... 30 | -------------------------------------------------------------------------------- /app/javascript/components/shared/WithFlash.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | const withFlash = (Component) => { 4 | return class withFlash extends React.Component { 5 | state = { 6 | toShow: true 7 | } 8 | 9 | componentDidMount = () => { 10 | if(this.state.toShow){ 11 | setTimeout(() => { 12 | this.setState({ toShow: false }) 13 | }, 2000) 14 | } 15 | } 16 | render(){ 17 | return ( 18 | 19 | { this.state.toShow ? 20 | : null 21 | } 22 | 23 | ) 24 | } 25 | } 26 | } 27 | 28 | export default withFlash 29 | -------------------------------------------------------------------------------- /app/controllers/sessions_controller.rb: -------------------------------------------------------------------------------- 1 | class SessionsController < ApplicationController 2 | 3 | def new 4 | redirect_to root_path if current_user 5 | end 6 | 7 | def create 8 | user = User.find_by(email: params[:email].downcase) 9 | if user && user.authenticate(params[:password]) 10 | cookies.signed[:user_id] = user.id 11 | flash[:notice] = 'Signed in successfully' 12 | redirect_to root_path 13 | else 14 | flash.now[:alert] = 'Invalid email/password combination' 15 | render :new 16 | end 17 | end 18 | 19 | def destroy 20 | cookies.delete :user_id 21 | flash[:notice] = 'You have logged out' 22 | redirect_to root_path 23 | end 24 | 25 | end 26 | -------------------------------------------------------------------------------- /app/views/layouts/application.html.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | OSales 5 | <%= csrf_meta_tags %> 6 | <%= csp_meta_tag %> 7 | 8 | <%= stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track': 'reload' %> 9 | <%= stylesheet_pack_tag 'application', media: 'all', 'data-turbolinks-track': 'reload' %> 10 | <%= javascript_pack_tag 'application', 'data-turbolinks-track': 'reload' %> 11 | 12 | 13 | 14 | 15 | 16 | 17 |
18 | <%= yield %> 19 |
20 | 21 | 22 | 23 | <%#= render 'shared/header' %> 24 | <%#= render 'shared/errors' %> 25 | 26 | <%#= render 'shared/footer' %> 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /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/database.yml: -------------------------------------------------------------------------------- 1 | # SQLite. Versions 3.8.0 and up are supported. 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: sqlite3 9 | pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %> 10 | timeout: 5000 11 | 12 | development: 13 | <<: *default 14 | database: db/development.sqlite3 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: db/test.sqlite3 22 | 23 | production: 24 | <<: *default 25 | database: db/production.sqlite3 26 | -------------------------------------------------------------------------------- /app/controllers/users_controller.rb: -------------------------------------------------------------------------------- 1 | class UsersController < ApplicationController 2 | def new 3 | if current_user 4 | flash[:notice] = 'You have already signed up' 5 | redirect_to root_path 6 | else 7 | @user = User.new 8 | end 9 | end 10 | 11 | def create 12 | @user = User.new(user_params) 13 | if @user.save 14 | cookies.signed[:user_id] = @user.id 15 | flash[:notice] = 'Sign up successfully' 16 | redirect_to root_path 17 | else 18 | flash.now[:alert] = 'There was an error' 19 | render :new 20 | end 21 | end 22 | 23 | 24 | 25 | private 26 | 27 | def user_params 28 | params.require(:user).permit(:first_name, :last_name, :email, :password) 29 | end 30 | 31 | end 32 | -------------------------------------------------------------------------------- /app/javascript/components/shared/ErrorMessagesHOC.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | import withFlash from './WithFlash' 4 | 5 | const ErrorMessages = ({ errors, colWidth="col-md-8 offset-md-2"}) => ( 6 |    7 |     
15 |       {errors.map((error, index) => ({error}

))} 16 |     
17 |    18 | ) 19 | 20 | ErrorMessages.propTypes = { 21 |   errors: PropTypes.array.isRequired, 22 |   colWidth: PropTypes.string 23 | } 24 | 25 | export default withFlash(ErrorMessages) 26 | -------------------------------------------------------------------------------- /app/javascript/shared/helpers.js: -------------------------------------------------------------------------------- 1 | export const inputClasses = (fieldName, state) => { 2 | let classes = 'form-control' 3 | if(state.errors[fieldName]) { 4 | return `${classes} is-invalid` 5 | } 6 | return classes 7 | } 8 | 9 | export const EMAIL_REGEX = /^[A-Z0-9#_-~!$&'()*+,;=:.%]+@[A-Z0-9.-]+\.[A-Z]{2,4}$/i 10 | 11 | 12 | export const verifyAndSetFieldErrors = (context, fieldNames) => { 13 | let errors = {} 14 | 15 | fieldNames.forEach(fieldName => { 16 | const fieldError = context.checkErrors(context.state, fieldName) 17 | errors = Object.assign({}, errors, fieldError) 18 | // errors = { ...errors, ...fieldError} 19 | }) 20 | if(Object.keys(errors).length > 0){ 21 | context.setState({ errors }) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "o_sales", 3 | "private": true, 4 | "dependencies": { 5 | "@babel/preset-react": "^7.9.4", 6 | "@rails/actioncable": "^6.0.0", 7 | "@rails/activestorage": "^6.0.0", 8 | "@rails/ujs": "^6.0.0", 9 | "@rails/webpacker": "4.2.2", 10 | "axios": "0.21.1", 11 | "babel-plugin-transform-react-remove-prop-types": "^0.4.24", 12 | "bootstrap": "4.3.1", 13 | "jquery": "^3.4.1", 14 | "moment": "^2.24.0", 15 | "popper.js": "^1.16.1", 16 | "prop-types": "^15.7.2", 17 | "react": "^16.13.1", 18 | "react-dom": "^16.13.1", 19 | "react-router-dom": "^5.1.2", 20 | "turbolinks": "^5.2.0" 21 | }, 22 | "version": "0.1.0", 23 | "devDependencies": { 24 | "webpack-dev-server": "^3.10.3" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /app/controllers/api/v1/comments_controller.rb: -------------------------------------------------------------------------------- 1 | class Api::V1::CommentsController < ApplicationController 2 | before_action :require_signin, only: [:create] 3 | before_action :set_product 4 | 5 | def index 6 | @comments = @product.comments if @product 7 | end 8 | 9 | def create 10 | @comment = @product.comments.build(comment_params) 11 | @comment.user = current_user 12 | 13 | unless @comment.save 14 | render json: @comment.errors.full_messages, 15 | status: :unprocessable_entity 16 | end 17 | end 18 | 19 | 20 | private 21 | 22 | def set_product 23 | @product = Product.where(id: params[:product_id]).first 24 | end 25 | 26 | def comment_params 27 | params.require(:comment).permit(:body) 28 | end 29 | 30 | end 31 | -------------------------------------------------------------------------------- /app/controllers/comments_controller.rb: -------------------------------------------------------------------------------- 1 | class CommentsController < ApplicationController 2 | before_action :require_signin 3 | before_action :set_product 4 | 5 | def create 6 | @comment = @product.comments.build(comment_params) 7 | @comment.user = current_user 8 | if @comment.save 9 | flash[:notice] = 'Comment has been posted' 10 | redirect_to @product 11 | else 12 | @comments = @product.comments 13 | flash.now[:alert] = 'Comment not created' 14 | render 'products/show' 15 | end 16 | end 17 | 18 | 19 | private 20 | 21 | def set_product 22 | @product = Product.where(id: params[:product_id]).first 23 | end 24 | 25 | def comment_params 26 | params.require(:comment).permit(:body) 27 | end 28 | 29 | end 30 | -------------------------------------------------------------------------------- /app/assets/stylesheets/application.css: -------------------------------------------------------------------------------- 1 | /* 2 | * This is a manifest file that'll be compiled into application.css, which will include all the files 3 | * listed below. 4 | * 5 | * Any CSS and SCSS file within this directory, lib/assets/stylesheets, or any plugin's 6 | * vendor/assets/stylesheets directory can be referenced here using a relative path. 7 | * 8 | * You're free to add application-wide styles to this file and they'll appear at the bottom of the 9 | * compiled file so the styles you add here take precedence over styles defined in any other CSS/SCSS 10 | * files in this directory. Styles in this file should be added after the last require_* statement. 11 | * It is generally better to create a new file per style scope. 12 | * 13 | *= require bootstrap 14 | *= require_tree . 15 | *= require_self 16 | */ 17 | -------------------------------------------------------------------------------- /app/javascript/components/products/Jumbotron.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | const Jumbotron = () => ( 4 |
5 |
6 |
7 |

Welcome to O-Sale online market place!

8 |

Lorem ipsum dolor sit amet,consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

9 |
10 |
11 |
12 | ) 13 | 14 | 15 | export default Jumbotron 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 | /db/*.sqlite3-* 14 | 15 | # Ignore all logfiles and tempfiles. 16 | /log/* 17 | /tmp/* 18 | !/log/.keep 19 | !/tmp/.keep 20 | 21 | # Ignore uploaded files in development. 22 | /storage/* 23 | !/storage/.keep 24 | 25 | /public/assets 26 | .byebug_history 27 | 28 | # Ignore master key for decrypting credentials and more. 29 | /config/master.key 30 | 31 | /public/packs 32 | /public/packs-test 33 | /node_modules 34 | /yarn-error.log 35 | yarn-debug.log* 36 | .yarn-integrity 37 | -------------------------------------------------------------------------------- /app/javascript/components/products/Product.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | import { Link } from 'react-router-dom' 4 | 5 | const Product = ({ product }) => ( 6 |
7 |
8 | Card image cap 9 |
10 |
11 | {product.price} 12 | { product.name } 13 |
14 |

15 | {product.description} 16 |

17 |
18 |
19 |
20 | ) 21 | 22 | Product.propTypes = { 23 | product: PropTypes.object.isRequired 24 | } 25 | export default Product 26 | -------------------------------------------------------------------------------- /app/javascript/components/shared/ErrorMessages.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react' 2 | import PropTypes from 'prop-types' 3 | 4 | const ErrorMessages = ({ errors, colWidth="col-md-8 offset-md-2", flash }) => { 5 | const [ toShow, setToShow ] = useState(true) 6 | 7 | useEffect(() => { 8 | if(flash){ 9 | setTimeout(() => { 10 | setToShow(false) 11 | },2000) 12 | } 13 | }, [toShow]) 14 | 15 | const message = toShow ? 16 |
17 |
18 | {errors.map((error, index) => (

{error}

))} 19 |
20 |
: null 21 | return message 22 | } 23 | 24 | ErrorMessages.propTypes = { 25 | errors: PropTypes.array.isRequired, 26 | colWidth: PropTypes.string 27 | } 28 | 29 | 30 | export default ErrorMessages 31 | -------------------------------------------------------------------------------- /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 | # The following keys must be escaped otherwise they will not be retrieved by 20 | # the default I18n backend: 21 | # 22 | # true, false, on, off, yes, no 23 | # 24 | # Instead, surround them with single quotes. 25 | # 26 | # en: 27 | # 'true': 'foo' 28 | # 29 | # To learn more, please read the Rails Internationalization guide 30 | # available at https://guides.rubyonrails.org/i18n.html. 31 | 32 | en: 33 | hello: "Hello world" 34 | -------------------------------------------------------------------------------- /config/routes.rb: -------------------------------------------------------------------------------- 1 | Rails.application.routes.draw do 2 | root to: 'api/v1/products#index' 3 | namespace :api do 4 | namespace :v1 do 5 | resources :products do 6 | resources :comments, only: [:create, :index] 7 | end 8 | 9 | resources :users, only: [:create] do 10 | collection do 11 | get :get_current_user 12 | end 13 | end 14 | post '/signin', to: 'sessions#create' 15 | delete '/signout', to: 'sessions#destroy', as: 'session' 16 | end 17 | end 18 | 19 | get '*path', to: 'api/v1/products#index' 20 | 21 | 22 | # root to: 'products#index' 23 | # get 'users/new', to: 'users#new', as: 'new_user' 24 | # get '/signup', to: 'users#new' 25 | # get '/signin', to: 'sessions#new' 26 | # post '/signin', to: 'sessions#create' 27 | # delete '/signout', to: 'sessions#destroy', as: 'session' 28 | # resources :users, only: [:create] 29 | # 30 | # 31 | # resources :products do 32 | # resources :comments, only: [:create] 33 | # end 34 | 35 | 36 | 37 | end 38 | -------------------------------------------------------------------------------- /app/javascript/packs/hello_react.jsx: -------------------------------------------------------------------------------- 1 | // Run this example by adding <%= javascript_pack_tag 'hello_react' %> to the head of your layout file, 2 | // like app/views/layouts/application.html.erb. All it does is render
Hello React
at the bottom 3 | // of the page. 4 | 5 | import React from 'react' 6 | import ReactDOM from 'react-dom' 7 | import PropTypes from 'prop-types' 8 | 9 | const Hello = props => ( 10 |
Hello {props.name}!
11 | ) 12 | 13 | 14 | 15 | // class Hello extends React.Component { 16 | // constructor(props){ 17 | // super(props) 18 | // } 19 | // render(){ 20 | // return ( 21 | //
Hello {this.props.name}!
22 | // ) 23 | // } 24 | // } 25 | 26 | Hello.defaultProps = { 27 | name: 'David' 28 | } 29 | 30 | Hello.propTypes = { 31 | name: PropTypes.string 32 | } 33 | 34 | document.addEventListener('DOMContentLoaded', () => { 35 | ReactDOM.render( 36 | , 37 | // document.body.appendChild(document.createElement('div')), 38 | document.getElementById('root'), 39 | ) 40 | }) 41 | -------------------------------------------------------------------------------- /app/views/comments/_form.html.erb: -------------------------------------------------------------------------------- 1 |
2 |
3 |

4 | Add Comments 5 |

6 | 7 |
8 | <%= form_with(model: [product, comment], local: true) do |f| %> 9 | 10 | <%= render 'shared/errors', object: comment %> 11 |
12 |
13 | <%= f.label :body, 'New Comment' %> 14 |
15 | 16 |
17 | <%= f.text_area :body, row: '5', class: 'form-control', placeholder: 'Your comment', autofocus: true %> 18 |
19 |
20 | 21 |
22 |
23 | <%= f.submit 'Add Comment', class: 'btn btn-outline-purple btn-lg' %> 24 |
25 |
26 | <% end %> 27 |
28 |
29 |
30 | -------------------------------------------------------------------------------- /app/views/products/index.html.erb: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |

Welcome to O-Sale online market place!

5 |

Lorem ipsum dolor sit amet,consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

6 |
7 |
8 |
9 | 10 |
11 |
12 |
13 |
14 |
15 | 16 | <% @products.each do |product| %> 17 | <%= render partial: 'product', locals: { product:product } %> 18 | <% end %> 19 | 20 | 21 | 22 |
23 |
24 |
25 |
26 |
27 | -------------------------------------------------------------------------------- /app/views/api/v1/products/index.html.erb: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |

Welcome to O-Sale online market place!

5 |

Lorem ipsum dolor sit amet,consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

6 |
7 |
8 |
9 | 10 |
11 |
12 |
13 |
14 |
15 | 16 | <% @products.each do |product| %> 17 | <%= render partial: 'product', locals: { product:product } %> 18 | <% end %> 19 | 20 | 21 | 22 |
23 |
24 |
25 |
26 |
27 | -------------------------------------------------------------------------------- /bin/setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require 'fileutils' 3 | 4 | # path to your application root. 5 | APP_ROOT = File.expand_path('..', __dir__) 6 | 7 | def system!(*args) 8 | system(*args) || abort("\n== Command #{args} failed ==") 9 | end 10 | 11 | FileUtils.chdir APP_ROOT do 12 | # This script is a way to setup or update your development environment automatically. 13 | # This script is idempotent, so that you can run it at anytime and get an expectable outcome. 14 | # Add necessary setup steps to this file. 15 | 16 | puts '== Installing dependencies ==' 17 | system! 'gem install bundler --conservative' 18 | system('bundle check') || system!('bundle install') 19 | 20 | # Install JavaScript dependencies 21 | # system('bin/yarn') 22 | 23 | # puts "\n== Copying sample files ==" 24 | # unless File.exist?('config/database.yml') 25 | # FileUtils.cp 'config/database.yml.sample', 'config/database.yml' 26 | # end 27 | 28 | puts "\n== Preparing database ==" 29 | system! 'bin/rails db:prepare' 30 | 31 | puts "\n== Removing old logs and tempfiles ==" 32 | system! 'bin/rails log:clear tmp:clear' 33 | 34 | puts "\n== Restarting application server ==" 35 | system! 'bin/rails restart' 36 | end 37 | -------------------------------------------------------------------------------- /config/storage.yml: -------------------------------------------------------------------------------- 1 | test: 2 | service: Disk 3 | root: <%= Rails.root.join("tmp/storage") %> 4 | 5 | local: 6 | service: Disk 7 | root: <%= Rails.root.join("storage") %> 8 | 9 | # Use rails credentials:edit to set the AWS secrets (as aws:access_key_id|secret_access_key) 10 | # amazon: 11 | # service: S3 12 | # access_key_id: <%= Rails.application.credentials.dig(:aws, :access_key_id) %> 13 | # secret_access_key: <%= Rails.application.credentials.dig(:aws, :secret_access_key) %> 14 | # region: us-east-1 15 | # bucket: your_own_bucket 16 | 17 | # Remember not to checkin your GCS keyfile to a repository 18 | # google: 19 | # service: GCS 20 | # project: your_project 21 | # credentials: <%= Rails.root.join("path/to/gcs.keyfile") %> 22 | # bucket: your_own_bucket 23 | 24 | # Use rails credentials:edit to set the Azure Storage secret (as azure_storage:storage_access_key) 25 | # microsoft: 26 | # service: AzureStorage 27 | # storage_account_name: your_account_name 28 | # storage_access_key: <%= Rails.application.credentials.dig(:azure_storage, :storage_access_key) %> 29 | # container: your_container_name 30 | 31 | # mirror: 32 | # service: Mirror 33 | # primary: local 34 | # mirrors: [ amazon, google, microsoft ] 35 | -------------------------------------------------------------------------------- /app/javascript/packs/application.js: -------------------------------------------------------------------------------- 1 | // This file is automatically compiled by Webpack, along with any other files 2 | // present in this directory. You're encouraged to place your actual application logic in 3 | // a relevant structure within app/javascript and only use these pack files to reference 4 | // that code so it'll be compiled. 5 | 6 | require("@rails/ujs").start() 7 | require("turbolinks").start() 8 | require("@rails/activestorage").start() 9 | require("channels") 10 | 11 | 12 | // Uncomment to copy all static images under ../images to the output folder and reference 13 | // them with the image_pack_tag helper in views (e.g <%= image_pack_tag 'rails.png' %>) 14 | // or the `imagePath` JavaScript helper below. 15 | // 16 | // const images = require.context('../images', true) 17 | // const imagePath = (name) => images(name, true) 18 | import "bootstrap" 19 | // import $ from 'jquery'; 20 | // import {$, JQuery} from 'jquery'; 21 | 22 | import React from 'react' 23 | import ReactDOM from 'react-dom' 24 | import PropTypes from 'prop-types' 25 | 26 | import App from '../containers/App' 27 | 28 | document.addEventListener('DOMContentLoaded', () => { 29 | const root = document.getElementById('root') 30 | 31 | ReactDOM.render(,root) 32 | }) 33 | -------------------------------------------------------------------------------- /config/application.rb: -------------------------------------------------------------------------------- 1 | require_relative 'boot' 2 | 3 | require "rails" 4 | # Pick the frameworks you want: 5 | require "active_model/railtie" 6 | require "active_job/railtie" 7 | require "active_record/railtie" 8 | require "active_storage/engine" 9 | require "action_controller/railtie" 10 | require "action_mailer/railtie" 11 | require "action_mailbox/engine" 12 | require "action_text/engine" 13 | require "action_view/railtie" 14 | require "action_cable/engine" 15 | require "sprockets/railtie" 16 | # require "rails/test_unit/railtie" 17 | 18 | # Require the gems listed in Gemfile, including any gems 19 | # you've limited to :test, :development, or :production. 20 | Bundler.require(*Rails.groups) 21 | 22 | module OSales 23 | class Application < Rails::Application 24 | # Initialize configuration defaults for originally generated Rails version. 25 | config.load_defaults 6.0 26 | 27 | # Settings in config/environments/* take precedence over those specified here. 28 | # Application configuration can go into files in config/initializers 29 | # -- all .rb files in that directory are automatically loaded after loading 30 | # the framework and any gems in your application. 31 | 32 | # Don't generate system test files. 33 | config.generators.system_tests = nil 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /app/javascript/components/shared/TextArea.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | import { inputClasses } from '../../shared/helpers' 4 | 5 | const TextArea = (props) => ( 6 |
7 | 8 |
9 | 10 | {props.state.errors[props.name] ? 11 |
{props.state.errors[props.name]} 12 |
: null} 13 |
14 |
15 | ) 16 | 17 | TextArea.propTypes = { 18 | name: PropTypes.string.isRequired, 19 | title: PropTypes.string.isRequired, 20 | value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), 21 | rows: PropTypes.string.isRequired, 22 | state: PropTypes.object.isRequired, 23 | autoFocus: PropTypes.bool.isRequired, 24 | placeholder: PropTypes.string.isRequired, 25 | onChange: PropTypes.func.isRequired, 26 | onBlur: PropTypes.func.isRequired 27 | 28 | } 29 | 30 | export default TextArea 31 | -------------------------------------------------------------------------------- /app/javascript/components/shared/Input.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | import { inputClasses } from '../../shared/helpers' 4 | 5 | const Input = (props) => ( 6 |
7 | 8 |
9 | 10 | {props.state.errors[props.name] ? 11 |
{props.state.errors[props.name]} 12 |
: null} 13 |
14 |
15 | ) 16 | 17 | Input.propTypes = { 18 | type: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired, 19 | name: PropTypes.string.isRequired, 20 | // id: PropTypes.string, 21 | title: PropTypes.string.isRequired, 22 | value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), 23 | state: PropTypes.object.isRequired, 24 | autoFocus: PropTypes.bool.isRequired, 25 | placeholder: PropTypes.string.isRequired, 26 | onChange: PropTypes.func.isRequired, 27 | onBlur: PropTypes.func.isRequired 28 | 29 | } 30 | 31 | export default Input 32 | -------------------------------------------------------------------------------- /app/views/sessions/new.html.erb: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |

5 | Sign In 6 |

7 | 8 | 9 | 10 |
11 | <%= form_with url: signin_path, local: true do |f| %> 12 |
13 |
14 | <%= f.label :email %> 15 |
16 |
17 | <%= f.email_field :email, id: 'email', class: 'form-control', placeholder: 'Your email address', autofocus: true %> 18 |
19 |
20 | 21 |
22 |
23 | <%= f.label :password %> 24 |
25 |
26 | <%= f.password_field :password, id: 'password', class: 'form-control', placesholder: 'Your password here' %> 27 |
28 |
29 | 30 |
31 |
32 | <%= f.submit 'Sign In', class: 'btn btn-outline-purple' %> 33 |
34 |
35 | 36 | <% end %> 37 |
38 | 39 | 40 | 41 |
42 |
43 |
44 | -------------------------------------------------------------------------------- /app/views/shared/_header.html.erb: -------------------------------------------------------------------------------- 1 | 38 | -------------------------------------------------------------------------------- /app/controllers/api/v1/products_controller.rb: -------------------------------------------------------------------------------- 1 | class Api::V1::ProductsController < ApplicationController 2 | before_action :require_signin, except: [:index, :show] 3 | before_action :find_product, only: [:show, :edit, :update, :destroy] 4 | before_action :require_owner, only: [:edit, :update, :destroy] 5 | 6 | def index 7 | @products = Product.all 8 | end 9 | 10 | def show 11 | @comment = @product.comments.build 12 | @comments = @product.comments 13 | end 14 | 15 | def create 16 | @product = Product.new(product_params) 17 | @product.user = current_user 18 | unless @product.save 19 | render json: @product.errors.full_messages, status: :unprocessable_entity 20 | 21 | end 22 | end 23 | 24 | def update 25 | unless @product.update(product_params) 26 | render json: @product.errors.full_messages, status: :unprocessable_entity 27 | end 28 | end 29 | 30 | def destroy 31 | @product.destroy 32 | end 33 | 34 | private 35 | 36 | def require_owner 37 | unless @product.owned_by?(current_user) 38 | render json: { error: 'Access denied' }, status: 403 39 | end 40 | end 41 | 42 | def find_product 43 | begin 44 | @product = Product.find(params[:id]) 45 | rescue ActiveRecord::RecordNotFound 46 | render json: { 47 | error: 'Could not find product' 48 | }, status: 404 49 | end 50 | end 51 | 52 | def product_params 53 | params.require(:product).permit([:name, :price, :description, :quantity, :image_url]) 54 | end 55 | 56 | 57 | end 58 | -------------------------------------------------------------------------------- /config/initializers/content_security_policy.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Define an application-wide content security policy 4 | # For further information see the following documentation 5 | # https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy 6 | 7 | # Rails.application.config.content_security_policy do |policy| 8 | # policy.default_src :self, :https 9 | # policy.font_src :self, :https, :data 10 | # policy.img_src :self, :https, :data 11 | # policy.object_src :none 12 | # policy.script_src :self, :https 13 | # policy.style_src :self, :https 14 | # # If you are using webpack-dev-server then specify webpack-dev-server host 15 | # policy.connect_src :self, :https, "http://localhost:3035", "ws://localhost:3035" if Rails.env.development? 16 | 17 | # # Specify URI for violation reports 18 | # # policy.report_uri "/csp-violation-report-endpoint" 19 | # end 20 | 21 | # If you are using UJS then enable automatic nonce generation 22 | # Rails.application.config.content_security_policy_nonce_generator = -> request { SecureRandom.base64(16) } 23 | 24 | # Set the nonce only to specific directives 25 | # Rails.application.config.content_security_policy_nonce_directives = %w(script-src) 26 | 27 | # Report CSP violations to a specified URI 28 | # For further information see the following documentation: 29 | # https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy-Report-Only 30 | # Rails.application.config.content_security_policy_report_only = true 31 | -------------------------------------------------------------------------------- /config/puma.rb: -------------------------------------------------------------------------------- 1 | # Puma can serve each request in a thread from an internal thread pool. 2 | # The `threads` method setting takes two numbers: a minimum and maximum. 3 | # Any libraries that use thread pools should be configured to match 4 | # the maximum value specified for Puma. Default is set to 5 threads for minimum 5 | # and maximum; this matches the default thread size of Active Record. 6 | # 7 | max_threads_count = ENV.fetch("RAILS_MAX_THREADS") { 5 } 8 | min_threads_count = ENV.fetch("RAILS_MIN_THREADS") { max_threads_count } 9 | threads min_threads_count, max_threads_count 10 | 11 | # Specifies the `port` that Puma will listen on to receive requests; default is 3000. 12 | # 13 | port ENV.fetch("PORT") { 3000 } 14 | 15 | # Specifies the `environment` that Puma will run in. 16 | # 17 | environment ENV.fetch("RAILS_ENV") { "development" } 18 | 19 | # Specifies the `pidfile` that Puma will use. 20 | pidfile ENV.fetch("PIDFILE") { "tmp/pids/server.pid" } 21 | 22 | # Specifies the number of `workers` to boot in clustered mode. 23 | # Workers are forked web server processes. If using threads and workers together 24 | # the concurrency of the application would be max `threads` * `workers`. 25 | # Workers do not work on JRuby or Windows (both of which do not support 26 | # processes). 27 | # 28 | # workers ENV.fetch("WEB_CONCURRENCY") { 2 } 29 | 30 | # Use the `preload_app!` method when specifying a `workers` number. 31 | # This directive tells Puma to first boot the application and load code 32 | # before forking the application. This takes advantage of Copy On Write 33 | # process behavior so workers use less memory. 34 | # 35 | # preload_app! 36 | 37 | # Allow puma to be restarted by `rails restart` command. 38 | plugin :tmp_restart 39 | -------------------------------------------------------------------------------- /app/views/products/show.html.erb: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 | 5 |
6 | 7 |
8 |
9 |

<%= number_to_currency(@product.price) %>

10 |
11 |
12 |

<%= @product.name %>

13 |
14 | 15 |
16 | <%= @product.description %> 17 |
18 | 19 | 20 | <% if current_user && @product.owned_by?(current_user) %> 21 |
22 | <%= link_to 'Delete', product_path(@product), method: :delete, data: {confirm: 'Are you sure you want to delete this product?' }, class: 'btn btn-outline-danger btn-lg' %> 23 |
24 | 25 |
26 | <%= link_to 'Edit', edit_product_path(@product), class: 'btn btn-outline-purple btn-lg' %> 27 |
28 | <% end %> 29 |
30 |
31 | <%= render partial: 'comments/form', locals: { product: @product, comment: @comment } %> 32 |
33 |
34 |

Customer comments (<%= @comments.count %>)

35 |
36 | <% persisted_comments(@comments).each do |comment| %> 37 | <%= render partial: 'comments/comment', object: comment %> 38 | <% end %> 39 |
40 |
41 | -------------------------------------------------------------------------------- /app/controllers/products_controller.rb: -------------------------------------------------------------------------------- 1 | class ProductsController < ApplicationController 2 | before_action :require_signin, except: [:index, :show] 3 | before_action :find_product, only: [:show, :edit, :update, :destroy] 4 | before_action :require_owner, only: [:edit, :update, :destroy] 5 | 6 | def index 7 | @products = Product.all 8 | end 9 | 10 | def show 11 | @comment = @product.comments.build 12 | @comments = @product.comments 13 | end 14 | 15 | def new 16 | @product = Product.new 17 | end 18 | 19 | def create 20 | @product = Product.new(product_params) 21 | @product.user = current_user 22 | if @product.save 23 | flash[:notice] = 'Product has been created' 24 | redirect_to root_path 25 | else 26 | flash.now[:alert] = 'There was an error, please try again' 27 | render :new 28 | end 29 | end 30 | 31 | def edit 32 | end 33 | 34 | def update 35 | if @product.update(product_params) 36 | flash[:notice] = 'Product has been updated' 37 | redirect_to root_path 38 | else 39 | flash.now[:alert] = 'There was an error, please try again' 40 | render :edit 41 | end 42 | end 43 | 44 | def destroy 45 | @product.destroy 46 | flash[:alert] = 'Product has been deleted' 47 | redirect_to root_path 48 | end 49 | 50 | private 51 | 52 | def require_owner 53 | unless @product.owned_by?(current_user) 54 | flash[:alert] = 'Access denied' 55 | redirect_to root_path 56 | end 57 | end 58 | 59 | def find_product 60 | begin 61 | @product = Product.find(params[:id]) 62 | rescue ActiveRecord::RecordNotFound 63 | redirect_to root_path 64 | end 65 | end 66 | 67 | def product_params 68 | params.require(:product).permit([:name, :price, :description, :quantity, :image_url]) 69 | end 70 | 71 | 72 | end 73 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /app/javascript/components/comments/CommentList.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import PropTypes from 'prop-types' 3 | 4 | import Comment from './Comment' 5 | import CommentForm from './CommentForm' 6 | 7 | class CommentList extends Component { 8 | render(){ 9 | const { comments, currentUser } = this.props 10 | let commentList = null 11 | 12 | const commentForm = currentUser ? ( 13 | 19 | ) : null 20 | 21 | if(!comments || comments.length === 0){ 22 | return ( 23 |
24 | { commentForm } 25 |
26 |
27 |

No Comments Yet

28 |
29 |
30 |
31 | ) 32 | } 33 | 34 | commentList = comments && comments.map(comment => ( 35 | 36 | )) 37 | 38 | return ( 39 |
40 | 41 | { commentForm } 42 |
43 |
44 |

Customer comments ({ comments && comments.length })

45 |
46 | { commentList } 47 |
48 |
49 | ) 50 | } 51 | } 52 | 53 | CommentList.propTypes = { 54 | comments: PropTypes.array, 55 | onCommentSubmit: PropTypes.func.isRequired, 56 | serverErrors: PropTypes.array.isRequired, 57 | saved: PropTypes.bool.isRequired, 58 | onResetSaved: PropTypes.func, 59 | currentUser: PropTypes.object, 60 | } 61 | 62 | export default CommentList 63 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | git_source(:github) { |repo| "https://github.com/#{repo}.git" } 3 | 4 | ruby '2.6.3' 5 | 6 | # Bundle edge Rails instead: gem 'rails', github: 'rails/rails' 7 | gem 'rails', '~> 6.0.2', '>= 6.0.2.1' 8 | # Use sqlite3 as the database for Active Record 9 | 10 | # Use Puma as the app server 11 | gem 'puma', '~> 4.1' 12 | # Use SCSS for stylesheets 13 | gem 'sass-rails', '>= 6' 14 | # Transpile app-like JavaScript. Read more: https://github.com/rails/webpacker 15 | gem 'webpacker', '~> 4.0' 16 | # Turbolinks makes navigating your web application faster. Read more: https://github.com/turbolinks/turbolinks 17 | gem 'turbolinks', '~> 5' 18 | # Build JSON APIs with ease. Read more: https://github.com/rails/jbuilder 19 | gem 'jbuilder', '~> 2.7' 20 | # Use Redis adapter to run Action Cable in production 21 | # gem 'redis', '~> 4.0' 22 | # Use Active Model has_secure_password 23 | gem 'bcrypt', '~> 3.1.7' 24 | 25 | # Use Active Storage variant 26 | # gem 'image_processing', '~> 1.2' 27 | 28 | # Reduces boot times through caching; required in config/boot.rb 29 | gem 'bootsnap', '>= 1.4.2', require: false 30 | 31 | group :development, :test do 32 | gem 'sqlite3', '~> 1.4' 33 | # Call 'byebug' anywhere in the code to stop execution and get a debugger console 34 | gem 'byebug', platforms: [:mri, :mingw, :x64_mingw] 35 | end 36 | 37 | group :development do 38 | # Access an interactive console on exception pages or by calling 'console' anywhere in the code. 39 | gem 'web-console', '>= 3.3.0' 40 | gem 'listen', '>= 3.0.5', '< 3.2' 41 | # Spring speeds up development by keeping your application running in the background. Read more: https://github.com/rails/spring 42 | gem 'spring' 43 | gem 'spring-watcher-listen', '~> 2.0.0' 44 | end 45 | 46 | group :production do 47 | gem 'pg' 48 | end 49 | 50 | # Windows does not include zoneinfo files, so bundle the tzinfo-data gem 51 | gem 'tzinfo-data', platforms: [:mingw, :mswin, :x64_mingw, :jruby] 52 | 53 | gem 'foreman', '~> 0.64.0' 54 | -------------------------------------------------------------------------------- /app/views/products/_form.html.erb: -------------------------------------------------------------------------------- 1 |
2 | 3 | <%= form_with model: @product, local: true do |f| %> 4 | <%= render 'shared/error_messages', object: @product %> 5 | 6 |
7 |
8 | <%= f.label :name %> 9 |
10 |
11 | <%= f.text_field :name, class: 'form-control', placeholder: 'Product Name', autofocus: true %> 12 |
13 |
14 | 15 |
16 |
17 | <%= f.label :price %> 18 |
19 |
20 | <%= f.text_field :price, class: 'form-control', placeholder: 'Product Price' %> 21 |
22 |
23 | 24 |
25 |
26 | <%= f.label :Description %> 27 |
28 |
29 | <%= f.text_area :description, id: 'description', class: 'form-control', placeholder: 'Product description', rows: 5 %> 30 |
31 |
32 | 33 |
34 |
35 | <%= f.label :quantity %> 36 |
37 |
38 | <%= f.number_field :quantity, class: 'form-control', placeholder: 'Product Quantity' %> 39 |
40 |
41 | 42 |
43 |
44 | <%= f.label :image_url, 'Image' %> 45 |
46 |
47 | <%= f.file_field :image_url, id: 'image', class: 'form-control' %> 48 |
49 |
50 | 51 |
52 |
53 | <%= f.submit class: 'btn btn-outline-purple' %> 54 | 55 |
56 |
57 | <% end %> 58 |
59 | -------------------------------------------------------------------------------- /app/javascript/components/products/NewProductForm.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import PropTypes from 'prop-types' 3 | 4 | import ErrorMessages from '../shared/ErrorMessages' 5 | import ProductForm from './ProductForm' 6 | import withProductForm from './WithProductForm' 7 | // import { verifyAndSetFieldErrors } from '../../shared/helpers' 8 | 9 | class NewProductForm extends Component { 10 | 11 | 12 | componentDidUpdate = () => { 13 | if(this.props.saved){ 14 | this.props.onSetFields() 15 | // this.setState({ 16 | // name: '', 17 | // price: '', 18 | // description: '', 19 | // quantity: '' 20 | // }) 21 | this.props.onResetSaved() 22 | } 23 | } 24 | 25 | 26 | 27 | render(){ 28 | const buttonText = "Create Product" 29 | const title = "Add New Product" 30 | return ( 31 |
32 |
33 | {this.props.serverErrors.length > 0 && 34 | } 35 |
36 |
37 |

38 | {title} 39 |

40 | 41 | 48 | 49 | 50 | 51 | 52 |
53 |
54 |
55 |
56 | ) 57 | } 58 | } 59 | 60 | NewProductForm.propTypes = { 61 | onSubmit: PropTypes.func.isRequired, 62 | serverErrors: PropTypes.array.isRequired, 63 | saved: PropTypes.bool.isRequired, 64 | onResetSaved: PropTypes.func.isRequired 65 | } 66 | 67 | export default withProductForm(NewProductForm) 68 | -------------------------------------------------------------------------------- /config/environments/test.rb: -------------------------------------------------------------------------------- 1 | # The test environment is used exclusively to run your application's 2 | # test suite. You never need to work with it otherwise. Remember that 3 | # your test database is "scratch space" for the test suite and is wiped 4 | # and recreated between test runs. Don't rely on the data there! 5 | 6 | Rails.application.configure do 7 | # Settings specified here will take precedence over those in config/application.rb. 8 | 9 | config.cache_classes = false 10 | 11 | # Do not eager load code on boot. This avoids loading your whole application 12 | # just for the purpose of running a single test. If you are using a tool that 13 | # preloads Rails for running tests, you may have to set it to true. 14 | config.eager_load = false 15 | 16 | # Configure public file server for tests with Cache-Control for performance. 17 | config.public_file_server.enabled = true 18 | config.public_file_server.headers = { 19 | 'Cache-Control' => "public, max-age=#{1.hour.to_i}" 20 | } 21 | 22 | # Show full error reports and disable caching. 23 | config.consider_all_requests_local = true 24 | config.action_controller.perform_caching = false 25 | config.cache_store = :null_store 26 | 27 | # Raise exceptions instead of rendering exception templates. 28 | config.action_dispatch.show_exceptions = false 29 | 30 | # Disable request forgery protection in test environment. 31 | config.action_controller.allow_forgery_protection = false 32 | 33 | # Store uploaded files on the local file system in a temporary directory. 34 | config.active_storage.service = :test 35 | 36 | config.action_mailer.perform_caching = false 37 | 38 | # Tell Action Mailer not to deliver emails to the real world. 39 | # The :test delivery method accumulates sent emails in the 40 | # ActionMailer::Base.deliveries array. 41 | config.action_mailer.delivery_method = :test 42 | 43 | # Print deprecation notices to the stderr. 44 | config.active_support.deprecation = :stderr 45 | 46 | # Raises error for missing translations. 47 | # config.action_view.raise_on_missing_translations = true 48 | end 49 | -------------------------------------------------------------------------------- /app/javascript/components/shared/Header.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Link, withRouter } from 'react-router-dom' 3 | import PropTypes from 'prop-types' 4 | 5 | const Header = ({ currentUser, onSignout, location, history }) => { 6 | const authLinks = currentUser && ( 7 | 20 | ) 21 | 22 | const unAuthLinks = !currentUser && ( 23 |
    24 |
  • 25 | Home (current) 26 |
  • 27 | 28 |
  • 29 | Sign In 30 |
  • 31 |
  • 32 | Sign Up 33 |
  • 34 |
35 | ) 36 | 37 | return ( 38 | 48 | ) 49 | 50 | } 51 | 52 | Header.propTypes = { 53 | currentUser: PropTypes.object, 54 | onSignout: PropTypes.func.isRequired 55 | } 56 | 57 | export default withRouter(Header) 58 | -------------------------------------------------------------------------------- /db/schema.rb: -------------------------------------------------------------------------------- 1 | # This file is auto-generated from the current state of the database. Instead 2 | # of editing this file, please use the migrations feature of Active Record to 3 | # incrementally modify your database, and then regenerate this schema definition. 4 | # 5 | # This file is the source Rails uses to define your schema when running `rails 6 | # db:schema:load`. When creating a new database, `rails db:schema:load` tends to 7 | # be faster and is potentially less error prone than running all of your 8 | # migrations from scratch. Old migrations may fail to apply correctly if those 9 | # migrations use external dependencies or application code. 10 | # 11 | # It's strongly recommended that you check this file into your version control system. 12 | 13 | ActiveRecord::Schema.define(version: 2020_03_24_191705) do 14 | 15 | create_table "comments", force: :cascade do |t| 16 | t.text "body" 17 | t.integer "product_id", null: false 18 | t.integer "user_id", null: false 19 | t.datetime "created_at", precision: 6, null: false 20 | t.datetime "updated_at", precision: 6, null: false 21 | t.index ["product_id"], name: "index_comments_on_product_id" 22 | t.index ["user_id"], name: "index_comments_on_user_id" 23 | end 24 | 25 | create_table "products", force: :cascade do |t| 26 | t.string "name" 27 | t.text "description" 28 | t.decimal "price", precision: 8, scale: 2 29 | t.string "image_url" 30 | t.datetime "created_at", precision: 6, null: false 31 | t.datetime "updated_at", precision: 6, null: false 32 | t.integer "user_id" 33 | t.integer "quantity" 34 | t.index ["user_id"], name: "index_products_on_user_id" 35 | end 36 | 37 | create_table "users", force: :cascade do |t| 38 | t.string "first_name" 39 | t.string "last_name" 40 | t.string "email" 41 | t.string "password_digest" 42 | t.datetime "created_at", precision: 6, null: false 43 | t.datetime "updated_at", precision: 6, null: false 44 | t.index ["email"], name: "index_users_on_email", unique: true 45 | end 46 | 47 | add_foreign_key "comments", "products" 48 | add_foreign_key "comments", "users" 49 | add_foreign_key "products", "users" 50 | end 51 | -------------------------------------------------------------------------------- /app/javascript/components/products/ProductForm.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | 4 | import Input from '../shared/Input' 5 | import Button from '../shared/Button' 6 | import TextArea from '../shared/TextArea' 7 | import Form from '../shared/Form' 8 | 9 | const ProductForm = (props) => ( 10 |
11 | 22 | 23 | 34 | 35 | 46 | 47 | 64 | 65 | 66 | 67 |
68 | 69 |
70 | 71 |
72 |
73 | 74 |
75 |
76 | 77 |
78 |
79 | 80 |
81 | 82 | 83 | 84 | 85 | 86 | 87 |
88 | Copyright ©2018 Emmanuel Asante 89 |
90 | 91 | 92 | 93 | 94 | 95 | -------------------------------------------------------------------------------- /public/register1.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 33 | 34 |
35 |
36 |
37 | 38 |

39 | Sign Up 40 |

41 | 42 |
43 |
44 |
45 | 48 |
49 | 50 |
51 |
52 | 53 |
54 | 57 |
58 | 59 |
60 |
61 | 62 |
63 | 64 |
65 | 66 |
67 |
68 | 69 |
70 | 71 |
72 | 73 |
74 |
75 | 76 |
77 |
78 | 79 |
80 |
81 |
82 |
83 | 84 |
85 |
86 |
87 | 88 |
89 | Copyright ©2018 Emmanuel Asante 90 |
91 | 92 | 93 | 94 | 95 | 96 | -------------------------------------------------------------------------------- /app/javascript/components/products/WithProductForm.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import axios from 'axios' 3 | 4 | import { verifyAndSetFieldErrors } from '../../shared/helpers' 5 | 6 | const withProductForm = (Component) => { 7 | return class WithProductForm extends React.Component { 8 | state = { 9 | fields: { 10 | name: '', 11 | description: '', 12 | price: '', 13 | quantity: '', 14 | }, 15 | errors: {} 16 | } 17 | 18 | handleSubmit = (event, cb) => { 19 | event.preventDefault() 20 | 21 | const fieldNames = Object.keys(this.state.fields) 22 | verifyAndSetFieldErrors(this, fieldNames) 23 | 24 | if(Object.keys(this.state.errors).length === 0){ 25 | const { id, name, description, price, quantity } = this.state.fields 26 | 27 | const product = { 28 | id, 29 | name, 30 | description, 31 | price: parseFloat(price), 32 | quantity: parseInt(quantity, 10) 33 | } 34 | id ? cb(product) : this.props.onSubmit(product) 35 | } 36 | 37 | } 38 | 39 | handleChange = (event) => { 40 | const { name, value } = event.target 41 | const { fields }= this.state 42 | fields[name] = value 43 | this.setState({ fields }) 44 | this.clearErrors(name, value) 45 | } 46 | 47 | clearErrors = (name, value) => { 48 | let errors = { ...this.state.errors } 49 | 50 | switch(name){ 51 | case 'name': 52 | if(value.length > 0){ 53 | delete errors['name'] 54 | } 55 | break 56 | case 'description': 57 | if(value.length > 0){ 58 | delete errors['description'] 59 | } 60 | break 61 | case 'price': 62 | if(parseFloat(value) > 0.0 || value.match(/^\d{1,}(\.\d{0,2})?$/)){ 63 | delete errors['price'] 64 | } 65 | break 66 | case 'quantity': 67 | if(parseInt(value, 10) > 0 || value.match(/^\d{1,}$/)){ 68 | delete errors['quantity'] 69 | } 70 | break 71 | default: 72 | } 73 | this.setState({ errors }) 74 | } 75 | 76 | checkErrors = (state, fieldName) => { 77 | const error = {} 78 | 79 | switch (fieldName){ 80 | case 'name': 81 | if(!state.fields.name){ 82 | error.name = 'Please provide a name' 83 | } 84 | break 85 | case 'description': 86 | if(!state.fields.description){ 87 | error.description = 'Please provide a description' 88 | } 89 | break 90 | case 'price': 91 | if(parseFloat(state.fields.price) <= 0.0 || !state.fields.price.toString().match(/^\d{1,}(\.\d{0,2})?$/)){ 92 | error.price = 'Price must be a positive number' 93 | } 94 | break 95 | case 'quantity': 96 | if(parseInt(state.fields.quantity, 10) <= 0 || !state.fields.quantity.toString().match(/^\d{1,}$/)){ 97 | error.quantity = 'Quantity must be a positive integer' 98 | } 99 | break 100 | } 101 | return error 102 | } 103 | 104 | handleBlur = (event) => { 105 | const { name } = event.target 106 | const fieldError = this.checkErrors(this.state, name) 107 | const errors = Object.assign({}, this.state.errors, fieldError) 108 | this.setState({ errors }) 109 | } 110 | 111 | setFields = (product, cb) => { 112 | this.setState({ 113 | fields: { 114 | id: product ? product.id : '', 115 | name: product ? product.name : '', 116 | price: product ? product.price : '', 117 | description: product ? product.description : '', 118 | quantity: product ? product.quantity : '', 119 | } 120 | }, cb ? cb() : null) 121 | } 122 | 123 | render(){ 124 | return ( 125 | 133 | ) 134 | } 135 | } 136 | } 137 | 138 | export default withProductForm 139 | -------------------------------------------------------------------------------- /app/javascript/containers/ProductsContainer.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import axios from 'axios' 3 | 4 | import Product from '../components/products/Product' 5 | import Jumbotron from '../components/products/Jumbotron' 6 | import NewProductForm from '../components/products/NewProductForm' 7 | import ErrorMessages from '../components/shared/ErrorMessages' 8 | import ErrorMessagesHOC from '../components/shared/ErrorMessagesHOC' 9 | 10 | 11 | class ProductList extends React.Component { 12 | 13 | state = { 14 | products: [], 15 | serverErrors: [], 16 | saved: false, 17 | isFormVisible: false 18 | } 19 | 20 | componentDidMount = () => { 21 | this.loadProductsFromServer() 22 | } 23 | shouldComponentUpdate = (nextProps, nextState) => { 24 | if(this.state.serverErrors.length > 0 && this.state.serverErrors.length !== nextState.serverErrors.length){ 25 | return false 26 | } 27 | return true 28 | } 29 | 30 | // How to display error message to screen: 31 | // 1. Test to see if error message is not added to the state 32 | // 2. Make sure the prop, this.props.history.location.state is not null 33 | // 3. Set state with error message 34 | // 4. Clear it from route's history - this is to ensure that you don't get error message over and over again 35 | 36 | componentDidUpdate = () => { 37 | if(!this.state.flash && this.props.history.location.state){ 38 | const flashMsg = this.props.history.location.state.error 39 | this.setState({ flash: flashMsg }, () => { 40 | this.props.history.replace('/', null) 41 | }) 42 | } 43 | } 44 | 45 | loadProductsFromServer = () => { 46 | axios 47 | .get('/api/v1/products.json') 48 | .then(response => { 49 | const { products } = response.data 50 | this.setState({ products: products }) 51 | }) 52 | .catch(error => { 53 | console.log(error.response.data) 54 | }) 55 | } 56 | handleProductSubmit = (data) => { 57 | const newProduct = { 58 | product: { ...data } 59 | } 60 | axios 61 | .post('/api/v1/products.json', newProduct) 62 | .then(response => { 63 | // const newProducts = this.state.products.concat(response.data.product) 64 | const newProducts = [ ...this.state.products, response.data.product ] 65 | this.setState({ 66 | products: newProducts, 67 | serverErrors: [], 68 | saved: true 69 | }) 70 | }) 71 | .catch(error => { 72 | const msgs = error.response.data 73 | let currentErrors = [...this.state.serverErrors] 74 | msgs.forEach((msg) => { 75 | if(!currentErrors.includes(msg)) { 76 | currentErrors = [...currentErrors, msg] 77 | } 78 | }) 79 | this.setState({ serverErrors: currentErrors}) 80 | }) 81 | } 82 | 83 | handleButtonClick = () => { 84 | this.setState({ 85 | isFormVisible: !this.state.isFormVisible 86 | }) 87 | } 88 | resetSaved = () => { 89 | this.setState({ 90 | saved: false, 91 | serverErrors: [] 92 | }) 93 | } 94 | 95 | // 1. The server error messages have to be displayed 96 | // 2. The form fields don't have to be cleared of the data 97 | // 3. Re-save the data when errors are fixed 98 | // 4. Set a flag to indicate status of the save action 99 | // 5. Clear the form fields only when the data is saved successfully 100 | 101 | render(){ 102 | const { products } = this.state 103 | // const products = this.state.products 104 | const productList = products.map(product => ( )) 105 | 106 | console.log(this.state) 107 | 108 | return ( 109 | 110 | 111 | {this.state.flash && 112 |
113 | 118 |
119 | } 120 |
121 |
122 |
123 | 124 |
125 |
126 |
127 | {this.state.isFormVisible && 128 | 132 | } 133 |
134 |
135 |
136 |
137 |
138 | { productList } 139 |
140 |
141 |
142 |
143 |
144 |
145 | ) 146 | } 147 | 148 | } 149 | 150 | export default ProductList 151 | -------------------------------------------------------------------------------- /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 | # Ensures that a master key has been made available in either ENV["RAILS_MASTER_KEY"] 18 | # or in config/master.key. This key is used to decrypt credentials (and other encrypted files). 19 | # config.require_master_key = true 20 | 21 | # Disable serving static files from the `/public` folder by default since 22 | # Apache or NGINX already handles this. 23 | config.public_file_server.enabled = ENV['RAILS_SERVE_STATIC_FILES'].present? 24 | 25 | # Compress CSS using a preprocessor. 26 | # config.assets.css_compressor = :sass 27 | 28 | # Do not fallback to assets pipeline if a precompiled asset is missed. 29 | config.assets.compile = false 30 | 31 | # Enable serving of images, stylesheets, and JavaScripts from an asset server. 32 | # config.action_controller.asset_host = 'http://assets.example.com' 33 | 34 | # Specifies the header that your server uses for sending files. 35 | # config.action_dispatch.x_sendfile_header = 'X-Sendfile' # for Apache 36 | # config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' # for NGINX 37 | 38 | # Store uploaded files on the local file system (see config/storage.yml for options). 39 | config.active_storage.service = :local 40 | 41 | # Mount Action Cable outside main process or domain. 42 | # config.action_cable.mount_path = nil 43 | # config.action_cable.url = 'wss://example.com/cable' 44 | # config.action_cable.allowed_request_origins = [ 'http://example.com', /http:\/\/example.*/ ] 45 | 46 | # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies. 47 | # config.force_ssl = true 48 | 49 | # Use the lowest log level to ensure availability of diagnostic information 50 | # when problems arise. 51 | config.log_level = :debug 52 | 53 | # Prepend all log lines with the following tags. 54 | config.log_tags = [ :request_id ] 55 | 56 | # Use a different cache store in production. 57 | # config.cache_store = :mem_cache_store 58 | 59 | # Use a real queuing backend for Active Job (and separate queues per environment). 60 | # config.active_job.queue_adapter = :resque 61 | # config.active_job.queue_name_prefix = "o_sales_production" 62 | 63 | config.action_mailer.perform_caching = false 64 | 65 | # Ignore bad email addresses and do not raise email delivery errors. 66 | # Set this to true and configure the email server for immediate delivery to raise delivery errors. 67 | # config.action_mailer.raise_delivery_errors = false 68 | 69 | # Enable locale fallbacks for I18n (makes lookups for any locale fall back to 70 | # the I18n.default_locale when a translation cannot be found). 71 | config.i18n.fallbacks = true 72 | 73 | # Send deprecation notices to registered listeners. 74 | config.active_support.deprecation = :notify 75 | 76 | # Use default logging formatter so that PID and timestamp are not suppressed. 77 | config.log_formatter = ::Logger::Formatter.new 78 | 79 | # Use a different logger for distributed setups. 80 | # require 'syslog/logger' 81 | # config.logger = ActiveSupport::TaggedLogging.new(Syslog::Logger.new 'app-name') 82 | 83 | if ENV["RAILS_LOG_TO_STDOUT"].present? 84 | logger = ActiveSupport::Logger.new(STDOUT) 85 | logger.formatter = config.log_formatter 86 | config.logger = ActiveSupport::TaggedLogging.new(logger) 87 | end 88 | 89 | # Do not dump schema after migrations. 90 | config.active_record.dump_schema_after_migration = false 91 | 92 | # Inserts middleware to perform automatic connection switching. 93 | # The `database_selector` hash is used to pass options to the DatabaseSelector 94 | # middleware. The `delay` is used to determine how long to wait after a write 95 | # to send a subsequent read to the primary. 96 | # 97 | # The `database_resolver` class is used by the middleware to determine which 98 | # database is appropriate to use based on the time delay. 99 | # 100 | # The `database_resolver_context` class is used by the middleware to set 101 | # timestamps for the last write to the primary. The resolver uses the context 102 | # class timestamps to determine how long to wait before reading from the 103 | # replica. 104 | # 105 | # By default Rails will store a last write timestamp in the session. The 106 | # DatabaseSelector middleware is designed as such you can define your own 107 | # strategy for connection switching and pass that into the middleware through 108 | # these configuration options. 109 | # config.active_record.database_selector = { delay: 2.seconds } 110 | # config.active_record.database_resolver = ActiveRecord::Middleware::DatabaseSelector::Resolver 111 | # config.active_record.database_resolver_context = ActiveRecord::Middleware::DatabaseSelector::Resolver::Session 112 | end 113 | -------------------------------------------------------------------------------- /app/javascript/containers/SigninFormContainer.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import PropTypes from 'prop-types' 3 | import { Redirect } from 'react-router-dom' 4 | import axios from 'axios' 5 | 6 | import Input from '../components/shared/Input' 7 | import Button from '../components/shared/Button' 8 | import SigninForm from '../components/shared/Form' 9 | 10 | import { EMAIL_REGEX } from '../shared/helpers' 11 | import { verifyAndSetFieldErrors } from '../shared/helpers' 12 | import ErrorMessages from '../components/shared/ErrorMessages' 13 | 14 | 15 | class Signin extends Component { 16 | state = { 17 | email: '', 18 | password: '', 19 | errors: {}, 20 | toHomePage: false, 21 | serverErrors: [], 22 | saved: false 23 | } 24 | 25 | componentDidUpdate = () => { 26 | if(this.state.saved){ 27 | this.setState({ 28 | email: '', 29 | password: '', 30 | toHomePage: true 31 | }) 32 | this.resetSaved() 33 | } 34 | } 35 | 36 | componentWillUnmount = () => { 37 | if(this.state.serverErrors.length > 0){ 38 | this.resetSaved() 39 | } 40 | } 41 | 42 | handleChange = (event) => { 43 | const { name, value } = event.target 44 | this.setState({ [name]: value }) 45 | this.clearErrors(name, value) 46 | } 47 | 48 | checkErrors = (state, fieldName) => { 49 | const error = {} 50 | 51 | switch (fieldName){ 52 | case 'email': 53 | if(!state.email || !EMAIL_REGEX.test(state.email)){ 54 | error.email = 'Please provide valid email address' 55 | } 56 | break 57 | case 'password': 58 | if(!state.password){ 59 | error.password = 'Please provide valid password' 60 | } 61 | break 62 | default: 63 | } 64 | return error 65 | } 66 | 67 | clearErrors = (name, value) => { 68 | let errors = { ...this.state.errors } 69 | 70 | switch (name){ 71 | case 'email': 72 | if(value.length > 0 && EMAIL_REGEX.test(value)){ 73 | delete errors['email'] 74 | } 75 | break 76 | case 'password': 77 | if(value.length > 0){ 78 | delete errors['password'] 79 | } 80 | break 81 | default: 82 | } 83 | this.setState({ errors }) 84 | } 85 | 86 | handleBlur = (event) => { 87 | const { name } = event.target 88 | const fieldError = this.checkErrors(this.state, name) 89 | const errors = Object.assign({}, this.state.errors, fieldError) 90 | this.setState({ errors }) 91 | } 92 | 93 | handleSubmit = (event) => { 94 | event.preventDefault() 95 | const fieldNames = ['email', 'password'] 96 | verifyAndSetFieldErrors(this, fieldNames) 97 | 98 | if(Object.keys(this.state.errors).length === 0){ 99 | const user = { 100 | email: this.state.email, 101 | password: this.state.password 102 | } 103 | this.handleSignin(user) 104 | } 105 | } 106 | 107 | handleSignin = (user) => { 108 | axios 109 | .post('/api/v1/signin.json', user) 110 | .then(response => { 111 | this.setState({ 112 | toHomePage: true, 113 | serverErrors: [], 114 | saved: true 115 | }, () => { 116 | this.props.onFetchCurrentUser() 117 | }) 118 | }) 119 | .catch(error => { 120 | const msg = error.response.data.error 121 | const idx = this.state.serverErrors.indexOf(msg) 122 | 123 | if(idx == -1){ 124 | this.setState({ 125 | serverErrors: [...this.state.serverErrors, msg] 126 | }) 127 | } 128 | }) 129 | } 130 | 131 | resetSaved = () => { 132 | this.setState({ 133 | saved: false, 134 | serverErrors: [] 135 | }) 136 | } 137 | 138 | render(){ 139 | if(this.state.toHomePage || this.props.currentUser){ 140 | return 141 | } 142 | return( 143 |
144 |
145 | {this.state.serverErrors.length > 0 && 146 | } 147 |
148 |

Sign In

149 | 150 | 161 | 172 | 173 | 174 |
175 |
176 |
177 | ) 178 | } 179 | } 180 | 181 | Signin.propTypes = { 182 | onFetchCurrentUser: PropTypes.func.isRequired, 183 | currentUser: PropTypes.object 184 | } 185 | 186 | export default Signin 187 | -------------------------------------------------------------------------------- /app/javascript/containers/ProductDetailContainer.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import axios from 'axios' 3 | import PropTypes from 'prop-types' 4 | import { Link, Route } from 'react-router-dom' 5 | 6 | import NewProductForm from '../components/products/NewProductForm' 7 | import CommentList from '../components/comments/CommentList' 8 | 9 | 10 | class ProductDetail extends React.Component { 11 | constructor(props){ 12 | super(props) 13 | 14 | this.state = { 15 | product: {}, 16 | editing: false, 17 | updated: false, 18 | comments: [], 19 | saved: false, 20 | serverErrors: [] 21 | } 22 | } 23 | 24 | componentDidMount(){ 25 | this.getProductAndComments() 26 | } 27 | 28 | componentDidUpate = () => { 29 | if(this.state.editing && this.state.updated){ 30 | this.getProductAndComments() 31 | } 32 | } 33 | 34 | getProductAndComments = () => { 35 | const id = this.props.match && this.props.match.params.id 36 | 37 | if(id){ 38 | axios 39 | .all([ 40 | axios.get(`/api/v1/products/${id}.json`), 41 | axios.get(`/api/v1/products/${id}/comments.json`) 42 | ]) 43 | .then(axios.spread((productResponse, commentsResponse) => { 44 | this.setState({ 45 | product: productResponse.data.product, 46 | comments: commentsResponse.data.comments 47 | }) 48 | })) 49 | .catch(error => { 50 | this.props.history.push({ 51 | pathname: '/', 52 | state: { error: error.response.data.error } 53 | }) 54 | }) 55 | } 56 | } 57 | 58 | 59 | editingProduct = (value) => { 60 | if(value == undefined){ 61 | this.setState({ editing: true }) 62 | } else if(value === "edited"){ 63 | this.setState({ editing: false }) 64 | } 65 | } 66 | 67 | setUpdated = (value) => { 68 | this.setState({updated: value}) 69 | } 70 | 71 | isOwner = (user, product) => { 72 | if(Object.keys(product).length > 0){ 73 | return user && user.id === product.user_id 74 | } 75 | return false 76 | } 77 | 78 | handleDelete = (event) => { 79 | event.preventDefault() 80 | this.handleProductDelete(this.props.match.params.id) 81 | } 82 | 83 | handleCommentSubmit = (data) => { 84 | const id = +this.props.match.params.id 85 | axios 86 | .post(`/api/v1/products/${id/comments.json}`, data) 87 | .then(response => { 88 | const comments = [response.data.comment, ...this.state.comments] 89 | this.setState({ comments }) 90 | }) 91 | .catch(error => ( 92 | this.setState({ serverErrors: error.response.data }) 93 | )) 94 | } 95 | 96 | handleProductDelete = (id) => { 97 | axios 98 | .delete(`/api/v1/products/${id}.json`) 99 | .then(response => { 100 | this.props.history.push('/') 101 | }) 102 | .catch(error => console.log(error)) 103 | } 104 | 105 | resetSaved = () => { 106 | this.setState({ 107 | saved: false, 108 | serverErrors: [] 109 | }) 110 | } 111 | 112 | render(){ 113 | const id = this.props.match && this.props.match.params.id 114 | const { product } = this.state 115 | const { currentUser } = this.props 116 | 117 | console.log(this.props) 118 | 119 | return ( 120 |
121 |
122 |
123 | 124 |
125 | 126 |
127 |
128 |

{product.price}

129 |
130 |
131 |

{product.name}

132 |
133 | 134 |
135 | {product.description} 136 |
137 | 138 | {this.isOwner(currentUser, product) && !this.state.editing ? 139 | 140 | 141 |
142 | Delete 143 |
144 | 145 |
146 | Edit 147 |
148 |
: null 149 | } 150 |
151 | {this.isOwner(currentUser, product) ? 152 | 153 | ( 154 | 159 | )} /> : null 160 | } 161 |
162 | 163 |
164 | {!this.state.editing ? 165 | : null 173 | } 174 | 175 |
176 | ) 177 | } 178 | } 179 | 180 | ProductDetail.propTypes = { 181 | currentUser: PropTypes.object 182 | } 183 | 184 | export default ProductDetail 185 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | actioncable (6.0.2.1) 5 | actionpack (= 6.0.2.1) 6 | nio4r (~> 2.0) 7 | websocket-driver (>= 0.6.1) 8 | actionmailbox (6.0.2.1) 9 | actionpack (= 6.0.2.1) 10 | activejob (= 6.0.2.1) 11 | activerecord (= 6.0.2.1) 12 | activestorage (= 6.0.2.1) 13 | activesupport (= 6.0.2.1) 14 | mail (>= 2.7.1) 15 | actionmailer (6.0.2.1) 16 | actionpack (= 6.0.2.1) 17 | actionview (= 6.0.2.1) 18 | activejob (= 6.0.2.1) 19 | mail (~> 2.5, >= 2.5.4) 20 | rails-dom-testing (~> 2.0) 21 | actionpack (6.0.2.1) 22 | actionview (= 6.0.2.1) 23 | activesupport (= 6.0.2.1) 24 | rack (~> 2.0, >= 2.0.8) 25 | rack-test (>= 0.6.3) 26 | rails-dom-testing (~> 2.0) 27 | rails-html-sanitizer (~> 1.0, >= 1.2.0) 28 | actiontext (6.0.2.1) 29 | actionpack (= 6.0.2.1) 30 | activerecord (= 6.0.2.1) 31 | activestorage (= 6.0.2.1) 32 | activesupport (= 6.0.2.1) 33 | nokogiri (>= 1.8.5) 34 | actionview (6.0.2.1) 35 | activesupport (= 6.0.2.1) 36 | builder (~> 3.1) 37 | erubi (~> 1.4) 38 | rails-dom-testing (~> 2.0) 39 | rails-html-sanitizer (~> 1.1, >= 1.2.0) 40 | activejob (6.0.2.1) 41 | activesupport (= 6.0.2.1) 42 | globalid (>= 0.3.6) 43 | activemodel (6.0.2.1) 44 | activesupport (= 6.0.2.1) 45 | activerecord (6.0.2.1) 46 | activemodel (= 6.0.2.1) 47 | activesupport (= 6.0.2.1) 48 | activestorage (6.0.2.1) 49 | actionpack (= 6.0.2.1) 50 | activejob (= 6.0.2.1) 51 | activerecord (= 6.0.2.1) 52 | marcel (~> 0.3.1) 53 | activesupport (6.0.2.1) 54 | concurrent-ruby (~> 1.0, >= 1.0.2) 55 | i18n (>= 0.7, < 2) 56 | minitest (~> 5.1) 57 | tzinfo (~> 1.1) 58 | zeitwerk (~> 2.2) 59 | bcrypt (3.1.13) 60 | bindex (0.8.1) 61 | bootsnap (1.4.6) 62 | msgpack (~> 1.0) 63 | builder (3.2.4) 64 | byebug (11.1.1) 65 | concurrent-ruby (1.1.6) 66 | crass (1.0.6) 67 | dotenv (0.7.0) 68 | erubi (1.9.0) 69 | ffi (1.12.2) 70 | foreman (0.64.0) 71 | dotenv (~> 0.7.0) 72 | thor (>= 0.13.6) 73 | globalid (0.4.2) 74 | activesupport (>= 4.2.0) 75 | i18n (1.8.2) 76 | concurrent-ruby (~> 1.0) 77 | jbuilder (2.10.0) 78 | activesupport (>= 5.0.0) 79 | listen (3.1.5) 80 | rb-fsevent (~> 0.9, >= 0.9.4) 81 | rb-inotify (~> 0.9, >= 0.9.7) 82 | ruby_dep (~> 1.2) 83 | loofah (2.4.0) 84 | crass (~> 1.0.2) 85 | nokogiri (>= 1.5.9) 86 | mail (2.7.1) 87 | mini_mime (>= 0.1.1) 88 | marcel (0.3.3) 89 | mimemagic (~> 0.3.2) 90 | method_source (1.0.0) 91 | mimemagic (0.3.4) 92 | mini_mime (1.0.2) 93 | mini_portile2 (2.4.0) 94 | minitest (5.14.0) 95 | msgpack (1.3.3) 96 | nio4r (2.5.2) 97 | nokogiri (1.10.9) 98 | mini_portile2 (~> 2.4.0) 99 | pg (1.1.4) 100 | puma (4.3.3) 101 | nio4r (~> 2.0) 102 | rack (2.2.2) 103 | rack-proxy (0.6.5) 104 | rack 105 | rack-test (1.1.0) 106 | rack (>= 1.0, < 3) 107 | rails (6.0.2.1) 108 | actioncable (= 6.0.2.1) 109 | actionmailbox (= 6.0.2.1) 110 | actionmailer (= 6.0.2.1) 111 | actionpack (= 6.0.2.1) 112 | actiontext (= 6.0.2.1) 113 | actionview (= 6.0.2.1) 114 | activejob (= 6.0.2.1) 115 | activemodel (= 6.0.2.1) 116 | activerecord (= 6.0.2.1) 117 | activestorage (= 6.0.2.1) 118 | activesupport (= 6.0.2.1) 119 | bundler (>= 1.3.0) 120 | railties (= 6.0.2.1) 121 | sprockets-rails (>= 2.0.0) 122 | rails-dom-testing (2.0.3) 123 | activesupport (>= 4.2.0) 124 | nokogiri (>= 1.6) 125 | rails-html-sanitizer (1.3.0) 126 | loofah (~> 2.3) 127 | railties (6.0.2.1) 128 | actionpack (= 6.0.2.1) 129 | activesupport (= 6.0.2.1) 130 | method_source 131 | rake (>= 0.8.7) 132 | thor (>= 0.20.3, < 2.0) 133 | rake (13.0.1) 134 | rb-fsevent (0.10.3) 135 | rb-inotify (0.10.1) 136 | ffi (~> 1.0) 137 | ruby_dep (1.5.0) 138 | sass-rails (6.0.0) 139 | sassc-rails (~> 2.1, >= 2.1.1) 140 | sassc (2.2.1) 141 | ffi (~> 1.9) 142 | sassc-rails (2.1.2) 143 | railties (>= 4.0.0) 144 | sassc (>= 2.0) 145 | sprockets (> 3.0) 146 | sprockets-rails 147 | tilt 148 | spring (2.1.0) 149 | spring-watcher-listen (2.0.1) 150 | listen (>= 2.7, < 4.0) 151 | spring (>= 1.2, < 3.0) 152 | sprockets (4.0.0) 153 | concurrent-ruby (~> 1.0) 154 | rack (> 1, < 3) 155 | sprockets-rails (3.2.1) 156 | actionpack (>= 4.0) 157 | activesupport (>= 4.0) 158 | sprockets (>= 3.0.0) 159 | sqlite3 (1.4.2) 160 | thor (1.0.1) 161 | thread_safe (0.3.6) 162 | tilt (2.0.10) 163 | turbolinks (5.2.1) 164 | turbolinks-source (~> 5.2) 165 | turbolinks-source (5.2.0) 166 | tzinfo (1.2.6) 167 | thread_safe (~> 0.1) 168 | web-console (4.0.1) 169 | actionview (>= 6.0.0) 170 | activemodel (>= 6.0.0) 171 | bindex (>= 0.4.0) 172 | railties (>= 6.0.0) 173 | webpacker (4.2.2) 174 | activesupport (>= 4.2) 175 | rack-proxy (>= 0.6.1) 176 | railties (>= 4.2) 177 | websocket-driver (0.7.1) 178 | websocket-extensions (>= 0.1.0) 179 | websocket-extensions (0.1.4) 180 | zeitwerk (2.3.0) 181 | 182 | PLATFORMS 183 | ruby 184 | 185 | DEPENDENCIES 186 | bcrypt (~> 3.1.7) 187 | bootsnap (>= 1.4.2) 188 | byebug 189 | foreman (~> 0.64.0) 190 | jbuilder (~> 2.7) 191 | listen (>= 3.0.5, < 3.2) 192 | pg 193 | puma (~> 4.1) 194 | rails (~> 6.0.2, >= 6.0.2.1) 195 | sass-rails (>= 6) 196 | spring 197 | spring-watcher-listen (~> 2.0.0) 198 | sqlite3 (~> 1.4) 199 | turbolinks (~> 5) 200 | tzinfo-data 201 | web-console (>= 3.3.0) 202 | webpacker (~> 4.0) 203 | 204 | RUBY VERSION 205 | ruby 2.6.3p62 206 | 207 | BUNDLED WITH 208 | 2.0.2 209 | -------------------------------------------------------------------------------- /public/index1.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 34 | 35 |
36 |
37 |
38 |

Welcome to O-Sale online market place!

39 |

Lorem ipsum dolor sit amet,consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

40 |
41 |
42 |
43 | 44 |
45 |
46 |
47 |
48 |
49 | 50 |
51 |
52 | Card image cap 53 |
54 |
55 | $99.99 56 | Name 1 57 |
58 |

59 | Lorem ipsum dolor sit amet,consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. 60 |

61 |
62 |
63 |
64 | 65 |
66 |
67 | Card image cap 68 |
69 |
70 | $57.99 71 | Name 2 72 |
73 |

74 | Lorem ipsum dolor sit amet,consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. 75 |

76 |
77 |
78 |
79 | 80 |
81 |
82 | Card image cap 83 |
84 |
85 | $38.99 86 | Name 3 87 |
88 |

89 | Lorem ipsum dolor sit amet,consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. 90 |

91 |
92 |
93 |
94 | 95 |
96 |
97 |
98 |
99 |
100 | 101 |
102 | Copyright ©2019 Emmanuel Asante 103 |
104 | 105 | 106 | 107 | 108 | 109 | -------------------------------------------------------------------------------- /public/detail-page.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 33 | 34 |
35 |
36 |
37 | 38 |
39 | 40 |
41 |
42 |

$99.99

43 |
44 |
45 |

Name 1

46 |
47 | 48 |
49 | Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. 50 |
51 | 52 |
53 | Delete 54 |
55 | 56 |
57 | Edit 58 |
59 |
60 |
61 | 62 |
63 |
64 |

65 | Add Comments 66 |

67 | 68 |
69 |
70 |
71 | 72 |
73 | 76 |
77 |
78 | 79 |
80 |
81 | 84 |
85 |
86 |
87 |
88 |
89 |
90 | 91 |
92 |
93 |

Customer comments (3)

94 |
95 | 96 |
97 |
98 | Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. 99 |
100 | Created 15 days ago by: 101 | John Doe 102 | 103 |
104 | 105 |
106 |
107 | Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. 108 |
109 | Created 11 days ago by: 110 | John Doe 111 | 112 |
113 | 114 |
115 |
116 | Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. 117 |
118 | Created 5 days ago by: 119 | John Doe 120 | 121 |
122 |
123 | 124 |
125 | 126 |
127 | Copyright ©2018 Emmanuel Asante 128 |
129 | 130 | 131 | 132 | 133 | 134 | -------------------------------------------------------------------------------- /app/javascript/containers/EditProductFormContainer.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import PropTypes from 'prop-types' 3 | import axios from 'axios' 4 | 5 | import ErrorMessages from '../components/shared/ErrorMessages' 6 | import ProductForm from '../components/products/ProductForm' 7 | import withProductForm from '../components/products/WithProductForm' 8 | // import { verifyAndSetFieldErrors } from '../shared/helpers' 9 | 10 | class EditProductForm extends Component { 11 | state = { 12 | serverErrors: [], 13 | saved: false 14 | } 15 | 16 | componentDidMount = () => { 17 | const id = this.props.match && +this.props.match.params.id 18 | if(id){ 19 | this.getProduct(id) 20 | } 21 | } 22 | 23 | componentWillUnmount = () => { 24 | const id = this.props.match && this.props.match.params.id 25 | id && this.props.onEdit('edited') 26 | this.props.onUpdate(false) 27 | 28 | if(this.state.serverErrors.length > 0){ 29 | this.resetSaved() 30 | } 31 | } 32 | 33 | getProduct = (id) => { 34 | axios 35 | .get(`/api/v1/products/${id}.json`) 36 | .then(response => { 37 | const product = response.data.product 38 | const idx = product.price.search(/\d/) 39 | product.price = product.price.slice(idx) 40 | 41 | this.props.onSetFields(product, () => { 42 | this.props.onEdit() 43 | }) 44 | 45 | // this.setState({ 46 | // id: product.id, 47 | // name: product.name, 48 | // description: product.description, 49 | // price: product.price, 50 | // quantity: product.quantity 51 | // }, () => { 52 | // this.props.onEdit() 53 | // }) 54 | }) 55 | .catch(error => console.log(error)) 56 | } 57 | 58 | resetSaved = () => { 59 | this.setState({ 60 | saved: false, 61 | serverErrors: [] 62 | }) 63 | } 64 | 65 | // handleChange = (event) => { 66 | // const { name, value } = event.target 67 | // this.setState({ [name]: value }) 68 | // this.clearErrors(name, value) 69 | // } 70 | 71 | // handleSubmit = (event) => { 72 | // event.preventDefault() 73 | // 74 | // const fieldNames = ['name', 'description', 'price', 'quantity'] 75 | // verifyAndSetFieldErrors(this, fieldNames) 76 | // 77 | // if(Object.keys(this.state.errors).length === 0){ 78 | // const { id, name, description, price, quantity } = this.state 79 | // const editedProduct = { 80 | // id, 81 | // name, 82 | // description, 83 | // price: parseFloat(price), 84 | // quantity: parseInt(quantity, 10) 85 | // } 86 | // this.handleProductUpdate(editedProduct) 87 | // } 88 | // 89 | // } 90 | 91 | handleSubmit = (event) => { 92 | this.props.onSubmit(event, this.handleProductUpdate) 93 | } 94 | 95 | handleProductUpdate = (data) => { 96 | const updatedProduct = { 97 | product: { ...data } 98 | } 99 | axios 100 | .put(`/api/v1/products/${data.id}.json`, updatedProduct) 101 | .then(response => { 102 | const { product } = response.data 103 | this.setState({ 104 | 105 | serverErrors: [], 106 | saved: true 107 | }, () => { 108 | this.props.onSetFields(product) 109 | this.props.onUpdate(true) 110 | this.props.history.push(`/products/${data.id}`) 111 | }) 112 | }) 113 | .catch(error => { 114 | const updatedErrors = [ 115 | ...this.state.serverErrors, 116 | ...error.response.data 117 | ] 118 | 119 | const errorsSet = new Set(updatedErrors) 120 | this.setState({ serverErrors: [...errorsSet] }) 121 | }) 122 | } 123 | 124 | // checkErrors = (state, fieldName) => { 125 | // const error = {} 126 | // 127 | // switch (fieldName){ 128 | // case 'name': 129 | // if(!state.name){ 130 | // error.name = 'Please provide a name' 131 | // } 132 | // break 133 | // case 'description': 134 | // if(!state.description){ 135 | // error.description = 'Please provide a description' 136 | // } 137 | // break 138 | // case 'price': 139 | // if(parseFloat(state.price) <= 0.0 || !state.price.toString().match(/^\d{1,}(\.\d{0,2})?$/)){ 140 | // error.price = 'Price must be a positive number' 141 | // } 142 | // break 143 | // case 'quantity': 144 | // if(parseInt(state.quantity, 10) <= 0 || !state.quantity.toString().match(/^\d{1,}$/)){ 145 | // error.quantity = 'Quantity must be a positive integer' 146 | // } 147 | // break 148 | // } 149 | // return error 150 | // } 151 | // 152 | // clearErrors = (name, value) => { 153 | // let errors = { ...this.state.errors } 154 | // 155 | // switch(name){ 156 | // case 'name': 157 | // if(value.length > 0){ 158 | // delete errors['name'] 159 | // } 160 | // break 161 | // case 'description': 162 | // if(value.length > 0){ 163 | // delete errors['description'] 164 | // } 165 | // break 166 | // case 'price': 167 | // if(parseFloat(value) > 0.0 || value.match(/^\d{1,}(\.\d{0,2})?$/)){ 168 | // delete errors['price'] 169 | // } 170 | // break 171 | // case 'quantity': 172 | // if(parseInt(value, 10) > 0 || value.match(/^\d{1,}$/)){ 173 | // delete errors['quantity'] 174 | // } 175 | // break 176 | // default: 177 | // } 178 | // this.setState({ errors }) 179 | // } 180 | // 181 | // handleBlur = (event) => { 182 | // const { name } = event.target 183 | // const fieldError = this.checkErrors(this.state, name) 184 | // const errors = Object.assign({}, this.state.errors, fieldError) 185 | // this.setState({ errors }) 186 | // } 187 | 188 | 189 | render(){ 190 | const buttonText = 'Update Product' 191 | const title = 'Editing Product' 192 | 193 | return ( 194 |
195 | {this.state.serverErrors.length > 0 && 196 | 197 | } 198 |
199 |
200 |
201 |

{ title }

202 | 209 |
210 |
211 |
212 |
213 | ) 214 | } 215 | } 216 | 217 | EditProductForm.propTypes = { 218 | onEdit: PropTypes.func.isRequired, 219 | onUpdate: PropTypes.func.isRequired 220 | } 221 | 222 | 223 | export default withProductForm(EditProductForm) 224 | -------------------------------------------------------------------------------- /app/javascript/containers/SignupFormContainer.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import PropTypes from 'prop-types' 3 | import { Redirect } from 'react-router-dom' 4 | import axios from 'axios' 5 | 6 | import Input from '../components/shared/Input' 7 | import Button from '../components/shared/Button' 8 | import SignupForm from '../components/shared/Form' 9 | import { EMAIL_REGEX } from '../shared/helpers' 10 | import ErrorMessages from '../components/shared/ErrorMessages' 11 | import {verifyAndSetFieldErrors} from '../shared/helpers' 12 | 13 | class Signup extends Component { 14 | state = { 15 | firstname: '', 16 | lastname: '', 17 | email: '', 18 | password: '', 19 | errors: {}, 20 | toHomePage: false, 21 | serverErrors: [], 22 | saved: false 23 | } 24 | 25 | componentDidUpdate = () => { 26 | if(this.state.saved){ 27 | this.setState({ 28 | firstname: '', 29 | lastname: '', 30 | email: '', 31 | password: '', 32 | toHomePage: true 33 | }) 34 | this.resetSaved() 35 | } 36 | } 37 | 38 | componentWillUnmount = () => { 39 | if(this.state.serverErrors.length > 0){ 40 | this.resetSaved() 41 | } 42 | } 43 | 44 | handleChange = (event) => { 45 | const { name, value} = event.target 46 | this.setState({ [name]: value }) 47 | this.clearErrors(name, value) 48 | } 49 | handleSubmit = (event) => { 50 | event.preventDefault() 51 | const fieldNames = ['firstname', 'lastname', 'email', 'password'] 52 | verifyAndSetFieldErrors(this, fieldNames) 53 | 54 | if(Object.keys(this.state.errors).length === 0){ 55 | const {firstname, lastname, email, password} = this.state 56 | const newUser = { 57 | user: { 58 | first_name: firstname, 59 | last_name: lastname, 60 | email, 61 | password 62 | } 63 | } 64 | this.handleSignup(newUser) 65 | } 66 | } 67 | 68 | 69 | handleSignup = (user) => { 70 | axios 71 | .post('/api/v1/users.json', user) 72 | .then(response => { 73 | this.setState({ 74 | serverErrors: [], 75 | saved: true 76 | }, () => { 77 | this.props.onFetchCurrentUser() 78 | }) 79 | }) 80 | .catch(error => { 81 | this.setState({ 82 | serverErrors: [...error.response.data] 83 | }) 84 | }) 85 | } 86 | handleBlur = (event) => { 87 | const { name } = event.target 88 | const fieldError = this.checkErrors(this.state, name) 89 | const errors = Object.assign({}, this.state.errors, fieldError) 90 | this.setState({ errors }) 91 | } 92 | checkErrors = (state, fieldName) => { 93 | const error = {} 94 | switch(fieldName) { 95 | case 'firstname': 96 | if(!state.firstname){ 97 | error.firstname = 'Please provide a first name' 98 | } 99 | break 100 | case 'lastname': 101 | if(!state.lastname){ 102 | error.lastname = 'Please provide a last name' 103 | } 104 | break 105 | case 'password': 106 | if(!state.password){ 107 | error.password = 'Please provide a password' 108 | } 109 | break 110 | case 'email': 111 |        if(!state.email || !EMAIL_REGEX.test(state.email)){ 112 | error.email = 'Please provide a valid email address' 113 | } 114 | break 115 | default: 116 | } 117 | return error 118 | } 119 | 120 | clearErrors = (name, value) => { 121 |     let errors = { ...this.state.errors } 122 |     switch (name) { 123 |       case 'firstname': 124 |         if (value.length > 0) { 125 |           delete errors['firstname'] 126 |         } 127 |         break 128 |       case 'lastname': 129 |         if (value.length > 0) { 130 |           delete errors['lastname'] 131 |         } 132 |         break 133 |       case 'email': 134 |         if (value.length > 0 && EMAIL_REGEX.test(this.state. email)) { 135 |           delete errors['email'] 136 |         } 137 |         break 138 |       case 'password': 139 |         if (value.length > 0) { 140 |           delete errors['password'] 141 |         } 142 |         break 143 |       default: 144 |     } 145 |     this.setState({ errors }) 146 |   } 147 | 148 | resetSaved = () => { 149 | this.setState({ 150 | saved: false, 151 | serverErrors: [] 152 | }) 153 | } 154 | 155 | render(){ 156 | if(this.state.toHomePage || this.props.currentUser){ 157 | return 158 | } 159 | 160 | return ( 161 |
162 |
163 | {this.state.serverErrors.length > 0 && } 164 |
165 |

Sign Up

166 | 167 | 178 | 179 | 190 | 191 | 202 | 203 | 214 | 215 | 216 | 217 | 218 | 219 |
220 |
221 |
222 | ) 223 | } 224 | } 225 | 226 | Signup.propTypes = { 227 | currentUser: PropTypes.object, 228 | onFetchCurrentUser: PropTypes.func.isRequired 229 | } 230 | 231 | export default Signup 232 | --------------------------------------------------------------------------------