↓↓クリックして頂けると励みになります。
【27 | Google認証】 << 【ホーム】 >> 【29 | Datepicker】
部屋を予約できる機能を実装していきます。
まずは枠組みを作っていきますが、モデル→コントローラー→ビューの流れで実装します。
予約モデル
ジェネレーターを使ってReservationモデルを作成します。
コマンド
長いですが1文です。
rails g model Reservation user:references room:references start_date:datetime end_date:datetime price:bigint total:bigint
マイグレーションを適用します。
コマンド マイグレーション
rails db:migrate
has_manyを使って部屋モデルに関連付けします。
記述追加 app\models\room.rb
「has_many :reservations」の記述追加(4行目)
class Room < ApplicationRecord belongs_to :user has_many :reservations has_many_attached :photos has_rich_text :description geocoded_by :address after_validation :geocode, if: :address_changed? validates :home_type, presence: true validates :room_type, presence: true validates :accommodate, presence: true validates :bed_room, presence: true validates :bath_room, presence: true end
同じくユーザーモデルにも関連付けを行います。
記述追加 app\models\user.rb
「has_many :reservations」の記述追加(4行目)
class User < ApplicationRecord has_many :rooms has_many :reservations has_one_attached :avatar validates :full_name, presence: true, length: {maximum: 50} # Include default devise modules. Others available are: # :confirmable, :lockable, :timeoutable, :trackable and :omniauthable devise :database_authenticatable, :registerable, :recoverable, :rememberable, :validatable, :confirmable, :omniauthable def self.from_omniauth(auth) user = User.where(email: auth.info.email).first if user return user else where(provider: auth.provider, uid: auth.uid).first_or_create do |user| user.email = auth.info.email user.password = Devise.friendly_token[0, 20] user.full_name = auth.info.name # ユーザーモデルに名前があると仮定 user.image = auth.info.image # ユーザーモデルに画像があると仮定 user.uid = auth.uid user.provider = auth.provider end end end end
ルートの設定をしておきます。
resources :roomsの中にresources :reservationsを含めます。
記述追加 config\routes.rb
「resources :reservations, only: [:create]」の記述追加(25行目)
Rails.application.routes.draw do # ルートを app\views\pages\home.html.erb に設定 root 'pages#home' # get get 'pages/home' get '/dashboard', to: 'users#dashboard' get '/users/:id', to: 'users#show', as: 'user' # post post '/users/edit', to: 'users#update' resources :rooms, except: [:edit] do member do get 'listing' get 'pricing' get 'description' get 'photo_upload' get 'amenities' get 'location' delete :delete_photo post :upload_photo end resources :reservations, only: [:create] end # device devise_for :users, path: '', path_names: {sign_up: 'register', sign_in: 'login', edit: 'profile', sign_out: 'logout'}, controllers: {omniauth_callbacks: 'omniauth_callbacks', registrations: 'registrations'} # Define your application routes per the DSL in https://guides.rubyonrails.org/routing.html # Reveal health status on /up that returns 200 if the app boots with no exceptions, otherwise 500. # Can be used by load balancers and uptime monitors to verify that the app is live. get "up" => "rails/health#show", as: :rails_health_check # Defines the root path route ("/") # root "posts#index" end
予約コントローラー
予約コントローラーの実装を行なっていきます。
「app\controllers」フォルダに「reservations_controller.rb」ファイルを新規作成します。
app\controllers\reservations_controller.rb(新規作成したファイル)
class ReservationsController < ApplicationController before_action :authenticate_user! def create room = Room.find(params[:room_id]) if current_user == room.user flash[:alert] = "ホストが予約することはできません。" else start_date = Date.parse(reservation_params[:start_date]) end_date = Date.parse(reservation_params[:end_date]) days = (end_date - start_date).to_i + 1 @reservation = current_user.reservations.build(reservation_params) @reservation.room = room @reservation.price = room.price @reservation.total = room.price * days @reservation.save flash[:notice] = "予約が完了しました。" end redirect_to room end private def reservation_params params.require(:reservation).permit(:start_date, :end_date) end end
ビューの編集
ビューの編集をしていきます。
まずはレンダービューを作成します。
「app\views」フォルダに「reservations」フォルダを新規作成します。
作成した「reservations」フォルダに「_form.html.erb」ファイルを新規作成します。
app\views\reservations\_form.html.erb(新規作成したファイル)
<%= form_for([@room, @room.reservations.new]) do |f| %> <div class="card"> <div class="card-body"> <h5 class="card-title text-danger h4 font1 mb-4"><strong><span>予約はこちら</span></strong></h5> <div class="row"> <div class="col-6"> <span class="badge bg-success">チェックイン </span> <%= f.text_field :start_date, readonly: true, placeholder: "チェックイン", class: "form-control" %> </div> <div class="col-6"> <span class="badge bg-primary">チェックアウト</span> <%= f.text_field :end_date, readonly: true, placeholder: "チェックアウト", class: "form-control" %> </div> </div> <div class="mt-4"> <%= f.submit "予約する", id: "btn_book", class: "btn btn-danger w-100", disabled: true %> </div> </div> </div> <% end %>
作成したレンダービューを組み込みます。
記述追加 app\views\rooms\show.html.erb
「<%= render 'reservations/form' %>」の記述追加(7行目)
<div class="container mt-4"> <div class="row"> <!-- 右側 --> <div class="col-md-3"> <!-- 予約フォーム --> <%= render 'reservations/form' %> </div> <!-- 左側 --> <div class="col-md-9 mb-4"> <!-- カルーセル表示 --> <div class="card"> <div class="card-body"> <div id="carouselExampleIndicators" class="carousel slide" data-bs-ride="carousel"> <div class="carousel-indicators"> <% @photos.each do |photo| %> <button type="button" data-bs-target="#carouselExampleIndicators" data-bs-slide-to="<%= @i %>" class="<%= 'active' if photo.id == @photos[0].id %>" aria-current="true" aria-label="Slide <%= @i+1 %>"></button> <% @i = @i +1 %> <% end %> </div> <div class="carousel-inner"> <% @photos.each do |photo| %> <div class="carousel-item <%= 'active' if photo.id == @photos[0].id %>"> <%= image_tag url_for(photo), class: "d-block w-100", style: "border-radius: 10px;" %> </div> <% end %> </div> <button class="carousel-control-prev" type="button" data-bs-target="#carouselExampleIndicators" data-bs-slide="prev"> <span class="carousel-control-prev-icon" aria-hidden="true"></span> <span class="visually-hidden">Previous</span> </button> <button class="carousel-control-next" type="button" data-bs-target="#carouselExampleIndicators" data-bs-slide="next"> <span class="carousel-control-next-icon" aria-hidden="true"></span> <span class="visually-hidden">Next</span> </button> </div> </div> </div> <div class="card mb-2 mt-2"> <div class="card-body"> <h2 class="mb-4 mt-4"><%= @room.listing_name %></h2> <div class="fs-5"><%= @room.address %></div> <div class="mt-4"> <%= link_to user_path(@room.user), style: "text-decoration:none;" do %> <%= image_tag avatar_url(@room.user), class: "bd-placeholder-img figure-img img-fluid rounded-pill", style: "width: 40px; height: 30px;" %> <span class="badge bg-light text-dark"><%= @room.user.full_name %></span> <% end %> </div> </div> </div> <!-- 部屋のインフォメーション --> <div class="card mb-2"> <div class="card-body"> <div class="row"> <div class="col-3"> <i class="fas fa-home fa-2x" style="color: cadetblue"></i><br/> <%= @room.home_type %> </div> <div class="col-3"> <i class="fas fa-user fa-2x" style="color: cadetblue"></i><br/> <%= pluralize(@room.accommodate, "人宿泊可能") %> </div> <div class="col-3"> <i class="fas fa-bed fa-2x" style="color: cadetblue"></i><br/> <%= pluralize(@room.bed_room, "台") %> </div> <div class="col-3"> <i class="fas fa-door-closed fa-2x" style="color: cadetblue"></i><br/> <%= pluralize(@room.bath_room, "部屋") %> </div> </div> </div> </div> <div class="card"> <div class="card-body"> <div class="badge bg-secondary mb-4">部屋の詳細</div> <p><%= @room.summary %></p> </div> </div> <!-- アメニティ --> <div class="card mt-2"> <div class="card-body"> <div class="badge bg-secondary mb-4">アメニティ</div> <div class="row"> <div class="col-6"> <ul style="list-style:none;"> <li class="<%= 'text-line-through' if !@room.is_tv %>"><span style="color: black;"><i class="fas fa-tv"></i> テレビ</span></li> <li class="<%= 'text-line-through' if !@room.is_kitchen %>"><span style="color: black;"><i class="fas fa-blender"></i> キッチン</span></li> <li class="<%= 'text-line-through' if !@room.is_internet %>"><span style="color: black;"><i class="fas fa-wifi"></i> Wi-Fi</span></li> </ul> </div> <div class="col-6"> <ul style="list-style:none;"> <li class="<%= 'text-line-through' if !@room.is_heating %>"><span style="color: black;"><i class="fab fa-hotjar"></i> 暖房</span></li> <li class="<%= 'text-line-through' if !@room.is_air %>"><span style="color: black;"><i class="fas fa-temperature-low"></i> エアコン</span></li> </ul> </div> </div> </div> </div> <!-- GOOGLE マップ --> <div class="card mt-4"> <div class="card-body"> <div id="map" style="width: 100%; height: 400px"></div> <script async defer src="https://maps.googleapis.com/maps/api/js?key=AIzaSyDoAh6S8kEpArgzGFEvjT_0xK_VeUL5SEs&callback=initMap" type="text/javascript"></script> <script> function initMap() { const uluru = { lat: <%= @room.latitude %>, lng: <%= @room.longitude %> }; const map = new google.maps.Map(document.getElementById("map"), { zoom: 14, center: uluru, }); const contentString = '<div id="content">' + '<%= image_tag room_cover(@room), style: "width: 200px;" %>' + "</div>" + "<h5>" + '<%= @room.listing_name %>' + "</h5>" + "<div>" + '<%= @room.address %>' + "</div>"; const infowindow = new google.maps.InfoWindow({ content: contentString, ariaLabel: "<%= @room.listing_name %>", }); const marker = new google.maps.Marker({ position: uluru, map, title: "<%= @room.listing_name %>", }); marker.addListener("click", () => { infowindow.open({ anchor: marker, map, }); }); } window.initMap = initMap; </script> </div> </div> <!-- 近くのお部屋を検索 --> <div class="card mt-4 mb-4"> <div class="card-body"> <h3 class="badge bg-secondary">近くの部屋</h3> <div class="row"> <% for room in @room.nearbys(10) %> <div class="col-md-4"> <div class="card mb-2"> <%= link_to room_path(room), data: { turbo: false} do %> <%= image_tag room_cover(room), class: "card-image-top", style: "width: 100%;" %> <% end %> <div class="card-body"> <div> <%= link_to room.listing_name, room, class: "btn btn-light", data: { turbo: false} %> </div> <div class="badge bg-danger">距離:<%= room.distance.round(2) %> Km</div> </div> </div> </div> <% end %> </div> </div> </div> </div> </div> </div>
ブラウザ確認
http://localhost:3000/rooms/1
【27 | Google認証】 << 【ホーム】 >> 【29 | Datepicker】
↓↓クリックして頂けると励みになります。