学生向けプログラミング入門 | 無料

学生向けにプログラミングを無料で解説。Java、C++、Ruby、PHP、データベース、Ruby on Rails, Python, Django

Rails7.1 | 民泊予約アプリ作成 | 33 | Ransack

↓↓クリックして頂けると励みになります。



32 | raty-js】 << 【ホーム】 >> 【34 | 予約ステータス



Ransackは簡単にセットアップでき、既存のRailsアプリケーションに統合しやすいです。
シンプルな構文を使用して検索フォームを作成し、モデルのクエリを生成できます。
また、Ransackはカスタマイズが容易です。
検索フォームのデザインや検索条件、ソートオプションなどを自由に調整できます。
カスタムの検索ロジックを実装することもできます。


まずはGemFileにgemの記述を追加します。

gem 'ransack', '~> 4.1', '>= 4.1.1'



バンドルします。
コマンド
bundle

モデル



「app/models/room.rb」ファイルの25行目に以下の記述を追加します。

  def self.ransackable_attributes(auth_object = nil)
    ["accommodate", "active", "address", "bath_room", "bed_room", "created_at", "home_type", "id", "id_value", "is_air", "is_heating", "is_internet", "is_kitchen", "is_tv", "latitude", "listing_name", "longitude", "price", "room_type", "summary", "updated_at", "user_id"]
  end



記述追加 【app/models/room.rb】25行目

class Room < ApplicationRecord

  belongs_to :user
  has_many :reservations

  has_many :guest_reviews
  has_many :reviews

  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

  def average_rating
    guest_reviews.count == 0 ? 0 : guest_reviews.average(:stars).round(2).to_i
  end  

  def self.ransackable_attributes(auth_object = nil)
    ["accommodate", "active", "address", "bath_room", "bed_room", "created_at", "home_type", "id", "id_value", "is_air", "is_heating", "is_internet", "is_kitchen", "is_tv", "latitude", "listing_name", "longitude", "price", "room_type", "summary", "updated_at", "user_id"]
  end

end


コントローラー



コントローラーを編集します。
記述追加 app\controllers\pages_controller.rb
「search()」メソッドの実装

class PagesController < ApplicationController
  
  def home
      @rooms = Room.where(active: true).limit(6)
  end
  
  def search
    # ステップ 1
    if params[:search].present? && params[:search].strip != ""
      session[:loc_search] = params[:search]
    end
    arrResult = Array.new
    # ステップ 2
    if session[:loc_search] && session[:loc_search] != ""
      @rooms_address = Room.where(active: true).near(session[:loc_search], 5, order: 'distance')
    else
      @rooms_address = Room.where(active: true).all
    end
    # ステップ 3
    @search = @rooms_address.ransack(params[:q])
    @rooms = @search.result
    @arrRooms = @rooms.to_a
    # ステップ 4
    if (params[:start_date] && params[:end_date] && !params[:start_date].empty? &&  !params[:end_date].empty?)
      start_date = Date.parse(params[:start_date])
      end_date = Date.parse(params[:end_date])
      @rooms.each do |room|
        not_available = room.reservations.where(
          "(? <= start_date AND start_date <= ?)
          OR (? <= end_date AND end_date <= ?)
          OR (start_date < ? AND ? < end_date)",
          start_date, end_date,
          start_date, end_date,
          start_date, end_date
        ).limit(1)
        if not_available.length > 0
          @arrRooms.delete(room)
        end
      end
    end
  end

end


ルート設定



ルートの設定をします。
記述追加 config\routes.rb
「get 'search', to: 'pages#search'」の記述を追加(12行目)

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'
  get '/your_trips', to: 'reservations#your_trips'
  get '/your_reservations', to: 'reservations#your_reservations'
  get 'search', to: 'pages#search'

  # 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'
      get 'preload'
      get 'preview'
      delete :delete_photo
      post :upload_photo
    end
    resources :reservations, only: [:create]
  end

  resources :guest_reviews, only: [:create, :destroy]
  resources :host_reviews, only: [:create, :destroy]

  # 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\assets\images」フォルダに「home」フォルダを新規作成してください。作成した「home」フォルダの中に何でも良いのでホームページの背景画像となる「background01.jpg」ファイルをコピーして下さい。


