Liz's Blog

12 in 12 Challenges #2:如何用Rails4做出部落格

| Comments

上週是〈如何利用Rails4打造出Reddit或Hacker News類型的網站〉,接著這週的挑戰是要做出部落格。

原影片請參考這裡,作者原GitHub,我的GitHub在此。

提示:此週並沒有像第一週都用scaffold做,但基本上都在複習MVC的概念,可參考Rails初探課程講解。

【建立新專案blog】
1.終端機:rails new blog
2.終端機:cd blog
3.在另一終端機頁面,並打開瀏覽器(localhost:3000)測試是否成功:rails s
4.終端機:git init
5.終端機:git status
6.終端機:git add .
7.終端機:git commit -am "Initial Commit"
8.終端機:rails g controller posts
9.首頁指向posts/index。

config/routes.rb
resources :posts
root 'posts#index'

10.在controller設定,連向index要指向哪裡。

app/controllers/posts_controller.rb
class PostsController < ApplicationController
  def index
  end
end

11.新增app/views/posts/index.html.erb檔案。

app/views/posts/index.html.erb
<h1>This is index.html.erb file ....</h1>

12.在controller設定,連向new要指向哪裡。

app/controllers/posts_controller.rb
....
  def new
  end
....

13.新增app/views/posts/new.html.erb檔案。

app/views/posts/new.html.erb
<h1>New Post</h1>

<%= form_for :post, url: posts_path do |f| %>
  <p>
    <%= f.label :title %></br>
    <%= f.text_field :title %>
  </p>
  <p>
    <%= f.label :body %></br>
    <%= f.text_field :body %>
  </p>
  <p>
    <%= f.submit %>
  </p>
<% end %>

14.終端機:rails g model Post title:string body:text
15.終端機:rake db:migrate
16.在controller設定,連向create要指向哪裡。

app/controllers/posts_controller.rb
....
def create
  @post = Post.new(post_params)
  @post.save

  redirect_to @post
end

private
  def post_params
    params.require(:post).permit(:title, :body)
  end
....

17.新增app/views/posts/show.html.erb檔案。

app/views/posts/show.html.erb
<h1 class="title">
  <%= @post.title %>
</h1>

<p class="date">
  Submitted <%= time_ago_in_words(@post.created_at) %> Ago
</p>

<p class="body">
  <%= @post.body %>
</p> 

18.在controller設定,連向show要指向哪裡。

app/controllers/posts_controller.rb
....
def show
  @post = Post.find(params[:id])
end

private
....

19.index裡的post要依照建立日期排列。

app/controllers/posts_controller.rb
....
def index
  @posts = Post.all.order('created_at DESC')
end
....

20.index的post要顯示名稱及日期。

app/views/posts/index.html.erb
<% @posts.each do |post| %>
  <div class="post_wrapper">
    <h2 class="title"><%= link_to post.title, post %></h2>
    <p class="date"><%= post.created_at.strftime("%B %d, %Y") %></p>
  </div>
<% end %> 

21.終端機:git status
22.終端機:git add .
23.終端機:git commit -am "Add Posts"

【版型改造】
1.終端機:git checkout -b styling
2.設定網頁版型,加入邊欄。

app/views/layouts/application.html.erb
....
<body>
  <div id="sidebar">
    <div id="logo">
      <%= link_to root_path do %>
        <%= image_tag "logo.svg" %>
      <% end %>
    </div>

    <ul>
      <li class="category">Website</li>
      <li><%= link_to "blog", root_path %></li>
      <li>About</li>
    </ul>

    <ul>
      <li class="category">Social</li>
      <li><a href="http://instagram.com/search.psop">Instagram</a></li>
      <li><a href="https://github.com/psop">Github</a></li>
      <li><a href="mailto:search.psop@gmailcom">Email</a></li>
    </ul>
  ....

3.加入logo.svg到app/assets/images/中。
4.新增_normalize.scss檔案。

app/assets/stylesheets/_normalize.scss
  //
// Reset
// Based on normalize.css v3.0.1
// git.io/normalize
// ------------------------------

