Liz's Blog

12 in 12 challenges #3:如何用Rails4打造一個食譜網站

| Comments

第三週除了之前已經練習過的Devise、Bootstrap、simple_form,還加入了haml、paperclip、cocoon等gem。

原則都是在練習MVC,請務必配合原作者Mackenzie Child影片Github使用。以下則是針對gem版本作些微調整,可參考我的Github

【開啟新專案recipe_box】
1.終端機:rails new recipe_box
2.終端機:cd recipe_box
3.終端機:git init
4.終端機:git add .
5.終端機:git commit -am “Initial Commit”
6.開啟另一視窗終端機:rails s

【打造recipe頁面,可供瀏覽、新增、修改、刪除】
1.安裝haml

Gemfile
gem 'haml', '~> 4.0', '>= 4.0.7'

2.終端機:bundle install
3.重啟另一視窗終端機:rails s
4.終端機:rails g controller recipes
5.新增recipes路由,首頁指向recipes#index。

config/routes.rb
resources :recipes

root “recipes#index”

6.設定index後該執行的動作。

app/controllers/recipes_controller.rb
def index
end

7.新增index.html.haml檔案。

app/views/recipes/index.html.haml
%h1 This is the placeholder for the Recipes#Index

8.設定show、new、create後該執行的動作,並且設定只有show、edit、update、destroy需要尋找recipe。

app/controllers/recipes_controller.rb
before_action :find_recipe, only: [:show, :edit, :update, :destroy]

def index
end

def show
end

def new
  @recipe = Recipe.new
end

def create
  @recipe = Recipe.new(recipe_params)
end

private

def recipe_params
  params.require(:recipe).permit(:title, :description)
end

def find_recipe
  @recipe = Recipe.find(params[:id])
end

9.終端機:rails g model Recipe title:string description:text user_id:integer
10.終端機:rake db:migrate
11.安裝simple_form

Gemfile
gem 'simple_form', '~> 3.4'

12.終端機:bundle install
13.終端機:rails g simple_form:install
14.終端機:rails g simple_form:install --bootstrap
15.重啟另一視窗終端機:rails s
16.新增_form.html.haml檔案,使用partial,因為好幾個檔案需要用到。

app/views/recipes/_form.html.haml
= simple_form_for @recipe, html: { multipart: true } do |f|
  - if @recipe.errors.any?
    #errors
      %p
        = @recipe.errors.count
        Prevented this recipe from saving
      %ul
        - @recipe.errors.full_messages.each do |msg|
          %li= msg
  .panel-body
    = f.input :title, input_html: { class: 'form-control' }
    = f.input :description, input_html: { class: 'form-control' }
  = f.button :submit, class: "btn btn-primary"

17.新增new.html.haml檔案,新增食譜用。

app/views/recipes/new.html.haml
%h1 New Recipe

= render 'form'

%br/

= link_to "Back", root_path, class: "btn btn-default"

18.如果食譜新增成功,出現Successfully created new recipe字樣。

app/controllers/recipes_controller.rb
def create
  @recipe = Recipe.new(recipe_params)

  if @recipe.save
    redirect_to @recipe, notice: "Successfully created new recipe"
  else
    render 'new'
  end
end

19.新增show.html.haml檔案,作為瀏覽食譜用。

app/views/recipes/show.html.haml
%h1= @recipe.title
%p= @recipe.description

= link_to "Back", root_path, class: "btn btn-default"

20.食譜首頁要列出所有食譜,並且依照時間排列。

app/controllers/recipes_controller.rb
def index
  @recipe = Recipe.all.order("created_at DESC")
end
….

21.食譜首頁顯示所有食譜名稱及連結。

app/views/recipes/index.html.haml
- @recipe.each do |recipe|
  %h2= link_to recipe.title, recipe

22.設定edit、update、destroy後該執行的動作。

app/controllers/recipes_controller.rb
….
def edit
end

def update
  if @recipe.update(recipe_params)
    redirect_to @recipe
  else
    render 'edit'
  end
end

def destroy
  @recipe.destroy
  redirect_to root_path, notice: "Successfully deleted recipe"
end

private
….

23.新增edit.html.haml頁面。

