Railsでのかっこいいスライドショーを作ろう！

# 素のRailsとJavascriptでのスライドショーの作り方
前回書いた通話機能は難しすぎたので
https://qiita.com/Reo-lab/items/90a037f5c18c027342d1

今回は、比較的簡単に作れるかっこいい（自分的に）
スライドショー機能を紹介していきたいと思います！

# 完成形
![Animation.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3888820/9355db0b-f9a4-4654-bae2-c5ce0a3da0c4.gif)

    左右の画像をクリックすると画像が移り変わり、真ん中の画像をクリックすると拡大表示されます。

# ER図
![スクリーンショット (474).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3888820/a4a3372e-a5d7-2e31-5350-5f75e98d97cb.png)
SlideImagesにActiveStorageでpositionと一緒に画像を1枚保存します。
それをUsers_slidesに一対多で紐づけて、写真を複数枚保存していきます。
# 構成
  - Model
      - Users_slides
      - SlideImages 
  -  Controller
      - UsersSlidesController  
  - View
      -  users/show.html.erb
      -  users_slides/edit.html.erb

今回のアプリでは、ユーザーのプロフィール画面(users/show)にスライドショーがあります。

https://gamefriend333.com/

# 作っていこう！
ActiveStorageを使用するので、入っていない方は
    ```
    bin/rails active_storage:install
    ```
    ```
    bin/rails db:migrate
    ```
で導入しましょう。
### モデルの作成
```migration.rb
# CreateUsersSlides
class CreateUsersSlides < ActiveRecord::Migration[7.0]
  def change
    create_table :users_slides do |t|
      t.references :user, null: false, foreign_key: true

      t.timestamps
    end
  end
end
```
```migration.rb
# CreateSlideImages
class CreateSlideImages < ActiveRecord::Migration[7.0]
  def change
    create_table :slide_images do |t|
      t.references :users_slide, null: false, foreign_key: true
      t.integer :position

      t.timestamps
    end
  end
end
```
- UserSlidesではUserとの紐づけ
- SlideImagesでusers_slideとの紐づけとpositionカラムを作成しています
### モデルに関連付けを追加
```model.rb
# User
class User < ApplicationRecord
  has_many :users_slides, dependent: :destroy
  has_many :slide_images, through: :users_slides
end
```
```model.rb
# UsersSlide
class UsersSlide < ApplicationRecord
  belongs_to :user
  has_many :slide_images, dependent: :destroy
end
```
```model.rb
# SlideImage
class SlideImage < ApplicationRecord
  belongs_to :users_slide
  has_one_attached :image
end
```
- User
    - UsersSlideを通じたSlideImageとの関連付けを追加します
- UsersSlide
    - Userモデルとの関連付け、SlideImageとの一対多の関連付けをします。
- SlideImage
    - UsersSlideとの関連付け
    - has_one_attached :image
        - ActiveStorageを使用して、SlideImageモデルに1つの画像を添付できるようにしています。

### Routes
```rb
resources :users do
  resources :users_slides
end
```
今回は、ユーザに紐づいてスライドがあるのでこんな感じです。
### View
```erb
#users/show.html.erb
<% if @user.slide_images.exists?%>
  <div id="slideshow-container">
    <div class="slide-wrapper">
      <!-- 左側の小さい画像 -->
      <div class="small-slide left">
        <%= image_tag @user.slide_images.last.image, class: "small-icon" if @user.slide_images.any? %>
      </div>
      <!-- 中央の大きな画像 -->
      <div class="main-slide">
        <%= image_tag @user.slide_images.first.image, class: "main-icon" if @user.slide_images.any? %>
      </div>
      <!-- 右側の小さい画像 -->
      <div class="small-slide right">
        <%= image_tag @user.slide_images.second.image, class: "small-icon" if @user.slide_images.count > 1 %>
      </div>
    </div>
  </div>
  <% if @users_slide.present?  && @user == current_user %>
    <%= link_to "Edit Slide", edit_user_users_slide_path(@user, @users_slide) %>
  <% else %>
    <p></p>
  <% end %>
<% else %>
  <% if @user == current_user  %>
  <h1>スライドショーを設定しましょう！</h1>
  <%= form_with model: @users_slide, url: user_users_slides_path(@user) , local: true do |form| %>
    <% (1..3).each do |i| %>
      <div>
        <%= form.label "images_#{i}", "スライドショー画像 #{i}", for: "users_slide_images_#{i}" %>
        <%= form.file_field "users_slide[images][]", id: "users_slide_images_#{i}", multiple: false %> <!-- 画像は一つずつ選択 -->
        <%= form.label "position#{i}", "Position", for: "users_slide_positions_#{i}" %>
        <%= form.number_field "users_slide[positions][]", id: "users_slide_positions_#{i}", value: i %> <!-- デフォルトでiを設定 -->
      </div>
    <% end %> 
    <div>
      <%= form.submit "スライドショーを設定する" %>
    </div>
  <% end %>
  <% end %>
<% end %>
```
- if @user.slide_images.exists? の部分がスライドショーを表示する部分。
- else 以降がスライドショーがない場合の作成フォームです。
    <% (1..3).each do |i| %>とすることで、positionを1.2.3に分けて３つの画像フォームを作成します。