html {
  font-family: sans-serif;
  -ms-text-size-adjust: 100%;
  -webkit-text-size-adjust: 100%;
}

body {
  margin: 0;
}

article,
aside,
details,
figcaption,
figure,
footer,
header,
hgroup,
main,
nav,
section,
summary {
  display: block;
}

audio,
canvas,
progress,
video {
  display: inline-block;
  vertical-align: baseline;
}

audio:not([controls]) {
  display: none;
  height: 0;
}

[hidden],
template {
  display: none;
}

a {
  background: transparent;
}

a:active,
a:hover {
  outline: 0;
}

abbr[title] {
  border-bottom: 1px dotted;
}

b,
strong {
  font-weight: bold;
}

dfn {
  font-style: italic;
}

h1 {
  font-size: 2em;
  margin: 0.67em 0;
}

mark {
  background: #ff0;
  color: #000;
}

small {
  font-size: 80%;
}

sub,
sup {
  font-size: 75%;
  line-height: 0;
  position: relative;
  vertical-align: baseline;
}

sup {
  top: -0.5em;
}

sub {
  bottom: -0.25em;
}

img {
  border: 0;
}

svg:not(:root) {
  overflow: hidden;
}

figure {
  margin: 1em 40px;
}

hr {
  -moz-box-sizing: content-box;
  box-sizing: content-box;
  height: 0;
}

pre {
  overflow: auto;
}

code,
kbd,
pre,
samp {
  font-family: monospace, monospace;
  font-size: 1em;
}

button,
input,
optgroup,
select,
textarea {
  color: inherit;
  font: inherit;
  margin: 0;
}

button {
  overflow: visible;
}

button,
select {
  text-transform: none;
}

button,
html input[type="button"],
input[type="reset"],
input[type="submit"] {
  -webkit-appearance: button;
  cursor: pointer;
}

button[disabled],
html input[disabled] {
  cursor: default;
}

button::-moz-focus-inner,
input::-moz-focus-inner {
  border: 0;
  padding: 0;
}

input {
  line-height: normal;
}

input[type="checkbox"],
input[type="radio"] {
  box-sizing: border-box;
  padding: 0;
}

input[type="number"]::-webkit-inner-spin-button,
input[type="number"]::-webkit-outer-spin-button {
  height: auto;
}

input[type="search"] {
  -webkit-appearance: textfield;
  -moz-box-sizing: content-box;
  -webkit-box-sizing: content-box;
  box-sizing: content-box;
}

input[type="search"]::-webkit-search-cancel-button,
input[type="search"]::-webkit-search-decoration {
  -webkit-appearance: none;
}

fieldset {
  border: 1px solid #c0c0c0;
  margin: 0 2px;
  padding: 0.35em 0.625em 0.75em;
}

legend {
  border: 0;
  padding: 0;
}

textarea {
  overflow: auto;
}

optgroup {
  font-weight: bold;
}

table {
  border-collapse: collapse;
  border-spacing: 0;
}

td,
th {
  padding: 0;
}

3.

app/assets/stylesheets/application.css.scss
....
@import "normalize";

4.將application.css改名為application.css.scss,並加入版型設計。

app/assets/stylesheets/application.css.scss
....
html, body {
    font-family: 'Raleway', sans-serif;
}

h1, h2, h3, h4, h5, h6 {
    font-weight: 500;
}

a {
    text-decoration: none;
    color: inherit;
}

#sidebar {
    width: 250px;
    position: fixed;
    left: 0;
    top: 0;
    height: 100%;
    background: #f5f7f9;
    padding: 7em 0 0 0;
    border-right: 1px solid #d6dce0;
    #logo {
        width: 40px;
        position: absolute;
        right: 3em;
        top: 3em;
    }
    ul {
        list-style: none;
        text-align: right;
        padding-right: 3em;
        .category {
            font-weight: 700;
            font-size: 0.7em;
            text-transform: uppercase;
            color: #33acb7;
        }
        li {
            padding: .5em 0;
            a {
                color: #9eafba;
                text-decoration: none;
                transition: all .4s ease;
                &:hover {
                    color: #33acb7;
                }
            }
        }
        .active {
            a {
                color: #33acb7;
            }
        }
    }
    .sign_in {
        position: absolute;
        right: 3em;
        top: 80%;
        font-size: .8em;
        color: #9eafba;
    }
}