app/views/recipes/edit.html.haml
%h1 Edit Recipe

= render 'form'

24.在show頁面,新增Back、Edit、Delete按鈕。

app/views/recipes/show.html.haml
….
= link_to "Back", root_path, class: "btn btn-default"
= link_to "Edit", edit_recipe_path, class: "btn btn-default"
= link_to "Delete", recipe_path, method: :delete, data: {confirm: "Are you sure?" }, class: "btn btn-default"

25.詳細安裝參考此篇

Gemfile
gem 'bootstrap-sass', '~> 3.3', '>= 3.3.7'

26.終端機:bundle install
27.重啟另一視窗終端機:rails s
28.改成haml檔案。

app/views/application.html.haml
!!! 5
%html
%head
  %title Recipe App
  = stylesheet_link_tag    'application', media: 'all', 'data-turbolinks-track' => true
  = javascript_include_tag 'application', 'data-turbolinks-track' => true
  = csrf_meta_tags

%body
  %nav.navbar.navbar-default
    .container
      .navbar-brand= link_to "Recipe Box", root_path

      %ul.nav.navbar-nav.navbar-right
        %li= link_to "New Recipe", new_recipe_path
        %li= link_to "Sign Out", root_path

  .container
    - flash.each do |name, msg|
      = content_tag :div, msg, class: "alert"
    = yield

29.安裝paperclip

Gemfile
gem 'paperclip', '~> 5.1'

30.終端機:bundle install
31.重啟另一視窗終端機:rails s
32.依照paperclip指示修改。

app/models/recipe.rb
class Recipe < ActiveRecord::Base
  has_attached_file :image, styles: { :medium => "400x400#" }
  validates_attachment_content_type :image, content_type: /\Aimage\/.*\Z/
end

33.終端機:rails g paperclip recipe image
34.終端機:rake db:migrate
35.加入可插入image的欄位。

app/views/recipes/_form.html.haml
….
= f.input :description, input_html: { class: 'form-control' }
= f.input :image, input_html: { class: 'form-control' }
….

36.controller允許插入image。

app/controllers/recipes_controller.rb
….
def recipe_params
  @params.require(:recipe).permit(:title, :description, :image)
end
….

37.show頁面加入image。

app/views/recipes/show.html.haml
= image_tag @recipe.image.url(:medium, class: "recipe_image”)
….

38.index頁面加入image。

app/views/recipes/index.html.haml
- @recipe.each do |recipe|
  = link_to recipe do
    = image_tag recipe.image.url(:medium)
  %h2= link_to recipe.title, recipe

39.index顯示食譜呈現三等份。

app/views/recipes/index.html.haml
- @recipe.each_slice(3) do |recipes|
    .row
        - recipes.each do |recipe|
            .col-md-4
                .recipe
                    .image_wrapper
                        = link_to recipe do
                            = image_tag recipe.image.url(:medium)
                    %h2= link_to recipe.title, recipe

40.application.css.scss套入版型。

app/stylesheets/application.css.scss
….
@mixin box-shadow {
    -webkit-box-shadow: rgba(0, 0, 0, 0.09) 0 2px 0;
    -moz-box-shadow: rgba(0, 0, 0, 0.09) 0 2px 0;
    box-shadow: rgba(0, 0, 0, 0.09) 0 2px 0;
}

$red: #DB6161;

body {
    background: rgb(235, 238, 243);
}

.main_content {
    padding: 0 0 50px 0;
}

.alert {
    padding: 15px;
    margin-bottom: 20px;
    border: 1px solid transparent;
    border-radius: 5px;
    @include box-shadow;
    background: white;
    font-weight: bold;
}

.navbar {
    margin-bottom: 50px;
    @include box-shadow;
    border: none;
    .navbar-brand {
        text-transform: uppercase;
        letter-spacing: -1px;
        font-weight: bold;
        font-size: 25px;
        a {
            color: $red;
        }
    }
}

.recipe {
    background: white;
    border-radius: 5px;
    margin-bottom: 40px;
    @include box-shadow;
    .image_wrapper {
        max-width: 100%;
        border-radius: 5px 5px 0 0;
        overflow: hidden;
    }
    img {
        width: 100%;
        -webkit-transition: all .3s ease-out;
      -moz-transition: all .3s ease-out;
      -o-transition: all .3s ease-out;
    transition: all .3s ease-out;
        &:hover {
            transform: scale(1.075);
        }
    }
    h2 {
        padding: 15px 5%;
        margin: 0;
        font-size: 20px;
        font-weight: normal;
        line-height: 1.5;
        a {
            color: $red;
        }
    }
}

#recipe_top {
    margin-bottom: 50px;
}

