testimonials 0.7.9 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/AGENTS.md +210 -0
- data/CHANGELOG.md +68 -0
- data/README.md +30 -4
- data/app/controllers/concerns/testimonials/request_context.rb +75 -0
- data/app/controllers/testimonials/application_controller.rb +10 -56
- data/app/controllers/testimonials/dashboard_controller.rb +40 -0
- data/app/controllers/testimonials/events_controller.rb +9 -0
- data/app/controllers/testimonials/nps_responses_controller.rb +1 -4
- data/app/controllers/testimonials/submissions_controller.rb +169 -0
- data/app/controllers/testimonials/testimonials_controller.rb +6 -166
- data/app/helpers/testimonials/widget_helper.rb +4 -0
- data/app/models/testimonials/prompt_event.rb +12 -0
- data/app/views/layouts/testimonials/application.html.erb +5 -8
- data/app/views/testimonials/nps_responses/index.html.erb +154 -152
- data/app/views/testimonials/nps_responses/show.html.erb +7 -5
- data/app/views/testimonials/shared/_dashboard.html.erb +18 -0
- data/app/views/testimonials/testimonials/index.html.erb +110 -108
- data/app/views/testimonials/testimonials/show.html.erb +7 -5
- data/config/routes.rb +18 -5
- data/examples/README.md +17 -0
- data/examples/badge.html.erb +13 -0
- data/examples/json_ld.html.erb +28 -0
- data/examples/static_site.md +43 -0
- data/examples/testimonial_card.html.erb +17 -0
- data/examples/wall_of_love.html.erb +43 -0
- data/lib/generators/testimonials/install/install_generator.rb +22 -10
- data/lib/generators/testimonials/install/templates/create_testimonials_tables.rb.tt +7 -5
- data/lib/generators/testimonials/install/templates/initializer.rb.tt +25 -2
- data/lib/generators/testimonials/migration_helpers.rb +35 -0
- data/lib/generators/testimonials/nps/nps_generator.rb +2 -6
- data/lib/generators/testimonials/nps/templates/create_testimonials_nps_responses.rb.tt +1 -1
- data/lib/generators/testimonials/prompt_events/prompt_events_generator.rb +35 -0
- data/lib/generators/testimonials/prompt_events/templates/create_testimonials_prompt_events.rb.tt +17 -0
- data/lib/generators/testimonials/tenant/templates/add_tenant_to_testimonials.rb.tt +11 -8
- data/lib/generators/testimonials/tenant/tenant_generator.rb +2 -6
- data/lib/testimonials/configuration.rb +34 -1
- data/lib/testimonials/dashboard.css +278 -221
- data/lib/testimonials/prompt_helper.rb +4 -0
- data/lib/testimonials/seeds.rb +3 -2
- data/lib/testimonials/version.rb +1 -1
- data/lib/testimonials/widget.js +4 -1
- data/lib/testimonials/widget.rb +3 -0
- data/lib/testimonials.rb +8 -0
- metadata +15 -1
|
@@ -1,238 +1,295 @@
|
|
|
1
|
+
/* Dashboard styles.
|
|
2
|
+
*
|
|
3
|
+
* Two layers, and the split is what lets `config.admin_layout` work:
|
|
4
|
+
*
|
|
5
|
+
* 1. Page shell — only ever applies under the gem's own layout, which is the
|
|
6
|
+
* only thing that sets the `tm-index` / `tm-nps-index` / `tm-show` body
|
|
7
|
+
* class. These are the full-viewport rules (100dvh, overflow hidden) that
|
|
8
|
+
* would fight a host's admin chrome for control of the page.
|
|
9
|
+
* 2. Component styles — everything inside `.tml-dashboard`, the wrapper the
|
|
10
|
+
* dashboard views render themselves. These travel with the views into any
|
|
11
|
+
* layout, host or gem.
|
|
12
|
+
*
|
|
13
|
+
* Nothing here styles a bare `body`, `a`, `table` or `*`, and every class and
|
|
14
|
+
* custom property is `tml-`-prefixed or nested, so loading this file inside a
|
|
15
|
+
* host admin cannot restyle the host's sidebar, topbar or anything else.
|
|
16
|
+
*/
|
|
17
|
+
|
|
1
18
|
:root {
|
|
2
19
|
color-scheme: light dark;
|
|
3
|
-
--bg: #f6f7f9; --surface: #fff; --text: #1c2024; --muted: #6b7280;
|
|
4
|
-
--border: #e5e7eb; --accent: #2563eb; --accent-text: #fff;
|
|
5
|
-
--pending: #d97706; --approved: #16a34a; --archived: #6b7280;
|
|
6
|
-
--star: #f59e0b; --danger: #dc2626;
|
|
7
|
-
--promoter: #16a34a; --passive: #d97706; --detractor: #dc2626;
|
|
20
|
+
--tml-bg: #f6f7f9; --tml-surface: #fff; --tml-text: #1c2024; --tml-muted: #6b7280;
|
|
21
|
+
--tml-border: #e5e7eb; --tml-accent: #2563eb; --tml-accent-text: #fff;
|
|
22
|
+
--tml-pending: #d97706; --tml-approved: #16a34a; --tml-archived: #6b7280;
|
|
23
|
+
--tml-star: #f59e0b; --tml-danger: #dc2626;
|
|
24
|
+
--tml-promoter: #16a34a; --tml-passive: #d97706; --tml-detractor: #dc2626;
|
|
8
25
|
/* The four bands an NPS number falls into: bad, ordinary, strong, rare. */
|
|
9
|
-
--nps-needs-work: #dc2626; --nps-good: #d97706; --nps-great: #16a34a; --nps-excellent: #0f766e;
|
|
26
|
+
--tml-nps-needs-work: #dc2626; --tml-nps-good: #d97706; --tml-nps-great: #16a34a; --tml-nps-excellent: #0f766e;
|
|
10
27
|
}
|
|
11
28
|
@media (prefers-color-scheme: dark) {
|
|
12
29
|
:root {
|
|
13
|
-
--bg: #111418; --surface: #1a1f26; --text: #e6e8ea; --muted: #9aa2ab;
|
|
14
|
-
--border: #2a313a; --accent: #3b82f6;
|
|
30
|
+
--tml-bg: #111418; --tml-surface: #1a1f26; --tml-text: #e6e8ea; --tml-muted: #9aa2ab;
|
|
31
|
+
--tml-border: #2a313a; --tml-accent: #3b82f6;
|
|
15
32
|
}
|
|
16
33
|
}
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
.
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
.
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
.nav
|
|
30
|
-
.nav a {
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
.
|
|
37
|
-
.
|
|
38
|
-
.tabs a.active { color: var(--text); font-weight: 600; border: 1px solid var(--border);
|
|
39
|
-
border-bottom: 2px solid var(--surface); background: var(--surface); margin-bottom: -1px; }
|
|
40
|
-
.tabs a:hover { text-decoration: none; color: var(--text); }
|
|
41
|
-
.count { display: inline-block; min-width: 20px; padding: 0 6px; margin-left: 4px; border-radius: 999px;
|
|
42
|
-
background: var(--border); font-size: 12px; text-align: center; }
|
|
43
|
-
.filters { margin-bottom: 16px; display: flex; gap: 8px; align-items: center; }
|
|
44
|
-
.filters select, .filters input[type=search] { padding: 6px 8px; border: 1px solid var(--border);
|
|
45
|
-
border-radius: 8px; background: var(--surface); color: var(--text); font: inherit; }
|
|
46
|
-
.filters input[type=search] { flex: 1; max-width: 320px; }
|
|
47
|
-
.card { background: var(--surface); border: 1px solid var(--border); border-radius: 12px; overflow: hidden; }
|
|
48
|
-
.card.pad { padding: 16px; overflow: visible; }
|
|
49
|
-
table { width: 100%; border-collapse: collapse; }
|
|
50
|
-
th, td { padding: 10px 14px; text-align: left; vertical-align: top; }
|
|
51
|
-
th { font-size: 12px; text-transform: uppercase; letter-spacing: .04em; color: var(--muted);
|
|
52
|
-
border-bottom: 1px solid var(--border); }
|
|
53
|
-
tr + tr td { border-top: 1px solid var(--border); }
|
|
54
|
-
.badge { display: inline-block; padding: 2px 10px; border-radius: 999px; font-size: 12px; font-weight: 600;
|
|
55
|
-
color: #fff; white-space: nowrap; }
|
|
56
|
-
.badge.status-pending { background: var(--pending); }
|
|
57
|
-
.badge.status-approved { background: var(--approved); }
|
|
58
|
-
.badge.status-archived { background: var(--archived); }
|
|
59
|
-
.badge.nps-promoter { background: var(--promoter); }
|
|
60
|
-
.badge.nps-passive { background: var(--passive); }
|
|
61
|
-
.badge.nps-detractor { background: var(--detractor); }
|
|
62
|
-
.badge.locale { background: transparent; color: var(--muted); border: 1px solid var(--border); }
|
|
63
|
-
.stars { color: var(--star); letter-spacing: 1px; white-space: nowrap; }
|
|
64
|
-
.stars .off { color: var(--border); }
|
|
65
|
-
.muted { color: var(--muted); font-size: 13px; }
|
|
66
|
-
.pager { margin-top: 16px; display: flex; gap: 16px; }
|
|
67
|
-
.actions { display: flex; gap: 8px; flex-wrap: wrap; margin: 16px 0; }
|
|
68
|
-
.actions form { display: inline; }
|
|
69
|
-
td.row-actions { white-space: nowrap; text-align: right; }
|
|
70
|
-
td.row-actions form { display: inline-block; }
|
|
71
|
-
td.row-actions form + form { margin-left: 6px; }
|
|
72
|
-
button, .button { padding: 8px 14px; border: 1px solid var(--border); border-radius: 8px; cursor: pointer;
|
|
73
|
-
background: var(--surface); color: var(--text); font: inherit; }
|
|
74
|
-
button.primary { background: var(--accent); border-color: var(--accent); color: var(--accent-text); }
|
|
75
|
-
button.danger { color: var(--danger); }
|
|
76
|
-
button.small { padding: 3px 10px; font-size: 13px; }
|
|
77
|
-
.message { margin: 0; white-space: pre-wrap; overflow-wrap: anywhere; }
|
|
78
|
-
dl { display: grid; grid-template-columns: max-content 1fr; gap: 6px 16px; margin: 0; }
|
|
79
|
-
dt { color: var(--muted); }
|
|
80
|
-
dd { margin: 0; overflow-wrap: anywhere; }
|
|
81
|
-
video.playback { width: 100%; max-width: 480px; border-radius: 10px; background: #000; }
|
|
82
|
-
img.avatar { width: 48px; height: 48px; border-radius: 50%; object-fit: cover; }
|
|
83
|
-
.empty { padding: 48px 16px; text-align: center; color: var(--muted); }
|
|
84
|
-
.breadcrumb { margin-bottom: 12px; font-size: 13px; }
|
|
85
|
-
.case-id { color: var(--muted); font-weight: 400; font-size: 15px; }
|
|
86
|
-
.stat-row { display: flex; gap: 12px; margin-bottom: 16px; flex-wrap: wrap; }
|
|
87
|
-
.stat { flex: 1; min-width: 140px; background: var(--surface); border: 1px solid var(--border);
|
|
88
|
-
border-radius: 12px; padding: 14px 16px; }
|
|
89
|
-
.stat b { display: block; font-size: 26px; }
|
|
90
|
-
.stat span { color: var(--muted); font-size: 13px; }
|
|
91
|
-
|
|
92
|
-
/* NPS score card: the number, the band it falls in, and where it sits on the
|
|
93
|
-
−100..100 scale — so nobody has to google whether 50 is any good. */
|
|
94
|
-
.stat-nps { flex: 2.4; min-width: 240px; }
|
|
95
|
-
.stat-headline { display: flex; align-items: center; gap: 10px; }
|
|
96
|
-
.stat-headline b { display: inline-block; }
|
|
97
|
-
/* `.stat span` would otherwise mute the badge's own white text. */
|
|
98
|
-
.stat .badge { color: #fff; font-size: 12px; }
|
|
99
|
-
.badge.nps-band-needs-work { background: var(--nps-needs-work); }
|
|
100
|
-
.badge.nps-band-good { background: var(--nps-good); }
|
|
101
|
-
.badge.nps-band-great { background: var(--nps-great); }
|
|
102
|
-
.badge.nps-band-excellent { background: var(--nps-excellent); }
|
|
103
|
-
.nps-meter { direction: ltr; margin-top: 10px; }
|
|
104
|
-
.nps-meter-track { position: relative; display: flex; height: 6px; border-radius: 999px; overflow: hidden; }
|
|
105
|
-
.nps-seg { display: block; height: 100%; opacity: .45; }
|
|
106
|
-
.nps-seg.nps-band-needs-work { flex: 100; background: var(--nps-needs-work); }
|
|
107
|
-
.nps-seg.nps-band-good { flex: 30; background: var(--nps-good); }
|
|
108
|
-
.nps-seg.nps-band-great { flex: 40; background: var(--nps-great); }
|
|
109
|
-
.nps-seg.nps-band-excellent { flex: 30; background: var(--nps-excellent); }
|
|
110
|
-
/* Hidden until dashboard.js reads data-nps-marker and positions it, so a
|
|
111
|
-
score of 50 never renders as if it were −100. */
|
|
112
|
-
.nps-marker { display: none; position: absolute; top: -3px; width: 4px; height: 12px; margin-left: -2px;
|
|
113
|
-
border-radius: 2px; background: var(--text); box-shadow: 0 0 0 2px var(--surface); }
|
|
114
|
-
.nps-marker.placed { display: block; }
|
|
115
|
-
.nps-meter-ticks { position: relative; height: 14px; margin-top: 3px; color: var(--muted); font-size: 11px; }
|
|
116
|
-
.nps-meter-ticks span { position: absolute; transform: translateX(-50%); }
|
|
117
|
-
.nps-meter-ticks .tick-min { left: 0; transform: none; }
|
|
118
|
-
.nps-meter-ticks .tick-zero { left: 50%; }
|
|
119
|
-
.nps-meter-ticks .tick-good { left: 65%; }
|
|
120
|
-
.nps-meter-ticks .tick-great { left: 85%; }
|
|
121
|
-
.nps-meter-ticks .tick-max { left: 100%; transform: translateX(-100%); }
|
|
122
|
-
.nps-sample { margin: 8px 0 0; font-size: 12px; }
|
|
123
|
-
|
|
124
|
-
/* The formula and the benchmarks, tucked behind a disclosure. */
|
|
125
|
-
.nps-guide { flex: 0 0 auto; margin-bottom: 14px; background: var(--surface);
|
|
126
|
-
border: 1px solid var(--border); border-radius: 12px; }
|
|
127
|
-
.nps-guide summary { padding: 10px 16px; color: var(--muted); font-size: 13px; cursor: pointer; }
|
|
128
|
-
.nps-guide summary:hover { color: var(--text); }
|
|
129
|
-
.nps-guide[open] summary { border-bottom: 1px solid var(--border); }
|
|
130
|
-
.nps-guide-body { padding: 12px 16px 14px; font-size: 13px; }
|
|
131
|
-
.nps-guide-body p { margin: 0; max-width: 78ch; }
|
|
132
|
-
.nps-bands { display: grid; grid-template-columns: max-content max-content 1fr; gap: 8px 14px;
|
|
133
|
-
margin: 12px 0; padding: 0; list-style: none; align-items: baseline; }
|
|
134
|
-
.nps-bands li { display: contents; }
|
|
135
|
-
.nps-bands b { font-variant-numeric: tabular-nums; white-space: nowrap; }
|
|
136
|
-
.best-line-form textarea { width: 100%; padding: 8px; border: 1px solid var(--border); border-radius: 8px;
|
|
137
|
-
background: var(--surface); color: var(--text); font: inherit; resize: vertical; }
|
|
138
|
-
.best-line-hint { margin: 2px 0 6px; }
|
|
139
|
-
.best-line-save { margin-top: 8px; }
|
|
140
|
-
.panel-head { flex: 0 0 auto; }
|
|
141
|
-
.panel-head h1 { margin-bottom: 12px; }
|
|
142
|
-
.detail-scroll { min-height: 0; display: flex; flex-direction: column; gap: 12px; }
|
|
143
|
-
.testimonial-panel, .nps-panel { min-width: 0; display: flex; flex-direction: column; }
|
|
144
|
-
.testimonial-panel .actions { flex: 0 0 auto; }
|
|
145
|
-
.testimonial-panel .detail-scroll > .actions { margin: 0; }
|
|
146
|
-
|
|
147
|
-
/* Desktop dashboard: queue on the left, selected record on the right. */
|
|
148
|
-
.tm-index, .tm-index .container,
|
|
149
|
-
.tm-nps-index, .tm-nps-index .container { height: 100vh; height: 100dvh; }
|
|
34
|
+
|
|
35
|
+
/* ── 1. Page shell (gem layout only) ─────────────────────────────────────── */
|
|
36
|
+
|
|
37
|
+
.tm-index, .tm-nps-index, .tm-show {
|
|
38
|
+
margin: 0; background: var(--tml-bg); color: var(--tml-text);
|
|
39
|
+
font: 15px/1.5 system-ui, -apple-system, "Segoe UI", sans-serif;
|
|
40
|
+
}
|
|
41
|
+
.tml-page { max-width: 1020px; margin: 0 auto; padding: 24px 16px 64px; }
|
|
42
|
+
.tml-nav { display: flex; gap: 8px; align-items: center; margin-bottom: 16px; flex-wrap: wrap; }
|
|
43
|
+
.tml-nav a { padding: 6px 16px; border: 1px solid var(--tml-border); border-radius: 999px;
|
|
44
|
+
background: var(--tml-surface); color: var(--tml-muted); font-weight: 600;
|
|
45
|
+
text-decoration: none; }
|
|
46
|
+
.tml-nav a:hover { text-decoration: none; color: var(--tml-text); }
|
|
47
|
+
.tml-nav a.active { background: var(--tml-accent); border-color: var(--tml-accent); color: var(--tml-accent-text); }
|
|
48
|
+
.tml-nav a.active:hover { color: var(--tml-accent-text); }
|
|
49
|
+
|
|
50
|
+
/* Desktop dashboard: queue on the left, selected record on the right. Locked to
|
|
51
|
+
the viewport, so it is the gem layout's business alone — under a host layout
|
|
52
|
+
the dashboard is a normal block that scrolls with the host's page. */
|
|
53
|
+
.tm-index, .tm-index .tml-page,
|
|
54
|
+
.tm-nps-index, .tm-nps-index .tml-page { height: 100vh; height: 100dvh; }
|
|
150
55
|
.tm-index, .tm-nps-index { overflow: hidden; }
|
|
151
|
-
.tm-index .
|
|
152
|
-
|
|
153
|
-
.tm-index .
|
|
154
|
-
.tm-nps-index .
|
|
155
|
-
|
|
156
|
-
.tm-nps-index .
|
|
157
|
-
.
|
|
158
|
-
|
|
159
|
-
.dashboard-sidebar
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
.dashboard-
|
|
163
|
-
|
|
164
|
-
.dashboard-sidebar .tabs a { flex: 1 1 0; position: relative; display: flex; align-items: center;
|
|
165
|
-
justify-content: center; gap: 6px; min-height: 34px; padding: 5px 10px 8px;
|
|
166
|
-
border-radius: 8px; color: var(--muted); font-weight: 600; }
|
|
167
|
-
.dashboard-sidebar .tabs a.active { color: var(--text); border: 0; background: transparent; box-shadow: none; margin: 0; }
|
|
168
|
-
.dashboard-sidebar .tabs a.active::after { content: ""; position: absolute; left: 20px; right: 20px; bottom: 0;
|
|
169
|
-
height: 2px; border-radius: 999px; background: var(--accent); }
|
|
170
|
-
.dashboard-sidebar .tabs a:not(.active):hover { text-decoration: none; color: var(--text); background: var(--bg); }
|
|
171
|
-
.dashboard-sidebar .tabs .count { margin-left: 0; background: color-mix(in srgb, var(--muted) 14%, transparent); }
|
|
172
|
-
.dashboard-sidebar .tabs a.active .count { background: var(--border); }
|
|
173
|
-
.dashboard-sidebar .filters { flex: 0 0 auto; display: grid; grid-template-columns: minmax(0, 1fr) auto;
|
|
174
|
-
margin: 0; padding: 12px; border-bottom: 1px solid var(--border); }
|
|
175
|
-
.dashboard-sidebar .filters select { grid-column: 1 / -1; width: 100%; }
|
|
176
|
-
.dashboard-sidebar .filters input[type=search] { min-width: 0; max-width: none; }
|
|
177
|
-
.record-list { flex: 1 1 auto; min-height: 0; overflow-y: auto; }
|
|
178
|
-
.record-row { display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 12px; padding: 13px 14px;
|
|
179
|
-
color: var(--text); border-bottom: 1px solid var(--border); }
|
|
180
|
-
.record-row:hover { text-decoration: none; background: var(--bg); }
|
|
181
|
-
.record-row.active { background: color-mix(in srgb, var(--accent) 9%, var(--surface));
|
|
182
|
-
box-shadow: inset 3px 0 0 var(--accent); }
|
|
183
|
-
.record-main { min-width: 0; display: flex; flex-direction: column; gap: 5px; }
|
|
184
|
-
.record-topline, .record-meta { display: flex; align-items: center; gap: 8px; min-width: 0; }
|
|
185
|
-
.record-copy, .record-meta span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
186
|
-
.record-copy { display: block; font-weight: 600; }
|
|
187
|
-
.record-time { margin-left: auto; white-space: nowrap; }
|
|
188
|
-
.record-side { display: flex; flex-direction: column; align-items: flex-end; justify-content: space-between;
|
|
189
|
-
gap: 8px; padding-top: 1px; }
|
|
190
|
-
.dashboard-sidebar .pager { flex: 0 0 auto; margin: 0; padding: 10px 12px; border-top: 1px solid var(--border); }
|
|
191
|
-
.dashboard-detail { min-width: 0; min-height: 0; display: flex; flex-direction: column; overflow: hidden; }
|
|
192
|
-
.dashboard-detail .testimonial-panel,
|
|
193
|
-
.dashboard-detail .nps-panel { flex: 1 1 auto; min-height: 0; }
|
|
194
|
-
.dashboard-detail .detail-scroll { flex: 1 1 auto; overflow-y: auto; padding-right: 2px; }
|
|
195
|
-
.dashboard-detail .actions { margin-bottom: 0; }
|
|
196
|
-
.empty-state { margin: auto; max-width: 320px; padding: 24px; text-align: center; }
|
|
197
|
-
.empty-state h2 { margin: 0 0 6px; font-size: 18px; }
|
|
198
|
-
.empty-state p { margin: 0; }
|
|
199
|
-
.mobile-back { display: none; }
|
|
56
|
+
.tm-index .tml-page, .tm-nps-index .tml-page { max-width: 1280px; display: flex; flex-direction: column;
|
|
57
|
+
padding-bottom: 16px; }
|
|
58
|
+
.tm-index .tml-nav, .tm-nps-index .tml-nav { flex: 0 0 auto; }
|
|
59
|
+
.tm-index .tml-dashboard, .tm-nps-index .tml-dashboard { flex: 1 1 auto; min-height: 0;
|
|
60
|
+
display: flex; flex-direction: column; }
|
|
61
|
+
.tm-index .page-head, .tm-nps-index .page-head { flex: 0 0 auto; margin-bottom: 14px; }
|
|
62
|
+
.tm-nps-index .stat-row { flex: 0 0 auto; margin-bottom: 14px; }
|
|
63
|
+
.tm-index .dashboard-shell, .tm-nps-index .dashboard-shell { flex: 1 1 auto; min-height: 0; }
|
|
64
|
+
.tm-index .dashboard-sidebar, .tm-nps-index .dashboard-sidebar { min-height: 0; }
|
|
65
|
+
.tm-index .dashboard-detail, .tm-nps-index .dashboard-detail { min-height: 0; overflow: hidden; }
|
|
66
|
+
.tm-index .record-list, .tm-nps-index .record-list { overflow-y: auto; }
|
|
67
|
+
.tm-index .dashboard-detail .detail-scroll,
|
|
68
|
+
.tm-nps-index .dashboard-detail .detail-scroll { flex: 1 1 auto; overflow-y: auto; padding-right: 2px; }
|
|
200
69
|
|
|
201
70
|
/* Standalone show pages keep normal page scrolling. */
|
|
202
71
|
.tm-show { min-height: 100vh; overflow: auto; }
|
|
203
72
|
.tm-show .detail-scroll { overflow: visible; }
|
|
73
|
+
|
|
74
|
+
/* ── 2. Dashboard components (any layout) ────────────────────────────────── */
|
|
75
|
+
|
|
76
|
+
.tml-dashboard {
|
|
77
|
+
color: var(--tml-text);
|
|
78
|
+
|
|
79
|
+
& * { box-sizing: border-box; }
|
|
80
|
+
& a { color: var(--tml-accent); text-decoration: none; }
|
|
81
|
+
& a:hover { text-decoration: underline; }
|
|
82
|
+
& h1 { font-size: 22px; margin: 0 0 16px; }
|
|
83
|
+
& h1.title-row { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
|
|
84
|
+
|
|
85
|
+
/* Index heading row: title on the left, the shareable public link on the right. */
|
|
86
|
+
& .page-head { display: flex; align-items: center; justify-content: space-between;
|
|
87
|
+
gap: 12px; flex-wrap: wrap; margin-bottom: 16px; }
|
|
88
|
+
& .page-head h1 { margin: 0; }
|
|
89
|
+
& .share-link { margin-left: auto; font-size: 13px; font-weight: 400; }
|
|
90
|
+
|
|
91
|
+
& .tabs { display: flex; gap: 4px; border-bottom: 1px solid var(--tml-border); margin-bottom: 16px; flex-wrap: wrap; }
|
|
92
|
+
& .tabs a { padding: 8px 14px; border-radius: 8px 8px 0 0; color: var(--tml-muted); }
|
|
93
|
+
& .tabs a.active { color: var(--tml-text); font-weight: 600; border: 1px solid var(--tml-border);
|
|
94
|
+
border-bottom: 2px solid var(--tml-surface); background: var(--tml-surface); margin-bottom: -1px; }
|
|
95
|
+
& .tabs a:hover { text-decoration: none; color: var(--tml-text); }
|
|
96
|
+
& .count { display: inline-block; min-width: 20px; padding: 0 6px; margin-left: 4px; border-radius: 999px;
|
|
97
|
+
background: var(--tml-border); font-size: 12px; text-align: center; }
|
|
98
|
+
|
|
99
|
+
& .filters { margin-bottom: 16px; display: flex; gap: 8px; align-items: center; }
|
|
100
|
+
& .filters select, & .filters input[type=search] { padding: 6px 8px; border: 1px solid var(--tml-border);
|
|
101
|
+
border-radius: 8px; background: var(--tml-surface); color: var(--tml-text); font: inherit; }
|
|
102
|
+
& .filters input[type=search] { flex: 1; max-width: 320px; }
|
|
103
|
+
|
|
104
|
+
& .card { background: var(--tml-surface); border: 1px solid var(--tml-border); border-radius: 12px; overflow: hidden; }
|
|
105
|
+
& .card.pad { padding: 16px; overflow: visible; }
|
|
106
|
+
|
|
107
|
+
& table { width: 100%; border-collapse: collapse; }
|
|
108
|
+
& th, & td { padding: 10px 14px; text-align: left; vertical-align: top; }
|
|
109
|
+
& th { font-size: 12px; text-transform: uppercase; letter-spacing: .04em; color: var(--tml-muted);
|
|
110
|
+
border-bottom: 1px solid var(--tml-border); }
|
|
111
|
+
& tr + tr td { border-top: 1px solid var(--tml-border); }
|
|
112
|
+
|
|
113
|
+
& .badge { display: inline-block; padding: 2px 10px; border-radius: 999px; font-size: 12px; font-weight: 600;
|
|
114
|
+
color: #fff; white-space: nowrap; }
|
|
115
|
+
& .badge.status-pending { background: var(--tml-pending); }
|
|
116
|
+
& .badge.status-approved { background: var(--tml-approved); }
|
|
117
|
+
& .badge.status-archived { background: var(--tml-archived); }
|
|
118
|
+
& .badge.nps-promoter { background: var(--tml-promoter); }
|
|
119
|
+
& .badge.nps-passive { background: var(--tml-passive); }
|
|
120
|
+
& .badge.nps-detractor { background: var(--tml-detractor); }
|
|
121
|
+
& .badge.locale { background: transparent; color: var(--tml-muted); border: 1px solid var(--tml-border); }
|
|
122
|
+
|
|
123
|
+
& .stars { color: var(--tml-star); letter-spacing: 1px; white-space: nowrap; }
|
|
124
|
+
& .stars .off { color: var(--tml-border); }
|
|
125
|
+
& .muted { color: var(--tml-muted); font-size: 13px; }
|
|
126
|
+
& .pager { margin-top: 16px; display: flex; gap: 16px; }
|
|
127
|
+
& .actions { display: flex; gap: 8px; flex-wrap: wrap; margin: 16px 0; }
|
|
128
|
+
& .actions form { display: inline; }
|
|
129
|
+
& td.row-actions { white-space: nowrap; text-align: right; }
|
|
130
|
+
& td.row-actions form { display: inline-block; }
|
|
131
|
+
& td.row-actions form + form { margin-left: 6px; }
|
|
132
|
+
|
|
133
|
+
& button, & .button { padding: 8px 14px; border: 1px solid var(--tml-border); border-radius: 8px; cursor: pointer;
|
|
134
|
+
background: var(--tml-surface); color: var(--tml-text); font: inherit; }
|
|
135
|
+
& button.primary { background: var(--tml-accent); border-color: var(--tml-accent); color: var(--tml-accent-text); }
|
|
136
|
+
& button.danger { color: var(--tml-danger); }
|
|
137
|
+
& button.small { padding: 3px 10px; font-size: 13px; }
|
|
138
|
+
|
|
139
|
+
& .message { margin: 0; white-space: pre-wrap; overflow-wrap: anywhere; }
|
|
140
|
+
& dl { display: grid; grid-template-columns: max-content 1fr; gap: 6px 16px; margin: 0; }
|
|
141
|
+
& dt { color: var(--tml-muted); }
|
|
142
|
+
& dd { margin: 0; overflow-wrap: anywhere; }
|
|
143
|
+
& video.playback { width: 100%; max-width: 480px; border-radius: 10px; background: #000; }
|
|
144
|
+
& img.avatar { width: 48px; height: 48px; border-radius: 50%; object-fit: cover; }
|
|
145
|
+
& .empty { padding: 48px 16px; text-align: center; color: var(--tml-muted); }
|
|
146
|
+
& .breadcrumb { margin-bottom: 12px; font-size: 13px; }
|
|
147
|
+
& .case-id { color: var(--tml-muted); font-weight: 400; font-size: 15px; }
|
|
148
|
+
|
|
149
|
+
& .stat-row { display: flex; gap: 12px; margin-bottom: 16px; flex-wrap: wrap; }
|
|
150
|
+
& .stat { flex: 1; min-width: 140px; background: var(--tml-surface); border: 1px solid var(--tml-border);
|
|
151
|
+
border-radius: 12px; padding: 14px 16px; }
|
|
152
|
+
& .stat b { display: block; font-size: 26px; }
|
|
153
|
+
& .stat span { color: var(--tml-muted); font-size: 13px; }
|
|
154
|
+
|
|
155
|
+
/* NPS score card: the number, the band it falls in, and where it sits on the
|
|
156
|
+
−100..100 scale — so nobody has to google whether 50 is any good. */
|
|
157
|
+
& .stat-nps { flex: 2.4; min-width: 240px; }
|
|
158
|
+
& .stat-headline { display: flex; align-items: center; gap: 10px; }
|
|
159
|
+
& .stat-headline b { display: inline-block; }
|
|
160
|
+
/* `.stat span` would otherwise mute the badge's own white text. */
|
|
161
|
+
& .stat .badge { color: #fff; font-size: 12px; }
|
|
162
|
+
& .badge.nps-band-needs-work { background: var(--tml-nps-needs-work); }
|
|
163
|
+
& .badge.nps-band-good { background: var(--tml-nps-good); }
|
|
164
|
+
& .badge.nps-band-great { background: var(--tml-nps-great); }
|
|
165
|
+
& .badge.nps-band-excellent { background: var(--tml-nps-excellent); }
|
|
166
|
+
& .nps-meter { direction: ltr; margin-top: 10px; }
|
|
167
|
+
& .nps-meter-track { position: relative; display: flex; height: 6px; border-radius: 999px; overflow: hidden; }
|
|
168
|
+
& .nps-seg { display: block; height: 100%; opacity: .45; }
|
|
169
|
+
& .nps-seg.nps-band-needs-work { flex: 100; background: var(--tml-nps-needs-work); }
|
|
170
|
+
& .nps-seg.nps-band-good { flex: 30; background: var(--tml-nps-good); }
|
|
171
|
+
& .nps-seg.nps-band-great { flex: 40; background: var(--tml-nps-great); }
|
|
172
|
+
& .nps-seg.nps-band-excellent { flex: 30; background: var(--tml-nps-excellent); }
|
|
173
|
+
/* Hidden until dashboard.js reads data-nps-marker and positions it, so a
|
|
174
|
+
score of 50 never renders as if it were −100. */
|
|
175
|
+
& .nps-marker { display: none; position: absolute; top: -3px; width: 4px; height: 12px; margin-left: -2px;
|
|
176
|
+
border-radius: 2px; background: var(--tml-text); box-shadow: 0 0 0 2px var(--tml-surface); }
|
|
177
|
+
& .nps-marker.placed { display: block; }
|
|
178
|
+
& .nps-meter-ticks { position: relative; height: 14px; margin-top: 3px; color: var(--tml-muted); font-size: 11px; }
|
|
179
|
+
& .nps-meter-ticks span { position: absolute; transform: translateX(-50%); }
|
|
180
|
+
& .nps-meter-ticks .tick-min { left: 0; transform: none; }
|
|
181
|
+
& .nps-meter-ticks .tick-zero { left: 50%; }
|
|
182
|
+
& .nps-meter-ticks .tick-good { left: 65%; }
|
|
183
|
+
& .nps-meter-ticks .tick-great { left: 85%; }
|
|
184
|
+
& .nps-meter-ticks .tick-max { left: 100%; transform: translateX(-100%); }
|
|
185
|
+
& .nps-sample { margin: 8px 0 0; font-size: 12px; }
|
|
186
|
+
|
|
187
|
+
/* The formula and the benchmarks, tucked behind a disclosure. */
|
|
188
|
+
& .nps-guide { flex: 0 0 auto; margin-bottom: 14px; background: var(--tml-surface);
|
|
189
|
+
border: 1px solid var(--tml-border); border-radius: 12px; }
|
|
190
|
+
& .nps-guide summary { padding: 10px 16px; color: var(--tml-muted); font-size: 13px; cursor: pointer; }
|
|
191
|
+
& .nps-guide summary:hover { color: var(--tml-text); }
|
|
192
|
+
& .nps-guide[open] summary { border-bottom: 1px solid var(--tml-border); }
|
|
193
|
+
& .nps-guide-body { padding: 12px 16px 14px; font-size: 13px; }
|
|
194
|
+
& .nps-guide-body p { margin: 0; max-width: 78ch; }
|
|
195
|
+
& .nps-bands { display: grid; grid-template-columns: max-content max-content 1fr; gap: 8px 14px;
|
|
196
|
+
margin: 12px 0; padding: 0; list-style: none; align-items: baseline; }
|
|
197
|
+
& .nps-bands li { display: contents; }
|
|
198
|
+
& .nps-bands b { font-variant-numeric: tabular-nums; white-space: nowrap; }
|
|
199
|
+
|
|
200
|
+
& .best-line-form textarea { width: 100%; padding: 8px; border: 1px solid var(--tml-border); border-radius: 8px;
|
|
201
|
+
background: var(--tml-surface); color: var(--tml-text); font: inherit; resize: vertical; }
|
|
202
|
+
& .best-line-hint { margin: 2px 0 6px; }
|
|
203
|
+
& .best-line-save { margin-top: 8px; }
|
|
204
|
+
& .panel-head { flex: 0 0 auto; }
|
|
205
|
+
& .panel-head h1 { margin-bottom: 12px; }
|
|
206
|
+
& .detail-scroll { min-height: 0; display: flex; flex-direction: column; gap: 12px; }
|
|
207
|
+
& .testimonial-panel, & .nps-panel { min-width: 0; display: flex; flex-direction: column; }
|
|
208
|
+
& .testimonial-panel .actions { flex: 0 0 auto; }
|
|
209
|
+
& .testimonial-panel .detail-scroll > .actions { margin: 0; }
|
|
210
|
+
|
|
211
|
+
& .dashboard-shell { display: grid; grid-template-columns: minmax(330px, 430px) 1fr; gap: 16px; }
|
|
212
|
+
& .dashboard-sidebar { min-width: 0; display: flex; flex-direction: column;
|
|
213
|
+
background: var(--tml-surface); border: 1px solid var(--tml-border);
|
|
214
|
+
border-radius: 8px; overflow: hidden; }
|
|
215
|
+
& .dashboard-sidebar .tabs { flex: 0 0 auto; margin: 12px 12px 0; padding: 0; background: transparent;
|
|
216
|
+
border: 0; border-radius: 0; gap: 6px; flex-wrap: nowrap; }
|
|
217
|
+
& .dashboard-sidebar .tabs a { flex: 1 1 0; position: relative; display: flex; align-items: center;
|
|
218
|
+
justify-content: center; gap: 6px; min-height: 34px; padding: 5px 10px 8px;
|
|
219
|
+
border-radius: 8px; color: var(--tml-muted); font-weight: 600; }
|
|
220
|
+
& .dashboard-sidebar .tabs a.active { color: var(--tml-text); border: 0; background: transparent;
|
|
221
|
+
box-shadow: none; margin: 0; }
|
|
222
|
+
& .dashboard-sidebar .tabs a.active::after { content: ""; position: absolute; left: 20px; right: 20px; bottom: 0;
|
|
223
|
+
height: 2px; border-radius: 999px; background: var(--tml-accent); }
|
|
224
|
+
& .dashboard-sidebar .tabs a:not(.active):hover { text-decoration: none; color: var(--tml-text);
|
|
225
|
+
background: var(--tml-bg); }
|
|
226
|
+
& .dashboard-sidebar .tabs .count { margin-left: 0; background: color-mix(in srgb, var(--tml-muted) 14%, transparent); }
|
|
227
|
+
& .dashboard-sidebar .tabs a.active .count { background: var(--tml-border); }
|
|
228
|
+
& .dashboard-sidebar .filters { flex: 0 0 auto; display: grid; grid-template-columns: minmax(0, 1fr) auto;
|
|
229
|
+
margin: 0; padding: 12px; border-bottom: 1px solid var(--tml-border); }
|
|
230
|
+
& .dashboard-sidebar .filters select { grid-column: 1 / -1; width: 100%; }
|
|
231
|
+
& .dashboard-sidebar .filters input[type=search] { min-width: 0; max-width: none; }
|
|
232
|
+
& .record-list { flex: 1 1 auto; min-height: 0; }
|
|
233
|
+
& .record-row { display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 12px; padding: 13px 14px;
|
|
234
|
+
color: var(--tml-text); border-bottom: 1px solid var(--tml-border); }
|
|
235
|
+
& .record-row:hover { text-decoration: none; background: var(--tml-bg); }
|
|
236
|
+
& .record-row.active { background: color-mix(in srgb, var(--tml-accent) 9%, var(--tml-surface));
|
|
237
|
+
box-shadow: inset 3px 0 0 var(--tml-accent); }
|
|
238
|
+
& .record-main { min-width: 0; display: flex; flex-direction: column; gap: 5px; }
|
|
239
|
+
& .record-topline, & .record-meta { display: flex; align-items: center; gap: 8px; min-width: 0; }
|
|
240
|
+
& .record-copy, & .record-meta span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
241
|
+
& .record-copy { display: block; font-weight: 600; }
|
|
242
|
+
& .record-time { margin-left: auto; white-space: nowrap; }
|
|
243
|
+
& .record-side { display: flex; flex-direction: column; align-items: flex-end; justify-content: space-between;
|
|
244
|
+
gap: 8px; padding-top: 1px; }
|
|
245
|
+
& .dashboard-sidebar .pager { flex: 0 0 auto; margin: 0; padding: 10px 12px; border-top: 1px solid var(--tml-border); }
|
|
246
|
+
& .dashboard-detail { min-width: 0; display: flex; flex-direction: column; }
|
|
247
|
+
& .dashboard-detail .testimonial-panel,
|
|
248
|
+
& .dashboard-detail .nps-panel { flex: 1 1 auto; min-height: 0; }
|
|
249
|
+
& .dashboard-detail .actions { margin-bottom: 0; }
|
|
250
|
+
& .empty-state { margin: auto; max-width: 320px; padding: 24px; text-align: center; }
|
|
251
|
+
& .empty-state h2 { margin: 0 0 6px; font-size: 18px; }
|
|
252
|
+
& .empty-state p { margin: 0; }
|
|
253
|
+
& .mobile-back { display: none; }
|
|
254
|
+
}
|
|
255
|
+
|
|
204
256
|
@media (max-width: 760px) {
|
|
205
|
-
|
|
206
|
-
.
|
|
257
|
+
.tm-index, .tm-nps-index, .tm-show { font-size: 14px; }
|
|
258
|
+
.tml-page { padding: 14px 10px 24px; }
|
|
207
259
|
.tm-index, .tm-nps-index, .tm-show { overflow-x: hidden; }
|
|
208
|
-
.dashboard-detail, .testimonial-panel, .nps-panel, .detail-scroll, .card, dl, dd,
|
|
209
|
-
.best-line-form, .best-line-form textarea { min-width: 0; max-width: 100%; }
|
|
210
|
-
.testimonial-panel .card, .nps-panel .card { width: calc(100vw - 20px); }
|
|
211
|
-
.best-line-form textarea { width: 100%; }
|
|
212
|
-
.message, .best-line-hint, dd, .muted { overflow-wrap: anywhere; }
|
|
213
|
-
.message, .best-line-hint, dd, .muted { max-width: calc(100vw - 54px); }
|
|
214
260
|
.tm-index, .tm-nps-index { overflow-y: auto; overflow-x: hidden; }
|
|
215
|
-
.tm-index, .tm-index .
|
|
216
|
-
.tm-nps-index, .tm-nps-index .
|
|
217
|
-
.tm-index .
|
|
218
|
-
.dashboard-shell
|
|
219
|
-
.dashboard-sidebar, .dashboard-detail
|
|
220
|
-
.dashboard-
|
|
221
|
-
.dashboard-
|
|
222
|
-
.record-list { overflow: visible; }
|
|
223
|
-
.
|
|
224
|
-
.
|
|
225
|
-
|
|
226
|
-
.dashboard
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
261
|
+
.tm-index, .tm-index .tml-page,
|
|
262
|
+
.tm-nps-index, .tm-nps-index .tml-page { min-height: 100vh; height: auto; }
|
|
263
|
+
.tm-index .tml-page, .tm-nps-index .tml-page { padding-bottom: 10px; }
|
|
264
|
+
.tm-index .dashboard-shell, .tm-nps-index .dashboard-shell { min-height: calc(100vh - 76px); }
|
|
265
|
+
.tm-index .dashboard-sidebar, .tm-index .dashboard-detail,
|
|
266
|
+
.tm-nps-index .dashboard-sidebar, .tm-nps-index .dashboard-detail { min-height: calc(100vh - 76px); }
|
|
267
|
+
.tm-index .dashboard-detail, .tm-nps-index .dashboard-detail { overflow: visible; }
|
|
268
|
+
.tm-index .record-list, .tm-nps-index .record-list { overflow: visible; }
|
|
269
|
+
.tm-index .dashboard-detail .testimonial-panel,
|
|
270
|
+
.tm-nps-index .dashboard-detail .nps-panel { min-height: calc(100vh - 64px); }
|
|
271
|
+
|
|
272
|
+
.tml-dashboard {
|
|
273
|
+
& .dashboard-detail, & .testimonial-panel, & .nps-panel, & .detail-scroll, & .card, & dl, & dd,
|
|
274
|
+
& .best-line-form, & .best-line-form textarea { min-width: 0; max-width: 100%; }
|
|
275
|
+
& .testimonial-panel .card, & .nps-panel .card { width: calc(100vw - 20px); }
|
|
276
|
+
& .best-line-form textarea { width: 100%; }
|
|
277
|
+
& .message, & .best-line-hint, & dd, & .muted { overflow-wrap: anywhere; max-width: calc(100vw - 54px); }
|
|
278
|
+
& .dashboard-shell { display: block; }
|
|
279
|
+
& .dashboard-shell.has-selected .dashboard-sidebar { display: none; }
|
|
280
|
+
& .dashboard-shell:not(.has-selected) .dashboard-detail { display: none; }
|
|
281
|
+
& .record-row { padding: 12px; }
|
|
282
|
+
& .record-meta { display: none; }
|
|
283
|
+
& .mobile-back { display: block; flex: 0 0 auto; margin: 10px 10px 0; font-size: 13px; }
|
|
284
|
+
& .panel-head h1 { font-size: 18px; }
|
|
285
|
+
& dl { grid-template-columns: 1fr; gap: 3px; }
|
|
286
|
+
& .stat-row { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; }
|
|
287
|
+
& .stat { min-width: 0; padding: 10px 12px; }
|
|
288
|
+
& .stat b { font-size: 22px; }
|
|
289
|
+
& .stat-nps { grid-column: 1 / -1; }
|
|
290
|
+
& .nps-bands { grid-template-columns: max-content 1fr; }
|
|
291
|
+
& .nps-bands b { grid-column: 2; }
|
|
292
|
+
& .nps-bands li > .muted { grid-column: 1 / -1; }
|
|
293
|
+
& .share-link { flex: 1 0 100%; margin-left: 0; text-align: center; }
|
|
294
|
+
}
|
|
238
295
|
}
|
|
@@ -7,6 +7,10 @@ module Testimonials
|
|
|
7
7
|
# throttle allows, so calling this liberally is safe: users who submitted,
|
|
8
8
|
# recently dismissed, or were already prompted max_prompts times are left
|
|
9
9
|
# alone.
|
|
10
|
+
#
|
|
11
|
+
# An install with `config.prompt_events = false` keeps no prompt history, so
|
|
12
|
+
# there is nothing to throttle with and this is a no-op — that app opens the
|
|
13
|
+
# widget on a click instead.
|
|
10
14
|
module PromptHelper
|
|
11
15
|
def testimonial_prompt!(kind = :testimonial)
|
|
12
16
|
flash[:testimonials_prompt] = kind.to_s
|
data/lib/testimonials/seeds.rb
CHANGED
|
@@ -90,9 +90,10 @@ module Testimonials
|
|
|
90
90
|
def self.load!(tenant: nil)
|
|
91
91
|
{
|
|
92
92
|
testimonials: load_testimonials!(tenant: tenant),
|
|
93
|
-
# An install run with --skip-nps has no table
|
|
93
|
+
# An install run with --skip-nps or --skip-prompt-events has no table
|
|
94
|
+
# to seed into.
|
|
94
95
|
nps_responses: (Testimonials.config.nps ? load_nps_responses!(tenant: tenant) : []),
|
|
95
|
-
prompt_events: load_prompt_events!(tenant: tenant)
|
|
96
|
+
prompt_events: (Testimonials.config.prompt_events ? load_prompt_events!(tenant: tenant) : [])
|
|
96
97
|
}
|
|
97
98
|
end
|
|
98
99
|
|
data/lib/testimonials/version.rb
CHANGED
data/lib/testimonials/widget.js
CHANGED
|
@@ -14,7 +14,8 @@
|
|
|
14
14
|
* a star expands into the full form (text or recorded video, consent, guest
|
|
15
15
|
* contact fields). NPS asks 0–10, and promoters are offered the testimonial
|
|
16
16
|
* form right after. Auto-opens report shown/dismissed to the throttle
|
|
17
|
-
* ledger; explicit opens don't
|
|
17
|
+
* ledger; explicit opens don't, and an install without the ledger
|
|
18
|
+
* (config.promptEvents.enabled false) reports nothing at all.
|
|
18
19
|
*
|
|
19
20
|
* On the public standalone pages the same code renders inline into
|
|
20
21
|
* <div data-testimonials-inline>: the testimonial form (config.mode ===
|
|
@@ -1054,6 +1055,8 @@
|
|
|
1054
1055
|
}
|
|
1055
1056
|
|
|
1056
1057
|
function postEvent(kind, action) {
|
|
1058
|
+
// No ledger on this install (--skip-prompt-events): nothing to report to.
|
|
1059
|
+
if (!config.promptEvents || !config.promptEvents.enabled) return;
|
|
1057
1060
|
var data = new FormData();
|
|
1058
1061
|
data.append("kind", kind);
|
|
1059
1062
|
data.append("event_action", action);
|
data/lib/testimonials/widget.rb
CHANGED
|
@@ -107,6 +107,9 @@ module Testimonials
|
|
|
107
107
|
maxSize: config.max_avatar_size.to_i
|
|
108
108
|
},
|
|
109
109
|
nps: { enabled: config.nps ? true : false },
|
|
110
|
+
# The throttle ledger. Off, and the widget skips the shown/dismissed
|
|
111
|
+
# posts: the endpoint would only refuse them.
|
|
112
|
+
promptEvents: { enabled: config.prompt_events ? true : false },
|
|
110
113
|
labels: labels
|
|
111
114
|
}
|
|
112
115
|
# Escape "</" so a value can't close the <script> block early.
|
data/lib/testimonials.rb
CHANGED
|
@@ -30,6 +30,14 @@ module Testimonials
|
|
|
30
30
|
!!config.enabled.call(request)
|
|
31
31
|
end
|
|
32
32
|
|
|
33
|
+
# The class Testimonials::DashboardController inherits from. Resolved on
|
|
34
|
+
# every call rather than memoized, so a host that reassigns
|
|
35
|
+
# base_controller_class in a reloadable initializer is not pinned to a
|
|
36
|
+
# stale, unloaded constant.
|
|
37
|
+
def base_controller
|
|
38
|
+
config.base_controller_class.to_s.constantize
|
|
39
|
+
end
|
|
40
|
+
|
|
33
41
|
# Can this request browse and triage submissions? Checked by every
|
|
34
42
|
# dashboard action.
|
|
35
43
|
def admin?(request)
|