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