#recipe_info, #ingredients, #directions {
    background: white;
    @include box-shadow;
    min-height: 360px;
    border-radius: 5px;
    padding: 2em 8%;
}

.recipe_image {
    max-width: 100%;
    border-radius: 5px;
    @include box-shadow;
}

#recipe_info {
    h1 {
        font-size: 36px;
        font-weight: normal;
        color: $red;
    }
    .description {
        color: #8A8A8A;
        font-size: 20px;
    }
}

#ingredients, #directions {
    margin-bottom: 50px;
    ul, ol {
        padding-left: 18px;
        li {
            padding: 1em 0;
            border-bottom: 1px solid #EAEAEA;
        }
    }
}

.form-inline {
    margin-top: 15px;
}
.form-input {
    width: 65% !important;
    float: left;
}
.form-button {
    float: left;
    width: 30% !important;
    margin-left: 5%;
}
.add-button {
    margin-top: 25px;
}

41.安裝cocoon

Gemfile
gem 'cocoon', '~> 1.2', '>= 1.2.9'

42.終端機:bundle install
43.重啟另一視窗終端機:rails s
44.在jquery_ujs以下加入//= require cocoon。

app/javascripts/application.js
//= require cocoon

45.終端機:rails g model Ingredient name:string recipe:belongs_to
46.終端機:rails g model Direction step:text recipe:belongs_to
47.終端機:rake db:migrate
48.依照cocoon指示,加入ingredients和directions複合型結構。

app/models/recipe.rb
….
has_many :ingredients
has_many :directions

accepts_nested_attributes_for :ingredients,
                           reject_if: proc { |attributes| attributes['name'].blank? },
                           allow_destroy: true
accepts_nested_attributes_for :directions,
                           reject_if: proc { |attributes| attributes['step'].blank? },
                           allow_destroy: true
validates :title, :description, :image, presence: true
….

49.允許可以加入ingredients和directions。

app/controllers/recipes_controller.rb
….
def recipe_params
  params.require(:recipe).permit(:title, :description, :image, ingredients_attributes: [:id, :name, :_destroy], directions_attributes: [:id, :step, :_destroy])
end
….

50.加入ingredients欄位。

app/views/recipes/_form.html.haml
….
= f.input :image, input_html: { class: 'form-control' }

.row
  .col-md-6
    %h3 Ingredients
    #ingredients
      = f.simple_fields_for :ingredients do |ingredient|
        = render 'ingredient_fields', f: ingredient
      .links
        = link_to_add_association 'Add Ingredient', f, :ingredients, class: "btn btn-default add-button"
….

51.新增_ingredient_fields.html.haml檔案。

app/views/recipes/_ingredient_fields.html.haml
.form-inline.clearfix
    .nested-fields
        = f.input :name, input_html: { class: 'form-input form-control' }
        = link_to_remove_association "Remove", f, class: "form-button btn btn-default"

52.加入directions欄位。

app/views/recipes/_form.html.haml
….
.row
  .col-md-6
    %h3 Ingredients
    #ingredients
      = f.simple_fields_for :ingredients do |ingredient|
          = render 'ingredient_fields', f: ingredient
      .links
        = link_to_add_association 'Add Ingredient', f, :ingredients, class: "btn btn-default add-button"

  .col-md-6
    %h3 Directions
    #directions
      = f.simple_fields_for :xdirections do |direction|
        = render 'direction_fields', f: direction
      .links
        = link_to_add_association 'Add Step', f, :directions, class: "btn btn-default add-button"
…

53.新增_direction_fields.html.haml檔案。

