Skip to content

Lesson 2 — EmptyState and the boards index

Components::EmptyState

When a user first signs up to KanbanFlow they have no boards. An EmptyState component handles this gracefully — a friendly message and an optional call-to-action when a collection is empty.

Empty states appear throughout the app — no boards, no cards in a column, no members on a board. Rather than handling each inline with scattered if @collection.empty? conditionals, we build a reusable component once.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# app/components/empty_state.rb
class Components::EmptyState < Components::Base
  prop :title,        String
  prop :message,      String
  prop :action_label, _Nilable(String), default: -> { nil }
  prop :action_url,   _Nilable(String), default: -> { nil }

  def view_template
    div(class: "text-center py-12 px-4") do
      p(class: "text-4xl mb-4") { "📋" }
      h3(class: "text-lg font-semibold text-gray-900 mb-2") { @title }
      p(class: "text-gray-500 text-sm mb-6") { @message }
      if @action_label && @action_url
        a(
          href:  @action_url,
          class: "inline-flex items-center justify-center rounded-md " \
                 "font-medium bg-blue-600 text-white hover:bg-blue-700 " \
                 "px-4 py-2 text-sm"
        ) { @action_label }
      end
    end
  end
end

Add a Lookbook preview:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
# test/components/previews/empty_state_preview.rb
class EmptyStatePreview < Lookbook::Preview
  def message_only
    render Components::EmptyState.new(
      title:   "No results found",
      message: "Try adjusting your search or filters."
    )
  end

  def with_action
    render Components::EmptyState.new(
      title:        "No boards yet",
      message:      "Create your first board to get started.",
      action_label: "Create a board",
      action_url:   "#"
    )
  end
end

The boards controller

The controller defines what the app does — fetch data, hand it to a view, redirect on success:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
# app/controllers/boards_controller.rb
class BoardsController < ApplicationController
  def index
    render Views::Boards::Index.new(boards: Board.all)
  end

  def show
    render Views::Boards::Show.new(board: board)
  end

  private

  def board
    @board ||= Board.find(params[:id])
  end
end

Update routes:

1
2
3
4
5
6
7
8
9
# config/routes.rb
Rails.application.routes.draw do
  if Rails.env.development?
    mount Lookbook::Engine, at: "/lookbook"
  end

  resources :boards, only: [:index, :show, :new, :create]
  root "boards#index"
end

Views::Boards::Index

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
# app/views/boards/index.rb
class Views::Boards::Index < Views::Base
  def page_title = "Your Boards"

  def initialize(boards:)
    @boards = boards
  end

  def view_template
    div(class: "flex items-center justify-between mb-6") do
      h1(class: "text-2xl font-bold text-gray-900") { "Your Boards" }
      Button(label: "+ New Board", href: new_board_path)
      end
    end

    if @boards.empty?
      EmptyState(
        title:        "No boards yet",
        message:      "Create your first board to get started.",
        action_label: "Create a board",
        action_url:   new_board_path
      )
    else
      div(class: "grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4") do
        @boards.each { |board| render_board_card(board) }
      end
    end
  end

  private

  def render_board_card(board)
    a(href: board_path(board),
      class: "block p-6 bg-white rounded-lg border hover:shadow-md " \
             "transition-shadow") do
      h2(class: "font-semibold text-gray-900 mb-1") { board.name }
      p(class: "text-sm text-gray-500") do
        plain "#{board.columns.count} columns · #{board.members.count} members"
      end
    end
  end
end

Views::Boards::Show

A placeholder for now — the full Kanban board UI is built in Module 9:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
# app/views/boards/show.rb
class Views::Boards::Show < Views::Base
  def page_title = @board.name

  def initialize(board:)
    @board = board
  end

  def view_template
    div(class: "flex items-center justify-between mb-6") do
      h1(class: "text-2xl font-bold text-gray-900") { @board.name }
      Button(label: "← Back to boards", href: boards_path, variant: :ghost)
    end

    div(class: "grid grid-cols-1 md:grid-cols-3 gap-4") do
      @board.columns.each do |column|
        render_column(column)
      end
    end
  end

  private

  def render_column(column)
    div(class: "bg-gray-100 rounded-lg p-4") do
      h2(class: "font-semibold text-gray-700 mb-3") { column.name }
      p(class: "text-sm text-gray-400") do
        plain "#{column.cards.count} cards · Full board view in Module 9."
      end
    end
  end
end

Visit http://localhost:3000. With seed data loaded you should see a grid of boards. Click one to see the column placeholder view.