「app/assets/stylesheets/application.scss」ファイルに記述を追加します。

//ホームページ用
.has-bg-img {
  background: url("/assets/home/background01.jpg") center center;
  background-size: cover;
}



記述追加 【app/assets/stylesheets/application.scss】54行目

/*
 * This is a manifest file that'll be compiled into application.css, which will include all the files
 * listed below.
 *
 * Any CSS (and SCSS, if configured) file within this directory, lib/assets/stylesheets, or any plugin's
 * vendor/assets/stylesheets directory can be referenced here using a relative path.
 *
 * You're free to add application-wide styles to this file and they'll appear at the bottom of the
 * compiled file so the styles you add here take precedence over styles defined in any other CSS
 * files in this directory. Styles in this file should be added after the last require_* statement.
 * It is generally better to create a new file per style scope.
 *
 *= require_tree .
 *= require_self
 */

 @import "bootstrap";

 // Google Fonts
 body * {
    font-family: Kosugi Maru;
 }

 .font1 {
    font-family: Rampart One;
 }

 .font2 {
    font-family: Kaisei Opti;
 }

//アバター オンライン
.avatar {
   position: relative;
   display: inline-block;
   &::before {
     content: "";
     position: absolute;
     bottom: 1px;
     left: 38px;
     width: 10px;
     height: 10px;
     border-radius: 100%;
     border: 1px solid white;
   }
   &.online:before {
     background-color: #1dbf73;
   }
   &.offline:before {
     background-color: gray;
   }
}

//ホームページ用
.has-bg-img {
   background: url("/assets/home/background01.jpg") center center;
   background-size: cover;
 }


ホームページビューを編集します。


記述編集 【app\views\pages\home.html.erb】

<div class="container ">

    <div class="has-bg-img mb-4">
  
        <h2 class="text-light font2" style="margin-left: 1rem;">
            <p class="pt-4">札幌で民泊可能な部屋を掲載</p>
            <p>どんな部屋をお探しですか?</p>
        </h2>
        <%= form_tag search_path, method: :get, data: { turbo: false} do %>
            <div class="row">
                <div class="col-md-6 mb-2">
                    <span class="badge bg-success" style="margin-left: 1rem;">検索ワード</span>
                    <%= text_field_tag :search, params[:search], placeholder: "どんな部屋をお探しですか?", class: "form-control rounded-pill" %>
                </div>
                <div class="col-md-3 mb-2">
                    <span class="badge bg-primary" style="margin-left: 1rem;">チェックイン</span>
                    <%= text_field_tag :start_date, params[:start_date], placeholder: "チェックイン", readonly: true, class: "form-control datepicker rounded-pill" %>
                </div>
                <div class="col-md-3 mb-2">
                    <span class="badge bg-warning" style="margin-left: 1rem;">チェックアウト</span>
                    <%= text_field_tag :end_date, params[:end_date], placeholder: "チェックアウト", readonly: true, class: "form-control datepicker rounded-pill" %>
                </div>
            </div>
            <div>
                <%= submit_tag "検索 search", class: "btn btn-danger rounded-pill w-100 mt-4 mb-4" %>
            </div>
        <% end %>
    </div>
</div>