.button {
    outline: none;
    background: transparent;
    border: 1px solid #d6dce0;
    padding: .5em 1.5em;
    border-radius: 1.5em;
    &:hover {
        border: 1px solid #33acb7;
        color: #33acb7;
        a {
            color: #33acb7 !important;
        }
    }
}

.clearfix:after {
   content: ".";
   visibility: hidden;
   display: block;
   height: 0;
   clear: both;
}

#main_content {
    margin-left: 250px;
    #header {
        padding: 1em 3em;
        border-bottom: 1px solid #d6dce0;
        background: #f5f7f9;
        color: #9eafba;
        p {
            display: inline;
        }
        a {
            color: #9eafba;
            text-decoration: none;
        }
        .buttons {
            float: right;
            margin-top: -6px;
            .button {
                font-size: .8em;
                margin-left: .5em;
            }
        }
    }
    .post_wrapper {
        padding: 3em;
        border-bottom: 1px solid #d6dce0;
        .title {
            margin: 0;
            a {
                font-weight: 500;
                text-decoration: none;
                color: #2a2f35;
                font-size: 1.5em;
                &:hover {
                    color: #33acb7;
                }
            }
        }
        .date_and_author {
            color: #9eafba;
            margin: .5em 0 0 0;
        }
    }
    #post_content {
        padding: 1em 3em;
        .title {
            font-weight: 500;
            text-decoration: none;
            color: #2a2f35;
            font-size: 2.5em;
            margin-bottom: 0;
        }
        .body {
            font-size: 1.1em;
            line-height: 1.75;
        }
        .date_and_author {
            color: #9eafba;
            margin: .5em 0 2em 0;
        }
        #comments {
            h2 {
                margin: 3em 0 1em 0;
                border-bottom: 1px solid #d6dce0;
                padding-bottom: 0.5em;
            }
            h3 {
                margin-top: 2em;
            }
            .comment {
                border-bottom: 1px solid #d6dce0;
                padding: 1.5em 2em;
                .clear_both {
                    clear: both;
                }
                &:after {
                    clear: both;
                }
                .comment_content {
                    float: left;
                    .comment_name {
                        margin: 1em 0 0 0;
                        font-size: 0.7em;
                        text-transform: uppercase;
                    }
                    .comment_body {
                        font-size: 1.2em;
                        margin: 0.2em 0 0 0;
                    }
                    .comment_time {
                        margin-top: 1.2em;
                        font-size: .8em;
                    }
                }
                .button {
                    float: right;
                }
            }
            input[type="text"], textarea {
                width: 50%;
            }
        }
    }
    #page_wrapper {
        padding: 3em;
        #profile_image {
            width: 300px;
            float: left;
            margin-right: 2em;
            img {
                width: 100%;
                border-radius: 0.35em;
            }
        }
        #content {
            h1 {
                font-weight: 500;
            }
            p {
                font-size: 1.1em;
                line-height: 1.75;
            }
            a {
                color: #33acb7;
                font-weight: 700;
                text-decoration: none;
            }
        }
    }
    .links {
        margin: 2em 0;
    }
    input[type="text"], input[type="email"], input[type="password"], textarea {
        width: 90%;
        border: 1px solid #d6dce0;
        border-radius: .35em;
        margin-top: 10px;
        padding: .5em 1em;
        line-height: 1.75;
    }
    input[type="text"] {
        height: 35px;
    }
    textarea {
        min-height: 180px;
    }
    input[type="submit"] {
        outline: none;
        background: transparent;
        border: 1px solid #d6dce0;
        padding: .5em 1.5em;
        font-size: 1.1em;
        border-radius: 1.5em;
        margin-left: .5em;
        &:hover {
            border: 1px solid #33acb7;
            color: #33acb7;
        }
    }
}

