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
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
# frozen_string_literal: true

require "phlex"

require_relative "../layout"

module RankKing
  module Views
    module ListViews
      class Show < Phlex::HTML
        VISIBLE = 10

        def initialize(list:, source:, csrf:, axis: nil)
          @list = list
          @source = source
          @csrf = csrf
          @axes = list.axes
          @selected = axis || @axes.first
        end

        def view_template
          render Layout.new(title: "#{@list.name} — Rank King") do
            render PageHeader.new(title: @list.name, back: "/", back_label: "Lists", size: "type-h2") do
              render_menu
            end

            if @axes.empty?
              render_items
            else
              render_axes
            end
          end
        end

        private

        # Every list action lives here, so a new one is a new entry
        # rather than a new button on the screen. The source decides what
        # syncing looks like: its entry opens a dialog holding its own form.
        def render_menu
          source = @source

          button(class: "icon-button", data_size: "lg", popovertarget: "list-actions", aria_label: "List actions",
                 style: "--gl-icon: var(--rk-icon-more)")
          nav(id: "list-actions", popover: true, aria_label: "List actions") do
            ul(class: "card") do
              li { button(type: "button", commandfor: "sync-dialog", command: "show-modal") { source.label } }
              li { a(href: "/lists/#{@list.id}/settings") { "Settings" } }
            end
          end
          dialog(id: "sync-dialog") { render source.form(csrf: @csrf) }
        end

        # Nothing to compare on yet. The "+ Axis" slot stands where the
        # axis tabs will, and Enter makes the first one.
        def render_items
          action = "/lists/#{@list.id}/axes"
          form(method: "post", action: action, data_first_axis: true) do
            input(type: "hidden", name: "_csrf", value: @csrf.call(action))
            span(class: "tag", data_dashed: true) do
              input(type: "text", name: "name", required: true, placeholder: "+ Axis", aria_label: "New axis")
            end
          end

          ul(class: "card") do
            @list.items.each { |item| li { render @source.item(item) } }
          end
        end

        def render_axes
          div(data_tabs: true) do
            div(role: "tablist", aria_label: "Axes") do
              @axes.each do |axis|
                selected = axis.id == @selected.id
                button(role: "tab", id: "axis-#{axis.id}-tab", aria_controls: "axis-#{axis.id}",
                       aria_selected: selected.to_s, tabindex: selected ? 0 : -1) { axis.name }
              end
            end

            @axes.each do |axis|
              div(role: "tabpanel", id: "axis-#{axis.id}", aria_labelledby: "axis-#{axis.id}-tab",
                  hidden: axis.id != @selected.id) do
                render_ranking(axis)
              end
            end
          end
        end

        def render_ranking(axis)
          ratings = axis.ratings
          ranked, unranked = axis.ranking(ratings)
          rows = ranked.each.with_index(1).map { |item, rank| [format("%02d", rank), item, ratings.fetch(item.id)] } +
                 unranked.map { |item| ["—", item, nil] }
          shown, hidden = rows.take(VISIBLE), rows.drop(VISIBLE)

          a(href: "/lists/#{@list.id}/axes/#{axis.id}/compare", class: "btn", data_variant: "primary") do
            "Compare on #{axis.name}"
          end

          render_rows(shown)
          unless hidden.empty?
            details do
              summary { "Show #{hidden.size} more" }
              render_rows(hidden, start: VISIBLE + 1)
            end
          end
        end

        def render_rows(rows, start: nil)
          model = CONFIG.open_skill
          ol(class: "card", start: start) do
            rows.each do |label, item, rating|
              li do
                span(class: "type-label") { label }
                render @source.item(item)
                if rating
                  span(class: "type-label", data_rating: true, title: format("μ %.1f σ %.1f", rating.mu, rating.sigma)) do
                    # Adding 0.0 turns a rounded -0.0 into 0.0.
                    format("%.1f", model.ordinal(rating).round(1) + 0.0)
                  end
                else
                  span(class: "gl-visually-hidden") { ", Unranked" }
                end
              end
            end
          end
        end
      end
    end
  end
end