1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

ポートフォリオ構築の振り返り(第9回:itemのカード表示と共通化)

Last updated at Posted at 2025-09-07

はじめに

これまでは 投稿一覧(Item, ItemPost) をそれぞれ表示していましたが、
一覧ページ以外にもユーザーページや、今後作成予定のグループページなどでも「カード形式」で統一して表示したいと考えました。

そのため、カード表示を部分テンプレート化し、Item と ItemPost の両方で使えるように共通化しました。

これまでの記事はこちら👇
ポートフォリオ構築の振り返り(第1回:プロジェクト概要と設計)
ポートフォリオ構築の振り返り(第2回:Railsアプリ立ち上げ〜トップページ表示)
ポートフォリオ構築の振り返り(第3回:Deviseでログイン機能を実装)
ポートフォリオ構築の振り返り(第4回:ヘッダーの作成とログイン機能の実装)
ポートフォリオ構築の振り返り(第5回:投稿機能と画像投稿フォームの作成)
ポートフォリオ構築の振り返り(第6回:投稿機能の作成)
ポートフォリオ構築の振り返り(第7回:ユーザーカラム追加)
ポートフォリオ構築の振り返り(第8回:部分テンプレートを使ったマイページ作成)


手順の流れ

  1. 部分テンプレート _card.html.erb を作成
  2. is_a? を利用して Item と ItemPost を判別
  3. 共通レイアウトにまとめ、再利用可能に

1. 部分テンプレート _card.html.erb の作成

app/views/shared/_card.html.erb

<div class="col-12 col-sm-6 col-md-4 col-lg-3 mb-4">
  <div class="card h-100 position-relative rounded-4">

    <!-- 画像とリンク -->
    <%= link_to item_path(card.is_a?(Item) ? card.id : card.item.id) do %>
      <%= image_tag(card.is_a?(Item) ? card.image : card.item.image, class: "card-img-top object-fit-cover") %>
    <% end %>

    <!-- ステータス -->
    <div class="position-absolute top-0 start-0 m-2 px-2 py-1 btn btn-sm no-hover <%= status_color_class(card.status) %>">
      <%= card.status_i18n %>
    </div>

    <!-- 非公開マーク -->
    <% if card.is_a?(Item) ? card.private : card.item.private %>
      <div class="position-absolute top-0 end-0 m-2 px-2 py-1 bg-white rounded small d-flex align-items-center gap-1 shadow-sm"
          style="border: 1px solid #ffc107; color: #e69500;">
        <i class="fa-solid fa-eye-slash"></i>
      </div>
    <% end %>

    <div class="card-body d-flex flex-column">
      <!-- タイトル・本文 -->
      <div class="mb-auto">
        <%= link_to item_path(card.is_a?(Item) ? card.id : card.item.id) do %>
          <h5 class="card-title">
            <%= card.is_a?(Item) ? card.title : card.item.title %>
          </h5>

          <p class="card-text">
            <%= truncate(card.is_a?(Item) ? card.body : card.review, length: 40) %>
          </p>
        <% end %>
      </div>

      <!-- カテゴリ -->
      <div class="btn btn-sm no-hover <%= category_color_class(card.is_a?(Item) ? card.category : card.item.category) %>">
        <%= card.is_a?(Item) ? card.category_i18n : card.item.category_i18n %>
      </div>

      <!-- 期限(自分の投稿のみ) -->
      <div>
        <% deadline = card.is_a?(Item) ? card.deadline : card.item.deadline %>
        <% user = card.user %>

        <% if user == current_user && deadline.present? %>
          <div class="text-muted small mt-1">
            期限:<%= deadline.strftime('%Y/%m/%d') %>
          </div>
        <% end %>
      </div>

      <!-- ユーザー名 or グループ名 -->
      <div class="d-flex align-items-center mt-2">
        <% group = card.is_a?(Item) ? card.group : card.item.group %>
        <% user  = card.user %>

        <% if group.present? %>
          <%= link_to group_path(group), class: "d-flex align-items-center text-decoration-none" do %>
            <%= image_tag group.image, size:"50x50", class: "rounded-3 me-2" %>
            <div>
              <strong><%= group.name %></strong><br>
              <small class="text-muted">
                (by <%= (user.withdrawn? || user.deactivated?) ? '退会したユーザー' : user.name %>)
              </small>
            </div>
          <% end %>
        <% else %>
          <%= link_to user_path(user.id), class: "d-flex align-items-center text-decoration-none" do %>
            <%= image_tag user.get_profile_image(50, 50),
              style: "width: 50px; height: 50px; object-fit: cover;",
              class: "rounded-circle me-2" %>
            <p class="mb-0">
              <%= (user.withdrawn? || user.deactivated?) ? '退会したユーザー' : user.name %>
            </p>
          <% end %>
        <% end %>
      </div>
    </div>
  </div>
</div>
  • Item と ItemPost の両方を受け取れるよう条件分岐
    card.is_a?(Item) ? ... : ... のように分岐し、
    親(Item)のデータか子(ItemPost)のデータかを判定して表示。(次で作り方まとめます)

  • ステータスやカテゴリを色付きラベルで表示
    状態をひと目でわかるように。

  • 「期限」は自分の投稿だけ表示
    他人には見せず、自分だけが管理できるように制御。

  • ユーザー or グループ表示
    投稿がグループに属していればグループを表示、(何回か後に作り方まとめます)
    そうでなければユーザーを表示。
    退会済ユーザーは「退会したユーザー」として表示。


2. コードの説明

  • card.is_a?(Item)
    そのオブジェクトが Item か ItemPost かを判定
  • status_i18n, category_i18n
    enum を日本語化して表示
  • 非公開アイコン(fa-eye-slash
    private が true の場合に表示
  • ユーザー表示
    退会済み or 無効化ユーザーの場合は「退会したユーザー」として表示

3. 利用方法

一覧ページ・ユーザーページ・グループページなどで以下のように呼び出せます。

<%= render partial: "shared/card", collection: @cards, as: :card %>

これにより、どのページでも同じデザインでカードを使い回せるようになりました。


まとめ

今回は Item と ItemPost をまとめてカード表示する部分テンプレート を作成しました。
今後ユーザーページやグループページでも統一的に使えるため、再利用性・保守性がアップしました!

次は、投稿に付属した 経過の投稿 ができるようにします!
ポートフォリオ構築の振り返り(第10回:親子関係/リレーションを作る)


用語説明

  • 部分テンプレート(partial)
    ビューを部品化して共通化する仕組み。_card.html.erb のように _ で始める。
  • is_a?
    Ruby のメソッドで、オブジェクトが指定したクラスのインスタンスかどうかを判定。
  • truncate
    長い文字列を省略して表示するための Rails ヘルパー。
  • enum + i18n
    モデルで定義したステータスやカテゴリを、Rails の国際化(i18n)で日本語に変換して表示できる。
  • collection: @cards, as: :card
    複数のオブジェクトをまとめて部分テンプレートに渡す書き方。それぞれの要素が card として使える。

---

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?