5.在application.html.erb加入邊欄等外觀顯示。

app/views/application.html.erb
<!DOCTYPE html>
<html>
<head>
  <title>Blog</title>
  <%= stylesheet_link_tag    'application', media: 'all', 'data-turbolinks-track' => true %>
  <%= stylesheet_link_tag 'application', 'http://fonts.googleapis.com/css?family=Raleway:400,700' %>
  <%= javascript_include_tag 'application', 'data-turbolinks-track' => true %>
  <%= csrf_meta_tags %>
</head>
<body>
  <div id="sidebar">
    <div id="logo">
      <%= link_to root_path do %>
        <%= image_tag "logo.svg" %>
      <% end %>
    </div>

    <ul>
      <li class="category">Website</li>
      <li><%= link_to "Blog", root_path %></li>
      <li>About</li>
    </ul>

    <ul>
      <li class="category">Social</li>
      <li><a href="http://instagram.com/searchpsop">Instagram</a></li>
      <li><a href="https://github.com/psop">Github</a></li>
      <li><a href="mailto:search.psop@gmail.com">Email</a></li>
    </ul>

    <p class="sign_in">Admin Login</p>
  </div>

  <div id="main_content">
    <div id="header">
      <p>All Posts</p>
        <div class="buttons">
          <button class="button"><%= link_to "New Post", new_post_path %></button>
          <button class="button">Log Out</button>
        </div>
    </div>

    <% flash.each do |name, msg| %>
      <%= content_tag(:div, msg, class: "alert") %>
    <% end %>

    <%= yield %>
  </div>
</body>
</html>

6.將new.html.erb套page_wrapper樣式。

app/views/posts/new.html.erb
<div id="page_wrapper">
  <h1>New Post</h1>

  <%= form_for :post, url: posts_path do |f| %>
    <p>
      <%= f.label :title %></br>
      <%= f.text_field :title %>
    </p>
    <p>
      <%= f.label :body %></br>
      <%= f.text_field :body %>
    </p>
    <p>
      <%= f.submit %>
    </p>
    <% end %>
</div>

7.將show.html.erb套入post_content樣式。

app/views/posts/show.html.erb
<div id="post_content">
  <h1 class="title">
    <%= @post.title %>
  </h1>

  <p class="date">
    Submitted <%= time_ago_in_words(@post.created_at) %> Ago
  </p>

  <p class="body">
    <%= @post.body %>
  </p>
</div> 

8.終端機:git status
9.終端機:git add .
10.終端機:git commit -am "Styling and structure"
11.終端機:git checkout master
12.終端機:git merge styling

【建立post】
1.post的title及body資料不可空白,且title要至少有五個字元。

app/models/post.rb
class Post < ActiveRecord::Base
  validates :title, presence: true, length: {minimum: 5}
  validates :body, presence: true
end

2.設定create動作,如果post成功儲存則會指向post目錄頁,否則則還是會停留在new。

app/controllers/posts_controller.rb
....
def new
  @post = Post.new
end

def create
  @post = Post.new(post_params)

  if @post.save
    redirect_to @post
  else
    render 'new'
  end
end
....

3.在new顯示頁面設定,如果新增post如果有違反post的title及body資料不可空白,且title要至少有五個字元,則不能儲存成功,則會顯示錯誤訊息。

app/views/posts/new.html.erb
....
<%= form_for :post, url: posts_path do |f| %>
  <% if @post.errors.any? %>
    <div id="errors">
      <h2><%= pluralize(@post.errors.count, "error") %> prevented this post from saving:</h2>
      <ul>
        <% @post.errors.full_messages.each do |msg| %>
          <li><%= msg %></li>
        <% end %>
      </ul>
    </div>
   <% end %>
....

4.終端機:git status
5.終端機:git add .
6.終端機:git commit -am "Add Validation

【建立新專案blog】
1.設定要如何執行edit及update。

app/controllers/posts_controller.rb
....
def edit
  @post = Post.find(params[:id])