<!-- ホーム -->
<div class="container">

    <div class="row mb-4">
        <div class="font1"><h3>民泊可能な部屋</h3></div>

        <% @rooms.each do |room| %>
            <div class="col-md-4">
                <div class="card mb-2">
                    <%= link_to room_path(room), data: { turbo: false} do %>
                        <%= image_tag room_cover(room), style: "width: 100%;", class: "card-image-top" %>
                    <% end %>
                    <div class="card-body">
                        <span><i class="fa fa-star fa-1x" style="color: gold;"></i><%= pluralize(room.average_rating, "") %></span>
                        <%= link_to room_path(room), data: { turbol: false} do %>
                            <h5 class="card-title mt-2">
                                <span class="btn btn-light"><%= room.listing_name %></span>
                            </h5>
                        <% end %>
                        <div class="card-text" style="margin-left: 0.5rem;">
                            <p style="font-size: 0.8rem; margin-bottom: -0.3rem;">Address</p>
                            <p style="margin-bottom: 2rem;"><%= room.address %></p>

                            <%= link_to user_path(room.user), style: "text-decoration:none;" do %>
                                <figure class="figure">
                                    <%= image_tag avatar_url(room.user), style: "width: 40px;", class: "figure-img img-fluid rounded-pill" %>
                                </figure>
                                <span class="badge bg-light text-dark" style="font-size: 0.9rem;"><%= room.user.full_name %></span>
                            <% end %>                    
                        </div>


                        <h5 class="badge rounded-pill bg-danger text-light" style="font-size: 1rem;">1泊<%= number_to_currency(room.price) %></h5>
                    </div>
                </div>
            </div>
        <% end %>
    </div>
</div>

<script>
$('#start_date').datepicker({
  dateFormat: 'dd-mm-yy',
  minDate: 0,
  maxDate: '3m',
  onSelect: function(selected) {
    $('#end_date').datepicker("option", "minDate", selected);
    $('#end_date').attr("disabled", false);
  }
});
$('#end_date').datepicker({
  dateFormat: 'dd-mm-yy',
  minDate: 0,
  maxDate: '3m',
  onSelect: function(selected) {
    $('#start_date').datepicker("option", "maxDate", selected);
  }
});
</script>



ホームページのレイアウトを確認してください。
ブラウザ確認
http://localhost:3000/

PCレイアウト
PCレイアウト


モバイルレイアウト
モバイルレイアウト



検索ページを作成します。


まずは「app\views\rooms」フォルダに「_rooms_list.html.erb」ファイルを新規作成して下さい。


app\views\rooms\_rooms_list.html.erb(新規作成したファイル)

<div class="row" id="room_listing">
    <% @arrRooms.each do |room| %>
        <div class="col-md-6 mb-2 mt-2">
            <div class="card">
                <%= link_to room_path(room), data: { turbo: false} do %>
                    <%= image_tag room_cover(room), style: "width: 100%;", class: "card-image-top" %>
                    <% end %>
                <div class="card-body">
                    <span><i class="fa fa-star fa-1x" style="color: gold;"></i><%= pluralize(room.average_rating, "") %></span>
                    <%= link_to room_path(room), data: { turbo: false} do %>
                        <h5 class="card-title mt-2">
                            <span class="btn btn-light"><%= room.listing_name %></span>
                        </h5>
                    <% end %>
                    <div class="card-text" style="margin-left: 0.5rem;">
                        <p style="font-size: 0.8rem; margin-bottom: -0.3rem;">Address</p>
                        <p style="margin-bottom: 2rem;"><%= room.address %></p>

                        <%= link_to user_path(room.user), style: "text-decoration:none;" do %>
                            <figure class="figure">
                                <%= image_tag avatar_url(room.user), style: "width: 40px;", class: "figure-img img-fluid rounded-pill" %>
                            </figure>
                            <span class="badge bg-light text-dark" style="font-size: 0.9rem;"><%= room.user.full_name %></span>
                        <% end %>                    
                    </div>
                    <div class="mb-2">
                        <span class="badge bg-success text-light"><%= room.home_type %></span>
                        <span class="badge bg-info text-light"><%= room.accommodate %></span>
                        <span class="badge bg-primary text-light">ベッド<%= room.bed_room %></span>                        
                    </div>
                    <h5 class="badge rounded-pill bg-danger text-light" style="font-size: 1rem;">1泊<%= number_to_currency(room.price) %></h5>
                </div>
            </div>
        </div>
    <% end %>
</div>



「app\views\pages」フォルダに「search.js.erb」ファイルを新規作成してください。