```erb
#users/show.html.erb
<!-- モーダル -->
<div id="image-modal" class="modal-slide">
  <span class="close">&times;</span>
  <img class="modal-slide-content" id="modal-image">
</div>
```
- 中心画像をクリックしたときに表示されるモーダルです
- スライドショーがあるページのどこかに追加しましょう
```erb
# users_slides/edit.html.erb
<%= form_with(model: @users_slide, url: user_users_slide_path(current_user, @users_slide), local: true) do |form| %>
  <% @users_slide.slide_images.each_with_index do |slide_image, index| %>
    <div>
      <%= slide_image.image.attached? ? image_tag(slide_image.image,class: "user-icon") : "No Image" %>
      <%= form.hidden_field "slide_images[][id]", value: slide_image.id %>
      <%= form.file_field "slide_images[][image]", id: "slide_image_#{index}" %>
      <%= form.number_field "slide_images[][position]", value: slide_image.position %>
      <%= hidden_field_tag "slide_images[][id]", slide_image.id %>
    </div>
  <% end %>
  <div>
    <%= form.submit "Update Slides" %>
  </div>
<% end %>
```
- スライドショーの編集画面です。

### Controller
```rb
# UsersController
class UsersController < ApplicationController
  def show
    @users_slide = @user.users_slides.first  #追加
  end
end
```
- users/showで表示しているのでusers_slideを追加しています
```rb
# UsersSlidesController
class UsersSlidesController < ApplicationController
  before_action :set_user
  before_action :set_user_slides

  def create
    @users_slide = current_user.users_slides.new
    Rails.logger.debug "Params: #{params.inspect}"
    if @users_slide.save
      create_slide_images
      redirect_to user_path(current_user), notice: 'Slide show was successfully created.'
    else
      render :new
    end
  end

  def update
    Rails.logger.debug "Params: #{params.inspect}"
    slide_images_params.each do |slide_image_param|
      slide_image = SlideImage.find(slide_image_param[:id])
      slide_image.image.attach(slide_image_param[:image]) if slide_image_param[:image].present?
      slide_image.update(position: slide_image_param[:position])
    end
    redirect_to @user, notice: 'Slides updated successfully.'
  end

  def destroy
    @users_slide = @user.users_slides
    @users_slide.destroy
    redirect_to user_users_slides_path(@user), notice: 'Slide show was successfully deleted.'
  end

  private

  def set_user
    @user = current_user
  end

  def set_user_slides
    @users_slide = @user.users_slides.includes(slide_images: { image_attachment: :blob }).first
  end

  def users_slide_params
    params.require(:users_slide).permit(images: [], positions: [])
  end

  def slide_images_params
    params.require(:users_slide).permit(slide_images: %i[id image position])[:slide_images]
  end

  def create_slide_images
    (0..2).each do |index|
      position = params[:users_slide][:positions][index].to_i
      image = get_image_for_slide(index)

      if image.present?
        create_slide_image(image, position)
      else
        attach_default_image(position)
      end
    end
  end

  def get_image_for_slide(index)
    params[:users_slide][:images].present? ? params[:users_slide][:images][index] : nil
  end

  def create_slide_image(image, position)
    @users_slide.slide_images.create(image:, position:)
  end

  def attach_default_image(position)
    default_image_file = load_default_image
    @users_slide.slide_images.create(
      image: {
        io: default_image_file,
        filename: 'GameFriend.jpg',
        content_type: 'image/jpeg'
      },
      position:
    )
    default_image_file.close
  end

  def load_default_image
    default_image_path = Rails.root.join('app/assets/images/GameFriend.jpg')
    File.open(default_image_path)
  end
end

```
- めちゃくちゃdefが多いなって感じますがLintチェックでコードが長すぎて警告されたので分割されまくってます。
    - 分割しないとcreateはこんな感じです
    ```rb
    def create
    @users_slide = current_user.users_slides.new
    Rails.logger.debug "Params: #{params.inspect}" 
    if @users_slide.save
      # スライドショー画像の配列をチェックして、選択されていない部分にデフォルトの画像を設定
      (0..2).each do |index|
        position = params[:users_slide][:positions][index].to_i    
        # 画像が提供されているか確認し、params[:users_slide][:images]が存在するか確認
        if params[:users_slide][:images].present? && params[:users_slide][:images][index].present?
          image = params[:users_slide][:images][index]
          @users_slide.slide_images.create(image: image, position: position)
        else
          # デフォルト画像を指定してActiveStorageで扱える形式に変換
          default_image_path = Rails.root.join("app/assets/images/GameFriend.jpg")
          default_image_file = File.open(default_image_path)
          # ActiveStorageで添付する場合はioとfilenameを明示する
          @users_slide.slide_images.create(
            image: {
              io: default_image_file, 
              filename: 'GameFriend.jpg',
              content_type: 'image/jpeg'  # 適切なMIMEタイプを指定
            }, 
            position: position
          )
          # ファイルを明示的に閉じる（セキュリティとパフォーマンスのため）
          default_image_file.close
        end
      end
      redirect_to user_path(current_user), notice: 'Slide show was successfully created.'
    else
      render :new
    end
  end
    ```
    - @users_slide = current_user.users_slides.new
     if @users_slide.save
        - ここでusers_slidesを作ってしまい、その後slide_imagesにpositionと画像を保存していくという流れです。
    - ユーザーが何も画像を指定しなかったら、defaultの画像が挿入されて作成されるようにしています