app/views/recipes/_direction_fields.html.haml
.form-inline.clearfix
  .nested-fields
    = f.input :step, input_html: { class: 'form-input form-control' }
    = link_to_remove_association "Remove Step", f, class: "btn btn-default form-button"

54.show.html.haml套入版型設計。

app/views/recipes/show.html.haml
.main_content
  #recipe_top.row
    .col-md-4
      = image_tag @recipe.image.url(:medium), class: "recipe_image"
    .col-md-8
      #recipe_info
        %h1= @recipe.title
        %p.description= @recipe.description

  .row
    .col-md-6
      #ingredients
        %h2 Ingredients
        %ul
          - @recipe.ingredients.each do |ingredient|
            %li= ingredient.name

    .col-md-6
      #directions
        %h2 Directions
        %ul
          - @recipe.directions.each do |direction|
            %li= direction.step

  .row
    .col-md-12
      = link_to "Back", root_path, class: "btn btn-default"
      = link_to "Edit", edit_recipe_path, class: "btn btn-default"
      = link_to "Delete", recipe_path, method: :delete, data: {confirm: "Are you sure?" }, class: "btn btn-default"

55.終端機:git status
56.終端機:git add .
57.終端機:git commit -am “Added Recipes with ingredients and directions in a nest form”

【加入會員系統】
1.安裝devise。

Gemifle
gem 'devise', '~> 4.3'

2.終端機:bundle install
3.終端機:rails g devise:install
4.修改development.rb環境。

config/environments/development.rb
….
config.action_mailer.default_url_options = { host: 'localhost', port: 3000 }

5.終端機:rails g devise:views
6.終端機:rails g devise User
7.終端機:rake db:migrate
8.user有許多recipes。

app/models/user.rb
….
  has_many :recipes
end

9.recipe屬於user。

app/models/recipe.rb
class Recipe < ActiveRecord::Base
  belongs_to :user
….

10.瀏覽器開啟localhost:3000/users/sign_up,並註冊會員。
11.修改new、create,新增食譜時會紀錄user。

app/controllers/recipes_controller.rb
….
def new
  @recipe = current_user.recipes.build
end

def create
  @recipe = current_user.recipes.build(recipe_params)

  if @recipe.save
    redirect_to @recipe, notice: "Successfully created new recipe"
  else
    render 'new'
  end
end
….

12.終端機:rails c
13.終端機:@recipe = Recipe.new
14.終端機:@recipe = current_user.recipes.build
15.在瀏覽器新增一個食譜。
16.終端機:@recipe = Recipe.last
17.在show.html.haml新增由誰Submitted。

app/views/recipes/show.html.haml
….
.col-md-8
  #recipe_info
    %h1= @recipe.title
    %p.description= @recipe.description
    %p
      Submitted by
      = @recipe.user.email
….

18.終端機:@user = User.first
19.終端機:@recipe = Recipe.first
20.終端機:@recipe.user = @user
21.終端機:@recipe.save
22.終端機:@recipe = Recipe.find(8)
23.終端機:@recipe.user = @user
24.終端機:@recipe.save
25.有登入的使用者才能新增及修改食譜。

app/controllers/recipes_controller.rb
….
before_action :authenticate_user!, except: [:index, :show]
….

26.若會員登入後,則顯示New Recipe和Sign Out。沒有登入,則顯示Sign Up和Sign In。

app/views/layouts/application.html.haml
!!! 5
%html
%head
  %title Recipe App
  = stylesheet_link_tag    'application', media: 'all', 'data-turbolinks-track' => true
  = javascript_include_tag 'application', 'data-turbolinks-track' => true
  = csrf_meta_tags

%body
  %nav.navbar.navbar-default
    .container
      .navbar-brand= link_to "Recipe Box", root_path

      - if user_signed_in?
        %ul.nav.navbar-nav.navbar-right
          %li= link_to "New Recipe", new_recipe_path
          %li= link_to "Sign Out", root_path
      - else
        %ul.nav.navbar-nav.navbar-right
          %li= link_to "Sign Up", new_user_registration_path
          %li= link_to "Sign In", new_user_session_path

  .container
    - flash.each do |name, msg|
      = content_tag :div, msg, class: "alert"

    = yield

