rails_onboarding 0.6.1

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.
Files changed (148) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.md +1351 -0
  4. data/Rakefile +6 -0
  5. data/app/assets/javascripts/rails_onboarding/admin/chart_controller.js +25 -0
  6. data/app/assets/javascripts/rails_onboarding/admin/filter_controller.js +49 -0
  7. data/app/assets/javascripts/rails_onboarding/admin/flash_controller.js +21 -0
  8. data/app/assets/javascripts/rails_onboarding/admin/flow_editor_controller.js +86 -0
  9. data/app/assets/javascripts/rails_onboarding/application.js +231 -0
  10. data/app/assets/javascripts/rails_onboarding/milestone_celebration_controller.js +177 -0
  11. data/app/assets/javascripts/rails_onboarding/milestone_dashboard_controller.js +132 -0
  12. data/app/assets/javascripts/rails_onboarding/milestone_detail_controller.js +34 -0
  13. data/app/assets/javascripts/rails_onboarding/navigation_controller.js +277 -0
  14. data/app/assets/javascripts/rails_onboarding/onboarding_controller.js +326 -0
  15. data/app/assets/javascripts/rails_onboarding/progress_controller.js +203 -0
  16. data/app/assets/javascripts/rails_onboarding/progressive_disclosure_controller.js +218 -0
  17. data/app/assets/javascripts/rails_onboarding/tooltip_controller.js +1237 -0
  18. data/app/assets/javascripts/rails_onboarding/tooltip_scheduler_controller.js +511 -0
  19. data/app/assets/javascripts/rails_onboarding/tour_controller.js +763 -0
  20. data/app/assets/stylesheets/rails_onboarding/accessibility.css +536 -0
  21. data/app/assets/stylesheets/rails_onboarding/admin.css +816 -0
  22. data/app/assets/stylesheets/rails_onboarding/application.css +1145 -0
  23. data/app/assets/stylesheets/rails_onboarding/flash_messages.css +237 -0
  24. data/app/assets/stylesheets/rails_onboarding/milestones.css +545 -0
  25. data/app/assets/stylesheets/rails_onboarding/mobile.css +856 -0
  26. data/app/assets/stylesheets/rails_onboarding/progressive_disclosure.css +299 -0
  27. data/app/assets/stylesheets/rails_onboarding/tooltips.css +468 -0
  28. data/app/assets/stylesheets/rails_onboarding/tour.css +495 -0
  29. data/app/assets/stylesheets/rails_onboarding/utilities.css +393 -0
  30. data/app/controllers/concerns/rails_onboarding/admin_authorization.rb +98 -0
  31. data/app/controllers/concerns/rails_onboarding/rate_limitable.rb +104 -0
  32. data/app/controllers/rails_onboarding/ab_tests_controller.rb +257 -0
  33. data/app/controllers/rails_onboarding/admin/ab_tests_controller.rb +121 -0
  34. data/app/controllers/rails_onboarding/admin/base_controller.rb +68 -0
  35. data/app/controllers/rails_onboarding/admin/dashboard_controller.rb +189 -0
  36. data/app/controllers/rails_onboarding/admin/flows_controller.rb +175 -0
  37. data/app/controllers/rails_onboarding/admin/users_controller.rb +366 -0
  38. data/app/controllers/rails_onboarding/application_controller.rb +9 -0
  39. data/app/controllers/rails_onboarding/milestones_controller.rb +126 -0
  40. data/app/controllers/rails_onboarding/onboarding_controller.rb +409 -0
  41. data/app/controllers/rails_onboarding/progressive_features_controller.rb +134 -0
  42. data/app/controllers/rails_onboarding/templates_controller.rb +162 -0
  43. data/app/controllers/rails_onboarding/test_sessions_controller.rb +16 -0
  44. data/app/controllers/rails_onboarding/tooltips_controller.rb +92 -0
  45. data/app/helpers/rails_onboarding/application_helper.rb +4 -0
  46. data/app/helpers/rails_onboarding/i18n_helper.rb +137 -0
  47. data/app/helpers/rails_onboarding/personalization_helper.rb +184 -0
  48. data/app/helpers/rails_onboarding/templates_helper.rb +253 -0
  49. data/app/jobs/rails_onboarding/application_job.rb +4 -0
  50. data/app/mailers/rails_onboarding/application_mailer.rb +6 -0
  51. data/app/mailers/rails_onboarding/onboarding_mailer.rb +72 -0
  52. data/app/models/concerns/rails_onboarding/ab_testable.rb +149 -0
  53. data/app/models/concerns/rails_onboarding/onboardable.rb +649 -0
  54. data/app/models/concerns/rails_onboarding/personalizable.rb +211 -0
  55. data/app/models/concerns/rails_onboarding/progressive_disclosure.rb +276 -0
  56. data/app/models/rails_onboarding/analytics.rb +255 -0
  57. data/app/models/rails_onboarding/analytics_event.rb +168 -0
  58. data/app/models/rails_onboarding/application_record.rb +5 -0
  59. data/app/models/rails_onboarding/flow.rb +41 -0
  60. data/app/services/rails_onboarding/milestone_service.rb +105 -0
  61. data/app/views/layouts/rails_onboarding/admin.html.erb +106 -0
  62. data/app/views/layouts/rails_onboarding/application.html.erb +65 -0
  63. data/app/views/rails_onboarding/ab_tests/index.html.erb +209 -0
  64. data/app/views/rails_onboarding/ab_tests/results.html.erb +253 -0
  65. data/app/views/rails_onboarding/admin/ab_tests/_ab_test_card.html.erb +24 -0
  66. data/app/views/rails_onboarding/admin/ab_tests/index.html.erb +57 -0
  67. data/app/views/rails_onboarding/admin/ab_tests/show.html.erb +41 -0
  68. data/app/views/rails_onboarding/admin/dashboard/index.html.erb +224 -0
  69. data/app/views/rails_onboarding/admin/flows/_form.html.erb +147 -0
  70. data/app/views/rails_onboarding/admin/flows/edit.html.erb +7 -0
  71. data/app/views/rails_onboarding/admin/flows/index.html.erb +125 -0
  72. data/app/views/rails_onboarding/admin/flows/new.html.erb +7 -0
  73. data/app/views/rails_onboarding/admin/flows/preview.html.erb +40 -0
  74. data/app/views/rails_onboarding/admin/flows/show.html.erb +114 -0
  75. data/app/views/rails_onboarding/admin/users/index.html.erb +180 -0
  76. data/app/views/rails_onboarding/admin/users/show.html.erb +187 -0
  77. data/app/views/rails_onboarding/milestones/index.html.erb +64 -0
  78. data/app/views/rails_onboarding/milestones/show.html.erb +57 -0
  79. data/app/views/rails_onboarding/onboarding/_custom_footer.html.erb +2 -0
  80. data/app/views/rails_onboarding/onboarding/_progress_indicator.html.erb +56 -0
  81. data/app/views/rails_onboarding/onboarding/_step_navigation.html.erb +44 -0
  82. data/app/views/rails_onboarding/onboarding/step.html.erb +38 -0
  83. data/app/views/rails_onboarding/onboarding/welcome.html.erb +37 -0
  84. data/app/views/rails_onboarding/onboarding_mailer/completion_email.html.erb +109 -0
  85. data/app/views/rails_onboarding/onboarding_mailer/completion_email.text.erb +29 -0
  86. data/app/views/rails_onboarding/onboarding_mailer/reminder_email.html.erb +99 -0
  87. data/app/views/rails_onboarding/onboarding_mailer/reminder_email.text.erb +20 -0
  88. data/app/views/rails_onboarding/onboarding_mailer/step_completed_email.html.erb +105 -0
  89. data/app/views/rails_onboarding/onboarding_mailer/step_completed_email.text.erb +19 -0
  90. data/app/views/rails_onboarding/onboarding_mailer/welcome_email.html.erb +74 -0
  91. data/app/views/rails_onboarding/onboarding_mailer/welcome_email.text.erb +17 -0
  92. data/app/views/rails_onboarding/shared/_flash.html.erb +25 -0
  93. data/app/views/rails_onboarding/shared/_milestone_badge.html.erb +22 -0
  94. data/app/views/rails_onboarding/shared/_milestone_celebration.html.erb +44 -0
  95. data/app/views/rails_onboarding/shared/_onboarding_banner.html.erb +48 -0
  96. data/app/views/rails_onboarding/templates/index.html.erb +255 -0
  97. data/config/importmap.rb +33 -0
  98. data/config/locales/en.yml +62 -0
  99. data/config/locales/es.yml +62 -0
  100. data/config/locales/fr.yml +62 -0
  101. data/config/routes.rb +79 -0
  102. data/lib/generators/rails_onboarding/install_generator.rb +124 -0
  103. data/lib/generators/rails_onboarding/templates/README +120 -0
  104. data/lib/generators/rails_onboarding/templates/add_analytics_to_rails_onboarding.rb +33 -0
  105. data/lib/generators/rails_onboarding/templates/add_milestone_tracking_to_users.rb +38 -0
  106. data/lib/generators/rails_onboarding/templates/add_onboarding_indexes.rb +93 -0
  107. data/lib/generators/rails_onboarding/templates/add_onboarding_to_users.rb +51 -0
  108. data/lib/generators/rails_onboarding/templates/add_robustness_fields_to_users.rb.tt +20 -0
  109. data/lib/generators/rails_onboarding/templates/create_rails_onboarding_flows.rb +18 -0
  110. data/lib/generators/rails_onboarding/templates/onboarding.css +18 -0
  111. data/lib/generators/rails_onboarding/templates/rails_onboarding.rb +75 -0
  112. data/lib/generators/rails_onboarding/update_generator.rb +73 -0
  113. data/lib/rails_onboarding/api_mode.rb +367 -0
  114. data/lib/rails_onboarding/backfill.rb +130 -0
  115. data/lib/rails_onboarding/background_jobs.rb +412 -0
  116. data/lib/rails_onboarding/caching.rb +180 -0
  117. data/lib/rails_onboarding/cdn_support.rb +202 -0
  118. data/lib/rails_onboarding/configuration/ab_testing.rb +24 -0
  119. data/lib/rails_onboarding/configuration/analytics.rb +29 -0
  120. data/lib/rails_onboarding/configuration/integrations.rb +31 -0
  121. data/lib/rails_onboarding/configuration/milestones.rb +92 -0
  122. data/lib/rails_onboarding/configuration/personalization.rb +25 -0
  123. data/lib/rails_onboarding/configuration/progressive_disclosure.rb +41 -0
  124. data/lib/rails_onboarding/configuration/rate_limiting.rb +18 -0
  125. data/lib/rails_onboarding/configuration/steps.rb +121 -0
  126. data/lib/rails_onboarding/configuration/templates.rb +85 -0
  127. data/lib/rails_onboarding/configuration/tooltips.rb +32 -0
  128. data/lib/rails_onboarding/configuration.rb +133 -0
  129. data/lib/rails_onboarding/configuration_errors.rb +19 -0
  130. data/lib/rails_onboarding/configuration_validator.rb +477 -0
  131. data/lib/rails_onboarding/controller_helpers.rb +153 -0
  132. data/lib/rails_onboarding/current.rb +13 -0
  133. data/lib/rails_onboarding/deprecation.rb +64 -0
  134. data/lib/rails_onboarding/devise_integration.rb +122 -0
  135. data/lib/rails_onboarding/engine.rb +245 -0
  136. data/lib/rails_onboarding/error_recovery.rb +173 -0
  137. data/lib/rails_onboarding/lazy_loading.rb +155 -0
  138. data/lib/rails_onboarding/multi_tenant.rb +306 -0
  139. data/lib/rails_onboarding/requirements_validator.rb +315 -0
  140. data/lib/rails_onboarding/responsive_helper.rb +218 -0
  141. data/lib/rails_onboarding/session_manager.rb +187 -0
  142. data/lib/rails_onboarding/skip_logic.rb +159 -0
  143. data/lib/rails_onboarding/turbo_compatibility.rb +229 -0
  144. data/lib/rails_onboarding/version.rb +3 -0
  145. data/lib/rails_onboarding.rb +44 -0
  146. data/lib/tasks/rails_onboarding_analytics.rake +136 -0
  147. data/lib/tasks/rails_onboarding_tasks.rake +162 -0
  148. metadata +303 -0