### Javascript
```javascript
document.addEventListener('turbo:load', function() {
  let slideIndex = 0; // 現在のスライドインデックス
  const images = <%= @user.slide_images.map { |slide| url_for(slide.image) }.to_json.html_safe %>;
  const mainSlide = document.querySelector(".main-slide img");
  const leftSlide = document.querySelector(".small-slide.left img");
  const rightSlide = document.querySelector(".small-slide.right img");

  const modal = document.getElementById("image-modal");
  const modalImage = document.getElementById("modal-image");
  const closeModal = document.querySelector(".close");

  // スライドを更新する関数
  function updateSlides() {
    mainSlide.src = images[slideIndex];
    leftSlide.src = images[(slideIndex - 1 + images.length) % images.length];
    rightSlide.src = images[(slideIndex + 1) % images.length];
  }
  // 画像をクリックするとモーダルを表示
  mainSlide.addEventListener("click", function() {
    modalImage.src = mainSlide.src; // データ属性に大きい画像URLを設定
    modal.style.display = "block";
  });
  // モーダルを閉じる
  closeModal.addEventListener("click", function() {
    modal.style.display = "none";
  });
  // モーダル外をクリックした場合も閉じる
  window.addEventListener("click", function(event) {
    if (event.target === modal) {
      modal.style.display = "none";
    }
  });
  // 左右スライドのクリックイベント
  leftSlide.addEventListener("click", function() {
    slideIndex = (slideIndex - 1 + images.length) % images.length;
    updateSlides();
  });
  rightSlide.addEventListener("click", function() {
    slideIndex = (slideIndex + 1) % images.length;
    updateSlides();
  });
  // 初期スライドの表示
  updateSlides();
});
```
- 案外短いです
    - slideIndex = (slideIndex - 1 + images.length) % images.length;
        これなんだよって感じるかもしれませんが、
        + images.lengthで負の値にならないように、
        % images.lengthで配列が循環するようにしています
        気になる方はすぐ出てくるの調べてみてください

# 完成！！
ここまででスライドショーが動くようになるはずです
あとは見た目を好きなようにいじりましょう！
参考までに私のCSSを張っておきます

```css
/*スライドショー*/
#slideshow-container {
  width: calc(100% - 15vh);
  height: auto;  
  position: relative;
  margin: auto;
  overflow: hidden;
  background-color: rgba(0, 0, 0, 0.6); /* 背景を半透明に */
}

.slide-wrapper {
  display: flex;
  justify-content: center; /* 中央揃え */
  align-items: center;     /* 垂直方向の中央揃え */
  gap: 10px;               /* 画像間に隙間を設定 */
}

.main-slide {
  flex: 2;                 /* メインの画像を大きく表示 */
  text-align: center;
}

.small-slide {
  flex: 1;                 /* 小さい画像のスペースを小さく */
  text-align: center;
}

.prev:hover, .next:hover {
  background-color: rgba(0,0,0,0.8);
}

.main-icon{
  width: 100%;  /* 画像の幅を全体に合わせる */ /* 画像の幅を指定 */
  height: 100%; /* 画像の高さを指定 */
  object-fit: contain;
}

.small-icon {
  width: 100%;             /* 小さい画像の幅をコンテナに合わせる */
  max-height: 150px;       /* 小さい画像の高さを設定 */
  object-fit: contain;       /* 画像の比率を保つ */
  opacity: 0.7;            /* 少し透明にして目立たないように */
  cursor: pointer;         /* マウスカーソルを変更（クリック可能に見せる） */
}

.small-icon:hover {
  opacity: 1;              /* ホバー時に画像を目立たせる */
}

.modal-slide {
  display: none; /* 非表示がデフォルト */
  position: fixed;
  z-index: 1000;
  padding-top: 60px;
  left: 0;
  top: 0;
  width: 100%;
  height: 100%;
  overflow: auto;
  background-color: rgba(0, 0, 0, 0.8); /* 背景を半透明に */
}

/* モーダルの画像 */
.modal-slide-content {
  margin: auto;
  display: block;
  max-width: 80%;
  object-fit: contain;
  background-color: transparent; /* 画像の周りの背景を透明に */
}

/* 閉じるボタン */
.close {
  position: absolute;
  top: 15px;
  right: 35px;
  color: #fff;
  font-size: 40px;
  font-weight: bold;
  cursor: pointer;
}

.close:hover,
.close:focus {
  color: #bbb;
  text-decoration: none;
  cursor: pointer;
}
```

# 最後に
今回は、私のアプリを使った説明で少し不要な機能も多かったので、
最低限の実装コードも機会があったら作成していきたいなと思っています。
かっこいいスライドショーを作っていきましょう！