end

def update
  @post = Post.find(params[:id])

  if @post.update(params[:post].permit(:title, :body))
    redirect_to @post
  else
    render 'edit'
  end
end

private
....

2.新增_form.html.erb,將app/views/posts/new.html.erb以下內容貼過來,因為Edit和new都要用到,用partial比較好。

app/views/posts/_form.html.erb
<%= form_for @post do |f| %>
    <% if @post.errors.any? %>
     <div id="errors">
       <h2><%= pluralize(@post.errors.count, "error") %> prevented this post from saving:</h2>
       <ul>
        <% @post.errors.full_messages.each do |msg| %>
          <li><%= msg %></li>
        <% end %>
       </ul>
     </div>
    <% end %>
    <p>
      <%= f.label :title %></br>
      <%= f.text_field :title %>
    </p>
    <p>
      <%= f.label :body %></br>
      <%= f.text_field :body %>
    </p>
    <p>
      <%= f.submit %>
    </p>
    <% end %>

3.將new.html.erb改成以下。

app/views/posts/new.html.erb
<div id="page_wrapper">
  <h1>New Post</h1>

  <%= render 'form' %>
</div>

4.新增edit.html.erb檔案。

app/views/posts/edit.html.erb
<div id="page_wrapper">
  <h1>Edit Post</h1>

  <%= render 'form' %>
</div>

5.將show新增可Edit按鍵。

app/views/posts/show.html.erb
....
<p class="date">
  Submitted <%= time_ago_in_words(@post.created_at) %> Ago
  | <%= link_to 'Edit', edit_post_path(@post) %> |
</p>
....

6.設定destroy要執行的動作。

app/controllers/posts_controller.rb
....
def destroy
  @post = Post.find(params[:id])
    @post.destroy
 
    redirect_to root_path
end

private
....

7.在show新增可Delete的按鍵。

app/views/posts/show.html.erb
....
<p class="date">
  Submitted <%= time_ago_in_words(@post.created_at) %> Ago
  | <%= link_to 'Edit', edit_post_path(@post) %> |
  <%= link_to 'Delete', post_path(@post), method: :delete, data: { confirm: 'Are you sure?' } %>
</p>
....

8.終端機:git status
9.終端機:git add .
10.終端機:git commit -am "Edit and Delete Posts

【新增Comment】
1.終端機:rails g model Comment name:string body:text post:references
2.終端機:rake db:migrate
3.設定post和comments的關聯性。

app/models/post.rb
class Post < ActiveRecord::Base
  has_many :comments
....
end

4.在routes.rb設定post和comment的巢狀結構。

config/routes.rb
Rails.application.routes.draw do
  resources :posts do
    resources :comments
  end
....

5.終端機:rake routes
6.終端機:rails g controller Comments
7.設定comment的create要執行的動作。

app/contollers/comments_controller.rb
....
def create
  @post = Post.find(params[:post_id])
  @comment = @post.comments.create(params[:comment].permit(:name, :body))

  redirect_to post_path(@post)
end
...

8.新增_comment.html.erb檔案。

app/views/comments/_comment.html.erb
<div class="comment clearfix">
  <div class="comment_content">
    <p class="comment_name"><strong><%= comment.name %></strong></p>
    <p class="comment_body"><%= comment.body %></p>
    <p class="comment_time"><%= time_ago_in_words(comment.created_at) %> Ago</p>
  </div>
</div>

9.新增_form.html.erb檔案。

app/views/comments/_form.html.erb
<%= form_for([@post, @post.comments.build]) do |f| %>
  <%= f.label :name %><br>
  <%= f.text_field :name %><br>
  <br>
  <%= f.label :body %><br>
  <%= f.text_area :body %><br>
  <br>
  <%= f.submit %>
<% end %> 

10.在show.html.erb設定顯示每篇post都有comment的數量及內容。

app/views/posts/show.html.erb
....
<p class="body">
  <%= @post.body %>
</p>