app\views\pages\search.js.erb(新規作成したファイル)

$('#room_listing').html('<%= j render partial: "rooms/rooms_list", locals: {rooms: @arrRooms} %>')
initialize(<%= raw @arrRooms.to_json %>)



「app\views\pages」フォルダに「search.html.erb」ファイルを新規作成してください。


app\views\pages\search.html.erb(新規作成したファイル)

<div class="container mt-4">
    <div class="row">
        <div class="col-md-8">
            <div id="filter" class="btn btn-success rounded-pill w-100" data-bs-toggle="collapse" data-bs-target="#collapsePanel" aria-expanded="false" aria-controls="collapsePanel">
                検索フィルター<i class="fa fa-chevron-down"></i>
            </div>

            <div class="collapse" id="collapsePanel">
                <div class="container" style="margin-top: 4rem; margin-bottom: 4rem;">
                    <%= search_form_for @search, url: search_path, remote: true do |f| %>
                        <div class="row">
                            <div class="col-md-8">
                            <label><i class="fas fa-yen-sign"></i>&nbsp;&nbsp;宿泊費(円)</label>
                            <div id="slider-range" style="margin-top: 20px;"></div>
                            </div>
                            <div class="col-md-2">
                            <label>最低(円)</label>
                            <%= f.text_field :price_gteq, class: "form-control rounded-pill" %>
                            </div>
                            <div class="col-md-2">
                            <label>最高(円)</label>
                            <%= f.text_field :price_lteq, class: "form-control rounded-pill" %>
                            </div>
                        </div>
                        <hr/>
                        <div class="row">

                            <label><i class="far fa-calendar-alt"></i>&nbsp;&nbsp;宿泊日</label>

                            <div class="col-md-6 mb-2">
                                <%= text_field_tag :start_date, params[:start_date], readonly: true, placeholder: "チェックイン日", class: "form-control datepicker rounded-pill" %>
                            </div>
                            <div class="col-md-6 mb-2">
                                <%= text_field_tag :end_date, params[:end_date], readonly: true, placeholder: "チェックアウト日", class: "form-control datepicker rounded-pill" %>
                            </div>
                        </div>
                        <hr/>
                        <div class="row">
                            <div class="col-md-4">
                            <%= check_box_tag "q[room_type_eq_any][]", "プライベート" %> プライベート
                            </div>
                            <div class="col-md-4">
                            <%= check_box_tag "q[room_type_eq_any][]", "シェア" %> シェア
                            </div>
                        </div>
                        <hr/>
                        <div class="row">
                            <div class="col-md-4">
                            <div class="form-group select">
                                <label><i class="fas fa-user-friends"></i>&nbsp;宿泊人数</label>
                                <%= f.select :accommodate_gteq, [["2人", 2], ["3人", 3], ["4人", 4], ["5人", 5], ["6人", 6]], id: "accommodate", prompt: "選択してください", class: "form-control" %>
                            </div>
                            </div>
                            <div class="col-md-4">
                            <div class="form-group select">
                                <label><i class="fas fa-bed"></i>&nbsp;ベッド数</label>
                                <%= f.select :bed_room_gteq, [["1台", 1], ["2台", 2], ["3台", 3], ["4台", 4], ["5台", 5], ["6台", 6]], id: "bed_room", prompt: "選択してください", class: "form-control" %>
                            </div>
                            </div>
                            <div class="col-md-4">
                            <div class="form-group select">
                                <label><i class="fas fa-door-closed"></i>&nbsp;部屋数</label>
                                <%= f.select :bath_room_gteq, [["1部屋", 1], ["2部屋", 2], ["3部屋", 3], ["4部屋", 4], ["5部屋", 5], ["6部屋", 6]], id: "bath_rooms", prompt: "選択してください", class: "form-control" %>
                            </div>
                            </div>
                        </div>
                        <hr/>
                        <div class="row">
                            <div class="col-md-4">
                            <%= check_box_tag "q[is_tv_eq]", true %> <i class="fas fa-tv"></i>&nbsp;テレビ
                            </div>
                            <div class="col-md-4">
                            <%= check_box_tag "q[is_kitchen_eq]", true %> <i class="fas fa-blender"></i>&nbsp;キッチン
                            </div>
                            <div class="col-md-4">
                            <%= check_box_tag "q[is_Internet_eq]", true %> <i class="fas fa-wifi"></i>&nbsp;インターネット
                            </div>
                        </div>
                        <div class="row">
                            <div class="col-md-4">
                            <%= check_box_tag "q[is_heating_eq]", true %> <i class="fab fa-hotjar"></i>&nbsp;暖房
                            </div>
                            <div class="col-md-4">
                            <%= check_box_tag "q[is_air_eq]", true %> <i class="fas fa-temperature-low"></i>&nbsp;エアコン
                            </div>
                        </div>
                        <div class="mt-4">
                            <%= f.submit "検索", class: "btn btn-danger w-100" %>
                        </div>
                    <% end %>
                </div>
            </div>
            <!-- 部屋 -->
            <%= render partial: "rooms/rooms_list", locals: {rooms: @arrRooms} %>                        
        </div>

        <div class="col-md-4">
            <!-- GOOGLE マップ -->
            <div class="card mt-4">
                <div class="card-body">
                    
                    <div id="map" style="width: 100%; height: 800px"></div>
                    <script src="https://maps.googleapis.com/maps/api/js?key=AIzaSyDoAh6S8kEpArgzGFEvjT_0xK_VeUL5SEs&callback"></script>
                    <script>
                        function initialize(rooms) {
                        var location = {lat: 43.061771, lng: 141.354451}
                        if (rooms.length > 0) {
                            location = {lat: rooms[0].latitude, lng: rooms[0].longitude}
                        }
                        var map = new google.maps.Map(document.getElementById('map'), {
                            center: location,
                            zoom: 12
                        });
                        var marker, inforwindow;
                        rooms.forEach(function(room) {
                            marker = new google.maps.Marker({
                            position: {lat: room.latitude, lng: room.longitude},
                            map: map
                            });
                            inforwindow = new google.maps.InfoWindow({
                            content: "<div class='map_listing_name'>" + room.listing_name + "</div>"+"<div class='map_price'>" + room.price + "円</div>"
                            });
                            inforwindow.open(map, marker);
                        })
                        }
                        google.maps.event.addDomListener(window, 'load', function() {
                        initialize(<%= raw @arrRooms.to_json %>)
                        });
                    </script>
                </div>
            </div> 
        </div>
    </div>
