Resources

Download Source Code

Summary

# Terminal
bundle add prosopite
bundle add pg_query
bundle add pagy

# Console
ActiveRecord::Migration.remove_index :order_items, :order_id
ActiveRecord::Migration.remove_index :order_items, :product_id
ActiveRecord::Migration.remove_index :orders, :customer_id
ActiveRecord::Migration.remove_index :products, :category_id
ActiveRecord::Migration.remove_index :products, :vendor_id
ActiveRecord::Migration.remove_index :reviews, :customer_id
ActiveRecord::Migration.remove_index :reviews, :product_id

ActiveRecord::Migration.add_index :order_items, :order_id
ActiveRecord::Migration.add_index :order_items, :product_id
ActiveRecord::Migration.add_index :orders, :customer_id
ActiveRecord::Migration.add_index :products, :category_id
ActiveRecord::Migration.add_index :products, :vendor_id
ActiveRecord::Migration.add_index :reviews, :customer_id
ActiveRecord::Migration.add_index :reviews, :product_id

# app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
  include Pagy::Method

  # Only allow modern browsers supporting webp images, web push, badges, import maps, CSS nesting, and CSS :has.
  allow_browser versions: :modern

  # Changes to the importmap will invalidate the etag for HTML responses
  stale_when_importmap_changes

  unless Rails.env.production?
    around_action :n_plus_one_detection

    def n_plus_one_detection
      Prosopite.scan
      yield
    ensure
      Prosopite.finish
    end
  end
end

# app/controllers/dashboard_controller.rb
class DashboardController < ApplicationController
  def index
    @pagy_products, @products = pagy(:offset, Product.includes(:vendor, :category, :reviews))
    @pagy_orders, @orders = pagy(:offset, Order.includes(:customer, order_items: :product))
    @pagy_reviews, @reviews = pagy(:offset, Review.includes(:customer, :product))
    @pagy_vendors, @vendors = pagy(:offset, Vendor.includes(products: :order_items))
  end
end

# app/views/dashboard/index.html.erb
<%= turbo_frame_tag :products do %>
  <table class="min-w-full border border-gray-300">
    <thead class="bg-gray-100">
      <tr>
        <th class="px-4 py-2 text-left text-sm font-medium text-gray-700 border-b">Name</th>
        <th class="px-4 py-2 text-left text-sm font-medium text-gray-700 border-b">SKU</th>
        <th class="px-4 py-2 text-right text-sm font-medium text-gray-700 border-b">Price</th>
        <th class="px-4 py-2 text-left text-sm font-medium text-gray-700 border-b">Vendor</th>
        <th class="px-4 py-2 text-left text-sm font-medium text-gray-700 border-b">Category</th>
        <th class="px-4 py-2 text-right text-sm font-medium text-gray-700 border-b">Avg Rating</th>
        <th class="px-4 py-2 text-right text-sm font-medium text-gray-700 border-b">Reviews</th>
      </tr>
    </thead>
    <tbody>
      <% @products.each do |product| %>
        <tr class="hover:bg-gray-50">
          <td class="px-4 py-2 text-sm border-b"><%= product.name %></td>
          <td class="px-4 py-2 text-sm text-gray-500 border-b"><%= product.sku %></td>
          <td class="px-4 py-2 text-sm text-right border-b"><%= number_to_currency(product.price) %></td>
          <td class="px-4 py-2 text-sm border-b"><%= product.vendor.name %></td>
          <td class="px-4 py-2 text-sm border-b"><%= product.category.name %></td>
          <td class="px-4 py-2 text-sm text-right border-b">
            <% if product.reviews.any? %>
              <%= (product.reviews.sum(&:rating).to_f / product.reviews.size).round(1) %>
            <% else %>
              N/A
            <% end %>
          </td>
          <td class="px-4 py-2 text-sm text-right border-b"><%= product.reviews.size %></td>
        </tr>
      <% end %>
    </tbody>
  </table>
  <%== @pagy_products.series_nav %>
<% end %>

# config/environments/development.rb
config.after_initialize do
  Prosopite.rails_logger = true
end