<div id="comments">
  <h2><%= @post.comments.count %> Comments</h2>
  <%= render @post.comments %>
 
  <h3>Add a comment:</h3>
  <%= render "comments/form" %>
</div>
....

11.設定刪除comment要執行的動作。

app/controllers/comments_controller.rb
....
def destroy
  @post = Post.find(params[:post_id])
  @comment = @post.comments.find(params[:id])
  @comment.destroy
 
  redirect_to post_path(@post)
end
....

12.設定刪除comment的方法。

app/views/comments/_comment.html.erb
....
<p><%= link_to 'Delete', [comment.post, comment],
                              method: :delete,
                              class: "button",
                            data: { confirm: 'Are you sure?' } %></p>
....

13.終端機:git status
14.終端機:git add .
15.終端機:git commint -am "Add comment"

【新增pages】
1.終端機:rails g controller pages
2.設定about頁面。

app/controllers/pages_controller.rb
class PagesController < ApplicationController
  def about
  end
end

3.在routes.rb設定要連到about頁面。

config/routes.rb
....
  root 'posts#index'

  get '/about', to: 'pages#about'
end

4.新增about.html.erb檔案。

app/views/pages/about.html.erb
<div id="page_wrapper">
  <div id="profile_image">
    <%= image_tag "profile.jpeg" %>
  </div>
 
  <div id="content">
    <h1>Hey, I'm Liz</h1>
      <p>Welcome to week 2 of my 12 Web Apps in 12 Weeks Challenge.</p>
      <p>This week I built a blog in Rails 4. You're actually on the demo application right now. Cool stuff, right!.</p>
  </div>
</div> 

5.修改首頁邊欄About連結。

app/views/layouts/application.html.erb
....
<ul>
  <li class="category">Website</li>
  <li><%= link_to "Blog", root_path %></li>
  <li><%= link_to "About", about_path %></li>
</ul>
....
<div id="main_content">
  <div id="header">
    <% if current_page?(root_path) %>
      <p>All Posts</p>
    <% elsif current_page?(about_path) %>
      <p>About</p>
    <% else %>
      <%= link_to "Back to All Posts", root_path %>
    <% end %>
....

6.終端機:git status
7.終端機:git add .
8.終端機:git commit -am "Add pages"

【新增user】
1.安裝devise。

Gemfile
gem 'devise', '~> 4.3'

2.終端機:bundle install
3.在另一終端機重啟:rails s
4.終端機:rails g devise:install
5.修改development.rb設定。

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

6.終端機:rails g devise:views
7.終端機:rails g devise user
8.終端機:rake db:migrate
9.在另一終端機重啟:rails s
10.在以下三個檔案最頭及最尾套入page_wrapper。
app/views/devise/registrations/new.html.erb
app/views/devise/sessions/new.html.erb
app/views/devise/registrations/edit.html.erb

<div id="page_wrapper">
</div>

11.除了index及show,其他動作皆須登入。

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

12.若有登入,則可看到New Post及Log Out的按鈕。

app/views/layouts/application.html.erb
....
<% if user_signed_in? %>
  <div class="buttons">
    <button class="button"><%= link_to "New Post", new_post_path %></button>
    <button class="button">Log Out</button>
  </div>
<% end %>
....

13.若有登入,則可看到Edit及Delete的按鈕。

app/views/posts/show.html.erb
....
<p class="date">
  Submitted <%= time_ago_in_words(@post.created_at) %> Ago
  <% if user_signed_in? %>
  | <%= link_to 'Edit', edit_post_path(@post) %> |
  <%= link_to 'Delete', post_path(@post), method: :delete, data: { confirm: 'Are you sure?' } %>
  <% end %>
  </p>
....

14.若有登入,則可看到Delete的按鈕。

app/views/comments/_comment.html.erb
<% if user_signed_in? %>
  <p><%= link_to 'Delete', [comment.post, comment],
                         method: :delete,
                         class: "button",
                         data: { confirm: 'Are you sure?' } %></p>
<% end %>
....

15.終端機:git status
16.終端機:git add .
17.終端機:git commit -am "Add Users"

【延伸閱讀】
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