</div>

<script>
  $('#start_date').datepicker({
    dateFormat: 'dd-mm-yy',
    minDate: 0,
    maxDate: '3m',
    onSelect: function(selected) {
      $('#end_date').datepicker("option", "minDate", selected);
      $('#end_date').attr("disabled", false);
    }
  });
  $('#end_date').datepicker({
    dateFormat: 'dd-mm-yy',
    minDate: 0,
    maxDate: '3m',
    onSelect: function(selected) {
      $('#start_date').datepicker("option", "maxDate", selected);
    }
  });

  $(function() {
    $("#q_price_gteq").val('1000');
    $("#q_price_lteq").val('15000');
    $("#slider-range").slider({
      range: true,
      min: 0,
      max: 15000,
      values: [1000, 15000],
      slide: function(event, ui) {
        $("#q_price_gteq").val(ui.values[0]);
        $("#q_price_lteq").val(ui.values[1]);
      }
    });
    $(".ui-widget-header").css('background', '#00A699');
    $(".ui-state-default, .ui-widget-content").css('background', 'white');
    $(".ui-state-default, .ui-widget-content").css('border-color', '#00A699');
  })
</script>



検索の動作を確認してください。
ブラウザ確認
http://localhost:3000/search

PCレイアウト
PCレイアウト


モバイルレイアウト
モバイルレイアウト



32 | raty-js】 << 【ホーム】 >> 【34 | 予約ステータス




↓↓クリックして頂けると励みになります。