27.若登入會員後,顯示Edit和Delete按鈕。

app/views/recipes/show.html.haml
.row
  .col-md-12
    = link_to "Back", root_path, class: "btn btn-default"
    - if user_signed_in?
      = link_to "Edit", edit_recipe_path, class: "btn btn-default"
      = link_to "Delete", recipe_path, method: :delete, data: {confirm: "Are you sure?" }, class: "btn btn-default"

28.要真的可以Sign Out。

app/views/layouts/application.html.haml
!!! 5
%html
%head
  %title Recipe App
  = stylesheet_link_tag    'application', media: 'all', 'data-turbolinks-track' => true
  = javascript_include_tag 'application', 'data-turbolinks-track' => true
  = csrf_meta_tags

%body
  %nav.navbar.navbar-default
    .container
      .navbar-brand= link_to "Recipe Box", root_path

      - if user_signed_in?
        %ul.nav.navbar-nav.navbar-right
          %li= link_to "New Recipe", new_recipe_path
          %li= link_to "Sign Out", destroy_user_session_path, method: :delete
      - else
        %ul.nav.navbar-nav.navbar-right
          %li= link_to "Sign Up", new_user_registration_path
          %li= link_to "Sign In", new_user_session_path

  .container
    - flash.each do |name, msg|
      = content_tag :div, msg, class: "alert"

    = yield

29.Sign in頁面套版。

app/views/devise/sessions/new.html.erb
<div class="row">
  <div class="col-md-6 col-md-offset-3">
    <h2>Sign in</h2>

    <%= simple_form_for(resource, as: resource_name, url: session_path(resource_name)) do |f| %>
      <div class="form-inputs">
        <%= f.input :email, required: false, autofocus: true, input_html: { class: 'form-control' } %>
          <%= f.input :password, required: false, input_html: { class: 'form-control' } %>
      <%= f.input :remember_me, as: :boolean, input_html: { class: 'form-control' } if devise_mapping.rememberable? %>
      </div>

      <div class="form-actions">
        <%= f.button :submit, "Sign in", class: 'btn btn-primary' %>
      </div>
      <br>
    <% end %>

    <%= render "devise/shared/links" %>
  </div>
</div>

30.Sign up頁面套版。

app/views/devise/registrations/new.html.erb
<div class="row">
  <div class="col-md-6 col-md-offset-3">
    <h2>Sign up</h2>

    <%= simple_form_for(resource, as: resource_name, url: registration_path(resource_name)) do |f| %>
      <%= f.error_notification %>

      <div class="form-inputs">
        <%= f.input :email, required: true, autofocus: true, input_html: { class: 'form-control' } %>
        <%= f.input :password, required: true, input_html: { class: 'form-control' } %>
        <%= f.input :password_confirmation, required: true, input_html: { class: 'form-control' } %>
      </div>
        <br>
      <div class="form-actions">
        <%= f.button :submit, "Sign up", class: "btn btn-primary" %>
      </div>
      <br>
    <% end %>

    <%= render "devise/shared/links" %>
  </div>
</div>

31.終端機:git status
32.終端機:git add .
33.終端機:git commit -am “Add users and finish styles”

【延伸閱讀】
1.12 in 12 Challenges #1:如何利用Rails4打造出Reddit或Hacker News類型的網站
2.12 in 12 Challenges #2:如何用Rails4做出部落格
3.12 in 12 challenges #3:如何用Rails4打造一個食譜網站
4.12 in 12 challenges #4:如何用Rails4打造Pinterest
5.12 in 12 challenges #5:如何用Rails4打造電影評論網
6.12 in 12 challenges #6:如何用Rails4打造待做清單
7.12 in 12 Challenges #7:如何利用Rails4打造出求職網站
8.12 in 12 Challenges #8:如何用Rails4做出健身紀錄
9.12 in 12 Challenges #9:如何用Rails4做出維基百科
10.12 in 12 Challenges #10:如何用Rails4做出論壇
11.12 in 12 Challenges #11:如何用Rails4做出Notebook
12.12 in 12 Challenges #12:如何用Rails4做出Dribbble

Comments

comments powered by Disqus