@@ -0,0 +1,65 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <%# Without this, mobile Safari renders at a virtual ~980px desktop-width
6
+ viewport and zooms out to fit the physical screen - everything (text,
7
+ buttons, spacing) looks shrunk even though the CSS itself is correct. %>
8
+ <meta name="viewport" content="width=device-width, initial-scale=1">
9
+ <title><%= Rails.application.class.module_parent.name %> Onboarding</title>
10
+ <%= csrf_meta_tags %>
11
+ <%= csp_meta_tag %>
12
+
13
+ <%= yield :head %>
14
+
15
+ <%= stylesheet_link_tag "rails_onboarding/application", media: "screen" %>
16
+ <% if defined?(Propshaft) %>
17
+ <%# Propshaft doesn't process application.css's Sprockets `require`
18
+ directives, so each stylesheet needs its own tag or it silently
19
+ never loads (tooltips, tour, milestones, mobile, etc). %>
20
+ <%= stylesheet_link_tag "rails_onboarding/tooltips", media: "screen" %>
21
+ <%= stylesheet_link_tag "rails_onboarding/utilities", media: "screen" %>
22
+ <%= stylesheet_link_tag "rails_onboarding/accessibility", media: "screen" %>
23
+ <%= stylesheet_link_tag "rails_onboarding/milestones", media: "screen" %>
24
+ <%= stylesheet_link_tag "rails_onboarding/tour", media: "screen" %>
25
+ <%= stylesheet_link_tag "rails_onboarding/flash_messages", media: "screen" %>
26
+ <%= stylesheet_link_tag "rails_onboarding/progressive_disclosure", media: "screen" %>
27
+ <%= stylesheet_link_tag "rails_onboarding/mobile", media: "screen" %>
28
+ <% end %>
29
+
30
+ <% if RailsOnboarding.configuration.include_host_styles %>
31
+ <%= stylesheet_link_tag "application", "data-turbo-track": "reload", media: "screen" %>
32
+ <% end %>
33
+
34
+ <%= javascript_include_tag "rails_onboarding/application", "data-turbo-track": "reload", defer: true %>
35
+ </head>
36
+ <body>
37
+
38
+ <%# .onboarding-container/.onboarding-content provide the page-level
39
+ min-height/centering/responsive padding that the stylesheets (see
40
+ application.css and mobile.css) already define for every onboarding
41
+ page - without this wrapper, yielded content rendered directly under
42
+ <body> with zero horizontal padding, which is invisible on wide
43
+ viewports (there's empty space outside the content's own max-width
44
+ either way) but runs text edge-to-edge on mobile. %>
45
+ <div class="onboarding-container">
46
+ <%# .onboarding-container is a three-row grid (auto 1fr auto) - header,
47
+ content, footer - and this fills the header row. The progress indicator
48
+ reads @current_step/@progress/@total_steps, which only the onboarding
49
+ controller assigns; milestones and A/B test pages share this layout and
50
+ set none of them, so it renders only when that context is present.
51
+ Putting it here rather than in each step template means host-supplied
52
+ step templates get it too, without having to know about it. %>
53
+ <% if @current_step && @total_steps %>
54
+ <header class="onboarding-header">
55
+ <%= render "rails_onboarding/onboarding/progress_indicator" %>
56
+ </header>
57
+ <% end %>
58
+
59
+ <div class="onboarding-content">
60
+ <%= yield %>
61
+ </div>
62
+ </div>
63
+
64
+ </body>
65
+ </html>
@@ -0,0 +1,209 @@
1
+ <div class="ab-tests-index">
2
+ <div class="header">
3
+ <h1>A/B Tests</h1>
4
+ <p class="subtitle">Manage and monitor your onboarding A/B tests</p>
5
+ </div>
6
+
7
+ <% if @ab_tests.empty? %>
8
+ <div class="onboarding-empty-state">
9
+ <p>No A/B tests configured yet.</p>
10
+ <p class="onboarding-help-text">Configure A/B tests in your Rails initializer:</p>
11
+ <pre><code>RailsOnboarding.configure do |config|
12
+ config.enable_ab_testing = true
13
+ config.ab_tests = {
14
+ onboarding_flow: {
15
+ variants: ['original', 'simplified'],
16
+ weights: [50, 50],
17
+ enabled: true
18
+ }
19
+ }
20
+ end</code></pre>
21
+ </div>
22
+ <% else %>
23
+ <div class="ab-tests-list">
24
+ <% @ab_tests.each do |test_name, config| %>
25
+ <div class="ab-test-card">
26
+ <div class="ab-test-header">
27
+ <h2><%= test_name.to_s.titleize %></h2>
28
+ <span class="status-badge <%= config[:enabled] ? 'enabled' : 'disabled' %>">
29
+ <%= config[:enabled] ? 'Active' : 'Inactive' %>
30
+ </span>
31
+ </div>
32
+
33
+ <div class="ab-test-details">
34
+ <div class="variants">
35
+ <h3>Variants</h3>
36
+ <ul>
37
+ <% config[:variants].each_with_index do |variant, index| %>
38
+ <li>
39
+ <%= variant %>
40
+ <% if config[:weights] && config[:weights][index] %>
41
+ <span class="weight"><%= config[:weights][index] %>%</span>
42
+ <% end %>
43
+ </li>
44
+ <% end %>
45
+ </ul>
46
+ </div>
47
+
48
+ <div class="actions">
49
+ <%= link_to 'View Results', results_ab_test_path(test_name), class: 'btn btn-primary' %>
50
+ <%= link_to 'Details', ab_test_path(test_name), class: 'btn btn-secondary' %>
51
+ <%= button_to config[:enabled] ? 'Disable' : 'Enable',
52
+ toggle_ab_test_path(test_name),
53
+ method: :post,
54
+ class: 'btn btn-toggle' %>
55
+ </div>
56
+ </div>
57
+ </div>
58
+ <% end %>
59
+ </div>
60
+ <% end %>
61
+ </div>
62
+
63
+ <style>
64
+ .ab-tests-index {
65
+ max-width: 1200px;
66
+ margin: 0 auto;
67
+ padding: 2rem;
68
+ }
69
+
70
+ .header {
71
+ margin-bottom: 2rem;
72
+ }
73
+
74
+ .header h1 {
75
+ font-size: 2rem;
76
+ font-weight: bold;
77
+ margin-bottom: 0.5rem;
78
+ }
79
+
80
+ .subtitle {
81
+ color: #666;
82
+ font-size: 1rem;
83
+ }
84
+
85
+ .empty-state {
86
+ background: #f5f5f5;
87
+ padding: 3rem;
88
+ border-radius: 8px;
89
+ text-align: center;
90
+ }
91
+
92
+ .empty-state .help-text {
93
+ margin-top: 1rem;
94
+ color: #666;
95
+ }
96
+
97
+ .empty-state pre {
98
+ background: white;
99
+ padding: 1rem;
100
+ border-radius: 4px;
101
+ text-align: left;
102
+ margin-top: 1rem;
103
+ overflow-x: auto;
104
+ }
105
+
106
+ .ab-tests-list {
107
+ display: grid;
108
+ gap: 1.5rem;
109
+ }
110
+
111
+ .ab-test-card {
112
+ background: white;
113
+ border: 1px solid #ddd;
114
+ border-radius: 8px;
115
+ padding: 1.5rem;
116
+ }
117
+
118
+ .ab-test-header {
119
+ display: flex;
120
+ justify-content: space-between;
121
+ align-items: center;
122
+ margin-bottom: 1rem;
123
+ }
124
+
125
+ .ab-test-header h2 {
126
+ font-size: 1.5rem;
127
+ font-weight: 600;
128
+ margin: 0;
129
+ }
130
+
131
+ .status-badge {
132
+ padding: 0.25rem 0.75rem;
133
+ border-radius: 12px;
134
+ font-size: 0.875rem;
135
+ font-weight: 500;
136
+ }
137
+
138
+ .status-badge.enabled {
139
+ background: #d4edda;
140
+ color: #155724;
141
+ }
142
+
143
+ .status-badge.disabled {
144
+ background: #f8d7da;
145
+ color: #721c24;
146
+ }
147
+
148
+ .ab-test-details {
149
+ display: flex;
150
+ justify-content: space-between;
151
+ align-items: flex-start;
152
+ }
153
+
154
+ .variants h3 {
155
+ font-size: 1rem;
156
+ font-weight: 600;
157
+ margin-bottom: 0.5rem;
158
+ }
159
+
160
+ .variants ul {
161
+ list-style: none;
162
+ padding: 0;
163
+ margin: 0;
164
+ }
165
+
166
+ .variants li {
167
+ padding: 0.5rem 0;
168
+ display: flex;
169
+ justify-content: space-between;
170
+ align-items: center;
171
+ }
172
+
173
+ .weight {
174
+ background: #e9ecef;
175
+ padding: 0.125rem 0.5rem;
176
+ border-radius: 4px;
177
+ font-size: 0.875rem;
178
+ margin-left: 0.5rem;
179
+ }
180
+
181
+ .actions {
182
+ display: flex;
183
+ gap: 0.5rem;
184
+ }
185
+
186
+ .btn {
187
+ padding: 0.5rem 1rem;
188
+ border-radius: 4px;
189
+ text-decoration: none;
190
+ border: none;
191
+ cursor: pointer;
192
+ font-size: 0.875rem;
193
+ }
194
+
195
+ .btn-primary {
196
+ background: #007bff;
197
+ color: white;
198
+ }
199
+
200
+ .btn-secondary {
201
+ background: #6c757d;
202
+ color: white;
203
+ }
204
+
205
+ .btn-toggle {
206
+ background: #ffc107;
207
+ color: #000;
208
+ }
209
+ </style>
@@ -0,0 +1,253 @@
1
+ <div class="ab-test-results">
2
+ <div class="header">
3
+ <h1><%= @test_name.to_s.titleize %> - Results</h1>
4
+ <%= link_to '← Back to A/B Tests', ab_tests_path, class: 'back-link' %>
5
+ </div>
6
+
7
+ <div class="results-summary">
8
+ <h2>Performance by Variant</h2>
9
+
10
+ <div class="variants-comparison">
11
+ <% @results.each do |variant, metrics| %>
12
+ <div class="variant-card">
13
+ <h3><%= variant.to_s.titleize %></h3>
14
+
15
+ <div class="metrics">
16
+ <div class="metric">
17
+ <span class="metric-label">Participants</span>
18
+ <span class="metric-value"><%= metrics[:participants] %></span>
19
+ </div>
20
+
21
+ <div class="metric">
22
+ <span class="metric-label">Completions</span>
23
+ <span class="metric-value"><%= metrics[:completions] %></span>
24
+ </div>
25
+
26
+ <div class="metric highlight">
27
+ <span class="metric-label">Conversion Rate</span>
28
+ <span class="metric-value"><%= metrics[:conversion_rate] %>%</span>
29
+ </div>
30
+
31
+ <div class="metric">
32
+ <span class="metric-label">Avg. Time (seconds)</span>
33
+ <span class="metric-value"><%= metrics[:average_time] %></span>
34
+ </div>
35
+
36
+ <div class="metric">
37
+ <span class="metric-label">Skip Rate</span>
38
+ <span class="metric-value"><%= metrics[:skip_rate] %>%</span>
39
+ </div>
40
+ </div>
41
+ </div>
42
+ <% end %>
43
+ </div>
44
+ </div>
45
+
46
+ <% if @funnel.present? %>
47
+ <div class="funnel-analysis">
48
+ <h2>Funnel Analysis by Variant</h2>
49
+
50
+ <div class="funnel-container">
51
+ <% @funnel.each do |variant, steps| %>
52
+ <div class="funnel-variant">
53
+ <h3><%= variant.to_s.titleize %></h3>
54
+
55
+ <div class="funnel-steps">
56
+ <% steps.each do |step_name, count| %>
57
+ <% step = RailsOnboarding.configuration.step_by_name(step_name) %>
58
+ <div class="funnel-step">
59
+ <div class="step-info">
60
+ <span class="step-icon"><%= step[:icon] if step %></span>
61
+ <span class="step-name"><%= step_name.to_s.titleize %></span>
62
+ </div>
63
+ <div class="step-count"><%= count %> users</div>
64
+ </div>
65
+ <% end %>
66
+ </div>
67
+ </div>
68
+ <% end %>
69
+ </div>
70
+ </div>
71
+ <% end %>
72
+
73
+ <div class="winner-recommendation">
74
+ <% winner = @results.max_by { |_, metrics| metrics[:conversion_rate] } %>
75
+ <% if winner %>
76
+ <h2>Recommendation</h2>
77
+ <div class="recommendation-card">
78
+ <p>
79
+ <strong><%= winner[0].to_s.titleize %></strong> variant shows the best performance
80
+ with a <%= winner[1][:conversion_rate] %>% conversion rate.
81
+ </p>
82
+ <% if winner[1][:participants] < 100 %>
83
+ <p class="onboarding-warning">
84
+ ⚠️ Note: Sample size is relatively small (<%= winner[1][:participants] %> participants).
85
+ Consider gathering more data for statistical significance.
86
+ </p>
87
+ <% end %>
88
+ </div>
89
+ <% end %>
90
+ </div>
91
+ </div>
92
+
93
+ <style>
94
+ .ab-test-results {
95
+ max-width: 1400px;
96
+ margin: 0 auto;
97
+ padding: 2rem;
98
+ }
99
+
100
+ .header {
101
+ margin-bottom: 2rem;
102
+ }
103
+
104
+ .header h1 {
105
+ font-size: 2rem;
106
+ font-weight: bold;
107
+ margin-bottom: 0.5rem;
108
+ }
109
+
110
+ .back-link {
111
+ color: #007bff;
112
+ text-decoration: none;
113
+ font-size: 0.875rem;
114
+ }
115
+
116
+ .results-summary {
117
+ margin-bottom: 3rem;
118
+ }
119
+
120
+ .results-summary h2,
121
+ .funnel-analysis h2,
122
+ .winner-recommendation h2 {
123
+ font-size: 1.5rem;
124
+ font-weight: 600;
125
+ margin-bottom: 1.5rem;
126
+ }
127
+
128
+ .variants-comparison {
129
+ display: grid;
130
+ grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
131
+ gap: 1.5rem;
132
+ }
133
+
134
+ .variant-card {
135
+ background: white;
136
+ border: 2px solid #ddd;
137
+ border-radius: 8px;
138
+ padding: 1.5rem;
139
+ }
140
+
141
+ .variant-card h3 {
142
+ font-size: 1.25rem;
143
+ font-weight: 600;
144
+ margin-bottom: 1rem;
145
+ padding-bottom: 0.75rem;
146
+ border-bottom: 2px solid #007bff;
147
+ }
148
+
149
+ .metrics {
150
+ display: flex;
151
+ flex-direction: column;
152
+ gap: 1rem;
153
+ }
154
+
155
+ .metric {
156
+ display: flex;
157
+ justify-content: space-between;
158
+ align-items: center;
159
+ padding: 0.75rem;
160
+ background: #f8f9fa;
161
+ border-radius: 4px;
162
+ }
163
+
164
+ .metric.highlight {
165
+ background: #d4edda;
166
+ font-weight: 600;
167
+ }
168
+
169
+ .metric-label {
170
+ font-size: 0.875rem;
171
+ color: #666;
172
+ }
173
+
174
+ .metric-value {
175
+ font-size: 1.25rem;
176
+ font-weight: 600;
177
+ color: #000;
178
+ }
179
+
180
+ .funnel-analysis {
181
+ margin-bottom: 3rem;
182
+ }
183
+
184
+ .funnel-container {
185
+ display: grid;
186
+ grid-template-columns: repeat(auto-fit, minmax(400px, 1fr));
187
+ gap: 2rem;
188
+ }
189
+
190
+ .funnel-variant {
191
+ background: white;
192
+ border: 1px solid #ddd;
193
+ border-radius: 8px;
194
+ padding: 1.5rem;
195
+ }
196
+
197
+ .funnel-variant h3 {
198
+ font-size: 1.25rem;
199
+ font-weight: 600;
200
+ margin-bottom: 1rem;
201
+ }
202
+
203
+ .funnel-steps {
204
+ display: flex;
205
+ flex-direction: column;
206
+ gap: 0.5rem;
207
+ }
208
+
209
+ .funnel-step {
210
+ display: flex;
211
+ justify-content: space-between;
212
+ align-items: center;
213
+ padding: 1rem;
214
+ background: linear-gradient(to right, #007bff 0%, #007bff calc(var(--progress, 0) * 1%), #f8f9fa calc(var(--progress, 0) * 1%));
215
+ border-radius: 4px;
216
+ border: 1px solid #ddd;
217
+ }
218
+
219
+ .step-info {
220
+ display: flex;
221
+ align-items: center;
222
+ gap: 0.5rem;
223
+ }
224
+
225
+ .step-icon {
226
+ font-size: 1.25rem;
227
+ }
228
+
229
+ .step-name {
230
+ font-weight: 500;
231
+ }
232
+
233
+ .step-count {
234
+ font-weight: 600;
235
+ color: #007bff;
236
+ }
237
+
238
+ .winner-recommendation {
239
+ background: #fff3cd;
240
+ border: 2px solid #ffc107;
241
+ border-radius: 8px;
242
+ padding: 1.5rem;
243
+ }
244
+
245
+ .recommendation-card p {
246
+ margin: 0.5rem 0;
247
+ }
248
+
249
+ .warning {
250
+ color: #856404;
251
+ font-style: italic;
252
+ }
253
+ </style>
@@ -0,0 +1,24 @@
1
+ <div class="admin-ab-test-card">
2
+ <div class="admin-ab-test-header">
3
+ <h3 class="admin-ab-test-title"><%= test[:name] %></h3>
4
+ <span class="admin-badge admin-badge-<%= test[:enabled] ? 'active' : 'draft' %>">
5
+ <%= test[:enabled] ? 'Active' : 'Inactive' %>
6
+ </span>
7
+ </div>
8
+
9
+ <div class="admin-ab-test-meta">
10
+ <div class="admin-ab-test-meta-item">
11
+ <strong>Variants:</strong> <%= test[:variants].join(', ') %>
12
+ </div>
13
+ </div>
14
+
15
+ <div class="admin-ab-test-actions">
16
+ <%= link_to "View Results", admin_ab_test_path(test[:name]), class: "admin-btn admin-btn-sm admin-btn-secondary" %>
17
+
18
+ <% if test[:enabled] %>
19
+ <%= button_to "Stop", stop_admin_ab_test_path(test[:name]), method: :post, class: "admin-btn admin-btn-sm admin-btn-warning" %>
20
+ <% else %>
21
+ <%= button_to "Start", start_admin_ab_test_path(test[:name]), method: :post, class: "admin-btn admin-btn-sm admin-btn-success" %>
22
+ <% end %>
23
+ </div>
24
+ </div>
@@ -0,0 +1,57 @@
1
+ <% content_for :page_title, "A/B Tests" %>
2
+
3
+ <!-- Stats -->
4
+ <div class="admin-stats-grid admin-stats-compact">
5
+ <div class="admin-stat-card">
6
+ <div class="admin-stat-label">Active Tests</div>
7
+ <div class="admin-stat-value"><%= @active_tests.count %></div>
8
+ </div>
9
+ <div class="admin-stat-card">
10
+ <div class="admin-stat-label">Inactive Tests</div>
11
+ <div class="admin-stat-value"><%= @inactive_tests.count %></div>
12
+ </div>
13
+ <div class="admin-stat-card">
14
+ <div class="admin-stat-label">Total Tests</div>
15
+ <div class="admin-stat-value"><%= @ab_tests.count %></div>
16
+ </div>
17
+ </div>
18
+
19
+ <!-- Active Tests (those on the current page; the stats above count them all) -->
20
+ <% if @page_active_tests.any? %>
21
+ <div class="admin-section">
22
+ <h2 class="admin-section-title">Active Tests</h2>
23
+ <div class="admin-ab-tests-grid">
24
+ <% @page_active_tests.each do |test| %>
25
+ <%= render 'ab_test_card', test: test %>
26
+ <% end %>
27
+ </div>
28
+ </div>
29
+ <% end %>
30
+
31
+ <!-- Inactive Tests -->
32
+ <% if @page_inactive_tests.any? %>
33
+ <div class="admin-section">
34
+ <h2 class="admin-section-title">Inactive Tests</h2>
35
+ <div class="admin-ab-tests-grid">
36
+ <% @page_inactive_tests.each do |test| %>
37
+ <%= render 'ab_test_card', test: test %>
38
+ <% end %>
39
+ </div>
40
+ </div>
41
+ <% end %>
42
+
43
+ <!-- Pagination -->
44
+ <% if @pagy.count.positive? %>
45
+ <div class="admin-pagination">
46
+ <%= raw @pagy.info_tag(item_name: "tests") %>
47
+ <%= raw @pagy.series_nav if @pagy.pages > 1 %>
48
+ </div>
49
+ <% end %>
50
+
51
+ <% if @ab_tests.empty? %>
52
+ <div class="admin-empty-state-large">
53
+ <div class="admin-empty-icon">🧪</div>
54
+ <h3>No A/B Tests Configured</h3>
55
+ <p>Define A/B tests in your <code>config/initializers/rails_onboarding.rb</code> via <code>config.ab_tests</code> to manage them here.</p>
56
+ </div>
57
+ <% end %>
@@ -0,0 +1,41 @@
1
+ <% content_for :page_title, @test_name %>
2
+
3
+ <% content_for :header_actions do %>
4
+ <%= link_to "← Back to A/B Tests", admin_ab_tests_path, class: "admin-btn admin-btn-secondary" %>
5
+
6
+ <% if @test_config[:enabled] %>
7
+ <%= button_to "Stop", stop_admin_ab_test_path(@test_name), method: :post, class: "admin-btn admin-btn-warning" %>
8
+ <% else %>
9
+ <%= button_to "Start", start_admin_ab_test_path(@test_name), method: :post, class: "admin-btn admin-btn-success" %>
10
+ <% end %>
11
+
12
+ <%= link_to "Export CSV", export_admin_ab_test_path(@test_name, format: :csv), class: "admin-btn admin-btn-secondary" %>
13
+ <% end %>
14
+
15
+ <div class="admin-card">
16
+ <div class="admin-card-header">
17
+ <h3 class="admin-card-title">Results by Variant</h3>
18
+ </div>
19
+ <div class="admin-card-body">
20
+ <table class="admin-table">
21
+ <thead>
22
+ <tr>
23
+ <th>Variant</th>
24
+ <th>Participants</th>
25
+ <th>Completions</th>
26
+ <th>Conversion Rate</th>
27
+ </tr>
28
+ </thead>
29
+ <tbody>
30
+ <% @results.each do |variant, data| %>
31
+ <tr>
32
+ <td><%= variant %></td>
33
+ <td><%= data[:participants] %></td>
34
+ <td><%= data[:completions] %></td>
35
+ <td><%= data[:conversion_rate] %>%</td>
36
+ </tr>
37
+ <% end %>
38
+ </tbody>
39
+ </table>
40
+ </div>
41
+ </div>