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.
- checksums.yaml +7 -0
- data/MIT-LICENSE +20 -0
- data/README.md +1351 -0
- data/Rakefile +6 -0
- data/app/assets/javascripts/rails_onboarding/admin/chart_controller.js +25 -0
- data/app/assets/javascripts/rails_onboarding/admin/filter_controller.js +49 -0
- data/app/assets/javascripts/rails_onboarding/admin/flash_controller.js +21 -0
- data/app/assets/javascripts/rails_onboarding/admin/flow_editor_controller.js +86 -0
- data/app/assets/javascripts/rails_onboarding/application.js +231 -0
- data/app/assets/javascripts/rails_onboarding/milestone_celebration_controller.js +177 -0
- data/app/assets/javascripts/rails_onboarding/milestone_dashboard_controller.js +132 -0
- data/app/assets/javascripts/rails_onboarding/milestone_detail_controller.js +34 -0
- data/app/assets/javascripts/rails_onboarding/navigation_controller.js +277 -0
- data/app/assets/javascripts/rails_onboarding/onboarding_controller.js +326 -0
- data/app/assets/javascripts/rails_onboarding/progress_controller.js +203 -0
- data/app/assets/javascripts/rails_onboarding/progressive_disclosure_controller.js +218 -0
- data/app/assets/javascripts/rails_onboarding/tooltip_controller.js +1237 -0
- data/app/assets/javascripts/rails_onboarding/tooltip_scheduler_controller.js +511 -0
- data/app/assets/javascripts/rails_onboarding/tour_controller.js +763 -0
- data/app/assets/stylesheets/rails_onboarding/accessibility.css +536 -0
- data/app/assets/stylesheets/rails_onboarding/admin.css +816 -0
- data/app/assets/stylesheets/rails_onboarding/application.css +1145 -0
- data/app/assets/stylesheets/rails_onboarding/flash_messages.css +237 -0
- data/app/assets/stylesheets/rails_onboarding/milestones.css +545 -0
- data/app/assets/stylesheets/rails_onboarding/mobile.css +856 -0
- data/app/assets/stylesheets/rails_onboarding/progressive_disclosure.css +299 -0
- data/app/assets/stylesheets/rails_onboarding/tooltips.css +468 -0
- data/app/assets/stylesheets/rails_onboarding/tour.css +495 -0
- data/app/assets/stylesheets/rails_onboarding/utilities.css +393 -0
- data/app/controllers/concerns/rails_onboarding/admin_authorization.rb +98 -0
- data/app/controllers/concerns/rails_onboarding/rate_limitable.rb +104 -0
- data/app/controllers/rails_onboarding/ab_tests_controller.rb +257 -0
- data/app/controllers/rails_onboarding/admin/ab_tests_controller.rb +121 -0
- data/app/controllers/rails_onboarding/admin/base_controller.rb +68 -0
- data/app/controllers/rails_onboarding/admin/dashboard_controller.rb +189 -0
- data/app/controllers/rails_onboarding/admin/flows_controller.rb +175 -0
- data/app/controllers/rails_onboarding/admin/users_controller.rb +366 -0
- data/app/controllers/rails_onboarding/application_controller.rb +9 -0
- data/app/controllers/rails_onboarding/milestones_controller.rb +126 -0
- data/app/controllers/rails_onboarding/onboarding_controller.rb +409 -0
- data/app/controllers/rails_onboarding/progressive_features_controller.rb +134 -0
- data/app/controllers/rails_onboarding/templates_controller.rb +162 -0
- data/app/controllers/rails_onboarding/test_sessions_controller.rb +16 -0
- data/app/controllers/rails_onboarding/tooltips_controller.rb +92 -0
- data/app/helpers/rails_onboarding/application_helper.rb +4 -0
- data/app/helpers/rails_onboarding/i18n_helper.rb +137 -0
- data/app/helpers/rails_onboarding/personalization_helper.rb +184 -0
- data/app/helpers/rails_onboarding/templates_helper.rb +253 -0
- data/app/jobs/rails_onboarding/application_job.rb +4 -0
- data/app/mailers/rails_onboarding/application_mailer.rb +6 -0
- data/app/mailers/rails_onboarding/onboarding_mailer.rb +72 -0
- data/app/models/concerns/rails_onboarding/ab_testable.rb +149 -0
- data/app/models/concerns/rails_onboarding/onboardable.rb +649 -0
- data/app/models/concerns/rails_onboarding/personalizable.rb +211 -0
- data/app/models/concerns/rails_onboarding/progressive_disclosure.rb +276 -0
- data/app/models/rails_onboarding/analytics.rb +255 -0
- data/app/models/rails_onboarding/analytics_event.rb +168 -0
- data/app/models/rails_onboarding/application_record.rb +5 -0
- data/app/models/rails_onboarding/flow.rb +41 -0
- data/app/services/rails_onboarding/milestone_service.rb +105 -0
- data/app/views/layouts/rails_onboarding/admin.html.erb +106 -0
- data/app/views/layouts/rails_onboarding/application.html.erb +65 -0
- data/app/views/rails_onboarding/ab_tests/index.html.erb +209 -0
- data/app/views/rails_onboarding/ab_tests/results.html.erb +253 -0
- data/app/views/rails_onboarding/admin/ab_tests/_ab_test_card.html.erb +24 -0
- data/app/views/rails_onboarding/admin/ab_tests/index.html.erb +57 -0
- data/app/views/rails_onboarding/admin/ab_tests/show.html.erb +41 -0
- data/app/views/rails_onboarding/admin/dashboard/index.html.erb +224 -0
- data/app/views/rails_onboarding/admin/flows/_form.html.erb +147 -0
- data/app/views/rails_onboarding/admin/flows/edit.html.erb +7 -0
- data/app/views/rails_onboarding/admin/flows/index.html.erb +125 -0
- data/app/views/rails_onboarding/admin/flows/new.html.erb +7 -0
- data/app/views/rails_onboarding/admin/flows/preview.html.erb +40 -0
- data/app/views/rails_onboarding/admin/flows/show.html.erb +114 -0
- data/app/views/rails_onboarding/admin/users/index.html.erb +180 -0
- data/app/views/rails_onboarding/admin/users/show.html.erb +187 -0
- data/app/views/rails_onboarding/milestones/index.html.erb +64 -0
- data/app/views/rails_onboarding/milestones/show.html.erb +57 -0
- data/app/views/rails_onboarding/onboarding/_custom_footer.html.erb +2 -0
- data/app/views/rails_onboarding/onboarding/_progress_indicator.html.erb +56 -0
- data/app/views/rails_onboarding/onboarding/_step_navigation.html.erb +44 -0
- data/app/views/rails_onboarding/onboarding/step.html.erb +38 -0
- data/app/views/rails_onboarding/onboarding/welcome.html.erb +37 -0
- data/app/views/rails_onboarding/onboarding_mailer/completion_email.html.erb +109 -0
- data/app/views/rails_onboarding/onboarding_mailer/completion_email.text.erb +29 -0
- data/app/views/rails_onboarding/onboarding_mailer/reminder_email.html.erb +99 -0
- data/app/views/rails_onboarding/onboarding_mailer/reminder_email.text.erb +20 -0
- data/app/views/rails_onboarding/onboarding_mailer/step_completed_email.html.erb +105 -0
- data/app/views/rails_onboarding/onboarding_mailer/step_completed_email.text.erb +19 -0
- data/app/views/rails_onboarding/onboarding_mailer/welcome_email.html.erb +74 -0
- data/app/views/rails_onboarding/onboarding_mailer/welcome_email.text.erb +17 -0
- data/app/views/rails_onboarding/shared/_flash.html.erb +25 -0
- data/app/views/rails_onboarding/shared/_milestone_badge.html.erb +22 -0
- data/app/views/rails_onboarding/shared/_milestone_celebration.html.erb +44 -0
- data/app/views/rails_onboarding/shared/_onboarding_banner.html.erb +48 -0
- data/app/views/rails_onboarding/templates/index.html.erb +255 -0
- data/config/importmap.rb +33 -0
- data/config/locales/en.yml +62 -0
- data/config/locales/es.yml +62 -0
- data/config/locales/fr.yml +62 -0
- data/config/routes.rb +79 -0
- data/lib/generators/rails_onboarding/install_generator.rb +124 -0
- data/lib/generators/rails_onboarding/templates/README +120 -0
- data/lib/generators/rails_onboarding/templates/add_analytics_to_rails_onboarding.rb +33 -0
- data/lib/generators/rails_onboarding/templates/add_milestone_tracking_to_users.rb +38 -0
- data/lib/generators/rails_onboarding/templates/add_onboarding_indexes.rb +93 -0
- data/lib/generators/rails_onboarding/templates/add_onboarding_to_users.rb +51 -0
- data/lib/generators/rails_onboarding/templates/add_robustness_fields_to_users.rb.tt +20 -0
- data/lib/generators/rails_onboarding/templates/create_rails_onboarding_flows.rb +18 -0
- data/lib/generators/rails_onboarding/templates/onboarding.css +18 -0
- data/lib/generators/rails_onboarding/templates/rails_onboarding.rb +75 -0
- data/lib/generators/rails_onboarding/update_generator.rb +73 -0
- data/lib/rails_onboarding/api_mode.rb +367 -0
- data/lib/rails_onboarding/backfill.rb +130 -0
- data/lib/rails_onboarding/background_jobs.rb +412 -0
- data/lib/rails_onboarding/caching.rb +180 -0
- data/lib/rails_onboarding/cdn_support.rb +202 -0
- data/lib/rails_onboarding/configuration/ab_testing.rb +24 -0
- data/lib/rails_onboarding/configuration/analytics.rb +29 -0
- data/lib/rails_onboarding/configuration/integrations.rb +31 -0
- data/lib/rails_onboarding/configuration/milestones.rb +92 -0
- data/lib/rails_onboarding/configuration/personalization.rb +25 -0
- data/lib/rails_onboarding/configuration/progressive_disclosure.rb +41 -0
- data/lib/rails_onboarding/configuration/rate_limiting.rb +18 -0
- data/lib/rails_onboarding/configuration/steps.rb +121 -0
- data/lib/rails_onboarding/configuration/templates.rb +85 -0
- data/lib/rails_onboarding/configuration/tooltips.rb +32 -0
- data/lib/rails_onboarding/configuration.rb +133 -0
- data/lib/rails_onboarding/configuration_errors.rb +19 -0
- data/lib/rails_onboarding/configuration_validator.rb +477 -0
- data/lib/rails_onboarding/controller_helpers.rb +153 -0
- data/lib/rails_onboarding/current.rb +13 -0
- data/lib/rails_onboarding/deprecation.rb +64 -0
- data/lib/rails_onboarding/devise_integration.rb +122 -0
- data/lib/rails_onboarding/engine.rb +245 -0
- data/lib/rails_onboarding/error_recovery.rb +173 -0
- data/lib/rails_onboarding/lazy_loading.rb +155 -0
- data/lib/rails_onboarding/multi_tenant.rb +306 -0
- data/lib/rails_onboarding/requirements_validator.rb +315 -0
- data/lib/rails_onboarding/responsive_helper.rb +218 -0
- data/lib/rails_onboarding/session_manager.rb +187 -0
- data/lib/rails_onboarding/skip_logic.rb +159 -0
- data/lib/rails_onboarding/turbo_compatibility.rb +229 -0
- data/lib/rails_onboarding/version.rb +3 -0
- data/lib/rails_onboarding.rb +44 -0
- data/lib/tasks/rails_onboarding_analytics.rake +136 -0
- data/lib/tasks/rails_onboarding_tasks.rake +162 -0
- metadata +303 -0
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# Test-only controller for setting session in integration tests
|
|
2
|
+
module RailsOnboarding
|
|
3
|
+
class TestSessionsController < ApplicationController
|
|
4
|
+
skip_before_action :verify_authenticity_token, only: :create
|
|
5
|
+
|
|
6
|
+
def create
|
|
7
|
+
# Only allow in test environment
|
|
8
|
+
if Rails.env.test?
|
|
9
|
+
session[:user_id] = params[:user_id]
|
|
10
|
+
head :ok
|
|
11
|
+
else
|
|
12
|
+
head :forbidden
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
module RailsOnboarding
|
|
2
|
+
class TooltipsController < ApplicationController
|
|
3
|
+
include RailsOnboarding::RateLimitable
|
|
4
|
+
|
|
5
|
+
before_action :authenticate_user!
|
|
6
|
+
|
|
7
|
+
# POST /tooltips/dismiss
|
|
8
|
+
def dismiss
|
|
9
|
+
tooltip_id = params[:tooltip_id]
|
|
10
|
+
|
|
11
|
+
if tooltip_id.blank?
|
|
12
|
+
render json: { success: false, message: "tooltip_id is required" }, status: :bad_request
|
|
13
|
+
return
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# For dismiss, we mark as shown and track as dismissed
|
|
17
|
+
if current_user.respond_to?(:dismiss_tooltip)
|
|
18
|
+
current_user.dismiss_tooltip(tooltip_id)
|
|
19
|
+
render json: { success: true }
|
|
20
|
+
elsif current_user.respond_to?(:mark_tooltip_shown!)
|
|
21
|
+
# Fallback: manually mark and track
|
|
22
|
+
tooltips = current_user.feature_tooltips_shown || {}
|
|
23
|
+
tooltips[tooltip_id.to_s] = Time.current.iso8601
|
|
24
|
+
current_user.update!(feature_tooltips_shown: tooltips)
|
|
25
|
+
|
|
26
|
+
# Track dismissal event
|
|
27
|
+
if current_user.respond_to?(:track_tooltip_interaction!)
|
|
28
|
+
current_user.track_tooltip_interaction!(tooltip_id, "dismissed")
|
|
29
|
+
end
|
|
30
|
+
render json: { success: true }
|
|
31
|
+
else
|
|
32
|
+
render json: { success: false, message: "User does not support tooltips" }, status: :unprocessable_entity
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# POST /tooltips/show
|
|
37
|
+
def show
|
|
38
|
+
tooltip_id = params[:tooltip_id]
|
|
39
|
+
|
|
40
|
+
if tooltip_id.present? && current_user.respond_to?(:mark_tooltip_shown!)
|
|
41
|
+
current_user.mark_tooltip_shown!(tooltip_id)
|
|
42
|
+
render json: { success: true }
|
|
43
|
+
else
|
|
44
|
+
render json: { success: false, message: "Invalid tooltip_id or user" }, status: :unprocessable_entity
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# POST /tooltips/reset
|
|
49
|
+
def reset
|
|
50
|
+
if current_user.respond_to?(:reset_tooltips!)
|
|
51
|
+
current_user.reset_tooltips!
|
|
52
|
+
render json: { success: true }
|
|
53
|
+
else
|
|
54
|
+
render json: { success: false, message: "User does not support tooltips" }, status: :unprocessable_entity
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# GET /tooltips/status
|
|
59
|
+
def status
|
|
60
|
+
tooltip_id = params[:tooltip_id]
|
|
61
|
+
|
|
62
|
+
if tooltip_id.present? && current_user.respond_to?(:show_feature_tooltip?)
|
|
63
|
+
shown = !current_user.show_feature_tooltip?(tooltip_id)
|
|
64
|
+
render json: { shown: shown, tooltip_id: tooltip_id }
|
|
65
|
+
else
|
|
66
|
+
render json: { shown: false, message: "Invalid tooltip_id or user" }, status: :unprocessable_entity
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Legacy action - kept for backward compatibility
|
|
71
|
+
def mark_shown
|
|
72
|
+
feature = params[:feature]
|
|
73
|
+
|
|
74
|
+
if feature.present? && current_user.respond_to?(:mark_tooltip_shown!)
|
|
75
|
+
current_user.mark_tooltip_shown!(feature)
|
|
76
|
+
render json: { status: "success", feature: feature }
|
|
77
|
+
else
|
|
78
|
+
render json: { status: "error", message: "Invalid feature or user" }, status: :unprocessable_entity
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
private
|
|
83
|
+
|
|
84
|
+
def authenticate_user!
|
|
85
|
+
# This should be overridden by the host app
|
|
86
|
+
# or use the host app's authentication
|
|
87
|
+
unless defined?(current_user) && current_user
|
|
88
|
+
render json: { status: "error", message: "Authentication required" }, status: :unauthorized
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
module RailsOnboarding
|
|
2
|
+
# I18n Helper Module
|
|
3
|
+
# Provides convenient methods for internationalization
|
|
4
|
+
module I18nHelper
|
|
5
|
+
# Translate a key with the rails_onboarding scope
|
|
6
|
+
#
|
|
7
|
+
# @param key [String, Symbol] Translation key
|
|
8
|
+
# @param options [Hash] Translation options
|
|
9
|
+
# @return [String] Translated string
|
|
10
|
+
def t_onboarding(key, **options)
|
|
11
|
+
I18n.t("rails_onboarding.#{key}", **options)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# Translate navigation text
|
|
15
|
+
def t_nav(key, **options)
|
|
16
|
+
t_onboarding("navigation.#{key}", **options)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# Translate action text
|
|
20
|
+
def t_action(key, **options)
|
|
21
|
+
t_onboarding("actions.#{key}", **options)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Translate message text
|
|
25
|
+
def t_message(key, **options)
|
|
26
|
+
t_onboarding("messages.#{key}", **options)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Translate error text
|
|
30
|
+
def t_error(key, **options)
|
|
31
|
+
t_onboarding("errors.#{key}", **options)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Translate common text
|
|
35
|
+
def t_common(key, **options)
|
|
36
|
+
t_onboarding("common.#{key}", **options)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Translate progress text
|
|
40
|
+
def t_progress(key, **options)
|
|
41
|
+
t_onboarding("progress.#{key}", **options)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Translate milestone text
|
|
45
|
+
def t_milestone(key, **options)
|
|
46
|
+
t_onboarding("milestones.#{key}", **options)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Get localized step title
|
|
50
|
+
#
|
|
51
|
+
# @param step [Hash] Step configuration
|
|
52
|
+
# @return [String] Localized step title
|
|
53
|
+
def localized_step_title(step)
|
|
54
|
+
return step[:title] unless step[:title_key]
|
|
55
|
+
|
|
56
|
+
I18n.t(step[:title_key], default: step[:title])
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Get localized step description
|
|
60
|
+
#
|
|
61
|
+
# @param step [Hash] Step configuration
|
|
62
|
+
# @return [String] Localized step description
|
|
63
|
+
def localized_step_description(step)
|
|
64
|
+
return step[:description] unless step[:description_key]
|
|
65
|
+
|
|
66
|
+
I18n.t(step[:description_key], default: step[:description] || "")
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# Get localized milestone title
|
|
70
|
+
#
|
|
71
|
+
# @param milestone [Hash] Milestone configuration
|
|
72
|
+
# @return [String] Localized milestone title
|
|
73
|
+
def localized_milestone_title(milestone)
|
|
74
|
+
return milestone[:title] unless milestone[:title_key]
|
|
75
|
+
|
|
76
|
+
I18n.t(milestone[:title_key], default: milestone[:title])
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Get localized milestone description
|
|
80
|
+
#
|
|
81
|
+
# @param milestone [Hash] Milestone configuration
|
|
82
|
+
# @return [String] Localized milestone description
|
|
83
|
+
def localized_milestone_description(milestone)
|
|
84
|
+
return milestone[:description] unless milestone[:description_key]
|
|
85
|
+
|
|
86
|
+
I18n.t(milestone[:description_key], default: milestone[:description] || "")
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# Get available locales for onboarding
|
|
90
|
+
#
|
|
91
|
+
# @return [Array<Symbol>] Available locale codes
|
|
92
|
+
def onboarding_locales
|
|
93
|
+
I18n.available_locales.select do |locale|
|
|
94
|
+
I18n.exists?("rails_onboarding", locale: locale)
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# Check if a locale is available
|
|
99
|
+
#
|
|
100
|
+
# @param locale [Symbol, String] Locale code
|
|
101
|
+
# @return [Boolean] True if locale is available
|
|
102
|
+
def locale_available?(locale)
|
|
103
|
+
I18n.available_locales.include?(locale.to_sym) &&
|
|
104
|
+
I18n.exists?("rails_onboarding", locale: locale)
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# Get user's preferred locale
|
|
108
|
+
#
|
|
109
|
+
# @param user [User] User object
|
|
110
|
+
# @return [Symbol] Locale code
|
|
111
|
+
def user_locale(user)
|
|
112
|
+
return I18n.default_locale unless user
|
|
113
|
+
|
|
114
|
+
# Try to get locale from user
|
|
115
|
+
user_locale = if user.respond_to?(:locale)
|
|
116
|
+
user.locale
|
|
117
|
+
elsif user.respond_to?(:language)
|
|
118
|
+
user.language
|
|
119
|
+
elsif user.respond_to?(:preferred_language)
|
|
120
|
+
user.preferred_language
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
return I18n.default_locale unless user_locale
|
|
124
|
+
|
|
125
|
+
locale_sym = user_locale.to_sym
|
|
126
|
+
locale_available?(locale_sym) ? locale_sym : I18n.default_locale
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
# Execute block with user's locale
|
|
130
|
+
#
|
|
131
|
+
# @param user [User] User object
|
|
132
|
+
# @yield Block to execute with user's locale
|
|
133
|
+
def with_user_locale(user, &block)
|
|
134
|
+
I18n.with_locale(user_locale(user), &block)
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
end
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RailsOnboarding
|
|
4
|
+
# Helper methods for personalization features in views
|
|
5
|
+
module PersonalizationHelper
|
|
6
|
+
# Render content only for specific user types
|
|
7
|
+
#
|
|
8
|
+
# @param user_types [Array<Symbol>, Symbol] User type(s) to show content for
|
|
9
|
+
# @param user [Object] User object (defaults to current_user)
|
|
10
|
+
# @yield Block to render if user type matches
|
|
11
|
+
#
|
|
12
|
+
# @example
|
|
13
|
+
# <%= for_user_types(:business, :enterprise) do %>
|
|
14
|
+
# <p>This is only shown to business and enterprise users</p>
|
|
15
|
+
# <% end %>
|
|
16
|
+
def for_user_types(*user_types, user: nil)
|
|
17
|
+
user ||= current_user if respond_to?(:current_user)
|
|
18
|
+
return unless user
|
|
19
|
+
|
|
20
|
+
return unless user.respond_to?(:personalization_type)
|
|
21
|
+
|
|
22
|
+
user_type = user.personalization_type
|
|
23
|
+
return unless user_types.map(&:to_sym).include?(user_type.to_sym)
|
|
24
|
+
|
|
25
|
+
yield if block_given?
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Render different content based on user type
|
|
29
|
+
#
|
|
30
|
+
# @param user [Object] User object (defaults to current_user)
|
|
31
|
+
# @yield [user_type] Block that receives the user type
|
|
32
|
+
#
|
|
33
|
+
# @example
|
|
34
|
+
# <%= personalized_content do |type| %>
|
|
35
|
+
# <% if type == :business %>
|
|
36
|
+
# <p>Welcome, business user!</p>
|
|
37
|
+
# <% else %>
|
|
38
|
+
# <p>Welcome!</p>
|
|
39
|
+
# <% end %>
|
|
40
|
+
# <% end %>
|
|
41
|
+
def personalized_content(user: nil, &block)
|
|
42
|
+
user ||= current_user if respond_to?(:current_user)
|
|
43
|
+
return unless user
|
|
44
|
+
|
|
45
|
+
return unless user.respond_to?(:personalization_type)
|
|
46
|
+
|
|
47
|
+
user_type = user.personalization_type
|
|
48
|
+
yield(user_type) if block_given?
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Get a personalized message based on user type
|
|
52
|
+
#
|
|
53
|
+
# @param messages [Hash] Hash of user_type => message
|
|
54
|
+
# @param default [String] Default message if user type not found
|
|
55
|
+
# @param user [Object] User object (defaults to current_user)
|
|
56
|
+
# @return [String] The personalized message
|
|
57
|
+
#
|
|
58
|
+
# @example
|
|
59
|
+
# <%= personalized_message(
|
|
60
|
+
# { business: "Welcome to your business dashboard",
|
|
61
|
+
# individual: "Welcome to your personal dashboard" },
|
|
62
|
+
# default: "Welcome"
|
|
63
|
+
# ) %>
|
|
64
|
+
def personalized_message(messages, default: "Welcome", user: nil)
|
|
65
|
+
user ||= current_user if respond_to?(:current_user)
|
|
66
|
+
return default unless user
|
|
67
|
+
|
|
68
|
+
return default unless user.respond_to?(:personalization_type)
|
|
69
|
+
|
|
70
|
+
user_type = user.personalization_type
|
|
71
|
+
messages[user_type.to_sym] || default
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Get personalized steps for the current user
|
|
75
|
+
#
|
|
76
|
+
# @param user [Object] User object (defaults to current_user)
|
|
77
|
+
# @return [Array<Hash>] Array of step configurations
|
|
78
|
+
def personalized_steps(user: nil)
|
|
79
|
+
user ||= current_user if respond_to?(:current_user)
|
|
80
|
+
return RailsOnboarding.configuration.steps unless user
|
|
81
|
+
|
|
82
|
+
return RailsOnboarding.configuration.steps unless user.respond_to?(:personalized_steps)
|
|
83
|
+
|
|
84
|
+
user.personalized_steps
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# Render a personalized progress indicator
|
|
88
|
+
#
|
|
89
|
+
# @param user [Object] User object (defaults to current_user)
|
|
90
|
+
# @return [String] HTML for progress indicator
|
|
91
|
+
def personalized_progress_indicator(user: nil)
|
|
92
|
+
user ||= current_user if respond_to?(:current_user)
|
|
93
|
+
return "" unless user
|
|
94
|
+
|
|
95
|
+
return "" unless user.respond_to?(:personalized_progress_percentage)
|
|
96
|
+
|
|
97
|
+
percentage = user.personalized_progress_percentage
|
|
98
|
+
total_steps = user.respond_to?(:personalized_total_steps) ? user.personalized_total_steps : 0
|
|
99
|
+
|
|
100
|
+
content_tag :div, class: "personalized-progress" do
|
|
101
|
+
concat(content_tag(:div, class: "progress-bar") do
|
|
102
|
+
content_tag(:div, "", class: "progress-fill", style: "width: #{percentage}%")
|
|
103
|
+
end)
|
|
104
|
+
concat(content_tag(:div, "#{percentage}% complete (Step #{user.personalized_step_index(user.onboarding_current_step) + 1} of #{total_steps})", class: "progress-text"))
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# Check if a feature should be shown based on personalization
|
|
109
|
+
#
|
|
110
|
+
# @param feature_name [Symbol, String] Feature name
|
|
111
|
+
# @param user [Object] User object (defaults to current_user)
|
|
112
|
+
# @return [Boolean] True if feature should be shown
|
|
113
|
+
def show_personalized_feature?(feature_name, user: nil)
|
|
114
|
+
user ||= current_user if respond_to?(:current_user)
|
|
115
|
+
return true unless user
|
|
116
|
+
|
|
117
|
+
return true unless user.respond_to?(:show_personalized_feature?)
|
|
118
|
+
|
|
119
|
+
user.show_personalized_feature?(feature_name)
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# Get personalized CTA (Call To Action) text
|
|
123
|
+
#
|
|
124
|
+
# @param action [Symbol, String] Action type (:next, :complete, :skip, etc.)
|
|
125
|
+
# @param user [Object] User object (defaults to current_user)
|
|
126
|
+
# @return [String] Personalized CTA text
|
|
127
|
+
def personalized_cta(action, user: nil)
|
|
128
|
+
user ||= current_user if respond_to?(:current_user)
|
|
129
|
+
|
|
130
|
+
user_type = user&.respond_to?(:personalization_type) ? user.personalization_type : nil
|
|
131
|
+
|
|
132
|
+
cta_map = {
|
|
133
|
+
next: {
|
|
134
|
+
business: "Continue to Business Setup",
|
|
135
|
+
enterprise: "Proceed to Enterprise Configuration",
|
|
136
|
+
individual: "Next Step",
|
|
137
|
+
default: "Continue"
|
|
138
|
+
},
|
|
139
|
+
complete: {
|
|
140
|
+
business: "Launch Your Business Account",
|
|
141
|
+
enterprise: "Activate Enterprise Features",
|
|
142
|
+
individual: "Get Started",
|
|
143
|
+
default: "Complete Setup"
|
|
144
|
+
},
|
|
145
|
+
skip: {
|
|
146
|
+
business: "Skip for Now (You can complete this later in Settings)",
|
|
147
|
+
enterprise: "Configure Later",
|
|
148
|
+
individual: "Skip This Step",
|
|
149
|
+
default: "Skip"
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
action_map = cta_map[action.to_sym] || {}
|
|
154
|
+
action_map[user_type&.to_sym] || action_map[:default] || action.to_s.titleize
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
# Render personalized recommendations
|
|
158
|
+
#
|
|
159
|
+
# @param user [Object] User object (defaults to current_user)
|
|
160
|
+
# @return [String] HTML for recommendations
|
|
161
|
+
def personalized_recommendations(user: nil)
|
|
162
|
+
user ||= current_user if respond_to?(:current_user)
|
|
163
|
+
return "" unless user
|
|
164
|
+
|
|
165
|
+
return "" unless user.respond_to?(:personalized_recommendations)
|
|
166
|
+
|
|
167
|
+
recommendations = user.personalized_recommendations
|
|
168
|
+
return "" if recommendations.empty?
|
|
169
|
+
|
|
170
|
+
content_tag :div, class: "personalized-recommendations" do
|
|
171
|
+
concat(content_tag(:h3, "Recommended for You"))
|
|
172
|
+
concat(content_tag(:div, class: "recommendations-list") do
|
|
173
|
+
recommendations.each do |rec|
|
|
174
|
+
concat(content_tag(:div, class: "recommendation-card") do
|
|
175
|
+
concat(content_tag(:h4, rec[:title]))
|
|
176
|
+
concat(content_tag(:p, rec[:description]))
|
|
177
|
+
concat(link_to(rec[:cta] || "Get Started", rec[:url], class: "btn btn-primary")) if rec[:url]
|
|
178
|
+
end)
|
|
179
|
+
end
|
|
180
|
+
end)
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
end
|
|
184
|
+
end
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RailsOnboarding
|
|
4
|
+
# Helper methods for working with onboarding templates
|
|
5
|
+
module TemplatesHelper
|
|
6
|
+
# Get all available templates
|
|
7
|
+
#
|
|
8
|
+
# @return [Hash] Hash of template_key => template_config
|
|
9
|
+
def available_templates
|
|
10
|
+
RailsOnboarding.configuration.onboarding_templates || {}
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
# Get a specific template
|
|
14
|
+
#
|
|
15
|
+
# @param template_key [Symbol, String] Template identifier
|
|
16
|
+
# @return [Hash, nil] Template configuration or nil
|
|
17
|
+
def get_template(template_key)
|
|
18
|
+
RailsOnboarding.configuration.template(template_key)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Apply a template to configuration
|
|
22
|
+
#
|
|
23
|
+
# @param template_key [Symbol, String] Template identifier
|
|
24
|
+
# @return [Boolean] Success status
|
|
25
|
+
def apply_template(template_key)
|
|
26
|
+
RailsOnboarding.configuration.apply_template(template_key)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Get recommended template based on context
|
|
30
|
+
#
|
|
31
|
+
# @param context [Hash] Context information (industry, user_count, etc.)
|
|
32
|
+
# @return [Symbol, nil] Recommended template key or nil
|
|
33
|
+
#
|
|
34
|
+
# @example
|
|
35
|
+
# recommended_template(industry: :ecommerce, user_count: 1)
|
|
36
|
+
# # => :ecommerce
|
|
37
|
+
def recommended_template(context = {})
|
|
38
|
+
templates = available_templates
|
|
39
|
+
|
|
40
|
+
# Recommend based on industry if provided
|
|
41
|
+
if context[:industry]
|
|
42
|
+
return context[:industry].to_sym if templates[context[:industry].to_sym]
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Recommend based on user type
|
|
46
|
+
if context[:user_type]
|
|
47
|
+
case context[:user_type].to_sym
|
|
48
|
+
when :student, :learner
|
|
49
|
+
return :education if templates[:education]
|
|
50
|
+
when :seller, :merchant
|
|
51
|
+
return :marketplace if templates[:marketplace]
|
|
52
|
+
when :business, :company
|
|
53
|
+
return :saas if templates[:saas]
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Recommend based on team size
|
|
58
|
+
if context[:team_size]
|
|
59
|
+
if context[:team_size] > 10
|
|
60
|
+
return :saas if templates[:saas]
|
|
61
|
+
elsif context[:team_size] == 1
|
|
62
|
+
return :community if templates[:community]
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Default to first available template
|
|
67
|
+
templates.keys.first
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Render template selection UI
|
|
71
|
+
#
|
|
72
|
+
# @param options [Hash] Display options
|
|
73
|
+
# @return [String] HTML for template selector
|
|
74
|
+
def template_selector(options = {})
|
|
75
|
+
templates = available_templates
|
|
76
|
+
current_template = options[:current] || detect_current_template
|
|
77
|
+
|
|
78
|
+
content_tag :div, class: "template-selector" do
|
|
79
|
+
concat(content_tag(:h3, options[:title] || "Choose a Template"))
|
|
80
|
+
|
|
81
|
+
concat(content_tag(:div, class: "template-options") do
|
|
82
|
+
templates.each do |key, template|
|
|
83
|
+
concat(template_option(key, template, key == current_template))
|
|
84
|
+
end
|
|
85
|
+
end)
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# Render a single template option
|
|
90
|
+
#
|
|
91
|
+
# @param key [Symbol] Template key
|
|
92
|
+
# @param template [Hash] Template configuration
|
|
93
|
+
# @param selected [Boolean] Whether this template is selected
|
|
94
|
+
# @return [String] HTML for template option
|
|
95
|
+
def template_option(key, template, selected = false)
|
|
96
|
+
content_tag :div, class: "template-option #{selected ? 'selected' : ''}", data: { template: key } do
|
|
97
|
+
concat(content_tag(:div, class: "template-icon") do
|
|
98
|
+
template[:icon] || "📋"
|
|
99
|
+
end)
|
|
100
|
+
concat(content_tag(:div, class: "template-info") do
|
|
101
|
+
concat(content_tag(:h4, template[:name]))
|
|
102
|
+
concat(content_tag(:p, template[:description] || "#{key.to_s.titleize} onboarding"))
|
|
103
|
+
concat(content_tag(:span, "#{template[:steps]&.size || 0} steps", class: "step-count"))
|
|
104
|
+
end)
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# Detect which template matches current configuration
|
|
109
|
+
#
|
|
110
|
+
# @return [Symbol, nil] Matched template key or nil
|
|
111
|
+
def detect_current_template
|
|
112
|
+
current_steps = RailsOnboarding.configuration.steps
|
|
113
|
+
return nil if current_steps.empty?
|
|
114
|
+
|
|
115
|
+
available_templates.find do |key, template|
|
|
116
|
+
template[:steps] == current_steps
|
|
117
|
+
end&.first
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# Compare templates
|
|
121
|
+
#
|
|
122
|
+
# @param template_keys [Array<Symbol>] Template keys to compare
|
|
123
|
+
# @return [Hash] Comparison data
|
|
124
|
+
def compare_templates(*template_keys)
|
|
125
|
+
comparison = {}
|
|
126
|
+
|
|
127
|
+
template_keys.each do |key|
|
|
128
|
+
template = get_template(key)
|
|
129
|
+
next unless template
|
|
130
|
+
|
|
131
|
+
comparison[key] = {
|
|
132
|
+
name: template[:name],
|
|
133
|
+
total_steps: template[:steps]&.size || 0,
|
|
134
|
+
required_steps: template[:steps]&.count { |s| !s[:skippable] } || 0,
|
|
135
|
+
optional_steps: template[:steps]&.count { |s| s[:skippable] } || 0,
|
|
136
|
+
steps: template[:steps]
|
|
137
|
+
}
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
comparison
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
# Render template comparison table
|
|
144
|
+
#
|
|
145
|
+
# @param template_keys [Array<Symbol>] Templates to compare
|
|
146
|
+
# @return [String] HTML table comparing templates
|
|
147
|
+
def render_template_comparison(*template_keys)
|
|
148
|
+
comparison = compare_templates(*template_keys)
|
|
149
|
+
return "" if comparison.empty?
|
|
150
|
+
|
|
151
|
+
content_tag :table, class: "template-comparison" do
|
|
152
|
+
concat(content_tag(:thead) do
|
|
153
|
+
content_tag(:tr) do
|
|
154
|
+
concat(content_tag(:th, "Feature"))
|
|
155
|
+
comparison.each_key do |key|
|
|
156
|
+
concat(content_tag(:th, comparison[key][:name]))
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
end)
|
|
160
|
+
|
|
161
|
+
concat(content_tag(:tbody) do
|
|
162
|
+
# Total steps row
|
|
163
|
+
concat(content_tag(:tr) do
|
|
164
|
+
concat(content_tag(:td, "Total Steps"))
|
|
165
|
+
comparison.each_value do |data|
|
|
166
|
+
concat(content_tag(:td, data[:total_steps]))
|
|
167
|
+
end
|
|
168
|
+
end)
|
|
169
|
+
|
|
170
|
+
# Required steps row
|
|
171
|
+
concat(content_tag(:tr) do
|
|
172
|
+
concat(content_tag(:td, "Required Steps"))
|
|
173
|
+
comparison.each_value do |data|
|
|
174
|
+
concat(content_tag(:td, data[:required_steps]))
|
|
175
|
+
end
|
|
176
|
+
end)
|
|
177
|
+
|
|
178
|
+
# Optional steps row
|
|
179
|
+
concat(content_tag(:tr) do
|
|
180
|
+
concat(content_tag(:td, "Optional Steps"))
|
|
181
|
+
comparison.each_value do |data|
|
|
182
|
+
concat(content_tag(:td, data[:optional_steps]))
|
|
183
|
+
end
|
|
184
|
+
end)
|
|
185
|
+
end)
|
|
186
|
+
end
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
# Get template metadata
|
|
190
|
+
#
|
|
191
|
+
# @param template_key [Symbol, String] Template identifier
|
|
192
|
+
# @return [Hash] Metadata about the template
|
|
193
|
+
def template_metadata(template_key)
|
|
194
|
+
template = get_template(template_key)
|
|
195
|
+
return {} unless template
|
|
196
|
+
|
|
197
|
+
{
|
|
198
|
+
name: template[:name],
|
|
199
|
+
total_steps: template[:steps]&.size || 0,
|
|
200
|
+
estimated_time: estimate_template_time(template),
|
|
201
|
+
difficulty: template_difficulty(template),
|
|
202
|
+
categories: template_categories(template)
|
|
203
|
+
}
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
# Estimate completion time for a template
|
|
207
|
+
#
|
|
208
|
+
# @param template [Hash] Template configuration
|
|
209
|
+
# @return [String] Estimated time (e.g., "5-10 minutes")
|
|
210
|
+
def estimate_template_time(template)
|
|
211
|
+
steps_count = template[:steps]&.size || 0
|
|
212
|
+
min_time = steps_count * 1 # 1 minute per step minimum
|
|
213
|
+
max_time = steps_count * 3 # 3 minutes per step maximum
|
|
214
|
+
|
|
215
|
+
"#{min_time}-#{max_time} minutes"
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
# Determine template difficulty
|
|
219
|
+
#
|
|
220
|
+
# @param template [Hash] Template configuration
|
|
221
|
+
# @return [Symbol] :easy, :medium, or :hard
|
|
222
|
+
def template_difficulty(template)
|
|
223
|
+
total_steps = template[:steps]&.size || 0
|
|
224
|
+
required_steps = template[:steps]&.count { |s| !s[:skippable] } || 0
|
|
225
|
+
|
|
226
|
+
if total_steps <= 3 && required_steps <= 2
|
|
227
|
+
:easy
|
|
228
|
+
elsif total_steps <= 5 && required_steps <= 3
|
|
229
|
+
:medium
|
|
230
|
+
else
|
|
231
|
+
:hard
|
|
232
|
+
end
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
# Get categories/tags for a template
|
|
236
|
+
#
|
|
237
|
+
# @param template [Hash] Template configuration
|
|
238
|
+
# @return [Array<Symbol>] Categories
|
|
239
|
+
def template_categories(template)
|
|
240
|
+
categories = []
|
|
241
|
+
|
|
242
|
+
step_names = template[:steps]&.map { |s| s[:name].to_s } || []
|
|
243
|
+
|
|
244
|
+
categories << :profile if step_names.any? { |n| n.include?("profile") }
|
|
245
|
+
categories << :team if step_names.any? { |n| n.include?("team") || n.include?("invite") }
|
|
246
|
+
categories << :payment if step_names.any? { |n| n.include?("payment") || n.include?("billing") }
|
|
247
|
+
categories << :content if step_names.any? { |n| n.include?("post") || n.include?("product") }
|
|
248
|
+
categories << :social if step_names.any? { |n| n.include?("connect") || n.include?("friends") }
|
|
249
|
+
|
|
250
|
+
categories
|
|
251
|
+
end
|
|
252
|
+
end
|
|
253
|
+
end
|