はじめに
本 Chapter では、顧客側の商品に関する実装を行います。
顧客側の実装になりますので、管理者が登録した商品を閲覧する機能のみを実装していきます。
コントローラを作成しよう
本 Chapter では、いきなりコントローラの作成から入ります。
モデルやテーブルの作成は不要です。なぜなら、前 Chapter で作成済みだからです。
コンテナが立ち上がっていることが確認できましたら、ecommerce_web コンテナに入ります。
その後、下記コマンドを実行して、必要なコントローラを作成しましょう。
$ rails g controller customer/products index show --no-helper
今回 customer ディレクトリ配下に ProductsController を作成するために、customer/products としています。前 Chapter でも、ProductsController を作成しましたが、異なる名前空間に属しているため、両者が競合することはありません。
続いて、ルーティングの設定を行います。
config/routes.rb
を開いて下記コードを追加してください。
Rails.application.routes.draw do
devise_for :admins, controllers: {
sessions: 'admin/sessions'
}
devise_for :customers, controllers: {
sessions: 'customer/sessions',
registrations: 'customer/registrations'
}
root to: 'pages#home'
namespace :admin do
resources :products, only: %i[index show new create edit update]
end
+ scope module: :customer do
+ resources :products, only: %i[index show]
+ end
get '/up/', to: 'up#index', as: :up
get '/up/databases', to: 'up#databases', as: :up_databases
end
上記のように
scope module: :customer do
end
で囲むと、ファイル構成は「customer/」となりますが、URL は「customer/」となりません。
少しわかりずらいので、生成されたパスを確認してみましょう。
$ rails routes | grep products | grep customer
下記のように、URL や名前付きパスには「customer」は付与されていません。しかし、呼び出されるコントローラは「customer」配下のコントローラが指定されています。
products GET /products(.:format) customer/products#index
product GET /products/:id(.:format) customer/products#show
なぜこのようなパスにしたかというと、管理者側の URL に「admin/」をつけているため、顧客側の URL に「customer/」をつけなくても両者を区別できるためです。
それでは次に、コントローラの各アクションの中身を記述していきます。
app/controllers/customer/products_controller.rb
を開いて下記コードを記述してください。
class Customer::ProductsController < ApplicationController
def index
@products, @sort = get_products(params)
end
def show
@product = Product.find(params[:id])
end
private
def get_products(params)
return Product.all, 'default' unless params[:latest] || params[:price_high_to_low] || params[:price_low_to_high]
return Product.latest, 'latest' if params[:latest]
return Product.price_high_to_low, 'price_high_to_low' if params[:price_high_to_low]
return Product.price_low_to_high, 'price_low_to_high' if params[:price_low_to_high]
end
end
show アクションについては、パラメータの id をもとに product を一件取得するという一般的な処理が記述されています。
index アクションについては、ソート機能を見越してパラメータの値によって product の取得方法を変化させています。
例えば、http://localhost:8000/products?latest=true という URL にアクセスした場合、params[:latest]
が true
となりますので、get_products
メソッド内の二つ目の条件に合致します。すると、Product.latest
が実行され、作成された順に product を取得することができます。
他の条件も同様です。
ちなみに @sort
というインスタンス変数は、現在適用されているソートの種類を識別するために使います。
それでは、
-
latest
スコープ -
price_high_to_low
スコープ -
price_low_to_high
スコープ
を定義しましょう。
latest
というスコープに関しては、全てのモデルについて使用するものですので、app/models/application_record.rb
に記述しましょう。
class ApplicationRecord < ActiveRecord::Base
primary_abstract_class
+ scope :latest, -> { order(created_at: :desc) }
end
ここで登場した scope
とは、クラスメソッドを使う際、可読性を保つために使います。
モデル名.latest
とすると、created_at の値を 降順 で並び替えてくれます。
次に、price_high_to_low
と price_low_to_high
というスコープを定義しましょう。app/models/product.rb
を開いて下記コードを記述してください。
class Product < ApplicationRecord
with_options presence: true do
validates :name
validates :description
validates :price
validates :stock
validates :image
end
has_one_attached :image
+ scope :price_high_to_low, -> { order(price: :desc) }
+ scope :price_low_to_high, -> { order(price: :asc) }
end
price_high_to_low
は価格を降順に、price_low_to_high
は価格を昇順にして product を取得することができます。
これで、http://localhost:8000/products にアクセスするときに、パラメータの値によって、ソートの仕方を変えることができます。
ビューを作成しよう
それでは、最後に Tailwind CSS を使ってビューを作成していきましょう。
まずは、商品の一覧画面です。app/views/customer/products/index.html.erb
を開いて下記コードを記述してください。
<div class="mb-8 text-center">
<span class="text-3xl font-bold">
Products
</span>
</div>
<div class="flex flex-wrap justify-center md:justify-start px-5">
<div class="mb-3 w-fit text-md font-medium mr-2 px-4 py-1.5 rounded <%= @sort === "latest" ? "bg-gray-800 text-gray-100" : "bg-gray-100 text-gray-800" %>">
<%= link_to 'Latest', products_path(latest: true) %>
</div>
<div class="mb-3 w-fit text-md font-medium mr-2 px-4 py-1.5 rounded <%= @sort === "price_high_to_low" ? "bg-gray-800 text-gray-100" : "bg-gray-100 text-gray-800" %>">
<%= link_to 'Price: High to Low', products_path(price_high_to_low: true) %>
</div>
<div class="mb-3 w-fit text-md font-medium mr-2 px-4 py-1.5 rounded <%= @sort === "price_low_to_high" ? "bg-gray-800 text-gray-100" : "bg-gray-100 text-gray-800" %>">
<%= link_to 'Price: Low to High', products_path(price_low_to_high: true) %>
</div>
</div>
<div class='flex flex-wrap justify-center'>
<% @products.each do |product| %>
<div class="w-full max-w-sm rounded-lg xl:w-4/12 md:w-6/12">
<%= link_to product_path(product) do %>
<%= image_tag product.image, class: "p-5 object-cover" %>
<div class="px-5 pb-5">
<p class="text-3xl font-semibold tracking-tight text-gray-900 mb-4"><%= product.name %></p>
<div class="flex justify-between items-center">
<span class="text-3xl font-bold text-gray-900"><%= number_to_currency(product.price, unit: "¥", strip_insignificant_zeros: true) %></span>
<% if product.stock > 0 %>
<span class="bg-blue-100 text-blue-800 text-xs font-semibold p-2 rounded">In stock</span>
<% else %>
<span class="bg-red-100 text-red-800 text-xs font-semibold p-2 rounded">Out of stock</span>
<% end %>
</div>
</div>
<% end %>
</div>
<% end %>
</div>
下記の三つのリンクはソート用のリンクになります。
URL にそれぞれ異なるパラメータを付与しています。
<div class="mb-3 w-fit text-md font-medium mr-2 px-4 py-1.5 rounded <%= @sort === "latest" ? "bg-gray-800 text-gray-100" : "bg-gray-100 text-gray-800" %>">
<%= link_to 'Latest', products_path(latest: true) %>
</div>
<div class="mb-3 w-fit text-md font-medium mr-2 px-4 py-1.5 rounded <%= @sort === "price_high_to_low" ? "bg-gray-800 text-gray-100" : "bg-gray-100 text-gray-800" %>">
<%= link_to 'Price: High to Low', products_path(price_high_to_low: true) %>
</div>
<div class="mb-3 w-fit text-md font-medium mr-2 px-4 py-1.5 rounded <%= @sort === "price_low_to_high" ? "bg-gray-800 text-gray-100" : "bg-gray-100 text-gray-800" %>">
<%= link_to 'Price: Low to High', products_path(price_low_to_high: true) %>
</div>
ビューの記述が完了しましたら、http://localhost:8000/products にアクセスして商品一覧画面を表示してみましょう。顧客の商品に関するページは特にアクセス制限を施していないため、ログインしていなくてもアクセスできるはずです。
下記のように商品の一覧が表示されていれば OK です。
試しにソートボタンをクリックしてみましょう。
「Price: High to Low」ボタンをクリックすると、価格が安い順に商品が並びます。
続いて、商品の詳細画面を作成しましょう。app/views/customer/products/show.html.erb
を開いて、下記コードを記述してください。
<div class="relative mx-auto max-w-screen-xl p-6">
<div class="grid grid-cols-1 items-start gap-9 md:grid-cols-2">
<div>
<%= image_tag @product.image, class: "aspect-square w-full rounded-xl object-cover" %>
</div>
<div class="sticky top-0">
<div class="flex flex-col justify-between">
<div class="flex justify-between mb-6">
<div class="max-w-[35ch]">
<h1 class="text-2xl font-bold">
<%= @product.name %>
</h1>
</div>
<p class="text-2xl font-bold"><%= number_to_currency(@product.price, unit: "¥", strip_insignificant_zeros: true) %></p>
</div>
<div class="mb-3">
<p>
<%= @product.description %>
</p>
</div>
<div class="mb-8">
<% if @product.stock > 0 %>
<span class="bg-blue-100 text-blue-800 text-xs font-semibold p-2 rounded">In stock (<%= @product.stock %>)</span>
<% else %>
<span class="bg-red-100 text-red-800 text-xs font-semibold p-2 rounded">Out of stock</span>
<% end %>
</div>
</div>
</div>
</div>
</div>
http://localhost:8000/products/1 にアクセスして下記のような画面が表示されていれば OK です。id は存在する product の id を指定してください。
商品に関するビューが完成しましたので、商品一覧画面へのリンクをナビゲーションバーに追加してあげましょう。
app/views/layouts/application.html.erb
を開いて、<% elsif customer_signed_in? %>
内に、下記コードを追加してください。
<% elsif customer_signed_in? %>
<li class='text-white no-underline'>
<%= link_to products_path, class: "p-4 relative border-2 border-transparent rounded-full focus:outline-none focus:text-gray-500 transition duration-150 ease-in-out flex" do %>
<svg width="24" height="24" xmlns="http://www.w3.org/2000/svg" fill-rule="evenodd" clip-rule="evenodd">
<path fill="white" d="M11.5 23l-8.5-4.535v-3.953l5.4 3.122 3.1-3.406v8.772zm1-.001v-8.806l3.162 3.343 5.338-2.958v3.887l-8.5 4.534zm-10.339-10.125l-2.161-1.244 3-3.302-3-2.823 8.718-4.505 3.215 2.385 3.325-2.385 8.742 4.561-2.995 2.771 2.995 3.443-2.242 1.241v-.001l-5.903 3.27-3.348-3.541 7.416-3.962-7.922-4.372-7.923 4.372 7.422 3.937v.024l-3.297 3.622-5.203-3.008-.16-.092-.679-.393v.002z"/>
</svg>
<% end %>
</li>
<li class='no-underline'>
<div class="flex items-center justify-center">
<div class="relative inline-block text-left dropdown">
<button class="inline-flex justify-center w-full px-4 py-2 text-sm font-medium leading-5 text-gray-700 transition duration-150 ease-in-out rounded-md" type="button" aria-haspopup="true" aria-expanded="true" aria-controls="headlessui-menu-items-117">
<svg class="h-6 w-6" id="dropdownDividerButton" data-dropdown-toggle="dropdownDivider" viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg">
<path fill="white" d="M288 320a224 224 0 1 0 448 0 224 224 0 1 0-448 0zm544 608H160a32 32 0 0 1-32-32v-96a160 160 0 0 1 160-160h448a160 160 0 0 1 160 160v96a32 32 0 0 1-32 32z">
</path>
</svg>
</button>
<div class="opacity-0 invisible dropdown-menu transition-all duration-300 transform origin-top-right -translate-y-2 scale-95">
<div class="absolute right-0 w-56 mt-2 origin-top-right bg-white border border-gray-200 divide-y divide-gray-100 rounded-md shadow-lg outline-none" aria-labelledby="headlessui-menu-button-1" id="headlessui-menu-items-117" role="menu">
<div class="px-4 py-3">
<p class="text-sm leading-5">Signed in as</p>
<p class="text-sm font-medium leading-5 text-gray-900 truncate"><%= current_customer.email %></p>
</div>
<div class="py-1">
<%= link_to edit_customer_registration_path, class: "text-gray-700 flex justify-between w-full px-4 py-2 text-sm leading-5 text-left" do %>
Account settings
<% end %>
</div>
<div class="py-1">
<%= link_to destroy_customer_session_path, data: { turbo_method: :delete }, class: "text-gray-700 flex justify-between w-full px-4 py-2 text-sm leading-5 text-left" do %>
Sign out
<% end %>
</div>
</div>
</div>
</div>
</div>
</li>
<% else %>
上記のコードを記述すると、ログインしている顧客のナビゲーションバーに、商品一覧へのリンクが付与されたアイコンが表示されるはずです。
最後に、トップページの「Find Products」ボタンのリンクを商品一覧画面へのパスに変更しておきましょう。
app/views/pages/home.html.erb
を開いてパスを修正してください。
<div class='pt-32 pb-12 md:pt-40 md:pb-20'>
<div class='pb-12 text-center md:pb-16'>
<h1 class='leading-tighter mb-4 text-7xl font-extrabold tracking-tighter md:text-8xl'>
<span class='p-2 bg-gradient-to-r from-purple-600 to-blue-500 bg-clip-text text-transparent'>
ECommerce
</span>
</h1>
<div class='mx-auto max-w-3xl'>
<p class='mb-8 text-xl'>You will find what you are looking for.</p>
<div class='flex justify-center mt-5'>
<div class='mt-3'>
<%= link_to products_path, class:'group inline-flex items-center justify-center overflow-hidden rounded-lg bg-gradient-to-br from-purple-600 to-blue-500 p-0.5 font-medium text-gray-900 hover:text-white focus:ring-4 focus:ring-blue-300 group-hover:from-purple-600 group-hover:to-blue-500' do %>
<span class='rounded-md bg-white px-5 py-2.5 transition-all duration-75 ease-in group-hover:bg-opacity-0'>
Find Products
</span>
<% end %>
</div>
</div>
</div>
</div>
</div>
それでは、RuboCop を実行し、必要であればコードを修正しましょう。
& rubocop
今回は、RuboCop の静的解析を全て PASS したようです。
Inspecting 48 files
................................................
48 files inspected, no offenses detected
もしも指摘事項があれば修正してください。
これで、顧客側の商品に関する実装は完了しました。
コミットしておきましょう。
$ git add . && git commit -m "Implementation on the customer's side of the product"
おわりに
お疲れ様でした。
本 Chapter では、顧客側の商品に関する機能を実装しました。
これで商品に関する実装は全て終了しましたので、次の Chapter ではカートに関する機能を実装します。